geneseed 4.6.0 → 4.8.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 (73) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +1 -1
  3. package/SHIPPED.md +1 -1
  4. package/adapters/claude-code/README.md +3 -1
  5. package/adapters/opencode/HOW-OPENCODE-LOADS.md +1 -1
  6. package/adapters/opencode/README.md +5 -4
  7. package/adapters/opencode/plugins/geneseed-guard.js +565 -37
  8. package/bin/geneseed-hook.mjs +9 -3
  9. package/docs/concepts/enforcement-table.md +1 -1
  10. package/docs/concepts/exclusions.md +20 -1
  11. package/docs/concepts/hooks.md +11 -3
  12. package/docs/concepts/host-matrix.md +3 -3
  13. package/docs/reference/plugin-guard.md +3 -3
  14. package/docs/reference/troubleshoot-doctor.md +4 -2
  15. package/docs/understand/a-day.md +6 -6
  16. package/docs/understand/design-choices-gates.md +2 -2
  17. package/docs/understand/enforced-vs-asked.md +4 -4
  18. package/docs/understand/harness.md +1 -1
  19. package/docs/understand/machine-claude.md +1 -1
  20. package/docs/understand/machine-opencode.md +2 -2
  21. package/docs/web-ui.md +3 -1
  22. package/harness.config.json +1 -1
  23. package/js/README.md +2 -1
  24. package/js/build/args.mjs +4 -1
  25. package/js/build/driver.mjs +11 -7
  26. package/js/build/emit-claude.mjs +4 -3
  27. package/js/build/emit-opencode.mjs +2 -1
  28. package/js/build/generate.mjs +13 -4
  29. package/js/build/render.mjs +4 -0
  30. package/js/build/source.mjs +4 -1
  31. package/js/cli-table.json +45 -0
  32. package/js/hosts/gitref.mjs +47 -1
  33. package/js/hosts/hooks.mjs +19 -39
  34. package/js/hosts/installs.mjs +20 -0
  35. package/js/hosts/loss.mjs +522 -0
  36. package/js/hosts/settings.mjs +79 -75
  37. package/js/inspect/checks-probe.mjs +18 -6
  38. package/js/inspect/diff.mjs +4 -1
  39. package/js/inspect/status.mjs +3 -1
  40. package/js/inspect/validate.mjs +6 -4
  41. package/js/maintain/uninstall.mjs +5 -2
  42. package/js/maintain/update.mjs +1 -1
  43. package/js/web/actions.mjs +20 -3
  44. package/js/web/api.mjs +4 -2
  45. package/package.json +1 -1
  46. package/src/AGENT.md.tmpl +1 -1
  47. package/src/laws/universal.md +17 -17
  48. package/src/skills/worktree.md +1 -1
  49. package/web/dist/assets/{Activity-Dj0izJwj.js → Activity-B-XbKlzu.js} +1 -1
  50. package/web/dist/assets/{ActivityDetail-wjq5W02K.js → ActivityDetail-8oq0fycD.js} +1 -1
  51. package/web/dist/assets/{Dashboard-UMbgHbzF.js → Dashboard-C4EMz5mf.js} +2 -2
  52. package/web/dist/assets/{Docs-D3ABE_ds.js → Docs-mq4XDQK_.js} +1 -1
  53. package/web/dist/assets/FilterInput-Ikvi-pS9.js +1 -0
  54. package/web/dist/assets/{GreenhouseView-C238LWBD.js → GreenhouseView-D1WOtq3n.js} +1 -1
  55. package/web/dist/assets/{Installs-XSm0a_zQ.js → Installs-DrqPaLDM.js} +1 -1
  56. package/web/dist/assets/{JournalView-BUj4Ff-6.js → JournalView-BYpASU-L.js} +1 -1
  57. package/web/dist/assets/Laws-A3PFwiPD.js +3 -0
  58. package/web/dist/assets/{Library-CkaPVUYU.js → Library-BCaqPf61.js} +1 -1
  59. package/web/dist/assets/{Loops-CexwOARr.js → Loops-Bx3HnlUA.js} +1 -1
  60. package/web/dist/assets/{Markdown-DrXLM6TA.js → Markdown-BK38dn4x.js} +1 -1
  61. package/web/dist/assets/{OperatorHudView-D5qTQat0.js → OperatorHudView-ChQPM8JE.js} +1 -1
  62. package/web/dist/assets/{Personal-B_Vx9a7a.js → Personal-DELm61DF.js} +1 -1
  63. package/web/dist/assets/{Tabs-YZbGqWPW.js → Tabs-DcMq5IvC.js} +1 -1
  64. package/web/dist/assets/{activity-dcg5vF9j.js → activity-D4w9t8fE.js} +1 -1
  65. package/web/dist/assets/{drawable-BoQnwVpr.js → drawable-BI2_tF3o.js} +1 -1
  66. package/web/dist/assets/{index-BoG1M8Xy.css → index-CHpydQJg.css} +1 -1
  67. package/web/dist/assets/{index-BmbWeoTM.js → index-r2jRgf6l.js} +2 -2
  68. package/web/dist/assets/{react-DRhoX0kS.js → react-Ci4wQD4a.js} +2 -2
  69. package/web/dist/assets/{usePoll-BmR631BB.js → usePoll-BcywUo55.js} +1 -1
  70. package/web/dist/index.html +3 -3
  71. package/web/src/pages/Laws.jsx +53 -5
  72. package/web/dist/assets/FilterInput-BSfpStOZ.js +0 -1
  73. package/web/dist/assets/Laws-yGBJFKgY.js +0 -3
@@ -70,10 +70,15 @@ const GATE_VERBS = new Set(['git-gate', 'rule-gate', 'tool-gate']);
70
70
  const VERBS = {
71
71
  context: { load: () => import('../js/hosts/hooks-context.mjs'), fn: 'cmdContext', flags: HOSTED },
72
72
  'git-gate': {
73
- load: gates, fn: 'cmdGitGate', flags: HOSTED, switches: { '--no-consent': 'noConsent' },
73
+ load: gates,
74
+ fn: 'cmdGitGate',
75
+ flags: HOSTED,
76
+ switches: { '--no-consent': 'noConsent', '--no-deletion-gate': 'noDeletionGate' },
74
77
  },
75
78
  'rule-gate': { load: gates, fn: 'cmdRuleGate', flags: HOSTED },
76
- 'tool-gate': { load: gates, fn: 'cmdToolGate', flags: HOSTED },
79
+ 'tool-gate': {
80
+ load: gates, fn: 'cmdToolGate', flags: HOSTED, switches: { '--no-deletion-gate': 'noDeletionGate' },
81
+ },
77
82
  learn: {
78
83
  load: () => import('../js/hosts/hooks-learn.mjs'),
79
84
  fn: 'cmdLearn',
@@ -98,7 +103,8 @@ function die(code, msg) {
98
103
  */
99
104
  function parse(spec, argv) {
100
105
  const args = {
101
- root: null, host: null, memory: null, consolidate: false, noConsent: false, file: null,
106
+ root: null, host: null, memory: null, consolidate: false, noConsent: false,
107
+ noDeletionGate: false, file: null,
102
108
  };
103
109
  for (let i = 0; i < argv.length; i += 1) {
104
110
  const tok = argv[i];
@@ -22,7 +22,7 @@ silently guarding the wrong rule after a renumber. See [ADR 0005](../adr/0005-st
22
22
  | id | Pack | Claude Code | OpenClaude | OpenCode | Bob |
23
23
  |---|---|---|---|---|---|
24
24
  | `sealed-secrets` | law | ask: rule-gate (credential-shaped content outside `.env*`) | same as Claude Code | block: guard plugin (key/credential file path, or vendor-prefixed secret content); warns only on `.env*` | block (exit 2) |
25
- | `deletion-is-deliberate` | law | ask: git-gate (the destructive-git set in [Hooks](hooks.md): force-push and every history-discarding git act it lists) | same as Claude Code | ask: permission prompt (the same destructive-git set) **+** block: guard plugin (catastrophic shell: `rm -rf /`, fork bomb, `format`, `dd` over a raw disk) **+** warn only: guard plugin (the destructive-git set, as a backstop to the permission prompt) | block (exit 2) |
25
+ | `deletion-is-deliberate` | law | ask: git-gate, only on real data loss judged against the repository's state (the list in [Hooks](hooks.md): a protected branch deleted or force-pushed, an unpushed unmerged branch deleted, uncommitted work discarded, a stash dropped, the reflog pruned, files git cannot restore removed); asks when it cannot verify. Switchable per install with `--no-deletion-gate` (see [Exclusions](exclusions.md)): the real-loss check and the Law IV `permission.bash` keys stand down, everything else in this table stays | same as Claude Code | ask: permission prompt (`git reflog expire*`, `git gc --prune*`, a force push or delete of `main`/`master` by name) **+** block: guard plugin (catastrophic shell: `rm -rf /`, fork bomb, `format`, `dd` over a raw disk) **+** block: guard plugin (every other real-loss case, the same state-aware decisions as the hook) | block (exit 2) |
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) |
@@ -7,7 +7,7 @@ section: "Configuration"
7
7
  description: "Three ways to switch things off: one rule, one skill, or every hook in a folder."
8
8
  link: {"hash": "#/harness", "label": "Manage in Harness →"}
9
9
  ---
10
- Geneseed has three ways to switch something off, and they work at different scales:
10
+ Geneseed has three ways to switch something off, and they work at different scales (plus one on/off switch for Law IV's gate, below):
11
11
 
12
12
  | | Sovereign repo | Per-rule exclusion | Per-skill exclusion |
13
13
  | --- | --- | --- | --- |
@@ -69,3 +69,22 @@ geneseed build --emit claude-global --exclude-skills "bruno,react-view-transitio
69
69
  The skill's files are not written, its catalogue row goes, and an OpenCode `/command` that runs it goes too. The install records the list (`Excluded skills: …` under its skill section), and every rebuild and upgrade keeps it. `--exclude-skills none` puts them all back.
70
70
 
71
71
  A few skills cannot be excluded, because the harness itself links to them by path — `council`, `rule`, `skill-forge`, `workflow` and a handful more. The flag names them when it refuses one. Skills you installed yourself are not Geneseed's to exclude: remove those from the host's skills folder directly.
72
+
73
+ ### Law IV deletion gate — on or off
74
+
75
+ *Deletion Is Deliberate* is a Rule and cannot be excluded, but its **tool-boundary gate** can be switched off for one install. It is armed by default.
76
+
77
+ ```bash
78
+ geneseed build --emit claude-global --no-deletion-gate
79
+ ```
80
+
81
+ The web console has the same switch on Rule IV's row of the Constitution page; the row's *Enforced by* reads **Gate off** while it is off. The install records it (`Deletion gate: off` under its pack list, written only when off) and every rebuild and upgrade keeps it; rebuild without the flag, or flip the switch back, to re-arm it. `geneseed status` shows `deletion gate: off` on the install's row.
82
+
83
+ What stops asking, per host: the real-loss check of `git-gate` (Claude Code, OpenClaude) and `tool-gate` (Bob, which then no longer exits 2 for it), the OpenCode guard plugin's real-loss block, and the Law IV keys of OpenCode's `permission.bash` (`git reflog expire*`, `git gc --prune*`, the `main`/`master` force-push and delete forms). What stays: the Rule's text in the carrier, *Sealed Secrets*, the root-wide scan check (*Commands Must Return*), *Consent Before Push* (a forced push still asks as a push while that rule is on), *External Gate*, and the OpenCode guard's catastrophic-command block (`rm -rf /`, `format`, `dd` over a disk).
84
+
85
+ Limits:
86
+ - **Claude Code, OpenClaude and Bob have no separate catastrophic-command check.** There, a removal outside a repository — `rm -rf /` included — is caught only by the real-loss check, so with the gate off it no longer asks.
87
+ - **OpenCode reads the switch at start.** The guard plugin reads `Deletion gate:` from the install's `AGENT.md` once, when OpenCode loads it: restart OpenCode after flipping it.
88
+ - **A global install beside a project install.** On Claude Code and OpenClaude the global `git-gate` stands down for a project install, so the project's switch decides. Bob's `tool-gate` and the OpenCode guard do not stand down: an armed global install still refuses real loss inside a project whose own install is off.
89
+ - **An existing `opencode.json` keeps its Law IV keys.** Switching off does not remove `permission.bash` entries already written — Geneseed cannot tell its own from yours — so the build warns that it keeps them and they keep asking. Delete them by hand.
90
+ - **An older hook shim silences the whole gate.** The flag is new. While the machine's shared hook shim still points at an older checkout, that checkout refuses the flag and the gate verb lets every call through on a disarmed install — on Claude Code and OpenClaude the root-scan and consent checks too, on Bob also *Sealed Secrets* and *External Gate*. Update the checkout the shim points at (`geneseed rebuild-all`) before switching the gate off.
@@ -17,7 +17,7 @@ Geneseed writes its hooks into the host's settings file. Every hook calls one pr
17
17
  | Event | When | Command | What it does | Conditional? |
18
18
  | --- | --- | --- | --- | --- |
19
19
  | `SessionStart` | `startup`, `resume`, `clear`, `compact`, `fork` | `geneseed-hook context` | injects your harness files and the repo's docs, or re-injects them after a resume, a fork, or an auto-compaction summarised them away | no |
20
- | `PreToolUse` | `Bash`, `PowerShell` | `geneseed-hook git-gate` | asks before destructive git, and before a recursive scan of a whole filesystem; asks before every commit and push | the commit/push question only |
20
+ | `PreToolUse` | `Bash`, `PowerShell` | `geneseed-hook git-gate` | asks before a command that would really lose data, and before a recursive scan of a whole filesystem; asks before every commit and push | the commit/push question only |
21
21
  | `PreToolUse` | `Write`, `Edit`, `NotebookEdit` | `geneseed-hook rule-gate` | asks before a secret lands in a file, or a write to your rules or memory | no |
22
22
  | `Stop` | the agent finishes a reply | `geneseed-hook learn` | distils durable facts into `memory/` | needs `GENESEED_LLM` |
23
23
  | `SubagentStop` | a subagent finishes | `geneseed-hook learn` | records a lesson for that agent in `memory/agents/<name>.md` | needs `GENESEED_LLM` |
@@ -28,8 +28,16 @@ Geneseed writes its hooks into the host's settings file. Every hook calls one pr
28
28
  **context.** Before your first message, the agent receives your own harness files — `user-rules.md`, `PROFILE.md`, the memory index, the notebook index, your wiki declaration — skipping any still untouched since install, then the repo's docs, discovered by convention or listed in a `context.json`. Each file is capped at 16 KB and the whole injection at 48 KB; what does not fit is listed for the agent to read on demand. You see nothing, except that the agent already knows your project. See [Project context](../guides/project-context.md).
29
29
 
30
30
  **git-gate.** Runs before every shell command and looks for `git` in it, including chained one-liners and `git -C <path>` forms, and for a recursive scan of a whole filesystem.
31
- - **Destructive git** — `reset --hard`, `clean -f`, `branch -D`, `checkout -- `, a `push --force` — always gets a confirmation prompt, citing *Deletion Is Deliberate*. That is a Rule, so it holds in every build.
32
- - **Every `git commit` and `git push`** gets a prompt citing *Consent Before Push*. This part belongs to the **process** doctrine pack: build without it, or exclude that rule, and the hook stays wired with `--no-consent` — commits and pushes go through without asking, destructive git is still stopped ([Rules](rules.md)).
31
+ - **Real data loss** gets a confirmation prompt citing *Deletion Is Deliberate* — and only real loss, judged against the repository's actual state:
32
+ - deleting or force-pushing a protected branch (`main`, `master`, or a remote's default branch), locally or on the remote;
33
+ - `branch -D` of a branch whose commits are on no remote branch and not merged into the current one;
34
+ - discarding uncommitted work (`reset --hard`, `checkout -f`, `checkout -- <paths>`, `switch --discard-changes`, `restore`, `clean -f`) when the tree actually holds some;
35
+ - on a protected branch only: `checkout <paths>` without `--` and `git rm -f` over changed files while `main`, `master` or the default branch is checked out, and `branch -f`/`checkout -B`/`switch -C` moving a protected branch off commits no remote and not the new target holds;
36
+ - `stash drop`/`clear` while a stash exists; `reflog expire` and `gc --prune`, always;
37
+ - removing files (`rm`, `rmdir`, `del`, `erase`, `Remove-Item`) that git cannot restore: untracked, ignored or modified files, a repository's `.git`, or anything outside a repository.
38
+
39
+ A force push to a feature branch, the protected-branch-only forms above on a feature branch, a new branch (`checkout -b`, `switch -c`), `reset --hard` on a clean tree, `branch -D` of a pushed branch, or `rm` of a tracked, unmodified file passes. When the gate cannot tell — a glob or `$variable` it would have to expand, a `cd` it cannot follow, `sh -c`/`xargs`, git failing or missing — it asks, and says it could not verify. To read the state it runs `git` itself, only once a command already looked destructive; every other command pays nothing. That is a Rule, so it holds in every build — unless you switch its gate off for one install with `--no-deletion-gate` (or the switch on Rule IV in the console's Constitution page). The hook is then wired with `--no-deletion-gate` and this check stands down; the Rule's text still binds the agent, and the root-scan and consent checks below still run ([Exclusions](exclusions.md)).
40
+ - **Every `git commit` and `git push`** gets a prompt citing *Consent Before Push*. This part belongs to the **process** doctrine pack: build without it, or exclude that rule, and the hook stays wired with `--no-consent` — commits and pushes go through without asking, real data loss is still stopped ([Rules](rules.md)).
33
41
  - **A recursive scan of a whole filesystem** — `find /`, `du -sh /`, `grep -r foo /`, `rg x C:\`, `Get-ChildItem C:\ -Recurse` — gets a prompt citing *Commands Must Return* (on Bob, it is blocked). In Git Bash `/` mounts every drive and mounted share, so such a scan runs for hours, and `| head` does not stop it. The prompt tells the agent to search a specific directory instead. A deeper path (`find /c/Users/me/project`), `ls /` without `-R` (`ls -r` is reverse order, not recursion), a quoted argument (a search pattern such as `grep -rn "/" src`), a `#` comment, and the remote half of an `ssh` command pass. This check is wired in every build, whatever packs or rules you excluded: it rides inside `git-gate` with no flag of its own, because a new flag or verb would reach machines still running an older hook shim, which would refuse it — and Claude Code blocks a tool call whose gate fails.
34
42
  - Any other command passes untouched.
35
43
 
@@ -14,7 +14,7 @@ Every [host](hosts.md) gets the same content. This table is what each one lets G
14
14
  | Agents · Skills · Memory · Notebook | ✅ | ✅ | ✅ | ✅ |
15
15
  | Project context loaded at session start | ⚙️ plugin | 🪝 hook | 🪝 hook¹ | 🪝 hook² |
16
16
  | Memory written back at session end (learn) | ⚙️ plugin | 🪝 hook | 🪝 hook¹ | 🪝 hook² |
17
- | Destructive git (force-push, `reset --hard`…) | 🔒 permission ask | 🪝 ask | 🪝 block¹ | 🪝 ask² |
17
+ | Real data loss (force-push to `main`, discarding uncommitted work, `rm` of untracked files…) | ⚙️ block³ · 🔒 ask (reflog, gc) | 🪝 ask | 🪝 block¹ | 🪝 ask² |
18
18
  | Secrets kept out of files | ⚙️ block⁴ | 🪝 ask | 🪝 block¹ | 🪝 ask² |
19
19
  | Consent before every commit and push | 🔒 permission ask | 🪝 ask | 🪝 warning¹ | 🪝 ask² |
20
20
  | Rule-or-memory write needs your call | ⚙️ speed bump³ | 🪝 ask | 🪝 warning¹ | 🪝 ask² |
@@ -25,9 +25,9 @@ Every [host](hosts.md) gets the same content. This table is what each one lets G
25
25
 
26
26
  ✅ native · ⚙️ OpenCode plugin · 🔒 `permission` entry in `opencode.json` · 🪝 `settings.json` hook · ➖ no host mechanism (the rule still applies as prose).
27
27
 
28
- ¹ **Bob** has its own hook contract: Claude's event names, but it ignores a hook's output on `PreToolUse`, so the only way to refuse is exit code 2. Geneseed therefore blocks only the rules that admit no judgement call — secrets, destructive git, a recursive scan of a whole filesystem, and a write under a project's own protected-checks list — and turns the two consent checks into a logged warning. It has no `SubagentStop` or `PreCompact` event, so learn runs on `Stop` only. **Unverified live** — there is no Bob install on the authoring machine; the rules preamble still holds if a hook does not fire.
28
+ ¹ **Bob** has its own hook contract: Claude's event names, but it ignores a hook's output on `PreToolUse`, so the only way to refuse is exit code 2. Geneseed therefore blocks only the rules that admit no judgement call — secrets, real data loss, a recursive scan of a whole filesystem, and a write under a project's own protected-checks list — and turns the two consent checks into a logged warning. It has no `SubagentStop` or `PreCompact` event, so learn runs on `Stop` only. **Unverified live** — there is no Bob install on the authoring machine; the rules preamble still holds if a hook does not fire.
29
29
  ² **OpenClaude** is a Claude Code fork and gets Claude's hooks and verdicts verbatim, in `~/.openclaude/settings.json` (global) or `.openclaude/settings.local.json` (per repo). **Unverified live** for the same reason.
30
- ³ OpenCode's plugin hook can only allow or throw — there is no "ask the user" — so this check stops the first such write once rather than prompting.
30
+ ³ OpenCode's plugin hook can only allow or throw — there is no "ask the user" — so the rule-or-memory check stops the first such write once rather than prompting, and a real data loss is blocked outright (as on Bob) rather than asked; `GENESEED_GUARD=warn` turns the block into a logged warning.
31
31
  ⁴ The checks differ in shape: OpenCode's guard plugin blocks writes *to* private-key and credential files by path; the Claude-shaped hook looks at the *content* being written for credential-shaped strings (cloud keys, tokens, private-key blocks) and asks. A `.env` file is exempt on the hook side — that is where a secret is allowed to live.
32
32
  ⁵ A linked git worktree of an excluded repo is excluded too: the hooks and plugins follow its `.git` file back to the main repository. `claudeMdExcludes` is the exception — it lives in the gitignored `settings.local.json`, which a worktree never gets — so `geneseed exclude add` and `exclude list` print the snippet to commit yourself. See [Exclusions](exclusions.md).
33
33
 
@@ -10,13 +10,13 @@ One of the [OpenCode plugins](opencode-plugins.md). [Environment variables](envi
10
10
 
11
11
  **What it does.** Enforces the safety rules at the tool boundary — before a tool call runs, not after. High-confidence patterns only, so legitimate work is not caught.
12
12
 
13
- - **Blocks** writes to private-key and credential files (*Sealed Secrets*), catastrophic shell commands such as deleting the filesystem root (*Deletion Is Deliberate*), a recursive scan rooted at a whole filesystem such as `find /` (*Commands Must Return* — search a specific directory instead), and any change — write, move, rename, delete — under a wiki folder declared `protected` in `geneseed-wiki.jsonc`, and writes to a check listed in the project's `.geneseed/protected-checks.txt` (*External Gate* — see [Hooks](../concepts/hooks.md)).
14
- - **Warns** (logged, allowed) on `.env` writes and force-push.
13
+ - **Blocks** writes to private-key and credential files (*Sealed Secrets*), catastrophic shell commands such as deleting the filesystem root and every command that would really lose data — a protected branch deleted or force-pushed, an unpushed unmerged branch deleted, a protected branch force-moved off commits nothing else holds, uncommitted work discarded, a stash dropped, files git cannot restore removed — judged against the repository's state, the same decisions the Claude/Bob hook makes (*Deletion Is Deliberate*; `reflog expire` and `gc --prune` are left to the `permission.bash` ask; an install built `--no-deletion-gate` carries `Deletion gate: off` in its `AGENT.md`, which the plugin reads once at load and then stops blocking real loss — catastrophic commands still block), a recursive scan rooted at a whole filesystem such as `find /` (*Commands Must Return* — search a specific directory instead), and any change — write, move, rename, delete — under a wiki folder declared `protected` in `geneseed-wiki.jsonc`, and writes to a check listed in the project's `.geneseed/protected-checks.txt` (*External Gate* — see [Hooks](../concepts/hooks.md)).
14
+ - **Warns** (logged, allowed) on `.env` writes.
15
15
  - **Speed-bumps** the *first* write to `user-rules.md` or a memory file: it is refused once, naming *Persist Insight* — whether something is a standing rule or a fact to remember is your call, settled through the `rule` skill. The re-issued write goes through.
16
16
 
17
17
  It ships whole on every install, whatever [doctrine packs](../concepts/rules.md) you chose. On Claude Code and Bob the same checks are hooks, which can *ask* you instead of refusing; OpenCode's tool hook has no ask tier. Protected wiki folders are enforced only here — on the other hosts they are an instruction.
18
18
 
19
- There is no DYNAMIC loop/* exemption on OpenCode — it declares a `permission.ask` plugin hook but never calls it, so this guard cannot let a loop's own commit through the way the Claude/Bob git-gate's `loopExempt` does. A loop's commit on OpenCode still gets the static `permission.bash` ask, every time. A push to a `loop/*` branch does not: pushing a loop branch is not pushing `main`/`master`, so [`js/hosts/settings.mjs`](../../js/README.md) wires a static `allow` for the exact refspec shapes the loop engine pushes (`git push [-u] <remote> HEAD:loop/<slug>`, and the `refs/heads/` spelling) — never `main`/`master`, and a force push, a `+refspec`, or a `--delete`/bare-`:` push to a `loop/*` branch still asks, because those keys are ordered to win.
19
+ There is no DYNAMIC loop/* exemption on OpenCode — it declares a `permission.ask` plugin hook but never calls it, so this guard cannot let a loop's own commit through the way the Claude/Bob git-gate's `loopExempt` does. A loop's commit on OpenCode still gets the static `permission.bash` ask, every time. A push to a `loop/*` branch does not: pushing a loop branch is not pushing `main`/`master`, so [`js/hosts/settings.mjs`](../../js/README.md) wires a static `allow` for the exact refspec shapes the loop engine pushes (`git push [-u] <remote> HEAD:loop/<slug>`, and the `refs/heads/` spelling) — never `main`/`master`, and a force push, a `+refspec`, or a `--delete`/bare-`:` push to a `loop/*` branch still asks, through the push chain's own tiers or the blanket `git push*` ask.
20
20
 
21
21
  **When it runs.** Before every tool call (`tool.execute.before`).
22
22
 
@@ -40,8 +40,10 @@ If the drift is your own edits, export them first — `geneseed diff` (or the co
40
40
  > `[probe] ~/.claude: git-gate under Git Bash registered but not refusing — …`
41
41
 
42
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
43
+ settings spell it, through the host's own shell, on `git reflog expire --expire=now --all` (which must be refused)
44
+ and `git status` (which must go through). A gate wired with `--no-deletion-gate` lets the first one
45
+ through by design, so it is probed on `find / -name geneseed-probe` instead — a root-wide scan it
46
+ must still refuse. An OpenCode install's guard plugin has no command to
45
47
  run: the probe imports it and calls its hook through both plugin APIs (V1 and V2), with `rm -rf /`
46
48
  as the call it must refuse. `GENESEED_GUARD=off` or `warn` in your terminal makes that a note,
47
49
  not a failure.
@@ -16,12 +16,12 @@ The agent interviews you briefly and drafts `PROFILE.md`: your role, your stack,
16
16
 
17
17
  ### 1. The agent tries to push
18
18
 
19
- You ask the agent to clean up a branch and it decides to force-push. A gate runs before the shell does, so the command waits for you. On Claude Code it looks like this (the prompt's layout depends on your version; the reason text is Geneseed's):
19
+ You ask the agent to clean up history and it decides to force-push `main`. A gate runs before the shell does, so the command waits for you. (A force-push to its own feature branch would not stop here: the gate asks only when data would really be lost.) On Claude Code it looks like this (the prompt's layout depends on your version; the reason text is Geneseed's):
20
20
 
21
21
  ```text
22
- ● Bash(git push --force origin feature/login)
23
- Geneseed (Deletion Is Deliberate) — a history-rewriting or discarding git act
24
- needs confirmation bound to this specific command
22
+ ● Bash(git push --force origin main)
23
+ Geneseed (Deletion Is Deliberate) — `git push` would force-push the protected
24
+ branch main. Confirm this specific command, or leave the data in place.
25
25
  Do you want to proceed? Yes / No
26
26
  ```
27
27
 
@@ -30,10 +30,10 @@ An ordinary `git commit` or `git push` gets a milder question when the **process
30
30
  Bob has no prompt, so the same force-push is refused outright:
31
31
 
32
32
  ```text
33
- [geneseed] BLOCKED: Geneseed (Deletion Is Deliberate) — a history-rewriting or discarding git act needs confirmation bound to this specific command
33
+ [geneseed] BLOCKED: Geneseed (Deletion Is Deliberate) — `git push` would force-push the protected branch main. Confirm this specific command, or leave the data in place.
34
34
  ```
35
35
 
36
- On OpenCode the question comes from OpenCode's own permission prompt, which Geneseed configures in `opencode.json`. Every catch goes into a log, and `geneseed status` counts the catches by rule.
36
+ OpenCode has no prompt a plugin can raise, so Geneseed's guard plugin refuses it the same way. Every catch goes into a log, and `geneseed status` counts the catches by rule.
37
37
 
38
38
  ### 2. You call a skill by name
39
39
 
@@ -15,9 +15,9 @@ The gates are the one place the harness does more than ask. Part of [Why Genesee
15
15
  - **Gates stay narrow and cheap to load.** Everything the hook entry point imports is paid on every tool call of every session. See [Hook gates](../concepts/hook-gates.md).
16
16
  - **One shim for the whole machine, rewritten by every build, last writer wins.** Moving a checkout and rebuilding then repairs every install at once. A checkout under the OS temp folder or in a git worktree never rewrites it (`ephemeralCheckout`), since the shim would outlive it. See [Hook gates](../concepts/hook-gates.md).
17
17
  - **A global gate stands down only for a live project install that runs the same hook.** `globalHookStandingDown` checks the session's project directory for an install whose manifest records a group running the same verb, still present in its settings file. A gate goes quiet only when an equivalent gate replaces it; a doubled prompt is safe and a silenced gate is not. See [Hook gates](../concepts/hook-gates.md).
18
- - **The destructive-git list is closed.** It covers the irreversible acts Deletion Is Deliberate names, so the gate adds no policy the law does not state. See [Hooks](../concepts/hooks.md).
18
+ - **Law IV's gate asks only on real loss, and fails closed.** It reads the repository's state (`js/hosts/loss.mjs`, with one `git` spawn reached only after a command looks destructive) and asks when a command would destroy something nothing else holds, or when it cannot tell. A gate that asks on a clean tree teaches you to click through it. See [Hooks](../concepts/hooks.md) and [ADR 0002](../adr/0002-no-spawn-on-the-generator-and-hook-path.md).
19
19
  - **A recursive scan rooted at a filesystem root asks on Claude Code and OpenClaude, and blocks on Bob and OpenCode.** Hosts with no ask tier must block, or the scan starts anyway. The remote half of an `ssh` command is exempt, since it runs on the remote machine. See [ADR 0010](../adr/0010-root-scan-guard.md).
20
- - **On OpenCode, a push to a `loop/*` branch is allowed.** A loop branch is never `main` or `master`; a force push, an extra refspec or a delete still asks, and so does every commit. See [LOOP.md, trailers and the loop branch](../concepts/loop-branch.md).
20
+ - **On OpenCode, a push to a `loop/*` branch is allowed.** A loop branch is never `main` or `master`; an extra refspec still asks, a force push or delete still meets the consent ask, and so does every commit. See [LOOP.md, trailers and the loop branch](../concepts/loop-branch.md).
21
21
  - **The OpenCode guard plugin allows the call when its own code fails.** OpenCode has no ask tier, so failing closed there would lock the session with nothing to answer it. See [The guard plugin](../reference/plugin-guard.md) and [ADR 0007](../adr/0007-gates-fail-closed-per-host-ceiling.md).
22
22
  - **Geneseed's hook groups are appended after yours.** A merge keeps your own `PreToolUse` entries in place and adds its groups at the end, so it never reorders hooks you wrote. See [Hook gates](../concepts/hook-gates.md).
23
23
  - **`learn` runs on `Stop`, `SubagentStop` and `PreCompact`, and moving it to a session-end event was turned down.** It distils the tail of the transcript, which a session-end event does not reliably leave it. See [Hooks](../concepts/hooks.md) and [Memory](../concepts/memory.md).
@@ -22,14 +22,14 @@ Most of the harness is *asked*. A few rules, where one mistake is expensive and
22
22
 
23
23
  On Claude Code and OpenClaude the hooks never block silently. They make the host **ask you**, with the reason shown:
24
24
 
25
- - **Destructive git**: a force-push, `reset --hard`, `clean -f`, `branch -D` or `checkout --`. Wired in every build.
25
+ - **Real data loss**: a force-push or delete of `main`/`master`/the remote's default branch, force-moving one off commits nothing else holds, `branch -D` of an unpushed unmerged branch, `reset --hard`/`checkout --`/`clean -f` and the like over uncommitted work, a stash dropped, the reflog pruned, an `rm` of files git cannot restore. Judged against the repository's real state, so the same command on a clean tree passes; when the gate cannot tell, it asks. Wired in every build.
26
26
  - **Commit and push**: every `git commit` and `git push`, including chained one-liners and `git -C <path>`. This question belongs to the **process** pack. Build without it and the git gate stays wired. Only this question goes away, and destructive git is still caught.
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
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.
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
+ 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 go through Claude Code's normal permission flow; an `rm` of files git cannot restore is the real-loss case above.
33
33
 
34
34
  **Bob** runs one combined gate and has no "ask": it refuses with exit code 2, and only for the rules that admit no judgement call: credentials, destructive git, a scan of a whole filesystem, and a write under a project's own protected-checks list. The commit/push and memory checks become a log line, and the call goes through. Bob's hook wiring follows its documentation but has not been tested on a live Bob install.
35
35
 
@@ -39,8 +39,8 @@ If a gate crashes, it asks instead of letting the call through. If it cannot sta
39
39
 
40
40
  On OpenCode, two mechanisms enforce rules:
41
41
 
42
- - **OpenCode's own permission prompt**, which Geneseed configures in `opencode.json`. It asks you before `rm -rf`, a force-push, `reset --hard`, `clean -f`, `branch -D` and `checkout --`, and, when the **process** pack is on, before every commit and push.
43
- - **The guard plugin**, which blocks outright: writes to key and credential files (`id_rsa`, `*.pem`, `.ssh/`, `.npmrc`…), catastrophic commands (`rm -rf /`, `rm -rf ~`, formatting or overwriting a disk), a recursive scan of a whole filesystem (`find /`, `du -sh /`, `Get-ChildItem C:\ -Recurse`), and any change inside a folder your wiki marks `protected`. It also refuses the first write to `user-rules.md` or memory once. Re-issue the write and it goes through.
42
+ - **OpenCode's own permission prompt**, which Geneseed configures in `opencode.json`. It asks you before `git reflog expire`, `git gc --prune` and a force-push or delete of `main`/`master`, and, when the **process** pack is on, before every commit and push.
43
+ - **The guard plugin**, which blocks outright: writes to key and credential files (`id_rsa`, `*.pem`, `.ssh/`, `.npmrc`…), catastrophic commands (`rm -rf /`, `rm -rf ~`, formatting or overwriting a disk), every other real data loss (the same state-aware decisions as the hook), a recursive scan of a whole filesystem (`find /`, `du -sh /`, `Get-ChildItem C:\ -Recurse`), and any change inside a folder your wiki marks `protected`. It also refuses the first write to `user-rules.md` or memory once. Re-issue the write and it goes through.
44
44
 
45
45
  The guard only warns on `.env` edits. It checks credentials by **file path**, not by content, so a key pasted into an ordinary source file is not caught here. One caveat about the process pack: if your `opencode.json` already has a `git commit*` entry, a rebuild without the pack leaves that entry in place and reports it. Geneseed cannot tell its own entry from one you typed.
46
46
 
@@ -25,7 +25,7 @@ Geneseed is a build that produces a harness. One source renders:
25
25
 
26
26
  It renders them for OpenCode, Claude Code, Bob, OpenClaude, or as a plain bundle for any tool that reads `AGENT.md`. The content is the same on every host. Only the wiring differs.
27
27
 
28
- Where the host lets it, Geneseed also installs **gates**: small programs that run before a tool call and stop a few things outright, such as a force-push or a credential written into a tracked file. Those rules do not depend on the model remembering them. [Enforced vs. asked](enforced-vs-asked.md) explains which rules a gate stops and which ones the model is only told about.
28
+ Where the host lets it, Geneseed also installs **gates**: small programs that run before a tool call and stop a few things outright, such as a force-push to `main` or a credential written into a tracked file. Those rules do not depend on the model remembering them. [Enforced vs. asked](enforced-vs-asked.md) explains which rules a gate stops and which ones the model is only told about.
29
29
 
30
30
  ### Install once, use everywhere
31
31
 
@@ -15,7 +15,7 @@ What a Claude Code or OpenClaude install writes, piece by piece. The **Kind** wo
15
15
  | `user-rules.md` | Your own standing rules, read every session. Seeded empty | asked | 0 until you write in it | Leave it empty |
16
16
  | `PROFILE.md` | Who you are and how you like answers pitched. Fill it with the `profile` skill | asked | 0 until you fill it | Leave the seeded text untouched |
17
17
  | context hook | At session start, and again after a resume or compaction, puts your repo's README, CONTRIBUTING and a listing of `docs/` in front of the agent, with your rules, profile, memory index and wiki | automatic | Your docs, capped at 16 KB a file and 48 KB a session | `geneseed exclude add <folder>` silences every hook in that folder |
18
- | git-gate hook | Before each shell command: asks you before any `git commit` or `git push`, and before a force-push, `reset --hard`, `clean -f`, `branch -D` or `checkout --` | enforced | ~14 ms per shell call | The commit/push question goes away if you build without the **process** pack; the destructive-git question has no switch except `geneseed exclude` |
18
+ | git-gate hook | Before each shell command: asks you before any `git commit` or `git push`, and before a command that would really lose data — a force-push to `main`, a `reset --hard` over uncommitted work, `branch -D` of an unpushed branch, an `rm` git cannot undo | enforced | ~14 ms per shell call | The commit/push question goes away if you build without the **process** pack; the destructive-git question has no switch except `geneseed exclude` |
19
19
  | rule-gate hook | Before each write or edit: asks you when the file would carry a credential-shaped string (outside `.env`), or when the agent writes to `user-rules.md` or memory | enforced | ~14 ms per write | `geneseed exclude add <folder>` |
20
20
  | learn hook | When the agent finishes a reply, and before compaction, distils durable facts from the recent conversation into `memory/`. Off until you set `GENESEED_LLM` to a model command such as `claude -p` | automatic | One model call each time it runs, once enabled | Leave `GENESEED_LLM` unset (the default) |
21
21
  | `skills/` | Written procedures you call by name, such as `brainstorm` or `plan` | on demand | ~4.1k tokens for names and descriptions; a skill's body loads only when called | No per-skill switch; `geneseed uninstall` removes them all |
@@ -14,9 +14,9 @@ What an OpenCode install writes, piece by piece. The **Kind** words are explaine
14
14
  | `AGENT.md` | The rules, the pointers to agents and skills, and how memory works | asked | ~7.2k tokens every session | Shrink it with fewer doctrine packs (`--doctrines`); remove it with `geneseed uninstall` |
15
15
  | `user-rules.md` | Your own standing rules, read every session. Seeded empty | asked | 0 until you write in it | Leave it empty |
16
16
  | `PROFILE.md` | Who you are and how you like answers pitched. Fill it with the `profile` skill | asked | 0 until you fill it | Leave the seeded text untouched |
17
- | `opencode.json` entries | `instructions` points OpenCode at `AGENT.md`. `permission.bash` makes OpenCode ask before `rm -rf`, a force-push, `reset --hard`, `clean -f`, `branch -D` or `checkout --`, and before every commit and push when the **process** pack is on. `permission.skill` denies each user-only skill, so the model never starts it but `/name` still runs it; Geneseed takes back only the denies it wrote | enforced | No tokens | Edit the `permission` block yourself. The commit/push questions follow the **process** pack |
17
+ | `opencode.json` entries | `instructions` points OpenCode at `AGENT.md`. `permission.bash` makes OpenCode ask before `git reflog expire`, `git gc --prune` or a force-push or delete of `main`/`master`, and before every commit and push when the **process** pack is on. `permission.skill` denies each user-only skill, so the model never starts it but `/name` still runs it; Geneseed takes back only the denies it wrote | enforced | No tokens | Edit the `permission` block yourself. The commit/push questions follow the **process** pack |
18
18
  | context plugin | Puts your repo's README, CONTRIBUTING and a listing of `docs/` in front of the agent, with your rules, profile, memory index and wiki | automatic | Your docs, capped at 16 KB a file and 48 KB a session | `GENESEED_CONTEXT_INJECT=off` |
19
- | guard plugin | Blocks writes to key and credential files (`id_rsa`, `.pem`, `.ssh/`…), catastrophic commands (`rm -rf /`, formatting a disk), a recursive scan of a whole filesystem (`find /`), and changes inside your wiki's protected folders. Refuses the first write to `user-rules.md` or memory once, so the choice is yours | enforced | No tokens | `GENESEED_GUARD=off`, or `GENESEED_GUARD=warn` to log instead of block |
19
+ | guard plugin | Blocks writes to key and credential files (`id_rsa`, `.pem`, `.ssh/`…), catastrophic commands (`rm -rf /`, formatting a disk), every other real data loss judged against the repository's state, a recursive scan of a whole filesystem (`find /`), and changes inside your wiki's protected folders. Refuses the first write to `user-rules.md` or memory once, so the choice is yours | enforced | No tokens | `GENESEED_GUARD=off`, or `GENESEED_GUARD=warn` to log instead of block |
20
20
  | learn plugin | Once a session has been quiet for 60 seconds, distils durable facts into `memory/` with the model the session already uses | automatic | One model call per quiet session | `geneseed exclude add <folder>` silences it in that folder |
21
21
  | activity plugin | Writes one small file per session that the web console's Activity view shows | automatic | No tokens | `GENESEED_ACTIVITY=off` |
22
22
  | notify plugin | A desktop notification when a turn took longer than 30 seconds | automatic | No tokens | `GENESEED_NOTIFY=off` |
package/docs/web-ui.md CHANGED
@@ -98,7 +98,9 @@ streams the output of background jobs and keeps their history across reloads.
98
98
  the **Ethos** (four prose sections), the **Invariants** (the nine numbered Rules, with
99
99
  a class facet — only non-empty classes render, and two of the six have had no invariant
100
100
  since the split), and the **Doctrines** (one group per pack, each marked active or not for
101
- this install). Every row expands to its full body, lazily fetched. The rail entry is
101
+ this install). Rule IV carries one switch of its own, the install's deletion gate
102
+ (`--no-deletion-gate`; *Enforced by* reads *Gate off* while it is off) — flipping it asks,
103
+ then rebuilds the install once. Every row expands to its full body, lazily fetched. The rail entry is
102
104
  labelled *Constitution* while the route, the id and the badge key stay `laws`; deep links
103
105
  are `#/item/law/ont:telos`, `#/item/law/IV` and `#/item/law/process.5` — three address
104
106
  shapes that cannot collide, because a slug carries a colon no numeral has and a doctrine
@@ -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.6.0"
5
+ "version": "4.8.0"
6
6
  }
package/js/README.md CHANGED
@@ -76,7 +76,8 @@ user co-owns, and the hook verbs those emitted configs then run.
76
76
  | `hooks-prims.mjs` | The Python path/text primitives the hook verbs share (`splitLines`, `readStdin`, a never-throwing `isFile`) |
77
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 |
78
78
  | `memory-files.mjs` | The memory store's file format — `frontmatter`, `MEMORY.md` index writes, agent lessons, consolidate. No spawn: the CLI and web console import it |
79
- | `gitref.mjs` | fs/path-only: `gitRootOf`, `gitDirOf`, `currentBranch`, `loopLaunched` — read straight off `.git`, never spawn git. Shared by `hooks.mjs`'s git-gate and `js/loop/cli.mjs`'s commit-message writer |
79
+ | `gitref.mjs` | fs/path-only: `gitRootOf`, `gitDirOf`, `gitCommonDirOf`, `currentBranch`, `protectedBranches`, `stashExists`, `loopLaunched` — read straight off `.git`, never spawn git. Shared by `hooks.mjs`'s git-gate, `loss.mjs` and `js/loop/cli.mjs`'s commit-message writer |
80
+ | `loss.mjs` | Law IV calibrated to real loss: `lossReason` decides whether a command would really lose data (protected branch, unpushed branch, dirty tree, stash, reflog, files git cannot restore) and fails closed. The gate path's one `git` spawn, reached only after `LOSS_CANDIDATE_RE` matched. Twin: the OpenCode guard plugin |
80
81
  | `settings.mjs` | Merges into user-owned `settings.json`/`opencode.json` (one `loadJsonObject` reader for all of them) and the managed blocks |
81
82
  | `shim.mjs` | The machine-wide hook shim: its path, body, liveness (`shimDead`, read on every CLI run), `hookPrefix`/`hookRunnerEntry`. Kept cheap to import |
82
83
  | `native.mjs` | Capability specs → host-native subagents and skills. The impure half of the emit |
package/js/build/args.mjs CHANGED
@@ -101,6 +101,9 @@ const VALUED = {
101
101
  };
102
102
  const FLAGS = {
103
103
  '--sync-themes': 'syncThemes', '--validate-only': 'validateOnly',
104
+ // Law IV's gate switch. No `harness.config.json` key feeds it, deliberately: a rebuild elides
105
+ // the flag when armed, so a config default could disarm an armed install on its next upgrade.
106
+ '--no-deletion-gate': 'noDeletionGate',
104
107
  '-v': 'verbose', '--verbose': 'verbose',
105
108
  };
106
109
 
@@ -308,7 +311,7 @@ export function parseArgs(argv, defaults) {
308
311
  excludeRules: defaults.excludeRules,
309
312
  excludeSkills: defaults.excludeSkills ?? [],
310
313
  out: 'Harness', emit: 'files', footprint: 'lean', root: null,
311
- syncThemes: false, validateOnly: false, verbose: false,
314
+ syncThemes: false, validateOnly: false, verbose: false, noDeletionGate: false,
312
315
  };
313
316
  const { tokens } = nodeParseArgs({
314
317
  args: argv, options: PARSE_OPTIONS, allowPositionals: true, strict: false, tokens: true,
@@ -647,17 +647,19 @@ const PROJECT_EMITS = {
647
647
  * mode readers return `null` for "undetectable" too, which an explicit `posture: null` would
648
648
  * carry straight past the parameter default.
649
649
  */
650
- function axesCfg({ posture, mode, trust, doctrines, excludeRules, excludeSkills }) {
650
+ function axesCfg({ posture, mode, trust, doctrines, excludeRules, excludeSkills, deletionGate = null }) {
651
651
  return makeCfg({
652
652
  ...(posture ? { posture } : {}), ...(mode ? { mode } : {}), ...(trust ? { trust } : {}),
653
653
  ...(doctrines ? { doctrines } : {}), ...(excludeRules ? { excludeRules } : {}),
654
654
  ...(excludeSkills ? { excludeSkills } : {}),
655
+ // Only an explicit `false` disarms; `null` (no opinion) keeps the gate.
656
+ ...(deletionGate === false ? { noDeletionGate: true } : {}),
655
657
  });
656
658
  }
657
659
 
658
660
  export function emitProjectInto(host, {
659
661
  theme, out, root, footprint = 'full', posture = null, mode = null, trust = null,
660
- doctrines = null, excludeRules = null, excludeSkills = null,
662
+ doctrines = null, excludeRules = null, excludeSkills = null, deletionGate = null,
661
663
  }) {
662
664
  const emit = PROJECT_EMITS[host];
663
665
  // The axes ride the same rail into every `*Into`, and for the same reason: a drift reader
@@ -665,7 +667,8 @@ export function emitProjectInto(host, {
665
667
  // edited on every narrowed install, and `validate --doctrines craft` that ignored its flag
666
668
  // checked an all-packs render nobody asked for.
667
669
  return withPlatformNewlines(() => emit(
668
- axesCfg({ posture, mode, trust, doctrines, excludeRules, excludeSkills }), { theme, footprint, root }, out,
670
+ axesCfg({ posture, mode, trust, doctrines, excludeRules, excludeSkills, deletionGate }),
671
+ { theme, footprint, root }, out,
669
672
  ));
670
673
  }
671
674
 
@@ -679,15 +682,16 @@ export function emitProjectInto(host, {
679
682
  */
680
683
  export function buildInto({
681
684
  theme, out, footprint = 'lean', posture = null, mode = null, trust = null, doctrines = null,
682
- excludeRules = null, excludeSkills = null,
685
+ excludeRules = null, excludeSkills = null, deletionGate = null,
683
686
  }) {
684
- return withPlatformNewlines(() => build(axesCfg({ posture, mode, trust, doctrines, excludeRules, excludeSkills }),
687
+ return withPlatformNewlines(() => build(axesCfg({
688
+ posture, mode, trust, doctrines, excludeRules, excludeSkills, deletionGate }),
685
689
  theme, out, { footprint, nativeCatalog: false }));
686
690
  }
687
691
 
688
692
  export function emitGlobalInto(host, {
689
693
  theme, out, cfgDir, footprint, posture = null, mode = null, doctrines = null, trust = null,
690
- excludeRules = null, excludeSkills = null,
694
+ excludeRules = null, excludeSkills = null, deletionGate = null,
691
695
  }) {
692
696
  // `build.HOSTS.get(host, build.HOSTS["opencode"])` — an unknown host falls back rather than
693
697
  // raising, because the host comes from a marker file a user can edit.
@@ -703,7 +707,7 @@ export function emitGlobalInto(host, {
703
707
  // edited on every install that chose a register, narrowed its packs or excluded a rule —
704
708
  // the same scar the footprint left in `diffCollect`, one axis over each time.
705
709
  return withPlatformNewlines(() => emit(
706
- axesCfg({ posture, mode, trust, doctrines, excludeRules, excludeSkills }),
710
+ axesCfg({ posture, mode, trust, doctrines, excludeRules, excludeSkills, deletionGate }),
707
711
  { theme, footprint, root: null, cfgDir }, out,
708
712
  ));
709
713
  }
@@ -182,7 +182,7 @@ export function emitClaudeRender(cfg, job) {
182
182
  // WIRE — see `claudeWire` below. One child per emit, so the two halves are two
183
183
  // functions rather than two spawns; the seam between them is a call, not a process.
184
184
  const managed = claudeWire(job, claudeMdText, agentText !== null, cfg.doctrines,
185
- cfg.excludeRules);
185
+ cfg.excludeRules, cfg.deletionGate);
186
186
 
187
187
  return {
188
188
  owned,
@@ -215,7 +215,8 @@ export function emitClaudeRender(cfg, job) {
215
215
  * not needed for it: Python tests `os.environ.get(...)` for truthiness and an env var is
216
216
  * always a string, so `''` is the only falsy value either language sees.
217
217
  */
218
- function claudeWire(job, claudeMdText, hasAgentText, doctrines = null, excludeRules = []) {
218
+ function claudeWire(job, claudeMdText, hasAgentText, doctrines = null, excludeRules = [],
219
+ deletionGate = true) {
219
220
  phaseLog('WIRE');
220
221
  const { cfgDir, claudeMd, scope, host, oldManaged, preambleExclude, hookOpts } = job;
221
222
  const old = oldManaged && typeof oldManaged === 'object' && !Array.isArray(oldManaged)
@@ -279,7 +280,7 @@ function claudeWire(job, claudeMdText, hasAgentText, doctrines = null, excludeRu
279
280
  // DECIDES the install's packs, so the marker on disk is still the previous build's.
280
281
  const [, managedHooks] = mergeClaudeSettings(
281
282
  settingsPath, oldSf === settingsName ? get(old, 'settings_hooks') : null, hookOpts,
282
- doctrines, excludeRules, host, cfgDir,
283
+ doctrines, excludeRules, host, cfgDir, deletionGate,
283
284
  );
284
285
  managed.settings_hooks = managedHooks;
285
286
 
@@ -97,7 +97,8 @@ function opencodeLayer(cfg, items, themeName, theme, dir, owned, opts) {
97
97
  const staleSkillNames = oldDenies.filter((n) => !userOnlySkills.includes(n));
98
98
  const wired = {};
99
99
  const cfgName = path.basename(mergeOpencodeJson(path.join(wireBase, 'opencode.json'),
100
- agentPath, cfg.doctrines, cfg.excludeRules, userOnlySkills, staleSkillNames, wired));
100
+ agentPath, cfg.doctrines, cfg.excludeRules, userOnlySkills, staleSkillNames, wired,
101
+ cfg.deletionGate));
101
102
  // The next record: Geneseed's earlier denies still wanted, plus the ones it added now. A merge
102
103
  // that wrote nothing (`skillsAdded` unset) keeps the old record whole.
103
104
  const skillDenies = wired.skillsAdded
@@ -40,7 +40,7 @@ import { DEFAULT_PRESET } from '../loop/score.mjs';
40
40
  import { opencodeConfigDir, resolvePath } from '../hosts/hosts.mjs';
41
41
  import {
42
42
  EMIT_HOST_SCOPE, defaultMode, defaultPosture, defaultTheme, doctrinesForBuild,
43
- excludedRulesOfDir, excludedSkillsOfDir, footprintOfDir, installState, installTargets,
43
+ deletionGateOfDir, excludedRulesOfDir, excludedSkillsOfDir, footprintOfDir, installState, installTargets,
44
44
  legacyProcessCarrierOfDir, modeOfDir, postureOfDir, readMaybe, themeOfDir, trustOfDir,
45
45
  } from '../hosts/installs.mjs';
46
46
  import { colorThemeFiles, colorThemeJson, PALETTE_ROLES } from '../hosts/opencode.mjs';
@@ -102,6 +102,8 @@ const BUILD_FORWARD = [
102
102
  export function cmdBuild(args) {
103
103
  const argv = [];
104
104
  for (const [key, flag] of BUILD_FORWARD) if (args[key] != null) argv.push(flag, args[key]);
105
+ // The one bare switch `build` forwards: it takes no value, so it is not a BUILD_FORWARD row.
106
+ if (args.noDeletionGate) argv.push('--no-deletion-gate');
105
107
  return driverMain(argv);
106
108
  }
107
109
 
@@ -138,7 +140,7 @@ export function cmdBuild(args) {
138
140
  */
139
141
  export function setupBuildArgs(theme, emit, out = null, root = null, footprint = 'lean',
140
142
  posture = 'peer', mode = 'direct', doctrines = null, allPacks = PACK_ORDER,
141
- excludeRules = null, trust = DEFAULT_PRESET, excludeSkills = null) {
143
+ excludeRules = null, trust = DEFAULT_PRESET, excludeSkills = null, deletionGate = true) {
142
144
  const argv = ['--theme', theme, '--emit', emit];
143
145
  if (!emit.endsWith('-global')) {
144
146
  if (out) argv.push('--out', out);
@@ -166,6 +168,9 @@ export function setupBuildArgs(theme, emit, out = null, root = null, footprint =
166
168
  // feeds it), so the flag's absence and `none` are the same answer, and every argv that
167
169
  // excludes nothing stays the bytes it was before the flag existed.
168
170
  if (excludeSkills?.length) argv.push('--exclude-skills', excludeSkills.join(','));
171
+ // Law IV's gate elides when armed: the driver's default IS armed (no config key feeds it), so
172
+ // only an explicit `false` speaks, and every armed argv stays the bytes it was.
173
+ if (deletionGate === false) argv.push('--no-deletion-gate');
169
174
  return argv;
170
175
  }
171
176
 
@@ -224,9 +229,12 @@ export function installProfile(host, scope, root, notify = null) {
224
229
  const excludeRules = excludedRulesOfDir(root, host);
225
230
  // Skills left out stay left out: an upgrade that forgot them would put them all back.
226
231
  const excludeSkills = excludedSkillsOfDir(root, host, notify);
232
+ // Law IV's gate switch: a rebuild that forgot it would silently re-arm (or, read wrong, disarm)
233
+ // the install. A missing marker reads as armed.
234
+ const deletionGate = deletionGateOfDir(root, host);
227
235
  const out = scope === 'global' ? null : root;
228
236
  const argv = setupBuildArgs(theme, emit, out, out, footprint, posture, mode, doctrines,
229
- PACK_ORDER, excludeRules, trust, excludeSkills);
237
+ PACK_ORDER, excludeRules, trust, excludeSkills, deletionGate);
230
238
  // A GLOBAL ROW NAMES ITS OWN DIR. `root` came from the host's config-dir resolver, which
231
239
  // honours env overrides (`OPENCODE_CONFIG_DIR`, `CLAUDE_CONFIG_DIR`…); without the flag the
232
240
  // same argv run from a shell lacking that variable re-emits into the DEFAULT dir — a second
@@ -234,7 +242,8 @@ export function installProfile(host, scope, root, notify = null) {
234
242
  if (scope === 'global') argv.push('--config-dir', root);
235
243
  return {
236
244
  host, scope, root, state: installState(root, host, scope),
237
- emit, theme, footprint, posture, mode, trust, doctrines, excludeRules, excludeSkills, argv,
245
+ emit, theme, footprint, posture, mode, trust, doctrines, excludeRules, excludeSkills,
246
+ deletionGate, argv,
238
247
  };
239
248
  }
240
249
 
@@ -444,6 +444,10 @@ export function effectiveTheme(cfg, themeName, { footprint = 'full', lawsPrefix
444
444
  const skipped = [...(cfg.excludeSkills ?? [])].sort();
445
445
  // It fills a template line of its own, so its value carries the blank line around it.
446
446
  theme.EXCLUDED_SKILLS_LINE = skipped.length ? `\nExcluded skills: ${skipped.join(', ')}\n` : '';
447
+ // Law IV's gate switch, under the same contract: written only when OFF, so a default build
448
+ // stays byte-identical and a missing line reads back as armed (`deletionGateOfDir`). The
449
+ // OpenCode guard plugin reads this line at load, so its wording is a parsed contract.
450
+ theme.DELETION_GATE_LINE = cfg.deletionGate === false ? '\nDeletion gate: off' : '';
447
451
  return theme;
448
452
  }
449
453
 
@@ -399,7 +399,7 @@ export function resolveSkillNames(names, {
399
399
  */
400
400
  export function makeCfg({
401
401
  posture = 'peer', mode = 'direct', trust = DEFAULT_PRESET, doctrines = PACK_ORDER,
402
- excludeRules = [], excludeSkills = [],
402
+ excludeRules = [], excludeSkills = [], noDeletionGate = false,
403
403
  } = {}) {
404
404
  return {
405
405
  root: ROOT,
@@ -427,5 +427,8 @@ export function makeCfg({
427
427
  excludeRules: Array.isArray(excludeRules) ? [...excludeRules] : [],
428
428
  // Skill names left out of this install — same "unspecified takes nothing away" default.
429
429
  excludeSkills: Array.isArray(excludeSkills) ? [...excludeSkills] : [],
430
+ // Law IV's deletion gate (`--no-deletion-gate`). Armed unless switched off in so many words:
431
+ // only a literal `true` disarms, so every caller that never heard of the switch keeps it.
432
+ deletionGate: noDeletionGate !== true,
430
433
  };
431
434
  }