@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,406 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, join, relative } from "node:path";
3
+ import { defineTool } from "@earendil-works/pi-coding-agent";
4
+ import { Type } from "typebox";
5
+ import { reportActivity } from "./activity.js";
6
+ import { maskSecrets } from "./masking.js";
7
+ import { NOTES_FILE } from "./notes.js";
8
+ import { modulesScratchDir } from "./paths.js";
9
+ import { RECORD_FILE } from "./records.js";
10
+ import { missingToolchain, runUnitScript, scriptFailure } from "./test-scripts.js";
11
+ import { COMMAND_TIMEOUT_MS } from "./shell.js";
12
+ /** Up to this much of a run's output is shown: all of it when it fits, otherwise its end. */
13
+ const MAX_OUTPUT_CHARS = 60_000;
14
+ /** A test run has the same time limit as any command: see COMMAND_TIMEOUT_MS. */
15
+ export const RUN_TIMEOUT_MS = COMMAND_TIMEOUT_MS;
16
+ /** Full outputs kept per project, newest first; older ones are removed. */
17
+ const MAX_LOGS = 100;
18
+ /**
19
+ * Output as an agent is shown it: whole when it fits, otherwise its last MAX_OUTPUT_CHARS - where a
20
+ * test run says what failed and why. With `log` the whole output is kept in that file too, and the
21
+ * cut says how to read it: through read_output, by the file's name - never by a path, which would
22
+ * point the agent outside its walls.
23
+ */
24
+ export function truncateOutput(text, log) {
25
+ if (text.length <= MAX_OUTPUT_CHARS)
26
+ return text;
27
+ const cut = text.length - MAX_OUTPUT_CHARS;
28
+ const where = log !== undefined ? ` - the whole output is kept as \`${basename(log)}\`: call read_output with that id - it is not a file in your folder` : "";
29
+ return `... [${cut} characters cut from the start${where}] ...\n\n${text.slice(-MAX_OUTPUT_CHARS)}`;
30
+ }
31
+ /** A test run's output up to this long is shown whole. */
32
+ const TEST_OUTPUT_WHOLE = 16_000;
33
+ /** Past it: lines after a failure kept with it - its trace - and lines kept from the end. */
34
+ const TRACE_LINES = 20;
35
+ const END_LINES = 80;
36
+ /** What says a test failed, and what says how many ran - in the frameworks' own words. */
37
+ const FAILURE_LINE = /FAIL|ERROR|Error:|Exception|Traceback|Assertion|AssertionError|expected|Caused by|panicked|✗|✕|×|<<<|BUILD (SUCCESS|FAILURE)|Tests run:|tests? (passed|failed)|\d+ (passed|failed)|Ran \d+ tests?|^(OK|FAILED)\b/;
38
+ /** A framework's or an application's logging below a warning: noise in a failing run. */
39
+ const LOG_NOISE = /^\S*\s*\S*\s*(DEBUG|TRACE)\b|^\[?(DEBUG|TRACE)\]?\s/;
40
+ /**
41
+ * A test run's output as an agent is shown it: whole when it is short; otherwise its failures with the trace after each,
42
+ * its counts and its end - DEBUG and TRACE logging left out - with the whole kept and one read_output away. Most of what
43
+ * agents carried in their context was a failing run's logging; what they act on is which test failed, where, and why.
44
+ */
45
+ export function shownTestOutput(text, log) {
46
+ if (text.length <= TEST_OUTPUT_WHOLE)
47
+ return text;
48
+ const lines = text.split("\n");
49
+ const keep = new Set();
50
+ lines.forEach((line, index) => {
51
+ if (LOG_NOISE.test(line) || !FAILURE_LINE.test(line))
52
+ return;
53
+ for (let at = index; at < Math.min(lines.length, index + TRACE_LINES); at += 1) {
54
+ if (at > index && LOG_NOISE.test(lines[at]))
55
+ break;
56
+ keep.add(at);
57
+ }
58
+ });
59
+ for (let at = Math.max(0, lines.length - END_LINES); at < lines.length; at += 1)
60
+ if (!LOG_NOISE.test(lines[at]))
61
+ keep.add(at);
62
+ const shown = [];
63
+ let left = 0;
64
+ lines.forEach((line, index) => {
65
+ if (keep.has(index)) {
66
+ if (left > 0)
67
+ shown.push(` ... [${left} lines left out] ...`);
68
+ left = 0;
69
+ shown.push(line);
70
+ }
71
+ else
72
+ left += 1;
73
+ });
74
+ if (left > 0)
75
+ shown.push(` ... [${left} lines left out] ...`);
76
+ let body = shown.join("\n");
77
+ if (body.length > TEST_OUTPUT_WHOLE)
78
+ body = `... [the start cut] ...\n${body.slice(-TEST_OUTPUT_WHOLE)}`;
79
+ const where = log !== undefined ? `the whole is kept as \`${basename(log)}\`: call read_output with that id for the rest - it is not a file in your folder` : "the rest was not kept";
80
+ return `[turri: the run printed ${lines.length} lines; shown are its failures, its counts and its end - ${where}]\n${body}`;
81
+ }
82
+ /** Where the whole outputs are kept: the project's scratch area, read only through read_output. */
83
+ export function outputsDir(dir) {
84
+ return join(modulesScratchDir(dir), "logs");
85
+ }
86
+ /** Keep `output` whole, whatever its length, in the outputs folder. Returns the file, or nothing when it could not be written. */
87
+ export function keepOutput(dir, label, output) {
88
+ try {
89
+ const logs = outputsDir(dir);
90
+ mkdirSync(logs, { recursive: true });
91
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
92
+ const name = label.replace(/[^\w.-]+/g, "_").slice(0, 60);
93
+ const file = join(logs, `${stamp}-${name}.log`);
94
+ writeFileSync(file, typeof output === "string" ? maskSecrets(output) : maskSecrets(output.toString("utf8")));
95
+ const all = readdirSync(logs).sort();
96
+ for (const old of all.slice(0, Math.max(0, all.length - MAX_LOGS)))
97
+ rmSync(join(logs, old), { force: true });
98
+ return file;
99
+ }
100
+ catch {
101
+ return undefined;
102
+ }
103
+ }
104
+ /**
105
+ * Keep a run's whole output where an agent can read it, when it is too long to be shown whole:
106
+ * in the project's scratch area, inside plain_modules. Returns the file, or nothing when the
107
+ * output fits.
108
+ */
109
+ export function keepFullOutput(dir, label, output) {
110
+ if (output.length <= MAX_OUTPUT_CHARS)
111
+ return undefined;
112
+ return keepOutput(dir, label, output);
113
+ }
114
+ /** Keep a test run's whole output for read_output when it is too long to be shown whole - past TEST_OUTPUT_WHOLE. */
115
+ export function keepTestOutput(dir, label, output) {
116
+ if (output.length <= TEST_OUTPUT_WHOLE)
117
+ return undefined;
118
+ return keepOutput(dir, label, output);
119
+ }
120
+ /** pi's bash, cut short, names the temporary file the whole output is in - on the host, outside the agent's walls. */
121
+ const SHELL_FULL_OUTPUT = /Full output: (\S+)\]$/;
122
+ /**
123
+ * A shell's output as the agent is shown it: when pi cut it and kept the whole in a temporary file of its own, the file
124
+ * is taken into the outputs folder, and the agent pointed at read_output instead of at the file.
125
+ */
126
+ export function keptShellOutput(dir, text) {
127
+ const found = SHELL_FULL_OUTPUT.exec(text);
128
+ if (found === null)
129
+ return text;
130
+ let kept;
131
+ try {
132
+ kept = keepOutput(dir, "bash", readFileSync(found[1]));
133
+ }
134
+ catch {
135
+ kept = undefined;
136
+ }
137
+ const pointer = kept !== undefined ? `The whole output is kept as \`${basename(kept)}\`: call read_output with that id - it is not a file in your folder]` : "The rest of the output was not kept]";
138
+ return text.slice(0, found.index) + pointer;
139
+ }
140
+ /** Lines read_output shows at most at once, and characters. */
141
+ const READ_OUTPUT_LINES = 2000;
142
+ /**
143
+ * `read_output`: the whole of an output that was cut - a test run's, a command's - read by the id the cut gave, a range
144
+ * of lines or the lines matching a pattern. It reads the kept outputs and nothing else.
145
+ */
146
+ export function readOutputTool(dir) {
147
+ return defineTool({
148
+ name: "read_output",
149
+ label: "Read output",
150
+ description: "Read the whole of an output that was cut short - a test run's or a command's - by the id the cut named: a range of " +
151
+ "lines (offset, from 1, and limit), or only the lines matching a pattern (a regular expression). Each line comes " +
152
+ "with its number.",
153
+ promptSnippet: "read_output: the whole of a cut output, by the id the cut named",
154
+ parameters: Type.Object({
155
+ id: Type.String({ description: "The id the cut output named, e.g. 2026-10-05T10-00-00-000Z-run_unittests.sh.log" }),
156
+ offset: Type.Optional(Type.Number({ description: "The first line to show, from 1" })),
157
+ limit: Type.Optional(Type.Number({ description: `How many lines to show (at most ${READ_OUTPUT_LINES})` })),
158
+ pattern: Type.Optional(Type.String({ description: "Show only the lines matching this regular expression" })),
159
+ }),
160
+ async execute(_id, params) {
161
+ const logs = outputsDir(dir);
162
+ const file = join(logs, params.id);
163
+ if (basename(params.id) !== params.id || !existsSync(file)) {
164
+ throw new Error(`${params.id} is not an output turri kept. Use the id a cut output named, as it named it.`);
165
+ }
166
+ const lines = maskSecrets(readFileSync(file, "utf8")).split("\n");
167
+ const numbered = lines.map((line, index) => ({ line, number: index + 1 }));
168
+ let text;
169
+ if (params.pattern !== undefined) {
170
+ const pattern = new RegExp(params.pattern);
171
+ const matching = numbered.filter(({ line }) => pattern.test(line));
172
+ const shown = matching.slice(0, READ_OUTPUT_LINES);
173
+ const more = matching.length > shown.length ? `, the first ${shown.length} shown` : "";
174
+ text = [...shown.map(({ line, number }) => `${number}: ${line}`), `[${matching.length} line${matching.length === 1 ? "" : "s"} of ${lines.length} match${matching.length === 1 ? "es" : ""} ${params.pattern}${more}]`].join("\n");
175
+ }
176
+ else {
177
+ const from = Math.max(1, Math.floor(params.offset ?? 1));
178
+ const count = Math.min(READ_OUTPUT_LINES, Math.max(1, Math.floor(params.limit ?? READ_OUTPUT_LINES)));
179
+ const shown = numbered.slice(from - 1, from - 1 + count);
180
+ const last = from + shown.length - 1;
181
+ text = [...shown.map(({ line, number }) => `${number}: ${line}`), `[lines ${from}-${last} of ${lines.length}]`].join("\n");
182
+ }
183
+ return { content: [{ type: "text", text: truncateOutput(text) }], details: { id: params.id } };
184
+ },
185
+ });
186
+ }
187
+ /** Output as an agent is shown it, with the whole of it kept in a file when it had to be cut. */
188
+ export function shownOutput(dir, label, output) {
189
+ return truncateOutput(output, keepFullOutput(dir, label, output));
190
+ }
191
+ /**
192
+ * Never part of the codebase: version control, installed dependencies, tool caches, and the
193
+ * harness's own bookkeeping. Installed dependencies alone can be tens of thousands of files.
194
+ */
195
+ export const NOT_CODE = new Set([
196
+ ".git",
197
+ "node_modules",
198
+ ".venv",
199
+ "venv",
200
+ "__pycache__",
201
+ ".pytest_cache",
202
+ ".mypy_cache",
203
+ ".ruff_cache",
204
+ ".turri",
205
+ "target",
206
+ ".cargo",
207
+ // Working notes for the agents: not part of the codebase either (see notes.ts).
208
+ NOTES_FILE,
209
+ RECORD_FILE,
210
+ ]);
211
+ export function listFiles(root) {
212
+ const found = [];
213
+ const walk = (dir) => {
214
+ for (const entry of readdirSync(dir)) {
215
+ // The state file is the harness's own bookkeeping, not part of the codebase: it is
216
+ // never shown to an agent as a file it wrote, copied forward to a requiring module,
217
+ // or counted when asking whether anything has been built.
218
+ if (NOT_CODE.has(entry))
219
+ continue;
220
+ const full = join(dir, entry);
221
+ if (statSync(full).isDirectory())
222
+ walk(full);
223
+ else
224
+ found.push(relative(root, full));
225
+ }
226
+ };
227
+ if (existsSync(root))
228
+ walk(root);
229
+ return found.sort();
230
+ }
231
+ /** A tool that runs the tests and lets the harness see the result, rather than taking the agent's word. */
232
+ export const TEST_FILE_PATTERN = /(^|[._/-])(test|spec)|test[._-]|Test\.|\.test\.|_test\./i;
233
+ const TEST_CASE_PATTERN = /\bdef\s+test\w*|\bit\s*\(|\btest\s*\(|@Test\b|\bfunc\s+Test\w*/g;
234
+ /**
235
+ * Which files look like tests, and roughly how many cases each holds.
236
+ *
237
+ * The count is a heuristic over common test idioms across languages. It is not a parser -
238
+ * it exists to notice that tests went missing between one turn and the next, which is a
239
+ * question worth asking even when the estimate is a case or two off.
240
+ */
241
+ export function inventoryTests(root) {
242
+ const inventory = new Map();
243
+ for (const file of listFiles(root)) {
244
+ if (!TEST_FILE_PATTERN.test(basename(file)))
245
+ continue;
246
+ let content;
247
+ try {
248
+ content = readFileSync(join(root, file), "utf8");
249
+ }
250
+ catch {
251
+ continue;
252
+ }
253
+ inventory.set(file, (content.match(TEST_CASE_PATTERN) ?? []).length);
254
+ }
255
+ return inventory;
256
+ }
257
+ /** Test files that disappeared, or lost cases, between two inventories. */
258
+ export function findTestRemovals(before, after) {
259
+ const removals = [];
260
+ for (const [file, count] of before) {
261
+ const now = after.get(file);
262
+ if (now === undefined)
263
+ removals.push({ file, before: count, after: 0, deleted: true });
264
+ else if (now < count)
265
+ removals.push({ file, before: count, after: now, deleted: false });
266
+ }
267
+ return removals;
268
+ }
269
+ export function describeRemovals(removals) {
270
+ return removals
271
+ .map((removal) => removal.deleted
272
+ ? `- ${removal.file} was deleted (it held ~${removal.before} tests)`
273
+ : `- ${removal.file} went from ~${removal.before} to ~${removal.after} tests`)
274
+ .join("\n");
275
+ }
276
+ /**
277
+ * A deleted test is a silent way to make a suite pass. The rule is narrow on purpose:
278
+ * a test may go only when it asserts nothing meaningful, never because it is failing.
279
+ */
280
+ export function removalChallengePrompt(removals, subject) {
281
+ return [
282
+ `Tests disappeared while you were working:`,
283
+ "",
284
+ describeRemovals(removals),
285
+ "",
286
+ `Removing ${subject} is only acceptable when a test asserts nothing meaningful - a test that`,
287
+ "cannot fail, duplicates another exactly, or tests a thing the specification does not require.",
288
+ "A test that is inconvenient, slow, or currently failing is not a reason to remove it.",
289
+ "",
290
+ "Restore every test you removed, unless it meets that bar. If a removal does meet it, keep it",
291
+ "removed and state plainly which test it was and why it asserted nothing.",
292
+ ].join("\n");
293
+ }
294
+ /**
295
+ * How many times a coding agent that ended its turn having written nothing is asked for a draft: once. Thinking a
296
+ * problem through and ending the turn with nothing written leaves nothing to test or review; a best guess does. A
297
+ * change that only rewords or describes may need no code, and the agent says so.
298
+ */
299
+ export const DRAFT_ASKS = 1;
300
+ /** For a coding agent that has written nothing yet: write a best-guess draft now, or say why no code is needed. */
301
+ export function draftPrompt() {
302
+ return [
303
+ "You have not written anything yet: the code folder is as it was when you started.",
304
+ "",
305
+ "If the change needs code - anything it adds to or changes in a functional spec, an implementation req or an",
306
+ "acceptance test - stop planning and write it now: your best guess at what the change asks, the code and its",
307
+ "unit tests, as a draft. If you are sure it needs no code at all - it only rewords, describes or renames - say so",
308
+ "in one line, and why.",
309
+ "Where the specification leaves something open, pick the most likely reading, write it, and note the assumption",
310
+ "with update_notes. Then run the unit tests.",
311
+ ].join("\n");
312
+ }
313
+ /**
314
+ * The harness's own run of the unit tests, which is what "the tests pass" rests on.
315
+ *
316
+ * The agent runs whatever commands it likes while it works. Whether the code is done is the
317
+ * project's unit testing script, run by the harness on the code as it now is - the same script,
318
+ * the same way, every time (see test-scripts.ts).
319
+ */
320
+ export async function verifyUnitSuite(buildDir, runs, script) {
321
+ const run = await runUnitScript(buildDir, script);
322
+ runs.push({ command: run.command, exitCode: run.exitCode });
323
+ if (run.exitCode === 0)
324
+ return { ok: true, command: run.command };
325
+ const missing = missingToolchain(run);
326
+ return {
327
+ ok: false,
328
+ command: run.command,
329
+ prompt: scriptFailure(run, "unit testing"),
330
+ run,
331
+ ...(missing !== undefined ? { missing } : {}),
332
+ };
333
+ }
334
+ /** Every file of the test code under `dirs`, by path, as it is now. */
335
+ export function snapshotFiles(dirs) {
336
+ const snapshot = new Map();
337
+ for (const dir of dirs) {
338
+ for (const file of listFiles(dir))
339
+ snapshot.set(join(dir, file), readFileSync(join(dir, file)));
340
+ }
341
+ return snapshot;
342
+ }
343
+ /** The files under `dirs` that differ from the snapshot: changed, added or deleted. */
344
+ export function changedSince(snapshot, dirs) {
345
+ const now = snapshotFiles(dirs);
346
+ const changed = new Set();
347
+ for (const [path, content] of now) {
348
+ const before = snapshot.get(path);
349
+ if (before === undefined || !before.equals(content))
350
+ changed.add(path);
351
+ }
352
+ for (const path of snapshot.keys())
353
+ if (!now.has(path))
354
+ changed.add(path);
355
+ return [...changed].sort();
356
+ }
357
+ /** Put the files under `dirs` back to the snapshot: its files restored, any others removed. */
358
+ export function restoreSnapshot(snapshot, dirs) {
359
+ for (const path of changedSince(snapshot, dirs)) {
360
+ const before = snapshot.get(path);
361
+ if (before === undefined)
362
+ rmSync(path, { force: true });
363
+ else {
364
+ mkdirSync(dirname(path), { recursive: true });
365
+ writeFileSync(path, before);
366
+ }
367
+ }
368
+ }
369
+ const TEST_GUARD_ROUNDS = 3;
370
+ /**
371
+ * Run an agent that fixes code with the conformance tests out of its reach - by the harness,
372
+ * not by its file access, which covers the whole of `plain_modules`.
373
+ *
374
+ * The tests say what the specification requires; an agent told to make them pass could
375
+ * otherwise do it by changing a test. So they are photographed before it starts and compared
376
+ * when it finishes. Anything it changed is put back, and it is sent to fix the code instead,
377
+ * until it leaves the tests alone or the rounds run out - after which the tests are put back
378
+ * all the same. Returns the agent's last report and every test file it had to have restored.
379
+ */
380
+ export async function keepingTestsUnchanged(input) {
381
+ const dirs = input.dirs.filter((dir) => existsSync(dir));
382
+ const snapshot = snapshotFiles(dirs);
383
+ let report = await input.start();
384
+ const restored = new Set();
385
+ for (let round = 0; round < TEST_GUARD_ROUNDS; round += 1) {
386
+ const changed = changedSince(snapshot, dirs);
387
+ if (changed.length === 0)
388
+ break;
389
+ restoreSnapshot(snapshot, dirs);
390
+ for (const path of changed)
391
+ restored.add(path);
392
+ reportActivity(`${input.label}: the fixer changed ${changed.length} conformance test file${changed.length === 1 ? "" : "s"} - put back`);
393
+ if (round === TEST_GUARD_ROUNDS - 1)
394
+ break;
395
+ report = await input.again([
396
+ "You changed conformance tests, which you must not do:",
397
+ "",
398
+ ...changed.map((path) => `- ${path}`),
399
+ "",
400
+ "They have been put back as they were. The tests state what the specification requires; a failing",
401
+ "test is fixed by changing the code so the behaviour is what the specification describes. Fix the",
402
+ "code, and leave every conformance test as it is.",
403
+ ].join("\n"));
404
+ }
405
+ return { report, restored: [...restored].sort() };
406
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ import { missingToolchain, runUnitScript, scriptFailure, staticChecksOnly } from "./test-scripts.js";
2
+ export function createUnitGate(buildDir, unit) {
3
+ return {
4
+ async verify(runs) {
5
+ const run = await runUnitScript(buildDir, unit);
6
+ runs.push({ command: run.command, exitCode: run.exitCode });
7
+ if (run.exitCode === 0)
8
+ return { ok: true };
9
+ const missing = run.timedOut === true ? undefined : missingToolchain(run);
10
+ return {
11
+ ok: false,
12
+ prompt: scriptFailure(run, "unit testing"),
13
+ run,
14
+ script: unit,
15
+ staticOnly: missing === undefined && staticChecksOnly(run),
16
+ ...(missing !== undefined ? { missing } : {}),
17
+ };
18
+ },
19
+ };
20
+ }