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