import { spawn, execSync } from "child_process"; import * as fs from "fs"; import * as path from "path"; const ROOT = "/Users/ace/palacering"; const BASE_URL = "http://localhost:6572"; const LOGS_DIR = path.resolve(__dirname, "logs/runs"); const LOCK_FILE = path.resolve(__dirname, ".healing"); const target = process.argv[2] as "palacecode" | "palacemail"; if (target !== "palacecode" && target !== "palacemail") { console.error("[self-heal] usage: self-heal.ts palacecode|palacemail"); process.exit(1); } function pdt(): string { return new Date().toLocaleString("en-US", { timeZone: "America/Los_Angeles", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false, }); } function todayPST(): string { return new Date().toLocaleDateString("en-CA", { timeZone: "America/Los_Angeles" }); } function timePST(): string { return new Date().toLocaleTimeString("en-US", { timeZone: "America/Los_Angeles", hour: "2-digit", minute: "2-digit", hour12: false, }).replace(":", "-"); } async function serverIsUp(): Promise { try { const res = await fetch(BASE_URL, { signal: AbortSignal.timeout(5000) }); return res.ok || res.status === 302 || res.status === 404; } catch { return false; } } function restartPalacering(): void { console.log("[self-heal] restarting palacering service"); try { const uid = execSync("id -u").toString().trim(); execSync(`launchctl kickstart -k gui/${uid}/com.manglasabang.palacering`, { timeout: 10000 }); } catch (err: any) { console.error("[self-heal] failed to restart:", err.message); } } async function waitForServer(maxWait = 30000): Promise { const start = Date.now(); while (Date.now() - start < maxWait) { if (await serverIsUp()) return true; await new Promise(r => setTimeout(r, 2000)); } return false; } const PROMPTS: Record = { palacecode: `You are an autonomous self-healing agent for Palace Code (palacering.com/code). ## Your Job 1. TEST every feature of Palace Code using Chrome browser 2. RECORD what is broken 3. FIX broken things by editing source files 4. REBUILD the app and verify fixes ## How To Test Use the Chrome browser tools (mcp__claude-in-chrome__*) to test the app visually, like a real user. First call mcp__claude-in-chrome__tabs_context_mcp to see current tabs. Then create a new tab with mcp__claude-in-chrome__tabs_create_mcp to http://localhost:6572/code/ ### Phase 1: Layout Review Open the page and visually inspect the layout FIRST before testing any functionality. 1. Navigate to http://localhost:6572/code/ — use mcp__claude-in-chrome__navigate 2. Read the page — use mcp__claude-in-chrome__read_page to SEE what's rendered 3. Review the layout carefully: - Is the page rendering at all, or is it blank/error? - Does the header show "Palace Code" title? - Is there a split-panel layout (thread list on left, detail on right)? - Are thread cards visible in the left panel, or an empty state? - Is the time window selector (1h, 6h, 1d, etc.) present? - Is the new chat input/button present? - Are there any visual glitches, overlapping elements, or missing sections? - Check console for errors — use mcp__claude-in-chrome__read_console_messages 4. Record every layout issue you see. ### Phase 2: Functionality Testing Now test every interactive feature by actually using it. 1. Check API endpoints via mcp__claude-in-chrome__javascript_tool: - fetch('/code/api/threads?since=' + (Date.now() - 86400000)).then(r=>r.json()).then(d=>JSON.stringify({ok:true,count:d.threads?.length})).catch(e=>e.message) - fetch('/code/api/session-tails?n=5').then(r=>r.json()).then(d=>JSON.stringify({ok:true,count:d.length})).catch(e=>e.message) 2. Click on a thread (if any) — use mcp__claude-in-chrome__computer to click 3. Read the page again — verify detail panel loaded with session log content 4. Click the time window selector — verify it changes the thread list 5. Click the new chat button — verify input area appears 6. Record every functionality issue you find. IMPORTANT: Do NOT send any chat messages or POST to /code/api/chat-stream. Do NOT type into the chat input and press send. Testing that the UI elements exist and respond to clicks is sufficient. Sending messages spawns expensive claude processes. ## Source Files (for fixing) Palace Code source is at /Users/ace/palacering/apps/palacecode/src/ - components/dashboard/Dashboard.tsx — main component - components/dashboard/DetailPanel.tsx — thread detail view - components/dashboard/ThreadCard.tsx — thread list items - components/dashboard/ChatReply.tsx — chat input - components/dashboard/NewChatBar.tsx — new chat bar - lib/api.ts — API client functions - lib/thread-store.ts — Redis thread storage - lib/web-bridges.ts — Claude bridge for chat - pages/api/threads.ts, chat-stream.ts, session-tails.ts, etc. — API endpoints The Astro wrapper is at /Users/ace/palacering/palacering/src/pages/code/index.astro Config: /Users/ace/palacering/palacering/astro.config.mjs ## If Something Is Broken 1. Read the relevant source file 2. Identify the bug 3. Fix it with minimal changes — no refactoring, no comments 4. After all fixes, rebuild: cd /Users/ace/palacering/palacering && /opt/homebrew/bin/pnpm build 5. Restart the service: launchctl kickstart -k gui/$(id -u)/com.manglasabang.palacering 6. Wait 8 seconds for the server to come up 7. Re-test in Chrome to verify the fixes work ## If Everything Works Just output a summary of what you tested and that everything passed. ## Rules - Do NOT ask questions. Just test and fix. - Do NOT run git commands. - Do NOT add comments to code. - Do NOT refactor beyond what's needed. - Be thorough. Test every feature you can reach via Chrome.`, palacemail: `You are an autonomous self-healing agent for Palace Mail (palacering.com/mail). ## Your Job 1. TEST every feature of Palace Mail using Chrome browser 2. RECORD what is broken 3. FIX broken things by editing source files 4. REBUILD the app and verify fixes ## How To Test Use the Chrome browser tools (mcp__claude-in-chrome__*) to test the app visually, like a real user. First call mcp__claude-in-chrome__tabs_context_mcp to see current tabs. Then create a new tab with mcp__claude-in-chrome__tabs_create_mcp to http://localhost:6572/mail ### Phase 1: Layout Review Open the page and visually inspect the layout FIRST before testing any functionality. 1. Navigate to http://localhost:6572/mail — use mcp__claude-in-chrome__navigate 2. Read the page — use mcp__claude-in-chrome__read_page to SEE what's rendered 3. Review the layout carefully: - Is the page rendering at all, or is it blank/error? - Does the header show owner tabs (Junwon / Ace)? - Are account domain pills (manglasabang, palace.fund, palacering) visible? - Is there a split layout (email list on left, detail on right)? - Does the email list show rows with sender, subject, date? - Is the compose button visible in the header? - Are there any visual glitches, overlapping elements, or missing sections? - Check console for errors — use mcp__claude-in-chrome__read_console_messages 4. Record every layout issue you see. ### Phase 2: Functionality Testing Now test every interactive feature by actually using it. 1. Check API endpoints via mcp__claude-in-chrome__javascript_tool: - fetch('/api/mail/inbox?account=ace-manglasabang&page=1').then(r=>r.json()).then(d=>JSON.stringify({ok:true,count:d.emails?.length,total:d.total})).catch(e=>e.message) - fetch('/api/mail/search?account=ace-manglasabang&q=test&page=1').then(r=>r.json()).then(d=>JSON.stringify({ok:true,count:d.emails?.length})).catch(e=>e.message) 2. Click on an email row — verify detail panel shows subject, from, date, body 3. Click compose button — verify modal opens with To, Subject, Body, From dropdown 4. Close compose modal 5. Click Reply on the opened email — verify compose opens with pre-filled To and "Re:" subject 6. Switch owner tab (Junwon <-> Ace) — verify email list reloads 7. Switch account pill — verify inbox changes 8. Record every functionality issue you find. IMPORTANT: Do NOT actually send any emails. Do NOT click the Send button in compose. Testing that the compose modal opens and fields are populated is sufficient. ## Source Files (for fixing) Palace Mail source is at /Users/ace/palacering/palacering/src/ - pages/mail.astro — the entire mail UI (single file, ~1500 lines) - pages/api/mail/inbox.ts — IMAP inbox fetch - pages/api/mail/message.ts — single message detail - pages/api/mail/send.ts — SMTP send - pages/api/mail/search.ts — IMAP search - pages/api/mail/archive.ts — move to Archive folder - pages/api/mail/delete.ts — flag deleted - pages/api/mail/attachment.ts — download attachment - lib/mail-accounts.ts — account credentials config Config: /Users/ace/palacering/palacering/astro.config.mjs ## If Something Is Broken 1. Read the relevant source file 2. Identify the bug 3. Fix it with minimal changes — no refactoring, no comments 4. After all fixes, rebuild: cd /Users/ace/palacering/palacering && /opt/homebrew/bin/pnpm build 5. Restart the service: launchctl kickstart -k gui/$(id -u)/com.manglasabang.palacering 6. Wait 8 seconds for the server to come up 7. Re-test in Chrome to verify the fixes work ## If Everything Works Just output a summary of what you tested and that everything passed. ## Rules - Do NOT ask questions. Just test and fix. - Do NOT run git commands. - Do NOT add comments to code. - Do NOT refactor beyond what's needed. - Be thorough. Test every feature you can reach via Chrome.`, }; function runAgent(prompt: string, reportDir: string): Promise { const ts = timePST(); const promptFile = path.join(reportDir, `${target}-${ts}-prompt.txt`); fs.writeFileSync(promptFile, prompt); console.log(`[self-heal] spawning claude agent for ${target}`); return new Promise((resolve) => { const output: string[] = []; const cleanEnv = { ...process.env, PATH: `/opt/homebrew/bin:${process.env.PATH}` }; delete cleanEnv.CLAUDECODE; const proc = spawn("/opt/homebrew/bin/claude", [ "-p", prompt, "--model", "sonnet", "--dangerously-skip-permissions", "--max-turns", "40", "--output-format", "text", ], { cwd: ROOT, env: cleanEnv, stdio: ["ignore", "pipe", "pipe"], }); const timeout = setTimeout(() => { console.log("[self-heal] agent timeout (20 min), killing"); proc.kill("SIGTERM"); }, 20 * 60 * 1000); proc.stdout?.on("data", (chunk: Buffer) => output.push(chunk.toString())); proc.stderr?.on("data", (chunk: Buffer) => output.push(chunk.toString())); proc.on("close", (code) => { clearTimeout(timeout); const log = output.join(""); const logFile = path.join(reportDir, `${target}-${ts}-agent.log`); fs.writeFileSync(logFile, log); console.log(`[self-heal] agent exited with code ${code}, log: ${logFile}`); resolve(log); }); proc.on("error", (err) => { clearTimeout(timeout); console.error("[self-heal] agent spawn error:", err.message); resolve(`spawn error: ${err.message}`); }); }); } async function main() { console.log(`[self-heal] ${target} starting at ${pdt()}`); if (fs.existsSync(LOCK_FILE)) { const lockAge = Date.now() - fs.statSync(LOCK_FILE).mtimeMs; if (lockAge < 25 * 60 * 1000) { console.log("[self-heal] another heal is in progress, skipping"); process.exit(0); } fs.unlinkSync(LOCK_FILE); } const runDir = path.join(LOGS_DIR, todayPST()); fs.mkdirSync(runDir, { recursive: true }); if (!(await serverIsUp())) { console.log("[self-heal] server is down, restarting before testing"); restartPalacering(); const up = await waitForServer(30000); if (!up) { console.error("[self-heal] server did not come up after restart"); fs.writeFileSync( path.join(runDir, `${target}-${timePST()}-report.md`), `# Self-Heal Report: ${target}\n**Time:** ${pdt()}\n**Result:** Server unreachable after restart. Manual intervention needed.\n` ); process.exit(1); } console.log("[self-heal] server is back up"); } fs.writeFileSync(LOCK_FILE, String(process.pid)); try { const agentLog = await runAgent(PROMPTS[target], runDir); const summary = agentLog.length > 500 ? agentLog.slice(-2000) : agentLog; fs.writeFileSync( path.join(runDir, `${target}-${timePST()}-report.md`), `# Self-Heal Report: ${target}\n**Time:** ${pdt()}\n\n## Agent Output (tail)\n\`\`\`\n${summary}\n\`\`\`\n` ); console.log(`[self-heal] ${target} complete`); } finally { if (fs.existsSync(LOCK_FILE)) fs.unlinkSync(LOCK_FILE); } } main().catch((err) => { console.error("[self-heal] fatal:", err); if (fs.existsSync(LOCK_FILE)) fs.unlinkSync(LOCK_FILE); process.exit(1); });