@su-record/vibe 2.12.5 → 2.13.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 (110) hide show
  1. package/CLAUDE.md +8 -2
  2. package/README.en.md +11 -11
  3. package/README.md +7 -7
  4. package/dist/cli/postinstall/fs-utils.d.ts +23 -0
  5. package/dist/cli/postinstall/fs-utils.d.ts.map +1 -1
  6. package/dist/cli/postinstall/fs-utils.js +71 -0
  7. package/dist/cli/postinstall/fs-utils.js.map +1 -1
  8. package/dist/cli/postinstall/fs-utils.test.js +69 -1
  9. package/dist/cli/postinstall/fs-utils.test.js.map +1 -1
  10. package/dist/cli/postinstall/main.d.ts.map +1 -1
  11. package/dist/cli/postinstall/main.js +12 -2
  12. package/dist/cli/postinstall/main.js.map +1 -1
  13. package/dist/cli/setup/CodexHooks.test.js +27 -0
  14. package/dist/cli/setup/CodexHooks.test.js.map +1 -1
  15. package/dist/cli/setup/ProjectSetup.js +2 -2
  16. package/dist/cli/setup/ProjectSetup.js.map +1 -1
  17. package/dist/infra/lib/DecisionTracer.d.ts +4 -0
  18. package/dist/infra/lib/DecisionTracer.d.ts.map +1 -1
  19. package/dist/infra/lib/DecisionTracer.js +4 -0
  20. package/dist/infra/lib/DecisionTracer.js.map +1 -1
  21. package/dist/infra/lib/LoopBreaker.d.ts +4 -0
  22. package/dist/infra/lib/LoopBreaker.d.ts.map +1 -1
  23. package/dist/infra/lib/LoopBreaker.js +4 -0
  24. package/dist/infra/lib/LoopBreaker.js.map +1 -1
  25. package/dist/infra/lib/ReviewRace.d.ts +4 -0
  26. package/dist/infra/lib/ReviewRace.d.ts.map +1 -1
  27. package/dist/infra/lib/ReviewRace.js +4 -0
  28. package/dist/infra/lib/ReviewRace.js.map +1 -1
  29. package/dist/infra/lib/SkillQualityGate.d.ts +4 -0
  30. package/dist/infra/lib/SkillQualityGate.d.ts.map +1 -1
  31. package/dist/infra/lib/SkillQualityGate.js +4 -0
  32. package/dist/infra/lib/SkillQualityGate.js.map +1 -1
  33. package/dist/infra/lib/UltraQA.d.ts +4 -0
  34. package/dist/infra/lib/UltraQA.d.ts.map +1 -1
  35. package/dist/infra/lib/UltraQA.js +4 -0
  36. package/dist/infra/lib/UltraQA.js.map +1 -1
  37. package/dist/infra/lib/VerificationLoop.d.ts +4 -0
  38. package/dist/infra/lib/VerificationLoop.d.ts.map +1 -1
  39. package/dist/infra/lib/VerificationLoop.js +4 -0
  40. package/dist/infra/lib/VerificationLoop.js.map +1 -1
  41. package/dist/infra/orchestrator/index.d.ts.map +1 -1
  42. package/dist/infra/orchestrator/index.js +1 -3
  43. package/dist/infra/orchestrator/index.js.map +1 -1
  44. package/dist/infra/orchestrator/parallelResearch.d.ts.map +1 -1
  45. package/dist/infra/orchestrator/parallelResearch.js +1 -4
  46. package/dist/infra/orchestrator/parallelResearch.js.map +1 -1
  47. package/dist/tools/convention/validateCodeQuality.d.ts.map +1 -1
  48. package/dist/tools/convention/validateCodeQuality.js +5 -4
  49. package/dist/tools/convention/validateCodeQuality.js.map +1 -1
  50. package/dist/tools/spec/traceabilityMatrix.d.ts +2 -0
  51. package/dist/tools/spec/traceabilityMatrix.d.ts.map +1 -1
  52. package/dist/tools/spec/traceabilityMatrix.js +50 -1
  53. package/dist/tools/spec/traceabilityMatrix.js.map +1 -1
  54. package/dist/tools/spec/traceabilityMatrix.path-resolution.test.d.ts +10 -0
  55. package/dist/tools/spec/traceabilityMatrix.path-resolution.test.d.ts.map +1 -0
  56. package/dist/tools/spec/traceabilityMatrix.path-resolution.test.js +89 -0
  57. package/dist/tools/spec/traceabilityMatrix.path-resolution.test.js.map +1 -0
  58. package/dist/tools/spec/traceabilityMatrix.test.js +19 -0
  59. package/dist/tools/spec/traceabilityMatrix.test.js.map +1 -1
  60. package/hooks/hooks.json +1 -0
  61. package/hooks/scripts/__tests__/.vibe/command-log.txt +39 -0
  62. package/hooks/scripts/__tests__/.vibe/memories/memories.db +0 -0
  63. package/hooks/scripts/__tests__/.vibe/memories/memories.db-shm +0 -0
  64. package/hooks/scripts/__tests__/.vibe/memories/memories.db-wal +0 -0
  65. package/hooks/scripts/__tests__/auto-test-debounce.test.js +145 -0
  66. package/hooks/scripts/__tests__/code-check-detectors.test.js +155 -0
  67. package/hooks/scripts/__tests__/dispatcher-inprocess.test.js +99 -0
  68. package/hooks/scripts/__tests__/post-edit-dispatcher.test.js +139 -0
  69. package/hooks/scripts/__tests__/pre-tool-guard.test.js +115 -1
  70. package/hooks/scripts/__tests__/run-ledger-verify-required.test.js +146 -0
  71. package/hooks/scripts/__tests__/run-ledger.test.js +330 -0
  72. package/hooks/scripts/__tests__/scope-from-spec.test.js +215 -0
  73. package/hooks/scripts/__tests__/sentinel-guard.test.js +79 -24
  74. package/hooks/scripts/__tests__/step-counter.test.js +95 -15
  75. package/hooks/scripts/__tests__/utils-npm-root.test.js +98 -0
  76. package/hooks/scripts/auto-commit.js +27 -1
  77. package/hooks/scripts/auto-format.js +85 -20
  78. package/hooks/scripts/auto-test.js +187 -37
  79. package/hooks/scripts/code-check.js +286 -90
  80. package/hooks/scripts/codex-hook-adapter.js +12 -1
  81. package/hooks/scripts/command-log.js +26 -16
  82. package/hooks/scripts/lib/dispatcher.js +38 -0
  83. package/hooks/scripts/lib/hook-context.js +101 -0
  84. package/hooks/scripts/lib/pr-gate-runner.js +62 -0
  85. package/hooks/scripts/lib/run-ledger.js +169 -0
  86. package/hooks/scripts/lib/scope-from-spec.js +40 -7
  87. package/hooks/scripts/post-edit-dispatcher.js +93 -20
  88. package/hooks/scripts/post-edit.js +40 -19
  89. package/hooks/scripts/pr-test-gate.js +8 -37
  90. package/hooks/scripts/pre-tool-dispatcher.js +18 -16
  91. package/hooks/scripts/pre-tool-guard.js +55 -52
  92. package/hooks/scripts/prompt-dispatcher.js +10 -0
  93. package/hooks/scripts/scope-guard.js +40 -39
  94. package/hooks/scripts/sentinel-guard.js +41 -41
  95. package/hooks/scripts/session-start.js +13 -1
  96. package/hooks/scripts/step-counter.js +100 -7
  97. package/hooks/scripts/stop-dispatcher.js +26 -0
  98. package/hooks/scripts/utils.js +63 -21
  99. package/hooks/scripts/verify-ledger.js +22 -0
  100. package/package.json +2 -2
  101. package/skills/spec/references/templates.md +11 -6
  102. package/skills/vibe.run/SKILL.md +144 -1681
  103. package/skills/vibe.run/references/brand-assets.md +59 -0
  104. package/skills/vibe.run/references/parallel-agents.md +326 -0
  105. package/skills/vibe.run/references/race-review.md +272 -0
  106. package/skills/vibe.run/references/ralph-loop.md +172 -0
  107. package/skills/vibe.run/references/ultrawork-mode.md +148 -0
  108. package/skills/vibe.trace/SKILL.md +25 -38
  109. package/skills/vibe.verify/SKILL.md +15 -0
  110. package/hooks/scripts/figma-guard.js +0 -219
@@ -4,49 +4,20 @@
4
4
  * mcp__github__create_pull_request 호출 시 테스트가 통과해야만 PR 생성 허용.
5
5
  * exit 2 = 차단, exit 0 = 통과
6
6
  */
7
- import { execSync } from 'child_process';
8
7
  import { PROJECT_DIR } from './utils.js';
9
- import { existsSync, readFileSync } from 'fs';
10
- import path from 'path';
8
+ import { runPrTestGate } from './lib/pr-gate-runner.js';
11
9
 
12
- function detectTestCommand() {
13
- const pkgPath = path.join(PROJECT_DIR, 'package.json');
14
- if (existsSync(pkgPath)) {
15
- try {
16
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
17
- if (pkg.scripts?.test && pkg.scripts.test !== 'echo "Error: no test specified" && exit 1') {
18
- return 'npm test';
19
- }
20
- } catch { /* ignore */ }
21
- }
22
- // Python
23
- if (existsSync(path.join(PROJECT_DIR, 'pytest.ini')) || existsSync(path.join(PROJECT_DIR, 'pyproject.toml'))) {
24
- return 'python -m pytest --tb=short -q';
25
- }
26
- // Go
27
- if (existsSync(path.join(PROJECT_DIR, 'go.mod'))) {
28
- return 'go test ./...';
29
- }
30
- return null;
31
- }
10
+ const { passed, testCmd, output } = runPrTestGate(PROJECT_DIR);
32
11
 
33
- try {
34
- const testCmd = detectTestCommand();
35
- if (!testCmd) {
36
- // No test command detected — allow PR
37
- process.exit(0);
38
- }
12
+ if (!testCmd) {
13
+ // 테스트 커맨드 없음 → PR 허용
14
+ process.exit(0);
15
+ }
39
16
 
40
- console.log(`[PR-GATE] Running tests before PR creation: ${testCmd}`);
41
- execSync(testCmd, {
42
- cwd: PROJECT_DIR,
43
- stdio: ['ignore', 'pipe', 'pipe'],
44
- timeout: 120000,
45
- });
17
+ if (passed) {
46
18
  console.log('[PR-GATE] Tests passed — PR creation allowed');
47
19
  process.exit(0);
48
- } catch (err) {
49
- const output = err.stdout ? err.stdout.toString().split('\n').slice(-5).join('\n') : '';
20
+ } else {
50
21
  console.log(`[PR-GATE] Tests failed — PR creation blocked\n${output}`);
51
22
  process.exit(2);
52
23
  }
@@ -2,43 +2,45 @@
2
2
  /**
3
3
  * PreToolUse dispatcher — Bash/Edit/Write 공용.
4
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을 인자로 받아 순차 실행.
5
+ * in-process 평탄화 (2026-06): 가드를 자식 node로 spawn하지 않고 import해서
6
+ * 같은 프로세스에서 실행한다. 자식 VM 기동(~20ms × N)과 stdin 재읽기 제거.
7
+ * daemon/IPC는 금지 (CLAUDE.md Gotchas) — 디스패처 프로세스 자체는 유지.
10
8
  *
11
9
  * Deny 시맨틱 보존:
12
- * sentinel-guard / pre-tool-guard exit 2(deny)를 반환하면 dispatcher도
13
- * 즉시 exit 2로 상위에 전파 → Claude Code가 도구 실행을 차단.
10
+ * sentinel-guard / pre-tool-guard / scope-guard의 run(ctx)이 2를 반환하면
11
+ * dispatchInProcess가 process.exit(2)로 상위에 전파 → Claude Code가 도구 실행 차단.
14
12
  *
15
13
  * 사용법: node pre-tool-dispatcher.js <Bash|Edit|Write>
16
14
  */
17
- import { dispatch } from './lib/dispatcher.js';
15
+ import { dispatchInProcess } from './lib/dispatcher.js';
16
+ import { run as sentinelGuard } from './sentinel-guard.js';
17
+ import { run as preToolGuard } from './pre-tool-guard.js';
18
+ import { run as scopeGuard } from './scope-guard.js';
19
+ import { run as commandLog } from './command-log.js';
18
20
 
19
21
  const toolName = process.argv[2] || '';
20
22
 
21
23
  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
+ { name: 'sentinel-guard', run: sentinelGuard, denyOnExit2: true },
25
+ { name: 'pre-tool-guard', run: preToolGuard, denyOnExit2: true },
24
26
  ];
25
27
 
26
- // scope-guard는 Edit/Write에만 의미 있음 — 불필요한 spawn 회피
28
+ // scope-guard는 Edit/Write에만 의미 있음
27
29
  if (toolName === 'Edit' || toolName === 'Write') {
28
- steps.push({ name: 'scope-guard', script: 'scope-guard.js', args: [toolName], denyOnExit2: true });
30
+ steps.push({ name: 'scope-guard', run: scopeGuard, denyOnExit2: true });
29
31
  }
30
32
 
31
33
  // command-log은 Bash 전용
32
34
  if (toolName === 'Bash') {
33
- steps.push({ name: 'command-log', script: 'command-log.js' });
35
+ steps.push({ name: 'command-log', run: commandLog });
34
36
  }
35
37
 
36
38
  // 하네스에 노이즈를 주지 않도록 디스패처 자체의 예외는 모두 흡수.
37
- // exit 2 (deny 전파)는 dispatch() 내부에서 process.exit(2)로 처리되므로
39
+ // exit 2 (deny 전파)는 dispatchInProcess 내부에서 process.exit(2)로 처리되므로
38
40
  // 여기까지 오면 "deny 아님" → 항상 exit 0.
39
41
  try {
40
- await dispatch(steps);
42
+ await dispatchInProcess(steps, { argvToolName: toolName });
41
43
  } catch {
42
- // ignore — 하위 스크립트 크래시가 상위 훅 실패로 표시되지 않도록
44
+ // ignore — step 크래시가 상위 훅 실패로 표시되지 않도록
43
45
  }
44
46
  process.exit(0);
@@ -4,8 +4,6 @@
4
4
  * 위험한 도구 사용 전 검증 및 경고
5
5
  */
6
6
 
7
- import { VIBE_PATH, PROJECT_DIR } from './utils.js';
8
-
9
7
  // 위험한 명령어 패턴
10
8
  //
11
9
  // 각 엔트리의 `target`은 매칭 대상 필드:
@@ -197,63 +195,68 @@ function formatOutput(toolName, validation) {
197
195
  return lines.join('\n');
198
196
  }
199
197
 
198
+ import { logHookDecision, PROJECT_DIR } from './utils.js';
199
+ import { buildCliCtx, isDirectRun } from './lib/hook-context.js';
200
+ import { runPrTestGate } from './lib/pr-gate-runner.js';
201
+
202
+ /** gh pr create 감지 정규식 (단어 경계 기준, 플래그 허용). */
203
+ const GH_PR_CREATE_RE = /\bgh\s+pr\s+create\b/;
204
+
200
205
  /**
201
- * stdin에서 JSON 페이로드 읽기 (Claude Code 하네스 호환)
202
- * stdin이 없거나 파싱 실패 argv/env 폴백
206
+ * in-process 진입점 디스패처가 ctx를 전달해 직접 호출.
207
+ * @param {{ toolName: string, toolInput: string, payload: object|null }} ctx
208
+ * @returns {Promise<number>} exit code (0 = allowed, 2 = denied)
203
209
  */
204
- function readStdinSync() {
205
- try {
206
- if (process.stdin.isTTY) return null;
207
- // fd 0을 직접 사용 (Windows는 '/dev/stdin'이 없음)
208
- const buf = Buffer.alloc(65536);
209
- const bytesRead = fs.readSync(0, buf, 0, buf.length, null);
210
- if (bytesRead > 0) {
211
- return JSON.parse(buf.toString('utf-8', 0, bytesRead));
210
+ export async function run(ctx) {
211
+ const toolName = ctx.toolName || 'Bash';
212
+ const toolInput = ctx.toolInput;
213
+
214
+ // 1단계: 입력 스키마 검증 (구조적 오류 탐지)
215
+ const schemaResult = validateInputSchema(toolName, ctx.payload?.tool_input || toolInput);
216
+ if (!schemaResult.valid) {
217
+ // stderr: Claude Code surfaces stderr in hook-error notifications; stdout is injected
218
+ // into the assistant's context and never shown to the user. Guard messages target the user.
219
+ console.error(`⚠️ INPUT VALIDATION: ${toolName}`);
220
+ for (const err of schemaResult.errors) {
221
+ console.error(` [SCHEMA] ${err}`);
212
222
  }
213
- } catch { /* 파싱 실패 시 폴백 */ }
214
- return null;
215
- }
223
+ logHookDecision('pre-tool-guard', toolName, 'warn', `schema: ${schemaResult.errors.join('; ')}`);
224
+ // 스키마 오류는 경고만 (차단하지 않음 — 레거시 호환)
225
+ }
216
226
 
217
- import fs from 'fs';
218
-
219
- // 메인 실행: stdin JSON 우선, argv 폴백
220
- const stdinPayload = readStdinSync();
221
- const toolName = stdinPayload?.tool_name || process.argv[2] || 'Bash';
222
- const toolInput = stdinPayload?.tool_input
223
- ? (typeof stdinPayload.tool_input === 'string'
224
- ? stdinPayload.tool_input
225
- : JSON.stringify(stdinPayload.tool_input))
226
- : (process.argv[3] || process.env.TOOL_INPUT || '');
227
-
228
- import { logHookDecision } from './utils.js';
229
-
230
- // 1단계: 입력 스키마 검증 (구조적 오류 탐지)
231
- const schemaResult = validateInputSchema(toolName, stdinPayload?.tool_input || toolInput);
232
- if (!schemaResult.valid) {
233
- // stderr: Claude Code surfaces stderr in hook-error notifications; stdout is injected
234
- // into the assistant's context and never shown to the user. Guard messages target the user.
235
- console.error(`⚠️ INPUT VALIDATION: ${toolName}`);
236
- for (const err of schemaResult.errors) {
237
- console.error(` [SCHEMA] ${err}`);
227
+ // 2단계: gh pr create Bash 명령 → PR 테스트 게이트
228
+ if (toolName === 'Bash' || toolName === 'bash') {
229
+ const command = extractTarget(toolInput, 'command') || toolInput;
230
+ if (GH_PR_CREATE_RE.test(command)) {
231
+ const gateResult = runPrTestGate(PROJECT_DIR);
232
+ if (!gateResult.passed) {
233
+ console.error(`[PR-GATE] Tests failed — gh pr create blocked\n${gateResult.output}`);
234
+ logHookDecision('pre-tool-guard', 'Bash', 'block', 'gh pr create: test gate failed');
235
+ return 2;
236
+ }
237
+ }
238
238
  }
239
- logHookDecision('pre-tool-guard', toolName, 'warn', `schema: ${schemaResult.errors.join('; ')}`);
240
- // 스키마 오류는 경고만 (차단하지 않음 — 레거시 호환)
241
- }
242
239
 
243
- // 2단계: 위험 패턴 검증 (보안 탐지)
244
- const validation = validateCommand(toolName, toolInput);
245
- const output = formatOutput(toolName, validation);
240
+ // 3단계: 위험 패턴 검증 (보안 탐지)
241
+ const validation = validateCommand(toolName, toolInput);
242
+ const output = formatOutput(toolName, validation);
246
243
 
247
- if (output) {
248
- console.error(output);
249
- }
244
+ if (output) {
245
+ console.error(output);
246
+ }
250
247
 
251
- // Hook trace logging
252
- if (!validation.allowed) {
253
- logHookDecision('pre-tool-guard', toolName, 'block', validation.warnings.join('; '));
254
- } else if (validation.warnings.length > 0) {
255
- logHookDecision('pre-tool-guard', toolName, 'warn', validation.warnings.join('; '));
248
+ // Hook trace logging
249
+ if (!validation.allowed) {
250
+ logHookDecision('pre-tool-guard', toolName, 'block', validation.warnings.join('; '));
251
+ } else if (validation.warnings.length > 0) {
252
+ logHookDecision('pre-tool-guard', toolName, 'warn', validation.warnings.join('; '));
253
+ }
254
+
255
+ // Exit code: 0 = allowed, 2 = denied (claw-code 규약)
256
+ return validation.allowed ? 0 : 2;
256
257
  }
257
258
 
258
- // Exit code: 0 = allowed, 2 = denied (claw-code 규약), 1 = 레거시 호환
259
- process.exit(validation.allowed ? 0 : 2);
259
+ // standalone CLI 모드: stdin JSON 우선, argv 폴백
260
+ if (isDirectRun(import.meta.url)) {
261
+ process.exit(await run(buildCliCtx()));
262
+ }
@@ -42,6 +42,16 @@ try {
42
42
 
43
43
  if (!prompt) process.exit(0);
44
44
 
45
+ // vibe.run 감지 — runStarted 기록 및 verifyPassed 리셋 (in-process, stdout 없음).
46
+ {
47
+ const { isVibeRunPrompt, extractRunFeature, recordRunStart } = await import('./lib/run-ledger.js');
48
+ if (isVibeRunPrompt(prompt)) {
49
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
50
+ const feature = extractRunFeature(prompt);
51
+ recordRunStart(projectDir, feature);
52
+ }
53
+ }
54
+
45
55
  // 레거시 SSOT 통합 — `/vibe.*` 진입 시 `.claude/vibe/` → `.vibe/` 자동 이동.
46
56
  // `vibe init`/`update` 와 동일한 `consolidateLegacyVibe` (dist/cli/setup/LegacyMigration.js) 를 직접 재사용. Idempotent.
47
57
  if (/^\s*\/vibe\b/i.test(prompt)) {
@@ -22,6 +22,7 @@
22
22
  import fs from 'fs';
23
23
  import path from 'path';
24
24
  import { PROJECT_DIR, logHookDecision, projectVibePath, projectVibeRoot } from './utils.js';
25
+ import { buildCliCtx, isDirectRun } from './lib/hook-context.js';
25
26
 
26
27
  const SCOPE_PATH = projectVibePath(PROJECT_DIR, 'scope.json');
27
28
 
@@ -86,16 +87,6 @@ function toRelative(filePath) {
86
87
  return rel || path.basename(filePath).replace(/\\/g, '/');
87
88
  }
88
89
 
89
- function readStdinSync() {
90
- try {
91
- if (process.stdin.isTTY) return null;
92
- const buf = Buffer.alloc(65536);
93
- const bytesRead = fs.readSync(0, buf, 0, buf.length, null);
94
- if (bytesRead > 0) return JSON.parse(buf.toString('utf-8', 0, bytesRead));
95
- } catch { /* ignore */ }
96
- return null;
97
- }
98
-
99
90
  function extractFilePath(toolInput) {
100
91
  if (!toolInput) return '';
101
92
  if (typeof toolInput === 'string') {
@@ -105,41 +96,51 @@ function extractFilePath(toolInput) {
105
96
  return typeof toolInput.file_path === 'string' ? toolInput.file_path : '';
106
97
  }
107
98
 
108
- const scope = readScope();
109
- if (!scope) process.exit(0); // no scope declared no-op
99
+ /**
100
+ * in-process 진입점 디스패처가 ctx를 전달해 직접 호출.
101
+ * @param {{ toolName: string, toolInput: string }} ctx
102
+ * @returns {Promise<number>} exit code (0 = allow/no-op, 2 = block)
103
+ */
104
+ export async function run(ctx) {
105
+ const scope = readScope();
106
+ if (!scope) return 0; // no scope declared → no-op
107
+
108
+ const toolName = ctx.toolName;
109
+ if (toolName !== 'Edit' && toolName !== 'Write') return 0;
110
110
 
111
- const stdinPayload = readStdinSync();
112
- const toolName = stdinPayload?.tool_name || process.argv[2] || '';
113
- if (toolName !== 'Edit' && toolName !== 'Write') process.exit(0);
111
+ const filePath = extractFilePath(ctx.toolInput);
112
+ if (!filePath) return 0;
114
113
 
115
- const rawInput = stdinPayload?.tool_input ?? process.argv[3] ?? process.env.TOOL_INPUT ?? '';
116
- const filePath = extractFilePath(rawInput);
117
- if (!filePath) process.exit(0);
114
+ const rel = toRelative(filePath);
118
115
 
119
- const rel = toRelative(filePath);
116
+ // 평가 순서: deny 우선 → allow 검증
117
+ const denied = scope.deny.length > 0 && matchesAny(rel, scope.deny);
118
+ const allowed = scope.allow.length === 0 || matchesAny(rel, scope.allow);
120
119
 
121
- // 평가 순서: deny 우선 → allow 검증
122
- const denied = scope.deny.length > 0 && matchesAny(rel, scope.deny);
123
- const allowed = scope.allow.length === 0 || matchesAny(rel, scope.allow);
120
+ const violated = denied || !allowed;
121
+ if (!violated) return 0;
124
122
 
125
- const violated = denied || !allowed;
126
- if (!violated) process.exit(0);
123
+ const lines = [];
124
+ lines.push(`🚧 SCOPE GUARD: ${toolName} — out of declared scope`);
125
+ lines.push(` file: ${rel}`);
126
+ if (denied) lines.push(` reason: matches deny pattern`);
127
+ else if (!allowed) lines.push(` reason: not in allow list`);
128
+ if (scope.reason) lines.push(` declared scope: ${scope.reason}`);
129
+ lines.push(` declared in: ${path.relative(PROJECT_DIR, SCOPE_PATH)} (mode=${scope.mode})`);
127
130
 
128
- const lines = [];
129
- lines.push(`🚧 SCOPE GUARD: ${toolName} — out of declared scope`);
130
- lines.push(` file: ${rel}`);
131
- if (denied) lines.push(` reason: matches deny pattern`);
132
- else if (!allowed) lines.push(` reason: not in allow list`);
133
- if (scope.reason) lines.push(` declared scope: ${scope.reason}`);
134
- lines.push(` declared in: ${path.relative(PROJECT_DIR, SCOPE_PATH)} (mode=${scope.mode})`);
131
+ const blocking = scope.mode === 'block';
132
+ if (blocking) {
133
+ lines.push('');
134
+ lines.push('🚫 BLOCKED. Edit scope.json or justify to the user before proceeding.');
135
+ }
135
136
 
136
- const blocking = scope.mode === 'block';
137
- if (blocking) {
138
- lines.push('');
139
- lines.push('🚫 BLOCKED. Edit scope.json or justify to the user before proceeding.');
140
- }
137
+ process.stderr.write(lines.join('\n') + '\n');
138
+ logHookDecision('scope-guard', toolName, blocking ? 'block' : 'warn', `${rel} ${denied ? '(deny)' : '(out-of-allow)'}`);
141
139
 
142
- console.log(lines.join('\n'));
143
- logHookDecision('scope-guard', toolName, blocking ? 'block' : 'warn', `${rel} ${denied ? '(deny)' : '(out-of-allow)'}`);
140
+ return blocking ? 2 : 0;
141
+ }
144
142
 
145
- process.exit(blocking ? 2 : 0);
143
+ // standalone CLI 모드: stdin JSON 우선, argv 폴백
144
+ if (isDirectRun(import.meta.url)) {
145
+ process.exit(await run(buildCliCtx()));
146
+ }
@@ -1,13 +1,31 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Sentinel Guard — PreToolUse hook
4
- * Protects sentinel files and blocks dangerous operations.
5
- * Runs before pre-tool-guard.js.
4
+ * 핵심 자율 기계 경로를 Write/Edit/Bash 로부터 보호한다.
5
+ * pre-tool-guard.js 이전에 실행된다.
6
+ *
7
+ * 보호 경로 결정 근거:
8
+ * src/infra/lib/autonomy/ 는 레포에 존재하지 않는 팬텀 경로였다.
9
+ * git log 와 src/ 전수 조사 결과, 자율 기계(evolution machinery)의 실제
10
+ * 위치는 src/infra/lib/evolution/ 이다. GuardAnalyzer·HookTraceReader 등이
11
+ * hook-traces.jsonl을 분석해 하네스 자기 개선 인사이트를 생성한다.
12
+ * 따라서 sentinel 대상을 이 실존 경로로 교체한다.
13
+ *
14
+ * 추가로 hooks/scripts/lib/ (디스패처 인프라)도 보호 — 이 디렉토리의
15
+ * hook-context.js / dispatcher.js 가 손상되면 모든 pre-tool 게이트가
16
+ * 무력화된다.
17
+ *
18
+ * exit 2 = deny 규약 (PreToolUse 차단).
6
19
  */
7
20
 
8
- const SENTINEL_PATH_PREFIX = 'src/infra/lib/autonomy/';
21
+ const SENTINEL_PREFIXES = [
22
+ 'src/infra/lib/evolution/',
23
+ 'hooks/scripts/lib/',
24
+ ];
9
25
 
10
- const SENTINEL_PATH_RE = /^(\.\/|\.\\)?src[\\/]infra[\\/]lib[\\/]autonomy[\\/]/;
26
+ // 빠른 prefix 검사용 정규식 (backslash 포함)
27
+ const SENTINEL_PATH_RE =
28
+ /^(?:\.[\\/])?(?:src[\\/]infra[\\/]lib[\\/]evolution|hooks[\\/]scripts[\\/]lib)[\\/]/;
11
29
 
12
30
  const DANGEROUS_BASH_RE =
13
31
  /\b(rm\s+-rf|kill\s+-9|drop\s+table|truncate|shutdown|reboot|mkfs|dd\s+if=)\b/i;
@@ -23,7 +41,6 @@ function extractFilePath(toolName, input) {
23
41
  return parsed.file_path || parsed.filePath || null;
24
42
  }
25
43
  } catch {
26
- // Not JSON, try as plain string
27
44
  return typeof input === 'string' ? input : null;
28
45
  }
29
46
  return null;
@@ -48,7 +65,8 @@ function extractBashCommand(input) {
48
65
  function isSentinelPath(filePath) {
49
66
  if (!filePath) return false;
50
67
  const normalized = filePath.replace(/\\/g, '/').replace(/^\.\//, '');
51
- return normalized.startsWith(SENTINEL_PATH_PREFIX) || SENTINEL_PATH_RE.test(filePath);
68
+ return SENTINEL_PREFIXES.some(p => normalized.startsWith(p)) ||
69
+ SENTINEL_PATH_RE.test(filePath);
52
70
  }
53
71
 
54
72
  /**
@@ -76,8 +94,7 @@ function guard(toolName, toolInput) {
76
94
  };
77
95
  }
78
96
  if (command && DANGEROUS_BASH_RE.test(command)) {
79
- // Check if command targets sentinel files
80
- if (command.includes(SENTINEL_PATH_PREFIX) || command.includes('src/infra/lib/autonomy')) {
97
+ if (SENTINEL_PREFIXES.some(p => command.includes(p))) {
81
98
  return {
82
99
  decision: 'block',
83
100
  reason: `Dangerous command targeting sentinel path: ${command}`,
@@ -86,45 +103,28 @@ function guard(toolName, toolInput) {
86
103
  }
87
104
  }
88
105
 
89
- // Allow — return undefined for normal flow
90
106
  return undefined;
91
107
  }
92
108
 
93
- import fs from 'fs';
109
+ import { logHookDecision } from './utils.js';
110
+ import { buildCliCtx, isDirectRun } from './lib/hook-context.js';
94
111
 
95
112
  /**
96
- * stdin에서 JSON 페이로드 읽기 (Claude Code 하네스 호환)
113
+ * in-process 진입점 디스패처가 ctx를 전달해 직접 호출.
114
+ * @param {{ toolName: string, toolInput: string }} ctx
115
+ * @returns {Promise<number>} exit code (2 = deny 규약, 0 = allow)
97
116
  */
98
- function readStdinSync() {
99
- try {
100
- if (process.stdin.isTTY) return null;
101
- // fd 0을 직접 사용 (Windows는 '/dev/stdin' 없음)
102
- const buf = Buffer.alloc(65536);
103
- const bytesRead = fs.readSync(0, buf, 0, buf.length, null);
104
- if (bytesRead > 0) {
105
- return JSON.parse(buf.toString('utf-8', 0, bytesRead));
106
- }
107
- } catch { /* 폴백 */ }
108
- return null;
117
+ export async function run(ctx) {
118
+ const result = guard(ctx.toolName, ctx.toolInput);
119
+ if (result) {
120
+ logHookDecision('sentinel-guard', ctx.toolName, 'block', result.reason);
121
+ console.log(JSON.stringify(result));
122
+ return 2;
123
+ }
124
+ return 0;
109
125
  }
110
126
 
111
- // Main execution: stdin JSON 우선, argv 폴백
112
- const stdinPayload = readStdinSync();
113
- const toolName = stdinPayload?.tool_name || process.argv[2] || '';
114
- const toolInput = stdinPayload?.tool_input
115
- ? (typeof stdinPayload.tool_input === 'string'
116
- ? stdinPayload.tool_input
117
- : JSON.stringify(stdinPayload.tool_input))
118
- : (process.argv[3] || process.env.TOOL_INPUT || '');
119
-
120
- import { logHookDecision } from './utils.js';
121
-
122
- const result = guard(toolName, toolInput);
123
-
124
- if (result) {
125
- logHookDecision('sentinel-guard', toolName, 'block', result.reason);
126
- console.log(JSON.stringify(result));
127
- process.exit(2); // deny 규약
127
+ // standalone CLI 모드 (antigravity-hooks.json / 기존 테스트): stdin JSON 우선, argv 폴백
128
+ if (isDirectRun(import.meta.url)) {
129
+ process.exit(await run(buildCliCtx()));
128
130
  }
129
-
130
- process.exit(0);
@@ -126,7 +126,7 @@ async function main() {
126
126
  }
127
127
  }
128
128
 
129
- // Scope sync — 사용자가 .vibe/config.json scopeGuard.enabled=true켰을 때만 동작.
129
+ // Scope sync — 기본 ON. scopeGuard.enabled=false명시적 opt-out 가능.
130
130
  try {
131
131
  const { syncScopeFile, isScopeGuardEnabled } = await import('./lib/scope-from-spec.js');
132
132
  if (isScopeGuardEnabled(PROJECT_DIR)) {
@@ -135,6 +135,18 @@ async function main() {
135
135
  console.log(`\n🚧 Scope ${result.action} from active SPECs (${path.relative(PROJECT_DIR, path.join(projectVibeRoot(PROJECT_DIR), 'scope.json'))})`);
136
136
  }
137
137
  }
138
+ // scope 상태 1줄 알림 — 컨텍스트에 주입되어 모델이 현재 scope 범위를 인지한다
139
+ const scopeJsonPath = path.join(projectVibeRoot(PROJECT_DIR), 'scope.json');
140
+ if (fs.existsSync(scopeJsonPath)) {
141
+ try {
142
+ const scopeData = JSON.parse(fs.readFileSync(scopeJsonPath, 'utf-8'));
143
+ const featureLabel = scopeData.reason || path.basename(scopeJsonPath);
144
+ const modeLabel = scopeData.mode || 'warn';
145
+ console.log(`\nscope-guard: ${featureLabel} (${modeLabel})`);
146
+ } catch { /* ignore */ }
147
+ } else {
148
+ console.log('\nscope-guard: no active scope (no active SPEC or no derivable paths) — out-of-scope edits are not monitored');
149
+ }
138
150
  } catch { /* scope sync is best-effort */ }
139
151
 
140
152
  // Autonomy status summary