geneseed 3.4.0 → 3.5.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 (164) hide show
  1. package/CHANGELOG.md +283 -0
  2. package/DESIGN.md +11 -14
  3. package/QUICKSTART.md +3 -1
  4. package/README.md +51 -42
  5. package/SETUP.md +25 -27
  6. package/SHIPPED.md +26 -32
  7. package/adapters/bob/README.md +12 -9
  8. package/adapters/copilot/README.md +26 -25
  9. package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +1 -1
  10. package/adapters/opencode/README.md +2 -2
  11. package/adapters/opencode/plugins/geneseed-context.js +5 -3
  12. package/adapters/opencode/plugins/geneseed-workflow.js +30 -3
  13. package/adapters/opencode/workflows/_runtime.js +97 -7
  14. package/bin/build-driver.mjs +5 -1248
  15. package/bin/geneseed-cli.mjs +11 -6
  16. package/bin/geneseed-hook.mjs +4 -2
  17. package/docs/candidate-hosts.md +1 -1
  18. package/docs/design-history.md +37 -37
  19. package/docs/extending.md +45 -24
  20. package/docs/token-footprint.md +52 -26
  21. package/docs/web/claude-hooks.md +3 -2
  22. package/docs/web/collaboration.md +2 -2
  23. package/docs/web/first-session.md +1 -1
  24. package/docs/web/footprint.md +2 -2
  25. package/docs/web/harnesses.md +1 -1
  26. package/docs/web/model.md +1 -1
  27. package/docs/web/plugin-workflow.md +1 -0
  28. package/docs/web/rules.md +3 -3
  29. package/docs/web/setup-choices.md +2 -2
  30. package/docs/web/themes-author.md +1 -1
  31. package/docs/web-ui.md +18 -10
  32. package/harness.config.json +1 -1
  33. package/js/README.md +6 -5
  34. package/js/build/driver.mjs +1191 -0
  35. package/js/build/emit-claude.mjs +17 -10
  36. package/js/build/emit-common.mjs +4 -4
  37. package/js/build/generate.mjs +1 -1
  38. package/js/build/render.mjs +48 -44
  39. package/js/build/stubs.mjs +1 -1
  40. package/js/build/themes.mjs +4 -4
  41. package/js/build/version.mjs +12 -0
  42. package/js/cli-table.json +21 -6
  43. package/js/hosts/hooks.mjs +89 -74
  44. package/js/hosts/hosts.mjs +19 -2
  45. package/js/hosts/mcp.mjs +14 -20
  46. package/js/hosts/native.mjs +23 -17
  47. package/js/hosts/settings.mjs +52 -104
  48. package/js/inspect/checks-authoring.mjs +117 -3
  49. package/js/inspect/diff.mjs +1 -1
  50. package/js/inspect/doctor.mjs +1 -1
  51. package/js/inspect/excludes.mjs +1 -1
  52. package/js/inspect/scan.mjs +3 -14
  53. package/js/inspect/status.mjs +2 -2
  54. package/js/inspect/validate.mjs +1 -1
  55. package/js/lib/fs.mjs +23 -8
  56. package/js/lib/json.mjs +7 -1
  57. package/js/lib/proc.mjs +6 -4
  58. package/js/maintain/migrate.mjs +1 -1
  59. package/js/maintain/setup.mjs +1 -1
  60. package/js/maintain/uninstall.mjs +17 -15
  61. package/js/ui/cli.mjs +19 -0
  62. package/js/ui/menu.mjs +1 -1
  63. package/js/web/actions.mjs +4 -4
  64. package/js/web/activity.mjs +4 -4
  65. package/js/web/api.mjs +14 -11
  66. package/js/web/daemon.mjs +1 -1
  67. package/js/web/docs.mjs +12 -12
  68. package/js/web/handler.mjs +12 -5
  69. package/js/web/jobs.mjs +1 -1
  70. package/package.json +1 -1
  71. package/src/AGENT.md.tmpl +69 -36
  72. package/src/agents/developer.md +2 -2
  73. package/src/agents/reviewer.md +5 -2
  74. package/src/doctrines/README.md +26 -3
  75. package/src/doctrines/craft.md +78 -5
  76. package/src/doctrines/ops.md +62 -0
  77. package/src/doctrines/process.md +91 -4
  78. package/src/doctrines/rigor.md +40 -0
  79. package/src/laws/universal.md +27 -21
  80. package/src/memory/README.md +4 -2
  81. package/src/modes/foreman.md +5 -5
  82. package/src/notebook/README.md +2 -1
  83. package/src/ontology/universal.md +71 -67
  84. package/src/postures/peer.md +3 -0
  85. package/src/skills/bruno-collection-generator.md +1 -1
  86. package/src/skills/bruno-test-writer.md +2 -2
  87. package/src/skills/daydream/LICENSE +21 -0
  88. package/src/skills/daydream/VENDOR.md +3 -1
  89. package/src/skills/geneseed-code-review.md +6 -2
  90. package/src/skills/parallel-agents.md +3 -3
  91. package/src/skills/pipeline.md +4 -4
  92. package/src/skills/react-view-transitions/VENDOR.md +7 -1
  93. package/src/skills/workflow.md +12 -4
  94. package/themes/_TEMPLATE.json +6 -4
  95. package/themes/biker.json +5 -3
  96. package/themes/commentator.json +5 -3
  97. package/themes/cyberpunk.json +5 -3
  98. package/themes/gamer.json +5 -3
  99. package/themes/imperial.json +5 -3
  100. package/themes/joker.json +5 -3
  101. package/themes/marvin.json +5 -3
  102. package/themes/mean.json +5 -3
  103. package/themes/military.json +5 -3
  104. package/themes/neutral.json +6 -4
  105. package/themes/opencode/README.md +2 -2
  106. package/themes/pirate.json +5 -3
  107. package/themes/sports.json +5 -3
  108. package/themes/verstappen.json +5 -3
  109. package/themes/wizard.json +5 -3
  110. package/web/dist/assets/Activity-Flgf4kHy.js +1 -0
  111. package/web/dist/assets/ActivityDetail-DIpr85nh.js +1 -0
  112. package/web/dist/assets/CatalogRow-CjyLjaLb.js +1 -0
  113. package/web/dist/assets/Diff-Deisdha8.js +2 -0
  114. package/web/dist/assets/Docs-DGUdY4l4.js +2 -0
  115. package/web/dist/assets/Doctor-F74lJ69P.js +1 -0
  116. package/web/dist/assets/FilterInput-BakJLvaV.js +1 -0
  117. package/web/dist/assets/GreenhouseView-D072lBiD.js +2 -0
  118. package/web/dist/assets/Harness-DNRP2Va7.js +1 -0
  119. package/web/dist/assets/JournalView-DWFqZ0Iw.js +1 -0
  120. package/web/dist/assets/Laws-BN6g3HU7.js +3 -0
  121. package/web/dist/assets/Library-CDOv4emM.js +1 -0
  122. package/web/dist/assets/Markdown-BQAvN8xx.js +38 -0
  123. package/web/dist/assets/OperatorHudView-CwNIUVX8.js +2 -0
  124. package/web/dist/assets/Profile-CpP9BTne.js +1 -0
  125. package/web/dist/assets/Rules-DFXsUCB7.js +1 -0
  126. package/web/dist/assets/Settings-BqZVUtJ5.js +1 -0
  127. package/web/dist/assets/Skills-Cu6kBgfF.js +1 -0
  128. package/web/dist/assets/StatusBadge-Cg6j-NJL.js +1 -0
  129. package/web/dist/assets/activity-iWaWVtsR.js +1 -0
  130. package/web/dist/assets/drawable-BgfrEBer.js +1 -0
  131. package/web/dist/assets/index-CAqrWhKl.js +2 -0
  132. package/web/dist/assets/index-Ruh93A3A.css +1 -0
  133. package/web/dist/assets/jobBuckets-Dbq8ZlWW.js +1 -0
  134. package/web/dist/assets/lawCats-BLgxl151.js +1 -0
  135. package/web/dist/assets/react-DbkvxWG5.js +8 -0
  136. package/web/dist/assets/rolldown-runtime-CbXtAM7H.js +1 -0
  137. package/web/dist/index.html +4 -3
  138. package/web/src/pages/Laws.jsx +22 -8
  139. package/src/skills/react-view-transitions/README.md +0 -42
  140. package/web/dist/assets/Activity-DMGhhjQc.js +0 -1
  141. package/web/dist/assets/ActivityDetail-CeQcDA7e.js +0 -1
  142. package/web/dist/assets/CatalogRow-CnSbDPyg.js +0 -1
  143. package/web/dist/assets/Diff-D_ksMBD1.js +0 -5
  144. package/web/dist/assets/Doctor-Brb3_oCT.js +0 -1
  145. package/web/dist/assets/FilterInput-DbRWPhtT.js +0 -1
  146. package/web/dist/assets/GreenhouseView-F3VNFr73.js +0 -2
  147. package/web/dist/assets/Harness-DJSwfYeF.js +0 -1
  148. package/web/dist/assets/JournalView-BLRYRnB3.js +0 -1
  149. package/web/dist/assets/Laws-CKYns9qe.js +0 -3
  150. package/web/dist/assets/Library-BK0YloSv.js +0 -1
  151. package/web/dist/assets/Markdown-BzTRzc7a.js +0 -46
  152. package/web/dist/assets/OperatorHudView-D3OQSGp5.js +0 -2
  153. package/web/dist/assets/Profile-CPA2u_0g.js +0 -1
  154. package/web/dist/assets/Rules-Lukel-Ru.js +0 -1
  155. package/web/dist/assets/Skills-BKRLHH7m.js +0 -1
  156. package/web/dist/assets/StatusBadge-DI3weRKd.js +0 -1
  157. package/web/dist/assets/activity-C_H2Bx1P.js +0 -1
  158. package/web/dist/assets/index-B7_Wga23.js +0 -2
  159. package/web/dist/assets/index-DzdQjvM1.css +0 -1
  160. package/web/dist/assets/index-N8dTrVro.js +0 -1
  161. package/web/dist/assets/index-Py_IQjgK.js +0 -10
  162. package/web/dist/assets/jobBuckets-DfTUJUxR.js +0 -1
  163. package/web/dist/assets/lawCats-DUS9UTH4.js +0 -1
  164. package/web/dist/assets/react-DOpiTn5v.js +0 -32
package/SETUP.md CHANGED
@@ -35,11 +35,11 @@ that matches your tool, then configure and verify. For the conceptual overview s
35
35
  Nothing in this guide needs a second interpreter. Three things are worth stating plainly,
36
36
  because each is a live constraint rather than a footnote:
37
37
 
38
- - **0 commands have no Node twin**: every command in this guide answers from the Node
39
- entry points. There is no full-screen browse panel: `geneseed tui` and `geneseed menu`
40
- are verbs that say the panel is not here and print the command list instead, and off a
41
- terminal (which is how scripts and CI run them) the bytes and the exit code are the
42
- recorded ones. `home` opens the web console, which is the visual front end.
38
+ - **Every command runs on Node.** There is no full-screen browse panel: `geneseed tui`
39
+ and `geneseed menu` are verbs that say the panel is not here and print the command list
40
+ instead. Bare `geneseed` — from the launchers and from an npm install alike — runs
41
+ `home`, which opens the web console when it can and otherwise prints the command list;
42
+ `geneseed --help` lists every command.
43
43
  - **Nothing you install needs an interpreter.** The `token-report` skill is a script
44
44
  rather than prose, and it ships as `scripts/token_report.mjs`, run with `node`;
45
45
  `daydream` and `herdr` hand their inline code to `node -e`.
@@ -79,7 +79,7 @@ tagline, loaded-sigil, and voice — so you hear the flavour before you choose;
79
79
  you pick, it speaks in that theme's accent through confirm and build, and the
80
80
  install ends on the theme's own **banner and benediction**.
81
81
 
82
- **Already installed?** Bare **`./geneseed`** (Windows: `.\geneseed.cmd`) opens the
82
+ **Already installed?** Bare **`geneseed`** (a checkout: `./geneseed`, Windows: `.\geneseed.cmd`) opens the
83
83
  **web console**, which is where browsing, local edits, rebuilds, memory, status, MCP
84
84
  servers and uninstall all live — see [The web console](docs/web-ui.md). `GENESEED_NO_WEB=1`
85
85
  suppresses that and prints the command list instead. Uninstall from the CLI is
@@ -103,8 +103,8 @@ hand? Pick a path below.
103
103
  > `node bin/build-driver.mjs` with the same flags. Every commit re-emits with it across every
104
104
  > theme × host × footprint combination and checks the result with `doctor --all`, on Linux
105
105
  > and Windows both. Until 2026-08-17 that output was also compared byte for byte against
106
- > recordings taken from the Python implementation; those were retired, and `docs/limits.md`
107
- > says why and what it cost.
106
+ > recordings taken from the Python implementation; those were retired (see the 2.0.0 entry
107
+ > in [CHANGELOG.md](CHANGELOG.md)).
108
108
 
109
109
  ---
110
110
 
@@ -238,16 +238,13 @@ the preamble rides a file Copilot auto-loads: the repo-root `AGENTS.md` (CLI, co
238
238
  agent, and VS Code agent mode) or the personal `~/.copilot/copilot-instructions.md`
239
239
  (CLI). Two differences from the Claude paths:
240
240
 
241
- - **Hooks at global scope only, block-or-warn.** The global emit wires two hooks
242
- into `~/.copilot/settings.json`: `sessionStart` (eager project-context injection,
243
- as on Claude Code) and `toolCall` (both gates behind one command, because Copilot
244
- takes one command per event). Copilot's `toolCall` has no "ask the user" tier, so
245
- Laws I and IV **block** and the two consent rules (process 1, process 5) **warn**
246
- on stderr instead of prompting. No `learn` hook — Copilot's `sessionEnd`/`agentStop`
247
- payloads carry no transcript — so the memory convention rides the preamble's
248
- instructions. The per-repo emit wires nothing: Copilot reads hooks from the
249
- personal settings file only, and a machine-absolute command in a shared
250
- `.github/` would fail on every teammate's machine.
241
+ - **Hooks at global scope only.** The global emit adds entries to four events in
242
+ `~/.copilot/settings.json`: `sessionStart` (eager project-context injection),
243
+ `preToolUse` (both gates behind one command, since entries carry no matcher —
244
+ every rule **asks**, as on Claude Code), and `agentStop` + `preCompact` (`learn`,
245
+ from the payload's `transcriptPath`). The per-repo emit wires nothing: a
246
+ machine-absolute command in a shared `.github/` would fail on every teammate's
247
+ machine.
251
248
  - **The per-repo layer lives in the shared `.github/`** (Copilot's repo config
252
249
  surface). Safe by construction: the ownership manifest + claim-on-create never
253
250
  touch files Geneseed didn't write — your workflows and same-named agents/skills
@@ -406,19 +403,19 @@ the agent reads — which is why the wizard's blurb for it says so.
406
403
  **Footprint** sets how much of the constitution `AGENT.md` carries *inline* on every turn — a
407
404
  token-cost dial, not a change to which Rules apply. Both states keep all Rules in force:
408
405
 
409
- - **lean** (default) — Sections 1–2 carry the heading and rule line of each Rule and each
410
- active doctrine rule, then a pointer to the full text. Lighter every turn; the complete
406
+ - **lean** (default) — Sections 1–2 carry the heading and the authored short form of each Rule
407
+ and each active doctrine rule, then a pointer to the full text. Lighter every turn; the complete
411
408
  `laws/universal.md`, `ontology/` and `doctrines/` still ship beside `AGENT.md` and the agent
412
409
  reads them on demand (and is told to before acting on secrets, deletion, git history, scope,
413
410
  or untrusted content).
414
411
  - **full** — Sections 1–2 inline every Rule's and every active doctrine rule's complete text
415
412
  *and* rationale. Maximum guidance density; the largest always-loaded block in the harness.
416
413
 
417
- **The Ontology is never truncated.** Lean keeps only the first sentence of each `### ` block,
418
- and the four ontology sections are flowing prose rather than numbered rules — cutting them
419
- would leave four orphan sentences, so they ship whole at both footprints. That is why adding
420
- the ontology tier grew the lean carrier proportionally more than the full one; it was taken as
421
- a deliberate cost.
414
+ **Nothing at lean is truncated — it is written.** Every tier carries a hand-authored short form
415
+ beside its full text: the Ontology (whose four flowing sections would have read as four orphan
416
+ sentences if cut), each Rule, and each doctrine rule. What lean ships is decided by an author
417
+ rather than by where a sentence happens to end — so a short form is judged on whether an agent
418
+ can act on it, which is something a reviewer can see.
422
419
 
423
420
  **Why:** context is scarce and metered. Lean reclaims that budget — and the tokens you
424
421
  pay for it — for the task, moving the *rationale* one read away while keeping the rules
@@ -917,7 +914,8 @@ not connected, walk these in order:
917
914
  | `GENESEED_NO_ANIM` | install animation | disable the themed install animation |
918
915
  | `GENESEED_LOG` | `geneseed upgrade` | override the install/upgrade log path |
919
916
  | `GENESEED_NET_TIMEOUT` | `upgrade` | seconds before download attempts give up (default 20, floor 5) |
920
- | `GENESEED_NO_WEB` | launcher / menu | `1` disables the web-first default of bare `./geneseed` — falls back to the terminal menu |
917
+ | `GENESEED_NO_WEB` | `home` | `1` disables the web-first default of bare `geneseed` — it prints the command list instead |
918
+ | `GENESEED_STACK_GLOBAL` | context hook | `1` disables project-bypasses-global: a project install of the same host no longer makes the global install's hook stand down, so both inject |
921
919
  | `OPENCODE_CONFIG_DIR` / `XDG_CONFIG_HOME` | global emit | where the global install is written |
922
920
  | `OPENCODE_DISABLE_LSP_DOWNLOAD` | OpenCode (LSP) | `true` stops OpenCode auto-downloading built-in language servers (typescript, pyright, jdtls) — set it on air-gapped machines and pre-install each server yourself |
923
921
 
@@ -965,7 +963,7 @@ harness no matter where the shim itself lives — the same shape the Windows arm
965
963
  always used. Once it's on `PATH`, drop the `./`:
966
964
 
967
965
  ```
968
- geneseed # the interactive main menu, from any directory
966
+ geneseed # the web console (else the command list), from any directory
969
967
  geneseed build # …and every subcommand
970
968
  ```
971
969
 
package/SHIPPED.md CHANGED
@@ -14,54 +14,48 @@ This page answers "what is actually in the harness, and where is its rationale?"
14
14
  The dated `docs/specs/*` files were the *design record* — the problem and the
15
15
  decisions behind each feature; this is the *index* over them, marking what has
16
16
  landed in the source tree. Specs are working documents: they live git-ignored on
17
- the authoring machine and are dropped once shipped, so the spec links below are
18
- historical pointers, not files distributed with the repo.
17
+ the authoring machine and are dropped once shipped, so the spec names below are
18
+ historical references, not files distributed with the repo.
19
19
 
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.4.0**.
24
-
25
- > **Spec status audit (2026-06-15).** Every dated spec authored before today was
26
- > verified against the source tree and confirmed implemented (code + tests + docs);
27
- > their `**Status:**` headers were flipped from `approved`/`implementing`/`Accepted`/`spec`
28
- > to `implemented`. The only open spec is the same-day
29
- > [shell-completion](docs/specs/2026-06-15-shell-completion.md) draft.
23
+ > string. Current label: **3.5.0**.
30
24
 
31
25
  ## Capabilities
32
26
 
33
- Every row below is present in the tree today; "Spec" links the design record.
27
+ Every row below is present in the tree today; "Spec" names the design record.
34
28
 
35
29
  | Capability | What it is | Spec |
36
30
  | --- | --- | --- |
37
31
  | **Themed generator** | One neutral `src/` → 14 themed bundles via `geneseed-build` (zero dependencies); structure stays theme-independent | [DESIGN.md](DESIGN.md) |
38
- | **Laws / Agents / Skills / Memory / Notebook** | 11 laws, 18 agents, 49 skills, the memory convention, and the agent's sovereign notebook | [notebook](docs/specs/2026-06-11-notebook-sovereign-space.md) · [agent-own-space](docs/specs/2026-06-10-notebook-agent-own-space.md) |
39
- | **Three-tier constitution** | The governance surface splits into an always-on **Ontology** (Telos, Evidence, Decisions, Conduct — the Pact absorbed into Telos), the eleven always-on **Invariants** (IX and XI retired in place — folded into rigor 5 and Law III, numbers kept), and four toggleable **Doctrine** packs (craft, rigor, ops, process) addressed `<pack> <n>`. 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 pack numbering, the per-rule Principle column, every `{{DOCTRINE}}` citation in `src/`, and the rule that an always-on tier may never cite a toggleable one | [three-tier-constitution](docs/specs/2026-08-19-three-tier-constitution-design.md) |
40
- | **`doctor`** | One check: unresolved tokens, dead/non-hermetic links, theme-key parity, authoring gates, rendered-bundle drift | [theme-aware doctor/diff](docs/specs/2026-06-07-theme-aware-doctor-diff.md) |
41
- | **`diff` + improvements export** | Deployed-vs-source drift report; export an `improvements.md` back-port | [improvements-export](docs/specs/2026-06-12-improvements-export.md) |
42
- | **`status` / `version`** | Headless dashboard + fingerprint verdict (no TUI needed) | [status-command](docs/specs/2026-06-07-status-command.md) · [version-and-uninstall](docs/specs/2026-06-07-version-and-uninstall.md) |
43
- | **Terminal rendering** | There is no full-screen control panel: `tui` and `menu` are verbs that say so and print the command list. The display-width and glyph layer ships on, behind `status` and the install animation | [refactor-polish](docs/specs/2026-06-07-tui-refactor-polish.md) · [modern-refresh](docs/specs/2026-06-08-tui-modern-refresh.md) · [windows-tui](docs/specs/2026-06-11-windows-tui.md) |
44
- | **Web console** | Local browser UI (Dashboard, Library, Graph, Docs, Specs, Changes, Doctor, Themes, Settings), auto-build, in-app docs menu | [web-ui-v2](docs/specs/2026-06-12-web-ui-v2.md) · [web-auto-build](docs/specs/2026-06-12-web-auto-build.md) · [docs-menu](docs/specs/2026-06-13-docs-menu.md) |
45
- | **OpenCode adapter** | Native agents/skills mapping + plugins (context, learn, guard, workflow, notify, ponytail, activity); global-emit link integrity; JSONC config target | [adapter-features](docs/specs/2026-06-07-opencode-adapter-features.md) · [global-emit](docs/specs/2026-06-07-global-emit-link-integrity.md) · [jsonc-target](docs/specs/2026-06-12-opencode-jsonc-target.md) · [ponytail](docs/specs/2026-06-16-ponytail-minimal-code-mode.md) |
46
- | **Host hooks, per dialect** | Every host gets the hook surface its runtime offers, spoken in that runtime's own verdict dialect via `--host`: Claude Code asks (`permissionDecision`), Copilot blocks (`{"block":true}`, `sessionStart` + `toolCall` in `~/.copilot/settings.json`), Bob refuses by exit code 2 (five Claude-named events, nested `~/.bob/settings/settings.json`, no subagent event), OpenCode's guard throws. Laws I/IV are the hard tier everywhere; the consent rules ask where a host can ask and warn where it cannot. `tool-gate` fuses the two gates for hosts that run one command per event; `PreCompact` runs `learn` before Claude compacts; every block or ask lands in `notebook/gates.jsonl` on every host | [copilot](adapters/copilot/README.md) · [bob](adapters/bob/README.md) · [claude-code](adapters/claude-code/README.md) · [candidate hosts](docs/candidate-hosts.md) |
47
- | **Workflow primitive** | Saved, code-driven subagent orchestration (`workflow` tool + runtime); shipped scripts include `dispatch` — decompose a goal, route each subtask to its owning agent, converge | [workflow-primitive](docs/specs/2026-06-09-opencode-workflow-primitive.md) |
48
- | **Per-agent memory** | Each capability agent keeps durable lessons in `memory/agents/<name>.md`: read at dispatch, written back mechanically by `learn` on session/subagent end (OpenCode plugin + Claude `SubagentStop`; Bob has no subagent event); `learn --consolidate` rebuilds the index | [orchestration-memory-upgrade](docs/specs/2026-07-04-orchestration-memory-upgrade.md) |
49
- | **Wiki integration** | Machine-wide knowledge base (`wiki.jsonc`): eager/lazy loading, conventions, protected folders enforced at the tool boundary | [wiki-knowledge-base](docs/specs/2026-06-11-wiki-knowledge-base.md) |
50
- | **Collaboration layer** | Negotiated postures (peer/mentor/expert/assistant/artisan, chosen at setup, inlined in AGENT.md), the bidirectional Pact, epistemically-typed memory (`force:` + Bridge rule), and a seed-once `PROFILE.md` identity with a web editor tab and a `profile` skill that interviews the user to draft it | [collaboration-layer](docs/specs/2026-07-12-collaboration-layer.md) |
51
- | **Rule front door** | Both durable stores enter through the `rule` skill's fork (*standing rule, or fact to remember?* — always asked, never inferred): the memory branch elicits the binding `force` and shows the file before writing, the rule branch pressure-tests by reformulation and a mandatory counter-example. Doctrine process 1 carries the norm on every host; a `rule-gate` PreToolUse hook (Claude, Bob) and a `geneseed-guard` speed bump (OpenCode) hold it at the tool boundary | [rule-front-door](docs/specs/2026-07-30-rule-front-door.md) |
52
- | **Entity registry** | `registry.json` records a lifecycle status, version, owner and add-date for every agent and skill in the tree (maintainer-side — never rendered into a bundle, so it costs no session context). `doctor` gates it against `src/` in both directions, sweeps everything that ships for credentials, and holds each vendored skill folder to an immutable upstream commit. The status surfaces as a badge in the web catalog, shown only where it deviates from `approved` | [entity-registry](docs/specs/2026-07-31-entity-registry.md) |
53
- | **Node runtime + npm package** | The whole harness runs from Node: every command `js/cli-table.json` declares, all five hook verbs, every web-console endpoint (the OS-native folder picker excepted, and declined on purpose) and both generators. Every commit runs the unit suite, `doctor --all` across every theme × host × footprint, and the generator's two self-comparisons, on Linux and Windows both; the frozen emit/cli/web recordings that also gated it retired on 2026-08-17 (`docs/limits.md`). It ships as the zero-dependency `geneseed` package (`geneseed`, `geneseed-hook`, `geneseed-build`), so `npx geneseed setup` needs no clone and nothing else installed; `geneseed migrate` moves an existing checkout install across. Node is the only runtime it asks for: the `token-report` skill's own script is `token_report.mjs`, gated over seeded transcripts for all four hosts, and `daydream` and `herdr` call `node -e` | [npx-distribution](docs/specs/2026-08-06-npx-distribution.md) |
54
- | **Runtime awareness** | MCP discovery, web research, tool-call batching, review discipline — carried by the doctrine packs (`ops 1` tool discovery, `process 4` read the docs first, `rigor 2`/`rigor 3` honest tests and cover-and-verify), not by the invariants | [runtime-awareness](docs/specs/2026-06-07-runtime-awareness-and-review-discipline.md) |
55
- | **Run from anywhere / uninstall** | `link`/`unlink` onto PATH; global uninstall that keeps the memory store | [version-and-uninstall](docs/specs/2026-06-07-version-and-uninstall.md) |
32
+ | **Laws / Agents / Skills / Memory / Notebook** | 11 laws, 18 agents, 52 skills, the memory convention, and the agent's sovereign notebook | `2026-06-11-notebook-sovereign-space` · `2026-06-10-notebook-agent-own-space` |
33
+ | **Three-tier constitution** | The governance surface splits into an always-on **Ontology** (Telos, Evidence, Decisions, Conduct — the Pact absorbed into Telos), the eleven always-on **Invariants** (IX and XI retired in place — folded into rigor 5 and Law III, numbers kept), and four toggleable **Doctrine** packs (craft, rigor, ops, process) addressed `<pack> <n>`. 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 pack numbering, the per-rule Principle column, every `{{DOCTRINE}}` 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
+ | **`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
+ | **`diff` + improvements export** | Deployed-vs-source drift report; export an `improvements.md` back-port | `2026-06-12-improvements-export` |
36
+ | **`status` / `version`** | Headless dashboard + fingerprint verdict (no TUI needed) | `2026-06-07-status-command` · `2026-06-07-version-and-uninstall` |
37
+ | **Terminal rendering** | There is no full-screen control panel: `tui` and `menu` are verbs that say so and print the command list. The display-width and glyph layer ships on, behind `status` and the install animation | `2026-06-07-tui-refactor-polish` · `2026-06-08-tui-modern-refresh` · `2026-06-11-windows-tui` |
38
+ | **Web console** | Local browser UI (Dashboard; Codex: Constitution, Rules, Profile, Skills, Agents, Library, Docs; Care: Activity, Changes, Doctor; Setup: Harness, Settings), auto-build, in-app docs menu | `2026-06-12-web-ui-v2` · `2026-06-12-web-auto-build` · `2026-06-13-docs-menu` |
39
+ | **OpenCode adapter** | Native agents/skills mapping + plugins (context, learn, guard, workflow, notify, ponytail, activity); global-emit link integrity; JSONC config target | `2026-06-07-opencode-adapter-features` · `2026-06-07-global-emit-link-integrity` · `2026-06-12-opencode-jsonc-target` · `2026-06-16-ponytail-minimal-code-mode` |
40
+ | **Host hooks, per dialect** | Every host gets the hook surface its runtime offers, spoken in that runtime's own verdict dialect via `--host`: Claude Code asks (`permissionDecision`), Copilot asks as Claude does (`sessionStart`, `preToolUse`, `agentStop`, `preCompact` in `~/.copilot/settings.json`, global scope only), Bob refuses by exit code 2 (five Claude-named events, nested `~/.bob/settings/settings.json`, no subagent event), OpenCode's guard throws. Laws I/IV are the hard tier everywhere; the consent rules ask where a host can ask and warn where it cannot. `tool-gate` fuses the two gates for hosts that run one command per event; `PreCompact` runs `learn` before Claude compacts; every block or ask lands in `notebook/gates.jsonl` on every host | [copilot](adapters/copilot/README.md) · [bob](adapters/bob/README.md) · [claude-code](adapters/claude-code/README.md) · [candidate hosts](docs/candidate-hosts.md) |
41
+ | **Workflow primitive** | Saved, code-driven subagent orchestration (`workflow` tool + runtime); shipped scripts include `dispatch` — decompose a goal, route each subtask to its owning agent, converge | `2026-06-09-opencode-workflow-primitive` |
42
+ | **Per-agent memory** | Each capability agent keeps durable lessons in `memory/agents/<name>.md`: read at dispatch, written back mechanically by `learn` on session/subagent end (OpenCode plugin + Claude `SubagentStop`; Bob has no subagent event); `learn --consolidate` rebuilds the index | `2026-07-04-orchestration-memory-upgrade` |
43
+ | **Wiki integration** | Machine-wide knowledge base (`wiki.jsonc`): eager/lazy loading, conventions, protected folders enforced at the tool boundary | `2026-06-11-wiki-knowledge-base` |
44
+ | **Collaboration layer** | Negotiated postures (peer/mentor/expert/assistant/artisan, chosen at setup, inlined in AGENT.md), the bidirectional Pact, epistemically-typed memory (`force:` + Bridge rule), and a seed-once `PROFILE.md` identity with a web editor tab and a `profile` skill that interviews the user to draft it | `2026-07-12-collaboration-layer` |
45
+ | **Rule front door** | Both durable stores enter through the `rule` skill's fork (*standing rule, or fact to remember?* — always asked, never inferred): the memory branch elicits the binding `force` and shows the file before writing, the rule branch pressure-tests by reformulation and a mandatory counter-example. Doctrine process 1 carries the norm on every host; a `rule-gate` PreToolUse hook (Claude, Bob) and a `geneseed-guard` speed bump (OpenCode) hold it at the tool boundary | `2026-07-30-rule-front-door` |
46
+ | **Entity registry** | `registry.json` records a lifecycle status, version, owner and add-date for every agent and skill in the tree (maintainer-side — never rendered into a bundle, so it costs no session context). `doctor` gates it against `src/` in both directions, sweeps everything that ships for credentials, and holds each vendored skill folder to an immutable upstream commit. The status surfaces as a badge in the web catalog, shown only where it deviates from `approved` | `2026-07-31-entity-registry` |
47
+ | **Node runtime + npm package** | The whole harness runs from Node: every command `js/cli-table.json` declares, all five hook verbs, every web-console endpoint (the OS-native folder picker excepted, and declined on purpose) and both generators. Every commit runs the unit suite, `doctor --all` across every theme × host × footprint, and the generator's two self-comparisons, on Linux and Windows both; the frozen emit/cli/web recordings that also gated it retired on 2026-08-17 (see the 2.0.0 entry in [CHANGELOG.md](CHANGELOG.md)). It ships as the zero-dependency `geneseed` package (`geneseed`, `geneseed-hook`, `geneseed-build`), so `npx geneseed setup` needs no clone and nothing else installed; `geneseed migrate` moves an existing checkout install across. Node is the only runtime it asks for: the `token-report` skill's own script is `token_report.mjs`, gated over seeded transcripts for all four hosts, and `daydream` and `herdr` call `node -e` | `2026-08-06-npx-distribution` |
48
+ | **Runtime awareness** | MCP discovery, web research, tool-call batching, review discipline — carried by the doctrine packs (`ops 1` tool discovery, `process 4` read the docs first, `rigor 2`/`rigor 3` honest tests and cover-and-verify), not by the invariants | `2026-06-07-runtime-awareness-and-review-discipline` |
49
+ | **Run from anywhere / uninstall** | `link`/`unlink` onto PATH; global uninstall that keeps the memory store | `2026-06-07-version-and-uninstall` |
56
50
 
57
51
  ## Workplans (not features)
58
52
 
59
- A few `docs/specs/` entries are multi-agent *workplans / audits*, not single features —
53
+ A few specs were multi-agent *workplans / audits*, not single features —
60
54
  they record direction, and their individual items landed across the capabilities above:
61
55
 
62
- - [2026-06-08-harness-perfection-workplan](docs/specs/2026-06-08-harness-perfection-workplan.md)
63
- - [2026-06-08-opencode-feature-coverage](docs/specs/2026-06-08-opencode-feature-coverage.md)
64
- - [2026-06-08-tui-professional-workplan](docs/specs/2026-06-08-tui-professional-workplan.md)
56
+ - `2026-06-08-harness-perfection-workplan`
57
+ - `2026-06-08-opencode-feature-coverage`
58
+ - `2026-06-08-tui-professional-workplan`
65
59
 
66
60
  ## Exploratory archive
67
61
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  [IBM Bob](https://bob.ibm.com) is **Claude-Code-shaped**: a project `.bob/`
6
6
  layer, an `AGENTS.md` instructions file, `SKILL.md` skills, subagents, and a
7
- `settings.json` that also carries `mcpServers`. So the Bob emit **reuses the
7
+ `settings.json` for hooks. So the Bob emit **reuses the
8
8
  Claude engine** verbatim — only the marker dir (`.bob`) and the instructions
9
9
  filename (`AGENTS.md`) change. There is nothing to install by hand: `geneseed
10
10
  setup` (or `geneseed build --emit bob` / `--emit bob-global`) writes everything.
@@ -25,7 +25,9 @@ limitation to be aware of on a shared repo.
25
25
  - `skills/<name>/SKILL.md` — **byte-identical** to every other host; skills
26
26
  are model-invoked via the `skill` tool.
27
27
  - `rules/geneseed.md` — a **slim shadow stub** (see *Bob-ism 1* below).
28
- - `settings.json` — Geneseed's lifecycle hooks + any `mcpServers` you wire.
28
+ - `settings.json` — Geneseed's lifecycle hooks (gitignored by the emit's
29
+ `.bob/.gitignore`, see the caveat below).
30
+ - `mcp.json` — any MCP servers you wire with `geneseed mcp` / the web console.
29
31
  - `memory/`, `notebook/` stores + their indices.
30
32
 
31
33
  ### Global (`--emit bob-global` → `~/.bob`, or `$BOB_CONFIG_DIR`)
@@ -94,7 +96,7 @@ Bob's hooks: the one group only ever exits 2 for Laws I and IV, which every buil
94
96
 
95
97
  **Migration.** A re-emit over an older install unwires Geneseed's groups from the flat
96
98
  `~/.bob/settings.json` (your own keys there are kept) and writes the new set to the
97
- nested file. `geneseed mcp` targets the nested file too.
99
+ nested file.
98
100
 
99
101
  **Unverified live.** There is no Bob install on the authoring machine; this is Bob's
100
102
  documented contract, with the payload field names assumed to be Claude's. If a field
@@ -111,15 +113,16 @@ out of shared git). Those hook commands name the **per-user hook shim**
111
113
  (`~/.geneseed/bin/geneseed-hook`) — no longer this machine's interpreter and
112
114
  checkout, but still a path under *your* home, and now also OS-specific (`.cmd` on
113
115
  Windows, extensionless elsewhere). If you commit `.bob/settings.json`, a teammate
114
- still inherits hooks pointing at a path they do not have. **Add
115
- `.bob/settings.json` to `.gitignore`** on a team repo, or keep the Bob install
116
- personal/global.
116
+ still inherits hooks pointing at a path they do not have. A fresh emit writes
117
+ `.bob/.gitignore` listing `settings.json`; if you already had a `.bob/.gitignore`,
118
+ add the line yourself.
117
119
 
118
120
  ## MCP
119
121
 
120
- MCP servers are wired at runtime by `geneseed mcp` into the `mcpServers` key of
121
- the relevant `settings.json` (`.bob/settings.json` per-repo, `~/.bob/settings.json`
122
- global) — see [`geneseed mcp`](../../README.md) and `rituals/_harness_mcp.py`.
122
+ MCP servers are wired at runtime (web console, listed by `geneseed mcp`) into the
123
+ `mcpServers` key of Bob's own MCP file — `.bob/mcp.json` per-repo,
124
+ `~/.bob/settings/mcp.json` global — never `settings.json`, which Bob does not read
125
+ servers from ([Bob docs](https://bob.ibm.com/docs/ide/configuration/mcp/mcp-in-bob)).
123
126
 
124
127
  ## What Bob does not get
125
128
 
@@ -9,8 +9,9 @@ Agent Skills), custom agents are markdown-with-frontmatter, and the repo-root
9
9
  a `host="copilot"` flag. Nothing to install by hand: `geneseed setup` (or
10
10
  `geneseed build --emit copilot` / `--emit copilot-global`) writes everything.
11
11
 
12
- Copilot is a **reduced host**: it has lifecycle hooks, but one command per event and
13
- no "ask the user" tier, so this page is mostly about what those hooks *can* and
12
+ Copilot is a **reduced host** at project scope (no hooks there) and close to Claude
13
+ Code at global scope: its lifecycle hooks have an "ask the user" tier, so the gates
14
+ prompt as they do on Claude. This page is mostly about what those hooks *can* and
14
15
  *cannot* automate and why the harness still holds where they cannot.
15
16
 
16
17
  ## What the emit writes
@@ -39,42 +40,42 @@ no "ask the user" tier, so this page is mostly about what those hooks *can* and
39
40
  the preamble rides it as a managed block. (Unlike Bob, Copilot *has* a real
40
41
  personal instructions carrier, so no rules-folder workaround is needed.)
41
42
  - `agents/<name>.agent.md` + `skills/` under `~/.copilot`.
42
- - **`settings.json` hooks** — two entries under `hooks`, see below.
43
+ - **`settings.json` hooks** — four entries under `hooks`, see below.
43
44
 
44
- ## Hooks — one command per event, block or warn
45
+ ## Hooks — ask, as on Claude Code
45
46
 
46
47
  The Copilot CLI reads lifecycle hooks from **`~/.copilot/settings.json`** as a map of
47
- event → one `{command}` object (no matcher groups, no list). The global emit wires
48
- two of them, each calling the machine-wide `geneseed-hook` shim with `--host copilot`
49
- so the verdict is spoken in Copilot's dialect:
48
+ event → **array** of `{type: "command", command}` entries
49
+ ([reference](https://docs.github.com/en/copilot/reference/hooks-configuration)). The
50
+ global emit adds one entry to each of four events, each calling the machine-wide
51
+ `geneseed-hook` shim:
50
52
 
51
53
  | event | verb | what it does |
52
54
  | --- | --- | --- |
53
55
  | `sessionStart` | `context` | eager project-context injection, as on Claude Code, returned as `{"additionalContext": …}` |
54
- | `toolCall` | `tool-gate` | the git gate and the rule gate **fused** — one command is all the event allows — dispatched on the payload: a `command` gets the git checks, a path gets the rule checks; the `after` phase is ignored |
56
+ | `preToolUse` | `tool-gate --host copilot` | the git gate and the rule gate **fused** — entries carry no matcher — dispatched on the payload: a `command` gets the git checks, a path gets the rule checks |
57
+ | `agentStop` | `learn` | distils the session transcript (`transcriptPath`) into memory, as Claude's `Stop` |
58
+ | `preCompact` | `learn` | the same, once more before compaction summarises the window away |
55
59
 
56
- Copilot's `toolCall` has **no "ask the user" tier** — its stdout contract is
57
- `{"block": true|false}` — so the verdict tiers are:
60
+ `preToolUse` has an ask tier, so every rule **asks** exactly where it asks on Claude
61
+ Code — Laws I and IV, process 1 and process 5 — answered top-level as
62
+ `{"permissionDecision": "ask", "permissionDecisionReason": …}`. A gate error asks
63
+ too (fail closed). `toolArgs` is read whether Copilot sends it as an object or as a
64
+ JSON string.
58
65
 
59
- - **Laws I and IV block.** A credential-shaped string headed for a tracked file, or
60
- a history-discarding git act (`reset --hard`, `clean -f`, `branch -D`,
61
- `checkout --`, `push --force`), is refused outright with the reason. Same stance
62
- as the OpenCode guard plugin, whose `tool.execute.before` can only allow or throw.
63
- - **Process 1 and process 5 warn.** The two consent rules are the *user's* calls; a
64
- hard block would make Copilot unable to commit at all. They log a `[geneseed] …`
65
- line on stderr (which Copilot records) and allow. A gate error warns the same way
66
- rather than locking every tool call behind a block nobody can clear.
66
+ **Your own hooks on those events stay**: Geneseed's entry goes beside them in the
67
+ array, and uninstall removes exactly the entries it claimed. Comments in the file
68
+ mean it is never rewritten.
67
69
 
68
- **A hook you already set on one of those events is left alone**, and that event is
69
- not claimed — one slot per event means Geneseed could only displace it. Uninstall
70
- removes exactly the hooks it claimed. Comments in the file mean it is never
71
- rewritten.
70
+ **Migration.** Before this, the emit wired a single-object `toolCall` hook — an event
71
+ Copilot no longer fires, so the gates never ran. A re-emit removes those recorded
72
+ entries (your own keys stay) and writes the array shape.
73
+
74
+ **Unverified live.** No Copilot CLI on the authoring machine; this is the documented
75
+ contract.
72
76
 
73
77
  **What is not wired, and why:**
74
78
 
75
- - **no `learn`** — Copilot's `sessionEnd` and `agentStop` payloads carry no
76
- `transcript_path`, so a distiller would have nothing to read. The memory
77
- convention rides the preamble's instructions (the agent writes its own memories);
78
79
  - **no hooks at project scope** — the CLI documents hooks in the personal settings
79
80
  file only, and a machine-absolute command committed into a shared `.github/`
80
81
  would fail on every teammate's machine;
@@ -185,7 +185,7 @@ repo's layout defeats the convention.
185
185
 
186
186
  ### 3.5 Injection block format (mirror `harness.py cmd_context`)
187
187
 
188
- Byte-identical structure to [harness.py:237-263](../../rituals/harness.py) so both
188
+ Byte-identical structure to the retired Python `harness.py` (lines 237-263) so both
189
189
  enforcement paths read the same. Adds a machine marker (first line) for §4.
190
190
 
191
191
  ```
@@ -380,8 +380,8 @@ behaviour** — nothing changes the machine's current agent/model unless you opt
380
380
  `tokyonight`, `rosepine`, `gruvbox`, `nord`, `everforest`, `kanagawa`, `dracula` — each
381
381
  in a **solid** (opaque) and a **transparent** (terminal-background-through) flavour.
382
382
  These are decoupled from the voice theme: any voice theme can run any colour theme.
383
- Authored under `themes/opencode/` (see its README and
384
- [the spec](../../docs/specs/2026-06-17-opencode-color-themes.md)).
383
+ Authored under `themes/opencode/` (see its README; the design record was the
384
+ `2026-06-17-opencode-color-themes` spec).
385
385
  - **Runtime guard plugin** (`geneseed-guard.js`, installed with the others). Enforces
386
386
  the safety Laws at the tool boundary: **blocks** writes to private-key/credential
387
387
  files (Law I), catastrophic shell like `rm -rf /` (Law IV), and any mutation under
@@ -138,10 +138,12 @@ let fallbackVisible = false
138
138
  function log(msg) { if (DEBUG) console.error(`[geneseed-context] ${msg}`) }
139
139
 
140
140
  // ---- convention --------------------------------------------------------------
141
- // Root-level files injected in full. Agent-directed rules + canonical entry docs.
141
+ // Root-level files injected in full. Canonical entry docs + agent-directed rules OpenCode
142
+ // does not read by itself. AGENT.md (opencode.json `instructions`), AGENTS.md and CLAUDE.md
143
+ // (OpenCode's own native roots) are deliberately absent: OpenCode already has them, and
144
+ // injecting them again re-sent the whole harness on every request until 2026-09.
142
145
  const EAGER_ROOT = new Set([
143
- "AGENTS.md", "AGENT.md", "CLAUDE.md", ".cursorrules",
144
- "README.md", "CONTRIBUTING.md", "user-rules.md", "PROFILE.md",
146
+ ".cursorrules", "README.md", "CONTRIBUTING.md", "user-rules.md", "PROFILE.md",
145
147
  ])
146
148
  // Doc trees walked recursively; everything found is lazy (listed, not injected).
147
149
  const LAZY_DIRS = ["docs", "doc", "documentation", "architecture", "adr", "ADR"]
@@ -17,6 +17,7 @@
17
17
  // Quiet by default; GENESEED_DEBUG=1 logs to stderr. GENESEED_WORKFLOWS_DIR overrides
18
18
  // where saved workflows are loaded from.
19
19
 
20
+ import { execFile } from "node:child_process"
20
21
  import { promises as fs } from "node:fs"
21
22
  import * as path from "node:path"
22
23
  import { fileURLToPath, pathToFileURL } from "node:url"
@@ -63,6 +64,17 @@ function nowStamp() {
63
64
  return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`
64
65
  }
65
66
 
67
+ // The runtime's git executor, for `isolation: "worktree"`. argv only, never a shell string:
68
+ // branch and directory names derive from agent labels a script author wrote.
69
+ function git(argv, cwd) {
70
+ return new Promise((resolve, reject) => {
71
+ execFile("git", argv, { cwd, windowsHide: true, maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => {
72
+ if (err) reject(new Error(`git ${argv[0]}: ${String(stderr || err.message).trim()}`))
73
+ else resolve(String(stdout))
74
+ })
75
+ })
76
+ }
77
+
66
78
  function safeJson(v) {
67
79
  try { return JSON.stringify(v, null, 2) } catch { return String(v) }
68
80
  }
@@ -73,7 +85,22 @@ function summarize(name, result, stats, tracePath) {
73
85
  (stats.phases.length ? `, phases: ${stats.phases.join(" → ")}` : "")
74
86
  let body = safeJson(result)
75
87
  if (body.length > 2000) body = body.slice(0, 2000) + `\n… (truncated — full result in ${tracePath})`
76
- return `${head}\n\nResult:\n${body}\n\nTrace: ${tracePath}`
88
+ return `${head}\n\nResult:\n${body}${isolationReport(stats)}\n\nTrace: ${tracePath}`
89
+ }
90
+
91
+ // Kept worktrees are work awaiting review, and an overlap is a conflict awaiting ONE
92
+ // merger (process 8) — both are said out loud, never left for the trace to hold.
93
+ function isolationReport(stats) {
94
+ const kept = stats.worktrees || []
95
+ if (!kept.length) return ""
96
+ const lines = ["", "", `Worktrees kept (${kept.length}) — review and merge them yourself; nothing was merged:`]
97
+ for (const w of kept) lines.push(`- ${w.label}: ${w.branch} at ${w.dir} — ${w.files.length} file(s)`)
98
+ const over = stats.overlaps || []
99
+ if (over.length) {
100
+ lines.push(`CONFLICTS — ${over.length} file(s) changed by more than one agent; merge these by hand or through one merge agent:`)
101
+ for (const o of over) lines.push(`- ${o.file}: ${o.labels.join(", ")}`)
102
+ }
103
+ return lines.join("\n")
77
104
  }
78
105
 
79
106
  async function writeTrace(rootDir, runId, lines) {
@@ -142,7 +169,7 @@ export const GeneseedWorkflow = async (ctx) => {
142
169
  try { ({ createRuntime } = await import(pathToFileURL(path.join(WORKFLOWS_DIR, "_runtime.js")).href + "?t=" + Date.now())) }
143
170
  catch (e) { return `Workflow runtime unavailable: ${e?.message || e}` }
144
171
 
145
- const rt = createRuntime({ client, directory, worktree, args: normalizeArgs(argv?.args), log: sink })
172
+ const rt = createRuntime({ client, directory, worktree, git, runId, args: normalizeArgs(argv?.args), log: sink })
146
173
  let result, failed = null
147
174
  try { result = await wf.run(rt) }
148
175
  catch (e) { failed = e; sink(`✗ workflow error: ${e?.stack || e?.message || e}`) }
@@ -152,7 +179,7 @@ export const GeneseedWorkflow = async (ctx) => {
152
179
  if (!failed) sink(`# result:\n${safeJson(result)}`)
153
180
  const tracePath = await writeTrace(rootDir, runId, lines)
154
181
 
155
- if (failed) return `Workflow "${name}" failed: ${failed?.message || failed}\nTrace: ${tracePath}`
182
+ if (failed) return `Workflow "${name}" failed: ${failed?.message || failed}${isolationReport(stats)}\nTrace: ${tracePath}`
156
183
  return summarize(name, result, stats, tracePath)
157
184
  }
158
185
 
@@ -17,6 +17,7 @@
17
17
  // hard sandbox guard. See docs/specs/2026-06-09-opencode-workflow-primitive.md.
18
18
 
19
19
  import * as os from "node:os"
20
+ import * as path from "node:path"
20
21
 
21
22
  const DEBUG = !!process.env.GENESEED_DEBUG
22
23
  function dlog(m) { if (DEBUG) console.error(`[geneseed-workflow] ${m}`) }
@@ -150,13 +151,17 @@ function usageFrom(res) {
150
151
  return { input: Number(tok?.input ?? 0) || 0, output: Number(tok?.output ?? 0) || 0 }
151
152
  }
152
153
 
153
- async function runChildSession(client, { prompt, agent, model, title }) {
154
+ async function runChildSession(client, { prompt, agent, model, title, directory }) {
154
155
  if (!client?.session?.create || !client?.session?.prompt) {
155
156
  throw new Error("OpenCode SDK session API unavailable")
156
157
  }
157
158
  let created
158
- try { created = await client.session.create({ body: { title: title || "geneseed-workflow" } }) }
159
- catch { created = await client.session.create() }
159
+ // `query.directory` routes the request to that directory's instance, and the session keeps
160
+ // it — so an isolated child runs in its worktree. Never retried without it: a child that
161
+ // asked for a worktree and silently ran in the shared tree is the failure isolation exists for.
162
+ const q = directory ? { query: { directory } } : {}
163
+ try { created = await client.session.create({ body: { title: title || "geneseed-workflow" }, ...q }) }
164
+ catch (e) { if (directory) throw e; created = await client.session.create() }
160
165
  const sid = sessionId(created)
161
166
  if (!sid) throw new Error("could not create child session")
162
167
  try {
@@ -164,7 +169,7 @@ async function runChildSession(client, { prompt, agent, model, title }) {
164
169
  if (agent) body.agent = agent
165
170
  if (model) body.model = model
166
171
  let res = null
167
- try { res = await client.session.prompt({ path: { id: sid }, body }) }
172
+ try { res = await client.session.prompt({ path: { id: sid }, body, ...q }) }
168
173
  catch (e) { dlog(`prompt failed: ${e?.message || e}`) }
169
174
  const r = unwrap(res)
170
175
  let text = partsText(r?.parts ?? r?.info?.parts ?? [])
@@ -175,9 +180,52 @@ async function runChildSession(client, { prompt, agent, model, title }) {
175
180
  }
176
181
  }
177
182
 
183
+ // ---- worktree isolation ------------------------------------------------------
184
+ // `agent(prompt, { isolation: "worktree" })` — the same option Claude Code's Workflow
185
+ // takes. Parallel editors in one tree lose each other's writes (process 8, one writer per
186
+ // file), so each isolated agent gets its own branch + worktree off the root's HEAD, and
187
+ // the files it changed are recorded for `overlaps()`. A worktree with no change is
188
+ // removed with its branch; one with changes is KEPT for the parent to review and merge —
189
+ // the runtime never merges, commits, or deletes work. `git(argv, cwd)` is injected by the
190
+ // plugin so this file stays pure and testable. The worktree has no untracked files from
191
+ // the root (no node_modules, no .env): an agent that must run the project installs first.
192
+ const slug = (t) => String(t).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32) || "agent"
193
+
194
+ export async function isolate(git, root, name) {
195
+ if (typeof git !== "function") throw new Error("worktree isolation unavailable — no git executor")
196
+ if (!root) throw new Error("worktree isolation unavailable — unknown working dir")
197
+ const base = (await git(["rev-parse", "HEAD"], root)).trim()
198
+ const branch = `geneseed-wf/${name}`
199
+ const dir = path.join(os.tmpdir(), "geneseed-wf", name)
200
+ await git(["worktree", "add", "-b", branch, dir, base], root)
201
+ return { dir, branch, base }
202
+ }
203
+
204
+ export async function changedFiles(git, w) {
205
+ const lines = (out) => String(out).split("\n").map((l) => l.trim()).filter(Boolean)
206
+ const tracked = lines(await git(["diff", "--name-only", w.base], w.dir))
207
+ const untracked = lines(await git(["ls-files", "--others", "--exclude-standard"], w.dir))
208
+ return [...new Set([...tracked, ...untracked])].sort()
209
+ }
210
+
211
+ // Files more than one isolated agent changed — the conflicts a merge would otherwise
212
+ // settle by whoever landed last. [{ file, labels }], sorted by file.
213
+ export function overlaps(worktrees) {
214
+ const by = new Map()
215
+ for (const w of worktrees || []) for (const f of w.files || []) {
216
+ if (!by.has(f)) by.set(f, [])
217
+ by.get(f).push(w.label)
218
+ }
219
+ return [...by].filter(([, l]) => l.length > 1).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
220
+ .map(([file, labels]) => ({ file, labels }))
221
+ }
222
+
178
223
  // ---- the runtime ------------------------------------------------------------
179
224
  export function createRuntime(ctx) {
180
- const { client, directory, worktree, log: sink } = ctx
225
+ const { client, directory, worktree, log: sink, git } = ctx
226
+ const root = worktree || directory
227
+ const runTag = slug(ctx.runId || "run")
228
+ const worktrees = []
181
229
  const args = ctx.args || {}
182
230
  const cap = ctx.concurrency || concurrencyCap()
183
231
  const limit = createLimiter(cap)
@@ -202,9 +250,32 @@ export function createRuntime(ctx) {
202
250
  async function spawn(prompt, opts) {
203
251
  return limit(() => runChildSession(client, {
204
252
  prompt, agent: opts.agent, model: opts.model, title: `geneseed-wf:${opts.label}`,
253
+ directory: opts.directory,
205
254
  }))
206
255
  }
207
256
 
257
+ // Git calls are serialised: concurrent `worktree add -b` calls contend for the same ref
258
+ // and worktree locks, and a lost lock fails an agent that did nothing wrong.
259
+ const gitQ = createLimiter(1)
260
+ const g = typeof git === "function" ? (argv, cwd) => gitQ(() => git(argv, cwd)) : undefined
261
+
262
+ // An isolated agent that changed nothing has its worktree and branch released; one that
263
+ // changed files is KEPT and recorded. A failed release is logged, never thrown.
264
+ async function settle(w, label) {
265
+ let files
266
+ try { files = await changedFiles(g, w) }
267
+ catch (e) { emit(` ⚠ ${label}: could not list changes — worktree kept at ${w.dir}: ${e?.message || e}`); return }
268
+ if (files.length) {
269
+ worktrees.push({ label, dir: w.dir, branch: w.branch, base: w.base, files })
270
+ emit(` ⎇ ${label}: ${files.length} file(s) changed — kept at ${w.dir} (branch ${w.branch})`)
271
+ return
272
+ }
273
+ try {
274
+ await g(["worktree", "remove", w.dir], root)
275
+ await g(["branch", "-D", w.branch], root)
276
+ } catch (e) { emit(` ⚠ ${label}: could not release unchanged worktree — ${e?.message || e}`) }
277
+ }
278
+
208
279
  async function agent(prompt, opts = {}) {
209
280
  if (agentCount >= MAX_AGENTS) throw new Error(`workflow agent cap (${MAX_AGENTS}) exceeded`)
210
281
  if (budgetTotal != null && spent >= budgetTotal) {
@@ -213,8 +284,21 @@ export function createRuntime(ctx) {
213
284
  agentCount++
214
285
  const label = opts.label || String(prompt).slice(0, 40)
215
286
  emit(`· agent: ${label}`)
287
+ if (opts.isolation !== "worktree") return runAgent(prompt, { ...opts, label })
288
+ // The worktree wraps EVERY attempt, schema retries included — a retry that ran in the
289
+ // shared tree would undo the isolation it was asked for.
290
+ let w
291
+ try { w = await isolate(g, root, `${runTag}-${agentCount}-${slug(label)}`) }
292
+ catch (e) { emit(`✗ agent failed: ${label} — ${e?.message || e}`); return null }
293
+ emit(` ⎇ ${label}: worktree ${w.dir}`)
294
+ try { return await runAgent(prompt, { ...opts, label, directory: w.dir }) }
295
+ finally { await settle(w, label) }
296
+ }
297
+
298
+ async function runAgent(prompt, opts) {
299
+ const { label } = opts
216
300
  let out
217
- try { out = await spawn(prompt, { ...opts, label }) }
301
+ try { out = await spawn(prompt, opts) }
218
302
  catch (e) { emit(`✗ agent failed: ${label} — ${e?.message || e}`); return null }
219
303
  spent += out.usage?.output || 0
220
304
  if (!opts.schema) return out.text
@@ -270,7 +354,13 @@ export function createRuntime(ctx) {
270
354
 
271
355
  return {
272
356
  agent, parallel, pipeline, phase, log, budget, args, directory, worktree,
273
- _stats: () => ({ agentCount, spent, cap, phases: [...phases], trace: [...trace] }),
357
+ // Isolated agents that changed something, and the files two or more of them touched.
358
+ worktrees: () => worktrees.map((w) => ({ ...w, files: [...w.files] })),
359
+ overlaps: () => overlaps(worktrees),
360
+ _stats: () => ({
361
+ agentCount, spent, cap, phases: [...phases], trace: [...trace],
362
+ worktrees: worktrees.map((w) => ({ ...w, files: [...w.files] })), overlaps: overlaps(worktrees),
363
+ }),
274
364
  }
275
365
  }
276
366