geneseed 3.11.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 (101) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +3 -3
  3. package/SHIPPED.md +2 -2
  4. package/adapters/opencode/plugins/geneseed-guard.js +157 -2
  5. package/bin/geneseed-cli.mjs +4 -0
  6. package/docs/concepts/foreman-mode.md +1 -1
  7. package/docs/concepts/loops.md +165 -0
  8. package/docs/extending.md +5 -0
  9. package/docs/guides/choose-your-setup.md +14 -0
  10. package/docs/guides/install.md +1 -1
  11. package/docs/reference/glossary.md +4 -0
  12. package/docs/reference/opencode-plugins.md +3 -1
  13. package/docs/web-ui.md +12 -1
  14. package/harness.config.json +1 -1
  15. package/js/README.md +24 -2
  16. package/js/build/driver.mjs +14 -8
  17. package/js/build/generate.mjs +10 -6
  18. package/js/build/render.mjs +5 -0
  19. package/js/build/source.mjs +5 -1
  20. package/js/cli-table.json +235 -0
  21. package/js/hosts/gitref.mjs +99 -0
  22. package/js/hosts/hooks.mjs +88 -1
  23. package/js/hosts/installs.mjs +21 -0
  24. package/js/inspect/checks-authoring.mjs +10 -0
  25. package/js/inspect/diff.mjs +3 -0
  26. package/js/inspect/doctor.mjs +2 -1
  27. package/js/inspect/inventory.mjs +1 -0
  28. package/js/inspect/status.mjs +1 -1
  29. package/js/loop/catalog.mjs +116 -0
  30. package/js/loop/cli.mjs +151 -0
  31. package/js/loop/graph.mjs +145 -0
  32. package/js/loop/score.mjs +143 -0
  33. package/js/loop/state.mjs +322 -0
  34. package/js/maintain/migrate.mjs +4 -2
  35. package/js/web/actions.mjs +9 -3
  36. package/js/web/api.mjs +38 -2
  37. package/js/web/daemon.mjs +43 -5
  38. package/js/web/handler.mjs +1 -0
  39. package/js/web/jobs.mjs +3 -1
  40. package/js/web/server.mjs +5 -3
  41. package/package.json +1 -1
  42. package/registry.json +14 -0
  43. package/src/AGENT.md.tmpl +2 -0
  44. package/src/bricks/README.md +32 -0
  45. package/src/bricks/_template.md +32 -0
  46. package/src/bricks/apply.md +17 -0
  47. package/src/bricks/baseline-green.md +17 -0
  48. package/src/bricks/bruno-test.md +16 -0
  49. package/src/bricks/ci-fix.md +17 -0
  50. package/src/bricks/deps-audit.md +14 -0
  51. package/src/bricks/docs-update.md +15 -0
  52. package/src/bricks/identify.md +20 -0
  53. package/src/bricks/lint.md +14 -0
  54. package/src/bricks/plan.md +17 -0
  55. package/src/bricks/reproduce.md +16 -0
  56. package/src/bricks/review.md +17 -0
  57. package/src/bricks/security-scan.md +17 -0
  58. package/src/bricks/test.md +14 -0
  59. package/src/doctrines/process.md +11 -8
  60. package/src/loops/bugfix.json +22 -0
  61. package/src/loops/feature.json +21 -0
  62. package/src/loops/refactor.json +20 -0
  63. package/src/skills/brick-forge.md +46 -0
  64. package/src/skills/geneseed.md +1 -1
  65. package/src/skills/loop.md +99 -0
  66. package/themes/_TEMPLATE.json +2 -0
  67. package/themes/biker.json +2 -0
  68. package/themes/commentator.json +2 -0
  69. package/themes/cyberpunk.json +2 -0
  70. package/themes/gamer.json +2 -0
  71. package/themes/imperial.json +2 -0
  72. package/themes/joker.json +2 -0
  73. package/themes/marvin.json +2 -0
  74. package/themes/mean.json +2 -0
  75. package/themes/military.json +2 -0
  76. package/themes/neutral.json +2 -0
  77. package/themes/pirate.json +2 -0
  78. package/themes/sports.json +2 -0
  79. package/themes/verstappen.json +2 -0
  80. package/themes/wizard.json +2 -0
  81. package/web/dist/assets/{Activity-C52LEb-2.js → Activity-GaaZWXTY.js} +1 -1
  82. package/web/dist/assets/{ActivityDetail-CjNLRvXg.js → ActivityDetail-4t7d45Du.js} +1 -1
  83. package/web/dist/assets/{Dashboard-B6_sSCHi.js → Dashboard-CYn7Tp16.js} +2 -2
  84. package/web/dist/assets/{Docs-Dj7BmCSA.js → Docs-BeFKLg2T.js} +1 -1
  85. package/web/dist/assets/{GreenhouseView-BALwjQsA.js → GreenhouseView-C6ibc8IE.js} +1 -1
  86. package/web/dist/assets/Installs-DRYjYnxs.js +2 -0
  87. package/web/dist/assets/{JournalView-JpAQ8Hqe.js → JournalView-1HbU1mfg.js} +1 -1
  88. package/web/dist/assets/{Laws-DK7eCWSm.js → Laws-DFoG8JWB.js} +1 -1
  89. package/web/dist/assets/{Library-CoamHelE.js → Library-DDjgNwz_.js} +1 -1
  90. package/web/dist/assets/Loops-D9VEoO0F.js +1 -0
  91. package/web/dist/assets/{OperatorHudView-DVx1HlBx.js → OperatorHudView-Bs7paWmh.js} +1 -1
  92. package/web/dist/assets/{Personal-eaXb1zcj.js → Personal-BY5n4gTw.js} +1 -1
  93. package/web/dist/assets/Tabs-ho-EYD8i.js +1 -0
  94. package/web/dist/assets/{api-Dc8f3ca-.js → api-CyMRbU2x.js} +1 -1
  95. package/web/dist/assets/{drawable-Du_PZrQj.js → drawable-CLS9drMb.js} +1 -1
  96. package/web/dist/assets/{index-Bz_KMRDH.css → index-CX_hXMi5.css} +1 -1
  97. package/web/dist/assets/index-Cjr7XCA3.js +2 -0
  98. package/web/dist/index.html +3 -3
  99. package/web/dist/assets/Installs-BbffDQm3.js +0 -2
  100. package/web/dist/assets/Tabs-BmGtt8E2.js +0 -1
  101. package/web/dist/assets/index-CNFH7o55.js +0 -2
package/CHANGELOG.md CHANGED
@@ -8,6 +8,45 @@ label. For the capability ↔ spec map, see [SHIPPED.md](SHIPPED.md).
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [3.12.0] — 2026-10-03
12
+
13
+ **Upgrading:** re-emit after installing (`geneseed rebuild-all`) to get the `loop` and
14
+ `brick-forge` skills, the loop-branch git-gate rule and the amended consent doctrine; then
15
+ `geneseed web restart` for the Loops page.
16
+
17
+ ### Added
18
+ - **Loops** — work a requirement in small validated iterations on a `loop/*` branch. A loop is a
19
+ graph of reusable **bricks** (13 shipped in `src/bricks/`; templates `bugfix`, `refactor`,
20
+ `feature` in `src/loops/`), or a graph the model composes from the catalogue. Your own bricks
21
+ live in `$XDG_CONFIG_HOME/geneseed/` or a repo's `.geneseed/`, and override shipped ones by name.
22
+ - **`geneseed loop check|init|next|score|record|decide`** — a deterministic state machine: the
23
+ model only reports outcomes. Each iteration's risk is the max of its declared actions' weights,
24
+ re-scored on the real diff before commit; `LOOP.md` holds the state and every iteration commit
25
+ carries `Loop-*` trailers. Ceilings: 20 iterations, 2 empty diffs, one re-split of an exhausted
26
+ inner ring.
27
+ - **`--trust prudent|balanced|aggressive`** build axis (default balanced, also in the console's
28
+ install form) — how much a loop may do before it stops: silent (trailers only), soft (listed for
29
+ review at the end), or blocking (the run ends and `geneseed loop decide` resumes it).
30
+ - **`loop` and `brick-forge` skills.**
31
+ - **Console: Loops page** — templates drawn as rings (inner rings for apply ⇄ test and review ⇄
32
+ fix, a gate before the first code-changing step), every brick with its source and origin, and
33
+ catalogue problems.
34
+ - **Documentation for loops** (`docs/concepts/loops.md`): bricks, templates and the three
35
+ origins, the trust presets and the three validation levels, `LOOP.md` and its trailers, the
36
+ `loop/*` branch exemption and why merging always asks, and the `geneseed loop` actions table.
37
+ Cross-linked from `foreman-mode.md`; `glossary.md` gains `brick`, `iteration`, `loop` and
38
+ `trust preset`.
39
+
40
+ ### Changed
41
+ - **Consent Before Push**: a launched loop's batch covers commit and push on its own `loop/*`
42
+ branch, in a fixed set of command forms (`git commit -F <file>`, `git push <remote> HEAD:<loop
43
+ branch>`); anything else asks, the final commit asks, and merging never belongs to the batch.
44
+ Enforced by the git gate on Claude Code, Bob and OpenClaude, and by `geneseed-guard` on OpenCode.
45
+
46
+ ### Fixed
47
+ - **`geneseed web stop|restart|status --port N`** act on that port only; a stop aimed at a test
48
+ instance no longer shuts down the console recorded on another port (#175).
49
+
11
50
  ## [3.11.0] — 2026-10-03
12
51
 
13
52
  **Upgrading:** re-emit after installing (`geneseed rebuild-all`) — it also repairs a dead hook
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  [![Node >= 22.3](https://img.shields.io/badge/node-%3E%3D22.3-5fa04e)](package.json)
11
11
  [![zero dependencies](https://img.shields.io/badge/deps-zero-success)](package.json)
12
12
  [![Themes](https://img.shields.io/badge/themes-14-9cf)](themes/)
13
- [![Skills](https://img.shields.io/badge/skills-54-blueviolet)](src/skills/)
13
+ [![Skills](https://img.shields.io/badge/skills-56-blueviolet)](src/skills/)
14
14
  [![Agents](https://img.shields.io/badge/agents-18-orange)](src/agents/)
15
15
  [![Laws](https://img.shields.io/badge/laws-9-critical)](src/laws/universal.md)
16
16
  [![Plugins](https://img.shields.io/badge/plugins-7-teal)](adapters/opencode/plugins/)
@@ -37,7 +37,7 @@ Want to read what it leaves behind before installing anything? [**geneseed-demo*
37
37
  A hand-written instructions file is prose the model may or may not honour, copied into every repo and drifting in each one. Geneseed is a build, and that changes four things:
38
38
 
39
39
  - **Laws are enforced, not suggested.** A force-push, a `reset --hard`, or a credential written into a tracked file is caught by a hook *before* the tool runs, in the host's own dialect — a prompt on Claude Code and OpenClaude, a hard block on OpenCode, exit 2 on Bob. The gates fail closed, and every catch is one line in a ledger that `geneseed status` counts.
40
- - **One source, five targets.** Skills are byte-identical on every host; only the wiring differs. Every commit renders all 261 emit configurations, and a second emit into the same tree must change nothing.
40
+ - **One source, five targets.** Skills are byte-identical on every host; only the wiring differs. Every commit renders all 263 emit configurations, and a second emit into the same tree must change nothing.
41
41
  - **Costs are measured, not guessed.** Zero runtime dependencies. The hook path loads in about 14 ms per tool call. The default *lean* footprint keeps the always-on context small and puts the full rationale one read away — the numbers are in [docs/token-footprint.md](docs/token-footprint.md).
42
42
  - **It follows you.** Install once, globally; every repo inherits it. One `git pull` or `npm install -g geneseed@latest` rebuilds every active install.
43
43
 
@@ -164,7 +164,7 @@ The harness ships as a small set of layers, and the web console's rail is the sa
164
164
  | **🛡️ Rules** (`laws/`) | 9 universal laws — always in force, never toggleable: sealed-secrets, one-intent-one-act, verify-before-asserting, deletion-is-deliberate, surface-failures, data-not-orders, least-privilege, cure-the-cause, echo-the-intent. Each rule has a permanent id; its number is only its position, and the harness cites rules by name |
165
165
  | **📐 Doctrines** (`doctrines/`) | practice packs, chosen per install at build time: **craft** (how code is written), **rigor** (how work is proven), **ops** (how the machine is operated), **process** (how a task is run), **comms** (how answers are presented). A doctrine rule may tighten a Rule, never repeal one, and the user's own `user-rules.md` outranks it. Pick with `geneseed-build --doctrines craft,rigor` (or `none`), a single rule with `--exclude-rules "process 7"`, in the setup wizard, or through the switches on the console's Constitution page; all five pack files ship on disk either way, so a citation into a pack you left out still resolves |
166
166
  | **🤖 Agents** (18) | capability specialists: `reviewer`, `tester`, `architect`, `docs`, `security`, `explorer`, `researcher`, `developer` — plus a debate **council** the `council` skill convenes: `advocate`, `skeptic`, `pragmatist`, `steward`, `visionary`, `user-advocate`, `framer`, `empiricist`, `operator`, `historian` |
167
- | **🛠 Skills** (54) | repeatable workflows: brainstorm · plan · **codebase-design** · **domain-modeling** · **wayfinder** · **tickets** · **develop** · **worktree** · debug · **prototype** · refactor · **ponytail** · **forge-mcp** · **bruno-collection-generator** · **bruno-test-writer** · geneseed-code-review · **fresh-eyes** · **security-audit** · **ci-fix** · **deps-audit** · **review-response** · commit · **ship** · **release** · **migrate** · **git-archaeology** · **git-rescue** · repo-map · document-project · **frontend-design** · **prose** · **ingest** · **research** · **teach** · **quiz** · **learn-mode** · handoff · roast-me · **council** · parallel-agents · **workflow** · **wiki** · **geneseed** · **rule** · **profile** · **consolidate-memory** · **skill-forge** · **opencode-theme** · **herdr** · **pipeline** · **daydream** · **react-view-transitions** · **token-report** · **explain-changes** |
167
+ | **🛠 Skills** (56) | repeatable workflows: brainstorm · plan · **codebase-design** · **domain-modeling** · **wayfinder** · **tickets** · **develop** · **worktree** · debug · **prototype** · refactor · **ponytail** · **forge-mcp** · **bruno-collection-generator** · **bruno-test-writer** · geneseed-code-review · **fresh-eyes** · **security-audit** · **ci-fix** · **deps-audit** · **review-response** · commit · **ship** · **release** · **migrate** · **git-archaeology** · **git-rescue** · repo-map · document-project · **frontend-design** · **prose** · **ingest** · **research** · **teach** · **quiz** · **learn-mode** · handoff · roast-me · **council** · parallel-agents · **workflow** · **wiki** · **geneseed** · **rule** · **profile** · **consolidate-memory** · **skill-forge** · **opencode-theme** · **herdr** · **pipeline** · **loop** · **brick-forge** · **daydream** · **react-view-transitions** · **token-report** · **explain-changes** |
168
168
  | **🔌 Plugins** (OpenCode) | `geneseed-context` injects project docs *and your machine wiki* every session (and across compaction); `geneseed-learn` distils memory at session end; `geneseed-guard` enforces the safety Laws and protected wiki folders at the tool boundary; `geneseed-workflow` registers the `workflow` tool that runs saved orchestration scripts; `geneseed-notify` sends a native OS notification when a long run finishes; `geneseed-ponytail` holds a minimal-code mode (`/ponytail lite\|full\|ultra\|off`), opt-in, injecting the laziest-that-works ruleset every turn so it doesn't drift; `geneseed-activity` streams what each session is doing to the web console's Activity view |
169
169
  | **🧠 Memory** (`memory/`) | one-fact-per-file durable knowledge, indexed by `MEMORY.md` (git-ignored, personal) |
170
170
  | **📓 Notebook** (`notebook/`) | the agent's sovereign space — any medium (code, tools, data, notes), self-ruled via a seed-once charter, always git-ignored; only its `.gitignore` is build-asserted |
package/SHIPPED.md CHANGED
@@ -20,7 +20,7 @@ historical references, not files distributed with the repo.
20
20
  > **On version numbers.** [`harness.config.json`](harness.config.json)'s `version` is a
21
21
  > human-readable label only. The canonical identity of an installed harness is the
22
22
  > **source fingerprint** stamped in `.geneseed-version` (see `geneseed version`), not the
23
- > string. Current label: **3.11.0**.
23
+ > string. Current label: **3.12.0**.
24
24
 
25
25
  ## Capabilities
26
26
 
@@ -29,7 +29,7 @@ Every row below is present in the tree today; "Spec" names the design record.
29
29
  | Capability | What it is | Spec |
30
30
  | --- | --- | --- |
31
31
  | **Themed generator** | One neutral `src/` → 14 themed bundles via `geneseed-build` (zero dependencies); structure stays theme-independent | [DESIGN.md](DESIGN.md) |
32
- | **Laws / Agents / Skills / Memory / Notebook** | 9 laws, 18 agents, 54 skills, the memory convention, and the agent's sovereign notebook | `2026-06-11-notebook-sovereign-space` · `2026-06-10-notebook-agent-own-space` |
32
+ | **Laws / Agents / Skills / Memory / Notebook** | 9 laws, 18 agents, 56 skills, the memory convention, and the agent's sovereign notebook | `2026-06-11-notebook-sovereign-space` · `2026-06-10-notebook-agent-own-space` |
33
33
  | **Three-tier constitution** | The governance surface splits into an always-on **Ontology** (Telos, Evidence, Decisions, Conduct — the Pact absorbed into Telos), the nine always-on **Invariants**, and five toggleable **Doctrine** packs (craft, rigor, ops, process, comms) addressed `<pack> <n>`. Every rule carries a permanent id; its number is its position, computed at render time, and a citation renders as the rule's principle name. Packs are chosen at build time with `--doctrines` or the setup wizard; the whole catalogue ships on disk either way, so a citation into an inactive pack still resolves. Precedence, in order: Ontology + Invariants, then the user's `user-rules.md`, then the active Doctrines, then `PROFILE.md`. `doctor`'s `constitutionProblems` gates rule ids (declared, unique, never a retired one), the per-rule Principle column, every `{{LAW:<id>}}` / `{{DOCTRINE:<id>}}` citation in `src/`, and the rule that an always-on tier may never cite a toggleable one | `2026-08-19-three-tier-constitution-design` |
34
34
  | **`doctor`** | One check: unresolved tokens, dead/non-hermetic links, theme-key parity, authoring gates, rendered-bundle drift | `2026-06-07-theme-aware-doctor-diff` |
35
35
  | **`diff` + improvements export** | Deployed-vs-source drift report; export an `improvements.md` back-port | `2026-06-12-improvements-export` |
@@ -16,8 +16,22 @@
16
16
  //
17
17
  // Install: dropped into the plugins dir by `build --emit opencode[-global]` (the *.js
18
18
  // glob), exactly like the context and learn plugins. Errors never break a tool call.
19
+ //
20
+ // `permission.ask` — the loop/* exemption (Consent Before Push). OpenCode's static
21
+ // `permission.bash` globs (`git commit*`/`git push*` = "ask", written by
22
+ // js/hosts/settings.mjs) cannot see which branch is checked out — they are a build-time
23
+ // string match with no request-time context. This hook is the ONLY seam where Geneseed can
24
+ // look at the live branch, and it only DOWNGRADES an ask to allow: only a fixed set of
25
+ // commit/push forms (see `loopExempt` below, the twin of js/hosts/hooks.mjs's) on a `loop/*`
26
+ // branch a loop was actually LAUNCHED on — not just named `loop/...`. It is a WHITELIST, not
27
+ // a blacklist: a false ask costs one prompt, a false allow publishes. It never raises the
28
+ // bar: anything not on the whitelist, anything off a launched loop branch, a status this hook
29
+ // did not itself leave as the default "ask", or any non-bash permission leaves `output.status`
30
+ // untouched and OpenCode's own ask (or another hook's deny) stands. A host version that never
31
+ // fires `permission.ask` simply never reaches this hook — the static ask still fires, so the
32
+ // failure mode is one extra prompt, never a silent allow.
19
33
 
20
- import { promises as fs } from "node:fs"
34
+ import { promises as fs, existsSync, statSync, lstatSync, readFileSync, openSync, readSync, closeSync } from "node:fs"
21
35
  import * as path from "node:path"
22
36
  import { fileURLToPath } from "node:url"
23
37
  import { homedir } from "node:os"
@@ -267,7 +281,131 @@ function ruleStoreTarget(p) {
267
281
  // substring stance: over-matching the class is harmless (see WRITE_TOOLS note).
268
282
  const WIKI_MUTATE_TOOLS = [...WRITE_TOOLS, "delete", "remove", "rename", "move", "trash"]
269
283
 
270
- export const GeneseedGuard = async () => {
284
+ // ---- the loop/* exemption (Consent Before Push) --------------------------------
285
+ // Twin of js/hosts/gitref.mjs (currentBranch/loopLaunched) and js/hosts/hooks.mjs's
286
+ // git-gate (the rest) — keep in step. Node's native sync `fs` (not the `fs.promises`
287
+ // import above): a worktree's `.git` is read the same way the Claude/Bob hook reads it, so
288
+ // the two decide the exemption identically. This plugin cannot import from `js/` at all
289
+ // (it is copied whole into an OpenCode install, outside this repo's module graph), so it
290
+ // keeps its own standalone copy rather than the single shared owner the Node side has.
291
+
292
+ const GIT_GATE_RE = /\bgit\b[^\n]*\b(?:commit|push)\b/
293
+
294
+ // No newline, backtick, or `$(` substitution anywhere, no `<`/`>` redirect, and no single
295
+ // `&` (a shell runs `a & b` as two commands exactly like `a && b`) — these can smuggle a
296
+ // second command (or a shared-branch target) past every check below. `(?<!&)&(?!&)` matches
297
+ // a lone `&` without matching either half of a doubled `&&`.
298
+ const UNSAFE_CHARS_RE = /[\n`<>]|\$\(|(?<!&)&(?!&)/
299
+
300
+ // The only three git shapes a loop's own automation ever needs — see js/hosts/hooks.mjs's
301
+ // identically-named constants for the full rationale. The token class `[\w./:@^~=+,-]`
302
+ // excludes quotes, braces, `!`, `*`, `$` and `&` — a brace/glob expansion is exactly how a
303
+ // shell turns one whitelisted-looking token into several unknown ones, which is also why the
304
+ // `-F`/`--file` path uses this same class rather than the looser `[^\s'"]+` it once did: that
305
+ // loose class let `git commit -F {m,--amend}` brace-expand into an amend of the last commit.
306
+ const ARG_RE = "[\\w./:@^~=+,-]+"
307
+ const SEG_ADD_RE = new RegExp(`^git\\s+add(\\s+${ARG_RE})*$`)
308
+ const SEG_COMMIT_RE = new RegExp(`^git\\s+commit(\\s+-q)?\\s+(-F\\s+|--file[=\\s])${ARG_RE}(\\s+-q)?$`)
309
+ const SEG_READONLY_RE = new RegExp(`^git\\s+(status|diff|log|rev-parse|show)(\\s+${ARG_RE})*$`)
310
+
311
+ /**
312
+ * `git push`, exactly: `[-u|--set-upstream] <remote> HEAD:<branch>` or
313
+ * `HEAD:refs/heads/<branch>` — the ONLY exempt form, same as js/hosts/hooks.mjs's
314
+ * `pushSegmentOk`. An explicit refspec is immune to `push.default`/`remote.*.push`
315
+ * redirection and refuses to coexist with a mirror/`+`/`:`-prefixed form; nothing else is.
316
+ */
317
+ function pushSegmentOk(seg, branch) {
318
+ const m = /^git\s+push(?:\s+(.*))?$/.exec(seg)
319
+ if (!m) return false
320
+ const rest = (m[1] || "").trim()
321
+ if (!rest) return false
322
+ const tokens = rest.split(/\s+/)
323
+ let i = 0
324
+ if (tokens[i] === "-u" || tokens[i] === "--set-upstream") i += 1
325
+ const remote = tokens[i]
326
+ if (remote === undefined || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(remote)) return false
327
+ i += 1
328
+ const ref = tokens[i]
329
+ i += 1
330
+ if (ref === undefined || i !== tokens.length) return false
331
+ return ref === `HEAD:${branch}` || ref === `HEAD:refs/heads/${branch}`
332
+ }
333
+
334
+ const LOOP_STATE_MARKER = "<!-- loop-state:begin -->"
335
+
336
+ /**
337
+ * Whether `<root>/LOOP.md` carries the engine's state marker — read at most 64 KB.
338
+ * `lstatSync` (never `statSync`, which follows a link) must find an ordinary file: a FIFO
339
+ * would block the read, a symlink could point anywhere outside the repo.
340
+ */
341
+ function loopLaunched(gitRoot) {
342
+ let fd
343
+ try {
344
+ const p = path.join(gitRoot, "LOOP.md")
345
+ if (!lstatSync(p).isFile()) return false
346
+ fd = openSync(p, "r")
347
+ const buf = Buffer.alloc(65536)
348
+ const n = readSync(fd, buf, 0, buf.length, 0)
349
+ return buf.toString("utf8", 0, n).includes(LOOP_STATE_MARKER)
350
+ } catch {
351
+ return false
352
+ } finally {
353
+ if (fd !== undefined) { try { closeSync(fd) } catch { /* already gone */ } }
354
+ }
355
+ }
356
+
357
+ /**
358
+ * The branch checked out in `cwd`, and the directory holding `.git` for it, read from
359
+ * .git/HEAD without spawning git — a worktree's `.git` is a FILE naming its gitdir, so both
360
+ * shapes are followed. `{ branch: null, root: null }` on anything unexpected (detached HEAD,
361
+ * no repo, unreadable): the caller then leaves the ask alone.
362
+ */
363
+ function currentBranch(cwd) {
364
+ try {
365
+ let dir = path.resolve(cwd)
366
+ for (;;) {
367
+ const dotgit = path.join(dir, ".git")
368
+ if (existsSync(dotgit)) {
369
+ let gitdir = dotgit
370
+ if (statSync(dotgit).isFile()) {
371
+ const m = /^gitdir:\s*(.+?)\s*$/m.exec(readFileSync(dotgit, "utf8"))
372
+ if (!m) return { branch: null, root: null }
373
+ gitdir = path.resolve(dir, m[1])
374
+ }
375
+ const ref = /^ref:\s*refs\/heads\/(.+?)\s*$/m.exec(readFileSync(path.join(gitdir, "HEAD"), "utf8"))
376
+ return { branch: ref ? ref[1] : null, root: dir }
377
+ }
378
+ const up = path.dirname(dir)
379
+ if (up === dir) return { branch: null, root: null }
380
+ dir = up
381
+ }
382
+ } catch {
383
+ return { branch: null, root: null }
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Consent Before Push's loop/* exemption — the twin of js/hosts/hooks.mjs's `loopExempt`.
389
+ * True only when the command carries none of the unsafe characters, every `&&`/`;`/`||`/`|`
390
+ * segment is one of the three whitelisted git shapes (every push segment also passing
391
+ * `pushSegmentOk`), and the branch checked out in `cwd` starts with `loop/` AND its git root
392
+ * carries a launched LOOP.md.
393
+ */
394
+ function loopExempt(command, cwd) {
395
+ if (typeof command !== "string" || UNSAFE_CHARS_RE.test(command)) return false
396
+ const { branch, root } = currentBranch(cwd)
397
+ if (!branch || !branch.startsWith("loop/")) return false
398
+ if (!root || !loopLaunched(root)) return false
399
+ const segments = command.split(/&&|\|\||[;|]/).map((s) => s.trim()).filter(Boolean)
400
+ if (!segments.length) return false
401
+ return segments.every((seg) => SEG_ADD_RE.test(seg) || SEG_COMMIT_RE.test(seg)
402
+ || SEG_READONLY_RE.test(seg) || pushSegmentOk(seg, branch))
403
+ }
404
+
405
+ export const GeneseedGuard = async (ctx) => {
406
+ // The session's working directory, the same resolution the context plugin uses — a
407
+ // worktree session's `.git` is a FILE, which `currentBranch` already follows.
408
+ const root = () => ctx?.worktree || ctx?.directory || process.cwd()
271
409
  return {
272
410
  "tool.execute.before": async (input, output) => {
273
411
  if (OFF) return
@@ -318,6 +456,23 @@ export const GeneseedGuard = async () => {
318
456
  log(`inspect error (ignored): ${err?.message ?? err}`)
319
457
  }
320
458
  },
459
+ "permission.ask": async (input, output) => {
460
+ // Never throw from a permission hook — leave `output.status` untouched on any
461
+ // error, exactly as a crashed check here must default to the static ask standing.
462
+ try {
463
+ if (OFF) return
464
+ // Never loosen anything but the plain "ask" this hook itself would otherwise leave
465
+ // alone — a deny (or any other status another hook already set) is never overwritten.
466
+ if (output?.status && output.status !== "ask") return
467
+ if (input?.type !== "bash") return
468
+ // ONLY `input.metadata?.command` — no pattern/title fallback. A value this hook did
469
+ // not itself read in full is a value it must not reason about.
470
+ const command = input?.metadata?.command
471
+ if (typeof command !== "string" || !command || !GIT_GATE_RE.test(command)) return
472
+ if (await sovereignBypass(root())) return
473
+ if (loopExempt(command, root())) output.status = "allow"
474
+ } catch { /* leave output.status untouched */ }
475
+ },
321
476
  }
322
477
  }
323
478
 
@@ -149,6 +149,10 @@ const VERBS = {
149
149
  catalog: () => import('../js/build/catalog.mjs').then((m) => m.cmdCatalog),
150
150
  mcp: () => import('../js/hosts/mcp.mjs').then((m) => m.cmdMcp),
151
151
  memory: () => import('../js/maintain/memory.mjs').then((m) => m.cmdMemory),
152
+ // Task 5. A fourth native verb: there was never a Python subparser for a state machine that
153
+ // lives entirely inside this port, so — like the three above — it has no recorded cell to
154
+ // compare against, only the absolute gates named beside it in `NATIVE`.
155
+ loop: () => import('../js/loop/cli.mjs').then((m) => m.cmdLoop),
152
156
  };
153
157
 
154
158
  function die(code, msg) {
@@ -9,7 +9,7 @@ link: {"hash": "#/harness", "label": "Set the mode in Harness →"}
9
9
 
10
10
  > **You already know this:** a tech lead with a team. Quick questions get answered on the spot; a real feature becomes a ticket that someone builds on a branch, and the lead reviews the result before it is merged.
11
11
 
12
- Mode is chosen at build time, so it does not drift mid-session, and kept across every rebuild and re-theme. It is separate from [posture](collaboration.md) (the relationship register), the doctrine packs ([Rules](rules.md)) and [footprint](footprint.md).
12
+ Mode is chosen at build time, so it does not drift mid-session, and kept across every rebuild and re-theme. It is separate from [posture](collaboration.md) (the relationship register), the doctrine packs ([Rules](rules.md)) and [footprint](footprint.md). It is also separate from a [loop](loops.md) — a pipeline is a one-shot, uncommitted crew for a single task; a loop is a long-running state machine that commits each validated iteration on its own branch.
13
13
 
14
14
  ### direct — the default
15
15
 
@@ -0,0 +1,165 @@
1
+ ---
2
+ group: concepts
3
+ order: 11
4
+ title: "Loops"
5
+ kind: "concept"
6
+ link: {"hash": "#/docs/reference/cli", "label": "geneseed loop --help →"}
7
+ ---
8
+ A **loop** is a requirement worked as small, validated, committed iterations instead of one long
9
+ turn. `geneseed loop` is the state machine: it decides every transition, ceiling, score and stop,
10
+ and hands the agent one node at a time to run. The `loop` [skill](skills.md) is the only thing
11
+ that talks to it — the agent runs the node it is given and reports the outcome, never its own
12
+ reading of the graph.
13
+
14
+ > **You already know this:** a checklist a tech lead walks through on a ticket, where some steps
15
+ > need a sign-off before continuing and others don't. The lead doesn't improvise the order; the
16
+ > checklist decides it, and only the risky steps stop for you.
17
+
18
+ ## The ring
19
+
20
+ A loop is a graph of **bricks** — one node each — wired by **edges**, one per outcome a brick can
21
+ report. One cycle through the graph back to its head is an **iteration**; the graph's `loops`
22
+ array names every cycle that is allowed to exist, each with a `max` number of times a node inside
23
+ it may be re-entered. Picture it as rings: the outer ring is the iteration itself (plan, apply,
24
+ test, review, back to plan), and inner rings sit inside it for retries (apply failed its test, try
25
+ `apply` again without going all the way back around). The three shipped templates —
26
+ `bugfix`, `feature`, `refactor` — all nest a short `apply`↔`test` retry ring inside the iteration
27
+ ring, and `bugfix` and `feature` nest a second, wider `apply`↔`test`↔`review` ring
28
+ (`review-fix`) around that.
29
+
30
+ Work closes into a commit whenever the graph re-enters the iteration loop's head — so any setup
31
+ work before the loop starts (reproducing a bug, say) is its own unit, numbered iteration 0, kept
32
+ out of iteration 1's diff. For the same reason the iteration has exactly one way in: its
33
+ **head** is the first node listed in the iteration loop (normally `identify`), and every edge that
34
+ enters the iteration from outside it — and `start`, when `start` is inside it — must target that
35
+ head. `geneseed loop check` refuses a graph that enters anywhere else.
36
+
37
+ **A node's re-entry budget is not each loop's `max` added up.** Every node inside one or more
38
+ inner loops gets a single shared counter: `1 + the largest max among the inner loops that contain
39
+ it`. A node in both a `max: 5` and a `max: 3` loop gets 6 re-entries total, not 8 — the loops don't
40
+ stack, the larger one wins and the `+1` is the node's own first run. A node inside no inner loop at
41
+ all gets exactly 1 — no re-entry — which is also why **every cycle inside an iteration must be a
42
+ named inner loop** in the graph JSON: `geneseed loop check` (and `loop init`, which runs the same
43
+ check) refuses a graph with an undeclared cycle inside an iteration, before a single node runs.
44
+
45
+ ## Bricks, templates, and the three origins
46
+
47
+ A **brick** is a markdown file: YAML-ish frontmatter (`name`, `description`, `effect` — `read` or
48
+ `mutate` — exactly one of `agent` or `skill`, and `outcomes`) plus a 4-10 line prose body. The
49
+ model never sees the file; `geneseed loop next` hands it the body as the prompt. A **loop
50
+ template** (`bugfix`, `feature`, `refactor`, or one composed by hand) is the graph JSON that wires
51
+ bricks together.
52
+
53
+ Both are looked up across three origins, each able to override the one before it **by name**:
54
+
55
+ | origin | path | who writes it |
56
+ |---|---|---|
57
+ | shipped | `src/bricks/`, `src/loops/` | Geneseed itself — read by the CLI from its own source, never emitted into an install |
58
+ | global | `$XDG_CONFIG_HOME/geneseed/{bricks,loops}` (default `~/.config/geneseed/…`) | the user, for every project on this machine |
59
+ | project | `<repo>/.geneseed/{bricks,loops}` | a team, committed and shared |
60
+
61
+ Precedence is project > global > shipped. A project or global brick named `apply` replaces the
62
+ shipped `apply` everywhere a template references it, without the template changing.
63
+ `geneseed loop check` lists every override it finds, never silently.
64
+
65
+ A per-install skill exclusion is invisible to the CLI — it has no way to know which skills your
66
+ install carries. The `loop` skill covers the gap itself: when composing a graph it skips a brick
67
+ whose `skill` it doesn't have.
68
+
69
+ To add or adapt a brick, use the `brick-forge` skill — ask it, or see
70
+ [the bricks README](https://github.com/geneseed-dev/geneseed/blob/main/src/bricks/README.md) in
71
+ the repository.
72
+
73
+ ## Risk, trust, and the three validation levels
74
+
75
+ Before a `mutate` brick runs, the loop scores the card the brick's setup declared: one risk weight
76
+ per action kind it named, and the score is the **max**, not a sum — one contract change outweighs
77
+ ten trivial renames, and ten trivial renames never add up to one. After the brick runs, the loop
78
+ re-scores against the real diff, and the diff can only raise the score, never lower it:
79
+
80
+ - a file touched outside the declared write set raises the score to at least **api** (`0.8`);
81
+ - more than 20 deleted lines raises it to at least the **delete** weight;
82
+ - touching a file listed in the loop's `contracts` raises it to at least the **api** weight.
83
+
84
+ `LOOP.md` itself is excluded from every score and every diff check — it's the engine's own
85
+ bookkeeping, rewritten on every action, never part of the work being judged.
86
+
87
+ The score is checked against a **trust preset** — `--preset` on `geneseed loop init`, editable
88
+ afterward in `LOOP.md`'s `preset` field, applying from the next score on:
89
+
90
+ | preset | silent ≤ | soft ≤ | above soft |
91
+ |---|---|---|---|
92
+ | prudent | 0.1 | 0.3 | blocking |
93
+ | balanced (default) | 0.2 | 0.6 | blocking |
94
+ | aggressive | 0.4 | 0.8 | blocking |
95
+
96
+ Three levels follow from where the score lands:
97
+
98
+ - **silent** — nothing more than the commit trailers below.
99
+ - **soft** — listed under "to review" in the run's final summary; no notification during the run.
100
+ - **blocking** — the run ends. `LOOP.md` is written with `status: awaiting`, and a person has to
101
+ answer with `geneseed loop decide --verdict ok|no|amend`. Amending a verdict marks the decision
102
+ `blocking (amended)` in the trailers rather than silently rewriting it to something else.
103
+
104
+ ## `LOOP.md` and its trailers
105
+
106
+ `LOOP.md`, at the worktree root, is both the human-readable record (title, requirement, and the
107
+ rule that you may edit `preset` or `contracts` there) and the engine's own JSON state block. Every
108
+ closing commit carries seven trailers, read straight off that state:
109
+
110
+ ```
111
+ Loop-Iteration: <n>
112
+ Loop-Bricks: <every brick name visited this unit, deduplicated>
113
+ Loop-Risk-Declared: <score>
114
+ Loop-Risk-Actual: <score>
115
+ Loop-Threshold: <preset> <silent>/<soft>
116
+ Loop-Decision: silent | soft | blocking[ (amended)]
117
+ Loop-Tests: <what the loop was told about test state>
118
+ ```
119
+
120
+ ## The `loop/*` branch, and why merge always asks
121
+
122
+ A loop runs in its own worktree, on a branch named `loop/<slug>` — the `loop` skill creates both
123
+ with `git worktree add -b loop/<slug>` before `geneseed loop init` writes `LOOP.md` there.
124
+ While that branch is checked out and `LOOP.md` carries the engine's state marker, the git gate lets
125
+ exactly one commit/push form through without asking:
126
+
127
+ ```
128
+ git add -A && git commit -F <message_file> && git push [-u] <remote> HEAD:<loop branch>
129
+ ```
130
+
131
+ Nothing else is exempt — not `git commit -m`, not a quoted refspec, not a plain `git push` with no
132
+ explicit `HEAD:<ref>` — any other shape still asks. The exemption only ever covers a `loop/*`
133
+ branch, and never a shared one (`main`, `master`, `develop`, `development`, `release/*`,
134
+ `hotfix/*`). The **final** commit of a run — the one that removes `LOOP.md` on `$close` — is
135
+ outside the exemption too, because the loop is no longer running when it lands; that ask is your
136
+ consent to close it. **Merging a loop branch into anything else always asks, with no exception.**
137
+ The loop reports the branch to merge and the iterations that landed in "to review" — it never
138
+ merges for you.
139
+
140
+ ## `geneseed loop` actions
141
+
142
+ | action | what it does |
143
+ |---|---|
144
+ | `check` | lists templates and every brick (available, unavailable, overridden); validates one graph file (`--graph`) or one brick file (`--brick`) |
145
+ | `init` | writes a fresh `LOOP.md` from a template or graph file, under a title, requirement and trust preset |
146
+ | `next` | the only driver of the cycle — returns the next thing to do: run a node, score a card, answer an `awaiting`, or a terminal |
147
+ | `score` | scores a declared card (`--declared`) before a mutate node runs, or the real diff (`--diff`, numstat piped on stdin) before a commit |
148
+ | `record` | records a brick's reported outcome and advances the graph |
149
+ | `decide` | answers an `awaiting` validation with a verdict and a note |
150
+
151
+ Run `geneseed loop --help` for the full flag list.
152
+
153
+ ## Limits
154
+
155
+ - **No parallel nodes.** The graph runs one node at a time; there is no fan-out.
156
+ - **A per-install skill exclusion is invisible to the CLI** (see above) — the `loop` skill is
157
+ responsible for skipping bricks it can't run, not the engine.
158
+
159
+ ## Not foreman mode
160
+
161
+ A loop and a [foreman-mode](foreman-mode.md) pipeline are both "hand work off and keep going," but
162
+ they solve different problems. A pipeline is a one-shot crew for a single task, uncommitted until
163
+ you accept it. A loop is a long-running, self-scoring state machine that commits each validated
164
+ iteration on its own branch as it goes, and only stops for a person when the risk crosses a
165
+ threshold.
package/docs/extending.md CHANGED
@@ -748,6 +748,11 @@ handler falls back to `index.html` at 200 with `text/html`, and the browser repo
748
748
  "no running server recorded" and exits 0, and `web restart` starts a *second* daemon. Recovery is
749
749
  to kill every pid on the port, then `geneseed web start`.
750
750
 
751
+ `--port N` on `web stop|restart|status` names *which* daemon: the verb acts only if the record is
752
+ on N, and refuses (exit 1, nothing sent) when it names another port. A test instance on another
753
+ port is therefore stopped with `--port`, never with a bare `web stop` — that one stops whatever
754
+ the record names, usually the live console on 4747.
755
+
751
756
  ---
752
757
 
753
758
  ## 10 — What to settle before the next wave
@@ -52,6 +52,20 @@ geneseed build --emit opencode-global --mode direct # back to the default
52
52
 
53
53
  More in [Foreman mode](../concepts/foreman-mode.md).
54
54
 
55
+ ## Loop trust: how far a loop goes before it asks
56
+
57
+ The `loop` skill scores every change it validates and stops to ask once the score passes a threshold. `--trust` sets the preset the skill starts a loop with; the user can still name another one per loop.
58
+
59
+ - **prudent**: asks early, on anything past a rename.
60
+ - **balanced** *(default)*: logic changes and new files pass with a note; API changes and deletions stop.
61
+ - **aggressive**: only an architecture-level change stops the loop.
62
+
63
+ The preset lives in the `loop` skill only, so it costs nothing in the always-on instructions.
64
+
65
+ ```
66
+ geneseed build --emit opencode-global --trust prudent
67
+ ```
68
+
55
69
  ## Doctrine packs: which practices bind
56
70
 
57
71
  The harness's rules come in tiers (see [Rules](../concepts/rules.md)). The Ethos and the Rules are always on. The **doctrines** are practices rather than principles, and they ship as packs you choose:
@@ -66,7 +66,7 @@ From a clone, every `geneseed …` command on these pages is `./geneseed …` (W
66
66
  geneseed build --emit opencode-global --theme neutral
67
67
  ```
68
68
 
69
- `geneseed build` takes the same flags as the generator `geneseed-build`. `--emit` picks the host and its scope: `files`, `opencode`, `opencode-global`, `claude`, `claude-global`, `bob`, `bob-global`, `openclaude` or `openclaude-global`. A `-global` emit writes into the tool's own config directory. A project emit writes into the repo you name with `--out <repo> --root <repo>`. All the other flags (`--theme`, `--footprint`, `--posture`, `--mode`, `--doctrines`, `--exclude-rules`) are covered in [Choose your setup](choose-your-setup.md).
69
+ `geneseed build` takes the same flags as the generator `geneseed-build`. `--emit` picks the host and its scope: `files`, `opencode`, `opencode-global`, `claude`, `claude-global`, `bob`, `bob-global`, `openclaude` or `openclaude-global`. A `-global` emit writes into the tool's own config directory. A project emit writes into the repo you name with `--out <repo> --root <repo>`. All the other flags (`--theme`, `--footprint`, `--posture`, `--mode`, `--trust`, `--doctrines`, `--exclude-rules`) are covered in [Choose your setup](choose-your-setup.md).
70
70
 
71
71
  To test a build before writing anything, use `geneseed validate` with the same flags. See [Verify](verify.md).
72
72
 
@@ -26,6 +26,10 @@ Every word Geneseed uses that you would not find in a general programming glossa
26
26
  | Posture | — | the relationship register the agent works in (peer, mentor, expert, assistant, artisan) | pairing style |
27
27
  | Mode | — | how work gets executed — direct (the agent works every task itself) or foreman (substantial tasks spawn an isolated pipeline) | solo vs. delegating to a sub-team |
28
28
  | Footprint | — | how much of the Rules loads inline each turn (full vs lean) | eager vs. lazy loading |
29
+ | Loop | — | a requirement worked as small, validated, committed iterations on their own branch, driven by `geneseed loop` | a state machine over a ticket's steps |
30
+ | Iteration | — | one cycle of a loop back to its head; closes into its own commit | one sprint-of-one through a checklist |
31
+ | Brick | — | one node of a loop graph: a prompt for one agent or skill, reporting one of a declared set of outcomes | a step in a runbook |
32
+ | Trust preset | — | the loop's risk tolerance — prudent, balanced (default) or aggressive — set at `loop init` and editable in `LOOP.md` | how much a reviewer lets through without a second look |
29
33
  | Profile | — | who you are — seeded once, colours but never binds | your onboarding notes |
30
34
  | Memory force | — | a memory's binding strength (constraint, choice, conviction, tempered) | MUST / SHOULD / MAY |
31
35
  | Tagline | TAGLINE | the one-line essence of the theme | — |
@@ -42,7 +42,9 @@ Every plugin swallows its own errors — none can block or crash a session. Each
42
42
 
43
43
  It ships whole on every install, whatever [doctrine packs](../concepts/rules.md) you chose. On Claude Code and Bob the same checks are hooks, which can *ask* you instead of refusing; OpenCode's tool hook has no ask tier. Protected wiki folders are enforced only here — on the other hosts they are an instruction.
44
44
 
45
- **When it runs.** Before every tool call (`tool.execute.before`).
45
+ A `permission.ask` hook lets through only a fixed set of commit/push forms on a `loop/*` branch with a running loop — anything else still asks, same as the Claude/Bob git-gate's loop/* exemption.
46
+
47
+ **When it runs.** Before every tool call (`tool.execute.before`), and before a static `permission.bash` ask is shown to you (`permission.ask`).
46
48
 
47
49
  **What you see.** A refused tool call: `[geneseed-guard] blocked: <reason> — set GENESEED_GUARD=off to allow`. Warnings go to stderr.
48
50
 
package/docs/web-ui.md CHANGED
@@ -51,7 +51,7 @@ The left rail mirrors the harness's own shape:
51
51
  | Group | Views |
52
52
  | --- | --- |
53
53
  | *(ungrouped)* | Dashboard |
54
- | **📖 Codex** | Constitution · Rules · Profile · Skills · Agents · Library · Docs |
54
+ | **📖 Codex** | Constitution · Rules · Profile · Skills · Agents · Library · Loops · Docs |
55
55
  | **🩺 Care** | Activity · Changes · Doctor |
56
56
  | **🎨 Setup** | Harness · Settings |
57
57
 
@@ -111,6 +111,17 @@ streams the output of background jobs and keeps their history across reloads.
111
111
  **Promote to rule** control turns a recurring lesson into a trial rule in
112
112
  `user-rules.md` — provenance recorded, the source fact deleted so it isn't loaded
113
113
  twice — landing you on the Rules page.
114
+ - **Loops** (`#/loops`) — the loop catalogue `geneseed loop` runs from, read-only.
115
+ **Templates** draws the selected template as a ring: the iteration turning clockwise from
116
+ its head, each inner retry loop hung outside on its head node, the ⛨ *validate* gate the
117
+ engine adds before the first mutate step, setup entering from the left and `done → close`
118
+ leaving from the head; a card per brick sits under it, and clicking a node scrolls to its
119
+ card. A graph a ring cannot draw cleanly (more than eight iteration steps, a node in more
120
+ than three loops) falls back to left-to-right boxes. **Bricks** lists every brick with its
121
+ origin (shipped / global / project), what it overrides, why it is unavailable, and its
122
+ source. **Active** (live runs) is announced for v2 and disabled. Reads `GET /api/loops`,
123
+ resolved from the selected install's root, so a project install shows its
124
+ `.geneseed/` bricks and templates.
114
125
  - **Profile** (`#/profile`) — `PROFILE.md` beside the deployed AGENT.md: who you are and
115
126
  how you like to work. Opens rendered; **Edit** is a whole-file editor. The save carries
116
127
  the fingerprint it loaded — if an agent session changed the file first, your edit stays
@@ -2,5 +2,5 @@
2
2
  "_comment": "version is a human-readable release label only; the canonical identity of an installed harness is the source fingerprint stamped in .geneseed-version (see `harness version`), not this string.",
3
3
  "theme": "neutral",
4
4
  "product": "Geneseed",
5
- "version": "3.11.0"
5
+ "version": "3.12.0"
6
6
  }
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
 
@@ -125,6 +127,26 @@ float `1.0` into one double, and that value gets written into a user's file. Do
125
127
  library for `udiff.mjs`: it is not Myers, ties resolve to the earliest match. Every function in
126
128
  `paths.mjs` branches on `process.platform`, so green on one OS proves nothing about the other.
127
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
+
128
150
  ## `js/maintain/` — lifecycle
129
151
 
130
152
  Verbs that change an install that already exists, as opposed to rendering one.