geneseed 2.0.0

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 (337) hide show
  1. package/CHANGELOG.md +938 -0
  2. package/DESIGN.md +1423 -0
  3. package/LICENSE +21 -0
  4. package/QUICKSTART.md +48 -0
  5. package/README.md +336 -0
  6. package/SETUP.md +1027 -0
  7. package/SHIPPED.md +70 -0
  8. package/adapters/bob/README.md +100 -0
  9. package/adapters/claude-code/README.md +101 -0
  10. package/adapters/claude-code/settings.json +47 -0
  11. package/adapters/claude-code/skills/geneseed/SKILL.md +72 -0
  12. package/adapters/copilot/README.md +76 -0
  13. package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +434 -0
  14. package/adapters/opencode/HOW-OPENCODE-LOADS.md +158 -0
  15. package/adapters/opencode/README.md +582 -0
  16. package/adapters/opencode/agents/orchestrator.md +25 -0
  17. package/adapters/opencode/opencode.json +44 -0
  18. package/adapters/opencode/plugins/geneseed-activity.js +430 -0
  19. package/adapters/opencode/plugins/geneseed-context.js +876 -0
  20. package/adapters/opencode/plugins/geneseed-guard.js +290 -0
  21. package/adapters/opencode/plugins/geneseed-learn.js +396 -0
  22. package/adapters/opencode/plugins/geneseed-notify.js +142 -0
  23. package/adapters/opencode/plugins/geneseed-ponytail.js +148 -0
  24. package/adapters/opencode/plugins/geneseed-workflow.js +183 -0
  25. package/adapters/opencode/workflows/_runtime.js +277 -0
  26. package/adapters/opencode/workflows/council.js +64 -0
  27. package/adapters/opencode/workflows/dispatch.js +89 -0
  28. package/adapters/opencode/workflows/research-plan-implement.js +43 -0
  29. package/adapters/opencode/workflows/review.js +57 -0
  30. package/bin/geneseed-cli.mjs +412 -0
  31. package/bin/geneseed-hook.mjs +129 -0
  32. package/bin/geneseed.mjs +1086 -0
  33. package/docs/declined.md +149 -0
  34. package/docs/limits.md +270 -0
  35. package/docs/opencode-plugin-setup.md +64 -0
  36. package/docs/token-footprint.md +68 -0
  37. package/docs/web/_groups.json +45 -0
  38. package/docs/web/about.md +6 -0
  39. package/docs/web/adapters-claude-code.md +8 -0
  40. package/docs/web/adapters-opencode-loads.md +8 -0
  41. package/docs/web/adapters-opencode-spec.md +8 -0
  42. package/docs/web/adapters-opencode.md +8 -0
  43. package/docs/web/agents.md +8 -0
  44. package/docs/web/authoring.md +11 -0
  45. package/docs/web/autostart.md +9 -0
  46. package/docs/web/claude-hooks.md +36 -0
  47. package/docs/web/cli.md +6 -0
  48. package/docs/web/collaboration.md +27 -0
  49. package/docs/web/configure-wiki.md +9 -0
  50. package/docs/web/design.md +7 -0
  51. package/docs/web/env-knobs.md +9 -0
  52. package/docs/web/exclusions.md +26 -0
  53. package/docs/web/first-session.md +28 -0
  54. package/docs/web/footprint.md +31 -0
  55. package/docs/web/foreman-mode.md +33 -0
  56. package/docs/web/glossary.md +6 -0
  57. package/docs/web/harnesses.md +28 -0
  58. package/docs/web/headless.md +9 -0
  59. package/docs/web/ingest-docs.md +9 -0
  60. package/docs/web/install-paths.md +9 -0
  61. package/docs/web/install-quick.md +35 -0
  62. package/docs/web/lsp-overview.md +37 -0
  63. package/docs/web/mcp-claude-code.md +10 -0
  64. package/docs/web/mcp-filesystem.md +9 -0
  65. package/docs/web/mcp-gitlab.md +9 -0
  66. package/docs/web/mcp-markitdown.md +9 -0
  67. package/docs/web/mcp-overview.md +34 -0
  68. package/docs/web/mcp-trouble.md +9 -0
  69. package/docs/web/mcp-verify.md +9 -0
  70. package/docs/web/memory.md +7 -0
  71. package/docs/web/migrate.md +39 -0
  72. package/docs/web/model.md +20 -0
  73. package/docs/web/notebook.md +7 -0
  74. package/docs/web/pipelines.md +24 -0
  75. package/docs/web/plugin-activity.md +23 -0
  76. package/docs/web/plugin-context.md +26 -0
  77. package/docs/web/plugin-guard.md +24 -0
  78. package/docs/web/plugin-learn.md +27 -0
  79. package/docs/web/plugin-notify.md +24 -0
  80. package/docs/web/plugin-ponytail.md +23 -0
  81. package/docs/web/plugin-setup.md +7 -0
  82. package/docs/web/plugin-workflow.md +24 -0
  83. package/docs/web/plugins.md +17 -0
  84. package/docs/web/project-context.md +9 -0
  85. package/docs/web/rules.md +8 -0
  86. package/docs/web/run-anywhere.md +9 -0
  87. package/docs/web/self-improve.md +8 -0
  88. package/docs/web/setup-choices.md +39 -0
  89. package/docs/web/skills.md +8 -0
  90. package/docs/web/themes-author.md +8 -0
  91. package/docs/web/themes.md +9 -0
  92. package/docs/web/token-footprint.md +7 -0
  93. package/docs/web/trouble.md +38 -0
  94. package/docs/web/upgrade.md +9 -0
  95. package/docs/web/verify.md +30 -0
  96. package/docs/web/wiki.md +7 -0
  97. package/docs/web/worktree.md +10 -0
  98. package/docs/web-ui.md +144 -0
  99. package/docs/wiki.md +43 -0
  100. package/geneseed +39 -0
  101. package/geneseed.cmd +34 -0
  102. package/harness.config.json +6 -0
  103. package/js/anim.mjs +326 -0
  104. package/js/catalog.mjs +112 -0
  105. package/js/checkout.mjs +88 -0
  106. package/js/cli-table.json +1034 -0
  107. package/js/cli.mjs +498 -0
  108. package/js/diff.mjs +384 -0
  109. package/js/doctor.mjs +1364 -0
  110. package/js/emit.mjs +1373 -0
  111. package/js/excludes.mjs +271 -0
  112. package/js/generate.mjs +379 -0
  113. package/js/hooks.mjs +1026 -0
  114. package/js/hosts.mjs +253 -0
  115. package/js/installs.mjs +378 -0
  116. package/js/inventory.mjs +175 -0
  117. package/js/lib/fs.mjs +913 -0
  118. package/js/lib/proc.mjs +28 -0
  119. package/js/lib/udiff.mjs +275 -0
  120. package/js/link.mjs +286 -0
  121. package/js/mcp.mjs +335 -0
  122. package/js/memory.mjs +108 -0
  123. package/js/menu.mjs +118 -0
  124. package/js/migrate.mjs +251 -0
  125. package/js/native.mjs +426 -0
  126. package/js/opencode.mjs +325 -0
  127. package/js/registry.mjs +110 -0
  128. package/js/render.mjs +282 -0
  129. package/js/settings.mjs +924 -0
  130. package/js/setup.mjs +492 -0
  131. package/js/status.mjs +281 -0
  132. package/js/themes.mjs +195 -0
  133. package/js/tui.mjs +444 -0
  134. package/js/uninstall.mjs +1137 -0
  135. package/js/update.mjs +910 -0
  136. package/js/web/actions.mjs +868 -0
  137. package/js/web/activity.mjs +284 -0
  138. package/js/web/api.mjs +873 -0
  139. package/js/web/docs.mjs +531 -0
  140. package/js/web/graph.mjs +0 -0
  141. package/js/web/jobs.mjs +480 -0
  142. package/js/web/server.mjs +1135 -0
  143. package/package.json +54 -0
  144. package/registry.json +474 -0
  145. package/src/AGENT.md.tmpl +390 -0
  146. package/src/agents/_template.md +76 -0
  147. package/src/agents/advocate.md +36 -0
  148. package/src/agents/architect.md +55 -0
  149. package/src/agents/developer.md +66 -0
  150. package/src/agents/docs.md +54 -0
  151. package/src/agents/empiricist.md +39 -0
  152. package/src/agents/explorer.md +54 -0
  153. package/src/agents/framer.md +36 -0
  154. package/src/agents/historian.md +51 -0
  155. package/src/agents/operator.md +37 -0
  156. package/src/agents/pragmatist.md +37 -0
  157. package/src/agents/reviewer.md +52 -0
  158. package/src/agents/security.md +57 -0
  159. package/src/agents/skeptic.md +48 -0
  160. package/src/agents/steward.md +36 -0
  161. package/src/agents/tester.md +54 -0
  162. package/src/agents/user-advocate.md +36 -0
  163. package/src/agents/visionary.md +36 -0
  164. package/src/laws/universal.md +387 -0
  165. package/src/memory/README.md +109 -0
  166. package/src/memory/gitignore +6 -0
  167. package/src/modes/README.md +26 -0
  168. package/src/modes/direct.md +2 -0
  169. package/src/modes/foreman.md +19 -0
  170. package/src/notebook/README.md +39 -0
  171. package/src/notebook/gitignore +7 -0
  172. package/src/postures/README.md +31 -0
  173. package/src/postures/artisan.md +14 -0
  174. package/src/postures/assistant.md +12 -0
  175. package/src/postures/expert.md +11 -0
  176. package/src/postures/mentor.md +11 -0
  177. package/src/postures/peer.md +12 -0
  178. package/src/skills/_template.md +47 -0
  179. package/src/skills/brainstorm.md +28 -0
  180. package/src/skills/clarify.md +28 -0
  181. package/src/skills/codebase-design.md +44 -0
  182. package/src/skills/commit.md +48 -0
  183. package/src/skills/council.md +34 -0
  184. package/src/skills/crash-course.md +28 -0
  185. package/src/skills/daydream/.claude-plugin/plugin.json +9 -0
  186. package/src/skills/daydream/SKILL.md +74 -0
  187. package/src/skills/daydream/VENDOR.md +25 -0
  188. package/src/skills/daydream/critic-prompt.md +38 -0
  189. package/src/skills/daydream/instructions.md +422 -0
  190. package/src/skills/daydream/synthesizer-prompt.md +41 -0
  191. package/src/skills/debug.md +37 -0
  192. package/src/skills/decode.md +28 -0
  193. package/src/skills/develop.md +29 -0
  194. package/src/skills/document-project.md +79 -0
  195. package/src/skills/domain-modeling.md +38 -0
  196. package/src/skills/drill.md +28 -0
  197. package/src/skills/feynman.md +26 -0
  198. package/src/skills/forge-mcp.md +48 -0
  199. package/src/skills/fresh-eyes.md +29 -0
  200. package/src/skills/frontend-design.md +58 -0
  201. package/src/skills/gap-detector.md +27 -0
  202. package/src/skills/geneseed-code-review.md +33 -0
  203. package/src/skills/geneseed.md +29 -0
  204. package/src/skills/git-archaeology.md +47 -0
  205. package/src/skills/git-rescue.md +68 -0
  206. package/src/skills/handoff.md +29 -0
  207. package/src/skills/herdr.md +164 -0
  208. package/src/skills/ingest.md +49 -0
  209. package/src/skills/learning-path.md +26 -0
  210. package/src/skills/migrate.md +45 -0
  211. package/src/skills/opencode-theme.md +52 -0
  212. package/src/skills/parallel-agents.md +28 -0
  213. package/src/skills/pipeline.md +52 -0
  214. package/src/skills/plan.md +45 -0
  215. package/src/skills/ponytail.md +29 -0
  216. package/src/skills/profile.md +58 -0
  217. package/src/skills/prose.md +42 -0
  218. package/src/skills/prototype.md +44 -0
  219. package/src/skills/react-view-transitions/AGENTS.md +955 -0
  220. package/src/skills/react-view-transitions/README.md +42 -0
  221. package/src/skills/react-view-transitions/SKILL.md +320 -0
  222. package/src/skills/react-view-transitions/VENDOR.md +16 -0
  223. package/src/skills/react-view-transitions/metadata.json +12 -0
  224. package/src/skills/react-view-transitions/references/css-recipes.md +242 -0
  225. package/src/skills/react-view-transitions/references/implementation.md +182 -0
  226. package/src/skills/react-view-transitions/references/nextjs.md +176 -0
  227. package/src/skills/react-view-transitions/references/patterns.md +262 -0
  228. package/src/skills/refactor.md +32 -0
  229. package/src/skills/release.md +43 -0
  230. package/src/skills/repo-map.md +36 -0
  231. package/src/skills/research.md +42 -0
  232. package/src/skills/review-response.md +36 -0
  233. package/src/skills/roast-me.md +28 -0
  234. package/src/skills/rule.md +100 -0
  235. package/src/skills/ship.md +49 -0
  236. package/src/skills/tdd.md +31 -0
  237. package/src/skills/tickets.md +44 -0
  238. package/src/skills/token-report/SKILL.md +76 -0
  239. package/src/skills/token-report/VENDOR.md +23 -0
  240. package/src/skills/token-report/scripts/token_report.mjs +1301 -0
  241. package/src/skills/wayfinder.md +49 -0
  242. package/src/skills/wiki.md +68 -0
  243. package/src/skills/workflow.md +38 -0
  244. package/themes/README.md +69 -0
  245. package/themes/_TEMPLATE.json +147 -0
  246. package/themes/biker.json +147 -0
  247. package/themes/commentator.json +147 -0
  248. package/themes/cyberpunk.json +147 -0
  249. package/themes/gamer.json +147 -0
  250. package/themes/imperial.json +147 -0
  251. package/themes/joker.json +147 -0
  252. package/themes/marvin.json +147 -0
  253. package/themes/mean.json +147 -0
  254. package/themes/military.json +147 -0
  255. package/themes/neutral.json +147 -0
  256. package/themes/opencode/README.md +58 -0
  257. package/themes/opencode/catppuccin.json +27 -0
  258. package/themes/opencode/dracula.json +27 -0
  259. package/themes/opencode/everforest.json +27 -0
  260. package/themes/opencode/gruvbox.json +27 -0
  261. package/themes/opencode/kanagawa.json +27 -0
  262. package/themes/opencode/neon.json +27 -0
  263. package/themes/opencode/nord.json +27 -0
  264. package/themes/opencode/rosepine.json +27 -0
  265. package/themes/opencode/tokyonight.json +27 -0
  266. package/themes/pirate.json +147 -0
  267. package/themes/sports.json +147 -0
  268. package/themes/verstappen.json +147 -0
  269. package/themes/wizard.json +147 -0
  270. package/web/dist/assets/About-DX1-VTBP.js +1 -0
  271. package/web/dist/assets/Activity-Dp9NaXOn.js +1 -0
  272. package/web/dist/assets/ActivityDetail-HpOBtrXS.js +1 -0
  273. package/web/dist/assets/Diff-Ct4G3Se8.js +5 -0
  274. package/web/dist/assets/Doctor-DGjkZFuA.js +1 -0
  275. package/web/dist/assets/Graph-b8o-S6kv.js +1 -0
  276. package/web/dist/assets/Harnesses-gr3-aHBF.js +1 -0
  277. package/web/dist/assets/Laws-DTwF5tTC.js +1 -0
  278. package/web/dist/assets/Library-BEZscyaQ.js +1 -0
  279. package/web/dist/assets/Markdown-k_LSLBGq.js +1 -0
  280. package/web/dist/assets/Profile-DydukTxP.js +1 -0
  281. package/web/dist/assets/Rules-CIRG4Ldt.js +1 -0
  282. package/web/dist/assets/Skills-Buoi1IBr.js +1 -0
  283. package/web/dist/assets/StatusBadge-CE_AJttl.js +1 -0
  284. package/web/dist/assets/Themes-CI4o3gJC.js +2 -0
  285. package/web/dist/assets/activity-BCW7e6Cy.js +1 -0
  286. package/web/dist/assets/audiowide-latin-400-normal-6GFCX7ni.woff2 +0 -0
  287. package/web/dist/assets/geist-mono-latin-400-normal-DKaoCDn5.woff2 +0 -0
  288. package/web/dist/assets/geist-mono-latin-500-normal-C3sF8Y1B.woff2 +0 -0
  289. package/web/dist/assets/geist-mono-latin-600-normal-BHzjB6_C.woff2 +0 -0
  290. package/web/dist/assets/hanken-grotesk-latin-400-italic-Bserb8X5.woff2 +0 -0
  291. package/web/dist/assets/hanken-grotesk-latin-400-normal-BG6hkEXj.woff2 +0 -0
  292. package/web/dist/assets/hanken-grotesk-latin-500-italic-BGfwwhek.woff2 +0 -0
  293. package/web/dist/assets/hanken-grotesk-latin-500-normal-DrDcrrxK.woff2 +0 -0
  294. package/web/dist/assets/hanken-grotesk-latin-600-normal-CIXX6EOa.woff2 +0 -0
  295. package/web/dist/assets/hanken-grotesk-latin-700-normal-CeQ8H3UY.woff2 +0 -0
  296. package/web/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  297. package/web/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  298. package/web/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  299. package/web/dist/assets/ibm-plex-serif-latin-400-normal-BIGslYFI.woff2 +0 -0
  300. package/web/dist/assets/ibm-plex-serif-latin-500-normal-REKAfBVG.woff2 +0 -0
  301. package/web/dist/assets/ibm-plex-serif-latin-600-normal-DAxitmSu.woff2 +0 -0
  302. package/web/dist/assets/index-B1PIx7RM.js +1 -0
  303. package/web/dist/assets/index-BnTQUSUz.js +12 -0
  304. package/web/dist/assets/index-Ck2iCdaC.js +2 -0
  305. package/web/dist/assets/index-DUCsDv7z.css +1 -0
  306. package/web/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  307. package/web/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  308. package/web/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
  309. package/web/dist/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
  310. package/web/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  311. package/web/dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  312. package/web/dist/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
  313. package/web/dist/assets/marked.esm-JKvyS0Pp.js +46 -0
  314. package/web/dist/assets/oswald-latin-400-normal-BAFo8sP8.woff2 +0 -0
  315. package/web/dist/assets/oswald-latin-500-normal-B-eyT0Dw.woff2 +0 -0
  316. package/web/dist/assets/oswald-latin-600-normal-plFmSr5g.woff2 +0 -0
  317. package/web/dist/assets/oswald-latin-700-normal-vy6hmKSr.woff2 +0 -0
  318. package/web/dist/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
  319. package/web/dist/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
  320. package/web/dist/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
  321. package/web/dist/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
  322. package/web/dist/assets/react-DOpiTn5v.js +32 -0
  323. package/web/dist/assets/roman-DkYN0LX9.js +1 -0
  324. package/web/dist/assets/space-grotesk-latin-400-normal-CJ-V5oYT.woff2 +0 -0
  325. package/web/dist/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
  326. package/web/dist/assets/space-grotesk-latin-600-normal-DjKNqYRj.woff2 +0 -0
  327. package/web/dist/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
  328. package/web/dist/assets/space-mono-latin-400-italic-YylcN9Ay.woff2 +0 -0
  329. package/web/dist/assets/space-mono-latin-400-normal-Rg4St2Dn.woff2 +0 -0
  330. package/web/dist/assets/space-mono-latin-700-italic-vNvENeTh.woff2 +0 -0
  331. package/web/dist/assets/space-mono-latin-700-normal-mWgeinG7.woff2 +0 -0
  332. package/web/dist/icon-maskable.svg +10 -0
  333. package/web/dist/icon.svg +8 -0
  334. package/web/dist/index.html +21 -0
  335. package/web/dist/manifest.webmanifest +14 -0
  336. package/web/dist/sw.js +40 -0
  337. package/web/src/pages/Laws.jsx +230 -0
package/geneseed ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Geneseed — front door. A thin shim: it finds the checkout and hands every argument
4
+ # to the Node CLI, which owns the verbs, the parsing and the refusals.
5
+ #
6
+ # ./geneseed open the web console, else the TUI menu (`home`)
7
+ # ./geneseed <command> [args] anything bin/geneseed-cli.mjs carries
8
+ # ./geneseed --help the verb list, printed by the CLI itself
9
+ #
10
+ # NO VERB TABLE HERE, deliberately. This launcher used to enumerate the commands it
11
+ # would forward, and that list had gone stale: `migrate`, `exclude` and `rule-gate`
12
+ # all existed, all fell to its `*)` arm and all exited 2 — while `geneseed.cmd`
13
+ # passed everything through, so the two front doors disagreed about what the harness
14
+ # could do. One CLI, one verb table, one refusal. `./geneseed` in a checkout now
15
+ # behaves exactly like the `geneseed` an `npm i -g` puts on PATH, because that is
16
+ # this same entry point (package.json `bin`). The four HOOK verbs (`context`,
17
+ # `learn`, `git-gate`, `rule-gate`) live on `bin/geneseed-hook.mjs` and are reached
18
+ # by hooks with a baked absolute path; the CLI's refusal names that file.
19
+ #
20
+ # $GENESEED_NODE overrides the interpreter — the analogue of the $PYTHON knob this
21
+ # file carried while the harness was Python. Unset, a bare `node` is used.
22
+ set -eo pipefail
23
+ # Resolve through symlinks so HERE is the real repo dir. `geneseed link` writes a
24
+ # shim naming its entry by absolute path rather than a symlink, but an install made
25
+ # before it did left ~/.local/bin/geneseed pointing at THIS file, and this loop is
26
+ # the only reason those still work.
27
+ SOURCE="${BASH_SOURCE[0]}"
28
+ while [ -h "$SOURCE" ]; do
29
+ dir="$(cd -P "$(dirname "$SOURCE")" && pwd)"
30
+ SOURCE="$(readlink "$SOURCE")"
31
+ case "$SOURCE" in /*) ;; *) SOURCE="$dir/$SOURCE" ;; esac
32
+ done
33
+ HERE="$(cd -P "$(dirname "$SOURCE")" && pwd)"
34
+
35
+ # Bare `./geneseed` is the `home` verb — the web console when the environment can
36
+ # show it (interactive + GUI browser), else the interactive TUI menu; GENESEED_NO_WEB=1
37
+ # disables the web-first default. The CLI entry requires a verb, so the default is here.
38
+ if [ "$#" -eq 0 ]; then set -- home; fi
39
+ exec "${GENESEED_NODE:-node}" "$HERE/bin/geneseed-cli.mjs" "$@"
package/geneseed.cmd ADDED
@@ -0,0 +1,34 @@
1
+ @echo off
2
+ rem Geneseed — native Windows front door (cmd.exe). A thin shim: it hands every
3
+ rem argument to the Node CLI, which owns the verbs, the parsing and the refusals.
4
+ rem
5
+ rem geneseed open the web console, else the TUI menu (`home`)
6
+ rem geneseed <command> [args] anything bin\geneseed-cli.mjs carries
7
+ rem geneseed --help the verb list, printed by the CLI itself
8
+ rem
9
+ rem There is no verb table here — see the bash `geneseed` for why the old one was a
10
+ rem liability. There is no PowerShell twin either: pwsh and Windows PowerShell both
11
+ rem run a .cmd directly, so `.\geneseed.cmd setup` is the PowerShell spelling too.
12
+ rem
13
+ rem %GENESEED_NODE% overrides the interpreter — the analogue of the %PYTHON% knob
14
+ rem this file carried while the harness was Python. Unset, a bare `node` is used.
15
+ rem
16
+ rem TWO MECHANICS, both measured rather than assumed:
17
+ rem * `exit /b %ERRORLEVEL%`, not a bare `exit /b`. A bare one propagates a child's
18
+ rem exit code fine, but returns 0 when the command could not be LAUNCHED at all —
19
+ rem which is exactly the %GENESEED_NODE%-is-wrong case, and "the front door
20
+ rem reported success having run nothing" is the worst answer available.
21
+ rem * a `goto`, not a parenthesized `if` block, so each `exit /b` sits on its own
22
+ rem top-level line. %ERRORLEVEL% inside a block expands at parse time and would
23
+ rem return a stale code — which is what forced the bare form in the first place.
24
+ setlocal
25
+ set "HERE=%~dp0"
26
+ set "NODE=node"
27
+ if defined GENESEED_NODE set "NODE=%GENESEED_NODE%"
28
+ if "%~1"=="" goto :home
29
+ "%NODE%" "%HERE%bin\geneseed-cli.mjs" %*
30
+ exit /b %ERRORLEVEL%
31
+
32
+ :home
33
+ "%NODE%" "%HERE%bin\geneseed-cli.mjs" home
34
+ exit /b %ERRORLEVEL%
@@ -0,0 +1,6 @@
1
+ {
2
+ "_comment": "version is a human-readable release label only; the canonical identity of an installed harness is the source fingerprint stamped in .geneseed-version (see `harness version`), not this string.",
3
+ "theme": "neutral",
4
+ "product": "Geneseed",
5
+ "version": "2.0.0"
6
+ }
package/js/anim.mjs ADDED
@@ -0,0 +1,326 @@
1
+ /**
2
+ * `rituals/theme_anim.py` — the per-theme install animation the LINE wizard plays.
3
+ *
4
+ * P7c, and the one leftover of that phase that is NOT the declared curses panel. P5i ported
5
+ * `_setup_lines` and left this out with a note — "belongs to P7 with the rest of the terminal
6
+ * layer" — and the rest of the terminal layer turned out to be the panel, which stays
7
+ * declared (`tests/test_tui_boundary.py` says why, and asserts it). This module is the part
8
+ * that has nothing to do with curses: it draws on the ORDINARY terminal, with cursor-up
9
+ * escapes and a sleep, and its only caller is a function that already crossed.
10
+ *
11
+ * SO IT IS NOT IN `js/tui.mjs`, DELIBERATELY. That module carries a structural assertion that
12
+ * it contains no escape sequence at all — the thing that makes "the panel is declared" a
13
+ * measurement instead of a promise. This file necessarily contains `\x1b[{n}A`, so putting it
14
+ * there would have turned that assertion red and invited someone to weaken it. A line-mode
15
+ * animation and a full-screen panel are different subsystems in the reference too: they live
16
+ * in different files there and they do here.
17
+ *
18
+ * WHAT THE PYTHON DOES THAT THIS DOES NOT, and it is one line. `_anim_ok` calls
19
+ * `os.system("")` on Windows, whose only purpose is the side effect of making the console
20
+ * enable VT/ANSI processing; it is not a probe and its return value is discarded. Node's
21
+ * console writer already enables virtual-terminal processing on Windows when stdout is a TTY
22
+ * (libuv does it when the stream is opened), so the effect is present and the call is not
23
+ * needed — which is fortunate, because it is a `child_process` spawn and this port is barred
24
+ * from those. The consequence is that the reference's `except Exception: return False` arm
25
+ * around it has no counterpart here. That arm needs `cmd.exe` to be unlaunchable on a machine
26
+ * that has already reached an interactive wizard, and `anim_ok` is compared over ten
27
+ * environments in `tests/test_pure_function_parity.py` — none of which can produce it on
28
+ * either side.
29
+ *
30
+ * WHAT GATES IT. `tests/test_pure_function_parity.py`'s `_theme_anim_cases()`: `art_for` over
31
+ * every key plus the prototype-chain names, `_place`/`_tile`/`_height` over their arithmetic,
32
+ * `_anim_ok` over its whole decision table, and `play_line` itself over 24 environments —
33
+ * captured on the reference side through a real `TextIOWrapper`, so the CRLF translation is
34
+ * inside the comparison rather than outside it.
35
+ */
36
+ import { pyInt, pyLen, pyLjust, pyPrint } from './lib/fs.mjs';
37
+
38
+ export const ART = {
39
+ imperial: {
40
+ title: '+ ADEPTUS ASTARTES +',
41
+ sprite: [
42
+ [' .---. ', ' |[o o]| ', '<#|=====|#====-', ' | ### | ', ' / \\ '],
43
+ [' .---. ', ' |[o o]| ', '<#|=====|#====-', ' | ### | ', ' \\ / '],
44
+ ],
45
+ ground: '_',
46
+ card: [
47
+ ' |>>| ',
48
+ " .--'=='--. ",
49
+ ' |[ O O ]| ',
50
+ ' <#|==========|#===---',
51
+ ' | ::##:: | ',
52
+ ' / \\ ',
53
+ ' THE EMPEROR PROTECTS',
54
+ ],
55
+ },
56
+ pirate: {
57
+ title: '~ HOIST THE COLOURS ~',
58
+ sprite: [
59
+ [' | ', ' /|\\ ', ' /_|_\\ ', ' \\=====/ '],
60
+ [' | ', ' /|\\ ', ' /_|_\\ ', ' \\=====/ '],
61
+ ],
62
+ ground: '~^~ ',
63
+ card: [
64
+ ' | | ',
65
+ ' /|\\ /| ',
66
+ ' /_|_\\/_| ',
67
+ ' __|~~~~~~~~~~|__ ',
68
+ ' \\===== JOLLY ====/ ',
69
+ ' ~^~~^~~^~~^~~^~~^~~^~~ ',
70
+ ' DEAD MEN TELL NO TALES',
71
+ ],
72
+ },
73
+ cyberpunk: {
74
+ title: '>_ JACK IN',
75
+ sprite: [
76
+ ['1 0 01 1 0', ' 0 11 0 1 0', '0 1 10 0 1', ' 11 0 1 10'],
77
+ ['0 1 10 0 1', ' 1 00 1 0 1', '1 0 01 1 0', ' 00 1 0 01'],
78
+ ],
79
+ ground: '=-',
80
+ card: [
81
+ ' 010 1 0 11 0 1 010 ',
82
+ '1 01 0 11 0 1 00 1 0',
83
+ ' 11 0 WAKE UP 1 0 1 ',
84
+ '0 1 01 0 11 0 1 00 1',
85
+ ' 1 0 THE NET IS YOURS 0',
86
+ '=-=-=-=-=-=-=-=-=-=-=-=-',
87
+ ],
88
+ },
89
+ gamer: {
90
+ title: '* PRESS START *',
91
+ sprite: [
92
+ [' ___ ', ' /C o o o ', ' \\___ '],
93
+ [' ___ ', ' (c o o o', ' \\___ '],
94
+ ],
95
+ ground: '.',
96
+ card: [
97
+ ' ___ .oOo. ',
98
+ ' /C o o o o o | x x | ',
99
+ ' \\___ |__o__| ',
100
+ ' HI-SCORE 999999 1UP x3 ',
101
+ ' >>> LEVEL CLEARED <<< ',
102
+ ],
103
+ },
104
+ military: {
105
+ title: '[ ADVANCE - MOVE OUT ]',
106
+ sprite: [
107
+ [' ____ ', ' __/ \\__==-', '| o o o | ', "'-O-O-O-O-' "],
108
+ [' ____ ', ' __/ \\__==-', '| o o o | ', "'O-O-O-O-O' "],
109
+ ],
110
+ ground: '^',
111
+ card: [
112
+ ' ____ ',
113
+ ' __/ \\_____===- ',
114
+ ' | o o o | ',
115
+ " '-O-O-O-O-O-O-' ",
116
+ ' ^^^^^^^^^^^^^^^^^^^^ ',
117
+ ' MISSION GO GO GO ',
118
+ ],
119
+ },
120
+ sports: {
121
+ title: '<< GAME ON >>',
122
+ sprite: [
123
+ [' (o) |‾|', ' | |', ' _________|_|'],
124
+ [' (o) |‾|', ' | |', ' _________|_|'],
125
+ ],
126
+ ground: '_',
127
+ card: [
128
+ ' .---. ',
129
+ ' (o) | | ',
130
+ ' / | G | ',
131
+ ' ________________ |_O_| ',
132
+ ' >>> GOOOAAL !!! <<< ',
133
+ ],
134
+ },
135
+ wizard: {
136
+ title: '~* CAST THE RITE *~',
137
+ sprite: [
138
+ [' ,* . ', ' /| * ', ' (o o) . ', ' /|*|\\ * ', ' | | '],
139
+ [' . *, ', ' /| . ', ' (o o) * ', ' /|*|\\ . ', ' | | '],
140
+ ],
141
+ ground: '.',
142
+ card: [
143
+ ' ,*. * . ',
144
+ ' /| * . * ',
145
+ ' (o o) . * . ',
146
+ ' /|**|\\ * * ',
147
+ ' | | . THE ',
148
+ ' ARCANE IS BOUND ',
149
+ ],
150
+ },
151
+ neutral: {
152
+ title: 'Geneseed',
153
+ sprite: [
154
+ ['>>> '],
155
+ [' >>> '],
156
+ [' >>> '],
157
+ ],
158
+ ground: '',
159
+ card: [
160
+ ' [############------] ',
161
+ ' harness ready. ',
162
+ ],
163
+ },
164
+ };
165
+
166
+ export const DEFAULT = 'neutral';
167
+
168
+ /**
169
+ * `theme_anim.art_for` — `ART.get(theme, ART[DEFAULT])`.
170
+ *
171
+ * `Object.hasOwn`, not `ART[theme] ?? ART[DEFAULT]`. `ART['constructor']` is a function on
172
+ * the prototype chain and therefore not nullish, so the `??` spelling would hand a Function
173
+ * back to `play_line` where the reference hands back the neutral theme. The third table in
174
+ * this port to carry the trap — `_icon` and `_mark` were the first two — and the corpus
175
+ * names all four inherited keys.
176
+ */
177
+ export function artFor(theme) {
178
+ return Object.hasOwn(ART, theme) ? ART[theme] : ART[DEFAULT];
179
+ }
180
+
181
+ /** A code-point slice — Python's `s[a:b]`, which does not count UTF-16 units. */
182
+ const cut = (s, a, b) => Array.from(s).slice(a, b).join('');
183
+
184
+ /**
185
+ * `shutil.get_terminal_size((80, 24)).columns`.
186
+ *
187
+ * `pyInt` and not `parseInt`, and the corpus has a case for exactly that: Python's `int()`
188
+ * refuses `'80x'` outright and `get_terminal_size` swallows the ValueError into the fallback,
189
+ * where `parseInt('80x')` is a cheerful 80. A width one column off is invisible in a terminal
190
+ * and loud in a byte comparison. `<= 0` is the reference's own guard, which is why `'0'` and
191
+ * `'-5'` fall through to the fallback rather than producing a zero-width canvas.
192
+ */
193
+ function terminalColumns() {
194
+ const env = pyInt(process.env.COLUMNS);
195
+ if (env !== null && env > 0) return env;
196
+ // `sys.__stdout__` over there, `process.stdout` here. Both are undefined-ish off a
197
+ // terminal, and both then take the (80, 24) fallback.
198
+ const real = process.stdout.columns;
199
+ return typeof real === 'number' && real > 0 ? real : 80;
200
+ }
201
+
202
+ /**
203
+ * `theme_anim._anim_ok` — whether an in-place ANSI animation is safe here.
204
+ *
205
+ * ⚠ TERM=dumb AND TERM UNSET ARE NOT REFUSALS ON WINDOWS. The reference guards that branch
206
+ * with `if os.name != "nt"`, so on Windows a dumb TERM still animates — which is right,
207
+ * because TERM is not what a Windows console is configured by. Reading the branch as an
208
+ * unconditional refusal would disable the animation on the only platform whose users get the
209
+ * line wizard by default.
210
+ */
211
+ export function animOk() {
212
+ if (process.env.GENESEED_NO_ANIM || process.env.GENESEED_TUI_PLAIN) return false;
213
+ if (!process.stdout.isTTY) return false;
214
+ const term = (process.env.TERM || '').toLowerCase();
215
+ if (term === 'dumb' || term === '') {
216
+ if (process.platform !== 'win32') return false;
217
+ }
218
+ // The reference's `os.system("")` goes here; see the module header for why it does not.
219
+ return true;
220
+ }
221
+
222
+ /** `theme_anim._place` — a sprite row drawn at column `x`, clipped to `width`. */
223
+ export function place(row, x, width) {
224
+ const s = x >= 0 ? ' '.repeat(x) + row : cut(row, -x);
225
+ return cut(s, 0, width);
226
+ }
227
+
228
+ /**
229
+ * `theme_anim._tile` — the ground line, scrolled by `phase`.
230
+ *
231
+ * Two Python arithmetic rules, both of which JavaScript spells differently. `width //
232
+ * len(tile)` is FLOOR division, so a negative width gives a negative repeat count — which
233
+ * Python turns into an empty string and `String.repeat` turns into a RangeError, hence the
234
+ * clamp. And `phase % len(tile)` is non-negative in Python for a positive divisor while
235
+ * JavaScript keeps the sign of the dividend, so a negative phase indexes off the front of the
236
+ * string instead of wrapping to the end. `_spin` had the same trap in P7b.
237
+ */
238
+ export function tile(tileStr, width, phase = 0) {
239
+ if (!tileStr) return '';
240
+ const n = pyLen(tileStr);
241
+ const base = tileStr.repeat(Math.max(0, Math.floor(width / n) + 2));
242
+ const off = ((phase % n) + n) % n;
243
+ return cut(base, off, off + width);
244
+ }
245
+
246
+ /** `theme_anim._height` — `max((len(p) for p in poses), default=0)`. */
247
+ export function height(poses) {
248
+ return Math.max(0, ...poses.map((p) => p.length));
249
+ }
250
+
251
+ /**
252
+ * `str.center(width)`, which is not `padStart`+`padEnd` and not symmetric.
253
+ *
254
+ * CPython's `stringlib` computes `left = marg // 2 + (marg & width & 1)`, and that last term
255
+ * is a genuine oddity rather than a rounding convention: when the margin AND the width are
256
+ * both odd the extra column goes on the LEFT, and otherwise on the right. Nothing about
257
+ * "centre this" implies it. The title line of every animation is the only caller.
258
+ */
259
+ function pyCenter(s, width) {
260
+ const marg = width - pyLen(s);
261
+ if (marg <= 0) return s;
262
+ /* eslint-disable-next-line no-bitwise */
263
+ const left = Math.floor(marg / 2) + (marg & width & 1);
264
+ return ' '.repeat(left) + s + ' '.repeat(marg - left);
265
+ }
266
+
267
+ /** `time.sleep(seconds)`, synchronously. `Atomics.wait` is the stdlib's only sync sleep. */
268
+ const sleep = (ms) => {
269
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
270
+ };
271
+
272
+ /**
273
+ * `theme_anim.play_line` — scroll the themed sprite, then settle on the static card.
274
+ *
275
+ * Never raises: the reference wraps the animation in `except Exception` and degrades to the
276
+ * static card, and `_setup_lines` wraps the whole call again in a bare `except: pass` marked
277
+ * "cosmetic only — never block a successful install". Both layers are kept. A failed
278
+ * animation must not be able to fail an install that already succeeded.
279
+ */
280
+ export function playLine(theme, ok = true) {
281
+ const art = artFor(theme);
282
+ const width = Math.min(terminalColumns() - 1, 56);
283
+ pyPrint('\n');
284
+ pyPrint(`${pyCenter(art.title, width)}\n`);
285
+ if (!ok) return;
286
+ if (!animOk()) {
287
+ for (const r of art.card) pyPrint(`${cut(r, 0, width)}\n`);
288
+ pyPrint('\n');
289
+ return;
290
+ }
291
+ const poses = art.sprite;
292
+ const ground = art.ground || '';
293
+ const h = height(poses);
294
+ const canvasH = h + (ground ? 1 : 0);
295
+ const spriteW = Math.max(0, ...poses.flatMap((p) => p.map((r) => pyLen(r))));
296
+ try {
297
+ pyPrint('\n'.repeat(canvasH));
298
+ const steps = Math.min(width + spriteW, 64); // cap to ~1.6s of motion
299
+ for (let i = 0; i <= steps; i += 1) {
300
+ const pose = poses[Math.floor(i / 3) % poses.length];
301
+ const x = width - i; // enter from the right, travel left
302
+ const lines = [];
303
+ for (let r = 0; r < h; r += 1) lines.push(place(r < pose.length ? pose[r] : '', x, width));
304
+ if (ground) lines.push(tile(ground, width, i));
305
+ pyPrint(`\x1b[${canvasH}A`);
306
+ for (const ln of lines) pyPrint(`\r${cut(pyLjust(ln, width), 0, width)}\n`);
307
+ sleep(25);
308
+ }
309
+ // settle on the card
310
+ pyPrint(`\x1b[${canvasH}A`);
311
+ const card = art.card;
312
+ for (let r = 0; r < canvasH; r += 1) {
313
+ const row = r < card.length ? cut(card[r], 0, width) : '';
314
+ pyPrint(`\r${cut(pyLjust(row, width), 0, width)}\n`);
315
+ }
316
+ // any extra card rows beyond the canvas
317
+ for (let r = canvasH; r < card.length; r += 1) pyPrint(`${cut(card[r], 0, width)}\n`);
318
+ pyPrint('\n');
319
+ } catch {
320
+ // Terminal didn't cooperate — leave a clean static card.
321
+ try {
322
+ for (const r of art.card) pyPrint(`${cut(r, 0, width)}\n`);
323
+ pyPrint('\n');
324
+ } catch { /* nothing left to try */ }
325
+ }
326
+ }
package/js/catalog.mjs ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * `geneseed catalog` — the shipped roster, in a terminal.
3
+ *
4
+ * WHY THIS VERB IS NEW RATHER THAN PORTED. The reference had no `catalog` subcommand: the
5
+ * same information existed only behind the web console's catalog endpoint and behind the
6
+ * full-screen browse panel, so the one way to ask "what does this harness actually ship"
7
+ * from a shell was to start a server. The endpoint's answer is the source of the fields
8
+ * rendered here — name, one-line purpose, source file, lifecycle status, and a skill's
9
+ * category — so this is a second FACE on one walk and not a second walk.
10
+ *
11
+ * THAT WALK IS `tuiInventory`, WHICH ALREADY HAD FOUR CALLERS and now has five. Nothing
12
+ * about the classification lives here; a listing that re-derived "which files are agents"
13
+ * would be the second classifier the inventory module's header exists to forbid.
14
+ *
15
+ * IT RETIRES THREE HELPERS THAT HAD NO PRODUCT CALLER. `tuiEntries`, `icon`/`fit`/`truncd`
16
+ * and the display-width table under them were ported for a panel that was then dropped, and
17
+ * were reachable only from their own gates. They are the right tools for this rendering
18
+ * — `dwidth` is what keeps an emoji-bearing purpose line from breaking the column — so the
19
+ * roster is built out of them rather than beside them. `detailLines` is NOT used and stays
20
+ * uncalled: it renders a selected entry's whole body into a detail pane, which a listing
21
+ * has no place for.
22
+ *
23
+ * NOTHING HERE PAINTS. No cursor moves, no clears, no alternate screen: this writes lines
24
+ * and stops, exactly as `status` does. That is what keeps the dropped panel dropped.
25
+ */
26
+ import { helpWidth } from './cli.mjs';
27
+ import { tuiInventory } from './inventory.mjs';
28
+ import { defaultTheme, installedDefaults } from './installs.mjs';
29
+ import { pyPrint } from './lib/fs.mjs';
30
+ import { dwidth, fit, icon, truncd, tuiEntries } from './tui.mjs';
31
+
32
+ /** The listing's section names, and the row kind each one selects. */
33
+ export const CATALOG_KINDS = { agents: 'agent', skills: 'skill', laws: 'law' };
34
+
35
+ /**
36
+ * The badge column: what the catalog endpoint carries beside the name that is not the
37
+ * purpose. An agent has a lifecycle status, a skill has that and a category, a law has a
38
+ * governance class and no status at all — which is the endpoint's shape, not a shortcut.
39
+ */
40
+ function badgeFor(kind, data) {
41
+ if (kind === 'law') return data.klass ?? 'craft';
42
+ if (kind === 'skill') return `${data.klass ?? 'build'}/${data.status ?? 'unknown'}`;
43
+ return data.status ?? 'unknown';
44
+ }
45
+
46
+ /**
47
+ * The rendered roster as lines, with no trailing newline on any of them.
48
+ *
49
+ * A SECTION WITH NO ROWS PRINTS NO HEADING. The heading is emitted lazily, when the first
50
+ * row that survives the filter arrives — so `catalog laws` on an install whose render
51
+ * produced no laws is EMPTY rather than a heading over nothing, and the caller can tell the
52
+ * two apart and say so.
53
+ *
54
+ * The name column is measured over the rows actually being printed, not over the whole
55
+ * inventory: filtering to `agents` must not leave a gutter the width of the longest law
56
+ * title. `dwidth` rather than `String.length`, because a purpose line can carry an emoji
57
+ * and a status glyph is two columns wide in every tier but ASCII.
58
+ */
59
+ export function catalogLines(inv, section, width, showSource = false) {
60
+ const kinds = section ? [CATALOG_KINDS[section]] : Object.values(CATALOG_KINDS);
61
+ const rows = tuiEntries(inv);
62
+ const shown = rows.filter(([kind]) => kinds.includes(kind));
63
+ if (!shown.length) return [];
64
+ const cap = Math.max(12, Math.floor(width / 2));
65
+ const nameW = Math.min(cap, Math.max(...shown.map(([, label]) => dwidth(label))));
66
+ const badgeW = Math.max(...shown.map(([kind, , data]) => dwidth(badgeFor(kind, data))));
67
+
68
+ const out = [];
69
+ let pending = null;
70
+ for (const [kind, label, data] of rows) {
71
+ if (kind === 'head') { pending = label; continue; }
72
+ if (!kinds.includes(kind)) continue;
73
+ if (pending !== null) {
74
+ if (out.length) out.push('');
75
+ out.push(pending);
76
+ pending = null;
77
+ }
78
+ // `source` is the endpoint's field and is an absolute path, so it replaces the purpose
79
+ // column rather than joining it — two long strings on one line is a column layout that
80
+ // is a wrapped mess on every terminal narrower than the sum of them.
81
+ //
82
+ // AND IT IS THE ONE COLUMN THAT IS NEVER TRUNCATED. A purpose line cut at the terminal
83
+ // edge still reads; half a path is not a path, and the reason to ask for one is to hand
84
+ // it to something else.
85
+ const tail = showSource ? (data.source ?? '') : (data.desc ?? '');
86
+ const row = ` ${icon(kind)} ${fit(label, nameW)} ${fit(badgeFor(kind, data), badgeW)} ${tail}`
87
+ .replace(/\s+$/, '');
88
+ out.push(showSource ? row : truncd(row, width));
89
+ }
90
+ return out;
91
+ }
92
+
93
+ /**
94
+ * The verb.
95
+ *
96
+ * An unknown `--theme` is refused by `effectiveTheme` on the way through the render, with
97
+ * the wording every other theme-taking verb already prints — so there is no second theme
98
+ * validator here, and the entry point's `exitCode` arm carries the refusal out.
99
+ */
100
+ export function cmdCatalog(args) {
101
+ const theme = args.theme || installedDefaults().theme || defaultTheme();
102
+ // `helpWidth()` is `COLUMNS`, else the terminal, else 80 — minus the two columns argparse
103
+ // subtracts for its own layout. This listing wants the whole terminal, so it adds them
104
+ // back rather than owning a second copy of that precedence chain.
105
+ const lines = catalogLines(tuiInventory(theme), args.section, helpWidth() + 2, args.source);
106
+ if (!lines.length) {
107
+ pyPrint(`[catalog] nothing to list${args.section ? ` under ${args.section}` : ''}.\n`);
108
+ return 1;
109
+ }
110
+ for (const line of lines) pyPrint(`${line}\n`);
111
+ return 0;
112
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The checkout this code is running FROM — `_build_core`'s ROOT and the paths under it,
3
+ * plus the `cfg` every render needs.
4
+ *
5
+ * The inverse of `js/hosts.mjs`. That module answers "where does the output go"; this one
6
+ * answers "where is the source". Both were `bin/geneseed.mjs`'s, and both leave it for the
7
+ * same arithmetic reason: `bin/geneseed-cli.mjs` needs them too, and the value that decides
8
+ * which tree gets rendered is the last thing that should exist twice.
9
+ *
10
+ * WHY A NON-EMITTING VERB NEEDS A cfg AT ALL. `harness status` counts agents, skills and
11
+ * laws, and it counts them by RENDERING — `_status_data` -> `_tui_inventory(theme)` ->
12
+ * `build.render_all(theme)`. So the CLI has to build the same `cfg` the driver builds, and
13
+ * until this move only the driver could.
14
+ *
15
+ * THE TWO ABSENT KEYS ARE STILL ABSENT, and the reason is bin/geneseed.mjs's, unchanged:
16
+ * `_build_core.js_cfg()` always sends `structure` and `capabilityLinkRe` because the Python
17
+ * originals are module-level names TESTS MUTATE (`_OWNED` membership asked one level out).
18
+ * A Node process has no Python module to mutate, so it sends neither and
19
+ * `js/render.mjs`'s `cfg.structure ?? STRUCTURE` takes its right-hand branch.
20
+ *
21
+ * The move is safe for the reason P5c's was: `tests/golden.py` drives `bin/geneseed.mjs`
22
+ * over 259 cells and compares the tree byte-for-byte, and every one of them builds a cfg
23
+ * from these paths. A depth error or a renamed key fails 259 cells, not zero.
24
+ */
25
+ import { readdirSync } from 'node:fs';
26
+ import path from 'node:path';
27
+ import { fileURLToPath } from 'node:url';
28
+
29
+ /**
30
+ * `_build_core.ROOT` — the checkout, from this file's own location (`js/..`).
31
+ *
32
+ * NOT redirectable, and that is a property rather than a gap. The Python side is
33
+ * `Path(__file__).resolve().parent` with every path under it derived at import; the
34
+ * `_OWNED` redirect the suite uses is an in-process write, and no env var moves it
35
+ * (`$GENESEED_ROOT` is `harness context`'s doc-discovery root — a different name for a
36
+ * different job). Both implementations therefore answer from their own file's location,
37
+ * which is what makes them comparable and what makes them unfenceable. See the
38
+ * status/version section of `tests/harness_golden.py`.
39
+ */
40
+ export const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
41
+ export const SRC = path.join(ROOT, 'src');
42
+ export const CONFIG = path.join(ROOT, 'harness.config.json');
43
+ export const THEMES = path.join(ROOT, 'themes');
44
+ export const PLUGIN_SRC = path.join(ROOT, 'adapters', 'opencode', 'plugins');
45
+ export const WORKFLOW_SRC = path.join(ROOT, 'adapters', 'opencode', 'workflows');
46
+
47
+ /**
48
+ * `_build_render.posture_names()` / `mode_names()` — discovered, never hardcoded, so a new
49
+ * posture file appears in both CLIs' choices with no code change.
50
+ *
51
+ * P5f moved this here from `bin/geneseed.mjs`, which had the only caller until
52
+ * `js/installs.mjs` needed the same two lists: `_posture_of_dir` scans a deployed carrier for
53
+ * `**<Name>**` and has to be scanning for the SAME names the driver's `--posture` accepts.
54
+ * Two discoveries of one directory is precisely the shape that lets a new posture file be
55
+ * buildable and undetectable.
56
+ */
57
+ export function discoverNames(dir, first) {
58
+ let names = [];
59
+ try {
60
+ names = readdirSync(path.join(SRC, dir))
61
+ .filter((f) => f.endsWith('.md') && path.basename(f, '.md').toLowerCase() !== 'readme')
62
+ .map((f) => path.basename(f, '.md'))
63
+ .sort();
64
+ } catch { /* missing dir — fall through to the single default below */ }
65
+ names.sort((a, b) => (a !== first) - (b !== first) || (a < b ? -1 : a > b ? 1 : 0));
66
+ return names.length ? names : [first];
67
+ }
68
+
69
+ /**
70
+ * `_build_core.js_cfg()`, originated rather than received.
71
+ *
72
+ * `posture` and `mode` are `_build_core`'s module defaults (`peer` / `direct`) unless a
73
+ * caller overrides them. `build.py`'s `main()` sets them from its flags; `rituals/harness.py`
74
+ * never touches them, so every harness verb that renders renders at the defaults.
75
+ */
76
+ export function makeCfg({ posture = 'peer', mode = 'direct' } = {}) {
77
+ return {
78
+ root: ROOT,
79
+ src: SRC,
80
+ themes: THEMES,
81
+ config: CONFIG,
82
+ colorThemes: path.join(THEMES, 'opencode'),
83
+ pluginSrc: PLUGIN_SRC,
84
+ workflowSrc: WORKFLOW_SRC,
85
+ posture,
86
+ mode,
87
+ };
88
+ }