mupengism 3.1.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +40 -0
- package/HEARTBEAT.md +7 -0
- package/IDENTITY.md +11 -0
- package/README.md +47 -294
- package/SOUL.md +43 -0
- package/hooks/action-guard/HOOK.md +29 -0
- package/hooks/action-guard/handler.ts +168 -0
- package/hooks/action-logger/HOOK.md +28 -0
- package/hooks/action-logger/handler.ts +127 -0
- package/hooks/context-recovery/HOOK.md +30 -0
- package/hooks/context-recovery/handler.ts +135 -0
- package/hooks/disciple-init/HOOK.md +20 -0
- package/hooks/disciple-init/handler.ts +80 -0
- package/hooks/event-bus/HOOK.md +39 -0
- package/hooks/event-bus/chains.json +55 -0
- package/hooks/event-bus/emit.sh +19 -0
- package/hooks/event-bus/handler.ts +156 -0
- package/hooks/index-builder/HOOK.md +39 -0
- package/hooks/index-builder/handler.ts +132 -0
- package/hooks/kernel-panic-guard/HOOK.md +39 -0
- package/hooks/kernel-panic-guard/README.md +136 -0
- package/hooks/kernel-panic-guard/WHITELIST.md +117 -0
- package/hooks/kernel-panic-guard/handler.ts +147 -0
- package/hooks/memory-consolidator/HOOK.md +31 -0
- package/hooks/memory-consolidator/handler.ts +111 -0
- package/hooks/reflex-engine/HOOK.md +30 -0
- package/hooks/reflex-engine/handler.ts +158 -0
- package/hooks/registry.md +27 -0
- package/hooks/self-healing/HOOK.md +17 -0
- package/hooks/self-healing/handler.ts +62 -0
- package/hooks/soul-evolution/HOOK.md +26 -0
- package/hooks/soul-evolution/handler.ts +166 -0
- package/hooks/soul-guard/HOOK.md +28 -0
- package/hooks/soul-guard/handler.ts +196 -0
- package/package.json +42 -53
- package/tools/kernel-guard/README.md +170 -0
- package/tools/kernel-guard/lockdown.cjs +152 -0
- package/tools/kernel-guard/register-hash.js +100 -0
- package/tools/kernel-guard/unlock.cjs +106 -0
- package/tools/kernel-guard/verify-kernel.js +133 -0
- package/tools/memory-ops/README.md +221 -0
- package/tools/memory-ops/dream.js +333 -0
- package/tools/memory-ops/forget.js +148 -0
- package/tools/memory-ops/immune.js +305 -0
- package/tools/self-loop/README.md +213 -0
- package/tools/self-loop/brake-check.js +191 -0
- package/tools/self-loop/example-check.sh +34 -0
- package/tools/self-loop/panic-detector.js +191 -0
- package/LICENSE +0 -21
- package/README-EN.md +0 -226
- package/SHOWCASE.md +0 -158
- package/guides/ADVANCED-SYSTEMS.md +0 -251
- package/guides/HEARTBEAT-GUIDE.md +0 -129
- package/guides/LEGION-GUIDE.md +0 -254
- package/guides/MEMORY-GUIDE.md +0 -264
- package/guides/QUICK-START.md +0 -94
- package/guides/THINKTANK-GUIDE.md +0 -227
- package/guides/WEEKLY-BREAK-GUIDE.md +0 -262
- package/installer/README.md +0 -52
- package/installer/cli.js +0 -796
- package/installer/en/README.md +0 -191
- package/installer/en/skill/MEMORY-SYSTEM.md +0 -348
- package/installer/en/skill/PRINCIPLES.md +0 -217
- package/installer/en/skill/SKILL.md +0 -116
- package/installer/en/skill/SOUL-TEMPLATE.md +0 -329
- package/installer/install.sh +0 -162
- package/installer/package.json +0 -31
- package/skill/AGENTS.md +0 -164
- package/skill/BRAKE-LOG-TEMPLATE.md +0 -38
- package/skill/HEARTBEAT-TEMPLATE.md +0 -67
- package/skill/L1-TEMPLATE.md +0 -35
- package/skill/L2-TEMPLATE.md +0 -41
- package/skill/PRINCIPLES.md +0 -192
- package/skill/README.md +0 -47
- package/skill/SKILL.md +0 -166
- package/skill/SOUL-TEMPLATE.md +0 -205
- package/skill/STATE-TEMPLATE.md +0 -54
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
// HookHandler type: (event: HookEvent) => void | Promise<void>
|
|
5
|
+
// HookEvent has: { name: string, metadata?: any, workspace?: string, messages?: any[] }
|
|
6
|
+
|
|
7
|
+
export default async function handler(event: any): Promise<void> {
|
|
8
|
+
try {
|
|
9
|
+
console.log('[memory-consolidator] Starting consolidation...');
|
|
10
|
+
|
|
11
|
+
const workspace = event.workspace || process.cwd();
|
|
12
|
+
const memoryDir = path.join(workspace, 'memory');
|
|
13
|
+
const consolidatedDir = path.join(memoryDir, 'consolidated');
|
|
14
|
+
|
|
15
|
+
// consolidated 디렉토리 생성
|
|
16
|
+
if (!fs.existsSync(consolidatedDir)) {
|
|
17
|
+
fs.mkdirSync(consolidatedDir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 7일 전 날짜 계산
|
|
21
|
+
const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
|
|
22
|
+
|
|
23
|
+
// memory 디렉토리 내 파일 스캔
|
|
24
|
+
if (!fs.existsSync(memoryDir)) {
|
|
25
|
+
console.log('[memory-consolidator] No memory directory found, skipping');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const files = fs.readdirSync(memoryDir);
|
|
30
|
+
const dailyLogPattern = /^\d{4}-\d{2}-\d{2}\.md$/;
|
|
31
|
+
|
|
32
|
+
let processedCount = 0;
|
|
33
|
+
|
|
34
|
+
for (const file of files) {
|
|
35
|
+
if (!dailyLogPattern.test(file)) continue;
|
|
36
|
+
|
|
37
|
+
const filePath = path.join(memoryDir, file);
|
|
38
|
+
const stats = fs.statSync(filePath);
|
|
39
|
+
|
|
40
|
+
// 7일 이상 된 파일만 처리
|
|
41
|
+
if (stats.mtimeMs > sevenDaysAgo) continue;
|
|
42
|
+
|
|
43
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
44
|
+
|
|
45
|
+
// 이미 처리된 파일 스킵
|
|
46
|
+
if (content.startsWith('<!-- consolidated:')) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 주제별 키워드 분류
|
|
51
|
+
const categories = categorizeContent(content);
|
|
52
|
+
|
|
53
|
+
if (categories.length === 0) {
|
|
54
|
+
// 카테고리 없으면 마커만 추가
|
|
55
|
+
const newContent = `<!-- consolidated: ${new Date().toISOString().split('T')[0]} -->\n\n${content}`;
|
|
56
|
+
fs.writeFileSync(filePath, newContent, 'utf-8');
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 각 카테고리별로 append
|
|
61
|
+
for (const category of categories) {
|
|
62
|
+
const categoryFile = path.join(consolidatedDir, `${category}.md`);
|
|
63
|
+
const timestamp = file.replace('.md', '');
|
|
64
|
+
const entry = `\n\n---\n\n## ${timestamp}\n\n${content}\n`;
|
|
65
|
+
|
|
66
|
+
fs.appendFileSync(categoryFile, entry, 'utf-8');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 원본 파일에 마커 추가
|
|
70
|
+
const today = new Date().toISOString().split('T')[0];
|
|
71
|
+
const newContent = `<!-- consolidated: ${today} -->\n\n${content}`;
|
|
72
|
+
fs.writeFileSync(filePath, newContent, 'utf-8');
|
|
73
|
+
|
|
74
|
+
processedCount++;
|
|
75
|
+
console.log(`[memory-consolidator] Processed: ${file} → ${categories.join(', ')}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(`[memory-consolidator] Completed. Processed ${processedCount} files.`);
|
|
79
|
+
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('[memory-consolidator] Error:', error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function categorizeContent(content: string): string[] {
|
|
86
|
+
const lower = content.toLowerCase();
|
|
87
|
+
const categories: string[] = [];
|
|
88
|
+
|
|
89
|
+
// 키워드 기반 분류
|
|
90
|
+
if (lower.match(/보안|시크릿|secret|password|auth|인젝션|injection|vulnerability/)) {
|
|
91
|
+
categories.push('security');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (lower.match(/가치관|철학|philosophy|생각|believe|principle|값|value/)) {
|
|
95
|
+
categories.push('philosophy');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (lower.match(/성장|학습|learn|grow|improve|개선|skill|스킬/)) {
|
|
99
|
+
categories.push('growth');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (lower.match(/전환|pivot|결정|decision|방향|direction|change|전략/)) {
|
|
103
|
+
categories.push('pivots');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (lower.match(/발견|discover|기술|tech|도구|tool|라이브러리|library|프레임워크|framework/)) {
|
|
107
|
+
categories.push('tech-discoveries');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return [...new Set(categories)]; // 중복 제거
|
|
111
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reflex-engine
|
|
3
|
+
description: "Agent Reflex Protocol — 행동 전 자동 규칙 체크 (message:received)"
|
|
4
|
+
metadata:
|
|
5
|
+
openclaw:
|
|
6
|
+
emoji: "⚡"
|
|
7
|
+
events: ["message:received"]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Reflex Engine Hook
|
|
11
|
+
|
|
12
|
+
## 목적
|
|
13
|
+
수신 메시지 문맥에서 reflex 규칙을 자동 체크하여, 에이전트가 실수를 반복하지 않도록 방지.
|
|
14
|
+
|
|
15
|
+
## 동작 (message:received)
|
|
16
|
+
1. `memory/reflex/` 하위 규칙 파일 로드
|
|
17
|
+
2. 메시지 내용 + 컨텍스트로 규칙 매칭
|
|
18
|
+
3. 매칭된 규칙의 action에 따라:
|
|
19
|
+
- `check`: 경고 메시지 주입
|
|
20
|
+
- `prevent`: 차단 지시 주입
|
|
21
|
+
- `warn`: 참고 정보 주입
|
|
22
|
+
|
|
23
|
+
## 규칙 로딩 순서
|
|
24
|
+
1. `memory/reflex/universal/rules/` — 모든 상황
|
|
25
|
+
2. `memory/reflex/domain/<channel>/rules/` — 채널별
|
|
26
|
+
3. `memory/reflex/personal/rules/` — 개인
|
|
27
|
+
|
|
28
|
+
## 토큰 비용
|
|
29
|
+
- 규칙 파일 없으면: 0
|
|
30
|
+
- 매칭된 규칙 있으면: ~30-100 토큰 (규칙 지시문 주입)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Reflex Engine Hook
|
|
6
|
+
*
|
|
7
|
+
* 이벤트: message:received
|
|
8
|
+
* memory/reflex/ 하위의 규칙 파일을 로드하고,
|
|
9
|
+
* 수신 메시지 문맥에 매칭되는 규칙이 있으면 에이전트에게 지시 주입.
|
|
10
|
+
* 토큰 비용: 매칭 없으면 0. 매칭 있으면 ~30-100 토큰.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface ReflexRule {
|
|
14
|
+
id: string;
|
|
15
|
+
trigger: {
|
|
16
|
+
pattern: string;
|
|
17
|
+
context?: string[];
|
|
18
|
+
negative?: string[];
|
|
19
|
+
};
|
|
20
|
+
action: {
|
|
21
|
+
type: 'check' | 'prevent' | 'replace' | 'warn';
|
|
22
|
+
instruction: string;
|
|
23
|
+
severity: 'critical' | 'high' | 'medium' | 'low';
|
|
24
|
+
};
|
|
25
|
+
domain?: string;
|
|
26
|
+
layer?: number;
|
|
27
|
+
confidence?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const REFLEX_DIR = 'memory/reflex';
|
|
31
|
+
const LAYER_DIRS = ['universal/rules', 'domain', 'personal/rules'];
|
|
32
|
+
|
|
33
|
+
export default async function handler(event: any): Promise<void> {
|
|
34
|
+
try {
|
|
35
|
+
if (event.type !== 'message' || event.action !== 'received') return;
|
|
36
|
+
|
|
37
|
+
const workspace = event.workspace || event.context?.workspaceDir || process.cwd();
|
|
38
|
+
const reflexDir = path.join(workspace, REFLEX_DIR);
|
|
39
|
+
|
|
40
|
+
if (!fs.existsSync(reflexDir)) {
|
|
41
|
+
return; // reflex 디렉토리 없으면 패스
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const content = event.context?.content || '';
|
|
45
|
+
if (!content || content.length < 3) return;
|
|
46
|
+
|
|
47
|
+
const channel = event.context?.channelId || event.context?.metadata?.provider || '';
|
|
48
|
+
|
|
49
|
+
// 규칙 로드
|
|
50
|
+
const rules = loadRules(reflexDir, channel);
|
|
51
|
+
|
|
52
|
+
if (rules.length === 0) return;
|
|
53
|
+
|
|
54
|
+
// 매칭
|
|
55
|
+
const matched = matchRules(rules, content, channel);
|
|
56
|
+
|
|
57
|
+
if (matched.length === 0) return;
|
|
58
|
+
|
|
59
|
+
console.log(`[reflex-engine] ⚡ ${matched.length} rule(s) triggered`);
|
|
60
|
+
|
|
61
|
+
// 심각도 순 정렬
|
|
62
|
+
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
63
|
+
matched.sort((a, b) => severityOrder[a.action.severity] - severityOrder[b.action.severity]);
|
|
64
|
+
|
|
65
|
+
// 에이전트에게 지시 주입
|
|
66
|
+
const instructions = matched.map(rule => {
|
|
67
|
+
const icon = rule.action.type === 'prevent' ? '🚫' :
|
|
68
|
+
rule.action.type === 'check' ? '⚠️' :
|
|
69
|
+
rule.action.type === 'warn' ? '💡' : '🔄';
|
|
70
|
+
return `${icon} [${rule.action.severity}] ${rule.action.instruction}`;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (event.messages && Array.isArray(event.messages)) {
|
|
74
|
+
event.messages.push({
|
|
75
|
+
role: 'system',
|
|
76
|
+
content: `⚡ [reflex-engine] 행동 규칙 매칭됨:\n${instructions.join('\n')}\n\n위 규칙을 참고하여 응답하세요.`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('[reflex-engine] Error (non-fatal):', error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function loadRules(reflexDir: string, channel: string): ReflexRule[] {
|
|
86
|
+
const rules: ReflexRule[] = [];
|
|
87
|
+
|
|
88
|
+
// Layer 1: universal
|
|
89
|
+
const universalDir = path.join(reflexDir, 'universal', 'rules');
|
|
90
|
+
rules.push(...loadRulesFromDir(universalDir));
|
|
91
|
+
|
|
92
|
+
// Layer 2: domain-specific
|
|
93
|
+
if (channel) {
|
|
94
|
+
const domainDir = path.join(reflexDir, 'domain', channel, 'rules');
|
|
95
|
+
rules.push(...loadRulesFromDir(domainDir));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Layer 3: personal
|
|
99
|
+
const personalDir = path.join(reflexDir, 'personal', 'rules');
|
|
100
|
+
rules.push(...loadRulesFromDir(personalDir));
|
|
101
|
+
|
|
102
|
+
return rules;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function loadRulesFromDir(dir: string): ReflexRule[] {
|
|
106
|
+
const rules: ReflexRule[] = [];
|
|
107
|
+
|
|
108
|
+
if (!fs.existsSync(dir)) return rules;
|
|
109
|
+
|
|
110
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
111
|
+
|
|
112
|
+
for (const file of files) {
|
|
113
|
+
try {
|
|
114
|
+
const content = fs.readFileSync(path.join(dir, file), 'utf-8');
|
|
115
|
+
const rule = JSON.parse(content) as ReflexRule;
|
|
116
|
+
if (rule.id && rule.trigger && rule.action) {
|
|
117
|
+
rules.push(rule);
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// 파싱 실패 무시
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return rules;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function matchRules(rules: ReflexRule[], content: string, channel: string): ReflexRule[] {
|
|
128
|
+
const lower = content.toLowerCase();
|
|
129
|
+
|
|
130
|
+
return rules.filter(rule => {
|
|
131
|
+
// trigger.pattern 매칭 (자연어 키워드 기반)
|
|
132
|
+
const pattern = rule.trigger.pattern.toLowerCase();
|
|
133
|
+
const keywords = pattern.split(/\s+/).filter(k => k.length > 2);
|
|
134
|
+
|
|
135
|
+
// 키워드 중 70% 이상 매칭되면 트리거
|
|
136
|
+
const matchCount = keywords.filter(k => lower.includes(k)).length;
|
|
137
|
+
const matchRatio = keywords.length > 0 ? matchCount / keywords.length : 0;
|
|
138
|
+
|
|
139
|
+
if (matchRatio < 0.7) return false;
|
|
140
|
+
|
|
141
|
+
// negative 패턴 체크 (있으면 제외)
|
|
142
|
+
if (rule.trigger.negative) {
|
|
143
|
+
for (const neg of rule.trigger.negative) {
|
|
144
|
+
if (lower.includes(neg.toLowerCase())) return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// context 태그 체크
|
|
149
|
+
if (rule.trigger.context && rule.trigger.context.length > 0) {
|
|
150
|
+
const hasContext = rule.trigger.context.some(ctx =>
|
|
151
|
+
channel.includes(ctx) || lower.includes(ctx.toLowerCase())
|
|
152
|
+
);
|
|
153
|
+
if (!hasContext) return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return true;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Hook Registry
|
|
2
|
+
|
|
3
|
+
## post-hooks
|
|
4
|
+
- git-auto → daily-report (auto-include commit records)
|
|
5
|
+
- insta-post → events/content-published + daily-report
|
|
6
|
+
- competitor-watch → events/market-update + notification-hub
|
|
7
|
+
- tokenmeter → cost-alert (urgent when exceeding $500/month)
|
|
8
|
+
- think-tank → events/insight-discovered + memory/bank
|
|
9
|
+
- cardnews → social-publisher (optional)
|
|
10
|
+
- code-review → git-auto (auto-commit when review passes)
|
|
11
|
+
- self-eval → update memory/growth-dashboard
|
|
12
|
+
|
|
13
|
+
## pre-hooks
|
|
14
|
+
- mail send → security-check (scan secrets/personal info)
|
|
15
|
+
- insta-post → image-validate (check specs: 1:1, JPG)
|
|
16
|
+
- release-discipline → version-check + test-run
|
|
17
|
+
|
|
18
|
+
## on-error hooks
|
|
19
|
+
- any → log to memory/errors/YYYY-MM-DD.md
|
|
20
|
+
- any → notification-hub (urgent)
|
|
21
|
+
- browser-dependent → check browser status + attempt restart
|
|
22
|
+
|
|
23
|
+
## scheduled hooks
|
|
24
|
+
- daily 09:00 → trend-radar
|
|
25
|
+
- daily 22:00 → auto-generate daily-report
|
|
26
|
+
- weekly Mon → self-eval
|
|
27
|
+
- weekly Fri → competitor-watch market-scan
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: self-healing
|
|
3
|
+
description: "시스템 자가 진단 — disk/cron 이상만 감지 (gateway 체크 제외)"
|
|
4
|
+
metadata: { "openclaw": { "emoji": "🏥", "events": ["command:new"] } }
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Self Healing Hook
|
|
8
|
+
|
|
9
|
+
## 목적
|
|
10
|
+
세션 시작 시 disk/cron 상태만 점검. Gateway 체크/재시작은 절대 하지 않음.
|
|
11
|
+
|
|
12
|
+
## ⚠️ 금지 사항
|
|
13
|
+
- gateway restart 호출 금지 (자폭 루프 원인)
|
|
14
|
+
- agent:bootstrap 이벤트 금지 (세션마다 실행되어 루프 유발)
|
|
15
|
+
|
|
16
|
+
## Gateway 헬스체크
|
|
17
|
+
별도 cron으로 분리: scripts/gateway-health-cron.sh
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self Healing Hook
|
|
3
|
+
*
|
|
4
|
+
* 이벤트: command:new (세션 리셋 시만)
|
|
5
|
+
* disk/cron 상태만 점검. Gateway 체크/재시작 절대 금지.
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ 교훈 (2026-02-27): agent:bootstrap + gateway restart = 자폭 루프
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
|
|
15
|
+
export default async function handler(event: any): Promise<void> {
|
|
16
|
+
try {
|
|
17
|
+
if (event.type !== 'command' || event.action !== 'new') return;
|
|
18
|
+
|
|
19
|
+
const issues: string[] = [];
|
|
20
|
+
|
|
21
|
+
// 1. 디스크 용량 체크 (workspace)
|
|
22
|
+
try {
|
|
23
|
+
const workspace = event.context?.workspaceDir ||
|
|
24
|
+
path.join(os.homedir(), '.openclaw', 'workspace');
|
|
25
|
+
const df = execSync(`df -k "${workspace}" 2>/dev/null | tail -1`, {
|
|
26
|
+
encoding: 'utf-8', timeout: 3000
|
|
27
|
+
});
|
|
28
|
+
const parts = df.trim().split(/\s+/);
|
|
29
|
+
const usePercent = parseInt(parts[4]);
|
|
30
|
+
if (usePercent >= 90) {
|
|
31
|
+
issues.push(`⚠️ 디스크 ${usePercent}% 사용 중`);
|
|
32
|
+
}
|
|
33
|
+
} catch { /* 무시 */ }
|
|
34
|
+
|
|
35
|
+
// 2. 오래된 daily log 체크 (30일 이상)
|
|
36
|
+
try {
|
|
37
|
+
const memDir = path.join(
|
|
38
|
+
event.context?.workspaceDir || path.join(os.homedir(), '.openclaw', 'workspace'),
|
|
39
|
+
'memory'
|
|
40
|
+
);
|
|
41
|
+
if (fs.existsSync(memDir)) {
|
|
42
|
+
const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
43
|
+
const old = fs.readdirSync(memDir)
|
|
44
|
+
.filter(f => /^\d{4}-\d{2}-\d{2}\.md$/.test(f))
|
|
45
|
+
.filter(f => {
|
|
46
|
+
const stat = fs.statSync(path.join(memDir, f));
|
|
47
|
+
return stat.mtimeMs < cutoff;
|
|
48
|
+
});
|
|
49
|
+
if (old.length > 5) {
|
|
50
|
+
issues.push(`📝 오래된 daily log ${old.length}개 (memory-consolidator 실행 권장)`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch { /* 무시 */ }
|
|
54
|
+
|
|
55
|
+
if (issues.length > 0 && event.messages) {
|
|
56
|
+
event.messages.push(`[self-healing] ${issues.join(' | ')}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('[self-healing] Error (non-fatal):', error);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: soul-evolution
|
|
3
|
+
description: "SOUL.md 진화 후보를 자동 감지하고 형님에게 제안"
|
|
4
|
+
metadata: { "openclaw": { "emoji": "🧬", "events": ["command:new"] } }
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Soul Evolution
|
|
8
|
+
|
|
9
|
+
주기적으로 brake-log, dreams, consolidated 분석해서 SOUL.md에 추가할 원칙 후보 제안.
|
|
10
|
+
|
|
11
|
+
## 동작 방식
|
|
12
|
+
|
|
13
|
+
- `command:new` 이벤트 수신
|
|
14
|
+
- 7일마다 자동 실행
|
|
15
|
+
- 반복 패턴 감지 (키워드 3회+ 등장)
|
|
16
|
+
- 원칙 후보를 `memory/soul-evolution-proposals.md`에 기록
|
|
17
|
+
|
|
18
|
+
## 분석 대상
|
|
19
|
+
|
|
20
|
+
- `memory/brake-log.md` — 실수와 교훈
|
|
21
|
+
- `memory/dreams/*.md` — 반복되는 생각
|
|
22
|
+
- `memory/consolidated/growth.md` — 성장 기록
|
|
23
|
+
|
|
24
|
+
## 목적
|
|
25
|
+
|
|
26
|
+
스스로 학습한 패턴을 감지하고, 형님 승인 후 SOUL.md에 반영.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
interface CommandEvent {
|
|
5
|
+
sessionKey: string;
|
|
6
|
+
messages?: Array<{ role: string; content: string }>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface EvolutionState {
|
|
10
|
+
lastAnalysis: string | null; // ISO date string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface KeywordCount {
|
|
14
|
+
[keyword: string]: {
|
|
15
|
+
count: number;
|
|
16
|
+
sources: string[];
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default async function handler(event: CommandEvent): Promise<void> {
|
|
21
|
+
try {
|
|
22
|
+
console.log('[soul-evolution] 🧬 자기 진화 시스템 시작');
|
|
23
|
+
|
|
24
|
+
const workspaceDir = process.cwd();
|
|
25
|
+
const memoryDir = path.join(workspaceDir, 'memory');
|
|
26
|
+
const stateFile = path.join(memoryDir, 'soul-evolution-state.json');
|
|
27
|
+
const proposalsFile = path.join(memoryDir, 'soul-evolution-proposals.md');
|
|
28
|
+
|
|
29
|
+
// memory 디렉토리 확인
|
|
30
|
+
if (!fs.existsSync(memoryDir)) {
|
|
31
|
+
fs.mkdirSync(memoryDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 상태 파일 읽기
|
|
35
|
+
let state: EvolutionState = { lastAnalysis: null };
|
|
36
|
+
if (fs.existsSync(stateFile)) {
|
|
37
|
+
try {
|
|
38
|
+
const stateContent = fs.readFileSync(stateFile, 'utf-8');
|
|
39
|
+
state = JSON.parse(stateContent);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error('[soul-evolution] 상태 파일 파싱 실패, 새로 시작');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 마지막 분석 후 7일 경과 체크
|
|
46
|
+
const now = new Date();
|
|
47
|
+
if (state.lastAnalysis) {
|
|
48
|
+
const lastDate = new Date(state.lastAnalysis);
|
|
49
|
+
const daysSince = (now.getTime() - lastDate.getTime()) / (1000 * 60 * 60 * 24);
|
|
50
|
+
|
|
51
|
+
if (daysSince < 7) {
|
|
52
|
+
console.log(`[soul-evolution] 마지막 분석 후 ${daysSince.toFixed(1)}일 경과 - 스킵 (7일 미만)`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log('[soul-evolution] 7일 경과 또는 첫 실행 - 분석 시작');
|
|
58
|
+
|
|
59
|
+
// 분석 대상 파일 목록
|
|
60
|
+
const targetFiles = [
|
|
61
|
+
path.join(memoryDir, 'brake-log.md'),
|
|
62
|
+
path.join(memoryDir, 'consolidated', 'growth.md')
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
// dreams/*.md 파일 추가
|
|
66
|
+
const dreamsDir = path.join(memoryDir, 'dreams');
|
|
67
|
+
if (fs.existsSync(dreamsDir)) {
|
|
68
|
+
const dreamFiles = fs.readdirSync(dreamsDir)
|
|
69
|
+
.filter(f => f.endsWith('.md'))
|
|
70
|
+
.map(f => path.join(dreamsDir, f));
|
|
71
|
+
targetFiles.push(...dreamFiles);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 키워드 카운팅
|
|
75
|
+
const keywords: KeywordCount = {};
|
|
76
|
+
let totalFilesRead = 0;
|
|
77
|
+
|
|
78
|
+
for (const filePath of targetFiles) {
|
|
79
|
+
if (!fs.existsSync(filePath)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
85
|
+
const fileName = path.basename(filePath);
|
|
86
|
+
|
|
87
|
+
// 간단한 키워드 추출: 3글자 이상 단어, 소문자 변환
|
|
88
|
+
const words = content
|
|
89
|
+
.toLowerCase()
|
|
90
|
+
.replace(/[^\w\sㄱ-ㅎ가-힣]/g, ' ')
|
|
91
|
+
.split(/\s+/)
|
|
92
|
+
.filter(w => w.length >= 3);
|
|
93
|
+
|
|
94
|
+
// 불용어 제외 (선택적)
|
|
95
|
+
const stopWords = new Set(['the', 'and', 'for', 'with', 'this', 'that', 'from', 'have', 'been']);
|
|
96
|
+
|
|
97
|
+
for (const word of words) {
|
|
98
|
+
if (stopWords.has(word)) continue;
|
|
99
|
+
|
|
100
|
+
if (!keywords[word]) {
|
|
101
|
+
keywords[word] = { count: 0, sources: [] };
|
|
102
|
+
}
|
|
103
|
+
keywords[word].count++;
|
|
104
|
+
if (!keywords[word].sources.includes(fileName)) {
|
|
105
|
+
keywords[word].sources.push(fileName);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
totalFilesRead++;
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error(`[soul-evolution] 파일 읽기 실패: ${filePath}`, err);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log(`[soul-evolution] ${totalFilesRead}개 파일 분석 완료`);
|
|
116
|
+
|
|
117
|
+
// 3회 이상 등장한 키워드 필터링
|
|
118
|
+
const candidates = Object.entries(keywords)
|
|
119
|
+
.filter(([_, data]) => data.count >= 3)
|
|
120
|
+
.sort((a, b) => b[1].count - a[1].count)
|
|
121
|
+
.slice(0, 10); // 상위 10개만
|
|
122
|
+
|
|
123
|
+
if (candidates.length === 0) {
|
|
124
|
+
console.log('[soul-evolution] 반복 패턴 없음 - 제안 없음');
|
|
125
|
+
|
|
126
|
+
// 상태 업데이트
|
|
127
|
+
state.lastAnalysis = now.toISOString();
|
|
128
|
+
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 제안 문서 생성
|
|
133
|
+
const today = now.toISOString().split('T')[0];
|
|
134
|
+
let proposalsContent = `# 🧬 SOUL.md 진화 제안 (${today})\n\n`;
|
|
135
|
+
proposalsContent += `## 후보 원칙\n\n`;
|
|
136
|
+
|
|
137
|
+
candidates.forEach(([keyword, data], idx) => {
|
|
138
|
+
proposalsContent += `${idx + 1}. **"${keyword}" 패턴 발견** (${data.count}회 등장)\n`;
|
|
139
|
+
proposalsContent += ` - 출처: ${data.sources.join(', ')}\n`;
|
|
140
|
+
proposalsContent += ` - 제안: SOUL.md 원칙 후보로 검토\n\n`;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
proposalsContent += `## 승인 대기\n\n`;
|
|
144
|
+
proposalsContent += `형님 승인 시에만 SOUL.md 수정.\n`;
|
|
145
|
+
|
|
146
|
+
fs.writeFileSync(proposalsFile, proposalsContent);
|
|
147
|
+
console.log(`[soul-evolution] ✅ 제안 파일 생성: ${proposalsFile}`);
|
|
148
|
+
|
|
149
|
+
// 상태 업데이트
|
|
150
|
+
state.lastAnalysis = now.toISOString();
|
|
151
|
+
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
152
|
+
|
|
153
|
+
// 이벤트 메시지 추가
|
|
154
|
+
if (event.messages) {
|
|
155
|
+
event.messages.push({
|
|
156
|
+
role: 'system',
|
|
157
|
+
content: `🧬 SOUL.md 진화 제안 ${candidates.length}건 — memory/soul-evolution-proposals.md 확인`
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log(`[soul-evolution] 🎉 완료: ${candidates.length}개 원칙 후보 제안`);
|
|
162
|
+
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('[soul-evolution] ⚠️ 에러 발생:', error);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: soul-guard
|
|
3
|
+
description: SOUL.md 파일의 변경을 감지하여 알림
|
|
4
|
+
metadata: { "openclaw": { "emoji": "🔗", "events": ["agent:bootstrap"] } }
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Soul Guard
|
|
8
|
+
|
|
9
|
+
에이전트 부트스트랩 시 SOUL.md의 변경을 감지합니다.
|
|
10
|
+
|
|
11
|
+
## 동작 방식
|
|
12
|
+
|
|
13
|
+
1. `SOUL.md`의 SHA-256 해시 계산
|
|
14
|
+
2. `memory/soul-hash.txt`에 저장된 이전 해시와 비교
|
|
15
|
+
3. 변경 감지 시: `event.messages.push()`로 경고 메시지 추가
|
|
16
|
+
4. 첫 실행 시: 해시를 저장만 하고 알림 없음
|
|
17
|
+
|
|
18
|
+
## 목적
|
|
19
|
+
|
|
20
|
+
SOUL.md는 에이전트의 정체성을 정의하는 핵심 파일입니다.
|
|
21
|
+
의도하지 않은 변경이나 공격을 조기에 감지하기 위해 매 세션마다 체크합니다.
|
|
22
|
+
|
|
23
|
+
## 알림 메시지
|
|
24
|
+
|
|
25
|
+
변경 감지 시:
|
|
26
|
+
```
|
|
27
|
+
⚠️ SOUL.md가 변경되었습니다. 의도한 변경인지 확인하세요.
|
|
28
|
+
```
|