@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
package/dist/btw.js ADDED
@@ -0,0 +1,174 @@
1
+ /**
2
+ * /btw: a question on the side, answered while the conversation is busy - a skill's interview, its long steps - without
3
+ * waiting for it, and without changing it.
4
+ *
5
+ * A skill runs inside one tool call, so the chat model can answer nothing until the skill returns, and whatever the
6
+ * person types meanwhile waits for it as a message. pi runs an extension's command at once, even mid-turn: /btw starts
7
+ * an agent of its own - fresh, read only - that is shown the conversation, what the running skill has done and said so
8
+ * far, and what is regenerating, and answers. Its answer is shown and kept out of every model's context: the
9
+ * conversation and the skill go on as if nothing was asked. While a question of the skill's has the keys, the same is
10
+ * typed into its "Something else…" - answered, and the question asked again with the answer above it.
11
+ */
12
+ import { readFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { confinedTools } from "./regenerate/confine.js";
15
+ import { TURRI_IDENTITY } from "./identity.js";
16
+ import { ask, BEGIN, createExecutor } from "./pi.js";
17
+ /** How many tool calls the side agent may make before it is told to answer. */
18
+ export const SIDE_CALLS = 20;
19
+ /** How much of a transcript the side agent is shown, from its end. */
20
+ const CONVERSATION_CHARS = 24_000;
21
+ const SKILL_SESSION_CHARS = 16_000;
22
+ /** "/btw what is a frid?" -> "what is a frid?"; "/btw" alone -> ""; anything else is not a side question. */
23
+ export function sideQuestionOf(text) {
24
+ const match = /^\s*\/btw(?:\s+|$)([\s\S]*)$/i.exec(text);
25
+ return match === null ? undefined : match[1].trim();
26
+ }
27
+ let running;
28
+ /** Says a skill is running, until the function it returns is called. */
29
+ export function skillRunStarted(run) {
30
+ running = run;
31
+ return () => {
32
+ if (running === run)
33
+ running = undefined;
34
+ };
35
+ }
36
+ export function runningSkill() {
37
+ return running;
38
+ }
39
+ /**
40
+ * A session's messages as plain text - who said what, tool calls named - its last `maxChars`. Thinking and tool
41
+ * results are left out: what was said, and what was done, is what a side question is about.
42
+ */
43
+ export function transcriptOf(entries, maxChars) {
44
+ const lines = [];
45
+ for (const entry of entries) {
46
+ const { type, message } = entry;
47
+ if (type !== "message" || message === undefined)
48
+ continue;
49
+ const who = message.role === "user" ? "user" : message.role === "assistant" ? "assistant" : undefined;
50
+ if (who === undefined)
51
+ continue;
52
+ const parts = typeof message.content === "string" ? [{ type: "text", text: message.content }] : Array.isArray(message.content) ? message.content : [];
53
+ for (const part of parts) {
54
+ if (part.type === "text" && part.text !== undefined && part.text.trim() !== "")
55
+ lines.push(`${who}: ${part.text.trim()}`);
56
+ else if (part.type === "toolCall" && part.name !== undefined)
57
+ lines.push(`${who} called ${part.name}`);
58
+ }
59
+ }
60
+ const text = lines.join("\n\n");
61
+ return text.length > maxChars ? `…${text.slice(-maxChars)}` : text;
62
+ }
63
+ /** The running skill's agent's session, as text: its last turns, read from the session file run.json names. */
64
+ export function skillSessionText(runDir, maxChars = SKILL_SESSION_CHARS) {
65
+ try {
66
+ const manifest = JSON.parse(readFileSync(join(runDir, "run.json"), "utf8"));
67
+ if (manifest.sessionFile === undefined)
68
+ return "";
69
+ const entries = readFileSync(manifest.sessionFile, "utf8")
70
+ .split("\n")
71
+ .filter((line) => line.trim() !== "")
72
+ .flatMap((line) => {
73
+ try {
74
+ return [JSON.parse(line)];
75
+ }
76
+ catch {
77
+ return [];
78
+ }
79
+ });
80
+ return transcriptOf(entries, maxChars);
81
+ }
82
+ catch {
83
+ return "";
84
+ }
85
+ }
86
+ export function sideQuestionPrompt(side) {
87
+ return [
88
+ TURRI_IDENTITY,
89
+ "",
90
+ "The person is in the middle of something with you - a skill, a pulse - and has asked you a question on the side",
91
+ "with /btw. Answer it as turri: in the first person, to them, in whole sentences - briefly and plainly, from what is",
92
+ "below and what you can read. Never speak of turri as someone else, and never give them a message to pass on to",
93
+ "turri: they are talking to turri. You can read the project and your own source, and change nothing. Nothing you",
94
+ "say here reaches the conversation or the skill: they go on as they were. If the question is really a change of",
95
+ "course - do something else, stop, change an answer - say how: answer the skill's next question that way, /halt a",
96
+ "pulse, or ask you for it in the conversation once the skill is done; then say what you will do when they do.",
97
+ "",
98
+ `The project is ${side.projectDir}: its .plain specs, config.turri.yaml, plain_modules. You are ${side.harnessDir}:`,
99
+ "your source (src/), your skills (genome/skills/), the ***plain authoring rules (genome/rules/) and your docs (docs/).",
100
+ "",
101
+ "Reply with the answer alone, as you would say it - no preamble about how you found it.",
102
+ "",
103
+ "<conversation>",
104
+ side.conversation.trim() !== "" ? side.conversation : "(nothing yet)",
105
+ "</conversation>",
106
+ ...(side.skill !== undefined
107
+ ? [
108
+ "",
109
+ `A skill is running now: ${side.skill.name}${side.skill.step !== "" ? `, in its ${side.skill.step}` : ""}. Its own folder is`,
110
+ `${side.skill.dir}. What it has done - steps, gates, every question and answer:`,
111
+ "<skill-trace>",
112
+ ...side.skill.trace,
113
+ "</skill-trace>",
114
+ ...(side.skill.session.trim() !== ""
115
+ ? ["", "Its agent's own session, the end of it - what it said and did:", "<skill-session>", side.skill.session, "</skill-session>"]
116
+ : []),
117
+ ]
118
+ : []),
119
+ ...(side.pulses.length > 0 ? ["", "Pulses this session:", ...side.pulses.map((line) => `- ${line}`)] : []),
120
+ "",
121
+ "<question>",
122
+ side.question,
123
+ "</question>",
124
+ ].join("\n");
125
+ }
126
+ /** Answers a side question with an agent of its own, read only, on `model`. */
127
+ export async function answerSideQuestion(side, model) {
128
+ const fence = confinedTools({ workdir: side.projectDir, read: [side.projectDir, side.harnessDir], write: [], noBash: true });
129
+ const session = await createExecutor({
130
+ cwd: side.projectDir,
131
+ customTools: fence.tools,
132
+ tools: fence.names,
133
+ model,
134
+ label: "side question",
135
+ role: "chat",
136
+ system: fence.note,
137
+ task: sideQuestionPrompt(side),
138
+ budget: { calls: SIDE_CALLS, finish: [] },
139
+ });
140
+ try {
141
+ const answer = (await ask(session, BEGIN)).trim();
142
+ return answer !== "" ? answer : "(no answer came back)";
143
+ }
144
+ finally {
145
+ session.dispose();
146
+ }
147
+ }
148
+ /** The answer to a side question, as it is shown above the question it was asked under. */
149
+ export function asideBlock(question, answer) {
150
+ return `btw: ${question}\n${answer}`;
151
+ }
152
+ export const BTW_HOW = "Type your question after /btw - /btw what does this step do?";
153
+ /**
154
+ * A question put to the person, where "/btw ..." typed as the answer is a side question: answered, and the same
155
+ * question asked again with the answer above it - the asker never sees it. Only the latest is kept above the question.
156
+ */
157
+ export async function askWithAsides(request, askUser, aside) {
158
+ let current = request;
159
+ for (;;) {
160
+ const answer = await askUser(current);
161
+ const side = sideQuestionOf(answer);
162
+ if (side === undefined)
163
+ return answer;
164
+ let reply;
165
+ try {
166
+ reply = side === "" ? BTW_HOW : await aside(side);
167
+ }
168
+ catch (error) {
169
+ reply = `The side question could not be answered: ${error instanceof Error ? error.message : String(error)}`;
170
+ }
171
+ const above = side === "" ? reply : asideBlock(side, reply);
172
+ current = { ...request, context: [above, request.context?.trim() ?? ""].filter((part) => part !== "").join("\n\n") };
173
+ }
174
+ }
@@ -0,0 +1,37 @@
1
+ import { modelSpec, projectModels } from "./config.js";
2
+ /**
3
+ * The two models of `turri`, from its arguments and the project's config.turri.yaml.
4
+ *
5
+ * The harness model is the one the person talks to: pi's own `--model`, which `--harness-model`
6
+ * is another name for, or else `harness-model:` in config.turri.yaml. The coding model is the one
7
+ * every render runs on: `--coding-model`, or else `coding-model:` in config.turri.yaml - pi knows
8
+ * nothing of it, so it reaches the extension through the environment. Either can still be
9
+ * changed in the conversation: pi's /model, engine.
10
+ */
11
+ /** The environment variable a chat's --coding-model reaches the extension through. */
12
+ export const CODING_MODEL_ENV = "TURRI_CODING_MODEL";
13
+ /** Take a `--name value` or `--name=value` out of the arguments: its value, and the rest. The last one wins. */
14
+ export function takeOption(args, name) {
15
+ const rest = [];
16
+ let value;
17
+ for (let i = 0; i < args.length; i += 1) {
18
+ const arg = args[i];
19
+ if (arg === name && i + 1 < args.length)
20
+ value = args[++i];
21
+ else if (arg.startsWith(`${name}=`))
22
+ value = arg.slice(name.length + 1);
23
+ else
24
+ rest.push(arg);
25
+ }
26
+ return { value, rest };
27
+ }
28
+ /** pi's arguments, with the harness model in them, and the coding model for the extension. */
29
+ export function chatModels(args, config) {
30
+ const coding = takeOption(args, "--coding-model");
31
+ const harness = takeOption(coding.rest, "--harness-model");
32
+ const piArgs = harness.rest;
33
+ // pi's own flags win: a --model or --provider given as they are is left alone.
34
+ const given = piArgs.some((arg) => /^--(model|provider)(=|$)/.test(arg));
35
+ const harnessModel = given ? undefined : (harness.value ?? modelSpec(projectModels(config).harness));
36
+ return { piArgs: harnessModel !== undefined ? ["--model", harnessModel, ...piArgs] : piArgs, codingModel: coding.value };
37
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The pi skills the conversation is not offered, because turri has them already.
3
+ *
4
+ * pi lists for the conversation every skill it finds: `~/.pi/agent/skills`, `~/.agents/skills`, and the `.agents/skills`
5
+ * of every folder from the one turri started in up to its git root - so a folder above a project can hand the chat
6
+ * another tool's ***plain skills, a `load-plain-reference` it then loads at the start of a session. turri needs none of
7
+ * them: the whole ***plain reference - the authoring rules - is in the conversation's system prompt already, and turri
8
+ * runs its own skills, and the project's, with `cultivate`, steps and gates and all. A skill the chat followed by hand
9
+ * instead would skip those gates, and one from another tool may say otherwise than turri's rules. So the conversation
10
+ * is offered neither: no skill turri runs itself, and none about ***plain. A skill about anything else - a commit
11
+ * message, a deploy - is offered as pi offers it.
12
+ */
13
+ /** What a skill says it is about, that turri covers: ***plain specs and their testing scripts. */
14
+ const ABOUT_PLAIN = /\*\*\*plain|\\\*\\\*\\\*plain|\.plain\b|\bcodeplain\b|functional specs?\b|testing scripts?\b|run_unittests|run_conformance_tests/i;
15
+ /** Whether turri has what a pi skill offers: it runs a skill of that name itself, or the skill is about ***plain. */
16
+ export function supersededByTurri(skill, turriRuns) {
17
+ return turriRuns.has(skill.name) || ABOUT_PLAIN.test(`${skill.name} ${skill.description}`);
18
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * One entry point, so the tools can be used from wherever the specification lives.
4
+ *
5
+ * Paths are resolved against the current working directory, and a .plain file's linked
6
+ * resources are too - so the intended use is to `cd` to the module and name the file,
7
+ * rather than to run from this repo and reach across with absolute paths.
8
+ */
9
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
10
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
11
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
12
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
13
+ });
14
+ }
15
+ return path;
16
+ };
17
+ import { spawnSync } from "node:child_process";
18
+ import { dirname, join } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+ import { CODING_MODEL_ENV, chatModels } from "./chat-models.js";
21
+ import { readProjectConfig } from "./config.js";
22
+ import { extensionInstalledGlobally, PACKAGE_ROOT, themesInstalledGlobally } from "./environment.js";
23
+ import { dropNoise } from "./quiet.js";
24
+ /**
25
+ * Start pi with this project's extension and themes already wired in.
26
+ *
27
+ * Saves both a global pi install and the `-e <absolute path>` that a project-local
28
+ * extension otherwise needs before the project is trusted.
29
+ */
30
+ /**
31
+ * Where this file is, and how its siblings are named: the sources in a clone (`src/*.ts`), the compiled files in an
32
+ * installed package (`dist/*.js`).
33
+ */
34
+ const HERE = dirname(fileURLToPath(import.meta.url));
35
+ const OWN_EXTENSION = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
36
+ /** Whether pi is installed where Node finds it - beside turri, or wherever npm hoisted it. */
37
+ function piInstalled() {
38
+ try {
39
+ import.meta.resolve("@earendil-works/pi-coding-agent");
40
+ return true;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ /** The theme `turri` starts in. */
47
+ const DEFAULT_THEME = "turri-dark";
48
+ function chat(args) {
49
+ if (!piInstalled()) {
50
+ console.error(`pi is not installed beside turri (${PACKAGE_ROOT}). Run npm install there, or install turri again.`);
51
+ process.exit(1);
52
+ }
53
+ // pi itself, through a launcher that leaves out pi's startup header - see pi-launch.ts.
54
+ const launcher = join(HERE, `pi-launch${OWN_EXTENSION}`);
55
+ // Loading the same extension twice is an error in pi, not a no-op, so the explicit -e is left out while an older
56
+ // turri's file still loads turri into every pi - turri doctor offers to remove it.
57
+ const extension = extensionInstalledGlobally()
58
+ ? []
59
+ : ["-e", join(HERE, `extension${OWN_EXTENSION}`)];
60
+ const themes = themesInstalledGlobally() ? [] : ["--theme", join(PACKAGE_ROOT, ".pi", "themes")];
61
+ // turri's own theme, unless this run names another. For this run only: pi's settings
62
+ // are left as they are, so a plain `pi` keeps its theme.
63
+ const useTheme = args.some((arg) => arg === "--use-theme" || arg.startsWith("--use-theme=")) ? [] : ["--use-theme", DEFAULT_THEME];
64
+ // The model the person talks to, and the one every render runs on - see chat-models.ts.
65
+ const { piArgs, codingModel } = chatModels(args, readProjectConfig(process.cwd()));
66
+ // Fullscreen: pi owns the viewport, so the conversation stays where it is scrolled to while the
67
+ // jobs redraw - in the terminal's own scrollback, every redraw pulls it back to the bottom.
68
+ const tuiMode = args.some((arg) => arg === "--tui-mode" || arg.startsWith("--tui-mode=")) ? [] : ["--tui-mode", "fullscreen"];
69
+ const result = spawnSync(process.execPath, [launcher, ...extension, ...themes, ...useTheme, ...tuiMode, ...piArgs], {
70
+ stdio: "inherit",
71
+ cwd: process.cwd(),
72
+ env: codingModel ? { ...process.env, [CODING_MODEL_ENV]: codingModel } : process.env,
73
+ });
74
+ process.exit(result.status ?? 1);
75
+ }
76
+ const COMMANDS = {
77
+ regenerate: { module: "./regenerate-cli.ts", blurb: "regenerate a module and its chain from the spec as it is (with frids: regenerate those again)" },
78
+ glimpse: { module: "./regenerate-cli.ts", blurb: "plan the work and print the first job's prompt, starting nothing", args: ["--dry-run"] },
79
+ drift: { module: "./drift-cli.ts", blurb: "what is left to regenerate: how far the code has drifted from its spec" },
80
+ conform: { module: "./conform-cli.ts", blurb: "write and run a module's conformance tests" },
81
+ "spec-diff": { module: "./spec-diff-cli.ts", blurb: "apply a change to the specs, given as a git diff, and regenerate it" },
82
+ validate: { module: "./validate-cli.ts", blurb: "validate a ***plain spec, regenerating nothing" },
83
+ lineage: { module: "./lineage-cli.ts", blurb: "a module's lineage: what was regenerated into it, and when" },
84
+ release: { module: "./release-cli.ts", blurb: "release what is regenerated to dist/<module> and activate it on this machine" },
85
+ cultivate: { module: "./cultivate-cli.ts", blurb: "run a gated skill" },
86
+ };
87
+ /** Getting the machine ready, and checking it is. */
88
+ const SETUP_COMMANDS = {
89
+ "init-config": "write a quick start config.turri.yaml here, from the vendor you use: a strong model to talk to, a smaller one to write the code",
90
+ doctor: "check what turri needs - in a project, what it needs of this machine, and that its models answer",
91
+ };
92
+ const USAGE = [
93
+ "usage: turri [options] open the conversation here: pi, with turri's tools and themes loaded",
94
+ " turri <command> [args]",
95
+ "",
96
+ ` ${"options".padEnd(12)} --harness-model <m> the model you chat with --coding-model <m> the model renders run on`,
97
+ ` ${"".padEnd(12)} --stack <name> build with config.turri.<name>.yaml over config.turri.yaml - here and in every command`,
98
+ ` ${"".padEnd(12)} --use-theme <t> another theme than turri-dark - and every option of pi's own`,
99
+ "",
100
+ ...Object.entries(COMMANDS).map(([name, { blurb }]) => ` ${name.padEnd(12)} ${blurb}`),
101
+ "",
102
+ ...Object.entries(SETUP_COMMANDS).map(([name, blurb]) => ` ${name.padEnd(12)} ${blurb}`),
103
+ "",
104
+ "Run `turri <command> --help` for that command's options.",
105
+ "",
106
+ "Paths are relative to the current directory. Run turri from the directory holding the",
107
+ "specification, so that linked resources inside it resolve the way the parser expects.",
108
+ ].join("\n");
109
+ dropNoise();
110
+ // --stack <name>, for any command and for the conversation: the stack everything is built with (stacks.ts), taken out
111
+ // before the rest is read - a command never sees it.
112
+ const argv = await withoutStack(process.argv.slice(2));
113
+ const [command, ...rest] = argv;
114
+ async function withoutStack(args) {
115
+ const kept = [];
116
+ let stack;
117
+ for (let at = 0; at < args.length; at += 1) {
118
+ const arg = args[at];
119
+ if (arg === "--stack")
120
+ stack = args[++at] ?? "";
121
+ else if (arg.startsWith("--stack="))
122
+ stack = arg.slice("--stack=".length);
123
+ else
124
+ kept.push(arg);
125
+ }
126
+ if (stack === undefined)
127
+ return kept;
128
+ const { switchStack } = await import("./stacks.js");
129
+ try {
130
+ switchStack(process.cwd(), stack);
131
+ }
132
+ catch (error) {
133
+ console.error(error instanceof Error ? error.message : String(error));
134
+ process.exit(1);
135
+ }
136
+ return kept;
137
+ }
138
+ if (command === "-h" || command === "--help") {
139
+ console.log(USAGE);
140
+ process.exit(0);
141
+ }
142
+ // turri alone is the conversation, and so is turri with pi's options - `turri -c`, `turri --harness-model <m>`. A word
143
+ // turri does not know is an error below, not a conversation started on it.
144
+ if (command === undefined || command.startsWith("-"))
145
+ chat(argv);
146
+ if (command === "init-config") {
147
+ const { main } = await import("./init-config.js");
148
+ process.exit(await main(rest));
149
+ }
150
+ if (command === "doctor") {
151
+ const { main } = await import("./setup.js");
152
+ process.exit(await main(rest));
153
+ }
154
+ const entry = COMMANDS[command];
155
+ if (entry === undefined) {
156
+ console.error(`unknown command: ${command}\n\n${USAGE}`);
157
+ process.exit(1);
158
+ }
159
+ // The subcommands parse process.argv themselves, so present it as if they were invoked
160
+ // directly. Their agents are the coding agents: --coding-model is their --model.
161
+ process.argv = [
162
+ process.argv[0],
163
+ process.argv[1],
164
+ ...rest.flatMap((arg) => arg === "--coding-model" ? ["--model"] : arg.startsWith("--coding-model=") ? ["--model", arg.slice("--coding-model=".length)] : [arg]),
165
+ ...(entry.args ?? []),
166
+ ];
167
+ // A headless run is stopped by a signal - SIGTERM, SIGHUP, Ctrl-C - whatever else listens for it: it exits, and what
168
+ // its agents left running goes with it (shell.ts kills every process group on exit). Left to the libraries' own
169
+ // handlers, one ran on for minutes after it was told to stop, building in the same codebase as the run after it.
170
+ if (!["validate", "check", "drift", "lineage"].includes(command)) {
171
+ for (const [signal, code] of [["SIGTERM", 143], ["SIGHUP", 129], ["SIGINT", 130]]) {
172
+ process.on(signal, () => {
173
+ console.error(`\nstopped (${signal})`);
174
+ process.exit(code);
175
+ });
176
+ }
177
+ }
178
+ await import(__rewriteRelativeImportExtension(entry.module));
@@ -0,0 +1,197 @@
1
+ import { CONFIG_FILE, LEGACY_CONFIG_FILE, THINKING_LEVELS, usesLegacyConfig } from "./config.js";
2
+ import { currentValue, dotenvFiles, EFFECT_TEXT, overriddenBy, parseValue, pathOptions, readConfigText, settingOf, settings, shownValue, writeSetting, } from "./config-edit.js";
3
+ /** The option that unsets a setting, for a setting. */
4
+ const unsetOption = (setting) => `unset - ${setting.unset}`;
5
+ const TYPE_ANOTHER = "type another…";
6
+ const DONE = "done";
7
+ const ROLES_ENTRY = "models.<role> - each agent role's own model, thinking and turn limit…";
8
+ /** The models pi can reach here, signed in: provider/id. */
9
+ function reachable(ctx) {
10
+ try {
11
+ return (ctx?.modelRegistry.getAvailable() ?? []).map((model) => `${model.provider}/${model.id}`).sort();
12
+ }
13
+ catch {
14
+ return [];
15
+ }
16
+ }
17
+ /** A setting's line in the list: its key, what it is now - or what it is unset - and what it does. */
18
+ function listLine(setting, text) {
19
+ const value = currentValue(text, setting.key);
20
+ return `${setting.key}: ${value === undefined ? `(unset - ${setting.unset})` : shownValue(value)}`;
21
+ }
22
+ export function registerTurriConfig(pi, options) {
23
+ const { cwd } = options;
24
+ // The models for completions, which are given no context: the last one seen.
25
+ let seen;
26
+ pi.on("session_start", (_event, ctx) => {
27
+ seen = ctx;
28
+ });
29
+ /** Set a setting to what was typed - or unset it - and say what it did and when it takes effect. */
30
+ const apply = async (setting, typed, ctx) => {
31
+ const models = reachable(ctx);
32
+ const parsed = parseValue(setting, typed, models.length > 0 ? (model) => models.includes(model) : undefined);
33
+ if ("error" in parsed) {
34
+ ctx.ui.notify(parsed.error, "error");
35
+ return;
36
+ }
37
+ const value = "unset" in parsed ? undefined : parsed.value;
38
+ // The first setting written where the project is still read from config.yaml: copied to config.turri.yaml first.
39
+ const adopting = usesLegacyConfig(cwd);
40
+ const text = writeSetting(cwd, setting.key, value);
41
+ const notes = [];
42
+ // What turri fixed at its start, changed now.
43
+ if (setting.key === "harness-model" || setting.key === "models.chat") {
44
+ const said = await options.live.chatModel(options.chatSpec(), ctx);
45
+ if (said !== undefined)
46
+ notes.push(said);
47
+ }
48
+ if (setting.key === "coding-model" || setting.key === "models.coding" || setting.key === "models.default") {
49
+ const said = options.live.codingModel();
50
+ if (said !== undefined)
51
+ notes.push(said);
52
+ }
53
+ if (setting.key === "parallel-jobs") {
54
+ const said = options.live.parallelJobs(typeof value === "number" ? value : undefined);
55
+ if (said !== undefined)
56
+ notes.push(said);
57
+ }
58
+ if (adopting)
59
+ notes.push(`${LEGACY_CONFIG_FILE} was copied to ${CONFIG_FILE} first - turri reads its own file from now on, and leaves ${LEGACY_CONFIG_FILE} as it is.`);
60
+ notes.push(...overriddenBy(cwd, text, setting));
61
+ const what = value === undefined ? `${setting.key} unset - ${setting.unset}` : `${setting.key}: ${shownValue(value)}`;
62
+ ctx.ui.notify([`${what} - written to ${CONFIG_FILE}, ${EFFECT_TEXT[setting.effect]}.`, ...notes].join("\n"), "info");
63
+ };
64
+ /** Ask for a setting's value, as its kind takes it: what is typed comes back, or nothing when the dialog is left. */
65
+ const askValue = async (setting, ctx) => {
66
+ const text = readConfigText(cwd);
67
+ const now = currentValue(text, setting.key);
68
+ const title = `${setting.key} - ${setting.about}\nNow: ${now === undefined ? `unset - ${setting.unset}` : shownValue(now)}`;
69
+ const pick = async (choices, typed = true) => {
70
+ const offered = [...choices, ...(typed ? [TYPE_ANOTHER] : []), unsetOption(setting)];
71
+ const chosen = await ctx.ui.select(title, offered);
72
+ if (chosen === undefined)
73
+ return undefined;
74
+ if (chosen === unsetOption(setting))
75
+ return "unset";
76
+ if (chosen === TYPE_ANOTHER)
77
+ return ctx.ui.input(title, setting.kind === "path" ? "path from the project root" : "value");
78
+ return chosen;
79
+ };
80
+ switch (setting.kind) {
81
+ case "boolean":
82
+ return pick(["true", "false"], false);
83
+ case "choice":
84
+ return pick(setting.options ?? [], false);
85
+ case "number":
86
+ case "budget":
87
+ return pick(setting.options ?? []);
88
+ case "path":
89
+ return pick(pathOptions(cwd, setting.key));
90
+ case "model": {
91
+ const model = await pick(reachable(ctx));
92
+ // Typed with its level - provider/id:low - it is whole already.
93
+ if (model === undefined || model === "unset" || model.includes(":"))
94
+ return model;
95
+ const level = await ctx.ui.select(`${model} - how hard it thinks`, [...THINKING_LEVELS, "the role's own level"]);
96
+ if (level === undefined)
97
+ return undefined;
98
+ return level === "the role's own level" ? model : `${model}:${level}`;
99
+ }
100
+ case "list": {
101
+ const choices = setting.key === "agent-env.files" ? dotenvFiles(cwd) : (setting.options ?? []);
102
+ if (choices.length === 0) {
103
+ const typed = await ctx.ui.input(`${title}\nType them, separated by commas - or unset.`, Array.isArray(now) ? now.join(", ") : "");
104
+ return typed;
105
+ }
106
+ // A tick a chosen one: pick to turn one on or off, done to keep them.
107
+ const chosen = new Set(Array.isArray(now) ? now.map(String) : []);
108
+ for (;;) {
109
+ const offered = [DONE, ...choices.map((choice) => `${chosen.has(choice) ? "✓" : " "} ${choice}`), unsetOption(setting)];
110
+ const picked = await ctx.ui.select(`${title}\nPick to turn one on or off, then done.`, offered);
111
+ if (picked === undefined)
112
+ return undefined;
113
+ if (picked === unsetOption(setting))
114
+ return "unset";
115
+ if (picked === DONE)
116
+ return chosen.size === 0 ? "unset" : [...chosen].join(", ");
117
+ const name = picked.slice(2);
118
+ if (chosen.has(name))
119
+ chosen.delete(name);
120
+ else
121
+ chosen.add(name);
122
+ }
123
+ }
124
+ }
125
+ };
126
+ /** Pick a setting from the list - the models of each role behind one entry. */
127
+ const askSetting = async (ctx) => {
128
+ const text = readConfigText(cwd);
129
+ const all = settings(options.rules());
130
+ const top = all.filter((setting) => !setting.key.startsWith("models."));
131
+ const lines = [...top.map((setting) => listLine(setting, text)), ROLES_ENTRY];
132
+ const chosen = await ctx.ui.select(`/turri-config - the project's ${CONFIG_FILE}. Pick a setting to change:`, lines);
133
+ if (chosen === undefined)
134
+ return undefined;
135
+ if (chosen === ROLES_ENTRY) {
136
+ const roles = all.filter((setting) => setting.key.startsWith("models."));
137
+ const role = await ctx.ui.select("Which role's model or turn limit?", roles.map((setting) => listLine(setting, text)));
138
+ return role === undefined ? undefined : roles.find((setting) => role.startsWith(`${setting.key}:`));
139
+ }
140
+ return top.find((setting) => chosen.startsWith(`${setting.key}:`));
141
+ };
142
+ pi.registerCommand("turri-config", {
143
+ description: `Change a setting of the project's ${CONFIG_FILE}`,
144
+ getArgumentCompletions: (prefix) => {
145
+ const all = settings(options.rules());
146
+ const space = prefix.indexOf(" ");
147
+ if (space === -1) {
148
+ const items = all.filter((setting) => setting.key.startsWith(prefix.trim().toLowerCase()));
149
+ return items.length === 0 ? null : items.map((setting) => ({ value: `${setting.key} `, label: setting.key, description: setting.about }));
150
+ }
151
+ const setting = settingOf(prefix.slice(0, space), options.rules());
152
+ if (setting === undefined)
153
+ return null;
154
+ const typed = prefix.slice(space + 1).trimStart();
155
+ const candidates = setting.kind === "boolean"
156
+ ? ["true", "false"]
157
+ : setting.kind === "model"
158
+ ? typed.includes(":")
159
+ ? THINKING_LEVELS.map((level) => `${typed.slice(0, typed.lastIndexOf(":"))}:${level}`)
160
+ : reachable(seen)
161
+ : setting.kind === "path"
162
+ ? pathOptions(cwd, setting.key)
163
+ : setting.key === "agent-env.files"
164
+ ? dotenvFiles(cwd)
165
+ : (setting.options ?? []);
166
+ const items = [...candidates, "unset"].filter((candidate) => candidate.startsWith(typed)).slice(0, 40);
167
+ return items.length === 0 ? null : items.map((candidate) => ({ value: `${setting.key} ${candidate}`, label: candidate }));
168
+ },
169
+ handler: async (args, ctx) => {
170
+ seen = ctx;
171
+ const typed = args.trim();
172
+ const space = typed.indexOf(" ");
173
+ const key = space === -1 ? typed : typed.slice(0, space);
174
+ const value = space === -1 ? "" : typed.slice(space + 1).trim();
175
+ if (!ctx.hasUI && (key === "" || value === ""))
176
+ return;
177
+ let setting;
178
+ if (key === "") {
179
+ setting = await askSetting(ctx);
180
+ if (setting === undefined)
181
+ return;
182
+ }
183
+ else {
184
+ setting = settingOf(key, options.rules());
185
+ if (setting === undefined) {
186
+ const near = settings(options.rules()).filter((entry) => entry.key.includes(key.toLowerCase())).map((entry) => entry.key);
187
+ ctx.ui.notify(`No setting ${key}.${near.length > 0 ? ` Did you mean ${near.slice(0, 5).join(", ")}?` : " /turri-config alone lists them."}`, "error");
188
+ return;
189
+ }
190
+ }
191
+ const given = value !== "" ? value : await askValue(setting, ctx);
192
+ if (given === undefined)
193
+ return;
194
+ await apply(setting, given, ctx);
195
+ },
196
+ });
197
+ }