@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.
Files changed (139) hide show
  1. package/CLAUDE.md +25 -16
  2. package/README.en.md +16 -14
  3. package/README.md +13 -11
  4. package/dist/cli/postinstall/constants.d.ts.map +1 -1
  5. package/dist/cli/postinstall/constants.js +1 -0
  6. package/dist/cli/postinstall/constants.js.map +1 -1
  7. package/dist/cli/postinstall/fs-utils.d.ts +23 -0
  8. package/dist/cli/postinstall/fs-utils.d.ts.map +1 -1
  9. package/dist/cli/postinstall/fs-utils.js +71 -0
  10. package/dist/cli/postinstall/fs-utils.js.map +1 -1
  11. package/dist/cli/postinstall/fs-utils.test.js +69 -1
  12. package/dist/cli/postinstall/fs-utils.test.js.map +1 -1
  13. package/dist/cli/postinstall/main.d.ts.map +1 -1
  14. package/dist/cli/postinstall/main.js +12 -2
  15. package/dist/cli/postinstall/main.js.map +1 -1
  16. package/dist/cli/setup/CodexHooks.test.js +27 -0
  17. package/dist/cli/setup/CodexHooks.test.js.map +1 -1
  18. package/dist/cli/setup/ProjectSetup.d.ts.map +1 -1
  19. package/dist/cli/setup/ProjectSetup.js +6 -5
  20. package/dist/cli/setup/ProjectSetup.js.map +1 -1
  21. package/dist/infra/lib/DecisionTracer.d.ts +4 -0
  22. package/dist/infra/lib/DecisionTracer.d.ts.map +1 -1
  23. package/dist/infra/lib/DecisionTracer.js +4 -0
  24. package/dist/infra/lib/DecisionTracer.js.map +1 -1
  25. package/dist/infra/lib/LoopBreaker.d.ts +4 -0
  26. package/dist/infra/lib/LoopBreaker.d.ts.map +1 -1
  27. package/dist/infra/lib/LoopBreaker.js +4 -0
  28. package/dist/infra/lib/LoopBreaker.js.map +1 -1
  29. package/dist/infra/lib/ReviewRace.d.ts +4 -0
  30. package/dist/infra/lib/ReviewRace.d.ts.map +1 -1
  31. package/dist/infra/lib/ReviewRace.js +4 -0
  32. package/dist/infra/lib/ReviewRace.js.map +1 -1
  33. package/dist/infra/lib/SkillQualityGate.d.ts +4 -0
  34. package/dist/infra/lib/SkillQualityGate.d.ts.map +1 -1
  35. package/dist/infra/lib/SkillQualityGate.js +4 -0
  36. package/dist/infra/lib/SkillQualityGate.js.map +1 -1
  37. package/dist/infra/lib/UltraQA.d.ts +4 -0
  38. package/dist/infra/lib/UltraQA.d.ts.map +1 -1
  39. package/dist/infra/lib/UltraQA.js +4 -0
  40. package/dist/infra/lib/UltraQA.js.map +1 -1
  41. package/dist/infra/lib/VerificationLoop.d.ts +4 -0
  42. package/dist/infra/lib/VerificationLoop.d.ts.map +1 -1
  43. package/dist/infra/lib/VerificationLoop.js +4 -0
  44. package/dist/infra/lib/VerificationLoop.js.map +1 -1
  45. package/dist/infra/orchestrator/index.d.ts.map +1 -1
  46. package/dist/infra/orchestrator/index.js +1 -3
  47. package/dist/infra/orchestrator/index.js.map +1 -1
  48. package/dist/infra/orchestrator/parallelResearch.d.ts.map +1 -1
  49. package/dist/infra/orchestrator/parallelResearch.js +1 -4
  50. package/dist/infra/orchestrator/parallelResearch.js.map +1 -1
  51. package/dist/tools/convention/validateCodeQuality.d.ts.map +1 -1
  52. package/dist/tools/convention/validateCodeQuality.js +5 -4
  53. package/dist/tools/convention/validateCodeQuality.js.map +1 -1
  54. package/dist/tools/index.d.ts +2 -0
  55. package/dist/tools/index.d.ts.map +1 -1
  56. package/dist/tools/index.js +2 -0
  57. package/dist/tools/index.js.map +1 -1
  58. package/dist/tools/loop/index.d.ts +6 -0
  59. package/dist/tools/loop/index.d.ts.map +1 -0
  60. package/dist/tools/loop/index.js +5 -0
  61. package/dist/tools/loop/index.js.map +1 -0
  62. package/dist/tools/loop/validateLoopDefinition.d.ts +38 -0
  63. package/dist/tools/loop/validateLoopDefinition.d.ts.map +1 -0
  64. package/dist/tools/loop/validateLoopDefinition.js +224 -0
  65. package/dist/tools/loop/validateLoopDefinition.js.map +1 -0
  66. package/dist/tools/loop/validateLoopDefinition.test.d.ts +14 -0
  67. package/dist/tools/loop/validateLoopDefinition.test.d.ts.map +1 -0
  68. package/dist/tools/loop/validateLoopDefinition.test.js +229 -0
  69. package/dist/tools/loop/validateLoopDefinition.test.js.map +1 -0
  70. package/dist/tools/spec/traceabilityMatrix.d.ts +2 -0
  71. package/dist/tools/spec/traceabilityMatrix.d.ts.map +1 -1
  72. package/dist/tools/spec/traceabilityMatrix.js +50 -1
  73. package/dist/tools/spec/traceabilityMatrix.js.map +1 -1
  74. package/dist/tools/spec/traceabilityMatrix.path-resolution.test.d.ts +10 -0
  75. package/dist/tools/spec/traceabilityMatrix.path-resolution.test.d.ts.map +1 -0
  76. package/dist/tools/spec/traceabilityMatrix.path-resolution.test.js +89 -0
  77. package/dist/tools/spec/traceabilityMatrix.path-resolution.test.js.map +1 -0
  78. package/dist/tools/spec/traceabilityMatrix.test.js +19 -0
  79. package/dist/tools/spec/traceabilityMatrix.test.js.map +1 -1
  80. package/hooks/hooks.json +1 -0
  81. package/hooks/scripts/__tests__/.vibe/command-log.txt +60 -0
  82. package/hooks/scripts/__tests__/.vibe/memories/memories.db +0 -0
  83. package/hooks/scripts/__tests__/.vibe/memories/memories.db-shm +0 -0
  84. package/hooks/scripts/__tests__/.vibe/memories/memories.db-wal +0 -0
  85. package/hooks/scripts/__tests__/auto-test-debounce.test.js +145 -0
  86. package/hooks/scripts/__tests__/code-check-detectors.test.js +155 -0
  87. package/hooks/scripts/__tests__/dispatcher-inprocess.test.js +99 -0
  88. package/hooks/scripts/__tests__/keyword-detector.test.js +26 -18
  89. package/hooks/scripts/__tests__/loop-ledger.test.js +321 -0
  90. package/hooks/scripts/__tests__/post-edit-dispatcher.test.js +139 -0
  91. package/hooks/scripts/__tests__/pre-tool-guard.test.js +115 -1
  92. package/hooks/scripts/__tests__/run-ledger-verify-required.test.js +146 -0
  93. package/hooks/scripts/__tests__/run-ledger.test.js +330 -0
  94. package/hooks/scripts/__tests__/scope-from-spec.test.js +215 -0
  95. package/hooks/scripts/__tests__/sentinel-guard.test.js +79 -24
  96. package/hooks/scripts/__tests__/step-counter.test.js +95 -15
  97. package/hooks/scripts/__tests__/utils-npm-root.test.js +98 -0
  98. package/hooks/scripts/auto-commit.js +27 -1
  99. package/hooks/scripts/auto-format.js +85 -20
  100. package/hooks/scripts/auto-test.js +187 -37
  101. package/hooks/scripts/code-check.js +286 -90
  102. package/hooks/scripts/codex-hook-adapter.js +12 -1
  103. package/hooks/scripts/command-log.js +26 -16
  104. package/hooks/scripts/keyword-detector.js +22 -22
  105. package/hooks/scripts/lib/dispatcher.js +38 -0
  106. package/hooks/scripts/lib/hook-context.js +130 -0
  107. package/hooks/scripts/lib/loop-ledger.js +118 -0
  108. package/hooks/scripts/lib/pr-gate-runner.js +62 -0
  109. package/hooks/scripts/lib/run-ledger.js +169 -0
  110. package/hooks/scripts/lib/scope-from-spec.js +40 -7
  111. package/hooks/scripts/loop-ledger.js +56 -0
  112. package/hooks/scripts/post-edit-dispatcher.js +93 -20
  113. package/hooks/scripts/post-edit.js +40 -19
  114. package/hooks/scripts/pr-test-gate.js +8 -37
  115. package/hooks/scripts/pre-tool-dispatcher.js +18 -16
  116. package/hooks/scripts/pre-tool-guard.js +55 -52
  117. package/hooks/scripts/prompt-dispatcher.js +10 -0
  118. package/hooks/scripts/scope-guard.js +40 -39
  119. package/hooks/scripts/sentinel-guard.js +41 -41
  120. package/hooks/scripts/session-start.js +13 -1
  121. package/hooks/scripts/step-counter.js +100 -7
  122. package/hooks/scripts/stop-dispatcher.js +26 -0
  123. package/hooks/scripts/utils.js +63 -21
  124. package/hooks/scripts/verify-ledger.js +22 -0
  125. package/package.json +2 -2
  126. package/skills/spec/references/templates.md +11 -6
  127. package/skills/vibe/SKILL.md +40 -23
  128. package/skills/vibe.loop/SKILL.md +116 -0
  129. package/skills/vibe.run/SKILL.md +153 -1686
  130. package/skills/vibe.run/references/brand-assets.md +59 -0
  131. package/skills/vibe.run/references/parallel-agents.md +326 -0
  132. package/skills/vibe.run/references/race-review.md +272 -0
  133. package/skills/vibe.run/references/ralph-loop.md +173 -0
  134. package/skills/vibe.run/references/ultrawork-mode.md +151 -0
  135. package/skills/vibe.trace/SKILL.md +25 -38
  136. package/skills/vibe.verify/SKILL.md +15 -0
  137. package/vibe/rules/loop-contract.md +54 -0
  138. package/vibe/templates/loop-template.md +69 -0
  139. 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
- process.on('uncaughtException', () => {});
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
- * stdin에서 hook input JSON을 읽어 파일 경로 추출
42
+ * 경량 glob RegExp 변환 (scope-guard와 동일 알고리즘, 독립 복사본).
43
+ * @param {string} glob
44
+ * @returns {RegExp}
16
45
  */
17
- function getModifiedFiles() {
18
- try {
19
- const input = process.env.HOOK_INPUT;
20
- if (input) {
21
- const parsed = JSON.parse(input);
22
- const filePath = parsed.tool_input?.file_path || parsed.tool_input?.path;
23
- return filePath ? [filePath] : [];
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
- * Detect `any` type usage and return line-level findings
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
- if (/:\s*any\b|<any>|as\s+any\b/.test(line)) {
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
- suggestion: 'Replace with: unknown + type guard pattern: if (typeof x === \'string\') { ... }'
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
- * Detect functions exceeding 50 lines
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
- suggestion: `Extract lines ${fnStart + 20}–${i + 1} into a separate helper function`
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
- * Detect nesting depth exceeding 3 levels
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
- suggestion: 'Use early return pattern: if (!condition) return; — instead of wrapping in else'
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
- * Detect console.log statements
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
- * Detect magic numbers (bare numeric literals ≥2 digits, outside comments/strings)
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
- if (nums.length > 0) {
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): ${nums.join(', ')}`,
145
- suggestion: `Extract to named constant: const LIMIT = ${nums[0]};`
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
- console.log(`\n🚨 [ESCALATION] ${path.basename(filePath)}: 동일 이슈 ${entry.count}회 반복`);
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
- async function main() {
228
- const files = getModifiedFiles();
229
- if (files.length === 0) return;
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
- // 1. Code quality check (changed files only — never scan entire project)
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
- if (critical.length > 0) {
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 — never block progress
436
+ // Silently continue on check failure
250
437
  }
251
438
 
252
- // 2. 관찰 자동 캡처
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
- // 관찰 캡처 실패해도 무시 (non-critical)
451
+ // 관찰 캡처 실패해도 무시
265
452
  }
453
+
454
+ return { exitCode: 0, findings };
266
455
  }
267
456
 
268
- main();
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
- writeAdditionalContext(combinedOutput(result));
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
- try {
16
- const input = JSON.parse(process.env.TOOL_INPUT || '{}');
17
- const command = input.command || '';
18
- if (!command) process.exit(0);
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
- const timestamp = new Date().toISOString();
21
- const truncated = command.length > MAX_CMD_LENGTH
22
- ? command.slice(0, MAX_CMD_LENGTH) + '...(truncated)'
23
- : command;
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
- const entry = `[${timestamp}] ${truncated}\n`;
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
- if (!existsSync(LOG_DIR)) mkdirSync(LOG_DIR, { recursive: true });
28
- appendFileSync(LOG_FILE, entry);
29
- } catch {
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 Loop',
14
- description: 'Continue until task is verified complete',
13
+ name: 'Ralph (deprecated alias)',
14
+ description: '[deprecated] Looping to convergence is the default; alias mapped',
15
15
  flags: ['persistence', 'verification'],
16
- output: '[RALPH MODE] Self-referential completion loop activated. Will continue until ALL tasks verified complete. NO early stopping.',
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: 'Maximum parallel execution, no pause',
22
+ description: 'automationLevel: autonomous + parallel ACT (deprecated alias)',
23
23
  flags: ['parallel', 'auto_continue', 'no_confirmation'],
24
- output: '[ULTRAWORK MODE] Use PARALLEL Task calls. Auto-continue through ALL phases. Auto-retry on errors up to 3 times. Do NOT ask for confirmation between phases.',
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 Mode',
53
- description: 'Strict verification after each step',
52
+ name: 'Verify (deprecated alias)',
53
+ description: '[deprecated] Deterministic verification is the default; alias mapped',
54
54
  flags: ['verification', 'strict'],
55
- output: '[VERIFY MODE] Strict verification enabled. Every change must be verified before proceeding.',
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 Mode',
71
- description: 'Fast execution, minimal verification',
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: '[QUICK MODE] Fast execution mode. Minimal verification, single round reviews.',
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 Ultrawork',
82
- output: '[RALPH+ULTRAWORK] Maximum persistence AND parallel execution. Will NOT stop until ALL phases complete with verification.',
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 Verify',
86
- output: '[RALPH+VERIFY] Persistent completion with strict verification at each step.',
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 Explore',
90
- output: '[ULTRAWORK+EXPLORE] Parallel exploration agents for maximum coverage.',
89
+ name: 'Ultrawork+Explore',
90
+ output: '[ULTRAWORK+EXPLORE] automationLevel: autonomous + parallel exploration agents.',
91
91
  },
92
92
  };
93
93