geneseed 4.9.1 → 4.9.2
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 +19 -0
- package/SHIPPED.md +1 -1
- package/adapters/opencode/plugins/geneseed-context.js +7 -1
- package/adapters/opencode/plugins/geneseed-guard.js +18 -10
- package/adapters/opencode/plugins/geneseed-learn.js +7 -1
- package/docs/concepts/hooks.md +1 -1
- package/harness.config.json +1 -1
- package/js/README.md +30 -9
- package/js/build/args.mjs +9 -8
- package/js/build/driver.mjs +21 -21
- package/js/build/emit-claude.mjs +3 -3
- package/js/build/generate.mjs +4 -5
- package/js/build/source.mjs +18 -22
- package/js/hosts/gitref.mjs +5 -3
- package/js/hosts/hooks-done.mjs +21 -9
- package/js/hosts/hooks-learn.mjs +23 -10
- package/js/hosts/hooks-prims.mjs +25 -0
- package/js/hosts/hooks.mjs +6 -1
- package/js/hosts/installs.mjs +5 -4
- package/js/hosts/native.mjs +3 -1
- package/js/hosts/owned.mjs +34 -6
- package/js/hosts/settings.mjs +5 -4
- package/js/inspect/cost.mjs +37 -5
- package/js/inspect/diff.mjs +132 -61
- package/js/inspect/validate.mjs +2 -2
- package/js/lib/fs.mjs +73 -2
- package/js/maintain/setup.mjs +31 -7
- package/js/maintain/uninstall.mjs +1 -1
- package/js/maintain/update.mjs +1 -1
- package/js/web/actions.mjs +36 -3
- package/js/web/api.mjs +12 -5
- package/js/web/handler.mjs +4 -16
- package/js/web/jobs.mjs +5 -3
- package/js/web/state.mjs +3 -0
- package/package.json +1 -1
- package/web/dist/assets/{Activity-GQGAZHIE.js → Activity-D7Igwuoo.js} +1 -1
- package/web/dist/assets/{ActivityDetail-DktWyxim.js → ActivityDetail-53rp05hx.js} +1 -1
- package/web/dist/assets/Dashboard-BoEH5qyY.js +2 -0
- package/web/dist/assets/{Docs-CsWYLnN1.js → Docs-BM56RCT8.js} +3 -3
- package/web/dist/assets/GreenhouseView-BTMvGVco.js +2 -0
- package/web/dist/assets/{Installs-CWuUU4I3.js → Installs-Dh1tB1NK.js} +2 -2
- package/web/dist/assets/JournalView-F4hlWTVB.js +1 -0
- package/web/dist/assets/Laws-iEheocNI.js +3 -0
- package/web/dist/assets/Library-d4_uOGmC.js +1 -0
- package/web/dist/assets/{Loops-BUBOeM0C.js → Loops-BbYeWl70.js} +2 -2
- package/web/dist/assets/{OperatorHudView-BJuCYsjf.js → OperatorHudView-Dki9Q6W3.js} +1 -1
- package/web/dist/assets/Personal-B_YQJjA_.js +1 -0
- package/web/dist/assets/{Tabs-GPQlocTw.js → Tabs-CAl6oFwG.js} +1 -1
- package/web/dist/assets/drawable-BdTDG5oI.js +1 -0
- package/web/dist/assets/index-BZgEGRbL.js +2 -0
- package/web/dist/index.html +1 -1
- package/web/src/pages/Laws.jsx +60 -353
- package/web/dist/assets/Dashboard-CmSn6JfK.js +0 -2
- package/web/dist/assets/GreenhouseView-Dt_cIxAY.js +0 -2
- package/web/dist/assets/JournalView-ClbZj_YF.js +0 -1
- package/web/dist/assets/Laws-R5zZCmya.js +0 -3
- package/web/dist/assets/Library-u36V7oSD.js +0 -1
- package/web/dist/assets/Personal-DWQuSAUz.js +0 -1
- package/web/dist/assets/drawable-CE-arUnF.js +0 -1
- package/web/dist/assets/index-3lnPT_1J.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,25 @@ label. For the capability ↔ spec map, see [SHIPPED.md](SHIPPED.md).
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [4.9.2] — 2026-10-18
|
|
12
|
+
|
|
13
|
+
Patch: the review pass — faster emits, console and Stop; Build and setup keep your gate switches; protected checks follow links both ways.
|
|
14
|
+
|
|
15
|
+
### Upgrade notes
|
|
16
|
+
- Run `geneseed rebuild-all` once after updating so installed hooks and OpenCode plugins pick up the changes.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- **A Stop no longer parses the whole transcript.** The done gate and `learn` each read the full session transcript on every Stop (real ones reach 50 MB), and an armed install with `GENESEED_LLM` set read it twice. Both now read backward from the end, in windows that start at 2 MB and grow fourfold: the done gate until the window holds the prompt that opened the turn (a turn longer than every window falls back to the whole file, so it is never judged on a truncated turn), `learn` until its notes hold the 16 000 characters it keeps, so the notes are the ones a full read gives. One Stop reads each window once, shared by both. On a generated 50 MB transcript (old and new interleaved, median of 10) the armed gate alone runs about 3× faster and gate + learn about 3× faster, and both now cost about the same as on a 1 KB transcript: a Stop no longer grows with the session.
|
|
20
|
+
- **The console stops re-rendering an install to measure its drift on every refresh.** The overview and the fleet table each rendered the selected install's expected tree afresh (~0.6 s per install per call); the render is now memoised per install, keyed on the source fingerprint and every axis the install was built with (theme, footprint, posture, mode, packs, trust, excluded rules and skills, gate switches), and re-done the moment any of them changes. The comparison against the deployed tree still runs on every call, so a hand edit shows at once. Measured on a two-install sandbox: overview ~0.75 s → ~0.2 s, fleet ~1.5 s → ~0.32 s after the first refresh. `geneseed diff` is unchanged.
|
|
21
|
+
- **Emits no longer read back what they just wrote to hash it.** The owned-file hashes in `.geneseed-manifest.json` are now computed from the text `writeText` recorded at write time (a copied binary, or a file rewritten since by any other writer, is still read), and the claim-on-create closure (`claimer`) resolves the config root once per emit instead of once per file. The per-file symlink and hard-link checks are unchanged, and manifests are byte-identical to before. A global re-emit runs about a quarter faster (measured on Windows: claude-global 560 → 424 ms, opencode-global 716 → 542 ms, median of 7).
|
|
22
|
+
- **The console reads user-rules.md once, under one key, and a rules or profile write reloads only its own file.** Personal's cards, the Overview's ledger and the Constitution page's "Your rules" tier share one cached `/api/rules` read (`web/src/hooks/useRules.js`), so a rule written on Personal shows on the other two at once. Each visit to one of those pages paints the cached copy and refetches it once, so a rule an agent session wrote shows without waiting for a build. On Personal, a rules write no longer refetches the profile and a profile save no longer refetches the rules. A memory fact promoted to a rule from the Library drops the cached copy, so the Rules tab it lands on shows the new rule. Install changes (build, toggle, switch, edits) still refresh the whole console.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **Protected checks follow links in both directions on every host.** Both the Claude/Bob hook and the OpenCode guard now read a write's target twice: as written, and with symlinks and junctions followed (the hook's `resolvePath`). A hit on either reading asks (Claude) or blocks (Bob, OpenCode). A write through a link from outside a repo into one of its protected folders is now caught on OpenCode, where before it passed; a protected folder inside a repo that links out of it is now caught by the hook, where before it passed. Ordinary repos are unchanged. The guard's git readers are now pinned to `js/hosts/gitref.mjs` by a test.
|
|
26
|
+
- **The console's always-on cost no longer counts skills the model never sees.** The skill part of each install's cost counted every `SKILL.md`, including user-only skills and aliases the host keeps out of the model's catalogue. It now skips them the way each host does: on their `disable-model-invocation: true` frontmatter for Claude Code and OpenClaude, and on the `permission.skill` deny in `opencode.json(c)` for OpenCode, which ignores that frontmatter key. Bob writes no `SKILL.md` for a user-only skill, so nothing changes there. Measured on a fresh claude-global lean emit, the skill part drops from 12,982 to 10,837 characters (opencode-global: 12,548 to 11,825). The root-file ceiling is unaffected.
|
|
27
|
+
- **The console's Build no longer re-arms switched-off gates or restores excluded rules and skills.** Rebuilding a deployed install from the console (the theme picker) assembled its own argv: no gate switches, `--exclude-rules none` and no excluded skills. It now takes the argv the install's own profile gives `rebuild-all` and `status`, with only the picked theme swapped in. A global install's Build also names its own `--config-dir`, and a project install's its own `--out`/`--root`.
|
|
28
|
+
- **Re-running `geneseed setup` no longer re-arms switched-off gates.** The wizard kept an install's excluded rules and skills but read none of its `Secrets gate:` / `Done gate:` / `Deletion gate:` lines, so holding Enter through a re-run switched every gate back on. It now carries them into the build like the exclusions, without asking about them.
|
|
29
|
+
|
|
11
30
|
## [4.9.1] — 2026-10-18
|
|
12
31
|
|
|
13
32
|
Patch: Deletion Is Deliberate and the root-scan check no longer fail open behind shell structure; the console stops re-running the doctor on every change.
|
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.9.
|
|
23
|
+
> string. Current label: **4.9.2**.
|
|
24
24
|
|
|
25
25
|
## Capabilities
|
|
26
26
|
|
|
@@ -64,7 +64,9 @@ const PLUGIN_DIR = path.dirname(fileURLToPath(import.meta.url))
|
|
|
64
64
|
// Sovereign-repo bypass — twin of the harness's own `sovereignBypass()`.
|
|
65
65
|
// <cfg>/excludes.json (user-owned, managed by `harness exclude`) lists folders where
|
|
66
66
|
// this GLOBAL install goes dormant. Any error degrades to "not excluded".
|
|
67
|
-
|
|
67
|
+
// Twin of js/hosts/hosts.mjs `resolvePath`: the absolute path with symlinks and junctions
|
|
68
|
+
// followed, the way the Claude/Bob hook resolves one. `norm` folds its case for matching.
|
|
69
|
+
function resolvePath(p) {
|
|
68
70
|
// Expand a leading "~" / "~/…" to the home dir — a hand-edited excludes.json entry
|
|
69
71
|
// like "~/vault" must resolve the same way here as it does for the Claude/Bob
|
|
70
72
|
// hook guard.
|
|
@@ -83,6 +85,10 @@ const norm = (p) => {
|
|
|
83
85
|
tail.unshift(path.basename(cur))
|
|
84
86
|
cur = up
|
|
85
87
|
}
|
|
88
|
+
return s
|
|
89
|
+
}
|
|
90
|
+
const norm = (p) => {
|
|
91
|
+
const s = resolvePath(p)
|
|
86
92
|
return process.platform === "win32" ? s.toLowerCase() : s
|
|
87
93
|
}
|
|
88
94
|
// A linked git worktree of an excluded repo is excluded too, wherever it sits on disk — twin of
|
|
@@ -59,7 +59,9 @@ const PLUGIN_DIR = path.dirname(fileURLToPath(import.meta.url))
|
|
|
59
59
|
// Sovereign-repo bypass — twin of the harness's own `sovereignBypass()`.
|
|
60
60
|
// <cfg>/excludes.json (user-owned, managed by `harness exclude`) lists folders where
|
|
61
61
|
// this GLOBAL install goes dormant. Any error degrades to "not excluded".
|
|
62
|
-
|
|
62
|
+
// Twin of js/hosts/hosts.mjs `resolvePath`: the absolute path with symlinks and junctions
|
|
63
|
+
// followed, the way the Claude/Bob hook resolves one. `norm` folds its case for matching.
|
|
64
|
+
function resolvePath(p) {
|
|
63
65
|
// Expand a leading "~" / "~/…" to the home dir — a hand-edited excludes.json entry
|
|
64
66
|
// like "~/vault" must resolve the same way here as it does for the Claude/Bob
|
|
65
67
|
// hook guard.
|
|
@@ -78,6 +80,10 @@ const norm = (p) => {
|
|
|
78
80
|
tail.unshift(path.basename(cur))
|
|
79
81
|
cur = up
|
|
80
82
|
}
|
|
83
|
+
return s
|
|
84
|
+
}
|
|
85
|
+
const norm = (p) => {
|
|
86
|
+
const s = resolvePath(p)
|
|
81
87
|
return process.platform === "win32" ? s.toLowerCase() : s
|
|
82
88
|
}
|
|
83
89
|
// A linked git worktree of an excluded repo is excluded too, wherever it sits on disk — twin of
|
|
@@ -234,8 +240,9 @@ const SHELL_BLOCK_RE = [
|
|
|
234
240
|
// the Bob behaviour — and `GENESEED_GUARD=warn` downgrades it to a log line.
|
|
235
241
|
//
|
|
236
242
|
// The fs-only readers below are the twins of js/hosts/gitref.mjs (gitRootOf, gitDirOf,
|
|
237
|
-
// gitCommonDirOf, currentBranch, protectedBranches, stashExists)
|
|
238
|
-
//
|
|
243
|
+
// gitCommonDirOf, currentBranch, protectedBranches, stashExists) — tests/plugins/guard.test.mjs
|
|
244
|
+
// compares each with its gitref.mjs original, quotes, semicolons and layout aside; the block
|
|
245
|
+
// between the twin markers is js/hosts/loss.mjs verbatim, and the same file diffs it.
|
|
239
246
|
function gitRootOf(cwd) {
|
|
240
247
|
try {
|
|
241
248
|
let dir = path.resolve(cwd)
|
|
@@ -1178,19 +1185,20 @@ function ruleStoreTarget(p) {
|
|
|
1178
1185
|
// must not edit in `.geneseed/protected-checks.txt` at its git root, one repo-relative path per
|
|
1179
1186
|
// line; the list protects itself. Found from the WRITE TARGET up to the first `.git`, re-read on
|
|
1180
1187
|
// every write (a short file, read only on a write call). No ask tier here, so a hit is a deny.
|
|
1188
|
+
// The target is read twice — as written, and with links followed — and a hit on either is a hit:
|
|
1189
|
+
// a link from outside into a protected folder and a protected folder that links out are both
|
|
1190
|
+
// caught, so following links never loosens the gate.
|
|
1181
1191
|
const SENSOR_LIST = ".geneseed/protected-checks.txt"
|
|
1182
1192
|
// The done gate's list (#289, js/hosts/hooks-done.mjs) protects itself the same way, with or
|
|
1183
1193
|
// without a protected-checks list: emptying it would disarm the gate. Twin of js/hosts/hooks.mjs.
|
|
1184
1194
|
const DONE_LIST = ".geneseed/done-checks.txt"
|
|
1185
1195
|
function protectedCheck(p) {
|
|
1196
|
+
return protectedIn(path.resolve(p)) ?? protectedIn(resolvePath(p))
|
|
1197
|
+
}
|
|
1198
|
+
function protectedIn(target) {
|
|
1186
1199
|
try {
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1189
|
-
while (!existsSync(path.join(root, ".git"))) {
|
|
1190
|
-
const up = path.dirname(root)
|
|
1191
|
-
if (up === root) return null
|
|
1192
|
-
root = up
|
|
1193
|
-
}
|
|
1200
|
+
const root = gitRootOf(path.dirname(target))
|
|
1201
|
+
if (!root) return null
|
|
1194
1202
|
const fold = (s) => nocase(s.replace(/\\/g, "/"))
|
|
1195
1203
|
const rel = fold(path.relative(root, target))
|
|
1196
1204
|
if (rel === fold(DONE_LIST)) return DONE_LIST
|
|
@@ -46,7 +46,9 @@ const PLUGIN_DIR = path.dirname(fileURLToPath(import.meta.url))
|
|
|
46
46
|
// Sovereign-repo bypass — twin of the harness's own `sovereignBypass()`.
|
|
47
47
|
// <cfg>/excludes.json (user-owned, managed by `harness exclude`) lists folders where
|
|
48
48
|
// this GLOBAL install goes dormant. Any error degrades to "not excluded".
|
|
49
|
-
|
|
49
|
+
// Twin of js/hosts/hosts.mjs `resolvePath`: the absolute path with symlinks and junctions
|
|
50
|
+
// followed, the way the Claude/Bob hook resolves one. `norm` folds its case for matching.
|
|
51
|
+
function resolvePath(p) {
|
|
50
52
|
// Expand a leading "~" / "~/…" to the home dir — a hand-edited excludes.json entry
|
|
51
53
|
// like "~/vault" must resolve the same way here as it does for the Claude/Bob
|
|
52
54
|
// hook guard.
|
|
@@ -65,6 +67,10 @@ const norm = (p) => {
|
|
|
65
67
|
tail.unshift(path.basename(cur))
|
|
66
68
|
cur = up
|
|
67
69
|
}
|
|
70
|
+
return s
|
|
71
|
+
}
|
|
72
|
+
const norm = (p) => {
|
|
73
|
+
const s = resolvePath(p)
|
|
68
74
|
return process.platform === "win32" ? s.toLowerCase() : s
|
|
69
75
|
}
|
|
70
76
|
// A linked git worktree of an excluded repo is excluded too, wherever it sits on disk — twin of
|
package/docs/concepts/hooks.md
CHANGED
|
@@ -48,7 +48,7 @@ Geneseed writes its hooks into the host's settings file. Every hook calls one pr
|
|
|
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. An install built `--no-done-gate` wires `learn` with that flag: the check is skipped, learning still runs ([Exclusions](exclusions.md)).
|
|
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 reads the transcript from its end, back only to the prompt that opened the turn, so its cost does not grow with the session. 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
|
|
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.9.
|
|
5
|
+
"version": "4.9.2"
|
|
6
6
|
}
|
package/js/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `js/` — the module map
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Every module under `js/`, in eight folders. This page is the address book: **what each one owns**, and
|
|
4
4
|
**which file to open first** for a given task. It is not documentation of behaviour — every module
|
|
5
5
|
has a docblock for that, and the docblock is the thing to read before editing.
|
|
6
6
|
|
|
@@ -26,8 +26,28 @@ js/ui/ what it prints at a terminal
|
|
|
26
26
|
js/web/ the `geneseed web` console
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
Dependency direction is one-way
|
|
30
|
-
|
|
29
|
+
**Dependency direction is only partly one-way, and no test or doctor arm enforces any of it.**
|
|
30
|
+
`lib/` is the one true leaf: it imports only Node builtins and itself, and every other folder
|
|
31
|
+
imports it. A few modules sit just above it and are read from almost everywhere: `build/source.mjs`
|
|
32
|
+
(the source paths, the rule canon, `aliasesOf`), `hosts/hosts.mjs` (the host config dirs) and
|
|
33
|
+
`loop/score.mjs` (builtins only, by its docblock). Above those, the folders are domains, not
|
|
34
|
+
layers. These pairs import each other (relative imports between folders, measured 2026-10):
|
|
35
|
+
|
|
36
|
+
| pair | edges A→B / B→A | the smaller direction |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| build ↔ hosts | 24 / 5 | hosts reads `build/source.mjs` (`installs`, `link`, `native`, `opencode`) and `build/version.mjs` (`opencode`) |
|
|
39
|
+
| build ↔ inspect | 2 / 23 | `build/driver.mjs` records deploy roots in `inspect/registry.mjs`; `build/catalog.mjs` reads `inspect/inventory.mjs` |
|
|
40
|
+
| build ↔ loop | 4 / 1 | `loop/catalog.mjs` reads `build/source.mjs` |
|
|
41
|
+
| build ↔ ui | 2 / 1 | `ui/cli.mjs` reads `ROOT` from `build/source.mjs` |
|
|
42
|
+
| build ↔ web | 1 / 9 | `build/generate.mjs` lazily imports `web/daemon.mjs` to restart a stale console after `rebuild-all` |
|
|
43
|
+
| hosts ↔ inspect | 1 / 36 | `hosts/installs.mjs` reads `inspect/registry.mjs`'s deploy roots |
|
|
44
|
+
| ui ↔ web | 1 / 1 | `ui/menu.mjs` starts the daemon; `web/docs.mjs` renders `ui/cli.mjs`'s CLI reference |
|
|
45
|
+
| maintain ↔ web | 1 / 6 | `maintain/update.mjs` lazily imports `web/daemon.mjs` for the same restart |
|
|
46
|
+
|
|
47
|
+
The only gates over the import graph are the transitive closure walks in
|
|
48
|
+
`tests/unit/hook_cli.test.mjs`. They pin which modules the generator and CLI entries may pull
|
|
49
|
+
`child_process` into, not which folder imports which. Adding a cross-folder import is therefore a
|
|
50
|
+
review question, not a test failure.
|
|
31
51
|
|
|
32
52
|
---
|
|
33
53
|
|
|
@@ -108,7 +128,7 @@ Everything that inspects an already-built or already-deployed harness and report
|
|
|
108
128
|
| `checks-constitution.mjs` | The constitution: rule declarations and citations, LEAN blocks, `HOOK_PINNED`, theme vocabulary, the console's `LAW_META`/`DOCTRINE_META` |
|
|
109
129
|
| `checks-counts.mjs` | The hand-written counts: AGENT.md tables, README badges and prose, `SHIPPED.md`, held against `src/` |
|
|
110
130
|
| `scan.mjs` | The shared walk/sort/strip/test primitives every `checks-*` uses. Owns no judgement itself |
|
|
111
|
-
| `validate.mjs` | The `--validate-only`
|
|
131
|
+
| `validate.mjs` | The `validate` verb (the generator's old `--validate-only`): renders a sandbox, scans it for tokens and dead links, own exit contract |
|
|
112
132
|
| `inventory.mjs` | The one catalog walk and its taxonomy tables, read by status, the TUI and the web console |
|
|
113
133
|
| `status.mjs` | `geneseed status` and `version` — the panel data, counts, accent colour, version verdict |
|
|
114
134
|
| `cost.mjs` | What one install injects every session, by part (root file, skill and agent lists, session files), in characters; owns `CARRIER_CEILING`, which the smoke test gates and the console draws |
|
|
@@ -182,15 +202,16 @@ Verbs that change an install that already exists, as opposed to rendering one.
|
|
|
182
202
|
| module | owns |
|
|
183
203
|
|---|---|
|
|
184
204
|
| `update.mjs` | `upgrade`/`sync-self`/`bootstrap` — git pull, doctor gate, rebuild, each via a respawn |
|
|
185
|
-
| `setup.mjs` | The install wizard
|
|
205
|
+
| `setup.mjs` | The install wizard. Its prompts go through `js/lib/prompt.mjs`'s stdin readers |
|
|
186
206
|
| `uninstall.mjs` | Removes or disables an install: manifest-driven reversal, plus stash deactivate/reactivate |
|
|
187
207
|
| `migrate.mjs` | `geneseed migrate` — all-or-nothing re-emit moving installs onto the npm install shape |
|
|
188
208
|
| `memory.mjs` | `geneseed memory list|rm` — CLI-side fact removal (archive by default, `--purge` deletes), twin of the web delete endpoint |
|
|
189
209
|
|
|
190
210
|
**Before editing:** `update.mjs` is the one module that MUST spawn rather than import — the code on
|
|
191
211
|
disk is replaced mid-run, so an in-process call would validate the *old* source and then render it.
|
|
192
|
-
|
|
193
|
-
facts
|
|
212
|
+
The stdin readers are not here: they live in `js/lib/prompt.mjs`, shared by `setup.mjs`,
|
|
213
|
+
`uninstall.mjs` and `js/web/server.mjs`. `memory.mjs` deletes user facts: `rm` moves a fact into
|
|
214
|
+
`archive/` by default, but `--purge` unlinks it and nothing can bring it back.
|
|
194
215
|
|
|
195
216
|
## `js/ui/` — the terminal
|
|
196
217
|
|
|
@@ -243,9 +264,9 @@ and inline lists are held against a surface list written out in
|
|
|
243
264
|
| a law (invariant) | `src/laws/universal.md` — `### {{LAW:<id>}} <Principle>`; cite by id, never by number | `docs/extending.md` §2c. Author both LEAN halves: the lean footprint ships nothing else |
|
|
244
265
|
| to remove or retire a rule | `docs/extending.md` §2e | `RETIRED_RULE_IDS` in `js/build/source.mjs` — a retired id is never reused |
|
|
245
266
|
| a skill or an agent | `src/skills/<name>.md` or `src/agents/<name>.md` | `docs/extending.md` §3 — eight steps. Write the FILE before the table row or the build aborts |
|
|
246
|
-
| a doctor check | `js/inspect/checks-{build,repo,authoring}.mjs` |
|
|
267
|
+
| a doctor check | `js/inspect/checks-{build,repo,authoring,constitution,counts}.mjs`; `checks-probe.mjs` for a check that runs a live install's gates | wire it in one of two ways. Give it its own `ran(...)` group in `doctor.mjs`, as the module map and the host tables have. Or append it to `authoringProblems`, which is how the constitution and count checks run. Then add a PLANTED FAULT in `tests/unit/harness.test.mjs` (`tests/unit/doctor_probe.test.mjs` for the probe) |
|
|
247
268
|
| a web endpoint | GET → `js/web/api.mjs`; POST → `js/web/actions.mjs` | declare it in `js/web/routes.mjs`; the partition test probes the real handler |
|
|
248
|
-
| a CLI verb | `js/cli-table.json` — the table IS the parser | the `VERBS` row in `bin/geneseed-cli.mjs`, the row count in `cli_table.test.mjs`, `NATIVE` in
|
|
269
|
+
| a CLI verb | `js/cli-table.json` — the table IS the parser | the `VERBS` row in `bin/geneseed-cli.mjs`, the row count in `cli_table.test.mjs`, and, for a Node-native verb with no recorded cell, `NATIVE` in `tests/unit/hook_cli.test.mjs` |
|
|
249
270
|
| a hook verb | `js/hosts/hooks.mjs` (a gate) or its own `js/hosts/hooks-<verb>.mjs` | `claudeHookGroups` in `settings.mjs`, the `VERBS` row (with its `load`) in `bin/geneseed-hook.mjs`. Keep the import graph tiny |
|
|
250
271
|
| a console doc page | `docs/<understand|guides|concepts|reference>/<id>.md` — the stem is the id | `docs/extending.md` §4c. Only `kind: "concept"` gets `{N_*}` substitution |
|
|
251
272
|
| an OpenCode plugin | `adapters/opencode/plugins/geneseed-<name>.js` | `docs/extending.md` §4a — the `geneseed-` prefix is mechanically load-bearing |
|
package/js/build/args.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { parseArgs as nodeParseArgs } from 'node:util';
|
|
|
11
11
|
import { withPlatformNewlines } from '../lib/fs.mjs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import {
|
|
14
|
-
CONFIG, discoverNames, PACK_ORDER, knownRuleIds, knownSkillIds, linkedSkillIds, resolveSkillNames,
|
|
14
|
+
CONFIG, discoverNames, GATE_SWITCHES, PACK_ORDER, knownRuleIds, knownSkillIds, linkedSkillIds, resolveSkillNames,
|
|
15
15
|
} from './source.mjs';
|
|
16
16
|
// The loop engine's trust presets — the `--trust` choices. `score.mjs` imports nothing, which is
|
|
17
17
|
// what lets it sit inside this module's `child_process` ban.
|
|
@@ -101,11 +101,10 @@ const VALUED = {
|
|
|
101
101
|
};
|
|
102
102
|
const FLAGS = {
|
|
103
103
|
'--sync-themes': 'syncThemes', '--validate-only': 'validateOnly',
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
'--no-secrets-gate': 'noSecretsGate', '--no-done-gate': 'noDoneGate',
|
|
104
|
+
// The gate switches (`GATE_SWITCHES`, law order). No `harness.config.json` key feeds them,
|
|
105
|
+
// deliberately: a rebuild elides a flag when armed, so a config default could disarm an armed
|
|
106
|
+
// install on its next upgrade.
|
|
107
|
+
...Object.fromEntries(GATE_SWITCHES.map((g) => [g.flag, g.arg])),
|
|
109
108
|
'-v': 'verbose', '--verbose': 'verbose',
|
|
110
109
|
};
|
|
111
110
|
|
|
@@ -306,6 +305,8 @@ function usage() {
|
|
|
306
305
|
* this one) refuses the whole typed token, `-bogus`, once.
|
|
307
306
|
*/
|
|
308
307
|
export function parseArgs(argv, defaults) {
|
|
308
|
+
// Typed open: the gate rows are spread in from `GATE_SWITCHES`, and `cfgDir` is set only when given.
|
|
309
|
+
/** @type {Record<string, any>} */
|
|
309
310
|
const args = {
|
|
310
311
|
theme: defaults.theme, posture: defaults.posture, mode: defaults.mode,
|
|
311
312
|
trust: defaults.trust,
|
|
@@ -313,8 +314,8 @@ export function parseArgs(argv, defaults) {
|
|
|
313
314
|
excludeRules: defaults.excludeRules,
|
|
314
315
|
excludeSkills: defaults.excludeSkills ?? [],
|
|
315
316
|
out: 'Harness', emit: 'files', footprint: 'lean', root: null,
|
|
316
|
-
syncThemes: false, validateOnly: false, verbose: false,
|
|
317
|
-
|
|
317
|
+
syncThemes: false, validateOnly: false, verbose: false,
|
|
318
|
+
...Object.fromEntries(GATE_SWITCHES.map((g) => [g.arg, false])),
|
|
318
319
|
};
|
|
319
320
|
const { tokens } = nodeParseArgs({
|
|
320
321
|
args: argv, options: PARSE_OPTIONS, allowPositionals: true, strict: false, tokens: true,
|
package/js/build/driver.mjs
CHANGED
|
@@ -40,7 +40,7 @@ import { emitClaudeRender } from './emit-claude.mjs';
|
|
|
40
40
|
import { emitOpencodeRender, emitOpencodeGlobalRender } from './emit-opencode.mjs';
|
|
41
41
|
import { settingsIntegrityCheck } from '../hosts/settings.mjs';
|
|
42
42
|
import { hookRunnerEntry } from '../hosts/shim.mjs';
|
|
43
|
-
import { writeText, withPlatformNewlines, isFile } from '../lib/fs.mjs';
|
|
43
|
+
import { writeText, withPlatformNewlines, isFile, withWriteRecord } from '../lib/fs.mjs';
|
|
44
44
|
import { parseJson, jsonDumpsIndent } from '../lib/json.mjs';
|
|
45
45
|
import { relPosix } from '../lib/text.mjs';
|
|
46
46
|
// P5c moved these out of this file: `bin/geneseed-cli.mjs` needs the same four resolvers to
|
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
// status` renders to count, so `bin/geneseed-cli.mjs` needs the same checkout paths and the
|
|
59
59
|
// same cfg. golden.py's 259 cells all build one, which is what made the move safe.
|
|
60
60
|
import {
|
|
61
|
-
ROOT, makeCfg,
|
|
61
|
+
GATE_SWITCHES, ROOT, makeCfg,
|
|
62
62
|
} from './source.mjs';
|
|
63
63
|
|
|
64
64
|
// P5f moved these, for the same arithmetic a third time: `harness rebuild-all` reads the
|
|
@@ -75,8 +75,11 @@ import { configDefaults, die, parseArgs } from './args.mjs';
|
|
|
75
75
|
export { parseDriverArgs } from './args.mjs';
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
78
|
+
/**
|
|
79
|
+
* `_build_emit.PRIMARY_AGENT_SRC`. Exported for `diffCollect`'s render memo: a render input
|
|
80
|
+
* `sourceFingerprint` does not hash, so the memo keys on its bytes itself.
|
|
81
|
+
*/
|
|
82
|
+
export const PRIMARY_AGENT_SRC = path.join(ROOT, 'adapters', 'opencode', 'agents', 'orchestrator.md');
|
|
80
83
|
|
|
81
84
|
/** `_build_render.resolve_out` — absolute, or relative to the CURRENT WORKING DIRECTORY
|
|
82
85
|
* (not to ROOT), so the harness renders straight into any repository.
|
|
@@ -555,9 +558,9 @@ const claudeShaped = (name) => (cfg, args, out) => {
|
|
|
555
558
|
const claudeMd = path.join(configDir || carrierInLayer ? cfgDir : root, carrier);
|
|
556
559
|
const hookOpts = hookRunnerEntry();
|
|
557
560
|
before?.();
|
|
558
|
-
const r = emitClaudeCore(cfg, args, {
|
|
561
|
+
const r = withWriteRecord(() => emitClaudeCore(cfg, args, {
|
|
559
562
|
cfgDir, claudeMd, scope: configDir ? 'global' : 'project', host, out, hookOpts,
|
|
560
|
-
});
|
|
563
|
+
}));
|
|
561
564
|
process.stdout.write(summary(root ?? cfgDir, r));
|
|
562
565
|
return configDir ? cfgDir : undefined;
|
|
563
566
|
};
|
|
@@ -604,7 +607,7 @@ function writeMarkers(markerDir, emit, footprint) {
|
|
|
604
607
|
* but this argument would overwrite the live install `diff` is only reading.
|
|
605
608
|
*/
|
|
606
609
|
const GLOBAL_EMITS = {
|
|
607
|
-
opencode: emitOpencodeGlobal,
|
|
610
|
+
opencode: (...a) => withWriteRecord(() => emitOpencodeGlobal(...a)),
|
|
608
611
|
claude: claudeShaped('claude-global'),
|
|
609
612
|
bob: claudeShaped('bob-global'),
|
|
610
613
|
openclaude: claudeShaped('openclaude-global'),
|
|
@@ -633,7 +636,7 @@ const PROJECT_EMITS = {
|
|
|
633
636
|
// P2. `opencode` joins the three for `cmdValidate`, which has to be able to render EVERY
|
|
634
637
|
// `--emit` choice into its sandbox and not only the three doctor already scans. Its call
|
|
635
638
|
// shape is the Claude-shaped emits' exactly — `(cfg, args, out)` — so the row is the whole change.
|
|
636
|
-
opencode: emitOpencode,
|
|
639
|
+
opencode: (...a) => withWriteRecord(() => emitOpencode(...a)),
|
|
637
640
|
};
|
|
638
641
|
|
|
639
642
|
/**
|
|
@@ -648,24 +651,21 @@ const PROJECT_EMITS = {
|
|
|
648
651
|
* carry straight past the parameter default.
|
|
649
652
|
*/
|
|
650
653
|
function axesCfg({
|
|
651
|
-
posture, mode, trust, doctrines, excludeRules, excludeSkills,
|
|
652
|
-
deletionGate = null, secretsGate = null, doneGate = null,
|
|
654
|
+
posture, mode, trust, doctrines, excludeRules, excludeSkills, ...gates
|
|
653
655
|
}) {
|
|
654
656
|
return makeCfg({
|
|
655
657
|
...(posture ? { posture } : {}), ...(mode ? { mode } : {}), ...(trust ? { trust } : {}),
|
|
656
658
|
...(doctrines ? { doctrines } : {}), ...(excludeRules ? { excludeRules } : {}),
|
|
657
659
|
...(excludeSkills ? { excludeSkills } : {}),
|
|
658
|
-
//
|
|
659
|
-
|
|
660
|
-
...(
|
|
661
|
-
...(doneGate === false ? { noDoneGate: true } : {}),
|
|
660
|
+
// The gate switches (`GATE_SWITCHES` keys): only an explicit `false` disarms; `null` (no
|
|
661
|
+
// opinion) keeps the gate.
|
|
662
|
+
...Object.fromEntries(GATE_SWITCHES.filter((g) => gates[g.key] === false).map((g) => [g.arg, true])),
|
|
662
663
|
});
|
|
663
664
|
}
|
|
664
665
|
|
|
665
666
|
export function emitProjectInto(host, {
|
|
666
667
|
theme, out, root, footprint = 'full', posture = null, mode = null, trust = null,
|
|
667
|
-
doctrines = null, excludeRules = null, excludeSkills = null,
|
|
668
|
-
secretsGate = null, doneGate = null,
|
|
668
|
+
doctrines = null, excludeRules = null, excludeSkills = null, ...gates
|
|
669
669
|
}) {
|
|
670
670
|
const emit = PROJECT_EMITS[host];
|
|
671
671
|
// The axes ride the same rail into every `*Into`, and for the same reason: a drift reader
|
|
@@ -674,7 +674,7 @@ export function emitProjectInto(host, {
|
|
|
674
674
|
// checked an all-packs render nobody asked for.
|
|
675
675
|
return withPlatformNewlines(() => emit(
|
|
676
676
|
axesCfg({
|
|
677
|
-
posture, mode, trust, doctrines, excludeRules, excludeSkills,
|
|
677
|
+
posture, mode, trust, doctrines, excludeRules, excludeSkills, ...gates }),
|
|
678
678
|
{ theme, footprint, root }, out,
|
|
679
679
|
));
|
|
680
680
|
}
|
|
@@ -689,16 +689,16 @@ export function emitProjectInto(host, {
|
|
|
689
689
|
*/
|
|
690
690
|
export function buildInto({
|
|
691
691
|
theme, out, footprint = 'lean', posture = null, mode = null, trust = null, doctrines = null,
|
|
692
|
-
excludeRules = null, excludeSkills = null,
|
|
692
|
+
excludeRules = null, excludeSkills = null, ...gates
|
|
693
693
|
}) {
|
|
694
694
|
return withPlatformNewlines(() => build(axesCfg({
|
|
695
|
-
posture, mode, trust, doctrines, excludeRules, excludeSkills,
|
|
695
|
+
posture, mode, trust, doctrines, excludeRules, excludeSkills, ...gates }),
|
|
696
696
|
theme, out, { footprint, nativeCatalog: false }));
|
|
697
697
|
}
|
|
698
698
|
|
|
699
699
|
export function emitGlobalInto(host, {
|
|
700
700
|
theme, out, cfgDir, footprint, posture = null, mode = null, doctrines = null, trust = null,
|
|
701
|
-
excludeRules = null, excludeSkills = null,
|
|
701
|
+
excludeRules = null, excludeSkills = null, ...gates
|
|
702
702
|
}) {
|
|
703
703
|
// `build.HOSTS.get(host, build.HOSTS["opencode"])` — an unknown host falls back rather than
|
|
704
704
|
// raising, because the host comes from a marker file a user can edit.
|
|
@@ -715,7 +715,7 @@ export function emitGlobalInto(host, {
|
|
|
715
715
|
// the same scar the footprint left in `diffCollect`, one axis over each time.
|
|
716
716
|
return withPlatformNewlines(() => emit(
|
|
717
717
|
axesCfg({
|
|
718
|
-
posture, mode, trust, doctrines, excludeRules, excludeSkills,
|
|
718
|
+
posture, mode, trust, doctrines, excludeRules, excludeSkills, ...gates }),
|
|
719
719
|
{ theme, footprint, root: null, cfgDir }, out,
|
|
720
720
|
));
|
|
721
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
|
|
185
|
+
cfg.excludeRules, cfg);
|
|
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
|
-
|
|
219
|
+
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,
|
|
283
|
+
doctrines, excludeRules, host, cfgDir, gates,
|
|
284
284
|
);
|
|
285
285
|
managed.settings_hooks = managedHooks;
|
|
286
286
|
|
package/js/build/generate.mjs
CHANGED
|
@@ -141,8 +141,7 @@ export function cmdBuild(args) {
|
|
|
141
141
|
*/
|
|
142
142
|
export function setupBuildArgs(theme, emit, out = null, root = null, footprint = 'lean',
|
|
143
143
|
posture = 'peer', mode = 'direct', doctrines = null, allPacks = PACK_ORDER,
|
|
144
|
-
excludeRules = null, trust = DEFAULT_PRESET, excludeSkills = null,
|
|
145
|
-
{ secretsGate = true, doneGate = true } = {}) {
|
|
144
|
+
excludeRules = null, trust = DEFAULT_PRESET, excludeSkills = null, gates = {}) {
|
|
146
145
|
const argv = ['--theme', theme, '--emit', emit];
|
|
147
146
|
if (!emit.endsWith('-global')) {
|
|
148
147
|
if (out) argv.push('--out', out);
|
|
@@ -172,9 +171,9 @@ export function setupBuildArgs(theme, emit, out = null, root = null, footprint =
|
|
|
172
171
|
if (excludeSkills?.length) argv.push('--exclude-skills', excludeSkills.join(','));
|
|
173
172
|
// The gate switches elide when armed: the driver's default IS armed (no config key feeds
|
|
174
173
|
// them), so only an explicit `false` speaks, and every armed argv stays the bytes it was.
|
|
174
|
+
// `gates` is one object keyed as `GATE_SWITCHES` keys it (`gatesOfDir`, a profile, a body).
|
|
175
175
|
// Law order, so a Law IV-only install still ends on `--no-deletion-gate` as it did.
|
|
176
|
-
const
|
|
177
|
-
for (const g of GATE_SWITCHES) if (gates[g.key] === false) argv.push(g.flag);
|
|
176
|
+
for (const g of GATE_SWITCHES) if (gates?.[g.key] === false) argv.push(g.flag);
|
|
178
177
|
return argv;
|
|
179
178
|
}
|
|
180
179
|
|
|
@@ -238,7 +237,7 @@ export function installProfile(host, scope, root, notify = null) {
|
|
|
238
237
|
const gates = gatesOfDir(root, host);
|
|
239
238
|
const out = scope === 'global' ? null : root;
|
|
240
239
|
const argv = setupBuildArgs(theme, emit, out, out, footprint, posture, mode, doctrines,
|
|
241
|
-
PACK_ORDER, excludeRules, trust, excludeSkills, gates
|
|
240
|
+
PACK_ORDER, excludeRules, trust, excludeSkills, gates);
|
|
242
241
|
// A GLOBAL ROW NAMES ITS OWN DIR. `root` came from the host's config-dir resolver, which
|
|
243
242
|
// honours env overrides (`OPENCODE_CONFIG_DIR`, `CLAUDE_CONFIG_DIR`…); without the flag the
|
|
244
243
|
// same argv run from a shell lacking that variable re-emits into the DEFAULT dir — a second
|
package/js/build/source.mjs
CHANGED
|
@@ -385,6 +385,19 @@ export function resolveSkillNames(names, {
|
|
|
385
385
|
return { ids: [...new Set(ids)].sort(), notices, unknown };
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
+
/**
|
|
389
|
+
* The laws whose hook gate an install can switch off, in law order: the cfg/profile key, the
|
|
390
|
+
* build flag, the carrier marker (`<marker>: off`, written only when off) and the law. ONE
|
|
391
|
+
* table, so the flag, the marker, the rebuild argv, the read-back and `status` cannot drift
|
|
392
|
+
* apart. Every gate is ARMED by default and the law's text binds either way. The deletion row
|
|
393
|
+
* is #319's, unchanged: existing installs carry its flag and marker.
|
|
394
|
+
*/
|
|
395
|
+
export const GATE_SWITCHES = [
|
|
396
|
+
{ key: 'secretsGate', flag: '--no-secrets-gate', arg: 'noSecretsGate', marker: 'Secrets gate', law: 'I' },
|
|
397
|
+
{ key: 'doneGate', flag: '--no-done-gate', arg: 'noDoneGate', marker: 'Done gate', law: 'III' },
|
|
398
|
+
{ key: 'deletionGate', flag: '--no-deletion-gate', arg: 'noDeletionGate', marker: 'Deletion gate', law: 'IV' },
|
|
399
|
+
];
|
|
400
|
+
|
|
388
401
|
/**
|
|
389
402
|
* `_build_core.js_cfg()`, originated rather than received.
|
|
390
403
|
*
|
|
@@ -399,8 +412,7 @@ export function resolveSkillNames(names, {
|
|
|
399
412
|
*/
|
|
400
413
|
export function makeCfg({
|
|
401
414
|
posture = 'peer', mode = 'direct', trust = DEFAULT_PRESET, doctrines = PACK_ORDER,
|
|
402
|
-
excludeRules = [], excludeSkills = [],
|
|
403
|
-
noDoneGate = false,
|
|
415
|
+
excludeRules = [], excludeSkills = [], ...switches
|
|
404
416
|
} = {}) {
|
|
405
417
|
return {
|
|
406
418
|
root: ROOT,
|
|
@@ -428,25 +440,9 @@ export function makeCfg({
|
|
|
428
440
|
excludeRules: Array.isArray(excludeRules) ? [...excludeRules] : [],
|
|
429
441
|
// Skill names left out of this install — same "unspecified takes nothing away" default.
|
|
430
442
|
excludeSkills: Array.isArray(excludeSkills) ? [...excludeSkills] : [],
|
|
431
|
-
//
|
|
432
|
-
// only a literal `true` disarms, so every
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
// same contract (user decision 2026-10-18; see GATE_SWITCHES).
|
|
436
|
-
secretsGate: noSecretsGate !== true,
|
|
437
|
-
doneGate: noDoneGate !== true,
|
|
443
|
+
// The gate switches (`GATE_SWITCHES`: `secretsGate`, `doneGate`, `deletionGate`), each armed
|
|
444
|
+
// unless its `no*` arg says so in so many words: only a literal `true` disarms, so every
|
|
445
|
+
// caller that never heard of a switch keeps the gate.
|
|
446
|
+
...Object.fromEntries(GATE_SWITCHES.map((g) => [g.key, switches[g.arg] !== true])),
|
|
438
447
|
};
|
|
439
448
|
}
|
|
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
|
-
];
|
package/js/hosts/gitref.mjs
CHANGED
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
* `child_process` ban like every other closure that feeds `bin/geneseed-cli.mjs` — may ever
|
|
13
13
|
* import. A module that is fs/path only is importable from anywhere.
|
|
14
14
|
*
|
|
15
|
-
* The OpenCode guard plugin
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* The OpenCode guard plugin (`adapters/opencode/plugins/geneseed-guard.js`) is copied whole
|
|
16
|
+
* into an install and cannot import this, so it carries twins of `gitRootOf`, `gitDirOf`,
|
|
17
|
+
* `gitCommonDirOf`, `currentBranch`, `protectedBranches` and `stashExists` for its Law IV and
|
|
18
|
+
* protected-checks gates. `tests/plugins/guard.test.mjs` compares each with the original here;
|
|
19
|
+
* change one, change both.
|
|
18
20
|
*/
|
|
19
21
|
import { existsSync, statSync, lstatSync, readFileSync, readdirSync, openSync, readSync, closeSync } from 'node:fs';
|
|
20
22
|
import path from 'node:path';
|