@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,75 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { NOTES_FILE } from "./notes.js";
5
+ import { RECORD_FILE } from "./records.js";
6
+ import { listFiles } from "./testing.js";
7
+ /**
8
+ * What a render agent is told about the code before it starts.
9
+ *
10
+ * Every render is a fresh session, and without this each one begins by rediscovering the
11
+ * codebase - listing directories, grepping for where things are - which on a long module is
12
+ * the same exploration repeated once per functionality, getting longer as the code does. The
13
+ * harness already knows the answers, and knows them exactly: the files, which
14
+ * functionality put which of them there, and the test command that passed. None of it is
15
+ * the agent's recollection, so none of it can be stale.
16
+ */
17
+ /** Files by content, so a change is noticed whatever happened to its timestamp. */
18
+ export function digestFiles(root) {
19
+ const digests = new Map();
20
+ for (const file of listFiles(root)) {
21
+ try {
22
+ digests.set(file, createHash("sha1").update(readFileSync(join(root, file))).digest("hex"));
23
+ }
24
+ catch {
25
+ // Gone between listing and reading: it is not there to describe.
26
+ }
27
+ }
28
+ return digests;
29
+ }
30
+ export function diffFiles(before, after) {
31
+ const added = [];
32
+ const changed = [];
33
+ for (const [file, digest] of after) {
34
+ const previous = before.get(file);
35
+ if (previous === undefined)
36
+ added.push(file);
37
+ else if (previous !== digest)
38
+ changed.push(file);
39
+ }
40
+ const removed = [...before.keys()].filter((file) => !after.has(file));
41
+ return { added: added.sort(), changed: changed.sort(), removed: removed.sort() };
42
+ }
43
+ /** Whether the work has written nothing in `root` since `atStart` - its working notes and the build record aside. */
44
+ export function wroteNothing(root, atStart) {
45
+ const { added, changed, removed } = diffFiles(atStart, digestFiles(root));
46
+ return [...added, ...changed, ...removed].every((file) => file === NOTES_FILE || file === RECORD_FILE);
47
+ }
48
+ /** Beyond this, the tree is summarised by directory rather than listed file by file. */
49
+ const MAX_LISTED_FILES = 150;
50
+ function fileTree(files) {
51
+ if (files.length <= MAX_LISTED_FILES)
52
+ return files.map((file) => ` ${file}`);
53
+ // Too many to list: one line per top-level directory with its file count, and the files
54
+ // at the top level itself, which are usually the entry points.
55
+ const counts = new Map();
56
+ const topLevel = [];
57
+ for (const file of files) {
58
+ const slash = file.indexOf("/");
59
+ if (slash === -1)
60
+ topLevel.push(file);
61
+ else
62
+ counts.set(file.slice(0, slash), (counts.get(file.slice(0, slash)) ?? 0) + 1);
63
+ }
64
+ return [
65
+ ...topLevel.map((file) => ` ${file}`),
66
+ ...[...counts].sort(([a], [b]) => a.localeCompare(b)).map(([dir, count]) => ` ${dir}/ (${count} files)`),
67
+ ];
68
+ }
69
+ /** The map of the codebase as it stands - or null when there is no code yet. */
70
+ export function codebaseMap(buildDir) {
71
+ const files = listFiles(buildDir);
72
+ if (files.length === 0)
73
+ return null;
74
+ return [`Files (${files.length}):`, ...fileTree(files)].join("\n");
75
+ }
@@ -0,0 +1,312 @@
1
+ import { existsSync } from "node:fs";
2
+ import { posix, win32 } from "node:path";
3
+ import { isCredentialFile } from "./credentials.js";
4
+ import { RECORD_FILE } from "./records.js";
5
+ /** What of the machine an agent's command may read, beside its roots and its toolchains: the system's own folders. */
6
+ export function machineReadable(platform = process.platform) {
7
+ if (platform === "win32") {
8
+ const root = process.env.SystemRoot ?? "C:\\Windows";
9
+ return [root, process.env.ProgramFiles ?? "C:\\Program Files", process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)"];
10
+ }
11
+ return [
12
+ "/usr",
13
+ "/bin",
14
+ "/sbin",
15
+ "/lib",
16
+ "/lib32",
17
+ "/lib64",
18
+ "/libexec",
19
+ "/etc",
20
+ "/private/etc",
21
+ "/opt/homebrew",
22
+ "/opt/local",
23
+ "/System",
24
+ "/Library/Java",
25
+ "/Library/Developer",
26
+ "/Applications/Xcode.app",
27
+ "/nix",
28
+ "/snap",
29
+ "/proc/self",
30
+ "/proc/cpuinfo",
31
+ "/proc/meminfo",
32
+ ];
33
+ }
34
+ /** Devices any command may use. */
35
+ const DEVICES = ["/dev/null", "/dev/zero", "/dev/random", "/dev/urandom", "/dev/stdin", "/dev/stdout", "/dev/stderr", "/dev/tty", "/dev/fd"];
36
+ /**
37
+ * The top-level folders a path is taken to name something on disk under - beside the fence's own and what exists here.
38
+ * `/api/v1` in a curl or `/orders/1` in a JSON body names a route, not a file, and is let through.
39
+ */
40
+ const KNOWN_TOPS = new Set([
41
+ "Users", "home", "root", "etc", "var", "tmp", "private", "opt", "usr", "bin", "sbin", "lib", "lib32", "lib64", "libexec",
42
+ "System", "Library", "Applications", "Volumes", "mnt", "media", "srv", "proc", "sys", "dev", "run", "boot", "snap", "nix", "cores",
43
+ ]);
44
+ /** Programs that run the command after them. */
45
+ const WRAPPERS = new Set(["env", "exec", "time", "nohup", "command", "nice", "setsid", "xargs", "timeout", "stdbuf", "builtin", "caffeinate"]);
46
+ /** The system's package managers, and what they install with. */
47
+ const SYSTEM_PACKAGE_MANAGERS = new Set(["brew", "apt", "apt-get", "aptitude", "dnf", "yum", "apk", "pacman", "port", "choco", "winget", "scoop", "zypper", "snap", "flatpak", "nix-env", "pkg", "pkg_add", "emerge"]);
48
+ const SYSTEM_INSTALL_WORDS = new Set(["install", "add", "upgrade", "update", "reinstall", "tap", "-S", "-Sy", "-Syu", "-U", "-i"]);
49
+ /** Toolchain managers: what installs a toolchain outside the project. */
50
+ const TOOLCHAIN_MANAGERS = new Set(["nvm", "sdk", "pyenv", "rbenv", "rvm", "volta", "fnm", "asdf", "mise", "rtx", "jenv", "goenv", "nodenv"]);
51
+ const GLOBAL_FLAGS = new Set(["-g", "--global", "--location=global"]);
52
+ const INSTALLER_FILE = /\.(run|pkg|msi|dmg)$/i;
53
+ /** A word as the shell would see it, quotes taken off. */
54
+ function words(segment) {
55
+ return (segment.match(/"[^"]*"|'[^']*'|[^\s"']+(?:(?:"[^"]*"|'[^']*')[^\s"']*)*/g) ?? []).map((word) => word.replace(/"([^"]*)"|'([^']*)'/g, "$1$2"));
56
+ }
57
+ /** The simple commands a command line is made of, in order: split on `;`, `&&`, `||`, `|`, `&`, newlines and subshells. */
58
+ function segments(command) {
59
+ return command
60
+ .split(/&&|\|\||[;|&\n()`]|\$\(/)
61
+ .map((part) => part.trim())
62
+ .filter((part) => part !== "");
63
+ }
64
+ /** A simple command's program and arguments, past assignments, wrappers and their own options. */
65
+ function programOf(segment) {
66
+ let all = words(segment);
67
+ let sudo = false;
68
+ for (;;) {
69
+ const first = all[0];
70
+ if (first === undefined)
71
+ return undefined;
72
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(first))
73
+ all = all.slice(1);
74
+ else if (first === "sudo" || first === "doas") {
75
+ sudo = true;
76
+ all = all.slice(1);
77
+ while (all[0] !== undefined && all[0].startsWith("-"))
78
+ all = all.slice(1);
79
+ }
80
+ else if (WRAPPERS.has(first)) {
81
+ all = all.slice(1);
82
+ while (all[0] !== undefined && (/^-/.test(all[0]) || /^\d+[smhd]?$/.test(all[0])))
83
+ all = all.slice(1);
84
+ }
85
+ else
86
+ break;
87
+ }
88
+ const [program, ...args] = all;
89
+ if (program === undefined)
90
+ return sudo ? { program: "", args: [], sudo } : undefined;
91
+ return { program: program.split(/[\\/]/).pop(), args, sudo };
92
+ }
93
+ /** Why one simple command installs software outside the project, or nothing when it does not. */
94
+ function installIn(segment) {
95
+ const found = programOf(segment);
96
+ if (found === undefined)
97
+ return undefined;
98
+ const { program, args, sudo } = found;
99
+ const has = (...wanted) => wanted.some((word) => args.includes(word));
100
+ if (sudo)
101
+ return "sudo runs it as the machine's administrator";
102
+ if (SYSTEM_PACKAGE_MANAGERS.has(program)) {
103
+ if (program === "pkg_add" || args.some((arg) => SYSTEM_INSTALL_WORDS.has(arg)))
104
+ return `${program} installs into the system`;
105
+ return undefined;
106
+ }
107
+ if (TOOLCHAIN_MANAGERS.has(program) && (has("install") || (program === "asdf" && has("plugin")) || ((program === "mise" || program === "rtx") && has("use") && args.some((arg) => GLOBAL_FLAGS.has(arg))))) {
108
+ return `${program} installs a toolchain on the machine`;
109
+ }
110
+ if (program === "rustup" && (has("install") || (has("target", "component", "toolchain") && has("add", "install"))))
111
+ return "rustup installs a toolchain on the machine";
112
+ if (program === "corepack" && (has("enable") || (has("install") && args.some((arg) => GLOBAL_FLAGS.has(arg)))))
113
+ return "corepack installs package managers on the machine";
114
+ if ((program === "npm" || program === "pnpm" || program === "bun") && args.some((arg) => GLOBAL_FLAGS.has(arg)))
115
+ return `${program} ${args.join(" ")} installs globally`;
116
+ if (program === "yarn" && args[0] === "global")
117
+ return "yarn global installs globally";
118
+ const pipArgs = program === "pip" || program === "pip3" ? args : /^python(\d+(\.\d+)?)?$/.test(program) && args[0] === "-m" && /^pip3?$/.test(args[1] ?? "") ? args.slice(2) : undefined;
119
+ if (pipArgs !== undefined && pipArgs.includes("install") && (pipArgs.includes("--user") || pipArgs.includes("--break-system-packages")))
120
+ return "pip installs into the user's or the system's Python";
121
+ if (program === "pipx" && args.some((arg) => ["install", "inject", "upgrade", "ensurepath", "reinstall"].includes(arg)))
122
+ return "pipx installs on the machine";
123
+ if (program === "gem" && has("install"))
124
+ return "gem install installs outside the project - use Bundler";
125
+ if (program === "installer" || program === "msiexec" || (program === "hdiutil" && has("attach")))
126
+ return `${program} runs an installer`;
127
+ if (INSTALLER_FILE.test(program) || ((program === "sh" || program === "bash" || program === "open") && args.some((arg) => INSTALLER_FILE.test(arg))))
128
+ return "it runs an installer";
129
+ return undefined;
130
+ }
131
+ /** Downloads run as programs: a script piped to a shell, or run from a download. */
132
+ const PIPED_DOWNLOAD = /\b(curl|wget)\b[^|;&\n]*\|\s*(sudo\s+(-\S+\s+)*)?((ba|z|da|k)?sh|python\d*|perl|ruby|node)\b/;
133
+ const RUN_DOWNLOAD = /\b(ba|z|da|k)?sh\s+(-\w+\s+)*["']?\$\(\s*(curl|wget)\b|<\(\s*(curl|wget)\b/;
134
+ /** Killing by name, or by what holds a port. */
135
+ const KILL_BY_LOOKUP = /\bkill\b[^;&|\n]*(\$\(|`)\s*(lsof|fuser|pgrep|pidof|ss|netstat)\b/;
136
+ const LOOKUP_TO_KILL = /\b(lsof|pgrep|pidof|fuser|ss|netstat)\b[^;&\n]*\|\s*xargs\s+(-\S+\s+)*kill\b/;
137
+ /** One check, the paths of one platform. */
138
+ class Paths {
139
+ path;
140
+ win;
141
+ constructor(platform) {
142
+ this.win = platform === "win32";
143
+ this.path = this.win ? win32 : posix;
144
+ }
145
+ within(target, root) {
146
+ const a = this.win ? this.path.resolve(target).toLowerCase() : this.path.resolve(target);
147
+ const b = this.win ? this.path.resolve(root).toLowerCase() : this.path.resolve(root);
148
+ const sep = this.path.sep;
149
+ return a === b || a.startsWith(b.endsWith(sep) ? b : b + sep);
150
+ }
151
+ inside(target, roots) {
152
+ return roots.some((root) => this.within(target, root));
153
+ }
154
+ }
155
+ /** What a word that names a path names, from `cwd`: `~`, `$HOME`, `$TMPDIR` as the fence has them; nothing when it cannot be told. */
156
+ function resolveWord(word, cwd, fence, paths) {
157
+ let path = word;
158
+ const home = fence.home;
159
+ if (path === "~" || path.startsWith("~/")) {
160
+ if (home === undefined)
161
+ return undefined;
162
+ path = home + path.slice(1);
163
+ }
164
+ else if (/^\$\{?HOME\}?(\/|$)/.test(path)) {
165
+ if (home === undefined)
166
+ return undefined;
167
+ path = path.replace(/^\$\{?HOME\}?/, home);
168
+ }
169
+ else if (/^\$\{?TMPDIR\}?(\/|$)/.test(path)) {
170
+ if (fence.tmp === undefined)
171
+ return undefined;
172
+ path = path.replace(/^\$\{?TMPDIR\}?/, fence.tmp);
173
+ }
174
+ // A variable we cannot know, or a glob: told apart at run time, by the sandbox.
175
+ if (path.includes("$"))
176
+ return undefined;
177
+ return paths.path.resolve(cwd, path);
178
+ }
179
+ /** The words of a command that name paths: absolute, home- or TMPDIR-relative, or climbing with `..` - in quotes too. */
180
+ function pathWords(text, paths, fence) {
181
+ const found = [];
182
+ // A quoted string that is one path, spaces and all: "C:\Program Files\node\node.exe".
183
+ let rest = text.replace(/"([^"\n]*)"|'([^'\n]*)'/g, (whole, a, b) => {
184
+ const inner = a ?? b ?? "";
185
+ if (/^([A-Za-z]:[\\/]|\/(?!\/)|~\/)/.test(inner) && !/\s-/.test(inner)) {
186
+ found.push(inner);
187
+ return " ".repeat(whole.length);
188
+ }
189
+ return ` ${inner} `;
190
+ });
191
+ const boundary = `(?:^|(?<=[\\s'"=(<>:,\\[{]))`;
192
+ const stop = `[^\\s'"\`;|&<>(){},\\]]*`;
193
+ const patterns = [
194
+ new RegExp(`${boundary}(\\/(?!\\/)${stop})`, "g"),
195
+ new RegExp(`${boundary}(~(?:\\/${stop})?)(?=$|[\\s'"\`;|&<>(){},\\]])`, "g"),
196
+ new RegExp(`${boundary}(\\$\\{?(?:HOME|TMPDIR)\\}?(?:\\/${stop})?)`, "g"),
197
+ new RegExp(`${boundary}((?:${stop}[\\/\\\\])?\\.\\.(?:[\\/\\\\]${stop})?)(?=$|[\\s'"\`;|&<>(){},\\]])`, "g"),
198
+ ];
199
+ if (paths.win)
200
+ patterns.push(new RegExp(`(?<![\\w])([A-Za-z]:[\\\\/]${stop})`, "g"));
201
+ for (const pattern of patterns) {
202
+ for (const match of rest.matchAll(pattern))
203
+ found.push(match[1]);
204
+ }
205
+ rest = "";
206
+ const fenceTops = new Set([...fence.read, ...fence.write, ...(fence.writable ?? []), ...(fence.readable ?? [])].map((root) => root.split(/[\\/]/).filter(Boolean)[0]));
207
+ return found.filter((word) => {
208
+ // The root alone - a slash in code (`case '/':`), a route, `ls /` - reads nothing but the names of the system's own
209
+ // folders; written to, it is a write target, and checked as one.
210
+ if (word === "/")
211
+ return false;
212
+ if (!word.startsWith("/") || paths.win)
213
+ return true;
214
+ // A path that names something on disk: under a known top folder, one of the fence's, or one that is there.
215
+ const top = word.split("/").filter(Boolean)[0];
216
+ if (top === undefined)
217
+ return true;
218
+ return KNOWN_TOPS.has(top) || fenceTops.has(top) || existsSync(`/${top}`);
219
+ });
220
+ }
221
+ /** The words a simple command writes to: redirections, tee, the target of cp and mv, a file edited in place. */
222
+ function writeTargets(segment) {
223
+ const targets = [];
224
+ for (const match of segment.matchAll(/(?:^|[^<>&\d])(?:\d|&)?>>?\|?\s*("[^"]*"|'[^']*'|[^\s;|&<>()]+)/g)) {
225
+ targets.push(match[1].replace(/^["']|["']$/g, ""));
226
+ }
227
+ for (const part of segments(segment)) {
228
+ const found = programOf(part);
229
+ if (found === undefined)
230
+ continue;
231
+ const { program, args } = found;
232
+ const operands = args.filter((arg) => !arg.startsWith("-"));
233
+ if (program === "tee")
234
+ targets.push(...operands);
235
+ else if ((program === "cp" || program === "mv" || program === "install" || program === "ln") && operands.length >= 2)
236
+ targets.push(operands[operands.length - 1]);
237
+ else if (["touch", "rm", "rmdir", "mkdir", "truncate", "chmod", "chown"].includes(program))
238
+ targets.push(...operands.slice(program === "chmod" || program === "chown" ? 1 : 0));
239
+ else if ((program === "sed" || program === "perl") && args.some((arg) => /^-p?i/.test(arg)))
240
+ targets.push(...operands.slice(1));
241
+ }
242
+ return targets;
243
+ }
244
+ function refusal(rule, reason) {
245
+ return { ok: false, rule, reason };
246
+ }
247
+ /** The folders a refusal names: what the command may reach. */
248
+ function reach(fence) {
249
+ const list = (dirs) => dirs.map((dir) => `\`${dir}\``).join(", ");
250
+ const own = fence.write.length > 0 ? `It may read ${list(fence.read)} and write ${list(fence.write)}` : `It may read ${list(fence.read)} and write nothing`;
251
+ return `${own}, beside its home (\`~\`), \`$TMPDIR\` and the machine's system folders and toolchains.`;
252
+ }
253
+ /** Check one command line against a role's fence: `{ ok: true }`, or which rule it breaks and what to do instead. */
254
+ export function checkCommand(command, fence) {
255
+ const paths = new Paths(fence.platform ?? (process.platform === "win32" ? "win32" : "posix"));
256
+ const readable = [...fence.read, ...fence.write, ...(fence.writable ?? []), ...(fence.readable ?? []), ...(paths.win ? [] : DEVICES)];
257
+ const writable = [...fence.write, ...(fence.writable ?? []), ...(paths.win ? [] : DEVICES)];
258
+ if (command.includes(RECORD_FILE)) {
259
+ return refusal("record", `${RECORD_FILE} is the harness's record of what the code is built from: no agent reads or writes it, and nothing in it is yours to go by.`);
260
+ }
261
+ // Installs and kills, a simple command at a time; downloads run as programs, on the whole line.
262
+ if (PIPED_DOWNLOAD.test(command) || RUN_DOWNLOAD.test(command))
263
+ return refusal("install", installReason("it runs a script downloaded from the web"));
264
+ if (KILL_BY_LOOKUP.test(command) || LOOKUP_TO_KILL.test(command))
265
+ return refusal("kill", killReason());
266
+ for (const segment of segments(command)) {
267
+ const why = installIn(segment);
268
+ if (why !== undefined)
269
+ return refusal("install", installReason(why));
270
+ const found = programOf(segment);
271
+ if (found !== undefined && (["pkill", "killall", "taskkill"].includes(found.program) || (found.program === "fuser" && found.args.some((arg) => /^-\w*k/.test(arg))))) {
272
+ return refusal("kill", killReason());
273
+ }
274
+ }
275
+ // Paths, a simple command at a time from where the cds before it left it.
276
+ let cwd = fence.cwd;
277
+ for (const segment of command.split(/&&|\|\||[;\n]/)) {
278
+ for (const word of pathWords(segment, paths, fence)) {
279
+ const target = resolveWord(word, cwd, fence, paths);
280
+ if (target !== undefined && !paths.inside(target, readable)) {
281
+ return refusal("path", `\`${word}\` is outside what this command may reach (${target}). ${reach(fence)} Everything you need from outside them is in your prompt.`);
282
+ }
283
+ }
284
+ for (const word of writeTargets(segment)) {
285
+ if (isCredentialFile(word)) {
286
+ return refusal("credential", `${word.split(/[\\/]/).pop()} holds credentials, and they are the person's to provide - never an agent's to write, not even a placeholder. If the software needs one that is missing, say so and name the variable.`);
287
+ }
288
+ const target = resolveWord(word, cwd, fence, paths);
289
+ if (target !== undefined && !paths.inside(target, writable)) {
290
+ return refusal("path", `\`${word}\` is outside what this command may write (${target}). ${reach(fence)}`);
291
+ }
292
+ }
293
+ const cd = /^\s*cd\s+("[^"]*"|'[^']*'|\S+)\s*$/.exec(segment);
294
+ if (cd !== null) {
295
+ const to = resolveWord(cd[1].replace(/^["']|["']$/g, ""), cwd, fence, paths);
296
+ if (to !== undefined)
297
+ cwd = to;
298
+ }
299
+ }
300
+ return { ok: true };
301
+ }
302
+ function installReason(why) {
303
+ return (`This installs software outside the project - ${why}. The machine is the person's: what it lacks - a toolchain, a ` +
304
+ "system library, a tool - is theirs to install. The project's own dependencies are yours: declare them in its " +
305
+ "manifest and install them into the project (npm install, pip into its venv, mvn, gradle, cargo, go mod). If the " +
306
+ "project cannot be built without something the machine lacks, say what is missing - with report_environment_error, if you have it.");
307
+ }
308
+ function killReason() {
309
+ return ("This kills processes your pulse did not start - by name, or by what holds a port - and those may be the person's " +
310
+ "or another agent's. Kill only what you started, by its process id (`$!`) or its job (`%1`). A port something else " +
311
+ "holds is not yours: use another port.");
312
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * What an agent's work is summed up to when its session gets long - one set of instructions per role.
3
+ *
4
+ * An agent that keeps its session through a job is compacted between turns once its conversation - what it has
5
+ * done, not its instructions - passes the project's compact-at (see pi.ts compactIfDue). Its task is its system
6
+ * prompt and is never summed up, so these say only what of the work the summary must keep for it to go on.
7
+ */
8
+ /** The coding agent, and the code fixer of a conformance run: what is done, what is not, and why. */
9
+ export const CODER_COMPACTION = "Keep what has been done - the files written or changed, and what each is for - what is still failing and why, the " +
10
+ "last test run's result, every decision made and why, every review problem and what became of it (fixed, or declined " +
11
+ "and why), and what was tried that did not work. Leave out file contents and full test output: they can be read or " +
12
+ "run again.";
13
+ /** The conformance agent: the tests written, what they cover, and what is still open. */
14
+ export const CONFORMANCE_COMPACTION = "Keep the conformance tests written and what each covers, what is still failing and whose fault it was found to be " +
15
+ "(the tests, the code or the spec), every review problem and what became of it, and what was tried that did not " +
16
+ "work. Leave out file contents and full test output: they can be read or run again.";
17
+ /** A reviewer: what it raised, and where each problem stands. */
18
+ export const REVIEWER_COMPACTION = "Keep every problem you raised, review by review, and where each stands now: open, fixed and checked in the code, or " +
19
+ "declined by the agent that did the work and whether you accepted why. Keep what you have already checked and found " +
20
+ "right, so it is not checked again. Leave out the diffs and file contents themselves - they can be read again.";