MergeSettings: nested __merge annotations under a user-only key are written raw into settings.json
I maintainer di solito rispondono entro 7 giorni
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 67/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- bun, typescript
- Ambito
- backend
Direzione di ricerca
Inizia da LIFEOS/TOOLS/MergeSettings.ts:202-209, dove vengono uniti o clonati i valori presenti solo nell’utente, e segui come vengono gestite le annotazioni __merge annidate. Riproduci il problema con il comando Bun fornito, poi controlla i casi suggeriti in LIFEOS/TOOLS/MergeSettings.test.ts se viene aggiunto quel file di test. Il lavoro è completato quando le annotazioni Append e Remove presenti solo nell’utente vengono risolte nel risultato e i valori semplici restano invariati.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Version
LifeOS 8.761.0 · attestation: 66 system file(s) differ from danielmiessler/[email protected]:
- hooks/IsaCodeSyncGate.hook.ts
- hooks/lib/presence-context.ts
- hooks/lib/micro-integrity.ts
- hooks/lib/blocker-text.ts
- hooks/lib/herdr-tabs.ts
- hooks/lib/work-kind.ts
- hooks/BlockerText.hook.ts
- hooks/StopGates.hook.ts
- skills/LifeOSHooks/hooks/LifeOSHooks.ts
- skills/VideoEssay/Tools/Theme.ts
- skills/VideoEssay/Intents/Explainer.md
- skills/Fabric/Patterns/create_better_frame/system.md
- skills/Fabric/Patterns/extract_article_wisdom/README.md
- skills/Fabric/Patterns/extract_wisdom/README.md
- skills/Fabric/Patterns/rate_value/README.md
- skills/Fabric/Patterns/extract_product_features/README.md
- skills/Fabric/Patterns/show_fabric_options_markmap/system.md
- commands/ew.md
- commands/cv.md
- commands/pv.md
- commands/r.md
- LIFEOS/TOOLS/ReleaseRecord.ts
- LIFEOS/TOOLS/MergeSettings.ts
- LIFEOS/TOOLS/ReviewSuite.ts
- LIFEOS/TOOLS/lib/DenyHistoryScan.ts
- LIFEOS/TOOLS/GenerateTelosSummary.ts
- LIFEOS/TOOLS/VoiceMaster.ts
- LIFEOS/TOOLS/ReviewCases.ts
- LIFEOS/TOOLS/model-pricing.json
- LIFEOS/TOOLS/CommitmentDetect.ts
- LIFEOS/DOCUMENTATION/SystemUserBoundary.md
- LIFEOS/DOCUMENTATION/Statvs/StatvsSystem.md
- LIFEOS/DOCUMENTATION/Assay/AssaySystem.md
- LIFEOS/DOCUMENTATION/Notifications/NotificationSystem.md
- LIFEOS/PULSE/VoiceServer/voice.ts
- LIFEOS/HERMES/Policy.ts
- LIFEOS/ASSAY/Report.ts
- LIFEOS/ASSAY/ReviewEntry.ts
- LIFEOS/RELAY/context/HeadsUp.ts
- LIFEOS/RELAY/context/LooseEnds.ts
- …and 26 more
What is broken
When a key exists only in the user overlay (settings.user.json) and its value is an object, mergeSettings clones that object verbatim instead of merging it, so any {"__merge":"append"} or {"__merge":"remove"} annotation nested inside it is never resolved and is written raw into settings.json. The common trigger is hooks: settings.system.json ships no hooks key, so an overlay with hooks.PreToolUse as an append wrapper produces a settings.json whose PreToolUse is the literal wrapper object rather than an array, and the harness ignores or rejects those hooks. MergeSettings --check then reports a mismatch on every SessionStart until the overlay is hand-flattened.
Where (file:line)
LIFEOS/TOOLS/MergeSettings.ts:209 (reached from :202)
Repro on a clean tree
On a clean 8.761.0 install (MergeSettings.ts 1.3.16), from the install root:
bun -e 'import { mergeSettings } from "./LIFEOS/TOOLS/MergeSettings.ts";
const m = mergeSettings({ env: {} }, { hooks: { PreToolUse: { __merge: "append", values: [{ matcher: "Bash", hooks: [] }] } } });
console.log(JSON.stringify(m.hooks.PreToolUse)); process.exit(Array.isArray(m.hooks.PreToolUse) ? 0 : 1);'
Expected: `[{"matcher":"Bash","hooks":[]}]`, exit 0.
Actual: `{"__merge":"append","values":[{"matcher":"Bash","hooks":[]}]}`, exit 1.
Same result end to end: put that `hooks.PreToolUse` wrapper in LIFEOS/USER/CONFIG/settings.user.json and run MergeSettings; settings.json carries the wrapper.
Cause: for a user-only key, mergeSettings calls `mergeSettings(undefined, user[key])`; the value is an object but not an annotation, the system side is not an object, so it falls through to `return cloneJsonValue(user)`.
Negative control
Probe: bun -e 'import { mergeSettings } from "./LIFEOS/TOOLS/MergeSettingsRelease.probe.ts"; const m = mergeSettings({ env: {} }, { hooks: { PreToolUse: { __merge: "append", values: [{ matcher: "Bash", hooks: [] }] } } }); console.log(JSON.stringify(m.hooks.PreToolUse)); process.exit(Array.isArray(m.hooks.PreToolUse) ? 0 : 1);' · exit 1 · captured 2026-11-06T21:00:09.379Z
Red looks like: {"__merge":"append","values":
{"__merge":"append","values":[{"matcher":"Bash","hooks":[]}]}
Suggested fix
At the end of mergeSettings, before the final clone, merge a user object against an empty layer: if (isObjectRecord(user)) return mergeSettings({}, user);. Nested append/remove annotations then resolve at any depth, plain values come out unchanged, and an invalid nested annotation still throws. The attached diff adds this plus MergeSettings.test.ts (append under a user-only key, the SessionStart preserving merge, remove under a user-only key, a user object replacing a system scalar, plain passthrough, invalid nested annotation). Running locally as a patch on 8.761.0; 6/6 tests pass and settings.json now matches the overlay whether PreToolUse is wrapped or flat.
diff --git a/LIFEOS/TOOLS/MergeSettings.test.ts b/LIFEOS/TOOLS/MergeSettings.test.ts
new file mode 100644
index 0000000..2557ad1
--- /dev/null
+++ b/LIFEOS/TOOLS/MergeSettings.test.ts
@@ -0,0 +1,41 @@
+import { describe, expect, test } from "bun:test";
+import { mergeSettings, mergeSettingsPreserving } from "./MergeSettings";
+
+// Local patch 1.3.17: annotations nested under a user-only key must resolve even when the
+// system layer has no such key (2026-11-06: hooks.PreToolUse wrapped in append, no system hooks).
+describe("user-only objects resolve nested merge annotations", () => {
+ const hookA = { matcher: "Bash", hooks: [{ type: "command", command: "a" }] };
+ const hookB = { matcher: "*", hooks: [{ type: "command", command: "b" }] };
+
+ test("hooks.PreToolUse wrapped in append with no system hooks becomes a plain array", () => {
+ const system = { env: { X: "1" } };
+ const user = { hooks: { PreToolUse: { __merge: "append", values: [hookA, hookB] } } };
+ const merged = mergeSettings(system, user);
+ expect(merged.hooks.PreToolUse).toEqual([hookA, hookB]);
+ expect(JSON.stringify(merged)).not.toContain("__merge");
+ });
+
+ test("the preserving merge used at SessionStart resolves it too", () => {
+ const merged = mergeSettingsPreserving({}, { hooks: { PreToolUse: { __merge: "append", values: [hookA] } } }, { hooks: {} });
+ expect(merged.hooks.PreToolUse).toEqual([hookA]);
+ });
+
+ test("remove annotations under a user-only key are dropped", () => {
+ const merged = mergeSettings({}, { hooks: { Stop: { __merge: "remove" }, PreToolUse: [hookA] } });
+ expect(merged.hooks).toEqual({ PreToolUse: [hookA] });
+ });
+
+ test("a user object replacing a system scalar still resolves its annotations", () => {
+ const merged = mergeSettings({ statusLine: "off" }, { statusLine: { list: { __merge: "append", values: [1] } } });
+ expect(merged.statusLine).toEqual({ list: [1] });
+ });
+
+ test("plain user-only values are unchanged", () => {
+ const user = { hooks: { PreToolUse: [hookA] }, flag: true, list: [1, { a: 2 }] };
+ expect(mergeSettings({}, user)).toEqual(user);
+ });
+
+ test("an invalid nested annotation still throws", () => {
+ expect(() => mergeSettings({}, { hooks: { PreToolUse: { __merge: "prepend", values: [] } } })).toThrow();
+ });
+});
diff --git a/LIFEOS/TOOLS/MergeSettings.ts b/LIFEOS/TOOLS/MergeSettings.ts
index 4e673d8..83822db 100755
--- a/LIFEOS/TOOLS/MergeSettings.ts
+++ b/LIFEOS/TOOLS/MergeSettings.ts
@@ -1,7 +1,11 @@
#!/usr/bin/env bun
/**
- * @version 1.3.16
+ * @version 1.3.17
+ * 1.3.17 (2026-11-06, local patch): a user-only object (no system counterpart, or a non-object one)
+ * is merged against an empty object instead of cloned verbatim, so nested append/remove annotations
+ * resolve even when the system layer has no such key (e.g. hooks.PreToolUse wrapped in append while
+ * settings.system.json ships no `hooks`). Before, the wrapper was written into settings.json as-is.
* Deep-merges a system settings JSON value with a user overlay.
* Objects merge recursively while preserving system key order.
* New user-only object keys are appended after system keys at each level.
@@ -206,6 +210,12 @@ export function mergeSettings(system: any, user: any): any {
return merged;
}
+ // A user object with no object to merge into still carries annotations at any depth; resolve
+ // them against an empty layer rather than cloning the raw overlay (local patch 1.3.17).
+ if (isObjectRecord(user)) {
+ return mergeSettings({}, user);
+ }
+
return cloneJsonValue(user);
}
Environment
- OS: macOS 26.6.2 (arm64)
- Harness: claude-code
- bun: 1.3.14 · git: 2.46.0
- Doctor broken: hook-interpreters
- Lingua principale
- TypeScript
- Stelle
- 19.7k
- Fork
- 2.5k
- Metriche di merge delle PR
- Nessuna PR unita negli ultimi 30g
Preparare l'ambiente
Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di danielmiessler/LifeOS
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 88/100
danielmiessler/LifeOS#2639 ·
I maintainer di solito rispondono entro 7 giorni
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
danielmiessler/LifeOS#2635 ·
I maintainer di solito rispondono entro 7 giorni
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
danielmiessler/LifeOS#2625 ·
I maintainer di solito rispondono entro 7 giorni
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 55/100
danielmiessler/LifeOS#2646 ·
I maintainer di solito rispondono entro 7 giorni
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 55/100
danielmiessler/LifeOS#2645 ·
I maintainer di solito rispondono entro 7 giorni
Tutte le issue di danielmiessler/LifeOS
Issue simili
-
twilio.webhook() rejects valid requests when the query string has an encoded `?` and a `+` spaceAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
twilio/twilio-node#1232 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 86/100
I maintainer di solito rispondono entro 1 giorno
-
Tiling managers are not destroyed in disable(), so window-created handlers keep running (Gio.Settings constructed without schema)Forse già presa @gnacho l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
domferr/tilingshell#641 ·
I maintainer di solito rispondono entro 3 giorni
-
🐛 bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
LibreChat-AI/LibreChat#17887 ·
I maintainer di solito rispondono entro 1 giorno
-
`isCI` is `true` inside a running Google Cloud Run service (also Railway, Render, Deno Deploy)Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 86/100