import { ClaudeBridge } from "../../channels/slack/bridge"; import * as fs from "fs"; import * as path from "path"; const MEMORY_DIR = path.resolve(__dirname, "../../../../palaces/manglasabang/secretariat/memory"); const DAILY_DIR = path.join(MEMORY_DIR, "last-one-week"); const STATE_FILE = path.join(MEMORY_DIR, "most-recent-reflections.json"); interface ReflectionState { "last-one-week": string | null; "last-one-month": string | null; "last-one-year": string | null; } const EMPTY_STATE: ReflectionState = { "last-one-week": null, "last-one-month": null, "last-one-year": null }; function readState(): ReflectionState { try { const data = JSON.parse(fs.readFileSync(STATE_FILE, "utf8")); return { ...EMPTY_STATE, ...data }; } catch { return { ...EMPTY_STATE }; } } function writeState(state: ReflectionState): void { fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2) + "\n"); } function today(): Date { return new Date( new Date().toLocaleDateString("en-CA", { timeZone: "America/Los_Angeles" }) ); } function fmt(d: Date): string { const y = String(d.getFullYear()).slice(2); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${y}${m}${day}`; } function isoDate(d: Date): string { return d.toISOString().split("T")[0]; } function addDays(d: Date, n: number): Date { const r = new Date(d); r.setDate(r.getDate() + n); return r; } /** * List daily note files matching YYYY-MM-DD.md pattern within a date range. */ function dailyFilesInRange(start: Date, end: Date): string[] { const files: string[] = []; const entries = fs.readdirSync(DAILY_DIR); for (const f of entries) { const match = f.match(/^(\d{4}-\d{2}-\d{2})(?:-.+)?\.md$/); if (!match) continue; const fileDate = new Date(match[1]); if (fileDate >= start && fileDate <= end) { files.push(path.join(DAILY_DIR, f)); } } return files.sort(); } interface CompilationTask { type: keyof ReflectionState | "compaction"; prompt: string; outputPath: string; sourceFiles?: string[]; } interface WordLimit { path: string; trigger: number; target: number; } const WORD_LIMITS: WordLimit[] = [ { path: "identity/JUNWON.md", trigger: 600, target: 300 }, { path: "identity/ACE.md", trigger: 3000, target: 1500 }, { path: "identity/TOOLS.md", trigger: 600, target: 300 }, { path: "identity/GUARDRAILS.md", trigger: 1000, target: 500 }, { path: "identity/TASK-MANAGEMENT.md", trigger: 1000, target: 500 }, { path: "identity/core-memories.md", trigger: 1000, target: 500 }, { path: "last-one-week/*.md", trigger: 300, target: 150 }, { path: "last-one-month/*.md", trigger: 400, target: 200 }, { path: "last-one-year/*.md", trigger: 100, target: 50 }, { path: "each-past-year/*.md", trigger: 500, target: 300 }, ]; function wordCount(filePath: string): number { return fs.readFileSync(filePath, "utf8").split(/\s+/).filter(Boolean).length; } function resolveMemoryFiles(pattern: string): string[] { const full = path.join(MEMORY_DIR, pattern); if (pattern.includes("*")) { const dir = path.dirname(full); if (!fs.existsSync(dir)) return []; return fs.readdirSync(dir) .filter(f => f.endsWith(".md")) .map(f => path.join(dir, f)) .sort(); } return fs.existsSync(full) ? [full] : []; } function allMemoryFiles(): string[] { const files: string[] = []; for (const limit of WORD_LIMITS) { files.push(...resolveMemoryFiles(limit.path)); } return files; } function checkCompaction(): CompilationTask[] { const tasks: CompilationTask[] = []; const allFiles = allMemoryFiles(); const contextList = allFiles.map(f => path.relative(MEMORY_DIR, f)).join(", "); for (const limit of WORD_LIMITS) { const files = resolveMemoryFiles(limit.path); for (const filePath of files) { const wc = wordCount(filePath); if (wc <= limit.trigger) continue; const relPath = path.relative(MEMORY_DIR, filePath); console.log(`[reflection] compaction triggered: ${relPath} is ${wc} words (trigger: ${limit.trigger})`); tasks.push({ type: "compaction", outputPath: filePath, prompt: `You are compacting a memory file that has grown too large. These files are loaded into an AI assistant's context at session start — they ARE Ace's memory. Every word matters. Wasted words mean Ace wakes up dumber. First, read ALL of these memory files to understand the full set: ${contextList} All files are in: ${MEMORY_DIR}/ Then rewrite this file: ${filePath} Current word count: ${wc}. Target: ${limit.target} words. Think of all loaded files as ONE document. Your job is to make THIS file's contribution to that document as useful as possible. Rules: - NEVER cut safety rules, anti-laziness rules, or hard rules Junwon established - NEVER cut active conventions or procedures that affect how Ace works today - Deduplicate across files. If the same info exists in another file that's also loaded, cut it here (or cut it there if it belongs here better). Don't repeat what another file already says. - Move before deleting. If a daily/weekly note has a lesson worth keeping permanently, check if it already exists in an identity file. If not, add it there before cutting it here. - Prioritize by usefulness to tomorrow's session. "What helps Ace do work?" beats "What happened historically?" Historical details belong in weekly/monthly compilations, not identity files. - Tighten language. Say the same thing in fewer words. Replace paragraphs with sentences. Replace sentences with phrases. If a rule can be one line, make it one line. - Cut: stale content, redundancy, verbose blow-by-blow narratives, architecture details that won't matter next week, things that no longer apply - Keep the same structure/headers where possible - The rewritten file must be self-contained and coherent — a new session reading it should understand everything without needing the old version - Write directly to the file. Do not explain what you did.`, }); } } return tasks; } async function runCompaction(task: CompilationTask): Promise { console.log(`[reflection] compacting ${path.basename(task.outputPath)}`); const bridge = new ClaudeBridge(); bridge.model = "sonnet"; bridge.channel = "reflection"; try { await bridge.send(task.prompt); const newWc = wordCount(task.outputPath); console.log(`[reflection] compacted ${path.basename(task.outputPath)} → ${newWc} words`); } finally { bridge.kill(); } } function checkWeekly(state: ReflectionState, now: Date): CompilationTask | null { if (now.getDay() !== 0) return null; // Sunday only const monday = addDays(now, -6); // Monday of the week ending today const saturday = addDays(now, -1); // Saturday // Skip if already compiled this week if (state["last-one-week"]) { const lastWeekly = new Date(state["last-one-week"]); if (lastWeekly >= monday) return null; } const sourceFiles = dailyFilesInRange(monday, saturday); if (sourceFiles.length === 0) return null; const outFile = `week-from-${fmt(monday)}-to-${fmt(saturday)}.md`; const outputPath = path.join(MEMORY_DIR, "last-one-month", outFile); const fileList = sourceFiles.map((f) => path.basename(f)).join(", "); return { type: "last-one-week", outputPath, sourceFiles, prompt: `You are compiling a weekly reflection. Read these daily memory files and postmortems from ${isoDate(monday)} to ${isoDate(saturday)}: ${fileList} Read each file in secretariat/memory/last-one-week/. Then write a single synthesis document (max 500 tokens) to: ${outputPath} The document should capture: - Key decisions made and their rationale - Tasks completed and outcomes - Patterns observed and lessons worth remembering Write concisely and precisely. Distill aggressively — only the most important things survive. Start the file with a header: "# Week of ${isoDate(monday)} to ${isoDate(saturday)}"`, }; } function checkMonthly(state: ReflectionState, now: Date): CompilationTask | null { if (now.getDate() !== 1) return null; // 1st of month only const prevMonth = new Date(now); prevMonth.setMonth(prevMonth.getMonth() - 1); const prevYear = prevMonth.getFullYear(); const prevMon = prevMonth.getMonth(); // 0-indexed // Skip if already compiled for prev month if (state["last-one-month"]) { const lastMonthly = new Date(state["last-one-month"]); if (lastMonthly.getFullYear() === prevYear && lastMonthly.getMonth() === prevMon) { return null; } } // Find weekly files whose end date falls in the previous month const weeklyDir = path.join(MEMORY_DIR, "last-one-month"); if (!fs.existsSync(weeklyDir)) return null; const weeklyFiles: string[] = []; for (const f of fs.readdirSync(weeklyDir)) { const match = f.match(/^week-from-(\d{6})-to-(\d{6})\.md$/); if (!match) continue; // Parse end date (YYMMDD) const endYY = parseInt(match[2].slice(0, 2)) + 2000; const endMM = parseInt(match[2].slice(2, 4)) - 1; // 0-indexed if (endYY === prevYear && endMM === prevMon) { weeklyFiles.push(path.join(weeklyDir, f)); } } if (weeklyFiles.length === 0) return null; const firstDay = new Date(prevYear, prevMon, 1); const lastDay = new Date(prevYear, prevMon + 1, 0); const outFile = `month-from-${fmt(firstDay)}-to-${fmt(lastDay)}.md`; const outputPath = path.join(MEMORY_DIR, "last-one-year", outFile); const fileList = weeklyFiles.map((f) => path.basename(f)).join(", "); const monthName = firstDay.toLocaleDateString("en-US", { month: "long", year: "numeric" }); return { type: "last-one-month", outputPath, sourceFiles: weeklyFiles, prompt: `You are compiling a monthly reflection for ${monthName}. Read these weekly compilation files: ${fileList} Read each file in secretariat/memory/last-one-month/. Then write a single synthesis document (max 2,000 words) to: ${outputPath} The document should capture: - Major projects and their progress/completion - Recurring themes and patterns across weeks - Strategic decisions and their outcomes - Significant lessons that emerged over the month - Anything that should propagate to core identity/tools docs Write at a higher level than weekly compilations. Focus on the arc of the month. Start the file with a header: "# ${monthName}"`, }; } function checkAnnual(state: ReflectionState, now: Date): CompilationTask | null { if (now.getMonth() !== 0 || now.getDate() !== 1) return null; // Jan 1 only const prevYear = now.getFullYear() - 1; if (state["last-one-year"]) { const lastAnnual = new Date(state["last-one-year"]); if (lastAnnual.getFullYear() === prevYear) return null; } const monthlyDir = path.join(MEMORY_DIR, "last-one-year"); if (!fs.existsSync(monthlyDir)) return null; const monthlyFiles: string[] = []; for (const f of fs.readdirSync(monthlyDir)) { const match = f.match(/^month-from-(\d{6})-to-(\d{6})\.md$/); if (!match) continue; const startYY = parseInt(match[1].slice(0, 2)) + 2000; if (startYY === prevYear) { monthlyFiles.push(path.join(monthlyDir, f)); } } if (monthlyFiles.length === 0) return null; const outputPath = path.join(MEMORY_DIR, "each-past-year", `${prevYear}.md`); const fileList = monthlyFiles.map((f) => path.basename(f)).join(", "); return { type: "last-one-year", outputPath, sourceFiles: monthlyFiles, prompt: `You are compiling an annual reflection for ${prevYear}. Read these monthly compilation files: ${fileList} Read each file in secretariat/memory/last-one-year/. Then write a single synthesis document (max 2,000 words) to: ${outputPath} The document should capture: - Major milestones and achievements - Strategic shifts and pivots - Growth patterns — what improved, what regressed - Persistent lessons that define how we operate - Anything that should propagate to core identity/tools docs Write at the highest level. This is the year-in-review. Start the file with a header: "# ${prevYear} — Year in Review"`, }; } const IDENTITY_DIR = path.join(MEMORY_DIR, "identity"); function cleanupSourceFiles(files: string[]): void { for (const f of files) { try { fs.unlinkSync(f); console.log(`[reflection] cleaned up ${path.basename(f)}`); } catch (err) { console.error(`[reflection] failed to clean up ${path.basename(f)}:`, err); } } } async function runCompilation(task: CompilationTask): Promise { console.log(`[reflection] running ${task.type} compilation → ${path.basename(task.outputPath)}`); const bridge = new ClaudeBridge(); bridge.model = "opus"; bridge.channel = "reflection"; try { const result = await bridge.send(task.prompt); console.log(`[reflection] ${task.type} done, result: ${result.length} chars`); } finally { bridge.kill(); } await propagateToIdentity(task); if (task.type === "last-one-week" && task.sourceFiles) { cleanupSourceFiles(task.sourceFiles); } if (task.type === "last-one-month" && task.sourceFiles) { cleanupSourceFiles(task.sourceFiles); } if (task.type === "last-one-year" && task.sourceFiles) { cleanupSourceFiles(task.sourceFiles); } } async function propagateToIdentity(task: CompilationTask): Promise { if (!fs.existsSync(task.outputPath)) return; console.log(`[reflection] propagating ${task.type} to identity`); const compilationFile = path.basename(task.outputPath); const compilationDir = path.dirname(task.outputPath); const bridge = new ClaudeBridge(); bridge.model = "sonnet"; bridge.channel = "reflection"; try { await bridge.send(`You just compiled a ${task.type} reflection. Now propagate insights to identity files. Read the compilation: ${compilationDir}/${compilationFile} Then update these files in ${IDENTITY_DIR}/: 1. **core-memories.md** — Add any moments that made Junwon happy. Wins, progress, good moments, things that went well. Core memories are ONLY for joy. No debugging, no failures. 2. **ACE.md, TOOLS.md, GUARDRAILS.md, TASK-MANAGEMENT.md** — Update with operational lessons, patterns, or conventions worth making permanent. Only add things that change how we work going forward. Don't add transient info. Rules: - Read each file before editing. Don't duplicate what's already there. - Be concise. A few lines per update, not paragraphs. - If nothing is worth propagating, don't force it. Skip silently.`); console.log(`[reflection] ${task.type} propagation done`); } finally { bridge.kill(); } } /** * Main entry point. Called by heartbeat or directly. * Checks all three tiers and runs any compilations that are due. * Returns list of compilation types that were run. */ export async function runReflection(): Promise { const now = today(); const state = readState(); const ran: string[] = []; console.log(`[reflection] checking (${isoDate(now)}, day=${now.getDay()}, date=${now.getDate()})`); console.log(`[reflection] state: last-one-week=${state["last-one-week"]}, last-one-month=${state["last-one-month"]}, last-one-year=${state["last-one-year"]}`); // Check each tier in order: weekly → monthly → annual const tasks: CompilationTask[] = []; const weekly = checkWeekly(state, now); if (weekly) tasks.push(weekly); const monthly = checkMonthly(state, now); if (monthly) tasks.push(monthly); const annual = checkAnnual(state, now); if (annual) tasks.push(annual); if (tasks.length === 0) { console.log("[reflection] nothing due"); return ran; } for (const task of tasks) { await runCompilation(task); state[task.type] = isoDate(now); writeState(state); ran.push(task.type); console.log(`[reflection] ${task.type} state updated to ${isoDate(now)}`); } // Per-file compaction — runs after temporal compilations const compactionTasks = checkCompaction(); for (const task of compactionTasks) { await runCompaction(task); ran.push(`compaction:${path.basename(task.outputPath)}`); } return ran; } // Allow direct execution if (require.main === module) { runReflection() .then((ran) => { if (ran.length === 0) { console.log("[reflection] no compilations needed today"); } else { console.log(`[reflection] completed: ${ran.join(", ")}`); } }) .catch((err) => { console.error("[reflection] fatal:", err); process.exit(1); }); }