geneseed 4.7.0 → 4.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +1 -1
- package/SHIPPED.md +1 -1
- package/adapters/claude-code/README.md +5 -1
- package/adapters/opencode/HOW-OPENCODE-LOADS.md +1 -1
- package/adapters/opencode/README.md +5 -4
- package/adapters/opencode/plugins/geneseed-context.js +17 -1
- package/adapters/opencode/plugins/geneseed-guard.js +569 -38
- package/bin/geneseed-hook.mjs +16 -5
- package/docs/concepts/enforcement-table.md +3 -3
- package/docs/concepts/exclusions.md +37 -1
- package/docs/concepts/hooks.md +13 -5
- package/docs/concepts/host-matrix.md +3 -3
- package/docs/reference/plugin-context.md +1 -1
- package/docs/reference/plugin-guard.md +3 -3
- package/docs/reference/troubleshoot-doctor.md +5 -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 +7 -4
- package/harness.config.json +1 -1
- package/js/README.md +2 -1
- package/js/build/args.mjs +7 -1
- package/js/build/driver.mjs +19 -7
- package/js/build/emit-claude.mjs +4 -3
- package/js/build/emit-opencode.mjs +2 -1
- package/js/build/generate.mjs +18 -5
- package/js/build/render.mjs +7 -1
- package/js/build/source.mjs +22 -1
- package/js/cli-table.json +120 -0
- package/js/hosts/gitref.mjs +47 -1
- package/js/hosts/hooks-done.mjs +4 -0
- package/js/hosts/hooks-learn.mjs +3 -1
- package/js/hosts/hooks.mjs +22 -43
- package/js/hosts/installs.mjs +30 -1
- package/js/hosts/loss.mjs +522 -0
- package/js/hosts/settings.mjs +90 -80
- package/js/inspect/checks-probe.mjs +18 -6
- package/js/inspect/diff.mjs +4 -1
- package/js/inspect/status.mjs +5 -2
- package/js/inspect/validate.mjs +11 -4
- package/js/maintain/uninstall.mjs +7 -2
- package/js/maintain/update.mjs +1 -1
- package/js/web/actions.mjs +24 -3
- package/js/web/api.mjs +5 -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-DZ3O_DsJ.js} +1 -1
- package/web/dist/assets/{ActivityDetail-wjq5W02K.js → ActivityDetail-BQSWwoJ-.js} +1 -1
- package/web/dist/assets/Dashboard-COLskkOJ.js +2 -0
- package/web/dist/assets/Docs-DUxizutI.js +5 -0
- package/web/dist/assets/FilterInput-zLFXoSt6.js +1 -0
- package/web/dist/assets/GreenhouseView-BSzVX_l7.js +2 -0
- package/web/dist/assets/{Installs-XSm0a_zQ.js → Installs-Ck4YyEzs.js} +2 -2
- package/web/dist/assets/JournalView-BbbACsYK.js +1 -0
- package/web/dist/assets/Laws-BZ_M86r3.js +3 -0
- package/web/dist/assets/{Library-CkaPVUYU.js → Library-D_4LbUg2.js} +1 -1
- package/web/dist/assets/{Loops-CexwOARr.js → Loops-Bzb2Y96W.js} +2 -2
- package/web/dist/assets/{Markdown-DrXLM6TA.js → Markdown-iHvBrpRN.js} +1 -1
- package/web/dist/assets/{OperatorHudView-D5qTQat0.js → OperatorHudView-D-nzB6_r.js} +1 -1
- package/web/dist/assets/{Personal-B_Vx9a7a.js → Personal-C66284wd.js} +1 -1
- package/web/dist/assets/Tabs-DkPfjG3H.js +1 -0
- package/web/dist/assets/{activity-dcg5vF9j.js → activity-DvPxoS4Y.js} +1 -1
- package/web/dist/assets/drawable-DlrXDOJ7.js +1 -0
- package/web/dist/assets/{index-BoG1M8Xy.css → index-BCJz4_8C.css} +1 -1
- package/web/dist/assets/{index-BmbWeoTM.js → index-DJorAjeH.js} +2 -2
- package/web/dist/assets/{react-DRhoX0kS.js → react-BOF6yL40.js} +2 -2
- package/web/dist/assets/{sections-GalMS0Zv.js → sections-BET3c1WT.js} +1 -1
- package/web/dist/assets/{usePoll-BmR631BB.js → usePoll-NSTzLOF4.js} +1 -1
- package/web/dist/index.html +4 -4
- package/web/src/pages/Laws.jsx +84 -9
- package/web/dist/assets/Dashboard-UMbgHbzF.js +0 -2
- package/web/dist/assets/Docs-D3ABE_ds.js +0 -5
- package/web/dist/assets/FilterInput-BSfpStOZ.js +0 -1
- package/web/dist/assets/GreenhouseView-C238LWBD.js +0 -2
- package/web/dist/assets/JournalView-BUj4Ff-6.js +0 -1
- package/web/dist/assets/Laws-yGBJFKgY.js +0 -3
- package/web/dist/assets/Tabs-YZbGqWPW.js +0 -1
- package/web/dist/assets/drawable-BoQnwVpr.js +0 -1
package/js/build/generate.mjs
CHANGED
|
@@ -35,12 +35,12 @@ import { existsSync, mkdirSync, statSync } from 'node:fs';
|
|
|
35
35
|
import path from 'node:path';
|
|
36
36
|
|
|
37
37
|
import { main as driverMain } from './driver.mjs';
|
|
38
|
-
import { makeCfg, PACK_ORDER } from './source.mjs';
|
|
38
|
+
import { GATE_SWITCHES, makeCfg, PACK_ORDER } from './source.mjs';
|
|
39
39
|
import { DEFAULT_PRESET } from '../loop/score.mjs';
|
|
40
40
|
import { opencodeConfigDir, resolvePath } from '../hosts/hosts.mjs';
|
|
41
41
|
import {
|
|
42
42
|
EMIT_HOST_SCOPE, defaultMode, defaultPosture, defaultTheme, doctrinesForBuild,
|
|
43
|
-
excludedRulesOfDir, excludedSkillsOfDir, footprintOfDir, installState, installTargets,
|
|
43
|
+
excludedRulesOfDir, excludedSkillsOfDir, footprintOfDir, gatesOfDir, installState, installTargets,
|
|
44
44
|
legacyProcessCarrierOfDir, modeOfDir, postureOfDir, readMaybe, themeOfDir, trustOfDir,
|
|
45
45
|
} from '../hosts/installs.mjs';
|
|
46
46
|
import { colorThemeFiles, colorThemeJson, PALETTE_ROLES } from '../hosts/opencode.mjs';
|
|
@@ -102,6 +102,9 @@ const BUILD_FORWARD = [
|
|
|
102
102
|
export function cmdBuild(args) {
|
|
103
103
|
const argv = [];
|
|
104
104
|
for (const [key, flag] of BUILD_FORWARD) if (args[key] != null) argv.push(flag, args[key]);
|
|
105
|
+
// The bare switches `build` forwards (the gate switches): they take no value, so they are
|
|
106
|
+
// not BUILD_FORWARD rows.
|
|
107
|
+
for (const g of GATE_SWITCHES) if (args[g.arg]) argv.push(g.flag);
|
|
105
108
|
return driverMain(argv);
|
|
106
109
|
}
|
|
107
110
|
|
|
@@ -138,7 +141,8 @@ export function cmdBuild(args) {
|
|
|
138
141
|
*/
|
|
139
142
|
export function setupBuildArgs(theme, emit, out = null, root = null, footprint = 'lean',
|
|
140
143
|
posture = 'peer', mode = 'direct', doctrines = null, allPacks = PACK_ORDER,
|
|
141
|
-
excludeRules = null, trust = DEFAULT_PRESET, excludeSkills = null
|
|
144
|
+
excludeRules = null, trust = DEFAULT_PRESET, excludeSkills = null, deletionGate = true,
|
|
145
|
+
{ secretsGate = true, doneGate = true } = {}) {
|
|
142
146
|
const argv = ['--theme', theme, '--emit', emit];
|
|
143
147
|
if (!emit.endsWith('-global')) {
|
|
144
148
|
if (out) argv.push('--out', out);
|
|
@@ -166,6 +170,11 @@ export function setupBuildArgs(theme, emit, out = null, root = null, footprint =
|
|
|
166
170
|
// feeds it), so the flag's absence and `none` are the same answer, and every argv that
|
|
167
171
|
// excludes nothing stays the bytes it was before the flag existed.
|
|
168
172
|
if (excludeSkills?.length) argv.push('--exclude-skills', excludeSkills.join(','));
|
|
173
|
+
// The gate switches elide when armed: the driver's default IS armed (no config key feeds
|
|
174
|
+
// them), so only an explicit `false` speaks, and every armed argv stays the bytes it was.
|
|
175
|
+
// Law order, so a Law IV-only install still ends on `--no-deletion-gate` as it did.
|
|
176
|
+
const gates = { secretsGate, doneGate, deletionGate };
|
|
177
|
+
for (const g of GATE_SWITCHES) if (gates[g.key] === false) argv.push(g.flag);
|
|
169
178
|
return argv;
|
|
170
179
|
}
|
|
171
180
|
|
|
@@ -224,9 +233,12 @@ export function installProfile(host, scope, root, notify = null) {
|
|
|
224
233
|
const excludeRules = excludedRulesOfDir(root, host);
|
|
225
234
|
// Skills left out stay left out: an upgrade that forgot them would put them all back.
|
|
226
235
|
const excludeSkills = excludedSkillsOfDir(root, host, notify);
|
|
236
|
+
// The gate switches: a rebuild that forgot one would silently re-arm (or, read wrong, disarm)
|
|
237
|
+
// the install. A missing marker reads as armed.
|
|
238
|
+
const gates = gatesOfDir(root, host);
|
|
227
239
|
const out = scope === 'global' ? null : root;
|
|
228
240
|
const argv = setupBuildArgs(theme, emit, out, out, footprint, posture, mode, doctrines,
|
|
229
|
-
PACK_ORDER, excludeRules, trust, excludeSkills);
|
|
241
|
+
PACK_ORDER, excludeRules, trust, excludeSkills, gates.deletionGate, gates);
|
|
230
242
|
// A GLOBAL ROW NAMES ITS OWN DIR. `root` came from the host's config-dir resolver, which
|
|
231
243
|
// honours env overrides (`OPENCODE_CONFIG_DIR`, `CLAUDE_CONFIG_DIR`…); without the flag the
|
|
232
244
|
// same argv run from a shell lacking that variable re-emits into the DEFAULT dir — a second
|
|
@@ -234,7 +246,8 @@ export function installProfile(host, scope, root, notify = null) {
|
|
|
234
246
|
if (scope === 'global') argv.push('--config-dir', root);
|
|
235
247
|
return {
|
|
236
248
|
host, scope, root, state: installState(root, host, scope),
|
|
237
|
-
emit, theme, footprint, posture, mode, trust, doctrines, excludeRules, excludeSkills,
|
|
249
|
+
emit, theme, footprint, posture, mode, trust, doctrines, excludeRules, excludeSkills,
|
|
250
|
+
...gates, argv,
|
|
238
251
|
};
|
|
239
252
|
}
|
|
240
253
|
|
package/js/build/render.mjs
CHANGED
|
@@ -34,7 +34,7 @@ import { readText } from '../lib/fs.mjs';
|
|
|
34
34
|
import { parseJson } from '../lib/json.mjs';
|
|
35
35
|
import { normcase, comparePaths } from '../lib/paths.mjs';
|
|
36
36
|
// `js/build/source.mjs` imports nothing but node builtins, so this direction cannot cycle.
|
|
37
|
-
import { PACK_ORDER, readPackText, resolveRuleIds, ruleCanon } from './source.mjs';
|
|
37
|
+
import { GATE_SWITCHES, PACK_ORDER, readPackText, resolveRuleIds, ruleCanon } from './source.mjs';
|
|
38
38
|
import { DEFAULT_PRESET } from '../loop/score.mjs';
|
|
39
39
|
|
|
40
40
|
/** Document STRUCTURE is theme-INDEPENDENT — mirrors `_build_render.STRUCTURE`. */
|
|
@@ -444,6 +444,12 @@ export function effectiveTheme(cfg, themeName, { footprint = 'full', lawsPrefix
|
|
|
444
444
|
const skipped = [...(cfg.excludeSkills ?? [])].sort();
|
|
445
445
|
// It fills a template line of its own, so its value carries the blank line around it.
|
|
446
446
|
theme.EXCLUDED_SKILLS_LINE = skipped.length ? `\nExcluded skills: ${skipped.join(', ')}\n` : '';
|
|
447
|
+
// The gate switches (`GATE_SWITCHES`: Laws I, III, IV), under the same contract: a line is
|
|
448
|
+
// written only when its gate is OFF, so a default build stays byte-identical and a missing
|
|
449
|
+
// line reads back as armed (`gatesOfDir`). The OpenCode plugins read these lines at load, so
|
|
450
|
+
// their wording is a parsed contract.
|
|
451
|
+
theme.GATE_LINES = GATE_SWITCHES.filter((g) => cfg[g.key] === false)
|
|
452
|
+
.map((g) => `\n${g.marker}: off`).join('');
|
|
447
453
|
return theme;
|
|
448
454
|
}
|
|
449
455
|
|
package/js/build/source.mjs
CHANGED
|
@@ -399,7 +399,8 @@ export function resolveSkillNames(names, {
|
|
|
399
399
|
*/
|
|
400
400
|
export function makeCfg({
|
|
401
401
|
posture = 'peer', mode = 'direct', trust = DEFAULT_PRESET, doctrines = PACK_ORDER,
|
|
402
|
-
excludeRules = [], excludeSkills = [],
|
|
402
|
+
excludeRules = [], excludeSkills = [], noDeletionGate = false, noSecretsGate = false,
|
|
403
|
+
noDoneGate = false,
|
|
403
404
|
} = {}) {
|
|
404
405
|
return {
|
|
405
406
|
root: ROOT,
|
|
@@ -427,5 +428,25 @@ export function makeCfg({
|
|
|
427
428
|
excludeRules: Array.isArray(excludeRules) ? [...excludeRules] : [],
|
|
428
429
|
// Skill names left out of this install — same "unspecified takes nothing away" default.
|
|
429
430
|
excludeSkills: Array.isArray(excludeSkills) ? [...excludeSkills] : [],
|
|
431
|
+
// Law IV's deletion gate (`--no-deletion-gate`). Armed unless switched off in so many words:
|
|
432
|
+
// only a literal `true` disarms, so every caller that never heard of the switch keeps it.
|
|
433
|
+
deletionGate: noDeletionGate !== true,
|
|
434
|
+
// Law I's secret scan (`--no-secrets-gate`) and Law III's done gate (`--no-done-gate`), the
|
|
435
|
+
// same contract (user decision 2026-10-18; see GATE_SWITCHES).
|
|
436
|
+
secretsGate: noSecretsGate !== true,
|
|
437
|
+
doneGate: noDoneGate !== true,
|
|
430
438
|
};
|
|
431
439
|
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* The laws whose hook gate an install can switch off, in law order: the cfg/profile key, the
|
|
443
|
+
* build flag, the carrier marker (`<marker>: off`, written only when off) and the law. ONE
|
|
444
|
+
* table, so the flag, the marker, the rebuild argv, the read-back and `status` cannot drift
|
|
445
|
+
* apart. Every gate is ARMED by default and the law's text binds either way. The deletion row
|
|
446
|
+
* is #319's, unchanged: existing installs carry its flag and marker.
|
|
447
|
+
*/
|
|
448
|
+
export const GATE_SWITCHES = [
|
|
449
|
+
{ key: 'secretsGate', flag: '--no-secrets-gate', arg: 'noSecretsGate', marker: 'Secrets gate', law: 'I' },
|
|
450
|
+
{ key: 'doneGate', flag: '--no-done-gate', arg: 'noDoneGate', marker: 'Done gate', law: 'III' },
|
|
451
|
+
{ key: 'deletionGate', flag: '--no-deletion-gate', arg: 'noDeletionGate', marker: 'Deletion gate', law: 'IV' },
|
|
452
|
+
];
|
package/js/cli-table.json
CHANGED
|
@@ -237,6 +237,51 @@
|
|
|
237
237
|
"is_flag": false,
|
|
238
238
|
"type": null,
|
|
239
239
|
"hidden": false
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"names": [
|
|
243
|
+
"--no-secrets-gate"
|
|
244
|
+
],
|
|
245
|
+
"dest": "no_secrets_gate",
|
|
246
|
+
"metavar": null,
|
|
247
|
+
"help": "switch Law I's secret scan off for this install: a credential-shaped write stops being refused (Sealed Secrets' text still binds); written to the carrier as 'Secrets 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
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
"names": [
|
|
258
|
+
"--no-done-gate"
|
|
259
|
+
],
|
|
260
|
+
"dest": "no_done_gate",
|
|
261
|
+
"metavar": null,
|
|
262
|
+
"help": "switch Law III's done gate off for this install: a turn may end with the project's declared checks unproven (Verify Before Asserting's text still binds); written to the carrier as 'Done gate: off' and replayed by rebuilds",
|
|
263
|
+
"choices": null,
|
|
264
|
+
"default": false,
|
|
265
|
+
"required": false,
|
|
266
|
+
"nargs": "0",
|
|
267
|
+
"is_flag": true,
|
|
268
|
+
"type": null,
|
|
269
|
+
"hidden": false
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"names": [
|
|
273
|
+
"--no-deletion-gate"
|
|
274
|
+
],
|
|
275
|
+
"dest": "no_deletion_gate",
|
|
276
|
+
"metavar": null,
|
|
277
|
+
"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",
|
|
278
|
+
"choices": null,
|
|
279
|
+
"default": false,
|
|
280
|
+
"required": false,
|
|
281
|
+
"nargs": "0",
|
|
282
|
+
"is_flag": true,
|
|
283
|
+
"type": null,
|
|
284
|
+
"hidden": false
|
|
240
285
|
}
|
|
241
286
|
],
|
|
242
287
|
"mutex": []
|
|
@@ -581,6 +626,21 @@
|
|
|
581
626
|
"is_flag": true,
|
|
582
627
|
"type": null,
|
|
583
628
|
"hidden": false
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
"names": [
|
|
632
|
+
"--no-deletion-gate"
|
|
633
|
+
],
|
|
634
|
+
"dest": "no_deletion_gate",
|
|
635
|
+
"metavar": null,
|
|
636
|
+
"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",
|
|
637
|
+
"choices": null,
|
|
638
|
+
"default": false,
|
|
639
|
+
"required": false,
|
|
640
|
+
"nargs": "0",
|
|
641
|
+
"is_flag": true,
|
|
642
|
+
"type": null,
|
|
643
|
+
"hidden": false
|
|
584
644
|
}
|
|
585
645
|
],
|
|
586
646
|
"mutex": []
|
|
@@ -642,6 +702,21 @@
|
|
|
642
702
|
"is_flag": true,
|
|
643
703
|
"type": null,
|
|
644
704
|
"hidden": false
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
"names": [
|
|
708
|
+
"--no-done-gate"
|
|
709
|
+
],
|
|
710
|
+
"dest": "no_done_gate",
|
|
711
|
+
"metavar": null,
|
|
712
|
+
"help": "skip the done gate on Stop (set by the emit when the install was built --no-done-gate); learning still runs",
|
|
713
|
+
"choices": null,
|
|
714
|
+
"default": false,
|
|
715
|
+
"required": false,
|
|
716
|
+
"nargs": "0",
|
|
717
|
+
"is_flag": true,
|
|
718
|
+
"type": null,
|
|
719
|
+
"hidden": false
|
|
645
720
|
}
|
|
646
721
|
],
|
|
647
722
|
"mutex": []
|
|
@@ -1120,6 +1195,21 @@
|
|
|
1120
1195
|
"is_flag": false,
|
|
1121
1196
|
"type": null,
|
|
1122
1197
|
"hidden": false
|
|
1198
|
+
},
|
|
1199
|
+
{
|
|
1200
|
+
"names": [
|
|
1201
|
+
"--no-secrets-gate"
|
|
1202
|
+
],
|
|
1203
|
+
"dest": "no_secrets_gate",
|
|
1204
|
+
"metavar": null,
|
|
1205
|
+
"help": "skip the Sealed Secrets credential scan (set by the emit when the install was built --no-secrets-gate); the protected checks and the persist check still run",
|
|
1206
|
+
"choices": null,
|
|
1207
|
+
"default": false,
|
|
1208
|
+
"required": false,
|
|
1209
|
+
"nargs": "0",
|
|
1210
|
+
"is_flag": true,
|
|
1211
|
+
"type": null,
|
|
1212
|
+
"hidden": false
|
|
1123
1213
|
}
|
|
1124
1214
|
],
|
|
1125
1215
|
"mutex": []
|
|
@@ -1382,6 +1472,36 @@
|
|
|
1382
1472
|
"is_flag": false,
|
|
1383
1473
|
"type": null,
|
|
1384
1474
|
"hidden": false
|
|
1475
|
+
},
|
|
1476
|
+
{
|
|
1477
|
+
"names": [
|
|
1478
|
+
"--no-deletion-gate"
|
|
1479
|
+
],
|
|
1480
|
+
"dest": "no_deletion_gate",
|
|
1481
|
+
"metavar": null,
|
|
1482
|
+
"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",
|
|
1483
|
+
"choices": null,
|
|
1484
|
+
"default": false,
|
|
1485
|
+
"required": false,
|
|
1486
|
+
"nargs": "0",
|
|
1487
|
+
"is_flag": true,
|
|
1488
|
+
"type": null,
|
|
1489
|
+
"hidden": false
|
|
1490
|
+
},
|
|
1491
|
+
{
|
|
1492
|
+
"names": [
|
|
1493
|
+
"--no-secrets-gate"
|
|
1494
|
+
],
|
|
1495
|
+
"dest": "no_secrets_gate",
|
|
1496
|
+
"metavar": null,
|
|
1497
|
+
"help": "skip the Sealed Secrets credential scan (set by the emit when the install was built --no-secrets-gate); the deletion, root-scan and protected checks still refuse",
|
|
1498
|
+
"choices": null,
|
|
1499
|
+
"default": false,
|
|
1500
|
+
"required": false,
|
|
1501
|
+
"nargs": "0",
|
|
1502
|
+
"is_flag": true,
|
|
1503
|
+
"type": null,
|
|
1504
|
+
"hidden": false
|
|
1385
1505
|
}
|
|
1386
1506
|
],
|
|
1387
1507
|
"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-done.mjs
CHANGED
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
* last-writer-wins: an older `geneseed-hook` would refuse a new verb or flag, and the Stop
|
|
11
11
|
* command's `|| exit 0` would then skip learning too, silently. Arming on a project FILE keeps
|
|
12
12
|
* every emitted settings file byte-identical, so a stale shim only means "no done gate".
|
|
13
|
+
* The ONE flag is the off switch, `--no-done-gate` (user decision 2026-10-18): it is emitted
|
|
14
|
+
* only on an install built with the gate off, so armed settings stay byte-identical — but on
|
|
15
|
+
* such an install a stale shim refuses the flag and that install loses LEARNING too, not just a
|
|
16
|
+
* gate it already switched off. Hence "run rebuild-all before switching it off" (exclusions.md).
|
|
13
17
|
*
|
|
14
18
|
* WHY STDOUT JSON AND NOT EXIT 2. The emitted Stop command ends in `|| exit 0`, which swallows
|
|
15
19
|
* a 2. `{"decision":"block"}` on exit 0 is the one signal that survives the existing wiring.
|
package/js/hosts/hooks-learn.mjs
CHANGED
|
@@ -312,7 +312,9 @@ export function cmdLearn(args) {
|
|
|
312
312
|
|
|
313
313
|
// The done gate (#289) judges the turn BEFORE anything here spawns, and only on Stop. A block
|
|
314
314
|
// means the turn goes on, so there is nothing to distil yet: the next Stop will.
|
|
315
|
-
|
|
315
|
+
// `--no-done-gate` (the install switched Law III's gate off) skips the check, never the
|
|
316
|
+
// distillation below.
|
|
317
|
+
if (hookEvent === 'Stop' && !args.file && !args.noDoneGate && doneGate(meta, args, install)) return 0;
|
|
316
318
|
|
|
317
319
|
// I2 (Bob): its Stop payload never carries `transcript_path`, so there is never a transcript
|
|
318
320
|
// to distil — and the bare envelope itself is not notes text (that would waste a model call
|
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,45 +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), `reflog expire` and `gc
|
|
94
|
-
// --prune=now` (delete the reflog/dangling-commit safety net recovery depends on), and `push
|
|
95
|
-
// :branch`/`push --delete` (deletes a remote branch — the colon form needs a space before the
|
|
96
|
-
// `:` so `push origin HEAD:main`, an ordinary refspec, is not caught).
|
|
97
|
-
//
|
|
98
|
-
// `worktree remove --force`/`-f` is deliberately NOT here (user decision, 2026-10-17): removing
|
|
99
|
-
// a worktree is routine agent housekeeping, so it passes like a plain `worktree remove`. The
|
|
100
|
-
// OpenCode guard's `SHELL_WARN_RE` and `LAW_IV_BASH` (js/hosts/settings.mjs) dropped it too.
|
|
101
|
-
//
|
|
102
|
-
// `restore` and `stash drop`/`stash clear` are carved OUT of the shared `\bgit\b[^\n]*\b(…)`
|
|
103
|
-
// wrapper above and anchored to the verb position instead (`git`, an optional `-C <path>`, then
|
|
104
|
-
// the verb immediately): every other arm here needs a flag that is vanishingly unlikely in a
|
|
105
|
-
// commit message or a filename, but "restore" needs no flag at all and "drop"/"clear" are
|
|
106
|
-
// ordinary English, so `git commit -m "restore working behavior"` or `git stash push -m "clear
|
|
107
|
-
// old state"` would otherwise trip Law IV on the MESSAGE TEXT, not the command. Anchoring to
|
|
108
|
-
// the verb position is also what lets `restore` ask for `--staged --worktree` together (that
|
|
109
|
-
// combination DOES discard working-tree changes, unlike `--staged` alone) while still deferring
|
|
110
|
-
// on `--staged` alone.
|
|
111
|
-
const DESTRUCTIVE_GIT_RE =
|
|
112
|
-
/\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)|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.
|
|
113
82
|
|
|
114
83
|
// `loopExempt` — a WHITELIST, not the blacklist this replaced. A false ask costs one prompt; a
|
|
115
84
|
// false allow publishes. So the exemption holds only when the ENTIRE command is built from
|
|
@@ -465,9 +434,17 @@ const rootArg = (t) => !/^["']/.test(t) && FS_ROOT_RE.test(t);
|
|
|
465
434
|
function gitDecide(args, payload) {
|
|
466
435
|
const command = ((payload && payload.tool_input) || {}).command;
|
|
467
436
|
if (typeof command !== 'string') return 0;
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
+
}
|
|
471
448
|
}
|
|
472
449
|
// Before `--no-consent`: that flag drops the process pack's rule, not this one.
|
|
473
450
|
const scan = rootScan(command);
|
|
@@ -592,7 +569,9 @@ function ruleDecide(args, payload) {
|
|
|
592
569
|
const edits = Array.isArray(ti.edits) ? ti.edits.map((e) => e && e.new_string) : [];
|
|
593
570
|
const body = [ti.content, ti.new_string, ti.new_source, diffReplaced(ti.diff), ti.replace, ...edits]
|
|
594
571
|
.filter((v) => typeof v === 'string').join('\n');
|
|
595
|
-
|
|
572
|
+
// `--no-secrets-gate`: the install switched Law I's scan off (user decision 2026-10-18). Only
|
|
573
|
+
// this check stands down; the protected checks and process 1 below still run.
|
|
574
|
+
if (!args.noSecretsGate && body && !DOTENV_RE.test(p) && SECRET_RE.test(body)) {
|
|
596
575
|
return ask(args, 'rule-gate', 'law-1', `Geneseed (Sealed Secrets) — ${p} would carry a `
|
|
597
576
|
+ 'credential-shaped string. Secrets live in .env or a secret manager, never in a '
|
|
598
577
|
+ 'tracked file.');
|
package/js/hosts/installs.mjs
CHANGED
|
@@ -35,7 +35,9 @@
|
|
|
35
35
|
import { existsSync, readdirSync } from 'node:fs';
|
|
36
36
|
import path from 'node:path';
|
|
37
37
|
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
CONFIG, GATE_SWITCHES, PACK_ORDER, THEMES, discoverNames, resolveSkillNames,
|
|
40
|
+
} from '../build/source.mjs';
|
|
39
41
|
import {
|
|
40
42
|
CLAUDE_STYLE, DISABLED_STASH, GLOBAL_MANIFEST, HOSTS, isHostGlobalDir, liveRecordedHooks,
|
|
41
43
|
resolvePath,
|
|
@@ -583,6 +585,33 @@ export function excludedSkillsOfDir(d, host = null, notify = null) {
|
|
|
583
585
|
}, [], host);
|
|
584
586
|
}
|
|
585
587
|
|
|
588
|
+
/**
|
|
589
|
+
* Whether a DEPLOYED install keeps one law's hook gate armed — `true` (armed) or `false` (its
|
|
590
|
+
* `<marker>: off` line, written by a build with that gate's `--no-*-gate` flag; `GATE_SWITCHES`).
|
|
591
|
+
* Read back so `upgrade`, `rebuild-all`, `diff` and the console re-render the same switch.
|
|
592
|
+
* FAILS CLOSED: a missing line, a missing carrier, or any value but exactly `off` reads as
|
|
593
|
+
* armed, because the line is only ever written when the gate is off and a gate is the thing a
|
|
594
|
+
* misread must never take away.
|
|
595
|
+
*/
|
|
596
|
+
function gateOfDir(d, marker, host = null) {
|
|
597
|
+
const re = new RegExp(`^${marker}:[ \\t]*(.+?)[ \\t]*$`, 'm');
|
|
598
|
+
return firstCarrier(d, (carrierPath) => {
|
|
599
|
+
const text = readMaybe(carrierPath);
|
|
600
|
+
if (text === null) return undefined;
|
|
601
|
+
const m = re.exec(text);
|
|
602
|
+
if (!m) return undefined;
|
|
603
|
+
return m[1] !== 'off';
|
|
604
|
+
}, true, host);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** Every switchable gate of a deployed install, keyed as `makeCfg` and `installProfile` key them. */
|
|
608
|
+
export function gatesOfDir(d, host = null) {
|
|
609
|
+
return Object.fromEntries(GATE_SWITCHES.map((g) => [g.key, gateOfDir(d, g.marker, host)]));
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** Law IV's switch alone (#319's reader, kept for its callers). */
|
|
613
|
+
export const deletionGateOfDir = (d, host = null) => gateOfDir(d, 'Deletion gate', host);
|
|
614
|
+
|
|
586
615
|
// ---- what host a deployed dir belongs to (`_harness_mcp`) ---------------------------------
|
|
587
616
|
|
|
588
617
|
/** `_harness_mcp._claude_read_manifest`. */
|