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,284 @@
1
+ /**
2
+ * `rituals/_web_activity.py` — the live-activity surface, and the first web endpoint that
3
+ * DELETES.
4
+ *
5
+ * It reads the per-session JSON files the `geneseed-activity` OpenCode plugin writes into
6
+ * `<opencode-cfg>/activity/`, prunes the dead and the stale ones (unlinking them, so the
7
+ * directory self-cleans), and returns what is left. Writer and reader share only that
8
+ * directory: no RPC, crash-isolated, and cross-tool by construction.
9
+ *
10
+ * ONLY THE GET HALF CROSSES IN P6e. `/api/activity` answers both verbs and
11
+ * `api_activity_toggle` is a WRITE, so the path stays in `NOT_PORTED_POST` and the
12
+ * partition test in `tests/test_web_server.py` keeps that honest. The consequence for the
13
+ * gate is that the on/off flag can be READ but never flipped, which is why its two arms are
14
+ * two cells rather than two requests in one.
15
+ *
16
+ * THE PLAN GUESSED `tasklist` AND THE SOURCE SAYS OTHERWISE. `_win_pid_alive` shells
17
+ * nothing: it goes through `ctypes` to `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)` and
18
+ * `GetExitCodeProcess`. So there is no sixth `_ALLOWED_SPAWNS` row to argue — not because
19
+ * the Node twin avoids a spawn, but because the REFERENCE never had one. `process.kill(pid,
20
+ * 0)` is the twin here; it is a core `process` API and not `child_process`, so the ban that
21
+ * P4a's source check enforces is untouched. libuv's `uv_kill` opens the process for query
22
+ * and answers `ESRCH` when the pid is free and `EPERM` when it cannot tell, which is
23
+ * exactly the reference's "no such process" / "err toward alive" pair.
24
+ *
25
+ * THE DIVERGENCE THAT LEAVES, DECLARED RATHER THAN HIDDEN. The reference also reads the
26
+ * exit code and calls a process that has EXITED but whose handle is still held dead;
27
+ * `process.kill` reports it alive, and the staleness backstop is what prunes it thirty
28
+ * minutes later. Reproducing the exit-code read would mean a native binding, which this
29
+ * port will not add for a best-effort liveness probe whose own docstring says any
30
+ * inconclusive answer errs toward alive.
31
+ *
32
+ * `parseJson` AND NOT `JSON.parse`, WHICH IS NOT A STYLE CHOICE. These files are written by
33
+ * a third party and carry `cost` — a float. Python's `json.loads` types `1.0` a float and
34
+ * `json.dumps` writes it back `1.0`; `JSON.parse` gives a bare `1` that `bareInts` then
35
+ * renders `1`. `parseJson`'s wrapper is the only thing that survives the round trip, and
36
+ * `s-live`'s `"cost": 1.0` is the cell that says so. Its companion is `pyTruthy`: an
37
+ * `entry.get("cost") or 0` over a wrapper object is TRUE for a wrapped zero, where Python's
38
+ * `0.0 or 0` is the int `0` — so `s-older`'s `"cost": 0.0` must come back `0` and not `0.0`.
39
+ */
40
+ import { mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs';
41
+ import path from 'node:path';
42
+
43
+ import {
44
+ normcase, parseJson, pyInt, pyStripSpace, pyTruthy, readText, writeText,
45
+ } from '../lib/fs.mjs';
46
+ import { NotFound } from './api.mjs';
47
+
48
+ /**
49
+ * `_web_activity.ACTIVITY_STALE_SECONDS` — a session whose writer is gone, or that has not
50
+ * been touched in this long, is pruned. pid-liveness is the real signal; the staleness
51
+ * backstop covers a reused pid and a platform where the probe cannot tell. Generous on
52
+ * purpose: streaming bumps `updated_at` constantly, so only a genuinely abandoned
53
+ * (process-still-alive) session ages out.
54
+ */
55
+ export const ACTIVITY_STALE_SECONDS = 1800;
56
+
57
+ const isDir = (p) => { try { return statSync(p).isDirectory(); } catch { return false; } };
58
+ const isFile = (p) => { try { return statSync(p).isFile(); } catch { return false; } };
59
+
60
+ /** `time.time()` — unix seconds as a float. */
61
+ const nowSeconds = () => Date.now() / 1000;
62
+
63
+ /**
64
+ * `_activity_dir` — `<target>/activity`. `state.target` is ALREADY the OpenCode config dir,
65
+ * so this must not append `opencode/` again; that double-join is the classic seam bug.
66
+ */
67
+ const activityDir = (state) => path.join(state.target, 'activity');
68
+
69
+ /** `_activity_flag` — the runtime on/off flag, the same path the plugin resolves. */
70
+ const activityFlag = (state) => path.join(state.target, '.geneseed-activity');
71
+
72
+ /**
73
+ * `_activity_enabled` — on unless the flag file explicitly says off. An absent (or
74
+ * unreadable) file is the default every user who has never toggled is in.
75
+ *
76
+ * The one input this cannot mirror is an undecodable flag file: Python's
77
+ * `read_text(encoding="utf-8")` raises a `UnicodeDecodeError`, which is a `ValueError` and
78
+ * not caught, where Node substitutes U+FFFD and reads on. No cell seeds one — it would take
79
+ * a binary file where a four-letter word belongs — and the reference's answer there is a
80
+ * 500, which is not a behaviour worth reproducing.
81
+ */
82
+ export function activityEnabled(state) {
83
+ let raw;
84
+ try {
85
+ raw = readText(activityFlag(state));
86
+ } catch {
87
+ return true;
88
+ }
89
+ return !['off', '0', 'false', 'no'].includes(pyStripSpace(raw).toLowerCase());
90
+ }
91
+
92
+ /**
93
+ * `int(value)` as `_pid_alive` calls it — `TypeError` and `ValueError` both mean "not a
94
+ * pid", so both come back null here.
95
+ *
96
+ * The value came out of `parseJson`, so a JSON number arrives inside a wrapper this module
97
+ * cannot import by name. It is recognised by the only thing the wrapper promises, a numeric
98
+ * `valueOf` — a plain object's returns itself and an array is excluded outright, which is
99
+ * what `int({})` and `int([])` raising looks like from here. `int(1.9)` truncates and
100
+ * `int(True)` is 1, both of which a hand-edited snapshot can produce.
101
+ */
102
+ function pyIntOf(raw) {
103
+ if (raw === true) return 1;
104
+ if (raw === false) return 0;
105
+ if (typeof raw === 'string') return pyInt(pyStripSpace(raw));
106
+ const n = (raw !== null && typeof raw === 'object' && !Array.isArray(raw))
107
+ ? raw.valueOf() : raw;
108
+ if (typeof n !== 'number' || !Number.isFinite(n)) return null;
109
+ return Math.trunc(n);
110
+ }
111
+
112
+ /** `_pid_alive` — best-effort liveness for the writer process. See this file's header. */
113
+ export function pidAlive(raw) {
114
+ const pid = pyIntOf(raw);
115
+ if (pid === null || pid <= 0) return false;
116
+ try {
117
+ process.kill(pid, 0);
118
+ return true;
119
+ } catch (e) {
120
+ // `ProcessLookupError` → dead. Anything else (`EPERM`, and whatever a locked-down host
121
+ // answers) → alive, and the staleness backstop decides.
122
+ return !(e && e.code === 'ESRCH');
123
+ }
124
+ }
125
+
126
+ /**
127
+ * `_normalize_entry` — one session snapshot into the stable shape the UI consumes. v1
128
+ * writers omit the enrichment keys, so those default to null/0 and the UI hides what is
129
+ * absent. The KEY ORDER is the reference's dict-literal order, which is what the byte
130
+ * comparison over every response gates.
131
+ */
132
+ function normalizeEntry(entry, stem) {
133
+ const g = (k) => (Object.hasOwn(entry, k) ? entry[k] : null);
134
+ const or = (k, dflt) => (pyTruthy(g(k)) ? g(k) : dflt);
135
+ return {
136
+ session_id: or('session_id', stem),
137
+ agent: g('agent'),
138
+ title: g('title'),
139
+ cwd: g('cwd'),
140
+ status: or('status', 'idle'),
141
+ updated_at: or('updated_at', 0),
142
+ model: g('model'),
143
+ phase: g('phase'),
144
+ turn_started_at: g('turn_started_at'),
145
+ cost: or('cost', 0),
146
+ tokens: or('tokens', 0),
147
+ files: g('files'),
148
+ todos: g('todos'),
149
+ blocked_on: g('blocked_on'),
150
+ error: g('error'),
151
+ };
152
+ }
153
+
154
+ /** `_read_entry` — null on a missing, garbage or non-dict file. Never throws. */
155
+ function readEntry(p) {
156
+ let entry;
157
+ try {
158
+ entry = parseJson(readText(p));
159
+ } catch {
160
+ return null;
161
+ }
162
+ return (entry !== null && typeof entry === 'object' && !Array.isArray(entry)) ? entry : null;
163
+ }
164
+
165
+ /** `_is_live` — the writer's pid is alive AND the snapshot is not stale. */
166
+ function isLive(entry, now) {
167
+ const updated = pyTruthy(Object.hasOwn(entry, 'updated_at') ? entry.updated_at : null)
168
+ ? entry.updated_at : 0;
169
+ return (now - Number(updated)) <= ACTIVITY_STALE_SECONDS && pidAlive(
170
+ Object.hasOwn(entry, 'pid') ? entry.pid : null);
171
+ }
172
+
173
+ /** `Path.stem` — the name without its LAST suffix, so `s-live.detail.json` keeps the dot. */
174
+ const stemOf = (name) => name.slice(0, name.length - path.extname(name).length);
175
+
176
+ /**
177
+ * `_activity_entries` — every live session, dead and stale ones pruned AND their files
178
+ * removed. Never throws: a missing dir is `[]`, a garbage file is skipped (and kept —
179
+ * only a snapshot that PARSED and failed the liveness test is unlinked), newest first.
180
+ */
181
+ export function activityEntries(state) {
182
+ const d = activityDir(state);
183
+ if (!isDir(d)) return [];
184
+ const now = nowSeconds();
185
+ const out = [];
186
+ let names;
187
+ try {
188
+ names = readdirSync(d);
189
+ } catch {
190
+ return [];
191
+ }
192
+ // `Path.glob("*.json")` is case-INSENSITIVE on Windows, as pathlib's is; `str.endswith`
193
+ // on the detail suffix is not, on either platform. Two different rules, deliberately.
194
+ for (const name of names.filter((n) => normcase(n).endsWith('.json')).sort()) {
195
+ if (name.endsWith('.detail.json')) continue;
196
+ const p = path.join(d, name);
197
+ const entry = readEntry(p);
198
+ if (entry === null) continue;
199
+ if (!isLive(entry, now)) {
200
+ try {
201
+ unlinkSync(p); // crashed / abandoned writer
202
+ unlinkSync(path.join(d, `${stemOf(name)}.detail.json`)); // its detail file too
203
+ } catch { /* `contextlib.suppress(OSError)` wraps BOTH unlinks, as this does */ }
204
+ continue;
205
+ }
206
+ out.push(normalizeEntry(entry, stemOf(name)));
207
+ }
208
+ // Stable in both languages, and the tie is the point: `sorted(reverse=True)` preserves the
209
+ // original order of equal keys, and so does a `b - a` comparator over ES2019's stable sort.
210
+ out.sort((a, b) => Number(b.updated_at) - Number(a.updated_at));
211
+ return out;
212
+ }
213
+
214
+ /** `_web_activity.api_activity` (GET). */
215
+ export function apiActivity(state) {
216
+ const enabled = activityEnabled(state);
217
+ return { enabled, activity: enabled ? activityEntries(state) : [] };
218
+ }
219
+
220
+ /**
221
+ * `_web_activity.api_activity_detail` — one session's snapshot plus its step timeline and
222
+ * its UNCAPPED files/todos. 404 if the session is absent or has aged out; a missing or
223
+ * garbage detail file degrades to an empty timeline rather than a 500.
224
+ */
225
+ export function apiActivityDetail(state, sid) {
226
+ const d = activityDir(state);
227
+ // Resolve by the writer's safe-name scheme, which is also what keeps the read inside the
228
+ // directory: every separator, `..` and drive colon becomes `_`, so there is no join to
229
+ // steer. The `u` flag makes an astral character ONE replacement, as Python's code-point
230
+ // `re.sub` does, rather than one per surrogate.
231
+ const stem = sid.replace(/[^A-Za-z0-9_.-]/gu, '_');
232
+ const snap = path.join(d, `${stem}.json`);
233
+ const entry = isFile(snap) ? readEntry(snap) : null;
234
+ if (entry === null || !isLive(entry, nowSeconds())) throw new NotFound(sid);
235
+ const session = normalizeEntry(entry, stem);
236
+ let timeline = [];
237
+ let full = {};
238
+ const det = readEntry(path.join(d, `${stem}.detail.json`));
239
+ if (pyTruthy(det)) {
240
+ const tl = Object.hasOwn(det, 'timeline') ? det.timeline : null;
241
+ timeline = Array.isArray(tl) ? tl : [];
242
+ full = det;
243
+ }
244
+ const fg = (k) => (Object.hasOwn(full, k) ? full[k] : null);
245
+ // The detail file carries the uncapped lists; fall back to the snapshot's capped ones.
246
+ // Assigning over an existing key keeps its POSITION in both languages, which is what lets
247
+ // the response stay byte-comparable.
248
+ session.files = pyTruthy(fg('files')) ? fg('files') : session.files;
249
+ session.todos = pyTruthy(fg('todos')) ? fg('todos') : session.todos;
250
+ // The compact transcript: an ordered list of {role, text, t} turns. Empty falls back to
251
+ // the title as the opening user turn, because OpenCode derives the title from the first
252
+ // prompt.
253
+ const conv = fg('conversation');
254
+ let conversation = Array.isArray(conv) ? conv : [];
255
+ if (conversation.length === 0 && pyTruthy(session.title)) {
256
+ conversation = [{ role: 'user', text: session.title }];
257
+ }
258
+ return { session, timeline, conversation };
259
+ }
260
+
261
+ /**
262
+ * `_web_activity.api_activity_toggle` — flip the runtime on/off flag.
263
+ *
264
+ * The OpenCode plugin reads this file on every event, so the change takes effect without
265
+ * restarting anything; writing `off` also makes the plugin clear its snapshots on its next
266
+ * event, while the reader gates the surface immediately.
267
+ *
268
+ * `bool(body.get("enabled", True))` DEFAULTS TO ON — an empty body enables, which is the
269
+ * arm a port spelled `body.enabled === true` inverts. And this is the one POST in P6f whose
270
+ * result is sent at 200 whatever `ok` says: the 409 convention is per-route in the
271
+ * reference, not a rule about the shape of the body.
272
+ */
273
+ export function apiActivityToggle(state, body) {
274
+ const raw = (body && Object.hasOwn(body, 'enabled')) ? body.enabled : true;
275
+ const enabled = pyTruthy(raw);
276
+ const p = activityFlag(state);
277
+ try {
278
+ mkdirSync(path.dirname(p), { recursive: true });
279
+ writeText(p, enabled ? 'on' : 'off');
280
+ } catch (e) {
281
+ return { ok: false, error: String(e && e.message ? e.message : e) };
282
+ }
283
+ return { ok: true, enabled };
284
+ }