geneseed 3.11.0 → 3.13.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 (265) hide show
  1. package/CHANGELOG.md +160 -0
  2. package/README.md +5 -5
  3. package/SHIPPED.md +2 -2
  4. package/adapters/bob/README.md +1 -1
  5. package/adapters/claude-code/README.md +1 -1
  6. package/adapters/openclaude/README.md +1 -1
  7. package/adapters/opencode/README.md +3 -3
  8. package/adapters/opencode/plugins/geneseed-guard.js +157 -2
  9. package/bin/geneseed-cli.mjs +4 -0
  10. package/docs/README.md +41 -22
  11. package/docs/concepts/agents.md +3 -1
  12. package/docs/concepts/collaboration.md +3 -1
  13. package/docs/concepts/exclusions.md +4 -2
  14. package/docs/concepts/footprint.md +3 -1
  15. package/docs/concepts/foreman-mode.md +4 -2
  16. package/docs/concepts/hook-gates.md +61 -0
  17. package/docs/concepts/hooks.md +8 -56
  18. package/docs/concepts/host-matrix.md +38 -0
  19. package/docs/concepts/hosts.md +6 -32
  20. package/docs/concepts/loop-branch.md +66 -0
  21. package/docs/concepts/loop-bricks.md +48 -0
  22. package/docs/concepts/loop-contracts.md +33 -0
  23. package/docs/concepts/loop-gates.md +30 -0
  24. package/docs/concepts/loop-notes.md +43 -0
  25. package/docs/concepts/loop-risk.md +36 -0
  26. package/docs/concepts/loop-templates.md +50 -0
  27. package/docs/concepts/loops.md +73 -0
  28. package/docs/concepts/lsp.md +2 -0
  29. package/docs/concepts/memory.md +3 -1
  30. package/docs/concepts/notebook.md +3 -1
  31. package/docs/concepts/rules.md +4 -2
  32. package/docs/concepts/skills.md +3 -1
  33. package/docs/concepts/themes.md +3 -1
  34. package/docs/extending.md +28 -8
  35. package/docs/guides/autostart.md +3 -1
  36. package/docs/guides/choose-your-setup.md +5 -66
  37. package/docs/guides/headless.md +5 -3
  38. package/docs/guides/install-agent-md.md +37 -0
  39. package/docs/guides/install-bob.md +20 -0
  40. package/docs/guides/install-claude-code.md +22 -0
  41. package/docs/guides/install-clone.md +34 -0
  42. package/docs/guides/install-openclaude.md +25 -0
  43. package/docs/guides/install-opencode.md +35 -0
  44. package/docs/guides/install-scripted.md +21 -0
  45. package/docs/guides/install.md +9 -118
  46. package/docs/guides/loop-finish.md +54 -0
  47. package/docs/guides/loop-running.md +66 -0
  48. package/docs/guides/loop-which-template.md +35 -0
  49. package/docs/guides/mcp-filesystem.md +41 -0
  50. package/docs/guides/mcp-gitlab.md +66 -0
  51. package/docs/guides/mcp-markitdown.md +36 -0
  52. package/docs/guides/mcp.md +8 -137
  53. package/docs/guides/migrate.md +3 -1
  54. package/docs/guides/project-context.md +4 -2
  55. package/docs/guides/read-non-markdown.md +17 -0
  56. package/docs/guides/run-a-loop.md +64 -0
  57. package/docs/guides/run-anywhere.md +2 -0
  58. package/docs/guides/setup-doctrines.md +55 -0
  59. package/docs/guides/setup-footprint.md +48 -0
  60. package/docs/guides/uninstall.md +3 -1
  61. package/docs/guides/upgrade.md +3 -1
  62. package/docs/guides/verify.md +3 -1
  63. package/docs/guides/wiki.md +3 -1
  64. package/docs/guides/worktree.md +3 -1
  65. package/docs/reference/about.md +3 -1
  66. package/docs/reference/cli.md +2 -0
  67. package/docs/reference/env-context.md +40 -0
  68. package/docs/reference/environment.md +10 -59
  69. package/docs/reference/glossary.md +7 -1
  70. package/docs/reference/loop-actions.md +20 -0
  71. package/docs/reference/opencode-plugins.md +14 -116
  72. package/docs/reference/plugin-activity.md +20 -0
  73. package/docs/reference/plugin-context.md +27 -0
  74. package/docs/reference/plugin-guard.md +27 -0
  75. package/docs/reference/plugin-learn.md +27 -0
  76. package/docs/reference/plugin-notify.md +20 -0
  77. package/docs/reference/plugin-ponytail.md +19 -0
  78. package/docs/reference/plugin-workflow.md +25 -0
  79. package/docs/reference/troubleshoot-doctor.md +50 -0
  80. package/docs/reference/troubleshoot-hooks.md +99 -0
  81. package/docs/reference/troubleshoot-install.md +108 -0
  82. package/docs/reference/troubleshoot-tools.md +56 -0
  83. package/docs/reference/troubleshoot-update.md +23 -0
  84. package/docs/reference/troubleshoot-web.md +66 -0
  85. package/docs/reference/troubleshooting.md +13 -337
  86. package/docs/understand/a-day.md +3 -1
  87. package/docs/understand/enforced-vs-asked.md +3 -1
  88. package/docs/understand/harness.md +2 -0
  89. package/docs/understand/machine-bob.md +17 -0
  90. package/docs/understand/machine-claude.md +35 -0
  91. package/docs/understand/machine-opencode.md +36 -0
  92. package/docs/understand/on-your-machine.md +12 -55
  93. package/docs/understand/take-it-out.md +13 -0
  94. package/docs/web-ui.md +36 -5
  95. package/harness.config.json +1 -1
  96. package/js/README.md +32 -2
  97. package/js/build/driver.mjs +14 -8
  98. package/js/build/generate.mjs +10 -6
  99. package/js/build/render.mjs +5 -0
  100. package/js/build/source.mjs +5 -1
  101. package/js/cli-table.json +261 -0
  102. package/js/hosts/gitref.mjs +99 -0
  103. package/js/hosts/hooks.mjs +88 -1
  104. package/js/hosts/installs.mjs +21 -0
  105. package/js/hosts/mcp.mjs +1 -1
  106. package/js/hosts/native.mjs +43 -5
  107. package/js/inspect/checks-authoring.mjs +10 -0
  108. package/js/inspect/checks-build.mjs +28 -0
  109. package/js/inspect/diff.mjs +3 -0
  110. package/js/inspect/doctor.mjs +2 -1
  111. package/js/inspect/inventory.mjs +1 -0
  112. package/js/inspect/status.mjs +1 -1
  113. package/js/loop/catalog.mjs +127 -0
  114. package/js/loop/cli.mjs +173 -0
  115. package/js/loop/graph.mjs +163 -0
  116. package/js/loop/registry.mjs +196 -0
  117. package/js/loop/score.mjs +182 -0
  118. package/js/loop/state.mjs +436 -0
  119. package/js/maintain/migrate.mjs +4 -2
  120. package/js/maintain/setup.mjs +1 -1
  121. package/js/web/actions.mjs +34 -3
  122. package/js/web/api.mjs +51 -2
  123. package/js/web/daemon.mjs +43 -5
  124. package/js/web/docs.mjs +64 -12
  125. package/js/web/handler.mjs +1 -0
  126. package/js/web/jobs.mjs +3 -1
  127. package/js/web/routes.mjs +5 -1
  128. package/js/web/server.mjs +5 -3
  129. package/package.json +1 -1
  130. package/registry.json +14 -0
  131. package/src/AGENT.md.tmpl +2 -0
  132. package/src/agents/explorer.md +2 -2
  133. package/src/agents/researcher.md +3 -3
  134. package/src/bricks/README.md +34 -0
  135. package/src/bricks/_template.md +32 -0
  136. package/src/bricks/adr-challenge.md +20 -0
  137. package/src/bricks/adr-draft.md +15 -0
  138. package/src/bricks/apply.md +17 -0
  139. package/src/bricks/baseline-green.md +17 -0
  140. package/src/bricks/bruno-test.md +16 -0
  141. package/src/bricks/characterize.md +17 -0
  142. package/src/bricks/ci-fix.md +18 -0
  143. package/src/bricks/ci-triage.md +20 -0
  144. package/src/bricks/compat-check.md +16 -0
  145. package/src/bricks/deps-audit.md +14 -0
  146. package/src/bricks/docs-update.md +15 -0
  147. package/src/bricks/done-check.md +16 -0
  148. package/src/bricks/fitness-define.md +22 -0
  149. package/src/bricks/flake-check.md +17 -0
  150. package/src/bricks/flake-repro.md +18 -0
  151. package/src/bricks/identify.md +23 -0
  152. package/src/bricks/lint.md +14 -0
  153. package/src/bricks/migration-plan.md +21 -0
  154. package/src/bricks/mutation-check.md +18 -0
  155. package/src/bricks/plan.md +17 -0
  156. package/src/bricks/red-test.md +16 -0
  157. package/src/bricks/reproduce.md +16 -0
  158. package/src/bricks/review.md +19 -0
  159. package/src/bricks/security-scan.md +17 -0
  160. package/src/bricks/spec.md +18 -0
  161. package/src/bricks/test.md +14 -0
  162. package/src/bricks/upgrade-scout.md +16 -0
  163. package/src/doctrines/process.md +11 -8
  164. package/src/loops/api-endpoint.json +31 -0
  165. package/src/loops/architecture-decision.json +22 -0
  166. package/src/loops/bugfix.json +23 -0
  167. package/src/loops/ci-repair.json +19 -0
  168. package/src/loops/deps-upgrade.json +29 -0
  169. package/src/loops/enforce-architecture-rule.json +28 -0
  170. package/src/loops/feature.json +24 -0
  171. package/src/loops/fix-flaky-tests.json +30 -0
  172. package/src/loops/legacy-refactor.json +26 -0
  173. package/src/loops/legacy-tests.json +29 -0
  174. package/src/loops/refactor.json +24 -0
  175. package/src/loops/remove-dead-code.json +30 -0
  176. package/src/loops/spec-first-feature.json +29 -0
  177. package/src/loops/tdd.json +25 -0
  178. package/src/loops/update-contract.json +33 -0
  179. package/src/skills/brick-forge.md +48 -0
  180. package/src/skills/bruno-collection-generator.md +3 -3
  181. package/src/skills/bruno-test-writer.md +3 -3
  182. package/src/skills/ci-fix.md +3 -3
  183. package/src/skills/consolidate-memory.md +4 -4
  184. package/src/skills/council.md +1 -1
  185. package/src/skills/fresh-eyes.md +1 -1
  186. package/src/skills/geneseed.md +1 -1
  187. package/src/skills/git-archaeology.md +1 -1
  188. package/src/skills/git-rescue.md +4 -3
  189. package/src/skills/handoff.md +1 -1
  190. package/src/skills/herdr.md +1 -1
  191. package/src/skills/learn-mode.md +2 -2
  192. package/src/skills/loop.md +111 -0
  193. package/src/skills/opencode-theme.md +3 -3
  194. package/src/skills/ponytail.md +1 -1
  195. package/src/skills/profile.md +3 -4
  196. package/src/skills/prose.md +2 -3
  197. package/src/skills/quiz.md +1 -1
  198. package/src/skills/react-view-transitions/SKILL.md +1 -1
  199. package/src/skills/react-view-transitions/VENDOR.md +8 -3
  200. package/src/skills/research.md +3 -3
  201. package/src/skills/review-response.md +2 -2
  202. package/src/skills/rule.md +3 -3
  203. package/src/skills/security-audit.md +4 -4
  204. package/src/skills/teach.md +1 -1
  205. package/src/skills/wiki.md +3 -3
  206. package/src/skills/workflow.md +4 -4
  207. package/src/skills/worktree.md +2 -2
  208. package/themes/_TEMPLATE.json +2 -0
  209. package/themes/biker.json +2 -0
  210. package/themes/commentator.json +2 -0
  211. package/themes/cyberpunk.json +2 -0
  212. package/themes/gamer.json +2 -0
  213. package/themes/imperial.json +2 -0
  214. package/themes/joker.json +2 -0
  215. package/themes/marvin.json +2 -0
  216. package/themes/mean.json +2 -0
  217. package/themes/military.json +2 -0
  218. package/themes/neutral.json +2 -0
  219. package/themes/pirate.json +2 -0
  220. package/themes/sports.json +2 -0
  221. package/themes/verstappen.json +2 -0
  222. package/themes/wizard.json +2 -0
  223. package/web/dist/assets/Activity-Ca2Aruu7.js +1 -0
  224. package/web/dist/assets/ActivityDetail-Bo7v4Z_r.js +1 -0
  225. package/web/dist/assets/Dashboard-BuMq46aT.js +2 -0
  226. package/web/dist/assets/Docs-BzccCllG.js +5 -0
  227. package/web/dist/assets/FilterInput-CBcJBQPI.js +1 -0
  228. package/web/dist/assets/GreenhouseView-BCIKcnGG.js +2 -0
  229. package/web/dist/assets/Installs-DMFJvq7s.js +2 -0
  230. package/web/dist/assets/JournalView-o1rmd1_1.js +1 -0
  231. package/web/dist/assets/Laws-BANxq_I_.js +3 -0
  232. package/web/dist/assets/Library-DW4x0fbh.js +1 -0
  233. package/web/dist/assets/Loops-DEBibbBP.js +2 -0
  234. package/web/dist/assets/{Markdown-qjM2_ofS.js → Markdown-CdTX0kLe.js} +14 -14
  235. package/web/dist/assets/OperatorHudView-CLD0fYaA.js +2 -0
  236. package/web/dist/assets/Personal-6uMJvyBG.js +1 -0
  237. package/web/dist/assets/Tabs-BubnyZhF.js +1 -0
  238. package/web/dist/assets/activity-CObqyxkL.js +1 -0
  239. package/web/dist/assets/{api-Dc8f3ca-.js → api-D_ixdyEY.js} +1 -1
  240. package/web/dist/assets/drawable-C_x5xncq.js +1 -0
  241. package/web/dist/assets/index-CMPgezFm.js +2 -0
  242. package/web/dist/assets/index-DMsT4Aa6.css +1 -0
  243. package/web/dist/assets/{react-B9Z_tXRV.js → react-Bbzhmq4v.js} +1 -1
  244. package/web/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
  245. package/web/dist/assets/sections-Dg23Hel5.js +1 -0
  246. package/web/dist/assets/usePoll-BOqeunzF.js +1 -0
  247. package/web/dist/index.html +6 -5
  248. package/web/dist/assets/Activity-C52LEb-2.js +0 -1
  249. package/web/dist/assets/ActivityDetail-CjNLRvXg.js +0 -1
  250. package/web/dist/assets/Dashboard-B6_sSCHi.js +0 -2
  251. package/web/dist/assets/Docs-Dj7BmCSA.js +0 -5
  252. package/web/dist/assets/FilterInput-CVyqmYWM.js +0 -1
  253. package/web/dist/assets/GreenhouseView-BALwjQsA.js +0 -2
  254. package/web/dist/assets/Installs-BbffDQm3.js +0 -2
  255. package/web/dist/assets/JournalView-JpAQ8Hqe.js +0 -1
  256. package/web/dist/assets/Laws-DK7eCWSm.js +0 -3
  257. package/web/dist/assets/Library-CoamHelE.js +0 -1
  258. package/web/dist/assets/OperatorHudView-DVx1HlBx.js +0 -2
  259. package/web/dist/assets/Personal-eaXb1zcj.js +0 -1
  260. package/web/dist/assets/Tabs-BmGtt8E2.js +0 -1
  261. package/web/dist/assets/drawable-Du_PZrQj.js +0 -1
  262. package/web/dist/assets/index-Bz_KMRDH.css +0 -1
  263. package/web/dist/assets/index-CNFH7o55.js +0 -2
  264. package/web/dist/assets/sections-GalMS0Zv.js +0 -1
  265. package/web/dist/assets/usePoll-O0sJEw1Y.js +0 -1
@@ -39,6 +39,8 @@ import {
39
39
  import { withStdoutSwallowed } from '../inspect/diff.mjs';
40
40
  import { excludeAdd, excludeRemove } from '../inspect/excludes.mjs';
41
41
  import { setupBuildArgs } from '../build/generate.mjs';
42
+ import { DEFAULT_PRESET, PRESETS } from '../loop/score.mjs';
43
+ import { setLoopPreset } from '../loop/registry.mjs';
42
44
  import { frontmatter, memoryDropIndex } from '../hosts/hooks.mjs';
43
45
  import {
44
46
  HOSTS, bobConfigDir, claudeConfigDir, expanduser, openclaudeConfigDir,
@@ -46,7 +48,7 @@ import {
46
48
  } from '../hosts/hosts.mjs';
47
49
  import {
48
50
  EMIT_HOST_SCOPE, doctrinesForBuild, excludedRulesOfDir, footprintOfDir, installState,
49
- installTargets, modeOfDir, postureOfDir, readMaybe,
51
+ installTargets, modeOfDir, postureOfDir, readMaybe, trustOfDir,
50
52
  } from '../hosts/installs.mjs';
51
53
  import {
52
54
  MCP_PRESETS, isDict, mcpApply, mcpCommented, mcpInstallTargets, mcpKnownNames, mcpLoad,
@@ -576,6 +578,30 @@ export function apiExcludesMutate(state, body) {
576
578
  return action === 'add' ? excludeAdd(p) : excludeRemove(p);
577
579
  }
578
580
 
581
+ // ---- loop preset picker ---------------------------------------------------------------------
582
+
583
+ /**
584
+ * The Active tab's one write: rewrite a registered worktree's `LOOP.md` `preset` field.
585
+ * `setLoopPreset` throws for both refusals the brief requires `POST /api/loops/preset` to make —
586
+ * a `root` the registry does not know (no arbitrary path writes) and a `preset` outside
587
+ * `PRESETS` — and this is where those two throws are turned into the `NotFound` convention
588
+ * every other mutating route here already answers in, so the handler's catch (`4xx`, not the
589
+ * 500 a bare `Error` would fall to) covers this route the same way it covers `apiRulesMutate`'s
590
+ * unknown id. Anything else `setLoopPreset` throws — a corrupt `LOOP.md` that fails to
591
+ * parse, say — is not one of those two refusals and is rethrown as-is, so it falls through to
592
+ * the handler's 500 instead of being misreported as "not found".
593
+ */
594
+ export function apiLoopsPresetMutate(state, body) {
595
+ const root = strOr(bget(body, 'root'));
596
+ const preset = strOr(bget(body, 'preset'));
597
+ try {
598
+ return { ok: true, loop: setLoopPreset(root, preset) };
599
+ } catch (e) {
600
+ if (e.message === 'unknown loop' || e.message.startsWith('unknown preset ')) throw new NotFound(e.message);
601
+ throw e;
602
+ }
603
+ }
604
+
579
605
  // ---- MCP ----------------------------------------------------------------------------------
580
606
 
581
607
  export function apiMcp() {
@@ -777,6 +803,7 @@ export function apiRestore(state, files) {
777
803
  footprint: state.footprint,
778
804
  posture: state.posture,
779
805
  mode: state.mode,
806
+ trust: state.trust,
780
807
  // THE PACK SELECTION IS THE FIFTH, and it is the one that makes this verb a BOUNDARY
781
808
  // question rather than a cosmetic one. `expected` rendered at all four packs, and this
782
809
  // verb COPIES OUT OF IT: restoring AGENT.md onto an install built `--doctrines craft`
@@ -858,6 +885,8 @@ export function apiInstallCmd(state, body) {
858
885
  const bmode = bget(body, 'mode');
859
886
  const mode = discoverNames('modes', 'direct').includes(bmode)
860
887
  ? bmode : (modeOfDir(root) || 'direct');
888
+ const btrust = bget(body, 'trust');
889
+ const trust = Object.keys(PRESETS).includes(btrust) ? btrust : (trustOfDir(root) || DEFAULT_PRESET);
861
890
  // Unspecified means "keep what this install already has", exactly as theme, footprint,
862
891
  // posture and mode above do — a rebuild through the console is not a place to silently
863
892
  // re-decide the constitution. ⚠ AND A CARRIER WITH NO `Active packs:` MARKER (a pre-2.3
@@ -872,7 +901,7 @@ export function apiInstallCmd(state, body) {
872
901
  const excludeRules = bodyExcludeRules(body) ?? excludedRulesOfDir(root);
873
902
  const out = scope === 'global' ? null : String(root);
874
903
  const argv = setupBuildArgs(theme || 'neutral', emit, out, out, fp, pos, mode, doctrines,
875
- PACK_ORDER, excludeRules);
904
+ PACK_ORDER, excludeRules, trust);
876
905
  return { cmd: [process.execPath, path.join(ROOT, 'bin', 'build-driver.mjs'), ...argv] };
877
906
  }
878
907
 
@@ -969,6 +998,8 @@ export function apiDeployCmd(state, body) {
969
998
  const pos = discoverNames('postures', 'peer').includes(bpos) ? bpos : 'peer';
970
999
  const bmode = bget(body, 'mode');
971
1000
  const mode = discoverNames('modes', 'direct').includes(bmode) ? bmode : 'direct';
1001
+ const btrust = bget(body, 'trust');
1002
+ const trust = Object.keys(PRESETS).includes(btrust) ? btrust : DEFAULT_PRESET;
972
1003
  // Same resolution as `apiInstallCmd` above, and for the same reason: the console's Deploy
973
1004
  // form sends host/path/theme/footprint/posture/mode and NO pack selection, and nothing stops
974
1005
  // it landing on a directory that already holds an install. Taking `bodyDoctrines` alone left
@@ -979,6 +1010,6 @@ export function apiDeployCmd(state, body) {
979
1010
  const excludeRules = bodyExcludeRules(body) ?? excludedRulesOfDir(root);
980
1011
  // project-scope emit name == host name (opencode / claude / bob / openclaude)
981
1012
  const argv = setupBuildArgs(theme || 'neutral', host, root, root, fp, pos, mode, doctrines,
982
- PACK_ORDER, excludeRules);
1013
+ PACK_ORDER, excludeRules, trust);
983
1014
  return { cmd: [process.execPath, path.join(ROOT, 'bin', 'build-driver.mjs'), ...argv] };
984
1015
  }
package/js/web/api.mjs CHANGED
@@ -21,10 +21,13 @@ import { excludesSnapshot } from '../inspect/excludes.mjs';
21
21
  import { GLOBAL_MANIFEST, HOSTS, opencodeConfigDir, resolvePath } from '../hosts/hosts.mjs';
22
22
  import {
23
23
  doctrinesForBuild, excludedRulesOfDir, footprintOfDir, installState, installTargets,
24
- installedDefaults, modeOfDir,
24
+ installedDefaults, modeOfDir, trustOfDir,
25
25
  postureOfDir, readJsonMaybe, readMaybe, themeOfDir,
26
26
  } from '../hosts/installs.mjs';
27
27
  import { frontmatter } from '../hosts/hooks.mjs';
28
+ import { DEFAULT_PRESET, PRESETS } from '../loop/score.mjs';
29
+ import { loadCatalog, catalogProblems } from '../loop/catalog.mjs';
30
+ import { activeLoops } from '../loop/registry.mjs';
28
31
  import {
29
32
  SKILL_CLASS, entityStatus, loadRegistry, tuiInventory,
30
33
  } from '../inspect/inventory.mjs';
@@ -87,6 +90,7 @@ export function webState(theme = null, target = null) {
87
90
  footprint: null,
88
91
  posture: null,
89
92
  mode: null,
93
+ trust: null,
90
94
  _inv: null,
91
95
  _doctor: null,
92
96
  };
@@ -98,6 +102,7 @@ export function webState(theme = null, target = null) {
98
102
  st.footprint = footprintOfDir(st.target); // 'full' when no marker
99
103
  st.posture = postureOfDir(st.target) || 'peer';
100
104
  st.mode = modeOfDir(st.target) || 'direct';
105
+ st.trust = trustOfDir(st.target) || DEFAULT_PRESET;
101
106
 
102
107
  Object.defineProperty(st, 'inventory', {
103
108
  get() {
@@ -154,6 +159,7 @@ export function webState(theme = null, target = null) {
154
159
  st.footprint = footprintOfDir(st.root);
155
160
  st.posture = postureOfDir(st.root) || 'peer';
156
161
  st.mode = modeOfDir(st.root) || 'direct';
162
+ st.trust = trustOfDir(st.root) || DEFAULT_PRESET;
157
163
  st._inv = null;
158
164
  st._doctor = null;
159
165
  };
@@ -165,6 +171,7 @@ export function webState(theme = null, target = null) {
165
171
  st.footprint = footprintOfDir(st.root);
166
172
  st.posture = postureOfDir(st.root) || 'peer';
167
173
  st.mode = modeOfDir(st.root) || 'direct';
174
+ st.trust = trustOfDir(st.root) || DEFAULT_PRESET;
168
175
  };
169
176
  return st;
170
177
  }
@@ -741,11 +748,12 @@ export function apiInstalls(state) {
741
748
  footprint: footprintOfDir(root),
742
749
  posture: postureOfDir(root),
743
750
  mode: modeOfDir(root),
751
+ trust: trustOfDir(root),
744
752
  selected: samePath(viewCfg(host, scope, root), state.target),
745
753
  });
746
754
  }
747
755
  return { installs: out, postures: discoverNames('postures', 'peer'),
748
- modes: discoverNames('modes', 'direct') };
756
+ modes: discoverNames('modes', 'direct'), trusts: Object.keys(PRESETS) };
749
757
  }
750
758
 
751
759
  /** Beside the deployed AGENT.md. */
@@ -848,6 +856,9 @@ export function apiOverview(state) {
848
856
  notebook: notebookItems(state).length,
849
857
  wiki: wikiItems(state).length,
850
858
  config: configItems(state).length,
859
+ // The rail's Loops badge: templates only, from the same catalogue `/api/loops` reads —
860
+ // a brick is a part, the template is what a person runs.
861
+ loops: loadCatalog({ projectRoot: state.root }).templates.size,
851
862
  },
852
863
  doctor: state.doctor,
853
864
  diff,
@@ -933,6 +944,42 @@ export function apiRecent(state) {
933
944
  return { items: items.slice(0, RECENT_LIMIT), skipped, limit: RECENT_LIMIT };
934
945
  }
935
946
 
947
+ /**
948
+ * The Loops page: the brick and template catalogue the `geneseed loop` CLI reads, resolved from
949
+ * the install ROOT — for a project install that is the repo, where `<repo>/.geneseed/` lives; for
950
+ * a global one it is the config dir, which has no `.geneseed/` and so adds nothing. Read-only:
951
+ * the page draws templates and lists bricks, it never writes one.
952
+ *
953
+ * A template's graph is passed whole (`graph`) rather than flattened, because the page's ring
954
+ * layout needs `nodes`, `edges` and `loops` exactly as the engine reads them.
955
+ */
956
+ export function apiLoops(state) {
957
+ const { bricks, templates, overridden } = loadCatalog({ projectRoot: state.root });
958
+ const byName = ([a], [b]) => (a < b ? -1 : 1);
959
+ return {
960
+ templates: [...templates].sort(byName).map(([name, { origin, ...graph }]) => ({
961
+ name, description: graph.description || '', origin, graph,
962
+ })),
963
+ bricks: [...bricks].sort(byName).map(([, b]) => ({ reason: null, ...b })),
964
+ overridden,
965
+ // A brick or template the catalogue skipped (bad frontmatter, not JSON, misnamed), and a
966
+ // template that fails the graph rules: the page says so — the same list `loop check`
967
+ // prints — or a team's broken override would just silently not be there.
968
+ problems: catalogProblems({ projectRoot: state.root }),
969
+ };
970
+ }
971
+
972
+ /**
973
+ * The Active tab's read: every worktree `geneseed loop init` has registered, with its live
974
+ * `LOOP.md` state re-read on every call (`activeLoops` itself, not a cached copy — the registry
975
+ * stores identity only, see `js/loop/registry.mjs`). Takes no `state`-derived filtering: the
976
+ * registry is machine-wide, not scoped to this install's root, so a loop launched from a
977
+ * different project still shows up here.
978
+ */
979
+ export function apiLoopsActive() {
980
+ return { loops: activeLoops() };
981
+ }
982
+
936
983
  /** The source's release label, or null when it cannot be read. */
937
984
  function releaseLabel() {
938
985
  const v = sourceReleaseVersion({ config: CONFIG });
@@ -982,4 +1029,6 @@ export const STATE_ROUTES = {
982
1029
  // answers — a daemon whose POST worked and whose GET 501'd would have no way to obtain one.
983
1030
  '/api/rules': apiRules,
984
1031
  '/api/mcp': apiMcp,
1032
+ '/api/loops': apiLoops,
1033
+ '/api/loops/active': apiLoopsActive,
985
1034
  };
package/js/web/daemon.mjs CHANGED
@@ -267,9 +267,42 @@ export async function startDaemon(theme, port, openBrowser = true) {
267
267
  return 1;
268
268
  }
269
269
 
270
- export async function stopDaemon(theme = null) {
270
+ /**
271
+ * `--port` NAMES WHICH DAEMON, and the record holds only one. With no `--port` (`port` null)
272
+ * the verbs act on whatever the record names, as they always have. With one, they act only if
273
+ * the record is on that port: `stop --port 4791` once POSTed shutdown to the recorded url and
274
+ * stopped the console on 4747. So a record on another port is refused — no request sent, record
275
+ * left in place — and a server answering on the named port that is NOT the record is explained,
276
+ * because its token is not on record and there is nothing to guess it from.
277
+ *
278
+ * Null when the verb may go ahead; otherwise the exit code, already explained on stdout.
279
+ */
280
+ async function portRefusal(st, port, verb) {
281
+ if (port === null || port === undefined) return null;
282
+ const recorded = isTruthy(st) && isTruthy(st.url) ? Number(st.port) : null;
283
+ if (recorded === port) return null;
284
+ if (recorded !== null) {
285
+ if (verb === 'status') {
286
+ printOut(`[web] not running on port ${port} (the recorded daemon is on port ${recorded}).\n`);
287
+ } else {
288
+ printOut(`[web] the recorded daemon is on port ${recorded}, not ${port} — not touching it.\n`);
289
+ printOut(` \`geneseed web ${verb} --port ${recorded}\` (or no --port) ${verb}s that one.\n`);
290
+ }
291
+ }
292
+ if (await probe(`http://127.0.0.1:${port}`)) {
293
+ printOut(`[web] a server answers on port ${port} but is not the recorded daemon, so its `
294
+ + 'token is unknown — stop it from its own page (Stop) or end its process.\n');
295
+ return 1;
296
+ }
297
+ // Nothing recorded and nothing on the named port: the verb's own no-daemon arm says so.
298
+ return recorded === null ? null : 1;
299
+ }
300
+
301
+ export async function stopDaemon(theme = null, port = null) {
271
302
  const { target } = webState(theme);
272
303
  const st = readDaemon(target);
304
+ const refused = await portRefusal(st, port, 'stop');
305
+ if (refused !== null) return refused;
273
306
  if (!isTruthy(st) || !isTruthy(st.url)) {
274
307
  // NO `clearDaemon` HERE — deliberately asymmetric with `liveDaemon`/`statusDaemon`,
275
308
  // which DO delete a record naming no url. Unifying the two would delete a file this
@@ -287,8 +320,10 @@ export async function stopDaemon(theme = null) {
287
320
  return 0;
288
321
  }
289
322
 
290
- export async function statusDaemon(theme = null) {
323
+ export async function statusDaemon(theme = null, port = null) {
291
324
  const { target } = webState(theme);
325
+ const refused = await portRefusal(readDaemon(target), port, 'status');
326
+ if (refused !== null) return refused;
292
327
  const st = await liveDaemon(target);
293
328
  if (st) {
294
329
  printOut(`[web] running on ${st.url} (theme: ${none(st.theme)}, pid ${none(st.pid)})\n`);
@@ -322,19 +357,22 @@ export async function bounceDaemonIfRunning(theme, log, { onlyIfStale = false }
322
357
  return;
323
358
  }
324
359
  try {
325
- await restartDaemon(theme, 4747, false, true);
360
+ await restartDaemon(theme, null, false, true);
326
361
  } catch (e) {
327
362
  log(`[geneseed] ⚠️ could not refresh the web daemon (${e && e.message ? e.message : e}) — \`geneseed web restart\` manually if it was running.`);
328
363
  }
329
364
  }
330
365
 
331
- export async function restartDaemon(theme = null, port = 4747, openBrowser = true,
366
+ /** `port` null keeps the recorded daemon's port (4747 if none); see `portRefusal`. */
367
+ export async function restartDaemon(theme = null, port = null, openBrowser = true,
332
368
  onlyIfRunning = false) {
333
369
  const { target } = webState(theme);
334
370
  const st = readDaemon(target);
371
+ const refused = await portRefusal(st, port, 'restart');
372
+ if (refused !== null) return refused;
335
373
  const live = (await liveDaemon(target)) !== null;
336
374
  if (onlyIfRunning && !live) return 0;
337
- const usePort = (st && isTruthy(st.port) ? st.port : null) || port;
375
+ const usePort = port ?? ((st && isTruthy(st.port) ? st.port : null) || 4747);
338
376
  let open = openBrowser;
339
377
  if (live) {
340
378
  // A live daemon means a tab is already open on this (preserved) port and will
package/js/web/docs.mjs CHANGED
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * THE `?harness=` QUERY PARAM IS the Docs selector, and it is the ONLY input to these
15
15
  * endpoints that is not the checkout itself — the one thing a test can vary. Every
16
- * filtering rule below is exercised by sending both values over one page.
16
+ * filtering rule below is exercised by sending each host over one page.
17
17
  */
18
18
  import { readdirSync } from 'node:fs';
19
19
  import path from 'node:path';
@@ -105,7 +105,7 @@ export function docGroups() {
105
105
  if (group === undefined) continue;
106
106
  const order = Object.hasOwn(meta, 'order') ? meta.order : 0;
107
107
  delete meta.order;
108
- const page = { id, rel, ...meta };
108
+ const page = { id, rel, ...meta, section: sectionOf(meta) };
109
109
  if (body.trim()) page.body = body.replace(/\n+$/, '');
110
110
  group.pages.push([order, page]);
111
111
  }
@@ -113,15 +113,44 @@ export function docGroups() {
113
113
  for (const g of groups) {
114
114
  const entry = byId.get(g.id);
115
115
  // STABLE sort: equal orders keep filename order.
116
- entry.pages = entry.pages
116
+ entry.pages = bySection(entry.pages
117
117
  .map((pair, i) => [pair[0], i, pair[1]])
118
118
  .sort((a, b) => (a[0] - b[0]) || (a[1] - b[1]))
119
- .map((t) => t[2]);
119
+ .map((t) => t[2]));
120
120
  out.push(entry);
121
121
  }
122
122
  return out;
123
123
  }
124
124
 
125
+ /** The heading a page without a `section:` is listed under, inside its part. */
126
+ export const GENERAL_SECTION = 'General';
127
+
128
+ /**
129
+ * A page's `section:` frontmatter, or `General`. Anything but a non-empty string falls back
130
+ * rather than throwing — `tests/unit/docs_tree.test.mjs` is where a bad value goes red.
131
+ */
132
+ export function sectionOf(meta) {
133
+ const s = meta.section;
134
+ return typeof s === 'string' && s.trim() ? s.trim() : GENERAL_SECTION;
135
+ }
136
+
137
+ /**
138
+ * Pages already in `order:` order, regrouped so each section's pages sit together.
139
+ *
140
+ * SECTION ORDER IS THE SMALLEST `order:` AMONG ITS PAGES — which, on a list already sorted by
141
+ * order, is simply the order in which sections first appear. Within a section the pages keep
142
+ * their `order:` order. One rule for the list, the breadcrumb and prev/next alike, so the
143
+ * console never shows two orders for one part.
144
+ */
145
+ export function bySection(pages) {
146
+ const runs = new Map();
147
+ for (const p of pages) {
148
+ if (!runs.has(p.section)) runs.set(p.section, []);
149
+ runs.get(p.section).push(p);
150
+ }
151
+ return [...runs.values()].flat();
152
+ }
153
+
125
154
  // ---- markdown tables as data ------------------------------------------------------------
126
155
 
127
156
  /** One table row's cells; a `\|` inside a cell is kept as a literal bar. */
@@ -259,8 +288,27 @@ function subCounts(state, body) {
259
288
 
260
289
  // ---- harness filtering -----------------------------------------------------------------
261
290
 
262
- const HARNESSES = ['opencode', 'claude'];
263
- const HARNESS_OPEN_RE = /^\s*<!--\s*harness:(opencode|claude)\s*-->\s*$/;
291
+ /**
292
+ * The Docs selector names a HOST; a tag names a host or a FAMILY. Bob and OpenClaude emit
293
+ * through the Claude engine, so a `claude` tag (page or block) is the Claude family and shows
294
+ * to all three of them, while `bob` / `openclaude` narrow to the one host. The same mapping as
295
+ * the console's host table (`web/src/lib/hosts.js`, its `docs` column).
296
+ */
297
+ export const HOST_FAMILY = { opencode: 'opencode', claude: 'claude', openclaude: 'claude', bob: 'claude' };
298
+ export const HARNESSES = Object.keys(HOST_FAMILY);
299
+ /**
300
+ * A page or block tagged `tag` shows to `host`. Untagged content shows to every host. One tag
301
+ * names a host or a family; a LIST names hosts exactly, no family widening, because picking
302
+ * some hosts of a family is the only thing a list says that one tag cannot:
303
+ * `["claude", "openclaude"]` is Claude Code and OpenClaude, not Bob.
304
+ */
305
+ export const harnessShows = (tag, host) => (Array.isArray(tag)
306
+ ? tag.includes(host)
307
+ : !tag || tag === host || tag === HOST_FAMILY[host]);
308
+ /** A block's tag: `claude` alone, or `claude,openclaude` — the list, read as above. */
309
+ const harnessTag = (raw) => (raw.includes(',') ? raw.split(/\s*,\s*/) : raw);
310
+ const HOST_ID = '(?:opencode|claude|openclaude|bob)';
311
+ const HARNESS_OPEN_RE = new RegExp(String.raw`^\s*<!--\s*harness:(${HOST_ID}(?:\s*,\s*${HOST_ID})*)\s*-->\s*$`);
264
312
  const HARNESS_CLOSE_RE = /^\s*<!--\s*\/harness\s*-->\s*$/;
265
313
  /**
266
314
  * The cheap presence test for the early-out. It must never be NARROWER than the open
@@ -272,7 +320,9 @@ const HARNESS_HINT_RE = /<!--\s*harness:/;
272
320
  export function normHarness(value, state) {
273
321
  const v = (value || '').trim().toLowerCase();
274
322
  if (HARNESSES.includes(v)) return v;
275
- return /^(claude|openclaude)/.test(String(state.emit || '')) ? 'claude' : 'opencode';
323
+ // Emit names start with their host id (`claude-global`, `bob`, …); `files` reads OpenCode's.
324
+ const emit = String(state.emit || '');
325
+ return HARNESSES.find((h) => emit.startsWith(h)) || 'opencode';
276
326
  }
277
327
 
278
328
  /**
@@ -309,7 +359,8 @@ export function harnessBlocksBalanced(lines) {
309
359
  * the marker; the console has already filtered the block to the reader's host, so there the
310
360
  * label only repeats what the host selector says, and it goes with the marker.
311
361
  */
312
- const HOST_LABEL_RE = /^\s*\*\((OpenCode|Claude Code) only\)\*\s*$/;
362
+ const HOST_NAME = '(?:OpenCode|Claude Code|OpenClaude|IBM Bob)';
363
+ const HOST_LABEL_RE = new RegExp(String.raw`^\s*\*\(${HOST_NAME}(?: and ${HOST_NAME})* only\)\*\s*$`);
313
364
 
314
365
  export function stripHarnessBlocks(body, harnessName) {
315
366
  if (!HARNESS_HINT_RE.test(body)) return body;
@@ -329,7 +380,7 @@ export function stripHarnessBlocks(body, harnessName) {
329
380
  }
330
381
  if (!inFence) {
331
382
  const m = HARNESS_OPEN_RE.exec(line);
332
- if (m) { keep = m[1] === harnessName; afterOpen = true; continue; }
383
+ if (m) { keep = harnessShows(harnessTag(m[1]), harnessName); afterOpen = true; continue; }
333
384
  if (HARNESS_CLOSE_RE.test(line)) { keep = true; continue; }
334
385
  if (labelSlot && HOST_LABEL_RE.test(line)) continue;
335
386
  }
@@ -353,8 +404,8 @@ function splitLines(s) {
353
404
  function visibleGroups(harnessName) {
354
405
  const groups = [];
355
406
  for (const g of docGroups()) {
356
- if (g.harness && g.harness !== harnessName) continue;
357
- const pages = g.pages.filter((p) => !p.harness || p.harness === harnessName);
407
+ if (!harnessShows(g.harness, harnessName)) continue;
408
+ const pages = g.pages.filter((p) => harnessShows(p.harness, harnessName));
358
409
  if (pages.length) groups.push({ ...g, pages });
359
410
  }
360
411
  return groups;
@@ -510,7 +561,8 @@ export function apiDocs(state, harnessName = null) {
510
561
  const groups = visibleGroups(hn).map((g) => ({
511
562
  id: g.id,
512
563
  label: g.label,
513
- pages: g.pages.map((p) => ({ id: p.id, title: p.title, kind: p.kind })),
564
+ pages: g.pages.map((p) => ({ id: p.id, title: p.title, kind: p.kind, section: p.section,
565
+ ...(typeof p.description === 'string' ? { description: p.description } : {}) })),
514
566
  }));
515
567
  return { groups, harness: hn };
516
568
  }
@@ -295,6 +295,7 @@ export function makeHandler(state, jm, token, dist, holder = null) {
295
295
  // packs — never to `harness.config.json`, which would drop the consent gate.
296
296
  const cmds = actionCommands(action, {
297
297
  theme, emit, footprint: state.footprint, posture: state.posture, mode: state.mode,
298
+ trust: state.trust,
298
299
  doctrines: doctrinesForBuild(state.target),
299
300
  });
300
301
  if (!isTruthy(cmds)) {
package/js/web/jobs.mjs CHANGED
@@ -88,6 +88,7 @@ import path from 'node:path';
88
88
 
89
89
  import { ROOT, PACK_ORDER } from '../build/source.mjs';
90
90
  import { setupBuildArgs } from '../build/generate.mjs';
91
+ import { DEFAULT_PRESET } from '../loop/score.mjs';
91
92
  import { isDict } from '../hosts/mcp.mjs';
92
93
  import { readText, writeText, isFile } from '../lib/fs.mjs';
93
94
  import { jsonDumpsCompact, parseJson, isTruthy } from '../lib/json.mjs';
@@ -402,9 +403,10 @@ export const INLINE_ACTIONS = ['restore', 'install', 'deploy'];
402
403
  function actionTable({
403
404
  theme = 'neutral', emit = 'opencode-global', footprint = 'full',
404
405
  posture = 'peer', mode = 'direct', doctrines = [...PACK_ORDER], excludeRules = [],
406
+ trust = DEFAULT_PRESET,
405
407
  } = {}) {
406
408
  const buildArgv = setupBuildArgs(theme, emit, null, null, footprint, posture, mode,
407
- doctrines, PACK_ORDER, excludeRules);
409
+ doctrines, PACK_ORDER, excludeRules, trust);
408
410
  return {
409
411
  doctor: [[NODE(), CLI(), 'doctor']],
410
412
  build: [[NODE(), GEN(), ...buildArgv]],
package/js/web/routes.mjs CHANGED
@@ -15,7 +15,7 @@
15
15
  * expose X?", which has nothing to do with sockets, daemons or gzip.
16
16
  */
17
17
  import { formatValue, isTruthy, parseJson } from '../lib/json.mjs';
18
- import { apiExcludesMutate, apiInstallToggle, apiMcpToggle, apiMemoryDelete, apiProfileSave, apiRulesMutate, apiRulesPromote, apiSelectView } from './actions.mjs';
18
+ import { apiExcludesMutate, apiInstallToggle, apiLoopsPresetMutate, apiMcpToggle, apiMemoryDelete, apiProfileSave, apiRulesMutate, apiRulesPromote, apiSelectView } from './actions.mjs';
19
19
  import { apiActivityToggle } from './activity.mjs';
20
20
 
21
21
  /**
@@ -45,6 +45,10 @@ export const POST_ROUTES = new Map([
45
45
  ['/api/rules', [apiRulesMutate, true]],
46
46
  ['/api/rules/promote', [apiRulesPromote, true]],
47
47
  ['/api/profile', [apiProfileSave, true]],
48
+ // No `ok: false` arm: success returns `{ok: true, loop}`, and both refusals (unknown root,
49
+ // unknown preset) throw `NotFound`, which `doPost`'s catch answers 404 before this column is
50
+ // ever consulted — the same shape as `/api/memory/delete` and `/api/activity` above.
51
+ ['/api/loops/preset', [apiLoopsPresetMutate, false]],
48
52
  ]);
49
53
 
50
54
  /**
package/js/web/server.mjs CHANGED
@@ -200,9 +200,11 @@ export async function cmdWeb(args) {
200
200
  const port = args.port === null ? 4747 : parseIntStrict(args.port);
201
201
  const openBrowser = !args.noBrowser;
202
202
  if (args.action === 'start') return startDaemon(args.theme, port, openBrowser);
203
- if (args.action === 'stop') return stopDaemon(args.theme);
204
- if (args.action === 'restart') return restartDaemon(args.theme, port, openBrowser);
205
- if (args.action === 'status') return statusDaemon(args.theme);
203
+ // Only an EXPLICIT --port scopes these three to one daemon; absent, they act on the record.
204
+ const named = args.port === null ? null : port;
205
+ if (args.action === 'stop') return stopDaemon(args.theme, named);
206
+ if (args.action === 'restart') return restartDaemon(args.theme, named, openBrowser);
207
+ if (args.action === 'status') return statusDaemon(args.theme, named);
206
208
  return serve({ theme: args.theme, port, openBrowser, daemon: args.daemonInternal });
207
209
  }
208
210
 
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": "3.11.0",
4
+ "version": "3.13.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",
package/registry.json CHANGED
@@ -134,6 +134,13 @@
134
134
  "added": "2026-06-03",
135
135
  "last_verified": "2026-09-06"
136
136
  },
137
+ "skills/brick-forge": {
138
+ "status": "experimental",
139
+ "version": "0.1.0",
140
+ "owner": "Guillaume Lemer",
141
+ "added": "2026-10-03",
142
+ "last_verified": ""
143
+ },
137
144
  "skills/bruno-collection-generator": {
138
145
  "status": "experimental",
139
146
  "version": "1.0.0",
@@ -302,6 +309,13 @@
302
309
  "added": "2026-10-02",
303
310
  "last_verified": ""
304
311
  },
312
+ "skills/loop": {
313
+ "status": "experimental",
314
+ "version": "0.1.0",
315
+ "owner": "Guillaume Lemer",
316
+ "added": "2026-10-03",
317
+ "last_verified": ""
318
+ },
305
319
  "skills/migrate": {
306
320
  "status": "approved",
307
321
  "version": "1.0.0",
package/src/AGENT.md.tmpl CHANGED
@@ -266,6 +266,8 @@ the ten council seats are convened by the [council {{SKILL}}]({{DIR_SKILLS}}/cou
266
266
  | [council]({{DIR_SKILLS}}/council.md) | a decision, change, or discussion needs debating from several points of view before committing |
267
267
  | [workflow]({{DIR_SKILLS}}/workflow.md) | deterministic, code-driven orchestration of subagents (fan-out, find→verify, phased handoff) and the host exposes a workflow tool |
268
268
  | [pipeline]({{DIR_SKILLS}}/pipeline.md) | foreman mode triages a substantial task, or the user asks to run a crew (analyst → developer → tester) on one, isolated in its own worktree and merged on mechanical proof |
269
+ | [loop]({{DIR_SKILLS}}/loop.md) | the user asks to run a loop |
270
+ | [brick-forge]({{DIR_SKILLS}}/brick-forge.md) | a new or adapted loop brick |
269
271
  | [wiki]({{DIR_SKILLS}}/wiki.md) | consulting or growing the user's machine-wide knowledge base declared in `wiki.jsonc` (§8) |
270
272
  | [herdr]({{DIR_SKILLS}}/herdr.md) | running inside a [herdr](https://herdr.dev) pane (`HERDR_ENV=1`) and a task needs another pane — a sibling server, a watched build, a spawned agent |
271
273
  | [rule]({{DIR_SKILLS}}/rule.md) | anything the user wants to outlive this session — a standing rule of their own (§1) or a durable fact to remember (§5); also a recurring {{MEMORY}} lesson due for promotion |
@@ -3,8 +3,8 @@
3
3
  > {{DESC_EXPLORER}}
4
4
 
5
5
  ## When to dispatch
6
- - A question needs sweeping many files or directories, but you only want the
7
- conclusion — not the file contents in your context.
6
+ - A question needs sweeping many files or directories, but only the conclusion
7
+ belongs in the main context — not the file contents.
8
8
  - Locating where something lives, how a subsystem fits together, or gathering
9
9
  facts scattered across the repo.
10
10
  - Your main context is small and the expensive reading should happen elsewhere.
@@ -3,9 +3,9 @@
3
3
  > {{DESC_RESEARCHER}}
4
4
 
5
5
  ## When to dispatch
6
- - A question needs answers from *outside* the repository — the open web, a
7
- vendor's docs, an upgrade guide, a specification — and you want the verified
8
- findings, not the pages, in your context ({{DOCTRINE:context-economy}}).
6
+ - A question needs verified answers from *outside* the repository. The open web, a
7
+ vendor's docs, an upgrade guide, a specification: the main context gets the verified
8
+ findings, not the pages ({{DOCTRINE:context-economy}}).
9
9
  - The research {{SKILL}} is running and the host can run subagents: this seat
10
10
  does the fetching and cross-checking so the main context stays small.
11
11
  - A migrate, forge-mcp, or ingest run needs a spec or changelog read before
@@ -0,0 +1,34 @@
1
+ # Bricks
2
+
3
+ A brick is one node of a loop graph: a single prompt, handed to one agent or skill, that reports
4
+ back one of a declared set of outcomes. `geneseed loop next` reads the current node's brick and
5
+ hands its body to the model verbatim — the model never sees this file as a file, only as the
6
+ instructions inside it.
7
+
8
+ A brick is a markdown file, `<name>.md`, with flat YAML-ish frontmatter (`name`, `description`,
9
+ `effect`, exactly one of `agent` or `skill`, `outcomes`) and a plain-prose body. See
10
+ `_template.md` for the field-by-field shape.
11
+
12
+ ## The three origins
13
+
14
+ A brick's name is looked up across three levels, each one able to override the level before it
15
+ by name:
16
+
17
+ | origin | path | who writes it |
18
+ |---|---|---|
19
+ | shipped | `src/bricks/` (this directory) | Geneseed itself |
20
+ | global | `$XDG_CONFIG_HOME/geneseed/bricks/` | the user, for every project on this machine |
21
+ | project | `<repo>/.geneseed/bricks/` | a team, committed and shared |
22
+
23
+ A global or project brick named `apply` replaces the shipped `apply` everywhere a loop template
24
+ references it — the template does not have to change, and `geneseed loop check` reports the
25
+ override rather than hiding it.
26
+
27
+ ## Writing one
28
+
29
+ Start from `_template.md`, or ask the `brick-forge` skill to draft one for an agent or skill you
30
+ already have. Keep the body to 4-10 lines of direct instruction, and end it with how the model
31
+ should choose among the outcomes you declared — that sentence is what turns a vague brief into a
32
+ loop that actually terminates. If the brick finds something the next brick needs, write it to a
33
+ file in the OS temp directory and report that file's path with the outcome — the loop skill
34
+ records it as a note.
@@ -0,0 +1,32 @@
1
+ <!--
2
+ A brick template — copy this file to `<name>.md` (kebab-case, matching the `name:` field
3
+ below) under `bricks/` at whichever level you are authoring for: shipped (this package's
4
+ `src/bricks/`), global ($XDG_CONFIG_HOME/geneseed/bricks/), or project (<repo>/.geneseed/bricks/).
5
+ A later level overrides an earlier one BY NAME. Leading-underscore files (like this one) and
6
+ README.md are never loaded as bricks.
7
+
8
+ Everything below the closing `---` is the brick's BODY: the prompt `geneseed loop next` hands
9
+ the agent, verbatim, plus the card fields it receives. No double-brace substitution tokens in
10
+ the body — the loop CLI reads this file raw and hands it straight to the model, unrendered.
11
+ -->
12
+ ---
13
+ name: my-brick
14
+ # One line, shown wherever the catalogue is listed. Not the instructions themselves.
15
+ description: One line describing what this brick does.
16
+ # `read` never touches a file — the engine does not score or diff its iteration.
17
+ # `mutate` may change files — its diff is scored against the declared card before commit.
18
+ effect: read
19
+ # Exactly one of agent OR skill (never both, never neither) — who carries out the body below.
20
+ # `agent: <name>` names a spec under `src/agents/`.
21
+ agent: tester
22
+ # `skill: <name>` names a spec under `src/skills/` instead of an agent — delete `agent:` above
23
+ # if you use this.
24
+ # skill: some-skill
25
+ # Every outcome the body can report, comma-separated — these become the edge labels a loop
26
+ # template routes on (`{from, on: "<outcome>", to}`). A loop template's graph check requires
27
+ # exactly one edge per outcome declared here.
28
+ outcomes: pass, fail
29
+ ---
30
+ Say, in direct instructions, what the agent or skill should do with the card it receives — 4 to
31
+ 10 lines, no more. End with exactly how to choose between the declared outcomes, so the model's
32
+ last read is the decision it has to make.