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
@@ -0,0 +1,396 @@
1
+ // Geneseed — OpenCode learn plugin.
2
+ //
3
+ // The runtime-agnostic counterpart of the Claude Code `Stop` hook: on
4
+ // `session.idle` it distils durable memories from the conversation and writes them
5
+ // into the bundle's memory/ dir, maintaining MEMORY.md — exactly what the harness's
6
+ // own `learn` verb does, but self-contained in this plugin so no separate process and
7
+ // no model CLI are required. It distils with the SAME model the session already used
8
+ // (read from the transcript), so it inherits your OpenCode provider config: no API
9
+ // key, nothing to set for the model.
10
+ //
11
+ // TIMING — `session.idle` fires after EVERY turn (it is OpenCode's "session became
12
+ // idle / response ready" signal), not only at the true end of a session. Distilling
13
+ // on the first idle would therefore mine just turn 1 and miss the rest. So each idle
14
+ // RE-ARMS a debounce timer (GENESEED_LEARN_DEBOUNCE_MS, default 60s) and the distil
15
+ // runs once the session has been quiet for that long — i.e. at the real end. A
16
+ // continued session simply re-arms and re-distils; slug dedup keeps writes
17
+ // idempotent. (Trade-off: if OpenCode is killed inside the window the final distil
18
+ // is lost — still strictly better than capturing only turn 1, and far cheaper than
19
+ // re-distilling every turn.)
20
+ //
21
+ // Install (global — the bundle is used everywhere, so the plugin should be too):
22
+ // copy this file to ~/.config/opencode/plugins/geneseed-learn.js
23
+ // Or per-project: .opencode/plugins/geneseed-learn.js (build --emit opencode
24
+ // drops it there for you).
25
+ //
26
+ // Where it writes — first match wins:
27
+ // 1. $GENESEED_MEMORY an explicit memory dir
28
+ // 2. $GENESEED_HARNESS/memory (or /anamnesis for the imperial theme)
29
+ // 3. ./memory or ./Harness/memory (when the bundle is inside the project)
30
+ // If none resolve, it logs once and does nothing — it never blocks a session.
31
+ //
32
+ // It is intentionally conservative: trivial sessions are skipped, each session is
33
+ // processed at most once per process, and any error is swallowed.
34
+
35
+ import { promises as fs } from "node:fs"
36
+ import * as path from "node:path"
37
+ import { fileURLToPath } from "node:url"
38
+ import { homedir } from "node:os"
39
+
40
+ // The plugin's own directory. In the global install the layout is
41
+ // <cfg>/plugins/geneseed-learn.js beside <cfg>/memory, so the store can be found
42
+ // relative to this file — no manual `export GENESEED_HARNESS` needed.
43
+ const PLUGIN_DIR = path.dirname(fileURLToPath(import.meta.url))
44
+
45
+ // Sovereign-repo bypass — twin of the harness's own `sovereignBypass()`.
46
+ // <cfg>/excludes.json (user-owned, managed by `harness exclude`) lists folders where
47
+ // this GLOBAL install goes dormant. Any error degrades to "not excluded".
48
+ const norm = (p) => {
49
+ // Expand a leading "~" / "~/…" to the home dir — a hand-edited excludes.json entry
50
+ // like "~/vault" must resolve the same way here as it does for the Claude/Bob
51
+ // hook guard.
52
+ let raw = String(p)
53
+ if (raw === "~" || raw.startsWith("~/") || raw.startsWith("~\\")) {
54
+ raw = raw === "~" ? homedir() : path.join(homedir(), raw.slice(2))
55
+ }
56
+ let s = path.resolve(raw)
57
+ return process.platform === "win32" ? s.toLowerCase() : s
58
+ }
59
+ async function sovereignBypass(cwd) {
60
+ try {
61
+ const cfg = path.dirname(PLUGIN_DIR) // plugins/ sits directly in <cfg>
62
+ const raw = await fs.readFile(path.join(cfg, "excludes.json"), "utf8")
63
+ const entries = (JSON.parse(raw).excludes) || []
64
+ const here = norm(cwd || process.cwd())
65
+ for (const e of entries) {
66
+ const p = typeof e === "string" ? e : e && e.path
67
+ if (typeof p !== "string" || !p.trim()) continue
68
+ const base = norm(p.trim()).replace(/[\\/]+$/, "")
69
+ if (here === base || here.startsWith(base + path.sep)) return true
70
+ }
71
+ } catch { /* degrade to active */ }
72
+ return false
73
+ }
74
+
75
+ const MAX_NOTES_CHARS = 16000 // cap the prompt; keep the most recent tail
76
+ const MIN_NOTES_CHARS = 200 // below this, the session is too trivial to mine
77
+ const MEMORY_DIR_NAMES = ["memory", "anamnesis"] // neutral + imperial
78
+ // NaN-safe: garbage in the env var would make setTimeout fire immediately (NaN
79
+ // delay → 0), distilling after EVERY turn instead of at session end.
80
+ const _debounceRaw = Number(process.env.GENESEED_LEARN_DEBOUNCE_MS || 60000)
81
+ const DEBOUNCE_MS = Number.isFinite(_debounceRaw) && _debounceRaw >= 0 ? _debounceRaw : 60000
82
+
83
+ // SINGLE SOURCE of the distil instructions: the harness extracts this exact
84
+ // LEARN_PROMPT_HEAD template literal at runtime, so there is nothing to keep in sync.
85
+ const LEARN_PROMPT_HEAD = `You are distilling durable memories from the notes below. Output zero or more
86
+ Markdown memory files in this exact format, separated by a line containing only
87
+ '---FILE---':
88
+
89
+ ---
90
+ name: <kebab-case-slug>
91
+ description: <one-line summary>
92
+ type: user | feedback | project | reference
93
+ ---
94
+ <the fact, stated plainly. For 'feedback' and 'project', add **Why:** and
95
+ **How to apply:** lines.>
96
+
97
+ DEFAULT TO 'NOTHING'. Writing a memory is the rare exception, not the norm — most
98
+ sessions should yield zero. Emit AT MOST ONE, and only if it is a GENERAL, reusable
99
+ principle you would want at the START of an unrelated future task.
100
+
101
+ CAPTURE only (high altitude, cross-session):
102
+ - a stable USER preference or working style the user showed or stated;
103
+ - a CONVENTION or constraint that will hold across many future tasks;
104
+ - a durable PROJECT decision or goal that outlives this session.
105
+
106
+ NEVER capture (this is the noise to cut):
107
+ - what was done, found, fixed, or resolved this session ("fixed X", "resolved
108
+ issue 7", "cleaned up the TODO", "renamed Y", "the landing screen redirects");
109
+ - anything tied to one file, function, ticket, bug, or feature;
110
+ - anything derivable from the code, the git history, or the current diff.
111
+
112
+ If you can only phrase it as "this session we …", it is NOT a memory:
113
+ - Too specific: "Fixed the failing test by adding await on line 42."
114
+ - Right (meta): "User wants async setup helpers always awaited."
115
+ - Too specific: "Resolved the magic-constants issue in config.js." -> NO memory
116
+ (a closed task, not a durable lesson).
117
+
118
+ When in doubt, output exactly: NOTHING.`
119
+
120
+ async function isDir(p) {
121
+ try { return (await fs.stat(p)).isDirectory() } catch { return false }
122
+ }
123
+
124
+ async function resolveMemoryDir() {
125
+ const env = process.env.GENESEED_MEMORY
126
+ if (env && (await isDir(env))) return env
127
+ const harness = process.env.GENESEED_HARNESS
128
+ const bases = []
129
+ if (harness) bases.push(harness)
130
+ // The plugin's own dir (global install: <cfg>/plugins → <cfg>/memory) and the repo.
131
+ bases.push(path.resolve(PLUGIN_DIR, ".."), process.cwd(), path.join(process.cwd(), "Harness"))
132
+ for (const base of bases) {
133
+ for (const name of MEMORY_DIR_NAMES) {
134
+ const cand = path.join(base, name)
135
+ if (await isDir(cand)) return cand
136
+ }
137
+ }
138
+ return null
139
+ }
140
+
141
+ function partsText(parts) {
142
+ return (parts || [])
143
+ .filter((p) => p && p.type === "text" && typeof p.text === "string")
144
+ .map((p) => p.text)
145
+ .join("\n")
146
+ }
147
+
148
+ // ---- per-agent memory (memory/agents/<name>.md) ------------------------------
149
+ // Child subagent sessions are task fragments — never mined into the main store —
150
+ // but a lesson about how the *agent* should operate next time is durable. Kept
151
+ // deliberately conservative, like the rest of this plugin: an unresolvable agent
152
+ // name skips entirely. The harness's own twin is behaviour-identical
153
+ // so claude/bob get the same loop via their Stop/SubagentStop hooks.
154
+ const AGENT_NAME_RE = /^[a-z][a-z0-9-]{1,40}$/
155
+ const MAX_AGENT_BULLETS = 100 // hard cap, oldest dropped; no pruning heuristics
156
+
157
+ // SINGLE SOURCE — the harness extracts this exact literal (see LEARN_PROMPT_HEAD).
158
+ export const AGENT_LESSON_PROMPT = `The notes below are one subagent dispatch ("agent run").
159
+ Output AT MOST ONE line: a durable lesson about how this agent should operate on a
160
+ FUTURE, unrelated dispatch (a boundary that proved wrong, an input it always needs,
161
+ a method that worked). Task residue ("fixed X", file names, ticket detail) is NOT a
162
+ lesson. If there is none — the overwhelmingly common case — output exactly: NOTHING.`
163
+
164
+ export function resolveAgentName(meta) {
165
+ const raw = meta?.agent ?? meta?.agentName ?? meta?.agentID ?? null
166
+ const name = typeof raw === "string" ? raw.trim().toLowerCase() : null
167
+ return name && AGENT_NAME_RE.test(name) ? name : null
168
+ }
169
+
170
+ export async function appendAgentLesson(memDir, agent, lesson) {
171
+ const dir = path.join(memDir, "agents")
172
+ await fs.mkdir(dir, { recursive: true })
173
+ const file = path.join(dir, `${agent}.md`)
174
+ let bullets = []
175
+ try {
176
+ bullets = (await fs.readFile(file, "utf8")).split("\n").filter((l) => l.startsWith("- "))
177
+ } catch { /* first lesson for this agent */ }
178
+ const day = new Date().toISOString().slice(0, 10)
179
+ bullets.push(`- ${day}: ${lesson.replace(/\s+/g, " ").trim()}`)
180
+ bullets = bullets.slice(-MAX_AGENT_BULLETS)
181
+ await fs.writeFile(file, `# ${agent} — lessons\n` + bullets.join("\n") + "\n", "utf8")
182
+ return file
183
+ }
184
+
185
+ // Flatten a session's messages into "role: text" notes, and pick the model the
186
+ // session actually used so we distil with the user's own provider config.
187
+ function flatten(messages) {
188
+ const lines = []
189
+ let model = null
190
+ for (const m of messages) {
191
+ const info = m?.info ?? {}
192
+ const role = info.role
193
+ if (role !== "user" && role !== "assistant") continue
194
+ const text = partsText(m?.parts).trim()
195
+ if (text) lines.push(`${role}: ${text}`)
196
+ if (role === "assistant") {
197
+ const providerID = info.providerID ?? info.provider
198
+ const modelID = info.modelID ?? info.model
199
+ if (providerID && modelID) model = { providerID, modelID }
200
+ }
201
+ }
202
+ return { notes: lines.join("\n\n"), model }
203
+ }
204
+
205
+ function envModel() {
206
+ const v = process.env.GENESEED_MODEL // "provider/model"
207
+ if (v && v.includes("/")) {
208
+ const [providerID, ...rest] = v.split("/")
209
+ return { providerID, modelID: rest.join("/") }
210
+ }
211
+ return null
212
+ }
213
+
214
+ async function existingSlugs(memDir) {
215
+ const skip = new Set(["memory", "readme"])
216
+ try {
217
+ const entries = await fs.readdir(memDir)
218
+ return new Set(
219
+ entries
220
+ .filter((f) => f.endsWith(".md"))
221
+ .map((f) => f.slice(0, -3))
222
+ .filter((s) => !skip.has(s.toLowerCase()))
223
+ )
224
+ } catch {
225
+ return new Set()
226
+ }
227
+ }
228
+
229
+ function buildPrompt(notes, existing) {
230
+ const parts = [LEARN_PROMPT_HEAD, ""]
231
+ if (existing.size) {
232
+ parts.push(
233
+ "ALREADY STORED — do NOT emit a memory matching any of these slugs " +
234
+ "(skip updates too; only genuinely new facts):"
235
+ )
236
+ for (const slug of [...existing].sort()) parts.push(`- ${slug}`)
237
+ parts.push("")
238
+ }
239
+ parts.push("NOTES:", notes)
240
+ return parts.join("\n")
241
+ }
242
+
243
+ function parseFrontmatter(chunk) {
244
+ const m = chunk.match(/^\s*---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/)
245
+ if (!m) return {}
246
+ const fm = {}
247
+ for (const line of m[1].split("\n")) {
248
+ const i = line.indexOf(":")
249
+ if (i > 0) fm[line.slice(0, i).trim()] = line.slice(i + 1).trim().replace(/^"|"$/g, "")
250
+ }
251
+ return fm
252
+ }
253
+
254
+ async function writeMemories(output, memDir, existing) {
255
+ const written = []
256
+ const indexLines = []
257
+ for (let chunk of output.split(/^---FILE---\s*$/m)) {
258
+ chunk = chunk.trim()
259
+ if (!chunk || chunk.toUpperCase() === "NOTHING") continue
260
+ const fm = parseFrontmatter(chunk)
261
+ const name = (fm.name || "").trim()
262
+ if (!name || existing.has(name)) continue
263
+ await fs.writeFile(path.join(memDir, `${name}.md`), chunk.replace(/\n+$/, "") + "\n", "utf8")
264
+ existing.add(name)
265
+ written.push(name)
266
+ const desc = (fm.description || "").trim()
267
+ indexLines.push(`- [${name}](${name}.md)` + (desc ? ` — ${desc}` : ""))
268
+ }
269
+ if (indexLines.length) {
270
+ const index = path.join(memDir, "MEMORY.md")
271
+ let current = "# Memory Index\n"
272
+ try { current = (await fs.readFile(index, "utf8")).replace(/\n+$/, "") + "\n" } catch {}
273
+ await fs.writeFile(index, current + indexLines.join("\n") + "\n", "utf8")
274
+ }
275
+ return written
276
+ }
277
+
278
+ export const GeneseedLearn = async ({ client }) => {
279
+ const ours = new Set() // throwaway distil sessions — never mine our own output
280
+ const timers = new Map() // sid -> debounce timer, re-armed on each idle
281
+ const inFlight = new Set() // sid currently being distilled (guards against overlap)
282
+ let warnedNoDir = false
283
+
284
+ async function runDistill(sid) {
285
+ if (await sovereignBypass(process.cwd())) return
286
+ if (inFlight.has(sid)) return // a run is already underway; the next idle re-arms
287
+ inFlight.add(sid)
288
+ try {
289
+ // A child session (carries a parent link) is a subagent dispatch: never mined
290
+ // into the shared store, but distilled for ONE per-agent lesson if we can name
291
+ // the agent. Field absent → top-level user session, distilled the old way.
292
+ // Unresolvable agent on a child → skip entirely (the prior behaviour).
293
+ const info = await client.session.get?.({ path: { id: sid } }).catch(() => null)
294
+ const meta = info?.data ?? info
295
+ const isChild = !!(meta?.parentID ?? meta?.parentId)
296
+ const agentName = isChild ? resolveAgentName(meta) : null
297
+ if (isChild && !agentName) return
298
+
299
+ const memDir = await resolveMemoryDir()
300
+ if (!memDir) {
301
+ if (!warnedNoDir) {
302
+ warnedNoDir = true
303
+ console.error(
304
+ "[geneseed-learn] no memory dir — set $GENESEED_HARNESS or $GENESEED_MEMORY."
305
+ )
306
+ }
307
+ return
308
+ }
309
+
310
+ const res = await client.session.messages({ path: { id: sid } })
311
+ const messages = Array.isArray(res) ? res : res?.data ?? []
312
+ let { notes, model } = flatten(messages)
313
+ if (notes.length < MIN_NOTES_CHARS) return
314
+ notes = notes.slice(-MAX_NOTES_CHARS)
315
+ model = model ?? envModel()
316
+ if (!model) {
317
+ console.error(
318
+ "[geneseed-learn] could not determine a model — set $GENESEED_MODEL=provider/model."
319
+ )
320
+ return
321
+ }
322
+
323
+ const existing = await existingSlugs(memDir)
324
+ const prompt = isChild
325
+ ? [AGENT_LESSON_PROMPT, "", "NOTES:", notes].join("\n")
326
+ : buildPrompt(notes, existing)
327
+
328
+ // Distil in a throwaway session with the session's own model, then drop it.
329
+ const session = await client.session.create({ body: { title: "geneseed-learn (auto)" } })
330
+ const newId = session?.id ?? session?.data?.id
331
+ if (!newId) {
332
+ // Prompting with {id: undefined} would 404 silently on every idle.
333
+ console.error("[geneseed-learn] session.create returned no id — skipping this distil.")
334
+ return
335
+ }
336
+ ours.add(newId)
337
+ let output = ""
338
+ try {
339
+ const reply = await client.session.prompt({
340
+ path: { id: newId },
341
+ body: { model, parts: [{ type: "text", text: prompt }] },
342
+ })
343
+ output = partsText(reply?.parts ?? reply?.data?.parts) || reply?.text || ""
344
+ } finally {
345
+ if (newId) await client.session.delete({ path: { id: newId } }).catch(() => {})
346
+ }
347
+
348
+ if (!output.trim() || output.trim().toUpperCase() === "NOTHING") return
349
+ if (isChild) {
350
+ const lesson = output.trim().split("\n")[0].replace(/^[-*]\s*/, "").trim()
351
+ if (lesson.length < 10 || lesson.length > 300) return // junk / truncation guard
352
+ const f = await appendAgentLesson(memDir, agentName, lesson)
353
+ console.error(`[geneseed-learn] agent lesson -> ${f}`)
354
+ return
355
+ }
356
+ const written = await writeMemories(output, memDir, existing)
357
+ if (written.length) {
358
+ console.error(`[geneseed-learn] wrote ${written.length} memory file(s): ${written.join(", ")}`)
359
+ }
360
+ } catch (err) {
361
+ // Never break the session over a memory write.
362
+ console.error(`[geneseed-learn] skipped: ${err?.message ?? err}`)
363
+ } finally {
364
+ inFlight.delete(sid)
365
+ }
366
+ }
367
+
368
+ return {
369
+ event: async ({ event }) => {
370
+ if (!event) return
371
+ const sid =
372
+ event.properties?.sessionID ??
373
+ event.payload?.sessionID ??
374
+ event.properties?.info?.id ??
375
+ event.payload?.info?.id
376
+ // A deleted session must not distil later from a stale timer (and its entry
377
+ // must not leak in the maps for the life of the process).
378
+ if (event.type === "session.deleted") {
379
+ const t = timers.get(sid)
380
+ if (t) clearTimeout(t)
381
+ timers.delete(sid)
382
+ ours.delete(sid)
383
+ return
384
+ }
385
+ if (event.type !== "session.idle") return
386
+ if (!sid || ours.has(sid)) return
387
+ // session.idle fires after every turn — debounce so we distil once, at the real
388
+ // end of the session (see header note). Each idle re-arms the timer.
389
+ const prev = timers.get(sid)
390
+ if (prev) clearTimeout(prev)
391
+ timers.set(sid, setTimeout(() => { timers.delete(sid); runDistill(sid) }, DEBOUNCE_MS))
392
+ },
393
+ }
394
+ }
395
+
396
+ export default GeneseedLearn
@@ -0,0 +1,142 @@
1
+ // Geneseed — OpenCode desktop-notification plugin.
2
+ //
3
+ // Pings the OS when the agent finishes a turn, so you can walk away from a long run
4
+ // and be called back when it is your move again. Hooks OpenCode's `session.idle`
5
+ // event (fired when the assistant goes quiet) the same "act, don't just instruct"
6
+ // way the other plugins hook their events.
7
+ //
8
+ // ANTI-SPAM: it only fires when the turn actually took a while — the gap between the
9
+ // session's last user prompt and now must exceed GENESEED_NOTIFY_MIN_SECONDS
10
+ // (default 30). A two-second back-and-forth never notifies; a five-minute build or
11
+ // test run does. Native subagent child sessions and the learn plugin's throwaway
12
+ // `geneseed-*` distil sessions are skipped, so background machinery stays silent.
13
+ //
14
+ // GENESEED_NOTIFY=off disable entirely.
15
+ // GENESEED_NOTIFY_MIN_SECONDS=N minimum turn length, in seconds, to notify (default 30; 0 = always).
16
+ // GENESEED_NOTIFY_TITLE="…" override the notification title (default "Geneseed").
17
+ // GENESEED_DEBUG=1 log decisions/delivery to stderr.
18
+ //
19
+ // DELIVERY is native and dependency-free — no npm package, nothing to install:
20
+ // macOS → `osascript` (always present)
21
+ // Linux → `notify-send` (from libnotify; absent → swallowed, no error)
22
+ // Windows → PowerShell balloon tip (System.Windows.Forms.NotifyIcon)
23
+ // The notifier is spawned detached and unref'd; a missing binary or any failure is
24
+ // swallowed, so it never blocks, delays, or breaks a session.
25
+ //
26
+ // Install: dropped into the plugins dir by `build --emit opencode[-global]` (the *.js
27
+ // glob), exactly like the context, learn, guard, and workflow plugins.
28
+
29
+ import { spawn } from "node:child_process"
30
+
31
+ const MODE = (process.env.GENESEED_NOTIFY || "on").toLowerCase()
32
+ const OFF = ["off", "0", "false", "no"].includes(MODE)
33
+ // NaN-safe: Math.max(0, NaN) is NaN, and `elapsed >= NaN` is always false — garbage
34
+ // in the env var would silently disable every notification.
35
+ const _minRaw = Number(process.env.GENESEED_NOTIFY_MIN_SECONDS ?? 30)
36
+ const MIN_MS = Math.max(0, Number.isFinite(_minRaw) ? _minRaw : 30) * 1000
37
+ const TITLE = process.env.GENESEED_NOTIFY_TITLE || "Geneseed"
38
+ const DEBUG = !!process.env.GENESEED_DEBUG
39
+ function log(msg) { if (DEBUG) console.error(`[geneseed-notify] ${msg}`) }
40
+
41
+ // The timestamp (ms) of the latest user prompt in a transcript — our proxy for "when
42
+ // this turn started", so we can measure how long the agent worked. Tolerant of the
43
+ // few shapes `info.time` takes across SDK versions. Exported for tests.
44
+ function lastUserMs(messages) {
45
+ if (!Array.isArray(messages)) return null
46
+ let t = null
47
+ for (const m of messages) {
48
+ const info = m?.info ?? {}
49
+ if (info.role !== "user") continue
50
+ const created = info?.time?.created ?? info?.time ?? info?.created
51
+ if (typeof created === "number") t = created
52
+ }
53
+ return t
54
+ }
55
+
56
+ // Pure gate: notify only for a real, top-level, long-enough turn. Exported for tests.
57
+ // - a subagent child session (has a parent) is background work → no.
58
+ // - a `geneseed-*` session is the learn plugin's throwaway distil → no.
59
+ // - unknown turn length → yes (don't suppress an opt-in ping over a missing field).
60
+ // - otherwise: only if the turn ran at least `minMs`.
61
+ function shouldNotify({ now, lastUserMs, parentID, title, minMs }) {
62
+ if (parentID) return false
63
+ if (typeof title === "string" && title.startsWith("geneseed-")) return false
64
+ if (lastUserMs == null) return true
65
+ return now - lastUserMs >= minMs
66
+ }
67
+
68
+ // AppleScript double-quoted string literal.
69
+ function asStr(s) { return '"' + String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"' }
70
+ // PowerShell single-quoted string literal (escape ' by doubling).
71
+ function psStr(s) { return "'" + String(s).replace(/'/g, "''") + "'" }
72
+
73
+ function spawnDetached(cmd, args) {
74
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true })
75
+ child.on("error", (e) => log(`spawn ${cmd} failed: ${e?.message ?? e}`)) // e.g. notifier not installed
76
+ child.unref()
77
+ }
78
+
79
+ // Best-effort native notification; every failure is swallowed.
80
+ function deliver(title, body) {
81
+ try {
82
+ if (process.platform === "darwin") {
83
+ spawnDetached("osascript", ["-e", `display notification ${asStr(body)} with title ${asStr(title)}`])
84
+ } else if (process.platform === "win32") {
85
+ const script =
86
+ "Add-Type -AssemblyName System.Windows.Forms;" +
87
+ "Add-Type -AssemblyName System.Drawing;" +
88
+ "$n=New-Object System.Windows.Forms.NotifyIcon;" +
89
+ "$n.Icon=[System.Drawing.SystemIcons]::Information;$n.Visible=$true;" +
90
+ `$n.ShowBalloonTip(5000,${psStr(title)},${psStr(body)},[System.Windows.Forms.ToolTipIcon]::Info);` +
91
+ "Start-Sleep -Seconds 6;$n.Dispose()"
92
+ spawnDetached("powershell", ["-NoProfile", "-NonInteractive", "-Command", script])
93
+ } else {
94
+ spawnDetached("notify-send", [title, body])
95
+ }
96
+ } catch (err) {
97
+ log(`deliver failed: ${err?.message ?? err}`)
98
+ }
99
+ }
100
+
101
+ export const GeneseedNotify = async ({ client }) => {
102
+ return {
103
+ event: async ({ event }) => {
104
+ if (OFF || !event || event.type !== "session.idle") return
105
+ const sid =
106
+ event.properties?.sessionID ?? event.payload?.sessionID ??
107
+ event.properties?.info?.id ?? event.payload?.info?.id
108
+ if (!sid) return
109
+ try {
110
+ let title = "", parentID
111
+ try {
112
+ const info = await client.session.get?.({ path: { id: sid } })
113
+ const meta = info?.data ?? info
114
+ title = meta?.title ?? ""
115
+ parentID = meta?.parentID ?? meta?.parentId
116
+ } catch {}
117
+
118
+ let started = null
119
+ try {
120
+ const res = await client.session.messages({ path: { id: sid } })
121
+ started = lastUserMs(Array.isArray(res) ? res : res?.data ?? [])
122
+ } catch {}
123
+
124
+ if (!shouldNotify({ now: Date.now(), lastUserMs: started, parentID, title, minMs: MIN_MS })) {
125
+ log(`skipped ${sid} (child/throwaway or turn under ${MIN_MS}ms)`)
126
+ return
127
+ }
128
+ deliver(TITLE, title ? `Done: ${title}` : "Agent finished — your turn.")
129
+ log(`notified for ${sid}`)
130
+ } catch (err) {
131
+ log(`skipped: ${err?.message ?? err}`)
132
+ }
133
+ },
134
+ }
135
+ }
136
+
137
+ // ponytail: OpenCode treats every export as a plugin and rejects non-functions; a
138
+ // bare helper export crashes startup or logs "not a function". Hang the test helpers
139
+ // off the factory instead — reachable via `import`, invisible to the loader.
140
+ Object.assign(GeneseedNotify, { lastUserMs, shouldNotify })
141
+
142
+ export default GeneseedNotify