@su-record/vibe 2.12.5 → 2.14.0
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 +25 -16
- package/README.en.md +16 -14
- package/README.md +13 -11
- package/dist/cli/postinstall/constants.d.ts.map +1 -1
- package/dist/cli/postinstall/constants.js +1 -0
- package/dist/cli/postinstall/constants.js.map +1 -1
- package/dist/cli/postinstall/fs-utils.d.ts +23 -0
- package/dist/cli/postinstall/fs-utils.d.ts.map +1 -1
- package/dist/cli/postinstall/fs-utils.js +71 -0
- package/dist/cli/postinstall/fs-utils.js.map +1 -1
- package/dist/cli/postinstall/fs-utils.test.js +69 -1
- package/dist/cli/postinstall/fs-utils.test.js.map +1 -1
- package/dist/cli/postinstall/main.d.ts.map +1 -1
- package/dist/cli/postinstall/main.js +12 -2
- package/dist/cli/postinstall/main.js.map +1 -1
- package/dist/cli/setup/CodexHooks.test.js +27 -0
- package/dist/cli/setup/CodexHooks.test.js.map +1 -1
- package/dist/cli/setup/ProjectSetup.d.ts.map +1 -1
- package/dist/cli/setup/ProjectSetup.js +6 -5
- package/dist/cli/setup/ProjectSetup.js.map +1 -1
- package/dist/infra/lib/DecisionTracer.d.ts +4 -0
- package/dist/infra/lib/DecisionTracer.d.ts.map +1 -1
- package/dist/infra/lib/DecisionTracer.js +4 -0
- package/dist/infra/lib/DecisionTracer.js.map +1 -1
- package/dist/infra/lib/LoopBreaker.d.ts +4 -0
- package/dist/infra/lib/LoopBreaker.d.ts.map +1 -1
- package/dist/infra/lib/LoopBreaker.js +4 -0
- package/dist/infra/lib/LoopBreaker.js.map +1 -1
- package/dist/infra/lib/ReviewRace.d.ts +4 -0
- package/dist/infra/lib/ReviewRace.d.ts.map +1 -1
- package/dist/infra/lib/ReviewRace.js +4 -0
- package/dist/infra/lib/ReviewRace.js.map +1 -1
- package/dist/infra/lib/SkillQualityGate.d.ts +4 -0
- package/dist/infra/lib/SkillQualityGate.d.ts.map +1 -1
- package/dist/infra/lib/SkillQualityGate.js +4 -0
- package/dist/infra/lib/SkillQualityGate.js.map +1 -1
- package/dist/infra/lib/UltraQA.d.ts +4 -0
- package/dist/infra/lib/UltraQA.d.ts.map +1 -1
- package/dist/infra/lib/UltraQA.js +4 -0
- package/dist/infra/lib/UltraQA.js.map +1 -1
- package/dist/infra/lib/VerificationLoop.d.ts +4 -0
- package/dist/infra/lib/VerificationLoop.d.ts.map +1 -1
- package/dist/infra/lib/VerificationLoop.js +4 -0
- package/dist/infra/lib/VerificationLoop.js.map +1 -1
- package/dist/infra/orchestrator/index.d.ts.map +1 -1
- package/dist/infra/orchestrator/index.js +1 -3
- package/dist/infra/orchestrator/index.js.map +1 -1
- package/dist/infra/orchestrator/parallelResearch.d.ts.map +1 -1
- package/dist/infra/orchestrator/parallelResearch.js +1 -4
- package/dist/infra/orchestrator/parallelResearch.js.map +1 -1
- package/dist/tools/convention/validateCodeQuality.d.ts.map +1 -1
- package/dist/tools/convention/validateCodeQuality.js +5 -4
- package/dist/tools/convention/validateCodeQuality.js.map +1 -1
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/loop/index.d.ts +6 -0
- package/dist/tools/loop/index.d.ts.map +1 -0
- package/dist/tools/loop/index.js +5 -0
- package/dist/tools/loop/index.js.map +1 -0
- package/dist/tools/loop/validateLoopDefinition.d.ts +38 -0
- package/dist/tools/loop/validateLoopDefinition.d.ts.map +1 -0
- package/dist/tools/loop/validateLoopDefinition.js +224 -0
- package/dist/tools/loop/validateLoopDefinition.js.map +1 -0
- package/dist/tools/loop/validateLoopDefinition.test.d.ts +14 -0
- package/dist/tools/loop/validateLoopDefinition.test.d.ts.map +1 -0
- package/dist/tools/loop/validateLoopDefinition.test.js +229 -0
- package/dist/tools/loop/validateLoopDefinition.test.js.map +1 -0
- package/dist/tools/spec/traceabilityMatrix.d.ts +2 -0
- package/dist/tools/spec/traceabilityMatrix.d.ts.map +1 -1
- package/dist/tools/spec/traceabilityMatrix.js +50 -1
- package/dist/tools/spec/traceabilityMatrix.js.map +1 -1
- package/dist/tools/spec/traceabilityMatrix.path-resolution.test.d.ts +10 -0
- package/dist/tools/spec/traceabilityMatrix.path-resolution.test.d.ts.map +1 -0
- package/dist/tools/spec/traceabilityMatrix.path-resolution.test.js +89 -0
- package/dist/tools/spec/traceabilityMatrix.path-resolution.test.js.map +1 -0
- package/dist/tools/spec/traceabilityMatrix.test.js +19 -0
- package/dist/tools/spec/traceabilityMatrix.test.js.map +1 -1
- package/hooks/hooks.json +1 -0
- package/hooks/scripts/__tests__/.vibe/command-log.txt +60 -0
- package/hooks/scripts/__tests__/.vibe/memories/memories.db +0 -0
- package/hooks/scripts/__tests__/.vibe/memories/memories.db-shm +0 -0
- package/hooks/scripts/__tests__/.vibe/memories/memories.db-wal +0 -0
- package/hooks/scripts/__tests__/auto-test-debounce.test.js +145 -0
- package/hooks/scripts/__tests__/code-check-detectors.test.js +155 -0
- package/hooks/scripts/__tests__/dispatcher-inprocess.test.js +99 -0
- package/hooks/scripts/__tests__/keyword-detector.test.js +26 -18
- package/hooks/scripts/__tests__/loop-ledger.test.js +321 -0
- package/hooks/scripts/__tests__/post-edit-dispatcher.test.js +139 -0
- package/hooks/scripts/__tests__/pre-tool-guard.test.js +115 -1
- package/hooks/scripts/__tests__/run-ledger-verify-required.test.js +146 -0
- package/hooks/scripts/__tests__/run-ledger.test.js +330 -0
- package/hooks/scripts/__tests__/scope-from-spec.test.js +215 -0
- package/hooks/scripts/__tests__/sentinel-guard.test.js +79 -24
- package/hooks/scripts/__tests__/step-counter.test.js +95 -15
- package/hooks/scripts/__tests__/utils-npm-root.test.js +98 -0
- package/hooks/scripts/auto-commit.js +27 -1
- package/hooks/scripts/auto-format.js +85 -20
- package/hooks/scripts/auto-test.js +187 -37
- package/hooks/scripts/code-check.js +286 -90
- package/hooks/scripts/codex-hook-adapter.js +12 -1
- package/hooks/scripts/command-log.js +26 -16
- package/hooks/scripts/keyword-detector.js +22 -22
- package/hooks/scripts/lib/dispatcher.js +38 -0
- package/hooks/scripts/lib/hook-context.js +130 -0
- package/hooks/scripts/lib/loop-ledger.js +118 -0
- package/hooks/scripts/lib/pr-gate-runner.js +62 -0
- package/hooks/scripts/lib/run-ledger.js +169 -0
- package/hooks/scripts/lib/scope-from-spec.js +40 -7
- package/hooks/scripts/loop-ledger.js +56 -0
- package/hooks/scripts/post-edit-dispatcher.js +93 -20
- package/hooks/scripts/post-edit.js +40 -19
- package/hooks/scripts/pr-test-gate.js +8 -37
- package/hooks/scripts/pre-tool-dispatcher.js +18 -16
- package/hooks/scripts/pre-tool-guard.js +55 -52
- package/hooks/scripts/prompt-dispatcher.js +10 -0
- package/hooks/scripts/scope-guard.js +40 -39
- package/hooks/scripts/sentinel-guard.js +41 -41
- package/hooks/scripts/session-start.js +13 -1
- package/hooks/scripts/step-counter.js +100 -7
- package/hooks/scripts/stop-dispatcher.js +26 -0
- package/hooks/scripts/utils.js +63 -21
- package/hooks/scripts/verify-ledger.js +22 -0
- package/package.json +2 -2
- package/skills/spec/references/templates.md +11 -6
- package/skills/vibe/SKILL.md +40 -23
- package/skills/vibe.loop/SKILL.md +116 -0
- package/skills/vibe.run/SKILL.md +153 -1686
- package/skills/vibe.run/references/brand-assets.md +59 -0
- package/skills/vibe.run/references/parallel-agents.md +326 -0
- package/skills/vibe.run/references/race-review.md +272 -0
- package/skills/vibe.run/references/ralph-loop.md +173 -0
- package/skills/vibe.run/references/ultrawork-mode.md +151 -0
- package/skills/vibe.trace/SKILL.md +25 -38
- package/skills/vibe.verify/SKILL.md +15 -0
- package/vibe/rules/loop-contract.md +54 -0
- package/vibe/templates/loop-template.md +69 -0
- package/hooks/scripts/figma-guard.js +0 -219
|
@@ -1,27 +1,115 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PostToolUse Hook - Write/Edit 후 코드 품질 검사 + 관찰 자동 캡처
|
|
3
|
+
*
|
|
4
|
+
* findings를 console.log가 아닌 반환값으로 전달 — 디스패처가 수집해 additionalContext에 주입.
|
|
5
|
+
* P1 이슈 발견 시 run-ledger에 verifyRequired 상태 기록.
|
|
3
6
|
*/
|
|
4
|
-
import { getToolsBaseUrl, PROJECT_DIR } from './utils.js';
|
|
7
|
+
import { getToolsBaseUrl, PROJECT_DIR, readProjectConfig } from './utils.js';
|
|
5
8
|
import { readFileSync, existsSync, writeFileSync } from 'fs';
|
|
6
9
|
import path from 'path';
|
|
7
10
|
import os from 'os';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
process.on('unhandledRejection', () => {});
|
|
11
|
+
import { buildCliCtx, isDirectRun } from './lib/hook-context.js';
|
|
12
|
+
import { recordVerifyRequired } from './lib/run-ledger.js';
|
|
11
13
|
|
|
12
14
|
const BASE_URL = getToolsBaseUrl();
|
|
13
15
|
|
|
16
|
+
// P1 이슈 판단 기준: .ts/.tsx 파일에서만 적용
|
|
17
|
+
const P1_DETECTORS = [
|
|
18
|
+
// `: any` — 타입 어노테이션
|
|
19
|
+
/:\s*any\b/,
|
|
20
|
+
// `as any` — 타입 캐스트
|
|
21
|
+
/\bas\s+any\b/,
|
|
22
|
+
// `<any>` — 제네릭 any (단, JSX 태그 제외 목적으로 뒤에 공백/쉼표/> 허용)
|
|
23
|
+
/<any[\s,>]/,
|
|
24
|
+
// @ts-ignore
|
|
25
|
+
/@ts-ignore\b/,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const TS_EXT_RE = /\.(ts|tsx)$/;
|
|
29
|
+
const CODE_EXT_RE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
|
|
30
|
+
|
|
31
|
+
// console.log 기본 허용 경로 (glob 패턴 → 정규식으로 변환)
|
|
32
|
+
const DEFAULT_CONSOLE_ALLOW_GLOBS = [
|
|
33
|
+
'hooks/scripts/**',
|
|
34
|
+
'scripts/**',
|
|
35
|
+
'**/cli/**',
|
|
36
|
+
'**/*.test.*',
|
|
37
|
+
'**/*.spec.*',
|
|
38
|
+
'**/__tests__/**',
|
|
39
|
+
];
|
|
40
|
+
|
|
14
41
|
/**
|
|
15
|
-
*
|
|
42
|
+
* 경량 glob → RegExp 변환 (scope-guard와 동일 알고리즘, 독립 복사본).
|
|
43
|
+
* @param {string} glob
|
|
44
|
+
* @returns {RegExp}
|
|
16
45
|
*/
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
46
|
+
function globToRegExp(glob) {
|
|
47
|
+
const normalized = glob.replace(/\\/g, '/');
|
|
48
|
+
let out = '';
|
|
49
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
50
|
+
const c = normalized[i];
|
|
51
|
+
if (c === '*') {
|
|
52
|
+
if (normalized[i + 1] === '*') {
|
|
53
|
+
out += '.*';
|
|
54
|
+
i++;
|
|
55
|
+
if (normalized[i + 1] === '/') i++;
|
|
56
|
+
} else {
|
|
57
|
+
out += '[^/]*';
|
|
58
|
+
}
|
|
59
|
+
} else if (c === '?') {
|
|
60
|
+
out += '[^/]';
|
|
61
|
+
} else if ('.+^$()|{}[]\\'.includes(c)) {
|
|
62
|
+
out += '\\' + c;
|
|
63
|
+
} else {
|
|
64
|
+
out += c;
|
|
24
65
|
}
|
|
66
|
+
}
|
|
67
|
+
return new RegExp('^' + out + '$');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* .vibe/config.json의 qualityCheck.consoleAllow 글로브 목록 로드.
|
|
72
|
+
* 기본 글로브와 병합하여 반환.
|
|
73
|
+
* @returns {RegExp[]}
|
|
74
|
+
*/
|
|
75
|
+
function loadConsoleAllowPatterns() {
|
|
76
|
+
try {
|
|
77
|
+
const cfg = readProjectConfig();
|
|
78
|
+
const extra = cfg?.qualityCheck?.consoleAllow;
|
|
79
|
+
const globs = Array.isArray(extra)
|
|
80
|
+
? [...DEFAULT_CONSOLE_ALLOW_GLOBS, ...extra]
|
|
81
|
+
: DEFAULT_CONSOLE_ALLOW_GLOBS;
|
|
82
|
+
return globs.map(g => globToRegExp(g));
|
|
83
|
+
} catch {
|
|
84
|
+
return DEFAULT_CONSOLE_ALLOW_GLOBS.map(g => globToRegExp(g));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 파일 경로가 console.log 허용 경로인지 판단.
|
|
90
|
+
* @param {string} filePath - 절대 또는 프로젝트 상대 경로
|
|
91
|
+
* @returns {boolean}
|
|
92
|
+
*/
|
|
93
|
+
function isConsoleAllowed(filePath) {
|
|
94
|
+
try {
|
|
95
|
+
const rel = path.relative(PROJECT_DIR, path.resolve(filePath)).replace(/\\/g, '/');
|
|
96
|
+
const patterns = loadConsoleAllowPatterns();
|
|
97
|
+
return patterns.some(re => re.test(rel));
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* hook input에서 수정된 파일 경로 추출.
|
|
105
|
+
* @param {object} ctx
|
|
106
|
+
* @returns {string[]}
|
|
107
|
+
*/
|
|
108
|
+
function getModifiedFiles(ctx) {
|
|
109
|
+
try {
|
|
110
|
+
const parsed = ctx.payload || (ctx.hookInput ? JSON.parse(ctx.hookInput) : null);
|
|
111
|
+
const filePath = parsed?.tool_input?.file_path || parsed?.tool_input?.path;
|
|
112
|
+
return filePath ? [filePath] : [];
|
|
25
113
|
} catch {
|
|
26
114
|
// ignore
|
|
27
115
|
}
|
|
@@ -30,6 +118,8 @@ function getModifiedFiles() {
|
|
|
30
118
|
|
|
31
119
|
/**
|
|
32
120
|
* 파일 확장자/경로로 관찰 타입 분류
|
|
121
|
+
* @param {string[]} files
|
|
122
|
+
* @returns {{ type: string, title: string }}
|
|
33
123
|
*/
|
|
34
124
|
function classifyObservation(files) {
|
|
35
125
|
const hasTest = files.some(f => /\.(test|spec)\.[jt]sx?$/.test(f) || /\/__tests__\//.test(f));
|
|
@@ -41,16 +131,44 @@ function classifyObservation(files) {
|
|
|
41
131
|
}
|
|
42
132
|
|
|
43
133
|
/**
|
|
44
|
-
*
|
|
134
|
+
* P1: any 타입 탐지 — .ts/.tsx 전용, 단어 경계 기반.
|
|
135
|
+
* @param {string[]} lines
|
|
136
|
+
* @returns {Array<{ line: number, match: string, severity: 'P1' }>}
|
|
45
137
|
*/
|
|
46
138
|
function detectAnyType(lines) {
|
|
47
139
|
const findings = [];
|
|
48
140
|
lines.forEach((line, i) => {
|
|
49
|
-
|
|
141
|
+
for (const re of P1_DETECTORS) {
|
|
142
|
+
if (re.test(line)) {
|
|
143
|
+
findings.push({
|
|
144
|
+
line: i + 1,
|
|
145
|
+
match: line.trim(),
|
|
146
|
+
severity: 'P1',
|
|
147
|
+
suggestion: "Replace with: unknown + type guard pattern: if (typeof x === 'string') { ... }",
|
|
148
|
+
});
|
|
149
|
+
break; // 한 줄에 여러 패턴이 있어도 중복 발견 방지
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
return findings;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* P1: console.log 탐지 — 허용 경로가 아닌 곳의 src/ 코드.
|
|
158
|
+
* @param {string[]} lines
|
|
159
|
+
* @param {string} filePath
|
|
160
|
+
* @returns {Array<{ line: number, match: string, severity: 'P1' }>}
|
|
161
|
+
*/
|
|
162
|
+
function detectConsoleLogs(lines, filePath) {
|
|
163
|
+
if (isConsoleAllowed(filePath)) return [];
|
|
164
|
+
const findings = [];
|
|
165
|
+
lines.forEach((line, i) => {
|
|
166
|
+
if (/console\.log\(/.test(line)) {
|
|
50
167
|
findings.push({
|
|
51
168
|
line: i + 1,
|
|
52
169
|
match: line.trim(),
|
|
53
|
-
|
|
170
|
+
severity: 'P1',
|
|
171
|
+
suggestion: 'Remove or replace with debugLog utility',
|
|
54
172
|
});
|
|
55
173
|
}
|
|
56
174
|
});
|
|
@@ -58,7 +176,9 @@ function detectAnyType(lines) {
|
|
|
58
176
|
}
|
|
59
177
|
|
|
60
178
|
/**
|
|
61
|
-
*
|
|
179
|
+
* P2: 함수 길이 초과 탐지.
|
|
180
|
+
* @param {string[]} lines
|
|
181
|
+
* @returns {Array<{ line: number, match: string, severity: 'P2' }>}
|
|
62
182
|
*/
|
|
63
183
|
function detectLongFunctions(lines) {
|
|
64
184
|
const findings = [];
|
|
@@ -80,7 +200,8 @@ function detectLongFunctions(lines) {
|
|
|
80
200
|
findings.push({
|
|
81
201
|
line: fnStart + 1,
|
|
82
202
|
match: `function '${fnName}' is ${length} lines`,
|
|
83
|
-
|
|
203
|
+
severity: 'P2',
|
|
204
|
+
suggestion: `Extract lines ${fnStart + 20}–${i + 1} into a separate helper function`,
|
|
84
205
|
});
|
|
85
206
|
}
|
|
86
207
|
fnStart = -1;
|
|
@@ -91,7 +212,9 @@ function detectLongFunctions(lines) {
|
|
|
91
212
|
}
|
|
92
213
|
|
|
93
214
|
/**
|
|
94
|
-
*
|
|
215
|
+
* P2: 중첩 깊이 초과 탐지.
|
|
216
|
+
* @param {string[]} lines
|
|
217
|
+
* @returns {Array<{ line: number, match: string, severity: 'P2' }>}
|
|
95
218
|
*/
|
|
96
219
|
function detectDeepNesting(lines) {
|
|
97
220
|
const findings = [];
|
|
@@ -104,7 +227,8 @@ function detectDeepNesting(lines) {
|
|
|
104
227
|
findings.push({
|
|
105
228
|
line: i + 1,
|
|
106
229
|
match: `nesting depth ${depth} at line ${i + 1}`,
|
|
107
|
-
|
|
230
|
+
severity: 'P2',
|
|
231
|
+
suggestion: 'Use early return pattern: if (!condition) return; — instead of wrapping in else',
|
|
108
232
|
});
|
|
109
233
|
reported = true;
|
|
110
234
|
}
|
|
@@ -113,70 +237,50 @@ function detectDeepNesting(lines) {
|
|
|
113
237
|
return findings;
|
|
114
238
|
}
|
|
115
239
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
*/
|
|
119
|
-
function detectConsoleLogs(lines) {
|
|
120
|
-
const findings = [];
|
|
121
|
-
lines.forEach((line, i) => {
|
|
122
|
-
if (/console\.log\(/.test(line)) {
|
|
123
|
-
findings.push({
|
|
124
|
-
line: i + 1,
|
|
125
|
-
match: line.trim(),
|
|
126
|
-
suggestion: 'Remove or replace with debugLog utility'
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
return findings;
|
|
131
|
-
}
|
|
240
|
+
// magic number 무시 값 목록 (0, 1, -1, 2, 10, 100, 1000)
|
|
241
|
+
const MAGIC_NUMBER_IGNORE = new Set(['10', '100', '1000']);
|
|
132
242
|
|
|
133
243
|
/**
|
|
134
|
-
*
|
|
244
|
+
* advisory: 매직 넘버 탐지 (P3, 스팸 방지 적용).
|
|
245
|
+
* 무시 조건: 0/1/-1/2 (단일 digit), ALLCAPS const 선언, 테스트 파일, 배열 인덱스
|
|
246
|
+
* @param {string[]} lines
|
|
247
|
+
* @param {string} filePath
|
|
248
|
+
* @returns {Array<{ line: number, match: string, severity: 'P3' }>}
|
|
135
249
|
*/
|
|
136
|
-
function detectMagicNumbers(lines) {
|
|
250
|
+
function detectMagicNumbers(lines, filePath) {
|
|
251
|
+
// 테스트 파일은 스킵
|
|
252
|
+
if (/\.(test|spec)\.[jt]sx?$/.test(filePath) || /\/__tests__\//.test(filePath)) {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
|
|
137
256
|
const findings = [];
|
|
138
257
|
lines.forEach((line, i) => {
|
|
258
|
+
// ALL_CAPS const 선언 줄은 스킵 (예: const LIMIT = 500)
|
|
259
|
+
if (/^\s*(?:export\s+)?const\s+[A-Z][A-Z0-9_]+\s*=/.test(line)) return;
|
|
260
|
+
|
|
139
261
|
const stripped = line.replace(/\/\/.*$/, '').replace(/(['"`]).*?\1/g, '""');
|
|
140
|
-
const nums = stripped.match(/\b\d{2,}\b/g) || [];
|
|
141
|
-
|
|
262
|
+
const nums = (stripped.match(/\b\d{2,}\b/g) || []).filter(n => MAGIC_NUMBER_IGNORE.has(n));
|
|
263
|
+
// 배열 인덱스: [NN] 패턴 제외
|
|
264
|
+
const nonIndexNums = (stripped.match(/\b\d{2,}\b/g) || []).filter(n => {
|
|
265
|
+
if (MAGIC_NUMBER_IGNORE.has(n)) return false;
|
|
266
|
+
// 배열 인덱스 [NN] 체크
|
|
267
|
+
const idx = stripped.indexOf(n);
|
|
268
|
+
if (idx > 0 && stripped[idx - 1] === '[') return false;
|
|
269
|
+
return true;
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (nonIndexNums.length > 0) {
|
|
142
273
|
findings.push({
|
|
143
274
|
line: i + 1,
|
|
144
|
-
match: `magic number(s): ${
|
|
145
|
-
|
|
275
|
+
match: `magic number(s): ${nonIndexNums.join(', ')}`,
|
|
276
|
+
severity: 'P3',
|
|
277
|
+
suggestion: `Extract to named constant: const LIMIT = ${nonIndexNums[0]};`,
|
|
146
278
|
});
|
|
147
279
|
}
|
|
148
280
|
});
|
|
149
281
|
return findings;
|
|
150
282
|
}
|
|
151
283
|
|
|
152
|
-
/**
|
|
153
|
-
* Run all self-heal detectors and emit [SELF-HEAL] messages
|
|
154
|
-
*/
|
|
155
|
-
function emitSelfHealMessages(filePath) {
|
|
156
|
-
let content;
|
|
157
|
-
try {
|
|
158
|
-
content = readFileSync(filePath, 'utf-8');
|
|
159
|
-
} catch {
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const lines = content.split('\n');
|
|
164
|
-
const detectors = [
|
|
165
|
-
{ label: 'any type detected', fn: detectAnyType },
|
|
166
|
-
{ label: 'function too long', fn: detectLongFunctions },
|
|
167
|
-
{ label: 'nesting too deep', fn: detectDeepNesting },
|
|
168
|
-
{ label: 'console.log found', fn: detectConsoleLogs },
|
|
169
|
-
{ label: 'magic number', fn: detectMagicNumbers },
|
|
170
|
-
];
|
|
171
|
-
|
|
172
|
-
for (const { label, fn } of detectors) {
|
|
173
|
-
const findings = fn(lines).slice(0, 2);
|
|
174
|
-
for (const f of findings) {
|
|
175
|
-
console.log(`[SELF-HEAL] ${label} at line ${f.line} → ${f.suggestion}`);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
284
|
// ─── Failure Escalation Tracking ───
|
|
181
285
|
|
|
182
286
|
const ESCALATION_THRESHOLD = 3;
|
|
@@ -197,6 +301,12 @@ function saveFailureTracker(tracker) {
|
|
|
197
301
|
} catch { /* ignore */ }
|
|
198
302
|
}
|
|
199
303
|
|
|
304
|
+
/**
|
|
305
|
+
* 실패 추적 — 동일 파일에 P1 이슈가 반복되면 에스컬레이션 메시지 반환.
|
|
306
|
+
* @param {string} filePath
|
|
307
|
+
* @param {string[]} issues
|
|
308
|
+
* @returns {string|null} 에스컬레이션 메시지 (없으면 null)
|
|
309
|
+
*/
|
|
200
310
|
function trackFailure(filePath, issues) {
|
|
201
311
|
const tracker = loadFailureTracker();
|
|
202
312
|
const key = filePath;
|
|
@@ -208,14 +318,12 @@ function trackFailure(filePath, issues) {
|
|
|
208
318
|
saveFailureTracker(tracker);
|
|
209
319
|
|
|
210
320
|
if (entry.count >= ESCALATION_THRESHOLD) {
|
|
211
|
-
|
|
212
|
-
console.log(` 이슈: ${entry.issues.join(' | ')}`);
|
|
213
|
-
console.log(' → 사용자 개입 필요 — 자동 수정이 수렴하지 않고 있습니다.');
|
|
214
|
-
console.log(' → 방향을 바꾸거나, 접근 방식을 재검토하세요.');
|
|
215
|
-
// 카운터 리셋 (다음 에스컬레이션까지 다시 3회)
|
|
321
|
+
const msg = `[ESCALATION] ${path.basename(filePath)}: 동일 이슈 ${entry.count}회 반복 — 수동 개입 필요`;
|
|
216
322
|
entry.count = 0;
|
|
217
323
|
saveFailureTracker(tracker);
|
|
324
|
+
return msg;
|
|
218
325
|
}
|
|
326
|
+
return null;
|
|
219
327
|
}
|
|
220
328
|
|
|
221
329
|
function clearFailure(filePath) {
|
|
@@ -224,11 +332,97 @@ function clearFailure(filePath) {
|
|
|
224
332
|
saveFailureTracker(tracker);
|
|
225
333
|
}
|
|
226
334
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
335
|
+
/**
|
|
336
|
+
* 모든 self-heal 탐지기 실행. findings 배열 반환.
|
|
337
|
+
* @param {string} filePath
|
|
338
|
+
* @returns {{ p1: string[], advisory: string[], escalation: string|null }}
|
|
339
|
+
*/
|
|
340
|
+
function runDetectors(filePath) {
|
|
341
|
+
let content;
|
|
342
|
+
try {
|
|
343
|
+
content = readFileSync(filePath, 'utf-8');
|
|
344
|
+
} catch {
|
|
345
|
+
return { p1: [], advisory: [], escalation: null };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const lines = content.split('\n');
|
|
349
|
+
const isTs = TS_EXT_RE.test(filePath);
|
|
350
|
+
|
|
351
|
+
const p1Findings = [];
|
|
352
|
+
const advisoryFindings = [];
|
|
353
|
+
|
|
354
|
+
// P1: any 탐지 — TS 파일만
|
|
355
|
+
if (isTs) {
|
|
356
|
+
const anyHits = detectAnyType(lines).slice(0, 2);
|
|
357
|
+
for (const f of anyHits) {
|
|
358
|
+
p1Findings.push(`P1 any-type line ${f.line}: ${f.match.substring(0, 60)}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// P1: console.log — 허용 경로 제외
|
|
363
|
+
const consoleHits = detectConsoleLogs(lines, filePath).slice(0, 2);
|
|
364
|
+
for (const f of consoleHits) {
|
|
365
|
+
p1Findings.push(`P1 console.log line ${f.line}: ${f.match.substring(0, 60)}`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// P2: 함수 길이
|
|
369
|
+
const fnHits = detectLongFunctions(lines).slice(0, 1);
|
|
370
|
+
for (const f of fnHits) {
|
|
371
|
+
advisoryFindings.push(`P2 ${f.match}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// P2: 중첩 깊이
|
|
375
|
+
const nestHits = detectDeepNesting(lines).slice(0, 1);
|
|
376
|
+
for (const f of nestHits) {
|
|
377
|
+
advisoryFindings.push(`P2 ${f.match}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// P3: 매직 넘버 (advisory, 스팸 방지)
|
|
381
|
+
const magicHits = detectMagicNumbers(lines, filePath).slice(0, 1);
|
|
382
|
+
for (const f of magicHits) {
|
|
383
|
+
advisoryFindings.push(`P3 ${f.match}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
let escalation = null;
|
|
387
|
+
if (p1Findings.length > 0) {
|
|
388
|
+
escalation = trackFailure(filePath, p1Findings);
|
|
389
|
+
} else {
|
|
390
|
+
clearFailure(filePath);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return { p1: p1Findings, advisory: advisoryFindings, escalation };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* in-process 진입점 — 품질 검사 + 관찰 캡처.
|
|
398
|
+
* findings 배열을 반환한다 (디스패처가 수집해 additionalContext에 주입).
|
|
399
|
+
* @param {{ payload: object|null, hookInput: string|null }} ctx
|
|
400
|
+
* @returns {Promise<{ exitCode: number, findings: string[] }>}
|
|
401
|
+
*/
|
|
402
|
+
export async function run(ctx) {
|
|
403
|
+
const findings = [];
|
|
404
|
+
const files = getModifiedFiles(ctx);
|
|
405
|
+
if (files.length === 0) return { exitCode: 0, findings };
|
|
406
|
+
|
|
407
|
+
// 1. Self-heal 탐지기 실행 (changed file only)
|
|
408
|
+
try {
|
|
409
|
+
const { p1, advisory, escalation } = runDetectors(files[0]);
|
|
410
|
+
|
|
411
|
+
for (const msg of p1) findings.push(msg);
|
|
412
|
+
for (const msg of advisory) findings.push(msg);
|
|
413
|
+
if (escalation) findings.push(escalation);
|
|
414
|
+
|
|
415
|
+
// P1 이슈 → run-ledger에 verifyRequired 기록
|
|
416
|
+
if (p1.length > 0) {
|
|
417
|
+
try {
|
|
418
|
+
recordVerifyRequired(PROJECT_DIR, p1[0]);
|
|
419
|
+
} catch { /* fail-open */ }
|
|
420
|
+
}
|
|
421
|
+
} catch {
|
|
422
|
+
// 탐지기 실패 → fail-open, 계속 진행
|
|
423
|
+
}
|
|
230
424
|
|
|
231
|
-
//
|
|
425
|
+
// 2. validateCodeQuality 호출 (P1/P2 필터)
|
|
232
426
|
try {
|
|
233
427
|
const module = await import(`${BASE_URL}convention/index.js`);
|
|
234
428
|
const result = await module.validateCodeQuality({
|
|
@@ -236,20 +430,13 @@ async function main() {
|
|
|
236
430
|
projectPath: PROJECT_DIR,
|
|
237
431
|
});
|
|
238
432
|
const text = result.content[0].text;
|
|
239
|
-
// Output P1/P2 only — skip P3 (style)
|
|
240
433
|
const critical = text.split('\n').filter(l => /\b(error|critical|P1|P2)\b/i.test(l)).slice(0, 3);
|
|
241
|
-
|
|
242
|
-
console.log('[CODE CHECK]', critical.join(' | '));
|
|
243
|
-
trackFailure(files[0], critical);
|
|
244
|
-
} else {
|
|
245
|
-
clearFailure(files[0]);
|
|
246
|
-
}
|
|
247
|
-
emitSelfHealMessages(files[0]);
|
|
434
|
+
for (const line of critical) findings.push(`[CODE CHECK] ${line}`);
|
|
248
435
|
} catch {
|
|
249
|
-
// Silently continue on check failure
|
|
436
|
+
// Silently continue on check failure
|
|
250
437
|
}
|
|
251
438
|
|
|
252
|
-
//
|
|
439
|
+
// 3. 관찰 자동 캡처
|
|
253
440
|
try {
|
|
254
441
|
const memModule = await import(`${BASE_URL}memory/index.js`);
|
|
255
442
|
const { type, title } = classifyObservation(files);
|
|
@@ -261,8 +448,17 @@ async function main() {
|
|
|
261
448
|
projectPath: PROJECT_DIR,
|
|
262
449
|
});
|
|
263
450
|
} catch {
|
|
264
|
-
// 관찰 캡처 실패해도 무시
|
|
451
|
+
// 관찰 캡처 실패해도 무시
|
|
265
452
|
}
|
|
453
|
+
|
|
454
|
+
return { exitCode: 0, findings };
|
|
266
455
|
}
|
|
267
456
|
|
|
268
|
-
|
|
457
|
+
// standalone CLI 모드 (직접 실행 시 — 디스패처 없이)
|
|
458
|
+
if (isDirectRun(import.meta.url)) {
|
|
459
|
+
process.on('uncaughtException', () => {});
|
|
460
|
+
process.on('unhandledRejection', () => {});
|
|
461
|
+
const { exitCode, findings } = await run(buildCliCtx());
|
|
462
|
+
if (findings.length > 0) process.stdout.write(findings.join('\n') + '\n');
|
|
463
|
+
process.exit(exitCode);
|
|
464
|
+
}
|
|
@@ -98,7 +98,18 @@ function handleUserPromptSubmit() {
|
|
|
98
98
|
|
|
99
99
|
function handlePostToolUse() {
|
|
100
100
|
const result = runScript('post-edit-dispatcher.js');
|
|
101
|
-
|
|
101
|
+
const output = combinedOutput(result);
|
|
102
|
+
if (!output) return;
|
|
103
|
+
// 디스패처가 이미 JSON hookSpecificOutput을 출력한 경우 그대로 전달 (이중 래핑 방지).
|
|
104
|
+
// 그 외(plain text)는 Codex 어댑터 표준 방식으로 래핑.
|
|
105
|
+
try {
|
|
106
|
+
const parsed = JSON.parse(output);
|
|
107
|
+
if (parsed?.hookSpecificOutput) {
|
|
108
|
+
writeJson(parsed);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
} catch { /* not JSON — fall through to text wrap */ }
|
|
112
|
+
writeAdditionalContext(output);
|
|
102
113
|
}
|
|
103
114
|
|
|
104
115
|
function handleStop() {
|
|
@@ -7,26 +7,36 @@
|
|
|
7
7
|
import { appendFileSync, mkdirSync, existsSync } from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { PROJECT_DIR, projectVibeRoot } from './utils.js';
|
|
10
|
+
import { buildCliCtx, isDirectRun } from './lib/hook-context.js';
|
|
10
11
|
|
|
11
|
-
const LOG_DIR = projectVibeRoot(PROJECT_DIR);
|
|
12
|
-
const LOG_FILE = path.join(LOG_DIR, 'command-log.txt');
|
|
13
12
|
const MAX_CMD_LENGTH = 500;
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
/**
|
|
15
|
+
* in-process 진입점 — 로깅만 수행, 항상 0 반환 (차단하지 않음).
|
|
16
|
+
* @param {{ toolInput: string }} ctx
|
|
17
|
+
* @returns {Promise<number>}
|
|
18
|
+
*/
|
|
19
|
+
export async function run(ctx) {
|
|
20
|
+
try {
|
|
21
|
+
const input = JSON.parse(ctx.toolInput || '{}');
|
|
22
|
+
const command = input.command || '';
|
|
23
|
+
if (!command) return 0;
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
const timestamp = new Date().toISOString();
|
|
26
|
+
const truncated = command.length > MAX_CMD_LENGTH
|
|
27
|
+
? command.slice(0, MAX_CMD_LENGTH) + '...(truncated)'
|
|
28
|
+
: command;
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
const logDir = projectVibeRoot(PROJECT_DIR);
|
|
31
|
+
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
|
|
32
|
+
appendFileSync(path.join(logDir, 'command-log.txt'), `[${timestamp}] ${truncated}\n`);
|
|
33
|
+
} catch {
|
|
34
|
+
// Never block on logging failure
|
|
35
|
+
}
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
26
38
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// Never block on logging failure
|
|
39
|
+
// standalone CLI 모드
|
|
40
|
+
if (isDirectRun(import.meta.url)) {
|
|
41
|
+
process.exit(await run(buildCliCtx()));
|
|
31
42
|
}
|
|
32
|
-
process.exit(0);
|
|
@@ -8,20 +8,20 @@ import { VIBE_PATH, PROJECT_DIR } from './utils.js';
|
|
|
8
8
|
|
|
9
9
|
// 매직 키워드 정의
|
|
10
10
|
const MAGIC_KEYWORDS = {
|
|
11
|
-
//
|
|
11
|
+
// Deprecated: 기본 루프 동작과 동일 (no-op). exit=coverage-100으로 해석.
|
|
12
12
|
ralph: {
|
|
13
|
-
name: 'Ralph
|
|
14
|
-
description: '
|
|
13
|
+
name: 'Ralph (deprecated alias)',
|
|
14
|
+
description: '[deprecated] Looping to convergence is the default; alias mapped',
|
|
15
15
|
flags: ['persistence', 'verification'],
|
|
16
|
-
output: '[
|
|
16
|
+
output: '[vibe] \'ralph\' is deprecated — looping to convergence is the default; alias mapped.',
|
|
17
17
|
},
|
|
18
18
|
|
|
19
|
-
// 울트라워크 모드 (
|
|
19
|
+
// 울트라워크 모드 (automationLevel: autonomous + 병렬 ACT)
|
|
20
20
|
ultrawork: {
|
|
21
21
|
name: 'Ultrawork',
|
|
22
|
-
description: '
|
|
22
|
+
description: 'automationLevel: autonomous + parallel ACT (deprecated alias)',
|
|
23
23
|
flags: ['parallel', 'auto_continue', 'no_confirmation'],
|
|
24
|
-
output: '[ULTRAWORK
|
|
24
|
+
output: '[ULTRAWORK] automationLevel: autonomous + parallel ACT. Loop runs to convergence; stuck → auto-TODO (no confirmation).',
|
|
25
25
|
},
|
|
26
26
|
ulw: {
|
|
27
27
|
alias: 'ultrawork',
|
|
@@ -47,12 +47,12 @@ const MAGIC_KEYWORDS = {
|
|
|
47
47
|
output: '[RALPLAN MODE] Iterative planning with consensus. Will refine plan until approved, then execute with Ralph persistence.',
|
|
48
48
|
},
|
|
49
49
|
|
|
50
|
-
// 검증
|
|
50
|
+
// Deprecated: 기본 JUDGE는 항상 결정론 검증 (no-op)
|
|
51
51
|
verify: {
|
|
52
|
-
name: 'Verify
|
|
53
|
-
description: '
|
|
52
|
+
name: 'Verify (deprecated alias)',
|
|
53
|
+
description: '[deprecated] Deterministic verification is the default; alias mapped',
|
|
54
54
|
flags: ['verification', 'strict'],
|
|
55
|
-
output: '[
|
|
55
|
+
output: '[vibe] \'verify\' is deprecated — deterministic JUDGE is the default; alias mapped.',
|
|
56
56
|
strict: true, // 일상어 ("please verify the fix" 오탐 방지)
|
|
57
57
|
},
|
|
58
58
|
|
|
@@ -65,29 +65,29 @@ const MAGIC_KEYWORDS = {
|
|
|
65
65
|
strict: true, // 일상어 ("let me explore the options" 오탐 방지)
|
|
66
66
|
},
|
|
67
67
|
|
|
68
|
-
//
|
|
68
|
+
// Deprecated: --max-iter 1 매핑
|
|
69
69
|
quick: {
|
|
70
|
-
name: 'Quick
|
|
71
|
-
description: '
|
|
70
|
+
name: 'Quick (deprecated alias)',
|
|
71
|
+
description: '[deprecated] Maps to --max-iter 1; use --max-iter 1 explicitly',
|
|
72
72
|
flags: ['fast', 'minimal_verification'],
|
|
73
|
-
output: '[
|
|
73
|
+
output: '[vibe] \'quick\' maps to --max-iter 1 (single-pass, minimal JUDGE).',
|
|
74
74
|
strict: true, // 일상어 ("quick question on auth" 오탐 방지)
|
|
75
75
|
},
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
-
// 키워드 조합 시너지
|
|
78
|
+
// 키워드 조합 시너지 (deprecated alias 조합도 매핑 유지)
|
|
79
79
|
const KEYWORD_SYNERGIES = {
|
|
80
80
|
'ralph+ultrawork': {
|
|
81
|
-
name: 'Ralph
|
|
82
|
-
output: '[
|
|
81
|
+
name: 'Ralph+Ultrawork (deprecated)',
|
|
82
|
+
output: '[vibe] \'ralph\'+\'ultrawork\' deprecated: automationLevel: autonomous + parallel ACT, exit=coverage-100.',
|
|
83
83
|
},
|
|
84
84
|
'ralph+verify': {
|
|
85
|
-
name: 'Ralph
|
|
86
|
-
output: '[
|
|
85
|
+
name: 'Ralph+Verify (deprecated)',
|
|
86
|
+
output: '[vibe] \'ralph\'+\'verify\' deprecated: both are default behavior; alias mapped.',
|
|
87
87
|
},
|
|
88
88
|
'ultrawork+explore': {
|
|
89
|
-
name: 'Ultrawork
|
|
90
|
-
output: '[ULTRAWORK+EXPLORE]
|
|
89
|
+
name: 'Ultrawork+Explore',
|
|
90
|
+
output: '[ULTRAWORK+EXPLORE] automationLevel: autonomous + parallel exploration agents.',
|
|
91
91
|
},
|
|
92
92
|
};
|
|
93
93
|
|