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/js/setup.mjs ADDED
@@ -0,0 +1,492 @@
1
+ /**
2
+ * `rituals/_harness_setup.py`'s wizard — the LINE-MODE install flow, and the first verb in
3
+ * this port that reads stdin.
4
+ *
5
+ * WHAT `setup` ACTUALLY IS, measured rather than taken from the brief. `cmd_setup` is 21
6
+ * lines and every one of them is a dispatch:
7
+ *
8
+ * not sys.stdin.isatty() -> two lines on stderr, exit 1
9
+ * curses.wrapper(_setup_flow) -> the full-screen wizard — `_harness_tui_views`, P7's
10
+ * any exception from that -> `[setup] TUI unavailable (…); using prompts.` + _setup_lines
11
+ *
12
+ * So it has the same shape as `cmd_menu` and `cmd_home`, which the P5i brief excluded from
13
+ * P5 for exactly that reason: a thin dispatcher whose loud arm belongs to a later phase. An
14
+ * import-following closure walk scored it at 1,307 LOC; a walk that also follows a callback
15
+ * passed by NAME scores it at 2,687, and the ~1,100 difference is `_setup_flow`. The brief
16
+ * named that blind spot for `cmd_menu` and did not apply it here.
17
+ *
18
+ * `setup` is still P5's, and the difference from `menu` is the phases table's own sentence:
19
+ * P7 is descoped for GA and GA ships with "the text wizard + web console". `_setup_lines` IS
20
+ * that text wizard. So the LINE arm crosses and the curses arm does not, and `cmdSetup` here
21
+ * goes straight from the TTY check to `setupLines`.
22
+ *
23
+ * THAT IS A DIVERGENCE, and it is stated rather than hidden. On a TTY the reference tries
24
+ * curses first and prints `[setup] TUI unavailable (…); using prompts.` when it fails; this
25
+ * has no curses to try, so it prints nothing and starts asking. On Windows — where the
26
+ * reference ALWAYS lands in the line wizard, because `import curses` is an ImportError —
27
+ * the only difference between the two is that one stderr line. It is invisible to every
28
+ * cell for the reason below, and it is P7's to reconcile when `_setup_flow` crosses.
29
+ *
30
+ * WHY NO CELL CAN REACH ANY OF THIS. `cmd_setup` refuses when `sys.stdin.isatty()` is false,
31
+ * and a cell's stdin is a pipe. So the whole wizard is unreachable from the acceptance
32
+ * matrix in the same way `_confirm` was in P5h — except that here it is not one branch, it
33
+ * is the verb. The `setup/*` cells therefore gate the REFUSAL, absolutely and on both sides,
34
+ * and everything past it is gated as a CORPUS in `tests/test_pure_function_parity.py`.
35
+ *
36
+ * The corpus is a new shape and the brief predicted the reason: a corpus over a stdin reader
37
+ * needs a SEEDED FD, not a string. `tests/fixtures/pure_probe.{py,mjs}` are run with stdin
38
+ * redirected from a file of answers and their WHOLE stdout compared — so the prompts, the
39
+ * numbered menus, the default markers, the `About to run:` line and the returned selection
40
+ * are all one byte comparison. That is a stricter gate than a cell would have been, because
41
+ * a cell could not have varied the answers at all.
42
+ */
43
+ import { spawnSync } from 'node:child_process';
44
+ import { existsSync, readSync } from 'node:fs';
45
+ import path from 'node:path';
46
+
47
+ import { main as driverMain, resolveOut } from '../bin/geneseed.mjs';
48
+ import { playLine } from './anim.mjs';
49
+ import { discoverNames } from './checkout.mjs';
50
+ import { exportImprovements } from './diff.mjs';
51
+ import { cmdDoctor } from './doctor.mjs';
52
+ import { setupBuildArgs } from './generate.mjs';
53
+ import { GLOBAL_MANIFEST, opencodeConfigDir } from './hosts.mjs';
54
+ import {
55
+ defaultMode, defaultPosture, defaultTheme, installedDefaults, themeFiles,
56
+ } from './installs.mjs';
57
+ import { pyInt, pyPrint, pyPrintErr, pyWhich } from './lib/fs.mjs';
58
+ import { NO_WINDOW } from './lib/proc.mjs';
59
+
60
+ // --------------------------------------------------------------------------------------
61
+ // the three readers
62
+ // --------------------------------------------------------------------------------------
63
+
64
+ /**
65
+ * `_harness_setup._ask` — `input(f"{prompt}{suffix}: ").strip()`, or the default at EOF.
66
+ *
67
+ * WHY IT READS A BYTE AT A TIME. Node has no synchronous line read: `readFileSync(0)` blocks
68
+ * until EOF rather than until Enter, so a wizard built on it would hang on a real terminal
69
+ * until the user pressed Ctrl-D. Reading fd 0 one byte at a time to the first `\n` is what
70
+ * `input()` does, and it is the only synchronous shape that stops where Enter stops. The
71
+ * body arrived in P5h inside `js/uninstall.mjs`'s `confirm`; `setup` is the second caller,
72
+ * so it moved to where the Python's own owner is — `_harness_setup` — and `confirm` is built
73
+ * on it here exactly as `_confirm` is built on `_ask` there.
74
+ *
75
+ * The EOF branch is the one a piped caller sees, and it agrees with Python by two different
76
+ * routes that happen to produce one answer: `input()` RAISES `EOFError` and the `except`
77
+ * returns the default, while a zero-length read here leaves `line` empty and `|| dflt` does
78
+ * the same. A partial line with no trailing newline is not EOF to either — Python returns
79
+ * the characters and so does this.
80
+ *
81
+ * `.trim()` is `str.strip()` in every case but U+FEFF, which JS counts as whitespace and
82
+ * Python does not; that is a standing item of this port and not this function's to settle.
83
+ */
84
+ export function ask(prompt, dflt = '') {
85
+ const suffix = dflt ? ` [${dflt}]` : '';
86
+ // `input()` writes its prompt to stdout with no newline — nothing for `pyPrint` to
87
+ // translate, and it must NOT gain one.
88
+ process.stdout.write(`${prompt}${suffix}: `);
89
+ // `?? ''` because `readLine` reports EOF-with-nothing-read as null since P6h; `_ask`
90
+ // catches `EOFError` and returns the default, which is what the empty string produces
91
+ // here through `|| dflt`. The two routes to one answer are the docblock's own point.
92
+ const ans = (readLine() ?? '').trim();
93
+ return ans || dflt;
94
+ }
95
+
96
+ /**
97
+ * `input(prompt)` with EOF told apart from an empty line — `null` where Python RAISES.
98
+ *
99
+ * `ask` above cannot express that difference and does not need to: its EOF and its empty
100
+ * answer both mean "take the default". `_web_server.serve`'s npm prompt is the first
101
+ * caller where they differ, and they differ dangerously — `""` is in its accepted set, so
102
+ * a port that read EOF as an empty line would answer YES to "run npm install now?" on
103
+ * every non-interactive run. The reference catches `(EOFError, KeyboardInterrupt)` and
104
+ * answers "n"; this returns null so the caller can.
105
+ *
106
+ * No `.trim()`: `input()` does not strip, and both callers strip for themselves.
107
+ */
108
+ export function promptLine(prompt) {
109
+ process.stdout.write(prompt);
110
+ return readLine();
111
+ }
112
+
113
+ /**
114
+ * Fd 0 to the first `\n`, dropping a `\r` before it — the CRLF a Windows console sends.
115
+ *
116
+ * THE BYTES ARE COLLECTED AND DECODED ONCE, and that is a fix rather than a style. The
117
+ * version this arrived as decoded EACH BYTE on its own — `buf.toString('utf8', 0, n)` inside
118
+ * the loop — which turns every multi-byte character into two or three U+FFFD. It was
119
+ * invisible for a phase because its only caller was `confirm`, whose answer is `y` or `n`;
120
+ * the wizard's first non-ASCII answer is what exposed it, and the corpus caught it on the
121
+ * first run. Scanning for the terminators a byte at a time is still correct: `\n` and `\r`
122
+ * are ASCII, and no continuation byte of a UTF-8 sequence is below 0x80.
123
+ */
124
+ function readLine() {
125
+ const buf = Buffer.alloc(1);
126
+ const bytes = [];
127
+ let eof = false;
128
+ for (;;) {
129
+ let n = 0;
130
+ try { n = readSync(0, buf, 0, 1, null); } catch { eof = true; break; }
131
+ if (n === 0) { eof = true; break; }
132
+ if (buf[0] === 0x0a) break;
133
+ if (buf[0] !== 0x0d) bytes.push(buf[0]);
134
+ }
135
+ // NULL ONLY FOR THE EMPTY EOF, which is the one case `input()` raises on. A partial line
136
+ // with no trailing newline is not EOF to Python — it returns the characters — and the
137
+ // `bytes.length` test is what keeps that arm returning a string here too.
138
+ if (eof && bytes.length === 0) return null;
139
+ return Buffer.from(bytes).toString('utf8');
140
+ }
141
+
142
+ /** `_harness_setup._confirm` — `_ask` with a Y/n suffix; the default on an empty answer. */
143
+ export function confirm(prompt, dflt = true) {
144
+ const ans = ask(`${prompt} (${dflt ? 'Y/n' : 'y/N'})`).toLowerCase();
145
+ return ans === '' ? dflt : ans[0] === 'y';
146
+ }
147
+
148
+ /**
149
+ * `_harness_setup._ask_choice` — a numbered menu; the chosen KEY, or the default.
150
+ *
151
+ * THREE THINGS ABOUT THE FALLBACK ORDER, all of them observable and none of them obvious:
152
+ *
153
+ * * The key match lives in Python's `except ValueError`, so an answer that PARSES as an
154
+ * integer never reaches it. `99` against a five-option menu returns the default; it does
155
+ * not go looking for an option literally named `99`.
156
+ * * An in-range index wins, a parsed out-of-range index falls to the default, and only an
157
+ * UNPARSEABLE answer is matched against the keys. `pyInt` is what separates the second
158
+ * case from the third, and it is not `Number()` — see its own docblock.
159
+ * * The default's index is computed with `next(...)` and no fallback, so a default that is
160
+ * not in `options` raises `StopIteration` on the reference. That is reachable (a
161
+ * deployed install can carry a theme this checkout no longer ships) and it is mirrored
162
+ * as a throw rather than smoothed over: the two implementations must fail in the same
163
+ * place, and a silent `-1` here would be a divergence dressed as robustness.
164
+ */
165
+ export function askChoice(prompt, options, dflt) {
166
+ pyPrint(`\n${prompt}:\n`);
167
+ options.forEach(([key, desc], i) => {
168
+ const label = desc ? `${key} — ${desc}` : key;
169
+ pyPrint(` ${i + 1}) ${label}${key === dflt ? ' (default)' : ''}\n`);
170
+ });
171
+ const defaultIdx = options.findIndex(([k]) => k === dflt);
172
+ if (defaultIdx < 0) throw new Error(`StopIteration: no option named ${dflt}`);
173
+ const raw = ask('Choose', String(defaultIdx + 1));
174
+ const idx = pyInt(raw);
175
+ if (idx !== null) {
176
+ if (idx >= 1 && idx <= options.length) return options[idx - 1][0];
177
+ } else {
178
+ for (const [key] of options) if (raw === key) return key;
179
+ }
180
+ return dflt;
181
+ }
182
+
183
+ // --------------------------------------------------------------------------------------
184
+ // the option tables
185
+ // --------------------------------------------------------------------------------------
186
+
187
+ /** `_harness_setup.THEME_BLURBS`. A theme without one shows its bare name. */
188
+ const THEME_BLURBS = {
189
+ neutral: 'plain professional voice',
190
+ imperial: 'Warhammer 40k',
191
+ military: 'ops / SOP / radio-brevity',
192
+ pirate: 'high-seas crew',
193
+ wizard: 'arcane grimoire',
194
+ cyberpunk: 'netrunner',
195
+ gamer: 'speedrunner / co-op',
196
+ sports: 'play-by-play commentator',
197
+ };
198
+
199
+ /** `_harness_setup.POSTURE_BLURBS`. */
200
+ const POSTURE_BLURBS = {
201
+ peer: 'candid equal — dense, challenges, no flattery (default)',
202
+ mentor: 'explains the why, checks understanding',
203
+ expert: 'maximum density, no basics',
204
+ assistant: 'precise execution, low initiative — you steer',
205
+ artisan: 'peer with toolsmith reflexes — terminal-first',
206
+ };
207
+
208
+ /** `_harness_setup.MODE_BLURBS`. */
209
+ const MODE_BLURBS = {
210
+ direct: 'the agent works every task itself, turn by turn (default)',
211
+ foreman: 'triages tasks, spawns pipelines for substantial work',
212
+ };
213
+
214
+ /**
215
+ * `_harness_setup._theme_options`.
216
+ *
217
+ * The sort key is `(name != "neutral", name)`, which floats `neutral` to the top and leaves
218
+ * the rest alphabetical — a boolean sorts False before True in Python, and `false < true` is
219
+ * the same order once both are numbers here. `themeFiles()` is already name-sorted, so the
220
+ * comparator only has to move one element.
221
+ */
222
+ export function themeOptions() {
223
+ const opts = themeFiles().map((p) => {
224
+ const stem = path.basename(p, '.json');
225
+ return [stem, THEME_BLURBS[stem] ?? ''];
226
+ });
227
+ opts.sort((a, b) => {
228
+ const ka = a[0] !== 'neutral';
229
+ const kb = b[0] !== 'neutral';
230
+ if (ka !== kb) return ka ? 1 : -1;
231
+ return a[0] < b[0] ? -1 : (a[0] > b[0] ? 1 : 0);
232
+ });
233
+ return opts.length ? opts : [['neutral', THEME_BLURBS.neutral]];
234
+ }
235
+
236
+ /** `_harness_setup._posture_options`. */
237
+ export function postureOptions() {
238
+ return discoverNames('postures', 'peer').map((n) => [n, POSTURE_BLURBS[n] ?? '']);
239
+ }
240
+
241
+ /** `_harness_setup._mode_options`. */
242
+ export function modeOptions() {
243
+ return discoverNames('modes', 'direct').map((n) => [n, MODE_BLURBS[n] ?? '']);
244
+ }
245
+
246
+ /**
247
+ * `_harness_setup.EMIT_OPTIONS`.
248
+ *
249
+ * Exported since P6b: `/api/themes` returns the same nine as its `emits` list, and a copy
250
+ * of a table under test silently stops being the table under test.
251
+ */
252
+ export const EMIT_OPTIONS = [
253
+ ['opencode-global', 'OpenCode global config dir — every repo inherits it (recommended).'],
254
+ ['claude-global', 'Claude Code global config dir (~/.claude) — CLAUDE.md, agents, skills, hooks.'],
255
+ ['opencode', 'Per-repo .opencode/ layer committed into one repository.'],
256
+ ['claude', 'Per-repo CLAUDE.md + .claude/ committed into one repository.'],
257
+ ['bob-global', 'IBM Bob global config dir (~/.bob) — rules/geneseed.md, agents, skills, settings.json.'],
258
+ ['bob', 'Per-repo AGENTS.md + .bob/ for IBM Bob, committed into one repository.'],
259
+ ['copilot-global', 'GitHub Copilot personal config dir (~/.copilot) — copilot-instructions.md, agents, skills.'],
260
+ ['copilot', 'Per-repo AGENTS.md + .github/ for GitHub Copilot, committed into one repository.'],
261
+ ['files', 'Plain bundle for any AGENT.md tool.'],
262
+ ];
263
+
264
+ /** `_harness_setup.FOOTPRINT_OPTIONS`. */
265
+ const FOOTPRINT_OPTIONS = [
266
+ ['lean', 'Lean — terse rule lines + a pointer to the full laws file (default, lighter context).'],
267
+ ['full', 'Full — every law\'s complete text inlined in AGENT.md.'],
268
+ ];
269
+
270
+ // --------------------------------------------------------------------------------------
271
+ // the gather
272
+ // --------------------------------------------------------------------------------------
273
+
274
+ /**
275
+ * `_harness_setup._collect_setup_lines` — the five questions, the plan, the confirm.
276
+ *
277
+ * Returns the selection, or null when the confirm is declined. Every default is the
278
+ * DEPLOYED value first (`installedDefaults`) and the CONFIGURED one second, which is why
279
+ * P5i is the phase that had to give `installedDefaults` its posture, mode and footprint keys
280
+ * back: three of the five questions read them.
281
+ */
282
+ export function collectSetupLines() {
283
+ pyPrint('Geneseed setup — answer a few questions; nothing is written until you confirm.\n');
284
+ const inst = installedDefaults();
285
+ const theme = askChoice('Theme', themeOptions(), inst.theme || defaultTheme());
286
+ const posture = askChoice('Posture', postureOptions(), inst.posture || defaultPosture());
287
+ const mode = askChoice('Mode', modeOptions(), inst.mode || defaultMode());
288
+ const emit = askChoice('Install mode', EMIT_OPTIONS, inst.emit || 'opencode-global');
289
+ const footprint = askChoice('Footprint', FOOTPRINT_OPTIONS.map(([k, d]) => [k, d]),
290
+ inst.footprint || 'lean');
291
+ let out = null;
292
+ let root = null;
293
+ // Every PROJECT emit needs the repo root — claude/bob/copilot included: without `--out`
294
+ // their CLAUDE.md/.claude land in the generator's default ./Harness, where the host never
295
+ // looks.
296
+ if (['opencode', 'claude', 'bob', 'copilot'].includes(emit)) {
297
+ root = ask('Repo root to install into', '.');
298
+ out = root;
299
+ } else if (emit === 'files') {
300
+ out = ask('Output dir for the bundle', 'Harness');
301
+ }
302
+ pyPrint(`\nAbout to run: geneseed build ${
303
+ setupBuildArgs(theme, emit, out, root, footprint, posture, mode).join(' ')}\n`);
304
+ if (!confirm('Proceed?', true)) return null;
305
+ return { theme, posture, mode, emit, out, root, footprint };
306
+ }
307
+
308
+ // --------------------------------------------------------------------------------------
309
+ // the summary
310
+ // --------------------------------------------------------------------------------------
311
+
312
+ /**
313
+ * `_harness_setup._java_major_ok` — does a `java -version` banner name a major >= minimum?
314
+ *
315
+ * `21.0.2` gives 21 and the legacy `1.8.0` gives 1, which is never >= 21 — that asymmetry is
316
+ * the whole function. `\p{Nd}` rather than `[0-9]` because Python's `\d` matches any Unicode
317
+ * decimal and JS's does not without the flag, and `pyInt` reads the group for the same
318
+ * reason. No `java` prints such a banner; matching Python where it is free is cheaper than a
319
+ * comment explaining where it does not.
320
+ */
321
+ export function javaMajorOk(versionOutput, minimum = 21) {
322
+ const m = /version "(\p{Nd}+)/u.exec(versionOutput);
323
+ if (!m) return false;
324
+ const n = pyInt(m[1]);
325
+ return n !== null && n >= minimum;
326
+ }
327
+
328
+ /**
329
+ * `_harness_setup._lsp_prereqs` — `(label, present, hint)` for what OpenCode cannot install
330
+ * for itself. Today that is a JDK 21+ for `jdtls`; the JS-runtime servers self-download.
331
+ *
332
+ * THE SECOND SPAWN ON THIS ENTRY POINT, and the reason `test_the_cli_reaches_child_process_\
333
+ * only_where_it_is_declared` became a TABLE. `js/doctor.mjs` was the sole allowed importer
334
+ * for one binding and one argv; this is the second, and the port's rule is that the second
335
+ * instance of anything turns a special case into a table cross-checked against the source.
336
+ * The argv is asserted literally there, so an allow-list entry cannot be reused to start an
337
+ * interpreter.
338
+ */
339
+ export function lspPrereqs() {
340
+ const java = pyWhich('java');
341
+ let ok = false;
342
+ if (java) {
343
+ try {
344
+ // `capture_output=True, text=True` — the banner goes to STDERR on every JVM, which is
345
+ // why the Python reads `.stderr` and not `.stdout`.
346
+ // `NO_WINDOW` on BOTH sides as of this commit. The reference reached for a bare
347
+ // `subprocess.run` here rather than `_harness_core.run`, so the flag was missing from
348
+ // the Python too and the java probe flashed a console from the web's setup action —
349
+ // a defect the two implementations SHARED, which is exactly what a cross-
350
+ // implementation comparison cannot report. Fixed in `_harness_setup.py` beside this.
351
+ const out = spawnSync(java, ['-version'], { encoding: 'utf8', ...NO_WINDOW }).stderr;
352
+ ok = javaMajorOk(out ?? '');
353
+ } catch { ok = false; }
354
+ }
355
+ return [['Java 21+ (jdtls)', ok,
356
+ 'install a JDK 21+ — e.g. `brew install openjdk@21`, '
357
+ + "SDKMAN `sdk install java 21-tem`, or your distro's package"]];
358
+ }
359
+
360
+ /**
361
+ * `_harness_setup._setup_summary_lines` — the post-build report as `[kind, text]` rows,
362
+ * `kind` being ok | warn | info.
363
+ *
364
+ * `ok` is the build's own success, so the first row is the only one that can say "build
365
+ * failed"; every later row assumes a tree on disk. The global-install warning is the row
366
+ * with real behaviour in it: a bundle or project emit made while an OpenCode GLOBAL install
367
+ * exists is loaded by nothing, and saying so is the difference between a wizard and a form.
368
+ */
369
+ export function setupSummaryLines(theme, emit, out, root, ok) {
370
+ const agentMd = emit === 'opencode-global'
371
+ ? path.join(opencodeConfigDir(), 'AGENT.md')
372
+ : path.join(resolveOut(out || 'Harness'), 'AGENT.md');
373
+ const lines = [];
374
+ if (ok && existsSync(agentMd)) {
375
+ lines.push(['ok', `AGENT.md written to ${agentMd}`]);
376
+ } else if (ok) {
377
+ lines.push(['warn', `expected AGENT.md at ${agentMd} but it is not there`]);
378
+ } else {
379
+ lines.push(['warn', 'build failed — see the output above']);
380
+ }
381
+ if (emit === 'opencode-global') {
382
+ const cfgDir = opencodeConfigDir();
383
+ const hint = process.platform === 'win32'
384
+ ? `learn plugin: $env:GENESEED_HARNESS = "${cfgDir}" (persist: setx GENESEED_HARNESS "${cfgDir}")`
385
+ : 'learn plugin: export GENESEED_HARNESS="$HOME/.config/opencode"';
386
+ lines.push(['info', hint]);
387
+ } else if (emit === 'files') {
388
+ lines.push(['info', `point your tool's instructions at ${agentMd}`]);
389
+ }
390
+ try {
391
+ const cfg = opencodeConfigDir();
392
+ if (emit !== 'opencode-global' && existsSync(path.join(cfg, GLOBAL_MANIFEST))) {
393
+ lines.push(['warn', `a global install exists at ${cfg} — OpenCode loads THAT, `
394
+ + `not this build; re-run with 'opencode-global' to change it`]);
395
+ }
396
+ } catch { /* as the Python's bare `except Exception` */ }
397
+ // LSP prereqs, for the OpenCode emits alone — they are the ones that get `"lsp": true`.
398
+ if (ok && emit.startsWith('opencode')) {
399
+ for (const [label, present, hint] of lspPrereqs()) {
400
+ lines.push(present ? ['ok', `${label} present`] : ['warn', `${label} missing — ${hint}`]);
401
+ }
402
+ }
403
+ lines.push(['info', `theme is now '${theme}' — start a NEW OpenCode session for the new voice`]);
404
+ return lines;
405
+ }
406
+
407
+ // --------------------------------------------------------------------------------------
408
+ // the flow
409
+ // --------------------------------------------------------------------------------------
410
+
411
+ /**
412
+ * `_harness_setup._setup_lines` — gather, export drift, build, summarise, offer the doctor.
413
+ *
414
+ * THE BUILD IS AN IMPORT, NOT A SPAWN, for the reason P5e established for `harness build`:
415
+ * the Python needs a second process because `build.py` is a different PROGRAM, and here the
416
+ * driver is a module in this one. The observable contract — the tree, the streams, the exit
417
+ * code — is the same, and `driverMain` already converts the generator's `die` into a throw
418
+ * so a refusal returns a code instead of killing the wizard.
419
+ *
420
+ * ONE THING THE REFERENCE DOES THAT THIS DOES NOT, stated in the module header's terms. The
421
+ * printed plan used to name the Python driver and its args on both sides — the reference's own
422
+ * wording, kept verbatim because the corpus compares it byte-for-byte. P2 rewrote it to
423
+ * `geneseed build <args>` in BOTH implementations at once, for the reason the whole task
424
+ * exists: the deletion phase may not move a recorded byte, so a string frozen in the corpus
425
+ * can only change while both sides are still here to be re-recorded together. `geneseed
426
+ * build` is not a paraphrase — it forwards its extra arguments to this generator on both
427
+ * sides (`_harness_build.cmd_build`, and `driverMain` here), so the line still names a
428
+ * command the reader can type, and names one that survives the deletion.
429
+ *
430
+ * `theme_anim.play_line` USED TO BE THE SECOND, and P7c crossed it — `js/anim.mjs`, gated by
431
+ * `_theme_anim_cases()` in `tests/test_pure_function_parity.py`. The bare `catch` around it
432
+ * is the reference's own, labelled there "cosmetic only — never block a successful install",
433
+ * and it is kept for the same reason: the animation runs AFTER the build has already
434
+ * succeeded, so nothing it can do may change the exit code.
435
+ */
436
+ export function setupLines() {
437
+ const sel = collectSetupLines();
438
+ if (!sel) {
439
+ pyPrint('[setup] cancelled — nothing written.\n');
440
+ return 0;
441
+ }
442
+ const {
443
+ theme, emit, out, root, footprint = 'lean', posture = 'peer', mode = 'direct',
444
+ } = sel;
445
+ if (emit === 'opencode-global') {
446
+ // The build below overwrites the deployed global harness, and the self-improvement loops
447
+ // may have edited it in place. Preserve that drift first.
448
+ try {
449
+ const [ipath] = exportImprovements();
450
+ if (ipath) {
451
+ pyPrint(`- local edits found in the deployed harness — saved to ${ipath}\n`);
452
+ pyPrint(' (hand that file to your agent to back-port them into src/)\n');
453
+ }
454
+ } catch (e) {
455
+ pyPrint(`! could not export local edits (${e && e.message}) — continuing.\n`);
456
+ }
457
+ }
458
+ const argv = setupBuildArgs(theme, emit, out, root, footprint, posture, mode);
459
+ pyPrint(`Running: geneseed build ${argv.join(' ')}\n`);
460
+ const rc = driverMain(argv);
461
+ if (rc !== 0) {
462
+ pyPrintErr('[setup] build failed — no harness written (see the output above).\n');
463
+ return rc;
464
+ }
465
+ try {
466
+ playLine(theme, true); // themed install animation (motion → reveal card)
467
+ } catch { /* cosmetic only — never block a successful install */ }
468
+ for (const [kind, text] of setupSummaryLines(theme, emit, out, root, true)) {
469
+ pyPrint(`${{ ok: '✓', warn: '!', info: '-' }[kind] ?? '-'} ${text}\n`);
470
+ }
471
+ if (confirm('\nRun a health check (doctor) now?', true)) {
472
+ // Scoped to the theme just installed — no full-sweep noise post-install.
473
+ return cmdDoctor({ theme, all: false, bundle: null, noBundle: false });
474
+ }
475
+ return 0;
476
+ }
477
+
478
+ /**
479
+ * `_harness_setup.cmd_setup` — the TTY gate, then the wizard.
480
+ *
481
+ * The refusal is the whole of what a cell can observe (see the module header) and it is the
482
+ * only part of this file the acceptance matrix compares. `process.stdin.isTTY` is `undefined`
483
+ * off a terminal rather than `false`, which is why it is negated rather than compared.
484
+ */
485
+ export function cmdSetup() {
486
+ if (!process.stdin.isTTY) {
487
+ pyPrintErr('[setup] needs an interactive terminal. Non-interactive? e.g.:\n'
488
+ + ' geneseed build --emit opencode-global --theme neutral\n');
489
+ return 1;
490
+ }
491
+ return setupLines();
492
+ }