Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions LifeOS/Tools/DeployCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@
* (dry-run by default — reports the plan per target without writing)
*/

import { existsSync, mkdirSync, readdirSync } from "node:fs";
import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { basename, dirname, join } from "node:path";
import { copyMissing, detectDevTree } from "./InstallEngine";
import { atomicWriteText } from "./lib/atomic-write";

// Runtime top-level entries this tool does NOT deploy:
// - USER shipped separately as a scaffold (ScaffoldUser) + symlinked (LinkUser)
Expand Down Expand Up @@ -184,7 +185,7 @@ function scaffoldMemory(configRoot: string, apply: boolean): DeployResult {
if (!apply) {
r.actions.push(`bun ${generator}`);
} else {
const proc = Bun.spawnSync(["bun", generator], { stdout: "pipe", stderr: "pipe" });
const proc = Bun.spawnSync(["bun", generator], { stdout: "pipe", stderr: "pipe", env: { ...process.env, CLAUDE_CONFIG_DIR: configRoot } });
if (proc.exitCode === 0) r.copied++;
else r.failures.push(`GenerateKnowledgeSchemaDoc exited ${proc.exitCode}: ${proc.stderr.toString().trim()}`);
}
Expand Down Expand Up @@ -325,6 +326,53 @@ function deployNestedDependencies(payloadInstall: string, configRoot: string, ap
return r;
}

/**
* (f) system settings layer: install/settings.system.json → configRoot/settings.system.json.
*
* The gap this fills: settings.json is a GENERATED artifact — the SessionStart MergeSettings
* hook rebuilds it every session by merging <configRoot>/settings.system.json (this file) with
* the USER overlay, and SettingsBackport + IntegrityCheck read the same path. But no prior Setup
* step ever PLACED it — InstallSettings writes the payload's CONTENT into settings.json, never
* the source layer itself — so the merge/backport machinery silently had no system source on a
* fresh install. Latent on the default ~/.claude; guaranteed to bite a relocated root, which has
* no global tree to fall back on. This step deploys the source layer the machinery assumes.
*
* Relocation: the payload pins `env.LIFEOS_DIR` to "$HOME/.claude/LIFEOS". On a relocated root
* that GLOBAL value — re-injected via the regenerated settings.json — would override the
* launcher's relocated LIFEOS_DIR and drag LIFEOS-data resolution back to ~/.claude. So we
* repoint it at THIS config root on the way in. Default installs keep the shipped string
* byte-for-byte. (LIFEOS_CONFIG_DIR is the USER-data location, chosen independently, so it is
* left alone.) Additive: never clobbers an existing, possibly user-tuned, system layer.
*/
function deploySystemSettings(payloadInstall: string, configRoot: string, apply: boolean): DeployResult {
const src = join(payloadInstall, "settings.system.json");
const dst = join(configRoot, "settings.system.json");
const r: DeployResult = { what: "system-settings", src, dst, present: existsSync(src), copied: 0, actions: [], blockers: [], failures: [] };
if (!r.present) {
r.blockers.push(`system settings missing: ${src} — point --skill-root at a staged release`);
return r;
}
const relocatedLifeosDir = configRoot === join(process.env.HOME || homedir(), ".claude")
? undefined // default root: ship the payload string as-is
: join(configRoot, "LIFEOS");
if (existsSync(dst)) return r; // additive — never overwrite a populated layer
if (!apply) {
r.actions.push(relocatedLifeosDir ? `write ${dst} (env.LIFEOS_DIR → ${relocatedLifeosDir})` : `copy ${src} → ${dst}`);
return r;
}
try {
const settings = JSON.parse(readFileSync(src, "utf8")) as Record<string, unknown>;
if (relocatedLifeosDir && settings.env && typeof settings.env === "object") {
(settings.env as Record<string, unknown>).LIFEOS_DIR = relocatedLifeosDir;
}
atomicWriteText(dst, JSON.stringify(settings, null, 2) + "\n");
r.copied = 1;
} catch (err) {
r.failures.push(`settings.system.json deploy failed: ${err instanceof Error ? err.message : String(err)}`);
}
return r;
}

function main(): void {
const a = process.argv.slice(2);
const home = process.env.HOME || homedir();
Expand All @@ -349,6 +397,7 @@ function main(): void {
scaffoldMemory(configRoot, apply),
deployDependencies(payloadInstall, configRoot, apply),
deployNestedDependencies(payloadInstall, configRoot, apply),
deploySystemSettings(payloadInstall, configRoot, apply),
];

// A missing required payload source (blocker) or a copy failure is a hard
Expand Down
29 changes: 25 additions & 4 deletions LifeOS/Tools/InstallEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -677,13 +677,34 @@ type MatcherGroup = { matcher?: string; hooks?: HookEntry[]; [k: string]: unknow
type HooksMap = Record<string, MatcherGroup[]>;

/**
* 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.
* Every spelling of the install root, collapsed to one token for dedup. ORDER MATTERS:
* the `${CLAUDE_CONFIG_DIR:-…}` default forms must match WHOLE, ahead of the bare
* ~/.claude / $HOME/.claude they contain, so an upgrade dedupes a stale `$HOME/.claude/x`
* entry against the new templated form of the same hook. LIFEOS_DIR / CLAUDE_PROJECT_DIR /
* CLAUDE_PLUGIN_ROOT stay in the set — dropping any would un-dedupe hooks written that way.
*/
const ROOT_FORMS: RegExp[] = [
/\$\{CLAUDE_CONFIG_DIR:-\$HOME\/\.claude\}/,
/\$\{CLAUDE_CONFIG_DIR:-\$\{HOME\}\/\.claude\}/,
/\$\{CLAUDE_CONFIG_DIR:-~\/\.claude\}/,
/\$\{?CLAUDE_CONFIG_DIR\}?/,
/\$\{?LIFEOS_DIR\}?/,
/\$\{?CLAUDE_PROJECT_DIR\}?/,
/\$\{?CLAUDE_PLUGIN_ROOT\}?/,
/~\/\.claude/,
/\$HOME\/\.claude/,
/\$\{HOME\}\/\.claude/,
];
const ROOT_PATTERN = new RegExp(ROOT_FORMS.map((r) => r.source).join("|"), "g");

/**
* Normalize a hook command for dedup: collapse any install-root spelling (see ROOT_FORMS)
* to `§ROOT§` and squeeze whitespace, so the same hook expressed as `${LIFEOS_DIR}/x`,
* `$HOME/.claude/x`, or `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/x` all dedupe to one.
*/
function normalizeCommand(cmd: string): string {
return cmd
.replace(/\$\{?LIFEOS_DIR\}?|\$\{?CLAUDE_PROJECT_DIR\}?|\$\{?CLAUDE_PLUGIN_ROOT\}?|~\/\.claude|\$HOME\/\.claude|\$\{HOME\}\/\.claude/g, "§ROOT§")
.replace(ROOT_PATTERN, "§ROOT§")
.replace(/\s+/g, " ")
.trim();
}
Expand Down
8 changes: 7 additions & 1 deletion LifeOS/install/LIFEOS/TOOLS/GenerateKnowledgeSchemaDoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ import {
RELATION_VOCAB, SOURCE_KINDS, STATUS_VALUES, SCHEMA_VERSION,
} from "./KnowledgeSchema";

const OUT = pathResolve(homedir(), ".claude/LIFEOS/MEMORY/KNOWLEDGE/_schema.md");
// Honor CLAUDE_CONFIG_DIR so a relocated install writes the schema under its own
// config root, not the global ~/.claude. Inlined rather than importing
// hooks/lib/paths.getClaudeDir(): that helper lives in the HOOKS runtime tree and a
// LIFEOS/TOOLS script does not reach across into it — sibling TOOLS (Doctor.ts) use
// this same one-liner, so it is the local convention.
const CLAUDE_ROOT = process.env.CLAUDE_CONFIG_DIR || pathResolve(homedir(), ".claude");
const OUT = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/KNOWLEDGE/_schema.md");

/** Absolute path of the generated doc — importers compare against it. */
export const SCHEMA_DOC_PATH = OUT;
Expand Down
4 changes: 3 additions & 1 deletion LifeOS/install/LIFEOS/TOOLS/SettingsBackport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ import os from "node:os";
import { mergeSettings, deepEqual, parseJsonFileOrThrow, MERGE_SNAPSHOT_PATH } from "./MergeSettings";
import { atomicWriteText } from "../PULSE/lib/atomic-write";

const CLAUDE_DIR = path.join(os.homedir(), ".claude");
// Honor CLAUDE_CONFIG_DIR (matches the install tools and Doctor.ts) so a
// relocated install backports its OWN settings, not the global ~/.claude.
const CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
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");
Expand Down
29 changes: 23 additions & 6 deletions LifeOS/install/LIFEOS/TOOLS/lifeos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,27 @@
import { spawn, spawnSync } from "bun";
import { existsSync, readFileSync, writeFileSync, readdirSync, symlinkSync, unlinkSync, lstatSync } from "fs";
import { homedir } from "os";
import { join, basename } from "path";
import { join, basename, resolve } from "path";
import { PULSE_BASE } from "../PULSE/endpoint";

// ============================================================================
// Configuration
// ============================================================================

const CLAUDE_DIR = join(homedir(), ".claude");
// The config root this launcher belongs to. It ships at
// <configRoot>/LIFEOS/TOOLS/lifeos.ts, so it self-locates its own root and
// exports it as CLAUDE_CONFIG_DIR for the spawned Claude (see launchEnv below) —
// that is what makes a relocated install actually run inside its own path
// instead of the global ~/.claude. An explicit CLAUDE_CONFIG_DIR still wins.
const CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR || resolve(import.meta.dir, "..", "..");
// LIFEOS data dir under the resolved root. Computed inline, NOT via the hooks
// runtime's paths.getLifeosDir(): that helper READS the env vars this launcher is
// about to SET (LIFEOS_DIR / CLAUDE_CONFIG_DIR), so calling it here would be
// circular — and it lives in the hooks tree, which a TOOL does not import across into.
const LIFEOS_DIR = join(CLAUDE_DIR, "LIFEOS");
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(LIFEOS_DIR, "TOOLS", "Banner.ts");
const VOICE_SERVER = `${PULSE_BASE}/notify/personality`;
const WALLPAPER_DIR = join(homedir(), "Projects", "Wallpaper");
// Note: RAW archiving removed - Claude Code handles its own cleanup (30-day retention in projects/)
Expand Down Expand Up @@ -434,7 +444,7 @@ function setWallpaper(filename: string): boolean {
* public PR #1637, @elhoim
*/
function cmdDoctor(args: string[]) {
const doctor = join(CLAUDE_DIR, "LIFEOS", "TOOLS", "Doctor.ts");
const doctor = join(LIFEOS_DIR, "TOOLS", "Doctor.ts");
const result = spawnSync(["bun", doctor, ...args], {
stdin: "inherit", stdout: "inherit", stderr: "inherit",
});
Expand Down Expand Up @@ -500,7 +510,7 @@ async function cmdLaunch(options: { mcp?: string; resume?: boolean; resumeId?: s

// LifeOS System Prompt — constitutional rules appended to Claude Code's system prompt
// These rules get highest instruction authority (system prompt layer > CLAUDE.md layer)
const systemPromptFile = options.systemPrompt ?? join(CLAUDE_DIR, "LIFEOS", "LIFEOS_SYSTEM_PROMPT.md");
const systemPromptFile = options.systemPrompt ?? join(LIFEOS_DIR, "LIFEOS_SYSTEM_PROMPT.md");
if (existsSync(systemPromptFile)) {
args.push("--append-system-prompt-file", systemPromptFile);
}
Expand Down Expand Up @@ -561,6 +571,10 @@ async function cmdLaunch(options: { mcp?: string; resume?: boolean; resumeId?: s
const launchEnv = { ...process.env };
delete launchEnv.ANTHROPIC_API_KEY;
launchEnv.CLAUDE_CODE_WORKFLOWS = "1";
// Pin Claude Code and every hook to THIS config root, so a relocated install
// resolves settings, skills, hooks, and LIFEOS data inside its own path.
launchEnv.CLAUDE_CONFIG_DIR = CLAUDE_DIR;
launchEnv.LIFEOS_DIR = LIFEOS_DIR;
const proc = spawn(args, {
stdio: ["inherit", "inherit", "inherit"],
env: launchEnv,
Expand Down Expand Up @@ -701,7 +715,7 @@ async function cmdPrompt(prompt: string) {

// Same constitutional layer as interactive launches — without this, one-shots
// ran bare Claude Code (CLAUDE.md only, no output format, no security protocol).
const systemPromptFile = join(CLAUDE_DIR, "LIFEOS", "LIFEOS_SYSTEM_PROMPT.md");
const systemPromptFile = join(LIFEOS_DIR, "LIFEOS_SYSTEM_PROMPT.md");
if (existsSync(systemPromptFile)) {
args.push("--append-system-prompt-file", systemPromptFile);
}
Expand All @@ -711,6 +725,9 @@ async function cmdPrompt(prompt: string) {
const env: Record<string, string> = { ...process.env } as Record<string, string>;
delete env.ANTHROPIC_API_KEY;
env.CLAUDE_CODE_WORKFLOWS = "1";
// Pin Claude Code and every hook to THIS config root (see interactive launch).
env.CLAUDE_CONFIG_DIR = CLAUDE_DIR;
env.LIFEOS_DIR = LIFEOS_DIR;
const proc = spawn(args, {
stdio: ["inherit", "inherit", "inherit"],
env,
Expand Down
5 changes: 2 additions & 3 deletions LifeOS/install/hooks/AgentInvocation.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@

import { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { paiPath } from './lib/paths';
import { paiPath, getClaudeDir } from './lib/paths';
import { getISOTimestamp } from './lib/time';
import { EFFORT_MODEL, CROSS_VENDOR } from '../LIFEOS/TOOLS/models';
import { liveModel } from './ModelRungGuard.hook';
Expand Down Expand Up @@ -89,7 +88,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 */ }
Expand Down
6 changes: 3 additions & 3 deletions LifeOS/install/hooks/AlgorithmNudge.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSy
import { join } from 'path';
import { resolveBun } from './lib/resolve-bin';
import { deriveAscent, type AscentState } from '../LIFEOS/TOOLS/ascent';
import { homedir } from "node:os";
import { getClaudeDir, getLifeosDir } from './lib/paths';

const PAI = join(homedir(), '.claude');
const PAI = getClaudeDir();
// Overridable so tests can PRODUCE a state-machine edge instead of hand-writing
// its precondition, and so test runs stop appending to the production diagnostic
// log (Forge delta audit H-B, M-E — the log was 7 lines, all of them test noise).
Expand Down Expand Up @@ -456,7 +456,7 @@ function loadIndex(): SkillIndex | null {
// self-heal must leave a trace something can see.
try {
appendFileSync(
join(homedir(), '.claude/LIFEOS/MEMORY/OBSERVABILITY/hook-selfheal.jsonl'),
join(getLifeosDir(), 'MEMORY/OBSERVABILITY/hook-selfheal.jsonl'),
JSON.stringify({ ts: new Date().toISOString(), hook: 'AlgorithmNudge', action: 'rebuild-index', bun, error: String(e) }) + '\n',
);
} catch { /* observability write itself is best-effort */ }
Expand Down
3 changes: 2 additions & 1 deletion LifeOS/install/hooks/BashSystemWriteGuard.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@
import { homedir } from "node:os";
import { resolve, join } from "node:path";
import { classifyTarget, loadPatterns, scanForFirstHit } from "./lib/system-file-guard-core";
import { getClaudeDir } from './lib/paths';

const HOME = process.env.HOME ?? homedir();
const CLAUDE_ROOT = join(HOME, ".claude");
const CLAUDE_ROOT = getClaudeDir();

interface HookInput {
tool_input?: { command?: unknown };
Expand Down
9 changes: 5 additions & 4 deletions LifeOS/install/hooks/CheckpointPerISC.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ import { readFileSync, existsSync, writeFileSync, statSync, realpathSync, mkdirS
import { execFileSync } from 'node:child_process';
import { basename, dirname, join } from 'node:path';
import { homedir } from 'node:os';
import { getClaudeDir, getLifeosDir, getSkillsDir } from './lib/paths';
import { parseFrontmatter, parseCriteriaList, ARTIFACT_FILENAME, LEGACY_ARTIFACT_FILENAME } from './lib/isa-utils';
import { isForeignToSystemRepo, looksLikePersonalTranscript } from '../LIFEOS/TOOLS/lib/ForeignDataCheck';

// 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 {
Expand Down Expand Up @@ -154,7 +155,7 @@ function runTouchedPaths(repo: string, startedMs: number): string[] {
});
}

const SYSTEM_REPO = join(homedir(), '.claude');
const SYSTEM_REPO = getClaudeDir();

function isSystemRepo(repo: string): boolean {
try { return realpathSync(repo) === realpathSync(SYSTEM_REPO); }
Expand Down Expand Up @@ -244,9 +245,9 @@ async function main() {
// dir — skill trees must stay publishable-clean and the sidecar carries
// absolute paths + SHAs, which trips SkillHygieneGate (found 2026-08-11,
// interview-evidence upgrade). Everything else keeps the beside-the-ISA path.
const skillsPrefix = join(homedir(), '.claude', 'skills') + '/';
const skillsPrefix = getSkillsDir() + '/';
const inSkillTree = slugDir.startsWith(skillsPrefix);
const skillStateDir = join(homedir(), '.claude', 'LIFEOS', 'MEMORY', 'STATE', 'checkpoints');
const skillStateDir = join(getLifeosDir(), 'MEMORY', 'STATE', 'checkpoints');
if (inSkillTree) mkdirSync(skillStateDir, { recursive: true });
const stateFile = inSkillTree
? join(skillStateDir, `skill-${slug}.checkpoint-state.json`)
Expand Down
4 changes: 2 additions & 2 deletions LifeOS/install/hooks/ConfigEvalFire.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
import { spawn } from 'node:child_process';
import { dirname, resolve } from 'node:path';
import { homedir } from 'node:os';
import { isSubagentContext as isSubagent } from './lib/subagent';
import { getClaudeDir } from './lib/paths';

const CLAUDE_ROOT = resolve(homedir(), '.claude');
const CLAUDE_ROOT = getClaudeDir();
const RUNNER = resolve(CLAUDE_ROOT, 'LIFEOS/TOOLS/ConfigEvalOnChange.ts');
const STATE = resolve(CLAUDE_ROOT, 'LIFEOS/MEMORY/OBSERVABILITY/config-eval-state.json');
const DEBOUNCE_MINUTES = 5;
Expand Down
9 changes: 4 additions & 5 deletions LifeOS/install/hooks/DeployRegistrationGate.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,12 @@

import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { join } from "path";
import { homedir } from "os";
import { readHookInput, parseTranscriptFromInput } from "./lib/hook-io";
import { getLifeosDir } from './lib/paths';

const HOME = homedir();
const PROJECTS_MD = join(HOME, ".claude/LIFEOS/USER/PROJECTS.md");
const INVENTORY_TS = join(HOME, ".claude/LIFEOS/USER/CUSTOMIZATIONS/ARBOL/Shared/infra-inventory.ts");
const STATE_DIR = join(HOME, ".claude/LIFEOS/MEMORY/STATE");
const PROJECTS_MD = join(getLifeosDir(), "USER/PROJECTS.md");
const INVENTORY_TS = join(getLifeosDir(), "USER/CUSTOMIZATIONS/ARBOL/Shared/infra-inventory.ts");
const STATE_DIR = join(getLifeosDir(), "MEMORY/STATE");
const STATE_PATH = join(STATE_DIR, "deploy-registration-gate.json");

// wrangler deploy trigger lines: ` <domain> (custom domain)` — wrangler emits
Expand Down
4 changes: 2 additions & 2 deletions LifeOS/install/hooks/DriftReminder.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,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 { homedir } from "node:os";
import { getLifeosDir } 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"]) {
Expand Down Expand Up @@ -70,7 +70,7 @@ const DEFAULT_LINE_CAP = 15;
// The principal asking for depth lifts the cap. His explicit call outranks the
// default; nothing else does.
const DEPTH_RE = /\b(extensive|thorough|comprehensive|exhaustive|deep[\s-]?dive|in[\s-]depth|detailed|long[\s-]form|full (?:analysis|report|breakdown|write[\s-]?up)|go deep|be verbose|everything (?:you|we) (?:know|have))\b/i;
const LIFEOS_DIR = process.env.LIFEOS_DIR || join(homedir(), ".claude", "LIFEOS");
const LIFEOS_DIR = getLifeosDir();
const LAST_RESPONSE_PATH = join(LIFEOS_DIR, "MEMORY", "STATE", "last-response.txt");
const STATE_PATH = join(LIFEOS_DIR, "MEMORY", "STATE", "drift-reminder.json");
const INITIAL_STATE: DriftState = {
Expand Down
Loading