From 9060b9253dbe0c5ce5a2b627ee833caa10eb91e7 Mon Sep 17 00:00:00 2001 From: sauldataman Date: Mon, 6 Jul 2026 14:47:40 +0800 Subject: [PATCH] fix(install): write standing settings.system.json snapshot on hooks install (#1402) install/settings.system.json is only a merge source; nothing ever wrote a standing copy to ~/.claude/settings.system.json, while MemoryHealthCheck CHECK 3 requires that path to exist and to register the four memory hooks. Every fresh install therefore reported CRITICAL from the first health run. InstallHooks.ts --apply now writes the snapshot (system half + the payload's own hooks -- incoming, not merged, so a user's foreign entries are never frozen into system truth). Dry-run unchanged; idempotent across re-applies. Both the skill source and the install payload mirror are updated, plus the Setup.md step-7 doc line. --- LifeOS/Tools/InstallHooks.ts | 13 ++++++++++++- LifeOS/Workflows/Setup.md | 2 +- LifeOS/install/skills/LifeOS/Tools/InstallHooks.ts | 13 ++++++++++++- LifeOS/install/skills/LifeOS/Workflows/Setup.md | 2 +- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/LifeOS/Tools/InstallHooks.ts b/LifeOS/Tools/InstallHooks.ts index 030fb9218..7307db264 100755 --- a/LifeOS/Tools/InstallHooks.ts +++ b/LifeOS/Tools/InstallHooks.ts @@ -101,7 +101,18 @@ function main(): void { cpSync(hooksPayloadDir, hooksDestDir, { recursive: true }); const hookFilesCopied = countFilesRec(hooksDestDir); - console.log(JSON.stringify({ ...report, written: true, backup, hookFilesCopied }, null, 2)); + // Standing source-of-truth snapshot read by MemoryHealthCheck CHECK 3. + // hooks = incoming (system wiring), not merged: never freeze a user's foreign entries in. + const systemHalfPath = join(skillRoot, "install", "settings.system.json"); + const sotPath = join(configRoot, "settings.system.json"); + let sot: Record = {}; + if (existsSync(systemHalfPath)) { + try { sot = JSON.parse(readFileSync(systemHalfPath, "utf-8")); } catch { sot = {}; } + } + sot.hooks = incoming; + writeFileSync(sotPath, JSON.stringify(sot, null, 2) + "\n"); + + console.log(JSON.stringify({ ...report, written: true, backup, hookFilesCopied, settingsSystemPath: sotPath }, null, 2)); process.exit(0); } diff --git a/LifeOS/Workflows/Setup.md b/LifeOS/Workflows/Setup.md index 6e4132150..64578e462 100644 --- a/LifeOS/Workflows/Setup.md +++ b/LifeOS/Workflows/Setup.md @@ -48,7 +48,7 @@ The skill ships everything for both tiers in its payload; nothing activates with | `pulse` | the Pulse dashboard as a `launchd` service (`:31337`) | optional | | `worksweep` / `derivedsync` | background `launchd` jobs | optional | - - **hooks → `bun Tools/InstallHooks.ts`** (trust-gated): reads `install/hooks/hooks.json`, shows the EXACT change (file + settings-entry + event count), waits for explicit permission, backs up `settings.json`, merges additively per matcher bucket (idempotent via normalized-command dedup, preserves `type:"http"` verbatim). + - **hooks → `bun Tools/InstallHooks.ts`** (trust-gated): reads `install/hooks/hooks.json`, shows the EXACT change (file + settings-entry + event count), waits for explicit permission, backs up `settings.json`, merges additively per matcher bucket (idempotent via normalized-command dedup, preserves `type:"http"` verbatim), and writes the standing source-of-truth snapshot to `~/.claude/settings.system.json` (system half + payload hooks) so `MemoryHealthCheck` CHECK 3 passes on fresh installs (#1402). - **everything else → `bun Tools/DeployComponents.ts`**: dry-run first (no `--apply`, `--all` shows the full plan), then `--apply --components ` with ONLY what the user picked. Reads enhancement settings from `install/settings.enhancements.json` (the keys split out of `settings.system.json` so they're genuinely opt-in, not force-bundled). A component whose prerequisite is absent reports a LOUD blocker and fails — never a silent no-op. macOS-only for `launchd`; skip silently on Linux/headless (`DetectEnv.display` false). - **Verify (two evidence classes)** per applied component: Pulse → `curl 127.0.0.1:31337/healthz` = 200; statusline/tooltips/spinnerverbs → re-read `settings.json` shows the key set; agents → files present under `agents/`; launchd jobs → `launchctl print` shows the label loaded. 8. **ActivateImports** — `bun Tools/ActivateImports.ts` → uncomment the identity `@`-imports in `CLAUDE.md`, each guarded by `existsSync` of the symlink-resolved target. Path literals stay as the canonical `@`-import form. diff --git a/LifeOS/install/skills/LifeOS/Tools/InstallHooks.ts b/LifeOS/install/skills/LifeOS/Tools/InstallHooks.ts index 030fb9218..7307db264 100755 --- a/LifeOS/install/skills/LifeOS/Tools/InstallHooks.ts +++ b/LifeOS/install/skills/LifeOS/Tools/InstallHooks.ts @@ -101,7 +101,18 @@ function main(): void { cpSync(hooksPayloadDir, hooksDestDir, { recursive: true }); const hookFilesCopied = countFilesRec(hooksDestDir); - console.log(JSON.stringify({ ...report, written: true, backup, hookFilesCopied }, null, 2)); + // Standing source-of-truth snapshot read by MemoryHealthCheck CHECK 3. + // hooks = incoming (system wiring), not merged: never freeze a user's foreign entries in. + const systemHalfPath = join(skillRoot, "install", "settings.system.json"); + const sotPath = join(configRoot, "settings.system.json"); + let sot: Record = {}; + if (existsSync(systemHalfPath)) { + try { sot = JSON.parse(readFileSync(systemHalfPath, "utf-8")); } catch { sot = {}; } + } + sot.hooks = incoming; + writeFileSync(sotPath, JSON.stringify(sot, null, 2) + "\n"); + + console.log(JSON.stringify({ ...report, written: true, backup, hookFilesCopied, settingsSystemPath: sotPath }, null, 2)); process.exit(0); } diff --git a/LifeOS/install/skills/LifeOS/Workflows/Setup.md b/LifeOS/install/skills/LifeOS/Workflows/Setup.md index 6e4132150..64578e462 100644 --- a/LifeOS/install/skills/LifeOS/Workflows/Setup.md +++ b/LifeOS/install/skills/LifeOS/Workflows/Setup.md @@ -48,7 +48,7 @@ The skill ships everything for both tiers in its payload; nothing activates with | `pulse` | the Pulse dashboard as a `launchd` service (`:31337`) | optional | | `worksweep` / `derivedsync` | background `launchd` jobs | optional | - - **hooks → `bun Tools/InstallHooks.ts`** (trust-gated): reads `install/hooks/hooks.json`, shows the EXACT change (file + settings-entry + event count), waits for explicit permission, backs up `settings.json`, merges additively per matcher bucket (idempotent via normalized-command dedup, preserves `type:"http"` verbatim). + - **hooks → `bun Tools/InstallHooks.ts`** (trust-gated): reads `install/hooks/hooks.json`, shows the EXACT change (file + settings-entry + event count), waits for explicit permission, backs up `settings.json`, merges additively per matcher bucket (idempotent via normalized-command dedup, preserves `type:"http"` verbatim), and writes the standing source-of-truth snapshot to `~/.claude/settings.system.json` (system half + payload hooks) so `MemoryHealthCheck` CHECK 3 passes on fresh installs (#1402). - **everything else → `bun Tools/DeployComponents.ts`**: dry-run first (no `--apply`, `--all` shows the full plan), then `--apply --components ` with ONLY what the user picked. Reads enhancement settings from `install/settings.enhancements.json` (the keys split out of `settings.system.json` so they're genuinely opt-in, not force-bundled). A component whose prerequisite is absent reports a LOUD blocker and fails — never a silent no-op. macOS-only for `launchd`; skip silently on Linux/headless (`DetectEnv.display` false). - **Verify (two evidence classes)** per applied component: Pulse → `curl 127.0.0.1:31337/healthz` = 200; statusline/tooltips/spinnerverbs → re-read `settings.json` shows the key set; agents → files present under `agents/`; launchd jobs → `launchctl print` shows the label loaded. 8. **ActivateImports** — `bun Tools/ActivateImports.ts` → uncomment the identity `@`-imports in `CLAUDE.md`, each guarded by `existsSync` of the symlink-resolved target. Path literals stay as the canonical `@`-import form.