From 383d20a16af2e8ce56dcfc3bf36627e421daf59f Mon Sep 17 00:00:00 2001 From: elhoim Date: Mon, 3 Aug 2026 07:01:04 +0000 Subject: [PATCH 1/7] fix(install): quote the launcher paths written into the shell alias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The alias body interpolated $LAUNCHER/$SYS_PROMPT unquoted, so a $HOME with a space broke the launcher and one with a quote corrupted the rc file — and the awk pass has already commented out the working old alias by then, leaving the user with no alias at all. Both shipped copies updated. --- LifeOS/install/install.sh | 15 ++++++++++++--- LifeOS/install/skills/LifeOS/install/install.sh | 15 ++++++++++++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/LifeOS/install/install.sh b/LifeOS/install/install.sh index 30d95458b2..b0a01c2700 100755 --- a/LifeOS/install/install.sh +++ b/LifeOS/install/install.sh @@ -284,6 +284,12 @@ step "5/6 Migrating launch aliases (pre-7.x upgrades)" CONFIG_ROOT="$(dirname "$LIFEOS_SKILLS_DIR")" LAUNCHER="$CONFIG_ROOT/LIFEOS/TOOLS/lifeos.ts" SYS_PROMPT="$CONFIG_ROOT/LIFEOS/LIFEOS_SYSTEM_PROMPT.md" +# Single-quote a value for safe embedding in shell source. A literal `'` is +# closed, backslash-escaped and reopened (it's → 'it'\''s'). Needed because the +# alias body is re-parsed by the shell: an unquoted $HOME with a space breaks the +# launcher, and one with a quote corrupts the rc file — and by this point the old +# working alias is already commented out, so the user is left with no alias. +shq() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"; } migrate_rc() { local rc="$1" stale names n ts [ -f "$rc" ] || return 0 @@ -304,16 +310,19 @@ migrate_rc() { { print } ' "$rc" > "$rc.lifeos-tmp" && mv "$rc.lifeos-tmp" "$rc" if [ -f "$LAUNCHER" ]; then - local add_lifeos=1 + local add_lifeos=1 alias_body + # Two levels of quoting for two levels of parsing: the inner shq protects the + # paths when the alias body runs, the outer one when the rc file is sourced. + alias_body="bun $(shq "$LAUNCHER") -s $(shq "$SYS_PROMPT")" printf '%s\n' $names | grep -qx lifeos && add_lifeos=0 grep -E '^[[:space:]]*alias[[:space:]]+lifeos=' "$rc" 2>/dev/null | grep -q 'LIFEOS_SYSTEM_PROMPT' && add_lifeos=0 { echo "" echo "# LifeOS ${LIFEOS_TAG} launch aliases (repointed from pre-7.x by install.sh)" for n in $names; do - echo "alias $n='bun $LAUNCHER -s $SYS_PROMPT'" + echo "alias $n=$(shq "$alias_body")" done - if [ "$add_lifeos" = "1" ]; then echo "alias lifeos='bun $LAUNCHER -s $SYS_PROMPT'"; fi + if [ "$add_lifeos" = "1" ]; then echo "alias lifeos=$(shq "$alias_body")"; fi } >> "$rc" success "Repointed $(echo $names | tr '\n' ' ')to the constituted 7.x launcher (backup: $(basename "$rc").lifeos-backup-$ts)" else diff --git a/LifeOS/install/skills/LifeOS/install/install.sh b/LifeOS/install/skills/LifeOS/install/install.sh index 30d95458b2..b0a01c2700 100755 --- a/LifeOS/install/skills/LifeOS/install/install.sh +++ b/LifeOS/install/skills/LifeOS/install/install.sh @@ -284,6 +284,12 @@ step "5/6 Migrating launch aliases (pre-7.x upgrades)" CONFIG_ROOT="$(dirname "$LIFEOS_SKILLS_DIR")" LAUNCHER="$CONFIG_ROOT/LIFEOS/TOOLS/lifeos.ts" SYS_PROMPT="$CONFIG_ROOT/LIFEOS/LIFEOS_SYSTEM_PROMPT.md" +# Single-quote a value for safe embedding in shell source. A literal `'` is +# closed, backslash-escaped and reopened (it's → 'it'\''s'). Needed because the +# alias body is re-parsed by the shell: an unquoted $HOME with a space breaks the +# launcher, and one with a quote corrupts the rc file — and by this point the old +# working alias is already commented out, so the user is left with no alias. +shq() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"; } migrate_rc() { local rc="$1" stale names n ts [ -f "$rc" ] || return 0 @@ -304,16 +310,19 @@ migrate_rc() { { print } ' "$rc" > "$rc.lifeos-tmp" && mv "$rc.lifeos-tmp" "$rc" if [ -f "$LAUNCHER" ]; then - local add_lifeos=1 + local add_lifeos=1 alias_body + # Two levels of quoting for two levels of parsing: the inner shq protects the + # paths when the alias body runs, the outer one when the rc file is sourced. + alias_body="bun $(shq "$LAUNCHER") -s $(shq "$SYS_PROMPT")" printf '%s\n' $names | grep -qx lifeos && add_lifeos=0 grep -E '^[[:space:]]*alias[[:space:]]+lifeos=' "$rc" 2>/dev/null | grep -q 'LIFEOS_SYSTEM_PROMPT' && add_lifeos=0 { echo "" echo "# LifeOS ${LIFEOS_TAG} launch aliases (repointed from pre-7.x by install.sh)" for n in $names; do - echo "alias $n='bun $LAUNCHER -s $SYS_PROMPT'" + echo "alias $n=$(shq "$alias_body")" done - if [ "$add_lifeos" = "1" ]; then echo "alias lifeos='bun $LAUNCHER -s $SYS_PROMPT'"; fi + if [ "$add_lifeos" = "1" ]; then echo "alias lifeos=$(shq "$alias_body")"; fi } >> "$rc" success "Repointed $(echo $names | tr '\n' ' ')to the constituted 7.x launcher (backup: $(basename "$rc").lifeos-backup-$ts)" else From 59e6627ae6a50c79b66627666541aeb31209c136 Mon Sep 17 00:00:00 2001 From: elhoim Date: Mon, 3 Aug 2026 07:01:04 +0000 Subject: [PATCH 2/7] fix(install): stop DeployCore running bun install across every user skill It walked all of configRoot/skills running bun install in every directory with a package.json, touching pre-existing user skills and double-installing the payload copy; failures land in blockers, so the documented offline install exited 1. Both shipped copies updated. --- LifeOS/Tools/DeployCore.ts | 30 +++++++++++++++---- .../install/skills/LifeOS/Tools/DeployCore.ts | 30 +++++++++++++++---- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/LifeOS/Tools/DeployCore.ts b/LifeOS/Tools/DeployCore.ts index a709db7bf6..d8687cbafe 100755 --- a/LifeOS/Tools/DeployCore.ts +++ b/LifeOS/Tools/DeployCore.ts @@ -24,7 +24,7 @@ import { existsSync, mkdirSync, readdirSync } from "node:fs"; import { homedir } from "node:os"; -import { join } from "node:path"; +import { basename, dirname, join } from "node:path"; import { copyMissing, detectDevTree } from "./InstallEngine"; // Runtime top-level entries this tool does NOT deploy: @@ -242,10 +242,10 @@ function deployDependencies(payloadInstall: string, configRoot: string, apply: b * pulse.ts otherwise reports as a copy-paste fix command, so a fresh Pulse * doesn't 503 until a human intervenes. */ -function findNestedDependencyDirs(runtimeDst: string): string[] { +function findNestedDependencyDirs(runtimeDst: string, skip: Set = new Set()): string[] { const found: string[] = []; const walk = (dir: string): void => { - if (!existsSync(dir)) return; + if (!existsSync(dir) || skip.has(dir)) return; for (const entry of readdirSync(dir, { withFileTypes: true })) { if (entry.name === "node_modules" || entry.name === ".git") continue; const p = join(dir, entry.name); @@ -260,7 +260,7 @@ function findNestedDependencyDirs(runtimeDst: string): string[] { return found.sort(); } -function deployNestedDependencies(configRoot: string, apply: boolean): DeployResult { +function deployNestedDependencies(payloadInstall: string, configRoot: string, apply: boolean): DeployResult { const runtimeDst = join(configRoot, "LIFEOS"); const skillsDst = join(configRoot, "skills"); const r: DeployResult = { @@ -272,9 +272,27 @@ function deployNestedDependencies(configRoot: string, apply: boolean): DeployRes // Walk skills/ alongside LIFEOS/ — skills ship nested package.json manifests // too (Apify, Evals, Prompting templates, Art/Remotion tools), and installing // only the runtime tree left them import-broken. Public issue #1605, @cristbc. + // + // Scoped to the skills THIS payload ships, per skill dir: configRoot/skills + // also holds the principal's own pre-existing skills — running `bun install` + // in those mutates dirs we never deployed (and whose install we don't own), + // and any failure there fails OUR deploy. Skills we skipped on a + // case-insensitive collision are excluded for the same reason (not in the + // payload-name → deployed-dir set we created). + const payloadSkills = join(payloadInstall, "skills"); + // The deployed LifeOS skill carries a second full copy of this very payload + // under /install — walking it re-installs every payload skill a second + // time, into a tree nothing imports from. + const nestedPayload = join(skillsDst, basename(dirname(payloadInstall)), "install"); + const ownSkillDirs = existsSync(payloadSkills) + ? readdirSync(payloadSkills, { withFileTypes: true }) + .filter((e) => e.isDirectory()) + .map((e) => join(skillsDst, e.name)) + .filter(existsSync) + : []; const dirs = [ ...findNestedDependencyDirs(runtimeDst), - ...(existsSync(skillsDst) ? findNestedDependencyDirs(skillsDst) : []), + ...ownSkillDirs.flatMap((d) => findNestedDependencyDirs(d, new Set([nestedPayload]))), ]; for (const dir of dirs) { const isObservability = dir === join(runtimeDst, "PULSE", "Observability"); @@ -330,7 +348,7 @@ function main(): void { deployRuntime(payloadInstall, configRoot, apply), scaffoldMemory(configRoot, apply), deployDependencies(payloadInstall, configRoot, apply), - deployNestedDependencies(configRoot, apply), + deployNestedDependencies(payloadInstall, configRoot, apply), ]; // A missing required payload source (blocker) or a copy failure is a hard diff --git a/LifeOS/install/skills/LifeOS/Tools/DeployCore.ts b/LifeOS/install/skills/LifeOS/Tools/DeployCore.ts index a709db7bf6..d8687cbafe 100755 --- a/LifeOS/install/skills/LifeOS/Tools/DeployCore.ts +++ b/LifeOS/install/skills/LifeOS/Tools/DeployCore.ts @@ -24,7 +24,7 @@ import { existsSync, mkdirSync, readdirSync } from "node:fs"; import { homedir } from "node:os"; -import { join } from "node:path"; +import { basename, dirname, join } from "node:path"; import { copyMissing, detectDevTree } from "./InstallEngine"; // Runtime top-level entries this tool does NOT deploy: @@ -242,10 +242,10 @@ function deployDependencies(payloadInstall: string, configRoot: string, apply: b * pulse.ts otherwise reports as a copy-paste fix command, so a fresh Pulse * doesn't 503 until a human intervenes. */ -function findNestedDependencyDirs(runtimeDst: string): string[] { +function findNestedDependencyDirs(runtimeDst: string, skip: Set = new Set()): string[] { const found: string[] = []; const walk = (dir: string): void => { - if (!existsSync(dir)) return; + if (!existsSync(dir) || skip.has(dir)) return; for (const entry of readdirSync(dir, { withFileTypes: true })) { if (entry.name === "node_modules" || entry.name === ".git") continue; const p = join(dir, entry.name); @@ -260,7 +260,7 @@ function findNestedDependencyDirs(runtimeDst: string): string[] { return found.sort(); } -function deployNestedDependencies(configRoot: string, apply: boolean): DeployResult { +function deployNestedDependencies(payloadInstall: string, configRoot: string, apply: boolean): DeployResult { const runtimeDst = join(configRoot, "LIFEOS"); const skillsDst = join(configRoot, "skills"); const r: DeployResult = { @@ -272,9 +272,27 @@ function deployNestedDependencies(configRoot: string, apply: boolean): DeployRes // Walk skills/ alongside LIFEOS/ — skills ship nested package.json manifests // too (Apify, Evals, Prompting templates, Art/Remotion tools), and installing // only the runtime tree left them import-broken. Public issue #1605, @cristbc. + // + // Scoped to the skills THIS payload ships, per skill dir: configRoot/skills + // also holds the principal's own pre-existing skills — running `bun install` + // in those mutates dirs we never deployed (and whose install we don't own), + // and any failure there fails OUR deploy. Skills we skipped on a + // case-insensitive collision are excluded for the same reason (not in the + // payload-name → deployed-dir set we created). + const payloadSkills = join(payloadInstall, "skills"); + // The deployed LifeOS skill carries a second full copy of this very payload + // under /install — walking it re-installs every payload skill a second + // time, into a tree nothing imports from. + const nestedPayload = join(skillsDst, basename(dirname(payloadInstall)), "install"); + const ownSkillDirs = existsSync(payloadSkills) + ? readdirSync(payloadSkills, { withFileTypes: true }) + .filter((e) => e.isDirectory()) + .map((e) => join(skillsDst, e.name)) + .filter(existsSync) + : []; const dirs = [ ...findNestedDependencyDirs(runtimeDst), - ...(existsSync(skillsDst) ? findNestedDependencyDirs(skillsDst) : []), + ...ownSkillDirs.flatMap((d) => findNestedDependencyDirs(d, new Set([nestedPayload]))), ]; for (const dir of dirs) { const isObservability = dir === join(runtimeDst, "PULSE", "Observability"); @@ -330,7 +348,7 @@ function main(): void { deployRuntime(payloadInstall, configRoot, apply), scaffoldMemory(configRoot, apply), deployDependencies(payloadInstall, configRoot, apply), - deployNestedDependencies(configRoot, apply), + deployNestedDependencies(payloadInstall, configRoot, apply), ]; // A missing required payload source (blocker) or a copy failure is a hard From 2cacf0f4ccf01b4a975c35cf09489366c07bbc5b Mon Sep 17 00:00:00 2001 From: elhoim Date: Mon, 3 Aug 2026 07:01:04 +0000 Subject: [PATCH 3/7] fix(tldraw): cascade binding removal to a fixpoint A single forward pass left the start binding dangling when the target shape was removed. Reproducible with the tool's own validate: "binding:shape-a-start: dangling binding" before, valid after. --- LifeOS/install/skills/Tldraw/Tools/Tldr.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/LifeOS/install/skills/Tldraw/Tools/Tldr.ts b/LifeOS/install/skills/Tldraw/Tools/Tldr.ts index 687948a44c..699b7ba997 100644 --- a/LifeOS/install/skills/Tldraw/Tools/Tldr.ts +++ b/LifeOS/install/skills/Tldraw/Tools/Tldr.ts @@ -267,9 +267,16 @@ async function cmdRemove(file: string) { if (!full) die(`no record ${id}`) drop.add(full) } - // cascade: bindings touching dropped shapes, and arrows left dangling - for (const b of doc.records.filter((r) => r.typeName === "binding")) { - if (drop.has(b.toId) || drop.has(b.fromId)) { drop.add(b.id); drop.add(b.fromId) } + // cascade: bindings touching dropped shapes, and arrows left dangling. + // Repeat to a fixpoint — dropping an arrow orphans its OTHER binding, which a + // single forward pass has already walked past (remove the `to` shape and the + // `start` binding survives, dangling). + const bindings = doc.records.filter((r) => r.typeName === "binding") + for (let size = -1; size !== drop.size; ) { + size = drop.size + for (const b of bindings) { + if (drop.has(b.toId) || drop.has(b.fromId)) { drop.add(b.id); drop.add(b.fromId) } + } } const before = doc.records.length doc.records = doc.records.filter((r) => !drop.has(r.id)) From d4cbd2f3be6a679a9cd97eef000b7ce469b42cce Mon Sep 17 00:00:00 2001 From: elhoim Date: Mon, 3 Aug 2026 07:01:05 +0000 Subject: [PATCH 4/7] fix(audioeditor): never print CLEAN off a decode that failed The verification decode's exit status was unchecked, so on failure it certified a file it had never scanned and exited 0. --- LifeOS/install/skills/AudioEditor/Tools/GateRepair.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/LifeOS/install/skills/AudioEditor/Tools/GateRepair.ts b/LifeOS/install/skills/AudioEditor/Tools/GateRepair.ts index a98e9c68dd..92eb484c84 100644 --- a/LifeOS/install/skills/AudioEditor/Tools/GateRepair.ts +++ b/LifeOS/install/skills/AudioEditor/Tools/GateRepair.ts @@ -182,6 +182,9 @@ for (let iter = 1; iter <= 5; iter++) { const enc = spawnSync("ffmpeg", ["-v", "error", "-i", inFile, "-i", tmpWav, "-map", "0:v:0", "-map", "1:a:0", "-c:v", "copy", "-c:a", "aac", "-b:a", ABR, "-ar", String(SR), "-movflags", "+faststart", outFile, "-y"]); if (enc.status !== 0) { console.error("encode failed:", enc.stderr?.toString().slice(0, 300)); process.exit(2); } const dec = spawnSync("ffmpeg", ["-v", "error", "-i", outFile, "-map", "0:a:0", "-ac", "1", "-ar", String(SR), "-f", "f32le", "-"], { maxBuffer: 8 * (1 << 30) }); + // An unchecked decode certifies CLEAN on an empty scan — never claim a file + // is clean off a read that failed. + if (dec.status !== 0 || !dec.stdout) { console.error("verification decode failed:", dec.stderr?.toString().slice(0, 300)); process.exit(2); } const xb: Buffer = dec.stdout; const xe = new Float32Array(xb.buffer, xb.byteOffset, Math.floor(xb.length / 4)); const sites = stepScan(xe, SR); From 911c78e04ed94dbbf2040c9c9f56487e13bb206a Mon Sep 17 00:00:00 2001 From: elhoim Date: Mon, 3 Aug 2026 07:01:05 +0000 Subject: [PATCH 5/7] fix(localintelligence): survive a malformed user sources.json An unguarded JSON.parse threw after all eight fetchers had already succeeded, so a trailing comma discarded the entire digest. --- .../skills/LocalIntelligence/Tools/UserSources.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/LifeOS/install/skills/LocalIntelligence/Tools/UserSources.ts b/LifeOS/install/skills/LocalIntelligence/Tools/UserSources.ts index 3f8ef8d1ff..ad93cad67b 100644 --- a/LifeOS/install/skills/LocalIntelligence/Tools/UserSources.ts +++ b/LifeOS/install/skills/LocalIntelligence/Tools/UserSources.ts @@ -85,7 +85,15 @@ export async function loadUserSources(): Promise { } catch { return [] // no config = no user sources, not an error } - const parsed = JSON.parse(raw) as { sources?: UserSource[] } + let parsed: { sources?: UserSource[] } + try { + parsed = JSON.parse(raw) as { sources?: UserSource[] } + } catch (err) { + // Malformed user config must not sink a whole refresh — the built-in + // fetchers have already run by this point and the digest is unsaved. + console.error(`[user-sources] ignoring malformed ${CONFIG_PATH}: ${(err as Error).message}`) + return [] + } return (parsed.sources ?? []).filter( (s) => s.enabled !== false && SECTION_KEYS.includes(s.section) && s.url && s.name ) From c6d1c0b455a1b887e2ca833be962a748dca00de7 Mon Sep 17 00:00:00 2001 From: elhoim Date: Mon, 3 Aug 2026 07:01:05 +0000 Subject: [PATCH 6/7] fix(evals): diagnose a legacy v1 suite instead of throwing a TypeError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The only shipped suite is still the v1 `tasks:` format, so every run died on `for (const c of undefined)`; callers that catch broadly turned that into a regression eval which silently never ran. No v1→v2 translation is attempted: UseCases/*.yaml define weighted graders and carry no prompt, so there is nothing for this single-shot runner to send. --- LifeOS/install/skills/Evals/Tools/EvalRunner.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/LifeOS/install/skills/Evals/Tools/EvalRunner.ts b/LifeOS/install/skills/Evals/Tools/EvalRunner.ts index 967edea5a1..5c98a673ee 100644 --- a/LifeOS/install/skills/Evals/Tools/EvalRunner.ts +++ b/LifeOS/install/skills/Evals/Tools/EvalRunner.ts @@ -113,6 +113,21 @@ export async function runSuite(name: string, override: Partial = {} } else { return { suite: name, type: 'unknown', passed: false, score: 0, pass_to_k: 0, pass_at_k: 0, summary: `Suite not found: ${name}`, run_id: 'error', cases: [] }; } + // Diagnose a v1 suite instead of dying on `for (const c of undefined)`. The + // only shipped suite (Suites/Regression/core-behaviors.yaml) still uses the + // legacy `tasks:` list, so every run threw a bare TypeError — and callers that + // catch broadly (ConfigEvalOnChange) turned that into a regression eval which + // silently never ran. The two formats are not mechanically convertible: + // UseCases/*.yaml define weighted graders and carry no `prompt`, so there is + // nothing for this single-shot runner to send. Say so rather than guess. + if (!Array.isArray(suite.cases) || suite.cases.length === 0) { + const legacy = (suite as unknown as { tasks?: unknown[] }).tasks; + throw new Error( + Array.isArray(legacy) + ? `Suite '${name}' is in the legacy v1 format: it lists ${legacy.length} \`tasks:\` referencing Evals/UseCases/*.yaml, which define graders and no \`prompt\`. This runner needs v2 \`cases:\` with prompt/assert. Migrate the suite, or run it through a UseCase-aware runner.` + : `Suite '${name}' has no \`cases:\` to run.`, + ); + } const threshold = suite.pass_threshold ?? 0.75; const trials = suite.trials ?? 3; const agentLevel: InferenceLevel = suite.agent_level ?? 'medium'; From 23f7df8283a646f475d315dad63b412075eceb7a Mon Sep 17 00:00:00 2001 From: elhoim Date: Mon, 3 Aug 2026 07:01:05 +0000 Subject: [PATCH 7/7] fix(evals): compute pass^k as all-k-passed, not as the mean passed/trials reported 2-of-3 as 67% where the true pass^k is 0, making a flaky case look mostly-passing. --- LifeOS/install/skills/Evals/Tools/EvalRunner.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/LifeOS/install/skills/Evals/Tools/EvalRunner.ts b/LifeOS/install/skills/Evals/Tools/EvalRunner.ts index 5c98a673ee..51fd7766d6 100644 --- a/LifeOS/install/skills/Evals/Tools/EvalRunner.ts +++ b/LifeOS/install/skills/Evals/Tools/EvalRunner.ts @@ -161,7 +161,10 @@ export async function runSuite(name: string, override: Partial = {} id: c.id, mean_score: trialResults.reduce((s, t) => s + t.score, 0) / trials, pass_at_k: passed > 0 ? 1 : 0, - pass_to_k: passed / trials, + // pass^k is "every one of the k trials passed" — a reliability measure. + // passed/trials is the MEAN, which reported 2-of-3 as 67% where the true + // pass^k is 0, making a flaky case look like a mostly-passing one. + pass_to_k: passed === trials ? 1 : 0, trials: trialResults, }); }