@su-record/vibe 3.1.0 → 3.2.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 +2 -2
- package/README.en.md +6 -4
- package/README.md +5 -3
- package/dist/__tests__/wiring-integrity.test.d.ts +2 -0
- package/dist/__tests__/wiring-integrity.test.d.ts.map +1 -0
- package/dist/__tests__/wiring-integrity.test.js +160 -0
- package/dist/__tests__/wiring-integrity.test.js.map +1 -0
- package/dist/cli/commands/info.d.ts.map +1 -1
- package/dist/cli/commands/info.js +2 -1
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +4 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/index.js +102 -128
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/postinstall/claude-agents.d.ts.map +1 -1
- package/dist/cli/postinstall/claude-agents.js +0 -5
- package/dist/cli/postinstall/claude-agents.js.map +1 -1
- package/dist/cli/postinstall/constants.d.ts +6 -0
- package/dist/cli/postinstall/constants.d.ts.map +1 -1
- package/dist/cli/postinstall/constants.js +42 -60
- package/dist/cli/postinstall/constants.js.map +1 -1
- package/dist/cli/postinstall/inline-skills.js +2 -2
- package/dist/cli/postinstall/inline-skills.js.map +1 -1
- package/dist/cli/postinstall/main.d.ts.map +1 -1
- package/dist/cli/postinstall/main.js +2 -0
- package/dist/cli/postinstall/main.js.map +1 -1
- package/dist/cli/postinstall.d.ts +6 -1
- package/dist/cli/postinstall.d.ts.map +1 -1
- package/dist/cli/postinstall.js +6 -1
- package/dist/cli/postinstall.js.map +1 -1
- package/dist/cli/setup/GlobalInstaller.d.ts +1 -15
- package/dist/cli/setup/GlobalInstaller.d.ts.map +1 -1
- package/dist/cli/setup/GlobalInstaller.js +16 -115
- package/dist/cli/setup/GlobalInstaller.js.map +1 -1
- package/dist/cli/setup/GlobalInstaller.test.d.ts +2 -0
- package/dist/cli/setup/GlobalInstaller.test.d.ts.map +1 -0
- package/dist/cli/setup/GlobalInstaller.test.js +16 -0
- package/dist/cli/setup/GlobalInstaller.test.js.map +1 -0
- package/dist/cli/setup/ProjectSetup.js +2 -2
- package/dist/cli/setup/ProjectSetup.js.map +1 -1
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +1 -1
- package/dist/cli/setup.js.map +1 -1
- package/dist/infra/lib/SkillRepository.js +1 -1
- package/dist/infra/lib/evolution/__tests__/integration.test.js +0 -42
- package/dist/infra/lib/evolution/__tests__/integration.test.js.map +1 -1
- package/hooks/scripts/__tests__/.vibe/command-log.txt +3 -3
- package/hooks/scripts/__tests__/clone-behaviors.test.js +33 -1
- package/hooks/scripts/__tests__/code-check-memory-write.test.js +81 -0
- package/hooks/scripts/__tests__/step-counter.test.js +21 -0
- package/hooks/scripts/auto-format.js +2 -11
- package/hooks/scripts/auto-test.js +2 -11
- package/hooks/scripts/clone-extract.js +146 -10
- package/hooks/scripts/code-check.js +27 -55
- package/hooks/scripts/lib/glob.js +32 -0
- package/hooks/scripts/lib/hook-context.js +19 -2
- package/hooks/scripts/post-edit.js +2 -3
- package/hooks/scripts/prompt-dispatcher.js +1 -1
- package/hooks/scripts/scope-guard.js +3 -42
- package/hooks/scripts/sentinel-guard.js +5 -20
- package/hooks/scripts/session-start.js +2 -2
- package/hooks/scripts/step-counter.js +35 -57
- package/package.json +2 -3
- package/skills/clone/SKILL.md +10 -216
- package/skills/clone/references/capture-rules.md +48 -0
- package/skills/clone/references/legal-and-error-recovery.md +40 -0
- package/skills/clone/references/refine-rules.md +61 -0
- package/skills/clone/references/scaffold-phases.md +59 -0
- package/skills/clone/references/setup-and-layout.md +30 -0
- package/skills/docs/SKILL.md +39 -4
- package/skills/docs/references/api-docs-changelog.md +34 -0
- package/skills/docs/references/codemaps-output.md +23 -0
- package/{agents/diagrammer.md → skills/docs/references/diagram-spec.md} +2 -15
- package/skills/exec-plan/SKILL.md +1 -1
- package/skills/exec-plan/templates/plan.md +1 -1
- package/skills/handoff/SKILL.md +3 -3
- package/skills/handoff/templates/handoff.md +1 -1
- package/skills/vibe/SKILL.md +3 -2
- package/skills/vibe.analyze/SKILL.md +12 -249
- package/skills/vibe.analyze/references/output-templates.md +188 -0
- package/skills/vibe.analyze/references/quality-gate.md +93 -0
- package/skills/vibe.clone/SKILL.md +14 -1
- package/skills/vibe.continue/SKILL.md +32 -0
- package/skills/vibe.design/SKILL.md +23 -1
- package/skills/vibe.design/references/ui-preview.md +20 -0
- package/skills/vibe.docs/SKILL.md +4 -2
- package/skills/vibe.figma/SKILL.md +33 -260
- package/skills/vibe.figma/references/branch-phases.md +113 -0
- package/skills/vibe.figma/references/state-schema.md +42 -0
- package/skills/vibe.figma/references/step-algorithms.md +204 -0
- package/skills/vibe.image/SKILL.md +62 -0
- package/skills/vibe.image/references/image-generation-examples.md +39 -0
- package/skills/vibe.reason/SKILL.md +3 -183
- package/skills/vibe.reason/references/output-format-template.md +57 -0
- package/skills/vibe.reason/references/quality-rubrics.md +94 -0
- package/skills/vibe.reason/references/worked-example.md +45 -0
- package/skills/vibe.review/SKILL.md +13 -348
- package/skills/vibe.review/references/boundary-check.md +25 -0
- package/skills/vibe.review/references/output-template.md +74 -0
- package/skills/vibe.review/references/quality-gate.md +84 -0
- package/skills/vibe.review/references/race-mode.md +117 -0
- package/skills/vibe.review/references/worked-examples.md +94 -0
- package/vibe/templates/claudemd-template.md +1 -1
- package/agents/acceptance-tester.md +0 -56
- package/agents/documenter.md +0 -43
- package/agents/figma/figma-engineer.md +0 -76
- package/dist/cli/postinstall/index.d.ts +0 -23
- package/dist/cli/postinstall/index.d.ts.map +0 -1
- package/dist/cli/postinstall/index.js +0 -23
- package/dist/cli/postinstall/index.js.map +0 -1
- package/dist/cli/setup/index.d.ts +0 -9
- package/dist/cli/setup/index.d.ts.map +0 -1
- package/dist/cli/setup/index.js +0 -12
- package/dist/cli/setup/index.js.map +0 -1
- 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/evolution-engine.js +0 -91
- package/hooks/scripts/hud-status.js +0 -321
- package/hooks/scripts/skill-injector.js +0 -83
- package/skills/vibe.utils/SKILL.md +0 -415
|
@@ -14,6 +14,7 @@ import { getToolsBaseUrl, PROJECT_DIR, readProjectConfig } from './utils.js';
|
|
|
14
14
|
import { readFileSync } from 'fs';
|
|
15
15
|
import path from 'path';
|
|
16
16
|
import { buildCliCtx, isDirectRun } from './lib/hook-context.js';
|
|
17
|
+
import { globToRegExp } from './lib/glob.js';
|
|
17
18
|
|
|
18
19
|
const BASE_URL = getToolsBaseUrl();
|
|
19
20
|
|
|
@@ -42,35 +43,6 @@ const DEFAULT_CONSOLE_ALLOW_GLOBS = [
|
|
|
42
43
|
'**/__tests__/**',
|
|
43
44
|
];
|
|
44
45
|
|
|
45
|
-
/**
|
|
46
|
-
* 경량 glob → RegExp 변환 (scope-guard와 동일 알고리즘, 독립 복사본).
|
|
47
|
-
* @param {string} glob
|
|
48
|
-
* @returns {RegExp}
|
|
49
|
-
*/
|
|
50
|
-
function globToRegExp(glob) {
|
|
51
|
-
const normalized = glob.replace(/\\/g, '/');
|
|
52
|
-
let out = '';
|
|
53
|
-
for (let i = 0; i < normalized.length; i++) {
|
|
54
|
-
const c = normalized[i];
|
|
55
|
-
if (c === '*') {
|
|
56
|
-
if (normalized[i + 1] === '*') {
|
|
57
|
-
out += '.*';
|
|
58
|
-
i++;
|
|
59
|
-
if (normalized[i + 1] === '/') i++;
|
|
60
|
-
} else {
|
|
61
|
-
out += '[^/]*';
|
|
62
|
-
}
|
|
63
|
-
} else if (c === '?') {
|
|
64
|
-
out += '[^/]';
|
|
65
|
-
} else if ('.+^$()|{}[]\\'.includes(c)) {
|
|
66
|
-
out += '\\' + c;
|
|
67
|
-
} else {
|
|
68
|
-
out += c;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return new RegExp('^' + out + '$');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
46
|
/**
|
|
75
47
|
* .vibe/config.json의 qualityCheck.consoleAllow 글로브 목록 로드.
|
|
76
48
|
* 기본 글로브와 병합하여 반환.
|
|
@@ -105,19 +77,12 @@ function isConsoleAllowed(filePath) {
|
|
|
105
77
|
}
|
|
106
78
|
|
|
107
79
|
/**
|
|
108
|
-
* hook
|
|
80
|
+
* hook ctx에서 수정된 파일 경로 추출.
|
|
109
81
|
* @param {object} ctx
|
|
110
82
|
* @returns {string[]}
|
|
111
83
|
*/
|
|
112
84
|
function getModifiedFiles(ctx) {
|
|
113
|
-
|
|
114
|
-
const parsed = ctx.payload || (ctx.hookInput ? JSON.parse(ctx.hookInput) : null);
|
|
115
|
-
const filePath = parsed?.tool_input?.file_path || parsed?.tool_input?.path;
|
|
116
|
-
return filePath ? [filePath] : [];
|
|
117
|
-
} catch {
|
|
118
|
-
// ignore
|
|
119
|
-
}
|
|
120
|
-
return [];
|
|
85
|
+
return ctx.filePath ? [ctx.filePath] : [];
|
|
121
86
|
}
|
|
122
87
|
|
|
123
88
|
/**
|
|
@@ -217,7 +182,7 @@ function runDetectors(filePath) {
|
|
|
217
182
|
/**
|
|
218
183
|
* in-process 진입점 — 품질 검사 + 관찰 캡처.
|
|
219
184
|
* findings 배열을 반환한다 (디스패처가 수집해 additionalContext에 주입).
|
|
220
|
-
* @param {{
|
|
185
|
+
* @param {{ filePath: string }} ctx
|
|
221
186
|
* @returns {Promise<{ exitCode: number, findings: string[] }>}
|
|
222
187
|
*/
|
|
223
188
|
export async function run(ctx) {
|
|
@@ -225,9 +190,10 @@ export async function run(ctx) {
|
|
|
225
190
|
const files = getModifiedFiles(ctx);
|
|
226
191
|
if (files.length === 0) return { exitCode: 0, findings };
|
|
227
192
|
|
|
228
|
-
// 1. 하드룰 탐지기 실행 (changed file only
|
|
229
|
-
// 커밋 게이트(verifyRequired)는 태우지 않는다:
|
|
230
|
-
// auto-commit을 차단하는 결합은 제거됨
|
|
193
|
+
// 1. 하드룰 탐지기 실행 (changed file only, regex only — 동적 import 없음) —
|
|
194
|
+
// additionalContext 주입만. 커밋 게이트(verifyRequired)는 태우지 않는다:
|
|
195
|
+
// 정규식 오탐 하나가 auto-commit을 차단하는 결합은 제거됨
|
|
196
|
+
// (harness-review-2026-07-01).
|
|
231
197
|
try {
|
|
232
198
|
const { p1 } = runDetectors(files[0]);
|
|
233
199
|
for (const msg of p1) findings.push(msg);
|
|
@@ -235,6 +201,10 @@ export async function run(ctx) {
|
|
|
235
201
|
// 탐지기 실패 → fail-open, 계속 진행
|
|
236
202
|
}
|
|
237
203
|
|
|
204
|
+
// validateCodeQuality가 처리하지 않는 확장자는 무거운 동적 import
|
|
205
|
+
// (convention/index.js·memory/index.js — 모듈 그래프 + SQLite 오픈) 전에 조기 반환.
|
|
206
|
+
if (!CODE_EXT_RE.test(files[0])) return { exitCode: 0, findings };
|
|
207
|
+
|
|
238
208
|
// 2. validateCodeQuality 호출 (P1/P2 필터)
|
|
239
209
|
try {
|
|
240
210
|
const module = await import(`${BASE_URL}convention/index.js`);
|
|
@@ -249,19 +219,21 @@ export async function run(ctx) {
|
|
|
249
219
|
// Silently continue on check failure
|
|
250
220
|
}
|
|
251
221
|
|
|
252
|
-
// 3. 관찰 자동 캡처
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
222
|
+
// 3. 관찰 자동 캡처 — 위반(findings)이 실제로 있을 때만 SQLite 기록 (clean edit은 write 생략)
|
|
223
|
+
if (findings.length > 0) {
|
|
224
|
+
try {
|
|
225
|
+
const memModule = await import(`${BASE_URL}memory/index.js`);
|
|
226
|
+
const { type, title } = classifyObservation(files);
|
|
227
|
+
|
|
228
|
+
await memModule.addObservation({
|
|
229
|
+
type,
|
|
230
|
+
title: `${title}: ${files.map(f => f.split(/[\\/]/).pop()).join(', ')}`,
|
|
231
|
+
filesModified: files,
|
|
232
|
+
projectPath: PROJECT_DIR,
|
|
233
|
+
});
|
|
234
|
+
} catch {
|
|
235
|
+
// 관찰 캡처 실패해도 무시
|
|
236
|
+
}
|
|
265
237
|
}
|
|
266
238
|
|
|
267
239
|
return { exitCode: 0, findings };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 경량 glob → RegExp 변환 — code-check.js·scope-guard.js 공용 (중복 제거).
|
|
3
|
+
* - `**` : 경로 구분자 포함 임의 문자열
|
|
4
|
+
* - `*` : 구분자 제외 임의 문자열
|
|
5
|
+
* - `?` : 구분자 제외 한 글자
|
|
6
|
+
* - 기타 정규식 메타문자는 이스케이프
|
|
7
|
+
* @param {string} glob
|
|
8
|
+
* @returns {RegExp}
|
|
9
|
+
*/
|
|
10
|
+
export function globToRegExp(glob) {
|
|
11
|
+
const normalized = glob.replace(/\\/g, '/');
|
|
12
|
+
let out = '';
|
|
13
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
14
|
+
const c = normalized[i];
|
|
15
|
+
if (c === '*') {
|
|
16
|
+
if (normalized[i + 1] === '*') {
|
|
17
|
+
out += '.*';
|
|
18
|
+
i++;
|
|
19
|
+
if (normalized[i + 1] === '/') i++; // `**/` → `.*`
|
|
20
|
+
} else {
|
|
21
|
+
out += '[^/]*';
|
|
22
|
+
}
|
|
23
|
+
} else if (c === '?') {
|
|
24
|
+
out += '[^/]';
|
|
25
|
+
} else if ('.+^$()|{}[]\\'.includes(c)) {
|
|
26
|
+
out += '\\' + c;
|
|
27
|
+
} else {
|
|
28
|
+
out += c;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return new RegExp('^' + out + '$');
|
|
32
|
+
}
|
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
* 2. standalone CLI: antigravity-hooks.json / 기존 테스트가 스크립트를
|
|
8
8
|
* 직접 실행 — isDirectRun()으로 감지해 stdin/argv/env에서 ctx를 구성
|
|
9
9
|
*
|
|
10
|
-
* ctx 형태: { toolName, toolInput, payload, hookInput }
|
|
10
|
+
* ctx 형태: { toolName, toolInput, payload, hookInput, filePath }
|
|
11
11
|
* - toolName: payload.tool_name 우선, argv 폴백 (현행 각 스크립트와 동일 우선순위)
|
|
12
12
|
* - toolInput: 문자열 정규화된 tool_input (payload → argv[3] → env.TOOL_INPUT)
|
|
13
13
|
* - payload: 파싱된 stdin JSON 또는 null
|
|
14
14
|
* - hookInput: stdin 원문 문자열 (code-check의 HOOK_INPUT 사용처 대체)
|
|
15
|
+
* - filePath: toolInput 에서 뽑은 대상 파일 경로 (file_path → notebook_path →
|
|
16
|
+
* path 우선순위 — 각 스크립트의 인라인 추출 로직과 동일 규약)
|
|
15
17
|
*/
|
|
16
18
|
import fs from 'fs';
|
|
17
19
|
import { pathToFileURL } from 'url';
|
|
@@ -96,6 +98,21 @@ export function readStdinSync() {
|
|
|
96
98
|
return { raw: null, parsed: null, truncated: false };
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
/**
|
|
102
|
+
* toolInput(문자열화된 JSON)에서 대상 파일 경로 추출.
|
|
103
|
+
* 각 훅 스크립트의 인라인 추출과 동일 우선순위: file_path → notebook_path → path.
|
|
104
|
+
* @param {string} toolInput
|
|
105
|
+
* @returns {string}
|
|
106
|
+
*/
|
|
107
|
+
function extractFilePath(toolInput) {
|
|
108
|
+
try {
|
|
109
|
+
const parsed = JSON.parse(toolInput || '{}');
|
|
110
|
+
return parsed.file_path || parsed.notebook_path || parsed.path || '';
|
|
111
|
+
} catch {
|
|
112
|
+
return '';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
99
116
|
/**
|
|
100
117
|
* 실행 컨텍스트 구성. 우선순위는 현행 스크립트들과 동일:
|
|
101
118
|
* toolName: payload.tool_name → argvToolName
|
|
@@ -106,7 +123,7 @@ export function buildCtx({ rawInput = null, payload = null, argvToolName = '', a
|
|
|
106
123
|
const toolInput = payload?.tool_input !== undefined && payload?.tool_input !== null
|
|
107
124
|
? (typeof payload.tool_input === 'string' ? payload.tool_input : JSON.stringify(payload.tool_input))
|
|
108
125
|
: (argvToolInput || process.env.TOOL_INPUT || '');
|
|
109
|
-
return { toolName, toolInput, payload, hookInput: rawInput };
|
|
126
|
+
return { toolName, toolInput, payload, hookInput: rawInput, filePath: extractFilePath(toolInput) };
|
|
110
127
|
}
|
|
111
128
|
|
|
112
129
|
/** standalone CLI 모드용 ctx — stdin/argv/env에서 구성 */
|
|
@@ -17,14 +17,13 @@ const CODE_EXT_RE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
|
|
|
17
17
|
/**
|
|
18
18
|
* in-process 진입점 — console.log 감지만 수행.
|
|
19
19
|
* findings 배열 반환 (디스패처가 수집해 additionalContext에 주입).
|
|
20
|
-
* @param {{
|
|
20
|
+
* @param {{ filePath: string }} ctx
|
|
21
21
|
* @returns {Promise<{ exitCode: number, findings: string[] }>}
|
|
22
22
|
*/
|
|
23
23
|
export async function run(ctx) {
|
|
24
24
|
const findings = [];
|
|
25
25
|
try {
|
|
26
|
-
const
|
|
27
|
-
const filePath = input.file_path || input.path || '';
|
|
26
|
+
const filePath = ctx.filePath;
|
|
28
27
|
|
|
29
28
|
if (filePath && CODE_EXT_RE.test(filePath)) {
|
|
30
29
|
const resolved = path.resolve(filePath);
|
|
@@ -76,7 +76,7 @@ const DISPATCH_RULES = [
|
|
|
76
76
|
{
|
|
77
77
|
pattern: /e2e.*테스트|e2e.*test|playwright|브라우저.*테스트|browser.*test/i,
|
|
78
78
|
script: null,
|
|
79
|
-
echo: '[E2E MODE] Use /vibe.
|
|
79
|
+
echo: '[E2E MODE] Use /vibe.verify --e2e for Playwright-based browser testing. Supports visual regression and video recording.',
|
|
80
80
|
label: 'e2e-echo',
|
|
81
81
|
},
|
|
82
82
|
|
|
@@ -23,6 +23,7 @@ import fs from 'fs';
|
|
|
23
23
|
import path from 'path';
|
|
24
24
|
import { PROJECT_DIR, logHookDecision, projectVibePath, projectVibeRoot } from './utils.js';
|
|
25
25
|
import { buildCliCtx, isDirectRun } from './lib/hook-context.js';
|
|
26
|
+
import { globToRegExp } from './lib/glob.js';
|
|
26
27
|
|
|
27
28
|
const SCOPE_PATH = projectVibePath(PROJECT_DIR, 'scope.json');
|
|
28
29
|
|
|
@@ -45,37 +46,6 @@ function readScope() {
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
/**
|
|
49
|
-
* 경량 glob → RegExp 변환.
|
|
50
|
-
* - `**` : 경로 구분자 포함 임의 문자열
|
|
51
|
-
* - `*` : 구분자 제외 임의 문자열
|
|
52
|
-
* - `?` : 구분자 제외 한 글자
|
|
53
|
-
* - 기타 정규식 메타문자는 이스케이프
|
|
54
|
-
*/
|
|
55
|
-
function globToRegExp(glob) {
|
|
56
|
-
const normalized = glob.replace(/\\/g, '/');
|
|
57
|
-
let out = '';
|
|
58
|
-
for (let i = 0; i < normalized.length; i++) {
|
|
59
|
-
const c = normalized[i];
|
|
60
|
-
if (c === '*') {
|
|
61
|
-
if (normalized[i + 1] === '*') {
|
|
62
|
-
out += '.*';
|
|
63
|
-
i++;
|
|
64
|
-
if (normalized[i + 1] === '/') i++; // `**/` → `.*`
|
|
65
|
-
} else {
|
|
66
|
-
out += '[^/]*';
|
|
67
|
-
}
|
|
68
|
-
} else if (c === '?') {
|
|
69
|
-
out += '[^/]';
|
|
70
|
-
} else if ('.+^$()|{}[]\\'.includes(c)) {
|
|
71
|
-
out += '\\' + c;
|
|
72
|
-
} else {
|
|
73
|
-
out += c;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return new RegExp('^' + out + '$');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
49
|
function matchesAny(relPath, patterns) {
|
|
80
50
|
return patterns.some(p => globToRegExp(p).test(relPath));
|
|
81
51
|
}
|
|
@@ -87,18 +57,9 @@ function toRelative(filePath) {
|
|
|
87
57
|
return rel || path.basename(filePath).replace(/\\/g, '/');
|
|
88
58
|
}
|
|
89
59
|
|
|
90
|
-
function extractFilePath(toolInput) {
|
|
91
|
-
if (!toolInput) return '';
|
|
92
|
-
if (typeof toolInput === 'string') {
|
|
93
|
-
try { return JSON.parse(toolInput).file_path || ''; }
|
|
94
|
-
catch { return toolInput; }
|
|
95
|
-
}
|
|
96
|
-
return typeof toolInput.file_path === 'string' ? toolInput.file_path : '';
|
|
97
|
-
}
|
|
98
|
-
|
|
99
60
|
/**
|
|
100
61
|
* in-process 진입점 — 디스패처가 ctx를 전달해 직접 호출.
|
|
101
|
-
* @param {{ toolName: string, toolInput: string }} ctx
|
|
62
|
+
* @param {{ toolName: string, toolInput: string, filePath: string }} ctx
|
|
102
63
|
* @returns {Promise<number>} exit code (0 = allow/no-op, 2 = block)
|
|
103
64
|
*/
|
|
104
65
|
export async function run(ctx) {
|
|
@@ -108,7 +69,7 @@ export async function run(ctx) {
|
|
|
108
69
|
const toolName = ctx.toolName;
|
|
109
70
|
if (toolName !== 'Edit' && toolName !== 'Write') return 0;
|
|
110
71
|
|
|
111
|
-
const filePath =
|
|
72
|
+
const filePath = ctx.filePath;
|
|
112
73
|
if (!filePath) return 0;
|
|
113
74
|
|
|
114
75
|
const rel = toRelative(filePath);
|
|
@@ -30,22 +30,6 @@ const SENTINEL_PATH_RE =
|
|
|
30
30
|
const DANGEROUS_BASH_RE =
|
|
31
31
|
/\b(rm\s+-rf|kill\s+-9|drop\s+table|truncate|shutdown|reboot|mkfs|dd\s+if=)\b/i;
|
|
32
32
|
|
|
33
|
-
/**
|
|
34
|
-
* Extract file path from tool input
|
|
35
|
-
*/
|
|
36
|
-
function extractFilePath(toolName, input) {
|
|
37
|
-
if (!input) return null;
|
|
38
|
-
try {
|
|
39
|
-
const parsed = typeof input === 'string' ? JSON.parse(input) : input;
|
|
40
|
-
if (toolName === 'Write' || toolName === 'Edit' || toolName === 'Read') {
|
|
41
|
-
return parsed.file_path || parsed.filePath || null;
|
|
42
|
-
}
|
|
43
|
-
} catch {
|
|
44
|
-
return typeof input === 'string' ? input : null;
|
|
45
|
-
}
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
33
|
/**
|
|
50
34
|
* Extract command from Bash tool input
|
|
51
35
|
*/
|
|
@@ -72,10 +56,11 @@ function isSentinelPath(filePath) {
|
|
|
72
56
|
/**
|
|
73
57
|
* Main guard logic
|
|
74
58
|
*/
|
|
75
|
-
function guard(
|
|
59
|
+
function guard(ctx) {
|
|
60
|
+
const { toolName, toolInput } = ctx;
|
|
76
61
|
// Write/Edit targeting sentinel files → block
|
|
77
62
|
if (toolName === 'Write' || toolName === 'Edit') {
|
|
78
|
-
const filePath =
|
|
63
|
+
const filePath = ctx.filePath;
|
|
79
64
|
if (isSentinelPath(filePath)) {
|
|
80
65
|
return {
|
|
81
66
|
decision: 'block',
|
|
@@ -111,11 +96,11 @@ import { buildCliCtx, isDirectRun } from './lib/hook-context.js';
|
|
|
111
96
|
|
|
112
97
|
/**
|
|
113
98
|
* in-process 진입점 — 디스패처가 ctx를 전달해 직접 호출.
|
|
114
|
-
* @param {{ toolName: string, toolInput: string }} ctx
|
|
99
|
+
* @param {{ toolName: string, toolInput: string, filePath: string }} ctx
|
|
115
100
|
* @returns {Promise<number>} exit code (2 = deny 규약, 0 = allow)
|
|
116
101
|
*/
|
|
117
102
|
export async function run(ctx) {
|
|
118
|
-
const result = guard(ctx
|
|
103
|
+
const result = guard(ctx);
|
|
119
104
|
if (result) {
|
|
120
105
|
logHookDecision('sentinel-guard', ctx.toolName, 'block', result.reason);
|
|
121
106
|
console.log(JSON.stringify(result));
|
|
@@ -89,7 +89,7 @@ async function main() {
|
|
|
89
89
|
// 인덱스만 주입 (harness-review-2026-07-01 P1-5):
|
|
90
90
|
// 이전에는 startSession 전체 요약(git log·테스트 상태·관찰·리플렉션·RAG
|
|
91
91
|
// goals/constraints/decisions)을 매 세션 덤프했다. 이제는 1줄 인덱스만
|
|
92
|
-
// 주입하고, 전체 컨텍스트 복원은 명시적 `/vibe.
|
|
92
|
+
// 주입하고, 전체 컨텍스트 복원은 명시적 `/vibe.continue` 전용.
|
|
93
93
|
const [time, memories, latestVersion] = await Promise.all([
|
|
94
94
|
timeModule.getCurrentTime({ format: 'human', timezone: 'Asia/Seoul' }),
|
|
95
95
|
memoryModule.listMemories({ limit: 5, projectPath: PROJECT_DIR }),
|
|
@@ -104,7 +104,7 @@ async function main() {
|
|
|
104
104
|
const iterMod = await import(`${LIB_BASE_P}IterationTracker.js`);
|
|
105
105
|
const progressLine = iterMod.getProgressSummary(PROJECT_DIR);
|
|
106
106
|
if (progressLine) {
|
|
107
|
-
console.log(`\n📋 ${progressLine} — resume full context: /vibe.
|
|
107
|
+
console.log(`\n📋 ${progressLine} — resume full context: /vibe.continue`);
|
|
108
108
|
}
|
|
109
109
|
} catch { /* progress index is best-effort */ }
|
|
110
110
|
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import fs from 'fs';
|
|
26
26
|
import path from 'path';
|
|
27
27
|
import { PROJECT_DIR, projectVibePath } from './utils.js';
|
|
28
|
+
import { buildCliCtx } from './lib/hook-context.js';
|
|
28
29
|
|
|
29
30
|
const METRICS_DIR = projectVibePath(PROJECT_DIR, 'metrics');
|
|
30
31
|
const ANTI_PATTERNS_DIR = projectVibePath(PROJECT_DIR, 'anti-patterns');
|
|
@@ -44,36 +45,14 @@ const READ_ONLY_TOOLS = new Set([
|
|
|
44
45
|
'TodoWrite', 'ToolSearch', 'ListMcpResourcesTool',
|
|
45
46
|
]);
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// ─────────────────────────────────────────────────────
|
|
50
|
-
function readStdinSync() {
|
|
51
|
-
try {
|
|
52
|
-
if (process.stdin.isTTY) return null;
|
|
53
|
-
const buf = Buffer.alloc(65536);
|
|
54
|
-
const bytesRead = fs.readSync(0, buf, 0, buf.length, null);
|
|
55
|
-
if (bytesRead > 0) return JSON.parse(buf.toString('utf-8', 0, bytesRead));
|
|
56
|
-
} catch { /* ignore */ }
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function parseToolInput(raw) {
|
|
61
|
-
if (!raw) return {};
|
|
62
|
-
if (typeof raw === 'string') {
|
|
63
|
-
try { return JSON.parse(raw); } catch { return {}; }
|
|
64
|
-
}
|
|
65
|
-
return raw;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function extractTargetFile(toolInput) {
|
|
69
|
-
const fp = toolInput.file_path || toolInput.notebook_path || toolInput.path || null;
|
|
70
|
-
if (!fp) return null;
|
|
48
|
+
function extractTargetFile(filePath) {
|
|
49
|
+
if (!filePath) return null;
|
|
71
50
|
try {
|
|
72
|
-
const abs = path.isAbsolute(
|
|
51
|
+
const abs = path.isAbsolute(filePath) ? filePath : path.resolve(PROJECT_DIR, filePath);
|
|
73
52
|
const rel = path.relative(path.resolve(PROJECT_DIR), abs).replace(/\\/g, '/');
|
|
74
|
-
return rel || path.basename(
|
|
53
|
+
return rel || path.basename(filePath);
|
|
75
54
|
} catch {
|
|
76
|
-
return
|
|
55
|
+
return filePath;
|
|
77
56
|
}
|
|
78
57
|
}
|
|
79
58
|
|
|
@@ -180,19 +159,18 @@ function bumpCounter(toolName, currentLineCount) {
|
|
|
180
159
|
// 책임 2: current-run.jsonl append + error_category 채움
|
|
181
160
|
// 항상 즉시 append — 스로틀 없음 (3-fail detector, recipe-extractor 가 소비)
|
|
182
161
|
// ─────────────────────────────────────────────────────
|
|
183
|
-
function appendJsonl(
|
|
184
|
-
const toolName =
|
|
162
|
+
function appendJsonl(ctx) {
|
|
163
|
+
const toolName = ctx.toolName;
|
|
185
164
|
if (!toolName) return null;
|
|
186
165
|
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
const errorCategory = ok ? null : classifyError(stdinPayload?.tool_response);
|
|
166
|
+
const ok = !isResponseError(ctx.payload?.tool_response);
|
|
167
|
+
const errorCategory = ok ? null : classifyError(ctx.payload?.tool_response);
|
|
190
168
|
|
|
191
169
|
const record = {
|
|
192
170
|
ts: new Date().toISOString(),
|
|
193
171
|
tool: toolName,
|
|
194
172
|
ok,
|
|
195
|
-
target_file: extractTargetFile(
|
|
173
|
+
target_file: extractTargetFile(ctx.filePath),
|
|
196
174
|
error_category: errorCategory,
|
|
197
175
|
};
|
|
198
176
|
|
|
@@ -214,18 +192,27 @@ function appendJsonl(stdinPayload) {
|
|
|
214
192
|
}
|
|
215
193
|
|
|
216
194
|
/**
|
|
217
|
-
* jsonl
|
|
218
|
-
*
|
|
195
|
+
* current-run.jsonl 을 1회만 읽어 (a) 스로틀 판단용 총 라인 수와
|
|
196
|
+
* (b) 3-fail detector 용 최근 FAIL_WINDOW 줄을 함께 파생한다.
|
|
197
|
+
*
|
|
198
|
+
* 이전에는 jsonlLineCount()·readTailWindow() 가 각자 파일 전체를 다시 읽어
|
|
199
|
+
* 실패 이벤트마다 이벤트당 최대 2회 풀리드가 발생했다 — 여기서 1회로 통합.
|
|
200
|
+
* 파일은 회전 상한(2MB / MAX_JSONL_LINES)을 벗어나지 않도록 유지되므로
|
|
201
|
+
* (appendJsonl의 회전 로직 참고) 이 read 로도 기존과 동일하게 정확한 라인 수를
|
|
202
|
+
* 얻는다 — 스로틀/3-fail 판정 결과는 회전 상한 이내 파일에서 이전과 동일하다.
|
|
203
|
+
* @returns {{ lineCount: number, tail: object[] }}
|
|
219
204
|
*/
|
|
220
|
-
function
|
|
205
|
+
function readJsonlState() {
|
|
206
|
+
let lines;
|
|
221
207
|
try {
|
|
222
|
-
|
|
223
|
-
// 실제 라인 수는 appendJsonl 직후 여기서 읽으면 +1 반영됨
|
|
224
|
-
const content = fs.readFileSync(CURRENT_RUN_JSONL, 'utf-8');
|
|
225
|
-
return content.split('\n').filter(Boolean).length;
|
|
208
|
+
lines = fs.readFileSync(CURRENT_RUN_JSONL, 'utf-8').split('\n').filter(Boolean);
|
|
226
209
|
} catch {
|
|
227
|
-
return 1; // 파일 없음 → 첫 이벤트로 간주
|
|
210
|
+
return { lineCount: 1, tail: [] }; // 파일 없음 → 첫 이벤트로 간주
|
|
228
211
|
}
|
|
212
|
+
const tail = lines.slice(-FAIL_WINDOW).map((l) => {
|
|
213
|
+
try { return JSON.parse(l); } catch { return null; }
|
|
214
|
+
}).filter(Boolean);
|
|
215
|
+
return { lineCount: lines.length, tail };
|
|
229
216
|
}
|
|
230
217
|
|
|
231
218
|
// ─────────────────────────────────────────────────────
|
|
@@ -248,16 +235,7 @@ function fileSlug(targetFile) {
|
|
|
248
235
|
return targetFile.replace(/[\/\.]/g, '-');
|
|
249
236
|
}
|
|
250
237
|
|
|
251
|
-
function
|
|
252
|
-
if (!fs.existsSync(CURRENT_RUN_JSONL)) return [];
|
|
253
|
-
const raw = fs.readFileSync(CURRENT_RUN_JSONL, 'utf-8').split('\n').filter(Boolean);
|
|
254
|
-
return raw.slice(-FAIL_WINDOW).map((l) => {
|
|
255
|
-
try { return JSON.parse(l); } catch { return null; }
|
|
256
|
-
}).filter(Boolean);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function detectThreeFail() {
|
|
260
|
-
const window = readTailWindow();
|
|
238
|
+
function detectThreeFail(window) {
|
|
261
239
|
const groups = new Map();
|
|
262
240
|
for (const r of window) {
|
|
263
241
|
if (r.ok || !r.error_category) continue;
|
|
@@ -318,8 +296,8 @@ try {
|
|
|
318
296
|
fs.mkdirSync(METRICS_DIR, { recursive: true });
|
|
319
297
|
}
|
|
320
298
|
|
|
321
|
-
const
|
|
322
|
-
const toolName =
|
|
299
|
+
const ctx = buildCliCtx();
|
|
300
|
+
const toolName = ctx.toolName;
|
|
323
301
|
|
|
324
302
|
// defense-in-depth: 읽기 전용 툴은 조기 종료 (matcher 단에서 이미 제거됨)
|
|
325
303
|
if (toolName && READ_ONLY_TOOLS.has(toolName)) {
|
|
@@ -328,16 +306,16 @@ try {
|
|
|
328
306
|
|
|
329
307
|
// 책임 2를 먼저 실행 — jsonl 라인 수가 bumpCounter 스로틀 판단에 쓰임
|
|
330
308
|
let lastRecord = null;
|
|
331
|
-
try { lastRecord = appendJsonl(
|
|
309
|
+
try { lastRecord = appendJsonl(ctx); } catch { /* 로깅 실패 무시 */ }
|
|
332
310
|
|
|
333
|
-
// 책임 1: jsonl append 후 라인 수로 스로틀 판단
|
|
334
|
-
const lineCount =
|
|
311
|
+
// 책임 1: jsonl append 후 라인 수로 스로틀 판단 (책임 3의 tail window 와 1회 읽기 공유)
|
|
312
|
+
const { lineCount, tail } = readJsonlState();
|
|
335
313
|
try { bumpCounter(toolName, lineCount); } catch { /* 카운터 실패 무시 */ }
|
|
336
314
|
|
|
337
315
|
// 책임 3
|
|
338
316
|
try {
|
|
339
317
|
if (lastRecord && !lastRecord.ok && lastRecord.error_category) {
|
|
340
|
-
const trip = detectThreeFail();
|
|
318
|
+
const trip = detectThreeFail(tail);
|
|
341
319
|
if (trip) writeAntiPattern(trip);
|
|
342
320
|
}
|
|
343
321
|
} catch { /* anti-pattern 작성 실패 무시 */ }
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@su-record/vibe",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "AI Coding Framework for Claude Code —
|
|
3
|
+
"version": "3.2.0",
|
|
4
|
+
"description": "AI Coding Framework for Claude Code — 7+ agents, 60 skills, multi-LLM orchestration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cli/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -94,7 +94,6 @@
|
|
|
94
94
|
"dist/",
|
|
95
95
|
"vibe/",
|
|
96
96
|
"languages/",
|
|
97
|
-
"commands/",
|
|
98
97
|
"agents/",
|
|
99
98
|
"skills/",
|
|
100
99
|
"hooks/",
|