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] ""`) + console.error(`Usage: bun ${shellQuote(join(claudeDir(), 'LIFEOS', 'TOOLS', 'Grok.ts'))} [--x-only|--web-only] [--model ] [--json] ""`) process.exit(1) } diff --git a/LifeOS/install/LIFEOS/TOOLS/GrokAudit.ts b/LifeOS/install/LIFEOS/TOOLS/GrokAudit.ts index 73ff62f61e..f49469ec35 100755 --- a/LifeOS/install/LIFEOS/TOOLS/GrokAudit.ts +++ b/LifeOS/install/LIFEOS/TOOLS/GrokAudit.ts @@ -36,9 +36,10 @@ import { readFile, appendFile, mkdir, stat } from "node:fs/promises"; import { existsSync, readFileSync } 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", "grok-findings.jsonl"); const API_URL = "https://api.x.ai/v1/chat/completions"; @@ -107,9 +108,7 @@ interface AuditResponse { } function loadEnv(): Record { - const envPath = process.env.LIFEOS_CONFIG_DIR - ? join(process.env.LIFEOS_CONFIG_DIR, ".env") - : join(HOME, ".claude", ".env"); + const envPath = join(claudeDir(), ".env"); const env: Record = {}; try { for (const line of readFileSync(envPath, "utf-8").split("\n")) { diff --git a/LifeOS/install/LIFEOS/TOOLS/HarvestExecutor.ts b/LifeOS/install/LIFEOS/TOOLS/HarvestExecutor.ts index ec663cc199..c17c26d0ed 100755 --- a/LifeOS/install/LIFEOS/TOOLS/HarvestExecutor.ts +++ b/LifeOS/install/LIFEOS/TOOLS/HarvestExecutor.ts @@ -12,9 +12,10 @@ import { parseArgs } from "node:util"; import * as fs from "fs"; import * as path from "path"; import { inference } from "./Inference"; +import { claudeDir } from "./lifeos-root"; const HOME = process.env.HOME!; -const LIFEOS_DIR = path.join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = path.join(claudeDir(), "LIFEOS"); const MEMORY_DIR = path.join(LIFEOS_DIR, "MEMORY"); const KNOWLEDGE_DIR = path.join(MEMORY_DIR, "KNOWLEDGE"); const LEARNING_DIR = path.join(MEMORY_DIR, "LEARNING"); diff --git a/LifeOS/install/LIFEOS/TOOLS/HealthSnapshot.ts b/LifeOS/install/LIFEOS/TOOLS/HealthSnapshot.ts index e3c2b79fac..86b517b1de 100644 --- a/LifeOS/install/LIFEOS/TOOLS/HealthSnapshot.ts +++ b/LifeOS/install/LIFEOS/TOOLS/HealthSnapshot.ts @@ -4,6 +4,7 @@ import { existsSync } from "node:fs" import { homedir } from "node:os" import { join } from "node:path" import { loadLifeosConfig } from "./LifeosConfig" +import { claudeDir } from "./lifeos-root"; const INBOX = join(homedir(), "Library/Mobile Documents/com~apple~CloudDocs/PAI/health/inbox") const PROCESSED = join(homedir(), "Library/Mobile Documents/com~apple~CloudDocs/PAI/health/processed") @@ -11,7 +12,7 @@ const PROCESSED = join(homedir(), "Library/Mobile Documents/com~apple~CloudDocs/ // a relocated LIFEOS_USER_DIR Just Works. const SNAPSHOTS = ((): string => { try { return join(loadLifeosConfig().paths.userDir, "TELOS/HEALTH/snapshots") } - catch { return join(homedir(), ".claude/LIFEOS/USER/TELOS/HEALTH/snapshots") } + catch { return join(claudeDir(), "LIFEOS/USER/TELOS/HEALTH/snapshots") } })() type HealthSnapshot = { diff --git a/LifeOS/install/LIFEOS/TOOLS/HealthSync.ts b/LifeOS/install/LIFEOS/TOOLS/HealthSync.ts index bfe00876c3..699afad44e 100755 --- a/LifeOS/install/LIFEOS/TOOLS/HealthSync.ts +++ b/LifeOS/install/LIFEOS/TOOLS/HealthSync.ts @@ -10,6 +10,7 @@ * bun LIFEOS/TOOLS/HealthSync.ts auth oura */ import { join } from "node:path"; +import { claudeDir } from "./lifeos-root"; import type { Ctx, CurrentJson, @@ -42,10 +43,8 @@ type CliCommand = "pull" | "status" | "current" | "auth"; const HOME = process.env.HOME || ""; const PREFIX = "[HealthSync]"; const SOURCE_NAMES: readonly SourceName[] = ["oura", "eightsleep", "apple", "function"]; -const CURRENT_PATH = join(HOME, ".claude", "LIFEOS", "USER", "HEALTH", "current.json"); -const HEALTHSYNC_LOG_PATH = join( - HOME, - ".claude", +const CURRENT_PATH = join(claudeDir(), "LIFEOS", "USER", "HEALTH", "current.json"); +const HEALTHSYNC_LOG_PATH = join(claudeDir(), "LIFEOS", "MEMORY", "OBSERVABILITY", diff --git a/LifeOS/install/LIFEOS/TOOLS/ISARender.ts b/LifeOS/install/LIFEOS/TOOLS/ISARender.ts index aa6520e5cf..28ed374d2a 100644 --- a/LifeOS/install/LIFEOS/TOOLS/ISARender.ts +++ b/LifeOS/install/LIFEOS/TOOLS/ISARender.ts @@ -19,17 +19,18 @@ import { readFileSync, writeFileSync, existsSync, statSync, renameSync, readdirS import { resolve, dirname, basename, join } from "node:path"; import { spawn } from "node:child_process"; import { homedir } from "node:os"; +import { claudeDir } from "./lifeos-root"; const HOME = process.env.HOME || homedir(); -const TOOLS_DIR = resolve(HOME, ".claude/LIFEOS/TOOLS"); +const TOOLS_DIR = resolve(claudeDir(), "LIFEOS/TOOLS"); const TEMPLATE_HTML = join(TOOLS_DIR, "ISARender/template.html"); const TEMPLATE_CSS = join(TOOLS_DIR, "ISARender/template.css"); // Brand logo: user override via LIFEOS_BRAND_LOGO_PATH env var (absolute path), // else system default under PAI/ASSETS/, else inert (empty src). const BRAND_LOGO_PATH_OVERRIDE = process.env.LIFEOS_BRAND_LOGO_PATH ?? ""; -const BRAND_LOGO_PATH_DEFAULT = resolve(HOME, ".claude/LIFEOS/ASSETS/pai-logo.png"); -const WORK_DIR = resolve(HOME, ".claude/LIFEOS/MEMORY/WORK"); -const WORK_JSON = resolve(HOME, ".claude/LIFEOS/MEMORY/STATE/work.json"); +const BRAND_LOGO_PATH_DEFAULT = resolve(claudeDir(), "LIFEOS/ASSETS/pai-logo.png"); +const WORK_DIR = resolve(claudeDir(), "LIFEOS/MEMORY/WORK"); +const WORK_JSON = resolve(claudeDir(), "LIFEOS/MEMORY/STATE/work.json"); const PHASES = ["observe", "think", "plan", "build", "execute", "verify", "learn", "complete"]; diff --git a/LifeOS/install/LIFEOS/TOOLS/Inference.ts b/LifeOS/install/LIFEOS/TOOLS/Inference.ts index 5fc9bc37e4..7d900d200e 100755 --- a/LifeOS/install/LIFEOS/TOOLS/Inference.ts +++ b/LifeOS/install/LIFEOS/TOOLS/Inference.ts @@ -113,6 +113,7 @@ export interface InferenceResult { } import { modelForEffort, pinnedModelForEffort, EFFORT_MODEL, LEVEL_TO_HARNESS_EFFORT, type EffortLevel, type HarnessEffort } from './models'; +import { claudeDir } from "./lifeos-root"; // Level configurations — models resolve via models.ts EFFORT_MODEL (the single // edit point on a lineup change). No model names appear here. `effort` is the @@ -177,7 +178,7 @@ export function verifyExecutedModel(modelUsage: unknown, expectedTier: string): * exact drift this catches and makes auditable. Logging must never break inference. */ function logModelVerification(entry: Record): void { try { - const dir = join(process.env.HOME || '', '.claude', 'LIFEOS', 'MEMORY', 'OBSERVABILITY'); + const dir = join(claudeDir(), 'LIFEOS', 'MEMORY', 'OBSERVABILITY'); mkdirSync(dir, { recursive: true }); appendFileSync(join(dir, 'model-verification.jsonl'), JSON.stringify({ ts: new Date().toISOString(), ...entry }) + '\n'); } catch { /* observability must never break inference */ } diff --git a/LifeOS/install/LIFEOS/TOOLS/InstallBlogDiscovery.ts b/LifeOS/install/LIFEOS/TOOLS/InstallBlogDiscovery.ts index bc0732cf99..7c593104de 100755 --- a/LifeOS/install/LIFEOS/TOOLS/InstallBlogDiscovery.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InstallBlogDiscovery.ts @@ -13,11 +13,12 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs"; import { join } from "path"; +import { claudeDir } from "./lifeos-root"; declare const Bun: { spawn: (cmd: string[], opts?: any) => any }; const HOME = process.env.HOME || ""; -const TEMPLATE_PATH = join(HOME, ".claude", "LIFEOS", "TOOLS", "com.lifeos.blogdiscovery.plist.template"); +const TEMPLATE_PATH = join(claudeDir(), "LIFEOS", "TOOLS", "com.lifeos.blogdiscovery.plist.template"); const LAUNCH_AGENTS_DIR = join(HOME, "Library", "LaunchAgents"); const TARGET_PLIST = join(LAUNCH_AGENTS_DIR, "com.lifeos.blogdiscovery.plist"); const LABEL = "com.lifeos.blogdiscovery"; diff --git a/LifeOS/install/LIFEOS/TOOLS/InstallBookmarkSweep.ts b/LifeOS/install/LIFEOS/TOOLS/InstallBookmarkSweep.ts index d6dd3f20e3..82486dd2e3 100644 --- a/LifeOS/install/LIFEOS/TOOLS/InstallBookmarkSweep.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InstallBookmarkSweep.ts @@ -15,11 +15,12 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs"; import { join } from "path"; +import { claudeDir } from "./lifeos-root"; declare const Bun: { spawn: (cmd: string[], opts?: any) => any }; const HOME = process.env.HOME || ""; -const TEMPLATE_PATH = join(HOME, ".claude", "LIFEOS", "TOOLS", "com.lifeos.bookmarksweep.plist.template"); +const TEMPLATE_PATH = join(claudeDir(), "LIFEOS", "TOOLS", "com.lifeos.bookmarksweep.plist.template"); const LAUNCH_AGENTS_DIR = join(HOME, "Library", "LaunchAgents"); const TARGET_PLIST = join(LAUNCH_AGENTS_DIR, "com.lifeos.bookmarksweep.plist"); const LABEL = "com.lifeos.bookmarksweep"; diff --git a/LifeOS/install/LIFEOS/TOOLS/InstallCodexUpdate.ts b/LifeOS/install/LIFEOS/TOOLS/InstallCodexUpdate.ts index b5d9e4d6e2..0e908cbcaa 100755 --- a/LifeOS/install/LIFEOS/TOOLS/InstallCodexUpdate.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InstallCodexUpdate.ts @@ -14,11 +14,12 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs"; import { join } from "path"; +import { claudeDir } from "./lifeos-root"; declare const Bun: { spawn: (cmd: string[], opts?: any) => any }; const HOME = process.env.HOME || ""; -const TEMPLATE_PATH = join(HOME, ".claude", "LIFEOS", "TOOLS", "com.lifeos.codexupdate.plist.template"); +const TEMPLATE_PATH = join(claudeDir(), "LIFEOS", "TOOLS", "com.lifeos.codexupdate.plist.template"); const LAUNCH_AGENTS_DIR = join(HOME, "Library", "LaunchAgents"); const TARGET_PLIST = join(LAUNCH_AGENTS_DIR, "com.lifeos.codexupdate.plist"); const LABEL = "com.lifeos.codexupdate"; diff --git a/LifeOS/install/LIFEOS/TOOLS/InstallCommitmentSweep.ts b/LifeOS/install/LIFEOS/TOOLS/InstallCommitmentSweep.ts index 66a55f3b6c..233115fe33 100755 --- a/LifeOS/install/LIFEOS/TOOLS/InstallCommitmentSweep.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InstallCommitmentSweep.ts @@ -12,12 +12,13 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from "fs"; import { join } from "path"; import { spawnSync } from "child_process"; +import { claudeDir } from "./lifeos-root"; const HOME = process.env.HOME || ""; -const TEMPLATE = join(HOME, ".claude", "LIFEOS", "TOOLS", "com.lifeos.commitmentsweep.plist.template"); +const TEMPLATE = join(claudeDir(), "LIFEOS", "TOOLS", "com.lifeos.commitmentsweep.plist.template"); const TARGET_DIR = join(HOME, "Library", "LaunchAgents"); const TARGET = join(TARGET_DIR, "com.lifeos.commitmentsweep.plist"); -const STATE_DIR = join(HOME, ".claude", "LIFEOS", "MEMORY", "STATE"); +const STATE_DIR = join(claudeDir(), "LIFEOS", "MEMORY", "STATE"); const LABEL = "com.lifeos.commitmentsweep"; function uid(): string { diff --git a/LifeOS/install/LIFEOS/TOOLS/InstallConveyorRunner.ts b/LifeOS/install/LIFEOS/TOOLS/InstallConveyorRunner.ts index edaa71b82b..4823e6f129 100755 --- a/LifeOS/install/LIFEOS/TOOLS/InstallConveyorRunner.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InstallConveyorRunner.ts @@ -16,11 +16,12 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs"; import { join } from "path"; +import { claudeDir } from "./lifeos-root"; declare const Bun: { spawn: (cmd: string[], opts?: any) => any }; const HOME = process.env.HOME || ""; -const TEMPLATE_PATH = join(HOME, ".claude", "LIFEOS", "TOOLS", "com.lifeos.conveyor-runner.plist.template"); +const TEMPLATE_PATH = join(claudeDir(), "LIFEOS", "TOOLS", "com.lifeos.conveyor-runner.plist.template"); const LAUNCH_AGENTS_DIR = join(HOME, "Library", "LaunchAgents"); const TARGET_PLIST = join(LAUNCH_AGENTS_DIR, "com.lifeos.conveyor-runner.plist"); const LABEL = "com.lifeos.conveyor-runner"; diff --git a/LifeOS/install/LIFEOS/TOOLS/InstallConveyorWatcher.ts b/LifeOS/install/LIFEOS/TOOLS/InstallConveyorWatcher.ts index 7b8f89a325..0d5b94cf72 100644 --- a/LifeOS/install/LIFEOS/TOOLS/InstallConveyorWatcher.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InstallConveyorWatcher.ts @@ -15,11 +15,12 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs"; import { join } from "path"; +import { claudeDir } from "./lifeos-root"; declare const Bun: { spawn: (cmd: string[], opts?: any) => any }; const HOME = process.env.HOME || ""; -const TEMPLATE_PATH = join(HOME, ".claude", "LIFEOS", "TOOLS", "com.lifeos.conveyor-watcher.plist.template"); +const TEMPLATE_PATH = join(claudeDir(), "LIFEOS", "TOOLS", "com.lifeos.conveyor-watcher.plist.template"); const LAUNCH_AGENTS_DIR = join(HOME, "Library", "LaunchAgents"); const TARGET_PLIST = join(LAUNCH_AGENTS_DIR, "com.lifeos.conveyor-watcher.plist"); const LABEL = "com.lifeos.conveyor-watcher"; diff --git a/LifeOS/install/LIFEOS/TOOLS/InstallDerivedSync.ts b/LifeOS/install/LIFEOS/TOOLS/InstallDerivedSync.ts index fca3b87ea0..fd82ab9723 100755 --- a/LifeOS/install/LIFEOS/TOOLS/InstallDerivedSync.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InstallDerivedSync.ts @@ -9,6 +9,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, realpathSync } from "fs"; import { join } from "path"; +import { claudeDir } from "./lifeos-root"; type SpawnProcess = { stdout: ReadableStream | null; @@ -34,7 +35,7 @@ type LaunchctlResult = { }; const HOME = process.env.HOME || ""; -const TEMPLATE_PATH = join(HOME, ".claude", "LIFEOS", "TOOLS", "com.lifeos.derivedsync.plist.template"); +const TEMPLATE_PATH = join(claudeDir(), "LIFEOS", "TOOLS", "com.lifeos.derivedsync.plist.template"); const LAUNCH_AGENTS_DIR = join(HOME, "Library", "LaunchAgents"); const TARGET_PLIST = join(LAUNCH_AGENTS_DIR, "com.lifeos.derivedsync.plist"); const LABEL = "com.lifeos.derivedsync"; @@ -87,7 +88,7 @@ async function install(): Promise { } const bunPath = await detectBun(); const bunDir = bunPath.replace(/\/bun$/, ""); - const userDir = realpathSync(join(HOME, ".claude", "LIFEOS", "USER")); + const userDir = realpathSync(join(claudeDir(), "LIFEOS", "USER")); console.log(`[InstallDerivedSync] detected bun at ${bunPath}`); const template = readFileSync(TEMPLATE_PATH, "utf-8"); const materialized = template diff --git a/LifeOS/install/LIFEOS/TOOLS/InstallHealthSync.ts b/LifeOS/install/LIFEOS/TOOLS/InstallHealthSync.ts index 4a8b70dd9f..e606872f9f 100755 --- a/LifeOS/install/LIFEOS/TOOLS/InstallHealthSync.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InstallHealthSync.ts @@ -9,6 +9,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs"; import { join } from "path"; +import { claudeDir } from "./lifeos-root"; type SpawnProcess = { exited: Promise; @@ -30,7 +31,7 @@ type LaunchctlResult = { }; const HOME = process.env.HOME || ""; -const TEMPLATE_PATH = join(HOME, ".claude", "LIFEOS", "TOOLS", "com.lifeos.healthsync.plist.template"); +const TEMPLATE_PATH = join(claudeDir(), "LIFEOS", "TOOLS", "com.lifeos.healthsync.plist.template"); const LAUNCH_AGENTS_DIR = join(HOME, "Library", "LaunchAgents"); const TARGET_PLIST = join(LAUNCH_AGENTS_DIR, "com.lifeos.healthsync.plist"); const LABEL = "com.lifeos.healthsync"; diff --git a/LifeOS/install/LIFEOS/TOOLS/InstallUsageAggregator.ts b/LifeOS/install/LIFEOS/TOOLS/InstallUsageAggregator.ts index 37c8d740a4..ea6b4be286 100755 --- a/LifeOS/install/LIFEOS/TOOLS/InstallUsageAggregator.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InstallUsageAggregator.ts @@ -13,11 +13,12 @@ */ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs"; import { join } from "path"; +import { claudeDir } from "./lifeos-root"; declare const Bun: { spawn: (cmd: string[], opts?: any) => any }; const HOME = process.env.HOME || ""; -const TEMPLATE_PATH = join(HOME, ".claude", "LIFEOS", "TOOLS", "com.lifeos.usage-aggregator.plist.template"); +const TEMPLATE_PATH = join(claudeDir(), "LIFEOS", "TOOLS", "com.lifeos.usage-aggregator.plist.template"); const LAUNCH_AGENTS_DIR = join(HOME, "Library", "LaunchAgents"); const TARGET_PLIST = join(LAUNCH_AGENTS_DIR, "com.lifeos.usage-aggregator.plist"); const LABEL = "com.lifeos.usage-aggregator"; diff --git a/LifeOS/install/LIFEOS/TOOLS/InstallWorkSweep.ts b/LifeOS/install/LIFEOS/TOOLS/InstallWorkSweep.ts index aff4ff2154..7b09e5aef5 100644 --- a/LifeOS/install/LIFEOS/TOOLS/InstallWorkSweep.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InstallWorkSweep.ts @@ -15,11 +15,12 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs"; import { join } from "path"; +import { claudeDir } from "./lifeos-root"; declare const Bun: { spawn: (cmd: string[], opts?: any) => any }; const HOME = process.env.HOME || ""; -const TEMPLATE_PATH = join(HOME, ".claude", "LIFEOS", "TOOLS", "com.lifeos.worksweep.plist.template"); +const TEMPLATE_PATH = join(claudeDir(), "LIFEOS", "TOOLS", "com.lifeos.worksweep.plist.template"); const LAUNCH_AGENTS_DIR = join(HOME, "Library", "LaunchAgents"); const TARGET_PLIST = join(LAUNCH_AGENTS_DIR, "com.lifeos.worksweep.plist"); const LABEL = "com.lifeos.worksweep"; diff --git a/LifeOS/install/LIFEOS/TOOLS/IntegrityCheck.ts b/LifeOS/install/LIFEOS/TOOLS/IntegrityCheck.ts index da9409e15e..4f16159d01 100755 --- a/LifeOS/install/LIFEOS/TOOLS/IntegrityCheck.ts +++ b/LifeOS/install/LIFEOS/TOOLS/IntegrityCheck.ts @@ -33,9 +33,10 @@ import { readFileSync, existsSync, readdirSync, statSync } from 'fs'; import { join } from 'path'; import { execFileSync } 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 TOOLS_DIR = join(LIFEOS_DIR, 'TOOLS'); const DOC_DIR = join(LIFEOS_DIR, 'DOCUMENTATION'); diff --git a/LifeOS/install/LIFEOS/TOOLS/IntegrityMaintenance.ts b/LifeOS/install/LIFEOS/TOOLS/IntegrityMaintenance.ts index da3999fe55..1dfeeb2856 100755 --- a/LifeOS/install/LIFEOS/TOOLS/IntegrityMaintenance.ts +++ b/LifeOS/install/LIFEOS/TOOLS/IntegrityMaintenance.ts @@ -22,7 +22,8 @@ import { spawn } from 'child_process'; import { readFileSync, existsSync } from 'fs'; import { join, basename, dirname } from 'path'; import { inference } from './Inference'; -import { getIdentity } from '../../../.claude/hooks/lib/identity'; +import { getIdentity } from '../../hooks/lib/identity'; +import { claudeDir, shellQuote } from "./lifeos-root"; // ============================================================================ // Types @@ -108,7 +109,7 @@ interface UpdateData { // Constants // ============================================================================ -const LIFEOS_DIR = process.env.HOME + '/.claude/LIFEOS'; +const LIFEOS_DIR = claudeDir() + "/LIFEOS"; const CREATE_UPDATE_SCRIPT = join(LIFEOS_DIR, 'skills/_LIFEOS/Tools/CreateUpdate.ts'); // Words that indicate generic/bad titles - reject these @@ -719,7 +720,7 @@ async function generateVerboseNarrative( future_impact: aiNarrative.future_impact, future_bullets: aiNarrative.future_bullets, verification_steps: aiNarrative.verification_steps, - verification_commands: [`bun ~/.claude/skills/_LIFEOS/Tools/UpdateSearch.ts recent 5`], + verification_commands: [`bun ${shellQuote(join(claudeDir(), "skills", "_LIFEOS", "Tools", "UpdateSearch.ts"))} recent 5`], confidence: 'high', }, aiTitle: aiNarrative.title, @@ -749,7 +750,7 @@ async function generateVerboseNarrative( future_impact: `The ${changeType.replace('_', ' ')} will use updated behavior.`, future_bullets: ['Changes are active for future sessions'], verification_steps: ['Changes applied via automatic detection'], - verification_commands: [`bun ~/.claude/skills/_LIFEOS/Tools/UpdateSearch.ts recent 5`], + verification_commands: [`bun ${shellQuote(join(claudeDir(), "skills", "_LIFEOS", "Tools", "UpdateSearch.ts"))} recent 5`], confidence: 'medium', }, }; diff --git a/LifeOS/install/LIFEOS/TOOLS/InterviewIdealState.ts b/LifeOS/install/LIFEOS/TOOLS/InterviewIdealState.ts index f7d0c67c15..78c7ba2705 100755 --- a/LifeOS/install/LIFEOS/TOOLS/InterviewIdealState.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InterviewIdealState.ts @@ -24,6 +24,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { import { readFileSync, writeFileSync, existsSync, readdirSync } 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"]) { @@ -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 TELOS_DIR = join(LIFEOS_DIR, "USER", "TELOS"); const IDEAL_DIR = join(TELOS_DIR, "IDEAL_STATE"); const STATE_FILE = join(LIFEOS_DIR, "USER", "TELOS", "CURRENT_STATE", "interview-state.json"); diff --git a/LifeOS/install/LIFEOS/TOOLS/InterviewScan.ts b/LifeOS/install/LIFEOS/TOOLS/InterviewScan.ts index 86546bf4c3..5784101e6b 100755 --- a/LifeOS/install/LIFEOS/TOOLS/InterviewScan.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InterviewScan.ts @@ -26,6 +26,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { import { readFileSync, existsSync } from "fs"; import { join } from "path"; import { readTelosFreshness, sectionSlug, type SectionFreshness } 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"]) { @@ -35,7 +36,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 TELOS_DIR = join(USER_DIR, "TELOS"); const TELOS_PATH = join(TELOS_DIR, "TELOS.md"); @@ -98,7 +99,7 @@ const REGISTRY: RegistryTarget[] = [ prompts: ["Main DA voice — pick from ElevenLabs library, or stick with default Rachel (21m00Tcm4TlvDq8ikWAM)?", "Algorithm voice (used for phase transitions) — default Adam (pNInz6obpgDQGcFmaJgB) is fine?", "Want voice notifications on by default? (default: yes)"] }, - { phase: 0, path: join(HOME, ".claude", ".env"), name: ".env/credentials", category: "setup", leverage: 10, + { phase: 0, path: join(claudeDir(), ".env"), name: ".env/credentials", category: "setup", leverage: 10, prompts: ["ANTHROPIC_API_KEY — required for inference. Paste here (will write to .env, won't echo back)?", "ELEVENLABS_API_KEY — required for voice notifications. Skip if you don't want voice.", "GH_TOKEN — optional, only if you want the work pipeline. Skip if not using GitHub issues.", diff --git a/LifeOS/install/LIFEOS/TOOLS/KnowledgeGraph.ts b/LifeOS/install/LIFEOS/TOOLS/KnowledgeGraph.ts index 3ac61c0675..eb9fc10bd3 100755 --- a/LifeOS/install/LIFEOS/TOOLS/KnowledgeGraph.ts +++ b/LifeOS/install/LIFEOS/TOOLS/KnowledgeGraph.ts @@ -33,6 +33,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"]) { @@ -46,7 +47,7 @@ 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 KNOWLEDGE_DIR = path.join(LIFEOS_DIR, "MEMORY", "KNOWLEDGE"); const DOMAINS = ["People", "Companies", "Ideas", "Research"]; const SKIP_FILES = new Set(["_index.md", "_schema.md", "_log.md"]); diff --git a/LifeOS/install/LIFEOS/TOOLS/KnowledgeHarvester.ts b/LifeOS/install/LIFEOS/TOOLS/KnowledgeHarvester.ts index 138cc12a09..6a5b7418f3 100755 --- a/LifeOS/install/LIFEOS/TOOLS/KnowledgeHarvester.ts +++ b/LifeOS/install/LIFEOS/TOOLS/KnowledgeHarvester.ts @@ -36,6 +36,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"]) { @@ -49,7 +50,7 @@ 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 MEMORY_DIR = path.join(LIFEOS_DIR, "MEMORY"); const KNOWLEDGE_DIR = path.join(MEMORY_DIR, "KNOWLEDGE"); const WORK_DIR = path.join(MEMORY_DIR, "WORK"); @@ -58,7 +59,7 @@ const RESEARCH_DIR = path.join(MEMORY_DIR, "RESEARCH"); const HARVEST_QUEUE_DIR = path.join(KNOWLEDGE_DIR, "_harvest-queue"); const ARCHIVE_DIR = path.join(KNOWLEDGE_DIR, "_archive"); -const PROJECTS_DIR = path.join(HOME, ".claude", "projects"); +const PROJECTS_DIR = path.join(claudeDir(), "projects"); /** * Auto-memory dirs — multi-instance aware (#1170). diff --git a/LifeOS/install/LIFEOS/TOOLS/KnowledgeLint.ts b/LifeOS/install/LIFEOS/TOOLS/KnowledgeLint.ts index 983338f373..4fbbfb0b9b 100755 --- a/LifeOS/install/LIFEOS/TOOLS/KnowledgeLint.ts +++ b/LifeOS/install/LIFEOS/TOOLS/KnowledgeLint.ts @@ -20,8 +20,9 @@ import { readdirSync, readFileSync, existsSync } from "node:fs"; import { resolve as pathResolve, join as pathJoin } from "node:path"; import { homedir } from "node:os"; import { parseNote, validate, DIR_TO_TYPE, ALL_DIRS, slugFromPath, SCHEMA_VERSION, type CanonicalType } from "./KnowledgeSchema"; +import { claudeDir } from "./lifeos-root"; -const KNOWLEDGE_DIR = pathResolve(homedir(), ".claude/LIFEOS/MEMORY/KNOWLEDGE"); +const KNOWLEDGE_DIR = pathResolve(claudeDir(), "LIFEOS/MEMORY/KNOWLEDGE"); const DIRS = ALL_DIRS; function listNotes(dir: string): string[] { diff --git a/LifeOS/install/LIFEOS/TOOLS/KnowledgeQuery.ts b/LifeOS/install/LIFEOS/TOOLS/KnowledgeQuery.ts index 75c1d3da86..e4e3a6c6df 100755 --- a/LifeOS/install/LIFEOS/TOOLS/KnowledgeQuery.ts +++ b/LifeOS/install/LIFEOS/TOOLS/KnowledgeQuery.ts @@ -41,8 +41,9 @@ import { readdirSync, readFileSync, existsSync } from "node:fs"; import { resolve as pathResolve, join as pathJoin } from "node:path"; import { homedir } from "node:os"; import { parseNote, slugFromPath, ALL_DIRS, type ParsedNote } from "./KnowledgeSchema"; +import { claudeDir } from "./lifeos-root"; -const KNOWLEDGE_DIR = pathResolve(homedir(), ".claude/LIFEOS/MEMORY/KNOWLEDGE"); +const KNOWLEDGE_DIR = pathResolve(claudeDir(), "LIFEOS/MEMORY/KNOWLEDGE"); const DIRS = ALL_DIRS; interface Rec { diff --git a/LifeOS/install/LIFEOS/TOOLS/LearningPatternSynthesis.ts b/LifeOS/install/LIFEOS/TOOLS/LearningPatternSynthesis.ts index 9aa5b24e7e..5c4145f8ba 100755 --- a/LifeOS/install/LIFEOS/TOOLS/LearningPatternSynthesis.ts +++ b/LifeOS/install/LIFEOS/TOOLS/LearningPatternSynthesis.ts @@ -26,12 +26,13 @@ import { parseArgs } from "util"; import * as fs from "fs"; import * as path from "path"; import * as crypto from "crypto"; +import { claudeDir } from "./lifeos-root"; // ============================================================================ // Configuration // ============================================================================ -const CLAUDE_DIR = path.join(process.env.HOME!, ".claude"); +const CLAUDE_DIR = path.join(claudeDir()); const LIFEOS_DIR = path.join(CLAUDE_DIR, "LIFEOS"); const MEMORY_DIR = path.join(LIFEOS_DIR, "MEMORY"); const LEARNING_DIR = path.join(MEMORY_DIR, "LEARNING"); diff --git a/LifeOS/install/LIFEOS/TOOLS/LifeosConfig.ts b/LifeOS/install/LIFEOS/TOOLS/LifeosConfig.ts index ca5d03d9b9..5864e8b7f9 100644 --- a/LifeOS/install/LIFEOS/TOOLS/LifeosConfig.ts +++ b/LifeOS/install/LIFEOS/TOOLS/LifeosConfig.ts @@ -20,6 +20,7 @@ import { existsSync, statSync } from "node:fs"; import { resolve } from "node:path"; import { homedir } from "node:os"; +import { claudeDir } from "./lifeos-root"; // Expand leading `~` (and `~/`) to the user's home directory. node:fs APIs do // not expand tildes, so any path returned from this loader must be absolute. @@ -85,7 +86,7 @@ export interface LifeosConfig { // ─────────── Resolution ─────────── const DEFAULT_HOME = process.env.HOME || homedir(); -const DEFAULT_CONFIG_PATH = resolve(DEFAULT_HOME, ".claude/LIFEOS/USER/CONFIG/LIFEOS_CONFIG.toml"); +const DEFAULT_CONFIG_PATH = resolve(claudeDir(), "LIFEOS/USER/CONFIG/LIFEOS_CONFIG.toml"); let cache: { config: LifeosConfig; mtime: number; path: string } | null = null; @@ -133,7 +134,7 @@ export function paiUserDir(): string { try { return loadLifeosConfig().paths.userDir; } catch { - return resolve(DEFAULT_HOME, ".claude/LIFEOS/USER"); + return resolve(claudeDir(), "LIFEOS/USER"); } } @@ -188,10 +189,10 @@ function validateAndNormalize(raw: unknown, path: string): LifeosConfig { }, paths: { userDir: expandHome( - root.paths?.userDir ?? root.paths?.user_dir ?? resolve(DEFAULT_HOME, ".claude/LIFEOS/USER"), + root.paths?.userDir ?? root.paths?.user_dir ?? resolve(claudeDir(), "LIFEOS/USER"), ), memoryDir: expandHome( - root.paths?.memoryDir ?? root.paths?.memory_dir ?? resolve(DEFAULT_HOME, ".claude/LIFEOS/MEMORY"), + root.paths?.memoryDir ?? root.paths?.memory_dir ?? resolve(claudeDir(), "LIFEOS/MEMORY"), ), projectsDir: expandHome( root.paths?.projectsDir ?? root.paths?.projects_dir ?? resolve(DEFAULT_HOME, "Projects"), diff --git a/LifeOS/install/LIFEOS/TOOLS/LifeosUpgrade.ts b/LifeOS/install/LIFEOS/TOOLS/LifeosUpgrade.ts index 5007662fa3..e2883846e4 100755 --- a/LifeOS/install/LIFEOS/TOOLS/LifeosUpgrade.ts +++ b/LifeOS/install/LIFEOS/TOOLS/LifeosUpgrade.ts @@ -20,9 +20,10 @@ import { existsSync, readFileSync, lstatSync, readlinkSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; +import { claudeDir } from "./lifeos-root"; const HOME = process.env.HOME ?? homedir(); -const CLAUDE_ROOT = join(HOME, ".claude"); +const CLAUDE_ROOT = join(claudeDir()); interface MigrationContext { claudeRoot: string; diff --git a/LifeOS/install/LIFEOS/TOOLS/LoadSkillConfig.ts b/LifeOS/install/LIFEOS/TOOLS/LoadSkillConfig.ts index 40d6fe0af7..46f30276c7 100755 --- a/LifeOS/install/LIFEOS/TOOLS/LoadSkillConfig.ts +++ b/LifeOS/install/LIFEOS/TOOLS/LoadSkillConfig.ts @@ -18,6 +18,7 @@ import { readFileSync, existsSync, readdirSync } from 'fs'; import { join, basename } from 'path'; import { homedir } from 'os'; import { parse as parseYaml } from 'yaml'; +import { claudeDir } from "./lifeos-root"; // Types interface CustomizationMetadata { @@ -35,7 +36,7 @@ interface ExtendManifest { // Constants const HOME = homedir(); -const CUSTOMIZATION_DIR = join(HOME, '.claude', 'LIFEOS', 'USER', 'SKILLCUSTOMIZATIONS'); +const CUSTOMIZATION_DIR = join(claudeDir(), 'LIFEOS', 'USER', 'SKILLCUSTOMIZATIONS'); /** * Deep merge two objects recursively diff --git a/LifeOS/install/LIFEOS/TOOLS/MemoryGraph.ts b/LifeOS/install/LIFEOS/TOOLS/MemoryGraph.ts index f720c69c28..0aafe4f6e5 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MemoryGraph.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemoryGraph.ts @@ -34,6 +34,7 @@ import louvain from "graphology-communities-louvain"; import pagerank from "graphology-metrics/centrality/pagerank"; 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"]) { @@ -43,7 +44,7 @@ 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 MEMORY = path.join(LIFEOS_DIR, "MEMORY"); const KNOWLEDGE_DIR = path.join(MEMORY, "KNOWLEDGE"); const WORK_DIR = path.join(MEMORY, "WORK"); diff --git a/LifeOS/install/LIFEOS/TOOLS/MemoryHealthCheck.ts b/LifeOS/install/LIFEOS/TOOLS/MemoryHealthCheck.ts index 9dfe303b51..b6c5f361b5 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MemoryHealthCheck.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemoryHealthCheck.ts @@ -23,9 +23,10 @@ import { existsSync, readFileSync, appendFileSync, mkdirSync, readdirSync, statSync } from "node:fs"; import { join } from "node:path"; +import { claudeDir } from "./lifeos-root"; const HOME = process.env.HOME || ""; -const CLAUDE = join(HOME, ".claude"); +const CLAUDE = join(claudeDir()); const HOOKS_DIR = join(CLAUDE, "hooks"); const TOOLS_DIR = join(CLAUDE, "LIFEOS/TOOLS"); const OBS_DIR = join(CLAUDE, "LIFEOS/MEMORY/OBSERVABILITY"); diff --git a/LifeOS/install/LIFEOS/TOOLS/MemoryInsights.ts b/LifeOS/install/LIFEOS/TOOLS/MemoryInsights.ts index 185075bdcd..3598317918 100644 --- a/LifeOS/install/LIFEOS/TOOLS/MemoryInsights.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemoryInsights.ts @@ -23,8 +23,9 @@ import { existsSync, readFileSync, statSync } from "node:fs"; import { resolve as pathResolve } from "node:path"; import { homedir } from "node:os"; import { getDAName } from "../../hooks/lib/identity" +import { claudeDir } from "./lifeos-root"; -const ROOT = pathResolve(homedir(), ".claude"); +const ROOT = pathResolve(claudeDir()); const OBS = pathResolve(ROOT, "LIFEOS/MEMORY/OBSERVABILITY"); const PRINCIPAL_MEM = pathResolve(ROOT, "LIFEOS/USER/PRINCIPAL/PRINCIPAL_MEMORY.md"); const DA_MEM = pathResolve(ROOT, "LIFEOS/USER/DIGITAL_ASSISTANT/DA_MEMORY.md"); diff --git a/LifeOS/install/LIFEOS/TOOLS/MemoryRestore.ts b/LifeOS/install/LIFEOS/TOOLS/MemoryRestore.ts index 8d7cbdbc94..c7fd823efd 100644 --- a/LifeOS/install/LIFEOS/TOOLS/MemoryRestore.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemoryRestore.ts @@ -15,8 +15,9 @@ import { readdirSync, readFileSync, writeFileSync, existsSync } from "node:fs"; import { resolve as pathResolve } from "node:path"; import { homedir } from "node:os"; +import { claudeDir } from "./lifeos-root"; -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = pathResolve(claudeDir()); const SNAPSHOT_DIR = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/memory-snapshots"); const TARGETS: Record = { principal: pathResolve(CLAUDE_ROOT, "LIFEOS/USER/PRINCIPAL/PRINCIPAL_MEMORY.md"), diff --git a/LifeOS/install/LIFEOS/TOOLS/MemoryRetriever.ts b/LifeOS/install/LIFEOS/TOOLS/MemoryRetriever.ts index 3922d54ab7..b0ffb93fe5 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MemoryRetriever.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemoryRetriever.ts @@ -42,6 +42,7 @@ import { parseArgs } from "util"; import * as fs from "fs"; import * as path from "path"; import { spawnSync } from "child_process"; +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"]) { @@ -54,7 +55,7 @@ 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 KNOWLEDGE_DIR = path.join(LIFEOS_DIR, "MEMORY", "KNOWLEDGE"); const DOMAINS = ["People", "Companies", "Ideas", "Research"]; @@ -457,8 +458,8 @@ function formatResults( // dual-tier prefetch, no graph traversal on hot path). const MEMORY_FILES: ReadonlyArray<{ path: string; title: string }> = [ - { path: path.join(HOME, ".claude", "LIFEOS", "USER", "PRINCIPAL", "PRINCIPAL_MEMORY.md"), title: "Principal Memory" }, - { path: path.join(HOME, ".claude", "LIFEOS", "USER", "DIGITAL_ASSISTANT", "DA_MEMORY.md"), title: "DA Memory" }, + { path: path.join(claudeDir(), "LIFEOS", "USER", "PRINCIPAL", "PRINCIPAL_MEMORY.md"), title: "Principal Memory" }, + { path: path.join(claudeDir(), "LIFEOS", "USER", "DIGITAL_ASSISTANT", "DA_MEMORY.md"), title: "DA Memory" }, ]; const RELEVANT_CACHE_TTL_MS = 60_000; @@ -632,7 +633,7 @@ function formatRelevantBlock(results: RelevantResultItem[]): string { if (results.length === 0) return ""; const lines: string[] = ["## RELEVANT MEMORY"]; for (const r of results) { - const shortPath = r.path.replace(HOME + "/.claude/", ""); + const shortPath = r.path.replace(claudeDir() + "/", ""); lines.push(""); lines.push(`### [${r.type} · ${r.score.toFixed(1)}] ${r.title}`); lines.push(``); diff --git a/LifeOS/install/LIFEOS/TOOLS/MemoryReviewer.ts b/LifeOS/install/LIFEOS/TOOLS/MemoryReviewer.ts index 8cb63d89ad..d4f7bd3ba0 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MemoryReviewer.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemoryReviewer.ts @@ -47,6 +47,7 @@ import { read as memoryWriterRead } from "./MemoryWriter"; import { isKnownType, type TypedItem } from "./MemoryTypes"; import { inference } from "./Inference"; import { getPrincipalName, getDAName } from "../../hooks/lib/identity"; +import { claudeDir } from "./lifeos-root"; import { applyProposalEdit, markProposal, @@ -55,8 +56,8 @@ import { // ── Constants ── -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); -const HARNESS_PROJECTS_DIR = pathResolve(homedir(), ".claude", "projects"); +const CLAUDE_ROOT = pathResolve(claudeDir()); +const HARNESS_PROJECTS_DIR = pathResolve(claudeDir(), "projects"); const RUNS_LOG_PATH = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/reviewer-runs.jsonl"); const RUNS_DEBUG_DIR = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/reviewer-runs"); const REVIEW_CONFIG_PATH = pathResolve(CLAUDE_ROOT, "LIFEOS/USER/CONFIG/memory-review.json"); @@ -681,7 +682,7 @@ async function smokeTest(): Promise { const mockResponse = JSON.stringify({ items: [ { type: "memory", actor: "principal", content: "PREFERENCE: smoke E2E mock" }, - { type: "proposal", target_file: pathJoin(homedir(), ".claude/LIFEOS/USER/PRINCIPAL/PRINCIPAL_IDENTITY.md"), edit: "RULE: E2E mock", confidence: 0.5, rationale: "smoke" }, + { type: "proposal", target_file: pathJoin(claudeDir(), "LIFEOS/USER/PRINCIPAL/PRINCIPAL_IDENTITY.md"), edit: "RULE: E2E mock", confidence: 0.5, rationale: "smoke" }, ], }); diff --git a/LifeOS/install/LIFEOS/TOOLS/MemoryStatus.ts b/LifeOS/install/LIFEOS/TOOLS/MemoryStatus.ts index c82729aa78..991f5957c2 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MemoryStatus.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemoryStatus.ts @@ -33,8 +33,9 @@ import { PENDING_PROPOSALS_PATH, } from "./MemoryTypes"; import { read as readMemory } from "./MemoryWriter"; +import { claudeDir } from "./lifeos-root"; -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = pathResolve(claudeDir()); const LIFEOS_DIR = pathJoin(CLAUDE_ROOT, "LIFEOS"); const IDEAS_DIR = pathJoin(LIFEOS_DIR, "MEMORY", "IDEAS"); const KNOWLEDGE_DIR = pathJoin(LIFEOS_DIR, "MEMORY", "KNOWLEDGE"); diff --git a/LifeOS/install/LIFEOS/TOOLS/MemorySystem.ts b/LifeOS/install/LIFEOS/TOOLS/MemorySystem.ts index 5e8f0dfe55..d60f0cd14f 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MemorySystem.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemorySystem.ts @@ -70,10 +70,11 @@ import { setEntries as memoryWriterSetEntries, read as memoryWriterRead } from " import { getTier } from "./MutationTier"; import { getRelevantContext, type RelevantResultItem } from "./MemoryRetriever"; import { mintId, slugFromPath, SCHEMA_VERSION } from "./KnowledgeSchema"; +import { claudeDir } from "./lifeos-root"; // ── Constants ── -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = pathResolve(claudeDir()); // ── Result types ── @@ -694,7 +695,7 @@ async function smokeTest(): Promise { // 5. ISC-156 — proposal enqueues const r5 = add({ type: "proposal", - target_file: pathJoin(homedir(), ".claude/LIFEOS/USER/PRINCIPAL/PRINCIPAL_IDENTITY.md"), + target_file: pathJoin(claudeDir(), "LIFEOS/USER/PRINCIPAL/PRINCIPAL_IDENTITY.md"), edit: "RULE: This is a smoke-test proposal — DO NOT APPLY.", confidence: 0.42, rationale: "smoke test", diff --git a/LifeOS/install/LIFEOS/TOOLS/MemoryTypes.ts b/LifeOS/install/LIFEOS/TOOLS/MemoryTypes.ts index 1fd9bee557..33591ee3b6 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MemoryTypes.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemoryTypes.ts @@ -37,10 +37,11 @@ import { resolve as pathResolve, join as pathJoin } from "node:path"; import { homedir } from "node:os"; +import { claudeDir } from "./lifeos-root"; // ── Paths ── -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = pathResolve(claudeDir()); const LIFEOS_DIR = pathJoin(CLAUDE_ROOT, "LIFEOS"); const KNOWLEDGE_DIR = pathJoin(LIFEOS_DIR, "MEMORY", "KNOWLEDGE"); diff --git a/LifeOS/install/LIFEOS/TOOLS/MemoryWriter.ts b/LifeOS/install/LIFEOS/TOOLS/MemoryWriter.ts index 86de367d6a..08dbd3888d 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MemoryWriter.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemoryWriter.ts @@ -51,10 +51,11 @@ import { } from "node:fs"; import { dirname, resolve as pathResolve } from "node:path"; import { homedir } from "node:os"; +import { claudeDir } from "./lifeos-root"; // ── Constants ── -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = pathResolve(claudeDir()); const ALLOWED_FILES = new Set([ pathResolve(CLAUDE_ROOT, "LIFEOS/USER/PRINCIPAL/PRINCIPAL_MEMORY.md"), diff --git a/LifeOS/install/LIFEOS/TOOLS/MergeSettings.ts b/LifeOS/install/LIFEOS/TOOLS/MergeSettings.ts index bd956f60e0..b4dff4d76e 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MergeSettings.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MergeSettings.ts @@ -13,6 +13,7 @@ import { readFile, writeFile } from "node:fs/promises"; import { existsSync } from "node:fs"; import path from "node:path"; import os from "node:os"; +import { claudeDir } from "./lifeos-root"; /** * Snapshot of the last merge output, written alongside every successful @@ -21,9 +22,7 @@ import os from "node:os"; * edits that haven't been merged yet never read as "drift" and never get * clobbered back into the overlay (the 2026-07-11 hooks-BPE incident). */ -export const MERGE_SNAPSHOT_PATH = path.join( - os.homedir(), - ".claude", +export const MERGE_SNAPSHOT_PATH = path.join(claudeDir(), "LIFEOS", "MEMORY", "STATE", diff --git a/LifeOS/install/LIFEOS/TOOLS/MigrateApprove.ts b/LifeOS/install/LIFEOS/TOOLS/MigrateApprove.ts index 87b7d93f2a..301bad9737 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MigrateApprove.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MigrateApprove.ts @@ -31,6 +31,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { import { readFileSync, writeFileSync, 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"]) { @@ -40,7 +41,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, "MEMORY", "MIGRATION", "migration-proposals.jsonl"); const COMMITTED_LOG = join(LIFEOS_DIR, "MEMORY", "MIGRATION", "committed.jsonl"); @@ -85,7 +86,7 @@ function resolveTargetPath(target: string): string { } if (target === "memory/feedback") { // Feedback memories live outside LifeOS dir in projects/${HARNESS_USER_DIR}/memory/ - return join(HOME, ".claude", "projects", "${HARNESS_USER_DIR}", "memory"); + return join(claudeDir(), "projects", "${HARNESS_USER_DIR}", "memory"); } return join(LIFEOS_DIR, target); } diff --git a/LifeOS/install/LIFEOS/TOOLS/MigrateContextFreshness.ts b/LifeOS/install/LIFEOS/TOOLS/MigrateContextFreshness.ts index 5bd03f305f..1d5038d38e 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MigrateContextFreshness.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MigrateContextFreshness.ts @@ -18,6 +18,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { import { createHash } from "crypto"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { basename, dirname, join, relative } from "path"; +import { claudeDir } from "./lifeos-root"; import { CONTEXT_FRESHNESS_REGISTRY, MARKER_RE, @@ -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 CLAUDE_DIR = dirname(LIFEOS_DIR); const SEED_ISO = "2026-05-03T23:00:00-07:00"; const BACKUP_TS = "2026-05-03-23-00-00"; diff --git a/LifeOS/install/LIFEOS/TOOLS/MigrateKnowledge.ts b/LifeOS/install/LIFEOS/TOOLS/MigrateKnowledge.ts index 9bae941ef1..79de0ce862 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MigrateKnowledge.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MigrateKnowledge.ts @@ -28,12 +28,13 @@ import { readdirSync, readFileSync, writeFileSync, renameSync, existsSync } from "node:fs"; import { resolve as pathResolve, join as pathJoin } from "node:path"; import { homedir } from "node:os"; +import { claudeDir } from "./lifeos-root"; import { parseNote, serializeNote, normalize, validate, DIR_TO_TYPE, slugFromPath, type CanonicalType, } from "./KnowledgeSchema"; -const KNOWLEDGE_DIR = pathResolve(homedir(), ".claude/LIFEOS/MEMORY/KNOWLEDGE"); +const KNOWLEDGE_DIR = pathResolve(claudeDir(), "LIFEOS/MEMORY/KNOWLEDGE"); const DIRS = ["People", "Companies", "Ideas", "Research", "Blogs"] as const; interface NoteOutcome { diff --git a/LifeOS/install/LIFEOS/TOOLS/MigrateScan.ts b/LifeOS/install/LIFEOS/TOOLS/MigrateScan.ts index 8d25405b15..1bbad42b71 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MigrateScan.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MigrateScan.ts @@ -29,6 +29,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, mkdirSync, appendFileSync } from "fs"; import { join, basename, dirname, extname } from "path"; import { randomUUID } from "crypto"; +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"]) { @@ -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 QUEUE_FILE = join(LIFEOS_DIR, "MEMORY", "MIGRATION", "migration-proposals.jsonl"); type Target = @@ -315,7 +316,7 @@ function main(): void { console.log(`⚠️ ${lowConf.length} chunks classified at <40% confidence — review recommended.`); } console.log(``); - console.log(`Next: bun ~/.claude/LIFEOS/TOOLS/MigrateApprove.ts --review`); + console.log(`Next: bun ${shellQuote(join(claudeDir(), "LIFEOS", "TOOLS", "MigrateApprove.ts"))} --review`); } main(); diff --git a/LifeOS/install/LIFEOS/TOOLS/MigrateTelosFreshness.ts b/LifeOS/install/LIFEOS/TOOLS/MigrateTelosFreshness.ts index d9ba452240..b1be0c1bd2 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MigrateTelosFreshness.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MigrateTelosFreshness.ts @@ -29,6 +29,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from "fs"; import { createHash } from "crypto"; 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"]) { @@ -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 TELOS_PATH = join(LIFEOS_DIR, "USER", "TELOS", "TELOS.md"); const BACKUP_DIR = join(LIFEOS_DIR, "USER", "TELOS", "Backups"); diff --git a/LifeOS/install/LIFEOS/TOOLS/MutationTier.ts b/LifeOS/install/LIFEOS/TOOLS/MutationTier.ts index 21ef51af47..b947995da6 100644 --- a/LifeOS/install/LIFEOS/TOOLS/MutationTier.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MutationTier.ts @@ -35,10 +35,11 @@ import { resolve as pathResolve } from "node:path"; import { homedir } from "node:os"; +import { claudeDir } from "./lifeos-root"; // ── Constants ── -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = pathResolve(claudeDir()); export type Tier = "A" | "B" | "C" | "D"; diff --git a/LifeOS/install/LIFEOS/TOOLS/NeofetchBanner.ts b/LifeOS/install/LIFEOS/TOOLS/NeofetchBanner.ts index a9a11fee72..91eee41db9 100755 --- a/LifeOS/install/LIFEOS/TOOLS/NeofetchBanner.ts +++ b/LifeOS/install/LIFEOS/TOOLS/NeofetchBanner.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/PangramScore.ts b/LifeOS/install/LIFEOS/TOOLS/PangramScore.ts index e3339e7cb1..546c42729c 100755 --- a/LifeOS/install/LIFEOS/TOOLS/PangramScore.ts +++ b/LifeOS/install/LIFEOS/TOOLS/PangramScore.ts @@ -27,6 +27,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { import { readFileSync, appendFileSync, mkdirSync } from "node:fs"; import { createHash } from "node:crypto"; import { dirname, join } from "node: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"]) { @@ -35,13 +36,13 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { } -const ENV_PATH = `${process.env.HOME}/.claude/.env`; +const ENV_PATH = `${claudeDir()}/.env`; // Run-record: proof the detector actually executed on a specific text. The // WritingGate Stop hook reads this so its pass condition is "Pangram ran on // this content", not "a token string is present" (Forge audit 2026-07-01). const RUNS_PATH = join( - process.env.LIFEOS_DIR || `${process.env.HOME}/.claude/LIFEOS`, + process.env.LIFEOS_DIR || `${claudeDir()}/LIFEOS`, "MEMORY", "OBSERVABILITY", "pangram-runs.jsonl", ); export function normalizeForHash(text: string): string { @@ -69,7 +70,7 @@ function loadKey(): string { const line = env.split("\n").find((l) => l.startsWith("PANGRAM_API_KEY=")); if (line) return line.slice("PANGRAM_API_KEY=".length).replace(/^["']|["']$/g, "").trim(); } catch {} - console.error("No PANGRAM_API_KEY found. Add it to ~/.claude/.env, then re-run."); + console.error(`No PANGRAM_API_KEY found. Add it to ${ENV_PATH}, then re-run.`); process.exit(1); } diff --git a/LifeOS/install/LIFEOS/TOOLS/PerplexitySearch.ts b/LifeOS/install/LIFEOS/TOOLS/PerplexitySearch.ts index 2c0c9bff1b..95d96f983d 100755 --- a/LifeOS/install/LIFEOS/TOOLS/PerplexitySearch.ts +++ b/LifeOS/install/LIFEOS/TOOLS/PerplexitySearch.ts @@ -36,8 +36,8 @@ */ import { readFileSync } from 'fs' -import { homedir } from 'os' import { join } from 'path' +import { claudeDir, shellQuote } from "./lifeos-root"; const colors = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', @@ -46,9 +46,7 @@ const colors = { // Load environment — mirrors LIFEOS/TOOLS/Grok.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') @@ -128,12 +126,12 @@ async function main() { const { opts, query } = parseArgs(process.argv.slice(2)) if (!API_KEY) { - console.error(`${colors.red}Error: PERPLEXITY_API_KEY not set in ~/.claude/.env${colors.reset}`) + console.error(`${colors.red}Error: PERPLEXITY_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/PerplexitySearch.ts [--model sonar-pro] [--recency day] [--json] ""`) + console.error(`Usage: bun ${shellQuote(join(claudeDir(), 'LIFEOS', 'TOOLS', 'PerplexitySearch.ts'))} [--model sonar-pro] [--recency day] [--json] ""`) process.exit(1) } diff --git a/LifeOS/install/LIFEOS/TOOLS/PiSync.sh b/LifeOS/install/LIFEOS/TOOLS/PiSync.sh index c36f17e10e..04804cdfe3 100755 --- a/LifeOS/install/LIFEOS/TOOLS/PiSync.sh +++ b/LifeOS/install/LIFEOS/TOOLS/PiSync.sh @@ -6,11 +6,11 @@ set -euo pipefail -LifeOS=~/.claude +LifeOS="$(dirname "${LIFEOS_DIR:-$HOME/.claude/LIFEOS}")" PI=~/.pi/agent [ -d "$PI" ] || { echo "✗ ~/.pi/agent missing"; exit 1; } -[ -d "$LifeOS/LIFEOS" ] || { echo "✗ ~/.claude/LIFEOS missing"; exit 1; } +[ -d "$LifeOS/LIFEOS" ] || { echo "✗ $LifeOS/LIFEOS missing"; exit 1; } echo "→ PiSync v2" diff --git a/LifeOS/install/LIFEOS/TOOLS/ProposeCurrentStateEntry.ts b/LifeOS/install/LIFEOS/TOOLS/ProposeCurrentStateEntry.ts index c3979888a8..2eba1e5311 100755 --- a/LifeOS/install/LIFEOS/TOOLS/ProposeCurrentStateEntry.ts +++ b/LifeOS/install/LIFEOS/TOOLS/ProposeCurrentStateEntry.ts @@ -27,6 +27,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { import { appendFileSync, mkdirSync, existsSync } from "fs"; import { join, dirname } from "path"; import { randomUUID } from "crypto"; +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"]) { @@ -36,7 +37,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 ALLOWED_SOURCES = ["lifelog", "calendar", "gmail", "homebridge", "manual", "amazon", "bills"]; diff --git a/LifeOS/install/LIFEOS/TOOLS/Recommend.ts b/LifeOS/install/LIFEOS/TOOLS/Recommend.ts index aceda1e817..739c216205 100755 --- a/LifeOS/install/LIFEOS/TOOLS/Recommend.ts +++ b/LifeOS/install/LIFEOS/TOOLS/Recommend.ts @@ -23,6 +23,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { import { readFileSync, 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"]) { @@ -32,7 +33,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 TELOS_DIR = join(LIFEOS_DIR, "USER", "TELOS"); const CURRENT_DIR = join(TELOS_DIR, "CURRENT_STATE"); diff --git a/LifeOS/install/LIFEOS/TOOLS/ReferenceCheck.ts b/LifeOS/install/LIFEOS/TOOLS/ReferenceCheck.ts index b3b506995d..2b7fe8b7ea 100755 --- a/LifeOS/install/LIFEOS/TOOLS/ReferenceCheck.ts +++ b/LifeOS/install/LIFEOS/TOOLS/ReferenceCheck.ts @@ -27,9 +27,10 @@ import { readFileSync, statSync, existsSync, readdirSync, realpathSync } from 'fs'; import { join, resolve, dirname, relative, extname, sep } 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'); // ── Arg parsing (manual, zero deps) ── @@ -288,7 +289,7 @@ const REF_PATTERNS: { re: RegExp; label: string }[] = [ { re: new RegExp('`((?:LifeOS|hooks|skills|agents|Pulse|USER|MEMORY|Components|Algorithm|Tools|Workflows|References)\\/[\\w/@.-]+?' + EXT + ')`', 'g'), label: 'backtick-anchored' }, // Backtick-quoted paths starting with ~/.claude/ { re: new RegExp('`~\\/\\.claude\\/([\\w/@.-]+?' + EXT + ')`', 'g'), label: 'backtick-home' }, - // Backtick-quoted paths with $HOME/.claude/ or ${HOME}/.claude/ + // Backtick-quoted paths with $HOME/.claude/ or ${claudeDir()}/ { re: new RegExp('`\\$(?:HOME|\\{HOME\\})\\/\\.claude\\/([\\w/@.-]+?' + EXT + ')`', 'g'), label: 'backtick-env-home' }, // @-import at start of line: @LIFEOS/USER/FILE.md { re: /^@(LifeOS\/[\w/@.-]+\.md)/gm, label: 'at-import' }, diff --git a/LifeOS/install/LIFEOS/TOOLS/Services.ts b/LifeOS/install/LIFEOS/TOOLS/Services.ts index 649e9ebea9..02a669e995 100755 --- a/LifeOS/install/LIFEOS/TOOLS/Services.ts +++ b/LifeOS/install/LIFEOS/TOOLS/Services.ts @@ -15,9 +15,12 @@ import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; +import { claudeDir } from "./lifeos-root"; const HOME = homedir(); -const CLAUDE = join(HOME, ".claude"); +// Config root via the canonical resolver (LIFEOS_DIR env / self-location), NOT a +// hardcoded ~/.claude — so `status`/install/uninstall target a custom LifeOS home too. +const CLAUDE = claudeDir(); const LIFEOS = join(CLAUDE, "LIFEOS"); const TOOLS = join(LIFEOS, "TOOLS"); const PULSE = join(LIFEOS, "PULSE"); diff --git a/LifeOS/install/LIFEOS/TOOLS/SessionHarvester.ts b/LifeOS/install/LIFEOS/TOOLS/SessionHarvester.ts index d0501ee871..c40e601ad2 100755 --- a/LifeOS/install/LIFEOS/TOOLS/SessionHarvester.ts +++ b/LifeOS/install/LIFEOS/TOOLS/SessionHarvester.ts @@ -22,16 +22,17 @@ import { parseArgs } from "util"; import * as fs from "fs"; import * as path from "path"; -import { getLearningCategory, isLearningCapture } from "../../../.claude/hooks/lib/learning-utils"; +import { getLearningCategory, isLearningCapture } from "../../hooks/lib/learning-utils"; +import { claudeDir } from "./lifeos-root"; // ============================================================================ // Configuration // ============================================================================ -const CLAUDE_DIR = path.join(process.env.HOME!, ".claude"); +const CLAUDE_DIR = path.join(claudeDir()); // Derive the project slug dynamically from CLAUDE_DIR (works on macOS and Linux) -// macOS: ${HOME}/.claude → ${HARNESS_USER_DIR} -// Linux: ${HOME}/.claude → ${HARNESS_USER_DIR} +// macOS: ${claudeDir()} → ${HARNESS_USER_DIR} +// Linux: ${claudeDir()} → ${HARNESS_USER_DIR} const CWD_SLUG = CLAUDE_DIR.replace(/[\/\.]/g, "-"); const PROJECTS_DIR = path.join(CLAUDE_DIR, "projects", CWD_SLUG); const LEARNING_DIR = path.join(CLAUDE_DIR, "LIFEOS", "MEMORY", "LEARNING"); diff --git a/LifeOS/install/LIFEOS/TOOLS/SessionProgress.ts b/LifeOS/install/LIFEOS/TOOLS/SessionProgress.ts index 8986f85348..9a8a99b505 100755 --- a/LifeOS/install/LIFEOS/TOOLS/SessionProgress.ts +++ b/LifeOS/install/LIFEOS/TOOLS/SessionProgress.ts @@ -11,6 +11,7 @@ import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs'; import { join } from 'path'; +import { claudeDir } from "./lifeos-root"; interface Decision { timestamp: string; @@ -44,7 +45,7 @@ interface SessionProgress { } // Progress files are now in STATE/progress/ (consolidated from MEMORY/PROGRESS/) -const PROGRESS_DIR = join(process.env.HOME || '', '.claude', 'LIFEOS', 'MEMORY', 'STATE', 'progress'); +const PROGRESS_DIR = join(claudeDir(), 'LIFEOS', 'MEMORY', 'STATE', 'progress'); function getProgressPath(project: string): string { return join(PROGRESS_DIR, `${project}-progress.json`); diff --git a/LifeOS/install/LIFEOS/TOOLS/SessionRename.ts b/LifeOS/install/LIFEOS/TOOLS/SessionRename.ts index 50c927b65d..27afe8c40c 100644 --- a/LifeOS/install/LIFEOS/TOOLS/SessionRename.ts +++ b/LifeOS/install/LIFEOS/TOOLS/SessionRename.ts @@ -40,8 +40,9 @@ import { homedir } from "node:os"; // event-sourced write path. This file previously carried a duplicate // tmp+rename implementation; that was the one writer outside writeRegistry. import { readRegistry, writeRegistry } from "../../hooks/lib/isa-utils"; +import { claudeDir } from "./lifeos-root"; -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = pathResolve(claudeDir()); const SESSION_NAMES_JSON = pathJoin(CLAUDE_ROOT, "LIFEOS/MEMORY/STATE/session-names.json"); interface WorkSession { diff --git a/LifeOS/install/LIFEOS/TOOLS/SettingsBackport.ts b/LifeOS/install/LIFEOS/TOOLS/SettingsBackport.ts index 60aed10d6f..65411eef56 100755 --- a/LifeOS/install/LIFEOS/TOOLS/SettingsBackport.ts +++ b/LifeOS/install/LIFEOS/TOOLS/SettingsBackport.ts @@ -36,8 +36,9 @@ import { existsSync } from "node:fs"; import path from "node:path"; import os from "node:os"; import { mergeSettings, deepEqual, parseJsonFileOrThrow, MERGE_SNAPSHOT_PATH } from "./MergeSettings"; +import { claudeDir } from "./lifeos-root"; -const CLAUDE_DIR = path.join(os.homedir(), ".claude"); +const CLAUDE_DIR = path.join(claudeDir()); const SYSTEM_PATH = path.join(CLAUDE_DIR, "settings.system.json"); const USER_PATH = path.join(CLAUDE_DIR, "LIFEOS", "USER", "CONFIG", "settings.user.json"); const GENERATED_PATH = path.join(CLAUDE_DIR, "settings.json"); diff --git a/LifeOS/install/LIFEOS/TOOLS/SyncIdentityToSettings.ts b/LifeOS/install/LIFEOS/TOOLS/SyncIdentityToSettings.ts index 37fce1d66f..548ed9a9c2 100644 --- a/LifeOS/install/LIFEOS/TOOLS/SyncIdentityToSettings.ts +++ b/LifeOS/install/LIFEOS/TOOLS/SyncIdentityToSettings.ts @@ -19,10 +19,11 @@ import { readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; import { paiUserDir } from './LifeosConfig'; +import { claudeDir } from "./lifeos-root"; const HOME = homedir(); const PRINCIPAL_PATH = join(paiUserDir(), 'PRINCIPAL/PRINCIPAL_IDENTITY.md'); -const SETTINGS_PATH = join(HOME, '.claude/settings.json'); +const SETTINGS_PATH = join(claudeDir(), "settings.json"); const VERBOSE = process.argv.includes('--verbose'); function snakeToCamel(s: string): string { diff --git a/LifeOS/install/LIFEOS/TOOLS/TelosFreshness.ts b/LifeOS/install/LIFEOS/TOOLS/TelosFreshness.ts index 7623a85c7c..82c51dd22c 100755 --- a/LifeOS/install/LIFEOS/TOOLS/TelosFreshness.ts +++ b/LifeOS/install/LIFEOS/TOOLS/TelosFreshness.ts @@ -33,6 +33,7 @@ import { readFileSync, writeFileSync, existsSync } from "fs"; import { getDAName } from "../../hooks/lib/identity" import { basename, 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"]) { @@ -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 TELOS_PATH = join(LIFEOS_DIR, "USER", "TELOS", "TELOS.md"); const DA_IDENTITY_PATH = join(LIFEOS_DIR, "USER", "DIGITAL_ASSISTANT", "DA_IDENTITY.md"); const PRINCIPAL_IDENTITY_PATH = join(LIFEOS_DIR, "USER", "PRINCIPAL", "PRINCIPAL_IDENTITY.md"); diff --git a/LifeOS/install/LIFEOS/TOOLS/TlpArchive.ts b/LifeOS/install/LIFEOS/TOOLS/TlpArchive.ts index d18ad15243..39671d9e1b 100755 --- a/LifeOS/install/LIFEOS/TOOLS/TlpArchive.ts +++ b/LifeOS/install/LIFEOS/TOOLS/TlpArchive.ts @@ -14,9 +14,10 @@ import { writeFileSync, existsSync, readFileSync, mkdirSync } from "node:fs"; import { join } from "node:path"; +import { claudeDir } from "./lifeos-root"; const HOME = process.env.HOME!; -const KNOWLEDGE_DIR = join(HOME, ".claude/LIFEOS/MEMORY/KNOWLEDGE/Blogs"); +const KNOWLEDGE_DIR = join(claudeDir(), "LIFEOS/MEMORY/KNOWLEDGE/Blogs"); const URL_FILE = "/tmp/tlp-urls.txt"; const FAILED_FILE = "/tmp/tlp-failed.txt"; const SUCCESS_FILE = "/tmp/tlp-success.txt"; diff --git a/LifeOS/install/LIFEOS/TOOLS/TokenXray/README.md b/LifeOS/install/LIFEOS/TOOLS/TokenXray/README.md index 9faf2294b9..c78d9f4834 100644 --- a/LifeOS/install/LIFEOS/TOOLS/TokenXray/README.md +++ b/LifeOS/install/LIFEOS/TOOLS/TokenXray/README.md @@ -1,7 +1,7 @@ # TokenXray TypeScript port of [`claude-code-token-xray`](https://github.com/Coral-Bricks-AI/coral-ai/tree/main/claude-code-token-xray) -(Apache 2.0, Coral Bricks AI). Reads only `~/.claude/projects/*/*.jsonl` — +(Apache 2.0, Coral Bricks AI). Reads only `{{LIFEOS_ROOT}}/projects/*/*.jsonl` — nothing leaves the machine. ## Quickstart @@ -29,7 +29,7 @@ Add `--json` to any subcommand for structured output. ### `actual` — subscription vs API -The other subcommands price every call at Opus 4.7 list rates. That's what your bill *would* be on the API. If you're on Claude Max, most of `~/.claude/projects/` is OAuth-billed (subscription), so the marginal cost is zero — the Max fee is fixed. Real API spend comes from LifeOS's separate channels (Inference.ts, bridge bots, admin tools), tracked in `LIFEOS/MEMORY/OBSERVABILITY/anthropic-cost.jsonl`. `actual` shows both side-by-side. +The other subcommands price every call at Opus 4.7 list rates. That's what your bill *would* be on the API. If you're on Claude Max, most of `{{LIFEOS_ROOT}}/projects/` is OAuth-billed (subscription), so the marginal cost is zero — the Max fee is fixed. Real API spend comes from LifeOS's separate channels (Inference.ts, bridge bots, admin tools), tracked in `LIFEOS/MEMORY/OBSERVABILITY/anthropic-cost.jsonl`. `actual` shows both side-by-side. ## Caveats diff --git a/LifeOS/install/LIFEOS/TOOLS/TokenXray/TokenXray.ts b/LifeOS/install/LIFEOS/TOOLS/TokenXray/TokenXray.ts index 4040211629..f7009a2b05 100644 --- a/LifeOS/install/LIFEOS/TOOLS/TokenXray/TokenXray.ts +++ b/LifeOS/install/LIFEOS/TOOLS/TokenXray/TokenXray.ts @@ -23,6 +23,7 @@ import { globSync } from "fs"; import { homedir } from "os"; import { join } from "path"; import { getEncoding } from "js-tiktoken"; +import { claudeDir } from "../lifeos-root"; const enc = getEncoding("cl100k_base"); @@ -105,11 +106,11 @@ function* readJsonl(path: string): IterableIterator> { } function mainLogs(): string[] { - return glob(join(homedir(), ".claude/projects/*/*.jsonl")); + return glob(join(claudeDir(), "projects/*/*.jsonl")); } function sideLogs(): string[] { - return glob(join(homedir(), ".claude/projects/*/*/subagents/*.jsonl")); + return glob(join(claudeDir(), "projects/*/*/subagents/*.jsonl")); } function fmtNum(n: number, w = 0): string { @@ -228,7 +229,7 @@ interface ApiCostSnapshot { } function loadLatestApiCostSnapshot(): ApiCostSnapshot | null { - const path = join(homedir(), ".claude/LIFEOS/MEMORY/OBSERVABILITY/anthropic-cost.jsonl"); + const path = join(claudeDir(), "LIFEOS/MEMORY/OBSERVABILITY/anthropic-cost.jsonl"); let raw: string; try { raw = readFileSync(path, "utf8"); diff --git a/LifeOS/install/LIFEOS/TOOLS/TranscriptParser.ts b/LifeOS/install/LIFEOS/TOOLS/TranscriptParser.ts index 594031c9f0..a236b7f5e2 100755 --- a/LifeOS/install/LIFEOS/TOOLS/TranscriptParser.ts +++ b/LifeOS/install/LIFEOS/TOOLS/TranscriptParser.ts @@ -17,7 +17,7 @@ */ import { readFileSync } from 'fs'; -import { getIdentity } from '../../../.claude/hooks/lib/identity'; +import { getIdentity } from '../../hooks/lib/identity'; const DA_IDENTITY = getIdentity(); diff --git a/LifeOS/install/LIFEOS/TOOLS/UpdateLifeosState.ts b/LifeOS/install/LIFEOS/TOOLS/UpdateLifeosState.ts index 1f33a77123..9c7f7dcc23 100755 --- a/LifeOS/install/LIFEOS/TOOLS/UpdateLifeosState.ts +++ b/LifeOS/install/LIFEOS/TOOLS/UpdateLifeosState.ts @@ -35,6 +35,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { import { readFileSync, writeFileSync, existsSync, 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"]) { @@ -44,7 +45,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 STATE_FILE = join(LIFEOS_DIR, "USER", "TELOS", "LIFEOS_STATE.json"); diff --git a/LifeOS/install/LIFEOS/TOOLS/UpdateModels.ts b/LifeOS/install/LIFEOS/TOOLS/UpdateModels.ts index 0d3a7de794..3b743116b0 100755 --- a/LifeOS/install/LIFEOS/TOOLS/UpdateModels.ts +++ b/LifeOS/install/LIFEOS/TOOLS/UpdateModels.ts @@ -24,6 +24,7 @@ import { readFileSync, writeFileSync, appendFileSync, readdirSync } from "fs"; import { join } from "path"; import { CURRENT, ALIAS, EFFORT_MODEL, CLAUDE_ID_PATTERN, type ClaudeTier } from "./models"; +import { claudeDir, shellQuote } from "./lifeos-root"; const CLAUDE_DIR = join(import.meta.dir, "..", ".."); const REGISTRY_PATH = join(import.meta.dir, "models.ts"); @@ -112,7 +113,7 @@ export function buildActionCommands(ids: string[]): string[] { return ids.map((id) => { const t = tierOf(id); return t - ? `bun ~/.claude/LIFEOS/TOOLS/UpdateModels.ts --apply ${t} ${id} && bun ~/.claude/LIFEOS/TOOLS/UpdateModels.ts --check` + ? `bun ${shellQuote(join(claudeDir(), "LIFEOS", "TOOLS", "UpdateModels.ts"))} --apply ${t} ${id} && bun ${shellQuote(join(claudeDir(), "LIFEOS", "TOOLS", "UpdateModels.ts"))} --check` : `review ${id} (unparseable tier)`; }); } diff --git a/LifeOS/install/LIFEOS/TOOLS/UsageAggregator.ts b/LifeOS/install/LIFEOS/TOOLS/UsageAggregator.ts index 6f31051a35..4e9cd4b9ff 100755 --- a/LifeOS/install/LIFEOS/TOOLS/UsageAggregator.ts +++ b/LifeOS/install/LIFEOS/TOOLS/UsageAggregator.ts @@ -26,8 +26,9 @@ import { existsSync, readFileSync, readdirSync, writeFileSync, statSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; +import { claudeDir } from "./lifeos-root"; -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 SESSION_COSTS = join(OBS_DIR, "session-costs.jsonl"); const PROJECTS_DIR = join(CLAUDE_DIR, "projects"); diff --git a/LifeOS/install/LIFEOS/TOOLS/WisdomCrossFrameSynthesizer.ts b/LifeOS/install/LIFEOS/TOOLS/WisdomCrossFrameSynthesizer.ts index 623cd8efc5..3a8920575d 100644 --- a/LifeOS/install/LIFEOS/TOOLS/WisdomCrossFrameSynthesizer.ts +++ b/LifeOS/install/LIFEOS/TOOLS/WisdomCrossFrameSynthesizer.ts @@ -24,6 +24,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; import { join, basename } from 'path'; import { parseArgs } from 'util'; +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"]) { @@ -32,7 +33,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { } -const BASE_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude'); +const BASE_DIR = process.env.LIFEOS_DIR || join(claudeDir()); const WISDOM_DIR = join(BASE_DIR, 'MEMORY', 'WISDOM'); const FRAMES_DIR = join(WISDOM_DIR, 'FRAMES'); const PRINCIPLES_DIR = join(WISDOM_DIR, 'PRINCIPLES'); diff --git a/LifeOS/install/LIFEOS/TOOLS/WisdomDomainClassifier.ts b/LifeOS/install/LIFEOS/TOOLS/WisdomDomainClassifier.ts index 0e9f67db09..662ba6605c 100644 --- a/LifeOS/install/LIFEOS/TOOLS/WisdomDomainClassifier.ts +++ b/LifeOS/install/LIFEOS/TOOLS/WisdomDomainClassifier.ts @@ -23,6 +23,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { import { existsSync, readdirSync, readFileSync } from 'fs'; import { join, basename } from 'path'; import { parseArgs } from 'util'; +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 BASE_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude'); +const BASE_DIR = process.env.LIFEOS_DIR || join(claudeDir()); const FRAMES_DIR = join(BASE_DIR, 'MEMORY', 'WISDOM', 'FRAMES'); // ── Domain Keyword Map ── diff --git a/LifeOS/install/LIFEOS/TOOLS/WisdomFrameUpdater.ts b/LifeOS/install/LIFEOS/TOOLS/WisdomFrameUpdater.ts index 112c5a5594..abaec9e5de 100644 --- a/LifeOS/install/LIFEOS/TOOLS/WisdomFrameUpdater.ts +++ b/LifeOS/install/LIFEOS/TOOLS/WisdomFrameUpdater.ts @@ -25,6 +25,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; import { join } from 'path'; import { parseArgs } from 'util'; +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"]) { @@ -33,7 +34,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { } -const BASE_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude'); +const BASE_DIR = process.env.LIFEOS_DIR || join(claudeDir()); const FRAMES_DIR = join(BASE_DIR, 'MEMORY', 'WISDOM', 'FRAMES'); // ── Types ── diff --git a/LifeOS/install/LIFEOS/TOOLS/WorkSweep.ts b/LifeOS/install/LIFEOS/TOOLS/WorkSweep.ts index 1e090fd46c..20134ca2b5 100755 --- a/LifeOS/install/LIFEOS/TOOLS/WorkSweep.ts +++ b/LifeOS/install/LIFEOS/TOOLS/WorkSweep.ts @@ -36,6 +36,7 @@ import { getDAName } from "../../hooks/lib/identity" import { join } from "path"; import { loadWorkConfig } from "../../hooks/lib/work-config"; +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"]) { @@ -47,7 +48,7 @@ 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 LIFEOS_DIR = process.env.LIFEOS_DIR || join(claudeDir(), "LIFEOS"); const WORK_DIR = join(LIFEOS_DIR, "MEMORY", "WORK"); const OBS_DIR = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY"); const OBS_LOG = join(OBS_DIR, "worksweep.jsonl"); @@ -532,7 +533,7 @@ async function sweepBpeCadence( "", `**Run:** \`Skill("BitterPillEngineering", "audit")\` over the ${BPE_AUDIT_TARGETS}.`, `**Cadence:** every ${BPE_CADENCE_DAYS} days. Last audit: ${days === null ? "never" : days + " days ago"}.`, - `**Then stamp it:** \`bun ~/.claude/LIFEOS/TOOLS/WorkSweep.ts --stamp-bpe\` to reset the clock.`, + `**Then stamp it:** \`bun ${shellQuote(join(claudeDir(), "LIFEOS", "TOOLS", "WorkSweep.ts"))} --stamp-bpe\` to reset the clock.`, "", `Propose-only — never auto-cut. A cut needs judgment + ratification (the producer-lock / shadow-log call is why).`, "", @@ -615,7 +616,7 @@ async function main(): Promise { // Final step: regenerate the TASKLIST.md and push (best-effort, never blocks) if (!dryRun) { const proc = Bun.spawn( - ["bun", join(HOME, ".claude", "skills", "_ULWORK", "Tools", "RegenerateTasklist.ts"), "--commit-push"], + ["bun", join(claudeDir(), "skills", "_ULWORK", "Tools", "RegenerateTasklist.ts"), "--commit-push"], { stdout: "inherit", stderr: "inherit", timeout: 30000 }, ); await proc.exited; diff --git a/LifeOS/install/LIFEOS/TOOLS/YouTubeApi.ts b/LifeOS/install/LIFEOS/TOOLS/YouTubeApi.ts index a8a9550f0f..c3e81fe88b 100755 --- a/LifeOS/install/LIFEOS/TOOLS/YouTubeApi.ts +++ b/LifeOS/install/LIFEOS/TOOLS/YouTubeApi.ts @@ -27,8 +27,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 } 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 +51,7 @@ const colors = { // Load environment 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') diff --git a/LifeOS/install/LIFEOS/TOOLS/algorithm.ts b/LifeOS/install/LIFEOS/TOOLS/algorithm.ts index 979dc50bdd..6acbf7c2ff 100755 --- a/LifeOS/install/LIFEOS/TOOLS/algorithm.ts +++ b/LifeOS/install/LIFEOS/TOOLS/algorithm.ts @@ -58,7 +58,8 @@ import { resolve, basename, join, dirname } from "path"; import { spawnSync, spawn } from "child_process"; import { resolveClaudeBin } from "./Inference"; // absolute claude path — ENOENT-safe under launchd/cron (PR #1460, author asdf8675309) import { randomUUID } from "crypto"; -import { generateISATemplate } from "../../../.claude/hooks/lib/isa-template"; +import { generateISATemplate } from "../../hooks/lib/isa-template"; +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"]) { @@ -70,7 +71,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { // ─── Paths ─────────────────────────────────────────────────────────────────── const HOME = process.env.HOME || "~"; -const BASE_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude"); +const BASE_DIR = process.env.LIFEOS_DIR || join(claudeDir()); const ALGORITHMS_DIR = join(BASE_DIR, "MEMORY", "STATE", "algorithms"); const SESSION_NAMES_PATH = join(BASE_DIR, "MEMORY", "STATE", "session-names.json"); const PROJECTS_DIR = process.env.PROJECTS_DIR || join(HOME, "Projects"); diff --git a/LifeOS/install/LIFEOS/TOOLS/healthsync/store.ts b/LifeOS/install/LIFEOS/TOOLS/healthsync/store.ts index ac2b583019..61e716bbd4 100644 --- a/LifeOS/install/LIFEOS/TOOLS/healthsync/store.ts +++ b/LifeOS/install/LIFEOS/TOOLS/healthsync/store.ts @@ -1,6 +1,7 @@ import { createHash } from "node:crypto"; import { appendFileSync, chmodSync, mkdirSync, renameSync } from "node:fs"; import { dirname, join } from "node:path"; +import { claudeDir } from "../lifeos-root"; import type { Ctx, DayFile, @@ -10,10 +11,10 @@ import type { } from "./types"; const HOME = process.env.HOME || ""; -const ENV_PATH = join(HOME, ".claude", ".env"); -const STATE_DIR = join(HOME, ".claude", "LIFEOS", "MEMORY", "STATE"); -const DATA_DIR = join(HOME, ".claude", "LIFEOS", "USER", "HEALTH", "DATA"); -const OBS_DIR = join(HOME, ".claude", "LIFEOS", "MEMORY", "OBSERVABILITY"); +const ENV_PATH = join(claudeDir(), ".env"); +const STATE_DIR = join(claudeDir(), "LIFEOS", "MEMORY", "STATE"); +const DATA_DIR = join(claudeDir(), "LIFEOS", "USER", "HEALTH", "DATA"); +const OBS_DIR = join(claudeDir(), "LIFEOS", "MEMORY", "OBSERVABILITY"); const TOKENS_PATH = join(STATE_DIR, "healthsync-tokens.json"); const STATE_PATH = join(STATE_DIR, "healthsync-state.json"); diff --git a/LifeOS/install/LIFEOS/TOOLS/lifeos-root.ts b/LifeOS/install/LIFEOS/TOOLS/lifeos-root.ts new file mode 100644 index 0000000000..71b0632378 --- /dev/null +++ b/LifeOS/install/LIFEOS/TOOLS/lifeos-root.ts @@ -0,0 +1,109 @@ +/** + * lifeos-root — the single source of truth for "where does LifeOS live" at runtime. + * + * Every LifeOS runtime tool/module MUST resolve the Claude home and the LIFEOS + * data dir through this module instead of hardcoding `join(homedir(), ".claude")`. + * That inline pattern silently breaks a custom LifeOS home (LIFEOS_HOME install, + * e.g. a project-scoped `~/Project/.claude`): the tool reads/writes `~/.claude` + * while the install lives elsewhere. + * + * Resolution order (claudeDir): + * 1. CLAUDE_PLUGIN_ROOT — packed-plugin install; the flattened root plays the + * `~/.claude` role. Must win: bin/pai exports LIFEOS_DIR + * == CLAUDE_PLUGIN_ROOT, where dirname() would escape it. + * 2. LIFEOS_DIR — set in settings.json env + service plists for every + * harness-spawned context; `/LIFEOS`, so the + * config root is its parent. This is the normal path. + * 3. self-location — belt-and-suspenders for a bare `bun Tool.ts` with no + * env: walk up from this module to the config root (the + * ancestor that contains a `LIFEOS/` child). This module + * ships to `/LIFEOS/TOOLS/`, so the walk finds + * the real custom root even without LIFEOS_DIR. + * 4. ~/.claude — plain-install default; byte-identical to the old + * hardcoded behavior. + * + * Mirrors hooks/lib/paths.ts (the hooks-tree resolver) — kept self-contained rather + * than cross-importing so it survives the plugin packer's tree flattening. + */ +import { existsSync } from "node:fs"; +import { homedir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +/** Expand a leading $HOME / ${HOME} / ~ in a path string. */ +export function expandPath(path: string): string { + const home = homedir(); + return path + .replace(/^\$HOME(?=\/|$)/, home) + .replace(/^\$\{HOME\}(?=\/|$)/, home) + .replace(/^~(?=\/|$)/, home); +} + +/** 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, `'"'"'`)}'`; +} + +/** + * Walk up from this module's own location to the config root — the ancestor + * directory that has a `LIFEOS/` child (installed tree: `/LIFEOS`; + * dev/payload tree: `install/LIFEOS`). Returns null if nothing matches (e.g. a + * standalone-packed skill without the LIFEOS tree beside it) so the caller can + * fall through to the default. Never throws. + */ +function selfConfigRoot(): string | null { + try { + let dir = dirname(fileURLToPath(import.meta.url)); // /LIFEOS/TOOLS + for (let i = 0; i < 8; i++) { + if (existsSync(join(dir, "LIFEOS"))) return dir; + const parent = dirname(dir); + if (parent === dir) break; + dir = parent; + } + } catch { + /* fileURLToPath / fs can fail in exotic runtimes — fall through */ + } + return null; +} + +/** The Claude Code home directory (holds settings.json, skills/, hooks/, .env, LIFEOS/). */ +export function claudeDir(): string { + const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT; + if (pluginRoot) return expandPath(pluginRoot); + + const envLifeosDir = process.env.LIFEOS_DIR; + if (envLifeosDir) return dirname(expandPath(envLifeosDir)); + + return selfConfigRoot() ?? join(homedir(), ".claude"); +} + +/** The LifeOS data directory (`/LIFEOS`: MEMORY, ALGORITHM, TOOLS, PULSE, USER). */ +export function lifeosDir(): string { + if (process.env.CLAUDE_PLUGIN_ROOT) return join(claudeDir(), "LIFEOS"); + + const envLifeosDir = process.env.LIFEOS_DIR; + if (envLifeosDir) return expandPath(envLifeosDir); + + return join(claudeDir(), "LIFEOS"); +} + +/** A path under the Claude home. */ +export function claudePath(...segments: string[]): string { + return join(claudeDir(), ...segments); +} + +/** A path under the LIFEOS data dir. */ +export function lifeosPath(...segments: string[]): string { + return join(lifeosDir(), ...segments); +} + +/** settings.json (in the Claude home). */ +export function settingsPath(): string { + return join(claudeDir(), "settings.json"); +} + +/** The authoritative .env (in the Claude home). */ +export function envPath(): string { + return join(claudeDir(), ".env"); +} diff --git a/LifeOS/install/LIFEOS/TOOLS/lifeos.ts b/LifeOS/install/LIFEOS/TOOLS/lifeos.ts index 697cdbf0af..aca300c89f 100755 --- a/LifeOS/install/LIFEOS/TOOLS/lifeos.ts +++ b/LifeOS/install/LIFEOS/TOOLS/lifeos.ts @@ -22,20 +22,51 @@ import { spawn, spawnSync } from "bun"; import { getIdentity, getStartupCatchphrase } from "../../hooks/lib/identity"; import { existsSync, readFileSync, writeFileSync, readdirSync, symlinkSync, unlinkSync, lstatSync } from "fs"; import { homedir } from "os"; -import { join, basename } from "path"; +import { join, basename, dirname, resolve } from "path"; +import { claudeDir } from "./lifeos-root"; // ============================================================================ // Configuration // ============================================================================ -const CLAUDE_DIR = join(homedir(), ".claude"); +const CLAUDE_DIR = join(claudeDir()); const MCP_DIR = join(CLAUDE_DIR, "MCPs"); const ACTIVE_MCP = join(CLAUDE_DIR, ".mcp.json"); -const BANNER_SCRIPT = join(homedir(), ".claude", "LIFEOS", "TOOLS", "Banner.ts"); +const BANNER_SCRIPT = join(claudeDir(), "LIFEOS", "TOOLS", "Banner.ts"); const VOICE_SERVER = "http://localhost:31337/notify/personality"; const WALLPAPER_DIR = join(homedir(), "Projects", "Wallpaper"); // Note: RAW archiving removed - Claude Code handles its own cleanup (30-day retention in projects/) +/** + * Make Claude load the same root this launcher came from. + * + * A project-scoped `/.claude` can preserve normal global+project + * merging by launching from ``. An arbitrary custom root has no + * discoverable project parent, so use CLAUDE_CONFIG_DIR for that process. + */ +function prepareClaudeLaunch(env: Record, stayLocal = false): void { + const root = resolve(CLAUDE_DIR); + const defaultRoot = resolve(join(homedir(), ".claude")); + if (root === defaultRoot) { + if (!stayLocal) process.chdir(root); + return; + } + + env.LIFEOS_HOME = root; + const projectRoot = basename(root) === ".claude" ? dirname(root) : null; + if (projectRoot && !stayLocal) { + // Project settings are discovered from the parent and merge with global. + delete env.CLAUDE_CONFIG_DIR; + process.chdir(projectRoot); + return; + } + + // Arbitrary roots (and --local, which keeps the caller's cwd) require the + // harness-level override to be discoverable. + env.CLAUDE_CONFIG_DIR = root; + if (!stayLocal) process.chdir(root); +} + // MCP shorthand mappings const MCP_SHORTCUTS: Record = { bd: "Brightdata-MCP.json", @@ -432,11 +463,6 @@ async function cmdLaunch(options: { mcp?: string; resume?: boolean; resumeId?: s } } - // Change to LifeOS directory unless --local flag is set - if (!options.local) { - process.chdir(CLAUDE_DIR); - } - // Voice notification (using focused marker for calmer tone). // Reads daidentity.startupCatchphrase from settings.json so the user's // install-time catchphrase is actually honored. Falls back to the @@ -447,9 +473,10 @@ async function cmdLaunch(options: { mcp?: string; resume?: boolean; resumeId?: s // BILLING: subscription, not API. Strip ANTHROPIC_API_KEY before spawn so the // interactive session uses OAuth (`claude /login`) instead of API-key billing. // Mirrors the protection in cmdPrompt() — same hazard, same fix. - const launchEnv = { ...process.env }; + const launchEnv: Record = { ...process.env } as Record; delete launchEnv.ANTHROPIC_API_KEY; launchEnv.CLAUDE_CODE_WORKFLOWS = "1"; + prepareClaudeLaunch(launchEnv, options.local); const proc = spawn(args, { stdio: ["inherit", "inherit", "inherit"], env: launchEnv, @@ -595,11 +622,10 @@ async function cmdPrompt(prompt: string) { args.push("--append-system-prompt-file", systemPromptFile); } - process.chdir(CLAUDE_DIR); - const env: Record = { ...process.env } as Record; delete env.ANTHROPIC_API_KEY; env.CLAUDE_CODE_WORKFLOWS = "1"; + prepareClaudeLaunch(env); const proc = spawn(args, { stdio: ["inherit", "inherit", "inherit"], env, diff --git a/LifeOS/install/LIFEOS/TOOLS/llcli/QUICKSTART.md b/LifeOS/install/LIFEOS/TOOLS/llcli/QUICKSTART.md index 9d6d37e49a..ff31dcc8d7 100755 --- a/LifeOS/install/LIFEOS/TOOLS/llcli/QUICKSTART.md +++ b/LifeOS/install/LIFEOS/TOOLS/llcli/QUICKSTART.md @@ -4,49 +4,49 @@ ## Installation -Already done! Located at: `~/.claude/LIFEOS/TOOLS/llcli/` +Already done! Located at: `{{LIFEOS_DIR}}/TOOLS/llcli/` ## Usage ```bash # Get help -~/.claude/LIFEOS/TOOLS/llcli/llcli.ts --help +"${LIFEOS_DIR}/TOOLS/llcli/llcli.ts" --help # Today's recordings -~/.claude/LIFEOS/TOOLS/llcli/llcli.ts today +"${LIFEOS_DIR}/TOOLS/llcli/llcli.ts" today # Specific date -~/.claude/LIFEOS/TOOLS/llcli/llcli.ts date 2025-11-17 +"${LIFEOS_DIR}/TOOLS/llcli/llcli.ts" date 2025-11-17 # Search -~/.claude/LIFEOS/TOOLS/llcli/llcli.ts search "consulting" +"${LIFEOS_DIR}/TOOLS/llcli/llcli.ts" search "consulting" # With custom limit -~/.claude/LIFEOS/TOOLS/llcli/llcli.ts today --limit 50 +"${LIFEOS_DIR}/TOOLS/llcli/llcli.ts" today --limit 50 ``` ## Piping to jq ```bash # Just titles -~/.claude/LIFEOS/TOOLS/llcli/llcli.ts today | jq -r '.data.lifelogs[].title' +"${LIFEOS_DIR}/TOOLS/llcli/llcli.ts" today | jq -r '.data.lifelogs[].title' # Count recordings -~/.claude/LIFEOS/TOOLS/llcli/llcli.ts date 2025-11-17 | jq '.data.lifelogs | length' +"${LIFEOS_DIR}/TOOLS/llcli/llcli.ts" date 2025-11-17 | jq '.data.lifelogs | length' # Long recordings (>30 min) -~/.claude/LIFEOS/TOOLS/llcli/llcli.ts today | jq '.data.lifelogs[] | select( +"${LIFEOS_DIR}/TOOLS/llcli/llcli.ts" today | jq '.data.lifelogs[] | select( ((.endTime | fromdateiso8601) - (.startTime | fromdateiso8601)) > 1800 )' ``` ## Configuration -API key already configured in `~/.claude/.env`: +API key already configured in `{{LIFEOS_ROOT}}/.env`: ```bash LIMITLESS_API_KEY=your_key ``` ## Full Documentation -See: `~/.claude/LIFEOS/TOOLS/llcli/README.md` +See: `{{LIFEOS_DIR}}/TOOLS/llcli/README.md` diff --git a/LifeOS/install/LIFEOS/TOOLS/llcli/README.md b/LifeOS/install/LIFEOS/TOOLS/llcli/README.md index 758c6797bc..ea2ce56cd0 100755 --- a/LifeOS/install/LIFEOS/TOOLS/llcli/README.md +++ b/LifeOS/install/LIFEOS/TOOLS/llcli/README.md @@ -35,26 +35,26 @@ This tool replaces ad-hoc bash scripts with a maintainable, version-controlled i 1. **Install the CLI:** ```bash - cd ~/.claude/LIFEOS/TOOLS/llcli + cd "${LIFEOS_DIR}/TOOLS/llcli" chmod +x llcli.ts ``` 2. **Add to PATH (optional):** ```bash # Add to ~/.zshrc or ~/.bashrc - export PATH="$HOME/.claude/LIFEOS/TOOLS/llcli:$PATH" + export PATH="${LIFEOS_DIR}/TOOLS/llcli:$PATH" ``` 3. **Configure API Key:** - Add to `~/.claude/.env`: + Add to `{{LIFEOS_ROOT}}/.env`: ```bash LIMITLESS_API_KEY=your_api_key_here ``` 4. **Verify Installation:** ```bash - ~/.claude/LIFEOS/TOOLS/llcli/llcli.ts --help + "${LIFEOS_DIR}/TOOLS/llcli/llcli.ts" --help ``` --- @@ -267,7 +267,7 @@ diff \ ### Environment Variables -**Location:** `~/.claude/.env` +**Location:** `{{LIFEOS_ROOT}}/.env` **Required:** ```bash @@ -360,17 +360,17 @@ llcli.ts ### "LIMITLESS_API_KEY not found" -**Solution:** Add API key to `~/.claude/.env`: +**Solution:** Add API key to `{{LIFEOS_ROOT}}/.env`: ```bash -echo "LIMITLESS_API_KEY=your_key" >> ~/.claude/.env +echo "LIMITLESS_API_KEY=your_key" >> "${LIFEOS_ROOT}/.env" ``` -### "Cannot read ~/.claude/.env file" +### "Cannot read {{LIFEOS_ROOT}}/.env file" **Solution:** Create the file: ```bash -touch ~/.claude/.env -chmod 600 ~/.claude/.env +touch "${LIFEOS_ROOT}/.env" +chmod 600 "${LIFEOS_ROOT}/.env" ``` ### "bun: command not found" @@ -384,7 +384,7 @@ curl -fsSL https://bun.sh/install | bash **Solution:** Make executable: ```bash -chmod +x ~/.claude/LIFEOS/TOOLS/llcli/llcli.ts +chmod +x "${LIFEOS_DIR}/TOOLS/llcli/llcli.ts" ``` ### API Errors @@ -449,12 +449,12 @@ Replace script calls: **Old:** ```bash -~/.claude/skills/lifelog/Scripts/fetch-lifelogs.sh today "" 20 +"${LIFEOS_ROOT}/skills/lifelog/Scripts/fetch-lifelogs.sh" today "" 20 ``` **New:** ```bash -~/.claude/LIFEOS/TOOLS/llcli/llcli.ts today --limit 20 +"${LIFEOS_DIR}/TOOLS/llcli/llcli.ts" today --limit 20 ``` ### With Workflows @@ -537,8 +537,8 @@ MIT ## Support For issues, questions, or contributions: -- File: `~/.claude/LIFEOS/TOOLS/llcli/` -- Skill: `~/.claude/skills/lifelog/` +- File: `{{LIFEOS_DIR}}/TOOLS/llcli/` +- Skill: `{{LIFEOS_ROOT}}/skills/lifelog/` - Constitution: `~/.claude/` --- diff --git a/LifeOS/install/LIFEOS/TOOLS/llcli/llcli.ts b/LifeOS/install/LIFEOS/TOOLS/llcli/llcli.ts index 18f1ba4d8a..450f2c7d16 100755 --- a/LifeOS/install/LIFEOS/TOOLS/llcli/llcli.ts +++ b/LifeOS/install/LIFEOS/TOOLS/llcli/llcli.ts @@ -19,6 +19,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { import { readFileSync } from 'fs'; import { join } from 'path'; import { homedir } 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"]) { @@ -67,7 +68,7 @@ const DEFAULTS = { * Load configuration from environment */ function loadConfig(): Config { - const envPath = process.env.LIFEOS_CONFIG_DIR ? join(process.env.LIFEOS_CONFIG_DIR, '.env') : join(homedir(), '.claude', '.env'); + const envPath = join(claudeDir(), '.env'); try { const envContent = readFileSync(envPath, 'utf-8'); @@ -78,7 +79,7 @@ function loadConfig(): Config { ?.trim(); if (!apiKey) { - console.error('Error: LIMITLESS_API_KEY not found in ~/.claude/.env'); + console.error(`Error: LIMITLESS_API_KEY not found in ${envPath}`); process.exit(1); } @@ -88,8 +89,8 @@ function loadConfig(): Config { baseUrl: DEFAULTS.baseUrl, }; } catch (error) { - console.error(`Error: Cannot read ~/.claude/.env file`); - console.error('Make sure LIMITLESS_API_KEY is set in ~/.claude/.env'); + console.error(`Error: Cannot read ${envPath}`); + console.error(`Make sure LIMITLESS_API_KEY is set in ${envPath}`); process.exit(1); } } diff --git a/LifeOS/install/USER/CONFIG/README.md b/LifeOS/install/USER/CONFIG/README.md index 39191df94b..59038a6a50 100644 --- a/LifeOS/install/USER/CONFIG/README.md +++ b/LifeOS/install/USER/CONFIG/README.md @@ -19,10 +19,10 @@ single source of truth for "where does my stuff live and how do I authenticate." `LIFEOS_CONFIG.yaml` itself does **not** store credentials. Secrets live in two places: -- **`~/.claude/.env`** — environment variables (`ELEVENLABS_API_KEY`, +- **`{{LIFEOS_ROOT}}/.env`** — environment variables (`ELEVENLABS_API_KEY`, `TELEGRAM_BOT_TOKEN`, etc.). Pulse loads this on boot. The installer writes here when you complete the voice / Telegram steps. -- **`~/.claude/LIFEOS/USER/CREDENTIALS/`** — credential JSON files (Google +- **`{{LIFEOS_DIR}}/USER/CREDENTIALS/`** — credential JSON files (Google OAuth, AWS profiles, etc.). The directory does not exist by default; create it on demand and `chmod 700` it. diff --git a/LifeOS/install/USER/DIGITAL_ASSISTANT/README.md b/LifeOS/install/USER/DIGITAL_ASSISTANT/README.md index 7eb49b9935..4a0cb592a2 100644 --- a/LifeOS/install/USER/DIGITAL_ASSISTANT/README.md +++ b/LifeOS/install/USER/DIGITAL_ASSISTANT/README.md @@ -23,9 +23,9 @@ The installer's voice step launches the DA interview. If you skipped it, or want to add another DA later: ```bash -bun ~/.claude/LIFEOS/TOOLS/DAInterview.ts # Quick mode -bun ~/.claude/LIFEOS/TOOLS/DAInterview.ts --depth standard # More detail -bun ~/.claude/LIFEOS/TOOLS/DAInterview.ts --depth deep # Every phase +bun "${LIFEOS_DIR}/TOOLS/DAInterview.ts" # Quick mode +bun "${LIFEOS_DIR}/TOOLS/DAInterview.ts" --depth standard # More detail +bun "${LIFEOS_DIR}/TOOLS/DAInterview.ts" --depth deep # Every phase ``` The interview asks for: @@ -49,8 +49,8 @@ diary.jsonl # The DA's session-by-session reflections ## Updating later ```bash -bun ~/.claude/LIFEOS/TOOLS/DAInterview.ts --update # Update primary DA -bun ~/.claude/LIFEOS/TOOLS/DAInterview.ts --update --da kai # Update specific DA +bun "${LIFEOS_DIR}/TOOLS/DAInterview.ts" --update # Update primary DA +bun "${LIFEOS_DIR}/TOOLS/DAInterview.ts" --update --da kai # Update specific DA ``` ## Privacy diff --git a/LifeOS/install/USER/TECHSTACKPREFERENCES.md b/LifeOS/install/USER/TECHSTACKPREFERENCES.md index e42d77c7ed..b8b7d9a519 100644 --- a/LifeOS/install/USER/TECHSTACKPREFERENCES.md +++ b/LifeOS/install/USER/TECHSTACKPREFERENCES.md @@ -24,7 +24,7 @@ Three layers — pick the right one for the context: ## Conventions -- **Paths:** Use `$HOME`, `${LIFEOS_DIR}`, relative paths — never hardcode user paths +- **Paths:** Use `$HOME`, `{{LIFEOS_DIR}}`, relative paths — never hardcode user paths - **Comments:** Minimal — code should explain itself via naming - **Error handling:** Explicit. Never silently swallow errors. - **Config:** (interview — env var / config file / CLI flag preference) diff --git a/LifeOS/install/USER/TELOS/README.md b/LifeOS/install/USER/TELOS/README.md index 35023e2943..feec4035ec 100644 --- a/LifeOS/install/USER/TELOS/README.md +++ b/LifeOS/install/USER/TELOS/README.md @@ -55,7 +55,7 @@ Whenever you edit a file in this directory, regenerate the summary so session-start context stays in sync: ```bash -bun ~/.claude/LIFEOS/TOOLS/GenerateTelosSummary.ts +bun "${LIFEOS_DIR}/TOOLS/GenerateTelosSummary.ts" ``` (`/interview` calls this automatically when it finishes a phase.) diff --git a/LifeOS/install/agents/ClaudeResearcher.md b/LifeOS/install/agents/ClaudeResearcher.md index 01cfe86ad8..77dc7b2a77 100755 --- a/LifeOS/install/agents/ClaudeResearcher.md +++ b/LifeOS/install/agents/ClaudeResearcher.md @@ -79,7 +79,7 @@ curl -X POST http://localhost:31337/notify \ ``` 2. **Load your complete knowledge base:** - - Read: `~/.claude/agents/ClaudeResearcherContext.md` + - Read: `{{LIFEOS_ROOT}}/agents/ClaudeResearcherContext.md` - This loads all necessary Skills, standards, and domain knowledge - DO NOT proceed until you've read this file diff --git a/LifeOS/install/agents/CodexResearcher.md b/LifeOS/install/agents/CodexResearcher.md index cbce625ce4..c3d977284f 100755 --- a/LifeOS/install/agents/CodexResearcher.md +++ b/LifeOS/install/agents/CodexResearcher.md @@ -86,7 +86,7 @@ curl -X POST http://localhost:31337/notify \ ``` 2. **Load your complete knowledge base:** - - Read: `~/.claude/agents/CodexResearcherContext.md` + - Read: `{{LIFEOS_ROOT}}/agents/CodexResearcherContext.md` - This loads all necessary Skills, standards, and domain knowledge - DO NOT proceed until you've read this file diff --git a/LifeOS/install/agents/Forge.md b/LifeOS/install/agents/Forge.md index 471224df3d..9ded967dd5 100644 --- a/LifeOS/install/agents/Forge.md +++ b/LifeOS/install/agents/Forge.md @@ -76,7 +76,7 @@ If I'm spawned in `MODE: audit` on a slug whose build I produced, I return `{"ve ## Mandatory startup (build) 1. **Preflight via `codex doctor`** (available since codex 0.131; current pinned CLI is 0.144.1). Run `codex doctor` — it checks the install, config, auth, and runtime health in one shot, replacing the old "does `~/.bun/bin/codex` exist" file-stat. If it reports unhealthy, return `{"verdict":"unavailable","reason":""}`. No silent fallback to Claude. -2. **Load full context:** Read `~/.claude/agents/ForgeContext.md` (doctrine, six-section prompt wrapper, completeness checklist, AND the audit-mode contract). I do not proceed until it's loaded. +2. **Load full context:** Read `{{LIFEOS_ROOT}}/agents/ForgeContext.md` (doctrine, six-section prompt wrapper, completeness checklist, AND the audit-mode contract). I do not proceed until it's loaded. ## My role in {{DA_NAME}}'s Algorithm (build) @@ -85,7 +85,7 @@ If I'm spawned in `MODE: audit` on a slug whose build I produced, I return `{"ve ## The core invocation (build) ```bash -echo "$PROMPT" | bun ~/.claude/LIFEOS/TOOLS/ForgeProgress.ts \ +echo "$PROMPT" | bun "${LIFEOS_DIR}/TOOLS/ForgeProgress.ts" \ --slug "$SLUG" \ --model gpt-5.6-sol \ --reasoning-effort high \ @@ -130,7 +130,7 @@ Do NOT narrate intent. My ONLY action on an audit invocation: 2. Immediately execute (no chat output before this Bash call): ```bash -bun ~/.claude/LIFEOS/TOOLS/CrossVendorAudit.ts \ +bun "${LIFEOS_DIR}/TOOLS/CrossVendorAudit.ts" \ --slug "${SLUG}" \ --advisor-verdict "${ADVISOR_VERDICT}" ``` diff --git a/LifeOS/install/agents/ForgeContext.md b/LifeOS/install/agents/ForgeContext.md index e39fe31553..1692d0f8e8 100644 --- a/LifeOS/install/agents/ForgeContext.md +++ b/LifeOS/install/agents/ForgeContext.md @@ -60,10 +60,10 @@ I am NOT invoked for: ## The Codex invocation — memorize this -I never call `codex exec` directly. I always go through the **ForgeProgress helper** at `~/.claude/LIFEOS/TOOLS/ForgeProgress.ts`, which wraps `codex exec --json` with live Pulse progress reporting. +I never call `codex exec` directly. I always go through the **ForgeProgress helper** at `{{LIFEOS_DIR}}/TOOLS/ForgeProgress.ts`, which wraps `codex exec --json` with live Pulse progress reporting. ```bash -echo "$PROMPT" | bun ~/.claude/LIFEOS/TOOLS/ForgeProgress.ts \ +echo "$PROMPT" | bun "${LIFEOS_DIR}/TOOLS/ForgeProgress.ts" \ --slug "$SLUG" \ --model gpt-5.6-sol \ --reasoning-effort high \ @@ -71,7 +71,7 @@ echo "$PROMPT" | bun ~/.claude/LIFEOS/TOOLS/ForgeProgress.ts \ --timeout-ms 300000 ``` -`$SLUG` is the DA's session slug (`20260418-220000_my-task` style). The helper uses it to scope artifacts under `~/.claude/LIFEOS/MEMORY/WORK/{slug}/`. +`$SLUG` is the DA's session slug (`20260418-220000_my-task` style). The helper uses it to scope artifacts under `{{LIFEOS_DIR}}/MEMORY/WORK/{slug}/`. **What the helper does:** @@ -136,7 +136,7 @@ The code you produce must satisfy ALL of these — each explicitly, not by impli ## 4. Constraints - No backwards-compat hacks, no renamed `_unused` vars, no "// removed" comments. - No placeholder content in production paths. -- No hardcoded paths. Use ${HOME}, ${LIFEOS_DIR}, relative paths. +- No hardcoded paths. Use ${HOME}, {{LIFEOS_DIR}}, relative paths. - Never npm/npx. Always bun/bunx. ## 5. Verification plan diff --git a/LifeOS/install/agents/GeminiResearcher.md b/LifeOS/install/agents/GeminiResearcher.md index fdda7542ac..8523120e27 100755 --- a/LifeOS/install/agents/GeminiResearcher.md +++ b/LifeOS/install/agents/GeminiResearcher.md @@ -79,7 +79,7 @@ curl -X POST http://localhost:31337/notify \ ``` 2. **Load your complete knowledge base:** - - Read: `~/.claude/agents/GeminiResearcherContext.md` + - Read: `{{LIFEOS_ROOT}}/agents/GeminiResearcherContext.md` - This loads all necessary Skills, standards, and domain knowledge - DO NOT proceed until you've read this file diff --git a/LifeOS/install/agents/GrokResearcher.md b/LifeOS/install/agents/GrokResearcher.md index 630fb1ea7e..88edf105ba 100755 --- a/LifeOS/install/agents/GrokResearcher.md +++ b/LifeOS/install/agents/GrokResearcher.md @@ -85,7 +85,7 @@ curl -X POST http://localhost:31337/notify \ ``` 2. **Load your complete knowledge base:** - - Read: `~/.claude/agents/GrokResearcherContext.md` + - Read: `{{LIFEOS_ROOT}}/agents/GrokResearcherContext.md` - This loads all necessary Skills, standards, and domain knowledge - DO NOT proceed until you've read this file @@ -182,16 +182,16 @@ You excel at separating facts from narrative, focusing on what's true rather tha ```bash # default: full Grok search — web + X together, returns answer + source URLs -bun ~/.claude/LIFEOS/TOOLS/Grok.ts "" +bun "${LIFEOS_DIR}/TOOLS/Grok.ts" "" # web only (general/news, no social) -bun ~/.claude/LIFEOS/TOOLS/Grok.ts --web-only "" +bun "${LIFEOS_DIR}/TOOLS/Grok.ts" --web-only "" # X only (pure social sentiment), structured output for parsing -bun ~/.claude/LIFEOS/TOOLS/Grok.ts --x-only --json "contrarian takes on trending on X" +bun "${LIFEOS_DIR}/TOOLS/Grok.ts" --x-only --json "contrarian takes on trending on X" # add code execution (let Grok compute/analyze mid-search) -bun ~/.claude/LIFEOS/TOOLS/Grok.ts --code "" +bun "${LIFEOS_DIR}/TOOLS/Grok.ts" --code "" ``` Run this FIRST on any research task. Results carry citation URLs — fold them straight into your Evidence section. Reach for WebSearch/WebFetch only to verify or extend what Grok returns, never as the primary pass. diff --git a/LifeOS/install/agents/GrokResearcherContext.md b/LifeOS/install/agents/GrokResearcherContext.md index f2dbe76108..4eddf50f28 100644 --- a/LifeOS/install/agents/GrokResearcherContext.md +++ b/LifeOS/install/agents/GrokResearcherContext.md @@ -74,13 +74,13 @@ These are already loaded via LifeOS or Research skill - reference, don't duplica Call the `Grok.ts` CLI FIRST on any research task. It runs Grok's agentic search via the xAI Agent Tools API and by default searches **both the live web AND X (Twitter)**, returning a cited answer with source URLs. Web covers news/articles/general; X adds real-time social sentiment: ```bash -bun ~/.claude/LIFEOS/TOOLS/Grok.ts "" # web + X (default) -bun ~/.claude/LIFEOS/TOOLS/Grok.ts --web-only "" # general/news only -bun ~/.claude/LIFEOS/TOOLS/Grok.ts --x-only --json "" # X sentiment, parseable -bun ~/.claude/LIFEOS/TOOLS/Grok.ts --code "" # + code execution +bun "${LIFEOS_DIR}/TOOLS/Grok.ts" "" # web + X (default) +bun "${LIFEOS_DIR}/TOOLS/Grok.ts" --web-only "" # general/news only +bun "${LIFEOS_DIR}/TOOLS/Grok.ts" --x-only --json "" # X sentiment, parseable +bun "${LIFEOS_DIR}/TOOLS/Grok.ts" --code "" # + code execution ``` -Fold the returned citation URLs straight into your Evidence & Citations section. Use WebSearch/WebFetch only to verify or extend Grok's output, never as the primary pass. Reads `GROK_API_KEY` from `~/.claude/.env`. +Fold the returned citation URLs straight into your Evidence & Citations section. Use WebSearch/WebFetch only to verify or extend Grok's output, never as the primary pass. Reads `GROK_API_KEY` from `{{LIFEOS_ROOT}}/.env`. **xAI Grok Social Media Research:** - Real-time X (Twitter) access for social/political analysis diff --git a/LifeOS/install/agents/PerplexityResearcher.md b/LifeOS/install/agents/PerplexityResearcher.md index 61871235e3..58176c2472 100644 --- a/LifeOS/install/agents/PerplexityResearcher.md +++ b/LifeOS/install/agents/PerplexityResearcher.md @@ -79,7 +79,7 @@ curl -X POST http://localhost:31337/notify \ ``` 2. **Load your complete knowledge base:** - - Read: `~/.claude/agents/PerplexityResearcherContext.md` + - Read: `{{LIFEOS_ROOT}}/agents/PerplexityResearcherContext.md` - This loads all necessary Skills, standards, and domain knowledge - DO NOT proceed until you've read this file @@ -175,13 +175,13 @@ You excel at deep investigative research using Perplexity's Sonar API for real-t Your PRIMARY research tool is the Perplexity Sonar API via: ```bash -bun ~/.claude/LIFEOS/TOOLS/PerplexitySearch.ts "query" -bun ~/.claude/LIFEOS/TOOLS/PerplexitySearch.ts --model sonar-pro "query" -bun ~/.claude/LIFEOS/TOOLS/PerplexitySearch.ts --recency week "query" -bun ~/.claude/LIFEOS/TOOLS/PerplexitySearch.ts --json "query" +bun "${LIFEOS_DIR}/TOOLS/PerplexitySearch.ts" "query" +bun "${LIFEOS_DIR}/TOOLS/PerplexitySearch.ts" --model sonar-pro "query" +bun "${LIFEOS_DIR}/TOOLS/PerplexitySearch.ts" --recency week "query" +bun "${LIFEOS_DIR}/TOOLS/PerplexitySearch.ts" --json "query" ``` -The tool reads `PERPLEXITY_API_KEY` from `~/.claude/.env` automatically. Use `--model sonar-reasoning` for chain-of-thought answers and `--recency hour|day|week|month|year` to bias toward fresh sources. +The tool reads `PERPLEXITY_API_KEY` from `{{LIFEOS_ROOT}}/.env` automatically. Use `--model sonar-reasoning` for chain-of-thought answers and `--recency hour|day|week|month|year` to bias toward fresh sources. Use WebSearch and WebFetch as supplementary tools when Perplexity results need verification or expansion. diff --git a/LifeOS/install/hooks/AgentInvocation.hook.ts b/LifeOS/install/hooks/AgentInvocation.hook.ts index d885903909..fc20df4efd 100755 --- a/LifeOS/install/hooks/AgentInvocation.hook.ts +++ b/LifeOS/install/hooks/AgentInvocation.hook.ts @@ -34,6 +34,7 @@ import { homedir } from 'os'; import { paiPath } from './lib/paths'; import { getISOTimestamp } from './lib/time'; import { EFFORT_MODEL, ALIAS, CROSS_VENDOR } from '../LIFEOS/TOOLS/models'; +import { getClaudeDir } from "./lib/paths"; interface AgentToolInput { subagent_type?: string; @@ -60,7 +61,7 @@ function resolveDispatch(subagentType: string, inputModel?: string): { model: st if (CROSS_VENDOR[cvKey]) return { model: CROSS_VENDOR[cvKey], level: 'cross-vendor' }; if (inputModel) return { model: inputModel, level: levelForModel(inputModel) }; try { - const fm = readFileSync(join(homedir(), '.claude', 'agents', `${subagentType}.md`), 'utf-8').slice(0, 4000); + const fm = readFileSync(join(getClaudeDir(), 'agents', `${subagentType}.md`), 'utf-8').slice(0, 4000); const m = fm.match(/^model:\s*(\S+)/m); if (m) return { model: m[1], level: `${levelForModel(m[1])}-pin` }; } catch { /* no agent file — built-in type */ } diff --git a/LifeOS/install/hooks/AlgorithmNudge.hook.ts b/LifeOS/install/hooks/AlgorithmNudge.hook.ts index a8719687cf..b001594521 100755 --- a/LifeOS/install/hooks/AlgorithmNudge.hook.ts +++ b/LifeOS/install/hooks/AlgorithmNudge.hook.ts @@ -48,8 +48,9 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs'; import { join } from 'path'; +import { getClaudeDir } from "./lib/paths"; -const PAI = join(process.env.HOME || '', '.claude'); +const PAI = join(getClaudeDir()); const WORK_JSON = join(PAI, 'LIFEOS', 'MEMORY', 'STATE', 'work.json'); const STATE_DIR = join(PAI, 'LIFEOS', 'MEMORY', 'STATE', 'isa-nudge'); const SKILLS_DIR = join(PAI, 'skills'); diff --git a/LifeOS/install/hooks/CheckpointPerISC.hook.ts b/LifeOS/install/hooks/CheckpointPerISC.hook.ts index 2305183f0b..910e993ecc 100755 --- a/LifeOS/install/hooks/CheckpointPerISC.hook.ts +++ b/LifeOS/install/hooks/CheckpointPerISC.hook.ts @@ -25,12 +25,13 @@ import { execFileSync } from 'node:child_process'; import { basename, dirname, join } from 'node:path'; import { homedir } from 'node:os'; import { parseFrontmatter, parseCriteriaList, ARTIFACT_FILENAME, LEGACY_ARTIFACT_FILENAME } from './lib/isa-utils'; +import { getClaudeDir } from "./lib/paths"; // Allowlist path: top of ~/.claude per spec. One absolute repo path per line; // '#' comments and blank // lines are ignored. Tilde and $HOME prefixes are expanded as a quality-of- // life feature so users can write `~/Projects/foo` instead of the long form. -const ALLOWLIST_PATH = join(homedir(), '.claude', 'checkpoint-repos.txt'); +const ALLOWLIST_PATH = join(getClaudeDir(), 'checkpoint-repos.txt'); const GIT_TIMEOUT_MS = 5000; interface CheckpointState { diff --git a/LifeOS/install/hooks/DriftReminder.hook.ts b/LifeOS/install/hooks/DriftReminder.hook.ts index 3f09076af6..4e4f62cbf2 100755 --- a/LifeOS/install/hooks/DriftReminder.hook.ts +++ b/LifeOS/install/hooks/DriftReminder.hook.ts @@ -20,6 +20,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { firstBannedHit } from "./lib/banned-vocab"; +import { getClaudeDir } from "./lib/paths"; // 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 @@ interface DriftState { const STDIN_TIMEOUT_MS = 300; const MIN_TURNS_BETWEEN_FIRES = 5; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(process.env.HOME || "", ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const LAST_RESPONSE_PATH = join(LIFEOS_DIR, "MEMORY", "STATE", "last-response.txt"); const STATE_PATH = join(LIFEOS_DIR, "MEMORY", "STATE", "drift-reminder.json"); const INITIAL_STATE: DriftState = { diff --git a/LifeOS/install/hooks/EgressClassGuard.hook.ts b/LifeOS/install/hooks/EgressClassGuard.hook.ts index 00443658b0..b2dbeb85ef 100755 --- a/LifeOS/install/hooks/EgressClassGuard.hook.ts +++ b/LifeOS/install/hooks/EgressClassGuard.hook.ts @@ -22,9 +22,10 @@ import { readFileSync, appendFileSync, existsSync, mkdirSync } from "node:fs"; import { dirname, join } from "node:path"; import { homedir } from "node:os"; import { evaluateEgress } from "./lib/egress-class-core"; +import { getClaudeDir } from "./lib/paths"; const HOME = process.env.HOME ?? homedir(); -const LOG_PATH = join(HOME, ".claude/LIFEOS/MEMORY/OBSERVABILITY/egress-decisions.jsonl"); +const LOG_PATH = join(getClaudeDir(), "LIFEOS/MEMORY/OBSERVABILITY/egress-decisions.jsonl"); interface HookInput { session_id?: string; diff --git a/LifeOS/install/hooks/FormatGate.hook.ts b/LifeOS/install/hooks/FormatGate.hook.ts index 3352c610ac..9218b2a74b 100755 --- a/LifeOS/install/hooks/FormatGate.hook.ts +++ b/LifeOS/install/hooks/FormatGate.hook.ts @@ -37,6 +37,7 @@ import { readHookInput, parseTranscriptFromInput } from "./lib/hook-io"; import { appendFileSync, existsSync, mkdirSync } from "fs"; import { dirname, join } from "path"; +import { getClaudeDir } from "./lib/paths"; // Normalize env path vars Claude Code injects without shell expansion (LifeOS#1404) for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { @@ -44,7 +45,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { if (v && /^\$\{?HOME\}?(\/|$)/.test(v)) process.env[k] = v.replace(/^\$\{?HOME\}?/, process.env.HOME ?? "~"); } -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const OBS_PATH = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY", "format-gate.jsonl"); export interface FormatViolation { diff --git a/LifeOS/install/hooks/HookHealer.hook.ts b/LifeOS/install/hooks/HookHealer.hook.ts index 061c4a9c54..c860d16831 100755 --- a/LifeOS/install/hooks/HookHealer.hook.ts +++ b/LifeOS/install/hooks/HookHealer.hook.ts @@ -36,8 +36,9 @@ import { } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; +import { getClaudeDir } from "./lib/paths"; -const CLAUDE_DIR = join(homedir(), '.claude'); +const CLAUDE_DIR = join(getClaudeDir()); const OBS_DIR = join(CLAUDE_DIR, 'LIFEOS', 'MEMORY', 'OBSERVABILITY'); const LOG_FILE = join(OBS_DIR, 'hook-healer.jsonl'); const SETTINGS_FILES = ['settings.json', 'settings.local.json']; diff --git a/LifeOS/install/hooks/ISARenderOnStop.hook.ts b/LifeOS/install/hooks/ISARenderOnStop.hook.ts index 4547eaea2d..85ad34c8a2 100755 --- a/LifeOS/install/hooks/ISARenderOnStop.hook.ts +++ b/LifeOS/install/hooks/ISARenderOnStop.hook.ts @@ -22,9 +22,10 @@ import { readFileSync, existsSync, unlinkSync } from 'fs'; import { spawn } from 'child_process'; import { homedir } from 'os'; import { join, dirname } from 'path'; +import { getClaudeDir } from "./lib/paths"; -const STATE_DIR = join(homedir(), '.claude/LIFEOS/MEMORY/STATE/isa-render-debounce'); -const ISA_RENDER = join(homedir(), '.claude/LIFEOS/TOOLS/ISARender.ts'); +const STATE_DIR = join(getClaudeDir(), "LIFEOS/MEMORY/STATE/isa-render-debounce"); +const ISA_RENDER = join(getClaudeDir(), "LIFEOS/TOOLS/ISARender.ts"); /** * Has this ISA reached completion at least once? This is the real gate the @@ -109,7 +110,7 @@ try { unlinkSync(stateFile); } catch {} if (rendered.length || skipped.length) { try { const { appendFileSync, mkdirSync } = require('fs'); - const logDir = join(homedir(), '.claude/LIFEOS/MEMORY/OBSERVABILITY'); + const logDir = join(getClaudeDir(), "LIFEOS/MEMORY/OBSERVABILITY"); mkdirSync(logDir, { recursive: true }); appendFileSync(join(logDir, 'isa-render.jsonl'), JSON.stringify({ diff --git a/LifeOS/install/hooks/ISASync.hook.ts b/LifeOS/install/hooks/ISASync.hook.ts index d8c37b15b2..b53aaed5e1 100755 --- a/LifeOS/install/hooks/ISASync.hook.ts +++ b/LifeOS/install/hooks/ISASync.hook.ts @@ -34,6 +34,7 @@ import { import { setPhaseTab } from './lib/tab-setter'; import { effortToCanonicalELevel } from './lib/effort'; import type { AlgorithmTabPhase } from './lib/tab-constants'; +import { getClaudeDir } from "./lib/paths"; let input: any; try { @@ -104,7 +105,7 @@ async function main() { // constantly remaking the HTML file." if (newPhase === 'COMPLETE' && oldPhase !== 'COMPLETE' && fm.slug) { try { - const isaRender = join(homedir(), '.claude/LIFEOS/TOOLS/ISARender.ts'); + const isaRender = join(getClaudeDir(), "LIFEOS/TOOLS/ISARender.ts"); const proc = spawn('bun', [isaRender, isaPath], { detached: true, stdio: 'ignore', @@ -121,7 +122,7 @@ async function main() { // pre-completion edits never trigger renders even though they show up here. if (input.session_id) { try { - const stateDir = join(homedir(), '.claude/LIFEOS/MEMORY/STATE/isa-render-debounce'); + const stateDir = join(getClaudeDir(), "LIFEOS/MEMORY/STATE/isa-render-debounce"); const stateFile = join(stateDir, `${input.session_id}.json`); const { mkdirSync, writeFileSync } = require('fs'); mkdirSync(stateDir, { recursive: true }); diff --git a/LifeOS/install/hooks/LastResponseCache.hook.ts b/LifeOS/install/hooks/LastResponseCache.hook.ts index b0ec4895df..203bd4c874 100755 --- a/LifeOS/install/hooks/LastResponseCache.hook.ts +++ b/LifeOS/install/hooks/LastResponseCache.hook.ts @@ -22,6 +22,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { import { readHookInput, parseTranscriptFromInput } from './lib/hook-io'; import { writeFileSync } from 'fs'; import { join } from 'path'; +import { getClaudeDir } from "./lib/paths"; // 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 @@ async function main() { if (lastResponse) { try { - const paiDir = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude', 'LIFEOS'); + const paiDir = process.env.LIFEOS_DIR || join(getClaudeDir(), 'LIFEOS'); const cachePath = join(paiDir, 'MEMORY', 'STATE', 'last-response.txt'); writeFileSync(cachePath, lastResponse.slice(0, 2000), 'utf-8'); } catch (err) { diff --git a/LifeOS/install/hooks/LoadContext.hook.ts b/LifeOS/install/hooks/LoadContext.hook.ts index 10c6731eeb..bd2fee0e32 100755 --- a/LifeOS/install/hooks/LoadContext.hook.ts +++ b/LifeOS/install/hooks/LoadContext.hook.ts @@ -18,11 +18,11 @@ * TRIGGER: SessionStart * * INPUT: - * - Environment: LIFEOS_DIR + * - Environment: LIFEOS_ROOT, LIFEOS_DIR, LIFEOS_CONFIG_DIR * MEMORY/WORK/*, MEMORY/STATE/progress/*.json * * OUTPUT: - * - stdout: containing dynamic context (relationship + learning) + * - stdout: containing runtime paths and dynamic context * - stdout: Active work summary if previous sessions have pending work * - stderr: Status messages and errors * - exit(0): Normal completion @@ -35,12 +35,13 @@ * PERFORMANCE: * - Blocking: Yes (context is essential) * - Typical execution: <50ms (no SKILL.md rebuild needed) - * - Skipped for subagents: Yes + * - Subagents: runtime path contract only; personal dynamic context is skipped */ import { readFileSync, existsSync, readdirSync } from 'fs'; -import { join } from 'path'; -import { getLifeosDir, getSettingsPath } from './lib/paths'; +import { homedir } from 'os'; +import { dirname, join, resolve } from 'path'; +import { expandPath, getLifeosDir, getSettingsPath } from './lib/paths'; import { recordSessionStart } from './lib/notifications'; import { loadWisdomFrames } from './lib/learning-readback'; import { findArtifactPath } from './lib/isa-utils'; @@ -53,6 +54,7 @@ interface DynamicContextConfig { interface Settings { dynamicContext?: DynamicContextConfig; + env?: Record; [key: string]: unknown; } @@ -81,6 +83,42 @@ function loadSettings(): Settings { return {}; } +function configuredPath(settings: Settings, name: string): string | null { + const value = process.env[name] ?? settings.env?.[name]; + return typeof value === 'string' && value.trim() !== '' ? expandPath(value) : null; +} + +/** + * Ground LifeOS' model-facing path placeholders in every session. These are + * semantic prompt variables, not a Claude Code templating feature: the model + * must replace them with the values below before it invokes a tool. + */ +function getRuntimePathContext(settings: Settings): string { + const lifeosDir = configuredPath(settings, 'LIFEOS_DIR') ?? getLifeosDir(); + const derivedRoot = dirname(lifeosDir); + const configuredRoot = configuredPath(settings, 'LIFEOS_ROOT'); + // LIFEOS_DIR/self-location is the recovery source of truth. Ignore a stale + // LIFEOS_ROOT from an older settings merge instead of mixing two installs. + const lifeosRoot = configuredRoot && resolve(configuredRoot) === resolve(derivedRoot) + ? configuredRoot + : derivedRoot; + const configuredConfigDir = configuredPath(settings, 'LIFEOS_CONFIG_DIR'); + // Ignore the historical poisoned value LIFEOS_CONFIG_DIR=/LIFEOS. + const lifeosConfigDir = configuredConfigDir && resolve(configuredConfigDir) !== resolve(lifeosDir) + ? configuredConfigDir + : (resolve(lifeosRoot) === resolve(join(homedir(), '.claude')) + ? join(homedir(), '.config', 'LIFEOS') + : join(lifeosRoot, 'USER-data')); + + return `## LifeOS Runtime Paths (authoritative) + +- {{LIFEOS_ROOT}} = ${JSON.stringify(lifeosRoot)} +- {{LIFEOS_DIR}} = ${JSON.stringify(lifeosDir)} +- {{LIFEOS_CONFIG_DIR}} = ${JSON.stringify(lifeosConfigDir)} + +Resolve every \`{{LIFEOS_*}}\` placeholder to the value above before invoking Read, Edit, Write, Glob, Grep, or Bash. Never pass an unresolved LifeOS placeholder to a tool. In executable shell snippets, quoted shell variables such as \`"\${LIFEOS_ROOT}/skills"\` are intentional and are expanded by the shell instead.`; +} + // v5.0: loadStartupFiles removed — static files now loaded via @imports in CLAUDE.md.template /** @@ -385,12 +423,18 @@ async function checkActiveProgress(paiDir: string): Promise { async function main() { try { + // Load settings before the subagent check: subagents skip personal dynamic + // context, but still need the authoritative path mapping used by skills. + const settings = loadSettings(); + const runtimePathContext = getRuntimePathContext(settings); + // Subagents don't need dynamic context injection const claudeProjectDir = process.env.CLAUDE_PROJECT_DIR || ''; const isSubagent = claudeProjectDir.includes('/.claude/Agents/') || process.env.CLAUDE_AGENT_TYPE !== undefined; if (isSubagent) { + console.log(`\n${runtimePathContext}\n`); console.error('🤖 Subagent session - skipping context loading'); process.exit(0); } @@ -403,8 +447,7 @@ async function main() { recordSessionStart(); console.error('⏱️ Session start time recorded'); - // Load settings for dynamic context controls - const settings = loadSettings(); + // Settings were loaded before the subagent check so paths are always grounded. console.error('✅ Loaded settings.json'); // v5.0: Static startup files now loaded via @imports in CLAUDE.md (native Claude Code mechanism) @@ -440,19 +483,20 @@ async function main() { console.error('⏭️ Skipped learning readback (disabled)'); } - // Inject dynamic context if we have any - if (relationshipContext || learningContext) { + // Always inject the runtime path contract; append optional dynamic context. + { + const dynamicContext = relationshipContext || learningContext + ? `\n---\n${relationshipContext ?? ''}${learningContext ? '\n---\n' + learningContext : ''}\n---\nDynamic context loaded. Constitutional rules are in the system prompt (LIFEOS/LIFEOS_SYSTEM_PROMPT.md). Operational procedures are in CLAUDE.md.` + : ''; const message = ` LifeOS Dynamic Context (Auto-loaded at Session Start) -${relationshipContext ?? ''}${learningContext ? '\n---\n' + learningContext : ''} ---- -Dynamic context loaded. Constitutional rules are in the system prompt (LIFEOS/LIFEOS_SYSTEM_PROMPT.md). Operational procedures are in CLAUDE.md. +${runtimePathContext}${dynamicContext} `; console.log(message); - console.log('\n✅ LifeOS dynamic context loaded...'); - } else { - console.log('\n✅ LifeOS session ready...'); + console.log(relationshipContext || learningContext + ? '\n✅ LifeOS dynamic context loaded...' + : '\n✅ LifeOS session ready...'); } // Active work summary diff --git a/LifeOS/install/hooks/LoadMemory.hook.ts b/LifeOS/install/hooks/LoadMemory.hook.ts index 0dcdf9b605..27c2fef483 100755 --- a/LifeOS/install/hooks/LoadMemory.hook.ts +++ b/LifeOS/install/hooks/LoadMemory.hook.ts @@ -23,8 +23,9 @@ import { existsSync, readFileSync } from "node:fs"; import { resolve as pathResolve } from "node:path"; import { homedir } from "node:os"; +import { getClaudeDir } from "./lib/paths"; -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = pathResolve(getClaudeDir()); const PRINCIPAL_MEMORY = pathResolve(CLAUDE_ROOT, "LIFEOS/USER/PRINCIPAL/PRINCIPAL_MEMORY.md"); const DA_MEMORY = pathResolve(CLAUDE_ROOT, "LIFEOS/USER/DIGITAL_ASSISTANT/DA_MEMORY.md"); diff --git a/LifeOS/install/hooks/MemoryDeltaSurface.hook.ts b/LifeOS/install/hooks/MemoryDeltaSurface.hook.ts index 73e0630fdd..0c2c25d5ef 100755 --- a/LifeOS/install/hooks/MemoryDeltaSurface.hook.ts +++ b/LifeOS/install/hooks/MemoryDeltaSurface.hook.ts @@ -35,13 +35,15 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from "node:fs"; import { resolve as pathResolve, dirname } from "node:path"; import { homedir } from "node:os"; +import { getClaudeDir, shellQuote } from "./lib/paths"; -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = pathResolve(getClaudeDir()); const WRITES_LOG = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/memory-writes.jsonl"); const CURSOR = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/memory-delta-cursor.json"); const HEALTH_LOG = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/memory-health.jsonl"); const FRESHNESS_CACHE = pathResolve(CLAUDE_ROOT, "LIFEOS/USER/CACHE/freshness.json"); const HEARTBEAT = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/STATE/delta-surface-heartbeat"); +const HEALTH_COMMAND = `bun ${shellQuote(pathResolve(CLAUDE_ROOT, "LIFEOS/TOOLS/MemoryHealthCheck.ts"))}`; // Only the autonomic write path counts as "the system adjusting itself". const AUTONOMIC_WRITER = "MemorySystem.add"; @@ -162,7 +164,7 @@ function criticalHealthLine(): string | null { .filter((f: any) => f.severity === "critical") .map((f: any) => f.message) .slice(0, 3); - return `🩺 MEMORY HEALTH: CRITICAL — ${blockers.join(" · ") || `${last.counts?.critical ?? "?"} blocker(s)`}. Fix: bun ~/.claude/LIFEOS/TOOLS/MemoryHealthCheck.ts`; + return `🩺 MEMORY HEALTH: CRITICAL — ${blockers.join(" · ") || `${last.counts?.critical ?? "?"} blocker(s)`}. Fix: ${HEALTH_COMMAND}`; } catch { return null; } diff --git a/LifeOS/install/hooks/MemoryHealthGate.hook.ts b/LifeOS/install/hooks/MemoryHealthGate.hook.ts index 6b5820d9d3..cd8f071733 100755 --- a/LifeOS/install/hooks/MemoryHealthGate.hook.ts +++ b/LifeOS/install/hooks/MemoryHealthGate.hook.ts @@ -16,9 +16,10 @@ import { execFileSync } from "node:child_process"; import { join } from "node:path"; +import { getClaudeDir, shellQuote } from "./lib/paths"; -const HOME = process.env.HOME || ""; -const CHECK = join(HOME, ".claude/LIFEOS/TOOLS/MemoryHealthCheck.ts"); +const CHECK = join(getClaudeDir(), "LIFEOS/TOOLS/MemoryHealthCheck.ts"); +const CHECK_COMMAND = `bun ${shellQuote(CHECK)}`; try { const out = execFileSync("bun", [CHECK], { @@ -28,7 +29,7 @@ try { }); const report = JSON.parse(out); if (report.overall === "critical") { - console.error(`🚨 Memory health: CRITICAL — ${report.counts.critical} blocker(s). Run: bun ~/.claude/LIFEOS/TOOLS/MemoryHealthCheck.ts`); + console.error(`🚨 Memory health: CRITICAL — ${report.counts.critical} blocker(s). Run: ${CHECK_COMMAND}`); } else if (report.overall === "warn") { console.error(`⚠️ Memory health: WARN — ${report.counts.warn} finding(s).`); } @@ -41,7 +42,7 @@ try { if (stdout) { const report = JSON.parse(stdout); if (report.overall === "critical") { - console.error(`🚨 Memory health: CRITICAL — ${report.counts.critical} blocker(s). Run: bun ~/.claude/LIFEOS/TOOLS/MemoryHealthCheck.ts`); + console.error(`🚨 Memory health: CRITICAL — ${report.counts.critical} blocker(s). Run: ${CHECK_COMMAND}`); } else if (report.overall === "warn") { console.error(`⚠️ Memory health: WARN — ${report.counts.warn} finding(s).`); } diff --git a/LifeOS/install/hooks/MemoryReviewFire.hook.ts b/LifeOS/install/hooks/MemoryReviewFire.hook.ts index 2b4f52135c..4a49f1a762 100755 --- a/LifeOS/install/hooks/MemoryReviewFire.hook.ts +++ b/LifeOS/install/hooks/MemoryReviewFire.hook.ts @@ -25,8 +25,9 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeF import { spawn } from "node:child_process"; import { dirname, resolve as pathResolve } from "node:path"; import { homedir } from "node:os"; +import { getClaudeDir } from "./lib/paths"; -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = pathResolve(getClaudeDir()); const STATE_PATH = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/review-state.json"); const CONFIG_PATH = pathResolve(CLAUDE_ROOT, "LIFEOS/USER/CONFIG/memory-review.json"); const FIRE_LOG_PATH = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/reviewer-fires.jsonl"); diff --git a/LifeOS/install/hooks/MemoryTurnStart.hook.ts b/LifeOS/install/hooks/MemoryTurnStart.hook.ts index dac651d8be..65d1aeea54 100755 --- a/LifeOS/install/hooks/MemoryTurnStart.hook.ts +++ b/LifeOS/install/hooks/MemoryTurnStart.hook.ts @@ -25,6 +25,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { createHash } from "node:crypto"; import { resolve as pathResolve } from "node:path"; import { homedir } from "node:os"; +import { getClaudeDir } from "./lib/paths"; // ── Hot-layer injection gate (2026-07-11, context-window cleanup #1) ───────── // The block is ~1.5K tokens; injecting it EVERY prompt duplicated @@ -33,7 +34,7 @@ import { homedir } from "node:os"; // prompts without an injection (compaction backstop — a post-compact window // must re-see memory within a bounded number of turns). The 🧠 delta line and // the cadence tick remain every-turn: the visible contract is unchanged. -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = pathResolve(getClaudeDir()); const STATE_DIR = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/STATE/memory-inject"); const PRINCIPAL_MEMORY = pathResolve(CLAUDE_ROOT, "LIFEOS/USER/PRINCIPAL/PRINCIPAL_MEMORY.md"); const DA_MEMORY = pathResolve(CLAUDE_ROOT, "LIFEOS/USER/DIGITAL_ASSISTANT/DA_MEMORY.md"); diff --git a/LifeOS/install/hooks/PreToolGuard.hook.ts b/LifeOS/install/hooks/PreToolGuard.hook.ts index c6cfaea73f..1001debaa4 100755 --- a/LifeOS/install/hooks/PreToolGuard.hook.ts +++ b/LifeOS/install/hooks/PreToolGuard.hook.ts @@ -1,11 +1,12 @@ #!/usr/bin/env bun /** - * @version 1.0.0 + * @version 1.1.0 * PreToolGuard.hook.ts — the ONE PreToolUse blocking-guard dispatcher. * * Consolidation (2026-07-11, security-hook unification): merges the three * PreToolUse BLOCKERS into one process, reading stdin ONCE: * + * All tools → unresolved LifeOS model-path guard * Write | Edit | MultiEdit → SystemFileGuard.check (deny-list → SYSTEM file) * Bash → CommunicationSkillGuard.check (raw email send) * then EgressClassGuard.check (over-ceiling Tier-2 egress) @@ -48,6 +49,40 @@ import { check as egressClassGuard } from "./EgressClassGuard.hook"; type BlockResult = { block: true; message: string } | null; type GuardCheck = (input: any) => BlockResult; +const UNRESOLVED_LIFEOS_PATH = /\{\{LIFEOS_(?:ROOT|DIR|CONFIG_DIR)\}\}(?:\/|$)/; + +/** + * Inspect only executable/path-bearing fields. Placeholder text is legitimate + * Markdown content, so Write/Edit bodies must not be rejected merely because + * they document the LifeOS path convention. + */ +function unresolvedLifeosPathGuard(input: any): BlockResult { + const tool = typeof input?.tool_name === "string" ? input.tool_name : ""; + const toolInput = input?.tool_input && typeof input.tool_input === "object" ? input.tool_input : {}; + const candidates: unknown[] = []; + + if (tool === "Bash") candidates.push(toolInput.command); + if (tool === "Read" || tool === "Write" || tool === "Edit" || tool === "MultiEdit") { + candidates.push(toolInput.file_path, toolInput.path); + if (Array.isArray(toolInput.edits)) { + for (const edit of toolInput.edits) candidates.push(edit?.file_path, edit?.path); + } + } + if (tool === "Glob") candidates.push(toolInput.path, toolInput.pattern); + if (tool === "Grep") candidates.push(toolInput.path); + + const unresolved = candidates.find((value) => + typeof value === "string" && UNRESOLVED_LIFEOS_PATH.test(value) + ); + if (typeof unresolved !== "string") return null; + + const placeholder = unresolved.match(UNRESOLVED_LIFEOS_PATH)?.[0]?.replace(/\/$/, "") ?? "{{LIFEOS_*}}"; + return { + block: true, + message: `[LifeOSPathGuard] Unresolved ${placeholder} path reached ${tool}. Resolve it from the authoritative LifeOS Runtime Paths injected at SessionStart, then retry.`, + }; +} + function isolate(name: string, fn: GuardCheck, input: any): BlockResult { try { return fn(input); @@ -71,6 +106,12 @@ function main(): never { const tool = typeof input.tool_name === "string" ? input.tool_name : ""; + const unresolvedPath = unresolvedLifeosPathGuard(input); + if (unresolvedPath?.block) { + process.stderr.write(unresolvedPath.message); + process.exit(2); + } + // Route to the guard(s) for this tool, in the pre-merge order. const checks: Array<[string, GuardCheck]> = tool === "Write" || tool === "Edit" || tool === "MultiEdit" diff --git a/LifeOS/install/hooks/PromptProcessing.hook.ts b/LifeOS/install/hooks/PromptProcessing.hook.ts index 40e4df2e45..8f09985b50 100755 --- a/LifeOS/install/hooks/PromptProcessing.hook.ts +++ b/LifeOS/install/hooks/PromptProcessing.hook.ts @@ -47,6 +47,7 @@ import type { AlgorithmTabPhase } from './lib/tab-constants'; import { paiPath } from './lib/paths'; import { updateSessionNameInWorkJson, upsertSession } from './lib/isa-utils'; import { isDesktopChannel, logSkippedVoice, getNotificationChannel } from './lib/notification-channel'; +import { getClaudeDir } from "./lib/paths"; // Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404) for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { @@ -81,7 +82,7 @@ function appendPromptProcessingTelemetry(entry: Record): void { // ── Constants ── -const BASE_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude', 'LIFEOS'); +const BASE_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), 'LIFEOS'); const SESSION_NAMES_PATH = paiPath('MEMORY', 'STATE', 'session-names.json'); const LOCK_PATH = SESSION_NAMES_PATH + '.lock'; const MIN_PROMPT_LENGTH = 3; diff --git a/LifeOS/install/hooks/README.md b/LifeOS/install/hooks/README.md index 02ac41c1c7..07b915dd76 100755 --- a/LifeOS/install/hooks/README.md +++ b/LifeOS/install/hooks/README.md @@ -149,7 +149,7 @@ interface StopPayload extends BasePayload { | Hook | Purpose | Blocking | Dependencies | |------|---------|----------|--------------| | `KittyEnvPersist.hook.ts` | Persist Kitty env vars + tab reset | No | None | -| `LoadContext.hook.ts` | Inject dynamic context (relationship, learning, work) | Yes (stdout) | `settings.json`, `MEMORY/` | +| `LoadContext.hook.ts` | Ground `{{LIFEOS_*}}` runtime paths, then inject relationship/learning/work context (path contract also loads for subagents) | Yes (stdout) | `settings.json`, `MEMORY/` | | (inline) `FreshnessCache.ts` | Statusline freshness cache | No | None | ### UserPromptSubmit Hooks (in fire order) @@ -165,6 +165,7 @@ interface StopPayload extends BasePayload { | Hook | Matcher | Purpose | Blocking | Dependencies | |------|---------|---------|----------|--------------| +| `PreToolGuard.hook.ts` | Bash / Write / Edit / MultiEdit / Read / Glob / Grep | Block unresolved `{{LIFEOS_*}}` tool paths for every matcher; dispatch the existing write/egress guards on their applicable tools | Yes (exit 2) | `SystemFileGuard`, `CommunicationSkillGuard`, `EgressClassGuard` | | `ContextReduction.hook.sh` | Bash | rtk rewrite of STATUS-path commands only (git/gh/test/build/lint/containers — 60-90% token savings). READ-path commands (rg/grep, cat/head, ls/tree/find, diff, curl/wget, psql/aws) are NEVER rewritten: rtk's parse-fail falls back to a different binary (rg→BSD grep) and silently corrupts results the model reasons over. Invariant in hook header; incident 2026-06-10. Regression gate: `cd hooks && bun test ContextReduction.test.ts` (30 probes — read-path identity + kept-class structure). | Yes (updatedInput) | `rtk` binary, `jq` | | `ArtWorkflowGuard.hook.ts` | Bash | Block freeform Art skill calls (force `--workflow=`) | Yes (decision) | None | | `SetQuestionTab.hook.ts` | AskUserQuestion | Set teal tab for questions | No | Kitty terminal | @@ -182,7 +183,7 @@ interface StopPayload extends BasePayload { | `Safety.hook.ts` | WebFetch / WebSearch | Tag external content with "treat as data" warning + injection-shape marker. Same file as the PermissionRequest hook below; dispatches by event. | No | `lib/safety-classifier.ts` | | `QuestionAnswered.hook.ts` | AskUserQuestion | Reset tab state after question answered | No | Kitty terminal | | `ISASync.hook.ts` | Edit / Write / MultiEdit | Sync ISA frontmatter → `work.json` + KV push | No | `MEMORY/WORK/`, `work.json` | -| `CheckpointPerISC.hook.ts` | Edit / Write / MultiEdit | Auto-commit per-ISC durability checkpoint | No | `~/.claude/checkpoint-repos.txt` | +| `CheckpointPerISC.hook.ts` | Edit / Write / MultiEdit | Auto-commit per-ISC durability checkpoint | No | `{{LIFEOS_ROOT}}/checkpoint-repos.txt` | | `TelosSummarySync.hook.ts` | Edit / Write / MultiEdit | Regenerate `PRINCIPAL_TELOS.md` on TELOS edits | No | `GenerateTelosSummary.ts` | | `ToolActivityTracker.hook.ts` | (catch-all) | Ground-truth audit log of every tool call | No | `MEMORY/OBSERVABILITY/tool-activity.jsonl` | @@ -413,8 +414,8 @@ Hooks are configured in `settings.json` under the `hooks` key: "SessionStart": [ { "hooks": [ - { "type": "command", "command": "$HOME/.claude/hooks/KittyEnvPersist.hook.ts" }, - { "type": "command", "command": "$HOME/.claude/hooks/LoadContext.hook.ts" } + { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/KittyEnvPersist.hook.ts" }, + { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/LoadContext.hook.ts" } ] } ], @@ -422,7 +423,7 @@ Hooks are configured in `settings.json` under the `hooks` key: { "matcher": "Bash", "hooks": [ - { "type": "command", "command": "$HOME/.claude/hooks/ContextReduction.hook.sh" } + { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/ContextReduction.hook.sh" } ] } ], @@ -430,7 +431,7 @@ Hooks are configured in `settings.json` under the `hooks` key: { "matcher": "WebFetch", "hooks": [ - { "type": "command", "command": "$HOME/.claude/hooks/Safety.hook.ts" } + { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/Safety.hook.ts" } ] } ], @@ -438,7 +439,7 @@ Hooks are configured in `settings.json` under the `hooks` key: { "matcher": "Write|Edit|MultiEdit|Bash", "hooks": [ - { "type": "command", "command": "$HOME/.claude/hooks/Safety.hook.ts" } + { "type": "command", "command": "{{LIFEOS_ROOT}}/hooks/Safety.hook.ts" } ] } ] @@ -579,7 +580,7 @@ When modifying ANY hook: 1. Verify `Safety.hook.ts` registered on `PermissionRequest` with matcher `Write|Edit|MultiEdit|Bash` 2. Test: `echo '{"session_id":"t","hook_event_name":"PermissionRequest","tool_name":"Bash","tool_input":{"command":"ls /tmp"}}' | bun hooks/Safety.hook.ts` — should emit `{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}` -3. Tail observability: `tail -f ~/.claude/LIFEOS/MEMORY/OBSERVABILITY/permission-decisions.jsonl` +3. Tail observability: `tail -f "${LIFEOS_DIR}/MEMORY/OBSERVABILITY/permission-decisions.jsonl"` --- diff --git a/LifeOS/install/hooks/ReminderRouter.hook.ts b/LifeOS/install/hooks/ReminderRouter.hook.ts index cb96fc6711..be0c51ff0b 100755 --- a/LifeOS/install/hooks/ReminderRouter.hook.ts +++ b/LifeOS/install/hooks/ReminderRouter.hook.ts @@ -25,9 +25,10 @@ import { getDAName } from "./lib/identity" import { createHash } from "crypto"; import { join, dirname } from "path"; import { loadWorkConfig } from "./lib/work-config"; +import { getClaudeDir } from "./lib/paths"; const HOME = process.env.HOME || ""; -const STATE_PATH = join(HOME, ".claude", "LIFEOS", "MEMORY", "STATE", "reminder-router-seen.json"); +const STATE_PATH = join(getClaudeDir(), "LIFEOS", "MEMORY", "STATE", "reminder-router-seen.json"); interface HookInput { session_id?: string; diff --git a/LifeOS/install/hooks/Safety.hook.ts b/LifeOS/install/hooks/Safety.hook.ts index d040f57859..6528e9308c 100755 --- a/LifeOS/install/hooks/Safety.hook.ts +++ b/LifeOS/install/hooks/Safety.hook.ts @@ -50,6 +50,7 @@ import { type ToolCall, } from "./lib/safety-classifier"; import { SECRET_VALUE_SHAPES } from "./lib/egress-class-core"; +import { getClaudeDir } from "./lib/paths"; const STDIN_CAP_BYTES = 2 * 1024 * 1024; const CACHE_MAX_BYTES = 10 * 1024 * 1024; @@ -60,7 +61,7 @@ const LIFEOS_DIR = process.env.LIFEOS_DIR /^\$\{?HOME\}?(?=\/|$)/, HOME, ) - : join(HOME, ".claude", "LIFEOS"); + : join(getClaudeDir(), "LIFEOS"); const STATE_DIR = join(LIFEOS_DIR, "MEMORY", "STATE"); const OBS_DIR = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY"); const CACHE_PATH = join(STATE_DIR, "permission-cache.json"); diff --git a/LifeOS/install/hooks/SatisfactionCapture.hook.ts b/LifeOS/install/hooks/SatisfactionCapture.hook.ts index 6b6722c78d..4b1a1db9de 100755 --- a/LifeOS/install/hooks/SatisfactionCapture.hook.ts +++ b/LifeOS/install/hooks/SatisfactionCapture.hook.ts @@ -38,6 +38,7 @@ import { getLearningCategory } from './lib/learning-utils'; import { getISOTimestamp, getPSTComponents } from './lib/time'; import { captureFailure } from '../LIFEOS/TOOLS/FailureCapture'; import { addRatingPulse } from './lib/isa-utils'; +import { getClaudeDir } from "./lib/paths"; // 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 @@ interface RatingEntry { // ── Constants ── -const BASE_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude', 'LIFEOS'); +const BASE_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), 'LIFEOS'); const SIGNALS_DIR = join(BASE_DIR, 'MEMORY', 'LEARNING', 'SIGNALS'); const RATINGS_FILE = join(SIGNALS_DIR, 'ratings.jsonl'); const LAST_RESPONSE_CACHE = join(BASE_DIR, 'MEMORY', 'STATE', 'last-response.txt'); diff --git a/LifeOS/install/hooks/SessionCleanup.hook.ts b/LifeOS/install/hooks/SessionCleanup.hook.ts index 52e93c2be6..75be475794 100755 --- a/LifeOS/install/hooks/SessionCleanup.hook.ts +++ b/LifeOS/install/hooks/SessionCleanup.hook.ts @@ -46,6 +46,7 @@ import { join } from 'path'; import { getISOTimestamp } from './lib/time'; import { setTabState, cleanupKittySession } from './lib/tab-setter'; import { readRegistry, writeRegistry, findArtifactPath, findActiveSessionByUUID } from './lib/isa-utils'; +import { getClaudeDir } from "./lib/paths"; // Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404) for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { @@ -54,7 +55,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { } -const BASE_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude', 'LIFEOS'); +const BASE_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), 'LIFEOS'); const MEMORY_DIR = join(BASE_DIR, 'MEMORY'); const STATE_DIR = join(MEMORY_DIR, 'STATE'); const WORK_DIR = join(MEMORY_DIR, 'WORK'); diff --git a/LifeOS/install/hooks/SystemFileGuard.hook.ts b/LifeOS/install/hooks/SystemFileGuard.hook.ts index 898469640b..fa54670be5 100755 --- a/LifeOS/install/hooks/SystemFileGuard.hook.ts +++ b/LifeOS/install/hooks/SystemFileGuard.hook.ts @@ -24,9 +24,10 @@ import { readFileSync, appendFileSync, existsSync, mkdirSync } from "node:fs"; import { dirname, join } from "node:path"; import { homedir } from "node:os"; import { evaluateWrite, extractNewContent } from "./lib/system-file-guard-core"; +import { getClaudeDir } from "./lib/paths"; const HOME = process.env.HOME ?? homedir(); -const LOG_PATH = join(HOME, ".claude/LIFEOS/MEMORY/OBSERVABILITY/system-file-guard.jsonl"); +const LOG_PATH = join(getClaudeDir(), "LIFEOS/MEMORY/OBSERVABILITY/system-file-guard.jsonl"); interface HookInput { session_id?: string; @@ -74,7 +75,7 @@ function denyMessage(relPath: string, pattern: string, match: string): string { " patterns. Move the user-specific content to a USER-zone location", " or read it through the LifeosConfig interface:", "", - " import { loadLifeosConfig, paiUserDir } from '~/.claude/LIFEOS/TOOLS/LifeosConfig';", + ` import { loadLifeosConfig, paiUserDir } from ${JSON.stringify(join(getClaudeDir(), "LIFEOS", "TOOLS", "LifeosConfig"))};`, "", " Canonical USER zones (any of these can hold the content):", " LIFEOS/USER/PRINCIPAL/ principal identity files", diff --git a/LifeOS/install/hooks/VerificationGate.hook.ts b/LifeOS/install/hooks/VerificationGate.hook.ts index a600dff0e0..f8e7bb0653 100755 --- a/LifeOS/install/hooks/VerificationGate.hook.ts +++ b/LifeOS/install/hooks/VerificationGate.hook.ts @@ -40,8 +40,9 @@ import { import { appendFileSync, mkdirSync, existsSync, readFileSync, writeFileSync } from "fs"; import { dirname, join } from "path"; import { createHash } from "crypto"; +import { getClaudeDir } from "./lib/paths"; -const LIFEOS = process.env.LIFEOS_DIR || join(process.env.HOME!, ".claude", "LIFEOS"); +const LIFEOS = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const OBS_PATH = join(LIFEOS, "MEMORY", "OBSERVABILITY", "verification-gate.jsonl"); const STATE_PATH = join(LIFEOS, "MEMORY", "STATE", "verification-gate-blocked.json"); diff --git a/LifeOS/install/hooks/WorkCompletionLearning.hook.ts b/LifeOS/install/hooks/WorkCompletionLearning.hook.ts index fdeb631d05..0b25d08fc4 100755 --- a/LifeOS/install/hooks/WorkCompletionLearning.hook.ts +++ b/LifeOS/install/hooks/WorkCompletionLearning.hook.ts @@ -62,6 +62,7 @@ import { join } from 'path'; import { getISOTimestamp, getPSTDate } from './lib/time'; import { getLearningCategory } from './lib/learning-utils'; import { findArtifactPath, findActiveSessionByUUID } from './lib/isa-utils'; +import { getClaudeDir } from "./lib/paths"; // Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404) for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { @@ -70,7 +71,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { } -const BASE_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude', 'LIFEOS'); +const BASE_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), 'LIFEOS'); const MEMORY_DIR = join(BASE_DIR, 'MEMORY'); const WORK_DIR = join(MEMORY_DIR, 'WORK'); const LEARNING_DIR = join(MEMORY_DIR, 'LEARNING'); diff --git a/LifeOS/install/hooks/WritingGate.hook.ts b/LifeOS/install/hooks/WritingGate.hook.ts index 04b12acd62..9afa025da7 100755 --- a/LifeOS/install/hooks/WritingGate.hook.ts +++ b/LifeOS/install/hooks/WritingGate.hook.ts @@ -37,6 +37,7 @@ import { readHookInput, parseTranscriptFromInput } from "./lib/hook-io"; import { appendFileSync, mkdirSync, existsSync, readFileSync } from "fs"; import { createHash } from "crypto"; import { dirname, join } from "path"; +import { getClaudeDir, shellQuote } from "./lib/paths"; // Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404) for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { @@ -45,10 +46,11 @@ 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(getClaudeDir(), "LIFEOS"); const OBS_PATH = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY", "writing-gate.jsonl"); const RUNS_PATH = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY", "pangram-runs.jsonl"); const RUN_WINDOW_MS = 30 * 60 * 1000; // a run counts as "this turn" within 30 min +const PANGRAM_COMMAND = `bun ${shellQuote(join(LIFEOS_DIR, "TOOLS", "PangramScore.ts"))} --file `; // ── Publication-content signals ─────────────────────────────────────────────── // STRONG (teeth): a disclosure hashtag ALONE on a line (caption shape, not a @@ -144,7 +146,7 @@ const BLOCK_REASON = "through the _WRITING skill AND the detector before it is shown (OPERATIONAL_RULES.md § Authored content). " + "The gate checks for a real PangramScore.ts run on the draft — a typed token does NOT satisfy it. Before " + "stopping: (1) run Skill(\"_WRITING\") DETECT mode on the draft and fix every P0/P1 in the right voice; " + - "(2) run `bun ~/.claude/LIFEOS/TOOLS/PangramScore.ts --file ` on the ACTUAL draft text so the run is " + + `(2) run \`${PANGRAM_COMMAND}\` on the ACTUAL draft text so the run is ` + "logged; (3) cite the detect result + the reported AI% in a `✍️ WRITING-AUDIT:` line. Pangram saturates on " + "model prose, so the number is REPORTED, not a pass/fail bar — the requirement is that the audit RAN."; diff --git a/LifeOS/install/hooks/handlers/UpdateCounts.ts b/LifeOS/install/hooks/handlers/UpdateCounts.ts index 46f678872d..c07497a4e4 100755 --- a/LifeOS/install/hooks/handlers/UpdateCounts.ts +++ b/LifeOS/install/hooks/handlers/UpdateCounts.ts @@ -22,6 +22,7 @@ import { readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; import { execSync } from 'child_process'; import { getLifeosDir } from '../lib/paths'; +import { getClaudeDir } from "../lib/paths"; /** * Refresh usage cache from Anthropic OAuth API. @@ -39,7 +40,7 @@ async function refreshUsageCache(paiDir: string): Promise { { encoding: 'utf-8', timeout: 3000 } ).trim(); } else { - const credPath = join(process.env.HOME || '', '.claude', '.credentials.json'); + const credPath = join(getClaudeDir(), '.credentials.json'); credJson = readFileSync(credPath, 'utf-8').trim(); } diff --git a/LifeOS/install/hooks/hooks.json b/LifeOS/install/hooks/hooks.json index ff8b570a12..0afe8c8007 100644 --- a/LifeOS/install/hooks/hooks.json +++ b/LifeOS/install/hooks/hooks.json @@ -42,7 +42,7 @@ ] }, { - "matcher": "Bash|Write|Edit|MultiEdit", + "matcher": "Bash|Write|Edit|MultiEdit|Read|Glob|Grep", "hooks": [ { "type": "command", diff --git a/LifeOS/install/hooks/lib/identity.ts b/LifeOS/install/hooks/lib/identity.ts index 0ae796f9cc..cf692206ba 100755 --- a/LifeOS/install/hooks/lib/identity.ts +++ b/LifeOS/install/hooks/lib/identity.ts @@ -14,9 +14,10 @@ import { readFileSync, existsSync } from 'fs'; import { join } from 'path'; import { parse as parseYaml } from 'yaml'; import { loadLifeosConfig } from '../../LIFEOS/TOOLS/LifeosConfig'; +import { getClaudeDir } from "./paths"; const HOME = process.env.HOME!; -const SETTINGS_PATH = join(HOME, '.claude/settings.json'); +const SETTINGS_PATH = join(getClaudeDir(), "settings.json"); // Identity-file paths derive from LifeosConfig's userDir. On fresh installs where // LIFEOS_CONFIG.toml hasn't been created yet, fall back to the conventional @@ -26,7 +27,7 @@ function paiUserDir(): string { try { return loadLifeosConfig().paths.userDir; } catch { - return join(HOME, '.claude/LIFEOS/USER'); + return join(getClaudeDir(), "LIFEOS/USER"); } } const DA_IDENTITY_PATH = join(paiUserDir(), 'DIGITAL_ASSISTANT/DA_IDENTITY.md'); diff --git a/LifeOS/install/hooks/lib/notifications.ts b/LifeOS/install/hooks/lib/notifications.ts index 54a52f4e47..56939a6587 100755 --- a/LifeOS/install/hooks/lib/notifications.ts +++ b/LifeOS/install/hooks/lib/notifications.ts @@ -7,9 +7,10 @@ import { readFileSync, writeFileSync, existsSync } from 'fs'; import { join } from 'path'; +import { getClaudeDir } from "./paths"; const HOME = process.env.HOME!; -const PULSE_TOML_PATH = join(HOME, '.claude/LIFEOS/PULSE/PULSE.toml'); +const PULSE_TOML_PATH = join(getClaudeDir(), "LIFEOS/PULSE/PULSE.toml"); // ============================================================================ // Session Timing diff --git a/LifeOS/install/hooks/lib/paths.ts b/LifeOS/install/hooks/lib/paths.ts index 205c4ef7fd..800448c39e 100755 --- a/LifeOS/install/hooks/lib/paths.ts +++ b/LifeOS/install/hooks/lib/paths.ts @@ -9,8 +9,33 @@ * import { getLifeosDir, getClaudeDir, paiPath } from ''; */ +import { existsSync } from 'fs'; import { homedir } from 'os'; -import { join } from 'path'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +/** + * Walk up from this module's own location to the config root — the ancestor dir + * that has a `LIFEOS/` child (this file ships to `/hooks/lib/paths.ts`). + * Belt-and-suspenders for a hook/tool spawned WITHOUT `LIFEOS_DIR` in its env: without + * this, getClaudeDir() falls back to ~/.claude and a custom-home install leaks hook + * state (MEMORY/STATE/...) into the real ~/.claude. Returns null if nothing matches. + * Never throws. + */ +function selfConfigRoot(): string | null { + try { + let dir = dirname(fileURLToPath(import.meta.url)); // /hooks/lib + for (let i = 0; i < 8; i++) { + if (existsSync(join(dir, 'LIFEOS'))) return dir; + const parent = dirname(dir); + if (parent === dir) break; + dir = parent; + } + } catch { + /* fileURLToPath / fs can fail in exotic runtimes — fall through */ + } + return null; +} /** * Expand shell variables in a path string @@ -25,6 +50,12 @@ export function expandPath(path: string): string { .replace(/^~(?=\/|$)/, home); } +/** 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, `'"'"'`)}'`; +} + /** * Get the LifeOS data directory (expanded). * @@ -51,6 +82,9 @@ export function getLifeosDir(): string { return expandPath(envLifeosDir); } + const self = selfConfigRoot(); + if (self) return join(self, 'LIFEOS'); + return join(homedir(), '.claude', 'LIFEOS'); } @@ -59,8 +93,17 @@ export function getLifeosDir(): string { * * Plugin install: CLAUDE_PLUGIN_ROOT is the flattened plugin root that plays the * live ~/.claude role (skills/ and hooks/ sit directly under it, matching live - * .claude/skills and .claude/hooks). Live default: ~/.claude — byte-identical to - * pre-plugin behavior, since CLAUDE_PLUGIN_ROOT is unset on a normal install. + * .claude/skills and .claude/hooks). + * + * Custom LifeOS home (LIFEOS_HOME install): LIFEOS_DIR is `/LIFEOS`, + * so the config root is its parent — deriving it here keeps every consumer + * (settings, skills, hooks, .env) inside the custom home instead of silently + * falling back to ~/.claude. Must stay BELOW the plugin guard: bin/pai exports + * LIFEOS_DIR equal to CLAUDE_PLUGIN_ROOT, where dirname() would escape the root. + * + * Live default: ~/.claude — byte-identical to pre-plugin behavior, since + * CLAUDE_PLUGIN_ROOT and LIFEOS_DIR are unset on a plain install and LIFEOS_DIR + * is ~/.claude/LIFEOS (same parent) on a default one. */ export function getClaudeDir(): string { const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT; @@ -69,6 +112,15 @@ export function getClaudeDir(): string { return expandPath(pluginRoot); } + const envLifeosDir = process.env.LIFEOS_DIR; + + if (envLifeosDir) { + return dirname(expandPath(envLifeosDir)); + } + + const self = selfConfigRoot(); + if (self) return self; + return join(homedir(), '.claude'); } diff --git a/LifeOS/install/hooks/lib/safety-classifier.ts b/LifeOS/install/hooks/lib/safety-classifier.ts index 0fba1d8515..ade133535c 100755 --- a/LifeOS/install/hooks/lib/safety-classifier.ts +++ b/LifeOS/install/hooks/lib/safety-classifier.ts @@ -1,5 +1,6 @@ import { homedir } from "node:os"; import { resolve } from "node:path"; +import { getClaudeDir } from "./paths"; export type Decision = "allow" | "neutral"; @@ -32,7 +33,7 @@ export interface ToolCall { const HOME = homedir(); export const TRUSTED_PREFIXES: readonly string[] = [ - resolve(HOME, ".claude"), + resolve(getClaudeDir()), resolve(HOME, "Projects"), resolve(HOME, "LocalProjects"), resolve(HOME, "Downloads"), diff --git a/LifeOS/install/hooks/lib/system-file-guard-core.ts b/LifeOS/install/hooks/lib/system-file-guard-core.ts index 3ab27badbe..13c0476a01 100644 --- a/LifeOS/install/hooks/lib/system-file-guard-core.ts +++ b/LifeOS/install/hooks/lib/system-file-guard-core.ts @@ -15,9 +15,10 @@ import { join } from "node:path"; import { homedir } from "node:os"; import { createHash } from "node:crypto"; import { isContained, isPatternAllowlisted, relativeToClaudeRoot } from "./containment-zones"; +import { getClaudeDir } from "./paths"; const HOME = process.env.HOME ?? homedir(); -const CLAUDE_ROOT = join(HOME, ".claude"); +const CLAUDE_ROOT = join(getClaudeDir()); const DEFAULT_DENY_LIST_PATH = join(CLAUDE_ROOT, "skills/_LIFEOS/DENY_LIST.txt"); const DEFAULT_HASHES_PATH = join(CLAUDE_ROOT, "skills/_LIFEOS/DENY_HASHES.json"); const DEFAULT_ENV_PATH = join(CLAUDE_ROOT, ".env"); diff --git a/LifeOS/install/hooks/lib/work-config.ts b/LifeOS/install/hooks/lib/work-config.ts index cf23ff592e..29296913ad 100755 --- a/LifeOS/install/hooks/lib/work-config.ts +++ b/LifeOS/install/hooks/lib/work-config.ts @@ -33,6 +33,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { */ import { existsSync, readFileSync, writeFileSync, chmodSync } from "fs"; import { join } from "path"; +import { getClaudeDir, shellQuote } from "./paths"; // Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404) for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { @@ -44,9 +45,10 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { declare const Bun: { spawnSync: (cmd: string[], opts?: any) => any }; const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const REPO_JSON_PATH = join(LIFEOS_DIR, "USER", "WORK", "work_repo.json"); const COLUMNS_YAML_PATH = join(LIFEOS_DIR, "USER", "WORK", "config.yaml"); +const SET_WORK_REPO_COMMAND = `bun ${shellQuote(join(getClaudeDir(), "skills", "_ULWORK", "Tools", "SetWorkRepo.ts"))} `; // Slimmed kanban — {{PRINCIPAL_NAME}} asked for 5 lanes that map to how he actually moves work. const DEFAULT_COLUMNS = ["Queued", "Blocked", "In-Progress", "In-Review", "Complete"]; @@ -118,7 +120,7 @@ export function loadWorkConfig(): WorkConfig { if (!existsSync(REPO_JSON_PATH)) { return disabled( "missing", - "USER/WORK/work_repo.json missing — run `bun ~/.claude/skills/_ULWORK/Tools/SetWorkRepo.ts `", + `USER/WORK/work_repo.json missing — run \`${SET_WORK_REPO_COMMAND}\``, ); } diff --git a/LifeOS/install/install.sh b/LifeOS/install/install.sh index 7daf137211..a3aa90de50 100755 --- a/LifeOS/install/install.sh +++ b/LifeOS/install/install.sh @@ -19,9 +19,34 @@ # # Local/offline install (no network): # LIFEOS_SRC=/path/to/LIFEOS_RELEASES/ bash install.sh +# +# Custom LifeOS home (install somewhere other than ~/.claude — e.g. a +# project-scoped `.claude` so a second, isolated LifeOS can live beside +# your global one): +# bash install.sh --home ~/MyProject/.claude +# LIFEOS_HOME=~/MyProject/.claude bash install.sh # equivalent +# The var is exported so the agentic `/lifeos-setup` (launched below in +# the same shell) installs into it too — without it, the setup offers +# the location choice interactively. See INSTALL.md § 2.5 for how a +# custom home is LOADED at runtime. # ═══════════════════════════════════════════════════════════════════ set -euo pipefail +# ─── Args ───────────────────────────────────────────────────────── +# --home sets LIFEOS_HOME (the custom LifeOS home). Env var form works too. +while [ $# -gt 0 ]; do + case "$1" in + --home) LIFEOS_HOME="${2:?--home needs a directory}"; shift 2 ;; + --home=*) LIFEOS_HOME="${1#--home=}"; shift ;; + *) echo "unknown argument: $1 (supported: --home )" >&2; exit 1 ;; + esac +done +LIFEOS_HOME="${LIFEOS_HOME:-}" +if [ -n "$LIFEOS_HOME" ]; then + case "$LIFEOS_HOME" in "~"*) LIFEOS_HOME="${HOME}${LIFEOS_HOME#\~}" ;; esac + export LIFEOS_HOME +fi + # ─── Release resolution — always the latest published release ───── # No pin: this resolves the newest GitHub Release at run time, so every new # release reaches every installer with zero edits here. Override with @@ -127,10 +152,12 @@ success "bun ($(command -v bun), v$(bun --version 2>/dev/null))" # ─── Step 2: Detect harness (no clobber) ───────────────────────── step "2/5 Detecting your harness" if [ -z "$LIFEOS_SKILLS_DIR" ]; then - if [ -d "$HOME/.claude" ]; then LIFEOS_SKILLS_DIR="$HOME/.claude/skills" + if [ -n "$LIFEOS_HOME" ]; then LIFEOS_SKILLS_DIR="$LIFEOS_HOME/skills" + elif [ -d "$HOME/.claude" ]; then LIFEOS_SKILLS_DIR="$HOME/.claude/skills" elif [ -d "$HOME/.config/claude" ]; then LIFEOS_SKILLS_DIR="$HOME/.config/claude/skills" else LIFEOS_SKILLS_DIR="$HOME/.claude/skills"; fi fi +[ -n "$LIFEOS_HOME" ] && info "Custom LifeOS home: ${BOLD}${LIFEOS_HOME/#$HOME/~}${RESET} (LIFEOS_HOME exported for the setup)" info "Skills dir: ${BOLD}${LIFEOS_SKILLS_DIR/#$HOME/~}${RESET}" TARGET="$LIFEOS_SKILLS_DIR/LifeOS" if [ -e "$TARGET" ]; then @@ -178,7 +205,18 @@ info "hooks with your permission. Nothing changes without you saying yes." echo if command -v claude >/dev/null 2>&1 && [ -z "${CLAUDECODE:-}" ]; then info "Launching setup..." + # LIFEOS_HOME is our installer override, not a Claude Code setting. Point the + # bootstrap session at the staged root so it can actually discover the new + # LifeOS skill regardless of the caller's current working directory. + if [ -n "$LIFEOS_HOME" ]; then + export CLAUDE_CONFIG_DIR="$LIFEOS_HOME" + fi exec claude "/lifeos-setup" else - printf " ${BOLD}Open your harness and run:${RESET} ${LIGHT_BLUE}/lifeos-setup${RESET}\n\n" + if [ -n "$LIFEOS_HOME" ]; then + printf -v SETUP_ROOT_Q '%q' "$LIFEOS_HOME" + printf " ${BOLD}Open a terminal and run:${RESET} ${LIGHT_BLUE}CLAUDE_CONFIG_DIR=%s claude /lifeos-setup${RESET}\n\n" "$SETUP_ROOT_Q" + else + printf " ${BOLD}Open your harness and run:${RESET} ${LIGHT_BLUE}/lifeos-setup${RESET}\n\n" + fi fi diff --git a/LifeOS/install/settings.system.json b/LifeOS/install/settings.system.json index 80dd4b29e9..ea82521027 100644 --- a/LifeOS/install/settings.system.json +++ b/LifeOS/install/settings.system.json @@ -3,10 +3,11 @@ "skillListingBudgetFraction": 0.15, "env": { "LIFEOS_DIR": "$HOME/.claude/LIFEOS", + "LIFEOS_ROOT": "$HOME/.claude", "PROJECTS_DIR": "$HOME/Projects", "BASH_DEFAULT_TIMEOUT_MS": "600000", "API_TIMEOUT_MS": "1800000", - "LIFEOS_CONFIG_DIR": "$HOME/.claude/LIFEOS", + "LIFEOS_CONFIG_DIR": "$HOME/.config/LIFEOS", "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1", "CLAUDE_CODE_FORK_SUBAGENT": "1" }, diff --git a/LifeOS/install/skills/ApertureOscillation/SKILL.md b/LifeOS/install/skills/ApertureOscillation/SKILL.md index b233bb9b56..300fb80e2c 100644 --- a/LifeOS/install/skills/ApertureOscillation/SKILL.md +++ b/LifeOS/install/skills/ApertureOscillation/SKILL.md @@ -8,7 +8,7 @@ effort: medium ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/ApertureOscillation/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/ApertureOscillation/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -110,7 +110,7 @@ Pass 3 (Synthesis): Tension — standalone webhook service vs. Arbol action. Res After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"ApertureOscillation","workflow":"Oscillate","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"ApertureOscillation","workflow":"Oscillate","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/Aphorisms/SKILL.md b/LifeOS/install/skills/Aphorisms/SKILL.md index 35b5e804a8..4b39f3124d 100644 --- a/LifeOS/install/skills/Aphorisms/SKILL.md +++ b/LifeOS/install/skills/Aphorisms/SKILL.md @@ -8,7 +8,7 @@ effort: low ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Aphorisms/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/Aphorisms/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -53,7 +53,7 @@ Running the **WorkflowName** workflow in the **Aphorisms** skill to ACTION... ## Database -**Location:** `~/.claude/skills/Aphorisms/Database/aphorisms.md` +**Location:** `${CLAUDE_SKILL_DIR}/Database/aphorisms.md` Organized by author, theme, context, and usage history. Per-aphorism metadata: full quote text, author attribution, theme tags, context/background, source reference. Sections: Initial Collection, per-thinker sections, Theme Index, Newsletter Usage History. @@ -79,7 +79,7 @@ Organized by author, theme, context, and usage history. Per-aphorism metadata: f After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Aphorisms","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Aphorisms","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/Aphorisms/Workflows/AddAphorism.md b/LifeOS/install/skills/Aphorisms/Workflows/AddAphorism.md index 47b6794822..84f258e917 100755 --- a/LifeOS/install/skills/Aphorisms/Workflows/AddAphorism.md +++ b/LifeOS/install/skills/Aphorisms/Workflows/AddAphorism.md @@ -22,7 +22,7 @@ Running **AddAphorism** in **Aphorisms**... - After research-thinker.md discovers quotes worth adding **Prerequisites:** -- Aphorism database exists at `~/.claude/skills/Aphorisms/Database/aphorisms.md` +- Aphorism database exists at `{{LIFEOS_ROOT}}/skills/Aphorisms/Database/aphorisms.md` - Quote text and author provided (or discoverable through research) - Database is Read first to check for duplicates @@ -90,8 +90,8 @@ WebSearch("misattributed quotes [author name]") ### Step 3: Check for Duplicates **Read database:** -```bash -Read ~/.claude/skills/Aphorisms/Database/aphorisms.md +```text +Read {{LIFEOS_ROOT}}/skills/Aphorisms/Database/aphorisms.md ``` **Check for:** @@ -249,13 +249,13 @@ Add quote reference to appropriate theme(s) in Theme Index section: **Use Edit tool to add quote:** -```bash +```text # Find appropriate section -Read ~/.claude/skills/Aphorisms/Database/aphorisms.md +Read {{LIFEOS_ROOT}}/skills/Aphorisms/Database/aphorisms.md # Add to correct location Edit( - file_path=~/.claude/skills/Aphorisms/Database/aphorisms.md, + file_path={{LIFEOS_ROOT}}/skills/Aphorisms/Database/aphorisms.md, old_string="[section where it should be inserted]", new_string="[section with new quote added]" ) diff --git a/LifeOS/install/skills/Aphorisms/Workflows/FindAphorism.md b/LifeOS/install/skills/Aphorisms/Workflows/FindAphorism.md index 514f1e7001..4e7f219927 100755 --- a/LifeOS/install/skills/Aphorisms/Workflows/FindAphorism.md +++ b/LifeOS/install/skills/Aphorisms/Workflows/FindAphorism.md @@ -22,7 +22,7 @@ Running **FindAphorism** in **Aphorisms**... - Working on newsletter and needs opening/closing wisdom quote **Prerequisites:** -- Aphorism database exists at `~/.claude/skills/Aphorisms/Database/aphorisms.md` +- Aphorism database exists at `{{LIFEOS_ROOT}}/skills/Aphorisms/Database/aphorisms.md` - Newsletter content or URL provided by user - Clear understanding of newsletter theme (if not provided, extract from content) @@ -97,8 +97,8 @@ Use deep thinking for deep thematic analysis. Identify: ### Step 3: Read Aphorism Database **Load database:** -```bash -Read ~/.claude/skills/Aphorisms/Database/aphorisms.md +```text +Read {{LIFEOS_ROOT}}/skills/Aphorisms/Database/aphorisms.md ``` **Review relevant sections:** diff --git a/LifeOS/install/skills/Aphorisms/Workflows/ResearchThinker.md b/LifeOS/install/skills/Aphorisms/Workflows/ResearchThinker.md index 96b174c442..6bd41df6cb 100755 --- a/LifeOS/install/skills/Aphorisms/Workflows/ResearchThinker.md +++ b/LifeOS/install/skills/Aphorisms/Workflows/ResearchThinker.md @@ -22,7 +22,7 @@ Running **ResearchThinker** in **Aphorisms**... - Building out thinker sections in database **Prerequisites:** -- Aphorism database exists at `~/.claude/skills/Aphorisms/Database/aphorisms.md` +- Aphorism database exists at `{{LIFEOS_ROOT}}/skills/Aphorisms/Database/aphorisms.md` - Clear understanding of which thinker to research - Optional: specific theme/topic to focus research on @@ -259,8 +259,8 @@ research_skill.parallel_research( ### Step 7: Add to Database **Read current database:** -```bash -Read ~/.claude/skills/Aphorisms/Database/aphorisms.md +```text +Read {{LIFEOS_ROOT}}/skills/Aphorisms/Database/aphorisms.md ``` **Locate thinker's section:** @@ -268,9 +268,9 @@ Read ~/.claude/skills/Aphorisms/Database/aphorisms.md - Section should exist with placeholder: `*Quotes to be added from research*` **Use Edit to replace placeholder:** -```bash +```text Edit( - file_path=~/.claude/skills/Aphorisms/Database/aphorisms.md, + file_path={{LIFEOS_ROOT}}/skills/Aphorisms/Database/aphorisms.md, old_string="### [Thinker Name]\n*Quotes to be added from research*", new_string="### [Thinker Name]\n\n[Organized quotes with themes and context]" ) diff --git a/LifeOS/install/skills/Aphorisms/Workflows/SearchAphorisms.md b/LifeOS/install/skills/Aphorisms/Workflows/SearchAphorisms.md index 14f1091c00..9caa497ddb 100755 --- a/LifeOS/install/skills/Aphorisms/Workflows/SearchAphorisms.md +++ b/LifeOS/install/skills/Aphorisms/Workflows/SearchAphorisms.md @@ -23,7 +23,7 @@ Running **SearchAphorisms** in **Aphorisms**... - Discovering what's available in database **Prerequisites:** -- Aphorism database exists at `~/.claude/skills/Aphorisms/Database/aphorisms.md` +- Aphorism database exists at `{{LIFEOS_ROOT}}/skills/Aphorisms/Database/aphorisms.md` - Search query or theme provided - Database Read for comprehensive search @@ -79,8 +79,8 @@ User: "Short quotes about action" ### Step 2: Read Database -```bash -Read ~/.claude/skills/Aphorisms/Database/aphorisms.md +```text +Read {{LIFEOS_ROOT}}/skills/Aphorisms/Database/aphorisms.md ``` **Load full context:** diff --git a/LifeOS/install/skills/Apify/INTEGRATION.md b/LifeOS/install/skills/Apify/INTEGRATION.md index dbc9ad9dd8..22819c0e73 100755 --- a/LifeOS/install/skills/Apify/INTEGRATION.md +++ b/LifeOS/install/skills/Apify/INTEGRATION.md @@ -25,7 +25,7 @@ The social skill now uses code-based Apify scripts instead of `mcp__apify` MCP t **Example Workflow:** 1. User: "Turn my latest tweet into a LinkedIn post" -2. System runs: `bun ~/.claude/filesystem-mcps/apify/get-latest-tweet.ts` +2. System runs: `bun "${LIFEOS_ROOT}/filesystem-mcps/apify/get-latest-tweet.ts"` 3. Script returns: Tweet text + metadata (~500 tokens) 4. System transforms tweet into LinkedIn format 5. **Token savings: 98%** (vs fetching unfiltered profile data) @@ -36,13 +36,13 @@ The social skill now uses code-based Apify scripts instead of `mcp__apify` MCP t ```bash # Research what ThePrimeagen is discussing -bun ~/.claude/filesystem-mcps/apify/get-user-tweets.ts ThePrimeagen 10 +bun "${LIFEOS_ROOT}/filesystem-mcps/apify/get-user-tweets.ts" ThePrimeagen 10 # Analyze Paul Graham's recent thoughts -bun ~/.claude/filesystem-mcps/apify/get-user-tweets.ts paulg 20 +bun "${LIFEOS_ROOT}/filesystem-mcps/apify/get-user-tweets.ts" paulg 20 # Track Simon Willison's posts -bun ~/.claude/filesystem-mcps/apify/get-user-tweets.ts simonw 15 +bun "${LIFEOS_ROOT}/filesystem-mcps/apify/get-user-tweets.ts" simonw 15 ``` **Token Efficiency:** @@ -56,7 +56,7 @@ bun ~/.claude/filesystem-mcps/apify/get-user-tweets.ts simonw 15 ```bash # Get user's thread about AI topic -bun ~/.claude/filesystem-mcps/apify/get-latest-thread.ts +bun "${LIFEOS_ROOT}/filesystem-mcps/apify/get-latest-thread.ts" # Expand thread into blog post format # Token efficient: only thread content in context @@ -109,12 +109,12 @@ mcp__Apify__get-actor-output(runId) ### After (Code-Based Approach) -```typescript -// All in one script, filtering in code -bun ~/.claude/filesystem-mcps/apify/get-latest-tweet.ts +```bash +# All in one script, filtering in code +bun "${LIFEOS_ROOT}/filesystem-mcps/apify/get-latest-tweet.ts" -// Returns only filtered result: ~500 tokens -// Savings: 98.2% +# Returns only filtered result: ~500 tokens +# Savings: 98.2% ``` ## Best Practices @@ -123,7 +123,7 @@ bun ~/.claude/filesystem-mcps/apify/get-latest-tweet.ts ✅ Use appropriate script for the task ✅ Let script filter data before returning ✅ Trust token savings calculations -✅ Run from `~/.claude/filesystem-mcps/apify/` directory or use full path +✅ Run from `{{LIFEOS_ROOT}}/filesystem-mcps/apify/` directory or use full path ✅ Check execution time (~10 seconds expected) ### DON'T: @@ -214,7 +214,7 @@ Q: Why not use MCP? A: 90-98% token savings, faster execution, better control. Q: What if script fails? -A: Check `APIFY_TOKEN` in `${LIFEOS_DIR}/.env`, verify network, check Apify status. +A: Check `APIFY_TOKEN` in `{{LIFEOS_DIR}}/.env`, verify network, check Apify status. Q: Can I add new actors? A: Yes! Follow `STANDARDS.md` pattern, hardcode actor ID, filter in code. diff --git a/LifeOS/install/skills/Apify/README.md b/LifeOS/install/skills/Apify/README.md index 899a7e9a78..927841ee05 100755 --- a/LifeOS/install/skills/Apify/README.md +++ b/LifeOS/install/skills/Apify/README.md @@ -7,7 +7,7 @@ Progressive disclosure interface for web scraping and automation via the Apify p ## Quick Start ```typescript -import { Apify } from '~/.claude/filesystem-mcps/apify' +import { Apify } from '{{LIFEOS_ROOT}}/filesystem-mcps/apify' const apify = new Apify(process.env.APIFY_TOKEN) @@ -290,7 +290,7 @@ Get your token from: https://console.apify.com/account/integrations All types are exported from the main module: ```typescript -import { Actor, ActorRun, DatasetOptions } from '~/.claude/filesystem-mcps/apify' +import { Actor, ActorRun, DatasetOptions } from '{{LIFEOS_ROOT}}/filesystem-mcps/apify' ``` ## Error Handling @@ -317,7 +317,7 @@ try { ```bash # Run the Instagram scraper example -cd ~/.claude/filesystem-mcps/apify +cd "${LIFEOS_ROOT}/filesystem-mcps/apify" bun run examples/instagram-scraper.ts # Or use bun directly diff --git a/LifeOS/install/skills/Apify/SKILL.md b/LifeOS/install/skills/Apify/SKILL.md index 7a677d4fcf..0cc7214843 100644 --- a/LifeOS/install/skills/Apify/SKILL.md +++ b/LifeOS/install/skills/Apify/SKILL.md @@ -8,7 +8,7 @@ effort: medium ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Apify/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/Apify/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -514,7 +514,7 @@ User: "scrape this company's LinkedIn page" After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Apify","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Apify","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/ArXiv/SKILL.md b/LifeOS/install/skills/ArXiv/SKILL.md index acd8a6d2ef..f0f6b18348 100644 --- a/LifeOS/install/skills/ArXiv/SKILL.md +++ b/LifeOS/install/skills/ArXiv/SKILL.md @@ -8,7 +8,7 @@ effort: low ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/ArXiv/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/ArXiv/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -116,5 +116,5 @@ User: "explain this paper: 2401.12345" After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"ArXiv","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"ArXiv","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` diff --git a/LifeOS/install/skills/Art/SKILL.md b/LifeOS/install/skills/Art/SKILL.md index ec3328c152..522a83b714 100644 --- a/LifeOS/install/skills/Art/SKILL.md +++ b/LifeOS/install/skills/Art/SKILL.md @@ -10,7 +10,7 @@ effort: medium ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Art/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/Art/` If this directory exists, load and apply: - `PREFERENCES.md` - Aesthetic preferences, default model, output location @@ -71,7 +71,7 @@ Reading the workflow's caps-warning, mentally noting "do the workflow," then com ### Workflow → command (copy-paste) ```bash -bun ~/.claude/skills/Art/Tools/Generate.ts \ +bun "${CLAUDE_SKILL_DIR}/Tools/Generate.ts" \ --workflow= \ --model nano-banana-pro \ --prompt "..." \ @@ -199,7 +199,7 @@ Route to the appropriate workflow based on the request. - Character design specifications - Scene composition rules -**Load from:** `~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Art/PREFERENCES.md` +**Load from:** `{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/Art/PREFERENCES.md` --- @@ -207,7 +207,7 @@ Route to the appropriate workflow based on the request. **User customization** may include reference images for consistent style. -Check `~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Art/PREFERENCES.md` for: +Check `{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/Art/PREFERENCES.md` for: - Reference image locations - Style examples by use case - Character and scene reference guidance @@ -298,7 +298,7 @@ bun run ${LIFEOS_SKILL_DIR}/Tools/Generate.ts \ - Up to 6 object reference images - Maximum 14 total reference images per request -**API keys in:** `${LIFEOS_DIR}/.env` +**API keys in:** `{{LIFEOS_DIR}}/.env` ## Examples @@ -351,7 +351,7 @@ User: "create icon for the skill system pack" - **`--remove-bg` is unsafe for thin-linework technical diagrams.** rembg classifies thin black ink on a light field as "background" and strips it, leaving a near-empty ghost. Documented 2026-05-11 on the free-will flowchart. Mitigations: (a) prompt for *thick* saturated linework first so rembg has a strong signal, or (b) skip `--remove-bg` entirely when the destination background matches the image's background (blog page is sepia #EAE9DF — opaque sepia diagram on sepia page composites with zero visible seam, no alpha needed). - **Logo fidelity breaks in 3D/perspective scenes even with a reference image.** Documented 2026-06-11 on the UL wallpaper set: straight-on and macro scenes held the glyph topology in 7/7 rolls, but the isometric 3D scene closed the open mark into a loop and dropped its isolated dot. For any perspective/3D composition with a logo, add topology-locked negative language to the prompt ("do not close the shape into a loop", "do not omit the isolated dot", name every stroke and terminal) on top of `--reference-image`, and vision-verify the topology specifically. - **nano-banana-pro "4K 16:9" is actually 5504×3072 (43:24, ~0.8% wider than 16:9), saved as .jpg even when `--output` says .png.** Disclose the native ratio when the spec says 16:9, and probe the real filename before Read/delivery. -- **White-box-on-cream bug (2026-06-20): flattening an OPAQUE jpeg on `#EAE9DF` is a no-op.** nano-banana-pro returns an opaque JPEG; `magick -background "#EAE9DF" -flatten` only fills *alpha*, so the model's baked near-white ground survives and paints a white rectangle on the cream blog page ("it has a fucking white background"). For inline blog headers, cut true alpha FIRST (`bun ~/.claude/LIFEOS/TOOLS/RemoveBg.ts`), then derive the WebP, and verify `identify -format "%[channels]" inline.webp` == `srgba`. Opaque-sepia inline is valid ONLY on an image that already has alpha. See Essay.md Step 7.0.5. +- **White-box-on-cream bug (2026-06-20): flattening an OPAQUE jpeg on `#EAE9DF` is a no-op.** nano-banana-pro returns an opaque JPEG; `magick -background "#EAE9DF" -flatten` only fills *alpha*, so the model's baked near-white ground survives and paints a white rectangle on the cream blog page ("it has a fucking white background"). For inline blog headers, cut true alpha FIRST (`bun "${LIFEOS_DIR}/TOOLS/RemoveBg.ts"`), then derive the WebP, and verify `identify -format "%[channels]" inline.webp` == `srgba`. Opaque-sepia inline is valid ONLY on an image that already has alpha. See Essay.md Step 7.0.5. - **Essay headers: run the Step 5A Best-Image Deliberation before prompting (2026-07-09 principal directive).** Subject-list prompts produce rejected flat tableaus; a composition reasoned deeply from the essay's specific argument — scene concepts compared, every element given a narrative role, connected structure — produces accepted images. The deliberation is the mandatory step; devices like cutaways are possible outcomes, not rules. See Essay.md Step 5A. - **Interior-white ban (2026-07-09, "giant white space" incident):** prompt large flat surfaces (desks, panels, windows, paper) as "warm cream paper tone", never bright white or unstated — baked-white interiors survive rembg intact and render as giant white rectangles on the cream page. Inside-the-subject sibling of the 2026-06-20 white-box bug. Also trim white padding off any external screenshot before embedding (`magick -fuzz 4% -trim` + sepia border). - **Reference-image edits: negative text loses to the reference (2026-07-09 studio-background session).** When nano-banana-pro keeps reproducing an unwanted object that exists in the reference photo (e.g. a second floor lamp), "do NOT add/duplicate" prompt language fails ~7/8 rolls — the model preserves what it sees over what you forbid. Fix: roll until ONE output has the corrected composition, then use THAT output as the new `--reference-image` for the remaining variations; compliance jumped to 7/7. Editing the reference beats describing the edit. @@ -362,7 +362,7 @@ User: "create icon for the skill system pack" After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Art","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Art","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/Art/Tools/Generate.ts b/LifeOS/install/skills/Art/Tools/Generate.ts index 2d07f23d10..bf3a0d0945 100755 --- a/LifeOS/install/skills/Art/Tools/Generate.ts +++ b/LifeOS/install/skills/Art/Tools/Generate.ts @@ -38,7 +38,7 @@ import { extname, resolve } from "node:path"; * This ensures API keys are available regardless of how the CLI is invoked */ async function loadEnv(): Promise { - const paiDir = process.env.LIFEOS_DIR || resolve(process.env.HOME!, '.claude'); + const paiDir = process.env.LIFEOS_DIR || resolve(claudeDir()); const envPath = resolve(paiDir, '.env'); try { const envContent = await readFile(envPath, 'utf-8'); @@ -218,7 +218,7 @@ async function detectMimeType(filePath: string): Promise { // ============================================================================ // LifeOS directory for documentation paths -const LIFEOS_DIR = process.env.LIFEOS_DIR || `${process.env.HOME}/.claude`; +const LIFEOS_DIR = process.env.LIFEOS_DIR || `${claudeDir()}`; function showHelp(): void { console.log(` @@ -352,7 +352,7 @@ MORE INFO: * --workflow= → exit 1 listing valid workflow names. */ function enforceWorkflowDiscipline(parsed: Partial): void { - const workflowsDir = `${process.env.HOME}/.claude/skills/Art/Workflows`; + const workflowsDir = `${claudeDir()}/skills/Art/Workflows`; let availableWorkflows: string[] = []; try { // readdirSync via Bun.readdirSync isn't a thing; use Node fs sync via dynamic @@ -681,6 +681,7 @@ function enhancePromptForTransparency(prompt: string): string { import { exec } from "node:child_process"; import { promisify } from "node:util"; +import { claudeDir } from "../../../LIFEOS/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"]) { diff --git a/LifeOS/install/skills/Art/Tools/GenerateMidjourneyImage.ts b/LifeOS/install/skills/Art/Tools/GenerateMidjourneyImage.ts index 0c1ee17773..39f5e68c78 100755 --- a/LifeOS/install/skills/Art/Tools/GenerateMidjourneyImage.ts +++ b/LifeOS/install/skills/Art/Tools/GenerateMidjourneyImage.ts @@ -23,6 +23,7 @@ import { DiscordBotClient } from '../lib/discord-bot.js'; import { MidjourneyClient, MidjourneyError } from '../lib/midjourney-client.js'; import { readFile } from 'node:fs/promises'; import { resolve } from 'node:path'; +import { claudeDir } from "../../../LIFEOS/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"]) { @@ -40,7 +41,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { * This ensures API keys are available regardless of how the CLI is invoked */ async function loadEnv(): Promise { - const paiDir = process.env.LIFEOS_DIR || resolve(process.env.HOME!, '.claude'); + const paiDir = process.env.LIFEOS_DIR || resolve(claudeDir()); const envPath = resolve(paiDir, '.env'); try { const envContent = await readFile(envPath, 'utf-8'); diff --git a/LifeOS/install/skills/Art/Tools/GeneratePrompt.ts b/LifeOS/install/skills/Art/Tools/GeneratePrompt.ts index 3202944174..f9d5c02500 100755 --- a/LifeOS/install/skills/Art/Tools/GeneratePrompt.ts +++ b/LifeOS/install/skills/Art/Tools/GeneratePrompt.ts @@ -19,6 +19,7 @@ import { readFileSync } from "node:fs"; import { resolve } from "node:path"; +import { claudeDir } from "../../../LIFEOS/TOOLS/lifeos-root"; // ============================================================================ // Types @@ -67,9 +68,7 @@ interface PromptOutput { // Constants // ============================================================================ -const ART_AESTHETIC_PATH = resolve( - process.env.HOME!, - ".claude/LIFEOS/Aesthetic.md" +const ART_AESTHETIC_PATH = resolve(claudeDir(), "LIFEOS/Aesthetic.md" ); const COLOR_HEX_MAP: Record = { diff --git a/LifeOS/install/skills/Art/Tools/PickExpression.ts b/LifeOS/install/skills/Art/Tools/PickExpression.ts index c705087305..7fe92cd189 100755 --- a/LifeOS/install/skills/Art/Tools/PickExpression.ts +++ b/LifeOS/install/skills/Art/Tools/PickExpression.ts @@ -20,8 +20,9 @@ import { existsSync, readdirSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; +import { claudeDir } from "../../../LIFEOS/TOOLS/lifeos-root"; -const DIR = join(homedir(), ".claude", "LIFEOS", "USER", "CUSTOMIZATIONS", "SKILLS", "Art", "HeadshotExamples"); +const DIR = join(claudeDir(), "LIFEOS", "USER", "CUSTOMIZATIONS", "SKILLS", "Art", "HeadshotExamples"); // sentiment -> headshot filename (without .png), with topic keywords that route to it. const MAP: Array<{ sentiment: string; file: string; keywords: string[] }> = [ diff --git a/LifeOS/install/skills/Art/Tools/ThumbnailText.ts b/LifeOS/install/skills/Art/Tools/ThumbnailText.ts index f1dc490dd2..8c174e6b80 100755 --- a/LifeOS/install/skills/Art/Tools/ThumbnailText.ts +++ b/LifeOS/install/skills/Art/Tools/ThumbnailText.ts @@ -32,6 +32,7 @@ import { execFileSync } from "node:child_process"; import { existsSync, rmSync } from "node:fs"; import { homedir } from "node:os"; import { join, parse } from "node:path"; +import { claudeDir } from "../../../LIFEOS/TOOLS/lifeos-root"; const W = 1280; const H = 720; @@ -41,7 +42,7 @@ const NAVY = "#1A2744"; const PERIWINKLE = "#6B8DD6"; const WHITE = "#FFFFFF"; const VARIANT_BORDER: Record = { core: "#316AE9", sponsored: "#306F1D" }; -const BRAND_LOGO = join(homedir(), ".claude", "LIFEOS", "USER", "CUSTOMIZATIONS", "SKILLS", "Art", "brand", "ti-logo-white.png"); +const BRAND_LOGO = join(claudeDir(), "LIFEOS", "USER", "CUSTOMIZATIONS", "SKILLS", "Art", "brand", "ti-logo-white.png"); function arg(name: string, def?: string): string | undefined { const i = process.argv.indexOf(`--${name}`); diff --git a/LifeOS/install/skills/Art/Workflows/AdHocYouTubeThumbnail.md b/LifeOS/install/skills/Art/Workflows/AdHocYouTubeThumbnail.md index 7a38dd0860..0ca7e2a929 100644 --- a/LifeOS/install/skills/Art/Workflows/AdHocYouTubeThumbnail.md +++ b/LifeOS/install/skills/Art/Workflows/AdHocYouTubeThumbnail.md @@ -128,7 +128,7 @@ Topic context: [EXTRACTED TOPIC] ### Generate Command ```bash -bun run ~/.claude/skills/Art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=AdHocYouTubeThumbnail \ --model nano-banana-pro \ --prompt "[BACKGROUND PROMPT]" \ @@ -212,11 +212,11 @@ Rembrandt lighting pattern. Looking at camera. Ultra-tight crop on face only. ```bash TIMESTAMP=$(date +%Y%m%d-%H%M%S) -bun ~/.claude/skills//Tools/Headshot.ts \ +bun "${LIFEOS_ROOT}/skills/"/Tools/Headshot.ts \ --prompt "[FACE-ONLY HEADSHOT PROMPT]" \ - --reference ~/.claude/skills//Examples/reference.png \ - --reference ~/.claude/skills//Examples/studio-style.png \ - --reference ~/.claude/skills//Examples/clean-smile.png \ + --reference "${LIFEOS_ROOT}/skills/"/Examples/reference.png \ + --reference "${LIFEOS_ROOT}/skills/"/Examples/studio-style.png \ + --reference "${LIFEOS_ROOT}/skills/"/Examples/clean-smile.png \ --size 2K \ --aspect-ratio 1:1 \ --output ~/Downloads/yt-headshot-${TIMESTAMP}.png @@ -227,7 +227,7 @@ bun ~/.claude/skills//Tools/Headshot.ts \ ### Remove Background ```bash -bun ~/.claude/LIFEOS/TOOLS/RemoveBg.ts ~/Downloads/yt-headshot-${TIMESTAMP}.png +bun "${LIFEOS_DIR}/TOOLS/RemoveBg.ts" ~/Downloads/yt-headshot-${TIMESTAMP}.png ``` --- @@ -239,7 +239,7 @@ bun ~/.claude/LIFEOS/TOOLS/RemoveBg.ts ~/Downloads/yt-headshot-${TIMESTAMP}.png ### Compose Command ```bash -bun ~/.claude/skills/Art/Tools/ComposeThumbnail.ts \ +bun "${LIFEOS_ROOT}/skills/Art/Tools/ComposeThumbnail.ts" \ --background ~/Downloads/yt-bg-${TIMESTAMP}.png \ --headshot ~/Downloads/yt-headshot-${TIMESTAMP}.png \ --title "[TITLE]" \ diff --git a/LifeOS/install/skills/Art/Workflows/AnnotatedScreenshots.md b/LifeOS/install/skills/Art/Workflows/AnnotatedScreenshots.md index a0be7721a8..c9dad12b0e 100644 --- a/LifeOS/install/skills/Art/Workflows/AnnotatedScreenshots.md +++ b/LifeOS/install/skills/Art/Workflows/AnnotatedScreenshots.md @@ -261,7 +261,7 @@ If generating combined image is difficult: **Option A: Generate combined (if model supports):** ```bash -bun run ~/.claude/skills/art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=AnnotatedScreenshots \ --model nano-banana-pro \ --reference-image /path/to/screenshot.png \ diff --git a/LifeOS/install/skills/Art/Workflows/Aphorisms.md b/LifeOS/install/skills/Art/Workflows/Aphorisms.md index d35342f6ba..8afebee7a7 100644 --- a/LifeOS/install/skills/Art/Workflows/Aphorisms.md +++ b/LifeOS/install/skills/Art/Workflows/Aphorisms.md @@ -239,7 +239,7 @@ Optional: Sign small in bottom right corner in charcoal (#2D2D2D). ### Step 5: Execute Generation ```bash -bun run ~/.claude/skills/art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=Aphorisms \ --model nano-banana-pro \ --prompt "[YOUR PROMPT]" \ diff --git a/LifeOS/install/skills/Art/Workflows/Comics.md b/LifeOS/install/skills/Art/Workflows/Comics.md index 4a0047f0d7..256961fcb3 100644 --- a/LifeOS/install/skills/Art/Workflows/Comics.md +++ b/LifeOS/install/skills/Art/Workflows/Comics.md @@ -310,7 +310,7 @@ Optional: Sign small in bottom right corner of final panel in charcoal (#2D2D2D) ### Step 5: Execute Generation ```bash -bun run ~/.claude/skills/art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=Comics \ --model nano-banana-pro \ --prompt "[YOUR PROMPT]" \ diff --git a/LifeOS/install/skills/Art/Workflows/Comparisons.md b/LifeOS/install/skills/Art/Workflows/Comparisons.md index b1dbcfa652..5b09339816 100644 --- a/LifeOS/install/skills/Art/Workflows/Comparisons.md +++ b/LifeOS/install/skills/Art/Workflows/Comparisons.md @@ -285,7 +285,7 @@ Optional: Sign small in bottom right corner in charcoal (#2D2D2D). ### Step 5: Execute Generation ```bash -bun run ~/.claude/skills/art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=Comparisons \ --model nano-banana-pro \ --prompt "[YOUR PROMPT]" \ diff --git a/LifeOS/install/skills/Art/Workflows/CreateLifeosPackIcon.md b/LifeOS/install/skills/Art/Workflows/CreateLifeosPackIcon.md index cb115f1ccb..7783fc9e74 100644 --- a/LifeOS/install/skills/Art/Workflows/CreateLifeosPackIcon.md +++ b/LifeOS/install/skills/Art/Workflows/CreateLifeosPackIcon.md @@ -91,7 +91,7 @@ BACKGROUND: Dark (#0a0a0f) - will be removed for transparency. **Command:** ```bash -bun run ~/.claude/skills/Art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=CreateLifeosPackIcon \ --model nano-banana-pro \ --prompt "[YOUR_PROMPT]" \ @@ -133,7 +133,7 @@ file ${PROJECTS_DIR}/LIFEOS/Packs/icons/[PACK_NAME].png ### Example 1: Hook System Pack ```bash -bun run ~/.claude/skills/Art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=CreateLifeosPackIcon \ --model nano-banana-pro \ --prompt "A stylized hook or fishing hook shape representing event hooks in software, simple flat icon design, 256x256 pixels. COLOR PALETTE: Primary electric blue (#4a90d9), Accent purple (#8b5cf6) sparingly. STYLE: Modern flat icon, simple enough to read at 64x64, no text, centered. BACKGROUND: Dark (#0a0a0f)." \ @@ -146,7 +146,7 @@ bun run ~/.claude/skills/Art/Tools/Generate.ts \ ### Example 2: Core Install Pack ```bash -bun run ~/.claude/skills/Art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=CreateLifeosPackIcon \ --model nano-banana-pro \ --prompt "A download arrow pointing into a foundation/base structure representing core installation, simple flat icon design, 256x256 pixels. COLOR PALETTE: Primary electric blue (#4a90d9), Accent purple (#8b5cf6) sparingly. STYLE: Modern flat icon, simple enough to read at 64x64, no text, centered. BACKGROUND: Dark (#0a0a0f)." \ @@ -159,7 +159,7 @@ bun run ~/.claude/skills/Art/Tools/Generate.ts \ ### Example 3: Memory System Pack ```bash -bun run ~/.claude/skills/Art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=CreateLifeosPackIcon \ --model nano-banana-pro \ --prompt "A brain with memory/data flowing in and out representing an AI memory system, simple flat icon design, 256x256 pixels. COLOR PALETTE: Primary electric blue (#4a90d9), Accent purple (#8b5cf6) sparingly. STYLE: Modern flat icon, simple enough to read at 64x64, no text, centered. BACKGROUND: Dark (#0a0a0f)." \ @@ -211,7 +211,7 @@ Before marking icon complete: ## Related Workflows -- `~/.claude/skills/_LIFEOS/Workflows/CreateRelease.md` - Release workflow (may include icon generation) +- `{{LIFEOS_ROOT}}/skills/_LIFEOS/Workflows/CreateRelease.md` - Release workflow (may include icon generation) *Note: Previously referenced CreatePack.md, ValidatePack.md, and LifeosIntegrityCheck.md have been removed.* diff --git a/LifeOS/install/skills/Art/Workflows/EmbossedLogoWallpaper.md b/LifeOS/install/skills/Art/Workflows/EmbossedLogoWallpaper.md index 398b6e4f94..65b61ae13b 100644 --- a/LifeOS/install/skills/Art/Workflows/EmbossedLogoWallpaper.md +++ b/LifeOS/install/skills/Art/Workflows/EmbossedLogoWallpaper.md @@ -144,7 +144,7 @@ open ~/Projects/Wallpaper/blue-purple-circuits.png ### Step 4: Generate ```bash -bun run ~/.claude/skills/Art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=EmbossedLogoWallpaper \ --model nano-banana-pro \ --prompt "[CONSTRUCTED_PROMPT]" \ diff --git a/LifeOS/install/skills/Art/Workflows/Essay.md b/LifeOS/install/skills/Art/Workflows/Essay.md index 49d04e5979..667cc88c52 100644 --- a/LifeOS/install/skills/Art/Workflows/Essay.md +++ b/LifeOS/install/skills/Art/Workflows/Essay.md @@ -151,8 +151,8 @@ Or use the slash command: **Read the aesthetic file and select the appropriate emotional vocabulary.** -```bash -Read ~/.claude/skills/Art/SKILL.md +```text +Read {{LIFEOS_ROOT}}/skills/Art/SKILL.md ``` **Match the contVent to one of these emotional registers:** @@ -617,7 +617,7 @@ The strict pipeline: ```bash # 1. GENERATE → ALWAYS to ~/Downloads/ -bun run ~/.claude/skills/Art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=Essay \ --model nano-banana-pro \ --prompt "[YOUR PROMPT]" \ @@ -650,7 +650,7 @@ cd ~/your-site && git add public/images/[name].* Based on user's request and the mapping tables above, construct the CLI command: ```bash -bun run ~/.claude/skills/Art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=Essay \ --model [SELECTED_MODEL from table] \ --prompt "[PROMPT from Step 5]" \ @@ -673,7 +673,7 @@ The `--thumbnail` flag generates TWO versions: ```bash # Example: Generates both my-header.png AND my-header-thumb.png in ~/Downloads/ # 🚨 --output MUST point to ~/Downloads/ — NEVER directly into cms/public/images/ -bun run ~/.claude/skills/Art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=Essay \ --model nano-banana-pro \ --prompt "[YOUR PROMPT]" \ @@ -720,10 +720,10 @@ For non-blog images that only need transparency, or to remove backgrounds after ```bash # Use the Images Skill for background removal -bun ~/.claude/LIFEOS/TOOLS/RemoveBg.ts /path/to/output.png +bun "${LIFEOS_DIR}/TOOLS/RemoveBg.ts" /path/to/output.png # Or batch 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 ``` @@ -782,7 +782,7 @@ magick ~/Downloads/[name]-trimmed.png -resize 1024x ~/Downloads/[name]-resized.p # Stage C — verify margins are now ≤ 2% on every edge (sanity check; any model whitespace inside # the bbox stays, but cropping has eliminated background bleed). -bun ~/.claude/skills/Art/Tools/FillFrame.ts \ +bun "${LIFEOS_ROOT}/skills/Art/Tools/FillFrame.ts" \ ~/Downloads/[name]-resized.png \ ~/Downloads/[name]-resized.png \ --report-only \ @@ -814,7 +814,7 @@ Generated images at 2K resolution (2048x2048) are 6-8MB each - far too large for # Step 7.0 (above) has already trimmed the image to its bbox. # 7.0.5 — CUT TO TRUE ALPHA (mandatory; the model output is an opaque JPEG) -bun ~/.claude/LIFEOS/TOOLS/RemoveBg.ts "~/Downloads/[name].jpg" # → ~/Downloads/[name].png with real alpha +bun "${LIFEOS_DIR}/TOOLS/RemoveBg.ts" "~/Downloads/[name].jpg" # → ~/Downloads/[name].png with real alpha magick "~/Downloads/[name].png" -trim +repage -resize 1024x "~/Downloads/[name].png" # 7.1 — "{{DA_NAME}}" SIGNATURE (human handwriting, NOT calligraphy) @@ -1134,7 +1134,7 @@ The cap exists because compute spent on 16+ failed generations is compute that s ``` 1. UNDERSTAND → Deeply read and comprehend the content 2. CSE-24 → Run Create Story Explanation (24 items) to extract narrative arc -3. EMOTION → Match to register in ~/.claude/LIFEOS/aesthetic.md +3. EMOTION → Match to register in {{LIFEOS_DIR}}/aesthetic.md 4. COMPOSITION → Design what to DRAW (content-relevant, NOT defaulting to architecture) 5. PROMPT → Build using charcoal sketch TECHNIQUE template 6. GENERATE → Execute with nano-banana-pro + --thumbnail flag diff --git a/LifeOS/install/skills/Art/Workflows/Frameworks.md b/LifeOS/install/skills/Art/Workflows/Frameworks.md index 1fba4b17ca..582b9e8991 100644 --- a/LifeOS/install/skills/Art/Workflows/Frameworks.md +++ b/LifeOS/install/skills/Art/Workflows/Frameworks.md @@ -276,7 +276,7 @@ Optional: Sign small in bottom right corner in charcoal (#2D2D2D). ### Step 5: Execute Generation ```bash -bun run ~/.claude/skills/art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=Frameworks \ --model nano-banana-pro \ --prompt "[YOUR PROMPT]" \ diff --git a/LifeOS/install/skills/Art/Workflows/LogoWallpaper.md b/LifeOS/install/skills/Art/Workflows/LogoWallpaper.md index 04df0ab0f7..319419948f 100644 --- a/LifeOS/install/skills/Art/Workflows/LogoWallpaper.md +++ b/LifeOS/install/skills/Art/Workflows/LogoWallpaper.md @@ -136,7 +136,7 @@ CRITICAL: ### Step 5: Generate Wallpaper ```bash -bun run ~/.claude/skills/Art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=LogoWallpaper \ --model nano-banana-pro \ --prompt "[CONSTRUCTED_PROMPT]" \ diff --git a/LifeOS/install/skills/Art/Workflows/Maps.md b/LifeOS/install/skills/Art/Workflows/Maps.md index 7673ea7c10..ba286a7017 100644 --- a/LifeOS/install/skills/Art/Workflows/Maps.md +++ b/LifeOS/install/skills/Art/Workflows/Maps.md @@ -329,7 +329,7 @@ Optional: Sign small in bottom corner in charcoal (#2D2D2D). ### Step 5: Execute Generation ```bash -bun run ~/.claude/skills/art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=Maps \ --model nano-banana-pro \ --prompt "[YOUR PROMPT]" \ diff --git a/LifeOS/install/skills/Art/Workflows/Mermaid.md b/LifeOS/install/skills/Art/Workflows/Mermaid.md index a2bda0c74a..7e8ff1cab6 100644 --- a/LifeOS/install/skills/Art/Workflows/Mermaid.md +++ b/LifeOS/install/skills/Art/Workflows/Mermaid.md @@ -646,7 +646,7 @@ Optional: Sign small in bottom right corner in charcoal (#2D2D2D). **Execute with optimal model for text-heavy diagrams:** ```bash -bun run ~/.claude/skills/art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=Mermaid \ --model nano-banana-pro \ --prompt "[YOUR COMPREHENSIVE PROMPT]" \ @@ -670,7 +670,7 @@ GOING INTO BLOG/WEBSITE: Remove background for transparency **For blog/website use** — use the **Images skill** for background removal: ```bash -bun ~/.claude/LIFEOS/TOOLS/RemoveBg.ts /path/to/mermaid-diagram.png +bun "${LIFEOS_DIR}/TOOLS/RemoveBg.ts" /path/to/mermaid-diagram.png ``` diff --git a/LifeOS/install/skills/Art/Workflows/RecipeCards.md b/LifeOS/install/skills/Art/Workflows/RecipeCards.md index f7bbe7f9f2..c4d0aae6fe 100644 --- a/LifeOS/install/skills/Art/Workflows/RecipeCards.md +++ b/LifeOS/install/skills/Art/Workflows/RecipeCards.md @@ -290,7 +290,7 @@ Optional: Sign small in bottom right corner in charcoal (#2D2D2D). ### Step 5: Execute Generation ```bash -bun run ~/.claude/skills/art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=RecipeCards \ --model nano-banana-pro \ --prompt "[YOUR PROMPT]" \ diff --git a/LifeOS/install/skills/Art/Workflows/RemoveBackground.md b/LifeOS/install/skills/Art/Workflows/RemoveBackground.md index 3bf375095f..8ad87bb68f 100644 --- a/LifeOS/install/skills/Art/Workflows/RemoveBackground.md +++ b/LifeOS/install/skills/Art/Workflows/RemoveBackground.md @@ -56,13 +56,13 @@ Use the LifeOS `RemoveBg.ts` wrapper, which calls local `rembg` and handles the ```bash # Single file (overwrites; renames .jpg→.png) -bun ~/.claude/LIFEOS/TOOLS/RemoveBg.ts input-image.png +bun "${LIFEOS_DIR}/TOOLS/RemoveBg.ts" input-image.png # Single file with explicit output path -bun ~/.claude/LIFEOS/TOOLS/RemoveBg.ts input-image.jpg output-image.png +bun "${LIFEOS_DIR}/TOOLS/RemoveBg.ts" input-image.jpg output-image.png # Batch (overwrites each in place) -bun ~/.claude/LIFEOS/TOOLS/RemoveBg.ts img1.png img2.png img3.png +bun "${LIFEOS_DIR}/TOOLS/RemoveBg.ts" img1.png img2.png img3.png ``` If you need to call `rembg` directly: @@ -104,13 +104,13 @@ cp output-image.png /destination/path/transparent-image.png ### Example 1: Remove background from a diagram ```bash -bun ~/.claude/LIFEOS/TOOLS/RemoveBg.ts ~/Downloads/TheAlgorithm.png +bun "${LIFEOS_DIR}/TOOLS/RemoveBg.ts" ~/Downloads/TheAlgorithm.png ``` ### Example 2: Remove background and save with new name ```bash -bun ~/.claude/LIFEOS/TOOLS/RemoveBg.ts \ +bun "${LIFEOS_DIR}/TOOLS/RemoveBg.ts" \ ~/your-site/public/images/logo-with-bg.png \ ~/your-site/public/images/logo-transparent.png ``` @@ -119,7 +119,7 @@ bun ~/.claude/LIFEOS/TOOLS/RemoveBg.ts \ ```bash cd ~/Downloads -bun ~/.claude/LIFEOS/TOOLS/RemoveBg.ts diagram-*.png +bun "${LIFEOS_DIR}/TOOLS/RemoveBg.ts" diagram-*.png ``` --- diff --git a/LifeOS/install/skills/Art/Workflows/Stats.md b/LifeOS/install/skills/Art/Workflows/Stats.md index 3c45f5336c..d61a63b40c 100644 --- a/LifeOS/install/skills/Art/Workflows/Stats.md +++ b/LifeOS/install/skills/Art/Workflows/Stats.md @@ -268,7 +268,7 @@ Optional: Sign small in bottom right corner in charcoal (#2D2D2D). ### Step 5: Execute Generation ```bash -bun run ~/.claude/skills/art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=Stats \ --model nano-banana-pro \ --prompt "[YOUR PROMPT]" \ diff --git a/LifeOS/install/skills/Art/Workflows/StyleMatchedThumbnail.md b/LifeOS/install/skills/Art/Workflows/StyleMatchedThumbnail.md index 6790c510b3..e5511d9eae 100644 --- a/LifeOS/install/skills/Art/Workflows/StyleMatchedThumbnail.md +++ b/LifeOS/install/skills/Art/Workflows/StyleMatchedThumbnail.md @@ -33,7 +33,7 @@ If a `REFERENCE` image is given, `Read` it and capture: dominant background grad ### 2. Pick the real expression-matched face ```bash -bun ~/.claude/skills/Art/Tools/PickExpression.ts --topic "" +bun "${LIFEOS_ROOT}/skills/Art/Tools/PickExpression.ts" --topic "" # or force it: --sentiment skeptical|neutral|positive|curious|thinking|disgust|shock|surprise|casual ``` @@ -41,7 +41,7 @@ Returns the path to a real expression-labeled headshot. `Read` it to confirm the *Optional `--generate-face`:* when the library has no fitting expression, generate a fresh expression FROM the real headshots as face refs (honors "a new face shot of me"): ```bash -bun ~/.claude/skills/Art/Tools/Generate.ts --workflow=StyleMatchedThumbnail --model nano-banana-pro \ +bun "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" --workflow=StyleMatchedThumbnail --model nano-banana-pro \ --size 2K --aspect-ratio 1:1 --no-signature \ --reference-image --reference-image \ --prompt "Photorealistic head-and-shoulders portrait of the man in the references, , looking at camera, plain near-black background, studio lighting, NO text." \ @@ -61,7 +61,7 @@ His real thumbnails are a **solid deep-navy field `#1A2744`** plus, optionally, **Solo layout** (Main-style: navy + supporting art + cutout face + text top-left): ```bash -bun ~/.claude/skills/Art/Tools/ThumbnailText.ts \ +bun "${LIFEOS_ROOT}/skills/Art/Tools/ThumbnailText.ts" \ --face "" --art "" \ --kicker "A DEEP DIVE ON MY" --title "PERSONAL AI" --subtitle "INFRASTRUCTURE" --tag "v2 (December 2025)" \ --variant core --face-side right \ @@ -70,7 +70,7 @@ bun ~/.claude/skills/Art/Tools/ThumbnailText.ts \ **Interview layout** (Sponsored/Main7-style: centered text + two framed stills + name labels): ```bash -bun ~/.claude/skills/Art/Tools/ThumbnailText.ts --mode interview \ +bun "${LIFEOS_ROOT}/skills/Art/Tools/ThumbnailText.ts" --mode interview \ --kicker "A CONVERSATION WITH" --title "GRANT LEE" --subtitle "ON BUILDING GAMMA" \ --face host.png --face2 guest.png --name1 "{{PRINCIPAL_FULL_NAME}}" --name2 "Grant Lee" \ --accent "#F5A623" --variant sponsored \ diff --git a/LifeOS/install/skills/Art/Workflows/Taxonomies.md b/LifeOS/install/skills/Art/Workflows/Taxonomies.md index 12f3b6e492..307827f02a 100644 --- a/LifeOS/install/skills/Art/Workflows/Taxonomies.md +++ b/LifeOS/install/skills/Art/Workflows/Taxonomies.md @@ -268,7 +268,7 @@ Optional: Sign small in bottom right corner in charcoal (#2D2D2D). ### Step 5: Execute Generation ```bash -bun run ~/.claude/skills/art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=Taxonomies \ --model nano-banana-pro \ --prompt "[YOUR PROMPT]" \ diff --git a/LifeOS/install/skills/Art/Workflows/TechnicalDiagrams.md b/LifeOS/install/skills/Art/Workflows/TechnicalDiagrams.md index 6f62dd12d4..f1c99f4b48 100644 --- a/LifeOS/install/skills/Art/Workflows/TechnicalDiagrams.md +++ b/LifeOS/install/skills/Art/Workflows/TechnicalDiagrams.md @@ -65,7 +65,7 @@ The workflow template below includes a title + subtitle block. **Override this w # Example image # Ignore for now -# ~/.claude/skills/Art/WorkflowExamples/TechnicalDiagrams/example.png +# {{LIFEOS_ROOT}}/skills/Art/WorkflowExamples/TechnicalDiagrams/example.png --- @@ -231,7 +231,7 @@ All the art components, labels, and such should mostly look hand-drawn, similar ### Generate Command ```bash -bun run ~/.claude/skills/Art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=TechnicalDiagrams \ --model [SELECTED_MODEL] \ --prompt "[PROMPT]" \ diff --git a/LifeOS/install/skills/Art/Workflows/Timelines.md b/LifeOS/install/skills/Art/Workflows/Timelines.md index cfff7fe669..c86341b8be 100644 --- a/LifeOS/install/skills/Art/Workflows/Timelines.md +++ b/LifeOS/install/skills/Art/Workflows/Timelines.md @@ -263,7 +263,7 @@ Optional: Sign small in bottom right corner in charcoal (#2D2D2D). ### Step 5: Execute Generation ```bash -bun run ~/.claude/skills/art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=Timelines \ --model nano-banana-pro \ --prompt "[YOUR PROMPT]" \ diff --git a/LifeOS/install/skills/Art/Workflows/Visualize.md b/LifeOS/install/skills/Art/Workflows/Visualize.md index e0a1de445b..8fe3c090da 100644 --- a/LifeOS/install/skills/Art/Workflows/Visualize.md +++ b/LifeOS/install/skills/Art/Workflows/Visualize.md @@ -98,7 +98,7 @@ TRANSPARENT: Use Images skill to remove background for overlay use **For transparent background** — use the **Images skill** for background removal: ```bash -bun ~/.claude/LIFEOS/TOOLS/RemoveBg.ts /path/to/visualization.png +bun "${LIFEOS_DIR}/TOOLS/RemoveBg.ts" /path/to/visualization.png ``` @@ -595,7 +595,7 @@ Optional: Sign small in bottom right corner in charcoal (#2D2D2D). **Construct command based on intent:** ```bash -bun run ~/.claude/skills/art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --workflow=Visualize \ --model [SELECTED_MODEL] \ --prompt "[YOUR COMPREHENSIVE PROMPT]" \ diff --git a/LifeOS/install/skills/AudioEditor/SKILL.md b/LifeOS/install/skills/AudioEditor/SKILL.md index 37cc6c0d54..5c5dbf8394 100644 --- a/LifeOS/install/skills/AudioEditor/SKILL.md +++ b/LifeOS/install/skills/AudioEditor/SKILL.md @@ -10,7 +10,7 @@ effort: medium ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/AudioEditor/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/AudioEditor/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -129,7 +129,7 @@ User: "aggressively clean this audio and polish it" After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"AudioEditor","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"AudioEditor","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/AudioEditor/Tools/Analyze.help.md b/LifeOS/install/skills/AudioEditor/Tools/Analyze.help.md index d809f184dc..440f329918 100644 --- a/LifeOS/install/skills/AudioEditor/Tools/Analyze.help.md +++ b/LifeOS/install/skills/AudioEditor/Tools/Analyze.help.md @@ -5,7 +5,7 @@ LLM-powered edit classification using Claude. Reads a word-level transcript and ## Usage ```bash -bun ~/.claude/skills/AudioEditor/Tools/Analyze.ts [--output ] [--aggressive] +bun "${LIFEOS_ROOT}/skills/AudioEditor/Tools/Analyze.ts" [--output ] [--aggressive] ``` ## Options diff --git a/LifeOS/install/skills/AudioEditor/Tools/Edit.help.md b/LifeOS/install/skills/AudioEditor/Tools/Edit.help.md index 6de8e43d49..5837a6b9be 100644 --- a/LifeOS/install/skills/AudioEditor/Tools/Edit.help.md +++ b/LifeOS/install/skills/AudioEditor/Tools/Edit.help.md @@ -5,7 +5,7 @@ Execute audio edits with ffmpeg. Reads an edit decision list and applies cuts wi ## Usage ```bash -bun ~/.claude/skills/AudioEditor/Tools/Edit.ts [--output ] +bun "${LIFEOS_ROOT}/skills/AudioEditor/Tools/Edit.ts" [--output ] ``` ## Options diff --git a/LifeOS/install/skills/AudioEditor/Tools/Pipeline.help.md b/LifeOS/install/skills/AudioEditor/Tools/Pipeline.help.md index 5ff0a0fd41..e975f5df13 100644 --- a/LifeOS/install/skills/AudioEditor/Tools/Pipeline.help.md +++ b/LifeOS/install/skills/AudioEditor/Tools/Pipeline.help.md @@ -5,7 +5,7 @@ End-to-end audio editing pipeline that chains all tools: Transcribe -> Analyze - ## Usage ```bash -bun ~/.claude/skills/AudioEditor/Tools/Pipeline.ts [options] +bun "${LIFEOS_ROOT}/skills/AudioEditor/Tools/Pipeline.ts" [options] ``` ## Options diff --git a/LifeOS/install/skills/AudioEditor/Tools/Polish.help.md b/LifeOS/install/skills/AudioEditor/Tools/Polish.help.md index e879a5801a..b6c213e769 100644 --- a/LifeOS/install/skills/AudioEditor/Tools/Polish.help.md +++ b/LifeOS/install/skills/AudioEditor/Tools/Polish.help.md @@ -5,7 +5,7 @@ Cleanvoice API cloud polish for final audio cleanup. ## Usage ```bash -bun ~/.claude/skills/AudioEditor/Tools/Polish.ts [--output ] +bun "${LIFEOS_ROOT}/skills/AudioEditor/Tools/Polish.ts" [--output ] ``` ## Options diff --git a/LifeOS/install/skills/AudioEditor/Tools/Polish.ts b/LifeOS/install/skills/AudioEditor/Tools/Polish.ts index 790cd38e74..420178aff2 100644 --- a/LifeOS/install/skills/AudioEditor/Tools/Polish.ts +++ b/LifeOS/install/skills/AudioEditor/Tools/Polish.ts @@ -22,8 +22,8 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { */ import { existsSync, readFileSync } from "fs"; -import { basename, dirname, extname, join, resolve } from "path"; -import { homedir } from "os"; +import { basename, dirname, extname, join } from "path"; +import { claudeDir } from "../../../LIFEOS/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,9 +37,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { // ============================================================================ function loadEnv(): void { - const envPath = process.env.LIFEOS_CONFIG_DIR - ? resolve(process.env.LIFEOS_CONFIG_DIR, ".env") - : resolve(homedir(), ".claude/.env"); + const envPath = join(claudeDir(), ".env"); try { const content = readFileSync(envPath, "utf-8"); for (const line of content.split("\n")) { diff --git a/LifeOS/install/skills/AudioEditor/Tools/Transcribe.help.md b/LifeOS/install/skills/AudioEditor/Tools/Transcribe.help.md index 43a77a3a4a..ec603afd0e 100644 --- a/LifeOS/install/skills/AudioEditor/Tools/Transcribe.help.md +++ b/LifeOS/install/skills/AudioEditor/Tools/Transcribe.help.md @@ -5,7 +5,7 @@ Word-level transcription via Whisper. Uses insanely-fast-whisper (MPS accelerate ## Usage ```bash -bun ~/.claude/skills/AudioEditor/Tools/Transcribe.ts [--output ] +bun "${LIFEOS_ROOT}/skills/AudioEditor/Tools/Transcribe.ts" [--output ] ``` ## Options diff --git a/LifeOS/install/skills/AudioEditor/Workflows/Clean.md b/LifeOS/install/skills/AudioEditor/Workflows/Clean.md index 611fd88711..67cea47b5b 100644 --- a/LifeOS/install/skills/AudioEditor/Workflows/Clean.md +++ b/LifeOS/install/skills/AudioEditor/Workflows/Clean.md @@ -36,7 +36,7 @@ Map the user's request to Pipeline.ts flags: ## Step 3: Run the Pipeline ```bash -bun ~/.claude/skills/AudioEditor/Tools/Pipeline.ts \ +bun "${LIFEOS_ROOT}/skills/AudioEditor/Tools/Pipeline.ts" \ "" \ [FLAGS_FROM_INTENT_MAPPING] \ --output "" @@ -63,14 +63,14 @@ For debugging or partial workflows, individual tools can be run standalone: ```bash # Transcription only -bun ~/.claude/skills/AudioEditor/Tools/Transcribe.ts +bun "${LIFEOS_ROOT}/skills/AudioEditor/Tools/Transcribe.ts" # Analysis only (requires transcript) -bun ~/.claude/skills/AudioEditor/Tools/Analyze.ts +bun "${LIFEOS_ROOT}/skills/AudioEditor/Tools/Analyze.ts" # Edit only (requires audio + edits) -bun ~/.claude/skills/AudioEditor/Tools/Edit.ts +bun "${LIFEOS_ROOT}/skills/AudioEditor/Tools/Edit.ts" # Polish only (requires CLEANVOICE_API_KEY) -bun ~/.claude/skills/AudioEditor/Tools/Polish.ts +bun "${LIFEOS_ROOT}/skills/AudioEditor/Tools/Polish.ts" ``` diff --git a/LifeOS/install/skills/BeCreative/SKILL.md b/LifeOS/install/skills/BeCreative/SKILL.md index 8c7a9b09ac..8a919a052b 100644 --- a/LifeOS/install/skills/BeCreative/SKILL.md +++ b/LifeOS/install/skills/BeCreative/SKILL.md @@ -8,7 +8,7 @@ effort: medium ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/BeCreative/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/BeCreative/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -146,7 +146,7 @@ User: "deep thinking this architecture problem" After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"BeCreative","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"BeCreative","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/BeCreative/Workflows/SyntheticDataExpansion.md b/LifeOS/install/skills/BeCreative/Workflows/SyntheticDataExpansion.md index c509818612..324d10161e 100644 --- a/LifeOS/install/skills/BeCreative/Workflows/SyntheticDataExpansion.md +++ b/LifeOS/install/skills/BeCreative/Workflows/SyntheticDataExpansion.md @@ -30,7 +30,7 @@ Running **SyntheticDataExpansion** in **BeCreative**... ## Outputs -Written to `~/.claude/LIFEOS/MEMORY/WORK/{slug}/synthetic-data/`: +Written to `{{LIFEOS_DIR}}/MEMORY/WORK/{slug}/synthetic-data/`: - `seed.json` — the original seed corpus - `schema.json` — the validation schema diff --git a/LifeOS/install/skills/BiasCheck/SKILL.md b/LifeOS/install/skills/BiasCheck/SKILL.md index 7100313f1d..b6435efd95 100644 --- a/LifeOS/install/skills/BiasCheck/SKILL.md +++ b/LifeOS/install/skills/BiasCheck/SKILL.md @@ -8,7 +8,7 @@ disallowed-tools: Edit, Write, NotebookEdit ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/BiasCheck/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/BiasCheck/` If this directory exists, load and apply any `PREFERENCES.md` or additional reference files found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -115,5 +115,5 @@ User: "bias check ~/Downloads/some-report.pdf" ## Execution Log ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"BiasCheck","workflow":"Check","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"BiasCheck","workflow":"Check","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` diff --git a/LifeOS/install/skills/BiasCheck/Workflows/Check.md b/LifeOS/install/skills/BiasCheck/Workflows/Check.md index b502c555be..ed9cd3abf7 100644 --- a/LifeOS/install/skills/BiasCheck/Workflows/Check.md +++ b/LifeOS/install/skills/BiasCheck/Workflows/Check.md @@ -75,7 +75,7 @@ If any of these are not stated, list it as "not disclosed" — non-disclosure is ## Step 4 — Three-Layer Analysis -Use the bias taxonomy at `BiasTaxonomy.md` as the catalog of categories. Load it with `Read ~/.claude/skills/BiasCheck/BiasTaxonomy.md`. Apply selectively — not every category fires for every source. +Use the bias taxonomy at `BiasTaxonomy.md` as the catalog of categories. Load it with `Read {{LIFEOS_ROOT}}/skills/BiasCheck/BiasTaxonomy.md`. Apply selectively — not every category fires for every source. ### Layer 1 — Biases inside the data @@ -214,5 +214,5 @@ Before delivering, verify: After completing, append: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"BiasCheck","workflow":"Check","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"BiasCheck","workflow":"Check","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` diff --git a/LifeOS/install/skills/BitterPillEngineering/SKILL.md b/LifeOS/install/skills/BitterPillEngineering/SKILL.md index bec5df46c4..2de4c571c6 100644 --- a/LifeOS/install/skills/BitterPillEngineering/SKILL.md +++ b/LifeOS/install/skills/BitterPillEngineering/SKILL.md @@ -8,7 +8,7 @@ effort: medium ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/BitterPillEngineering/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/BitterPillEngineering/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -156,5 +156,5 @@ Exception — four keep-classes are legitimate HOW, never cut them: **safety-gat After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"BitterPillEngineering","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"BitterPillEngineering","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` diff --git a/LifeOS/install/skills/BrightData/SKILL.md b/LifeOS/install/skills/BrightData/SKILL.md index ce7c98ba0a..8b019a9371 100644 --- a/LifeOS/install/skills/BrightData/SKILL.md +++ b/LifeOS/install/skills/BrightData/SKILL.md @@ -8,7 +8,7 @@ effort: medium ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/BrightData/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/BrightData/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -63,14 +63,14 @@ Crawl picks Light Crawl (MCP `scrape_batch` + link loop, ≤50 pages, ~$0.006/pa - **4-tier escalation: WebFetch → curl → Interceptor → Bright Data proxy.** Always start at Tier 1 and escalate only when blocked. Playwright is banned across LifeOS. - **Bright Data proxy has usage costs.** Don't use Tier 4 for sites accessible via Tier 1-3. - **CAPTCHA-solving introduces latency.** Allow extra time for Tier 4 responses. -- **Credentials in `~/.claude/.env`** — BRIGHTDATA_API_KEY. +- **Credentials in `{{LIFEOS_ROOT}}/.env`** — BRIGHTDATA_API_KEY. ## Execution Log After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"BrightData","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"BrightData","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/CMUX/DESIGN.md b/LifeOS/install/skills/CMUX/DESIGN.md index b95ccda4dd..faf87a8a53 100644 --- a/LifeOS/install/skills/CMUX/DESIGN.md +++ b/LifeOS/install/skills/CMUX/DESIGN.md @@ -30,7 +30,7 @@ Two facts shape everything downstream. **Mac-only**: no Linux path, which matter ## Feature map — HIS features (the source video) → how we implement -Every wrapper subcommand below is `bun ~/.claude/skills/CMUX/Tools/cmux.ts `. +Every wrapper subcommand below is `bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" `. | # | Feature (his) | cmux mechanism | Our wrapper subcommand | Status | |---|---------------|----------------|------------------------|--------| diff --git a/LifeOS/install/skills/CMUX/SKILL.md b/LifeOS/install/skills/CMUX/SKILL.md index 3f3b059dda..5f3eb26c78 100644 --- a/LifeOS/install/skills/CMUX/SKILL.md +++ b/LifeOS/install/skills/CMUX/SKILL.md @@ -8,7 +8,7 @@ description: "Drives cmux as an agent cockpit to boot, race, and monitor visible Make cmux the cockpit for every agent — LifeOS's own and your hands-on coding teams. One command boots a named, color-identified workspace of agents you can *see, prompt, and steer*, because an agent you can't see is an agent you can't improve. {{DA_NAME}} drives them through cmux's real send/read/open-close loop; a poll-based monitor speaks up when they finish. -Everything routes through one wrapper: `bun ~/.claude/skills/CMUX/Tools/cmux.ts `. It auto-launches the cmux app — but cmux's socket is **default-deny**, so driving it needs auth (see the first Gotcha). +Everything routes through one wrapper: `bun "${CLAUDE_SKILL_DIR}/Tools/cmux.ts" `. It auto-launches the cmux app — but cmux's socket is **default-deny**, so driving it needs auth (see the first Gotcha). > **Status (2026-07-07):** built and offline-verified — wrapper is type-clean (`tsc`/`bun build`), `voice` works live, public-clean grep passes, Kitty hooks untouched. **Live-driving (boot-team/race/fleet/monitor) is UNPROVEN** — it needs the socket-auth handshake, which has not yet executed. To prove it: run the wrapper *inside a cmux surface* (inherits auth), or set a cmux Settings socket password → `CMUX_SOCKET_PASSWORD`. **Security note:** a socket password lets any local process holding it drive your whole agent fleet — set it deliberately and never commit it to a public file. @@ -24,7 +24,7 @@ Everything routes through one wrapper: `bun ~/.claude/skills/CMUX/Tools/cmux.ts ## Quick Reference ```bash -CT=~/.claude/skills/CMUX/Tools/cmux.ts +CT="${CLAUDE_SKILL_DIR}/Tools/cmux.ts" bun $CT ping # ensure cmux is up (auto-launches) bun $CT boot-team --name debug --tiers orchestrator,lead,worker,worker bun $CT race --feature login-500 --agents 4 # first-to-solve wins @@ -48,7 +48,7 @@ bun $CT flash --workspace workspace:1 # visual attention - **Sidebar metadata is a no-auth Pulse bridge.** `report_meta` / `report_meta_block` / `set-status` / `set-progress` / `log` write agent status/progress into the workspace sidebar and persist to the session JSON at `~/Library/Application Support/cmux/session-*.json` — which is **readable without the socket**. LifeOS reads that file to mirror cmux agent state into Pulse without touching the auth wall. - **Mac-only.** cmux is a macOS app. The remote fleet still runs LifeOS, but cmux drives it via local SSH panes, not by running cmux on the minis. No Linux/WSL — that path is tmux. - **Refs are positional and can shift.** `workspace:1/surface:2` indexes move as you open/close things. For anything long-lived, resolve UUIDs (`--id-format uuids`) from `tree` and hold those. -- **Public skill — private specifics live in USER config.** The remote fleet's hosts come from `~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/CMUX/fleet.json` (`{"hosts":[{"name","ssh"}]}`), never from this skill's files. The socket password comes from `CMUX_SOCKET_PASSWORD`. +- **Public skill — private specifics live in USER config.** The remote fleet's hosts come from `{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/CMUX/fleet.json` (`{"hosts":[{"name","ssh"}]}`), never from this skill's files. The socket password comes from `CMUX_SOCKET_PASSWORD`. ## Examples diff --git a/LifeOS/install/skills/CMUX/Tools/cmux.ts b/LifeOS/install/skills/CMUX/Tools/cmux.ts index 2f45ae8a8a..36424a7d4d 100755 --- a/LifeOS/install/skills/CMUX/Tools/cmux.ts +++ b/LifeOS/install/skills/CMUX/Tools/cmux.ts @@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; +import { claudeDir } from "../../../LIFEOS/TOOLS/lifeos-root"; type FlagValue = string | boolean; type ParsedArgs = { @@ -657,7 +658,7 @@ function isHostConfig(value: unknown): value is HostConfig { } function loadFleetConfig(): HostConfig[] | JsonObject { - const configPath = join(homedir(), ".claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/CMUX/fleet.json"); + const configPath = join(claudeDir(), "LIFEOS/USER/CUSTOMIZATIONS/SKILLS/CMUX/fleet.json"); if (!existsSync(configPath)) { return { ok: false, diff --git a/LifeOS/install/skills/CMUX/Workflows/AgentRace.md b/LifeOS/install/skills/CMUX/Workflows/AgentRace.md index 542e0af116..e664e2539e 100644 --- a/LifeOS/install/skills/CMUX/Workflows/AgentRace.md +++ b/LifeOS/install/skills/CMUX/Workflows/AgentRace.md @@ -11,7 +11,7 @@ A production bug where any one agent might solve it — but you can't predict wh 1. **Launch the race.** One workspace, N surfaces, each running the same launch command against the same problem: ```bash - bun ~/.claude/skills/CMUX/Tools/cmux.ts race \ + bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" race \ --feature checkout-500 --agents 4 --cwd ~/Projects/App \ --cmd "claude 'The /checkout endpoint 500s on empty cart. Find and fix it.'" ``` @@ -32,13 +32,13 @@ A production bug where any one agent might solve it — but you can't predict wh 3. **Poll for a winner.** Watch all four; `monitor` classifies each surface idle/working/done/awaiting and fires {{DA_NAME}} voice the moment one hits `done`: ```bash - bun ~/.claude/skills/CMUX/Tools/cmux.ts monitor --workspace workspace:7 --interval 3 + bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" monitor --workspace workspace:7 --interval 3 ``` 4. **Capture the winning answer.** Read the surface that finished first: ```bash - bun ~/.claude/skills/CMUX/Tools/cmux.ts read --surface surface:32 --lines 80 + bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" read --surface surface:32 --lines 80 ``` Save the diff / explanation from `text`. That's the keeper. @@ -59,18 +59,18 @@ Serial retries pay the full latency of each failed attempt before you learn anyt ```bash # 1. six agents at the login regression, each free to pick its own theory -bun ~/.claude/skills/CMUX/Tools/cmux.ts race \ +bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" race \ --feature login-lockout --agents 6 --cwd ~/Projects/App \ --cmd "claude 'Prod: all logins fail with 401 since the last deploy. Root-cause and patch.'" # 2. watch; voice fires on first done -bun ~/.claude/skills/CMUX/Tools/cmux.ts monitor --workspace workspace:9 --interval 2 +bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" monitor --workspace workspace:9 --interval 2 # 3. race-4 solved it first — grab the fix -bun ~/.claude/skills/CMUX/Tools/cmux.ts read --surface surface:44 --lines 100 +bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" read --surface surface:44 --lines 100 # 4. close the other five, ship race-4's patch -bun ~/.claude/skills/CMUX/Tools/cmux.ts flash --workspace workspace:9 # mark the winner visually +bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" flash --workspace workspace:9 # mark the winner visually ``` Watching mechanics live in Monitor.md; named parallel teams (not racing the same problem) live in Fleet.md. diff --git a/LifeOS/install/skills/CMUX/Workflows/BootTeam.md b/LifeOS/install/skills/CMUX/Workflows/BootTeam.md index 0e59bad39d..46c0449a92 100644 --- a/LifeOS/install/skills/CMUX/Workflows/BootTeam.md +++ b/LifeOS/install/skills/CMUX/Workflows/BootTeam.md @@ -11,7 +11,7 @@ You want a hands-on coding team (or a LifeOS agent team) laid out so you can wat 1. **Boot the workspace.** One command lays out the whole team: ```bash - bun ~/.claude/skills/CMUX/Tools/cmux.ts boot-team \ + bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" boot-team \ --name auth-fix --cwd ~/Projects/App \ --tiers orchestrator,lead,worker,worker ``` @@ -32,7 +32,7 @@ You want a hands-on coding team (or a LifeOS agent team) laid out so you can wat 3. **Prompt the lead.** Send text and submit in one shot with `--enter`: ```bash - bun ~/.claude/skills/CMUX/Tools/cmux.ts send --surface surface:11 --enter \ + bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" send --surface surface:11 --enter \ "You lead this team. Break the JWT refresh bug into two tasks, hand one to each worker." ``` @@ -41,14 +41,14 @@ You want a hands-on coding team (or a LifeOS agent team) laid out so you can wat 4. **Fan out to workers.** Same pattern, one per worker surface: ```bash - bun ~/.claude/skills/CMUX/Tools/cmux.ts send --surface surface:12 --enter \ + bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" send --surface surface:12 --enter \ "Fix the token expiry check in src/auth/refresh.ts. Report back when green." ``` 5. **Read them back.** Round-trip to see what any agent said or is waiting on: ```bash - bun ~/.claude/skills/CMUX/Tools/cmux.ts read --surface surface:12 --lines 40 + bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" read --surface surface:12 --lines 40 ``` Returns `{ok:true,text:"..."}` — the tail of that surface's screen. @@ -68,15 +68,15 @@ The tiers are a convention for how you lay out and think about the team, not a r ```bash # 1. boot a 3-worker team over the repo -bun ~/.claude/skills/CMUX/Tools/cmux.ts boot-team \ +bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" boot-team \ --name testfix --cwd ~/Projects/App --tiers lead,worker,worker,worker # 2. tell the lead to triage (surface refs from the boot JSON) -bun ~/.claude/skills/CMUX/Tools/cmux.ts send --surface surface:21 --enter \ +bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" send --surface surface:21 --enter \ "Read the 3 failing suites, assign one per worker (surfaces 22/23/24), track their status." # 3. later, watch the whole team without switching windows -bun ~/.claude/skills/CMUX/Tools/cmux.ts monitor --workspace workspace:5 --once +bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" monitor --workspace workspace:5 --once ``` Hand off long-running watching to `monitor` (see Monitor.md); boot a fleet of parallel teams with Fleet.md. diff --git a/LifeOS/install/skills/CMUX/Workflows/Fleet.md b/LifeOS/install/skills/CMUX/Workflows/Fleet.md index ee100e69fc..88f4e7faa9 100644 --- a/LifeOS/install/skills/CMUX/Workflows/Fleet.md +++ b/LifeOS/install/skills/CMUX/Workflows/Fleet.md @@ -11,7 +11,7 @@ You want several agents laid out as a persistent, named team — `alpha`/`beta`/ 1. **Boot a 2x2.** One cmd per cell, semicolon-separated: ```bash - bun ~/.claude/skills/CMUX/Tools/cmux.ts fleet \ + bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" fleet \ --name alpha --grid 2x2 \ --cmds "claude 'watch src/api';claude 'watch src/web';bun test --watch;btop" ``` @@ -22,7 +22,7 @@ You want several agents laid out as a persistent, named team — `alpha`/`beta`/ ```bash # visual attention pulse - bun ~/.claude/skills/CMUX/Tools/cmux.ts flash --workspace alpha + bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" flash --workspace alpha # theme/banner via the raw CLI when you want a named look cmux workspace-action --action set-theme --workspace alpha --title "ALPHA · API team" ``` @@ -38,14 +38,14 @@ You want several agents laid out as a persistent, named team — `alpha`/`beta`/ Open one SSH pane per configured host — one workspace watching the whole fleet of remote boxes: ```bash -bun ~/.claude/skills/CMUX/Tools/cmux.ts mini-fleet +bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" mini-fleet ``` -Hosts come from `~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/CMUX/fleet.json` +Hosts come from `{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/CMUX/fleet.json` (shape `{"hosts":[{"name":"box-a","ssh":"user@box-a"}]}`). No hostnames live in the skill itself — the config is private and user-owned. Override ad hoc with `--hosts`: ```bash -bun ~/.claude/skills/CMUX/Tools/cmux.ts mini-fleet --hosts "user@box-a,user@box-b" +bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" mini-fleet --hosts "user@box-a,user@box-b" ``` Each host becomes its own SSH pane; from there you `send`/`read` exactly like a local surface. @@ -58,19 +58,19 @@ A fleet command IS the recipe — save the exact `fleet` / `mini-fleet` invocati ```bash # 1. local 2x2: api agent, web agent, test watcher, logs -bun ~/.claude/skills/CMUX/Tools/cmux.ts fleet \ +bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" fleet \ --name beta --grid 2x2 \ --cmds "claude 'implement /orders API';claude 'build orders UI';bun test --watch;tail -f dev.log" # 2. brand it + add a live preview browser -bun ~/.claude/skills/CMUX/Tools/cmux.ts flash --workspace beta +bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" flash --workspace beta cmux new-pane --type browser --direction right --workspace beta --url http://localhost:5173 # 3. spin up the remote mini-fleet to run the same feature on the fleet boxes -bun ~/.claude/skills/CMUX/Tools/cmux.ts mini-fleet +bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" mini-fleet # 4. watch all of it -bun ~/.claude/skills/CMUX/Tools/cmux.ts monitor --workspace beta --interval 3 +bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" monitor --workspace beta --interval 3 ``` Three-tier teams (lead/worker layout) live in BootTeam.md; the observe loop lives in Monitor.md. diff --git a/LifeOS/install/skills/CMUX/Workflows/Monitor.md b/LifeOS/install/skills/CMUX/Workflows/Monitor.md index 83708a8694..c7ba2990ef 100644 --- a/LifeOS/install/skills/CMUX/Workflows/Monitor.md +++ b/LifeOS/install/skills/CMUX/Workflows/Monitor.md @@ -15,7 +15,7 @@ cmux has no push or event-subscribe command. There is no "notify me when done" c 1. **Start the loop over a workspace:** ```bash - bun ~/.claude/skills/CMUX/Tools/cmux.ts monitor --workspace beta --interval 3 + bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" monitor --workspace beta --interval 3 ``` Each pass, per surface it runs `surface-health` + `read-screen` (tail) and classifies: @@ -35,7 +35,7 @@ cmux has no push or event-subscribe command. There is no "notify me when done" c 3. **One pass, no loop.** For a scripted spot-check (e.g. inside another workflow), `--once` does a single classification pass and exits: ```bash - bun ~/.claude/skills/CMUX/Tools/cmux.ts monitor --workspace beta --once + bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" monitor --workspace beta --once ``` ## How it feeds Pulse and voice @@ -46,7 +46,7 @@ cmux has no push or event-subscribe command. There is no "notify me when done" c ```bash # a 5-agent race is running in workspace:7 (see AgentRace.md) -bun ~/.claude/skills/CMUX/Tools/cmux.ts monitor --workspace workspace:7 --interval 2 +bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" monitor --workspace workspace:7 --interval 2 # ... you go do something else ... # {{DA_NAME}}: "workspace:7 race-3 finished" <- first done, voice fires ``` @@ -54,8 +54,8 @@ bun ~/.claude/skills/CMUX/Tools/cmux.ts monitor --workspace workspace:7 --interv Then pull the winner: ```bash -bun ~/.claude/skills/CMUX/Tools/cmux.ts read --surface surface:32 --lines 80 -bun ~/.claude/skills/CMUX/Tools/cmux.ts flash --workspace workspace:7 # mark it visually +bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" read --surface surface:32 --lines 80 +bun "${LIFEOS_ROOT}/skills/CMUX/Tools/cmux.ts" flash --workspace workspace:7 # mark it visually ``` Teams to monitor come from BootTeam.md (tiered) and Fleet.md (grids); the race pattern that pairs with hands-free monitoring is in AgentRace.md. diff --git a/LifeOS/install/skills/ContextSearch/SKILL.md b/LifeOS/install/skills/ContextSearch/SKILL.md index 00ab554fc5..62e7ac6836 100644 --- a/LifeOS/install/skills/ContextSearch/SKILL.md +++ b/LifeOS/install/skills/ContextSearch/SKILL.md @@ -11,7 +11,7 @@ effort: low ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/ContextSearch/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/ContextSearch/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -40,7 +40,7 @@ Single-tool skill — no `Workflows/` directory. Every invocation routes to the ## Run the search ```bash -bun run ~/.claude/skills/ContextSearch/Tools/ContextSearch.ts "$ARGUMENTS" --pretty --limit 10 +bun run "${CLAUDE_SKILL_DIR}/Tools/ContextSearch.ts" "$ARGUMENTS" --pretty --limit 10 ``` The tool auto-detects TTY for output mode; explicit `--pretty` keeps the human-readable block when the skill is invoked from inside a Claude Code session (where stdout is not a true TTY). Pipe to `--json` for structured output to jq. @@ -49,13 +49,13 @@ The tool auto-detects TTY for output mode; explicit `--pretty` keeps the human-r ```bash # Limit results -bun run ~/.claude/skills/ContextSearch/Tools/ContextSearch.ts "$ARGUMENTS" --limit 5 +bun run "${CLAUDE_SKILL_DIR}/Tools/ContextSearch.ts" "$ARGUMENTS" --limit 5 # Constrain by date -bun run ~/.claude/skills/ContextSearch/Tools/ContextSearch.ts "$ARGUMENTS" --since 2026-05-01 +bun run "${CLAUDE_SKILL_DIR}/Tools/ContextSearch.ts" "$ARGUMENTS" --since 2026-05-01 # JSON for programmatic use -bun run ~/.claude/skills/ContextSearch/Tools/ContextSearch.ts "$ARGUMENTS" --json | jq '.results[0]' +bun run "${CLAUDE_SKILL_DIR}/Tools/ContextSearch.ts" "$ARGUMENTS" --json | jq '.results[0]' ``` The tool also parses date phrases inline: `"yesterday markdown"` becomes a bounded since/until of yesterday only, plus token search for `markdown`. @@ -132,5 +132,5 @@ User: "what did I work on last week?" After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"ContextSearch","workflow":"Search","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"ContextSearch","workflow":"Search","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` diff --git a/LifeOS/install/skills/ContextSearch/Tools/ContextSearch.ts b/LifeOS/install/skills/ContextSearch/Tools/ContextSearch.ts index 1a95fce65d..f5de42b4b3 100755 --- a/LifeOS/install/skills/ContextSearch/Tools/ContextSearch.ts +++ b/LifeOS/install/skills/ContextSearch/Tools/ContextSearch.ts @@ -4,9 +4,10 @@ declare const Bun: any; import { readFileSync, statSync, existsSync, readdirSync } from "node:fs"; import { join, basename } from "node:path"; import { homedir } from "node:os"; +import { claudeDir } from "../../../LIFEOS/TOOLS/lifeos-root"; const HOME = homedir(); -const LIFEOS_DIR = join(HOME, ".claude"); +const LIFEOS_DIR = join(claudeDir()); const STATE_DIR = join(LIFEOS_DIR, "LIFEOS", "MEMORY", "STATE"); const WORK_DIR = join(LIFEOS_DIR, "LIFEOS", "MEMORY", "WORK"); // Claude Code names each project dir by its absolute path with "/" and "." mapped to "-", @@ -352,7 +353,7 @@ async function searchIsaBodies(tokens: string[], since: Date | null, until: Date async function searchJsonl(tokens: string[], since: Date | null, until: Date | null = null): Promise { if (tokens.length === 0 || !existsSync(JSONL_DIR)) return []; const realDir = JSONL_DIR; - if (!realDir.startsWith(join(HOME, ".claude", "Projects", PROJECT_SLUG))) return []; + if (!realDir.startsWith(join(claudeDir(), "Projects", PROJECT_SLUG))) return []; const pattern = tokens.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"); const out = await ripgrep(pattern, realDir, ["--glob", "*.jsonl", "-c"]); const byFile = new Map(); diff --git a/LifeOS/install/skills/Council/SKILL.md b/LifeOS/install/skills/Council/SKILL.md index 2c56cb2aad..89b75a4a89 100755 --- a/LifeOS/install/skills/Council/SKILL.md +++ b/LifeOS/install/skills/Council/SKILL.md @@ -9,7 +9,7 @@ context: fork ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Council/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/Council/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -133,7 +133,7 @@ Pure adversarial analysis is not a Council workflow — redirect to the RedTeam After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Council","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Council","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/CreateCLI/FrameworkComparison.md b/LifeOS/install/skills/CreateCLI/FrameworkComparison.md index aeba13ac8d..b7f3d79ffc 100755 --- a/LifeOS/install/skills/CreateCLI/FrameworkComparison.md +++ b/LifeOS/install/skills/CreateCLI/FrameworkComparison.md @@ -102,7 +102,7 @@ main().catch(error => { - ✅ Fast development priority ### Reference Implementation -**Location:** `~/.claude/LIFEOS/TOOLS/llcli/llcli.ts` (327 lines) +**Location:** `{{LIFEOS_DIR}}/TOOLS/llcli/llcli.ts` (327 lines) **Commands:** today, date, search **Pattern:** Exactly what this tier generates @@ -470,7 +470,7 @@ Check dist/ folder size. Tier 1 CLIs are <100 KB. --- **Sources:** -- llcli production implementation (~/.claude/LIFEOS/TOOLS/llcli/) +- llcli production implementation ({{LIFEOS_DIR}}/TOOLS/llcli/) - Commander.js 12.x documentation - oclif core documentation - Perplexity research (32 sub-queries on CLI frameworks) diff --git a/LifeOS/install/skills/CreateCLI/Patterns.md b/LifeOS/install/skills/CreateCLI/Patterns.md index 1b9775d5ff..806566de34 100755 --- a/LifeOS/install/skills/CreateCLI/Patterns.md +++ b/LifeOS/install/skills/CreateCLI/Patterns.md @@ -34,7 +34,7 @@ function loadConfig(): Config { ?.trim(); if (!apiKey) { - console.error('Error: API_KEY not found in ${LIFEOS_DIR}/.env'); + console.error('Error: API_KEY not found in {{LIFEOS_DIR}}/.env'); console.error('Add: API_KEY=your_key_here'); process.exit(1); } @@ -44,15 +44,15 @@ function loadConfig(): Config { baseUrl: process.env.API_BASE_URL || DEFAULTS.baseUrl, }; } catch (error) { - console.error('Error: Cannot read ${LIFEOS_DIR}/.env'); - console.error('Create file: touch ${LIFEOS_DIR}/.env'); + console.error('Error: Cannot read {{LIFEOS_DIR}}/.env'); + console.error('Create file: touch {{LIFEOS_DIR}}/.env'); process.exit(1); } } ``` **Key principles:** -- Load from ${LIFEOS_DIR}/.env (LifeOS standard) +- Load from {{LIFEOS_DIR}}/.env (LifeOS standard) - Clear error messages with resolution steps - Defaults for optional config - Type-safe Config interface @@ -238,7 +238,7 @@ OUTPUT: Exit code: 0 = success, 1 = error CONFIGURATION: - API Key: ${LIFEOS_DIR}/.env (API_KEY=your_key) + API Key: {{LIFEOS_DIR}}/.env (API_KEY=your_key) Base URL: ${DEFAULTS.baseUrl} PHILOSOPHY: @@ -249,7 +249,7 @@ PHILOSOPHY: - Documented: This help + README - Testable: Predictable behavior -For full documentation: ~/.claude/LIFEOS/TOOLS/${CLI_NAME}/README.md +For full documentation: {{LIFEOS_DIR}}/TOOLS/${CLI_NAME}/README.md Version: ${VERSION} `); } @@ -471,7 +471,7 @@ describe('CLI', () => { When building a CLI, use these patterns: -- [ ] Configuration loading (from ${LIFEOS_DIR}/.env) +- [ ] Configuration loading (from {{LIFEOS_DIR}}/.env) - [ ] API client with error handling - [ ] One function per command - [ ] Manual argument parsing (Tier 1) or Commander (Tier 2) diff --git a/LifeOS/install/skills/CreateCLI/SKILL.md b/LifeOS/install/skills/CreateCLI/SKILL.md index 44d70d320f..43a958b082 100644 --- a/LifeOS/install/skills/CreateCLI/SKILL.md +++ b/LifeOS/install/skills/CreateCLI/SKILL.md @@ -8,7 +8,7 @@ effort: medium ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/CreateCLI/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/CreateCLI/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -174,7 +174,7 @@ Generated CLIs follow LifeOS standards: ### Repository Placement Generated CLIs go to: -- `~/.claude/LIFEOS/TOOLS/[cli-name]/` - Personal CLIs (like llcli) +- `{{LIFEOS_DIR}}/TOOLS/[cli-name]/` - Personal CLIs (like llcli) - `~/Projects/[project-name]/` - Project-specific CLIs - `${PROJECTS_DIR}/LIFEOS/Examples/clis/` - Example CLIs (PUBLIC repo) @@ -216,7 +216,7 @@ Every generated CLI follows: **Generated Structure:** ``` -~/.claude/LIFEOS/TOOLS/ghcli/ +{{LIFEOS_DIR}}/TOOLS/ghcli/ ├── ghcli.ts # 350 lines, complete implementation ├── package.json # Bun + TypeScript ├── tsconfig.json # Strict mode @@ -242,7 +242,7 @@ ghcli --help **Generated Structure:** ``` -~/.claude/LIFEOS/TOOLS/md2html/ +{{LIFEOS_DIR}}/TOOLS/md2html/ ├── md2html.ts ├── package.json ├── README.md @@ -265,7 +265,7 @@ md2html extract-frontmatter post.md **Generated Structure:** ``` -~/.claude/LIFEOS/TOOLS/data-cli/ +{{LIFEOS_DIR}}/TOOLS/data-cli/ ├── data-cli.ts # Commander.js with subcommands ├── package.json ├── README.md @@ -373,7 +373,7 @@ The `llcli` CLI (Limitless.ai API) proves this pattern works: After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"CreateCLI","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"CreateCLI","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/CreateCLI/TypescriptPatterns.md b/LifeOS/install/skills/CreateCLI/TypescriptPatterns.md index afaf547e0a..30b1535a78 100755 --- a/LifeOS/install/skills/CreateCLI/TypescriptPatterns.md +++ b/LifeOS/install/skills/CreateCLI/TypescriptPatterns.md @@ -678,7 +678,7 @@ class CLIError extends Error { } function loadConfig(): Config { - // ... load from ${LIFEOS_DIR}/.env + // ... load from {{LIFEOS_DIR}}/.env throw new CLIError('API_KEY not found', 'ERR_NO_API_KEY'); } diff --git a/LifeOS/install/skills/CreateCLI/Workflows/AddCommand.md b/LifeOS/install/skills/CreateCLI/Workflows/AddCommand.md index 3eff9a9788..98eb6eb7ae 100755 --- a/LifeOS/install/skills/CreateCLI/Workflows/AddCommand.md +++ b/LifeOS/install/skills/CreateCLI/Workflows/AddCommand.md @@ -40,7 +40,7 @@ Add one or more commands to an existing CLI without breaking existing functional ```bash # Find CLI location -ls -la ~/.claude/LIFEOS/TOOLS/[cli-name]/ +ls -la "${LIFEOS_DIR}/TOOLS/[cli-name]/" # or ls -la ~/Projects/[project]/ ``` diff --git a/LifeOS/install/skills/CreateCLI/Workflows/CreateCli.md b/LifeOS/install/skills/CreateCLI/Workflows/CreateCli.md index cdc671f427..75379fa24d 100755 --- a/LifeOS/install/skills/CreateCLI/Workflows/CreateCli.md +++ b/LifeOS/install/skills/CreateCLI/Workflows/CreateCli.md @@ -202,7 +202,10 @@ const DEFAULTS = { * Load configuration from environment */ function loadConfig(): Config { - const envPath = process.env.LIFEOS_CONFIG_DIR ? join(process.env.LIFEOS_CONFIG_DIR, '.env') : join(homedir(), '.claude', 'LifeOS', '.env'); + const configRoot = process.env.LIFEOS_HOME + ?? process.env.CLAUDE_CONFIG_DIR + ?? (process.env.LIFEOS_DIR ? dirname(process.env.LIFEOS_DIR) : join(homedir(), '.claude')); + const envPath = join(configRoot, '.env'); try { const envContent = readFileSync(envPath, 'utf-8'); @@ -213,7 +216,7 @@ function loadConfig(): Config { ?.trim(); if (!apiKey) { - console.error('Error: {{ENV_VAR_NAME}} not found in ${LIFEOS_CONFIG_DIR}/.env'); + console.error(`Error: {{ENV_VAR_NAME}} not found in ${envPath}`); process.exit(1); } @@ -223,8 +226,8 @@ function loadConfig(): Config { {{ADDITIONAL_CONFIG}} }; } catch (error) { - console.error(`Error: Cannot read ${LIFEOS_CONFIG_DIR}/.env file`); - console.error('Make sure {{ENV_VAR_NAME}} is set in ${LIFEOS_CONFIG_DIR}/.env'); + console.error(`Error: Cannot read ${envPath}`); + console.error(`Make sure {{ENV_VAR_NAME}} is set in ${envPath}`); process.exit(1); } } @@ -276,7 +279,7 @@ const format = formatIdx !== -1 ? args[formatIdx + 1] : 'json'; 4. **Value flags**: `--flag ` for choices 5. **Composable**: Flags should combine logically -**Reference:** `~/.claude/LIFEOS/DOCUMENTATION/Tools/CliFirstArchitecture.md` (Configuration Flags section) +**Reference:** `{{LIFEOS_DIR}}/DOCUMENTATION/Tools/CliFirstArchitecture.md` (Configuration Flags section) --- @@ -371,7 +374,7 @@ PHILOSOPHY: - Documented: Full help and examples - Testable: Predictable behavior -For more information, see ~/.claude/LIFEOS/TOOLS/{{CLI_NAME}}/README.md +For more information, see {{LIFEOS_DIR}}/TOOLS/{{CLI_NAME}}/README.md Version: 1.0.0 `); @@ -537,7 +540,7 @@ main().catch((error) => { ## Full Documentation -See: ~/.claude/LIFEOS/TOOLS/{{CLI_NAME}}/README.md +See: {{LIFEOS_DIR}}/TOOLS/{{CLI_NAME}}/README.md ``` --- @@ -603,7 +606,7 @@ See: ~/.claude/LIFEOS/TOOLS/{{CLI_NAME}}/README.md **Validation Commands:** ```bash -cd ~/.claude/LIFEOS/TOOLS/{{CLI_NAME}}/ +cd "${LIFEOS_DIR}/TOOLS/"{{CLI_NAME}}/ chmod +x {{CLI_NAME}}.ts ./{{CLI_NAME}}.ts --help ./{{CLI_NAME}}.ts --version @@ -611,7 +614,7 @@ chmod +x {{CLI_NAME}}.ts **Report to user:** ``` -✅ CLI Created: ~/.claude/LIFEOS/TOOLS/{{CLI_NAME}}/ +✅ CLI Created: {{LIFEOS_DIR}}/TOOLS/{{CLI_NAME}}/ Files generated: - {{CLI_NAME}}.ts ({{LINE_COUNT}} lines) @@ -622,11 +625,11 @@ Files generated: - QUICKSTART.md Next steps: -1. Configure: Add {{ENV_VAR_NAME}} to ${LIFEOS_CONFIG_DIR}/.env +1. Configure: Add {{ENV_VAR_NAME}} to the LifeOS config root's `.env` 2. Test: ./{{CLI_NAME}}.ts --help 3. Use: ./{{CLI_NAME}}.ts {{EXAMPLE_COMMAND}} -Documentation: ~/.claude/LIFEOS/TOOLS/{{CLI_NAME}}/README.md +Documentation: {{LIFEOS_DIR}}/TOOLS/{{CLI_NAME}}/README.md ``` --- @@ -638,7 +641,7 @@ Documentation: ~/.claude/LIFEOS/TOOLS/{{CLI_NAME}}/README.md **Generated Output:** ``` -✅ CLI Created: ~/.claude/LIFEOS/TOOLS/notioncli/ +✅ CLI Created: {{LIFEOS_DIR}}/TOOLS/notioncli/ Files generated: - notioncli.ts (342 lines) @@ -655,9 +658,9 @@ Commands available: - notioncli --help # Show full help Next steps: -1. Add NOTION_API_KEY=your_key to ${LIFEOS_CONFIG_DIR}/.env +1. Add `NOTION_API_KEY=your_key` to the LifeOS config root's `.env` 2. Test: notioncli databases -3. Read: ~/.claude/LIFEOS/TOOLS/notioncli/README.md +3. Read: {{LIFEOS_DIR}}/TOOLS/notioncli/README.md The CLI follows llcli pattern with type safety, error handling, and comprehensive documentation. @@ -751,7 +754,7 @@ Show real usage examples, not just flag descriptions. Run `--help` and version command before reporting success. ### 8. **Follow llcli Pattern** -Use proven structure from ~/.claude/LIFEOS/TOOLS/llcli/ as reference. +Use proven structure from {{LIFEOS_DIR}}/TOOLS/llcli/ as reference. --- diff --git a/LifeOS/install/skills/CreateCLI/Workflows/UpgradeTier.md b/LifeOS/install/skills/CreateCLI/Workflows/UpgradeTier.md index 49fcb21332..0e120652fd 100755 --- a/LifeOS/install/skills/CreateCLI/Workflows/UpgradeTier.md +++ b/LifeOS/install/skills/CreateCLI/Workflows/UpgradeTier.md @@ -44,7 +44,7 @@ Convert Tier 1 CLI (llcli-style) to Tier 2 (Commander.js) when complexity demand ### 1. Install Commander.js ```bash -cd ~/.claude/LIFEOS/TOOLS/[cli-name]/ +cd "${LIFEOS_DIR}/TOOLS/[cli-name]/" bun add commander ``` diff --git a/LifeOS/install/skills/CreateSkill/SKILL.md b/LifeOS/install/skills/CreateSkill/SKILL.md index cdbe1ed9fa..9781728fb4 100644 --- a/LifeOS/install/skills/CreateSkill/SKILL.md +++ b/LifeOS/install/skills/CreateSkill/SKILL.md @@ -8,7 +8,7 @@ effort: medium ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/CreateSkill/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/CreateSkill/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -38,9 +38,9 @@ Complete skill development lifecycle: **structure** (create, validate, canonical ## Authoritative Source -**Before creating ANY skill, READ:** `~/.claude/LIFEOS/DOCUMENTATION/Skills/SkillSystem.md` +**Before creating ANY skill, READ:** `{{LIFEOS_DIR}}/DOCUMENTATION/Skills/SkillSystem.md` -**Canonical example to follow:** any well-formed public skill in `~/.claude/skills/` (e.g. `Research/SKILL.md`, `Daemon/SKILL.md`, `CreateSkill/SKILL.md` itself). +**Canonical example to follow:** any well-formed public skill in `{{LIFEOS_ROOT}}/skills/` (e.g. `Research/SKILL.md`, `Daemon/SKILL.md`, `CreateSkill/SKILL.md` itself). ## Naming Convention — Public vs Private @@ -68,7 +68,7 @@ Complete skill development lifecycle: **structure** (create, validate, canonical ### Choosing public vs private — the decision rule -Ask: **"Could this skill be dropped, as-is, into a stranger's `~/.claude/skills/` and just work?"** +Ask: **"Could this skill be dropped, as-is, into a stranger's `{{LIFEOS_ROOT}}/skills/` and just work?"** - **Yes** → public skill (`TitleCase`). Body must be generic; user-specific config layers in via `LIFEOS/USER/CUSTOMIZATIONS/SKILLS//`. - **No, because it references my identity, my contacts, my business, my customer, my paid API, my private infra, my domain, my private repo, my partner, or my financial/health/security data** → private skill (`_ALLCAPS`). @@ -124,13 +124,13 @@ If none of the above apply and the skill is fully generic — it can be `TitleCa ### Where Personal Layering Goes for Public Skills -A public skill can be made user-specific at runtime via `~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS//PREFERENCES.md`. The skill body stays generic; the user's customization file overlays per-instance context. Use this when a skill is fundamentally generic but benefits from per-user tweaks (preferred voice, default formats, personal taste). +A public skill can be made user-specific at runtime via `{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS//PREFERENCES.md`. The skill body stays generic; the user's customization file overlays per-instance context. Use this when a skill is fundamentally generic but benefits from per-user tweaks (preferred voice, default formats, personal taste). **Do not use SKILLCUSTOMIZATIONS to smuggle private content into a public skill.** If the skill *requires* private context to function (real customer name, real API account, real internal infra), it is a private skill — name it `_ALLCAPS` and stop. ### Allowed in Public Skills -- Generic `~/` paths (`~/.claude/skills/`, `~/Projects//`) — resolve per-user +- Generic `~/` paths (`{{LIFEOS_ROOT}}/skills/`, `~/Projects//`) — resolve per-user - Public repo URLs for tools the skill depends on - Public API endpoints that are conventions, not secrets (e.g., `localhost:31337/notify`) - Example values clearly marked as placeholders (``, ``, `test@example.com`) @@ -140,7 +140,7 @@ A public skill can be made user-specific at runtime via `~/.claude/LIFEOS/USER/C Before shipping or modifying any `TitleCase` skill, run: ```bash -rg -i "||||/Users/[a-z]+/" ~/.claude/skills// +rg -i "||||/Users/[a-z]+/" "${LIFEOS_ROOT}/skills/"/ ``` Zero matches = ready for public release. Any match = either scrub it, move it to SKILLCUSTOMIZATIONS, or rename the skill to `_ALLCAPS` and stop pretending it's public. **`_ALLCAPS` skills are exempt from this grep — they are private by design.** @@ -195,7 +195,7 @@ skills/SkillName/Tools/Utils/Helper.ts # THREE levels - NO **If you need to organize many workflows, use clear filenames instead of subdirectories:** -**See:** `~/.claude/LIFEOS/DOCUMENTATION/Skills/SkillSystem.md` (Flat Folder Structure section) +**See:** `{{LIFEOS_DIR}}/DOCUMENTATION/Skills/SkillSystem.md` (Flat Folder Structure section) --- @@ -308,7 +308,7 @@ Brief description. - **Efficiency:** Workflows load only what they actually need - **Maintainability:** Easier to update individual sections -**See:** `~/.claude/LIFEOS/DOCUMENTATION/Skills/SkillSystem.md` (Dynamic Loading Pattern section) +**See:** `{{LIFEOS_DIR}}/DOCUMENTATION/Skills/SkillSystem.md` (Dynamic Loading Pattern section) --- @@ -500,7 +500,7 @@ User: "The research skill output is too verbose — improve it" After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"CreateSkill","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"CreateSkill","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/CreateSkill/Workflows/CanonicalizeSkill.md b/LifeOS/install/skills/CreateSkill/Workflows/CanonicalizeSkill.md index 8452b2fc3b..3f1d917a3f 100755 --- a/LifeOS/install/skills/CreateSkill/Workflows/CanonicalizeSkill.md +++ b/LifeOS/install/skills/CreateSkill/Workflows/CanonicalizeSkill.md @@ -20,7 +20,7 @@ Running the **CanonicalizeSkill** workflow in the **CreateSkill** skill to restr **REQUIRED FIRST:** Read the canonical structure: ``` -~/.claude/LIFEOS/SkillSystem.md +{{LIFEOS_DIR}}/SkillSystem.md ``` This defines exactly what "canonicalize" means. @@ -30,7 +30,7 @@ This defines exactly what "canonicalize" means. ## Step 2: Read the Current Skill ```bash -~/.claude/skills/[skill-name]/SKILL.md +"${LIFEOS_ROOT}/skills/[skill-name]/SKILL.md" ``` Identify what's wrong: @@ -47,10 +47,10 @@ Identify what's wrong: ## Step 3: Backup ```bash -cp -r ~/.claude/skills/[skill-name]/ ~/.claude/History/Backups/[skill-name]-backup-$(date +%Y%m%d)/ +cp -r "${LIFEOS_ROOT}/skills/[skill-name]/" "${LIFEOS_ROOT}/History/Backups/[skill-name]-backup-$(date +%Y%m%d)/" ``` -**Note:** Backups go to `~/.claude/History/Backups/`, NEVER inside skill directories. +**Note:** Backups go to `{{LIFEOS_ROOT}}/History/Backups/`, NEVER inside skill directories. --- @@ -85,7 +85,7 @@ cp -r ~/.claude/skills/[skill-name]/ ~/.claude/History/Backups/[skill-name]-back **Rename files if needed:** ```bash # Example: rename workflow files -cd ~/.claude/skills/[SkillName]/Workflows/ +cd "${LIFEOS_ROOT}/skills/[SkillName]/Workflows/" mv create.md Create.md mv update-info.md UpdateInfo.md mv sync_repo.md SyncRepo.md @@ -103,7 +103,7 @@ Scan for folders deeper than 2 levels: ```bash # Find any folders 3+ levels deep (FORBIDDEN) -find ~/.claude/skills/[SkillName]/ -type d -mindepth 2 -maxdepth 3 +find "${LIFEOS_ROOT}/skills/[SkillName]/" -type d -mindepth 2 -maxdepth 3 ``` ### ❌ Common Violations to Fix @@ -224,7 +224,7 @@ If the markdown body already had routing information in a different format, cons List workflow files: ```bash -ls ~/.claude/skills/[SkillName]/Workflows/ +ls "${LIFEOS_ROOT}/skills/[SkillName]/Workflows/" ``` For EACH file: diff --git a/LifeOS/install/skills/CreateSkill/Workflows/CreateSkill.md b/LifeOS/install/skills/CreateSkill/Workflows/CreateSkill.md index be807e2de0..619fd2dfbb 100755 --- a/LifeOS/install/skills/CreateSkill/Workflows/CreateSkill.md +++ b/LifeOS/install/skills/CreateSkill/Workflows/CreateSkill.md @@ -17,8 +17,8 @@ Running the **CreateSkill** workflow in the **CreateSkill** skill to create new **REQUIRED FIRST:** -1. Read the skill system documentation: `~/.claude/LIFEOS/DOCUMENTATION/Skills/SkillSystem.md` -2. Read a canonical example skill — pick any existing public skill in `~/.claude/skills/` (e.g. `Research/SKILL.md`, `Daemon/SKILL.md`) and study its frontmatter, voice notification, workflow routing, and examples sections. +1. Read the skill system documentation: `{{LIFEOS_DIR}}/DOCUMENTATION/Skills/SkillSystem.md` +2. Read a canonical example skill — pick any existing public skill in `{{LIFEOS_ROOT}}/skills/` (e.g. `Research/SKILL.md`, `Daemon/SKILL.md`) and study its frontmatter, voice notification, workflow routing, and examples sections. ## Step 2: Understand the Request @@ -72,14 +72,14 @@ Before building, apply the bitter lesson test: **"Would a smarter model make thi ## Step 4: Create the Skill Directory ```bash -mkdir -p ~/.claude/skills/[SkillName]/Workflows -mkdir -p ~/.claude/skills/[SkillName]/Tools +mkdir -p "${LIFEOS_ROOT}/skills/[SkillName]/Workflows" +mkdir -p "${LIFEOS_ROOT}/skills/[SkillName]/Tools" ``` **Example:** ```bash -mkdir -p ~/.claude/skills/_DAEMON/Workflows -mkdir -p ~/.claude/skills/_DAEMON/Tools +mkdir -p "${LIFEOS_ROOT}/skills/_DAEMON/Workflows" +mkdir -p "${LIFEOS_ROOT}/skills/_DAEMON/Tools" ``` ## Step 5: Create SKILL.md @@ -114,7 +114,7 @@ description: [What it does]. USE WHEN [intent triggers using OR]. NOT FOR [confu Running **WorkflowName** in **SkillName**... ``` -**Full documentation:** `~/.claude/LIFEOS/DOCUMENTATION/Notifications/NotificationSystem.md` +**Full documentation:** `{{LIFEOS_DIR}}/DOCUMENTATION/Notifications/NotificationSystem.md` ## Workflow Routing @@ -172,7 +172,7 @@ This is template-level — new skills include it by default. Retrofit of existin ## Step 5b: Public Release Readiness (MANDATORY) -**Every skill in `~/.claude/skills/` ships with the LifeOS public release.** Write generic from the start — do not rely on a scrub at release-time. +**Every skill in `{{LIFEOS_ROOT}}/skills/` ships with the LifeOS public release.** Write generic from the start — do not rely on a scrub at release-time. ### Required @@ -182,13 +182,13 @@ This is template-level — new skills include it by default. Retrofit of existin ### Where Personal Context Belongs -User-specific preferences, project names, domain lists, and war stories go in `~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS//` — the skill body loads these at runtime via the Customization block. This keeps the public skill generic while each LifeOS user layers their own context. +User-specific preferences, project names, domain lists, and war stories go in `{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS//` — the skill body loads these at runtime via the Customization block. This keeps the public skill generic while each LifeOS user layers their own context. ### Pre-Flight Check Before finalizing, grep the skill for personal refs: ```bash -rg -i "danielmiessler|unsupervised|ULAdmin|thesurface|human3|ul\.live|/Users/[a-z]+/" ~/.claude/skills/[SkillName]/ +rg -i "danielmiessler|unsupervised|ULAdmin|thesurface|human3|ul\.live|/Users/[a-z]+/" "${LIFEOS_ROOT}/skills/[SkillName]/" ``` Zero matches = ready. Any match = replace with generic language or move to `SKILLCUSTOMIZATIONS/`. @@ -198,7 +198,7 @@ Zero matches = ready. Any match = replace with generic language or move to `SKIL For each workflow in the routing section: ```bash -touch ~/.claude/skills/[SkillName]/Workflows/[WorkflowName].md +touch "${LIFEOS_ROOT}/skills/[SkillName]/Workflows/[WorkflowName].md" ``` ### Workflow-to-Tool Integration (REQUIRED for workflows with CLI tools) @@ -240,23 +240,23 @@ bun ToolName.ts \ - Workflows should expose this flexibility, not hardcode single patterns - Users speak naturally; workflows translate to precise CLI -**Reference:** `~/.claude/LIFEOS/DOCUMENTATION/Tools/CliFirstArchitecture.md` (Workflow-to-Tool Integration section) +**Reference:** `{{LIFEOS_DIR}}/DOCUMENTATION/Tools/CliFirstArchitecture.md` (Workflow-to-Tool Integration section) **Examples (TitleCase):** ```bash -touch ~/.claude/skills/MyDaemon/Workflows/UpdateDaemonInfo.md -touch ~/.claude/skills/MyDaemon/Workflows/UpdatePublicRepo.md -touch ~/.claude/skills/MyBlog/Workflows/Create.md -touch ~/.claude/skills/MyBlog/Workflows/Publish.md +touch "${LIFEOS_ROOT}/skills/MyDaemon/Workflows/UpdateDaemonInfo.md" +touch "${LIFEOS_ROOT}/skills/MyDaemon/Workflows/UpdatePublicRepo.md" +touch "${LIFEOS_ROOT}/skills/MyBlog/Workflows/Create.md" +touch "${LIFEOS_ROOT}/skills/MyBlog/Workflows/Publish.md" ``` ## Step 7: Verify TitleCase Run this check: ```bash -ls ~/.claude/skills/[SkillName]/ -ls ~/.claude/skills/[SkillName]/Workflows/ -ls ~/.claude/skills/[SkillName]/Tools/ +ls "${LIFEOS_ROOT}/skills/[SkillName]/" +ls "${LIFEOS_ROOT}/skills/[SkillName]/Workflows/" +ls "${LIFEOS_ROOT}/skills/[SkillName]/Tools/" ``` Verify ALL files use TitleCase: diff --git a/LifeOS/install/skills/CreateSkill/Workflows/OptimizeDescription.md b/LifeOS/install/skills/CreateSkill/Workflows/OptimizeDescription.md index dcacaf484e..8cf0be5a27 100644 --- a/LifeOS/install/skills/CreateSkill/Workflows/OptimizeDescription.md +++ b/LifeOS/install/skills/CreateSkill/Workflows/OptimizeDescription.md @@ -85,7 +85,7 @@ This step matters — bad eval queries lead to bad descriptions. First, collect all skill names and descriptions: ```bash -rg '^(name|description):' ~/.claude/skills/*/SKILL.md ~/.claude/skills/*/*/SKILL.md --no-filename 2>/dev/null | head -200 +rg '^(name|description):' "${LIFEOS_ROOT}/skills/"*/SKILL.md "${LIFEOS_ROOT}/skills/"*/*/SKILL.md --no-filename 2>/dev/null | head -200 ``` Then spawn a **single** Agent subagent that evaluates ALL queries at once (batching avoids 20+ separate agent spawns): diff --git a/LifeOS/install/skills/CreateSkill/Workflows/TestSkill.md b/LifeOS/install/skills/CreateSkill/Workflows/TestSkill.md index cbfc53adec..a4bad8c0e7 100644 --- a/LifeOS/install/skills/CreateSkill/Workflows/TestSkill.md +++ b/LifeOS/install/skills/CreateSkill/Workflows/TestSkill.md @@ -22,7 +22,7 @@ Running the **TestSkill** workflow in the **CreateSkill** skill to test skill ef Read the target skill's SKILL.md: ``` -~/.claude/skills/[path]/SKILL.md +{{LIFEOS_ROOT}}/skills/[path]/SKILL.md ``` Note the skill's: diff --git a/LifeOS/install/skills/CreateSkill/Workflows/UpdateSkill.md b/LifeOS/install/skills/CreateSkill/Workflows/UpdateSkill.md index 1b3a6f8962..5bbb47e80c 100755 --- a/LifeOS/install/skills/CreateSkill/Workflows/UpdateSkill.md +++ b/LifeOS/install/skills/CreateSkill/Workflows/UpdateSkill.md @@ -20,7 +20,7 @@ Running the **UpdateSkill** workflow in the **CreateSkill** skill to modify exis **REQUIRED FIRST:** Read the canonical structure: ``` -~/.claude/LIFEOS/SkillSystem.md +{{LIFEOS_DIR}}/SkillSystem.md ``` --- @@ -28,7 +28,7 @@ Running the **UpdateSkill** workflow in the **CreateSkill** skill to modify exis ## Step 2: Read the Current Skill ```bash -~/.claude/skills/[SkillName]/SKILL.md +"${LIFEOS_ROOT}/skills/[SkillName]/SKILL.md" ``` Understand the current: @@ -57,12 +57,12 @@ What needs to change? 2. **Create the workflow file:** ```bash -touch ~/.claude/skills/[SkillName]/Workflows/[WorkflowName].md +touch "${LIFEOS_ROOT}/skills/[SkillName]/Workflows/[WorkflowName].md" ``` Example: ```bash -touch ~/.claude/skills/_DAEMON/Workflows/UpdatePublicRepo.md +touch "${LIFEOS_ROOT}/skills/_DAEMON/Workflows/UpdatePublicRepo.md" ``` 3. **Add entry to `## Workflow Routing` section in SKILL.md:** @@ -88,13 +88,13 @@ description: [What it does]. USE WHEN [updated intent triggers using OR]. [Capab 1. **Create TitleCase tool file:** ```bash -touch ~/.claude/skills/[SkillName]/Tools/ToolName.ts -touch ~/.claude/skills/[SkillName]/Tools/ToolName.help.md +touch "${LIFEOS_ROOT}/skills/[SkillName]/Tools/ToolName.ts" +touch "${LIFEOS_ROOT}/skills/[SkillName]/Tools/ToolName.help.md" ``` 2. **Ensure Tools/ directory exists:** ```bash -mkdir -p ~/.claude/skills/[SkillName]/Tools +mkdir -p "${LIFEOS_ROOT}/skills/[SkillName]/Tools" ``` --- @@ -104,8 +104,8 @@ mkdir -p ~/.claude/skills/[SkillName]/Tools After making changes, verify naming: ```bash -ls ~/.claude/skills/[SkillName]/Workflows/ -ls ~/.claude/skills/[SkillName]/Tools/ +ls "${LIFEOS_ROOT}/skills/[SkillName]/Workflows/" +ls "${LIFEOS_ROOT}/skills/[SkillName]/Tools/" ``` All files must use TitleCase: diff --git a/LifeOS/install/skills/CreateSkill/Workflows/ValidateSkill.md b/LifeOS/install/skills/CreateSkill/Workflows/ValidateSkill.md index 653fbb0b06..ffa4e34eb7 100755 --- a/LifeOS/install/skills/CreateSkill/Workflows/ValidateSkill.md +++ b/LifeOS/install/skills/CreateSkill/Workflows/ValidateSkill.md @@ -20,7 +20,7 @@ Running the **ValidateSkill** workflow in the **CreateSkill** skill to validate **REQUIRED FIRST:** Read the canonical structure: ``` -~/.claude/LIFEOS/DOCUMENTATION/Skills/SkillSystem.md +{{LIFEOS_DIR}}/DOCUMENTATION/Skills/SkillSystem.md ``` --- @@ -28,7 +28,7 @@ Running the **ValidateSkill** workflow in the **CreateSkill** skill to validate ## Step 2: Read the Target Skill ```bash -~/.claude/skills/[SkillName]/SKILL.md +"${LIFEOS_ROOT}/skills/[SkillName]/SKILL.md" ``` --- @@ -37,7 +37,7 @@ Running the **ValidateSkill** workflow in the **CreateSkill** skill to validate ### Skill Directory ```bash -ls ~/.claude/skills/ | grep -i [skillname] +ls "${LIFEOS_ROOT}/skills/" | grep -i [skillname] ``` Verify TitleCase: @@ -46,7 +46,7 @@ Verify TitleCase: ### Workflow Files ```bash -ls ~/.claude/skills/[SkillName]/Workflows/ +ls "${LIFEOS_ROOT}/skills/[SkillName]/Workflows/" ``` Verify TitleCase: @@ -55,7 +55,7 @@ Verify TitleCase: ### Tool Files ```bash -ls ~/.claude/skills/[SkillName]/Tools/ +ls "${LIFEOS_ROOT}/skills/[SkillName]/Tools/" ``` Verify TitleCase: @@ -148,7 +148,7 @@ Common confusable pairs to check: research-style skills (Research vs investigati Every skill ships with the LifeOS public release. Verify the skill is clean of personal/sensitive content: ```bash -rg -i "danielmiessler|unsupervised|ULAdmin|thesurface|human3|ul\.live|/Users/[a-z]+/" ~/.claude/skills/[SkillName]/ +rg -i "danielmiessler|unsupervised|ULAdmin|thesurface|human3|ul\.live|/Users/[a-z]+/" "${LIFEOS_ROOT}/skills/[SkillName]/" ``` **Check for violations:** @@ -158,7 +158,7 @@ rg -i "danielmiessler|unsupervised|ULAdmin|thesurface|human3|ul\.live|/Users/[a- - User-specific absolute paths (`/Users//...`) — use `~/` instead - Personal domain names (.example, .example, .example) — unless the skill is specifically about operating that domain -**Zero matches = PASS.** Any match = FAIL, recommend moving to `~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS//` or rewriting in generic language. +**Zero matches = PASS.** Any match = FAIL, recommend moving to `{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS//` or rewriting in generic language. --- @@ -176,7 +176,7 @@ Apply the bitter lesson test to the skill's instructions: ## Step 6: Check Workflow Files ```bash -ls ~/.claude/skills/[SkillName]/Workflows/ +ls "${LIFEOS_ROOT}/skills/[SkillName]/Workflows/" ``` Verify: @@ -190,7 +190,7 @@ Verify: ## Step 7: Check Structure ```bash -ls -la ~/.claude/skills/[SkillName]/ +ls -la "${LIFEOS_ROOT}/skills/[SkillName]/" ``` Verify: @@ -208,7 +208,7 @@ Verify: Check each tool for flag-based configuration: ```bash -bun ~/.claude/skills/[SkillName]/Tools/[ToolName].ts --help +bun "${LIFEOS_ROOT}/skills/[SkillName]/Tools/[ToolName].ts" --help ``` Verify the tool exposes behavioral configuration via flags: @@ -222,7 +222,7 @@ Verify the tool exposes behavioral configuration via flags: For workflows that call CLI tools, check for intent-to-flag mapping tables: ```bash -grep -l "Intent-to-Flag" ~/.claude/skills/[SkillName]/Workflows/*.md +grep -l "Intent-to-Flag" "${LIFEOS_ROOT}/skills/[SkillName]/Workflows/"*.md ``` **Required pattern in workflows with CLI tools:** @@ -235,7 +235,7 @@ grep -l "Intent-to-Flag" ~/.claude/skills/[SkillName]/Workflows/*.md | (default) | `--model sonnet` | Balanced | ``` -**Reference:** `~/.claude/LIFEOS/DOCUMENTATION/Tools/CliFirstArchitecture.md` +**Reference:** `{{LIFEOS_DIR}}/DOCUMENTATION/Tools/CliFirstArchitecture.md` --- diff --git a/LifeOS/install/skills/Daemon/SKILL.md b/LifeOS/install/skills/Daemon/SKILL.md index 95e5beb035..7589258032 100644 --- a/LifeOS/install/skills/Daemon/SKILL.md +++ b/LifeOS/install/skills/Daemon/SKILL.md @@ -8,7 +8,7 @@ effort: medium ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Daemon/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/Daemon/` If this directory exists, load and apply any SecurityOverrides.md or PREFERENCES.md found there. These override default security classification. If the directory does not exist, proceed with skill defaults. @@ -181,5 +181,5 @@ User: "preview daemon" After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Daemon","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Daemon","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` diff --git a/LifeOS/install/skills/Daemon/Tools/DaemonAggregator.ts b/LifeOS/install/skills/Daemon/Tools/DaemonAggregator.ts index 5b1066f6d8..9933638612 100755 --- a/LifeOS/install/skills/Daemon/Tools/DaemonAggregator.ts +++ b/LifeOS/install/skills/Daemon/Tools/DaemonAggregator.ts @@ -25,6 +25,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { import { readFileSync, existsSync, writeFileSync, readdirSync, statSync } from "fs"; import { join, resolve } from "path"; import { filterContent, filterDaemonData, loadSecurityOverrides } from "./SecurityFilter.ts"; +import { claudeDir } from "../../../LIFEOS/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"]) { @@ -36,7 +37,7 @@ for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { // ─── Path Resolution ─── const HOME = process.env.HOME || process.env.USERPROFILE || ""; -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 MEMORY_DIR = join(LIFEOS_DIR, "MEMORY"); const TELOS_DIR = join(USER_DIR, "TELOS"); @@ -395,7 +396,7 @@ function readExistingDaemon(): Record { const daemonPath = join(USER_DAEMON_DIR, "daemon.md"); if (!existsSync(daemonPath)) { // Fall back to old location - const oldPath = join(HOME, ".claude", "skills", "_DAEMON", "Mcp", "daemon.md"); + const oldPath = join(claudeDir(), "skills", "_DAEMON", "Mcp", "daemon.md"); if (!existsSync(oldPath)) return {}; return parseDaemonMd(readFileSync(oldPath, "utf-8")); } diff --git a/LifeOS/install/skills/Delegation/SKILL.md b/LifeOS/install/skills/Delegation/SKILL.md index 8e3c32510e..d34e35bf1d 100644 --- a/LifeOS/install/skills/Delegation/SKILL.md +++ b/LifeOS/install/skills/Delegation/SKILL.md @@ -266,7 +266,7 @@ User: "launch an agent team to research these 5 topics" After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Delegation","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Delegation","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/Evals/PROJECT.md b/LifeOS/install/skills/Evals/PROJECT.md index 970ef9574b..c6278c1134 100755 --- a/LifeOS/install/skills/Evals/PROJECT.md +++ b/LifeOS/install/skills/Evals/PROJECT.md @@ -733,7 +733,7 @@ evals backup --output evals-backup-2025-11-15.tar.gz ## File Structure ``` -~/.claude/skills/evals/ +{{LIFEOS_ROOT}}/skills/Evals/ ├── PROJECT.md # This file ├── SKILL.md # Skill definition │ diff --git a/LifeOS/install/skills/Evals/SKILL.md b/LifeOS/install/skills/Evals/SKILL.md index 52a1f2bfb5..b924822159 100644 --- a/LifeOS/install/skills/Evals/SKILL.md +++ b/LifeOS/install/skills/Evals/SKILL.md @@ -9,7 +9,7 @@ context: fork ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Evals/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/Evals/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -289,7 +289,7 @@ User: "run evals on the Research skill after the update" After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Evals","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Evals","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/Evals/TemplateIntegration.md b/LifeOS/install/skills/Evals/TemplateIntegration.md index 4c2646944d..c783b6880c 100755 --- a/LifeOS/install/skills/Evals/TemplateIntegration.md +++ b/LifeOS/install/skills/Evals/TemplateIntegration.md @@ -3,7 +3,7 @@ ## Available Templates ``` -~/.claude/Templates/Evals/ +{{LIFEOS_ROOT}}/Templates/Evals/ ├── Judge.hbs # Configurable LLM-as-Judge prompts ├── Rubric.hbs # Evaluation criteria definitions ├── TestCase.hbs # Test case specifications @@ -18,10 +18,10 @@ Use the JUDGE template for custom evaluation: ```bash -bun run ~/.claude/Templates/Tools/RenderTemplate.ts \ +bun run "${LIFEOS_ROOT}/Templates/Tools/RenderTemplate.ts" \ -t Evals/Judge.hbs \ - -d ~/.claude/skills/Evals/UseCases//judge-config.yaml \ - -o ~/.claude/skills/Evals/UseCases//judge-prompt.md + -d "${LIFEOS_ROOT}/skills/Evals/UseCases/"/judge-config.yaml \ + -o "${LIFEOS_ROOT}/skills/Evals/UseCases/"/judge-prompt.md ``` ### Judge Config Example @@ -55,10 +55,10 @@ output: Use the RUBRIC template for scoring criteria: ```bash -bun run ~/.claude/Templates/Tools/RenderTemplate.ts \ +bun run "${LIFEOS_ROOT}/Templates/Tools/RenderTemplate.ts" \ -t Evals/Rubric.hbs \ - -d ~/.claude/skills/Evals/UseCases//rubric.yaml \ - -o ~/.claude/skills/Evals/UseCases//rubric.md + -d "${LIFEOS_ROOT}/skills/Evals/UseCases/"/rubric.yaml \ + -o "${LIFEOS_ROOT}/skills/Evals/UseCases/"/rubric.md ``` --- diff --git a/LifeOS/install/skills/Evals/Tools/AlgorithmBridge.ts b/LifeOS/install/skills/Evals/Tools/AlgorithmBridge.ts index a3c96bb91f..e6a9cc4385 100755 --- a/LifeOS/install/skills/Evals/Tools/AlgorithmBridge.ts +++ b/LifeOS/install/skills/Evals/Tools/AlgorithmBridge.ts @@ -13,6 +13,7 @@ import { join } from 'path'; import { parse as parseYaml } from 'yaml'; import { parseArgs } from 'util'; import { $ } from 'bun'; +import { claudeDir } from '../../../LIFEOS/TOOLS/lifeos-root'; const EVALS_DIR = join(import.meta.dir, '..'); // Run artifacts live outside the skill tree (runtime state, not skill content). @@ -159,8 +160,9 @@ export function formatForISC(result: AlgorithmEvalResult): string { */ export async function updateISCWithResult(result: AlgorithmEvalResult): Promise { const status = result.passed ? 'DONE' : 'BLOCKED'; + const iscManager = join(claudeDir(), 'skills', 'THEALGORITHM', 'Tools', 'ISCManager.ts'); - await $`bun run ~/.claude/skills/THEALGORITHM/Tools/ISCManager.ts update --row ${result.isc_row} --status ${status} --note "${formatForISC(result)}"`.quiet(); + await $`bun run ${iscManager} update --row ${result.isc_row} --status ${status} --note ${formatForISC(result)}`.quiet(); } // CLI interface diff --git a/LifeOS/install/skills/Evals/Workflows/CompareModels.md b/LifeOS/install/skills/Evals/Workflows/CompareModels.md index a58311a707..16dc16311d 100755 --- a/LifeOS/install/skills/Evals/Workflows/CompareModels.md +++ b/LifeOS/install/skills/Evals/Workflows/CompareModels.md @@ -46,7 +46,7 @@ models: ### Step 3: Create Model Comparison Config -Create `~/.claude/skills/Evals/UseCases//model-comparisons/.yaml`: +Create `{{LIFEOS_ROOT}}/skills/Evals/UseCases//model-comparisons/.yaml`: ```yaml model_comparison: @@ -98,18 +98,18 @@ Run the same suite once per model via `AlgorithmBridge.ts`, then collect the per ```bash # Sequential — three runs, one per model -bun run ~/.claude/skills/Evals/Tools/AlgorithmBridge.ts -s -claude -bun run ~/.claude/skills/Evals/Tools/AlgorithmBridge.ts -s -gpt -bun run ~/.claude/skills/Evals/Tools/AlgorithmBridge.ts -s -gemini +bun run "${LIFEOS_ROOT}/skills/Evals/Tools/AlgorithmBridge.ts" -s -claude +bun run "${LIFEOS_ROOT}/skills/Evals/Tools/AlgorithmBridge.ts" -s -gpt +bun run "${LIFEOS_ROOT}/skills/Evals/Tools/AlgorithmBridge.ts" -s -gemini # Parallel — same three runs in the background -bun run ~/.claude/skills/Evals/Tools/AlgorithmBridge.ts -s -claude & -bun run ~/.claude/skills/Evals/Tools/AlgorithmBridge.ts -s -gpt & -bun run ~/.claude/skills/Evals/Tools/AlgorithmBridge.ts -s -gemini & +bun run "${LIFEOS_ROOT}/skills/Evals/Tools/AlgorithmBridge.ts" -s -claude & +bun run "${LIFEOS_ROOT}/skills/Evals/Tools/AlgorithmBridge.ts" -s -gpt & +bun run "${LIFEOS_ROOT}/skills/Evals/Tools/AlgorithmBridge.ts" -s -gemini & wait ``` -Each run's `results.json` lands at `~/.claude/LIFEOS/MEMORY/STATE/Evals-Results/-//results.json`. Side-by-side comparison is done by reading those JSONs (`jq`) — there is no built-in cross-model comparison CLI in this skill. +Each run's `results.json` lands at `{{LIFEOS_DIR}}/MEMORY/STATE/Evals-Results/-//results.json`. Side-by-side comparison is done by reading those JSONs (`jq`) — there is no built-in cross-model comparison CLI in this skill. 5. View side-by-side results ### Step 5: Collect Results @@ -123,10 +123,10 @@ Results stored in: Use Report template: ```bash -bun run ~/.claude/Templates/Tools/RenderTemplate.ts \ +bun run "${LIFEOS_ROOT}/Templates/Tools/RenderTemplate.ts" \ -t Evals/Report.hbs \ - -d ~/.claude/LIFEOS/MEMORY/STATE/Evals-Results//models//summary.yaml \ - -o ~/.claude/LIFEOS/MEMORY/STATE/Evals-Results//models//report.md + -d "${LIFEOS_DIR}/MEMORY/STATE/Evals-Results/"/models//summary.yaml \ + -o "${LIFEOS_DIR}/MEMORY/STATE/Evals-Results/"/models//report.md ``` ### Step 7: Analyze Results diff --git a/LifeOS/install/skills/Evals/Workflows/ComparePrompts.md b/LifeOS/install/skills/Evals/Workflows/ComparePrompts.md index 7100b7cdd2..b7cb05e05c 100755 --- a/LifeOS/install/skills/Evals/Workflows/ComparePrompts.md +++ b/LifeOS/install/skills/Evals/Workflows/ComparePrompts.md @@ -67,7 +67,7 @@ Ask the user: ```bash # Check prompts exist -ls ~/.claude/skills/Evals/UseCases//prompts/ +ls "${LIFEOS_ROOT}/skills/Evals/UseCases/"/prompts/ # Should see both versions: # v1.0.0.md @@ -76,7 +76,7 @@ ls ~/.claude/skills/Evals/UseCases//prompts/ ### Step 3: Create Comparison Config -Create `~/.claude/skills/Evals/UseCases//comparisons/.yaml`: +Create `{{LIFEOS_ROOT}}/skills/Evals/UseCases//comparisons/.yaml`: ```yaml comparison: @@ -120,8 +120,8 @@ Run the suite once per prompt version via `AlgorithmBridge.ts`. The use-case con ```bash # Run v1.0.0 first, then v1.1.0 -bun run ~/.claude/skills/Evals/Tools/AlgorithmBridge.ts -s -v1.0.0 -bun run ~/.claude/skills/Evals/Tools/AlgorithmBridge.ts -s -v1.1.0 +bun run "${LIFEOS_ROOT}/skills/Evals/Tools/AlgorithmBridge.ts" -s -v1.0.0 +bun run "${LIFEOS_ROOT}/skills/Evals/Tools/AlgorithmBridge.ts" -s -v1.1.0 ``` Position-swap protection (for pairwise judges that may favor the first/second option presented) is implemented inside the judge config — see the `position_swap: true` flag on `pairwise_comparison` graders in the use-case `config.yaml`. The grader handles randomization; the runner doesn't need a separate flag. @@ -294,10 +294,10 @@ focus: "depth" For detailed comparison setup, use the Comparison template: ```bash -bun run ~/.claude/Templates/Tools/RenderTemplate.ts \ +bun run "${LIFEOS_ROOT}/Templates/Tools/RenderTemplate.ts" \ -t Evals/Comparison.hbs \ - -d ~/.claude/skills/Evals/UseCases//comparisons/.yaml \ - -o ~/.claude/skills/Evals/UseCases//comparisons/-setup.md \ + -d "${LIFEOS_ROOT}/skills/Evals/UseCases/"/comparisons/.yaml \ + -o "${LIFEOS_ROOT}/skills/Evals/UseCases/"/comparisons/-setup.md \ --preview ``` diff --git a/LifeOS/install/skills/Evals/Workflows/CreateJudge.md b/LifeOS/install/skills/Evals/Workflows/CreateJudge.md index 03d46bce51..6e61c6bab3 100755 --- a/LifeOS/install/skills/Evals/Workflows/CreateJudge.md +++ b/LifeOS/install/skills/Evals/Workflows/CreateJudge.md @@ -33,7 +33,7 @@ Ask the user: ### Step 2: Create Judge Config -Create `~/.claude/skills/Evals/UseCases//judge-config.yaml`: +Create `{{LIFEOS_ROOT}}/skills/Evals/UseCases//judge-config.yaml`: ```yaml judge: @@ -65,10 +65,10 @@ output: ### Step 3: Render Judge Prompt ```bash -bun run ~/.claude/Templates/Tools/RenderTemplate.ts \ +bun run "${LIFEOS_ROOT}/Templates/Tools/RenderTemplate.ts" \ -t Evals/Judge.hbs \ - -d ~/.claude/skills/Evals/UseCases//judge-config.yaml \ - -o ~/.claude/skills/Evals/UseCases//judge-prompt.md \ + -d "${LIFEOS_ROOT}/skills/Evals/UseCases/"/judge-config.yaml \ + -o "${LIFEOS_ROOT}/skills/Evals/UseCases/"/judge-prompt.md \ --preview ``` @@ -99,8 +99,8 @@ criteria: Run the suite (which contains the use case + judge) via `AlgorithmBridge.ts` and inspect the output: ```bash -bun run ~/.claude/skills/Evals/Tools/AlgorithmBridge.ts -s -cat ~/.claude/LIFEOS/MEMORY/STATE/Evals-Results///results.json | jq '.trials[0].graders' +bun run "${LIFEOS_ROOT}/skills/Evals/Tools/AlgorithmBridge.ts" -s +cat "${LIFEOS_DIR}/MEMORY/STATE/Evals-Results/"//results.json | jq '.trials[0].graders' ``` To exercise only a single test case while iterating on the judge, scope the suite config to one task in `UseCases//test-cases/` and re-run. diff --git a/LifeOS/install/skills/Evals/Workflows/CreateUseCase.md b/LifeOS/install/skills/Evals/Workflows/CreateUseCase.md index 62114e4d45..477047010b 100755 --- a/LifeOS/install/skills/Evals/Workflows/CreateUseCase.md +++ b/LifeOS/install/skills/Evals/Workflows/CreateUseCase.md @@ -34,12 +34,12 @@ Ask the user: ### Step 2: Create Use Case Directory ```bash -mkdir -p ~/.claude/skills/Evals/UseCases//{test-cases,golden-outputs,prompts} +mkdir -p "${LIFEOS_ROOT}/skills/Evals/UseCases/"/{test-cases,golden-outputs,prompts} ``` ### Step 3: Create Config File -Create `~/.claude/skills/Evals/UseCases//config.yaml`: +Create `{{LIFEOS_ROOT}}/skills/Evals/UseCases//config.yaml`: ```yaml name: @@ -97,7 +97,7 @@ models: ### Step 4: Create Initial Prompt Version -Create `~/.claude/skills/Evals/UseCases//prompts/v1.0.0.md`: +Create `{{LIFEOS_ROOT}}/skills/Evals/UseCases//prompts/v1.0.0.md`: ```markdown # Prompt v1.0.0 @@ -121,7 +121,7 @@ Create `~/.claude/skills/Evals/UseCases//prompts/v1.0.0.md`: ### Step 5: Create Test Cases -Create test cases in `~/.claude/skills/Evals/UseCases//test-cases/`: +Create test cases in `{{LIFEOS_ROOT}}/skills/Evals/UseCases//test-cases/`: Each test case is a YAML file: @@ -173,7 +173,7 @@ Golden outputs serve as: ### Step 7: Create README -Create `~/.claude/skills/Evals/UseCases//README.md`: +Create `{{LIFEOS_ROOT}}/skills/Evals/UseCases//README.md`: ```markdown # @@ -208,7 +208,7 @@ Create `~/.claude/skills/Evals/UseCases//README.md`: ## Running Evaluations \`\`\`bash -bun run ~/.claude/skills/Evals/Tools/AlgorithmBridge.ts -s +bun run {{LIFEOS_ROOT}}/skills/Evals/Tools/AlgorithmBridge.ts -s \`\`\` ## Version History @@ -220,18 +220,18 @@ bun run ~/.claude/skills/Evals/Tools/AlgorithmBridge.ts -s ```bash # Check structure -ls -la ~/.claude/skills/Evals/UseCases// +ls -la "${LIFEOS_ROOT}/skills/Evals/UseCases/"/ # Validate suite via SuiteManager -bun run ~/.claude/skills/Evals/Tools/SuiteManager.ts show +bun run "${LIFEOS_ROOT}/skills/Evals/Tools/SuiteManager.ts" show ``` ### Step 9: Run Initial Eval ```bash # Run first evaluation to verify setup -bun run ~/.claude/skills/Evals/Tools/AlgorithmBridge.ts -s -cat ~/.claude/LIFEOS/MEMORY/STATE/Evals-Results//$(ls -1t ~/.claude/LIFEOS/MEMORY/STATE/Evals-Results// | head -1)/results.json | jq '.summary' +bun run "${LIFEOS_ROOT}/skills/Evals/Tools/AlgorithmBridge.ts" -s +cat "${LIFEOS_DIR}/MEMORY/STATE/Evals-Results/"/$(ls -1t "${LIFEOS_DIR}/MEMORY/STATE/Evals-Results/"/ | head -1)/results.json | jq '.summary' ``` Review: diff --git a/LifeOS/install/skills/Evals/Workflows/RunEval.md b/LifeOS/install/skills/Evals/Workflows/RunEval.md index 98245553bc..1484f6a458 100755 --- a/LifeOS/install/skills/Evals/Workflows/RunEval.md +++ b/LifeOS/install/skills/Evals/Workflows/RunEval.md @@ -27,7 +27,7 @@ Running the **RunEval** workflow in the **Evals** skill to execute evaluation... ```bash # Check use case exists -ls ~/.claude/skills/Evals/UseCases//config.yaml +ls "${LIFEOS_ROOT}/skills/Evals/UseCases/"/config.yaml ``` If missing, redirect to `CreateUseCase.md` workflow. @@ -36,13 +36,13 @@ If missing, redirect to `CreateUseCase.md` workflow. ```bash # Run an eval suite via AlgorithmBridge (the canonical entry point) -bun run ~/.claude/skills/Evals/Tools/AlgorithmBridge.ts -s +bun run "${LIFEOS_ROOT}/skills/Evals/Tools/AlgorithmBridge.ts" -s # With ISC row binding (auto-updates the Algorithm ISC row with result): -bun run ~/.claude/skills/Evals/Tools/AlgorithmBridge.ts -s -r -u +bun run "${LIFEOS_ROOT}/skills/Evals/Tools/AlgorithmBridge.ts" -s -r -u # To see saturation status alongside the run: -bun run ~/.claude/skills/Evals/Tools/AlgorithmBridge.ts -s --show-saturation +bun run "${LIFEOS_ROOT}/skills/Evals/Tools/AlgorithmBridge.ts" -s --show-saturation ``` ### Step 3: Collect Results diff --git a/LifeOS/install/skills/Evals/Workflows/ViewResults.md b/LifeOS/install/skills/Evals/Workflows/ViewResults.md index cb82cf6cb2..062722022b 100755 --- a/LifeOS/install/skills/Evals/Workflows/ViewResults.md +++ b/LifeOS/install/skills/Evals/Workflows/ViewResults.md @@ -20,7 +20,7 @@ Running the **ViewResults** workflow in the **Evals** skill to display eval resu Per-run output (source of truth): ``` -~/.claude/LIFEOS/MEMORY/STATE/Evals-Results///results.json +{{LIFEOS_DIR}}/MEMORY/STATE/Evals-Results///results.json ``` Each `results.json` contains the run summary, per-trial scores, grader outputs, and failure details. The `LIFEOS/MEMORY/STATE/Evals-Results/` directory is the canonical store — query it with standard tools (`jq`, `rg`, `cat`). @@ -33,27 +33,27 @@ Each `results.json` contains the run summary, per-trial scores, grader outputs, ```bash # Show all runs for a use case (newest first) -ls -1t ~/.claude/LIFEOS/MEMORY/STATE/Evals-Results// +ls -1t "${LIFEOS_DIR}/MEMORY/STATE/Evals-Results/"/ # Or via SuiteManager -bun run ~/.claude/skills/Evals/Tools/SuiteManager.ts list +bun run "${LIFEOS_ROOT}/skills/Evals/Tools/SuiteManager.ts" list ``` ### Step 2: View latest run summary ```bash # Latest run results.json -LATEST=$(ls -1t ~/.claude/LIFEOS/MEMORY/STATE/Evals-Results// | head -1) -cat ~/.claude/LIFEOS/MEMORY/STATE/Evals-Results//$LATEST/results.json | jq '.summary' +LATEST=$(ls -1t "${LIFEOS_DIR}/MEMORY/STATE/Evals-Results/"/ | head -1) +cat "${LIFEOS_DIR}/MEMORY/STATE/Evals-Results/"/$LATEST/results.json | jq '.summary' # Or for a specific run -cat ~/.claude/LIFEOS/MEMORY/STATE/Evals-Results///results.json | jq '.summary' +cat "${LIFEOS_DIR}/MEMORY/STATE/Evals-Results/"//results.json | jq '.summary' ``` ### Step 3: Check saturation (when a suite is graduating capability → regression) ```bash -bun run ~/.claude/skills/Evals/Tools/SuiteManager.ts check-saturation +bun run "${LIFEOS_ROOT}/skills/Evals/Tools/SuiteManager.ts" check-saturation ``` ### Step 4: View per-trial scores or failure detail diff --git a/LifeOS/install/skills/ExtractWisdom/SKILL.md b/LifeOS/install/skills/ExtractWisdom/SKILL.md index d450bdbe6f..6eb8895a8f 100644 --- a/LifeOS/install/skills/ExtractWisdom/SKILL.md +++ b/LifeOS/install/skills/ExtractWisdom/SKILL.md @@ -8,7 +8,7 @@ effort: medium ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/ExtractWisdom/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/ExtractWisdom/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -222,7 +222,7 @@ User: "extract the key insights from this blog post" After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"ExtractWisdom","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"ExtractWisdom","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/Fabric/SKILL.md b/LifeOS/install/skills/Fabric/SKILL.md index 3ba56ebb6f..bd716ff5ef 100644 --- a/LifeOS/install/skills/Fabric/SKILL.md +++ b/LifeOS/install/skills/Fabric/SKILL.md @@ -8,7 +8,7 @@ effort: medium ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Fabric/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/Fabric/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -29,7 +29,7 @@ If this directory exists, load and apply any PREFERENCES.md, configurations, or Running the **WorkflowName** workflow in the **Fabric** skill to ACTION... ``` -**Full documentation:** `~/.claude/LIFEOS/DOCUMENTATION/Notifications/NotificationSystem.md` +**Full documentation:** `{{LIFEOS_DIR}}/DOCUMENTATION/Notifications/NotificationSystem.md` # Fabric @@ -208,7 +208,7 @@ Each pattern's `system.md` contains the full prompt that defines: After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Fabric","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Fabric","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/Fabric/Workflows/ExecutePattern.md b/LifeOS/install/skills/Fabric/Workflows/ExecutePattern.md index b6eba5405c..acafc0b14b 100644 --- a/LifeOS/install/skills/Fabric/Workflows/ExecutePattern.md +++ b/LifeOS/install/skills/Fabric/Workflows/ExecutePattern.md @@ -37,14 +37,14 @@ Read the pattern's system.md file: ```bash PATTERN_NAME="[selected_pattern]" -PATTERN_PATH="$HOME/.claude/skills/Fabric/Patterns/$PATTERN_NAME/system.md" +PATTERN_PATH="${LIFEOS_ROOT}/skills/Fabric/Patterns/$PATTERN_NAME/system.md" if [ -f "$PATTERN_PATH" ]; then cat "$PATTERN_PATH" else echo "Pattern not found: $PATTERN_NAME" echo "Available patterns:" - ls ~/.claude/skills/Fabric/Patterns/ | head -20 + ls "${LIFEOS_ROOT}/skills/Fabric/Patterns/" | head -20 fi ``` @@ -103,7 +103,7 @@ Use when native URL fetching fails. **Invocation** — run in the BACKGROUND so it does not block the summary: ```bash -bun ~/.claude/skills/_HARVEST/Tools/harvest.ts "" +bun "${LIFEOS_ROOT}/skills/_HARVEST/Tools/harvest.ts" "" ``` Use `Bash` with `run_in_background: true`. The CLI handles source detection (URL / YouTube / text), body fetch, Arbol classification, and executor dispatch — never re-implement the writer here. @@ -191,7 +191,7 @@ User Request │ ├─ "micro" or "tldr" → create_micro_summary │ └─ Default → summarize │ *(auto-harvest side-effect: any summarize-family pattern with a URL or - │ text ≥200 chars also fires `~/.claude/skills/_HARVEST/Tools/harvest.ts` + │ text ≥200 chars also fires `{{LIFEOS_ROOT}}/skills/_HARVEST/Tools/harvest.ts` │ in the background — see Step 4b)* │ ├─ Contains "threat model"? @@ -250,7 +250,7 @@ User Request **Pattern not found:** ``` -Pattern '[name]' not found in ~/.claude/skills/Fabric/Patterns/ +Pattern '[name]' not found in {{LIFEOS_ROOT}}/skills/Fabric/Patterns/ Similar patterns: - [suggestion 1] diff --git a/LifeOS/install/skills/Fabric/Workflows/UpdatePatterns.md b/LifeOS/install/skills/Fabric/Workflows/UpdatePatterns.md index 86f2944da5..774874292b 100644 --- a/LifeOS/install/skills/Fabric/Workflows/UpdatePatterns.md +++ b/LifeOS/install/skills/Fabric/Workflows/UpdatePatterns.md @@ -29,7 +29,7 @@ curl -s -X POST http://localhost:31337/notify \ ### Step 2: Check Current Pattern Count ```bash -CURRENT_COUNT=$(ls -1 ~/.claude/skills/Fabric/Patterns/ 2>/dev/null | wc -l | tr -d ' ') +CURRENT_COUNT=$(ls -1 "${LIFEOS_ROOT}/skills/Fabric/Patterns/" 2>/dev/null | wc -l | tr -d ' ') echo "Current patterns: $CURRENT_COUNT" ``` @@ -48,13 +48,13 @@ This updates patterns in `~/.config/fabric/patterns/`. Copy updated patterns to the Fabric skill's local storage: ```bash -rsync -av --delete ~/.config/fabric/patterns/ ~/.claude/skills/Fabric/Patterns/ +rsync -av --delete ~/.config/fabric/patterns/ "${LIFEOS_ROOT}/skills/Fabric/Patterns/" ``` ### Step 5: Report Results ```bash -NEW_COUNT=$(ls -1 ~/.claude/skills/Fabric/Patterns/ 2>/dev/null | wc -l | tr -d ' ') +NEW_COUNT=$(ls -1 "${LIFEOS_ROOT}/skills/Fabric/Patterns/" 2>/dev/null | wc -l | tr -d ' ') echo "" echo "Pattern update complete!" echo "Previous count: $CURRENT_COUNT" @@ -71,7 +71,7 @@ Confirm critical patterns are present: ```bash for pattern in extract_wisdom summarize create_threat_model analyze_claims; do - if [ -d ~/.claude/skills/Fabric/Patterns/$pattern ]; then + if [ -d "${LIFEOS_ROOT}/skills/Fabric/Patterns/$pattern" ]; then echo "✓ $pattern" else echo "✗ $pattern MISSING" @@ -96,7 +96,7 @@ else fi # Sync patterns -rsync -av --delete patterns/ ~/.claude/skills/Fabric/Patterns/ +rsync -av --delete patterns/ "${LIFEOS_ROOT}/skills/Fabric/Patterns/" # Cleanup cd /tmp && rm -rf fabric @@ -110,10 +110,10 @@ After update, verify with: ```bash # Count patterns -ls -1 ~/.claude/skills/Fabric/Patterns/ | wc -l +ls -1 "${LIFEOS_ROOT}/skills/Fabric/Patterns/" | wc -l # List recent additions (if patterns have dates) -ls -lt ~/.claude/skills/Fabric/Patterns/ | head -10 +ls -lt "${LIFEOS_ROOT}/skills/Fabric/Patterns/" | head -10 ``` --- diff --git a/LifeOS/install/skills/FirstPrinciples/SKILL.md b/LifeOS/install/skills/FirstPrinciples/SKILL.md index 3606d15541..a0e267314d 100644 --- a/LifeOS/install/skills/FirstPrinciples/SKILL.md +++ b/LifeOS/install/skills/FirstPrinciples/SKILL.md @@ -8,7 +8,7 @@ effort: high ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/FirstPrinciples/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/FirstPrinciples/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -146,7 +146,7 @@ When using FirstPrinciples, output should include: After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"FirstPrinciples","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"FirstPrinciples","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/HTML/SKILL.md b/LifeOS/install/skills/HTML/SKILL.md index aa7ae99a35..be968aa6dc 100644 --- a/LifeOS/install/skills/HTML/SKILL.md +++ b/LifeOS/install/skills/HTML/SKILL.md @@ -33,7 +33,7 @@ Turns whatever the session just produced into one self-contained, designed HTML ## Quick Reference -- Renderer: `bun ~/.claude/skills/HTML/Tools/Render.ts --json content.json --register dossier --out artifact.html` +- Renderer: `bun "${CLAUDE_SKILL_DIR}/Tools/Render.ts" --json content.json --register dossier --out artifact.html` - `--schema` prints the content JSON shape with an example; `--registers` lists registers. - Registers: `dossier` (dark ink-green / orange, condensed display + typewriter — evidence files, red teams, investigations) and `ledger` (dark navy / gold, old-style serif — reports, finance, plans, comparisons). Alternate between them so consecutive outputs don't converge; add new registers to Render.ts rather than hand-styling one-offs. - Output is Artifact-CSP safe: inline CSS, fonts embedded as data URIs from local font files, zero external requests. diff --git a/LifeOS/install/skills/HTML/Workflows/Render.md b/LifeOS/install/skills/HTML/Workflows/Render.md index cc47a0b054..361820b546 100644 --- a/LifeOS/install/skills/HTML/Workflows/Render.md +++ b/LifeOS/install/skills/HTML/Workflows/Render.md @@ -26,9 +26,9 @@ If the session contains no substantive output to render (nothing analyzed, resea ## Tool Contract ```bash -bun ~/.claude/skills/HTML/Tools/Render.ts --schema # content JSON shape + example -bun ~/.claude/skills/HTML/Tools/Render.ts --registers # available registers -bun ~/.claude/skills/HTML/Tools/Render.ts \ +bun "${LIFEOS_ROOT}/skills/HTML/Tools/Render.ts" --schema # content JSON shape + example +bun "${LIFEOS_ROOT}/skills/HTML/Tools/Render.ts" --registers # available registers +bun "${LIFEOS_ROOT}/skills/HTML/Tools/Render.ts" \ --json \ --register \ --out diff --git a/LifeOS/install/skills/Harvest/SKILL.md b/LifeOS/install/skills/Harvest/SKILL.md index 537dd5c839..a49d2e65de 100644 --- a/LifeOS/install/skills/Harvest/SKILL.md +++ b/LifeOS/install/skills/Harvest/SKILL.md @@ -43,7 +43,7 @@ Take one piece of content and ask a single question: **is there anything in here ``` harvest https://youtu.be/VIDEO_ID # → fabric -y transcript → extract candidates → map to LifeOS surfaces → ranked table + verdict -# → ALWAYS finishes: bun ~/.claude/skills/_HARVEST/Tools/harvest.ts → KNOWLEDGE note written +# → ALWAYS finishes: bun {{LIFEOS_ROOT}}/skills/_HARVEST/Tools/harvest.ts → KNOWLEDGE note written harvest https://someblog.com/post-on-agent-memory # → WebFetch body → same analysis @@ -57,5 +57,5 @@ harvest "long pasted idea about a new eval technique..." After completing the workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Harvest","workflow":"Harvest","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Harvest","workflow":"Harvest","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` diff --git a/LifeOS/install/skills/Harvest/Workflows/Harvest.md b/LifeOS/install/skills/Harvest/Workflows/Harvest.md index 98a3f0734e..c06ea18fb5 100644 --- a/LifeOS/install/skills/Harvest/Workflows/Harvest.md +++ b/LifeOS/install/skills/Harvest/Workflows/Harvest.md @@ -81,7 +81,7 @@ Rules for the report: Every harvest ends by preserving the source as a KNOWLEDGE note, regardless of how thin the mining verdict was: ```bash -bun ~/.claude/skills/_HARVEST/Tools/harvest.ts "" +bun "${LIFEOS_ROOT}/skills/_HARVEST/Tools/harvest.ts" "" ``` - Runs the canonical Arbol pipeline (`_F_HARVEST` classify → `HarvestExecutor.ts` write). Never write to `MEMORY/KNOWLEDGE/` by hand. diff --git a/LifeOS/install/skills/ISA/SKILL.md b/LifeOS/install/skills/ISA/SKILL.md index 728c0e9e67..53154bc12c 100644 --- a/LifeOS/install/skills/ISA/SKILL.md +++ b/LifeOS/install/skills/ISA/SKILL.md @@ -218,12 +218,12 @@ The Algorithm at OBSERVE invokes this skill to scaffold or read an ISA. The skil - PLAN: `Skill("ISA", "extract feature as ephemeral file")` → ephemeral excerpt. - LEARN: `Skill("ISA", "reconcile ")` → deterministic merge. -The Algorithm doctrine spec at `~/.claude/LIFEOS/ALGORITHM/v7.0.0.md` (or LATEST) governs invocation cadence. This skill is invocation-agnostic — it works the same whether called by the Algorithm or directly by the user. +The Algorithm doctrine spec at `{{LIFEOS_DIR}}/ALGORITHM/v7.0.0.md` (or LATEST) governs invocation cadence. This skill is invocation-agnostic — it works the same whether called by the Algorithm or directly by the user. --- ## Format spec cross-reference -The full ISA format spec lives at `~/.claude/LIFEOS/DOCUMENTATION/Isa/IsaFormat.md`. This skill implements that spec; if there is ever a contradiction, the format spec wins and this skill is updated to match. +The full ISA format spec lives at `{{LIFEOS_DIR}}/DOCUMENTATION/Isa/IsaFormat.md`. This skill implements that spec; if there is ever a contradiction, the format spec wins and this skill is updated to match. -The system-architecture doc — five identities, three-guardrail taxonomy, fourteen-section body, six workflows, two homes, subsystem relationships — lives at `~/.claude/LIFEOS/DOCUMENTATION/Isa/IsaSystem.md`. Read that for the conceptual frame; read this file (and `IsaFormat.md`) for the operational contract. +The system-architecture doc — five identities, three-guardrail taxonomy, fourteen-section body, six workflows, two homes, subsystem relationships — lives at `{{LIFEOS_DIR}}/DOCUMENTATION/Isa/IsaSystem.md`. Read that for the conceptual frame; read this file (and `IsaFormat.md`) for the operational contract. diff --git a/LifeOS/install/skills/ISA/Workflows/Scaffold.md b/LifeOS/install/skills/ISA/Workflows/Scaffold.md index 3ab061cfd7..6338a57469 100644 --- a/LifeOS/install/skills/ISA/Workflows/Scaffold.md +++ b/LifeOS/install/skills/ISA/Workflows/Scaffold.md @@ -21,8 +21,8 @@ Generate a fresh ISA from a prompt at a specified effort tier. The output is a p A markdown file at one of: - `/ISA.md` — when `project` is supplied (existing project ISA is read-extended, not overwritten) -- `~/.claude/LIFEOS/MEMORY/WORK/{slug}/ISA.md` — when no project (slug = `YYYYMMDD-HHMMSS_kebab-task-description`) -- `~/.claude/LIFEOS/MEMORY/WORK/{slug}/_ephemeral/.md` — when `ephemeral_feature` is set +- `{{LIFEOS_DIR}}/MEMORY/WORK/{slug}/ISA.md` — when no project (slug = `YYYYMMDD-HHMMSS_kebab-task-description`) +- `{{LIFEOS_DIR}}/MEMORY/WORK/{slug}/_ephemeral/.md` — when `ephemeral_feature` is set ## Procedure @@ -37,7 +37,7 @@ curl -s -X POST http://localhost:31337/notify \ ### Step 2 — Pick the canonical template -Always start by reading `~/.claude/skills/ISA/Examples/canonical-isa.md` for section headers and tone. For E1 reference, read `e1-minimal.md`. For E5 reference, read `e5-enterprise.md`. +Always start by reading `{{LIFEOS_ROOT}}/skills/ISA/Examples/canonical-isa.md` for section headers and tone. For E1 reference, read `e1-minimal.md`. For E5 reference, read `e5-enterprise.md`. ### Step 3 — Preserve principal-stated goal, then derive (Algorithm v7.0.0 R1) diff --git a/LifeOS/install/skills/Ideate/SKILL.md b/LifeOS/install/skills/Ideate/SKILL.md index 492801147d..5bc397eba5 100644 --- a/LifeOS/install/skills/Ideate/SKILL.md +++ b/LifeOS/install/skills/Ideate/SKILL.md @@ -9,7 +9,7 @@ context: fork ## Customization Before executing, check for user customizations at: -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Ideate/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/Ideate/` # Ideate — The Cognitive Progress Engine @@ -143,7 +143,7 @@ Loop Controller decides actual cycle count adaptively, not a fixed count. ## State Persistence -Each run persists to `~/.claude/LIFEOS/MEMORY/WORK/{slug}/ideate/`: +Each run persists to `{{LIFEOS_DIR}}/MEMORY/WORK/{slug}/ideate/`: ``` ideate/ @@ -279,5 +279,5 @@ When the LifeOS Algorithm sets `mode: ideate` (via `LIFEOS/ALGORITHM/ideate-loop After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Ideate","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Ideate","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` diff --git a/LifeOS/install/skills/Ideate/Workflows/Dream.md b/LifeOS/install/skills/Ideate/Workflows/Dream.md index 6a10f28254..259dacb2d2 100644 --- a/LifeOS/install/skills/Ideate/Workflows/Dream.md +++ b/LifeOS/install/skills/Ideate/Workflows/Dream.md @@ -62,5 +62,5 @@ curl -s -X POST http://localhost:31337/notify \ ## Execution Log ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Ideate","workflow":"Dream","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Ideate","workflow":"Dream","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` diff --git a/LifeOS/install/skills/Ideate/Workflows/FullCycle.md b/LifeOS/install/skills/Ideate/Workflows/FullCycle.md index c313171d7f..4bca0348b3 100644 --- a/LifeOS/install/skills/Ideate/Workflows/FullCycle.md +++ b/LifeOS/install/skills/Ideate/Workflows/FullCycle.md @@ -183,7 +183,7 @@ Runs once after the Loop Controller issues STOP. Analyzes the entire evolutionar ## State Persistence -Each run persists to `~/.claude/LIFEOS/MEMORY/WORK/{slug}/ideate/`. See `../SKILL.md` § "State Persistence" for the full directory layout and idea data structure. +Each run persists to `{{LIFEOS_DIR}}/MEMORY/WORK/{slug}/ideate/`. See `../SKILL.md` § "State Persistence" for the full directory layout and idea data structure. ## Final Output @@ -192,5 +192,5 @@ See `../SKILL.md` § "Final Output Format" for the markdown template. ## Execution Log ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Ideate","workflow":"FullCycle","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Ideate","workflow":"FullCycle","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` diff --git a/LifeOS/install/skills/Ideate/Workflows/Mate.md b/LifeOS/install/skills/Ideate/Workflows/Mate.md index 71194b1f88..6559f2e0ca 100644 --- a/LifeOS/install/skills/Ideate/Workflows/Mate.md +++ b/LifeOS/install/skills/Ideate/Workflows/Mate.md @@ -82,5 +82,5 @@ curl -s -X POST http://localhost:31337/notify \ ## Execution Log ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Ideate","workflow":"Mate","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Ideate","workflow":"Mate","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` diff --git a/LifeOS/install/skills/Ideate/Workflows/QuickCycle.md b/LifeOS/install/skills/Ideate/Workflows/QuickCycle.md index 23cb69de10..511996580d 100644 --- a/LifeOS/install/skills/Ideate/Workflows/QuickCycle.md +++ b/LifeOS/install/skills/Ideate/Workflows/QuickCycle.md @@ -66,5 +66,5 @@ No Insight Extractor (single cycle has no cross-cycle pattern to extract). No Lo ## Execution Log ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Ideate","workflow":"QuickCycle","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Ideate","workflow":"QuickCycle","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` diff --git a/LifeOS/install/skills/Ideate/Workflows/Steal.md b/LifeOS/install/skills/Ideate/Workflows/Steal.md index 2b6e60780d..7d3f955dba 100644 --- a/LifeOS/install/skills/Ideate/Workflows/Steal.md +++ b/LifeOS/install/skills/Ideate/Workflows/Steal.md @@ -70,5 +70,5 @@ curl -s -X POST http://localhost:31337/notify \ ## Execution Log ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Ideate","workflow":"Steal","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Ideate","workflow":"Steal","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` diff --git a/LifeOS/install/skills/Ideate/Workflows/Test.md b/LifeOS/install/skills/Ideate/Workflows/Test.md index 2cff3a73e9..5a8b2506a8 100644 --- a/LifeOS/install/skills/Ideate/Workflows/Test.md +++ b/LifeOS/install/skills/Ideate/Workflows/Test.md @@ -86,5 +86,5 @@ curl -s -X POST http://localhost:31337/notify \ ## Execution Log ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Ideate","workflow":"Test","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Ideate","workflow":"Test","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` diff --git a/LifeOS/install/skills/Interceptor/SKILL.md b/LifeOS/install/skills/Interceptor/SKILL.md index 6ab3d8dff5..9e45db0287 100644 --- a/LifeOS/install/skills/Interceptor/SKILL.md +++ b/LifeOS/install/skills/Interceptor/SKILL.md @@ -8,7 +8,7 @@ effort: medium ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Interceptor/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/Interceptor/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -50,7 +50,7 @@ Interceptor is a Chrome extension that operates through the actual browser UI pl **Tool:** `interceptor` CLI — Chrome/Brave extension that controls the real browser from inside, plus a macOS bridge that drives native apps, OS-level input, and full VM lifecycle. **Repo:** https://github.com/Hacker-Valley-Media/Interceptor **Install:** `~/Projects/interceptor` (built from source — see `Workflows/Update.md`) -**Chrome "Load unpacked" target:** `~/.claude/skills/Interceptor/Extension/` — a **pinned copy** (not a symlink) of upstream `extension/dist`, captured by `Tools/Pin.sh`; provenance recorded in `Extension/PINNED_FROM.txt` (source path, manifest version, content SHA256, timestamp). Chrome disables unpacked extensions on every manifest bump, so after any binary upgrade the copy must be **re-pinned and reloaded**. It does NOT auto-follow upstream. +**Chrome "Load unpacked" target:** `${CLAUDE_SKILL_DIR}/Extension/` — a **pinned copy** (not a symlink) of upstream `extension/dist`, captured by `Tools/Pin.sh`; provenance recorded in `Extension/PINNED_FROM.txt` (source path, manifest version, content SHA256, timestamp). Chrome disables unpacked extensions on every manifest bump, so after any binary upgrade the copy must be **re-pinned and reloaded**. It does NOT auto-follow upstream. **Pinned binary:** `0.22.2` — `interceptor --version` reports `0.22.2 (580a7de, 2026-07-04)`. Upstream is now a **three-surface** control plane: **Browser** + **macOS** + **iOS** (drive an owned, Developer-Mode iPhone via an on-device XCUITest runner over WiFi — `interceptor ios *`). ### Capabilities Overview — Six Verb Trees @@ -110,7 +110,7 @@ CDP-based browser automation gets detected by sites. Interceptor is a Chrome ext Before any `interceptor open|read|act|inspect|screenshot|navigate|tab|monitor|net|cookies|scroll|click|type` lands in Chrome, the workflow runs the gate. Prefer the **auto-recovering entry point** — it runs the gate and, if the test profile window just isn't open, launches it and re-verifies before returning: ```bash -bash ~/.claude/skills/Interceptor/Tools/EnsureTestProfile.sh # runs the gate; auto-launches the test profile on exit 5/6; prints READY on success +bash "${CLAUDE_SKILL_DIR}/Tools/EnsureTestProfile.sh" # runs the gate; auto-launches the test profile on exit 5/6; prints READY on success ``` `EnsureTestProfile.sh` wraps `PreflightIsolation.sh` (the raw gate — still callable directly when you want no auto-launch). Both exit non-zero on any unrecoverable failure; on non-zero, STOP and surface — never fall back to Default. The gate asserts these invariants: @@ -172,7 +172,7 @@ Operating rule: if the user asks for native and `status` reports `mode: browser- ### Prerequisites -- Chrome or Brave (or Edge/Vivaldi on supported platforms) running with the Interceptor extension loaded — load it once via `chrome://extensions/` → Developer Mode → "Load unpacked" → `~/.claude/skills/Interceptor/Extension/` +- Chrome or Brave (or Edge/Vivaldi on supported platforms) running with the Interceptor extension loaded — load it once via `chrome://extensions/` → Developer Mode → "Load unpacked" → `${CLAUDE_SKILL_DIR}/Extension/` - `interceptor` CLI in PATH (`/opt/homebrew/bin/interceptor`) - `interceptor-daemon` in PATH (`/opt/homebrew/bin/interceptor-daemon`) - Native messaging manifest registered (`bash ~/Projects/interceptor/scripts/install.sh --chrome --skip-extension`) @@ -315,7 +315,7 @@ Agent(subagent_type="general-purpose", prompt=" Refs use eN syntax (no @ prefix) from tree output. Treat refs as short-lived. Background-first: only --activate and app activate move focus. PROFILE ISOLATION (MANDATORY GATE): the FIRST action of this task is - bash ~/.claude/skills/Interceptor/Tools/PreflightIsolation.sh + bash "${CLAUDE_SKILL_DIR}/Tools/PreflightIsolation.sh" If that script exits non-zero, STOP and surface the message verbatim — do NOT fall back to operating against the Default profile, and do NOT use screencapture or osascript as substitutes. After the preflight returns OK, every browser @@ -373,9 +373,9 @@ Agent(subagent_type="general-purpose", prompt=" - **Multiple tabs at the same URL confuse routing without `--context`.** When two tabs both load `localhost:5180/`, `tab switch ` reports `ok` but the visually-active Chrome tab may not change. Either close duplicates, work from a freshly-opened single tab, or pass `--context ` to scope unambiguously. - **`eval` is CSP-blocked on most sites.** Use `eval --main` to run in the page's main world. Even with `--main`, strict CSP (`script-src 'self'`) still blocks string-eval; pass small expressions, avoid `Function`-constructor patterns. - **Bridge needs Sparkle.framework.** When rebuilding from source on Apple Silicon, the bridge won't load until `Sparkle.framework` is installed at `/usr/local/Frameworks/`. The running bridge is the `.app`-bundle binary at `~/.local/share/interceptor/interceptor-bridge.app/Contents/MacOS/interceptor-bridge` (LaunchAgent `com.interceptor.bridge`, plist `~/Library/LaunchAgents/com.interceptor.bridge.plist`), NOT `/usr/local/bin/interceptor-bridge` (stale copy). The Update workflow handles this; symptom of forgetting is `bridge: not running` with `dyld[*]: Library not loaded: @rpath/Sparkle.framework/...` in `/tmp/interceptor-bridge.stderr.log`. *(2026-05-03, bridge topology corrected 2026-06-17.)* -- **Manifest version bump = manual extension reload + re-pin required.** Chrome does not auto-reload unpacked extensions. `~/.claude/skills/Interceptor/Extension/` is a pinned COPY, not a symlink — it does not auto-follow upstream. After a binary upgrade, re-pin via the Update workflow, then delete the existing extension card in `chrome://extensions` and Load Unpacked again from `~/.claude/skills/Interceptor/Extension/`. The extension `key` is deterministic so the extension ID stays stable across reloads. +- **Manifest version bump = manual extension reload + re-pin required.** Chrome does not auto-reload unpacked extensions. `${CLAUDE_SKILL_DIR}/Extension/` is a pinned COPY, not a symlink — it does not auto-follow upstream. After a binary upgrade, re-pin via the Update workflow, then delete the existing extension card in `chrome://extensions` and Load Unpacked again from `${CLAUDE_SKILL_DIR}/Extension/`. The extension `key` is deterministic so the extension ID stays stable across reloads. - **"native port disconnected" is NOT a screenshot error.** It's the daemon logging that Chrome's Native-Messaging stdio port dropped (extension SW recycled / Chrome closed); the daemon survives and falls through to its WebSocket transport. If neither WS nor relay is up, commands queue (cap 50) and time out. **Fix = reconnect the extension** (reload the tab / re-open the configured browser), not restarting the daemon. -- **"screenshot-runner.js could not load" = per-frame injection failure.** (The old "html-to-image library not loaded" error is gone — v0.18.3 replaced the library with a native renderer.) The page disallows script injection (`chrome://`, Web Store, PDF viewer, strict-CSP frame), the tab navigated mid-inject, OR — most common after a binary upgrade — a **stale loaded extension** whose bundled runner doesn't match the daemon. **Fix = reload/re-pin the extension** (Load Unpacked from `~/.claude/skills/Interceptor/Extension/`). +- **"screenshot-runner.js could not load" = per-frame injection failure.** (The old "html-to-image library not loaded" error is gone — v0.18.3 replaced the library with a native renderer.) The page disallows script injection (`chrome://`, Web Store, PDF viewer, strict-CSP frame), the tab navigated mid-inject, OR — most common after a binary upgrade — a **stale loaded extension** whose bundled runner doesn't match the daemon. **Fix = reload/re-pin the extension** (Load Unpacked from `${CLAUDE_SKILL_DIR}/Extension/`). - **Daemon↔extension WebSocket can wedge in a half-alive state — try other verb trees BEFORE declaring Interceptor unusable.** Symptom: `status`/`contexts`/`tabs` answer (control-plane message types) while `screenshot`/`eval` hang at timeout (data-plane types). Each verb in the Capabilities Overview uses a different WebSocket message type, so a wedge on `screenshot` rarely affects `eval`, `net log`, `tree`, `read --markdown`, `monitor`, or `inspect`. Recovery ladder: (1) **swap the capture path** (pixel↔DOM-render — a different message type often unwedges) or substitute a verb from another capability class — `read --markdown` / `eval --main document.body.innerText` instead of `screenshot`, `net log` for failed requests; (2) one `pkill -f interceptor-daemon` + a single retry; (3) reload the extension (surface: *"Interceptor extension is wedged — please reload it from chrome://extensions/."*) and STOP. Only after all three fail, tell the operator verification cannot be captured — **never fall back to `screencapture` / `osascript`.** **The recurring mistake this catches: claiming "Interceptor is broken" after a single `screenshot` timeout when `eval` would have answered the question in one tool call.** *(2026-05-13, sharpened 2026-06-17.)* - **Dead-bridge self-heal (macOS `macos_*` paths only — browser screenshot does NOT need the bridge).** "Loaded" ≠ "running" — probe the process via `interceptor status`, not just `launchctl list`: ```bash @@ -422,5 +422,5 @@ Passes all major bot detection: After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Interceptor","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Interceptor","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` diff --git a/LifeOS/install/skills/Interceptor/Tools/Capture.sh b/LifeOS/install/skills/Interceptor/Tools/Capture.sh index 522c67f0c6..0f02aa8e72 100755 --- a/LifeOS/install/skills/Interceptor/Tools/Capture.sh +++ b/LifeOS/install/skills/Interceptor/Tools/Capture.sh @@ -29,7 +29,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -USER_PREFS="${HOME}/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env" +USER_PREFS="${LIFEOS_DIR:-$HOME/.claude/LIFEOS}/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env" usage() { cat <&2 - local log="${HOME}/.claude/LIFEOS/MEMORY/OBSERVABILITY/capture-guard.jsonl" + local log="${LIFEOS_DIR:-$HOME/.claude/LIFEOS}/MEMORY/OBSERVABILITY/capture-guard.jsonl" mkdir -p "$(dirname "$log")" 2>/dev/null || true printf '{"ts":"%s","event":"guard-skipped","reason":"%s","out":"%s"}\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$reason" "$OUT" >> "$log" 2>/dev/null || true } diff --git a/LifeOS/install/skills/Interceptor/Tools/EnsureTestProfile.sh b/LifeOS/install/skills/Interceptor/Tools/EnsureTestProfile.sh index 8c6f1c7d6c..d4f61526db 100755 --- a/LifeOS/install/skills/Interceptor/Tools/EnsureTestProfile.sh +++ b/LifeOS/install/skills/Interceptor/Tools/EnsureTestProfile.sh @@ -22,7 +22,7 @@ set -uo pipefail DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PREFS="${HOME}/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env" +PREFS="${LIFEOS_DIR:-$HOME/.claude/LIFEOS}/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env" # shellcheck disable=SC1090 [ -f "$PREFS" ] && . "$PREFS" diff --git a/LifeOS/install/skills/Interceptor/Tools/LaunchTestProfile.sh b/LifeOS/install/skills/Interceptor/Tools/LaunchTestProfile.sh index 18f39a791b..2dfca37c69 100755 --- a/LifeOS/install/skills/Interceptor/Tools/LaunchTestProfile.sh +++ b/LifeOS/install/skills/Interceptor/Tools/LaunchTestProfile.sh @@ -23,7 +23,7 @@ set -euo pipefail # resolves from the single canonical home (preferences.env), not a guessed # default. The preflight sources this too; this script must not rely on the # preflight having run first. -USER_PREFS="${HOME}/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env" +USER_PREFS="${LIFEOS_DIR:-$HOME/.claude/LIFEOS}/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env" if [ -f "$USER_PREFS" ]; then # shellcheck disable=SC1090 . "$USER_PREFS" diff --git a/LifeOS/install/skills/Interceptor/Tools/PreflightIsolation.sh b/LifeOS/install/skills/Interceptor/Tools/PreflightIsolation.sh index 7d6aa664a7..06f8e48bde 100755 --- a/LifeOS/install/skills/Interceptor/Tools/PreflightIsolation.sh +++ b/LifeOS/install/skills/Interceptor/Tools/PreflightIsolation.sh @@ -40,7 +40,7 @@ set -euo pipefail # Source per-machine USER customizations if present (Chrome profile dir name, # pinned context ID, working-profile deny-list). Lives outside the public skill # body so the skill stays generic. -USER_PREFS="${HOME}/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env" +USER_PREFS="${LIFEOS_DIR:-$HOME/.claude/LIFEOS}/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env" if [ -f "$USER_PREFS" ]; then # shellcheck disable=SC1090 . "$USER_PREFS" diff --git a/LifeOS/install/skills/Interceptor/Workflows/DriveRichEditor.md b/LifeOS/install/skills/Interceptor/Workflows/DriveRichEditor.md index e1bec96c29..896de25e5f 100644 --- a/LifeOS/install/skills/Interceptor/Workflows/DriveRichEditor.md +++ b/LifeOS/install/skills/Interceptor/Workflows/DriveRichEditor.md @@ -5,8 +5,8 @@ You are driving a rich editor: Canva, Google Docs, Google Slides, Sheets, Figma, ## Preflight Isolation Gate (MANDATORY first step) ```bash -source ~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env -bash ~/.claude/skills/Interceptor/Tools/PreflightIsolation.sh +source "${LIFEOS_DIR}/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env" +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/PreflightIsolation.sh" ``` This is the heaviest trusted-input workflow — dispatched events mutate live editor content, so a wrong-profile run can corrupt the operator's real documents. Non-zero exit → STOP and surface the message verbatim. Do not fall back to the Default profile. Every `interceptor` verb below passes `--context "$INTERCEPTOR_TEST_CONTEXT_ID"` (the pinned isolated context from `preferences.env`); the examples show it on the runnable blocks. Screenshots/thumbnails go through `Tools/Capture.sh` where a pixel capture is needed. diff --git a/LifeOS/install/skills/Interceptor/Workflows/LaunchTestProfile.md b/LifeOS/install/skills/Interceptor/Workflows/LaunchTestProfile.md index a73a7ba56a..eff95a9c29 100644 --- a/LifeOS/install/skills/Interceptor/Workflows/LaunchTestProfile.md +++ b/LifeOS/install/skills/Interceptor/Workflows/LaunchTestProfile.md @@ -43,7 +43,7 @@ In the new profile window: 1. Open `chrome://extensions`. 2. Toggle **Developer mode** on (top-right). 3. Click **Load unpacked**. -4. Select `~/.claude/skills/Interceptor/Extension` — a **pinned copy** (not a symlink) of upstream `extension/dist`, captured by `Tools/Pin.sh`; provenance in `Extension/PINNED_FROM.txt`. Chrome disables unpacked extensions on every manifest bump, so re-pin and reload after any binary upgrade — the copy does NOT auto-follow upstream. +4. Select `{{LIFEOS_ROOT}}/skills/Interceptor/Extension` — a **pinned copy** (not a symlink) of upstream `extension/dist`, captured by `Tools/Pin.sh`; provenance in `Extension/PINNED_FROM.txt`. Chrome disables unpacked extensions on every manifest bump, so re-pin and reload after any binary upgrade — the copy does NOT auto-follow upstream. 5. Confirm the Interceptor card appears in this profile. The extension's deterministic `key` field means the extension ID matches the one already in the Default profile — that's fine; the daemon allowlist already includes it. @@ -63,14 +63,14 @@ Save. The daemon now sees this connection as `interceptor-test` and routes comma Set the directory name in `preferences.env` — the single canonical home for it. Do NOT write it to `~/.zshrc` (that creates a second, unsynced source of truth the launcher and preflight can disagree with): ```bash -# Edit ~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env: +# Edit {{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env: # export INTERCEPTOR_TEST_CHROME_PROFILE="Profile 4" # whichever profile dir Chrome made ``` Or set it inline for a one-off launch (still keep `preferences.env` authoritative for daily use): ```bash -INTERCEPTOR_TEST_CHROME_PROFILE="Profile 4" bash ~/.claude/skills/Interceptor/Tools/LaunchTestProfile.sh +INTERCEPTOR_TEST_CHROME_PROFILE="Profile 4" bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/LaunchTestProfile.sh" ``` ### 6. Verify (via the canonical preflight) @@ -78,10 +78,10 @@ INTERCEPTOR_TEST_CHROME_PROFILE="Profile 4" bash ~/.claude/skills/Interceptor/To The single verification step is the **Preflight Isolation Gate**. It checks binary version, daemon connection, and context registration in one shot — anything that would let a browser command leak into the Default profile. ```bash -bash ~/.claude/skills/Interceptor/Tools/LaunchTestProfile.sh "https://example.com" +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/LaunchTestProfile.sh" "https://example.com" # A new Chrome window opens for the test profile, navigates to example.com. -bash ~/.claude/skills/Interceptor/Tools/PreflightIsolation.sh +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/PreflightIsolation.sh" # Expect: # [PreflightIsolation] OK — interceptor 0.16.9, context "" connected. ``` @@ -90,7 +90,7 @@ If the preflight fails, read the structured remediation message it prints and ad ```bash # Only after preflight passes (source preferences.env so $INTERCEPTOR_TEST_CONTEXT_ID resolves): -source ~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env +source "${LIFEOS_DIR}/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env" interceptor open "https://example.com" --context "$INTERCEPTOR_TEST_CONTEXT_ID" # Lands in the test profile's window only. ``` @@ -101,10 +101,10 @@ After the one-time setup, every session starts with the preflight gate. It runs ```bash # Operator launches the test window once per session (or leaves it running) -bash ~/.claude/skills/Interceptor/Tools/LaunchTestProfile.sh +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/LaunchTestProfile.sh" # Agent's first action before any browser command: -bash ~/.claude/skills/Interceptor/Tools/PreflightIsolation.sh \ +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/PreflightIsolation.sh" \ || { echo "Preflight failed — STOP and surface to operator. Do not fall back."; exit 1; } # Only after the preflight returns exit 0 (preferences.env sourced for $INTERCEPTOR_TEST_CONTEXT_ID): @@ -128,7 +128,7 @@ Once the test profile is set up and pinned: ## Pitfalls - **Don't use `--user-data-dir` here.** That gives you a fully-sandboxed Chrome with no auth, which is useless for testing the operator's authenticated tooling. The right boundary is profile, not user-data-dir. -- **Extension reload after a bump.** The `~/.claude/skills/Interceptor/Extension/` copy is pinned, not a symlink — after any binary upgrade, re-pin via `Tools/Pin.sh` (per the Update workflow), then Load Unpacked again in **both** profiles (Default + test). Chrome disables unpacked extensions on every manifest version bump and never auto-refreshes them. +- **Extension reload after a bump.** The `{{LIFEOS_ROOT}}/skills/Interceptor/Extension/` copy is pinned, not a symlink — after any binary upgrade, re-pin via `Tools/Pin.sh` (per the Update workflow), then Load Unpacked again in **both** profiles (Default + test). Chrome disables unpacked extensions on every manifest version bump and never auto-refreshes them. - **Context ID must be unique.** Two contexts named `interceptor-test` makes `--context` routing ambiguous. One test profile per context name. - **Profile dir name confusion.** Chrome names the on-disk directories `Profile 1`, `Profile 2`, … even though you give the profile a friendly name like "{{DA_NAME}}". The launcher needs the on-disk name (default `Profile 1`); override via `INTERCEPTOR_TEST_CHROME_PROFILE` if needed. - **Lock conflict if you force `-n`.** Don't add `-n` (new instance) to the `open` command — two Chrome processes can't share the same user-data-dir. The launcher relies on the existing Chrome handling `--profile-directory` natively. diff --git a/LifeOS/install/skills/Interceptor/Workflows/MultiPageCompare.md b/LifeOS/install/skills/Interceptor/Workflows/MultiPageCompare.md index dc36f870c4..60252b1bb3 100644 --- a/LifeOS/install/skills/Interceptor/Workflows/MultiPageCompare.md +++ b/LifeOS/install/skills/Interceptor/Workflows/MultiPageCompare.md @@ -5,8 +5,8 @@ Extracting facts from N pages to answer a comparative question — "who designed ## Preflight Isolation Gate (MANDATORY first step) ```bash -source ~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env -bash ~/.claude/skills/Interceptor/Tools/PreflightIsolation.sh +source "${LIFEOS_DIR}/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env" +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/PreflightIsolation.sh" ``` Non-zero exit → STOP and surface the message verbatim. Do not fall back to the Default profile. `--context "$INTERCEPTOR_TEST_CONTEXT_ID"` (the pinned isolated context from `preferences.env`) is doctrine on every `open` below, not optional — it does not count against the command budget. diff --git a/LifeOS/install/skills/Interceptor/Workflows/OverrideXhr.md b/LifeOS/install/skills/Interceptor/Workflows/OverrideXhr.md index 2518abd5c6..87aae2f93a 100644 --- a/LifeOS/install/skills/Interceptor/Workflows/OverrideXhr.md +++ b/LifeOS/install/skills/Interceptor/Workflows/OverrideXhr.md @@ -8,8 +8,8 @@ You are mutating an HTTP request before it hits the server, or rewriting a respo ## Preflight Isolation Gate (MANDATORY first step) ```bash -source ~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env -bash ~/.claude/skills/Interceptor/Tools/PreflightIsolation.sh +source "${LIFEOS_DIR}/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env" +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/PreflightIsolation.sh" ``` Non-zero exit → STOP and surface the message verbatim. Do not fall back to the Default profile. This workflow mutates live network traffic; a wrong-profile run would rewrite requests in the operator's real session. Every `interceptor` verb below passes `--context "$INTERCEPTOR_TEST_CONTEXT_ID"` (the pinned isolated context from `preferences.env`). **Mandatory cleanup:** install a cleanup so overrides are always cleared, even if a step fails: diff --git a/LifeOS/install/skills/Interceptor/Workflows/ReadAndExtract.md b/LifeOS/install/skills/Interceptor/Workflows/ReadAndExtract.md index 4bad70618f..735036d871 100644 --- a/LifeOS/install/skills/Interceptor/Workflows/ReadAndExtract.md +++ b/LifeOS/install/skills/Interceptor/Workflows/ReadAndExtract.md @@ -5,8 +5,8 @@ Extract structured information from a webpage — a fact, value, list, table con ## Preflight Isolation Gate (MANDATORY first step) ```bash -source ~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env -bash ~/.claude/skills/Interceptor/Tools/PreflightIsolation.sh +source "${LIFEOS_DIR}/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env" +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/PreflightIsolation.sh" ``` Non-zero exit → STOP and surface the message verbatim. Do not fall back to the Default profile. Every browser verb in this workflow passes `--context "$INTERCEPTOR_TEST_CONTEXT_ID"` (the pinned isolated context from `preferences.env`) — the examples below omit it for brevity, but append it to each call. Screenshots, if any, go through `Tools/Capture.sh`, never raw `interceptor screenshot`. diff --git a/LifeOS/install/skills/Interceptor/Workflows/RecordFlow.md b/LifeOS/install/skills/Interceptor/Workflows/RecordFlow.md index ce234d2cdb..fd3961c768 100644 --- a/LifeOS/install/skills/Interceptor/Workflows/RecordFlow.md +++ b/LifeOS/install/skills/Interceptor/Workflows/RecordFlow.md @@ -18,8 +18,8 @@ Record a user workflow by capturing browser actions into a replayable script. Us ## Preflight Isolation Gate (MANDATORY first step) ```bash -source ~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env -bash ~/.claude/skills/Interceptor/Tools/PreflightIsolation.sh +source "${LIFEOS_DIR}/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env" +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/PreflightIsolation.sh" ``` Non-zero exit → STOP and surface the message verbatim. Do not fall back to the Default profile. Recording in the operator's main profile would capture their real session events and credentials — record only in the pinned isolated context. Every `interceptor` verb below passes `--context "$INTERCEPTOR_TEST_CONTEXT_ID"` (from `preferences.env`). diff --git a/LifeOS/install/skills/Interceptor/Workflows/ReplayFlow.md b/LifeOS/install/skills/Interceptor/Workflows/ReplayFlow.md index e3528f6ee2..d6c0a740db 100644 --- a/LifeOS/install/skills/Interceptor/Workflows/ReplayFlow.md +++ b/LifeOS/install/skills/Interceptor/Workflows/ReplayFlow.md @@ -18,8 +18,8 @@ Replay a previously recorded user flow to verify it still works after a deploy o ## Preflight Isolation Gate (MANDATORY first step) ```bash -source ~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env -bash ~/.claude/skills/Interceptor/Tools/PreflightIsolation.sh +source "${LIFEOS_DIR}/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env" +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/PreflightIsolation.sh" ``` Non-zero exit → STOP and surface the message verbatim. Do not fall back to the Default profile. Every `interceptor` verb below — including the `batch` example — passes `--context "$INTERCEPTOR_TEST_CONTEXT_ID"` (from `preferences.env`). Screenshots go through `Tools/Capture.sh`, never raw `interceptor screenshot`. @@ -38,7 +38,7 @@ Non-zero exit → STOP and surface the message verbatim. Do not fall back to the Recorded flows live in `skills/Interceptor/Flows/`. List available flows: ```bash -ls ~/.claude/skills/Interceptor/Flows/ +ls "${LIFEOS_ROOT}/skills/Interceptor/Flows/" ``` Or regenerate from a monitor session: @@ -94,7 +94,7 @@ Compare against the baseline network log from the original recording. Look for: ### 5. Capture Final State ```bash -bash ~/.claude/skills/Interceptor/Tools/Capture.sh --current +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/Capture.sh" --current ``` Read the printed image path and compare the final screenshot against the expected end state of the flow. diff --git a/LifeOS/install/skills/Interceptor/Workflows/Reproduce.md b/LifeOS/install/skills/Interceptor/Workflows/Reproduce.md index 2edc5a083e..675ece90dc 100644 --- a/LifeOS/install/skills/Interceptor/Workflows/Reproduce.md +++ b/LifeOS/install/skills/Interceptor/Workflows/Reproduce.md @@ -27,11 +27,11 @@ Reproduce a reported bug by opening the affected page in real Chrome BEFORE read ### 0. Preflight Isolation Gate (MANDATORY first step) ```bash -source ~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env -bash ~/.claude/skills/Interceptor/Tools/PreflightIsolation.sh +source "${LIFEOS_DIR}/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env" +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/PreflightIsolation.sh" ``` -Prefer `bash ~/.claude/skills/Interceptor/Tools/EnsureTestProfile.sh` — it runs the gate and auto-launches the test profile if it isn't open (exit 5/6), only proceeding after the pinned-UUID match passes. Non-zero exit → STOP and surface the message verbatim; do not fall back to the Default profile. `INTERCEPTOR_TEST_CONTEXT_ID` is the pinned isolated context; every browser verb below passes it. Reproduce in the isolated profile by default; only route to the main profile if the bug is specifically tied to the operator's signed-in session (and they said so). Screenshots go through `Tools/Capture.sh`, never raw `interceptor screenshot`. +Prefer `bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/EnsureTestProfile.sh"` — it runs the gate and auto-launches the test profile if it isn't open (exit 5/6), only proceeding after the pinned-UUID match passes. Non-zero exit → STOP and surface the message verbatim; do not fall back to the Default profile. `INTERCEPTOR_TEST_CONTEXT_ID` is the pinned isolated context; every browser verb below passes it. Reproduce in the isolated profile by default; only route to the main profile if the bug is specifically tied to the operator's signed-in session (and they said so). Screenshots go through `Tools/Capture.sh`, never raw `interceptor screenshot`. ### 1. Open the Affected Page (in the isolated profile) @@ -44,7 +44,7 @@ Do NOT read code first. Do NOT form theories. Open the page and look at it. ### 2. Capture Visual State ```bash -bash ~/.claude/skills/Interceptor/Tools/Capture.sh "" +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/Capture.sh" "" ``` Read the printed image path. Is the reported bug visible? Document what you see vs what's expected. diff --git a/LifeOS/install/skills/Interceptor/Workflows/ScreenshotForVlm.md b/LifeOS/install/skills/Interceptor/Workflows/ScreenshotForVlm.md index 07f6f8971c..f6152cefad 100644 --- a/LifeOS/install/skills/Interceptor/Workflows/ScreenshotForVlm.md +++ b/LifeOS/install/skills/Interceptor/Workflows/ScreenshotForVlm.md @@ -7,8 +7,8 @@ Taking a screenshot of a webpage for a vision-language model (VLM) to read. Use ## Preflight Isolation Gate (MANDATORY first step) ```bash -source ~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env -bash ~/.claude/skills/Interceptor/Tools/PreflightIsolation.sh +source "${LIFEOS_DIR}/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env" +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/PreflightIsolation.sh" ``` Non-zero exit → STOP and surface the message verbatim. Do not fall back to the Default profile. Capture goes through `Tools/Capture.sh`, which re-runs this gate, routes to `INTERCEPTOR_TEST_CONTEXT_ID`, and resolves the destination to `~/Downloads/` for review artifacts — never raw `interceptor screenshot`. @@ -18,7 +18,7 @@ Non-zero exit → STOP and surface the message verbatim. Do not fall back to the **1 command.** The agent-default recipe below IS the budget. ```bash -bash ~/.claude/skills/Interceptor/Tools/Capture.sh --current +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/Capture.sh" --current ``` If the first screenshot doesn't answer the question, do NOT take a second exploratory screenshot — re-evaluate whether pixels are actually the answer. The "exploratory then second screenshot" pattern is the failure mode this budget exists to prevent. If you need a second capture, scope it tightly with the underlying `--selector`, `--element `, or `--region X,Y,W,H` flags — still 1 command, not a re-take. @@ -26,7 +26,7 @@ If the first screenshot doesn't answer the question, do NOT take a second explor ## The agent-default recipe ```bash -bash ~/.claude/skills/Interceptor/Tools/Capture.sh --current +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/Capture.sh" --current ``` `Capture.sh` runs `interceptor screenshot` under the hood with the DOM-render path, the pinned `--context`, `--save`, and a `~/Downloads/` destination, then prints the absolute saved-image path on its only stdout line. No inline base64; the path re-reads on demand and never bloats your context. The defaults it applies are load-bearing: diff --git a/LifeOS/install/skills/Interceptor/Workflows/TestForm.md b/LifeOS/install/skills/Interceptor/Workflows/TestForm.md index 8fc4ced71d..736cccfc2c 100644 --- a/LifeOS/install/skills/Interceptor/Workflows/TestForm.md +++ b/LifeOS/install/skills/Interceptor/Workflows/TestForm.md @@ -27,11 +27,11 @@ Discover, fill, submit, and verify a form on any page. Uses Interceptor's semant ### 0. Preflight Isolation Gate (MANDATORY first step) ```bash -source ~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env -bash ~/.claude/skills/Interceptor/Tools/PreflightIsolation.sh +source "${LIFEOS_DIR}/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env" +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/PreflightIsolation.sh" ``` -Prefer `bash ~/.claude/skills/Interceptor/Tools/EnsureTestProfile.sh` — it runs the gate and auto-launches the test profile if it isn't open (exit 5/6), only proceeding after the pinned-UUID match passes. Non-zero exit → STOP and surface the message verbatim; do not fall back to the Default profile. `INTERCEPTOR_TEST_CONTEXT_ID` is the pinned isolated context; every browser verb below passes it. Form testing in the isolated profile keeps test submissions away from any real auth state in the operator's main session. Screenshots go through `Tools/Capture.sh`, never raw `interceptor screenshot`. +Prefer `bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/EnsureTestProfile.sh"` — it runs the gate and auto-launches the test profile if it isn't open (exit 5/6), only proceeding after the pinned-UUID match passes. Non-zero exit → STOP and surface the message verbatim; do not fall back to the Default profile. `INTERCEPTOR_TEST_CONTEXT_ID` is the pinned isolated context; every browser verb below passes it. Form testing in the isolated profile keeps test submissions away from any real auth state in the operator's main session. Screenshots go through `Tools/Capture.sh`, never raw `interceptor screenshot`. ### 1. Open the Page with the Form (in the isolated profile) @@ -84,7 +84,7 @@ interceptor click "radio:Monthly Plan" --context "$INTERCEPTOR_TEST_CONTEXT_ID" Before submitting, verify the form looks correct: ```bash -bash ~/.claude/skills/Interceptor/Tools/Capture.sh --current +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/Capture.sh" --current ``` Read the printed image path to confirm fields are populated correctly and no validation errors are showing. `Capture.sh --current` shoots the already-open page in the pinned context. @@ -115,7 +115,7 @@ interceptor read --text-only --context "$INTERCEPTOR_TEST_CONTEXT_ID" interceptor net log --json --context "$INTERCEPTOR_TEST_CONTEXT_ID" # Capture the result page -bash ~/.claude/skills/Interceptor/Tools/Capture.sh --current +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/Capture.sh" --current ``` Look for: @@ -140,7 +140,7 @@ interceptor read --text-only --context "$INTERCEPTOR_TEST_CONTEXT_ID" # Very long input interceptor type "textbox:Name" "A very long name that might break layout assumptions in the form" --context "$INTERCEPTOR_TEST_CONTEXT_ID" -bash ~/.claude/skills/Interceptor/Tools/Capture.sh --current +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/Capture.sh" --current ``` ## Notes diff --git a/LifeOS/install/skills/Interceptor/Workflows/Update.md b/LifeOS/install/skills/Interceptor/Workflows/Update.md index 60bd7795cf..db767c90ac 100644 --- a/LifeOS/install/skills/Interceptor/Workflows/Update.md +++ b/LifeOS/install/skills/Interceptor/Workflows/Update.md @@ -90,12 +90,12 @@ cp ~/Projects/interceptor/daemon/interceptor-daemon /opt/homebrew/bin/ ### 4a. Pin the Extension into the skill -`~/.claude/skills/Interceptor/Extension/` is a **pinned copy** of the built `extension/dist/`, not a symlink. Two reasons: Chrome disables unpacked extensions on every manifest version bump (a stable copy survives that), and the public LifeOS release ships this skill — a symlink to a local build dir is useless to other users. +`{{LIFEOS_ROOT}}/skills/Interceptor/Extension/` is a **pinned copy** of the built `extension/dist/`, not a symlink. Two reasons: Chrome disables unpacked extensions on every manifest version bump (a stable copy survives that), and the public LifeOS release ships this skill — a symlink to a local build dir is useless to other users. Re-pin after every build: ```bash -bash ~/.claude/skills/Interceptor/Tools/Pin.sh +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/Pin.sh" ``` `Pin.sh` rsyncs `dist/` → `Extension/`, **scrubs absolute home paths** that esbuild bakes into bundled JS (the `__dirname` literal in CommonJS wrappers → `"."`), regenerates `PINNED_FROM.txt` with a relative source path, and exits non-zero if any absolute home path survives. Set `INTERCEPTOR_SRC` to override the source repo location. @@ -223,7 +223,7 @@ If `Extension/manifest.json` changed (especially `version` or `key`): 1. Open `chrome://extensions`, enable Developer Mode. 2. **Delete** the existing Interceptor card (don't just hit reload — if the manifest `key` changed, the extension ID changed and the old card is dead). -3. **Load unpacked** → `~/.claude/skills/Interceptor/Extension` (a pinned copy of the built `extension/dist`, captured by `Tools/Pin.sh` — NOT a symlink; it does not auto-follow upstream, so it must be re-pinned after every build). +3. **Load unpacked** → `{{LIFEOS_ROOT}}/skills/Interceptor/Extension` (a pinned copy of the built `extension/dist`, captured by `Tools/Pin.sh` — NOT a symlink; it does not auto-follow upstream, so it must be re-pinned after every build). 4. Quit Chrome fully (⌘Q, not just close window) and relaunch — service worker needs a clean restart, especially with `userScripts` permission added. 5. Accept any new permission prompts (`userScripts`, etc.). diff --git a/LifeOS/install/skills/Interceptor/Workflows/VerifyDeploy.md b/LifeOS/install/skills/Interceptor/Workflows/VerifyDeploy.md index e8ce08997f..df7a557a33 100644 --- a/LifeOS/install/skills/Interceptor/Workflows/VerifyDeploy.md +++ b/LifeOS/install/skills/Interceptor/Workflows/VerifyDeploy.md @@ -29,8 +29,8 @@ Verify a deployment by opening the target URL in real Chrome and capturing a **f ### 0. Preflight Isolation Gate (MANDATORY first step) ```bash -source ~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env -bash ~/.claude/skills/Interceptor/Tools/EnsureTestProfile.sh +source "${LIFEOS_DIR}/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env" +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/EnsureTestProfile.sh" ``` `EnsureTestProfile.sh` runs the isolation gate AND auto-recovers: if the test profile window just isn't open (exit 5/6) it launches the configured profile, polls until the pinned context connects, and prints `READY`. It only ever succeeds after the gate passes (pinned-UUID match + Default-deny), so a wrong launch can never be driven. Non-zero exit → STOP and surface the message verbatim; do not fall back to the Default profile. (Call `PreflightIsolation.sh` directly when you explicitly want the gate with NO auto-launch.) `INTERCEPTOR_TEST_CONTEXT_ID` is the pinned isolated context; every browser verb below passes it. Screenshots go through `Tools/Capture.sh`, never raw `interceptor screenshot`. @@ -94,9 +94,9 @@ Third-party 4xx (trackers, ads) is noted, not failing. ### 5. Probe D — screenshot ```bash -bash ~/.claude/skills/Interceptor/Tools/Capture.sh "" +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/Capture.sh" "" # long pages: -bash ~/.claude/skills/Interceptor/Tools/Capture.sh "" --full +bash "${LIFEOS_ROOT}/skills/Interceptor/Tools/Capture.sh" "" --full ``` `Capture.sh` re-runs the isolation gate, routes to the pinned context, prefers the DOM-render path (no foreground needed), and prints the absolute saved-image path on its only stdout line. Read that image to visually confirm rendering. Never call raw `interceptor screenshot` here — it loses the deny-Default guard and CWD-destination handling. diff --git a/LifeOS/install/skills/Interview/SKILL.md b/LifeOS/install/skills/Interview/SKILL.md index 424680e958..5dc44b8c78 100644 --- a/LifeOS/install/skills/Interview/SKILL.md +++ b/LifeOS/install/skills/Interview/SKILL.md @@ -30,7 +30,7 @@ The skill reads the constitutional files via the freshness tooling, scores each **Routing decision (run before either workflow):** ```bash -bun ~/.claude/LIFEOS/TOOLS/InterviewScan.ts --json | jq '[.targets[] | select(.phase == 0 and .completeness_score < 80)] | length' +bun "${LIFEOS_DIR}/TOOLS/InterviewScan.ts" --json | jq '[.targets[] | select(.phase == 0 and .completeness_score < 80)] | length' ``` - `> 0` → run **Phase0Setup** first, then ContextCheckin. @@ -41,13 +41,13 @@ bun ~/.claude/LIFEOS/TOOLS/InterviewScan.ts --json | jq '[.targets[] | select(.p - The TELOS is on file. **Read it before asking.** Generic "what's your mission?" prompts are forbidden when TELOS is populated. - Staleness is **information, not failure** — a 95-day-old Goals section might still be right; the prompt is "still right?", not "you're behind." - **Per-entry on typed-ID sections** (G3, M0, P2…), **section-level on prose** (Current State, Sparks). -- **Bump on every approved edit:** `bun ~/.claude/LIFEOS/TOOLS/TelosFreshness.ts --bump `. Without this the staleness signal degrades to noise. +- **Bump on every approved edit:** `bun "${LIFEOS_DIR}/TOOLS/TelosFreshness.ts" --bump `. Without this the staleness signal degrades to noise. - **Stop signals are sacred.** "Enough" / "stop" / "later" exits gracefully. State persists in the file itself. - **ID-stability rule:** G3 stays G3 even when edited or dropped; new entries get the next sequential ID. ## Gotchas -- **Migration must run once before TelosCheckin works.** A TELOS without YAML frontmatter (no `last_updated:`) returns `fileUpdated: null` and every section reads as stale. Run `bun ~/.claude/LIFEOS/TOOLS/MigrateTelosFreshness.ts` once; idempotent and content-preserving (verifies sha256 of stripped content). +- **Migration must run once before TelosCheckin works.** A TELOS without YAML frontmatter (no `last_updated:`) returns `fileUpdated: null` and every section reads as stale. Run `bun "${LIFEOS_DIR}/TOOLS/MigrateTelosFreshness.ts"` once; idempotent and content-preserving (verifies sha256 of stripped content). - **The slug is normalized:** "Current State" → `current_state`, "Wrong (Things I've been wrong about)" → `wrong`, "2036 — A Day in the Life…" → `2036`. Always run heading text through `sectionSlug()` from `TelosFreshness.ts`. - **Pulse caches freshness for 60s.** After bumping, the next `/api/telos/freshness/summary` call returns the cached value until invalidation. Send `/reload` (POST) to invalidate the cache immediately, or wait 60s. - **TelosRenderer (`GenerateTelosSummary.ts`) preserves the markers.** It splits by `^## ` headings; the per-section HTML comments live inside the section body and are not re-emitted in `PRINCIPAL_TELOS.md`. Safe to run after edits. diff --git a/LifeOS/install/skills/Interview/Workflows/ContextCheckin.md b/LifeOS/install/skills/Interview/Workflows/ContextCheckin.md index 018689152b..4ce1e2e275 100644 --- a/LifeOS/install/skills/Interview/Workflows/ContextCheckin.md +++ b/LifeOS/install/skills/Interview/Workflows/ContextCheckin.md @@ -24,8 +24,8 @@ curl -s -X POST http://localhost:31337/notify \ Two readers. Both at the start. The constitutional context is on file; we ground every prompt in what's there, never in what we'd ask if we had no idea. ```bash -bun ~/.claude/LIFEOS/TOOLS/TelosFreshness.ts --json # per-section TELOS -bun -e "import { readContextFreshness } from '$HOME/.claude/LIFEOS/TOOLS/TelosFreshness'; console.log(JSON.stringify(readContextFreshness(), null, 2))" +bun "${LIFEOS_DIR}/TOOLS/TelosFreshness.ts" --json # per-section TELOS +bun -e "import { readContextFreshness } from '${LIFEOS_DIR}/TOOLS/TelosFreshness'; console.log(JSON.stringify(readContextFreshness(), null, 2))" ``` Or via Pulse (single round-trip): @@ -37,7 +37,7 @@ curl -s http://localhost:31337/api/telos/freshness | jq # per-section TELOS Parse and combine into a single sorted list of stale items — TELOS sections AND constitutional files share the same conceptual surface from the principal's view. Sort most-stale-first by days-over-threshold. -If `readContextFreshness()` reports a file with `why: "no frontmatter"`, the migration hasn't been run on that file. Stop and prompt: *" doesn't have the freshness convention yet. Want me to run `bun ~/.claude/LIFEOS/TOOLS/MigrateContextFreshness.ts` first?"* +If `readContextFreshness()` reports a file with `why: "no frontmatter"`, the migration hasn't been run on that file. Stop and prompt: *" doesn't have the freshness convention yet. Want me to run `bun "${LIFEOS_DIR}/TOOLS/MigrateContextFreshness.ts"` first?"* If `readContextFreshness()` reports a file with `why: "source missing"`, the file's `derived_from:` source doesn't have a freshness signal — the derivative can't inherit one. Surface this to the principal: *" derives from which has no freshness frontmatter. Want me to add it?"* @@ -119,10 +119,10 @@ workflow (and equivalent principal-driven review flows) should call it. ```bash # TELOS section — section-level marker -bun ~/.claude/LIFEOS/TOOLS/TelosFreshness.ts --bump +bun "${LIFEOS_DIR}/TOOLS/TelosFreshness.ts" --bump # Constitutional file — review marker (NOT bumpContextTimestamp; that's for writes) -bun -e "import { bumpReviewedTimestamp } from '$HOME/.claude/LIFEOS/TOOLS/TelosFreshness'; console.log(bumpReviewedTimestamp('', 'user'))" +bun -e "import { bumpReviewedTimestamp } from '${LIFEOS_DIR}/TOOLS/TelosFreshness'; console.log(bumpReviewedTimestamp('', 'user'))" ``` Without this, files stay at grade F forever because no other path sets `last_reviewed:`. @@ -149,8 +149,8 @@ The principal can say "next", "skip", "enough", "stop", "later" at any prompt. H When the principal says enough: ```bash -bun ~/.claude/LIFEOS/TOOLS/TelosFreshness.ts # final TELOS state -bun ~/.claude/LIFEOS/TOOLS/ContextAudit.ts # content quality findings (read-only) +bun "${LIFEOS_DIR}/TOOLS/TelosFreshness.ts" # final TELOS state +bun "${LIFEOS_DIR}/TOOLS/ContextAudit.ts" # content quality findings (read-only) ``` Voice-summarize: @@ -160,8 +160,8 @@ Voice-summarize: Regenerate the auto-derived files so future sessions pick up source changes: ```bash -bun ~/.claude/LIFEOS/TOOLS/GenerateTelosSummary.ts 2>/dev/null || true -bun ~/.claude/LIFEOS/TOOLS/ArchitectureSummaryGenerator.ts generate 2>/dev/null || true +bun "${LIFEOS_DIR}/TOOLS/GenerateTelosSummary.ts" 2>/dev/null || true +bun "${LIFEOS_DIR}/TOOLS/ArchitectureSummaryGenerator.ts" generate 2>/dev/null || true ``` Send a Pulse `/reload` so the freshness cache invalidates: @@ -229,7 +229,7 @@ voice-confirm ## Failure modes -- **Migration not run yet.** First call to `readContextFreshness()` returns one or more files with `why: "no frontmatter"`. Run `bun ~/.claude/LIFEOS/TOOLS/MigrateContextFreshness.ts` once before continuing. +- **Migration not run yet.** First call to `readContextFreshness()` returns one or more files with `why: "no frontmatter"`. Run `bun "${LIFEOS_DIR}/TOOLS/MigrateContextFreshness.ts"` once before continuing. - **Pulse not running.** Voice notifications fail silently; HTTP routes return connection errors. Conversation continues using the lib directly. - **Source missing for a derived file.** `architecture_summary` may show `why: "source missing"` if `LifeosSystemArchitecture.md` has no `last_updated` frontmatter. Surface to the principal; offer to add it. - **Slug not found by bumpTelosTimestamp.** Returns `sectionFound: false`. Re-check `sectionSlug(headingText)`. diff --git a/LifeOS/install/skills/Interview/Workflows/Phase0Setup.md b/LifeOS/install/skills/Interview/Workflows/Phase0Setup.md index 9597c2d540..78a30b8bb1 100644 --- a/LifeOS/install/skills/Interview/Workflows/Phase0Setup.md +++ b/LifeOS/install/skills/Interview/Workflows/Phase0Setup.md @@ -22,7 +22,7 @@ curl -s -X POST http://localhost:31337/notify \ Run the scanner to see Phase 0 completeness: ```bash -bun ~/.claude/LIFEOS/TOOLS/InterviewScan.ts --json | jq '.targets[] | select(.phase == 0)' +bun "${LIFEOS_DIR}/TOOLS/InterviewScan.ts" --json | jq '.targets[] | select(.phase == 0)' ``` If every Phase 0 target reads ≥80% complete, **skip this workflow** and route to TelosCheckin. @@ -38,7 +38,7 @@ Walk the six setup targets one at a time. Each writes to a specific file. Voice- | 0.1 | **DA Identity** — name, full name, color, role, personality summary | `USER/DIGITAL_ASSISTANT/DA_IDENTITY.md` (always — YAML frontmatter holds structured schema, body holds prose); `USER/DA/{name}/DA_IDENTITY.md` (if multi-DA structure exists or principal opts in) | DA_IDENTITY.md no longer reads "LifeOS" / "LifeOS Assistant" | | 0.2 | **Principal Identity** — name (with pronunciation), location, timezone, role, focus | `USER/PRINCIPAL/PRINCIPAL_IDENTITY.md` Quick Reference section | PRINCIPAL_IDENTITY.md no longer reads "User" with "(interview)" markers | | 0.3 | **Voice IDs** — main DA voice + algorithm voice (offer ElevenLabs library link or "use defaults") | `USER/DIGITAL_ASSISTANT/DA_IDENTITY.md` Voice section + `PULSE/PULSE.toml` `[voice]` block | Voice IDs no longer match the generic Rachel/Adam defaults | -| 0.4 | **Credentials** — ANTHROPIC_API_KEY, ELEVENLABS_API_KEY (skippable with explanation), optional GH_TOKEN, STRIPE_KEY | `~/.claude/.env` (or `~/.config/LIFEOS/.env` symlink target) | `.env` exists with at least ANTHROPIC_API_KEY set | +| 0.4 | **Credentials** — ANTHROPIC_API_KEY, ELEVENLABS_API_KEY (skippable with explanation), optional GH_TOKEN, STRIPE_KEY | `{{LIFEOS_ROOT}}/.env` (or `~/.config/LIFEOS/.env` symlink target) | `.env` exists with at least ANTHROPIC_API_KEY set | | 0.5 | **First project** — at least one row so PROJECTS routing works | `USER/PROJECTS.md` table + Routing Aliases | PROJECTS.md has ≥1 non-sample row | | 0.6 | **Work repo** — GitHub repo for issues OR explicit "skip + disable work pipeline" | `USER/WORK/config.yaml` `WORK.REPO` field; or `[work] enabled = false` in PULSE.toml | USER/WORK/config.yaml WORK.REPO points at an existing repo | @@ -56,7 +56,7 @@ Walk the six setup targets one at a time. Each writes to a specific file. Voice- 4. Credentials (0.4) — never echo keys back to the principal in voice. Confirm only "captured ANTHROPIC_API_KEY" / "captured ELEVENLABS_API_KEY". If the principal pastes a key in chat, immediately write it to `.env` and ask the principal to clear it from scrollback. 5. After Phase 0 completes, **regenerate PRINCIPAL_TELOS.md** (it interpolates the principal name) and **send a Pulse `/reload`** so the running daemon picks up the new identity: ```bash - bun ~/.claude/LIFEOS/TOOLS/GenerateTelosSummary.ts 2>/dev/null || true + bun "${LIFEOS_DIR}/TOOLS/GenerateTelosSummary.ts" 2>/dev/null || true curl -s -X POST http://localhost:31337/reload > /dev/null 2>&1 & ``` 6. Voice the transition: diff --git a/LifeOS/install/skills/IterativeDepth/SKILL.md b/LifeOS/install/skills/IterativeDepth/SKILL.md index 90d8927317..a769ff886a 100644 --- a/LifeOS/install/skills/IterativeDepth/SKILL.md +++ b/LifeOS/install/skills/IterativeDepth/SKILL.md @@ -8,7 +8,7 @@ effort: high ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/IterativeDepth/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/IterativeDepth/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -56,7 +56,7 @@ Passes stop when a new lens repeats what earlier lenses already found. Non-redun After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"IterativeDepth","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"IterativeDepth","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/Knowledge/SKILL.md b/LifeOS/install/skills/Knowledge/SKILL.md index 601a304f98..de602b6c2b 100644 --- a/LifeOS/install/skills/Knowledge/SKILL.md +++ b/LifeOS/install/skills/Knowledge/SKILL.md @@ -19,9 +19,9 @@ Notes you save in isolation are notes you never find again. A flat folder of fac ## How It Works -Manage the LifeOS Knowledge Archive at `~/.claude/LIFEOS/MEMORY/KNOWLEDGE/`. Each operation routes through a subcommand below; notes follow the archive schema and ship with typed cross-links. +Manage the LifeOS Knowledge Archive at `{{LIFEOS_DIR}}/MEMORY/KNOWLEDGE/`. Each operation routes through a subcommand below; notes follow the archive schema and ship with typed cross-links. -**Archive schema:** `~/.claude/LIFEOS/MEMORY/KNOWLEDGE/_schema.md` +**Archive schema:** `{{LIFEOS_DIR}}/MEMORY/KNOWLEDGE/_schema.md` ## Workflow Routing @@ -51,7 +51,7 @@ If `$ARGUMENTS` doesn't match a subcommand, treat it as a search query. Run the harvester status command and display results: ```bash -bun ~/.claude/LIFEOS/TOOLS/KnowledgeHarvester.ts status +bun "${LIFEOS_DIR}/TOOLS/KnowledgeHarvester.ts" status ``` Also show: @@ -70,17 +70,17 @@ Search the Knowledge Archive for notes matching `$ARGUMENTS`. **Step 1 — Lexical search:** ```bash -rg -i "$ARGUMENTS" ~/.claude/LIFEOS/MEMORY/KNOWLEDGE/ --type md -l +rg -i "$ARGUMENTS" "${LIFEOS_DIR}/MEMORY/KNOWLEDGE/" --type md -l ``` **Step 2 — Frontmatter search (tags and titles):** ```bash -rg -i "title:.*$ARGUMENTS|tags:.*$ARGUMENTS" ~/.claude/LIFEOS/MEMORY/KNOWLEDGE/ --type md -l +rg -i "title:.*$ARGUMENTS|tags:.*$ARGUMENTS" "${LIFEOS_DIR}/MEMORY/KNOWLEDGE/" --type md -l ``` **Step 3 — Wikilink search:** ```bash -rg "\[\[.*$ARGUMENTS.*\]\]" ~/.claude/LIFEOS/MEMORY/KNOWLEDGE/ --type md -l +rg "\[\[.*$ARGUMENTS.*\]\]" "${LIFEOS_DIR}/MEMORY/KNOWLEDGE/" --type md -l ``` Deduplicate results across all three. For each match, read the first 5 lines of frontmatter to show title, domain, status, tags. @@ -107,7 +107,7 @@ Create a new note manually in the specified entity type. 7. Verify every slug in `related:` exists in the archive before saving 8. Regenerate the type's MOC: ```bash -bun ~/.claude/LIFEOS/TOOLS/KnowledgeHarvester.ts index +bun "${LIFEOS_DIR}/TOOLS/KnowledgeHarvester.ts" index ``` **Topic is a tag, not a type.** A security insight is an Idea with a `security` tag. A security company is a Company with a `security` tag. The entity type determines the schema; the tag determines the topic. @@ -146,13 +146,13 @@ related: **How to find related notes before writing:** ```bash # By topic/keyword -rg -l "TOPIC" ~/.claude/LIFEOS/MEMORY/KNOWLEDGE/ --type md +rg -l "TOPIC" "${LIFEOS_DIR}/MEMORY/KNOWLEDGE/" --type md # By tag overlap -rg "^tags:.*TAG" ~/.claude/LIFEOS/MEMORY/KNOWLEDGE/ --type md -l +rg "^tags:.*TAG" "${LIFEOS_DIR}/MEMORY/KNOWLEDGE/" --type md -l # For People/Companies — grep by name -rg -l "Person Name" ~/.claude/LIFEOS/MEMORY/KNOWLEDGE/ +rg -l "Person Name" "${LIFEOS_DIR}/MEMORY/KNOWLEDGE/" ``` **Enforcement:** @@ -168,7 +168,7 @@ rg -l "Person Name" ~/.claude/LIFEOS/MEMORY/KNOWLEDGE/ Run the KnowledgeHarvester to pull new knowledge from all LifeOS sources: ```bash -bun ~/.claude/LIFEOS/TOOLS/KnowledgeHarvester.ts harvest +bun "${LIFEOS_DIR}/TOOLS/KnowledgeHarvester.ts" harvest ``` Display results. If nothing was harvested, explain that sources are already up to date. @@ -183,7 +183,7 @@ The weekly gardening workflow. Surface seedling notes that are ready for enrichm **Step 1 — Find seedlings:** ```bash -rg "^status: seedling" ~/.claude/LIFEOS/MEMORY/KNOWLEDGE/ --type md -l +rg "^status: seedling" "${LIFEOS_DIR}/MEMORY/KNOWLEDGE/" --type md -l ``` **Step 2 — For each seedling:** @@ -233,10 +233,10 @@ Search for existing notes that relate to this new content: ```bash # Search by extracted tags -rg -i "TAG1|TAG2|TAG3" ~/.claude/LIFEOS/MEMORY/KNOWLEDGE/ --type md -l --glob '!_*' +rg -i "TAG1|TAG2|TAG3" "${LIFEOS_DIR}/MEMORY/KNOWLEDGE/" --type md -l --glob '!_*' # Search by key entities/concepts mentioned -rg -i "ENTITY1|ENTITY2" ~/.claude/LIFEOS/MEMORY/KNOWLEDGE/ --type md -l --glob '!_*' +rg -i "ENTITY1|ENTITY2" "${LIFEOS_DIR}/MEMORY/KNOWLEDGE/" --type md -l --glob '!_*' ``` For each related note found (up to 10): @@ -280,7 +280,7 @@ Append to `KNOWLEDGE/_log.md`: Regenerate MOCs: ```bash -bun ~/.claude/LIFEOS/TOOLS/KnowledgeHarvester.ts index +bun "${LIFEOS_DIR}/TOOLS/KnowledgeHarvester.ts" index ``` Present in NATIVE mode. @@ -295,7 +295,7 @@ Find and review conflicting claims across Knowledge notes. Run the KnowledgeHarvester contradiction finder: ```bash -bun ~/.claude/LIFEOS/TOOLS/KnowledgeHarvester.ts contradictions +bun "${LIFEOS_DIR}/TOOLS/KnowledgeHarvester.ts" contradictions ``` This outputs pairs of notes with high tag overlap (2+ shared tags), ranked by overlap count. @@ -348,21 +348,21 @@ Navigate the Knowledge Archive as a graph. **No argument — stats overview:** ```bash -bun ~/.claude/LIFEOS/TOOLS/KnowledgeGraph.ts stats +bun "${LIFEOS_DIR}/TOOLS/KnowledgeGraph.ts" stats ``` Show node count, edge count, top clusters, most connected hubs, and isolated nodes. **With slug — traverse from a note:** ```bash -bun ~/.claude/LIFEOS/TOOLS/KnowledgeGraph.ts traverse --hops 2 +bun "${LIFEOS_DIR}/TOOLS/KnowledgeGraph.ts" traverse --hops 2 ``` Show all notes connected within 2 hops via tags, wikilinks, and typed relationships. Useful for exploring how knowledge connects across domains. **Related notes only:** ```bash -bun ~/.claude/LIFEOS/TOOLS/KnowledgeGraph.ts related +bun "${LIFEOS_DIR}/TOOLS/KnowledgeGraph.ts" related ``` Present in NATIVE mode. @@ -374,14 +374,14 @@ Present in NATIVE mode. Compressed context retrieval over the Knowledge Archive using BM25-lite scoring. ```bash -bun ~/.claude/LIFEOS/TOOLS/MemoryRetriever.ts "" --top 5 +bun "${LIFEOS_DIR}/TOOLS/MemoryRetriever.ts" "" --top 5 ``` Returns the top matching notes with compressed summaries, ranked by title match, tag overlap, and content frequency. Useful for loading relevant knowledge context without reading full files. For raw excerpts without LLM compression: ```bash -bun ~/.claude/LIFEOS/TOOLS/MemoryRetriever.ts "" --raw +bun "${LIFEOS_DIR}/TOOLS/MemoryRetriever.ts" "" --raw ``` Present in NATIVE mode. @@ -393,14 +393,14 @@ Present in NATIVE mode. Mine recent conversations for memory candidates (decisions, preferences, milestones, problems). ```bash -bun ~/.claude/LIFEOS/TOOLS/SessionHarvester.ts --mine --recent 10 +bun "${LIFEOS_DIR}/TOOLS/SessionHarvester.ts" --mine --recent 10 ``` Candidates are written to `KNOWLEDGE/_harvest-queue/` for review — never directly to KNOWLEDGE/. Use `/knowledge harvest` to process the queue. For dry run (preview only): ```bash -bun ~/.claude/LIFEOS/TOOLS/SessionHarvester.ts --mine --recent 10 --dry-run +bun "${LIFEOS_DIR}/TOOLS/SessionHarvester.ts" --mine --recent 10 --dry-run ``` Present in NATIVE mode. diff --git a/LifeOS/install/skills/LifeOS/INSTALL.md b/LifeOS/install/skills/LifeOS/INSTALL.md index 2996cbd875..23bee063e5 100644 --- a/LifeOS/install/skills/LifeOS/INSTALL.md +++ b/LifeOS/install/skills/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/install/skills/LifeOS/Tools/ActivateImports.ts b/LifeOS/install/skills/LifeOS/Tools/ActivateImports.ts index c5e652fa95..645272a11d 100755 --- a/LifeOS/install/skills/LifeOS/Tools/ActivateImports.ts +++ b/LifeOS/install/skills/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/install/skills/LifeOS/Tools/DeployComponents.ts b/LifeOS/install/skills/LifeOS/Tools/DeployComponents.ts index dd3e33a547..05eb6fb18d 100755 --- a/LifeOS/install/skills/LifeOS/Tools/DeployComponents.ts +++ b/LifeOS/install/skills/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/install/skills/LifeOS/Tools/DeployCore.ts b/LifeOS/install/skills/LifeOS/Tools/DeployCore.ts index 3e1fd61b48..fca73c8051 100755 --- a/LifeOS/install/skills/LifeOS/Tools/DeployCore.ts +++ b/LifeOS/install/skills/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/install/skills/LifeOS/Tools/InstallEngine.ts b/LifeOS/install/skills/LifeOS/Tools/InstallEngine.ts index 920d6a25fb..98f4d3632c 100644 --- a/LifeOS/install/skills/LifeOS/Tools/InstallEngine.ts +++ b/LifeOS/install/skills/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); @@ -560,7 +664,7 @@ type HooksMap = Record; /** * Normalize a hook command for dedup: collapse the harness/PAI path-var forms to * a single canonical token and squeeze whitespace, so the same hook expressed as - * `${LIFEOS_DIR}/x`, `$LIFEOS_DIR/x`, or `~/.claude/x` dedupes to one. + * `${LIFEOS_DIR}/x`, `$CLAUDE_PROJECT_DIR/x`, or `~/.claude/x` dedupes to one. */ function normalizeCommand(cmd: string): string { return cmd diff --git a/LifeOS/install/skills/LifeOS/Tools/InstallHooks.ts b/LifeOS/install/skills/LifeOS/Tools/InstallHooks.ts index 030fb92181..9298edc63c 100755 --- a/LifeOS/install/skills/LifeOS/Tools/InstallHooks.ts +++ b/LifeOS/install/skills/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/install/skills/LifeOS/Tools/InstallSettings.ts b/LifeOS/install/skills/LifeOS/Tools/InstallSettings.ts index 4b81c130e2..8ffd84b147 100644 --- a/LifeOS/install/skills/LifeOS/Tools/InstallSettings.ts +++ b/LifeOS/install/skills/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/install/skills/LifeOS/Tools/LinkUser.ts b/LifeOS/install/skills/LifeOS/Tools/LinkUser.ts index a0899480e2..51c840aa04 100755 --- a/LifeOS/install/skills/LifeOS/Tools/LinkUser.ts +++ b/LifeOS/install/skills/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/install/skills/LifeOS/Tools/ScaffoldUser.ts b/LifeOS/install/skills/LifeOS/Tools/ScaffoldUser.ts index 5230b2db92..9480ece4c0 100755 --- a/LifeOS/install/skills/LifeOS/Tools/ScaffoldUser.ts +++ b/LifeOS/install/skills/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/install/skills/LifeOS/Tools/SeedPulse.ts b/LifeOS/install/skills/LifeOS/Tools/SeedPulse.ts index d9ee9a8784..e62775e48a 100755 --- a/LifeOS/install/skills/LifeOS/Tools/SeedPulse.ts +++ b/LifeOS/install/skills/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/install/skills/LifeOS/Workflows/Setup.md b/LifeOS/install/skills/LifeOS/Workflows/Setup.md index 40a435c1ce..6d1119d09d 100644 --- a/LifeOS/install/skills/LifeOS/Workflows/Setup.md +++ b/LifeOS/install/skills/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/install/skills/LifeOS/Workflows/Update.md b/LifeOS/install/skills/LifeOS/Workflows/Update.md index 162fbfe192..aceef44db4 100644 --- a/LifeOS/install/skills/LifeOS/Workflows/Update.md +++ b/LifeOS/install/skills/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/skills/LifeOS/install/CLAUDE.template.md b/LifeOS/install/skills/LifeOS/install/CLAUDE.template.md index 0211d3ed7c..a0db7ec71a 100644 --- a/LifeOS/install/skills/LifeOS/install/CLAUDE.template.md +++ b/LifeOS/install/skills/LifeOS/install/CLAUDE.template.md @@ -1,4 +1,4 @@ -# LifeOS {{LIFEOS_VERSION}} — LifeOS (the Life Operating System) +# LifeOS 7.1.1 — LifeOS (the Life Operating System) > **LifeOS is the Life OS. The DA is the principal's AI assistant. Pulse is the Life Dashboard.** > Canonical thesis: `LIFEOS/DOCUMENTATION/LifeOs/LifeOsThesis.md`. Everyone running LifeOS names their own DA. LifeOS targets AS3 on the LifeOS Maturity Model, with lineage from "The Real Internet of Things" (2016). @@ -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/skills/LifeOS/install/install.sh b/LifeOS/install/skills/LifeOS/install/install.sh index 7daf137211..a3aa90de50 100755 --- a/LifeOS/install/skills/LifeOS/install/install.sh +++ b/LifeOS/install/skills/LifeOS/install/install.sh @@ -19,9 +19,34 @@ # # Local/offline install (no network): # LIFEOS_SRC=/path/to/LIFEOS_RELEASES/ bash install.sh +# +# Custom LifeOS home (install somewhere other than ~/.claude — e.g. a +# project-scoped `.claude` so a second, isolated LifeOS can live beside +# your global one): +# bash install.sh --home ~/MyProject/.claude +# LIFEOS_HOME=~/MyProject/.claude bash install.sh # equivalent +# The var is exported so the agentic `/lifeos-setup` (launched below in +# the same shell) installs into it too — without it, the setup offers +# the location choice interactively. See INSTALL.md § 2.5 for how a +# custom home is LOADED at runtime. # ═══════════════════════════════════════════════════════════════════ set -euo pipefail +# ─── Args ───────────────────────────────────────────────────────── +# --home sets LIFEOS_HOME (the custom LifeOS home). Env var form works too. +while [ $# -gt 0 ]; do + case "$1" in + --home) LIFEOS_HOME="${2:?--home needs a directory}"; shift 2 ;; + --home=*) LIFEOS_HOME="${1#--home=}"; shift ;; + *) echo "unknown argument: $1 (supported: --home )" >&2; exit 1 ;; + esac +done +LIFEOS_HOME="${LIFEOS_HOME:-}" +if [ -n "$LIFEOS_HOME" ]; then + case "$LIFEOS_HOME" in "~"*) LIFEOS_HOME="${HOME}${LIFEOS_HOME#\~}" ;; esac + export LIFEOS_HOME +fi + # ─── Release resolution — always the latest published release ───── # No pin: this resolves the newest GitHub Release at run time, so every new # release reaches every installer with zero edits here. Override with @@ -127,10 +152,12 @@ success "bun ($(command -v bun), v$(bun --version 2>/dev/null))" # ─── Step 2: Detect harness (no clobber) ───────────────────────── step "2/5 Detecting your harness" if [ -z "$LIFEOS_SKILLS_DIR" ]; then - if [ -d "$HOME/.claude" ]; then LIFEOS_SKILLS_DIR="$HOME/.claude/skills" + if [ -n "$LIFEOS_HOME" ]; then LIFEOS_SKILLS_DIR="$LIFEOS_HOME/skills" + elif [ -d "$HOME/.claude" ]; then LIFEOS_SKILLS_DIR="$HOME/.claude/skills" elif [ -d "$HOME/.config/claude" ]; then LIFEOS_SKILLS_DIR="$HOME/.config/claude/skills" else LIFEOS_SKILLS_DIR="$HOME/.claude/skills"; fi fi +[ -n "$LIFEOS_HOME" ] && info "Custom LifeOS home: ${BOLD}${LIFEOS_HOME/#$HOME/~}${RESET} (LIFEOS_HOME exported for the setup)" info "Skills dir: ${BOLD}${LIFEOS_SKILLS_DIR/#$HOME/~}${RESET}" TARGET="$LIFEOS_SKILLS_DIR/LifeOS" if [ -e "$TARGET" ]; then @@ -178,7 +205,18 @@ info "hooks with your permission. Nothing changes without you saying yes." echo if command -v claude >/dev/null 2>&1 && [ -z "${CLAUDECODE:-}" ]; then info "Launching setup..." + # LIFEOS_HOME is our installer override, not a Claude Code setting. Point the + # bootstrap session at the staged root so it can actually discover the new + # LifeOS skill regardless of the caller's current working directory. + if [ -n "$LIFEOS_HOME" ]; then + export CLAUDE_CONFIG_DIR="$LIFEOS_HOME" + fi exec claude "/lifeos-setup" else - printf " ${BOLD}Open your harness and run:${RESET} ${LIGHT_BLUE}/lifeos-setup${RESET}\n\n" + if [ -n "$LIFEOS_HOME" ]; then + printf -v SETUP_ROOT_Q '%q' "$LIFEOS_HOME" + printf " ${BOLD}Open a terminal and run:${RESET} ${LIGHT_BLUE}CLAUDE_CONFIG_DIR=%s claude /lifeos-setup${RESET}\n\n" "$SETUP_ROOT_Q" + else + printf " ${BOLD}Open your harness and run:${RESET} ${LIGHT_BLUE}/lifeos-setup${RESET}\n\n" + fi fi diff --git a/LifeOS/install/skills/LifeOS/install/settings.system.json b/LifeOS/install/skills/LifeOS/install/settings.system.json index 805f1c4ea5..ea82521027 100644 --- a/LifeOS/install/skills/LifeOS/install/settings.system.json +++ b/LifeOS/install/skills/LifeOS/install/settings.system.json @@ -3,10 +3,11 @@ "skillListingBudgetFraction": 0.15, "env": { "LIFEOS_DIR": "$HOME/.claude/LIFEOS", + "LIFEOS_ROOT": "$HOME/.claude", "PROJECTS_DIR": "$HOME/Projects", "BASH_DEFAULT_TIMEOUT_MS": "600000", "API_TIMEOUT_MS": "1800000", - "LIFEOS_CONFIG_DIR": "$HOME/.claude/LIFEOS", + "LIFEOS_CONFIG_DIR": "$HOME/.config/LIFEOS", "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1", "CLAUDE_CODE_FORK_SUBAGENT": "1" }, @@ -48,8 +49,6 @@ "Write(~/Projects/**)", "Edit(~/.claude/**)", "Edit(~/Projects/**)", - "MultiEdit(~/.claude/**)", - "MultiEdit(~/Projects/**)", "Bash(grep:*)", "Bash(rg:*)", "Bash(egrep:*)", @@ -170,25 +169,22 @@ "Agent(Forge)", "Agent(pr-review-toolkit:*)", "Agent(statusline-setup)", - "mcp__*", + "mcp__claude_ai_Gmail__*", + "mcp__claude_ai_Google_Calendar__*", + "mcp__claude_ai_Google_Drive__*", + "mcp__claude_ai_Spotify__*", "Write(~/Downloads/**)", "Edit(~/Downloads/**)", - "MultiEdit(~/Downloads/**)", "Write(/tmp/**)", "Edit(/tmp/**)", - "MultiEdit(/tmp/**)", "Write(/private/tmp/**)", "Edit(/private/tmp/**)", - "MultiEdit(/private/tmp/**)", "Write(**/tmp/**)", "Edit(**/tmp/**)", - "MultiEdit(**/tmp/**)", "Write(tmp/**)", "Edit(tmp/**)", - "MultiEdit(tmp/**)", "Write(/var/folders/**)", "Edit(/var/folders/**)", - "MultiEdit(/var/folders/**)", "Bash(git push:*)", "Bash(git mv:*)", "Bash(git add:*)", @@ -304,11 +300,7 @@ "Edit(~/.claude/.env)", "Edit(~/.claude/.env.*)", "Edit(~/.claude/**/.env)", - "Edit(~/.claude/**/.env.*)", - "MultiEdit(~/.claude/.env)", - "MultiEdit(~/.claude/.env.*)", - "MultiEdit(~/.claude/**/.env)", - "MultiEdit(~/.claude/**/.env.*)" + "Edit(~/.claude/**/.env.*)" ], "defaultMode": "auto", "additionalDirectories": [] @@ -321,11 +313,6 @@ "http://localhost:31337/hooks/*" ], "httpHookAllowedEnvVars": [], - "statusLine": { - "type": "command", - "command": "$HOME/.claude/LIFEOS/LIFEOS_StatusLine.sh", - "refreshInterval": 1 - }, "enabledPlugins": { "code-review@claude-plugins-official": true, "pr-review-toolkit@claude-plugins-official": true, @@ -395,7 +382,6 @@ "~/Projects/ contains project repositories. ALL operations within ~/Projects/ are trusted — cd, build, deploy, query, test commands are routine development work.", "Routine trusted operations include: creating directories under MEMORY/WORK/ for ISAs, writing/editing any file under ~/.claude/LIFEOS/, running bun, bunx, git, curl, wrangler commands, spawning agents and tasks, mkdir -p for any path under ~/.claude/, and all file operations on LifeOS infrastructure files.", "Compound shell commands (cd X && bunx Y, cmd | rg Z) are standard development patterns — always approve when sub-commands are individually trusted.", - "Source control: github.com/", "Trusted local service: pai:31337" ] }, diff --git a/LifeOS/install/skills/LocalIntelligence/References/DataSources.md b/LifeOS/install/skills/LocalIntelligence/References/DataSources.md index 7ac99449d9..76b1e09ce4 100644 --- a/LifeOS/install/skills/LocalIntelligence/References/DataSources.md +++ b/LifeOS/install/skills/LocalIntelligence/References/DataSources.md @@ -68,4 +68,4 @@ Public-data only. No paid people-search aggregators, no bypassing CAPTCHAs. ## Optional Customization Layer -Per-user source overrides go in `~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/LocalIntelligence/PREFERENCES.md`. Examples: an OpenStates API key for higher rate limits, a Google News topic ID, additional regional newspaper RSS feeds. The skill body never hardcodes any of this. +Per-user source overrides go in `{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/LocalIntelligence/PREFERENCES.md`. Examples: an OpenStates API key for higher rate limits, a Google News topic ID, additional regional newspaper RSS feeds. The skill body never hardcodes any of this. diff --git a/LifeOS/install/skills/LocalIntelligence/SKILL.md b/LifeOS/install/skills/LocalIntelligence/SKILL.md index ece3b4454b..6f76ec2a7c 100644 --- a/LifeOS/install/skills/LocalIntelligence/SKILL.md +++ b/LifeOS/install/skills/LocalIntelligence/SKILL.md @@ -10,7 +10,7 @@ effort: medium ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/LocalIntelligence/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/LocalIntelligence/` If this directory exists, load and apply any `PREFERENCES.md`, optional source-list overrides, or per-source API keys (e.g., OpenStates, Google News topic ID). These override defaults. If the directory does not exist, proceed with skill defaults — universal sources only. @@ -52,7 +52,7 @@ import { readHometown } from "./Tools/Hometown.ts" const { city, state, zip, county } = await readHometown() ``` -`Tools/Hometown.ts` parses the `**Hometown:**` line from `~/.claude/LIFEOS/USER/PRINCIPAL/PRINCIPAL_IDENTITY.md`. If absent, every workflow surfaces a clear "no hometown set" message and refuses to fetch. There is no fallback city. +`Tools/Hometown.ts` parses the `**Hometown:**` line from `{{LIFEOS_DIR}}/USER/PRINCIPAL/PRINCIPAL_IDENTITY.md`. If absent, every workflow surfaces a clear "no hometown set" message and refuses to fetch. There is no fallback city. ## Workflow Routing @@ -98,7 +98,7 @@ LocalIntelligence/ └── DataSources.md catalog of universal civic sources keyed off {city,state} ``` -Output: `~/.claude/LIFEOS/MEMORY/DATA/LocalIntelligence/___digest.json` plus a copy at `latest.json` for Pulse to read. +Output: `{{LIFEOS_DIR}}/MEMORY/DATA/LocalIntelligence/___digest.json` plus a copy at `latest.json` for Pulse to read. ## Fetcher Contract @@ -116,8 +116,8 @@ Fetchers return the empty/unavailable case rather than throwing. `Refresh.ts` ru The skill writes JSON; Pulse reads it. Coupling lives in two places: -1. **Pulse module** at `~/.claude/LIFEOS/PULSE/modules/local-intelligence.ts` — read-only over `MEMORY/DATA/LocalIntelligence/latest.json`. Endpoints: `GET /api/local-intelligence`, `POST /api/local-intelligence/refresh`. -2. **Pulse dashboard tab** at `~/.claude/LIFEOS/PULSE/Observability/src/app/local/page.tsx` — fetches the JSON and renders nine section cards. Nav entry in `AppHeader.tsx` `lifeNav` between `LIFE` and `WORK`. +1. **Pulse module** at `{{LIFEOS_DIR}}/PULSE/modules/local-intelligence.ts` — read-only over `MEMORY/DATA/LocalIntelligence/latest.json`. Endpoints: `GET /api/local-intelligence`, `POST /api/local-intelligence/refresh`. +2. **Pulse dashboard tab** at `{{LIFEOS_DIR}}/PULSE/Observability/src/app/local/page.tsx` — fetches the JSON and renders nine section cards. Nav entry in `AppHeader.tsx` `lifeNav` between `LIFE` and `WORK`. Daily refresh: `[[job]]` in `PULSE.toml` at `0 6 * * *` running `bun run skills/LocalIntelligence/Tools/Refresh.ts`. @@ -167,7 +167,7 @@ User clicks "Refresh now" on the LOCAL tab This skill body is generic by design. Pre-flight grep: ```bash -rg -i "|||/Users/[a-z]+/" ~/.claude/skills/LocalIntelligence/ +rg -i "|||/Users/[a-z]+/" "${CLAUDE_SKILL_DIR}/" ``` Zero matches required before treating the skill as releasable. The principal's actual hometown lives in `PRINCIPAL_IDENTITY.md`, never here. @@ -177,5 +177,5 @@ Zero matches required before treating the skill as releasable. The principal's a After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"LocalIntelligence","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"LocalIntelligence","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` diff --git a/LifeOS/install/skills/LocalIntelligence/Tools/Hometown.ts b/LifeOS/install/skills/LocalIntelligence/Tools/Hometown.ts index 9dabd18b94..4290e451d0 100755 --- a/LifeOS/install/skills/LocalIntelligence/Tools/Hometown.ts +++ b/LifeOS/install/skills/LocalIntelligence/Tools/Hometown.ts @@ -19,6 +19,7 @@ import { readFile } from "node:fs/promises" import { join } from "node:path" import { homedir } from "node:os" +import { claudeDir } from "../../../LIFEOS/TOOLS/lifeos-root"; export interface Hometown { city: string @@ -42,9 +43,7 @@ export class NoHometownError extends Error { } } -const IDENTITY_DEFAULT = join( - homedir(), - ".claude", +const IDENTITY_DEFAULT = join(claudeDir(), "LIFEOS", "USER", "PRINCIPAL", diff --git a/LifeOS/install/skills/LocalIntelligence/Tools/Refresh.ts b/LifeOS/install/skills/LocalIntelligence/Tools/Refresh.ts index b804f19ee3..2916c58ac3 100755 --- a/LifeOS/install/skills/LocalIntelligence/Tools/Refresh.ts +++ b/LifeOS/install/skills/LocalIntelligence/Tools/Refresh.ts @@ -21,8 +21,9 @@ import { fetchElections } from "./FetchElections.ts" import { fetchArrests } from "./FetchArrests.ts" import { fetchNews } from "./FetchNews.ts" import { fetchCrime } from "./FetchCrime.ts" +import { claudeDir } from "../../../LIFEOS/TOOLS/lifeos-root"; -const DATA_DIR = join(homedir(), ".claude", "LIFEOS", "MEMORY", "DATA", "LocalIntelligence") +const DATA_DIR = join(claudeDir(), "LIFEOS", "MEMORY", "DATA", "LocalIntelligence") const fetchers: Record = { construction: fetchConstruction, diff --git a/LifeOS/install/skills/LocalIntelligence/Workflows/DailyBrief.md b/LifeOS/install/skills/LocalIntelligence/Workflows/DailyBrief.md index 9276abccab..4df1207e15 100644 --- a/LifeOS/install/skills/LocalIntelligence/Workflows/DailyBrief.md +++ b/LifeOS/install/skills/LocalIntelligence/Workflows/DailyBrief.md @@ -16,8 +16,8 @@ Running **DailyBrief** in **LocalIntelligence**... ## Procedure 1. Resolve hometown via `Tools/Hometown.ts`. If absent, surface a setup-help message and exit. -2. Run `bun run ~/.claude/skills/LocalIntelligence/Tools/Refresh.ts` — orchestrator runs all eight fetchers via `Promise.allSettled`. -3. Read the resulting `~/.claude/LIFEOS/MEMORY/DATA/LocalIntelligence/latest.json`. +2. Run `bun run "${LIFEOS_ROOT}/skills/LocalIntelligence/Tools/Refresh.ts"` — orchestrator runs all eight fetchers via `Promise.allSettled`. +3. Read the resulting `{{LIFEOS_DIR}}/MEMORY/DATA/LocalIntelligence/latest.json`. 4. Summarize top 3 items per category, with date and source link. 5. Surface any `meta.errors` entries — name the failing source, do not hide it. @@ -30,11 +30,11 @@ Running **DailyBrief** in **LocalIntelligence**... | "json only" | `--json` | Emit raw JSON, no chat summary | ```bash -bun run ~/.claude/skills/LocalIntelligence/Tools/Refresh.ts [--force] [--summary] [--json] +bun run "${LIFEOS_ROOT}/skills/LocalIntelligence/Tools/Refresh.ts" [--force] [--summary] [--json] ``` ## Output -- File: `~/.claude/LIFEOS/MEMORY/DATA/LocalIntelligence/___digest.json` -- Symlink: `~/.claude/LIFEOS/MEMORY/DATA/LocalIntelligence/latest.json` +- File: `{{LIFEOS_DIR}}/MEMORY/DATA/LocalIntelligence/___digest.json` +- Symlink: `{{LIFEOS_DIR}}/MEMORY/DATA/LocalIntelligence/latest.json` - Chat: top-3 items per section + `meta.errors` listed if any. diff --git a/LifeOS/install/skills/Loop/SKILL.md b/LifeOS/install/skills/Loop/SKILL.md index 49bc0a1635..e5b08ea6ed 100644 --- a/LifeOS/install/skills/Loop/SKILL.md +++ b/LifeOS/install/skills/Loop/SKILL.md @@ -24,7 +24,7 @@ Each iteration is a full Algorithm cycle (OBSERVE → LEARN). The LEARN phase of ``` /loop --target "path/to/target" --iterations 5 -/loop --target "~/.claude/skills/Art/Workflows/TechnicalDiagrams.md" --goal "make diagrams more consistent" +/loop --target "{{LIFEOS_ROOT}}/skills/Art/Workflows/TechnicalDiagrams.md" --goal "make diagrams more consistent" /loop --resume # Resume a previous loop /loop --status # Show iteration history ``` @@ -70,7 +70,7 @@ Default /loop behavior is unchanged — autoresearch is opt-in only. Intended fo ## Examples ``` -/loop --target "~/.claude/skills/Research" --goal "improve output quality" --iterations 5 +/loop --target "{{LIFEOS_ROOT}}/skills/Research" --goal "improve output quality" --iterations 5 /loop --target "prompts/summarize.md" --goal "more concise, less filler" ``` diff --git a/LifeOS/install/skills/Migrate/SKILL.md b/LifeOS/install/skills/Migrate/SKILL.md index 3555611104..0283bd83bf 100644 --- a/LifeOS/install/skills/Migrate/SKILL.md +++ b/LifeOS/install/skills/Migrate/SKILL.md @@ -75,9 +75,9 @@ Collect the source path. If content is pasted, write it to a temp file first. Run the scanner: ```bash -bun ~/.claude/LIFEOS/TOOLS/MigrateScan.ts --source +bun "${LIFEOS_DIR}/TOOLS/MigrateScan.ts" --source # or -echo "$CONTENT" | bun ~/.claude/LIFEOS/TOOLS/MigrateScan.ts --stdin +echo "$CONTENT" | bun "${LIFEOS_DIR}/TOOLS/MigrateScan.ts" --stdin ``` Scanner output includes: @@ -114,19 +114,19 @@ Based on the user's preference: **Fast path** (he says "approve all trusted"): ```bash -bun ~/.claude/LIFEOS/TOOLS/MigrateApprove.ts --approve-all +bun "${LIFEOS_DIR}/TOOLS/MigrateApprove.ts" --approve-all ``` Commits everything non-UNCLEAR. Then walk through UNCLEAR chunks conversationally. **Category path** (he says "approve goals and wisdom, skip knowledge"): ```bash -bun ~/.claude/LIFEOS/TOOLS/MigrateApprove.ts --approve-target TELOS/GOALS.md -bun ~/.claude/LIFEOS/TOOLS/MigrateApprove.ts --approve-target TELOS/WISDOM.md +bun "${LIFEOS_DIR}/TOOLS/MigrateApprove.ts" --approve-target TELOS/GOALS.md +bun "${LIFEOS_DIR}/TOOLS/MigrateApprove.ts" --approve-target TELOS/WISDOM.md ``` **Walk-through path** (he wants careful review): ```bash -bun ~/.claude/LIFEOS/TOOLS/MigrateApprove.ts --review +bun "${LIFEOS_DIR}/TOOLS/MigrateApprove.ts" --review ``` Show each pending chunk. For each: - Show preview + proposed target + confidence + alternatives diff --git a/LifeOS/install/skills/Optimize/SKILL.md b/LifeOS/install/skills/Optimize/SKILL.md index 8c2aae4059..bcf8389703 100644 --- a/LifeOS/install/skills/Optimize/SKILL.md +++ b/LifeOS/install/skills/Optimize/SKILL.md @@ -43,10 +43,10 @@ Inspired by Karpathy's [autoresearch](https://github.com/karpathy/autoresearch) ### Eval Mode (skill/prompt/agent targets) ``` -/optimize --target "~/.claude/skills/ExtractWisdom" -/optimize --target "~/.claude/skills/Research/Workflows/QuickResearch.md" +/optimize --target "{{LIFEOS_ROOT}}/skills/ExtractWisdom" +/optimize --target "{{LIFEOS_ROOT}}/skills/Research/Workflows/QuickResearch.md" /optimize --target "prompts/my-prompt.md" -/optimize --target "~/.claude/skills/ExtractWisdom" --max-experiments 20 +/optimize --target "{{LIFEOS_ROOT}}/skills/ExtractWisdom" --max-experiments 20 ``` In eval mode, the system automatically: @@ -117,9 +117,9 @@ When `/optimize` is invoked, the Algorithm enters with `mode: optimize` in the I ISC criteria become **guard rails** — assertions that must hold true across ALL experiments. Guard rails must REMAIN satisfied perpetually. A violation triggers automatic revert regardless of score improvement. **Reference files:** -- `~/.claude/LIFEOS/ALGORITHM/optimize-loop.md` — the full loop protocol -- `~/.claude/LIFEOS/ALGORITHM/eval-guide.md` — how to write good eval criteria -- `~/.claude/LIFEOS/ALGORITHM/target-types.md` — target detection and ISC generation +- `{{LIFEOS_DIR}}/ALGORITHM/optimize-loop.md` — the full loop protocol +- `{{LIFEOS_DIR}}/ALGORITHM/eval-guide.md` — how to write good eval criteria +- `{{LIFEOS_DIR}}/ALGORITHM/target-types.md` — target detection and ISC generation ## Examples @@ -155,7 +155,7 @@ ISC criteria become **guard rails** — assertions that must hold true across AL **Optimize a skill's Extract workflow:** ``` -/optimize --target "~/.claude/skills/ExtractWisdom" --max-experiments 15 +/optimize --target "{{LIFEOS_ROOT}}/skills/ExtractWisdom" --max-experiments 15 ``` **Optimize a standalone prompt:** @@ -165,7 +165,7 @@ ISC criteria become **guard rails** — assertions that must hold true across AL **Optimize with custom criteria:** ``` -/optimize --target "~/.claude/skills/Research/Workflows/QuickResearch.md" \ +/optimize --target "{{LIFEOS_ROOT}}/skills/Research/Workflows/QuickResearch.md" \ --criteria "Does the output contain specific facts with sources?" \ "Is the output structured with clear sections?" \ "Does the output avoid generic filler?" \ diff --git a/LifeOS/install/skills/PrivateInvestigator/SKILL.md b/LifeOS/install/skills/PrivateInvestigator/SKILL.md index 7248f5d7da..3d7c4d2724 100644 --- a/LifeOS/install/skills/PrivateInvestigator/SKILL.md +++ b/LifeOS/install/skills/PrivateInvestigator/SKILL.md @@ -8,7 +8,7 @@ effort: high ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/PrivateInvestigator/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/PrivateInvestigator/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -208,7 +208,7 @@ User: "Find Jane Doe's social media, she's a marketing professional in Denver" After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"PrivateInvestigator","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"PrivateInvestigator","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/Prompting/SKILL.md b/LifeOS/install/skills/Prompting/SKILL.md index a8a33390cb..c8e03c04a7 100644 --- a/LifeOS/install/skills/Prompting/SKILL.md +++ b/LifeOS/install/skills/Prompting/SKILL.md @@ -8,7 +8,7 @@ effort: medium ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Prompting/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/Prompting/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -199,7 +199,7 @@ The templating system eliminated **~35,000 tokens (65% reduction)** across LifeO After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Prompting","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Prompting","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/Prompting/Standards.md b/LifeOS/install/skills/Prompting/Standards.md index e606f728e4..e93be8afdc 100755 --- a/LifeOS/install/skills/Prompting/Standards.md +++ b/LifeOS/install/skills/Prompting/Standards.md @@ -761,7 +761,7 @@ An **open-source framework** for augmenting humans using AI. ## Native Fabric Patterns in LifeOS -**Location:** `~/.claude/skills/Fabric/Patterns/` +**Location:** `{{LIFEOS_ROOT}}/skills/Fabric/Patterns/` LifeOS maintains a local copy of all Fabric patterns for native execution. Instead of spawning the `fabric` CLI for every pattern-based task, the system reads and applies patterns directly as prompts. @@ -790,7 +790,7 @@ These operations require the CLI because they access external services or config Run the update script to sync latest patterns: ```bash -~/.claude/skills/Fabric/Tools/update-patterns.sh +"${LIFEOS_ROOT}/skills/Fabric/Tools/update-patterns.sh" ``` This pulls upstream updates via `fabric -U` and syncs to LifeOS's local copy. @@ -1225,7 +1225,7 @@ Reusable quality and completion checks. ## Template Location -All templates live in `~/.claude/skills/Prompting/Templates/`: +All templates live in `{{LIFEOS_ROOT}}/skills/Prompting/Templates/`: ``` skills/Prompting/ @@ -1252,7 +1252,7 @@ skills/Prompting/ **CLI Usage:** ```bash -bun ~/.claude/skills/Prompting/Tools/RenderTemplate.ts \ +bun "${LIFEOS_ROOT}/skills/Prompting/Tools/RenderTemplate.ts" \ --template Primitives/Roster.hbs \ --data Data/Agents.yaml \ --output Compiled/AgentRoster.md @@ -1260,7 +1260,8 @@ bun ~/.claude/skills/Prompting/Tools/RenderTemplate.ts \ **Programmatic Usage:** ```typescript -import { renderTemplate } from '~/.claude/skills/Prompting/Tools/RenderTemplate.ts'; +// From a script at the Prompting skill root: +import { renderTemplate } from './Tools/RenderTemplate.ts'; const output = renderTemplate('Primitives/Briefing.hbs', { agent: { id: 'EN-1', name: 'Skeptical Thinker', personality: '...' }, diff --git a/LifeOS/install/skills/Prompting/Templates/README.md b/LifeOS/install/skills/Prompting/Templates/README.md index 2198590795..a42d114d95 100755 --- a/LifeOS/install/skills/Prompting/Templates/README.md +++ b/LifeOS/install/skills/Prompting/Templates/README.md @@ -120,7 +120,7 @@ Statistical reporting with confidence intervals. ### Basic Rendering ```bash -bun run ~/.claude/skills/Prompting/Tools/RenderTemplate.ts \ +bun run "${LIFEOS_ROOT}/skills/Prompting/Tools/RenderTemplate.ts" \ --template Primitives/Roster.hbs \ --data Data/Agents.yaml \ --output Compiled/AgentRoster.md @@ -164,7 +164,7 @@ bun run ~/.claude/skills/Prompting/Tools/RenderTemplate.ts \ If anything breaks, rollback to v2.5.0: ```bash -cd ~/.claude +cd "${LIFEOS_ROOT}" git checkout v2.5.0 # Or to just undo templating: rm -rf Templates/ @@ -229,7 +229,7 @@ The RenderTemplate.ts engine provides these custom Handlebars helpers: ### Render a Template ```bash -bun run ~/.claude/skills/Prompting/Tools/RenderTemplate.ts \ +bun run "${LIFEOS_ROOT}/skills/Prompting/Tools/RenderTemplate.ts" \ --template Primitives/Roster.hbs \ --data Data/Agents.yaml \ --output Compiled/AgentRoster.md @@ -238,7 +238,7 @@ bun run ~/.claude/skills/Prompting/Tools/RenderTemplate.ts \ ### Preview Without Writing ```bash -bun run ~/.claude/skills/Prompting/Tools/RenderTemplate.ts \ +bun run "${LIFEOS_ROOT}/skills/Prompting/Tools/RenderTemplate.ts" \ --template Evals/Judge.hbs \ --data path/to/judge-config.yaml \ --preview @@ -247,7 +247,7 @@ bun run ~/.claude/skills/Prompting/Tools/RenderTemplate.ts \ ### Validate Template Syntax ```bash -bun run ~/.claude/skills/Prompting/Tools/ValidateTemplate.ts \ +bun run "${LIFEOS_ROOT}/skills/Prompting/Tools/ValidateTemplate.ts" \ --template Primitives/Briefing.hbs \ --data Data/sample-briefing.yaml ``` @@ -304,7 +304,7 @@ This system is based on research from: ## Related Documentation -- `~/.claude/LIFEOS/Prompting.md` (Templating section) -- `~/.claude/History/research/2025-12/2025-12-09-templating-system-research.md` -- `~/.claude/History/learnings/2025-12/2025-12-09-021700_LEARNING_complete-templating-system-and-evals-integration.md` -- `~/.claude/skills/Evals/SKILL.md` +- `{{LIFEOS_DIR}}/Prompting.md` (Templating section) +- `{{LIFEOS_ROOT}}/History/research/2025-12/2025-12-09-templating-system-research.md` +- `{{LIFEOS_ROOT}}/History/learnings/2025-12/2025-12-09-021700_LEARNING_complete-templating-system-and-evals-integration.md` +- `{{LIFEOS_ROOT}}/skills/Evals/SKILL.md` diff --git a/LifeOS/install/skills/RedTeam/SKILL.md b/LifeOS/install/skills/RedTeam/SKILL.md index b7427567c4..c5b5fae168 100755 --- a/LifeOS/install/skills/RedTeam/SKILL.md +++ b/LifeOS/install/skills/RedTeam/SKILL.md @@ -8,7 +8,7 @@ effort: high ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/RedTeam/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/RedTeam/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -119,7 +119,7 @@ User: "battle of bots - which approach is better for this feature?" After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"RedTeam","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"RedTeam","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/Remotion/ArtIntegration.md b/LifeOS/install/skills/Remotion/ArtIntegration.md index 1f876e4f61..3441322976 100644 --- a/LifeOS/install/skills/Remotion/ArtIntegration.md +++ b/LifeOS/install/skills/Remotion/ArtIntegration.md @@ -6,7 +6,7 @@ 1. **Load Art preferences:** ``` - ~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Art/PREFERENCES.md + {{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/Art/PREFERENCES.md ``` 2. **Apply the LifeOS Theme** derived from Art preferences: @@ -21,18 +21,20 @@ 3. **Use Theme Constants:** ``` - ~/.claude/skills/Remotion/Tools/Theme.ts + {{LIFEOS_ROOT}}/skills/Remotion/Tools/Theme.ts ``` + Copy this file into the generated project as `src/theme.ts`; project code + imports that local copy instead of embedding an installation-specific path. 4. **Reference images** (when visual style reference needed): ``` - ~/.claude/skills/Art/Examples/ + {{LIFEOS_ROOT}}/skills/Art/Examples/ ``` ## LifeOS Theme Quick Reference ```typescript -import { LIFEOS_THEME } from '~/.claude/skills/Remotion/Tools/Theme' +import { LIFEOS_THEME } from './theme' // Colors LIFEOS_THEME.colors.background // #0f172a - Deep slate @@ -59,7 +61,7 @@ LIFEOS_THEME.spacing.element // 30px between elements ## Using the Theme in Components ```typescript -import { LIFEOS_THEME, titleScreenStyle, fadeInterpolation } from '~/.claude/skills/Remotion/Tools/Theme' +import { LIFEOS_THEME, titleScreenStyle, fadeInterpolation } from './theme' export const MyScene: React.FC = () => { const frame = useCurrentFrame() diff --git a/LifeOS/install/skills/Remotion/Patterns.md b/LifeOS/install/skills/Remotion/Patterns.md index 12bdce2d2f..066ac9e1fc 100644 --- a/LifeOS/install/skills/Remotion/Patterns.md +++ b/LifeOS/install/skills/Remotion/Patterns.md @@ -127,7 +127,7 @@ import { Audio, Video, staticFile } from 'remotion' For detailed patterns on specific topics, see: ``` -~/.claude/skills/Remotion/Tools/Reference/ +{{LIFEOS_ROOT}}/skills/Remotion/Tools/Reference/ ``` Topics include: animations, audio, 3d, charts, captions, fonts, transitions, and more. diff --git a/LifeOS/install/skills/Remotion/SKILL.md b/LifeOS/install/skills/Remotion/SKILL.md index 60c726c720..16973f8f72 100644 --- a/LifeOS/install/skills/Remotion/SKILL.md +++ b/LifeOS/install/skills/Remotion/SKILL.md @@ -41,7 +41,7 @@ Define a composition as React, animate with `useCurrentFrame()`, render with `bu ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Remotion/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/Remotion/` ## Workflow Routing @@ -105,7 +105,7 @@ User: "create a video showing how the Algorithm works" After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Remotion","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Remotion","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/Remotion/Workflows/ContentToAnimation.md b/LifeOS/install/skills/Remotion/Workflows/ContentToAnimation.md index cc9b74803c..f663c6ba5d 100644 --- a/LifeOS/install/skills/Remotion/Workflows/ContentToAnimation.md +++ b/LifeOS/install/skills/Remotion/Workflows/ContentToAnimation.md @@ -40,7 +40,7 @@ This workflow handles ANY input via the Parser skill: **For YouTube:** ```bash # Get transcript via Parser skill -# Load: ~/.claude/skills/Parser/Workflows/ExtractYoutube.md +# Load: {{LIFEOS_ROOT}}/skills/Parser/Workflows/ExtractYoutube.md ``` **For articles/blogs:** @@ -362,7 +362,7 @@ Cannot proceed - fix logical issues before rendering **MANDATORY: Apply LifeOS Theme** ```typescript -import { LIFEOS_THEME } from '~/.claude/skills/Remotion/theme' +import { LIFEOS_THEME } from './theme' // All components MUST use: // - LIFEOS_THEME.colors for all colors @@ -545,6 +545,6 @@ User: animate this content: "The three pillars of AI safety are..." ## Integration with Art Skill This workflow inherits visual theming from Art preferences: -- Load: `~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Art/PREFERENCES.md` +- Load: `{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/Art/PREFERENCES.md` - Apply: Charcoal aesthetic, purple accents, organic animations - Reference: `~/.claude/` diff --git a/LifeOS/install/skills/Remotion/Workflows/GeneratedContentVideo.md b/LifeOS/install/skills/Remotion/Workflows/GeneratedContentVideo.md index cf08d18589..22685aa682 100644 --- a/LifeOS/install/skills/Remotion/Workflows/GeneratedContentVideo.md +++ b/LifeOS/install/skills/Remotion/Workflows/GeneratedContentVideo.md @@ -39,7 +39,7 @@ Output: `script.txt` and a parallel `scenes.json` with 4-8 entries describing vi For each scene in `scenes.json`, invoke the Art skill in parallel: ```bash -bun ~/.claude/skills/Art/Tools/Generate.ts \ +bun "${LIFEOS_ROOT}/skills/Art/Tools/Generate.ts" \ --prompt "" \ --model nano-banana-pro \ --aspect 9:16 \ diff --git a/LifeOS/install/skills/Research/MigrationNotes.md b/LifeOS/install/skills/Research/MigrationNotes.md index 98b1d45605..065e46ff5d 100755 --- a/LifeOS/install/skills/Research/MigrationNotes.md +++ b/LifeOS/install/skills/Research/MigrationNotes.md @@ -14,36 +14,36 @@ Successfully migrated 4 research commands to the research skill's workflows dire ## Files Migrated ### 1. Claude WebSearch Research -- **Source:** `~/.claude/commands/perform-claude-research.md` -- **Destination:** `~/.claude/skills/Research/Workflows/ClaudeResearch.md` +- **Source:** `{{LIFEOS_ROOT}}/commands/perform-claude-research.md` +- **Destination:** `{{LIFEOS_ROOT}}/skills/Research/Workflows/ClaudeResearch.md` - **Size:** 3.6K - **Description:** Intelligent query decomposition with Claude's WebSearch tool (free, no API keys) - **Triggers:** "claude research", "use websearch", "claude only" ### 2. Perplexity API Research -- **Source:** `~/.claude/commands/perform-perplexity-research.md` -- **Destination:** `~/.claude/skills/Research/Workflows/PerplexityResearch.md` +- **Source:** `{{LIFEOS_ROOT}}/commands/perform-perplexity-research.md` +- **Destination:** `{{LIFEOS_ROOT}}/skills/Research/Workflows/PerplexityResearch.md` - **Size:** 8.1K - **Description:** Fast web search with query decomposition via Perplexity API - **Triggers:** "perplexity research", "use perplexity", "sonar" ### 3. Interview Preparation -- **Source:** `~/.claude/commands/perform-interview-research.md` -- **Destination:** `~/.claude/skills/Research/Workflows/InterviewResearch.md` +- **Source:** `{{LIFEOS_ROOT}}/commands/perform-interview-research.md` +- **Destination:** `{{LIFEOS_ROOT}}/skills/Research/Workflows/InterviewResearch.md` - **Size:** 4.4K - **Description:** Tyler Cowen-style interview prep with Shannon surprise principle - **Triggers:** "interview research", "prepare interview questions", "sponsored interview" ### 4. AI Trends Analysis -- **Source:** `~/.claude/commands/analyze-ai-trends.md` -- **Destination:** `~/.claude/skills/Research/Workflows/AnalyzeAiTrends.md` +- **Source:** `{{LIFEOS_ROOT}}/commands/analyze-ai-trends.md` +- **Destination:** `{{LIFEOS_ROOT}}/skills/Research/Workflows/AnalyzeAiTrends.md` - **Size:** 3.0K - **Description:** Deep trend analysis across historical AI news logs - **Triggers:** "analyze ai trends", "trend analysis", "ai industry trends" ## Workflows Directory Status -**Location:** `~/.claude/skills/Research/Workflows/` +**Location:** `{{LIFEOS_ROOT}}/skills/Research/Workflows/` **Note (2026-01):** Conduct.md and PerplexityResearch.md were later removed. Perplexity functionality consolidated into QuickResearch.md (single-agent) and StandardResearch.md (multi-agent). @@ -86,7 +86,7 @@ Each workflow has: ✅ **ALL ORIGINALS PRESERVED** -The original command files remain in `~/.claude/commands/`: +The original command files remain in `{{LIFEOS_ROOT}}/commands/`: - `perform-claude-research.md` ✓ - `perform-perplexity-research.md` ✓ - `perform-interview-research.md` ✓ diff --git a/LifeOS/install/skills/Research/SKILL.md b/LifeOS/install/skills/Research/SKILL.md index b63d8e6748..8b307f76f1 100755 --- a/LifeOS/install/skills/Research/SKILL.md +++ b/LifeOS/install/skills/Research/SKILL.md @@ -24,7 +24,7 @@ context: fork ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Research/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/Research/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -184,7 +184,7 @@ See `Workflows/Verify.md` for full verification protocol. → Exit: When all CRITICAL/HIGH entities researched + all categories covered ``` -**Artifacts persist** at `~/.claude/LIFEOS/MEMORY/RESEARCH/{date}_{topic}/` — the vault survives across sessions. +**Artifacts persist** at `{{LIFEOS_DIR}}/MEMORY/RESEARCH/{date}_{topic}/` — the vault survives across sessions. See `Workflows/DeepInvestigation.md` for full workflow details. @@ -192,12 +192,12 @@ See `Workflows/DeepInvestigation.md` for full workflow details. ## File Organization -**Working files (temporary work artifacts):** `~/.claude/LIFEOS/MEMORY/WORK/{current_work}/` +**Working files (temporary work artifacts):** `{{LIFEOS_DIR}}/MEMORY/WORK/{current_work}/` - Read `~/.claude/` to get the `work_dir` value - All iterative work artifacts go in the current work item directory - This ties research artifacts to the work item for learning and context -**History (permanent):** `~/.claude/History/research/YYYY-MM/YYYY-MM-DD_[topic]/` +**History (permanent):** `{{LIFEOS_ROOT}}/History/research/YYYY-MM/YYYY-MM-DD_[topic]/` ## Gotchas @@ -248,7 +248,7 @@ User: "do a deep investigation of the AI agent market" After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Research","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Research","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/Research/Workflows/AnalyzeAiTrends.md b/LifeOS/install/skills/Research/Workflows/AnalyzeAiTrends.md index b0be5644cc..acf36a2df8 100755 --- a/LifeOS/install/skills/Research/Workflows/AnalyzeAiTrends.md +++ b/LifeOS/install/skills/Research/Workflows/AnalyzeAiTrends.md @@ -3,7 +3,7 @@ You are executing the analyze-ai-trends command to perform deep trend analysis a **Your task:** 1. **Load all historical AI news research** - - Read all files from: `~/.claude/History/research/` (filter for AI news files) + - Read all files from: `{{LIFEOS_ROOT}}/History/research/` (filter for AI news files) - Files may be in various formats: analysis.md, comprehensive-analysis.md, etc. - Sort chronologically to understand evolution over time diff --git a/LifeOS/install/skills/Research/Workflows/ClaudeResearch.md b/LifeOS/install/skills/Research/Workflows/ClaudeResearch.md index 972ced9a5a..2b6954ff24 100755 --- a/LifeOS/install/skills/Research/Workflows/ClaudeResearch.md +++ b/LifeOS/install/skills/Research/Workflows/ClaudeResearch.md @@ -6,7 +6,7 @@ * * ## Usage * ```bash - * bun ${LIFEOS_DIR}/commands/perform-claude-research.md "your complex research question here" + * bun {{LIFEOS_DIR}}/commands/perform-claude-research.md "your complex research question here" * ``` * * ## Features @@ -31,7 +31,7 @@ const originalQuestion = process.argv.slice(2).join(' '); if (!originalQuestion) { console.error('❌ Please provide a research question'); - console.error('Usage: bun ${LIFEOS_DIR}/commands/perform-claude-research.md "your question here"'); + console.error('Usage: bun {{LIFEOS_DIR}}/commands/perform-claude-research.md "your question here"'); process.exit(1); } diff --git a/LifeOS/install/skills/Research/Workflows/DeepInvestigation.md b/LifeOS/install/skills/Research/Workflows/DeepInvestigation.md index 382c7966aa..ac8ed988bf 100644 --- a/LifeOS/install/skills/Research/Workflows/DeepInvestigation.md +++ b/LifeOS/install/skills/Research/Workflows/DeepInvestigation.md @@ -39,7 +39,7 @@ Iteration 3+: Continue until coverage gates pass All artifacts persist at: ``` -~/.claude/LIFEOS/MEMORY/RESEARCH/{YYYY-MM}/{YYYY-MM-DD}_{topic-slug}/ +{{LIFEOS_DIR}}/MEMORY/RESEARCH/{YYYY-MM}/{YYYY-MM-DD}_{topic-slug}/ ``` @@ -340,7 +340,7 @@ IF either gate FAILS: ## Domain Template Packs -Templates live at `~/.claude/skills/Research/Templates/{DomainName}.md` +Templates live at `{{LIFEOS_ROOT}}/skills/Research/Templates/{DomainName}.md` Each template pack defines: 1. **Entity categories** for this domain (what types of things to discover) diff --git a/LifeOS/install/skills/Research/Workflows/ExtractAlpha.md b/LifeOS/install/skills/Research/Workflows/ExtractAlpha.md index 1ae82d8a00..c0e99a9701 100755 --- a/LifeOS/install/skills/Research/Workflows/ExtractAlpha.md +++ b/LifeOS/install/skills/Research/Workflows/ExtractAlpha.md @@ -121,13 +121,13 @@ Capture the subtle genius buried in the content. **Use the current work item directory for all working files during analysis:** ```bash -~/.claude/LIFEOS/MEMORY/WORK/{current_work}/ +"${LIFEOS_DIR}/MEMORY/WORK/"{current_work}/ ``` **To get the current work directory:** 1. Read `~/.claude/` 2. Extract the `work_dir` value -3. Use `~/.claude/LIFEOS/MEMORY/WORK/{work_dir}/` for temporary artifacts +3. Use `{{LIFEOS_DIR}}/MEMORY/WORK/{work_dir}/` for temporary artifacts **What goes in the work item directory:** - Raw transcripts from fabric -y @@ -143,7 +143,7 @@ Capture the subtle genius buried in the content. **Example work item structure:** ``` -~/.claude/LIFEOS/MEMORY/WORK/20260111-172408_extract-alpha-analysis/ +{{LIFEOS_DIR}}/MEMORY/WORK/20260111-172408_extract-alpha-analysis/ ├── raw-transcript.txt ├── deep thinking-notes.md ├── draft-insights.md @@ -155,7 +155,7 @@ Capture the subtle genius buried in the content. **Save final outputs to permanent history:** ```bash -~/.claude/History/research/YYYY-MM-DD_description/ +"${LIFEOS_ROOT}/History/research/YYYY-MM-DD_description/" ``` **What goes in history/research/:** @@ -166,7 +166,7 @@ Capture the subtle genius buried in the content. **Example history structure:** ``` -~/.claude/History/research/2025-10-26_podcast-analysis/ +{{LIFEOS_ROOT}}/History/research/2025-10-26_podcast-analysis/ ├── README.md # Research session documentation ├── extract_alpha.md # Final 24-30 insights ├── deep thinking-analysis.md # Full deep analysis @@ -209,16 +209,16 @@ Create a README.md in the history directory documenting the research: 1. **Check if hooks captured the output:** ```bash # Check most recent history entries - ls -lt ~/.claude/History/research/ | head -5 + ls -lt "${LIFEOS_ROOT}/History/research/" | head -5 # Verify your research directory exists - ls ~/.claude/History/research/YYYY-MM-DD_description/ + ls "${LIFEOS_ROOT}/History/research/YYYY-MM-DD_description/" ``` 2. **If hooks did NOT capture automatically:** ```bash # Create directory structure manually - mkdir -p ~/.claude/History/research/YYYY-MM-DD_description/ + mkdir -p "${LIFEOS_ROOT}/History/research/YYYY-MM-DD_description/" # Save extract_alpha.md (final insights) # Save deep thinking-analysis.md (full analysis) @@ -228,7 +228,7 @@ Create a README.md in the history directory documenting the research: 3. **Confirm all files saved:** ```bash - ls -lah ~/.claude/History/research/YYYY-MM-DD_description/ + ls -lah "${LIFEOS_ROOT}/History/research/YYYY-MM-DD_description/" # Should show: README.md, extract_alpha.md, deep thinking-analysis.md, metadata.json ``` @@ -236,10 +236,10 @@ Create a README.md in the history directory documenting the research: ```bash # 1. Get current work directory -WORK_DIR=$(jq -r '.sessions | to_entries | map(select(.value.phase != "complete" and .value.mode != "native" and .value.mode != "starting")) | sort_by(.value.updatedAt) | reverse | .[0].key // empty' ~/.claude/LIFEOS/MEMORY/STATE/work.json) +WORK_DIR=$(jq -r '.sessions | to_entries | map(select(.value.phase != "complete" and .value.mode != "native" and .value.mode != "starting")) | sort_by(.value.updatedAt) | reverse | .[0].key // empty' "${LIFEOS_DIR}/MEMORY/STATE/work.json") # 2. Work in current work item directory -cd ~/.claude/LIFEOS/MEMORY/WORK/${WORK_DIR}/ +cd "${LIFEOS_DIR}/MEMORY/WORK/$"{WORK_DIR}/ # 3. Extract content to work item directory fabric -y "YOUTUBE_URL" > raw-transcript.txt @@ -251,7 +251,7 @@ fabric -y "YOUTUBE_URL" > raw-transcript.txt # [Extract 24-30 insights from deep thinking analysis, draft in work item directory] # 6. Create permanent history directory -mkdir -p ~/.claude/History/research/$(date +%Y-%m-%d)_podcast-analysis/ +mkdir -p "${LIFEOS_ROOT}/History/research/$(date +%Y-%m-%d)_podcast-analysis/" # 7. Save final outputs to history # - extract_alpha.md (final insights) @@ -260,7 +260,7 @@ mkdir -p ~/.claude/History/research/$(date +%Y-%m-%d)_podcast-analysis/ # - metadata.json (source info) # 8. Verify hooks captured it -ls -lah ~/.claude/History/research/$(date +%Y-%m-%d)_podcast-analysis/ +ls -lah "${LIFEOS_ROOT}/History/research/$(date +%Y-%m-%d)_podcast-analysis/" # 9. Note: working artifacts remain tied to work item for learning # (Don't delete working files - they provide context for the work item) @@ -372,10 +372,10 @@ When this skill activates, LifeOS should: 1. **Load content** via appropriate method (fabric -y, WebFetch, Read, or paste) 2. **Get current work directory** - Read `~/.claude/` for `work_dir` -3. **Use work item directory** - Work in `~/.claude/LIFEOS/MEMORY/WORK/{work_dir}/` +3. **Use work item directory** - Work in `{{LIFEOS_DIR}}/MEMORY/WORK/{work_dir}/` 4. **Engage deep thinking mode** - Deep extended thinking through all 10 dimensions 5. **Extract insights** - Extract 24-30 highest-alpha ideas focusing on low-probability brilliant insights -6. **Save to history** - Final outputs to `~/.claude/History/research/YYYY-MM-DD_description/` +6. **Save to history** - Final outputs to `{{LIFEOS_ROOT}}/History/research/YYYY-MM-DD_description/` 7. **Verify capture** - Ensure hooks captured or manually save all files 8. **Output simple list** - Unformatted markdown, Paul Graham style, 8-12 words each 9. **Prioritize surprise** - Novel ideas over obvious takeaways diff --git a/LifeOS/install/skills/Research/Workflows/Fabric.md b/LifeOS/install/skills/Research/Workflows/Fabric.md index c3714d37bb..f9d2aff2d0 100755 --- a/LifeOS/install/skills/Research/Workflows/Fabric.md +++ b/LifeOS/install/skills/Research/Workflows/Fabric.md @@ -6,8 +6,8 @@ Intelligent pattern selection for Fabric CLI. Automatically selects the right pa **The Fabric skill has moved to a dedicated skill directory.** -**Primary Skill:** `~/.claude/skills/Fabric/SKILL.md` -**Patterns Location:** `~/.claude/skills/Fabric/Patterns/` +**Primary Skill:** `{{LIFEOS_ROOT}}/skills/Fabric/SKILL.md` +**Patterns Location:** `{{LIFEOS_ROOT}}/skills/Fabric/Patterns/` For pattern updates, use: "update fabric patterns" → invokes Fabric skill's UpdatePatterns workflow. @@ -257,7 +257,7 @@ fabric "your text here" -p [pattern] ## Updating Patterns -Patterns are managed by the Fabric skill at `~/.claude/skills/Fabric/`. +Patterns are managed by the Fabric skill at `{{LIFEOS_ROOT}}/skills/Fabric/`. **To update patterns:** @@ -266,7 +266,7 @@ Say: "update fabric patterns" → invokes Fabric skill's UpdatePatterns workflow **To see all available patterns:** ```bash -ls ~/.claude/skills/Fabric/Patterns/ +ls "${LIFEOS_ROOT}/skills/Fabric/Patterns/" ``` ## 💡 Usage Examples @@ -341,10 +341,10 @@ cat wisdom.txt | fabric -p create_5_sentence_summary ## Supplementary Resources -**Full Pattern List:** `ls ~/.claude/skills/Fabric/Patterns/` -**Fabric Skill:** `~/.claude/skills/Fabric/SKILL.md` +**Full Pattern List:** `ls "${LIFEOS_ROOT}/skills/Fabric/Patterns/"` +**Fabric Skill:** `{{LIFEOS_ROOT}}/skills/Fabric/SKILL.md` **Fabric Documentation:** https://github.com/danielmiessler/fabric -**Pattern Templates:** See `~/.claude/skills/Fabric/Patterns/official_pattern_template/` +**Pattern Templates:** See `{{LIFEOS_ROOT}}/skills/Fabric/Patterns/official_pattern_template/` ## 🔑 Key Insight diff --git a/LifeOS/install/skills/Research/Workflows/Retrieve.md b/LifeOS/install/skills/Research/Workflows/Retrieve.md index e9f68571dd..d4ff6d597a 100755 --- a/LifeOS/install/skills/Research/Workflows/Retrieve.md +++ b/LifeOS/install/skills/Research/Workflows/Retrieve.md @@ -403,12 +403,12 @@ mcp__Apify__apify-slash-rag-web-browser({ ## 📁 Working Files → History Pattern -**Working Directory:** `~/.claude/LIFEOS/MEMORY/WORK/{current_work}/` +**Working Directory:** `{{LIFEOS_DIR}}/MEMORY/WORK/{current_work}/` **Getting Current Work Directory:** 1. Read `~/.claude/` 2. Extract the `work_dir` value -3. Use `~/.claude/LIFEOS/MEMORY/WORK/{work_dir}/` for temporary artifacts +3. Use `{{LIFEOS_DIR}}/MEMORY/WORK/{work_dir}/` for temporary artifacts **Process:** @@ -422,7 +422,7 @@ mcp__Apify__apify-slash-rag-web-browser({ 2. **History (Permanent Archive):** - - Move to `~/.claude/History/research/YYYY-MM-DD_[description]/` when complete + - Move to `{{LIFEOS_ROOT}}/History/research/YYYY-MM-DD_[description]/` when complete - Include: `README.md`, final extracted content, metadata - Archive for future reference and reuse @@ -436,7 +436,7 @@ mcp__Apify__apify-slash-rag-web-browser({ **Working files (in current work item directory):** ``` -~/.claude/LIFEOS/MEMORY/WORK/20260111-172408_retrieve-react19-docs/ +{{LIFEOS_DIR}}/MEMORY/WORK/20260111-172408_retrieve-react19-docs/ ├── raw-content/ │ ├── page1.md (Layer 2 output) │ ├── page2.md (Layer 2 output) @@ -450,7 +450,7 @@ mcp__Apify__apify-slash-rag-web-browser({ **History (permanent archive):** ``` -~/.claude/History/research/2025-10-26_react19-documentation/ +{{LIFEOS_ROOT}}/History/research/2025-10-26_react19-documentation/ ├── README.md (retrieval documentation) ├── content.md (final extracted content) ├── metadata.json (sources, layers used, timestamps) diff --git a/LifeOS/install/skills/Research/Workflows/WebScraping.md b/LifeOS/install/skills/Research/Workflows/WebScraping.md index b6fc0570c3..c253fddb3d 100755 --- a/LifeOS/install/skills/Research/Workflows/WebScraping.md +++ b/LifeOS/install/skills/Research/Workflows/WebScraping.md @@ -55,5 +55,5 @@ Web scraping and crawling using WebFetch for simple pages, BrightData MCP for CA - Don't overwhelm servers ## Supplementary Resources -For advanced scraping: `read ~/.claude/docs/web-scraping-advanced.md` -For MCP tools: `read ~/.claude/docs/mcp-servers-reference.md` +For advanced scraping: `read {{LIFEOS_ROOT}}/docs/web-scraping-advanced.md` +For MCP tools: `read {{LIFEOS_ROOT}}/docs/mcp-servers-reference.md` diff --git a/LifeOS/install/skills/Research/Workflows/YoutubeExtraction.md b/LifeOS/install/skills/Research/Workflows/YoutubeExtraction.md index 086f13f6d5..402adc4239 100755 --- a/LifeOS/install/skills/Research/Workflows/YoutubeExtraction.md +++ b/LifeOS/install/skills/Research/Workflows/YoutubeExtraction.md @@ -61,4 +61,4 @@ fabric -y "https://www.youtube.com/watch?v=VIDEO_ID" -p summarize 5. If pattern specified, processes through pattern ## Supplementary Resources -For Fabric patterns: `read ~/.claude/docs/fabric-patterns.md` +For Fabric patterns: `read {{LIFEOS_ROOT}}/docs/fabric-patterns.md` diff --git a/LifeOS/install/skills/RootCauseAnalysis/SKILL.md b/LifeOS/install/skills/RootCauseAnalysis/SKILL.md index c8b0d82f62..37bda31839 100644 --- a/LifeOS/install/skills/RootCauseAnalysis/SKILL.md +++ b/LifeOS/install/skills/RootCauseAnalysis/SKILL.md @@ -9,7 +9,7 @@ context: fork ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/RootCauseAnalysis/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/RootCauseAnalysis/` If this directory exists, load and apply any `PREFERENCES.md`, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -173,5 +173,5 @@ User: "this flaky test only fails in CI, not locally" After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"RootCauseAnalysis","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"RootCauseAnalysis","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` diff --git a/LifeOS/install/skills/Sales/SKILL.md b/LifeOS/install/skills/Sales/SKILL.md index b2623f24c0..f549214a39 100644 --- a/LifeOS/install/skills/Sales/SKILL.md +++ b/LifeOS/install/skills/Sales/SKILL.md @@ -8,7 +8,7 @@ effort: medium ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Sales/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/Sales/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -155,7 +155,7 @@ User: "create a visual for this sales story" After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Sales","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Sales","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/Sales/Workflows/CreateSalesPackage.md b/LifeOS/install/skills/Sales/Workflows/CreateSalesPackage.md index 875380b51d..3d8f4eb73e 100755 --- a/LifeOS/install/skills/Sales/Workflows/CreateSalesPackage.md +++ b/LifeOS/install/skills/Sales/Workflows/CreateSalesPackage.md @@ -157,7 +157,7 @@ NO other text. ### Generate with CLI ```bash -bun run ~/.claude/skills/art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/art/Tools/Generate.ts" \ --model nano-banana-pro \ --prompt "[YOUR PROMPT]" \ --size 2K \ diff --git a/LifeOS/install/skills/Sales/Workflows/CreateVisual.md b/LifeOS/install/skills/Sales/Workflows/CreateVisual.md index 29f4b03c58..1dc29b69a4 100755 --- a/LifeOS/install/skills/Sales/Workflows/CreateVisual.md +++ b/LifeOS/install/skills/Sales/Workflows/CreateVisual.md @@ -106,7 +106,7 @@ NO other text. ### Step 5: Generate Image ```bash -bun run ~/.claude/skills/art/Tools/Generate.ts \ +bun run "${LIFEOS_ROOT}/skills/art/Tools/Generate.ts" \ --model nano-banana-pro \ --prompt "[YOUR PROMPT]" \ --size 2K \ diff --git a/LifeOS/install/skills/Science/SKILL.md b/LifeOS/install/skills/Science/SKILL.md index 1c98bb2e10..69eb5b714e 100644 --- a/LifeOS/install/skills/Science/SKILL.md +++ b/LifeOS/install/skills/Science/SKILL.md @@ -8,7 +8,7 @@ effort: high ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Science/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/Science/` If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -185,7 +185,7 @@ User: "experiment with different prompt structures for better output" After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Science","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Science","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` Replace `WORKFLOW_USED` with the workflow executed, `8_WORD_SUMMARY` with a brief input description, and `SECONDS` with approximate wall-clock time. Log `status: "error"` if the workflow failed. diff --git a/LifeOS/install/skills/Science/Workflows/DesignExperiment.md b/LifeOS/install/skills/Science/Workflows/DesignExperiment.md index 74079267b0..f76887a776 100755 --- a/LifeOS/install/skills/Science/Workflows/DesignExperiment.md +++ b/LifeOS/install/skills/Science/Workflows/DesignExperiment.md @@ -214,7 +214,7 @@ Evals implements the Science Protocol for prompt engineering. Don't reinvent eva **CLI Quick Reference:** ```bash # Run prompt comparison via Evals skill -bun run ~/.claude/skills/Evals/EvalServer/cli-run.ts \ +bun run "${LIFEOS_ROOT}/skills/Evals/EvalServer/cli-run.ts" \ --use-case \ --compare prompts/baseline.md prompts/variant-1.md \ --position-swap diff --git a/LifeOS/install/skills/SystemsThinking/SKILL.md b/LifeOS/install/skills/SystemsThinking/SKILL.md index 32d1e352c6..b09d8202ec 100644 --- a/LifeOS/install/skills/SystemsThinking/SKILL.md +++ b/LifeOS/install/skills/SystemsThinking/SKILL.md @@ -9,7 +9,7 @@ context: fork ## Customization **Before executing, check for user customizations at:** -`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/SystemsThinking/` +`{{LIFEOS_DIR}}/USER/CUSTOMIZATIONS/SKILLS/SystemsThinking/` If this directory exists, load and apply any `PREFERENCES.md`, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. @@ -170,5 +170,5 @@ User: "we're about to add a rate limit to stop abuse" After completing any workflow, append a single JSONL entry: ```bash -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"SystemsThinking","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/LIFEOS/MEMORY/SKILLS/execution.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"SystemsThinking","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> "${LIFEOS_DIR}/MEMORY/SKILLS/execution.jsonl" ``` diff --git a/LifeOS/install/skills/Telos/DashboardTemplate/App/add-file/page.tsx b/LifeOS/install/skills/Telos/DashboardTemplate/App/add-file/page.tsx deleted file mode 100755 index 015207f7f1..0000000000 --- a/LifeOS/install/skills/Telos/DashboardTemplate/App/add-file/page.tsx +++ /dev/null @@ -1,269 +0,0 @@ -"use client" - -import { useState } from "react" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { Upload, FileText, Table, CheckCircle, XCircle, Loader2 } from "lucide-react" - -interface UploadedFile { - name: string - type: string - status: "success" | "error" | "uploading" - message?: string -} - -export default function AddFilePage() { - const [uploadedFiles, setUploadedFiles] = useState([]) - const [isDragging, setIsDragging] = useState(false) - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault() - setIsDragging(true) - } - - const handleDragLeave = () => { - setIsDragging(false) - } - - const handleDrop = async (e: React.DragEvent) => { - e.preventDefault() - setIsDragging(false) - - const files = Array.from(e.dataTransfer.files) - await processFiles(files) - } - - const handleFileInput = async (e: React.ChangeEvent) => { - if (!e.target.files) return - const files = Array.from(e.target.files) - await processFiles(files) - } - - const processFiles = async (files: File[]) => { - for (const file of files) { - // Validate file type - if (!file.name.endsWith('.md') && !file.name.endsWith('.csv')) { - setUploadedFiles(prev => [...prev, { - name: file.name, - type: file.name.split('.').pop() || '', - status: "error", - message: "Only .md and .csv files are allowed" - }]) - continue - } - - // Add uploading status - setUploadedFiles(prev => [...prev, { - name: file.name, - type: file.name.split('.').pop() || '', - status: "uploading" - }]) - - // Upload file - try { - const formData = new FormData() - formData.append('file', file) - - const response = await fetch('/api/upload', { - method: 'POST', - body: formData, - }) - - if (!response.ok) { - throw new Error('Upload failed') - } - - const data = await response.json() - - // Update status to success - setUploadedFiles(prev => prev.map(f => - f.name === file.name && f.status === "uploading" - ? { ...f, status: "success" as const, message: data.message } - : f - )) - - // Trigger a custom event to notify sidebar to refresh - window.dispatchEvent(new CustomEvent('telosFileUploaded', { detail: { filename: file.name } })) - } catch (error) { - // Update status to error - setUploadedFiles(prev => prev.map(f => - f.name === file.name && f.status === "uploading" - ? { ...f, status: "error" as const, message: "Upload failed" } - : f - )) - } - } - } - - return ( -
-
-

- - Add File -

-

- Upload .md or .csv files to add them to your TELOS dashboard -

-
- -
- {/* Upload Area */} - - - Upload Files - - -
- -

- Drag and drop files here -

-

- or click to browse -

- - -

- Supported formats: .md (Markdown), .csv (Comma-separated values) -

-
-
-
- - {/* Upload History */} - {uploadedFiles.length > 0 && ( - - - Upload History - - -
- {uploadedFiles.map((file, index) => ( -
-
- {file.type === 'md' ? ( - - ) : ( - - )} -
-

{file.name}

- {file.message && ( -

{file.message}

- )} -
- -
- - {file.type.toUpperCase()} - - {file.status === "success" && ( - - )} - {file.status === "error" && ( - - )} - {file.status === "uploading" && ( - - )} -
- - ))} - - - - )} - - {/* Instructions */} - - - How It Works - - -
-
-
- 1 -
-
-

Upload Your Files

-

Drag and drop or click to select .md or .csv files from your computer

-
-
-
-
- 2 -
-
-

Automatic Analysis

-

Files are automatically analyzed and their content is incorporated into the TELOS system

-
-
-
-
- 3 -
-
-

Dashboard Updates

-

New data becomes immediately available in the dashboard and AI chat

-
-
-
-
- 4 -
-
-

Persistent Storage

-

Files are saved to your TELOS directory (~/.claude/skills/Telos/)

-
-
-
-
-
- - - ) -} diff --git a/LifeOS/install/skills/Telos/DashboardTemplate/App/api/chat/route.ts b/LifeOS/install/skills/Telos/DashboardTemplate/App/api/chat/route.ts deleted file mode 100755 index 76709f8a42..0000000000 --- a/LifeOS/install/skills/Telos/DashboardTemplate/App/api/chat/route.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { NextResponse } from "next/server" -import { getTelosContext } from "@/lib/telos-data" -import { spawn } from "child_process" - -export async function POST(request: Request) { - try { - const { message } = await request.json() - - if (!message) { - return NextResponse.json( - { error: "Message is required" }, - { status: 400 } - ) - } - - // Load all TELOS context - const telosContext = getTelosContext() - - const systemPrompt = `You are a helpful AI assistant with access to the user's complete Personal TELOS (Life Operating System). - -${telosContext} - -When answering questions: -- Reference specific information from the TELOS files above -- Be conversational and helpful -- If asked about goals, projects, beliefs, wisdom, etc., use the exact information from the relevant sections -- If information isn't in the TELOS data, say so clearly -- Keep responses concise but informative` - - // Use Inference tool instead of direct API - const inferenceResult = await new Promise<{ success: boolean; output?: string; error?: string }>((resolve) => { - const homeDir = process.env.HOME || '' - // medium (Sonnet): cross-section synthesis over the user's full TELOS; low/haiku goes shallow (task-intelligence review P3) - const proc = spawn('bun', ['run', `${homeDir}/.claude/LIFEOS/TOOLS/Inference.ts`, '--level', 'medium', systemPrompt, message], { - stdio: ['ignore', 'pipe', 'pipe'], - }) - - let stdout = '' - let stderr = '' - - proc.stdout.on('data', (data) => { stdout += data.toString() }) - proc.stderr.on('data', (data) => { stderr += data.toString() }) - - proc.on('close', (code) => { - if (code !== 0) { - resolve({ success: false, error: stderr || `Process exited with code ${code}` }) - } else { - resolve({ success: true, output: stdout.trim() }) - } - }) - - proc.on('error', (err) => { - resolve({ success: false, error: err.message }) - }) - }) - - if (!inferenceResult.success) { - console.error("Inference Error:", inferenceResult.error) - throw new Error(`Inference failed: ${inferenceResult.error}`) - } - - const assistantMessage = inferenceResult.output - - if (!assistantMessage) { - throw new Error("No response from inference") - } - - return NextResponse.json({ response: assistantMessage }) - } catch (error) { - console.error("Error in chat API:", error) - return NextResponse.json( - { error: "Failed to process request" }, - { status: 500 } - ) - } -} diff --git a/LifeOS/install/skills/Telos/DashboardTemplate/App/api/file/get/route.ts b/LifeOS/install/skills/Telos/DashboardTemplate/App/api/file/get/route.ts deleted file mode 100755 index 9ab4362cce..0000000000 --- a/LifeOS/install/skills/Telos/DashboardTemplate/App/api/file/get/route.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { NextResponse } from "next/server" -import { getAllTelosData } from "@/lib/telos-data" - -export const dynamic = 'force-dynamic' - -export async function GET(request: Request) { - try { - const { searchParams } = new URL(request.url) - const filename = searchParams.get('filename') - - if (!filename) { - return NextResponse.json( - { error: "Filename parameter is required" }, - { status: 400 } - ) - } - - // Get all TELOS files - const files = getAllTelosData() - - // Find the matching file - const file = files.find(f => f.filename === filename) - - if (!file) { - return NextResponse.json( - { error: `File ${filename} not found` }, - { status: 404 } - ) - } - - return NextResponse.json({ - content: file.content, - type: file.type, - name: file.name, - filename: file.filename - }) - } catch (error) { - console.error("Error fetching file:", error) - return NextResponse.json( - { error: "Failed to fetch file" }, - { status: 500 } - ) - } -} diff --git a/LifeOS/install/skills/Telos/DashboardTemplate/App/api/file/save/route.ts b/LifeOS/install/skills/Telos/DashboardTemplate/App/api/file/save/route.ts deleted file mode 100755 index 5e4bb48cad..0000000000 --- a/LifeOS/install/skills/Telos/DashboardTemplate/App/api/file/save/route.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { NextResponse } from "next/server" -import fs from 'fs' -import path from 'path' -import os from 'os' - -const TELOS_DIR = path.join(os.homedir(), '.claude/skills/Telos') - -export async function POST(request: Request) { - try { - const { filename, content } = await request.json() - - if (!filename || content === undefined) { - return NextResponse.json( - { error: "Filename and content are required" }, - { status: 400 } - ) - } - - // Determine file path - const isCSV = filename.endsWith('.csv') - let filePath: string - - if (isCSV) { - const csvDir = path.join(TELOS_DIR, 'data') - filePath = path.join(csvDir, filename) - } else { - filePath = path.join(TELOS_DIR, filename) - } - - // Verify file exists before overwriting - if (!fs.existsSync(filePath)) { - return NextResponse.json( - { error: `File ${filename} does not exist` }, - { status: 404 } - ) - } - - // Save file - fs.writeFileSync(filePath, content, 'utf-8') - - // Log the edit - const timestamp = new Date().toISOString() - const logMessage = `\n## ${timestamp}\n\n- **Action:** File edited via dashboard\n- **File:** ${filename}\n` - - const updatesPath = path.join(TELOS_DIR, 'updates.md') - if (fs.existsSync(updatesPath)) { - fs.appendFileSync(updatesPath, logMessage) - } - - return NextResponse.json({ - success: true, - message: `${filename} saved successfully`, - }) - } catch (error) { - console.error("Error saving file:", error) - return NextResponse.json( - { error: "Failed to save file" }, - { status: 500 } - ) - } -} diff --git a/LifeOS/install/skills/Telos/DashboardTemplate/App/api/files/count/route.ts b/LifeOS/install/skills/Telos/DashboardTemplate/App/api/files/count/route.ts deleted file mode 100755 index 7435b4ee58..0000000000 --- a/LifeOS/install/skills/Telos/DashboardTemplate/App/api/files/count/route.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NextResponse } from "next/server" -import { getTelosFileCount, getTelosFileList } from "@/lib/telos-data" - -export const dynamic = 'force-dynamic' - -export async function GET() { - try { - const count = getTelosFileCount() - const files = getTelosFileList() - - return NextResponse.json({ - count, - files, - }) - } catch (error) { - console.error("Error getting file count:", error) - return NextResponse.json( - { error: "Failed to get file count" }, - { status: 500 } - ) - } -} diff --git a/LifeOS/install/skills/Telos/DashboardTemplate/App/api/upload/route.ts b/LifeOS/install/skills/Telos/DashboardTemplate/App/api/upload/route.ts deleted file mode 100755 index ea23ee2fe1..0000000000 --- a/LifeOS/install/skills/Telos/DashboardTemplate/App/api/upload/route.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { NextResponse } from "next/server" -import fs from 'fs' -import path from 'path' -import os from 'os' - -const TELOS_DIR = path.join(os.homedir(), '.claude/skills/Telos') - -export async function POST(request: Request) { - try { - const formData = await request.formData() - const file = formData.get('file') as File - - if (!file) { - return NextResponse.json( - { error: "No file provided" }, - { status: 400 } - ) - } - - // Validate file type - const fileName = file.name - const isMarkdown = fileName.endsWith('.md') - const isCSV = fileName.endsWith('.csv') - - if (!isMarkdown && !isCSV) { - return NextResponse.json( - { error: "Only .md and .csv files are allowed" }, - { status: 400 } - ) - } - - // Read file content - const arrayBuffer = await file.arrayBuffer() - const buffer = Buffer.from(arrayBuffer) - - // Ensure TELOS directory exists - if (!fs.existsSync(TELOS_DIR)) { - fs.mkdirSync(TELOS_DIR, { recursive: true }) - } - - // Determine save path - let savePath: string - if (isCSV) { - // CSV files go in data subdirectory - const csvDir = path.join(TELOS_DIR, 'data') - if (!fs.existsSync(csvDir)) { - fs.mkdirSync(csvDir, { recursive: true }) - } - savePath = path.join(csvDir, fileName) - } else { - // MD files go in root TELOS directory - savePath = path.join(TELOS_DIR, fileName) - } - - // Check if file already exists - if (fs.existsSync(savePath)) { - return NextResponse.json( - { error: `File ${fileName} already exists. Please delete the existing file first or rename your file.` }, - { status: 409 } - ) - } - - // Save file - fs.writeFileSync(savePath, buffer) - - // Log the upload - const timestamp = new Date().toISOString() - const logMessage = `\n## ${timestamp}\n\n- **Action:** File uploaded via dashboard\n- **File:** ${fileName}\n- **Type:** ${isCSV ? 'CSV' : 'Markdown'}\n- **Path:** ${savePath}\n` - - const updatesPath = path.join(TELOS_DIR, 'updates.md') - if (fs.existsSync(updatesPath)) { - fs.appendFileSync(updatesPath, logMessage) - } - - return NextResponse.json({ - success: true, - message: `${fileName} uploaded successfully to ${isCSV ? 'data/' : ''}`, - path: savePath, - }) - } catch (error) { - console.error("Error in upload API:", error) - return NextResponse.json( - { error: "Failed to upload file" }, - { status: 500 } - ) - } -} diff --git a/LifeOS/install/skills/Telos/DashboardTemplate/App/ask/page.tsx b/LifeOS/install/skills/Telos/DashboardTemplate/App/ask/page.tsx deleted file mode 100755 index 0f1d1724a4..0000000000 --- a/LifeOS/install/skills/Telos/DashboardTemplate/App/ask/page.tsx +++ /dev/null @@ -1,187 +0,0 @@ -"use client" - -import { useState } from "react" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { MessageSquare, Send, Bot, User } from "lucide-react" - -interface Message { - role: "user" | "assistant" - content: string - timestamp: Date -} - -export default function AskPage() { - const [messages, setMessages] = useState([]) - const [input, setInput] = useState("") - const [isLoading, setIsLoading] = useState(false) - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - if (!input.trim() || isLoading) return - - const userMessage: Message = { - role: "user", - content: input.trim(), - timestamp: new Date(), - } - - setMessages((prev) => [...prev, userMessage]) - setInput("") - setIsLoading(true) - - try { - const response = await fetch("/api/chat", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - message: userMessage.content, - }), - }) - - if (!response.ok) { - throw new Error("Failed to get response") - } - - const data = await response.json() - - const assistantMessage: Message = { - role: "assistant", - content: data.response, - timestamp: new Date(), - } - - setMessages((prev) => [...prev, assistantMessage]) - } catch (error) { - console.error("Error:", error) - const errorMessage: Message = { - role: "assistant", - content: "Sorry, I encountered an error. Please try again.", - timestamp: new Date(), - } - setMessages((prev) => [...prev, errorMessage]) - } finally { - setIsLoading(false) - } - } - - return ( -
-
-

- - Ask AI -

-

- Chat with Claude Haiku 4.5 to get instant answers -

-
- - - - - Chat Interface - - Claude Haiku 4.5 - - - - - {/* Messages Container */} -
- {messages.length === 0 ? ( -
-
- -

Start a conversation by typing a message below

-
-
- ) : ( - messages.map((message, index) => ( -
- {message.role === "assistant" && ( -
-
- -
-
- )} -
-
- {message.content} -
-
- {message.timestamp.toLocaleTimeString()} -
-
- {message.role === "user" && ( -
-
- -
-
- )} -
- )) - )} - {isLoading && ( -
-
-
- -
-
-
-
-
-
-
-
-
-
- )} -
- - {/* Input Form */} -
- setInput(e.target.value)} - placeholder="Type your message..." - disabled={isLoading} - className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#2e7de9] focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed" - /> - - - - -
- ) -} diff --git a/LifeOS/install/skills/Telos/DashboardTemplate/App/file/[slug]/page.tsx b/LifeOS/install/skills/Telos/DashboardTemplate/App/file/[slug]/page.tsx deleted file mode 100755 index 6a83aa88fc..0000000000 --- a/LifeOS/install/skills/Telos/DashboardTemplate/App/file/[slug]/page.tsx +++ /dev/null @@ -1,215 +0,0 @@ -"use client" - -import { useState, useEffect } from "react" -import { useParams, notFound } from "next/navigation" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Button } from "@/components/ui/button" -import { FileText, Table as TableIcon, Edit2, Save, X } from "lucide-react" -import ReactMarkdown from "react-markdown" - -export default function FilePage() { - const params = useParams() - const slug = params.slug as string - - const [file, setFile] = useState(null) - const [isEditing, setIsEditing] = useState(false) - const [editedContent, setEditedContent] = useState("") - const [isSaving, setIsSaving] = useState(false) - const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle") - - useEffect(() => { - // Fetch file data - fetch('/api/files/count') - .then(res => res.json()) - .then(data => { - const matchingFile = data.files.find((f: string) => { - const fileSlug = f.replace('.md', '').replace('.csv', '').replace('data/', '') - return fileSlug === slug - }) - - if (matchingFile) { - // Fetch the actual file content - fetch(`/api/file/get?filename=${encodeURIComponent(matchingFile)}`) - .then(res => res.json()) - .then(fileData => { - setFile({ - name: slug, - filename: matchingFile, - content: fileData.content, - type: matchingFile.endsWith('.csv') ? 'csv' : 'markdown' - }) - setEditedContent(fileData.content) - }) - } - }) - }, [slug]) - - if (!file) { - return
Loading...
- } - - const isCSV = file.type === 'csv' - - const handleSave = async () => { - setIsSaving(true) - setSaveStatus("idle") - - try { - const response = await fetch('/api/file/save', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - filename: file.filename, - content: editedContent, - }), - }) - - if (!response.ok) { - throw new Error('Save failed') - } - - file.content = editedContent - setIsEditing(false) - setSaveStatus("success") - setTimeout(() => setSaveStatus("idle"), 3000) - } catch (error) { - console.error('Error saving file:', error) - setSaveStatus("error") - } finally { - setIsSaving(false) - } - } - - return ( -
-
-
-

- {isCSV ? ( - - ) : ( - - )} - {file.name} -

-

- {file.filename} -

-
-
- {!isEditing ? ( - - ) : ( - <> - - - - )} -
-
- - {saveStatus === "success" && ( -
- File saved successfully! -
- )} - - {saveStatus === "error" && ( -
- Error saving file. Please try again. -
- )} - - - - Content - - - {isEditing ? ( -