import fs from "node:fs"; import path from "node:path"; const MAX_CONTEXT_CHARS = 3000; /** * Read critical sections from workspace AGENTS.md for post-compaction injection. * Returns formatted system event text, or null if no AGENTS.md or no relevant sections. */ export async function readPostCompactionContext(workspaceDir: string): Promise { const agentsPath = path.join(workspaceDir, "AGENTS.md"); try { if (!fs.existsSync(agentsPath)) { return null; } const content = await fs.promises.readFile(agentsPath, "utf-8"); // Extract "## Session Startup" and "## Red Lines" sections // Each section ends at the next "## " heading or end of file const sections = extractSections(content, ["Session Startup", "Red Lines"]); if (sections.length === 0) { return null; } const combined = sections.join("\n\n"); const safeContent = combined.length > MAX_CONTEXT_CHARS ? combined.slice(0, MAX_CONTEXT_CHARS) + "\n...[truncated]..." : combined; return ( "[Post-compaction context refresh]\n\n" + "Session was just compacted. The conversation summary above is a hint, NOT a substitute for your startup sequence. " + "Execute your Session Startup sequence now — read the required files before responding to the user.\n\n" + "Critical rules from AGENTS.md:\n\n" + safeContent ); } catch { return null; } } /** * Extract named sections from markdown content. * Matches H2 (##) or H3 (###) headings case-insensitively. * Skips content inside fenced code blocks. * Captures until the next heading of same or higher level, or end of string. */ export function extractSections(content: string, sectionNames: string[]): string[] { const results: string[] = []; const lines = content.split("\n"); for (const name of sectionNames) { let sectionLines: string[] = []; let inSection = false; let sectionLevel = 0; let inCodeBlock = false; for (const line of lines) { // Track fenced code blocks if (line.trimStart().startsWith("```")) { inCodeBlock = !inCodeBlock; if (inSection) { sectionLines.push(line); } continue; } // Skip heading detection inside code blocks if (inCodeBlock) { if (inSection) { sectionLines.push(line); } continue; } // Check if this line is a heading const headingMatch = line.match(/^(#{2,3})\s+(.+?)\s*$/); if (headingMatch) { const level = headingMatch[1].length; // 2 or 3 const headingText = headingMatch[2]; if (!inSection) { // Check if this is our target section (case-insensitive) if (headingText.toLowerCase() === name.toLowerCase()) { inSection = true; sectionLevel = level; sectionLines = [line]; continue; } } else { // We're in section — stop if we hit a heading of same or higher level if (level <= sectionLevel) { break; } // Lower-level heading (e.g., ### inside ##) — include it sectionLines.push(line); continue; } } if (inSection) { sectionLines.push(line); } } if (sectionLines.length > 0) { results.push(sectionLines.join("\n").trim()); } } return results; }