diff --git a/LifeOS/install/LIFEOS/TOOLS/Inference.ts b/LifeOS/install/LIFEOS/TOOLS/Inference.ts index f13b5cea0a..c4e2bcbd27 100755 --- a/LifeOS/install/LIFEOS/TOOLS/Inference.ts +++ b/LifeOS/install/LIFEOS/TOOLS/Inference.ts @@ -67,6 +67,32 @@ */ import { spawn } from "child_process"; +import { existsSync } from "fs"; + +// Resolve `claude` to an absolute path. Bare `spawn('claude')` relies on PATH, +// which is fine interactively but ENOENTs under launchd/cron (minimal PATH lacks +// ~/.local/bin) — the failure that silently wedges any scheduled inference caller. +let cachedClaudeBin: string | null = null; +export function resolveClaudeBin(): string { + if (cachedClaudeBin) return cachedClaudeBin; + const explicit = process.env.CLAUDE_BIN; + if (explicit && existsSync(explicit)) return (cachedClaudeBin = explicit); + const home = process.env.HOME ?? ""; + const candidates = [ + home ? `${home}/.local/bin/claude` : "", + home ? `${home}/.claude/local/claude` : "", + "/opt/homebrew/bin/claude", + "/usr/local/bin/claude", + "/usr/bin/claude", + ].filter(Boolean); + for (const p of candidates) { + if (existsSync(p)) return (cachedClaudeBin = p); + } + for (const dir of (process.env.PATH ?? "").split(":")) { + if (dir && existsSync(`${dir}/claude`)) return (cachedClaudeBin = `${dir}/claude`); + } + return (cachedClaudeBin = "claude"); // last resort: preserve prior behavior +} /** The four run levels — mirrors models.ts EffortLevel. */ export type InferenceLevel = 'low' | 'medium' | 'high' | 'max'; @@ -182,7 +208,7 @@ async function inferenceAttempt(options: InferenceOptions, modelOverride?: strin let stdout = ''; let stderr = ''; - const proc = spawn('claude', args, { + const proc = spawn(resolveClaudeBin(), args, { env, stdio: ['pipe', 'pipe', 'pipe'], }); diff --git a/LifeOS/install/LIFEOS/TOOLS/algorithm.ts b/LifeOS/install/LIFEOS/TOOLS/algorithm.ts index 296bb39eba..ca22c3e364 100644 --- a/LifeOS/install/LIFEOS/TOOLS/algorithm.ts +++ b/LifeOS/install/LIFEOS/TOOLS/algorithm.ts @@ -51,6 +51,7 @@ import { resolve, basename, join, dirname } from "path"; import { spawnSync, spawn } from "child_process"; import { randomUUID } from "crypto"; import { generateISATemplate } from "../../../.claude/hooks/lib/isa-template"; +import { resolveClaudeBin } from "./Inference"; // Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404) for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { @@ -842,7 +843,7 @@ async function runParallelIteration( const processes = assignments.map(assignment => { const criterion = assignment.criteriaDetails[0]; // One criterion per agent const prompt = buildWorkerPrompt(isaPath, assignment.agentId, criterion, iteration); - const proc = Bun.spawn(["claude", "-p", prompt, + const proc = Bun.spawn([resolveClaudeBin(), "-p", prompt, "--allowedTools", "Edit,Write,Bash,Read,Glob,Grep,WebFetch,WebSearch,NotebookEdit", ], { cwd: dirname(isaPath), @@ -1437,7 +1438,7 @@ function runInteractive(isaPath: string): void { console.log(` Launching claude...\n`); // Launch interactive claude session with ISA context - const child = spawn("claude", [ + const child = spawn(resolveClaudeBin(), [ prompt, "--allowedTools", "Edit,Write,Bash,Read,Glob,Grep,WebFetch,WebSearch,Task,TaskCreate,TaskUpdate,TaskList,NotebookEdit", ], { @@ -1503,7 +1504,7 @@ function runIdeate( console.log(` Launching claude...\n`); // Launch interactive claude session with ideate context - const child = spawn("claude", [ + const child = spawn(resolveClaudeBin(), [ prompt, "--allowedTools", "Edit,Write,Bash,Read,Glob,Grep,WebFetch,WebSearch,Task,TaskCreate,TaskUpdate,TaskList,NotebookEdit", ], {