@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,233 @@
1
+ # The sandbox
2
+
3
+ A rendering agent is building one module. It has no business reading the `.plain` specs it was
4
+ not given, the rest of the project, or anything else on the machine - and pi's `cwd` is only
5
+ where relative paths start: an absolute path resolves to itself and `bash` can `cd` anywhere. So
6
+ turri fences its agents: their file tools check every path, and every command they run on this
7
+ machine passes a command check, runs in an environment of the module's own behind an OS sandbox
8
+ where the OS has one, and is audited afterwards. The rules live in code that runs the same on every
9
+ platform; the OS sandbox is an extra wall under them.
10
+
11
+ ## What an agent can reach
12
+
13
+ Each agent's file tools reach its own module and only the parts its role needs:
14
+
15
+ | Role | Reads | Writes |
16
+ | --- | --- | --- |
17
+ | Coding agent, regression repair | its module's `code/` | its module's `code/` |
18
+ | Conformance agent | its module's `tests/` and `code/` | its module's `tests/` |
19
+ | Code fixer | its module's `code/` and `tests/` | its module's `code/` |
20
+ | Explorers | its module's `code/` | nothing |
21
+ | Test review | its module's `tests/` and `code/` | nothing |
22
+
23
+ Nothing else exists for them: not another module, not turri's scratch area `plain_modules/.turri` (a test run's
24
+ whole output reaches them through `read_output`, not by path), not the module's lineage `.plaingit`, and nothing
25
+ above `plain_modules`. The specs it is
26
+ building from reach it only through the prompt the harness writes. Nor does any context file: pi puts
27
+ every `AGENTS.md` / `CLAUDE.md` from an agent's folder up into its system prompt, and turri
28
+ turns that off for every agent it starts - so the project's own `AGENTS.md`, or yours in
29
+ `~/.pi/agent`, never reaches a rendering agent, a judge or a skill's agent. Nor do pi's skills, which
30
+ pi would list for an agent to load: each agent turri starts has one job, and turri runs its
31
+ own gated skills itself. The conversation you talk to still loads both, as pi does - but for the skills turri has
32
+ already, its own and any about \*\*\*plain (see [a project's own skills](skills-and-gates.md#a-projects-own-skills)).
33
+
34
+ ## Every agent knows its walls
35
+
36
+ Each agent's system prompt says where its walls are, in words generated from its fence, so they say exactly what is
37
+ enforced and are the same on every platform:
38
+
39
+ - its own folder or folders, by absolute path, and what its file tools read and write there - a role that only reads
40
+ is told it writes nothing, "not with your file tools, not with your shell"; the conformance agent that it reads the
41
+ code under test but writes tests, not code; the code fixer that it reads the conformance tests but fixes the code;
42
+ - that its shell runs on this machine, in its folder, with this machine's toolchains, a home folder and a temporary
43
+ folder of its own (`$HOME`, `$TMPDIR`), the package caches and an open network, and that nothing else exists for
44
+ it - not the specs, not the project's other folders or modules, not your home;
45
+ - what happens on a reach outside, matter-of-fact: a command that names a path outside is refused before it runs,
46
+ and anything a command changes outside is put back, and it is told;
47
+ - that the project's dependencies are its own, through the build manifest, and installing anything else is not: what
48
+ the machine lacks it reports with `report_environment_error`, naming what is missing, and goes no further with what
49
+ depends on it (see [when the environment lacks something](regenerating.md#when-the-environment-lacks-something));
50
+ - that it may stop only what it started, and that a port something else holds means another port; that `ps` may show
51
+ nothing but what it started, so a server is checked with `kill -0 $!`, its log or a request;
52
+ - that temporary files go in `$TMPDIR`, `/tmp` being outside its walls.
53
+
54
+ Beside its walls, an agent that writes code or tests - the coding agent, the conformance agent, the code fixer, the
55
+ repair agent - is told up front what it would otherwise go looking for (`src/regenerate/workspace.ts`): whether its folder is
56
+ empty, with nothing above it to find, or holds code whose unit tests passed when it was committed; that it is not a git
57
+ repository, and `my_diff` shows its work; the versions of the toolchains its module's implementation reqs name, probed
58
+ once on this machine, or that one is missing; that the package caches and Maven's repository are set for it, and to
59
+ leave `JAVA_TOOL_OPTIONS` set; and that the network is open.
60
+
61
+ No other path is named - nothing into `plain_modules/.turri`. A test run or a command whose output was cut names the
62
+ whole of it by an id - not a file in its folder, which `read` says if it is given one - and the agent reads it with
63
+ `read_output`: not by a path into `plain_modules/.turri` or to a
64
+ temporary file on this machine (see [long output](regenerating.md#the-tests-must-actually-pass)).
65
+
66
+ ## File tools
67
+
68
+ `src/regenerate/confine.ts` rebuilds `read`, `write`, `edit`, `ls`, `grep` and `find` from pi's own
69
+ factories with a check in front of every path. The check runs twice: once on the argument the
70
+ tool was called with, and once on the absolute path the tool resolved it to, because the two can
71
+ differ. Symlinks are resolved before comparing, so a link cannot walk out, and a glob containing
72
+ `..` is refused outright.
73
+
74
+ The record of what the code is built from, `code/.turri.json`, is the harness's: no agent reads or writes it. `read`
75
+ refuses it, and `ls`, `grep` and `find` leave it out of what they list - as the command check, the OS sandbox and the
76
+ audit keep it from commands.
77
+
78
+ Credentials are the person's: no agent writes a `.env` or a variant of one (`.env.local`, `.env.test`) - a
79
+ template, `.env.example`, is fine. The file tools refuse it, the command check refuses a command that would
80
+ write one, and what a command does to one anyway is put back by the audit, the agent told why. A made-up credential makes every later run fail without anyone seeing why; a missing one is the
81
+ judge's to report (see [regenerating the code](regenerating.md)), the person's to provide.
82
+
83
+ ## Commands on this machine
84
+
85
+ `bash` is the case no argument check can cover - one string, and no way to read for certain where it will read or
86
+ write. So every command an agent runs - its `bash`, a service it starts - goes through every wall at once
87
+ (`src/regenerate/agent-shell.ts`, `agentShell`), from its role's fence, the same as its file tools':
88
+
89
+ 1. **the command check**, before it runs - refused with exit 126 and the reason, a line in the job's log;
90
+ 2. **the module's own environment** - not yours: the path, the locale, the terminal, the user and the toolchains'
91
+ variables pass, nothing else - no provider key, no token, no SSH agent; a home folder of the module's own in
92
+ `plain_modules/.turri/home/<module>`, kept between pulses, a `TMPDIR`, the package caches in the project, git
93
+ stopped at the module folder, and what `agent-env:` passes (see [credentials for the agents](config.md#credentials-for-the-agents)).
94
+ Every JVM - a test JVM Maven or Gradle forks too - is given the module's `TMPDIR` and home through
95
+ `JAVA_TOOL_OPTIONS`, since Java takes both from the system rather than from `TMPDIR` and `HOME`; each says so in a
96
+ "Picked up JAVA_TOOL_OPTIONS" line on its stderr;
97
+ 3. **the OS sandbox**, where one is up - Seatbelt on macOS, bubblewrap on Linux and WSL2;
98
+ 4. **the audit**, after it, which puts back what it changed that its role may not, and tells the agent.
99
+
100
+ The job's log says, for each agent with a shell, which walls are up - and where no OS sandbox starts, why.
101
+
102
+ The command runs through the host runner (`src/regenerate/shell.ts`): `bash -c` from the agent's folder, in a process group of
103
+ its own. No command can hang a build: every one has a time limit - 2 minutes, or `TURRI_COMMAND_TIMEOUT` seconds: the
104
+ agent's own `bash` unless it gives a timeout (at most 30 minutes), its unit and conformance test runs, the harness's own
105
+ runs. A command that runs out is killed, it and everything it started, and reports exit 124 with a note on what to do
106
+ instead: a server, a watcher or a prompt waiting for input never ends on its own, and has to be started in the
107
+ background or given its input. A job that is stopped ends the command it is waiting on at once, the same way.
108
+
109
+ A server a command leaves running in the background is its pulse's: it runs on until the pulse ends, and is killed
110
+ then - a pulse running beside it keeps its own. Everything still running is killed when turri exits, ended by a signal
111
+ too. A turri killed outright cannot do that itself, so one small watcher beside it does, within two seconds, from the
112
+ groups it recorded in the temporary folder (`turri-groups/`); and groups a turri left that nothing killed are swept when
113
+ turri next starts. On Windows there are no process groups: a command's process tree is killed with `taskkill /T /F`,
114
+ best effort.
115
+
116
+ The project's testing scripts - their static checks included - are not agents' commands: they are
117
+ yours, and run on this machine as you would run them, unfenced, in your own environment - see [the testing scripts](regenerating.md#the-testing-scripts). The agent gets their
118
+ output, nothing more.
119
+
120
+ ### The command check
121
+
122
+ Before an agent's command runs, its text is checked (`src/regenerate/command-check.ts`, `checkCommand`), on every platform, and
123
+ refused - not run - with the rule it broke and what to do instead, when it:
124
+
125
+ - names a path outside the role's roots - absolute, a `..` that climbs out of the agent's folder (after the `cd`s
126
+ before it), or inside a quoted string (`python3 -c "..."`). `~`, `$HOME` and `$TMPDIR` are the module's own and
127
+ allowed, as are the package caches, the machine's system folders and the toolchains, and the devices
128
+ (`/dev/null`). A path is checked when it names something on disk - under a top folder such as `/Users`, `/home`,
129
+ `/etc` or `/tmp`, one of the fence's, or one that exists - so an API route in a curl or a JSON body is not one, and
130
+ nor is `/` alone, a slash in code (`case '/':`), though writing to `/` is refused;
131
+ - names the code record, `.turri.json`;
132
+ - writes a credential file (`.env` and its variants; a template is fine) - by redirect, `tee`, `cp`, `mv` or an edit
133
+ in place;
134
+ - installs software outside the project: `sudo`, the system's package managers installing (`brew`, `apt`, `dnf`,
135
+ `apk`, `pacman`, `choco`, `winget`, ...), toolchain managers installing (`nvm`, `sdk`, `rustup`, `pyenv`, `asdf`,
136
+ ...), global installs (`npm -g`, `yarn global`, `pip --user`, `pipx`, `gem install`), a script downloaded and run
137
+ (`curl ... | sh`), an installer (`.pkg`, `.msi`, `.dmg`, `.run`). The project's own dependencies - `npm install`,
138
+ pip into its venv, `mvn`, `cargo`, `go mod` - are building the code, and pass;
139
+ - kills what its pulse did not start: `pkill`, `killall`, `taskkill`, `fuser -k`, or `kill` aimed at a port or name
140
+ lookup. `kill $!` and `kill %1` pass.
141
+
142
+ On Windows, drive letters, backslashes and any case are read as Windows has them. It knowingly misses a path built at
143
+ run time (`$(...)`, a variable, a script that reads one from a file): the OS sandbox and the audit are for those.
144
+
145
+ ### The OS sandbox
146
+
147
+ Under the check, where the OS offers one, the command runs behind an OS sandbox (`src/regenerate/os-sandbox.ts`), driven
148
+ directly from the role's fence: Seatbelt (`sandbox-exec`) on macOS, bubblewrap on Linux and WSL2. It is an extra wall,
149
+ never where a rule lives - where neither starts (native Windows, WSL1, a container that does not allow user
150
+ namespaces) the check and the audit hold alone. What a command behind it reaches:
151
+
152
+ - reads: its role's roots, the module's home, `TMPDIR` and the package caches, and of the machine only the system's
153
+ folders, every folder on the agent's `PATH` (the folder itself, not its parent: `~/.cargo/bin`, not `~/.cargo`) - but
154
+ a version manager's install whole, a `bin` whose parent is a version (`…/mise/installs/node/24.16.0/bin`), as its
155
+ tools are links into the install beside it (`npm` into `lib/node_modules`) - the folders the toolchain variables
156
+ point at (`JAVA_HOME`, `RUSTUP_HOME`, `NVM_DIR`, mise's `MISE_DATA_DIR`, fnm's, rbenv's, the Pythons uv fetches, ...)
157
+ and the project's `toolchains:` - nothing else of the person's home;
158
+ - writes: its role's own roots - none for a role that only reads - and the module's home, `TMPDIR` and caches. On macOS
159
+ the developer tools' own scratch files too (`xcrun`'s database, clang's module cache), which go where macOS says, not
160
+ where `TMPDIR` does - and a folder of the module's own in macOS's per-user temporary folder
161
+ (`DIRHELPER_USER_DIR_SUFFIX`), where its JVMs keep their attach sockets, so Mockito's inline mocks, `jcmd` and
162
+ `jstack` attach to a JVM it started; the person's JVMs' sockets, beside it, stay out of reach;
163
+ - never the code record; no Unix socket outside its folders (on macOS, the name service's excepted); signals and
164
+ process listings only within its own sandbox (bubblewrap's PID namespace, Seatbelt's same-sandbox), so `ps` shows
165
+ nothing else and `kill` reaches nothing else;
166
+ - the network as this machine has it;
167
+ - at most 4096 processes and files of at most 8 GiB, set with `ulimit` inside.
168
+
169
+ `sandboxAvailable()` tries it once and says which wall is up, or why none starts and how to give it one: bubblewrap not
170
+ installed, user namespaces off (Docker: a user that is not root, `--security-opt seccomp=unconfined`), AppArmor in the way (Docker:
171
+ `--security-opt apparmor=unconfined`; Ubuntu 24.04: its user-namespace restriction), or Docker's masked `/proc` - for
172
+ which bubblewrap falls back to binding the host's `/proc` rather than mounting its own.
173
+
174
+ ### The audit
175
+
176
+ After every agent command, on every platform - a sandbox up or not - turri puts back what the command's role may not
177
+ change inside the project (`src/regenerate/audit.ts`): the specs, `config*.yaml`, the testing scripts, every credential file
178
+ (the agent's own folder included), the code record, the module's lineage `.plaingit` (its refs and settings), the other
179
+ modules, the folders the role only reads, and the part of its own module it does not write - the coding agent's
180
+ conformance tests. A changed file is put back, a new one removed, a deleted one brought back: from a copy kept before
181
+ the command, or - another module - as its last commit has it, unless a pulse is at work in that module, whose writes are
182
+ not this command's to undo (left, and logged). Nor is a change the conversation made while the command ran: the chat's
183
+ `edit` and `write`, a spec diff it applied, or anything while a command of its own ran - the chat editing a spec beside a
184
+ pulse is the person's change, not the agent's, so it is left and logged (`src/regenerate/conversation-writes.ts`). The agents
185
+ of a pulse start without extensions, so nothing of theirs is marked so. The agent is told in its command's result what was put back; the job's
186
+ log gets a line; you are not told, and the pulse goes on. It compares sizes and modification times, skipping
187
+ dependencies and build output, and contents only where those differ - about 35 ms on a project of 3000 files. Outside
188
+ the project nothing can be put back: that is the OS sandbox's, and on Windows an accepted gap.
189
+
190
+ ## The web
191
+
192
+ Every rendering agent - render, spec change, conformance, regression repair - can also search the
193
+ web and read a page, for a library's documentation or an error it has not seen:
194
+
195
+ | Tool | What it does |
196
+ | --- | --- |
197
+ | `web_search` | asks DuckDuckGo (no key needed) and returns title, address and snippet per result |
198
+ | `web_fetch` | reads one page and returns it as text - HTML without its markup, text, JSON or XML as it is; long pages come in parts |
199
+
200
+ `web_fetch` reads only the public web: localhost, the private ranges and the cloud metadata address are
201
+ refused, and so is every redirect that leads to one - what the code talks to on this machine, the agent
202
+ reaches with its own commands.
203
+
204
+ DuckDuckGo is used without a key through its Lite page, which is not an official API: it rate-limits,
205
+ and a search that is turned away tells the agent to wait or fetch a page it knows instead.
206
+
207
+ ## Outside the sandbox: deploy
208
+
209
+ What `deploy` starts runs on your machine unfenced: you asked to try the software, with your toolchains,
210
+ in your browser. It runs from `dist/<module>`, a copy of the last
211
+ commit outside `plain_modules`, so no rendering agent reaches it. Nothing is installed there without
212
+ your say-so - every command that would install or download is put to you first, and the chat's shell
213
+ refuses the rest. See [deploy](deploy.md).
214
+
215
+ ## What turri keeps beside the code
216
+
217
+ It lives in `plain_modules/.turri`, which ignores itself in git:
218
+
219
+ | | |
220
+ | --- | --- |
221
+ | `cache/` | package caches - pip, npm, cargo, Maven, Gradle, Go - so a dependency downloads once per project |
222
+ | `home/<module>` | each module's home folder for its agents' commands, kept between pulses |
223
+ | `tmp/<module>` | each module's `TMPDIR` |
224
+ | `scripts/` | the throwaway copies test runs happen in, emptied after each run |
225
+ | `logs/` | whole outputs cut short for an agent, read through `read_output` |
226
+ | `services/` | what the services agents started printed |
227
+
228
+ ## What the machine lacks
229
+
230
+ The agents use this machine's toolchains, and install nothing on it: not a toolchain, a system package, a driver or a
231
+ global package. The project's own dependencies are theirs, through its package manager, into the project and its
232
+ caches. What the machine lacks is yours to install: an agent that finds something missing reports it, the pulse stops,
233
+ and you are told what to install - see [when the environment lacks something](regenerating.md#when-the-environment-lacks-something).
@@ -0,0 +1,254 @@
1
+ # Skills and gates
2
+
3
+ A skill's review step is normally a paragraph asking the agent to check its own work. Agents talk
4
+ their way past paragraphs. turri makes the review an edge in a graph instead: the harness
5
+ decides when a gate runs, spawns the judges itself, and branches on a structured verdict. The
6
+ model's only job is producing the artifact under review.
7
+
8
+ pi supplies the agent runtime (sessions, tools, multi-provider models); LangGraph supplies the
9
+ control flow. The gate syntax lives in `SKILL.md`, so a gated skill still loads in any
10
+ spec-compliant harness - it just runs ungated there.
11
+
12
+ ## The syntax
13
+
14
+ Under `metadata:`, which the Agent Skills spec reserves for arbitrary key-values:
15
+
16
+ ```yaml
17
+ ---
18
+ name: draft-spec
19
+ description: Drafts a ***plain spec, judged before it is kept.
20
+ allowed-tools: [read, write, edit, bash, ls, grep, find]
21
+ metadata:
22
+ gates:
23
+ - after: product # the ### step this gate guards
24
+ review:
25
+ rubric: ./references/rubric.md
26
+ judges: 2 # independent sessions, not n samples of one
27
+ model: anthropic/claude-sonnet-5 # optional; defaults to the executor's model
28
+ quorum: all # all | majority | any
29
+ on_fail: retry # retry | escalate | abort
30
+ max_retries: 2
31
+ judge_tools: [read, ls, grep, find] # read-only tools the rubric needs
32
+ ---
33
+
34
+ ## Steps
35
+
36
+ ### product
37
+ ...prose the executor reads...
38
+
39
+ ### finalize
40
+ ...
41
+ ```
42
+
43
+ A skill declares its steps with a `## Steps` section whose `###` headings are the steps; without
44
+ it the whole skill is one step, which is what most skills are. Steps are never inferred from `###`
45
+ headings elsewhere in the body - prose skills are full of those. Each gate becomes a conditional
46
+ edge after its step; a single-step skill is gated by naming the skill itself. None of the skills turri ships
47
+ declares a gate today - `brood`'s phases end on the person's confirmation alone - but a project's own skill can.
48
+
49
+ `paced: false` under `metadata:` lets a skill write spec changes one after another on its own:
50
+ `write_spec_diff` otherwise refuses a new change until the user has answered a question since the
51
+ last one - one answer, one change, as an interview writes them. `spawn` is unpaced: the tool tells it so, and after
52
+ each change to write the next at once, not to carry on with an interview it does not have.
53
+
54
+ `questioning:` under `metadata:` names the steps the questioning level caps (`src/questioning.ts`) - `brood`'s
55
+ `[stack, product]`. When there are any and someone to ask, the harness itself asks the first question of the first
56
+ step: how many questions those steps may ask - light (3), medium (5), thorough (10) or best judgement - and tells the
57
+ agent the answer beside each step's instructions, with how broad each question is to be: broad at light, a whole area
58
+ at once, narrow at thorough, an edge case at a time. Every level covers the same ground, in less or more depth. In a
59
+ capped step `ask_user` refuses a question past the cap; the step's closing confirmation, asked with
60
+ `confirms_phase: true`, is never counted. `end_questions` ends the step's questions sooner - best judgement's only
61
+ end. What the questions leave open, the agent decides itself and says what it chose. The level, and how many
62
+ questions each step has asked, are kept in the run's `run.json`, so a resumed run keeps them. A step it does not name,
63
+ or a run with nobody to ask, is not capped.
64
+
65
+ `spec-diff-per-answer:` under `metadata:` names the steps where every answer is a spec change - `brood`'s
66
+ `[product]`, at every questioning level. There `ask_user` refuses the next question until the last answer is written
67
+ with `write_spec_diff` (a confirmation, or a question dismissed, owes nothing), and `write_spec_diff` no longer waits
68
+ for an answer before a change: the agent writes as many as the step needs, those it decides itself included. The
69
+ level limits the questions, never the spec changes.
70
+
71
+ A step ends when its agent's turn does. A turn that ends on a question in the reply would end the step there, half
72
+ done, on a question no one reads - the person sees only what `ask_user` asks. So such a step is sent back, twice at
73
+ most: to ask with `ask_user` if the question is truly the person's, or, in an unpaced skill, to decide it itself -
74
+ then to carry on until the step is done.
75
+
76
+ `phases:` under `metadata:` says what each step does, in a short line a step by its name - for the person, not
77
+ the agent: while the skill runs, the line above the input shows its phases, and under them the one it is in with
78
+ its line (`groundwork - the language, frameworks, storage and architecture`). A phase's name alone
79
+ says little to someone who has not read the skill. Every skill with more than one step describes each; a line for a
80
+ step the skill does not have is an error, as a gate after one is.
81
+
82
+ A skill gets the tools its `allowed-tools` names, and nothing else: one that names none has none. So
83
+ every skill here names its own - the file tools (`read`, `write`, `edit`, `bash`, `ls`, `grep`, `find`)
84
+ for the ones that write specs, resources or scripts, only the reading ones for those that analyse
85
+ and report, and turri's own (`write_spec_diff` and the ones below) where a skill uses them.
86
+
87
+ What a run changed is part of its result: every file of the project it added, changed or removed -
88
+ its specs, resources, scripts; not the built code - or that it changed none, which is said as such,
89
+ so a run that wrote nothing is never reported as done. A skill without gates says it was not
90
+ reviewed. A `.plain` file a skill edited with the file tools is not rendered by it: the result says
91
+ so, for the conversation to render the change.
92
+
93
+ A skill acts on the work loop with the conversation's own tools, on the conversation's loop - never
94
+ copies of them (`src/loop-tools.ts`). Every skill with the loop has `drift`: what is left to regenerate, what
95
+ is regenerating, and "Fully regenerated" once nothing is. One that lists `regenerate` and `halt` in `allowed-tools` -
96
+ as `mature` does - can regenerate what a module's spec holds and halt a pulse, with why. `mature` lists two
97
+ tools of its own as well: `watch_jobs` waits for its pulses, coming back as soon as one struggles -
98
+ sent back four times or more, five setbacks, a spec conflict, a failure run again, over half an hour -
99
+ with what it went through; `spec_findings` reads back, numbered and each once, what the pulses recorded
100
+ since the run started: the assumptions and pitfalls in every codebase's working notes, a failed pulse's
101
+ diagnosis, a spec that contradicts itself.
102
+
103
+ Each step is told where its skill's files are, so a path it names - `references/intent.md` - and a
104
+ skill it says to follow - `implement-unit-testing-script` - are found without searching.
105
+
106
+ ## What the harness enforces, not the model
107
+
108
+ - **Whether review happens.** The gate is an edge. There is no prompt the executor can write that
109
+ skips it.
110
+ - **How many attempts.** `max_retries` is a counter in graph state. The executor is never asked how
111
+ many tries it would like.
112
+ - **What "fail" means.** Judges answer through a schema (`submit_verdict`), so the router branches
113
+ on a value. A judge that returns no verdict counts as a fail - silence is not consent.
114
+ - **What gets reviewed.** The executor's only write path for the artifact is `write_artifact`,
115
+ pointed at a path the graph chose. A step that produces no artifact fails its gate without
116
+ spending a single judge call.
117
+ - **Who is judging.** Each judge is a fresh session given the artifact and the rubric and nothing
118
+ else. It never sees the executor's transcript, because a judge that inherits the actor's
119
+ reasoning inherits its rationalisations.
120
+
121
+ `on_fail: escalate` (and `retry` once retries are exhausted) pauses the graph on a LangGraph
122
+ `interrupt`: in the TUI that is a real confirm dialog; with no UI there is nobody to ask, so the
123
+ gate stands and the run stops.
124
+
125
+ `judge_tools` gives a gate's judges read-only tools, for a rubric that checks what is on disk.
126
+ Judges get nothing by default, which is right when the artifact is the whole subject.
127
+
128
+ A gate can also have a `continuous` reviewer: a read-only agent that re-reads the work after every
129
+ change the step makes and puts its findings on the executor's next tool result.
130
+
131
+ ## What a step can do
132
+
133
+ - **Interview the user.** `ask_user` puts one question to the person. In the TUI it is a dialog;
134
+ from the CLI the options are numbered. Offering choices is required, not optional - a call with
135
+ neither `options` nor `free_form: true` is refused - and every list carries "Something else…".
136
+ `multi_select: true` keeps offering what is left until they are done. The skill's agent talks in a
137
+ session of its own that the person never sees, so what the person must read to answer - the brief
138
+ to confirm, the plan to approve - it passes as `ask_user`'s `context`, shown above the question in
139
+ the dialog; `brood`, `spawn` and `bud` pass their briefs so. The agents are told to keep it to what can be taken in at
140
+ a glance, about fifteen lines; one longer scrolls in a window of its own above the question (`src/question-dialog.ts`) -
141
+ PgUp/PgDn, Shift+Up/Down or the mouse wheel - so the question and its options always show below it. pi's own select,
142
+ which turri used before, put it all in its title, and a long brief pushed the options off the screen. Nothing else it wrote is shown there:
143
+ the last thing it said before asking - its account of a probe, a regeneration - is kept with the answer,
144
+ in the run's trace and the transcript, never in the dialog.
145
+
146
+ Beside "Something else…", every list an agent's question shows carries "Chat about this": the person says what they
147
+ want to talk over - why it is asked, what an option means for later - and it goes back to the agent that asked, as no
148
+ answer. A skill's agent answers in the `context` of the question asked again, since nothing else it writes reaches
149
+ them; the conversation's agent answers in its reply and ends its turn, the person going on in the chat. It is not
150
+ counted against the questioning level, and owes no spec change. The harness's own first question - how many the
151
+ phases may ask - has no agent to answer, and does not offer it. Either choice is backed out of with Esc, or by typing
152
+ nothing: the options come back, so one picked by accident costs nothing. Esc on the options themselves dismisses the
153
+ question.
154
+ - **Be asked something on the side.** A skill runs inside one `cultivate` call, so the conversation can answer
155
+ nothing until it returns. `/btw <question>` (`btw.ts`) can: pi runs a command at once, even mid-turn, and it starts
156
+ an agent of its own - fresh, on the chat's model, reading the project and turri and changing nothing - shown the
157
+ conversation, the running skill's trace and the end of its agent's session, and the pulses. It is told it is turri,
158
+ as the conversation's agent is (`identity.ts`), and answers as turri, in the first person - never as an onlooker
159
+ who sends the person to turri with a message to pass on. Its answer goes in the
160
+ transcript as an entry, in no model's context: the conversation and the skill go on as if nothing was asked. While
161
+ a question of the skill's has the keys, `/btw <question>` typed as the answer (under "Something else…") is the
162
+ same: answered, and the question asked again with the answer above it - the skill's agent never sees it.
163
+ - **Build while it interviews.** `write_spec_diff` (`brood`, `spawn`) writes a change to the
164
+ spec and queues its render in one call; the interview carries on while the builds run in the
165
+ background, one after another. `drift` shows how they stand. The person sees its calls as
166
+ `mutate`: a tool can be shown by a name of its own (`SHOWN_AS` in `regenerate/activity.ts`), while its
167
+ agent calls it by the one that says what it takes.
168
+ - **Regenerate, and conform.** A skill that names them in `allowed-tools` gets the conversation's own
169
+ `regenerate`, `drift`, `halt` and `conform`, on its loop: `conform` schedules a conformance pulse for functional specs not built
170
+ yet, as it does from the chat. The skill never waits for them.
171
+ - **Choose the render model**, when the user says so - `engine`, the same tool the
172
+ conversation has.
173
+
174
+ The harness still owns the sequence. A step chooses what to ask; it does not choose whether the
175
+ gate afterwards runs. A run is saved as it goes, so an interrupted one can be resumed where it
176
+ stopped.
177
+
178
+ While a skill runs in the chat, `cultivate`'s output leads with what its agent is doing now: the skill,
179
+ its step and how long each has taken, its last few tool calls, and the end of what it is saying,
180
+ refreshed every few seconds. pi shows only a running tool's first lines, and a step can take many
181
+ minutes without a new line in the trace. The result the conversation gets back keeps the trace
182
+ alone.
183
+
184
+ ## A project's own skills
185
+
186
+ A project can have skills of its own, in the same format: a folder a skill, with its `SKILL.md` - steps, gates,
187
+ rubrics in its `references/` - in `.turri/skills/` or `.agents/skills/` of the project's folder (`src/skills.ts`).
188
+ turri finds them as it starts, beside its own, and `cultivate` lists each as the project's, with where it is; `turri
189
+ cultivate <name>` runs one by name, as it does turri's own. They are run as turri's own are: the harness owns their
190
+ steps and gates, and their agents get the tools their frontmatter names.
191
+
192
+ A name is one skill. turri's own come first, then `.turri/skills/`, then `.agents/skills/`; a skill whose name is taken
193
+ already is left out, and the list says so - a project's `brood` never quietly stands in for turri's. A
194
+ skill that does not parse is said to have failed to load, by its folder.
195
+
196
+ `.turri/` is turri's scratch folder and ignores itself, all but `skills/`: its `.gitignore` is `*`, `!skills/`,
197
+ `!skills/**`, and one written before is brought up to date. pi reads `.agents/skills/` too, as skills of its own the
198
+ chat could follow by hand - the chat is told to run any `cultivate` lists with `cultivate`, so its gates still run.
199
+
200
+ The skills pi offers the chat leave out what turri has already (`src/chat-skills.ts`): a skill of a name turri runs
201
+ itself - its own, or the project's - and any skill about \*\*\*plain, its specs or their testing scripts. pi gathers
202
+ skills from `~/.pi/agent/skills`, `~/.agents/skills` and the `.agents/skills` of every folder from where turri started
203
+ up to its git root, so a folder above the project can hold another tool's \*\*\*plain skills - a `load-plain-reference`
204
+ the chat would load at the start of every session, though the whole \*\*\*plain reference, turri's
205
+ [rules](#rules), is in its system prompt already. The person is told once which were left out. A skill about anything
206
+ else is offered as pi offers it.
207
+
208
+ A skill is found once, as turri starts: one added while it runs is there the next time. A project's skill agent runs
209
+ as turri's do - pi's file tools and `bash` on the machine, no fence - so a project's skills are as trusted as the
210
+ project.
211
+
212
+ ## Rules
213
+
214
+ `genome/rules/` holds the \*\*\*plain authoring rules. Every rule, in full, is appended to the system
215
+ prompt of the conversation and of the agents that write specs - never of the ones that write code.
216
+ The block is byte-identical every time, so it sits inside the cached prefix - but it is ~40k
217
+ tokens. The trade is deliberate: a rule the model has to decide to go and read is a rule it can
218
+ skip. `src/rules.ts` is where to change it.
219
+
220
+ ## Trying the gate semantics
221
+
222
+ The gate machinery - the no-artifact rejection, the retry loop, escalation - is verified by live
223
+ runs rather than automated tests. To poke at it, give a skill a gate like the one in [the syntax](#the-syntax) - a
224
+ project's own, in `.turri/skills/`, with a rubric in its `references/` - run it with `turri cultivate <name>`, then
225
+ change the gate:
226
+
227
+ | change | expected |
228
+ | --- | --- |
229
+ | `max_retries: 0` | the first failure escalates instead of retrying |
230
+ | `on_fail: abort` | the first failure ends the run, exit 1 |
231
+ | `quorum: any` | one passing judge is enough |
232
+ | `judges: 3` | three sessions run concurrently |
233
+ | `model: google/gemini-3-pro` | judges run on another provider; an unconfigured model falls back with a note |
234
+ | `after: drafts` | rejected at load: `gate references unknown step 'drafts'` |
235
+
236
+ A trace, with a rubric strict enough that the first draft fails:
237
+
238
+ ```
239
+ > step draft attempt 1
240
+ | Expired cached tokens are now ignored and removed instead of being returned.
241
+ > gate draft 2 independent judges
242
+ judge 1: fail (2)
243
+ judge 2: fail (2)
244
+ retry <- gate draft
245
+ - [starts with Fixed:/Added:/Changed:] ...
246
+ - [names the source file that changed] ...
247
+
248
+ > step draft attempt 2
249
+ | Fixed: src/cache.ts now ignores and removes expired cached tokens instead of returning them.
250
+ > gate draft 2 independent judges
251
+ judge 1: pass
252
+ judge 2: pass
253
+ advance <- gate draft
254
+ ```
@@ -0,0 +1,76 @@
1
+ # Theming
2
+
3
+ Two brand themes ship in `.pi/themes/`, built from the brand tokens. `turri` starts in
4
+ `turri-dark`, the dark one; `--use-theme` picks another for that run:
5
+
6
+ ```bash
7
+ turri # turri-dark
8
+ turri --use-theme turri-light/turri-dark # follows terminal appearance
9
+ turri --use-theme dark # pi's own
10
+ ```
11
+
12
+ The second form follows your terminal's light/dark appearance. The default is for
13
+ `turri` only: pi's settings are left alone, so a plain `pi` keeps its theme. With the
14
+ themes copied to `~/.pi/agent/themes/` (`cp .pi/themes/*.json ~/.pi/agent/themes/`), `{"theme": "turri-dark"}` in
15
+ `~/.pi/agent/settings.json` gives a plain `pi` the same one.
16
+
17
+ Bad input fails quietly - an unknown `--use-theme` name and a malformed theme file both run without
18
+ complaint - so if the colours do not change, suspect a typo rather than the mechanism.
19
+
20
+ ## Why there are two
21
+
22
+ Half the palette is unusable on the other background. Contrast against brand black and brand
23
+ white:
24
+
25
+ | token | on `#0f0f0f` | on `#ffffff` |
26
+ |---|---|---|
27
+ | brand-yellow | 17.07 | 1.12 |
28
+ | brand-green | 14.77 | 1.30 |
29
+ | brand-green-light | 13.34 | 1.44 |
30
+ | brand-blue | **1.97** | 9.72 |
31
+ | brand-green-dark | 1.56 | 12.31 |
32
+ | brand-red | 3.38 | 5.67 |
33
+
34
+ So `turri-dark` carries yellow, green and green-light, and `turri-light` carries blue and
35
+ green-dark. **Brand blue never appears as text in the dark theme** - at 1.97:1 on brand black it is
36
+ illegible - so the dark theme's accent is brand yellow, and blue appears only in the light theme
37
+ and in both themes' HTML-export backgrounds. The one place blue stands for something in both is the spec
38
+ map's `◆` - a functional spec its conformance tests pass for: brand blue in a light theme, and in a dark
39
+ one its tint 55% of the way to white, `#919aec`, 7.34:1 on brand black (`brandBlue` in
40
+ `src/regenerate/plain-palette.ts`).
41
+
42
+ `--color-brand-status-warning` on white is 3.49:1, under the 4.5:1 bar for body text, so it is used
43
+ only for accents in the light theme, never for prose.
44
+
45
+ ## Backgrounds
46
+
47
+ The dark theme paints no panels: the backgrounds of your messages, turri's messages and the
48
+ tool rows are `""`, the terminal's own background, so the transcript reads as one surface whatever
49
+ the terminal's colour is - brand black or not, no block shows up as a box. Two are deliberately
50
+ painted, because they have to be visible to work:
51
+
52
+ | token | value | why |
53
+ |---|---|---|
54
+ | `selectedBg` | `#223936` | a selection you cannot see is not a selection |
55
+ | `searchMatchBg` | `#e0ff6e` | search hits, with black text on top |
56
+
57
+ With tool rows carrying no background, their state is signalled by the title and the diff colours.
58
+
59
+ **The window's background is your terminal's setting**, and the transcript is drawn on it. For the
60
+ brand look, set it to brand black, `#0f0f0f`, in your terminal profile.
61
+
62
+ ## Spec diffs
63
+
64
+ A spec diff in the panels is not painted with the theme's colours for code: those are made for code,
65
+ and the turri themes' backgrounds are all the surface colour, so a diff drawn with them would have no
66
+ fills and hardly any highlighting. It has a palette of its own (`src/regenerate/plain-palette.ts`), one for
67
+ a dark theme and one for a light, picked by the theme's appearance: a hue for each piece of \*\*\*plain -
68
+ a section's header, a concept, a link, text in quotes or backticks, a front matter key, a module,
69
+ a comment - at least 7:1 on the surface, and a fill behind each added and removed line, green and red,
70
+ that every colour still reads on.
71
+
72
+ ## The font
73
+
74
+ `--font-brand` cannot be applied. pi draws into a terminal, so the typeface is your terminal
75
+ emulator's setting, and NeueMontreal is proportional - a terminal needs a monospaced face. Set it
76
+ in iTerm2 / Ghostty / Alacritty, not here.