@su-record/vibe 2.12.3 → 2.12.5

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 (145) hide show
  1. package/CLAUDE.md +15 -13
  2. package/README.md +2 -0
  3. package/dist/cli/collaborator.d.ts.map +1 -1
  4. package/dist/cli/collaborator.js +23 -6
  5. package/dist/cli/collaborator.js.map +1 -1
  6. package/dist/cli/commands/init.d.ts +8 -0
  7. package/dist/cli/commands/init.d.ts.map +1 -1
  8. package/dist/cli/commands/init.js +26 -1
  9. package/dist/cli/commands/init.js.map +1 -1
  10. package/dist/cli/postinstall/claude-agents.d.ts +11 -1
  11. package/dist/cli/postinstall/claude-agents.d.ts.map +1 -1
  12. package/dist/cli/postinstall/claude-agents.js +37 -14
  13. package/dist/cli/postinstall/claude-agents.js.map +1 -1
  14. package/dist/cli/postinstall/constants.d.ts +20 -2
  15. package/dist/cli/postinstall/constants.d.ts.map +1 -1
  16. package/dist/cli/postinstall/constants.js +52 -2
  17. package/dist/cli/postinstall/constants.js.map +1 -1
  18. package/dist/cli/postinstall/fs-utils.d.ts +5 -0
  19. package/dist/cli/postinstall/fs-utils.d.ts.map +1 -1
  20. package/dist/cli/postinstall/fs-utils.js +55 -0
  21. package/dist/cli/postinstall/fs-utils.js.map +1 -1
  22. package/dist/cli/postinstall/fs-utils.test.d.ts +2 -0
  23. package/dist/cli/postinstall/fs-utils.test.d.ts.map +1 -0
  24. package/dist/cli/postinstall/fs-utils.test.js +31 -0
  25. package/dist/cli/postinstall/fs-utils.test.js.map +1 -0
  26. package/dist/cli/postinstall/index.d.ts +2 -2
  27. package/dist/cli/postinstall/index.d.ts.map +1 -1
  28. package/dist/cli/postinstall/index.js +2 -2
  29. package/dist/cli/postinstall/index.js.map +1 -1
  30. package/dist/cli/postinstall/main.d.ts.map +1 -1
  31. package/dist/cli/postinstall/main.js +9 -6
  32. package/dist/cli/postinstall/main.js.map +1 -1
  33. package/dist/cli/postinstall.d.ts +1 -1
  34. package/dist/cli/postinstall.d.ts.map +1 -1
  35. package/dist/cli/postinstall.js +1 -1
  36. package/dist/cli/postinstall.js.map +1 -1
  37. package/dist/cli/setup/ProjectSetup.d.ts.map +1 -1
  38. package/dist/cli/setup/ProjectSetup.js +13 -1
  39. package/dist/cli/setup/ProjectSetup.js.map +1 -1
  40. package/dist/infra/lib/ContextCompressor.d.ts +11 -2
  41. package/dist/infra/lib/ContextCompressor.d.ts.map +1 -1
  42. package/dist/infra/lib/ContextCompressor.js +26 -41
  43. package/dist/infra/lib/ContextCompressor.js.map +1 -1
  44. package/dist/infra/lib/ContextCompressor.test.d.ts +2 -0
  45. package/dist/infra/lib/ContextCompressor.test.d.ts.map +1 -0
  46. package/dist/infra/lib/ContextCompressor.test.js +25 -0
  47. package/dist/infra/lib/ContextCompressor.test.js.map +1 -0
  48. package/dist/infra/lib/CostAccumulator.d.ts +19 -2
  49. package/dist/infra/lib/CostAccumulator.d.ts.map +1 -1
  50. package/dist/infra/lib/CostAccumulator.js +60 -2
  51. package/dist/infra/lib/CostAccumulator.js.map +1 -1
  52. package/dist/infra/lib/antigravity/chat.d.ts.map +1 -1
  53. package/dist/infra/lib/antigravity/chat.js +6 -0
  54. package/dist/infra/lib/antigravity/chat.js.map +1 -1
  55. package/dist/infra/lib/antigravity/orchestration.d.ts.map +1 -1
  56. package/dist/infra/lib/antigravity/orchestration.js +13 -1
  57. package/dist/infra/lib/antigravity/orchestration.js.map +1 -1
  58. package/dist/infra/lib/antigravity/types.d.ts +8 -0
  59. package/dist/infra/lib/antigravity/types.d.ts.map +1 -1
  60. package/dist/infra/lib/embedding/VectorStore.d.ts +9 -2
  61. package/dist/infra/lib/embedding/VectorStore.d.ts.map +1 -1
  62. package/dist/infra/lib/embedding/VectorStore.js +42 -19
  63. package/dist/infra/lib/embedding/VectorStore.js.map +1 -1
  64. package/dist/infra/lib/gpt/auth.d.ts.map +1 -1
  65. package/dist/infra/lib/gpt/auth.js +13 -1
  66. package/dist/infra/lib/gpt/auth.js.map +1 -1
  67. package/dist/infra/lib/gpt/chat.d.ts.map +1 -1
  68. package/dist/infra/lib/gpt/chat.js +52 -31
  69. package/dist/infra/lib/gpt/chat.js.map +1 -1
  70. package/dist/infra/lib/gpt/embedding.d.ts.map +1 -1
  71. package/dist/infra/lib/gpt/embedding.js +6 -5
  72. package/dist/infra/lib/gpt/embedding.js.map +1 -1
  73. package/dist/infra/lib/gpt/orchestration.d.ts.map +1 -1
  74. package/dist/infra/lib/gpt/orchestration.js +13 -1
  75. package/dist/infra/lib/gpt/orchestration.js.map +1 -1
  76. package/dist/infra/lib/gpt/types.d.ts +8 -0
  77. package/dist/infra/lib/gpt/types.d.ts.map +1 -1
  78. package/dist/infra/lib/llm/timeout.d.ts +34 -0
  79. package/dist/infra/lib/llm/timeout.d.ts.map +1 -0
  80. package/dist/infra/lib/llm/timeout.js +45 -0
  81. package/dist/infra/lib/llm/timeout.js.map +1 -0
  82. package/dist/infra/lib/llm/timeout.test.d.ts +2 -0
  83. package/dist/infra/lib/llm/timeout.test.d.ts.map +1 -0
  84. package/dist/infra/lib/llm/timeout.test.js +50 -0
  85. package/dist/infra/lib/llm/timeout.test.js.map +1 -0
  86. package/dist/infra/lib/memory/MemoryStorage.d.ts +12 -0
  87. package/dist/infra/lib/memory/MemoryStorage.d.ts.map +1 -1
  88. package/dist/infra/lib/memory/MemoryStorage.js +57 -0
  89. package/dist/infra/lib/memory/MemoryStorage.js.map +1 -1
  90. package/dist/infra/lib/memory/ReflectionStore.d.ts.map +1 -1
  91. package/dist/infra/lib/memory/ReflectionStore.js +8 -27
  92. package/dist/infra/lib/memory/ReflectionStore.js.map +1 -1
  93. package/dist/infra/orchestrator/AgentExecutor.d.ts.map +1 -1
  94. package/dist/infra/orchestrator/AgentExecutor.js +3 -1
  95. package/dist/infra/orchestrator/AgentExecutor.js.map +1 -1
  96. package/dist/infra/orchestrator/LLMCluster.d.ts +4 -0
  97. package/dist/infra/orchestrator/LLMCluster.d.ts.map +1 -1
  98. package/dist/infra/orchestrator/LLMCluster.js +39 -3
  99. package/dist/infra/orchestrator/LLMCluster.js.map +1 -1
  100. package/dist/infra/orchestrator/MultiLlmResearch.d.ts +5 -0
  101. package/dist/infra/orchestrator/MultiLlmResearch.d.ts.map +1 -1
  102. package/dist/infra/orchestrator/MultiLlmResearch.js +7 -0
  103. package/dist/infra/orchestrator/MultiLlmResearch.js.map +1 -1
  104. package/dist/infra/orchestrator/PhasePipeline.d.ts +5 -5
  105. package/dist/infra/orchestrator/PhasePipeline.d.ts.map +1 -1
  106. package/dist/infra/orchestrator/PhasePipeline.js +19 -15
  107. package/dist/infra/orchestrator/PhasePipeline.js.map +1 -1
  108. package/dist/infra/orchestrator/SmartRouter.d.ts +10 -0
  109. package/dist/infra/orchestrator/SmartRouter.d.ts.map +1 -1
  110. package/dist/infra/orchestrator/SmartRouter.js +23 -6
  111. package/dist/infra/orchestrator/SmartRouter.js.map +1 -1
  112. package/dist/infra/orchestrator/SmartRouter.test.js +46 -19
  113. package/dist/infra/orchestrator/SmartRouter.test.js.map +1 -1
  114. package/dist/infra/orchestrator/parallelResearch.d.ts.map +1 -1
  115. package/dist/infra/orchestrator/parallelResearch.js +41 -9
  116. package/dist/infra/orchestrator/parallelResearch.js.map +1 -1
  117. package/hooks/hooks.json +2 -9
  118. package/hooks/scripts/__tests__/keyword-detector.test.js +51 -16
  119. package/hooks/scripts/auto-commit.js +17 -17
  120. package/hooks/scripts/auto-format.js +11 -6
  121. package/hooks/scripts/code-check.js +17 -20
  122. package/hooks/scripts/codex-hook-adapter.js +5 -1
  123. package/hooks/scripts/keyword-detector.js +14 -2
  124. package/hooks/scripts/llm-orchestrate.js +13 -2
  125. package/hooks/scripts/prompt-dispatcher.js +32 -9
  126. package/hooks/scripts/session-start.js +28 -15
  127. package/hooks/scripts/utils.js +33 -9
  128. package/package.json +1 -1
  129. package/skills/arch-guard/SKILL.md +2 -2
  130. package/skills/characterization-test/SKILL.md +2 -2
  131. package/skills/design-audit/SKILL.md +1 -0
  132. package/skills/design-normalize/SKILL.md +1 -0
  133. package/skills/design-teach/SKILL.md +1 -1
  134. package/skills/exec-plan/SKILL.md +2 -2
  135. package/skills/spec/SKILL.md +6 -312
  136. package/skills/spec/references/askuser-examples.md +57 -0
  137. package/skills/spec/references/example-session.md +87 -0
  138. package/skills/spec/references/templates.md +189 -0
  139. package/skills/test/SKILL.md +9 -9
  140. package/skills/ui-ux-pro-max/SKILL.md +1 -0
  141. package/skills/vibe.run/SKILL.md +5 -5
  142. package/skills/vibe.test/SKILL.md +1 -1
  143. package/skills/vibe.verify/SKILL.md +1 -1
  144. package/vibe/rules/standards/complexity-metrics.md +2 -2
  145. package/vibe/templates/claudemd-template.md +1 -1
@@ -71,38 +71,71 @@ describe('keyword-detector', () => {
71
71
  });
72
72
  });
73
73
 
74
- describe('verify keyword', () => {
75
- it('should detect verify keyword', () => {
76
- const result = runDetector('verify the implementation is correct');
74
+ // strict 키워드(일상어): 명령 끝 위치 또는 --flag 에서만 발동.
75
+ // 일상 영어("please verify", "quick question") 오탐을 막기 위함.
76
+ describe('verify keyword (strict)', () => {
77
+ it('should detect verify at command tail', () => {
78
+ const result = runDetector('make the implementation correct, verify');
77
79
  expect(result.stdout).toContain('[VERIFY MODE]');
78
80
  expect(result.stdout).toContain('verification');
79
81
  });
82
+
83
+ it('should detect --verify flag', () => {
84
+ const result = runDetector('fix the bug --verify');
85
+ expect(result.stdout).toContain('[VERIFY MODE]');
86
+ });
80
87
  });
81
88
 
82
- describe('quick keyword', () => {
83
- it('should detect quick keyword', () => {
84
- const result = runDetector('quick fix this typo');
89
+ describe('quick keyword (strict)', () => {
90
+ it('should detect quick at command tail', () => {
91
+ const result = runDetector('fix this typo quick');
85
92
  expect(result.stdout).toContain('[QUICK MODE]');
86
93
  expect(result.stdout).toContain('fast');
87
94
  });
95
+
96
+ it('should detect --quick flag', () => {
97
+ const result = runDetector('build the payment API --quick');
98
+ expect(result.stdout).toContain('[QUICK MODE]');
99
+ });
88
100
  });
89
101
 
90
- describe('explore keyword', () => {
91
- it('should detect explore keyword', () => {
92
- const result = runDetector('explore the codebase structure');
102
+ describe('explore keyword (strict)', () => {
103
+ it('should detect --explore flag', () => {
104
+ const result = runDetector('analyze the codebase --explore');
93
105
  expect(result.stdout).toContain('[EXPLORE MODE]');
94
106
  expect(result.stdout).toContain('exploration');
95
107
  });
96
108
  });
97
109
 
98
- describe('plan keyword', () => {
99
- it('should detect plan keyword', () => {
100
- const result = runDetector('plan the new feature');
110
+ describe('plan keyword (strict)', () => {
111
+ it('should detect --plan flag', () => {
112
+ const result = runDetector('the new feature --plan');
101
113
  expect(result.stdout).toContain('[PLAN MODE]');
102
114
  expect(result.stdout).toContain('planning');
103
115
  });
104
116
  });
105
117
 
118
+ // ══════════════════════════════════════════════════
119
+ // 오탐 방지 회귀 테스트 — 일상어가 명령 중간/시작에 올 때 발동 금지
120
+ // ══════════════════════════════════════════════════
121
+ describe('strict keyword false-positive guard', () => {
122
+ it('should NOT trigger on "quick question on auth"', () => {
123
+ expect(runDetector('quick question on auth').stdout).toBe('');
124
+ });
125
+
126
+ it('should NOT trigger on "please verify the fix works"', () => {
127
+ expect(runDetector('please verify the fix works').stdout).toBe('');
128
+ });
129
+
130
+ it('should NOT trigger on "I plan to refactor later"', () => {
131
+ expect(runDetector('I plan to refactor later').stdout).toBe('');
132
+ });
133
+
134
+ it('should NOT trigger on "let me explore the options first"', () => {
135
+ expect(runDetector('let me explore the options first').stdout).toBe('');
136
+ });
137
+ });
138
+
106
139
  // ══════════════════════════════════════════════════
107
140
  // Keyword combinations / synergies
108
141
  // ══════════════════════════════════════════════════
@@ -115,15 +148,16 @@ describe('keyword-detector', () => {
115
148
  });
116
149
 
117
150
  it('should detect ralph+verify synergy', () => {
118
- const result = runDetector('ralph verify each step carefully');
151
+ // verify is strict → use --verify flag (ralph stays bare)
152
+ const result = runDetector('ralph fix each step --verify');
119
153
  expect(result.stdout).toContain('[RALPH+VERIFY]');
120
154
  });
121
155
 
122
156
  it('should output both keywords when no synergy key matches sorted order', () => {
123
157
  // KEYWORD_SYNERGIES defines 'ultrawork+explore' but processCombinations
124
158
  // sorts keywords alphabetically → tries 'explore+ultrawork' which has no match.
125
- // So individual outputs are emitted instead.
126
- const result = runDetector('ultrawork explore the entire project');
159
+ // So individual outputs are emitted instead. explore is strict → --explore.
160
+ const result = runDetector('ultrawork analyze the entire project --explore');
127
161
  expect(result.stdout).toContain('[ULTRAWORK MODE]');
128
162
  expect(result.stdout).toContain('[EXPLORE MODE]');
129
163
  expect(result.stdout).toContain('[FLAGS]');
@@ -190,7 +224,8 @@ describe('keyword-detector', () => {
190
224
  });
191
225
 
192
226
  it('should merge flags from multiple keywords', () => {
193
- const result = runDetector('ralph quick finish this');
227
+ // quick is strict place at command tail
228
+ const result = runDetector('ralph finish this quick');
194
229
  expect(result.stdout).toContain('[FLAGS]');
195
230
  expect(result.stdout).toContain('persistence');
196
231
  expect(result.stdout).toContain('fast');
@@ -1,16 +1,23 @@
1
1
  /**
2
2
  * Stop Hook - 에이전트 응답 완료 시 자동 커밋 + 롤백 체크포인트
3
3
  *
4
- * 변경사항이 있으면 자동으로 git add + commit.
5
- * 커밋 메시지는 변경 파일 목록 기반으로 생성.
6
- * feature branch에서만 동작 (main/master 보호).
4
+ * ⚠️ OPT-IN ONLY (기본 비활성).
5
+ * 자동 커밋은 "사용자가 요청할 때만 커밋한다"는 원칙과 충돌하고,
6
+ * `git add -A` 스코프 밖 파일(임시/미완성)까지 스테이징하는 부작용이 있다.
7
+ * 따라서 `.vibe/config.json` 에서 `hooks["auto-commit"].enabled === true`
8
+ * 로 명시적으로 켰을 때만 동작한다.
7
9
  *
8
- * 체크포인트: 커밋마다 vibe-checkpoint 태그를 생성해
9
- * 문제 발생 `git reset --hard vibe-checkpoint-N` 으로 롤백 가능.
10
- * 최근 5개만 유지, 오래된 체크포인트는 자동 정리.
10
+ * 동작 시: 변경사항이 있으면 git add -A + commit (커밋 메시지는 변경 파일 목록 기반),
11
+ * feature branch 에서만 (main/master 보호).
12
+ * 체크포인트: 커밋마다 vibe-checkpoint 태그 생성 `git reset --hard vibe-checkpoint-N`
13
+ * 으로 롤백 가능. 최근 5개만 유지.
11
14
  */
12
15
  import { execSync } from 'child_process';
13
- import { PROJECT_DIR } from './utils.js';
16
+ import { PROJECT_DIR, readProjectConfig } from './utils.js';
17
+
18
+ // Opt-in 가드 — 명시적으로 켜지 않았으면 아무것도 하지 않는다.
19
+ const __autoCommitCfg = readProjectConfig();
20
+ if (__autoCommitCfg?.hooks?.['auto-commit']?.enabled !== true) process.exit(0);
14
21
 
15
22
  const PROTECTED_BRANCHES = ['main', 'master', 'develop', 'production'];
16
23
  const MAX_FILES_IN_MSG = 5;
@@ -24,14 +31,6 @@ function getCurrentBranch() {
24
31
  }).trim();
25
32
  }
26
33
 
27
- function hasChanges() {
28
- const status = execSync('git status --porcelain', {
29
- cwd: PROJECT_DIR,
30
- encoding: 'utf-8',
31
- }).trim();
32
- return status.length > 0;
33
- }
34
-
35
34
  function getChangedFiles() {
36
35
  const status = execSync('git status --porcelain', {
37
36
  cwd: PROJECT_DIR,
@@ -81,9 +80,10 @@ try {
81
80
  process.exit(0);
82
81
  }
83
82
 
84
- if (!hasChanges()) process.exit(0);
85
-
83
+ // 변경 유무와 파일 목록을 단일 `git status --porcelain` 호출로 처리
86
84
  const files = getChangedFiles();
85
+ if (files.length === 0) process.exit(0);
86
+
87
87
  const msg = buildCommitMessage(files);
88
88
 
89
89
  execSync('git add -A', { cwd: PROJECT_DIR, stdio: 'ignore' });
@@ -19,13 +19,18 @@ function getFilePath() {
19
19
  return input.file_path || input.path || '';
20
20
  }
21
21
 
22
+ // PATH 직접 스캔 — `which` execSync는 매 파일 저장마다 자식 프로세스를 동기
23
+ // spawn하므로, fs.existsSync로 대체하고 프로세스 내 캐싱한다.
24
+ const _binCache = new Map();
22
25
  function hasBin(name) {
23
- try {
24
- execSync(`which ${name}`, { stdio: 'ignore' });
25
- return true;
26
- } catch {
27
- return false;
28
- }
26
+ const cached = _binCache.get(name);
27
+ if (cached !== undefined) return cached;
28
+ const candidates = process.platform === 'win32' ? [`${name}.exe`, `${name}.cmd`, name] : [name];
29
+ const found = (process.env.PATH || '').split(path.delimiter).some(
30
+ dir => dir && candidates.some(c => existsSync(path.join(dir, c))),
31
+ );
32
+ _binCache.set(name, found);
33
+ return found;
29
34
  }
30
35
 
31
36
  function hasPrettier() {
@@ -225,35 +225,32 @@ function clearFailure(filePath) {
225
225
  }
226
226
 
227
227
  async function main() {
228
+ const files = getModifiedFiles();
229
+ if (files.length === 0) return;
230
+
228
231
  // 1. Code quality check (changed files only — never scan entire project)
229
232
  try {
230
- const files = getModifiedFiles();
231
- if (files.length > 0) {
232
- const module = await import(`${BASE_URL}convention/index.js`);
233
- const result = await module.validateCodeQuality({
234
- targetPath: files[0],
235
- projectPath: PROJECT_DIR,
236
- });
237
- const text = result.content[0].text;
238
- // Output P1/P2 only — skip P3 (style)
239
- const critical = text.split('\n').filter(l => /\b(error|critical|P1|P2)\b/i.test(l)).slice(0, 3);
240
- if (critical.length > 0) {
241
- console.log('[CODE CHECK]', critical.join(' | '));
242
- trackFailure(files[0], critical);
243
- } else {
244
- clearFailure(files[0]);
245
- }
246
- emitSelfHealMessages(files[0]);
233
+ const module = await import(`${BASE_URL}convention/index.js`);
234
+ const result = await module.validateCodeQuality({
235
+ targetPath: files[0],
236
+ projectPath: PROJECT_DIR,
237
+ });
238
+ const text = result.content[0].text;
239
+ // Output P1/P2 only — skip P3 (style)
240
+ 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]);
247
246
  }
247
+ emitSelfHealMessages(files[0]);
248
248
  } catch {
249
249
  // Silently continue on check failure — never block progress
250
250
  }
251
251
 
252
252
  // 2. 관찰 자동 캡처
253
253
  try {
254
- const files = getModifiedFiles();
255
- if (files.length === 0) return;
256
-
257
254
  const memModule = await import(`${BASE_URL}memory/index.js`);
258
255
  const { type, title } = classifyObservation(files);
259
256
 
@@ -37,11 +37,15 @@ function childEnv() {
37
37
 
38
38
  function runScript(scriptName, args = []) {
39
39
  const scriptPath = path.join(__dirname, scriptName);
40
+ // prompt-dispatcher 는 명시적 외부 LLM 호출(hook 모드 최대 ~50s)을 포함할 수 있어
41
+ // 그보다 약간 긴 timeout 으로 감싼다. 나머지 경량 스크립트는 30s. (B-2 정합 —
42
+ // 30s 고정이면 prompt-dispatcher 의 외부 LLM 호출을 다시 hard-kill 한다)
43
+ const timeout = scriptName.includes('prompt-dispatcher') ? 55000 : 30000;
40
44
  return spawnSync(process.execPath, [scriptPath, ...args], {
41
45
  input: stdinData || JSON.stringify(payload),
42
46
  encoding: 'utf-8',
43
47
  env: childEnv(),
44
- timeout: 30000,
48
+ timeout,
45
49
  });
46
50
  }
47
51
 
@@ -36,6 +36,7 @@ const MAGIC_KEYWORDS = {
36
36
  description: 'Planning interview mode',
37
37
  flags: ['planning', 'interview'],
38
38
  output: '[PLAN MODE] Enter planning interview mode. Gather requirements before implementation.',
39
+ strict: true, // 일상어 — 명령 끝 위치 또는 --plan 플래그에서만 (오탐 방지)
39
40
  },
40
41
 
41
42
  // Ralph + Plan 조합
@@ -52,6 +53,7 @@ const MAGIC_KEYWORDS = {
52
53
  description: 'Strict verification after each step',
53
54
  flags: ['verification', 'strict'],
54
55
  output: '[VERIFY MODE] Strict verification enabled. Every change must be verified before proceeding.',
56
+ strict: true, // 일상어 ("please verify the fix" 오탐 방지)
55
57
  },
56
58
 
57
59
  // 탐색 모드
@@ -60,6 +62,7 @@ const MAGIC_KEYWORDS = {
60
62
  description: 'Deep codebase exploration',
61
63
  flags: ['exploration', 'thorough'],
62
64
  output: '[EXPLORE MODE] Deep exploration enabled. Use multiple Explore agents for thorough codebase analysis.',
65
+ strict: true, // 일상어 ("let me explore the options" 오탐 방지)
63
66
  },
64
67
 
65
68
  // 빠른 모드
@@ -68,6 +71,7 @@ const MAGIC_KEYWORDS = {
68
71
  description: 'Fast execution, minimal verification',
69
72
  flags: ['fast', 'minimal_verification'],
70
73
  output: '[QUICK MODE] Fast execution mode. Minimal verification, single round reviews.',
74
+ strict: true, // 일상어 ("quick question on auth" 오탐 방지)
71
75
  },
72
76
  };
73
77
 
@@ -96,8 +100,16 @@ function detectKeywords(text) {
96
100
  const resolvedKeywords = new Map();
97
101
 
98
102
  for (const [keyword, config] of Object.entries(MAGIC_KEYWORDS)) {
99
- // 키워드 매칭 (단어 경계)
100
- const regex = new RegExp(`\\b${keyword}\\b`, 'i');
103
+ // alias는 본체의 strict 설정을 따른다.
104
+ const target = config.alias ? MAGIC_KEYWORDS[config.alias] : config;
105
+ // strict 키워드(일상어: quick/plan/verify/explore)는 단어경계만으로는
106
+ // "quick question", "please verify" 등에 오탐한다. 따라서
107
+ // ① 명령 끝 위치의 독립 토큰 ("... 만들어줘 quick") 또는
108
+ // ② 명시 플래그 ("--quick", "-quick", "/quick")
109
+ // 에서만 매칭한다. 조어(ralph/ultrawork)는 오탐이 거의 없어 단어경계 유지.
110
+ const regex = target.strict
111
+ ? new RegExp(`(?:(?:^|\\s)--?${keyword}\\b)|(?:(?:^|\\s)${keyword}\\s*$)`, 'i')
112
+ : new RegExp(`\\b${keyword}\\b`, 'i');
101
113
  if (regex.test(lowerText)) {
102
114
  // alias 해결
103
115
  const resolved = config.alias ? MAGIC_KEYWORDS[config.alias] : config;
@@ -42,7 +42,9 @@ const mode = process.argv[3] || 'orchestrate';
42
42
  // WHY 3 retries: Enough to ride out brief 503/overload blips (typically 1-2
43
43
  // consecutive), but not so many that a genuinely down provider delays the
44
44
  // fallback chain for minutes.
45
- const MAX_RETRIES = 3;
45
+ // VIBE_LLM_MAX_RETRIES override: UserPromptSubmit(hook) 모드에서는 1(재시도 없음)로
46
+ // 낮춰 부모 dispatcher timeout 안에 단일 시도로 끝낸다. (B-2: hard-kill/무음실패 방지)
47
+ const MAX_RETRIES = Number(process.env.VIBE_LLM_MAX_RETRIES) || 3;
46
48
  // WHY 2000ms initial delay: LLM rate-limit windows are typically 1-5s;
47
49
  // starting at 2s with exponential backoff (2s, 4s, 8s) covers most reset intervals.
48
50
  const INITIAL_DELAY_MS = 2000;
@@ -203,7 +205,9 @@ function parseAnalyzeImageArgs(args) {
203
205
  // CLI Provider Functions
204
206
  // ============================================
205
207
 
206
- const CLI_TIMEOUT_MS = 180000;
208
+ // VIBE_LLM_PRIMARY_TIMEOUT_MS override: hook 모드에서는 부모 dispatcher timeout 보다
209
+ // 짧게 잡아(예: 45s) 자식이 스스로 정리하고 의미있는 메시지를 반환하게 한다.
210
+ const CLI_TIMEOUT_MS = Number(process.env.VIBE_LLM_PRIMARY_TIMEOUT_MS) || 180000;
207
211
  const CLI_FALLBACK_TIMEOUT_MS = 30000;
208
212
  const IS_WINDOWS = os.platform() === 'win32';
209
213
 
@@ -599,6 +603,13 @@ async function main() {
599
603
  providerChain = claudeSecondary ? ['antigravity', 'claude'] : ['antigravity', 'gpt'];
600
604
  }
601
605
 
606
+ // hook(UserPromptSubmit) 모드: 사용자가 `gpt`/`agy` 접두사로 명시적으로 부른 단발
607
+ // 보조 호출이라 cross-provider fallback 이 불필요하다. primary 1개로 단축해
608
+ // 부모 dispatcher timeout 안에 단일 시도로 끝낸다. (B-2)
609
+ if (process.env.VIBE_LLM_HOOK_MODE) {
610
+ providerChain = [providerChain[0]];
611
+ }
612
+
602
613
  const vibeConfig = readVibeConfig();
603
614
 
604
615
  for (let i = 0; i < providerChain.length; i++) {
@@ -82,39 +82,47 @@ const DISPATCH_RULES = [
82
82
  label: 'e2e-echo',
83
83
  },
84
84
 
85
- // 외부 LLM 호출 (GPT/Antigravity) - 패턴 매칭 필수
85
+ // 외부 LLM 호출 (GPT/Antigravity) 명시적 provider 접두사 필수.
86
+ //
87
+ // 과거에는 "추론해", "코드 리뷰", "디버깅해" 같은 자연어 패턴이 prompt
88
+ // 어디서든 매칭되어, 평범한 한국어/영어 요청에도 외부 LLM이 동기로 spawn되고
89
+ // 그 응답이 컨텍스트에 주입되었다(컨텍스트 오염 + 최대 30s 블로킹).
90
+ // 이제는 prompt가 `gpt`/`agy`/`antigravity` 로 **시작**할 때만 발동한다.
91
+ // 즉 사용자가 외부 LLM을 콕 집어 부를 때만 동작하고, 일상 요청엔 걸리지 않는다.
92
+ // (참고: `/vibe.reason` 스킬이 일반 추론을 담당하므로 자연어 자동호출은 불필요)
93
+ // `s` 플래그(dotAll)로 여러 줄 prompt 의 역할 키워드도 매칭한다.
86
94
  {
87
- pattern: /아키텍처.*(검토|리뷰|분석)|architecture.*(review|analyz)|설계.*검토|구조.*분석.*해/i,
95
+ pattern: /^\s*gpt\b.*(아키텍처|architecture|설계|구조)/is,
88
96
  script: 'llm-orchestrate.js',
89
97
  args: ['gpt', 'orchestrate', 'You are a software architect. Analyze and review the architecture.'],
90
98
  label: 'gpt-architecture',
91
99
  },
92
100
  {
93
- pattern: /(UI|UX).*(리뷰|검토|피드백|개선)|사용자.*경험.*검토|디자인.*리뷰|design.*feedback/i,
101
+ pattern: /^\s*(agy|antigravity)\b.*(ui|ux|디자인|design|사용자.*경험)/is,
94
102
  script: 'llm-orchestrate.js',
95
103
  args: ['antigravity', 'orchestrate', 'You are a UI/UX expert. Analyze and provide feedback.'],
96
104
  label: 'antigravity-uiux',
97
105
  },
98
106
  {
99
- pattern: /디버깅.*해|버그.*찾아|find.*bug|debug.*this.*code/i,
107
+ pattern: /^\s*gpt\b.*(디버깅|debug|버그|bug)/is,
100
108
  script: 'llm-orchestrate.js',
101
109
  args: ['gpt', 'orchestrate', 'You are a debugging expert. Find bugs and suggest fixes.'],
102
110
  label: 'gpt-debug',
103
111
  },
104
112
  {
105
- pattern: /코드.*정적.*분석|코드.*분석.*해줘|analyze.*code.*quality/i,
113
+ pattern: /^\s*(agy|antigravity)\b.*(분석|analyz|코드.*품질|code.*quality)/is,
106
114
  script: 'llm-orchestrate.js',
107
115
  args: ['antigravity', 'orchestrate', 'You are a code analysis expert. Review and analyze the code.'],
108
116
  label: 'antigravity-analysis',
109
117
  },
110
118
  {
111
- pattern: /코드.*리뷰|code.*review|PR.*리뷰|리뷰.*해줘.*코드/i,
119
+ pattern: /^\s*gpt\b.*(리뷰|review)/is,
112
120
  script: 'llm-orchestrate.js',
113
121
  args: ['gpt', 'orchestrate', 'You are a code review expert. Review the code for best practices, security, and performance.'],
114
122
  label: 'gpt-codereview',
115
123
  },
116
124
  {
117
- pattern: /추론.*해|reasoning|복잡.*분석|deep.*analysis/i,
125
+ pattern: /^\s*gpt\b.*(추론|reasoning|복잡.*분석|deep.*analysis)/is,
118
126
  script: 'llm-orchestrate.js',
119
127
  args: ['gpt', 'orchestrate', 'You are a reasoning expert. Analyze the problem deeply and provide detailed reasoning.'],
120
128
  label: 'gpt-reasoning',
@@ -155,14 +163,29 @@ for (const rule of DISPATCH_RULES) {
155
163
  const scriptPath = path.join(__dirname, rule.script);
156
164
  const args = rule.args || [];
157
165
 
166
+ // 외부 LLM 호출(llm-orchestrate)은 부모/자식 timeout 을 정합시킨다 (B-2):
167
+ // 자식은 hook 모드(primary 45s, fallback 없음, retry 없음)로 단일 시도 후 스스로
168
+ // 정리하고, 부모는 그보다 약간 긴 50s 로 감싸 hard-kill 을 피한다.
169
+ // 경량 스크립트(keyword-detector 등)는 기존 30s 유지.
170
+ const isLlm = rule.script === 'llm-orchestrate.js';
171
+ const execTimeout = isLlm ? 50000 : 30000;
172
+ const childEnv = isLlm
173
+ ? { ...process.env, VIBE_LLM_HOOK_MODE: '1', VIBE_LLM_PRIMARY_TIMEOUT_MS: '45000', VIBE_LLM_MAX_RETRIES: '1' }
174
+ : { ...process.env };
175
+
158
176
  execPromises.push(
159
177
  new Promise((resolve) => {
160
178
  execFile('node', [scriptPath, ...args], {
161
- timeout: 30000,
162
- env: { ...process.env },
179
+ timeout: execTimeout,
180
+ env: childEnv,
163
181
  }, (error, stdout, stderr) => {
164
182
  if (stdout?.trim()) {
165
183
  process.stdout.write(stdout);
184
+ } else if (error) {
185
+ // 무음실패 방지 — 외부 LLM 호출이 timeout 또는 에러로 결과 없이 죽을 때,
186
+ // 사용자가 원인을 알 수 있도록 한 줄만 노출한다.
187
+ const reason = error.killed ? `timed out (${execTimeout / 1000}s)` : (error.message || 'failed');
188
+ process.stdout.write(`[${rule.label}] external LLM call ${reason} — no result injected.\n`);
166
189
  }
167
190
  resolve();
168
191
  });
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * SessionStart Hook - 세션 시작 시 메모리/시간 로드 + 버전 체크
3
3
  */
4
- import { getToolsBaseUrl, PROJECT_DIR, projectVibePath, projectVibeRoot } from './utils.js';
4
+ import { getToolsBaseUrl, getGlobalNpmPath, PROJECT_DIR, projectVibePath, projectVibeRoot } from './utils.js';
5
5
  import fs from 'fs';
6
6
  import path from 'path';
7
7
  import os from 'os';
8
8
  import https from 'https';
9
- import { execSync } from 'child_process';
10
9
 
11
10
  const BASE_URL = getToolsBaseUrl();
12
11
 
@@ -33,6 +32,30 @@ function fetchLatestVersion() {
33
32
  });
34
33
  }
35
34
 
35
+ // 버전 체크 결과 24시간 파일 캐시 — 매 SessionStart마다 npm registry에
36
+ // HTTP 요청(타임아웃 시 3초 블로킹)을 보내지 않도록 한다. 릴리즈 주기 대비
37
+ // 하루 1회 확인이면 충분하다.
38
+ const VERSION_CACHE_PATH = path.join(os.homedir(), '.vibe', 'version-check.json');
39
+ const VERSION_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
40
+
41
+ async function getLatestVersionCached() {
42
+ try {
43
+ const cached = JSON.parse(fs.readFileSync(VERSION_CACHE_PATH, 'utf8'));
44
+ if (cached.version && Date.now() - cached.checkedAt < VERSION_CACHE_TTL_MS) {
45
+ return cached.version;
46
+ }
47
+ } catch { /* 캐시 없음/손상 → 네트워크 조회 */ }
48
+
49
+ const version = await fetchLatestVersion();
50
+ if (version) {
51
+ try {
52
+ fs.mkdirSync(path.dirname(VERSION_CACHE_PATH), { recursive: true });
53
+ fs.writeFileSync(VERSION_CACHE_PATH, JSON.stringify({ version, checkedAt: Date.now() }));
54
+ } catch { /* 캐시 기록 실패는 무시 */ }
55
+ }
56
+ return version;
57
+ }
58
+
36
59
  function compareVersions(a, b) {
37
60
  const partsA = a.replace(/^v/, '').split('.').map(Number);
38
61
  const partsB = b.replace(/^v/, '').split('.').map(Number);
@@ -47,18 +70,8 @@ function compareVersions(a, b) {
47
70
 
48
71
  function getCurrentVersion() {
49
72
  try {
50
- let globalNpmPath;
51
- try {
52
- globalNpmPath = execSync('npm root -g', { encoding: 'utf8', timeout: 3000 }).trim();
53
- } catch {
54
- const homeDir = os.homedir();
55
- const fallbacks = [
56
- '/usr/local/lib/node_modules',
57
- path.join(homeDir, '.npm-global', 'lib', 'node_modules'),
58
- ];
59
- globalNpmPath = fallbacks.find(p => fs.existsSync(p)) || fallbacks[0];
60
- }
61
- const pkgPath = path.join(globalNpmPath, '@su-record', 'vibe', 'package.json');
73
+ // getToolsBaseUrl()이 이미 `npm root -g` 결과를 캐싱하므로 재사용 — 중복 spawn 제거
74
+ const pkgPath = path.join(getGlobalNpmPath(), '@su-record', 'vibe', 'package.json');
62
75
  if (fs.existsSync(pkgPath)) {
63
76
  return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version || null;
64
77
  }
@@ -77,7 +90,7 @@ async function main() {
77
90
  memoryModule.startSession({ projectPath: PROJECT_DIR }),
78
91
  timeModule.getCurrentTime({ format: 'human', timezone: 'Asia/Seoul' }),
79
92
  memoryModule.listMemories({ limit: 5, projectPath: PROJECT_DIR }),
80
- fetchLatestVersion(),
93
+ getLatestVersionCached(),
81
94
  ]);
82
95
 
83
96
  console.log(session.content[0].text);
@@ -75,32 +75,44 @@ export function projectMemoryDir(projectDir = PROJECT_DIR) {
75
75
  return path.join(projectDir, '.vibe', 'memories');
76
76
  }
77
77
 
78
+ // config 캐시 — 훅 스크립트는 단명 프로세스이므로 프로세스 생명주기 내에서
79
+ // config 파일이 바뀌지 않는다고 가정한다 (llm-orchestrate처럼 한 프로세스에서
80
+ // 3회 이상 읽는 경로의 중복 read+parse 제거).
81
+ let _vibeConfigCache = null;
82
+ let _projectConfigCache = null;
83
+
78
84
  /**
79
- * ~/.vibe/config.json 읽기
85
+ * ~/.vibe/config.json 읽기 (프로세스 내 캐시)
80
86
  * @returns {object} 파싱된 config 또는 빈 객체
81
87
  */
82
88
  export function readVibeConfig() {
89
+ if (_vibeConfigCache !== null) return _vibeConfigCache;
83
90
  const configPath = path.join(VIBE_HOME_DIR, 'config.json');
84
91
  try {
85
92
  if (fs.existsSync(configPath)) {
86
- return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
93
+ _vibeConfigCache = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
94
+ return _vibeConfigCache;
87
95
  }
88
96
  } catch { /* ignore */ }
89
- return {};
97
+ _vibeConfigCache = {};
98
+ return _vibeConfigCache;
90
99
  }
91
100
 
92
101
  /**
93
- * 프로젝트 설정(.vibe/config.json) 읽기 — legacy `.claude/vibe/` fallback 포함
102
+ * 프로젝트 설정(.vibe/config.json) 읽기 — legacy `.claude/vibe/` fallback 포함 (프로세스 내 캐시)
94
103
  * @returns {object} 파싱된 config 또는 빈 객체
95
104
  */
96
105
  export function readProjectConfig() {
106
+ if (_projectConfigCache !== null) return _projectConfigCache;
97
107
  const configPath = projectVibePath(PROJECT_DIR, 'config.json');
98
108
  try {
99
109
  if (fs.existsSync(configPath)) {
100
- return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
110
+ _projectConfigCache = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
111
+ return _projectConfigCache;
101
112
  }
102
113
  } catch { /* ignore */ }
103
- return {};
114
+ _projectConfigCache = {};
115
+ return _projectConfigCache;
104
116
  }
105
117
 
106
118
  /**
@@ -153,7 +165,7 @@ function toFileUrl(fsPath) {
153
165
 
154
166
  // 전역 npm 경로 캐시
155
167
  let _globalNpmPath = null;
156
- function getGlobalNpmPath() {
168
+ export function getGlobalNpmPath() {
157
169
  if (_globalNpmPath === null) {
158
170
  try {
159
171
  _globalNpmPath = execSync('npm root -g', { encoding: 'utf8' }).trim();
@@ -190,20 +202,32 @@ function hasRuntimeDeps(packageRoot) {
190
202
  * 패키지 하위 경로의 file:// URL 반환 (크로스 플랫폼)
191
203
  * 우선순위: 로컬 빌드 → ~/.vibe/ → 전역 npm
192
204
  * 각 후보는 probeFile 존재 + 런타임 deps 해석 가능해야 채택 (dist-only 복사본 회피)
205
+ *
206
+ * 프로세스 내 메모이제이션: getToolsBaseUrl/getLibBaseUrl/getCliBaseUrl이 한 훅
207
+ * 프로세스에서 각각 호출되면 최악 6회 동기 stat이 반복되므로 결과를 캐싱한다.
193
208
  */
209
+ const _packageUrlCache = new Map();
194
210
  function getPackageUrl(subpath, probeFile) {
211
+ const cacheKey = `${subpath}|${probeFile}`;
212
+ const cached = _packageUrlCache.get(cacheKey);
213
+ if (cached) return cached;
214
+
195
215
  const localRoot = VIBE_PATH;
196
216
  const vibeRoot = path.join(VIBE_HOME_DIR, 'node_modules', '@su-record', 'vibe');
197
217
  const globalRoot = path.join(getGlobalNpmPath(), '@su-record', 'vibe');
198
218
 
199
219
  const candidates = [localRoot, vibeRoot, globalRoot];
220
+ let result = null;
200
221
  for (const root of candidates) {
201
222
  const target = path.join(root, subpath);
202
223
  if (fs.existsSync(path.join(target, probeFile)) && hasRuntimeDeps(root)) {
203
- return toFileUrl(target);
224
+ result = toFileUrl(target);
225
+ break;
204
226
  }
205
227
  }
206
- return toFileUrl(path.join(globalRoot, subpath));
228
+ if (result === null) result = toFileUrl(path.join(globalRoot, subpath));
229
+ _packageUrlCache.set(cacheKey, result);
230
+ return result;
207
231
  }
208
232
 
209
233
  export function getToolsBaseUrl() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@su-record/vibe",
3
- "version": "2.12.3",
3
+ "version": "2.12.5",
4
4
  "description": "AI Coding Framework for Claude Code — 56 agents, 45 skills, multi-LLM orchestration",
5
5
  "type": "module",
6
6
  "main": "dist/cli/index.js",
@@ -3,8 +3,8 @@ name: arch-guard
3
3
  user-invocable: false
4
4
  invocation: [auto]
5
5
  tier: core
6
- description: "Generate architecture boundary tests that mechanically enforce layer constraints. Use when adding new modules, refactoring layers, or after detecting circular dependencies. Creates import-rule tests (e.g., 'UI must not import DB') that fail CI on violation. Must use this skill when user mentions layer enforcement, dependency rules, or architectural boundaries — even casually like 'make sure services don't import controllers'."
7
- triggers: [arch guard, architecture test, layer test, boundary test, structural test, arch validation]
6
+ description: "Generate import-rule tests that mechanically enforce architecture layer constraints (e.g., 'UI must not import DB') for new modules, layer refactors, or circular dependencies."
7
+ triggers: [arch guard, architecture test, layer test, boundary test, structural test, arch validation, layer enforcement, dependency rules, architectural boundaries, circular dependency]
8
8
  priority: 60
9
9
  ---
10
10
 
@@ -3,8 +3,8 @@ name: characterization-test
3
3
  user-invocable: false
4
4
  invocation: [auto]
5
5
  tier: core
6
- description: "Lock existing behavior with characterization tests before modifying code. Use BEFORE any refactor, rewrite, or large-scale modification of existing code — especially legacy code without tests. Captures current input/output behavior as test cases so regressions are caught immediately. Must use this skill when touching files >200 lines with no existing tests, when user says 'refactor', 'rewrite', 'modernize', or 'clean up' existing code."
7
- triggers: [legacy, characterization test, lock behavior, regression prevention, before refactor, large file]
6
+ description: "Lock existing behavior with characterization tests BEFORE refactoring or modifying legacy/untested code (files >200 lines without tests)."
7
+ triggers: [legacy, characterization test, lock behavior, regression prevention, before refactor, large file, refactor, rewrite, modernize, clean up]
8
8
  priority: 65
9
9
  ---
10
10