geneseed 4.8.0 → 4.9.1
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 +34 -0
- package/SHIPPED.md +1 -1
- package/adapters/claude-code/README.md +3 -1
- package/adapters/opencode/plugins/geneseed-context.js +17 -1
- package/adapters/opencode/plugins/geneseed-guard.js +138 -32
- package/bin/geneseed-hook.mjs +9 -4
- package/docs/concepts/enforcement-table.md +2 -2
- package/docs/concepts/exclusions.md +18 -1
- package/docs/concepts/hooks.md +2 -2
- package/docs/reference/plugin-context.md +1 -1
- package/docs/reference/plugin-guard.md +1 -1
- package/docs/reference/troubleshoot-doctor.md +2 -1
- package/docs/web-ui.md +10 -8
- package/harness.config.json +1 -1
- package/js/build/args.mjs +3 -0
- package/js/build/driver.mjs +14 -6
- package/js/build/emit-claude.mjs +3 -3
- package/js/build/generate.mjs +16 -12
- package/js/build/render.mjs +7 -5
- package/js/build/source.mjs +19 -1
- package/js/cli-table.json +75 -0
- package/js/hosts/hooks-done.mjs +4 -0
- package/js/hosts/hooks-learn.mjs +3 -1
- package/js/hosts/hooks.mjs +14 -8
- package/js/hosts/installs.mjs +20 -11
- package/js/hosts/loss.mjs +124 -18
- package/js/hosts/settings.mjs +17 -8
- package/js/inspect/diff.mjs +4 -4
- package/js/inspect/doctor.mjs +4 -3
- package/js/inspect/status.mjs +4 -3
- package/js/inspect/validate.mjs +9 -4
- package/js/maintain/uninstall.mjs +7 -5
- package/js/maintain/update.mjs +1 -1
- package/js/web/actions.mjs +22 -17
- package/js/web/api.mjs +13 -11
- package/js/web/handler.mjs +1 -1
- package/js/web/routes.mjs +3 -1
- package/js/web/server.mjs +3 -1
- package/js/web/state.mjs +32 -8
- package/package.json +1 -1
- package/src/AGENT.md.tmpl +1 -1
- package/web/dist/assets/{Activity-B-XbKlzu.js → Activity-GQGAZHIE.js} +1 -1
- package/web/dist/assets/{ActivityDetail-8oq0fycD.js → ActivityDetail-DktWyxim.js} +1 -1
- package/web/dist/assets/Dashboard-CmSn6JfK.js +2 -0
- package/web/dist/assets/Docs-CsWYLnN1.js +5 -0
- package/web/dist/assets/FilterInput-zLFXoSt6.js +1 -0
- package/web/dist/assets/GreenhouseView-Dt_cIxAY.js +2 -0
- package/web/dist/assets/{Installs-DrqPaLDM.js → Installs-CWuUU4I3.js} +2 -2
- package/web/dist/assets/JournalView-ClbZj_YF.js +1 -0
- package/web/dist/assets/Laws-R5zZCmya.js +3 -0
- package/web/dist/assets/{Library-BCaqPf61.js → Library-u36V7oSD.js} +1 -1
- package/web/dist/assets/{Loops-Bx3HnlUA.js → Loops-BUBOeM0C.js} +2 -2
- package/web/dist/assets/{Markdown-BK38dn4x.js → Markdown-iHvBrpRN.js} +1 -1
- package/web/dist/assets/{OperatorHudView-ChQPM8JE.js → OperatorHudView-BJuCYsjf.js} +1 -1
- package/web/dist/assets/{Personal-DELm61DF.js → Personal-DWQuSAUz.js} +1 -1
- package/web/dist/assets/Tabs-GPQlocTw.js +1 -0
- package/web/dist/assets/{activity-D4w9t8fE.js → activity-DvPxoS4Y.js} +1 -1
- package/web/dist/assets/api-CiXlkbe0.js +1 -0
- package/web/dist/assets/drawable-CE-arUnF.js +1 -0
- package/web/dist/assets/{index-r2jRgf6l.js → index-3lnPT_1J.js} +2 -2
- package/web/dist/assets/{index-CHpydQJg.css → index-BCJz4_8C.css} +1 -1
- package/web/dist/assets/{react-Ci4wQD4a.js → react-BOF6yL40.js} +1 -1
- package/web/dist/assets/{sections-GalMS0Zv.js → sections-BET3c1WT.js} +1 -1
- package/web/dist/assets/{usePoll-BcywUo55.js → usePoll-NSTzLOF4.js} +1 -1
- package/web/dist/index.html +5 -5
- package/web/src/pages/Laws.jsx +52 -25
- package/web/dist/assets/Dashboard-C4EMz5mf.js +0 -2
- package/web/dist/assets/Docs-mq4XDQK_.js +0 -5
- package/web/dist/assets/FilterInput-Ikvi-pS9.js +0 -1
- package/web/dist/assets/GreenhouseView-D1WOtq3n.js +0 -2
- package/web/dist/assets/JournalView-BYpASU-L.js +0 -1
- package/web/dist/assets/Laws-A3PFwiPD.js +0 -3
- package/web/dist/assets/Tabs-DcMq5IvC.js +0 -1
- package/web/dist/assets/api-Bp5eW_6m.js +0 -1
- package/web/dist/assets/drawable-BI2_tF3o.js +0 -1
package/js/build/driver.mjs
CHANGED
|
@@ -647,19 +647,25 @@ const PROJECT_EMITS = {
|
|
|
647
647
|
* mode readers return `null` for "undetectable" too, which an explicit `posture: null` would
|
|
648
648
|
* carry straight past the parameter default.
|
|
649
649
|
*/
|
|
650
|
-
function axesCfg({
|
|
650
|
+
function axesCfg({
|
|
651
|
+
posture, mode, trust, doctrines, excludeRules, excludeSkills,
|
|
652
|
+
deletionGate = null, secretsGate = null, doneGate = null,
|
|
653
|
+
}) {
|
|
651
654
|
return makeCfg({
|
|
652
655
|
...(posture ? { posture } : {}), ...(mode ? { mode } : {}), ...(trust ? { trust } : {}),
|
|
653
656
|
...(doctrines ? { doctrines } : {}), ...(excludeRules ? { excludeRules } : {}),
|
|
654
657
|
...(excludeSkills ? { excludeSkills } : {}),
|
|
655
658
|
// Only an explicit `false` disarms; `null` (no opinion) keeps the gate.
|
|
656
659
|
...(deletionGate === false ? { noDeletionGate: true } : {}),
|
|
660
|
+
...(secretsGate === false ? { noSecretsGate: true } : {}),
|
|
661
|
+
...(doneGate === false ? { noDoneGate: true } : {}),
|
|
657
662
|
});
|
|
658
663
|
}
|
|
659
664
|
|
|
660
665
|
export function emitProjectInto(host, {
|
|
661
666
|
theme, out, root, footprint = 'full', posture = null, mode = null, trust = null,
|
|
662
667
|
doctrines = null, excludeRules = null, excludeSkills = null, deletionGate = null,
|
|
668
|
+
secretsGate = null, doneGate = null,
|
|
663
669
|
}) {
|
|
664
670
|
const emit = PROJECT_EMITS[host];
|
|
665
671
|
// The axes ride the same rail into every `*Into`, and for the same reason: a drift reader
|
|
@@ -667,7 +673,8 @@ export function emitProjectInto(host, {
|
|
|
667
673
|
// edited on every narrowed install, and `validate --doctrines craft` that ignored its flag
|
|
668
674
|
// checked an all-packs render nobody asked for.
|
|
669
675
|
return withPlatformNewlines(() => emit(
|
|
670
|
-
axesCfg({
|
|
676
|
+
axesCfg({
|
|
677
|
+
posture, mode, trust, doctrines, excludeRules, excludeSkills, deletionGate, secretsGate, doneGate }),
|
|
671
678
|
{ theme, footprint, root }, out,
|
|
672
679
|
));
|
|
673
680
|
}
|
|
@@ -682,16 +689,16 @@ export function emitProjectInto(host, {
|
|
|
682
689
|
*/
|
|
683
690
|
export function buildInto({
|
|
684
691
|
theme, out, footprint = 'lean', posture = null, mode = null, trust = null, doctrines = null,
|
|
685
|
-
excludeRules = null, excludeSkills = null, deletionGate = null,
|
|
692
|
+
excludeRules = null, excludeSkills = null, deletionGate = null, secretsGate = null, doneGate = null,
|
|
686
693
|
}) {
|
|
687
694
|
return withPlatformNewlines(() => build(axesCfg({
|
|
688
|
-
posture, mode, trust, doctrines, excludeRules, excludeSkills, deletionGate }),
|
|
695
|
+
posture, mode, trust, doctrines, excludeRules, excludeSkills, deletionGate, secretsGate, doneGate }),
|
|
689
696
|
theme, out, { footprint, nativeCatalog: false }));
|
|
690
697
|
}
|
|
691
698
|
|
|
692
699
|
export function emitGlobalInto(host, {
|
|
693
700
|
theme, out, cfgDir, footprint, posture = null, mode = null, doctrines = null, trust = null,
|
|
694
|
-
excludeRules = null, excludeSkills = null, deletionGate = null,
|
|
701
|
+
excludeRules = null, excludeSkills = null, deletionGate = null, secretsGate = null, doneGate = null,
|
|
695
702
|
}) {
|
|
696
703
|
// `build.HOSTS.get(host, build.HOSTS["opencode"])` — an unknown host falls back rather than
|
|
697
704
|
// raising, because the host comes from a marker file a user can edit.
|
|
@@ -707,7 +714,8 @@ export function emitGlobalInto(host, {
|
|
|
707
714
|
// edited on every install that chose a register, narrowed its packs or excluded a rule —
|
|
708
715
|
// the same scar the footprint left in `diffCollect`, one axis over each time.
|
|
709
716
|
return withPlatformNewlines(() => emit(
|
|
710
|
-
axesCfg({
|
|
717
|
+
axesCfg({
|
|
718
|
+
posture, mode, trust, doctrines, excludeRules, excludeSkills, deletionGate, secretsGate, doneGate }),
|
|
711
719
|
{ theme, footprint, root: null, cfgDir }, out,
|
|
712
720
|
));
|
|
713
721
|
}
|
package/js/build/emit-claude.mjs
CHANGED
|
@@ -182,7 +182,7 @@ export function emitClaudeRender(cfg, job) {
|
|
|
182
182
|
// WIRE — see `claudeWire` below. One child per emit, so the two halves are two
|
|
183
183
|
// functions rather than two spawns; the seam between them is a call, not a process.
|
|
184
184
|
const managed = claudeWire(job, claudeMdText, agentText !== null, cfg.doctrines,
|
|
185
|
-
cfg.excludeRules, cfg.deletionGate);
|
|
185
|
+
cfg.excludeRules, cfg.deletionGate, { secretsGate: cfg.secretsGate, doneGate: cfg.doneGate });
|
|
186
186
|
|
|
187
187
|
return {
|
|
188
188
|
owned,
|
|
@@ -216,7 +216,7 @@ export function emitClaudeRender(cfg, job) {
|
|
|
216
216
|
* always a string, so `''` is the only falsy value either language sees.
|
|
217
217
|
*/
|
|
218
218
|
function claudeWire(job, claudeMdText, hasAgentText, doctrines = null, excludeRules = [],
|
|
219
|
-
deletionGate = true) {
|
|
219
|
+
deletionGate = true, gates = {}) {
|
|
220
220
|
phaseLog('WIRE');
|
|
221
221
|
const { cfgDir, claudeMd, scope, host, oldManaged, preambleExclude, hookOpts } = job;
|
|
222
222
|
const old = oldManaged && typeof oldManaged === 'object' && !Array.isArray(oldManaged)
|
|
@@ -280,7 +280,7 @@ function claudeWire(job, claudeMdText, hasAgentText, doctrines = null, excludeRu
|
|
|
280
280
|
// DECIDES the install's packs, so the marker on disk is still the previous build's.
|
|
281
281
|
const [, managedHooks] = mergeClaudeSettings(
|
|
282
282
|
settingsPath, oldSf === settingsName ? get(old, 'settings_hooks') : null, hookOpts,
|
|
283
|
-
doctrines, excludeRules, host, cfgDir, deletionGate,
|
|
283
|
+
doctrines, excludeRules, host, cfgDir, deletionGate, gates,
|
|
284
284
|
);
|
|
285
285
|
managed.settings_hooks = managedHooks;
|
|
286
286
|
|
package/js/build/generate.mjs
CHANGED
|
@@ -35,12 +35,12 @@ import { existsSync, mkdirSync, statSync } from 'node:fs';
|
|
|
35
35
|
import path from 'node:path';
|
|
36
36
|
|
|
37
37
|
import { main as driverMain } from './driver.mjs';
|
|
38
|
-
import { makeCfg, PACK_ORDER } from './source.mjs';
|
|
38
|
+
import { GATE_SWITCHES, makeCfg, PACK_ORDER } from './source.mjs';
|
|
39
39
|
import { DEFAULT_PRESET } from '../loop/score.mjs';
|
|
40
40
|
import { opencodeConfigDir, resolvePath } from '../hosts/hosts.mjs';
|
|
41
41
|
import {
|
|
42
42
|
EMIT_HOST_SCOPE, defaultMode, defaultPosture, defaultTheme, doctrinesForBuild,
|
|
43
|
-
|
|
43
|
+
excludedRulesOfDir, excludedSkillsOfDir, footprintOfDir, gatesOfDir, installState, installTargets,
|
|
44
44
|
legacyProcessCarrierOfDir, modeOfDir, postureOfDir, readMaybe, themeOfDir, trustOfDir,
|
|
45
45
|
} from '../hosts/installs.mjs';
|
|
46
46
|
import { colorThemeFiles, colorThemeJson, PALETTE_ROLES } from '../hosts/opencode.mjs';
|
|
@@ -102,8 +102,9 @@ const BUILD_FORWARD = [
|
|
|
102
102
|
export function cmdBuild(args) {
|
|
103
103
|
const argv = [];
|
|
104
104
|
for (const [key, flag] of BUILD_FORWARD) if (args[key] != null) argv.push(flag, args[key]);
|
|
105
|
-
// The
|
|
106
|
-
|
|
105
|
+
// The bare switches `build` forwards (the gate switches): they take no value, so they are
|
|
106
|
+
// not BUILD_FORWARD rows.
|
|
107
|
+
for (const g of GATE_SWITCHES) if (args[g.arg]) argv.push(g.flag);
|
|
107
108
|
return driverMain(argv);
|
|
108
109
|
}
|
|
109
110
|
|
|
@@ -140,7 +141,8 @@ export function cmdBuild(args) {
|
|
|
140
141
|
*/
|
|
141
142
|
export function setupBuildArgs(theme, emit, out = null, root = null, footprint = 'lean',
|
|
142
143
|
posture = 'peer', mode = 'direct', doctrines = null, allPacks = PACK_ORDER,
|
|
143
|
-
excludeRules = null, trust = DEFAULT_PRESET, excludeSkills = null, deletionGate = true
|
|
144
|
+
excludeRules = null, trust = DEFAULT_PRESET, excludeSkills = null, deletionGate = true,
|
|
145
|
+
{ secretsGate = true, doneGate = true } = {}) {
|
|
144
146
|
const argv = ['--theme', theme, '--emit', emit];
|
|
145
147
|
if (!emit.endsWith('-global')) {
|
|
146
148
|
if (out) argv.push('--out', out);
|
|
@@ -168,9 +170,11 @@ export function setupBuildArgs(theme, emit, out = null, root = null, footprint =
|
|
|
168
170
|
// feeds it), so the flag's absence and `none` are the same answer, and every argv that
|
|
169
171
|
// excludes nothing stays the bytes it was before the flag existed.
|
|
170
172
|
if (excludeSkills?.length) argv.push('--exclude-skills', excludeSkills.join(','));
|
|
171
|
-
//
|
|
172
|
-
// only an explicit `false` speaks, and every armed argv stays the bytes it was.
|
|
173
|
-
|
|
173
|
+
// The gate switches elide when armed: the driver's default IS armed (no config key feeds
|
|
174
|
+
// them), so only an explicit `false` speaks, and every armed argv stays the bytes it was.
|
|
175
|
+
// Law order, so a Law IV-only install still ends on `--no-deletion-gate` as it did.
|
|
176
|
+
const gates = { secretsGate, doneGate, deletionGate };
|
|
177
|
+
for (const g of GATE_SWITCHES) if (gates[g.key] === false) argv.push(g.flag);
|
|
174
178
|
return argv;
|
|
175
179
|
}
|
|
176
180
|
|
|
@@ -229,12 +233,12 @@ export function installProfile(host, scope, root, notify = null) {
|
|
|
229
233
|
const excludeRules = excludedRulesOfDir(root, host);
|
|
230
234
|
// Skills left out stay left out: an upgrade that forgot them would put them all back.
|
|
231
235
|
const excludeSkills = excludedSkillsOfDir(root, host, notify);
|
|
232
|
-
//
|
|
236
|
+
// The gate switches: a rebuild that forgot one would silently re-arm (or, read wrong, disarm)
|
|
233
237
|
// the install. A missing marker reads as armed.
|
|
234
|
-
const
|
|
238
|
+
const gates = gatesOfDir(root, host);
|
|
235
239
|
const out = scope === 'global' ? null : root;
|
|
236
240
|
const argv = setupBuildArgs(theme, emit, out, out, footprint, posture, mode, doctrines,
|
|
237
|
-
PACK_ORDER, excludeRules, trust, excludeSkills, deletionGate);
|
|
241
|
+
PACK_ORDER, excludeRules, trust, excludeSkills, gates.deletionGate, gates);
|
|
238
242
|
// A GLOBAL ROW NAMES ITS OWN DIR. `root` came from the host's config-dir resolver, which
|
|
239
243
|
// honours env overrides (`OPENCODE_CONFIG_DIR`, `CLAUDE_CONFIG_DIR`…); without the flag the
|
|
240
244
|
// same argv run from a shell lacking that variable re-emits into the DEFAULT dir — a second
|
|
@@ -243,7 +247,7 @@ export function installProfile(host, scope, root, notify = null) {
|
|
|
243
247
|
return {
|
|
244
248
|
host, scope, root, state: installState(root, host, scope),
|
|
245
249
|
emit, theme, footprint, posture, mode, trust, doctrines, excludeRules, excludeSkills,
|
|
246
|
-
|
|
250
|
+
...gates, argv,
|
|
247
251
|
};
|
|
248
252
|
}
|
|
249
253
|
|
package/js/build/render.mjs
CHANGED
|
@@ -34,7 +34,7 @@ import { readText } from '../lib/fs.mjs';
|
|
|
34
34
|
import { parseJson } from '../lib/json.mjs';
|
|
35
35
|
import { normcase, comparePaths } from '../lib/paths.mjs';
|
|
36
36
|
// `js/build/source.mjs` imports nothing but node builtins, so this direction cannot cycle.
|
|
37
|
-
import { PACK_ORDER, readPackText, resolveRuleIds, ruleCanon } from './source.mjs';
|
|
37
|
+
import { GATE_SWITCHES, PACK_ORDER, readPackText, resolveRuleIds, ruleCanon } from './source.mjs';
|
|
38
38
|
import { DEFAULT_PRESET } from '../loop/score.mjs';
|
|
39
39
|
|
|
40
40
|
/** Document STRUCTURE is theme-INDEPENDENT — mirrors `_build_render.STRUCTURE`. */
|
|
@@ -444,10 +444,12 @@ export function effectiveTheme(cfg, themeName, { footprint = 'full', lawsPrefix
|
|
|
444
444
|
const skipped = [...(cfg.excludeSkills ?? [])].sort();
|
|
445
445
|
// It fills a template line of its own, so its value carries the blank line around it.
|
|
446
446
|
theme.EXCLUDED_SKILLS_LINE = skipped.length ? `\nExcluded skills: ${skipped.join(', ')}\n` : '';
|
|
447
|
-
//
|
|
448
|
-
// stays byte-identical and a missing
|
|
449
|
-
//
|
|
450
|
-
|
|
447
|
+
// The gate switches (`GATE_SWITCHES`: Laws I, III, IV), under the same contract: a line is
|
|
448
|
+
// written only when its gate is OFF, so a default build stays byte-identical and a missing
|
|
449
|
+
// line reads back as armed (`gatesOfDir`). The OpenCode plugins read these lines at load, so
|
|
450
|
+
// their wording is a parsed contract.
|
|
451
|
+
theme.GATE_LINES = GATE_SWITCHES.filter((g) => cfg[g.key] === false)
|
|
452
|
+
.map((g) => `\n${g.marker}: off`).join('');
|
|
451
453
|
return theme;
|
|
452
454
|
}
|
|
453
455
|
|
package/js/build/source.mjs
CHANGED
|
@@ -399,7 +399,8 @@ export function resolveSkillNames(names, {
|
|
|
399
399
|
*/
|
|
400
400
|
export function makeCfg({
|
|
401
401
|
posture = 'peer', mode = 'direct', trust = DEFAULT_PRESET, doctrines = PACK_ORDER,
|
|
402
|
-
excludeRules = [], excludeSkills = [], noDeletionGate = false,
|
|
402
|
+
excludeRules = [], excludeSkills = [], noDeletionGate = false, noSecretsGate = false,
|
|
403
|
+
noDoneGate = false,
|
|
403
404
|
} = {}) {
|
|
404
405
|
return {
|
|
405
406
|
root: ROOT,
|
|
@@ -430,5 +431,22 @@ export function makeCfg({
|
|
|
430
431
|
// Law IV's deletion gate (`--no-deletion-gate`). Armed unless switched off in so many words:
|
|
431
432
|
// only a literal `true` disarms, so every caller that never heard of the switch keeps it.
|
|
432
433
|
deletionGate: noDeletionGate !== true,
|
|
434
|
+
// Law I's secret scan (`--no-secrets-gate`) and Law III's done gate (`--no-done-gate`), the
|
|
435
|
+
// same contract (user decision 2026-10-18; see GATE_SWITCHES).
|
|
436
|
+
secretsGate: noSecretsGate !== true,
|
|
437
|
+
doneGate: noDoneGate !== true,
|
|
433
438
|
};
|
|
434
439
|
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* The laws whose hook gate an install can switch off, in law order: the cfg/profile key, the
|
|
443
|
+
* build flag, the carrier marker (`<marker>: off`, written only when off) and the law. ONE
|
|
444
|
+
* table, so the flag, the marker, the rebuild argv, the read-back and `status` cannot drift
|
|
445
|
+
* apart. Every gate is ARMED by default and the law's text binds either way. The deletion row
|
|
446
|
+
* is #319's, unchanged: existing installs carry its flag and marker.
|
|
447
|
+
*/
|
|
448
|
+
export const GATE_SWITCHES = [
|
|
449
|
+
{ key: 'secretsGate', flag: '--no-secrets-gate', arg: 'noSecretsGate', marker: 'Secrets gate', law: 'I' },
|
|
450
|
+
{ key: 'doneGate', flag: '--no-done-gate', arg: 'noDoneGate', marker: 'Done gate', law: 'III' },
|
|
451
|
+
{ key: 'deletionGate', flag: '--no-deletion-gate', arg: 'noDeletionGate', marker: 'Deletion gate', law: 'IV' },
|
|
452
|
+
];
|
package/js/cli-table.json
CHANGED
|
@@ -238,6 +238,36 @@
|
|
|
238
238
|
"type": null,
|
|
239
239
|
"hidden": false
|
|
240
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
|
+
},
|
|
241
271
|
{
|
|
242
272
|
"names": [
|
|
243
273
|
"--no-deletion-gate"
|
|
@@ -672,6 +702,21 @@
|
|
|
672
702
|
"is_flag": true,
|
|
673
703
|
"type": null,
|
|
674
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
|
|
675
720
|
}
|
|
676
721
|
],
|
|
677
722
|
"mutex": []
|
|
@@ -1150,6 +1195,21 @@
|
|
|
1150
1195
|
"is_flag": false,
|
|
1151
1196
|
"type": null,
|
|
1152
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
|
|
1153
1213
|
}
|
|
1154
1214
|
],
|
|
1155
1215
|
"mutex": []
|
|
@@ -1427,6 +1487,21 @@
|
|
|
1427
1487
|
"is_flag": true,
|
|
1428
1488
|
"type": null,
|
|
1429
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
|
|
1430
1505
|
}
|
|
1431
1506
|
],
|
|
1432
1507
|
"mutex": []
|
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,7 +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
|
+
import { LOSS_CANDIDATE_RE, lossReason, stripStructure } from './loss.mjs';
|
|
62
62
|
import { selfAndParents, readStdin } from './hooks-prims.mjs';
|
|
63
63
|
import { recordUse } from './usage.mjs';
|
|
64
64
|
|
|
@@ -349,13 +349,14 @@ const SCAN_WORD_RE = /\b(?:find|du|tree|rg|fd|where\.exe|grep|egrep|ls|dir|get-c
|
|
|
349
349
|
const SCAN_TOKEN_RE = /(?:[^\s"']+|"[^"]*"?|'[^']*'?)+/g;
|
|
350
350
|
const SCAN_WRAPPERS = new Set(['sudo', 'command', '\\builtin', 'time', 'nice']);
|
|
351
351
|
// One left-to-right pass: a comment (`#` starting a word, to the end of the line — an apostrophe
|
|
352
|
-
// in it is no quote), a quoted run (kept whole, so its `;`/`|` is data), a separator
|
|
352
|
+
// in it is no quote), a quoted run (kept whole, so its `;`/`|` is data), a separator (a lone `&`
|
|
353
|
+
// backgrounds a command and runs the next, but `2>&1`, `<&3`, `&>` and `|&` are not one), or a
|
|
353
354
|
// heredoc opener (`<<TAG`, `<<-TAG`, `<< 'TAG'`, `<<"TAG"` — never the here-string `<<<`).
|
|
354
355
|
// Every alternative consumes at least one character, and none can backtrack past its start.
|
|
355
|
-
const SCAN_SPLIT_RE = /(?<![^\s;|&(])#[^\n]*|"[^"]*"?|'[^']*'?|\|\||&&|[|;\n]|(?<!<)<<(?!<)(-?)[ \t]*(["']?)([A-Za-z_]\w*)\2/g;
|
|
356
|
+
const SCAN_SPLIT_RE = /(?<![^\s;|&(])#[^\n]*|"[^"]*"?|'[^']*'?|\|\||&&|[|;\n]|(?<![&<>|])&(?![&>])|(?<!<)<<(?!<)(-?)[ \t]*(["']?)([A-Za-z_]\w*)\2/g;
|
|
356
357
|
|
|
357
358
|
/**
|
|
358
|
-
* The command cut at
|
|
359
|
+
* The command cut at `|`/`;`/`&`/`&&`/`||`/newline OUTSIDE quotes, with every heredoc body (the
|
|
359
360
|
* lines after `<<TAG` up to the `TAG` line; `<<-` lets it be tab-indented) dropped: both are data.
|
|
360
361
|
* Without this, `git commit -F - <<'EOF'` — the mandated multi-line commit form — or a quoted
|
|
361
362
|
* `-m "fix; find / loop"` whose text merely names a root scan reads as one, which on Bob and
|
|
@@ -401,7 +402,8 @@ export function rootScan(command) {
|
|
|
401
402
|
if (!SCAN_WORD_RE.test(command)) return null;
|
|
402
403
|
for (const seg of scanSegments(command)) {
|
|
403
404
|
// Tokens keep their quotes until the root test, so a quoted `"a:"` can be told from a bare `C:`.
|
|
404
|
-
|
|
405
|
+
// `(find / …)`, `then find / …`: the grammar around the command is not its program.
|
|
406
|
+
let toks = stripStructure(seg.match(SCAN_TOKEN_RE) || []).filter((t) => !/^\w+=/.test(t));
|
|
405
407
|
while (toks.length && SCAN_WRAPPERS.has(unquote(toks[0]))) toks = toks.slice(1);
|
|
406
408
|
if (!toks.length) continue;
|
|
407
409
|
// The program word without its path (either slash) or an `.exe` suffix: `C:\tools\rg.exe`.
|
|
@@ -427,6 +429,8 @@ export function rootScan(command) {
|
|
|
427
429
|
return null;
|
|
428
430
|
}
|
|
429
431
|
const unquote = (t) => t.replace(/^["']+|["']+$/g, '');
|
|
432
|
+
// The segments `lossReason` reads: raw tokens, quotes still on — also how it cuts a `$(…)` body.
|
|
433
|
+
const lossSegments = (s) => scanSegments(s).map((seg) => seg.match(SCAN_TOKEN_RE) || []);
|
|
430
434
|
// A QUOTED token is data — a grep pattern (`grep -rn "/" src`), a path named in a search
|
|
431
435
|
// (`rg "/mnt/c" README.md`) — never a root target; only an unquoted root is one.
|
|
432
436
|
const rootArg = (t) => !/^["']/.test(t) && FS_ROOT_RE.test(t);
|
|
@@ -439,8 +443,8 @@ function gitDecide(args, payload) {
|
|
|
439
443
|
if (!args.noDeletionGate && LOSS_CANDIDATE_RE.test(command)) {
|
|
440
444
|
// A pass here FALLS THROUGH: a force push to a feature branch loses nothing, but it is still
|
|
441
445
|
// a push, and process 5 below still asks for it.
|
|
442
|
-
const loss = lossReason(
|
|
443
|
-
|
|
446
|
+
const loss = lossReason(lossSegments(command), (payload && payload.cwd) || process.cwd(),
|
|
447
|
+
{ split: lossSegments });
|
|
444
448
|
if (loss) {
|
|
445
449
|
return ask(args, 'git-gate', 'law-4', `Geneseed (Deletion Is Deliberate) \u2014 ${loss}. `
|
|
446
450
|
+ 'Confirm this specific command, or leave the data in place.');
|
|
@@ -569,7 +573,9 @@ function ruleDecide(args, payload) {
|
|
|
569
573
|
const edits = Array.isArray(ti.edits) ? ti.edits.map((e) => e && e.new_string) : [];
|
|
570
574
|
const body = [ti.content, ti.new_string, ti.new_source, diffReplaced(ti.diff), ti.replace, ...edits]
|
|
571
575
|
.filter((v) => typeof v === 'string').join('\n');
|
|
572
|
-
|
|
576
|
+
// `--no-secrets-gate`: the install switched Law I's scan off (user decision 2026-10-18). Only
|
|
577
|
+
// this check stands down; the protected checks and process 1 below still run.
|
|
578
|
+
if (!args.noSecretsGate && body && !DOTENV_RE.test(p) && SECRET_RE.test(body)) {
|
|
573
579
|
return ask(args, 'rule-gate', 'law-1', `Geneseed (Sealed Secrets) — ${p} would carry a `
|
|
574
580
|
+ 'credential-shaped string. Secrets live in .env or a secret manager, never in a '
|
|
575
581
|
+ '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,26 +585,33 @@ export function excludedSkillsOfDir(d, host = null, notify = null) {
|
|
|
583
585
|
}, [], host);
|
|
584
586
|
}
|
|
585
587
|
|
|
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
588
|
/**
|
|
590
|
-
* Whether a DEPLOYED install keeps
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
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
595
|
*/
|
|
596
|
-
|
|
596
|
+
function gateOfDir(d, marker, host = null) {
|
|
597
|
+
const re = new RegExp(`^${marker}:[ \\t]*(.+?)[ \\t]*$`, 'm');
|
|
597
598
|
return firstCarrier(d, (carrierPath) => {
|
|
598
599
|
const text = readMaybe(carrierPath);
|
|
599
600
|
if (text === null) return undefined;
|
|
600
|
-
const m =
|
|
601
|
+
const m = re.exec(text);
|
|
601
602
|
if (!m) return undefined;
|
|
602
603
|
return m[1] !== 'off';
|
|
603
604
|
}, true, host);
|
|
604
605
|
}
|
|
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
|
+
|
|
606
615
|
// ---- what host a deployed dir belongs to (`_harness_mcp`) ---------------------------------
|
|
607
616
|
|
|
608
617
|
/** `_harness_mcp._claude_read_manifest`. */
|