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/tui.mjs ADDED
@@ -0,0 +1,444 @@
1
+ /**
2
+ * `tui` — and the TUI's pure half, which is the part of the panel that has no curses in it.
3
+ *
4
+ * WHAT CROSSES HERE AND WHAT DOES NOT.
5
+ *
6
+ * `cmd_tui` (`rituals/_harness_tui.py`) is twenty-six lines and three arms. Off a TTY it
7
+ * prints one line and returns 1; when `import curses` fails it prints one line and returns
8
+ * 1; otherwise it builds the inventory and hands the screen to `curses.wrapper(_tui_loop)`.
9
+ * **Every cell in `tests/harness_golden.py` runs off a TTY**, so the first arm is the whole
10
+ * of `tui`'s behaviour that the acceptance matrix can reach, and it is ported byte for byte.
11
+ *
12
+ * THE PANEL ITSELF IS P7C'S, AND THIS ENTRY FALLS BACK RATHER THAN INVENTING ONE — the same
13
+ * decision `js/menu.mjs` made in P7a, for the same reason. `cmd_tui` already owns a fourth
14
+ * path: an `except Exception` around `curses.wrapper` that prints `[tui] full-screen panel
15
+ * unavailable (<reason>)` and returns 1, written for the Windows shim's `Unsupported`. That
16
+ * is the reference's own contract for "no full-screen panel is available here", so the port
17
+ * speaks it with a reason that is permanent rather than machine-dependent. A Node panel that
18
+ * differed from the reference on every screen, that no cell could reach, and that would be
19
+ * deleted the day the real one crossed, would be a second user interface and not a port.
20
+ *
21
+ * WHAT DOES CROSS is the alignment contract — the functions below that decide what a row
22
+ * looks like before any curses call is made. They have no caller in this file yet, and that
23
+ * is deliberate and dated: **P7c is the due date**, when `_tui_loop` and the eighteen
24
+ * screens of `_harness_tui_views.py` arrive and consume them. They are here now because they
25
+ * are the half that can be gated exhaustively, and `tests/test_pure_function_parity.py`
26
+ * gates every one of them in both glyph modes. Deleting any of them turns that gate red —
27
+ * this port has shipped code before whose absence nothing would have noticed, and these are
28
+ * not that.
29
+ *
30
+ * WHAT IS DELIBERATELY NOT HERE: `_bx` (its non-ASCII arm returns `curses.ACS_*` chtype ints,
31
+ * which belong with the window), `_wrap_lines` (no consumer until the screens arrive, so
32
+ * porting it now would be a reduced port with no caller to check it against), and every
33
+ * `(stdscr, curses, pal)` drawing helper.
34
+ *
35
+ * **DO NOT WRITE A SECOND LINE BREAKER.** This block used to argue that `textwrap.wrap` was
36
+ * "a second `difflib` — a stdlib payload with no Node twin". It has one since P1:
37
+ * `pyTextWrap` in `js/cli.mjs`, written because `--help` was the caller that finally
38
+ * required it, frozen at `tests/__snapshots__/textwrap.json` and swept against the running
39
+ * interpreter at every width from 11 to 198. When P7c brings `_wrap_lines` across it wraps
40
+ * THAT, and the only open question is the ASCII/glyph width the panel measures in. `_parse_laws`, `load_registry`, `entity_status` and
41
+ * `_tui_inventory` crossed in P6c and live in `js/inventory.mjs`.
42
+ */
43
+ import path from 'node:path';
44
+
45
+ import { THEMES } from './checkout.mjs';
46
+ import { readJsonMaybe } from './installs.mjs';
47
+ import { pySplitLines } from './lib/udiff.mjs';
48
+ import { pyPrint } from './lib/fs.mjs';
49
+
50
+ // ---- display tiers -------------------------------------------------------------------
51
+ //
52
+ // The reference reads these into MODULE CONSTANTS at import time (`_TUI_ASCII = bool(
53
+ // os.environ.get(...))`), so a process answers one way for its whole life and the corpus
54
+ // runs one probe process per mode rather than monkeypatching. Read once here too, for the
55
+ // same reason. `bool(os.environ.get(x))` and `!!process.env.x` agree on the case that
56
+ // matters: an EMPTY value is not set.
57
+ const TUI_ASCII = !!process.env.GENESEED_TUI_ASCII;
58
+ const TUI_PLAIN = !!process.env.GENESEED_TUI_PLAIN;
59
+ const TUI_EMOJI = !(TUI_ASCII || TUI_PLAIN);
60
+ const TUI_ANIM = TUI_EMOJI;
61
+
62
+ /** `_glyphs` — the one glyph table for the whole TUI, unicode or ASCII stand-ins. */
63
+ export function glyphs(asciiMode) {
64
+ return {
65
+ sel: asciiMode ? '>' : '▸',
66
+ up: asciiMode ? '^' : '▴',
67
+ down: asciiMode ? 'v' : '▾',
68
+ head: asciiMode ? '*' : '◆',
69
+ skill: asciiMode ? '*' : '✦',
70
+ law: asciiMode ? '#' : '§',
71
+ };
72
+ }
73
+
74
+ export const GLYPH = glyphs(TUI_ASCII);
75
+
76
+ // ---- display width -------------------------------------------------------------------
77
+ //
78
+ // `_dwidth` is the one function in this phase with NO equivalent at any rung of the ladder.
79
+ // It calls `unicodedata.east_asian_width` and `unicodedata.combining`; JavaScript has
80
+ // neither. `RegExp` Unicode property escapes carry `\p{General_Category}` and `\p{Script}`
81
+ // but NOT `East_Asian_Width`, and `\p{Mn}|\p{Me}` is not the predicate `combining(ch) != 0`
82
+ // — measured, it disagrees on 897 codepoints across 229 ranges, because a spacing mark is
83
+ // `Mc` with combining class 0 and a great many `Mn` have class 0 too. So the tables below
84
+ // are the port's own, and no dependency was added to get them.
85
+ //
86
+ // A HAND-CARRIED UNICODE TABLE IS THE KIND OF THING THAT IS RIGHT UNTIL IT IS NOT, so the
87
+ // gate over it is not a case list. `tests/test_pure_function_parity.py` walks EVERY
88
+ // codepoint from U+0000 to U+2FFFF on both sides, run-length encodes the widths, and
89
+ // compares the run lists — 196 608 inputs, and a range this table gets wrong has nowhere to
90
+ // hide. The comparison is against Python's LIVE `unicodedata`, never against a copy of it,
91
+ // so the day the reference's Unicode version moves under these tables the gate says so
92
+ // instead of agreeing with itself.
93
+ //
94
+ // THE VERSION IS DECLARED, not left in a comment, and that changed on the day the gate fired
95
+ // for real. `python3.14` ships unidata 16.0.0, which assigned U+0897 a combining class these
96
+ // tables (built from 15.1.0) do not carry — so the sweep went red naming a codepoint, exactly
97
+ // as designed, on a machine whose only difference was the interpreter. That is not a defect
98
+ // in either implementation and it must not read as one: the sweep is a comparison against a
99
+ // SPECIFIC Unicode version, and a comparison is meaningless against a different one. So the
100
+ // version is a constant the test reads, `.github/workflows/ci.yml` pins the interpreter that
101
+ // supplies it, and `tests/test_pure_function_parity.py` asserts the pin and the constant
102
+ // still agree. Regenerating the tables means bumping all three together.
103
+ export const DWIDTH_UNIDATA = '15.1.0';
104
+
105
+ //: East_Asian_Width W or F, below U+1F000 — above it the reference's own `>= 0x1F000` rule
106
+ //: already answers 2, so the table stops where that rule takes over.
107
+ const WIDE = [
108
+ 0x1100, 0x115F, 0x231A, 0x231B, 0x2329, 0x232A, 0x23E9, 0x23EC, 0x23F0, 0x23F0,
109
+ 0x23F3, 0x23F3, 0x25FD, 0x25FE, 0x2614, 0x2615, 0x2648, 0x2653, 0x267F, 0x267F,
110
+ 0x2693, 0x2693, 0x26A1, 0x26A1, 0x26AA, 0x26AB, 0x26BD, 0x26BE, 0x26C4, 0x26C5,
111
+ 0x26CE, 0x26CE, 0x26D4, 0x26D4, 0x26EA, 0x26EA, 0x26F2, 0x26F3, 0x26F5, 0x26F5,
112
+ 0x26FA, 0x26FA, 0x26FD, 0x26FD, 0x2705, 0x2705, 0x270A, 0x270B, 0x2728, 0x2728,
113
+ 0x274C, 0x274C, 0x274E, 0x274E, 0x2753, 0x2755, 0x2757, 0x2757, 0x2795, 0x2797,
114
+ 0x27B0, 0x27B0, 0x27BF, 0x27BF, 0x2B1B, 0x2B1C, 0x2B50, 0x2B50, 0x2B55, 0x2B55,
115
+ 0x2E80, 0x2E99, 0x2E9B, 0x2EF3, 0x2F00, 0x2FD5, 0x2FF0, 0x303E, 0x3041, 0x3096,
116
+ 0x3099, 0x30FF, 0x3105, 0x312F, 0x3131, 0x318E, 0x3190, 0x31E3, 0x31EF, 0x321E,
117
+ 0x3220, 0x3247, 0x3250, 0x4DBF, 0x4E00, 0xA48C, 0xA490, 0xA4C6, 0xA960, 0xA97C,
118
+ 0xAC00, 0xD7A3, 0xF900, 0xFAFF, 0xFE10, 0xFE19, 0xFE30, 0xFE52, 0xFE54, 0xFE66,
119
+ 0xFE68, 0xFE6B, 0xFF01, 0xFF60, 0xFFE0, 0xFFE6, 0x16FE0, 0x16FE4, 0x16FF0, 0x16FF1,
120
+ 0x17000, 0x187F7, 0x18800, 0x18CD5, 0x18D00, 0x18D08, 0x1AFF0, 0x1AFF3, 0x1AFF5, 0x1AFFB,
121
+ 0x1AFFD, 0x1AFFE, 0x1B000, 0x1B122, 0x1B132, 0x1B132, 0x1B150, 0x1B152, 0x1B155, 0x1B155,
122
+ 0x1B164, 0x1B167, 0x1B170, 0x1B2FB,
123
+ ];
124
+
125
+ //: Canonical combining class != 0 — `unicodedata.combining(ch)` truthy.
126
+ const COMBINING = [
127
+ 0x0300, 0x034E, 0x0350, 0x036F, 0x0483, 0x0487, 0x0591, 0x05BD, 0x05BF, 0x05BF,
128
+ 0x05C1, 0x05C2, 0x05C4, 0x05C5, 0x05C7, 0x05C7, 0x0610, 0x061A, 0x064B, 0x065F,
129
+ 0x0670, 0x0670, 0x06D6, 0x06DC, 0x06DF, 0x06E4, 0x06E7, 0x06E8, 0x06EA, 0x06ED,
130
+ 0x0711, 0x0711, 0x0730, 0x074A, 0x07EB, 0x07F3, 0x07FD, 0x07FD, 0x0816, 0x0819,
131
+ 0x081B, 0x0823, 0x0825, 0x0827, 0x0829, 0x082D, 0x0859, 0x085B, 0x0898, 0x089F,
132
+ 0x08CA, 0x08E1, 0x08E3, 0x08FF, 0x093C, 0x093C, 0x094D, 0x094D, 0x0951, 0x0954,
133
+ 0x09BC, 0x09BC, 0x09CD, 0x09CD, 0x09FE, 0x09FE, 0x0A3C, 0x0A3C, 0x0A4D, 0x0A4D,
134
+ 0x0ABC, 0x0ABC, 0x0ACD, 0x0ACD, 0x0B3C, 0x0B3C, 0x0B4D, 0x0B4D, 0x0BCD, 0x0BCD,
135
+ 0x0C3C, 0x0C3C, 0x0C4D, 0x0C4D, 0x0C55, 0x0C56, 0x0CBC, 0x0CBC, 0x0CCD, 0x0CCD,
136
+ 0x0D3B, 0x0D3C, 0x0D4D, 0x0D4D, 0x0DCA, 0x0DCA, 0x0E38, 0x0E3A, 0x0E48, 0x0E4B,
137
+ 0x0EB8, 0x0EBA, 0x0EC8, 0x0ECB, 0x0F18, 0x0F19, 0x0F35, 0x0F35, 0x0F37, 0x0F37,
138
+ 0x0F39, 0x0F39, 0x0F71, 0x0F72, 0x0F74, 0x0F74, 0x0F7A, 0x0F7D, 0x0F80, 0x0F80,
139
+ 0x0F82, 0x0F84, 0x0F86, 0x0F87, 0x0FC6, 0x0FC6, 0x1037, 0x1037, 0x1039, 0x103A,
140
+ 0x108D, 0x108D, 0x135D, 0x135F, 0x1714, 0x1715, 0x1734, 0x1734, 0x17D2, 0x17D2,
141
+ 0x17DD, 0x17DD, 0x18A9, 0x18A9, 0x1939, 0x193B, 0x1A17, 0x1A18, 0x1A60, 0x1A60,
142
+ 0x1A75, 0x1A7C, 0x1A7F, 0x1A7F, 0x1AB0, 0x1ABD, 0x1ABF, 0x1ACE, 0x1B34, 0x1B34,
143
+ 0x1B44, 0x1B44, 0x1B6B, 0x1B73, 0x1BAA, 0x1BAB, 0x1BE6, 0x1BE6, 0x1BF2, 0x1BF3,
144
+ 0x1C37, 0x1C37, 0x1CD0, 0x1CD2, 0x1CD4, 0x1CE0, 0x1CE2, 0x1CE8, 0x1CED, 0x1CED,
145
+ 0x1CF4, 0x1CF4, 0x1CF8, 0x1CF9, 0x1DC0, 0x1DFF, 0x20D0, 0x20DC, 0x20E1, 0x20E1,
146
+ 0x20E5, 0x20F0, 0x2CEF, 0x2CF1, 0x2D7F, 0x2D7F, 0x2DE0, 0x2DFF, 0x302A, 0x302F,
147
+ 0x3099, 0x309A, 0xA66F, 0xA66F, 0xA674, 0xA67D, 0xA69E, 0xA69F, 0xA6F0, 0xA6F1,
148
+ 0xA806, 0xA806, 0xA82C, 0xA82C, 0xA8C4, 0xA8C4, 0xA8E0, 0xA8F1, 0xA92B, 0xA92D,
149
+ 0xA953, 0xA953, 0xA9B3, 0xA9B3, 0xA9C0, 0xA9C0, 0xAAB0, 0xAAB0, 0xAAB2, 0xAAB4,
150
+ 0xAAB7, 0xAAB8, 0xAABE, 0xAABF, 0xAAC1, 0xAAC1, 0xAAF6, 0xAAF6, 0xABED, 0xABED,
151
+ 0xFB1E, 0xFB1E, 0xFE20, 0xFE2F, 0x101FD, 0x101FD, 0x102E0, 0x102E0, 0x10376, 0x1037A,
152
+ 0x10A0D, 0x10A0D, 0x10A0F, 0x10A0F, 0x10A38, 0x10A3A, 0x10A3F, 0x10A3F, 0x10AE5, 0x10AE6,
153
+ 0x10D24, 0x10D27, 0x10EAB, 0x10EAC, 0x10EFD, 0x10EFF, 0x10F46, 0x10F50, 0x10F82, 0x10F85,
154
+ 0x11046, 0x11046, 0x11070, 0x11070, 0x1107F, 0x1107F, 0x110B9, 0x110BA, 0x11100, 0x11102,
155
+ 0x11133, 0x11134, 0x11173, 0x11173, 0x111C0, 0x111C0, 0x111CA, 0x111CA, 0x11235, 0x11236,
156
+ 0x112E9, 0x112EA, 0x1133B, 0x1133C, 0x1134D, 0x1134D, 0x11366, 0x1136C, 0x11370, 0x11374,
157
+ 0x11442, 0x11442, 0x11446, 0x11446, 0x1145E, 0x1145E, 0x114C2, 0x114C3, 0x115BF, 0x115C0,
158
+ 0x1163F, 0x1163F, 0x116B6, 0x116B7, 0x1172B, 0x1172B, 0x11839, 0x1183A, 0x1193D, 0x1193E,
159
+ 0x11943, 0x11943, 0x119E0, 0x119E0, 0x11A34, 0x11A34, 0x11A47, 0x11A47, 0x11A99, 0x11A99,
160
+ 0x11C3F, 0x11C3F, 0x11D42, 0x11D42, 0x11D44, 0x11D45, 0x11D97, 0x11D97, 0x11F41, 0x11F42,
161
+ 0x16AF0, 0x16AF4, 0x16B30, 0x16B36, 0x16FF0, 0x16FF1, 0x1BC9E, 0x1BC9E, 0x1D165, 0x1D169,
162
+ 0x1D16D, 0x1D172, 0x1D17B, 0x1D182, 0x1D185, 0x1D18B, 0x1D1AA, 0x1D1AD, 0x1D242, 0x1D244,
163
+ 0x1E000, 0x1E006, 0x1E008, 0x1E018, 0x1E01B, 0x1E021, 0x1E023, 0x1E024, 0x1E026, 0x1E02A,
164
+ 0x1E08F, 0x1E08F, 0x1E130, 0x1E136, 0x1E2AE, 0x1E2AE, 0x1E2EC, 0x1E2EF, 0x1E4EC, 0x1E4EF,
165
+ 0x1E8D0, 0x1E8D6, 0x1E944, 0x1E94A,
166
+ ];
167
+
168
+ /** Binary search over a flat [start, end, start, end, …] range table. */
169
+ function inRanges(table, cp) {
170
+ let lo = 0;
171
+ let hi = table.length / 2 - 1;
172
+ while (lo <= hi) {
173
+ const mid = (lo + hi) >> 1;
174
+ if (cp < table[mid * 2]) hi = mid - 1;
175
+ else if (cp > table[mid * 2 + 1]) lo = mid + 1;
176
+ else return true;
177
+ }
178
+ return false;
179
+ }
180
+
181
+ /**
182
+ * `_dwidth` — display width of `s` in terminal columns.
183
+ *
184
+ * `for (const ch of s)` iterates by CODE POINT, which is what `for ch in s` does on the
185
+ * reference side; a `for (let i …)` loop over UTF-16 units would count every emoji twice.
186
+ */
187
+ export function dwidth(s) {
188
+ let w = 0;
189
+ let prev = 0;
190
+ for (const ch of s) {
191
+ if (ch === '️') { // emoji-presentation selector: base becomes wide
192
+ if (prev === 1) { w += 1; prev = 2; }
193
+ continue;
194
+ }
195
+ if (ch === '︎') continue; // text-presentation selector: leave the base as-is
196
+ const cp = ch.codePointAt(0);
197
+ if (inRanges(COMBINING, cp)) { prev = 0; continue; }
198
+ const cw = (cp >= 0x1F000 || inRanges(WIDE, cp)) ? 2 : 1;
199
+ w += cw;
200
+ prev = cw;
201
+ }
202
+ return w;
203
+ }
204
+
205
+ /** `_truncd` — truncate to at most `width` display columns, never splitting a glyph. */
206
+ export function truncd(s, width) {
207
+ if (width <= 0) return '';
208
+ if (dwidth(s) <= width) return s;
209
+ let out = '';
210
+ let w = 0;
211
+ for (const ch of s) {
212
+ const cw = dwidth(ch);
213
+ if (w + cw > width) break;
214
+ out += ch;
215
+ w += cw;
216
+ }
217
+ return out;
218
+ }
219
+
220
+ /** `_fit` — truncate to `width` display columns, then pad to exactly `width`. */
221
+ export function fit(s, width) {
222
+ const t = truncd(s, width);
223
+ return t + ' '.repeat(Math.max(0, width - dwidth(t)));
224
+ }
225
+
226
+ // ---- glyph tables --------------------------------------------------------------------
227
+
228
+ const ICONS = {
229
+ browse: ['📖', '▤', '#'],
230
+ diff: ['🔍', '≈', '~'],
231
+ setup: ['🧩', '⊞', '%'],
232
+ theme: ['🎨', '◈', '*'],
233
+ update: ['🔄', '↻', '@'],
234
+ bootstrap: ['🚀', '⇧', '^'],
235
+ build: ['🔨', '⚒', '+'],
236
+ memory: ['🧠', '❖', '&'],
237
+ status: ['📊', '▦', '='],
238
+ settings: ['🔧', '⚙', '%'],
239
+ quit: ['🚪', '✕', 'x'],
240
+ doctor: ['🩺', '✚', '+'],
241
+ mcp: ['🔌', '⊕', '&'],
242
+ link: ['🔗', '∞', '&'],
243
+ unlink: ['🔓', '∝', '-'],
244
+ uninstall: ['🗑', '⊗', 'x'],
245
+ back: ['🔙', '←', '<'],
246
+ agent: ['🤖', '◆', '@'],
247
+ skill: ['✨', '✦', '*'],
248
+ law: ['📜', '§', '#'],
249
+ library: ['📚', '❒', 'L'],
250
+ notebook: ['📓', '✎', 'N'],
251
+ wiki: ['📘', '▥', 'W'],
252
+ config: ['🧾', '⌗', 'C'],
253
+ badge: ['🧬', '⬡', 'G'],
254
+ web: ['🌐', '◍', 'W'],
255
+ };
256
+
257
+ /** `_icon` — the icon for `name` in the active display tier. */
258
+ export function icon(name) {
259
+ // `_ICONS.get(name, (...))`, and `Object.hasOwn` rather than a truthiness test so a name
260
+ // like `constructor` cannot reach up the prototype chain and answer with a function.
261
+ const [emoji, sym, asc] = Object.hasOwn(ICONS, name) ? ICONS[name] : ['•', '•', '*'];
262
+ return TUI_ASCII ? asc : (TUI_EMOJI ? emoji : sym);
263
+ }
264
+
265
+ const MARKS = {
266
+ ok: ['✅', '✓', '+'],
267
+ fail: ['❌', '✗', 'x'],
268
+ warn: ['⚠️', '!', '!'],
269
+ info: ['ℹ️', '·', '-'],
270
+ pending: ['·', '·', '-'],
271
+ edited: ['📝', '~', '~'],
272
+ added: ['🆕', '+', '+'],
273
+ missing: ['🗑', '-', '-'],
274
+ mcp_on: ['🟢', '●', 'x'],
275
+ mcp_off: ['⚪', '○', '~'],
276
+ mcp_absent: ['⚫', '·', ' '],
277
+ };
278
+
279
+ /** `_mark` — status glyph for ok/fail/warn/info in the active tier. */
280
+ export function mark(kind) {
281
+ const [emoji, sym, asc] = Object.hasOwn(MARKS, kind) ? MARKS[kind] : ['•', '·', '-'];
282
+ return TUI_ASCII ? asc : (TUI_EMOJI ? emoji : sym);
283
+ }
284
+
285
+ const SPIN = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏';
286
+
287
+ /** `_spin` — one spinner frame for tick `i`; a static dot in the non-animated tiers. */
288
+ export function spin(i) {
289
+ if (!TUI_ANIM) return TUI_ASCII ? '-' : '·';
290
+ const frames = TUI_ASCII ? '|/-\\' : SPIN;
291
+ // `frames[i % len]` on the reference indexes CODE POINTS; the braille frames are BMP so
292
+ // UTF-16 indexing agrees, but spelling it through `[...frames]` keeps that an argument
293
+ // about the data rather than a coincidence. Python's `%` on a negative `i` returns a
294
+ // non-negative remainder and JavaScript's does not, so the modulo is corrected.
295
+ const f = [...frames];
296
+ return f[((i % f.length) + f.length) % f.length];
297
+ }
298
+
299
+ const LOGO_FONT = {
300
+ G: [' ### ', '# ', '# ## ', '# # ', ' ### '],
301
+ E: ['#### ', '# ', '### ', '# ', '#### '],
302
+ N: ['# #', '## #', '# # #', '# ##', '# #'],
303
+ S: [' ####', '# ', ' ### ', ' #', '#### '],
304
+ D: ['### ', '# # ', '# #', '# # ', '### '],
305
+ };
306
+
307
+ /** `_logo_lines` — the GENESEED wordmark as 5 text rows. */
308
+ export function logoLines() {
309
+ const ink = TUI_ASCII ? '#' : '█';
310
+ const rows = [];
311
+ for (let r = 0; r < 5; r += 1) {
312
+ rows.push([...'GENESEED'].map((c) => LOGO_FONT[c][r].replaceAll('#', ink)).join(' '));
313
+ }
314
+ return rows;
315
+ }
316
+
317
+ // ---- layout maths --------------------------------------------------------------------
318
+
319
+ /** `_clamp` — clamp a scroll offset so [top, top+viewH) stays inside `total`. */
320
+ export function clamp(top, total, viewH) {
321
+ return Math.max(0, Math.min(top, Math.max(0, total - viewH)));
322
+ }
323
+
324
+ const BAR_EIGHTHS = ' ▏▎▍▌▋▊▉█';
325
+
326
+ /** `_progress_bar` — a determinate bar exactly `width` display columns wide. */
327
+ export function progressBar(frac, width = 24) {
328
+ const f = Math.max(0.0, Math.min(1.0, frac));
329
+ if (TUI_ASCII) {
330
+ const filled = pyRound(f * width);
331
+ return '#'.repeat(filled) + '-'.repeat(width - filled);
332
+ }
333
+ const eighths = pyRound(f * width * 8);
334
+ const full = Math.floor(eighths / 8);
335
+ const rem = eighths % 8;
336
+ if (full >= width) return '█'.repeat(width);
337
+ return '█'.repeat(full) + [...BAR_EIGHTHS][rem] + ' '.repeat(width - full - 1);
338
+ }
339
+
340
+ /**
341
+ * `round()` — Python rounds HALF TO EVEN and `Math.round` rounds half UP, and this is the
342
+ * one place in the file where that shows: a bar at exactly 0.5 of a cell is the frontier
343
+ * case the eighths resolution exists to draw, and `round(0.5)` is 0 there and 1 here.
344
+ */
345
+ function pyRound(x) {
346
+ const f = Math.floor(x);
347
+ const d = x - f;
348
+ if (d > 0.5) return f + 1;
349
+ if (d < 0.5) return f;
350
+ return f % 2 === 0 ? f : f + 1;
351
+ }
352
+
353
+ // ---- theme reads ---------------------------------------------------------------------
354
+
355
+ /** `_theme_preview` — right-panel preview rows for a theme, as (kind, text) pairs. */
356
+ export function themePreview(key) {
357
+ const data = readJsonMaybe(path.join(THEMES, `${key}.json`));
358
+ if (!data || typeof data !== 'object') return [['dim', '(no preview available)']];
359
+ const lines = [['title', key], ['', '']];
360
+ if (data.TAGLINE) lines.push(['dim', data.TAGLINE], ['', '']);
361
+ if (data.LOADED_SIGIL) lines.push(['ok', data.LOADED_SIGIL], ['', '']);
362
+ if (data.VOICE) lines.push(['', `Voice — ${data.VOICE}`], ['', '']);
363
+ if (data.LEX_I) lines.push(['', `e.g. Rule I — ${data.LEX_I}`]);
364
+ if (data.BENEDICTION) lines.push(['', ''], ['dim', data.BENEDICTION]);
365
+ return lines;
366
+ }
367
+
368
+ /** `_theme_flair` — the voice elements the setup chrome speaks in once a theme is chosen. */
369
+ export function themeFlair(theme) {
370
+ const data = readJsonMaybe(path.join(THEMES, `${theme}.json`)) || {};
371
+ const banner = data.BANNER ?? '';
372
+ return {
373
+ accent: data.ACCENT ?? 'cyan',
374
+ tagline: data.TAGLINE ?? '',
375
+ sigil: data.LOADED_SIGIL ?? '',
376
+ // `str.splitlines()` — `js/lib/udiff.mjs`' twin, already gated. It answers `[]` for
377
+ // the empty string where `''.split('\n')` answers `['']`, which would put a blank row
378
+ // in the banner of every theme that omits one; and its boundary set is Python's rather
379
+ // than `\s`, which P6d measured apart.
380
+ banner: pySplitLines(banner),
381
+ benediction: data.BENEDICTION ?? '',
382
+ };
383
+ }
384
+
385
+ // ---- the inventory's two consumers ----------------------------------------------------
386
+
387
+ /**
388
+ * `_tui_entries` — the ordered (kind, label, data) rows for the left list. `head` is a
389
+ * section divider and is not selectable.
390
+ */
391
+ export function tuiEntries(inv) {
392
+ const rows = [['head', `AGENTS (${inv.agents.length})`, null]];
393
+ for (const e of inv.agents) rows.push(['agent', e.name, e]);
394
+ rows.push(['head', `SKILLS (${inv.skills.length})`, null]);
395
+ for (const e of inv.skills) rows.push(['skill', e.name, e]);
396
+ rows.push(['head', `LAWS (${inv.laws.length})`, null]);
397
+ for (const e of inv.laws) rows.push(['law', `Rule ${e.num} — ${e.title}`, e]);
398
+ return rows;
399
+ }
400
+
401
+ /**
402
+ * `_detail_lines` — right-pane content for the selected entry.
403
+ *
404
+ * The reference falls off the end for every other `kind`, so it returns `None` — not `[]`.
405
+ * `null` here, because a caller that treated the two alike would render an empty pane where
406
+ * the reference renders nothing at all.
407
+ *
408
+ * ⚠ A REFERENCE BUG LIVES ON THIS RETURN, FOUND BY P7B'S CORPUS AND DELIBERATELY NOT FIXED
409
+ * (it is in the panel, which is P7c's). `_tui_loop` does, at `_harness_tui.py`:614:
410
+ *
411
+ * wrapped = _wrap_lines(_detail_lines(kind, label, data), riw)
412
+ *
413
+ * and `_wrap_lines` iterates its argument. For a `head` row `_detail_lines` returns `None`,
414
+ * so that is a `TypeError: 'NoneType' object is not iterable`. It is normally unreachable
415
+ * because line 554 moves `sel` onto the first SELECTABLE row — but line 555 is
416
+ * `if not selectable: sel = 0`, and row 0 is always the AGENTS header. So an inventory with
417
+ * no agents, no skills AND no laws crashes the panel on its first frame instead of drawing
418
+ * an empty state. `_TUI_INV_EMPTY` in `tests/test_pure_function_parity.py` is the corpus
419
+ * entry that surfaced it. **P7c must not reproduce the crash when it ports `_tui_loop`** —
420
+ * the fix is one `or []` at the call site, and it belongs in the same change as the port.
421
+ */
422
+ export function detailLines(kind, label, data) {
423
+ if (kind === 'law') return [label, ''].concat(data ? pySplitLines(data.body) : []);
424
+ if ((kind === 'agent' || kind === 'skill') && data) return pySplitLines(data.body);
425
+ return null;
426
+ }
427
+
428
+ // ---- the verb ------------------------------------------------------------------------
429
+
430
+ /**
431
+ * `cmd_tui`. Arm one is the reachable one and is byte for byte; the panel is P7c's and this
432
+ * falls back through `cmd_tui`'s own `except` arm — see this module's header.
433
+ */
434
+ export function cmdTui() {
435
+ if (!process.stdin.isTTY) {
436
+ pyPrint('[tui] not an interactive terminal. Use `harness setup`, `doctor`, or `build`.\n');
437
+ return 1;
438
+ }
439
+ // The pointer to the Python panel is gone for `js/menu.mjs`'s reason: the
440
+ // panel it named is the Python being deleted. What is left is true and stays true.
441
+ pyPrint('[tui] full-screen panel unavailable (this entry carries the TUI\'s layout '
442
+ + 'half, not its screens). Use `harness setup`, `doctor`, or `build`.\n');
443
+ return 1;
444
+ }