diff --git a/LifeOS/Tools/InstallEngine.ts b/LifeOS/Tools/InstallEngine.ts index 41b71badb..835b8b629 100644 --- a/LifeOS/Tools/InstallEngine.ts +++ b/LifeOS/Tools/InstallEngine.ts @@ -548,6 +548,28 @@ function normalizeCommand(cmd: string): string { .trim(); } +/** + * Rewrite the default `~/.claude` config-root spellings inside hook commands to + * the actually-detected configRoot. The payload's hooks.json ships commands + * against the default root (`$HOME/.claude/...`); on a multi-profile setup + * (Claude Code's CLAUDE_CONFIG_DIR) those paths point at a different — usually + * nonexistent — profile, so every hook errors on startup. No-op when configRoot + * IS the default, keeping default installs byte-identical. Pure (no I/O). + */ +export function rewriteHooksConfigRoot(hooks: HooksMap, configRoot: string, homeDir: string): HooksMap { + if (resolve(configRoot) === resolve(join(homeDir, ".claude"))) return hooks; + const defaultRootPattern = /(\$\{HOME\}|\$HOME|~)\/\.claude(?=\/|\s|$)/g; + const rewritten: HooksMap = JSON.parse(JSON.stringify(hooks ?? {})); + for (const groups of Object.values(rewritten)) { + for (const group of groups) { + for (const h of group.hooks ?? []) { + if (typeof h.command === "string") h.command = h.command.replace(defaultRootPattern, configRoot); + } + } + } + return rewritten; +} + /** Identity key for a hook entry: http → url, else normalized command. */ function hookKey(h: HookEntry): string { if (h.type === "http" && h.url) return `http:${h.url}`; diff --git a/LifeOS/Tools/InstallHooks.ts b/LifeOS/Tools/InstallHooks.ts index 030fb9218..f3f60dd36 100755 --- a/LifeOS/Tools/InstallHooks.ts +++ b/LifeOS/Tools/InstallHooks.ts @@ -16,7 +16,7 @@ import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -import { detectDevTree, mergeHooks } from "./InstallEngine"; +import { detectDevTree, mergeHooks, rewriteHooksConfigRoot } from "./InstallEngine"; interface Args { configRoot: string; skillRoot: string; apply: boolean; allowDev: boolean; } @@ -59,7 +59,14 @@ function main(): void { console.log(JSON.stringify({ ok: false, error: `payload hooks.json not found at ${hooksJsonPath}` }, null, 2)); process.exit(1); } - const incoming = JSON.parse(readFileSync(hooksJsonPath, "utf-8"))?.hooks ?? {}; + // Payload commands are written against the default ~/.claude root; retarget + // them at the detected configRoot so non-default CLAUDE_CONFIG_DIR profiles + // get hooks that point at the files this installer actually deploys. + const incoming = rewriteHooksConfigRoot( + JSON.parse(readFileSync(hooksJsonPath, "utf-8"))?.hooks ?? {}, + configRoot, + process.env.HOME || "", + ); // The hook SCRIPTS (*.hook.ts|sh + lib/**) live beside hooks.json in the payload. // Merging hooks.json into settings.json wires commands that point at these files, diff --git a/LifeOS/install/LIFEOS/TOOLS/FreshnessCache.ts b/LifeOS/install/LIFEOS/TOOLS/FreshnessCache.ts index 1e2c12339..75ed841c6 100755 --- a/LifeOS/install/LIFEOS/TOOLS/FreshnessCache.ts +++ b/LifeOS/install/LIFEOS/TOOLS/FreshnessCache.ts @@ -15,9 +15,9 @@ import { writeFileSync, renameSync, mkdirSync, existsSync } from "fs"; import { join } from "path"; import { readContextFreshness } from "./TelosFreshness"; +import { getLifeosDir } from "./Paths"; -const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = getLifeosDir(); const CACHE_DIR = join(LIFEOS_DIR, "USER", "CACHE"); const CACHE_PATH = join(CACHE_DIR, "freshness.json"); diff --git a/LifeOS/install/LIFEOS/TOOLS/ISARender.ts b/LifeOS/install/LIFEOS/TOOLS/ISARender.ts index aa6520e5c..c8beb31eb 100644 --- a/LifeOS/install/LIFEOS/TOOLS/ISARender.ts +++ b/LifeOS/install/LIFEOS/TOOLS/ISARender.ts @@ -18,18 +18,18 @@ import { readFileSync, writeFileSync, existsSync, statSync, renameSync, readdirSync } from "node:fs"; import { resolve, dirname, basename, join } from "node:path"; import { spawn } from "node:child_process"; -import { homedir } from "node:os"; +import { getLifeosDir } from "./Paths"; -const HOME = process.env.HOME || homedir(); -const TOOLS_DIR = resolve(HOME, ".claude/LIFEOS/TOOLS"); +const LIFEOS_DIR = getLifeosDir(); +const TOOLS_DIR = join(LIFEOS_DIR, "TOOLS"); const TEMPLATE_HTML = join(TOOLS_DIR, "ISARender/template.html"); const TEMPLATE_CSS = join(TOOLS_DIR, "ISARender/template.css"); // Brand logo: user override via LIFEOS_BRAND_LOGO_PATH env var (absolute path), // else system default under PAI/ASSETS/, else inert (empty src). const BRAND_LOGO_PATH_OVERRIDE = process.env.LIFEOS_BRAND_LOGO_PATH ?? ""; -const BRAND_LOGO_PATH_DEFAULT = resolve(HOME, ".claude/LIFEOS/ASSETS/pai-logo.png"); -const WORK_DIR = resolve(HOME, ".claude/LIFEOS/MEMORY/WORK"); -const WORK_JSON = resolve(HOME, ".claude/LIFEOS/MEMORY/STATE/work.json"); +const BRAND_LOGO_PATH_DEFAULT = join(LIFEOS_DIR, "ASSETS/pai-logo.png"); +const WORK_DIR = join(LIFEOS_DIR, "MEMORY/WORK"); +const WORK_JSON = join(LIFEOS_DIR, "MEMORY/STATE/work.json"); const PHASES = ["observe", "think", "plan", "build", "execute", "verify", "learn", "complete"]; diff --git a/LifeOS/install/LIFEOS/TOOLS/IntegrityMaintenance.ts b/LifeOS/install/LIFEOS/TOOLS/IntegrityMaintenance.ts index 62f028319..1f792e920 100755 --- a/LifeOS/install/LIFEOS/TOOLS/IntegrityMaintenance.ts +++ b/LifeOS/install/LIFEOS/TOOLS/IntegrityMaintenance.ts @@ -22,7 +22,7 @@ import { spawn } from 'child_process'; import { readFileSync, existsSync } from 'fs'; import { join, basename, dirname } from 'path'; import { inference } from './Inference'; -import { getIdentity } from '../../../.claude/hooks/lib/identity'; +import { getIdentity } from '../../hooks/lib/identity'; // ============================================================================ // Types diff --git a/LifeOS/install/LIFEOS/TOOLS/LifeosConfig.ts b/LifeOS/install/LIFEOS/TOOLS/LifeosConfig.ts index ca5d03d9b..6f694a764 100644 --- a/LifeOS/install/LIFEOS/TOOLS/LifeosConfig.ts +++ b/LifeOS/install/LIFEOS/TOOLS/LifeosConfig.ts @@ -20,6 +20,7 @@ import { existsSync, statSync } from "node:fs"; import { resolve } from "node:path"; import { homedir } from "node:os"; +import { getClaudeDir } from "./Paths"; // Expand leading `~` (and `~/`) to the user's home directory. node:fs APIs do // not expand tildes, so any path returned from this loader must be absolute. @@ -85,7 +86,7 @@ export interface LifeosConfig { // ─────────── Resolution ─────────── const DEFAULT_HOME = process.env.HOME || homedir(); -const DEFAULT_CONFIG_PATH = resolve(DEFAULT_HOME, ".claude/LIFEOS/USER/CONFIG/LIFEOS_CONFIG.toml"); +const DEFAULT_CONFIG_PATH = resolve(getClaudeDir(), "LIFEOS/USER/CONFIG/LIFEOS_CONFIG.toml"); let cache: { config: LifeosConfig; mtime: number; path: string } | null = null; @@ -133,7 +134,7 @@ export function paiUserDir(): string { try { return loadLifeosConfig().paths.userDir; } catch { - return resolve(DEFAULT_HOME, ".claude/LIFEOS/USER"); + return resolve(getClaudeDir(), "LIFEOS/USER"); } } @@ -188,10 +189,10 @@ function validateAndNormalize(raw: unknown, path: string): LifeosConfig { }, paths: { userDir: expandHome( - root.paths?.userDir ?? root.paths?.user_dir ?? resolve(DEFAULT_HOME, ".claude/LIFEOS/USER"), + root.paths?.userDir ?? root.paths?.user_dir ?? resolve(getClaudeDir(), "LIFEOS/USER"), ), memoryDir: expandHome( - root.paths?.memoryDir ?? root.paths?.memory_dir ?? resolve(DEFAULT_HOME, ".claude/LIFEOS/MEMORY"), + root.paths?.memoryDir ?? root.paths?.memory_dir ?? resolve(getClaudeDir(), "LIFEOS/MEMORY"), ), projectsDir: expandHome( root.paths?.projectsDir ?? root.paths?.projects_dir ?? resolve(DEFAULT_HOME, "Projects"), diff --git a/LifeOS/install/LIFEOS/TOOLS/MemoryHealthCheck.ts b/LifeOS/install/LIFEOS/TOOLS/MemoryHealthCheck.ts index 68d061ecd..6183323a5 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MemoryHealthCheck.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemoryHealthCheck.ts @@ -23,9 +23,9 @@ import { existsSync, readFileSync, appendFileSync, mkdirSync, readdirSync, statSync } from "node:fs"; import { join } from "node:path"; +import { getClaudeDir } from "./Paths"; -const HOME = process.env.HOME || ""; -const CLAUDE = join(HOME, ".claude"); +const CLAUDE = getClaudeDir(); const HOOKS_DIR = join(CLAUDE, "hooks"); const TOOLS_DIR = join(CLAUDE, "LIFEOS/TOOLS"); const OBS_DIR = join(CLAUDE, "LIFEOS/MEMORY/OBSERVABILITY"); diff --git a/LifeOS/install/LIFEOS/TOOLS/MemoryReviewer.ts b/LifeOS/install/LIFEOS/TOOLS/MemoryReviewer.ts index 5a0521093..ea4b90483 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MemoryReviewer.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemoryReviewer.ts @@ -40,7 +40,6 @@ import { writeFileSync, } from "node:fs"; import { dirname, join as pathJoin, resolve as pathResolve } from "node:path"; -import { homedir } from "node:os"; import { add as memoryAdd, type AddResult } from "./MemorySystem"; import { read as memoryWriterRead } from "./MemoryWriter"; @@ -51,11 +50,12 @@ import { markProposal, logProposalEvent, } from "../PULSE/lib/telegram-proposals"; +import { getClaudeDir } from "./Paths"; // ── Constants ── -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); -const HARNESS_PROJECTS_DIR = pathResolve(homedir(), ".claude", "projects"); +const CLAUDE_ROOT = getClaudeDir(); +const HARNESS_PROJECTS_DIR = pathJoin(CLAUDE_ROOT, "projects"); const RUNS_LOG_PATH = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/reviewer-runs.jsonl"); const RUNS_DEBUG_DIR = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/reviewer-runs"); const REVIEW_CONFIG_PATH = pathResolve(CLAUDE_ROOT, "LIFEOS/USER/CONFIG/memory-review.json"); @@ -648,7 +648,7 @@ async function smokeTest(): Promise { const mockResponse = JSON.stringify({ items: [ { type: "memory", actor: "principal", content: "PREFERENCE: smoke E2E mock" }, - { type: "proposal", target_file: pathJoin(homedir(), ".claude/LIFEOS/USER/PRINCIPAL/PRINCIPAL_IDENTITY.md"), edit: "RULE: E2E mock", confidence: 0.5, rationale: "smoke" }, + { type: "proposal", target_file: pathJoin(CLAUDE_ROOT, "LIFEOS/USER/PRINCIPAL/PRINCIPAL_IDENTITY.md"), edit: "RULE: E2E mock", confidence: 0.5, rationale: "smoke" }, ], }); diff --git a/LifeOS/install/LIFEOS/TOOLS/Paths.ts b/LifeOS/install/LIFEOS/TOOLS/Paths.ts new file mode 100644 index 000000000..3150e0633 --- /dev/null +++ b/LifeOS/install/LIFEOS/TOOLS/Paths.ts @@ -0,0 +1,58 @@ +/** + * Paths.ts — centralized config-root resolution for LIFEOS/TOOLS. + * + * Mirrors hooks/lib/paths.ts (the hooks' resolver) so runtime tools agree with + * the hooks about where the Claude config root lives: + * + * 1. CLAUDE_PLUGIN_ROOT (plugin install) — the flattened plugin root + * 2. CLAUDE_CONFIG_DIR — Claude Code's own config-dir override + * (multi-profile setups) + * 3. ~/.claude — the default, byte-identical to prior behavior + * + * LIFEOS data resolves via LIFEOS_DIR when set, else /LIFEOS. + */ + +import { homedir } from "node:os"; +import { join } from "node:path"; + +/** Expand $HOME, ${HOME}, and ~ prefixes in a path string. */ +export function expandPath(path: string): string { + const home = homedir(); + + return path + .replace(/^\$HOME(?=\/|$)/, home) + .replace(/^\$\{HOME\}(?=\/|$)/, home) + .replace(/^~(?=\/|$)/, home); +} + +/** Get the Claude Code config directory (see resolution order above). */ +export function getClaudeDir(): string { + const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT; + + if (pluginRoot) { + return expandPath(pluginRoot); + } + + const configDir = process.env.CLAUDE_CONFIG_DIR; + + if (configDir) { + return expandPath(configDir); + } + + return join(homedir(), ".claude"); +} + +/** Get the LifeOS data directory: LIFEOS_DIR when set, else /LIFEOS. */ +export function getLifeosDir(): string { + if (process.env.CLAUDE_PLUGIN_ROOT) { + return join(getClaudeDir(), "LIFEOS"); + } + + const envLifeosDir = process.env.LIFEOS_DIR; + + if (envLifeosDir) { + return expandPath(envLifeosDir); + } + + return join(getClaudeDir(), "LIFEOS"); +} diff --git a/LifeOS/install/LIFEOS/TOOLS/SessionHarvester.ts b/LifeOS/install/LIFEOS/TOOLS/SessionHarvester.ts index d0501ee87..f3962c564 100755 --- a/LifeOS/install/LIFEOS/TOOLS/SessionHarvester.ts +++ b/LifeOS/install/LIFEOS/TOOLS/SessionHarvester.ts @@ -22,7 +22,7 @@ import { parseArgs } from "util"; import * as fs from "fs"; import * as path from "path"; -import { getLearningCategory, isLearningCapture } from "../../../.claude/hooks/lib/learning-utils"; +import { getLearningCategory, isLearningCapture } from "../../hooks/lib/learning-utils"; // ============================================================================ // Configuration diff --git a/LifeOS/install/LIFEOS/TOOLS/SettingsBackport.ts b/LifeOS/install/LIFEOS/TOOLS/SettingsBackport.ts index 769bd0487..c90316323 100755 --- a/LifeOS/install/LIFEOS/TOOLS/SettingsBackport.ts +++ b/LifeOS/install/LIFEOS/TOOLS/SettingsBackport.ts @@ -27,8 +27,9 @@ import { existsSync } from "node:fs"; import path from "node:path"; import os from "node:os"; import { mergeSettings, deepEqual, parseJsonFileOrThrow } from "./MergeSettings"; +import { getClaudeDir } from "./Paths"; -const CLAUDE_DIR = path.join(os.homedir(), ".claude"); +const CLAUDE_DIR = getClaudeDir(); const SYSTEM_PATH = path.join(CLAUDE_DIR, "settings.system.json"); const USER_PATH = path.join(CLAUDE_DIR, "LIFEOS", "USER", "CONFIG", "settings.user.json"); const GENERATED_PATH = path.join(CLAUDE_DIR, "settings.json"); diff --git a/LifeOS/install/LIFEOS/TOOLS/TranscriptParser.ts b/LifeOS/install/LIFEOS/TOOLS/TranscriptParser.ts index 594031c9f..a236b7f5e 100755 --- a/LifeOS/install/LIFEOS/TOOLS/TranscriptParser.ts +++ b/LifeOS/install/LIFEOS/TOOLS/TranscriptParser.ts @@ -17,7 +17,7 @@ */ import { readFileSync } from 'fs'; -import { getIdentity } from '../../../.claude/hooks/lib/identity'; +import { getIdentity } from '../../hooks/lib/identity'; const DA_IDENTITY = getIdentity(); diff --git a/LifeOS/install/LIFEOS/TOOLS/algorithm.ts b/LifeOS/install/LIFEOS/TOOLS/algorithm.ts index 609a8a28c..789a6956f 100644 --- a/LifeOS/install/LIFEOS/TOOLS/algorithm.ts +++ b/LifeOS/install/LIFEOS/TOOLS/algorithm.ts @@ -50,7 +50,7 @@ import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, append import { resolve, basename, join, dirname } from "path"; import { spawnSync, spawn } from "child_process"; import { randomUUID } from "crypto"; -import { generateISATemplate } from "../../../.claude/hooks/lib/isa-template"; +import { generateISATemplate } from "../../hooks/lib/isa-template"; // ─── Paths ─────────────────────────────────────────────────────────────────── diff --git a/LifeOS/install/LIFEOS/TOOLS/lifeos.ts b/LifeOS/install/LIFEOS/TOOLS/lifeos.ts index 65a1fd9b6..0b30fd0bd 100755 --- a/LifeOS/install/LIFEOS/TOOLS/lifeos.ts +++ b/LifeOS/install/LIFEOS/TOOLS/lifeos.ts @@ -19,19 +19,20 @@ */ import { spawn, spawnSync } from "bun"; -import { getIdentity, getStartupCatchphrase } from "../../../.claude/hooks/lib/identity"; +import { getIdentity, getStartupCatchphrase } from "../../hooks/lib/identity"; import { existsSync, readFileSync, writeFileSync, readdirSync, symlinkSync, unlinkSync, lstatSync } from "fs"; import { homedir } from "os"; import { join, basename } from "path"; +import { getClaudeDir, getLifeosDir } from "./Paths"; // ============================================================================ // Configuration // ============================================================================ -const CLAUDE_DIR = join(homedir(), ".claude"); +const CLAUDE_DIR = getClaudeDir(); const MCP_DIR = join(CLAUDE_DIR, "MCPs"); const ACTIVE_MCP = join(CLAUDE_DIR, ".mcp.json"); -const BANNER_SCRIPT = join(homedir(), ".claude", "LIFEOS", "TOOLS", "Banner.ts"); +const BANNER_SCRIPT = join(getLifeosDir(), "TOOLS", "Banner.ts"); const VOICE_SERVER = "http://localhost:31337/notify/personality"; const WALLPAPER_DIR = join(homedir(), "Projects", "Wallpaper"); // Note: RAW archiving removed - Claude Code handles its own cleanup (30-day retention in projects/) diff --git a/LifeOS/install/hooks/AgentInvocation.hook.ts b/LifeOS/install/hooks/AgentInvocation.hook.ts index b676be239..2837a49a1 100755 --- a/LifeOS/install/hooks/AgentInvocation.hook.ts +++ b/LifeOS/install/hooks/AgentInvocation.hook.ts @@ -19,8 +19,7 @@ import { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; -import { homedir } from 'os'; -import { paiPath } from './lib/paths'; +import { getClaudeDir, paiPath } from './lib/paths'; import { getISOTimestamp } from './lib/time'; import { EFFORT_MODEL, ALIAS, CROSS_VENDOR } from '../LIFEOS/TOOLS/models'; @@ -49,7 +48,7 @@ function resolveDispatch(subagentType: string, inputModel?: string): { model: st if (CROSS_VENDOR[cvKey]) return { model: CROSS_VENDOR[cvKey], level: 'cross-vendor' }; if (inputModel) return { model: inputModel, level: levelForModel(inputModel) }; try { - const fm = readFileSync(join(homedir(), '.claude', 'agents', `${subagentType}.md`), 'utf-8').slice(0, 4000); + const fm = readFileSync(join(getClaudeDir(), 'agents', `${subagentType}.md`), 'utf-8').slice(0, 4000); const m = fm.match(/^model:\s*(\S+)/m); if (m) return { model: m[1], level: `${levelForModel(m[1])}-pin` }; } catch { /* no agent file — built-in type */ } diff --git a/LifeOS/install/hooks/ArtWorkflowGuard.hook.ts b/LifeOS/install/hooks/ArtWorkflowGuard.hook.ts index 8172a86c5..bb0bd902b 100755 --- a/LifeOS/install/hooks/ArtWorkflowGuard.hook.ts +++ b/LifeOS/install/hooks/ArtWorkflowGuard.hook.ts @@ -16,6 +16,7 @@ */ import { readFileSync, readdirSync } from "node:fs"; +import { displayPath, getSkillsDir } from "./lib/paths"; interface HookInput { session_id?: string; @@ -23,7 +24,6 @@ interface HookInput { tool_input?: { command?: string; description?: string }; } -const HOME = process.env.HOME ?? ""; const ART_TOOL_PATH_FRAGMENTS = [ "skills/Art/Tools/Generate.ts", ".claude/skills/Art/Tools/Generate.ts", @@ -39,7 +39,7 @@ function readHookInput(): HookInput { } function listWorkflows(): string[] { - const dir = `${HOME}/.claude/skills/Art/Workflows`; + const dir = `${getSkillsDir()}/Art/Workflows`; try { return readdirSync(dir) .filter((f) => f.endsWith(".md")) @@ -83,7 +83,7 @@ function main(): never { " ArtWorkflowGuard — Generate.ts call BLOCKED before execution.", "═══════════════════════════════════════════════════════════════════════════", "", - " This Bash command invokes ~/.claude/skills/Art/Tools/Generate.ts but", + ` This Bash command invokes ${displayPath(getSkillsDir())}/Art/Tools/Generate.ts but`, " does not name a workflow. The Art skill requires every image generation", " to run through a named workflow. Freeform prompts are documented to fail.", "", @@ -93,7 +93,7 @@ function main(): never { "", " Available workflows (each is a file under skills/Art/Workflows/):", ...workflows.map( - (w) => ` --workflow=${w.padEnd(28)} → ~/.claude/skills/Art/Workflows/${w}.md` + (w) => ` --workflow=${w.padEnd(28)} → ${displayPath(getSkillsDir())}/Art/Workflows/${w}.md` ), "", " Recommended next step: Read the workflow file matching your task,", diff --git a/LifeOS/install/hooks/CheckpointPerISC.hook.ts b/LifeOS/install/hooks/CheckpointPerISC.hook.ts index d1f5eabd4..34479f730 100755 --- a/LifeOS/install/hooks/CheckpointPerISC.hook.ts +++ b/LifeOS/install/hooks/CheckpointPerISC.hook.ts @@ -24,12 +24,13 @@ import { execFileSync } from 'node:child_process'; import { basename, dirname, join } from 'node:path'; import { homedir } from 'node:os'; import { parseFrontmatter, parseCriteriaList, ARTIFACT_FILENAME, LEGACY_ARTIFACT_FILENAME } from './lib/isa-utils'; +import { getClaudeDir } from './lib/paths'; // Allowlist path: top of ~/.claude per spec. One absolute repo path per line; // '#' comments and blank // lines are ignored. Tilde and $HOME prefixes are expanded as a quality-of- // life feature so users can write `~/Projects/foo` instead of the long form. -const ALLOWLIST_PATH = join(homedir(), '.claude', 'checkpoint-repos.txt'); +const ALLOWLIST_PATH = join(getClaudeDir(), 'checkpoint-repos.txt'); const GIT_TIMEOUT_MS = 5000; interface CheckpointState { diff --git a/LifeOS/install/hooks/DriftReminder.hook.ts b/LifeOS/install/hooks/DriftReminder.hook.ts index 50c6fe679..96e773d29 100755 --- a/LifeOS/install/hooks/DriftReminder.hook.ts +++ b/LifeOS/install/hooks/DriftReminder.hook.ts @@ -12,6 +12,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { firstBannedHit } from "./lib/banned-vocab"; +import { getLifeosDir } from './lib/paths'; interface HookInput { prompt?: string; @@ -27,7 +28,7 @@ interface DriftState { const STDIN_TIMEOUT_MS = 300; const MIN_TURNS_BETWEEN_FIRES = 5; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(process.env.HOME || "", ".claude", "LIFEOS"); +const LIFEOS_DIR = getLifeosDir(); const LAST_RESPONSE_PATH = join(LIFEOS_DIR, "MEMORY", "STATE", "last-response.txt"); const STATE_PATH = join(LIFEOS_DIR, "MEMORY", "STATE", "drift-reminder.json"); const INITIAL_STATE: DriftState = { diff --git a/LifeOS/install/hooks/EgressClassGuard.hook.ts b/LifeOS/install/hooks/EgressClassGuard.hook.ts index 07a559351..3a9e99f4c 100755 --- a/LifeOS/install/hooks/EgressClassGuard.hook.ts +++ b/LifeOS/install/hooks/EgressClassGuard.hook.ts @@ -19,11 +19,10 @@ import { readFileSync, appendFileSync, existsSync, mkdirSync } from "node:fs"; import { dirname, join } from "node:path"; -import { homedir } from "node:os"; import { evaluateEgress } from "./lib/egress-class-core"; +import { paiPath } from "./lib/paths"; -const HOME = process.env.HOME ?? homedir(); -const LOG_PATH = join(HOME, ".claude/LIFEOS/MEMORY/OBSERVABILITY/egress-decisions.jsonl"); +const LOG_PATH = paiPath("MEMORY/OBSERVABILITY/egress-decisions.jsonl"); interface HookInput { session_id?: string; diff --git a/LifeOS/install/hooks/HookHealer.hook.ts b/LifeOS/install/hooks/HookHealer.hook.ts index 0eea7bb37..1898fb144 100755 --- a/LifeOS/install/hooks/HookHealer.hook.ts +++ b/LifeOS/install/hooks/HookHealer.hook.ts @@ -34,9 +34,9 @@ import { mkdirSync, openSync, readSync, closeSync, realpathSync, } from 'fs'; import { join } from 'path'; -import { homedir } from 'os'; +import { getClaudeDir } from './lib/paths'; -const CLAUDE_DIR = join(homedir(), '.claude'); +const CLAUDE_DIR = getClaudeDir(); const OBS_DIR = join(CLAUDE_DIR, 'LIFEOS', 'MEMORY', 'OBSERVABILITY'); const LOG_FILE = join(OBS_DIR, 'hook-healer.jsonl'); const SETTINGS_FILES = ['settings.json', 'settings.local.json']; diff --git a/LifeOS/install/hooks/ISARenderOnStop.hook.ts b/LifeOS/install/hooks/ISARenderOnStop.hook.ts index 3758a22e6..1b30bd0bc 100755 --- a/LifeOS/install/hooks/ISARenderOnStop.hook.ts +++ b/LifeOS/install/hooks/ISARenderOnStop.hook.ts @@ -19,11 +19,11 @@ import { readFileSync, existsSync, unlinkSync } from 'fs'; import { spawn } from 'child_process'; -import { homedir } from 'os'; import { join, dirname } from 'path'; +import { paiPath } from './lib/paths'; -const STATE_DIR = join(homedir(), '.claude/LIFEOS/MEMORY/STATE/isa-render-debounce'); -const ISA_RENDER = join(homedir(), '.claude/LIFEOS/TOOLS/ISARender.ts'); +const STATE_DIR = paiPath('MEMORY/STATE/isa-render-debounce'); +const ISA_RENDER = paiPath('TOOLS/ISARender.ts'); /** * Has this ISA reached completion at least once? This is the real gate the @@ -108,7 +108,7 @@ try { unlinkSync(stateFile); } catch {} if (rendered.length || skipped.length) { try { const { appendFileSync, mkdirSync } = require('fs'); - const logDir = join(homedir(), '.claude/LIFEOS/MEMORY/OBSERVABILITY'); + const logDir = paiPath('MEMORY/OBSERVABILITY'); mkdirSync(logDir, { recursive: true }); appendFileSync(join(logDir, 'isa-render.jsonl'), JSON.stringify({ diff --git a/LifeOS/install/hooks/ISASync.hook.ts b/LifeOS/install/hooks/ISASync.hook.ts index 557d9c587..7c7468fb2 100755 --- a/LifeOS/install/hooks/ISASync.hook.ts +++ b/LifeOS/install/hooks/ISASync.hook.ts @@ -20,8 +20,8 @@ import { readFileSync, existsSync } from 'fs'; import { spawn } from 'child_process'; -import { homedir } from 'os'; import { join } from 'path'; +import { paiPath } from './lib/paths'; import { parseFrontmatter, syncToWorkJson, @@ -103,7 +103,7 @@ async function main() { // constantly remaking the HTML file." if (newPhase === 'COMPLETE' && oldPhase !== 'COMPLETE' && fm.slug) { try { - const isaRender = join(homedir(), '.claude/LIFEOS/TOOLS/ISARender.ts'); + const isaRender = paiPath('TOOLS/ISARender.ts'); const proc = spawn('bun', [isaRender, isaPath], { detached: true, stdio: 'ignore', @@ -120,7 +120,7 @@ async function main() { // pre-completion edits never trigger renders even though they show up here. if (input.session_id) { try { - const stateDir = join(homedir(), '.claude/LIFEOS/MEMORY/STATE/isa-render-debounce'); + const stateDir = paiPath('MEMORY/STATE/isa-render-debounce'); const stateFile = join(stateDir, `${input.session_id}.json`); const { mkdirSync, writeFileSync } = require('fs'); mkdirSync(stateDir, { recursive: true }); diff --git a/LifeOS/install/hooks/InstructionsLoadedHandler.hook.ts b/LifeOS/install/hooks/InstructionsLoadedHandler.hook.ts index db0276964..5d99f62fc 100755 --- a/LifeOS/install/hooks/InstructionsLoadedHandler.hook.ts +++ b/LifeOS/install/hooks/InstructionsLoadedHandler.hook.ts @@ -24,21 +24,20 @@ import { existsSync, mkdirSync } from 'fs'; import { join } from 'path'; -import { homedir } from 'os'; +import { getClaudeDir, getLifeosDir } from './lib/paths'; // ======================================== // Configuration // ======================================== -const HOME = homedir(); -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, '.claude', 'LIFEOS'); +const LIFEOS_DIR = getLifeosDir(); const STATE_DIR = join(LIFEOS_DIR, 'MEMORY', 'STATE'); const HASHES_FILE = join(STATE_DIR, 'instruction-hashes.json'); const INTEGRITY_LOG = join(STATE_DIR, 'instruction-integrity.jsonl'); /** Critical LifeOS instruction files to monitor */ const CRITICAL_FILES: Record = { - 'CLAUDE.md': join(HOME, '.claude', 'CLAUDE.md'), + 'CLAUDE.md': join(getClaudeDir(), 'CLAUDE.md'), 'SYSTEM-PROMPT': join(LIFEOS_DIR, 'LIFEOS_SYSTEM_PROMPT.md'), 'DA_IDENTITY': join(LIFEOS_DIR, 'USER', 'DA_IDENTITY.md'), 'PRINCIPAL_IDENTITY': join(LIFEOS_DIR, 'USER', 'PRINCIPAL_IDENTITY.md'), diff --git a/LifeOS/install/hooks/KittyEnvPersist.hook.ts b/LifeOS/install/hooks/KittyEnvPersist.hook.ts index 8bfcf7ea7..a49e751c4 100755 --- a/LifeOS/install/hooks/KittyEnvPersist.hook.ts +++ b/LifeOS/install/hooks/KittyEnvPersist.hook.ts @@ -12,7 +12,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; import { join } from 'path'; -import { getLifeosDir } from './lib/paths'; +import { getClaudeDir, getLifeosDir } from './lib/paths'; import { setTabState, readTabState, persistKittySession } from './lib/tab-setter'; import { getDAName } from './lib/identity'; @@ -20,7 +20,7 @@ const paiDir = getLifeosDir(); // Skip for subagents const claudeProjectDir = process.env.CLAUDE_PROJECT_DIR || ''; -const isSubagent = claudeProjectDir.includes('/.claude/Agents/') || +const isSubagent = claudeProjectDir.includes(join(getClaudeDir(), 'Agents') + '/') || process.env.CLAUDE_AGENT_TYPE !== undefined; if (isSubagent) process.exit(0); diff --git a/LifeOS/install/hooks/LastResponseCache.hook.ts b/LifeOS/install/hooks/LastResponseCache.hook.ts index 61f773cce..e03926af9 100755 --- a/LifeOS/install/hooks/LastResponseCache.hook.ts +++ b/LifeOS/install/hooks/LastResponseCache.hook.ts @@ -14,6 +14,7 @@ import { readHookInput, parseTranscriptFromInput } from './lib/hook-io'; import { writeFileSync } from 'fs'; import { join } from 'path'; +import { getLifeosDir } from './lib/paths'; async function main() { const input = await readHookInput(); @@ -28,7 +29,7 @@ async function main() { if (lastResponse) { try { - const paiDir = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude', 'LIFEOS'); + const paiDir = getLifeosDir(); const cachePath = join(paiDir, 'MEMORY', 'STATE', 'last-response.txt'); writeFileSync(cachePath, lastResponse.slice(0, 2000), 'utf-8'); } catch (err) { diff --git a/LifeOS/install/hooks/LoadContext.hook.ts b/LifeOS/install/hooks/LoadContext.hook.ts index 3bdc4767a..9f7fae1bd 100755 --- a/LifeOS/install/hooks/LoadContext.hook.ts +++ b/LifeOS/install/hooks/LoadContext.hook.ts @@ -39,7 +39,7 @@ import { readFileSync, existsSync, readdirSync } from 'fs'; import { join } from 'path'; -import { getLifeosDir, getSettingsPath } from './lib/paths'; +import { getClaudeDir, getLifeosDir, getSettingsPath } from './lib/paths'; import { recordSessionStart } from './lib/notifications'; import { loadLearningDigest, loadWisdomFrames, loadFailurePatterns, loadSignalTrends, loadSynthesisPatterns } from './lib/learning-readback'; import { findArtifactPath } from './lib/isa-utils'; @@ -386,7 +386,7 @@ async function main() { try { // Subagents don't need dynamic context injection const claudeProjectDir = process.env.CLAUDE_PROJECT_DIR || ''; - const isSubagent = claudeProjectDir.includes('/.claude/Agents/') || + const isSubagent = claudeProjectDir.includes(join(getClaudeDir(), 'Agents') + '/') || process.env.CLAUDE_AGENT_TYPE !== undefined; if (isSubagent) { diff --git a/LifeOS/install/hooks/LoadMemory.hook.ts b/LifeOS/install/hooks/LoadMemory.hook.ts index ea2dfd706..50bb56362 100755 --- a/LifeOS/install/hooks/LoadMemory.hook.ts +++ b/LifeOS/install/hooks/LoadMemory.hook.ts @@ -21,9 +21,9 @@ import { existsSync, readFileSync } from "node:fs"; import { resolve as pathResolve } from "node:path"; -import { homedir } from "node:os"; +import { getClaudeDir } from './lib/paths'; -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = getClaudeDir(); const PRINCIPAL_MEMORY = pathResolve(CLAUDE_ROOT, "LIFEOS/USER/PRINCIPAL/PRINCIPAL_MEMORY.md"); const DA_MEMORY = pathResolve(CLAUDE_ROOT, "LIFEOS/USER/DIGITAL_ASSISTANT/DA_MEMORY.md"); diff --git a/LifeOS/install/hooks/MemoryDeltaSurface.hook.ts b/LifeOS/install/hooks/MemoryDeltaSurface.hook.ts index 246aa0ca5..8461078db 100755 --- a/LifeOS/install/hooks/MemoryDeltaSurface.hook.ts +++ b/LifeOS/install/hooks/MemoryDeltaSurface.hook.ts @@ -33,9 +33,9 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from "node:fs"; import { resolve as pathResolve, dirname } from "node:path"; -import { homedir } from "node:os"; +import { displayPath, getClaudeDir } from './lib/paths'; -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = getClaudeDir(); const WRITES_LOG = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/memory-writes.jsonl"); const CURSOR = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/memory-delta-cursor.json"); const HEALTH_LOG = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/memory-health.jsonl"); @@ -161,7 +161,7 @@ function criticalHealthLine(): string | null { .filter((f: any) => f.severity === "critical") .map((f: any) => f.message) .slice(0, 3); - return `🩺 MEMORY HEALTH: CRITICAL — ${blockers.join(" · ") || `${last.counts?.critical ?? "?"} blocker(s)`}. Fix: bun ~/.claude/LIFEOS/TOOLS/MemoryHealthCheck.ts`; + return `🩺 MEMORY HEALTH: CRITICAL — ${blockers.join(" · ") || `${last.counts?.critical ?? "?"} blocker(s)`}. Fix: bun ${displayPath(pathResolve(CLAUDE_ROOT, "LIFEOS/TOOLS/MemoryHealthCheck.ts"))}`; } catch { return null; } diff --git a/LifeOS/install/hooks/MemoryHealthGate.hook.ts b/LifeOS/install/hooks/MemoryHealthGate.hook.ts index b2a654b56..247694968 100755 --- a/LifeOS/install/hooks/MemoryHealthGate.hook.ts +++ b/LifeOS/install/hooks/MemoryHealthGate.hook.ts @@ -14,10 +14,9 @@ */ import { execFileSync } from "node:child_process"; -import { join } from "node:path"; +import { displayPath, paiPath } from "./lib/paths"; -const HOME = process.env.HOME || ""; -const CHECK = join(HOME, ".claude/LIFEOS/TOOLS/MemoryHealthCheck.ts"); +const CHECK = paiPath("TOOLS/MemoryHealthCheck.ts"); try { const out = execFileSync("bun", [CHECK], { @@ -27,7 +26,7 @@ try { }); const report = JSON.parse(out); if (report.overall === "critical") { - console.error(`🚨 Memory health: CRITICAL — ${report.counts.critical} blocker(s). Run: bun ~/.claude/LIFEOS/TOOLS/MemoryHealthCheck.ts`); + console.error(`🚨 Memory health: CRITICAL — ${report.counts.critical} blocker(s). Run: bun ${displayPath(CHECK)}`); } else if (report.overall === "warn") { console.error(`⚠️ Memory health: WARN — ${report.counts.warn} finding(s).`); } @@ -40,7 +39,7 @@ try { if (stdout) { const report = JSON.parse(stdout); if (report.overall === "critical") { - console.error(`🚨 Memory health: CRITICAL — ${report.counts.critical} blocker(s). Run: bun ~/.claude/LIFEOS/TOOLS/MemoryHealthCheck.ts`); + console.error(`🚨 Memory health: CRITICAL — ${report.counts.critical} blocker(s). Run: bun ${displayPath(CHECK)}`); } else if (report.overall === "warn") { console.error(`⚠️ Memory health: WARN — ${report.counts.warn} finding(s).`); } diff --git a/LifeOS/install/hooks/MemoryReviewFire.hook.ts b/LifeOS/install/hooks/MemoryReviewFire.hook.ts index ba5c1b75f..091a9667e 100755 --- a/LifeOS/install/hooks/MemoryReviewFire.hook.ts +++ b/LifeOS/install/hooks/MemoryReviewFire.hook.ts @@ -22,9 +22,9 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; import { spawn } from "node:child_process"; import { dirname, resolve as pathResolve } from "node:path"; -import { homedir } from "node:os"; +import { getClaudeDir } from './lib/paths'; -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = getClaudeDir(); const STATE_PATH = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/review-state.json"); const FIRE_LOG_PATH = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/reviewer-fires.jsonl"); const REVIEWER_PATH = pathResolve(CLAUDE_ROOT, "LIFEOS/TOOLS/MemoryReviewer.ts"); diff --git a/LifeOS/install/hooks/MemoryReviewTrigger.hook.ts b/LifeOS/install/hooks/MemoryReviewTrigger.hook.ts index 508ee6987..1cdc07778 100755 --- a/LifeOS/install/hooks/MemoryReviewTrigger.hook.ts +++ b/LifeOS/install/hooks/MemoryReviewTrigger.hook.ts @@ -38,9 +38,9 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; import { dirname, resolve as pathResolve } from "node:path"; -import { homedir } from "node:os"; +import { getClaudeDir } from './lib/paths'; -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = getClaudeDir(); const STATE_PATH = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/review-state.json"); const CONFIG_PATH = pathResolve(CLAUDE_ROOT, "LIFEOS/USER/CONFIG/memory-review.json"); const EFFORT_ROUTER_LOG = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/effort-router.jsonl"); diff --git a/LifeOS/install/hooks/OutputFormatGate.hook.ts b/LifeOS/install/hooks/OutputFormatGate.hook.ts index 605cc1126..6d807c89f 100755 --- a/LifeOS/install/hooks/OutputFormatGate.hook.ts +++ b/LifeOS/install/hooks/OutputFormatGate.hook.ts @@ -29,9 +29,10 @@ import { collectCurrentResponseText } from "../LIFEOS/TOOLS/TranscriptParser"; import { scanAiSpeak } from "./lib/ai-speak-patterns"; import { readFileSync, appendFileSync, mkdirSync } from "fs"; import { dirname, join } from "path"; +import { getLifeosDir } from './lib/paths'; const OBS_PATH = join( - process.env.LIFEOS_DIR || join(process.env.HOME!, ".claude", "LIFEOS"), + getLifeosDir(), "MEMORY", "OBSERVABILITY", "format-gate.jsonl", diff --git a/LifeOS/install/hooks/PromptProcessing.hook.ts b/LifeOS/install/hooks/PromptProcessing.hook.ts index f30a04c0e..770ad538d 100755 --- a/LifeOS/install/hooks/PromptProcessing.hook.ts +++ b/LifeOS/install/hooks/PromptProcessing.hook.ts @@ -39,6 +39,7 @@ import type { AlgorithmTabPhase } from './lib/tab-constants'; import { paiPath } from './lib/paths'; import { updateSessionNameInWorkJson, upsertSession } from './lib/isa-utils'; import { isDesktopChannel, logSkippedVoice, getNotificationChannel } from './lib/notification-channel'; +import { getLifeosDir } from './lib/paths'; // ── Types ── @@ -66,7 +67,7 @@ function appendPromptProcessingTelemetry(entry: Record): void { // ── Constants ── -const BASE_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude', 'LIFEOS'); +const BASE_DIR = getLifeosDir(); const SESSION_NAMES_PATH = paiPath('MEMORY', 'STATE', 'session-names.json'); const LOCK_PATH = SESSION_NAMES_PATH + '.lock'; const MIN_PROMPT_LENGTH = 3; diff --git a/LifeOS/install/hooks/ReminderRouter.hook.ts b/LifeOS/install/hooks/ReminderRouter.hook.ts index 8635ab041..b90e4dc7a 100755 --- a/LifeOS/install/hooks/ReminderRouter.hook.ts +++ b/LifeOS/install/hooks/ReminderRouter.hook.ts @@ -22,9 +22,9 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs"; import { createHash } from "crypto"; import { join, dirname } from "path"; import { loadWorkConfig } from "./lib/work-config"; +import { paiPath } from "./lib/paths"; -const HOME = process.env.HOME || ""; -const STATE_PATH = join(HOME, ".claude", "LIFEOS", "MEMORY", "STATE", "reminder-router-seen.json"); +const STATE_PATH = paiPath("MEMORY", "STATE", "reminder-router-seen.json"); interface HookInput { session_id?: string; diff --git a/LifeOS/install/hooks/Safety.hook.ts b/LifeOS/install/hooks/Safety.hook.ts index 4afd1b5f6..068e28659 100755 --- a/LifeOS/install/hooks/Safety.hook.ts +++ b/LifeOS/install/hooks/Safety.hook.ts @@ -39,7 +39,6 @@ import { statSync, writeFileSync, } from "node:fs"; -import { homedir } from "node:os"; import { join } from "node:path"; import { createHash } from "node:crypto"; import { @@ -48,17 +47,12 @@ import { type Classification, type ToolCall, } from "./lib/safety-classifier"; +import { getLifeosDir } from "./lib/paths"; const STDIN_CAP_BYTES = 2 * 1024 * 1024; const CACHE_MAX_BYTES = 10 * 1024 * 1024; -const HOME = homedir(); -const LIFEOS_DIR = process.env.LIFEOS_DIR - ? process.env.LIFEOS_DIR.replace(/^~(?=\/|$)/, HOME).replace( - /^\$\{?HOME\}?(?=\/|$)/, - HOME, - ) - : join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = getLifeosDir(); const STATE_DIR = join(LIFEOS_DIR, "MEMORY", "STATE"); const OBS_DIR = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY"); const CACHE_PATH = join(STATE_DIR, "permission-cache.json"); diff --git a/LifeOS/install/hooks/SatisfactionCapture.hook.ts b/LifeOS/install/hooks/SatisfactionCapture.hook.ts index 9385ec502..ac30ac9fc 100755 --- a/LifeOS/install/hooks/SatisfactionCapture.hook.ts +++ b/LifeOS/install/hooks/SatisfactionCapture.hook.ts @@ -31,6 +31,7 @@ import { getLearningCategory } from './lib/learning-utils'; import { getISOTimestamp, getPSTComponents } from './lib/time'; import { captureFailure } from '../LIFEOS/TOOLS/FailureCapture'; import { addRatingPulse } from './lib/isa-utils'; +import { getLifeosDir } from './lib/paths'; // ── Types ── @@ -63,7 +64,7 @@ interface SentimentResult { // ── Constants ── -const BASE_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude', 'LIFEOS'); +const BASE_DIR = getLifeosDir(); const SIGNALS_DIR = join(BASE_DIR, 'MEMORY', 'LEARNING', 'SIGNALS'); const RATINGS_FILE = join(SIGNALS_DIR, 'ratings.jsonl'); const LAST_RESPONSE_CACHE = join(BASE_DIR, 'MEMORY', 'STATE', 'last-response.txt'); diff --git a/LifeOS/install/hooks/SessionCleanup.hook.ts b/LifeOS/install/hooks/SessionCleanup.hook.ts index 340e58af2..0f85ffa9c 100755 --- a/LifeOS/install/hooks/SessionCleanup.hook.ts +++ b/LifeOS/install/hooks/SessionCleanup.hook.ts @@ -38,8 +38,9 @@ import { join } from 'path'; import { getISOTimestamp } from './lib/time'; import { setTabState, cleanupKittySession } from './lib/tab-setter'; import { readRegistry, writeRegistry, findArtifactPath, findActiveSessionByUUID } from './lib/isa-utils'; +import { getLifeosDir } from './lib/paths'; -const BASE_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude', 'LIFEOS'); +const BASE_DIR = getLifeosDir(); const MEMORY_DIR = join(BASE_DIR, 'MEMORY'); const STATE_DIR = join(MEMORY_DIR, 'STATE'); const WORK_DIR = join(MEMORY_DIR, 'WORK'); diff --git a/LifeOS/install/hooks/SettingsBackport.hook.ts b/LifeOS/install/hooks/SettingsBackport.hook.ts index b7e6ae0ff..c66346600 100755 --- a/LifeOS/install/hooks/SettingsBackport.hook.ts +++ b/LifeOS/install/hooks/SettingsBackport.hook.ts @@ -14,9 +14,9 @@ import { readFileSync } from 'fs'; import { execSync } from 'child_process'; import { homedir } from 'os'; import { join } from 'path'; -import { paiPath } from './lib/paths'; +import { getSettingsPath, paiPath } from './lib/paths'; -const SETTINGS_PATH = join(homedir(), '.claude', 'settings.json'); +const SETTINGS_PATH = getSettingsPath(); const BACKPORT = paiPath('TOOLS', 'SettingsBackport.ts'); let input: any; diff --git a/LifeOS/install/hooks/SkillSurface.hook.ts b/LifeOS/install/hooks/SkillSurface.hook.ts index f1daa1efb..f3bf104ae 100755 --- a/LifeOS/install/hooks/SkillSurface.hook.ts +++ b/LifeOS/install/hooks/SkillSurface.hook.ts @@ -11,6 +11,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs"; import { basename, dirname, join } from "node:path"; +import { getLifeosDir, getSkillsDir } from './lib/paths'; interface HookInput { prompt?: string; @@ -44,8 +45,8 @@ interface ScoredSkill { const STDIN_TIMEOUT_MS = 300; const MAX_SKILLS_TO_EMIT = 3; const MIN_DISTINCT_TRIGGER_TOKEN_HITS = 2; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(process.env.HOME || "", ".claude", "LIFEOS"); -const SKILLS_DIR = join(process.env.HOME || "", ".claude", "skills"); +const LIFEOS_DIR = getLifeosDir(); +const SKILLS_DIR = getSkillsDir(); const CACHE_PATH = join(LIFEOS_DIR, "MEMORY", "STATE", "skill-index.json"); async function readStdinWithTimeout(timeoutMs: number = STDIN_TIMEOUT_MS): Promise { diff --git a/LifeOS/install/hooks/SystemFileGuard.hook.ts b/LifeOS/install/hooks/SystemFileGuard.hook.ts index e6927ba88..e1e796f03 100755 --- a/LifeOS/install/hooks/SystemFileGuard.hook.ts +++ b/LifeOS/install/hooks/SystemFileGuard.hook.ts @@ -21,11 +21,10 @@ import { readFileSync, appendFileSync, existsSync, mkdirSync } from "node:fs"; import { dirname, join } from "node:path"; -import { homedir } from "node:os"; import { evaluateWrite, extractNewContent } from "./lib/system-file-guard-core"; +import { displayPath, paiPath } from "./lib/paths"; -const HOME = process.env.HOME ?? homedir(); -const LOG_PATH = join(HOME, ".claude/LIFEOS/MEMORY/OBSERVABILITY/system-file-guard.jsonl"); +const LOG_PATH = paiPath("MEMORY/OBSERVABILITY/system-file-guard.jsonl"); interface HookInput { session_id?: string; @@ -73,7 +72,7 @@ function denyMessage(relPath: string, pattern: string, match: string): string { " patterns. Move the user-specific content to a USER-zone location", " or read it through the LifeosConfig interface:", "", - " import { loadLifeosConfig, paiUserDir } from '~/.claude/LIFEOS/TOOLS/LifeosConfig';", + ` import { loadLifeosConfig, paiUserDir } from '${displayPath(paiPath("TOOLS/LifeosConfig"))}';`, "", " Canonical USER zones (any of these can hold the content):", " LIFEOS/USER/PRINCIPAL/ principal identity files", diff --git a/LifeOS/install/hooks/WorkCompletionLearning.hook.ts b/LifeOS/install/hooks/WorkCompletionLearning.hook.ts index 376e15ecd..7a06d3263 100755 --- a/LifeOS/install/hooks/WorkCompletionLearning.hook.ts +++ b/LifeOS/install/hooks/WorkCompletionLearning.hook.ts @@ -54,8 +54,9 @@ import { join } from 'path'; import { getISOTimestamp, getPSTDate } from './lib/time'; import { getLearningCategory } from './lib/learning-utils'; import { findArtifactPath, findActiveSessionByUUID } from './lib/isa-utils'; +import { getLifeosDir } from './lib/paths'; -const BASE_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude', 'LIFEOS'); +const BASE_DIR = getLifeosDir(); const MEMORY_DIR = join(BASE_DIR, 'MEMORY'); const WORK_DIR = join(MEMORY_DIR, 'WORK'); const LEARNING_DIR = join(MEMORY_DIR, 'LEARNING'); diff --git a/LifeOS/install/hooks/WritingGate.hook.ts b/LifeOS/install/hooks/WritingGate.hook.ts index a8710b37d..c0fb7a3c9 100755 --- a/LifeOS/install/hooks/WritingGate.hook.ts +++ b/LifeOS/install/hooks/WritingGate.hook.ts @@ -29,8 +29,9 @@ import { readHookInput, parseTranscriptFromInput } from "./lib/hook-io"; import { appendFileSync, mkdirSync, existsSync, readFileSync } from "fs"; import { createHash } from "crypto"; import { dirname, join } from "path"; +import { displayPath, getLifeosDir } from './lib/paths'; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, ".claude", "LIFEOS"); +const LIFEOS_DIR = getLifeosDir(); const OBS_PATH = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY", "writing-gate.jsonl"); const RUNS_PATH = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY", "pangram-runs.jsonl"); const RUN_WINDOW_MS = 30 * 60 * 1000; // a run counts as "this turn" within 30 min @@ -129,7 +130,7 @@ const BLOCK_REASON = "through the _WRITING skill AND the detector before it is shown (OPERATIONAL_RULES.md § Authored content). " + "The gate checks for a real PangramScore.ts run on the draft — a typed token does NOT satisfy it. Before " + "stopping: (1) run Skill(\"_WRITING\") DETECT mode on the draft and fix every P0/P1 in the right voice; " + - "(2) run `bun ~/.claude/LIFEOS/TOOLS/PangramScore.ts --file ` on the ACTUAL draft text so the run is " + + "(2) run `bun " + displayPath(join(getLifeosDir(), "TOOLS/PangramScore.ts")) + " --file ` on the ACTUAL draft text so the run is " + "logged; (3) cite the detect result + the reported AI% in a `✍️ WRITING-AUDIT:` line. Pangram saturates on " + "model prose, so the number is REPORTED, not a pass/fail bar — the requirement is that the audit RAN."; diff --git a/LifeOS/install/hooks/handlers/UpdateCounts.ts b/LifeOS/install/hooks/handlers/UpdateCounts.ts index 46f678872..3334e9bb4 100755 --- a/LifeOS/install/hooks/handlers/UpdateCounts.ts +++ b/LifeOS/install/hooks/handlers/UpdateCounts.ts @@ -22,6 +22,7 @@ import { readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; import { execSync } from 'child_process'; import { getLifeosDir } from '../lib/paths'; +import { getClaudeDir } from '../lib/paths'; /** * Refresh usage cache from Anthropic OAuth API. @@ -39,7 +40,7 @@ async function refreshUsageCache(paiDir: string): Promise { { encoding: 'utf-8', timeout: 3000 } ).trim(); } else { - const credPath = join(process.env.HOME || '', '.claude', '.credentials.json'); + const credPath = join(getClaudeDir(), '.credentials.json'); credJson = readFileSync(credPath, 'utf-8').trim(); } diff --git a/LifeOS/install/hooks/lib/identity.ts b/LifeOS/install/hooks/lib/identity.ts index 0ae796f9c..78b12b9dd 100755 --- a/LifeOS/install/hooks/lib/identity.ts +++ b/LifeOS/install/hooks/lib/identity.ts @@ -14,9 +14,9 @@ import { readFileSync, existsSync } from 'fs'; import { join } from 'path'; import { parse as parseYaml } from 'yaml'; import { loadLifeosConfig } from '../../LIFEOS/TOOLS/LifeosConfig'; +import { getSettingsPath, paiPath } from './paths'; -const HOME = process.env.HOME!; -const SETTINGS_PATH = join(HOME, '.claude/settings.json'); +const SETTINGS_PATH = getSettingsPath(); // Identity-file paths derive from LifeosConfig's userDir. On fresh installs where // LIFEOS_CONFIG.toml hasn't been created yet, fall back to the conventional @@ -26,7 +26,7 @@ function paiUserDir(): string { try { return loadLifeosConfig().paths.userDir; } catch { - return join(HOME, '.claude/LIFEOS/USER'); + return paiPath('USER'); } } const DA_IDENTITY_PATH = join(paiUserDir(), 'DIGITAL_ASSISTANT/DA_IDENTITY.md'); diff --git a/LifeOS/install/hooks/lib/notifications.ts b/LifeOS/install/hooks/lib/notifications.ts index 54a52f4e4..da3d246a8 100755 --- a/LifeOS/install/hooks/lib/notifications.ts +++ b/LifeOS/install/hooks/lib/notifications.ts @@ -7,9 +7,9 @@ import { readFileSync, writeFileSync, existsSync } from 'fs'; import { join } from 'path'; +import { paiPath } from './paths'; -const HOME = process.env.HOME!; -const PULSE_TOML_PATH = join(HOME, '.claude/LIFEOS/PULSE/PULSE.toml'); +const PULSE_TOML_PATH = paiPath('PULSE/PULSE.toml'); // ============================================================================ // Session Timing diff --git a/LifeOS/install/hooks/lib/paths.ts b/LifeOS/install/hooks/lib/paths.ts index 205c4ef7f..f1f64e3fe 100755 --- a/LifeOS/install/hooks/lib/paths.ts +++ b/LifeOS/install/hooks/lib/paths.ts @@ -25,13 +25,25 @@ export function expandPath(path: string): string { .replace(/^~(?=\/|$)/, home); } +/** + * Abbreviate the home prefix to `~` in a path shown to the user. Purely + * cosmetic — the absolute path would work as-is. Exists so that messages + * built from resolved path constants render byte-identical to the previous + * hardcoded `~/.claude/...` strings on a default-root install, and follow + * the usual home-relative display convention everywhere else. + */ +export function displayPath(path: string): string { + const home = homedir(); + return path.startsWith(home + '/') ? '~' + path.slice(home.length) : path; +} + /** * Get the LifeOS data directory (expanded). * * Priority: * 1. CLAUDE_PLUGIN_ROOT (plugin install) → /PAI * 2. LIFEOS_DIR env var (expanded) - * 3. ~/.claude/LIFEOS (live default — byte-identical to pre-plugin behavior) + * 3. /LIFEOS (getClaudeDir() — honors CLAUDE_CONFIG_DIR, defaults ~/.claude/LIFEOS) * * The CLAUDE_PLUGIN_ROOT guard MUST precede the LIFEOS_DIR check: in a packed * plugin, bin/pai exports LIFEOS_DIR equal to CLAUDE_PLUGIN_ROOT (the flattened @@ -51,7 +63,7 @@ export function getLifeosDir(): string { return expandPath(envLifeosDir); } - return join(homedir(), '.claude', 'LIFEOS'); + return join(getClaudeDir(), 'LIFEOS'); } /** @@ -59,8 +71,13 @@ export function getLifeosDir(): string { * * Plugin install: CLAUDE_PLUGIN_ROOT is the flattened plugin root that plays the * live ~/.claude role (skills/ and hooks/ sit directly under it, matching live - * .claude/skills and .claude/hooks). Live default: ~/.claude — byte-identical to - * pre-plugin behavior, since CLAUDE_PLUGIN_ROOT is unset on a normal install. + * .claude/skills and .claude/hooks). + * + * Live install: CLAUDE_CONFIG_DIR is Claude Code's own override for its config + * directory (multi-profile setups). When the harness runs from a non-default + * config dir, hooks inherit that env var, so honoring it keeps every LifeOS + * path inside the profile that actually loaded the hooks. Default: ~/.claude — + * byte-identical to prior behavior when neither env var is set. */ export function getClaudeDir(): string { const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT; @@ -69,6 +86,12 @@ export function getClaudeDir(): string { return expandPath(pluginRoot); } + const configDir = process.env.CLAUDE_CONFIG_DIR; + + if (configDir) { + return expandPath(configDir); + } + return join(homedir(), '.claude'); } diff --git a/LifeOS/install/hooks/lib/safety-classifier.ts b/LifeOS/install/hooks/lib/safety-classifier.ts index 9b7cbda1e..86fed1f0c 100755 --- a/LifeOS/install/hooks/lib/safety-classifier.ts +++ b/LifeOS/install/hooks/lib/safety-classifier.ts @@ -1,5 +1,6 @@ import { homedir } from "node:os"; import { resolve } from "node:path"; +import { getClaudeDir } from "./paths"; export type Decision = "allow" | "neutral"; @@ -31,7 +32,7 @@ export interface ToolCall { const HOME = homedir(); export const TRUSTED_PREFIXES: readonly string[] = [ - resolve(HOME, ".claude"), + getClaudeDir(), resolve(HOME, "Projects"), resolve(HOME, "LocalProjects"), resolve(HOME, "Downloads"), diff --git a/LifeOS/install/hooks/lib/system-file-guard-core.ts b/LifeOS/install/hooks/lib/system-file-guard-core.ts index 4eb835cd6..55de3377c 100644 --- a/LifeOS/install/hooks/lib/system-file-guard-core.ts +++ b/LifeOS/install/hooks/lib/system-file-guard-core.ts @@ -12,11 +12,10 @@ import { readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; -import { homedir } from "node:os"; import { isContained, isPatternAllowlisted, relativeToClaudeRoot } from "./containment-zones"; +import { getClaudeDir } from "./paths"; -const HOME = process.env.HOME ?? homedir(); -const CLAUDE_ROOT = join(HOME, ".claude"); +const CLAUDE_ROOT = getClaudeDir(); const DEFAULT_DENY_LIST_PATH = join(CLAUDE_ROOT, "skills/_LIFEOS/DENY_LIST.txt"); export type GuardClassification = "system" | "user" | "out-of-tree"; diff --git a/LifeOS/install/hooks/lib/work-config.ts b/LifeOS/install/hooks/lib/work-config.ts index 7e4643ac9..0c1b17501 100755 --- a/LifeOS/install/hooks/lib/work-config.ts +++ b/LifeOS/install/hooks/lib/work-config.ts @@ -26,11 +26,11 @@ */ import { existsSync, readFileSync, writeFileSync, chmodSync } from "fs"; import { join } from "path"; +import { displayPath, getClaudeDir, getLifeosDir } from "./paths"; declare const Bun: { spawnSync: (cmd: string[], opts?: any) => any }; -const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = getLifeosDir(); const REPO_JSON_PATH = join(LIFEOS_DIR, "USER", "WORK", "work_repo.json"); const COLUMNS_YAML_PATH = join(LIFEOS_DIR, "USER", "WORK", "config.yaml"); @@ -104,7 +104,7 @@ export function loadWorkConfig(): WorkConfig { if (!existsSync(REPO_JSON_PATH)) { return disabled( "missing", - "USER/WORK/work_repo.json missing — run `bun ~/.claude/skills/_ULWORK/Tools/SetWorkRepo.ts `", + `USER/WORK/work_repo.json missing — run \`bun ${displayPath(join(getClaudeDir(), "skills/_ULWORK/Tools/SetWorkRepo.ts"))} \``, ); } diff --git a/LifeOS/install/install.sh b/LifeOS/install/install.sh index 19a7de744..113508a6b 100755 --- a/LifeOS/install/install.sh +++ b/LifeOS/install/install.sh @@ -100,7 +100,10 @@ fi # ─── Step 2: Detect harness (no clobber) ───────────────────────── step "2/5 Detecting your harness" if [ -z "$LIFEOS_SKILLS_DIR" ]; then - if [ -d "$HOME/.claude" ]; then LIFEOS_SKILLS_DIR="$HOME/.claude/skills" + # CLAUDE_CONFIG_DIR is Claude Code's own config-dir override (multi-profile + # setups) — when set, it IS the config root, so probe it before the defaults. + if [ -n "${CLAUDE_CONFIG_DIR:-}" ] && [ -d "$CLAUDE_CONFIG_DIR" ]; then LIFEOS_SKILLS_DIR="$CLAUDE_CONFIG_DIR/skills" + elif [ -d "$HOME/.claude" ]; then LIFEOS_SKILLS_DIR="$HOME/.claude/skills" elif [ -d "$HOME/.config/claude" ]; then LIFEOS_SKILLS_DIR="$HOME/.config/claude/skills" else LIFEOS_SKILLS_DIR="$HOME/.claude/skills"; fi fi diff --git a/LifeOS/install/skills/LifeOS/Tools/InstallEngine.ts b/LifeOS/install/skills/LifeOS/Tools/InstallEngine.ts index c9970874b..d204863d7 100644 --- a/LifeOS/install/skills/LifeOS/Tools/InstallEngine.ts +++ b/LifeOS/install/skills/LifeOS/Tools/InstallEngine.ts @@ -548,6 +548,28 @@ function normalizeCommand(cmd: string): string { .trim(); } +/** + * Rewrite the default `~/.claude` config-root spellings inside hook commands to + * the actually-detected configRoot. The payload's hooks.json ships commands + * against the default root (`$HOME/.claude/...`); on a multi-profile setup + * (Claude Code's CLAUDE_CONFIG_DIR) those paths point at a different — usually + * nonexistent — profile, so every hook errors on startup. No-op when configRoot + * IS the default, keeping default installs byte-identical. Pure (no I/O). + */ +export function rewriteHooksConfigRoot(hooks: HooksMap, configRoot: string, homeDir: string): HooksMap { + if (resolve(configRoot) === resolve(join(homeDir, ".claude"))) return hooks; + const defaultRootPattern = /(\$\{HOME\}|\$HOME|~)\/\.claude(?=\/|\s|$)/g; + const rewritten: HooksMap = JSON.parse(JSON.stringify(hooks ?? {})); + for (const groups of Object.values(rewritten)) { + for (const group of groups) { + for (const h of group.hooks ?? []) { + if (typeof h.command === "string") h.command = h.command.replace(defaultRootPattern, configRoot); + } + } + } + return rewritten; +} + /** Identity key for a hook entry: http → url, else normalized command. */ function hookKey(h: HookEntry): string { if (h.type === "http" && h.url) return `http:${h.url}`; diff --git a/LifeOS/install/skills/LifeOS/Tools/InstallHooks.ts b/LifeOS/install/skills/LifeOS/Tools/InstallHooks.ts index 030fb9218..f3f60dd36 100755 --- a/LifeOS/install/skills/LifeOS/Tools/InstallHooks.ts +++ b/LifeOS/install/skills/LifeOS/Tools/InstallHooks.ts @@ -16,7 +16,7 @@ import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -import { detectDevTree, mergeHooks } from "./InstallEngine"; +import { detectDevTree, mergeHooks, rewriteHooksConfigRoot } from "./InstallEngine"; interface Args { configRoot: string; skillRoot: string; apply: boolean; allowDev: boolean; } @@ -59,7 +59,14 @@ function main(): void { console.log(JSON.stringify({ ok: false, error: `payload hooks.json not found at ${hooksJsonPath}` }, null, 2)); process.exit(1); } - const incoming = JSON.parse(readFileSync(hooksJsonPath, "utf-8"))?.hooks ?? {}; + // Payload commands are written against the default ~/.claude root; retarget + // them at the detected configRoot so non-default CLAUDE_CONFIG_DIR profiles + // get hooks that point at the files this installer actually deploys. + const incoming = rewriteHooksConfigRoot( + JSON.parse(readFileSync(hooksJsonPath, "utf-8"))?.hooks ?? {}, + configRoot, + process.env.HOME || "", + ); // The hook SCRIPTS (*.hook.ts|sh + lib/**) live beside hooks.json in the payload. // Merging hooks.json into settings.json wires commands that point at these files, diff --git a/LifeOS/install/skills/LifeOS/install/install.sh b/LifeOS/install/skills/LifeOS/install/install.sh index 19a7de744..113508a6b 100755 --- a/LifeOS/install/skills/LifeOS/install/install.sh +++ b/LifeOS/install/skills/LifeOS/install/install.sh @@ -100,7 +100,10 @@ fi # ─── Step 2: Detect harness (no clobber) ───────────────────────── step "2/5 Detecting your harness" if [ -z "$LIFEOS_SKILLS_DIR" ]; then - if [ -d "$HOME/.claude" ]; then LIFEOS_SKILLS_DIR="$HOME/.claude/skills" + # CLAUDE_CONFIG_DIR is Claude Code's own config-dir override (multi-profile + # setups) — when set, it IS the config root, so probe it before the defaults. + if [ -n "${CLAUDE_CONFIG_DIR:-}" ] && [ -d "$CLAUDE_CONFIG_DIR" ]; then LIFEOS_SKILLS_DIR="$CLAUDE_CONFIG_DIR/skills" + elif [ -d "$HOME/.claude" ]; then LIFEOS_SKILLS_DIR="$HOME/.claude/skills" elif [ -d "$HOME/.config/claude" ]; then LIFEOS_SKILLS_DIR="$HOME/.config/claude/skills" else LIFEOS_SKILLS_DIR="$HOME/.claude/skills"; fi fi