Skip to content

Commit 3baba67

Browse files
release 7.0.0: The Bitter Pill Release — BPE simplification, ~69% context reduction, modes/tiers retired, Algorithm v8.3.0 + AlgorithmNudge, hook consolidation, 5 community PRs merged
1 parent 0fc5ba2 commit 3baba67

684 files changed

Lines changed: 27572 additions & 31522 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎LifeOS/INSTALL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Do not begin a partial install you can't finish.
4545

4646
### 2. Get the release and detect the environment
4747

48-
Fetch the **latest** LifeOS release from `danielmiessler/LifeOS`. Resolve the newest published release with `curl -fsSL https://api.github.com/repos/danielmiessler/LifeOS/releases/latest` and read its `tag_name`, then download that tag's source tarball (`https://github.com/danielmiessler/LifeOS/archive/refs/tags/<tag>.tar.gz`, HTTPS, no auth) and extract the `LifeOS/` skill directory. (Or use a local release directory if your human already has one.) Then, from the LifeOS skill directory, run:
48+
Fetch the pinned LifeOS release for the repo and version on the install page (the tag tarball over HTTPS, no auth), or use a local release directory if given one. Then, from the LifeOS skill directory, run:
4949

5050
```
5151
bun Tools/DetectEnv.ts

‎LifeOS/SKILL.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
---
22
name: LifeOS
3+
version: 1.4.19
34
description: Install and onboard a user into LifeOS — the Life Operating System (current state → ideal state via TELOS + the Algorithm). The agentic installer detects your OS + harness, wires hooks with permission, scaffolds your USER tree, pulls in sources you provide, and runs the TELOS / current→ideal interview that seeds your Pulse dashboard. USE WHEN install LifeOS, set up LifeOS, lifeos setup, lifeos-setup, lifeos interview, onboard me, run the interview, integrate LifeOS into my harness, update LifeOS, uninstall LifeOS, first-time setup. NOT FOR building or cutting a LifeOS release (private release tooling), editing TELOS after onboarding (use Telos / Interview), or LifeOS system maintenance (use the private maintenance skill).
5+
disable-model-invocation: true
46
argument-hint: "[setup|interview|update|uninstall]"
57
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
68
---

‎LifeOS/Tools/DeployComponents.ts‎

Lines changed: 117 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,26 @@ import { copyMissing, detectDevTree } from "./InstallEngine";
4040
// Enhancement components are the à-la-carte half of setup. The "LifeOS Core"
4141
// (skills + system prompt + base settings + CLAUDE.md) is installed by Setup's
4242
// core steps; these are the opt-in extras the user (or their AI) picks some/all/none of.
43-
const KNOWN_COMPONENTS = ["statusline", "tooltips", "spinnerverbs", "agents", "pulse", "worksweep", "derivedsync"] as const;
43+
//
44+
// Two kinds:
45+
// - Non-launchd: statusline, tooltips, spinnerverbs, agents, commands — settings.json merges + file copies
46+
// - Launchd services: delegated to Services.ts (single source of truth for all 16 background services)
47+
//
48+
// Component names for launchd services are their short labels (pulse, worksweep, amberroute, etc.)
49+
// or the full label (com.lifeos.pulse). Services.ts handles the mapping.
50+
const NON_LAUNCHD_COMPONENTS = ["statusline", "tooltips", "spinnerverbs", "agents", "commands"] as const;
51+
type NonLaunchdComponent = (typeof NON_LAUNCHD_COMPONENTS)[number];
52+
53+
// Launchd components — kept in sync with Services.ts. The install for these is delegated to Services.ts.
54+
// Short labels (without com.lifeos. prefix) for convenience; Services.ts accepts both forms.
55+
const LAUNCHD_COMPONENTS = [
56+
"pulse", "pulse-menubar", "deriver", "conduit", "conduit.insight", "synthesis",
57+
"worksweep", "derivedsync", "healthsync", "codexupdate", "commitmentsweep",
58+
"blogdiscovery", "usage-aggregator", "bookmark-watchdog", "backups", "amberroute"
59+
] as const;
60+
type LaunchdComponent = (typeof LAUNCHD_COMPONENTS)[number];
61+
62+
const KNOWN_COMPONENTS = [...NON_LAUNCHD_COMPONENTS, ...LAUNCHD_COMPONENTS] as const;
4463
type Component = (typeof KNOWN_COMPONENTS)[number];
4564

4665
interface Ctx {
@@ -49,6 +68,7 @@ interface Ctx {
4968
payloadRoot: string; // <skillRoot>/install/LIFEOS — the shipped runtime tree
5069
installRoot: string; // <skillRoot>/install — settings.enhancements.json + agents/ live here
5170
home: string;
71+
bun: string; // resolved bun binary path — substituted for __BUN_PATH__ in launchd plists
5272
launchAgents: string;
5373
apply: boolean;
5474
}
@@ -124,71 +144,8 @@ function launchctl(args: string[]): { ok: boolean; out: string } {
124144
}
125145
}
126146

127-
function httpCode(url: string): string {
128-
try {
129-
return execFileSync("curl", ["-s", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", "3", url]).toString().trim();
130-
} catch {
131-
return "000";
132-
}
133-
}
134-
135147
// ── component deployers ──────────────────────────────────────────────
136148

137-
/** Pulse: ensure the PULSE tree is laid down, then install + load its plist. */
138-
function deployPulse(ctx: Ctx): ComponentResult {
139-
const r: ComponentResult = { component: "pulse", ready: false, actions: [], blockers: [] };
140-
const av = availability("PULSE", ctx);
141-
const pulseDir = join(ctx.lifeosDir, "PULSE");
142-
const plistDst = join(ctx.launchAgents, "com.lifeos.pulse.plist");
143-
144-
if (!av.inLive && !av.inPayload) {
145-
r.blockers.push(`PULSE not in live tree (${pulseDir}) or payload (${join(ctx.payloadRoot, "PULSE")})`);
146-
return r;
147-
}
148-
r.ready = true;
149-
if (!ctx.apply) {
150-
if (!av.inLive) r.actions.push(`copy PULSE from payload → ${pulseDir}`);
151-
r.actions.push(`materialize ${plistDst} (__HOME__ → ${ctx.home})`, "launchctl bootstrap gui/<uid> (skip if already loaded + unchanged)", "poll 127.0.0.1:31337/healthz until 200");
152-
return r;
153-
}
154-
155-
try {
156-
ensurePresent("PULSE", ctx);
157-
const plistSrc = join(pulseDir, "com.lifeos.pulse.plist");
158-
if (!existsSync(plistSrc)) throw new Error(`plist template missing at ${plistSrc}`);
159-
const materialized = readFileSync(plistSrc, "utf-8").replaceAll("__HOME__", ctx.home);
160-
const u = uid();
161-
const sameOnDisk = existsSync(plistDst) && readFileSync(plistDst, "utf-8") === materialized;
162-
const alreadyLoaded = launchctl(["print", `gui/${u}/com.lifeos.pulse`]).ok;
163-
164-
// Idempotent: a loaded service with an identical plist is left running.
165-
if (sameOnDisk && alreadyLoaded) {
166-
r.applied = false;
167-
r.probe = { name: "pulse-healthz", passed: httpCode("http://127.0.0.1:31337/healthz") === "200", detail: "already loaded, plist unchanged (idempotent)" };
168-
return r;
169-
}
170-
171-
if (existsSync(plistDst) && !sameOnDisk) backup(plistDst);
172-
mkdirSync(ctx.launchAgents, { recursive: true });
173-
writeFileSync(plistDst, materialized);
174-
if (alreadyLoaded) launchctl(["bootout", `gui/${u}`, plistDst]);
175-
const boot = launchctl(["bootstrap", `gui/${u}`, plistDst]);
176-
r.applied = true;
177-
178-
// Readiness poll: launchd bootstrap returns before Pulse binds :31337.
179-
let code = "000";
180-
for (let i = 0; i < 12; i++) {
181-
code = httpCode("http://127.0.0.1:31337/healthz");
182-
if (code === "200") break;
183-
Bun.sleepSync(500);
184-
}
185-
r.probe = { name: "pulse-healthz", passed: code === "200", detail: `healthz → ${code}${boot.ok ? "" : ` (bootstrap: ${boot.out.trim()})`}` };
186-
} catch (err) {
187-
r.error = err instanceof Error ? err.message : String(err);
188-
}
189-
return r;
190-
}
191-
192149
/** Statusline: place the script, chmod +x, wire settings.json statusLine. */
193150
function deployStatusline(ctx: Ctx): ComponentResult {
194151
const r: ComponentResult = { component: "statusline", ready: false, actions: [], blockers: [] };
@@ -247,44 +204,6 @@ function deployStatusline(ctx: Ctx): ComponentResult {
247204
return r;
248205
}
249206

250-
/** Delegate a launchd job to its own standalone installer (no-arg = install). */
251-
function deployLaunchdJob(component: Component, installerRel: string, ctx: Ctx): ComponentResult {
252-
const r: ComponentResult = { component, ready: false, actions: [], blockers: [] };
253-
const av = availability(installerRel, ctx);
254-
// The installer needs sibling TOOLS files (templates, WorkSweep.ts, …), so the
255-
// unit of staging is the whole TOOLS dir — uniform with pulse/statusline.
256-
const toolsAv = availability("TOOLS", ctx);
257-
const installer = join(ctx.lifeosDir, installerRel);
258-
259-
if (!av.inLive && !toolsAv.inPayload) {
260-
r.blockers.push(`${installerRel} not in live tree and TOOLS not in payload (${join(ctx.payloadRoot, "TOOLS")}) — runtime tree not staged`);
261-
return r;
262-
}
263-
r.ready = true;
264-
if (!ctx.apply) {
265-
if (!av.inLive) r.actions.push(`stage TOOLS from payload → ${join(ctx.lifeosDir, "TOOLS")}`);
266-
r.actions.push(`bun ${installer} (delegates plist materialize + launchctl bootstrap)`);
267-
return r;
268-
}
269-
270-
try {
271-
if (!existsSync(installer)) ensurePresent("TOOLS", ctx);
272-
if (!existsSync(installer)) {
273-
r.blockers.push(`installer still missing after staging: ${installer}`);
274-
return r;
275-
}
276-
const out = execFileSync("bun", [installer], { stdio: ["pipe", "pipe", "pipe"], timeout: 60000 }).toString();
277-
r.applied = true;
278-
// Confirm the job actually loaded, not just that the installer exited 0.
279-
const label = component === "worksweep" ? "com.lifeos.worksweep" : "com.lifeos.derivedsync";
280-
const loaded = launchctl(["print", `gui/${uid()}/${label}`]).ok;
281-
r.probe = { name: `${component}-loaded`, passed: loaded, detail: loaded ? `${label} loaded` : `installer exit 0 but ${label} not loaded: ${out.trim().split("\n").slice(-1)[0]}` };
282-
} catch (err) {
283-
r.error = err instanceof Error ? err.message : String(err);
284-
}
285-
return r;
286-
}
287-
288207
/**
289208
* Merge a single Claude-Code enhancement key (spinnerTipsOverride / spinnerVerbs)
290209
* from the shipped `install/settings.enhancements.json` into the user's
@@ -361,16 +280,105 @@ function deployAgents(ctx: Ctx): ComponentResult {
361280
return r;
362281
}
363282

283+
// commands mirrors agents exactly: copy the payload's install/commands/ into the
284+
// user's ~/.claude/commands/, never overwriting. The payload is already filtered
285+
// at emit time to public commands only (a command ships iff its target skill
286+
// ships), so there is nothing private-pointing to guard against here.
287+
function deployCommands(ctx: Ctx): ComponentResult {
288+
const r: ComponentResult = { component: "commands", ready: false, actions: [], blockers: [] };
289+
const src = join(ctx.installRoot, "commands");
290+
const dst = join(ctx.configRoot, "commands");
291+
if (!existsSync(src) && !existsSync(dst)) {
292+
r.blockers.push(`commands not in payload (${src}) and not already installed (${dst})`);
293+
return r;
294+
}
295+
r.ready = true;
296+
if (!ctx.apply) {
297+
r.actions.push(existsSync(src) ? `copyMissing commands → ${dst} (never overwrites existing)` : `commands already present at ${dst} — no-op`);
298+
return r;
299+
}
300+
try {
301+
if (!existsSync(src)) {
302+
r.applied = false;
303+
r.probe = { name: "commands-present", passed: true, detail: `already present at ${dst} (no payload to copy)` };
304+
return r;
305+
}
306+
const { copied, failures } = copyMissing(src, dst);
307+
r.applied = copied > 0;
308+
r.probe = { name: "commands-copied", passed: failures.length === 0 && existsSync(dst), detail: `${copied} command file(s) copied${failures.length ? `, ${failures.length} failed` : ""}` };
309+
} catch (err) {
310+
r.error = err instanceof Error ? err.message : String(err);
311+
}
312+
return r;
313+
}
314+
315+
/**
316+
* Delegate launchd service install to Services.ts — single source of truth for all 16 services.
317+
* This keeps DeployComponents focused on non-launchd components (settings merges, file copies)
318+
* while Services.ts owns the full launchd machinery.
319+
*/
320+
function deployViaServices(component: LaunchdComponent, ctx: Ctx): ComponentResult {
321+
const r: ComponentResult = { component, ready: false, actions: [], blockers: [] };
322+
const servicesTs = join(ctx.lifeosDir, "TOOLS", "Services.ts");
323+
324+
// Services.ts must be present (either in live tree or staged from payload)
325+
const av = availability("TOOLS", ctx);
326+
if (!av.inLive && !av.inPayload) {
327+
r.blockers.push(`TOOLS not in live tree (${join(ctx.lifeosDir, "TOOLS")}) or payload`);
328+
return r;
329+
}
330+
r.ready = true;
331+
332+
// Build the label — Services.ts accepts short form (pulse) or full (com.lifeos.pulse)
333+
const label = component.startsWith("com.lifeos.") ? component : `com.lifeos.${component}`;
334+
335+
if (!ctx.apply) {
336+
if (!av.inLive) r.actions.push(`stage TOOLS from payload → ${join(ctx.lifeosDir, "TOOLS")}`);
337+
r.actions.push(`bun ${servicesTs.replace(ctx.home, "~")} install --only ${component} --yes`);
338+
return r;
339+
}
340+
341+
try {
342+
ensurePresent("TOOLS", ctx);
343+
if (!existsSync(servicesTs)) {
344+
r.blockers.push(`Services.ts still missing after staging: ${servicesTs}`);
345+
return r;
346+
}
347+
// Delegate to Services.ts
348+
const out = execFileSync("bun", [servicesTs, "install", "--only", component, "--yes"], {
349+
stdio: ["pipe", "pipe", "pipe"],
350+
timeout: 120000, // some services take longer (e.g. Pulse waits for healthz)
351+
cwd: dirname(servicesTs),
352+
}).toString();
353+
r.applied = true;
354+
// Confirm the job actually loaded
355+
const loaded = launchctl(["print", `gui/${uid()}/${label}`]).ok;
356+
r.probe = { name: `${component}-loaded`, passed: loaded, detail: loaded ? `${label} loaded via Services.ts` : `Services.ts exit 0 but ${label} not loaded: ${out.trim().split("\n").slice(-1)[0]}` };
357+
} catch (err) {
358+
r.error = err instanceof Error ? err.message : String(err);
359+
}
360+
return r;
361+
}
362+
363+
function isLaunchdComponent(c: Component): c is LaunchdComponent {
364+
return (LAUNCHD_COMPONENTS as readonly string[]).includes(c);
365+
}
366+
364367
function deploy(component: Component, ctx: Ctx): ComponentResult {
368+
// Non-launchd components: handled directly
365369
switch (component) {
366-
case "pulse": return deployPulse(ctx);
367370
case "statusline": return deployStatusline(ctx);
368371
case "tooltips": return deploySettingsKey("tooltips", "spinnerTipsOverride", ctx);
369372
case "spinnerverbs": return deploySettingsKey("spinnerverbs", "spinnerVerbs", ctx);
370373
case "agents": return deployAgents(ctx);
371-
case "worksweep": return deployLaunchdJob("worksweep", join("TOOLS", "InstallWorkSweep.ts"), ctx);
372-
case "derivedsync": return deployLaunchdJob("derivedsync", join("TOOLS", "InstallDerivedSync.ts"), ctx);
374+
case "commands": return deployCommands(ctx);
375+
}
376+
// Launchd components: delegate to Services.ts
377+
if (isLaunchdComponent(component)) {
378+
return deployViaServices(component, ctx);
373379
}
380+
// Fallback (shouldn't reach here with proper types, but TypeScript wants exhaustiveness)
381+
return { component, ready: false, actions: [], blockers: [`unknown component: ${component}`] };
374382
}
375383

376384
// ── main ─────────────────────────────────────────────────────────────
@@ -417,6 +425,10 @@ function main(): void {
417425
: join(skillRoot, "install", "LifeOS"),
418426
installRoot: join(skillRoot, "install"),
419427
home,
428+
// launchd runs the plist with a minimal PATH, so ProgramArguments[0] must be an
429+
// absolute bun path. Prefer the interpreter running this installer; fall back to
430+
// the standard bun install location.
431+
bun: /\/bun$/.test(process.execPath) ? process.execPath : join(home, ".bun", "bin", "bun"),
420432
launchAgents: join(home, "Library", "LaunchAgents"),
421433
apply,
422434
};

‎LifeOS/Tools/DeployCore.ts‎

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,18 @@ import { copyMissing, detectDevTree } from "./InstallEngine";
3232
// - MEMORY per-install state, never shipped — but scaffoldMemory() creates the
3333
// empty tree at install so ISASync/hooks/memory writes have a home
3434
// (this is where EmitSkill's "MEMORY scaffolded fresh at setup" becomes true)
35-
// - LIFEOS_INSTALL legacy installer engine, not part of the live runtime
3635
// - node_modules / .git never deploy
37-
// copyMissing's own SKIP_DIRS covers MEMORY/LIFEOS_INSTALL when nested, but these
36+
// copyMissing's own SKIP_DIRS covers MEMORY when nested, but these
3837
// are TOP-LEVEL entries of the runtime payload, so we filter them here explicitly.
39-
const RUNTIME_SKIP = new Set(["USER", "MEMORY", "LIFEOS_INSTALL", "node_modules", ".git"]);
38+
const RUNTIME_SKIP = new Set(["USER", "MEMORY", "node_modules", ".git"]);
4039

4140
function arg(a: string[], flag: string): string | undefined {
4241
const i = a.indexOf(flag);
4342
return i >= 0 && a[i + 1] && !a[i + 1].startsWith("--") ? a[i + 1] : undefined;
4443
}
4544

4645
interface DeployResult {
47-
what: "skills" | "runtime" | "memory";
46+
what: "skills" | "runtime" | "memory" | "dependencies";
4847
src: string;
4948
dst: string;
5049
present: boolean;
@@ -132,6 +131,39 @@ function scaffoldMemory(configRoot: string, apply: boolean): DeployResult {
132131
return r;
133132
}
134133

134+
/**
135+
* (d) shared runtime deps: install/package.json → configRoot/package.json, then
136+
* `bun install` in configRoot. Several deployed hooks/TOOLS scripts (e.g.
137+
* hooks/lib/identity.ts, LIFEOS/TOOLS/Banner.ts) import npm packages (yaml)
138+
* that resolve via node_modules walked up from configRoot — without this step
139+
* those scripts throw "Cannot find package" on first run after a fresh install.
140+
*/
141+
function deployDependencies(payloadInstall: string, configRoot: string, apply: boolean): DeployResult {
142+
const src = join(payloadInstall, "package.json");
143+
const dst = join(configRoot, "package.json");
144+
const r: DeployResult = { what: "dependencies", src, dst, present: existsSync(src), copied: 0, actions: [], blockers: [], failures: [] };
145+
if (!r.present) {
146+
r.blockers.push(`dependency manifest missing: ${src} — point --skill-root at a staged release`);
147+
return r;
148+
}
149+
if (!apply) {
150+
r.actions.push(`copyMissing ${src} → ${dst}`, `bun install --cwd ${configRoot}`);
151+
return r;
152+
}
153+
const { copied, failures } = copyMissing(src, dst);
154+
r.copied = copied;
155+
r.failures = failures;
156+
if (failures.length === 0) {
157+
const proc = Bun.spawnSync(["bun", "install"], { cwd: configRoot, stdout: "pipe", stderr: "pipe" });
158+
if (proc.exitCode !== 0) {
159+
r.failures.push(`bun install --cwd ${configRoot} exited ${proc.exitCode}: ${proc.stderr.toString().trim()}`);
160+
} else {
161+
r.actions.push(`bun install --cwd ${configRoot}`);
162+
}
163+
}
164+
return r;
165+
}
166+
135167
function main(): void {
136168
const a = process.argv.slice(2);
137169
const home = process.env.HOME || homedir();
@@ -154,6 +186,7 @@ function main(): void {
154186
deploySkills(payloadInstall, configRoot, apply),
155187
deployRuntime(payloadInstall, configRoot, apply),
156188
scaffoldMemory(configRoot, apply),
189+
deployDependencies(payloadInstall, configRoot, apply),
157190
];
158191

159192
// A missing required payload source (blocker) or a copy failure is a hard

0 commit comments

Comments
 (0)