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/diff.mjs ADDED
@@ -0,0 +1,384 @@
1
+ /**
2
+ * `harness diff` — `rituals/_harness_diff.py`. What a DEPLOYED install has that a fresh
3
+ * render of `src/` does not, so in-place edits can be reviewed and back-ported.
4
+ *
5
+ * WHAT THE CLOSURE WALK MISSED, AND IT IS A NEW KIND OF MISS. An import-aware `ast` walk puts
6
+ * this verb at 252 LOC over 10 functions, and every one of those is a few lines of reading a
7
+ * marker or a manifest. The verb's entire user-visible payload is `difflib.unified_diff`,
8
+ * which the walk counts as zero because `difflib` is not in `rituals/`. P5e learned that a
9
+ * walk cannot see through a `subprocess`; this is the same blind spot pointed the other way —
10
+ * it cannot see through the STANDARD LIBRARY either, and a stdlib call with no JS equivalent
11
+ * is a port, not a call. `js/lib/udiff.mjs` is where that went, and it is 273 lines against
12
+ * this file's 312 — the module the walk described as the whole verb is not half of it.
13
+ *
14
+ * THE SECOND thing the walk shows only as one name: `_diff_collect` runs a whole GLOBAL EMIT
15
+ * into a temp dir. `build.HOSTS[host]["emit_global"]` is a `build.*` reference like any other
16
+ * in the table, and behind it is the generator. That call is why this file imports
17
+ * `bin/geneseed.mjs` — the same in-process route `js/generate.mjs` established for `build`,
18
+ * under the same transitive `child_process` ban.
19
+ *
20
+ * WHAT NO CELL CAN VARY, stated rather than left implicit:
21
+ *
22
+ * * **The diff algorithm.** A cell can produce ONE edited file and see one hunk. It cannot
23
+ * produce the shapes where `difflib`'s choices differ from any other diff's — ties, the
24
+ * recursion's alignment, and above all `autojunk`, which only engages at 200 lines and
25
+ * changes the hunks on every real harness file. Gated as a corpus in
26
+ * `tests/test_pure_function_parity.py`, measured in both directions (28 of the 176 cases
27
+ * there disagree when `autojunk` is switched off, so the corpus is not describing a
28
+ * constant).
29
+ * * **The timestamp.** `_write_improvements` names the file `improvements-%Y%m%d-%H%M%S.md`
30
+ * and stamps the report with `datetime.now()`, so the two implementations cannot write the
31
+ * same bytes across a second boundary — and a cell runs them in sequence, so the boundary
32
+ * is a coin flip. `harness_golden._STAMPS` normalises both spellings out of the snapshot,
33
+ * which costs the FORMAT assertion and pays it back in
34
+ * `test_the_improvements_filename_is_stamped_the_same_way_by_both` — absolute, per
35
+ * implementation, because a comparison made tolerant of a value owes one somewhere else.
36
+ */
37
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync } from 'node:fs';
38
+ import os from 'node:os';
39
+ import path from 'node:path';
40
+
41
+ import { emitGlobalInto } from '../bin/geneseed.mjs';
42
+ import { GLOBAL_MANIFEST, VERSION_MARKER, expanduser, opencodeConfigDir } from './hosts.mjs';
43
+ import {
44
+ EMIT_HOST_SCOPE, defaultTheme, footprintOfDir, modeOfDir, postureOfDir, readJsonMaybe,
45
+ readMaybe, themeOfDir,
46
+ } from './installs.mjs';
47
+ import { unifiedDiff, pySplitLines } from './lib/udiff.mjs';
48
+ import { pyPathStr, pyPrint, pyPrintErr, readText, writeText } from './lib/fs.mjs';
49
+
50
+ const isFile = (p) => { try { return statSync(p).isFile(); } catch { return false; } };
51
+
52
+ /**
53
+ * `_harness_core._T0` — this process's start, in epoch seconds.
54
+ *
55
+ * Python samples it at `_harness_core` import time and `_flush_export_notes` is its only
56
+ * reader. Module load is the same moment here for the same reason: the entry imports this
57
+ * module before it parses an argument, so nothing an invocation does can have happened
58
+ * before it.
59
+ */
60
+ const T0 = Date.now() / 1000;
61
+
62
+ /**
63
+ * `_harness_diff._cmp_key` — how an owned file is keyed for drift.
64
+ *
65
+ * `.geneseed-version` carries a build-DATE stamp that is not a local edit, so it is keyed on
66
+ * the fingerprint token alone. `text.split()` is Python's whitespace split with no separator:
67
+ * it collapses runs AND strips leading whitespace, which `String.split(' ')` does neither of.
68
+ */
69
+ export function cmpKey(rel, text) {
70
+ if (rel === VERSION_MARKER) {
71
+ const toks = text.split(/\s+/).filter((s) => s !== '');
72
+ return toks.length ? toks[0] : text;
73
+ }
74
+ return text;
75
+ }
76
+
77
+ /** `_harness_diff._owned_set` — the files a global install claims, per its manifest. */
78
+ function ownedSet(d) {
79
+ const data = readJsonMaybe(path.join(d, GLOBAL_MANIFEST));
80
+ if (data === null || typeof data !== 'object') return new Set();
81
+ const owned = data.owned;
82
+ return new Set(Array.isArray(owned) ? owned : []);
83
+ }
84
+
85
+ /**
86
+ * `_harness_diff._manifest_scope` — "project" or "global", defaulting to "global".
87
+ *
88
+ * Every manifest predating this key is a GLOBAL install, so a missing key, an unreadable file
89
+ * and a malformed one all read as "global". `data.get("scope") or "global"` — an EMPTY string
90
+ * falls back too, which `?? 'global'` alone would not do.
91
+ */
92
+ export function manifestScope(d) {
93
+ const data = readJsonMaybe(path.join(d, GLOBAL_MANIFEST));
94
+ if (data === null || typeof data !== 'object') return 'global';
95
+ return data.scope || 'global';
96
+ }
97
+
98
+ /**
99
+ * `contextlib.redirect_stdout(io.StringIO())` around the emit.
100
+ *
101
+ * The emit prints its own summary line, which is not part of `diff`'s output; the Python
102
+ * swallows it and lets stderr through, so a WARN from the emit still reaches the user. Both
103
+ * halves matter and only the first needs code here — `pyPrint` and every driver funnel go
104
+ * through `process.stdout.write`, so replacing that one method is the whole redirect.
105
+ *
106
+ * `finally`, because the emit can throw: a swallow that leaked would silence every later line
107
+ * this process printed, including the refusal explaining why.
108
+ */
109
+ export function withStdoutSwallowed(fn) {
110
+ const real = process.stdout.write.bind(process.stdout);
111
+ process.stdout.write = () => true;
112
+ try {
113
+ return fn();
114
+ } finally {
115
+ process.stdout.write = real;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * `_harness_diff._diff_collect` — returns `{target, theme, files}`, with `files === null` for
121
+ * "nothing to compare here".
122
+ *
123
+ * FIVE VALUES ARE READ OFF THE DEPLOYMENT rather than defaulted, and each has a scar behind
124
+ * it. The theme, so themed wording is not reported as a local edit. The EMIT, so a Claude
125
+ * install is not diffed against an OpenCode-dialect tree — which would flag every agent and
126
+ * AGENT.md as drift. The FOOTPRINT, because rendering 'expected' at the wrong one reports
127
+ * AGENT.md and laws/universal.md as edited on every lean install, so a freshly rebuilt
128
+ * harness looked drifted the moment it finished building and no rebuild could clear it.
129
+ *
130
+ * The POSTURE and the MODE joined them late, and the reference is where the hole came from:
131
+ * `_build_core`'s defaults are `peer`/`direct` and `rituals/harness.py` never overrode them,
132
+ * so every verb that rendered rendered at the defaults. An install that chose any other
133
+ * register reported its own `## Posture` and `## Mode` sections as a local edit for as long
134
+ * as it lived — and `apiRestore` would have written the default register back over the user's
135
+ * choice, which is the same defect with teeth. `rebuild-all` never had it: it reads
136
+ * `postureOfDir`/`modeOfDir` per install root, and this now asks the same two questions.
137
+ */
138
+ export function diffCollect({ target = null, theme = null, emit = null, footprint = null } = {}) {
139
+ // `Path(target).expanduser()` — expanded and NOT resolved, which is the opposite of
140
+ // `cmd_version`'s `.expanduser().resolve()` two files over. It is observable: `target` is
141
+ // printed on the summary line, so `--target .` reports `.` here and an absolute path
142
+ // there. Reproduced rather than made consistent, because consistency would be a change to
143
+ // the reference's output.
144
+ const dir = target ? expanduser(target) : opencodeConfigDir();
145
+ if (!existsSync(path.join(dir, GLOBAL_MANIFEST))) return { target: dir, theme, files: null };
146
+ if (manifestScope(dir) === 'project') return { target: dir, theme, files: null };
147
+
148
+ let themeName = theme;
149
+ if (!themeName) themeName = themeOfDir(dir);
150
+ // The Python INLINES this read rather than calling `_default_theme`, and the two bodies
151
+ // are identical — so this calls the one owner instead of copying the body a third time.
152
+ if (!themeName) themeName = defaultTheme();
153
+
154
+ let emitName = emit;
155
+ if (!emitName) {
156
+ const marker = readMaybe(path.join(dir, '.geneseed-emit'));
157
+ emitName = marker === null ? '' : marker.trim();
158
+ }
159
+ const host = (EMIT_HOST_SCOPE.get(emitName) ?? ['opencode', 'global'])[0];
160
+
161
+ const fp = footprint || footprintOfDir(dir);
162
+
163
+ const files = [];
164
+ const tmp = mkdtempSync(path.join(os.tmpdir(), 'geneseed-diff-'));
165
+ try {
166
+ const expected = path.join(tmp, 'expected');
167
+ withStdoutSwallowed(() => emitGlobalInto(host, {
168
+ theme: themeName, out: path.join(tmp, 'bundle'), cfgDir: expected, footprint: fp,
169
+ posture: postureOfDir(dir), mode: modeOfDir(dir),
170
+ }));
171
+ const rels = [...new Set([...ownedSet(dir), ...ownedSet(expected)])].sort();
172
+ for (const rel of rels) {
173
+ const a = path.join(dir, rel);
174
+ const b = path.join(expected, rel);
175
+ if (isFile(a) && isFile(b)) {
176
+ // `read_text(errors="replace")` — Node's utf8 decoder substitutes U+FFFD for the
177
+ // same bytes, and `readText` folds the line endings the way `Path.read_text` does.
178
+ const ta = readText(a);
179
+ const tb = readText(b);
180
+ if (cmpKey(rel, ta) !== cmpKey(rel, tb)) {
181
+ const diff = unifiedDiff(pySplitLines(tb), pySplitLines(ta), {
182
+ fromfile: `source/${rel}`, tofile: `deployed/${rel}`, lineterm: '',
183
+ });
184
+ files.push({ rel, status: 'edited', diff });
185
+ }
186
+ } else if (isFile(a)) {
187
+ const body = pySplitLines(readText(a));
188
+ files.push({
189
+ rel,
190
+ status: 'added',
191
+ diff: ['(only in deployed — your addition)', '', ...body.map((ln) => `+${ln}`)],
192
+ });
193
+ } else {
194
+ files.push({
195
+ rel, status: 'missing', diff: ['(in source, not deployed — re-emit to add)'],
196
+ });
197
+ }
198
+ }
199
+ } finally {
200
+ // `tempfile.TemporaryDirectory()`'s cleanup, which runs on the exception path too.
201
+ rmSync(tmp, { recursive: true, force: true });
202
+ }
203
+ // Already sorted by `rel` above; the Python sorts again after the loop and so does this,
204
+ // because the two sorts are over different things — that one is over the RECORDS, and a
205
+ // port that dropped it would agree only while the set-union sort happens to be stable.
206
+ files.sort((x, y) => (x.rel < y.rel ? -1 : x.rel > y.rel ? 1 : 0));
207
+ return { target: dir, theme: themeName, files };
208
+ }
209
+
210
+ /** `_harness_diff._improvements_md` — pure; `when` is the caller's timestamp. */
211
+ export function improvementsMd(target, theme, files, when) {
212
+ const edited = files.filter((f) => f.status === 'edited');
213
+ const added = files.filter((f) => f.status === 'added');
214
+ const missing = files.filter((f) => f.status === 'missing');
215
+ const lines = [
216
+ '# Geneseed — deployed improvements to back-port',
217
+ '',
218
+ `- captured: ${when}`,
219
+ `- deployed: \`${target}\``,
220
+ `- theme: ${theme}`,
221
+ `- ${edited.length} edited · ${added.length} added in deployed · `
222
+ + `${missing.length} missing from deployed`,
223
+ '',
224
+ 'The deployed harness drifted from a fresh render of `src/` — typically the',
225
+ 'self-improvement loops editing agent/skill files in place. Hand this file to',
226
+ 'an agent in the Geneseed source repo and ask it to fold the changes below',
227
+ 'back into `src/`. Diffs read source -> deployed; the expected copy was',
228
+ 'rendered in the deployed theme, so only genuine local edits appear.',
229
+ '',
230
+ ];
231
+ const label = {
232
+ edited: 'edited in deployed',
233
+ added: 'only in deployed — your addition',
234
+ missing: 'in source, not deployed',
235
+ };
236
+ for (const f of files) {
237
+ lines.push(`## \`${f.rel}\` (${label[f.status]})`, '', '```diff', ...f.diff, '```', '');
238
+ }
239
+ return `${lines.join('\n')}\n`;
240
+ }
241
+
242
+ /**
243
+ * `datetime.now().strftime(...)` for the two formats this file uses.
244
+ *
245
+ * Local time, not UTC — `datetime.datetime.now()` has no timezone argument here. Written out
246
+ * rather than reached for through `toISOString()` (which is UTC) or `toLocaleString()` (which
247
+ * is locale-dependent and would change the filename on a French machine).
248
+ */
249
+ function stamp(d) {
250
+ const p2 = (n) => String(n).padStart(2, '0');
251
+ const date = `${d.getFullYear()}${p2(d.getMonth() + 1)}${p2(d.getDate())}`;
252
+ const time = `${p2(d.getHours())}${p2(d.getMinutes())}${p2(d.getSeconds())}`;
253
+ return {
254
+ file: `improvements-${date}-${time}.md`,
255
+ when: `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())} `
256
+ + `${p2(d.getHours())}:${p2(d.getMinutes())}:${p2(d.getSeconds())}`,
257
+ };
258
+ }
259
+
260
+ /**
261
+ * `_harness_diff._write_improvements`.
262
+ *
263
+ * The default destination is `<deployed>/improvements/<timestamp>.md` — inside the install it
264
+ * describes, and never in its manifest: rebuilds compare only owned files so the report is
265
+ * not itself reported as drift, re-emits do not clobber it, and uninstall leaves it (the same
266
+ * contract as the memory store).
267
+ *
268
+ * ONE `now` FOR BOTH USES, as the Python has: the filename and the `captured:` line come from
269
+ * the same instant, so a run that crosses a second boundary cannot name a file one second
270
+ * away from what it says inside.
271
+ */
272
+ export function writeImprovements(target, theme, files, outPath = null) {
273
+ const now = stamp(new Date());
274
+ // `Path(out_path).expanduser()` — again NOT resolved, and again observable: the path is
275
+ // printed, so `--out p.md` reports `p.md`.
276
+ const p = outPath
277
+ ? expanduser(outPath)
278
+ : path.join(target, 'improvements', now.file);
279
+ mkdirSync(path.dirname(p), { recursive: true });
280
+ writeText(p, improvementsMd(target, theme, files, now.when));
281
+ return p;
282
+ }
283
+
284
+ /**
285
+ * `_harness_diff.export_improvements` — the drift report as a LIBRARY call, for the callers
286
+ * that want the artifact without the verb's printing.
287
+ *
288
+ * Returns `[path, files]`: `path` is null when nothing was written, and the two reasons are
289
+ * distinguishable in `files` — `null` is "no deployed global install to compare", `[]` is
290
+ * "an install, and no drift". `setup` is the caller and reads exactly that: it prints the
291
+ * path when there is one and says nothing when there is not.
292
+ *
293
+ * `flushExportNotes` below is its neighbour in the Python, and P5f left it out on the
294
+ * keep-vs-delete criterion this port uses: a function with no caller in this binary. P8b is
295
+ * the due date that docblock named — `cmdBootstrap` is the caller.
296
+ */
297
+ export function exportImprovements(target = null, theme = null, outPath = null) {
298
+ const collected = diffCollect({ target, theme });
299
+ // Python's `if not files` — null AND the empty list both return, which is why the caller
300
+ // gets `files` back rather than a bare path and can tell the two apart itself.
301
+ if (!collected.files || collected.files.length === 0) return [null, collected.files];
302
+ return [writeImprovements(collected.target, collected.theme, collected.files, outPath),
303
+ collected.files];
304
+ }
305
+
306
+ /**
307
+ * `_harness_diff._flush_export_notes` — re-print, on the RESTORED terminal, every improvements
308
+ * file exported since this process started.
309
+ *
310
+ * WHY IT SCANS RATHER THAN TRACKS. The export that matters most is the one an update STEP made
311
+ * — in the reference that is a child process, so a call counter in this one would never see it.
312
+ * It reads the GLOBAL install's `improvements/` and takes anything newer than `T0 - 1`, and the
313
+ * one-second slack is the reference's: a file written in the same second the process started
314
+ * can carry an mtime a hair below it.
315
+ *
316
+ * Its callers in the Python are `cmd_setup`'s curses arm, the TUI's teardown and
317
+ * `cmd_bootstrap`; here it is `cmdBootstrap` alone, because the first two are P7's.
318
+ */
319
+ export function flushExportNotes() {
320
+ let fresh = [];
321
+ try {
322
+ const d = path.join(String(opencodeConfigDir()), 'improvements');
323
+ // `Path.glob` on a directory that is not there yields nothing rather than raising, which
324
+ // is why the reference needs no existence check and this needs the readdir in the try.
325
+ fresh = readdirSync(d)
326
+ .filter((n) => n.startsWith('improvements-') && n.endsWith('.md'))
327
+ .map((n) => path.join(d, n))
328
+ .filter((p) => statSync(p).mtimeMs / 1000 >= T0 - 1)
329
+ .sort();
330
+ } catch { return; }
331
+ if (fresh.length === 0) return;
332
+ for (const p of fresh) pyPrint(`[geneseed] improvements file saved: ${pyPathStr(p)}\n`);
333
+ pyPrint('[geneseed] the deployed harness carried local edits — hand the file to '
334
+ + 'your agent to back-port them into src/.\n');
335
+ }
336
+
337
+ /** `_harness_diff.cmd_diff`. */
338
+ export function cmdDiff(args) {
339
+ const { target, theme, files } = diffCollect({ target: args.target, theme: args.theme });
340
+ if (files === null) {
341
+ if (existsSync(path.join(target, GLOBAL_MANIFEST)) && manifestScope(target) === 'project') {
342
+ pyPrintErr(`[diff] ${target} is a PROJECT install — diff only compares GLOBAL `
343
+ + 'installs against a fresh render (a project layer is diffed against '
344
+ + 'its own repo, which this command does not do). Pass --target at a '
345
+ + 'global config dir (opencode-global/claude-global/bob-global/'
346
+ + 'copilot-global), or omit '
347
+ + '--target to use the default.\n');
348
+ } else {
349
+ pyPrintErr(`[diff] no global Geneseed install at ${target} (no ${GLOBAL_MANIFEST}). `
350
+ + 'Pass --target, or run `--emit opencode-global` first.\n');
351
+ }
352
+ return 1;
353
+ }
354
+ const edited = files.filter((f) => f.status === 'edited');
355
+ const added = files.filter((f) => f.status === 'added');
356
+ const missing = files.filter((f) => f.status === 'missing');
357
+ pyPrint(`[diff] deployed ${target} vs source (theme: ${theme})\n`);
358
+ pyPrint(`[diff] ${edited.length} edited, ${added.length} added-in-deployed, `
359
+ + `${missing.length} missing-from-deployed\n`);
360
+ for (const f of edited) pyPrint(` ~ ${f.rel} (edited in deployed — review to back-port)\n`);
361
+ for (const f of added) pyPrint(` + ${f.rel} (only in deployed — your addition)\n`);
362
+ for (const f of missing) pyPrint(` - ${f.rel} (in source, not deployed — re-emit to add)\n`);
363
+ if (args.out) {
364
+ if (files.length) {
365
+ // Bare `--out` is argparse's `nargs="?"` const: True, meaning "the default timestamped
366
+ // path". `parse()` gives a flag `true` and an option a string, so the two arrive
367
+ // distinguishable here without a third kind of entry in the VERBS table.
368
+ const outPath = args.out === true ? null : args.out;
369
+ pyPrint(`[diff] improvements file written: ${
370
+ writeImprovements(target, theme, files, outPath)}\n`);
371
+ } else {
372
+ pyPrint('[diff] no differences — nothing written.\n');
373
+ }
374
+ }
375
+ if (args.full) {
376
+ for (const f of edited) {
377
+ pyPrint(`\n--- ${f.rel} (source -> deployed) ---\n`);
378
+ pyPrint(`${f.diff.join('\n')}\n`);
379
+ }
380
+ } else if (edited.length && !args.out) {
381
+ pyPrint('\nRun with --full to see the line-level diffs, or --out FILE to export them.\n');
382
+ }
383
+ return 0;
384
+ }