#!/usr/bin/env node /** * Daily Self-Check Script * * Purpose: Quick daily review of yesterday's self-evaluations * Runs: Daily at 06:00 * * Features: * 1. Review yesterday's self-review entries * 2. Compare with previous 3 days * 3. Detect immediate repetition (< 3 days) * 4. Send instant alert if pattern repeats * * Difference from detect-patterns.js: * - detect-patterns.js: Deep analysis, 7-day scan, 3+ threshold * - daily-self-check.js: Quick check, 4-day window, instant feedback */ const fs = require('fs'); const path = require('path'); const https = require('https'); // ============================================================================ // Configuration // ============================================================================ const CONFIG = { // Paths MEMORY_DIR: path.join(process.env.HOME, 'openclaw', 'memory'), // Check window DAYS_TO_CHECK: 4, // Yesterday + previous 3 days // Similarity SIMILARITY_THRESHOLD: 0.65, // Slightly higher for faster detection // Discord DISCORD_WEBHOOK: 'https://discord.com/api/webhooks/1469274752517537960/8qaedz2GgFICpNGME316opmDNuiMrw72ZHPGSr83iCOi-H_uvr6h5V-EKGqjQ_AjrAPh', }; // ============================================================================ // Utilities (imported from detect-patterns.js) // ============================================================================ function getLastNDays(n) { const dates = []; for (let i = 0; i < n; i++) { const date = new Date(); date.setDate(date.getDate() - i); dates.push(date.toISOString().split('T')[0]); } return dates; } function extractFailures(content) { const failures = []; const regex = /\*\*이번 실패\/미흡\*\*[^\n]*\n([\s\S]*?)(?=\n\s*│\s*\*\*|╰|$)/g; let match; while ((match = regex.exec(content)) !== null) { const section = match[1]; const bullets = section.match(/│?\s*[•\-\*]\s*(.+)/g); if (bullets) { bullets.forEach(bullet => { const text = bullet.replace(/│?\s*[•\-\*]\s*/, '').trim(); if (text && text !== '[구체적 사항]' && text.length > 5) { failures.push(text); } }); } } return failures; } function extractKeywords(text) { const stopwords = ['이', '그', '저', '것', '수', '등', '및', '를', '을', '가', '이', '은', '는', '의', '에', '와', '과']; const words = text .toLowerCase() .replace(/[^\w가-힣\s]/g, ' ') .split(/\s+/) .filter(w => w.length > 1 && !stopwords.includes(w)); return [...new Set(words)]; } function calculateSimilarity(text1, text2) { const keywords1 = new Set(extractKeywords(text1)); const keywords2 = new Set(extractKeywords(text2)); const intersection = new Set([...keywords1].filter(k => keywords2.has(k))); const union = new Set([...keywords1, ...keywords2]); return union.size > 0 ? intersection.size / union.size : 0; } async function sendDiscordAlert(message) { const data = JSON.stringify(message); const url = new URL(CONFIG.DISCORD_WEBHOOK); const options = { hostname: url.hostname, path: url.pathname + url.search, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': data.length } }; return new Promise((resolve, reject) => { const req = https.request(options, (res) => { if (res.statusCode === 204) { resolve(); } else { reject(new Error(`Discord API returned ${res.statusCode}`)); } }); req.on('error', reject); req.write(data); req.end(); }); } // ============================================================================ // Main Logic // ============================================================================ async function main() { console.log('🌅 Daily Self-Check'); console.log('===================\n'); const dates = getLastNDays(CONFIG.DAYS_TO_CHECK); const yesterday = dates[0]; console.log(`Yesterday: ${yesterday}`); console.log(`Comparing with: ${dates.slice(1).join(', ')}\n`); // 1. Load yesterday's failures const yesterdayFile = path.join(CONFIG.MEMORY_DIR, `self-review-${yesterday}.md`); if (!fs.existsSync(yesterdayFile)) { console.log(`⏭️ No self-review file for ${yesterday}`); console.log('This is expected if no cron jobs ran yesterday.\n'); return; } const yesterdayContent = fs.readFileSync(yesterdayFile, 'utf8'); const yesterdayFailures = extractFailures(yesterdayContent); console.log(`Yesterday's failures: ${yesterdayFailures.length}\n`); if (yesterdayFailures.length === 0) { console.log('✅ No failures recorded yesterday. Great job!'); return; } // 2. Load previous days' failures const previousFailures = []; for (const date of dates.slice(1)) { const filepath = path.join(CONFIG.MEMORY_DIR, `self-review-${date}.md`); if (fs.existsSync(filepath)) { const content = fs.readFileSync(filepath, 'utf8'); const failures = extractFailures(content); failures.forEach(text => { previousFailures.push({ date, text }); }); console.log(` ${date}: ${failures.length} failures`); } else { console.log(` ${date}: (no file)`); } } console.log(`\nPrevious failures total: ${previousFailures.length}\n`); // 3. Check for repetitions const repetitions = []; for (const yesterdayFailure of yesterdayFailures) { for (const prevFailure of previousFailures) { const similarity = calculateSimilarity(yesterdayFailure, prevFailure.text); if (similarity >= CONFIG.SIMILARITY_THRESHOLD) { repetitions.push({ yesterday: yesterdayFailure, previous: prevFailure, similarity: similarity.toFixed(2) }); } } } console.log(`Repetitions detected: ${repetitions.length}\n`); // 4. Alert if repetitions found if (repetitions.length > 0) { console.log('⚠️ REPEATED FAILURES:\n'); for (const rep of repetitions) { console.log(` Yesterday: "${rep.yesterday.slice(0, 60)}..."`); console.log(` Previous (${rep.previous.date}): "${rep.previous.text.slice(0, 60)}..."`); console.log(` Similarity: ${(rep.similarity * 100).toFixed(0)}%\n`); } // Send Discord notification try { await sendDiscordAlert({ embeds: [{ title: '⚠️ 일일 체크: 반복 실패 감지', description: `어제(${yesterday}) 기록된 실패/미흡 중 **${repetitions.length}건**이 최근 3일 내 반복되었습니다.`, color: 0xFFA500, // Orange fields: repetitions.slice(0, 3).map(rep => ({ name: `반복 패턴 (${rep.previous.date} → ${yesterday})`, value: `\`\`\`${rep.yesterday.slice(0, 150)}${rep.yesterday.length > 150 ? '...' : ''}\`\`\``, inline: false })).concat([{ name: '권장 조치', value: [ '1. 근본 원인 파악 필요', '2. 즉시 개선 항목 재검토', '3. 패턴 탐지 스크립트 실행: `node ~/openclaw/scripts/detect-patterns.js`' ].join('\n'), inline: false }]), footer: { text: 'Daily Self-Check V1.0' }, timestamp: new Date().toISOString() }] }); console.log('✅ Discord alert sent\n'); } catch (e) { console.error(`❌ Failed to send Discord alert: ${e.message}\n`); } } else { console.log('✅ No repetitions detected. All failures are new.\n'); } // 5. Summary console.log('==================='); console.log(`Summary: ${yesterdayFailures.length} failures yesterday, ${repetitions.length} repeated`); if (repetitions.length > 0) { console.log('\n💡 Next steps:'); console.log(' 1. Review repeated failures'); console.log(' 2. Update "즉시 개선" actions'); console.log(' 3. Consider logging to .learnings/'); } } // ============================================================================ // Run // ============================================================================ if (require.main === module) { main().catch(err => { console.error('❌ Fatal error:', err); process.exit(1); }); } module.exports = { main };