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,127 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Action Logger Hook
|
|
6
|
+
*
|
|
7
|
+
* 이벤트: message:sent
|
|
8
|
+
* 모든 외부 발송 메시지를 감사 로그로 기록 + 이벤트 버스 역할.
|
|
9
|
+
* 토큰 비용: 0 (파일 I/O만)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const ACTIONS_LOG = 'events/actions.jsonl';
|
|
13
|
+
const DAILY_STATS = 'events/daily-stats.json';
|
|
14
|
+
|
|
15
|
+
export default async function handler(event: any): Promise<void> {
|
|
16
|
+
try {
|
|
17
|
+
if (event.type !== 'message' || event.action !== 'sent') return;
|
|
18
|
+
|
|
19
|
+
const workspace = event.context?.workspaceDir || event.workspace || process.cwd();
|
|
20
|
+
const eventsDir = path.join(workspace, 'events');
|
|
21
|
+
|
|
22
|
+
// events 디렉토리 확보
|
|
23
|
+
if (!fs.existsSync(eventsDir)) {
|
|
24
|
+
fs.mkdirSync(eventsDir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const channel = event.context?.channelId || 'unknown';
|
|
28
|
+
const success = event.context?.success !== false;
|
|
29
|
+
const to = event.context?.to || 'unknown';
|
|
30
|
+
const contentPreview = truncate(event.context?.content || '', 100);
|
|
31
|
+
|
|
32
|
+
// 이벤트 타입 분류
|
|
33
|
+
const eventType = classifyEvent(channel, contentPreview);
|
|
34
|
+
|
|
35
|
+
// ===== 1. 감사 로그 기록 =====
|
|
36
|
+
const logEntry = {
|
|
37
|
+
timestamp: new Date().toISOString(),
|
|
38
|
+
type: eventType,
|
|
39
|
+
channel,
|
|
40
|
+
to,
|
|
41
|
+
success,
|
|
42
|
+
preview: contentPreview,
|
|
43
|
+
error: success ? undefined : (event.context?.error || 'unknown'),
|
|
44
|
+
sessionKey: event.sessionKey || undefined,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const logPath = path.join(eventsDir, 'actions.jsonl');
|
|
48
|
+
fs.appendFileSync(logPath, JSON.stringify(logEntry) + '\n', 'utf-8');
|
|
49
|
+
|
|
50
|
+
// ===== 2. 일일 통계 업데이트 =====
|
|
51
|
+
updateDailyStats(eventsDir, channel, success);
|
|
52
|
+
|
|
53
|
+
// ===== 3. 에러 이벤트 =====
|
|
54
|
+
if (!success) {
|
|
55
|
+
const errorEntry = {
|
|
56
|
+
timestamp: new Date().toISOString(),
|
|
57
|
+
type: 'error:message-send',
|
|
58
|
+
channel,
|
|
59
|
+
to,
|
|
60
|
+
error: event.context?.error || 'send failed',
|
|
61
|
+
};
|
|
62
|
+
const errorLogPath = path.join(eventsDir, 'errors.jsonl');
|
|
63
|
+
fs.appendFileSync(errorLogPath, JSON.stringify(errorEntry) + '\n', 'utf-8');
|
|
64
|
+
|
|
65
|
+
console.log(`[action-logger] ❌ Send failed: ${channel} → ${to}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(`[action-logger] 📊 ${eventType} → ${channel}:${to} (${success ? 'ok' : 'fail'})`);
|
|
69
|
+
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('[action-logger] Error (non-fatal):', error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function classifyEvent(channel: string, content: string): string {
|
|
76
|
+
const ch = channel.toLowerCase();
|
|
77
|
+
|
|
78
|
+
if (ch.includes('discord') || ch.includes('telegram') || ch.includes('slack')) {
|
|
79
|
+
return 'message:social';
|
|
80
|
+
}
|
|
81
|
+
if (ch.includes('email') || ch.includes('gmail') || ch.includes('mail')) {
|
|
82
|
+
return 'message:email';
|
|
83
|
+
}
|
|
84
|
+
if (ch.includes('whatsapp') || ch.includes('signal') || ch.includes('imessage')) {
|
|
85
|
+
return 'message:chat';
|
|
86
|
+
}
|
|
87
|
+
return 'message:other';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function updateDailyStats(eventsDir: string, channel: string, success: boolean): void {
|
|
91
|
+
try {
|
|
92
|
+
const statsPath = path.join(eventsDir, 'daily-stats.json');
|
|
93
|
+
const today = new Date().toISOString().split('T')[0];
|
|
94
|
+
|
|
95
|
+
let stats: any = { date: today, channels: {}, total: 0, errors: 0 };
|
|
96
|
+
|
|
97
|
+
if (fs.existsSync(statsPath)) {
|
|
98
|
+
const existing = JSON.parse(fs.readFileSync(statsPath, 'utf-8'));
|
|
99
|
+
if (existing.date === today) {
|
|
100
|
+
stats = existing;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 채널별 카운트
|
|
105
|
+
if (!stats.channels[channel]) {
|
|
106
|
+
stats.channels[channel] = { sent: 0, failed: 0 };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (success) {
|
|
110
|
+
stats.channels[channel].sent++;
|
|
111
|
+
} else {
|
|
112
|
+
stats.channels[channel].failed++;
|
|
113
|
+
stats.errors++;
|
|
114
|
+
}
|
|
115
|
+
stats.total++;
|
|
116
|
+
|
|
117
|
+
fs.writeFileSync(statsPath, JSON.stringify(stats, null, 2), 'utf-8');
|
|
118
|
+
} catch {
|
|
119
|
+
// 통계 실패는 무시
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function truncate(text: string, maxLen: number): string {
|
|
124
|
+
if (!text) return '';
|
|
125
|
+
const oneLine = text.replace(/\n/g, ' ').trim();
|
|
126
|
+
return oneLine.length > maxLen ? oneLine.slice(0, maxLen) + '...' : oneLine;
|
|
127
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: context-recovery
|
|
3
|
+
description: "세션 간 문맥 손실 방지 — 세션 종료 시 저장, 새 세션 시작 시 복원"
|
|
4
|
+
metadata:
|
|
5
|
+
openclaw:
|
|
6
|
+
emoji: "🔄"
|
|
7
|
+
events: ["command:new", "command:reset", "agent:bootstrap"]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Context Recovery Hook
|
|
11
|
+
|
|
12
|
+
## 목적
|
|
13
|
+
세션 간 문맥 손실 방지. 세션 끝날 때 "지금 하던 일"을 저장하고, 새 세션에서 자동 복원.
|
|
14
|
+
|
|
15
|
+
## 동작
|
|
16
|
+
1. **저장** (`command:new` / `command:reset`): 최근 대화에서 "하던 일" 추출 → `memory/context-stack.md` 저장
|
|
17
|
+
2. **복원** (`agent:bootstrap`): context-stack.md를 bootstrapFiles에 주입 (24시간 이내만)
|
|
18
|
+
|
|
19
|
+
## context-stack.md 포맷
|
|
20
|
+
```
|
|
21
|
+
# Context Stack
|
|
22
|
+
- **하던 일**: [구체적 작업]
|
|
23
|
+
- **마지막 응답**: [에이전트 마지막 답변 요약]
|
|
24
|
+
- **대기 중**: [형님 응답/외부 결과 기다리는 것]
|
|
25
|
+
- **갱신**: [ISO 타임스탬프]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 규칙
|
|
29
|
+
- 24시간 이상 된 컨텍스트는 자동 무시
|
|
30
|
+
- 에러 시 조용히 실패 (세션 부팅 방해 금지)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Context Recovery Hook
|
|
6
|
+
*
|
|
7
|
+
* 이벤트:
|
|
8
|
+
* - command:new / command:reset → 현재 컨텍스트 저장
|
|
9
|
+
* - agent:bootstrap → 저장된 컨텍스트 복원 (bootstrapFiles 주입)
|
|
10
|
+
*
|
|
11
|
+
* 세션 간 문맥 손실 방지. "지금 하던 일"을 5줄 이내로 저장/복원.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const CONTEXT_FILE = 'memory/context-stack.md';
|
|
15
|
+
const MAX_LINES = 8; // 헤더 포함
|
|
16
|
+
|
|
17
|
+
export default async function handler(event: any): Promise<void> {
|
|
18
|
+
try {
|
|
19
|
+
const workspace = event.workspace || process.cwd();
|
|
20
|
+
const contextPath = path.join(workspace, CONTEXT_FILE);
|
|
21
|
+
const eventName = event.name || event.event || '';
|
|
22
|
+
|
|
23
|
+
// ===== 1. 저장 (세션 종료 시) =====
|
|
24
|
+
if (eventName === 'command:new' || eventName === 'command:reset' || eventName === 'beforeReset') {
|
|
25
|
+
console.log('[context-recovery] 💾 Saving context stack...');
|
|
26
|
+
|
|
27
|
+
// 이전 메시지에서 최근 대화 요약 추출
|
|
28
|
+
const messages = event.messages || [];
|
|
29
|
+
const recentMessages = messages
|
|
30
|
+
.filter((m: any) => m.role === 'user' || m.role === 'assistant')
|
|
31
|
+
.slice(-6); // 최근 6개 메시지
|
|
32
|
+
|
|
33
|
+
if (recentMessages.length === 0) {
|
|
34
|
+
console.log('[context-recovery] No messages to save, skipping');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 마지막 사용자 메시지 = "하던 일"
|
|
39
|
+
const lastUserMsg = [...recentMessages]
|
|
40
|
+
.reverse()
|
|
41
|
+
.find((m: any) => m.role === 'user');
|
|
42
|
+
const lastAssistantMsg = [...recentMessages]
|
|
43
|
+
.reverse()
|
|
44
|
+
.find((m: any) => m.role === 'assistant');
|
|
45
|
+
|
|
46
|
+
const doingWhat = lastUserMsg
|
|
47
|
+
? truncate(lastUserMsg.content, 120)
|
|
48
|
+
: '알 수 없음';
|
|
49
|
+
const lastResponse = lastAssistantMsg
|
|
50
|
+
? truncate(lastAssistantMsg.content, 120)
|
|
51
|
+
: '없음';
|
|
52
|
+
|
|
53
|
+
const now = new Date().toISOString();
|
|
54
|
+
const stack = [
|
|
55
|
+
'# Context Stack',
|
|
56
|
+
`- **하던 일**: ${doingWhat}`,
|
|
57
|
+
`- **마지막 응답**: ${lastResponse}`,
|
|
58
|
+
`- **대기 중**: 없음`,
|
|
59
|
+
`- **갱신**: ${now}`,
|
|
60
|
+
].join('\n');
|
|
61
|
+
|
|
62
|
+
// memory 디렉토리 확보
|
|
63
|
+
const memoryDir = path.dirname(contextPath);
|
|
64
|
+
if (!fs.existsSync(memoryDir)) {
|
|
65
|
+
fs.mkdirSync(memoryDir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fs.writeFileSync(contextPath, stack, 'utf-8');
|
|
69
|
+
console.log('[context-recovery] ✅ Context saved');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ===== 2. 복원 (세션 시작 시) =====
|
|
74
|
+
if (eventName === 'agent:bootstrap' || eventName === 'beforeAgentStart') {
|
|
75
|
+
console.log('[context-recovery] 🔄 Checking for saved context...');
|
|
76
|
+
|
|
77
|
+
if (!fs.existsSync(contextPath)) {
|
|
78
|
+
console.log('[context-recovery] No context-stack.md found, skipping');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const content = fs.readFileSync(contextPath, 'utf-8').trim();
|
|
83
|
+
if (!content || content === '# Context Stack') {
|
|
84
|
+
console.log('[context-recovery] Empty context, skipping');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 갱신 시간 체크 — 24시간 이상 된 컨텍스트는 무시
|
|
89
|
+
const updatedMatch = content.match(/\*\*갱신\*\*:\s*(.+)/);
|
|
90
|
+
if (updatedMatch) {
|
|
91
|
+
const updatedAt = new Date(updatedMatch[1]).getTime();
|
|
92
|
+
const hoursSince = (Date.now() - updatedAt) / (1000 * 60 * 60);
|
|
93
|
+
if (hoursSince > 24) {
|
|
94
|
+
console.log(`[context-recovery] Context is ${hoursSince.toFixed(1)}h old, skipping`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// bootstrapFiles에 주입
|
|
100
|
+
if (event.context && event.context.bootstrapFiles) {
|
|
101
|
+
if (Array.isArray(event.context.bootstrapFiles)) {
|
|
102
|
+
event.context.bootstrapFiles.push({
|
|
103
|
+
path: CONTEXT_FILE,
|
|
104
|
+
content: content,
|
|
105
|
+
});
|
|
106
|
+
} else {
|
|
107
|
+
// object 형태
|
|
108
|
+
event.context.bootstrapFiles[CONTEXT_FILE] = content;
|
|
109
|
+
}
|
|
110
|
+
console.log('[context-recovery] ✅ Context injected into bootstrap');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// messages에도 힌트 추가
|
|
114
|
+
if (event.messages && Array.isArray(event.messages)) {
|
|
115
|
+
event.messages.push({
|
|
116
|
+
role: 'system',
|
|
117
|
+
content: `🔄 이전 세션 컨텍스트 복원됨:\n${content}`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log(`[context-recovery] Unknown event: ${eventName}, skipping`);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// 조용히 실패 — 컨텍스트 복구 실패가 세션을 망가뜨리면 안 됨
|
|
127
|
+
console.error('[context-recovery] Error (non-fatal):', error);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function truncate(text: string, maxLen: number): string {
|
|
132
|
+
if (!text) return '';
|
|
133
|
+
const oneLine = text.replace(/\n/g, ' ').trim();
|
|
134
|
+
return oneLine.length > maxLen ? oneLine.slice(0, maxLen) + '...' : oneLine;
|
|
135
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: disciple-init
|
|
3
|
+
description: "서브에이전트 spawn 시 SOUL.md 핵심 원칙 + 관련 기억을 선택적으로 주입"
|
|
4
|
+
metadata: { "openclaw": { "emoji": "📜", "events": ["agent:bootstrap"] } }
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Disciple Init
|
|
8
|
+
|
|
9
|
+
서브에이전트에게 경전의 필요한 챕터만 전달하는 훅.
|
|
10
|
+
|
|
11
|
+
## 동작 방식
|
|
12
|
+
|
|
13
|
+
- `agent:bootstrap` 이벤트 수신
|
|
14
|
+
- 서브에이전트 세션인 경우만 동작
|
|
15
|
+
- `memory/reflex/*.md` 파일들을 읽어서 컨텍스트에 주입
|
|
16
|
+
- 보안 규칙은 모든 제자에게 전달
|
|
17
|
+
|
|
18
|
+
## 목적
|
|
19
|
+
|
|
20
|
+
서브에이전트가 필요한 지식만 선택적으로 받아 토큰을 절약하면서도 핵심 원칙은 유지.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
interface BootstrapEvent {
|
|
5
|
+
sessionKey: string;
|
|
6
|
+
context: {
|
|
7
|
+
bootstrapFiles?: Array<{ path: string; content: string }>;
|
|
8
|
+
};
|
|
9
|
+
messages?: Array<{ role: string; content: string }>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default async function handler(event: BootstrapEvent): Promise<void> {
|
|
13
|
+
try {
|
|
14
|
+
console.log('[disciple-init] 📜 전승 시스템 시작');
|
|
15
|
+
|
|
16
|
+
// 서브에이전트 세션인지 확인
|
|
17
|
+
if (!event.sessionKey || !event.sessionKey.includes('subagent')) {
|
|
18
|
+
console.log('[disciple-init] 메인 세션 - 스킵');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(`[disciple-init] 서브에이전트 세션 감지: ${event.sessionKey}`);
|
|
23
|
+
|
|
24
|
+
// workspace 경로 추정 (현재 디렉토리 기준)
|
|
25
|
+
const workspaceDir = process.cwd();
|
|
26
|
+
const reflexDir = path.join(workspaceDir, 'memory', 'reflex');
|
|
27
|
+
|
|
28
|
+
// memory/reflex/ 디렉토리 확인
|
|
29
|
+
if (!fs.existsSync(reflexDir)) {
|
|
30
|
+
console.log('[disciple-init] memory/reflex/ 디렉토리 없음 - 생성 스킵');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// reflex/*.md 파일 읽기
|
|
35
|
+
const reflexFiles = fs.readdirSync(reflexDir)
|
|
36
|
+
.filter(f => f.endsWith('.md'))
|
|
37
|
+
.sort();
|
|
38
|
+
|
|
39
|
+
if (reflexFiles.length === 0) {
|
|
40
|
+
console.log('[disciple-init] reflex 파일 없음');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// bootstrapFiles 배열 초기화
|
|
45
|
+
if (!event.context.bootstrapFiles) {
|
|
46
|
+
event.context.bootstrapFiles = [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 각 reflex 파일을 bootstrapFiles에 추가
|
|
50
|
+
let injectedCount = 0;
|
|
51
|
+
for (const fileName of reflexFiles) {
|
|
52
|
+
const filePath = path.join(reflexDir, fileName);
|
|
53
|
+
try {
|
|
54
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
55
|
+
event.context.bootstrapFiles.push({
|
|
56
|
+
path: `memory/reflex/${fileName}`,
|
|
57
|
+
content: content
|
|
58
|
+
});
|
|
59
|
+
injectedCount++;
|
|
60
|
+
console.log(`[disciple-init] ✅ 주입: ${fileName}`);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error(`[disciple-init] ❌ 읽기 실패: ${fileName}`, err);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 완료 메시지 추가
|
|
67
|
+
if (injectedCount > 0 && event.messages) {
|
|
68
|
+
event.messages.push({
|
|
69
|
+
role: 'system',
|
|
70
|
+
content: `📜 제자 초기화 완료 (${injectedCount}개 전승 파일 주입)`
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(`[disciple-init] 🎉 완료: ${injectedCount}개 파일 주입`);
|
|
75
|
+
|
|
76
|
+
} catch (error) {
|
|
77
|
+
// 조용히 실패 (다른 훅에 영향 없도록)
|
|
78
|
+
console.error('[disciple-init] ⚠️ 에러 발생 (조용히 실패):', error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: event-bus
|
|
3
|
+
description: "스킬 체이닝 이벤트 버스 — 이벤트 큐 처리 + 후속 작업 트리거"
|
|
4
|
+
metadata:
|
|
5
|
+
openclaw:
|
|
6
|
+
emoji: "🔀"
|
|
7
|
+
events: ["agent:bootstrap"]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Event Bus Hook
|
|
11
|
+
|
|
12
|
+
## 목적
|
|
13
|
+
스킬 실행 후 발생한 이벤트를 큐에서 읽어 후속 작업을 트리거하는 이벤트 버스.
|
|
14
|
+
|
|
15
|
+
## 동작 (agent:bootstrap)
|
|
16
|
+
1. `events/queue.jsonl` 에서 미처리 이벤트 읽기
|
|
17
|
+
2. `hooks/event-bus/chains.json` 규칙에 따라 후속 작업 결정
|
|
18
|
+
3. 후속 작업을 messages에 주입 (에이전트가 실행)
|
|
19
|
+
4. 처리된 이벤트를 processed로 마킹
|
|
20
|
+
|
|
21
|
+
## 이벤트 발행 (스킬에서)
|
|
22
|
+
스킬이 완료되면 events/queue.jsonl에 한 줄 추가:
|
|
23
|
+
```json
|
|
24
|
+
{"event":"git:commit","source":"git-auto","data":{"branch":"main","message":"feat: ..."},"ts":"...","processed":false}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 체이닝 규칙 (chains.json)
|
|
28
|
+
```json
|
|
29
|
+
[
|
|
30
|
+
{"trigger":"git:commit","action":"daily-report에 기록","priority":"low"},
|
|
31
|
+
{"trigger":"insta:posted","action":"콘텐츠 발행 이벤트 기록","priority":"medium"},
|
|
32
|
+
{"trigger":"error:*","action":"형님 알림","priority":"high"}
|
|
33
|
+
]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 규칙
|
|
37
|
+
- 미처리 이벤트가 없으면 즉시 리턴 (토큰 0)
|
|
38
|
+
- 이벤트 큐 최대 100개 (넘으면 오래된 것 삭제)
|
|
39
|
+
- 처리된 이벤트는 7일 후 자동 정리
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"trigger": "git:commit",
|
|
4
|
+
"action": "memory/일일 로그에 git 커밋 기록 추가",
|
|
5
|
+
"priority": "low"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"trigger": "insta:posted",
|
|
9
|
+
"action": "콘텐츠 발행 이벤트를 memory/일일 로그에 기록하고 performance-tracker 참고",
|
|
10
|
+
"priority": "medium"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"trigger": "cardnews:created",
|
|
14
|
+
"action": "형님에게 카드뉴스 완성 알림 + 업로드 여부 확인",
|
|
15
|
+
"priority": "medium",
|
|
16
|
+
"requiresApproval": true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"trigger": "competitor:analyzed",
|
|
20
|
+
"action": "분석 결과 요약을 memory/일일 로그에 기록",
|
|
21
|
+
"priority": "low"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"trigger": "self-eval:completed",
|
|
25
|
+
"action": "memory/consolidated/growth.md에 자기평가 결과 반영",
|
|
26
|
+
"priority": "medium"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"trigger": "think-tank:insight",
|
|
30
|
+
"action": "인사이트를 memory/일일 로그에 기록 + 형님 제안 고려",
|
|
31
|
+
"priority": "low"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"trigger": "code-review:passed",
|
|
35
|
+
"action": "git 자동 커밋 가능 여부 확인 후 실행",
|
|
36
|
+
"priority": "medium",
|
|
37
|
+
"requiresApproval": true
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"trigger": "error:*",
|
|
41
|
+
"action": "에러를 events/errors.jsonl에 기록하고, 심각도 높으면 형님 알림",
|
|
42
|
+
"priority": "high"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"trigger": "tokenmeter:high-cost",
|
|
46
|
+
"action": "토큰 비용 경고를 형님에게 알림",
|
|
47
|
+
"priority": "high",
|
|
48
|
+
"condition": "cost > 500"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"trigger": "release:published",
|
|
52
|
+
"action": "릴리즈 기록을 memory/일일 로그에 추가 + 릴리즈 쿨다운 시작",
|
|
53
|
+
"priority": "medium"
|
|
54
|
+
}
|
|
55
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 이벤트 발행 헬퍼
|
|
3
|
+
# 사용: ./emit.sh <event_name> <source> [data_json]
|
|
4
|
+
# 예시: ./emit.sh "git:commit" "git-auto" '{"branch":"main","message":"feat: hook engine"}'
|
|
5
|
+
|
|
6
|
+
EVENT="${1:?이벤트명 필요 (예: git:commit)}"
|
|
7
|
+
SOURCE="${2:?소스 필요 (예: git-auto)}"
|
|
8
|
+
DATA="${3:-{}}"
|
|
9
|
+
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
10
|
+
|
|
11
|
+
QUEUE_FILE="${HOME}/.openclaw/workspace/events/queue.jsonl"
|
|
12
|
+
|
|
13
|
+
# events 디렉토리 확보
|
|
14
|
+
mkdir -p "$(dirname "$QUEUE_FILE")"
|
|
15
|
+
|
|
16
|
+
# JSONL 한 줄 추가
|
|
17
|
+
echo "{\"event\":\"${EVENT}\",\"source\":\"${SOURCE}\",\"data\":${DATA},\"ts\":\"${TS}\",\"processed\":false}" >> "$QUEUE_FILE"
|
|
18
|
+
|
|
19
|
+
echo "[emit] ✅ ${EVENT} from ${SOURCE}"
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Event Bus Hook
|
|
6
|
+
*
|
|
7
|
+
* 이벤트: agent:bootstrap
|
|
8
|
+
* 세션 시작 시 이벤트 큐를 체크하고, 미처리 이벤트에 대한 후속 작업을 주입.
|
|
9
|
+
* 토큰 비용: 미처리 이벤트 없으면 0. 있으면 체이닝 지시문 ~50-200 토큰.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const QUEUE_FILE = 'events/queue.jsonl';
|
|
13
|
+
const CHAINS_FILE = 'hooks/event-bus/chains.json';
|
|
14
|
+
const MAX_QUEUE = 100;
|
|
15
|
+
const MAX_PENDING_ACTIONS = 3; // 한 세션에서 처리할 최대 체이닝 수
|
|
16
|
+
|
|
17
|
+
interface QueueEvent {
|
|
18
|
+
event: string;
|
|
19
|
+
source: string;
|
|
20
|
+
data?: any;
|
|
21
|
+
ts: string;
|
|
22
|
+
processed: boolean;
|
|
23
|
+
processedAt?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ChainRule {
|
|
27
|
+
trigger: string;
|
|
28
|
+
action: string;
|
|
29
|
+
priority: 'low' | 'medium' | 'high';
|
|
30
|
+
requiresApproval?: boolean;
|
|
31
|
+
condition?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default async function handler(event: any): Promise<void> {
|
|
35
|
+
try {
|
|
36
|
+
if (event.type !== 'agent' || event.action !== 'bootstrap') return;
|
|
37
|
+
|
|
38
|
+
const workspace = event.workspace || event.context?.workspaceDir || process.cwd();
|
|
39
|
+
const queuePath = path.join(workspace, QUEUE_FILE);
|
|
40
|
+
const chainsPath = path.join(workspace, CHAINS_FILE);
|
|
41
|
+
|
|
42
|
+
// 큐 파일 없으면 할 일 없음
|
|
43
|
+
if (!fs.existsSync(queuePath)) {
|
|
44
|
+
console.log('[event-bus] No queue file, skipping');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 큐 읽기
|
|
49
|
+
const lines = fs.readFileSync(queuePath, 'utf-8')
|
|
50
|
+
.split('\n')
|
|
51
|
+
.filter(l => l.trim());
|
|
52
|
+
|
|
53
|
+
const events: QueueEvent[] = lines.map(l => {
|
|
54
|
+
try { return JSON.parse(l); }
|
|
55
|
+
catch { return null; }
|
|
56
|
+
}).filter(Boolean);
|
|
57
|
+
|
|
58
|
+
// 미처리 이벤트만
|
|
59
|
+
const pending = events.filter(e => !e.processed);
|
|
60
|
+
|
|
61
|
+
if (pending.length === 0) {
|
|
62
|
+
console.log('[event-bus] No pending events');
|
|
63
|
+
// 큐 정리 (7일+ 처리된 이벤트 삭제)
|
|
64
|
+
cleanOldEvents(events, queuePath);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(`[event-bus] 🔀 ${pending.length} pending event(s) found`);
|
|
69
|
+
|
|
70
|
+
// 체이닝 규칙 로드
|
|
71
|
+
let chains: ChainRule[] = [];
|
|
72
|
+
if (fs.existsSync(chainsPath)) {
|
|
73
|
+
chains = JSON.parse(fs.readFileSync(chainsPath, 'utf-8'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 매칭 + 액션 생성
|
|
77
|
+
const actions: string[] = [];
|
|
78
|
+
const processedIndices: number[] = [];
|
|
79
|
+
|
|
80
|
+
for (const pendingEvent of pending) {
|
|
81
|
+
if (actions.length >= MAX_PENDING_ACTIONS) break;
|
|
82
|
+
|
|
83
|
+
const matchedChains = findMatchingChains(pendingEvent.event, chains);
|
|
84
|
+
|
|
85
|
+
for (const chain of matchedChains) {
|
|
86
|
+
if (actions.length >= MAX_PENDING_ACTIONS) break;
|
|
87
|
+
|
|
88
|
+
const actionText = chain.requiresApproval
|
|
89
|
+
? `[승인 필요] ${chain.action} (이벤트: ${pendingEvent.event}, 소스: ${pendingEvent.source})`
|
|
90
|
+
: `${chain.action} (이벤트: ${pendingEvent.event}, 소스: ${pendingEvent.source})`;
|
|
91
|
+
|
|
92
|
+
actions.push(`- [${chain.priority}] ${actionText}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 처리 완료 마킹
|
|
96
|
+
pendingEvent.processed = true;
|
|
97
|
+
pendingEvent.processedAt = new Date().toISOString();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 큐 파일 업데이트
|
|
101
|
+
const updatedLines = events.map(e => JSON.stringify(e));
|
|
102
|
+
|
|
103
|
+
// 큐 크기 제한
|
|
104
|
+
const trimmed = updatedLines.length > MAX_QUEUE
|
|
105
|
+
? updatedLines.slice(-MAX_QUEUE)
|
|
106
|
+
: updatedLines;
|
|
107
|
+
|
|
108
|
+
fs.writeFileSync(queuePath, trimmed.join('\n') + '\n', 'utf-8');
|
|
109
|
+
|
|
110
|
+
// 에이전트에게 체이닝 작업 알림
|
|
111
|
+
if (actions.length > 0 && event.messages && Array.isArray(event.messages)) {
|
|
112
|
+
const actionList = actions.join('\n');
|
|
113
|
+
event.messages.push({
|
|
114
|
+
role: 'system',
|
|
115
|
+
content: `🔀 [event-bus] 미처리 이벤트 ${pending.length}건 감지. 후속 작업:\n${actionList}\n\n우선순위 높은 것부터 처리하되, [승인 필요] 항목은 형님 확인 후 실행.`,
|
|
116
|
+
});
|
|
117
|
+
console.log(`[event-bus] ✅ ${actions.length} chain action(s) injected`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error('[event-bus] Error (non-fatal):', error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function findMatchingChains(eventName: string, chains: ChainRule[]): ChainRule[] {
|
|
126
|
+
return chains.filter(chain => {
|
|
127
|
+
const trigger = chain.trigger;
|
|
128
|
+
|
|
129
|
+
// 와일드카드 매칭 (error:*)
|
|
130
|
+
if (trigger.endsWith(':*')) {
|
|
131
|
+
const prefix = trigger.slice(0, -1); // "error:"
|
|
132
|
+
return eventName.startsWith(prefix);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 정확 매칭
|
|
136
|
+
return trigger === eventName;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function cleanOldEvents(events: QueueEvent[], queuePath: string): void {
|
|
141
|
+
try {
|
|
142
|
+
const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
|
|
143
|
+
const cleaned = events.filter(e => {
|
|
144
|
+
if (!e.processed || !e.processedAt) return true;
|
|
145
|
+
return new Date(e.processedAt).getTime() > sevenDaysAgo;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (cleaned.length < events.length) {
|
|
149
|
+
const lines = cleaned.map(e => JSON.stringify(e));
|
|
150
|
+
fs.writeFileSync(queuePath, lines.join('\n') + '\n', 'utf-8');
|
|
151
|
+
console.log(`[event-bus] 🧹 Cleaned ${events.length - cleaned.length} old events`);
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// 정리 실패 무시
|
|
155
|
+
}
|
|
156
|
+
}
|