@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,95 @@
1
+ /**
2
+ * The values of the credentials a project passes to its agents (`agent-env:`) never appear in what an agent is shown
3
+ * or what turri writes: each is replaced by its name, `[D365_CLIENT_SECRET]` - whole, URL-encoded, or split over the
4
+ * chunks of a stream. A value is registered when the agents' environment is worked out (`agentEnvironment`), and masked
5
+ * from then on everywhere in this process: a command's output as it streams, every tool result an agent is given and
6
+ * every refusal, the kept whole outputs, the job and debug logs - and so the session transcripts, which hold only what
7
+ * the tools returned.
8
+ */
9
+ /** Shorter values are not masked: `1`, `eu`, `true` are everywhere in ordinary output. */
10
+ const MIN_LENGTH = 4;
11
+ /** Each form a value is masked in, to its name - longest first, so a value inside another is not cut in two. */
12
+ let forms = [];
13
+ /** Mask these values, by name, from now on. */
14
+ export function registerSecrets(values) {
15
+ const all = new Map(forms);
16
+ for (const [name, value] of Object.entries(values)) {
17
+ if (value.length < MIN_LENGTH)
18
+ continue;
19
+ for (const form of [value, encodeURIComponent(value), encodeURIComponent(value).replace(/%20/g, "+")])
20
+ all.set(form, name);
21
+ }
22
+ forms = [...all].sort(([a], [b]) => b.length - a.length);
23
+ }
24
+ /** For tests: mask nothing. */
25
+ export function forgetSecretsForTests() {
26
+ forms = [];
27
+ }
28
+ /** The longest form registered: how much of a stream has to be held back to catch one split over chunks. */
29
+ function longest() {
30
+ return forms[0]?.[0].length ?? 0;
31
+ }
32
+ /** `text` with every registered value replaced by its name. */
33
+ export function maskSecrets(text) {
34
+ if (forms.length === 0)
35
+ return text;
36
+ let masked = text;
37
+ for (const [form, name] of forms)
38
+ if (masked.includes(form))
39
+ masked = masked.split(form).join(`[${name}]`);
40
+ return masked;
41
+ }
42
+ /**
43
+ * A stream that masks what passes through it before `onData` sees it: the end of each chunk is held back - as long as
44
+ * the longest value, less one - until the next shows whether a value runs across them. `end` lets the rest out.
45
+ */
46
+ export function maskingStream(onData) {
47
+ let held = "";
48
+ return {
49
+ write(data) {
50
+ if (forms.length === 0 && held === "") {
51
+ onData(data);
52
+ return;
53
+ }
54
+ const text = maskSecrets(held + data.toString("utf8"));
55
+ const keep = Math.max(0, longest() - 1);
56
+ const cut = Math.max(0, text.length - keep);
57
+ held = text.slice(cut);
58
+ if (cut > 0)
59
+ onData(Buffer.from(text.slice(0, cut)));
60
+ },
61
+ end() {
62
+ if (held !== "")
63
+ onData(Buffer.from(maskSecrets(held)));
64
+ held = "";
65
+ },
66
+ };
67
+ }
68
+ /** The tools, each result they return - and each error they throw, a refusal - masked before the agent sees it. */
69
+ export function maskToolResults(tools) {
70
+ return tools.map((tool) => ({
71
+ ...tool,
72
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
73
+ const update = onUpdate === undefined ? undefined : (partial) => onUpdate(maskResult(partial));
74
+ try {
75
+ return maskResult(await tool.execute(toolCallId, params, signal, update, ctx));
76
+ }
77
+ catch (error) {
78
+ if (error instanceof Error)
79
+ error.message = maskSecrets(error.message);
80
+ throw error;
81
+ }
82
+ },
83
+ }));
84
+ }
85
+ function maskResult(result) {
86
+ if (forms.length === 0 || result === null || typeof result !== "object")
87
+ return result;
88
+ const content = result.content;
89
+ if (!Array.isArray(content))
90
+ return result;
91
+ return {
92
+ ...result,
93
+ content: content.map((part) => (part?.type === "text" && typeof part.text === "string" ? { ...part, text: maskSecrets(part.text) } : part)),
94
+ };
95
+ }
@@ -0,0 +1,397 @@
1
+ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, join } from "node:path";
3
+ import { defineTool } from "@earendil-works/pi-coding-agent";
4
+ import { Type } from "typebox";
5
+ import { reportActivity } from "./activity.js";
6
+ /**
7
+ * Working notes: what one agent on a codebase leaves the next.
8
+ *
9
+ * Every agent the harness starts is a fresh session. Without notes, each one spends its first
10
+ * turns finding its bearings - where the entry point is, how the tests run, what was installed,
11
+ * which approach already failed. The notes carry that forward: each agent on a codebase gets
12
+ * them the moment it starts, and updates them - a line added, changed or removed - before it
13
+ * finishes, so they get longer with the codebase instead of being rewritten by whoever came last.
14
+ *
15
+ * Stubborn on the specs, flexible on the assumptions. What the software must do is in the spec,
16
+ * and the notes never repeat or override it. Where the spec is silent, an agent has to assume
17
+ * something, and the assumptions it made are written down - so the next agent builds on the
18
+ * same ones instead of guessing afresh, and changes one the moment the code or the spec shows
19
+ * it to be wrong.
20
+ *
21
+ * Every line says when and where it came from - `[2026-09-24 16:49 · backend 3]` for
22
+ * functionality 3 of backend, `backend 3 conformance`, `backend spec change` - stamped by the
23
+ * harness, not written by the agent, so a line read two modules later can still be traced to the
24
+ * step that wrote it. A line that is changed is stamped again: an assumption one agent revised
25
+ * shows as newer than the ones around it, and where two lines disagree the later one is right. And every line
26
+ * has to say something: "none", "N/A", "nothing new" are refused - a section with nothing to add
27
+ * is left alone.
28
+ *
29
+ * Leaving them is not optional, and neither is how: only update_notes writes the file. The file
30
+ * tools refuse it, a change made any other way - a shell, a script - is undone, and an agent
31
+ * that finishes without calling update_notes is asked again until it does.
32
+ *
33
+ * The notes are disposable: delete the file and nothing breaks, the next agent just explores a
34
+ * little more. They live beside the code, so they travel with it - copied up with it to a module
35
+ * that requires this one, committed with it, and reset with it when a run is interrupted.
36
+ */
37
+ export const NOTES_FILE = ".turri-notes.md";
38
+ /** The sections, in order, and what each is for - shown to the agent as the heading. */
39
+ export const SECTIONS = {
40
+ where: "Where things are",
41
+ run: "How to build, run and test",
42
+ assumptions: "Assumptions",
43
+ gaps: "Spec gaps",
44
+ pitfalls: "Pitfalls",
45
+ pulses: "Pulses",
46
+ };
47
+ /** The sections only the harness writes: what each pulse did, as it ended. update_notes refuses them. */
48
+ const HARNESS_ONLY = ["pulses"];
49
+ /** How many pulses the notes keep: the last ones - the module's history has every one. */
50
+ export const PULSES_KEPT = 10;
51
+ const ORDER = Object.keys(SECTIONS);
52
+ export function notesPath(buildDir) {
53
+ return join(buildDir, NOTES_FILE);
54
+ }
55
+ function empty() {
56
+ return { where: [], run: [], assumptions: [], gaps: [], pitfalls: [], pulses: [] };
57
+ }
58
+ /** The notes, as lines per section. Lines under no known heading go to pitfalls, not lost. */
59
+ export function parseNotes(text) {
60
+ const notes = empty();
61
+ let section = "pitfalls";
62
+ for (const raw of text.split("\n")) {
63
+ const line = raw.trim();
64
+ const heading = /^##\s+(.+)$/.exec(line);
65
+ if (heading) {
66
+ section = ORDER.find((key) => SECTIONS[key].toLowerCase() === heading[1].trim().toLowerCase()) ?? section;
67
+ continue;
68
+ }
69
+ const item = /^[-*]\s+(.+)$/.exec(line);
70
+ if (item)
71
+ notes[section].push(item[1].trim());
72
+ }
73
+ return notes;
74
+ }
75
+ export function formatNotes(notes) {
76
+ const parts = ["# Working notes", ""];
77
+ for (const key of ORDER) {
78
+ parts.push(`## ${SECTIONS[key]}`, "", ...(notes[key].length > 0 ? notes[key].map((line) => `- ${line}`) : ["(nothing yet)"]), "");
79
+ }
80
+ return parts.join("\n").trimEnd() + "\n";
81
+ }
82
+ export function readNotes(buildDir) {
83
+ const path = notesPath(buildDir);
84
+ return existsSync(path) ? parseNotes(readFileSync(path, "utf8")) : empty();
85
+ }
86
+ function isEmpty(notes) {
87
+ return ORDER.every((key) => notes[key].length === 0);
88
+ }
89
+ const GUIDANCE = [
90
+ "Before you finish, call update_notes to leave the next agent on this codebase what you learned - a fresh",
91
+ "session that knows nothing of your work. Update them, do not rewrite them: add a line, change one that is",
92
+ "no longer true, remove one that is wrong. In short lines, per section:",
93
+ `- ${SECTIONS.where}: entry points, layout, where each functionality and its tests live.`,
94
+ `- ${SECTIONS.run}: the commands that build, run and test it, and what had to be installed for them.`,
95
+ `- ${SECTIONS.assumptions}: what you assumed where the specification is silent - a format, a default, a`,
96
+ " choice it leaves open. Build on the assumptions already there; change or remove one the moment the code",
97
+ " or the specification shows it wrong.",
98
+ `- ${SECTIONS.gaps}: what the specification should say and does not - a behaviour you had to decide that the`,
99
+ " person would want to decide, not a detail of how. Each is put to the person, to be written into the",
100
+ " specification. One a line, as the question it leaves: \"What happens to a contact archived in HubSpot after it",
101
+ " was synced?\"",
102
+ `- ${SECTIONS.pitfalls}: what went wrong and what fixed it, approaches that did not work.`,
103
+ `- ${SECTIONS.pulses}: written by turri as each pulse ends - read them, never write them.`,
104
+ "Each line stands on its own, for someone who never saw your work. When and where it came from is added",
105
+ "to it for you - do not write it. Where two lines disagree, the later one is right: change the older one",
106
+ 'rather than add a contradicting line. Leave a section with nothing new alone: "none" is not a note.',
107
+ "Stubborn on the specification, flexible on the assumptions: the specification is never an assumption, and",
108
+ "never copied here. Where an assumption and the specification disagree, the specification is right.",
109
+ ].join("\n");
110
+ /** A prompt, led by the notes earlier agents left - and told to update them for the next. */
111
+ export function withNotes(prompt, buildDir) {
112
+ const notes = readNotes(buildDir);
113
+ const earlier = isEmpty(notes)
114
+ ? "No earlier agent has left notes on this codebase yet."
115
+ : [
116
+ "<notes from earlier agents>",
117
+ formatNotes(notes).trimEnd(),
118
+ "</notes from earlier agents>",
119
+ "",
120
+ "Earlier agents on this codebase wrote these down, to spare you exploring from scratch; they may be out of",
121
+ "date - how to weigh them is below. The pitfalls include what earlier reviews sent tests back for: write your",
122
+ "tests so they would pass those reviews the first time.",
123
+ ].join("\n");
124
+ return `${earlier}\n\n${prompt}\n\n${GUIDANCE}`;
125
+ }
126
+ /**
127
+ * What an agent a review sent tests back to is asked to leave: the lesson, not the fix - the kind of
128
+ * test the review found weak or missing, and how to write it instead - so every later agent on the
129
+ * codebase writes it right from its first test, and the first reviews have less to send back.
130
+ */
131
+ export const REVIEW_LESSON = [
132
+ "",
133
+ "Then leave the lesson for the agents after you: call update_notes and add to the pitfalls what this review taught",
134
+ "you - the kind of test it found weak or missing, and how to write it instead - as a rule the next agent can follow",
135
+ "from its first test, not a record of this one fix: e.g. \"Tests assert the exact response body and status, never only",
136
+ "that one came back\". One short line per lesson; skip one the notes already have, and sharpen one that was not enough.",
137
+ ];
138
+ /**
139
+ * After an agent worked on what a review found: when it left no new pitfall - no lesson - it is asked
140
+ * once more, so the next agent does not make the same tests the review has to send back again.
141
+ */
142
+ export async function reflectOnReview(buildDir, pitfallsBefore, send) {
143
+ const known = new Set(pitfallsBefore);
144
+ if (readNotes(buildDir).pitfalls.some((line) => !known.has(line)))
145
+ return;
146
+ await send([
147
+ "You worked on what the review found, but left no lesson in the notes. Call update_notes now and add to the pitfalls",
148
+ "the kind of test the review found weak or missing, and how to write it instead - a rule the next agent on this",
149
+ "codebase can follow from its first test, so the review does not have to send the same back again. Change nothing else.",
150
+ ].join("\n"));
151
+ }
152
+ const SOURCE = /^\[[^\]\n]+\]\s+/;
153
+ /** A line without the `[where it came from]` in front. */
154
+ function body(line) {
155
+ return line.trim().replace(/^[-*]\s+/, "").replace(SOURCE, "");
156
+ }
157
+ /**
158
+ * What says nothing: a line whose whole point is that there is nothing to say. Kept narrow, so
159
+ * "None of the endpoints need auth" is still a note.
160
+ */
161
+ const FILLER = [
162
+ /^(none|n\/a|na|nothing|not applicable)( (specifically|so far|yet|found|noted|new|here|at all|for (this|now|the)|to (add|note|report))\b.*)?[.!]?$/i,
163
+ /^no (new |other |specific )?(pitfalls?|assumptions?|notes?|issues?|problems?)( (so far|yet|found|noted|here|for (this|now|the))\b.*)?[.!]?$/i,
164
+ ];
165
+ export function isFiller(text) {
166
+ const line = body(text);
167
+ return line === "" || FILLER.some((pattern) => pattern.test(line));
168
+ }
169
+ /** A line in a section that is, or starts with, what the agent quoted - where it came from aside. */
170
+ function find(lines, quoted) {
171
+ const wanted = body(quoted);
172
+ const exact = lines.findIndex((line) => body(line) === wanted);
173
+ return exact !== -1 ? exact : lines.findIndex((line) => body(line).startsWith(wanted));
174
+ }
175
+ /** A note about running the project's testing scripts by hand. */
176
+ const RUNS_TESTING_SCRIPTS = /test_scripts\/|\brun_(unittests|unit_tests|conformance_tests)\w*\.sh\b/;
177
+ /**
178
+ * Apply the changes; the notes that result, or what could not be applied. A line added or
179
+ * changed is stamped with `stamp` - when, and which step wrote it.
180
+ */
181
+ export function applyNotesChanges(notes, changes, stamp) {
182
+ const next = Object.fromEntries(ORDER.map((key) => [key, [...(notes[key] ?? [])]]));
183
+ const problems = [];
184
+ for (const change of changes) {
185
+ const lines = next[change.section];
186
+ const said = body(change.text.replace(/\s*\n\s*/g, " "));
187
+ const text = stamp ? `[${stamp}] ${said}` : said;
188
+ // How the tests are run is the harness's: a note telling the next agent to run the project's
189
+ // testing scripts itself sends it looking for files it cannot reach, and writing its own.
190
+ if (change.action !== "remove" && RUNS_TESTING_SCRIPTS.test(said)) {
191
+ problems.push(`"${change.text.trim()}" tells the next agent to run the testing scripts itself. It cannot: they are not in the ` +
192
+ "codebase, and run_unit_tests and run_conformance_tests are how the tests are run. Leave that out.");
193
+ continue;
194
+ }
195
+ if (change.action !== "remove" && isFiller(said)) {
196
+ problems.push(`"${change.text.trim()}" in ${SECTIONS[change.section]} is not a note. Leave a section with nothing new alone.`);
197
+ continue;
198
+ }
199
+ if (change.action === "add") {
200
+ if (!lines.some((line) => body(line) === said))
201
+ lines.push(text);
202
+ continue;
203
+ }
204
+ const target = change.action === "change" ? change.replaces ?? "" : text;
205
+ if (target.trim() === "") {
206
+ problems.push(`A ${change.action} in ${SECTIONS[change.section]} has to name the line it ${change.action === "change" ? "replaces, in `replaces`" : "removes"}.`);
207
+ continue;
208
+ }
209
+ const at = find(lines, target);
210
+ if (at === -1) {
211
+ problems.push(`${SECTIONS[change.section]} has no line "${target}". Its lines now:\n` +
212
+ (lines.length > 0 ? lines.map((line) => ` - ${line}`).join("\n") : " (none)"));
213
+ continue;
214
+ }
215
+ if (change.action === "remove")
216
+ lines.splice(at, 1);
217
+ else
218
+ lines[at] = text;
219
+ }
220
+ return { notes: next, problems };
221
+ }
222
+ /** The local time, to the minute: `2026-09-24 16:49`. */
223
+ function now() {
224
+ const d = new Date();
225
+ const two = (n) => String(n).padStart(2, "0");
226
+ return `${d.getFullYear()}-${two(d.getMonth() + 1)}-${two(d.getDate())} ${two(d.getHours())}:${two(d.getMinutes())}`;
227
+ }
228
+ /** `update_notes`: lines added, changed or removed, never the notes rewritten. */
229
+ export function createNotesTool(buildDir, onWrite, before = () => { }, source) {
230
+ return defineTool({
231
+ name: "update_notes",
232
+ label: "Update notes",
233
+ description: "Update the working notes on this codebase for the next agent - add a line, change one that is no longer " +
234
+ "true, remove one that is wrong - in its sections: where things are, how to build, run and test, " +
235
+ "assumptions made where the specification is silent, spec gaps the person should decide, and pitfalls. Never " +
236
+ "requirements: the specification is never an assumption.",
237
+ promptSnippet: "leave the next agent what you learned and assumed about this codebase",
238
+ parameters: Type.Object({
239
+ changes: Type.Array(Type.Object({
240
+ // Any wording of a section is taken - "where", "Where things are" - rather than failing the whole call.
241
+ section: Type.String({
242
+ description: `One of: ${ORDER.map((key) => `${key} (${SECTIONS[key]})`).join(", ")}`,
243
+ }),
244
+ action: Type.Union([Type.Literal("add"), Type.Literal("change"), Type.Literal("remove")]),
245
+ text: Type.String({
246
+ description: "The line to add, the new line for a change, or the line to remove - one short line that stands on its " +
247
+ "own. When and where it came from is added for you.",
248
+ }),
249
+ replaces: Type.Optional(Type.String({ description: "For a change: the line it replaces, as it reads now" })),
250
+ }), { minItems: 1 }),
251
+ }),
252
+ async execute(_id, params) {
253
+ before();
254
+ const unknown = params.changes.filter((change) => sectionNamed(change.section) === undefined);
255
+ if (unknown.length > 0) {
256
+ throw new Error(`Nothing saved - ${unknown.map((change) => `"${change.section}"`).join(", ")} is not a section. ` +
257
+ `The sections are: ${ORDER.map((key) => `${key} (${SECTIONS[key]})`).join(", ")}.`);
258
+ }
259
+ const changes = params.changes.map((change) => ({ ...change, section: sectionNamed(change.section) }));
260
+ const theirs = changes.filter((change) => HARNESS_ONLY.includes(change.section));
261
+ if (theirs.length > 0) {
262
+ throw new Error(`Nothing saved - ${theirs.map((change) => SECTIONS[change.section]).join(", ")} is written by turri as each pulse ends, not by you.`);
263
+ }
264
+ const { notes, problems } = applyNotesChanges(readNotes(buildDir), changes, source === undefined ? undefined : `${now()} · ${source}`);
265
+ if (problems.length > 0) {
266
+ throw new Error(`Nothing saved - fix these and call update_notes again:\n\n${problems.join("\n\n")}`);
267
+ }
268
+ const text = formatNotes(notes);
269
+ writeFileSync(notesPath(buildDir), text);
270
+ onWrite(text);
271
+ return { content: [{ type: "text", text: "Notes updated." }], details: { characters: text.length } };
272
+ },
273
+ });
274
+ }
275
+ /** The section a name means - its key or its heading, in any case, or the heading's first words. */
276
+ export function sectionNamed(name) {
277
+ const wanted = name.trim().toLowerCase().replace(/[^a-z, ]+/g, " ").replace(/\s+/g, " ").trim();
278
+ if (wanted === "")
279
+ return undefined;
280
+ return (ORDER.find((key) => key === wanted || SECTIONS[key].toLowerCase() === wanted) ??
281
+ ORDER.find((key) => SECTIONS[key].toLowerCase().startsWith(wanted) || wanted.startsWith(key)));
282
+ }
283
+ /** How many times an agent that finishes without update_notes is asked to call it. */
284
+ export const NOTES_ASKS = 3;
285
+ /** The turn that asks for the notes: update_notes is the only tool it may use - the work it follows is done. */
286
+ export const NOTES_TURN = { onlyTools: ["update_notes"] };
287
+ /** What the notes turn says first, once the harness's own run of `script` has passed on the work. */
288
+ export function passedOutcome(script) {
289
+ return (`The harness ran the project's ${script} on your work itself, and it passed: your work is done and accepted. ` +
290
+ "Write the notes as of work that succeeded - never that the tests could not be made to pass.");
291
+ }
292
+ function current(buildDir) {
293
+ const path = notesPath(buildDir);
294
+ return existsSync(path) ? readFileSync(path, "utf8") : undefined;
295
+ }
296
+ /**
297
+ * A line the harness itself writes into the notes - an attempt at a failing conformance run, what
298
+ * was judged wrong and what was done about it - stamped like an agent's, and kept as update_notes
299
+ * would keep it, so no session takes it for a direct write. Never fails the work it records.
300
+ */
301
+ export function recordNote(buildDir, section, text, source) {
302
+ try {
303
+ const { notes, problems } = applyNotesChanges(readNotes(buildDir), [{ section, action: "add", text }], `${now()} · ${source}`);
304
+ if (problems.length > 0)
305
+ return;
306
+ const formatted = formatNotes(notes);
307
+ writeFileSync(notesPath(buildDir), formatted);
308
+ knownNotes.set(notesPath(buildDir), formatted);
309
+ }
310
+ catch {
311
+ // The notes are a help, not the work.
312
+ }
313
+ }
314
+ /**
315
+ * A pulse's line, written by the harness as the pulse ends - landed or not: what it regenerated, how it went, where its
316
+ * log is. The last PULSES_KEPT stay; the module's history has every pulse.
317
+ */
318
+ export function recordPulse(buildDir, text, source) {
319
+ try {
320
+ const notes = readNotes(buildDir);
321
+ notes.pulses = [...notes.pulses, `[${now()} · ${source}] ${text.replace(/\s+/g, " ").trim()}`].slice(-PULSES_KEPT);
322
+ const formatted = formatNotes(notes);
323
+ writeFileSync(notesPath(buildDir), formatted);
324
+ knownNotes.set(notesPath(buildDir), formatted);
325
+ }
326
+ catch {
327
+ // The notes are a help, not the work.
328
+ }
329
+ }
330
+ /** The lines of a section added since `before` - a pulse's new spec gaps, say - without when and where they came from. */
331
+ export function linesAddedSince(before, buildDir, section) {
332
+ const known = new Set(before[section]);
333
+ return readNotes(buildDir)[section].filter((line) => !known.has(line)).map((line) => line.replace(/^\[[^\]]*\]\s*/, ""));
334
+ }
335
+ /** The notes, for an agent that reads them but leaves none - a judge. Empty when there are none. */
336
+ export function notesToRead(buildDir) {
337
+ const notes = readNotes(buildDir);
338
+ return isEmpty(notes) ? "" : `<notes from earlier agents>\n${formatNotes(notes).trimEnd()}\n</notes from earlier agents>`;
339
+ }
340
+ /**
341
+ * The notes tool for one agent's session, and the check that it was used. The notes as they
342
+ * were when the agent started, or as update_notes last left them, are the only notes: a change
343
+ * made to the file any other way is undone. An agent that finishes without calling update_notes
344
+ * is asked to, until it has. `source` is the step it is - `backend 3`, `backend 3 conformance` -
345
+ * and every line it writes says so, with the time.
346
+ */
347
+ /**
348
+ * The notes as update_notes last left them, by file - shared by every session on the codebase, so
349
+ * one agent's update is not taken for another's direct write. Two agents can work one codebase in
350
+ * the same run: the conformance agent, and the coding agent it sends the code to.
351
+ */
352
+ const knownNotes = new Map();
353
+ export function notesForSession(buildDir, source) {
354
+ let written = false;
355
+ const file = notesPath(buildDir);
356
+ // What is there when the session starts is what it starts from; after that, update_notes says.
357
+ knownNotes.set(file, current(buildDir));
358
+ /** Put back what update_notes did not write; whether there was anything to put back. */
359
+ const undoDirectWrites = () => {
360
+ const known = knownNotes.get(file);
361
+ if (current(buildDir) === known)
362
+ return false;
363
+ if (known === undefined)
364
+ rmSync(file, { force: true });
365
+ else
366
+ writeFileSync(file, known);
367
+ return true;
368
+ };
369
+ return {
370
+ tool: createNotesTool(buildDir, (text) => {
371
+ written = true;
372
+ knownNotes.set(file, text);
373
+ }, undoDirectWrites, source),
374
+ /**
375
+ * Ask for the notes until this agent has updated them; whether it has. `outcome`: how the harness's own check of the
376
+ * work went, said first - so the notes tell the next agent the work passed, never that it could not be made to.
377
+ */
378
+ async ensureWritten(ask, outcome) {
379
+ for (let round = 0; round < NOTES_ASKS && !written; round++) {
380
+ const undone = undoDirectWrites();
381
+ await ask((outcome !== undefined ? `${outcome} ` : "") +
382
+ (undone
383
+ ? `You changed ${NOTES_FILE} directly, and that was undone: the notes are written only through update_notes. `
384
+ : "") +
385
+ "Before you finish: call update_notes - add what you learned about this codebase, change what is no longer " +
386
+ "true, and record the assumptions you made where the specification is silent. Call the tool itself; " +
387
+ "writing the file does not count. Your work is done: this turn is for the notes alone - every other tool is " +
388
+ "refused, so change nothing else.");
389
+ }
390
+ undoDirectWrites();
391
+ if (!written) {
392
+ reportActivity(`${basename(dirname(buildDir))}: its agent left no notes, though asked ${NOTES_ASKS} times`);
393
+ }
394
+ return written;
395
+ },
396
+ };
397
+ }