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,89 @@
1
+ // dispatch — live orchestrator: decompose a multi-domain goal, route each
2
+ // subtask to its owning capability agent, converge the results.
3
+ //
4
+ // The model-driven fallback is the parallel-agents skill; this runs the same
5
+ // shape on rails: the script, not the model, enforces routing and the handoff
6
+ // envelope (context in, output contract back, no commits — Law XX stays with
7
+ // the caller).
8
+ //
9
+ // args: { goal: string, context?: string, agents?: string[] }
10
+ // goal — the multi-part objective (required).
11
+ // context — optional shared background prepended to every dispatch.
12
+ // agents — optional whitelist restricting which capabilities may be used.
13
+
14
+ export const meta = {
15
+ name: "dispatch",
16
+ description: "Decompose a goal, route subtasks to owning capability agents, converge.",
17
+ phases: [{ title: "Decompose" }, { title: "Dispatch" }, { title: "Converge" }],
18
+ }
19
+
20
+ // [name, ownership] — name doubles as the OpenCode subagent name (Geneseed emits these).
21
+ const CAPABILITIES = [
22
+ ["explorer", "sweep many files for an answer; read-only reconnaissance"],
23
+ ["architect", "design or plan before code is written"],
24
+ ["reviewer", "correctness + quality pass on a ready change"],
25
+ ["tester", "write or run tests, diagnose a failure"],
26
+ ["security", "auth, input handling, secrets, dependencies"],
27
+ ["docs", "user-facing docs after code has landed"],
28
+ ]
29
+
30
+ const PLAN_SCHEMA = {
31
+ type: "object",
32
+ required: ["subtasks"],
33
+ properties: { subtasks: { type: "array" } },
34
+ }
35
+
36
+ export default async function run(rt) {
37
+ const { agent, parallel, phase, log, args } = rt
38
+ const goal = args?.goal
39
+ if (!goal) return { error: "pass args.goal (the multi-part objective)" }
40
+
41
+ const allowed = new Set(
42
+ Array.isArray(args?.agents) && args.agents.length ? args.agents : CAPABILITIES.map(([n]) => n),
43
+ )
44
+ const roster = CAPABILITIES.filter(([n]) => allowed.has(n))
45
+ if (!roster.length) return { error: `no capability matched — valid: ${CAPABILITIES.map(([n]) => n).join(", ")}` }
46
+
47
+ phase("Decompose")
48
+ const rosterText = roster.map(([n, o]) => `- ${n}: ${o}`).join("\n")
49
+ const plan = await agent(
50
+ `Decompose this goal into 1-6 independent subtasks and route each to the ` +
51
+ `single owning capability below.\n\nGoal: ${goal}\n\nCapabilities:\n${rosterText}\n\n` +
52
+ `Reply as JSON: {"subtasks":[{"agent":"<name>","goal":"<what to achieve>",` +
53
+ `"inputs":"<files/scope/acceptance criteria the agent needs>"}]}. ` +
54
+ `Only subtasks with no ordering between them; fold dependent steps into one subtask.`,
55
+ { label: "decompose", phase: "Decompose", schema: PLAN_SCHEMA },
56
+ )
57
+ const subtasks = (plan?.subtasks || []).filter(
58
+ (t) => t && allowed.has(t.agent) && typeof t.goal === "string" && t.goal.trim(),
59
+ )
60
+ if (!subtasks.length) return { error: "decomposition produced no routable subtasks", plan }
61
+ log(`routing ${subtasks.length} subtask(s): ${subtasks.map((t) => t.agent).join(", ")}`)
62
+
63
+ phase("Dispatch")
64
+ const results = (await parallel(subtasks.map((t, i) => () =>
65
+ agent(
66
+ (args?.context ? `Shared context:\n${args.context}\n\n` : "") +
67
+ `Subtask: ${t.goal}\nInputs: ${t.inputs || "none given"}\n\n` +
68
+ `Handoff envelope: fulfil ONLY this subtask. Read your per-agent memory ` +
69
+ `file first if it exists. Return your output contract as stated in your ` +
70
+ `spec; if you cannot fulfil it, say what is missing — never invent. ` +
71
+ `Do not commit, push, or widen scope; those stay with the caller.`,
72
+ { label: `dispatch:${t.agent}`, phase: "Dispatch", agent: t.agent },
73
+ ).then((text) => ({ ...t, index: i, result: text }))))).filter((r) => r && r.result)
74
+
75
+ phase("Converge")
76
+ const dossier = results
77
+ .map((r) => `### ${r.agent} — ${r.goal}\n${r.result}`)
78
+ .join("\n\n")
79
+ const synthesis = await agent(
80
+ `Goal: ${goal}\n\nSubtask results:\n\n${dossier}\n\n` +
81
+ `Reconcile: state what was achieved, name conflicts between results and how ` +
82
+ `to resolve them, list what remains undone, and give the caller one next ` +
83
+ `action. Report faithfully — unfulfilled contracts are findings, not gaps to paper over.`,
84
+ { label: "converge", phase: "Converge" },
85
+ )
86
+
87
+ log(`dispatched "${goal}" across ${results.length} agent(s)`)
88
+ return { goal, subtasks, results, synthesis }
89
+ }
@@ -0,0 +1,43 @@
1
+ // research-plan-implement — three clean phases with fresh-context handoffs.
2
+ // Each phase runs in its own child session so the next starts on a clean slate,
3
+ // carrying only the distilled output of the prior phase (not its full transcript).
4
+ //
5
+ // args: { topic?: string, question?: string } — what to research/plan/implement.
6
+
7
+ export const meta = {
8
+ name: "research-plan-implement",
9
+ description: "Sequential research → plan → implement, each a clean-context handoff.",
10
+ phases: [{ title: "Research" }, { title: "Plan" }, { title: "Implement" }],
11
+ }
12
+
13
+ export default async function run(rt) {
14
+ const { agent, phase, log, args } = rt
15
+ const topic = args?.topic || args?.question
16
+ if (!topic) return { error: "pass args.topic (what to research, plan, and implement)" }
17
+
18
+ phase("Research")
19
+ const research = await agent(
20
+ `Research best practices and prior art for: ${topic}. ` +
21
+ `Summarise the key findings, trade-offs, and any sources.`,
22
+ { label: "research", phase: "Research", agent: "explorer" },
23
+ )
24
+ // agent() returns null on child failure — stop the cascade instead of
25
+ // interpolating the literal string "null" into the next phase's prompt.
26
+ if (!research) return { error: "research phase failed — see trace", topic }
27
+
28
+ phase("Plan")
29
+ const plan = await agent(
30
+ `Design a concrete implementation plan for: ${topic}.\n\nGround it in this research:\n${research}`,
31
+ { label: "plan", phase: "Plan", agent: "architect" },
32
+ )
33
+ if (!plan) return { error: "plan phase failed — see trace", topic, research }
34
+
35
+ phase("Implement")
36
+ const implementation = await agent(
37
+ `Produce the implementation for: ${topic}, following this plan exactly.\n\nPlan:\n${plan}`,
38
+ { label: "implement", phase: "Implement" },
39
+ )
40
+
41
+ log("research → plan → implement complete")
42
+ return { topic, research, plan, implementation }
43
+ }
@@ -0,0 +1,57 @@
1
+ // review — sweep a change across dimensions, then adversarially verify each finding.
2
+ // The canonical find→verify pipeline: each dimension's findings verify as soon as that
3
+ // dimension's review returns (no barrier between review and verify).
4
+ //
5
+ // args: { target?: string } — what to review (default: the working-tree diff).
6
+
7
+ export const meta = {
8
+ name: "review",
9
+ description: "Review a change across correctness/security/quality, then adversarially verify each finding.",
10
+ phases: [{ title: "Review" }, { title: "Verify" }],
11
+ }
12
+
13
+ const DIMENSIONS = [
14
+ { key: "correctness", prompt: "logic errors, broken contracts, unhandled edge cases" },
15
+ { key: "security", prompt: "injection, auth gaps, unsafe input handling, leaked secrets" },
16
+ { key: "quality", prompt: "dead code, duplication, unclear naming, a simpler equivalent" },
17
+ ]
18
+
19
+ const FINDINGS_SCHEMA = (v) => (Array.isArray(v?.findings) ? true : 'expected { "findings": [...] }')
20
+ const VERDICT_SCHEMA = (v) => (typeof v?.real === "boolean" ? true : 'expected { "real": boolean }')
21
+
22
+ export default async function run(rt) {
23
+ const { agent, parallel, pipeline, phase, log, args } = rt
24
+ const target = args?.target || "the current working-tree changes (git diff)"
25
+ phase("Review")
26
+ // Stage 2 runs once per dimension — announce the Verify phase only on the first
27
+ // entry or the trace shows one banner per dimension.
28
+ let verifyAnnounced = false
29
+
30
+ const results = await pipeline(
31
+ DIMENSIONS,
32
+ // Stage 1 — review one dimension, return structured findings.
33
+ (d) => agent(
34
+ `Review ${target} for ${d.key} issues: ${d.prompt}.\n` +
35
+ `Return JSON: { "findings": [ { "title": "", "file": "", "line": 0, "detail": "" } ] }. ` +
36
+ `Empty array if there are none.`,
37
+ { label: `review:${d.key}`, phase: "Review", schema: FINDINGS_SCHEMA },
38
+ ),
39
+ // Stage 2 — verify this dimension's findings in parallel (no barrier on other dims).
40
+ (review, d) => {
41
+ if (!verifyAnnounced) { verifyAnnounced = true; phase("Verify") }
42
+ const findings = review?.findings || []
43
+ if (!findings.length) { log(`${d.key}: no findings`); return [] }
44
+ return parallel(findings.map((f) => () =>
45
+ agent(
46
+ `Adversarially verify this ${d.key} finding. Try to REFUTE it; default to ` +
47
+ `real=false if uncertain.\nFinding: ${JSON.stringify(f)}\n` +
48
+ `Return JSON: { "real": true|false, "why": "" }.`,
49
+ { label: `verify:${f.file || d.key}`, phase: "Verify", schema: VERDICT_SCHEMA },
50
+ ).then((v) => ({ ...f, dimension: d.key, verdict: v }))))
51
+ },
52
+ )
53
+
54
+ const confirmed = results.flat().filter(Boolean).filter((f) => f.verdict?.real)
55
+ log(`confirmed ${confirmed.length} finding(s) across ${DIMENSIONS.length} dimension(s)`)
56
+ return { target, dimensions: DIMENSIONS.map((d) => d.key), confirmed }
57
+ }
@@ -0,0 +1,412 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The harness CLI entry — a Node twin of `rituals/harness.py` for the verbs a HOOK never
4
+ * invokes. `bin/geneseed-hook.mjs` is the twin of the four it does.
5
+ *
6
+ * WHY A THIRD BINARY. P5c's whole question, and the three candidates were: grow the hook
7
+ * entry, put verbs in `bin/geneseed.mjs`, or this. Each of the other two costs something
8
+ * specific.
9
+ *
10
+ * * GROWING `bin/geneseed-hook.mjs` weakens the gate the design now rests on.
11
+ * `test_the_entry_carries_exactly_the_verbs_the_emitter_wires` asserts EQUALITY between
12
+ * that file's table and the hooks `js/settings.mjs` wires, and P5b made it load-bearing:
13
+ * the shim is machine-wide (`~/.geneseed/bin/geneseed-hook[.cmd]`, no per-install
14
+ * component) and last-writer-wins, so a Node emit owns the hooks of installs Python
15
+ * wrote, and the verb sets matching is what makes that safe. Relaxing it to a subset
16
+ * relation to admit `exclude` would be the single most damaging edit available here.
17
+ * Two more reasons point the same way, and the second is not in that file's own
18
+ * argument: `exclude`'s writer imports `js/settings.mjs` and `js/emit.mjs`, ~2,100 lines
19
+ * that would then load on EVERY PreToolUse call — the spec's hook-latency budget says
20
+ * the shim must exec a minimal entry, not the CLI. And the entry's refusal list, which
21
+ * names every verb it does not carry, is what makes "silently accepted `doctor` and did
22
+ * nothing" impossible; every Geneseed hook exits 0 and signals through stdout, so a
23
+ * no-op and a success are one observation.
24
+ * * `bin/geneseed.mjs` IS `build.py`'s `main()` — a different Python program from
25
+ * `rituals/harness.py`, not a different half of the same one. It parses generator FLAGS
26
+ * with a partition asserted over them (`test_the_node_driver_classifies_every_emit`),
27
+ * `tests/golden.py` drives it 259 times per run, and it is under a hard `child_process`
28
+ * ban that is half the proof it is not a passthrough. `exclude` spawns nothing and would
29
+ * not have broken that ban — but `web`, `upgrade` and `setup` will, and the ban should
30
+ * outlive them rather than be dismantled by the first verb that does not need it.
31
+ *
32
+ * What this costs is what the P5b handoff predicted: a second acceptance matrix. It is paid
33
+ * in `tests/harness_golden.py`, which now carries a `bin` per cell and takes `--new-cli`
34
+ * beside `--new` — and it would have been paid identically by the `bin/geneseed.mjs` option,
35
+ * so it is not a discriminator between the two.
36
+ *
37
+ * THE VERB SET REFUSES THE REST BY NAME, exactly as the hook entry does. It carries 22 of the
38
+ * 26 names the reference's parser answered to (25 subparsers, plus the alias) — the other
39
+ * four are the hook verbs — and, since the three rows at the end of the table below, three
40
+ * that the reference never had at all. `test_the_two_entry_points_carry_disjoint_verb_sets`
41
+ * keeps the two tables from ever answering the same verb twice, since the shim bakes only one
42
+ * of them.
43
+ *
44
+ * P6h MADE THE DISPATCH ASYNCHRONOUS and put `js/web/` on this entry's import graph. Both
45
+ * are noted where they happen (`main`'s `await`, and the `web` row below); the consequence
46
+ * for the file as a whole is that its transitive imports now reach two modules that spawn,
47
+ * which `_ALLOWED_SPAWNS` in `tests/test_hook_cli_parity.py` declares argv by argv.
48
+ */
49
+ import { cmdCatalog } from '../js/catalog.mjs';
50
+ import { cliSpec, printHelp } from '../js/cli.mjs';
51
+ import { cmdDiff } from '../js/diff.mjs';
52
+ import { cmdDoctor, cmdValidate } from '../js/doctor.mjs';
53
+ import { parseDriverArgs } from './geneseed.mjs';
54
+ import { cmdExclude } from '../js/excludes.mjs';
55
+ import { cmdBuild, cmdPrompt, cmdRebuildAll, cmdTheme } from '../js/generate.mjs';
56
+ import { cmdMigrate } from '../js/migrate.mjs';
57
+ import { pyInt } from '../js/lib/fs.mjs';
58
+ import { cmdLink, cmdUnlink } from '../js/link.mjs';
59
+ import { cmdMcp } from '../js/mcp.mjs';
60
+ import { cmdMemory } from '../js/memory.mjs';
61
+ import { cmdHome, cmdMenu } from '../js/menu.mjs';
62
+ import { cmdTui } from '../js/tui.mjs';
63
+ import { cmdSetup } from '../js/setup.mjs';
64
+ import { cmdStatus, cmdVersion } from '../js/status.mjs';
65
+ import { cmdUninstall } from '../js/uninstall.mjs';
66
+ import { cmdBootstrap, cmdSyncSelf, cmdUpgrade } from '../js/update.mjs';
67
+ import { cmdWeb } from '../js/web/server.mjs';
68
+
69
+ /**
70
+ * The verbs this entry answers — and, since P10c, NOTHING ELSE ABOUT THEM.
71
+ *
72
+ * Every row used to carry its own `positionals` / `options` / `flags` / `optValue` / `ints` /
73
+ * `mutex` / `variadic`, transcribed by hand from `rituals/harness.py`: 18 rows describing 43
74
+ * `add_argument` calls, gated against the subparsers by NAME only. When
75
+ * `_web_docs._cli_reference` stopped walking the parser and started reading the CLI table,
76
+ * keeping that would have made THREE transcriptions of one parser — the parser, the file, and
77
+ * here. So the argument surface comes from `js/cli.mjs`'s `cliSpec()`, derived from the same
78
+ * file the docs page serves, and what is left in a row is the one thing a data file cannot
79
+ * hold: the FUNCTION.
80
+ *
81
+ * IT IS STILL A TABLE and still the DISPATCH rather than a declaration beside one.
82
+ * `tests/test_hook_cli_parity.py` scrapes `const VERBS = {` for its three matrix gates, and
83
+ * `main`'s refusal lists `Object.keys(VERBS)` IN THIS ORDER, which `harness_golden` cells
84
+ * assert verbatim — the order is the old table's and is not alphabetical.
85
+ */
86
+ const VERBS = {
87
+ exclude: {
88
+ fn: cmdExclude,
89
+ },
90
+ status: {
91
+ fn: cmdStatus,
92
+ },
93
+ version: {
94
+ fn: cmdVersion,
95
+ },
96
+ build: {
97
+ fn: cmdBuild,
98
+ },
99
+ prompt: {
100
+ fn: cmdPrompt,
101
+ },
102
+ theme: {
103
+ fn: cmdTheme,
104
+ },
105
+ diff: {
106
+ fn: cmdDiff,
107
+ },
108
+ 'rebuild-all': {
109
+ fn: cmdRebuildAll,
110
+ },
111
+ migrate: {
112
+ fn: cmdMigrate,
113
+ },
114
+ doctor: {
115
+ fn: cmdDoctor,
116
+ },
117
+ uninstall: {
118
+ fn: cmdUninstall,
119
+ },
120
+ link: {
121
+ fn: cmdLink,
122
+ },
123
+ unlink: {
124
+ fn: cmdUnlink,
125
+ },
126
+ setup: {
127
+ fn: cmdSetup,
128
+ },
129
+ web: {
130
+ fn: cmdWeb,
131
+ },
132
+ upgrade: {
133
+ fn: cmdUpgrade,
134
+ },
135
+ // `up.add_parser(..., aliases=["update"])`, reproduced in P8c as a ROW OF ITS OWN rather than
136
+ // as an `aliases` field on the row above — because a field would be a DECLARATION and the
137
+ // three matrix gates read this table as the DISPATCH (rule 7, and M23 is where it was
138
+ // learned). As a key it is a real verb: `test_every_entry_verb_is_a_real_harness_subcommand`
139
+ // reads argparse's aliases out of `harness.py` and finds it, and
140
+ // `test_the_matrix_covers_every_verb_it_claims` demands the `update/` cell group that proves
141
+ // it reaches `cmdUpgrade` — including the theme re-read, which is what separates it from
142
+ // `sync-self`.
143
+ //
144
+ // It is NOT what the web console's `update` ACTION needs: that row's argv names `upgrade`,
145
+ // the subparser, and always did. The two were coupled only in a handoff's note.
146
+ update: {
147
+ fn: cmdUpgrade,
148
+ },
149
+ 'sync-self': {
150
+ // Points at `cmdSyncSelf` rather than at `cmdUpgrade`, and that is not tidiness:
151
+ // `sync-self` DROPS its `ref` before `upgrade` ever sees it, where `upgrade` both WARNS
152
+ // about one and re-reads it as a theme — so the two verbs answer `sync-self cyberpunk`
153
+ // and `sync-self v1.2.3` differently. See `js/update.mjs`'s `syncSelf` for both halves.
154
+ fn: cmdSyncSelf,
155
+ },
156
+ bootstrap: {
157
+ fn: cmdBootstrap,
158
+ },
159
+ // P7a. Both are DISPATCHERS whose off-TTY arm is the whole of what a cell can reach, and
160
+ // `menu`'s on-TTY arm falls back rather than opening a panel — `js/menu.mjs`'s header
161
+ // argues both.
162
+ menu: {
163
+ fn: cmdMenu,
164
+ },
165
+ home: {
166
+ fn: cmdHome,
167
+ },
168
+ // P7b, and it is the twenty-fifth and last. `cmd_tui`'s FIRST arm is `if not sys.stdin.
169
+ // isatty()`, so off a TTY — which is every cell there is — the verb is one line and an
170
+ // exit code, and that arm crosses byte for byte. The panel behind it is P7c's;
171
+ // `js/tui.mjs`'s header argues why this entry falls back to `cmd_tui`'s own
172
+ // panel-unavailable line rather than inventing a second full-screen UI, and
173
+ // `tests/test_tui_boundary.py` asserts that the arm it declares is genuinely unreachable
174
+ // here rather than merely untested.
175
+ tui: {
176
+ fn: cmdTui,
177
+ },
178
+ // THE THREE THAT NEVER HAD A PYTHON ORIGINAL, and they are last for that reason rather
179
+ // than by alphabet: every row above is the twin of a subparser, and these three are not.
180
+ // The information behind them existed only behind the web console — the catalog endpoint,
181
+ // the MCP read endpoint, the memory-delete endpoint — so the one way to reach any of it
182
+ // from a shell was to start a server. Each verb calls the module the console already calls;
183
+ // none of the three re-derives anything.
184
+ //
185
+ // ⚠ THEY CANNOT BE COMPARED AGAINST ANYTHING, and that is the whole reason they landed in
186
+ // this change rather than after it. The recorded help corpus was rendered from a live
187
+ // argparse object, and there was no subparser to render these from; the acceptance matrix
188
+ // is a comparison of two implementations, and there is only one. So their gates are
189
+ // ABSOLUTE — `tests/snapshot/cli_help.test.mjs` splits the two populations by name and refuses a
190
+ // verb that falls into neither, and each has its own unit gate stating what it does rather
191
+ // than that it agrees.
192
+ catalog: {
193
+ fn: cmdCatalog,
194
+ },
195
+ mcp: {
196
+ fn: cmdMcp,
197
+ },
198
+ memory: {
199
+ fn: cmdMemory,
200
+ },
201
+ };
202
+
203
+ function die(code, msg) {
204
+ // CRLF on Windows, for the same reason `js/hooks.mjs`'s funnels translate: argparse writes
205
+ // this line through `sys.stderr`, which does.
206
+ process.stderr.write(`geneseed: error: ${msg}${process.platform === 'win32' ? '\r\n' : '\n'}`);
207
+ return code;
208
+ }
209
+
210
+ /**
211
+ * `argparse`'s surface for the verbs here, and only theirs: ordered positionals, some
212
+ * optional, some with `choices`.
213
+ *
214
+ * `spec` comes from `js/cli.mjs`'s `cliSpec()` since P10c — derived from `js/cli-table.json`,
215
+ * which P2 made the owned document. This function's rules are unchanged; what
216
+ * changed is that the table they run over is no longer written out by hand beside them. Every
217
+ * argparse feature named below is a row of that derivation.
218
+ *
219
+ * The WORDING of an argparse failure is deliberately not reproduced — argparse prints a
220
+ * usage block computed from the whole parser tree, and P5a set the precedent for the hook
221
+ * entry: state the fault plainly, gate it absolutely in `test_hook_cli_parity.py`, and keep
222
+ * it out of the compared matrix. What IS compared is every error the command's own body
223
+ * raises, `exclude`'s missing-path refusal among them.
224
+ */
225
+ function parse(spec, argv) {
226
+ const args = {};
227
+ for (const p of spec.positionals) args[p.name] = null;
228
+ for (const name of Object.values(spec.options ?? {})) args[name] = null;
229
+ // `nargs="?"` with `default=None` — the same default as a plain option, and a different
230
+ // value when the flag is present with nothing after it (`const=True`).
231
+ for (const name of Object.values(spec.optValue ?? {})) args[name] = null;
232
+ // `action="store_true"` defaults to False, not None — the difference matters because
233
+ // `cmd_theme` branches on truthiness and `_resolve_themes_dir` reads `--global` with a
234
+ // `getattr(args, "global_dir", False)`.
235
+ for (const name of Object.values(spec.flags ?? {})) args[name] = false;
236
+
237
+ // Options first, in one pass, so an interleaved `version --target X` leaves only
238
+ // positionals behind — argparse does not care about the order and neither may this.
239
+ const rest = [];
240
+ const seen = new Set();
241
+ for (let i = 0; i < argv.length; i += 1) {
242
+ const tok = argv[i];
243
+ const eq = tok.indexOf('=');
244
+ const flag = eq > 0 ? tok.slice(0, eq) : tok;
245
+ const switchDest = (spec.flags ?? {})[flag];
246
+ if (switchDest !== undefined) {
247
+ // A store_true takes no value, so `--global=x` is an error rather than a truthy set.
248
+ if (eq > 0) return { error: `argument ${flag}: ignored explicit argument '${tok.slice(eq + 1)}'` };
249
+ args[switchDest] = true;
250
+ seen.add(flag);
251
+ continue;
252
+ }
253
+ const optDest = (spec.optValue ?? {})[flag];
254
+ if (optDest !== undefined) {
255
+ seen.add(flag);
256
+ if (eq > 0) { args[optDest] = tok.slice(eq + 1); continue; }
257
+ // argparse consumes the next token for a `nargs="?"` only if it does not LOOK like an
258
+ // option — so `diff --out --full` is a bare `--out` followed by `--full`, not an
259
+ // improvements file named `--full`. A lone `-` is a value in argparse and is treated
260
+ // as one here, matching the positional rule below.
261
+ const next = argv[i + 1];
262
+ if (next === undefined || (next.startsWith('-') && next !== '-')) {
263
+ args[optDest] = true;
264
+ continue;
265
+ }
266
+ args[optDest] = next;
267
+ i += 1;
268
+ continue;
269
+ }
270
+ const dest = (spec.options ?? {})[flag];
271
+ if (dest === undefined) { rest.push(tok); continue; }
272
+ const value = eq > 0 ? tok.slice(eq + 1) : argv[i += 1];
273
+ if (value === undefined) return { error: `argument ${flag}: expected one argument` };
274
+ // `type=int`. argparse's own wording, minus the usage block it prints around it — the
275
+ // same rule the rest of this parser follows. The VALUE stays a string in `args`; the
276
+ // command converts, and this is what guarantees the conversion cannot fail there.
277
+ if ((spec.ints ?? []).includes(flag) && pyInt(value) === null) {
278
+ return { error: `argument ${flag}: invalid int value: '${value}'` };
279
+ }
280
+ args[dest] = value;
281
+ seen.add(flag);
282
+ }
283
+
284
+ // `add_mutually_exclusive_group()`. Reproduced as a REFUSAL rather than left out: the
285
+ // wording is not argparse's (see the docblock above), but "refuses" and "silently takes
286
+ // the first one" are different behaviours and only the wording is out of scope.
287
+ for (const group of spec.mutex ?? []) {
288
+ const given = group.filter((f) => seen.has(f));
289
+ if (given.length > 1) {
290
+ return { error: `argument ${given[1]}: not allowed with argument ${given[0]}` };
291
+ }
292
+ }
293
+
294
+ for (const p of spec.positionals) {
295
+ const tok = rest.shift();
296
+ if (tok === undefined) {
297
+ if (p.optional) break;
298
+ return { error: `the following arguments are required: ${p.name}` };
299
+ }
300
+ if (tok.startsWith('-') && tok !== '-') {
301
+ return { error: `unrecognized arguments: ${tok}` };
302
+ }
303
+ if (p.choices && !p.choices.includes(tok)) {
304
+ return { error: `argument ${p.name}: invalid choice: '${tok}' (choose from `
305
+ + `${p.choices.map((c) => `'${c}'`).join(', ')})` };
306
+ }
307
+ args[p.name] = tok;
308
+ }
309
+ // `nargs="*"` — a trailing sink that absorbs whatever the named positionals did not take.
310
+ // It runs AFTER them, which is argparse's order too: `bootstrap main imperial` binds `main`
311
+ // to `ref` and leaves `imperial` here.
312
+ //
313
+ // IT ABSORBS POSITIONALS, NOT OPTIONS, and the distinction is the whole safety of it.
314
+ // argparse fails an unknown `--flag` with `unrecognized arguments` however many `nargs="*"`
315
+ // positionals a subparser has; a sink that swallowed it would make `geneseed bootstrap
316
+ // --no-setpu` run the FULL bootstrap — an update and a setup wizard — while the user
317
+ // believes they asked for neither. The same rule as the named positionals above.
318
+ if (spec.variadic !== undefined) {
319
+ const flagLike = rest.filter((t) => t.startsWith('-') && t !== '-');
320
+ if (flagLike.length) return { error: `unrecognized arguments: ${flagLike.join(' ')}` };
321
+ args[spec.variadic] = rest.splice(0, rest.length);
322
+ }
323
+ if (rest.length) return { error: `unrecognized arguments: ${rest.join(' ')}` };
324
+ return { args };
325
+ }
326
+
327
+ async function main(argv) {
328
+ // `geneseed exclude list | head` closes stdout early. Python exits quietly through
329
+ // BrokenPipeError; the Node equivalent is an EPIPE that would otherwise be an unhandled
330
+ // 'error' event and a stack trace.
331
+ process.stdout.on('error', (e) => { if (e.code === 'EPIPE') process.exit(0); });
332
+
333
+ const verb = argv[0];
334
+ if (!verb || verb === '-h' || verb === '--help') {
335
+ return die(2, `the following arguments are required: cmd (one of ${
336
+ Object.keys(VERBS).join(', ')})`);
337
+ }
338
+ // `validate` — the generator's `--validate-only`, and the one verb this entry answers that is NOT
339
+ // in the table above. It is dispatched here, before the lookup, for a reason that is a
340
+ // property of the gates rather than a preference:
341
+ //
342
+ // * `VERBS` is asserted to be the twin of `rituals/harness.py`'s SUBPARSERS
343
+ // (`test_every_entry_verb_is_a_real_harness_subcommand`), and `--validate-only` is a
344
+ // GENERATOR flag — `harness.py` has never had a `validate` subcommand to be a twin of;
345
+ // * `test_the_matrix_covers_every_verb_it_claims` demands a recorded cell group per table
346
+ // row, and a cell has an `lf` half this host cannot produce.
347
+ //
348
+ // So a row would have meant amending two gates and recording a corpus half blind. It takes
349
+ // the generator's own parser (see `parseDriverArgs`) because its flags are the generator's,
350
+ // and it is on THIS binary — not on `bin/geneseed.mjs` — because it runs the doctor, which
351
+ // starts a process the driver is banned from reaching.
352
+ if (verb === 'validate') {
353
+ try {
354
+ return cmdValidate(parseDriverArgs(argv.slice(1)));
355
+ } catch (e) {
356
+ if (e && e.exitCode !== undefined) return e.exitCode;
357
+ throw e;
358
+ }
359
+ }
360
+ const spec = VERBS[verb];
361
+ if (!spec) {
362
+ // "and every other harness subcommand is still Python — run the interpreter against the
363
+ // old harness script" is GONE, and its removal is not cosmetic: with the four hook verbs
364
+ // named, the two entry points now cover every subcommand there is, so the clause was not
365
+ // just about to become false — it was already claiming a third place for verbs that do
366
+ // not exist.
367
+ return die(2, `invalid choice: '${verb}'. This entry point carries only `
368
+ + `${Object.keys(VERBS).join(', ')}; the four hook verbs live in `
369
+ + 'bin/geneseed-hook.mjs.');
370
+ }
371
+ // `js/cli-table.json`, the CLI as data. A verb this table dispatches and the file does not
372
+ // describe is a REFUSAL and not a parse with no rules: an empty spec would accept every
373
+ // token and bind none of them, so `geneseed uninstall --target X` would run against the
374
+ // default target while the operator believes they named one.
375
+ const argSpec = cliSpec(verb);
376
+ if (argSpec === null) {
377
+ return die(2, `js/cli-table.json describes no subcommand '${verb}' — this install's CLI `
378
+ + 'table is missing or damaged. Reinstall with `npm i -g geneseed@latest`, or restore '
379
+ + 'the file from a checkout.');
380
+ }
381
+ // BEFORE `parse`, because `-h`/`--help` is not in any subcommand's spec and never can be:
382
+ // argparse owned it at the parser, not in the argument table the walk produced — so `parse`
383
+ // answers `unrecognized arguments: --help`, which is what this entry did on all 26 verbs
384
+ // until Task 10 compared the two implementations' help for the first time. AFTER the spec
385
+ // lookup, so a damaged table still refuses with the sentence that names the fix rather
386
+ // than printing half a help text. Anything else on the line is ignored, which is argparse's
387
+ // own rule: `harness diff --nope --help` prints the help.
388
+ if (argv.slice(1).some((t) => t === '-h' || t === '--help')) return printHelp('geneseed', verb);
389
+ const parsed = parse(argSpec, argv.slice(1));
390
+ if (parsed.error) return die(2, parsed.error);
391
+ try {
392
+ // `await`, since P6h. `cmdWeb` is the first verb whose body is asynchronous — Node has
393
+ // no synchronous HTTP client, so `_probe` and `_post_shutdown` cannot be — and without
394
+ // it `process.exitCode` would be assigned a PENDING PROMISE, which Node coerces to 0
395
+ // and prints nothing about. `web status` would then report "not running" and exit 0.
396
+ // Awaiting a number is a no-op for the ten synchronous verbs.
397
+ return await spec.fn(parsed.args);
398
+ } catch (e) {
399
+ // `e.exitCode` is the generator's existing marker for a DELIBERATE refusal that has
400
+ // already explained itself on stderr — `js/emit.mjs:1289` reads the same flag, and it is
401
+ // `sys.exit(<message>)` on the Python side. Anything without it is a crash and keeps its
402
+ // stack: Python prints a traceback for an unhandled exception, and turning one into a
403
+ // tidy one-liner here would hide a bug behind a refusal's clothing.
404
+ //
405
+ // No verb needed this until `status`, which renders — and renders at a theme read out
406
+ // of a marker file rather than off a validated flag.
407
+ if (e && e.exitCode) return e.exitCode;
408
+ throw e;
409
+ }
410
+ }
411
+
412
+ process.exitCode = await main(process.argv.slice(2));