geneseed 3.10.0 → 3.12.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 (227) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/DESIGN.md +1 -1
  3. package/README.md +27 -45
  4. package/SHIPPED.md +2 -2
  5. package/adapters/bob/README.md +1 -1
  6. package/adapters/claude-code/README.md +17 -12
  7. package/adapters/openclaude/README.md +1 -1
  8. package/adapters/opencode/README.md +4 -4
  9. package/adapters/opencode/plugins/geneseed-guard.js +157 -2
  10. package/bin/build-driver.mjs +2 -0
  11. package/bin/geneseed-cli.mjs +21 -0
  12. package/bin/geneseed-hook.mjs +2 -0
  13. package/docs/README.md +42 -0
  14. package/docs/_groups.json +18 -0
  15. package/docs/concepts/agents.md +44 -0
  16. package/docs/concepts/collaboration.md +62 -0
  17. package/docs/concepts/exclusions.md +55 -0
  18. package/docs/concepts/footprint.md +52 -0
  19. package/docs/concepts/foreman-mode.md +69 -0
  20. package/docs/concepts/hooks.md +101 -0
  21. package/docs/concepts/hosts.md +55 -0
  22. package/docs/concepts/loops.md +165 -0
  23. package/docs/concepts/lsp.md +50 -0
  24. package/docs/{web → concepts}/memory.md +2 -2
  25. package/docs/{web → concepts}/notebook.md +2 -2
  26. package/docs/concepts/rules.md +52 -0
  27. package/docs/concepts/skills.md +49 -0
  28. package/docs/concepts/themes.md +50 -0
  29. package/docs/extending.md +32 -21
  30. package/docs/guides/autostart.md +70 -0
  31. package/docs/guides/choose-your-setup.md +134 -0
  32. package/docs/guides/headless.md +22 -0
  33. package/docs/guides/install.md +161 -0
  34. package/docs/guides/mcp.md +195 -0
  35. package/docs/guides/migrate.md +41 -0
  36. package/docs/guides/project-context.md +87 -0
  37. package/docs/guides/run-anywhere.md +45 -0
  38. package/docs/guides/uninstall.md +51 -0
  39. package/docs/guides/upgrade.md +65 -0
  40. package/docs/guides/verify.md +65 -0
  41. package/docs/guides/wiki.md +69 -0
  42. package/docs/guides/worktree.md +47 -0
  43. package/docs/reference/about.md +7 -0
  44. package/docs/reference/cli.md +7 -0
  45. package/docs/reference/environment.md +99 -0
  46. package/docs/reference/glossary.md +37 -0
  47. package/docs/reference/opencode-plugins.md +133 -0
  48. package/docs/reference/troubleshooting.md +350 -0
  49. package/docs/understand/a-day.md +77 -0
  50. package/docs/understand/enforced-vs-asked.md +64 -0
  51. package/docs/understand/harness.md +36 -0
  52. package/docs/understand/on-your-machine.md +76 -0
  53. package/docs/web-ui.md +13 -2
  54. package/harness.config.json +1 -1
  55. package/js/README.md +27 -4
  56. package/js/build/driver.mjs +14 -8
  57. package/js/build/generate.mjs +22 -7
  58. package/js/build/render.mjs +5 -0
  59. package/js/build/source.mjs +11 -1
  60. package/js/cli-table.json +236 -1
  61. package/js/hosts/gitref.mjs +99 -0
  62. package/js/hosts/hooks.mjs +88 -1
  63. package/js/hosts/installs.mjs +21 -0
  64. package/js/hosts/mcp.mjs +1 -1
  65. package/js/hosts/settings.mjs +13 -1
  66. package/js/inspect/checks-authoring.mjs +15 -3
  67. package/js/inspect/diff.mjs +3 -0
  68. package/js/inspect/doctor.mjs +3 -2
  69. package/js/inspect/inventory.mjs +2 -1
  70. package/js/inspect/registry.mjs +20 -3
  71. package/js/inspect/status.mjs +8 -3
  72. package/js/lib/node-floor.mjs +30 -0
  73. package/js/loop/catalog.mjs +116 -0
  74. package/js/loop/cli.mjs +151 -0
  75. package/js/loop/graph.mjs +145 -0
  76. package/js/loop/score.mjs +143 -0
  77. package/js/loop/state.mjs +322 -0
  78. package/js/maintain/migrate.mjs +4 -2
  79. package/js/maintain/setup.mjs +109 -21
  80. package/js/maintain/update.mjs +4 -15
  81. package/js/ui/cli.mjs +2 -2
  82. package/js/web/actions.mjs +9 -3
  83. package/js/web/api.mjs +41 -2
  84. package/js/web/daemon.mjs +94 -4
  85. package/js/web/docs.mjs +149 -65
  86. package/js/web/handler.mjs +1 -0
  87. package/js/web/jobs.mjs +3 -1
  88. package/js/web/server.mjs +10 -5
  89. package/package.json +6 -4
  90. package/registry.json +21 -0
  91. package/src/AGENT.md.tmpl +6 -0
  92. package/src/bricks/README.md +32 -0
  93. package/src/bricks/_template.md +32 -0
  94. package/src/bricks/apply.md +17 -0
  95. package/src/bricks/baseline-green.md +17 -0
  96. package/src/bricks/bruno-test.md +16 -0
  97. package/src/bricks/ci-fix.md +17 -0
  98. package/src/bricks/deps-audit.md +14 -0
  99. package/src/bricks/docs-update.md +15 -0
  100. package/src/bricks/identify.md +20 -0
  101. package/src/bricks/lint.md +14 -0
  102. package/src/bricks/plan.md +17 -0
  103. package/src/bricks/reproduce.md +16 -0
  104. package/src/bricks/review.md +17 -0
  105. package/src/bricks/security-scan.md +17 -0
  106. package/src/bricks/test.md +14 -0
  107. package/src/doctrines/process.md +11 -8
  108. package/src/loops/bugfix.json +22 -0
  109. package/src/loops/feature.json +21 -0
  110. package/src/loops/refactor.json +20 -0
  111. package/src/postures/mentor.md +1 -0
  112. package/src/skills/brick-forge.md +46 -0
  113. package/src/skills/geneseed.md +1 -1
  114. package/src/skills/learn-mode.md +89 -0
  115. package/src/skills/loop.md +99 -0
  116. package/src/skills/pipeline.md +2 -2
  117. package/src/skills/quiz.md +2 -1
  118. package/src/skills/teach.md +2 -1
  119. package/themes/_TEMPLATE.json +3 -0
  120. package/themes/biker.json +3 -0
  121. package/themes/commentator.json +3 -0
  122. package/themes/cyberpunk.json +3 -0
  123. package/themes/gamer.json +3 -0
  124. package/themes/imperial.json +3 -0
  125. package/themes/joker.json +3 -0
  126. package/themes/marvin.json +3 -0
  127. package/themes/mean.json +3 -0
  128. package/themes/military.json +3 -0
  129. package/themes/neutral.json +3 -0
  130. package/themes/pirate.json +3 -0
  131. package/themes/sports.json +3 -0
  132. package/themes/verstappen.json +3 -0
  133. package/themes/wizard.json +3 -0
  134. package/web/dist/assets/Activity-GaaZWXTY.js +1 -0
  135. package/web/dist/assets/ActivityDetail-4t7d45Du.js +1 -0
  136. package/web/dist/assets/Dashboard-CYn7Tp16.js +2 -0
  137. package/web/dist/assets/Docs-BeFKLg2T.js +5 -0
  138. package/web/dist/assets/{GreenhouseView-D0cbCJ5m.js → GreenhouseView-C6ibc8IE.js} +1 -1
  139. package/web/dist/assets/Installs-DRYjYnxs.js +2 -0
  140. package/web/dist/assets/JournalView-1HbU1mfg.js +1 -0
  141. package/web/dist/assets/Laws-DFoG8JWB.js +3 -0
  142. package/web/dist/assets/Library-DDjgNwz_.js +1 -0
  143. package/web/dist/assets/Loops-D9VEoO0F.js +1 -0
  144. package/web/dist/assets/{Markdown-CkFyfgNf.js → Markdown-qjM2_ofS.js} +5 -5
  145. package/web/dist/assets/{OperatorHudView-CiAkAkQL.js → OperatorHudView-Bs7paWmh.js} +1 -1
  146. package/web/dist/assets/Personal-BY5n4gTw.js +1 -0
  147. package/web/dist/assets/Tabs-ho-EYD8i.js +1 -0
  148. package/web/dist/assets/api-CyMRbU2x.js +1 -0
  149. package/web/dist/assets/drawable-CLS9drMb.js +1 -0
  150. package/web/dist/assets/index-CX_hXMi5.css +1 -0
  151. package/web/dist/assets/index-Cjr7XCA3.js +2 -0
  152. package/web/dist/index.html +3 -2
  153. package/QUICKSTART.md +0 -50
  154. package/SETUP.md +0 -1148
  155. package/docs/web/_groups.json +0 -45
  156. package/docs/web/about.md +0 -6
  157. package/docs/web/adapters-claude-code.md +0 -8
  158. package/docs/web/adapters-opencode-loads.md +0 -8
  159. package/docs/web/adapters-opencode-spec.md +0 -8
  160. package/docs/web/adapters-opencode.md +0 -8
  161. package/docs/web/agents.md +0 -8
  162. package/docs/web/authoring.md +0 -11
  163. package/docs/web/autostart.md +0 -9
  164. package/docs/web/claude-hooks.md +0 -37
  165. package/docs/web/cli.md +0 -6
  166. package/docs/web/collaboration.md +0 -27
  167. package/docs/web/configure-wiki.md +0 -9
  168. package/docs/web/design.md +0 -7
  169. package/docs/web/env-knobs.md +0 -9
  170. package/docs/web/exclusions.md +0 -26
  171. package/docs/web/first-session.md +0 -28
  172. package/docs/web/footprint.md +0 -38
  173. package/docs/web/foreman-mode.md +0 -33
  174. package/docs/web/glossary.md +0 -6
  175. package/docs/web/harnesses.md +0 -30
  176. package/docs/web/headless.md +0 -9
  177. package/docs/web/ingest-docs.md +0 -9
  178. package/docs/web/install-paths.md +0 -9
  179. package/docs/web/install-quick.md +0 -35
  180. package/docs/web/lsp-overview.md +0 -37
  181. package/docs/web/mcp-claude-code.md +0 -10
  182. package/docs/web/mcp-filesystem.md +0 -9
  183. package/docs/web/mcp-gitlab.md +0 -9
  184. package/docs/web/mcp-markitdown.md +0 -9
  185. package/docs/web/mcp-overview.md +0 -34
  186. package/docs/web/mcp-trouble.md +0 -9
  187. package/docs/web/mcp-verify.md +0 -9
  188. package/docs/web/migrate.md +0 -39
  189. package/docs/web/model.md +0 -20
  190. package/docs/web/pipelines.md +0 -24
  191. package/docs/web/plugin-activity.md +0 -23
  192. package/docs/web/plugin-context.md +0 -26
  193. package/docs/web/plugin-guard.md +0 -24
  194. package/docs/web/plugin-learn.md +0 -27
  195. package/docs/web/plugin-notify.md +0 -24
  196. package/docs/web/plugin-ponytail.md +0 -23
  197. package/docs/web/plugin-setup.md +0 -7
  198. package/docs/web/plugin-workflow.md +0 -25
  199. package/docs/web/plugins.md +0 -17
  200. package/docs/web/project-context.md +0 -9
  201. package/docs/web/rules.md +0 -30
  202. package/docs/web/run-anywhere.md +0 -9
  203. package/docs/web/self-improve.md +0 -8
  204. package/docs/web/setup-choices.md +0 -64
  205. package/docs/web/skills.md +0 -8
  206. package/docs/web/themes-author.md +0 -20
  207. package/docs/web/themes.md +0 -9
  208. package/docs/web/token-footprint.md +0 -7
  209. package/docs/web/trouble.md +0 -38
  210. package/docs/web/upgrade.md +0 -9
  211. package/docs/web/verify.md +0 -30
  212. package/docs/web/wiki.md +0 -7
  213. package/docs/web/worktree.md +0 -10
  214. package/docs/wiki.md +0 -43
  215. package/web/dist/assets/Activity-BjVp522q.js +0 -1
  216. package/web/dist/assets/ActivityDetail-8ep1bspW.js +0 -1
  217. package/web/dist/assets/Dashboard-OkC_PCH6.js +0 -2
  218. package/web/dist/assets/Docs-CKPWz00o.js +0 -2
  219. package/web/dist/assets/Installs-BtGGdoTt.js +0 -2
  220. package/web/dist/assets/JournalView-Ckl4dmzR.js +0 -1
  221. package/web/dist/assets/Laws-CLpdRRk8.js +0 -3
  222. package/web/dist/assets/Library-wPPuKQrI.js +0 -1
  223. package/web/dist/assets/Personal-D8CnDo9v.js +0 -1
  224. package/web/dist/assets/Tabs-B4HNrrCs.js +0 -1
  225. package/web/dist/assets/drawable-BiwaOo6x.js +0 -1
  226. package/web/dist/assets/index-C1yil5j1.js +0 -2
  227. package/web/dist/assets/index-CDPDk2vj.css +0 -1
package/js/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `js/` — the module map
2
2
 
3
- Fifty-seven modules in seven folders. This page is the address book: **what each one owns**, and
3
+ Sixty-one modules in eight folders. This page is the address book: **what each one owns**, and
4
4
  **which file to open first** for a given task. It is not documentation of behaviour — every module
5
5
  has a docblock for that, and the docblock is the thing to read before editing.
6
6
 
@@ -20,6 +20,7 @@ js/build/ src/ + themes/ → rendered text → a bundle → a per-host
20
20
  js/hosts/ where it stops being a renderer and touches the machine
21
21
  js/inspect/ the read-only half: doctor, status, diff, validate, catalog
22
22
  js/lib/ primitives, every one a deliberate divergence from the Node default
23
+ js/loop/ `geneseed loop` — the state machine over LOOP.md, and what it steps through
23
24
  js/maintain/ lifecycle verbs that change an install that already exists
24
25
  js/ui/ what it prints at a terminal
25
26
  js/web/ the `geneseed web` console
@@ -68,10 +69,11 @@ user co-owns, and the hook verbs those emitted configs then run.
68
69
  |---|---|
69
70
  | `hosts.mjs` | The four host config dirs (opencode/claude/bob/openclaude), plus `resolvePath`/`expanduser` |
70
71
  | `hooks.mjs` | The verbs the emitted hooks run every session: `context`, `git-gate`, `rule-gate`, `tool-gate` (the two gates fused, for Bob), `learn`. `--host` picks the verdict dialect |
72
+ | `gitref.mjs` | fs/path-only: `gitRootOf`, `gitDirOf`, `currentBranch`, `loopLaunched` — read straight off `.git`, never spawn git. Shared by `hooks.mjs`'s git-gate and `js/loop/cli.mjs`'s commit-message writer |
71
73
  | `settings.mjs` | Merges into user-owned `settings.json`/`opencode.json`; owns the hook shim and the managed blocks |
72
74
  | `native.mjs` | Capability specs → host-native subagents and skills. The impure half of the emit |
73
75
  | `opencode.mjs` | OpenCode-only extras: colour themes, primary agent, `/commands`, plugins, overrides stub |
74
- | `installs.mjs` | Detects deployed installs and reads back their theme, footprint, posture and mode |
76
+ | `installs.mjs` | Detects deployed installs and reads back their theme, footprint, posture, mode and loop trust preset |
75
77
  | `mcp.mjs` | MCP server presets, the per-host `mcpServers` config read/write, and toggle state |
76
78
  | `link.mjs` | `geneseed link`/`unlink` — the PATH shim, and the only Windows USER-Path registry edit |
77
79
 
@@ -118,12 +120,33 @@ into files users have committed, so change the test that states it in the same c
118
120
  | `text.mjs` | String primitives: `codePointLength`, `WHITESPACE`, `parseIntStrict`, `percentDecode` |
119
121
  | `udiff.mjs` | `geneseed diff`'s own difflib clone — the alignment IS the printed hunks — plus `splitLines` |
120
122
  | `proc.mjs` | One constant: the `windowsHide` flag every spawn in `js/` must reuse |
123
+ | `node-floor.mjs` | The first import of all three binaries: below Node 22.3, one sentence on stderr and exit 1 (0 in the hook) instead of a stack trace |
121
124
 
122
125
  **Before editing:** parse with `parseJson`, never `JSON.parse` — the latter collapses int `1` and
123
126
  float `1.0` into one double, and that value gets written into a user's file. Do not swap in a diff
124
127
  library for `udiff.mjs`: it is not Myers, ties resolve to the earliest match. Every function in
125
128
  `paths.mjs` branches on `process.platform`, so green on one OS proves nothing about the other.
126
129
 
130
+ ## `js/loop/` — the loop engine
131
+
132
+ `geneseed loop` — a state machine over a brick graph, persisted as a JSON block in `LOOP.md` at
133
+ the git root. The model only ever runs the node it is handed and reports an outcome; every
134
+ transition, score and stop is computed here.
135
+
136
+ | module | owns |
137
+ |---|---|
138
+ | `graph.mjs` | `checkGraph` — the rules a loop graph must satisfy before a node runs: nodes name real bricks, edges cover every outcome, `$close` is reachable, every cycle is inside a declared loop |
139
+ | `score.mjs` | The risk arithmetic: `WEIGHTS`, `PRESETS`, `declaredRisk`/`actualRisk`/`decide`, `parseNumstat`. Pure — no import but node builtins |
140
+ | `catalog.mjs` | `loadCatalog` — bricks and loop templates from shipped/global/project origins, later overrides earlier BY NAME |
141
+ | `state.mjs` | The engine itself: `initState`, `nextStep`, `scoreDeclared`/`scoreDiff`, `recordOutcome`, `decideAwaiting`, `renderLoopFile`/`parseLoopFile` |
142
+ | `cli.mjs` | `geneseed loop check\|init\|next\|score\|record\|decide` — the only face either a model or a human reaches this engine through |
143
+
144
+ **Before editing:** `score.mjs` is imported by the build driver (the `--trust` choices), so it must
145
+ stay free of every import but node builtins and `graph.mjs`. `state.mjs` excludes `LOOP.md` itself
146
+ from every score and every read-brick porcelain check — it is the engine's own bookkeeping, never
147
+ part of the work being scored. `cli.mjs` may not spawn: git output is piped in on stdin by the
148
+ model, never read by this entry.
149
+
127
150
  ## `js/maintain/` — lifecycle
128
151
 
129
152
  Verbs that change an install that already exists, as opposed to rendering one.
@@ -168,7 +191,7 @@ the deployed harness as JSON.
168
191
  | `actions.mjs` | The mutating endpoints — rules, profile, memory, MCP, excludes — plus install/deploy argv |
169
192
  | `daemon.mjs` | Detached web start/stop/status/restart, the `.geneseed-web.json` record, and `openUrl` |
170
193
  | `jobs.mjs` | `JobManager` and the action→argv table. The only module here that spawns job children |
171
- | `docs.mjs` | The Docs tab: the `docs/web/` page registry, `?harness=` filtering, the five page kinds |
194
+ | `docs.mjs` | The Docs tab: the `docs/<folder>/` page registry, `?harness=` filtering, link rewriting, the six page kinds |
172
195
  | `activity.mjs` | The `activity/` session files: pid-liveness prune, list, detail, and the on/off flag |
173
196
 
174
197
  **Before editing:** `makeHandler(state, jm, token, dist, holder)`'s arity is load-bearing — three
@@ -190,6 +213,6 @@ and inline lists are held against a surface list written out in
190
213
  | a web endpoint | GET → `js/web/api.mjs`; POST → `js/web/actions.mjs` | declare it in `js/web/routes.mjs`; the partition test probes the real handler |
191
214
  | a CLI verb | `js/cli-table.json` — the table IS the parser | the `VERBS` row in `bin/geneseed-cli.mjs`, the row count in `cli_table.test.mjs`, `NATIVE` in **two** test files |
192
215
  | a hook verb | `js/hosts/hooks.mjs` | `claudeHookGroups` in `settings.mjs`, `VERBS` in `bin/geneseed-hook.mjs`. Keep the import graph tiny |
193
- | a console doc page | `docs/web/<id>.md` — the stem is the id | `docs/extending.md` §4c. Only `kind: "concept"` gets `{N_*}` substitution |
216
+ | a console doc page | `docs/<understand|guides|concepts|reference>/<id>.md` — the stem is the id | `docs/extending.md` §4c. Only `kind: "concept"` gets `{N_*}` substitution |
194
217
  | an OpenCode plugin | `adapters/opencode/plugins/geneseed-<name>.js` | `docs/extending.md` §4a — the `geneseed-` prefix is mechanically load-bearing |
195
218
  | to change a `js/lib/` primitive | `tests/fixtures/pure_probe.mjs` **first** | the docblock says which Node default it deliberately departs from — change that sentence too, or the next reader restores the default |
@@ -65,6 +65,9 @@ import {
65
65
  // registry to find every install it must re-emit, and a CLI verb may not reach into a driver
66
66
  // for a reader. See js/inspect/registry.mjs.
67
67
  import { registryRecord, registryRoots } from '../inspect/registry.mjs';
68
+ // The loop engine's trust presets — the `--trust` choices. `score.mjs` imports nothing, which is
69
+ // what lets it sit inside this driver's total `child_process` ban.
70
+ import { DEFAULT_PRESET, PRESETS } from '../loop/score.mjs';
68
71
 
69
72
  // P2. `--sync-themes` crossed into its own module rather than into this file: it is 90 lines
70
73
  // of textual surgery over committed files with a corpus of its own, and it is the half of the
@@ -111,8 +114,8 @@ export function resolveOut(raw) {
111
114
  */
112
115
  function configDefaults() {
113
116
  const d = {
114
- theme: 'neutral', posture: 'peer', mode: 'direct', doctrines: [...PACK_ORDER],
115
- excludeRules: [],
117
+ theme: 'neutral', posture: 'peer', mode: 'direct', trust: DEFAULT_PRESET,
118
+ doctrines: [...PACK_ORDER], excludeRules: [],
116
119
  };
117
120
  if (!existsSync(CONFIG)) return d;
118
121
  try {
@@ -163,7 +166,7 @@ function configDefaults() {
163
166
  * flag is added to one and not the other, and nothing downstream would say so.
164
167
  */
165
168
  const VALUED = {
166
- '--theme': 'theme', '--posture': 'posture', '--mode': 'mode',
169
+ '--theme': 'theme', '--posture': 'posture', '--mode': 'mode', '--trust': 'trust',
167
170
  '--doctrines': 'doctrines', '--exclude-rules': 'excludeRules',
168
171
  '--out': 'out', '--target': 'out', '--emit': 'emit',
169
172
  '--footprint': 'footprint', '--root': 'root', '--config-dir': 'cfgDir',
@@ -194,13 +197,13 @@ const FLAG_BY_NAME = Object.fromEntries(
194
197
  );
195
198
 
196
199
  /**
197
- * The four flags `choice` validates, as ONE table for the same anti-drift reason.
200
+ * The five flags `choice` validates, as ONE table for the same anti-drift reason.
198
201
  *
199
- * A function and not a constant because two of the four read the checkout: `discoverNames`
202
+ * A function and not a constant because two of the five read the checkout: `discoverNames`
200
203
  * scans `postures/` and `modes/`, so the answer depends on the source tree this driver is
201
204
  * standing in and cannot be frozen at import.
202
205
  *
203
- * `--doctrines` is DELIBERATELY not a fifth row. Every consumer of this table treats an
206
+ * `--doctrines` is DELIBERATELY not a sixth row. Every consumer of this table treats an
204
207
  * entry as one value drawn from a closed list — `choice` refuses anything else, and
205
208
  * `usage`'s metavar prints the list as the whole legal surface. `--doctrines` takes a comma
206
209
  * LIST plus the sentinel `none`, so a row here would have refused `craft,rigor` outright and
@@ -213,6 +216,7 @@ function choicesFor() {
213
216
  '--footprint': ['lean', 'full'],
214
217
  '--posture': discoverNames('postures', 'peer'),
215
218
  '--mode': discoverNames('modes', 'direct'),
219
+ '--trust': Object.keys(PRESETS),
216
220
  };
217
221
  }
218
222
 
@@ -340,6 +344,7 @@ function usage() {
340
344
  function parseArgs(argv, defaults) {
341
345
  const args = {
342
346
  theme: defaults.theme, posture: defaults.posture, mode: defaults.mode,
347
+ trust: defaults.trust,
343
348
  doctrines: defaults.doctrines,
344
349
  excludeRules: defaults.excludeRules,
345
350
  out: 'Harness', emit: 'files', footprint: 'lean', root: null,
@@ -1035,7 +1040,7 @@ export function buildInto({ theme, out, footprint = 'lean' }) {
1035
1040
  }
1036
1041
 
1037
1042
  export function emitGlobalInto(host, {
1038
- theme, out, cfgDir, footprint, posture = null, mode = null, doctrines = null,
1043
+ theme, out, cfgDir, footprint, posture = null, mode = null, doctrines = null, trust = null,
1039
1044
  }) {
1040
1045
  // `build.HOSTS.get(host, build.HOSTS["opencode"])` — an unknown host falls back rather than
1041
1046
  // raising, because the host comes from a marker file a user can edit.
@@ -1058,7 +1063,8 @@ export function emitGlobalInto(host, {
1058
1063
  // at `[]`, so the key is OMITTED instead of passed through — `makeCfg`'s own default is
1059
1064
  // the fail-closed answer and an explicit `doctrines: null` would not reach it.
1060
1065
  makeCfg({
1061
- posture: posture || 'peer', mode: mode || 'direct', ...(doctrines ? { doctrines } : {}),
1066
+ posture: posture || 'peer', mode: mode || 'direct', trust: trust || DEFAULT_PRESET,
1067
+ ...(doctrines ? { doctrines } : {}),
1062
1068
  }),
1063
1069
  { theme, footprint, root: null, cfgDir }, out,
1064
1070
  ),
@@ -34,11 +34,12 @@ import path from 'node:path';
34
34
 
35
35
  import { main as driverMain } from './driver.mjs';
36
36
  import { makeCfg, PACK_ORDER } from './source.mjs';
37
+ import { DEFAULT_PRESET } from '../loop/score.mjs';
37
38
  import { opencodeConfigDir, resolvePath } from '../hosts/hosts.mjs';
38
39
  import {
39
40
  EMIT_HOST_SCOPE, defaultMode, defaultPosture, defaultTheme, doctrinesForBuild,
40
41
  excludedRulesOfDir, footprintOfDir, installState, installTargets, modeOfDir, postureOfDir,
41
- readMaybe, themeOfDir,
42
+ readMaybe, themeOfDir, trustOfDir,
42
43
  } from '../hosts/installs.mjs';
43
44
  import { colorThemeFiles, colorThemeJson, PALETTE_ROLES } from '../hosts/opencode.mjs';
44
45
  import { renderAll } from './render.mjs';
@@ -93,7 +94,7 @@ function sysExit(msg) {
93
94
  */
94
95
  const BUILD_FORWARD = [
95
96
  ['theme', '--theme'], ['emit', '--emit'], ['footprint', '--footprint'],
96
- ['posture', '--posture'], ['mode', '--mode'], ['doctrines', '--doctrines'],
97
+ ['posture', '--posture'], ['mode', '--mode'], ['trust', '--trust'], ['doctrines', '--doctrines'],
97
98
  ['excludeRules', '--exclude-rules'], ['out', '--out'], ['root', '--root'],
98
99
  ['configDir', '--config-dir'],
99
100
  ];
@@ -114,7 +115,7 @@ export function cmdBuild(args) {
114
115
  * The elision rules are not symmetric and the Python says why: `footprint` is ALWAYS passed,
115
116
  * because omitting it when it matched the generator's default silently coupled the answer to
116
117
  * whatever that default happened to be — the moment the default moved, picking the old one
117
- * produced the new one. `posture` and `mode` still elide at theirs, and neither has moved.
118
+ * produced the new one. `posture`, `mode` and `trust` still elide at theirs, and none has moved.
118
119
  *
119
120
  * ⚠ `doctrines` DOES NOT ELIDE, AND THAT IS THE SAFETY DECISION, NOT A STYLE ONE. It used to
120
121
  * elide on the full set, by analogy with posture and mode — but those two compare against a
@@ -137,7 +138,7 @@ export function cmdBuild(args) {
137
138
  */
138
139
  export function setupBuildArgs(theme, emit, out = null, root = null, footprint = 'lean',
139
140
  posture = 'peer', mode = 'direct', doctrines = null, allPacks = PACK_ORDER,
140
- excludeRules = null) {
141
+ excludeRules = null, trust = DEFAULT_PRESET) {
141
142
  const argv = ['--theme', theme, '--emit', emit];
142
143
  if (!emit.endsWith('-global')) {
143
144
  if (out) argv.push('--out', out);
@@ -146,6 +147,7 @@ export function setupBuildArgs(theme, emit, out = null, root = null, footprint =
146
147
  if (footprint) argv.push('--footprint', footprint);
147
148
  if (posture && posture !== 'peer') argv.push('--posture', posture);
148
149
  if (mode && mode !== 'direct') argv.push('--mode', mode);
150
+ if (trust && trust !== DEFAULT_PRESET) argv.push('--trust', trust);
149
151
  if (Array.isArray(doctrines)) {
150
152
  // Emitted in `allPacks` order, not the order handed in: a caller may legitimately pass an
151
153
  // unsorted list, and the flag has to be canonical for the same reason the driver
@@ -199,6 +201,8 @@ export function installProfile(host, scope, root) {
199
201
  const footprint = footprintOfDir(root);
200
202
  const posture = postureOfDir(root) || defaultPosture();
201
203
  const mode = modeOfDir(root) || defaultMode();
204
+ // Read off the deployed loop skill; no config fallback — `DEFAULT_PRESET` is the engine's.
205
+ const trust = trustOfDir(root) || DEFAULT_PRESET;
202
206
  // A rebuild that defaulted the pack selection would re-emit an install into a constitution
203
207
  // its owner did not choose — and in one direction that is not merely surprising: dropping
204
208
  // the process pack takes the commit/push consent RULES out of AGENT.md while
@@ -213,7 +217,7 @@ export function installProfile(host, scope, root) {
213
217
  const excludeRules = excludedRulesOfDir(root);
214
218
  const out = scope === 'global' ? null : root;
215
219
  const argv = setupBuildArgs(theme, emit, out, out, footprint, posture, mode, doctrines,
216
- PACK_ORDER, excludeRules);
220
+ PACK_ORDER, excludeRules, trust);
217
221
  // A GLOBAL ROW NAMES ITS OWN DIR. `root` came from the host's config-dir resolver, which
218
222
  // honours env overrides (`OPENCODE_CONFIG_DIR`, `CLAUDE_CONFIG_DIR`…); without the flag the
219
223
  // same argv run from a shell lacking that variable re-emits into the DEFAULT dir — a second
@@ -221,7 +225,7 @@ export function installProfile(host, scope, root) {
221
225
  if (scope === 'global') argv.push('--config-dir', root);
222
226
  return {
223
227
  host, scope, root, state: installState(root, host, scope),
224
- emit, theme, footprint, posture, mode, doctrines, excludeRules, argv,
228
+ emit, theme, footprint, posture, mode, trust, doctrines, excludeRules, argv,
225
229
  };
226
230
  }
227
231
 
@@ -254,7 +258,18 @@ export function rebuildCommand(argv) {
254
258
  * choose — which is the same failure mode `_footprint_of_dir`'s WARN exists to prevent, at a
255
259
  * larger scale.
256
260
  */
257
- export function cmdRebuildAll() {
261
+ export async function cmdRebuildAll() {
262
+ const rc = rebuildAll();
263
+ // A console started before an upgrade keeps serving the old `index.html`; restart it — only
264
+ // when stale, see `bounceDaemonIfRunning`. Lazy: no other verb in this module needs js/web/.
265
+ const { bounceDaemonIfRunning } = await import('../web/daemon.mjs');
266
+ await bounceDaemonIfRunning(null, (m) => printOut(`${m}
267
+ `), { onlyIfStale: true });
268
+ return rc;
269
+ }
270
+
271
+ /** The rebuild itself, synchronous — `cmdRebuildAll` is it plus the console restart. */
272
+ export function rebuildAll() {
258
273
  const targets = installTargets().filter(([h, s, r]) => installState(r, h, s) === 'active');
259
274
  if (!targets.length) {
260
275
  printOut('[rebuild-all] no active installs detected.\n');
@@ -35,6 +35,7 @@ import { parseJson } from '../lib/json.mjs';
35
35
  import { normcase, comparePaths } from '../lib/paths.mjs';
36
36
  // `js/build/source.mjs` imports nothing but node builtins, so this direction cannot cycle.
37
37
  import { PACK_ORDER, readPackText, resolveRuleIds } from './source.mjs';
38
+ import { DEFAULT_PRESET } from '../loop/score.mjs';
38
39
 
39
40
  /** Document STRUCTURE is theme-INDEPENDENT — mirrors `_build_render.STRUCTURE`. */
40
41
  export const STRUCTURE = {
@@ -391,6 +392,10 @@ export function effectiveTheme(cfg, themeName, { footprint = 'full', lawsPrefix
391
392
  const theme = { ...loadTheme(cfg, themeName), ...(cfg.structure ?? STRUCTURE) };
392
393
  theme.POSTURE_BODY = registerBody(cfg, theme, 'postures', cfg.posture ?? 'peer', 'peer');
393
394
  theme.MODE_BODY = registerBody(cfg, theme, 'modes', cfg.mode ?? 'direct', 'direct');
395
+ // The loop skill's default trust preset (`--trust`), raw for `--preset` and capitalised for
396
+ // the `Default trust preset: **<Label>**` line `trustOfDir` reads back off an install.
397
+ theme.TRUST = cfg.trust ?? DEFAULT_PRESET;
398
+ theme.TRUST_LABEL = theme.TRUST[0].toUpperCase() + theme.TRUST.slice(1);
394
399
  // The doctrines pair is threaded, not read off `cfg`: `footprint` is a per-RENDER value
395
400
  // (`renderAll`'s option, and the same cfg renders both ways in one process), so parking it
396
401
  // on cfg would make the terse/full choice sticky across calls. An options object rather
@@ -26,6 +26,7 @@
26
26
  */
27
27
  import { readdirSync, readFileSync, statSync } from 'node:fs';
28
28
  import path from 'node:path';
29
+ import { DEFAULT_PRESET } from '../loop/score.mjs';
29
30
 
30
31
  /**
31
32
  * One doctrine pack file used to be read three times per render — the survives gate and the
@@ -168,6 +169,12 @@ export const CONFIG = path.join(ROOT, 'harness.config.json');
168
169
  export const THEMES = path.join(ROOT, 'themes');
169
170
  export const PLUGIN_SRC = path.join(ROOT, 'adapters', 'opencode', 'plugins');
170
171
  export const WORKFLOW_SRC = path.join(ROOT, 'adapters', 'opencode', 'workflows');
172
+ /**
173
+ * The user docs: one folder per kind of reading, all under `docs/` so GitHub renders the same
174
+ * files the console serves. A page's id is its basename, so ids must be unique across folders.
175
+ */
176
+ export const DOCS = path.join(ROOT, 'docs');
177
+ export const DOC_FOLDERS = ['understand', 'guides', 'concepts', 'reference'];
171
178
 
172
179
  /**
173
180
  * `_build_render.posture_names()` / `mode_names()` — discovered, never hardcoded, so a new
@@ -250,7 +257,8 @@ export function knownRuleIds() {
250
257
  * `PACK_ORDER` for the whole process.
251
258
  */
252
259
  export function makeCfg({
253
- posture = 'peer', mode = 'direct', doctrines = PACK_ORDER, excludeRules = [],
260
+ posture = 'peer', mode = 'direct', trust = DEFAULT_PRESET, doctrines = PACK_ORDER,
261
+ excludeRules = [],
254
262
  } = {}) {
255
263
  return {
256
264
  root: ROOT,
@@ -262,6 +270,8 @@ export function makeCfg({
262
270
  workflowSrc: WORKFLOW_SRC,
263
271
  posture,
264
272
  mode,
273
+ // The loop skill's default preset (`--trust`); the skill is its only reader.
274
+ trust,
265
275
  doctrines: Array.isArray(doctrines) ? [...doctrines] : [...PACK_ORDER],
266
276
  // ⚠ THE SECOND DOCTRINE AXIS, AND ITS DEFAULT IS THE OPPOSITE WAY ROUND. `doctrines`
267
277
  // defaults to EVERYTHING because an unknown selection must bind the most; `excludeRules`
package/js/cli-table.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "prog": "harness",
2
+ "prog": "geneseed",
3
3
  "commands": [
4
4
  {
5
5
  "name": "bootstrap",
@@ -133,6 +133,21 @@
133
133
  "type": null,
134
134
  "hidden": false
135
135
  },
136
+ {
137
+ "names": [
138
+ "--trust"
139
+ ],
140
+ "dest": "trust",
141
+ "metavar": null,
142
+ "help": "default loop trust preset: how much a loop may do before it stops to ask (default balanced)",
143
+ "choices": null,
144
+ "default": null,
145
+ "required": false,
146
+ "nargs": null,
147
+ "is_flag": false,
148
+ "type": null,
149
+ "hidden": false
150
+ },
136
151
  {
137
152
  "names": [
138
153
  "--doctrines"
@@ -623,6 +638,226 @@
623
638
  "options": [],
624
639
  "mutex": []
625
640
  },
641
+ {
642
+ "name": "loop",
643
+ "help": "run a loop: check graphs, start one in LOOP.md, and step its state machine (next/score/record/decide)",
644
+ "description": "",
645
+ "positionals": [
646
+ {
647
+ "names": [],
648
+ "dest": "action",
649
+ "metavar": null,
650
+ "help": "",
651
+ "choices": [
652
+ "check",
653
+ "init",
654
+ "next",
655
+ "score",
656
+ "record",
657
+ "decide"
658
+ ],
659
+ "default": null,
660
+ "required": true,
661
+ "nargs": null,
662
+ "is_flag": false,
663
+ "type": null,
664
+ "hidden": false
665
+ }
666
+ ],
667
+ "options": [
668
+ {
669
+ "names": ["--graph"],
670
+ "dest": "graph",
671
+ "metavar": null,
672
+ "help": "a loop template name or a .json graph file",
673
+ "choices": null,
674
+ "default": null,
675
+ "required": false,
676
+ "nargs": null,
677
+ "is_flag": false,
678
+ "type": null,
679
+ "hidden": false
680
+ },
681
+ {
682
+ "names": ["--brick"],
683
+ "dest": "brick",
684
+ "metavar": null,
685
+ "help": "check one brick file instead of the whole catalogue",
686
+ "choices": null,
687
+ "default": null,
688
+ "required": false,
689
+ "nargs": null,
690
+ "is_flag": false,
691
+ "type": null,
692
+ "hidden": false
693
+ },
694
+ {
695
+ "names": ["--title"],
696
+ "dest": "title",
697
+ "metavar": null,
698
+ "help": "the loop's title, written into LOOP.md",
699
+ "choices": null,
700
+ "default": null,
701
+ "required": false,
702
+ "nargs": null,
703
+ "is_flag": false,
704
+ "type": null,
705
+ "hidden": false
706
+ },
707
+ {
708
+ "names": ["--requirement"],
709
+ "dest": "requirement",
710
+ "metavar": null,
711
+ "help": "the requirement the loop exists to satisfy",
712
+ "choices": null,
713
+ "default": null,
714
+ "required": false,
715
+ "nargs": null,
716
+ "is_flag": false,
717
+ "type": null,
718
+ "hidden": false
719
+ },
720
+ {
721
+ "names": ["--preset"],
722
+ "dest": "preset",
723
+ "metavar": null,
724
+ "help": "the risk preset to run under (default: balanced)",
725
+ "choices": [
726
+ "prudent",
727
+ "balanced",
728
+ "aggressive"
729
+ ],
730
+ "default": null,
731
+ "required": false,
732
+ "nargs": null,
733
+ "is_flag": false,
734
+ "type": null,
735
+ "hidden": false
736
+ },
737
+ {
738
+ "names": ["--declared"],
739
+ "dest": "declared",
740
+ "metavar": null,
741
+ "help": "score the card just declared, before it runs",
742
+ "choices": null,
743
+ "default": false,
744
+ "required": false,
745
+ "nargs": "0",
746
+ "is_flag": true,
747
+ "type": null,
748
+ "hidden": false
749
+ },
750
+ {
751
+ "names": ["--diff"],
752
+ "dest": "diff",
753
+ "metavar": null,
754
+ "help": "score the actual diff, read as numstat on stdin",
755
+ "choices": null,
756
+ "default": false,
757
+ "required": false,
758
+ "nargs": "0",
759
+ "is_flag": true,
760
+ "type": null,
761
+ "hidden": false
762
+ },
763
+ {
764
+ "names": ["--actions"],
765
+ "dest": "actions",
766
+ "metavar": null,
767
+ "help": "comma-separated declared action kinds (new-file, logic, api, ...)",
768
+ "choices": null,
769
+ "default": null,
770
+ "required": false,
771
+ "nargs": null,
772
+ "is_flag": false,
773
+ "type": null,
774
+ "hidden": false
775
+ },
776
+ {
777
+ "names": ["--write-set"],
778
+ "dest": "write_set",
779
+ "metavar": null,
780
+ "help": "comma-separated files the declared card expects to touch",
781
+ "choices": null,
782
+ "default": null,
783
+ "required": false,
784
+ "nargs": null,
785
+ "is_flag": false,
786
+ "type": null,
787
+ "hidden": false
788
+ },
789
+ {
790
+ "names": ["--intent"],
791
+ "dest": "intent",
792
+ "metavar": null,
793
+ "help": "a short label for what the declared card is doing",
794
+ "choices": null,
795
+ "default": null,
796
+ "required": false,
797
+ "nargs": null,
798
+ "is_flag": false,
799
+ "type": null,
800
+ "hidden": false
801
+ },
802
+ {
803
+ "names": ["--outcome"],
804
+ "dest": "outcome",
805
+ "metavar": null,
806
+ "help": "the brick's reported outcome, one of its declared outcomes",
807
+ "choices": null,
808
+ "default": null,
809
+ "required": false,
810
+ "nargs": null,
811
+ "is_flag": false,
812
+ "type": null,
813
+ "hidden": false
814
+ },
815
+ {
816
+ "names": ["--card"],
817
+ "dest": "card",
818
+ "metavar": null,
819
+ "help": "a JSON card to record alongside the outcome",
820
+ "choices": null,
821
+ "default": null,
822
+ "required": false,
823
+ "nargs": null,
824
+ "is_flag": false,
825
+ "type": null,
826
+ "hidden": false
827
+ },
828
+ {
829
+ "names": ["--verdict"],
830
+ "dest": "verdict",
831
+ "metavar": null,
832
+ "help": "the user's answer to an awaiting validation",
833
+ "choices": [
834
+ "ok",
835
+ "no",
836
+ "amend"
837
+ ],
838
+ "default": null,
839
+ "required": false,
840
+ "nargs": null,
841
+ "is_flag": false,
842
+ "type": null,
843
+ "hidden": false
844
+ },
845
+ {
846
+ "names": ["--note"],
847
+ "dest": "note",
848
+ "metavar": null,
849
+ "help": "a note attached to the verdict, written into LOOP.md",
850
+ "choices": null,
851
+ "default": null,
852
+ "required": false,
853
+ "nargs": null,
854
+ "is_flag": false,
855
+ "type": null,
856
+ "hidden": false
857
+ }
858
+ ],
859
+ "mutex": []
860
+ },
626
861
  {
627
862
  "name": "mcp",
628
863
  "help": "list the MCP servers wired into each active install, and the presets available to add",
@@ -0,0 +1,99 @@
1
+ /**
2
+ * js/hosts/gitref.mjs — "what branch, and is a loop running here", read straight off the
3
+ * filesystem. Never spawns git.
4
+ *
5
+ * ONE OWNER, fs/path ONLY, NOTHING ELSE. Before this module, `currentBranch` and the
6
+ * launched-loop evidence reader were duplicated in `js/hosts/hooks.mjs` (the hook's
7
+ * `loopExempt`) and `js/loop/cli.mjs` (the CLI's commit-message writer) — two copies of the
8
+ * same worktree-following walk that could silently drift apart. Both now import this one.
9
+ * `js/hosts/hooks.mjs` is NOT a safe shared home for it: it pulls in `node:child_process`
10
+ * (`spawnSync`, for the destructive-act guard's own concerns) and the hook-dispatch
11
+ * machinery (`jsonDumpsCompact`, the ledger), none of which `js/loop/cli.mjs` — under a hard
12
+ * `child_process` ban like every other closure that feeds `bin/geneseed-cli.mjs` — may ever
13
+ * import. A module that is fs/path only is importable from anywhere.
14
+ *
15
+ * The OpenCode guard plugin (`adapters/opencode/plugins/geneseed-guard.js`) keeps its own
16
+ * standalone copy — it cannot import from `js/` at all — kept in step by hand, commented as
17
+ * this module's twin.
18
+ */
19
+ import { existsSync, statSync, lstatSync, readFileSync, openSync, readSync, closeSync } from 'node:fs';
20
+ import path from 'node:path';
21
+
22
+ /**
23
+ * The directory holding `.git` for `cwd` — walking up from `cwd`. A worktree's `.git` is a
24
+ * FILE naming its gitdir elsewhere; LOOP.md always lives beside that FILE, not beside the
25
+ * gitdir it points to, so this is the directory the launched-loop evidence reader looks in.
26
+ * null on anything unexpected.
27
+ */
28
+ export function gitRootOf(cwd) {
29
+ try {
30
+ let dir = path.resolve(cwd);
31
+ for (;;) {
32
+ if (existsSync(path.join(dir, '.git'))) return dir;
33
+ const up = path.dirname(dir);
34
+ if (up === dir) return null;
35
+ dir = up;
36
+ }
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * The `.git` DIRECTORY for `root` — itself, or the gitdir a worktree's `.git` FILE names.
44
+ * Throws if `.git` is a file that does not name one: a malformed worktree link is a louder
45
+ * failure than a silent "no gitdir", and every caller here wants to know before it acts on it.
46
+ */
47
+ export function gitDirOf(root) {
48
+ const dotgit = path.join(root, '.git');
49
+ if (statSync(dotgit).isFile()) {
50
+ const m = /^gitdir:\s*(.+?)\s*$/m.exec(readFileSync(dotgit, 'utf8'));
51
+ if (!m) throw new Error(`${dotgit} does not name a gitdir`);
52
+ return path.resolve(root, m[1]);
53
+ }
54
+ return dotgit;
55
+ }
56
+
57
+ /**
58
+ * The branch checked out in `cwd`, read from .git/HEAD without spawning git. null on
59
+ * anything unexpected (detached HEAD, no repo, a malformed worktree link, unreadable) — the
60
+ * caller then falls back to asking, never to allowing.
61
+ */
62
+ export function currentBranch(cwd) {
63
+ try {
64
+ const root = gitRootOf(cwd);
65
+ if (!root) return null;
66
+ const ref = /^ref:\s*refs\/heads\/(.+?)\s*$/m.exec(readFileSync(path.join(gitDirOf(root), 'HEAD'), 'utf8'));
67
+ return ref ? ref[1] : null;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ const LOOP_STATE_MARKER = '<!-- loop-state:begin -->';
74
+
75
+ /**
76
+ * Whether `<root>/LOOP.md` carries the engine's state marker — `geneseed loop init`'s mark
77
+ * that a loop was actually LAUNCHED here, not merely that the branch is named `loop/...`,
78
+ * which anyone can type. `lstatSync` (never `statSync`, which follows a link) must find an
79
+ * ordinary file: a FIFO would block the read, and a symlink could point anywhere outside the
80
+ * repo — either makes "LOOP.md says so" a claim this reader cannot stand behind. Read at
81
+ * most the first 64 KB: the marker is not on the first line (it follows the title and the
82
+ * requirement), so it must appear within those 64 KB — a huge LOOP.md (notes, history) must
83
+ * never make this check slow.
84
+ */
85
+ export function loopLaunched(root) {
86
+ let fd;
87
+ try {
88
+ const p = path.join(root, 'LOOP.md');
89
+ if (!lstatSync(p).isFile()) return false;
90
+ fd = openSync(p, 'r');
91
+ const buf = Buffer.alloc(65536);
92
+ const n = readSync(fd, buf, 0, buf.length, 0);
93
+ return buf.toString('utf8', 0, n).includes(LOOP_STATE_MARKER);
94
+ } catch {
95
+ return false;
96
+ } finally {
97
+ if (fd !== undefined) { try { closeSync(fd); } catch { /* already gone */ } }
98
+ }
99
+ }