@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,100 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
+ /**
4
+ * What the conversation itself writes in the project, for the audit to leave alone.
5
+ *
6
+ * The audit puts back what changes in the project's own files while an agent's command runs, taking every change then
7
+ * for the command's. But the conversation goes on beside the pulses: the person's model edits a spec, writes a spec
8
+ * diff or runs a command of its own, and a change of its that lands while an agent's `ls` runs would be put back as the
9
+ * agent's - the person's edit undone, silently. So the conversation's file tools, its shell and the spec diffs it applies
10
+ * say here what they write and when, and the audit leaves a change the conversation made while the command ran.
11
+ *
12
+ * Only the conversation reaches this: the agents of a pulse are started without extensions, so its hooks never see
13
+ * their tools, and their own writes are fenced to their folders.
14
+ */
15
+ /** Anything still marked as in progress after this long is taken to have ended: a hook that never heard its end. */
16
+ const STALE_MS = 15 * 60 * 1000;
17
+ /** Paths being written now, with each write in progress. */
18
+ const writing = new Map();
19
+ /** Paths written, with when the last write ended. */
20
+ const wrote = new Map();
21
+ /** The conversation's commands running now - what they write cannot be known. */
22
+ const commands = new Set();
23
+ let lastCommandEnd = 0;
24
+ /** A path as the audit walks it: absolute, with its symlinks resolved where it exists. */
25
+ function canonical(path) {
26
+ const absolute = resolve(path);
27
+ try {
28
+ return realpathSync(absolute);
29
+ }
30
+ catch {
31
+ try {
32
+ return join(realpathSync(dirname(absolute)), basename(absolute));
33
+ }
34
+ catch {
35
+ return absolute;
36
+ }
37
+ }
38
+ }
39
+ /** The conversation is about to write these files: call what it returns once it has. */
40
+ export function conversationWriting(paths) {
41
+ const write = { started: Date.now() };
42
+ const marked = paths.map(canonical);
43
+ for (const path of marked)
44
+ writing.set(path, (writing.get(path) ?? new Set()).add(write));
45
+ let done = false;
46
+ return () => {
47
+ if (done)
48
+ return;
49
+ done = true;
50
+ const now = Date.now();
51
+ for (const path of marked) {
52
+ const left = writing.get(path);
53
+ left?.delete(write);
54
+ if (left?.size === 0)
55
+ writing.delete(path);
56
+ wrote.set(path, now);
57
+ }
58
+ };
59
+ }
60
+ /** The conversation starts a command of its own: call what it returns once it has ended. */
61
+ export function conversationCommand() {
62
+ const command = { started: Date.now() };
63
+ commands.add(command);
64
+ let done = false;
65
+ return () => {
66
+ if (done)
67
+ return;
68
+ done = true;
69
+ commands.delete(command);
70
+ lastCommandEnd = Date.now();
71
+ };
72
+ }
73
+ /** The conversation's turn is over: nothing of its is in progress any more. */
74
+ export function conversationSettled() {
75
+ const now = Date.now();
76
+ for (const path of writing.keys())
77
+ wrote.set(path, now);
78
+ writing.clear();
79
+ if (commands.size > 0)
80
+ lastCommandEnd = now;
81
+ commands.clear();
82
+ }
83
+ /** Whether the conversation may have written this file after `since` - a write of its to it, or a command of its own. */
84
+ export function writtenByConversation(path, since) {
85
+ const now = Date.now();
86
+ const live = (running) => now - running.started < STALE_MS;
87
+ if ([...commands].some(live) || lastCommandEnd > since)
88
+ return true;
89
+ const key = canonical(path);
90
+ if ([...(writing.get(key) ?? [])].some(live))
91
+ return true;
92
+ return (wrote.get(key) ?? 0) > since;
93
+ }
94
+ /** For tests: forget everything. */
95
+ export function resetConversationWrites() {
96
+ writing.clear();
97
+ wrote.clear();
98
+ commands.clear();
99
+ lastCommandEnd = 0;
100
+ }
@@ -0,0 +1,88 @@
1
+ import { existsSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { basename, join } from "node:path";
3
+ /**
4
+ * Credentials are the person's: no agent writes them. A run that needs one and does not have it is told
5
+ * so - by its judge, to the person - rather than given one an agent made up, which every later run then
6
+ * fails on without anyone seeing why.
7
+ *
8
+ * A credential file is a `.env` or one of its variants (`.env.local`, `.env.test`); a template of one -
9
+ * `.env.example`, `.env.sample`, `.env.template`, `.env.dist` - holds no secret and is not one.
10
+ */
11
+ export function isCredentialFile(path) {
12
+ const name = basename(path);
13
+ if (name === ".env")
14
+ return true;
15
+ const variant = /^\.env\.(.+)$/.exec(name)?.[1];
16
+ return variant !== undefined && !["example", "sample", "template", "dist"].includes(variant.toLowerCase());
17
+ }
18
+ /** What an agent is told when it tries to write one. */
19
+ export function credentialRefusal(path) {
20
+ return (`${basename(path)} holds credentials, and they are the person's to provide - never an agent's to write, ` +
21
+ "not even a placeholder: a made-up one makes every later run fail without anyone seeing why. If the software or its " +
22
+ "tests need a credential that is missing or rejected, say so - in submit_work - and name the variable.");
23
+ }
24
+ /** Folders a credential file is never looked for in: dependencies, caches, histories. */
25
+ const SKIPPED = new Set(["node_modules", ".venv", "venv", ".git", ".plaingit", "cache", "logs", "__pycache__", ".pytest_cache", "target", "build", "dist", ".gradle", ".m2"]);
26
+ const DEPTH = 5;
27
+ /** The credential files under `roots`, with what each holds: what an agent's command is checked against. */
28
+ export function credentialFiles(roots) {
29
+ const found = new Map();
30
+ const visit = (dir, depth) => {
31
+ let entries;
32
+ try {
33
+ entries = readdirSync(dir);
34
+ }
35
+ catch {
36
+ return;
37
+ }
38
+ for (const name of entries) {
39
+ const path = join(dir, name);
40
+ let isDir;
41
+ try {
42
+ isDir = statSync(path).isDirectory();
43
+ }
44
+ catch {
45
+ continue;
46
+ }
47
+ if (isDir) {
48
+ if (depth < DEPTH && !SKIPPED.has(name))
49
+ visit(path, depth + 1);
50
+ }
51
+ else if (isCredentialFile(path)) {
52
+ try {
53
+ found.set(path, readFileSync(path, "utf8"));
54
+ }
55
+ catch {
56
+ /* unreadable: nothing to put back */
57
+ }
58
+ }
59
+ }
60
+ };
61
+ for (const root of roots)
62
+ visit(root, 0);
63
+ return found;
64
+ }
65
+ /**
66
+ * Undo what a command did to the credential files under `roots`, from what they held before it: one it made is
67
+ * removed, one it changed or removed is put back. Says which it put back.
68
+ */
69
+ export function restoreCredentialFiles(before, roots) {
70
+ const after = credentialFiles(roots);
71
+ const restored = [];
72
+ for (const [path, text] of after) {
73
+ if (before.get(path) === text)
74
+ continue;
75
+ if (before.has(path))
76
+ writeFileSync(path, before.get(path));
77
+ else
78
+ rmSync(path, { force: true });
79
+ restored.push(path);
80
+ }
81
+ for (const [path, text] of before) {
82
+ if (after.has(path) || existsSync(path))
83
+ continue;
84
+ writeFileSync(path, text);
85
+ restored.push(path);
86
+ }
87
+ return restored;
88
+ }
@@ -0,0 +1,228 @@
1
+ import { appendFileSync, existsSync } from "node:fs";
2
+ import { maskSecrets } from "./masking.js";
3
+ import { dirname, join, resolve, sep } from "node:path";
4
+ import { currentJob, reportActivity, reportTranscript } from "./activity.js";
5
+ import { ensureInScratch, MODULES_DIR, projectRootOf, scratchDir } from "./paths.js";
6
+ /**
7
+ * Every render agent's whole conversation, streamed to a file of its own as it happens - always.
8
+ *
9
+ * A job's transcript keeps a tool call's arguments to 600 characters and its result to 2000, and only while pi is
10
+ * running. When an agent goes wrong, what it was actually
11
+ * given and what actually came back is what there is to go on. So each agent a render starts -
12
+ * coding, conformance, repair, the spec-steps planner - writes everything to `.turri/debug/`
13
+ * in the project: the system prompt, every prompt, its replies and thinking as they stream, every
14
+ * tool call with its arguments and its whole result, each response's stop reason and usage, and the
15
+ * provider's retries and errors.
16
+ *
17
+ * Each event is on disk the moment it arrives, so a crashed or killed run leaves its log up to
18
+ * the moment it stopped, and `tail -f` follows an agent live. The folder is in the project, not in
19
+ * `plain_modules`, so no agent can read another's log. A log that cannot be written is given up
20
+ * on, never allowed to fail the render.
21
+ */
22
+ /** `.turri/debug` in the project: beside `plain_modules`, never inside it. */
23
+ export function debugDir(project) {
24
+ return join(scratchDir(join(project, MODULES_DIR)), "debug");
25
+ }
26
+ /**
27
+ * Where the log of an agent working in `cwd` goes: always outside what that agent can reach. In
28
+ * `plain_modules`, or in the project that holds it, that is the project's; an explicit build
29
+ * folder elsewhere is its own fence (see `modulesRootOf`), so its logs go beside it, not in it.
30
+ */
31
+ export function debugDirFor(cwd) {
32
+ const dir = resolve(cwd);
33
+ if (dir.split(sep).includes(MODULES_DIR))
34
+ return debugDir(projectRootOf(dir));
35
+ if (existsSync(join(dir, MODULES_DIR)))
36
+ return debugDir(dir);
37
+ return join(dirname(dir), ".turri", "debug");
38
+ }
39
+ const logs = new WeakMap();
40
+ /** The debug log a session is streaming to, if it has one. */
41
+ export function debugLogOf(session) {
42
+ return logs.get(session);
43
+ }
44
+ /**
45
+ * Start streaming a session to a log of its own. `label` says what the agent is - `app 3 coding
46
+ * agent` - and names the file.
47
+ */
48
+ export function startDebugLog(session, options) {
49
+ let file;
50
+ try {
51
+ const dir = debugDirFor(options.cwd);
52
+ ensureInScratch(dirname(dir), dir);
53
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
54
+ const name = options.label.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "agent";
55
+ file = join(dir, `${stamp}-${name}.log`);
56
+ for (let n = 2; existsSync(file); n += 1)
57
+ file = join(dir, `${stamp}-${name}-${n}.log`);
58
+ appendFileSync(file, maskSecrets(header(session, options)), { flag: "wx" });
59
+ }
60
+ catch (error) {
61
+ reportActivity(`debug log not started: ${error instanceof Error ? error.message : String(error)}`);
62
+ return undefined;
63
+ }
64
+ let broken = false;
65
+ const write = (text) => {
66
+ if (broken)
67
+ return;
68
+ try {
69
+ appendFileSync(file, maskSecrets(text));
70
+ }
71
+ catch (error) {
72
+ // Given up on, not retried: a disk that is full or a folder that went is not going to
73
+ // come back mid-turn, and the render matters more than its log.
74
+ broken = true;
75
+ reportActivity(`debug log stopped: ${error instanceof Error ? error.message : String(error)}`);
76
+ }
77
+ };
78
+ let prompts = 0;
79
+ /** Each call's number and start, by its id: calls in one response run at once and end in any order. */
80
+ const toolStarted = new Map();
81
+ let calls = 0;
82
+ /** How much of the reply or thinking block in progress has streamed. */
83
+ let streamed = 0;
84
+ const record = (event) => {
85
+ switch (event.type) {
86
+ case "message_update": {
87
+ const update = event.assistantMessageEvent;
88
+ if (update.type === "text_start" || update.type === "thinking_start") {
89
+ streamed = 0;
90
+ write(`\n${stamp()} ${update.type === "text_start" ? "reply" : "thinking"}\n`);
91
+ }
92
+ else if (update.type === "text_delta" || update.type === "thinking_delta") {
93
+ streamed += update.delta.length;
94
+ write(update.delta);
95
+ }
96
+ else if (update.type === "text_end" || update.type === "thinking_end") {
97
+ // A block can arrive whole, with no deltas - redacted thinking does: then its end has it.
98
+ write(streamed === 0 && update.content ? `${update.content}\n` : "\n");
99
+ }
100
+ break;
101
+ }
102
+ case "tool_execution_start":
103
+ calls += 1;
104
+ toolStarted.set(event.toolCallId, { n: calls, at: Date.now() });
105
+ write(`\n${stamp()} → #${calls} ${event.toolName}\n${formatArgs(event.args)}`);
106
+ break;
107
+ case "tool_execution_end": {
108
+ const started = toolStarted.get(event.toolCallId);
109
+ toolStarted.delete(event.toolCallId);
110
+ const which = started !== undefined ? `#${started.n} ` : "";
111
+ const took = started !== undefined ? ` · ${duration(Date.now() - started.at)}` : "";
112
+ write(`\n${stamp()} ← ${which}${event.toolName} ${event.isError ? "failed" : "done"}${took}\n${indent(resultText(event.result))}`);
113
+ break;
114
+ }
115
+ case "message_end": {
116
+ const message = event.message;
117
+ if (message.role !== "assistant")
118
+ break;
119
+ const parts = [`stop: ${message.stopReason ?? "unknown"}`];
120
+ if (message.usage !== undefined) {
121
+ const { input, output, cacheRead, cacheWrite, cost } = message.usage;
122
+ parts.push(`tokens: ${input} in, ${output} out, ${cacheRead} cache read, ${cacheWrite} cache write`);
123
+ if (typeof cost?.total === "number")
124
+ parts.push(`$${cost.total.toFixed(4)}`);
125
+ }
126
+ write(`\n${stamp()} end of response · ${parts.join(" · ")}\n`);
127
+ if (message.errorMessage !== undefined)
128
+ write(`${stamp()} error: ${message.errorMessage}\n`);
129
+ // A response cut off runs none of its tool calls, so they never reach the lines above.
130
+ if (message.stopReason === "error" || message.stopReason === "aborted" || message.stopReason === "length") {
131
+ for (const block of message.content ?? []) {
132
+ if (block.type === "toolCall")
133
+ write(`${stamp()} (cut off, not run) → ${block.name ?? "a tool"}\n${formatArgs(block.arguments ?? {})}`);
134
+ }
135
+ }
136
+ break;
137
+ }
138
+ case "auto_retry_start":
139
+ write(`\n${stamp()} provider retry ${event.attempt} of ${event.maxAttempts} in ${duration(event.delayMs)}: ${event.errorMessage}\n`);
140
+ break;
141
+ case "auto_retry_end":
142
+ write(`${stamp()} provider retry ${event.success ? "succeeded" : `gave up${event.finalError ? `: ${event.finalError}` : ""}`}\n`);
143
+ break;
144
+ case "compaction_start":
145
+ write(`\n${stamp()} compacting the context (${event.reason})\n`);
146
+ break;
147
+ case "compaction_end":
148
+ write(`${stamp()} compaction ${event.aborted ? "aborted" : event.errorMessage ? `failed: ${event.errorMessage}` : "done"}\n`);
149
+ break;
150
+ }
151
+ };
152
+ // pi calls this from inside the agent's turn: nothing in it may throw back into that.
153
+ session.subscribe((event) => {
154
+ try {
155
+ record(event);
156
+ }
157
+ catch (error) {
158
+ write(`\n${stamp()} (an event could not be logged: ${error instanceof Error ? error.message : String(error)})\n`);
159
+ }
160
+ });
161
+ const log = {
162
+ file,
163
+ prompt(text) {
164
+ prompts += 1;
165
+ write(`\n\n${"=".repeat(24)} prompt ${prompts} · ${stamp()} ${"=".repeat(24)}\n${text}\n`);
166
+ },
167
+ note(line) {
168
+ write(`\n${stamp()} ${line}\n`);
169
+ },
170
+ };
171
+ logs.set(session, log);
172
+ // In the transcript, not the activity: where to look, not something an agent did.
173
+ reportTranscript({ kind: "stage", text: `debug log: ${file}` });
174
+ return log;
175
+ }
176
+ function header(session, options) {
177
+ const job = currentJob();
178
+ const model = session.model?.id !== undefined ? `${session.model.provider ?? "?"}/${session.model.id}` : "pi's default";
179
+ const lines = [
180
+ `turri debug log · ${options.label}`,
181
+ `started ${new Date().toISOString()}`,
182
+ `job ${job !== undefined ? `${job.id} · ${job.kind} · ${job.summary}` : "none - run from the command line, or waited on"}`,
183
+ `model ${model}${session.thinkingLevel !== undefined ? ` · thinking ${session.thinkingLevel}` : ""}`,
184
+ `cwd ${options.cwd}`,
185
+ `session ${session.sessionFile ?? "not saved"}`,
186
+ ...(options.tools !== undefined ? [`tools ${options.tools.join(", ")}`] : []),
187
+ ];
188
+ const system = session.systemPrompt;
189
+ return `${lines.join("\n")}\n${system ? `\n${"=".repeat(24)} system prompt ${"=".repeat(24)}\n${system}\n` : ""}`;
190
+ }
191
+ /** Local time of day to the millisecond: when each thing happened, and how long between them. */
192
+ function stamp() {
193
+ const now = new Date();
194
+ const pad = (n, width = 2) => String(n).padStart(width, "0");
195
+ return `[${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(now.getMilliseconds(), 3)}]`;
196
+ }
197
+ function duration(ms) {
198
+ if (ms < 1000)
199
+ return `${ms}ms`;
200
+ const s = ms / 1000;
201
+ return s < 60 ? `${s.toFixed(1)}s` : `${Math.floor(s / 60)}m ${Math.round(s % 60)}s`;
202
+ }
203
+ /** Every line indented, so a tool's output stands apart from the lines around it. */
204
+ function indent(text) {
205
+ let end = text.length;
206
+ while (end > 0 && text.charCodeAt(end - 1) === 10)
207
+ end -= 1;
208
+ if (end === 0)
209
+ return "";
210
+ return `${text.slice(0, end).split("\n").map((line) => ` ${line}`).join("\n")}\n`;
211
+ }
212
+ /** A tool call's arguments, one per line, whole: a file's content is shown as the file, not as JSON. */
213
+ function formatArgs(args) {
214
+ if (args === null || typeof args !== "object" || Array.isArray(args))
215
+ return indent(JSON.stringify(args) ?? "");
216
+ return Object.entries(args)
217
+ .map(([key, value]) => typeof value === "string" && value.includes("\n")
218
+ ? ` ${key}:\n${indent(value)}`
219
+ : ` ${key}: ${typeof value === "string" ? value : JSON.stringify(value)}\n`)
220
+ .join("");
221
+ }
222
+ /** A tool result's text, whole; anything else in it is named rather than dropped silently. */
223
+ function resultText(result) {
224
+ const content = result?.content;
225
+ if (!Array.isArray(content))
226
+ return result === undefined ? "" : (JSON.stringify(result) ?? "");
227
+ return content.map((part) => (part.type === "text" && typeof part.text === "string" ? part.text : `[${part.type ?? "unknown"} content]`)).join("\n");
228
+ }
@@ -0,0 +1,90 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { defineTool } from "@earendil-works/pi-coding-agent";
3
+ import { Type } from "typebox";
4
+ import { ask, BEGIN, createExecutor } from "../pi.js";
5
+ import { confinedTools } from "./confine.js";
6
+ import { createWebTools, WEB_TOOLS } from "./web.js";
7
+ /**
8
+ * A fresh look at a failure the coding agent keeps hitting.
9
+ *
10
+ * The coding agent's session fills with its own attempts, and an agent stuck on a failure tends to keep circling the
11
+ * same few changes - moving files about, reordering a fixture - when the cause is one it has not thought of: an
12
+ * in-memory database every connection opens anew, a test runner that does not put the project on the path. When the
13
+ * unit tests fail the same way round after round, or the agent goes round in circles in its own runs, the harness starts
14
+ * this agent: a session of its own - on the `debugger` role's model, else a stronger one than the coding agent's, the
15
+ * judge model or the harness model - that reads the code, the tests, the build files and the
16
+ * failing run - and the web - and says what the cause is and what change fixes it. It changes nothing; the coding
17
+ * agent is given what it found, and goes on.
18
+ */
19
+ /** How many tool calls the debugger has before it must hand over what it found. */
20
+ const DEBUGGER_CALLS = 40;
21
+ /** The debugger's account, as the coding agent is given it. */
22
+ export function diagnosisText(diagnosis) {
23
+ return [`<root cause>\n${diagnosis.rootCause.trim()}\n</root cause>`, `<fix>\n${diagnosis.fix.trim()}\n</fix>`].join("\n\n");
24
+ }
25
+ function debugPrompt(input) {
26
+ const script = input.script !== undefined && existsSync(input.script) ? readFileSync(input.script, "utf8") : undefined;
27
+ return [
28
+ `The unit tests of the codebase in ${input.buildDir} have failed the same way ${input.times} times in a row. The coding agent`,
29
+ "keeps changing things and the failure does not move: it has not seen the cause. You look at it with fresh eyes.",
30
+ "",
31
+ "Find the root cause - the one thing that makes this failure happen - and the change that fixes it. Read the failing",
32
+ "run below, the tests it names, the code they exercise, the build files (pyproject.toml, package.json, go.mod, pom.xml,",
33
+ "...) and the testing script below: it decides pass or fail, and how and where it runs the tests is often the cause.",
34
+ "Look the error up on the web when it is one a library or tool is known for. Think about what the coding agent may have",
35
+ "missed rather than what it has already tried: a test runner that does not put the code on its import path, an",
36
+ "in-memory database that every connection opens empty, a name that shadows another, a configuration a tool needs.",
37
+ "",
38
+ "You change nothing - you only read. When you know, call submit_diagnosis: the root cause, with the evidence for it,",
39
+ "and the fix - which files, and exactly what to change in them.",
40
+ "",
41
+ "<what the change is meant to do>",
42
+ input.task.trim(),
43
+ "</what the change is meant to do>",
44
+ ...(script !== undefined ? ["", `<testing script path="${input.script}">`, script.trimEnd(), "</testing script>"] : []),
45
+ "",
46
+ "<failing run>",
47
+ input.failure.trimEnd().slice(-8000),
48
+ "</failing run>",
49
+ ].join("\n");
50
+ }
51
+ /** Have a fresh agent find why the unit tests keep failing the same way: its root cause and fix, or undefined when it found none. */
52
+ export async function debugFailure(input) {
53
+ let found;
54
+ const submit = defineTool({
55
+ name: "submit_diagnosis",
56
+ label: "Submit the diagnosis",
57
+ description: "Hand over the root cause of the failure and the change that fixes it. The coding agent gets only this.",
58
+ parameters: Type.Object({
59
+ root_cause: Type.String({ description: "The one thing that makes the failure happen, and the evidence for it: the lines of the run, the code, the docs" }),
60
+ fix: Type.String({ description: "The change that fixes it: which files, and exactly what to change in them" }),
61
+ }),
62
+ async execute(_id, params) {
63
+ const given = params;
64
+ found = { rootCause: given.root_cause, fix: given.fix };
65
+ return { content: [{ type: "text", text: "Recorded." }], details: {}, terminate: true };
66
+ },
67
+ });
68
+ // It reads the code and the web, and changes nothing: the file tools, no shell.
69
+ const fence = confinedTools({ workdir: input.buildDir, read: [input.buildDir], write: [], noBash: true });
70
+ const session = await createExecutor({
71
+ cwd: input.buildDir,
72
+ customTools: [submit, ...createWebTools(), ...fence.tools],
73
+ tools: [...fence.names, ...WEB_TOOLS, "submit_diagnosis"],
74
+ ...(input.model !== undefined ? { model: input.model } : {}),
75
+ label: `${input.label} debugger`,
76
+ role: "debugger",
77
+ system: fence.note,
78
+ task: debugPrompt(input),
79
+ budget: { calls: DEBUGGER_CALLS, finish: ["submit_diagnosis"] },
80
+ });
81
+ try {
82
+ await ask(session, BEGIN, input.onDelta);
83
+ if (found === undefined)
84
+ await ask(session, "Call submit_diagnosis now with what you found: the most likely root cause, and the fix.", input.onDelta);
85
+ }
86
+ finally {
87
+ session.dispose();
88
+ }
89
+ return found;
90
+ }
@@ -0,0 +1,19 @@
1
+ import { relative } from "node:path";
2
+ /**
3
+ * How a job that lands code ends: the person can try what is built, by asking - `deploy` sets it up
4
+ * from what is built.
5
+ */
6
+ function shortPath(buildDir) {
7
+ const fromHere = relative(process.cwd(), buildDir);
8
+ return fromHere === "" || fromHere.startsWith("..") ? buildDir : fromHere;
9
+ }
10
+ /** What every job that lands code ends with: the person can try it now, by asking. */
11
+ export const TRY_IT_LINE = "Want to try it? Just ask me - I'll set it up from what's built so far.";
12
+ /** The same for a command-line run, which has no one to ask: the command that does it. */
13
+ export function tryItCommand(plainFile) {
14
+ return `To try it: turri release ${shortPath(plainFile)}`;
15
+ }
16
+ /** A job's report, ending with how to try what it built when it landed. */
17
+ export function withTryItLine(report, landed) {
18
+ return landed ? `${report}\n\n${TRY_IT_LINE}` : report;
19
+ }
@@ -0,0 +1,40 @@
1
+ import { describeJob } from "./jobs.js";
2
+ /**
3
+ * Why a job did not land, from its transcript: how it ended, the last failing run of a testing script, the review that
4
+ * sent the work back last, the stages that went wrong and the tool calls that failed - what someone reading the whole
5
+ * log would look for first, so the conversation does not have to guess from the report.
6
+ */
7
+ const LAST_RUN_CHARS = 6_000;
8
+ const LAST_REVIEW_CHARS = 4_000;
9
+ const SHOWN_ERRORS = 5;
10
+ const SHOWN_STAGES = 12;
11
+ const textOf = (entry) => entry.full ?? entry.text;
12
+ /** A testing script's run that failed, as the harness or an agent's run_unit_tests / run_conformance_tests saw it. */
13
+ const FAILED_RUN = /\bexited ([1-9]\d*)\b|had not finished after \d+ seconds/;
14
+ /** A stage that says something went wrong. */
15
+ const WRONG = /not passing|did not|failing|failed|✗|undoing|stopped|halt|wrong|missing/i;
16
+ export function whyFailed(job) {
17
+ const ended = describeJob(job);
18
+ if (job.status === "running" || job.status === "queued")
19
+ return `${ended}\n\nIt has not ended yet: pulses says how it is going.`;
20
+ const transcript = job.transcript;
21
+ // An agent's run is a tool result; the harness's own reaches the agent as the prompt that sends it back.
22
+ const lastRun = [...transcript].reverse().find((entry) => (entry.kind === "result" || entry.kind === "prompt") && FAILED_RUN.test(textOf(entry)));
23
+ const lastReview = [...transcript].reverse().find((entry) => entry.kind === "prompt" && /A review of the work found|review found/i.test(textOf(entry)));
24
+ const stages = transcript.filter((entry) => entry.kind === "stage" && WRONG.test(entry.text)).slice(-SHOWN_STAGES);
25
+ const errors = transcript.filter((entry) => entry.kind === "result" && entry.error === true).slice(-SHOWN_ERRORS);
26
+ const tail = (text, chars) => (text.length > chars ? `… ${text.slice(-chars)}` : text);
27
+ return [
28
+ ended,
29
+ ...(job.diagnosis !== undefined ? ["", "<diagnosis>", job.diagnosis.trim(), "</diagnosis>"] : []),
30
+ ...(stages.length > 0 ? ["", "<what went wrong, as it happened>", ...stages.map((entry) => `- ${entry.text.trim()}`), "</what went wrong, as it happened>"] : []),
31
+ ...(lastRun !== undefined
32
+ ? ["", `<last failing test run${lastRun.agent !== undefined ? ` by="${lastRun.agent.label}"` : ""}>`, tail(textOf(lastRun).trim(), LAST_RUN_CHARS), "</last failing test run>"]
33
+ : []),
34
+ ...(lastReview !== undefined ? ["", "<last review that sent the work back>", tail(textOf(lastReview).trim(), LAST_REVIEW_CHARS), "</last review that sent the work back>"] : []),
35
+ ...(errors.length > 0
36
+ ? ["", "<tool calls that failed, last ones>", ...errors.map((entry) => `- ${entry.agent?.label ?? "an agent"}: ${textOf(entry).trim().slice(0, 400)}`), "</tool calls that failed, last ones>"]
37
+ : []),
38
+ ...(job.logFile !== undefined ? ["", `The whole transcript: ${job.logFile}`] : []),
39
+ ].join("\n");
40
+ }
@@ -0,0 +1,81 @@
1
+ import { defineTool } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import { ask, BEGIN, createExecutor } from "../pi.js";
4
+ import { confinedTools } from "./confine.js";
5
+ /** How many tool calls the diagnosis may make before it is told to decide. */
6
+ export const DIAGNOSIS_CALLS = 40;
7
+ export function describeDiagnosis(diagnosis) {
8
+ const whose = { spec: "the spec", environment: "the environment", code: "the code", harness: "turri itself" }[diagnosis.whose];
9
+ return [
10
+ `What happened: ${diagnosis.whatHappened.trim()}`,
11
+ `Why: ${diagnosis.cause.trim()}`,
12
+ `To fix - in ${whose}:`,
13
+ ...diagnosis.todo.map((step, index) => ` ${index + 1}. ${step.trim()}`),
14
+ ].join("\n");
15
+ }
16
+ export async function diagnoseFailure(options) {
17
+ let diagnosis;
18
+ const submit = defineTool({
19
+ name: "submit_diagnosis",
20
+ label: "Submit the diagnosis",
21
+ description: "Hand over what happened, why, whose it is to fix and exactly what to do. Call this once.",
22
+ parameters: Type.Object({
23
+ what_happened: Type.String({ description: "What the job did and where it failed, both times - in a few sentences" }),
24
+ cause: Type.String({ description: "The root cause, as the logs, the code, the tests or turri's source show it - quote what shows it" }),
25
+ whose: Type.Union([Type.Literal("spec"), Type.Literal("environment"), Type.Literal("code"), Type.Literal("harness")], {
26
+ description: "Where the fix belongs: the spec, the environment (a service, a credential, a toolchain), the code, or turri itself",
27
+ }),
28
+ todo: Type.Array(Type.String(), { description: "Exactly what to do, step by step - the file and the change, the command, the setting" }),
29
+ }),
30
+ async execute(_id, params) {
31
+ const given = params;
32
+ diagnosis = { whatHappened: given.what_happened, cause: given.cause, whose: given.whose, todo: given.todo };
33
+ return { content: [{ type: "text", text: "Recorded." }], details: {}, terminate: true };
34
+ },
35
+ });
36
+ const fence = confinedTools({ workdir: options.projectDir, read: [options.projectDir, options.harnessDir], write: [], noBash: true });
37
+ const session = await createExecutor({
38
+ cwd: options.projectDir,
39
+ customTools: [submit, ...fence.tools],
40
+ tools: [...fence.names, "submit_diagnosis"],
41
+ model: options.model,
42
+ label: `${options.job.summary} diagnosis`, role: "diagnosis",
43
+ system: fence.note,
44
+ task: diagnosisPrompt(options),
45
+ budget: { calls: DIAGNOSIS_CALLS, finish: ["submit_diagnosis"] },
46
+ });
47
+ try {
48
+ await ask(session, BEGIN);
49
+ if (diagnosis === undefined)
50
+ await ask(session, "You have not called submit_diagnosis. Call it now, with what you know.");
51
+ }
52
+ catch {
53
+ // Without a diagnosis the person still gets both failures, as they were.
54
+ }
55
+ finally {
56
+ session.dispose();
57
+ }
58
+ return diagnosis;
59
+ }
60
+ export function diagnosisPrompt(options) {
61
+ return [
62
+ `A turri job failed twice: ${options.job.kind} - ${options.job.summary} (job ${options.job.id}). It was tried a`,
63
+ "second time and failed again, so it has stopped, and every job queued behind it has failed without running. Work",
64
+ "out why, and what has to be done so it passes - the person is told exactly what you say.",
65
+ "",
66
+ "You can read, and change nothing:",
67
+ `- the project, in ${options.projectDir}: its .plain specs and templates, config.turri.yaml, the testing scripts, and`,
68
+ " plain_modules - each module's code and conformance tests, the notes (.turri-notes.md in the code), the full",
69
+ " outputs of the test runs (plain_modules/.turri/logs), and every agent's own log",
70
+ ` (${options.projectDir}/.turri/debug) - newest last;`,
71
+ ...(options.jobLog !== undefined ? [`- this job's own log: ${options.jobLog};`] : []),
72
+ `- turri itself, in ${options.harnessDir}: its source (src/), its skills (genome/skills/), the ***plain authoring rules`,
73
+ " (genome/rules/) and its docs (docs/) - so a failure that comes from the harness can be named as that.",
74
+ "",
75
+ "Find the root cause, not the last symptom: read the failing run's output and the agents' logs around where it",
76
+ "failed, and follow it to the spec line, the code, the test, the environment or the harness it comes from. Then",
77
+ "call submit_diagnosis: what happened, why, whose it is to fix, and exactly what to do.",
78
+ "",
79
+ ...options.failures.flatMap((failure, index) => [`<attempt ${index + 1}>`, failure.trim().slice(-12_000), `</attempt ${index + 1}>`, ""]),
80
+ ].join("\n");
81
+ }