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/status.mjs ADDED
@@ -0,0 +1,281 @@
1
+ /**
2
+ * `harness status` and `harness version` — `rituals/_harness_status.py`, and the parts of
3
+ * four other modules those two reach.
4
+ *
5
+ * WHAT THE CLOSURE ACTUALLY IS, MEASURED. An `ast` walk over `rituals/` puts `status` at 527
6
+ * LOC across 7 modules with 17 `build.*` names, which is what carried it through two phases
7
+ * as "the cheap half at 159 LOC". Both numbers are wrong in opposite directions, and the
8
+ * second one is the interesting one: a closure walk counts what the CALLEE computes, and
9
+ * `_status_data` consumes a fraction of it.
10
+ *
11
+ * * `_tui_inventory` returns every agent and skill with its one-line purpose, full
12
+ * rendered body, source path and lifecycle badge, and every law with its title, body
13
+ * and governance class. `_status_data` reads three `len()`s off it. So `SKILL_CLASS`,
14
+ * `LAW_CLASS`, `ENTITY_STATUSES`, `load_registry`, `entity_status` and
15
+ * `build._first_blockquote` — the taxonomy P7 owns, ~111 LOC — are in the walk and are
16
+ * not in this file. `inventoryCounts` is the counting half and says so.
17
+ * * `_installed_defaults` detects theme, emit, posture, mode and footprint. The panel
18
+ * prints theme and emit. `_posture_of_dir`/`_mode_of_dir` are therefore not part of the
19
+ * value it returns, and `status/posture-and-mode-in-the-carrier-change-nothing` is the
20
+ * cell that says the omission is invisible rather than assuming it. **P5f ported those
21
+ * two detectors and did NOT change this** — `rebuild-all` calls them per install root,
22
+ * not through `_installed_defaults`, so the cell above is still a positive control over
23
+ * an unchanged omission rather than a claim that has quietly gone stale.
24
+ * * `_footprint_of_dir` is reached from here, and it is the exception that names the rule:
25
+ * its return value is not consumed by the panel either, but an unrecognised marker makes
26
+ * it WARN on stderr, and stderr is a compared column. A closure pruned by what the
27
+ * caller consumes drops a function whose whole contribution is a side effect.
28
+ * * `_memory_facts` builds a record per fact through `_frontmatter`; the panel counts
29
+ * them. `memoryFactCount` is the count, and `frontmatter` stays where it is.
30
+ *
31
+ * WHERE THE DETECTORS WENT. `themeOfDir`, `footprintOfDir`, `installedDefaults`,
32
+ * `manifestIsClaude` and the configured defaults were written here in P5d because `status`
33
+ * was their only caller. `js/installs.mjs` owns them since P5f, when `diff` and `rebuild-all`
34
+ * became the second and third. Nothing about them changed in the move — the 166-cell
35
+ * acceptance matrix is what licensed attempting it, `status`'s own eleven cells among them.
36
+ *
37
+ * WHAT NO CELL CAN REACH, and where it is gated instead. `tests/harness_golden.py`'s
38
+ * status/version section carries the long form; the short form is that ROOT is not
39
+ * redirectable across a process boundary, so three things here are unreachable from any
40
+ * cell: `versionVerdict`'s "up to date" (needs a marker holding a fingerprint no cell can
41
+ * know), `accentFor`'s cyan fallback (an unknown theme is refused upstream by
42
+ * `effectiveTheme`), and the whole ANSI half of `statusLines` (`_color_enabled` is
43
+ * `sys.stdout.isatty()`, and every harness captures stdout through a pipe). All three are
44
+ * PURE FUNCTIONS of their arguments, so all three are gated as a corpus in
45
+ * `tests/test_pure_function_parity.py` instead — which is a third answer to the colour
46
+ * question the P5c handoff posed as a choice between shipping it ungated and not shipping
47
+ * it.
48
+ *
49
+ * `manifestIsClaude` was a FOURTH until wave 2 of the P0/P1 review: it is only reached for
50
+ * a candidate with no known host, and `ROOT/"Harness"` was ordered ahead of the sandbox's
51
+ * own. Both ROOT-relative bundle candidates are gone from this walk and from
52
+ * `installedDefaults`, so the only no-known-host candidate left is `cwd/"Harness"`, which
53
+ * is inside the sandbox. It is reachable now; no cell seeds a manifest without an emit
54
+ * marker, so it is still gated only by the corpus, and that is a gap rather than a wall.
55
+ */
56
+ import { existsSync, readdirSync } from 'node:fs';
57
+ import path from 'node:path';
58
+
59
+ import { THEMES, ROOT, makeCfg } from './checkout.mjs';
60
+ import { tuiInventory } from './inventory.mjs';
61
+ import { readVersion, sourceFingerprint } from './emit.mjs';
62
+ import {
63
+ claudeConfigDir, bobConfigDir, copilotConfigDir, opencodeConfigDir, pyResolve,
64
+ resolveMemoryDir,
65
+ } from './hosts.mjs';
66
+ // P5f moved the install DETECTORS out of this file — `diff` renders its expected copy in the
67
+ // deployed theme and footprint, and `rebuild-all` re-emits in the deployed everything, so
68
+ // three verbs now read them. `defaultTheme`, `manifestIsClaude` and `installedDefaults` are
69
+ // re-exported below because `tests/fixtures/pure_probe.mjs` names this module for two of them
70
+ // and a corpus that followed the code to its new file would stop testing the caller's view.
71
+ import {
72
+ defaultTheme, installedDefaults, manifestIsClaude, readJsonMaybe, readMaybe,
73
+ } from './installs.mjs';
74
+ import { pyLen, pyLjust, pyPrint } from './lib/fs.mjs';
75
+
76
+ export { defaultTheme, manifestIsClaude };
77
+
78
+ // ---- version ---------------------------------------------------------------------------
79
+
80
+ /** `_harness_status._version_verdict`. */
81
+ export function versionVerdict(installed, current) {
82
+ if (installed === null || installed === undefined) {
83
+ return 'no Geneseed install detected to compare';
84
+ }
85
+ if (installed === current) return 'up to date with this source';
86
+ return 'installed build differs from the current source — run '
87
+ + '`./geneseed update` (or rebuild) to apply it';
88
+ }
89
+
90
+ /** `_harness_status.cmd_version`. */
91
+ export function cmdVersion(args) {
92
+ const cfg = makeCfg();
93
+ const current = sourceFingerprint(cfg);
94
+ // `Path(args.target).expanduser().resolve()` — `pyResolve` already expands, so the tilde
95
+ // is handled once rather than twice.
96
+ let target = args.target ? pyResolve(args.target) : opencodeConfigDir();
97
+ let installed = readVersion(target);
98
+ if (installed === null) {
99
+ // A claude/bob/copilot-only machine must not report "no install detected". The bundle
100
+ // candidates are cwd-relative only — see the Python for why `ROOT / "Harness"` left.
101
+ for (const base of [claudeConfigDir(), bobConfigDir(), copilotConfigDir(),
102
+ path.join(process.cwd(), 'Harness'), process.cwd()]) {
103
+ const v = readVersion(base);
104
+ if (v) { installed = v; target = base; break; }
105
+ }
106
+ }
107
+ pyPrint(`[version] source: ${current} (${ROOT})\n`);
108
+ pyPrint(`[version] installed: ${installed || '(none found)'}`
109
+ + (installed ? ` (${target})` : '') + '\n');
110
+ pyPrint(`[version] ${versionVerdict(installed, current)}\n`);
111
+ return 0;
112
+ }
113
+
114
+ // ---- what the panel counts ---------------------------------------------------------------
115
+
116
+ /**
117
+ * `_harness_tui._tui_inventory`, reduced to the three counts `_status_data` reads off it.
118
+ *
119
+ * THREE `length`s OFF THE REAL WALK SINCE P6c, and the change is the point. P5d shipped a
120
+ * counting-only twin — the bodies, purposes, source paths and taxonomy classes were P7's,
121
+ * and porting them to produce three integers would have been absurd — with a docblock
122
+ * warning that the two must not become two classifiers. `api_catalog` consumes exactly the
123
+ * fields the count threw away, so P6c is the phase that would have made them two. It did
124
+ * not: `tuiInventory` in `js/inventory.mjs` is the one walk, this is three `length`s off
125
+ * it, and the eleven `status` cells and the catalog cells now gate the same classifier
126
+ * from opposite ends.
127
+ */
128
+ export function inventoryCounts(themeName) {
129
+ const inv = tuiInventory(themeName);
130
+ return { agents: inv.agents.length, skills: inv.skills.length, laws: inv.laws.length };
131
+ }
132
+
133
+ /**
134
+ * `_harness_tui_views._memory_facts`, reduced to its length.
135
+ *
136
+ * A path `glob("*.md")` matches but `read_text` cannot read — a DIRECTORY called `notes.md`
137
+ * is the reachable case — is skipped, not counted and not fatal.
138
+ */
139
+ export function memoryFactCount(mdir) {
140
+ let names;
141
+ try { names = readdirSync(mdir); } catch { return 0; }
142
+ let n = 0;
143
+ for (const name of names.filter((x) => x.endsWith('.md')).sort()) {
144
+ const stem = name.slice(0, -3);
145
+ if (['memory', 'readme'].includes(stem.toLowerCase())) continue;
146
+ if (readMaybe(path.join(mdir, name)) === null) continue;
147
+ n += 1;
148
+ }
149
+ return n;
150
+ }
151
+
152
+ /** `_harness_tui_draw._accent_for` — the ACCENT a theme declares, default cyan. */
153
+ export function accentFor(theme) {
154
+ const doc = readJsonMaybe(path.join(THEMES, `${theme}.json`));
155
+ if (!doc || typeof doc !== 'object') return 'cyan';
156
+ return doc.ACCENT ?? 'cyan';
157
+ }
158
+
159
+ // ---- the panel ----------------------------------------------------------------------------
160
+
161
+ /** `_harness_status._status_data`. */
162
+ export function statusData() {
163
+ const inst = installedDefaults();
164
+ const theme = inst.theme || defaultTheme();
165
+ const mdir = resolveMemoryDir(null);
166
+ const inv = inventoryCounts(theme);
167
+ let cfgDir = null;
168
+ try { cfgDir = opencodeConfigDir(); } catch { cfgDir = null; }
169
+ const sourceFp = sourceFingerprint(makeCfg());
170
+ let installedFp = null;
171
+ let verTarget = null;
172
+ const otherCfg = [];
173
+ for (const fn of [claudeConfigDir, bobConfigDir, copilotConfigDir]) {
174
+ try { otherCfg.push(fn()); } catch { /* a missing host dir must not sink status */ }
175
+ }
176
+ // Same walk as `cmdVersion`'s fallback, and cwd-relative for the same reason.
177
+ const candidates = [...(cfgDir ? [cfgDir] : []), ...otherCfg,
178
+ path.join(process.cwd(), 'Harness'), process.cwd()];
179
+ for (const base of candidates) {
180
+ const v = readVersion(base);
181
+ if (v) { installedFp = v; verTarget = base; break; }
182
+ }
183
+ const agentMd = (inst.emit === 'opencode-global' && cfgDir)
184
+ ? path.join(cfgDir, 'AGENT.md') : null;
185
+ return {
186
+ theme,
187
+ accent: accentFor(theme),
188
+ emit: inst.emit || '—',
189
+ agents: inv.agents,
190
+ skills: inv.skills,
191
+ laws: inv.laws,
192
+ memory_dir: mdir ? String(mdir) : null,
193
+ facts: mdir ? memoryFactCount(mdir) : 0,
194
+ source_fp: sourceFp,
195
+ installed_fp: installedFp,
196
+ version_target: verTarget ? String(verTarget) : null,
197
+ version_verdict: versionVerdict(installedFp, sourceFp),
198
+ agent_md: agentMd ? String(agentMd) : null,
199
+ agent_md_present: Boolean(agentMd && existsSync(agentMd)),
200
+ };
201
+ }
202
+
203
+ /** `_harness_status._ANSI_CODES`. */
204
+ const ANSI_CODES = {
205
+ red: '31', green: '32', yellow: '33', blue: '34', magenta: '35', cyan: '36', white: '37',
206
+ };
207
+
208
+ /**
209
+ * `_harness_status._color_enabled`.
210
+ *
211
+ * `process.stdout.isTTY` is `undefined` rather than `false` off a terminal, so the Boolean
212
+ * is not optional. The three terms are Python's, in Python's order — and because `and`
213
+ * short-circuits on the first, NO_COLOR and TERM are never read through a pipe, which is
214
+ * why `golden.cell_env` clearing them is insurance rather than a fix.
215
+ */
216
+ export function colorEnabled() {
217
+ return Boolean(process.stdout.isTTY) && process.env.NO_COLOR === undefined
218
+ && process.env.TERM !== 'dumb';
219
+ }
220
+
221
+ /**
222
+ * `_harness_status._status_lines` — pure, returns the lines.
223
+ *
224
+ * `_TUI_ASCII` is read from the environment at CALL time here where Python reads it at
225
+ * import time (`_harness_tui_draw:12`). Same value for a process that runs one verb and
226
+ * exits; spelled this way because a module-level constant in a file the corpus gate imports
227
+ * would freeze at whatever the test runner's environment was.
228
+ */
229
+ export function statusLines(d, color = false) {
230
+ const asc = Boolean(process.env.GENESEED_TUI_ASCII);
231
+ const [H, V] = asc ? ['-', '|'] : ['─', '│'];
232
+ const [TL, TR, BL, BR, LT, RT] = asc
233
+ ? ['+', '+', '+', '+', '+', '+'] : ['┌', '┐', '└', '┘', '├', '┤'];
234
+ const DOT = asc ? '-' : '·';
235
+ const badge = asc ? '*' : '◆';
236
+ const emdash = asc ? '-' : '—';
237
+
238
+ const up = d.version_verdict.includes('up to date');
239
+ const noneInst = d.installed_fp === null || d.installed_fp === undefined;
240
+ const mark = up ? (asc ? 'OK' : '✓') : (noneInst ? (asc ? '-' : '·') : '!');
241
+ const vcode = up ? '32' : (noneInst ? '2' : '33');
242
+
243
+ const rows = [
244
+ ['theme', `${d.theme} (accent: ${d.accent})`],
245
+ ['install', d.emit],
246
+ ['components', `${d.agents} agents ${DOT} ${d.skills} skills ${DOT} ${d.laws} laws`],
247
+ ['memory', `${d.memory_dir || '(not found)'} `
248
+ + `(${d.facts} fact${d.facts === 1 ? '' : 's'})`],
249
+ ['version', `${d.installed_fp || '(none)'} ${DOT} source ${d.source_fp}`],
250
+ ];
251
+ if (d.agent_md) {
252
+ rows.push(['AGENT.md',
253
+ `${d.agent_md} (${d.agent_md_present ? 'present' : 'MISSING'})`]);
254
+ }
255
+
256
+ const labelW = Math.max(...rows.map(([k]) => pyLen(k)));
257
+ const body = rows.map(([k, v]) => ` ${pyLjust(k, labelW)} ${v}`);
258
+ const verdict = ` ${mark} ${d.version_verdict}`;
259
+ const title = ` ${badge} Geneseed ${emdash} status `;
260
+ const width = Math.max(...body.map(pyLen), pyLen(verdict), pyLen(title) + 2);
261
+
262
+ const ac = ANSI_CODES[d.accent] ?? '36';
263
+ const c = (s, code) => (color ? `\x1b[${code}m${s}\x1b[0m` : s);
264
+
265
+ const top = color
266
+ ? c(TL + H, ac) + c(title, `${ac};1`) + c(H.repeat(width - pyLen(title) - 1) + TR, ac)
267
+ : TL + H + title + H.repeat(width - pyLen(title) - 1) + TR;
268
+ const edge = c(V, ac);
269
+ const lines = [top];
270
+ for (const b of body) lines.push(edge + pyLjust(b, width) + edge);
271
+ lines.push(c(LT + H.repeat(width) + RT, ac));
272
+ lines.push(edge + c(pyLjust(verdict, width), vcode) + edge);
273
+ lines.push(c(BL + H.repeat(width) + BR, ac));
274
+ return lines;
275
+ }
276
+
277
+ /** `_harness_status.cmd_status`. */
278
+ export function cmdStatus() {
279
+ for (const line of statusLines(statusData(), colorEnabled())) pyPrint(`${line}\n`);
280
+ return 0;
281
+ }
package/js/themes.mjs ADDED
@@ -0,0 +1,195 @@
1
+ /**
2
+ * `_build_render.sync_themes` / `_insert_theme_keys` — the generator's `--sync-themes`, the
3
+ * maintainer tool that fills every shipped theme with the keys `_TEMPLATE.json` defines.
4
+ *
5
+ * IT IS A BYTE-EXACT TEXTUAL EDIT OF COMMITTED FILES, which is what makes it the most
6
+ * dangerous thing in this phase and why it has its own corpus before it has a port. Every
7
+ * other tool this port has crossed WRITES A TREE it also renders; this one reaches into
8
+ * fourteen files a human wrote, inserts a line into each, and leaves the other ~170 lines
9
+ * untouched. A port that got the insertion right and the newline translation wrong would
10
+ * rewrite every line of every theme on the first maintainer run, and `git diff` — not a
11
+ * gate — is where that would be discovered.
12
+ *
13
+ * NEITHER `--sync-themes` NOR `--validate-only` IS REACHABLE FROM ANY CELL. `golden.py`'s
14
+ * `_argv` emits neither flag (argued in situ at `bin/geneseed.mjs`'s dispatch), so the
15
+ * acceptance matrix that gates all nine emits is structurally blind to both, and the driver
16
+ * refused them rather than crossing for exactly that reason. The gate is
17
+ * `tests/test_maintainer_tools_parity.py`: a corpus of hand-made theme directories run
18
+ * through BOTH implementations, comparing the printed output, the return value, and the
19
+ * resulting FILE BYTES.
20
+ *
21
+ * WHY THIS MODULE IS NOT `js/doctor.mjs`. `--sync-themes` has no doctor dependency, so it
22
+ * stays on the generator driver where its flag lives — and the driver is under a transitive
23
+ * ban on starting processes, which this module's imports respect. Its sibling
24
+ * `--validate-only` needs the doctor and therefore could not stay; see `cmdValidate`.
25
+ *
26
+ * THE PRIMITIVES ARE BORROWED, NOT REWRITTEN. `pySplitLines`, `pyStripSpace`/`pyLStripSpace`/
27
+ * `pyRStripSpace`, `parseJson`, `pyEq`, `jsonDumpsCompact`, `jsonDumpsIndent`, `readText` and
28
+ * `writeText` all already exist and are all already gated. This file's documented failure
29
+ * mode is the same primitive existing twice under two names and the two disagreeing, and
30
+ * `str.strip` and `json.dumps` are precisely the two that would have been re-derived here.
31
+ */
32
+ import path from 'node:path';
33
+
34
+ import { THEMES } from './checkout.mjs';
35
+ import { themeFiles } from './installs.mjs';
36
+ import { pySplitLines } from './lib/udiff.mjs';
37
+ import {
38
+ jsonDumpsCompact, jsonDumpsIndent, parseJson, pyEq, pyLStripSpace, pyRStripSpace,
39
+ pyStripSpace, readText, writeText,
40
+ } from './lib/fs.mjs';
41
+
42
+ /**
43
+ * `_build_render._insert_theme_keys` — insert ONLY the missing keys into the theme's existing
44
+ * TEXT, each as one new line at its template-order position.
45
+ *
46
+ * A full `json.dumps` round-trip cannot reproduce the shipped files byte-for-byte (they mix
47
+ * raw Unicode with legacy `\\uXXXX` escapes), so re-dumping made a one-key sync a ~170-line
48
+ * rewrite per theme. Textual insertion keeps the diff to the inserted line plus at most a
49
+ * comma on its predecessor.
50
+ *
51
+ * Returns `null` when the file does not follow the shipped convention (`{` alone on line 1,
52
+ * one ` "KEY": value` per line) or when the result fails the reparse safety check — the
53
+ * caller then falls back to a full re-dump.
54
+ *
55
+ * FOUR PRIMITIVES CARRY THE WHOLE THING, and each is a place the two languages differ:
56
+ * - `pySplitLines` is `str.splitlines()`, which breaks on U+2028/U+2029 where `split('\\n')`
57
+ * does not — a theme file that acquired one from a paste would be re-lined by Python and
58
+ * not by a naive port, and the insertion index would then point at a different key;
59
+ * - the three `strip` spellings are Python's whitespace set, not `String.prototype.trim`'s;
60
+ * - `jsonDumpsCompact(v, {ensureAscii: false})` is `json.dumps(v, ensure_ascii=False)`,
61
+ * including the `', '`/`': '` separators — which matter for exactly ONE key,
62
+ * `AGENT_COLORS`, the only non-string among `_TEMPLATE.json`'s 137 values. `ensureAscii`
63
+ * is false because the reference passes `ensure_ascii=False`, and 33 template values
64
+ * carry non-ASCII text that would otherwise be escaped into the committed file;
65
+ * - `pyEq`, because the safety net compares two `json.loads` results and `===` is identity
66
+ * for containers — a `===` here would return `null` every time and silently route every
67
+ * theme through the re-dump fallback, i.e. rewrite all 14 files in full.
68
+ */
69
+ export function insertThemeKeys(raw, theme, tmpl, tmplKeys, missing) {
70
+ const lines = pySplitLines(raw);
71
+ if (!lines.length || pyStripSpace(lines[0]) !== '{') return null;
72
+
73
+ const lineOf = (key) => {
74
+ // `f' "{key}":'` — raw interpolation, NOT a JSON-escaped key. A theme key carrying a
75
+ // quote would not match on either side, which is the same answer both give.
76
+ const prefix = ` "${key}":`;
77
+ for (let i = 0; i < lines.length; i += 1) if (lines[i].startsWith(prefix)) return i;
78
+ return null;
79
+ };
80
+
81
+ // Every existing key must be locatable as its own line, or the format assumption is wrong
82
+ // and inserting would corrupt the file.
83
+ for (const k of Object.keys(theme)) if (lineOf(k) === null) return null;
84
+
85
+ for (const k of missing) { // template order, so an earlier insert can anchor a later one
86
+ const entry = ` "${k}": ${jsonDumpsCompact(tmpl[k], { ensureAscii: false })}`;
87
+ let pred = null;
88
+ const earlier = tmplKeys.slice(0, tmplKeys.indexOf(k));
89
+ for (let i = earlier.length - 1; i >= 0; i -= 1) {
90
+ const li = lineOf(earlier[i]);
91
+ if (li !== null) { pred = li; break; }
92
+ }
93
+ if (pred === null) {
94
+ // No earlier template key exists in this theme: insert right after `{`.
95
+ const follows = lines.slice(1).some((ln) => pyLStripSpace(ln).startsWith('"'));
96
+ lines.splice(1, 0, entry + (follows ? ',' : ''));
97
+ } else if (pyRStripSpace(lines[pred]).endsWith(',')) {
98
+ lines.splice(pred + 1, 0, `${entry},`);
99
+ } else {
100
+ // Predecessor was the last entry: it gains the comma, the new line is last.
101
+ lines[pred] += ',';
102
+ lines.splice(pred + 1, 0, entry);
103
+ }
104
+ }
105
+
106
+ const newText = `${lines.join('\n')}\n`;
107
+ // Safety net: the surgically-edited text must parse back to exactly the merge we meant.
108
+ let reparsed;
109
+ try { reparsed = parseJson(newText); } catch { return null; }
110
+ const want = { ...theme };
111
+ for (const k of missing) want[k] = tmpl[k];
112
+ return pyEq(reparsed, want) ? newText : null;
113
+ }
114
+
115
+ /**
116
+ * `_build_render.sync_themes` — fill every shipped theme with the template's keys.
117
+ *
118
+ * Returns the number of themes actually modified (0 == already in sync); the driver maps that
119
+ * to the exit code, so CI can run the tool as a drift check.
120
+ *
121
+ * `themesDir` is a parameter for the corpus, whose whole subject is what this writes; the
122
+ * default is the checkout's own `themes/`, which is what the driver passes.
123
+ *
124
+ * PRINTS THROUGH A BARE `process.stdout.write`, like the generator's other ~25 print sites and
125
+ * unlike every CLI verb: `bin/geneseed.mjs`'s `main` wraps the whole run in `withPyNewlines`,
126
+ * so a `pyPrint` here would translate a second time and put `\r\r\n` on every line.
127
+ *
128
+ * ONE DELIBERATE DIVERGENCE FROM THE REFERENCE, changed on BOTH sides in this commit: a
129
+ * missing or unreadable `_TEMPLATE.json` used to print and `return 0`, which the CLI maps to
130
+ * exit 0 — "already in sync". That is a false green in the one job the tool exists for, and
131
+ * the input that produces it is the one that makes the whole run vacuous. It now refuses with
132
+ * exit 2. Dropping the interpolated exception text is the second half of the same change and
133
+ * is what makes the branch gateable at all: `json.JSONDecodeError` and V8's parse error
134
+ * disagree on both wording and offset, so a message carrying `{e}` can never be compared
135
+ * across the two implementations. The PER-THEME unreadable message below keeps its `{e}` and
136
+ * therefore keeps that divergence, matching the convention `js/doctor.mjs` already ships at
137
+ * six sites — a skipped theme is a warning, not the tool's verdict.
138
+ */
139
+ export function syncThemes(themesDir = THEMES) {
140
+ const tmplPath = path.join(themesDir, '_TEMPLATE.json');
141
+ let tmpl;
142
+ try {
143
+ tmpl = parseJson(readText(tmplPath));
144
+ } catch {
145
+ process.stdout.write(`[sync-themes] ${path.basename(tmplPath)} is missing or unreadable `
146
+ + '— there is nothing to sync against.\n');
147
+ const e = new Error('template unreadable');
148
+ e.exitCode = 2;
149
+ throw e;
150
+ }
151
+ const tmplKeys = Object.keys(tmpl);
152
+ let changed = 0;
153
+ for (const p of themeFiles(themesDir)) {
154
+ const name = path.basename(p);
155
+ let raw;
156
+ let theme;
157
+ try {
158
+ raw = readText(p);
159
+ theme = parseJson(raw);
160
+ } catch (e) {
161
+ process.stdout.write(`[sync-themes] ${name}: unreadable (${e.message}) — skipped\n`);
162
+ continue;
163
+ }
164
+ // `Object.hasOwn`, never `k in theme`: a theme defining a `constructor` or `toString`
165
+ // voice token would otherwise be reported as already carrying every key it lacks.
166
+ const missing = tmplKeys.filter((k) => !Object.hasOwn(theme, k));
167
+ const extra = Object.keys(theme).filter((k) => !Object.hasOwn(tmpl, k)).sort();
168
+ if (!missing.length) {
169
+ if (extra.length) {
170
+ process.stdout.write(`[sync-themes] ${name}: in sync (extra key(s) not in template, `
171
+ + `not removed: ${extra.join(', ')})\n`);
172
+ }
173
+ continue;
174
+ }
175
+ let newText = insertThemeKeys(raw, theme, tmpl, tmplKeys, missing);
176
+ if (newText === null) {
177
+ // Unconventional formatting: fall back to a full re-dump — template order, theme-only
178
+ // extras preserved at the end, real Unicode kept as-is.
179
+ const merged = {};
180
+ for (const k of tmplKeys) merged[k] = Object.hasOwn(theme, k) ? theme[k] : tmpl[k];
181
+ for (const k of Object.keys(theme)) if (!Object.hasOwn(merged, k)) merged[k] = theme[k];
182
+ newText = `${jsonDumpsIndent(merged, { ensureAscii: false })}\n`;
183
+ }
184
+ writeText(p, newText);
185
+ changed += 1;
186
+ process.stdout.write(`[sync-themes] ${name}: added ${missing.length} key(s) from the `
187
+ + `template — RESTYLE these in this theme's voice: ${missing.join(', ')}\n`);
188
+ if (extra.length) {
189
+ process.stdout.write(`[sync-themes] ${name}: extra key(s) not in template, not removed: `
190
+ + `${extra.join(', ')}\n`);
191
+ }
192
+ }
193
+ if (!changed) process.stdout.write('[sync-themes] all themes already carry every template key.\n');
194
+ return changed;
195
+ }