@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 +4 -5
- package/hooks/scripts/post-edit.js +18 -48
- package/hooks/scripts/prompt-dispatcher.js +0 -25
- package/package.json +1 -1
- package/hooks/scripts/__tests__/skill-injector.test.js +0 -234
- package/hooks/scripts/autonomy-controller.js +0 -101
- package/hooks/scripts/code-review.js +0 -22
- package/hooks/scripts/complexity.js +0 -22
- package/hooks/scripts/compound.js +0 -23
- package/hooks/scripts/evolution-engine.js +0 -101
- package/hooks/scripts/hud-multiline.js +0 -262
- package/hooks/scripts/post-tool-verify.js +0 -210
- package/hooks/scripts/recall.js +0 -22
- package/hooks/scripts/skill-injector.js +0 -654
- package/hooks/scripts/skill-requirements.js +0 -83
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
|
|
198
|
-
| UserPromptSubmit | `prompt-dispatcher.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 {
|
|
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
|
-
|
|
12
|
-
const
|
|
10
|
+
const CONSOLE_LOG_RE = /console\.log/;
|
|
11
|
+
const CODE_EXT_RE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// 경로 검증: resolve로 정규화
|
|
17
|
+
if (filePath && CODE_EXT_RE.test(filePath)) {
|
|
24
18
|
const resolved = path.resolve(filePath);
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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,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
|
-
});
|