@su-record/vibe 2.9.12 → 2.9.14

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 (44) hide show
  1. package/README.ko.md +12 -1
  2. package/README.md +12 -1
  3. package/dist/cli/commands/info.d.ts.map +1 -1
  4. package/dist/cli/commands/info.js +3 -2
  5. package/dist/cli/commands/info.js.map +1 -1
  6. package/dist/cli/postinstall/constants.d.ts.map +1 -1
  7. package/dist/cli/postinstall/constants.js +2 -0
  8. package/dist/cli/postinstall/constants.js.map +1 -1
  9. package/dist/cli/postinstall/main.d.ts.map +1 -1
  10. package/dist/cli/postinstall/main.js +62 -52
  11. package/dist/cli/postinstall/main.js.map +1 -1
  12. package/dist/cli/setup/ProjectSetup.d.ts.map +1 -1
  13. package/dist/cli/setup/ProjectSetup.js +15 -0
  14. package/dist/cli/setup/ProjectSetup.js.map +1 -1
  15. package/dist/cli/utils/cli-detector.d.ts +14 -1
  16. package/dist/cli/utils/cli-detector.d.ts.map +1 -1
  17. package/dist/cli/utils/cli-detector.js +36 -1
  18. package/dist/cli/utils/cli-detector.js.map +1 -1
  19. package/dist/infra/lib/codex-proxy.d.ts.map +1 -1
  20. package/dist/infra/lib/codex-proxy.js +22 -6
  21. package/dist/infra/lib/codex-proxy.js.map +1 -1
  22. package/dist/infra/lib/llm-availability.d.ts +5 -2
  23. package/dist/infra/lib/llm-availability.d.ts.map +1 -1
  24. package/dist/infra/lib/llm-availability.js +11 -4
  25. package/dist/infra/lib/llm-availability.js.map +1 -1
  26. package/dist/infra/orchestrator/LLMCluster.d.ts +11 -2
  27. package/dist/infra/orchestrator/LLMCluster.d.ts.map +1 -1
  28. package/dist/infra/orchestrator/LLMCluster.js +25 -5
  29. package/dist/infra/orchestrator/LLMCluster.js.map +1 -1
  30. package/hooks/hooks.json +10 -58
  31. package/hooks/scripts/__tests__/pre-tool-guard.test.js +4 -3
  32. package/hooks/scripts/__tests__/sentinel-guard.test.js +6 -8
  33. package/hooks/scripts/figma-guard.js +2 -3
  34. package/hooks/scripts/lib/dispatcher.js +83 -0
  35. package/hooks/scripts/llm-orchestrate.js +84 -11
  36. package/hooks/scripts/post-edit-dispatcher.js +24 -0
  37. package/hooks/scripts/pre-tool-dispatcher.js +31 -0
  38. package/hooks/scripts/pre-tool-guard.js +2 -3
  39. package/hooks/scripts/prompt-dispatcher.js +5 -0
  40. package/hooks/scripts/sentinel-guard.js +2 -3
  41. package/hooks/scripts/stop-dispatcher.js +27 -0
  42. package/package.json +1 -1
  43. package/skills/rob-pike/SKILL.md +64 -0
  44. package/skills/systematic-debugging/SKILL.md +140 -0
package/hooks/hooks.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "_comment": "WHY this hook ordering: SessionStart initializes state; PreToolUse runs sentinel-guard BEFORE pre-tool-guard because sentinel blocks dangerous commands while pre-tool-guard does finer-grained checks; UserPromptSubmit interrupt echo MUST precede prompt-dispatcher so cancelled tasks are not resumed; Notification thresholds (80/90/95%) trigger progressively urgent context saves.",
2
+ "_comment": "Dispatcher pattern PreToolUse/PostToolUse/Stop는 단일 디스패처가 stdin을 읽어 순차 실행 (병렬 spawn 폭주 제거, cascade 격리, config.hooks.{name}.enabled 토글 지원). UserPromptSubmit VIBE_HOOK_DEPTH 재귀 가드 탑재된 prompt-dispatcher 사용.",
3
3
  "permissions": {
4
4
  "allow": [],
5
5
  "deny": [],
@@ -16,91 +16,55 @@
16
16
  ]
17
17
  }
18
18
  ],
19
- "_comment_PreToolUse": "WHY sentinel-guard runs before pre-tool-guard: sentinel blocks catastrophic commands (rm -rf, etc.) early; pre-tool-guard handles project-specific restrictions. Order matters — fail fast on danger.",
20
19
  "PreToolUse": [
21
20
  {
22
21
  "matcher": "Bash",
23
22
  "hooks": [
24
23
  {
25
24
  "type": "command",
26
- "command": "node {{VIBE_PATH}}/hooks/scripts/sentinel-guard.js Bash"
27
- },
28
- {
29
- "type": "command",
30
- "command": "node {{VIBE_PATH}}/hooks/scripts/pre-tool-guard.js Bash"
31
- },
32
- {
33
- "type": "command",
34
- "command": "node {{VIBE_PATH}}/hooks/scripts/command-log.js"
25
+ "command": "node {{VIBE_PATH}}/hooks/scripts/pre-tool-dispatcher.js Bash"
35
26
  }
36
27
  ]
37
28
  },
38
29
  {
39
- "matcher": "mcp__github__create_pull_request",
30
+ "matcher": "Edit",
40
31
  "hooks": [
41
32
  {
42
33
  "type": "command",
43
- "command": "node {{VIBE_PATH}}/hooks/scripts/pr-test-gate.js"
34
+ "command": "node {{VIBE_PATH}}/hooks/scripts/pre-tool-dispatcher.js Edit"
44
35
  }
45
36
  ]
46
37
  },
47
38
  {
48
- "matcher": "Edit",
39
+ "matcher": "Write",
49
40
  "hooks": [
50
41
  {
51
42
  "type": "command",
52
- "command": "node {{VIBE_PATH}}/hooks/scripts/sentinel-guard.js Edit"
53
- },
54
- {
55
- "type": "command",
56
- "command": "node {{VIBE_PATH}}/hooks/scripts/pre-tool-guard.js Edit"
43
+ "command": "node {{VIBE_PATH}}/hooks/scripts/pre-tool-dispatcher.js Write"
57
44
  }
58
45
  ]
59
46
  },
60
47
  {
61
- "matcher": "Write",
48
+ "matcher": "mcp__github__create_pull_request",
62
49
  "hooks": [
63
50
  {
64
51
  "type": "command",
65
- "command": "node {{VIBE_PATH}}/hooks/scripts/sentinel-guard.js Write"
66
- },
67
- {
68
- "type": "command",
69
- "command": "node {{VIBE_PATH}}/hooks/scripts/pre-tool-guard.js Write"
52
+ "command": "node {{VIBE_PATH}}/hooks/scripts/pr-test-gate.js"
70
53
  }
71
54
  ]
72
55
  }
73
56
  ],
74
- "_comment_PostToolUse": "WHY ordering: format first (normalize style), then lint/check (catch issues on clean code), then test (verify behavior). This chain ensures review-ready code on every edit.",
75
57
  "PostToolUse": [
76
58
  {
77
59
  "matcher": "Write|Edit",
78
60
  "hooks": [
79
61
  {
80
62
  "type": "command",
81
- "command": "node {{VIBE_PATH}}/hooks/scripts/auto-format.js"
82
- },
83
- {
84
- "type": "command",
85
- "command": "node {{VIBE_PATH}}/hooks/scripts/code-check.js"
86
- },
87
- {
88
- "type": "command",
89
- "command": "node {{VIBE_PATH}}/hooks/scripts/auto-test.js"
90
- }
91
- ]
92
- },
93
- {
94
- "matcher": "Edit",
95
- "hooks": [
96
- {
97
- "type": "command",
98
- "command": "node {{VIBE_PATH}}/hooks/scripts/post-edit.js"
63
+ "command": "node {{VIBE_PATH}}/hooks/scripts/post-edit-dispatcher.js"
99
64
  }
100
65
  ]
101
66
  }
102
67
  ],
103
- "_comment_UserPromptSubmit": "WHY interrupt echo is first: It injects a cancellation signal into the context BEFORE the dispatcher processes the new prompt, preventing the LLM from resuming cancelled work.",
104
68
  "UserPromptSubmit": [
105
69
  {
106
70
  "hooks": [
@@ -153,19 +117,7 @@
153
117
  "hooks": [
154
118
  {
155
119
  "type": "command",
156
- "command": "node {{VIBE_PATH}}/hooks/scripts/codex-review-gate.js"
157
- },
158
- {
159
- "type": "command",
160
- "command": "node {{VIBE_PATH}}/hooks/scripts/stop-notify.js"
161
- },
162
- {
163
- "type": "command",
164
- "command": "node {{VIBE_PATH}}/hooks/scripts/auto-commit.js"
165
- },
166
- {
167
- "type": "command",
168
- "command": "node {{VIBE_PATH}}/hooks/scripts/devlog-gen.js"
120
+ "command": "node {{VIBE_PATH}}/hooks/scripts/stop-dispatcher.js"
169
121
  }
170
122
  ]
171
123
  }
@@ -23,13 +23,14 @@ function runGuard({ args = [] } = {}) {
23
23
  }
24
24
 
25
25
  /**
26
- * Run pre-tool-guard.js with stdin JSON payload (using shell pipe).
26
+ * Run pre-tool-guard.js with stdin JSON payload.
27
+ * 스크립트가 fs.readSync(0, ...)로 stdin을 읽으므로 execFileSync input 옵션이 동작.
27
28
  */
28
29
  function runGuardWithStdin(payload) {
29
30
  const json = typeof payload === 'string' ? payload : JSON.stringify(payload);
30
- const escaped = json.replace(/'/g, "'\\''");
31
31
  try {
32
- const stdout = execSync(`echo '${escaped}' | node ${SCRIPT}`, {
32
+ const stdout = execFileSync('node', [SCRIPT], {
33
+ input: json,
33
34
  encoding: 'utf-8',
34
35
  timeout: 5000,
35
36
  });
@@ -23,16 +23,14 @@ function runGuard(args = []) {
23
23
  }
24
24
 
25
25
  /**
26
- * Run sentinel-guard.js with stdin JSON payload (using shell pipe).
27
- * The script reads stdin via fs.openSync('/dev/stdin'), which requires
28
- * a real pipe — execFileSync input option does not work.
26
+ * Run sentinel-guard.js with stdin JSON payload.
27
+ * 스크립트가 fs.readSync(0, ...)로 stdin 읽으므로 execFileSync input 옵션이 동작.
29
28
  */
30
29
  function runGuardWithStdin(payload) {
31
30
  const json = typeof payload === 'string' ? payload : JSON.stringify(payload);
32
- // Escape single quotes in JSON for shell safety
33
- const escaped = json.replace(/'/g, "'\\''");
34
31
  try {
35
- const stdout = execSync(`echo '${escaped}' | node ${SCRIPT}`, {
32
+ const stdout = execFileSync('node', [SCRIPT], {
33
+ input: json,
36
34
  encoding: 'utf-8',
37
35
  timeout: 5000,
38
36
  });
@@ -194,9 +192,9 @@ describe('sentinel-guard', () => {
194
192
  tool_name: 'Write',
195
193
  tool_input: { file_path: 'src/infra/lib/autonomy/x.ts' },
196
194
  });
197
- const escaped = payload.replace(/'/g, "'\\''");
198
195
  try {
199
- execSync(`echo '${escaped}' | node ${SCRIPT} Read '{}'`, {
196
+ execFileSync('node', [SCRIPT, 'Read', '{}'], {
197
+ input: payload,
200
198
  encoding: 'utf-8',
201
199
  timeout: 5000,
202
200
  });
@@ -26,10 +26,9 @@ import os from 'os';
26
26
  function readStdinSync() {
27
27
  try {
28
28
  if (process.stdin.isTTY) return null;
29
- const fd = fs.openSync('/dev/stdin', 'r');
29
+ // fd 0을 직접 사용 (Windows는 '/dev/stdin' 없음)
30
30
  const buf = Buffer.alloc(1024 * 1024); // 1MB
31
- const bytesRead = fs.readSync(fd, buf, 0, buf.length, null);
32
- fs.closeSync(fd);
31
+ const bytesRead = fs.readSync(0, buf, 0, buf.length, null);
33
32
  if (bytesRead > 0) {
34
33
  return JSON.parse(buf.toString('utf-8', 0, bytesRead));
35
34
  }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Hook dispatcher library — 여러 hook script를 단일 이벤트에서 직렬 실행.
3
+ *
4
+ * 목적:
5
+ * - 동일 이벤트에 등록된 N개 스크립트의 **병렬 spawn 폭주**를 순차화
6
+ * - stdin을 한 번만 읽어 각 자식에 그대로 pipe (중복 파싱/읽기 방지)
7
+ * - config.hooks[name].enabled 로 개별 토글
8
+ * - 한 스크립트 실패가 다음 실행을 막지 않도록 cascade 격리
9
+ * - PreToolUse 계열: 자식이 exit 2(deny)면 즉시 상위에 전파
10
+ */
11
+ import { spawn } from 'child_process';
12
+ import path from 'path';
13
+ import fs from 'fs';
14
+ import { fileURLToPath } from 'url';
15
+
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
+ const SCRIPTS_DIR = path.resolve(__dirname, '..');
18
+
19
+ function loadHookConfig() {
20
+ try {
21
+ const configPath = path.join(
22
+ process.env.CLAUDE_PROJECT_DIR || process.cwd(),
23
+ '.claude', 'vibe', 'config.json'
24
+ );
25
+ if (!fs.existsSync(configPath)) return {};
26
+ return JSON.parse(fs.readFileSync(configPath, 'utf-8')).hooks || {};
27
+ } catch {
28
+ return {};
29
+ }
30
+ }
31
+
32
+ function isEnabled(hookConfig, name) {
33
+ const entry = hookConfig[name];
34
+ if (entry && typeof entry === 'object' && entry.enabled === false) return false;
35
+ return true;
36
+ }
37
+
38
+ async function readStdin() {
39
+ if (process.stdin.isTTY) return '';
40
+ let data = '';
41
+ for await (const chunk of process.stdin) data += chunk;
42
+ return data;
43
+ }
44
+
45
+ /**
46
+ * 단일 스크립트 실행. stdin을 통해 입력 전달, stdout은 메인 stdout으로 통과.
47
+ * @returns {Promise<number>} exit code
48
+ */
49
+ function runScript(scriptName, args, stdinData, timeoutMs) {
50
+ return new Promise((resolve) => {
51
+ const scriptPath = path.join(SCRIPTS_DIR, scriptName);
52
+ const proc = spawn(process.execPath, [scriptPath, ...args], {
53
+ stdio: ['pipe', 'inherit', 'inherit'],
54
+ timeout: timeoutMs,
55
+ });
56
+ if (stdinData) proc.stdin.end(stdinData);
57
+ else proc.stdin.end();
58
+ proc.on('close', (code) => resolve(code ?? 0));
59
+ proc.on('error', () => resolve(1));
60
+ });
61
+ }
62
+
63
+ /**
64
+ * 디스패처 실행.
65
+ * @param {Array<{name: string, script: string, args?: string[], denyOnExit2?: boolean, timeoutMs?: number}>} steps
66
+ */
67
+ export async function dispatch(steps) {
68
+ const stdinData = await readStdin();
69
+ const hookConfig = loadHookConfig();
70
+
71
+ for (const step of steps) {
72
+ if (!isEnabled(hookConfig, step.name)) continue;
73
+ const code = await runScript(
74
+ step.script,
75
+ step.args || [],
76
+ stdinData,
77
+ step.timeoutMs || 30000
78
+ );
79
+ if (step.denyOnExit2 && code === 2) {
80
+ process.exit(2);
81
+ }
82
+ }
83
+ }
@@ -1,11 +1,11 @@
1
1
  /**
2
- * UserPromptSubmit Hook - LLM 오케스트레이션 (GPT/Gemini)
2
+ * UserPromptSubmit Hook - LLM 오케스트레이션 (GPT/Gemini/Claude)
3
3
  *
4
4
  * Usage:
5
5
  * node llm-orchestrate.js <provider> <mode> "prompt"
6
6
  * node llm-orchestrate.js <provider> <mode> "systemPrompt" "prompt"
7
7
  *
8
- * provider: gpt | gemini
8
+ * provider: gpt | gemini | claude
9
9
  * mode: orchestrate | orchestrate-json | image | analyze-image
10
10
  *
11
11
  * Image Mode:
@@ -13,7 +13,7 @@
13
13
  * node llm-orchestrate.js gemini image "prompt" --output "./image.png" --size "1920x1080"
14
14
  *
15
15
  * Features:
16
- * - CLI-based: GPT → codex exec, Gemini → gemini -p
16
+ * - CLI-based: GPT → codex exec, Gemini → gemini -p, Claude → claude --print
17
17
  * - Exponential backoff retry (3 attempts)
18
18
  * - Auto fallback: gpt ↔ gemini
19
19
  * - Overload/rate-limit detection
@@ -103,9 +103,31 @@ function resolveModel(providerName, config) {
103
103
  if (providerName === 'gpt-codex') return config.models?.gptCodex || 'gpt-5.3-codex';
104
104
  if (providerName === 'gpt') return config.models?.gpt || 'gpt-5.4';
105
105
  if (providerName === 'gemini') return config.models?.gemini || 'gemini-3.1-pro-preview';
106
+ if (providerName === 'claude') return 'claude';
106
107
  return providerName;
107
108
  }
108
109
 
110
+ /**
111
+ * 주관 LLM 자동 감지 — Claude가 보조로 사용되어야 하는 환경인지 판별
112
+ *
113
+ * true인 경우:
114
+ * - vibe-codex: ANTHROPIC_BASE_URL이 localhost (프록시 모드)
115
+ * - coco: ~/.coco/ 존재 또는 COCO_HOME 설정
116
+ * - 명시적: VIBE_SECONDARY_LLM=claude
117
+ */
118
+ function useClaudeAsSecondary() {
119
+ // 1. 명시적 환경변수
120
+ if (process.env.VIBE_SECONDARY_LLM === 'claude') return true;
121
+ // 2. vibe-codex 프록시 모드
122
+ const baseUrl = process.env.ANTHROPIC_BASE_URL || '';
123
+ if (baseUrl.includes('localhost') || baseUrl.includes('127.0.0.1')) return true;
124
+ // 3. coco 환경
125
+ if (process.env.COCO_HOME) return true;
126
+ const cocoDir = path.join(os.homedir(), '.coco');
127
+ if (fs.existsSync(cocoDir)) return true;
128
+ return false;
129
+ }
130
+
109
131
  // Errors that should skip retry and go to fallback immediately
110
132
  const SKIP_RETRY_PATTERNS = [
111
133
  /rate.?limit/i,
@@ -283,6 +305,43 @@ function callGeminiCli(prompt, sysPrompt, jsonMode, model, timeoutMs) {
283
305
  });
284
306
  }
285
307
 
308
+ function callClaudeCli(prompt, sysPrompt, jsonMode, timeoutMs) {
309
+ const fullPrompt = buildCliPrompt(prompt, sysPrompt, jsonMode);
310
+ const args = ['--print', '--dangerously-skip-permissions'];
311
+ const effectiveTimeout = timeoutMs || CLI_TIMEOUT_MS;
312
+
313
+ // 재귀 가드 — 자식 Claude 세션의 UserPromptSubmit hook이 또 claude CLI를
314
+ // spawn하는 포크 폭탄을 차단 (prompt-dispatcher.js가 이 env를 보고 즉시 종료).
315
+ const currentDepth = parseInt(process.env.VIBE_HOOK_DEPTH || '0', 10);
316
+ const childEnv = { ...process.env, VIBE_HOOK_DEPTH: String(currentDepth + 1) };
317
+
318
+ return new Promise((resolve, reject) => {
319
+ const proc = spawnCli('claude', args, {
320
+ stdio: ['pipe', 'pipe', 'pipe'],
321
+ timeout: effectiveTimeout,
322
+ env: childEnv,
323
+ });
324
+ proc.stdin.end(fullPrompt);
325
+
326
+ let stdout = '';
327
+ let stderr = '';
328
+ proc.stdout.on('data', (d) => { stdout += d.toString(); });
329
+ proc.stderr.on('data', (d) => { stderr += d.toString(); });
330
+
331
+ proc.on('close', (code) => {
332
+ if (code === 0 && stdout.trim()) {
333
+ resolve(stdout.trim());
334
+ } else {
335
+ reject(new Error(`claude cli failed (code ${code}): ${(stderr || stdout).slice(0, 500)}`));
336
+ }
337
+ });
338
+
339
+ proc.on('error', (err) => {
340
+ reject(new Error(`claude cli spawn error: ${err.message}`));
341
+ });
342
+ });
343
+ }
344
+
286
345
  async function callProvider(providerName, prompt, sysPrompt, jsonMode, timeoutMs) {
287
346
  const vibeConfig = readVibeConfig();
288
347
 
@@ -303,6 +362,10 @@ async function callProvider(providerName, prompt, sysPrompt, jsonMode, timeoutMs
303
362
  return await callGeminiCli(prompt, sysPrompt, jsonMode, model, timeoutMs);
304
363
  }
305
364
 
365
+ if (providerName === 'claude') {
366
+ return await callClaudeCli(prompt, sysPrompt, jsonMode, timeoutMs);
367
+ }
368
+
306
369
  throw new Error(`Unknown provider: ${providerName}`);
307
370
  }
308
371
 
@@ -526,14 +589,24 @@ async function main() {
526
589
  }
527
590
 
528
591
  // Provider chain: primary → cross fallback
529
- // WHY GPT → Gemini (not reverse): GPT is the primary code/reasoning model;
530
- // Gemini serves as cross-vendor fallback so a single vendor outage never
531
- // blocks the user. When Gemini is primary (e.g. web-search), GPT is fallback.
532
- const providerLabels = { gpt: 'GPT', 'gpt-codex': 'GPT Codex', gemini: 'Gemini' };
533
- const isGpt = provider === 'gpt' || provider === 'gpt-codex';
534
- const providerChain = isGpt
535
- ? [provider, 'gemini']
536
- : ['gemini', 'gpt'];
592
+ // 프록시 모드 (주관=GPT): 보조로 Claude CLI 사용
593
+ // 직접 모드 (주관=Claude): 보조로 GPT/Gemini 사용
594
+ const providerLabels = { gpt: 'GPT', 'gpt-codex': 'GPT Codex', gemini: 'Gemini', claude: 'Claude' };
595
+ const isGpt = provider === 'gpt' || provider === 'gpt-codex' || provider === 'gpt-spark';
596
+ const isClaude = provider === 'claude';
597
+ const claudeSecondary = useClaudeAsSecondary();
598
+
599
+ let providerChain;
600
+ if (isClaude) {
601
+ // 명시적 claude 호출
602
+ providerChain = ['claude', 'gemini'];
603
+ } else if (isGpt) {
604
+ // GPT 주관 → claude fallback (vibe-codex/coco), gemini fallback (직접 모드)
605
+ providerChain = claudeSecondary ? [provider, 'claude'] : [provider, 'gemini'];
606
+ } else {
607
+ // gemini 주관 → claude fallback (vibe-codex/coco), gpt fallback (직접 모드)
608
+ providerChain = claudeSecondary ? ['gemini', 'claude'] : ['gemini', 'gpt'];
609
+ }
537
610
 
538
611
  const vibeConfig = readVibeConfig();
539
612
 
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PostToolUse dispatcher — Write/Edit 이후 순차 실행.
4
+ *
5
+ * 기존: PostToolUse.Write|Edit 배열에 3개 스크립트가 병렬 spawn (프로세스 피크 3배)
6
+ * + PostToolUse.Edit 추가로 post-edit.js 1개 더
7
+ * 현재: 단일 디스패처에서 순차 실행. config.hooks.{name}.enabled로 개별 토글.
8
+ *
9
+ * 실행 순서:
10
+ * 1. auto-format — 코드 스타일 정규화
11
+ * 2. code-check — 린트/품질 검사
12
+ * 3. auto-test — 관련 테스트 실행
13
+ * 4. post-edit — Edit 전용 후처리 (Write에서는 스크립트 내부에서 스킵)
14
+ *
15
+ * 실패 격리: 한 스크립트 실패해도 다음은 계속 진행.
16
+ */
17
+ import { dispatch } from './lib/dispatcher.js';
18
+
19
+ await dispatch([
20
+ { name: 'auto-format', script: 'auto-format.js' },
21
+ { name: 'code-check', script: 'code-check.js' },
22
+ { name: 'auto-test', script: 'auto-test.js' },
23
+ { name: 'post-edit', script: 'post-edit.js' },
24
+ ]);
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PreToolUse dispatcher — Bash/Edit/Write 공용.
4
+ *
5
+ * 기존: matcher별로 2~3개 스크립트가 병렬 spawn.
6
+ * - Bash: sentinel-guard + pre-tool-guard + command-log
7
+ * - Edit: sentinel-guard + pre-tool-guard
8
+ * - Write: sentinel-guard + pre-tool-guard
9
+ * 현재: 단일 디스패처가 tool name을 인자로 받아 순차 실행.
10
+ *
11
+ * Deny 시맨틱 보존:
12
+ * sentinel-guard / pre-tool-guard가 exit 2(deny)를 반환하면 dispatcher도
13
+ * 즉시 exit 2로 상위에 전파 → Claude Code가 도구 실행을 차단.
14
+ *
15
+ * 사용법: node pre-tool-dispatcher.js <Bash|Edit|Write>
16
+ */
17
+ import { dispatch } from './lib/dispatcher.js';
18
+
19
+ const toolName = process.argv[2] || '';
20
+
21
+ const steps = [
22
+ { name: 'sentinel-guard', script: 'sentinel-guard.js', args: [toolName], denyOnExit2: true },
23
+ { name: 'pre-tool-guard', script: 'pre-tool-guard.js', args: [toolName], denyOnExit2: true },
24
+ ];
25
+
26
+ // command-log은 Bash 전용
27
+ if (toolName === 'Bash') {
28
+ steps.push({ name: 'command-log', script: 'command-log.js' });
29
+ }
30
+
31
+ await dispatch(steps);
@@ -160,10 +160,9 @@ function formatOutput(toolName, validation) {
160
160
  function readStdinSync() {
161
161
  try {
162
162
  if (process.stdin.isTTY) return null;
163
- const fd = fs.openSync('/dev/stdin', 'r');
163
+ // fd 0을 직접 사용 (Windows는 '/dev/stdin' 없음)
164
164
  const buf = Buffer.alloc(65536);
165
- const bytesRead = fs.readSync(fd, buf, 0, buf.length, null);
166
- fs.closeSync(fd);
165
+ const bytesRead = fs.readSync(0, buf, 0, buf.length, null);
167
166
  if (bytesRead > 0) {
168
167
  return JSON.parse(buf.toString('utf-8', 0, bytesRead));
169
168
  }
@@ -16,6 +16,11 @@ import path from 'path';
16
16
 
17
17
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
18
 
19
+ // 재귀 가드 — 자식 Claude 세션에서 이 hook이 다시 실행되는 것 차단.
20
+ // llm-orchestrate.js의 callClaudeCli가 VIBE_HOOK_DEPTH=1을 주입하므로,
21
+ // 값이 있으면 즉시 종료해 프로세스 폭탄을 막는다.
22
+ if (process.env.VIBE_HOOK_DEPTH) process.exit(0);
23
+
19
24
  // stdin에서 prompt 읽기
20
25
  let inputData = '';
21
26
  for await (const chunk of process.stdin) {
@@ -98,10 +98,9 @@ import fs from 'fs';
98
98
  function readStdinSync() {
99
99
  try {
100
100
  if (process.stdin.isTTY) return null;
101
- const fd = fs.openSync('/dev/stdin', 'r');
101
+ // fd 0을 직접 사용 (Windows는 '/dev/stdin' 없음)
102
102
  const buf = Buffer.alloc(65536);
103
- const bytesRead = fs.readSync(fd, buf, 0, buf.length, null);
104
- fs.closeSync(fd);
103
+ const bytesRead = fs.readSync(0, buf, 0, buf.length, null);
105
104
  if (bytesRead > 0) {
106
105
  return JSON.parse(buf.toString('utf-8', 0, bytesRead));
107
106
  }
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Stop dispatcher — Claude 응답 종료 시 4개 스크립트 순차 실행.
4
+ *
5
+ * 기존: Stop 배열에 4개 병렬 spawn (codex-review-gate + stop-notify + auto-commit + devlog-gen)
6
+ * → auto-commit의 git cascade와 겹쳐 프로세스 폭주 유발 가능.
7
+ * 현재: 단일 디스패처에서 순차 실행.
8
+ *
9
+ * 실행 순서:
10
+ * 1. codex-review-gate — 리뷰 필요 여부 판단 (stdout → Claude 지시 주입)
11
+ * 2. stop-notify — 완료 알림
12
+ * 3. auto-commit — 변경 자동 커밋 (git hook cascade 주의)
13
+ * 4. devlog-gen — 개발 로그 기록
14
+ *
15
+ * 재귀 가드 상속: callClaudeCli가 VIBE_HOOK_DEPTH=1 env를 자식에 주입했다면
16
+ * 이 Stop dispatcher도 건너뛴다 (자식 세션에서 auto-commit 등이 돌 이유 없음).
17
+ */
18
+ import { dispatch } from './lib/dispatcher.js';
19
+
20
+ if (process.env.VIBE_HOOK_DEPTH) process.exit(0);
21
+
22
+ await dispatch([
23
+ { name: 'codex-review-gate', script: 'codex-review-gate.js' },
24
+ { name: 'stop-notify', script: 'stop-notify.js' },
25
+ { name: 'auto-commit', script: 'auto-commit.js' },
26
+ { name: 'devlog-gen', script: 'devlog-gen.js' },
27
+ ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@su-record/vibe",
3
- "version": "2.9.12",
3
+ "version": "2.9.14",
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",
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: rob-pike
3
+ tier: core
4
+ description: "Rob Pike's 5 Rules — block premature optimization. Auto-activates on optimize, slow, performance, cache, parallelize keywords."
5
+ triggers: [optimize, slow, performance, cache, parallelize, bottleneck, speed up, faster, latency, benchmark]
6
+ priority: 90
7
+ ---
8
+
9
+ # Rob Pike's 5 Rules of Programming
10
+
11
+ ## The Rules
12
+
13
+ 1. **You can't tell where a program is going to spend its time.** Bottlenecks occur in surprising places. Don't guess — prove it.
14
+ 2. **Measure.** Don't tune for speed until you've measured. Even then, don't unless one part of the code overwhelms the rest.
15
+ 3. **Fancy algorithms are slow when n is small, and n is usually small.** Big-O doesn't matter when constants dominate. Use Rule 2 first.
16
+ 4. **Fancy algorithms are buggier than simple ones.** Use simple algorithms and simple data structures.
17
+ 5. **Data dominates.** Choose the right data structures and the algorithms become self-evident. "Write stupid code that uses smart objects."
18
+
19
+ ## Before Any Optimization
20
+
21
+ ### Step 0: Check for Existing Instrumentation
22
+
23
+ Before asking "have you measured?", determine whether measurement is even **possible** right now.
24
+
25
+ **Scan the codebase** for signs of existing instrumentation:
26
+ - Logging: logger imports, log calls, structured logging libraries
27
+ - Profiling: profiler imports, benchmark files, tracing setup
28
+ - Timing: duration measurements, stopwatch patterns, timing decorators
29
+ - APM/Observability: metrics exports, spans, trace contexts
30
+
31
+ **Then ask the user:**
32
+
33
+ 1. If instrumentation **exists**: "I found logging/profiling in [locations]. Are there specific areas you suspect are slow, or should we look at what the existing measurements tell us?"
34
+ 2. If instrumentation is **missing**: "There's no measurement in place. Before optimizing anything — where do you suspect the bottleneck is? Let's add measurement there first, then let the data decide."
35
+
36
+ ### Step 1: Ask the Measurement Questions
37
+
38
+ Stop and ask these questions in order:
39
+
40
+ 1. **"Have I measured?"** — If no, measure first.
41
+ 2. **"Does one part overwhelm the rest?"** — If no single area dominates, nothing worth optimizing.
42
+ 3. **"What's n?"** — If n is small (and it usually is), the simple O(n^2) approach likely beats the clever O(n log n) one.
43
+ 4. **"Is this a data structure problem?"** — Before changing the algorithm, consider whether a different data structure makes the problem trivial.
44
+ 5. **"Is the added complexity worth it?"** — Simple code that is 10% slower is almost always preferable to clever code that is fragile.
45
+
46
+ ## Anti-Patterns to Block
47
+
48
+ | Impulse | Rule violated | Response |
49
+ |---|---|---|
50
+ | "This loop looks slow, let me optimize it" | Rule 1 | Have you profiled? The bottleneck may be elsewhere entirely. |
51
+ | "Let me add a cache here" | Rule 2 | Measure first. Does this path actually dominate runtime? |
52
+ | "Let me use a B-tree / trie / skip list" | Rule 3 | What's n? If small, a sorted slice + binary search wins. |
53
+ | "Let me implement a custom allocator" | Rule 4 | Start simple. Measure. Only get fancy if data forces you. |
54
+ | "The algorithm is O(n^2), needs fixing" | Rule 3 | What's n? O(n^2) with n=100 is 10us. Measure first. |
55
+ | "Let me parallelize this" | Rule 2 | Is this actually CPU-bound? Measure. Often it's I/O. |
56
+
57
+ ## When Optimization IS Justified
58
+
59
+ Proceed only when ALL of these are true:
60
+
61
+ - You have measurement data showing a specific bottleneck
62
+ - That bottleneck dominates overall runtime (not just 5-10% of it)
63
+ - The proposed fix is the simplest change that addresses the measured problem
64
+ - You will re-measure after the change to confirm improvement