@su-record/vibe
Advanced tools
| /** | ||
| * curation-index.js — Phase 3 인덱스 로더 테스트 | ||
| * | ||
| * session-start.js 가 다음 세션에 prepend 할 1줄 요약을 만드는 모듈. | ||
| * 본격 YAML parser 의존 없이 우리가 작성한 frontmatter 형식만 처리한다. | ||
| */ | ||
| import { describe, it, expect, beforeEach } from 'vitest'; | ||
| import fs from 'fs'; | ||
| import os from 'os'; | ||
| import path from 'path'; | ||
| import { loadCurationIndex, _internal } from '../lib/curation-index.js'; | ||
| const { parseFrontmatter } = _internal; | ||
| function makeTmpProject() { | ||
| return fs.mkdtempSync(path.join(os.tmpdir(), 'vibe-curation-index-')); | ||
| } | ||
| function writeRecipe(projectDir, filename, frontmatter, body = 'body text') { | ||
| const dir = path.join(projectDir, '.vibe', 'recipes'); | ||
| fs.mkdirSync(dir, { recursive: true }); | ||
| const fm = Object.entries(frontmatter) | ||
| .map(([k, v]) => typeof v === 'string' ? `${k}: "${v}"` : `${k}: ${v}`) | ||
| .join('\n'); | ||
| fs.writeFileSync(path.join(dir, filename), `---\n${fm}\n---\n\n${body}\n`); | ||
| } | ||
| function writeAntiPattern(projectDir, filename, frontmatter, body = 'body') { | ||
| const dir = path.join(projectDir, '.vibe', 'anti-patterns'); | ||
| fs.mkdirSync(dir, { recursive: true }); | ||
| const fm = Object.entries(frontmatter) | ||
| .map(([k, v]) => typeof v === 'string' ? `${k}: "${v}"` : `${k}: ${v}`) | ||
| .join('\n'); | ||
| fs.writeFileSync(path.join(dir, filename), `---\n${fm}\n---\n\n${body}\n`); | ||
| } | ||
| describe('parseFrontmatter', () => { | ||
| it('단순 key-value 파싱', () => { | ||
| const input = `---\nslug: foo\ntype: recipe\n---\nbody`; | ||
| expect(parseFrontmatter(input)).toEqual({ slug: 'foo', type: 'recipe' }); | ||
| }); | ||
| it('따옴표로 감싼 값', () => { | ||
| const input = `---\nrecipe: "use claude --model"\n---\n`; | ||
| expect(parseFrontmatter(input).recipe).toBe('use claude --model'); | ||
| }); | ||
| it('이스케이프된 따옴표', () => { | ||
| const input = `---\nmsg: "say \\"hi\\""\n---\n`; | ||
| expect(parseFrontmatter(input).msg).toBe('say "hi"'); | ||
| }); | ||
| it('frontmatter 가 없으면 null', () => { | ||
| expect(parseFrontmatter('no frontmatter here')).toBeNull(); | ||
| }); | ||
| it('알 수 없는 라인 무시', () => { | ||
| const input = `---\nslug: foo\n# comment line\nrandom garbage\ntype: recipe\n---\n`; | ||
| expect(parseFrontmatter(input)).toEqual({ slug: 'foo', type: 'recipe' }); | ||
| }); | ||
| }); | ||
| describe('loadCurationIndex', () => { | ||
| let projectDir; | ||
| beforeEach(() => { | ||
| projectDir = makeTmpProject(); | ||
| }); | ||
| it('빈 .vibe/ 면 빈 인덱스', () => { | ||
| const idx = loadCurationIndex(projectDir); | ||
| expect(idx.recipes).toEqual([]); | ||
| expect(idx.antiPatterns).toEqual([]); | ||
| }); | ||
| it('recipe 1개 로드', () => { | ||
| writeRecipe(projectDir, 'a.md', { | ||
| slug: 'login__20260507-100000', | ||
| type: 'recipe', | ||
| 'symptom-context': 'login flow', | ||
| recipe: 'pin model and retry transient', | ||
| created: '2026-05-07', | ||
| }); | ||
| const idx = loadCurationIndex(projectDir); | ||
| expect(idx.recipes).toHaveLength(1); | ||
| expect(idx.recipes[0]).toEqual({ | ||
| slug: 'login__20260507-100000', | ||
| summary: 'pin model and retry transient', | ||
| }); | ||
| }); | ||
| it('recipe 정렬: created 내림차순', () => { | ||
| writeRecipe(projectDir, 'old.md', { slug: 'old', recipe: 'old', created: '2026-01-01' }); | ||
| writeRecipe(projectDir, 'mid.md', { slug: 'mid', recipe: 'mid', created: '2026-03-01' }); | ||
| writeRecipe(projectDir, 'new.md', { slug: 'new', recipe: 'new', created: '2026-05-01' }); | ||
| const idx = loadCurationIndex(projectDir); | ||
| expect(idx.recipes.map((r) => r.slug)).toEqual(['new', 'mid', 'old']); | ||
| }); | ||
| it('limit 적용', () => { | ||
| for (let i = 0; i < 10; i++) { | ||
| writeRecipe(projectDir, `r${i}.md`, { | ||
| slug: `r${i}`, | ||
| recipe: `recipe ${i}`, | ||
| created: `2026-05-${String(i + 1).padStart(2, '0')}`, | ||
| }); | ||
| } | ||
| const idx = loadCurationIndex(projectDir, { recipeLimit: 3 }); | ||
| expect(idx.recipes).toHaveLength(3); | ||
| expect(idx.recipes[0].slug).toBe('r9'); | ||
| }); | ||
| it('anti-pattern 로드 — tag + suggested-stop', () => { | ||
| writeAntiPattern(projectDir, 'a.md', { | ||
| slug: 'nullability__src__20260507', | ||
| type: 'anti-pattern', | ||
| 'root-cause-tag': 'nullability', | ||
| 'trigger-signature': '(file=src/x.ts, category=nullability)', | ||
| 'suggested-stop': '같은 위치 null 처리 반복 — 타입 가드 필요', | ||
| created: '2026-05-07', | ||
| }); | ||
| const idx = loadCurationIndex(projectDir); | ||
| expect(idx.antiPatterns).toHaveLength(1); | ||
| expect(idx.antiPatterns[0].tag).toBe('nullability'); | ||
| expect(idx.antiPatterns[0].summary).toBe('같은 위치 null 처리 반복 — 타입 가드 필요'); | ||
| }); | ||
| it('frontmatter 손상 파일은 스킵', () => { | ||
| writeRecipe(projectDir, 'good.md', { slug: 'good', recipe: 'works', created: '2026-05-07' }); | ||
| fs.writeFileSync(path.join(projectDir, '.vibe', 'recipes', 'broken.md'), 'no frontmatter'); | ||
| fs.writeFileSync(path.join(projectDir, '.vibe', 'recipes', 'no-slug.md'), '---\ntype: recipe\n---\n'); | ||
| const idx = loadCurationIndex(projectDir); | ||
| expect(idx.recipes).toHaveLength(1); | ||
| expect(idx.recipes[0].slug).toBe('good'); | ||
| }); | ||
| it('README.md, _cluster*.md 등 메타 파일 제외', () => { | ||
| writeRecipe(projectDir, 'r.md', { slug: 'r', recipe: 'good', created: '2026-05-07' }); | ||
| fs.writeFileSync(path.join(projectDir, '.vibe', 'recipes', 'README.md'), '---\nslug: should-skip\n---\n'); | ||
| fs.writeFileSync(path.join(projectDir, '.vibe', 'recipes', '_cluster-x.md'), '---\nslug: should-skip\n---\n'); | ||
| const idx = loadCurationIndex(projectDir); | ||
| expect(idx.recipes.map((r) => r.slug)).toEqual(['r']); | ||
| }); | ||
| it('recipe + anti-pattern 동시 로드 (독립)', () => { | ||
| writeRecipe(projectDir, 'r.md', { slug: 'r', recipe: 'do this', created: '2026-05-07' }); | ||
| writeAntiPattern(projectDir, 'a.md', { | ||
| slug: 'a', | ||
| 'root-cause-tag': 'auth', | ||
| 'suggested-stop': 'check tokens', | ||
| created: '2026-05-07', | ||
| }); | ||
| const idx = loadCurationIndex(projectDir); | ||
| expect(idx.recipes).toHaveLength(1); | ||
| expect(idx.antiPatterns).toHaveLength(1); | ||
| }); | ||
| }); |
| /** | ||
| * recipe-extractor.js — Phase 3 테스트 | ||
| * | ||
| * 검증 범위: | ||
| * - 휴리스틱 게이트: total_tools≥8 AND fails≥3 AND last=success | ||
| * - VIBE_RECIPE_LLM=mock 으로 LLM 우회 (외부 의존 0) | ||
| * - frontmatter schema 충족 | ||
| * - silent fail 정책 (jsonl 없음 / 게이트 실패 / 디렉토리 부재) | ||
| * | ||
| * 외부 의존: | ||
| * - 실제 claude CLI 는 호출하지 않음 (mock 모드 사용) | ||
| */ | ||
| import { describe, it, expect, beforeEach } from 'vitest'; | ||
| import { spawnSync } from 'child_process'; | ||
| import fs from 'fs'; | ||
| import os from 'os'; | ||
| import path from 'path'; | ||
| import { fileURLToPath } from 'url'; | ||
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | ||
| const SCRIPT = path.resolve(__dirname, '..', 'recipe-extractor.js'); | ||
| function makeTmpProject() { | ||
| return fs.mkdtempSync(path.join(os.tmpdir(), 'vibe-recipe-extractor-')); | ||
| } | ||
| function writeJsonl(projectDir, records) { | ||
| const dir = path.join(projectDir, '.vibe', 'metrics'); | ||
| fs.mkdirSync(dir, { recursive: true }); | ||
| const lines = records.map((r) => JSON.stringify(r)).join('\n') + '\n'; | ||
| fs.writeFileSync(path.join(dir, 'current-run.jsonl'), lines); | ||
| } | ||
| function writeRunMeta(projectDir, meta) { | ||
| const dir = path.join(projectDir, '.vibe', 'metrics'); | ||
| fs.mkdirSync(dir, { recursive: true }); | ||
| fs.writeFileSync(path.join(dir, 'current-run.json'), JSON.stringify(meta)); | ||
| } | ||
| function runExtractor(projectDir, { mock = true } = {}) { | ||
| const result = spawnSync('node', [SCRIPT], { | ||
| encoding: 'utf-8', | ||
| timeout: 5000, | ||
| env: { | ||
| ...process.env, | ||
| CLAUDE_PROJECT_DIR: projectDir, | ||
| ...(mock ? { VIBE_RECIPE_LLM: 'mock' } : {}), | ||
| VIBE_HOOK_DEPTH: '0', | ||
| }, | ||
| }); | ||
| return result; | ||
| } | ||
| function listRecipes(projectDir) { | ||
| const dir = path.join(projectDir, '.vibe', 'recipes'); | ||
| if (!fs.existsSync(dir)) return []; | ||
| return fs.readdirSync(dir).filter((f) => f.endsWith('.md')); | ||
| } | ||
| function makeRecord({ tool = 'Bash', ok = true, file = null, category = null }) { | ||
| return { | ||
| ts: new Date().toISOString(), | ||
| tool, | ||
| ok, | ||
| target_file: file, | ||
| error_category: ok ? null : category, | ||
| }; | ||
| } | ||
| describe('recipe-extractor (Phase 3)', () => { | ||
| let projectDir; | ||
| beforeEach(() => { | ||
| projectDir = makeTmpProject(); | ||
| }); | ||
| // ───────── 게이트 통과 조건 ───────── | ||
| describe('휴리스틱 게이트', () => { | ||
| it('총 ≥8 + 실패 ≥3 + 마지막 성공 → recipe 생성', () => { | ||
| const records = [ | ||
| makeRecord({ ok: false, category: 'integration' }), | ||
| makeRecord({ ok: false, category: 'integration' }), | ||
| makeRecord({ ok: false, category: 'auth' }), | ||
| makeRecord({ ok: true }), | ||
| makeRecord({ ok: true }), | ||
| makeRecord({ ok: true, tool: 'Edit', file: 'src/foo.ts' }), | ||
| makeRecord({ ok: true, tool: 'Read', file: 'src/foo.ts' }), | ||
| makeRecord({ ok: true, tool: 'Bash' }), // 마지막=성공 | ||
| ]; | ||
| writeJsonl(projectDir, records); | ||
| writeRunMeta(projectDir, { feature: 'auth-fix', startedAt: '2026-05-07T00:00:00.000Z', steps: 8 }); | ||
| const r = runExtractor(projectDir); | ||
| expect(r.status).toBe(0); | ||
| expect(listRecipes(projectDir)).toHaveLength(1); | ||
| }); | ||
| it('툴콜 부족 (<8) → 생성 안 함', () => { | ||
| const records = [ | ||
| makeRecord({ ok: false, category: 'integration' }), | ||
| makeRecord({ ok: false, category: 'integration' }), | ||
| makeRecord({ ok: false, category: 'integration' }), | ||
| makeRecord({ ok: true }), | ||
| ]; | ||
| writeJsonl(projectDir, records); | ||
| writeRunMeta(projectDir, { feature: 'x' }); | ||
| runExtractor(projectDir); | ||
| expect(listRecipes(projectDir)).toHaveLength(0); | ||
| }); | ||
| it('실패 부족 (<3) → 생성 안 함', () => { | ||
| const records = Array.from({ length: 10 }, () => makeRecord({ ok: true })); | ||
| records[0] = makeRecord({ ok: false, category: 'other' }); | ||
| records[1] = makeRecord({ ok: false, category: 'other' }); | ||
| writeJsonl(projectDir, records); | ||
| writeRunMeta(projectDir, { feature: 'x' }); | ||
| runExtractor(projectDir); | ||
| expect(listRecipes(projectDir)).toHaveLength(0); | ||
| }); | ||
| it('마지막 호출이 실패 → 생성 안 함 (task 미완료)', () => { | ||
| const records = [ | ||
| ...Array.from({ length: 5 }, () => makeRecord({ ok: true })), | ||
| makeRecord({ ok: false, category: 'integration' }), | ||
| makeRecord({ ok: false, category: 'integration' }), | ||
| makeRecord({ ok: false, category: 'integration' }), | ||
| ]; | ||
| writeJsonl(projectDir, records); | ||
| writeRunMeta(projectDir, { feature: 'x' }); | ||
| runExtractor(projectDir); | ||
| expect(listRecipes(projectDir)).toHaveLength(0); | ||
| }); | ||
| }); | ||
| // ───────── frontmatter 검증 ───────── | ||
| describe('frontmatter schema', () => { | ||
| function makeQualifyingRun() { | ||
| const records = [ | ||
| makeRecord({ tool: 'Bash', ok: false, category: 'integration' }), | ||
| makeRecord({ tool: 'Bash', ok: false, category: 'integration' }), | ||
| makeRecord({ tool: 'Edit', file: 'src/foo.ts', ok: false, category: 'type-narrow' }), | ||
| makeRecord({ tool: 'Edit', file: 'src/foo.ts', ok: true }), | ||
| makeRecord({ tool: 'Read', file: 'src/foo.ts', ok: true }), | ||
| makeRecord({ tool: 'Bash', ok: true }), | ||
| makeRecord({ tool: 'Bash', ok: true }), | ||
| makeRecord({ tool: 'Bash', ok: true }), | ||
| ]; | ||
| writeJsonl(projectDir, records); | ||
| writeRunMeta(projectDir, { feature: 'login flow', startedAt: '2026-05-07T10:00:00.000Z', steps: 8 }); | ||
| } | ||
| it('생성된 recipe 의 frontmatter 가 schema 충족', () => { | ||
| makeQualifyingRun(); | ||
| runExtractor(projectDir); | ||
| const [filename] = listRecipes(projectDir); | ||
| const content = fs.readFileSync(path.join(projectDir, '.vibe', 'recipes', filename), 'utf-8'); | ||
| expect(content).toMatch(/^---\n/); | ||
| expect(content).toMatch(/^slug: login-flow__\d{8}-\d{6}$/m); | ||
| expect(content).toMatch(/^type: recipe$/m); | ||
| expect(content).toMatch(/^symptom-context: "login flow"$/m); | ||
| expect(content).toMatch(/^recipe: ".+"$/m); | ||
| expect(content).toMatch(/^tools-touched: \[/m); | ||
| expect(content).toMatch(/^retry-count-saved: 3$/m); | ||
| expect(content).toMatch(/^created: \d{4}-\d{2}-\d{2}$/m); | ||
| expect(content).toMatch(/^source-run: "2026-05-07T10:00:00\.000Z"$/m); | ||
| expect(content).toMatch(/^confidence: low$/m); | ||
| }); | ||
| it('파일명이 timestamp slug 포함', () => { | ||
| makeQualifyingRun(); | ||
| runExtractor(projectDir); | ||
| const [filename] = listRecipes(projectDir); | ||
| expect(filename).toMatch(/^login-flow__\d{8}-\d{6}\.md$/); | ||
| }); | ||
| it('feature 누락 시 anon slug', () => { | ||
| const records = Array.from({ length: 8 }, (_, i) => makeRecord({ | ||
| ok: i < 3 ? false : true, | ||
| category: i < 3 ? 'integration' : null, | ||
| })); | ||
| writeJsonl(projectDir, records); | ||
| writeRunMeta(projectDir, { feature: null }); | ||
| runExtractor(projectDir); | ||
| const [filename] = listRecipes(projectDir); | ||
| expect(filename).toMatch(/^anon__/); | ||
| }); | ||
| }); | ||
| // ───────── 안전성 ───────── | ||
| describe('silent fail 정책', () => { | ||
| it('jsonl 없음 → exit 0, 파일 생성 안 함', () => { | ||
| const r = runExtractor(projectDir); | ||
| expect(r.status).toBe(0); | ||
| expect(listRecipes(projectDir)).toHaveLength(0); | ||
| }); | ||
| it('빈 jsonl → exit 0, 생성 안 함', () => { | ||
| writeJsonl(projectDir, []); | ||
| const r = runExtractor(projectDir); | ||
| expect(r.status).toBe(0); | ||
| expect(listRecipes(projectDir)).toHaveLength(0); | ||
| }); | ||
| it('VIBE_HOOK_DEPTH≥1 → 재귀 가드, 생성 안 함', () => { | ||
| const records = [ | ||
| ...Array.from({ length: 5 }, () => makeRecord({ ok: false, category: 'integration' })), | ||
| makeRecord({ ok: true }), makeRecord({ ok: true }), makeRecord({ ok: true }), | ||
| ]; | ||
| writeJsonl(projectDir, records); | ||
| writeRunMeta(projectDir, { feature: 'x' }); | ||
| const r = spawnSync('node', [SCRIPT], { | ||
| encoding: 'utf-8', | ||
| timeout: 5000, | ||
| env: { | ||
| ...process.env, | ||
| CLAUDE_PROJECT_DIR: projectDir, | ||
| VIBE_RECIPE_LLM: 'mock', | ||
| VIBE_HOOK_DEPTH: '1', | ||
| }, | ||
| }); | ||
| expect(r.status).toBe(0); | ||
| expect(listRecipes(projectDir)).toHaveLength(0); | ||
| }); | ||
| it('손상된 jsonl 라인은 무시', () => { | ||
| const dir = path.join(projectDir, '.vibe', 'metrics'); | ||
| fs.mkdirSync(dir, { recursive: true }); | ||
| // 일부 라인 손상 + 8개 정상 | ||
| const good = Array.from({ length: 8 }, (_, i) => JSON.stringify(makeRecord({ | ||
| ok: i < 3 ? false : true, | ||
| category: i < 3 ? 'integration' : null, | ||
| }))); | ||
| const lines = ['{ broken', ...good].join('\n') + '\n'; | ||
| fs.writeFileSync(path.join(dir, 'current-run.jsonl'), lines); | ||
| writeRunMeta(projectDir, { feature: 'x' }); | ||
| const r = runExtractor(projectDir); | ||
| expect(r.status).toBe(0); | ||
| expect(listRecipes(projectDir)).toHaveLength(1); | ||
| }); | ||
| }); | ||
| }); |
| /** | ||
| * step-counter.js — Phase 1 + Phase 2 테스트 | ||
| * | ||
| * 검증 범위: | ||
| * - 책임 1) current-run.json steps 증가 (회귀 테스트) | ||
| * - 책임 2) current-run.jsonl append (Phase 1) | ||
| * - 책임 3) error_category 분류기 + 3-fail detector → anti-pattern md (Phase 2) | ||
| * - 책임 간 독립성, hot-path 안정성 | ||
| * | ||
| * 패턴: keyword-detector.test.js 와 동일한 execFileSync 방식. | ||
| */ | ||
| import { describe, it, expect, beforeEach } from 'vitest'; | ||
| import { spawnSync } from 'child_process'; | ||
| import fs from 'fs'; | ||
| import os from 'os'; | ||
| import path from 'path'; | ||
| import { fileURLToPath } from 'url'; | ||
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | ||
| const SCRIPT = path.resolve(__dirname, '..', 'step-counter.js'); | ||
| /** | ||
| * 격리된 임시 프로젝트에서 step-counter 를 1회 실행. | ||
| * stdin 으로 PostToolUse payload 전달. | ||
| */ | ||
| function runCounter({ payload, projectDir }) { | ||
| const result = spawnSync('node', [SCRIPT], { | ||
| input: payload ? JSON.stringify(payload) : '', | ||
| encoding: 'utf-8', | ||
| timeout: 5000, | ||
| env: { | ||
| ...process.env, | ||
| CLAUDE_PROJECT_DIR: projectDir, | ||
| // VIBE_HOOK_DEPTH 미설정 — 재귀 가드 영향 없음 | ||
| }, | ||
| }); | ||
| return result; | ||
| } | ||
| function makeTmpProject() { | ||
| const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vibe-step-counter-')); | ||
| // .vibe/metrics/ 는 step-counter 가 직접 생성하도록 미리 만들지 않음 | ||
| return dir; | ||
| } | ||
| function readJson(p) { | ||
| return JSON.parse(fs.readFileSync(p, 'utf-8')); | ||
| } | ||
| function readJsonl(p) { | ||
| return fs.readFileSync(p, 'utf-8').split('\n').filter(Boolean).map((l) => JSON.parse(l)); | ||
| } | ||
| describe('step-counter PostToolUse hook', () => { | ||
| let projectDir; | ||
| let runJson; | ||
| let runJsonl; | ||
| beforeEach(() => { | ||
| projectDir = makeTmpProject(); | ||
| runJson = path.join(projectDir, '.vibe', 'metrics', 'current-run.json'); | ||
| runJsonl = path.join(projectDir, '.vibe', 'metrics', 'current-run.jsonl'); | ||
| }); | ||
| // ───────── 책임 1 회귀 ───────── | ||
| describe('책임 1: current-run.json 카운터', () => { | ||
| it('첫 호출 시 steps=1, startedAt 채움', () => { | ||
| const r = runCounter({ | ||
| payload: { tool_name: 'Bash', tool_input: { command: 'ls' }, tool_response: { is_error: false } }, | ||
| projectDir, | ||
| }); | ||
| expect(r.status).toBe(0); | ||
| const data = readJson(runJson); | ||
| expect(data.steps).toBe(1); | ||
| expect(data.startedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/); | ||
| }); | ||
| it('연속 호출 시 steps 누적', () => { | ||
| for (let i = 0; i < 3; i++) { | ||
| runCounter({ | ||
| payload: { tool_name: 'Bash', tool_input: { command: 'echo hi' }, tool_response: {} }, | ||
| projectDir, | ||
| }); | ||
| } | ||
| const data = readJson(runJson); | ||
| expect(data.steps).toBe(3); | ||
| }); | ||
| it('손상된 JSON 이 있어도 새로 시작', () => { | ||
| fs.mkdirSync(path.join(projectDir, '.vibe', 'metrics'), { recursive: true }); | ||
| fs.writeFileSync(runJson, '{ broken json'); | ||
| const r = runCounter({ | ||
| payload: { tool_name: 'Bash', tool_input: {}, tool_response: {} }, | ||
| projectDir, | ||
| }); | ||
| expect(r.status).toBe(0); | ||
| const data = readJson(runJson); | ||
| expect(data.steps).toBe(1); | ||
| }); | ||
| }); | ||
| // ───────── 책임 2 신규 ───────── | ||
| describe('책임 2: current-run.jsonl 로깅', () => { | ||
| it('툴콜 1회 = jsonl 1라인', () => { | ||
| runCounter({ | ||
| payload: { tool_name: 'Edit', tool_input: { file_path: 'src/foo.ts' }, tool_response: {} }, | ||
| projectDir, | ||
| }); | ||
| const lines = readJsonl(runJsonl); | ||
| expect(lines).toHaveLength(1); | ||
| expect(lines[0].tool).toBe('Edit'); | ||
| expect(lines[0].ok).toBe(true); | ||
| expect(lines[0].target_file).toBe('src/foo.ts'); | ||
| expect(lines[0].error_category).toBeNull(); | ||
| expect(lines[0].ts).toMatch(/^\d{4}-\d{2}-\d{2}T/); | ||
| }); | ||
| it('tool_response.is_error=true 면 ok=false', () => { | ||
| runCounter({ | ||
| payload: { tool_name: 'Bash', tool_input: { command: 'false' }, tool_response: { is_error: true } }, | ||
| projectDir, | ||
| }); | ||
| const [line] = readJsonl(runJsonl); | ||
| expect(line.ok).toBe(false); | ||
| }); | ||
| it('절대 경로 file_path 를 프로젝트 상대 경로로 정규화', () => { | ||
| const abs = path.join(projectDir, 'src', 'bar.ts'); | ||
| runCounter({ | ||
| payload: { tool_name: 'Write', tool_input: { file_path: abs }, tool_response: {} }, | ||
| projectDir, | ||
| }); | ||
| const [line] = readJsonl(runJsonl); | ||
| expect(line.target_file).toBe('src/bar.ts'); | ||
| }); | ||
| it('file_path 없는 툴콜은 target_file=null', () => { | ||
| runCounter({ | ||
| payload: { tool_name: 'Bash', tool_input: { command: 'pwd' }, tool_response: {} }, | ||
| projectDir, | ||
| }); | ||
| const [line] = readJsonl(runJsonl); | ||
| expect(line.target_file).toBeNull(); | ||
| }); | ||
| it('tool_name 없으면 jsonl 라인 안 씀 (steps 는 증가)', () => { | ||
| runCounter({ payload: {}, projectDir }); | ||
| expect(fs.existsSync(runJsonl)).toBe(false); | ||
| expect(readJson(runJson).steps).toBe(1); | ||
| }); | ||
| it('연속 호출 시 jsonl 누적', () => { | ||
| runCounter({ | ||
| payload: { tool_name: 'Read', tool_input: { file_path: 'a.ts' }, tool_response: {} }, | ||
| projectDir, | ||
| }); | ||
| runCounter({ | ||
| payload: { tool_name: 'Edit', tool_input: { file_path: 'a.ts' }, tool_response: {} }, | ||
| projectDir, | ||
| }); | ||
| const lines = readJsonl(runJsonl); | ||
| expect(lines).toHaveLength(2); | ||
| expect(lines.map((l) => l.tool)).toEqual(['Read', 'Edit']); | ||
| }); | ||
| }); | ||
| // ───────── 독립성 ───────── | ||
| describe('두 책임의 독립성', () => { | ||
| it('payload 가 비어 stdin 무효여도 카운터는 증가', () => { | ||
| const r = runCounter({ payload: null, projectDir }); | ||
| expect(r.status).toBe(0); | ||
| expect(readJson(runJson).steps).toBe(1); | ||
| }); | ||
| }); | ||
| // ───────── Phase 2: error_category 분류 ───────── | ||
| describe('책임 3a: error_category 분류기', () => { | ||
| it('TypeError of undefined → nullability', () => { | ||
| runCounter({ | ||
| payload: { | ||
| tool_name: 'Bash', | ||
| tool_input: { command: 'node x.js' }, | ||
| tool_response: { is_error: true, error: "TypeError: Cannot read properties of undefined (reading 'foo')" }, | ||
| }, | ||
| projectDir, | ||
| }); | ||
| const [line] = readJsonl(runJsonl); | ||
| expect(line.error_category).toBe('nullability'); | ||
| }); | ||
| it('TS2345 → type-narrow', () => { | ||
| runCounter({ | ||
| payload: { | ||
| tool_name: 'Bash', | ||
| tool_input: { command: 'tsc' }, | ||
| tool_response: { is_error: true, error: "src/x.ts(3,4): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'." }, | ||
| }, | ||
| projectDir, | ||
| }); | ||
| const [line] = readJsonl(runJsonl); | ||
| expect(line.error_category).toBe('type-narrow'); | ||
| }); | ||
| it('ECONNREFUSED → integration', () => { | ||
| runCounter({ | ||
| payload: { | ||
| tool_name: 'Bash', | ||
| tool_input: { command: 'curl' }, | ||
| tool_response: { is_error: true, error: 'connect ECONNREFUSED 127.0.0.1:5432' }, | ||
| }, | ||
| projectDir, | ||
| }); | ||
| const [line] = readJsonl(runJsonl); | ||
| expect(line.error_category).toBe('integration'); | ||
| }); | ||
| it('알 수 없는 에러 → other', () => { | ||
| runCounter({ | ||
| payload: { | ||
| tool_name: 'Bash', | ||
| tool_input: { command: 'x' }, | ||
| tool_response: { is_error: true, error: 'some unrecognized failure mode 12345' }, | ||
| }, | ||
| projectDir, | ||
| }); | ||
| const [line] = readJsonl(runJsonl); | ||
| expect(line.error_category).toBe('other'); | ||
| }); | ||
| it('성공 툴콜은 error_category=null', () => { | ||
| runCounter({ | ||
| payload: { tool_name: 'Bash', tool_input: { command: 'ls' }, tool_response: { is_error: false } }, | ||
| projectDir, | ||
| }); | ||
| const [line] = readJsonl(runJsonl); | ||
| expect(line.ok).toBe(true); | ||
| expect(line.error_category).toBeNull(); | ||
| }); | ||
| }); | ||
| // ───────── Phase 2: 3-fail detector ───────── | ||
| describe('책임 3b: 3-fail detector → anti-pattern md', () => { | ||
| function failBash(command, errorText, projectDir) { | ||
| runCounter({ | ||
| payload: { | ||
| tool_name: 'Bash', | ||
| tool_input: { command }, | ||
| tool_response: { is_error: true, error: errorText }, | ||
| }, | ||
| projectDir, | ||
| }); | ||
| } | ||
| function listAntiPatterns(projectDir) { | ||
| const dir = path.join(projectDir, '.vibe', 'anti-patterns'); | ||
| if (!fs.existsSync(dir)) return []; | ||
| return fs.readdirSync(dir).filter((f) => f.endsWith('.md')); | ||
| } | ||
| it('같은 (file, category) 3회 누적 시 anti-pattern md 생성', () => { | ||
| const err = "TypeError: Cannot read properties of undefined (reading 'x')"; | ||
| // file_path 없는 Bash 실패 3회 → target_file=null, category=nullability | ||
| for (let i = 0; i < 3; i++) failBash(`run-${i}`, err, projectDir); | ||
| const files = listAntiPatterns(projectDir); | ||
| expect(files).toHaveLength(1); | ||
| expect(files[0]).toMatch(/^nullability__global__\d{8}\.md$/); | ||
| }); | ||
| it('2회만 누적되면 생성 안 함', () => { | ||
| const err = 'connect ECONNREFUSED foo'; | ||
| for (let i = 0; i < 2; i++) failBash(`x-${i}`, err, projectDir); | ||
| expect(listAntiPatterns(projectDir)).toHaveLength(0); | ||
| }); | ||
| it('3회지만 카테고리 다르면 생성 안 함', () => { | ||
| failBash('a', "Cannot read properties of undefined", projectDir); // nullability | ||
| failBash('b', "ECONNREFUSED", projectDir); // integration | ||
| failBash('c', "TS2345 not assignable", projectDir); // type-narrow | ||
| expect(listAntiPatterns(projectDir)).toHaveLength(0); | ||
| }); | ||
| it('동일 (file, category) 4회 — md 1개만 (dedup)', () => { | ||
| const err = "TypeError: Cannot read properties of undefined"; | ||
| for (let i = 0; i < 4; i++) failBash(`r-${i}`, err, projectDir); | ||
| expect(listAntiPatterns(projectDir)).toHaveLength(1); | ||
| }); | ||
| it('frontmatter 가 schema 충족', () => { | ||
| const err = "TypeError: Cannot read properties of null"; | ||
| for (let i = 0; i < 3; i++) failBash(`r-${i}`, err, projectDir); | ||
| const [filename] = listAntiPatterns(projectDir); | ||
| const content = fs.readFileSync(path.join(projectDir, '.vibe', 'anti-patterns', filename), 'utf-8'); | ||
| expect(content).toMatch(/^---\n/); | ||
| expect(content).toMatch(/^slug: nullability__global__\d{8}$/m); | ||
| expect(content).toMatch(/^type: anti-pattern$/m); | ||
| expect(content).toMatch(/^root-cause-tag: nullability$/m); | ||
| expect(content).toMatch(/^trigger-signature: "/m); | ||
| expect(content).toMatch(/^fail-count: 3$/m); | ||
| expect(content).toMatch(/^suggested-stop: "/m); | ||
| expect(content).toMatch(/^created: \d{4}-\d{2}-\d{2}$/m); | ||
| }); | ||
| it('파일 경로 있는 실패는 file slug 사용', () => { | ||
| const filePath = 'src/cli/foo.ts'; | ||
| const err = "TypeError: Cannot read properties of undefined"; | ||
| for (let i = 0; i < 3; i++) { | ||
| runCounter({ | ||
| payload: { | ||
| tool_name: 'Edit', | ||
| tool_input: { file_path: filePath }, | ||
| tool_response: { is_error: true, error: err }, | ||
| }, | ||
| projectDir, | ||
| }); | ||
| } | ||
| const [filename] = listAntiPatterns(projectDir); | ||
| expect(filename).toMatch(/^nullability__src-cli-foo-ts__\d{8}\.md$/); | ||
| }); | ||
| it('윈도우 외 실패는 카운트 안 됨', () => { | ||
| const err = "TypeError: Cannot read properties of undefined"; | ||
| // 같은 카테고리 실패 2회 | ||
| failBash('a', err, projectDir); | ||
| failBash('b', err, projectDir); | ||
| // 성공 툴콜로 윈도우 채움 (10줄 이상) | ||
| for (let i = 0; i < 10; i++) { | ||
| runCounter({ | ||
| payload: { tool_name: 'Read', tool_input: { file_path: `f${i}.ts` }, tool_response: {} }, | ||
| projectDir, | ||
| }); | ||
| } | ||
| // 마지막 실패 1회 — 윈도우(10줄) 안에는 이 실패 + 직전 성공만 있으므로 트립 안 함 | ||
| failBash('c', err, projectDir); | ||
| expect(listAntiPatterns(projectDir)).toHaveLength(0); | ||
| }); | ||
| }); | ||
| // ───────── 차단 금지 ───────── | ||
| describe('hot path 안정성', () => { | ||
| it('항상 exit 0', () => { | ||
| const r = runCounter({ | ||
| payload: { tool_name: 'Bash', tool_input: { command: 'x' }, tool_response: {} }, | ||
| projectDir, | ||
| }); | ||
| expect(r.status).toBe(0); | ||
| }); | ||
| it('잘못된 stdin JSON 도 차단 안 함', () => { | ||
| const r = spawnSync('node', [SCRIPT], { | ||
| input: 'not json at all', | ||
| encoding: 'utf-8', | ||
| timeout: 5000, | ||
| env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir }, | ||
| }); | ||
| expect(r.status).toBe(0); | ||
| }); | ||
| }); | ||
| }); |
| /** | ||
| * Phase 3 — Curation index loader | ||
| * | ||
| * `.vibe/recipes/*.md` 와 `.vibe/anti-patterns/*.md` 의 frontmatter 만 parse 해 | ||
| * 1줄 요약 인덱스를 만든다. 본문은 읽지 않음 (세션 컨텍스트 절약). | ||
| * | ||
| * SPEC 결정: | ||
| * - INDEX.jsonl 미사용. 디렉토리 스캔 + frontmatter parse 가 충분히 빠르다 (<100 파일). | ||
| * - 최근 N=5 상한 (created 내림차순). | ||
| * | ||
| * 의도적 제한: | ||
| * - 본격 YAML parser 의존성 추가 거부. 우리가 *직접 작성*한 frontmatter 만 | ||
| * 읽으므로 문법이 정해져 있다. 라인별 정규식이면 충분. | ||
| */ | ||
| import fs from 'fs'; | ||
| import path from 'path'; | ||
| import { projectVibeRoot } from '../utils.js'; | ||
| const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/; | ||
| // "key: value" 또는 'key: "quoted value"' (이스케이프 \\" 포함) | ||
| const FIELD_RE = /^([a-z][a-z0-9_-]*):\s*(?:"((?:[^"\\]|\\.)*)"|(.+?))\s*$/; | ||
| function parseFrontmatter(content) { | ||
| const m = FRONTMATTER_RE.exec(content); | ||
| if (!m) return null; | ||
| const fields = {}; | ||
| for (const line of m[1].split('\n')) { | ||
| const fm = FIELD_RE.exec(line); | ||
| if (!fm) continue; | ||
| const key = fm[1]; | ||
| const value = fm[2] !== undefined ? fm[2].replace(/\\"/g, '"') : fm[3]; | ||
| fields[key] = value; | ||
| } | ||
| return fields; | ||
| } | ||
| function readHead(filePath, bytes = 2048) { | ||
| const fd = fs.openSync(filePath, 'r'); | ||
| try { | ||
| const buf = Buffer.alloc(bytes); | ||
| const n = fs.readSync(fd, buf, 0, bytes, 0); | ||
| return buf.toString('utf-8', 0, n); | ||
| } finally { | ||
| fs.closeSync(fd); | ||
| } | ||
| } | ||
| function listMd(dir) { | ||
| if (!fs.existsSync(dir)) return []; | ||
| return fs.readdirSync(dir) | ||
| .filter((f) => f.endsWith('.md') && !f.startsWith('_') && f !== 'README.md') | ||
| .map((f) => path.join(dir, f)); | ||
| } | ||
| function safeParse(filePath) { | ||
| try { | ||
| const head = readHead(filePath); | ||
| const fields = parseFrontmatter(head); | ||
| if (!fields || !fields.slug) return null; | ||
| return fields; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| function compareCreatedDesc(a, b) { | ||
| // created 가 ISO 면 문자열 비교로 시간 정렬 가능, 아니면 mtime fallback | ||
| return (b.created || '').localeCompare(a.created || ''); | ||
| } | ||
| /** | ||
| * 프로젝트의 recipes + anti-patterns 인덱스를 로드. | ||
| * @returns { recipes: [{slug, summary}], antiPatterns: [{tag, summary}] } | ||
| */ | ||
| export function loadCurationIndex(projectDir, opts = {}) { | ||
| const { recipeLimit = 5, antiPatternLimit = 5 } = opts; | ||
| const root = projectVibeRoot(projectDir); | ||
| const recipes = listMd(path.join(root, 'recipes')) | ||
| .map(safeParse).filter(Boolean) | ||
| .sort(compareCreatedDesc) | ||
| .slice(0, recipeLimit) | ||
| .map((f) => ({ | ||
| slug: f.slug, | ||
| summary: f.recipe || f['symptom-context'] || '(no summary)', | ||
| })); | ||
| const antiPatterns = listMd(path.join(root, 'anti-patterns')) | ||
| .map(safeParse).filter(Boolean) | ||
| .sort(compareCreatedDesc) | ||
| .slice(0, antiPatternLimit) | ||
| .map((f) => ({ | ||
| tag: f['root-cause-tag'] || 'other', | ||
| summary: f['suggested-stop'] || f['trigger-signature'] || '(no summary)', | ||
| })); | ||
| return { recipes, antiPatterns }; | ||
| } | ||
| /** Test-only: frontmatter parser 노출 */ | ||
| export const _internal = { parseFrontmatter }; |
| #!/usr/bin/env node | ||
| /** | ||
| * Phase 3 — Recipe extractor (post-task curation) | ||
| * | ||
| * `/vibe.verify` 마지막 단계에서 호출됨. PostToolUse hot-path 가 아니라 | ||
| * 명시적 1회 호출이므로 LLM 호출 허용. | ||
| * | ||
| * 동작: | ||
| * 1) `.vibe/metrics/current-run.jsonl` 읽기 (Phase 1 의 산출) | ||
| * 2) 휴리스틱 게이트: total_tools ≥ 8 AND fail_count ≥ 3 (= 어렵게 푼 task) | ||
| * 3) Haiku 로 3줄 요약 (When/Recipe/Anti-tip) | ||
| * 4) `.vibe/recipes/<slug>.md` 작성 (frontmatter 강제) | ||
| * | ||
| * 안전성: | ||
| * - 모든 실패 silent — recipe 가 없는 게 잘못된 recipe 보다 낫다. | ||
| * - LLM 호출 재귀 가드: VIBE_HOOK_DEPTH (다른 hook 와 동일 패턴). | ||
| * - 테스트 모드: VIBE_RECIPE_LLM=mock 이면 LLM 스킵, 결정적 stub 사용. | ||
| * | ||
| * 사용: | ||
| * node hooks/scripts/recipe-extractor.js | ||
| */ | ||
| import fs from 'fs'; | ||
| import path from 'path'; | ||
| import { spawnSync } from 'child_process'; | ||
| import { PROJECT_DIR, projectVibePath, projectVibeRoot } from './utils.js'; | ||
| const METRICS_DIR = projectVibePath(PROJECT_DIR, 'metrics'); | ||
| const CURRENT_RUN_JSONL = path.join(METRICS_DIR, 'current-run.jsonl'); | ||
| const CURRENT_RUN_JSON = path.join(METRICS_DIR, 'current-run.json'); | ||
| // 휴리스틱 게이트 — SPEC 의 "Risk #1: LLM 호출 비용" 대응 | ||
| const MIN_TOOLS = 8; | ||
| const MIN_FAILS = 3; | ||
| const HAIKU_MODEL = 'claude-haiku-4-5-20251001'; | ||
| // ───────────────────────────────────────────────────── | ||
| // 재귀 가드 | ||
| // ───────────────────────────────────────────────────── | ||
| function isRecursive() { | ||
| const depth = parseInt(process.env.VIBE_HOOK_DEPTH || '0', 10); | ||
| return depth >= 1; | ||
| } | ||
| // ───────────────────────────────────────────────────── | ||
| // jsonl 로드 + 게이트 평가 | ||
| // ───────────────────────────────────────────────────── | ||
| function loadJsonl() { | ||
| if (!fs.existsSync(CURRENT_RUN_JSONL)) return []; | ||
| return fs.readFileSync(CURRENT_RUN_JSONL, 'utf-8') | ||
| .split('\n').filter(Boolean) | ||
| .map((l) => { try { return JSON.parse(l); } catch { return null; } }) | ||
| .filter(Boolean); | ||
| } | ||
| function loadCurrentRunMeta() { | ||
| try { return JSON.parse(fs.readFileSync(CURRENT_RUN_JSON, 'utf-8')); } | ||
| catch { return { feature: null, startedAt: null, steps: 0 }; } | ||
| } | ||
| function evaluateGate(records) { | ||
| const total = records.length; | ||
| const fails = records.filter((r) => r.ok === false).length; | ||
| const lastIsSuccess = records.length > 0 && records[records.length - 1].ok === true; | ||
| const passes = total >= MIN_TOOLS && fails >= MIN_FAILS && lastIsSuccess; | ||
| return { passes, total, fails }; | ||
| } | ||
| // ───────────────────────────────────────────────────── | ||
| // LLM 프롬프트 빌드 | ||
| // ───────────────────────────────────────────────────── | ||
| function buildPrompt(records, meta) { | ||
| const tools = [...new Set(records.map((r) => r.tool))].join(', '); | ||
| const failsByCategory = {}; | ||
| for (const r of records) { | ||
| if (r.ok === false && r.error_category) { | ||
| failsByCategory[r.error_category] = (failsByCategory[r.error_category] || 0) + 1; | ||
| } | ||
| } | ||
| const failSummary = Object.entries(failsByCategory) | ||
| .map(([cat, n]) => `${cat}×${n}`).join(', ') || 'none'; | ||
| // 최근 12개 호출만 — 토큰 절약 | ||
| const tail = records.slice(-12).map((r, i) => { | ||
| const mark = r.ok ? '✓' : '✗'; | ||
| const cat = r.error_category ? ` [${r.error_category}]` : ''; | ||
| const file = r.target_file ? ` ${r.target_file}` : ''; | ||
| return `${i + 1}. ${mark} ${r.tool}${file}${cat}`; | ||
| }).join('\n'); | ||
| return `You are summarizing a successfully completed task into a 3-line reusable recipe. | ||
| Task feature: ${meta.feature || 'unknown'} | ||
| Tool calls: ${records.length} total, ${failSummary} | ||
| Tools used: ${tools} | ||
| Last 12 calls (chronological): | ||
| ${tail} | ||
| Output EXACTLY 3 lines of plain text, no markdown, no preamble: | ||
| LINE 1 — When to use this recipe (1 sentence, the situation/context) | ||
| LINE 2 — The working approach (1-2 sentences, what actually worked) | ||
| LINE 3 — Anti-tip: one thing to avoid (1 sentence) | ||
| Be specific — name the tools, the file types, the error categories. Do not output anything else.`; | ||
| } | ||
| // ───────────────────────────────────────────────────── | ||
| // claude CLI 호출 — --model 우선, fallback 가능 | ||
| // ───────────────────────────────────────────────────── | ||
| let _modelFlagSupported = null; | ||
| function detectModelFlag() { | ||
| if (_modelFlagSupported !== null) return _modelFlagSupported; | ||
| try { | ||
| const help = spawnSync('claude', ['--help'], { encoding: 'utf-8', timeout: 3000 }); | ||
| const out = (help.stdout || '') + (help.stderr || ''); | ||
| _modelFlagSupported = /--model\b/.test(out); | ||
| } catch { | ||
| _modelFlagSupported = false; | ||
| } | ||
| return _modelFlagSupported; | ||
| } | ||
| function callClaude(prompt) { | ||
| if (process.env.VIBE_RECIPE_LLM === 'mock') { | ||
| // 테스트용 결정적 stub | ||
| return { | ||
| ok: true, | ||
| text: 'When auth + integration retries pile up.\nUse claude CLI with explicit model + retry on transient failures.\nAvoid swallowing stderr; surface error_category for downstream classification.', | ||
| }; | ||
| } | ||
| const args = ['--print', '--dangerously-skip-permissions']; | ||
| if (detectModelFlag()) { | ||
| args.unshift('--model', HAIKU_MODEL); | ||
| } | ||
| const env = { ...process.env, VIBE_HOOK_DEPTH: '1' }; | ||
| try { | ||
| const r = spawnSync('claude', args, { | ||
| input: prompt, | ||
| encoding: 'utf-8', | ||
| timeout: 60_000, | ||
| env, | ||
| }); | ||
| if (r.status !== 0) return { ok: false, text: '' }; | ||
| const text = (r.stdout || '').trim(); | ||
| if (!text) return { ok: false, text: '' }; | ||
| return { ok: true, text }; | ||
| } catch { | ||
| return { ok: false, text: '' }; | ||
| } | ||
| } | ||
| // ───────────────────────────────────────────────────── | ||
| // Recipe md 작성 | ||
| // ───────────────────────────────────────────────────── | ||
| function timestampSlug() { | ||
| const d = new Date(); | ||
| const pad = (n) => String(n).padStart(2, '0'); | ||
| return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`; | ||
| } | ||
| function safeFeature(name) { | ||
| if (!name) return 'anon'; | ||
| return String(name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40) || 'anon'; | ||
| } | ||
| function writeRecipe({ records, meta, summary }) { | ||
| const vibeRoot = projectVibeRoot(PROJECT_DIR); | ||
| const dir = path.join(vibeRoot, 'recipes'); | ||
| if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); | ||
| const slug = `${safeFeature(meta.feature)}__${timestampSlug()}`; | ||
| const file = path.join(dir, `${slug}.md`); | ||
| if (fs.existsSync(file)) return null; // 동시 호출 dedup | ||
| const today = new Date().toISOString().slice(0, 10); | ||
| const tools = [...new Set(records.map((r) => r.tool))]; | ||
| const failCount = records.filter((r) => r.ok === false).length; | ||
| // summary 가 3줄 보장 안 되면 채워서 안전화 | ||
| const lines = summary.split('\n').map((l) => l.trim()).filter(Boolean); | ||
| while (lines.length < 3) lines.push('—'); | ||
| const [whenLine, recipeLine, antiTip] = lines.slice(0, 3); | ||
| const body = | ||
| `--- | ||
| slug: ${slug} | ||
| type: recipe | ||
| symptom-context: "${(meta.feature || 'unknown').replace(/"/g, '\\"')}" | ||
| recipe: "${recipeLine.replace(/"/g, '\\"')}" | ||
| tools-touched: [${tools.map((t) => JSON.stringify(t)).join(', ')}] | ||
| retry-count-saved: ${failCount} | ||
| created: ${today} | ||
| source-run: "${meta.startedAt || 'unknown'}" | ||
| confidence: low | ||
| --- | ||
| # Recipe: ${meta.feature || 'unknown'} | ||
| ## When to use | ||
| ${whenLine} | ||
| ## What worked | ||
| ${recipeLine} | ||
| ## Avoid | ||
| ${antiTip} | ||
| ## Stats | ||
| - Total tool calls: ${records.length} | ||
| - Failures before success: ${failCount} | ||
| - Tools touched: ${tools.join(', ')} | ||
| - Source run started: ${meta.startedAt || 'unknown'} | ||
| > Auto-generated by \`recipe-extractor.js\` from \`.vibe/metrics/current-run.jsonl\`. \ | ||
| Confidence \`low\` — single observation. Increment to \`medium\`/\`high\` when the same recipe is seen multiple times across runs. | ||
| `; | ||
| fs.writeFileSync(file, body); | ||
| return file; | ||
| } | ||
| // ───────────────────────────────────────────────────── | ||
| // 메인 | ||
| // ───────────────────────────────────────────────────── | ||
| function main() { | ||
| if (isRecursive()) return; // claude CLI 가 다시 우리 hook 을 트리거하는 것 방지 | ||
| let records; | ||
| try { records = loadJsonl(); } catch { return; } | ||
| if (records.length === 0) return; | ||
| const gate = evaluateGate(records); | ||
| if (!gate.passes) return; | ||
| const meta = loadCurrentRunMeta(); | ||
| const prompt = buildPrompt(records, meta); | ||
| const llm = callClaude(prompt); | ||
| if (!llm.ok) return; | ||
| try { writeRecipe({ records, meta, summary: llm.text }); } | ||
| catch { /* silent */ } | ||
| } | ||
| try { main(); } catch { /* never throw */ } | ||
| process.exit(0); |
+1
-1
@@ -105,3 +105,3 @@ # VIBE | ||
| **Include**: `.vibe/{plans,specs,features,todos,research,regressions,contracts,config.json,constitution.md}`, `CLAUDE.md` | ||
| **Include**: `.vibe/{plans,specs,features,todos,research,regressions,contracts,recipes,anti-patterns,config.json,constitution.md}`, `CLAUDE.md` | ||
| **Vibe-global (not project-local)**: `~/.vibe/test-reports/` — `/vibe.test` artifacts live with the vibe install, not with the project | ||
@@ -108,0 +108,0 @@ **Exclude**: `~/.claude/{rules,commands,agents,skills}/`, `.claude/settings.local.json`, `.coco/settings.local.json`, `.vibe/{memories,checkpoints,metrics}/` |
@@ -202,2 +202,8 @@ --- | ||
| }catch{}" | ||
| # Phase 3 — Recipe extraction (post-task curation, best-effort silent) | ||
| # 휴리스틱 게이트 (total≥8 AND fails≥3) 통과 시만 LLM 호출. | ||
| # .vibe/recipes/<slug>.md 자동 생성 → 다음 세션의 session-start 가 prepend. | ||
| HOOKS_DIR="${VIBE_PATH:-$(npm root -g 2>/dev/null)/@su-record/vibe}/hooks/scripts" | ||
| [ -f "$HOOKS_DIR/recipe-extractor.js" ] && node "$HOOKS_DIR/recipe-extractor.js" 2>/dev/null || true | ||
| ``` | ||
@@ -204,0 +210,0 @@ |
@@ -278,3 +278,3 @@ /** | ||
| // .vibe/ 표준 하위 디렉토리 (SSOT — Claude/coco 공용) | ||
| for (const sub of ['specs', 'features', 'plans', 'todos', 'memories', 'metrics']) { | ||
| for (const sub of ['specs', 'features', 'plans', 'todos', 'memories', 'metrics', 'recipes', 'anti-patterns']) { | ||
| ensureDir(path.join(coreDir, sub)); | ||
@@ -284,3 +284,3 @@ } | ||
| if (!fs.existsSync(vibeReadme)) { | ||
| fs.writeFileSync(vibeReadme, '# .vibe/\n\nVibe SSOT — Claude Code / coco CLI 공용 프로젝트 에셋.\n\n- `config.json` — 프로젝트 스택/capabilities\n- `constitution.md` — 하드 룰\n- `specs/`, `plans/`, `features/`, `todos/` — 워크플로우 문서\n- `memories/` — 프로젝트 메모리 DB\n- `metrics/` — 세션/스텝 카운터\n- `scope.json` — SPEC 기반 자동 스코프 (auto=true)\n'); | ||
| fs.writeFileSync(vibeReadme, '# .vibe/\n\nVibe SSOT — Claude Code / coco CLI 공용 프로젝트 에셋.\n\n- `config.json` — 프로젝트 스택/capabilities\n- `constitution.md` — 하드 룰\n- `specs/`, `plans/`, `features/`, `todos/` — 워크플로우 문서\n- `memories/` — 프로젝트 메모리 DB\n- `metrics/` — 세션/스텝 카운터\n- `recipes/` — 성공한 task 의 압축 경로 (post-task curation, frontmatter 강제)\n- `anti-patterns/` — 같은 카테고리 3회 실패 시 자동 기록 (frontmatter 강제)\n- `scope.json` — SPEC 기반 자동 스코프 (auto=true)\n'); | ||
| } | ||
@@ -287,0 +287,0 @@ }); |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/cli/commands/init.ts"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/cli/commands/init.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClF,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAClE,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,iBAAiB,EACjB,eAAe,EACf,YAAY,EACZ,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,uBAAuB,EACvB,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,EACtB,sBAAsB,EACtB,2BAA2B,EAC3B,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,CAAC,MAAM,gBAAgB,CAAC;AACpC,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC1F,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CACtC,iBAA2B,EAAE,EAC7B,UAAsB,EAAE,MAAM,EAAE,KAAK,EAAE;IAEvC,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;QAClE,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;QAC9D,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;QAEtD,iEAAiE;QACjE,MAAM,kBAAkB,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC;QACnF,MAAM,mBAAmB,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;QAChE,MAAM,YAAY,GAAG,EAAE,CAAC,UAAU,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,mBAAmB,CAAC;QAElG,kCAAkC;QAClC,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;QACrE,IAAI,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;YAChC,mBAAmB,CAAC,YAAY,EAAE,eAAe,CAAC,CAAC;QACrD,CAAC;QAED,iDAAiD;QACjD,MAAM,sBAAsB,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,gBAAgB,CAAC,CAAC;QACpF,mBAAmB,CAAC,sBAAsB,EAAE,cAAc,EAAE,YAAY,CAAC,CAAC;QAE1E,mCAAmC;QACnC,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;QACrE,oBAAoB,CAAC,eAAe,CAAC,CAAC;IAExC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,wCAAwC;QACxC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YACpB,OAAO,CAAC,IAAI,CAAC,uCAAwC,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QAChF,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,WAAmB,EACnB,UAAoB,EACpB,eAAyB,EAAE,EAC3B,aAAqB,SAAS;IAE9B,MAAM,WAAW,GAAG,kBAAkB,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;IACjE,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAErC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC9D,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAC1D,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IACtD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC;QAAE,OAAO;IAEzC,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;IACpE,kBAAkB,CAAC,YAAY,EAAE,cAAc,EAAE,WAAW,CAAC,CAAC;IAC9D,GAAG,CAAC,iCAAiC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACnE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAClC,WAAmB,EACnB,UAAoB,EACpB,aAAqB,SAAS;IAE9B,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC;IAClF,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC;IAChF,MAAM,kBAAkB,GAAG,EAAE,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,eAAe,CAAC;IAChG,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,kBAAkB,CAAC;QAAE,OAAO;IAE/C,yBAAyB;IACzB,MAAM,aAAa,GAAG,IAAI,GAAG,EAAU,CAAC;IACxC,KAAK,MAAM,KAAK,IAAI,UAAU,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,sBAAsB,CAAC,KAAK,CAAC,CAAC;QAC3C,IAAI,IAAI;YAAE,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACpC,CAAC;IACD,IAAI,aAAa,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO;IAErC,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;IACvE,SAAS,CAAC,iBAAiB,CAAC,CAAC;IAE7B,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;QACjC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,IAAI,CAAC,CAAC;QAChD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,IAAI,CAAC,CAAC;QAChD,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACvB,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAC3B,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvB,CAAC;IACH,CAAC;IAED,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,GAAG,CAAC,mCAAmC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnE,CAAC;AACH,CAAC;AAED;;GAEG;AACH,SAAS,OAAO,CACd,CAAiC,EACjC,IAAY,EACZ,EAAc;IAEd,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAChB,IAAI,CAAC;QACH,EAAE,EAAE,CAAC;IACP,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7D,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,eAAe,GAAG,EAAE,CAAC,CAAC;IAC1C,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,IAAI,CACxB,WAAoB,EACpB,SAA6C,IAAI;IAEjD,IAAI,CAAC;QACH,MAAM,WAAW,GAA0D;YACzE,EAAE,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,aAAa,EAAE;YAC5C,IAAI,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE;YACrC,KAAK,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE;YACxC,MAAM,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE;SAC5C,CAAC;QACF,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,YAAY,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;QAErE,IAAI,WAAW,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;QAChC,IAAI,YAAY,GAAG,KAAK,CAAC;QAEzB,IAAI,WAAW,EAAE,CAAC;YAChB,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,WAAW,CAAC,CAAC;YAEpD,IAAI,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;gBAC/B,GAAG,CAAC,4BAA4B,WAAW,GAAG,CAAC,CAAC;gBAChD,OAAO;YACT,CAAC;YAED,GAAG,CAAC,wBAAwB,WAAW,KAAK,CAAC,CAAC;YAC9C,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/C,YAAY,GAAG,IAAI,CAAC;QACtB,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;QACrD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QAChD,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3B,GAAG,CAAC,0BAA0B,CAAC,CAAC;YAChC,OAAO;QACT,CAAC;QAED,qGAAqG;QACrG,SAAS,CAAC,OAAO,CAAC,CAAC;QACnB,MAAM,YAAY,GAAG,qBAAqB,CAAC,WAAW,CAAC,CAAC;QACxD,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC5B,GAAG,CAAC,gCAAgC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnE,CAAC;QACD,GAAG,CAAC,sBAAsB,YAAY,KAAK,UAAU,eAAe,CAAC,CAAC;QACtE,GAAG,CAAC,6BAA6B,CAAC,CAAC;QAEnC,2DAA2D;QAC3D,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;QACnC,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,CAAC;QAEtB,CAAC,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;QACnC,OAAO,CAAC,CAAC,EAAE,kBAAkB,EAAE,GAAG,EAAE,CAAC,iBAAiB,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;QAC9E,OAAO,CAAC,CAAC,EAAE,qBAAqB,EAAE,GAAG,EAAE,CAAC,eAAe,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC;QAElF,CAAC,CAAC,OAAO,CAAC,uBAAuB,CAAC,CAAC;QACnC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAC;QACxF,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;QAEnC,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9B,GAAG,CAAC,0BAA0B,CAAC,CAAC;YAChC,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE;gBAC1B,GAAG,CAAC,WAAW,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YAChE,CAAC,CAAC,CAAC;YACH,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtC,GAAG,CAAC,eAAe,YAAY,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC5D,CAAC;YACD,IAAI,YAAY,CAAC,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC5C,GAAG,CAAC,kBAAkB,YAAY,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACrE,CAAC;QACH,CAAC;QAED,mCAAmC;QACnC,IAAI,YAAY,CAAC,YAAY,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,IAAI,sBAAsB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACnG,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,WAAW,CAAC;gBACnC,OAAO,EAAE,kEAAkE;gBAC3E,OAAO,EAAE;oBACP,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,2BAA2B,EAAE;oBACvE,GAAG,sBAAsB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;wBAClC,KAAK,EAAE,CAAC,CAAC,KAAK;wBACd,KAAK,EAAE,CAAC,CAAC,KAAK;wBACd,IAAI,EAAE,CAAC,CAAC,IAAI;qBACb,CAAC,CAAC;iBACJ;gBACD,QAAQ,EAAE,KAAK;aAChB,CAAC,CAAC;YACH,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACrD,MAAM,QAAQ,GAAI,QAAqB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,UAAU,CAAC,CAAC;gBACtE,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBACxB,YAAY,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC;gBAC9C,CAAC;YACH,CAAC;QACH,CAAC;QAED,kDAAkD;QAClD,IAAI,YAAiD,CAAC;QACtD,IAAI,YAAY,CAAC,YAAY,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACpE,MAAM,UAAU,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC;gBAC9B,OAAO,EAAE,4BAA4B;gBACrC,WAAW,EAAE,oBAAoB;gBACjC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;oBACd,IAAI,CAAC,CAAC;wBAAE,OAAO,UAAU,CAAC;oBAC1B,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;wBAAE,OAAO,uBAAuB,CAAC;oBACxD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;wBAAE,OAAO,qBAAqB,CAAC;oBACpD,OAAO,SAAS,CAAC;gBACnB,CAAC;aACF,CAAC,CAAC;YACH,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;gBAC5B,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC;oBAC7B,OAAO,EAAE,kBAAkB;oBAC3B,YAAY,EAAE,OAAO;oBACrB,WAAW,EAAE,OAAO;iBACrB,CAAC,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC;oBAC1B,OAAO,EAAE,cAAc;oBACvB,YAAY,EAAE,QAAQ;oBACtB,WAAW,EAAE,QAAQ;iBACtB,CAAC,CAAC;gBACH,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC;oBAC5B,OAAO,EAAE,qBAAqB;oBAC9B,YAAY,EAAE,IAAI;oBAClB,WAAW,EAAE,IAAI;oBACjB,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;wBACd,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;wBACpB,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;4BAAE,OAAO,2BAA2B,CAAC;wBAC1D,OAAO,SAAS,CAAC;oBACnB,CAAC;iBACF,CAAC,CAAC;gBACH,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,OAAO,CAAC;oBAC/B,OAAO,EAAE,gCAAgC;oBACzC,YAAY,EAAE,KAAK;iBACpB,CAAC,CAAC;gBAEH,YAAY,GAAG;oBACb,OAAO,EAAE,IAAI;oBACb,UAAU,EAAE,UAAoB;oBAChC,SAAS,EAAE,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAmB,IAAI,OAAO,CAAC;oBAC7E,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAgB,IAAI,QAAQ,CAAC;oBACtE,QAAQ,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE;oBAC5D,QAAQ,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ;oBACjD,IAAI,EAAE,gBAAgB,EAAE;iBACzB,CAAC;YACJ,CAAC;QACH,CAAC;QAED,2DAA2D;QAC3D,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;QACvC,MAAM,EAAE,GAAG,CAAC,CAAC,OAAO,EAAE,CAAC;QACvB,EAAE,CAAC,KAAK,CAAC,2BAA2B,CAAC,CAAC;QAEtC,oEAAoE;QACpE,OAAO,CAAC,EAAE,EAAE,2CAA2C,EAAE,GAAG,EAAE;YAC5D,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;YAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;YAE9C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC5B,SAAS,CAAC,OAAO,CAAC,CAAC;gBACnB,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,EAC/B,qRAAqR,CACtR,CAAC;YACJ,CAAC;YAED,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC3B,SAAS,CAAC,MAAM,CAAC,CAAC;gBAClB,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;gBAC1C,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;gBACxC,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,EAC9B,4KAA4K,CAC7K,CAAC;YACJ,CAAC;YAED,4CAA4C;YAC5C,KAAK,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,eAAe,CAAC,EAAE,CAAC;gBAC7G,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;YACrC,CAAC;YACD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;YACnD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;gBAC/B,EAAE,CAAC,aAAa,CACd,UAAU,EACV,maAAma,CACpa,CAAC;YACJ,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,OAAO,CAAC,EAAE,EAAE,0BAA0B,EAAE,GAAG,EAAE,CAAC,kBAAkB,CAAC,OAAO,EAAE,cAAc,EAAE,YAAY,CAAC,CAAC,CAAC;QACzG,OAAO,CAAC,EAAE,EAAE,sBAAsB,EAAE,GAAG,EAAE;YACvC,YAAY,CAAC,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC;YACvE,IAAI,YAAY,EAAE,CAAC;gBACjB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;gBACrD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;gBAChE,MAAM,CAAC,MAAM,GAAG,YAAY,CAAC;gBAC7B,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YAChE,CAAC;QACH,CAAC,CAAC,CAAC;QACH,OAAO,CAAC,EAAE,EAAE,eAAe,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC,CAAC,CAAC;QAChF,OAAO,CAAC,EAAE,EAAE,sCAAsC,EAAE,GAAG,EAAE,CAAC,4BAA4B,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC;QAEjH,EAAE,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;QAElC,2DAA2D;QAC3D,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;QACtC,MAAM,EAAE,GAAG,CAAC,CAAC,OAAO,EAAE,CAAC;QACvB,EAAE,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAErC,MAAM,UAAU,GAAG,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QAErD,sEAAsE;QACtE,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;YACxB,OAAO,CAAC,EAAE,EAAE,0BAA0B,EAAE,GAAG,EAAE,CAAC,mBAAmB,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC;QAC9F,CAAC;QACD,OAAO,CAAC,EAAE,EAAE,+BAA+B,EAAE,GAAG,EAAE,CAAC,wBAAwB,CAAC,UAAU,CAAC,CAAC,CAAC;QACzF,OAAO,CAAC,EAAE,EAAE,yBAAyB,EAAE,GAAG,EAAE,CAAC,kBAAkB,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC;QAC1F,OAAO,CAAC,EAAE,EAAE,2BAA2B,EAAE,GAAG,EAAE,CAAC,oBAAoB,CAAC,WAAW,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC;QAE1G,OAAO,CAAC,EAAE,EAAE,yBAAyB,EAAE,GAAG,EAAE;YAC1C,kBAAkB,CAAC,WAAW,EAAE,UAAU,EAAE,YAAY,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;QACrF,CAAC,CAAC,CAAC;QAEH,OAAO,CAAC,EAAE,EAAE,4BAA4B,EAAE,GAAG,EAAE;YAC7C,qBAAqB,CAAC,WAAW,EAAE,UAAU,EAAE,YAAY,CAAC,YAAY,CAAC,CAAC;QAC5E,CAAC,CAAC,CAAC;QAEH,IAAI,YAAY,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACzC,GAAG,CAAC,yBAAyB,YAAY,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzE,CAAC;QAED,OAAO,CAAC,EAAE,EAAE,iCAAiC,EAAE,GAAG,EAAE;YAClD,MAAM,eAAe,GAAG,WAAW,CAAC,SAAS,CAAC,WAAW,EAAE,cAAc,EAAE,YAAY,CAAC,CAAC;YACzF,IAAI,eAAe,CAAC,eAAe,EAAE,CAAC;gBACpC,GAAG,CAAC,sCAAsC,CAAC,CAAC;YAC9C,CAAC;YACD,IAAI,eAAe,CAAC,qBAAqB,EAAE,CAAC;gBAC1C,GAAG,CAAC,iCAAiC,CAAC,CAAC;YACzC,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,4FAA4F;QAC5F,oEAAoE;QACpE,MAAM,UAAU,GAAG,aAAa,EAAE,CAAC;QACnC,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;QACrC,MAAM,YAAY,GAAG,eAAe,EAAE,CAAC;QAEvC,wCAAwC;QACxC,OAAO,CAAC,EAAE,EAAE,iCAAiC,EAAE,GAAG,EAAE;YAClD,sBAAsB,EAAE,CAAC;YACzB,IAAI,UAAU,CAAC,SAAS;gBAAE,sBAAsB,EAAE,CAAC;YACnD,IAAI,WAAW,CAAC,SAAS;gBAAE,2BAA2B,EAAE,CAAC;YACzD,IAAI,YAAY,CAAC,SAAS;gBAAE,sBAAsB,EAAE,CAAC;YACrD,MAAM,SAAS,GAAG,CAAC,QAAQ;gBACzB,UAAU,CAAC,SAAS,IAAI,MAAM;gBAC9B,WAAW,CAAC,SAAS,IAAI,OAAO;gBAChC,YAAY,CAAC,SAAS,IAAI,QAAQ;aACnC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC7B,GAAG,CAAC,+BAA+B,SAAS,IAAI,CAAC,CAAC;QACpD,CAAC,CAAC,CAAC;QAEH,uCAAuC;QACvC,OAAO,CAAC,EAAE,EAAE,+BAA+B,EAAE,GAAG,EAAE;YAChD,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;gBACpB,uBAAuB,CAAC,WAAW,EAAE,cAAc,EAAE,YAAY,CAAC,CAAC;gBACnE,GAAG,CAAC,6BAA6B,CAAC,CAAC;gBACnC,kDAAkD;gBAClD,IAAI,UAAU,CAAC,SAAS,IAAI,WAAW,CAAC,SAAS,EAAE,CAAC;oBAClD,uBAAuB,CAAC,WAAW,EAAE,cAAc,EAAE,YAAY,CAAC,CAAC;oBACnE,GAAG,CAAC,sDAAsD,CAAC,CAAC;gBAC9D,CAAC;YACH,CAAC;iBAAM,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;gBACnD,uBAAuB,CAAC,WAAW,EAAE,cAAc,EAAE,YAAY,CAAC,CAAC;gBACnE,GAAG,CAAC,8BAA8B,MAAM,KAAK,CAAC,CAAC;YACjD,CAAC;iBAAM,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;gBAC/B,uBAAuB,CAAC,WAAW,EAAE,cAAc,EAAE,YAAY,CAAC,CAAC;gBACnE,GAAG,CAAC,6BAA6B,CAAC,CAAC;YACrC,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;QAEjC,0EAA0E;QAC1E,sDAAsD;QACtD,IAAI,mBAAmB,CAAC,WAAW,CAAC,EAAE,CAAC;YACrC,IAAI,CAAC;gBACH,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;gBAC9D,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;gBAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,oBAAoB,CAAC,CAAC;gBAC5F,IAAI,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;oBAC/B,QAAQ,CAAC,SAAS,WAAW,MAAM,WAAW,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;gBAC1F,CAAC;YACH,CAAC;YAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;QAC/B,CAAC;QAED,SAAS;QACT,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;QAErC,GAAG,CAAC,wBAAwB,WAAW,CAAC,OAAO;EACjD,eAAe,EAAE;;;QAGX,YAAY,CAAC,CAAC,CAAC,MAAM,WAAW,MAAM,CAAC,CAAC,CAAC,EAAE;CAClD,CAAC,CAAC;IAED,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACxB,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACvE,OAAO,CAAC,KAAK,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;QACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC"} |
@@ -88,2 +88,21 @@ /** | ||
| // Phase 3 — Recipes + anti-patterns 인덱스 (post-task curation) | ||
| // 본문이 아닌 1줄 요약만. session-context 가 비대해지지 않도록 N=5 상한. | ||
| try { | ||
| const { loadCurationIndex } = await import('./lib/curation-index.js'); | ||
| const index = loadCurationIndex(PROJECT_DIR, { recipeLimit: 5, antiPatternLimit: 5 }); | ||
| if (index.recipes.length > 0) { | ||
| console.log('\n[Recipes — succeeded patterns]'); | ||
| for (const r of index.recipes) { | ||
| console.log(` • ${r.slug} — ${r.summary}`); | ||
| } | ||
| } | ||
| if (index.antiPatterns.length > 0) { | ||
| console.log('\n[Anti-patterns — known pitfalls]'); | ||
| for (const a of index.antiPatterns) { | ||
| console.log(` ⚠ ${a.tag}: ${a.summary}`); | ||
| } | ||
| } | ||
| } catch { /* curation is best-effort */ } | ||
| // Version check | ||
@@ -90,0 +109,0 @@ if (latestVersion) { |
| #!/usr/bin/env node | ||
| /** | ||
| * PostToolUse Hook - 툴콜 스텝 카운터 | ||
| * PostToolUse Hook — 툴콜 스텝 카운터 + 패턴 로거 + 3-fail 감지기 | ||
| * | ||
| * 목적: vibe 멀티-오케스트레이션의 "목표 달성까지 몇 스텝" 측정. | ||
| * 모든 성공 툴콜을 1 스텝으로 집계하여 `/vibe.verify` 리포트에 노출. | ||
| * 책임 1) 모든 성공 툴콜을 1 스텝으로 집계 → `current-run.json` | ||
| * ↳ /vibe.verify 가 history.jsonl에 append 후 출력 | ||
| * 책임 2) 각 툴콜 한 줄을 `current-run.jsonl` 에 append (post-task-curation SPEC) | ||
| * ↳ Phase 3 의 recipe extractor 가 소비 | ||
| * 책임 3) (Phase 2) 실패 메시지 분류 + 같은 (file, category) 3회 반복 감지 → | ||
| * `.vibe/anti-patterns/<slug>.md` 로 자동 저장. | ||
| * | ||
| * 저장 위치: .claude/vibe/metrics/current-run.json | ||
| * - /vibe.run 시작 시 overwrite (feature, startedAt, steps=0) | ||
| * - 이후 툴콜마다 steps += 1 | ||
| * - /vibe.verify가 읽어서 history.jsonl에 append 후 출력 | ||
| * | ||
| * 실패 시 조용히 통과 (차단 금지). | ||
| * 세 책임은 독립 — 어느 한쪽 실패가 다른 쪽을 막지 않는다. | ||
| * PostToolUse 는 hot path 이므로 LLM 호출/외부 spawn 금지, fs 만 사용. | ||
| */ | ||
@@ -20,24 +20,233 @@ import fs from 'fs'; | ||
| const METRICS_DIR = projectVibePath(PROJECT_DIR, 'metrics'); | ||
| const CURRENT_RUN = path.join(METRICS_DIR, 'current-run.json'); | ||
| const ANTI_PATTERNS_DIR = projectVibePath(PROJECT_DIR, 'anti-patterns'); | ||
| const CURRENT_RUN_JSON = path.join(METRICS_DIR, 'current-run.json'); | ||
| const CURRENT_RUN_JSONL = path.join(METRICS_DIR, 'current-run.jsonl'); | ||
| const MAX_JSONL_LINES = 5000; | ||
| const FAIL_WINDOW = 10; | ||
| const FAIL_THRESHOLD = 3; | ||
| try { | ||
| if (!fs.existsSync(METRICS_DIR)) { | ||
| fs.mkdirSync(METRICS_DIR, { recursive: true }); | ||
| // ───────────────────────────────────────────────────── | ||
| // stdin / env 에서 PostToolUse payload 추출 | ||
| // ───────────────────────────────────────────────────── | ||
| function readStdinSync() { | ||
| try { | ||
| if (process.stdin.isTTY) return null; | ||
| const buf = Buffer.alloc(65536); | ||
| const bytesRead = fs.readSync(0, buf, 0, buf.length, null); | ||
| if (bytesRead > 0) return JSON.parse(buf.toString('utf-8', 0, bytesRead)); | ||
| } catch { /* ignore */ } | ||
| return null; | ||
| } | ||
| function parseToolInput(raw) { | ||
| if (!raw) return {}; | ||
| if (typeof raw === 'string') { | ||
| try { return JSON.parse(raw); } catch { return {}; } | ||
| } | ||
| return raw; | ||
| } | ||
| function extractTargetFile(toolInput) { | ||
| const fp = toolInput.file_path || toolInput.notebook_path || toolInput.path || null; | ||
| if (!fp) return null; | ||
| try { | ||
| const abs = path.isAbsolute(fp) ? fp : path.resolve(PROJECT_DIR, fp); | ||
| const rel = path.relative(path.resolve(PROJECT_DIR), abs).replace(/\\/g, '/'); | ||
| return rel || path.basename(fp); | ||
| } catch { | ||
| return fp; | ||
| } | ||
| } | ||
| function isResponseError(toolResponse) { | ||
| if (!toolResponse) return false; | ||
| if (typeof toolResponse === 'object') { | ||
| if (toolResponse.is_error === true) return true; | ||
| if (toolResponse.error) return true; | ||
| if (Array.isArray(toolResponse.errors) && toolResponse.errors.length > 0) return true; | ||
| } | ||
| return false; | ||
| } | ||
| // ───────────────────────────────────────────────────── | ||
| // 책임 3a: error_category 분류 (regex 만 — vibe-regress tag enum 일부) | ||
| // ───────────────────────────────────────────────────── | ||
| const ERROR_CATEGORIES = [ | ||
| { tag: 'nullability', re: /Cannot read propert(?:y|ies) of (?:undefined|null)|TypeError[^\n]*(?:undefined|null)/i }, | ||
| { tag: 'type-narrow', re: /\bTS2345\b|not assignable to (?:parameter|type)/i }, | ||
| { tag: 'compilation', re: /\bTS\d{4}\b|cannot find name|build failed|compilation (?:error|failed)/i }, | ||
| { tag: 'syntax', re: /SyntaxError|Unexpected token/i }, | ||
| { tag: 'integration', re: /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET|connect failed/i }, | ||
| { tag: 'auth', re: /\b(?:401|403)\b|unauthori[sz]ed|forbidden|invalid (?:token|credentials)/i }, | ||
| { tag: 'permission', re: /EACCES|permission denied/i }, | ||
| { tag: 'not-found', re: /ENOENT|no such file|not found/i }, | ||
| ]; | ||
| function classifyError(toolResponse) { | ||
| if (!toolResponse) return null; | ||
| let text; | ||
| if (typeof toolResponse === 'string') text = toolResponse; | ||
| else if (toolResponse.error) text = String(toolResponse.error); | ||
| else if (Array.isArray(toolResponse.errors)) text = JSON.stringify(toolResponse.errors); | ||
| else if (toolResponse.content) text = JSON.stringify(toolResponse.content); | ||
| else text = JSON.stringify(toolResponse); | ||
| for (const { tag, re } of ERROR_CATEGORIES) { | ||
| if (re.test(text)) return tag; | ||
| } | ||
| return 'other'; | ||
| } | ||
| // ───────────────────────────────────────────────────── | ||
| // 책임 1: current-run.json 카운터 | ||
| // ───────────────────────────────────────────────────── | ||
| function bumpCounter() { | ||
| let data = { feature: null, startedAt: null, steps: 0 }; | ||
| if (fs.existsSync(CURRENT_RUN)) { | ||
| if (fs.existsSync(CURRENT_RUN_JSON)) { | ||
| try { | ||
| data = JSON.parse(fs.readFileSync(CURRENT_RUN, 'utf-8')); | ||
| } catch { | ||
| // 손상 파일 무시, 새로 시작 | ||
| data = JSON.parse(fs.readFileSync(CURRENT_RUN_JSON, 'utf-8')); | ||
| } catch { /* 손상 → 새로 시작 */ } | ||
| } | ||
| if (!data.startedAt) data.startedAt = new Date().toISOString(); | ||
| data.steps = (data.steps || 0) + 1; | ||
| fs.writeFileSync(CURRENT_RUN_JSON, JSON.stringify(data, null, 2)); | ||
| } | ||
| // ───────────────────────────────────────────────────── | ||
| // 책임 2: current-run.jsonl append + error_category 채움 | ||
| // ───────────────────────────────────────────────────── | ||
| function appendJsonl(stdinPayload) { | ||
| const toolName = stdinPayload?.tool_name || process.argv[2] || ''; | ||
| if (!toolName) return null; | ||
| const toolInput = parseToolInput(stdinPayload?.tool_input ?? process.env.TOOL_INPUT); | ||
| const ok = !isResponseError(stdinPayload?.tool_response); | ||
| const errorCategory = ok ? null : classifyError(stdinPayload?.tool_response); | ||
| const record = { | ||
| ts: new Date().toISOString(), | ||
| tool: toolName, | ||
| ok, | ||
| target_file: extractTargetFile(toolInput), | ||
| error_category: errorCategory, | ||
| }; | ||
| fs.appendFileSync(CURRENT_RUN_JSONL, JSON.stringify(record) + '\n'); | ||
| // 회전: 라인 수가 상한 초과 시 마지막 절반만 남김. | ||
| try { | ||
| const stat = fs.statSync(CURRENT_RUN_JSONL); | ||
| if (stat.size > 2 * 1024 * 1024) { | ||
| const lines = fs.readFileSync(CURRENT_RUN_JSONL, 'utf-8').split('\n').filter(Boolean); | ||
| if (lines.length > MAX_JSONL_LINES) { | ||
| const keep = lines.slice(-Math.floor(MAX_JSONL_LINES / 2)); | ||
| fs.writeFileSync(CURRENT_RUN_JSONL, keep.join('\n') + '\n'); | ||
| } | ||
| } | ||
| } catch { /* 회전 실패는 무시 */ } | ||
| return record; | ||
| } | ||
| // ───────────────────────────────────────────────────── | ||
| // 책임 3b: 3-fail detector → anti-pattern md | ||
| // ───────────────────────────────────────────────────── | ||
| const SUGGESTED_STOPS = { | ||
| nullability: '같은 위치 null/undefined 처리 반복 — 타입 가드 또는 옵셔널 체이닝 필요', | ||
| 'type-narrow': '같은 위치 타입 좁히기 반복 — 타입 정의 점검 필요', | ||
| compilation: '컴파일 실패 반복 — 타입/임포트 정의 확인 필요', | ||
| syntax: '구문 오류 반복 — 파서 호환성/버전 확인 필요', | ||
| integration: '외부 서비스 연결 실패 반복 — endpoint/포트/네트워크 점검 필요', | ||
| auth: '인증 실패 반복 — 토큰/자격증명 점검 필요', | ||
| permission: '권한 실패 반복 — 파일/디렉토리 권한 점검 필요', | ||
| 'not-found': '리소스 부재 반복 — 경로/존재 확인 필요', | ||
| other: '같은 패턴 실패 반복 — 접근 방식 재검토 필요', | ||
| }; | ||
| function fileSlug(targetFile) { | ||
| if (!targetFile) return 'global'; | ||
| return targetFile.replace(/[\/\.]/g, '-'); | ||
| } | ||
| function readTailWindow() { | ||
| if (!fs.existsSync(CURRENT_RUN_JSONL)) return []; | ||
| const raw = fs.readFileSync(CURRENT_RUN_JSONL, 'utf-8').split('\n').filter(Boolean); | ||
| return raw.slice(-FAIL_WINDOW).map((l) => { | ||
| try { return JSON.parse(l); } catch { return null; } | ||
| }).filter(Boolean); | ||
| } | ||
| function detectThreeFail() { | ||
| const window = readTailWindow(); | ||
| const groups = new Map(); | ||
| for (const r of window) { | ||
| if (r.ok || !r.error_category) continue; | ||
| const key = `${r.target_file ?? 'null'}::${r.error_category}`; | ||
| const cur = groups.get(key) ?? 0; | ||
| groups.set(key, cur + 1); | ||
| } | ||
| for (const [key, count] of groups) { | ||
| if (count >= FAIL_THRESHOLD) { | ||
| const sep = key.indexOf('::'); | ||
| const file = key.slice(0, sep); | ||
| const cat = key.slice(sep + 2); | ||
| return { category: cat, targetFile: file === 'null' ? null : file, count }; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| if (!data.startedAt) { | ||
| data.startedAt = new Date().toISOString(); | ||
| function writeAntiPattern({ category, targetFile, count }) { | ||
| if (!fs.existsSync(ANTI_PATTERNS_DIR)) { | ||
| fs.mkdirSync(ANTI_PATTERNS_DIR, { recursive: true }); | ||
| } | ||
| data.steps = (data.steps || 0) + 1; | ||
| const today = new Date().toISOString().slice(0, 10); | ||
| const dateSlug = today.replace(/-/g, ''); | ||
| const fSlug = fileSlug(targetFile); | ||
| const slug = `${category}__${fSlug}__${dateSlug}`; | ||
| const filePath = path.join(ANTI_PATTERNS_DIR, `${slug}.md`); | ||
| if (fs.existsSync(filePath)) return; // dedup | ||
| fs.writeFileSync(CURRENT_RUN, JSON.stringify(data, null, 2)); | ||
| const trigger = `(file=${targetFile ?? 'null'}, category=${category})`; | ||
| const stop = SUGGESTED_STOPS[category] ?? SUGGESTED_STOPS.other; | ||
| const content = `--- | ||
| slug: ${slug} | ||
| type: anti-pattern | ||
| root-cause-tag: ${category} | ||
| trigger-signature: "${trigger}" | ||
| fail-count: ${count} | ||
| suggested-stop: "${stop}" | ||
| created: ${today} | ||
| --- | ||
| ## Trigger | ||
| ${trigger} 이 ${FAIL_WINDOW} 툴콜 윈도우 내 ${count} 회 발생. | ||
| ## Suggested Stop | ||
| ${stop} | ||
| > 자동 생성 — 같은 패턴 반복 시 다른 접근법을 고려하거나 사용자에게 조언을 구할 것. | ||
| `; | ||
| fs.writeFileSync(filePath, content); | ||
| } | ||
| // ───────────────────────────────────────────────────── | ||
| // 메인 — 세 책임을 독립적으로 try | ||
| // ───────────────────────────────────────────────────── | ||
| try { | ||
| if (!fs.existsSync(METRICS_DIR)) { | ||
| fs.mkdirSync(METRICS_DIR, { recursive: true }); | ||
| } | ||
| const stdinPayload = readStdinSync(); | ||
| try { bumpCounter(); } catch { /* 카운터 실패 무시 */ } | ||
| let lastRecord = null; | ||
| try { lastRecord = appendJsonl(stdinPayload); } catch { /* 로깅 실패 무시 */ } | ||
| try { | ||
| if (lastRecord && !lastRecord.ok && lastRecord.error_category) { | ||
| const trip = detectThreeFail(); | ||
| if (trip) writeAntiPattern(trip); | ||
| } | ||
| } catch { /* anti-pattern 작성 실패 무시 */ } | ||
| } catch { | ||
@@ -44,0 +253,0 @@ // Never block on counter failure |
+1
-1
| { | ||
| "name": "@su-record/vibe", | ||
| "version": "2.9.38", | ||
| "version": "2.9.40", | ||
| "description": "AI Coding Framework for Claude Code — 56 agents, 45 skills, multi-LLM orchestration", | ||
@@ -5,0 +5,0 @@ "type": "module", |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
7272014
0.72%1684
0.3%75630
1.6%313
3.99%62
5.08%