geneseed 3.1.1 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -1
- package/DESIGN.md +15 -15
- package/README.md +1 -1
- package/SETUP.md +1 -1
- package/SHIPPED.md +1 -1
- package/bin/{geneseed.mjs → build-driver.mjs} +39 -42
- package/bin/geneseed-cli.mjs +33 -32
- package/bin/geneseed-hook.mjs +7 -7
- package/docs/design-history.md +40 -40
- package/docs/extending.md +111 -83
- package/harness.config.json +1 -1
- package/js/README.md +203 -0
- package/js/build/bundle.mjs +231 -0
- package/js/{catalog.mjs → build/catalog.mjs} +7 -7
- package/js/build/emit-claude.mjs +287 -0
- package/js/build/emit-common.mjs +255 -0
- package/js/build/emit-opencode.mjs +214 -0
- package/js/{generate.mjs → build/generate.mjs} +27 -28
- package/js/{render.mjs → build/render.mjs} +13 -9
- package/js/{checkout.mjs → build/source.mjs} +9 -9
- package/js/build/stubs.mjs +256 -0
- package/js/{themes.mjs → build/themes.mjs} +24 -24
- package/js/build/version.mjs +155 -0
- package/js/{hooks.mjs → hosts/hooks.mjs} +47 -44
- package/js/{hosts.mjs → hosts/hosts.mjs} +30 -28
- package/js/{installs.mjs → hosts/installs.mjs} +18 -16
- package/js/{link.mjs → hosts/link.mjs} +29 -28
- package/js/{mcp.mjs → hosts/mcp.mjs} +10 -9
- package/js/{native.mjs → hosts/native.mjs} +24 -22
- package/js/{opencode.mjs → hosts/opencode.mjs} +12 -7
- package/js/{settings.mjs → hosts/settings.mjs} +63 -55
- package/js/inspect/checks-authoring.mjs +761 -0
- package/js/inspect/checks-build.mjs +231 -0
- package/js/inspect/checks-repo.mjs +308 -0
- package/js/{diff.mjs → inspect/diff.mjs} +34 -31
- package/js/inspect/doctor.mjs +286 -0
- package/js/{excludes.mjs → inspect/excludes.mjs} +30 -33
- package/js/{inventory.mjs → inspect/inventory.mjs} +20 -17
- package/js/{registry.mjs → inspect/registry.mjs} +12 -10
- package/js/inspect/scan.mjs +125 -0
- package/js/{status.mjs → inspect/status.mjs} +27 -25
- package/js/inspect/validate.mjs +199 -0
- package/js/lib/fs.mjs +37 -759
- package/js/lib/json.mjs +344 -0
- package/js/lib/paths.mjs +242 -0
- package/js/lib/proc.mjs +3 -3
- package/js/lib/text.mjs +181 -0
- package/js/lib/udiff.mjs +1 -1
- package/js/{memory.mjs → maintain/memory.mjs} +10 -10
- package/js/{migrate.mjs → maintain/migrate.mjs} +28 -26
- package/js/{setup.mjs → maintain/setup.mjs} +34 -32
- package/js/{uninstall.mjs → maintain/uninstall.mjs} +55 -56
- package/js/{update.mjs → maintain/update.mjs} +51 -51
- package/js/{anim.mjs → ui/anim.mjs} +30 -28
- package/js/{cli.mjs → ui/cli.mjs} +10 -10
- package/js/{menu.mjs → ui/menu.mjs} +10 -9
- package/js/{tui.mjs → ui/tui.mjs} +36 -34
- package/js/web/actions.mjs +84 -85
- package/js/web/activity.mjs +22 -21
- package/js/web/api.mjs +72 -50
- package/js/web/daemon.mjs +319 -0
- package/js/web/docs.mjs +24 -21
- package/js/web/graph.mjs +0 -0
- package/js/web/handler.mjs +442 -0
- package/js/web/jobs.mjs +21 -19
- package/js/web/routes.mjs +223 -0
- package/js/web/server.mjs +56 -987
- package/package.json +11 -3
- package/src/doctrines/README.md +1 -1
- package/themes/opencode/README.md +1 -1
- package/web/dist/assets/{About-DyK7yRy7.js → About-CPvIFZGh.js} +1 -1
- package/web/dist/assets/{Activity-D_sYBlW-.js → Activity-z0uXdS7J.js} +1 -1
- package/web/dist/assets/{ActivityDetail-BomKcMhy.js → ActivityDetail-CEj5IJpF.js} +1 -1
- package/web/dist/assets/{Diff-BSvi1WUg.js → Diff-BMR8fPuG.js} +1 -1
- package/web/dist/assets/{Doctor-Cz1s-y1n.js → Doctor-Bz5qE4r9.js} +1 -1
- package/web/dist/assets/{Graph-CuntzFJ0.js → Graph-BV6a6t2l.js} +1 -1
- package/web/dist/assets/{Harnesses-DgRyOviS.js → Harnesses-C5Ag5PaQ.js} +1 -1
- package/web/dist/assets/{Laws-CWk3Mpri.js → Laws-BK07NKw9.js} +1 -1
- package/web/dist/assets/{Library-DXhTN9ju.js → Library-CiXN9sC3.js} +1 -1
- package/web/dist/assets/{Markdown-BoiZcdUl.js → Markdown-DG6FjUz-.js} +1 -1
- package/web/dist/assets/{Profile-DfCHlZ03.js → Profile-B2oSvdxH.js} +1 -1
- package/web/dist/assets/{Rules-eND7hJv9.js → Rules-DobVPd80.js} +1 -1
- package/web/dist/assets/{Skills-CDl9opZO.js → Skills-DQPsvsn9.js} +1 -1
- package/web/dist/assets/{StatusBadge-DSDtcArX.js → StatusBadge-BhJ3HhdI.js} +1 -1
- package/web/dist/assets/{Themes-CoAWuIeM.js → Themes-BP38FBU9.js} +1 -1
- package/web/dist/assets/{activity-Dmb0FTAx.js → activity-D9mfv7pY.js} +1 -1
- package/web/dist/assets/index-BE226Gv6.css +1 -0
- package/web/dist/assets/{index-jrY4m9jP.js → index-BZ7cPLMI.js} +1 -1
- package/web/dist/assets/index-BgIs5eIa.js +12 -0
- package/web/dist/assets/{index-Dg58ScBe.js → index-DGM5j2hB.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +1 -1
- package/docs/declined.md +0 -200
- package/docs/limits.md +0 -329
- package/js/doctor.mjs +0 -1750
- package/js/emit.mjs +0 -1411
- package/web/dist/assets/index-B-mXquBZ.css +0 -1
- package/web/dist/assets/index-CH_Z725C.js +0 -12
|
@@ -7,16 +7,20 @@
|
|
|
7
7
|
* last is that it is also the unit the RUNTIME drives: eleven of its names have a consumer
|
|
8
8
|
* outside the emit tree, ten of them in `rituals/_harness_mcp.py` (remerge, deactivate,
|
|
9
9
|
* reactivate, uninstall), two in `rituals/_harness_exclude.py`, one in
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Every emit and every lifecycle verb goes through this module:
|
|
11
|
+
* `js/build/emit-opencode.mjs` and `js/build/emit-claude.mjs` wire the host config,
|
|
12
|
+
* `js/maintain/uninstall.mjs` and `js/maintain/migrate.mjs` drive the unwire and the shim
|
|
13
|
+
* migration, `js/inspect/checks-repo.mjs` reads `shimDeadPaths` for the doctor, and
|
|
14
|
+
* `js/web/api.mjs` borrows the same readers for the console.
|
|
12
15
|
*
|
|
13
16
|
* THE STDOUT RULE BINDS HARDEST HERE. The hooks this module writes signal their verdict as
|
|
14
17
|
* a JSON object on stdout and return 0 on EVERY path (`|| exit 0` is not what it looks
|
|
15
18
|
* like — see the shim comment below), so a stray byte printed on a hook path does not make
|
|
16
19
|
* noise, it silently disables a gate. Everything printed here is a generator-time message;
|
|
17
|
-
* the
|
|
18
|
-
*
|
|
19
|
-
* to stderr) is
|
|
20
|
+
* the split between them is asserted absolutely by `tests/unit/settings_jsonc.test.mjs` and
|
|
21
|
+
* `tests/unit/settings_integrity.test.mjs`. The asymmetry (`_warn_commented_jsonc` prints to
|
|
22
|
+
* STDOUT, every other message to stderr) is inherited from `_build_settings.py` and kept
|
|
23
|
+
* deliberately rather than tidied.
|
|
20
24
|
*
|
|
21
25
|
* HOOKS ARE APPEND-ONLY AND FAIL-CLOSED. Nothing here rewrites a hook group in place:
|
|
22
26
|
* `mergeClaudeSettings` appends what is missing and removes only groups the manifest
|
|
@@ -38,10 +42,11 @@ import {
|
|
|
38
42
|
} from 'node:fs';
|
|
39
43
|
import path from 'node:path';
|
|
40
44
|
import os from 'node:os';
|
|
45
|
+
import { printErr, readText, writeText } from '../lib/fs.mjs';
|
|
41
46
|
import {
|
|
42
|
-
jsonDumps, jsonDumpsCompact, jsonDumpsIndent, parseJson,
|
|
43
|
-
|
|
44
|
-
} from '
|
|
47
|
+
jsonDumps, jsonDumpsCompact, jsonDumpsIndent, parseJson, deepEquals, formatRepr,
|
|
48
|
+
indexOfDeepEqual,
|
|
49
|
+
} from '../lib/json.mjs';
|
|
45
50
|
|
|
46
51
|
export const OPENCODE_SCHEMA = 'https://opencode.ai/config.json';
|
|
47
52
|
|
|
@@ -49,7 +54,7 @@ export const OPENCODE_SCHEMA = 'https://opencode.ai/config.json';
|
|
|
49
54
|
*
|
|
50
55
|
* `loaded[event]` where `event` came out of a manifest hands back `Object.prototype`'s
|
|
51
56
|
* member for `constructor`, `toString` or `valueOf`, where Python's `.get` returns None.
|
|
52
|
-
* The same hazard `js/native.mjs` hit with `overrides[stem]`, on a dict the USER edits. */
|
|
57
|
+
* The same hazard `js/hosts/native.mjs` hit with `overrides[stem]`, on a dict the USER edits. */
|
|
53
58
|
function get(obj, key) {
|
|
54
59
|
return Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : undefined;
|
|
55
60
|
}
|
|
@@ -349,7 +354,7 @@ export function mergeOpencodeJson(p, agentPath, doctrines = null, excluded = [])
|
|
|
349
354
|
raw = readText(target);
|
|
350
355
|
} catch (e) {
|
|
351
356
|
if (!isOsError(e)) throw e;
|
|
352
|
-
process.stderr.write(`[geneseed] WARN: could not read ${target} (${
|
|
357
|
+
process.stderr.write(`[geneseed] WARN: could not read ${target} (${asOsError(e)}) `
|
|
353
358
|
+ `— NOT touching it. Add ${jsonDumps(agentPath)} to its "instructions" array by `
|
|
354
359
|
+ "hand once it's readable again.\n");
|
|
355
360
|
return target;
|
|
@@ -367,7 +372,7 @@ export function mergeOpencodeJson(p, agentPath, doctrines = null, excluded = [])
|
|
|
367
372
|
if (!has(config, '$schema')) config.$schema = OPENCODE_SCHEMA;
|
|
368
373
|
let instr = get(config, 'instructions');
|
|
369
374
|
if (!Array.isArray(instr)) instr = [];
|
|
370
|
-
const addInstr =
|
|
375
|
+
const addInstr = indexOfDeepEqual(instr, agentPath) < 0;
|
|
371
376
|
if (addInstr) instr.push(agentPath);
|
|
372
377
|
config.instructions = instr;
|
|
373
378
|
// Absent ⇒ write the whole block; present ⇒ reconcile Geneseed's own git keys. Adding is
|
|
@@ -428,7 +433,7 @@ export function mergeOpencodeJson(p, agentPath, doctrines = null, excluded = [])
|
|
|
428
433
|
atomicWriteJson(target, config);
|
|
429
434
|
} catch (e) {
|
|
430
435
|
if (!isOsError(e)) throw e;
|
|
431
|
-
process.stderr.write(`[geneseed] WARN: could not write ${target} (${
|
|
436
|
+
process.stderr.write(`[geneseed] WARN: could not write ${target} (${asOsError(e)}) — `
|
|
432
437
|
+ `the harness will NOT auto-load until this is fixed. Add ${jsonDumps(agentPath)} `
|
|
433
438
|
+ 'to its "instructions" array by hand.\n');
|
|
434
439
|
}
|
|
@@ -444,7 +449,7 @@ export function mergeOpencodeJson(p, agentPath, doctrines = null, excluded = [])
|
|
|
444
449
|
* with an injectable failure instead and compares everything except the OS's own wording.
|
|
445
450
|
* Named here so the gap is a decision rather than a surprise at GA.
|
|
446
451
|
*/
|
|
447
|
-
function
|
|
452
|
+
function asOsError(e) {
|
|
448
453
|
return e.message;
|
|
449
454
|
}
|
|
450
455
|
|
|
@@ -475,7 +480,7 @@ export const SHIM_ARGV = new Set(['$@', '%*']);
|
|
|
475
480
|
* Every quoted token in a shim body that names a path which is not there — empty is healthy.
|
|
476
481
|
*
|
|
477
482
|
* ONE OWNER, because the rule has two consumers now and a second copy is a second place for the
|
|
478
|
-
* `"$@"` defect above to come back: `js/
|
|
483
|
+
* `"$@"` defect above to come back: `js/inspect/checks-repo.mjs` turns this into the `[shim] … does not
|
|
479
484
|
* exist` report, and `hookPrefix` reads it to decide whether the shim already on disk is worth
|
|
480
485
|
* protecting from a checkout that will not outlive the emit.
|
|
481
486
|
*/
|
|
@@ -501,7 +506,7 @@ export function shimHome() {
|
|
|
501
506
|
}
|
|
502
507
|
|
|
503
508
|
/**
|
|
504
|
-
* `Path(x).expanduser()` — private twin of `js/hosts.mjs`'s `expanduser`, same guard.
|
|
509
|
+
* `Path(x).expanduser()` — private twin of `js/hosts/hosts.mjs`'s `expanduser`, same guard.
|
|
505
510
|
*
|
|
506
511
|
* `GENESEED_HOME` is user-set, same as any config-dir env var there, so a `~user` value here
|
|
507
512
|
* has the identical failure mode (a hook shim written under a literal `~user` directory) and
|
|
@@ -514,7 +519,7 @@ function expanduser(p) {
|
|
|
514
519
|
if (p.startsWith('~')) {
|
|
515
520
|
const msg = `refusing '${p}': a '~user' path is not expanded by this port; pass an `
|
|
516
521
|
+ 'absolute path, or "~" / "~/…" for your own home directory';
|
|
517
|
-
|
|
522
|
+
printErr(`${msg}\n`);
|
|
518
523
|
const e = new Error(msg);
|
|
519
524
|
e.exitCode = 1;
|
|
520
525
|
throw e;
|
|
@@ -536,12 +541,12 @@ export function hookShimPath(platform = process.platform) {
|
|
|
536
541
|
* refuses to compare the emitted shim for that reason (`_SHIM_GLOB`) and asserts
|
|
537
542
|
* `_shim_health` instead.
|
|
538
543
|
*
|
|
539
|
-
* So the
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
* off`, the CRLF, `exit /b` vs `exec`, the
|
|
543
|
-
*
|
|
544
|
-
*
|
|
544
|
+
* So the answer here is NOT "skip it". Taking `runner`, `entry` and `platform` as arguments
|
|
545
|
+
* makes the BODY a pure function of three inputs, which is what lets
|
|
546
|
+
* `tests/unit/hook_form.test.mjs` assert BOTH platform shapes absolutely in one run on
|
|
547
|
+
* whichever host it happens to be on: the `@echo off`, the CRLF, `exit /b` vs `exec`, the
|
|
548
|
+
* quoting that keeps `--root "<cfg>"` intact. What it does not prove is WHICH values the
|
|
549
|
+
* driver passes — that is `bin/build-driver.mjs`'s `hookRunnerEntry()`, gated separately.
|
|
545
550
|
*/
|
|
546
551
|
export function hookShimBody(runner, entry, platform = process.platform) {
|
|
547
552
|
if (platform === 'win32') {
|
|
@@ -757,7 +762,7 @@ export function claudeHookGroups(cfg, hookOpts, doctrines = null, excluded = [])
|
|
|
757
762
|
*
|
|
758
763
|
* `scope` is accepted and unused, exactly as in the Python.
|
|
759
764
|
*/
|
|
760
|
-
export function mergeClaudeSettings(p,
|
|
765
|
+
export function mergeClaudeSettings(p, _scope = 'global', priorHooks = null, hookOpts = {},
|
|
761
766
|
doctrines = null, excluded = []) {
|
|
762
767
|
const prior = (priorHooks || []).filter(isDict);
|
|
763
768
|
let config = {};
|
|
@@ -785,12 +790,12 @@ export function mergeClaudeSettings(p, scope = 'global', priorHooks = null, hook
|
|
|
785
790
|
}
|
|
786
791
|
let pruned = false;
|
|
787
792
|
for (const rec of prior) {
|
|
788
|
-
if (
|
|
793
|
+
if (indexOfDeepEqual(canonFlat, rec) >= 0) continue;
|
|
789
794
|
const event = get(rec, 'event');
|
|
790
795
|
const group = get(rec, 'group');
|
|
791
796
|
const arr = get(hooks, event);
|
|
792
797
|
if (Array.isArray(arr)) {
|
|
793
|
-
const at =
|
|
798
|
+
const at = indexOfDeepEqual(arr, group);
|
|
794
799
|
if (at >= 0) {
|
|
795
800
|
arr.splice(at, 1);
|
|
796
801
|
pruned = true;
|
|
@@ -803,14 +808,14 @@ export function mergeClaudeSettings(p, scope = 'global', priorHooks = null, hook
|
|
|
803
808
|
let arr = get(hooks, event);
|
|
804
809
|
if (!Array.isArray(arr)) arr = [];
|
|
805
810
|
for (const g of newGroups) {
|
|
806
|
-
if (
|
|
811
|
+
if (indexOfDeepEqual(arr, g) >= 0) continue;
|
|
807
812
|
arr.push(g);
|
|
808
813
|
added.push({ event, group: g });
|
|
809
814
|
}
|
|
810
815
|
hooks[event] = arr;
|
|
811
816
|
}
|
|
812
|
-
const survivors = prior.filter((r) =>
|
|
813
|
-
const managedNow = survivors.concat(added.filter((a) =>
|
|
817
|
+
const survivors = prior.filter((r) => indexOfDeepEqual(canonFlat, r) >= 0);
|
|
818
|
+
const managedNow = survivors.concat(added.filter((a) => indexOfDeepEqual(survivors, a) < 0));
|
|
814
819
|
if (!added.length && !pruned) return [p, managedNow];
|
|
815
820
|
if (hadComments) {
|
|
816
821
|
process.stderr.write(`[geneseed] ${path.basename(p)} has comments — not rewriting it `
|
|
@@ -822,8 +827,10 @@ export function mergeClaudeSettings(p, scope = 'global', priorHooks = null, hook
|
|
|
822
827
|
// carries four events, so by the time control reaches here `hooks` is never empty. A
|
|
823
828
|
// mutation keeping the empty block instead of deleting it is the one of thirty-three that
|
|
824
829
|
// stays green, and it stays green because there is nothing to detect — recorded the way
|
|
825
|
-
// `themed_rel`
|
|
826
|
-
//
|
|
830
|
+
// `themed_rel` is, so "the gate cannot see it" and "there is nothing to see" keep their
|
|
831
|
+
// distance. NOT the way `lstripNewlines` is: that one looked unobservable for two phases
|
|
832
|
+
// and turned out to be mutation M7, killed by `golden.mjs --idempotent` through
|
|
833
|
+
// `managedBlockWrite`'s `updated` branch, which only a RE-emit reaches.
|
|
827
834
|
if (Object.keys(hooks).length) config.hooks = hooks;
|
|
828
835
|
else delete config.hooks;
|
|
829
836
|
mkdirSync(path.dirname(p), { recursive: true });
|
|
@@ -855,7 +862,7 @@ export function unwireClaudeSettings(p, added) {
|
|
|
855
862
|
const group = get(rec, 'group');
|
|
856
863
|
let arr = get(hooks, event);
|
|
857
864
|
if (Array.isArray(arr)) {
|
|
858
|
-
const at =
|
|
865
|
+
const at = indexOfDeepEqual(arr, group);
|
|
859
866
|
if (at >= 0) arr.splice(at, 1);
|
|
860
867
|
}
|
|
861
868
|
arr = get(hooks, event);
|
|
@@ -896,7 +903,7 @@ export function wireClaudeExcludes(p, excludes) {
|
|
|
896
903
|
}
|
|
897
904
|
let cur = get(config, 'claudeMdExcludes');
|
|
898
905
|
if (!Array.isArray(cur)) cur = [];
|
|
899
|
-
const added = want.filter((e) =>
|
|
906
|
+
const added = want.filter((e) => indexOfDeepEqual(cur, e) < 0);
|
|
900
907
|
if (!added.length) return [];
|
|
901
908
|
if (hadComments) {
|
|
902
909
|
process.stderr.write(`[geneseed] ${path.basename(p)} has comments — not rewriting it `
|
|
@@ -911,7 +918,7 @@ export function wireClaudeExcludes(p, excludes) {
|
|
|
911
918
|
atomicWriteJson(p, config);
|
|
912
919
|
} catch (e) {
|
|
913
920
|
if (!isOsError(e)) throw e;
|
|
914
|
-
process.stderr.write(`[geneseed] WARN: could not write ${p} (${
|
|
921
|
+
process.stderr.write(`[geneseed] WARN: could not write ${p} (${asOsError(e)}) — `
|
|
915
922
|
+ 'claudeMdExcludes were not wired. Add to its "claudeMdExcludes" array by hand: '
|
|
916
923
|
+ `${jsonDumpsCompact(added)}\n`);
|
|
917
924
|
return [];
|
|
@@ -934,7 +941,7 @@ export function unwireClaudeExcludes(p, excludes) {
|
|
|
934
941
|
const cur = get(loaded, 'claudeMdExcludes');
|
|
935
942
|
if (!Array.isArray(cur)) return;
|
|
936
943
|
for (const e of excludes) {
|
|
937
|
-
const at =
|
|
944
|
+
const at = indexOfDeepEqual(cur, e);
|
|
938
945
|
if (at >= 0) cur.splice(at, 1);
|
|
939
946
|
}
|
|
940
947
|
if (cur.length) loaded.claudeMdExcludes = cur;
|
|
@@ -971,8 +978,9 @@ export const SHIM_ENTRY_MARK = 'geneseed-hook.mjs';
|
|
|
971
978
|
/**
|
|
972
979
|
* `_build_settings._migrate_shape` — 'legacy' | 'current' | 'none'.
|
|
973
980
|
*
|
|
974
|
-
* PURE
|
|
975
|
-
*
|
|
981
|
+
* PURE — and currently reached only END TO END, by the `migrate/a-node-baked-shim-reads-as-current`
|
|
982
|
+
* cell. No unit corpus drives it directly, which is worth knowing before trusting it.
|
|
983
|
+
* 'none' is not a fault: a Copilot install wires no hooks, and a user's own
|
|
976
984
|
* settings.json may carry three hand-written hooks and no Geneseed entry. Both must read as
|
|
977
985
|
* "nothing to migrate" rather than "unrecognised", or `migrate` refuses on the commonest
|
|
978
986
|
* config on any machine.
|
|
@@ -1060,7 +1068,7 @@ export function settingsIntegrityCheck(p, managed, expect = 'present') {
|
|
|
1060
1068
|
[loaded] = readJsonc(readText(p));
|
|
1061
1069
|
} catch (e) {
|
|
1062
1070
|
if (!isOsError(e)) throw e;
|
|
1063
|
-
problems.push(`${p}: could not read the file (${
|
|
1071
|
+
problems.push(`${p}: could not read the file (${asOsError(e)})`);
|
|
1064
1072
|
return flush();
|
|
1065
1073
|
}
|
|
1066
1074
|
if (!isDict(loaded)) {
|
|
@@ -1077,25 +1085,25 @@ export function settingsIntegrityCheck(p, managed, expect = 'present') {
|
|
|
1077
1085
|
for (const rec of recordedHooks) {
|
|
1078
1086
|
const event = get(rec, 'event');
|
|
1079
1087
|
const group = get(rec, 'group');
|
|
1080
|
-
const hit = presentGroups.some(([e, g]) =>
|
|
1088
|
+
const hit = presentGroups.some(([e, g]) => deepEquals(e, event) && deepEquals(g, group));
|
|
1081
1089
|
if (expect === 'present' && !hit) {
|
|
1082
|
-
problems.push(`${p}: recorded hook group missing — event=${
|
|
1090
|
+
problems.push(`${p}: recorded hook group missing — event=${formatRepr(event ?? null)} `
|
|
1083
1091
|
+ `group=${jsonDumpsCompact(group ?? null)}`);
|
|
1084
1092
|
} else if (expect === 'absent' && hit) {
|
|
1085
1093
|
problems.push(`${p}: recorded hook group still present after unwire — `
|
|
1086
|
-
+ `event=${
|
|
1094
|
+
+ `event=${formatRepr(event ?? null)} group=${jsonDumpsCompact(group ?? null)}`);
|
|
1087
1095
|
}
|
|
1088
1096
|
}
|
|
1089
1097
|
|
|
1090
1098
|
let exclCur = get(loaded, 'claudeMdExcludes');
|
|
1091
1099
|
exclCur = Array.isArray(exclCur) ? exclCur : [];
|
|
1092
1100
|
for (const entry of (get(m, 'settings_excludes') || [])) {
|
|
1093
|
-
const hit =
|
|
1101
|
+
const hit = indexOfDeepEqual(exclCur, entry) >= 0;
|
|
1094
1102
|
if (expect === 'present' && !hit) {
|
|
1095
|
-
problems.push(`${p}: recorded claudeMdExcludes entry missing: ${
|
|
1103
|
+
problems.push(`${p}: recorded claudeMdExcludes entry missing: ${formatRepr(entry)}`);
|
|
1096
1104
|
} else if (expect === 'absent' && hit) {
|
|
1097
1105
|
problems.push(`${p}: recorded claudeMdExcludes entry still present after unwire: `
|
|
1098
|
-
+ `${
|
|
1106
|
+
+ `${formatRepr(entry)}`);
|
|
1099
1107
|
}
|
|
1100
1108
|
}
|
|
1101
1109
|
|
|
@@ -1103,17 +1111,17 @@ export function settingsIntegrityCheck(p, managed, expect = 'present') {
|
|
|
1103
1111
|
// auto-delete (they may be user-authored, or a claim this run legitimately did not
|
|
1104
1112
|
// inherit).
|
|
1105
1113
|
const recordedSet = new Set(recordedHooks.map(
|
|
1106
|
-
(r) => `${
|
|
1114
|
+
(r) => `${formatRepr(get(r, 'event') ?? null)}\u0000`
|
|
1107
1115
|
+ `${jsonDumpsCompact(get(r, 'group') ?? null, { sortKeys: true })}`));
|
|
1108
1116
|
for (const [event, group] of presentGroups) {
|
|
1109
|
-
const key = `${
|
|
1117
|
+
const key = `${formatRepr(event)}\u0000${jsonDumpsCompact(group, { sortKeys: true })}`;
|
|
1110
1118
|
if (recordedSet.has(key)) continue;
|
|
1111
1119
|
const cmds = (get(group, 'hooks') || []).filter(isDict)
|
|
1112
1120
|
.map((h) => (has(h, 'command') ? h.command : ''));
|
|
1113
1121
|
if (cmds.some((c) => typeof c === 'string'
|
|
1114
1122
|
&& GENESEED_HOOK_SNIFF.some((mk) => c.includes(mk)))) {
|
|
1115
1123
|
problems.push(`${p}: Geneseed-pattern hook present but NOT recorded in the manifest `
|
|
1116
|
-
+ `(event=${
|
|
1124
|
+
+ `(event=${formatRepr(event)}) — possibly user-authored; left alone`);
|
|
1117
1125
|
}
|
|
1118
1126
|
}
|
|
1119
1127
|
|
|
@@ -1133,7 +1141,7 @@ function delimiters(blockId) {
|
|
|
1133
1141
|
*/
|
|
1134
1142
|
export function managedBlockWrite(p, content, blockId = 'GENESEED') {
|
|
1135
1143
|
const [begin, end] = delimiters(blockId);
|
|
1136
|
-
const block = `${begin}\n${
|
|
1144
|
+
const block = `${begin}\n${stripEnd(content)}\n${end}\n`;
|
|
1137
1145
|
if (!existsSync(p)) {
|
|
1138
1146
|
mkdirSync(path.dirname(p), { recursive: true });
|
|
1139
1147
|
writeText(p, block);
|
|
@@ -1163,7 +1171,7 @@ export function managedBlockRemove(p, blockId = 'GENESEED', whole = false) {
|
|
|
1163
1171
|
if (!existing.includes(begin) || !existing.includes(end)) return;
|
|
1164
1172
|
const pre = existing.split(begin)[0];
|
|
1165
1173
|
const post = existing.slice(existing.indexOf(end) + end.length);
|
|
1166
|
-
const rest =
|
|
1174
|
+
const rest = stripEnds(`${stripTrailingNewlines(pre)}\n${lstripNewlines(post)}`);
|
|
1167
1175
|
if (rest) writeText(p, `${rest}\n`);
|
|
1168
1176
|
else unlinkSync(p);
|
|
1169
1177
|
}
|
|
@@ -1183,17 +1191,17 @@ export function managedBlockRead(p, blockId = 'GENESEED') {
|
|
|
1183
1191
|
// U+001C–U+001F, which `trim()` leaves. A managed block is user-editable prose in a file
|
|
1184
1192
|
// Geneseed shares with the user, so the set is spelled out rather than approximated — and
|
|
1185
1193
|
// spelled with escapes, never literals: a raw U+2028 in a JS source file ends the line.
|
|
1186
|
-
const
|
|
1194
|
+
const WHITESPACE = '[\t\n\v\f\r\x1c-\x1f \x85\xa0\u1680\u2000-\u200a'
|
|
1187
1195
|
+ '\u2028\u2029\u202f\u205f\u3000]';
|
|
1188
1196
|
|
|
1189
|
-
const RSTRIP_RE = new RegExp(`${
|
|
1190
|
-
const LSTRIP_RE = new RegExp(`^${
|
|
1197
|
+
const RSTRIP_RE = new RegExp(`${WHITESPACE}+$`);
|
|
1198
|
+
const LSTRIP_RE = new RegExp(`^${WHITESPACE}+`);
|
|
1191
1199
|
|
|
1192
|
-
function
|
|
1200
|
+
function stripEnd(s) {
|
|
1193
1201
|
return s.replace(RSTRIP_RE, '');
|
|
1194
1202
|
}
|
|
1195
1203
|
|
|
1196
|
-
function
|
|
1204
|
+
function stripEnds(s) {
|
|
1197
1205
|
return s.replace(LSTRIP_RE, '').replace(RSTRIP_RE, '');
|
|
1198
1206
|
}
|
|
1199
1207
|
|
|
@@ -1201,10 +1209,10 @@ function lstripNewlines(s) {
|
|
|
1201
1209
|
return s.replace(/^\n+/, '');
|
|
1202
1210
|
}
|
|
1203
1211
|
|
|
1204
|
-
function
|
|
1212
|
+
function stripTrailingNewlines(s) {
|
|
1205
1213
|
return s.replace(/\n+$/, '');
|
|
1206
1214
|
}
|
|
1207
1215
|
|
|
1208
1216
|
function stripNewlines(s) {
|
|
1209
|
-
return lstripNewlines(
|
|
1217
|
+
return lstripNewlines(stripTrailingNewlines(s));
|
|
1210
1218
|
}
|