geneseed 4.8.0 → 4.9.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.
- package/CHANGELOG.md +13 -0
- package/SHIPPED.md +1 -1
- package/adapters/claude-code/README.md +3 -1
- package/adapters/opencode/plugins/geneseed-context.js +17 -1
- package/adapters/opencode/plugins/geneseed-guard.js +17 -15
- package/bin/geneseed-hook.mjs +9 -4
- package/docs/concepts/enforcement-table.md +2 -2
- package/docs/concepts/exclusions.md +18 -1
- package/docs/concepts/hooks.md +2 -2
- package/docs/reference/plugin-context.md +1 -1
- package/docs/reference/plugin-guard.md +1 -1
- package/docs/reference/troubleshoot-doctor.md +2 -1
- package/docs/web-ui.md +7 -6
- package/harness.config.json +1 -1
- package/js/build/args.mjs +3 -0
- package/js/build/driver.mjs +14 -6
- package/js/build/emit-claude.mjs +3 -3
- package/js/build/generate.mjs +16 -12
- package/js/build/render.mjs +7 -5
- package/js/build/source.mjs +19 -1
- package/js/cli-table.json +75 -0
- package/js/hosts/hooks-done.mjs +4 -0
- package/js/hosts/hooks-learn.mjs +3 -1
- package/js/hosts/hooks.mjs +3 -1
- package/js/hosts/installs.mjs +20 -11
- package/js/hosts/settings.mjs +17 -8
- package/js/inspect/diff.mjs +4 -4
- package/js/inspect/status.mjs +4 -3
- package/js/inspect/validate.mjs +9 -4
- package/js/maintain/uninstall.mjs +7 -5
- package/js/maintain/update.mjs +1 -1
- package/js/web/actions.mjs +20 -16
- package/js/web/api.mjs +4 -3
- package/package.json +1 -1
- package/src/AGENT.md.tmpl +1 -1
- package/web/dist/assets/{Activity-B-XbKlzu.js → Activity-DZ3O_DsJ.js} +1 -1
- package/web/dist/assets/{ActivityDetail-8oq0fycD.js → ActivityDetail-BQSWwoJ-.js} +1 -1
- package/web/dist/assets/Dashboard-COLskkOJ.js +2 -0
- package/web/dist/assets/Docs-DUxizutI.js +5 -0
- package/web/dist/assets/FilterInput-zLFXoSt6.js +1 -0
- package/web/dist/assets/GreenhouseView-BSzVX_l7.js +2 -0
- package/web/dist/assets/{Installs-DrqPaLDM.js → Installs-Ck4YyEzs.js} +2 -2
- package/web/dist/assets/JournalView-BbbACsYK.js +1 -0
- package/web/dist/assets/Laws-BZ_M86r3.js +3 -0
- package/web/dist/assets/{Library-BCaqPf61.js → Library-D_4LbUg2.js} +1 -1
- package/web/dist/assets/{Loops-Bx3HnlUA.js → Loops-Bzb2Y96W.js} +2 -2
- package/web/dist/assets/{Markdown-BK38dn4x.js → Markdown-iHvBrpRN.js} +1 -1
- package/web/dist/assets/{OperatorHudView-ChQPM8JE.js → OperatorHudView-D-nzB6_r.js} +1 -1
- package/web/dist/assets/{Personal-DELm61DF.js → Personal-C66284wd.js} +1 -1
- package/web/dist/assets/Tabs-DkPfjG3H.js +1 -0
- package/web/dist/assets/{activity-D4w9t8fE.js → activity-DvPxoS4Y.js} +1 -1
- package/web/dist/assets/drawable-DlrXDOJ7.js +1 -0
- package/web/dist/assets/{index-CHpydQJg.css → index-BCJz4_8C.css} +1 -1
- package/web/dist/assets/{index-r2jRgf6l.js → index-DJorAjeH.js} +2 -2
- package/web/dist/assets/{react-Ci4wQD4a.js → react-BOF6yL40.js} +1 -1
- package/web/dist/assets/{sections-GalMS0Zv.js → sections-BET3c1WT.js} +1 -1
- package/web/dist/assets/{usePoll-BcywUo55.js → usePoll-NSTzLOF4.js} +1 -1
- package/web/dist/index.html +4 -4
- package/web/src/pages/Laws.jsx +52 -25
- package/web/dist/assets/Dashboard-C4EMz5mf.js +0 -2
- package/web/dist/assets/Docs-mq4XDQK_.js +0 -5
- package/web/dist/assets/FilterInput-Ikvi-pS9.js +0 -1
- package/web/dist/assets/GreenhouseView-D1WOtq3n.js +0 -2
- package/web/dist/assets/JournalView-BYpASU-L.js +0 -1
- package/web/dist/assets/Laws-A3PFwiPD.js +0 -3
- package/web/dist/assets/Tabs-DcMq5IvC.js +0 -1
- package/web/dist/assets/drawable-BI2_tF3o.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,19 @@ label. For the capability ↔ spec map, see [SHIPPED.md](SHIPPED.md).
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [4.9.0] — 2026-10-18
|
|
12
|
+
|
|
13
|
+
Minor: on/off switches for the Sealed Secrets and done gates, and the console calls the laws Dictates.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- **On/off switches for the Sealed Secrets and Verify Before Asserting gates**, beside Law IV's. `geneseed build --no-secrets-gate` stands down the credential scan (`rule-gate` on Claude Code and OpenClaude, `tool-gate` on Bob, the OpenCode guard's key-file block and content scan); `--no-done-gate` stands down the done check in `learn` on Stop (and OpenCode's reminder), while learning still runs. Both armed by default; the Rule's text still binds. The install records `Secrets gate: off` / `Done gate: off` beside its pack list; rebuilds, upgrades, `status`, `diff`, `validate` and the console keep them. The console's Constitution page now has a toggle column on every Rule: a switch on Rules I, III and IV, *no gate* on the others.
|
|
17
|
+
|
|
18
|
+
### Upgrade notes
|
|
19
|
+
- The secrets and done gate switches: run `geneseed rebuild-all` before switching either off. While the machine's hook shim points at an older checkout, a disarmed install's `rule-gate`/`tool-gate` refuses the new flag and lets every write through (External Gate and Persist Insight too, and on Bob every check), and its `learn` refuses `--no-done-gate`, so that install stops learning as well. OpenCode reads both switches when it starts: restart it after flipping one.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- **The console calls the laws Dictates**, the name AGENT.md gives them, instead of Invariants: the Laws tab and band, the dashboard KPI tile, the constitution map, Greenhouse and the Operator HUD. Routes and internal keys are unchanged (#321).
|
|
23
|
+
|
|
11
24
|
## [4.8.0] — 2026-10-18
|
|
12
25
|
|
|
13
26
|
Minor: Deletion Is Deliberate asks only on real data loss, and can be switched off per install.
|
package/SHIPPED.md
CHANGED
|
@@ -20,7 +20,7 @@ historical references, not files distributed with the repo.
|
|
|
20
20
|
> **On version numbers.** [`harness.config.json`](harness.config.json)'s `version` is a
|
|
21
21
|
> human-readable label only. The canonical identity of an installed harness is the
|
|
22
22
|
> **source fingerprint** stamped in `.geneseed-version` (see `geneseed version`), not the
|
|
23
|
-
> string. Current label: **4.
|
|
23
|
+
> string. Current label: **4.9.0**.
|
|
24
24
|
|
|
25
25
|
## Capabilities
|
|
26
26
|
|
|
@@ -20,7 +20,9 @@ absolute-path hooks instead — `geneseed setup` does that for you.) It:
|
|
|
20
20
|
wired with `--no-consent`: commits and pushes pass without asking, while the
|
|
21
21
|
Law IV refusals (asked only on real data loss: a force-push to `main`, a `reset --hard`
|
|
22
22
|
over uncommitted work, an `rm` git cannot undo) stay in every build unless the install is
|
|
23
|
-
built `--no-deletion-gate`, which appends that flag and stands the real-loss check down
|
|
23
|
+
built `--no-deletion-gate`, which appends that flag and stands the real-loss check down
|
|
24
|
+
(`--no-secrets-gate` does the same for `rule-gate`'s credential scan, `--no-done-gate` for
|
|
25
|
+
`learn`'s done check). The hook inspects
|
|
24
26
|
command and, when it runs a `git commit` or `git push` (bare, flagged, `-C <path>`,
|
|
25
27
|
or chained like `git add . && git commit … && git push`), returns
|
|
26
28
|
`permissionDecision: "ask"` so Claude Code **prompts on every such call**. Crucially
|
|
@@ -895,6 +895,21 @@ const DONE_LIST = ".geneseed/done-checks.txt"
|
|
|
895
895
|
const DONE_EDIT_TOOLS = new Set(["edit", "write", "patch", "apply_patch", "multiedit"])
|
|
896
896
|
const DONE_SHELL_TOOLS = new Set(["bash", "shell"])
|
|
897
897
|
|
|
898
|
+
// Law III's gate switch (`--no-done-gate`, user decision 2026-10-18): the build writes
|
|
899
|
+
// `Done gate: off` into the install's AGENT.md only when the gate is off. Read ONCE, at the first
|
|
900
|
+
// use, from the harness dir (a switch flip takes an OpenCode restart); anything unreadable reads
|
|
901
|
+
// as ARMED. Off silences the reminder and the V2 notice, nothing else.
|
|
902
|
+
let doneGateOffOnce = null
|
|
903
|
+
function doneGateOff() {
|
|
904
|
+
doneGateOffOnce ??= (async () => {
|
|
905
|
+
const dir = await harnessDir()
|
|
906
|
+
if (!dir) return false
|
|
907
|
+
const text = await fs.readFile(path.join(dir, "AGENT.md"), "utf8")
|
|
908
|
+
return /^Done gate:[ \t]*off[ \t]*\r?$/m.test(text)
|
|
909
|
+
})().catch(() => false)
|
|
910
|
+
return doneGateOffOnce
|
|
911
|
+
}
|
|
912
|
+
|
|
898
913
|
async function readDoneChecks(root) {
|
|
899
914
|
try {
|
|
900
915
|
const text = await fs.readFile(path.join(root, DONE_LIST), "utf8")
|
|
@@ -1140,6 +1155,7 @@ export const GeneseedContext = async (ctx) => {
|
|
|
1140
1155
|
// INJECT_OFF and the visible delivery leave it on, and no early return below can skip it.
|
|
1141
1156
|
// Its own function, so its early returns end the reminder, never the context injection.
|
|
1142
1157
|
await (async () => {
|
|
1158
|
+
if (await doneGateOff()) return
|
|
1143
1159
|
const checks = await readDoneChecks(root)
|
|
1144
1160
|
if (!checks.length) return
|
|
1145
1161
|
const owed = doneOwed(output.messages, checks, root)
|
|
@@ -1211,7 +1227,7 @@ async function setupV2(ctx) {
|
|
|
1211
1227
|
})
|
|
1212
1228
|
|
|
1213
1229
|
// The done gate is not judged on OpenCode 2 (see `doneOwed`): said once, where its log goes.
|
|
1214
|
-
if ((await readDoneChecks(root).catch(() => [])).length) {
|
|
1230
|
+
if (!(await doneGateOff()) && (await readDoneChecks(root).catch(() => [])).length) {
|
|
1215
1231
|
console.error(`[geneseed-context] done gate: ${DONE_LIST} found, but OpenCode 2 turns are not judged.`)
|
|
1216
1232
|
}
|
|
1217
1233
|
|
|
@@ -124,26 +124,28 @@ const MODE = (process.env.GENESEED_GUARD || "on").toLowerCase()
|
|
|
124
124
|
const OFF = ["off", "0", "false", "no"].includes(MODE)
|
|
125
125
|
const WARN_ONLY = MODE === "warn"
|
|
126
126
|
|
|
127
|
-
//
|
|
128
|
-
// `Deletion gate: off`
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
127
|
+
// The gate switches (`--no-deletion-gate` 2026-10-17, `--no-secrets-gate` 2026-10-18, user
|
|
128
|
+
// decisions). The build writes `Deletion gate: off` / `Secrets gate: off` into the install's
|
|
129
|
+
// AGENT.md only when that gate is off; this reads the carrier ONCE at load, from the same
|
|
130
|
+
// harness dir the context plugin resolves ($GENESEED_HARNESS, else <plugin dir>/.. for a global
|
|
131
|
+
// install, else <plugin dir>/../.. for a project one), so flipping a switch takes an OpenCode
|
|
132
|
+
// restart. Anything unreadable or unexpected reads as ARMED. Each stands down only its own check
|
|
133
|
+
// below (the real-loss check; the Sealed Secrets path block and content scan) — catastrophic
|
|
134
|
+
// commands, root scans and protected checks still block.
|
|
135
|
+
function carrierText() {
|
|
135
136
|
try {
|
|
136
137
|
const dirs = [path.resolve(PLUGIN_DIR, ".."), path.resolve(PLUGIN_DIR, "..", "..")]
|
|
137
138
|
if (process.env.GENESEED_HARNESS) dirs.unshift(process.env.GENESEED_HARNESS)
|
|
138
139
|
for (const d of dirs) {
|
|
139
|
-
|
|
140
|
-
try { text = readFileSync(path.join(d, "AGENT.md"), "utf8") } catch { continue }
|
|
141
|
-
return /^Deletion gate:[ \t]*off[ \t]*\r?$/m.test(text)
|
|
140
|
+
try { return readFileSync(path.join(d, "AGENT.md"), "utf8") } catch { continue }
|
|
142
141
|
}
|
|
143
142
|
} catch { /* fall through: armed */ }
|
|
144
|
-
return
|
|
143
|
+
return ""
|
|
145
144
|
}
|
|
146
|
-
const
|
|
145
|
+
const CARRIER_TEXT = carrierText()
|
|
146
|
+
const gateOff = (marker) => new RegExp(`^${marker}:[ \\t]*off[ \\t]*\\r?$`, "m").test(CARRIER_TEXT)
|
|
147
|
+
const DELETION_GATE_OFF = gateOff("Deletion gate")
|
|
148
|
+
const SECRETS_GATE_OFF = gateOff("Secrets gate")
|
|
147
149
|
|
|
148
150
|
function log(msg) { console.error(`[geneseed-guard] ${msg}`) }
|
|
149
151
|
|
|
@@ -1168,7 +1170,7 @@ export const GeneseedGuard = async (ctx) => {
|
|
|
1168
1170
|
// blocks the whole call (see `pickPath` for why this is not a single path).
|
|
1169
1171
|
const paths = pickPath(args, cwd)
|
|
1170
1172
|
for (const p of paths) {
|
|
1171
|
-
if (SECRET_RE.some((re) => re.test(p))) { await deny(`write to secret/key file ${p} (Sealed Secrets)`, "law-1"); return }
|
|
1173
|
+
if (!SECRETS_GATE_OFF && SECRET_RE.some((re) => re.test(p))) { await deny(`write to secret/key file ${p} (Sealed Secrets)`, "law-1"); return }
|
|
1172
1174
|
if (SECRET_WARN_RE.some((re) => re.test(p))) log(`WARN: writing ${p} — keep secrets out of tracked files (Sealed Secrets)`)
|
|
1173
1175
|
const sensor = protectedCheck(p)
|
|
1174
1176
|
if (sensor) { await deny(`${p} is a protected check (${sensor === DONE_LIST ? "the done gate list itself" : `${sensor}, listed in ${SENSOR_LIST}`}) — a check the agent can edit is not a check (External Gate)`, "rigor-5"); return }
|
|
@@ -1185,7 +1187,7 @@ export const GeneseedGuard = async (ctx) => {
|
|
|
1185
1187
|
// FILE, not pooled — a patch touching `.env` AND an ordinary file must still catch a
|
|
1186
1188
|
// secret in the ordinary file; only the hunk whose OWN path is `.env`-shaped is
|
|
1187
1189
|
// exempt, same as the path WARN tier above.
|
|
1188
|
-
for (const { path: cp, content } of pickAddedContentByPath(args, cwd)) {
|
|
1190
|
+
for (const { path: cp, content } of SECRETS_GATE_OFF ? [] : pickAddedContentByPath(args, cwd)) {
|
|
1189
1191
|
if (SECRET_WARN_RE.some((re) => re.test(cp))) continue
|
|
1190
1192
|
if (SECRET_CONTENT_RE.test(content)) {
|
|
1191
1193
|
await deny(`${cp} would carry a credential-shaped string (Sealed Secrets) — secrets `
|
package/bin/geneseed-hook.mjs
CHANGED
|
@@ -75,16 +75,21 @@ const VERBS = {
|
|
|
75
75
|
flags: HOSTED,
|
|
76
76
|
switches: { '--no-consent': 'noConsent', '--no-deletion-gate': 'noDeletionGate' },
|
|
77
77
|
},
|
|
78
|
-
'rule-gate': {
|
|
78
|
+
'rule-gate': {
|
|
79
|
+
load: gates, fn: 'cmdRuleGate', flags: HOSTED, switches: { '--no-secrets-gate': 'noSecretsGate' },
|
|
80
|
+
},
|
|
79
81
|
'tool-gate': {
|
|
80
|
-
load: gates,
|
|
82
|
+
load: gates,
|
|
83
|
+
fn: 'cmdToolGate',
|
|
84
|
+
flags: HOSTED,
|
|
85
|
+
switches: { '--no-deletion-gate': 'noDeletionGate', '--no-secrets-gate': 'noSecretsGate' },
|
|
81
86
|
},
|
|
82
87
|
learn: {
|
|
83
88
|
load: () => import('../js/hosts/hooks-learn.mjs'),
|
|
84
89
|
fn: 'cmdLearn',
|
|
85
90
|
// `--host` only keys the global stand-down's marker (js/hosts/hosts.mjs); learn has no dialect.
|
|
86
91
|
flags: { '--memory': 'memory', '--host': 'host' },
|
|
87
|
-
switches: { '--consolidate': 'consolidate' },
|
|
92
|
+
switches: { '--consolidate': 'consolidate', '--no-done-gate': 'noDoneGate' },
|
|
88
93
|
positional: 'file',
|
|
89
94
|
},
|
|
90
95
|
};
|
|
@@ -104,7 +109,7 @@ function die(code, msg) {
|
|
|
104
109
|
function parse(spec, argv) {
|
|
105
110
|
const args = {
|
|
106
111
|
root: null, host: null, memory: null, consolidate: false, noConsent: false,
|
|
107
|
-
noDeletionGate: false, file: null,
|
|
112
|
+
noDeletionGate: false, noSecretsGate: false, noDoneGate: false, file: null,
|
|
108
113
|
};
|
|
109
114
|
for (let i = 0; i < argv.length; i += 1) {
|
|
110
115
|
const tok = argv[i];
|
|
@@ -21,12 +21,12 @@ silently guarding the wrong rule after a renumber. See [ADR 0005](../adr/0005-st
|
|
|
21
21
|
|
|
22
22
|
| id | Pack | Claude Code | OpenClaude | OpenCode | Bob |
|
|
23
23
|
|---|---|---|---|---|---|
|
|
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) |
|
|
24
|
+
| `sealed-secrets` | law | ask: rule-gate (credential-shaped content outside `.env*`). Switchable per install with `--no-secrets-gate` (see [Exclusions](exclusions.md)): this scan stands down on every host, the guard plugin's path block included | same as Claude Code | block: guard plugin (key/credential file path, or vendor-prefixed secret content); warns only on `.env*` | block (exit 2) |
|
|
25
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) |
|
|
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
|
+
| `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; switchable per install with `--no-done-gate` (see [Exclusions](exclusions.md)) | 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) |
|
|
30
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) |
|
|
31
31
|
|
|
32
32
|
Bob has no "ask" tier (see [ADR 0007](../adr/0007-gates-fail-closed-per-host-ceiling.md)), so a
|
|
@@ -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 (plus
|
|
10
|
+
Geneseed has three ways to switch something off, and they work at different scales (plus an on/off switch for the hook gates of Laws I, III and IV, below):
|
|
11
11
|
|
|
12
12
|
| | Sovereign repo | Per-rule exclusion | Per-skill exclusion |
|
|
13
13
|
| --- | --- | --- | --- |
|
|
@@ -88,3 +88,20 @@ Limits:
|
|
|
88
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
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
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.
|
|
91
|
+
|
|
92
|
+
### Law I secrets gate and Law III done gate — on or off
|
|
93
|
+
|
|
94
|
+
The same switch, for two more gates. Both are armed by default, and the Rule's text in the carrier still binds the agent with either off.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
geneseed build --emit claude-global --no-secrets-gate # Sealed Secrets
|
|
98
|
+
geneseed build --emit claude-global --no-done-gate # Verify Before Asserting
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The console's Constitution page has a switch on Rule I's and Rule III's rows too (*Enforced by* reads **Gate off** while one is off); the other Rules show *no gate*, because only their text enforces them. The install records `Secrets gate: off` / `Done gate: off` beside `Deletion gate:`, written only when off; rebuilds, upgrades and `geneseed status` carry them like Law IV's.
|
|
102
|
+
|
|
103
|
+
What stops, per host:
|
|
104
|
+
- **`--no-secrets-gate`**: the credential scan of `rule-gate` (Claude Code, OpenClaude) and of `tool-gate` (Bob, which then no longer exits 2 for it), and on OpenCode the guard plugin's *Sealed Secrets* block — both the key/credential file path block and the content scan. *External Gate*, *Persist Insight*, the deletion gate and the root-scan check stay.
|
|
105
|
+
- **`--no-done-gate`**: the done check `learn` runs on Stop (Claude Code, OpenClaude; on Bob, the "turn not judged" log line), and on OpenCode the context plugin's reminder (OpenCode 1) and its one-line notice (OpenCode 2). Learning itself still runs, and `.geneseed/done-checks.txt` stays protected by *External Gate*.
|
|
106
|
+
|
|
107
|
+
Limits: OpenCode reads both switches from the install's `AGENT.md` when it starts, so restart it after flipping one. A global install beside a project install: `rule-gate` and Bob's `tool-gate` do not stand down for a project install on any host, and neither does the OpenCode guard, so an armed global install still asks about (or blocks) a credential write inside a project whose own install has the secrets gate off. `learn` does stand down on Claude Code and OpenClaude, so there the project's done gate switch decides. And the older-shim limit above is sharper for the done gate: its flag rides on `learn`, so while the shim points at an older checkout an install with the done gate off **loses learning too**, not only the gate. Run `geneseed rebuild-all` before switching either gate off.
|
package/docs/concepts/hooks.md
CHANGED
|
@@ -42,13 +42,13 @@ Geneseed writes its hooks into the host's settings file. Every hook calls one pr
|
|
|
42
42
|
- Any other command passes untouched.
|
|
43
43
|
|
|
44
44
|
**rule-gate.** Runs before every file write.
|
|
45
|
-
- 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*.
|
|
45
|
+
- 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*. An install built `--no-secrets-gate` wires the hook with that flag and this check alone stands down ([Exclusions](exclusions.md)).
|
|
46
46
|
- 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).
|
|
47
47
|
- 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.
|
|
48
48
|
- 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.
|
|
49
49
|
- Ordinary edits pass untouched.
|
|
50
50
|
|
|
51
|
-
**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.
|
|
51
|
+
**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. An install built `--no-done-gate` wires `learn` with that flag: the check is skipped, learning still runs ([Exclusions](exclusions.md)).
|
|
52
52
|
|
|
53
53
|
**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:
|
|
54
54
|
|
|
@@ -17,7 +17,7 @@ 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.
|
|
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, and so does an install built `--no-done-gate` (`Done gate: off` in its `AGENT.md`, read once when OpenCode starts), which also silences the OpenCode 2 log line.
|
|
21
21
|
|
|
22
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.
|
|
23
23
|
|
|
@@ -10,7 +10,7 @@ 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
|
|
13
|
+
- **Blocks** writes to private-key and credential files and writes whose content carries a credential (*Sealed Secrets*; an install built `--no-secrets-gate` carries `Secrets gate: off` in its `AGENT.md`, read once at load, and both stop blocking), 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
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
|
|
|
@@ -43,7 +43,8 @@ These come only from `geneseed doctor --probe`. It runs each installed gate exac
|
|
|
43
43
|
settings spell it, through the host's own shell, on `git reflog expire --expire=now --all` (which must be refused)
|
|
44
44
|
and `git status` (which must go through). A gate wired with `--no-deletion-gate` lets the first one
|
|
45
45
|
through by design, so it is probed on `find / -name geneseed-probe` instead — a root-wide scan it
|
|
46
|
-
must still refuse.
|
|
46
|
+
must still refuse. `--no-secrets-gate` and `--no-done-gate` change nothing here: neither judges a
|
|
47
|
+
shell command. An OpenCode install's guard plugin has no command to
|
|
47
48
|
run: the probe imports it and calls its hook through both plugin APIs (V1 and V2), with `rm -rf /`
|
|
48
49
|
as the call it must refuse. `GENESEED_GUARD=off` or `warn` in your terminal makes that a note,
|
|
49
50
|
not a failure.
|
package/docs/web-ui.md
CHANGED
|
@@ -77,8 +77,8 @@ streams the output of background jobs and keeps their history across reloads.
|
|
|
77
77
|
**Operator** (a searchable table of every deployed capability). Headlines reflect the
|
|
78
78
|
active theme's voice.
|
|
79
79
|
|
|
80
|
-
The KPI band reads **Agents · Skills ·
|
|
81
|
-
labelled *
|
|
80
|
+
The KPI band reads **Agents · Skills · Dictates · Local edits**. That third tile is
|
|
81
|
+
labelled *Dictates* (the name AGENT.md gives the laws) and not *Constitution* on purpose: its value is the law count
|
|
82
82
|
alone, and calling it the constitution would under-report the ontology and the doctrine
|
|
83
83
|
packs. The two Status *layouts* that have room for it add the missing half: Greenhouse
|
|
84
84
|
puts an active-packs fraction under the tile (`3/4 packs · N rules`), and the Operator HUD
|
|
@@ -95,12 +95,13 @@ streams the output of background jobs and keeps their history across reloads.
|
|
|
95
95
|
content the page last read — if an agent session edited the file meanwhile, the save
|
|
96
96
|
409s and the page reloads instead of clobbering it.
|
|
97
97
|
- **Constitution** (`#/laws`) — the whole governance surface on one page, in three bands:
|
|
98
|
-
the **Ethos** (four prose sections), the **
|
|
98
|
+
the **Ethos** (four prose sections), the **Dictates** (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).
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
this install). Rules I, III and IV each carry a switch for the install's hook gate — the
|
|
102
|
+
secrets gate, the done gate and the deletion gate (`--no-secrets-gate`, `--no-done-gate`,
|
|
103
|
+
`--no-deletion-gate`; *Enforced by* reads *Gate off* while one is off) — flipping one asks,
|
|
104
|
+
then rebuilds the install once; the other Rules read *no gate*. Every row expands to its full body, lazily fetched. The rail entry is
|
|
104
105
|
labelled *Constitution* while the route, the id and the badge key stay `laws`; deep links
|
|
105
106
|
are `#/item/law/ont:telos`, `#/item/law/IV` and `#/item/law/process.5` — three address
|
|
106
107
|
shapes that cannot collide, because a slug carries a colon no numeral has and a doctrine
|
package/harness.config.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "4.9.0"
|
|
6
6
|
}
|
package/js/build/args.mjs
CHANGED
|
@@ -104,6 +104,8 @@ const FLAGS = {
|
|
|
104
104
|
// Law IV's gate switch. No `harness.config.json` key feeds it, deliberately: a rebuild elides
|
|
105
105
|
// the flag when armed, so a config default could disarm an armed install on its next upgrade.
|
|
106
106
|
'--no-deletion-gate': 'noDeletionGate',
|
|
107
|
+
// Laws I and III, the same contract (`GATE_SWITCHES` in source.mjs).
|
|
108
|
+
'--no-secrets-gate': 'noSecretsGate', '--no-done-gate': 'noDoneGate',
|
|
107
109
|
'-v': 'verbose', '--verbose': 'verbose',
|
|
108
110
|
};
|
|
109
111
|
|
|
@@ -312,6 +314,7 @@ export function parseArgs(argv, defaults) {
|
|
|
312
314
|
excludeSkills: defaults.excludeSkills ?? [],
|
|
313
315
|
out: 'Harness', emit: 'files', footprint: 'lean', root: null,
|
|
314
316
|
syncThemes: false, validateOnly: false, verbose: false, noDeletionGate: false,
|
|
317
|
+
noSecretsGate: false, noDoneGate: false,
|
|
315
318
|
};
|
|
316
319
|
const { tokens } = nodeParseArgs({
|
|
317
320
|
args: argv, options: PARSE_OPTIONS, allowPositionals: true, strict: false, tokens: true,
|
package/js/build/driver.mjs
CHANGED
|
@@ -647,19 +647,25 @@ 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({
|
|
650
|
+
function axesCfg({
|
|
651
|
+
posture, mode, trust, doctrines, excludeRules, excludeSkills,
|
|
652
|
+
deletionGate = null, secretsGate = null, doneGate = null,
|
|
653
|
+
}) {
|
|
651
654
|
return makeCfg({
|
|
652
655
|
...(posture ? { posture } : {}), ...(mode ? { mode } : {}), ...(trust ? { trust } : {}),
|
|
653
656
|
...(doctrines ? { doctrines } : {}), ...(excludeRules ? { excludeRules } : {}),
|
|
654
657
|
...(excludeSkills ? { excludeSkills } : {}),
|
|
655
658
|
// Only an explicit `false` disarms; `null` (no opinion) keeps the gate.
|
|
656
659
|
...(deletionGate === false ? { noDeletionGate: true } : {}),
|
|
660
|
+
...(secretsGate === false ? { noSecretsGate: true } : {}),
|
|
661
|
+
...(doneGate === false ? { noDoneGate: true } : {}),
|
|
657
662
|
});
|
|
658
663
|
}
|
|
659
664
|
|
|
660
665
|
export function emitProjectInto(host, {
|
|
661
666
|
theme, out, root, footprint = 'full', posture = null, mode = null, trust = null,
|
|
662
667
|
doctrines = null, excludeRules = null, excludeSkills = null, deletionGate = null,
|
|
668
|
+
secretsGate = null, doneGate = null,
|
|
663
669
|
}) {
|
|
664
670
|
const emit = PROJECT_EMITS[host];
|
|
665
671
|
// The axes ride the same rail into every `*Into`, and for the same reason: a drift reader
|
|
@@ -667,7 +673,8 @@ export function emitProjectInto(host, {
|
|
|
667
673
|
// edited on every narrowed install, and `validate --doctrines craft` that ignored its flag
|
|
668
674
|
// checked an all-packs render nobody asked for.
|
|
669
675
|
return withPlatformNewlines(() => emit(
|
|
670
|
-
axesCfg({
|
|
676
|
+
axesCfg({
|
|
677
|
+
posture, mode, trust, doctrines, excludeRules, excludeSkills, deletionGate, secretsGate, doneGate }),
|
|
671
678
|
{ theme, footprint, root }, out,
|
|
672
679
|
));
|
|
673
680
|
}
|
|
@@ -682,16 +689,16 @@ export function emitProjectInto(host, {
|
|
|
682
689
|
*/
|
|
683
690
|
export function buildInto({
|
|
684
691
|
theme, out, footprint = 'lean', posture = null, mode = null, trust = null, doctrines = null,
|
|
685
|
-
excludeRules = null, excludeSkills = null, deletionGate = null,
|
|
692
|
+
excludeRules = null, excludeSkills = null, deletionGate = null, secretsGate = null, doneGate = null,
|
|
686
693
|
}) {
|
|
687
694
|
return withPlatformNewlines(() => build(axesCfg({
|
|
688
|
-
posture, mode, trust, doctrines, excludeRules, excludeSkills, deletionGate }),
|
|
695
|
+
posture, mode, trust, doctrines, excludeRules, excludeSkills, deletionGate, secretsGate, doneGate }),
|
|
689
696
|
theme, out, { footprint, nativeCatalog: false }));
|
|
690
697
|
}
|
|
691
698
|
|
|
692
699
|
export function emitGlobalInto(host, {
|
|
693
700
|
theme, out, cfgDir, footprint, posture = null, mode = null, doctrines = null, trust = null,
|
|
694
|
-
excludeRules = null, excludeSkills = null, deletionGate = null,
|
|
701
|
+
excludeRules = null, excludeSkills = null, deletionGate = null, secretsGate = null, doneGate = null,
|
|
695
702
|
}) {
|
|
696
703
|
// `build.HOSTS.get(host, build.HOSTS["opencode"])` — an unknown host falls back rather than
|
|
697
704
|
// raising, because the host comes from a marker file a user can edit.
|
|
@@ -707,7 +714,8 @@ export function emitGlobalInto(host, {
|
|
|
707
714
|
// edited on every install that chose a register, narrowed its packs or excluded a rule —
|
|
708
715
|
// the same scar the footprint left in `diffCollect`, one axis over each time.
|
|
709
716
|
return withPlatformNewlines(() => emit(
|
|
710
|
-
axesCfg({
|
|
717
|
+
axesCfg({
|
|
718
|
+
posture, mode, trust, doctrines, excludeRules, excludeSkills, deletionGate, secretsGate, doneGate }),
|
|
711
719
|
{ theme, footprint, root: null, cfgDir }, out,
|
|
712
720
|
));
|
|
713
721
|
}
|
package/js/build/emit-claude.mjs
CHANGED
|
@@ -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, cfg.deletionGate);
|
|
185
|
+
cfg.excludeRules, cfg.deletionGate, { secretsGate: cfg.secretsGate, doneGate: cfg.doneGate });
|
|
186
186
|
|
|
187
187
|
return {
|
|
188
188
|
owned,
|
|
@@ -216,7 +216,7 @@ export function emitClaudeRender(cfg, job) {
|
|
|
216
216
|
* always a string, so `''` is the only falsy value either language sees.
|
|
217
217
|
*/
|
|
218
218
|
function claudeWire(job, claudeMdText, hasAgentText, doctrines = null, excludeRules = [],
|
|
219
|
-
deletionGate = true) {
|
|
219
|
+
deletionGate = true, gates = {}) {
|
|
220
220
|
phaseLog('WIRE');
|
|
221
221
|
const { cfgDir, claudeMd, scope, host, oldManaged, preambleExclude, hookOpts } = job;
|
|
222
222
|
const old = oldManaged && typeof oldManaged === 'object' && !Array.isArray(oldManaged)
|
|
@@ -280,7 +280,7 @@ function claudeWire(job, claudeMdText, hasAgentText, doctrines = null, excludeRu
|
|
|
280
280
|
// DECIDES the install's packs, so the marker on disk is still the previous build's.
|
|
281
281
|
const [, managedHooks] = mergeClaudeSettings(
|
|
282
282
|
settingsPath, oldSf === settingsName ? get(old, 'settings_hooks') : null, hookOpts,
|
|
283
|
-
doctrines, excludeRules, host, cfgDir, deletionGate,
|
|
283
|
+
doctrines, excludeRules, host, cfgDir, deletionGate, gates,
|
|
284
284
|
);
|
|
285
285
|
managed.settings_hooks = managedHooks;
|
|
286
286
|
|
package/js/build/generate.mjs
CHANGED
|
@@ -35,12 +35,12 @@ import { existsSync, mkdirSync, statSync } from 'node:fs';
|
|
|
35
35
|
import path from 'node:path';
|
|
36
36
|
|
|
37
37
|
import { main as driverMain } from './driver.mjs';
|
|
38
|
-
import { makeCfg, PACK_ORDER } from './source.mjs';
|
|
38
|
+
import { GATE_SWITCHES, makeCfg, PACK_ORDER } from './source.mjs';
|
|
39
39
|
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
|
-
|
|
43
|
+
excludedRulesOfDir, excludedSkillsOfDir, footprintOfDir, gatesOfDir, 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,8 +102,9 @@ 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
|
|
106
|
-
|
|
105
|
+
// The bare switches `build` forwards (the gate switches): they take no value, so they are
|
|
106
|
+
// not BUILD_FORWARD rows.
|
|
107
|
+
for (const g of GATE_SWITCHES) if (args[g.arg]) argv.push(g.flag);
|
|
107
108
|
return driverMain(argv);
|
|
108
109
|
}
|
|
109
110
|
|
|
@@ -140,7 +141,8 @@ export function cmdBuild(args) {
|
|
|
140
141
|
*/
|
|
141
142
|
export function setupBuildArgs(theme, emit, out = null, root = null, footprint = 'lean',
|
|
142
143
|
posture = 'peer', mode = 'direct', doctrines = null, allPacks = PACK_ORDER,
|
|
143
|
-
excludeRules = null, trust = DEFAULT_PRESET, excludeSkills = null, deletionGate = true
|
|
144
|
+
excludeRules = null, trust = DEFAULT_PRESET, excludeSkills = null, deletionGate = true,
|
|
145
|
+
{ secretsGate = true, doneGate = true } = {}) {
|
|
144
146
|
const argv = ['--theme', theme, '--emit', emit];
|
|
145
147
|
if (!emit.endsWith('-global')) {
|
|
146
148
|
if (out) argv.push('--out', out);
|
|
@@ -168,9 +170,11 @@ export function setupBuildArgs(theme, emit, out = null, root = null, footprint =
|
|
|
168
170
|
// feeds it), so the flag's absence and `none` are the same answer, and every argv that
|
|
169
171
|
// excludes nothing stays the bytes it was before the flag existed.
|
|
170
172
|
if (excludeSkills?.length) argv.push('--exclude-skills', excludeSkills.join(','));
|
|
171
|
-
//
|
|
172
|
-
// only an explicit `false` speaks, and every armed argv stays the bytes it was.
|
|
173
|
-
|
|
173
|
+
// The gate switches elide when armed: the driver's default IS armed (no config key feeds
|
|
174
|
+
// them), so only an explicit `false` speaks, and every armed argv stays the bytes it was.
|
|
175
|
+
// Law order, so a Law IV-only install still ends on `--no-deletion-gate` as it did.
|
|
176
|
+
const gates = { secretsGate, doneGate, deletionGate };
|
|
177
|
+
for (const g of GATE_SWITCHES) if (gates[g.key] === false) argv.push(g.flag);
|
|
174
178
|
return argv;
|
|
175
179
|
}
|
|
176
180
|
|
|
@@ -229,12 +233,12 @@ export function installProfile(host, scope, root, notify = null) {
|
|
|
229
233
|
const excludeRules = excludedRulesOfDir(root, host);
|
|
230
234
|
// Skills left out stay left out: an upgrade that forgot them would put them all back.
|
|
231
235
|
const excludeSkills = excludedSkillsOfDir(root, host, notify);
|
|
232
|
-
//
|
|
236
|
+
// The gate switches: a rebuild that forgot one would silently re-arm (or, read wrong, disarm)
|
|
233
237
|
// the install. A missing marker reads as armed.
|
|
234
|
-
const
|
|
238
|
+
const gates = gatesOfDir(root, host);
|
|
235
239
|
const out = scope === 'global' ? null : root;
|
|
236
240
|
const argv = setupBuildArgs(theme, emit, out, out, footprint, posture, mode, doctrines,
|
|
237
|
-
PACK_ORDER, excludeRules, trust, excludeSkills, deletionGate);
|
|
241
|
+
PACK_ORDER, excludeRules, trust, excludeSkills, gates.deletionGate, gates);
|
|
238
242
|
// A GLOBAL ROW NAMES ITS OWN DIR. `root` came from the host's config-dir resolver, which
|
|
239
243
|
// honours env overrides (`OPENCODE_CONFIG_DIR`, `CLAUDE_CONFIG_DIR`…); without the flag the
|
|
240
244
|
// same argv run from a shell lacking that variable re-emits into the DEFAULT dir — a second
|
|
@@ -243,7 +247,7 @@ export function installProfile(host, scope, root, notify = null) {
|
|
|
243
247
|
return {
|
|
244
248
|
host, scope, root, state: installState(root, host, scope),
|
|
245
249
|
emit, theme, footprint, posture, mode, trust, doctrines, excludeRules, excludeSkills,
|
|
246
|
-
|
|
250
|
+
...gates, argv,
|
|
247
251
|
};
|
|
248
252
|
}
|
|
249
253
|
|
package/js/build/render.mjs
CHANGED
|
@@ -34,7 +34,7 @@ import { readText } from '../lib/fs.mjs';
|
|
|
34
34
|
import { parseJson } from '../lib/json.mjs';
|
|
35
35
|
import { normcase, comparePaths } from '../lib/paths.mjs';
|
|
36
36
|
// `js/build/source.mjs` imports nothing but node builtins, so this direction cannot cycle.
|
|
37
|
-
import { PACK_ORDER, readPackText, resolveRuleIds, ruleCanon } from './source.mjs';
|
|
37
|
+
import { GATE_SWITCHES, PACK_ORDER, readPackText, resolveRuleIds, ruleCanon } from './source.mjs';
|
|
38
38
|
import { DEFAULT_PRESET } from '../loop/score.mjs';
|
|
39
39
|
|
|
40
40
|
/** Document STRUCTURE is theme-INDEPENDENT — mirrors `_build_render.STRUCTURE`. */
|
|
@@ -444,10 +444,12 @@ 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
|
-
//
|
|
448
|
-
// stays byte-identical and a missing
|
|
449
|
-
//
|
|
450
|
-
|
|
447
|
+
// The gate switches (`GATE_SWITCHES`: Laws I, III, IV), under the same contract: a line is
|
|
448
|
+
// written only when its gate is OFF, so a default build stays byte-identical and a missing
|
|
449
|
+
// line reads back as armed (`gatesOfDir`). The OpenCode plugins read these lines at load, so
|
|
450
|
+
// their wording is a parsed contract.
|
|
451
|
+
theme.GATE_LINES = GATE_SWITCHES.filter((g) => cfg[g.key] === false)
|
|
452
|
+
.map((g) => `\n${g.marker}: off`).join('');
|
|
451
453
|
return theme;
|
|
452
454
|
}
|
|
453
455
|
|
package/js/build/source.mjs
CHANGED
|
@@ -399,7 +399,8 @@ 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 = [], noDeletionGate = false,
|
|
402
|
+
excludeRules = [], excludeSkills = [], noDeletionGate = false, noSecretsGate = false,
|
|
403
|
+
noDoneGate = false,
|
|
403
404
|
} = {}) {
|
|
404
405
|
return {
|
|
405
406
|
root: ROOT,
|
|
@@ -430,5 +431,22 @@ export function makeCfg({
|
|
|
430
431
|
// Law IV's deletion gate (`--no-deletion-gate`). Armed unless switched off in so many words:
|
|
431
432
|
// only a literal `true` disarms, so every caller that never heard of the switch keeps it.
|
|
432
433
|
deletionGate: noDeletionGate !== true,
|
|
434
|
+
// Law I's secret scan (`--no-secrets-gate`) and Law III's done gate (`--no-done-gate`), the
|
|
435
|
+
// same contract (user decision 2026-10-18; see GATE_SWITCHES).
|
|
436
|
+
secretsGate: noSecretsGate !== true,
|
|
437
|
+
doneGate: noDoneGate !== true,
|
|
433
438
|
};
|
|
434
439
|
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* The laws whose hook gate an install can switch off, in law order: the cfg/profile key, the
|
|
443
|
+
* build flag, the carrier marker (`<marker>: off`, written only when off) and the law. ONE
|
|
444
|
+
* table, so the flag, the marker, the rebuild argv, the read-back and `status` cannot drift
|
|
445
|
+
* apart. Every gate is ARMED by default and the law's text binds either way. The deletion row
|
|
446
|
+
* is #319's, unchanged: existing installs carry its flag and marker.
|
|
447
|
+
*/
|
|
448
|
+
export const GATE_SWITCHES = [
|
|
449
|
+
{ key: 'secretsGate', flag: '--no-secrets-gate', arg: 'noSecretsGate', marker: 'Secrets gate', law: 'I' },
|
|
450
|
+
{ key: 'doneGate', flag: '--no-done-gate', arg: 'noDoneGate', marker: 'Done gate', law: 'III' },
|
|
451
|
+
{ key: 'deletionGate', flag: '--no-deletion-gate', arg: 'noDeletionGate', marker: 'Deletion gate', law: 'IV' },
|
|
452
|
+
];
|