@su-record/vibe 3.2.13 → 3.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CLAUDE.md +15 -14
  2. package/README.en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/__tests__/engines-contract.test.d.ts +2 -0
  5. package/dist/__tests__/engines-contract.test.d.ts.map +1 -0
  6. package/dist/__tests__/engines-contract.test.js +72 -0
  7. package/dist/__tests__/engines-contract.test.js.map +1 -0
  8. package/dist/__tests__/instruction-drift.test.d.ts +2 -0
  9. package/dist/__tests__/instruction-drift.test.d.ts.map +1 -0
  10. package/dist/__tests__/instruction-drift.test.js +116 -0
  11. package/dist/__tests__/instruction-drift.test.js.map +1 -0
  12. package/dist/__tests__/stakes-contract.test.js +7 -0
  13. package/dist/__tests__/stakes-contract.test.js.map +1 -1
  14. package/dist/__tests__/stuck-semantics.test.js +52 -0
  15. package/dist/__tests__/stuck-semantics.test.js.map +1 -1
  16. package/dist/cli/setup/ProjectSetup.d.ts +6 -0
  17. package/dist/cli/setup/ProjectSetup.d.ts.map +1 -1
  18. package/dist/cli/setup/ProjectSetup.js +19 -13
  19. package/dist/cli/setup/ProjectSetup.js.map +1 -1
  20. package/hooks/hooks.json +1 -1
  21. package/hooks/scripts/__tests__/.vibe/command-log.txt +3 -3
  22. package/hooks/scripts/__tests__/anchor-inbox.test.js +119 -0
  23. package/hooks/scripts/__tests__/code-check-false-positive.test.js +74 -0
  24. package/hooks/scripts/__tests__/fixtures/seq-harness.js +13 -0
  25. package/hooks/scripts/__tests__/fixtures/seq-step.js +22 -0
  26. package/hooks/scripts/__tests__/stop-dispatcher-sequential.test.js +74 -0
  27. package/hooks/scripts/code-check.js +60 -50
  28. package/hooks/scripts/lib/anchor.js +74 -0
  29. package/hooks/scripts/lib/console-allow.js +66 -0
  30. package/hooks/scripts/lib/dispatcher.js +28 -16
  31. package/hooks/scripts/lib/inbox.js +60 -0
  32. package/hooks/scripts/loop-ledger.js +24 -1
  33. package/hooks/scripts/post-edit-dispatcher.js +4 -3
  34. package/hooks/scripts/post-edit.js +2 -2
  35. package/package.json +5 -3
  36. package/skills/vibe/SKILL.md +2 -1
  37. package/skills/vibe.clone/references/verification-loops.md +6 -3
  38. package/skills/vibe.loop/SKILL.md +7 -4
  39. package/skills/vibe.review/SKILL.md +5 -1
  40. package/skills/vibe.run/references/e2e-and-autofix.md +1 -1
  41. package/skills/vibe.run/references/process-steps.md +1 -1
  42. package/vibe/constitution.md +1 -1
  43. package/vibe/rules/loop-contract.md +40 -3
  44. package/vibe/rules/quality/checklist.md +3 -3
  45. package/vibe/templates/constitution-template.md +1 -1
@@ -0,0 +1,74 @@
1
+ /**
2
+ * ANCHOR — 루프 회전 시작 시 디스크에서 상태를 재고정한다.
3
+ *
4
+ * loop-contract 는 ANCHOR 를 컨텍스트 오염 방어의 근거로 규정하는데, JUDGE·RECORD·stuck 과
5
+ * 달리 실행 수단이 없어 모델 재량으로 남아 있었다 (감사 2026-07-28 L3). 재고정 대상이
6
+ * 무엇인지 결정론적으로 답하는 것이 이 모듈의 역할이다 — 파일 내용을 해석하지는 않는다.
7
+ */
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import { readLedger } from './run-ledger.js';
11
+
12
+ /** 우선순위대로 첫 번째로 존재하는 경로를 고른다 */
13
+ function firstExisting(projectDir, candidates) {
14
+ for (const rel of candidates) {
15
+ if (fs.existsSync(path.join(projectDir, rel))) return rel;
16
+ }
17
+ return null;
18
+ }
19
+
20
+ /** `.vibe/.last-feature` 에 기록된 직전 feature 이름 */
21
+ function readLastFeature(projectDir) {
22
+ try {
23
+ const raw = fs.readFileSync(path.join(projectDir, '.vibe', '.last-feature'), 'utf-8').trim();
24
+ return raw.length > 0 ? raw : null;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ /** SPEC 경로 — 신규 레이아웃 우선, 레거시 폴백 */
31
+ function findSpec(projectDir, feature) {
32
+ if (!feature) return null;
33
+ return firstExisting(projectDir, [
34
+ path.join('.vibe', 'specs', `${feature}.md`),
35
+ path.join('.vibe', 'specs', feature, '_index.md'),
36
+ path.join('.claude', 'vibe', 'specs', `${feature}.md`),
37
+ path.join('.claude', 'specs', `${feature}.md`),
38
+ ]);
39
+ }
40
+
41
+ /** 인박스에서 가장 최근 블록(다음 `## ` 직전까지) */
42
+ function readLatestInboxBlock(projectDir) {
43
+ try {
44
+ const raw = fs.readFileSync(path.join(projectDir, '.vibe', 'loops', 'inbox.md'), 'utf-8');
45
+ const start = raw.indexOf('## ');
46
+ if (start === -1) return null;
47
+ const next = raw.indexOf('\n## ', start + 3);
48
+ return (next === -1 ? raw.slice(start) : raw.slice(start, next)).trim() || null;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * 재고정 번들 — loop-contract ANCHOR 절이 지정한 SPEC + run-ledger + scope.json + 직전 인박스.
56
+ *
57
+ * @param {string} projectDir
58
+ * @param {string} [feature] - 생략 시 `.vibe/.last-feature`
59
+ * @returns {{ feature: string|null, spec: string|null, scope: string|null,
60
+ * ledger: object|null, latestInbox: string|null, missing: string[] }}
61
+ */
62
+ export function buildAnchor(projectDir, feature) {
63
+ const resolved = feature || readLastFeature(projectDir);
64
+ const spec = findSpec(projectDir, resolved);
65
+ const scope = firstExisting(projectDir, [path.join('.vibe', 'scope.json')]);
66
+ const ledger = readLedger(projectDir);
67
+
68
+ const missing = [];
69
+ if (!resolved) missing.push('feature');
70
+ if (!spec) missing.push('spec');
71
+ if (!ledger) missing.push('run-ledger');
72
+
73
+ return { feature: resolved, spec, scope, ledger, latestInbox: readLatestInboxBlock(projectDir), missing };
74
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * console.log 탐지의 적용 범위 판단 — code-check.js·post-edit.js 공용 (중복 제거).
3
+ *
4
+ * 두 훅이 같은 검사를 각자 들고 있으면 한쪽의 허용 경로 설계가 다른 쪽 경고에
5
+ * 무력화된다. 범위 규칙은 여기 하나만 둔다.
6
+ */
7
+ import path from 'path';
8
+ import { PROJECT_DIR, readProjectConfig } from '../utils.js';
9
+ import { globToRegExp } from './glob.js';
10
+
11
+ /**
12
+ * 코드 확장자 — 마크다운·JSON·텍스트에 인용된 `console.log(` 는 커밋되면 안 되는
13
+ * 디버그 코드가 아니라 문서상의 예시다.
14
+ */
15
+ export const CODE_EXT_RE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
16
+
17
+ // console.log 기본 허용 경로 (glob 패턴 → 정규식으로 변환)
18
+ const DEFAULT_CONSOLE_ALLOW_GLOBS = [
19
+ 'hooks/scripts/**',
20
+ 'scripts/**',
21
+ '**/cli/**',
22
+ '**/*.test.*',
23
+ '**/*.spec.*',
24
+ '**/__tests__/**',
25
+ ];
26
+
27
+ /**
28
+ * .vibe/config.json의 qualityCheck.consoleAllow 글로브 목록 로드.
29
+ * 기본 글로브와 병합하여 반환.
30
+ * @returns {RegExp[]}
31
+ */
32
+ function loadConsoleAllowPatterns() {
33
+ try {
34
+ const cfg = readProjectConfig();
35
+ const extra = cfg?.qualityCheck?.consoleAllow;
36
+ const globs = Array.isArray(extra)
37
+ ? [...DEFAULT_CONSOLE_ALLOW_GLOBS, ...extra]
38
+ : DEFAULT_CONSOLE_ALLOW_GLOBS;
39
+ return globs.map(g => globToRegExp(g));
40
+ } catch {
41
+ return DEFAULT_CONSOLE_ALLOW_GLOBS.map(g => globToRegExp(g));
42
+ }
43
+ }
44
+
45
+ /**
46
+ * 파일 경로가 console.log 허용 경로인지 판단.
47
+ * @param {string} filePath - 절대 또는 프로젝트 상대 경로
48
+ * @returns {boolean}
49
+ */
50
+ export function isConsoleAllowed(filePath) {
51
+ try {
52
+ const rel = path.relative(PROJECT_DIR, path.resolve(filePath)).replace(/\\/g, '/');
53
+ return loadConsoleAllowPatterns().some(re => re.test(rel));
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * console.log 검사 대상 파일인지 — 코드 확장자이면서 허용 경로가 아닌 경우.
61
+ * @param {string} filePath
62
+ * @returns {boolean}
63
+ */
64
+ export function shouldCheckConsole(filePath) {
65
+ return CODE_EXT_RE.test(filePath) && !isConsoleAllowed(filePath);
66
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Hook dispatcher library — 여러 hook script를 단일 이벤트에서 병렬 실행.
2
+ * Hook dispatcher library — 여러 hook script를 단일 이벤트에서 실행.
3
3
  *
4
4
  * 목적:
5
5
  * - stdin을 한 번만 읽어 각 자식에 동일 버퍼로 pipe (중복 파싱/읽기 방지)
@@ -7,14 +7,23 @@
7
7
  * - 한 스크립트 실패가 다른 스크립트를 막지 않도록 cascade 격리
8
8
  * - PreToolUse 계열: 자식 중 하나라도 exit 2(deny)면 상위에 전파
9
9
  *
10
- * 직렬 병렬 전환 (2026-04):
11
- * 기존 직렬 실행은 tool당 150~300ms 누적 오버헤드를 유발.
12
- * PreToolUse 가드는 모두 독립적 검증자이므로 병렬화해도 의미상 문제 없음.
13
- * 트레이드오프:
14
- * - early-deny 낭비: sentinel-guard가 block이어도 pre-tool/scope-guard가
15
- * 이미 spawn됨. 실측 μs 수준이라 무시.
16
- * - stderr 인터리빙: 가드 2개가 동시 block 시 경고 메시지가 섞일 수 있음.
17
- * 메시지는 자체적으로 완결된 라인이라 가독성 문제 없음.
10
+ * 실행 모델이 둘로 갈린다 — 스텝이 서로 독립인지가 기준:
11
+ *
12
+ * dispatch() = 순차 (spawn). 유일한 사용처인 Stop 스텝끼리
13
+ * 부작용을 공유한다(auto-commit 의 git 상태를
14
+ * devlog-gen 읽는다). 병렬화하면 auto-commit
15
+ * git cascade 겹쳐 프로세스가 폭주하고,
16
+ * devlog 커밋 이전 상태를 관측한다.
17
+ * 회귀 방지: __tests__/stop-dispatcher-sequential.test.js
18
+ *
19
+ * dispatchInProcess() = 병렬 (import). PreToolUse 가드는 모두 독립적
20
+ * 검증자라 순서가 의미 없고, 직렬 실행은 tool당
21
+ * 150~300ms 누적 오버헤드를 유발한다.
22
+ * 트레이드오프:
23
+ * - early-deny 낭비: sentinel-guard가 block이어도
24
+ * pre-tool/scope-guard가 이미 실행됨. 실측 μs 수준.
25
+ * - stderr 인터리빙: 가드 2개가 동시 block 시 경고가
26
+ * 섞일 수 있음. 각 메시지가 완결된 라인이라 무해.
18
27
  */
19
28
  import { spawn } from 'child_process';
20
29
  import path from 'path';
@@ -93,7 +102,11 @@ function buildChildEnv(stdinData) {
93
102
  }
94
103
 
95
104
  /**
96
- * 디스패처 실행 — 활성화된 스텝을 병렬로 spawn.
105
+ * 디스패처 실행 — 활성화된 스텝을 선언 순서대로 **순차** spawn.
106
+ *
107
+ * 순차인 이유는 파일 상단 주석 참고 — 스텝이 git 상태 같은 부작용을 공유한다.
108
+ * 앞 스텝이 실패해도 다음 스텝은 계속 실행한다(cascade 격리 유지).
109
+ *
97
110
  * @param {Array<{name: string, script: string, args?: string[], denyOnExit2?: boolean, timeoutMs?: number}>} steps
98
111
  */
99
112
  export async function dispatch(steps) {
@@ -101,12 +114,11 @@ export async function dispatch(steps) {
101
114
  const hookConfig = loadHookConfig();
102
115
 
103
116
  const enabledSteps = steps.filter(s => isEnabled(hookConfig, s.name));
104
- const results = await Promise.all(
105
- enabledSteps.map(step =>
106
- runScript(step.script, step.args || [], stdinData, step.timeoutMs || 30000)
107
- .then(code => ({ step, code }))
108
- )
109
- );
117
+ const results = [];
118
+ for (const step of enabledSteps) {
119
+ const code = await runScript(step.script, step.args || [], stdinData, step.timeoutMs || 30000);
120
+ results.push({ step, code });
121
+ }
110
122
 
111
123
  // 하나라도 deny(exit 2) 반환 → 상위에 전파
112
124
  if (results.some(({ step, code }) => step.denyOnExit2 && code === 2)) {
@@ -0,0 +1,60 @@
1
+ /**
2
+ * 루프 인박스 — 사람 리뷰 큐(`.vibe/loops/inbox.md`) 기록.
3
+ *
4
+ * loop-history.jsonl 은 결정론적으로 기록되는데 인박스만 모델이 마크다운을 직접
5
+ * prepend 하고 있었다 (감사 2026-07-28 L5). 블록 형식과 최신순 정렬을 코드가 보장한다.
6
+ *
7
+ * fail-open — 기록 실패가 루프를 멈추지 않는다.
8
+ */
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+
12
+ const HEADER = '# Loop Inbox\n\n> 루프가 남긴 사람 리뷰 큐. 최신 항목이 위에 온다.\n';
13
+
14
+ function inboxPath(projectDir) {
15
+ return path.join(projectDir, '.vibe', 'loops', 'inbox.md');
16
+ }
17
+
18
+ /**
19
+ * 인박스 블록을 최상단에 prepend 한다.
20
+ *
21
+ * @param {string} projectDir
22
+ * @param {{ loop: string, result: 'ok'|'fail'|'stuck', at: string, lines?: string[] }} entry
23
+ * at 은 호출자가 넘긴다 — 이 모듈은 시각을 직접 읽지 않는다 (테스트 결정성)
24
+ * @returns {boolean} 성공 여부
25
+ */
26
+ export function prependInboxBlock(projectDir, entry) {
27
+ try {
28
+ if (!entry?.loop || !entry?.result || !entry?.at) return false;
29
+
30
+ const body = (entry.lines ?? []).map(l => `- ${l}`).join('\n');
31
+ const block = `## ${entry.loop} — ${entry.at} — ${entry.result}\n${body}\n`;
32
+
33
+ const target = inboxPath(projectDir);
34
+ fs.mkdirSync(path.dirname(target), { recursive: true });
35
+
36
+ const existing = fs.existsSync(target) ? fs.readFileSync(target, 'utf-8') : '';
37
+ const blocksStart = existing.indexOf('## ');
38
+ const head = blocksStart === -1 ? HEADER : existing.slice(0, blocksStart);
39
+ const rest = blocksStart === -1 ? '' : existing.slice(blocksStart);
40
+
41
+ fs.writeFileSync(target, `${head}\n${block}\n${rest}`.replace(/\n{3,}/g, '\n\n'), 'utf-8');
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * 아직 처리되지 않은 블록 수 — `## ` 로 시작하는 줄의 개수.
50
+ * @param {string} projectDir
51
+ * @returns {number}
52
+ */
53
+ export function countInboxBlocks(projectDir) {
54
+ try {
55
+ const raw = fs.readFileSync(inboxPath(projectDir), 'utf-8');
56
+ return raw.split('\n').filter(l => l.startsWith('## ')).length;
57
+ } catch {
58
+ return 0;
59
+ }
60
+ }
@@ -6,12 +6,17 @@
6
6
  * node hooks/scripts/loop-ledger.js start <name>
7
7
  * node hooks/scripts/loop-ledger.js end <name> <ok|fail|stuck> [summary]
8
8
  * node hooks/scripts/loop-ledger.js check-stuck <name> <discoverHash>
9
+ * node hooks/scripts/loop-ledger.js anchor [feature]
10
+ * node hooks/scripts/loop-ledger.js inbox <name> <ok|fail|stuck> [line...]
9
11
  *
10
12
  * check-stuck: 'stuck' 또는 'ok'를 stdout에 출력하고 항상 exit 0.
13
+ * anchor: 재고정 번들 JSON을 stdout에 출력한다 (loop-contract ANCHOR 절).
11
14
  * 항상 exit 0 (fail-open).
12
15
  */
13
16
 
14
17
  import { appendLoopEvent, isStuck } from './lib/loop-ledger.js';
18
+ import { buildAnchor } from './lib/anchor.js';
19
+ import { prependInboxBlock } from './lib/inbox.js';
15
20
 
16
21
  const [, , subcommand, ...args] = process.argv;
17
22
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
@@ -47,9 +52,27 @@ if (subcommand === 'start') {
47
52
  appendLoopEvent(projectDir, { loop, event: 'discover', discoverHash });
48
53
  process.stdout.write(stuck ? 'stuck\n' : 'ok\n');
49
54
 
55
+ } else if (subcommand === 'anchor') {
56
+ // 회전 시작 시 디스크 재고정 — 모델이 무엇을 다시 읽어야 하는지 결정론적으로 답한다
57
+ process.stdout.write(JSON.stringify(buildAnchor(projectDir, args[0]), null, 2) + '\n');
58
+
59
+ } else if (subcommand === 'inbox') {
60
+ const [loop, result, ...lines] = args;
61
+ if (!loop || !result) {
62
+ process.stdout.write('[loop-ledger] error: inbox 에 루프 이름과 결과(ok|fail|stuck)가 필요합니다\n');
63
+ process.exit(0);
64
+ }
65
+ const at = new Date().toISOString();
66
+ const ok = prependInboxBlock(projectDir, { loop, result, at, lines });
67
+ process.stdout.write(
68
+ ok ? `[loop-ledger] inbox recorded: loop=${loop} result=${result}\n`
69
+ : '[loop-ledger] WARNING: inbox write failed\n'
70
+ );
71
+
50
72
  } else {
51
73
  process.stdout.write(
52
- '[loop-ledger] 사용법: start <name> | end <name> <ok|fail|stuck> [summary] | check-stuck <name> <hash>\n'
74
+ '[loop-ledger] 사용법: start <name> | end <name> <ok|fail|stuck> [summary] | '
75
+ + 'check-stuck <name> <hash> | anchor [feature] | inbox <name> <ok|fail|stuck> [line...]\n'
53
76
  );
54
77
  }
55
78
 
@@ -10,7 +10,10 @@
10
10
  * auto-format — 코드 스타일 정규화 (변경 시 finding 반환)
11
11
  * code-check — 하드룰(any/console.log) 탐지 (additionalContext 주입만, 커밋 게이트 미연동)
12
12
  * auto-test — 관련 테스트 실행 (debounce 지원)
13
- * post-edit — console.log 감지
13
+ *
14
+ * post-edit.js 는 여기서 돌리지 않는다 — console.log 감지는 code-check 가 같은
15
+ * 허용 경로 규칙(lib/console-allow.js)으로 이미 수행한다. 둘 다 돌리면 허용
16
+ * 경로에서도 경고가 남는다. post-edit.js 는 antigravity-hooks.json 단독 등록용.
14
17
  *
15
18
  * 출력 계약 (Claude Code PostToolUse):
16
19
  * findings 있음 → stdout에 JSON hookSpecificOutput 1개 출력, exit 0
@@ -33,7 +36,6 @@ import path from 'path';
33
36
  import { run as autoFormat } from './auto-format.js';
34
37
  import { run as codeCheck } from './code-check.js';
35
38
  import { run as autoTest } from './auto-test.js';
36
- import { run as postEdit } from './post-edit.js';
37
39
 
38
40
  // ─── 설정 로딩 ────────────────────────────────────────────────────────
39
41
  function loadHookConfig() {
@@ -59,7 +61,6 @@ const steps = [
59
61
  { name: 'auto-format', run: autoFormat },
60
62
  { name: 'code-check', run: codeCheck },
61
63
  { name: 'auto-test', run: autoTest },
62
- { name: 'post-edit', run: postEdit },
63
64
  ];
64
65
 
65
66
  const enabledSteps = steps.filter(s => isEnabled(hookConfig, s.name));
@@ -10,9 +10,9 @@
10
10
  import { existsSync, readFileSync } from 'fs';
11
11
  import path from 'path';
12
12
  import { buildCliCtx, isDirectRun } from './lib/hook-context.js';
13
+ import { shouldCheckConsole } from './lib/console-allow.js';
13
14
 
14
15
  const CONSOLE_LOG_RE = /console\.log\(/;
15
- const CODE_EXT_RE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
16
16
 
17
17
  /**
18
18
  * in-process 진입점 — console.log 감지만 수행.
@@ -25,7 +25,7 @@ export async function run(ctx) {
25
25
  try {
26
26
  const filePath = ctx.filePath;
27
27
 
28
- if (filePath && CODE_EXT_RE.test(filePath)) {
28
+ if (filePath && shouldCheckConsole(filePath)) {
29
29
  const resolved = path.resolve(filePath);
30
30
  if (existsSync(resolved)) {
31
31
  const lines = readFileSync(resolved, 'utf-8').split('\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@su-record/vibe",
3
- "version": "3.2.13",
3
+ "version": "3.2.14",
4
4
  "description": "AI Coding Framework for Claude Code — 7+ agents, 52 skills, multi-LLM orchestration",
5
5
  "type": "module",
6
6
  "main": "dist/cli/index.js",
@@ -31,6 +31,7 @@
31
31
  "validate:counts": "npx tsx scripts/validate-counts.ts",
32
32
  "test": "vitest run",
33
33
  "test:watch": "vitest",
34
+ "test:coverage": "vitest run --coverage",
34
35
  "prepublishOnly": "pnpm build",
35
36
  "postinstall": "node -e \"import('./dist/cli/postinstall/main.js').then(m=>m.main()).catch(()=>{})\"",
36
37
  "release": "pnpm version patch && git push origin main --follow-tags"
@@ -66,7 +67,7 @@
66
67
  "access": "public"
67
68
  },
68
69
  "engines": {
69
- "node": ">=18.0.0"
70
+ "node": ">=20.12.0"
70
71
  },
71
72
  "optionalDependencies": {
72
73
  "@anthropic-ai/claude-agent-sdk": "^0.2.6",
@@ -86,9 +87,10 @@
86
87
  "@types/better-sqlite3": "^7.6.13",
87
88
  "@types/node": "^22.0.0",
88
89
  "@types/papaparse": "^5.5.2",
90
+ "@vitest/coverage-v8": "^4.0.9",
89
91
  "ajv": "^8.17.1",
90
92
  "typescript": "^5.5.4",
91
- "vitest": "^4.0.9"
93
+ "vitest": "^4.1.10"
92
94
  },
93
95
  "files": [
94
96
  "dist/",
@@ -178,10 +178,11 @@ Phase 4: /vibe.verify → 검증
178
178
  스킬 **이름과 인자**가 계약이고, 그것을 실제 호출로 바꾸는 것은 각 하네스의 몫이다.
179
179
 
180
180
  각 phase 종료 후 JUDGE 단계:
181
- - 게이트 통과 (P1=0 ∧ verifyPassed) → 루프 종료, Phase 5 보고
181
+ - 게이트 통과 (**측정된** P1=0 ∧ verifyPassed) → 루프 종료, Phase 5 보고. 판정된 P1(리뷰어 findings)은 단독으로 게이트를 막지 않는다 — SSOT: `vibe/rules/loop-contract.md` Judge 권한 경계
182
182
  - 게이트 미통과 → RECORD(run-ledger + loop-history.jsonl) 후 다음 ANCHOR로
183
183
  - stuck(연속 2회 동일 findings 해시) → **어느 automationLevel 에서도 루프를 종료한다.** `confirm`이면 사용자 질문, `autonomous`이면 질문 없이 TODO 기록 후 다음 독립 단위로. 미달을 완료로 기록하지 않는다 (SSOT: `vibe/rules/loop-contract.md` stuck 절)
184
184
  - max_iterations(기본 10) 도달 → 잔여를 인박스로 이월
185
+ - **실행 실패(error)** — 스킬 미설치·도구 부재·파일 없음·명령 비정상 종료는 stuck 이 아니다(해시 비교로 안 잡힌다). 같은 방식으로 재시도하지 않고 루프를 종료한다: `confirm` 이면 원인을 제시하고 조치/건너뛰기/중단을 묻고, `autonomous` 이면 `loop-ledger.js inbox <name> fail "<원인>"` 기록 후 다음 독립 단위로. 실행 실패도 완료로 기록하지 않는다 (SSOT: `vibe/rules/loop-contract.md` 실행 실패 절)
185
186
 
186
187
  ### Phase 5: 종료 보고
187
188
 
@@ -6,7 +6,8 @@
6
6
  ## Phase 4: Compile Gate
7
7
 
8
8
  ```
9
- No round cap. Loop until compile succeeds (or stuck → ask user).
9
+ No round cap. Loop until compile succeeds (or stuck → end loop; automationLevel decides
10
+ whether the user is asked — see Termination below).
10
11
 
11
12
  0. Capture baseline (before Phase 3): record existing tsc + build errors
12
13
  → Phase 4 only fixes NEW errors
@@ -33,7 +34,8 @@ Termination:
33
34
  **⛔ Skipping Phase 5 makes the entire clone "incomplete".**
34
35
 
35
36
  ```
36
- No round cap. Loop until P1=0 (or stuck → ask user).
37
+ No round cap. Loop until P1=0 (or stuck → end loop; automationLevel decides whether the
38
+ user is asked — see Termination below).
37
39
  Infrastructure: src/infra/lib/browser/ (Puppeteer + CDP) — same as figma Phase 6.
38
40
 
39
41
  1. Render scaffolded page in dev server at matching viewport
@@ -52,7 +54,8 @@ Narrowing scope:
52
54
  Termination:
53
55
  ✅ P1=0 AND no new findings → complete
54
56
  ⚠️ Stuck: same findings → ask user (resolve / proceed / abort)
55
- automationLevel: autonomous → on stuck, record TODO without prompting and complete
57
+ automationLevel: autonomous → on stuck, record TODO without prompting and end the loop
58
+ as `stuck` — never record unresolved P1 as complete (SSOT: vibe/rules/loop-contract.md)
56
59
 
57
60
  Responsive: after MO verification → change viewport → repeat against PC screenshot
58
61
  Post-merge (Phase 3C): re-run at BOTH viewports (375×812 vs mo/screenshot.png,
@@ -69,6 +69,9 @@ node -e "import('{{VIBE_PATH_URL}}/node_modules/@su-record/vibe/dist/tools/index
69
69
  `status: paused` 루프는 즉시 종료. 실행 순서는 **전부 의무**이며 생략 불가:
70
70
 
71
71
  ```
72
+ 0. ANCHOR node "$HOOKS_DIR/loop-ledger.js" anchor [feature]
73
+ → missing[] 이 비어 있지 않으면 없는 아티팩트를 기억으로 메우지 않는다.
74
+ 재고정 실패를 인박스에 남기고 종료한다.
72
75
  1. 검증 validateLoopDefinition 통과 확인 (위 design 3의 명령) — 실패 시 인박스에 기록 후 종료
73
76
  2. 시작 기록 node "$HOOKS_DIR/loop-ledger.js" start <name>
74
77
  3. DISCOVER 정의의 discover 지시 실행 → 일거리 목록 산출
@@ -84,10 +87,10 @@ node -e "import('{{VIBE_PATH_URL}}/node_modules/@su-record/vibe/dist/tools/index
84
87
  · tests: 정의의 test_command 실행 → exit 0 만 성공
85
88
  · none: 판정 생략(보고만). "코드를 보니 잘 된 것 같다"는 판정이 아니다.
86
89
  7. 종료 기록 node "$HOOKS_DIR/loop-ledger.js" end <name> <ok|fail|stuck> "<한 줄 요약>"
87
- 8. 인박스 $INBOX 상단에 결과 블록 prepend:
88
- ## <name> <ISO 시각> <ok|fail|stuck>
89
- - 발견: N건 / 처리: M건 / 검증: <기준과 결과>
90
- - 리뷰 필요: <항목들 없으면 "없음">
90
+ 8. 인박스 node "$HOOKS_DIR/loop-ledger.js" inbox <name> <ok|fail|stuck> \
91
+ "발견: N건 / 처리: M건 / 검증: <기준과 결과>" \
92
+ "리뷰 필요: <항목들 없으면 없음>"
93
+ 블록 형식과 최신순 정렬은 명령이 보장한다. 손으로 마크다운을 쓰지 않는다.
91
94
  ```
92
95
 
93
96
  **금지**: `git push`, `gh pr merge`, `npm publish`, 버전 범프, 릴리즈 — 루프는 커밋까지만 가며(auto-commit verify 게이트 통과 시), 그 이상은 인박스를 본 사람이 한다.
@@ -79,7 +79,11 @@ user-invocable: true
79
79
 
80
80
  - **P1 = 0 means MERGE READY** — mergeable even with remaining P2/P3
81
81
  - **P1 = 0 after auto-fix means DONE** — record P2 auto-fix failures as TODO and stop
82
- - **Final P1 list unchanged after Review Debate → DONE** — no new findings = converged
82
+ - **P1 = 0 AND final P1 list unchanged after Review Debate → DONE** — converged
83
+ - **P1 > 0 AND final P1 list unchanged → STUCK, not DONE** — 같은 발견이 2회 연속이면
84
+ 루프는 종료하되 **완료로 기록하지 않는다**. `confirm` 이면 사용자에게 묻고, `autonomous`
85
+ 이면 TODO 로 남긴다 (SSOT: `vibe/rules/loop-contract.md` stuck 절). "목록이 안 바뀌었으니
86
+ 수렴했다" 는 남은 P1 을 완료로 포장하는 것이다
83
87
 
84
88
  ### Anti-Patterns (FORBIDDEN)
85
89
 
@@ -34,7 +34,7 @@ Scenario verification failed
34
34
 
35
35
  **Stakes 프로파일 (SSOT: `vibe/rules/loop-contract.md` Stakes 표):**
36
36
  - `demo`/`prototype` → max_iterations 1, 리뷰 1패스, **검증 스크립트 신규 생성 금지** — 검증은 기존 테스트 러너·브라우저 게이트만 사용한다. 새 verify_*.py / 검증 전용 스크립트 파일을 만들지 않는다.
37
- - JUDGE 검증 산출물 절제 (모든 stakes): 이번 feature 신규 검증 코드 바이트 합이 신규 구현 코드 바이트 합을 초과하면 (`git diff --numstat` 기준) P2 경고를 run-ledger 기록한다. advisory — 게이트 통과 여부는 불변.
37
+ - JUDGE 검증 산출물 절제 (모든 stakes): 이번 feature 신규 검증 코드 수가 신규 구현 코드 수를 초과하면 (`git diff --numstat` 기준) **최종 보고에 P2 경고 1줄**을 적는다. run-ledger 에는 적재하지 않는다 (경고 필드 없음). advisory — 게이트 통과 여부는 불변.
38
38
 
39
39
  ---
40
40
 
@@ -173,7 +173,7 @@ node -e "import('{{VIBE_PATH_URL}}/node_modules/@su-record/vibe/dist/tools/index
173
173
 
174
174
  > Default SPEC path is `.vibe/specs/<feature>.md`. `status === 'empty'` must be treated as failed/not-applicable — never as 100% pass.
175
175
 
176
- JUDGE: `coveragePercent === 100` → 루프 종료. stuck(연속 2회 동일 커버리지) → automationLevel confirm이면 사용자 질문; autonomous이면 TODO + done.
176
+ JUDGE: `coveragePercent === 100` → 루프 종료. stuck(연속 2회 동일 발견 해시 — `loop-ledger.js check-stuck`; 커버리지 수치가 아니라 발견으로 판정한다) → **어느 automationLevel 에서도 루프를 종료한다**; confirm이면 사용자 질문, autonomous이면 TODO 기록 후 다음 독립 단위로. 미달 커버리지를 완료로 기록하지 않는다 (SSOT: `vibe/rules/loop-contract.md`).
177
177
 
178
178
  ---
179
179
 
@@ -81,7 +81,7 @@ All reference documents are stored globally and specified in `.vibe/config.json`
81
81
  - **DRY**: Don't Repeat Yourself
82
82
  - **SRP**: Single Responsibility Principle
83
83
  - **YAGNI**: You Aren't Gonna Need It
84
- - **Functions ≤30 lines** (recommended), ≤50 lines (allowed)
84
+ - **Functions ≤50 lines** (SSOT: `CLAUDE.md` Complexity Limits)
85
85
  - **Cyclomatic Complexity ≤10**
86
86
  - **Cognitive Complexity ≤15**
87
87
 
@@ -21,15 +21,35 @@
21
21
  Model Judge(advisory-only): 발견을 제안하지만 완료 권한 없음
22
22
  Human Taste(release-only): UX·브랜드·제품 감각을 판단하지만 루프 완료 권한 없음
23
23
  RECORD run-ledger + `.vibe/runs/{run-id}/evidence.json` + loop-history.jsonl
24
- → 종료(EXIT): 게이트 전부 통과 │ stuck │ max_iterations │ 예산 상한
24
+ → 종료(EXIT): 게이트 전부 통과 │ stuck │ max_iterations │ 예산 상한 │ 실행 실패(error)
25
25
  ```
26
26
 
27
27
  ### ANCHOR가 컨텍스트 오염 방어인 이유
28
28
  루프 상태는 컨텍스트가 아니라 디스크에 산다. 매 회전이 아티팩트에서 다시 시작하므로 컨텍스트가 오염되거나 compact로 소실돼도 루프는 깨지지 않으며, 회전마다 fresh 컨텍스트(서브에이전트)로 돌려도 된다.
29
29
 
30
+ **재고정은 명령으로 한다** — 모델의 기억이 아니라 디스크가 답한다:
31
+
32
+ ```bash
33
+ node "$HOOKS_DIR/loop-ledger.js" anchor [feature]
34
+ # → { feature, spec, scope, ledger, latestInbox, missing[] }
35
+ ```
36
+
37
+ `missing` 이 비어 있지 않으면 그 회전은 재고정에 실패한 것이다 — 없는 아티팩트를 기억으로 메우지 않는다. JUDGE·RECORD·stuck 이 전부 명령으로 판정되는데 ANCHOR만 산문 지시로 남아 있으면, 정작 오염 방어의 근거가 되는 단계가 가장 약해진다.
38
+
30
39
  ### Judge 권한 경계
31
40
  종료 권한은 테스트 exit code·run-ledger·RTM 같은 **결정론적 Judge**에만 있다. Model Judge는 누락·모순·위험을 발견하는 보조 수단이며, 발견을 테스트나 관측 가능한 기준으로 내리기 전에는 차단 근거가 아니다. Human Taste는 공개·배포 시점의 사람 판단으로 남고 루프의 완료 상태를 변경하지 않는다.
32
41
 
42
+ #### P1 은 출처가 둘이다 — exit 기준은 이를 구분한다
43
+
44
+ "P1" 이 가리키는 것이 두 가지이고, 위 권한 경계는 그중 하나에만 적용된다.
45
+
46
+ | 출처 | 예 | 성격 | exit 게이트 |
47
+ |---|---|---|---|
48
+ | **측정된 P1** | clone Phase 5 `pixelmatch diffRatio > 0.05`, computed CSS delta > 2px, contract drift, 테스트 실패 | 결정론 | **차단한다** — 게이트 통과 = 측정 P1 0 |
49
+ | **판정된 P1** | `vibe.review` 리뷰어 findings | Model Judge | **단독으로 차단하지 않는다** — 테스트·관측 기준으로 내려야 게이트가 된다 |
50
+
51
+ 판정된 P1 이 남았는데 내릴 기준이 없으면, 그것은 게이트 실패가 아니라 **인박스로 가는 리뷰 항목**이다. 동일한 판정 P1 이 2회 연속 반복되면 stuck 이며 — 완료가 아니다 (아래 stuck 절).
52
+
33
53
  ### stuck (결정론)
34
54
  연속 2회 회전의 발견(discover/findings) 해시가 동일 → **그 루프는 종료한다** (`loop-ledger.js check-stuck`이 판정·기록). "다시 해보면 될 것 같다"는 모델 판단으로 무시 금지.
35
55
 
@@ -42,12 +62,25 @@
42
62
 
43
63
  > `autonomous` 의 "계속" 은 **stuck 난 루프를 더 돌린다는 뜻이 아니다** — 2회 연속 동일 발견은 정의상 재시도가 무의미하다. 같은 목표를 붙잡지 않고 다음 단위로 넘어간다는 뜻이며, 미달은 TODO/인박스에 남는다. 미달 상태를 **완료로 기록하지 않는다.**
44
64
 
65
+ ### 실행 실패 (error) — stuck 과 다른 종료 사유
66
+
67
+ stuck 은 **같은 발견이 반복되는** 상태다. 스킬이 로드되지 않거나, 도구가 없거나, 파일이 없거나, 명령이 비정상 종료하는 것은 stuck 이 아니라 **실행 실패**이며 해시 비교로는 잡히지 않는다. 재시도 대상도 아니다 — 환경이 바뀌지 않는 한 결과가 같다.
68
+
69
+ | | 루프 | 사람에게 질문 | 그 다음 |
70
+ |---|---|---|---|
71
+ | `confirm` | 종료 | **한다** (원인 제시 + 조치 요청 / 건너뛰기 / 중단) | 사용자 응답에 따름 |
72
+ | `autonomous` | 종료 | 하지 않음 | 인박스에 원인 기록 후 **다음 독립 단위로** |
73
+
74
+ - 실패한 단계를 **같은 방식으로 재시도하지 않는다.** 재시도가 의미 있으려면 무엇이 달라지는지 말할 수 있어야 한다 (`vibe.review` 의 escalation ladder 가 그 예 — 재시도 1회 → 다른 하네스 1회 → TODO).
75
+ - 실행 실패도 **완료가 아니다.** stuck 과 동일하게, 미달 상태를 완료로 기록하지 않는다.
76
+ - 원인은 인박스에 남긴다: `loop-ledger.js inbox <name> fail "<원인 한 줄>"`.
77
+
45
78
  ## 파라미터 (기본값)
46
79
 
47
80
  | 파라미터 | 기본 | 의미 |
48
81
  |---|---|---|
49
82
  | `max_iterations` | 10 | 회전 상한. 도달 시 잔여를 인박스로 이월 |
50
- | `exit` | 게이트 통과 (P1=0 ∧ verifyPassed) | 종료 기준. coverage 100% 등으로 상향 가능 |
83
+ | `exit` | 게이트 통과 (**측정된** P1=0 ∧ verifyPassed) | 종료 기준. coverage 100% 등으로 상향 가능. 판정된 P1 은 위 Judge 권한 경계 표를 따른다 |
51
84
  | `--interactive` | off | 단계별 확인 모드 (회전마다 사람 승인 — 과거의 기본값) |
52
85
  | `--max-iter N` | — | 회전 상한 명시 (N=1이면 1회 시도) |
53
86
  | `automationLevel` | `confirm` | `confirm`(SPEC·stuck에서 질문) / `autonomous`(질문 없이 TODO 기록 후 다음 단위로, 비대화형) — `.vibe/config.json`. **어느 값에서도 stuck 은 루프를 종료한다** (위 stuck 절) |
@@ -69,7 +102,11 @@
69
102
 
70
103
  ### JUDGE 검증 산출물 절제 (모든 stakes 공통)
71
104
 
72
- JUDGE는 이번 feature의 **신규 생성 파일** 기준으로 검증 코드 총량(테스트·검증 스크립트)과 구현 코드 총량을 `git diff --numstat` 비교한다. 검증 코드 바이트 합 > 구현 코드 바이트 합이면 **P2 경고**를 run-ledger 에 기록한다 (restraint 원칙의 프로세스 적용). 경고는 advisory — 게이트 통과 여부를 바꾸지 않는다.
105
+ 검증은 실패 비용보다 싸야 한다 검증 코드가 구현 코드보다 커지면 루프는 남는 장사가 아니다.
106
+
107
+ JUDGE는 이번 feature의 **신규 생성 파일** 기준으로 검증 코드 총량(테스트·검증 스크립트)과 구현 코드 총량을 `git diff --numstat` 로 비교하고, 검증 코드 줄 수가 구현 코드 줄 수를 넘으면 **최종 보고에 P2 경고 1줄**을 적는다 (restraint 원칙의 프로세스 적용).
108
+
109
+ > ⚠️ 이 경고는 **보고용이며 어디에도 적재되지 않는다.** run-ledger 스키마(`runId`·`runStarted`·`runFeature`·`verifyPassed`·`verifyAt`·`stopWarned`·`verifyRequired`·`verifyRequiredReason`)에는 경고 필드가 없다 — 기록을 지시하면 갈 곳 없는 지시가 된다. 게이트 통과 여부를 바꾸지 않는다.
73
110
 
74
111
  ## 금지 (루프 권한 경계)
75
112
 
@@ -28,7 +28,7 @@ const typeSafety = {
28
28
  ```typescript
29
29
  const codeStructure = {
30
30
  singleResponsibility: true, // ✅ Single Responsibility Principle
31
- functionUnder30Lines: true, // ✅ Functions ≤30 lines (recommended), 50 allowed
31
+ functionUnder50Lines: true, // ✅ Functions ≤50 lines
32
32
  maxNesting3Levels: true, // ✅ Max nesting 3 levels
33
33
  cyclomaticComplexity: 10, // ✅ Cyclomatic complexity ≤ 10
34
34
  cognitiveComplexity: 15, // ✅ Cognitive complexity ≤ 15
@@ -171,7 +171,7 @@ const bundleOptimization = {
171
171
 
172
172
  ```text
173
173
  [ ] Follow Single Responsibility Principle
174
- [ ] Keep function length ≤30 lines (max 50)
174
+ [ ] Keep function length ≤50 lines
175
175
  [ ] Nesting depth ≤3 levels
176
176
  [ ] Extract magic numbers to constants
177
177
  [ ] Ensure type safety
@@ -266,7 +266,7 @@ npm run format:check
266
266
  ```text
267
267
  ✅ Only modified requested scope?
268
268
  ✅ No any types?
269
- ✅ Functions ≤30 lines? (max 50)
269
+ ✅ Functions ≤50 lines?
270
270
  ✅ Nesting ≤3 levels?
271
271
  ✅ Error handling implemented?
272
272
  ✅ Magic numbers extracted to constants?
@@ -99,7 +99,7 @@ All reference documents are stored globally and specified in `.vibe/config.json`
99
99
  - **DRY**: Don't Repeat Yourself
100
100
  - **SRP**: Single Responsibility Principle
101
101
  - **YAGNI**: You Aren't Gonna Need It
102
- - **Functions ≤30 lines** (recommended), ≤50 lines (allowed)
102
+ - **Functions ≤50 lines** (SSOT: `CLAUDE.md` Complexity Limits)
103
103
  - **Cyclomatic Complexity ≤10**
104
104
  - **Cognitive Complexity ≤15**
105
105