@su-record/vibe 2.12.4 → 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.
- package/CLAUDE.md +10 -3
- package/README.en.md +11 -11
- package/README.md +7 -7
- package/dist/cli/commands/init.d.ts +8 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +24 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/postinstall/claude-agents.d.ts +11 -1
- package/dist/cli/postinstall/claude-agents.d.ts.map +1 -1
- package/dist/cli/postinstall/claude-agents.js +22 -2
- package/dist/cli/postinstall/claude-agents.js.map +1 -1
- package/dist/cli/postinstall/constants.d.ts +18 -0
- package/dist/cli/postinstall/constants.d.ts.map +1 -1
- package/dist/cli/postinstall/constants.js +50 -0
- package/dist/cli/postinstall/constants.js.map +1 -1
- package/dist/cli/postinstall/fs-utils.d.ts +23 -0
- package/dist/cli/postinstall/fs-utils.d.ts.map +1 -1
- package/dist/cli/postinstall/fs-utils.js +71 -0
- package/dist/cli/postinstall/fs-utils.js.map +1 -1
- package/dist/cli/postinstall/fs-utils.test.js +69 -1
- package/dist/cli/postinstall/fs-utils.test.js.map +1 -1
- package/dist/cli/postinstall/index.d.ts +1 -1
- package/dist/cli/postinstall/index.d.ts.map +1 -1
- package/dist/cli/postinstall/index.js +1 -1
- package/dist/cli/postinstall/index.js.map +1 -1
- package/dist/cli/postinstall/main.d.ts.map +1 -1
- package/dist/cli/postinstall/main.js +15 -4
- package/dist/cli/postinstall/main.js.map +1 -1
- package/dist/cli/postinstall.d.ts +1 -1
- package/dist/cli/postinstall.d.ts.map +1 -1
- package/dist/cli/postinstall.js +1 -1
- package/dist/cli/postinstall.js.map +1 -1
- package/dist/cli/setup/CodexHooks.test.js +27 -0
- package/dist/cli/setup/CodexHooks.test.js.map +1 -1
- package/dist/cli/setup/ProjectSetup.js +2 -2
- package/dist/cli/setup/ProjectSetup.js.map +1 -1
- package/dist/infra/lib/ContextCompressor.d.ts +11 -2
- package/dist/infra/lib/ContextCompressor.d.ts.map +1 -1
- package/dist/infra/lib/ContextCompressor.js +26 -41
- package/dist/infra/lib/ContextCompressor.js.map +1 -1
- package/dist/infra/lib/ContextCompressor.test.d.ts +2 -0
- package/dist/infra/lib/ContextCompressor.test.d.ts.map +1 -0
- package/dist/infra/lib/ContextCompressor.test.js +25 -0
- package/dist/infra/lib/ContextCompressor.test.js.map +1 -0
- package/dist/infra/lib/DecisionTracer.d.ts +4 -0
- package/dist/infra/lib/DecisionTracer.d.ts.map +1 -1
- package/dist/infra/lib/DecisionTracer.js +4 -0
- package/dist/infra/lib/DecisionTracer.js.map +1 -1
- package/dist/infra/lib/LoopBreaker.d.ts +4 -0
- package/dist/infra/lib/LoopBreaker.d.ts.map +1 -1
- package/dist/infra/lib/LoopBreaker.js +4 -0
- package/dist/infra/lib/LoopBreaker.js.map +1 -1
- package/dist/infra/lib/ReviewRace.d.ts +4 -0
- package/dist/infra/lib/ReviewRace.d.ts.map +1 -1
- package/dist/infra/lib/ReviewRace.js +4 -0
- package/dist/infra/lib/ReviewRace.js.map +1 -1
- package/dist/infra/lib/SkillQualityGate.d.ts +4 -0
- package/dist/infra/lib/SkillQualityGate.d.ts.map +1 -1
- package/dist/infra/lib/SkillQualityGate.js +4 -0
- package/dist/infra/lib/SkillQualityGate.js.map +1 -1
- package/dist/infra/lib/UltraQA.d.ts +4 -0
- package/dist/infra/lib/UltraQA.d.ts.map +1 -1
- package/dist/infra/lib/UltraQA.js +4 -0
- package/dist/infra/lib/UltraQA.js.map +1 -1
- package/dist/infra/lib/VerificationLoop.d.ts +4 -0
- package/dist/infra/lib/VerificationLoop.d.ts.map +1 -1
- package/dist/infra/lib/VerificationLoop.js +4 -0
- package/dist/infra/lib/VerificationLoop.js.map +1 -1
- package/dist/infra/lib/embedding/VectorStore.d.ts +9 -2
- package/dist/infra/lib/embedding/VectorStore.d.ts.map +1 -1
- package/dist/infra/lib/embedding/VectorStore.js +42 -19
- package/dist/infra/lib/embedding/VectorStore.js.map +1 -1
- package/dist/infra/lib/memory/MemoryStorage.d.ts +12 -0
- package/dist/infra/lib/memory/MemoryStorage.d.ts.map +1 -1
- package/dist/infra/lib/memory/MemoryStorage.js +57 -0
- package/dist/infra/lib/memory/MemoryStorage.js.map +1 -1
- package/dist/infra/lib/memory/ReflectionStore.d.ts.map +1 -1
- package/dist/infra/lib/memory/ReflectionStore.js +8 -27
- package/dist/infra/lib/memory/ReflectionStore.js.map +1 -1
- package/dist/infra/orchestrator/LLMCluster.d.ts +4 -0
- package/dist/infra/orchestrator/LLMCluster.d.ts.map +1 -1
- package/dist/infra/orchestrator/LLMCluster.js +35 -8
- package/dist/infra/orchestrator/LLMCluster.js.map +1 -1
- package/dist/infra/orchestrator/index.d.ts.map +1 -1
- package/dist/infra/orchestrator/index.js +1 -3
- package/dist/infra/orchestrator/index.js.map +1 -1
- package/dist/infra/orchestrator/parallelResearch.d.ts.map +1 -1
- package/dist/infra/orchestrator/parallelResearch.js +1 -4
- package/dist/infra/orchestrator/parallelResearch.js.map +1 -1
- package/dist/tools/convention/validateCodeQuality.d.ts.map +1 -1
- package/dist/tools/convention/validateCodeQuality.js +5 -4
- package/dist/tools/convention/validateCodeQuality.js.map +1 -1
- package/dist/tools/spec/traceabilityMatrix.d.ts +2 -0
- package/dist/tools/spec/traceabilityMatrix.d.ts.map +1 -1
- package/dist/tools/spec/traceabilityMatrix.js +50 -1
- package/dist/tools/spec/traceabilityMatrix.js.map +1 -1
- package/dist/tools/spec/traceabilityMatrix.path-resolution.test.d.ts +10 -0
- package/dist/tools/spec/traceabilityMatrix.path-resolution.test.d.ts.map +1 -0
- package/dist/tools/spec/traceabilityMatrix.path-resolution.test.js +89 -0
- package/dist/tools/spec/traceabilityMatrix.path-resolution.test.js.map +1 -0
- package/dist/tools/spec/traceabilityMatrix.test.js +19 -0
- package/dist/tools/spec/traceabilityMatrix.test.js.map +1 -1
- package/hooks/hooks.json +3 -9
- package/hooks/scripts/__tests__/.vibe/command-log.txt +39 -0
- package/hooks/scripts/__tests__/.vibe/memories/memories.db +0 -0
- package/hooks/scripts/__tests__/.vibe/memories/memories.db-shm +0 -0
- package/hooks/scripts/__tests__/.vibe/memories/memories.db-wal +0 -0
- package/hooks/scripts/__tests__/auto-test-debounce.test.js +145 -0
- package/hooks/scripts/__tests__/code-check-detectors.test.js +155 -0
- package/hooks/scripts/__tests__/dispatcher-inprocess.test.js +99 -0
- package/hooks/scripts/__tests__/post-edit-dispatcher.test.js +139 -0
- package/hooks/scripts/__tests__/pre-tool-guard.test.js +115 -1
- package/hooks/scripts/__tests__/run-ledger-verify-required.test.js +146 -0
- package/hooks/scripts/__tests__/run-ledger.test.js +330 -0
- package/hooks/scripts/__tests__/scope-from-spec.test.js +215 -0
- package/hooks/scripts/__tests__/sentinel-guard.test.js +79 -24
- package/hooks/scripts/__tests__/step-counter.test.js +95 -15
- package/hooks/scripts/__tests__/utils-npm-root.test.js +98 -0
- package/hooks/scripts/auto-commit.js +30 -11
- package/hooks/scripts/auto-format.js +96 -26
- package/hooks/scripts/auto-test.js +187 -37
- package/hooks/scripts/code-check.js +292 -99
- package/hooks/scripts/codex-hook-adapter.js +12 -1
- package/hooks/scripts/command-log.js +26 -16
- package/hooks/scripts/lib/dispatcher.js +38 -0
- package/hooks/scripts/lib/hook-context.js +101 -0
- package/hooks/scripts/lib/pr-gate-runner.js +62 -0
- package/hooks/scripts/lib/run-ledger.js +169 -0
- package/hooks/scripts/lib/scope-from-spec.js +40 -7
- package/hooks/scripts/post-edit-dispatcher.js +93 -20
- package/hooks/scripts/post-edit.js +40 -19
- package/hooks/scripts/pr-test-gate.js +8 -37
- package/hooks/scripts/pre-tool-dispatcher.js +18 -16
- package/hooks/scripts/pre-tool-guard.js +55 -52
- package/hooks/scripts/prompt-dispatcher.js +10 -0
- package/hooks/scripts/scope-guard.js +40 -39
- package/hooks/scripts/sentinel-guard.js +41 -41
- package/hooks/scripts/session-start.js +41 -16
- package/hooks/scripts/step-counter.js +100 -7
- package/hooks/scripts/stop-dispatcher.js +26 -0
- package/hooks/scripts/utils.js +96 -30
- package/hooks/scripts/verify-ledger.js +22 -0
- package/package.json +2 -2
- package/skills/arch-guard/SKILL.md +2 -2
- package/skills/characterization-test/SKILL.md +2 -2
- package/skills/exec-plan/SKILL.md +2 -2
- package/skills/spec/SKILL.md +6 -312
- package/skills/spec/references/askuser-examples.md +57 -0
- package/skills/spec/references/example-session.md +87 -0
- package/skills/spec/references/templates.md +194 -0
- package/skills/vibe.run/SKILL.md +145 -1682
- package/skills/vibe.run/references/brand-assets.md +59 -0
- package/skills/vibe.run/references/parallel-agents.md +326 -0
- package/skills/vibe.run/references/race-review.md +272 -0
- package/skills/vibe.run/references/ralph-loop.md +172 -0
- package/skills/vibe.run/references/ultrawork-mode.md +148 -0
- package/skills/vibe.trace/SKILL.md +25 -38
- package/skills/vibe.verify/SKILL.md +15 -0
- package/vibe/templates/claudemd-template.md +1 -1
- 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 {
|
|
10
|
-
import path from 'path';
|
|
8
|
+
import { runPrTestGate } from './lib/pr-gate-runner.js';
|
|
11
9
|
|
|
12
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
process.exit(0);
|
|
38
|
-
}
|
|
12
|
+
if (!testCmd) {
|
|
13
|
+
// 테스트 커맨드 없음 → PR 허용
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
39
16
|
|
|
40
|
-
|
|
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
|
-
}
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
13
|
-
*
|
|
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 {
|
|
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',
|
|
23
|
-
{ name: 'pre-tool-guard',
|
|
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에만 의미 있음
|
|
28
|
+
// scope-guard는 Edit/Write에만 의미 있음
|
|
27
29
|
if (toolName === 'Edit' || toolName === 'Write') {
|
|
28
|
-
steps.push({ name: 'scope-guard',
|
|
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',
|
|
35
|
+
steps.push({ name: 'command-log', run: commandLog });
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
// 하네스에 노이즈를 주지 않도록 디스패처 자체의 예외는 모두 흡수.
|
|
37
|
-
// exit 2 (deny 전파)는
|
|
39
|
+
// exit 2 (deny 전파)는 dispatchInProcess 내부에서 process.exit(2)로 처리되므로
|
|
38
40
|
// 여기까지 오면 "deny 아님" → 항상 exit 0.
|
|
39
41
|
try {
|
|
40
|
-
await
|
|
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
|
-
*
|
|
202
|
-
*
|
|
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
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
223
|
+
logHookDecision('pre-tool-guard', toolName, 'warn', `schema: ${schemaResult.errors.join('; ')}`);
|
|
224
|
+
// 스키마 오류는 경고만 (차단하지 않음 — 레거시 호환)
|
|
225
|
+
}
|
|
216
226
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
249
|
-
}
|
|
244
|
+
if (output) {
|
|
245
|
+
console.error(output);
|
|
246
|
+
}
|
|
250
247
|
|
|
251
|
-
// Hook trace logging
|
|
252
|
-
if (!validation.allowed) {
|
|
253
|
-
|
|
254
|
-
} else if (validation.warnings.length > 0) {
|
|
255
|
-
|
|
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
|
-
//
|
|
259
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
|
112
|
-
|
|
113
|
-
if (toolName !== 'Edit' && toolName !== 'Write') process.exit(0);
|
|
111
|
+
const filePath = extractFilePath(ctx.toolInput);
|
|
112
|
+
if (!filePath) return 0;
|
|
114
113
|
|
|
115
|
-
const
|
|
116
|
-
const filePath = extractFilePath(rawInput);
|
|
117
|
-
if (!filePath) process.exit(0);
|
|
114
|
+
const rel = toRelative(filePath);
|
|
118
115
|
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
|
126
|
-
|
|
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
|
|
129
|
-
|
|
130
|
-
lines.push(
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
143
|
-
|
|
140
|
+
return blocking ? 2 : 0;
|
|
141
|
+
}
|
|
144
142
|
|
|
145
|
-
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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
|
|
21
|
+
const SENTINEL_PREFIXES = [
|
|
22
|
+
'src/infra/lib/evolution/',
|
|
23
|
+
'hooks/scripts/lib/',
|
|
24
|
+
];
|
|
9
25
|
|
|
10
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
109
|
+
import { logHookDecision } from './utils.js';
|
|
110
|
+
import { buildCliCtx, isDirectRun } from './lib/hook-context.js';
|
|
94
111
|
|
|
95
112
|
/**
|
|
96
|
-
*
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
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);
|
|
@@ -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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
93
|
+
getLatestVersionCached(),
|
|
81
94
|
]);
|
|
82
95
|
|
|
83
96
|
console.log(session.content[0].text);
|
|
@@ -113,7 +126,7 @@ async function main() {
|
|
|
113
126
|
}
|
|
114
127
|
}
|
|
115
128
|
|
|
116
|
-
// Scope sync —
|
|
129
|
+
// Scope sync — 기본 ON. scopeGuard.enabled=false 로 명시적 opt-out 가능.
|
|
117
130
|
try {
|
|
118
131
|
const { syncScopeFile, isScopeGuardEnabled } = await import('./lib/scope-from-spec.js');
|
|
119
132
|
if (isScopeGuardEnabled(PROJECT_DIR)) {
|
|
@@ -122,6 +135,18 @@ async function main() {
|
|
|
122
135
|
console.log(`\n🚧 Scope ${result.action} from active SPECs (${path.relative(PROJECT_DIR, path.join(projectVibeRoot(PROJECT_DIR), 'scope.json'))})`);
|
|
123
136
|
}
|
|
124
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
|
+
}
|
|
125
150
|
} catch { /* scope sync is best-effort */ }
|
|
126
151
|
|
|
127
152
|
// Autonomy status summary
|