geneseed 2.0.0 → 3.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 (196) hide show
  1. package/CHANGELOG.md +217 -5
  2. package/DESIGN.md +149 -1326
  3. package/QUICKSTART.md +1 -1
  4. package/README.md +30 -22
  5. package/SETUP.md +98 -24
  6. package/SHIPPED.md +6 -5
  7. package/adapters/bob/README.md +3 -3
  8. package/adapters/claude-code/README.md +6 -4
  9. package/adapters/claude-code/skills/geneseed/SKILL.md +10 -6
  10. package/adapters/copilot/README.md +5 -4
  11. package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +11 -6
  12. package/adapters/opencode/HOW-OPENCODE-LOADS.md +1 -1
  13. package/adapters/opencode/README.md +15 -9
  14. package/adapters/opencode/agents/orchestrator.md +1 -1
  15. package/adapters/opencode/opencode.json +0 -9
  16. package/adapters/opencode/plugins/geneseed-context.js +7 -7
  17. package/adapters/opencode/plugins/geneseed-guard.js +6 -6
  18. package/adapters/opencode/workflows/dispatch.js +1 -1
  19. package/bin/geneseed.mjs +74 -8
  20. package/docs/declined.md +53 -2
  21. package/docs/design-history.md +1290 -0
  22. package/docs/extending.md +672 -0
  23. package/docs/limits.md +141 -82
  24. package/docs/token-footprint.md +15 -4
  25. package/docs/web/agents.md +1 -1
  26. package/docs/web/authoring.md +1 -1
  27. package/docs/web/claude-hooks.md +2 -2
  28. package/docs/web/collaboration.md +3 -3
  29. package/docs/web/first-session.md +2 -2
  30. package/docs/web/footprint.md +14 -7
  31. package/docs/web/foreman-mode.md +2 -2
  32. package/docs/web/harnesses.md +5 -3
  33. package/docs/web/install-quick.md +1 -1
  34. package/docs/web/model.md +3 -3
  35. package/docs/web/plugin-context.md +2 -2
  36. package/docs/web/plugin-guard.md +3 -3
  37. package/docs/web/plugin-ponytail.md +1 -1
  38. package/docs/web/rules.md +23 -3
  39. package/docs/web/setup-choices.md +20 -7
  40. package/docs/web/themes-author.md +13 -1
  41. package/docs/web-ui.md +36 -12
  42. package/geneseed +0 -0
  43. package/harness.config.json +1 -1
  44. package/js/catalog.mjs +29 -4
  45. package/js/checkout.mjs +24 -1
  46. package/js/cli-table.json +9 -7
  47. package/js/diff.mjs +8 -3
  48. package/js/doctor.mjs +374 -26
  49. package/js/emit.mjs +48 -11
  50. package/js/generate.mjs +39 -5
  51. package/js/hooks.mjs +8 -7
  52. package/js/installs.mjs +101 -4
  53. package/js/inventory.mjs +150 -20
  54. package/js/lib/fs.mjs +6 -2
  55. package/js/mcp.mjs +3 -15
  56. package/js/migrate.mjs +10 -4
  57. package/js/render.mjs +116 -11
  58. package/js/settings.mjs +297 -28
  59. package/js/setup.mjs +95 -5
  60. package/js/status.mjs +45 -4
  61. package/js/themes.mjs +1 -1
  62. package/js/tui.mjs +46 -3
  63. package/js/uninstall.mjs +30 -11
  64. package/js/web/actions.mjs +76 -9
  65. package/js/web/api.mjs +95 -16
  66. package/js/web/docs.mjs +33 -5
  67. package/js/web/jobs.mjs +12 -3
  68. package/js/web/server.mjs +59 -2
  69. package/package.json +1 -1
  70. package/src/AGENT.md.tmpl +75 -65
  71. package/src/agents/_template.md +4 -4
  72. package/src/agents/advocate.md +4 -4
  73. package/src/agents/architect.md +2 -2
  74. package/src/agents/developer.md +5 -5
  75. package/src/agents/docs.md +2 -2
  76. package/src/agents/empiricist.md +3 -3
  77. package/src/agents/explorer.md +3 -3
  78. package/src/agents/framer.md +3 -3
  79. package/src/agents/historian.md +3 -3
  80. package/src/agents/operator.md +3 -3
  81. package/src/agents/pragmatist.md +3 -3
  82. package/src/agents/reviewer.md +2 -2
  83. package/src/agents/security.md +2 -2
  84. package/src/agents/skeptic.md +4 -4
  85. package/src/agents/steward.md +3 -3
  86. package/src/agents/tester.md +2 -2
  87. package/src/agents/user-advocate.md +3 -3
  88. package/src/agents/visionary.md +3 -3
  89. package/src/doctrines/README.md +41 -0
  90. package/src/doctrines/craft.md +51 -0
  91. package/src/doctrines/ops.md +61 -0
  92. package/src/doctrines/process.md +87 -0
  93. package/src/doctrines/rigor.md +52 -0
  94. package/src/laws/universal.md +17 -301
  95. package/src/memory/README.md +2 -2
  96. package/src/modes/README.md +2 -2
  97. package/src/modes/foreman.md +1 -1
  98. package/src/notebook/README.md +2 -2
  99. package/src/ontology/universal.md +89 -0
  100. package/src/postures/README.md +3 -3
  101. package/src/postures/artisan.md +3 -3
  102. package/src/postures/assistant.md +1 -1
  103. package/src/postures/expert.md +1 -1
  104. package/src/postures/mentor.md +1 -1
  105. package/src/postures/peer.md +2 -2
  106. package/src/skills/_template.md +2 -2
  107. package/src/skills/brainstorm.md +2 -2
  108. package/src/skills/clarify.md +2 -2
  109. package/src/skills/codebase-design.md +1 -1
  110. package/src/skills/commit.md +5 -5
  111. package/src/skills/council.md +4 -4
  112. package/src/skills/crash-course.md +1 -1
  113. package/src/skills/daydream/SKILL.md +1 -1
  114. package/src/skills/debug.md +2 -2
  115. package/src/skills/decode.md +1 -1
  116. package/src/skills/develop.md +6 -6
  117. package/src/skills/document-project.md +4 -4
  118. package/src/skills/domain-modeling.md +1 -1
  119. package/src/skills/drill.md +1 -1
  120. package/src/skills/feynman.md +1 -1
  121. package/src/skills/forge-mcp.md +3 -3
  122. package/src/skills/fresh-eyes.md +4 -4
  123. package/src/skills/frontend-design.md +1 -1
  124. package/src/skills/gap-detector.md +1 -1
  125. package/src/skills/geneseed-code-review.md +1 -1
  126. package/src/skills/geneseed.md +4 -4
  127. package/src/skills/git-archaeology.md +1 -1
  128. package/src/skills/git-rescue.md +3 -3
  129. package/src/skills/handoff.md +4 -4
  130. package/src/skills/herdr.md +1 -1
  131. package/src/skills/ingest.md +3 -3
  132. package/src/skills/learning-path.md +1 -1
  133. package/src/skills/migrate.md +5 -5
  134. package/src/skills/opencode-theme.md +1 -1
  135. package/src/skills/parallel-agents.md +2 -2
  136. package/src/skills/pipeline.md +2 -2
  137. package/src/skills/plan.md +2 -2
  138. package/src/skills/ponytail.md +1 -1
  139. package/src/skills/profile.md +1 -1
  140. package/src/skills/prose.md +2 -2
  141. package/src/skills/prototype.md +2 -2
  142. package/src/skills/refactor.md +2 -2
  143. package/src/skills/release.md +4 -4
  144. package/src/skills/repo-map.md +5 -5
  145. package/src/skills/research.md +6 -6
  146. package/src/skills/review-response.md +3 -3
  147. package/src/skills/roast-me.md +1 -1
  148. package/src/skills/rule.md +5 -5
  149. package/src/skills/ship.md +3 -3
  150. package/src/skills/tdd.md +2 -2
  151. package/src/skills/tickets.md +1 -1
  152. package/src/skills/wayfinder.md +1 -1
  153. package/src/skills/wiki.md +4 -4
  154. package/src/skills/workflow.md +2 -2
  155. package/themes/README.md +17 -7
  156. package/themes/_TEMPLATE.json +37 -33
  157. package/themes/biker.json +37 -33
  158. package/themes/commentator.json +37 -33
  159. package/themes/cyberpunk.json +37 -33
  160. package/themes/gamer.json +37 -33
  161. package/themes/imperial.json +37 -33
  162. package/themes/joker.json +37 -33
  163. package/themes/marvin.json +37 -33
  164. package/themes/mean.json +37 -33
  165. package/themes/military.json +37 -33
  166. package/themes/neutral.json +37 -33
  167. package/themes/pirate.json +37 -33
  168. package/themes/sports.json +37 -33
  169. package/themes/verstappen.json +37 -33
  170. package/themes/wizard.json +37 -33
  171. package/web/dist/assets/{About-DX1-VTBP.js → About-C0Mk0igT.js} +1 -1
  172. package/web/dist/assets/{Activity-Dp9NaXOn.js → Activity-C4aJJVXz.js} +1 -1
  173. package/web/dist/assets/{ActivityDetail-HpOBtrXS.js → ActivityDetail-DaQ85za0.js} +1 -1
  174. package/web/dist/assets/{Diff-Ct4G3Se8.js → Diff-B3tlO2Yj.js} +1 -1
  175. package/web/dist/assets/{Doctor-DGjkZFuA.js → Doctor-BNC-5Dmo.js} +1 -1
  176. package/web/dist/assets/{Graph-b8o-S6kv.js → Graph-us0T6ZcV.js} +1 -1
  177. package/web/dist/assets/Harnesses-DAMgyfM_.js +1 -0
  178. package/web/dist/assets/Laws-rOxegJ75.js +1 -0
  179. package/web/dist/assets/{Library-BEZscyaQ.js → Library-KmwrHA1J.js} +1 -1
  180. package/web/dist/assets/{Markdown-k_LSLBGq.js → Markdown-0eQOVGSF.js} +1 -1
  181. package/web/dist/assets/{Profile-DydukTxP.js → Profile-ZU7WV6Xl.js} +1 -1
  182. package/web/dist/assets/{Rules-CIRG4Ldt.js → Rules-CtDoPhhf.js} +1 -1
  183. package/web/dist/assets/{Skills-Buoi1IBr.js → Skills-BhoOxdkD.js} +1 -1
  184. package/web/dist/assets/{StatusBadge-CE_AJttl.js → StatusBadge-CGvYVCwa.js} +1 -1
  185. package/web/dist/assets/{Themes-CI4o3gJC.js → Themes-Dr7OOkD7.js} +1 -1
  186. package/web/dist/assets/{activity-BCW7e6Cy.js → activity-B8aq3qtl.js} +1 -1
  187. package/web/dist/assets/index-2wqPDxuN.css +1 -0
  188. package/web/dist/assets/{index-B1PIx7RM.js → index-AhMzkdbI.js} +1 -1
  189. package/web/dist/assets/index-CRftEtBX.js +12 -0
  190. package/web/dist/assets/{index-Ck2iCdaC.js → index-D0lPN_qW.js} +1 -1
  191. package/web/dist/index.html +2 -2
  192. package/web/src/pages/Laws.jsx +282 -88
  193. package/web/dist/assets/Harnesses-gr3-aHBF.js +0 -1
  194. package/web/dist/assets/Laws-DTwF5tTC.js +0 -1
  195. package/web/dist/assets/index-BnTQUSUz.js +0 -12
  196. package/web/dist/assets/index-DUCsDv7z.css +0 -1
package/js/web/api.mjs CHANGED
@@ -61,7 +61,7 @@ import { doctorCollect } from '../doctor.mjs';
61
61
  import { excludesSnapshot } from '../excludes.mjs';
62
62
  import { GLOBAL_MANIFEST, HOSTS, opencodeConfigDir, pyResolve } from '../hosts.mjs';
63
63
  import {
64
- footprintOfDir, installState, installTargets, installedDefaults, modeOfDir,
64
+ doctrinesForBuild, footprintOfDir, installState, installTargets, installedDefaults, modeOfDir,
65
65
  postureOfDir, readJsonMaybe, readMaybe, themeOfDir,
66
66
  } from '../installs.mjs';
67
67
  import { frontmatter } from '../hooks.mjs';
@@ -273,16 +273,22 @@ export function specEntries(root, nested) {
273
273
  * `_web_core._deployed_inventory` — the agents and skills actually installed at
274
274
  * `state.target`, not a fresh render of `src/`.
275
275
  *
276
- * Laws still come from the render: once deployed they live inside AGENT.md rather than as
277
- * separate files, so the deployed arm replaces two of the three and not all three. A
278
- * deployed spec file carries neither a category nor a lifecycle status, so each is tagged
279
- * BY NAME from the same two sources the source render uses — and an entity the registry
280
- * does not know reads "personal" for both, which is the honest answer rather than a
281
- * borrowed one: filing it under the `build` fallback would claim a Geneseed taxonomy slot
282
- * it was never given.
276
+ * All three constitutional tiers still come from the render: once deployed they live inside
277
+ * AGENT.md rather than as separate files, so the deployed arm replaces the two ENTITY rosters
278
+ * and none of the constitution. A deployed spec file carries neither a category nor a
279
+ * lifecycle status, so each is tagged BY NAME from the same two sources the source render
280
+ * uses — and an entity the registry does not know reads "personal" for both, which is the
281
+ * honest answer rather than a borrowed one: filing it under the `build` fallback would claim
282
+ * a Geneseed taxonomy slot it was never given.
283
+ *
284
+ * ⚠ THE PACK SELECTION IS THE ONE THING THE RENDER CANNOT KNOW. `renderAll` emits all four
285
+ * pack files whatever `cfg.doctrines` says — measured: `makeCfg({doctrines:['craft']})` yields
286
+ * the same 105 items — because the filtering happens when AGENT.md's §2 is assembled, not in
287
+ * the walk. So the catalogue is always whole, and which packs this INSTALL built in comes from
288
+ * its own carrier via `doctrinesForBuild`, which resolves "no marker" to every pack.
283
289
  */
284
290
  export function deployedInventory(state) {
285
- const render = tuiInventory(state.theme);
291
+ const render = tuiInventory(state.theme, doctrinesForBuild(state.target));
286
292
  const registry = loadRegistry();
287
293
  const skills = specEntries(path.join(state.target, 'skills'), true);
288
294
  for (const e of skills) {
@@ -292,7 +298,12 @@ export function deployedInventory(state) {
292
298
  }
293
299
  const agents = specEntries(path.join(state.target, 'agents'), false);
294
300
  for (const e of agents) e.status = entityStatus(registry, `agents/${e.name}`);
295
- return { agents, skills, laws: render.laws, theme: state.theme };
301
+ return { agents,
302
+ skills,
303
+ laws: render.laws,
304
+ ontology: render.ontology,
305
+ doctrines: render.doctrines,
306
+ theme: state.theme };
296
307
  }
297
308
 
298
309
  /** `WebState.inventory` — the deployed record when there is one, else the source render. */
@@ -592,9 +603,53 @@ function configManifest(name, p) {
592
603
  return null;
593
604
  }
594
605
 
595
- /** `_web_core.SECTIONS` — a closed list; anything else is a 404. */
606
+ /**
607
+ * `_web_core.SECTIONS` — a closed list; anything else is a 404.
608
+ *
609
+ * ⚠ `laws` STAYS ONE SECTION FOR ALL THREE TIERS, and that is a decision rather than an
610
+ * oversight. The console has ONE Constitution entry, not three, so a reader keeps the "read it
611
+ * top to bottom" property; the id stays `laws` because `tests/helpers/cli_golden.mjs`
612
+ * hard-requires `web/src/pages/Laws.jsx` and because a route rename buys nothing and costs a
613
+ * redirect. The tier lives on the ITEM (`tier: 'ontology' | 'invariant' | 'doctrine'`), which
614
+ * is where a consumer can act on it.
615
+ */
596
616
  export const SECTIONS = ['agents', 'skills', 'laws', 'memory', 'notebook', 'wiki', 'config'];
597
617
 
618
+ /**
619
+ * The `laws` section's items — the whole constitution, in constitutional order, one flat list.
620
+ *
621
+ * FLAT AND NOT NESTED, because every other section is `{section, items}` and a second shape
622
+ * for one section is a second client path for one roster. The pack's grouping metadata rides
623
+ * on each doctrine row (`pack`, `packTitle`, `packDesc`, `active`), so a consumer groups by
624
+ * `pack` and has everything a header needs without a second fetch.
625
+ *
626
+ * Names are ADDRESSES and the three shapes cannot collide: `ont:<slug>` carries a colon no
627
+ * numeral has, an invariant is `[IVXLCDM]+`, a doctrine rule is `<pack>.<n>`. `apiItem`
628
+ * resolves all three off the same list.
629
+ */
630
+ function constitutionItems(inv) {
631
+ return [
632
+ ...(inv.ontology ?? []).map((e) => ({ name: `ont:${e.id}`, title: e.title, desc: '',
633
+ tier: 'ontology' })),
634
+ ...inv.laws.map((e) => ({ name: e.num, title: `Rule ${e.num} — ${e.title}`, desc: '',
635
+ klass: e.klass ?? 'craft', tier: 'invariant' })),
636
+ ...(inv.doctrines ?? []).flatMap((p) => p.rules.map((r) => ({
637
+ name: `${r.pack}.${r.n}`,
638
+ title: `Doctrine ${r.pack} ${r.n} — ${r.title}`,
639
+ desc: '',
640
+ klass: r.klass ?? p.pack,
641
+ tier: 'doctrine',
642
+ pack: p.pack,
643
+ packTitle: p.title,
644
+ packDesc: p.desc,
645
+ // ⚠ EVERY RULE IS LISTED WHETHER OR NOT ITS PACK IS BUILT IN. An inactive pack that
646
+ // vanished from the payload would be indistinguishable from one that never shipped, and
647
+ // the console could not then offer the command that turns it back on.
648
+ active: p.active,
649
+ }))),
650
+ ];
651
+ }
652
+
598
653
  /** `_web_catalog.api_catalog`. */
599
654
  export function apiCatalog(state, section) {
600
655
  if (!SECTIONS.includes(section)) throw new NotFound(section);
@@ -607,8 +662,7 @@ export function apiCatalog(state, section) {
607
662
  items = inv.skills.map((e) => ({ name: e.name, title: e.name, desc: e.desc,
608
663
  source: e.source ?? null, klass: e.klass ?? 'build', status: e.status ?? 'unknown' }));
609
664
  } else if (section === 'laws') {
610
- items = inv.laws.map((e) => ({ name: e.num, title: `Rule ${e.num} — ${e.title}`,
611
- desc: '', klass: e.klass ?? 'craft' }));
665
+ items = constitutionItems(inv);
612
666
  } else if (section === 'memory') {
613
667
  items = memoryItems(state);
614
668
  } else if (section === 'notebook') {
@@ -634,10 +688,21 @@ export function apiItem(state, type, name) {
634
688
  return out;
635
689
  }
636
690
  if (type === 'law') {
637
- const e = inv.laws.find((x) => x.num === name);
691
+ // ONE ARM FOR THREE TIERS, resolved off the same list the catalogue publishes — so a name
692
+ // that appears in `/api/catalog/laws` always opens, and one that does not always 404s.
693
+ // Building the list twice is what would let the two drift.
694
+ const e = constitutionItems(inv).find((x) => x.name === name);
638
695
  if (!e) throw new NotFound(name);
639
- return { type, name, title: `Rule ${e.num} — ${e.title}`, desc: '', body: e.body,
640
- links: [], klass: e.klass ?? 'craft' };
696
+ const body = e.tier === 'ontology'
697
+ ? (inv.ontology.find((s) => `ont:${s.id}` === name)?.body ?? '')
698
+ : (e.tier === 'invariant'
699
+ ? (inv.laws.find((x) => x.num === name)?.body ?? '')
700
+ : (inv.doctrines.flatMap((p) => p.rules)
701
+ .find((r) => `${r.pack}.${r.n}` === name)?.body ?? ''));
702
+ // `links: []` — a constitution body is not link-resolved. It cites its siblings by
703
+ // ADDRESS (`Rule IV`, `Doctrine ops 1`, `Ontology: Telos`), not by path, and there is no
704
+ // file behind those to resolve to.
705
+ return { ...e, type, name, body, links: [] };
641
706
  }
642
707
  if (type === 'memory' || type === 'notebook') {
643
708
  flatName(name);
@@ -795,7 +860,21 @@ export function apiOverview(state) {
795
860
  counts: {
796
861
  agents: inv.agents.length,
797
862
  skills: inv.skills.length,
863
+ // ⚠ `laws` STAYS THE INVARIANT COUNT. The rail badge reads it, `docCounts` mirrors it as
864
+ // `{N_LAWS}`, and the doctor's frozen `proseMirrorProblems` compares README and
865
+ // SHIPPED.md prose against the same number. Widening it to "the whole constitution"
866
+ // would silently move all four at once.
798
867
  laws: inv.laws.length,
868
+ ontology: (inv.ontology ?? []).length,
869
+ // The tier's own summary, kept out of the section counts because it is not a section:
870
+ // `active`/`total` is the fraction the dashboard shows, `rules` counts only the packs
871
+ // this install built in — what it is bound by, not what its bundle carries.
872
+ doctrines: {
873
+ active: (inv.doctrines ?? []).filter((p) => p.active).length,
874
+ total: (inv.doctrines ?? []).length,
875
+ rules: (inv.doctrines ?? []).filter((p) => p.active)
876
+ .reduce((n, p) => n + p.rules.length, 0),
877
+ },
799
878
  memory: memoryItems(state).length,
800
879
  notebook: notebookItems(state).length,
801
880
  wiki: wikiItems(state).length,
package/js/web/docs.mjs CHANGED
@@ -130,8 +130,15 @@ export function docGroups() {
130
130
  * the one-line description. A key of `null` marks a term no theme renames.
131
131
  */
132
132
  const GLOSSARY_KEYS = [
133
- ['Rule (Law)', 'LAW', 'the governance rules the agent obeys'],
134
- ['Rules (Laws)', 'LAWS', 'the body of governance rules'],
133
+ // The three tiers, in constitutional order and before the entities — a reader who does not
134
+ // know what an Ontology is here cannot read the rest of the glossary's first rows.
135
+ ['Ontology', 'ONTOLOGY', 'the always-on worldview under the rules — telos, evidence, '
136
+ + 'decisions, conduct; it holds the Pact'],
137
+ ['Rule (Law)', 'LAW', 'one of the nine always-on invariants: what is never done'],
138
+ ['Rules (Laws)', 'LAWS', 'the body of always-on invariants'],
139
+ ['Doctrine', 'DOCTRINE', 'one practice rule, cited `<pack> <n>` — how work is done here'],
140
+ ['Doctrines', 'DOCTRINES', 'the practice packs, chosen at build time; inactive packs still '
141
+ + 'ship in the bundle'],
135
142
  ['Agent', 'AGENT', 'a capability specialist'],
136
143
  ['Agents', 'AGENTS', 'the roster of specialists'],
137
144
  ['Skill', 'SKILL', 'a repeatable workflow'],
@@ -139,7 +146,10 @@ const GLOSSARY_KEYS = [
139
146
  ['Memory', 'MEMORY', 'durable, one-fact-per-file knowledge'],
140
147
  ['Notebook', 'NOTEBOOK', "the agent's sovereign space"],
141
148
  ['Wiki', 'WIKI', 'the machine-wide knowledge base'],
142
- ['Pact', 'PACT', 'the two-way collaboration contract'],
149
+ // Still a token, still themed, but it names a CONCEPT INSIDE the ontology's Telos now
150
+ // rather than a peer of the Rules — `src/postures/peer.md` cites it and the section that
151
+ // used to carry it is the Ontology.
152
+ ['Pact', 'PACT', "the two-way collaboration contract, stated in the Ontology's Telos"],
143
153
  ['Posture', null, 'the relationship register the agent works in '
144
154
  + '(peer, mentor, expert, assistant, artisan)'],
145
155
  ['Mode', null, 'how work gets executed — direct (the agent works every '
@@ -156,8 +166,14 @@ const GLOSSARY_KEYS = [
156
166
 
157
167
  // ---- counts substituted into concept bodies -------------------------------------------
158
168
 
159
- /** `_web_docs._doc_counts`. */
160
- function docCounts(state) {
169
+ /**
170
+ * `_web_docs._doc_counts`.
171
+ *
172
+ * Exported so the tier tokens can be gated on the day they are ADDED rather than on the day a
173
+ * page first spends one. Only one page carries `{N_LAWS}` today; a test that could only reach
174
+ * these through a rendered body would leave four of the seven untested until Task 6.2.
175
+ */
176
+ export function docCounts(state) {
161
177
  const inv = state.inventory;
162
178
  const plugins = path.join(ROOT, 'adapters', 'opencode', 'plugins');
163
179
  let n = 0;
@@ -165,11 +181,23 @@ function docCounts(state) {
165
181
  n = readdirSync(plugins).filter((f) => f.startsWith('geneseed-') && f.endsWith('.js'))
166
182
  .length;
167
183
  } catch { n = 0; }
184
+ // `{N_LAWS}` IS THE INVARIANT COUNT AND NOTHING ELSE. The doctor's frozen
185
+ // `proseMirrorProblems` holds README and SHIPPED.md prose to the same number, so a token
186
+ // that quietly meant "the whole constitution" would put the docs and the gate at odds with
187
+ // no way to re-bless either. The other three tiers get their own tokens.
188
+ const packs = inv.doctrines ?? [];
189
+ const on = packs.filter((p) => p.active);
168
190
  return {
169
191
  '{N_LAWS}': (inv.laws || []).length,
170
192
  '{N_AGENTS}': (inv.agents || []).length,
171
193
  '{N_SKILLS}': (inv.skills || []).length,
172
194
  '{N_PLUGINS}': n,
195
+ '{N_ONTOLOGY}': (inv.ontology ?? []).length,
196
+ '{N_PACKS}': packs.length,
197
+ '{N_PACKS_ACTIVE}': on.length,
198
+ // Rules in the packs this install BUILT IN — the number a reader of these pages is
199
+ // actually bound by. The catalogue always ships whole, so it is never the smaller claim.
200
+ '{N_DOCTRINE_RULES}': on.reduce((t, p) => t + p.rules.length, 0),
173
201
  };
174
202
  }
175
203
 
package/js/web/jobs.mjs CHANGED
@@ -107,7 +107,7 @@ import { statSync } from 'node:fs';
107
107
  import { StringDecoder } from 'node:string_decoder';
108
108
  import path from 'node:path';
109
109
 
110
- import { ROOT } from '../checkout.mjs';
110
+ import { ROOT, PACK_ORDER } from '../checkout.mjs';
111
111
  import { setupBuildArgs } from '../generate.mjs';
112
112
  import { isDict } from '../mcp.mjs';
113
113
  import { jsonDumpsCompact, parseJson, pyTruthy, readText, writeText } from '../lib/fs.mjs';
@@ -444,11 +444,20 @@ export const NOT_PORTED_ACTIONS = new Set();
444
444
  */
445
445
  export const INLINE_ACTIONS = ['restore', 'install', 'deploy'];
446
446
 
447
+ /**
448
+ * ⚠ `doctrines` DEFAULTS TO THE FULL SET, and it is the one axis here whose default is not the
449
+ * generator's own. `theme`/`emit`/`footprint`/`posture`/`mode` fall back to a frozen literal
450
+ * that costs nothing when it is wrong; a missing `--doctrines` falls through to
451
+ * `harness.config.json`, and a `{"doctrines":["craft"]}` there re-emits the deployed install
452
+ * without its commit/push consent gate. So a caller that forgets the axis gets ALL packs —
453
+ * unknown resolves to the full set, never to a config value (`doctrinesForBuild`).
454
+ */
447
455
  function actionTable({
448
456
  theme = 'neutral', emit = 'opencode-global', footprint = 'full',
449
- posture = 'peer', mode = 'direct',
457
+ posture = 'peer', mode = 'direct', doctrines = [...PACK_ORDER],
450
458
  } = {}) {
451
- const buildArgv = setupBuildArgs(theme, emit, null, null, footprint, posture, mode);
459
+ const buildArgv = setupBuildArgs(theme, emit, null, null, footprint, posture, mode,
460
+ doctrines);
452
461
  return {
453
462
  doctor: [[NODE(), CLI(), 'doctor']],
454
463
  build: [[NODE(), GEN(), ...buildArgv]],
package/js/web/server.mjs CHANGED
@@ -45,7 +45,7 @@ import {
45
45
  existsSync, readFileSync, statSync, unlinkSync, chmodSync, mkdirSync, openSync,
46
46
  } from 'node:fs';
47
47
  import { createServer, request as httpRequest } from 'node:http';
48
- import { join, resolve as pathResolve, extname, basename, sep } from 'node:path';
48
+ import { join, resolve as pathResolve, dirname, extname, basename, sep } from 'node:path';
49
49
  import { fileURLToPath } from 'node:url';
50
50
  import { gzipSync } from 'node:zlib';
51
51
 
@@ -60,11 +60,12 @@ import { promptLine } from '../setup.mjs';
60
60
  // which resolves because everything either side needs of the other is a hoisted function
61
61
  // declaration, not a value read at module-evaluation time.
62
62
  import { preflight } from '../update.mjs';
63
+ import { doctrinesForBuild } from '../installs.mjs';
63
64
  import { NotFound, PREFIX_ROUTES, STATE_ROUTES, webState } from './api.mjs';
64
65
  import {
65
66
  apiDeployCmd, apiExcludesMutate, apiInstallCmd, apiInstallToggle, apiMcpToggle,
66
67
  apiMemoryDelete, apiProfileSave,
67
- apiRestore, apiRulesMutate, apiRulesPromote, apiSelectView, buildOverride,
68
+ apiRestore, apiRulesMutate, apiRulesPromote, apiSelectView, buildOverride, mcpTargetPaths,
68
69
  } from './actions.mjs';
69
70
  import { apiActivityToggle } from './activity.mjs';
70
71
  import { apiDocs, apiDocsPage } from './docs.mjs';
@@ -248,6 +249,29 @@ export const PORTED_INLINE = ['/api/ping', '/api/docs', '/api/docs/page/',
248
249
  export const PORTED_POST_INLINE = ['/api/shutdown', '/api/restart', '/api/jobs/',
249
250
  '/api/actions/'];
250
251
 
252
+ /**
253
+ * POST routes THAT NEVER EXISTED ON THE REFERENCE — the first of them, and the reason this set
254
+ * has to exist at all.
255
+ *
256
+ * `test_every_post_route_is_either_ported_or_declared_unported` asserts SET EQUALITY against
257
+ * `REF_POST`, a frozen literal of the paths the Python daemon answered. That was exactly right
258
+ * while the port was in flight: a route on one side and not the other was a defect either way.
259
+ * The port is finished and the reference is deleted, so the assertion now says something else —
260
+ * "this daemon may never grow a route" — which is a rule nobody chose.
261
+ *
262
+ * SO THE PARTITION GAINS A FOURTH PART RATHER THAN LOSING ITS EQUALITY. Widening `REF_POST` would
263
+ * have been the cheap edit and it would have been a lie: that list is a RECORD of what the
264
+ * reference answered, and a route added in 2026 was never in it. Keeping the record honest and
265
+ * declaring the additions beside it keeps both claims checkable — the reference's surface is
266
+ * still pinned, and everything past it is enumerated here instead of merely appearing.
267
+ *
268
+ * A route belongs here only once it is dispatched below; the dispatcher probe is what keeps this
269
+ * from becoming a declaration nobody honours.
270
+ */
271
+ export const POST_BEYOND_REF = new Set([
272
+ '/api/reveal',
273
+ ]);
274
+
251
275
  function notPorted(path) {
252
276
  return NOT_PORTED.has(path) || NOT_PORTED_PREFIXES.some((p) => path.startsWith(p));
253
277
  }
@@ -419,6 +443,34 @@ export function makeHandler(state, jm, token, dist, holder = null) {
419
443
  requestRestart(state.theme);
420
444
  return sendJson(res, { restarting: true }, 200, ae);
421
445
  }
446
+ if (path === '/api/reveal') {
447
+ // "It asks for a token and a URL and I don't know where to put them." The screen already
448
+ // PRINTS the config path above each target's rows; what it could not do is take you there.
449
+ //
450
+ // THE ALLOWLIST IS THE WHOLE SECURITY OF THIS ENDPOINT, same as the toggle beside it, and
451
+ // it is the same allowlist — `mcpTargetPaths()`, one function with two callers rather than
452
+ // two copies that drift. An unlisted path is a 404 before anything is opened. Without it
453
+ // this is "ask the desktop to open any path on the machine", behind a CSRF token and
454
+ // nothing else, and on Windows `openUrl` interpolates into a `cmd` command line with
455
+ // `windowsVerbatimArguments` — so the exact-match lookup is also what bounds the strings
456
+ // that can ever reach a shell to the ones the machine's own install registry produced.
457
+ //
458
+ // THE FOLDER, NOT THE FILE. `open`/`start`/`xdg-open` on a `.json` hands it to whatever
459
+ // claims that extension — which on a stock Windows box is a browser, i.e. a read-only view
460
+ // of the exact file the user is trying to EDIT. The containing folder lands in Explorer /
461
+ // Finder / the file manager every time, and the filename is already on screen beside the
462
+ // button.
463
+ const want = String(readJsonBody(body).path ?? '');
464
+ if (!mcpTargetPaths().has(want)) {
465
+ return sendJson(res, { error: `not found: ${want || '(none)'}` }, 404, ae);
466
+ }
467
+ const dir = dirname(want);
468
+ // `openUrl` SWALLOWS EVERY FAILURE BY DESIGN (a headless box has no opener), so `ok` here
469
+ // means "the path was allowed and the request was made", never "a window appeared". The
470
+ // response says which directory, so the UI can print it as the fallback instruction.
471
+ openUrl(dir);
472
+ return sendJson(res, { ok: true, dir }, 200, ae);
473
+ }
422
474
  const route = POST_ROUTES.get(path);
423
475
  if (route !== undefined) {
424
476
  const [fn, okIs409] = route;
@@ -498,8 +550,13 @@ export function makeHandler(state, jm, token, dist, holder = null) {
498
550
  // install, so a re-theme preserves lean/full, the register and the operating mode.
499
551
  const [theme, emit] = action === 'build'
500
552
  ? buildOverride(state, body) : [state.theme, state.emit];
553
+ // The pack selection follows the install like footprint/posture/mode, and is read here
554
+ // rather than left to the table's default: a console Build must re-emit the constitution
555
+ // the deployment already carries. Unknown (no `Active packs:` marker) resolves to ALL
556
+ // packs — never to `harness.config.json`, which would drop the consent gate.
501
557
  const cmds = actionCommands(action, {
502
558
  theme, emit, footprint: state.footprint, posture: state.posture, mode: state.mode,
559
+ doctrines: doctrinesForBuild(state.target),
503
560
  });
504
561
  if (!pyTruthy(cmds)) {
505
562
  return sendJson(res, { error: `unknown action ${action}` }, 404, ae);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//": "Every field here is argued and gated in tests/unit/package_manifest.test.mjs. `files`, `bin` and `engines` each have a test that fails when reality drifts from this file; `version` mirrors harness.config.json and is gated too. Read that docblock before editing this one.",
3
3
  "name": "geneseed",
4
- "version": "2.0.0",
4
+ "version": "3.0.0",
5
5
  "description": "A portable, theme-able harness you implant once and use everywhere to grow a disciplined AI coding agent.",
6
6
  "keywords": [
7
7
  "ai",