import { mkdir, readFile, writeFile, readdir, rm, appendFile } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { join, resolve } from 'node:path'; export const RUNS_REL = '.boo/runs'; export const DAILY_REL = '.boo/runs/daily'; export const GUIDELINES_REL = '.boo/guidelines'; export interface SessionJson { session_id: string; task: string; start_time: string; end_time?: string; status: 'in_progress' | 'completed'; expected_record_types: string[]; } export interface AuditTrailEntry { timestamp: string; record_type: string; action_type: string; tool?: string; files?: string[]; detail?: string; input?: string; output?: string; } export interface IndexEntry { id: string; task: string; status: string; record_count: number; start_time: string; max_anomaly_level?: string; } export interface IndexJson { entries: IndexEntry[]; } export interface StartSessionResult { sessionId: string; contextSummary: { recentActivity: IndexEntry[]; userCorrections: UserCorrectionRecord[]; unfinishedSessions: SessionJson[]; }; } export interface EndSessionResult { sessionId: string; integrity: IntegrityCheck[]; correctionCount: number; summaryPath: string; } export interface IntegrityCheck { check: string; passed: boolean; detail?: string; } export interface RecoverResult { level: number; sessionId?: string; task?: string; recentActivity: IndexEntry[]; lastTrailEntries: AuditTrailEntry[]; userCorrections: UserCorrectionRecord[]; conclusions: string[]; dailyAnomalies: string[]; dailyBacklog: string[]; fullTrail?: AuditTrailEntry[]; anomalies?: string[]; } export interface DailyReport { date: string; sections: { taskOverview: string; operationStats: { label: string; count: number }[]; changes: { time: string; target: string; detail: string }[]; userFeedback: { feedback: string; resolution: string; persistedTo: string }[]; anomalyAlerts: string[]; backlogTracking: string[]; integritySummary: string; }; path: string; } export interface UserCorrectionRecord { record_type: 'conversation'; action_type: 'user_correction'; priority: 'critical_for_recovery'; timestamp: string; original_claim: string; correction: string; principle_extracted: string; persisted_to: string[]; } function runsDir(basePath?: string): string { return resolve(basePath ?? process.cwd(), RUNS_REL); } function dailyDir(basePath?: string): string { return resolve(basePath ?? process.cwd(), DAILY_REL); } function sessionDir(sessionId: string, basePath?: string): string { return join(runsDir(basePath), sessionId); } function currentSessionPath(basePath?: string): string { return join(runsDir(basePath), '.current_session'); } function indexJsonPath(basePath?: string): string { return join(runsDir(basePath), 'index.json'); } function auditBufferPath(basePath?: string): string { return join(runsDir(basePath), 'audit_buffer.jsonl'); } function auditPendingPath(basePath?: string): string { return join(runsDir(basePath), 'audit_pending.jsonl'); } function trailPath(sessionId: string, basePath?: string): string { return join(sessionDir(sessionId, basePath), 'audit_trail.jsonl'); } function sessionJsonPath(sessionId: string, basePath?: string): string { return join(sessionDir(sessionId, basePath), 'session.json'); } function summaryPath(sessionId: string, basePath?: string): string { return join(sessionDir(sessionId, basePath), 'session_summary.md'); } export function generateSessionId(): string { const now = new Date(); const y = now.getFullYear(); const m = String(now.getMonth() + 1).padStart(2, '0'); const d = String(now.getDate()).padStart(2, '0'); const hh = String(now.getHours()).padStart(2, '0'); const mm = String(now.getMinutes()).padStart(2, '0'); return `adhoc_${y}${m}${d}_${hh}${mm}`; } function isoNow(): string { return new Date().toISOString(); } function isoDate(d?: Date): string { const dt = d ?? new Date(); return `${dt.getFullYear()}${String(dt.getMonth() + 1).padStart(2, '0')}${String(dt.getDate()).padStart(2, '0')}`; } function isTodayIso(iso: string): boolean { return iso.startsWith(new Date().toISOString().slice(0, 10)); } function tryParseJson(raw: string): T | null { try { return JSON.parse(raw) as T; } catch { return null; } } async function ensureDir(p: string): Promise { if (!existsSync(p)) { await mkdir(p, { recursive: true }); } } async function readLines(p: string): Promise { try { const content = await readFile(p, 'utf-8'); return content.split('\n').filter(Boolean); } catch { return []; } } async function readJsonFile(p: string): Promise { try { const raw = await readFile(p, 'utf-8'); return tryParseJson(raw); } catch { return null; } } function appendLine(p: string, line: string): Promise { return appendFile(p, line + '\n', 'utf-8'); } async function clearFile(p: string): Promise { try { await writeFile(p, '', 'utf-8'); } catch { // File may not exist } } export async function getCurrentSession(basePath?: string): Promise { try { const raw = await readFile(currentSessionPath(basePath), 'utf-8'); return raw.trim(); } catch { return null; } } export async function getSessionJson(sessionId: string, basePath?: string): Promise { return readJsonFile(sessionJsonPath(sessionId, basePath)); } export async function getIndex(basePath?: string): Promise { return readJsonFile(indexJsonPath(basePath)); } async function writeIndex(entries: IndexEntry[], basePath?: string): Promise { await ensureDir(runsDir(basePath)); await writeFile(indexJsonPath(basePath), JSON.stringify({ entries }, null, 2), 'utf-8'); } async function appendIndex(sessionId: string, task: string, basePath?: string): Promise { const existing = await getIndex(basePath); const entry: IndexEntry = { id: sessionId, task, status: 'in_progress', record_count: 0, start_time: isoNow(), }; const entries = [entry, ...(existing?.entries ?? [])].slice(0, 100); await writeIndex(entries, basePath); } async function updateIndexStatus(sessionId: string, status: string, basePath?: string): Promise { const idx = await getIndex(basePath); if (!idx) return; for (const e of idx.entries) { if (e.id === sessionId) { e.status = status; } } await writeIndex(idx.entries, basePath); } export async function startSession(task: string, basePath?: string): Promise { const sessionId = generateSessionId(); const sDir = sessionDir(sessionId, basePath); await ensureDir(sDir); const session: SessionJson = { session_id: sessionId, task, start_time: isoNow(), status: 'in_progress', expected_record_types: ['data', 'change', 'conversation'], }; await writeFile(sessionJsonPath(sessionId, basePath), JSON.stringify(session, null, 2), 'utf-8'); await writeFile(currentSessionPath(basePath), sessionId, 'utf-8'); await appendIndex(sessionId, task, basePath); // L0 context recovery const idx = await getIndex(basePath); const recentActivity = idx?.entries.slice(0, 5) ?? []; // L2 user correction scan const allCorrections = await scanAllTrailsForCorrections(basePath); // Check for unfinished sessions const unfinishedSessions = await findUnfinishedSessions(basePath); return { sessionId, contextSummary: { recentActivity, userCorrections: allCorrections, unfinishedSessions, }, }; } async function findUnfinishedSessions(basePath?: string): Promise { const rDir = runsDir(basePath); if (!existsSync(rDir)) return []; const entries = await readdir(rDir, { withFileTypes: true }); const unfinished: SessionJson[] = []; for (const entry of entries) { if (!entry.isDirectory()) continue; const sess = await getSessionJson(entry.name, basePath); if (sess && sess.status === 'in_progress') { unfinished.push(sess); } } return unfinished; } async function scanAllTrailsForCorrections(basePath?: string): Promise { const rDir = runsDir(basePath); if (!existsSync(rDir)) return []; const entries = await readdir(rDir, { withFileTypes: true }); const corrections: UserCorrectionRecord[] = []; for (const entry of entries) { if (!entry.isDirectory()) continue; const lines = await readLines(trailPath(entry.name, basePath)); for (const line of lines) { const record = tryParseJson(line); if (record?.action_type === 'user_correction') { corrections.push(record); } } } // Also scan audit_pending.jsonl const pendingLines = await readLines(auditPendingPath(basePath)); for (const line of pendingLines) { const record = tryParseJson(line); if (record?.action_type === 'user_correction') { corrections.push(record); } } return corrections; } export async function endSession(basePath?: string): Promise { const sessionId = await getCurrentSession(basePath); if (!sessionId) return null; const sDir = sessionDir(sessionId, basePath); await ensureDir(sDir); // Collect remaining buffer data const bufferLines = await readLines(auditBufferPath(basePath)); const pendingLines = await readLines(auditPendingPath(basePath)); const allRemaining = [...bufferLines, ...pendingLines]; // Append to audit_trail.jsonl const trail = trailPath(sessionId, basePath); if (allRemaining.length > 0) { await appendFile(trail, allRemaining.join('\n') + '\n', 'utf-8'); } // Clear buffer files await clearFile(auditBufferPath(basePath)); await clearFile(auditPendingPath(basePath)); // Read current trail for stats const trailLines = await readLines(trail); // Extract user_correction records const corrections: UserCorrectionRecord[] = []; for (const line of trailLines) { const record = tryParseJson(line); if (record?.action_type === 'user_correction') { corrections.push(record); } } // Integrity checks const integrity: IntegrityCheck[] = [ { check: 'Audit records exist', passed: trailLines.length > 0, detail: trailLines.length > 0 ? `${trailLines.length} records` : 'No audit records found', }, { check: 'File modifications tracked', passed: trailLines.some((l) => { const r = tryParseJson(l); return r && (r.tool === 'Write' || r.tool === 'Edit'); }), detail: 'Checking for Write/Edit tool entries', }, { check: 'User corrections persisted', passed: corrections.every((c) => (c.persisted_to?.length ?? 0) > 0), detail: corrections.length > 0 ? `${corrections.length} corrections found, ${corrections.filter((c) => (c.persisted_to?.length ?? 0) > 0).length} persisted` : 'No corrections to persist', }, ]; // Generate session summary const summaryContent = generateSessionSummary(sessionId, trailLines, corrections); const summaryFile = summaryPath(sessionId, basePath); await writeFile(summaryFile, summaryContent, 'utf-8'); // Update session.json const session = await getSessionJson(sessionId, basePath); if (session) { session.status = 'completed'; session.end_time = isoNow(); await writeFile(sessionJsonPath(sessionId, basePath), JSON.stringify(session, null, 2), 'utf-8'); await updateIndexStatus(sessionId, 'completed', basePath); } // Update index.json record count const idx = await getIndex(basePath); if (idx) { for (const e of idx.entries) { if (e.id === sessionId) { e.record_count = trailLines.length; e.status = 'completed'; } } await writeIndex(idx.entries, basePath); } // Clear .current_session try { await rm(currentSessionPath(basePath)); } catch { // Ok if already gone } return { sessionId, integrity, correctionCount: corrections.length, summaryPath: summaryFile, }; } function generateSessionSummary( sessionId: string, trailLines: string[], corrections: UserCorrectionRecord[], ): string { const actions: string[] = []; const outputs: string[] = []; for (const line of trailLines) { const record = tryParseJson(line); if (record) { if (record.action_type) actions.push(record.action_type); if (record.output) outputs.push(record.output); } } return [ `# Session Summary | ${sessionId}`, '', `## Time: ${isoNow()}`, `## Status: completed`, '', '## Completed work', ...actions.map((a) => `- ${a}`), '', '## Key conclusions', ...outputs.map((o) => `- ${o}`), '', '## User corrections', ...(corrections.length > 0 ? corrections.map((c) => `- ${c.original_claim} → ${c.correction} (${c.principle_extracted})`) : ['- None']), '', ].join('\n'); } export async function recoverSession( level: number, specificSessionId?: string, basePath?: string, ): Promise { const result: RecoverResult = { level, recentActivity: [], lastTrailEntries: [], userCorrections: [], conclusions: [], dailyAnomalies: [], dailyBacklog: [] }; // L0: index summary const idx = await getIndex(basePath); result.recentActivity = idx?.entries.slice(0, 5) ?? []; if (level === 0) return result; // L1: current session + last 3 trail entries let activeSessionId = specificSessionId ?? await getCurrentSession(basePath); if (activeSessionId) { result.sessionId = activeSessionId; const session = await getSessionJson(activeSessionId, basePath); if (session) { result.task = session.task; } const trailLines = await readLines(trailPath(activeSessionId, basePath)); result.lastTrailEntries = trailLines.slice(-3).map((l) => { const r = tryParseJson(l); return r ?? { timestamp: '', record_type: '', action_type: '', input: l, output: l }; }); } if (level === 1) return result; // L2: user corrections + conclusions + daily anomalies result.userCorrections = await scanAllTrailsForCorrections(basePath); // Extract conclusions from trail entries const allTrailLines = await readLines(trailPath(activeSessionId ?? '', basePath)); for (const line of allTrailLines) { const record = tryParseJson(line); if (record?.output) { result.conclusions.push(record.output); } } // Read daily reports for anomalies + backlog const dDir = dailyDir(basePath); if (existsSync(dDir)) { const dailyFiles = (await readdir(dDir)).filter((f) => f.endsWith('_daily.md')).sort().reverse(); if (dailyFiles.length > 0) { const latest = await readFile(join(dDir, dailyFiles[0]!), 'utf-8'); const anomalies = latest.match(/## (?:四|4).*?[\s\S]*?(?=##|$)/); if (anomalies) result.dailyAnomalies.push(anomalies[0]); const backlog = latest.match(/## (?:六|6).*?[\s\S]*?(?=##|$)/); if (backlog) result.dailyBacklog.push(backlog[0]); } } if (level === 2) return result; // L3: full trail + pending if (level >= 3) { if (activeSessionId) { const fullLines = await readLines(trailPath(activeSessionId, basePath)); result.fullTrail = fullLines.map((l) => { const r = tryParseJson(l); return r ?? { timestamp: '', record_type: '', action_type: '', input: l, output: l }; }); } } return result; } export async function generateDailyReport( targetDate?: string, review?: boolean, basePath?: string, ): Promise { const date = targetDate ?? isoDate(); const idx = await getIndex(basePath); const rDir = runsDir(basePath); const todayEntries = (idx?.entries ?? []).filter((e) => e.start_time.startsWith(date.slice(0, 4) + '-' + date.slice(4, 6) + '-' + date.slice(6, 8))); let totalWriteEdit = 0; let totalBash = 0; let totalAuditBlocks = 0; const changes: { time: string; target: string; detail: string }[] = []; const feedback: { feedback: string; resolution: string; persistedTo: string }[] = []; const anomalies: string[] = []; for (const entry of todayEntries) { const lines = await readLines(trailPath(entry.id, basePath)); for (const line of lines) { const record = tryParseJson(line); if (!record) continue; if (record.tool === 'Write' || record.tool === 'Edit') totalWriteEdit++; if (record.tool === 'Bash') totalBash++; if (record.action_type === 'audit_block') totalAuditBlocks++; if (record.tool && (record.tool === 'Write' || record.tool === 'Edit') && record.files) { changes.push({ time: record.timestamp, target: record.files.join(', '), detail: record.detail ?? '' }); } if (record.action_type === 'user_correction') { const uc = record as unknown as UserCorrectionRecord; feedback.push({ feedback: uc.original_claim, resolution: uc.correction, persistedTo: (uc.persisted_to ?? []).join(', ') }); } } } // Check for anomalies.json if (existsSync(rDir)) { const sessionDirs = await readdir(rDir, { withFileTypes: true }); for (const d of sessionDirs) { if (!d.isDirectory()) continue; const anomPath = join(rDir, d.name, 'anomalies.json'); if (existsSync(anomPath)) { const anomContent = await readFile(anomPath, 'utf-8'); anomalies.push(`[${d.name}] ${anomContent.slice(0, 200)}`); } } } // Read previous day backlog const prevDate = isoDate(new Date(Date.now() - 86400000)); let backlog: string[] = []; const prevDailyPath = join(dailyDir(basePath), `${prevDate}_daily.md`); if (existsSync(prevDailyPath)) { const prevContent = await readFile(prevDailyPath, 'utf-8'); const m = prevContent.match(/## (?:六|6|明日待办)[\s\S]*?(?=##|$)/); if (m) backlog = m[0].split('\n').filter((l) => l.trim().startsWith('-')).map((l) => l.replace(/^-\s*/, '')); } const reportPath = join(dailyDir(basePath), `${date}_daily.md`); await ensureDir(dailyDir(basePath)); const sections = { taskOverview: todayEntries.length > 0 ? todayEntries.map((e) => `| ${e.id} | ${e.task} | ${e.status} | ${e.record_count} |`).join('\n') : 'No activity', operationStats: [ { label: 'Write/Edit operations', count: totalWriteEdit }, { label: 'Bash executions', count: totalBash }, { label: 'Audit blocks', count: totalAuditBlocks }, ], changes, userFeedback: feedback, anomalyAlerts: anomalies, backlogTracking: backlog, integritySummary: [ `| All sessions have audit records | ${todayEntries.every((e) => e.record_count > 0) ? '✅' : '⚠️'} |`, `| Audit blocks persisted | ${totalAuditBlocks > 0 ? '✅' : '⚠️'} |`, `| User corrections persisted | ${feedback.every((f) => f.persistedTo.length > 0) ? '✅' : '⚠️'} |`, ].join('\n'), }; const reportContent = generateDailyReportContent(date, sections); await writeFile(reportPath, reportContent, 'utf-8'); // If review mode, also generate morning review if (review) { const reviewPath = join(dailyDir(basePath), `${date}_morning_review.md`); const reviewContent = generateMorningReview(sections, date); await writeFile(reviewPath, reviewContent, 'utf-8'); } return { date, sections, path: reportPath }; } function generateDailyReportContent(date: string, sections: DailyReport['sections']): string { return [ `# Work Report | ${date}`, '', `> Auto-generated: ${isoNow()}`, `> Data source: .boo/runs/index.json + session audit_trail`, `> Coverage: ${date.slice(0, 4)}-${date.slice(4, 6)}-${date.slice(6, 8)} 00:00 — 23:59`, '', '---', '', '## I. Task Overview', '', '| Session ID | Task | Status | Records |', '|-----------|------|--------|---------|', sections.taskOverview, '', '---', '', '## II. Operation Stats', '', '| Metric | Count |', '|--------|-------|', ...sections.operationStats.map((s) => `| ${s.label} | ${s.count} |`), '', '---', '', '## III. Change Records', '', ...(sections.changes.length > 0 ? ['| Time | Target | Detail |', '|------|--------|--------|', ...sections.changes.map((c) => `| ${c.time} | ${c.target} | ${c.detail} |`)] : ['No changes recorded today.']), '', '---', '', '## IV. User Feedback & Corrections', '', ...(sections.userFeedback.length > 0 ? ['| Feedback | Resolution | Persisted To |', '|---------|------------|--------------|', ...sections.userFeedback.map((f) => `| ${f.feedback} | ${f.resolution} | ${f.persistedTo} |`)] : ['None.']), '', '---', '', '## V. Anomaly Alerts', '', ...(sections.anomalyAlerts.length > 0 ? sections.anomalyAlerts.map((a) => `- ${a}`) : ['None.']), '', '---', '', '## VI. Backlog Tracking', '', ...(sections.backlogTracking.length > 0 ? sections.backlogTracking.map((b) => `- ${b}`) : ['None.']), '', '---', '', '## VII. Integrity Summary', '', '| Check | Result |', '|-------|--------|', sections.integritySummary, '', ].join('\n'); } function generateMorningReview(sections: DailyReport['sections'], date: string): string { const anomalies = sections.anomalyAlerts; const hasUnhandledAnomalies = anomalies.some((a) => !a.includes('resolved')); const hasUnpersistedFeedback = sections.userFeedback.some((f) => !f.persistedTo); const hasIncompleteBacklog = sections.backlogTracking.length > 0; return [ `# Morning Self-Review | ${date}`, '', `> Generated: ${isoNow()}`, '', '## Self-Correction Check', '', `- Unresolved anomalies: ${hasUnhandledAnomalies ? '⚠️ Yes — needs attention' : '✅ None'}`, `- Unpersisted user feedback: ${hasUnpersistedFeedback ? '⚠️ Needs documentation' : '✅ All persisted'}`, `- Outstanding backlog: ${hasIncompleteBacklog ? '⚠️ Carry-over items' : '✅ Clean slate'}`, '', '## Today\'s Recommended Priorities', '', ...(sections.backlogTracking.length > 0 ? sections.backlogTracking.map((b) => `- [ ] ${b} (carry-over)`) : []), '- [ ] Review yesterday\'s user feedback and persist any remaining corrections', '- [ ] Continue highest-priority task from session overview', '', ].join('\n'); } export async function ensureBooDirs(basePath?: string): Promise { await ensureDir(runsDir(basePath)); await ensureDir(dailyDir(basePath)); } export async function writeAuditBuffer(entry: AuditTrailEntry, basePath?: string): Promise { await ensureDir(runsDir(basePath)); await appendLine(auditBufferPath(basePath), JSON.stringify(entry)); } export async function writeAuditPending(entry: AuditTrailEntry, basePath?: string): Promise { await ensureDir(runsDir(basePath)); await appendLine(auditPendingPath(basePath), JSON.stringify(entry)); }