geneseed 4.6.0 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +1 -1
- package/SHIPPED.md +1 -1
- package/adapters/claude-code/README.md +3 -1
- package/adapters/opencode/HOW-OPENCODE-LOADS.md +1 -1
- package/adapters/opencode/README.md +5 -4
- package/adapters/opencode/plugins/geneseed-guard.js +565 -37
- package/bin/geneseed-hook.mjs +9 -3
- package/docs/concepts/enforcement-table.md +1 -1
- package/docs/concepts/exclusions.md +20 -1
- package/docs/concepts/hooks.md +11 -3
- package/docs/concepts/host-matrix.md +3 -3
- package/docs/reference/plugin-guard.md +3 -3
- package/docs/reference/troubleshoot-doctor.md +4 -2
- package/docs/understand/a-day.md +6 -6
- package/docs/understand/design-choices-gates.md +2 -2
- package/docs/understand/enforced-vs-asked.md +4 -4
- package/docs/understand/harness.md +1 -1
- package/docs/understand/machine-claude.md +1 -1
- package/docs/understand/machine-opencode.md +2 -2
- package/docs/web-ui.md +3 -1
- package/harness.config.json +1 -1
- package/js/README.md +2 -1
- package/js/build/args.mjs +4 -1
- package/js/build/driver.mjs +11 -7
- package/js/build/emit-claude.mjs +4 -3
- package/js/build/emit-opencode.mjs +2 -1
- package/js/build/generate.mjs +13 -4
- package/js/build/render.mjs +4 -0
- package/js/build/source.mjs +4 -1
- package/js/cli-table.json +45 -0
- package/js/hosts/gitref.mjs +47 -1
- package/js/hosts/hooks.mjs +19 -39
- package/js/hosts/installs.mjs +20 -0
- package/js/hosts/loss.mjs +522 -0
- package/js/hosts/settings.mjs +79 -75
- package/js/inspect/checks-probe.mjs +18 -6
- package/js/inspect/diff.mjs +4 -1
- package/js/inspect/status.mjs +3 -1
- package/js/inspect/validate.mjs +6 -4
- package/js/maintain/uninstall.mjs +5 -2
- package/js/maintain/update.mjs +1 -1
- package/js/web/actions.mjs +20 -3
- package/js/web/api.mjs +4 -2
- package/package.json +1 -1
- package/src/AGENT.md.tmpl +1 -1
- package/src/laws/universal.md +17 -17
- package/src/skills/worktree.md +1 -1
- package/web/dist/assets/{Activity-Dj0izJwj.js → Activity-B-XbKlzu.js} +1 -1
- package/web/dist/assets/{ActivityDetail-wjq5W02K.js → ActivityDetail-8oq0fycD.js} +1 -1
- package/web/dist/assets/{Dashboard-UMbgHbzF.js → Dashboard-C4EMz5mf.js} +2 -2
- package/web/dist/assets/{Docs-D3ABE_ds.js → Docs-mq4XDQK_.js} +1 -1
- package/web/dist/assets/FilterInput-Ikvi-pS9.js +1 -0
- package/web/dist/assets/{GreenhouseView-C238LWBD.js → GreenhouseView-D1WOtq3n.js} +1 -1
- package/web/dist/assets/{Installs-XSm0a_zQ.js → Installs-DrqPaLDM.js} +1 -1
- package/web/dist/assets/{JournalView-BUj4Ff-6.js → JournalView-BYpASU-L.js} +1 -1
- package/web/dist/assets/Laws-A3PFwiPD.js +3 -0
- package/web/dist/assets/{Library-CkaPVUYU.js → Library-BCaqPf61.js} +1 -1
- package/web/dist/assets/{Loops-CexwOARr.js → Loops-Bx3HnlUA.js} +1 -1
- package/web/dist/assets/{Markdown-DrXLM6TA.js → Markdown-BK38dn4x.js} +1 -1
- package/web/dist/assets/{OperatorHudView-D5qTQat0.js → OperatorHudView-ChQPM8JE.js} +1 -1
- package/web/dist/assets/{Personal-B_Vx9a7a.js → Personal-DELm61DF.js} +1 -1
- package/web/dist/assets/{Tabs-YZbGqWPW.js → Tabs-DcMq5IvC.js} +1 -1
- package/web/dist/assets/{activity-dcg5vF9j.js → activity-D4w9t8fE.js} +1 -1
- package/web/dist/assets/{drawable-BoQnwVpr.js → drawable-BI2_tF3o.js} +1 -1
- package/web/dist/assets/{index-BoG1M8Xy.css → index-CHpydQJg.css} +1 -1
- package/web/dist/assets/{index-BmbWeoTM.js → index-r2jRgf6l.js} +2 -2
- package/web/dist/assets/{react-DRhoX0kS.js → react-Ci4wQD4a.js} +2 -2
- package/web/dist/assets/{usePoll-BmR631BB.js → usePoll-BcywUo55.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/src/pages/Laws.jsx +53 -5
- package/web/dist/assets/FilterInput-BSfpStOZ.js +0 -1
- package/web/dist/assets/Laws-yGBJFKgY.js +0 -3
package/js/hosts/settings.mjs
CHANGED
|
@@ -84,55 +84,47 @@ const consentRuleOn = (d, excluded = []) =>
|
|
|
84
84
|
* doctrine section — and a boundary that keeps asking for a rule the install did not adopt is
|
|
85
85
|
* the one disagreement the tiering is not allowed to produce. (It does NOT follow that the
|
|
86
86
|
* string is gone from the bundle; see `claudeHookGroups` for what D5 ships anyway and why.)
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* an always-on invariant — not the process pack's. They mirror `DESTRUCTIVE_GIT_RE` in
|
|
90
|
-
* js/hosts/hooks.mjs, so Claude's git-gate and OpenCode's permission ask about the same acts;
|
|
91
|
-
* before, OpenCode only logged the last four. A glob is not a regex, so the mirror is the
|
|
92
|
-
* closest glob for each act rather than an exact copy: the `* ` forms catch a flag that comes
|
|
93
|
-
* after other words (`push origin -f`, `reset -q --hard`, `clean -d -f`) and `git push *+*`
|
|
94
|
-
* a `+refspec` force push. The regex stays the stricter reader — it also sees a flag cluster
|
|
95
|
-
* with `f` not first (`clean -d -xf`), which a glob cannot say without matching `--exclude=f`.
|
|
87
|
+
* The Law IV keys stay in EVERY build: they are Law IV's territory — an always-on invariant —
|
|
88
|
+
* not the process pack's.
|
|
96
89
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
90
|
+
* LAW IV HERE IS ONLY WHAT LOSES DATA IN EVERY STATE (user decision, 2026-10-17). The gate asks
|
|
91
|
+
* only when a command would really lose data, and whether it would — a dirty tree, an untracked
|
|
92
|
+
* file, an unpushed branch, a protected target, a stash — is STATE, which a glob cannot read. So
|
|
93
|
+
* the state-aware decisions moved into the OpenCode guard plugin (`lossReason`, the twin of
|
|
94
|
+
* js/hosts/loss.mjs, which BLOCKS a real loss because `tool.execute.before` has no ask tier), and
|
|
95
|
+
* this list keeps what loses data whatever the state: the protected-branch forms below, and the
|
|
96
|
+
* two acts that delete the recovery net, `reflog expire` and `gc --prune` (matched bare and with a word before it, since a `--prune` with no `=now`
|
|
97
|
+
* still eventually prunes). Every force-push, `reset --hard`, `clean -f`, `branch -D`,
|
|
98
|
+
* `checkout`/`switch` force, `stash drop`/`clear` and remote-delete glob this list used to carry
|
|
99
|
+
* was REMOVED, the way PR #313 removed `git worktree remove -f`: removal from this list also
|
|
100
|
+
* removes the keys from `OWNED_BASH`, so an `opencode.json` written before keeps them — neither
|
|
101
|
+
* taken back nor reported as stranded, since Geneseed no longer claims them. They still ask
|
|
102
|
+
* there (fail-safe, one prompt); the user deletes them by hand (the CHANGELOG upgrade note says
|
|
103
|
+
* which). The process-5 `git push*` ask still covers every push while that rule is on.
|
|
111
104
|
*
|
|
112
|
-
* `
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* `
|
|
117
|
-
*
|
|
118
|
-
* the
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* `
|
|
105
|
+
* `rm -rf *` WENT TOO (user ruling, 2026-10-17): a blanket ask on every recursive removal is the
|
|
106
|
+
* glob-cannot-read-state problem again — the guard's `lossReason` lets an `rm` of tracked,
|
|
107
|
+
* unmodified files through and blocks one git cannot restore, and its `SHELL_BLOCK_RE` still
|
|
108
|
+
* blocks `rm -rf /`, `~` and the drive-root forms outright. Same stranded-key handling as above:
|
|
109
|
+
* an existing `opencode.json` keeps its `rm -rf *` key, asking, until the user deletes it.
|
|
110
|
+
*
|
|
111
|
+
* PROTECTED BRANCHES BY NAME are the one state-free loss a glob CAN say: a force push to, or a
|
|
112
|
+
* delete of, `main`/`master` loses work whatever the tree holds. So `PROTECTED_GLOBS` asks for
|
|
113
|
+
* those forms even when the guard plugin is off or in `warn` mode (a remote's OTHER default
|
|
114
|
+
* branch needs `refs/remotes/<r>/HEAD`, which only the guard reads). Every key ENDS in the
|
|
115
|
+
* branch name — never in a literal `" *"`, which upstream's `Wildcard.match` makes optional —
|
|
116
|
+
* so `mainline` or `HEAD:loop/x` never match, and none matches a push that is not forced or a
|
|
117
|
+
* delete. They come last, so `findLast` lets them win over the loop chain's allows.
|
|
122
118
|
*/
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
'git worktree remove * -f*', 'git worktree remove * --force*',
|
|
133
|
-
'git reflog expire*', 'git gc --prune*', 'git gc * --prune*',
|
|
134
|
-
'git stash drop*', 'git stash clear*',
|
|
135
|
-
'git push *--delete*', 'git push * :*'];
|
|
119
|
+
const PROTECTED_GLOBS = ['main', 'master'].flatMap((b) => [
|
|
120
|
+
`git push --force* ${b}`, `git push * --force* ${b}`, `git push -f* ${b}`, `git push * -f* ${b}`,
|
|
121
|
+
`git push --force* *:${b}`, `git push * --force* *:${b}`, `git push -f* *:${b}`, `git push * -f* *:${b}`,
|
|
122
|
+
`git push * +${b}`, `git push * +*:${b}`, `git push * +*:refs/heads/${b}`,
|
|
123
|
+
`git push * :${b}`, `git push * :refs/heads/${b}`, `git push *--delete* ${b}`,
|
|
124
|
+
`git branch -D ${b}`, `git branch * -D ${b}`,
|
|
125
|
+
`git branch *--delete*--force* ${b}`, `git branch *--force*--delete* ${b}`,
|
|
126
|
+
]);
|
|
127
|
+
const LAW_IV_BASH = ['git reflog expire*', 'git gc --prune*', 'git gc * --prune*', ...PROTECTED_GLOBS];
|
|
136
128
|
|
|
137
129
|
/**
|
|
138
130
|
* User decision (2026-10-10): a loop runs on its own `loop/*` branch, never `main`/`master`, so
|
|
@@ -147,12 +139,13 @@ const LAW_IV_BASH = ['git push --force*', 'git push -f*', 'git push *--force*',
|
|
|
147
139
|
*
|
|
148
140
|
* ORDER IS LOAD-BEARING. `PUSH_CHAIN` (which `defaultPermission` and `OWNED_BASH` both read)
|
|
149
141
|
* puts these AFTER `git push*`'s ask (so a loop
|
|
150
|
-
* push is not swallowed by the blanket consent ask) and BEFORE `LAW_IV_BASH
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
142
|
+
* push is not swallowed by the blanket consent ask) and BEFORE `LAW_IV_BASH`. Never move this
|
|
143
|
+
* block to before `git push*` or after `LAW_IV_BASH` — either moves the `findLast` boundary the
|
|
144
|
+
* ordering above depends on. Since `LAW_IV_BASH` keeps no push glob (2026-10-17), a forced or
|
|
145
|
+
* `+refspec` push to `loop/*` is no longer re-asked by Law IV: `loop/*` is never a protected
|
|
146
|
+
* branch, so it loses nothing Law IV guards. The tiers below still ask for every 2+-token prefix
|
|
147
|
+
* (`-f origin` included), and `git push*` still asks for a `+HEAD:loop/x` or `:loop/x` form,
|
|
148
|
+
* which no allow key matches.
|
|
156
149
|
*/
|
|
157
150
|
const LOOP_PUSH_ALLOW = ['git push * HEAD:loop/*', 'git push * HEAD:refs/heads/loop/*'];
|
|
158
151
|
|
|
@@ -208,9 +201,9 @@ const LOOP_PUSH_ALLOW = ['git push * HEAD:loop/*', 'git push * HEAD:refs/heads/l
|
|
|
208
201
|
* as a legitimate bare remote) — indistinguishable from a real remote name by token count alone,
|
|
209
202
|
* and not a practical gap: a leading `-`/`--` token is parsed by git as an OPTION, never as the
|
|
210
203
|
* remote, so this exact shape either errors (no remote given) or reads the refspec text itself
|
|
211
|
-
* as the remote and fails to resolve it — it does not reach a working push to `main`.
|
|
212
|
-
*
|
|
213
|
-
*
|
|
204
|
+
* as the remote and fails to resolve it — it does not reach a working push to `main`. (Before
|
|
205
|
+
* 2026-10-17 `LAW_IV_BASH`'s force-push globs re-asked it too; they are gone, and the OpenCode
|
|
206
|
+
* guard's `lossReason` now judges a forced push by its real target.)
|
|
214
207
|
*/
|
|
215
208
|
const LOOP_PUSH_ASK_EXTRA = ['git push * * HEAD:loop/*', 'git push * * HEAD:refs/heads/loop/*'];
|
|
216
209
|
const LOOP_PUSH_ALLOW_FLAG = [
|
|
@@ -232,13 +225,18 @@ const PUSH_CHAIN = [
|
|
|
232
225
|
[LOOP_PUSH_ASK_FLAG_EXTRA, 'ask'], [LOOP_PUSH_ASK_TRAILING, 'ask'], [LAW_IV_BASH, 'ask'],
|
|
233
226
|
];
|
|
234
227
|
|
|
235
|
-
|
|
236
|
-
|
|
228
|
+
// `deletionGate === false` (`--no-deletion-gate`) drops the `LAW_IV_BASH` tier and nothing else;
|
|
229
|
+
// the keys stay OWNED, so an existing file reports them as stranded.
|
|
230
|
+
function defaultPermission(doctrines = null, excluded = [], deletionGate = true) {
|
|
231
|
+
const bash = {};
|
|
237
232
|
if (consentRuleOn(doctrines, excluded)) {
|
|
238
233
|
bash['git commit*'] = 'ask';
|
|
239
234
|
bash['git push*'] = 'ask';
|
|
240
235
|
}
|
|
241
|
-
for (const [keys, verdict] of PUSH_CHAIN)
|
|
236
|
+
for (const [keys, verdict] of PUSH_CHAIN) {
|
|
237
|
+
if (keys === LAW_IV_BASH && deletionGate === false) continue;
|
|
238
|
+
for (const k of keys) bash[k] = verdict;
|
|
239
|
+
}
|
|
242
240
|
return { bash };
|
|
243
241
|
}
|
|
244
242
|
|
|
@@ -249,7 +247,7 @@ function defaultPermission(doctrines = null, excluded = []) {
|
|
|
249
247
|
* has to be able to recognise a key that is Geneseed's business at all — to add it back when
|
|
250
248
|
* the pack returns, and to name it when this build no longer wants it but will not remove it.
|
|
251
249
|
*/
|
|
252
|
-
const OWNED_BASH = ['
|
|
250
|
+
const OWNED_BASH = ['git commit*', 'git push*',
|
|
253
251
|
...PUSH_CHAIN.flatMap(([keys]) => keys)];
|
|
254
252
|
|
|
255
253
|
/**
|
|
@@ -506,7 +504,8 @@ export function loadJsonObject(p, { strict = false } = {}) {
|
|
|
506
504
|
* `'add'`, which is what the boolean callers this replaced meant.
|
|
507
505
|
*/
|
|
508
506
|
function warnCommentedJsonc(target, agentPath, permission,
|
|
509
|
-
includeLsp = false, prefix = 'geneseed', doctrines = null, excluded = [], skillNames = []
|
|
507
|
+
includeLsp = false, prefix = 'geneseed', doctrines = null, excluded = [], skillNames = [],
|
|
508
|
+
deletionGate = true) {
|
|
510
509
|
process.stdout.write(`[${prefix}] ${path.basename(target)} has comments — not rewriting `
|
|
511
510
|
+ 'it (your edits are kept). Add this to its "instructions" array by hand:\n');
|
|
512
511
|
process.stdout.write(`[${prefix}] ${jsonDumps(agentPath)}\n`);
|
|
@@ -515,7 +514,7 @@ function warnCommentedJsonc(target, agentPath, permission,
|
|
|
515
514
|
? `[${prefix}] and its "permission".bash is missing gates this build wires — `
|
|
516
515
|
+ 'reconcile it by hand against:\n'
|
|
517
516
|
: `[${prefix}] and, for Geneseed's default ask-gates, a "permission" key:\n`);
|
|
518
|
-
for (const line of jsonDumpsIndent(defaultPermission(doctrines, excluded)).split('\n')) {
|
|
517
|
+
for (const line of jsonDumpsIndent(defaultPermission(doctrines, excluded, deletionGate)).split('\n')) {
|
|
519
518
|
process.stdout.write(`[${prefix}] ${line}\n`);
|
|
520
519
|
}
|
|
521
520
|
if (skillNames.length) {
|
|
@@ -566,7 +565,7 @@ export function atomicWriteJson(p, config) {
|
|
|
566
565
|
* every path that wrote nothing — unreadable, not an object, commented `.jsonc`, write
|
|
567
566
|
* failure — so the caller keeps its previous record rather than claiming denies never written. */
|
|
568
567
|
export function mergeOpencodeJson(p, agentPath, doctrines = null, excluded = [], skillNames = [],
|
|
569
|
-
staleSkillNames = [], result = {}) {
|
|
568
|
+
staleSkillNames = [], result = {}, deletionGate = true) {
|
|
570
569
|
const target = opencodeTarget(p);
|
|
571
570
|
let config = { $schema: OPENCODE_SCHEMA, instructions: [] };
|
|
572
571
|
const { state, data, hadComments, error } = loadJsonObject(target);
|
|
@@ -601,7 +600,7 @@ export function mergeOpencodeJson(p, agentPath, doctrines = null, excluded = [],
|
|
|
601
600
|
let permIsObject = true;
|
|
602
601
|
const added = [];
|
|
603
602
|
if (addPerm) {
|
|
604
|
-
config.permission = defaultPermission(doctrines, excluded);
|
|
603
|
+
config.permission = defaultPermission(doctrines, excluded, deletionGate);
|
|
605
604
|
if (skillNames.length) {
|
|
606
605
|
config.permission.skill = defaultSkillPermission(skillNames);
|
|
607
606
|
added.push(...skillNames);
|
|
@@ -610,7 +609,7 @@ export function mergeOpencodeJson(p, agentPath, doctrines = null, excluded = [],
|
|
|
610
609
|
const perm = get(config, 'permission');
|
|
611
610
|
permIsObject = isDict(perm);
|
|
612
611
|
[permChanged, stranded, opaque] = reconcileOpencodePermission(perm,
|
|
613
|
-
defaultPermission(doctrines, excluded));
|
|
612
|
+
defaultPermission(doctrines, excluded, deletionGate));
|
|
614
613
|
// Independent of the `bash` shape: a `permission.bash` the build cannot wire into must
|
|
615
614
|
// not also block the UNRELATED `permission.skill` key from being reconciled.
|
|
616
615
|
if (permIsObject) {
|
|
@@ -630,7 +629,7 @@ export function mergeOpencodeJson(p, agentPath, doctrines = null, excluded = [],
|
|
|
630
629
|
// skill reconcile above never ran either — the unwired skill denies are folded into THIS
|
|
631
630
|
// one WARN rather than going unmentioned (a dedicated `skillOpaque` needs `perm` to be a
|
|
632
631
|
// dict to even look at `perm.skill`, which this case by definition is not).
|
|
633
|
-
const wanted = defaultPermission(doctrines, excluded).bash;
|
|
632
|
+
const wanted = defaultPermission(doctrines, excluded, deletionGate).bash;
|
|
634
633
|
const names = [...OWNED_BASH.filter((k) => has(wanted, k)),
|
|
635
634
|
...(!permIsObject ? skillNames : [])];
|
|
636
635
|
process.stderr.write(`[geneseed] WARN: "permission" in ${path.basename(target)} `
|
|
@@ -650,9 +649,9 @@ export function mergeOpencodeJson(p, agentPath, doctrines = null, excluded = [],
|
|
|
650
649
|
if (stranded.length) {
|
|
651
650
|
process.stderr.write(`[geneseed] WARN: ${path.basename(target)} keeps `
|
|
652
651
|
+ `${stranded.map((k) => jsonDumps(k)).join(', ')} under "permission".bash — the `
|
|
653
|
-
+ '
|
|
654
|
-
+ 'own entry from one you wrote, so it removes neither. Delete
|
|
655
|
-
+ 'them gone.\n');
|
|
652
|
+
+ 'rule behind those gates (a doctrine, or the deletion gate) is off in this build, but '
|
|
653
|
+
+ 'Geneseed cannot tell its own entry from one you wrote, so it removes neither. Delete '
|
|
654
|
+
+ 'them by hand if you want them gone.\n');
|
|
656
655
|
}
|
|
657
656
|
const addLsp = !has(config, 'lsp');
|
|
658
657
|
if (addLsp) config.lsp = true;
|
|
@@ -673,7 +672,7 @@ export function mergeOpencodeJson(p, agentPath, doctrines = null, excluded = [],
|
|
|
673
672
|
// block to paste, split by which of the two situations the reader is actually in. It is
|
|
674
673
|
// stdout, not stderr, because on this path the paste-in IS the output.
|
|
675
674
|
warnCommentedJsonc(target, agentPath, addPerm ? 'add' : (permChanged && 'reconcile'),
|
|
676
|
-
addLsp, 'geneseed', doctrines, excluded, skillNames);
|
|
675
|
+
addLsp, 'geneseed', doctrines, excluded, skillNames, deletionGate);
|
|
677
676
|
return target;
|
|
678
677
|
}
|
|
679
678
|
try {
|
|
@@ -726,7 +725,8 @@ export function mergeOpencodeJson(p, agentPath, doctrines = null, excluded = [],
|
|
|
726
725
|
* belongs in — the user's call — not how work is run, so it is not the process pack's to
|
|
727
726
|
* remove. `default` here means unknown, and unknown keeps the gate (see `processPackOn`).
|
|
728
727
|
*/
|
|
729
|
-
export function claudeHookGroups(cfg, hookOpts, doctrines = null, excluded = [], host = 'claude'
|
|
728
|
+
export function claudeHookGroups(cfg, hookOpts, doctrines = null, excluded = [], host = 'claude',
|
|
729
|
+
deletionGate = true) {
|
|
730
730
|
const run = hookPrefix(hookOpts);
|
|
731
731
|
const mem = `--memory "${path.join(cfg, 'memory')}"`;
|
|
732
732
|
// --root carries the install's own dir so a GLOBAL hook can stand down when a live, wired
|
|
@@ -757,14 +757,17 @@ export function claudeHookGroups(cfg, hookOpts, doctrines = null, excluded = [],
|
|
|
757
757
|
// else. Unverified live: no Bob install on the authoring machine. Revisit a matcher only
|
|
758
758
|
// once a live Bob session can confirm both the current tool list and the regex dialect.
|
|
759
759
|
const b = ' --host bob';
|
|
760
|
+
// `--no-deletion-gate` only when the install switched Law IV's gate off, so an armed
|
|
761
|
+
// install's command stays byte-identical (and an older shim never sees the flag).
|
|
762
|
+
const dg = deletionGate === false ? ' --no-deletion-gate' : '';
|
|
760
763
|
return {
|
|
761
764
|
SessionStart: [{ hooks: [{ type: 'command', command: `${run} context --root "${cfg}"${b} || exit 0` }] }],
|
|
762
765
|
// One group, both gates: process 5's consent is a stderr line here (Bob has no ask
|
|
763
766
|
// tier), so the pack toggle that drops Claude's git-gate group has nothing to drop —
|
|
764
|
-
// `tool-gate` only ever EXITS 2 for Laws I and IV
|
|
767
|
+
// `tool-gate` only ever EXITS 2 for Laws I and IV (IV unless built `--no-deletion-gate`), and for a
|
|
765
768
|
// write under a project's own `.geneseed/protected-checks.txt` — the project opted in —
|
|
766
769
|
// and for a recursive scan rooted at a whole filesystem (ops 2, the root-scan check).
|
|
767
|
-
PreToolUse: [{ hooks: [{ type: 'command', command: `${run} tool-gate --root "${cfg}"${b}` }] }],
|
|
770
|
+
PreToolUse: [{ hooks: [{ type: 'command', command: `${run} tool-gate --root "${cfg}"${b}${dg}` }] }],
|
|
768
771
|
// Bob's Stop payload never carries `transcript_path` (I2) — `learn` reads `--host bob`
|
|
769
772
|
// and returns immediately rather than wasting a model call on the bare envelope. KEPT
|
|
770
773
|
// rather than DROPPED: the stand-down costs nothing (no transcript read, no spawn), and
|
|
@@ -781,7 +784,8 @@ export function claudeHookGroups(cfg, hookOpts, doctrines = null, excluded = [],
|
|
|
781
784
|
// `--host` on `git-gate` but not on `learn`, which then exits 1 under `|| exit 0` and skips.
|
|
782
785
|
const h = host === 'openclaude' ? ' --host openclaude' : '';
|
|
783
786
|
const context = `${run} context --root "${cfg}"${h} || exit 0`;
|
|
784
|
-
const gate = `${run} git-gate --root "${cfg}"${h}${consentRuleOn(doctrines, excluded) ? '' : ' --no-consent'}
|
|
787
|
+
const gate = `${run} git-gate --root "${cfg}"${h}${consentRuleOn(doctrines, excluded) ? '' : ' --no-consent'}`
|
|
788
|
+
+ (deletionGate === false ? ' --no-deletion-gate' : '');
|
|
785
789
|
const ruleGate = `${run} rule-gate --root "${cfg}"`;
|
|
786
790
|
const learn = `${run} learn ${mem}${h} || exit 0`;
|
|
787
791
|
// Claude Code only (hooks.md, v2.1.295+; Task 1): a `command` hook that can't start, times
|
|
@@ -920,14 +924,14 @@ function readSettingsForMerge(p, consequence) {
|
|
|
920
924
|
* global that never stood down for a project install.
|
|
921
925
|
*/
|
|
922
926
|
export function mergeClaudeSettings(p, priorHooks = null, hookOpts = {},
|
|
923
|
-
doctrines = null, excluded = [], host = 'claude', cfgDir = path.dirname(p)) {
|
|
927
|
+
doctrines = null, excluded = [], host = 'claude', cfgDir = path.dirname(p), deletionGate = true) {
|
|
924
928
|
const prior = (priorHooks || []).filter(isDict);
|
|
925
929
|
const loaded = readSettingsForMerge(p, 'Hooks were not wired.');
|
|
926
930
|
if (!loaded) return [p, prior];
|
|
927
931
|
const [config, hadComments] = loaded;
|
|
928
932
|
let hooks = get(config, 'hooks');
|
|
929
933
|
if (!isDict(hooks)) hooks = {};
|
|
930
|
-
const canonical = claudeHookGroups(cfgDir, hookOpts, doctrines, excluded, host);
|
|
934
|
+
const canonical = claudeHookGroups(cfgDir, hookOpts, doctrines, excluded, host, deletionGate);
|
|
931
935
|
const canonFlat = [];
|
|
932
936
|
for (const [event, groups] of Object.entries(canonical)) {
|
|
933
937
|
for (const g of groups) canonFlat.push({ event, group: g });
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* shim's paths and `hookShellProblems` infers shell drift — none of them observes a verdict. So
|
|
7
7
|
* this runs each live install's own gate command, AS ITS SETTINGS SPELL IT, through the shell
|
|
8
8
|
* the host would use, on two canned payloads: one Law IV call that every build must refuse
|
|
9
|
-
* (`git
|
|
9
|
+
* (`git reflog expire --expire=now --all`, Law IV in Bob's `BLOCK_RULES`) and one that must pass (`git status`).
|
|
10
|
+
* A gate wired `--no-deletion-gate` passes the Law IV call by design, so `badCommandFor` swaps in a
|
|
11
|
+
* root-wide scan (ops 2) it must still refuse — a disarmed install is not a gate that stopped working.
|
|
10
12
|
* Both directions are judged, because "allows everything" and "refuses everything" (the
|
|
11
13
|
* PowerShell-fallback case `hookShellProblems` can only infer) are both broken gates.
|
|
12
14
|
*
|
|
@@ -44,7 +46,16 @@ import { spawnSync } from 'node:child_process';
|
|
|
44
46
|
*/
|
|
45
47
|
const PROBED_VERB = { claude: 'git-gate', openclaude: 'git-gate', bob: 'tool-gate' };
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
// A Law IV act that loses data in EVERY state: since the gate asks only on real loss
|
|
50
|
+
// (2026-10-17), a state-dependent act such as `reset --hard` would pass on a clean tree.
|
|
51
|
+
export const BAD_COMMAND = 'git reflog expire --expire=now --all';
|
|
52
|
+
// An install built `--no-deletion-gate` passes BAD_COMMAND by design, so its gate is probed on
|
|
53
|
+
// what it must still refuse: a recursive scan of a whole filesystem (ops 2, in Bob's
|
|
54
|
+
// `BLOCK_RULES` too).
|
|
55
|
+
export const DISARMED_BAD_COMMAND = 'find / -name geneseed-probe';
|
|
56
|
+
/** The must-refuse payload for one gate command, as its own flags say it is wired. */
|
|
57
|
+
export const badCommandFor = (command) => (/ --no-deletion-gate(?:\s|$)/.test(command)
|
|
58
|
+
? DISARMED_BAD_COMMAND : BAD_COMMAND);
|
|
48
59
|
export const GOOD_COMMAND = 'git status';
|
|
49
60
|
|
|
50
61
|
const PROBE_TIMEOUT_MS = 20_000;
|
|
@@ -95,7 +106,7 @@ export function refused(host, r) {
|
|
|
95
106
|
* The verdict for one gate, from its two runs — null when the gate is sound, or the problem.
|
|
96
107
|
* Pure, so the whole verdict table is pinned without spawning.
|
|
97
108
|
*/
|
|
98
|
-
export function probeVerdict(host, bad, good) {
|
|
109
|
+
export function probeVerdict(host, bad, good, badCommand = BAD_COMMAND) {
|
|
99
110
|
for (const r of [bad, good]) {
|
|
100
111
|
if (r.status !== 0 && r.status !== 2) {
|
|
101
112
|
const why = r.error ? r.error : `exit ${r.status}`;
|
|
@@ -105,7 +116,7 @@ export function probeVerdict(host, bad, good) {
|
|
|
105
116
|
: ' — the host lets every call through while this lasts'}`;
|
|
106
117
|
}
|
|
107
118
|
}
|
|
108
|
-
if (!refused(host, bad)) return `registered but not refusing — \`${
|
|
119
|
+
if (!refused(host, bad)) return `registered but not refusing — \`${badCommand}\` went through`;
|
|
109
120
|
if (refused(host, good)) return `refuses everything — \`${GOOD_COMMAND}\` was refused too`;
|
|
110
121
|
return null;
|
|
111
122
|
}
|
|
@@ -159,9 +170,10 @@ export function gateProbe(targets = installTargets(), { platform = process.platf
|
|
|
159
170
|
}
|
|
160
171
|
const verdict = withTempDir((tmp) => {
|
|
161
172
|
const runEnv = { ...env, GENESEED_GATE_PROBE: '1', CLAUDE_PROJECT_DIR: tmp };
|
|
162
|
-
const
|
|
173
|
+
const badCommand = badCommandFor(handler.command);
|
|
174
|
+
const bad = runOnce(shell, probePayload(badCommand, tmp), tmp, runEnv);
|
|
163
175
|
const good = runOnce(shell, probePayload(GOOD_COMMAND, tmp), tmp, runEnv);
|
|
164
|
-
return probeVerdict(host, bad, good);
|
|
176
|
+
return probeVerdict(host, bad, good, badCommand);
|
|
165
177
|
}, 'geneseed-probe-');
|
|
166
178
|
if (verdict) problems.push(`${label} under ${shell.label} ${verdict}`);
|
|
167
179
|
else probed.push(`${path.normalize(cfg)} (${verb}, ${shell.label})`);
|
package/js/inspect/diff.mjs
CHANGED
|
@@ -44,7 +44,8 @@ import {
|
|
|
44
44
|
GLOBAL_MANIFEST, VERSION_MARKER, expanduser, opencodeConfigDir, resolvePath,
|
|
45
45
|
} from '../hosts/hosts.mjs';
|
|
46
46
|
import {
|
|
47
|
-
EMIT_HOST_SCOPE, defaultTheme, doctrinesForBuild, excludedRulesOfDir,
|
|
47
|
+
EMIT_HOST_SCOPE, defaultTheme, deletionGateOfDir, doctrinesForBuild, excludedRulesOfDir,
|
|
48
|
+
excludedSkillsOfDir,
|
|
48
49
|
footprintOfDir, modeOfDir, postureOfDir,
|
|
49
50
|
trustOfDir,
|
|
50
51
|
readJsonMaybe, readMaybe, themeOfDir,
|
|
@@ -183,6 +184,8 @@ export function diffCollect({ target = null, theme = null, emit = null, footprin
|
|
|
183
184
|
excludeRules: excludedRulesOfDir(dir),
|
|
184
185
|
// The excluded skills, or every skill the install left out reads as deleted.
|
|
185
186
|
excludeSkills: excludedSkillsOfDir(dir),
|
|
187
|
+
// Law IV's gate switch, or a disarmed install reads its `Deletion gate: off` line as drift.
|
|
188
|
+
deletionGate: deletionGateOfDir(dir),
|
|
186
189
|
}));
|
|
187
190
|
// The one place a render depends on WHERE it is installed: a global Bob emit writes each
|
|
188
191
|
// folder skill's absolute directory into its SKILL.md (see `writeNativeLayer`), so the
|
package/js/inspect/status.mjs
CHANGED
|
@@ -421,7 +421,9 @@ export function statusLines(d, color = false) {
|
|
|
421
421
|
const packs = p.doctrines.length ? p.doctrines.join(',') : 'no packs';
|
|
422
422
|
const excl = p.excludeRules?.length ? `excluding ${p.excludeRules.join(',')}` : 'no rules excluded';
|
|
423
423
|
rows.push([`${p.host}:${p.scope}`, [p.state, p.theme, p.footprint, `${p.posture}/${p.mode}/${p.trust}`,
|
|
424
|
-
packs, excl, ...(p.excludeSkills?.length ? [`skills off: ${p.excludeSkills.join(',')}`] : [])
|
|
424
|
+
packs, excl, ...(p.excludeSkills?.length ? [`skills off: ${p.excludeSkills.join(',')}`] : []),
|
|
425
|
+
// Elided when armed. Law IV's text still binds; only its tool-boundary gate is off.
|
|
426
|
+
...(p.deletionGate === false ? ['deletion gate: off (Law IV text still binds)'] : [])]
|
|
425
427
|
.join(` ${DOT} `) + where]);
|
|
426
428
|
}
|
|
427
429
|
|
package/js/inspect/validate.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import path from 'node:path';
|
|
|
10
10
|
import { buildInto, emitGlobalInto, emitProjectInto } from '../build/driver.mjs';
|
|
11
11
|
import { resolvePath } from '../hosts/hosts.mjs';
|
|
12
12
|
import {
|
|
13
|
-
doctrinesOfDir, excludedRulesOfDir, modeOfDir, postureOfDir,
|
|
13
|
+
deletionGateOfDir, doctrinesOfDir, excludedRulesOfDir, modeOfDir, postureOfDir,
|
|
14
14
|
} from '../hosts/installs.mjs';
|
|
15
15
|
import { validateIsVendored } from '../hosts/native.mjs';
|
|
16
16
|
import { printOut, printErr } from '../lib/fs.mjs';
|
|
@@ -33,7 +33,7 @@ export const VALIDATE_USAGE = [
|
|
|
33
33
|
'usage: geneseed validate [-h] [--theme THEME] [--emit EMIT] [--out OUT] [--root ROOT]',
|
|
34
34
|
' [--footprint {lean,full}] [--posture POSTURE] [--mode MODE]',
|
|
35
35
|
' [--trust TRUST] [--doctrines DOCTRINES] [--exclude-rules RULES]',
|
|
36
|
-
' [--exclude-skills SKILLS] [-v]',
|
|
36
|
+
' [--exclude-skills SKILLS] [--no-deletion-gate] [-v]',
|
|
37
37
|
'',
|
|
38
38
|
"Render and emit into a throwaway sandbox, run the doctor's checks, and write nothing real.",
|
|
39
39
|
"Takes the generator's render flags (see geneseed-build --help) except --sync-themes and",
|
|
@@ -128,7 +128,7 @@ export function cmdValidate(args) {
|
|
|
128
128
|
const opts = {
|
|
129
129
|
theme: args.theme, footprint: args.footprint, posture: args.posture, mode: args.mode,
|
|
130
130
|
trust: args.trust, doctrines: args.doctrines, excludeRules: args.excludeRules,
|
|
131
|
-
excludeSkills: args.excludeSkills,
|
|
131
|
+
excludeSkills: args.excludeSkills, deletionGate: args.noDeletionGate ? false : null,
|
|
132
132
|
};
|
|
133
133
|
let scanDirs;
|
|
134
134
|
try {
|
|
@@ -172,7 +172,9 @@ export function cmdValidate(args) {
|
|
|
172
172
|
const excluded = excludedRulesOfDir(at);
|
|
173
173
|
printOut(`[validate-only] rendered at posture=${postureOfDir(at) ?? '?'} `
|
|
174
174
|
+ `mode=${modeOfDir(at) ?? '?'} packs=${packs ? packs.join(',') || 'none' : '?'} `
|
|
175
|
-
+ `excluded=${excluded.map((id) => id.replace('.', ' ')).join(', ') || 'none'}
|
|
175
|
+
+ `excluded=${excluded.map((id) => id.replace('.', ' ')).join(', ') || 'none'}`
|
|
176
|
+
// Elided when armed, so an armed dry run prints the line it always did.
|
|
177
|
+
+ `${deletionGateOfDir(at) ? '' : ' deletion-gate=off'}\n`);
|
|
176
178
|
printOut(`[validate-only] would write ${written.length} file(s) under ${args.out}`
|
|
177
179
|
+ (args.root ? ` (root ${args.root})` : '')
|
|
178
180
|
+ ' — nothing was actually written (sandboxed).\n');
|
|
@@ -51,7 +51,7 @@ import { hookRunnerEntry } from '../hosts/shim.mjs';
|
|
|
51
51
|
import { confirm } from '../lib/prompt.mjs';
|
|
52
52
|
import {
|
|
53
53
|
claudeCfg, claudeReadManifest, doctrinesOfDir, EMIT_HOST_SCOPE, emitHostScopeOf,
|
|
54
|
-
excludedRulesOfDir, installKind, installState,
|
|
54
|
+
deletionGateOfDir, excludedRulesOfDir, installKind, installState,
|
|
55
55
|
registeredTargets,
|
|
56
56
|
} from '../hosts/installs.mjs';
|
|
57
57
|
import {
|
|
@@ -1173,8 +1173,11 @@ function remergeClaudeHooks(cfg, root = cfg, host = 'claude') {
|
|
|
1173
1173
|
const excluded = excludedRulesOfDir(root, host).length ? excludedRulesOfDir(root, host)
|
|
1174
1174
|
: excludedRulesOfDir(cfg, host);
|
|
1175
1175
|
// `host` reaches the group builder: a Bob reactivate must re-wire Gemini-named groups.
|
|
1176
|
+
// Law IV's gate switch, the same two-carrier read: off only when a carrier says so, so a
|
|
1177
|
+
// reactivate never disarms an install that did not ask for it.
|
|
1178
|
+
const deletionGate = deletionGateOfDir(root, host) && deletionGateOfDir(cfg, host);
|
|
1176
1179
|
const [, claims] = mergeClaudeSettings(settingsFile(cfg, managed),
|
|
1177
|
-
managed.settings_hooks ?? null, hookRunnerEntry(), doctrines, excluded, host, cfg);
|
|
1180
|
+
managed.settings_hooks ?? null, hookRunnerEntry(), doctrines, excluded, host, cfg, deletionGate);
|
|
1178
1181
|
// `and data` — a manifest that did not parse is not one to write back.
|
|
1179
1182
|
if (!deepEquals(claims, managed.settings_hooks ?? null) && Object.keys(data).length > 0) {
|
|
1180
1183
|
managed.settings_hooks = claims;
|
package/js/maintain/update.mjs
CHANGED
|
@@ -632,7 +632,7 @@ export async function installAxes(dir, { installProfile, setupBuildArgs }, notif
|
|
|
632
632
|
if (legacyProcessCarrierOfDir(String(dir), 'opencode')) return null;
|
|
633
633
|
const p = installProfile('opencode', 'global', String(dir), notify);
|
|
634
634
|
return setupBuildArgs('', 'files', null, null, p.footprint, p.posture, p.mode, p.doctrines,
|
|
635
|
-
undefined, p.excludeRules, p.trust, p.excludeSkills).slice(4);
|
|
635
|
+
undefined, p.excludeRules, p.trust, p.excludeSkills, p.deletionGate).slice(4);
|
|
636
636
|
}
|
|
637
637
|
|
|
638
638
|
/**
|
package/js/web/actions.mjs
CHANGED
|
@@ -46,7 +46,8 @@ import {
|
|
|
46
46
|
opencodeConfigDir, resolvePath,
|
|
47
47
|
} from '../hosts/hosts.mjs';
|
|
48
48
|
import {
|
|
49
|
-
EMIT_HOST_SCOPE, doctrinesForBuild, excludedRulesOfDir, excludedSkillsOfDir,
|
|
49
|
+
EMIT_HOST_SCOPE, deletionGateOfDir, doctrinesForBuild, excludedRulesOfDir, excludedSkillsOfDir,
|
|
50
|
+
footprintOfDir,
|
|
50
51
|
installState, installTargets, legacyProcessCarrierOfDir, modeOfDir, postureOfDir, trustOfDir,
|
|
51
52
|
} from '../hosts/installs.mjs';
|
|
52
53
|
import {
|
|
@@ -129,6 +130,16 @@ function bodyExcludeRules(body) {
|
|
|
129
130
|
(n) => n.trim().replace(/[ \t]+/, '.'));
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Law IV's gate switch across the trust boundary: only a real JSON boolean counts. Anything
|
|
135
|
+
* else — a string `"false"`, a number, absence — is `null`, and the caller keeps the install's
|
|
136
|
+
* own answer, so a malformed body can never disarm the gate by accident.
|
|
137
|
+
*/
|
|
138
|
+
function bodyDeletionGate(body) {
|
|
139
|
+
const v = bget(body, 'deletionGate');
|
|
140
|
+
return typeof v === 'boolean' ? v : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
132
143
|
/**
|
|
133
144
|
* The five scalar axes of a build request — theme, footprint, posture, mode, trust — each
|
|
134
145
|
* taken from the body only when it names a value this checkout ships, else from `dflt`. The
|
|
@@ -490,6 +501,8 @@ export function apiRestore(state, files) {
|
|
|
490
501
|
// re-states a rule whose hook gate the install deliberately left unwired.
|
|
491
502
|
excludeRules: excludedRulesOfDir(target),
|
|
492
503
|
excludeSkills: excludedSkillsOfDir(target),
|
|
504
|
+
// Law IV's gate switch: a restored AGENT.md must keep the install's `Deletion gate:` line.
|
|
505
|
+
deletionGate: deletionGateOfDir(target),
|
|
493
506
|
}));
|
|
494
507
|
for (const raw of (isTruthy(files) ? files : [])) {
|
|
495
508
|
const rel = stripWhitespace(formatValue(raw).replace(/\\/g, '/')).replace(/^\/+/, '');
|
|
@@ -572,9 +585,12 @@ export function apiInstallCmd(state, body) {
|
|
|
572
585
|
// the silence a pre-2.3 carrier gives on packs. Unspecified therefore keeps the install's
|
|
573
586
|
// own answer, and a pre-marker install answers the empty list — which is what it has.
|
|
574
587
|
const excludeRules = bodyExcludeRules(body) ?? excludedRulesOfDir(root, host);
|
|
588
|
+
// Law IV's gate switch (the Constitution page's toggle): a strict boolean in the body, else
|
|
589
|
+
// the install's own answer — so a pack Apply, which sends none, keeps the switch as it is.
|
|
590
|
+
const deletionGate = bodyDeletionGate(body) ?? deletionGateOfDir(root, host);
|
|
575
591
|
const out = scope === 'global' ? null : String(root);
|
|
576
592
|
const argv = setupBuildArgs(theme || 'neutral', emit, out, out, fp, pos, mode, doctrines,
|
|
577
|
-
PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, host));
|
|
593
|
+
PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, host), deletionGate);
|
|
578
594
|
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'build-driver.mjs'), ...argv] };
|
|
579
595
|
}
|
|
580
596
|
|
|
@@ -673,8 +689,9 @@ export function apiDeployCmd(state, body) {
|
|
|
673
689
|
// and `PreToolUse::Bash` went with them. `doctrinesForBuild` resolves unknown to ALL packs.
|
|
674
690
|
const doctrines = bodyDoctrines(body) ?? doctrinesForBuild(root, host);
|
|
675
691
|
const excludeRules = bodyExcludeRules(body) ?? excludedRulesOfDir(root, host);
|
|
692
|
+
const deletionGate = bodyDeletionGate(body) ?? deletionGateOfDir(root, host);
|
|
676
693
|
// project-scope emit name == host name (opencode / claude / bob / openclaude)
|
|
677
694
|
const argv = setupBuildArgs(theme || 'neutral', host, root, root, fp, pos, mode, doctrines,
|
|
678
|
-
PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, host));
|
|
695
|
+
PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, host), deletionGate);
|
|
679
696
|
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'build-driver.mjs'), ...argv] };
|
|
680
697
|
}
|
package/js/web/api.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import { doctorCollect } from '../inspect/doctor.mjs';
|
|
|
20
20
|
import { excludesSnapshot } from '../inspect/excludes.mjs';
|
|
21
21
|
import { CLAUDE_STYLE, HOSTS, VERSION_MARKER, resolvePath } from '../hosts/hosts.mjs';
|
|
22
22
|
import {
|
|
23
|
-
footprintOfDir, installState, installTargets, modeOfDir, trustOfDir,
|
|
23
|
+
deletionGateOfDir, footprintOfDir, installState, installTargets, modeOfDir, trustOfDir,
|
|
24
24
|
postureOfDir, readJsonMaybe, themeOfDir,
|
|
25
25
|
} from '../hosts/installs.mjs';
|
|
26
26
|
import { PRESETS } from '../loop/score.mjs';
|
|
@@ -247,7 +247,9 @@ export function apiOverview(state) {
|
|
|
247
247
|
for (const [host, scope, root] of installTargets()) {
|
|
248
248
|
try {
|
|
249
249
|
if (samePath(viewCfg(host, scope, root), state.target)) {
|
|
250
|
-
|
|
250
|
+
// `deletionGate`: Law IV's gate switch, for the Constitution page's toggle on row IV.
|
|
251
|
+
install = { host, scope, path: root, footprint: footprintOfDir(root),
|
|
252
|
+
deletionGate: deletionGateOfDir(root, host) };
|
|
251
253
|
break;
|
|
252
254
|
}
|
|
253
255
|
} catch { /* an unreadable candidate just isn't a match */ }
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "Every field here is argued and gated in tests/unit/package_manifest.test.mjs. `files`, `bin` and `engines` each have a test that fails when reality drifts from this file; `version` mirrors harness.config.json and is gated too. Read that docblock before editing this one.",
|
|
3
3
|
"name": "geneseed",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.8.0",
|
|
5
5
|
"description": "A portable, theme-able harness you implant once and use everywhere to grow a disciplined AI coding agent.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"ai",
|
package/src/AGENT.md.tmpl
CHANGED
|
@@ -144,7 +144,7 @@ Inactive packs still ship under `{{DIR_DOCTRINES}}/`, so a citation into one res
|
|
|
144
144
|
|
|
145
145
|
{{DOCTRINES_BODY}}
|
|
146
146
|
|
|
147
|
-
Active packs: {{DOCTRINES_LIST}}{{EXCLUDED_RULES_LINE}}
|
|
147
|
+
Active packs: {{DOCTRINES_LIST}}{{EXCLUDED_RULES_LINE}}{{DELETION_GATE_LINE}}
|
|
148
148
|
|
|
149
149
|
---
|
|
150
150
|
|
package/src/laws/universal.md
CHANGED
|
@@ -69,24 +69,24 @@ governs the goal).
|
|
|
69
69
|
|
|
70
70
|
### {{LAW:deletion-is-deliberate}} Deletion Is Deliberate
|
|
71
71
|
<!-- LEAN:begin -->
|
|
72
|
-
|
|
73
|
-
outward-facing act
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
72
|
+
Real data loss and any irreversible, financial, privilege-changing or
|
|
73
|
+
outward-facing act (publishing, sending data to a third party) need explicit
|
|
74
|
+
confirmation bound to that act, or a durable authorization covering it; one
|
|
75
|
+
yes never stretches to the next. Real loss: deleting what version control
|
|
76
|
+
cannot restore (untracked, modified, unpushed or unmerged work), discarding
|
|
77
|
+
uncommitted changes, rewriting, deleting or force-moving main, master or the
|
|
78
|
+
default branch, or erasing reflog/gc data. Classify every action as Create,
|
|
79
|
+
Read, Update, or Delete and tier by reversibility: what git restores — an
|
|
80
|
+
edit, removing tracked clean files, force-pushing your own branch, removing a
|
|
81
|
+
worktree — runs freely.
|
|
82
82
|
<!-- LEAN:else -->
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
83
|
+
Real data loss — deleting untracked, modified, unpushed, unmerged work,
|
|
84
|
+
discarding uncommitted work, deleting or force-moving main, master or the
|
|
85
|
+
default branch, pruning reflog/gc — and any irreversible, financial,
|
|
86
|
+
privilege-changing or outward-facing act need explicit confirmation bound to
|
|
87
|
+
that act, or a durable authorization covering it; one yes never stretches to
|
|
88
|
+
the next. What git restores runs freely: tracked clean files, a worktree,
|
|
89
|
+
force-pushing your own branch.
|
|
90
90
|
<!-- LEAN:end -->
|
|
91
91
|
|
|
92
92
|
### {{LAW:surface-failures}} Surface Failures
|