@su-record/vibe 3.1.0 → 3.2.1

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 (183) hide show
  1. package/CLAUDE.md +2 -2
  2. package/README.en.md +9 -6
  3. package/README.md +9 -4
  4. package/dist/__tests__/wiring-integrity.test.d.ts +2 -0
  5. package/dist/__tests__/wiring-integrity.test.d.ts.map +1 -0
  6. package/dist/__tests__/wiring-integrity.test.js +160 -0
  7. package/dist/__tests__/wiring-integrity.test.js.map +1 -0
  8. package/dist/cli/commands/info.d.ts.map +1 -1
  9. package/dist/cli/commands/info.js +2 -1
  10. package/dist/cli/commands/info.js.map +1 -1
  11. package/dist/cli/commands/init.d.ts.map +1 -1
  12. package/dist/cli/commands/init.js +4 -2
  13. package/dist/cli/commands/init.js.map +1 -1
  14. package/dist/cli/design/design-md-parser.test.js +2 -2
  15. package/dist/cli/design/design-md-parser.test.js.map +1 -1
  16. package/dist/cli/index.js +102 -128
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/cli/postinstall/claude-agents.d.ts.map +1 -1
  19. package/dist/cli/postinstall/claude-agents.js +0 -5
  20. package/dist/cli/postinstall/claude-agents.js.map +1 -1
  21. package/dist/cli/postinstall/constants.d.ts +6 -0
  22. package/dist/cli/postinstall/constants.d.ts.map +1 -1
  23. package/dist/cli/postinstall/constants.js +42 -60
  24. package/dist/cli/postinstall/constants.js.map +1 -1
  25. package/dist/cli/postinstall/inline-skills.js +2 -2
  26. package/dist/cli/postinstall/inline-skills.js.map +1 -1
  27. package/dist/cli/postinstall/main.d.ts.map +1 -1
  28. package/dist/cli/postinstall/main.js +2 -0
  29. package/dist/cli/postinstall/main.js.map +1 -1
  30. package/dist/cli/postinstall.d.ts +6 -1
  31. package/dist/cli/postinstall.d.ts.map +1 -1
  32. package/dist/cli/postinstall.js +6 -1
  33. package/dist/cli/postinstall.js.map +1 -1
  34. package/dist/cli/setup/GlobalInstaller.d.ts +1 -15
  35. package/dist/cli/setup/GlobalInstaller.d.ts.map +1 -1
  36. package/dist/cli/setup/GlobalInstaller.js +16 -115
  37. package/dist/cli/setup/GlobalInstaller.js.map +1 -1
  38. package/dist/cli/setup/GlobalInstaller.test.d.ts +2 -0
  39. package/dist/cli/setup/GlobalInstaller.test.d.ts.map +1 -0
  40. package/dist/cli/setup/GlobalInstaller.test.js +16 -0
  41. package/dist/cli/setup/GlobalInstaller.test.js.map +1 -0
  42. package/dist/cli/setup/ProjectSetup.js +2 -2
  43. package/dist/cli/setup/ProjectSetup.js.map +1 -1
  44. package/dist/cli/setup/Provisioner.d.ts.map +1 -1
  45. package/dist/cli/setup/Provisioner.js +23 -7
  46. package/dist/cli/setup/Provisioner.js.map +1 -1
  47. package/dist/cli/setup.d.ts +1 -1
  48. package/dist/cli/setup.d.ts.map +1 -1
  49. package/dist/cli/setup.js +1 -1
  50. package/dist/cli/setup.js.map +1 -1
  51. package/dist/cli/utils/cli-detector.d.ts +3 -3
  52. package/dist/cli/utils/cli-detector.d.ts.map +1 -1
  53. package/dist/cli/utils/cli-detector.js +7 -27
  54. package/dist/cli/utils/cli-detector.js.map +1 -1
  55. package/dist/cli/utils/cli-detector.test.js +3 -2
  56. package/dist/cli/utils/cli-detector.test.js.map +1 -1
  57. package/dist/infra/lib/SkillRepository.js +1 -1
  58. package/dist/infra/lib/evolution/__tests__/integration.test.js +0 -42
  59. package/dist/infra/lib/evolution/__tests__/integration.test.js.map +1 -1
  60. package/dist/infra/lib/llm-availability.d.ts.map +1 -1
  61. package/dist/infra/lib/llm-availability.js +2 -10
  62. package/dist/infra/lib/llm-availability.js.map +1 -1
  63. package/dist/infra/lib/llm-availability.test.js +3 -2
  64. package/dist/infra/lib/llm-availability.test.js.map +1 -1
  65. package/dist/infra/lib/utils.d.ts +6 -0
  66. package/dist/infra/lib/utils.d.ts.map +1 -1
  67. package/dist/infra/lib/utils.js +33 -0
  68. package/dist/infra/lib/utils.js.map +1 -1
  69. package/dist/tools/convention/validateCodeQuality.d.ts +1 -0
  70. package/dist/tools/convention/validateCodeQuality.d.ts.map +1 -1
  71. package/dist/tools/convention/validateCodeQuality.js +3 -2
  72. package/dist/tools/convention/validateCodeQuality.js.map +1 -1
  73. package/dist/tools/index.d.ts +2 -2
  74. package/dist/tools/index.d.ts.map +1 -1
  75. package/dist/tools/index.js +2 -0
  76. package/dist/tools/index.js.map +1 -1
  77. package/dist/tools/spec/executionPacket.d.ts +101 -0
  78. package/dist/tools/spec/executionPacket.d.ts.map +1 -0
  79. package/dist/tools/spec/executionPacket.js +394 -0
  80. package/dist/tools/spec/executionPacket.js.map +1 -0
  81. package/dist/tools/spec/executionPacket.test.d.ts +2 -0
  82. package/dist/tools/spec/executionPacket.test.d.ts.map +1 -0
  83. package/dist/tools/spec/executionPacket.test.js +360 -0
  84. package/dist/tools/spec/executionPacket.test.js.map +1 -0
  85. package/dist/tools/spec/index.d.ts +2 -0
  86. package/dist/tools/spec/index.d.ts.map +1 -1
  87. package/dist/tools/spec/index.js +2 -0
  88. package/dist/tools/spec/index.js.map +1 -1
  89. package/dist/tools/spec/specGenerator.d.ts +12 -0
  90. package/dist/tools/spec/specGenerator.d.ts.map +1 -1
  91. package/dist/tools/spec/specGenerator.js +96 -14
  92. package/dist/tools/spec/specGenerator.js.map +1 -1
  93. package/dist/tools/spec/specGenerator.test.d.ts +2 -0
  94. package/dist/tools/spec/specGenerator.test.d.ts.map +1 -0
  95. package/dist/tools/spec/specGenerator.test.js +130 -0
  96. package/dist/tools/spec/specGenerator.test.js.map +1 -0
  97. package/hooks/scripts/__tests__/.vibe/command-log.txt +3 -3
  98. package/hooks/scripts/__tests__/clone-behaviors.test.js +33 -1
  99. package/hooks/scripts/__tests__/code-check-memory-write.test.js +81 -0
  100. package/hooks/scripts/__tests__/llm-orchestrate-antigravity.test.js +5 -2
  101. package/hooks/scripts/__tests__/run-ledger-verify-required.test.js +7 -2
  102. package/hooks/scripts/__tests__/run-ledger.test.js +161 -10
  103. package/hooks/scripts/__tests__/step-counter.test.js +21 -0
  104. package/hooks/scripts/__tests__/utils-npm-root.test.js +5 -4
  105. package/hooks/scripts/auto-format.js +2 -11
  106. package/hooks/scripts/auto-test.js +2 -11
  107. package/hooks/scripts/clone-extract.js +146 -10
  108. package/hooks/scripts/code-check.js +27 -55
  109. package/hooks/scripts/lib/glob.js +32 -0
  110. package/hooks/scripts/lib/hook-context.js +19 -2
  111. package/hooks/scripts/lib/run-ledger.js +123 -21
  112. package/hooks/scripts/post-edit.js +2 -3
  113. package/hooks/scripts/prompt-dispatcher.js +1 -1
  114. package/hooks/scripts/scope-guard.js +3 -42
  115. package/hooks/scripts/sentinel-guard.js +5 -20
  116. package/hooks/scripts/session-start.js +2 -2
  117. package/hooks/scripts/step-counter.js +35 -57
  118. package/hooks/scripts/verify-ledger.js +23 -3
  119. package/package.json +2 -3
  120. package/skills/clone/SKILL.md +10 -216
  121. package/skills/clone/references/capture-rules.md +48 -0
  122. package/skills/clone/references/legal-and-error-recovery.md +40 -0
  123. package/skills/clone/references/refine-rules.md +61 -0
  124. package/skills/clone/references/scaffold-phases.md +59 -0
  125. package/skills/clone/references/setup-and-layout.md +30 -0
  126. package/skills/docs/SKILL.md +39 -4
  127. package/skills/docs/references/api-docs-changelog.md +34 -0
  128. package/skills/docs/references/codemaps-output.md +23 -0
  129. package/{agents/diagrammer.md → skills/docs/references/diagram-spec.md} +2 -15
  130. package/skills/exec-plan/SKILL.md +1 -1
  131. package/skills/exec-plan/templates/plan.md +1 -1
  132. package/skills/handoff/SKILL.md +3 -3
  133. package/skills/handoff/templates/handoff.md +1 -1
  134. package/skills/spec/SKILL.md +7 -0
  135. package/skills/vibe/SKILL.md +3 -2
  136. package/skills/vibe.analyze/SKILL.md +12 -249
  137. package/skills/vibe.analyze/references/output-templates.md +188 -0
  138. package/skills/vibe.analyze/references/quality-gate.md +93 -0
  139. package/skills/vibe.clone/SKILL.md +14 -1
  140. package/skills/vibe.continue/SKILL.md +32 -0
  141. package/skills/vibe.design/SKILL.md +23 -1
  142. package/skills/vibe.design/references/ui-preview.md +20 -0
  143. package/skills/vibe.docs/SKILL.md +4 -2
  144. package/skills/vibe.figma/SKILL.md +33 -260
  145. package/skills/vibe.figma/references/branch-phases.md +113 -0
  146. package/skills/vibe.figma/references/state-schema.md +42 -0
  147. package/skills/vibe.figma/references/step-algorithms.md +204 -0
  148. package/skills/vibe.image/SKILL.md +62 -0
  149. package/skills/vibe.image/references/image-generation-examples.md +39 -0
  150. package/skills/vibe.reason/SKILL.md +3 -183
  151. package/skills/vibe.reason/references/output-format-template.md +57 -0
  152. package/skills/vibe.reason/references/quality-rubrics.md +94 -0
  153. package/skills/vibe.reason/references/worked-example.md +45 -0
  154. package/skills/vibe.review/SKILL.md +13 -348
  155. package/skills/vibe.review/references/boundary-check.md +25 -0
  156. package/skills/vibe.review/references/output-template.md +74 -0
  157. package/skills/vibe.review/references/quality-gate.md +84 -0
  158. package/skills/vibe.review/references/race-mode.md +117 -0
  159. package/skills/vibe.review/references/worked-examples.md +94 -0
  160. package/skills/vibe.run/SKILL.md +29 -6
  161. package/skills/vibe.trace/SKILL.md +3 -3
  162. package/skills/vibe.verify/SKILL.md +10 -4
  163. package/vibe/rules/loop-contract.md +7 -3
  164. package/vibe/templates/claudemd-template.md +1 -1
  165. package/vibe/templates/spec-template.md +32 -5
  166. package/agents/acceptance-tester.md +0 -56
  167. package/agents/documenter.md +0 -43
  168. package/agents/figma/figma-engineer.md +0 -76
  169. package/dist/cli/postinstall/index.d.ts +0 -23
  170. package/dist/cli/postinstall/index.d.ts.map +0 -1
  171. package/dist/cli/postinstall/index.js +0 -23
  172. package/dist/cli/postinstall/index.js.map +0 -1
  173. package/dist/cli/setup/index.d.ts +0 -9
  174. package/dist/cli/setup/index.d.ts.map +0 -1
  175. package/dist/cli/setup/index.js +0 -12
  176. package/dist/cli/setup/index.js.map +0 -1
  177. package/hooks/scripts/__tests__/.vibe/memories/memories.db +0 -0
  178. package/hooks/scripts/__tests__/.vibe/memories/memories.db-shm +0 -0
  179. package/hooks/scripts/__tests__/.vibe/memories/memories.db-wal +0 -0
  180. package/hooks/scripts/evolution-engine.js +0 -91
  181. package/hooks/scripts/hud-status.js +0 -321
  182. package/hooks/scripts/skill-injector.js +0 -83
  183. package/skills/vibe.utils/SKILL.md +0 -415
@@ -14,6 +14,7 @@ import { getToolsBaseUrl, PROJECT_DIR, readProjectConfig } from './utils.js';
14
14
  import { readFileSync } from 'fs';
15
15
  import path from 'path';
16
16
  import { buildCliCtx, isDirectRun } from './lib/hook-context.js';
17
+ import { globToRegExp } from './lib/glob.js';
17
18
 
18
19
  const BASE_URL = getToolsBaseUrl();
19
20
 
@@ -42,35 +43,6 @@ const DEFAULT_CONSOLE_ALLOW_GLOBS = [
42
43
  '**/__tests__/**',
43
44
  ];
44
45
 
45
- /**
46
- * 경량 glob → RegExp 변환 (scope-guard와 동일 알고리즘, 독립 복사본).
47
- * @param {string} glob
48
- * @returns {RegExp}
49
- */
50
- function globToRegExp(glob) {
51
- const normalized = glob.replace(/\\/g, '/');
52
- let out = '';
53
- for (let i = 0; i < normalized.length; i++) {
54
- const c = normalized[i];
55
- if (c === '*') {
56
- if (normalized[i + 1] === '*') {
57
- out += '.*';
58
- i++;
59
- if (normalized[i + 1] === '/') i++;
60
- } else {
61
- out += '[^/]*';
62
- }
63
- } else if (c === '?') {
64
- out += '[^/]';
65
- } else if ('.+^$()|{}[]\\'.includes(c)) {
66
- out += '\\' + c;
67
- } else {
68
- out += c;
69
- }
70
- }
71
- return new RegExp('^' + out + '$');
72
- }
73
-
74
46
  /**
75
47
  * .vibe/config.json의 qualityCheck.consoleAllow 글로브 목록 로드.
76
48
  * 기본 글로브와 병합하여 반환.
@@ -105,19 +77,12 @@ function isConsoleAllowed(filePath) {
105
77
  }
106
78
 
107
79
  /**
108
- * hook input에서 수정된 파일 경로 추출.
80
+ * hook ctx에서 수정된 파일 경로 추출.
109
81
  * @param {object} ctx
110
82
  * @returns {string[]}
111
83
  */
112
84
  function getModifiedFiles(ctx) {
113
- try {
114
- const parsed = ctx.payload || (ctx.hookInput ? JSON.parse(ctx.hookInput) : null);
115
- const filePath = parsed?.tool_input?.file_path || parsed?.tool_input?.path;
116
- return filePath ? [filePath] : [];
117
- } catch {
118
- // ignore
119
- }
120
- return [];
85
+ return ctx.filePath ? [ctx.filePath] : [];
121
86
  }
122
87
 
123
88
  /**
@@ -217,7 +182,7 @@ function runDetectors(filePath) {
217
182
  /**
218
183
  * in-process 진입점 — 품질 검사 + 관찰 캡처.
219
184
  * findings 배열을 반환한다 (디스패처가 수집해 additionalContext에 주입).
220
- * @param {{ payload: object|null, hookInput: string|null }} ctx
185
+ * @param {{ filePath: string }} ctx
221
186
  * @returns {Promise<{ exitCode: number, findings: string[] }>}
222
187
  */
223
188
  export async function run(ctx) {
@@ -225,9 +190,10 @@ export async function run(ctx) {
225
190
  const files = getModifiedFiles(ctx);
226
191
  if (files.length === 0) return { exitCode: 0, findings };
227
192
 
228
- // 1. 하드룰 탐지기 실행 (changed file only)additionalContext 주입만.
229
- // 커밋 게이트(verifyRequired)는 태우지 않는다: 정규식 오탐 하나가
230
- // auto-commit을 차단하는 결합은 제거됨 (harness-review-2026-07-01).
193
+ // 1. 하드룰 탐지기 실행 (changed file only, regex only 동적 import 없음) —
194
+ // additionalContext 주입만. 커밋 게이트(verifyRequired)는 태우지 않는다:
195
+ // 정규식 오탐 하나가 auto-commit을 차단하는 결합은 제거됨
196
+ // (harness-review-2026-07-01).
231
197
  try {
232
198
  const { p1 } = runDetectors(files[0]);
233
199
  for (const msg of p1) findings.push(msg);
@@ -235,6 +201,10 @@ export async function run(ctx) {
235
201
  // 탐지기 실패 → fail-open, 계속 진행
236
202
  }
237
203
 
204
+ // validateCodeQuality가 처리하지 않는 확장자는 무거운 동적 import
205
+ // (convention/index.js·memory/index.js — 모듈 그래프 + SQLite 오픈) 전에 조기 반환.
206
+ if (!CODE_EXT_RE.test(files[0])) return { exitCode: 0, findings };
207
+
238
208
  // 2. validateCodeQuality 호출 (P1/P2 필터)
239
209
  try {
240
210
  const module = await import(`${BASE_URL}convention/index.js`);
@@ -249,19 +219,21 @@ export async function run(ctx) {
249
219
  // Silently continue on check failure
250
220
  }
251
221
 
252
- // 3. 관찰 자동 캡처
253
- try {
254
- const memModule = await import(`${BASE_URL}memory/index.js`);
255
- const { type, title } = classifyObservation(files);
256
-
257
- await memModule.addObservation({
258
- type,
259
- title: `${title}: ${files.map(f => f.split(/[\\/]/).pop()).join(', ')}`,
260
- filesModified: files,
261
- projectPath: PROJECT_DIR,
262
- });
263
- } catch {
264
- // 관찰 캡처 실패해도 무시
222
+ // 3. 관찰 자동 캡처 — 위반(findings)이 실제로 있을 때만 SQLite 기록 (clean edit은 write 생략)
223
+ if (findings.length > 0) {
224
+ try {
225
+ const memModule = await import(`${BASE_URL}memory/index.js`);
226
+ const { type, title } = classifyObservation(files);
227
+
228
+ await memModule.addObservation({
229
+ type,
230
+ title: `${title}: ${files.map(f => f.split(/[\\/]/).pop()).join(', ')}`,
231
+ filesModified: files,
232
+ projectPath: PROJECT_DIR,
233
+ });
234
+ } catch {
235
+ // 관찰 캡처 실패해도 무시
236
+ }
265
237
  }
266
238
 
267
239
  return { exitCode: 0, findings };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 경량 glob → RegExp 변환 — code-check.js·scope-guard.js 공용 (중복 제거).
3
+ * - `**` : 경로 구분자 포함 임의 문자열
4
+ * - `*` : 구분자 제외 임의 문자열
5
+ * - `?` : 구분자 제외 한 글자
6
+ * - 기타 정규식 메타문자는 이스케이프
7
+ * @param {string} glob
8
+ * @returns {RegExp}
9
+ */
10
+ export function globToRegExp(glob) {
11
+ const normalized = glob.replace(/\\/g, '/');
12
+ let out = '';
13
+ for (let i = 0; i < normalized.length; i++) {
14
+ const c = normalized[i];
15
+ if (c === '*') {
16
+ if (normalized[i + 1] === '*') {
17
+ out += '.*';
18
+ i++;
19
+ if (normalized[i + 1] === '/') i++; // `**/` → `.*`
20
+ } else {
21
+ out += '[^/]*';
22
+ }
23
+ } else if (c === '?') {
24
+ out += '[^/]';
25
+ } else if ('.+^$()|{}[]\\'.includes(c)) {
26
+ out += '\\' + c;
27
+ } else {
28
+ out += c;
29
+ }
30
+ }
31
+ return new RegExp('^' + out + '$');
32
+ }
@@ -7,11 +7,13 @@
7
7
  * 2. standalone CLI: antigravity-hooks.json / 기존 테스트가 스크립트를
8
8
  * 직접 실행 — isDirectRun()으로 감지해 stdin/argv/env에서 ctx를 구성
9
9
  *
10
- * ctx 형태: { toolName, toolInput, payload, hookInput }
10
+ * ctx 형태: { toolName, toolInput, payload, hookInput, filePath }
11
11
  * - toolName: payload.tool_name 우선, argv 폴백 (현행 각 스크립트와 동일 우선순위)
12
12
  * - toolInput: 문자열 정규화된 tool_input (payload → argv[3] → env.TOOL_INPUT)
13
13
  * - payload: 파싱된 stdin JSON 또는 null
14
14
  * - hookInput: stdin 원문 문자열 (code-check의 HOOK_INPUT 사용처 대체)
15
+ * - filePath: toolInput 에서 뽑은 대상 파일 경로 (file_path → notebook_path →
16
+ * path 우선순위 — 각 스크립트의 인라인 추출 로직과 동일 규약)
15
17
  */
16
18
  import fs from 'fs';
17
19
  import { pathToFileURL } from 'url';
@@ -96,6 +98,21 @@ export function readStdinSync() {
96
98
  return { raw: null, parsed: null, truncated: false };
97
99
  }
98
100
 
101
+ /**
102
+ * toolInput(문자열화된 JSON)에서 대상 파일 경로 추출.
103
+ * 각 훅 스크립트의 인라인 추출과 동일 우선순위: file_path → notebook_path → path.
104
+ * @param {string} toolInput
105
+ * @returns {string}
106
+ */
107
+ function extractFilePath(toolInput) {
108
+ try {
109
+ const parsed = JSON.parse(toolInput || '{}');
110
+ return parsed.file_path || parsed.notebook_path || parsed.path || '';
111
+ } catch {
112
+ return '';
113
+ }
114
+ }
115
+
99
116
  /**
100
117
  * 실행 컨텍스트 구성. 우선순위는 현행 스크립트들과 동일:
101
118
  * toolName: payload.tool_name → argvToolName
@@ -106,7 +123,7 @@ export function buildCtx({ rawInput = null, payload = null, argvToolName = '', a
106
123
  const toolInput = payload?.tool_input !== undefined && payload?.tool_input !== null
107
124
  ? (typeof payload.tool_input === 'string' ? payload.tool_input : JSON.stringify(payload.tool_input))
108
125
  : (argvToolInput || process.env.TOOL_INPUT || '');
109
- return { toolName, toolInput, payload, hookInput: rawInput };
126
+ return { toolName, toolInput, payload, hookInput: rawInput, filePath: extractFilePath(toolInput) };
110
127
  }
111
128
 
112
129
  /** standalone CLI 모드용 ctx — stdin/argv/env에서 구성 */
@@ -2,7 +2,7 @@
2
2
  * Run Ledger — vibe.run 실행 및 vibe.verify 결과 추적.
3
3
  *
4
4
  * 파일 위치: <projectDir>/.vibe/metrics/run-ledger.json
5
- * 형식: { runStarted, runFeature, verifyPassed, verifyAt, stopWarned,
5
+ * 형식: { runId, runStarted, runFeature, verifyPassed, verifyAt, stopWarned,
6
6
  * verifyRequired, verifyRequiredReason }
7
7
  *
8
8
  * 모든 함수는 fail-open (try/catch, 오류 시 null/false 반환).
@@ -10,14 +10,56 @@
10
10
  */
11
11
 
12
12
  import fs from 'fs';
13
- import os from 'os';
13
+ import { randomUUID } from 'crypto';
14
14
  import path from 'path';
15
15
 
16
+ const EVIDENCE_SCHEMA_VERSION = '1.0.0';
17
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
18
+
16
19
  /** 레저 파일 경로 */
17
20
  function ledgerPath(projectDir) {
18
21
  return path.join(projectDir, '.vibe', 'metrics', 'run-ledger.json');
19
22
  }
20
23
 
24
+ function evidencePath(projectDir, runId) {
25
+ if (!UUID_PATTERN.test(runId)) return null;
26
+ const runsDir = path.resolve(projectDir, '.vibe', 'runs');
27
+ const target = path.resolve(runsDir, runId, 'evidence.json');
28
+ return target.startsWith(`${runsDir}${path.sep}`) ? target : null;
29
+ }
30
+
31
+ function writeJsonAtomic(targetPath, data) {
32
+ if (!targetPath) return false;
33
+ const dir = path.dirname(targetPath);
34
+ fs.mkdirSync(dir, { recursive: true });
35
+ const tmp = path.join(dir, `.${path.basename(targetPath)}.${process.pid}.${Date.now()}.tmp`);
36
+ try {
37
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
38
+ fs.renameSync(tmp, targetPath);
39
+ return true;
40
+ } catch {
41
+ try { fs.rmSync(tmp, { force: true }); } catch { /* fail-open */ }
42
+ return false;
43
+ }
44
+ }
45
+
46
+ function withLedgerLock(projectDir, operation) {
47
+ const lockPath = path.join(projectDir, '.vibe', 'metrics', 'run-ledger.lock');
48
+ let descriptor;
49
+ try {
50
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
51
+ descriptor = fs.openSync(lockPath, 'wx');
52
+ return operation();
53
+ } catch {
54
+ return false;
55
+ } finally {
56
+ if (descriptor !== undefined) {
57
+ fs.closeSync(descriptor);
58
+ try { fs.rmSync(lockPath, { force: true }); } catch { /* fail-open */ }
59
+ }
60
+ }
61
+ }
62
+
21
63
  /**
22
64
  * 레저 파일 읽기.
23
65
  * @param {string} projectDir
@@ -41,12 +83,61 @@ export function readLedger(projectDir) {
41
83
  */
42
84
  function writeLedger(projectDir, data) {
43
85
  try {
44
- const p = ledgerPath(projectDir);
45
- fs.mkdirSync(path.dirname(p), { recursive: true });
46
- const tmp = path.join(os.tmpdir(), `run-ledger-${process.pid}-${Date.now()}.tmp`);
47
- fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
48
- fs.renameSync(tmp, p);
49
- return true;
86
+ return writeJsonAtomic(ledgerPath(projectDir), data);
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ function verificationResults(results) {
93
+ if (Array.isArray(results)) {
94
+ return results
95
+ .filter(item => item
96
+ && typeof item === 'object'
97
+ && typeof item.command === 'string'
98
+ && Number.isInteger(item.exitCode))
99
+ .map(item => ({
100
+ command: item.command,
101
+ exitCode: item.exitCode,
102
+ }));
103
+ }
104
+ return [];
105
+ }
106
+
107
+ function resolveSpecPath(projectDir, feature) {
108
+ if (typeof feature !== 'string' || feature.includes('..') || /[\\/]/.test(feature)) {
109
+ return null;
110
+ }
111
+ const flatPath = `.vibe/specs/${feature}.md`;
112
+ if (fs.existsSync(path.join(projectDir, flatPath))) return flatPath;
113
+ const splitPath = `.vibe/specs/${feature}/_index.md`;
114
+ return fs.existsSync(path.join(projectDir, splitPath)) ? splitPath : null;
115
+ }
116
+
117
+ function buildEvidence(projectDir, ledger, passed, generatedAt) {
118
+ return {
119
+ schemaVersion: EVIDENCE_SCHEMA_VERSION,
120
+ runId: ledger.runId,
121
+ specPath: resolveSpecPath(projectDir, ledger.runFeature),
122
+ generatedAt,
123
+ judges: {
124
+ deterministic: {
125
+ authority: 'blocking',
126
+ verifyPassed: Boolean(passed),
127
+ verificationResults: verificationResults(ledger.verificationResults),
128
+ },
129
+ model: { authority: 'advisory-only', canComplete: false },
130
+ humanTaste: { authority: 'release-only', canComplete: false },
131
+ },
132
+ };
133
+ }
134
+
135
+ function writeEvidence(projectDir, ledger, passed, generatedAt) {
136
+ try {
137
+ return writeJsonAtomic(
138
+ evidencePath(projectDir, ledger.runId),
139
+ buildEvidence(projectDir, ledger, passed, generatedAt),
140
+ );
50
141
  } catch {
51
142
  return false;
52
143
  }
@@ -59,10 +150,12 @@ function writeLedger(projectDir, data) {
59
150
  * @returns {boolean} 성공 여부
60
151
  */
61
152
  export function recordRunStart(projectDir, feature) {
62
- try {
153
+ return withLedgerLock(projectDir, () => {
63
154
  const existing = readLedger(projectDir) || {};
155
+ const { verificationResults: _results, verificationCommands: _commands, ...retained } = existing;
64
156
  const next = {
65
- ...existing,
157
+ ...retained,
158
+ runId: randomUUID(),
66
159
  runStarted: new Date().toISOString(),
67
160
  runFeature: feature || null,
68
161
  verifyPassed: false,
@@ -70,9 +163,7 @@ export function recordRunStart(projectDir, feature) {
70
163
  stopWarned: false,
71
164
  };
72
165
  return writeLedger(projectDir, next);
73
- } catch {
74
- return false;
75
- }
166
+ });
76
167
  }
77
168
 
78
169
  /**
@@ -80,25 +171,36 @@ export function recordRunStart(projectDir, feature) {
80
171
  * pass 시 verifyRequired 상태를 클리어한다.
81
172
  * @param {string} projectDir
82
173
  * @param {boolean} passed - 검증 통과 여부
174
+ * @param {{runId?: string, verificationResults?: object[]}} options
83
175
  * @returns {boolean} 성공 여부
84
176
  */
85
- export function recordVerify(projectDir, passed) {
86
- try {
177
+ export function recordVerify(projectDir, passed, options = {}) {
178
+ return withLedgerLock(projectDir, () => {
87
179
  const existing = readLedger(projectDir) || {};
180
+ const existingRunId = UUID_PATTERN.test(existing.runId || '') ? existing.runId : null;
181
+ if (existing.runId !== undefined && !existingRunId) return false;
182
+ if (existingRunId && options.runId !== existingRunId) return false;
183
+ const runId = existingRunId || randomUUID();
184
+ const results = verificationResults(options.verificationResults);
185
+ if (passed && (results.length === 0 || results.some(result => result.exitCode !== 0))) {
186
+ return false;
187
+ }
188
+ const generatedAt = new Date().toISOString();
88
189
  const next = {
89
190
  ...existing,
191
+ runId,
90
192
  verifyPassed: Boolean(passed),
91
- verifyAt: new Date().toISOString(),
193
+ verifyAt: generatedAt,
194
+ verificationResults: results,
92
195
  };
93
196
  // pass 시 verifyRequired 클리어
94
197
  if (passed) {
95
198
  next.verifyRequired = false;
96
199
  next.verifyRequiredReason = null;
97
200
  }
201
+ if (!writeEvidence(projectDir, next, passed, generatedAt)) return false;
98
202
  return writeLedger(projectDir, next);
99
- } catch {
100
- return false;
101
- }
203
+ });
102
204
  }
103
205
 
104
206
  /**
@@ -144,9 +246,9 @@ export function markStopWarned(projectDir) {
144
246
  */
145
247
  export function extractRunFeature(prompt) {
146
248
  try {
147
- const m = prompt.match(/(?:\/|\$)vibe\.run\s+([^\s]+)/i);
249
+ const m = prompt.match(/(?:\/|\$)vibe\.run\s+(?:"([^"]+)"|'([^']+)'|([^\s]+))/i);
148
250
  if (!m) return null;
149
- const token = m[1];
251
+ const token = m[1] || m[2] || m[3];
150
252
  // 플래그(-- 시작)나 키워드는 기능명이 아님
151
253
  if (token.startsWith('-')) return null;
152
254
  return token;
@@ -17,14 +17,13 @@ const CODE_EXT_RE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
17
17
  /**
18
18
  * in-process 진입점 — console.log 감지만 수행.
19
19
  * findings 배열 반환 (디스패처가 수집해 additionalContext에 주입).
20
- * @param {{ toolInput: string }} ctx
20
+ * @param {{ filePath: string }} ctx
21
21
  * @returns {Promise<{ exitCode: number, findings: string[] }>}
22
22
  */
23
23
  export async function run(ctx) {
24
24
  const findings = [];
25
25
  try {
26
- const input = JSON.parse(ctx.toolInput || '{}');
27
- const filePath = input.file_path || input.path || '';
26
+ const filePath = ctx.filePath;
28
27
 
29
28
  if (filePath && CODE_EXT_RE.test(filePath)) {
30
29
  const resolved = path.resolve(filePath);
@@ -76,7 +76,7 @@ const DISPATCH_RULES = [
76
76
  {
77
77
  pattern: /e2e.*테스트|e2e.*test|playwright|브라우저.*테스트|browser.*test/i,
78
78
  script: null,
79
- echo: '[E2E MODE] Use /vibe.utils --e2e for Playwright-based browser testing. Supports visual regression and video recording.',
79
+ echo: '[E2E MODE] Use /vibe.verify --e2e for Playwright-based browser testing. Supports visual regression and video recording.',
80
80
  label: 'e2e-echo',
81
81
  },
82
82
 
@@ -23,6 +23,7 @@ import fs from 'fs';
23
23
  import path from 'path';
24
24
  import { PROJECT_DIR, logHookDecision, projectVibePath, projectVibeRoot } from './utils.js';
25
25
  import { buildCliCtx, isDirectRun } from './lib/hook-context.js';
26
+ import { globToRegExp } from './lib/glob.js';
26
27
 
27
28
  const SCOPE_PATH = projectVibePath(PROJECT_DIR, 'scope.json');
28
29
 
@@ -45,37 +46,6 @@ function readScope() {
45
46
  }
46
47
  }
47
48
 
48
- /**
49
- * 경량 glob → RegExp 변환.
50
- * - `**` : 경로 구분자 포함 임의 문자열
51
- * - `*` : 구분자 제외 임의 문자열
52
- * - `?` : 구분자 제외 한 글자
53
- * - 기타 정규식 메타문자는 이스케이프
54
- */
55
- function globToRegExp(glob) {
56
- const normalized = glob.replace(/\\/g, '/');
57
- let out = '';
58
- for (let i = 0; i < normalized.length; i++) {
59
- const c = normalized[i];
60
- if (c === '*') {
61
- if (normalized[i + 1] === '*') {
62
- out += '.*';
63
- i++;
64
- if (normalized[i + 1] === '/') i++; // `**/` → `.*`
65
- } else {
66
- out += '[^/]*';
67
- }
68
- } else if (c === '?') {
69
- out += '[^/]';
70
- } else if ('.+^$()|{}[]\\'.includes(c)) {
71
- out += '\\' + c;
72
- } else {
73
- out += c;
74
- }
75
- }
76
- return new RegExp('^' + out + '$');
77
- }
78
-
79
49
  function matchesAny(relPath, patterns) {
80
50
  return patterns.some(p => globToRegExp(p).test(relPath));
81
51
  }
@@ -87,18 +57,9 @@ function toRelative(filePath) {
87
57
  return rel || path.basename(filePath).replace(/\\/g, '/');
88
58
  }
89
59
 
90
- function extractFilePath(toolInput) {
91
- if (!toolInput) return '';
92
- if (typeof toolInput === 'string') {
93
- try { return JSON.parse(toolInput).file_path || ''; }
94
- catch { return toolInput; }
95
- }
96
- return typeof toolInput.file_path === 'string' ? toolInput.file_path : '';
97
- }
98
-
99
60
  /**
100
61
  * in-process 진입점 — 디스패처가 ctx를 전달해 직접 호출.
101
- * @param {{ toolName: string, toolInput: string }} ctx
62
+ * @param {{ toolName: string, toolInput: string, filePath: string }} ctx
102
63
  * @returns {Promise<number>} exit code (0 = allow/no-op, 2 = block)
103
64
  */
104
65
  export async function run(ctx) {
@@ -108,7 +69,7 @@ export async function run(ctx) {
108
69
  const toolName = ctx.toolName;
109
70
  if (toolName !== 'Edit' && toolName !== 'Write') return 0;
110
71
 
111
- const filePath = extractFilePath(ctx.toolInput);
72
+ const filePath = ctx.filePath;
112
73
  if (!filePath) return 0;
113
74
 
114
75
  const rel = toRelative(filePath);
@@ -30,22 +30,6 @@ const SENTINEL_PATH_RE =
30
30
  const DANGEROUS_BASH_RE =
31
31
  /\b(rm\s+-rf|kill\s+-9|drop\s+table|truncate|shutdown|reboot|mkfs|dd\s+if=)\b/i;
32
32
 
33
- /**
34
- * Extract file path from tool input
35
- */
36
- function extractFilePath(toolName, input) {
37
- if (!input) return null;
38
- try {
39
- const parsed = typeof input === 'string' ? JSON.parse(input) : input;
40
- if (toolName === 'Write' || toolName === 'Edit' || toolName === 'Read') {
41
- return parsed.file_path || parsed.filePath || null;
42
- }
43
- } catch {
44
- return typeof input === 'string' ? input : null;
45
- }
46
- return null;
47
- }
48
-
49
33
  /**
50
34
  * Extract command from Bash tool input
51
35
  */
@@ -72,10 +56,11 @@ function isSentinelPath(filePath) {
72
56
  /**
73
57
  * Main guard logic
74
58
  */
75
- function guard(toolName, toolInput) {
59
+ function guard(ctx) {
60
+ const { toolName, toolInput } = ctx;
76
61
  // Write/Edit targeting sentinel files → block
77
62
  if (toolName === 'Write' || toolName === 'Edit') {
78
- const filePath = extractFilePath(toolName, toolInput);
63
+ const filePath = ctx.filePath;
79
64
  if (isSentinelPath(filePath)) {
80
65
  return {
81
66
  decision: 'block',
@@ -111,11 +96,11 @@ import { buildCliCtx, isDirectRun } from './lib/hook-context.js';
111
96
 
112
97
  /**
113
98
  * in-process 진입점 — 디스패처가 ctx를 전달해 직접 호출.
114
- * @param {{ toolName: string, toolInput: string }} ctx
99
+ * @param {{ toolName: string, toolInput: string, filePath: string }} ctx
115
100
  * @returns {Promise<number>} exit code (2 = deny 규약, 0 = allow)
116
101
  */
117
102
  export async function run(ctx) {
118
- const result = guard(ctx.toolName, ctx.toolInput);
103
+ const result = guard(ctx);
119
104
  if (result) {
120
105
  logHookDecision('sentinel-guard', ctx.toolName, 'block', result.reason);
121
106
  console.log(JSON.stringify(result));
@@ -89,7 +89,7 @@ async function main() {
89
89
  // 인덱스만 주입 (harness-review-2026-07-01 P1-5):
90
90
  // 이전에는 startSession 전체 요약(git log·테스트 상태·관찰·리플렉션·RAG
91
91
  // goals/constraints/decisions)을 매 세션 덤프했다. 이제는 1줄 인덱스만
92
- // 주입하고, 전체 컨텍스트 복원은 명시적 `/vibe.utils --continue` 전용.
92
+ // 주입하고, 전체 컨텍스트 복원은 명시적 `/vibe.continue` 전용.
93
93
  const [time, memories, latestVersion] = await Promise.all([
94
94
  timeModule.getCurrentTime({ format: 'human', timezone: 'Asia/Seoul' }),
95
95
  memoryModule.listMemories({ limit: 5, projectPath: PROJECT_DIR }),
@@ -104,7 +104,7 @@ async function main() {
104
104
  const iterMod = await import(`${LIB_BASE_P}IterationTracker.js`);
105
105
  const progressLine = iterMod.getProgressSummary(PROJECT_DIR);
106
106
  if (progressLine) {
107
- console.log(`\n📋 ${progressLine} — resume full context: /vibe.utils --continue`);
107
+ console.log(`\n📋 ${progressLine} — resume full context: /vibe.continue`);
108
108
  }
109
109
  } catch { /* progress index is best-effort */ }
110
110