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
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# LifeOS — AGENTS.md (portable agent context)
LifeOS constitution: LifeOS/install/LIFEOS/HERMES_CONSTITUTION.md
Hermes context: LifeOS/install/HERMES.md
See LifeOS/INSTALL.md for full installation instructions.
23 changes: 15 additions & 8 deletions LifeOS/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,16 @@ Creates the personal config tree from templates and links it in. This is empty s

This is the one place harnesses genuinely differ. Show the exact change and get a yes.

- **Claude Code** — run `bun Tools/InstallHooks.ts` (merges the hook set into `settings.json`, backing it up first) and `bun Tools/ActivateImports.ts` (turns on the identity context imports). This is what lights up the always-on behavior: the LifeOS response format, the memory loop, and per-turn context injection.
- **Claude Code** — run `bun Tools/InstallHooks.ts` (merges the hook set into `settings.json`, backing it up first) and `bun Tools/ActivateImports.ts` (turns on the identity context imports). This remains the Claude-specific integration path.

- **Any other harness (Cursor / Cline / Codex / Gemini / other)** — LifeOS's always-on behavior is enforced by Claude Code *hooks*, which are a Claude Code mechanism. They don't auto-wire on other harnesses **yet**. So instead:
1. Write an `AGENTS.md` (or the harness's own context file — e.g. `.cursor/rules`) that points the harness at the LifeOS tree, so it loads the LifeOS context every session.
2. Tell your human, plainly and honestly: *"On <harness>, the always-on hooks aren't wired yet. You get the skill, your USER data, Pulse, and context loading every session, and you run Setup and Interview on request. Full always-on behavior is on the roadmap for this harness."*
3. **Do not** write Claude hook files or a Claude `settings.json` `hooks` block into a non-Claude harness — it would sit there inert and do nothing.
- **Hermes** — the HermesSetup workflow handles everything:
1. Load the constitution as ephemeral_system_prompt
2. Install portable skills into $HERMES_HOME/skills/
3. Wire TELOS from the principal-supplied path
4. Verify with hermes skills list + Hindsight health check
See LifeOS/Workflows/HermesSetup.md for the full procedure.

- **Any other harness (Cursor / Cline / Codex / Gemini / other)** — LifeOS's always-on behavior is harness-specific. Write an `AGENTS.md` (or the harness's own context file) that points the harness at the LifeOS tree, and state clearly which lifecycle integration is and is not available. Never write inert Claude hook configuration into another harness.

### 7. Wire the launch command — HOW LifeOS actually turns on (WITH PERMISSION)

Expand All @@ -101,7 +105,9 @@ The payload ships the launcher — `install/LIFEOS/TOOLS/lifeos.ts` — which sp
```
fish: `alias lifeos "bun <configRoot>/LIFEOS/TOOLS/lifeos.ts -s <configRoot>/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`).

- **Any other harness** — use that harness's own system-prompt flag against the same file. e.g. pi: `pi --append-system-prompt <configRoot>/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.
- **Hermes** — pass `install/LIFEOS/HERMES_CONSTITUTION.md` as Hermes' `ephemeral_system_prompt` during agent initialization. It is runtime-only and must not be saved into trajectories or memory.

- **Any other harness** — use that harness's own system-prompt flag against the adapted Hermes constitution when supported. If a harness has no system-prompt facility, load it through the harness context file as a documented degraded equivalent.

If your human declines the shell edit, give them the one-line launch command to run by hand so the constitution still loads:
```
Expand Down Expand Up @@ -155,8 +161,9 @@ Run the **Setup** workflow (`Workflows/Setup.md`) to finish integration and veri
| Harness / OS | Skill + USER data + Pulse | Always-on behavior (response format, memory loop, context injection) |
|---|---|---|
| **Claude Code — macOS / Linux** | ✅ | ✅ full (native hooks) |
| **Claude Code — Windows** | ✅ (copy fallback where symlinks need admin) | ✅ full |
| **Cursor / Cline / Codex / Gemini / other** | ✅ | ⚠️ context loads every session via `AGENTS.md`; workflows run on request; always-on hooks not wired yet (roadmap) |
| **Claude Code — Windows** | ✅ (copy fallback where symlinks need admin) | ✅ full Claude integration |
| **Hermes** | ✅ | ✅ constitution via ephemeral system prompt; Hindsight/LCM/cognitive graph/skills via Hermes-native layers; Claude hooks not used |
| **Cursor / Cline / Codex / Gemini / other** | ✅ | ⚠️ context/system-prompt integration is harness-specific; no inert Claude hooks |
| **Chat-only assistants (no files / no commands)** | ❌ | ❌ — install stops at the capability gate |

Full-doctrine features additionally depend on the external tools in step 8.5 (codex, browser, Cloudflare, ElevenLabs). Without one, the dependent feature runs degraded **and says so** — it never silently pretends. The Doctor table is the live source of truth for what's on.
Expand Down
3 changes: 2 additions & 1 deletion LifeOS/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The install + onboarding surface for **LifeOS** — the Life Operating System (f

LifeOS is distributed as **one self-contained skill** — the `LifeOS/` directory is the *entire* distribution. Everything ships inside it: the orchestrator (`SKILL.md`, `Workflows/`, `Tools/`), the whole-system payload under `install/`, and the one-line bootstrap at `install/install.sh`. **Nothing ships outside the skill** — no release-root `install.sh`, no `.claude/` clone.

**The primary install is AI-native: give `INSTALL.md` (served at `ourlifeos.ai/install`) to your AI and say "install this."** LifeOS is AI-native, so the install is too — you hand the doc (or its link) to whatever harness you already use, and your AI installs LifeOS on your OS and harness, with permission at each step. It's the same document a human can read and follow. `INSTALL.md` opens with a capability gate, drives the install Tools (which run under `bun` on any OS, not a shell), wires integration per-harness (honest about what each gets), then runs Setup → Interview.
**The primary install is AI-native: give `INSTALL.md` (served at `ourlifeos.ai/install`) to your AI and say "install this." The Hermes-native constitution is `install/LIFEOS/HERMES_CONSTITUTION.md`; Hermes loads it through its ephemeral system-prompt facility rather than Claude `settings.json` hooks.** LifeOS is AI-native, so the install is too — you hand the doc (or its link) to whatever harness you already use, and your AI installs LifeOS on your OS and harness, with permission at each step. It's the same document a human can read and follow. `INSTALL.md` opens with a capability gate, drives the install Tools (which run under `bun` on any OS, not a shell), wires integration per-harness (honest about what each gets), then runs Setup → Interview.

A terminal shortcut stays for Claude Code on macOS/Linux:

Expand All @@ -30,6 +30,7 @@ Both are served from the skill's own single sources of truth — `INSTALL.md` at
| Trigger | Workflow |
|---------|----------|
| `setup`, `/lifeos-setup`, "install LifeOS", "integrate into my harness" | `Workflows/Setup.md` |
| `hermes-setup`, "install LifeOS on Hermes" | `Workflows/HermesSetup.md` |
| `interview`, "onboard me", "run the interview", TELOS capture | `Workflows/Interview.md` |
| `update`, "update LifeOS", after a version bump | `Workflows/Update.md` |
| `uninstall`, "remove LifeOS" | `Workflows/Uninstall.md` |
Expand Down
34 changes: 32 additions & 2 deletions LifeOS/Tools/ActivateImports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
* bun ActivateImports.ts [--config-root <dir>] [--apply] [--allow-dev]
*/

import { join } from "node:path";
import { activateImports, detectDevTree } from "./InstallEngine";
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { activateImports, detectDevTree, detectHarness, isHermes } from "./InstallEngine";

function main(): void {
const a = process.argv.slice(2);
Expand All @@ -29,6 +31,34 @@ function main(): void {
process.exit(2);
}

// Hermes has no CLAUDE.md @-import mechanism. Instead, LifeOS ships an
// install/HERMES.md that Hermes auto-loads. Place a copy under $HERMES_HOME
// (skipping the CLAUDE.md activation entirely) so identity/context load there.
const harness = detectHarness(process.env.HOME || homedir());
if (isHermes(harness)) {
const hermesHome = process.env.HERMES_HOME || join(process.env.HOME || homedir(), ".hermes");
const skillRoot = join(import.meta.dir, "..");
const sourceHermesMd = join(skillRoot, "install", "HERMES.md");
const destHermesMd = join(hermesHome, "LifeOS", "install", "HERMES.md");
const sourceExists = existsSync(sourceHermesMd);
const destExists = existsSync(destHermesMd);
let wrote = false;
if (sourceExists && !destExists && apply) {
mkdirSync(dirname(destHermesMd), { recursive: true });
copyFileSync(sourceHermesMd, destHermesMd);
wrote = true;
}
const note = !sourceExists
? "source HERMES.md missing in skill dir"
: destExists
? "HERMES.md already present at $HERMES_HOME"
: wrote
? "Hermes detected — wrote LifeOS/install/HERMES.md for auto-loading"
: "Hermes detected — would write LifeOS/install/HERMES.md for auto-loading (dry-run)";
console.log(JSON.stringify({ ok: sourceExists, harness: "hermes", written: wrote, dest: destHermesMd, note }, null, 2));
process.exit(sourceExists ? 0 : 1);
}

const claudeMd = join(configRoot, "CLAUDE.md");

if (!apply) {
Expand Down
210 changes: 210 additions & 0 deletions LifeOS/Tools/ImportSkills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
#!/usr/bin/env bun
/**
* ImportSkills — copy the portable LifeOS skills from the install payload into the
* unified Hermes skill body ($HERMES_HOME/skills/) as `source: local` skills.
*
* Three boundaries are enforced, in order:
* 1. Private (`_ALLCAPS`) skills are skipped UNCONDITIONALLY — they carry real
* names, credentials, and identity-bound preferences. Reported by COUNT only,
* never by name (names may leak context).
* 2. Claude/macOS-specific skills (Interceptor, Daemon, Art, Remotion — plus any
* the manifest marks non-portable) are skipped.
* 3. TitleCase dir names are normalized to lowercase-kebab (WorldThreatModel →
* world-threat-model; ISA → isa; USMetrics → us-metrics).
*
* Collisions are resolved by SHA-256 of SKILL.md: identical → skip; different →
* reported as a conflict and NOT overwritten (the operator decides).
*
* Read-only in --dry-run: prints the full plan, writes nothing.
*
* Usage:
* bun ImportSkills.ts [--dry-run]
*/

import { createHash } from "node:crypto";
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { detectHarness, getHarnessSkillsDir } from "./InstallEngine";

// ── Skip lists ───────────────────────────────────────────────────────────────
/** Claude/macOS-only skills that cannot run under Hermes/Windows. */
const PLATFORM_SKIP = new Set(["Interceptor", "Daemon", "Art", "Remotion"]);

/** Subdirs a skill may carry — copied wholesale by cpSync's recursive walk. */
const SRC_SKILLS = join(import.meta.dir, "..", "install", "skills");
const MANIFEST = join(import.meta.dir, "..", "install", "SKILL_MANIFEST.md");

// ── Normalization: TitleCase/PascalCase → lowercase-kebab ─────────────────────
/**
* WorldThreatModel → world-threat-model · ISA → isa · HTML → html ·
* USMetrics → us-metrics · CreateCLI → create-cli · Research → research.
* Acronym runs stay together; a hyphen is inserted only at real word boundaries.
*/
export function normalizeName(name: string): string {
return name
.replace(/([a-z0-9])([A-Z])/g, "$1-$2") // camel boundary: worldT → world-T
.replace(/([A-Z]+)([A-Z][a-z0-9]+)/g, "$1-$2") // acronym→word: USMetrics → US-Metrics
.toLowerCase();
}

// ── Manifest parse (augments PLATFORM_SKIP with any non-portable entries) ──────
function nonPortableFromManifest(): Set<string> {
const skip = new Set<string>();
if (!existsSync(MANIFEST)) return skip;
const text = readFileSync(MANIFEST, "utf-8");
const marker = text.indexOf("not ported");
if (marker === -1) return skip;
// Bold skill names (`**Name**`) appearing after the "not ported" heading.
for (const m of text.slice(marker).matchAll(/\*\*([A-Za-z0-9_]+)\*\*/g)) skip.add(m[1]);
return skip;
}

// ── Hashing ──────────────────────────────────────────────────────────────────
function sha256(path: string): string | null {
try {
return createHash("sha256").update(readFileSync(path)).digest("hex");
} catch {
return null;
}
}

// ── Result types ─────────────────────────────────────────────────────────────
interface Plan {
installed: Array<{ from: string; to: string }>;
skippedPrivate: number;
skippedPlatform: string[];
collisionsIdentical: string[];
collisionsConflict: string[];
invalid: string[];
}

function buildPlan(skillsDir: string): Plan {
const plan: Plan = {
installed: [],
skippedPrivate: 0,
skippedPlatform: [],
collisionsIdentical: [],
collisionsConflict: [],
invalid: [],
};
if (!existsSync(SRC_SKILLS)) {
console.error(`✗ source skills dir not found: ${SRC_SKILLS}`);
process.exit(1);
}
const platformSkip = new Set([...PLATFORM_SKIP, ...nonPortableFromManifest()]);

for (const entry of readdirSync(SRC_SKILLS, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const name = entry.name;

// 1. Private boundary — unconditional, counted only (never named).
if (name.startsWith("_")) {
plan.skippedPrivate++;
continue;
}
// 2. Platform boundary.
if (platformSkip.has(name)) {
plan.skippedPlatform.push(name);
continue;
}
// A skill dir without a SKILL.md is not a skill.
const srcSkillMd = join(SRC_SKILLS, name, "SKILL.md");
if (!existsSync(srcSkillMd)) {
plan.invalid.push(name);
continue;
}
// 3. Normalize + collision-check on SKILL.md hash.
const normalized = normalizeName(name);
const targetDir = join(skillsDir, normalized);
const targetSkillMd = join(targetDir, "SKILL.md");
if (existsSync(targetSkillMd)) {
const same = sha256(srcSkillMd) === sha256(targetSkillMd);
(same ? plan.collisionsIdentical : plan.collisionsConflict).push(normalized);
continue; // never overwrite an existing target
}
plan.installed.push({ from: join(SRC_SKILLS, name), to: targetDir });
}
return plan;
}

// ── Apply ────────────────────────────────────────────────────────────────────
function apply(plan: Plan, skillsDir: string): void {
mkdirSync(skillsDir, { recursive: true });
for (const { from, to } of plan.installed) {
cpSync(from, to, { recursive: true });
}
// Record provenance: imported skills are `source: local` (survive Hermes curator
// lifecycle, clearly marked as imported — distinct from `builtin`/`official`).
const manifestPath = join(skillsDir, ".lifeos-import.json");
let prior: Record<string, unknown> = {};
if (existsSync(manifestPath)) {
try {
prior = JSON.parse(readFileSync(manifestPath, "utf-8"));
} catch {
prior = {};
}
}
const skills = { ...((prior.skills as Record<string, string>) ?? {}) };
for (const { to } of plan.installed) skills[to.split(/[\\/]/).pop()!] = "local";
writeFileSync(
manifestPath,
JSON.stringify({ source: "lifeos", trust: "local", skills }, null, 2),
);
}

// ── Report ───────────────────────────────────────────────────────────────────
function report(plan: Plan, skillsDir: string, dryRun: boolean): void {
const tag = dryRun ? "[dry-run] " : "";
console.log(`\n${tag}LifeOS → Hermes skill import`);
console.log(` target: ${skillsDir}\n`);

console.log(` ${dryRun ? "would install" : "installed"}: ${plan.installed.length}`);
for (const { from, to } of plan.installed) {
console.log(` ${from.split(/[\\/]/).pop()} → ${to.split(/[\\/]/).pop()}`);
}
console.log(` skipped (private): ${plan.skippedPrivate}`);
console.log(` skipped (platform): ${plan.skippedPlatform.length}${plan.skippedPlatform.length ? ` (${plan.skippedPlatform.join(", ")})` : ""}`);
console.log(` collisions (identical, skipped): ${plan.collisionsIdentical.length}${plan.collisionsIdentical.length ? ` (${plan.collisionsIdentical.join(", ")})` : ""}`);
console.log(` collisions (CONFLICT, not written): ${plan.collisionsConflict.length}${plan.collisionsConflict.length ? ` (${plan.collisionsConflict.join(", ")})` : ""}`);
if (plan.invalid.length) console.log(` skipped (no SKILL.md): ${plan.invalid.join(", ")}`);

const total =
plan.installed.length +
plan.skippedPrivate +
plan.skippedPlatform.length +
plan.collisionsIdentical.length +
plan.collisionsConflict.length +
plan.invalid.length;
console.log(` total scanned: ${total}`);

if (plan.collisionsConflict.length) {
console.log(`\n ⚠ ${plan.collisionsConflict.length} conflict(s): a differing SKILL.md already exists.`);
console.log(` Resolve by hand — nothing was overwritten.`);
}

console.log(`\n Next steps:`);
console.log(` hermes skills list # verify the imported LifeOS skills appear`);
console.log(` /reload-skills # load the new skill body into an active session`);
if (dryRun) console.log(`\n Re-run without --dry-run to write.`);
}

// ── Entrypoint ───────────────────────────────────────────────────────────────
function main(): void {
const dryRun = process.argv.includes("--dry-run");
const home = homedir();
// detectHarness resolves where skills load from; force the Hermes convention
// ($HERMES_HOME/skills or ~/.hermes/skills) since this importer targets Hermes.
const harness = detectHarness(home);
const skillsDir =
harness.name === "hermes" && harness.skillsDir
? harness.skillsDir
: getHarnessSkillsDir("hermes", home);

const plan = buildPlan(skillsDir);
if (!dryRun) apply(plan, skillsDir);
report(plan, skillsDir, dryRun);
process.exit(0);
}

main();
Loading