@turri-dev/cli 0.0.0-stage → 0.1.1

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 +319 -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 +75 -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 +118 -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,936 @@
1
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
2
+ import { basename, extname, join, relative, resolve } from "node:path";
3
+ import { ask, BEGIN, createExecutor, refuseIfModelIsBlind } from "../pi.js";
4
+ import { PLAIN_SOURCE_FILE_EXTENSION, plainFileParser } from "../plain/parser.js";
5
+ import { ACCEPTANCE_TESTS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, getFrids, getInheritedFunctionalSpecs, getSpecificationsForFrid, TEST_REQUIREMENTS, } from "../plain/spec.js";
6
+ import { reportActivity, reportLogNote, reportSpec, reportStage, unitOf } from "./activity.js";
7
+ import { confinedTools } from "./confine.js";
8
+ import { abandonWork, diffSinceSnapshot, moduleDirOf, recordHistory, startWork, workDiff, workSnapshot } from "./lineage.js";
9
+ import { createWorkLedger, describeAccounts, SUBMIT_WORK_NOTE, unanswered, unansweredPrompt } from "./submit-work.js";
10
+ import { CONFORMANCE_COMPACTION } from "./compaction.js";
11
+ import { NOTES_TURN, notesForSession, passedOutcome, readNotes, reflectOnReview, withNotes } from "./notes.js";
12
+ import { labelsPrompt, labelsPromptFor, misplacedConformanceTests, misplacedTestsPrompt, missingTestLabels } from "./test-kinds.js";
13
+ import { createScriptTool, findTestScripts, projectRootFor, requireTestScripts, runConformanceScript, runUnitScript, failureSignature, STUCK_ROUNDS, scriptFailure, staticChecksOnly, withTestingScripts, } from "./test-scripts.js";
14
+ import { createWebTools, WEB_TOOLS } from "./web.js";
15
+ import { createMyDiffTool } from "./self-check.js";
16
+ import { createServiceTools } from "./services.js";
17
+ import { missingToolchainError, toolchainError } from "./environment-error.js";
18
+ import { workspaceBlock } from "./workspace.js";
19
+ import { codeFixPrompt, conflictKey, createCodeFixer, answerAsExplorer, createAskExplorerTool, exploreInvocation, judgeFailure, SPEC_CONFLICT_RAISES, ENVIRONMENT_RAISES, environmentCheckPrompt, createTestReviewer, testReviewRequest, reviewFeedbackPrompt, testsFixPrompt, sortedRefs, } from "./conformance-agents.js";
20
+ import { resolveRequiresChain } from "./requires.js";
21
+ import { notesToRead, recordNote } from "./notes.js";
22
+ import { reportSpecConflict } from "./activity.js";
23
+ import { ATTEMPTS } from "./stop-report.js";
24
+ import { ARBITER_AFTER, arbitrateReview, dismissedNote, REVIEW_LIMIT } from "./arbiter.js";
25
+ import { sectionsOf } from "./state.js";
26
+ import { configForSpec, inheritModel, modelOnly, modelSpec } from "../config.js";
27
+ import { defaultBuildDir, modulesRoot, testsDirUnder, projectDirOf } from "./paths.js";
28
+ import { buildConformancePrompt, functionalitiesByModule, moduleSection, section, sectionModules, specChangeBlock } from "./prompt.js";
29
+ import { diffFiles, digestFiles } from "./codebase.js";
30
+ import { createCopyResourceTool, loadLinkedResources, resourceImages } from "./resources.js";
31
+ import { resolveTemplateDirs } from "./paths.js";
32
+ import { findTestRemovals, inventoryTests, listFiles, removalChallengePrompt, shownOutput, truncateOutput, verifyUnitSuite, } from "./testing.js";
33
+ const DEFAULT_MAX_ROUNDS = ATTEMPTS.conformance;
34
+ /**
35
+ * A module's conformance tests: one directory, `plain_modules/<module>/tests`, organised
36
+ * however the tests are best organised. It holds the tests of the modules it requires too,
37
+ * copied up with their code, so running it is the regression suite of the whole chain.
38
+ */
39
+ export function conformanceDirFor(moduleName, root) {
40
+ return testsDirUnder(root ?? modulesRoot(), moduleName);
41
+ }
42
+ /**
43
+ * Write black-box conformance tests for one functionality of a .plain specification.
44
+ *
45
+ * Runs after the functionality has been rendered: the build directory is the subject,
46
+ * and this refuses to start without one.
47
+ */
48
+ export async function renderConformanceTests(options) {
49
+ if (extname(options.plainFile) !== PLAIN_SOURCE_FILE_EXTENSION) {
50
+ throw new Error(`Expected a ${PLAIN_SOURCE_FILE_EXTENSION} file, got '${options.plainFile}'.`);
51
+ }
52
+ const templateDirs = resolveTemplateDirs(options.plainFile, options.templateDirs);
53
+ const [moduleName, tree] = options.spec
54
+ ? [options.spec.moduleName, options.spec.tree]
55
+ : plainFileParser(basename(options.plainFile), templateDirs);
56
+ // For a spec change, the tests follow the change, whatever it touches - a definition, a req -
57
+ // with or without a functional spec yet; for one functionality, they are that functionality's.
58
+ const forChange = options.diff !== undefined;
59
+ const frids = [...getFrids(tree)];
60
+ const frid = options.frid ?? (forChange ? frids.at(-1) : frids[0]);
61
+ if (frid === undefined && !forChange)
62
+ throw new Error(`Module '${moduleName}' has no functionalities.`);
63
+ if (frid !== undefined && !frids.includes(frid)) {
64
+ throw new Error(`Functionality ${frid} does not exist. Available: ${frids.join(", ")}.`);
65
+ }
66
+ const buildDir = resolve(options.buildDir ?? defaultBuildDir(moduleName, projectDirOf(options.plainFile)));
67
+ // The agents that give a verdict - the judge, the test review - may run on a stronger model than the rest.
68
+ const judgeModel = options.judgeModel ?? modelSpec(inheritModel(configForSpec(options.plainFile).judgeModel, modelOnly(options.model)));
69
+ const conformanceDir = resolve(options.conformanceDir ?? conformanceDirFor(moduleName, modulesRoot(projectDirOf(options.plainFile))));
70
+ const stage = (label) => {
71
+ if (!options.dryRun)
72
+ reportStage(`${forChange ? unitOf(moduleName) : unitOf(moduleName, frid)}: ${label}`);
73
+ };
74
+ stage(options.diff !== undefined ? "conformance tests for the spec change" : `conformance tests for functionality ${frid}`);
75
+ if (!options.dryRun && !forChange)
76
+ reportSpec(buildingSpec(moduleName, frid, frids.length, getSpecificationsForFrid(tree, frid)[0]));
77
+ // From the last known good state, unless this step continues work not yet committed.
78
+ // On its own it is one unit - its tests, and any change to the code - landed whole or undone
79
+ // whole. Continuing work already under way, it is part of that work's unit instead.
80
+ let start;
81
+ if (!options.dryRun && options.continuing !== true) {
82
+ stage("starting from the codebase's last commit");
83
+ start = startWork([buildDir, conformanceDir], moduleName);
84
+ }
85
+ const manifest = listFiles(buildDir);
86
+ if (manifest.length === 0) {
87
+ throw new Error(`Nothing to test: ${buildDir} is empty or missing. ` +
88
+ `Build the code first: turri regenerate ${relative(process.cwd(), options.plainFile)}.`);
89
+ }
90
+ // The spec as it stands: every section, and the functional specs - none, for a change made
91
+ // before any was written.
92
+ const specifications = frid !== undefined
93
+ ? getSpecificationsForFrid(tree, frid)[0]
94
+ : { ...sectionsOf(tree), [FUNCTIONAL_REQUIREMENTS]: [], [ACCEPTANCE_TESTS]: [] };
95
+ // Conformance tests are written from the test reqs: they say how the suite is built and
96
+ // run. Without them there is nothing to write tests against, and an agent asked anyway
97
+ // would invent a convention that nothing else in the project shares.
98
+ if (specifications[TEST_REQUIREMENTS].length === 0) {
99
+ throw new Error(`Module '${moduleName}' has no ${TEST_REQUIREMENTS}, so conformance tests cannot be written. ` +
100
+ `Add a ${TEST_REQUIREMENTS} section saying how conformance tests are written and run, or render ` +
101
+ "with conformance turned off.");
102
+ }
103
+ // Whether the tests pass is the project's conformance testing script - and, when the code is
104
+ // changed, its unit testing script: without them, nothing is written.
105
+ stage("finding the project's testing scripts");
106
+ const root = projectRootFor(options.plainFile, buildDir);
107
+ const scripts = findTestScripts(root, options.plainFile, buildDir);
108
+ if (!options.dryRun)
109
+ requireTestScripts(moduleName, scripts, { conformance: true });
110
+ const linkedResources = loadLinkedResources(tree, forChange ? null : frid, templateDirs);
111
+ // The spec's images need a model that reads them: refused before any of its agents starts.
112
+ if (!options.dryRun)
113
+ await refuseIfModelIsBlind(options.model, resourceImages(linkedResources).length);
114
+ const required = getInheritedFunctionalSpecs(tree);
115
+ const prompt = withTestingScripts(buildConformancePrompt({
116
+ ...(options.cases !== undefined ? { cases: options.cases } : {}),
117
+ moduleName,
118
+ frid,
119
+ specifications,
120
+ modules: sectionModules(tree, moduleName),
121
+ linkedResources,
122
+ manifest,
123
+ codeDir: buildDir,
124
+ existingTests: listFiles(conformanceDir),
125
+ required,
126
+ diff: options.change ?? options.diff,
127
+ }), scripts, ["conformance", "unit"], root);
128
+ if (options.dryRun) {
129
+ return {
130
+ moduleName,
131
+ frid: frid ?? "",
132
+ buildDir,
133
+ conformanceDir,
134
+ prompt,
135
+ response: "",
136
+ files: listFiles(conformanceDir),
137
+ runs: [],
138
+ passed: false,
139
+ codeChanged: false,
140
+ testRemovals: [],
141
+ };
142
+ }
143
+ mkdirSync(conformanceDir, { recursive: true });
144
+ reportLogNote(` conformance testing script: ${relative(root, scripts.conformance)}`);
145
+ reportActivity(` the suite so far: ${listFiles(conformanceDir).length} files in ${relative(root, conformanceDir)}`);
146
+ const runs = [];
147
+ const unitRuns = [];
148
+ const testRemovals = [];
149
+ const codeBefore = digestFiles(buildDir);
150
+ // The code and the tests at the last green run: the whole suite and the unit tests passing on them.
151
+ let green;
152
+ const codeChanged = () => {
153
+ const diff = diffFiles(codeBefore, digestFiles(buildDir));
154
+ return diff.added.length + diff.changed.length + diff.removed.length > 0;
155
+ };
156
+ // The code and its tests are rendered apart: the tests in their own folder, by an agent that
157
+ // may read the code but writes only tests. An explorer reads the code first and writes down how the
158
+ // software is invoked, which is what the tests are written against.
159
+ const label = `${moduleName} ${options.diff !== undefined ? "spec change" : frid}`;
160
+ const specContext = { moduleName, text: specText(specifications, moduleName, frids, required, options.change ?? options.diff, sectionModules(tree, moduleName)) };
161
+ // For a spec change: the functionalities it touches, whose acceptance tests are labelled, checked and reviewed.
162
+ const touched = forChange ? touchedFunctionalities(tree, frids, options.diff) : [];
163
+ // Any functionality of the codebase - this module's, and those of the modules it requires. A
164
+ // functionality's run may raise a spec conflict only with that functionality; a spec diff's, between any two.
165
+ const own = (forChange ? frids : frids.slice(0, frids.indexOf(frid) + 1)).map((id) => ({ module: moduleName, frid: id }));
166
+ const scope = {
167
+ functionalities: [...required.map((spec) => ({ module: spec.module, frid: spec.frid })), ...own],
168
+ ...(forChange ? {} : { tested: { module: moduleName, frid: frid } }),
169
+ };
170
+ /** How many times each spec conflict has been raised in this run, by the two functionalities it names. */
171
+ const conflicts = new Map();
172
+ /**
173
+ * How many times each failure has come back in this run - in a row or not: a run that swings
174
+ * between two failures, each side undoing the other, is as stuck as one that repeats one.
175
+ */
176
+ const seenFailures = new Map();
177
+ let reExplored = false;
178
+ let stuck;
179
+ /** What the review last found, while it has not passed. */
180
+ let lastReview;
181
+ /** The run's test reviewer, made at its first review and kept; what it saw last; what the arbiter let go of its last review. */
182
+ let reviewer;
183
+ let reviewSeen;
184
+ let reviewLetGo = [];
185
+ let failedReviews = 0;
186
+ let reviewWaived;
187
+ /** What each review that sent the tests back found, oldest first: what the arbiter weighs the next one against. */
188
+ // The config's `review:` - off unless it says true: the tests are not reviewed.
189
+ const reviewOn = configForSpec(options.plainFile).review === true;
190
+ const reviewHistory = [];
191
+ let specConflict;
192
+ let environment;
193
+ /** How many times in a row the run's environment was judged what is wrong. */
194
+ let environmentRaises = 0;
195
+ /** How many fixes in a row - the code's or the tests' - changed no file: an agent that will not, or cannot, act on the verdict. */
196
+ let idleFixes = 0;
197
+ /**
198
+ * One attempt at the failing run, written into the notes' pitfalls - what the next judge reads
199
+ * first - as cause and effect: what failed, how it was judged, and what was changed; the next
200
+ * attempt's line shows what came of it. Numbered in the line itself: two attempts that went the
201
+ * same way are two lines, not one.
202
+ */
203
+ const recordAttempt = (attempt, run, judged, change) => {
204
+ const files = change.files.length === 0 ? "nothing" : `${change.files.slice(0, 6).join(", ")}${change.files.length > 6 ? ` and ${change.files.length - 6} more` : ""}`;
205
+ recordNote(conformanceDir, "pitfalls", `attempt ${attempt}: ${typeof run === "string" ? run : `failed with ${failureLine(run)}`} → judged ${judged.replace(/\s+/g, " ").slice(0, 240)} → the ${change.who} changed ${files}`, `${label} conformance attempt ${attempt}`);
206
+ };
207
+ /** Run a fix, and say which files of the code and the tests it changed. */
208
+ const changing = async (work) => {
209
+ const code = digestFiles(buildDir);
210
+ const tests = digestFiles(conformanceDir);
211
+ const result = await work();
212
+ const named = (prefix, diff) => [
213
+ ...diff.added.map((file) => `${prefix}/${file} (added)`),
214
+ ...diff.changed.map((file) => `${prefix}/${file}`),
215
+ ...diff.removed.map((file) => `${prefix}/${file} (removed)`),
216
+ ];
217
+ return { result, files: [...named("code", diffFiles(code, digestFiles(buildDir))), ...named("tests", diffFiles(tests, digestFiles(conformanceDir)))] };
218
+ };
219
+ stage(`exploring the code for how it is invoked${options.model ? ` (${options.model})` : ""}`);
220
+ // The explorer reads the conformance testing script too: it is how the tests are built and run, and so how they reach the software.
221
+ const testingScripts = [
222
+ { path: relative(root, scripts.conformance), runs: "<script> <copy of the build> <copy of the whole conformance suite>", content: readFileSync(scripts.conformance, "utf8") },
223
+ ];
224
+ // The acceptance tests the explorer's plan has to carry out, step by step.
225
+ const toCarryOut = (forChange ? touched : [{ frid: frid, acceptanceTests: specifications[ACCEPTANCE_TESTS] ?? [] }])
226
+ .map((item) => ({ module: moduleName, frid: item.frid, count: item.acceptanceTests.length }))
227
+ .filter((item) => item.count > 0);
228
+ let invocation = await exploreInvocation({ buildDir, spec: specContext, scope, testingScripts, acceptance: toCarryOut, label, model: options.model, onDelta: options.onDelta });
229
+ reportActivity(` how to invoke it: ${invocation.split("\n")[0].slice(0, 120)}`);
230
+ const agentPrompt = withTestingScripts(buildConformancePrompt({
231
+ ...(options.cases !== undefined ? { cases: options.cases } : {}),
232
+ moduleName,
233
+ frid,
234
+ specifications,
235
+ modules: sectionModules(tree, moduleName),
236
+ linkedResources,
237
+ manifest,
238
+ existingTests: listFiles(conformanceDir),
239
+ required,
240
+ diff: options.change ?? options.diff,
241
+ invocation,
242
+ readCode: buildDir,
243
+ manifests: buildManifests(buildDir),
244
+ touched,
245
+ }), scripts, ["conformance"], root);
246
+ // The conformance agent: its file tools write the tests folder and read the code too - how it is reached, what it is
247
+ // called - and its shell holds only the tests. The full outputs of test runs come through read_output, not by path.
248
+ const fence = confinedTools({ workdir: conformanceDir, read: [conformanceDir, buildDir], write: [conformanceDir], reportsEnvironment: true });
249
+ // Its notes are the tests' own, beside them: what it learns of black-box testing this module is no coding agent's to read.
250
+ const notes = notesForSession(conformanceDir, `${label} conformance`);
251
+ // Each agent runs the suite three times a turn at most: the third ends its turn, and the harness runs it and judges.
252
+ const conformanceRuns = turnLimited(createScriptTool({
253
+ kind: "conformance",
254
+ script: scripts.conformance,
255
+ run: () => runConformanceScript(buildDir, [conformanceDir], scripts),
256
+ runs,
257
+ onRun: options.onRun,
258
+ }), RUNS_PER_TURN);
259
+ const fixerConformanceRuns = turnLimited(createScriptTool({ kind: "conformance", script: scripts.conformance, run: () => runConformanceScript(buildDir, [conformanceDir], scripts), runs: [] }), RUNS_PER_TURN);
260
+ // What the code fixer starts to try the code - a server - stopped when the run ends.
261
+ const fixerServices = createServiceTools({ dir: buildDir, read: [buildDir, conformanceDir], write: [buildDir] });
262
+ // What it hands over at the end of a turn, for its reviewer.
263
+ const work = createWorkLedger();
264
+ stage(`starting the conformance agent${options.model ? ` (${options.model})` : ""}`);
265
+ // One session for the whole run: its task is its instructions, its work compacted as it gets longer.
266
+ const executor = await createExecutor({
267
+ cwd: conformanceDir,
268
+ customTools: [
269
+ notes.tool,
270
+ work.tool,
271
+ ...createWebTools(),
272
+ ...fence.tools,
273
+ conformanceRuns.tool,
274
+ createMyDiffTool({ dir: conformanceDir, what: "tests" }),
275
+ createAskExplorerTool({
276
+ ask: (question) => {
277
+ stage("the conformance agent asks the explorer");
278
+ return answerAsExplorer({ buildDir, spec: specContext, account: invocation, question, label, model: options.model, onDelta: options.onDelta });
279
+ },
280
+ }),
281
+ createCopyResourceTool({ resources: linkedResources, dir: conformanceDir }),
282
+ ],
283
+ tools: [...fence.names, "run_conformance_tests", "my_diff", "ask_explorer", "update_notes", "submit_work", "copy_resource", ...WEB_TOOLS],
284
+ model: options.model,
285
+ label: `${label} conformance agent`,
286
+ role: "conformance",
287
+ system: `${fence.note}\n\n${workspaceBlock({ dir: conformanceDir, hasMyDiff: true })}`,
288
+ task: [withNotes(agentPrompt, conformanceDir), "", ...SUBMIT_WORK_NOTE].join("\n"),
289
+ compaction: { instructions: CONFORMANCE_COMPACTION },
290
+ checkEnvironment: { run: () => runConformanceScript(buildDir, [conformanceDir], scripts), script: scripts.conformance },
291
+ images: resourceImages(linkedResources),
292
+ });
293
+ /** A turn of the conformance agent: a fresh count of its runs of the suite. */
294
+ const askConformance = (text, onDelta, turn) => {
295
+ conformanceRuns.newTurn();
296
+ return ask(executor, text, onDelta, turn);
297
+ };
298
+ // The coding agent, made the first time the code is what is wrong.
299
+ let fixer;
300
+ const fixerNotes = notesForSession(buildDir, `${label} conformance code fix`);
301
+ const askFixer = async (text) => {
302
+ fixer ??= await createCodeFixer({
303
+ buildDir,
304
+ conformanceDir,
305
+ label,
306
+ model: options.model,
307
+ spec: specContext,
308
+ // The unit tests, and the conformance suite - to reproduce the failure and check the fix, seeing only its output.
309
+ tools: [
310
+ fixerNotes.tool,
311
+ createScriptTool({ kind: "unit", script: scripts.unit, run: () => runUnitScript(buildDir, scripts.unit), runs: unitRuns }),
312
+ fixerConformanceRuns.tool,
313
+ ...fixerServices.tools,
314
+ ],
315
+ toolNames: ["run_unit_tests", "run_conformance_tests", ...fixerServices.names, "update_notes"],
316
+ checkEnvironment: { run: () => runUnitScript(buildDir, scripts.unit), script: scripts.unit },
317
+ });
318
+ fixerConformanceRuns.newTurn();
319
+ return ask(fixer, withNotes(text, buildDir), options.onDelta);
320
+ };
321
+ try {
322
+ let inventory = inventoryTests(conformanceDir);
323
+ // What the tests were when this run began: what it adds or changes is what the review gate reads.
324
+ const testsAtStart = digestFiles(conformanceDir);
325
+ let unitInventory = inventoryTests(buildDir);
326
+ // Acceptance tests only when the spec has them for this functionality.
327
+ const acceptance = options.diff === undefined ? (specifications[ACCEPTANCE_TESTS] ?? []).length : 0;
328
+ stage(acceptance > 0
329
+ ? `turri is writing conformance tests from the spec, and its ${acceptance} acceptance test${acceptance === 1 ? "" : "s"} apart from them`
330
+ : options.diff !== undefined
331
+ ? "turri is bringing the conformance tests in line with the spec change"
332
+ : "turri is writing conformance tests from the spec");
333
+ let response = await askConformance(BEGIN, options.onDelta);
334
+ const maxRounds = options.maxRounds ?? DEFAULT_MAX_ROUNDS;
335
+ let verified = false;
336
+ let rounds = 0;
337
+ let labelAsks = 0;
338
+ let unlabelled;
339
+ for (let round = 0; round < maxRounds; round += 1) {
340
+ const current = inventoryTests(conformanceDir);
341
+ const removals = findTestRemovals(inventory, current);
342
+ if (removals.length > 0) {
343
+ stage(`justifying ${removals.length} removed conformance test${removals.length === 1 ? "" : "s"}`);
344
+ testRemovals.push(...removals);
345
+ inventory = current;
346
+ response = await askConformance(removalChallengePrompt(removals, "conformance tests"), options.onDelta);
347
+ continue;
348
+ }
349
+ // The coding agent keeps the unit tests it found.
350
+ const unitNow = inventoryTests(buildDir);
351
+ const unitRemovals = findTestRemovals(unitInventory, unitNow);
352
+ unitInventory = unitNow;
353
+ if (unitRemovals.length > 0 && fixer !== undefined) {
354
+ stage(`justifying ${unitRemovals.length} removed unit test${unitRemovals.length === 1 ? "" : "s"}`);
355
+ testRemovals.push(...unitRemovals);
356
+ await askFixer(removalChallengePrompt(unitRemovals, "unit tests"));
357
+ continue;
358
+ }
359
+ // The conformance tests and each acceptance test have to be there, and labelled as what
360
+ // they are, before anything else counts.
361
+ if (options.diff === undefined) {
362
+ const missing = missingTestLabels(conformanceDir, frid, specifications[ACCEPTANCE_TESTS] ?? []);
363
+ if (missing.length > 0) {
364
+ if (labelAsks >= LABEL_ASKS) {
365
+ stage(`the tests were sent back ${LABEL_ASKS} times to be written and labelled, and still are not - stopping`);
366
+ unlabelled = missing;
367
+ break;
368
+ }
369
+ labelAsks += 1;
370
+ stage(listFiles(conformanceDir).length === 0 ? "no tests in the tests folder - the conformance agent writes them" : "labelling the conformance and acceptance tests");
371
+ response = await askConformance(labelsPrompt(frid, missing, listFiles(conformanceDir).length === 0, (specifications[ACCEPTANCE_TESTS] ?? []).length > 0), options.onDelta);
372
+ continue;
373
+ }
374
+ }
375
+ else {
376
+ // A spec change: every functionality it touches, as a functionality's own run would have it - every one missing
377
+ // asked for in one request, checked whole again after each answer, so none is asked for in place of another.
378
+ const pending = touched
379
+ .map((item) => ({ frid: item.frid, missing: missingTestLabels(conformanceDir, item.frid, item.acceptanceTests) }))
380
+ .filter((item) => item.missing.length > 0);
381
+ if (pending.length > 0) {
382
+ if (labelAsks >= LABEL_ASKS) {
383
+ stage(`the tests were sent back ${LABEL_ASKS} times to be written and labelled, and still are not - stopping`);
384
+ unlabelled = pending.flatMap((item) => item.missing);
385
+ break;
386
+ }
387
+ labelAsks += 1;
388
+ stage(`labelling the conformance and acceptance tests of functionalit${pending.length === 1 ? "y" : "ies"} ${pending.map((item) => item.frid).join(", ")}, which the change touches`);
389
+ const acceptance = touched.some((item) => item.acceptanceTests.length > 0);
390
+ response = await askConformance(labelsPromptFor(pending, listFiles(conformanceDir).length === 0, acceptance), options.onDelta);
391
+ continue;
392
+ }
393
+ }
394
+ // The suite is the tests folder: a conformance test in the code folder is in the wrong place,
395
+ // and not the coding agent's to write.
396
+ const misplaced = misplacedConformanceTests(buildDir);
397
+ if (misplaced.length > 0) {
398
+ stage(`conformance tests in the code folder - the coding agent removes ${misplaced.length === 1 ? "it" : "them"}`);
399
+ await askFixer(misplacedTestsPrompt(misplaced, buildDir, conformanceDir));
400
+ continue;
401
+ }
402
+ // Whether it is done is the harness's run of the project's conformance testing script, on
403
+ // every functionality's tests - and the code's unit testing script.
404
+ stage(`running the conformance testing script on the whole suite (attempt ${rounds + 1} of ${maxRounds})`);
405
+ rounds += 1;
406
+ let recheck = await runConformanceScript(buildDir, [conformanceDir], scripts);
407
+ runs.push({ command: recheck.command, exitCode: recheck.exitCode });
408
+ options.onRun?.(recheck.command, recheck.exitCode);
409
+ // The script cannot find its toolchain: the machine lacks it, and no change to the tests or the code would pass it.
410
+ // The pulse stops - rolled back by the loop - and the person is told; no agent is sent to work around it.
411
+ const lacking = recheck.exitCode !== 0 ? missingToolchainError(recheck, { moduleName, script: scripts.conformance, kind: "conformance" }) : undefined;
412
+ if (lacking !== undefined) {
413
+ if (start !== undefined)
414
+ abandonWork(start, moduleName, "the conformance testing script could not run");
415
+ throw lacking;
416
+ }
417
+ if (recheck.exitCode !== 0) {
418
+ // Nothing changing: the same failure, taken the same way, round after round. Once, the explorer
419
+ // is asked what the tests lack - most often how they reach the software; after that, it stops.
420
+ const unmoved = async () => {
421
+ const key = failureSignature(recheck);
422
+ const seen = (seenFailures.get(key) ?? 0) + 1;
423
+ seenFailures.set(key, seen);
424
+ if (seen < STUCK_ROUNDS)
425
+ return undefined;
426
+ seenFailures.clear();
427
+ if (reExplored) {
428
+ stuck = { failure: truncateOutput(recheck.output, undefined).slice(-4000), rounds, error: failureLine(recheck), times: STUCK_ROUNDS };
429
+ stage(`the same failure ${STUCK_ROUNDS} times again, after the explorer was asked - stopping`);
430
+ recordNote(conformanceDir, "pitfalls", `stuck after ${rounds} attempts: failed with ${failureLine(recheck)} ${STUCK_ROUNDS} times, before and after the explorer was asked again`, `${label} conformance`);
431
+ return "stuck";
432
+ }
433
+ reExplored = true;
434
+ stage(`the same failure ${STUCK_ROUNDS} times - asking the explorer again what the tests lack`);
435
+ invocation = await exploreInvocation({
436
+ buildDir,
437
+ spec: specContext,
438
+ scope,
439
+ testingScripts,
440
+ question: { failure: shownOutput(buildDir, "conformance-stuck", recheck.output), account: invocation },
441
+ acceptance: toCarryOut,
442
+ label,
443
+ model: options.model,
444
+ onDelta: options.onDelta,
445
+ });
446
+ const asked = await changing(() => ask(executor, [
447
+ `The conformance tests have failed the same way ${STUCK_ROUNDS} times, so the explorer was asked again what they`,
448
+ "lack. This is its new account - it replaces the one you had. Read it, and the conformance testing script at",
449
+ "the top of your prompt, for how the tests reach the software; then fix the tests accordingly.",
450
+ "",
451
+ `<how to invoke it>\n${invocation.trim()}\n</how to invoke it>`,
452
+ "",
453
+ ...buildManifests(buildDir).map((manifest) => `<code build file path="${manifest.path}">\n${manifest.content.trimEnd()}\n</code build file>\n`),
454
+ "<failing run>",
455
+ recheck.output.trimEnd().slice(-6000),
456
+ "</failing run>",
457
+ ].join("\n"), options.onDelta));
458
+ response = asked.result;
459
+ recordAttempt(rounds, recheck, `the same failure ${STUCK_ROUNDS} times - the explorer was asked again what the tests lack`, { who: "conformance agent", files: asked.files });
460
+ return "asked";
461
+ };
462
+ // The tests pass and only their static checks do not: the findings are in the tests, the conformance agent's
463
+ // alone - no judge is asked whether the code or the tests are wrong.
464
+ if (staticChecksOnly(recheck)) {
465
+ const moved = await unmoved();
466
+ if (moved === "stuck")
467
+ break;
468
+ if (moved === "asked")
469
+ continue;
470
+ stage("the conformance tests pass, and their static checks do not - back to the conformance agent");
471
+ const fixed = await changing(() => askConformance(scriptFailure(recheck, "conformance testing"), options.onDelta));
472
+ response = fixed.result;
473
+ recordAttempt(rounds, recheck, "the tests' static checks", { who: "conformance agent", files: fixed.files });
474
+ if (fixed.files.length === 0 && ++idleFixes >= IDLE_FIXES) {
475
+ stuck = { failure: truncateOutput(recheck.output, undefined).slice(-4000), rounds, error: failureLine(recheck), times: idleFixes, changedNothing: true };
476
+ stage(`${IDLE_FIXES} fixes in a row changed nothing - stopping`);
477
+ break;
478
+ }
479
+ if (fixed.files.length > 0)
480
+ idleFixes = 0;
481
+ continue;
482
+ }
483
+ // A run out of time is not a question of test or code: the tests' environment is the conformance agent's.
484
+ if (recheck.timedOut === true) {
485
+ const moved = await unmoved();
486
+ if (moved === "stuck")
487
+ break;
488
+ if (moved === "asked")
489
+ continue;
490
+ stage("the conformance run did not get to the tests - back to the conformance agent");
491
+ const fixed = await changing(() => askConformance(scriptFailure(recheck, "conformance testing"), options.onDelta));
492
+ response = fixed.result;
493
+ recordAttempt(rounds, recheck, `not a question of code or tests (${recheck.timedOut === true ? "the time limit" : `exit ${recheck.exitCode}`})`, { who: "conformance agent", files: fixed.files });
494
+ continue;
495
+ }
496
+ stage(`conformance tests failing - working out whether the code, the tests or the spec are wrong${judgeModel ? ` (${judgeModel})` : ""}`);
497
+ // A fix that changed no file leaves the next run as this one: IDLE_FIXES of them in a row, and nothing will change.
498
+ const idle = (files) => {
499
+ idleFixes = files.length === 0 ? idleFixes + 1 : 0;
500
+ if (idleFixes < IDLE_FIXES)
501
+ return false;
502
+ stuck = { failure: truncateOutput(recheck.output, undefined).slice(-4000), rounds, error: failureLine(recheck), times: idleFixes, changedNothing: true };
503
+ stage(`${IDLE_FIXES} fixes in a row changed nothing - stopping`);
504
+ recordNote(conformanceDir, "pitfalls", `stuck after ${rounds} attempts: failed with ${failureLine(recheck)}, and ${IDLE_FIXES} fixes in a row changed nothing`, `${label} conformance`);
505
+ return true;
506
+ };
507
+ const verdict = await judgeFailure({
508
+ buildDir,
509
+ conformanceDir,
510
+ spec: specContext,
511
+ run: recheck,
512
+ invocation,
513
+ scope,
514
+ // The tests' notes - every attempt so far - and the code's: it weighs the one against the other.
515
+ notes: [notesToRead(conformanceDir), notesToRead(buildDir)].filter((part) => part !== "").join("\n\n"),
516
+ label,
517
+ model: judgeModel,
518
+ onDelta: options.onDelta,
519
+ });
520
+ const reason = verdict.reason.split("\n")[0].slice(0, 160);
521
+ // The environment - what the run is given - is the person's to fix: once judged so twice in a row, the run stops.
522
+ if (verdict.culprit === "environment") {
523
+ environmentRaises += 1;
524
+ reportActivity(` the environment (judged ${environmentRaises} of ${ENVIRONMENT_RAISES} in a row): ${reason}`);
525
+ if (environmentRaises >= ENVIRONMENT_RAISES) {
526
+ stage("the run's environment is what is wrong, and only the person can fix it - stopping");
527
+ environment = { reason: verdict.reason, times: environmentRaises };
528
+ recordNote(conformanceDir, "pitfalls", `the conformance run's environment is wrong, for the person to fix: ${verdict.reason.replace(/\s+/g, " ").slice(0, 300)}`, `${label} conformance`);
529
+ break;
530
+ }
531
+ stage(`the run's environment is judged wrong (${environmentRaises} of ${ENVIRONMENT_RAISES}) - the conformance agent checks the tests do not cause it`);
532
+ const fixed = await changing(() => askConformance(environmentCheckPrompt({ run: recheck, reason: verdict.reason }), options.onDelta));
533
+ response = fixed.result;
534
+ recordAttempt(rounds, recheck, `the environment: ${verdict.reason}`, { who: "conformance agent", files: fixed.files });
535
+ continue;
536
+ }
537
+ environmentRaises = 0;
538
+ // A spec conflict has counting of its own; code or tests the same way, again and again, is nothing changing.
539
+ if (verdict.culprit !== "spec") {
540
+ const moved = await unmoved();
541
+ if (moved === "stuck")
542
+ break;
543
+ if (moved === "asked")
544
+ continue;
545
+ }
546
+ if (verdict.culprit === "spec") {
547
+ // Counted per pair of functionalities: it stops the run only when the same two are named often enough.
548
+ const pair = sortedRefs(verdict.functionalities);
549
+ const key = conflictKey(pair);
550
+ const said = [...(conflicts.get(key) ?? []), verdict.reason.trim()];
551
+ conflicts.set(key, said);
552
+ const raised = said.length;
553
+ reportActivity(` the spec, functionalities ${key} (raised ${raised} of ${SPEC_CONFLICT_RAISES}): ${reason}`);
554
+ if (raised >= SPEC_CONFLICT_RAISES) {
555
+ stage(`the spec contradicts itself: functionalities ${key} cannot both hold - stopping`);
556
+ specConflict = { functionalities: pair, reason: verdict.reason };
557
+ // For the person, whole: both functional specs as their modules have them, and every time it was raised.
558
+ const ownSpecs = specifications[FUNCTIONAL_REQUIREMENTS];
559
+ const files = plainFilesOf(options.plainFile, templateDirs);
560
+ reportSpecConflict({
561
+ moduleName,
562
+ plainFile: options.plainFile,
563
+ specs: pair.map((ref) => ({
564
+ ...ref,
565
+ plainFile: files.get(ref.module) ?? options.plainFile,
566
+ text: ref.module === moduleName
567
+ ? (ownSpecs[frids.indexOf(ref.frid)] ?? "")
568
+ : (required.find((spec) => spec.module === ref.module && spec.frid === ref.frid)?.spec ?? ""),
569
+ })),
570
+ reasons: said,
571
+ });
572
+ recordNote(conformanceDir, "pitfalls", `functionalities ${key} of the spec conflict: ${verdict.reason.replace(/\s+/g, " ").slice(0, 300)}`, `${label} conformance`);
573
+ break;
574
+ }
575
+ // Recorded, and the round still changes something: the tests are looked at again, so the
576
+ // next judgement is of a new run rather than the same one asked about again.
577
+ stage(`a spec conflict between functionalities ${key} is recorded (${raised} of ${SPEC_CONFLICT_RAISES}) - the conformance agent looks at the tests again`);
578
+ const fixed = await changing(() => ask(executor, testsFixPrompt({
579
+ run: recheck,
580
+ reason: `Functionalities ${key} of the specification may conflict: ${verdict.reason}\n\n` +
581
+ "Before that is taken as the answer, check the tests of both against the specification once more: " +
582
+ "that each asserts only what its functionality says, and sets up what it needs without undoing what the other relies on.",
583
+ }), options.onDelta));
584
+ response = fixed.result;
585
+ recordAttempt(rounds, recheck, `a spec conflict between functionalities ${key}: ${verdict.reason}`, { who: "conformance agent", files: fixed.files });
586
+ }
587
+ else if (verdict.culprit === "code") {
588
+ reportActivity(` the code: ${reason}`);
589
+ stage("the code is wrong - the coding agent fixes it");
590
+ // One fixer for the run, starting each fix from what the ones before it tried: its work is compacted as
591
+ // it gets longer, so a session carried through the run does not re-read every earlier fix and suite output
592
+ // whole.
593
+ const fixed = await changing(() => askFixer(codeFixPrompt({ run: recheck, reason: verdict.reason })));
594
+ recordAttempt(rounds, recheck, `the code wrong: ${verdict.reason}`, { who: "coding agent", files: fixed.files });
595
+ if (idle(fixed.files))
596
+ break;
597
+ // The fix has to build and pass its own unit tests before the suite runs again: a build
598
+ // error is the coding agent's to fix, not a judge's to work out from the suite's output.
599
+ for (let again = 0; again < FIX_UNIT_CHECKS; again += 1) {
600
+ stage("checking the fix: the code's unit tests");
601
+ const unit = await verifyUnitSuite(buildDir, unitRuns, scripts.unit);
602
+ if (unit.ok)
603
+ break;
604
+ if (unit.missing !== undefined)
605
+ throw toolchainError(unit.missing, { moduleName, script: scripts.unit, kind: "unit" });
606
+ stage("the fix does not build or pass its unit tests - back to the coding agent");
607
+ await changing(() => askFixer(`Your fix does not build or pass the code's unit tests - it has to before the conformance suite runs again.\n\n${unit.prompt}`));
608
+ }
609
+ }
610
+ else {
611
+ reportActivity(` the tests: ${reason}`);
612
+ stage("the tests are wrong - the conformance agent fixes them");
613
+ const fixed = await changing(() => askConformance(testsFixPrompt({ run: recheck, reason: verdict.reason }), options.onDelta));
614
+ response = fixed.result;
615
+ recordAttempt(rounds, recheck, `the tests wrong: ${verdict.reason}`, { who: "conformance agent", files: fixed.files });
616
+ if (idle(fixed.files))
617
+ break;
618
+ }
619
+ continue;
620
+ }
621
+ stage(`conformance tests pass (attempt ${rounds})`);
622
+ // Done is both: the conformance suite passing, and the unit tests with it - whether or not
623
+ // the code looks changed. Conformance passing on code whose unit tests fail is not done.
624
+ stage("running the unit testing script too");
625
+ const unit = await verifyUnitSuite(buildDir, unitRuns, scripts.unit);
626
+ if (!unit.ok && unit.missing !== undefined)
627
+ throw toolchainError(unit.missing, { moduleName, script: scripts.unit, kind: "unit" });
628
+ if (!unit.ok) {
629
+ stage("unit tests NOT passing - the coding agent fixes the code");
630
+ await askFixer(`The conformance tests pass, but the code's unit tests do not - and both have to.\n\n${unit.prompt}`);
631
+ continue;
632
+ }
633
+ stage("unit tests pass too");
634
+ green = { code: digestFiles(buildDir), tests: digestFiles(conformanceDir) };
635
+ // The review gate: passing tests prove nothing when they assert nothing, mock the software, or leave out
636
+ // what the spec asks for. It has to pass, as the suite and the unit tests do.
637
+ const testChanges = diffFiles(testsAtStart, digestFiles(conformanceDir));
638
+ const changedTests = [...testChanges.added, ...testChanges.changed];
639
+ // After the last review's problems were worked on, the tests go on once they pass: not reviewed again. With
640
+ // the review off in the config, they go on once they pass from the first.
641
+ if (reviewOn && changedTests.length > 0 && failedReviews < REVIEW_LIMIT) {
642
+ stage(`optimising the tests (review ${failedReviews + 1} of at most ${REVIEW_LIMIT}): black box, and the acceptance tests doing what the spec says`);
643
+ // One reviewer for the run, made the first time the tests are reviewed.
644
+ reviewer ??= await createTestReviewer({
645
+ ...(options.cases !== undefined ? { cases: options.cases } : {}),
646
+ buildDir,
647
+ conformanceDir,
648
+ spec: specContext,
649
+ functionalities: forChange ? touched : [{ frid: frid, acceptanceTests: specifications[ACCEPTANCE_TESTS] ?? [] }],
650
+ forChange,
651
+ invocation,
652
+ label,
653
+ model: judgeModel,
654
+ onDelta: options.onDelta,
655
+ });
656
+ const account = describeAccounts(work.take(), lastReview);
657
+ const request = reviewSeen === undefined || lastReview === undefined
658
+ ? testReviewRequest({ diff: workDiff(conformanceDir), changed: changedTests, account })
659
+ : testReviewRequest({ diff: diffSinceSnapshot(conformanceDir, reviewSeen), changed: changedTests, account, earlier: lastReview, letGo: reviewLetGo });
660
+ reviewSeen = workSnapshot(conformanceDir);
661
+ reviewLetGo = [];
662
+ const review = await reviewer.review(request);
663
+ if (!review.pass) {
664
+ failedReviews += 1;
665
+ let problems = review.problems;
666
+ const letGo = (why, waived) => {
667
+ stage(why);
668
+ reviewWaived = [...(reviewWaived ?? []), ...waived];
669
+ lastReview = undefined;
670
+ verified = true;
671
+ };
672
+ // After the first REVIEW_ROUNDS reviews that send them back, the arbiter weighs each: only what it upholds goes to the conformance agent.
673
+ if (failedReviews > REVIEW_ROUNDS) {
674
+ stage(`the review sent the tests back ${failedReviews === 1 ? "once" : `${failedReviews} times`} - the arbiter weighs what it found`);
675
+ const ruling = await arbitrateReview({
676
+ subject: `the conformance tests of ${forChange ? `the spec change in module \`${moduleName}\`` : `functionality ${frid} of module \`${moduleName}\``}`,
677
+ workdir: conformanceDir,
678
+ read: [conformanceDir, buildDir],
679
+ uphold: [
680
+ "- uphold it when because of it the functionality is not tested black box - a test or an acceptance test",
681
+ " the specification states missing, cut down or skipping what it checks, the software mocked or not reached",
682
+ " from outside, a test that cannot fail. Then the reviewer is right, and the conformance agent fixes it.",
683
+ ],
684
+ changed: changedTests,
685
+ spec: `<specification>\n${specContext.text.trim()}\n</specification>`,
686
+ problems: review.problems,
687
+ history: reviewHistory,
688
+ label: `${label} conformance`,
689
+ ...(judgeModel !== undefined ? { model: judgeModel } : {}),
690
+ ...(options.onDelta !== undefined ? { onDelta: options.onDelta } : {}),
691
+ });
692
+ // No ruling: the review stands.
693
+ if (ruling !== undefined) {
694
+ if (ruling.dismissed.length > 0)
695
+ recordNote(conformanceDir, "pitfalls", dismissedNote(ruling, "conformance test"), `${label} conformance`);
696
+ if (ruling.upheld.length === 0) {
697
+ letGo("the arbiter approved the tests as they are - what the review still raised is let go, and in the notes", ruling.dismissed.map((entry) => entry.problem));
698
+ break;
699
+ }
700
+ reviewWaived = [...(reviewWaived ?? []), ...ruling.dismissed.map((entry) => entry.problem)];
701
+ stage(`the arbiter upheld ${ruling.upheld.length} of the review's ${review.problems.length} problem${review.problems.length === 1 ? "" : "s"} - the conformance agent fixes ${ruling.upheld.length === 1 ? "it" : "those"}`);
702
+ problems = ruling.upheld.map((entry) => entry.problem);
703
+ reviewLetGo = ruling.dismissed.map((entry) => entry.problem);
704
+ }
705
+ }
706
+ // An optimiser, not a gate: what the last review sends back goes to the conformance agent once more, and the
707
+ // tests go on once the suite and the unit tests pass - they are not reviewed again.
708
+ if (failedReviews >= REVIEW_LIMIT) {
709
+ recordNote(conformanceDir, "pitfalls", `the conformance tests went on after ${REVIEW_LIMIT} reviews, the last one's problems worked on but not reviewed again: ${problems.map((problem) => `${problem.file}${problem.test ? ` ${problem.test}` : ""}: ${problem.what.trim()}`).join("; ").slice(0, 600)}`, `${label} conformance`);
710
+ stage(`the last of ${REVIEW_LIMIT} reviews sends the tests back once more - after that they go on once they pass, without another review`);
711
+ }
712
+ reviewHistory.push(review.problems);
713
+ lastReview = problems;
714
+ const blocking = problems.filter((problem) => problem.blocking === true);
715
+ reportActivity(` the review found ${problems.length} problem${problems.length === 1 ? "" : "s"}: ${problems[0].what.slice(0, 140)}`);
716
+ stage(blocking.length > 0
717
+ ? `the review found the functionality not yet tested black box (${blocking.length} of ${problems.length} problem${problems.length === 1 ? "" : "s"}) - the conformance agent completes the tests`
718
+ : `the review found ${problems.length} way${problems.length === 1 ? "" : "s"} to improve the tests - the conformance agent improves them`);
719
+ const pitfalls = readNotes(conformanceDir).pitfalls;
720
+ const fixed = await changing(async () => {
721
+ const answered = await askConformance(reviewFeedbackPrompt(problems), options.onDelta);
722
+ // Every problem answered - fixed, or declined and why - so the reviewer knows what became of each: sent back once.
723
+ const missing = unanswered(work.peek(), problems.length);
724
+ return missing.length > 0 ? askConformance(unansweredPrompt(missing), options.onDelta) : answered;
725
+ });
726
+ // The lesson, left for the agents after it: the next functionality's conformance tests start from it.
727
+ await reflectOnReview(conformanceDir, pitfalls, (prompt) => askConformance(prompt, options.onDelta));
728
+ response = fixed.result;
729
+ recordAttempt(rounds, "passed, but the review found " + problems.map((problem) => `${problem.kind} ${problem.file}: ${problem.what}`).join("; ").slice(0, 220), "the tests", { who: "conformance agent", files: fixed.files });
730
+ continue;
731
+ }
732
+ stage("the review has nothing left to improve in the tests");
733
+ }
734
+ lastReview = undefined;
735
+ verified = true;
736
+ break;
737
+ }
738
+ // Notes are kept only with work that lands: work that does not is rolled back, notes and all.
739
+ if (verified) {
740
+ stage("checking the working notes were left");
741
+ await notes.ensureWritten((next) => askConformance(next, options.onDelta, NOTES_TURN), passedOutcome("conformance testing script"));
742
+ if (fixer !== undefined)
743
+ await fixerNotes.ensureWritten((next) => ask(fixer, next, options.onDelta, NOTES_TURN), passedOutcome("conformance testing script"));
744
+ }
745
+ const changed = codeChanged();
746
+ if (verified) {
747
+ // The tests, and the code when it changed: one commit if they share a module folder.
748
+ const subject = `conformance ${moduleName} ${options.diff !== undefined ? "for a spec change" : `fs ${frid}`}${changed ? " (code changed)" : ""}`;
749
+ stage(`committing the conformance tests${changed ? " and the code changes" : ""}`);
750
+ recordHistory(conformanceDir, subject);
751
+ if (moduleDirOf(buildDir) !== moduleDirOf(conformanceDir))
752
+ recordHistory(buildDir, subject);
753
+ }
754
+ else if (start !== undefined) {
755
+ const why = notLandedWhy(options.diff !== undefined ? "for the spec change" : `of functionality ${frid}`, unlabelled);
756
+ stage(`${why} - undoing them`);
757
+ abandonWork(start, moduleName, why);
758
+ }
759
+ return {
760
+ moduleName,
761
+ frid: frid ?? "",
762
+ buildDir,
763
+ conformanceDir,
764
+ prompt: agentPrompt,
765
+ response,
766
+ files: listFiles(conformanceDir),
767
+ runs,
768
+ passed: verified,
769
+ rounds,
770
+ codeChanged: changed,
771
+ testRemovals,
772
+ ...(specConflict !== undefined ? { specConflict } : {}),
773
+ ...(stuck !== undefined ? { stuck } : {}),
774
+ ...(environment !== undefined ? { environment } : {}),
775
+ ...(unlabelled !== undefined ? { unlabelled } : {}),
776
+ ...(reviewWaived !== undefined ? { reviewWaived } : {}),
777
+ ...(verified && green !== undefined ? { green } : {}),
778
+ ...(!verified && lastReview !== undefined ? { review: lastReview } : {}),
779
+ };
780
+ }
781
+ finally {
782
+ executor.dispose();
783
+ fixer?.dispose();
784
+ reviewer?.dispose();
785
+ await fixerServices.stopAll();
786
+ }
787
+ }
788
+ /**
789
+ * Why conformance tests did not land, in a line. Tests sent back to be labelled and never labelled may well pass: the
790
+ * line says so, rather than that they failed.
791
+ */
792
+ export function notLandedWhy(what, unlabelled) {
793
+ if (unlabelled === undefined)
794
+ return `the conformance tests ${what} do not pass`;
795
+ return `the conformance tests ${what} were not written and labelled after ${LABEL_ASKS} requests - still missing: ${unlabelled.join("; ")} - whatever the test runs said`;
796
+ }
797
+ /** Each module of the chain, by name, and its .plain file - for a conflict that crosses modules. */
798
+ function plainFilesOf(plainFile, templateDirs) {
799
+ try {
800
+ return new Map(resolveRequiresChain(plainFile, templateDirs).map((entry) => [entry.moduleName, entry.plainFile]));
801
+ }
802
+ catch {
803
+ return new Map();
804
+ }
805
+ }
806
+ /**
807
+ * The functionalities a spec change touches - its functional spec, or one of its acceptance tests,
808
+ * has a line the change adds or removes - with their acceptance tests: what a spec diff's
809
+ * conformance run labels, checks and reviews, as a functionality's run does for its one.
810
+ */
811
+ export function touchedFunctionalities(tree, frids, diff) {
812
+ const bare = (line) => line.trim().replace(/^-\s+/, "").replace(/\s+/g, " ");
813
+ const changed = new Set(diff
814
+ .split("\n")
815
+ .filter((line) => /^[+-]/.test(line) && !/^(\+\+\+|---)/.test(line))
816
+ .map((line) => bare(line.slice(1)))
817
+ .filter((line) => line.length > 12));
818
+ return frids.flatMap((frid) => {
819
+ const specifications = getSpecificationsForFrid(tree, frid)[0];
820
+ const spec = specifications[FUNCTIONAL_REQUIREMENTS].at(-1) ?? "";
821
+ const acceptanceTests = specifications[ACCEPTANCE_TESTS] ?? [];
822
+ const lines = [spec, ...acceptanceTests].flatMap((text) => text.split("\n")).map(bare);
823
+ return lines.some((line) => changed.has(line)) ? [{ frid, acceptanceTests }] : [];
824
+ });
825
+ }
826
+ /** From the how-manieth review that sends the tests back the arbiter weighs each one (see arbiter.ts). */
827
+ export const REVIEW_ROUNDS = ARBITER_AFTER;
828
+ /** How many times the tests are sent back to be written and labelled before the run stops. */
829
+ export const LABEL_ASKS = 3;
830
+ /** How many times a code fix that does not build or pass its unit tests goes back to the coding agent before the suite runs again. */
831
+ export const FIX_UNIT_CHECKS = 3;
832
+ /** How many times an agent runs the conformance suite in a turn before its turn ends and the harness takes over. */
833
+ export const RUNS_PER_TURN = 3;
834
+ /**
835
+ * A testing script tool an agent may call so many times a turn. The last allowed call ends its turn
836
+ * with the result - the harness then runs the suite itself and judges what is wrong - so an agent
837
+ * cannot spend its turn rerunning a failure it cannot fix. `newTurn` starts the count again.
838
+ */
839
+ export function turnLimited(tool, limit) {
840
+ let calls = 0;
841
+ const limited = {
842
+ ...tool,
843
+ execute: async (...args) => {
844
+ calls += 1;
845
+ if (calls > limit) {
846
+ return {
847
+ content: [{ type: "text", text: `Not run: that is ${limit} runs this turn. Your turn ends here - the harness runs the suite itself and works out what is wrong.` }],
848
+ details: {},
849
+ terminate: true,
850
+ };
851
+ }
852
+ const result = (await tool.execute(...args));
853
+ if (calls < limit)
854
+ return result;
855
+ return {
856
+ ...result,
857
+ content: [...result.content, { type: "text", text: `\n\nThat was your ${limit}rd run this turn: your turn ends here. The harness runs the suite itself, works out whether the code or the tests are wrong, and sends it to whichever it is.` }],
858
+ terminate: true,
859
+ };
860
+ },
861
+ };
862
+ return { tool: limited, newTurn: () => void (calls = 0) };
863
+ }
864
+ export { failureSignature, STUCK_ROUNDS };
865
+ /** How many fixes in a row that change no file - the code's or the tests' - is nothing changing. */
866
+ export const IDLE_FIXES = 2;
867
+ /** The build files a project's name, coordinates and version live in, by ecosystem. */
868
+ const BUILD_MANIFESTS = [
869
+ "pom.xml",
870
+ "build.gradle",
871
+ "build.gradle.kts",
872
+ "settings.gradle",
873
+ "settings.gradle.kts",
874
+ "package.json",
875
+ "pyproject.toml",
876
+ "setup.py",
877
+ "setup.cfg",
878
+ "go.mod",
879
+ "Cargo.toml",
880
+ "composer.json",
881
+ "Gemfile",
882
+ "mix.exs",
883
+ ];
884
+ /**
885
+ * The code's own build files, at the top of its folder, as they are - read by the harness, for the
886
+ * author of the conformance tests, who cannot read the code. They are interface, not implementation:
887
+ * what a separate test project has to point at - the name, the coordinates, the version.
888
+ */
889
+ export function buildManifests(buildDir) {
890
+ return BUILD_MANIFESTS.filter((name) => existsSync(join(buildDir, name))).map((name) => {
891
+ const content = readFileSync(join(buildDir, name), "utf8");
892
+ return { path: name, content: content.length > 20_000 ? `${content.slice(0, 20_000)}\n… [${content.length - 20_000} more characters]` : content };
893
+ });
894
+ }
895
+ /** What a failing run failed with, in one line as it said it: its first line that says what went wrong. */
896
+ export function failureLine(run) {
897
+ if (run.timedOut === true)
898
+ return "the time limit - the run did not finish";
899
+ const lines = run.output.split("\n").map((line) => line.trim()).filter((line) => line !== "");
900
+ // The error itself - `AssertionError: …`, `ConnectException: …` - says more than a summary line: up to two, each once.
901
+ const errors = [...new Set(lines.filter((line) => /\w(Error|Exception)\b[^\n]*:/.test(line)))];
902
+ const telling = errors.length > 0 ? errors : lines.filter((line) => /error|exception|fail|refused|cannot|unable|not found|denied/i.test(line));
903
+ const picked = telling.length > 0 ? telling.slice(0, 2) : [lines.at(-1) ?? `exit ${run.exitCode}`];
904
+ return picked.map((line) => `"${line.length > 160 ? `${line.slice(0, 159)}…` : line}"`).join(" and ");
905
+ }
906
+ /**
907
+ * The spec as the explorer, the judge and the coding agent are shown it - its functional specs
908
+ * numbered with their ids, so a spec conflict can name them.
909
+ */
910
+ function specText(specifications, moduleName, frids, required, diff, modules) {
911
+ // Every functionality in the codebase, each with its module: the required modules' first, then this one's.
912
+ const numbered = (key, items) => key === FUNCTIONAL_REQUIREMENTS
913
+ ? [
914
+ functionalitiesByModule([...required, ...items.map((item, index) => ({ module: moduleName, frid: frids[index] ?? String(index + 1), spec: item }))], moduleName),
915
+ ]
916
+ : items;
917
+ // Each section in its tag, as every agent is shown it (see specPrimer).
918
+ const sections = [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS, FUNCTIONAL_REQUIREMENTS, ACCEPTANCE_TESTS]
919
+ .map((key) => [key, specifications[key] ?? []])
920
+ .filter(([, items]) => items.length > 0)
921
+ .map(([key, items]) => (key === FUNCTIONAL_REQUIREMENTS || key === ACCEPTANCE_TESTS ? section(key, numbered(key, items)) : moduleSection(key, items, moduleName, modules)));
922
+ if (diff !== undefined)
923
+ sections.push(specChangeBlock(diff));
924
+ return sections.join("\n\n");
925
+ }
926
+ /**
927
+ * The functional spec a conformance run writes tests for, for the person watching: which one of how
928
+ * many, and its text as the spec has it, with its acceptance tests after it.
929
+ */
930
+ export function buildingSpec(moduleName, frid, total, specifications) {
931
+ const acceptance = specifications[ACCEPTANCE_TESTS] ?? [];
932
+ return {
933
+ label: `${moduleName} functionality ${frid} of ${total}`,
934
+ text: [(specifications["functional specs"].at(-1) ?? "").trim(), ...(acceptance.length > 0 ? ["", "acceptance tests:", ...acceptance.map((test) => test.trim())] : [])].join("\n"),
935
+ };
936
+ }