geneseed 4.4.1 → 4.6.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 (79) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +6 -6
  3. package/SHIPPED.md +1 -1
  4. package/adapters/opencode/HOW-OPENCODE-LOADS.md +3 -2
  5. package/adapters/opencode/plugins/geneseed-context.js +195 -52
  6. package/adapters/opencode/plugins/geneseed-guard.js +42 -29
  7. package/adapters/opencode/plugins/geneseed-learn.js +1 -1
  8. package/docs/concepts/enforcement-table.md +9 -3
  9. package/docs/concepts/hooks.md +3 -0
  10. package/docs/extending.md +1 -1
  11. package/docs/guides/install-clone.md +1 -1
  12. package/docs/guides/install.md +2 -2
  13. package/docs/reference/plugin-context.md +2 -0
  14. package/docs/reference/troubleshoot-doctor.md +31 -0
  15. package/docs/reference/troubleshoot-install.md +5 -5
  16. package/docs/understand/enforced-vs-asked.md +1 -0
  17. package/harness.config.json +1 -1
  18. package/js/README.md +8 -5
  19. package/js/build/bundle.mjs +11 -2
  20. package/js/build/driver.mjs +1 -10
  21. package/js/build/emit-claude.mjs +7 -22
  22. package/js/build/emit-common.mjs +16 -9
  23. package/js/build/emit-opencode.mjs +5 -11
  24. package/js/build/generate.mjs +1 -1
  25. package/js/cli-table.json +15 -0
  26. package/js/hosts/hooks-context.mjs +1 -4
  27. package/js/hosts/hooks-done.mjs +186 -0
  28. package/js/hosts/hooks-learn.mjs +9 -4
  29. package/js/hosts/hooks.mjs +27 -8
  30. package/js/hosts/hosts.mjs +2 -0
  31. package/js/hosts/installs.mjs +47 -11
  32. package/js/hosts/native.mjs +5 -13
  33. package/js/hosts/opencode.mjs +37 -22
  34. package/js/hosts/owned.mjs +19 -5
  35. package/js/hosts/settings.mjs +17 -11
  36. package/js/hosts/shim.mjs +24 -10
  37. package/js/hosts/usage.mjs +4 -3
  38. package/js/inspect/checks-authoring.mjs +9 -7
  39. package/js/inspect/checks-constitution.mjs +2 -2
  40. package/js/inspect/checks-probe.mjs +292 -0
  41. package/js/inspect/checks-repo.mjs +13 -54
  42. package/js/inspect/cost.mjs +5 -6
  43. package/js/inspect/doctor.mjs +29 -3
  44. package/js/inspect/scan.mjs +9 -5
  45. package/js/inspect/scorecard-svg.mjs +3 -3
  46. package/js/inspect/scorecard-yaml.mjs +407 -0
  47. package/js/inspect/scorecard.mjs +7 -405
  48. package/js/inspect/status.mjs +14 -13
  49. package/js/lib/node-floor.mjs +4 -3
  50. package/js/maintain/migrate.mjs +2 -3
  51. package/js/maintain/uninstall.mjs +20 -26
  52. package/js/web/actions.mjs +2 -2
  53. package/js/web/api.mjs +36 -24
  54. package/js/web/gates-api.mjs +5 -8
  55. package/js/web/usage-api.mjs +14 -17
  56. package/package.json +2 -2
  57. package/src/skills/rule.md +11 -2
  58. package/web/dist/assets/{Activity-BZE6REef.js → Activity-Dj0izJwj.js} +1 -1
  59. package/web/dist/assets/{ActivityDetail-DCB8Cr2W.js → ActivityDetail-wjq5W02K.js} +1 -1
  60. package/web/dist/assets/Dashboard-UMbgHbzF.js +2 -0
  61. package/web/dist/assets/{Docs-D30CiqYy.js → Docs-D3ABE_ds.js} +3 -3
  62. package/web/dist/assets/{GreenhouseView-Djt-4oO5.js → GreenhouseView-C238LWBD.js} +1 -1
  63. package/web/dist/assets/{Installs-B3bw5ckh.js → Installs-XSm0a_zQ.js} +2 -2
  64. package/web/dist/assets/JournalView-BUj4Ff-6.js +1 -0
  65. package/web/dist/assets/{Laws-B9day2FV.js → Laws-yGBJFKgY.js} +1 -1
  66. package/web/dist/assets/{Library-BpIrLH-O.js → Library-CkaPVUYU.js} +1 -1
  67. package/web/dist/assets/{Loops-CxH14kyE.js → Loops-CexwOARr.js} +1 -1
  68. package/web/dist/assets/{OperatorHudView-otOz6Gha.js → OperatorHudView-D5qTQat0.js} +1 -1
  69. package/web/dist/assets/{Personal-abL6089y.js → Personal-B_Vx9a7a.js} +1 -1
  70. package/web/dist/assets/Tabs-YZbGqWPW.js +1 -0
  71. package/web/dist/assets/drawable-BoQnwVpr.js +1 -0
  72. package/web/dist/assets/index-BmbWeoTM.js +2 -0
  73. package/web/dist/assets/{index-CO-9wMJY.css → index-BoG1M8Xy.css} +1 -1
  74. package/web/dist/index.html +2 -2
  75. package/web/dist/assets/Dashboard-B4Drq5wp.js +0 -2
  76. package/web/dist/assets/JournalView-OWhx9YKg.js +0 -1
  77. package/web/dist/assets/Tabs-DjWN0i7A.js +0 -1
  78. package/web/dist/assets/drawable-C2ZsBZCv.js +0 -1
  79. package/web/dist/assets/index-CdpLpPQN.js +0 -2
@@ -13,8 +13,8 @@ few rules are enforced; the rest are asked on every host, through the root rules
13
13
 
14
14
  Derived from the code, not from this project's own planning notes: `js/hosts/hooks.mjs`'s
15
15
  `BLOCK_RULES` and gate functions, `HOOK_PINNED` in `js/inspect/checks-constitution.mjs`,
16
- `LAW_IV_BASH` and `CONSENT_RULE` in `js/hosts/settings.mjs`, and
17
- `adapters/opencode/plugins/geneseed-guard.js`. `HOOK_PINNED` is what keeps this table from
16
+ `LAW_IV_BASH` and `CONSENT_RULE` in `js/hosts/settings.mjs`, `js/hosts/hooks-done.mjs`, and
17
+ `adapters/opencode/plugins/geneseed-guard.js` and `geneseed-context.js`. `HOOK_PINNED` is what keeps this table from
18
18
  silently guarding the wrong rule after a renumber. See [ADR 0005](../adr/0005-stable-rule-ids.md).
19
19
 
20
20
  ### The enforced rules
@@ -26,6 +26,7 @@ silently guarding the wrong rule after a renumber. See [ADR 0005](../adr/0005-st
26
26
  | `persist-insight` | process | ask: rule-gate (a write to `user-rules.md` or the memory store) | same as Claude Code | ask once: guard plugin (refuses the *first* write to that path; a re-issued write goes through) | log only (warning, call goes through) |
27
27
  | `consent-before-push` | process | ask: git-gate (every `git commit`/`git push`). Togglable: build without the process pack, or drop this rule alone with `--exclude-rules` (see [Exclusions](exclusions.md)). Only this question drops; the `deletion-is-deliberate` gate stays. | same as Claude Code | ask: permission prompt (`git commit*`/`git push*`), wired only when the process pack is on; a `loop/*` push is statically allowed | log only |
28
28
  | `external-gate` | rigor | ask: rule-gate (a write under a project's `.geneseed/protected-checks.txt` list) | same as Claude Code | block: guard plugin (same list) | block (exit 2) |
29
+ | `verify-before-asserting` | law | block at Stop, once per turn: the done gate in `learn` (a turn that edited files, while a check in the project's `.geneseed/done-checks.txt` has not passed since the last edit). Opt-in per project | same as Claude Code (**unverified live**: a frozen fork, and no check runs a model turn) | remind: context plugin, on the next request (OpenCode 1 only; OpenCode 2 is not judged, and logs so) | not judged: Stop carries no transcript (log line) |
29
30
  | `commands-must-return` | ops | ask: git-gate (a recursive scan rooted at a filesystem root: `find /`, `du`, `grep -r`, `rg`, `Get-ChildItem -Recurse` at `/`, `/c`, `C:\`) | same as Claude Code | block: guard plugin (same root-scan pattern) | block (exit 2) |
30
31
 
31
32
  Bob has no "ask" tier (see [ADR 0007](../adr/0007-gates-fail-closed-per-host-ceiling.md)), so a
@@ -37,12 +38,17 @@ mechanisms: its own permission prompt (`opencode.json`, static globs) and the gu
37
38
  See [ADR 0008](../adr/0008-host-parity-ceiling-of-the-weakest-host.md) for why a gate that only
38
39
  one host can run still ships for every host it can.
39
40
 
41
+ A gate that is registered but broken looks exactly like a working one. `geneseed doctor --probe`
42
+ runs the installed Claude Code, OpenClaude and Bob gates through each host's own shell, and calls
43
+ the OpenCode guard plugin in-process, on one call each must refuse and one it must let through
44
+ ([Doctor findings](../reference/troubleshoot-doctor.md)).
45
+
40
46
  ### Everything else: asked only, on every host
41
47
 
42
48
  Every other rule carries no gate on any host. A human reads them because the model was told to
43
49
  follow them, not because a tool call was ever inspected.
44
50
 
45
- **Laws** — `one-intent-one-act` · `verify-before-asserting` · `surface-failures` ·
51
+ **Laws** — `one-intent-one-act` · `verify-before-asserting` (in a project without a done list) · `surface-failures` ·
46
52
  `data-not-orders` · `least-privilege` · `cure-the-cause` · `echo-the-intent`.
47
53
 
48
54
  **Doctrine: process** — `plan-before-acting` · `context-economy` · `read-the-docs-first` ·
@@ -37,8 +37,11 @@ Geneseed writes its hooks into the host's settings file. Every hook calls one pr
37
37
  - Content that looks like a credential (a cloud access key, a GitHub or Anthropic token, a Slack token, a private-key block) going into any file other than `.env*` gets a prompt citing *Sealed Secrets*.
38
38
  - A write to `user-rules.md`, `MEMORY.md` or a file in the install's `memory/` gets a prompt citing *Persist Insight*: whether something is a standing rule or a fact to remember is your call, settled through the `rule` [skill](skills.md).
39
39
  - A write to a check the project protects gets a prompt citing *External Gate* (on Bob, it is blocked). A project opts in with `.geneseed/protected-checks.txt` at its git root: one repo-relative file or folder per line, `#` for comments — the checks and tests the agent is judged by. A check the agent can edit is not a check. The list protects itself. Only file-write tools are seen: a shell `sed -i` or redirect is not, so run the same checks in CI as the boundary that holds.
40
+ - A write to `.geneseed/done-checks.txt`, the done gate's own list (below), gets the same *External Gate* prompt: emptying it would disarm the gate.
40
41
  - Ordinary edits pass untouched.
41
42
 
43
+ **The done gate.** Runs when a turn ends, inside `learn` and before it calls any model. A project opts in with `.geneseed/done-checks.txt` at its git root: one command per line, `#` for comments — the checks that prove the work, for example `npm run lint` and `node --test`. If the turn edited a file under that root and a listed check has not passed since the last edit, the turn does not end: the agent is told which checks are owed, citing *Verify Before Asserting*. A check counts when it ran as a shell command, unpiped (`npm test | tail` exits with `tail`'s code, so it proves nothing), and its result was not an error. The gate never runs a check itself. It blocks at most once per turn; the second stop ends the turn and leaves a line in the gate ledger. Only file-write tools count as edits: a shell `sed -i` or redirect is not seen. Without the file, nothing changes. On OpenCode the gate is a reminder on the next request (OpenCode 1 only); on Bob, whose Stop carries no transcript, the turn is not judged and a log line says so.
44
+
42
45
  **learn.** Takes the tail of the transcript and asks a model to distil what is worth keeping into one-fact-per-file entries under `memory/`. It is shown the facts already stored, so it updates one with the merged text rather than writing a twin (the old version is kept in `memory/archive/`), and moves one the session proved obsolete into `memory/archive/`. It needs `GENESEED_LLM` set to a model command, for example:
43
46
 
44
47
  ```bash
package/docs/extending.md CHANGED
@@ -125,7 +125,7 @@ Notes that matter:
125
125
 
126
126
  ### The gates CI does not run
127
127
 
128
- - **The mutation matrix itself.** `tests/mutate.mjs` — 33 rows, each a one-string edit to a
128
+ - **The mutation matrix itself.** `tests/mutate.mjs` — 35 rows, each a one-string edit to a
129
129
  product file with a written argument for which gate should catch it. The full run is **in no
130
130
  workflow** and should not be: it mutates the product tree 33 times and runs a gate per edit.
131
131
  Run `node tests/mutate.mjs --all` (or `--only M7`) by hand when you change something a gate is
@@ -6,7 +6,7 @@ kind: "concept"
6
6
  section: "Install"
7
7
  description: "Install from a git clone when npm is out of reach, or when you want to change the harness."
8
8
  ---
9
- Clone the repository when npm is out of reach (for example, a corporate network with no registry access), or when you want to change the harness rather than just use it. A clone needs **git** as well as Node 22.3 or newer. git is what updates the clone.
9
+ Clone the repository when npm is out of reach (for example, a corporate network with no registry access), or when you want to change the harness rather than just use it. A clone needs **git** as well as Node 22.7 or newer. git is what updates the clone.
10
10
 
11
11
  Each OS has a one-step installer at the root of the clone. It checks for Node and git. If one is missing, it names what to install and where to get it, then stops. **It never installs anything itself.** When the checks pass, it runs the setup wizard and offers to open the web console.
12
12
 
@@ -10,7 +10,7 @@ Geneseed builds a harness (the rules, agents, skills and hooks your AI coding to
10
10
 
11
11
  ## The 5-minute path
12
12
 
13
- The only prerequisite is **Node 22.3 or newer**.
13
+ The only prerequisite is **Node 22.7 or newer**.
14
14
 
15
15
  ```
16
16
  npx geneseed setup
@@ -30,7 +30,7 @@ Next: [Verify it works](verify.md). To see what the install just wrote, read [Wh
30
30
 
31
31
  ## Prerequisites
32
32
 
33
- - **Node 22.3 or newer.** This is the only hard requirement. Geneseed has zero runtime dependencies, and nothing it installs needs a second interpreter.
33
+ - **Node 22.7 or newer.** This is the only hard requirement. Geneseed has zero runtime dependencies, and nothing it installs needs a second interpreter.
34
34
  - **Your agent tool**: OpenCode (recommended), Claude Code, IBM Bob, OpenClaude, or any tool that reads a root instructions file.
35
35
  - **git**, for a clone only. A clone updates itself with `git pull`.
36
36
  - *Optional:* a document converter (MarkItDown, Pandoc or Docling) if you want the agent to read PDFs and Office files. See [Reading non-markdown docs](read-non-markdown.md).
@@ -17,6 +17,8 @@ One of the [OpenCode plugins](opencode-plugins.md). Its switches are also listed
17
17
 
18
18
  **When it runs.** On every request: the context rides invisibly in the request's message list, so it also survives compaction. If your OpenCode build lacks that (experimental) hook, the plugin notices and falls back to one visible message at session start, re-pushed when the session compacts.
19
19
 
20
+ **The done gate's reminder.** In a project with `.geneseed/done-checks.txt` (see [Hooks](../concepts/hooks.md)), a request that follows a turn which edited files without every listed check passing since gets one more invisible message naming the checks owed. OpenCode has no turn-end event a plugin can block on, so this is a reminder, not a block. OpenCode 1 only: on OpenCode 2 the turn is not judged, and the plugin logs that once. `GENESEED_CONTEXT_INJECT=off` does not turn it off; removing the file does.
21
+
20
22
  **What you see.** Normally nothing — the context is there, but not shown. With the visible fallback, a `PROJECT CONTEXT` block at the top of the session.
21
23
 
22
24
  **Switches.**
@@ -35,6 +35,37 @@ If the drift is your own edits, export them first — `geneseed diff` (or the co
35
35
 
36
36
  **Confirm.** `geneseed doctor` no longer reports the bundle.
37
37
 
38
+ ## `[probe] … registered but not refusing` / `refuses everything` / `does not run`
39
+
40
+ > `[probe] ~/.claude: git-gate under Git Bash registered but not refusing — …`
41
+
42
+ These come only from `geneseed doctor --probe`. It runs each installed gate exactly as the host's
43
+ settings spell it, through the host's own shell, on `git reset --hard HEAD` (which must be refused)
44
+ and `git status` (which must go through). An OpenCode install's guard plugin has no command to
45
+ run: the probe imports it and calls its hook through both plugin APIs (V1 and V2), with `rm -rf /`
46
+ as the call it must refuse. `GENESEED_GUARD=off` or `warn` in your terminal makes that a note,
47
+ not a failure.
48
+
49
+ **Cause.**
50
+ - **Not refusing:** the gate launches but lets a destructive call through. Usually something in
51
+ the install tells it to stand down: an exclusion covering the temp folder, or a command that
52
+ lost its `--host bob`.
53
+ - **Refuses everything:** the gate asks on every call. This is typically the hooks emitted for
54
+ one shell and run under another.
55
+ - **Does not run:** the shim or its runner is gone, or the shell is missing.
56
+
57
+ **Fix.**
58
+
59
+ ```bash
60
+ geneseed rebuild-all
61
+ ```
62
+
63
+ **Confirm.** `geneseed doctor --probe` prints `[probe] N gate(s) refused a destructive call and let
64
+ a safe one through`.
65
+
66
+ The probe runs under *this terminal's* environment (PATH, HOME), not the host app's. A host started
67
+ with a different environment can still break a gate that passes here.
68
+
38
69
  ## `registry.json unreadable` / `registry.json is not valid JSON`
39
70
 
40
71
  > `[authoring] registry.json unreadable: …`
@@ -12,7 +12,7 @@ Each entry gives the likely cause, the fix, and how to confirm it worked. First
12
12
 
13
13
  The one-step installer (`./install`, `install.cmd`, `install.command`) checks Node first and stops:
14
14
 
15
- > `xx Node v20.11.0 is too old — Geneseed needs 22.3 or newer.`
15
+ > `xx Node v20.11.0 is too old — Geneseed needs 22.7 or newer.`
16
16
 
17
17
  **Cause.** No Node on PATH, or an older one — Linux distribution packages are often behind.
18
18
 
@@ -24,15 +24,15 @@ The one-step installer (`./install`, `install.cmd`, `install.command`) checks No
24
24
  node --version
25
25
  ```
26
26
 
27
- prints 22.3 or newer, and the installer's first line reads `ok Node v…`.
27
+ prints 22.7 or newer, and the installer's first line reads `ok Node v…`.
28
28
 
29
- ## `geneseed needs Node 22.3 or newer`
29
+ ## `geneseed needs Node 22.7 or newer`
30
30
 
31
31
  Run without the installer — `npx geneseed`, a global npm install, or a hook — on an older Node, every Geneseed command stops with one sentence:
32
32
 
33
- > `geneseed needs Node 22.3 or newer; this is Node 20.18.1. Install a current Node from https://nodejs.org (or your package manager), then run the command again.`
33
+ > `geneseed needs Node 22.7 or newer; this is Node 20.18.1. Install a current Node from https://nodejs.org (or your package manager), then run the command again.`
34
34
 
35
- **Cause.** The `node` on PATH is older than 22.3. npm only *warns* about the engine requirement, so a global install succeeds anyway. A hook prints the same sentence and lets the tool call through rather than block it, so on Claude Code, Bob or OpenClaude the hooks are off until Node is upgraded.
35
+ **Cause.** The `node` on PATH is older than 22.7. npm only *warns* about the engine requirement, so a global install succeeds anyway. A hook prints the same sentence and lets the tool call through rather than block it, so on Claude Code, Bob or OpenClaude the hooks are off until Node is upgraded.
36
36
 
37
37
  **Fix.** Install the current Node.js LTS (see above), open a **new** terminal, and run the command again.
38
38
 
@@ -27,6 +27,7 @@ On Claude Code and OpenClaude the hooks never block silently. They make the host
27
27
  - **A scan of a whole filesystem**: a recursive `find`, `du`, `grep -r`, `rg` or `Get-ChildItem -Recurse` rooted at `/`, `/c` or `C:\`, which in Git Bash walks every drive for hours. Wired in every build.
28
28
  - **Credentials**: a write or edit whose content looks like a real key (AWS, GitHub, Anthropic, Slack, or a private-key block), anywhere except a `.env` file.
29
29
  - **Your rules and memory**: any write by the agent to `user-rules.md` or to the memory store. Whether something is a standing rule or a fact to remember is your call.
30
+ - **Ending a turn without the proof**: only in a project that lists its checks in `.geneseed/done-checks.txt`. A turn that edited files cannot end until every listed check has passed since the last edit. The agent is stopped once, then the turn may end.
30
31
 
31
32
  If a gate crashes, it asks instead of letting the call through. If it cannot start at all (a deleted or broken shim) or times out, Claude Code blocks the call instead: see [Troubleshoot hooks](../reference/troubleshoot-hooks.md) to recover. Other shell commands, `rm -rf` included, go through Claude Code's normal permission flow.
32
33
 
@@ -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": "4.4.1"
5
+ "version": "4.6.0"
6
6
  }
package/js/README.md CHANGED
@@ -71,6 +71,7 @@ user co-owns, and the hook verbs those emitted configs then run.
71
71
  | `hosts.mjs` | The four host config dirs (opencode/claude/bob/openclaude), their `family` (`CLAUDE_STYLE`), plus `resolvePath`/`expanduser` |
72
72
  | `hooks.mjs` | The gate verbs, run on every tool call: `git-gate`, `rule-gate`, `tool-gate` (the two gates fused, for Bob), plus `ask`/the gate ledger/`guardGate`. `--host` picks the verdict dialect |
73
73
  | `hooks-context.mjs` | The `context` verb — SessionStart: session files plus the repo's context docs |
74
+ | `hooks-done.mjs` | The done gate (#289): on Stop, inside `learn` and before its spawn, blocks a turn that edited files until `.geneseed/done-checks.txt`'s checks passed after the last edit |
74
75
  | `hooks-learn.mjs` | The `learn` verb — Stop/SubagentStop/PreCompact distiller; the hook path's ONE spawn (`$GENESEED_LLM`) |
75
76
  | `hooks-prims.mjs` | The Python path/text primitives the hook verbs share (`splitLines`, `readStdin`, a never-throwing `isFile`) |
76
77
  | `usage.mjs` | The usage counter `tool-gate` runs: one `{day, kind, name}` line per Skill or subagent call into `notebook/usage.jsonl`. Hook path: builtins only, silent, never throws |
@@ -81,7 +82,7 @@ user co-owns, and the hook verbs those emitted configs then run.
81
82
  | `native.mjs` | Capability specs → host-native subagents and skills. The impure half of the emit |
82
83
  | `opencode.mjs` | OpenCode-only extras: colour themes, primary agent, `/commands`, plugins, overrides stub |
83
84
  | `installs.mjs` | Detects deployed installs and reads back their theme, footprint, posture, mode and loop trust preset |
84
- | `mcp.mjs` | MCP server presets, the per-host `mcpServers` config read/write, and toggle state |
85
+ | `mcp.mjs` | Reads each host's MCP config (`mcpServers`, or OpenCode's `mcp`) and where it lives — read-only since 2026-10: the user edits the file, Geneseed only shows it |
85
86
  | `link.mjs` | `geneseed link`/`unlink` — the PATH shim, and the only Windows USER-Path registry edit |
86
87
  | `owned.mjs` | Manifest rel -> path on disk decisions: `ownedWithin`, the containment boundary `uninstall.mjs` and the driver's prune both go through before acting on an `owned` list entry |
87
88
 
@@ -100,8 +101,9 @@ Everything that inspects an already-built or already-deployed harness and report
100
101
  |---|---|
101
102
  | `doctor.mjs` | The `geneseed doctor` **driver** only: picks themes, runs the checks, prints the verdict |
102
103
  | `checks-build.mjs` | Gates over what the build PRODUCED: links, tokens, theme parity, OpenCode colours, bundle drift |
103
- | `checks-repo.mjs` | Gates over the repo's own records: `registry.json`, committed secrets, the hook shim, vendored pins |
104
- | `checks-authoring.mjs` | `authoringProblems`, the aggregate self-consistency gate: spec purpose lines, the learn-prompt literal, `node --check` over the plugins (the doctor's one spawn), then the groups below |
104
+ | `checks-repo.mjs` | Gates over the repo's own records: `registry.json`, committed secrets, the hook shim, vendored pins and folder skills, skill aliases and side files, this module map, the host tables, the scorecard ratchet |
105
+ | `checks-authoring.mjs` | `authoringProblems`, the aggregate self-consistency gate: spec purpose lines, the learn-prompt literal, `node --check` over the plugins (the doctor's one default spawn), then the groups below |
106
+ | `checks-probe.mjs` | `doctor --probe` (opt-in): runs each live install's own gate command through the host's shell on a must-refuse and a must-pass payload — the doctor's second spawn; the OpenCode guard plugin is imported and called in-process instead, through both plugin APIs |
105
107
  | `checks-constitution.mjs` | The constitution: rule declarations and citations, LEAN blocks, `HOOK_PINNED`, theme vocabulary, the console's `LAW_META`/`DOCTRINE_META` |
106
108
  | `checks-counts.mjs` | The hand-written counts: AGENT.md tables, README badges and prose, `SHIPPED.md`, held against `src/` |
107
109
  | `scan.mjs` | The shared walk/sort/strip/test primitives every `checks-*` uses. Owns no judgement itself |
@@ -112,7 +114,8 @@ Everything that inspects an already-built or already-deployed harness and report
112
114
  | `diff.mjs` | `geneseed diff` — what a deployed install has that a fresh render of `src/` does not |
113
115
  | `excludes.mjs` | `geneseed exclude add|remove|list` — the WRITER for `excludes.json`; `hooks.mjs` only reads it |
114
116
  | `registry.mjs` | `installs.json` under XDG — the persistent deploy-root list `rebuild-all` re-emits into |
115
- | `scorecard.mjs` | `geneseed scorecard` — a port of the AI Harness Scorecard (upstream pinned in its docblock): the repo walk, the CI-YAML subset parser, `assessRepo`, the text/JSON renderers. Pure: no spawn, no network |
117
+ | `scorecard.mjs` | `geneseed scorecard` — a port of the AI Harness Scorecard (upstream pinned in its docblock): the repo walk, the CI-YAML parsing, `assessRepo`, the text/JSON renderers. Pure: no spawn, no network |
118
+ | `scorecard-yaml.mjs` | The YAML 1.1 subset the scorecard reads CI configs with, plus Python's `str()`/`bool()` over its values; `scorecard.mjs` re-exports it. Its ceiling is in its `ponytail:` note |
116
119
  | `scorecard-svg.mjs` | The scorecard as an SVG card for a README (`geneseed scorecard --svg`): grade plus one bar per pillar, deterministic so doctor can compare the committed card byte for byte |
117
120
  | `scorecard-checks.mjs` | The scorecard's 31 checks in five pillars, ported line for line — ids, points, partial scores, evidence and fixes as upstream writes them |
118
121
 
@@ -136,7 +139,7 @@ into files users have committed, so change the test that states it in the same c
136
139
  | `udiff.mjs` | `geneseed diff`'s own difflib clone — the alignment IS the printed hunks — plus `splitLines` |
137
140
  | `proc.mjs` | One constant: the `windowsHide` flag every spawn in `js/` must reuse |
138
141
  | `prompt.mjs` | The line-mode stdin readers — `ask`, `promptLine`, `confirm`, `askChoice` — read a byte at a time so they stop where Enter stops. Shared by `setup`, `uninstall` and `web` without dragging in the wizard |
139
- | `node-floor.mjs` | The first import of all three binaries: below Node 22.3, one sentence on stderr and exit 1 (0 in the hook) instead of a stack trace |
142
+ | `node-floor.mjs` | The first import of all three binaries: below Node 22.7, one sentence on stderr and exit 1 (0 in the hook) instead of a stack trace |
140
143
 
141
144
  **Before editing:** parse with `parseJson`, never `JSON.parse` — the latter collapses int `1` and
142
145
  float `1.0` into one double, and that value gets written into a user's file. Do not swap in a diff
@@ -24,6 +24,16 @@ import {
24
24
  import { writeVersion } from './version.mjs';
25
25
  import { existsSync, mkdirSync, renameSync, rmSync } from 'node:fs';
26
26
 
27
+ /**
28
+ * Seed the memory index, then move a flat one to topics — the pair every emit runs on its
29
+ * memory store, in that order. Here and not in `js/hosts/memory-files.mjs`: `stubs.mjs`
30
+ * imports that module, so the reverse import would be a cycle.
31
+ */
32
+ export function ensureMemoryStore(memDir) {
33
+ ensureMemoryIndex(memDir);
34
+ migrateMemoryIndex(memDir);
35
+ }
36
+
27
37
  // ---------------------------------------------------------------------------
28
38
  // Source completeness
29
39
  // ---------------------------------------------------------------------------
@@ -223,8 +233,7 @@ export function build(cfg, themeName, out, { footprint = 'full', nativeCatalog =
223
233
  ensureRulesStub(out);
224
234
  ensureProfileStub(out);
225
235
  ensureBundleGitignore(out);
226
- ensureMemoryIndex(path.join(out, theme[SRC_DIR_TOKENS.memory] ?? 'memory'));
227
- migrateMemoryIndex(path.join(out, theme[SRC_DIR_TOKENS.memory] ?? 'memory'));
236
+ ensureMemoryStore(path.join(out, theme[SRC_DIR_TOKENS.memory] ?? 'memory'));
228
237
  ensureNotebookIndex(path.join(out, theme[SRC_DIR_TOKENS.notebook] ?? 'notebook'));
229
238
  process.stdout.write(`[geneseed] built theme '${themeName}' -> ${out} (${items.length} files)\n`);
230
239
  }
@@ -51,7 +51,7 @@ import {
51
51
  openclaudeConfigDir, hostCatalogsNatively,
52
52
  } from '../hosts/hosts.mjs';
53
53
  import {
54
- hashesOf, ownedHashes, ownedState, ownedWithin,
54
+ hashesOf, ownedHashes, ownedState, ownedWithin, recordedDenies,
55
55
  } from '../hosts/owned.mjs';
56
56
 
57
57
  // P5d moved these out of this file for the reason P5c moved the host resolvers: `harness
@@ -257,15 +257,6 @@ function readManifest(manifestPath) {
257
257
  return { existed, doc };
258
258
  }
259
259
 
260
- /**
261
- * An OpenCode manifest's `skill_denies` — the `permission.skill` denies Geneseed itself added
262
- * (see `opencodeLayer`) — or `[]` when absent or malformed, which sweeps nothing.
263
- */
264
- function recordedDenies(doc) {
265
- const d = doc && doc.skill_denies;
266
- return Array.isArray(d) ? d.filter((n) => typeof n === 'string') : [];
267
- }
268
-
269
260
  /** The "+ primary agent, N command(s)" tail both OpenCode summary lines share. */
270
261
  function extrasTail(stats) {
271
262
  const extras = [...(stats.primary ? ['primary agent'] : []),
@@ -7,24 +7,21 @@
7
7
  */
8
8
  import path from 'node:path';
9
9
  import { VERSION_MARKER } from '../hosts/hosts.mjs';
10
- import { migrateMemoryIndex } from '../hosts/memory-files.mjs';
11
10
  import { claimer, loadAgentOverrides, writeNativeLayer } from '../hosts/native.mjs';
12
11
  import { ensureAgentOverridesStub, writeAliasCommands } from '../hosts/opencode.mjs';
13
12
  import {
14
13
  managedBlockRemove, managedBlockWrite, mergeClaudeSettings,
15
14
  unwireClaudeExcludes, unwireClaudeSettings, wireClaudeExcludes,
16
15
  } from '../hosts/settings.mjs';
17
- import { claimLeaf } from '../hosts/owned.mjs';
18
- import { writeText } from '../lib/fs.mjs';
19
16
  import { get, isDict, isTruthy } from '../lib/json.mjs';
20
17
  import { relPosix } from '../lib/text.mjs';
21
- import { assertSourceComplete, phaseLog } from './bundle.mjs';
18
+ import { assertSourceComplete, phaseLog, ensureMemoryStore } from './bundle.mjs';
22
19
  import {
23
- globalMemory, globalNotebook, shipLeanLaws, stripCapabilityLinks,
20
+ claimAndWrite, globalMemory, globalNotebook, shipLeanLaws, stripCapabilityLinks,
24
21
  } from './emit-common.mjs';
25
22
  import { renderAll, renderFile } from './render.mjs';
26
23
  import {
27
- BOB_RULES_STUB, ensureContextStub, ensureExcludesStub, ensureMemoryIndex, ensureNotebookIndex,
24
+ BOB_RULES_STUB, ensureContextStub, ensureExcludesStub, ensureNotebookIndex,
28
25
  ensureProfileStub,
29
26
  ensureRulesStub, ensureWikiStub, WIKI_FILE,
30
27
  } from './stubs.mjs';
@@ -100,16 +97,8 @@ export function emitClaudeRender(cfg, job) {
100
97
  // slim shadow stub whose only job is to have the same filename.
101
98
  if (isBob && agentText !== null) {
102
99
  const rulesMd = path.join(cfgDir, 'rules', 'geneseed.md');
103
- // `claimLeaf` BEFORE `mkdirSync`: `safeLeaf` walks up to the nearest EXISTING ancestor
104
- // (`resolvePath`'s own contract), so it needs no directory created first — and creating
105
- // `rules/` first would plant an empty directory at a linked ancestor's target the moment
106
- // before the refusal fires.
107
- if (claimLeaf(cfgDir, rulesMd, 'rules/geneseed.md')) {
108
- mkdirSync(path.dirname(rulesMd), { recursive: true });
109
- writeText(rulesMd, scope === 'project' ? BOB_RULES_STUB
110
- : stripCapabilityLinks(prefixedAgentText('../') || agentText));
111
- owned.push('rules/geneseed.md');
112
- }
100
+ claimAndWrite(cfgDir, rulesMd, 'rules/geneseed.md', scope === 'project' ? BOB_RULES_STUB
101
+ : stripCapabilityLinks(prefixedAgentText('../') || agentText), owned);
113
102
  }
114
103
 
115
104
  ensureAgentOverridesStub(cfg, cfgDir);
@@ -146,8 +135,7 @@ export function emitClaudeRender(cfg, job) {
146
135
  }
147
136
 
148
137
  const memStatus = globalMemory(cfgDir, items, out, cfg.src);
149
- ensureMemoryIndex(path.join(cfgDir, 'memory'));
150
- migrateMemoryIndex(path.join(cfgDir, 'memory'));
138
+ ensureMemoryStore(path.join(cfgDir, 'memory'));
151
139
  const nbStatus = globalNotebook(cfgDir, items, out, cfg.src);
152
140
  ensureNotebookIndex(path.join(cfgDir, 'notebook'));
153
141
  // context.json too: the root names it as the project's own manifest, and until 2026-10 this
@@ -172,10 +160,7 @@ export function emitClaudeRender(cfg, job) {
172
160
  // `!existsSync(gi)` is exactly the case a DANGLING symlink also satisfies — it reads
173
161
  // as absent — so `claimLeaf` (which lstats) runs before the write, not instead of
174
162
  // this check.
175
- if (claimLeaf(cfgDir, gi, '.gitignore')) {
176
- writeText(gi, `${giLines.join('\n')}\n`);
177
- owned.push('.gitignore');
178
- }
163
+ claimAndWrite(cfgDir, gi, '.gitignore', `${giLines.join('\n')}\n`, owned);
179
164
  } else if (oldOwned.includes('.gitignore')) {
180
165
  owned.push('.gitignore');
181
166
  }
@@ -63,7 +63,7 @@ export const TMPL_SPEC_RE = /\{\{DIR_(AGENTS|SKILLS)\}\}\/([A-Za-z0-9_-]+)\.md/g
63
63
  * `Path.rglob("*")` restricted to files, skipping `__pycache__` as Python's callers do.
64
64
  *
65
65
  * `recursive: true, withFileTypes: true` (Node >= 20.1, `parentPath` >= 21.4 — this repo
66
- * requires >= 22.3) replaces a hand-rolled walk; the order it yields is NOT the walk's
66
+ * requires >= 22.7) replaces a hand-rolled walk; the order it yields is NOT the walk's
67
67
  * order, but `sourceFingerprint`, this function's one caller, re-sorts every result by a
68
68
  * plain string key before it is ever observed, so no caller depends on it.
69
69
  */
@@ -184,15 +184,22 @@ export function shipLeanLaws(items, theme, cfgDir, owned, footprint) {
184
184
  // nothing to detect, not because the gate cannot see it.
185
185
  if (text !== null && parts.length && dirs.has(parts[0])) {
186
186
  const dest = path.join(cfgDir, ...parts);
187
- // `claimLeaf` BEFORE `mkdirSync`, for the same reason `emitClaudeRender`'s rules stub
188
- // does: `safeLeaf` needs no directory created first, and creating one ahead of the
189
- // refusal would plant it at a linked ancestor's target.
190
- if (claimLeaf(cfgDir, dest, rel)) {
191
- mkdirSync(path.dirname(dest), { recursive: true });
192
- writeText(dest, text);
193
- owned.push(rel);
194
- }
187
+ claimAndWrite(cfgDir, dest, rel, text, owned);
195
188
  }
196
189
  }
197
190
  }
198
191
 
192
+ /**
193
+ * Claim-on-create one leaf: write `text` to `dest` and own `rel` only when `claimLeaf`
194
+ * allows it. `claimLeaf` runs BEFORE `mkdirSync`: `safeLeaf` walks up to the nearest
195
+ * EXISTING ancestor (`resolvePath`'s own contract), so it needs no directory created first —
196
+ * and creating the parent first would plant an empty directory at a linked ancestor's target
197
+ * the moment before the refusal fires. A leaf directly in an existing `root` makes the
198
+ * `mkdirSync` a no-op.
199
+ */
200
+ export function claimAndWrite(root, dest, rel, text, owned) {
201
+ if (!claimLeaf(root, dest, rel)) return;
202
+ mkdirSync(path.dirname(dest), { recursive: true });
203
+ writeText(dest, text);
204
+ owned.push(rel);
205
+ }
@@ -8,23 +8,21 @@
8
8
  */
9
9
  import path from 'node:path';
10
10
  import { VERSION_MARKER } from '../hosts/hosts.mjs';
11
- import { migrateMemoryIndex } from '../hosts/memory-files.mjs';
12
11
  import { claimer, loadAgentOverrides, writeNativeLayer } from '../hosts/native.mjs';
13
12
  import {
14
13
  copyPlugins, copyWorkflows, ensureAgentOverridesStub, writeColorThemes, writeCommandLayer,
15
14
  writeAliasCommands, writePonytailCommand, writePrimaryAgent, writeTheme,
16
15
  } from '../hosts/opencode.mjs';
17
16
  import { mergeOpencodeJson, opencodeSentinelWrite } from '../hosts/settings.mjs';
18
- import { claimLeaf } from '../hosts/owned.mjs';
19
17
  import { isFile, readText, writeText } from '../lib/fs.mjs';
20
18
  import { relPosix } from '../lib/text.mjs';
21
- import { assertSourceComplete, build, phaseLog } from './bundle.mjs';
19
+ import { assertSourceComplete, build, phaseLog, ensureMemoryStore } from './bundle.mjs';
22
20
  import {
23
- globalMemory, globalNotebook, shipLeanLaws, stripCapabilityLinks,
21
+ claimAndWrite, globalMemory, globalNotebook, shipLeanLaws, stripCapabilityLinks,
24
22
  } from './emit-common.mjs';
25
23
  import { renderAll } from './render.mjs';
26
24
  import {
27
- ensureExcludesStub, ensureMemoryIndex, ensureNotebookIndex, ensureProfileStub, ensureRulesStub,
25
+ ensureExcludesStub, ensureNotebookIndex, ensureProfileStub, ensureRulesStub,
28
26
  ensureWikiStub,
29
27
  } from './stubs.mjs';
30
28
  import { writeVersion } from './version.mjs';
@@ -216,10 +214,7 @@ export function emitOpencodeGlobalRender(cfg, job) {
216
214
  // store are siblings, so `memory/` resolves from AGENT.md's own location and stays
217
215
  // hermetic — no absolute path a doctor check would flag as an escape.
218
216
  const agentDest = path.join(cfgDir, 'AGENT.md');
219
- if (claimLeaf(cfgDir, agentDest, 'AGENT.md')) {
220
- writeText(agentDest, stripCapabilityLinks(agentText));
221
- owned.push('AGENT.md');
222
- }
217
+ claimAndWrite(cfgDir, agentDest, 'AGENT.md', stripCapabilityLinks(agentText), owned);
223
218
  }
224
219
  // Co-owned, so never in `owned` — see `opencodeSentinelWrite`.
225
220
  opencodeSentinelWrite(cfgDir);
@@ -233,8 +228,7 @@ export function emitOpencodeGlobalRender(cfg, job) {
233
228
  });
234
229
 
235
230
  const memStatus = globalMemory(cfgDir, items, out, cfg.src);
236
- ensureMemoryIndex(path.join(cfgDir, 'memory'));
237
- migrateMemoryIndex(path.join(cfgDir, 'memory'));
231
+ ensureMemoryStore(path.join(cfgDir, 'memory'));
238
232
  const nbStatus = globalNotebook(cfgDir, items, out, cfg.src);
239
233
  ensureNotebookIndex(path.join(cfgDir, 'notebook'));
240
234
  ensureWikiStub(cfgDir);
@@ -223,7 +223,7 @@ export function installProfile(host, scope, root, notify = null) {
223
223
  // one of them. `null` (no marker) stays `null` so the argv omits the flag, as it always did.
224
224
  const excludeRules = excludedRulesOfDir(root, host);
225
225
  // Skills left out stay left out: an upgrade that forgot them would put them all back.
226
- const excludeSkills = excludedSkillsOfDir(root, notify, host);
226
+ const excludeSkills = excludedSkillsOfDir(root, host, notify);
227
227
  const out = scope === 'global' ? null : root;
228
228
  const argv = setupBuildArgs(theme, emit, out, out, footprint, posture, mode, doctrines,
229
229
  PACK_ORDER, excludeRules, trust, excludeSkills);
package/js/cli-table.json CHANGED
@@ -473,6 +473,21 @@
473
473
  "is_flag": true,
474
474
  "type": null,
475
475
  "hidden": false
476
+ },
477
+ {
478
+ "names": [
479
+ "--probe"
480
+ ],
481
+ "dest": "probe",
482
+ "metavar": null,
483
+ "help": "also run each installed gate on a destructive and a safe payload: it must refuse the first and let the second through. Claude Code, OpenClaude and Bob gates run as their settings spell them, through the host's shell; the OpenCode guard plugin is imported and called in-process (runs under this terminal's environment, not the host app's)",
484
+ "choices": null,
485
+ "default": false,
486
+ "required": false,
487
+ "nargs": "0",
488
+ "is_flag": true,
489
+ "type": null,
490
+ "hidden": false
476
491
  }
477
492
  ],
478
493
  "mutex": []
@@ -150,7 +150,7 @@ export { SESSION_FILES, SEED_SHA256 };
150
150
  const MAP_COUNT_RE = /^- \[([^\]]*)\]\(topics\/[^)]+\.md\) — (\d+) facts?$/;
151
151
  // Past this many facts a topic file stops being a quick read, and the curator splits it.
152
152
  const CURATE_TOPIC_MAX = 40;
153
- const CURATE_NUDGE = '[memory] A topic holds over 40 facts, or facts sit in misc: offer the user '
153
+ const CURATE_NUDGE = `[memory] A topic holds over ${CURATE_TOPIC_MAX} facts, or facts sit in misc: offer the user `
154
154
  + 'to dispatch the memory-curator agent to merge, re-topic and archive this store.';
155
155
  export { CURATE_TOPIC_MAX, CURATE_NUDGE };
156
156
 
@@ -193,9 +193,6 @@ export function sessionFiles(hookRoot) {
193
193
  return found;
194
194
  }
195
195
 
196
- // Moved to `hosts.mjs` so the gates and `learn` share it; re-exported for this verb's callers.
197
- export { globalHookStandingDown };
198
-
199
196
  /** `_disp` — relative to the repo root when it sits under it, else verbatim. */
200
197
  function disp(pathStr, root) {
201
198
  // A path on another drive has no relative form, so it is printed as given. `path.relative`