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,148 @@
1
+ // Geneseed — OpenCode ponytail plugin (sustained minimal-code mode).
2
+ //
3
+ // The `ponytail` skill (src/skills/ponytail.md) is the invokable, themed source of
4
+ // truth for the "laziest solution that works" discipline. This plugin is its
5
+ // SUSTAINED counterpart: once you opt in with `/ponytail lite|full|ultra`, it appends
6
+ // a compact ruleset to the system prompt EVERY turn so the agent does not drift back
7
+ // to over-building mid-session, and persists the level across turns. Mirrors the
8
+ // upstream ponytail OpenCode plugin (DietrichGebert/ponytail) but is self-contained —
9
+ // no shared `hooks/` module to require, no external state convention.
10
+ //
11
+ // OPT-IN by default (Geneseed treats ponytail as a skill, not an always-on Law): the
12
+ // mode starts at `off` and nothing is injected until you switch it on. Set
13
+ // GENESEED_PONYTAIL=lite|full|ultra to make a level the default for new installs, or
14
+ // GENESEED_PONYTAIL=off (the default) to keep it dormant until asked.
15
+ //
16
+ // Two hooks, mirroring the upstream design:
17
+ // - experimental.chat.system.transform — append the ruleset at the active level.
18
+ // - command.execute.before — intercept `/ponytail <level>` and persist
19
+ // it; the switch applies from the NEXT turn (the transform reads what the command
20
+ // wrote). Levels: lite | full | ultra | off.
21
+ //
22
+ // State lives beside OpenCode's config as `.geneseed-ponytail` (one word: the level),
23
+ // so it survives restarts. Quiet by default; GENESEED_DEBUG=1 logs switches. Every
24
+ // failure is swallowed — it never blocks a session. Experimental hooks; on a build
25
+ // that lacks `chat.system.transform` the plugin simply never injects (the skill still
26
+ // covers the invokable path).
27
+
28
+ import fs from "node:fs"
29
+ import os from "node:os"
30
+ import path from "node:path"
31
+
32
+ const MODES = new Set(["lite", "full", "ultra", "off"])
33
+ const DEBUG = !!process.env.GENESEED_DEBUG
34
+ function log(msg) { if (DEBUG) console.error(`[geneseed-ponytail] ${msg}`) }
35
+
36
+ // --- pure helpers (exported for tests) ---------------------------------------
37
+
38
+ // Normalise a raw mode string to a known level, or null if unrecognised. Tolerant of
39
+ // surrounding whitespace and case; "normal"/"stop"/"none" are spelled as `off`.
40
+ function normalizeMode(raw) {
41
+ const s = String(raw ?? "").trim().toLowerCase()
42
+ if (!s) return null
43
+ if (s === "normal" || s === "stop" || s === "none" || s === "disable") return "off"
44
+ return MODES.has(s) ? s : null
45
+ }
46
+
47
+ // The default level when no switch has been made yet — $GENESEED_PONYTAIL, else `off`.
48
+ // Opt-in: Geneseed keeps ponytail dormant until the user asks for it.
49
+ function defaultMode() {
50
+ return normalizeMode(process.env.GENESEED_PONYTAIL) || "off"
51
+ }
52
+
53
+ // The compact ruleset appended to the system prompt for a non-off level. Kept lean on
54
+ // purpose — it rides every turn, so ponytail eats its own dog food. null when off.
55
+ function ponytailInstructions(mode) {
56
+ if (mode === "off" || !MODES.has(mode)) return null
57
+ const ladder =
58
+ "Before writing code, climb the ladder and stop at the first rung that holds: " +
59
+ "(1) does this need to exist at all? speculative need → skip it, say so in one line; " +
60
+ "(2) stdlib does it? use it; (3) native platform feature covers it? prefer it; " +
61
+ "(4) an already-installed dependency solves it? use it, never add a new one for what a few lines do; " +
62
+ "(5) can it be one line? one line; (6) only then the minimum code that works."
63
+ const rules =
64
+ "No abstraction with one implementation, no scaffolding \"for later\", deletion over addition, " +
65
+ "fewest files, shortest working diff. Mark deliberate shortcuts with a `ponytail:` comment naming " +
66
+ "the ceiling and upgrade path. Non-trivial logic leaves ONE runnable check. Never simplify away " +
67
+ "input validation at trust boundaries, error handling that prevents data loss, security, " +
68
+ "accessibility basics, hardware calibration, or anything explicitly requested. Output code first, " +
69
+ "then at most three short lines: skipped X, add when Y."
70
+ const level = {
71
+ lite: "LITE: build what's asked, but name the lazier alternative in one line and let the user pick.",
72
+ full: "FULL: enforce the ladder. Stdlib and native first, shortest diff, shortest explanation.",
73
+ ultra: "ULTRA: YAGNI extremist. Ship the one-liner and challenge the rest of the requirement in the same breath.",
74
+ }[mode]
75
+ return `PONYTAIL MODE — write the laziest solution that works (lazy = efficient, not careless).\n${level}\n${ladder}\n${rules}`
76
+ }
77
+
78
+ // --- state (flag file beside the OpenCode config) ----------------------------
79
+
80
+ function statePath() {
81
+ // Same precedence as build._opencode_config_dir() and activity's configBase()
82
+ // ($OPENCODE_CONFIG_DIR > $XDG_CONFIG_HOME/opencode > ~/.config/opencode) — an
83
+ // XDG-only check diverges whenever $OPENCODE_CONFIG_DIR is set, so the /ponytail
84
+ // toggle would land in a different dir than the console reads.
85
+ const cfg =
86
+ process.env.OPENCODE_CONFIG_DIR ||
87
+ path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "opencode")
88
+ return path.join(cfg, ".geneseed-ponytail")
89
+ }
90
+
91
+ function readMode() {
92
+ try {
93
+ return normalizeMode(fs.readFileSync(statePath(), "utf8")) || defaultMode()
94
+ } catch {
95
+ return defaultMode()
96
+ }
97
+ }
98
+
99
+ function writeMode(mode) {
100
+ try {
101
+ const p = statePath()
102
+ fs.mkdirSync(path.dirname(p), { recursive: true })
103
+ fs.writeFileSync(p, mode)
104
+ return true
105
+ } catch (err) {
106
+ log(`could not persist mode: ${err?.message ?? err}`)
107
+ return false
108
+ }
109
+ }
110
+
111
+ // --- plugin ------------------------------------------------------------------
112
+
113
+ export const GeneseedPonytail = async () => ({
114
+ // Append the ruleset to the system prompt every turn at the active level.
115
+ "experimental.chat.system.transform": async (_input, output) => {
116
+ try {
117
+ const mode = readMode()
118
+ const text = ponytailInstructions(mode)
119
+ if (!text) return
120
+ if (Array.isArray(output?.system)) output.system.push(text)
121
+ } catch (err) {
122
+ log(`transform error: ${err?.message ?? err}`)
123
+ }
124
+ },
125
+
126
+ // Persist `/ponytail <level>`; the next turn's injection follows it. A bare
127
+ // `/ponytail` with no argument means `full` (the conventional on switch).
128
+ "command.execute.before": async (input) => {
129
+ try {
130
+ if (!input || input.command !== "ponytail") return
131
+ const arg = (input.arguments || "").trim()
132
+ const mode = normalizeMode(arg) || (arg ? null : "full")
133
+ if (!mode) { log(`ignored unknown level: ${arg}`); return }
134
+ writeMode(mode)
135
+ log(`ponytail ${mode}`)
136
+ } catch (err) {
137
+ log(`command error: ${err?.message ?? err}`)
138
+ }
139
+ },
140
+ })
141
+
142
+ // ponytail: OpenCode treats every export in a plugin file as a plugin and rejects
143
+ // any that isn't a function — a bare helper export crashes startup ("null is not an
144
+ // object") or logs "not a function". Hang the test helpers off the factory instead;
145
+ // they ride along for `import`, invisible to the loader. Upgrade path: none needed.
146
+ Object.assign(GeneseedPonytail, { normalizeMode, defaultMode, ponytailInstructions })
147
+
148
+ export default GeneseedPonytail
@@ -0,0 +1,183 @@
1
+ // Geneseed — OpenCode workflow plugin.
2
+ //
3
+ // Registers ONE custom tool, `workflow`, that runs saved, code-driven orchestration
4
+ // scripts from the sibling `workflows/` dir (`.opencode/workflows/` in a per-repo
5
+ // emit, `<config>/workflows/` global). The model calls `workflow({ name, args })`;
6
+ // the named script — not the model — drives the control flow, spawning subagents via
7
+ // the runtime (`_runtime.js`). This is the deterministic counterpart to the
8
+ // model-driven `council` / `parallel-agents` skills. See
9
+ // adapters/opencode/workflows/_runtime.js and
10
+ // docs/specs/2026-06-09-opencode-workflow-primitive.md.
11
+ //
12
+ // Saved workflows ONLY — no model-authored/eval'd scripts (v1). Synchronous: the tool
13
+ // blocks until the workflow finishes; a phase-by-phase trace and the full result are
14
+ // written to `.geneseed/workflow-runs/<runId>.log`. Every failure is contained and
15
+ // reported, never thrown into the session.
16
+ //
17
+ // Quiet by default; GENESEED_DEBUG=1 logs to stderr. GENESEED_WORKFLOWS_DIR overrides
18
+ // where saved workflows are loaded from.
19
+
20
+ import { promises as fs } from "node:fs"
21
+ import * as path from "node:path"
22
+ import { fileURLToPath, pathToFileURL } from "node:url"
23
+
24
+ const DEBUG = !!process.env.GENESEED_DEBUG
25
+ function dlog(m) { if (DEBUG) console.error(`[geneseed-workflow] ${m}`) }
26
+
27
+ const __dir = path.dirname(fileURLToPath(import.meta.url))
28
+ const WORKFLOWS_DIR = process.env.GENESEED_WORKFLOWS_DIR
29
+ ? path.resolve(process.env.GENESEED_WORKFLOWS_DIR)
30
+ : path.resolve(__dir, "..", "workflows")
31
+
32
+ const NAME_RE = /^[a-z0-9][a-z0-9_-]*$/i
33
+
34
+ async function listWorkflows() {
35
+ try {
36
+ const entries = await fs.readdir(WORKFLOWS_DIR)
37
+ return entries
38
+ .filter((f) => f.endsWith(".js") && !f.startsWith("_"))
39
+ .map((f) => f.slice(0, -3))
40
+ .sort()
41
+ } catch { return [] }
42
+ }
43
+
44
+ async function loadWorkflow(name) {
45
+ if (!NAME_RE.test(name)) throw new Error(`invalid workflow name: ${name}`)
46
+ const file = path.join(WORKFLOWS_DIR, `${name}.js`)
47
+ const rel = path.relative(WORKFLOWS_DIR, file)
48
+ if (rel.startsWith("..") || path.isAbsolute(rel)) throw new Error("workflow path escapes workflows dir")
49
+ await fs.access(file)
50
+ // Cache-bust: Node's ESM cache holds the first import for the process lifetime,
51
+ // so an edited workflow script would otherwise run stale until OpenCode restarts.
52
+ const mod = await import(pathToFileURL(file).href + "?t=" + Date.now())
53
+ const run = mod.default || mod.run
54
+ if (typeof run !== "function") throw new Error(`workflow "${name}" has no default/run export`)
55
+ return { meta: mod.meta || { name }, run }
56
+ }
57
+
58
+ function nowStamp() {
59
+ // Plugin-side only (NOT inside a workflow script) — a run id needs wall-clock; the
60
+ // runtime itself stays clock-free for reproducibility.
61
+ const d = new Date()
62
+ const p = (n) => String(n).padStart(2, "0")
63
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`
64
+ }
65
+
66
+ function safeJson(v) {
67
+ try { return JSON.stringify(v, null, 2) } catch { return String(v) }
68
+ }
69
+
70
+ function summarize(name, result, stats, tracePath) {
71
+ const head = `workflow "${name}" complete — ${stats.agentCount} agent run(s)` +
72
+ (stats.spent ? `, ~${stats.spent} output tokens` : "") +
73
+ (stats.phases.length ? `, phases: ${stats.phases.join(" → ")}` : "")
74
+ let body = safeJson(result)
75
+ if (body.length > 2000) body = body.slice(0, 2000) + `\n… (truncated — full result in ${tracePath})`
76
+ return `${head}\n\nResult:\n${body}\n\nTrace: ${tracePath}`
77
+ }
78
+
79
+ async function writeTrace(rootDir, runId, lines) {
80
+ if (!rootDir) return "(no trace file — unknown working dir)"
81
+ try {
82
+ const dir = path.join(rootDir, ".geneseed", "workflow-runs")
83
+ await fs.mkdir(dir, { recursive: true })
84
+ const file = path.join(dir, `${runId}.log`)
85
+ await fs.writeFile(file, lines.join("\n") + "\n", "utf8")
86
+ return file
87
+ } catch (e) { dlog(`trace write failed: ${e?.message || e}`); return "(trace write failed)" }
88
+ }
89
+
90
+ export const GeneseedWorkflow = async (ctx) => {
91
+ const { client, directory, worktree } = ctx
92
+ const rootDir = worktree || directory
93
+ let toolHelper
94
+ try { ({ tool: toolHelper } = await import("@opencode-ai/plugin")) }
95
+ catch { dlog("@opencode-ai/plugin tool helper unavailable — registering a raw tool def") }
96
+
97
+ // One-line load banner (always on) so a restart visibly confirms the plugin loaded
98
+ // and the `workflow` tool is registered — and WARNS if this OpenCode build lacks the
99
+ // custom-tool helper (the silent "tool never appears" failure).
100
+ try {
101
+ const names = await listWorkflows()
102
+ const warn = toolHelper ? ""
103
+ : " — WARN: @opencode-ai/plugin 'tool' helper missing; the workflow tool may NOT register on this OpenCode version"
104
+ console.error(`[geneseed-workflow] loaded — tool 'workflow' registered, ${names.length} workflow(s) [${names.join(", ") || "none"}] from ${WORKFLOWS_DIR}${warn}`)
105
+ } catch (e) { console.error(`[geneseed-workflow] loaded but could not list workflows: ${e?.message || e}`) }
106
+
107
+ // Normalize the `args` parameter. OpenCode's tool layer can deliver a structured
108
+ // object param as a JSON STRING (or drop it) when the schema is untyped — so coerce
109
+ // a string back to an object and reject non-object shapes to `{}`. Without this, the
110
+ // workflow script's `args` arrives empty and every `args.foo` read is undefined.
111
+ const normalizeArgs = (raw) => {
112
+ let a = raw ?? {}
113
+ if (typeof a === "string") {
114
+ try { a = JSON.parse(a) } catch { a = {} }
115
+ }
116
+ if (a == null || typeof a !== "object" || Array.isArray(a)) a = {}
117
+ return a
118
+ }
119
+
120
+ const execute = async (argv) => {
121
+ const name = (argv?.name || "").trim()
122
+ const available = await listWorkflows()
123
+ if (!name) {
124
+ return `Available workflows: ${available.join(", ") || "(none found in " + WORKFLOWS_DIR + ")"}.\n` +
125
+ `Run one with workflow({ name, args }).`
126
+ }
127
+ if (!available.includes(name)) {
128
+ return `Unknown workflow "${name}". Available: ${available.join(", ") || "(none)"}.`
129
+ }
130
+
131
+ let wf
132
+ try { wf = await loadWorkflow(name) }
133
+ catch (e) { return `Failed to load workflow "${name}": ${e?.message || e}` }
134
+
135
+ const runId = `${name}-${nowStamp()}`
136
+ const lines = []
137
+ const sink = (line) => lines.push(line)
138
+ sink(`# workflow ${name} — ${wf.meta?.description || ""}`)
139
+
140
+ // Lazily import the runtime so a broken runtime never blocks tool registration.
141
+ let createRuntime
142
+ try { ({ createRuntime } = await import(pathToFileURL(path.join(WORKFLOWS_DIR, "_runtime.js")).href + "?t=" + Date.now())) }
143
+ catch (e) { return `Workflow runtime unavailable: ${e?.message || e}` }
144
+
145
+ const rt = createRuntime({ client, directory, worktree, args: normalizeArgs(argv?.args), log: sink })
146
+ let result, failed = null
147
+ try { result = await wf.run(rt) }
148
+ catch (e) { failed = e; sink(`✗ workflow error: ${e?.stack || e?.message || e}`) }
149
+
150
+ const stats = rt._stats()
151
+ sink(`\n# done — ${stats.agentCount} agent run(s), ~${stats.spent} output tokens`)
152
+ if (!failed) sink(`# result:\n${safeJson(result)}`)
153
+ const tracePath = await writeTrace(rootDir, runId, lines)
154
+
155
+ if (failed) return `Workflow "${name}" failed: ${failed?.message || failed}\nTrace: ${tracePath}`
156
+ return summarize(name, result, stats, tracePath)
157
+ }
158
+
159
+ const def = {
160
+ description:
161
+ "Run a saved, code-driven Geneseed workflow that orchestrates subagents " +
162
+ "deterministically (parallel fan-out, find→verify pipeline, phased handoff). " +
163
+ "Call with no name to list available workflows.",
164
+ args: toolHelper
165
+ ? {
166
+ name: toolHelper.schema.string().optional().describe("saved workflow name; omit to list available"),
167
+ // Keep this `any()` — it's the only param shape known to register cleanly on
168
+ // this OpenCode build (record()/object schemas wedged tool registration → every
169
+ // session message stuck "queued"). The provider may still deliver the object as
170
+ // a JSON string or empty; normalizeArgs() in execute() recovers both cases.
171
+ args: toolHelper.schema.any().optional().describe("inputs passed to the workflow script as `args` (object, or a JSON object string)"),
172
+ }
173
+ : {
174
+ name: { type: "string", description: "saved workflow name; omit to list available" },
175
+ args: { type: "object", description: "inputs passed to the workflow script as `args`" },
176
+ },
177
+ async execute(argv) { return execute(argv) },
178
+ }
179
+
180
+ return { tool: { workflow: toolHelper ? toolHelper(def) : def } }
181
+ }
182
+
183
+ export default GeneseedWorkflow
@@ -0,0 +1,277 @@
1
+ // Geneseed — deterministic workflow runtime (OpenCode).
2
+ //
3
+ // The API handed to every saved workflow script: agent(), parallel(), pipeline(),
4
+ // phase(), log(), budget, args. It is code-driven orchestration — the script, not
5
+ // the model, decides the control flow. Child work runs as real OpenCode sessions
6
+ // spawned through the plugin's SDK `client`; orchestration logic here is pure and
7
+ // unit-testable against a mock client (see tests/workflow_runtime.test.mjs).
8
+ //
9
+ // SDK binding note: OpenCode's session API shape shifts between versions, so the
10
+ // child-session helpers below are defensive (envelope/`data` unwrap, multiple parts
11
+ // shapes, graceful degradation). The orchestration primitives do not depend on it.
12
+ //
13
+ // Determinism: this runtime uses no Date.now()/Math.random(), so a run is a pure
14
+ // function of (workflow, args, child replies). Saved scripts SHOULD likewise avoid
15
+ // wall-clock/random for reproducibility (pass timestamps via args). This is an
16
+ // authoring convention — imported modules cannot have their globals shadowed — not a
17
+ // hard sandbox guard. See docs/specs/2026-06-09-opencode-workflow-primitive.md.
18
+
19
+ import * as os from "node:os"
20
+
21
+ const DEBUG = !!process.env.GENESEED_DEBUG
22
+ function dlog(m) { if (DEBUG) console.error(`[geneseed-workflow] ${m}`) }
23
+
24
+ export const MAX_AGENTS = 1000
25
+ export const MAX_ITEMS = 4096
26
+
27
+ export function concurrencyCap() {
28
+ let cpu = 4
29
+ try { cpu = (os.cpus() || []).length || 4 } catch {}
30
+ return Math.max(1, Math.min(16, cpu - 2))
31
+ }
32
+
33
+ // A tiny promise-pool limiter: at most `max` in flight; the rest queue.
34
+ export function createLimiter(max) {
35
+ let active = 0
36
+ const queue = []
37
+ const pump = () => {
38
+ if (active >= max || queue.length === 0) return
39
+ active++
40
+ const { fn, resolve, reject } = queue.shift()
41
+ Promise.resolve().then(fn).then(
42
+ (v) => { active--; resolve(v); pump() },
43
+ (e) => { active--; reject(e); pump() },
44
+ )
45
+ }
46
+ return (fn) => new Promise((resolve, reject) => { queue.push({ fn, resolve, reject }); pump() })
47
+ }
48
+
49
+ // ---- structured-output validation -------------------------------------------
50
+ // Accepts a Zod schema (.safeParse), a predicate fn (value) => true | "error", or a
51
+ // tiny JSON-schema-like descriptor ({ type, required, properties }).
52
+ export function validateSchema(schema, value) {
53
+ if (!schema) return { ok: true, value }
54
+ if (typeof schema === "function") {
55
+ const r = schema(value)
56
+ if (r === true) return { ok: true, value }
57
+ return { ok: false, error: typeof r === "string" ? r : "predicate rejected value" }
58
+ }
59
+ if (typeof schema.safeParse === "function") {
60
+ const r = schema.safeParse(value)
61
+ return r.success
62
+ ? { ok: true, value: r.data }
63
+ : { ok: false, error: r.error?.message || "schema mismatch" }
64
+ }
65
+ const t = schema.type
66
+ if (t === "array") return Array.isArray(value) ? { ok: true, value } : { ok: false, error: "expected array" }
67
+ if (t === "object" || schema.properties || schema.required) {
68
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return { ok: false, error: "expected object" }
69
+ for (const k of (schema.required || [])) if (!(k in value)) return { ok: false, error: `missing key: ${k}` }
70
+ return { ok: true, value }
71
+ }
72
+ if (t && typeof value !== t) return { ok: false, error: `expected ${t}` }
73
+ return { ok: true, value }
74
+ }
75
+
76
+ // First balanced {...} or [...] span in a string (string-literal aware).
77
+ function sliceBalanced(text) {
78
+ const start = (() => {
79
+ const a = text.indexOf("{"), b = text.indexOf("[")
80
+ if (a < 0) return b
81
+ if (b < 0) return a
82
+ return Math.min(a, b)
83
+ })()
84
+ if (start < 0) return null
85
+ const open = text[start], close = open === "{" ? "}" : "]"
86
+ let depth = 0, inStr = false, esc = false
87
+ for (let i = start; i < text.length; i++) {
88
+ const c = text[i]
89
+ if (inStr) {
90
+ if (esc) esc = false
91
+ else if (c === "\\") esc = true
92
+ else if (c === '"') inStr = false
93
+ } else if (c === '"') inStr = true
94
+ else if (c === open) depth++
95
+ else if (c === close) { depth--; if (depth === 0) return text.slice(start, i + 1) }
96
+ }
97
+ return null
98
+ }
99
+
100
+ export function extractJson(text) {
101
+ if (typeof text !== "string") return undefined
102
+ const candidates = []
103
+ const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/i)
104
+ if (fence) candidates.push(fence[1].trim())
105
+ candidates.push(text.trim())
106
+ const braced = sliceBalanced(text)
107
+ if (braced) candidates.push(braced)
108
+ for (const c of candidates) {
109
+ try { return JSON.parse(c) } catch { /* try next */ }
110
+ }
111
+ return undefined
112
+ }
113
+
114
+ // ---- defensive SDK helpers --------------------------------------------------
115
+ function unwrap(res) {
116
+ if (res && typeof res === "object" && "data" in res && res.data &&
117
+ res.id === undefined && res.parts === undefined) return res.data
118
+ return res
119
+ }
120
+ function sessionId(created) {
121
+ const c = unwrap(created)
122
+ return c?.id ?? c?.sessionID ?? c?.session?.id
123
+ }
124
+ function partsText(parts) {
125
+ if (!Array.isArray(parts)) return ""
126
+ return parts
127
+ .filter((p) => p && (p.type === "text" || typeof p.text === "string"))
128
+ .map((p) => p.text || "")
129
+ .join("\n")
130
+ .trim()
131
+ }
132
+ async function lastAssistantText(client, sid) {
133
+ try {
134
+ const res = await client.session.messages({ path: { id: sid } })
135
+ const u = unwrap(res)
136
+ const list = Array.isArray(res) ? res : (Array.isArray(u) ? u : (u?.data ?? []))
137
+ for (let i = list.length - 1; i >= 0; i--) {
138
+ const m = list[i]
139
+ const role = m?.info?.role ?? m?.role
140
+ if (role && role !== "assistant") continue
141
+ const t = partsText(m?.parts ?? m?.info?.parts ?? [])
142
+ if (t) return t
143
+ }
144
+ } catch (e) { dlog(`messages read failed: ${e?.message || e}`) }
145
+ return ""
146
+ }
147
+ function usageFrom(res) {
148
+ const r = unwrap(res)
149
+ const tok = r?.tokens ?? r?.info?.tokens ?? r?.assistant?.tokens
150
+ return { input: Number(tok?.input ?? 0) || 0, output: Number(tok?.output ?? 0) || 0 }
151
+ }
152
+
153
+ async function runChildSession(client, { prompt, agent, model, title }) {
154
+ if (!client?.session?.create || !client?.session?.prompt) {
155
+ throw new Error("OpenCode SDK session API unavailable")
156
+ }
157
+ let created
158
+ try { created = await client.session.create({ body: { title: title || "geneseed-workflow" } }) }
159
+ catch { created = await client.session.create() }
160
+ const sid = sessionId(created)
161
+ if (!sid) throw new Error("could not create child session")
162
+ try {
163
+ const body = { parts: [{ type: "text", text: prompt }] }
164
+ if (agent) body.agent = agent
165
+ if (model) body.model = model
166
+ let res = null
167
+ try { res = await client.session.prompt({ path: { id: sid }, body }) }
168
+ catch (e) { dlog(`prompt failed: ${e?.message || e}`) }
169
+ const r = unwrap(res)
170
+ let text = partsText(r?.parts ?? r?.info?.parts ?? [])
171
+ if (!text) text = await lastAssistantText(client, sid)
172
+ return { text, usage: usageFrom(res) }
173
+ } finally {
174
+ try { await client.session.delete?.({ path: { id: sid } }) } catch { /* best effort */ }
175
+ }
176
+ }
177
+
178
+ // ---- the runtime ------------------------------------------------------------
179
+ export function createRuntime(ctx) {
180
+ const { client, directory, worktree, log: sink } = ctx
181
+ const args = ctx.args || {}
182
+ const cap = ctx.concurrency || concurrencyCap()
183
+ const limit = createLimiter(cap)
184
+ let agentCount = 0
185
+ let spent = 0
186
+ const budgetTotal =
187
+ (args && typeof args.budget === "number") ? args.budget
188
+ : (typeof ctx.budget === "number" ? ctx.budget : null)
189
+ const phases = []
190
+ const trace = []
191
+ const emit = (line) => { trace.push(line); if (typeof sink === "function") sink(line) }
192
+
193
+ // Budget meters OUTPUT tokens only (deliberate, per the v1 spec): output is what
194
+ // the workflow author controls via prompt design; input varies with repo size and
195
+ // would make the same script exhaust wildly different budgets across machines.
196
+ const budget = {
197
+ get total() { return budgetTotal },
198
+ spent: () => spent,
199
+ remaining: () => (budgetTotal == null ? Infinity : Math.max(0, budgetTotal - spent)),
200
+ }
201
+
202
+ async function spawn(prompt, opts) {
203
+ return limit(() => runChildSession(client, {
204
+ prompt, agent: opts.agent, model: opts.model, title: `geneseed-wf:${opts.label}`,
205
+ }))
206
+ }
207
+
208
+ async function agent(prompt, opts = {}) {
209
+ if (agentCount >= MAX_AGENTS) throw new Error(`workflow agent cap (${MAX_AGENTS}) exceeded`)
210
+ if (budgetTotal != null && spent >= budgetTotal) {
211
+ throw new Error(`workflow token budget (${budgetTotal}) exhausted`)
212
+ }
213
+ agentCount++
214
+ const label = opts.label || String(prompt).slice(0, 40)
215
+ emit(`· agent: ${label}`)
216
+ let out
217
+ try { out = await spawn(prompt, { ...opts, label }) }
218
+ catch (e) { emit(`✗ agent failed: ${label} — ${e?.message || e}`); return null }
219
+ spent += out.usage?.output || 0
220
+ if (!opts.schema) return out.text
221
+
222
+ let lastErr = "no JSON found in reply"
223
+ for (let attempt = 0; attempt <= 2; attempt++) {
224
+ const parsed = extractJson(out.text)
225
+ if (parsed !== undefined) {
226
+ const v = validateSchema(opts.schema, parsed)
227
+ if (v.ok) return v.value
228
+ lastErr = v.error
229
+ }
230
+ if (attempt === 2) break
231
+ emit(`↻ retry ${label} (schema): ${lastErr}`)
232
+ try {
233
+ out = await spawn(
234
+ `${prompt}\n\nYour previous reply did not satisfy the required schema (${lastErr}). ` +
235
+ `Reply with ONLY a single valid JSON value — no prose, no code fence.`,
236
+ { ...opts, label: `${label}:retry${attempt + 1}` },
237
+ )
238
+ spent += out.usage?.output || 0
239
+ } catch (e) { lastErr = String(e?.message || e); break }
240
+ }
241
+ emit(`✗ schema unmet: ${label} — ${lastErr}`)
242
+ return null
243
+ }
244
+
245
+ function phase(title) { phases.push(title); emit(`\n=== phase: ${title} ===`) }
246
+ function log(msg) { emit(` ${msg}`) }
247
+
248
+ // BARRIER fan-out: awaits all thunks. A throwing thunk resolves to null.
249
+ async function parallel(thunks) {
250
+ if (!Array.isArray(thunks)) throw new Error("parallel() needs an array of thunks")
251
+ if (thunks.length > MAX_ITEMS) throw new Error(`parallel() item cap (${MAX_ITEMS}) exceeded`)
252
+ return Promise.all(thunks.map((fn) =>
253
+ Promise.resolve().then(fn).catch((e) => { emit(`✗ parallel unit: ${e?.message || e}`); return null })))
254
+ }
255
+
256
+ // NO-BARRIER staged flow: each item runs all stages independently. A throwing
257
+ // stage drops that item to null and skips its remaining stages.
258
+ async function pipeline(items, ...stages) {
259
+ if (!Array.isArray(items)) throw new Error("pipeline() needs an array of items")
260
+ if (items.length > MAX_ITEMS) throw new Error(`pipeline() item cap (${MAX_ITEMS}) exceeded`)
261
+ return Promise.all(items.map(async (item, i) => {
262
+ let acc = item
263
+ for (let s = 0; s < stages.length; s++) {
264
+ try { acc = await stages[s](acc, item, i) }
265
+ catch (e) { emit(`✗ pipeline item ${i} stage ${s}: ${e?.message || e}`); return null }
266
+ }
267
+ return acc
268
+ }))
269
+ }
270
+
271
+ return {
272
+ agent, parallel, pipeline, phase, log, budget, args, directory, worktree,
273
+ _stats: () => ({ agentCount, spent, cap, phases: [...phases], trace: [...trace] }),
274
+ }
275
+ }
276
+
277
+ export default createRuntime
@@ -0,0 +1,64 @@
1
+ // council — the `council` skill as deterministic code.
2
+ //
3
+ // Seats the standing stance agents, gathers their steelmanned positions in parallel
4
+ // (each in its own isolated context, no groupthink), then a neutral chair synthesises
5
+ // a verdict and preserves the strongest dissent. The model-driven `council` skill asks
6
+ // the agent to convene seats in prose; this runs the same shape on rails.
7
+ //
8
+ // args: { motion?: string, question?: string, seats?: string[] }
9
+ // motion — the decision/change/claim under debate (required).
10
+ // seats — optional subset of seat roles to convene (default: the core six).
11
+
12
+ export const meta = {
13
+ name: "council",
14
+ description: "Council debate as code: stance agents argue in parallel, a chair returns a verdict + preserved dissent.",
15
+ phases: [{ title: "Positions" }, { title: "Synthesis" }],
16
+ }
17
+
18
+ // [role, charter] — role doubles as the OpenCode subagent name (Geneseed emits these).
19
+ const SEATS = [
20
+ ["advocate", "the strongest case FOR the motion — the prize, the cost of inaction"],
21
+ ["skeptic", "devil's advocate — failure modes, hidden assumptions, why NOT"],
22
+ ["pragmatist", "cost, effort, and complexity weighed against the payoff (YAGNI)"],
23
+ ["steward", "long-term architecture, technical debt, and maintainability"],
24
+ ["operator", "running it in production — reliability, on-call, operational cost"],
25
+ ["empiricist", "every claim held to evidence; what would prove or disprove it"],
26
+ ]
27
+
28
+ export default async function run(rt) {
29
+ const { agent, parallel, phase, log, args } = rt
30
+ const motion = args?.motion || args?.question
31
+ if (!motion) return { error: "pass args.motion (the decision, change, or claim to debate)" }
32
+
33
+ const wanted = Array.isArray(args?.seats) && args.seats.length ? args.seats : null
34
+ const seats = wanted ? SEATS.filter(([role]) => wanted.includes(role)) : SEATS
35
+ if (!seats.length) {
36
+ return { error: `no seats matched ${JSON.stringify(wanted)} — valid: ${SEATS.map(([r]) => r).join(", ")}` }
37
+ }
38
+ if (wanted && seats.length < wanted.length) {
39
+ log(`ignored unknown seat name(s): ${wanted.filter((w) => !SEATS.some(([r]) => r === w)).join(", ")}`)
40
+ }
41
+
42
+ phase("Positions")
43
+ const briefs = (await parallel(seats.map(([role, charter]) => () =>
44
+ agent(
45
+ `You are the ${role} on a decision council. Motion: "${motion}".\n` +
46
+ `Argue ONLY this stance: ${charter}. Give a tight, steelmanned brief — ` +
47
+ `no hedging, no strawmen.`,
48
+ { label: `seat:${role}`, phase: "Positions", agent: role },
49
+ ).then((text) => ({ role, text }))))).filter((b) => b && b.text)
50
+
51
+ phase("Synthesis")
52
+ const dossier = briefs.map((b) => `### ${b.role}\n${b.text}`).join("\n\n")
53
+ const verdict = await agent(
54
+ `You are the neutral chair of a decision council. Motion: "${motion}".\n\n` +
55
+ `The seat briefs:\n\n${dossier}\n\n` +
56
+ `Synthesise in a neutral voice: state the verdict and its reasoning, record the ` +
57
+ `single strongest surviving dissent verbatim, list what would change the verdict, ` +
58
+ `and name one next action. The council advises — do not write code or push anything.`,
59
+ { label: "chair", phase: "Synthesis" },
60
+ )
61
+
62
+ log(`debated "${motion}" across ${briefs.length} seat(s)`)
63
+ return { motion, seats: seats.map(([r]) => r), verdict, briefs }
64
+ }