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/cli-table.json
CHANGED
|
@@ -237,6 +237,21 @@
|
|
|
237
237
|
"is_flag": false,
|
|
238
238
|
"type": null,
|
|
239
239
|
"hidden": false
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"names": [
|
|
243
|
+
"--no-deletion-gate"
|
|
244
|
+
],
|
|
245
|
+
"dest": "no_deletion_gate",
|
|
246
|
+
"metavar": null,
|
|
247
|
+
"help": "switch Law IV's deletion gate off for this install: the real-loss checks stop asking (Deletion Is Deliberate's text still binds); written to the carrier as 'Deletion gate: off' and replayed by rebuilds",
|
|
248
|
+
"choices": null,
|
|
249
|
+
"default": false,
|
|
250
|
+
"required": false,
|
|
251
|
+
"nargs": "0",
|
|
252
|
+
"is_flag": true,
|
|
253
|
+
"type": null,
|
|
254
|
+
"hidden": false
|
|
240
255
|
}
|
|
241
256
|
],
|
|
242
257
|
"mutex": []
|
|
@@ -581,6 +596,21 @@
|
|
|
581
596
|
"is_flag": true,
|
|
582
597
|
"type": null,
|
|
583
598
|
"hidden": false
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
"names": [
|
|
602
|
+
"--no-deletion-gate"
|
|
603
|
+
],
|
|
604
|
+
"dest": "no_deletion_gate",
|
|
605
|
+
"metavar": null,
|
|
606
|
+
"help": "skip the Deletion Is Deliberate real-loss check (set by the emit when the install was built --no-deletion-gate); the root-scan and consent asks still run",
|
|
607
|
+
"choices": null,
|
|
608
|
+
"default": false,
|
|
609
|
+
"required": false,
|
|
610
|
+
"nargs": "0",
|
|
611
|
+
"is_flag": true,
|
|
612
|
+
"type": null,
|
|
613
|
+
"hidden": false
|
|
584
614
|
}
|
|
585
615
|
],
|
|
586
616
|
"mutex": []
|
|
@@ -1382,6 +1412,21 @@
|
|
|
1382
1412
|
"is_flag": false,
|
|
1383
1413
|
"type": null,
|
|
1384
1414
|
"hidden": false
|
|
1415
|
+
},
|
|
1416
|
+
{
|
|
1417
|
+
"names": [
|
|
1418
|
+
"--no-deletion-gate"
|
|
1419
|
+
],
|
|
1420
|
+
"dest": "no_deletion_gate",
|
|
1421
|
+
"metavar": null,
|
|
1422
|
+
"help": "skip the Deletion Is Deliberate real-loss check (set by the emit when the install was built --no-deletion-gate); Sealed Secrets, the root-scan check and the protected checks still refuse",
|
|
1423
|
+
"choices": null,
|
|
1424
|
+
"default": false,
|
|
1425
|
+
"required": false,
|
|
1426
|
+
"nargs": "0",
|
|
1427
|
+
"is_flag": true,
|
|
1428
|
+
"type": null,
|
|
1429
|
+
"hidden": false
|
|
1385
1430
|
}
|
|
1386
1431
|
],
|
|
1387
1432
|
"mutex": []
|
package/js/hosts/gitref.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* loop exemption; upstream never calls that hook, so the twin was deleted (OpenCode verdict
|
|
17
17
|
* I-2). This module has no copy to keep in step.
|
|
18
18
|
*/
|
|
19
|
-
import { existsSync, statSync, lstatSync, readFileSync, openSync, readSync, closeSync } from 'node:fs';
|
|
19
|
+
import { existsSync, statSync, lstatSync, readFileSync, readdirSync, openSync, readSync, closeSync } from 'node:fs';
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
|
|
22
22
|
/**
|
|
@@ -94,6 +94,52 @@ export function currentBranch(cwd) {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* The SHARED `.git` directory for `root`: the gitdir itself in a main worktree, the one its
|
|
99
|
+
* `commondir` names in a linked worktree. Refs under `refs/remotes/`, `refs/stash` and
|
|
100
|
+
* `packed-refs` live there, not in a worktree's private gitdir. Throws like `gitDirOf`.
|
|
101
|
+
*/
|
|
102
|
+
export function gitCommonDirOf(root) {
|
|
103
|
+
const gitdir = gitDirOf(root);
|
|
104
|
+
try {
|
|
105
|
+
return path.resolve(gitdir, readFileSync(path.join(gitdir, 'commondir'), 'utf8').trim());
|
|
106
|
+
} catch {
|
|
107
|
+
return gitdir;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The branch names Deletion Is Deliberate treats as protected in `root`'s repository: `main`,
|
|
113
|
+
* `master`, and every remote's default branch — the target of each `refs/remotes/<r>/HEAD`
|
|
114
|
+
* symref (git writes those loose, never packed). A remote whose HEAD cannot be read simply adds
|
|
115
|
+
* nothing; `main`/`master` are always in. Throws when the gitdir itself cannot be found, so a
|
|
116
|
+
* caller can tell "no remote default" from "no repository".
|
|
117
|
+
*/
|
|
118
|
+
export function protectedBranches(root) {
|
|
119
|
+
const names = new Set(['main', 'master']);
|
|
120
|
+
const remotes = path.join(gitCommonDirOf(root), 'refs', 'remotes');
|
|
121
|
+
let dirs = [];
|
|
122
|
+
try { dirs = readdirSync(remotes); } catch { /* no remotes */ }
|
|
123
|
+
for (const r of dirs) {
|
|
124
|
+
try {
|
|
125
|
+
const m = /^ref:\s*refs\/remotes\/[^/]+\/(.+?)\s*$/m.exec(readFileSync(path.join(remotes, r, 'HEAD'), 'utf8'));
|
|
126
|
+
if (m) names.add(m[1]);
|
|
127
|
+
} catch { /* this remote has no HEAD symref */ }
|
|
128
|
+
}
|
|
129
|
+
return names;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Whether `root`'s repository holds a stash — `refs/stash` loose or in `packed-refs`. Throws like `gitDirOf`. */
|
|
133
|
+
export function stashExists(root) {
|
|
134
|
+
const common = gitCommonDirOf(root);
|
|
135
|
+
if (existsSync(path.join(common, 'refs', 'stash'))) return true;
|
|
136
|
+
try {
|
|
137
|
+
return /^[0-9a-f]+ refs\/stash$/m.test(readFileSync(path.join(common, 'packed-refs'), 'utf8'));
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
97
143
|
const LOOP_STATE_MARKER = '<!-- loop-state:begin -->';
|
|
98
144
|
|
|
99
145
|
/**
|
package/js/hosts/hooks.mjs
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
GATE_LEDGER, globalHookStandingDown, hookProjectDir, resolvePath, sovereignBypass,
|
|
59
59
|
} from './hosts.mjs';
|
|
60
60
|
import { currentBranch, gitRootOf, loopLaunched } from './gitref.mjs';
|
|
61
|
+
import { LOSS_CANDIDATE_RE, lossReason } from './loss.mjs';
|
|
61
62
|
import { selfAndParents, readStdin } from './hooks-prims.mjs';
|
|
62
63
|
import { recordUse } from './usage.mjs';
|
|
63
64
|
|
|
@@ -71,42 +72,13 @@ import { recordUse } from './usage.mjs';
|
|
|
71
72
|
// one line and a `commit` on the next are two commands, not one.
|
|
72
73
|
const GIT_GATE_RE = /\bgit\b[^\n]*\b(?:commit|push)\b/;
|
|
73
74
|
|
|
74
|
-
// Law IV's boundary half
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
// `
|
|
80
|
-
//
|
|
81
|
-
// A forced push is `--force*`, a `-f` cluster, or a `+refspec` (`push origin +main`) — the last
|
|
82
|
-
// is a force push with no flag at all. `push --force*` trips this gate rather than process 5's
|
|
83
|
-
// so the stronger reason is the one the user reads.
|
|
84
|
-
//
|
|
85
|
-
// B5 (claude-code.md / claude-verdict.md) found the long/modern spellings of the same acts
|
|
86
|
-
// still passed: `clean --force` (the long flag, not a `-f` cluster), `branch --delete --force`
|
|
87
|
-
// (the long form of `-D`, EITHER flag order — `--force --delete` is the same act; an un-forced
|
|
88
|
-
// `--delete` refuses on an unmerged branch exactly like `-d`, so it stays OUT; `-D --force`/
|
|
89
|
-
// `--delete -f`/`-d --force`/`-df` are the SAME act again and are a known ceiling this does not
|
|
90
|
-
// chase — add them if a probe ever shows one in real use), `checkout -f`/`--force` and
|
|
91
|
-
// `switch -f`/`--force`/`--discard-changes`
|
|
92
|
-
// (git's own `-h` lists `-f, --force` AND a separate `--discard-changes` for switch — both
|
|
93
|
-
// discard uncommitted work the same way `checkout --` does), `worktree remove --force`/`-f`
|
|
94
|
-
// (can discard an uncommitted worktree), `reflog expire` and `gc --prune=now` (delete the
|
|
95
|
-
// reflog/dangling-commit safety net recovery depends on), and `push :branch`/`push --delete`
|
|
96
|
-
// (deletes a remote branch — the colon form needs a space before the `:` so `push origin
|
|
97
|
-
// HEAD:main`, an ordinary refspec, is not caught).
|
|
98
|
-
//
|
|
99
|
-
// `restore` and `stash drop`/`stash clear` are carved OUT of the shared `\bgit\b[^\n]*\b(…)`
|
|
100
|
-
// wrapper above and anchored to the verb position instead (`git`, an optional `-C <path>`, then
|
|
101
|
-
// the verb immediately): every other arm here needs a flag that is vanishingly unlikely in a
|
|
102
|
-
// commit message or a filename, but "restore" needs no flag at all and "drop"/"clear" are
|
|
103
|
-
// ordinary English, so `git commit -m "restore working behavior"` or `git stash push -m "clear
|
|
104
|
-
// old state"` would otherwise trip Law IV on the MESSAGE TEXT, not the command. Anchoring to
|
|
105
|
-
// the verb position is also what lets `restore` ask for `--staged --worktree` together (that
|
|
106
|
-
// combination DOES discard working-tree changes, unlike `--staged` alone) while still deferring
|
|
107
|
-
// on `--staged` alone.
|
|
108
|
-
const DESTRUCTIVE_GIT_RE =
|
|
109
|
-
/\bgit\b[^\n]*\b(?:reset\b[^\n]*\s--hard\b|clean\b[^\n]*\s(?:-[a-zA-Z]*f|--force\b)|branch\b[^\n]*\s(?:-D\b|--delete\b[^\n]*--force\b|--force\b[^\n]*--delete\b)|checkout\s+--\s|checkout\b[^\n]*\s(?:-[a-zA-Z]*f\b|--force\b)|switch\b[^\n]*\s(?:-[a-zA-Z]*f\b|--force\b|--discard-changes\b)|worktree\b[^\n]*\bremove\b[^\n]*(?:-[a-zA-Z]*f\b|--force\b)|reflog\b[^\n]*\bexpire\b|gc\b[^\n]*--prune\b|push\b[^\n]*(?:\s--force|\s-[a-zA-Z]*f\b|\s\+\S|\s--delete\b|\s:\S))|\bgit(?:\s+-C\s+\S+)*\s+restore\b(?:(?![^\n]*--staged\b)|(?=[^\n]*--staged\b)(?=[^\n]*--worktree\b))|\bgit(?:\s+-C\s+\S+)*\s+stash\s+(?:drop|clear)\b/;
|
|
75
|
+
// Law IV's boundary half lives in `./loss.mjs`: it asks only when a command would really lose
|
|
76
|
+
// data — a protected branch deleted or force-pushed, an unpushed unmerged branch deleted,
|
|
77
|
+
// uncommitted work discarded on a tree that holds some, a stash dropped while one exists, the
|
|
78
|
+
// reflog or unreachable commits pruned, a removal of what git cannot restore — and fails closed
|
|
79
|
+
// when it cannot tell (user decision, 2026-10-17, replacing the form-only `DESTRUCTIVE_GIT_RE`).
|
|
80
|
+
// `LOSS_CANDIDATE_RE` is the cheap prefilter: an ordinary command fails it and never reaches the
|
|
81
|
+
// parse or the one `git` spawn behind it. `worktree remove --force` stays out, as before.
|
|
110
82
|
|
|
111
83
|
// `loopExempt` — a WHITELIST, not the blacklist this replaced. A false ask costs one prompt; a
|
|
112
84
|
// false allow publishes. So the exemption holds only when the ENTIRE command is built from
|
|
@@ -462,9 +434,17 @@ const rootArg = (t) => !/^["']/.test(t) && FS_ROOT_RE.test(t);
|
|
|
462
434
|
function gitDecide(args, payload) {
|
|
463
435
|
const command = ((payload && payload.tool_input) || {}).command;
|
|
464
436
|
if (typeof command !== 'string') return 0;
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
437
|
+
// `--no-deletion-gate`: the install switched Law IV's boundary off (user decision,
|
|
438
|
+
// 2026-10-17). Only this check stands down — ops 2 and process 5 below still run.
|
|
439
|
+
if (!args.noDeletionGate && LOSS_CANDIDATE_RE.test(command)) {
|
|
440
|
+
// A pass here FALLS THROUGH: a force push to a feature branch loses nothing, but it is still
|
|
441
|
+
// a push, and process 5 below still asks for it.
|
|
442
|
+
const loss = lossReason(scanSegments(command).map((seg) => seg.match(SCAN_TOKEN_RE) || []),
|
|
443
|
+
(payload && payload.cwd) || process.cwd());
|
|
444
|
+
if (loss) {
|
|
445
|
+
return ask(args, 'git-gate', 'law-4', `Geneseed (Deletion Is Deliberate) \u2014 ${loss}. `
|
|
446
|
+
+ 'Confirm this specific command, or leave the data in place.');
|
|
447
|
+
}
|
|
468
448
|
}
|
|
469
449
|
// Before `--no-consent`: that flag drops the process pack's rule, not this one.
|
|
470
450
|
const scan = rootScan(command);
|
package/js/hosts/installs.mjs
CHANGED
|
@@ -583,6 +583,26 @@ export function excludedSkillsOfDir(d, host = null, notify = null) {
|
|
|
583
583
|
}, [], host);
|
|
584
584
|
}
|
|
585
585
|
|
|
586
|
+
/** `Deletion gate: off` — written only when Law IV's gate was switched off. */
|
|
587
|
+
const DELETION_GATE_RE = /^Deletion gate:[ \t]*(.+?)[ \t]*$/m;
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Whether a DEPLOYED install keeps Law IV's deletion gate armed — `true` (armed) or `false`
|
|
591
|
+
* (built `--no-deletion-gate`). Read back so `upgrade`, `rebuild-all`, `diff` and the console
|
|
592
|
+
* re-render the same switch. FAILS CLOSED: a missing line, a missing carrier, or any value but
|
|
593
|
+
* exactly `off` reads as armed, because the line is only ever written when the gate is off and
|
|
594
|
+
* a gate is the thing a misread must never take away.
|
|
595
|
+
*/
|
|
596
|
+
export function deletionGateOfDir(d, host = null) {
|
|
597
|
+
return firstCarrier(d, (carrierPath) => {
|
|
598
|
+
const text = readMaybe(carrierPath);
|
|
599
|
+
if (text === null) return undefined;
|
|
600
|
+
const m = DELETION_GATE_RE.exec(text);
|
|
601
|
+
if (!m) return undefined;
|
|
602
|
+
return m[1] !== 'off';
|
|
603
|
+
}, true, host);
|
|
604
|
+
}
|
|
605
|
+
|
|
586
606
|
// ---- what host a deployed dir belongs to (`_harness_mcp`) ---------------------------------
|
|
587
607
|
|
|
588
608
|
/** `_harness_mcp._claude_read_manifest`. */
|