@su-record/vibe 2.7.0 → 2.7.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/CLAUDE.md CHANGED
@@ -193,12 +193,11 @@ Lite, Dev (ULTRAWORK), Research, Review Debate, SPEC Debate, Debug, Security, Mi
193
193
  | Event | Hooks |
194
194
  |-------|-------|
195
195
  | SessionStart | `session-start.js` |
196
- | PreToolUse | `pre-tool-guard.js` |
197
- | PostToolUse | `post-edit.js`, `code-check.js`, `post-tool-verify.js` |
198
- | UserPromptSubmit | `prompt-dispatcher.js`, `skill-injector.js`, `keyword-detector.js`, `hud-status.js` |
196
+ | PreToolUse | `sentinel-guard.js`, `pre-tool-guard.js` |
197
+ | PostToolUse | `post-edit.js`, `code-check.js` |
198
+ | UserPromptSubmit | `prompt-dispatcher.js` `keyword-detector.js`, `llm-orchestrate.js` |
199
199
  | Notification | `context-save.js` (80/90/95%) |
200
-
201
- Additional: `code-review.js`, `llm-orchestrate.js`, `recall.js`, `complexity.js`, `compound.js`, `stop-notify.js`
200
+ | Stop | `stop-notify.js` |
202
201
 
203
202
  ## Language Support (25 frameworks)
204
203
 
@@ -2,61 +2,31 @@
2
2
  * PostToolUse Hook - Edit 후 console.log 감지
3
3
  *
4
4
  * NOTE: tsc, prettier 제거 — 빌드/커밋 시점에 실행하므로 Edit마다 불필요
5
+ * grep spawn 대신 fs.readFileSync + regex로 프로세스 오버헤드 제거
5
6
  */
6
- import { execFileSync } from 'child_process';
7
- import { existsSync } from 'fs';
7
+ import { existsSync, readFileSync } from 'fs';
8
8
  import path from 'path';
9
- import { PROJECT_DIR } from './utils.js';
10
9
 
11
- // Claude Code에서 전달받는 환경변수에서 파일 경로 추출
12
- const toolInput = process.env.TOOL_INPUT || '{}';
10
+ const CONSOLE_LOG_RE = /console\.log/;
11
+ const CODE_EXT_RE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
13
12
 
14
- function main() {
15
- try {
16
- const input = JSON.parse(toolInput);
17
- const filePath = input.file_path || input.path || '';
13
+ try {
14
+ const input = JSON.parse(process.env.TOOL_INPUT || '{}');
15
+ const filePath = input.file_path || input.path || '';
18
16
 
19
- if (!filePath) {
20
- return;
21
- }
22
-
23
- // 경로 검증: resolve로 정규화
17
+ if (filePath && CODE_EXT_RE.test(filePath)) {
24
18
  const resolved = path.resolve(filePath);
25
- if (!existsSync(resolved)) {
26
- return;
27
- }
28
-
29
- // TypeScript/JavaScript 파일인지 확인
30
- const isTs = /\.(ts|tsx)$/.test(resolved);
31
- const isJs = /\.(js|jsx|mjs|cjs)$/.test(resolved);
32
-
33
- if (!isTs && !isJs) {
34
- return;
35
- }
36
-
37
- const results = [];
38
-
39
- // console.log 감지 (execFileSync로 command injection 방지)
40
- try {
41
- const grepResult = execFileSync(
42
- 'grep', ['-n', 'console\\.log', resolved],
43
- { cwd: PROJECT_DIR, stdio: 'pipe', encoding: 'utf-8' }
44
- );
45
- if (grepResult.trim()) {
46
- const lines = grepResult.trim().split('\n').slice(0, 3).map(l => l.split(':')[0]).join(',');
47
- results.push(`console.log at line ${lines}`);
19
+ if (existsSync(resolved)) {
20
+ const lines = readFileSync(resolved, 'utf-8').split('\n');
21
+ const hits = [];
22
+ for (let i = 0; i < lines.length && hits.length < 3; i++) {
23
+ if (CONSOLE_LOG_RE.test(lines[i])) hits.push(i + 1);
24
+ }
25
+ if (hits.length > 0) {
26
+ console.log(`[POST-EDIT] ${path.basename(resolved)}: console.log at line ${hits.join(',')}`);
48
27
  }
49
- } catch {
50
- // grep 실패는 console.log 없음을 의미
51
- }
52
-
53
- if (results.length > 0) {
54
- console.log(`[POST-EDIT] ${path.basename(resolved)}: ${results.join(' | ')}`);
55
28
  }
56
-
57
- } catch {
58
- // 조용히 실패
59
29
  }
30
+ } catch {
31
+ // 조용히 실패
60
32
  }
61
-
62
- main();
@@ -49,31 +49,6 @@ const DISPATCH_RULES = [
49
49
  script: null, // keyword-detector가 이미 처리
50
50
  label: 'skip',
51
51
  },
52
- {
53
- pattern: /버그.*(해결|수정|고침)|문제.*(해결|수정)|bug.*(fixed|resolved|solved)|issue.*(fixed|resolved)|PR.*(merged|머지)/i,
54
- script: 'compound.js',
55
- args: [],
56
- label: 'compound',
57
- },
58
- {
59
- pattern: /코드\s*리뷰|code\s*review|PR\s*리뷰|리뷰.*해줘|review.*code/i,
60
- script: 'code-review.js',
61
- args: [],
62
- label: 'code-review',
63
- },
64
- {
65
- pattern: /복잡도.*분석|복잡도.*확인|complexity.*analyz|코드.*복잡도/i,
66
- script: 'complexity.js',
67
- args: [],
68
- label: 'complexity',
69
- },
70
- {
71
- pattern: /뭐였지|이전에.*결정|저번에.*결정|previous.*decision|what was.*decided/i,
72
- script: 'recall.js',
73
- args: [],
74
- label: 'recall',
75
- },
76
-
77
52
  // echo 전용 (stdout으로 직접 출력)
78
53
  {
79
54
  pattern: /e2e.*테스트|e2e.*test|playwright|브라우저.*테스트|browser.*test/i,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@su-record/vibe",
3
- "version": "2.7.0",
3
+ "version": "2.7.1",
4
4
  "description": "AI Coding Framework for Claude Code — 49 agents, 41+ tools, multi-LLM orchestration",
5
5
  "type": "module",
6
6
  "main": "dist/cli/index.js",
@@ -1,234 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import os from 'os';
5
- import {
6
- loadSkillMetadataOnly,
7
- loadSkillBody,
8
- scoreSkillMatch,
9
- listSkillResources,
10
- parseSkillFrontmatter,
11
- } from '../skill-injector.js';
12
-
13
- // ============================================
14
- // loadSkillMetadataOnly
15
- // ============================================
16
-
17
- describe('loadSkillMetadataOnly', () => {
18
- const tmpDir = path.join(os.tmpdir(), 'skill-injector-test-' + Date.now());
19
-
20
- beforeEach(() => {
21
- if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
22
- });
23
-
24
- it('should parse frontmatter with string, array, and number fields', () => {
25
- const skillPath = path.join(tmpDir, 'test-skill.md');
26
- fs.writeFileSync(skillPath, `---
27
- name: test-skill
28
- description: A test skill
29
- triggers: [build, compile, deploy]
30
- tier: 2
31
- maxBodyTokens: 500
32
- ---
33
- # Body content here
34
- `);
35
-
36
- const meta = loadSkillMetadataOnly(skillPath);
37
- expect(meta).not.toBeNull();
38
- expect(meta.name).toBe('test-skill');
39
- expect(meta.description).toBe('A test skill');
40
- expect(meta.triggers).toEqual(['build', 'compile', 'deploy']);
41
- expect(meta.tier).toBe(2);
42
- expect(meta.maxBodyTokens).toBe(500);
43
- });
44
-
45
- it('should return null for non-frontmatter files', () => {
46
- const skillPath = path.join(tmpDir, 'no-frontmatter.md');
47
- fs.writeFileSync(skillPath, '# Just a markdown file\nNo frontmatter here.');
48
-
49
- const meta = loadSkillMetadataOnly(skillPath);
50
- expect(meta).toBeNull();
51
- });
52
-
53
- it('should return null for nonexistent files', () => {
54
- const meta = loadSkillMetadataOnly(path.join(tmpDir, 'nonexistent.md'));
55
- expect(meta).toBeNull();
56
- });
57
-
58
- it('should parse quoted string values', () => {
59
- const skillPath = path.join(tmpDir, 'quoted.md');
60
- fs.writeFileSync(skillPath, `---
61
- name: "quoted-skill"
62
- description: 'single quoted'
63
- ---
64
- body
65
- `);
66
-
67
- const meta = loadSkillMetadataOnly(skillPath);
68
- expect(meta.name).toBe('quoted-skill');
69
- expect(meta.description).toBe('single quoted');
70
- });
71
-
72
- it('should parse os array field', () => {
73
- const skillPath = path.join(tmpDir, 'os-skill.md');
74
- fs.writeFileSync(skillPath, `---
75
- name: platform-specific
76
- os: [darwin, linux]
77
- requires: [ffmpeg, imagemagick]
78
- ---
79
- body
80
- `);
81
-
82
- const meta = loadSkillMetadataOnly(skillPath);
83
- expect(meta.os).toEqual(['darwin', 'linux']);
84
- expect(meta.requires).toEqual(['ffmpeg', 'imagemagick']);
85
- });
86
- });
87
-
88
- // ============================================
89
- // loadSkillBody
90
- // ============================================
91
-
92
- describe('loadSkillBody', () => {
93
- const tmpDir = path.join(os.tmpdir(), 'skill-body-test-' + Date.now());
94
-
95
- beforeEach(() => {
96
- if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
97
- });
98
-
99
- it('should extract body after frontmatter', () => {
100
- const skillPath = path.join(tmpDir, 'with-body.md');
101
- fs.writeFileSync(skillPath, `---
102
- name: test
103
- ---
104
- # Skill Body
105
-
106
- Instructions here.`);
107
-
108
- const body = loadSkillBody(skillPath);
109
- expect(body).toBe('# Skill Body\n\nInstructions here.');
110
- });
111
-
112
- it('should return full content if no frontmatter', () => {
113
- const skillPath = path.join(tmpDir, 'no-fm.md');
114
- fs.writeFileSync(skillPath, '# Just content\nNo frontmatter.');
115
-
116
- const body = loadSkillBody(skillPath);
117
- expect(body).toBe('# Just content\nNo frontmatter.');
118
- });
119
-
120
- it('should return empty string for nonexistent files', () => {
121
- const body = loadSkillBody(path.join(tmpDir, 'nope.md'));
122
- expect(body).toBe('');
123
- });
124
- });
125
-
126
- // ============================================
127
- // scoreSkillMatch
128
- // ============================================
129
-
130
- describe('scoreSkillMatch', () => {
131
- it('should return 0 for no triggers', () => {
132
- expect(scoreSkillMatch([], 'build the project')).toBe(0);
133
- expect(scoreSkillMatch(null, 'build the project')).toBe(0);
134
- });
135
-
136
- it('should score partial match (substring)', () => {
137
- const score = scoreSkillMatch(['build'], 'rebuild the project');
138
- expect(score).toBeGreaterThan(0);
139
- });
140
-
141
- it('should score higher for word boundary match', () => {
142
- const partialScore = scoreSkillMatch(['build'], 'rebuild the project');
143
- const exactScore = scoreSkillMatch(['build'], 'build the project');
144
- expect(exactScore).toBeGreaterThan(partialScore);
145
- });
146
-
147
- it('should accumulate scores for multiple trigger matches', () => {
148
- const singleScore = scoreSkillMatch(['build'], 'build and deploy');
149
- const doubleScore = scoreSkillMatch(['build', 'deploy'], 'build and deploy');
150
- expect(doubleScore).toBeGreaterThan(singleScore);
151
- });
152
-
153
- it('should be case insensitive', () => {
154
- const score = scoreSkillMatch(['BUILD'], 'build the project');
155
- expect(score).toBeGreaterThan(0);
156
- });
157
- });
158
-
159
- // ============================================
160
- // listSkillResources
161
- // ============================================
162
-
163
- describe('listSkillResources', () => {
164
- const tmpDir = path.join(os.tmpdir(), 'skill-resources-test-' + Date.now());
165
-
166
- beforeEach(() => {
167
- if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
168
- });
169
-
170
- it('should list files from scripts/, references/, assets/ subdirs', () => {
171
- const scriptsDir = path.join(tmpDir, 'scripts');
172
- const assetsDir = path.join(tmpDir, 'assets');
173
- fs.mkdirSync(scriptsDir, { recursive: true });
174
- fs.mkdirSync(assetsDir, { recursive: true });
175
- fs.writeFileSync(path.join(scriptsDir, 'run.sh'), '#!/bin/bash');
176
- fs.writeFileSync(path.join(assetsDir, 'logo.png'), 'fake-png');
177
-
178
- const resources = listSkillResources(tmpDir);
179
- expect(resources).toContain('scripts/run.sh');
180
- expect(resources).toContain('assets/logo.png');
181
- });
182
-
183
- it('should return empty array for dir without subdirs', () => {
184
- const emptyDir = path.join(tmpDir, 'empty-skill');
185
- fs.mkdirSync(emptyDir, { recursive: true });
186
-
187
- const resources = listSkillResources(emptyDir);
188
- expect(resources).toEqual([]);
189
- });
190
-
191
- it('should skip hidden files', () => {
192
- const scriptsDir = path.join(tmpDir, 'scripts2');
193
- fs.mkdirSync(scriptsDir, { recursive: true });
194
- fs.writeFileSync(path.join(scriptsDir, '.hidden'), 'secret');
195
- fs.writeFileSync(path.join(scriptsDir, 'visible.sh'), 'ok');
196
-
197
- const parentDir = path.join(tmpDir, 'skip-hidden');
198
- fs.mkdirSync(parentDir, { recursive: true });
199
- // Create scripts subdir under parent
200
- const parentScripts = path.join(parentDir, 'scripts');
201
- fs.mkdirSync(parentScripts, { recursive: true });
202
- fs.writeFileSync(path.join(parentScripts, '.hidden'), 'secret');
203
- fs.writeFileSync(path.join(parentScripts, 'visible.sh'), 'ok');
204
-
205
- const resources = listSkillResources(parentDir);
206
- expect(resources).toContain('scripts/visible.sh');
207
- expect(resources).not.toContain('scripts/.hidden');
208
- });
209
- });
210
-
211
- // ============================================
212
- // parseSkillFrontmatter (backward compat)
213
- // ============================================
214
-
215
- describe('parseSkillFrontmatter', () => {
216
- it('should parse frontmatter and return metadata + template', () => {
217
- const content = `---
218
- name: legacy-skill
219
- triggers: [test, verify]
220
- ---
221
- # Legacy body content`;
222
-
223
- const result = parseSkillFrontmatter(content);
224
- expect(result).not.toBeNull();
225
- expect(result.metadata.name).toBe('legacy-skill');
226
- expect(result.metadata.triggers).toEqual(['test', 'verify']);
227
- expect(result.template).toBe('# Legacy body content');
228
- });
229
-
230
- it('should return null for non-frontmatter content', () => {
231
- const result = parseSkillFrontmatter('# No frontmatter');
232
- expect(result).toBeNull();
233
- });
234
- });
@@ -1,101 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Autonomy Controller — /autonomy suggest|auto|disabled|status
4
- *
5
- * config.json의 autonomy.mode를 변경하거나 상태를 표시합니다.
6
- */
7
- import fs from 'fs';
8
- import path from 'path';
9
-
10
- const prompt = process.argv[2] || '';
11
- const match = /^\/autonomy\s+(\w+)/i.exec(prompt);
12
- if (!match) process.exit(0);
13
-
14
- const action = match[1].toLowerCase();
15
- const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
16
- const configPath = path.join(PROJECT_DIR, '.claude', 'vibe', 'config.json');
17
-
18
- function loadConfig() {
19
- try {
20
- if (fs.existsSync(configPath)) {
21
- return JSON.parse(fs.readFileSync(configPath, 'utf8'));
22
- }
23
- } catch { /* ignore */ }
24
- return {};
25
- }
26
-
27
- function saveConfig(config) {
28
- try {
29
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
30
- } catch (e) {
31
- process.stderr.write(`[Autonomy] Config save error: ${e.message}\n`);
32
- }
33
- }
34
-
35
- const VALID_MODES = ['suggest', 'auto', 'disabled'];
36
-
37
- if (VALID_MODES.includes(action)) {
38
- const config = loadConfig();
39
- if (!config.autonomy) config.autonomy = {};
40
- config.autonomy.mode = action;
41
- saveConfig(config);
42
-
43
- const modeDesc = {
44
- suggest: 'Suggest mode — 분해 결과만 표시, 사용자 승인 후 실행',
45
- auto: 'Auto mode — LOW/MEDIUM 자동 실행, HIGH는 오너 확인',
46
- disabled: 'Disabled — TaskDecomposer 비활성화',
47
- };
48
-
49
- console.log(`🤖 Autonomy mode → ${action}`);
50
- console.log(` ${modeDesc[action]}`);
51
- } else if (action === 'status') {
52
- const config = loadConfig();
53
- const mode = config.autonomy?.mode || 'suggest';
54
- const sentinelEnabled = config.sentinel?.enabled !== false;
55
- const proactiveEnabled = config.autonomy?.proactive?.enabled !== false;
56
- const timeout = config.sentinel?.confirmationTimeout || 300;
57
- const maxSteps = config.autonomy?.maxConcurrentSteps || 3;
58
-
59
- console.log(`🤖 Agent Autonomy: ${mode} mode`);
60
- console.log(` 🛡️ Sentinel: ${sentinelEnabled ? 'enabled' : 'disabled'}`);
61
- console.log(` 💡 Proactive: ${proactiveEnabled ? 'enabled' : 'disabled'}`);
62
- console.log(` ⏱️ Confirmation timeout: ${timeout}s`);
63
- console.log(` 🔄 Max concurrent steps: ${maxSteps}`);
64
-
65
- // DB 통계 (가능하면)
66
- try {
67
- const { getLibBaseUrl } = await import('./utils.js');
68
- const LIB_BASE = getLibBaseUrl();
69
- const memMod = await import(`${LIB_BASE}memory/MemoryStorage.js`);
70
- const storage = new memMod.MemoryStorage(PROJECT_DIR);
71
- const db = storage.getDatabase();
72
-
73
- const last24h = new Date(Date.now() - 86_400_000).toISOString();
74
-
75
- let totalActions = 0;
76
- let blockedActions = 0;
77
- let pendingSuggestions = 0;
78
- let pendingConfirmations = 0;
79
-
80
- try {
81
- totalActions = db.prepare('SELECT COUNT(*) as c FROM audit_events WHERE createdAt >= ?').get(last24h)?.c ?? 0;
82
- blockedActions = db.prepare("SELECT COUNT(*) as c FROM audit_events WHERE createdAt >= ? AND outcome = 'blocked'").get(last24h)?.c ?? 0;
83
- } catch { /* table may not exist */ }
84
-
85
- try {
86
- pendingSuggestions = db.prepare("SELECT COUNT(*) as c FROM suggestions WHERE status = 'pending'").get()?.c ?? 0;
87
- } catch { /* table may not exist */ }
88
-
89
- try {
90
- pendingConfirmations = db.prepare("SELECT COUNT(*) as c FROM confirmations WHERE status = 'pending'").get()?.c ?? 0;
91
- } catch { /* table may not exist */ }
92
-
93
- if (totalActions > 0 || pendingSuggestions > 0 || pendingConfirmations > 0) {
94
- console.log(` 📊 Last 24h: ${totalActions} actions (${totalActions - blockedActions} allowed, ${blockedActions} blocked)`);
95
- if (pendingSuggestions > 0) console.log(` 💡 ${pendingSuggestions} pending suggestions`);
96
- if (pendingConfirmations > 0) console.log(` ⏳ ${pendingConfirmations} pending confirmations`);
97
- }
98
-
99
- storage.close();
100
- } catch { /* DB not available, skip stats */ }
101
- }
@@ -1,22 +0,0 @@
1
- /**
2
- * UserPromptSubmit Hook - 코드 리뷰 요청 시 품질 검사
3
- */
4
- import { getToolsBaseUrl, PROJECT_DIR } from './utils.js';
5
-
6
- const BASE_URL = getToolsBaseUrl();
7
-
8
- async function main() {
9
- try {
10
- const module = await import(`${BASE_URL}convention/index.js`);
11
- const result = await module.validateCodeQuality({
12
- targetPath: '.',
13
- projectPath: PROJECT_DIR,
14
- });
15
- const lines = result.content[0].text.split('\n').slice(0, 5).join(' | ');
16
- console.log('[CODE REVIEW]', lines);
17
- } catch (e) {
18
- console.log('[CODE REVIEW] Error:', e.message);
19
- }
20
- }
21
-
22
- main();
@@ -1,22 +0,0 @@
1
- /**
2
- * UserPromptSubmit Hook - 복잡도 분석
3
- */
4
- import { getToolsBaseUrl, PROJECT_DIR } from './utils.js';
5
-
6
- const BASE_URL = getToolsBaseUrl();
7
-
8
- async function main() {
9
- try {
10
- const module = await import(`${BASE_URL}convention/index.js`);
11
- const result = await module.analyzeComplexity({
12
- targetPath: '.',
13
- projectPath: PROJECT_DIR,
14
- });
15
- const lines = result.content[0].text.split('\n').slice(0, 5).join(' | ');
16
- console.log('[COMPLEXITY]', lines);
17
- } catch (e) {
18
- console.log('[COMPLEXITY] Error:', e.message);
19
- }
20
- }
21
-
22
- main();
@@ -1,23 +0,0 @@
1
- /**
2
- * UserPromptSubmit Hook - 버그 해결/PR 머지 시 솔루션 저장
3
- */
4
- import { getToolsBaseUrl, PROJECT_DIR } from './utils.js';
5
-
6
- const BASE_URL = getToolsBaseUrl();
7
-
8
- async function main() {
9
- try {
10
- const module = await import(`${BASE_URL}memory/index.js`);
11
- const result = await module.saveMemory({
12
- key: `solution-${Date.now()}`,
13
- value: `Solution documented at ${new Date().toISOString()}`,
14
- category: 'solution',
15
- projectPath: PROJECT_DIR,
16
- });
17
- console.log('[COMPOUND]', result.content[0].text);
18
- } catch (e) {
19
- console.log('[COMPOUND] ✗ Error:', e.message);
20
- }
21
- }
22
-
23
- main();
@@ -1,101 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Evolution Engine Hook - PostToolUse (Write/Edit)
4
- * Triggers insight extraction and optional generation every N tool uses.
5
- * Fire-and-forget: hook returns immediately, processing runs in background.
6
- */
7
- import { getToolsBaseUrl, getLibBaseUrl, PROJECT_DIR } from './utils.js';
8
- import fs from 'fs';
9
- import path from 'path';
10
- import os from 'os';
11
-
12
- // Throttle: only run every 5th tool use per session
13
- let toolUseCount = 0;
14
- const TRIGGER_EVERY = 5;
15
-
16
- // Read config
17
- function getEvolutionConfig() {
18
- try {
19
- const configPath = path.join(PROJECT_DIR, '.claude', 'vibe', 'config.json');
20
- if (fs.existsSync(configPath)) {
21
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
22
- return config.evolution || {};
23
- }
24
- } catch { /* ignore */ }
25
- return {};
26
- }
27
-
28
- // stdin에서 hook input 읽기
29
- let inputData = '';
30
- for await (const chunk of process.stdin) {
31
- inputData += chunk;
32
- }
33
-
34
- let hookInput;
35
- try {
36
- hookInput = JSON.parse(inputData);
37
- } catch {
38
- process.exit(0);
39
- }
40
-
41
- // Only trigger on Write/Edit tool completions
42
- const toolName = hookInput?.tool_name || '';
43
- if (toolName !== 'Write' && toolName !== 'Edit') {
44
- process.exit(0);
45
- }
46
-
47
- toolUseCount++;
48
- if (toolUseCount % TRIGGER_EVERY !== 0) {
49
- process.exit(0);
50
- }
51
-
52
- // Check if evolution is enabled
53
- const config = getEvolutionConfig();
54
- if (config.enabled === false) {
55
- process.exit(0);
56
- }
57
-
58
- // Fire-and-forget: schedule background processing
59
- setImmediate(async () => {
60
- try {
61
- const LIB_BASE = getLibBaseUrl();
62
- const [memModule, insightModule, orchestratorModule] = await Promise.all([
63
- import(`${LIB_BASE}memory/MemoryStorage.js`),
64
- import(`${LIB_BASE}evolution/InsightExtractor.js`),
65
- import(`${LIB_BASE}evolution/EvolutionOrchestrator.js`),
66
- ]);
67
-
68
- const storage = new memModule.MemoryStorage(PROJECT_DIR);
69
- const extractor = new insightModule.InsightExtractor(storage);
70
-
71
- // Step 1: Extract insights
72
- const result = extractor.extractFromRecent(20);
73
-
74
- if (result.newInsights.length > 0) {
75
- const mode = config.mode || 'suggest';
76
-
77
- // Step 2: Generate if new insights found
78
- const orchestrator = new orchestratorModule.EvolutionOrchestrator(storage, {
79
- mode,
80
- maxGenerationsPerCycle: config.maxGenerationsPerCycle || 5,
81
- minQualityScore: config.minQualityScore || 60,
82
- });
83
-
84
- const genResult = orchestrator.generate();
85
-
86
- if (mode === 'suggest' && genResult.generated.length > 0) {
87
- // Notify user about new candidates
88
- process.stderr.write(
89
- `[Evolution] New candidates: ${genResult.generated.length} generated, ` +
90
- `${genResult.rejected.length} rejected\n`
91
- );
92
- }
93
- }
94
-
95
- storage.close();
96
- } catch (error) {
97
- process.stderr.write(
98
- `[Evolution] Background error: ${error instanceof Error ? error.message : 'Unknown'}\n`
99
- );
100
- }
101
- });