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,531 @@
1
+ /**
2
+ * The web console's Docs pages — `rituals/_web_docs.py` and the registry
3
+ * `_web_core.py` builds from `docs/web/`.
4
+ *
5
+ * WHAT CROSSES HERE AND WHAT DOES NOT, measured before the phase was taken rather than
6
+ * discovered inside it. `api_docs_page` dispatches on five KINDS, and 57 of the 59 pages
7
+ * are one of the three that are pure string work: `markdown` (read, slice, strip),
8
+ * `concept` (an inline body with live counts substituted) and `glossary`. Those three
9
+ * crossed in P6d, `about` in P8c (four of five), and `cli` is P10c's. The two deferrals had
10
+ * a reason that is not "it is long":
11
+ *
12
+ * * `cli` walked `harness.build_argparser()` — **182 lines, 24 subparsers, 43
13
+ * `add_argument` calls**, every one carrying help text, a metavar, choices and a
14
+ * default. A Node twin cannot introspect it; reproducing it means transcribing the
15
+ * whole CLI surface into a second table that would describe verbs `bin/geneseed-cli.mjs`
16
+ * cannot even run. This port's standing rule is that a copy of a value under test stops
17
+ * being the value under test, and this would be the largest copy in it. **PAID IN P10c**,
18
+ * by the fix the deferral named: the parser's metadata became a FILE both implementations
19
+ * read — `js/cli-table.json` since P2. `js/cli.mjs` is the reader,
20
+ * `tests/test_cli_reference.py` is what fails when the table and the parser part company
21
+ * for as long as the parser exists, and `bin/geneseed-cli.mjs`'s `VERBS` table
22
+ * — which was a SECOND transcription of the same 43 calls — reads it too, so the port
23
+ * ended with one description of the CLI where it had been heading for three.
24
+ * * `about` called `_update._origin_display()`, which shells out to `git remote get-url`.
25
+ * `rituals/_update.py` was P8's whole subject and P6d would have needed an
26
+ * `_ALLOWED_SPAWNS` row for `git` ahead of the phase that argues for it. **PAID IN P8c**:
27
+ * P8a made that argument and ported `originDisplay()`, so the deferral was a one-line
28
+ * removal from `NOT_PORTED_KINDS` and a row in `KIND_ROUTES`.
29
+ *
30
+ * So `NOT_PORTED_KINDS` declares what is left, `api_docs_page` answers 501 for it, and
31
+ * `tests/test_web_server.py` cross-checks that set against the kinds `_web_docs.py`
32
+ * actually dispatches on — the same partition shape the route table took in P6b, which is
33
+ * why it is a table here too and not two `if`s.
34
+ *
35
+ * THE `?harness=` QUERY PARAM LANDS HERE. P6a skipped it deliberately (no caller); it is
36
+ * the Docs selector, and it is the ONLY input to these endpoints that is not the checkout,
37
+ * so it is also the only thing a cell can vary. Every filtering rule below is gated by
38
+ * sending both values over one page.
39
+ */
40
+ import { readdirSync, statSync } from 'node:fs';
41
+ import path from 'node:path';
42
+
43
+ import { ROOT, THEMES } from '../checkout.mjs';
44
+ import { cliReference } from '../cli.mjs';
45
+ import { readJsonMaybe, readMaybe } from '../installs.mjs';
46
+ import { pyResolve } from '../hosts.mjs';
47
+ import { PY_SPACE, normcase, parseJson, pyStripSpace } from '../lib/fs.mjs';
48
+ import { statusData } from '../status.mjs';
49
+ import { originDisplay } from '../update.mjs';
50
+ import { NotFound, deployed, resolveLinks } from './api.mjs';
51
+
52
+ const isFile = (p) => { try { return statSync(p).isFile(); } catch { return false; } };
53
+
54
+ const DOC_DIR = path.join(ROOT, 'docs', 'web');
55
+
56
+ /**
57
+ * `_web_core._doc_frontmatter` — a docs page split into its frontmatter map and its body.
58
+ *
59
+ * Deliberately not YAML: each value is a JSON scalar (or a JSON object for `link`), which
60
+ * covers every key these pages use and parses with the standard library. A value that is
61
+ * not valid JSON is kept as the raw string, which is how `title: Something` works.
62
+ */
63
+ export function docFrontmatter(text) {
64
+ const marker = '---\n';
65
+ if (!text.startsWith(marker)) return [{}, text];
66
+ const rest = text.slice(marker.length);
67
+ const at = rest.indexOf(marker);
68
+ if (at < 0) return [{}, text];
69
+ const head = rest.slice(0, at);
70
+ const body = rest.slice(at + marker.length);
71
+ const meta = {};
72
+ for (const line of head.split('\n')) {
73
+ const colon = line.indexOf(':');
74
+ const key = (colon < 0 ? line : line.slice(0, colon)).trim();
75
+ const raw = (colon < 0 ? '' : line.slice(colon + 1)).trim();
76
+ if (!key || !raw) continue;
77
+ // `json.loads` first, the raw string on a decode error — and `parseJson`, so a
78
+ // frontmatter number keeps the int/float distinction the rest of this port protects.
79
+ try { meta[key] = parseJson(raw); } catch { meta[key] = raw; }
80
+ }
81
+ return [meta, body];
82
+ }
83
+
84
+ /**
85
+ * `_web_core._load_doc_groups` — the Docs registry, built from `docs/web/`.
86
+ *
87
+ * A page whose file is missing or malformed is skipped rather than crashing the server; an
88
+ * unreadable registry yields an empty Docs section, which the UI renders as "no pages".
89
+ *
90
+ * NOT cached at module load, where the Python builds `DOC_GROUPS` once at import. The
91
+ * difference is invisible to a request — `docs/web/` cannot change under a running daemon
92
+ * any more than `dist/` can — and a module-level constant would freeze whatever `ROOT` was
93
+ * when the corpus first imported this file.
94
+ */
95
+ export function docGroups() {
96
+ const groups = readJsonMaybe(path.join(DOC_DIR, '_groups.json'));
97
+ if (!Array.isArray(groups)) return [];
98
+ const byId = new Map();
99
+ for (const g of groups) byId.set(g.id, { ...g, pages: [] });
100
+ let names;
101
+ try { names = readdirSync(DOC_DIR); } catch { return []; }
102
+ for (const name of names.filter((n) => normcase(n).endsWith('.md')).sort()) {
103
+ const text = readMaybe(path.join(DOC_DIR, name));
104
+ if (text === null) continue;
105
+ const [meta, body] = docFrontmatter(text);
106
+ const group = byId.get(meta.group);
107
+ delete meta.group;
108
+ if (group === undefined) continue;
109
+ const order = Object.hasOwn(meta, 'order') ? meta.order : 0;
110
+ delete meta.order;
111
+ const page = { id: name.slice(0, -3), ...meta };
112
+ if (body.trim()) page.body = body.replace(/\n+$/, '');
113
+ group.pages.push([order, page]);
114
+ }
115
+ const out = [];
116
+ for (const g of groups) {
117
+ const entry = byId.get(g.id);
118
+ // `sorted(key=lambda t: t[0])` is STABLE, so equal orders keep filename order.
119
+ entry.pages = entry.pages
120
+ .map((pair, i) => [pair[0], i, pair[1]])
121
+ .sort((a, b) => (a[0] - b[0]) || (a[1] - b[1]))
122
+ .map((t) => t[2]);
123
+ out.push(entry);
124
+ }
125
+ return out;
126
+ }
127
+
128
+ /**
129
+ * `_web_core.GLOSSARY_KEYS` — the neutral term, the theme key whose value renames it, and
130
+ * the one-line description. A key of `null` marks a term no theme renames.
131
+ */
132
+ const GLOSSARY_KEYS = [
133
+ ['Rule (Law)', 'LAW', 'the governance rules the agent obeys'],
134
+ ['Rules (Laws)', 'LAWS', 'the body of governance rules'],
135
+ ['Agent', 'AGENT', 'a capability specialist'],
136
+ ['Agents', 'AGENTS', 'the roster of specialists'],
137
+ ['Skill', 'SKILL', 'a repeatable workflow'],
138
+ ['Skills', 'SKILLS', 'the catalogue of workflows'],
139
+ ['Memory', 'MEMORY', 'durable, one-fact-per-file knowledge'],
140
+ ['Notebook', 'NOTEBOOK', "the agent's sovereign space"],
141
+ ['Wiki', 'WIKI', 'the machine-wide knowledge base'],
142
+ ['Pact', 'PACT', 'the two-way collaboration contract'],
143
+ ['Posture', null, 'the relationship register the agent works in '
144
+ + '(peer, mentor, expert, assistant, artisan)'],
145
+ ['Mode', null, 'how work gets executed — direct (the agent works every '
146
+ + 'task itself) or foreman (substantial tasks spawn an isolated pipeline)'],
147
+ ['Footprint', null, 'how much of the Rules loads inline each turn '
148
+ + '(full vs lean)'],
149
+ ['Profile', null, 'who you are — seeded once, colours but never binds'],
150
+ ['Memory force', null, "a memory's binding strength (constraint, "
151
+ + 'choice, conviction, tempered)'],
152
+ ['Tagline', 'TAGLINE', 'the one-line essence of the theme'],
153
+ ['Loaded sigil', 'LOADED_SIGIL', 'what the agent emits when ready'],
154
+ ['Benediction', 'BENEDICTION', 'the closing line of an install'],
155
+ ];
156
+
157
+ // ---- counts substituted into concept bodies -------------------------------------------
158
+
159
+ /** `_web_docs._doc_counts`. */
160
+ function docCounts(state) {
161
+ const inv = state.inventory;
162
+ const plugins = path.join(ROOT, 'adapters', 'opencode', 'plugins');
163
+ let n = 0;
164
+ try {
165
+ n = readdirSync(plugins).filter((f) => f.startsWith('geneseed-') && f.endsWith('.js'))
166
+ .length;
167
+ } catch { n = 0; }
168
+ return {
169
+ '{N_LAWS}': (inv.laws || []).length,
170
+ '{N_AGENTS}': (inv.agents || []).length,
171
+ '{N_SKILLS}': (inv.skills || []).length,
172
+ '{N_PLUGINS}': n,
173
+ };
174
+ }
175
+
176
+ /** `_web_docs._sub_counts`. */
177
+ function subCounts(state, body) {
178
+ if (!body.includes('{N_')) return body;
179
+ let out = body;
180
+ for (const [token, n] of Object.entries(docCounts(state))) {
181
+ out = out.split(token).join(String(n));
182
+ }
183
+ return out;
184
+ }
185
+
186
+ // ---- harness filtering -----------------------------------------------------------------
187
+
188
+ const HARNESSES = ['opencode', 'claude'];
189
+ const HARNESS_OPEN_RE = /^\s*<!--\s*harness:(opencode|claude)\s*-->\s*$/;
190
+ const HARNESS_CLOSE_RE = /^\s*<!--\s*\/harness\s*-->\s*$/;
191
+ /**
192
+ * The cheap presence test for the early-out. It must never be NARROWER than the open
193
+ * pattern above — a stray-spaced marker would then slip the guard and leak unstripped —
194
+ * which is why it allows the same `\s*` after `<!--`.
195
+ */
196
+ const HARNESS_HINT_RE = /<!--\s*harness:/;
197
+
198
+ /** `_web_docs._norm_harness`. */
199
+ export function normHarness(value, state) {
200
+ const v = (value || '').trim().toLowerCase();
201
+ if (HARNESSES.includes(v)) return v;
202
+ return String(state.emit || '').startsWith('claude') ? 'claude' : 'opencode';
203
+ }
204
+
205
+ /**
206
+ * `_web_docs._harness_blocks_balanced` — every open has a matching close, with no nesting.
207
+ *
208
+ * A marker inside a ``` fence is example text, not a marker (the same rule `sliceSection`
209
+ * uses). Its purpose is to FAIL OPEN: an unbalanced marker leaves the body untouched, so a
210
+ * typo can never blank the rest of a page.
211
+ *
212
+ * EXPORTED FOR THE AUTHORING GATE, in the shape `installAgentEntryOf` established: failing
213
+ * open means a typo is INVISIBLE in the rendered output — the page simply shows everything,
214
+ * which is also what a correct page with no markers does. The only way to catch it is to ask
215
+ * the predicate directly, so `tests/unit/web_api.test.mjs` walks every doc source through it.
216
+ */
217
+ export function harnessBlocksBalanced(lines) {
218
+ let open = false;
219
+ let inFence = false;
220
+ for (const line of lines) {
221
+ if (line.startsWith('```')) { inFence = !inFence; continue; }
222
+ if (inFence) continue;
223
+ if (HARNESS_OPEN_RE.test(line)) {
224
+ if (open) return false;
225
+ open = true;
226
+ } else if (HARNESS_CLOSE_RE.test(line)) {
227
+ if (!open) return false;
228
+ open = false;
229
+ }
230
+ }
231
+ return !open;
232
+ }
233
+
234
+ /** `_web_docs._strip_harness_blocks`. */
235
+ export function stripHarnessBlocks(body, harnessName) {
236
+ if (!HARNESS_HINT_RE.test(body)) return body;
237
+ const lines = pySplitlines(body);
238
+ if (!harnessBlocksBalanced(lines)) return body;
239
+ const out = [];
240
+ let keep = true;
241
+ let inFence = false;
242
+ for (const line of lines) {
243
+ if (line.startsWith('```')) {
244
+ inFence = !inFence;
245
+ if (keep) out.push(line);
246
+ continue;
247
+ }
248
+ if (!inFence) {
249
+ const m = HARNESS_OPEN_RE.exec(line);
250
+ if (m) { keep = m[1] === harnessName; continue; }
251
+ if (HARNESS_CLOSE_RE.test(line)) { keep = true; continue; }
252
+ }
253
+ if (keep) out.push(line);
254
+ }
255
+ return out.join('\n');
256
+ }
257
+
258
+ /**
259
+ * `str.splitlines()` — which is NOT `split('\n')`: it drops a single trailing newline
260
+ * instead of yielding a final empty string, and that difference reaches the output here
261
+ * because the result is re-joined with `\n`.
262
+ */
263
+ function pySplitlines(s) {
264
+ const parts = s.split('\n');
265
+ if (parts.length && parts[parts.length - 1] === '') parts.pop();
266
+ return parts;
267
+ }
268
+
269
+ /** `_web_docs._visible_groups` — pure; the registry is never mutated. */
270
+ function visibleGroups(harnessName) {
271
+ const groups = [];
272
+ for (const g of docGroups()) {
273
+ if (g.harness && g.harness !== harnessName) continue;
274
+ const pages = g.pages.filter((p) => !p.harness || p.harness === harnessName);
275
+ if (pages.length) groups.push({ ...g, pages });
276
+ }
277
+ return groups;
278
+ }
279
+
280
+ // ---- slugs and section slicing ----------------------------------------------------------
281
+
282
+ const SLUG_STRIP_RE = new RegExp(`[^a-z0-9${PY_SPACE}-]`, 'g');
283
+ const SLUG_WS_RE = new RegExp(`[${PY_SPACE}]+`, 'g');
284
+ const SLUG_DASH_RE = /-+/g;
285
+ const HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
286
+
287
+ /**
288
+ * `_web_docs._slugify_heading` — the same rules the frontend's `slug()` uses, so a
289
+ * registry `anchor` written against a heading matches the id the renderer assigns.
290
+ *
291
+ * `PY_SPACE` and `pyStripSpace` rather than `\s` and `trim()`, and the corpus in
292
+ * `tests/test_pure_function_parity.py` is what made that necessary rather than tidy: the
293
+ * two languages' whitespace classes differ at U+FEFF and at U+001C-U+001F, and a heading
294
+ * carrying one slugs differently on each side. See `PY_SPACE`'s docblock for the measured
295
+ * set.
296
+ */
297
+ export function slugifyHeading(text) {
298
+ let s = pyStripSpace(text.toLowerCase()).replace(SLUG_STRIP_RE, '');
299
+ s = s.replace(SLUG_WS_RE, '-').replace(SLUG_DASH_RE, '-');
300
+ return s.replace(/^-+|-+$/g, '');
301
+ }
302
+
303
+ /**
304
+ * `_web_docs._slice_section` — the body trimmed to one heading's section.
305
+ *
306
+ * Returns `[body, ok]`; `ok === false` (with the ORIGINAL body) when the anchor is
307
+ * missing, so the caller falls back to the whole document. An H1 slice stops at the first
308
+ * H2 — `max(level, 2)` — so it captures an intro paragraph rather than the whole file.
309
+ */
310
+ export function sliceSection(body, anchor) {
311
+ const lines = pySplitlines(body);
312
+ let start = -1;
313
+ let startLevel = 0;
314
+ let inFence = false;
315
+ for (let i = 0; i < lines.length; i += 1) {
316
+ const ln = lines[i];
317
+ if (ln.startsWith('```')) { inFence = !inFence; continue; }
318
+ if (inFence) continue;
319
+ const m = HEADING_RE.exec(ln);
320
+ if (!m) continue;
321
+ if (slugifyHeading(m[2]) === anchor) {
322
+ start = i;
323
+ startLevel = Math.max(m[1].length, 2);
324
+ break;
325
+ }
326
+ }
327
+ if (start < 0) return [body, false];
328
+ const out = [lines[start]];
329
+ inFence = false;
330
+ for (let j = start + 1; j < lines.length; j += 1) {
331
+ const ln = lines[j];
332
+ if (ln.startsWith('```')) { inFence = !inFence; out.push(ln); continue; }
333
+ if (inFence) { out.push(ln); continue; }
334
+ const m = HEADING_RE.exec(ln);
335
+ if (m && m[1].length <= startLevel) break;
336
+ out.push(ln);
337
+ }
338
+ while (out.length && out[out.length - 1].trim() === '') out.pop();
339
+ return [`${out.join('\n')}\n`, true];
340
+ }
341
+
342
+ // ---- the endpoints -----------------------------------------------------------------------
343
+
344
+ /** `_web_docs._find_doc_page`. */
345
+ function findDocPage(pageId) {
346
+ for (const g of docGroups()) {
347
+ for (const p of g.pages) if (p.id === pageId) return p;
348
+ }
349
+ return null;
350
+ }
351
+
352
+ /**
353
+ * `_web_docs._read_doc_source` — a markdown file relative to ROOT, guarded against escapes
354
+ * the same way the catalog is. A GET carries no token, so `rel` comes out of the registry
355
+ * but the guard is what makes that safe to say.
356
+ */
357
+ function readDocSource(rel) {
358
+ const target = pyResolve(path.join(ROOT, rel));
359
+ if (!within(target, ROOT) || !isFile(target)) throw new NotFound(rel);
360
+ return readMaybe(target) ?? '';
361
+ }
362
+
363
+ function within(child, parent) {
364
+ const c = normcase(child).split(/[\\/]/);
365
+ const p = normcase(parent).split(/[\\/]/);
366
+ return p.length <= c.length && p.every((seg, i) => c[i] === seg);
367
+ }
368
+
369
+ /** `_web_docs._glossary` — the deployed theme's words beside the neutral ones. */
370
+ function glossary(state) {
371
+ const load = (theme) => {
372
+ const doc = readJsonMaybe(path.join(THEMES, `${theme}.json`));
373
+ return doc && typeof doc === 'object' && !Array.isArray(doc) ? doc : {};
374
+ };
375
+ const neutral = load('neutral');
376
+ const themed = state.theme !== 'neutral' ? load(state.theme) : neutral;
377
+ const rows = [];
378
+ for (const [label, key, desc] of GLOSSARY_KEYS) {
379
+ if (key === null) {
380
+ const term = label.toLowerCase();
381
+ rows.push({ label, neutral: term, themed: term, desc });
382
+ continue;
383
+ }
384
+ rows.push({
385
+ label,
386
+ neutral: String(Object.hasOwn(neutral, key) ? neutral[key] : '').trim(),
387
+ themed: String(Object.hasOwn(themed, key) ? themed[key] : '').trim(),
388
+ desc,
389
+ });
390
+ }
391
+ return { theme: state.theme, rows };
392
+ }
393
+
394
+ /**
395
+ * `_web_docs._about` — the About page: a version line, the deployed install, and the links.
396
+ *
397
+ * `repo` is the install's OWN git origin (where updates come from), and `repo_is_github` is
398
+ * what gates the github-shaped deep links in the UI. Both come from `originDisplay()`, which
399
+ * is the one thing P6d could not have: it shells `git remote get-url`, and P8a is the phase
400
+ * that argued for the `git` row in `_ALLOWED_SPAWNS`.
401
+ *
402
+ * `version` IS NOT WHAT IT LOOKS LIKE, and it is the shape this page has always had rather than
403
+ * a shortcut here. It reads a `version` key off the status snapshot and THERE IS NO SUCH KEY —
404
+ * the snapshot spells the fingerprints `source_fp` / `installed_fp` / `version_verdict`. So the
405
+ * field is `{}`, always, and `sd.version || {}` reproduces the EXPRESSION rather than the
406
+ * constant: if the key ever appears, the page gains it without another edit.
407
+ *
408
+ * THERE IS NO INTERPRETER FIELD, and the absence is deliberate — see `apiSetup`, which spells
409
+ * the same absence for the same reason.
410
+ *
411
+ * `statusData()` is called for one dead field, and deliberately: this page has always paid that
412
+ * cost, and skipping it would answer faster in a way no gate measures and no user asked for.
413
+ */
414
+ function about(state) {
415
+ const sd = statusData();
416
+ const od = originDisplay();
417
+ return {
418
+ version: sd.version || {},
419
+ theme: state.theme,
420
+ emit: state.emit,
421
+ deployed: deployed(state),
422
+ target: String(state.target),
423
+ root: String(ROOT),
424
+ repo: od.url,
425
+ // `githubSlug`, not `github_slug`. `OriginDisplay`'s JS twin keeps the Python field in
426
+ // camelCase the way every other ported record does, and the RESPONSE key is snake_case
427
+ // because it is the wire format — the two spellings meet exactly here. Reading the wire
428
+ // name off the record gives `undefined`, which is a perfectly plausible `false`: the About
429
+ // page silently drops every github deep link and nothing else changes. Caught by comparing
430
+ // the two payloads by hand before a cell existed; `docs/the-about-page-...` is the cell.
431
+ repo_is_github: Boolean(od.githubSlug),
432
+ license: 'MIT',
433
+ };
434
+ }
435
+
436
+ /**
437
+ * EMPTY SINCE P10c, AND KEPT DECLARED — the third partition in this port to reach that state,
438
+ * after `NOT_PORTED_POST` and `NOT_PORTED_ACTIONS`.
439
+ *
440
+ * `tests/test_web_server.py` cross-checks this set against the kinds
441
+ * `_web_docs.api_docs_page` dispatches on by `ast`, and that check is at its STRONGEST when
442
+ * the set is empty: it then says every kind the reference dispatches on is answered here. A
443
+ * sixth kind added to the reference still cannot quietly fall through to the `NotFound` at
444
+ * the bottom, and the next unported kind has a place to be declared rather than reading as a
445
+ * typo'd page id.
446
+ *
447
+ * WHAT DIED WITH THE LAST ROW is the 501 arm's only reachable target. `test_web_server.py`'s
448
+ * running probe used to GET `/api/docs/page/cli` and require 501; with the set empty no page
449
+ * has an unported kind and none can be borrowed. That probe is now the positive control for
450
+ * `cli` (200), and the claim that the 501 arm still works rests on the declaration check
451
+ * alone — strictly weaker, discipline #7, and said out loud there as it is here.
452
+ *
453
+ * `about` left in P8c (`_origin_display` and the `git` spawn behind it, not the page).
454
+ * `cli` left in P10c, and its blocker was neither: it was that `harness.build_argparser()`
455
+ * cannot be introspected from Node at all. See `js/cli.mjs`.
456
+ */
457
+ export const NOT_PORTED_KINDS = new Set();
458
+
459
+ /** `_web_docs.api_docs`. */
460
+ export function apiDocs(state, harnessName = null) {
461
+ const hn = normHarness(harnessName, state);
462
+ const groups = visibleGroups(hn).map((g) => ({
463
+ id: g.id,
464
+ label: g.label,
465
+ pages: g.pages.map((p) => ({ id: p.id, title: p.title, kind: p.kind })),
466
+ }));
467
+ return { groups, harness: hn };
468
+ }
469
+
470
+ /**
471
+ * `_web_docs.api_docs_page`.
472
+ *
473
+ * The lookup deliberately ignores a page's own `harness` tag: a deep link to a page the
474
+ * active harness hides still resolves, and the client redirects it out of view.
475
+ */
476
+ export function apiDocsPage(state, pageId, harnessName = null) {
477
+ const page = findDocPage(pageId);
478
+ if (!page) throw new NotFound(pageId);
479
+ const hn = normHarness(harnessName, state);
480
+ const render = KIND_ROUTES[page.kind];
481
+ if (render !== undefined) return render(state, pageId, page, hn);
482
+ if (NOT_PORTED_KINDS.has(page.kind)) {
483
+ const e = new Error(`not ported yet: docs kind '${page.kind}'`);
484
+ e.notPorted = true;
485
+ throw e;
486
+ }
487
+ throw new NotFound(pageId);
488
+ }
489
+
490
+ /**
491
+ * The three kinds that cross, as a TABLE and not three `if`s.
492
+ *
493
+ * `tests/test_web_server.py` requires `Object.keys(KIND_ROUTES) ∪ NOT_PORTED_KINDS` to
494
+ * equal the kinds `_web_docs.api_docs_page` dispatches on, and M23 is why that reads the
495
+ * table the dispatcher actually consults rather than a list beside it: a gate on a
496
+ * declaration cannot see a dispatcher that stopped using it.
497
+ */
498
+ const KIND_ROUTES = {
499
+ markdown: (state, pageId, page, hn) => {
500
+ let body = readDocSource(page.source);
501
+ let anchor = page.anchor ?? null;
502
+ // A successful slice drops the anchor: the heading is already at the top, so the
503
+ // client must not also try to scroll to it.
504
+ if (anchor && page.slice) {
505
+ const [sliced, ok] = sliceSection(body, anchor);
506
+ body = sliced;
507
+ if (ok) anchor = null;
508
+ }
509
+ body = stripHarnessBlocks(body, hn);
510
+ return { id: pageId, title: page.title, kind: 'markdown', body,
511
+ source: page.source, anchor, links: resolveLinks(state, body) };
512
+ },
513
+ concept: (state, pageId, page, hn) => {
514
+ const body = subCounts(state, stripHarnessBlocks(page.body ?? '', hn));
515
+ return { id: pageId, title: page.title, kind: 'concept', body,
516
+ link: page.link ?? null, links: resolveLinks(state, body) };
517
+ },
518
+ glossary: (state, pageId, page) => ({
519
+ id: pageId, title: page.title, kind: 'glossary', ...glossary(state),
520
+ }),
521
+ about: (state, pageId, page) => ({
522
+ id: pageId, title: page.title, kind: 'about', ...about(state),
523
+ }),
524
+ // P6d's deferral, paid in P10c. The row is one line because the work was not in the page:
525
+ // it was in making `harness.build_argparser()`'s metadata a file both implementations read.
526
+ cli: (state, pageId, page) => ({
527
+ id: pageId, title: page.title, kind: 'cli', ...cliReference(),
528
+ }),
529
+ };
530
+
531
+ export const PORTED_KINDS = Object.keys(KIND_ROUTES);
Binary file