@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
@@ -3,20 +3,43 @@
3
3
  *
4
4
  * 수정된 파일에 대응하는 테스트 파일을 찾아 실행.
5
5
  * 실패 시 마지막 5줄만 출력해서 context window 오염 방지.
6
- * exit 0 항상 — 차단하지 않고 에이전트에게 결과만 전달.
6
+ *
7
+ * debounce 지원: autoTest.mode='debounce'(기본) | 'always' | 'off' via .vibe/config.json
8
+ * debounce 모드: 동일 테스트 파일을 DEBOUNCE_COOLDOWN_MS(120s) 내에
9
+ * 소스 변경 없이 재실행 스킵.
10
+ *
11
+ * findings 반환 구조 (디스패처가 additionalContext에 주입).
7
12
  */
8
- import { execSync } from 'child_process';
9
- import { existsSync } from 'fs';
13
+ import { execFile } from 'child_process';
14
+ import { promisify } from 'util';
15
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
16
+ import { createHash } from 'crypto';
10
17
  import path from 'path';
11
- import { PROJECT_DIR } from './utils.js';
18
+ import { PROJECT_DIR, readProjectConfig } from './utils.js';
19
+ import { buildCliCtx, isDirectRun } from './lib/hook-context.js';
20
+
21
+ // WHY async execFile (not execSync): in-process 디스패처에서 다른 step과
22
+ // Promise.all로 병렬 실행되므로, 60초 동기 실행은 체인 전체를 직렬화시킨다.
23
+ const execFileAsync = promisify(execFile);
12
24
 
13
25
  const CODE_EXT_RE = /\.(ts|tsx|js|jsx)$/;
14
26
  const TEST_SUFFIXES = ['.test.', '.spec.'];
15
27
  const MAX_OUTPUT_LINES = 5;
28
+ const TEST_TIMEOUT_MS = 60000;
16
29
 
17
- function getFilePath() {
18
- const input = JSON.parse(process.env.TOOL_INPUT || '{}');
19
- return input.file_path || input.path || '';
30
+ /** debounce 쿨다운 — 동일 테스트 + 동일 소스 해시에 대해 스킵하는 시간(ms) */
31
+ const DEBOUNCE_COOLDOWN_MS = 120_000;
32
+
33
+ /** debounce 상태 파일 경로 */
34
+ const DEBOUNCE_STATE_FILE = path.join(PROJECT_DIR, '.vibe', 'metrics', 'auto-test-state.json');
35
+
36
+ function getFilePath(ctx) {
37
+ try {
38
+ const input = JSON.parse(ctx.toolInput || '{}');
39
+ return input.file_path || input.path || '';
40
+ } catch {
41
+ return '';
42
+ }
20
43
  }
21
44
 
22
45
  function isTestFile(filePath) {
@@ -46,36 +69,163 @@ function hasJest() {
46
69
  return existsSync(path.join(PROJECT_DIR, 'node_modules', '.bin', 'jest'));
47
70
  }
48
71
 
49
- try {
50
- const filePath = getFilePath();
51
- if (!filePath || !CODE_EXT_RE.test(filePath)) process.exit(0);
52
-
53
- const testFile = isTestFile(filePath) ? filePath : findTestFile(filePath);
54
- if (!testFile) process.exit(0);
55
-
56
- const relPath = path.relative(PROJECT_DIR, testFile);
57
- let cmd = '';
58
- if (hasVitest()) {
59
- cmd = `npx vitest run "${relPath}" --reporter=verbose`;
60
- } else if (hasJest()) {
61
- cmd = `npx jest "${relPath}" --no-coverage`;
62
- } else {
63
- process.exit(0);
72
+ /**
73
+ * autoTest.mode 설정 읽기 — 기본 'debounce'.
74
+ * @returns {'debounce'|'always'|'off'}
75
+ */
76
+ function getTestMode() {
77
+ try {
78
+ const cfg = readProjectConfig();
79
+ const mode = cfg?.autoTest?.mode;
80
+ if (mode === 'always' || mode === 'off' || mode === 'debounce') return mode;
81
+ } catch { /* ignore */ }
82
+ return 'debounce';
83
+ }
84
+
85
+ /**
86
+ * 파일 내용 SHA-256 해시 (앞 16자만 사용). 파일 없으면 빈 문자열.
87
+ * @param {string} filePath
88
+ * @returns {string}
89
+ */
90
+ function fileHash(filePath) {
91
+ try {
92
+ const content = readFileSync(filePath, 'utf-8');
93
+ return createHash('sha256').update(content).digest('hex').slice(0, 16);
94
+ } catch {
95
+ return '';
64
96
  }
97
+ }
98
+
99
+ /**
100
+ * debounce 상태 파일 읽기. fail-open → 빈 객체.
101
+ * @returns {Record<string, { lastRun: number, srcHash: string }>}
102
+ */
103
+ function readDebounceState() {
104
+ try {
105
+ if (existsSync(DEBOUNCE_STATE_FILE)) {
106
+ return JSON.parse(readFileSync(DEBOUNCE_STATE_FILE, 'utf-8'));
107
+ }
108
+ } catch { /* ignore */ }
109
+ return {};
110
+ }
111
+
112
+ /**
113
+ * debounce 상태 파일 쓰기. fail-open.
114
+ * @param {Record<string, { lastRun: number, srcHash: string }>} state
115
+ */
116
+ function writeDebounceState(state) {
117
+ try {
118
+ mkdirSync(path.dirname(DEBOUNCE_STATE_FILE), { recursive: true });
119
+ writeFileSync(DEBOUNCE_STATE_FILE, JSON.stringify(state, null, 2), 'utf-8');
120
+ } catch { /* fail-open */ }
121
+ }
122
+
123
+ /**
124
+ * debounce 체크: testFile을 스킵해야 하면 true.
125
+ * @param {string} testFile — 절대 경로
126
+ * @param {string} srcFile — 절대 경로 (소스 파일, 테스트가 아닌 경우)
127
+ * @returns {boolean}
128
+ */
129
+ function shouldSkipDebounce(testFile, srcFile) {
130
+ try {
131
+ const state = readDebounceState();
132
+ const entry = state[testFile];
133
+ if (!entry) return false;
134
+
135
+ const now = Date.now();
136
+ const elapsed = now - entry.lastRun;
137
+ if (elapsed > DEBOUNCE_COOLDOWN_MS) return false;
138
+
139
+ const currentHash = fileHash(srcFile);
140
+ if (currentHash !== entry.srcHash) return false;
141
+
142
+ return true; // 쿨다운 내 + 소스 미변경 → 스킵
143
+ } catch {
144
+ return false; // fail-open
145
+ }
146
+ }
147
+
148
+ /**
149
+ * debounce 상태 업데이트.
150
+ * @param {string} testFile
151
+ * @param {string} srcFile
152
+ */
153
+ function updateDebounceState(testFile, srcFile) {
154
+ try {
155
+ const state = readDebounceState();
156
+ state[testFile] = {
157
+ lastRun: Date.now(),
158
+ srcHash: fileHash(srcFile),
159
+ };
160
+ writeDebounceState(state);
161
+ } catch { /* fail-open */ }
162
+ }
163
+
164
+ /**
165
+ * in-process 진입점 — 테스트 실행. findings 반환.
166
+ * @param {{ toolInput: string }} ctx
167
+ * @returns {Promise<{ exitCode: number, findings: string[] }>}
168
+ */
169
+ export async function run(ctx) {
170
+ const findings = [];
171
+ try {
172
+ const filePath = getFilePath(ctx);
173
+ if (!filePath || !CODE_EXT_RE.test(filePath)) return { exitCode: 0, findings };
174
+
175
+ const mode = getTestMode();
176
+ if (mode === 'off') return { exitCode: 0, findings };
177
+
178
+ const srcFile = path.resolve(filePath);
179
+ const testFile = isTestFile(filePath) ? srcFile : findTestFile(srcFile);
180
+ if (!testFile) return { exitCode: 0, findings };
181
+
182
+ // debounce 모드: 스킵 여부 확인
183
+ if (mode === 'debounce') {
184
+ const skipSrc = isTestFile(filePath) ? testFile : srcFile;
185
+ if (shouldSkipDebounce(testFile, skipSrc)) {
186
+ // 스팸 방지: 스킵 시 finding 없음 (조용히)
187
+ return { exitCode: 0, findings };
188
+ }
189
+ }
190
+
191
+ const relPath = path.relative(PROJECT_DIR, testFile);
192
+ let args = null;
193
+ if (hasVitest()) {
194
+ args = ['vitest', 'run', relPath, '--reporter=verbose'];
195
+ } else if (hasJest()) {
196
+ args = ['jest', relPath, '--no-coverage'];
197
+ } else {
198
+ return { exitCode: 0, findings };
199
+ }
200
+
201
+ // debounce 상태 업데이트 (실행 전)
202
+ if (mode === 'debounce') {
203
+ const skipSrc = isTestFile(filePath) ? testFile : srcFile;
204
+ updateDebounceState(testFile, skipSrc);
205
+ }
206
+
207
+ const { stdout } = await execFileAsync('npx', args, {
208
+ cwd: PROJECT_DIR,
209
+ timeout: TEST_TIMEOUT_MS,
210
+ // Windows에서 npx는 npx.cmd — shell 없이는 execFile이 찾지 못함
211
+ shell: process.platform === 'win32',
212
+ });
213
+
214
+ const tail = stdout.trim().split('\n').slice(-MAX_OUTPUT_LINES).join('\n');
215
+ findings.push(`[AUTO-TEST] PASSED: ${relPath}\n${tail}`);
216
+ } catch (err) {
217
+ const stderr = err.stderr ? err.stderr.toString() : '';
218
+ const stdout = err.stdout ? err.stdout.toString() : '';
219
+ const combined = (stdout + '\n' + stderr).trim();
220
+ const tail = combined.split('\n').slice(-MAX_OUTPUT_LINES).join('\n');
221
+ findings.push(`[AUTO-TEST] FAILED\n${tail}`);
222
+ }
223
+ return { exitCode: 0, findings };
224
+ }
65
225
 
66
- console.log(`[AUTO-TEST] Running: ${relPath}`);
67
- const output = execSync(cmd, {
68
- cwd: PROJECT_DIR,
69
- stdio: ['ignore', 'pipe', 'pipe'],
70
- timeout: 60000,
71
- }).toString();
72
-
73
- const tail = output.trim().split('\n').slice(-MAX_OUTPUT_LINES).join('\n');
74
- console.log(`[AUTO-TEST] PASSED\n${tail}`);
75
- } catch (err) {
76
- const stderr = err.stderr ? err.stderr.toString() : '';
77
- const stdout = err.stdout ? err.stdout.toString() : '';
78
- const combined = (stdout + '\n' + stderr).trim();
79
- const tail = combined.split('\n').slice(-MAX_OUTPUT_LINES).join('\n');
80
- console.log(`[AUTO-TEST] FAILED\n${tail}`);
226
+ // standalone CLI 모드
227
+ if (isDirectRun(import.meta.url)) {
228
+ const { exitCode, findings } = await run(buildCliCtx());
229
+ if (findings.length > 0) process.stdout.write(findings.join('\n') + '\n');
230
+ process.exit(exitCode);
81
231
  }