diff --git a/LifeOS/CUSTOM_HOME.md b/LifeOS/CUSTOM_HOME.md
new file mode 100644
index 0000000000..502df3d925
--- /dev/null
+++ b/LifeOS/CUSTOM_HOME.md
@@ -0,0 +1,142 @@
+# Custom LifeOS Home — design & implementation notes
+
+This document summarizes how LifeOS supports installing into a **custom home**
+(a config root other than `~/.claude`, e.g. a project-scoped `~/Project/.claude`
+so an isolated second LifeOS can live beside the global one), and how the
+codebase was made root-agnostic. It is a reviewer-facing companion to the PR;
+`INSTALL.md § 2.5` covers the user-facing launch story.
+
+## The override
+
+- **Env var `LIFEOS_HOME`** (highest priority) and **`install.sh --home
`**
+ (which exports it) select the config root. `detectEnv()` resolves
+ `configRoot = --config-root || LIFEOS_HOME || CLAUDE_CONFIG_DIR ||
+ dirname(LIFEOS_DIR) || ~/.claude`. `LIFEOS_DIR` is the persisted recovery path
+ for later sessions after the bootstrap shell exits.
+- `configDir` (private USER data) follows the custom home
+ (`/USER-data`), overridable with `--config-dir`, so two instances
+ never share USER data — isolation is the whole point. A direct custom
+ `--config-root` or `CLAUDE_CONFIG_DIR` gets the same isolated default. The legacy poisoned
+ `LIFEOS_CONFIG_DIR=/LIFEOS` value is ignored and repaired.
+
+The bootstrap temporarily launches Claude Code with
+`CLAUDE_CONFIG_DIR=` so `/lifeos-setup` can discover the staged skill
+from any caller directory. The installed `lifeos` launcher preserves normal
+global+project merging for `/.claude` roots by starting from the project
+parent; arbitrary custom roots use `CLAUDE_CONFIG_DIR` for the spawned process.
+
+## Three layers had to become root-aware
+
+**1. The install / deploy pipeline** (DetectEnv, DeployCore, InstallSettings,
+InstallHooks, ScaffoldUser, LinkUser, DeployComponents). These already accept
+`--config-root` / `--config-dir`; `InstallSettings` retargets the settings
+template's `~/.claude` strings (env values, permission globs, guidance) and
+`InstallHooks` retargets the payload's hook commands to ``. The
+written `settings.json` must contain **zero** `~/.claude` references.
+
+**2. The runtime layer** (the ~230 files under `LIFEOS/PULSE`, `LIFEOS/TOOLS`,
+`hooks/`, `skills/*`, plus service plists and shell scripts). Historically each
+computed `join(homedir(), ".claude")` inline, so a custom-home install would
+still read/write `~/.claude` at runtime — leaking state (e.g. `MEMORY/STATE/…`)
+into the wrong tree. This is the layer the PR makes root-agnostic.
+
+**3. The instruction layer** — the model-read markdown (SKILL.md files, skill
+workflows, `LIFEOS/DOCUMENTATION`, agents). These files carry literal shell
+commands and file references like `bun ~/.claude/LIFEOS/TOOLS/Foo.ts`, and the
+model executes them verbatim — on a custom-home install they fail (or worse,
+touch a parallel default install). The payload markdown uses semantic path
+placeholders in model-facing prose and tool instructions:
+
+- `{{LIFEOS_DIR}}` → `/LIFEOS` — for everything under `LIFEOS/`.
+- `{{LIFEOS_ROOT}}` → `` — for root-level paths (`skills/`,
+ `hooks/`, `agents/`, `History/`, `Pulse/`, `settings.json`, `.env`, …).
+- `{{LIFEOS_CONFIG_DIR}}` → the private USER-data home.
+
+`LoadContext.hook.ts` grounds all three placeholders with their concrete values
+at every `SessionStart`, including subagent sessions, and instructs the model to
+resolve them before invoking Read/Edit/Write/Glob/Grep/Bash. Claude Code does
+not natively substitute arbitrary `{{...}}` tokens; this is an explicit LifeOS
+model-context contract. The settings template still carries `LIFEOS_ROOT`,
+`LIFEOS_DIR`, and `LIFEOS_CONFIG_DIR` as environment variables for executable
+shell code and runtime processes. As a fail-safe, `PreToolGuard.hook.ts` rejects
+unresolved LifeOS placeholders in Bash commands and Read/Write/Edit/Glob/Grep
+path fields before a tool can touch the wrong tree; placeholder text in document
+content remains allowed.
+
+The two layers stay deliberately distinct: prose and model tool paths use
+`{{LIFEOS_*}}`; executable shell snippets use quoted variables such as
+`"${LIFEOS_DIR}/TOOLS/Doctor.ts"`. A skill's own `SKILL.md` may use Claude
+Code's native `${CLAUDE_SKILL_DIR}` substitution. Supporting workflow/reference
+files do not rely on that substitution because they are loaded later through
+Read rather than rendered as the skill entrypoint. Harness session storage is
+rooted at `{{LIFEOS_ROOT}}/projects/`, so arbitrary `CLAUDE_CONFIG_DIR` installs
+do not fall back to the real `~/.claude` tree.
+
+## How we changed all the paths — one pattern, applied mechanically
+
+There is a single source of truth for "where does LifeOS live", resolved in this
+order (never a bare hardcode):
+
+1. `CLAUDE_PLUGIN_ROOT` — packed-plugin install (flattened root plays `~/.claude`).
+2. `LIFEOS_DIR` env → `dirname()` — set in `settings.json` env and every service
+ plist, so all harness-spawned contexts resolve correctly.
+3. **Self-location** — walk up from the module's own file location to the ancestor
+ containing a `LIFEOS/` child. Covers a bare `bun Tool.ts` with no env, and is
+ what stops hook state leaking into `~/.claude` when a hook runs detached.
+4. `~/.claude` — plain-install default; byte-identical to the old behavior.
+
+Concretely:
+
+- **TypeScript runtime** — a dependency-free resolver `LIFEOS/TOOLS/lifeos-root.ts`
+ (exports `claudeDir()`, `lifeosDir()`, `claudePath()`, …). The hooks tree keeps
+ its own `hooks/lib/paths.ts` (`getClaudeDir()` / `getLifeosDir()`) so it survives
+ the plugin packer's tree flattening; both share the same 4-step logic incl. the
+ self-location fallback. Every file that hardcoded `join(HOME, ".claude", …)` was
+ rewritten to route through the resolver via an **idempotent codemod** covering
+ the idiom set (`join`/`resolve`/`path.join`/aliases, `${HOME}/.claude` template
+ literals, `HOME + "/.claude"` concatenation). The codemod is conservative: it
+ rewrites only well-defined path-building idioms and leaves comments, user-facing
+ prose, string comparisons (`entry === ".claude"`), and regexes untouched.
+- **launchd / systemd services** (Pulse, deriver, menu-bar) — the plist/service
+ templates use a `__LIFEOS_DIR__` / `__CLAUDE_DIR__` placeholder (not
+ `__HOME__/.claude`), and `manage.sh` / `manage-deriver.sh` / `MenuBar/install.sh`
+ derive their target from the **script's own location** (they ship inside
+ `/LIFEOS/PULSE`) and substitute it in. Plists also carry `LIFEOS_DIR`
+ in their environment so the spawned process resolves the same root. `manage.sh
+ render` prints the substituted unit without loading it (dry-run + test hook).
+- **Shell scripts** — hardcoded `$HOME/.claude/LIFEOS` became
+ `"${LIFEOS_DIR:-$HOME/.claude/LIFEOS}"` (env with default), or self-location
+ where the script sits in the tree.
+- **`Services.ts`** — the launchd control surface resolves its base via the
+ canonical resolver instead of `join(HOME, ".claude")`.
+
+## Backward compatibility
+
+For a default install (`LIFEOS_HOME` unset), every path resolves to `~/.claude`
+exactly as before — the resolver's default branch is byte-identical, the settings
+template's `~/.claude` globs are left untouched, and hook commands still emit
+`$HOME/.claude/...`.
+
+## Verification
+
+`Tools/InstallIntegrationTest.ts` (bun, throwaway `$HOME` under `mktemp`) proves it
+end-to-end:
+
+- **Scenario A** — full custom-home install; asserts everything lands under the
+ custom root and the written `settings.json` has **zero** `.claude` references,
+ the symlink contract holds, and nothing leaks to `/.claude`.
+- **Scenario B** — ambient legacy config-dir recovery plus explicit self-symlink guard.
+- **Scenario C** — default-install regression: with no `LIFEOS_HOME`, everything
+ lands in `~/.claude`; default permission globs and hook commands remain unchanged.
+- **Scenario D** — Pulse runtime + launchd wiring: the resolver ships under the
+ custom root, a PULSE module routes through it, and `manage.sh render` emits a
+ plist with **zero** `.claude`, `WorkingDirectory` and `LIFEOS_DIR` pointing at
+ the custom root, and no unresolved placeholder.
+- **Lifecycle/edge checks** — execute both deployed resolvers with root env vars
+ scrubbed, launch a fake Claude process through both the bootstrap and installed
+ launcher, recover a later session from `LIFEOS_DIR`, repair the legacy default
+ config-dir collision, verify direct `--config-root` / `CLAUDE_CONFIG_DIR`
+ isolation, exercise a root containing spaces, audit statusline/runtime commands,
+ and assert the packaged nested LifeOS installer matches the authoritative skill.
+
+The current suite passes **93/93** checks.
diff --git a/LifeOS/INSTALL.md b/LifeOS/INSTALL.md
index 2996cbd875..23bee063e5 100644
--- a/LifeOS/INSTALL.md
+++ b/LifeOS/INSTALL.md
@@ -53,6 +53,36 @@ bun Tools/DetectEnv.ts
Read its output. It reports the OS (macOS / Linux / Windows), the harness (Claude Code / Cursor / Cline / Codex / Gemini / other), the config root, and whether LifeOS is already present. **Every path below comes from this — don't assume `~/.claude` or any single harness.**
+### 2.5 The install-location choice — default home or a custom directory
+
+LifeOS normally installs into the harness's config root — right for a human who wants LifeOS in **every** session. But that makes LifeOS global: its `CLAUDE.md`, hooks, and settings load into *every* Claude Code session, including plain coding work. A human who also wants to use Claude Code **without** LifeOS should install it into a custom directory instead — LifeOS then loads only when launched against that root, and plain `claude` stays vanilla. The same mechanism gives you an **isolated second instance** (e.g. a project-scoped assistant in `~/MyProject/.claude`).
+
+**Ask once.** If `LIFEOS_HOME` is not already set, offer your human the choice now, before anything is written:
+
+> "Install LifeOS into the default location (``) so it's active in every session, or into a custom directory so you can keep using Claude Code without LifeOS and opt in per launch?"
+
+Default answer → proceed unchanged; don't mention custom homes again. Custom answer → export the directory they name as `LIFEOS_HOME` for **every** subsequent step (and re-run `DetectEnv` to confirm the resolved `configRoot`/`configDir`). If `LIFEOS_HOME` is already set (e.g. by `install.sh --home`), skip the question and just confirm the target.
+
+The override, settable up front instead:
+
+```
+export LIFEOS_HOME=~/MyProject/.claude # or: bash install.sh --home ~/MyProject/.claude
+```
+
+What it changes, and what it doesn't:
+
+- `LIFEOS_HOME` moves **only where LifeOS installs** (`configRoot`: `LIFEOS/`, `settings.json`, `hooks/`, `skills/`, `CLAUDE.md`). It is NOT `CLAUDE_CONFIG_DIR` — it doesn't relocate the harness's own config or disable merging with your global `~/.claude` at runtime.
+- The private user-data home (`configDir`, symlink target of `/LIFEOS/USER`) follows the custom home to `/USER-data`, so two instances never share one USER tree. Override with `--config-dir` on `ScaffoldUser`/`LinkUser` if you want it elsewhere.
+- The install tools retarget everything the harness reads literally — `settings.json` env values and permission globs, and every hook command — to the custom root. `DetectEnv` reports the resolved `configRoot`/`configDir`; verify the written `settings.json` contains no `~/.claude` references.
+- The bootstrap launches `/lifeos-setup` with a temporary `CLAUDE_CONFIG_DIR=` so Claude Code can discover the staged skill even when the installer was run from another directory. Written custom settings persist both `LIFEOS_HOME` and `LIFEOS_DIR`, so later setup/update runs recover the same root.
+
+**The runtime half (don't skip):** installing into a custom home only places the files — the harness must also LOAD them. For Claude Code there are two ways:
+
+1. **Project-scoped `.claude`** (recommended for a per-project assistant): install into `/.claude` and launch Claude Code from `` — it picks the directory's `.claude` up as project settings alongside your global config.
+2. **Whole-config relocation:** launch with `CLAUDE_CONFIG_DIR= claude` — the harness then uses ONLY that config dir (no global merge). This is the right tool when the instance should be fully self-contained.
+
+Wire the `lifeos` launch command (step 7) against the custom `` either way. The launcher preserves global+project merging for a `/.claude` root by launching from ``; for an arbitrary custom root it sets `CLAUDE_CONFIG_DIR` for the spawned process.
+
### 3. Scan for conflicts (read-only)
```
@@ -97,15 +127,15 @@ The payload ships the launcher — `install/LIFEOS/TOOLS/lifeos.ts` — which sp
- **Claude Code (zsh / bash)** — append to `~/.zshrc` (or `~/.bashrc`):
```
- alias lifeos='bun /LIFEOS/TOOLS/lifeos.ts -s /LIFEOS/LIFEOS_SYSTEM_PROMPT.md'
+ alias lifeos='bun "/LIFEOS/TOOLS/lifeos.ts" -s "/LIFEOS/LIFEOS_SYSTEM_PROMPT.md"'
```
- fish: `alias lifeos "bun /LIFEOS/TOOLS/lifeos.ts -s /LIFEOS/LIFEOS_SYSTEM_PROMPT.md"; funcsave lifeos`. After this, **`lifeos` launches Claude WITH the constitution**; plain `claude` stays vanilla (which is fine — the user opts in by launching `lifeos`).
+ fish: `alias lifeos 'bun "/LIFEOS/TOOLS/lifeos.ts" -s "/LIFEOS/LIFEOS_SYSTEM_PROMPT.md"'; funcsave lifeos`. After this, **`lifeos` launches Claude WITH the constitution and the matching custom settings/hooks/skills**; plain `claude` stays vanilla (which is fine — the user opts in by launching `lifeos`).
- **Any other harness** — use that harness's own system-prompt flag against the same file. e.g. pi: `pi --append-system-prompt /LIFEOS/LIFEOS_SYSTEM_PROMPT.md`. If a harness has no system-prompt flag, load `LIFEOS_SYSTEM_PROMPT.md` through its context file (AGENTS.md / rules) as the closest equivalent, and tell your human plainly that the constitution is loading as context, not as a true system-prompt layer.
If your human declines the shell edit, give them the one-line launch command to run by hand so the constitution still loads:
```
-bun /LIFEOS/TOOLS/lifeos.ts -s /LIFEOS/LIFEOS_SYSTEM_PROMPT.md
+bun "/LIFEOS/TOOLS/lifeos.ts" -s "/LIFEOS/LIFEOS_SYSTEM_PROMPT.md"
```
### 8. Choose the components — install all, or pick a subset (WITH PERMISSION)
diff --git a/LifeOS/Tools/ActivateImports.ts b/LifeOS/Tools/ActivateImports.ts
index c5e652fa95..645272a11d 100755
--- a/LifeOS/Tools/ActivateImports.ts
+++ b/LifeOS/Tools/ActivateImports.ts
@@ -11,7 +11,7 @@
*/
import { join } from "node:path";
-import { activateImports, detectDevTree } from "./InstallEngine";
+import { activateImports, detectDevTree, resolveConfigRoot } from "./InstallEngine";
function main(): void {
const a = process.argv.slice(2);
@@ -19,8 +19,7 @@ function main(): void {
const i = a.indexOf(f);
return i >= 0 && a[i + 1] && !a[i + 1].startsWith("--") ? a[i + 1] : undefined;
};
- const home = process.env.HOME || "";
- const configRoot = get("--config-root") || process.env.CLAUDE_CONFIG_DIR || join(home, ".claude");
+ const configRoot = resolveConfigRoot(get("--config-root"));
const apply = a.includes("--apply");
const allowDev = a.includes("--allow-dev");
diff --git a/LifeOS/Tools/DeployComponents.ts b/LifeOS/Tools/DeployComponents.ts
index dd3e33a547..05eb6fb18d 100755
--- a/LifeOS/Tools/DeployComponents.ts
+++ b/LifeOS/Tools/DeployComponents.ts
@@ -35,7 +35,7 @@
import { execFileSync } from "node:child_process";
import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
-import { copyMissing, detectDevTree } from "./InstallEngine";
+import { copyMissing, detectDevTree, resolveConfigRoot, shellQuote } from "./InstallEngine";
// Enhancement components are the à-la-carte half of setup. The "LifeOS Core"
// (skills + system prompt + base settings + CLAUDE.md) is installed by Setup's
@@ -155,9 +155,7 @@ function deployStatusline(ctx: Ctx): ComponentResult {
// Build the settings.json command from the ACTUAL install root (ctx.lifeosDir),
// not a hardcoded ~/.claude — a custom --config-root (e.g. ~/.claude-fable) places
// the script under its own LIFEOS/, and the old literal pointed at the wrong tree.
- const command = scriptPath.startsWith(`${ctx.home}/`)
- ? `$HOME/${scriptPath.slice(ctx.home.length + 1)}`
- : scriptPath;
+ const command = shellQuote(scriptPath);
if (!av.inLive && !av.inPayload) {
r.blockers.push(`LIFEOS_StatusLine.sh not in live tree (${scriptPath}) or payload`);
@@ -386,7 +384,7 @@ function deploy(component: Component, ctx: Ctx): ComponentResult {
function main(): void {
const a = process.argv.slice(2);
const home = process.env.HOME || "";
- const configRoot = arg(a, "--config-root") || process.env.CLAUDE_CONFIG_DIR || join(home, ".claude");
+ const configRoot = resolveConfigRoot(arg(a, "--config-root"));
const skillRoot = arg(a, "--skill-root") || join(import.meta.dir, "..");
const apply = a.includes("--apply");
const allowDev = a.includes("--allow-dev");
diff --git a/LifeOS/Tools/DeployCore.ts b/LifeOS/Tools/DeployCore.ts
index 3e1fd61b48..fca73c8051 100755
--- a/LifeOS/Tools/DeployCore.ts
+++ b/LifeOS/Tools/DeployCore.ts
@@ -23,9 +23,8 @@
*/
import { existsSync, mkdirSync, readdirSync } from "node:fs";
-import { homedir } from "node:os";
import { join } from "node:path";
-import { copyMissing, detectDevTree } from "./InstallEngine";
+import { copyMissing, detectDevTree, resolveConfigRoot } from "./InstallEngine";
// Runtime top-level entries this tool does NOT deploy:
// - USER shipped separately as a scaffold (ScaffoldUser) + symlinked (LinkUser)
@@ -166,8 +165,7 @@ function deployDependencies(payloadInstall: string, configRoot: string, apply: b
function main(): void {
const a = process.argv.slice(2);
- const home = process.env.HOME || homedir();
- const configRoot = arg(a, "--config-root") || process.env.CLAUDE_CONFIG_DIR || join(home, ".claude");
+ const configRoot = resolveConfigRoot(arg(a, "--config-root"));
const skillRoot = arg(a, "--skill-root") || join(import.meta.dir, "..");
const payloadInstall = join(skillRoot, "install");
const apply = a.includes("--apply");
diff --git a/LifeOS/Tools/InstallEngine.ts b/LifeOS/Tools/InstallEngine.ts
index 2d09ed9098..98f4d3632c 100644
--- a/LifeOS/Tools/InstallEngine.ts
+++ b/LifeOS/Tools/InstallEngine.ts
@@ -15,7 +15,7 @@
import { execSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
-import { join, resolve } from "node:path";
+import { basename, dirname, join, resolve } from "node:path";
// ── Types (inlined — the skill ships without the engine's types.ts) ──
@@ -72,9 +72,97 @@ export interface EnvDetection {
claudeMdExists: boolean;
homeDir: string;
configRoot: string;
+ /** Private user-data home (`/USER` is the symlink target). */
+ configDir: string;
timezone: string;
}
+// ── Root resolution (single source of truth for every setup Tool) ──
+
+/** Expand a LEADING $HOME / ${HOME} / ~ path segment. Mid-string refs are left alone. */
+export function expandHomePrefix(value: string, home: string): string {
+ if (!home) return value;
+ if (value === "$HOME" || value === "${HOME}" || value === "~") return home;
+ if (value.startsWith("$HOME/")) return home + value.slice("$HOME".length);
+ if (value.startsWith("${HOME}/")) return home + value.slice("${HOME}".length);
+ if (value.startsWith("~/")) return home + value.slice(1);
+ return value;
+}
+
+/** Quote one POSIX-shell argument, preserving readable output for safe paths. */
+export function shellQuote(value: string): string {
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) return value;
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
+}
+
+/** Recover `` from the persisted `/LIFEOS` runtime env. */
+export function configRootFromLifeosDir(value = process.env.LIFEOS_DIR): string | undefined {
+ if (!value) return undefined;
+ const home = process.env.HOME || homedir();
+ const expanded = expandHomePrefix(value, home);
+ return basename(expanded).toLowerCase() === "lifeos" ? dirname(expanded) : undefined;
+}
+
+/**
+ * Resolve the LifeOS config root (where LIFEOS/, settings.json, hooks/,
+ * CLAUDE.md land). Priority: explicit --config-root flag > LIFEOS_HOME (the
+ * dedicated LifeOS custom-home override — relocates ONLY LifeOS, not the
+ * harness's whole config dir) > CLAUDE_CONFIG_DIR (the harness's own override)
+ * > persisted LIFEOS_DIR parent > ~/.claude. Every setup Tool resolves its root through this one function so
+ * no Tool can drift to a different default (the "defensive fallbacks" lesson).
+ */
+export function resolveConfigRoot(flagValue?: string): string {
+ const home = process.env.HOME || homedir();
+ const raw = flagValue || process.env.LIFEOS_HOME || process.env.CLAUDE_CONFIG_DIR || configRootFromLifeosDir() || join(home, ".claude");
+ return expandHomePrefix(raw, home);
+}
+
+/**
+ * Resolve the private user-data home (configDir; `/USER` is the
+ * symlink target of `/LIFEOS/USER`). Priority: explicit
+ * --config-dir flag > custom-home default `/USER-data` when the
+ * config root is non-default (two LifeOS instances must NEVER share one USER
+ * tree — isolation is the point of a custom home) > LIFEOS_CONFIG_DIR >
+ * ~/.config/LIFEOS. The legacy poisoned value `/LIFEOS` is ignored.
+ */
+export function resolveConfigDir(configRoot: string, flagValue?: string): string {
+ const home = process.env.HOME || homedir();
+ if (flagValue) return expandHomePrefix(flagValue, home);
+ const defaultRoot = join(home, ".claude");
+ if (resolve(configRoot) !== resolve(defaultRoot)) return join(configRoot, "USER-data");
+ const envDir = process.env.LIFEOS_CONFIG_DIR;
+ if (envDir) {
+ const expanded = expandHomePrefix(envDir, home);
+ // Old settings used this runtime-system path as if it were the private data
+ // home. Falling through repairs reruns instead of merely refusing them.
+ if (resolve(expanded) !== resolve(join(configRoot, "LIFEOS"))) return expanded;
+ }
+ return join(home, ".config", "LIFEOS");
+}
+
+/**
+ * Guard the system/user separation contract BEFORE any linking: the data home
+ * `/USER` must never resolve to the live `/LIFEOS/USER`
+ * itself — LinkUser would symlink the dir onto itself and destroy the contract.
+ * Legacy ambient settings that used this value are repaired by
+ * `resolveConfigDir`; this guard remains the final defense for an explicitly
+ * supplied unsafe `--config-dir`. Fail LOUD instead.
+ */
+export function checkUserDataSeparation(configRoot: string, configDir: string): { ok: boolean; detail: string } {
+ const liveUserDir = resolve(join(configRoot, "LIFEOS", "USER"));
+ const dataUserDir = resolve(join(configDir, "USER"));
+ if (liveUserDir === dataUserDir) {
+ return {
+ ok: false,
+ detail:
+ `user-data home ${dataUserDir} IS the live /LIFEOS/USER — refusing to symlink a directory onto itself. ` +
+ `Likely cause: LIFEOS_CONFIG_DIR in the environment points at /LIFEOS (the settings.json env value) ` +
+ `instead of the private data home. Pass an explicit --config-dir outside /LIFEOS.`,
+ };
+ }
+ return { ok: true, detail: `${dataUserDir} is distinct from the live USER dir` };
+}
+
// ── Low-level probes (from engine detect.ts, unchanged) ──
function tryExec(cmd: string): string | null {
@@ -166,6 +254,15 @@ export function detectEnv(): EnvDetection {
const home = homedir();
const os = detectOS();
const harness = detectHarness(home);
+ // Explicit/persisted root signals relocate the LifeOS install root (and with
+ // it the skills dir) without changing which harness was detected. LIFEOS_DIR
+ // is what settings persist into later sessions after LIFEOS_HOME is gone.
+ const persistedRoot = configRootFromLifeosDir();
+ if (process.env.LIFEOS_HOME || process.env.CLAUDE_CONFIG_DIR || persistedRoot) {
+ const overrideRoot = resolveConfigRoot();
+ harness.configRoot = overrideRoot;
+ harness.skillsDir = join(overrideRoot, "skills");
+ }
const configRoot = harness.configRoot || join(home, ".claude");
const settingsPath = join(configRoot, "settings.json");
const claudeMdPath = join(configRoot, "CLAUDE.md");
@@ -187,6 +284,7 @@ export function detectEnv(): EnvDetection {
claudeMdExists: existsSync(claudeMdPath),
homeDir: home,
configRoot,
+ configDir: resolveConfigDir(configRoot),
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
};
}
@@ -482,6 +580,12 @@ export function setupUserSeparation(
const liveUserDir = join(configRoot, "LIFEOS", "USER");
const dataUserDir = join(configDir, "USER");
+ // Guard: never link a dir onto itself (see checkUserDataSeparation).
+ const separation = checkUserDataSeparation(configRoot, configDir);
+ if (!separation.ok) {
+ return { action: "linked", target: dataUserDir, copied: 0, error: separation.detail };
+ }
+
// Branch (a): already a correct symlink → no-op.
if (existsSync(liveUserDir)) {
const st = lstatSync(liveUserDir);
diff --git a/LifeOS/Tools/InstallHooks.ts b/LifeOS/Tools/InstallHooks.ts
index 030fb92181..9298edc63c 100755
--- a/LifeOS/Tools/InstallHooks.ts
+++ b/LifeOS/Tools/InstallHooks.ts
@@ -9,14 +9,20 @@
* The skill's Setup workflow shows the user the exact change (from the dry-run
* counts) and gets explicit permission BEFORE calling this with --apply.
*
+ * Custom LifeOS home (LIFEOS_HOME / --config-root ≠ ~/.claude): the payload's
+ * hook commands ship as `$HOME/.claude/hooks/...`; they are retargeted to the
+ * real `/hooks/...` (absolute — the shell evaluates hook commands,
+ * but the hooks live under configRoot, not under any env var Claude Code
+ * guarantees) before the merge. A default install merges the payload verbatim.
+ *
* Usage:
* bun InstallHooks.ts [--config-root ] [--skill-root ] [--apply] [--allow-dev]
* (dry-run by default — reports added/skipped without writing)
*/
import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
-import { join } from "node:path";
-import { detectDevTree, mergeHooks } from "./InstallEngine";
+import { join, resolve } from "node:path";
+import { detectDevTree, mergeHooks, resolveConfigRoot, shellQuote } from "./InstallEngine";
interface Args { configRoot: string; skillRoot: string; apply: boolean; allowDev: boolean; }
@@ -26,15 +32,47 @@ function parseArgs(): Args {
const i = a.indexOf(flag);
return i >= 0 && a[i + 1] && !a[i + 1].startsWith("--") ? a[i + 1] : undefined;
};
- const home = process.env.HOME || "";
return {
- configRoot: get("--config-root") || process.env.CLAUDE_CONFIG_DIR || join(home, ".claude"),
+ configRoot: resolveConfigRoot(get("--config-root")),
skillRoot: get("--skill-root") || join(import.meta.dir, ".."),
apply: a.includes("--apply"),
allowDev: a.includes("--allow-dev"),
};
}
+// Every spelling of the default root the payload's hook commands may use.
+const DEFAULT_ROOT_FORMS = ["${HOME}/.claude", "$HOME/.claude", "~/.claude"];
+
+/**
+ * Retarget the default-root spellings in every incoming `command` string to the
+ * real config root (custom home only). Mutates in place; returns replacements.
+ */
+function retargetHookCommands(incoming: Record }>>, configRoot: string): number {
+ let replaced = 0;
+ const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ for (const groups of Object.values(incoming ?? {})) {
+ if (!Array.isArray(groups)) continue;
+ for (const group of groups) {
+ if (!Array.isArray(group.hooks)) continue;
+ for (const h of group.hooks) {
+ if (typeof h.command !== "string") continue;
+ let cmd = h.command;
+ for (const form of DEFAULT_ROOT_FORMS) {
+ // Quote the complete path token, not just the root, so custom homes
+ // containing spaces/metacharacters remain one safe shell argument.
+ const pathToken = new RegExp(`${escapeRegExp(form)}([^\\s;&|<>()"']*)`, "g");
+ cmd = cmd.replace(pathToken, (_match, suffix: string) => {
+ replaced++;
+ return shellQuote(configRoot + suffix);
+ });
+ }
+ h.command = cmd;
+ }
+ }
+ }
+ return replaced;
+}
+
function countFilesRec(dir: string): number {
if (!existsSync(dir)) return 0;
let n = 0;
@@ -61,6 +99,10 @@ function main(): void {
}
const incoming = JSON.parse(readFileSync(hooksJsonPath, "utf-8"))?.hooks ?? {};
+ const home = process.env.HOME || "";
+ const isCustomRoot = home !== "" && resolve(configRoot) !== resolve(join(home, ".claude"));
+ const commandsRetargeted = isCustomRoot ? retargetHookCommands(incoming, configRoot) : 0;
+
// 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,
// so they MUST be copied onto disk too — else every hook resolves to a nonexistent
@@ -79,13 +121,16 @@ function main(): void {
const { merged, added, skipped } = mergeHooks(existingHooks as never, incoming);
- const report = { ok: true, apply, settingsPath, added, skipped, events: Object.keys(merged).length, hooksDestDir, hookFiles };
+ const report = { ok: true, apply, settingsPath, added, skipped, events: Object.keys(merged).length, hooksDestDir, hookFiles, customRoot: isCustomRoot ? configRoot : undefined, commandsRetargeted: isCustomRoot ? commandsRetargeted : undefined };
if (!apply) {
console.log(JSON.stringify({ ...report, dryRun: true, note: "no changes written; re-run with --apply after permission" }, null, 2));
process.exit(0);
}
+ // A custom home (and ~/.claude on a clean machine) may not exist yet.
+ mkdirSync(configRoot, { recursive: true });
+
// Back up settings.json before writing (only if it exists).
let backup: string | undefined;
if (existsSync(settingsPath)) {
diff --git a/LifeOS/Tools/InstallIntegrationTest.ts b/LifeOS/Tools/InstallIntegrationTest.ts
new file mode 100644
index 0000000000..ea8cdaf1bb
--- /dev/null
+++ b/LifeOS/Tools/InstallIntegrationTest.ts
@@ -0,0 +1,556 @@
+#!/usr/bin/env bun
+/**
+ * InstallIntegrationTest — end-to-end proof of the custom-LifeOS-home install
+ * (LIFEOS_HOME), plus a default-install regression check. Everything runs
+ * against a THROWAWAY fake $HOME under mktemp — the real home is never touched,
+ * which doubles as the isolation proof.
+ *
+ * Scenarios:
+ * A. Custom home: full tool chain (DetectEnv → InstallSettings → DeployCore →
+ * InstallHooks → CLAUDE.md overlay → ScaffoldUser → LinkUser →
+ * ActivateImports → DeployComponents statusline) into
+ * `/lifeos-home`, then asserts: everything landed under the
+ * custom root, the written settings.json (env values, permission globs,
+ * hook commands) contains ZERO `.claude` references, the symlink contract
+ * holds against `/USER-data`, identity imports activated, and
+ * nothing was written to `/.claude` or `/.config/LIFEOS`.
+ * B. Legacy recovery + explicit self-symlink guard: an ambient legacy
+ * LIFEOS_CONFIG_DIR value is repaired, while an explicitly requested
+ * unsafe data home is still refused before any filesystem mutation.
+ * C. Default regression: with NO LIFEOS_HOME the install lands in
+ * `/.claude` and the settings template ships byte-identical
+ * (`~/.claude` permission globs untouched, `$HOME`-expanded env values).
+ *
+ * Requires network once for `bun install` (DeployCore's dependency step) unless
+ * bun's cache already holds the runtime deps.
+ *
+ * Usage:
+ * bun InstallIntegrationTest.ts [--keep]
+ * (--keep retains the temp workspace; it is always retained on failure)
+ */
+
+import { chmodSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { basename, dirname, join } from "node:path";
+
+const toolsDir = import.meta.dir;
+const skillRoot = join(toolsDir, "..");
+
+interface Check { scenario: string; name: string; passed: boolean; detail?: string }
+const checks: Check[] = [];
+function check(scenario: string, name: string, passed: boolean, detail?: string): void {
+ checks.push({ scenario, name, passed, detail: passed ? undefined : detail });
+}
+
+/** Env for a spawned tool: real PATH etc., fake HOME, LifeOS path vars scrubbed. */
+function toolEnv(fakeHome: string, extra: Record = {}): Record {
+ const env: Record = {};
+ const scrub = new Set(["LIFEOS_HOME", "CLAUDE_CONFIG_DIR", "LIFEOS_CONFIG_DIR", "LIFEOS_DIR", "LIFEOS_ROOT", "CLAUDE_PLUGIN_ROOT"]);
+ for (const [k, v] of Object.entries(process.env)) {
+ if (v === undefined || scrub.has(k)) continue;
+ env[k] = v;
+ }
+ env.HOME = fakeHome;
+ Object.assign(env, extra);
+ return env;
+}
+
+function runTool(tool: string, args: string[], env: Record): { code: number; json: Record | null; raw: string } {
+ const proc = Bun.spawnSync(["bun", join(toolsDir, tool), ...args], { env, stdout: "pipe", stderr: "pipe" });
+ const out = proc.stdout.toString();
+ let json: Record | null = null;
+ try { json = JSON.parse(out); } catch { /* non-JSON output stays raw */ }
+ return { code: proc.exitCode ?? -1, json, raw: out + proc.stderr.toString() };
+}
+
+function isQuotedAt(line: string, position: number): boolean {
+ let single = false;
+ let double = false;
+ let escaped = false;
+ for (let i = 0; i < position; i++) {
+ const char = line[i];
+ if (escaped) { escaped = false; continue; }
+ if (char === "\\" && !single) { escaped = true; continue; }
+ if (char === "'" && !double) single = !single;
+ if (char === '"' && !single) double = !double;
+ }
+ return single || double;
+}
+
+/** Enforce the boundary between model placeholders and executable shell vars. */
+function markdownPathConventionViolations(root: string): string[] {
+ const violations: string[] = [];
+ const files: string[] = [];
+ const walk = (dir: string): void => {
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
+ const path = join(dir, entry.name);
+ if (entry.isDirectory()) walk(path);
+ else if (entry.isFile() && entry.name.endsWith(".md")) files.push(path);
+ }
+ };
+ walk(root);
+
+ for (const file of files) {
+ let fence: string | null = null;
+ const inlineShellCommand = /^\s*(?:bun(?:\s+run)?|bash|sh|zsh|cd|ls|rg|grep|cat|tail|head|find|cp|mv|mkdir|touch|chmod|source|echo|jq|rsync|fabric|git)\b/;
+ const lines = readFileSync(file, "utf-8").split("\n");
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+ const marker = line.match(/^\s*```([^`]*)/);
+ if (marker) {
+ fence = fence === null ? marker[1].trim().toLowerCase() : null;
+ continue;
+ }
+ const shell = ["bash", "sh", "shell", "zsh"].includes(fence ?? "") || line.trimStart().startsWith("!`");
+ const label = `${file}:${i + 1}`;
+ const modelFacingLine = shell ? line : line.replace(/`([^`\n]+)`/g, (span, code: string) => {
+ if (!inlineShellCommand.test(code)) return span;
+ if (/\{\{LIFEOS_(?:ROOT|DIR|CONFIG_DIR)\}\}/.test(code)) {
+ violations.push(`${label}: model placeholder in inline shell command`);
+ }
+ if (/\$LIFEOS_(?:ROOT|DIR|CONFIG_DIR)\b/.test(code)) {
+ violations.push(`${label}: unbraced variable in inline shell command`);
+ }
+ for (const match of code.matchAll(/\$\{(?:LIFEOS_(?:ROOT|DIR|CONFIG_DIR)|CLAUDE_SKILL_DIR)\}/g)) {
+ if (!isQuotedAt(code, match.index ?? 0)) violations.push(`${label}: unquoted inline shell path`);
+ }
+ return "";
+ });
+ if (shell) {
+ if (!line.trimStart().startsWith("#") && /\{\{LIFEOS_(?:ROOT|DIR|CONFIG_DIR)\}\}/.test(line)) {
+ violations.push(`${label}: model placeholder in executable shell`);
+ }
+ if (/\$LIFEOS_(?:ROOT|DIR|CONFIG_DIR)\b/.test(line)) {
+ violations.push(`${label}: unbraced LifeOS shell variable`);
+ }
+ for (const match of line.matchAll(/\$\{(?:LIFEOS_(?:ROOT|DIR|CONFIG_DIR)|CLAUDE_SKILL_DIR)\}/g)) {
+ if (!isQuotedAt(line, match.index ?? 0)) violations.push(`${label}: unquoted shell path variable`);
+ }
+ } else if (/\$\{?LIFEOS_(?:ROOT|DIR|CONFIG_DIR)\}?/.test(modelFacingLine)) {
+ violations.push(`${label}: shell variable in model-facing content`);
+ }
+ if (basename(file) !== "SKILL.md" && /\$\{CLAUDE_SKILL_DIR\}/.test(line)) {
+ violations.push(`${label}: CLAUDE_SKILL_DIR outside rendered SKILL.md`);
+ }
+ if (/(?:bun|bash|sh|rg|grep|ls|cd)\s+\$\{CLAUDE_SKILL_DIR\}/.test(line)) {
+ violations.push(`${label}: unquoted CLAUDE_SKILL_DIR command path`);
+ }
+ }
+ }
+ return violations;
+}
+
+const keep = process.argv.includes("--keep");
+const workRoot = mkdtempSync(join(tmpdir(), "lifeos-install-test-"));
+
+// ─── Scenario A: custom home ─────────────────────────────────────────
+{
+ const S = "A:custom-home";
+ const fakeHome = join(workRoot, "home-custom");
+ const configRoot = join(fakeHome, "lifeos home"); // spaces exercise shell-safe command generation; deliberately NO ".claude"
+ const configDir = join(configRoot, "USER-data");
+ const env = toolEnv(fakeHome, { LIFEOS_HOME: configRoot });
+ const flags = ["--config-root", configRoot]; // belt-and-suspenders on top of LIFEOS_HOME, like the Setup workflow passes
+
+ // DetectEnv resolves the override
+ const detect = runTool("DetectEnv.ts", [], env);
+ check(S, "DetectEnv reports the custom configRoot", detect.json?.configRoot === configRoot, detect.raw);
+ check(S, "DetectEnv reports configDir = /USER-data", detect.json?.configDir === configDir, detect.raw);
+
+ // InstallSettings
+ const settingsRun = runTool("InstallSettings.ts", [...flags, "--skill-root", skillRoot, "--apply"], env);
+ const settingsPath = join(configRoot, "settings.json");
+ check(S, "InstallSettings ok", settingsRun.code === 0 && settingsRun.json?.ok === true, settingsRun.raw);
+ check(S, "settings.json written under the custom root", existsSync(settingsPath));
+
+ // Managed root variables must be repaired, not preserved as arbitrary user
+ // overrides: otherwise the next model/session context points at two trees.
+ if (existsSync(settingsPath)) {
+ const stale = JSON.parse(readFileSync(settingsPath, "utf-8")) as Record;
+ stale.env.LIFEOS_ROOT = join(fakeHome, ".claude");
+ stale.env.LIFEOS_DIR = join(fakeHome, ".claude", "LIFEOS");
+ stale.env.LIFEOS_HOME = join(fakeHome, ".claude");
+ writeFileSync(settingsPath, JSON.stringify(stale, null, 2) + "\n");
+ const repairedRun = runTool("InstallSettings.ts", [...flags, "--skill-root", skillRoot, "--apply"], env);
+ const repaired = JSON.parse(readFileSync(settingsPath, "utf-8")) as Record;
+ check(S, "InstallSettings repairs stale managed root variables", repairedRun.code === 0 &&
+ repaired.env?.LIFEOS_ROOT === configRoot &&
+ repaired.env?.LIFEOS_DIR === join(configRoot, "LIFEOS") &&
+ repaired.env?.LIFEOS_HOME === configRoot, repairedRun.raw + JSON.stringify(repaired.env));
+ }
+
+ // DeployCore (skills + runtime + MEMORY scaffold + deps)
+ const core = runTool("DeployCore.ts", [...flags, "--skill-root", skillRoot, "--apply"], env);
+ check(S, "DeployCore ok", core.code === 0 && core.json?.ok === true, core.raw.slice(0, 2000));
+ check(S, "LIFEOS runtime under the custom root", existsSync(join(configRoot, "LIFEOS", "VERSION")) || existsSync(join(configRoot, "LIFEOS")));
+ check(S, "skills library under the custom root", existsSync(join(configRoot, "skills")));
+
+ // InstallHooks (merge + script deploy)
+ const hooks = runTool("InstallHooks.ts", [...flags, "--skill-root", skillRoot, "--apply"], env);
+ check(S, "InstallHooks ok", hooks.code === 0 && hooks.json?.ok === true, hooks.raw);
+ check(S, "hook commands retargeted (>0)", typeof hooks.json?.commandsRetargeted === "number" && (hooks.json.commandsRetargeted as number) > 0, hooks.raw);
+ check(S, "hook scripts deployed under the custom root", existsSync(join(configRoot, "hooks", "lib", "paths.ts")));
+
+ // The whole settings.json — env values, permission globs, guidance prose, merged
+ // hook commands — must now reference ONLY the custom root.
+ if (existsSync(settingsPath)) {
+ const settingsRaw = readFileSync(settingsPath, "utf-8");
+ const settings = JSON.parse(settingsRaw) as Record;
+ check(S, "settings.json has ZERO .claude references", !settingsRaw.includes(".claude"),
+ `found: ${(settingsRaw.match(/[^"\s]*\.claude[^"\s]*/g) || []).slice(0, 5).join(", ")}`);
+ check(S, "env.LIFEOS_DIR points at the custom root", settings.env?.LIFEOS_DIR === join(configRoot, "LIFEOS"), String(settings.env?.LIFEOS_DIR));
+ check(S, "env.LIFEOS_CONFIG_DIR points at the user-data home", settings.env?.LIFEOS_CONFIG_DIR === configDir, String(settings.env?.LIFEOS_CONFIG_DIR));
+ check(S, "env.LIFEOS_ROOT points at the custom root", settings.env?.LIFEOS_ROOT === configRoot, String(settings.env?.LIFEOS_ROOT));
+ check(S, "env.LIFEOS_HOME persists the custom root", settings.env?.LIFEOS_HOME === configRoot, String(settings.env?.LIFEOS_HOME));
+ check(S, "permission globs carry the custom root", JSON.stringify(settings.permissions?.allow ?? []).includes(`Write(${configRoot}/**)`), "no retargeted Write glob found");
+ const hookCmds = JSON.stringify(settings.hooks ?? {});
+ check(S, "merged hook commands carry the custom root", hookCmds.includes(join(configRoot, "hooks")), hookCmds.slice(0, 500));
+ check(S, "hook paths with spaces are shell-quoted", hookCmds.includes(`'${join(configRoot, "hooks")}`), hookCmds.slice(0, 500));
+ check(S, "unresolved-path guard covers model path tools", hookCmds.includes("Read|Glob|Grep"), hookCmds.slice(0, 1000));
+ }
+
+ // Instruction layer: deployed model-read markdown must not carry executable
+ // default-root paths — the `~/.claude/LIFEOS` form is what the payload codemod
+ // rewrote to `$LIFEOS_DIR`/`$LIFEOS_ROOT` (changelogs are historical records).
+ {
+ const mdRoots = [join(configRoot, "skills"), join(configRoot, "LIFEOS", "DOCUMENTATION")].filter((d) => existsSync(d));
+ const g = Bun.spawnSync(["grep", "-rlE", "(~|\\$HOME|\\$\\{HOME\\})/\\.claude/LIFEOS", "--include=*.md", ...mdRoots]);
+ const hits = g.stdout.toString().trim().split("\n").filter(Boolean).filter((f) => !/changelog/i.test(f));
+ check(S, "deployed markdown has no hardcoded ~/.claude/LIFEOS paths", mdRoots.length > 0 && hits.length === 0, hits.slice(0, 5).join(", ") || `scanned: ${mdRoots.join(", ")}`);
+ }
+
+ const conventionViolations = markdownPathConventionViolations(join(skillRoot, "install"));
+ check(S, "markdown separates model placeholders from quoted shell variables",
+ conventionViolations.length === 0, conventionViolations.slice(0, 20).join("\n"));
+
+ // CLAUDE.md overlay (Setup step 4 does this by hand) + ScaffoldUser + LinkUser + ActivateImports
+ cpSync(join(skillRoot, "install", "CLAUDE.template.md"), join(configRoot, "CLAUDE.md"));
+ const scaffold = runTool("ScaffoldUser.ts", [...flags, "--config-dir", configDir, "--skill-root", skillRoot, "--apply"], env);
+ check(S, "ScaffoldUser ok", scaffold.code === 0 && scaffold.json?.ok === true, scaffold.raw);
+ check(S, "USER tree scaffolded into /USER-data", existsSync(join(configDir, "USER", "PROJECTS.md")));
+
+ const link = runTool("LinkUser.ts", [...flags, "--config-dir", configDir, "--apply"], env);
+ check(S, "LinkUser ok + contract passed", link.code === 0 && (link.json?.contract as any)?.passed === true, link.raw);
+ const liveUser = join(configRoot, "LIFEOS", "USER");
+ check(S, "LIFEOS/USER symlink → /USER-data/USER",
+ existsSync(liveUser) && lstatSync(liveUser).isSymbolicLink() && readlinkSync(liveUser) === join(configDir, "USER"),
+ existsSync(liveUser) ? String(lstatSync(liveUser).isSymbolicLink() && readlinkSync(liveUser)) : "missing");
+
+ const imports = runTool("ActivateImports.ts", [...flags, "--apply"], env);
+ const activated = (imports.json?.activated as string[]) ?? [];
+ check(S, "ActivateImports activated identity imports (>0)", imports.code === 0 && activated.length > 0, imports.raw);
+ check(S, "every activated import resolves under the custom root",
+ activated.every((imp) => existsSync(join(configRoot, imp.replace(/^@/, "")))), activated.join(", "));
+
+ // Statusline enhancement wires against the custom root
+ const status = runTool("DeployComponents.ts", [...flags, "--skill-root", skillRoot, "--apply", "--components", "statusline"], env);
+ check(S, "DeployComponents statusline ok", status.code === 0 && status.json?.ok === true, status.raw.slice(0, 1500));
+ if (existsSync(settingsPath)) {
+ const cmd = (JSON.parse(readFileSync(settingsPath, "utf-8")) as any).statusLine?.command ?? "";
+ check(S, "statusLine command targets the custom root", cmd.includes("lifeos home/LIFEOS/LIFEOS_StatusLine.sh"), cmd);
+ check(S, "statusLine path with spaces is shell-quoted", cmd === `'${join(configRoot, "LIFEOS", "LIFEOS_StatusLine.sh")}'`, cmd);
+ }
+
+ // Isolation: nothing leaked into the default locations of the (fake) home.
+ check(S, "no writes to /.claude", !existsSync(join(fakeHome, ".claude")));
+ check(S, "no writes to /.config/LIFEOS", !existsSync(join(fakeHome, ".config", "LIFEOS")));
+
+ // ─── Scenario D: Pulse runtime + launchd wiring is custom-home-aware ──
+ // DeployCore (above) shipped LIFEOS/PULSE + LIFEOS/TOOLS/lifeos-root.ts into the
+ // custom root. The whole runtime layer must resolve the custom root — not ~/.claude.
+ // configRoot here is `/lifeos-home` (NO ".claude" in the name), so a clean
+ // "zero .claude" assertion catches any surviving hardcode, exactly like settings.json.
+ const SP = "D:pulse-custom-home";
+ const lifeosDir = join(configRoot, "LIFEOS");
+ const pulseDir = join(lifeosDir, "PULSE");
+ const statuslinePath = join(lifeosDir, "LIFEOS_StatusLine.sh");
+
+ check(SP, "canonical resolver lifeos-root.ts deployed under custom root", existsSync(join(lifeosDir, "TOOLS", "lifeos-root.ts")));
+ check(SP, "a PULSE module routes through the resolver (codemod shipped)",
+ existsSync(join(pulseDir, "modules", "memory.ts")) && readFileSync(join(pulseDir, "modules", "memory.ts"), "utf-8").includes("lifeos-root"),
+ "memory.ts does not import lifeos-root");
+ if (existsSync(statuslinePath)) {
+ const statusline = readFileSync(statuslinePath, "utf-8");
+ const statuslineLeaks = [
+ 'CLAUDE_HOME="$HOME/.claude"',
+ 'source "$HOME/.claude/.env"',
+ 'bun "$HOME/.claude/LIFEOS/TOOLS/GetCounts.ts"',
+ '"$HOME"/.claude/projects/',
+ ].filter((needle) => statusline.includes(needle));
+ check(SP, "statusline internals use the resolved custom root", statuslineLeaks.length === 0, statuslineLeaks.join(", "));
+ }
+
+ const pulsePlistTpl = join(pulseDir, "com.lifeos.pulse.plist");
+ if (existsSync(pulsePlistTpl)) {
+ const tpl = readFileSync(pulsePlistTpl, "utf-8");
+ check(SP, "pulse plist template is __LIFEOS_DIR__-based (no __HOME__/.claude)",
+ tpl.includes("__LIFEOS_DIR__") && !tpl.includes("__HOME__/.claude"), tpl.match(/__HOME__\/\.claude\S*/)?.[0] ?? "");
+ }
+
+ // `manage.sh render` performs the real substitution (self-locating its own dir) —
+ // no launchctl load, so it is safe in CI.
+ const manage = join(pulseDir, "manage.sh");
+ check(SP, "manage.sh deployed under custom root", existsSync(manage));
+ if (existsSync(manage)) {
+ const render = Bun.spawnSync(["bash", manage, "render"], { env, stdout: "pipe", stderr: "pipe" });
+ const plist = render.stdout.toString();
+ check(SP, "rendered pulse plist has ZERO .claude references", plist.length > 0 && !plist.includes(".claude"),
+ (plist.match(/\S*\.claude\S*/g) || []).slice(0, 3).join(", ") || render.stderr.toString().slice(0, 300));
+ check(SP, "no unresolved __LIFEOS_DIR__ placeholder", plist.length > 0 && !plist.includes("__LIFEOS_DIR__"), plist.slice(0, 200));
+ // Format differs per OS: macOS renders a launchd plist (PATH);
+ // Linux renders a systemd unit (WorkingDirectory=PATH / Environment=LIFEOS_DIR=PATH).
+ const isLinux = process.platform === "linux";
+ check(SP, "WorkingDirectory targets /LIFEOS/PULSE",
+ isLinux ? plist.includes(`WorkingDirectory=${pulseDir}`) : plist.includes(`${pulseDir}`),
+ plist.slice(0, 500));
+ check(SP, "LIFEOS_DIR env set to /LIFEOS",
+ isLinux ? plist.includes(`LIFEOS_DIR=${lifeosDir}`) : plist.includes(`${lifeosDir}`),
+ plist.slice(0, 500));
+ }
+
+ // Execute both deployed resolvers with every root env scrubbed. This verifies
+ // self-location behavior instead of only checking that imports exist in text.
+ const resolverEnv = toolEnv(fakeHome);
+ const runtimeResolver = join(lifeosDir, "TOOLS", "lifeos-root.ts");
+ const runtimeProbe = Bun.spawnSync(["bun", "-e", `const m = await import(${JSON.stringify(runtimeResolver)}); console.log(m.claudeDir())`], { env: resolverEnv, stdout: "pipe", stderr: "pipe" });
+ check(SP, "deployed runtime resolver self-locates the custom root", runtimeProbe.exitCode === 0 && runtimeProbe.stdout.toString().trim() === realpathSync(configRoot), runtimeProbe.stdout.toString() + runtimeProbe.stderr.toString());
+ const hookResolver = join(configRoot, "hooks", "lib", "paths.ts");
+ const hookProbe = Bun.spawnSync(["bun", "-e", `const m = await import(${JSON.stringify(hookResolver)}); console.log(m.getClaudeDir())`], { env: resolverEnv, stdout: "pipe", stderr: "pipe" });
+ check(SP, "deployed hook resolver self-locates the custom root", hookProbe.exitCode === 0 && hookProbe.stdout.toString().trim() === realpathSync(configRoot), hookProbe.stdout.toString() + hookProbe.stderr.toString());
+
+ // SessionStart must ground semantic placeholders even for subagents, which
+ // intentionally skip the personal relationship/learning context.
+ const loadContext = join(configRoot, "hooks", "LoadContext.hook.ts");
+ const contextEnv = toolEnv(fakeHome, {
+ // Deliberately stale/legacy values: LIFEOS_DIR and hook self-location win.
+ LIFEOS_ROOT: join(fakeHome, ".claude"),
+ LIFEOS_DIR: lifeosDir,
+ LIFEOS_CONFIG_DIR: lifeosDir,
+ CLAUDE_AGENT_TYPE: "integration-test",
+ });
+ const contextProbe = Bun.spawnSync(["bun", loadContext], { env: contextEnv, stdout: "pipe", stderr: "pipe" });
+ const contextOut = contextProbe.stdout.toString();
+ check(SP, "SessionStart grounds all LifeOS model path placeholders",
+ contextProbe.exitCode === 0 &&
+ contextOut.includes(`{{LIFEOS_ROOT}} = ${JSON.stringify(configRoot)}`) &&
+ contextOut.includes(`{{LIFEOS_DIR}} = ${JSON.stringify(lifeosDir)}`) &&
+ contextOut.includes(`{{LIFEOS_CONFIG_DIR}} = ${JSON.stringify(configDir)}`),
+ contextOut + contextProbe.stderr.toString());
+ check(SP, "SessionStart forbids unresolved placeholders in tool calls",
+ contextOut.includes("Never pass an unresolved LifeOS placeholder to a tool"), contextOut);
+
+ const preToolGuard = join(configRoot, "hooks", "PreToolGuard.hook.ts");
+ const unresolvedProbe = Bun.spawnSync(["bun", preToolGuard], {
+ env: contextEnv,
+ stdin: new Blob([JSON.stringify({
+ tool_name: "Read",
+ tool_input: { file_path: "{{LIFEOS_ROOT}}/settings.json" },
+ })]),
+ stdout: "pipe",
+ stderr: "pipe",
+ });
+ check(SP, "PreToolUse blocks unresolved LifeOS paths",
+ unresolvedProbe.exitCode === 2 && unresolvedProbe.stderr.toString().includes("Unresolved {{LIFEOS_ROOT}}"),
+ unresolvedProbe.stdout.toString() + unresolvedProbe.stderr.toString());
+
+ const documentedPlaceholderProbe = Bun.spawnSync(["bun", preToolGuard], {
+ env: contextEnv,
+ stdin: new Blob([JSON.stringify({
+ tool_name: "Write",
+ tool_input: {
+ file_path: join(configRoot, "example.md"),
+ content: "Model paths use {{LIFEOS_ROOT}}/skills in documentation.",
+ },
+ })]),
+ stdout: "pipe",
+ stderr: "pipe",
+ });
+ check(SP, "PreToolUse allows placeholders as documentation content",
+ documentedPlaceholderProbe.exitCode === 0,
+ documentedPlaceholderProbe.stdout.toString() + documentedPlaceholderProbe.stderr.toString());
+
+ // The installed launcher must make an arbitrary custom root visible to a
+ // fresh Claude process. A fake binary captures cwd/env without contacting
+ // Claude or touching any real user configuration.
+ const fakeBin = join(workRoot, "fake-bin");
+ const launchCapture = join(workRoot, "launcher-capture.json");
+ mkdirSync(fakeBin, { recursive: true });
+ const fakeClaude = join(fakeBin, "claude");
+ writeFileSync(fakeClaude, `#!/bin/sh\nif [ "$1" = "--version" ]; then echo "2.0.0"; exit 0; fi\nprintf '{"cwd":"%s","lifeosHome":"%s","claudeConfigDir":"%s"}\\n' "$PWD" "$LIFEOS_HOME" "$CLAUDE_CONFIG_DIR" > "$LIFEOS_LAUNCH_CAPTURE"\n`);
+ chmodSync(fakeClaude, 0o755);
+ const launcherEnv = toolEnv(fakeHome, {
+ PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
+ LIFEOS_LAUNCH_CAPTURE: launchCapture,
+ });
+ const launcher = Bun.spawnSync(["bun", join(lifeosDir, "TOOLS", "lifeos.ts")], { env: launcherEnv, stdout: "pipe", stderr: "pipe" });
+ const captured = existsSync(launchCapture) ? JSON.parse(readFileSync(launchCapture, "utf-8")) as Record : {};
+ check(SP, "launcher exports and enters the arbitrary custom root", launcher.exitCode === 0 && captured.cwd === realpathSync(configRoot) && captured.lifeosHome === realpathSync(configRoot) && captured.claudeConfigDir === realpathSync(configRoot), launcher.stdout.toString() + launcher.stderr.toString() + JSON.stringify(captured));
+
+ // A later session no longer has the installer's transient LIFEOS_HOME. It
+ // does retain LIFEOS_DIR/LIFEOS_CONFIG_DIR from settings and must recover one
+ // coherent pair rather than mixing ~/.claude with the custom data home.
+ const resumedEnv = toolEnv(fakeHome, { LIFEOS_DIR: lifeosDir, LIFEOS_CONFIG_DIR: configDir });
+ const resumedDetect = runTool("DetectEnv.ts", [], resumedEnv);
+ check(SP, "later session recovers configRoot from LIFEOS_DIR", resumedDetect.json?.configRoot === configRoot, resumedDetect.raw);
+ check(SP, "later session keeps the matching configDir", resumedDetect.json?.configDir === configDir, resumedDetect.raw);
+ const resumedScaffold = runTool("ScaffoldUser.ts", ["--skill-root", skillRoot], resumedEnv);
+ check(SP, "later ScaffoldUser defaults stay inside the custom root", resumedScaffold.code === 0 && resumedScaffold.json?.to === join(configDir, "USER"), resumedScaffold.raw);
+
+ const actionableFiles = [
+ join(pulseDir, "modules", "hooks.ts"),
+ join(pulseDir, "modules", "work.ts"),
+ join(configRoot, "hooks", "MemoryHealthGate.hook.ts"),
+ join(configRoot, "hooks", "MemoryDeltaSurface.hook.ts"),
+ join(configRoot, "hooks", "WritingGate.hook.ts"),
+ ];
+ const actionableLeaks = actionableFiles.filter((p) => existsSync(p) && /bun (?:\$HOME\/\.claude|~\/\.claude)/.test(readFileSync(p, "utf-8")));
+ check(SP, "agent-actionable runtime commands have no default-root hardcode", actionableLeaks.length === 0, actionableLeaks.join(", "));
+
+ // ─── Scenario B: legacy recovery + explicit self-symlink guard ─────
+ const SG = "B:legacy-and-guard";
+ // A legacy ambient value is ignored and automatically recovers to the
+ // isolated custom data home.
+ const poisoned = toolEnv(fakeHome, { LIFEOS_CONFIG_DIR: join(configRoot, "LIFEOS") });
+ const guardLink = runTool("LinkUser.ts", ["--config-root", configRoot, "--apply"], poisoned);
+ check(SG, "LinkUser recovers from an ambient legacy configDir", guardLink.code === 0 && (guardLink.json?.contract as any)?.passed === true, guardLink.raw);
+ const guardScaffold = runTool("ScaffoldUser.ts", ["--config-root", configRoot, "--skill-root", skillRoot, "--apply"], poisoned);
+ check(SG, "ScaffoldUser recovers from an ambient legacy configDir", guardScaffold.code === 0 && guardScaffold.json?.to === join(configDir, "USER"), guardScaffold.raw);
+
+ // An explicit unsafe request remains an error: explicit flags outrank the
+ // recovery heuristic, and the separation guard prevents self-linking.
+ const unsafeDir = join(configRoot, "LIFEOS");
+ const explicitGuardLink = runTool("LinkUser.ts", ["--config-root", configRoot, "--config-dir", unsafeDir, "--apply"], env);
+ check(SG, "LinkUser refuses an explicitly unsafe configDir", explicitGuardLink.code !== 0 && explicitGuardLink.json?.refused === "user-data-separation", explicitGuardLink.raw);
+ const explicitGuardScaffold = runTool("ScaffoldUser.ts", ["--config-root", configRoot, "--config-dir", unsafeDir, "--skill-root", skillRoot, "--apply"], env);
+ check(SG, "ScaffoldUser refuses an explicitly unsafe configDir", explicitGuardScaffold.code !== 0 && explicitGuardScaffold.json?.refused === "user-data-separation", explicitGuardScaffold.raw);
+ check(SG, "live USER symlink survived the explicit refused runs",
+ lstatSync(liveUser).isSymbolicLink() && readlinkSync(liveUser) === join(configDir, "USER"));
+}
+
+// ─── Scenario C: default-install regression (no LIFEOS_HOME) ─────────
+{
+ const S = "C:default-regression";
+ const fakeHome = join(workRoot, "home-default");
+ const env = toolEnv(fakeHome);
+ const defaultRoot = join(fakeHome, ".claude");
+
+ const detect = runTool("DetectEnv.ts", [], env);
+ check(S, "DetectEnv falls back to ~/.claude", detect.json?.configRoot === defaultRoot, detect.raw);
+ check(S, "DetectEnv configDir falls back to ~/.config/LIFEOS", detect.json?.configDir === join(fakeHome, ".config", "LIFEOS"), detect.raw);
+
+ const settingsRun = runTool("InstallSettings.ts", ["--skill-root", skillRoot, "--apply"], env);
+ const settingsPath = join(defaultRoot, "settings.json");
+ check(S, "InstallSettings ok into ~/.claude", settingsRun.code === 0 && existsSync(settingsPath), settingsRun.raw);
+ if (existsSync(settingsPath)) {
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8")) as Record;
+ check(S, "env.LIFEOS_DIR is the expanded default", settings.env?.LIFEOS_DIR === join(defaultRoot, "LIFEOS"), String(settings.env?.LIFEOS_DIR));
+ check(S, "env.LIFEOS_ROOT is the expanded default", settings.env?.LIFEOS_ROOT === defaultRoot, String(settings.env?.LIFEOS_ROOT));
+ check(S, "env.LIFEOS_CONFIG_DIR points at the private default data home", settings.env?.LIFEOS_CONFIG_DIR === join(fakeHome, ".config", "LIFEOS"), String(settings.env?.LIFEOS_CONFIG_DIR));
+ check(S, "template ~/.claude permission globs untouched", JSON.stringify(settings.permissions?.allow ?? []).includes("Write(~/.claude/**)"), "default globs were rewritten — regression!");
+ check(S, "no retarget was reported", settingsRun.json?.retargetedStrings === undefined, JSON.stringify(settingsRun.json));
+ }
+
+ const hooks = runTool("InstallHooks.ts", ["--skill-root", skillRoot, "--apply"], env);
+ check(S, "InstallHooks ok into ~/.claude", hooks.code === 0 && hooks.json?.ok === true, hooks.raw);
+ if (existsSync(settingsPath)) {
+ const hookCmds = JSON.stringify((JSON.parse(readFileSync(settingsPath, "utf-8")) as any).hooks ?? {});
+ check(S, "default hook commands still use $HOME/.claude", hookCmds.includes("$HOME/.claude/hooks/"), hookCmds.slice(0, 300));
+ }
+
+ // Legacy settings poisoned LIFEOS_CONFIG_DIR with /LIFEOS. The
+ // resolver must ignore it immediately and InstallSettings must repair it.
+ const legacyEnv = toolEnv(fakeHome, { LIFEOS_DIR: join(defaultRoot, "LIFEOS"), LIFEOS_CONFIG_DIR: join(defaultRoot, "LIFEOS") });
+ const legacyScaffold = runTool("ScaffoldUser.ts", ["--skill-root", skillRoot], legacyEnv);
+ check(S, "legacy poisoned configDir no longer blocks a default rerun", legacyScaffold.code === 0 && legacyScaffold.json?.to === join(fakeHome, ".config", "LIFEOS", "USER"), legacyScaffold.raw);
+ if (existsSync(settingsPath)) {
+ const legacySettings = JSON.parse(readFileSync(settingsPath, "utf-8")) as Record;
+ legacySettings.env.LIFEOS_CONFIG_DIR = join(defaultRoot, "LIFEOS");
+ writeFileSync(settingsPath, JSON.stringify(legacySettings, null, 2) + "\n");
+ const repair = runTool("InstallSettings.ts", ["--skill-root", skillRoot, "--apply"], legacyEnv);
+ const repaired = JSON.parse(readFileSync(settingsPath, "utf-8")) as Record;
+ check(S, "InstallSettings repairs the legacy LIFEOS_CONFIG_DIR value", repair.code === 0 && repaired.env?.LIFEOS_CONFIG_DIR === join(fakeHome, ".config", "LIFEOS"), repair.raw);
+ }
+}
+
+// ─── Scenario E: direct explicit root is isolated without LIFEOS_HOME ──
+{
+ const S = "E:explicit-root";
+ const fakeHome = join(workRoot, "home-explicit");
+ const explicitRoot = join(fakeHome, "explicit root");
+ const env = toolEnv(fakeHome);
+ const scaffold = runTool("ScaffoldUser.ts", ["--config-root", explicitRoot, "--skill-root", skillRoot], env);
+ check(S, "--config-root defaults USER data under that root", scaffold.code === 0 && scaffold.json?.to === join(explicitRoot, "USER-data", "USER"), scaffold.raw);
+ const harnessEnv = toolEnv(fakeHome, { CLAUDE_CONFIG_DIR: explicitRoot });
+ const harnessDetect = runTool("DetectEnv.ts", [], harnessEnv);
+ check(S, "CLAUDE_CONFIG_DIR override gets an isolated USER data home", harnessDetect.json?.configRoot === explicitRoot && harnessDetect.json?.configDir === join(explicitRoot, "USER-data"), harnessDetect.raw);
+}
+
+// ─── Scenario G: bootstrap makes the staged custom skill discoverable ──
+{
+ const S = "G:bootstrap-handoff";
+ const fakeHome = join(workRoot, "home-bootstrap");
+ const configRoot = join(fakeHome, "bootstrap root");
+ const fakeBin = join(workRoot, "bootstrap-bin");
+ const capturePath = join(workRoot, "bootstrap-capture.json");
+ mkdirSync(fakeBin, { recursive: true });
+ const fakeClaude = join(fakeBin, "claude");
+ writeFileSync(fakeClaude, `#!/bin/sh\nprintf '{"arg":"%s","lifeosHome":"%s","claudeConfigDir":"%s"}\\n' "$1" "$LIFEOS_HOME" "$CLAUDE_CONFIG_DIR" > "$LIFEOS_BOOTSTRAP_CAPTURE"\n`);
+ chmodSync(fakeClaude, 0o755);
+ const env = toolEnv(fakeHome, {
+ PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
+ LIFEOS_SRC: dirname(skillRoot),
+ LIFEOS_TAG: "v-test",
+ LIFEOS_BOOTSTRAP_CAPTURE: capturePath,
+ });
+ const bootstrap = Bun.spawnSync(["bash", join(skillRoot, "install", "install.sh"), "--home", configRoot], { env, stdout: "pipe", stderr: "pipe" });
+ const captured = existsSync(capturePath) ? JSON.parse(readFileSync(capturePath, "utf-8")) as Record : {};
+ check(S, "bootstrap stages LifeOS under the custom root", existsSync(join(configRoot, "skills", "LifeOS", "SKILL.md")), bootstrap.stdout.toString() + bootstrap.stderr.toString());
+ check(S, "bootstrap launches the staged skill through CLAUDE_CONFIG_DIR", bootstrap.exitCode === 0 && captured.arg === "/lifeos-setup" && captured.lifeosHome === configRoot && captured.claudeConfigDir === configRoot, bootstrap.stdout.toString() + bootstrap.stderr.toString() + JSON.stringify(captured));
+}
+
+// The nested LifeOS payload is a supported direct-deploy path; keep its
+// installer implementation synchronized with the authoritative outer skill.
+{
+ const S = "F:packaged-copy";
+ const nested = join(skillRoot, "install", "skills", "LifeOS");
+ for (const rel of [
+ "Tools/ActivateImports.ts",
+ "Tools/DetectEnv.ts",
+ "Tools/DeployComponents.ts",
+ "Tools/DeployCore.ts",
+ "Tools/InstallEngine.ts",
+ "Tools/InstallHooks.ts",
+ "Tools/InstallSettings.ts",
+ "Tools/LinkUser.ts",
+ "Tools/ScaffoldUser.ts",
+ "Tools/ScanConflicts.ts",
+ "Tools/SeedPulse.ts",
+ "Workflows/Interview.md",
+ "Workflows/Setup.md",
+ "Workflows/Uninstall.md",
+ "Workflows/Update.md",
+ "INSTALL.md",
+ "install/CLAUDE.template.md",
+ "install/install.sh",
+ "install/settings.system.json",
+ ]) {
+ check(S, `packaged ${rel} matches the authoritative skill`, readFileSync(join(skillRoot, rel), "utf-8") === readFileSync(join(nested, rel), "utf-8"), rel);
+ }
+}
+
+// ─── Report ──────────────────────────────────────────────────────────
+const failed = checks.filter((c) => !c.passed);
+const ok = failed.length === 0;
+const retain = keep || !ok;
+if (!retain) rmSync(workRoot, { recursive: true, force: true });
+
+console.log(JSON.stringify({
+ ok,
+ passed: checks.length - failed.length,
+ failed: failed.length,
+ checks: ok ? checks.map(({ scenario, name }) => `${scenario} ✓ ${name}`) : checks,
+ workRoot: retain ? workRoot : undefined,
+ note: retain && !ok ? "temp workspace retained for inspection" : undefined,
+}, null, 2));
+process.exit(ok ? 0 : 1);
diff --git a/LifeOS/Tools/InstallSettings.ts b/LifeOS/Tools/InstallSettings.ts
index 4b81c130e2..8ffd84b147 100644
--- a/LifeOS/Tools/InstallSettings.ts
+++ b/LifeOS/Tools/InstallSettings.ts
@@ -9,22 +9,32 @@
* `$HOME/` directory on disk that silently captures runtime state. Command
* strings (hooks, statusLine) are shell-evaluated and ship untouched.
*
+ * Custom LifeOS home (LIFEOS_HOME / --config-root ≠ ~/.claude): the template
+ * hardcodes `~/.claude` in env values, permission globs, and permission-guidance
+ * prose. Permission globs expand NO variables, so every one of those strings is
+ * retargeted to the real config root at write time. A default install is
+ * byte-identical to before. `env.LIFEOS_CONFIG_DIR` is pointed at the private
+ * user-data home (configDir) so the installers and the runtime agree on it.
+ *
* Semantics match the sibling installers (DeployCore/InstallHooks):
* - settings.json absent → write the expanded template whole.
* - settings.json present → additive merge: only ABSENT top-level keys and
- * ABSENT env keys are added (expanded); existing values are never touched.
+ * ABSENT env keys are added (expanded); existing values are never touched
+ * except the one known legacy LIFEOS_CONFIG_DIR self-link value, which is
+ * repaired to the resolved private data home.
* - Dry-run by default; `--apply` mutates; backup before every write.
* - REFUSES the author's live source tree (`--allow-dev` to override).
*
* Usage:
- * bun InstallSettings.ts [--config-root ] [--skill-root ] [--apply] [--allow-dev]
+ * bun InstallSettings.ts [--config-root ] [--config-dir ] [--skill-root ] [--apply] [--allow-dev]
*/
-import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
-import { join } from "node:path";
-import { detectDevTree } from "./InstallEngine";
+import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
+import { homedir } from "node:os";
+import { join, resolve } from "node:path";
+import { detectDevTree, resolveConfigDir, resolveConfigRoot } from "./InstallEngine";
-interface Args { configRoot: string; skillRoot: string; apply: boolean; allowDev: boolean; }
+interface Args { configRoot: string; configDir: string; skillRoot: string; apply: boolean; allowDev: boolean; }
function parseArgs(): Args {
const a = process.argv.slice(2);
@@ -32,9 +42,10 @@ function parseArgs(): Args {
const i = a.indexOf(flag);
return i >= 0 && a[i + 1] && !a[i + 1].startsWith("--") ? a[i + 1] : undefined;
};
- const home = process.env.HOME || "";
+ const configRoot = resolveConfigRoot(get("--config-root"));
return {
- configRoot: get("--config-root") || process.env.CLAUDE_CONFIG_DIR || join(home, ".claude"),
+ configRoot,
+ configDir: resolveConfigDir(configRoot, get("--config-dir")),
skillRoot: get("--skill-root") || join(import.meta.dir, ".."),
apply: a.includes("--apply"),
allowDev: a.includes("--allow-dev"),
@@ -64,10 +75,45 @@ function expandEnvBlock(settings: Record, home: string): number
return n;
}
+// Every spelling of the default root that appears in the shipped template. The
+// longest-prefix forms sort first so `${HOME}/.claude` never half-matches.
+const DEFAULT_ROOT_FORMS = ["${HOME}/.claude", "$HOME/.claude", "~/.claude"];
+
+/**
+ * Deep-walk every STRING value in the template and retarget the default-root
+ * spellings to the real config root. Covers env values, permission globs
+ * (`Write(~/.claude/**)` — globs expand no variables, so they must carry the
+ * absolute custom path), and permission-guidance prose. Returns replacements.
+ */
+function retargetStrings(node: unknown, configRoot: string): { node: unknown; replaced: number } {
+ let replaced = 0;
+ const rewrite = (s: string): string => {
+ let out = s;
+ for (const form of DEFAULT_ROOT_FORMS) {
+ const parts = out.split(form);
+ replaced += parts.length - 1;
+ out = parts.join(configRoot);
+ }
+ return out;
+ };
+ const walk = (n: unknown): unknown => {
+ if (typeof n === "string") return rewrite(n);
+ if (Array.isArray(n)) return n.map(walk);
+ if (n && typeof n === "object") {
+ const o: Record = {};
+ for (const [k, v] of Object.entries(n as Record)) o[k] = walk(v);
+ return o;
+ }
+ return n;
+ };
+ return { node: walk(node), replaced };
+}
+
const args = parseArgs();
-const home = process.env.HOME || "";
+const home = process.env.HOME || homedir();
const templatePath = join(args.skillRoot, "install", "settings.system.json");
const targetPath = join(args.configRoot, "settings.json");
+const isCustomRoot = home !== "" && resolve(args.configRoot) !== resolve(join(home, ".claude"));
if (detectDevTree(args.configRoot) && !args.allowDev) {
console.log(JSON.stringify({ ok: false, error: "dev tree detected — refusing to touch the author's live settings (--allow-dev to override)" }, null, 2));
@@ -77,11 +123,30 @@ if (!existsSync(templatePath)) {
console.log(JSON.stringify({ ok: false, error: `payload settings.system.json not found at ${templatePath}` }, null, 2));
process.exit(1);
}
+// A custom home (and ~/.claude on a clean machine) may not exist yet — writes
+// below assume the config root is a directory.
+if (args.apply) mkdirSync(args.configRoot, { recursive: true });
-const template = JSON.parse(readFileSync(templatePath, "utf8")) as Record;
+let template = JSON.parse(readFileSync(templatePath, "utf8")) as Record;
+let retargetedCount = 0;
+if (isCustomRoot) {
+ const retargeted = retargetStrings(template, args.configRoot);
+ template = retargeted.node as Record;
+ retargetedCount = retargeted.replaced;
+}
+// LIFEOS_CONFIG_DIR is the private user-data home, NOT /LIFEOS.
+// Pin it on every install so a later setup/update session cannot reinterpret
+// the old runtime-system value and collapse the USER symlink onto itself.
+if (template.env && typeof template.env === "object") {
+ const templateEnv = template.env as Record;
+ templateEnv.LIFEOS_CONFIG_DIR = args.configDir;
+ // Persist the dedicated root into future project-scoped sessions too. Runtime
+ // consumers still use LIFEOS_DIR; this value is for installer/update tools.
+ if (isCustomRoot) templateEnv.LIFEOS_HOME = args.configRoot;
+}
const expandedCount = expandEnvBlock(template, home);
-const report: Record = { ok: true, apply: args.apply, target: targetPath, envValuesExpanded: expandedCount };
+const report: Record = { ok: true, apply: args.apply, target: targetPath, envValuesExpanded: expandedCount, customRoot: isCustomRoot ? args.configRoot : undefined, retargetedStrings: isCustomRoot ? retargetedCount : undefined };
if (!existsSync(targetPath)) {
report.mode = "create";
@@ -97,13 +162,41 @@ if (!existsSync(targetPath)) {
const curEnv = (current.env && typeof current.env === "object" ? current.env : (current.env = {})) as Record;
const tplEnv = (template.env || {}) as Record;
const addedEnv: string[] = [];
+ const updatedEnv: string[] = [];
for (const [k, v] of Object.entries(tplEnv)) {
- if (!(k in curEnv)) { curEnv[k] = v; addedEnv.push(k); }
+ if (!(k in curEnv)) {
+ curEnv[k] = v;
+ addedEnv.push(k);
+ continue;
+ }
+ // These values define the active installation root and are owned by the
+ // installer. Preserving a stale value here makes every later session and
+ // the model-facing runtime-path contract point at the wrong tree.
+ if (
+ (k === "LIFEOS_ROOT" || k === "LIFEOS_DIR" || k === "LIFEOS_HOME") &&
+ curEnv[k] !== v
+ ) {
+ curEnv[k] = v;
+ updatedEnv.push(k);
+ continue;
+ }
+ // Narrow migration for the shipped legacy collision only; arbitrary user
+ // overrides remain untouched.
+ if (
+ k === "LIFEOS_CONFIG_DIR" &&
+ typeof curEnv[k] === "string" &&
+ resolve(expandLeadingHome(curEnv[k] as string, home)) === resolve(join(args.configRoot, "LIFEOS")) &&
+ curEnv[k] !== v
+ ) {
+ curEnv[k] = v;
+ updatedEnv.push(k);
+ }
}
report.mode = "merge";
report.addedKeys = addedKeys;
report.addedEnv = addedEnv;
- if (args.apply && (addedKeys.length || addedEnv.length)) {
+ report.updatedEnv = updatedEnv;
+ if (args.apply && (addedKeys.length || addedEnv.length || updatedEnv.length)) {
copyFileSync(targetPath, targetPath + ".backup-" + new Date().toISOString().replace(/[:.]/g, "-"));
writeFileSync(targetPath, JSON.stringify(current, null, 2) + "\n");
} else if (args.apply) {
diff --git a/LifeOS/Tools/LinkUser.ts b/LifeOS/Tools/LinkUser.ts
index a0899480e2..51c840aa04 100755
--- a/LifeOS/Tools/LinkUser.ts
+++ b/LifeOS/Tools/LinkUser.ts
@@ -18,7 +18,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
*/
import { join } from "node:path";
-import { checkSymlinkContract, detectDevTree, setupUserSeparation } from "./InstallEngine";
+import { checkSymlinkContract, checkUserDataSeparation, detectDevTree, resolveConfigDir, resolveConfigRoot, setupUserSeparation } from "./InstallEngine";
// Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404)
for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
@@ -33,9 +33,8 @@ function main(): void {
const i = a.indexOf(f);
return i >= 0 && a[i + 1] && !a[i + 1].startsWith("--") ? a[i + 1] : undefined;
};
- const home = process.env.HOME || "";
- const configRoot = get("--config-root") || process.env.CLAUDE_CONFIG_DIR || join(home, ".claude");
- const configDir = get("--config-dir") || process.env.LIFEOS_CONFIG_DIR || join(home, ".config", "LIFEOS");
+ const configRoot = resolveConfigRoot(get("--config-root"));
+ const configDir = resolveConfigDir(configRoot, get("--config-dir"));
const apply = a.includes("--apply");
const allowDev = a.includes("--allow-dev");
@@ -44,6 +43,12 @@ function main(): void {
process.exit(2);
}
+ const separation = checkUserDataSeparation(configRoot, configDir);
+ if (!separation.ok) {
+ console.log(JSON.stringify({ ok: false, refused: "user-data-separation", detail: separation.detail }, null, 2));
+ process.exit(1);
+ }
+
if (!apply) {
const contract = checkSymlinkContract(configRoot, configDir);
console.log(JSON.stringify({ ok: true, dryRun: true, currentContract: contract, willLink: `${join(configRoot, "LIFEOS", "USER")} → ${join(configDir, "USER")}` }, null, 2));
diff --git a/LifeOS/Tools/ScaffoldUser.ts b/LifeOS/Tools/ScaffoldUser.ts
index 5230b2db92..9480ece4c0 100755
--- a/LifeOS/Tools/ScaffoldUser.ts
+++ b/LifeOS/Tools/ScaffoldUser.ts
@@ -18,7 +18,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
import { existsSync } from "node:fs";
import { join } from "node:path";
-import { copyMissing, detectDevTree } from "./InstallEngine";
+import { checkUserDataSeparation, copyMissing, detectDevTree, resolveConfigDir, resolveConfigRoot } from "./InstallEngine";
// Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404)
for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
@@ -33,9 +33,8 @@ function main(): void {
const i = a.indexOf(f);
return i >= 0 && a[i + 1] && !a[i + 1].startsWith("--") ? a[i + 1] : undefined;
};
- const home = process.env.HOME || "";
- const configRoot = get("--config-root") || process.env.CLAUDE_CONFIG_DIR || join(home, ".claude");
- const configDir = get("--config-dir") || process.env.LIFEOS_CONFIG_DIR || join(home, ".config", "LIFEOS");
+ const configRoot = resolveConfigRoot(get("--config-root"));
+ const configDir = resolveConfigDir(configRoot, get("--config-dir"));
const skillRoot = get("--skill-root") || join(import.meta.dir, "..");
const apply = a.includes("--apply");
const allowDev = a.includes("--allow-dev");
@@ -45,6 +44,12 @@ function main(): void {
process.exit(2);
}
+ const separation = checkUserDataSeparation(configRoot, configDir);
+ if (!separation.ok) {
+ console.log(JSON.stringify({ ok: false, refused: "user-data-separation", detail: separation.detail }, null, 2));
+ process.exit(1);
+ }
+
const templateUser = join(skillRoot, "install", "USER");
const dataUser = join(configDir, "USER");
if (!existsSync(templateUser)) {
diff --git a/LifeOS/Tools/SeedPulse.ts b/LifeOS/Tools/SeedPulse.ts
index d9ee9a8784..e62775e48a 100755
--- a/LifeOS/Tools/SeedPulse.ts
+++ b/LifeOS/Tools/SeedPulse.ts
@@ -20,7 +20,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
import { execFileSync } from "node:child_process";
import { existsSync } from "node:fs";
import { join } from "node:path";
-import { detectDevTree } from "./InstallEngine";
+import { detectDevTree, resolveConfigDir, resolveConfigRoot } from "./InstallEngine";
// Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404)
for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
@@ -37,9 +37,8 @@ function main(): void {
const i = a.indexOf(f);
return i >= 0 && a[i + 1] && !a[i + 1].startsWith("--") ? a[i + 1] : undefined;
};
- const home = process.env.HOME || "";
- const configRoot = get("--config-root") || process.env.CLAUDE_CONFIG_DIR || join(home, ".claude");
- const configDir = get("--config-dir") || process.env.LIFEOS_CONFIG_DIR || join(home, ".config", "LIFEOS");
+ const configRoot = resolveConfigRoot(get("--config-root"));
+ const configDir = resolveConfigDir(configRoot, get("--config-dir"));
const apply = a.includes("--apply");
const allowDev = a.includes("--allow-dev");
diff --git a/LifeOS/Workflows/Setup.md b/LifeOS/Workflows/Setup.md
index 40a435c1ce..6d1119d09d 100644
--- a/LifeOS/Workflows/Setup.md
+++ b/LifeOS/Workflows/Setup.md
@@ -18,10 +18,15 @@ Deployment is **two tiers**, and the install presents them that way:
The skill ships everything for both tiers in its payload; nothing activates without the matching consent.
+## Root plumbing (applies to EVERY step)
+
+`DetectEnv` reports `configRoot` (where LifeOS installs: `LIFEOS/`, `settings.json`, `hooks/`, `CLAUDE.md`) and `configDir` (the private user-data home; `/LIFEOS/USER` symlinks into it). A custom LifeOS home (`LIFEOS_HOME` env / `install.sh --home`) changes both. **Pass them explicitly to every tool call** — `--config-root ` on all tools, plus `--config-dir ` on `ScaffoldUser`/`LinkUser`/`SeedPulse` — and use `` wherever a step writes a file or wires a command. Never assume `~/.claude`. (The tools resolve the same defaults themselves via `InstallEngine.resolveConfigRoot/resolveConfigDir`, so the flags are belt-and-suspenders — but explicit beats inherited env.)
+
## Steps
-1. **DetectEnv** — `bun Tools/DetectEnv.ts` → `{os, harness, display, ssh, bun, existingInstall, isDevTree, settingsExists, claudeMdExists}`. Thin entry point over the sibling `Tools/InstallEngine.ts` (`detectEnv()`) — a reshaped, bare-skill subset of the legacy installer engine.
+1. **DetectEnv** — `bun Tools/DetectEnv.ts` → `{os, harness, display, ssh, bun, existingInstall, isDevTree, settingsExists, claudeMdExists, configRoot, configDir}`. Thin entry point over the sibling `Tools/InstallEngine.ts` (`detectEnv()`) — a reshaped, bare-skill subset of the legacy installer engine.
- **If `isDevTree` → STOP.** Never mutate the author's source repo. Print the refusal and exit.
+ - **The install-location choice:** if `LIFEOS_HOME` is NOT set, ask the user once — install into the default `` (LifeOS active in every session), or into a custom directory (keep plain Claude Code vanilla and opt into LifeOS per launch; also how you run an isolated second instance)? Default → proceed unchanged. Custom → export the named dir as `LIFEOS_HOME` for every subsequent step and re-run DetectEnv. If `LIFEOS_HOME` IS set (e.g. via `install.sh --home`), skip the question and just confirm the target ("Installing LifeOS into `` — correct?") before any write.
2. **ScanConflicts** (read-only) — `bun Tools/ScanConflicts.ts` → existing settings hooks, skill-name collisions, existing populated config tree. Produces the branch decision for `LinkUser`.
3. **Prereqs** — confirm `bun` present; confirm harness is one of the supported set; surface any missing prerequisite as a plain-language fix, do not auto-install system packages.
- **If `harness.confidence` is `"assumed"`, confirm the harness with the user before branching** — detection was a guess (config dir without the harness binary, or the clean-machine default), and a leftover `~/.claude` dir must not send a non-Claude-Code install down the Claude Code path (hooks + `lifeos` alias both require the `claude` CLI). Ask which harness is actually running this setup, and branch on the answer.
@@ -53,10 +58,11 @@ The skill ships everything for both tiers in its payload; nothing activates with
- **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.
-8.5. **Wire the launch command (Core)** — the constitutional system prompt placed in step 4 (`LIFEOS_SYSTEM_PROMPT.md`) only loads when the harness is launched with it appended; a plain `claude` session never sees it. Wire a `lifeos` launch command per `INSTALL.md` step 7 — **Claude Code:** a permission-gated `lifeos` alias in the user's shell rc (`alias lifeos='bun /LIFEOS/TOOLS/lifeos.ts -s /LIFEOS/LIFEOS_SYSTEM_PROMPT.md'`), shown exactly, rc backed up first, idempotent (never double-add); **other harnesses:** their own system-prompt flag against the same file. If the user declines the shell edit, hand them the one-line launch command to run by hand. **Without this, LifeOS Core is installed but launches un-constituted** (plain `claude`, no mode banner / verification / security layer).
+8.5. **Wire the launch command (Core)** — the constitutional system prompt placed in step 4 (`LIFEOS_SYSTEM_PROMPT.md`) only loads when the harness is launched with it appended; a plain `claude` session never sees it. Wire a `lifeos` launch command per `INSTALL.md` step 7 — **Claude Code:** a permission-gated `lifeos` alias in the user's shell rc (`alias lifeos='bun "/LIFEOS/TOOLS/lifeos.ts" -s "/LIFEOS/LIFEOS_SYSTEM_PROMPT.md"'`), shown exactly, rc backed up first, idempotent (never double-add). Keep the path quoting: custom roots may contain spaces. The launcher starts a project-scoped `.claude` from its parent (preserving merge) and uses `CLAUDE_CONFIG_DIR` for arbitrary roots; **other harnesses:** their own system-prompt flag against the same file. If the user declines the shell edit, hand them the one-line launch command to run by hand. **Without this, LifeOS Core is installed but launches un-constituted** (plain `claude`, no mode banner / verification / security layer).
9. **Verify (three evidence classes)** — (a) the config tree resolves (the identity `@`-imports load) — ALWAYS checked, it's Core; (b) IF the user opted into `hooks`, a probe session shows the mode banner / context injection fire. If hooks were declined, skip (b) and surface the caveat plainly: the constitutional mode banner and the memory/voice loop are hook-enforced, so without hooks LifeOS Core installs but runs un-bannered and un-augmented — recommend hooks unless there's a reason to decline; (c) the **launch command is wired** — grep the shell rc for the `lifeos` alias (or confirm the user was handed the manual one-line launch). Without it, Core installed but launches un-constituted — say so plainly. Report what was checked; never claim a hooks-fire pass when hooks weren't installed.
10. **Transition** — print: "Setup complete. Now let's get you into LifeOS —" and roll into `Workflows/Interview.md`.
## Notes
- Cross-platform: branch on `DetectEnv.os` for hook command shapes and path separators.
- Cross-harness: branch on `DetectEnv.harness` for the skills-dir location and hook command shapes; every harness gets the same imperative, permissioned hook install.
+- Custom LifeOS home: with `LIFEOS_HOME` set, `InstallSettings` retargets the template's `~/.claude` strings (env values, permission globs, guidance) and `InstallHooks` retargets the payload's hook commands to `` — verify afterwards that the written `settings.json` contains no `~/.claude` references. Remind the user of the runtime half per `INSTALL.md` § 2.5 (the install-location choice): the harness must be LAUNCHED against that root (project-scoped `.claude`, or `CLAUDE_CONFIG_DIR`) or LifeOS won't load.
diff --git a/LifeOS/Workflows/Update.md b/LifeOS/Workflows/Update.md
index 162fbfe192..aceef44db4 100644
--- a/LifeOS/Workflows/Update.md
+++ b/LifeOS/Workflows/Update.md
@@ -11,12 +11,18 @@ curl -s -X POST http://localhost:31337/notify -H "Content-Type: application/json
## Steps
-1. **DetectEnv** — `bun Tools/DetectEnv.ts`. If `isDevTree` → STOP (the source repo updates itself via git, not this workflow).
+Every tool call in this workflow uses the values returned by DetectEnv: pass
+`--config-root ` everywhere, plus `--config-dir ` to
+`ScaffoldUser`/`LinkUser`/`SeedPulse`. Never reconstruct either path from
+`~/.claude`. A custom session persists its root through `LIFEOS_HOME` and
+`LIFEOS_DIR`, but explicit flags keep the update deterministic.
+
+1. **DetectEnv** — `bun Tools/DetectEnv.ts`. Record `configRoot` and `configDir`. If `isDevTree` → STOP (the source repo updates itself via git, not this workflow).
2. **Version diff** — the skill carries no version field and there is no plugin manifest; versioning lives at the distribution layer (the GitHub release tag + `LIFEOS_RELEASES//` + the `install.sh` fetch's `LIFEOS_VERSION`). Compare the release version being updated to against the user's current install marker. If equal, report "already current" and exit.
-3. **Re-overlay system** — re-copy the system templates (CLAUDE, system prompt, `settings.system.json` minus hooks). These are system-owned and safe to overwrite.
-4. **Re-merge hooks** — `bun Tools/InstallHooks.ts` (idempotent): adds new hook entries, leaves existing ones, never duplicates (normalized-command dedup). Backs up `settings.json` first.
-5. **Scaffold new USER templates only** — `bun Tools/ScaffoldUser.ts` copyMissing: adds any NEW template files introduced by the version, never overwrites the user's existing files.
-6. **Re-activate imports** — `bun Tools/ActivateImports.ts` for any newly-shipped identity import lines.
+3. **Re-overlay system** — re-copy the system templates (CLAUDE, system prompt, `settings.system.json` minus hooks) using `bun Tools/InstallSettings.ts --config-root --config-dir ` for settings. These are system-owned and safe to overwrite. The settings tool also repairs the legacy `LIFEOS_CONFIG_DIR=/LIFEOS` collision when encountered.
+4. **Re-merge hooks** — `bun Tools/InstallHooks.ts --config-root ` (idempotent): adds new hook entries, leaves existing ones, never duplicates (normalized-command dedup). Backs up `settings.json` first.
+5. **Scaffold new USER templates only** — `bun Tools/ScaffoldUser.ts --config-root --config-dir ` copyMissing: adds any NEW template files introduced by the version, never overwrites the user's existing files.
+6. **Re-activate imports** — `bun Tools/ActivateImports.ts --config-root ` for any newly-shipped identity import lines.
7. **Verify** — two evidence classes (hooks fire + imports resolve), same as Setup step 9.
## Rule
diff --git a/LifeOS/install/CLAUDE.template.md b/LifeOS/install/CLAUDE.template.md
index 4d42f0b6a9..a0db7ec71a 100644
--- a/LifeOS/install/CLAUDE.template.md
+++ b/LifeOS/install/CLAUDE.template.md
@@ -16,7 +16,7 @@
Constitutional rules, the unified response format, verification doctrine, hard prohibitions, security protocol, and operational rules all live in the system prompt: `LIFEOS/LIFEOS_SYSTEM_PROMPT.md`. When this file and the system prompt disagree, the system prompt wins.
-This file is the **routing table** — it tells you where everything lives. The only mandatory startup `@`-import shipped with public LifeOS is `ARCHITECTURE_SUMMARY`. The five identity files (`PRINCIPAL_TELOS`, `PRINCIPAL_IDENTITY`, `DA_IDENTITY`, `PROJECTS`, `OPERATIONAL_RULES`) are commented out above — the agentic `/lifeos-setup` (via `Tools/ActivateImports.ts`) uncomments them once the principal's USER scaffold is populated. Claude Code does not follow transitive `@`-imports from inside imported files, so each identity file must be listed here at top level. Everything below is **on-demand** lookup. Paths are relative to `~/.claude/` unless noted.
+This file is the **routing table** — it tells you where everything lives. The only mandatory startup `@`-import shipped with public LifeOS is `ARCHITECTURE_SUMMARY`. The five identity files (`PRINCIPAL_TELOS`, `PRINCIPAL_IDENTITY`, `DA_IDENTITY`, `PROJECTS`, `OPERATIONAL_RULES`) are commented out above — the agentic `/lifeos-setup` (via `Tools/ActivateImports.ts`) uncomments them once the principal's USER scaffold is populated. Claude Code does not follow transitive `@`-imports from inside imported files, so each identity file must be listed here at top level. Everything below is **on-demand** lookup. Paths are relative to the harness config root (default `~/.claude/`; a custom LifeOS home if this install used one) unless noted.
## LifeOS System (paths under `LIFEOS/DOCUMENTATION/` unless noted)
@@ -81,7 +81,7 @@ Populated during `/lifeos-setup`. Typical layout:
- Finances — `FINANCES/`
- Integration configs — `INTEGRATIONS/*.yaml`
- Work system — `WORK/config.yaml`
-- Secrets — `~/.claude/.env` (canonical; see OPERATIONAL_RULES.md)
+- Secrets — `/.env`, default `~/.claude/.env` (canonical; see OPERATIONAL_RULES.md)
## Project-Specific Rules
diff --git a/LifeOS/install/LIFEOS/ALGORITHM/changelog.md b/LifeOS/install/LIFEOS/ALGORITHM/changelog.md
index 611e2e219b..fc02786720 100644
--- a/LifeOS/install/LIFEOS/ALGORITHM/changelog.md
+++ b/LifeOS/install/LIFEOS/ALGORITHM/changelog.md
@@ -172,7 +172,7 @@ v6.2.0 ships the long-running ISA expansion conversation {{PRINCIPAL_NAME}} and
- Forge drafted the AlgorithmSystem.md full rewrite (334 lines, swapped from v3.26.0-stale to v6.2.0-current) in parallel with the doctrine spec writes, in 209 seconds.
- Advisor flagged two real risks during the v6.2.0.md write: (1) E1 fast-path bypass risks becoming a doctrine-evasion route — addressed by specifying the bypass as a whitelist condition, not a heuristic; (2) section-name drift between the skill's `canonical-isa.md` and `IsaFormat.md` v2.7 — addressed by writing both with the same twelve-section list in the same order this run.
-**v6.2.0 in-flight refactor (LATEST as single source of truth):** initially shipped an `AlgorithmVersionSync.hook.ts` to keep `settings.json .pai.algorithmVersion` synced with `LIFEOS/ALGORITHM/LATEST` on every Write/Edit/MultiEdit. {{PRINCIPAL_NAME}} called Bitter Pill — a hook that synchronizes duplicated state is overhead a smarter design renders unnecessary. Refactored to make LATEST the sole record: statusline reads `cat $LIFEOS_DIR/ALGORITHM/LATEST` directly; `Banner.ts` and `ArchitectureSummaryGenerator.ts::detectAlgorithmVersion()` read LATEST first; `UpdateCounts.ts` no longer mirrors CLAUDE.md regex into settings; `CLAUDE.md MANDATORY FIRST ACTION` became a two-step "Read LATEST → Read v${contents}.md" instruction; `.pai.algorithmVersion` deleted from settings.json; hook + its three Write/Edit/MultiEdit registrations removed. Net change: one source, drift physically impossible. Live-probed end-to-end: bumping LATEST to `6.99.99` flowed through the statusline within one second with no other file touched. Lesson written to v6.2.x doctrine note: when the symptom is "synced field drifted," the right answer is usually to remove the duplicated field, not to add a synchronizer.
+**v6.2.0 in-flight refactor (LATEST as single source of truth):** initially shipped an `AlgorithmVersionSync.hook.ts` to keep `settings.json .pai.algorithmVersion` synced with `LIFEOS/ALGORITHM/LATEST` on every Write/Edit/MultiEdit. {{PRINCIPAL_NAME}} called Bitter Pill — a hook that synchronizes duplicated state is overhead a smarter design renders unnecessary. Refactored to make LATEST the sole record: statusline reads `cat "${LIFEOS_DIR}/ALGORITHM/LATEST"` directly; `Banner.ts` and `ArchitectureSummaryGenerator.ts::detectAlgorithmVersion()` read LATEST first; `UpdateCounts.ts` no longer mirrors CLAUDE.md regex into settings; `CLAUDE.md MANDATORY FIRST ACTION` became a two-step "Read LATEST → Read v${contents}.md" instruction; `.pai.algorithmVersion` deleted from settings.json; hook + its three Write/Edit/MultiEdit registrations removed. Net change: one source, drift physically impossible. Live-probed end-to-end: bumping LATEST to `6.99.99` flowed through the statusline within one second with no other file touched. Lesson written to v6.2.x doctrine note: when the symptom is "synced field drifted," the right answer is usually to remove the duplicated field, not to add a synchronizer.
**Files updated:**
- NEW: `LIFEOS/ALGORITHM/v6.2.0.md` (doctrine file)
diff --git a/LifeOS/install/LIFEOS/ALGORITHM/ideate-loop.md b/LifeOS/install/LIFEOS/ALGORITHM/ideate-loop.md
index 466fbc725b..c2f815f2fd 100644
--- a/LifeOS/install/LIFEOS/ALGORITHM/ideate-loop.md
+++ b/LifeOS/install/LIFEOS/ALGORITHM/ideate-loop.md
@@ -16,7 +16,7 @@ status: redirect
- **Ideate mode doctrine:** [`modes/ideate.md`](modes/ideate.md) — 9 phases, parameters, presets, Meta-Learner, effort tier mapping
- **Mode taxonomy:** [`modes/README.md`](modes/README.md)
- **Parameter schema:** [`parameter-schema.md`](parameter-schema.md)
-- **Ideate skill (router):** `~/.claude/skills/Ideate/SKILL.md`
+- **Ideate skill (router):** `{{LIFEOS_ROOT}}/skills/Ideate/SKILL.md`
## Backwards-compat note
diff --git a/LifeOS/install/LIFEOS/ALGORITHM/modes/README.md b/LifeOS/install/LIFEOS/ALGORITHM/modes/README.md
index efd52f9dcd..a3246d0725 100644
--- a/LifeOS/install/LIFEOS/ALGORITHM/modes/README.md
+++ b/LifeOS/install/LIFEOS/ALGORITHM/modes/README.md
@@ -110,8 +110,8 @@ E-level sets the tier; mode sets the execution pattern. Both can be set simultan
- Pulse tab source of truth: `LIFEOS/PULSE/Observability/src/app/agents/page.tsx` lines 23-30
- Dashboard components: `LIFEOS/PULSE/Observability/src/components/activity/{UnifiedWorkDashboard,OptimizeDashboard,LoopDashboard,NativeDashboard,NoveltyDashboard}.tsx`
- Ladder page: `LIFEOS/PULSE/Observability/src/app/ladder/page.tsx`
-- Loop skill: `~/.claude/skills/Loop/SKILL.md`
-- Ideate skill (router stub): `~/.claude/skills/Ideate/SKILL.md`
+- Loop skill: `{{LIFEOS_ROOT}}/skills/Loop/SKILL.md`
+- Ideate skill (router stub): `{{LIFEOS_ROOT}}/skills/Ideate/SKILL.md`
- Parameter schema: `../parameter-schema.md`
- Capabilities: the system-prompt skill list is the sole inventory (capabilities.md removed at v7.0.0)
- Target types: `../target-types.md`
diff --git a/LifeOS/install/LIFEOS/ALGORITHM/modes/ideate.md b/LifeOS/install/LIFEOS/ALGORITHM/modes/ideate.md
index bd1b57af09..34e71b6f61 100644
--- a/LifeOS/install/LIFEOS/ALGORITHM/modes/ideate.md
+++ b/LifeOS/install/LIFEOS/ALGORITHM/modes/ideate.md
@@ -135,5 +135,5 @@ principal_stated_goal: ""
- All modes: [`README.md`](README.md)
- Parameter schema: [`../parameter-schema.md`](../parameter-schema.md)
-- Ideate skill (router): `~/.claude/skills/Ideate/SKILL.md`
+- Ideate skill (router): `{{LIFEOS_ROOT}}/skills/Ideate/SKILL.md`
- Current Algorithm doctrine: `../v6.5.0.md`
diff --git a/LifeOS/install/LIFEOS/ALGORITHM/modes/loop.md b/LifeOS/install/LIFEOS/ALGORITHM/modes/loop.md
index 65a739381c..76a565571e 100644
--- a/LifeOS/install/LIFEOS/ALGORITHM/modes/loop.md
+++ b/LifeOS/install/LIFEOS/ALGORITHM/modes/loop.md
@@ -185,6 +185,6 @@ The ID-stability rule (v6.5.0) is preserved across iterations: ISC IDs never re-
- All modes: [`README.md`](README.md)
- Goal anchor mechanism: `../v6.5.0.md` § "Principal-Stated Goal"
- Density gate (one-question pattern): `../v6.5.0.md` § "Density × Tier Gate"
-- Loop skill (router): `~/.claude/skills/Loop/SKILL.md`
+- Loop skill (router): `{{LIFEOS_ROOT}}/skills/Loop/SKILL.md`
- LoopRunner.ts (pending): LIFEOS/TOOLS/LoopRunner.ts — next ISA, not yet on disk
- Algorithm v6.6.0 doctrine bump (pending): `../v6.6.0.md` — paired with LoopRunner ship
diff --git a/LifeOS/install/LIFEOS/ALGORITHM/modes/optimize.md b/LifeOS/install/LIFEOS/ALGORITHM/modes/optimize.md
index 1c48057753..7505be80a9 100644
--- a/LifeOS/install/LIFEOS/ALGORITHM/modes/optimize.md
+++ b/LifeOS/install/LIFEOS/ALGORITHM/modes/optimize.md
@@ -129,5 +129,5 @@ ISCs are **guard rails** — invariant assertions that must hold regardless of s
- Parameter schema: [`../parameter-schema.md`](../parameter-schema.md)
- Eval-mode guide: [`../eval-guide.md`](../eval-guide.md)
- Target types: [`../target-types.md`](../target-types.md)
-- Optimize skill (router): `~/.claude/skills/Optimize/SKILL.md`
+- Optimize skill (router): `{{LIFEOS_ROOT}}/skills/Optimize/SKILL.md`
- Current Algorithm doctrine: `../v6.5.0.md`
diff --git a/LifeOS/install/LIFEOS/ALGORITHM/optimize-loop.md b/LifeOS/install/LIFEOS/ALGORITHM/optimize-loop.md
index ba439151d7..b31823fb3d 100644
--- a/LifeOS/install/LIFEOS/ALGORITHM/optimize-loop.md
+++ b/LifeOS/install/LIFEOS/ALGORITHM/optimize-loop.md
@@ -18,7 +18,7 @@ status: redirect
- **Parameter schema:** [`parameter-schema.md`](parameter-schema.md)
- **Eval mode guide:** [`eval-guide.md`](eval-guide.md)
- **Target types:** [`target-types.md`](target-types.md)
-- **Optimize skill (router):** `~/.claude/skills/Optimize/SKILL.md`
+- **Optimize skill (router):** `{{LIFEOS_ROOT}}/skills/Optimize/SKILL.md`
## Backwards-compat note
diff --git a/LifeOS/install/LIFEOS/ALGORITHM/target-types.md b/LifeOS/install/LIFEOS/ALGORITHM/target-types.md
index 6ac1597f49..f4bf294f0a 100644
--- a/LifeOS/install/LIFEOS/ALGORITHM/target-types.md
+++ b/LifeOS/install/LIFEOS/ALGORITHM/target-types.md
@@ -8,7 +8,7 @@ Read the target path and content to classify into one of five types:
| Type | Detection Rule |
|------|---------------|
-| **skill** | Directory contains `SKILL.md`, or path points to a skill directory under `~/.claude/skills/` |
+| **skill** | Directory contains `SKILL.md`, or path points to a skill directory under `{{LIFEOS_ROOT}}/skills/` |
| **prompt** | Standalone `.md` or `.txt` file containing prompt instructions (no SKILL.md in parent) |
| **agent** | `.md` file with agent frontmatter (`name`, `description`, tools/capabilities fields) |
| **code** | Source file(s) with `metric_command` provided in ISA — `.ts`, `.js`, `.py`, `.go`, etc. |
diff --git a/LifeOS/install/LIFEOS/ALGORITHM/v8.4.0.md b/LifeOS/install/LIFEOS/ALGORITHM/v8.4.0.md
index 2305e6d2fc..b8f56db374 100644
--- a/LifeOS/install/LIFEOS/ALGORITHM/v8.4.0.md
+++ b/LifeOS/install/LIFEOS/ALGORITHM/v8.4.0.md
@@ -86,5 +86,5 @@ Two spend facts are not judgment calls:
- **Observability is the ISA itself.** The `phase:` frontmatter enum stays byte-stable for Pulse (observe→think→plan→build→execute→verify→learn→complete), but the ceremony is gone (8.2.0): no per-transition CLI calls, chat headers, or voice curls. Write `phase:` at run start, at `complete`, and in between only when the value genuinely changes; ISASync mirrors it. `AlgoPhase.ts` remains a manual tool, not a required station.
- **Reflection (at learn, for any run that did real work):** append a schema-7 record (operational fields only, no self-scores) to `MEMORY/LEARNING/REFLECTIONS/algorithm-reflections.jsonl`; join `ratings.jsonl` by session_id or null.
- **Resume:** an ISA body edit on a `phase: complete` task rewinds to `learn`, `iteration+1` (hook-owned; `frozen: true` bypasses). After compaction: read the ISA, continue, never redo passed gates. Registry: `MEMORY/STATE/work.json`.
-- **Cross-vendor audit invocations** (only when the Algorithm elects one per claim 11, or the principal asks; thread the slug explicitly): Forge `Agent(subagent_type:"Forge", prompt:"MODE: audit\nslug: …")` · Grok `bun ~/.claude/LIFEOS/TOOLS/GrokAudit.ts --slug ` (unreachable → skipped for cause, never blocks).
+- **Cross-vendor audit invocations** (only when the Algorithm elects one per claim 11, or the principal asks; thread the slug explicitly): Forge `Agent(subagent_type:"Forge", prompt:"MODE: audit\nslug: …")` · Grok `bun "${LIFEOS_DIR}/TOOLS/GrokAudit.ts" --slug ` (unreachable → skipped for cause, never blocks).
- **Close** — a run ends in the ONE LifeOS format (system prompt § Output Format): the answer leads, carries which claims closed on what evidence and what's open, CHANGE/VERIFY bullets when work mutated things, the hook-fed 🧠 line, the 🗣️ closer. No separate close-block template.
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Agents/AgentSystem.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Agents/AgentSystem.md
index f58e6f5d8d..d3a1b3c134 100755
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Agents/AgentSystem.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Agents/AgentSystem.md
@@ -167,7 +167,7 @@ Task({
## Knowledge Archive Access
-Agents can query the **Knowledge Archive** (`~/.claude/LIFEOS/MEMORY/KNOWLEDGE/`) for accumulated knowledge organized by 4 entity types: People (human beings), Companies (organizations), Ideas (insights/theses/analyses), Research (longer-form research notes). Topic is a tag, not a domain. Managed by Algorithm LEARN phase (direct writes), `LIFEOS/TOOLS/KnowledgeHarvester.ts` (validation/maintenance), and the `/knowledge` skill. Particularly useful for research agents and custom agents composed with specialized traits.
+Agents can query the **Knowledge Archive** (`{{LIFEOS_DIR}}/MEMORY/KNOWLEDGE/`) for accumulated knowledge organized by 4 entity types: People (human beings), Companies (organizations), Ideas (insights/theses/analyses), Research (longer-form research notes). Topic is a tag, not a domain. Managed by Algorithm LEARN phase (direct writes), `LIFEOS/TOOLS/KnowledgeHarvester.ts` (validation/maintenance), and the `/knowledge` skill. Particularly useful for research agents and custom agents composed with specialized traits.
---
@@ -262,7 +262,7 @@ Background agents can hang or go silent with no visibility. The Pulse agent-guar
## References
-- **Master Architecture:** `~/.claude/LIFEOS/DOCUMENTATION/LifeosSystemArchitecture.md` — authoritative system-of-systems reference
+- **Master Architecture:** `{{LIFEOS_DIR}}/DOCUMENTATION/LifeosSystemArchitecture.md` — authoritative system-of-systems reference
- **Agent Personalities:** Individual `agents/*.md` files — Named agent backstories and voice settings
- **Managed Agents:** https://www.anthropic.com/engineering/managed-agents — Anthropic cloud agent API
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Algorithm/AlgorithmSystem.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Algorithm/AlgorithmSystem.md
index 67e7646acc..c619fe02c9 100644
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Algorithm/AlgorithmSystem.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Algorithm/AlgorithmSystem.md
@@ -66,7 +66,7 @@ A registry of known-good refactors **intentionally deferred** because they're pr
**Status:** Deferred. Re-open trigger: a second concrete artifact-owner needs interview-shaped clarification.
-**Current state (N=1):** the ISA Interview workflow (`~/.claude/skills/ISA/Workflows/Interview.md`) walks an ISA's thin sections, asks one question at a time, writes answers back. Telos has a parallel-shape workflow (`~/.claude/skills/Telos/Workflows/Update.md`) performing single-section TELOS edits.
+**Current state (N=1):** the ISA Interview workflow (`{{LIFEOS_ROOT}}/skills/ISA/Workflows/Interview.md`) walks an ISA's thin sections, asks one question at a time, writes answers back. Telos has a parallel-shape workflow (`{{LIFEOS_ROOT}}/skills/Telos/Workflows/Update.md`) performing single-section TELOS edits.
**Why not extract today:** at N=1.5 (ISA fully real, Telos parallel-but-different), DRY-ing into a shared `Clarify(artifact, schema, thin_section_detector, question_generator)` primitive would force a speculative API shape. The mechanic differs in cadence (per-task vs quarterly), audience (task-deliverable vs life-context), and detector logic (section-fillability vs TELOS-freshness staleness).
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Amber/AmberSystem.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Amber/AmberSystem.md
index 87e6acd082..57eef116e2 100644
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Amber/AmberSystem.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Amber/AmberSystem.md
@@ -102,7 +102,7 @@ This is the piece that makes "a lot more inputs" cheap instead of costly, and it
- **Write-ahead.** The record hits the append-only ledger *first*, unconditionally, before grading. Nothing is lost if grading or routing fails.
- **Idempotent.** Dedup identity = normalized `url` + content hash (falling back to `source`+`external_id`). The same item arriving via three inputs is one ledger row; a retry never duplicates.
- **Async downstream.** Grade and route run after the write, off the capture path, so capture is always fast and never blocks on a model call.
-- **Privacy-gated.** A `personal` record never crosses to cloud storage without an explicit rule — the local→cloud analog of the `~/.claude`→public boundary.
+- **Privacy-gated.** A `personal` record never crosses to cloud storage without an explicit rule — the local→cloud analog of the `{{LIFEOS_ROOT}}`→public boundary.
**Adding an input = implement this contract.** A new source — a Slack star, an email forward, a Kindle highlight — writes one adapter that emits this record shape and hands it to Amber. It inherits preservation, dedup, grading, routing, and resurfacing for free. That is the entire payoff of naming the system: the contract is the thing "more inputs" plug into.
@@ -220,7 +220,7 @@ Phased so each phase is independently valuable and shippable. Nothing here is bu
- **System of record (no ambiguity).** The D1 ledger is the append-only source of truth — *every* capture, including grade-rejects. KNOWLEDGE `idea` notes are a **curated promotion layer built FROM the ledger**, never a parallel history. Two co-equal "histories" diverge and rot; don't build that.
- **Dedup key.** The same URL arrives via hotkey, bookmark cron, and Feed. Canonical idea identity = normalized URL + content hash, so the ledger doesn't fill with dupes and grading doesn't needlessly re-run.
- **Grade versioning.** Store the grader/TELOS version next to each score, or historical scores become uninterpretable once TELOS evolves.
-- **Privacy boundary.** Voice markers and personal captures flowing to cloud D1 cross a local→cloud line — the adjacent case to the `~/.claude`→public constitutional rule. Specify which content classes are ledger-eligible before wiring the personal inputs.
+- **Privacy boundary.** Voice markers and personal captures flowing to cloud D1 cross a local→cloud line — the adjacent case to the `{{LIFEOS_ROOT}}`→public constitutional rule. Specify which content classes are ledger-eligible before wiring the personal inputs.
- **Backfill-ready.** Design the schema to accept the existing sheet's rows (timestamp, source attribution) so the Phase-5 migration is lossless.
**Phase 2 — Auto-routing (✅ SHIPPED 2026-07-08).** `amber route` grades every unrouted ledger capture against TELOS (the 10-way taxonomy via `Inference.ts`) and fans it: `knowledge`/`blog_seed`/`help_understand` → a KNOWLEDGE `idea`-note, `work_item` → `Type:queue`, `project_integration` → `Type:project` — then marks the row `routed` via the worker's enrichment endpoint (raw capture stays immutable). Dry-run-first (`--dry-run` writes nothing); GH issues gated behind `--commit-issues`. Live-verified end to end, idempotent (routed rows skip). The manual "where does this go?" step is gone. Refinements pending: typed-`related`-link backfill on the notes; a launchd schedule so routing runs unattended.
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Arbol/ArbolSystem.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Arbol/ArbolSystem.md
index 3702d07d64..02dc01b270 100644
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Arbol/ArbolSystem.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Arbol/ArbolSystem.md
@@ -198,7 +198,7 @@ Actions declare dependencies in `action.json` under `requires`. The runner injec
**Local:**
```bash
-cd ~/.claude/LIFEOS/ARBOL/Actions
+cd "${LIFEOS_DIR}/ARBOL/Actions"
bun lib/runner.v2.ts run A_LABEL_AND_RATE --input '{"content": "Your text here"}'
bun lib/runner.v2.ts list
```
@@ -228,11 +228,11 @@ curl -X POST https://arbol-a-your-action.YOUR-SUBDOMAIN.workers.dev/ \
### Creating a New Action
-1. Create directory: `mkdir ~/.claude/LIFEOS/USER/CUSTOMIZATIONS/ARBOL/ACTIONS/A_YOUR_ACTION`
+1. Create directory: `mkdir "${LIFEOS_DIR}/USER/CUSTOMIZATIONS/ARBOL/ACTIONS/A_YOUR_ACTION"`
2. Define manifest (`action.json`) with name, description, input/output schema, requires
3. Implement logic (`action.ts`) using `execute(input, ctx)` pattern
4. Test locally: `bun lib/runner.v2.ts run A_YOUR_ACTION --input '{"content": "test"}'`
-5. Deploy to cloud (optional): add Worker under `~/.claude/LIFEOS/USER/CUSTOMIZATIONS/ARBOL/Workers/a-your-action/`, then `bash deploy.sh a-your-action`
+5. Deploy to cloud (optional): add Worker under `{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/ARBOL/Workers/a-your-action/`, then `bash deploy.sh a-your-action`
### Action Best Practices
@@ -279,7 +279,7 @@ actions:
**PIPELINE.md Format (local):**
-Local pipeline definitions live in `~/.claude/LIFEOS/ARBOL/Pipelines/[Domain]_[Pipeline-Name]/PIPELINE.md`:
+Local pipeline definitions live in `{{LIFEOS_DIR}}/ARBOL/Pipelines/[Domain]_[Pipeline-Name]/PIPELINE.md`:
```markdown
# [Pipeline_Name] Pipeline
@@ -300,12 +300,12 @@ Local pipeline definitions live in `~/.claude/LIFEOS/ARBOL/Pipelines/[Domain]_[P
- **Prefix:** `P_` for pipelines
- **Worker name:** `arbol-p-{kebab-case-name}`
-- **Local directory:** `~/.claude/LIFEOS/ARBOL/Pipelines/[Domain]_[Pipeline-Name]/PIPELINE.md`
+- **Local directory:** `{{LIFEOS_DIR}}/ARBOL/Pipelines/[Domain]_[Pipeline-Name]/PIPELINE.md`
### Running Pipelines
```bash
-cd ~/.claude/LIFEOS/ARBOL/Actions
+cd "${LIFEOS_DIR}/ARBOL/Actions"
bun lib/pipeline-runner.ts run P_LABEL_AND_RATE --url "https://youtube.com/watch?v=..."
bun lib/pipeline-runner.ts list
```
@@ -313,7 +313,7 @@ bun lib/pipeline-runner.ts list
### Creating a New Pipeline
1. Identify the workflow: what Actions exist, what needs creating, what data passes between steps
-2. Create directory: `mkdir -p ~/.claude/LIFEOS/ARBOL/Pipelines/[Domain]_[Pipeline-Name]`
+2. Create directory: `mkdir -p "${LIFEOS_DIR}/ARBOL/Pipelines/[Domain]_[Pipeline-Name]"`
3. Define overview table in PIPELINE.md
4. For each step, specify action, input (from upstream), and output fields
5. For cloud deployment, create a Worker with service bindings to each action
@@ -615,7 +615,7 @@ Each layer depends on the one below:
### Deploy Script
```bash
-cd ~/.claude/LIFEOS/USER/CUSTOMIZATIONS/ARBOL
+cd "${LIFEOS_DIR}/USER/CUSTOMIZATIONS/ARBOL"
bash deploy.sh a-your-action # Deploy single worker
echo "token" | bunx wrangler secret put AUTH_TOKEN --name arbol-a-your-action
```
@@ -711,15 +711,15 @@ Check `flow-state.json` for errors. Common: malformed pipeline output, AUTH_TOKE
| Document | Path | Description |
|----------|------|-------------|
-| Source Code | `~/.claude/LIFEOS/USER/CUSTOMIZATIONS/ARBOL/` | Cloudflare Workers source repository |
+| Source Code | `{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/ARBOL/` | Cloudflare Workers source repository |
| Cloudflare Skill | `Cloudflare` skill (LifeOS Skill registry) | MCP + wrangler dual-mode operations |
| Architecture | `LifeosSystemArchitecture.md` | LifeOS system architecture |
-| System Actions | `~/.claude/LIFEOS/ARBOL/Actions/` | Framework actions (examples) |
-| System Pipelines | `~/.claude/LIFEOS/ARBOL/Pipelines/` | Framework pipelines (examples) |
-| System Flows | `~/.claude/LIFEOS/ARBOL/Flows/` | Framework flows (examples) |
-| Personal Actions | `~/.claude/LIFEOS/USER/CUSTOMIZATIONS/ARBOL/ACTIONS/` | User-defined actions (override system) |
-| Personal Pipelines | `~/.claude/LIFEOS/USER/CUSTOMIZATIONS/ARBOL/PIPELINES/` | User-defined pipelines (override system) |
-| Personal Flows | `~/.claude/LIFEOS/USER/CUSTOMIZATIONS/ARBOL/FLOWS/` | User-defined flows (override system) |
+| System Actions | `{{LIFEOS_DIR}}/ARBOL/Actions/` | Framework actions (examples) |
+| System Pipelines | `{{LIFEOS_DIR}}/ARBOL/Pipelines/` | Framework pipelines (examples) |
+| System Flows | `{{LIFEOS_DIR}}/ARBOL/Flows/` | Framework flows (examples) |
+| Personal Actions | `{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/ARBOL/ACTIONS/` | User-defined actions (override system) |
+| Personal Pipelines | `{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/ARBOL/PIPELINES/` | User-defined pipelines (override system) |
+| Personal Flows | `{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/ARBOL/FLOWS/` | User-defined flows (override system) |
---
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Config/ConfigSystem.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Config/ConfigSystem.md
index 3f259cffcb..7a6c9b98c5 100644
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Config/ConfigSystem.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Config/ConfigSystem.md
@@ -11,7 +11,7 @@ LifeOS configuration follows the **system/user separation** contract (`LIFEOS/DO
## The split, end-to-end
```
-~/.claude/ # SYSTEM tree (public LifeOS)
+{{LIFEOS_ROOT}}/ # SYSTEM tree (public LifeOS)
├── settings.json # Generated at SessionStart by MergeSettings.ts
├── settings.system.json # SYSTEM defaults (public-safe, ships in LifeOS)
├── CLAUDE.md # SYSTEM routing table (public-safe)
@@ -70,11 +70,11 @@ LifeOS configuration follows the **system/user separation** contract (`LIFEOS/DO
## Two-repo sync
The two trees are physically separate git repos:
-- `~/.claude/` → `/.claude` (PRIVATE GitHub)
+- `{{LIFEOS_ROOT}}/` → `/.claude` (PRIVATE GitHub)
- `~/.config/LIFEOS/USER/` → `/` (PRIVATE GitHub)
-`~/.claude/.git/hooks/pre-push` auto-commits and pushes `~/.config/LIFEOS/USER/` before each push from `~/.claude/`, so the two repos stay in sync structurally. A "kai update" / "push both repos" workflow wraps this with four boundary gates:
-1. USER-zone leak check on pending `~/.claude` changes
+`{{LIFEOS_ROOT}}/.git/hooks/pre-push` auto-commits and pushes `~/.config/LIFEOS/USER/` before each push from `{{LIFEOS_ROOT}}/`, so the two repos stay in sync structurally. A "kai update" / "push both repos" workflow wraps this with four boundary gates:
+1. USER-zone leak check on pending `{{LIFEOS_ROOT}}` changes
2. `DenyListCheck.ts` must return 0 real-leaks
3. Both remotes confirmed private via `gh api`
4. Post-push HEAD verification on both repos
@@ -83,7 +83,7 @@ The two trees are physically separate git repos:
## Public releases
-The Shadow Release system (`skills/_LIFEOS/Tools/ShadowRelease.ts`) produces public staging at `~/.claude/LIFEOS/LIFEOS_RELEASES/{VERSION}/.claude/` via **containment** — clone the live tree, delete sensitive zones (USER, MEMORY, private underscore-prefixed skills), overlay fixed public templates from `skills/_LIFEOS/RELEASE_TEMPLATES/` (including `CLAUDE.public.md` + `settings.public.json`), run 14 gates (G1–G14: zone deletion, identity grep, CF ID grep, trufflehog, .env strays, private tokens, ref integrity, private-skill refs, username-path leak, staging boot, dashboard leak, template-only USER/MEMORY, hidden-file leakage, critical-artifact presence). Write `.shadow-state.json` report. `EmitSkill.ts` then reshapes this `.claude/` staging into the shippable `{VERSION}/LifeOS/` skill (and drops the staging clone) — the published distribution unit is that one self-contained skill, not the `.claude/` tree. Staging is isolated from `~/Projects/LIFEOS/`; public publish is a separate explicit step.
+The Shadow Release system (`skills/_LIFEOS/Tools/ShadowRelease.ts`) produces public staging at `{{LIFEOS_DIR}}/LIFEOS_RELEASES/{VERSION}/.claude/` via **containment** — clone the live tree, delete sensitive zones (USER, MEMORY, private underscore-prefixed skills), overlay fixed public templates from `skills/_LIFEOS/RELEASE_TEMPLATES/` (including `CLAUDE.public.md` + `settings.public.json`), run 14 gates (G1–G14: zone deletion, identity grep, CF ID grep, trufflehog, .env strays, private tokens, ref integrity, private-skill refs, username-path leak, staging boot, dashboard leak, template-only USER/MEMORY, hidden-file leakage, critical-artifact presence). Write `.shadow-state.json` report. `EmitSkill.ts` then reshapes this `.claude/` staging into the shippable `{VERSION}/LifeOS/` skill (and drops the staging clone) — the published distribution unit is that one self-contained skill, not the `.claude/` tree. Staging is isolated from `~/Projects/LIFEOS/`; public publish is a separate explicit step.
The `` skill workflows:
- **ReviewContainmentZones** — reconcile zone module against live tree (mandatory before any release build).
@@ -98,7 +98,7 @@ The `` skill workflows:
- **Phase C (2026-05-22)** — `CLAUDE.md` becomes thin router with direct `@`-imports of USER identity files. `OPERATIONAL_RULES.md` created in `LIFEOS/USER/CONFIG/`. `CLAUDE.user.md` sidecar created and then deleted 2026-05-23 (merged back into `CLAUDE.md`) because CC doesn't follow transitive `@`-imports.
- **Phase E (2026-05-22)** — `SystemFileGuard.hook.ts` runtime write-time enforcement begins.
- **Phase F (2026-05-22)** — `LifeosConfig.ts` typed loader + `LIFEOS_CONFIG.toml` populated. Private skills migrated from hardcoded credentials to `LifeosConfig.load()`.
-- **Phase G (2026-05-22→23)** — separate private GitHub repo created for USER data. `~/.claude/LIFEOS/USER` becomes symlink to `~/.config/LIFEOS/USER`. Pre-push hook installed for two-repo sync. A two-repo-push workflow ("update the kai repo" / "push both repos") ships in the private `` skill with four boundary gates.
+- **Phase G (2026-05-22→23)** — separate private GitHub repo created for USER data. `{{LIFEOS_DIR}}/USER` becomes symlink to `~/.config/LIFEOS/USER`. Pre-push hook installed for two-repo sync. A two-repo-push workflow ("update the kai repo" / "push both repos") ships in the private `` skill with four boundary gates.
- **Phase H (deferred)** — PR-time `DenyListCheck` via GitHub Actions; community v5→v6 migration tool.
## Pre-v6.0 history
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Delegation/DelegationSystem.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Delegation/DelegationSystem.md
index 10a4796738..9fabaf6927 100755
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Delegation/DelegationSystem.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Delegation/DelegationSystem.md
@@ -202,12 +202,12 @@ Three primitives for non-blocking work. Pick the right one:
## Knowledge Archive Access
-Delegated agents can query the **Knowledge Archive** (`~/.claude/LIFEOS/MEMORY/KNOWLEDGE/`) for accumulated knowledge organized by 4 entity types: People (human beings), Companies (organizations), Ideas (insights/theses/analyses), Research (longer-form research notes). Topic is a tag, not a domain. Managed by Algorithm LEARN phase (direct writes), `LIFEOS/TOOLS/KnowledgeHarvester.ts` (validation/maintenance), and the `/knowledge` skill. Include archive query instructions in agent prompts when the task benefits from prior research or domain context.
+Delegated agents can query the **Knowledge Archive** (`{{LIFEOS_DIR}}/MEMORY/KNOWLEDGE/`) for accumulated knowledge organized by 4 entity types: People (human beings), Companies (organizations), Ideas (insights/theses/analyses), Research (longer-form research notes). Topic is a tag, not a domain. Managed by Algorithm LEARN phase (direct writes), `LIFEOS/TOOLS/KnowledgeHarvester.ts` (validation/maintenance), and the `/knowledge` skill. Include archive query instructions in agent prompts when the task benefits from prior research or domain context.
---
**See Also:**
-- `~/.claude/LIFEOS/DOCUMENTATION/LifeosSystemArchitecture.md` — Master architecture reference (system-of-systems)
+- `{{LIFEOS_DIR}}/DOCUMENTATION/LifeosSystemArchitecture.md` — Master architecture reference (system-of-systems)
- SKILL.md > Delegation (Quick Reference) - Condensed trigger table
- Workflows/Delegation.md - Operational delegation procedures
- Workflows/BackgroundDelegation.md - Background agent patterns
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Fabric/FabricSystem.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Fabric/FabricSystem.md
index 088350a27d..74790abc66 100755
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Fabric/FabricSystem.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Fabric/FabricSystem.md
@@ -10,7 +10,7 @@ version: 1.1.6
> Fabric patterns are reusable moves in the LifeOS hill-climb (`LifeOs/LifeOsThesis.md`) — packaged transformations the DA applies to incoming content.
-**Primary Skill:** `~/.claude/skills/Fabric/SKILL.md`
+**Primary Skill:** `{{LIFEOS_ROOT}}/skills/Fabric/SKILL.md`
This document provides a quick reference. For full functionality, invoke the Fabric skill.
@@ -18,7 +18,7 @@ This document provides a quick reference. For full functionality, invoke the Fab
## Quick Reference
-**Patterns Location:** `~/.claude/skills/Fabric/Patterns/` (240+ patterns)
+**Patterns Location:** `{{LIFEOS_ROOT}}/skills/Fabric/Patterns/` (240+ patterns)
### Invoke Fabric Skill
@@ -77,20 +77,20 @@ Only use `fabric` command for:
User: "Update fabric patterns"
-> Fabric skill > UpdatePatterns workflow
-> Runs fabric -U
--> Syncs to ~/.claude/skills/Fabric/Patterns/
+-> Syncs to {{LIFEOS_ROOT}}/skills/Fabric/Patterns/
```
**Manual:**
```bash
-fabric -U && rsync -av ~/.config/fabric/patterns/ ~/.claude/skills/Fabric/Patterns/
+fabric -U && rsync -av ~/.config/fabric/patterns/ "${LIFEOS_ROOT}/skills/Fabric/Patterns/"
```
---
## See Also
-- **Full Skill:** `~/.claude/skills/Fabric/SKILL.md`
-- **Pattern Execution:** `~/.claude/skills/Fabric/Workflows/ExecutePattern.md`
-- **Pattern Updates:** `~/.claude/skills/Fabric/Workflows/UpdatePatterns.md`
-- **All Patterns:** `~/.claude/skills/Fabric/Patterns/`
-- **Architecture:** `~/.claude/LIFEOS/DOCUMENTATION/LifeosSystemArchitecture.md` — Master LifeOS architecture reference
+- **Full Skill:** `{{LIFEOS_ROOT}}/skills/Fabric/SKILL.md`
+- **Pattern Execution:** `{{LIFEOS_ROOT}}/skills/Fabric/Workflows/ExecutePattern.md`
+- **Pattern Updates:** `{{LIFEOS_ROOT}}/skills/Fabric/Workflows/UpdatePatterns.md`
+- **All Patterns:** `{{LIFEOS_ROOT}}/skills/Fabric/Patterns/`
+- **Architecture:** `{{LIFEOS_DIR}}/DOCUMENTATION/LifeosSystemArchitecture.md` — Master LifeOS architecture reference
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Freshness/FreshnessSystem.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Freshness/FreshnessSystem.md
index ac68c7e17b..91972d99ac 100644
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Freshness/FreshnessSystem.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Freshness/FreshnessSystem.md
@@ -90,7 +90,7 @@ Statusline FRESH line and `/api/freshness/summary` both surface these grades.
## Library
-Single source: `~/.claude/LIFEOS/TOOLS/TelosFreshness.ts` (named for historical reasons; covers all constitutional files).
+Single source: `{{LIFEOS_DIR}}/TOOLS/TelosFreshness.ts` (named for historical reasons; covers all constitutional files).
```ts
// TELOS-specific (per-section)
@@ -156,22 +156,22 @@ Override per-installation by editing the `STALENESS_THRESHOLDS` map.
```bash
# Per-section TELOS freshness (the original surface)
-bun ~/.claude/LIFEOS/TOOLS/TelosFreshness.ts
-bun ~/.claude/LIFEOS/TOOLS/TelosFreshness.ts --json
-bun ~/.claude/LIFEOS/TOOLS/TelosFreshness.ts --bump goals
+bun "${LIFEOS_DIR}/TOOLS/TelosFreshness.ts"
+bun "${LIFEOS_DIR}/TOOLS/TelosFreshness.ts" --json
+bun "${LIFEOS_DIR}/TOOLS/TelosFreshness.ts" --bump goals
# Multi-file constitutional freshness
-bun ~/.claude/LIFEOS/TOOLS/TelosFreshness.ts context
-bun ~/.claude/LIFEOS/TOOLS/TelosFreshness.ts context --json
+bun "${LIFEOS_DIR}/TOOLS/TelosFreshness.ts" context
+bun "${LIFEOS_DIR}/TOOLS/TelosFreshness.ts" context --json
# Content quality audit (read-only)
-bun ~/.claude/LIFEOS/TOOLS/ContextAudit.ts
-bun ~/.claude/LIFEOS/TOOLS/ContextAudit.ts --json
+bun "${LIFEOS_DIR}/TOOLS/ContextAudit.ts"
+bun "${LIFEOS_DIR}/TOOLS/ContextAudit.ts" --json
```
## Pulse routes
-Pulse module at `~/.claude/LIFEOS/PULSE/modules/telos.ts` exposes the same data over HTTP:
+Pulse module at `{{LIFEOS_DIR}}/PULSE/modules/telos.ts` exposes the same data over HTTP:
- `GET /api/telos/freshness` — full TELOS per-section freshness
- `GET /api/telos/freshness/stale` — TELOS stale sections only
@@ -186,7 +186,7 @@ Response is cached for 60s. Reload via Pulse `/reload` invalidates the cache and
The statusline FRESH line cannot afford a network call on every refresh (1s interval, blocking on Pulse-down would freeze rendering). `LIFEOS/TOOLS/FreshnessCache.ts` writes a tiny mirror of `/api/freshness/summary` to a private file the statusline reads directly with `jq`.
```
-~/.claude/LIFEOS/USER/CACHE/freshness.json
+{{LIFEOS_DIR}}/USER/CACHE/freshness.json
```
The file is private (under `USER/`, never released), atomically written (temp file + rename), shape-identical to `/api/freshness/summary` plus a `generated_at` timestamp, and capped under 4KB.
@@ -212,7 +212,7 @@ Pulse remains the canonical reader for the dashboard; the cache file is the cano
## Interview integration
-`/interview` runs the **ContextCheckin** workflow at `~/.claude/skills/Interview/Workflows/ContextCheckin.md`. The workflow:
+`/interview` runs the **ContextCheckin** workflow at `{{LIFEOS_ROOT}}/skills/Interview/Workflows/ContextCheckin.md`. The workflow:
1. Reads `readContextFreshness()` plus `readTelosFreshness()` for the per-section detail.
2. Identifies stale files and stale TELOS sections, sorted most-stale-first.
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Hooks/HookSystem.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Hooks/HookSystem.md
index c48b7dd4a0..ed513addb3 100755
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Hooks/HookSystem.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Hooks/HookSystem.md
@@ -15,8 +15,8 @@ version: 1.2.50
**Event-Driven Automation Infrastructure**
-**Location:** `~/.claude/hooks/`
-**Configuration:** `~/.claude/settings.json` (GENERATED — merged from `settings.system.json` + `LIFEOS/USER/CONFIG/settings.user.json` by `MergeSettings.ts`; for events the user file defines as a plain array — UserPromptSubmit, PostToolUse, PreToolUse, Stop, SessionEnd — the user array REPLACES the system array, so `settings.json` is the only registration truth)
+**Location:** `{{LIFEOS_ROOT}}/hooks/`
+**Configuration:** `{{LIFEOS_ROOT}}/settings.json` (GENERATED — merged from `settings.system.json` + `LIFEOS/USER/CONFIG/settings.user.json` by `MergeSettings.ts`; for events the user file defines as a plain array — UserPromptSubmit, PostToolUse, PreToolUse, Stop, SessionEnd — the user array REPLACES the system array, so `settings.json` is the only registration truth)
**Status:** Active — hook count auto-computed by `UpdateCounts.ts` at session end
> **Post-consolidation state (2026-07-11 hooks-BPE pass).** 30 distinct `.hook.ts` files are registered in `settings.json` (31 counting `ContextReduction.hook.sh`), plus 2 Pulse HTTP routes; **38 `.hook.ts` files exist on disk.** The 8 files that are on disk but NOT registered directly are not dead — each is imported as a `run()`/`check()` module by a consolidating dispatcher and still runnable standalone via its own shim: `SystemFileGuard`, `CommunicationSkillGuard`, `EgressClassGuard` → `PreToolGuard`; `VerificationGate`, `WritingGate` → `StopGates`; `LoadMemory`, `MemoryDeltaSurface` → `MemoryTurnStart`; `LoopDetector` → `PostToolObserver`. The consolidation retired `TheRouter` entirely (mode/tier classification abolished; model rungs now live in `LIFEOS/TOOLS/models.ts` + `AgentInvocation.hook.ts`) and folded a family of single-purpose loggers/gates/painters into `EventLogger`, `TabState`, `StopGates`, `MemoryTurnStart`, `MemoryReviewFire`, and `PostToolObserver`. Details per event below.
@@ -30,13 +30,13 @@ The LifeOS hook system is an event-driven automation infrastructure built on Cla
**Core Capabilities:**
- **Session Management** - Auto-load context, capture summaries, manage state
- **Voice Notifications** - Text-to-speech announcements for task completions
-- **History Capture** - Automatic work/learning documentation to `~/.claude/LIFEOS/MEMORY/`
+- **History Capture** - Automatic work/learning documentation to `{{LIFEOS_DIR}}/MEMORY/`
- **Security Validation** - Active (v5+, consolidated 2026-05-14) — Single `Safety.hook.ts` dispatching by `hook_event_name`: PermissionRequest gates outgoing tool calls via the shape classifier in `lib/safety-classifier.ts` (auto-allows safe shapes, neutral on dangerous/credential/injection); PostToolUse tags WebFetch/WebSearch responses with the "treat as data" warning + injection marker. Replaces the prior split between `SmartApprover.hook.ts` and `PromptInjection.hook.ts`. The v4.0 Inspector Pipeline was deleted 2026-05-06. See `LIFEOS/DOCUMENTATION/Security/README.md`.
- **Multi-Agent Support** - Agent-specific hooks with voice routing
- **Tab Titles** - Dynamic terminal tab updates with task context
- **Unified Event Stream** - All hooks emit structured events to `events.jsonl` for real-time observability
-**Key Principle:** Most hooks run asynchronously and fail gracefully. Security hooks (e.g. `hooks/Safety.hook.ts`) are synchronous — the PermissionRequest path emits `decision: allow` JSON when safe (otherwise stdout is empty and the native engine prompts). All `.ts` hooks have `#!/usr/bin/env bun` shebangs and `+x` permissions — settings.json references them directly (e.g., `$HOME/.claude/hooks/Safety.hook.ts`) without a `bun` prefix. HTTP hooks (SkillGuard, AgentGuard) run via Pulse routes on `localhost:31337`.
+**Key Principle:** Most hooks run asynchronously and fail gracefully. Security hooks (e.g. `hooks/Safety.hook.ts`) are synchronous — the PermissionRequest path emits `decision: allow` JSON when safe (otherwise stdout is empty and the native engine prompts). All `.ts` hooks have `#!/usr/bin/env bun` shebangs and `+x` permissions — settings.json references them directly (e.g., `{{LIFEOS_ROOT}}/hooks/Safety.hook.ts`) without a `bun` prefix. HTTP hooks (SkillGuard, AgentGuard) run via Pulse routes on `localhost:31337`.
**Freshness Authority:** When adding or modifying hooks, consult the `claude-code-guide` agent to verify current hook event types, return value schemas, and available fields.
@@ -59,11 +59,11 @@ Claude Code supports the following hook events:
"SessionStart": [
{
"hooks": [
- { "type": "command", "command": "bun $HOME/.claude/hooks/HookHealer.hook.ts", "timeout": 10 },
- { "type": "command", "command": "$HOME/.claude/hooks/KittyEnvPersist.hook.ts" },
- { "type": "command", "command": "$HOME/.claude/hooks/LoadContext.hook.ts" },
- { "type": "command", "command": "bun $HOME/.claude/LIFEOS/TOOLS/FreshnessCache.ts --quiet", "timeout": 5, "async": true },
- { "type": "command", "command": "bun $HOME/.claude/LIFEOS/TOOLS/SettingsBackport.ts; bun $HOME/.claude/LIFEOS/TOOLS/MergeSettings.ts --system $HOME/.claude/settings.system.json --user $HOME/.claude/LIFEOS/USER/CONFIG/settings.user.json --output $HOME/.claude/settings.json", "timeout": 15, "async": true }
+ { "type": "command", "command": "bun {{LIFEOS_ROOT}}/hooks/HookHealer.hook.ts", "timeout": 10 },
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/KittyEnvPersist.hook.ts" },
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/LoadContext.hook.ts" },
+ { "type": "command", "command": "bun {{LIFEOS_DIR}}/TOOLS/FreshnessCache.ts --quiet", "timeout": 5, "async": true },
+ { "type": "command", "command": "bun {{LIFEOS_DIR}}/TOOLS/SettingsBackport.ts; bun {{LIFEOS_DIR}}/TOOLS/MergeSettings.ts --system {{LIFEOS_ROOT}}/settings.system.json --user {{LIFEOS_DIR}}/USER/CONFIG/settings.user.json --output {{LIFEOS_ROOT}}/settings.json", "timeout": 15, "async": true }
]
}
]
@@ -94,17 +94,17 @@ Claude Code supports the following hook events:
"SessionEnd": [
{
"hooks": [
- { "type": "command", "command": "$HOME/.claude/hooks/WorkCompletionLearning.hook.ts" },
- { "type": "command", "command": "$HOME/.claude/hooks/SessionCleanup.hook.ts" },
- { "type": "command", "command": "$HOME/.claude/hooks/UpdateCounts.hook.ts" },
- { "type": "command", "command": "$HOME/.claude/hooks/MemoryHealthGate.hook.ts" },
- { "type": "command", "command": "$HOME/.claude/hooks/DocIntegrity.hook.ts" },
- { "type": "command", "command": "$HOME/.claude/hooks/IntegrityCheck.hook.ts" }
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/WorkCompletionLearning.hook.ts" },
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/SessionCleanup.hook.ts" },
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/UpdateCounts.hook.ts" },
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/MemoryHealthGate.hook.ts" },
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/DocIntegrity.hook.ts" },
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/IntegrityCheck.hook.ts" }
]
},
{
"hooks": [
- { "type": "command", "command": "$HOME/.claude/hooks/ULWorkSync.hook.ts", "timeout": 60 }
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/ULWorkSync.hook.ts", "timeout": 60 }
]
}
]
@@ -136,12 +136,12 @@ Claude Code supports the following hook events:
```json
{
"UserPromptSubmit": [
- { "hooks": [ { "type": "command", "command": "$HOME/.claude/hooks/PromptProcessing.hook.ts", "timeout": 30, "async": true } ] },
- { "hooks": [ { "type": "command", "command": "$HOME/.claude/hooks/SatisfactionCapture.hook.ts", "timeout": 20, "async": true } ] },
- { "hooks": [ { "type": "command", "command": "$HOME/.claude/hooks/ReminderRouter.hook.ts", "timeout": 5, "async": true } ] },
- { "hooks": [ { "type": "command", "command": "$HOME/.claude/hooks/DriftReminder.hook.ts", "timeout": 5, "async": true } ] },
- { "hooks": [ { "type": "command", "command": "$HOME/.claude/hooks/MemoryTurnStart.hook.ts", "timeout": 8 } ] },
- { "hooks": [ { "type": "command", "command": "$HOME/.claude/hooks/AlgorithmNudge.hook.ts", "timeout": 5, "async": true } ] }
+ { "hooks": [ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/PromptProcessing.hook.ts", "timeout": 30, "async": true } ] },
+ { "hooks": [ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/SatisfactionCapture.hook.ts", "timeout": 20, "async": true } ] },
+ { "hooks": [ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/ReminderRouter.hook.ts", "timeout": 5, "async": true } ] },
+ { "hooks": [ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/DriftReminder.hook.ts", "timeout": 5, "async": true } ] },
+ { "hooks": [ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/MemoryTurnStart.hook.ts", "timeout": 8 } ] },
+ { "hooks": [ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/AlgorithmNudge.hook.ts", "timeout": 5, "async": true } ] }
]
}
```
@@ -203,12 +203,12 @@ Claude Code supports the following hook events:
"Stop": [
{
"hooks": [
- { "type": "command", "command": "$HOME/.claude/hooks/LastResponseCache.hook.ts" },
- { "type": "command", "command": "$HOME/.claude/hooks/TabState.hook.ts" },
- { "type": "command", "command": "$HOME/.claude/hooks/VoiceCompletion.hook.ts" },
- { "type": "command", "command": "$HOME/.claude/hooks/ISARenderOnStop.hook.ts" },
- { "type": "command", "command": "$HOME/.claude/hooks/StopGates.hook.ts" },
- { "type": "command", "command": "$HOME/.claude/hooks/MemoryReviewFire.hook.ts" }
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/LastResponseCache.hook.ts" },
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/TabState.hook.ts" },
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/VoiceCompletion.hook.ts" },
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/ISARenderOnStop.hook.ts" },
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/StopGates.hook.ts" },
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/MemoryReviewFire.hook.ts" }
]
}
]
@@ -256,7 +256,7 @@ Each Stop hook is a self-contained `.hook.ts` file that reads stdin via shared `
### 5. **PreToolUse**
**When:** Before Claude executes any tool
**Use Cases:**
-- Security validation across write/exec operations (Bash, Write, Edit, MultiEdit) — `PreToolGuard` blocks USER content landing in SYSTEM files, raw email sends, and over-ceiling Tier-2 egress; the native permission denylist covers the rest
+- Runtime-path validation across Bash/Write/Edit/MultiEdit/Read/Glob/Grep — `PreToolGuard` blocks unresolved `{{LIFEOS_*}}` tool paths, then applies the existing SYSTEM-file, raw-email, and Tier-2 egress guards where relevant
- Context reduction (RTK command rewrite on Bash)
- Tab state updates on questions
- Agent execution guardrails — Pulse HTTP route at localhost:31337/hooks/agent-guard
@@ -266,19 +266,20 @@ Each Stop hook is a self-contained `.hook.ts` file that reads stdin via shared `
```json
{
"PreToolUse": [
- { "matcher": "Bash", "hooks": [ { "type": "command", "command": "$HOME/.claude/hooks/ContextReduction.hook.sh" } ] },
+ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/ContextReduction.hook.sh" } ] },
{ "matcher": "Skill", "hooks": [ { "type": "http", "url": "http://localhost:31337/hooks/skill-guard" } ] },
{ "matcher": "Agent", "hooks": [
{ "type": "http", "url": "http://localhost:31337/hooks/agent-guard" },
- { "type": "command", "command": "$HOME/.claude/hooks/AgentInvocation.hook.ts" }
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/AgentInvocation.hook.ts" }
] },
- { "matcher": "AskUserQuestion", "hooks": [ { "type": "command", "command": "$HOME/.claude/hooks/TabState.hook.ts" } ] },
- { "matcher": "Bash|Write|Edit|MultiEdit", "hooks": [ { "type": "command", "command": "$HOME/.claude/hooks/PreToolGuard.hook.ts" } ] }
+ { "matcher": "AskUserQuestion", "hooks": [ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/TabState.hook.ts" } ] },
+ { "matcher": "Bash|Write|Edit|MultiEdit|Read|Glob|Grep", "hooks": [ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/PreToolGuard.hook.ts" } ] }
]
}
```
**Security guard (active — `PreToolGuard.hook.ts`, the ONE PreToolUse blocking dispatcher, consolidated 2026-07-11):** Reads stdin ONCE and routes by tool to the three isolated blocker modules (each still runnable standalone via its own shim), FIRST block wins (`exit 2`, message to stderr → model):
+- `Bash | Write | Edit | MultiEdit | Read | Glob | Grep` → inline LifeOS path guard (blocks unresolved `{{LIFEOS_ROOT}}`, `{{LIFEOS_DIR}}`, or `{{LIFEOS_CONFIG_DIR}}` in executable/path-bearing fields; intentionally ignores Write/Edit content bodies so documentation can contain semantic placeholders)
- `Write | Edit | MultiEdit` → `SystemFileGuard.check` (blocks deny-list USER content landing in a SYSTEM file; fail-OPEN)
- `Bash` → `CommunicationSkillGuard.check` (blocks raw email send; fail-OPEN), then `EgressClassGuard.check` (blocks over-ceiling Tier-2 egress; fail-CLOSED on a Tier-2-signature call whose classification throws, fail-OPEN otherwise)
@@ -288,7 +289,7 @@ Each check is wrapped in its own try/catch so one guard throwing can never suppr
- `ContextReduction.hook.sh` - Context reduction via [RTK](https://github.com/rtk-ai/rtk). Transparently rewrites Bash commands to `rtk` equivalents for 60-90% token reduction across git, build, test, lint, and package manager output. Runs on the Bash matcher. Meta commands (use directly, not through hook): `rtk gain` (savings analytics), `rtk gain --history` (command history), `rtk discover` (missed opportunities), `rtk proxy ` (bypass filtering). Note: if `rtk gain` fails, check for name collision with reachingforthejack/rtk (Rust Type Kit).
- `TabState.hook.ts` *(PreToolUse:AskUserQuestion branch; formerly `SetQuestionTab.hook.ts`)* - Sets the tab to teal "awaiting input" and saves the previous title so the PostToolUse branch can restore it when the question is answered.
-> **Historical (retired 2026-07-11):** `SecurityPipeline.hook.ts` (the v4.0 Inspector Pipeline: PatternInspector → EgressInspector) is no longer registered — its blocking role was absorbed by `PreToolGuard` above and the native permission denylist. `SetQuestionTab.hook.ts` was merged into the unified `TabState.hook.ts`. The `Read` matcher was dropped (no PreToolUse security hook fires on Read anymore).
+> **Historical (retired 2026-07-11):** `SecurityPipeline.hook.ts` (the v4.0 Inspector Pipeline: PatternInspector → EgressInspector) is no longer registered — its blocking role was absorbed by `PreToolGuard` above and the native permission denylist. `SetQuestionTab.hook.ts` was merged into the unified `TabState.hook.ts`. Its broad Read-time content inspection remains retired; Read/Glob/Grep now route only through the narrow unresolved-LifeOS-path guard.
---
@@ -300,27 +301,27 @@ Each check is wrapped in its own try/catch so one guard throwing can never suppr
```json
{
"PostToolUse": [
- { "matcher": "Agent", "hooks": [ { "type": "command", "command": "$HOME/.claude/hooks/AgentInvocation.hook.ts" } ] },
- { "matcher": "WebFetch", "hooks": [ { "type": "command", "command": "$HOME/.claude/hooks/Safety.hook.ts", "timeout": 5 } ] },
- { "matcher": "WebSearch", "hooks": [ { "type": "command", "command": "$HOME/.claude/hooks/Safety.hook.ts", "timeout": 5 } ] },
- { "matcher": "mcp__.*([Gg]mail|[Mm]ail|[Dd]rive|[Cc]alendar|[Ii]nbox).*", "hooks": [ { "type": "command", "command": "$HOME/.claude/hooks/Safety.hook.ts", "timeout": 5 } ] },
- { "matcher": "ToolSearch", "hooks": [ { "type": "command", "command": "$HOME/.claude/hooks/Safety.hook.ts", "timeout": 5 } ] },
- { "matcher": "AskUserQuestion", "hooks": [ { "type": "command", "command": "$HOME/.claude/hooks/TabState.hook.ts" } ] },
+ { "matcher": "Agent", "hooks": [ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/AgentInvocation.hook.ts" } ] },
+ { "matcher": "WebFetch", "hooks": [ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/Safety.hook.ts", "timeout": 5 } ] },
+ { "matcher": "WebSearch", "hooks": [ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/Safety.hook.ts", "timeout": 5 } ] },
+ { "matcher": "mcp__.*([Gg]mail|[Mm]ail|[Dd]rive|[Cc]alendar|[Ii]nbox).*", "hooks": [ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/Safety.hook.ts", "timeout": 5 } ] },
+ { "matcher": "ToolSearch", "hooks": [ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/Safety.hook.ts", "timeout": 5 } ] },
+ { "matcher": "AskUserQuestion", "hooks": [ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/TabState.hook.ts" } ] },
{ "matcher": "Write", "hooks": [
- { "type": "command", "command": "$HOME/.claude/hooks/ISASync.hook.ts" },
- { "type": "command", "command": "$HOME/.claude/hooks/CheckpointPerISC.hook.ts", "timeout": 30 }
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/ISASync.hook.ts" },
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/CheckpointPerISC.hook.ts", "timeout": 30 }
] },
{ "matcher": "Edit", "hooks": [
- { "type": "command", "command": "$HOME/.claude/hooks/ISASync.hook.ts" },
- { "type": "command", "command": "$HOME/.claude/hooks/CheckpointPerISC.hook.ts", "timeout": 30 }
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/ISASync.hook.ts" },
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/CheckpointPerISC.hook.ts", "timeout": 30 }
] },
{ "matcher": "MultiEdit", "hooks": [
- { "type": "command", "command": "$HOME/.claude/hooks/ISASync.hook.ts" },
- { "type": "command", "command": "$HOME/.claude/hooks/CheckpointPerISC.hook.ts", "timeout": 30 }
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/ISASync.hook.ts" },
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/CheckpointPerISC.hook.ts", "timeout": 30 }
] },
{ "hooks": [
- { "type": "command", "command": "$HOME/.claude/hooks/PostToolObserver.hook.ts", "timeout": 5 },
- { "type": "command", "command": "$HOME/.claude/hooks/EventLogger.hook.ts", "timeout": 5, "async": true }
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/PostToolObserver.hook.ts", "timeout": 5 },
+ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/EventLogger.hook.ts", "timeout": 5, "async": true }
] }
]
}
@@ -361,8 +362,8 @@ Each check is wrapped in its own try/catch so one guard throwing can never suppr
```json
{
"PostToolUseFailure": [
- { "hooks": [ { "type": "command", "command": "$HOME/.claude/hooks/EventLogger.hook.ts" } ] },
- { "hooks": [ { "type": "command", "command": "$HOME/.claude/hooks/AlgorithmNudge.hook.ts", "timeout": 5 } ] }
+ { "hooks": [ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/EventLogger.hook.ts" } ] },
+ { "hooks": [ { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/AlgorithmNudge.hook.ts", "timeout": 5 } ] }
]
}
```
@@ -394,7 +395,7 @@ Each check is wrapped in its own try/catch so one guard throwing can never suppr
"hooks": [
{
"type": "command",
- "command": "$HOME/.claude/hooks/EventLogger.hook.ts"
+ "command": "{{LIFEOS_ROOT}}/hooks/EventLogger.hook.ts"
}
]
}
@@ -446,7 +447,7 @@ To restore: `git revert 31a4b9ad9` (or `git show pre-bpe-cuts-2026-05-06:hooks/T
"hooks": [
{
"type": "command",
- "command": "$HOME/.claude/hooks/TaskGovernance.hook.ts"
+ "command": "{{LIFEOS_ROOT}}/hooks/TaskGovernance.hook.ts"
}
]
}
@@ -471,7 +472,7 @@ To restore: `git revert 31a4b9ad9` (or `git show pre-bpe-cuts-2026-05-06:hooks/T
"hooks": [
{
"type": "command",
- "command": "$HOME/.claude/hooks/EventLogger.hook.ts"
+ "command": "{{LIFEOS_ROOT}}/hooks/EventLogger.hook.ts"
}
]
}
@@ -509,24 +510,24 @@ To restore: `git revert c43dbc019`.
## Configuration
### Location
-**File:** `~/.claude/settings.json`
+**File:** `{{LIFEOS_ROOT}}/settings.json`
**Section:** `"hooks": { ... }`
### Environment Variables
-Hooks have access to all environment variables from `~/.claude/settings.json` `"env"` section:
+Hooks have access to all environment variables from `{{LIFEOS_ROOT}}/settings.json` `"env"` section:
```json
{
"env": {
- "LIFEOS_DIR": "$HOME/.claude",
+ "LIFEOS_DIR": "{{LIFEOS_ROOT}}",
"CLAUDE_CODE_MAX_OUTPUT_TOKENS": "64000"
}
}
```
**Key Variables:**
-- `LIFEOS_DIR` - LifeOS installation directory (typically `~/.claude`)
-- Hook scripts reference `$HOME/.claude` in command paths
+- `LIFEOS_DIR` - LifeOS installation directory (typically `{{LIFEOS_ROOT}}`)
+- Hook scripts reference `{{LIFEOS_ROOT}}` in command paths
### Identity Configuration (Central to Install Wizard)
@@ -584,7 +585,7 @@ const VOICE_ID = getVoiceId(); // from settings.json daidentity.voices.ma
"hooks": [
{
"type": "command",
- "command": "$HOME/.claude/hooks/my-hook.ts --arg value"
+ "command": "{{LIFEOS_ROOT}}/hooks/my-hook.ts --arg value"
}
]
}
@@ -721,7 +722,7 @@ else if (hookData.cwd && hookData.cwd.includes('/agents/')) {
}
```
-**Session Mapping:** `~/.claude/LIFEOS/MEMORY/STATE/agent-sessions.json`
+**Session Mapping:** `{{LIFEOS_DIR}}/MEMORY/STATE/agent-sessions.json`
```json
{
"session-id-abc123": "engineer",
@@ -768,7 +769,7 @@ else if (hookData.cwd && hookData.cwd.includes('/agents/')) {
- 🧠 Brain - AI inference in progress (Haiku/Sonnet thinking)
- ⚙️ Gear - Processing/working state
-**Full Documentation:** See `~/.claude/LIFEOS/DOCUMENTATION/Pulse/TerminalTabs.md`
+**Full Documentation:** See `{{LIFEOS_DIR}}/DOCUMENTATION/Pulse/TerminalTabs.md`
---
@@ -862,9 +863,9 @@ main();
### Step 3: Make Executable
```bash
-chmod +x ~/.claude/hooks/my-custom-hook.ts
+chmod +x "${LIFEOS_ROOT}/hooks/my-custom-hook.ts"
```
-> **Note:** Not needed when using the `bun` prefix in settings.json — all LifeOS hooks use `bun $HOME/.claude/hooks/...` which doesn't require the execute bit.
+> **Note:** Not needed when using the `bun` prefix in settings.json — all LifeOS hooks use `bun "${LIFEOS_ROOT}/hooks/..."` which doesn't require the execute bit.
### Step 4: Add to settings.json
```json
@@ -875,7 +876,7 @@ chmod +x ~/.claude/hooks/my-custom-hook.ts
"hooks": [
{
"type": "command",
- "command": "$HOME/.claude/hooks/my-custom-hook.ts"
+ "command": "{{LIFEOS_ROOT}}/hooks/my-custom-hook.ts"
}
]
}
@@ -887,7 +888,7 @@ chmod +x ~/.claude/hooks/my-custom-hook.ts
### Step 5: Test
```bash
# Test hook directly
-echo '{"session_id":"test","transcript_path":"/tmp/test.jsonl","hook_event_name":"Stop"}' | bun ~/.claude/hooks/my-custom-hook.ts
+echo '{"session_id":"test","transcript_path":"/tmp/test.jsonl","hook_event_name":"Stop"}' | bun "${LIFEOS_ROOT}/hooks/my-custom-hook.ts"
```
### Step 6: Restart Claude Code
@@ -934,7 +935,7 @@ await Promise.race([readPromise, timeoutPromise]);
### 6. **Environment Access**
- All `settings.json` env vars available via `process.env`
-- Use `$HOME/.claude` in settings.json for portability
+- Use `{{LIFEOS_ROOT}}` in settings.json for portability
- Access in code via `process.env.LIFEOS_DIR`
### 7. **Logging**
@@ -949,18 +950,18 @@ await Promise.race([readPromise, timeoutPromise]);
### Hook Not Running
**Check:**
-1. Is hook script executable? `chmod +x ~/.claude/hooks/my-hook.ts` (not needed when using `bun` prefix — all LifeOS hooks use `bun` prefix)
-2. Is path correct in settings.json? Use `bun $HOME/.claude/hooks/...`
-3. Is settings.json valid JSON? `jq . ~/.claude/settings.json`
+1. Is hook script executable? `chmod +x "${LIFEOS_ROOT}/hooks/my-hook.ts"` (not needed when using `bun` prefix — all LifeOS hooks use `bun` prefix)
+2. Is path correct in settings.json? Use `bun "${LIFEOS_ROOT}/hooks/..."`
+3. Is settings.json valid JSON? `jq . "${LIFEOS_ROOT}/settings.json"`
4. Did you restart Claude Code after editing settings.json?
**Debug:**
```bash
# Test hook directly
-echo '{"session_id":"test","transcript_path":"/tmp/test.jsonl","hook_event_name":"Stop"}' | bun ~/.claude/hooks/my-hook.ts
+echo '{"session_id":"test","transcript_path":"/tmp/test.jsonl","hook_event_name":"Stop"}' | bun "${LIFEOS_ROOT}/hooks/my-hook.ts"
# Check hook logs (stderr output)
-tail -f ~/.claude/hooks/debug.log # If you add logging
+tail -f "${LIFEOS_ROOT}/hooks/debug.log" # If you add logging
```
---
@@ -992,7 +993,7 @@ setTimeout(() => {
1. Is voice server running? `curl http://localhost:31337/health`
2. Is voice_id correct? See `settings.json` `daidentity.voices` for mappings
3. Is message format correct? `{"message":"...", "voice_id":"...", "title":"..."}`
-4. Is ElevenLabs API key in `~/.claude/.env`?
+4. Is ElevenLabs API key in `{{LIFEOS_ROOT}}/.env`?
**Debug:**
```bash
@@ -1012,25 +1013,25 @@ curl -X POST http://localhost:31337/notify \
### Work Not Capturing
**Check:**
-1. Does `~/.claude/LIFEOS/MEMORY/` directory exist?
-2. Does `work.json` contain an entry for this session? `jq '.sessions | to_entries[] | select(.value.sessionUUID == "")' ~/.claude/LIFEOS/MEMORY/STATE/work.json`
-3. Is hook actually running? Check `~/.claude/LIFEOS/MEMORY/RAW/` for events
-4. File permissions? `ls -la ~/.claude/LIFEOS/MEMORY/WORK/`
+1. Does `{{LIFEOS_DIR}}/MEMORY/` directory exist?
+2. Does `work.json` contain an entry for this session? `jq '.sessions | to_entries[] | select(.value.sessionUUID == "")' "${LIFEOS_DIR}/MEMORY/STATE/work.json"`
+3. Is hook actually running? Check `{{LIFEOS_DIR}}/MEMORY/RAW/` for events
+4. File permissions? `ls -la "${LIFEOS_DIR}/MEMORY/WORK/"`
**Debug:**
```bash
# All sessions in the registry, sorted by recency:
-jq '.sessions | to_entries | map({slug: .key, phase: .value.phase, mode: .value.mode, updatedAt: .value.updatedAt}) | sort_by(.updatedAt) | reverse' ~/.claude/LIFEOS/MEMORY/STATE/work.json
+jq '.sessions | to_entries | map({slug: .key, phase: .value.phase, mode: .value.mode, updatedAt: .value.updatedAt}) | sort_by(.updatedAt) | reverse' "${LIFEOS_DIR}/MEMORY/STATE/work.json"
# Hot session list via the Pulse API (uses the same data):
curl -s http://localhost:31337/api/algorithm | jq '.algorithms | map({sessionId, phase: .currentPhase, active})'
# Check recent work directories
-ls -lt ~/.claude/LIFEOS/MEMORY/WORK/ | head -10
-ls -lt ~/.claude/LIFEOS/MEMORY/LEARNING/$(date +%Y-%m)/ | head -10
+ls -lt "${LIFEOS_DIR}/MEMORY/WORK/" | head -10
+ls -lt "${LIFEOS_DIR}/MEMORY/LEARNING/$(date +%Y-%m)/" | head -10
# Check UUID-collision anomalies (multiple ISAs on one harness UUID):
-tail ~/.claude/LIFEOS/MEMORY/OBSERVABILITY/work-anomalies.jsonl
+tail "${LIFEOS_DIR}/MEMORY/OBSERVABILITY/work-anomalies.jsonl"
```
**Common Issues:**
@@ -1054,17 +1055,17 @@ tail ~/.claude/LIFEOS/MEMORY/OBSERVABILITY/work-anomalies.jsonl
### Agent Detection Failing
**Check:**
-1. Is `~/.claude/LIFEOS/MEMORY/STATE/agent-sessions.json` writable?
+1. Is `{{LIFEOS_DIR}}/MEMORY/STATE/agent-sessions.json` writable?
2. Is `[AGENT:type]` tag in `🎯 COMPLETED:` line?
3. Is agent running from correct directory? (`/agents/name/`)
**Debug:**
```bash
# Check session mappings
-cat ~/.claude/LIFEOS/MEMORY/STATE/agent-sessions.json | jq .
+cat "${LIFEOS_DIR}/MEMORY/STATE/agent-sessions.json" | jq .
# Check subagent-stop debug log
-tail -f ~/.claude/hooks/subagent-stop-debug.log
+tail -f "${LIFEOS_ROOT}/hooks/subagent-stop-debug.log"
```
**Fix:**
@@ -1091,7 +1092,8 @@ tail -f ~/.claude/hooks/subagent-stop-debug.log
**Verification:**
```bash
# Check transcript type field
-grep '"type":"user"' "$(ls -d ~/.claude/projects/*/ | head -1)"*.jsonl | head -1 | jq '.type'
+PROJECT_DIR=$(find "${LIFEOS_ROOT}/projects" -mindepth 1 -maxdepth 1 -type d | head -1)
+grep '"type":"user"' "$PROJECT_DIR"/*.jsonl | head -1 | jq '.type'
# Should output: "user" (not "human")
```
@@ -1102,14 +1104,14 @@ grep '"type":"user"' "$(ls -d ~/.claude/projects/*/ | head -1)"*.jsonl | head -1
### Context Loading Issues (SessionStart)
**Check:**
-1. Does `~/.claude/CLAUDE.md` exist?
+1. Does `{{LIFEOS_ROOT}}/CLAUDE.md` exist?
2. Is `LoadContext.hook.ts` executable?
3. Is `LIFEOS_DIR` env variable set correctly?
**Debug:**
```bash
# Test context loading directly
-bun ~/.claude/hooks/LoadContext.hook.ts
+bun "${LIFEOS_ROOT}/hooks/LoadContext.hook.ts"
# Should output with SKILL.md content
```
@@ -1117,7 +1119,7 @@ bun ~/.claude/hooks/LoadContext.hook.ts
**Common Issues:**
- Subagent sessions loading main context → Fixed (subagent detection in hook)
- File not found → Check `LIFEOS_DIR` environment variable
-- Permission denied → `chmod +x ~/.claude/hooks/LoadContext.hook.ts` (not needed when using `bun` prefix — all LifeOS hooks use `bun` prefix)
+- Permission denied → `chmod +x "${LIFEOS_ROOT}/hooks/LoadContext.hook.ts"` (not needed when using `bun` prefix — all LifeOS hooks use `bun` prefix)
---
@@ -1132,7 +1134,7 @@ Hooks in same event execute **sequentially** in order defined in settings.json:
"Stop": [
{
"hooks": [
- { "command": "$HOME/.claude/hooks/VoiceCompletion.hook.ts" } // Example: one of several Stop hooks
+ { "command": "{{LIFEOS_ROOT}}/hooks/VoiceCompletion.hook.ts" } // Example: one of several Stop hooks
]
}
]
@@ -1236,9 +1238,9 @@ Hooks in same event execute **sequentially** in order defined in settings.json:
## Related Documentation
-- **Voice System:** `~/.claude/`
+- **Voice System:** `{{LIFEOS_ROOT}}/`
- **Agent System:** `LIFEOS/DOCUMENTATION/Agents/AgentSystem.md`
-- **History/Memory:** `~/.claude/LIFEOS/DOCUMENTATION/Memory/MemorySystem.md`
+- **History/Memory:** `{{LIFEOS_DIR}}/DOCUMENTATION/Memory/MemorySystem.md`
---
@@ -1284,7 +1286,8 @@ PRE TOOL USE (4 distinct hooks + 2 Pulse HTTP routes):
PreToolGuard.hook.ts ONE blocking dispatcher: SystemFileGuard.check [via X] on
Write/Edit/MultiEdit; CommunicationSkillGuard.check [via X] +
EgressClassGuard.check [via X] on Bash. FIRST block wins (exit 2).
- [Bash|Write|Edit|MultiEdit]
+ All matched tools first reject unresolved {{LIFEOS_*}} paths.
+ [Bash|Write|Edit|MultiEdit|Read|Glob|Grep]
POST TOOL USE (7 distinct hooks):
AgentInvocation.hook.ts subagent_stop with duration [Agent]
@@ -1353,23 +1356,23 @@ RETIRED IN THE 2026-07-11 HOOKS-BPE PASS (deleted or folded):
SystemFileGuard+CommunicationSkillGuard+EgressClassGuard → PreToolGuard.
KEY FILES:
-~/.claude/settings.json Hook configuration (GENERATED by MergeSettings.ts — read, don't hand-edit)
-~/.claude/settings.system.json System-side hook/permission source (SYSTEM)
-~/.claude/LIFEOS/USER/CONFIG/settings.user.json User-side source (USER); its plain-array events REPLACE the system array
-~/.claude/LIFEOS/TOOLS/MergeSettings.ts Merges system + user → settings.json (runs async at SessionStart)
-~/.claude/hooks/ Hook scripts (39 files: 38 .hook.ts + ContextReduction.hook.sh; 30 .hook.ts registered)
-~/.claude/hooks/handlers/ Handler modules (7 files)
-~/.claude/hooks/lib/ Shared libraries (27 files)
-~/.claude/hooks/lib/learning-utils.ts Learning categorization
-~/.claude/hooks/lib/time.ts PST timestamp utilities
-~/.claude/LIFEOS/MEMORY/WORK/ Work tracking
-~/.claude/LIFEOS/MEMORY/LEARNING/ Learning captures
-~/.claude/LIFEOS/MEMORY/STATE/ Runtime state
-~/.claude/LIFEOS/MEMORY/STATE/events.jsonl Unified event log (append-only)
-~/.claude/LIFEOS/MEMORY/OBSERVABILITY/ Tool failures, agent spawns, config changes
+{{LIFEOS_ROOT}}/settings.json Hook configuration (GENERATED by MergeSettings.ts — read, don't hand-edit)
+{{LIFEOS_ROOT}}/settings.system.json System-side hook/permission source (SYSTEM)
+{{LIFEOS_DIR}}/USER/CONFIG/settings.user.json User-side source (USER); its plain-array events REPLACE the system array
+{{LIFEOS_DIR}}/TOOLS/MergeSettings.ts Merges system + user → settings.json (runs async at SessionStart)
+{{LIFEOS_ROOT}}/hooks/ Hook scripts (39 files: 38 .hook.ts + ContextReduction.hook.sh; 30 .hook.ts registered)
+{{LIFEOS_ROOT}}/hooks/handlers/ Handler modules (7 files)
+{{LIFEOS_ROOT}}/hooks/lib/ Shared libraries (27 files)
+{{LIFEOS_ROOT}}/hooks/lib/learning-utils.ts Learning categorization
+{{LIFEOS_ROOT}}/hooks/lib/time.ts PST timestamp utilities
+{{LIFEOS_DIR}}/MEMORY/WORK/ Work tracking
+{{LIFEOS_DIR}}/MEMORY/LEARNING/ Learning captures
+{{LIFEOS_DIR}}/MEMORY/STATE/ Runtime state
+{{LIFEOS_DIR}}/MEMORY/STATE/events.jsonl Unified event log (append-only)
+{{LIFEOS_DIR}}/MEMORY/OBSERVABILITY/ Tool failures, agent spawns, config changes
INFERENCE TOOL (for hooks needing AI):
-Path: ~/.claude/LIFEOS/TOOLS/Inference.ts
+Path: {{LIFEOS_DIR}}/TOOLS/Inference.ts
Import: import { inference } from '../../.claude/LIFEOS/TOOLS/Inference'
Levels: fast (haiku/15s) | standard (sonnet/30s) | smart (opus/90s)
@@ -1495,7 +1498,7 @@ Hooks call `appendEvent()` as a secondary write **alongside** their existing sta
```typescript
// Inside an existing hook, AFTER the normal state write:
-// appendEvent() writes to ~/.claude/LIFEOS/MEMORY/STATE/events.jsonl
+// appendEvent() writes to {{LIFEOS_DIR}}/MEMORY/STATE/events.jsonl
appendEvent({ type: 'work.created', source: 'ISASync', slug: 'my-task' });
```
@@ -1531,10 +1534,10 @@ Events use a dot-separated topic hierarchy for filtering. A `custom.*` escape ha
```bash
# Live tail (real-time monitoring)
-tail -f ~/.claude/LIFEOS/MEMORY/STATE/events.jsonl | jq
+tail -f "${LIFEOS_DIR}/MEMORY/STATE/events.jsonl" | jq
# Filter by type
-tail -f ~/.claude/LIFEOS/MEMORY/STATE/events.jsonl | jq 'select(.type | startswith("algorithm."))'
+tail -f "${LIFEOS_DIR}/MEMORY/STATE/events.jsonl" | jq 'select(.type | startswith("algorithm."))'
# Programmatic (Node/Bun fs.watch)
import { watch } from 'fs';
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Isa/IsaFormat.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Isa/IsaFormat.md
index f358bb4aba..fecf728ba3 100644
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Isa/IsaFormat.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Isa/IsaFormat.md
@@ -15,7 +15,7 @@ version: 1.5.19
The ISA is the single source of truth for the thing being articulated.
The AI writes all ISA content directly using Write/Edit tools and the ISA skill workflows. Hooks only read ISAs to sync state.
-**v2.7 (Algorithm v6.2.0+, still current at v6.3.0):** the body grew from six sections to **twelve in fixed order**: Problem, Vision, Out of Scope, Principles, Constraints, Goal, Criteria, Test Strategy, Features, Decisions, Changelog, Verification. Three-guardrail taxonomy locks the conceptual surface (Principles bind thinking, Constraints bind solution space, Out of Scope binds vision, Anti-criteria bind test surface). Tier Completeness Gate is HARD at every tier (E1 = Goal+Criteria; E5 = all twelve + active Interview). New ID-stability rule prevents ISC renumbering on edit. New `## Changelog` section uses Deutsch conjecture/refutation/learning format. Empty sections never appear. The ISA Skill at `~/.claude/skills/ISA/` owns the canonical template and six workflows; Algorithm at OBSERVE invokes `Skill("ISA", "scaffold from prompt at tier T")` at E2+. Full skeleton, tier gate table, three-guardrail table, and ID-stability rule live in the skill's SKILL.md and `Examples/canonical-isa.md`. The Append workflow gates the Changelog format and refuses partial entries (all four pieces — `conjectured`, `refuted by`, `learned`, `criterion now` — are required).
+**v2.7 (Algorithm v6.2.0+, still current at v6.3.0):** the body grew from six sections to **twelve in fixed order**: Problem, Vision, Out of Scope, Principles, Constraints, Goal, Criteria, Test Strategy, Features, Decisions, Changelog, Verification. Three-guardrail taxonomy locks the conceptual surface (Principles bind thinking, Constraints bind solution space, Out of Scope binds vision, Anti-criteria bind test surface). Tier Completeness Gate is HARD at every tier (E1 = Goal+Criteria; E5 = all twelve + active Interview). New ID-stability rule prevents ISC renumbering on edit. New `## Changelog` section uses Deutsch conjecture/refutation/learning format. Empty sections never appear. The ISA Skill at `{{LIFEOS_ROOT}}/skills/ISA/` owns the canonical template and six workflows; Algorithm at OBSERVE invokes `Skill("ISA", "scaffold from prompt at tier T")` at E2+. Full skeleton, tier gate table, three-guardrail table, and ID-stability rule live in the skill's SKILL.md and `Examples/canonical-isa.md`. The Append workflow gates the Changelog format and refuses partial entries (all four pieces — `conjectured`, `refuted by`, `learned`, `criterion now` — are required).
**v2.6 (Algorithm v6.0.0+):** Two ISA homes are now canonical:
- **Project ISAs** at `/ISA.md` — for any thing with persistent identity (applications, CLI tools, libraries, content pipelines, infrastructure, the Algorithm itself). Lives in the project's repo as system of record. Tasks operating on the project read/modify/extend this single file.
@@ -284,7 +284,7 @@ algorithm_config:
- `started`: Set once at creation. Never modified.
- `updated`: Set on every Edit/Write. Use current ISO 8601 timestamp.
- `iteration`: Omitted on first run. Set to `2` on first continuation, incremented thereafter.
-- `algorithm_config`: Omitted for interactive mode. Written during OBSERVE when mode is ideate, optimize, or loop. Contains resolved parameter values (after preset → focus → overrides resolution). The `params` section always contains the RESOLVED values, not raw user input. Full parameter schema: `~/.claude/LIFEOS/ALGORITHM/parameter-schema.md`.
+- `algorithm_config`: Omitted for interactive mode. Written during OBSERVE when mode is ideate, optimize, or loop. Contains resolved parameter values (after preset → focus → overrides resolution). The `params` section always contains the RESOLVED values, not raw user input. Full parameter schema: `{{LIFEOS_DIR}}/ALGORITHM/parameter-schema.md`.
## Body Sections (v2.7)
@@ -451,7 +451,7 @@ This rule **is** the operational form of hard-to-variability. An ISC that has a
- **v5.5.0+ Spec v2.5:** added `## Goal` section as the hard-to-vary prose spine (1–3 sentences, required when ideal state isn't trivially captured by `task` frontmatter); explicit living-document doctrine (ISA tightens through pursuit; refinements logged in `## Decisions` with `refined:` prefix); nested ISCs allowed natively for organizing complex ISAs (granularity rule applies at leaves); re-anchored the granularity rule as the operational form of hard-to-variability (testability ≡ hard-to-variability). Tier floors count atomic testable claims (leaves when nested). All v2.4 ISAs continue to parse — the four additions are additive. New ISAs from v2.5 onward should include `## Goal` when non-trivial.
- **Algorithm v6.0.0 / Spec v2.6:** Two ISA homes are canonical — Project ISAs at `/ISA.md` for things with persistent identity, Task ISAs at `MEMORY/WORK/{slug}/ISA.md` for ad-hoc work.
- **Algorithm v6.1.0:** Thinking-capability floor becomes HARD at every tier; tier ISC count floor becomes HARD on count at E4/E5; cannot be relaxed via "show your math".
-- **Algorithm v6.2.0 / Spec v2.7 (current shape):** body grew to **twelve sections in fixed order** — Problem, Vision, Out of Scope, Principles, Constraints, Goal, Criteria, Test Strategy, Features, Decisions, Changelog, Verification. Three-guardrail taxonomy locked. ID-stability rule (no re-numbering on edit). The **ISA Skill** at `~/.claude/skills/ISA/` owns canonical workflows (Scaffold, Interview, CheckCompleteness, Reconcile, Seed, Append). `## Changelog` uses Deutsch conjecture/refutation/learning format; partial entries refused.
+- **Algorithm v6.2.0 / Spec v2.7 (current shape):** body grew to **twelve sections in fixed order** — Problem, Vision, Out of Scope, Principles, Constraints, Goal, Criteria, Test Strategy, Features, Decisions, Changelog, Verification. Three-guardrail taxonomy locked. ID-stability rule (no re-numbering on edit). The **ISA Skill** at `{{LIFEOS_ROOT}}/skills/ISA/` owns canonical workflows (Scaffold, Interview, CheckCompleteness, Reconcile, Seed, Append). `## Changelog` uses Deutsch conjecture/refutation/learning format; partial entries refused.
- **Algorithm v6.3.0 (current):** thinking-capability vocabulary becomes a **closed enumeration** (verbatim list of 19 names). Phantom thinking-capability names are CRITICAL FAILURE. **Capability-Name Audit Gate** fires at OBSERVE→THINK boundary. The ISA file shape itself is unchanged from v2.7 — v6.3.0 is a doctrine-layer evolution, not a format-layer one.
### ## Experiments (optimize mode only)
@@ -514,7 +514,7 @@ Evidence for each criterion. Written during VERIFY phase.
## File Location
```
-~/.claude/LIFEOS/MEMORY/WORK/{slug}/ISA.md
+{{LIFEOS_DIR}}/MEMORY/WORK/{slug}/ISA.md
```
Directory created with `mkdir -p MEMORY/WORK/{slug}/` during OBSERVE. Legacy `PRD.md` files in this path are inert — the fallback was removed at Algorithm v4.2.0 and the system is now at v6.3.0.
@@ -553,7 +553,7 @@ Key design choices:
- **Checkboxes over EARS/BDD**: Simpler to parse, write, and verify. ISC pattern proven over 48+ ISAs.
- **YAML frontmatter over JSON**: Universal standard (Jekyll, Hugo, Astro, Kiro, spec-kit all use it).
- **Convention-based sections**: Sections appear when needed, not as empty boilerplate.
-- **Reference file pattern**: This spec lives at `~/.claude/LIFEOS/DOCUMENTATION/Isa/IsaFormat.md`, not inline in CLAUDE.md. Saves ~2,500 tokens/response.
+- **Reference file pattern**: This spec lives at `{{LIFEOS_DIR}}/DOCUMENTATION/Isa/IsaFormat.md`, not inline in CLAUDE.md. Saves ~2,500 tokens/response.
- **Universal primitive (v2.5)**: The same artifact structure serves software, science, art, philosophy, life decisions, business strategy. Hard-to-variability is the universal quality standard for the writing; testability is its operational form; the scientific method is verification; refinement through pursuit is the living document.
- **Bitter Pill discipline (v2.5)**: No structural fields a smarter model would render unnecessary. Probe fields, separate Ideal State / Current State sections, separate Variation Audit section, and tier-gated additions were all proposed and rejected during v2.5 design — clear ISC text + recurring ILA at phase boundaries do the same work without scaffolding.
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Isa/IsaHtmlMirror.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Isa/IsaHtmlMirror.md
index f85e2a5fa7..560f4a7dff 100644
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Isa/IsaHtmlMirror.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Isa/IsaHtmlMirror.md
@@ -11,7 +11,7 @@ The ISA is the canonical articulation of "done" for any Algorithm run. It lives
## Canonical Theme — UL Technical v1.0.0 (2026-05-13)
-**The UL Technical theme is the official HTML format for every ISA in the system going forward.** Tokyo-Night-Storm-derived dark navy ground (`--ul-bg-base: #0d1325`), JetBrains Mono throughout, lavender-white type, calm cyan/violet accents, status-pill ISC rows that read "OPEN / DONE / WIP / ANTI / ANTE" at a glance. Lives at `~/.claude/LIFEOS/TOOLS/ISARender/template.css`.
+**The UL Technical theme is the official HTML format for every ISA in the system going forward.** Tokyo-Night-Storm-derived dark navy ground (`--ul-bg-base: #0d1325`), JetBrains Mono throughout, lavender-white type, calm cyan/violet accents, status-pill ISC rows that read "OPEN / DONE / WIP / ANTI / ANTE" at a glance. Lives at `{{LIFEOS_DIR}}/TOOLS/ISARender/template.css`.
**Lineage.** The theme was specified by the Pulse-side parent design at `MEMORY/WORK/20260513-pulse-ul-theme-2-hackery-dark-blue/ISA.md` (token palette, naming convention, dimension preservation rules) and applied to the ISA mirror surface by `MEMORY/WORK/20260513-isa-html-mirror-redesign-dark-hackery/ISA.md` plus the status-clarity refinement at `MEMORY/WORK/20260513-isa-template-status-clarity-canonize-backfill/ISA.md`.
@@ -48,7 +48,7 @@ Three trigger primitives. None of them fire during in-flight authoring. The user
|---|---|---|
| **Initial completion** | `ISASync.hook.ts` (PostToolUse Edit/Write) | Frontmatter `phase:` transitions to the literal string `complete`. The render produces the sibling `ISA.html` for the first time. |
| **Post-completion update batch** | `ISARenderOnStop.hook.ts` (Stop event) | The assistant's turn ends, and at least one ISA was edited during the turn, AND that ISA has **reached completion at least once**. The fast signal is an existing `/ISA.html`; the cold-path signal (html missing) reads the frontmatter directly — `phase: complete`, `iteration > 1`, or a `resumed_from_phase` marker. Pre-completion edits on a brand-new iteration-1 ISA never fire. |
-| **Manual** | direct CLI / `/render-isa` slash command | The operator invokes `bun ~/.claude/LIFEOS/TOOLS/ISARender.ts ` from any shell. Bypasses the completion gate. Always works. |
+| **Manual** | direct CLI / `/render-isa` slash command | The operator invokes `bun "${LIFEOS_DIR}/TOOLS/ISARender.ts" ` from any shell. Bypasses the completion gate. Always works. |
The completion gate is the doctrinal centerpiece — "has this ISA ever been completed?", with `ISA.html` existence as the fast proxy and the frontmatter as the cold-path backstop. It means:
- During the FIRST authoring pass (iteration 1, never completed), the ISA churns; no render fires.
@@ -58,11 +58,11 @@ The completion gate is the doctrinal centerpiece — "has this ISA ever been com
## The engine
-`~/.claude/LIFEOS/TOOLS/ISARender.ts` — single Bun script, ~470 LOC, hand-rolled ISA-aware markdown parser. No external dependencies; no `marked`, no Handlebars, no Bun install side effects.
+`{{LIFEOS_DIR}}/TOOLS/ISARender.ts` — single Bun script, ~470 LOC, hand-rolled ISA-aware markdown parser. No external dependencies; no `marked`, no Handlebars, no Bun install side effects.
| Surface | Behavior |
|---|---|
-| Invocation | `bun ~/.claude/LIFEOS/TOOLS/ISARender.ts ` |
+| Invocation | `bun "${LIFEOS_DIR}/TOOLS/ISARender.ts" ` |
| Slug resolution | path → resolved directly; slug → looked up via `MEMORY/STATE/work.json`, or by scanning `MEMORY/WORK/` for a matching directory name |
| Output | `/ISA.html`, atomically written via `tmp + rename` |
| Cold render | ~25ms wall-clock on a typical E4 ISA |
@@ -114,7 +114,7 @@ Key behavior:
## File layout
```
-~/.claude/
+{{LIFEOS_ROOT}}/
├── LIFEOS/TOOLS/
│ ├── ISARender.ts # CLI entry point
│ └── ISARender/
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Isa/IsaSystem.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Isa/IsaSystem.md
index e0d48d8926..55ba7e9284 100644
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Isa/IsaSystem.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Isa/IsaSystem.md
@@ -11,7 +11,7 @@ The ISA — Ideal State Artifact — is the universal primitive that holds the a
The companion documents:
- **Format spec** — `LIFEOS/DOCUMENTATION/Isa/IsaFormat.md` — the file-shape contract: frontmatter fields, body section schemas, ID-stability rule, status markers.
-- **Skill** — `~/.claude/skills/ISA/SKILL.md` — the workflows that generate, refine, score, and merge ISAs (Scaffold, Interview, CheckCompleteness, Reconcile, Seed, Append).
+- **Skill** — `{{LIFEOS_ROOT}}/skills/ISA/SKILL.md` — the workflows that generate, refine, score, and merge ISAs (Scaffold, Interview, CheckCompleteness, Reconcile, Seed, Append).
- **Algorithm doctrine** — `LIFEOS/ALGORITHM/v8.4.0.md` (or LATEST) — the invocation cadence: when each workflow fires across the seven Algorithm phases.
If this document and the format spec disagree, the format spec wins and this document updates to match.
@@ -100,7 +100,7 @@ The ISC count also has a tier floor: at E2+ the natural granular decomposition m
## Six Workflows
-The ISA skill at `~/.claude/skills/ISA/` owns six workflows. They map verb-in-the-request to a deterministic procedure.
+The ISA skill at `{{LIFEOS_ROOT}}/skills/ISA/` owns six workflows. They map verb-in-the-request to a deterministic procedure.
| Workflow | Trigger Verbs | Purpose |
|----------|---------------|---------|
@@ -153,7 +153,7 @@ The ISA is the artifact every other LifeOS subsystem orbits.
- **Memory** (`LIFEOS/DOCUMENTATION/Memory/MemorySystem.md`) — task ISAs live under `MEMORY/WORK/{slug}/`. The Memory subsystem provides the directory structure and the WORK→LEARNING→KNOWLEDGE compaction lifecycle. Task ISAs are archived to KNOWLEDGE when their associated learnings have been harvested.
- **Skills** (`LIFEOS/DOCUMENTATION/Skills/SkillSystem.md`) — the ISA skill is one skill among many; it follows the same canonical form (TitleCase directory for public, `_ALLCAPS` for private; `Workflows/` + optional `Tools/` + optional `Examples/`; mandatory voice-notification block; mandatory Gotchas section).
- **Hooks** (`LIFEOS/DOCUMENTATION/Hooks/HookSystem.md`) — `ISASync.hook.ts` watches Edit/Write events on ISA frontmatter and syncs `phase` and `progress` to Pulse via `work.json`. `CheckpointPerISC.hook.ts` auto-commits per-ISC transitions. Hooks only read the ISA; the ISA is mutated by the AI directly via Edit/Write or via the ISA skill's workflows.
-- **CreateSkill** (`~/.claude/skills/CreateSkill/SKILL.md`) — the public-skill content rule that the ISA skill itself must obey. Public skills like ISA are stranger-safe; private skills like `` carry identity-bound content.
+- **CreateSkill** (`{{LIFEOS_ROOT}}/skills/CreateSkill/SKILL.md`) — the public-skill content rule that the ISA skill itself must obey. Public skills like ISA are stranger-safe; private skills like `` carry identity-bound content.
- **Pulse** (`LIFEOS/DOCUMENTATION/Pulse/PulseSystem.md`) — renders ISA `phase` and `progress` in real-time. The dashboard's phase widget reflects ISA frontmatter mutations as they happen.
The ISA is the gravitational center the rest of the system orbits — every task is a transition from current state to ideal state, and the ISA is what articulates the ideal state for that specific task or project.
@@ -163,8 +163,8 @@ The ISA is the gravitational center the rest of the system orbits — every task
## Cross-References
- **Format spec (file-shape contract):** `LIFEOS/DOCUMENTATION/Isa/IsaFormat.md`
-- **Skill (workflow implementations):** `~/.claude/skills/ISA/SKILL.md`
-- **Skill canonical example:** `~/.claude/skills/ISA/Examples/canonical-isa.md`
+- **Skill (workflow implementations):** `{{LIFEOS_ROOT}}/skills/ISA/SKILL.md`
+- **Skill canonical example:** `{{LIFEOS_ROOT}}/skills/ISA/Examples/canonical-isa.md`
- **Algorithm doctrine:** `LIFEOS/ALGORITHM/v8.4.0.md` (current); `LIFEOS/ALGORITHM/LATEST` for version pointer
- **Algorithm system doc:** `LIFEOS/DOCUMENTATION/Algorithm/AlgorithmSystem.md`
- **Memory system doc:** `LIFEOS/DOCUMENTATION/Memory/MemorySystem.md`
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/LifeOs/LifeOsSchema.md b/LifeOS/install/LIFEOS/DOCUMENTATION/LifeOs/LifeOsSchema.md
index 0834aaf31d..a32c5fe025 100644
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/LifeOs/LifeOsSchema.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/LifeOs/LifeOsSchema.md
@@ -241,7 +241,7 @@ USER/BoardGames.md # new taste file
- **`ShadowRelease.ts` never reads USER/.** It reads `skills/_LIFEOS/RELEASE_TEMPLATES/USER/` + this spec.
New LifeOS user experience:
-1. Install LifeOS the platform-agnostic, AI-native way: hand the installer (`skills/LifeOS/install/install.sh`, or the install doc) to your own AI/harness, which installs the `LifeOS/` skill and scaffolds `USER/` from the release templates (`skills/_LIFEOS/RELEASE_TEMPLATES/USER/`). No `git clone` into `~/.claude`.
+1. Install LifeOS the platform-agnostic, AI-native way: hand the installer (`skills/LifeOS/install/install.sh`, or the install doc) to your own AI/harness, which installs the `LifeOS/` skill and scaffolds `USER/` from the release templates (`skills/_LIFEOS/RELEASE_TEMPLATES/USER/`). No `git clone` into `{{LIFEOS_ROOT}}`.
2. Run `Interview` skill → conversation walks them through filling in files, phase by phase
3. Pulse dashboard lights up as files gain content
4. Daemon publishes nothing until the user sets `publish:` flags
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/LifeosSystemArchitecture.md b/LifeOS/install/LIFEOS/DOCUMENTATION/LifeosSystemArchitecture.md
index 18f206a907..fa3f42f03d 100755
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/LifeosSystemArchitecture.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/LifeosSystemArchitecture.md
@@ -107,7 +107,7 @@ The infrastructure serves the philosophy. If the philosophy is clear, the infras
## Directory Structure
```
-~/.claude/ # Claude Code native directory
+{{LIFEOS_ROOT}}/ # Claude Code native directory
CLAUDE.md # Operational instructions (directly edited)
settings.json # Runtime settings (directly edited)
LIFEOS_CONFIG.yaml # Credentials store for private skills (gitignored)
@@ -204,14 +204,14 @@ Explicit permission to say "I don't know" prevents hallucinations. Fabricating a
The LifeOS live tree and the public release are structurally identical modulo a symlink — system data and user data live in two physically separate trees, joined at runtime. The separation is enforced at three independent layers; each catches drift the others would miss.
**Physical layout (post-Phase-A→G migration, 2026-05-20→23):**
-- `~/.claude/` — public LifeOS SYSTEM tree (what ships on GitHub). Contains the symlink `LIFEOS/USER → ~/.config/LIFEOS/USER`.
+- `{{LIFEOS_ROOT}}/` — public LifeOS SYSTEM tree (what ships on GitHub). Contains the symlink `LIFEOS/USER → ~/.config/LIFEOS/USER`.
- `~/.config/LIFEOS/USER/` — private user-data git working tree (the user's own private USER-data repo). Mounted via symlink so Claude Code's `@`-import resolver reaches identity/TELOS/config files at session start.
- `~/.config/LIFEOS/USER/MEMORY/` — durable subset of memory (KNOWLEDGE, WORK//ISA.md, RELATIONSHIP, WISDOM, PLANS, RESEARCH, STATE/work.json, BOOKMARKS, REFERENCE, SKILLS, PROJECT, TEAMS, SYSTEMUPDATES, VERIFICATION) git-tracked in the user's private USER-data repo; ephemeral subset (OBSERVABILITY, STATE caches, LEARNING signals, SECURITY artifacts, VOICE event log, _BROWSER_STATE) gitignored locally.
**Four allowed access patterns** (any fifth pattern is a boundary violation):
1. `LifeosConfig.load()` typed loader (`LIFEOS/TOOLS/LifeosConfig.ts`) — primary channel for identity, voice, integrations, paths
2. Paths computed from `LifeosConfig.paths.userDir + relative` — never from literal `LIFEOS/USER/` strings in system code
-3. At-startup `@`-imports declared at the top of `~/.claude/CLAUDE.md` (CC does NOT follow transitive `@`-imports)
+3. At-startup `@`-imports declared at the top of `{{LIFEOS_ROOT}}/CLAUDE.md` (CC does NOT follow transitive `@`-imports)
4. HTTP/IPC via the Pulse server at `localhost:31337`
**Three enforcement layers:**
@@ -219,7 +219,7 @@ The LifeOS live tree and the public release are structurally identical modulo a
2. **PR-time** (Phase H, deferred) — GitHub Actions runs `DenyListCheck.ts` on every PR against the public repo.
3. **Release-time** — `skills/_LIFEOS/Tools/ShadowRelease.ts` runs 14 gates (G1–G14: zone deletion, identity grep, CF ID grep, trufflehog, .env strays, private tokens, ref integrity, private-skill refs, username-path leak, staging boot, dashboard leak, template-only USER/MEMORY, hidden-file leakage, critical-artifact presence). Backstop. Should consistently return zero findings if layers 1 and 2 are healthy.
-**Two-repo sync** — `~/.claude/.git/hooks/pre-push` auto-commits and pushes `~/.config/LIFEOS/USER/` to the user's private USER-data repo before each push from `~/.claude/`. The two repos stay in sync structurally. A private "kai update" / "push both repos" workflow wraps this with 4 boundary gates (USER-zone leak check, DenyListCheck, both-remotes-private confirmation, post-push HEAD verification).
+**Two-repo sync** — `{{LIFEOS_ROOT}}/.git/hooks/pre-push` auto-commits and pushes `~/.config/LIFEOS/USER/` to the user's private USER-data repo before each push from `{{LIFEOS_ROOT}}/`. The two repos stay in sync structurally. A private "kai update" / "push both repos" workflow wraps this with 4 boundary gates (USER-zone leak check, DenyListCheck, both-remotes-private confirmation, post-push HEAD verification).
Configuration files (`settings.json`, `CLAUDE.md`, `LIFEOS_SYSTEM_PROMPT.md`) are directly edited; `settings.json` is split into `settings.system.json` (public) + `settings.user.json` (USER) merged at SessionStart by `LIFEOS/TOOLS/MergeSettings.ts`. Identity values live in `settings.json` under `daidentity` and `principal` keys; private skills read credentials via `LifeosConfig.ts` from `LIFEOS/USER/CONFIG/LIFEOS_CONFIG.toml`. Full contract: `LIFEOS/DOCUMENTATION/SystemUserBoundary.md`.
@@ -237,7 +237,7 @@ Layer 1: SYSTEM PROMPT (highest authority, survives compaction)
verification requirement, hard prohibitions, operational rules, security protocol.
Layer 2: CLAUDE.MD (user context, loaded natively, survives compaction)
- File: ~/.claude/CLAUDE.md (directly edited)
+ File: {{LIFEOS_ROOT}}/CLAUDE.md (directly edited)
Contains: Routing table only -- @-imports, where each subsystem doc lives,
pointers into {{PRINCIPAL_NAME}}'s identity/voice/TELOS/work content. ~75 lines.
@@ -265,8 +265,8 @@ Layer 4: DYNAMIC CONTEXT (session-specific, ephemeral, does NOT survive compacti
| File | Purpose |
|------|---------|
| `LIFEOS/LIFEOS_SYSTEM_PROMPT.md` | Constitutional rules (system prompt layer) |
-| `~/.claude/CLAUDE.md` | Routing table — @-imports + subsystem pointers (directly edited) |
-| `~/.claude/settings.json` | Runtime settings (directly edited) |
+| `{{LIFEOS_ROOT}}/CLAUDE.md` | Routing table — @-imports + subsystem pointers (directly edited) |
+| `{{LIFEOS_ROOT}}/settings.json` | Runtime settings (directly edited) |
| `LIFEOS/TOOLS/lifeos.ts` | Launcher -- wires `--append-system-prompt-file` |
| `hooks/LoadContext.hook.ts` | Injects startup files + dynamic context |
@@ -319,10 +319,10 @@ Operation vocabulary, never conflated: **UpdateKaiRepo** = private sync + tag (v
**Composite skills are the organizational unit for all domain expertise.**
-Each skill lives in `~/.claude/skills//` with a mandatory `SKILL.md` defining triggers, workflows, and tools. Skills self-activate based on user intent via `USE WHEN` descriptions parsed by Claude Code. **Naming encodes the public/private boundary** — public skills use `TitleCase` (templated, safe, ships in LifeOS public release); private skills use `_ALLCAPS` with a leading underscore (anything personal, identity-bound, customer-bound, or environment-specific; excluded from release tooling via `skills/_*/**` in `hooks/lib/containment-zones.ts`). Within a skill, sub-files (workflows, references, tools) always use `TitleCase` regardless of the parent skill's form.
+Each skill lives in `{{LIFEOS_ROOT}}/skills//` with a mandatory `SKILL.md` defining triggers, workflows, and tools. Skills self-activate based on user intent via `USE WHEN` descriptions parsed by Claude Code. **Naming encodes the public/private boundary** — public skills use `TitleCase` (templated, safe, ships in LifeOS public release); private skills use `_ALLCAPS` with a leading underscore (anything personal, identity-bound, customer-bound, or environment-specific; excluded from release tooling via `skills/_*/**` in `hooks/lib/containment-zones.ts`). Within a skill, sub-files (workflows, references, tools) always use `TitleCase` regardless of the parent skill's form.
- **Status:** Active
-- **Location:** `~/.claude/skills/`
+- **Location:** `{{LIFEOS_ROOT}}/skills/`
- **Full doc:** `LIFEOS/DOCUMENTATION/Skills/SkillSystem.md`
### Hook System
@@ -332,7 +332,7 @@ Each skill lives in `~/.claude/skills//` with a mandatory `SKILL.md`
Hooks are executable scripts (TypeScript) that run automatically in response to Claude Code events: SessionStart, PreToolUse, PostToolUse, UserPromptSubmit, Stop, SessionEnd, PreCompact, PostCompact, PermissionRequest, and more. Most hooks run asynchronously and fail gracefully. Security is one consolidated hook: `Safety.hook.ts` dispatches by `hook_event_name` — PermissionRequest path auto-approves safe shapes via the shape classifier in `lib/safety-classifier.ts` (DANGEROUS_PATTERNS / CREDENTIAL_PATHS / INJECTION_SHAPES / READ_ONLY_COMMAND_PATTERNS / SEARCH_TOOLS / DEV_BINARIES / TRUSTED_PREFIXES + a shell-aware single-quote pre-pass that distinguishes data from execution); PostToolUse path on WebFetch/WebSearch annotates external content with the "treat as data" header plus an `[INJECTION SHAPE DETECTED: ...]` marker when the shared INJECTION_SHAPES catalog matches. All hooks emit structured events to `events.jsonl` for observability. Includes RTK (Rust Token Killer) integration via ContextReduction hook -- transparently rewrites Bash commands through `rtk` for 60-90% token savings on dev operations.
- **Status:** Active (LifeOS 5.0.0)
-- **Location:** `~/.claude/hooks/`
+- **Location:** `{{LIFEOS_ROOT}}/hooks/`
- **Configuration:** `settings.json` under `hooks` key
- **Full doc:** `LIFEOS/DOCUMENTATION/Hooks/HookSystem.md`
@@ -361,7 +361,7 @@ Two storage layers: LifeOS MEMORY (`LIFEOS/MEMORY/`) for structured, hook-driven
Task Tool Subagent Types are pre-built agents in Claude Code (Architect, Engineer, Explore, etc.) for internal workflow use. `BrowserAgent`, `UIReviewer`, `QATester`, `Artist`, `Algorithm`, and `Anvil` (Kimi K2.6) were removed 2026-06-10 by principal directive — browser validation runs through the **Interceptor** skill (real Chrome, no CDP fingerprint). Cross-vendor agents extend coverage: **Forge** (OpenAI-family GPT-5.6 Sol via `codex exec`) writes production-grade code at E3+ in **build mode** and runs the optional cross-vendor **audit mode** at E4/E5 in VERIFY when the Algorithm elects it (Algorithm Rule 2a — discretionary, not a mandatory gate; the former standalone Cato agent folded into Forge audit mode 2026-06-17). Named Agents are persistent identities with backstories and ElevenLabs voices for recurring work. Custom Agents are dynamic compositions via ComposeAgent from base traits. The word "custom" is the routing trigger -- when the user says "custom agents," invoke the Agents skill, never Task tool subagent types. Background agents are supervised by the Agent Watchdog (`Tools/AgentWatchdog.ts`) — a Monitor-tool script that detects hung agents via tool-activity.jsonl silence, auto-triggered by the Pulse agent-guard hook.
- **Status:** Active
-- **Location:** `~/.claude/agents/`
+- **Location:** `{{LIFEOS_ROOT}}/agents/`
- **Full doc:** `LIFEOS/DOCUMENTATION/Agents/AgentSystem.md`
### Delegation System
@@ -429,7 +429,7 @@ LifeOS is the OS. Pulse is the Dashboard. Everything a human (or the DA) can *se
**Implementation:** A single Bun process managed by launchd (`com.lifeos.pulse`), listening on port 31337. Pulse absorbed all previously separate daemon services into a module architecture: voice notifications (`modules/voice.ts`), observability server (`Observability/observability.ts`), Telegram bot (`modules/telegram.ts`), iMessage bot (`modules/imessage.ts`), and session hooks (`modules/hooks.ts`). Reads job definitions from PULSE.toml, evaluates cron schedules, executes due jobs (shell scripts or Claude CLI invocations), and routes output through existing notification channels. Circuit breaker pattern: 3 consecutive failures skip the job.
- **Version:** 2.0.0
-- **Location:** `~/.claude/LIFEOS/PULSE/`
+- **Location:** `{{LIFEOS_DIR}}/PULSE/`
- **Launchd:** `com.lifeos.pulse`
- **Port:** 31337
- **API:** ~40 endpoints across 8 categories (observability, algorithm, life, user-index, security, knowledge, wiki, DA, voice, hooks). Full reference: `LIFEOS/DOCUMENTATION/Observability/ObservabilitySystem.md` → "API Reference"
@@ -463,7 +463,7 @@ Walks `USER/` (root + one level), parses frontmatter + body of each `.md`, compu
Formalizes how Pulse instantiates, manages, and evolves a Digital Assistant. Replaces manual DA_IDENTITY.md editing with a structured YAML schema, adds proactive heartbeat evaluation (2-layer: free context gathering + cheap Haiku eval), natural-language scheduled tasks, and bounded identity growth over time. Supports multiple DAs via a registry with primary/worker roles.
- **Status:** Active
-- **Location:** `~/.claude/LIFEOS/USER/DA/` (identity data), `~/.claude/` (runtime)
+- **Location:** `{{LIFEOS_DIR}}/USER/DA/` (identity data), `{{LIFEOS_ROOT}}/` (runtime)
- **Full doc:** `LIFEOS/DOCUMENTATION/Pulse/DaSubsystem.md`, `LIFEOS/DOCUMENTATION/Pulse/PulseSystem.md` (DA Module section)
### Browser Automation
@@ -473,7 +473,7 @@ Formalizes how Pulse instantiates, manages, and evolves a Digital Assistant. Rep
Handles screenshots, multi-step sessions, authenticated browsing, and DOM/console/network inspection through the actual browser UI, so it stays logged in and passes bot detection. Legacy built-in agents (BrowserAgent, UIReviewer, QATester) were removed 2026-06-10, and the headless agent-browser wrapper (the Browser skill) was retired 2026-07-04 in favor of Interceptor. All web-based output MUST be verified through the **Interceptor skill** before showing to the user. Playwright is banned across LifeOS.
- **Status:** Active
-- **Location:** `~/.claude/skills/Interceptor/` (real-Chrome automation + computer use, mandatory for verification)
+- **Location:** `{{LIFEOS_ROOT}}/skills/Interceptor/` (real-Chrome automation + computer use, mandatory for verification)
### Cloud Execution (Arbol)
@@ -503,7 +503,7 @@ Monitors content sources, processes everything through an AI pipeline (ingest, s
LifeOS executes Fabric patterns natively by reading `Patterns/{name}/system.md` and applying instructions directly. Use `fabric` CLI only for YouTube transcript extraction (`-y URL`). Patterns cover summarization, wisdom extraction, threat modeling, and dozens of other content operations.
- **Status:** Active (240+ patterns)
-- **Location:** `~/.claude/skills/Fabric/`
+- **Location:** `{{LIFEOS_ROOT}}/skills/Fabric/`
- **Full doc:** `LIFEOS/DOCUMENTATION/Fabric/FabricSystem.md`
### Terminal Tab System
@@ -553,8 +553,8 @@ Five states with distinct colors: Inference (purple), Working (orange), Complete
```
PRIVATE (never make public):
- ~/.claude/ -- hooks, skills, settings, agents, CLAUDE.md
- ~/.claude/LIFEOS/ -- Algorithm, Components, Tools, MEMORY, Pulse (unified daemon)
+ {{LIFEOS_ROOT}}/ -- hooks, skills, settings, agents, CLAUDE.md
+ {{LIFEOS_DIR}}/ -- Algorithm, Components, Tools, MEMORY, Pulse (unified daemon)
PUBLIC (sanitized):
~/Projects/LIFEOS/ -- Sanitized examples, generic templates, community sharing
@@ -605,7 +605,7 @@ LifeOS manages its own integrity, security, and documentation through the System
| Function | Description |
|----------|-------------|
-| **Integrity Audits** | 16 parallel agents verify broken references across ~/.claude |
+| **Integrity Audits** | 16 parallel agents verify broken references across {{LIFEOS_ROOT}} |
| **Secret Scanning** | TruffleHog credential detection in any directory |
| **Privacy Validation** | Ensures USER/WORK content isolation from regular skills |
| **Cross-Repo Validation** | Verifies private/public repository separation |
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Memory/CurationCoverage.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Memory/CurationCoverage.md
index 9789f7e3f6..9cca951e34 100644
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Memory/CurationCoverage.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Memory/CurationCoverage.md
@@ -24,7 +24,7 @@ Defined in `LIFEOS/TOOLS/MutationTier.ts` as a closed allowlist (default-deny):
| **C** | Propose-only (Telegram approval, or auto-apply at confidence ≥ 0.70) | `PRINCIPAL_IDENTITY.md`, `DA_IDENTITY.md`, `WRITINGSTYLE.md`, `DEFINITIONS.md`, `CANONICAL_CONTENT.md`, `RESUME.md`, `OPERATIONAL_RULES.md` |
| **D** | Untouchable by memory subsystem | Everything else |
-A new file added to `~/.claude/` is Tier D by default until a code change promotes it.
+A new file added to `{{LIFEOS_ROOT}}/` is Tier D by default until a code change promotes it.
## Coverage matrix
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Memory/MemorySystem.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Memory/MemorySystem.md
index 246da2bd74..e871b32fad 100755
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Memory/MemorySystem.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Memory/MemorySystem.md
@@ -11,12 +11,12 @@ version: 1.5.19
This is not a narrow event log or a preferences store. This is LifeOS's comprehensive knowledge system — the full shared memory between the principal and the DA. If we built knowledge together, it belongs here. That includes: work tracking, learnings from failures and successes, research and OSINT investigations, contact dossiers, security events, runtime state, voice events, observability metrics, and any other knowledge that would be valuable in future conversations.
**One storage layer:**
-- **LifeOS MEMORY** (`~/.claude/LIFEOS/MEMORY/`) — structured, hook-driven, entity-based
+- **LifeOS MEMORY** (`{{LIFEOS_DIR}}/MEMORY/`) — structured, hook-driven, entity-based
-LifeOS MEMORY is the system of record. Claude Code's built-in auto-memory (`~/.claude/projects//memory/`) is disabled by design (`autoMemoryEnabled: false` in shipped settings, plus deny rules on that path) — see "Claude Code Auto-Memory & Auto-Dream" below.
+LifeOS MEMORY is the system of record. Claude Code's built-in auto-memory (`{{LIFEOS_ROOT}}/projects//memory/`) is disabled by design (`autoMemoryEnabled: false` in shipped settings, plus deny rules on that path) — see "Claude Code Auto-Memory & Auto-Dream" below.
**Version:** 8.2.0 (Proposal Subtypes + Session Rename CLI, 2026-05-25; preserves 8.1 inventory + drift + autonomic loop + health gate)
-**Location:** `~/.claude/LIFEOS/MEMORY/`
+**Location:** `{{LIFEOS_DIR}}/MEMORY/`
---
@@ -109,7 +109,7 @@ Confidence guidance:
### Curation coverage at a glance
-The reviewer + four-tier classifier covers a defined slice of `~/.claude/` content. The canonical coverage matrix (which files the system curates, which it ignores, which other autonomic pipelines own) lives in [`CurationCoverage.md`](./CurationCoverage.md).
+The reviewer + four-tier classifier covers a defined slice of `{{LIFEOS_ROOT}}/` content. The canonical coverage matrix (which files the system curates, which it ignores, which other autonomic pipelines own) lives in [`CurationCoverage.md`](./CurationCoverage.md).
### Four mutation tiers
@@ -162,9 +162,9 @@ When all three hold, the next Stop hook spawns the reviewer subprocess. New User
### Reading status
-```
-bun ~/.claude/LIFEOS/TOOLS/MemoryStatus.ts # human-formatted block
-bun ~/.claude/LIFEOS/TOOLS/MemoryStatus.ts --json # machine-readable
+```bash
+bun "${LIFEOS_DIR}/TOOLS/MemoryStatus.ts" # human-formatted block
+bun "${LIFEOS_DIR}/TOOLS/MemoryStatus.ts" --json # machine-readable
```
Reports hot-layer file utilization, corpus sizes, pending-proposals depth, reviewer state, last reviewer run, last retrieval.
@@ -259,7 +259,7 @@ Verdict: fresh-with-misses
## Directory Inventory (authoritative)
-This is the canonical list of every directory under `~/.claude/LIFEOS/MEMORY/`. The `MemoryDirIntegrity.ts` drift handler (called by `DocIntegrity.hook.ts` on Stop) parses this table and warns whenever the on-disk tree contains a directory not listed here, or this table lists a directory that no longer exists. Add new memory subsystems by adding a row to this table FIRST, then creating the directory.
+This is the canonical list of every directory under `{{LIFEOS_DIR}}/MEMORY/`. The `MemoryDirIntegrity.ts` drift handler (called by `DocIntegrity.hook.ts` on Stop) parses this table and warns whenever the on-disk tree contains a directory not listed here, or this table lists a directory that no longer exists. Add new memory subsystems by adding a row to this table FIRST, then creating the directory.
| Directory | Class | Status | Purpose | Primary writers |
|-----------|-------|--------|---------|-----------------|
@@ -315,7 +315,7 @@ These directories are listed in the Directory Inventory so the drift hook recogn
### Claude Code projects/ — Native Session Storage
-**Location:** `~/.claude/projects/-Users-{username}--claude/`
+**Location:** `{{LIFEOS_ROOT}}/projects/-Users-{username}--claude/`
*(Replace `{username}` with your system username, e.g., `-Users-john--claude`)*
**What populates it:** Claude Code automatically (every conversation)
**Content:** Complete session transcripts in JSONL format
@@ -494,7 +494,7 @@ This is mutable state that changes during execution — not historical records.
**`events.jsonl` — Unified Event Log:**
-An append-only JSONL file where hooks emit structured, typed events alongside their normal state writes. Each line is a JSON object with `timestamp`, `session_id`, `source`, `type`, and type-specific fields. The type field uses a dot-separated topic hierarchy (e.g., `algorithm.phase`, `work.created`, `rating.captured`, `voice.sent`). This file is an observability layer — it does NOT replace any of the mutable state files listed above. Events are written by `${LIFEOS_DIR}/hooks/lib/observability-transport.ts` using synchronous append, and errors are silently swallowed so the event log never disrupts hook execution. Consumers can tail or `fs.watch` this file for real-time visibility into LifeOS activity.
+An append-only JSONL file where hooks emit structured, typed events alongside their normal state writes. Each line is a JSON object with `timestamp`, `session_id`, `source`, `type`, and type-specific fields. The type field uses a dot-separated topic hierarchy (e.g., `algorithm.phase`, `work.created`, `rating.captured`, `voice.sent`). This file is an observability layer — it does NOT replace any of the mutable state files listed above. Events are written by `{{LIFEOS_DIR}}/hooks/lib/observability-transport.ts` using synchronous append, and errors are silently swallowed so the event log never disrupts hook execution. Consumers can tail or `fs.watch` this file for real-time visibility into LifeOS activity.
### OBSERVABILITY/ — Structured Telemetry
@@ -601,7 +601,7 @@ An append-only JSONL file where hooks emit structured, typed events alongside th
### AUTO/ — Reserved (legacy auto-memory location)
-**Status:** Reserved. The auto-memory role moved to `~/.claude/projects//memory/` in v7.4 (2026-03-31). This directory is retained as a stub with its README so the public release taxonomy stays stable; nothing currently writes to it. Content that would historically have landed here now lands in CC native memory.
+**Status:** Reserved. The auto-memory role moved to `{{LIFEOS_ROOT}}/projects//memory/` in v7.4 (2026-03-31). This directory is retained as a stub with its README so the public release taxonomy stays stable; nothing currently writes to it. Content that would historically have landed here now lands in CC native memory.
### RAW/ — Reserved (legacy firehose location)
@@ -664,7 +664,7 @@ An append-only JSONL file where hooks emit structured, typed events alongside th
The `MemoryDirIntegrity.ts` handler (run from `DocIntegrity.hook.ts` on Stop) keeps the Directory Inventory table above honest. On every Stop where any system file changed, it:
-1. Lists every directory under `~/.claude/LIFEOS/MEMORY/` (one level deep, excluding `.git`, `.DS_Store`, etc.)
+1. Lists every directory under `{{LIFEOS_DIR}}/MEMORY/` (one level deep, excluding `.git`, `.DS_Store`, etc.)
2. Parses the Directory Inventory table in this file
3. Reports any directory on disk not in the table (**unknown subsystem**)
4. Reports any directory in the table not on disk (**missing subsystem** — only flagged for `active` rows; `reserved` rows are allowed to be empty or absent)
@@ -707,114 +707,114 @@ WisdomCrossFrameSynthesizer → analyzes FRAMES/ → writes PRINCIPLES/
### Check current work
```bash
# All sessions (active + resumable), keyed by slug:
-jq '.sessions | to_entries | map({slug: .key, phase: .value.phase, updatedAt: .value.updatedAt}) | sort_by(.updatedAt) | reverse' ~/.claude/LIFEOS/MEMORY/STATE/work.json
+jq '.sessions | to_entries | map({slug: .key, phase: .value.phase, updatedAt: .value.updatedAt}) | sort_by(.updatedAt) | reverse' "${LIFEOS_DIR}/MEMORY/STATE/work.json"
# Just the non-complete ones:
-jq '.sessions | to_entries | map(select(.value.phase != "complete")) | from_entries' ~/.claude/LIFEOS/MEMORY/STATE/work.json
+jq '.sessions | to_entries | map(select(.value.phase != "complete")) | from_entries' "${LIFEOS_DIR}/MEMORY/STATE/work.json"
-ls ~/.claude/LIFEOS/MEMORY/WORK/ | tail -5
+ls "${LIFEOS_DIR}/MEMORY/WORK/" | tail -5
```
### Check ratings
```bash
-tail ~/.claude/LIFEOS/MEMORY/LEARNING/SIGNALS/ratings.jsonl
+tail "${LIFEOS_DIR}/MEMORY/LEARNING/SIGNALS/ratings.jsonl"
```
### View session transcripts
```bash
# List recent sessions (newest first)
# Replace {username} with your system username
-ls -lt ~/.claude/projects/-Users-{username}--claude/*.jsonl | head -5
+ls -lt "${LIFEOS_ROOT}/projects/-Users-"{username}--claude/*.jsonl | head -5
# View last session events
-tail ~/.claude/projects/-Users-{username}--claude/$(ls -t ~/.claude/projects/-Users-{username}--claude/*.jsonl | head -1) | jq .
+tail "${LIFEOS_ROOT}/projects/-Users-"{username}--claude/$(ls -t "${LIFEOS_ROOT}/projects/-Users-"{username}--claude/*.jsonl | head -1) | jq .
```
### Check learnings
```bash
-ls ~/.claude/LIFEOS/MEMORY/LEARNING/SYSTEM/
-ls ~/.claude/LIFEOS/MEMORY/LEARNING/ALGORITHM/
-ls ~/.claude/LIFEOS/MEMORY/LEARNING/SYNTHESIS/
+ls "${LIFEOS_DIR}/MEMORY/LEARNING/SYSTEM/"
+ls "${LIFEOS_DIR}/MEMORY/LEARNING/ALGORITHM/"
+ls "${LIFEOS_DIR}/MEMORY/LEARNING/SYNTHESIS/"
```
### Check failures
```bash
# List recent failure captures
-ls -lt ~/.claude/LIFEOS/MEMORY/LEARNING/FAILURES/$(date +%Y-%m)/ 2>/dev/null | head -10
+ls -lt "${LIFEOS_DIR}/MEMORY/LEARNING/FAILURES/$(date +%Y-%m)/" 2>/dev/null | head -10
# View a specific failure
-cat ~/.claude/LIFEOS/MEMORY/LEARNING/FAILURES/2026-01/*/CONTEXT.md | head -100
+cat "${LIFEOS_DIR}/MEMORY/LEARNING/FAILURES/2026-01/"*/CONTEXT.md | head -100
# Migrate historical low ratings to FAILURES
-bun run ~/.claude/LIFEOS/TOOLS/FailureCapture.ts --migrate
+bun run "${LIFEOS_DIR}/TOOLS/FailureCapture.ts" --migrate
```
### Check observability
```bash
# Recent tool failures
-tail ~/.claude/LIFEOS/MEMORY/OBSERVABILITY/tool-failures.jsonl | jq .
+tail "${LIFEOS_DIR}/MEMORY/OBSERVABILITY/tool-failures.jsonl" | jq .
# Anthropic spend ledger
-tail ~/.claude/LIFEOS/MEMORY/OBSERVABILITY/anthropic-cost.jsonl | jq .
+tail "${LIFEOS_DIR}/MEMORY/OBSERVABILITY/anthropic-cost.jsonl" | jq .
# Config edits this session
-tail ~/.claude/LIFEOS/MEMORY/OBSERVABILITY/config-changes.jsonl | jq .
+tail "${LIFEOS_DIR}/MEMORY/OBSERVABILITY/config-changes.jsonl" | jq .
```
### Check relationship notes
```bash
# Today's note
-cat ~/.claude/LIFEOS/MEMORY/RELATIONSHIP/$(date +%Y-%m)/$(date +%Y-%m-%d).md 2>/dev/null
+cat "${LIFEOS_DIR}/MEMORY/RELATIONSHIP/$(date +%Y-%m)/$(date +%Y-%m-%d).md" 2>/dev/null
# Generate a reflection
-bun run ~/.claude/LIFEOS/TOOLS/RelationshipReflect.ts
+bun run "${LIFEOS_DIR}/TOOLS/RelationshipReflect.ts"
```
### Check multi-session progress
```bash
-ls ~/.claude/LIFEOS/MEMORY/STATE/progress/
+ls "${LIFEOS_DIR}/MEMORY/STATE/progress/"
```
### Run harvesting tools
```bash
# Harvest learnings from recent sessions
-bun run ~/.claude/LIFEOS/TOOLS/SessionHarvester.ts --recent 10
+bun run "${LIFEOS_DIR}/TOOLS/SessionHarvester.ts" --recent 10
# Mine conversations for decisions, preferences, milestones, problems
-bun run ~/.claude/LIFEOS/TOOLS/SessionHarvester.ts --mine --recent 10
+bun run "${LIFEOS_DIR}/TOOLS/SessionHarvester.ts" --mine --recent 10
# Generate pattern synthesis
-bun run ~/.claude/LIFEOS/TOOLS/LearningPatternSynthesis.ts --week
+bun run "${LIFEOS_DIR}/TOOLS/LearningPatternSynthesis.ts" --week
```
### Retrieve knowledge (compressed context)
```bash
# Search knowledge archive with BM25 ranking
-bun run ~/.claude/LIFEOS/TOOLS/MemoryRetriever.ts "query terms"
+bun run "${LIFEOS_DIR}/TOOLS/MemoryRetriever.ts" "query terms"
# Raw excerpts without LLM compression
-bun run ~/.claude/LIFEOS/TOOLS/MemoryRetriever.ts "query terms" --raw --top 5
+bun run "${LIFEOS_DIR}/TOOLS/MemoryRetriever.ts" "query terms" --raw --top 5
```
### Navigate knowledge graph
```bash
# Graph stats: nodes, edges, clusters
-bun run ~/.claude/LIFEOS/TOOLS/KnowledgeGraph.ts stats
+bun run "${LIFEOS_DIR}/TOOLS/KnowledgeGraph.ts" stats
# BFS traversal from a note
-bun run ~/.claude/LIFEOS/TOOLS/KnowledgeGraph.ts traverse --hops 2
+bun run "${LIFEOS_DIR}/TOOLS/KnowledgeGraph.ts" traverse --hops 2
# Directly connected notes
-bun run ~/.claude/LIFEOS/TOOLS/KnowledgeGraph.ts related
+bun run "${LIFEOS_DIR}/TOOLS/KnowledgeGraph.ts" related
# Find notes by tag
-bun run ~/.claude/LIFEOS/TOOLS/KnowledgeGraph.ts find
+bun run "${LIFEOS_DIR}/TOOLS/KnowledgeGraph.ts" find
```
### Check the inventory drift hook
```bash
# Run the drift handler ad-hoc against current MEMORY/
-bun run ~/.claude/hooks/handlers/MemoryDirIntegrity.ts
+bun run "${LIFEOS_ROOT}/hooks/handlers/MemoryDirIntegrity.ts"
```
---
@@ -865,13 +865,13 @@ bun run ~/.claude/hooks/handlers/MemoryDirIntegrity.ts
**2026-03-31:** v7.4 — Eliminated LIFEOS/MEMORY/AUTO active role
- Removed `autoMemoryDirectory` setting from settings.json (was still redirecting to LIFEOS/MEMORY/AUTO/)
-- Migrated 2 unique feedback items to CC's native memory at `~/.claude/projects/-Users-{YourName}--claude/memory/`
+- Migrated 2 unique feedback items to CC's native memory at `{{LIFEOS_ROOT}}/projects/-Users-{YourName}--claude/memory/`
- AUTO/ retained as a stub directory with README for taxonomy stability — see Directory Inventory above (status: reserved)
- Updated LifeOS Upgrade redistribution workflow to scan CC native memory path
- Updated statusline token estimation to use CC native memory path
**2026-03-24:** v7.3 — Auto-Memory & PreCompact Integration
-- Enabled Claude Code's built-in auto-memory using default path (`~/.claude/projects//memory/`)
+- Enabled Claude Code's built-in auto-memory using default path (`{{LIFEOS_ROOT}}/projects//memory/`)
- Replaced blocking MEMORY.md ("Do Not Store Memories Here") with clean index
- Created `PreCompact.hook.ts` — captures work state before conversation compaction
- Added PreCompact hook to settings.json (matcher: `"*"` for auto + manual)
@@ -924,13 +924,13 @@ bun run ~/.claude/hooks/handlers/MemoryDirIntegrity.ts
- Consolidated WORKSYSTEM.md into MEMORYSYSTEM.md
**2026-01-09:** v4.0 — Major restructure
-- Moved BACKUPS to `~/.claude/BACKUPS/` (outside MEMORY)
+- Moved BACKUPS to `{{LIFEOS_ROOT}}/BACKUPS/` (outside MEMORY)
- Renamed RAW-OUTPUTS to RAW
- All directories now ALL CAPS
**2026-01-05:** v1.0 — Unified Memory System migration
-- Previous: `~/.claude/history/`, `~/.claude/context/`, `~/.claude/progress/`
-- Current: `~/.claude/LIFEOS/MEMORY/`
+- Previous: `{{LIFEOS_ROOT}}/history/`, `{{LIFEOS_ROOT}}/context/`, `{{LIFEOS_ROOT}}/progress/`
+- Current: `{{LIFEOS_DIR}}/MEMORY/`
- Files migrated: 8,415+
---
@@ -940,11 +940,11 @@ bun run ~/.claude/hooks/handlers/MemoryDirIntegrity.ts
Harness auto-memory is intentionally disabled. LifeOS's own memory surfaces are the system of record.
### Auto-Memory (disabled by design)
-Claude Code ships a built-in auto-memory feature: `~/.claude/projects//memory/` with a `MEMORY.md` index and `feedback_*.md` files. LifeOS turns it off.
+Claude Code ships a built-in auto-memory feature: `{{LIFEOS_ROOT}}/projects//memory/` with a `MEMORY.md` index and `feedback_*.md` files. LifeOS turns it off.
**Configuration (shipped):**
- `autoMemoryEnabled: false` in `settings.json` (mirrored in the release template `settings.public.json`)
-- `permissions.deny` blocks `Write` and `Edit` on `~/.claude/projects/**/memory/**`
+- `permissions.deny` blocks `Write` and `Edit` on `{{LIFEOS_ROOT}}/projects/**/memory/**`
**Why:** since the v5.4.0 Unified Learning Router, all learning routes through LifeOS surfaces instead. Rules, preferences, and operational behavior get encoded where they're structurally enforced (CLAUDE.md, hooks, settings.json, skills); reusable knowledge goes to `MEMORY/KNOWLEDGE/`; task state to `MEMORY/WORK/`; corrections and ratings to `MEMORY/LEARNING/`. A feedback memo in harness memory treats the symptom (the AI didn't remember) instead of the cause (the rule wasn't encoded where it lives) — see the system prompt's "Override of harness auto-memory" rule.
@@ -980,7 +980,7 @@ LifeOS doesn't control auto-dream activation. With auto-memory disabled there is
- **Hook System:** `../Hooks/HookSystem.md`
- **Architecture:** `../LifeosSystemArchitecture.md`
- **ISA format:** `../IsaFormat.md`
-- **Drift handler source:** `~/.claude/hooks/handlers/MemoryDirIntegrity.ts`
+- **Drift handler source:** `{{LIFEOS_ROOT}}/hooks/handlers/MemoryDirIntegrity.ts`
### Retrieval absence statements + tag-match semantics (2026-06-10)
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Notifications/NotificationSystem.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Notifications/NotificationSystem.md
index 19b722380a..72bb77aaac 100755
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Notifications/NotificationSystem.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Notifications/NotificationSystem.md
@@ -8,7 +8,7 @@ version: 1.2.14
**Voice notifications for LifeOS workflows and task execution.**
-> **Infrastructure:** The voice notification endpoint (`http://localhost:31337/notify`) is served by the unified Pulse daemon (`~/.claude/LIFEOS/PULSE/`). Voice is implemented at `~/.claude/LIFEOS/PULSE/VoiceServer/voice.ts` and routed through Pulse -- there is no separate VoiceServer process. One daemon, one port, one launchd plist (`com.lifeos.pulse`).
+> **Infrastructure:** The voice notification endpoint (`http://localhost:31337/notify`) is served by the unified Pulse daemon (`{{LIFEOS_DIR}}/PULSE/`). Voice is implemented at `{{LIFEOS_DIR}}/PULSE/VoiceServer/voice.ts` and routed through Pulse -- there is no separate VoiceServer process. One daemon, one port, one launchd plist (`com.lifeos.pulse`).
> **Pronunciation normalization:** Before any text reaches ElevenLabs it passes through two transforms — `applyPronunciations()` (literal term map from `LIFEOS/USER/PRINCIPAL/PRONUNCIATIONS.json`) wrapped around `disambiguateHomographs()` (`LIFEOS/PULSE/lib/homographs.ts`). The homograph stage exists because ElevenLabs guesses a reading from context and gets some words wrong; the worst offender is "live", where the broadcast/adjective sense (/laɪv/ — "the site is live", "live-verified") otherwise reads as the verb (/lɪv/ — "where you live"). It respells **only** context-matched broadcast occurrences to `lyve`, never a flat substitution, so verb uses ("live freely") stay correct. Adding a new spoken phrasing that reads wrong means adding a context regex, not a global replace. Shared by both the VoiceServer (`voice.ts`) and the Telegram voice path (`modules/telegram.ts`) so every spoken channel reads identically.
@@ -120,7 +120,7 @@ curl -s -X POST http://localhost:31337/notify \
| **{DA_IDENTITY.NAME}** (default) | `{DA_IDENTITY.VOICEID}` | Use for most workflows |
| **Priya** (Artist) | `21m00Tcm4TlvDq8ikWAM` | Art skill workflows |
-**Full voice registry:** `LIFEOS/DOCUMENTATION/Agents/AgentSystem.md` (see Named Agents) and `~/.claude/settings.json` (daidentity.voices.main.voiceId)
+**Full voice registry:** `LIFEOS/DOCUMENTATION/Agents/AgentSystem.md` (see Named Agents) and `{{LIFEOS_ROOT}}/settings.json` (daidentity.voices.main.voiceId)
---
@@ -218,7 +218,7 @@ Notifications are automatically routed based on event type:
### Configuration
-Located in `~/.claude/settings.json`:
+Located in `{{LIFEOS_ROOT}}/settings.json`:
```json
{
@@ -282,7 +282,7 @@ Topic name acts as password - use random string for security.
### Implementation
-The notification service is in `~/.claude/hooks/lib/notifications.ts`:
+The notification service is in `{{LIFEOS_ROOT}}/hooks/lib/notifications.ts`:
```typescript
import { notify, notifyTaskComplete, notifyBackgroundAgent, notifyError } from './lib/notifications';
@@ -306,7 +306,7 @@ await sendDiscord("Message", { title: "Title", color: 0x00ff00 });
## Event Log Channel (events.jsonl)
-Events are emitted directly from each hook via `fs.appendFileSync` to `~/.claude/LIFEOS/MEMORY/OBSERVABILITY/*.jsonl` — synchronous, fire-and-forget, no shared transport library. This channel is additive — it does not replace any of the notification channels above, and hooks emit events alongside their existing state writes and notifications.
+Events are emitted directly from each hook via `fs.appendFileSync` to `{{LIFEOS_DIR}}/MEMORY/OBSERVABILITY/*.jsonl` — synchronous, fire-and-forget, no shared transport library. This channel is additive — it does not replace any of the notification channels above, and hooks emit events alongside their existing state writes and notifications.
---
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Observability/ObservabilitySystem.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Observability/ObservabilitySystem.md
index 43596f9fac..4724a99d12 100644
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Observability/ObservabilitySystem.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Observability/ObservabilitySystem.md
@@ -8,7 +8,7 @@ version: 1.5.7
Single-source local event pipeline for LifeOS tool activity, voice events, subagent lifecycle, and tool failures. Pulse is the only consumer; it reads JSONL from local disk on demand.
-> **Infrastructure:** The observability HTTP server (`localhost:31337`) runs as a module inside the unified Pulse daemon (`~/.claude/LIFEOS/PULSE/Observability/observability.ts`). There is no separate observability server process -- Pulse serves all local HTTP endpoints on port 31337.
+> **Infrastructure:** The observability HTTP server (`localhost:31337`) runs as a module inside the unified Pulse daemon (`{{LIFEOS_DIR}}/PULSE/Observability/observability.ts`). There is no separate observability server process -- Pulse serves all local HTTP endpoints on port 31337.
## Architecture
@@ -78,9 +78,9 @@ Pulse reads on demand. The Observatory dashboard polls `/api/events/recent` ever
| File | Role |
|------|------|
-| `~/.claude/hooks/EventLogger.hook.ts` | Consolidated event writer (absorbed ToolActivityTracker + ToolFailureTracker + SkillExecutionLog + ConfigAudit + StopFailureHandler 2026-07-11). PostToolUse catch-all → tool-activity.jsonl (+ SKILLS/execution.jsonl on Skill); PostToolUseFailure → tool-failures.jsonl; ConfigChange → config-changes.jsonl; StopFailure → SECURITY stop-failures (log-only) |
-| `~/.claude/LIFEOS/PULSE/Observability/observability.ts` | Observability module inside unified Pulse daemon — serves events from JSONL at :31337 |
-| `~/.claude/LIFEOS/PULSE/Observability/` | Next.js static dashboard — polls `/api/events/recent` |
+| `{{LIFEOS_ROOT}}/hooks/EventLogger.hook.ts` | Consolidated event writer (absorbed ToolActivityTracker + ToolFailureTracker + SkillExecutionLog + ConfigAudit + StopFailureHandler 2026-07-11). PostToolUse catch-all → tool-activity.jsonl (+ SKILLS/execution.jsonl on Skill); PostToolUseFailure → tool-failures.jsonl; ConfigChange → config-changes.jsonl; StopFailure → SECURITY stop-failures (log-only) |
+| `{{LIFEOS_DIR}}/PULSE/Observability/observability.ts` | Observability module inside unified Pulse daemon — serves events from JSONL at :31337 |
+| `{{LIFEOS_DIR}}/PULSE/Observability/` | Next.js static dashboard — polls `/api/events/recent` |
## Dashboard Locations
@@ -96,9 +96,9 @@ The LifeOS Observatory is the local observability UI -- a Next.js 15.5 static ex
| Item | Value |
|------|-------|
-| Source | `~/.claude/LIFEOS/PULSE/Observability/` |
-| Build command | `cd ~/.claude/LIFEOS/PULSE/Observability && bun run build` (outputs to `out/`) |
-| Serving mechanism | Direct: `~/.claude/LIFEOS/PULSE/Observability/out` (configured in PULSE.toml `dashboard_dir`) |
+| Source | `{{LIFEOS_DIR}}/PULSE/Observability/` |
+| Build command | `cd "${LIFEOS_DIR}/PULSE/Observability" && bun run build` (outputs to `out/`) |
+| Serving mechanism | Direct: `{{LIFEOS_DIR}}/PULSE/Observability/out` (configured in PULSE.toml `dashboard_dir`) |
| URL | `http://localhost:31337/` (served by Pulse observability module) |
| Process management | Pulse runs under launchd (`com.lifeos.pulse`) with auto-restart. **Always** use `launchctl stop/start com.lifeos.pulse` -- never `kill`. |
@@ -218,8 +218,8 @@ All endpoints served by the Pulse daemon's observability module (`Observability/
### Deployment Checklist
-1. Edit source in `~/.claude/LIFEOS/PULSE/Observability/src/`
-2. Build: `cd ~/.claude/LIFEOS/PULSE/Observability && bun run build`
+1. Edit source in `{{LIFEOS_DIR}}/PULSE/Observability/src/`
+2. Build: `cd "${LIFEOS_DIR}/PULSE/Observability" && bun run build`
3. Restart Pulse: `launchctl stop com.lifeos.pulse && launchctl start com.lifeos.pulse`
4. Hard refresh browser: Cmd+Shift+R
@@ -227,7 +227,7 @@ All endpoints served by the Pulse daemon's observability module (`Observability/
Distinct from the event pipeline above, session state (active sessions, phase, progress, criteria, ratings) flows through a single canonical file. Both the Pulse dashboard and any external admin dashboard's agents page read the same file so they never drift.
-**Canonical source:** `$LIFEOS_DIR/MEMORY/STATE/work.json`
+**Canonical source:** `{{LIFEOS_DIR}}/MEMORY/STATE/work.json`
```
Writers (atomic read-modify-write via isa-utils.ts:writeRegistry)
@@ -255,4 +255,4 @@ Readers (both use identical mapping)
## See Also
-- `~/.claude/LIFEOS/DOCUMENTATION/LifeosSystemArchitecture.md` — Master LifeOS architecture reference
+- `{{LIFEOS_DIR}}/DOCUMENTATION/LifeosSystemArchitecture.md` — Master LifeOS architecture reference
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Pulse/PulseSystem.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Pulse/PulseSystem.md
index 4eb6ea7be9..3b5f0ef88f 100644
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Pulse/PulseSystem.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Pulse/PulseSystem.md
@@ -18,7 +18,7 @@ Every Pulse module is a sub-surface of the Dashboard: real-time observability, v
**Implementation:** The unified daemon of LifeOS — a single always-on process that handles cron jobs, voice notifications, hook validation, observability APIs + dashboard, Telegram chat, iMessage chat, and GitHub work polling. Pulse is THE local runtime for all LifeOS services. It absorbed VoiceServer, TelegramBot, the iMessage bot, and the Observability server into crash-isolated modules running under one process, one port (31337), and one launchd plist (`com.lifeos.pulse`).
**Version:** 2.0 (2026-04-01)
-**Location:** `~/.claude/LIFEOS/PULSE/`
+**Location:** `{{LIFEOS_DIR}}/PULSE/`
---
@@ -118,7 +118,7 @@ sendVoiceSummary(ctx, fullText) — fire-and-forget IIFE
- **Module load order matters for ELEVENLABS_API_KEY scrubbing.** Telegram imports first in Pulse; if it deletes `ELEVENLABS_API_KEY` from `process.env` at import time, VoiceServer's later init reads `apiKeyConfigured: false` and the CLI `/notify` channel goes dark. Telegram captures the key into a module-local const but does NOT delete from env. (The Anthropic-key scrubs at module top do still delete — those keys have a single consumer; the ElevenLabs key has two.)
- **disallowedTools shape gotcha.** The SDK's `disallowedTools?: string[]` field accepts tool **names** (`"Bash"`, `"Read"`), not glob patterns like `"Bash(curl *31337/notify*)"` (that's Claude Code's `settings.json permissions.deny` syntax). An earlier attempt to backstop the prompt-level `/notify` ban via `disallowedTools` patterns was silently ignored by the SDK. The hard barrier is now the `LIFEOS_NOTIFICATION_CHANNEL` env-var channel gate inside each voice-firing hook (see "Channel separation" row above) — `canUseTool` stops the model from calling /notify directly, but the env gate stops the hooks themselves from leaking even when the model behaves perfectly. The two together close every path: model→Bash→/notify blocked by canUseTool; SDK subprocess→Stop hook→/notify blocked by the env gate.
-- **SDK subprocess Stop hooks were the actual desktop-voice leak.** Discovered 2026-05-24: even with `canUseTool` blocking model-initiated `/notify` calls, the SDK subprocess's own Stop / StopFailure / UserPromptSubmit events fired the user-level hooks in `~/.claude/settings.json`, four of which POST to `localhost:31337/notify`. `voice-events.jsonl` showed Telegram replies (e.g., "can't write from Telegram. Open a terminal session.") being played on the desktop speaker. Fixed by propagating `LIFEOS_NOTIFICATION_CHANNEL=telegram` via `sdkOptions.env` so the hooks themselves can detect the remote channel and skip — see `hooks/lib/notification-channel.ts` and the "Channel separation" row above.
+- **SDK subprocess Stop hooks were the actual desktop-voice leak.** Discovered 2026-05-24: even with `canUseTool` blocking model-initiated `/notify` calls, the SDK subprocess's own Stop / StopFailure / UserPromptSubmit events fired the user-level hooks in `{{LIFEOS_ROOT}}/settings.json`, four of which POST to `localhost:31337/notify`. `voice-events.jsonl` showed Telegram replies (e.g., "can't write from Telegram. Open a terminal session.") being played on the desktop speaker. Fixed by propagating `LIFEOS_NOTIFICATION_CHANNEL=telegram` via `sdkOptions.env` so the hooks themselves can detect the remote channel and skip — see `hooks/lib/notification-channel.ts` and the "Channel separation" row above.
- **Sonnet via Inference.ts is slower than naive estimates.** Real measured cost is 4-6 seconds for short-summary tasks (claude CLI subprocess spawn + actual inference). The outer race was originally 3.5s — too tight, fired routinely. Now 10s. Total post-text voice latency is typically 5-8s.
### Doctrine cross-refs
@@ -273,7 +273,7 @@ enabled = true
### Script Jobs (`type = "script"`)
-Run a shell command via `bash -c`. The working directory is `~/.claude/Pulse/`. Environment variables from `~/.claude/.env` are available. The process has a 60-second timeout (SIGTERM on expiry).
+Run a shell command via `bash -c`. The working directory is `{{LIFEOS_ROOT}}/Pulse/`. Environment variables from `{{LIFEOS_ROOT}}/.env` are available. The process has a 60-second timeout (SIGTERM on expiry).
Cost: $0. All computation is local or uses free APIs.
@@ -346,7 +346,7 @@ The threshold is hardcoded at `MAX_FAILURES = 3` in `pulse.ts`.
### state.json
-Located at `~/.claude/Pulse/state/state.json`. Written atomically (write to `.tmp`, rename) after each job execution.
+Located at `{{LIFEOS_ROOT}}/Pulse/state/state.json`. Written atomically (write to `.tmp`, rename) after each job execution.
```json
{
@@ -400,9 +400,9 @@ Pulse is managed by macOS launchd via `com.lifeos.pulse.plist`. Key properties:
| `RunAtLoad` | `true` | Starts on login |
| `KeepAlive` | `true` | Auto-restarts on crash |
| `ThrottleInterval` | `30` | Minimum 30 seconds between restart attempts |
-| `WorkingDirectory` | `~/.claude/Pulse` | CWD for the process |
+| `WorkingDirectory` | `{{LIFEOS_ROOT}}/Pulse` | CWD for the process |
-Logs go to `~/.claude/Pulse/logs/pulse-stdout.log` and `pulse-stderr.log`.
+Logs go to `{{LIFEOS_ROOT}}/Pulse/logs/pulse-stdout.log` and `pulse-stderr.log`.
### Startup
@@ -471,7 +471,7 @@ output = "telegram"
enabled = true
```
-3. Restart Pulse: `~/.claude/Pulse/manage.sh restart`
+3. Restart Pulse: `{{LIFEOS_ROOT}}/Pulse/manage.sh restart`
### Modifying an Existing Job
@@ -586,7 +586,7 @@ With all jobs enabled including proactive-suggestions: ~$0.065/day.
### Check Status
```bash
-~/.claude/Pulse/manage.sh status
+"${LIFEOS_ROOT}/Pulse/manage.sh" status
```
Shows PID, uptime, and per-job last run times with failure counts.
@@ -595,13 +595,13 @@ Shows PID, uptime, and per-job last run times with failure counts.
```bash
# Recent stdout (structured JSON)
-tail -50 ~/.claude/Pulse/logs/pulse-stdout.log
+tail -50 "${LIFEOS_ROOT}/Pulse/logs/pulse-stdout.log"
# Recent errors
-tail -50 ~/.claude/Pulse/logs/pulse-stderr.log
+tail -50 "${LIFEOS_ROOT}/Pulse/logs/pulse-stderr.log"
# Follow live
-tail -f ~/.claude/Pulse/logs/pulse-stdout.log | bun -e "process.stdin.on('data', d => { try { const e = JSON.parse(d); console.log(e.ts, e.level, e.msg) } catch {} })"
+tail -f "${LIFEOS_ROOT}/Pulse/logs/pulse-stdout.log" | bun -e "process.stdin.on('data', d => { try { const e = JSON.parse(d); console.log(e.ts, e.level, e.msg) } catch {} })"
```
### Common Issues
@@ -611,10 +611,10 @@ tail -f ~/.claude/Pulse/logs/pulse-stdout.log | bun -e "process.stdin.on('data',
| "NOT RUNNING (no PID file)" | Pulse not started or crashed without recovery | `manage.sh install` |
| "DEAD (stale PID)" | Process died but launchd did not restart | `manage.sh restart` |
| Job stuck in circuit breaker | 3+ consecutive failures | Fix the check script, then `manage.sh restart` |
-| "Telegram dispatch skipped" | Missing env vars | Set `TELEGRAM_BOT_TOKEN` and `TELEGRAM_PRINCIPAL_CHAT_ID` in `~/.claude/.env` |
-| "ntfy dispatch skipped" | Missing env var | Set `NTFY_TOPIC` in `~/.claude/.env` |
+| "Telegram dispatch skipped" | Missing env vars | Set `TELEGRAM_BOT_TOKEN` and `TELEGRAM_PRINCIPAL_CHAT_ID` in `{{LIFEOS_ROOT}}/.env` |
+| "ntfy dispatch skipped" | Missing env var | Set `NTFY_TOPIC` in `{{LIFEOS_ROOT}}/.env` |
| Voice notifications silent | Voice module not running or Pulse down | `manage.sh restart`; check `[voice] enabled = true` in PULSE.toml |
-| Calendar returns NO_EVENTS always | Missing or expired refresh token | Set `GOOGLE_CALENDAR_REFRESH_TOKEN` in `~/.claude/.env` |
+| Calendar returns NO_EVENTS always | Missing or expired refresh token | Set `GOOGLE_CALENDAR_REFRESH_TOKEN` in `{{LIFEOS_ROOT}}/.env` |
| State file corrupt | Interrupted write (unlikely, writes are atomic) | Delete `state/state.json` and restart |
### Manual Job Test
@@ -622,7 +622,7 @@ tail -f ~/.claude/Pulse/logs/pulse-stdout.log | bun -e "process.stdin.on('data',
Run a check script directly to verify it works:
```bash
-cd ~/.claude/Pulse
+cd "${LIFEOS_ROOT}/Pulse"
bun run checks/health.ts
bun run checks/calendar.ts
bun run checks/email.ts
@@ -634,7 +634,7 @@ bun run checks/github.ts
## File Inventory
```
-~/.claude/Pulse/
+{{LIFEOS_ROOT}}/Pulse/
├── pulse.ts # Main daemon -- startup, module init, heartbeat loop
├── PULSE.toml # Job + module configuration
├── manage.sh # Process management -- start/stop/status/install
@@ -682,7 +682,7 @@ bun run checks/github.ts
LifeOS Pulse includes a native macOS menu bar app that shows daemon status at a glance. The menu bar app is launched automatically by Pulse on startup -- no separate launchd plist needed.
-**Location:** `~/.claude/LIFEOS/PULSE/MenuBar/`
+**Location:** `{{LIFEOS_DIR}}/PULSE/MenuBar/`
**Installed to:** `~/Applications/LifeOS Pulse.app`
**Launched by:** Pulse process on startup (no separate launchd plist)
@@ -704,7 +704,7 @@ Reads `state/state.json` directly every 5 seconds (no HTTP endpoint needed). Che
### Building and Installing
```bash
-cd ~/.claude/LIFEOS/PULSE/MenuBar
+cd "${LIFEOS_DIR}/PULSE/MenuBar"
bash install.sh # Builds, deploys to ~/Applications, installs plist
```
@@ -735,7 +735,7 @@ Pulse includes an integrated HTTP hook validation server as the `hooks` module (
### Hook Configuration
-The hooks are configured in `~/.claude/settings.json` as HTTP hooks pointing to `http://localhost:31337/hooks/*`.
+The hooks are configured in `{{LIFEOS_ROOT}}/settings.json` as HTTP hooks pointing to `http://localhost:31337/hooks/*`.
---
@@ -802,7 +802,7 @@ This ensures the browser always picks up new builds without stale content.
After building the Observatory dashboard, Pulse must be restarted to pick up new files:
```bash
-cd ~/.claude/LIFEOS/Observability && bun run build
+cd "${LIFEOS_DIR}/Observability" && bun run build
launchctl stop com.lifeos.pulse && launchctl start com.lifeos.pulse
```
@@ -849,7 +849,7 @@ Note: static catalogs baked into a page or module (e.g. Amber's 11-input list mi
- **Keepalive:** `: keepalive\n\n` comment every 25s per subscriber so corporate proxies / macOS loopback don't drop idle connections.
- **Disable:** `LIFEOS_NO_SSE=1` env var → 503 on the endpoint, dashboard falls back to legacy 2s polling identically to pre-change behavior.
- **Dashboard contract:** `useAlgorithmState` hook tries SSE on mount; on three consecutive `onerror` events drops the EventSource and reverts polling to 2s. While SSE is connected, polling drops to 30s (safety net).
-- **Atomic emitter:** `bun ~/.claude/LIFEOS/TOOLS/AlgoPhase.ts ` writes the current session's phase into `work.json` in ~22ms (p95 27ms). The Algorithm doctrine prescribes invocation at every phase transition. (Until 2026-07-11 a `TheRouter.hook.ts` pre-emit via `markAlgorithmStarting` closed the wrong-phase window at session start; that hook was deleted with the mode/tier retirement, so the first `AlgoPhase.ts`/`ISASync` write now sets the phase.)
+- **Atomic emitter:** `bun "${LIFEOS_DIR}/TOOLS/AlgoPhase.ts" ` writes the current session's phase into `work.json` in ~22ms (p95 27ms). The Algorithm doctrine prescribes invocation at every phase transition. (Until 2026-07-11 a `TheRouter.hook.ts` pre-emit via `markAlgorithmStarting` closed the wrong-phase window at session start; that hook was deleted with the mode/tier retirement, so the first `AlgoPhase.ts`/`ISASync` write now sets the phase.)
Full architectural reasoning in `MEMORY/WORK/20260524-072107_pulse-agents-realtime-phase-tracking/ISA.md`.
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Security/NpmRapidResponse.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Security/NpmRapidResponse.md
index 95295928fe..15f1f085b0 100644
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Security/NpmRapidResponse.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Security/NpmRapidResponse.md
@@ -13,7 +13,7 @@ Fire this runbook when ANY new npm supply-chain advisory drops (TanStack-style w
## Standing defense (already in place)
-- **`minimumReleaseAge = 86400`** in `~/.bunfig.toml` and `~/.claude/bunfig.toml` rejects any package published in the last 24 hours. The May 11 Mini Shai-Hulud worm lived 3 hours from publish to npm-pull; this filter neutralizes that attack window without requiring vendor trust.
+- **`minimumReleaseAge = 86400`** in `~/.bunfig.toml` and `{{LIFEOS_ROOT}}/bunfig.toml` rejects any package published in the last 24 hours. The May 11 Mini Shai-Hulud worm lived 3 hours from publish to npm-pull; this filter neutralizes that attack window without requiring vendor trust.
- **Bun's `trustedDependencies` allowlist** — transitive packages don't get to run lifecycle scripts unless their name is in your `package.json`'s `trustedDependencies` array. Default-deny on transitive scripts.
- **All active CI Actions SHA-pinned** in each project's `.github/workflows/` directory with Dependabot auto-PR'ing bumps weekly.
@@ -30,13 +30,13 @@ Pull the named compromised packages and their version ranges into a working note
IOC='@evil/pkg-one|@evil/pkg-two|"some-package":'
# Scan all lockfiles
-find ~/Projects ~/LocalProjects ~/.claude -maxdepth 6 \
+find ~/Projects ~/LocalProjects "${LIFEOS_ROOT}" -maxdepth 6 \
\( -name "bun.lock" -o -name "package-lock.json" -o -name "yarn.lock" -o -name "pnpm-lock.yaml" \) \
-not -path "*/node_modules/*" -not -path "*/LIFEOS_RELEASES/*" -not -path "*/.next/*" \
2>/dev/null | xargs rg -l "$IOC"
# Scan all package.json (for declared deps that may have lockfile drift)
-find ~/Projects ~/LocalProjects ~/.claude -maxdepth 6 -name "package.json" \
+find ~/Projects ~/LocalProjects "${LIFEOS_ROOT}" -maxdepth 6 -name "package.json" \
-not -path "*/node_modules/*" -not -path "*/LIFEOS_RELEASES/*" -not -path "*/.next/*" \
2>/dev/null | xargs rg -l "$IOC"
```
@@ -49,7 +49,7 @@ If either returns hits → record the project path + version, then per-project r
```bash
# Replace dates with the advisory's worm window
-for lf in $(find ~/Projects ~/LocalProjects ~/.claude -maxdepth 6 -name "bun.lock" -not -path "*/node_modules/*" 2>/dev/null); do
+for lf in $(find ~/Projects ~/LocalProjects "${LIFEOS_ROOT}" -maxdepth 6 -name "bun.lock" -not -path "*/node_modules/*" 2>/dev/null); do
mt=$(stat -f "%Sm" -t "%Y-%m-%d" "$lf" 2>/dev/null)
case "$mt" in
YYYY-MM-DD|YYYY-MM-DD) # <-- replace with worm window dates
@@ -77,7 +77,7 @@ bun install --frozen-lockfile
- npm token (if any) — `npm token list`, revoke, regenerate
- GitHub PAT (any token used in CI on the host) — `gh auth refresh`
- AWS / GCP / Azure on dev hosts where install ran during worm window
-- See `~/.claude/skills/_INCIDENT_RESPONSE/SKILL.md` for the full rotation protocol
+- See `{{LIFEOS_ROOT}}/skills/_INCIDENT_RESPONSE/SKILL.md` for the full rotation protocol
## Hardening levers (one per attack surface)
@@ -102,7 +102,7 @@ bun install --frozen-lockfile
## Reference
- May 11, 2026 Mini Shai-Hulud worm postmortem: https://tanstack.com/blog/npm-supply-chain-compromise-postmortem
-- Audit ISA that produced this runbook: `~/.claude/LIFEOS/MEMORY/WORK/20260520-npm-exposure-audit/ISA.md`
-- Implementation ISA: `~/.claude/LIFEOS/MEMORY/WORK/20260520-npm-hardening-implementation/ISA.md`
+- Audit ISA that produced this runbook: `{{LIFEOS_DIR}}/MEMORY/WORK/20260520-npm-exposure-audit/ISA.md`
+- Implementation ISA: `{{LIFEOS_DIR}}/MEMORY/WORK/20260520-npm-hardening-implementation/ISA.md`
- Bun bunfig docs: https://bun.sh/docs/runtime/bunfig
- `_INCIDENT_RESPONSE` skill for the parallel credential rotation protocol.
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Security/README.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Security/README.md
index 80b758bb08..97287e00b3 100644
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Security/README.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Security/README.md
@@ -93,7 +93,7 @@ That's the entire defense delta beyond L1 and L2 on the ingress side.
## Release Deny-List (canonical sensitive-pattern source)
-The release pipeline has its own constitutional surface: the **deny-list** at `~/.claude/skills/_LIFEOS/DENY_LIST.txt`. Plain text, one ripgrep-compatible regex per line, four sections (`Identity`, `Hostnames`, `Cloudflare IDs`, `Private Tokens`).
+The release pipeline has its own constitutional surface: the **deny-list** at `{{LIFEOS_ROOT}}/skills/_LIFEOS/DENY_LIST.txt`. Plain text, one ripgrep-compatible regex per line, four sections (`Identity`, `Hostnames`, `Cloudflare IDs`, `Private Tokens`).
| Consumer | What it does |
|----------|--------------|
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Services/BackgroundServices.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Services/BackgroundServices.md
index 535c257102..4f99ba6f27 100644
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Services/BackgroundServices.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Services/BackgroundServices.md
@@ -13,11 +13,11 @@ generator: LIFEOS/TOOLS/Services.ts (run `bun Services.ts doc` to regenerate the
## The one-shot
```bash
-bun ~/.claude/LIFEOS/TOOLS/Services.ts status # what's running vs installed vs available
-bun ~/.claude/LIFEOS/TOOLS/Services.ts install --all --yes # stand up every service (macOS)
-bun ~/.claude/LIFEOS/TOOLS/Services.ts install --only worksweep,amberroute --yes
-bun ~/.claude/LIFEOS/TOOLS/Services.ts uninstall --only amberroute
-bun ~/.claude/LIFEOS/TOOLS/Services.ts doc # regenerate the table below
+bun "${LIFEOS_DIR}/TOOLS/Services.ts" status # what's running vs installed vs available
+bun "${LIFEOS_DIR}/TOOLS/Services.ts" install --all --yes # stand up every service (macOS)
+bun "${LIFEOS_DIR}/TOOLS/Services.ts" install --only worksweep,amberroute --yes
+bun "${LIFEOS_DIR}/TOOLS/Services.ts" uninstall --only amberroute
+bun "${LIFEOS_DIR}/TOOLS/Services.ts" doc # regenerate the table below
```
`status` and `doc` are **read-only** (safe anytime). `install`/`uninstall` call `launchctl` — a privileged step, so the human (or the installing AI, with permission) runs them; `install` without `--yes` is a dry preview. Core services install by default; opt-in ones need `--all` or `--only`.
@@ -40,23 +40,23 @@ bun ~/.claude/LIFEOS/TOOLS/Services.ts doc # regenerate the tabl
| Service | Category | Cadence | Opt-in | Purpose | Install |
|---------|----------|---------|--------|---------|---------|
-| **Pulse (dashboard server)** `com.lifeos.pulse` | pulse | at load | core | The Life Dashboard HTTP server on :31337 — Pulse, the visible surface onto LifeOS. | `bash ~/.claude/LIFEOS/PULSE/manage.sh install` |
-| **Pulse menu-bar app** `com.lifeos.pulse-menubar` | pulse | at load | core | macOS menu-bar app for Pulse — quick status + open the dashboard. | `bash ~/.claude/LIFEOS/PULSE/MenuBar/install.sh` |
-| **Pulse deriver** `com.lifeos.deriver` | pulse | daily/scheduled | core | Regenerates Pulse's derived Data-Plane pages on a cadence. | `bash ~/.claude/LIFEOS/PULSE/manage-deriver.sh install` |
-| **Conduit (sensory capture)** `com.lifeos.conduit` | capture | every 2m | core | Local current-state capture — feeds memory + TELOS current state. | `bun ~/.claude/LIFEOS/PULSE/Conduit/InstallConduit.ts` |
-| **Conduit insight builder** `com.lifeos.conduit.insight` | capture | every 1h | core | Builds insights from Conduit's captured signal. | `bun ~/.claude/LIFEOS/PULSE/Conduit/InstallConduitInsight.ts` |
+| **Pulse (dashboard server)** `com.lifeos.pulse` | pulse | at load | core | The Life Dashboard HTTP server on :31337 — Pulse, the visible surface onto LifeOS. | `bash "${LIFEOS_DIR}/PULSE/manage.sh" install` |
+| **Pulse menu-bar app** `com.lifeos.pulse-menubar` | pulse | at load | core | macOS menu-bar app for Pulse — quick status + open the dashboard. | `bash "${LIFEOS_DIR}/PULSE/MenuBar/install.sh"` |
+| **Pulse deriver** `com.lifeos.deriver` | pulse | daily/scheduled | core | Regenerates Pulse's derived Data-Plane pages on a cadence. | `bash "${LIFEOS_DIR}/PULSE/manage-deriver.sh" install` |
+| **Conduit (sensory capture)** `com.lifeos.conduit` | capture | every 2m | core | Local current-state capture — feeds memory + TELOS current state. | `bun "${LIFEOS_DIR}/PULSE/Conduit/InstallConduit.ts"` |
+| **Conduit insight builder** `com.lifeos.conduit.insight` | capture | every 1h | core | Builds insights from Conduit's captured signal. | `bun "${LIFEOS_DIR}/PULSE/Conduit/InstallConduitInsight.ts"` |
| **Synthesis** `com.lifeos.synthesis` | maintenance | daily/scheduled | yes | Periodic synthesis pass over recent state/memory (weekly-style rollup). | installed with the Pulse/Conduit stack — see PULSE/ |
-| **Work sweep** `com.lifeos.worksweep` | sweep | every 1h | yes | Hourly UL work capture — untracked sessions, stale items, project checks, TELOS-goal derivation. | `bun ~/.claude/LIFEOS/TOOLS/InstallWorkSweep.ts` |
-| **Conveyor inbox watcher** `com.lifeos.conveyor-watcher` | capture | KeepAlive daemon | yes | Watches `~/Recordings/Inbox` (CONVEYOR_INBOX override); quiescence-gated, hash-idempotent registration of dropped recordings into the content-pipeline ledger (Conveyor P1). | `bun ~/.claude/LIFEOS/TOOLS/InstallConveyorWatcher.ts` |
-| **Derived-file sync** `com.lifeos.derivedsync` | sync | on file-change | yes | Watches 31 USER source files; regenerates PRINCIPAL_TELOS, LIFEOS_STATE, Data-Plane on hand-edits. | `bun ~/.claude/LIFEOS/TOOLS/InstallDerivedSync.ts` |
-| **Health sync** `com.lifeos.healthsync` | sync | every 1h | yes | Syncs health data into CURRENT_STATE. | `bun ~/.claude/LIFEOS/TOOLS/InstallHealthSync.ts` |
-| **Codex update** `com.lifeos.codexupdate` | maintenance | daily/scheduled | yes | Keeps the Codex mirror / update state current. | `bun ~/.claude/LIFEOS/TOOLS/InstallCodexUpdate.ts` |
-| **Commitment sweep** `com.lifeos.commitmentsweep` | sweep | daily/scheduled | yes | Sweeps commitments/reminders on a cadence. | `bun ~/.claude/LIFEOS/TOOLS/InstallCommitmentSweep.ts` |
-| **Blog discovery** `com.lifeos.blogdiscovery` | sweep | daily/scheduled | yes | Discovers blog-worthy signal on a cadence. | `bun ~/.claude/LIFEOS/TOOLS/InstallBlogDiscovery.ts` |
-| **Usage aggregator** `com.lifeos.usage-aggregator` | maintenance | daily/scheduled | yes | Aggregates usage/cost telemetry for Pulse. | `bun ~/.claude/LIFEOS/TOOLS/InstallUsageAggregator.ts` |
+| **Work sweep** `com.lifeos.worksweep` | sweep | every 1h | yes | Hourly UL work capture — untracked sessions, stale items, project checks, TELOS-goal derivation. | `bun "${LIFEOS_DIR}/TOOLS/InstallWorkSweep.ts"` |
+| **Conveyor inbox watcher** `com.lifeos.conveyor-watcher` | capture | KeepAlive daemon | yes | Watches `~/Recordings/Inbox` (CONVEYOR_INBOX override); quiescence-gated, hash-idempotent registration of dropped recordings into the content-pipeline ledger (Conveyor P1). | `bun "${LIFEOS_DIR}/TOOLS/InstallConveyorWatcher.ts"` |
+| **Derived-file sync** `com.lifeos.derivedsync` | sync | on file-change | yes | Watches 31 USER source files; regenerates PRINCIPAL_TELOS, LIFEOS_STATE, Data-Plane on hand-edits. | `bun "${LIFEOS_DIR}/TOOLS/InstallDerivedSync.ts"` |
+| **Health sync** `com.lifeos.healthsync` | sync | every 1h | yes | Syncs health data into CURRENT_STATE. | `bun "${LIFEOS_DIR}/TOOLS/InstallHealthSync.ts"` |
+| **Codex update** `com.lifeos.codexupdate` | maintenance | daily/scheduled | yes | Keeps the Codex mirror / update state current. | `bun "${LIFEOS_DIR}/TOOLS/InstallCodexUpdate.ts"` |
+| **Commitment sweep** `com.lifeos.commitmentsweep` | sweep | daily/scheduled | yes | Sweeps commitments/reminders on a cadence. | `bun "${LIFEOS_DIR}/TOOLS/InstallCommitmentSweep.ts"` |
+| **Blog discovery** `com.lifeos.blogdiscovery` | sweep | daily/scheduled | yes | Discovers blog-worthy signal on a cadence. | `bun "${LIFEOS_DIR}/TOOLS/InstallBlogDiscovery.ts"` |
+| **Usage aggregator** `com.lifeos.usage-aggregator` | maintenance | daily/scheduled | yes | Aggregates usage/cost telemetry for Pulse. | `bun "${LIFEOS_DIR}/TOOLS/InstallUsageAggregator.ts"` |
| **Bookmark pipeline watchdog** `com.lifeos.bookmark-watchdog` | capture | every 4h | yes | Watches the X bookmark → summarize/idea pipeline for stalls. | ARBOL/BookmarkPipelineWatchdog.ts — see Arbol |
| **Backups** `com.lifeos.backups` | maintenance | daily/scheduled | yes | Daily 03:00 PT repo backup (Git LFS). | Backups project — installed from its own repo (backup.sh) |
-| **Amber router** `com.lifeos.amberroute` | capture | every 30m | yes | Every 30 min: TELOS-grade unrouted Amber captures → KNOWLEDGE notes / UL issues. | `bun ~/.claude/LIFEOS/USER/CUSTOMIZATIONS/ARBOL/Workers/_A_AMBER_LEDGER/Tools/InstallAmberRoute.ts` |
+| **Amber router** `com.lifeos.amberroute` | capture | every 30m | yes | Every 30 min: TELOS-grade unrouted Amber captures → KNOWLEDGE notes / UL issues. | `bun "${LIFEOS_DIR}/USER/CUSTOMIZATIONS/ARBOL/Workers/_A_AMBER_LEDGER/Tools/InstallAmberRoute.ts"` |
## Fresh install
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Skills/SkillSystem.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Skills/SkillSystem.md
index bbc7080348..a0f3211291 100755
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Skills/SkillSystem.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Skills/SkillSystem.md
@@ -51,7 +51,7 @@ version: 1.5.1
### Public skills (`TitleCase`) — ship in the LifeOS public release
- ONLY templated, safe, public, ready content
- Generic instructions any LifeOS user could follow; placeholder values, public API references
-- Can reference `~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS//` at runtime for per-user tweaks
+- Can reference `{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS//` at runtime for per-user tweaks
- Exported to the public LifeOS repository as-is
**Forbidden in public skills:**
@@ -67,7 +67,7 @@ version: 1.5.1
### Private skills (`_ALLCAPS`) — never leave the local repo
- Anything goes: real names, real domains, real customers, real internal infra
- The underscore IS the safety boundary; release tooling skips them
-- The decision rule: *"Could this skill be dropped, as-is, into a stranger's `~/.claude/skills/` and just work?"* If no, it must be `_ALLCAPS`
+- The decision rule: *"Could this skill be dropped, as-is, into a stranger's `{{LIFEOS_ROOT}}/skills/` and just work?"* If no, it must be `_ALLCAPS`
**Decision test — these triggers FORCE `_ALLCAPS` naming:**
@@ -87,12 +87,12 @@ version: 1.5.1
**Listing skills by category:**
```bash
-ls -1 ~/.claude/skills/ | grep -v '^_' # Public (TitleCase)
-ls -1 ~/.claude/skills/ | grep '^_' # Private (_ALLCAPS)
+ls -1 "${LIFEOS_ROOT}/skills/" | grep -v '^_' # Public (TitleCase)
+ls -1 "${LIFEOS_ROOT}/skills/" | grep '^_' # Private (_ALLCAPS)
```
**Pattern for per-user layering in public skills:**
-A public skill can be templated to load runtime customizations from `~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS//PREFERENCES.md`. The skill body stays generic; the customization file overlays per-instance context. **Do not use SKILLCUSTOMIZATIONS to smuggle private content into a public skill** — if the skill *requires* private context to function, it must be renamed `_ALLCAPS`.
+A public skill can be templated to load runtime customizations from `{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS//PREFERENCES.md`. The skill body stays generic; the customization file overlays per-instance context. **Do not use SKILLCUSTOMIZATIONS to smuggle private content into a public skill** — if the skill *requires* private context to function, it must be renamed `_ALLCAPS`.
**NEVER hardcode personal data in public skills.**
@@ -112,7 +112,7 @@ All skills include this standard instruction block after the YAML frontmatter:
## Customization
**Before executing, check for user customizations at:**
-`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/{SkillName}/`
+`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/{SkillName}/`
If this directory exists, load and apply:
- `PREFERENCES.md` - User preferences and configuration
@@ -124,7 +124,7 @@ These define user-specific preferences. If the directory does not exist, proceed
### Directory Structure
```
-~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/
+{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/
├── README.md # Documentation for this system
├── Art/ # Art skill customizations
│ ├── EXTEND.yaml # Extension manifest
@@ -177,7 +177,7 @@ description: "What this customization adds"
### Creating a Customization
-1. **Create directory**: `mkdir -p ~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/SkillName`
+1. **Create directory**: `mkdir -p "${LIFEOS_DIR}/USER/CUSTOMIZATIONS/SKILLS/SkillName"`
2. **Create EXTEND.yaml**: Define what files to load and merge strategy
3. **Create PREFERENCES.md**: User preferences for this skill
4. **Add additional files**: Any skill-specific configurations
@@ -252,7 +252,7 @@ science_cycle_time: meso
- **Research** - Investigation through hypotheses and evidence gathering
- **Council** - Debate as parallel hypothesis testing
-**See:** `~/.claude/skills/Science/Protocol.md` for the full protocol interface
+**See:** `{{LIFEOS_ROOT}}/skills/Science/Protocol.md` for the full protocol interface
### 2. Markdown Body (Workflow Routing + Examples + Documentation)
@@ -278,7 +278,7 @@ science_cycle_time: meso
Running the **WorkflowName** workflow in the **SkillName** skill to ACTION...
```
-**Full documentation:** `~/.claude/LIFEOS/DOCUMENTATION/Notifications/NotificationSystem.md`
+**Full documentation:** `{{LIFEOS_DIR}}/DOCUMENTATION/Notifications/NotificationSystem.md`
## Workflow Routing
@@ -451,9 +451,9 @@ SkillSearch('art tools') # Loads Tools.md from skill root
```
Or reference them directly:
-```bash
+```text
# Read specific context file
-Read ~/.claude/skills/Art/Aesthetic.md
+Read {{LIFEOS_ROOT}}/skills/Art/Aesthetic.md
```
Context files can reference workflows and tools:
@@ -538,7 +538,7 @@ Don't bother for:
Use the CreateSkill skill's CanonicalizeSkill workflow:
```
-~/.claude/skills/CreateSkill/Workflows/CanonicalizeSkill.md
+{{LIFEOS_ROOT}}/skills/CreateSkill/Workflows/CanonicalizeSkill.md
```
Or manually:
@@ -546,14 +546,14 @@ Or manually:
2. Update YAML frontmatter to single-line description
3. Add `## Workflow Routing` table
4. Add `## Examples` section
-5. Move backups to `~/.claude/LIFEOS/MEMORY/Backups/`
+5. Move backups to `{{LIFEOS_DIR}}/MEMORY/Backups/`
6. Verify against checklist
### How to Test Effectiveness
After creating or canonicalizing a skill, verify it actually improves outcomes:
```
-~/.claude/skills/CreateSkill/Workflows/TestSkill.md
+{{LIFEOS_ROOT}}/skills/CreateSkill/Workflows/TestSkill.md
```
This runs the skill against real prompts with a no-skill baseline comparison. If the skill underperforms, use `ImproveSkill.md` to iterate. If the description doesn't trigger reliably, use `OptimizeDescription.md` to test and refine trigger accuracy.
@@ -617,7 +617,7 @@ description: Complete blog workflow. USE WHEN user mentions doing anything with
## Complete Canonical Example: a personal blogging skill
-**Reference:** any well-formed skill in `~/.claude/skills/` follows the same pattern; private personal skills use the `_NAME/` form below.
+**Reference:** any well-formed skill in `{{LIFEOS_ROOT}}/skills/` follows the same pattern; private personal skills use the `_NAME/` form below.
```yaml
---
@@ -646,7 +646,7 @@ Complete blog workflow.
Running the **WorkflowName** workflow in the **Blogging** skill to ACTION...
```
-**Full documentation:** `~/.claude/LIFEOS/DOCUMENTATION/Notifications/NotificationSystem.md`
+**Full documentation:** `{{LIFEOS_DIR}}/DOCUMENTATION/Notifications/NotificationSystem.md`
## Core Paths
@@ -860,7 +860,7 @@ bun ToolName.ts \
\`\`\`
```
-**See:** `~/.claude/LIFEOS/DOCUMENTATION/Tools/CliFirstArchitecture.md` (Workflow-to-Tool Integration section)
+**See:** `{{LIFEOS_DIR}}/DOCUMENTATION/Tools/CliFirstArchitecture.md` (Workflow-to-Tool Integration section)
---
@@ -965,7 +965,7 @@ bun Generate.ts \
4. **Value flags**: `--flag ` for choices
5. **Composable**: Flags should combine logically
-**See:** `~/.claude/LIFEOS/DOCUMENTATION/Tools/CliFirstArchitecture.md` (Configuration Flags section) for full documentation
+**See:** `{{LIFEOS_DIR}}/DOCUMENTATION/Tools/CliFirstArchitecture.md` (Configuration Flags section) for full documentation
### Tool Structure
@@ -975,7 +975,7 @@ bun Generate.ts \
* ToolName.ts - Brief description
*
* Usage:
- * bun ~/.claude/skills/SkillName/Tools/ToolName.ts [options]
+ * bun {{LIFEOS_ROOT}}/skills/SkillName/Tools/ToolName.ts [options]
*
* Commands:
* start Start the thing
@@ -1093,5 +1093,5 @@ This system ensures:
## Related Systems
-- **Master Architecture:** `~/.claude/LIFEOS/DOCUMENTATION/LifeosSystemArchitecture.md` — authoritative system-of-systems reference
-- **Knowledge Archive:** `~/.claude/LIFEOS/MEMORY/KNOWLEDGE/` — entity-based archive with 4 types (People, Companies, Ideas, Research), managed by Algorithm LEARN phase (direct writes), `LIFEOS/TOOLS/KnowledgeHarvester.ts` (validation/maintenance), and the `/knowledge` skill. Topic is a tag, not a domain. Skills that perform research or analysis can query the archive for accumulated knowledge.
+- **Master Architecture:** `{{LIFEOS_DIR}}/DOCUMENTATION/LifeosSystemArchitecture.md` — authoritative system-of-systems reference
+- **Knowledge Archive:** `{{LIFEOS_DIR}}/MEMORY/KNOWLEDGE/` — entity-based archive with 4 types (People, Companies, Ideas, Research), managed by Algorithm LEARN phase (direct writes), `LIFEOS/TOOLS/KnowledgeHarvester.ts` (validation/maintenance), and the `/knowledge` skill. Topic is a tag, not a domain. Skills that perform research or analysis can query the archive for accumulated knowledge.
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/SystemUserBoundary.md b/LifeOS/install/LIFEOS/DOCUMENTATION/SystemUserBoundary.md
index 7dc115f1a8..dafc593b04 100644
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/SystemUserBoundary.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/SystemUserBoundary.md
@@ -26,42 +26,42 @@ Public-by-construction code, documentation, and templates that ship in every Lif
| Path | Status |
|------|--------|
-| `~/.claude/CLAUDE.md` | SYSTEM (after Phase C — split into system router + user @-imports) |
-| `~/.claude/settings.json` | SYSTEM (after Phase B — split into system defaults + user overlay merged at startup) |
-| `~/.claude/skills/LifeOS/install/install.sh` | SYSTEM (installer bootstrap — ships inside the `LifeOS/` skill; there is no root-level `install.sh` in a release) |
-| `~/.claude/LICENSE` | SYSTEM |
-| `~/.claude/.gitignore`, `.gitattributes`, `.gitmodules`, `.mcp.json`, `.lsp.json`, `bunfig.toml` | SYSTEM (config) |
-| `~/.claude/LIFEOS/LIFEOS_SYSTEM_PROMPT.md` | SYSTEM (after Phase C — operational rules with user-specific content move out) |
-| `~/.claude/LIFEOS/VERSION` | SYSTEM |
-| `~/.claude/LIFEOS/LIFEOS_StatusLine.sh` | SYSTEM |
-| `~/.claude/LIFEOS/ALGORITHM/**` | SYSTEM |
-| `~/.claude/LIFEOS/DOCUMENTATION/**` | SYSTEM (after Phase D — categorical placeholders only) |
-| `~/.claude/LIFEOS/PULSE/` (source code only — excludes `Assistant/state/`, `state/`, `logs/`, `Plans/`, `Observability/out/`) | SYSTEM (after Phase F — parameterized via LifeosConfig) |
-| `~/.claude/LIFEOS/TOOLS/**` (after the few user-path leaks are scrubbed) | SYSTEM |
-| `~/.claude/LIFEOS/ScheduledTasks/**` (system-shipped scheduled task templates, NOT user instances) | SYSTEM |
-| `~/.claude/hooks/**` | SYSTEM |
-| `~/.claude/skills//**` where `` does NOT start with `_` | SYSTEM |
-| `~/.claude/commands/**` | SYSTEM |
-| `~/.claude/agents/**` (shipped agent definitions) | SYSTEM |
+| `{{LIFEOS_ROOT}}/CLAUDE.md` | SYSTEM (after Phase C — split into system router + user @-imports) |
+| `{{LIFEOS_ROOT}}/settings.json` | SYSTEM (after Phase B — split into system defaults + user overlay merged at startup) |
+| `{{LIFEOS_ROOT}}/skills/LifeOS/install/install.sh` | SYSTEM (installer bootstrap — ships inside the `LifeOS/` skill; there is no root-level `install.sh` in a release) |
+| `{{LIFEOS_ROOT}}/LICENSE` | SYSTEM |
+| `{{LIFEOS_ROOT}}/.gitignore`, `.gitattributes`, `.gitmodules`, `.mcp.json`, `.lsp.json`, `bunfig.toml` | SYSTEM (config) |
+| `{{LIFEOS_DIR}}/LIFEOS_SYSTEM_PROMPT.md` | SYSTEM (after Phase C — operational rules with user-specific content move out) |
+| `{{LIFEOS_DIR}}/VERSION` | SYSTEM |
+| `{{LIFEOS_DIR}}/LIFEOS_StatusLine.sh` | SYSTEM |
+| `{{LIFEOS_DIR}}/ALGORITHM/**` | SYSTEM |
+| `{{LIFEOS_DIR}}/DOCUMENTATION/**` | SYSTEM (after Phase D — categorical placeholders only) |
+| `{{LIFEOS_DIR}}/PULSE/` (source code only — excludes `Assistant/state/`, `state/`, `logs/`, `Plans/`, `Observability/out/`) | SYSTEM (after Phase F — parameterized via LifeosConfig) |
+| `{{LIFEOS_DIR}}/TOOLS/**` (after the few user-path leaks are scrubbed) | SYSTEM |
+| `{{LIFEOS_DIR}}/ScheduledTasks/**` (system-shipped scheduled task templates, NOT user instances) | SYSTEM |
+| `{{LIFEOS_ROOT}}/hooks/**` | SYSTEM |
+| `{{LIFEOS_ROOT}}/skills//**` where `` does NOT start with `_` | SYSTEM |
+| `{{LIFEOS_ROOT}}/commands/**` | SYSTEM |
+| `{{LIFEOS_ROOT}}/agents/**` (shipped agent definitions) | SYSTEM |
### USER
Private, user-owned data that ships in the user's own private repo and is mounted into the LifeOS tree via symlink or git submodule. USER files may contain anything — they are never inspected by the public release pipeline because they live outside the system tree.
-**Physical mount (post-Phase-G, 2026-05-22):** `~/.claude/LIFEOS/USER/` is a symlink to `~/.config/LIFEOS/USER/` (XDG-style data location). The actual git working tree for the user's private USER-data repo lives at `~/.config/LIFEOS/USER/` — the symlink at `~/.claude/LIFEOS/USER/` exists only so Claude Code's `@`-import resolver (which evaluates paths relative to `~/.claude/`) can reach identity / TELOS / config files at session start. Boundary semantics are unchanged: the "USER zone" still refers to anything under `~/.claude/LIFEOS/USER/**` logically; the only difference is that the bytes live at the XDG path on disk.
+**Physical mount (post-Phase-G, 2026-05-22):** `{{LIFEOS_DIR}}/USER/` is a symlink to `~/.config/LIFEOS/USER/` (XDG-style data location). The actual git working tree for the user's private USER-data repo lives at `~/.config/LIFEOS/USER/` — the symlink at `{{LIFEOS_DIR}}/USER/` exists only so Claude Code's `@`-import resolver (which evaluates paths relative to `~/.claude/`) can reach identity / TELOS / config files at session start. Boundary semantics are unchanged: the "USER zone" still refers to anything under `{{LIFEOS_DIR}}/USER/**` logically; the only difference is that the bytes live at the XDG path on disk.
| Path | Status |
|------|--------|
-| `~/.claude/.env`, `.env.*` | USER (secrets) |
-| `~/.claude/LIFEOS/USER/**` (symlink → `~/.config/LIFEOS/USER/**`) | USER (identity, TELOS, projects, integrations, contacts, finances, health, business, customizations) |
-| `~/.claude/LIFEOS/MEMORY/**` (symlink → `~/.config/LIFEOS/USER/MEMORY/**`, post-Phase-G.2, 2026-05-23) | USER (work history, knowledge graph, learning signals, observability logs, research, reflections, relationships). Durable subset (KNOWLEDGE, WORK//ISA.md, RELATIONSHIP, WISDOM, PLANS, RESEARCH, STATE/work.json, BOOKMARKS, REFERENCE, SKILLS, PROJECT, TEAMS, SYSTEMUPDATES, VERIFICATION) is git-tracked in the user's private USER-data repo; ephemeral subset (OBSERVABILITY JSONLs, _BROWSER_STATE, LEARNING signals, SECURITY artifacts, VOICE event log, STATE caches, _AIRGRADIENT, _NETWORK, _HELIOS, PULSE_DATA, SCRATCHPAD, RAW, AUTO, CALLS, INBOX, ARCHIVE, DATA, WORK//* intermediates) gitignored from the private repo, local-only. |
-| `~/.claude/LIFEOS/ARBOL/**` | USER (private cloud worker code) |
-| `~/.claude/LIFEOS/Backups/**` | USER (backup state) |
-| `~/.claude/skills/_/**` (underscore-prefixed) | USER (private/proprietary skills) |
-| `~/.claude/daemon/`, `daemon.log` | USER (private daemon profile state) |
-| `~/.claude/jobs/**` (user-instance scheduled tasks) | USER |
-| `~/.claude/downloads/**` | USER (working files) |
-| `~/.claude/MEMORY/**` (root-level orphan; should migrate to `LIFEOS/MEMORY/`) | USER (orphan — Phase A cleanup) |
+| `{{LIFEOS_ROOT}}/.env`, `.env.*` | USER (secrets) |
+| `{{LIFEOS_DIR}}/USER/**` (symlink → `~/.config/LIFEOS/USER/**`) | USER (identity, TELOS, projects, integrations, contacts, finances, health, business, customizations) |
+| `{{LIFEOS_DIR}}/MEMORY/**` (symlink → `~/.config/LIFEOS/USER/MEMORY/**`, post-Phase-G.2, 2026-05-23) | USER (work history, knowledge graph, learning signals, observability logs, research, reflections, relationships). Durable subset (KNOWLEDGE, WORK//ISA.md, RELATIONSHIP, WISDOM, PLANS, RESEARCH, STATE/work.json, BOOKMARKS, REFERENCE, SKILLS, PROJECT, TEAMS, SYSTEMUPDATES, VERIFICATION) is git-tracked in the user's private USER-data repo; ephemeral subset (OBSERVABILITY JSONLs, _BROWSER_STATE, LEARNING signals, SECURITY artifacts, VOICE event log, STATE caches, _AIRGRADIENT, _NETWORK, _HELIOS, PULSE_DATA, SCRATCHPAD, RAW, AUTO, CALLS, INBOX, ARCHIVE, DATA, WORK//* intermediates) gitignored from the private repo, local-only. |
+| `{{LIFEOS_DIR}}/ARBOL/**` | USER (private cloud worker code) |
+| `{{LIFEOS_DIR}}/Backups/**` | USER (backup state) |
+| `{{LIFEOS_ROOT}}/skills/_/**` (underscore-prefixed) | USER (private/proprietary skills) |
+| `{{LIFEOS_ROOT}}/daemon/`, `daemon.log` | USER (private daemon profile state) |
+| `{{LIFEOS_ROOT}}/jobs/**` (user-instance scheduled tasks) | USER |
+| `{{LIFEOS_ROOT}}/downloads/**` | USER (working files) |
+| `{{LIFEOS_ROOT}}/MEMORY/**` (root-level orphan; should migrate to `LIFEOS/MEMORY/`) | USER (orphan — Phase A cleanup) |
### INTERFACE
@@ -69,11 +69,11 @@ The boundary itself — the documented contract by which SYSTEM code reads USER
| Path | Status |
|------|--------|
-| `~/.claude/LIFEOS/TOOLS/LifeosConfig.ts` | INTERFACE — typed loader, ships in SYSTEM (after Phase F) |
-| `~/.claude/LIFEOS/USER/CONFIG/LIFEOS_CONFIG.{json\|yaml\|toml}` | INTERFACE — user-supplied implementation (after Phase F; format pending ISC-56.1) |
-| `~/.claude/LIFEOS/USER/CONFIG/settings.user.json` | INTERFACE — user settings overlay (after Phase B) |
-| `~/.claude/LIFEOS/USER/CONFIG/OPERATIONAL_RULES.md` | INTERFACE — user-specific operational rules (after Phase C); @-imported directly from `~/.claude/CLAUDE.md` since CC does not follow transitive @-imports |
-| `~/.claude/LIFEOS/USER/INTEGRATIONS/*.yaml` | INTERFACE — typed config for integrations (homebridge, unifi, airgradient, work-system, etc.) |
+| `{{LIFEOS_DIR}}/TOOLS/LifeosConfig.ts` | INTERFACE — typed loader, ships in SYSTEM (after Phase F) |
+| `{{LIFEOS_DIR}}/USER/CONFIG/LIFEOS_CONFIG.{json\|yaml\|toml}` | INTERFACE — user-supplied implementation (after Phase F; format pending ISC-56.1) |
+| `{{LIFEOS_DIR}}/USER/CONFIG/settings.user.json` | INTERFACE — user settings overlay (after Phase B) |
+| `{{LIFEOS_DIR}}/USER/CONFIG/OPERATIONAL_RULES.md` | INTERFACE — user-specific operational rules (after Phase C); @-imported directly from `{{LIFEOS_ROOT}}/CLAUDE.md` since CC does not follow transitive @-imports |
+| `{{LIFEOS_DIR}}/USER/INTEGRATIONS/*.yaml` | INTERFACE — typed config for integrations (homebridge, unifi, airgradient, work-system, etc.) |
### RUNTIME-STATE
@@ -81,15 +81,15 @@ Harness-owned or LifeOS-runtime-owned ephemeral files. Never shipped, never user
| Path | Status | Gitignored |
|------|--------|------------|
-| `~/.claude/sessions/`, `todos/`, `tasks/`, `teams/` | RUNTIME (Claude Code harness) | yes |
-| `~/.claude/history.jsonl` | RUNTIME (Claude Code) | yes (under root anchors) |
-| `~/.claude/cache/`, `shell-snapshots/`, `session-env/`, `paste-cache/`, `file-history/` | RUNTIME (harness/LifeOS) | yes |
-| `~/.claude/.next/`, `.wrangler/`, `.venv/`, `coverage/`, `test-results/`, `telemetry/` | RUNTIME (build/test) | partial — telemetry/ is NOT gitignored and contains identity-bearing data; add to .gitignore in Phase A |
-| `~/.claude/plugins/`, `Plugins/` | RUNTIME (Claude Code plugin install state) | partial |
-| `~/.claude/ide/` | RUNTIME (Claude Code IDE state) | implied |
-| `~/.claude/.DS_Store`, `.last-cleanup`, `.quote-cache` | RUNTIME (OS / harness) | yes |
-| `~/.claude/interceptor-screenshot-*.{png,jpg}` (root) | RUNTIME (debug captures) | yes |
-| `~/.claude/LIFEOS/PULSE/{state,logs,Assistant/state,.playwright-cli,Observability/out}/**` | RUNTIME (Pulse runtime) | implied |
+| `{{LIFEOS_ROOT}}/sessions/`, `todos/`, `tasks/`, `teams/` | RUNTIME (Claude Code harness) | yes |
+| `{{LIFEOS_ROOT}}/history.jsonl` | RUNTIME (Claude Code) | yes (under root anchors) |
+| `{{LIFEOS_ROOT}}/cache/`, `shell-snapshots/`, `session-env/`, `paste-cache/`, `file-history/` | RUNTIME (harness/LifeOS) | yes |
+| `{{LIFEOS_ROOT}}/.next/`, `.wrangler/`, `.venv/`, `coverage/`, `test-results/`, `telemetry/` | RUNTIME (build/test) | partial — telemetry/ is NOT gitignored and contains identity-bearing data; add to .gitignore in Phase A |
+| `{{LIFEOS_ROOT}}/plugins/`, `Plugins/` | RUNTIME (Claude Code plugin install state) | partial |
+| `{{LIFEOS_ROOT}}/ide/` | RUNTIME (Claude Code IDE state) | implied |
+| `{{LIFEOS_ROOT}}/.DS_Store`, `.last-cleanup`, `.quote-cache` | RUNTIME (OS / harness) | yes |
+| `{{LIFEOS_ROOT}}/interceptor-screenshot-*.{png,jpg}` (root) | RUNTIME (debug captures) | yes |
+| `{{LIFEOS_DIR}}/PULSE/{state,logs,Assistant/state,.playwright-cli,Observability/out}/**` | RUNTIME (Pulse runtime) | implied |
## The four allowed access patterns
@@ -104,7 +104,7 @@ Anything else — direct `Read('LIFEOS/USER/...')`, hardcoded voice IDs in modul
## Two-repo sync (post-Phase-G.1, 2026-05-22)
-The USER tree is its own private git repo: `~/.config/LIFEOS/USER/` → the user's `/` (PRIVATE GitHub). The SYSTEM tree is `~/.claude/` → the user's `/.claude` (PRIVATE GitHub). A pre-push hook at `~/.claude/.git/hooks/pre-push` (1836 bytes) auto-commits and pushes the USER repo before every `git push` from `~/.claude/`, so the two repos stay in sync structurally. A workflow ("update the kai repo" / "push both repos") wraps this with four boundary gates: (G1) USER-zone leak check on pending `~/.claude` changes, (G2) `DenyListCheck.ts` must return 0 real-leaks, (G3) both remotes confirmed private via `gh api`, (G4) post-push HEAD verification on both repos. **Pre-flight refuses to proceed if the public LifeOS repo appears in either remote** — this workflow is explicit private-only. Public LifeOS release goes through the shadow-release pipeline (`skills/_LIFEOS/Tools/ShadowRelease.ts`) with the separate 14-gate sanitization; the shipped distribution unit is the single `LifeOS/` skill emitted from that staging tree, not the `.claude/` clone.
+The USER tree is its own private git repo: `~/.config/LIFEOS/USER/` → the user's `/` (PRIVATE GitHub). The SYSTEM tree is `~/.claude/` → the user's `/.claude` (PRIVATE GitHub). A pre-push hook at `{{LIFEOS_ROOT}}/.git/hooks/pre-push` (1836 bytes) auto-commits and pushes the USER repo before every `git push` from `~/.claude/`, so the two repos stay in sync structurally. A workflow ("update the kai repo" / "push both repos") wraps this with four boundary gates: (G1) USER-zone leak check on pending `~/.claude` changes, (G2) `DenyListCheck.ts` must return 0 real-leaks, (G3) both remotes confirmed private via `gh api`, (G4) post-push HEAD verification on both repos. **Pre-flight refuses to proceed if the public LifeOS repo appears in either remote** — this workflow is explicit private-only. Public LifeOS release goes through the shadow-release pipeline (`skills/_LIFEOS/Tools/ShadowRelease.ts`) with the separate 14-gate sanitization; the shipped distribution unit is the single `LifeOS/` skill emitted from that staging tree, not the `.claude/` clone.
## Enforcement layers
@@ -131,7 +131,7 @@ This document is the Phase A deliverable. Phases B–H land progressively. Each
## The boundary's success criterion
-The deny-list precheck `bun ~/.claude/skills/_LIFEOS/Tools/DenyListCheck.ts ~/.claude` returns:
+The deny-list precheck `bun "${LIFEOS_ROOT}/skills/_LIFEOS/Tools/DenyListCheck.ts" "${LIFEOS_ROOT}"` returns:
```
Real-leak: 0
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Testing/TestingDoctrine.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Testing/TestingDoctrine.md
index 358f76e591..0172dcbb12 100644
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Testing/TestingDoctrine.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Testing/TestingDoctrine.md
@@ -11,9 +11,9 @@ version: 1.0.24
> Testing is how the Life OS knows what it knows (`LIFEOS/DOCUMENTATION/LifeOs/LifeOsThesis.md`): the hill-climb only counts when each step is verified, and `bun test` is the canonical probe behind every ISC claim. An unverified gap-closure is a guess wearing a checkmark.
-> **TL;DR.** Tests for LifeOS run on `bun test`. The shared harness lives at `~/.claude/test/harness.ts` and exports zero-external-dep helpers (`paiTestEnv`, `tempDir`, `claudeFixture`, platform predicates, custom matchers). Tests live in a parallel `~/.claude/test/` tree that mirrors the source API surface — *not* co-located. Coverage is corpus-based: every documented hook, skill workflow, and tool surface gets at least one test file. No retries, no hardcoded ports, no time-based waits, no per-test timeouts. The ISA's `## Test Strategy` `tool: bun test path/to/foo.test.ts` is the canonical bridge from criterion to probe.
+> **TL;DR.** Tests for LifeOS run on `bun test`. The shared harness lives at `{{LIFEOS_ROOT}}/test/harness.ts` and exports zero-external-dep helpers (`paiTestEnv`, `tempDir`, `claudeFixture`, platform predicates, custom matchers). Tests live in a parallel `{{LIFEOS_ROOT}}/test/` tree that mirrors the source API surface — *not* co-located. Coverage is corpus-based: every documented hook, skill workflow, and tool surface gets at least one test file. No retries, no hardcoded ports, no time-based waits, no per-test timeouts. The ISA's `## Test Strategy` `tool: bun test path/to/foo.test.ts` is the canonical bridge from criterion to probe.
-This document is the result of a deep analysis of how the Bun project itself (`oven-sh/bun`) achieves its reputation for thorough test coverage and harness discipline, mapped onto LifeOS's TypeScript surface. Citations to Bun source are inline; the full research lives at `~/.claude/LIFEOS/MEMORY/WORK/20260507-bun-testing-doctrine-analysis/ISA.md`.
+This document is the result of a deep analysis of how the Bun project itself (`oven-sh/bun`) achieves its reputation for thorough test coverage and harness discipline, mapped onto LifeOS's TypeScript surface. Citations to Bun source are inline; the full research lives at `{{LIFEOS_DIR}}/MEMORY/WORK/20260507-bun-testing-doctrine-analysis/ISA.md`.
---
@@ -29,7 +29,7 @@ The ISA already declares the test surface (Algorithm v6.3.0 doctrine: *"the ISA
| Pattern | Bun source | LifeOS adoption |
|---------|-----------|--------------|
-| **Single shared harness, zero external deps** | `test/harness.ts` (1985 lines, 98 exports, header comment forbids external deps) | `~/.claude/test/harness.ts` |
+| **Single shared harness, zero external deps** | `test/harness.ts` (1985 lines, 98 exports, header comment forbids external deps) | `{{LIFEOS_ROOT}}/test/harness.ts` |
| **Parallel `test/` tree mirroring API surface, not co-location** | `src/bun.js/api/spawn.zig` → `test/js/bun/spawn/*.test.ts` | `hooks/.hook.ts` → `test/hooks/.hook.test.ts` |
| **Subprocess testing for CLI behavior** | 591 of 1526 test files spawn `bun` | `Inference.ts`, hook handlers tested via `Bun.spawn` (the former `TheRouter.hook.ts` suite was removed with the hook, 2026-07-11) |
| **`tempDir` with `DisposableString`** | `test/harness.ts:263-294` | `paiTempDir(prefix, fileMap)` returns `using`-disposable |
@@ -60,11 +60,11 @@ The ISA already declares the test surface (Algorithm v6.3.0 doctrine: *"the ISA
These are non-negotiable. Violations get caught at the ISA `## Test Strategy` review or at the VERIFY-phase Forge cross-vendor audit.
-### 1. The harness is `~/.claude/test/harness.ts`. Zero external deps.
+### 1. The harness is `{{LIFEOS_ROOT}}/test/harness.ts`. Zero external deps.
It must be importable before `bun install` runs. It depends only on Bun built-ins (`Bun`, `bun:test`, `node:fs`, `node:path`, `node:os`). Adding a dependency to `package.json` for the harness is a CRITICAL FAILURE.
-### 2. Tests live at `~/.claude/test//.test.ts`.
+### 2. Tests live at `{{LIFEOS_ROOT}}/test//.test.ts`.
Mirror the source path, not co-locate. Map:
@@ -226,7 +226,7 @@ This is strictly stronger than the example form — it catches `ANTHROPIC_API_KE
## bunfig.toml
-LifeOS's test config lives at `~/.claude/bunfig.toml` `[test]` section:
+LifeOS's test config lives at `{{LIFEOS_ROOT}}/bunfig.toml` `[test]` section:
```toml
[test]
@@ -250,7 +250,7 @@ coveragePathIgnorePatterns = [
# coverageThreshold = { line = 0.8, function = 0.9, statement = 0.85 }
```
-`bun test` from `~/.claude/` walks `test/` and runs every `*.test.ts`.
+`bun test` from `{{LIFEOS_ROOT}}/` walks `test/` and runs every `*.test.ts`.
---
@@ -317,7 +317,7 @@ Phases 2–5 are tracked separately as PROJECTS.md entries; they're not part of
- ❌ Adding a `--retry 3` flag to any test invocation in CI. Banned.
- ❌ Hardcoding port `31337` in a Pulse test. Use `port: 0` and capture.
- ❌ `await Bun.sleep(2000)` to "let the hook finish." Wait on the actual signal.
-- ❌ Reading real `~/.claude/LIFEOS/USER/PRINCIPAL/PRINCIPAL_IDENTITY.md` from a test. Use a fixture.
+- ❌ Reading real `{{LIFEOS_DIR}}/USER/PRINCIPAL/PRINCIPAL_IDENTITY.md` from a test. Use a fixture.
- ❌ Spawning real `claude` subprocess in a test. Use a fixture-replay subprocess.
- ❌ Inventing an `acceptance.yaml` or `acceptance.ts` parallel to the ISA. The ISA IS the test harness.
@@ -325,9 +325,9 @@ Phases 2–5 are tracked separately as PROJECTS.md entries; they're not part of
## Cross-references
-- Algorithm doctrine on ISA-as-test-harness: `~/.claude/LIFEOS/ALGORITHM/v8.4.0.md` § Doctrine (line 17)
-- ISA format spec: `~/.claude/LIFEOS/DOCUMENTATION/Isa/IsaFormat.md`
+- Algorithm doctrine on ISA-as-test-harness: `{{LIFEOS_DIR}}/ALGORITHM/v8.4.0.md` § Doctrine (line 17)
+- ISA format spec: `{{LIFEOS_DIR}}/DOCUMENTATION/Isa/IsaFormat.md`
- Bun test docs: , , , , ,
- Bun's own test doctrine:
- Bun harness source:
-- Working scaffold (this ISA's deliverable): `~/.claude/test/harness.ts`, `~/.claude/test/tools/Inference.test.ts`, `~/.claude/bunfig.toml`
+- Working scaffold (this ISA's deliverable): `{{LIFEOS_ROOT}}/test/harness.ts`, `{{LIFEOS_ROOT}}/test/tools/Inference.test.ts`, `{{LIFEOS_ROOT}}/bunfig.toml`
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Tools/Cli.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Tools/Cli.md
index f1f9b4577b..5e4ef4f19c 100644
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Tools/Cli.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Tools/Cli.md
@@ -19,7 +19,7 @@ Both tools use `bun` as their runtime.
## The Algorithm CLI
-**Location:** `~/.claude/LIFEOS/TOOLS/algorithm.ts`
+**Location:** `{{LIFEOS_DIR}}/TOOLS/algorithm.ts`
The Algorithm CLI executes the LifeOS Algorithm (Observe → Think → Plan → Build → Execute → Verify → Learn) against ISA files. It supports three modes: autonomous loop execution (no human needed), interactive sessions (human-in-the-loop), and optimize (autonomous hill-climbing against a measurable metric).
@@ -27,13 +27,13 @@ The Algorithm CLI executes the LifeOS Algorithm (Observe → Think → Plan →
```bash
# Run the Algorithm in autonomous loop mode
-bun ~/.claude/LIFEOS/TOOLS/algorithm.ts -m loop -p -n 20
+bun "${LIFEOS_DIR}/TOOLS/algorithm.ts" -m loop -p -n 20
# Run in interactive mode (launches a claude session with ISA context)
-bun ~/.claude/LIFEOS/TOOLS/algorithm.ts -m interactive -p
+bun "${LIFEOS_DIR}/TOOLS/algorithm.ts" -m interactive -p
# Run in optimize mode (launches claude with /optimize arguments)
-bun ~/.claude/LIFEOS/TOOLS/algorithm.ts -m optimize -p
+bun "${LIFEOS_DIR}/TOOLS/algorithm.ts" -m optimize -p
# Or use /optimize directly inside any claude session:
/optimize --metric "bundle_size" --lower-is-better \
@@ -41,7 +41,7 @@ bun ~/.claude/LIFEOS/TOOLS/algorithm.ts -m optimize -p
--files "src/**/*.ts"
# Check status of all ISAs
-bun ~/.claude/LIFEOS/TOOLS/algorithm.ts status
+bun "${LIFEOS_DIR}/TOOLS/algorithm.ts" status
```
### Usage
@@ -78,13 +78,13 @@ Loop mode runs the Algorithm iteratively without human interaction. Each iterati
```bash
# Basic loop — single agent, up to 128 iterations
-bun ~/.claude/LIFEOS/TOOLS/algorithm.ts -m loop -p ISA-20260213-auth.md
+bun "${LIFEOS_DIR}/TOOLS/algorithm.ts" -m loop -p ISA-20260213-auth.md
# Fast loop — 20 max iterations
-bun ~/.claude/LIFEOS/TOOLS/algorithm.ts -m loop -p ISA-20260213-auth.md -n 20
+bun "${LIFEOS_DIR}/TOOLS/algorithm.ts" -m loop -p ISA-20260213-auth.md -n 20
# Parallel loop — 4 agents working on different criteria simultaneously
-bun ~/.claude/LIFEOS/TOOLS/algorithm.ts -m loop -p ISA-20260213-auth.md -n 20 -a 4
+bun "${LIFEOS_DIR}/TOOLS/algorithm.ts" -m loop -p ISA-20260213-auth.md -n 20 -a 4
```
**Parallel agents (`-a N`):** When N > 1, the CLI partitions failing criteria across N agents. Each agent receives exactly one criterion and operates as a focused worker. The CLI uses domain-aware partitioning — criteria from the same domain (e.g., `ISC-AUTH-1`, `ISC-AUTH-2`) are assigned to the same agent to avoid conflicts. After all agents complete, the parent process reconciles results into the ISA.
@@ -105,7 +105,7 @@ bun ~/.claude/LIFEOS/TOOLS/algorithm.ts -m loop -p ISA-20260213-auth.md -n 20 -a
Interactive mode launches a full `claude` session with the ISA context pre-loaded. You work with Claude directly to make progress on criteria.
```bash
-bun ~/.claude/LIFEOS/TOOLS/algorithm.ts -m interactive -p ISA-20260213-feature.md
+bun "${LIFEOS_DIR}/TOOLS/algorithm.ts" -m interactive -p ISA-20260213-feature.md
```
This opens an interactive Claude session with:
@@ -118,19 +118,19 @@ This opens an interactive Claude session with:
```bash
# Show all ISAs and their status
-bun ~/.claude/LIFEOS/TOOLS/algorithm.ts status
+bun "${LIFEOS_DIR}/TOOLS/algorithm.ts" status
# Show status of a specific ISA
-bun ~/.claude/LIFEOS/TOOLS/algorithm.ts status -p ISA-20260213-auth
+bun "${LIFEOS_DIR}/TOOLS/algorithm.ts" status -p ISA-20260213-auth
# Pause a running loop (loop checks between iterations)
-bun ~/.claude/LIFEOS/TOOLS/algorithm.ts pause -p ISA-20260213-auth
+bun "${LIFEOS_DIR}/TOOLS/algorithm.ts" pause -p ISA-20260213-auth
# Resume a paused loop
-bun ~/.claude/LIFEOS/TOOLS/algorithm.ts resume -p ISA-20260213-auth
+bun "${LIFEOS_DIR}/TOOLS/algorithm.ts" resume -p ISA-20260213-auth
# Stop a loop permanently
-bun ~/.claude/LIFEOS/TOOLS/algorithm.ts stop -p ISA-20260213-auth
+bun "${LIFEOS_DIR}/TOOLS/algorithm.ts" stop -p ISA-20260213-auth
```
### ISA Resolution
@@ -199,14 +199,14 @@ Loop mode displays a live progress dashboard:
## The Arbol CLI (pai)
-**Location:** `~/.claude/LIFEOS/ARBOL/Actions/lifeos.ts`
+**Location:** `{{LIFEOS_DIR}}/ARBOL/Actions/lifeos.ts`
The Arbol CLI (`pai`) provides a unified interface for running actions and pipelines locally. It supports JSON input via arguments, stdin piping, and UNIX-style action composition.
### Quick Start
```bash
-cd ~/.claude/LIFEOS/ARBOL/Actions
+cd "${LIFEOS_DIR}/ARBOL/Actions"
# Run an action with inline JSON
bun lifeos.ts action A_EXAMPLE_SUMMARIZE --input '{"content": "Your text here"}'
@@ -279,12 +279,12 @@ bun lifeos.ts action A_EXAMPLE_SUMMARIZE --input '{"content": "test"}' -v
## The Arbol Runner (Low-Level)
-**Location:** `~/.claude/LIFEOS/ARBOL/Actions/lib/runner.v2.ts`
+**Location:** `{{LIFEOS_DIR}}/ARBOL/Actions/lib/runner.v2.ts`
The runner is the lower-level engine that the `pai` CLI and pipeline runner both use. You can call it directly:
```bash
-cd ~/.claude/LIFEOS/ARBOL/Actions
+cd "${LIFEOS_DIR}/ARBOL/Actions"
# Run an action (input as JSON argument)
bun lib/runner.v2.ts run A_EXAMPLE_SUMMARIZE '{"content": "Your text here"}'
@@ -314,12 +314,12 @@ bun lib/runner.v2.ts list
## The Pipeline Runner
-**Location:** `~/.claude/LIFEOS/ARBOL/Actions/lib/pipeline-runner.ts`
+**Location:** `{{LIFEOS_DIR}}/ARBOL/Actions/lib/pipeline-runner.ts`
The pipeline runner loads YAML pipeline definitions and chains actions sequentially.
```bash
-cd ~/.claude/LIFEOS/ARBOL/Actions
+cd "${LIFEOS_DIR}/ARBOL/Actions"
# Run a pipeline with named parameters
bun lib/pipeline-runner.ts run P_EXAMPLE_SUMMARIZE_AND_FORMAT --content "Your text here"
@@ -364,14 +364,14 @@ For convenience, add aliases to your shell configuration (`.zshrc`, `.bashrc`):
```bash
# The Algorithm CLI
-alias algorithm="bun ~/.claude/LIFEOS/TOOLS/algorithm.ts"
+alias algorithm='bun "${LIFEOS_DIR}/TOOLS/algorithm.ts"'
# The Arbol CLI
-alias pai="bun ~/.claude/LIFEOS/ARBOL/Actions/lifeos.ts"
+alias pai='bun "${LIFEOS_DIR}/ARBOL/Actions/lifeos.ts"'
# Runners (optional — pai CLI wraps these)
-alias arbol-run="bun ~/.claude/LIFEOS/ARBOL/Actions/lib/runner.v2.ts"
-alias arbol-pipe="bun ~/.claude/LIFEOS/ARBOL/Actions/lib/pipeline-runner.ts"
+alias arbol-run='bun "${LIFEOS_DIR}/ARBOL/Actions/lib/runner.v2.ts"'
+alias arbol-pipe='bun "${LIFEOS_DIR}/ARBOL/Actions/lib/pipeline-runner.ts"'
```
Then use:
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Tools/CliFirstArchitecture.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Tools/CliFirstArchitecture.md
index 4942ab8c96..7fd8ee8b00 100755
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Tools/CliFirstArchitecture.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Tools/CliFirstArchitecture.md
@@ -596,7 +596,7 @@ AI should orchestrate deterministic tools, not replace them with ad-hoc promptin
## Related Documentation
-- **Architecture**: `~/.claude/LIFEOS/DOCUMENTATION/LifeosSystemArchitecture.md`
+- **Architecture**: `{{LIFEOS_DIR}}/DOCUMENTATION/LifeosSystemArchitecture.md`
---
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Tools/Containment.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Tools/Containment.md
index 8d983c2013..6965f7e91f 100644
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Tools/Containment.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Tools/Containment.md
@@ -45,7 +45,7 @@ The underscore-prefix rule for `private-skills` is the interface contract. If a
Zones drift. Before running `ShadowRelease --create `:
1. Open `hooks/lib/containment-zones.ts`.
-2. Walk `~/.claude/` at depth 1-2 (e.g. `ls -la && ls -la LIFEOS/ && ls -la skills/`) and compare against the zone list.
+2. Walk `{{LIFEOS_ROOT}}/` at depth 1-2 (e.g. `ls -la && ls -la LIFEOS/ && ls -la skills/`) and compare against the zone list.
3. Ask, for every new top-level or first-nested dir since the last release:
- Does it contain anything principal-specific? → **Add a zone or extend an existing one.**
- Is it runtime state the harness writes? → **Add it to `install-state` or the RSYNC_EXCLUDES in `ShadowRelease.ts`.**
@@ -75,13 +75,13 @@ The concrete patterns live in `skills/_LIFEOS/Tools/ShadowRelease.ts` (`IDENTITY
### I am writing a new file and it needs to reference the principal
-Use `${HOME}`, `${LIFEOS_DIR}`, `${LIFEOS_DIR}`, or a configurable placeholder. Never hard-code absolute paths containing the principal's username in a public file.
+Use `${HOME}`, `{{LIFEOS_DIR}}`, `{{LIFEOS_DIR}}`, or a configurable placeholder. Never hard-code absolute paths containing the principal's username in a public file.
### I am writing a new file and it needs secrets
1. Load from `process.env.X` at runtime.
2. Document the var name in the file itself, no default value that contains the secret.
-3. Fallback path: read from `~/.claude/.env` directly (file is the canonical env source; `LIFEOS/.env` and `~/.config/LIFEOS/.env` are symlinks to it). Use Node `fs.readFileSync` + a small parser, not a shared helper — no central env helper exists by design.
+3. Fallback path: read from `{{LIFEOS_ROOT}}/.env` directly (file is the canonical env source; `LIFEOS/.env` and `~/.config/LIFEOS/.env` are symlinks to it). Use Node `fs.readFileSync` + a small parser, not a shared helper — no central env helper exists by design.
4. If the secret lookup misses, emit a single stderr warning and degrade gracefully — never silently continue with an empty string.
### I am adding personal notes, work sessions, or memory
@@ -162,7 +162,7 @@ Populated by the audit. Updated as files are sanitized or relocated.
| `LIFEOS/TOOLS/SessionHarvester.ts` | Comment references derivation, not literal path | **KEEP** — uses `CLAUDE_DIR.replace(...)` dynamically |
| `LIFEOS/TOOLS/gmail.ts` | Uses `homedir()` at runtime, not a literal path | **KEEP** — dynamic resolution |
| `LIFEOS/PULSE/checks/health.ts` | Hardcoded site list for health monitoring | **TODO-REFACTOR** — move site list to `LIFEOS_CONFIG.yaml`, read at startup |
-| `agents/.md` | Write-permission path literals in agent definitions | **TODO-REFACTOR** — verify env-expansion support in Claude Code agent spec, then replace with `${HOME}/.claude/...` |
+| `agents/.md` | Write-permission path literals in agent definitions | **TODO-REFACTOR** — verify env-expansion support in Claude Code agent spec, then replace with `{{LIFEOS_ROOT}}/...` |
---
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Tools/Tools.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Tools/Tools.md
index 3e33ad626c..aec2fa118f 100755
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Tools/Tools.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Tools/Tools.md
@@ -16,7 +16,7 @@ This file documents single-purpose CLI utilities that have been consolidated fro
## Inference.ts - Unified AI Inference Tool
-**Location:** `~/.claude/LIFEOS/TOOLS/Inference.ts`
+**Location:** `{{LIFEOS_DIR}}/TOOLS/Inference.ts`
**Use this — never import `@anthropic-ai/sdk` directly.** Inference.ts handles auth, retries, timeouts, and LifeOS-specific defaults. Hooks, skills, agents, and ad-hoc Bash all route through it.
@@ -27,22 +27,22 @@ Single inference tool with four run levels for different speed/capability trade-
**Usage:**
```bash
# Low (Haiku) - quick tasks, simple generation
-bun ~/.claude/LIFEOS/TOOLS/Inference.ts --level low "System prompt" "User prompt"
+bun "${LIFEOS_DIR}/TOOLS/Inference.ts" --level low "System prompt" "User prompt"
# Medium (Sonnet) - balanced reasoning, typical analysis
-bun ~/.claude/LIFEOS/TOOLS/Inference.ts --level medium "System prompt" "User prompt"
+bun "${LIFEOS_DIR}/TOOLS/Inference.ts" --level medium "System prompt" "User prompt"
# High (Opus) - deep reasoning, strategic decisions
-bun ~/.claude/LIFEOS/TOOLS/Inference.ts --level high "System prompt" "User prompt"
+bun "${LIFEOS_DIR}/TOOLS/Inference.ts" --level high "System prompt" "User prompt"
# Max (Fable) - hardest reasoning, top tier
-bun ~/.claude/LIFEOS/TOOLS/Inference.ts --level max "System prompt" "User prompt"
+bun "${LIFEOS_DIR}/TOOLS/Inference.ts" --level max "System prompt" "User prompt"
# With JSON output
-bun ~/.claude/LIFEOS/TOOLS/Inference.ts --json --level low "Return JSON" "Input"
+bun "${LIFEOS_DIR}/TOOLS/Inference.ts" --json --level low "Return JSON" "Input"
# Custom timeout
-bun ~/.claude/LIFEOS/TOOLS/Inference.ts --level medium --timeout 60000 "Prompt" "Input"
+bun "${LIFEOS_DIR}/TOOLS/Inference.ts" --level medium --timeout 60000 "Prompt" "Input"
```
**Run Levels:**
@@ -55,7 +55,7 @@ bun ~/.claude/LIFEOS/TOOLS/Inference.ts --level medium --timeout 60000 "Prompt"
**Programmatic Usage:**
```typescript
-// From hooks (at ~/.claude/hooks/):
+// From hooks (at {{LIFEOS_ROOT}}/hooks/):
import { inference } from '../../.claude/LIFEOS/TOOLS/Inference';
const result = await inference({
@@ -88,20 +88,20 @@ if (result.success) {
## RemoveBg.ts - Remove Image Backgrounds
-**Location:** `~/.claude/LIFEOS/TOOLS/RemoveBg.ts`
+**Location:** `{{LIFEOS_DIR}}/TOOLS/RemoveBg.ts`
Remove backgrounds from images using local `rembg` (no external API).
**Usage:**
```bash
# Remove background from single image (overwrites; renames .jpg→.png)
-bun ~/.claude/LIFEOS/TOOLS/RemoveBg.ts /path/to/image.png
+bun "${LIFEOS_DIR}/TOOLS/RemoveBg.ts" /path/to/image.png
# Remove background and save to different path
-bun ~/.claude/LIFEOS/TOOLS/RemoveBg.ts /path/to/input.png /path/to/output.png
+bun "${LIFEOS_DIR}/TOOLS/RemoveBg.ts" /path/to/input.png /path/to/output.png
# Process multiple images
-bun ~/.claude/LIFEOS/TOOLS/RemoveBg.ts image1.png image2.png image3.png
+bun "${LIFEOS_DIR}/TOOLS/RemoveBg.ts" image1.png image2.png image3.png
```
**Requirements:**
@@ -117,17 +117,17 @@ bun ~/.claude/LIFEOS/TOOLS/RemoveBg.ts image1.png image2.png image3.png
## AddBg.ts - Add Background Color
-**Location:** `~/.claude/LIFEOS/TOOLS/AddBg.ts`
+**Location:** `{{LIFEOS_DIR}}/TOOLS/AddBg.ts`
Add solid background color to transparent images.
**Usage:**
```bash
# Add specific background color
-bun ~/.claude/LIFEOS/TOOLS/AddBg.ts /path/to/transparent.png "#EAE9DF" /path/to/output.png
+bun "${LIFEOS_DIR}/TOOLS/AddBg.ts" /path/to/transparent.png "#EAE9DF" /path/to/output.png
# Add your brand background color (uses the color from LifeOS config)
-bun ~/.claude/LIFEOS/TOOLS/AddBg.ts /path/to/transparent.png --brand /path/to/output.png
+bun "${LIFEOS_DIR}/TOOLS/AddBg.ts" /path/to/transparent.png --brand /path/to/output.png
```
**When to Use:**
@@ -141,17 +141,17 @@ bun ~/.claude/LIFEOS/TOOLS/AddBg.ts /path/to/transparent.png --brand /path/to/ou
## GetTranscript.ts - Extract YouTube Transcripts
-**Location:** `~/.claude/LIFEOS/TOOLS/GetTranscript.ts`
+**Location:** `{{LIFEOS_DIR}}/TOOLS/GetTranscript.ts`
Extract transcripts from YouTube videos using yt-dlp (via fabric).
**Usage:**
```bash
# Extract transcript to stdout
-bun ~/.claude/LIFEOS/TOOLS/GetTranscript.ts "https://www.youtube.com/watch?v=VIDEO_ID"
+bun "${LIFEOS_DIR}/TOOLS/GetTranscript.ts" "https://www.youtube.com/watch?v=VIDEO_ID"
# Save transcript to file
-bun ~/.claude/LIFEOS/TOOLS/GetTranscript.ts "https://www.youtube.com/watch?v=VIDEO_ID" --save /path/to/transcript.txt
+bun "${LIFEOS_DIR}/TOOLS/GetTranscript.ts" "https://www.youtube.com/watch?v=VIDEO_ID" --save /path/to/transcript.txt
```
**Supported URL Formats:**
@@ -175,23 +175,23 @@ bun ~/.claude/LIFEOS/TOOLS/GetTranscript.ts "https://www.youtube.com/watch?v=VID
## MemoryRetriever.ts - Compressed Knowledge Retrieval
-**Location:** `~/.claude/LIFEOS/TOOLS/MemoryRetriever.ts`
+**Location:** `{{LIFEOS_DIR}}/TOOLS/MemoryRetriever.ts`
BM25-lite search across the Knowledge Archive with optional LLM compression. Finds relevant notes by keyword matching, tag co-occurrence, and content frequency, then compresses results into a dense context-efficient summary.
**Usage:**
```bash
# Search and return compressed results (default: top 3)
-bun ~/.claude/LIFEOS/TOOLS/MemoryRetriever.ts "memory architecture"
+bun "${LIFEOS_DIR}/TOOLS/MemoryRetriever.ts" "memory architecture"
# Return top 5 results
-bun ~/.claude/LIFEOS/TOOLS/MemoryRetriever.ts "security policy" --top 5
+bun "${LIFEOS_DIR}/TOOLS/MemoryRetriever.ts" "security policy" --top 5
# Skip LLM compression, return raw excerpts
-bun ~/.claude/LIFEOS/TOOLS/MemoryRetriever.ts "karpathy" --raw
+bun "${LIFEOS_DIR}/TOOLS/MemoryRetriever.ts" "karpathy" --raw
# Custom token budget for output
-bun ~/.claude/LIFEOS/TOOLS/MemoryRetriever.ts "threat model" --budget 800
+bun "${LIFEOS_DIR}/TOOLS/MemoryRetriever.ts" "threat model" --budget 800
```
**Scoring:**
@@ -212,29 +212,29 @@ bun ~/.claude/LIFEOS/TOOLS/MemoryRetriever.ts "threat model" --budget 800
## KnowledgeGraph.ts - Associative Knowledge Navigation
-**Location:** `~/.claude/LIFEOS/TOOLS/KnowledgeGraph.ts`
+**Location:** `{{LIFEOS_DIR}}/TOOLS/KnowledgeGraph.ts`
Builds an in-memory graph from Knowledge Archive frontmatter (tags, wikilinks, related fields) and enables BFS traversal for associative recall. No persistent storage — computed from existing markdown files at query time.
**Usage:**
```bash
# BFS traversal from a note (default: 2 hops)
-bun ~/.claude/LIFEOS/TOOLS/KnowledgeGraph.ts traverse karpathy
+bun "${LIFEOS_DIR}/TOOLS/KnowledgeGraph.ts" traverse karpathy
# Traverse with custom depth
-bun ~/.claude/LIFEOS/TOOLS/KnowledgeGraph.ts traverse mempalace --hops 3
+bun "${LIFEOS_DIR}/TOOLS/KnowledgeGraph.ts" traverse mempalace --hops 3
# Show directly connected notes
-bun ~/.claude/LIFEOS/TOOLS/KnowledgeGraph.ts related mempalace
+bun "${LIFEOS_DIR}/TOOLS/KnowledgeGraph.ts" related mempalace
# Graph summary: nodes, edges, clusters
-bun ~/.claude/LIFEOS/TOOLS/KnowledgeGraph.ts stats
+bun "${LIFEOS_DIR}/TOOLS/KnowledgeGraph.ts" stats
# Top 10 most-connected notes
-bun ~/.claude/LIFEOS/TOOLS/KnowledgeGraph.ts hubs
+bun "${LIFEOS_DIR}/TOOLS/KnowledgeGraph.ts" hubs
# Find all notes with a specific tag
-bun ~/.claude/LIFEOS/TOOLS/KnowledgeGraph.ts find architecture
+bun "${LIFEOS_DIR}/TOOLS/KnowledgeGraph.ts" find architecture
```
**Edge Types:**
@@ -289,7 +289,7 @@ sleep 2
- "perform this"
**Technical Details:**
-- Pulse must be running (voice handler lives at `~/.claude/LIFEOS/PULSE/VoiceServer/voice.ts`, port 31337)
+- Pulse must be running (voice handler lives at `{{LIFEOS_DIR}}/PULSE/VoiceServer/voice.ts`, port 31337)
- Segments longer than 450 chars should be split
- Natural 2-second pauses between segments for storytelling flow
- Uses ElevenLabs API under the hood
@@ -298,14 +298,14 @@ sleep 2
## extract-transcript.py - Transcribe Audio/Video Files
-**Location:** `~/.claude/LIFEOS/TOOLS/extract-transcript.py`
+**Location:** `{{LIFEOS_DIR}}/TOOLS/extract-transcript.py`
Local transcription using faster-whisper (4x faster than OpenAI Whisper, 50% less memory). Self-contained UV script for offline transcription.
**Usage:**
```bash
# Transcribe single file (base.en model - recommended)
-cd ~/.claude/LIFEOS/TOOLS/
+cd "${LIFEOS_DIR}/TOOLS/"
uv run extract-transcript.py /path/to/audio.m4a
# Use different model
@@ -359,24 +359,24 @@ uv run extract-transcript.py /path/to/folder/ --batch --model base.en
## YouTubeApi.ts - YouTube Channel & Video Stats
-**Location:** `~/.claude/LIFEOS/TOOLS/YouTubeApi.ts`
+**Location:** `{{LIFEOS_DIR}}/TOOLS/YouTubeApi.ts`
Wrapper around YouTube Data API v3 for channel statistics and video metrics.
**Usage:**
```bash
# Get channel statistics
-bun ~/.claude/LIFEOS/TOOLS/YouTubeApi.ts --channel-stats
+bun "${LIFEOS_DIR}/TOOLS/YouTubeApi.ts" --channel-stats
# Get video statistics
-bun ~/.claude/LIFEOS/TOOLS/YouTubeApi.ts --video-stats VIDEO_ID
+bun "${LIFEOS_DIR}/TOOLS/YouTubeApi.ts" --video-stats VIDEO_ID
# Get latest uploads
-bun ~/.claude/LIFEOS/TOOLS/YouTubeApi.ts --latest-videos
+bun "${LIFEOS_DIR}/TOOLS/YouTubeApi.ts" --latest-videos
```
**Environment Variables:**
-- `YOUTUBE_API_KEY` - Required for API access (from `~/.claude/.env`)
+- `YOUTUBE_API_KEY` - Required for API access (from `{{LIFEOS_ROOT}}/.env`)
- `YOUTUBE_CHANNEL_ID` - Default channel ID
**When to Use:**
@@ -508,12 +508,12 @@ The Monitor tool starts a background script whose stdout lines become chat notif
The Pulse agent-guard hook automatically injects a watchdog reminder when background agents are spawned. The watchdog monitors tool-activity.jsonl for silence while agents are active:
-```bash
+```text
Monitor({
description: "Agent watchdog",
persistent: true,
timeout_ms: 3600000,
- command: "bun $HOME/.claude/LIFEOS/TOOLS/AgentWatchdog.ts"
+ command: "bun {{LIFEOS_DIR}}/TOOLS/AgentWatchdog.ts"
})
```
@@ -537,12 +537,12 @@ Monitor({
Watch Pulse daemon logs for errors during a debugging session:
-```bash
+```text
Monitor({
description: "Pulse error watcher",
persistent: true,
timeout_ms: 300000,
- command: "tail -f ~/.claude/Pulse/logs/pulse-stdout.log | grep --line-buffered -i -E '(error|fatal|crash|unhandled)'"
+ command: "tail -f {{LIFEOS_ROOT}}/Pulse/logs/pulse-stdout.log | grep --line-buffered -i -E '(error|fatal|crash|unhandled)'"
})
```
@@ -550,7 +550,7 @@ Monitor({
Monitor a long test suite and get notified on failures:
-```bash
+```text
Monitor({
description: "Test failure watcher",
persistent: false,
@@ -563,7 +563,7 @@ Monitor({
Poll GitHub for CI status changes on a PR:
-```bash
+```text
Monitor({
description: "CI status for PR #42",
persistent: true,
@@ -576,7 +576,7 @@ Monitor({
Tail security scan results for critical findings:
-```bash
+```text
Monitor({
description: "Security scan critical findings",
persistent: false,
@@ -614,7 +614,7 @@ Monitor({
When adding a new utility tool to this system:
-1. **Add tool file:** Place `.ts` or `.py` file directly in `~/.claude/LIFEOS/TOOLS/`
+1. **Add tool file:** Place `.ts` or `.py` file directly in `{{LIFEOS_DIR}}/TOOLS/`
- Use **Title Case** for filenames (e.g., `GetTranscript.ts`, not `get-transcript.ts`)
- Keep the directory flat - NO subdirectories
@@ -649,7 +649,7 @@ Archived skill files have been removed.
## algorithm.ts - The Algorithm CLI
-**Location:** `~/.claude/LIFEOS/TOOLS/algorithm.ts`
+**Location:** `{{LIFEOS_DIR}}/TOOLS/algorithm.ts`
Run the LifeOS Algorithm in Loop, Interactive, Ideate, or Optimize mode against a ISA.
@@ -688,13 +688,13 @@ algorithm resume -p # Resume a paused loop
algorithm stop -p # Stop a loop
```
-**Parameter schema:** `~/.claude/LIFEOS/ALGORITHM/parameter-schema.md`
+**Parameter schema:** `{{LIFEOS_DIR}}/ALGORITHM/parameter-schema.md`
---
## AlgorithmPhaseReport.ts - Algorithm State Reporter
-**Location:** `~/.claude/LIFEOS/TOOLS/AlgorithmPhaseReport.ts`
+**Location:** `{{LIFEOS_DIR}}/TOOLS/AlgorithmPhaseReport.ts`
Writes algorithm execution state to `algorithm-phase.json` for dashboard consumption.
@@ -724,26 +724,26 @@ bun AlgorithmPhaseReport.ts meta-adjust --param selectionPressure --from 0.3 --t
## KnowledgeHarvester.ts - Knowledge Archive Harvester
-**Location:** `~/.claude/LIFEOS/TOOLS/KnowledgeHarvester.ts`
+**Location:** `{{LIFEOS_DIR}}/TOOLS/KnowledgeHarvester.ts`
Validate and maintain the KNOWLEDGE/ archive (4 entity types: People, Companies, Ideas, Research). Validates against schemas in `_schema.md`, handles MOC regeneration and maintenance. Note: Algorithm LEARN phase writes knowledge directly; harvester reflections are disabled. The harvester's primary role is now validation, maintenance, and index regeneration.
**Usage:**
```bash
# Harvest from all sources
-bun ~/.claude/LIFEOS/TOOLS/KnowledgeHarvester.ts harvest
+bun "${LIFEOS_DIR}/TOOLS/KnowledgeHarvester.ts" harvest
# Harvest from specific source
-bun ~/.claude/LIFEOS/TOOLS/KnowledgeHarvester.ts harvest --source work
+bun "${LIFEOS_DIR}/TOOLS/KnowledgeHarvester.ts" harvest --source work
# Preview without writing
-bun ~/.claude/LIFEOS/TOOLS/KnowledgeHarvester.ts harvest --dry-run
+bun "${LIFEOS_DIR}/TOOLS/KnowledgeHarvester.ts" harvest --dry-run
# Archive health dashboard
-bun ~/.claude/LIFEOS/TOOLS/KnowledgeHarvester.ts status
+bun "${LIFEOS_DIR}/TOOLS/KnowledgeHarvester.ts" status
# Regenerate all MOC dashboards
-bun ~/.claude/LIFEOS/TOOLS/KnowledgeHarvester.ts index
+bun "${LIFEOS_DIR}/TOOLS/KnowledgeHarvester.ts" index
```
**Sources:**
@@ -764,7 +764,7 @@ bun ~/.claude/LIFEOS/TOOLS/KnowledgeHarvester.ts index
## models.ts + UpdateModels.ts — Model ID Registry & Release Auto-Track
-**Location:** `~/.claude/LIFEOS/TOOLS/models.ts` (registry) + `~/.claude/LIFEOS/TOOLS/UpdateModels.ts` (updater).
+**Location:** `{{LIFEOS_DIR}}/TOOLS/models.ts` (registry) + `{{LIFEOS_DIR}}/TOOLS/UpdateModels.ts` (updater).
`models.ts` is the **single source of truth** for current Claude model IDs (`CURRENT.opus/sonnet/haiku`) plus cross-vendor pins (inventory only). The rule of the road:
@@ -774,24 +774,24 @@ bun ~/.claude/LIFEOS/TOOLS/KnowledgeHarvester.ts index
**On a new model release (propose-not-auto):**
1. The `_NEWS` Anthropic monitor (`CheckAnthropicChanges.ts`) scans fetched source bodies for a new Claude ID and, on a hit, records it to `LIFEOS/MEMORY/OBSERVABILITY/model-releases.jsonl` and best-effort fires `/notify` (voice/Telegram). It never edits the registry. (A model bump is a command, not a markdown-section text edit, so it deliberately does NOT use the `pending-proposals.jsonl` Telegram queue — that queue's apply path only appends text under a header.)
-2. A human reviews, then bumps the one edit point: `bun ~/.claude/LIFEOS/TOOLS/UpdateModels.ts --apply `.
-3. Confirm nothing else drifted: `bun ~/.claude/LIFEOS/TOOLS/UpdateModels.ts --check` (also runnable any time as a drift alarm; exit 1 on drift).
+2. A human reviews, then bumps the one edit point: `bun "${LIFEOS_DIR}/TOOLS/UpdateModels.ts" --apply `.
+3. Confirm nothing else drifted: `bun "${LIFEOS_DIR}/TOOLS/UpdateModels.ts" --check` (also runnable any time as a drift alarm; exit 1 on drift).
Historical Algorithm doctrine snapshots (`LIFEOS/ALGORITHM/v*.md`) keep their period IDs and are excluded from the drift scan.
## ArchitectureSummaryGenerator.ts - Architecture Summary Generator
-**Location:** `~/.claude/LIFEOS/TOOLS/ArchitectureSummaryGenerator.ts`
+**Location:** `{{LIFEOS_DIR}}/TOOLS/ArchitectureSummaryGenerator.ts`
Generate `LIFEOS_ARCHITECTURE_SUMMARY.md` from LifeosSystemArchitecture.md and subsystem docs. Provides a compact architecture overview derived from the master architecture document.
**Usage:**
```bash
# Generate/regenerate the architecture summary
-bun ~/.claude/LIFEOS/TOOLS/ArchitectureSummaryGenerator.ts generate
+bun "${LIFEOS_DIR}/TOOLS/ArchitectureSummaryGenerator.ts" generate
# Check if summary is stale (exit 1 if stale, 0 if fresh)
-bun ~/.claude/LIFEOS/TOOLS/ArchitectureSummaryGenerator.ts check
+bun "${LIFEOS_DIR}/TOOLS/ArchitectureSummaryGenerator.ts" check
```
**When to Use:**
@@ -803,7 +803,7 @@ bun ~/.claude/LIFEOS/TOOLS/ArchitectureSummaryGenerator.ts check
## Doctor.ts - Capability Prober & Health Manifest
-**Location:** `~/.claude/LIFEOS/TOOLS/Doctor.ts`
+**Location:** `{{LIFEOS_DIR}}/TOOLS/Doctor.ts`
Probes the external tools LifeOS doctrine assumes but the core install does not ship — `codex` (cross-vendor audit), Interceptor (browser verification), Cloudflare/wrangler (scheduled flows), ElevenLabs (voice) — plus core wiring, and writes an **advisory** capability manifest at `MEMORY/STATE/capabilities.json`. Born from onboarding-friction feedback (discussion #1461): capabilities assumed but never verified degrade silently, and change-scoped checks can't see what's dormant at rest.
@@ -812,18 +812,18 @@ Four states per capability: `live` / `broken` / `declined` / `stale`. The manife
**Usage:**
```bash
# Probe (offline checks), human table
-bun ~/.claude/LIFEOS/TOOLS/Doctor.ts
+bun "${LIFEOS_DIR}/TOOLS/Doctor.ts"
# Include network probes (only for configured capabilities — no pre-consent egress)
-bun ~/.claude/LIFEOS/TOOLS/Doctor.ts --network
-
-bun ~/.claude/LIFEOS/TOOLS/Doctor.ts --json # machine-readable
-bun ~/.claude/LIFEOS/TOOLS/Doctor.ts --verify # integrity-check the manifest (exit 2 on tamper)
-bun ~/.claude/LIFEOS/TOOLS/Doctor.ts --reconcile # hooks declared-on-disk vs registered-in-settings
-bun ~/.claude/LIFEOS/TOOLS/Doctor.ts --statusline # one glyph if a NEW regression since ack, else empty
-bun ~/.claude/LIFEOS/TOOLS/Doctor.ts decline # permanent silent opt-out
-bun ~/.claude/LIFEOS/TOOLS/Doctor.ts enable # undo a decline
-bun ~/.claude/LIFEOS/TOOLS/Doctor.ts ack # acknowledge the current broken set (statusline delta base)
+bun "${LIFEOS_DIR}/TOOLS/Doctor.ts" --network
+
+bun "${LIFEOS_DIR}/TOOLS/Doctor.ts" --json # machine-readable
+bun "${LIFEOS_DIR}/TOOLS/Doctor.ts" --verify # integrity-check the manifest (exit 2 on tamper)
+bun "${LIFEOS_DIR}/TOOLS/Doctor.ts" --reconcile # hooks declared-on-disk vs registered-in-settings
+bun "${LIFEOS_DIR}/TOOLS/Doctor.ts" --statusline # one glyph if a NEW regression since ack, else empty
+bun "${LIFEOS_DIR}/TOOLS/Doctor.ts" decline # permanent silent opt-out
+bun "${LIFEOS_DIR}/TOOLS/Doctor.ts" enable # undo a decline
+bun "${LIFEOS_DIR}/TOOLS/Doctor.ts" ack # acknowledge the current broken set (statusline delta base)
```
**Consumers (read the manifest, never write it):** the statusline delta line (precomputed sidecar), the `AlgorithmNudge` capability row (fires at the moment a broken capability's command fails — reads only `state`, fix command is a static in-hook constant), and the Pulse System Health panel (`/api/doctor`). Never install-fatal: the default run always exits 0; every probe is timeout-bounded.
@@ -836,7 +836,7 @@ bun ~/.claude/LIFEOS/TOOLS/Doctor.ts ack # acknowledge the current b
## MCP Servers
-**Config:** `~/.claude/.mcp.json`
+**Config:** `{{LIFEOS_ROOT}}/.mcp.json`
LifeOS connects to external MCP servers for domain-specific tool access.
@@ -869,8 +869,8 @@ MCP tool results are truncated by default. Servers can override this by adding `
## Related Documentation
-- **Architecture**: `~/.claude/LIFEOS/DOCUMENTATION/LifeosSystemArchitecture.md` (master architecture reference)
-- **CLI Tools**: `~/.claude/LIFEOS/DOCUMENTATION/Tools/Cli.md` (Algorithm CLI, Arbol CLI)
+- **Architecture**: `{{LIFEOS_DIR}}/DOCUMENTATION/LifeosSystemArchitecture.md` (master architecture reference)
+- **CLI Tools**: `{{LIFEOS_DIR}}/DOCUMENTATION/Tools/Cli.md` (Algorithm CLI, Arbol CLI)
---
diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Work/WorkSystem.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Work/WorkSystem.md
index 4f73bfcc7d..1d85903c91 100644
--- a/LifeOS/install/LIFEOS/DOCUMENTATION/Work/WorkSystem.md
+++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Work/WorkSystem.md
@@ -89,7 +89,7 @@ Fires on every prompt. Precision-biased regex looks for `remind me to X`, `resea
### 3. Periodic sweep — `LIFEOS/TOOLS/WorkSweep.ts` + launchd 60min
-Runs every 60 minutes via `~/Library/LaunchAgents/com.lifeos.worksweep.plist` (installed via `bun ~/.claude/LIFEOS/TOOLS/InstallWorkSweep.ts`). Four sub-sweeps:
+Runs every 60 minutes via `~/Library/LaunchAgents/com.lifeos.worksweep.plist` (installed via `bun "${LIFEOS_DIR}/TOOLS/InstallWorkSweep.ts"`). Four sub-sweeps:
| Sub-sweep | Trigger | Output |
|-----------|---------|--------|
@@ -112,7 +112,7 @@ Safety:
## Label taxonomy
-The canonical label list lives at `USER/WORK/labels.yml` and gets pushed to the configured repo by `bun ~/.claude/skills/_ULWORK/Tools/BootstrapLabels.ts`. Additive — never deletes existing labels.
+The canonical label list lives at `USER/WORK/labels.yml` and gets pushed to the configured repo by `bun "${LIFEOS_ROOT}/skills/_ULWORK/Tools/BootstrapLabels.ts"`. Additive — never deletes existing labels.
| Family | Members | Purpose |
|--------|---------|---------|
@@ -174,13 +174,13 @@ A `-can-take` label serves as the queue marker for "the DA should pick
| Layer | Lives in | Contains | Ships in release? |
|-------|----------|----------|-------------------|
-| **System code (public)** | `~/.claude/LIFEOS/PULSE/`, `~/.claude/LIFEOS/TOOLS/`, generic capture hooks under `~/.claude/hooks/` | Modules, CLIs, generic hooks | YES — scrubbed, public-clean |
-| **Private components** | `~/.claude/skills/_ULWORK/`, `~/.claude/hooks/ULWorkSync.hook.ts` | Underscore-private skill + principal-specific SessionEnd capture hook (target the private UL work repo) | NO — rsync-excluded from the public release payload, same as any underscore-prefixed private skill |
-| **User config** | `~/.claude/LIFEOS/USER/WORK/` | `labels.yml`, `config.yaml`, `work_repo.json`, `README.md` | NO — USER zone, excluded by containment |
-| **Templates for new users** | `~/.claude/skills/_LIFEOS/RELEASE_TEMPLATES/WORK_REPO/` | README template, TASKLIST starter, .github/labels.yml, ISSUE_TEMPLATE, workflows | YES — placeholder substitution at user-setup time (planned, not yet built) |
+| **System code (public)** | `{{LIFEOS_DIR}}/PULSE/`, `{{LIFEOS_DIR}}/TOOLS/`, generic capture hooks under `{{LIFEOS_ROOT}}/hooks/` | Modules, CLIs, generic hooks | YES — scrubbed, public-clean |
+| **Private components** | `{{LIFEOS_ROOT}}/skills/_ULWORK/`, `{{LIFEOS_ROOT}}/hooks/ULWorkSync.hook.ts` | Underscore-private skill + principal-specific SessionEnd capture hook (target the private UL work repo) | NO — rsync-excluded from the public release payload, same as any underscore-prefixed private skill |
+| **User config** | `{{LIFEOS_DIR}}/USER/WORK/` | `labels.yml`, `config.yaml`, `work_repo.json`, `README.md` | NO — USER zone, excluded by containment |
+| **Templates for new users** | `{{LIFEOS_ROOT}}/skills/_LIFEOS/RELEASE_TEMPLATES/WORK_REPO/` | README template, TASKLIST starter, .github/labels.yml, ISSUE_TEMPLATE, workflows | YES — placeholder substitution at user-setup time (planned, not yet built) |
| **Live repo** | The configured private GitHub repo | Issues, TASKLIST.md, README, SOPs, CHANGELOG | NO — user's private property |
-A new LifeOS user runs `bun ~/.claude/skills/_ULWORK/Tools/SetWorkRepo.ts --bootstrap ` (planned). That single command verifies the repo is private, writes the privacy-attested `work_repo.json`, runs BootstrapLabels to seed the taxonomy, clones the template into the new repo with placeholder substitution, commits, and pushes. From that point the entire system points at their repo with zero code changes.
+A new LifeOS user runs `bun "${LIFEOS_ROOT}/skills/_ULWORK/Tools/SetWorkRepo.ts" --bootstrap ` (planned). That single command verifies the repo is private, writes the privacy-attested `work_repo.json`, runs BootstrapLabels to seed the taxonomy, clones the template into the new repo with placeholder substitution, commits, and pushes. From that point the entire system points at their repo with zero code changes.
## Failure modes
@@ -197,10 +197,10 @@ A new LifeOS user runs `bun ~/.claude/skills/_ULWORK/Tools/SetWorkRepo.ts --boot
## Setup for a new LifeOS user
1. Create a private GitHub repo
-2. `bun ~/.claude/skills/_ULWORK/Tools/SetWorkRepo.ts --bootstrap ` (planned — for now do steps 3-5 manually)
-3. `bun ~/.claude/skills/_ULWORK/Tools/BootstrapLabels.ts --repo ` to seed the label taxonomy
-4. Restart Pulse: `bash ~/.claude/LIFEOS/PULSE/manage.sh restart`
-5. `bun ~/.claude/LIFEOS/TOOLS/InstallWorkSweep.ts` to register the launchd job
+2. `bun "${LIFEOS_ROOT}/skills/_ULWORK/Tools/SetWorkRepo.ts" --bootstrap ` (planned — for now do steps 3-5 manually)
+3. `bun "${LIFEOS_ROOT}/skills/_ULWORK/Tools/BootstrapLabels.ts" --repo ` to seed the label taxonomy
+4. Restart Pulse: `bash "${LIFEOS_DIR}/PULSE/manage.sh" restart`
+5. `bun "${LIFEOS_DIR}/TOOLS/InstallWorkSweep.ts"` to register the launchd job
6. Run any Algorithm session — `ULWorkSync.hook.ts` opens the first issue at SessionEnd; sweep catches everything else within an hour
## Tunables
diff --git a/LifeOS/install/LIFEOS/LIFEOS_SYSTEM_PROMPT.md b/LifeOS/install/LIFEOS/LIFEOS_SYSTEM_PROMPT.md
index d9160ab507..754ced679b 100644
--- a/LifeOS/install/LIFEOS/LIFEOS_SYSTEM_PROMPT.md
+++ b/LifeOS/install/LIFEOS/LIFEOS_SYSTEM_PROMPT.md
@@ -78,7 +78,7 @@ These rules govern **visual layout** — how content is arranged on the page. Th
## The Algorithm
-Substantial work — anything where "done" needs articulating, building, or verifying — runs the Algorithm loop. **First action for such work:** Read `~/.claude/LIFEOS/ALGORITHM/LATEST` for the version string `V`, then Read `~/.claude/LIFEOS/ALGORITHM/v${V}.md` and follow it: the work climbs against an ISA, claims close on tool evidence, the run leaves its trail. (LATEST is the single source of truth for the version.) Trivial and conversational turns skip it — no ISA, no ceremony, just the format above.
+Substantial work — anything where "done" needs articulating, building, or verifying — runs the Algorithm loop. **First action for such work:** Read `{{LIFEOS_DIR}}/ALGORITHM/LATEST` for the version string `V`, then Read `{{LIFEOS_DIR}}/ALGORITHM/v${V}.md` and follow it: the work climbs against an ISA, claims close on tool evidence, the run leaves its trail. (LATEST is the single source of truth for the version.) Trivial and conversational turns skip it — no ISA, no ceremony, just the format above.
How much to spend is discovered from the work, never predicted from a rubric; the principal steers in plain language ("go heavy", "quick pass"), which outranks my judgment. Only the primary DA runs the Algorithm; subagents execute their briefs.
@@ -161,18 +161,18 @@ The LifeOS Security System protects Customer data (anything customer-owned that
Self-check before anything leaves this machine: 1. Is the destination public or cacheable? 2. Does the content carry identity, paths, or `/USER` data? 3. Is the `` release workflow the path? Wrong answer to any → stop.
-**The `~/.claude` repository (the principal's private installation; remote is a PRIVATE git repo) holds the principal's complete personal AI infrastructure: identity, voice, contacts, opinions, financial context, business state, project state, security findings, hooks, skills, settings, ISAs, knowledge archive, and conversation history. Its contents are PRIVATE FOREVER. They MUST NEVER reach any public location.**
+**The repository rooted at `{{LIFEOS_ROOT}}` (the principal's private installation; remote is a PRIVATE git repo) holds the principal's complete personal AI infrastructure: identity, voice, contacts, opinions, financial context, business state, project state, security findings, hooks, skills, settings, ISAs, knowledge archive, and conversation history. Its contents are PRIVATE FOREVER. They MUST NEVER reach any public location.**
This is a constitutional non-negotiable, not a preference. Concretely:
-- **Never push to a public remote.** Only the principal's private `.claude` remote is legitimate. Never add a public remote, never push to one, never `git push --mirror` anywhere else.
-- **Never copy `~/.claude` content into public repos.** Files, snippets, paths, commit-message excerpts, ISA contents, hook code, skill code, identity fields — none of it goes into any public LifeOS fork, blog post, public Gist, social media, release artifact, or any other public surface.
-- **Never paste `~/.claude` content into web tools.** That includes diagram renderers, pastebins, online formatters, public LLM playgrounds — anything that could cache or index it.
-- **Never quote absolute `~/.claude` paths in public-destined output.** Public docs reference `${LIFEOS_DIR}` or relative paths. The release-time containment gates (G1-G14 in `skills/_LIFEOS/Tools/ShadowRelease.ts`, particularly G2 identity-grep and G9 username-path leak) catch hardcoded user-home paths before any public push. There is no runtime guard hook — the 2026-05-06 simplification consolidated enforcement to a single release-build pass. Don't write the leaks in the first place; the gates are a backstop, not a license.
-- **The `` skill's release workflow is the ONLY sanctioned path** that moves anything from `~/.claude` toward public visibility. It stages a copy under `~/.claude/LIFEOS_RELEASES/`, scrubs containment-zone violations against `hooks/lib/containment-zones.ts`, and gates publication on a zero-match audit. Never bypass it.
+- **Never push to a public remote.** Only the private remote for `{{LIFEOS_ROOT}}` is legitimate. Never add a public remote, never push to one, never `git push --mirror` anywhere else.
+- **Never copy content from `{{LIFEOS_ROOT}}` into public repos.** Files, snippets, paths, commit-message excerpts, ISA contents, hook code, skill code, identity fields — none of it goes into any public LifeOS fork, blog post, public Gist, social media, release artifact, or any other public surface.
+- **Never paste content from `{{LIFEOS_ROOT}}` into web tools.** That includes diagram renderers, pastebins, online formatters, public LLM playgrounds — anything that could cache or index it.
+- **Never quote an absolute local config-root path in public-destined output.** This includes both the default `~/.claude` location and the resolved value of `{{LIFEOS_ROOT}}`. Public docs reference semantic placeholders such as `{{LIFEOS_ROOT}}` / `{{LIFEOS_DIR}}` or use relative paths. The release-time containment gates (G1-G14 in `skills/_LIFEOS/Tools/ShadowRelease.ts`, particularly G2 identity-grep and G9 username-path leak) catch hardcoded user-home paths before any public push. There is no runtime guard hook — the 2026-05-06 simplification consolidated enforcement to a single release-build pass. Don't write the leaks in the first place; the gates are a backstop, not a license.
+- **The `` skill's release workflow is the ONLY sanctioned path** that moves anything from the local config root toward public visibility. It stages a copy under `{{LIFEOS_ROOT}}/LIFEOS_RELEASES/`, scrubs containment-zone violations against `hooks/lib/containment-zones.ts`, and gates publication on a zero-match audit. Never bypass it.
- **When in doubt, don't share.** The cost of leaving something useful internal is zero; the cost of leaking identity, business data, or security context is permanent.
-This rule applies to every file under `~/.claude` regardless of subdirectory, every commit on this repo, every output produced while operating on this repo, and every artifact derived from it. The privacy boundary is the repository root.
+This rule applies to every file under the resolved `{{LIFEOS_ROOT}}` regardless of subdirectory, every commit on this repo, every output produced while operating on this repo, and every artifact derived from it. The privacy boundary is the repository root.
## Personal Use Boundary
diff --git a/LifeOS/install/LIFEOS/LIFEOS_StatusLine.sh b/LifeOS/install/LIFEOS/LIFEOS_StatusLine.sh
index e1e21745ec..44224e6275 100755
--- a/LifeOS/install/LIFEOS/LIFEOS_StatusLine.sh
+++ b/LifeOS/install/LIFEOS/LIFEOS_StatusLine.sh
@@ -36,7 +36,9 @@ case "$LIFEOS_DIR" in
*'$HOME'*|*'${HOME}'*|*'~'*) echo "LifeOS"; exit 0 ;;
esac
-CLAUDE_HOME="$HOME/.claude"
+# The statusline is deployed beside this LIFEOS directory. Derive its config
+# root from LIFEOS_DIR so custom homes do not read global settings or caches.
+CLAUDE_HOME="$(dirname "$LIFEOS_DIR")"
SETTINGS_FILE="$CLAUDE_HOME/settings.json"
RATINGS_FILE="$LIFEOS_DIR/MEMORY/LEARNING/SIGNALS/ratings.jsonl"
MODEL_CACHE="$LIFEOS_DIR/MEMORY/STATE/model-cache.txt"
@@ -81,13 +83,9 @@ USER_TZ="${USER_TZ:-UTC}"
# LIFEOS_VERSION: read from PAI/VERSION (canonical, also read by install.sh,
# Banner.ts, _LIFEOS/Tools/UpdateLifeosVersion.ts, ShadowRelease.ts, install web
# server). Same multi-path pattern as ALGO_VERSION below to survive
-# hook-spawn contexts where HOME/LIFEOS_DIR may not resolve.
+# hook-spawn contexts where HOME may not resolve.
LIFEOS_VERSION=""
-for _pai_v_path in \
- "$LIFEOS_DIR/VERSION" \
- "$HOME/.claude/LIFEOS/VERSION" \
- "/Users/$(id -un 2>/dev/null)/.claude/LIFEOS/VERSION" \
- "$(eval echo ~"$(id -un 2>/dev/null)")/.claude/LIFEOS/VERSION"; do
+for _pai_v_path in "$LIFEOS_DIR/VERSION"; do
if [ -n "$_pai_v_path" ] && [ -f "$_pai_v_path" ]; then
LIFEOS_VERSION="$(cat "$_pai_v_path" 2>/dev/null | tr -d '[:space:]')"
[ -n "$LIFEOS_VERSION" ] && break
@@ -95,15 +93,9 @@ for _pai_v_path in \
done
LIFEOS_VERSION="${LIFEOS_VERSION:-—}"
# v6.2.0+: LATEST is the single source of truth for the Algorithm version.
-# Hardened against Claude Code's hook-spawn context where $HOME or $LIFEOS_DIR
-# may not resolve as expected (subprocess spawn with non-default env). Try
-# multiple candidate paths in order, keeping the first non-empty result.
+# Hardened against Claude Code's hook-spawn context where $HOME may not resolve.
ALGO_VERSION=""
-for _algo_path in \
- "$LIFEOS_DIR/ALGORITHM/LATEST" \
- "$HOME/.claude/LIFEOS/ALGORITHM/LATEST" \
- "/Users/$(id -un 2>/dev/null)/.claude/LIFEOS/ALGORITHM/LATEST" \
- "$(eval echo ~"$(id -un 2>/dev/null)")/.claude/LIFEOS/ALGORITHM/LATEST"; do
+for _algo_path in "$LIFEOS_DIR/ALGORITHM/LATEST"; do
if [ -n "$_algo_path" ] && [ -f "$_algo_path" ]; then
ALGO_VERSION="$(cat "$_algo_path" 2>/dev/null | tr -d '[:space:]')"
[ -n "$ALGO_VERSION" ] && break
@@ -131,10 +123,8 @@ USAGE_CACHE_TTL=900 # 15 min: /api/oauth/usage has aggressive per-token rat
USAGE_HARD_EXPIRY=21600 # P5: 6h. Show last-known-good (dimmed + stale badge) until here, then hide —
# replaces the old 1800s cliff that deleted the cache and vanished the counters.
-# Source .env for API keys. Canonical location is $HOME/.claude/.env (which is
-# typically a symlink to $HOME/.config/LIFEOS/.env). The historical $HOME/.claude/LIFEOS/.env
-# path is wrong and has been removed everywhere else — do not reintroduce it.
-[ -f "$HOME/.claude/.env" ] && source "$HOME/.claude/.env"
+# Source .env for API keys from the resolved config root.
+[ -f "$CLAUDE_HOME/.env" ] && source "$CLAUDE_HOME/.env"
# Cross-platform file mtime (seconds since epoch). Detect stat flavor once;
# probing both variants on every mtime check is expensive on macOS.
@@ -312,8 +302,8 @@ if [ "$context_pct" = "0" ] && [ "$total_input" -eq 0 ] 2>/dev/null; then
[ -n "$_f" ] && [ -f "$LIFEOS_DIR/$_f" ] && _est=$((_est + $(wc -c < "$LIFEOS_DIR/$_f") * 10 / 35))
done < <(jq -r '.loadAtStartup.files[]? // empty' "$SETTINGS_FILE" 2>/dev/null)
- # Project memory files (CC native memory at ~/.claude/projects/*/memory/)
- for _f in "$HOME"/.claude/projects/*/memory/MEMORY.md; do
+ # Project memory files (CC native memory under the resolved config root)
+ for _f in "$CLAUDE_HOME"/projects/*/memory/MEMORY.md; do
[ -f "$_f" ] && _est=$((_est + $(wc -c < "$_f") * 10 / 35))
done
@@ -729,7 +719,7 @@ if [ "$MODE" != "nano" ]; then
# Hook count flows through GetCounts.ts — same source banner uses. --single hooks
# short-circuits all other walks (~20ms). Don't reintroduce inline jq here.
- _hooks_cnt=$(bun "$HOME/.claude/LIFEOS/TOOLS/GetCounts.ts" --single hooks 2>/dev/null || echo 0)
+ _hooks_cnt=$(bun "$LIFEOS_DIR/TOOLS/GetCounts.ts" --single hooks 2>/dev/null || echo 0)
_ratings_cnt=0
[ -f "$RATINGS_FILE" ] && _ratings_cnt=$(wc -l < "$RATINGS_FILE" 2>/dev/null | tr -d ' ')
diff --git a/LifeOS/install/LIFEOS/PULSE/Conduit/paths.ts b/LifeOS/install/LIFEOS/PULSE/Conduit/paths.ts
index 67fcf71cf8..a9e3f66db6 100644
--- a/LifeOS/install/LIFEOS/PULSE/Conduit/paths.ts
+++ b/LifeOS/install/LIFEOS/PULSE/Conduit/paths.ts
@@ -8,9 +8,10 @@
*/
import { homedir } from "node:os";
import { join } from "node:path";
+import { claudeDir } from "../../TOOLS/lifeos-root";
/** LifeOS install root — honors CLAUDE_CONFIG_DIR, else ~/.claude. */
-export const CLAUDE_ROOT = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
+export const CLAUDE_ROOT = process.env.CLAUDE_CONFIG_DIR || join(claudeDir());
/** All Conduit data lives here, under USER. Nothing Conduit writes escapes this dir. */
export const DATA_ROOT = join(CLAUDE_ROOT, "LIFEOS", "USER", "CONDUIT");
diff --git a/LifeOS/install/LIFEOS/PULSE/MenuBar/com.lifeos.pulse-menubar.plist b/LifeOS/install/LIFEOS/PULSE/MenuBar/com.lifeos.pulse-menubar.plist
index 4e1879f7e6..f659af98a3 100644
--- a/LifeOS/install/LIFEOS/PULSE/MenuBar/com.lifeos.pulse-menubar.plist
+++ b/LifeOS/install/LIFEOS/PULSE/MenuBar/com.lifeos.pulse-menubar.plist
@@ -15,11 +15,13 @@
EnvironmentVariables
LIFEOS_PULSE_DIR
- __HOME__/.claude/LIFEOS/PULSE
+ __LIFEOS_DIR__/PULSE
+ LIFEOS_DIR
+ __LIFEOS_DIR__
StandardOutPath
- __HOME__/.claude/LIFEOS/PULSE/logs/menubar-stdout.log
+ __LIFEOS_DIR__/PULSE/logs/menubar-stdout.log
StandardErrorPath
- __HOME__/.claude/LIFEOS/PULSE/logs/menubar-stderr.log
+ __LIFEOS_DIR__/PULSE/logs/menubar-stderr.log
diff --git a/LifeOS/install/LIFEOS/PULSE/MenuBar/install.sh b/LifeOS/install/LIFEOS/PULSE/MenuBar/install.sh
index 84bd645243..1587148a4e 100755
--- a/LifeOS/install/LIFEOS/PULSE/MenuBar/install.sh
+++ b/LifeOS/install/LIFEOS/PULSE/MenuBar/install.sh
@@ -6,6 +6,11 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
HOME_DIR="$HOME"
+# PULSE + LIFEOS dirs derived from this script's location (ships in
+# /LIFEOS/PULSE/MenuBar) — correct for default AND custom LifeOS homes.
+PULSE_DIR="$(dirname "$SCRIPT_DIR")"
+LIFEOS_DIR_RESOLVED="$(dirname "$PULSE_DIR")"
+CLAUDE_DIR_RESOLVED="$(dirname "$LIFEOS_DIR_RESOLVED")"
APP_NAME="LifeOS Pulse"
APP_DIR="$HOME_DIR/Applications"
APP_DEST="$APP_DIR/$APP_NAME.app"
@@ -63,12 +68,12 @@ echo " Installed $APP_DEST"
# Step 6: Install and load launchd plist
echo "[6/6] Installing LaunchAgent..."
-# Substitute __HOME__ placeholder with actual home directory
-sed "s|__HOME__|$HOME_DIR|g" "$PLIST_SRC" > "$PLIST_DST"
+# Substitute __HOME__ / __LIFEOS_DIR__ / __CLAUDE_DIR__ placeholders
+sed -e "s|__LIFEOS_DIR__|$LIFEOS_DIR_RESOLVED|g" -e "s|__CLAUDE_DIR__|$CLAUDE_DIR_RESOLVED|g" -e "s|__HOME__|$HOME_DIR|g" "$PLIST_SRC" > "$PLIST_DST"
echo " Installed $PLIST_DST"
# Ensure logs directory exists
-mkdir -p "$HOME_DIR/.claude/LIFEOS/PULSE/logs"
+mkdir -p "$PULSE_DIR/logs"
launchctl load "$PLIST_DST"
echo " Loaded $PLIST_LABEL"
diff --git a/LifeOS/install/LIFEOS/PULSE/Observability/observability.ts b/LifeOS/install/LIFEOS/PULSE/Observability/observability.ts
index b0fa18ee71..30697aadad 100644
--- a/LifeOS/install/LIFEOS/PULSE/Observability/observability.ts
+++ b/LifeOS/install/LIFEOS/PULSE/Observability/observability.ts
@@ -33,6 +33,7 @@ import { join, extname } from "path"
import { readFileSync, readdirSync, existsSync, realpathSync, statSync, watch, type FSWatcher } from "fs"
import YAML from "yaml"
import { effortToCanonicalTierName } from "../../../hooks/lib/effort"
+import { claudeDir } from "../../TOOLS/lifeos-root";
// Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404)
for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
@@ -69,7 +70,7 @@ export interface ObservabilityConfig {
// ── Path Construction ──
const HOME = process.env.HOME ?? ""
-const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS")
+const LIFEOS_DIR = join(claudeDir(), "LIFEOS")
const MEMORY_DIR = join(LIFEOS_DIR, "MEMORY")
const WORK_JSON_PATH = join(MEMORY_DIR, "STATE", "work.json")
@@ -78,7 +79,7 @@ const SUBAGENT_EVENTS_PATH = join(MEMORY_DIR, "OBSERVABILITY", "subagent-events.
const VOICE_EVENTS_PATH = join(MEMORY_DIR, "VOICE", "voice-events.jsonl")
const TOOL_FAILURES_PATH = join(MEMORY_DIR, "OBSERVABILITY", "tool-failures.jsonl")
const TOOL_ACTIVITY_PATH = join(MEMORY_DIR, "OBSERVABILITY", "tool-activity.jsonl")
-const SETTINGS_PATH = join(HOME, ".claude", "settings.json")
+const SETTINGS_PATH = join(claudeDir(), "settings.json")
const LADDER_DIR = join(HOME, "Projects", "Ladder")
const DEFAULT_DASHBOARD_DIR = join(LIFEOS_DIR, "PULSE", "Observability", "out")
@@ -156,7 +157,7 @@ function getDashboardDir(): string {
const dir = config.dashboard_dir ?? DEFAULT_DASHBOARD_DIR
// Resolve relative paths against Pulse directory
if (!dir.startsWith("/")) {
- return join(HOME, ".claude", "LIFEOS", "PULSE", dir)
+ return join(claudeDir(), "LIFEOS", "PULSE", dir)
}
return dir
}
@@ -1640,7 +1641,7 @@ function readDirMdFiles(dir: string): { name: string, content: string, sections:
function handleUserIndexApi(filter: string | null): Response {
try {
- const LIFEOS_DIR = process.env.LIFEOS_DIR || join(process.env.HOME || "", ".claude", "LIFEOS")
+ const LIFEOS_DIR = process.env.LIFEOS_DIR || join(claudeDir(), "LIFEOS")
const indexPath = join(LIFEOS_DIR, "PULSE", "state", "user-index.json")
const raw = Bun.file(indexPath)
if (!raw.size) {
diff --git a/LifeOS/install/LIFEOS/PULSE/Performance/cost-aggregator.ts b/LifeOS/install/LIFEOS/PULSE/Performance/cost-aggregator.ts
index d92b8749c8..2339ff3026 100755
--- a/LifeOS/install/LIFEOS/PULSE/Performance/cost-aggregator.ts
+++ b/LifeOS/install/LIFEOS/PULSE/Performance/cost-aggregator.ts
@@ -12,10 +12,11 @@
import { join, basename, dirname } from "path"
import { existsSync, readFileSync, writeFileSync, appendFileSync, readdirSync, statSync, mkdirSync } from "fs"
+import { claudeDir } from "../../TOOLS/lifeos-root";
const HOME = process.env.HOME ?? ""
-const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS")
-const PROJECTS_DIR = join(HOME, ".claude", "projects")
+const LIFEOS_DIR = join(claudeDir(), "LIFEOS")
+const PROJECTS_DIR = join(claudeDir(), "projects")
const OUTPUT_FILE = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY", "session-costs.jsonl")
const STATE_FILE = join(LIFEOS_DIR, "PULSE", "Performance", "aggregator-state.json")
diff --git a/LifeOS/install/LIFEOS/PULSE/Performance/module.ts b/LifeOS/install/LIFEOS/PULSE/Performance/module.ts
index aa44617f3d..cf8a312398 100644
--- a/LifeOS/install/LIFEOS/PULSE/Performance/module.ts
+++ b/LifeOS/install/LIFEOS/PULSE/Performance/module.ts
@@ -12,9 +12,10 @@
import { join } from "path"
import { existsSync, readFileSync } from "fs"
+import { claudeDir } from "../../TOOLS/lifeos-root";
const HOME = process.env.HOME ?? ""
-const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS")
+const LIFEOS_DIR = join(claudeDir(), "LIFEOS")
const MEMORY_DIR = join(LIFEOS_DIR, "MEMORY")
const SESSION_COSTS_PATH = join(MEMORY_DIR, "OBSERVABILITY", "session-costs.jsonl")
const TOOL_FAILURES_PATH = join(MEMORY_DIR, "OBSERVABILITY", "tool-failures.jsonl")
@@ -278,7 +279,7 @@ async function handleAnthropicCostApi(): Promise {
const { readFileSync, existsSync } = await import("fs")
const { join } = await import("path")
const home = process.env.HOME ?? ""
- const obsDir = join(home, ".claude", "LIFEOS", "MEMORY", "OBSERVABILITY")
+ const obsDir = join(claudeDir(), "LIFEOS", "MEMORY", "OBSERVABILITY")
const ledgerPath = join(obsDir, "anthropic-cost.jsonl")
const sitesPath = join(obsDir, "anthropic-call-sites.json")
diff --git a/LifeOS/install/LIFEOS/PULSE/Tools/ReleaseAudit.ts b/LifeOS/install/LIFEOS/PULSE/Tools/ReleaseAudit.ts
index 1bbae62500..e3c74a8e85 100644
--- a/LifeOS/install/LIFEOS/PULSE/Tools/ReleaseAudit.ts
+++ b/LifeOS/install/LIFEOS/PULSE/Tools/ReleaseAudit.ts
@@ -3,6 +3,7 @@ import { readdirSync, statSync, readFileSync, existsSync } from "node:fs";
import { resolve, join, relative } from "node:path";
import { homedir } from "node:os";
import { parseFrontmatter } from "../lib/frontmatter";
+import { claudeDir } from "../../TOOLS/lifeos-root";
const args = process.argv.slice(2);
const stagingArg = args.find((a) => !a.startsWith("--"));
@@ -34,7 +35,7 @@ function loadProhibitedStrings(): string[] {
const candidates = [
process.env.LIFEOS_RELEASE_AUDIT_STRINGS,
join(homedir(), ".config/LIFEOS/USER/CONFIG/release-audit-strings.json"),
- join(homedir(), ".claude/LIFEOS/USER/CONFIG/release-audit-strings.json"),
+ join(claudeDir(), "LIFEOS/USER/CONFIG/release-audit-strings.json"),
].filter(Boolean) as string[];
for (const p of candidates) {
try {
diff --git a/LifeOS/install/LIFEOS/PULSE/Tools/SnapshotFromFixture.ts b/LifeOS/install/LIFEOS/PULSE/Tools/SnapshotFromFixture.ts
index fdfe381892..675fd81a31 100755
--- a/LifeOS/install/LIFEOS/PULSE/Tools/SnapshotFromFixture.ts
+++ b/LifeOS/install/LIFEOS/PULSE/Tools/SnapshotFromFixture.ts
@@ -4,10 +4,11 @@ import { resolve, join, basename } from "node:path";
import { renderShell, renderPage } from "../ui/render";
import type { PageData } from "../Schema/PulseSchema";
import { PageDataSchema } from "../Schema/PulseSchema";
+import { claudeDir } from "../../TOOLS/lifeos-root";
const HOME = process.env.HOME!;
-const FIX_DIR = resolve(HOME, ".claude", "LIFEOS", "PULSE", "Schema", "Fixtures");
-const OUT_DIR = resolve(HOME, ".claude", "LIFEOS", "PULSE", "Schema", "Snapshots");
+const FIX_DIR = resolve(claudeDir(), "LIFEOS", "PULSE", "Schema", "Fixtures");
+const OUT_DIR = resolve(claudeDir(), "LIFEOS", "PULSE", "Schema", "Snapshots");
mkdirSync(OUT_DIR, { recursive: true });
const fixtures = readdirSync(FIX_DIR).filter((f) => f.endsWith(".json") && !f.startsWith("invalid"));
diff --git a/LifeOS/install/LIFEOS/PULSE/Tools/SnapshotPages.ts b/LifeOS/install/LIFEOS/PULSE/Tools/SnapshotPages.ts
index d2b20f7eb8..d07d8e452f 100755
--- a/LifeOS/install/LIFEOS/PULSE/Tools/SnapshotPages.ts
+++ b/LifeOS/install/LIFEOS/PULSE/Tools/SnapshotPages.ts
@@ -5,9 +5,10 @@ import { readFileSync, existsSync } from "node:fs";
import { renderShell, renderPage, renderEmpty } from "../ui/render";
import { readIndex, readPage } from "../lib/data-plane";
import { loadAllManifests } from "../lib/manifest-loader";
+import { claudeDir } from "../../TOOLS/lifeos-root";
const HOME = process.env.HOME!;
-const SNAP_DIR = resolve(HOME, ".claude", "LIFEOS", "PULSE", "Schema", "Snapshots");
+const SNAP_DIR = resolve(claudeDir(), "LIFEOS", "PULSE", "Schema", "Snapshots");
mkdirSync(SNAP_DIR, { recursive: true });
const idx = readIndex();
diff --git a/LifeOS/install/LIFEOS/PULSE/VoiceServer/voice.ts b/LifeOS/install/LIFEOS/PULSE/VoiceServer/voice.ts
index 94086ad36f..6a43545f74 100644
--- a/LifeOS/install/LIFEOS/PULSE/VoiceServer/voice.ts
+++ b/LifeOS/install/LIFEOS/PULSE/VoiceServer/voice.ts
@@ -18,6 +18,7 @@ import { join } from "path"
import { existsSync, readFileSync, rmSync } from "fs"
import { log } from "../lib"
import { disambiguateHomographs } from "../lib/homographs"
+import { claudeDir } from "../../TOOLS/lifeos-root";
// ── Public Config Interface ──
@@ -163,7 +164,7 @@ function escapeRegex(str: string): string {
}
function loadPronunciations(customPath?: string): void {
- const paiDir = join(process.env.HOME ?? "~", ".claude", "LIFEOS")
+ const paiDir = join(claudeDir(), "LIFEOS")
const userPronPath = customPath ?? join(paiDir, "USER", "PRINCIPAL", "PRONUNCIATIONS.json")
try {
@@ -196,7 +197,7 @@ function applyPronunciations(text: string): string {
// ── Voice Config from settings.json ──
function loadVoiceConfigFromSettings(): LoadedVoiceConfig {
- const settingsPath = join(process.env.HOME ?? "~", ".claude", "settings.json")
+ const settingsPath = join(claudeDir(), "settings.json")
try {
if (!existsSync(settingsPath)) {
@@ -712,7 +713,7 @@ export async function handleVoiceRequest(req: Request): Promise
// /notify/personality honest with whatever the user last selected.
let voiceId: string | null = null
try {
- const settingsFile = join(process.env.HOME ?? "~", ".claude", "settings.json")
+ const settingsFile = join(claudeDir(), "settings.json")
const settings = JSON.parse(readFileSync(settingsFile, "utf-8"))
const main = settings?.daidentity?.voices?.main
const vid = (main?.voiceId || main?.VOICE_ID || main?.voice_id) as string | undefined
diff --git a/LifeOS/install/LIFEOS/PULSE/adapters/AdapterRunner.ts b/LifeOS/install/LIFEOS/PULSE/adapters/AdapterRunner.ts
index effa27b897..2e71bc8b63 100644
--- a/LifeOS/install/LIFEOS/PULSE/adapters/AdapterRunner.ts
+++ b/LifeOS/install/LIFEOS/PULSE/adapters/AdapterRunner.ts
@@ -6,9 +6,10 @@ import { hashFile, combineSourceHashes } from "../lib/cache";
import { writePage, writeError, clearError, readMeta, type DataPlaneFile, PULSE_DATA_DIR } from "../lib/data-plane";
import { getProvenance } from "../lib/frontmatter";
import { inference, type InferenceLevel } from "../../TOOLS/Inference";
+import { claudeDir } from "../../TOOLS/lifeos-root";
const HOME = process.env.HOME!;
-const OBSERVABILITY_DIR = resolve(HOME, ".claude", "LIFEOS", "MEMORY", "OBSERVABILITY");
+const OBSERVABILITY_DIR = resolve(claudeDir(), "LIFEOS", "MEMORY", "OBSERVABILITY");
const RUNS_LOG = join(OBSERVABILITY_DIR, "adapter-runs.jsonl");
const ADAPTER_TIMEOUT_MS = 120_000;
diff --git a/LifeOS/install/LIFEOS/PULSE/checks/airgradient-poll.ts b/LifeOS/install/LIFEOS/PULSE/checks/airgradient-poll.ts
index 5915340463..7c6b736ba0 100755
--- a/LifeOS/install/LIFEOS/PULSE/checks/airgradient-poll.ts
+++ b/LifeOS/install/LIFEOS/PULSE/checks/airgradient-poll.ts
@@ -12,9 +12,10 @@
import { join } from "node:path"
import { mkdirSync, writeFileSync, appendFileSync, readFileSync, existsSync } from "node:fs"
+import { claudeDir } from "../../TOOLS/lifeos-root";
const HOME = process.env.HOME ?? ""
-const CACHE_DIR = join(HOME, ".claude", "LIFEOS", "MEMORY", "_AIRGRADIENT")
+const CACHE_DIR = join(claudeDir(), "LIFEOS", "MEMORY", "_AIRGRADIENT")
const LATEST = join(CACHE_DIR, "latest.json")
const HISTORY = join(CACHE_DIR, "history.jsonl")
@@ -23,7 +24,7 @@ const API_BASE = "https://api.airgradient.com/public/api/v1"
// Bun auto-loads .env from CWD only; Pulse cron runs from LIFEOS/PULSE/, so the
// symlink at ~/.claude/.env isn't picked up. Read it directly if env is empty.
function loadTokenFromDotenv(): string | null {
- const envPath = join(HOME, ".claude", ".env")
+ const envPath = join(claudeDir(), ".env")
if (!existsSync(envPath)) return null
try {
const raw = readFileSync(envPath, "utf8")
diff --git a/LifeOS/install/LIFEOS/PULSE/checks/calendar.ts b/LifeOS/install/LIFEOS/PULSE/checks/calendar.ts
index bb97533f7a..92cb8a6ee3 100644
--- a/LifeOS/install/LIFEOS/PULSE/checks/calendar.ts
+++ b/LifeOS/install/LIFEOS/PULSE/checks/calendar.ts
@@ -10,6 +10,7 @@
import { readFileSync } from "fs"
import { join } from "path"
+import { claudeDir } from "../../TOOLS/lifeos-root";
const HOME = process.env.HOME ?? ""
const LOOKAHEAD_MS = 30 * 60 * 1000
@@ -17,7 +18,7 @@ const LOOKAHEAD_MS = 30 * 60 * 1000
function loadEnv(): Record {
const env: Record = {}
try {
- const content = readFileSync(join(HOME, ".claude", ".env"), "utf-8")
+ const content = readFileSync(join(claudeDir(), ".env"), "utf-8")
for (const line of content.split("\n")) {
const match = line.match(/^([^#=]+)=(.*)$/)
if (match) {
diff --git a/LifeOS/install/LIFEOS/PULSE/checks/github-work.ts b/LifeOS/install/LIFEOS/PULSE/checks/github-work.ts
index 41b1b5b7d0..b617e8841a 100755
--- a/LifeOS/install/LIFEOS/PULSE/checks/github-work.ts
+++ b/LifeOS/install/LIFEOS/PULSE/checks/github-work.ts
@@ -12,9 +12,10 @@ import { join } from "path"
import { readFileSync } from "fs"
import { parse } from "smol-toml"
import { SignJWT, importPKCS8 } from "jose"
+import { claudeDir } from "../../TOOLS/lifeos-root";
const HOME = process.env.HOME ?? ""
-const PULSE_DIR = join(HOME, ".claude", "LIFEOS", "PULSE")
+const PULSE_DIR = join(claudeDir(), "LIFEOS", "PULSE")
const STATE_FILE = join(PULSE_DIR, "state", "work-token.json")
// ── Worker Config (from PULSE.toml [worker] section) ──
diff --git a/LifeOS/install/LIFEOS/PULSE/checks/github.ts b/LifeOS/install/LIFEOS/PULSE/checks/github.ts
index a2f2cef6ec..89958a549e 100755
--- a/LifeOS/install/LIFEOS/PULSE/checks/github.ts
+++ b/LifeOS/install/LIFEOS/PULSE/checks/github.ts
@@ -10,10 +10,11 @@
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"
import { dirname, join } from "path"
+import { claudeDir } from "../../TOOLS/lifeos-root";
const HOME = process.env.HOME ?? ""
-const LEGACY_STATE_FILE = join(HOME, ".claude", "LIFEOS", "PULSE", "state", "github-seen.json")
-const STATE_FILE = join(HOME, ".claude", "LIFEOS", "PULSE", "state", "github-seen.jsonl")
+const LEGACY_STATE_FILE = join(claudeDir(), "LIFEOS", "PULSE", "state", "github-seen.json")
+const STATE_FILE = join(claudeDir(), "LIFEOS", "PULSE", "state", "github-seen.jsonl")
// Repos to monitor for new issues / activity. Override via LIFEOS_PULSE_REPOS
// env var (comma-separated "owner/name" pairs). Empty default keeps fresh
// installs from polling repos the user hasn't opted into.
diff --git a/LifeOS/install/LIFEOS/PULSE/checks/life-morning-brief.ts b/LifeOS/install/LIFEOS/PULSE/checks/life-morning-brief.ts
index a4bb7d6a56..8a0a6fb8bc 100755
--- a/LifeOS/install/LIFEOS/PULSE/checks/life-morning-brief.ts
+++ b/LifeOS/install/LIFEOS/PULSE/checks/life-morning-brief.ts
@@ -11,9 +11,10 @@
import { join } from "path"
import { existsSync, readFileSync } from "fs"
+import { claudeDir } from "../../TOOLS/lifeos-root";
const HOME = process.env.HOME ?? ""
-const TELOS_DIR = join(HOME, ".claude", "LIFEOS", "USER", "TELOS")
+const TELOS_DIR = join(claudeDir(), "LIFEOS", "USER", "TELOS")
function readFile(name: string): string {
const p = join(TELOS_DIR, name)
diff --git a/LifeOS/install/LIFEOS/PULSE/checks/notification-governor.ts b/LifeOS/install/LIFEOS/PULSE/checks/notification-governor.ts
index ded8715d86..0f76e0dd66 100755
--- a/LifeOS/install/LIFEOS/PULSE/checks/notification-governor.ts
+++ b/LifeOS/install/LIFEOS/PULSE/checks/notification-governor.ts
@@ -38,6 +38,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from "fs";
import { join, dirname } from "path";
import { createHash } from "crypto";
+import { claudeDir } from "../../TOOLS/lifeos-root";
// Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404)
for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
@@ -47,7 +48,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
const HOME = process.env.HOME || "";
-const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS");
+const LIFEOS_DIR = process.env.LIFEOS_DIR || join(claudeDir(), "LIFEOS");
const STATE_FILE = join(LIFEOS_DIR, "PULSE", "state", "notification-governor.json");
const LOG_FILE = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY", "notification-governor.jsonl");
const NOTIFY_URL = "http://localhost:31337/notify";
diff --git a/LifeOS/install/LIFEOS/PULSE/checks/poller-meta-monitor.ts b/LifeOS/install/LIFEOS/PULSE/checks/poller-meta-monitor.ts
index c605fb3c37..faddba221c 100755
--- a/LifeOS/install/LIFEOS/PULSE/checks/poller-meta-monitor.ts
+++ b/LifeOS/install/LIFEOS/PULSE/checks/poller-meta-monitor.ts
@@ -24,6 +24,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
import { readFileSync, existsSync } from "fs";
import { join } from "path";
+import { claudeDir } from "../../TOOLS/lifeos-root";
// Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404)
for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
@@ -33,7 +34,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
const HOME = process.env.HOME || "";
-const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS");
+const LIFEOS_DIR = process.env.LIFEOS_DIR || join(claudeDir(), "LIFEOS");
const PULSE_STATE = join(LIFEOS_DIR, "PULSE", "state", "state.json");
const PULSE_TOML = join(LIFEOS_DIR, "PULSE", "PULSE.toml");
diff --git a/LifeOS/install/LIFEOS/PULSE/com.lifeos.deriver.plist b/LifeOS/install/LIFEOS/PULSE/com.lifeos.deriver.plist
index 8439efa91f..6b33a678c5 100644
--- a/LifeOS/install/LIFEOS/PULSE/com.lifeos.deriver.plist
+++ b/LifeOS/install/LIFEOS/PULSE/com.lifeos.deriver.plist
@@ -9,17 +9,19 @@
__BUN_PATH__
run
- __HOME__/.claude/LIFEOS/TOOLS/LearningPatternSynthesis.ts
+ __LIFEOS_DIR__/TOOLS/LearningPatternSynthesis.ts
--hypothesize
WorkingDirectory
- __HOME__/.claude
+ __CLAUDE_DIR__
EnvironmentVariables
HOME
__HOME__
PATH
__HOME__/.local/bin:__HOME__/.bun/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
+ LIFEOS_DIR
+ __LIFEOS_DIR__
StartCalendarInterval
@@ -31,9 +33,9 @@
RunAtLoad
StandardOutPath
- __HOME__/.claude/LIFEOS/MEMORY/OBSERVABILITY/deriver.log
+ __LIFEOS_DIR__/MEMORY/OBSERVABILITY/deriver.log
StandardErrorPath
- __HOME__/.claude/LIFEOS/MEMORY/OBSERVABILITY/deriver.log
+ __LIFEOS_DIR__/MEMORY/OBSERVABILITY/deriver.log
ThrottleInterval
60
diff --git a/LifeOS/install/LIFEOS/PULSE/com.lifeos.pulse.plist b/LifeOS/install/LIFEOS/PULSE/com.lifeos.pulse.plist
index eb2c21eaa3..a83e3ac782 100644
--- a/LifeOS/install/LIFEOS/PULSE/com.lifeos.pulse.plist
+++ b/LifeOS/install/LIFEOS/PULSE/com.lifeos.pulse.plist
@@ -12,22 +12,24 @@
pulse.ts
WorkingDirectory
- __HOME__/.claude/LIFEOS/PULSE
+ __LIFEOS_DIR__/PULSE
EnvironmentVariables
HOME
__HOME__
PATH
__HOME__/.bun/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
+ LIFEOS_DIR
+ __LIFEOS_DIR__
RunAtLoad
KeepAlive
StandardOutPath
- __HOME__/.claude/LIFEOS/PULSE/logs/pulse-stdout.log
+ __LIFEOS_DIR__/PULSE/logs/pulse-stdout.log
StandardErrorPath
- __HOME__/.claude/LIFEOS/PULSE/logs/pulse-stderr.log
+ __LIFEOS_DIR__/PULSE/logs/pulse-stderr.log
ThrottleInterval
30
diff --git a/LifeOS/install/LIFEOS/PULSE/com.lifeos.pulse.service b/LifeOS/install/LIFEOS/PULSE/com.lifeos.pulse.service
index abc295fc30..536641042f 100644
--- a/LifeOS/install/LIFEOS/PULSE/com.lifeos.pulse.service
+++ b/LifeOS/install/LIFEOS/PULSE/com.lifeos.pulse.service
@@ -5,14 +5,15 @@ After=network.target
[Service]
Type=simple
ExecStart=__BUN_PATH__ run pulse.ts
-WorkingDirectory=__HOME__/.claude/LIFEOS/PULSE
+WorkingDirectory=__LIFEOS_DIR__/PULSE
Restart=on-failure
RestartSec=30
TimeoutStopSec=5
Environment=HOME=__HOME__
Environment=PATH=__HOME__/.bun/bin:/usr/local/bin:/usr/bin:/bin
-StandardOutput=append:__HOME__/.claude/LIFEOS/PULSE/logs/pulse-stdout.log
-StandardError=append:__HOME__/.claude/LIFEOS/PULSE/logs/pulse-stderr.log
+Environment=LIFEOS_DIR=__LIFEOS_DIR__
+StandardOutput=append:__LIFEOS_DIR__/PULSE/logs/pulse-stdout.log
+StandardError=append:__LIFEOS_DIR__/PULSE/logs/pulse-stderr.log
[Install]
WantedBy=default.target
diff --git a/LifeOS/install/LIFEOS/PULSE/edit/edit-handler.ts b/LifeOS/install/LIFEOS/PULSE/edit/edit-handler.ts
index 5eefb80f27..15fc5482e6 100644
--- a/LifeOS/install/LIFEOS/PULSE/edit/edit-handler.ts
+++ b/LifeOS/install/LIFEOS/PULSE/edit/edit-handler.ts
@@ -3,10 +3,11 @@ import { resolve, join } from "node:path";
import { atomicWriteText } from "../lib/atomic-write";
import { parseFrontmatter, serializeFrontmatter } from "../lib/frontmatter";
import { sha256Hex } from "../lib/cache";
+import { claudeDir } from "../../TOOLS/lifeos-root";
const HOME = process.env.HOME!;
-const USER_ROOT = resolve(HOME, ".claude", "LIFEOS", "USER");
-const EDITS_LOG = resolve(HOME, ".claude", "LIFEOS", "MEMORY", "OBSERVABILITY", "pulse-edits.jsonl");
+const USER_ROOT = resolve(claudeDir(), "LIFEOS", "USER");
+const EDITS_LOG = resolve(claudeDir(), "LIFEOS", "MEMORY", "OBSERVABILITY", "pulse-edits.jsonl");
const CONTAINMENT_PREFIX_DENY = ["MEMORY/PULSE_DATA", "MEMORY/OBSERVABILITY"];
export interface EditRequest {
@@ -32,7 +33,7 @@ function isInUserTree(absPath: string): boolean {
}
function isContainmentPath(absPath: string): boolean {
- const rel = absPath.replace(resolve(HOME, ".claude", "LIFEOS") + "/", "");
+ const rel = absPath.replace(resolve(claudeDir(), "LIFEOS") + "/", "");
return CONTAINMENT_PREFIX_DENY.some((p) => rel.startsWith(p));
}
diff --git a/LifeOS/install/LIFEOS/PULSE/lib.ts b/LifeOS/install/LIFEOS/PULSE/lib.ts
index 5ba5cb96fa..00f9b326ad 100644
--- a/LifeOS/install/LIFEOS/PULSE/lib.ts
+++ b/LifeOS/install/LIFEOS/PULSE/lib.ts
@@ -10,6 +10,7 @@ import { join } from "path"
import { existsSync } from "fs"
import { rename } from "fs/promises"
import { modelForEffort } from "../TOOLS/models.ts"
+import { claudeDir } from "../TOOLS/lifeos-root";
// ── Types ──
@@ -48,9 +49,7 @@ export interface DaemonConfig {
// written here is automatically stripped from shadow releases. That's the
// structural privacy lever — no separate scrub policy needed.
-export const USER_CRON_PATH = join(
- process.env.HOME ?? "~",
- ".claude", "LIFEOS", "USER", "CONFIG", "PULSE.user.toml",
+export const USER_CRON_PATH = join(claudeDir(), "LIFEOS", "USER", "CONFIG", "PULSE.user.toml",
)
export interface JobState {
@@ -319,7 +318,7 @@ export async function spawnScript(command: string, timeoutMs = 60_000): Promise<
const proc = Bun.spawn([BASH_PATH, "-c", command], {
stdout: "pipe",
stderr: "pipe",
- cwd: join(process.env.HOME ?? "~", ".claude", "LIFEOS", "PULSE"),
+ cwd: join(claudeDir(), "LIFEOS", "PULSE"),
env: { ...process.env },
})
diff --git a/LifeOS/install/LIFEOS/PULSE/lib/data-plane.ts b/LifeOS/install/LIFEOS/PULSE/lib/data-plane.ts
index 096a65df15..b8961e2672 100644
--- a/LifeOS/install/LIFEOS/PULSE/lib/data-plane.ts
+++ b/LifeOS/install/LIFEOS/PULSE/lib/data-plane.ts
@@ -3,9 +3,10 @@ import { resolve, join } from "node:path";
import { paiRoot } from "./manifest-loader";
import { atomicWriteJSON } from "./atomic-write";
import type { PageData, PageMeta, Provenance } from "../Schema/PulseSchema";
+import { claudeDir } from "../../TOOLS/lifeos-root";
const HOME = process.env.HOME!;
-export const PULSE_DATA_DIR = resolve(HOME, ".claude", "LIFEOS", "MEMORY", "PULSE_DATA");
+export const PULSE_DATA_DIR = resolve(claudeDir(), "LIFEOS", "MEMORY", "PULSE_DATA");
export interface DataPlaneFile {
schemaVersion: string;
diff --git a/LifeOS/install/LIFEOS/PULSE/lib/provenance-watcher.ts b/LifeOS/install/LIFEOS/PULSE/lib/provenance-watcher.ts
index dd2e1297e8..449ab46554 100644
--- a/LifeOS/install/LIFEOS/PULSE/lib/provenance-watcher.ts
+++ b/LifeOS/install/LIFEOS/PULSE/lib/provenance-watcher.ts
@@ -3,9 +3,10 @@ import { watch } from "node:fs";
import { resolve } from "node:path";
import { atomicWriteText } from "./atomic-write";
import { parseFrontmatter, serializeFrontmatter } from "./frontmatter";
+import { claudeDir } from "../../TOOLS/lifeos-root";
const HOME = process.env.HOME!;
-const EDITS_LOG = resolve(HOME, ".claude", "LIFEOS", "MEMORY", "OBSERVABILITY", "pulse-edits.jsonl");
+const EDITS_LOG = resolve(claudeDir(), "LIFEOS", "MEMORY", "OBSERVABILITY", "pulse-edits.jsonl");
const PULSE_EDIT_GRACE_MS = 5_000;
export interface WatcherOptions {
diff --git a/LifeOS/install/LIFEOS/PULSE/lib/telegram-proposals.ts b/LifeOS/install/LIFEOS/PULSE/lib/telegram-proposals.ts
index a8dd519fd5..e320001dc9 100644
--- a/LifeOS/install/LIFEOS/PULSE/lib/telegram-proposals.ts
+++ b/LifeOS/install/LIFEOS/PULSE/lib/telegram-proposals.ts
@@ -14,6 +14,7 @@
import { appendFileSync, existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
+import { claudeDir } from "../../TOOLS/lifeos-root";
import {
PENDING_PROPOSALS_PATH,
inferProposalKind,
@@ -21,7 +22,7 @@ import {
} from "../../TOOLS/MemoryTypes";
const HOME = process.env.HOME ?? homedir();
-const OBS_DIR = join(HOME, ".claude", "LIFEOS", "MEMORY", "OBSERVABILITY");
+const OBS_DIR = join(claudeDir(), "LIFEOS", "MEMORY", "OBSERVABILITY");
const PROPOSAL_REPLIES_LOG_PATH = join(OBS_DIR, "proposal-replies.jsonl");
const IDENTITY_PROPOSALS_LOG_PATH = join(OBS_DIR, "identity-proposals.jsonl");
@@ -123,7 +124,7 @@ export function logProposalReply(event: Record, path: string =
}
export function formatProposalMessage(p: ProposalRow, home: string = HOME): string {
- const fileLabel = p.target_file.replace(`${home}/.claude/`, "");
+ const fileLabel = p.target_file.replace(`${claudeDir()}/`, "");
const conf = p.confidence.toFixed(2);
const obs = p.observed_across_sessions ?? 1;
// P1 2026-05-25: prepend subtype badge so the principal sees at a glance
diff --git a/LifeOS/install/LIFEOS/PULSE/lib/telegram-sessions.ts b/LifeOS/install/LIFEOS/PULSE/lib/telegram-sessions.ts
index 809ca96410..5697336e10 100644
--- a/LifeOS/install/LIFEOS/PULSE/lib/telegram-sessions.ts
+++ b/LifeOS/install/LIFEOS/PULSE/lib/telegram-sessions.ts
@@ -17,9 +17,10 @@
import { Database } from "bun:sqlite"
import { join } from "path"
import { mkdirSync } from "fs"
+import { claudeDir } from "../../TOOLS/lifeos-root";
const HOME = process.env.HOME ?? ""
-const DB_DIR = join(HOME, ".claude", "LIFEOS", "PULSE", "state", "telegram")
+const DB_DIR = join(claudeDir(), "LIFEOS", "PULSE", "state", "telegram")
const DB_PATH = join(DB_DIR, "sessions.db")
// Compaction threshold — after this many turns, we'll compact the session
diff --git a/LifeOS/install/LIFEOS/PULSE/manage-deriver.sh b/LifeOS/install/LIFEOS/PULSE/manage-deriver.sh
index df012bf14e..ab827fe7bd 100755
--- a/LifeOS/install/LIFEOS/PULSE/manage-deriver.sh
+++ b/LifeOS/install/LIFEOS/PULSE/manage-deriver.sh
@@ -13,11 +13,15 @@
set -euo pipefail
-PULSE_DIR="$HOME/.claude/LIFEOS/PULSE"
+# Derive paths from this script's OWN location (ships in /LIFEOS/PULSE),
+# not a hardcoded ~/.claude — correct for default AND custom LifeOS homes.
+PULSE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+LIFEOS_DIR_RESOLVED="$(dirname "$PULSE_DIR")"
+CLAUDE_DIR_RESOLVED="$(dirname "$LIFEOS_DIR_RESOLVED")"
PLIST_NAME="com.lifeos.deriver"
PLIST_SRC="$PULSE_DIR/$PLIST_NAME.plist"
PLIST_DST="$HOME/Library/LaunchAgents/$PLIST_NAME.plist"
-OBSERVABILITY_DIR="$HOME/.claude/LIFEOS/MEMORY/OBSERVABILITY"
+OBSERVABILITY_DIR="$LIFEOS_DIR_RESOLVED/MEMORY/OBSERVABILITY"
if [ -x "$HOME/.bun/bin/bun" ]; then
BUN_PATH="$HOME/.bun/bin/bun"
@@ -49,7 +53,7 @@ case "${1:-}" in
if [ -f "$PLIST_DST" ]; then
launchctl unload "$PLIST_DST" 2>/dev/null || true
fi
- sed -e "s|__HOME__|$HOME|g" -e "s|__BUN_PATH__|$BUN_PATH|g" "$PLIST_SRC" > "$PLIST_DST"
+ sed -e "s|__LIFEOS_DIR__|$LIFEOS_DIR_RESOLVED|g" -e "s|__CLAUDE_DIR__|$CLAUDE_DIR_RESOLVED|g" -e "s|__HOME__|$HOME|g" -e "s|__BUN_PATH__|$BUN_PATH|g" "$PLIST_SRC" > "$PLIST_DST"
launchctl load "$PLIST_DST"
echo "LifeOS deriver installed (bun: $BUN_PATH, schedule: daily 03:00)"
;;
@@ -81,7 +85,7 @@ case "${1:-}" in
run-now)
# One-shot manual invocation for testing / first-run priming.
- exec "$BUN_PATH" run "$HOME/.claude/LIFEOS/TOOLS/LearningPatternSynthesis.ts" --hypothesize
+ exec "$BUN_PATH" run "$LIFEOS_DIR_RESOLVED/TOOLS/LearningPatternSynthesis.ts" --hypothesize
;;
*)
diff --git a/LifeOS/install/LIFEOS/PULSE/manage.sh b/LifeOS/install/LIFEOS/PULSE/manage.sh
index e08ecd0929..3db47d611d 100755
--- a/LifeOS/install/LIFEOS/PULSE/manage.sh
+++ b/LifeOS/install/LIFEOS/PULSE/manage.sh
@@ -2,7 +2,13 @@
# LifeOS Pulse — Process Management
# Usage: manage.sh {start|stop|restart|status|install|uninstall}
-PULSE_DIR="$HOME/.claude/LIFEOS/PULSE"
+# Resolve PULSE_DIR from this script's OWN location — not a hardcoded ~/.claude.
+# manage.sh always ships alongside the PULSE tree it manages, so this is correct for
+# both the default install (~/.claude/LIFEOS/PULSE) and a custom LifeOS home
+# (LIFEOS_HOME install, e.g. ~/Project/.claude/LIFEOS/PULSE). __LIFEOS_DIR__ in the
+# plist/service template is substituted with the derived /LIFEOS below.
+PULSE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+LIFEOS_DIR_RESOLVED="$(dirname "$PULSE_DIR")"
PLIST_NAME="com.lifeos.pulse"
PLIST_SRC="$PULSE_DIR/$PLIST_NAME.plist"
PLIST_DST="$HOME/Library/LaunchAgents/$PLIST_NAME.plist"
@@ -43,7 +49,7 @@ case "$1" in
if [ ! -f "$PLIST_DST" ]; then
# Substitute __HOME__ + __BUN_PATH__ placeholders (public template);
# no-op on plists that already have literal paths.
- sed -e "s|__HOME__|$HOME|g" -e "s|__BUN_PATH__|$BUN_PATH|g" "$PLIST_SRC" > "$PLIST_DST"
+ sed -e "s|__LIFEOS_DIR__|$LIFEOS_DIR_RESOLVED|g" -e "s|__HOME__|$HOME|g" -e "s|__BUN_PATH__|$BUN_PATH|g" "$PLIST_SRC" > "$PLIST_DST"
fi
launchctl load "$PLIST_DST" 2>/dev/null
echo "LifeOS Pulse started"
@@ -118,7 +124,7 @@ case "$1" in
sleep 1
# Substitute __HOME__ + __BUN_PATH__ placeholders (public template);
# no-op on service files that already have literal paths.
- sed -e "s|__HOME__|$HOME|g" -e "s|__BUN_PATH__|$BUN_PATH|g" "$SERVICE_SRC" > "$SERVICE_DST"
+ sed -e "s|__LIFEOS_DIR__|$LIFEOS_DIR_RESOLVED|g" -e "s|__HOME__|$HOME|g" -e "s|__BUN_PATH__|$BUN_PATH|g" "$SERVICE_SRC" > "$SERVICE_DST"
# Ensure user services survive logout/reboot (no-op if already enabled)
loginctl enable-linger "$USER" 2>/dev/null || true
systemctl --user daemon-reload
@@ -136,7 +142,7 @@ case "$1" in
# Substitute __HOME__ + __BUN_PATH__ placeholders (public template);
# no-op on plists that already have literal paths.
- sed -e "s|__HOME__|$HOME|g" -e "s|__BUN_PATH__|$BUN_PATH|g" "$PLIST_SRC" > "$PLIST_DST"
+ sed -e "s|__LIFEOS_DIR__|$LIFEOS_DIR_RESOLVED|g" -e "s|__HOME__|$HOME|g" -e "s|__BUN_PATH__|$BUN_PATH|g" "$PLIST_SRC" > "$PLIST_DST"
launchctl load "$PLIST_DST"
fi
@@ -168,8 +174,16 @@ case "$1" in
echo "LifeOS Pulse uninstalled"
;;
+ render)
+ # Dry-run: print the substituted launchd plist (macOS) or systemd unit (Linux)
+ # to stdout WITHOUT loading it. Lets you inspect the wiring — and the install
+ # integration test assert it targets the resolved (custom) home, not ~/.claude.
+ if [ "$OS" = "Linux" ]; then SRC="$SERVICE_SRC"; else SRC="$PLIST_SRC"; fi
+ sed -e "s|__LIFEOS_DIR__|$LIFEOS_DIR_RESOLVED|g" -e "s|__HOME__|$HOME|g" -e "s|__BUN_PATH__|$BUN_PATH|g" "$SRC"
+ ;;
+
*)
- echo "Usage: $0 {start|stop|restart|status|install|uninstall}"
+ echo "Usage: $0 {start|stop|restart|status|install|uninstall|render}"
exit 1
;;
esac
diff --git a/LifeOS/install/LIFEOS/PULSE/modules/amber.ts b/LifeOS/install/LIFEOS/PULSE/modules/amber.ts
index 8dca82153a..eb172c5231 100644
--- a/LifeOS/install/LIFEOS/PULSE/modules/amber.ts
+++ b/LifeOS/install/LIFEOS/PULSE/modules/amber.ts
@@ -20,9 +20,10 @@
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
+import { claudeDir } from "../../TOOLS/lifeos-root";
const MODULE_NAME = "amber";
-const HOME = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
+const HOME = process.env.CLAUDE_CONFIG_DIR || join(claudeDir());
const KNOWLEDGE_DIR = join(HOME, "LIFEOS", "MEMORY", "KNOWLEDGE");
const X_STATE_DIR = join(HOME, "skills", "_X", "State");
const ENV_PATH = join(HOME, ".env");
diff --git a/LifeOS/install/LIFEOS/PULSE/modules/assets.ts b/LifeOS/install/LIFEOS/PULSE/modules/assets.ts
index 1513b858ec..6877fd89d2 100644
--- a/LifeOS/install/LIFEOS/PULSE/modules/assets.ts
+++ b/LifeOS/install/LIFEOS/PULSE/modules/assets.ts
@@ -18,9 +18,10 @@
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
+import { claudeDir } from "../../TOOLS/lifeos-root";
const MODULE_NAME = "assets";
-const CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
+const CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR || join(claudeDir());
const GEAR_PATH = join(CLAUDE_DIR, "LIFEOS", "USER", "GEAR.md");
const NETWORK_DIR = join(CLAUDE_DIR, "LIFEOS", "MEMORY", "_NETWORK");
const NETWORK_ASSETS_JSON = join(NETWORK_DIR, "assets.json");
diff --git a/LifeOS/install/LIFEOS/PULSE/modules/books.ts b/LifeOS/install/LIFEOS/PULSE/modules/books.ts
index c9e9faf767..f2fab1b2ba 100644
--- a/LifeOS/install/LIFEOS/PULSE/modules/books.ts
+++ b/LifeOS/install/LIFEOS/PULSE/modules/books.ts
@@ -8,9 +8,10 @@
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
+import { claudeDir } from "../../TOOLS/lifeos-root";
const MODULE_NAME = "books";
-const BOOKS_PATH = join(process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"), "LIFEOS", "USER", "BOOKS.md");
+const BOOKS_PATH = join(process.env.CLAUDE_CONFIG_DIR || join(claudeDir()), "LIFEOS", "USER", "BOOKS.md");
const state = { running: false };
interface Book {
diff --git a/LifeOS/install/LIFEOS/PULSE/modules/content.ts b/LifeOS/install/LIFEOS/PULSE/modules/content.ts
index ea7ccffa6b..1c755e746c 100644
--- a/LifeOS/install/LIFEOS/PULSE/modules/content.ts
+++ b/LifeOS/install/LIFEOS/PULSE/modules/content.ts
@@ -22,6 +22,7 @@ import { existsSync, mkdirSync, renameSync, rmSync, watch } from "fs";
import { basename, dirname, join } from "path";
import { homedir } from "os";
import { LEGS, appendEvents, eventsPath, readState } from "../../TOOLS/Conveyor/Ledger";
+import { claudeDir } from "../../TOOLS/lifeos-root";
const MODULE = "content";
const STAGES = ["inbox", "prep", "produce", "review", "publishing", "done"] as const;
@@ -160,7 +161,7 @@ function deleteItem(id: string): Response {
// 3. Artifacts (audio, transcript, derivatives).
const artifacts = join(
- process.env.LIFEOS_DIR || join(homedir(), ".claude", "LIFEOS"),
+ process.env.LIFEOS_DIR || join(claudeDir(), "LIFEOS"),
"MEMORY", "STATE", "content-pipeline", "artifacts", id,
);
try {
diff --git a/LifeOS/install/LIFEOS/PULSE/modules/doctor.ts b/LifeOS/install/LIFEOS/PULSE/modules/doctor.ts
index 878a4aa06b..133f39139f 100644
--- a/LifeOS/install/LIFEOS/PULSE/modules/doctor.ts
+++ b/LifeOS/install/LIFEOS/PULSE/modules/doctor.ts
@@ -13,9 +13,10 @@
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
+import { claudeDir } from "../../TOOLS/lifeos-root";
const MODULE_NAME = "doctor";
-const CONFIG_ROOT = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
+const CONFIG_ROOT = process.env.CLAUDE_CONFIG_DIR || join(claudeDir());
const LIFEOS_DIR = join(CONFIG_ROOT, "LIFEOS");
const STATE_DIR = join(LIFEOS_DIR, "MEMORY", "STATE");
const MANIFEST_PATH = join(STATE_DIR, "capabilities.json");
diff --git a/LifeOS/install/LIFEOS/PULSE/modules/hooks.ts b/LifeOS/install/LIFEOS/PULSE/modules/hooks.ts
index 95607e5234..78c00071ad 100644
--- a/LifeOS/install/LIFEOS/PULSE/modules/hooks.ts
+++ b/LifeOS/install/LIFEOS/PULSE/modules/hooks.ts
@@ -5,6 +5,9 @@
* Validates skill and agent tool calls via HTTP hooks.
*/
+import { join } from "node:path"
+import { claudeDir, shellQuote } from "../../TOOLS/lifeos-root"
+
// ── Types ──
export interface HooksConfig {
@@ -111,11 +114,12 @@ function handleAgentGuard(body: {
if (ti.run_in_background === true) {
stats.agentGuard.passed++
const name = ti.description || ti.name || ti.subagent_type || "unknown"
+ const watchdog = shellQuote(join(claudeDir(), "LIFEOS", "TOOLS", "AgentWatchdog.ts"))
return Response.json({
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "allow",
- additionalContext: `WATCHDOG: Background agent "${name}" launching. If not already running, start an agent watchdog Monitor:\nMonitor({ description: "Agent watchdog", persistent: true, timeout_ms: 3600000, command: "bun $HOME/.claude/LIFEOS/TOOLS/AgentWatchdog.ts" })`,
+ additionalContext: `WATCHDOG: Background agent "${name}" launching. If not already running, start an agent watchdog Monitor:\nMonitor({ description: "Agent watchdog", persistent: true, timeout_ms: 3600000, command: "bun ${watchdog}" })`,
},
})
}
diff --git a/LifeOS/install/LIFEOS/PULSE/modules/hypotheses.ts b/LifeOS/install/LIFEOS/PULSE/modules/hypotheses.ts
index c3638efb0e..bc2aea6a6d 100644
--- a/LifeOS/install/LIFEOS/PULSE/modules/hypotheses.ts
+++ b/LifeOS/install/LIFEOS/PULSE/modules/hypotheses.ts
@@ -33,6 +33,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
import { existsSync, readFileSync, readdirSync, writeFileSync, unlinkSync, mkdirSync } from "fs";
import { join } from "path";
+import { claudeDir } from "../../TOOLS/lifeos-root";
// Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404)
for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
@@ -42,7 +43,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
const HOME = process.env.HOME || "";
-const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS");
+const LIFEOS_DIR = process.env.LIFEOS_DIR || join(claudeDir(), "LIFEOS");
const FRAMES_DIR = join(LIFEOS_DIR, "MEMORY", "WISDOM", "FRAMES");
const HYPOTHESES_DIR = join(FRAMES_DIR, "_hypotheses");
const ARCHIVE_DIR = join(HYPOTHESES_DIR, "_archive");
diff --git a/LifeOS/install/LIFEOS/PULSE/modules/imessage.ts b/LifeOS/install/LIFEOS/PULSE/modules/imessage.ts
index 003b855f09..6b13fe5940 100644
--- a/LifeOS/install/LIFEOS/PULSE/modules/imessage.ts
+++ b/LifeOS/install/LIFEOS/PULSE/modules/imessage.ts
@@ -28,6 +28,7 @@ import { sendMessage } from "../lib/imessage-send"
import { join } from "path"
import { appendFile, mkdir, rename } from "fs/promises"
import { stripModeScaffolding, hasModeScaffolding } from "../lib/strip-mode-scaffolding"
+import { claudeDir } from "../../TOOLS/lifeos-root";
// BILLING: Strip ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN before any SDK
// query() call. Same rationale as modules/telegram.ts — both outrank OAuth in
@@ -62,9 +63,9 @@ export interface IMessageHealth {
// ── Module State ──
const HOME = process.env.HOME ?? ""
-const CWD = join(HOME, ".claude")
-const STATE_DIR = join(HOME, ".claude", "LIFEOS", "PULSE", "state", "imessage")
-const LOGS_DIR = join(HOME, ".claude", "LIFEOS", "PULSE", "logs", "imessage")
+const CWD = join(claudeDir())
+const STATE_DIR = join(claudeDir(), "LIFEOS", "PULSE", "state", "imessage")
+const LOGS_DIR = join(claudeDir(), "LIFEOS", "PULSE", "logs", "imessage")
let pollTimer: ReturnType | null = null
let running = false
diff --git a/LifeOS/install/LIFEOS/PULSE/modules/local-intelligence.ts b/LifeOS/install/LIFEOS/PULSE/modules/local-intelligence.ts
index b2534fd661..ae3d5f1998 100644
--- a/LifeOS/install/LIFEOS/PULSE/modules/local-intelligence.ts
+++ b/LifeOS/install/LIFEOS/PULSE/modules/local-intelligence.ts
@@ -19,19 +19,20 @@ import { readFile, mkdir, writeFile, stat } from "node:fs/promises"
import { join } from "node:path"
import { homedir } from "node:os"
import { randomUUID } from "node:crypto"
+import { claudeDir } from "../../TOOLS/lifeos-root";
const HOME = process.env.HOME ?? homedir()
const MODULE_NAME = "local-intelligence"
// Primary path: user-scoped customizations directory (per {{PRINCIPAL_NAME}} directive 2026-05-03).
// Fallback path: legacy MEMORY/DATA path (used when customizations file absent).
-const CUSTOMIZATIONS_DIR = join(HOME, ".claude", "LIFEOS", "USER", "CUSTOMIZATIONS", "SKILLS", "LocalIntelligence")
-const LEGACY_DATA_DIR = join(HOME, ".claude", "LIFEOS", "MEMORY", "DATA", "LocalIntelligence")
+const CUSTOMIZATIONS_DIR = join(claudeDir(), "LIFEOS", "USER", "CUSTOMIZATIONS", "SKILLS", "LocalIntelligence")
+const LEGACY_DATA_DIR = join(claudeDir(), "LIFEOS", "MEMORY", "DATA", "LocalIntelligence")
const LATEST_PATH = join(CUSTOMIZATIONS_DIR, "latest.json")
const LEGACY_LATEST_PATH = join(LEGACY_DATA_DIR, "latest.json")
const DATA_DIR = CUSTOMIZATIONS_DIR // alias for existing references in this file
const RUNS_DIR = join(CUSTOMIZATIONS_DIR, "runs")
-const REFRESH_SCRIPT = join(HOME, ".claude", "skills", "LocalIntelligence", "Tools", "Refresh.ts")
+const REFRESH_SCRIPT = join(claudeDir(), "skills", "LocalIntelligence", "Tools", "Refresh.ts")
async function readLatest(): Promise {
try { return await readFile(LATEST_PATH, "utf8") } catch {}
diff --git a/LifeOS/install/LIFEOS/PULSE/modules/memory.ts b/LifeOS/install/LIFEOS/PULSE/modules/memory.ts
index 22db4510aa..559f965a40 100644
--- a/LifeOS/install/LIFEOS/PULSE/modules/memory.ts
+++ b/LifeOS/install/LIFEOS/PULSE/modules/memory.ts
@@ -28,9 +28,10 @@ import {
statSync,
} from "node:fs";
import { join } from "node:path";
+import { claudeDir } from "../../TOOLS/lifeos-root";
const HOME = process.env.HOME || "";
-const CLAUDE = join(HOME, ".claude");
+const CLAUDE = join(claudeDir());
const OBS_DIR = join(CLAUDE, "LIFEOS/MEMORY/OBSERVABILITY");
const REVIEW_STATE = join(OBS_DIR, "review-state.json");
diff --git a/LifeOS/install/LIFEOS/PULSE/modules/menubar.ts b/LifeOS/install/LIFEOS/PULSE/modules/menubar.ts
index 0b41ebee93..f065a7ed91 100644
--- a/LifeOS/install/LIFEOS/PULSE/modules/menubar.ts
+++ b/LifeOS/install/LIFEOS/PULSE/modules/menubar.ts
@@ -16,11 +16,12 @@
import { existsSync, readFileSync, statSync } from "node:fs"
import { homedir } from "node:os"
import { join } from "node:path"
+import { claudeDir } from "../../TOOLS/lifeos-root";
const MODULE_NAME = "menubar"
const state = { running: false, startedAt: null as Date | null }
-const CLAUDE = join(homedir(), ".claude")
+const CLAUDE = join(claudeDir())
const LIFEOS = join(CLAUDE, "LIFEOS")
const OBS = join(LIFEOS, "MEMORY", "OBSERVABILITY")
const STATE_DIR = join(LIFEOS, "PULSE", "state")
diff --git a/LifeOS/install/LIFEOS/PULSE/modules/projects.ts b/LifeOS/install/LIFEOS/PULSE/modules/projects.ts
index a1c2928b6c..77acc21a8a 100644
--- a/LifeOS/install/LIFEOS/PULSE/modules/projects.ts
+++ b/LifeOS/install/LIFEOS/PULSE/modules/projects.ts
@@ -12,10 +12,11 @@
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
+import { claudeDir } from "../../TOOLS/lifeos-root";
const MODULE_NAME = "projects";
const PROJECTS_PATH = join(
- process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"),
+ process.env.CLAUDE_CONFIG_DIR || join(claudeDir()),
"LIFEOS",
"USER",
"PROJECTS.md",
diff --git a/LifeOS/install/LIFEOS/PULSE/modules/siri.ts b/LifeOS/install/LIFEOS/PULSE/modules/siri.ts
index 9c1daf1f23..901b06e18e 100644
--- a/LifeOS/install/LIFEOS/PULSE/modules/siri.ts
+++ b/LifeOS/install/LIFEOS/PULSE/modules/siri.ts
@@ -19,8 +19,9 @@
import { query } from "@anthropic-ai/claude-agent-sdk"
import { buildLifeosContextBlock } from "./telegram"
+import { claudeDir } from "../../TOOLS/lifeos-root";
-const CWD = `${process.env.HOME}/.claude`
+const CWD = `${claudeDir()}`
const IDLE_TIMEOUT_MS = 60 * 60 * 1000 // 60 min — same thread boundary as Telegram
const SDK_TIMEOUT_MS = 50_000 // Shortcuts' Get Contents of URL times out ~60s; stay under it
const MAX_TURNS = 10 // speed over depth — this is a spoken exchange, not a work session
diff --git a/LifeOS/install/LIFEOS/PULSE/modules/syslog.ts b/LifeOS/install/LIFEOS/PULSE/modules/syslog.ts
index d2bbca59bb..419834e7b2 100644
--- a/LifeOS/install/LIFEOS/PULSE/modules/syslog.ts
+++ b/LifeOS/install/LIFEOS/PULSE/modules/syslog.ts
@@ -15,15 +15,14 @@
import { createSocket, type Socket } from "dgram"
import { appendFileSync, mkdirSync, existsSync, statSync, readFileSync } from "fs"
import { dirname, join } from "path"
+import { claudeDir } from "../../TOOLS/lifeos-root";
const HOME = process.env.HOME ?? ""
const MODULE_NAME = "syslog"
const DEFAULT_PORT = 5514
const MAX_FILE_SIZE = 50 * 1024 * 1024 // 50 MB rotation threshold
-const LOG_PATH = join(
- HOME,
- ".claude",
+const LOG_PATH = join(claudeDir(),
"LIFEOS",
"MEMORY",
"OBSERVABILITY",
diff --git a/LifeOS/install/LIFEOS/PULSE/modules/tab-freshness.ts b/LifeOS/install/LIFEOS/PULSE/modules/tab-freshness.ts
index 4c2cee6d9a..2a928e10a4 100644
--- a/LifeOS/install/LIFEOS/PULSE/modules/tab-freshness.ts
+++ b/LifeOS/install/LIFEOS/PULSE/modules/tab-freshness.ts
@@ -28,9 +28,10 @@
import { existsSync, statSync, readdirSync, readFileSync } from "fs"
import { join } from "path"
+import { claudeDir } from "../../TOOLS/lifeos-root";
const HOME = process.env.HOME ?? "~"
-const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS")
+const LIFEOS_DIR = join(claudeDir(), "LIFEOS")
const USER_DIR = join(LIFEOS_DIR, "USER")
const TELOS_DIR = join(USER_DIR, "TELOS")
@@ -82,14 +83,14 @@ const REGISTRY: Record = {
{ name: "KNOWLEDGE/", path: join(LIFEOS_DIR, "MEMORY", "KNOWLEDGE"), expand: true },
],
hooks: [
- { name: "hooks/", path: join(HOME, ".claude", "hooks"), expand: true },
- { name: "settings.json", path: join(HOME, ".claude", "settings.json") },
+ { name: "hooks/", path: join(claudeDir(), "hooks"), expand: true },
+ { name: "settings.json", path: join(claudeDir(), "settings.json") },
],
skills: [
- { name: "skills/", path: join(HOME, ".claude", "skills"), expand: true },
+ { name: "skills/", path: join(claudeDir(), "skills"), expand: true },
],
agents: [
- { name: "agents/", path: join(HOME, ".claude", "agents"), expand: true },
+ { name: "agents/", path: join(claudeDir(), "agents"), expand: true },
],
docs: [
{ name: "DOCUMENTATION/", path: join(LIFEOS_DIR, "DOCUMENTATION"), expand: true },
@@ -105,7 +106,7 @@ const REGISTRY: Record = {
],
amber: [
{ name: "KNOWLEDGE/Ideas/", path: join(LIFEOS_DIR, "MEMORY", "KNOWLEDGE", "Ideas"), expand: true },
- { name: "_X/State/", path: join(HOME, ".claude", "skills", "_X", "State") },
+ { name: "_X/State/", path: join(claudeDir(), "skills", "_X", "State") },
],
assistant: [
{ name: "DA_IDENTITY.md", path: join(USER_DIR, "DIGITAL_ASSISTANT", "DA_IDENTITY.md") },
diff --git a/LifeOS/install/LIFEOS/PULSE/modules/telegram.ts b/LifeOS/install/LIFEOS/PULSE/modules/telegram.ts
index 41512798d3..8b5b5b27e7 100644
--- a/LifeOS/install/LIFEOS/PULSE/modules/telegram.ts
+++ b/LifeOS/install/LIFEOS/PULSE/modules/telegram.ts
@@ -32,6 +32,7 @@ import {
type ProposalReply,
} from "../lib/telegram-proposals"
import { stripModeScaffolding, hasModeScaffolding } from "../lib/strip-mode-scaffolding"
+import { claudeDir } from "../../TOOLS/lifeos-root";
// BILLING: Strip ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN before any SDK
// query() call. Bun auto-loads ~/.claude/.env into this process; if either key
@@ -67,9 +68,9 @@ export interface TelegramConfig {
// ── Constants ──
const HOME = process.env.HOME ?? ""
-const CWD = join(HOME, ".claude")
-const STATE_DIR = join(HOME, ".claude", "LIFEOS", "PULSE", "state", "telegram")
-const LOGS_DIR = join(HOME, ".claude", "LIFEOS", "PULSE", "logs", "telegram")
+const CWD = join(claudeDir())
+const STATE_DIR = join(claudeDir(), "LIFEOS", "PULSE", "state", "telegram")
+const LOGS_DIR = join(claudeDir(), "LIFEOS", "PULSE", "logs", "telegram")
const STALE_ACK_CACHE_DIR = join(STATE_DIR, "ack-cache")
const MAX_TELEGRAM_LENGTH = 4096
const CURSOR = " ▌"
@@ -80,7 +81,7 @@ const IDLE_TIMEOUT_MS = 60 * 60 * 1000 // 1 hour — gap of silence tha
const INFERENCE_HARD_BUDGET_MS = 10_000 // outer race cap on summarize; measured Sonnet subprocess cost is 4-6s, this gives slack without losing the voice trailing the text by too much
const MIN_FALLBACK_WORDS = 6 // a fallback summary shorter than this is presumed too thin to be worth voicing
const MEANINGFUL_REPLY_WORDS = 25 // when a reply is at least this long, a too-short fallback is a regression — skip voice rather than ship a "0:00" stub
-const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS")
+const LIFEOS_DIR = join(claudeDir(), "LIFEOS")
// ── Bidirectional Telegram images (ported from public PR #1384, @klausagnoletti) ──
//
@@ -487,7 +488,7 @@ async function handleProposalReply(chatId: number, reply: ProposalReply, ctx: {
markProposal(row.id, { status: "accepted", resolved_at: new Date().toISOString(), applied_edit: row.edit })
logProposalEvent({ id: row.id, file: row.target_file, edit: row.edit, confidence: row.confidence, status: "accepted" })
logProposalReply({ kind: "yes", id: row.id, outcome: "applied", chatId })
- const fileLabel = row.target_file.replace(`${HOME}/.claude/`, "")
+ const fileLabel = row.target_file.replace(`${claudeDir()}/`, "")
await ctx.reply(`✅ Applied to ${fileLabel}`).catch(() => {})
} else {
logProposalReply({ kind: "yes", id: row.id, outcome: "apply-failed", reason: result.reason, chatId })
@@ -514,7 +515,7 @@ async function handleProposalReply(chatId: number, reply: ProposalReply, ctx: {
markProposal(row.id, { status: "edited", resolved_at: new Date().toISOString(), applied_edit: reply.editText })
logProposalEvent({ id: row.id, file: row.target_file, edit: reply.editText, confidence: row.confidence, status: "edited" })
logProposalReply({ kind: "edit", id: row.id, outcome: "applied", chatId })
- const fileLabel = row.target_file.replace(`${HOME}/.claude/`, "")
+ const fileLabel = row.target_file.replace(`${claudeDir()}/`, "")
await ctx.reply(`✅ Applied your edit to ${fileLabel}`).catch(() => {})
} else {
logProposalReply({ kind: "edit", id: row.id, outcome: "apply-failed", reason: result.reason, chatId })
diff --git a/LifeOS/install/LIFEOS/PULSE/modules/usage.ts b/LifeOS/install/LIFEOS/PULSE/modules/usage.ts
index e17f12dca1..8f3941f830 100644
--- a/LifeOS/install/LIFEOS/PULSE/modules/usage.ts
+++ b/LifeOS/install/LIFEOS/PULSE/modules/usage.ts
@@ -16,9 +16,10 @@
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
+import { claudeDir } from "../../TOOLS/lifeos-root";
const MODULE_NAME = "usage";
-const CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
+const CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR || join(claudeDir());
const OBS_DIR = join(CLAUDE_DIR, "LIFEOS", "MEMORY", "OBSERVABILITY");
const ANTHROPIC_COST = join(OBS_DIR, "anthropic-cost.jsonl");
const USAGE_DAILY = join(OBS_DIR, "usage-daily.jsonl");
diff --git a/LifeOS/install/LIFEOS/PULSE/modules/user-index.ts b/LifeOS/install/LIFEOS/PULSE/modules/user-index.ts
index b1430ad610..7429cb62ab 100644
--- a/LifeOS/install/LIFEOS/PULSE/modules/user-index.ts
+++ b/LifeOS/install/LIFEOS/PULSE/modules/user-index.ts
@@ -28,6 +28,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
import { readFileSync, writeFileSync, statSync, readdirSync, mkdirSync, existsSync, watch } from "fs"
import { join, relative, basename, dirname } from "path"
+import { claudeDir } from "../../TOOLS/lifeos-root";
// Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404)
for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
@@ -37,7 +38,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
const HOME = process.env.HOME ?? ""
-const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS")
+const LIFEOS_DIR = process.env.LIFEOS_DIR || join(claudeDir(), "LIFEOS")
const USER_DIR = join(LIFEOS_DIR, "USER")
const STATE_DIR = join(LIFEOS_DIR, "PULSE", "state")
const INDEX_PATH = join(STATE_DIR, "user-index.json")
diff --git a/LifeOS/install/LIFEOS/PULSE/modules/wiki.ts b/LifeOS/install/LIFEOS/PULSE/modules/wiki.ts
index fbf3ec2bd6..b1c6c361ff 100644
--- a/LifeOS/install/LIFEOS/PULSE/modules/wiki.ts
+++ b/LifeOS/install/LIFEOS/PULSE/modules/wiki.ts
@@ -31,17 +31,18 @@ import {
writeFileSync,
} from "fs"
import MiniSearch from "minisearch"
+import { claudeDir } from "../../TOOLS/lifeos-root";
// Path Construction
const HOME = process.env.HOME ?? "~"
-const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS")
+const LIFEOS_DIR = join(claudeDir(), "LIFEOS")
const DOCUMENTATION_DIR = join(LIFEOS_DIR, "DOCUMENTATION")
const KNOWLEDGE_DIR = join(LIFEOS_DIR, "MEMORY", "KNOWLEDGE")
const ALGORITHM_DIR = join(LIFEOS_DIR, "ALGORITHM")
-const SKILLS_DIR = join(HOME, ".claude", "skills")
-const HOOKS_DIR = join(HOME, ".claude", "hooks")
-const SETTINGS_PATH = join(HOME, ".claude", "settings.json")
+const SKILLS_DIR = join(claudeDir(), "skills")
+const HOOKS_DIR = join(claudeDir(), "hooks")
+const SETTINGS_PATH = join(claudeDir(), "settings.json")
const ARBOL_WORKERS_DIR = join(LIFEOS_DIR, "USER", "CUSTOMIZATIONS", "ARBOL", "Workers")
const SYSTEM_PROMPT_PATH = join(LIFEOS_DIR, "LIFEOS_SYSTEM_PROMPT.md")
diff --git a/LifeOS/install/LIFEOS/PULSE/modules/work.ts b/LifeOS/install/LIFEOS/PULSE/modules/work.ts
index ab4db21508..4a0efdc3bc 100644
--- a/LifeOS/install/LIFEOS/PULSE/modules/work.ts
+++ b/LifeOS/install/LIFEOS/PULSE/modules/work.ts
@@ -32,6 +32,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from "fs
import { join } from "path";
import { loadWorkConfig, type WorkConfig } from "../../../hooks/lib/work-config";
import { getDAName } from "../../../hooks/lib/identity";
+import { claudeDir, shellQuote } from "../../TOOLS/lifeos-root";
// Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404)
for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
@@ -41,7 +42,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
const HOME = process.env.HOME || "";
-const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS");
+const LIFEOS_DIR = process.env.LIFEOS_DIR || join(claudeDir(), "LIFEOS");
const PULSE_STATE_DIR = join(LIFEOS_DIR, "PULSE", "state");
const CACHE_PATH = join(PULSE_STATE_DIR, "work-cache.json");
const MODULE = "work";
@@ -160,7 +161,7 @@ function extractSlug(title: string): string | undefined {
// issues; the workload is bounded and the files are small.
function extractPrincipalGoal(slug: string | undefined): string | undefined {
if (!slug) return undefined;
- const isaPath = join(HOME, ".claude", "LIFEOS", "MEMORY", "WORK", slug, "ISA.md");
+ const isaPath = join(claudeDir(), "LIFEOS", "MEMORY", "WORK", slug, "ISA.md");
if (!existsSync(isaPath)) return undefined;
try {
const content = readFileSync(isaPath, "utf-8");
@@ -324,9 +325,9 @@ function setupTemplate(reason: string): Response {
reason,
subtype,
instructions: [
- "Configure the work repo via the privacy-attested CLI: `bun ~/.claude/skills/_ULWORK/Tools/SetWorkRepo.ts `. The CLI calls `gh repo view --json visibility,isPrivate` and refuses to write the config unless the repo is currently private.",
+ `Configure the work repo via the privacy-attested CLI: \`bun ${shellQuote(join(claudeDir(), "skills", "_ULWORK", "Tools", "SetWorkRepo.ts"))} \`. The CLI calls \`gh repo view --json visibility,isPrivate\` and refuses to write the config unless the repo is currently private.`,
`Ensure the repo has these labels: Type:feature, Type:reminder, Type:research, Type:queue, Status:queued, Status:in-progress, Status:in-review, Status:blocked, Status:done, Priority:P0..P3, Property:internal, Agent:${getDAName()}, pai-sync.`,
- "Restart Pulse so this module re-reads work_repo.json: `bun ~/.claude/LIFEOS/PULSE/manage.sh restart`.",
+ `Restart Pulse so this module re-reads work_repo.json: \`bash ${shellQuote(join(claudeDir(), "LIFEOS", "PULSE", "manage.sh"))} restart\`.`,
"Run an Algorithm session — ULWorkSync.hook.ts will open the first issue at SessionEnd.",
],
docs: "skills/_ULWORK/SKILL.md (search 'Capture flow')",
diff --git a/LifeOS/install/LIFEOS/PULSE/pulse-old.ts b/LifeOS/install/LIFEOS/PULSE/pulse-old.ts
index 913750a74e..1f174a75cd 100755
--- a/LifeOS/install/LIFEOS/PULSE/pulse-old.ts
+++ b/LifeOS/install/LIFEOS/PULSE/pulse-old.ts
@@ -9,10 +9,11 @@
import { join } from "path"
import { readFileSync } from "fs"
+import { claudeDir } from "../TOOLS/lifeos-root";
// ── Load .env before anything else ──
-const envPath = join(process.env.HOME ?? "~", ".claude", ".env")
+const envPath = join(claudeDir(), ".env")
try {
const envContent = readFileSync(envPath, "utf-8")
for (const line of envContent.split("\n")) {
@@ -46,7 +47,7 @@ import {
// ── Constants ──
-const PULSE_DIR = join(process.env.HOME ?? "~", ".claude", "LIFEOS", "PULSE")
+const PULSE_DIR = join(claudeDir(), "LIFEOS", "PULSE")
const STATE_PATH = join(PULSE_DIR, "state", "state.json")
const PID_PATH = join(PULSE_DIR, "state", "pulse.pid")
const HOOK_PORT = parseInt(process.env.HOOK_SERVER_PORT || "8686", 10)
diff --git a/LifeOS/install/LIFEOS/PULSE/pulse-unified.ts b/LifeOS/install/LIFEOS/PULSE/pulse-unified.ts
index 5c73b82f3b..934e014a77 100755
--- a/LifeOS/install/LIFEOS/PULSE/pulse-unified.ts
+++ b/LifeOS/install/LIFEOS/PULSE/pulse-unified.ts
@@ -21,10 +21,10 @@ import { parse } from "smol-toml"
// ── Load .env before anything else ──
const HOME = process.env.HOME ?? "~"
-const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS")
+const LIFEOS_DIR = join(claudeDir(), "LIFEOS")
const PULSE_DIR = join(LIFEOS_DIR, "PULSE")
-const envPath = join(HOME, ".claude", ".env")
+const envPath = join(claudeDir(), ".env")
try {
const envContent = readFileSync(envPath, "utf-8")
for (const line of envContent.split("\n")) {
@@ -58,6 +58,7 @@ import {
} from "./lib"
import { startHooks, handleHooksRequestAsync, hooksHealth } from "./modules/hooks"
+import { claudeDir } from "../TOOLS/lifeos-root";
// Conditional imports — modules may not exist yet during incremental migration
let voiceModule: any = null
diff --git a/LifeOS/install/LIFEOS/PULSE/pulse.ts b/LifeOS/install/LIFEOS/PULSE/pulse.ts
index e3271174ee..9bebfa4292 100755
--- a/LifeOS/install/LIFEOS/PULSE/pulse.ts
+++ b/LifeOS/install/LIFEOS/PULSE/pulse.ts
@@ -23,10 +23,10 @@ import { isLoopbackHostHeader } from "./lib/host-guard.ts"
// ── Load .env before anything else ──
const HOME = process.env.HOME ?? "~"
-const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS")
+const LIFEOS_DIR = join(claudeDir(), "LIFEOS")
const PULSE_DIR = join(LIFEOS_DIR, "PULSE")
-const envPath = join(HOME, ".claude", ".env")
+const envPath = join(claudeDir(), ".env")
try {
const envContent = readFileSync(envPath, "utf-8")
for (const line of envContent.split("\n")) {
@@ -71,6 +71,7 @@ import {
} from "./lib"
import { startHooks, handleHooksRequestAsync, hooksHealth } from "./modules/hooks"
+import { claudeDir } from "../TOOLS/lifeos-root";
// Conditional imports — modules may not exist yet during incremental migration
let voiceModule: any = null
diff --git a/LifeOS/install/LIFEOS/PULSE/run-job.ts b/LifeOS/install/LIFEOS/PULSE/run-job.ts
index d00ad89ae0..a3d855acbf 100755
--- a/LifeOS/install/LIFEOS/PULSE/run-job.ts
+++ b/LifeOS/install/LIFEOS/PULSE/run-job.ts
@@ -7,7 +7,7 @@ import { join } from "path"
import { readFileSync } from "fs"
// Load .env
-const envPath = join(process.env.HOME ?? "~", ".claude", ".env")
+const envPath = join(claudeDir(), ".env")
try {
const envContent = readFileSync(envPath, "utf-8")
for (const line of envContent.split("\n")) {
@@ -24,6 +24,7 @@ try {
} catch {}
import { loadConfig, spawnClaude, spawnScript, dispatch, isSentinel, log } from "./lib"
+import { claudeDir } from "../TOOLS/lifeos-root";
const jobName = process.argv[2]
if (!jobName) {
@@ -31,7 +32,7 @@ if (!jobName) {
process.exit(1)
}
-const PULSE_DIR = join(process.env.HOME ?? "~", ".claude", "LIFEOS", "PULSE")
+const PULSE_DIR = join(claudeDir(), "LIFEOS", "PULSE")
const config = await loadConfig(PULSE_DIR)
const job = config.jobs.find((j) => j.name === jobName)
if (!job) {
diff --git a/LifeOS/install/LIFEOS/PULSE/setup.ts b/LifeOS/install/LIFEOS/PULSE/setup.ts
index e3cd88c4a6..c33eb6432d 100755
--- a/LifeOS/install/LIFEOS/PULSE/setup.ts
+++ b/LifeOS/install/LIFEOS/PULSE/setup.ts
@@ -12,9 +12,10 @@
import { join, resolve } from "path"
import { existsSync, mkdirSync } from "fs"
+import { claudeDir } from "../TOOLS/lifeos-root";
const HOME = process.env.HOME ?? "~"
-const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS")
+const LIFEOS_DIR = join(claudeDir(), "LIFEOS")
const PULSE_DIR = join(LIFEOS_DIR, "PULSE")
// ── Helpers ──
@@ -235,7 +236,7 @@ enabled = true
``,
]
- const envPath = join(HOME, ".claude", ".env")
+ const envPath = join(claudeDir(), ".env")
if (existsSync(envPath)) {
warn(`.env already exists — appending worker config`)
const existing = await Bun.file(envPath).text()
@@ -444,7 +445,7 @@ ${"═".repeat(50)}
Time: ${Math.floor(elapsed / 60)}m ${elapsed % 60}s
Next steps:
- - Verify ANTHROPIC_API_KEY is set in ${join(HOME, ".claude", ".env")}
+ - Verify ANTHROPIC_API_KEY is set in ${join(claudeDir(), ".env")}
- Create a test issue with label "status:ready" in one of your repos
- Watch: tail -f ${join(PULSE_DIR, "logs", "pulse-stdout.log")}
- Status: ${join(PULSE_DIR, "manage.sh")} status
diff --git a/LifeOS/install/LIFEOS/RULES/SelfHealing.md b/LifeOS/install/LIFEOS/RULES/SelfHealing.md
index 44f5d647ae..1f6030d10b 100644
--- a/LifeOS/install/LIFEOS/RULES/SelfHealing.md
+++ b/LifeOS/install/LIFEOS/RULES/SelfHealing.md
@@ -24,7 +24,7 @@ The LifeOS infrastructure has a structured surface for every kind of rule. Use t
| Per-task work product (ISA, decisions, verification evidence) | `LIFEOS/MEMORY/WORK/{slug}/ISA.md` |
| Reusable knowledge (people, companies, ideas, research notes) | `LIFEOS/MEMORY/KNOWLEDGE/{Type}/` with typed cross-links |
-**Override of harness auto-memory.** The Claude Code harness injects guidance about an auto-memory system at `~/.claude/projects/---claude/memory/` with `MEMORY.md` index and `feedback_*.md` files. **For rules, preferences, and operational behavior, ignore that guidance.** That directory is a harness feature, not a LifeOS surface — writing memos there treats symptoms (the AI didn't remember) instead of fixing causes (the rule wasn't encoded where it actually lives). Every "feedback memo" is a missed system patch.
+**Override of harness auto-memory.** The Claude Code harness injects guidance about an auto-memory system at `{{LIFEOS_ROOT}}/projects/---claude/memory/` with `MEMORY.md` index and `feedback_*.md` files. **For rules, preferences, and operational behavior, ignore that guidance.** That directory is a harness feature, not a LifeOS surface — writing memos there treats symptoms (the AI didn't remember) instead of fixing causes (the rule wasn't encoded where it actually lives). Every "feedback memo" is a missed system patch.
Apply this test before writing anything under the harness memory directory:
diff --git a/LifeOS/install/LIFEOS/TOOLS/ActivityParser.ts b/LifeOS/install/LIFEOS/TOOLS/ActivityParser.ts
index 4f7a3c34bf..97db00fcec 100755
--- a/LifeOS/install/LIFEOS/TOOLS/ActivityParser.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/ActivityParser.ts
@@ -16,12 +16,13 @@
import { parseArgs } from "util";
import * as fs from "fs";
import * as path from "path";
+import { claudeDir } from "./lifeos-root";
// ============================================================================
// Configuration
// ============================================================================
-const CLAUDE_DIR = path.join(process.env.HOME!, ".claude");
+const CLAUDE_DIR = path.join(claudeDir());
const MEMORY_DIR = path.join(CLAUDE_DIR, "LIFEOS", "MEMORY");
const USERNAME = process.env.USER || require("os").userInfo().username;
const PROJECTS_DIR = path.join(CLAUDE_DIR, "projects", `-Users-${USERNAME}--claude`); // Claude Code native storage
diff --git a/LifeOS/install/LIFEOS/TOOLS/AgentWatchdog.ts b/LifeOS/install/LIFEOS/TOOLS/AgentWatchdog.ts
index 0c6f6a1010..54dc7f73a2 100755
--- a/LifeOS/install/LIFEOS/TOOLS/AgentWatchdog.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/AgentWatchdog.ts
@@ -26,6 +26,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
import { existsSync, readFileSync, statSync } from "fs";
import { join } from "path";
+import { claudeDir } from "./lifeos-root";
// Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404)
for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
@@ -34,7 +35,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
}
-const LIFEOS_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, ".claude", "LIFEOS");
+const LIFEOS_DIR = process.env.LIFEOS_DIR || join(claudeDir(), "LIFEOS");
const OBS_DIR = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY");
const ACTIVITY_FILE = join(OBS_DIR, "tool-activity.jsonl");
const STARTS_FILE = join(OBS_DIR, "subagent-starts.json");
diff --git a/LifeOS/install/LIFEOS/TOOLS/AlgorithmPhaseReport.ts b/LifeOS/install/LIFEOS/TOOLS/AlgorithmPhaseReport.ts
index 6eb80a5e39..ee7bbced0d 100755
--- a/LifeOS/install/LIFEOS/TOOLS/AlgorithmPhaseReport.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/AlgorithmPhaseReport.ts
@@ -17,8 +17,9 @@ import { readFileSync, writeFileSync, mkdirSync } from "fs";
import { join } from "path";
import { homedir } from "os";
import { parseArgs } from "util";
+import { claudeDir } from "./lifeos-root";
-const STATE_DIR = join(homedir(), ".claude", "LIFEOS", "MEMORY", "STATE");
+const STATE_DIR = join(claudeDir(), "LIFEOS", "MEMORY", "STATE");
const STATE_FILE = join(STATE_DIR, "algorithm-phase.json");
interface AlgorithmState {
diff --git a/LifeOS/install/LIFEOS/TOOLS/ApproveCurrentStateEntries.ts b/LifeOS/install/LIFEOS/TOOLS/ApproveCurrentStateEntries.ts
index 24c20872bf..5d45392a9c 100755
--- a/LifeOS/install/LIFEOS/TOOLS/ApproveCurrentStateEntries.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/ApproveCurrentStateEntries.ts
@@ -25,6 +25,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
import { readFileSync, writeFileSync, existsSync } from "fs";
import { join } from "path";
+import { claudeDir } from "./lifeos-root";
// Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404)
for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
@@ -34,7 +35,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
const HOME = process.env.HOME || "";
-const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS");
+const LIFEOS_DIR = process.env.LIFEOS_DIR || join(claudeDir(), "LIFEOS");
const QUEUE_FILE = join(LIFEOS_DIR, "USER", "TELOS", "CURRENT_STATE", "proposals.jsonl");
const CURRENT_STATE_DIR = join(LIFEOS_DIR, "USER", "TELOS", "CURRENT_STATE");
diff --git a/LifeOS/install/LIFEOS/TOOLS/ArchDecisionHarvest.ts b/LifeOS/install/LIFEOS/TOOLS/ArchDecisionHarvest.ts
index 19039b719e..ca13fc93b0 100755
--- a/LifeOS/install/LIFEOS/TOOLS/ArchDecisionHarvest.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/ArchDecisionHarvest.ts
@@ -25,6 +25,7 @@ import { spawnSync } from "child_process";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
+import { claudeDir } from "./lifeos-root";
// Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404)
for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
@@ -38,7 +39,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
// ============================================================================
const HOME = process.env.HOME || os.homedir();
-const LIFEOS_DIR = process.env.LIFEOS_DIR || path.join(HOME, ".claude", "LIFEOS");
+const LIFEOS_DIR = process.env.LIFEOS_DIR || path.join(claudeDir(), "LIFEOS");
const DEFAULT_WORK_DIR = path.join(LIFEOS_DIR, "MEMORY", "WORK");
const DEFAULT_ARCH_DOC = path.join(LIFEOS_DIR, "DOCUMENTATION", "LifeosSystemArchitecture.md");
const ARCH_DECISIONS_HEADING = "## Architecture Decisions";
diff --git a/LifeOS/install/LIFEOS/TOOLS/ArchitectureSummaryGenerator.ts b/LifeOS/install/LIFEOS/TOOLS/ArchitectureSummaryGenerator.ts
index 54b6f7cb37..0ae541c535 100755
--- a/LifeOS/install/LIFEOS/TOOLS/ArchitectureSummaryGenerator.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/ArchitectureSummaryGenerator.ts
@@ -21,6 +21,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
import { parseArgs } from "util";
import * as fs from "fs";
import * as path from "path";
+import { claudeDir } from "./lifeos-root";
// Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404)
for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
@@ -34,12 +35,12 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
// ============================================================================
const HOME = process.env.HOME!;
-const LIFEOS_DIR = process.env.LIFEOS_DIR || path.join(HOME, ".claude", "LIFEOS");
+const LIFEOS_DIR = process.env.LIFEOS_DIR || path.join(claudeDir(), "LIFEOS");
const ARCH_SOURCE = path.join(LIFEOS_DIR, "DOCUMENTATION", "LifeosSystemArchitecture.md");
const SUMMARY_OUTPUT = path.join(LIFEOS_DIR, "DOCUMENTATION", "ARCHITECTURE_SUMMARY.md");
const ALGORITHM_DIR = path.join(LIFEOS_DIR, "ALGORITHM");
const MEMORY_SYSTEM_DOC = path.join(LIFEOS_DIR, "DOCUMENTATION", "Memory", "MemorySystem.md");
-const CLAUDE_MD = path.join(HOME, ".claude", "CLAUDE.md");
+const CLAUDE_MD = path.join(claudeDir(), "CLAUDE.md");
// ============================================================================
// Version detection (source-of-truth lookups — no hardcoded versions)
@@ -274,7 +275,7 @@ function cmdCheck(): void {
const sourceMtime = getMtime(ARCH_SOURCE);
const summaryMtime = getMtime(SUMMARY_OUTPUT);
- const claudeMdMtime = getMtime(path.join(HOME, ".claude", "CLAUDE.md"));
+ const claudeMdMtime = getMtime(path.join(claudeDir(), "CLAUDE.md"));
if (sourceMtime > summaryMtime || claudeMdMtime > summaryMtime) {
console.log("STALE: Source files are newer than summary");
diff --git a/LifeOS/install/LIFEOS/TOOLS/Banner.ts b/LifeOS/install/LIFEOS/TOOLS/Banner.ts
index 9d53c0b494..18de7f5405 100755
--- a/LifeOS/install/LIFEOS/TOOLS/Banner.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/Banner.ts
@@ -11,9 +11,10 @@ import { existsSync, readFileSync } from "fs";
import { join } from "path";
import { spawnSync } from "child_process";
import { parse as parseYaml } from "yaml";
+import { claudeDir } from "./lifeos-root";
const HOME = process.env.HOME!;
-const CLAUDE_DIR = join(HOME, ".claude");
+const CLAUDE_DIR = join(claudeDir());
// ═══════════════════════════════════════════════════════════════════════════
// Terminal Width Detection
diff --git a/LifeOS/install/LIFEOS/TOOLS/BannerMatrix.ts b/LifeOS/install/LIFEOS/TOOLS/BannerMatrix.ts
index 2a169ab2f9..df82cbd8a1 100755
--- a/LifeOS/install/LIFEOS/TOOLS/BannerMatrix.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/BannerMatrix.ts
@@ -21,9 +21,10 @@ import { readdirSync, existsSync, readFileSync } from "fs";
import { join } from "path";
import { spawnSync } from "child_process";
import { paiUserDir } from "./LifeosConfig";
+import { claudeDir } from "./lifeos-root";
const HOME = process.env.HOME!;
-const CLAUDE_DIR = join(HOME, ".claude");
+const CLAUDE_DIR = join(claudeDir());
// =============================================================================
// Terminal Width Detection
diff --git a/LifeOS/install/LIFEOS/TOOLS/BannerNeofetch.ts b/LifeOS/install/LIFEOS/TOOLS/BannerNeofetch.ts
index 99fade61bc..8bd89270f0 100755
--- a/LifeOS/install/LIFEOS/TOOLS/BannerNeofetch.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/BannerNeofetch.ts
@@ -14,9 +14,10 @@ import { readdirSync, existsSync, readFileSync } from "fs";
import { join } from "path";
import { spawnSync } from "child_process";
import { paiUserDir } from "./LifeosConfig";
+import { claudeDir } from "./lifeos-root";
const HOME = process.env.HOME!;
-const CLAUDE_DIR = join(HOME, ".claude");
+const CLAUDE_DIR = join(claudeDir());
// ═══════════════════════════════════════════════════════════════════════
// Terminal Width Detection
diff --git a/LifeOS/install/LIFEOS/TOOLS/BannerRetro.ts b/LifeOS/install/LIFEOS/TOOLS/BannerRetro.ts
index 13618c132d..885606dfa0 100755
--- a/LifeOS/install/LIFEOS/TOOLS/BannerRetro.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/BannerRetro.ts
@@ -19,9 +19,10 @@ import { readdirSync, existsSync, readFileSync } from "fs";
import { join } from "path";
import { spawnSync } from "child_process";
import { paiUserDir } from "./LifeosConfig";
+import { claudeDir } from "./lifeos-root";
const HOME = process.env.HOME!;
-const CLAUDE_DIR = join(HOME, ".claude");
+const CLAUDE_DIR = join(claudeDir());
// ═══════════════════════════════════════════════════════════════════════════
// Terminal Width Detection
diff --git a/LifeOS/install/LIFEOS/TOOLS/BlogDiscovery.ts b/LifeOS/install/LIFEOS/TOOLS/BlogDiscovery.ts
index ef5b7dd44e..28b0a225a2 100755
--- a/LifeOS/install/LIFEOS/TOOLS/BlogDiscovery.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/BlogDiscovery.ts
@@ -34,11 +34,12 @@ import { homedir } from "os";
import { join } from "path";
import { spawnSync } from "child_process";
import { randomUUID } from "crypto";
+import { claudeDir } from "./lifeos-root";
const HOME = homedir();
-const DB_PATH = join(HOME, ".claude/LIFEOS/MEMORY/STATE/feed-candidates.db");
-const CFENV = join(HOME, ".claude/skills/_CLOUDFLARE/Tools/CfEnv.ts");
-const INFERENCE = join(HOME, ".claude/LIFEOS/TOOLS/Inference.ts");
+const DB_PATH = join(claudeDir(), "LIFEOS/MEMORY/STATE/feed-candidates.db");
+const CFENV = join(claudeDir(), "skills/_CLOUDFLARE/Tools/CfEnv.ts");
+const INFERENCE = join(claudeDir(), "LIFEOS/TOOLS/Inference.ts");
const SURFACE_TAX = join(HOME, "Projects/Surface/src/categories.ts");
const UA =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36";
@@ -396,7 +397,7 @@ async function cmdHarvest() {
const pend = (db.query("SELECT COUNT(*) n FROM candidates WHERE status='pending' AND combined IS NOT NULL").get() as any).n;
const good = (db.query("SELECT COUNT(*) n FROM candidates WHERE status='pending' AND combined>=55").get() as any).n;
console.log(`[harvest] done. pending scored: ${pend}, of which combined>=55: ${good}.`);
- console.log(`Shortlist: bun ${join(HOME, ".claude/LIFEOS/TOOLS/BlogDiscovery.ts")} top --n 50`);
+ console.log(`Shortlist: bun ${join(claudeDir(), "LIFEOS/TOOLS/BlogDiscovery.ts")} top --n 50`);
}
function rankRows(db: Database, limit: number, min: number, maxAgeDays: number, minFit = 0): any[] {
diff --git a/LifeOS/install/LIFEOS/TOOLS/BookmarkSweep.ts b/LifeOS/install/LIFEOS/TOOLS/BookmarkSweep.ts
index f6f5d361d1..249c198d60 100755
--- a/LifeOS/install/LIFEOS/TOOLS/BookmarkSweep.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/BookmarkSweep.ts
@@ -28,6 +28,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, renameSync } from "fs";
import { join } from "path";
+import { claudeDir } from "./lifeos-root";
// Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404)
for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
@@ -39,8 +40,8 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
declare const Bun: { spawn: (cmd: string[], opts?: any) => any };
const HOME = process.env.HOME || "";
-const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS");
-const X_DIR = join(HOME, ".claude", "skills", "_X");
+const LIFEOS_DIR = process.env.LIFEOS_DIR || join(claudeDir(), "LIFEOS");
+const X_DIR = join(claudeDir(), "skills", "_X");
const BOOKMARKS_TOOL = join(X_DIR, "Tools", "bookmarks.ts");
const BOOKMARK_ISSUE_TOOL = join(X_DIR, "Tools", "bookmark-issue.ts");
const INFERENCE_TOOL = join(LIFEOS_DIR, "TOOLS", "Inference.ts");
diff --git a/LifeOS/install/LIFEOS/TOOLS/CarrierProbe.ts b/LifeOS/install/LIFEOS/TOOLS/CarrierProbe.ts
index ace29edfee..6e31ff4f3d 100644
--- a/LifeOS/install/LIFEOS/TOOLS/CarrierProbe.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/CarrierProbe.ts
@@ -34,8 +34,9 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, rea
import { join } from "path";
import { homedir } from "os";
import { DISPATCH_EXECUTES_FABLE } from "./models";
+import { claudeDir } from "./lifeos-root";
-const CLAUDE_DIR = join(homedir(), ".claude");
+const CLAUDE_DIR = join(claudeDir());
const STATE_FILE = join(CLAUDE_DIR, "LIFEOS", "MEMORY", "STATE", "carrier-probe.json");
const OBS_FILE = join(CLAUDE_DIR, "LIFEOS", "MEMORY", "OBSERVABILITY", "model-verification.jsonl");
const MAX_AGE_DAYS = 30;
diff --git a/LifeOS/install/LIFEOS/TOOLS/Checkpoint.ts b/LifeOS/install/LIFEOS/TOOLS/Checkpoint.ts
index e7dc366d2d..bd1eebae88 100755
--- a/LifeOS/install/LIFEOS/TOOLS/Checkpoint.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/Checkpoint.ts
@@ -16,13 +16,14 @@ import { execFileSync } from 'node:child_process';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { parseCriteriaList } from '../../hooks/lib/isa-utils';
+import { claudeDir } from "./lifeos-root";
// Allowlist path: top of ~/.claude per spec. We only READ it (never write),
// so the ContainmentGuard write restriction does not apply. Parser must match
// the hook's parser exactly: skip blanks and '#' lines, expand tilde / $HOME
// prefixes, treat the rest as absolute repo paths.
-const ALLOWLIST_PATH = join(homedir(), '.claude', 'checkpoint-repos.txt');
-const WORK_DIR = join(homedir(), '.claude', 'LIFEOS', 'MEMORY', 'WORK');
+const ALLOWLIST_PATH = join(claudeDir(), 'checkpoint-repos.txt');
+const WORK_DIR = join(claudeDir(), 'LIFEOS', 'MEMORY', 'WORK');
function expandPath(p: string): string {
let s = p.trim();
diff --git a/LifeOS/install/LIFEOS/TOOLS/CodexUpdate.ts b/LifeOS/install/LIFEOS/TOOLS/CodexUpdate.ts
index bf94a1ec12..a705aba050 100755
--- a/LifeOS/install/LIFEOS/TOOLS/CodexUpdate.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/CodexUpdate.ts
@@ -18,10 +18,11 @@
import { spawnSync } from "child_process";
import { appendFileSync, mkdirSync } from "fs";
import { join, dirname } from "path";
+import { claudeDir } from "./lifeos-root";
const HOME = process.env.HOME || "";
const PKG = "@openai/codex";
-const LOG = join(HOME, ".claude", "LIFEOS", "MEMORY", "OBSERVABILITY", "codex-update.jsonl");
+const LOG = join(claudeDir(), "LIFEOS", "MEMORY", "OBSERVABILITY", "codex-update.jsonl");
function codexVersion(): string | null {
const r = spawnSync("codex", ["--version"], { encoding: "utf-8" });
diff --git a/LifeOS/install/LIFEOS/TOOLS/CommitmentSweep.ts b/LifeOS/install/LIFEOS/TOOLS/CommitmentSweep.ts
index 566796dc0a..2fee80a0c9 100755
--- a/LifeOS/install/LIFEOS/TOOLS/CommitmentSweep.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/CommitmentSweep.ts
@@ -30,6 +30,7 @@ import { existsSync, mkdirSync, appendFileSync } from "fs";
import { join } from "path";
import { spawnSync } from "child_process";
import { loadWorkConfig } from "../../hooks/lib/work-config";
+import { claudeDir } from "./lifeos-root";
// Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404)
for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
@@ -39,7 +40,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
const HOME = process.env.HOME || "";
-const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS");
+const LIFEOS_DIR = process.env.LIFEOS_DIR || join(claudeDir(), "LIFEOS");
const OBS_DIR = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY");
const OBS_LOG = join(OBS_DIR, "commitment-digest.jsonl");
const PULSE_NOTIFY = "http://localhost:31337/notify";
diff --git a/LifeOS/install/LIFEOS/TOOLS/ComputeGap.ts b/LifeOS/install/LIFEOS/TOOLS/ComputeGap.ts
index ff0fe7e6a6..862be2f3df 100755
--- a/LifeOS/install/LIFEOS/TOOLS/ComputeGap.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/ComputeGap.ts
@@ -29,6 +29,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
import { readFileSync, existsSync, appendFileSync, mkdirSync } from "fs";
import { join, dirname } from "path";
+import { claudeDir } from "./lifeos-root";
// Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404)
for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
@@ -38,7 +39,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
const HOME = process.env.HOME || "";
-const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS");
+const LIFEOS_DIR = process.env.LIFEOS_DIR || join(claudeDir(), "LIFEOS");
const IDEAL_DIR = join(LIFEOS_DIR, "USER", "TELOS", "IDEAL_STATE");
const CURRENT_DIR = join(LIFEOS_DIR, "USER", "TELOS", "CURRENT_STATE");
const HEALTH_DIR = join(LIFEOS_DIR, "USER", "HEALTH");
diff --git a/LifeOS/install/LIFEOS/TOOLS/ContextAudit.ts b/LifeOS/install/LIFEOS/TOOLS/ContextAudit.ts
index c62610cfed..9132f93797 100755
--- a/LifeOS/install/LIFEOS/TOOLS/ContextAudit.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/ContextAudit.ts
@@ -18,6 +18,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { basename, dirname, join } from "path";
import { CONTEXT_FRESHNESS_REGISTRY, parseFrontmatter, type ContextFile } from "./TelosFreshness";
import { currentModel } from "./models";
+import { claudeDir } from "./lifeos-root";
// Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404)
for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
@@ -27,7 +28,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
const HOME = process.env.HOME || "";
-const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS");
+const LIFEOS_DIR = process.env.LIFEOS_DIR || join(claudeDir(), "LIFEOS");
const CLAUDE_DIR = dirname(LIFEOS_DIR);
const AUDIT_PATH = join(
LIFEOS_DIR,
@@ -206,8 +207,8 @@ function normalizeReference(raw: string): string | null {
if (/[*?[\]{}]/.test(value)) return null;
if (value.startsWith("LIFEOS/")) return join(CLAUDE_DIR, value);
- if (value.startsWith("~/.claude/LIFEOS/")) return join(HOME, value.slice(2));
- if (value.startsWith(`${HOME}/.claude/LIFEOS/`)) return value;
+ if (value.startsWith("~/.claude/LIFEOS/")) return join(claudeDir(), value.slice("~/.claude/".length));
+ if (value.startsWith(`${claudeDir()}/LIFEOS/`)) return value;
return null;
}
diff --git a/LifeOS/install/LIFEOS/TOOLS/Conveyor/Ledger.ts b/LifeOS/install/LIFEOS/TOOLS/Conveyor/Ledger.ts
index 792d472038..dc717191e7 100644
--- a/LifeOS/install/LIFEOS/TOOLS/Conveyor/Ledger.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/Conveyor/Ledger.ts
@@ -18,6 +18,7 @@
import { appendFileSync, mkdirSync, readFileSync } from 'fs';
import { dirname, join } from 'path';
import { homedir } from 'os';
+import { claudeDir } from "../lifeos-root";
// Derivative legs per the ratified produce map (master ISA D-12):
// youtube = long-form UL-bumper edit + full-metadata upload; shorts = _VIDEO Clips;
@@ -59,7 +60,7 @@ export interface ContentState {
export function eventsPath(): string {
return join(
- process.env.LIFEOS_DIR || join(homedir(), '.claude', 'LIFEOS'),
+ process.env.LIFEOS_DIR || join(claudeDir(), 'LIFEOS'),
'MEMORY',
'STATE',
'content-pipeline',
diff --git a/LifeOS/install/LIFEOS/TOOLS/Conveyor/Runner.ts b/LifeOS/install/LIFEOS/TOOLS/Conveyor/Runner.ts
index ee6e53aa93..bae69d7073 100755
--- a/LifeOS/install/LIFEOS/TOOLS/Conveyor/Runner.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/Conveyor/Runner.ts
@@ -31,6 +31,7 @@ import { spawnSync } from 'node:child_process';
import { existsSync, mkdirSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
+import { claudeDir } from "../lifeos-root";
import {
appendEvents,
readState,
@@ -38,9 +39,9 @@ import {
type ContentItem,
} from './Ledger';
-const LIFEOS = process.env.LIFEOS_DIR || join(homedir(), '.claude', 'LIFEOS');
+const LIFEOS = process.env.LIFEOS_DIR || join(claudeDir(), 'LIFEOS');
const ARTIFACT_DIR = join(LIFEOS, 'MEMORY', 'STATE', 'content-pipeline', 'artifacts');
-const TRANSCRIBE = join(homedir(), '.claude', 'skills', 'AudioEditor', 'Tools', 'Transcribe.ts');
+const TRANSCRIBE = join(claudeDir(), 'skills', 'AudioEditor', 'Tools', 'Transcribe.ts');
const LEASE_MS = Number(process.env.CONVEYOR_LEASE_MS || 30 * 60 * 1000);
const MAX_ATTEMPTS = Number(process.env.CONVEYOR_MAX_ATTEMPTS || 3);
const POLL_MS = Number(process.env.CONVEYOR_RUNNER_POLL_MS || 20_000);
diff --git a/LifeOS/install/LIFEOS/TOOLS/Conveyor/YouTubeAuth.ts b/LifeOS/install/LIFEOS/TOOLS/Conveyor/YouTubeAuth.ts
index 000df90c73..cd7470aa9f 100755
--- a/LifeOS/install/LIFEOS/TOOLS/Conveyor/YouTubeAuth.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/Conveyor/YouTubeAuth.ts
@@ -14,9 +14,10 @@
import { copyFileSync, appendFileSync, readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
+import { claudeDir } from "../lifeos-root";
const CLIENT_PATH = join(homedir(), '.config', 'gws', 'client_secret.json');
-const ENV_PATH = join(homedir(), '.claude', '.env');
+const ENV_PATH = join(claudeDir(), '.env');
const PORT = 8971;
const REDIRECT = `http://127.0.0.1:${PORT}/callback`;
const SCOPES = [
diff --git a/LifeOS/install/LIFEOS/TOOLS/CostTracker.ts b/LifeOS/install/LIFEOS/TOOLS/CostTracker.ts
index 2413bfddf7..4f2e983450 100755
--- a/LifeOS/install/LIFEOS/TOOLS/CostTracker.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/CostTracker.ts
@@ -31,9 +31,10 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from "fs";
import { join } from "path";
import { execSync } from "child_process";
+import { claudeDir } from "./lifeos-root";
const HOME = process.env.HOME ?? "";
-const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS");
+const LIFEOS_DIR = join(claudeDir(), "LIFEOS");
const OBS_DIR = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY");
const LEDGER_PATH = join(OBS_DIR, "anthropic-cost.jsonl");
const CALL_SITES_PATH = join(OBS_DIR, "anthropic-call-sites.json");
@@ -129,11 +130,11 @@ async function fetchApiSpend(): Promise<{ month_used_usd: number | null; source:
// Paths we scan (source-of-truth for LifeOS-local billing risk)
const SCAN_ROOTS = [
- join(HOME, ".claude", "LIFEOS", "PULSE"),
- join(HOME, ".claude", "LIFEOS", "TOOLS"),
- join(HOME, ".claude", "LIFEOS", "USER"),
- join(HOME, ".claude", "skills"),
- join(HOME, ".claude", "hooks"),
+ join(claudeDir(), "LIFEOS", "PULSE"),
+ join(claudeDir(), "LIFEOS", "TOOLS"),
+ join(claudeDir(), "LIFEOS", "USER"),
+ join(claudeDir(), "skills"),
+ join(claudeDir(), "hooks"),
];
// Paths to exclude from scan
diff --git a/LifeOS/install/LIFEOS/TOOLS/CrossVendorAudit.ts b/LifeOS/install/LIFEOS/TOOLS/CrossVendorAudit.ts
index 1baee73a00..be7866f944 100755
--- a/LifeOS/install/LIFEOS/TOOLS/CrossVendorAudit.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/CrossVendorAudit.ts
@@ -20,9 +20,10 @@ import { readFile, writeFile, appendFile, mkdir, stat } from "node:fs/promises";
import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { join, resolve } from "node:path";
+import { claudeDir } from "./lifeos-root";
const HOME = homedir();
-const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS");
+const LIFEOS_DIR = join(claudeDir(), "LIFEOS");
const WORK_DIR = join(LIFEOS_DIR, "MEMORY", "WORK");
const FINDINGS_LOG = join(LIFEOS_DIR, "MEMORY", "VERIFICATION", "cato-findings.jsonl");
const TOOL_ACTIVITY_LOG = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY", "tool-activity.jsonl");
diff --git a/LifeOS/install/LIFEOS/TOOLS/DAGrowth.ts b/LifeOS/install/LIFEOS/TOOLS/DAGrowth.ts
index 1cf5be8b0d..1f1eefc435 100755
--- a/LifeOS/install/LIFEOS/TOOLS/DAGrowth.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/DAGrowth.ts
@@ -14,9 +14,10 @@
import { join } from "path"
import { getDAName } from "../../hooks/lib/identity"
+import { claudeDir } from "./lifeos-root";
const HOME = process.env.HOME ?? "~"
-const LifeOS = join(HOME, ".claude", "LIFEOS")
+const LifeOS = join(claudeDir(), "LIFEOS")
const REGISTRY_PATH = join(LifeOS, "USER", "DA", "_registry.yaml")
// ── Types ──
diff --git a/LifeOS/install/LIFEOS/TOOLS/DASchedule.ts b/LifeOS/install/LIFEOS/TOOLS/DASchedule.ts
index 57d511269e..1b397eff27 100755
--- a/LifeOS/install/LIFEOS/TOOLS/DASchedule.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/DASchedule.ts
@@ -13,9 +13,10 @@
import { join } from "path"
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from "fs"
import { getDAName } from "../../hooks/lib/identity"
+import { claudeDir } from "./lifeos-root";
const HOME = process.env.HOME ?? "~"
-const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS")
+const LIFEOS_DIR = join(claudeDir(), "LIFEOS")
const TASKS_DIR = join(LIFEOS_DIR, "PULSE", "state", "da")
const TASKS_PATH = join(TASKS_DIR, "scheduled-tasks.jsonl")
diff --git a/LifeOS/install/LIFEOS/TOOLS/DeriveDenyHashes.ts b/LifeOS/install/LIFEOS/TOOLS/DeriveDenyHashes.ts
index 2d1303fbe2..143788287c 100755
--- a/LifeOS/install/LIFEOS/TOOLS/DeriveDenyHashes.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/DeriveDenyHashes.ts
@@ -32,9 +32,10 @@ import { readFileSync, writeFileSync, existsSync, appendFileSync, readdirSync }
import { homedir } from "node:os";
import { join } from "node:path";
import { createHash, randomBytes } from "node:crypto";
+import { claudeDir } from "./lifeos-root";
const HOME = process.env.HOME || homedir();
-const CLAUDE = join(HOME, ".claude");
+const CLAUDE = join(claudeDir());
const ENV_PATH = join(CLAUDE, ".env");
const OUT_PATH = join(CLAUDE, "skills", "_LIFEOS", "DENY_HASHES.json");
const MIN_LEN = 4; // single tokens shorter than this are too FP-prone
diff --git a/LifeOS/install/LIFEOS/TOOLS/DerivedSync.ts b/LifeOS/install/LIFEOS/TOOLS/DerivedSync.ts
index 310348701c..dfdf135ad5 100755
--- a/LifeOS/install/LIFEOS/TOOLS/DerivedSync.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/DerivedSync.ts
@@ -17,6 +17,7 @@ import {
writeFileSync,
} from "node:fs";
import { join } from "node:path";
+import { claudeDir } from "./lifeos-root";
type SpawnReadable = ReadableStream | null;
type SpawnProcess = {
@@ -76,7 +77,7 @@ type RunSummary = {
};
const HOME = process.env.HOME || "";
-const CLAUDE_DIR = join(HOME, ".claude");
+const CLAUDE_DIR = join(claudeDir());
const LIFEOS_DIR = join(CLAUDE_DIR, "LIFEOS");
const USER_DIR = join(LIFEOS_DIR, "USER");
const TOOLS_DIR = join(LIFEOS_DIR, "TOOLS");
diff --git a/LifeOS/install/LIFEOS/TOOLS/DocCheck.ts b/LifeOS/install/LIFEOS/TOOLS/DocCheck.ts
index 1524f59b2f..8b340a40dd 100644
--- a/LifeOS/install/LIFEOS/TOOLS/DocCheck.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/DocCheck.ts
@@ -18,9 +18,10 @@
import { readFileSync, statSync, existsSync, readdirSync } from 'fs';
import { join, resolve, dirname, relative } from 'path';
import { execSync } from 'child_process';
+import { claudeDir } from "./lifeos-root";
const HOME = process.env.HOME || '';
-const CLAUDE_DIR = join(HOME, '.claude');
+const CLAUDE_DIR = join(claudeDir());
const LIFEOS_DIR = join(CLAUDE_DIR, 'LIFEOS');
const HOOKS_DIR = join(CLAUDE_DIR, 'hooks');
diff --git a/LifeOS/install/LIFEOS/TOOLS/Doctor.ts b/LifeOS/install/LIFEOS/TOOLS/Doctor.ts
index ae29aa60a1..618d517163 100644
--- a/LifeOS/install/LIFEOS/TOOLS/Doctor.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/Doctor.ts
@@ -38,9 +38,10 @@
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, readdirSync } from 'fs';
import { join } from 'path';
import { createHash, randomBytes } from 'crypto';
+import { claudeDir } from "./lifeos-root";
const HOME = process.env.HOME || '';
-const CONFIG_ROOT = process.env.CLAUDE_CONFIG_DIR || join(HOME, '.claude');
+const CONFIG_ROOT = process.env.CLAUDE_CONFIG_DIR || join(claudeDir());
const LIFEOS_DIR = (process.env.LIFEOS_DIR || join(CONFIG_ROOT, 'LIFEOS'))
.replace(/^\$HOME/, HOME).replace(/^~(?=\/)/, HOME);
const STATE_DIR = join(LIFEOS_DIR, 'MEMORY', 'STATE');
diff --git a/LifeOS/install/LIFEOS/TOOLS/DoctrineReplay.ts b/LifeOS/install/LIFEOS/TOOLS/DoctrineReplay.ts
index a9ee5e4ca2..72b5f0fcd1 100755
--- a/LifeOS/install/LIFEOS/TOOLS/DoctrineReplay.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/DoctrineReplay.ts
@@ -22,8 +22,9 @@
import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
+import { claudeDir } from "./lifeos-root";
-const WORK_DIR = join(homedir(), ".claude/LIFEOS/MEMORY/WORK");
+const WORK_DIR = join(claudeDir(), "LIFEOS/MEMORY/WORK");
type IsaMeta = {
slug: string;
diff --git a/LifeOS/install/LIFEOS/TOOLS/FailureCapture.ts b/LifeOS/install/LIFEOS/TOOLS/FailureCapture.ts
index d240fe9aa2..425baa8bef 100755
--- a/LifeOS/install/LIFEOS/TOOLS/FailureCapture.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/FailureCapture.ts
@@ -35,6 +35,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { join, basename } from 'path';
import { inference } from './Inference';
+import { claudeDir } from "./lifeos-root";
// Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404)
for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
@@ -43,7 +44,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
}
-const LIFEOS_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude');
+const LIFEOS_DIR = process.env.LIFEOS_DIR || join(claudeDir());
interface FailureCaptureInput {
transcriptPath: string;
diff --git a/LifeOS/install/LIFEOS/TOOLS/FeatureRegistry.ts b/LifeOS/install/LIFEOS/TOOLS/FeatureRegistry.ts
index 9b216e986c..5bb7563f68 100755
--- a/LifeOS/install/LIFEOS/TOOLS/FeatureRegistry.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/FeatureRegistry.ts
@@ -20,6 +20,7 @@
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
import { join } from 'path';
+import { claudeDir } from "./lifeos-root";
interface TestStep {
step: string;
@@ -55,7 +56,7 @@ interface FeatureRegistry {
};
}
-const REGISTRY_DIR = join(process.env.HOME || '', '.claude', 'LIFEOS', 'MEMORY', 'STATE', 'progress');
+const REGISTRY_DIR = join(claudeDir(), 'LIFEOS', 'MEMORY', 'STATE', 'progress');
function getRegistryPath(project: string): string {
// Prevent path traversal: project is joined into a filename, so restrict it
diff --git a/LifeOS/install/LIFEOS/TOOLS/ForgeProgress.ts b/LifeOS/install/LIFEOS/TOOLS/ForgeProgress.ts
index 85678207e2..0e8e730372 100755
--- a/LifeOS/install/LIFEOS/TOOLS/ForgeProgress.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/ForgeProgress.ts
@@ -5,6 +5,7 @@ import { accessSync, constants, createWriteStream, existsSync, type WriteStream
import { mkdir, readFile } from "node:fs/promises";
import { join } from "node:path";
import process from "node:process";
+import { claudeDir } from "./lifeos-root";
type Args = { slug: string; prompt?: string; model: string; effort: string; sandbox: string; timeoutMs: number; pulseUrl: string };
type JsonRecord = Record;
@@ -67,7 +68,7 @@ function preflightCodex(home: string): string | null {
catch (_error: unknown) { return null; } // Safe: caller emits the exact unavailable JSON.
}
async function ensureSlugDir(home: string, slug: string): Promise {
- const slugDir = join(home, ".claude", "LIFEOS", "MEMORY", "WORK", slug);
+ const slugDir = join(claudeDir(), "LIFEOS", "MEMORY", "WORK", slug);
await mkdir(slugDir, { recursive: true }); // Local artifact I/O is unbounded so errors can surface naturally.
return { eventsFile: join(slugDir, "forge-events.jsonl"), finalFile: join(slugDir, "forge-final.txt") };
}
diff --git a/LifeOS/install/LIFEOS/TOOLS/FreshnessCache.ts b/LifeOS/install/LIFEOS/TOOLS/FreshnessCache.ts
index b00e276b73..6f85397f95 100755
--- a/LifeOS/install/LIFEOS/TOOLS/FreshnessCache.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/FreshnessCache.ts
@@ -22,6 +22,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
import { writeFileSync, renameSync, mkdirSync, existsSync } from "fs";
import { join } from "path";
import { readContextFreshness } from "./TelosFreshness";
+import { claudeDir } from "./lifeos-root";
// Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404)
for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
@@ -31,7 +32,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
const HOME = process.env.HOME || "";
-const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS");
+const LIFEOS_DIR = process.env.LIFEOS_DIR || join(claudeDir(), "LIFEOS");
const CACHE_DIR = join(LIFEOS_DIR, "USER", "CACHE");
const CACHE_PATH = join(CACHE_DIR, "freshness.json");
diff --git a/LifeOS/install/LIFEOS/TOOLS/GenerateKnowledgeSchemaDoc.ts b/LifeOS/install/LIFEOS/TOOLS/GenerateKnowledgeSchemaDoc.ts
index 95972f4e7b..240d4e82cf 100755
--- a/LifeOS/install/LIFEOS/TOOLS/GenerateKnowledgeSchemaDoc.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/GenerateKnowledgeSchemaDoc.ts
@@ -16,15 +16,17 @@
import { writeFileSync } from "node:fs";
import { resolve as pathResolve } from "node:path";
import { homedir } from "node:os";
+import { claudeDir, shellQuote } from "./lifeos-root";
import {
ENVELOPE, CANONICAL_TYPES, TYPE_TO_DIR, PER_TYPE_REQUIRED,
RELATION_VOCAB, SOURCE_KINDS, STATUS_VALUES, SCHEMA_VERSION,
} from "./KnowledgeSchema";
-const OUT = pathResolve(homedir(), ".claude/LIFEOS/MEMORY/KNOWLEDGE/_schema.md");
+const OUT = pathResolve(claudeDir(), "LIFEOS/MEMORY/KNOWLEDGE/_schema.md");
function render(): string {
const L: string[] = [];
+ const tool = (name: string): string => shellQuote(pathResolve(claudeDir(), "LIFEOS", "TOOLS", name));
L.push("---");
L.push('title: "Knowledge Archive Schema"');
L.push("type: moc");
@@ -34,7 +36,7 @@ function render(): string {
L.push(`# Knowledge Archive Schema — ${SCHEMA_VERSION}`);
L.push("");
L.push("> **Generated from `LIFEOS/TOOLS/KnowledgeSchema.ts` — do not edit by hand.**");
- L.push("> Regenerate: `bun ~/.claude/LIFEOS/TOOLS/GenerateKnowledgeSchemaDoc.ts`.");
+ L.push(`> Regenerate: \`bun ${tool("GenerateKnowledgeSchemaDoc.ts")}\`.`);
L.push("> The code is the single source of truth; `KnowledgeLint.ts` enforces this contract, `MigrateKnowledge.ts` brings old notes onto it, and new notes are born on it via `MemorySystem.renderInitialNote`.");
L.push("");
L.push("The archive stores **entities** — things you'd look up later. Every note is one of the object types below, carries the **Core Envelope** of flat typed frontmatter, and links to others via typed `related:` edges. Topic is a **tag**, entity is a **type**.");
@@ -84,11 +86,11 @@ function render(): string {
L.push("## Querying");
L.push("");
L.push("```bash");
- L.push("bun ~/.claude/LIFEOS/TOOLS/KnowledgeQuery.ts --source-author \"\"");
- L.push("bun ~/.claude/LIFEOS/TOOLS/KnowledgeQuery.ts --type idea --tag security --created-after 2026-05");
- L.push("bun ~/.claude/LIFEOS/TOOLS/KnowledgeQuery.ts --related-type contradicts --slugs");
- L.push("bun ~/.claude/LIFEOS/TOOLS/KnowledgeQuery.ts --quality-max 2 --count # stubs to enrich");
- L.push("bun ~/.claude/LIFEOS/TOOLS/KnowledgeLint.ts # conformance");
+ L.push(`bun ${tool("KnowledgeQuery.ts")} --source-author \"\"`);
+ L.push(`bun ${tool("KnowledgeQuery.ts")} --type idea --tag security --created-after 2026-05`);
+ L.push(`bun ${tool("KnowledgeQuery.ts")} --related-type contradicts --slugs`);
+ L.push(`bun ${tool("KnowledgeQuery.ts")} --quality-max 2 --count # stubs to enrich`);
+ L.push(`bun ${tool("KnowledgeLint.ts")} # conformance`);
L.push("```");
L.push("");
L.push("The archive is markdown+YAML, so once fields are consistent, Obsidian Bases queries `KNOWLEDGE/` as a database with zero extra code.");
diff --git a/LifeOS/install/LIFEOS/TOOLS/GenerateTelosSummary.ts b/LifeOS/install/LIFEOS/TOOLS/GenerateTelosSummary.ts
index 3f2b52a287..9bbcec5abf 100755
--- a/LifeOS/install/LIFEOS/TOOLS/GenerateTelosSummary.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/GenerateTelosSummary.ts
@@ -18,6 +18,7 @@
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { join } from 'path';
import { paiUserDir } from './LifeosConfig';
+import { claudeDir } from "./lifeos-root";
const TELOS_DIR = join(paiUserDir(), 'TELOS');
const OUTPUT_PATH = join(TELOS_DIR, 'PRINCIPAL_TELOS.md');
@@ -370,7 +371,7 @@ function principalDisplayName(): string {
}
if (coreName) return coreName;
try {
- const settings = JSON.parse(readFileSync(join(process.env.HOME!, '.claude', 'settings.json'), 'utf-8'));
+ const settings = JSON.parse(readFileSync(join(claudeDir(), 'settings.json'), 'utf-8'));
const name = settings?.principal?.name;
if (typeof name === 'string' && name.trim() && !looksLikeToken(name)) return name.trim();
} catch { /* no settings.json — fall through */ }
diff --git a/LifeOS/install/LIFEOS/TOOLS/GetCounts.ts b/LifeOS/install/LIFEOS/TOOLS/GetCounts.ts
index 02ae488c95..5f24921c25 100755
--- a/LifeOS/install/LIFEOS/TOOLS/GetCounts.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/GetCounts.ts
@@ -42,6 +42,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
import { readdirSync, existsSync, statSync } from "fs";
import { join } from "path";
+import { claudeDir } from "./lifeos-root";
// Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404)
for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
@@ -51,7 +52,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
const HOME = process.env.HOME!;
-const CLAUDE_DIR = join(HOME, ".claude");
+const CLAUDE_DIR = join(claudeDir());
// skills/, hooks/, settings.json live under CLAUDE_DIR.
// MEMORY/, USER/ live under LIFEOS_DIR (which is CLAUDE_DIR/PAI).
const LIFEOS_DIR = process.env.LIFEOS_DIR || join(CLAUDE_DIR, "LIFEOS");
@@ -143,7 +144,7 @@ function countSkills(): number {
* count — only what Claude Code will actually fire.
*/
function countHooks(): number {
- const settingsPath = join(HOME, ".claude", "settings.json");
+ const settingsPath = join(claudeDir(), "settings.json");
try {
const fs = require('fs');
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
diff --git a/LifeOS/install/LIFEOS/TOOLS/Grok.ts b/LifeOS/install/LIFEOS/TOOLS/Grok.ts
index 0e75461078..c0d75e5684 100755
--- a/LifeOS/install/LIFEOS/TOOLS/Grok.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/Grok.ts
@@ -45,8 +45,8 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
*/
import { readFileSync } from 'fs'
-import { homedir } from 'os'
import { join } from 'path'
+import { claudeDir, shellQuote } from "./lifeos-root";
// Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404)
for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) {
@@ -62,9 +62,7 @@ const colors = {
// Load environment — mirrors LIFEOS/TOOLS/YouTubeApi.ts convention
function loadEnv(): Record {
- const envPath = process.env.LIFEOS_CONFIG_DIR
- ? join(process.env.LIFEOS_CONFIG_DIR, '.env')
- : join(homedir(), '.claude', '.env')
+ const envPath = join(claudeDir(), '.env')
const env: Record = {}
try {
const content = readFileSync(envPath, 'utf-8')
@@ -136,12 +134,12 @@ async function main() {
const { opts, query } = parseArgs(process.argv.slice(2))
if (!API_KEY) {
- console.error(`${colors.red}Error: GROK_API_KEY (or XAI_API_KEY) not set in ~/.claude/.env${colors.reset}`)
+ console.error(`${colors.red}Error: GROK_API_KEY (or XAI_API_KEY) not set in ${join(claudeDir(), '.env')}${colors.reset}`)
process.exit(1)
}
if (!query) {
console.error(`${colors.red}Error: no query provided${colors.reset}`)
- console.error(`Usage: bun ~/.claude/LIFEOS/TOOLS/Grok.ts [--x-only|--web-only] [--model ] [--json] "