@su-record/vibe 2.9.22 → 2.9.24
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/README.en.md +220 -0
- package/README.md +75 -124
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +11 -0
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +16 -0
- package/dist/cli/commands/update.js.map +1 -1
- package/hooks/scripts/code-check.js +3 -0
- package/hooks/scripts/lib/scope-from-spec.js +261 -0
- package/hooks/scripts/post-edit.js +3 -0
- package/hooks/scripts/pre-tool-dispatcher.js +5 -0
- package/hooks/scripts/scope-guard.js +145 -0
- package/hooks/scripts/session-start.js +9 -0
- package/package.json +10 -5
- package/README.ko.md +0 -171
|
@@ -6,6 +6,7 @@ import path from 'path';
|
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import os from 'os';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
9
10
|
import { log, ensureDir, getPackageJson } from '../utils.js';
|
|
10
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
12
|
const __dirname = path.dirname(__filename);
|
|
@@ -17,6 +18,19 @@ import { updateCursorGlobalAssets, installLocalSkills, installLanguageRules, } f
|
|
|
17
18
|
import { installExternalSkills } from './skills.js';
|
|
18
19
|
import { detectCocoCli } from '../utils/cli-detector.js';
|
|
19
20
|
import { Provisioner } from '../setup/Provisioner.js';
|
|
21
|
+
/**
|
|
22
|
+
* scope.json을 활성 SPEC 기반으로 자동 동기화.
|
|
23
|
+
* 수동 관리(auto != true) 중이거나 SPEC이 없으면 no-op.
|
|
24
|
+
*/
|
|
25
|
+
function syncProjectScope(projectRoot, packageRoot) {
|
|
26
|
+
try {
|
|
27
|
+
const scriptPath = path.join(packageRoot, 'hooks', 'scripts', 'lib', 'scope-from-spec.js');
|
|
28
|
+
if (!fs.existsSync(scriptPath))
|
|
29
|
+
return;
|
|
30
|
+
execSync(`node "${scriptPath}" "${projectRoot}"`, { stdio: 'inherit', timeout: 5000 });
|
|
31
|
+
}
|
|
32
|
+
catch { /* best-effort */ }
|
|
33
|
+
}
|
|
20
34
|
/**
|
|
21
35
|
* update 명령어 실행 — 프로젝트 설정만 업데이트
|
|
22
36
|
*/
|
|
@@ -104,6 +118,8 @@ export function update(options = { silent: false }) {
|
|
|
104
118
|
cleanupClaudeConfig();
|
|
105
119
|
// 레거시 mcp 폴더 정리
|
|
106
120
|
cleanupLegacyMcp(coreDir);
|
|
121
|
+
// scope.json 자동 동기화 (활성 SPEC 기반)
|
|
122
|
+
syncProjectScope(projectRoot, packageRoot);
|
|
107
123
|
const packageJson = getPackageJson();
|
|
108
124
|
log(`\n✅ vibe updated (v${packageJson.version})\n\n${formatLLMStatus()}\n📦 Context7 plugin (recommended): /plugin install context7\n`);
|
|
109
125
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update.js","sourceRoot":"","sources":["../../../src/cli/commands/update.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"update.js","sourceRoot":"","sources":["../../../src/cli/commands/update.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope synthesizer — derive allow-glob patterns from active SPECs.
|
|
3
|
+
*
|
|
4
|
+
* 활성 SPEC(`.claude/vibe/specs/*.md` with frontmatter status ∈ pending|in-progress|active)을
|
|
5
|
+
* 스캔해 백틱으로 감싼 파일 경로를 수집 → `<dir>/**` glob으로 변환한다.
|
|
6
|
+
*
|
|
7
|
+
* 수동 편집된 scope.json은 건드리지 않는다 — `auto: true` 플래그가 있는 파일만 덮어쓴다.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
|
|
13
|
+
const ACTIVE_STATUSES = new Set(['pending', 'in-progress', 'in_progress', 'active', 'running']);
|
|
14
|
+
|
|
15
|
+
// 항상 허용: SPEC/plan/TODO 등 메타 문서는 구현 중에도 갱신 가능해야 함
|
|
16
|
+
const DEFAULT_ALLOW = ['.claude/vibe/**', 'CLAUDE.md', 'AGENTS.md'];
|
|
17
|
+
|
|
18
|
+
function readFrontmatter(content) {
|
|
19
|
+
if (!content.startsWith('---')) return {};
|
|
20
|
+
const end = content.indexOf('\n---', 3);
|
|
21
|
+
if (end < 0) return {};
|
|
22
|
+
const block = content.slice(3, end);
|
|
23
|
+
const out = {};
|
|
24
|
+
for (const line of block.split('\n')) {
|
|
25
|
+
const m = line.match(/^([a-zA-Z_][\w-]*)\s*:\s*(.*)$/);
|
|
26
|
+
if (m) out[m[1]] = m[2].trim();
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 본문에서 백틱으로 감싼 파일 경로 후보 추출.
|
|
33
|
+
* - 확장자 있는 경로 (foo.ts, hooks/scripts/bar.js)
|
|
34
|
+
* - 슬래시 포함 상대경로 (src/cli/commands)
|
|
35
|
+
* - 절대경로(/etc/...) 및 URL(http://...) 제외
|
|
36
|
+
*/
|
|
37
|
+
// 허용 파일명 확장자 (실제 소스/자산). SPEC에 적힌 임의 토큰을 걸러낸다.
|
|
38
|
+
const ALLOWED_EXTS = new Set([
|
|
39
|
+
'ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs', 'json', 'md', 'mdx',
|
|
40
|
+
'css', 'scss', 'sass', 'less', 'html', 'yaml', 'yml', 'toml',
|
|
41
|
+
'py', 'rs', 'go', 'java', 'kt', 'rb', 'php', 'swift', 'c', 'cpp', 'h', 'hpp',
|
|
42
|
+
'sh', 'bash', 'sql', 'env', 'feature', 'txt', 'svg', 'png', 'jpg', 'webp',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
function extractPaths(markdown) {
|
|
46
|
+
const paths = new Set();
|
|
47
|
+
const backtickRe = /`([^`\n]+)`/g;
|
|
48
|
+
let m;
|
|
49
|
+
while ((m = backtickRe.exec(markdown)) !== null) {
|
|
50
|
+
const raw = m[1].trim();
|
|
51
|
+
if (!raw || raw.length > 200) continue;
|
|
52
|
+
if (raw.startsWith('/') || raw.startsWith('~')) continue; // 시스템/홈
|
|
53
|
+
if (raw.startsWith('http') || raw.includes('://')) continue;
|
|
54
|
+
if (/[\s{}[\]()<>`'"=,;|&$?!*@#%^+]/.test(raw)) continue; // 코드/템플릿/속성 접근자
|
|
55
|
+
|
|
56
|
+
const hasSlash = raw.includes('/');
|
|
57
|
+
const extMatch = raw.match(/\.([a-zA-Z0-9]{1,6})$/);
|
|
58
|
+
const ext = extMatch ? extMatch[1].toLowerCase() : null;
|
|
59
|
+
|
|
60
|
+
// 확장자 있으면 화이트리스트 체크, 없으면 디렉토리로 간주 (슬래시 필수)
|
|
61
|
+
if (ext) {
|
|
62
|
+
if (!ALLOWED_EXTS.has(ext)) continue;
|
|
63
|
+
} else {
|
|
64
|
+
if (!hasSlash) continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 명백한 비경로 제외
|
|
68
|
+
if (/^[A-Z_]+$/.test(raw)) continue; // 상수
|
|
69
|
+
if (/^\d+$/.test(raw)) continue; // 숫자
|
|
70
|
+
if (raw.startsWith('.') && !raw.startsWith('./') && !raw.startsWith('.claude')) continue;
|
|
71
|
+
|
|
72
|
+
const normalized = raw.replace(/^\.\//, '').replace(/\\/g, '/');
|
|
73
|
+
// 각 세그먼트 안전성 재검증 — "a.b.c" 같은 필드 경로 제거
|
|
74
|
+
const segments = normalized.split('/');
|
|
75
|
+
const lastSeg = segments[segments.length - 1];
|
|
76
|
+
// 파일명에 점이 2개 이상이면 보통 필드 경로 (except .test.ts, .d.ts 같은 허용 패턴)
|
|
77
|
+
const dotCount = (lastSeg.match(/\./g) || []).length;
|
|
78
|
+
if (dotCount > 2) continue;
|
|
79
|
+
if (dotCount === 2 && !/\.(test|spec|d|config|module|stories)\.[a-z]+$/i.test(lastSeg)) continue;
|
|
80
|
+
|
|
81
|
+
paths.add(normalized);
|
|
82
|
+
}
|
|
83
|
+
return [...paths];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 경로 목록 → glob 패턴 집합.
|
|
88
|
+
* - 확장자 있는 파일: dirname을 `<dir>/**`로
|
|
89
|
+
* - 디렉토리(확장자 없음, 슬래시 포함): `<path>/**`
|
|
90
|
+
* - 최상위 파일은 그대로 포함
|
|
91
|
+
*/
|
|
92
|
+
function pathsToGlobs(paths) {
|
|
93
|
+
const globs = new Set();
|
|
94
|
+
for (const p of paths) {
|
|
95
|
+
const hasExt = /\.[a-zA-Z0-9]{1,6}$/.test(p);
|
|
96
|
+
if (hasExt) {
|
|
97
|
+
const dir = path.posix.dirname(p);
|
|
98
|
+
if (dir && dir !== '.') globs.add(`${dir}/**`);
|
|
99
|
+
else globs.add(p);
|
|
100
|
+
} else {
|
|
101
|
+
const trimmed = p.replace(/\/+$/, '');
|
|
102
|
+
globs.add(`${trimmed}/**`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return collapseDominated([...globs]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* `a/**`가 `a/b/**`를 포함하면 후자 제거.
|
|
110
|
+
*/
|
|
111
|
+
function collapseDominated(globs) {
|
|
112
|
+
const sorted = [...new Set(globs)].sort((a, b) => a.length - b.length);
|
|
113
|
+
const kept = [];
|
|
114
|
+
for (const g of sorted) {
|
|
115
|
+
const base = g.replace(/\/\*\*$/, '');
|
|
116
|
+
const dominated = kept.some(k => {
|
|
117
|
+
const kb = k.replace(/\/\*\*$/, '');
|
|
118
|
+
return g !== k && g.endsWith('/**') && k.endsWith('/**') && (base === kb || base.startsWith(kb + '/'));
|
|
119
|
+
});
|
|
120
|
+
if (!dominated) kept.push(g);
|
|
121
|
+
}
|
|
122
|
+
return kept;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 프로젝트의 활성 SPEC 목록 수집.
|
|
127
|
+
*/
|
|
128
|
+
export function findActiveSpecs(projectDir) {
|
|
129
|
+
const specsDir = path.join(projectDir, '.claude', 'vibe', 'specs');
|
|
130
|
+
if (!fs.existsSync(specsDir)) return [];
|
|
131
|
+
const results = [];
|
|
132
|
+
const walk = (dir) => {
|
|
133
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
134
|
+
const full = path.join(dir, entry.name);
|
|
135
|
+
if (entry.isDirectory()) walk(full);
|
|
136
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) results.push(full);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
walk(specsDir);
|
|
140
|
+
return results.filter(f => {
|
|
141
|
+
try {
|
|
142
|
+
const content = fs.readFileSync(f, 'utf-8');
|
|
143
|
+
const fm = readFrontmatter(content);
|
|
144
|
+
const status = (fm.status || '').toLowerCase();
|
|
145
|
+
if (!fm.status) return true; // status 없으면 활성 간주
|
|
146
|
+
return ACTIVE_STATUSES.has(status);
|
|
147
|
+
} catch { return false; }
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* SPEC 파일 목록 → scope.json 객체.
|
|
153
|
+
*/
|
|
154
|
+
export function synthesizeScope(specFiles, { projectDir } = {}) {
|
|
155
|
+
const allPaths = new Set();
|
|
156
|
+
const sourceNames = [];
|
|
157
|
+
for (const f of specFiles) {
|
|
158
|
+
try {
|
|
159
|
+
const content = fs.readFileSync(f, 'utf-8');
|
|
160
|
+
for (const p of extractPaths(content)) allPaths.add(p);
|
|
161
|
+
sourceNames.push(path.relative(projectDir || '.', f).replace(/\\/g, '/'));
|
|
162
|
+
} catch { /* ignore */ }
|
|
163
|
+
}
|
|
164
|
+
const derived = pathsToGlobs([...allPaths]);
|
|
165
|
+
// 파일시스템 검증: 디렉토리 glob은 실제 존재할 때만 포함. 파일은 파일명만
|
|
166
|
+
// 있을 수 있으므로 (중첩 경로의 basename), 확장자 있으면 통과.
|
|
167
|
+
const verified = projectDir
|
|
168
|
+
? derived.filter(g => {
|
|
169
|
+
if (!g.endsWith('/**')) return true; // 파일은 유지
|
|
170
|
+
const base = g.replace(/\/\*\*$/, '');
|
|
171
|
+
try { return fs.existsSync(path.join(projectDir, base)); }
|
|
172
|
+
catch { return false; }
|
|
173
|
+
})
|
|
174
|
+
: derived;
|
|
175
|
+
const allow = collapseDominated([...DEFAULT_ALLOW, ...verified]);
|
|
176
|
+
return {
|
|
177
|
+
auto: true,
|
|
178
|
+
mode: 'warn',
|
|
179
|
+
allow,
|
|
180
|
+
deny: [],
|
|
181
|
+
reason: sourceNames.length > 0
|
|
182
|
+
? `auto-derived from active SPECs: ${sourceNames.join(', ')}`
|
|
183
|
+
: 'auto-derived (no active SPECs found)',
|
|
184
|
+
generatedAt: new Date().toISOString(),
|
|
185
|
+
sources: sourceNames,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* scope.json을 활성 SPEC 기반으로 동기화.
|
|
191
|
+
* - 파일 없음 → 생성 (활성 SPEC이 있을 때만)
|
|
192
|
+
* - 파일 있음 + `auto: true` → 갱신
|
|
193
|
+
* - 파일 있음 + `auto` 없거나 false → 수동 관리 중, 건드리지 않음
|
|
194
|
+
*
|
|
195
|
+
* @returns {{ action: 'created'|'updated'|'skipped-manual'|'skipped-no-specs'|'unchanged', path: string }}
|
|
196
|
+
*/
|
|
197
|
+
export function syncScopeFile(projectDir) {
|
|
198
|
+
const scopePath = path.join(projectDir, '.claude', 'vibe', 'scope.json');
|
|
199
|
+
const specs = findActiveSpecs(projectDir);
|
|
200
|
+
|
|
201
|
+
// 기존 파일이 수동 관리면 스킵
|
|
202
|
+
if (fs.existsSync(scopePath)) {
|
|
203
|
+
try {
|
|
204
|
+
const existing = JSON.parse(fs.readFileSync(scopePath, 'utf-8'));
|
|
205
|
+
if (existing.auto !== true) {
|
|
206
|
+
return { action: 'skipped-manual', path: scopePath };
|
|
207
|
+
}
|
|
208
|
+
} catch { /* 파싱 실패 → 자동 생성 덮어쓰기 */ }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (specs.length === 0) {
|
|
212
|
+
// 자동 파일만 있고 활성 SPEC 없음 → 제거
|
|
213
|
+
if (fs.existsSync(scopePath)) {
|
|
214
|
+
try {
|
|
215
|
+
const existing = JSON.parse(fs.readFileSync(scopePath, 'utf-8'));
|
|
216
|
+
if (existing.auto === true) {
|
|
217
|
+
fs.unlinkSync(scopePath);
|
|
218
|
+
return { action: 'removed', path: scopePath };
|
|
219
|
+
}
|
|
220
|
+
} catch { /* ignore */ }
|
|
221
|
+
}
|
|
222
|
+
return { action: 'skipped-no-specs', path: scopePath };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const next = synthesizeScope(specs, { projectDir });
|
|
226
|
+
const nextStr = JSON.stringify(next, null, 2);
|
|
227
|
+
|
|
228
|
+
// 변경 없음 체크 (generatedAt 제외)
|
|
229
|
+
if (fs.existsSync(scopePath)) {
|
|
230
|
+
try {
|
|
231
|
+
const existing = JSON.parse(fs.readFileSync(scopePath, 'utf-8'));
|
|
232
|
+
const { generatedAt: _a, ...exRest } = existing;
|
|
233
|
+
const { generatedAt: _b, ...nextRest } = next;
|
|
234
|
+
if (JSON.stringify(exRest) === JSON.stringify(nextRest)) {
|
|
235
|
+
return { action: 'unchanged', path: scopePath };
|
|
236
|
+
}
|
|
237
|
+
} catch { /* ignore */ }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const existed = fs.existsSync(scopePath);
|
|
241
|
+
fs.mkdirSync(path.dirname(scopePath), { recursive: true });
|
|
242
|
+
fs.writeFileSync(scopePath, nextStr + '\n', 'utf-8');
|
|
243
|
+
return { action: existed ? 'updated' : 'created', path: scopePath };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// CLI 엔트리: `node hooks/scripts/lib/scope-from-spec.js [projectDir]`
|
|
247
|
+
// init/update나 스크립트에서 자동 동기화 용도로 직접 호출 가능.
|
|
248
|
+
import { fileURLToPath } from 'url';
|
|
249
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
|
|
250
|
+
const dir = process.argv[2] || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
251
|
+
const result = syncScopeFile(dir);
|
|
252
|
+
const label = {
|
|
253
|
+
created: '✓ scope.json created',
|
|
254
|
+
updated: '✓ scope.json updated',
|
|
255
|
+
unchanged: '· scope.json unchanged',
|
|
256
|
+
removed: '✓ scope.json removed (no active SPECs)',
|
|
257
|
+
'skipped-manual': '· scope.json is manually managed (auto=false)',
|
|
258
|
+
'skipped-no-specs': '· no active SPECs — scope.json not generated',
|
|
259
|
+
}[result.action] || result.action;
|
|
260
|
+
console.log(`[scope-sync] ${label}`);
|
|
261
|
+
}
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
import { existsSync, readFileSync } from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
|
|
10
|
+
process.on('uncaughtException', () => {});
|
|
11
|
+
process.on('unhandledRejection', () => {});
|
|
12
|
+
|
|
10
13
|
const CONSOLE_LOG_RE = /console\.log/;
|
|
11
14
|
const CODE_EXT_RE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
|
|
12
15
|
|
|
@@ -23,6 +23,11 @@ const steps = [
|
|
|
23
23
|
{ name: 'pre-tool-guard', script: 'pre-tool-guard.js', args: [toolName], denyOnExit2: true },
|
|
24
24
|
];
|
|
25
25
|
|
|
26
|
+
// scope-guard는 Edit/Write에만 의미 있음 — 불필요한 spawn 회피
|
|
27
|
+
if (toolName === 'Edit' || toolName === 'Write') {
|
|
28
|
+
steps.push({ name: 'scope-guard', script: 'scope-guard.js', args: [toolName], denyOnExit2: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
// command-log은 Bash 전용
|
|
27
32
|
if (toolName === 'Bash') {
|
|
28
33
|
steps.push({ name: 'command-log', script: 'command-log.js' });
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Scope Guard — declared-scope enforcement for Edit/Write.
|
|
4
|
+
*
|
|
5
|
+
* CLAUDE.md의 Hard Rule "Modify only requested scope"를 훅으로 강제한다.
|
|
6
|
+
* 사용자가 `.claude/vibe/scope.json`에 범위를 선언하면, Edit/Write가 그
|
|
7
|
+
* 범위를 벗어날 때 경고(warn) 또는 차단(block)한다.
|
|
8
|
+
*
|
|
9
|
+
* 스코프 파일이 없거나 비어있으면 no-op — 기존 동작을 바꾸지 않는다.
|
|
10
|
+
*
|
|
11
|
+
* scope.json 스키마:
|
|
12
|
+
* {
|
|
13
|
+
* "mode": "warn" | "block", // default: "warn"
|
|
14
|
+
* "allow": ["src/cli/**"], // glob 패턴 (둘 중 하나라도 있으면 allow-list 모드)
|
|
15
|
+
* "deny": ["src/hooks/**"], // glob 패턴 (allow 통과 후에도 deny 매칭 시 차단)
|
|
16
|
+
* "reason": "CLI refactor" // 메시지에 표시
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* 매칭 대상: tool_input.file_path (project-relative 또는 absolute 모두 허용)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import fs from 'fs';
|
|
23
|
+
import path from 'path';
|
|
24
|
+
import { PROJECT_DIR, logHookDecision } from './utils.js';
|
|
25
|
+
|
|
26
|
+
const SCOPE_PATH = path.join(PROJECT_DIR, '.claude', 'vibe', 'scope.json');
|
|
27
|
+
|
|
28
|
+
function readScope() {
|
|
29
|
+
try {
|
|
30
|
+
if (!fs.existsSync(SCOPE_PATH)) return null;
|
|
31
|
+
const raw = fs.readFileSync(SCOPE_PATH, 'utf-8');
|
|
32
|
+
const parsed = JSON.parse(raw);
|
|
33
|
+
const allow = Array.isArray(parsed.allow) ? parsed.allow : [];
|
|
34
|
+
const deny = Array.isArray(parsed.deny) ? parsed.deny : [];
|
|
35
|
+
if (allow.length === 0 && deny.length === 0) return null;
|
|
36
|
+
return {
|
|
37
|
+
mode: parsed.mode === 'block' ? 'block' : 'warn',
|
|
38
|
+
allow,
|
|
39
|
+
deny,
|
|
40
|
+
reason: typeof parsed.reason === 'string' ? parsed.reason : '',
|
|
41
|
+
};
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 경량 glob → RegExp 변환.
|
|
49
|
+
* - `**` : 경로 구분자 포함 임의 문자열
|
|
50
|
+
* - `*` : 구분자 제외 임의 문자열
|
|
51
|
+
* - `?` : 구분자 제외 한 글자
|
|
52
|
+
* - 기타 정규식 메타문자는 이스케이프
|
|
53
|
+
*/
|
|
54
|
+
function globToRegExp(glob) {
|
|
55
|
+
const normalized = glob.replace(/\\/g, '/');
|
|
56
|
+
let out = '';
|
|
57
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
58
|
+
const c = normalized[i];
|
|
59
|
+
if (c === '*') {
|
|
60
|
+
if (normalized[i + 1] === '*') {
|
|
61
|
+
out += '.*';
|
|
62
|
+
i++;
|
|
63
|
+
if (normalized[i + 1] === '/') i++; // `**/` → `.*`
|
|
64
|
+
} else {
|
|
65
|
+
out += '[^/]*';
|
|
66
|
+
}
|
|
67
|
+
} else if (c === '?') {
|
|
68
|
+
out += '[^/]';
|
|
69
|
+
} else if ('.+^$()|{}[]\\'.includes(c)) {
|
|
70
|
+
out += '\\' + c;
|
|
71
|
+
} else {
|
|
72
|
+
out += c;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return new RegExp('^' + out + '$');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function matchesAny(relPath, patterns) {
|
|
79
|
+
return patterns.some(p => globToRegExp(p).test(relPath));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function toRelative(filePath) {
|
|
83
|
+
if (!filePath) return '';
|
|
84
|
+
const abs = path.isAbsolute(filePath) ? filePath : path.resolve(PROJECT_DIR, filePath);
|
|
85
|
+
const rel = path.relative(path.resolve(PROJECT_DIR), abs).replace(/\\/g, '/');
|
|
86
|
+
return rel || path.basename(filePath).replace(/\\/g, '/');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readStdinSync() {
|
|
90
|
+
try {
|
|
91
|
+
if (process.stdin.isTTY) return null;
|
|
92
|
+
const buf = Buffer.alloc(65536);
|
|
93
|
+
const bytesRead = fs.readSync(0, buf, 0, buf.length, null);
|
|
94
|
+
if (bytesRead > 0) return JSON.parse(buf.toString('utf-8', 0, bytesRead));
|
|
95
|
+
} catch { /* ignore */ }
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function extractFilePath(toolInput) {
|
|
100
|
+
if (!toolInput) return '';
|
|
101
|
+
if (typeof toolInput === 'string') {
|
|
102
|
+
try { return JSON.parse(toolInput).file_path || ''; }
|
|
103
|
+
catch { return toolInput; }
|
|
104
|
+
}
|
|
105
|
+
return typeof toolInput.file_path === 'string' ? toolInput.file_path : '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const scope = readScope();
|
|
109
|
+
if (!scope) process.exit(0); // no scope declared → no-op
|
|
110
|
+
|
|
111
|
+
const stdinPayload = readStdinSync();
|
|
112
|
+
const toolName = stdinPayload?.tool_name || process.argv[2] || '';
|
|
113
|
+
if (toolName !== 'Edit' && toolName !== 'Write') process.exit(0);
|
|
114
|
+
|
|
115
|
+
const rawInput = stdinPayload?.tool_input ?? process.argv[3] ?? process.env.TOOL_INPUT ?? '';
|
|
116
|
+
const filePath = extractFilePath(rawInput);
|
|
117
|
+
if (!filePath) process.exit(0);
|
|
118
|
+
|
|
119
|
+
const rel = toRelative(filePath);
|
|
120
|
+
|
|
121
|
+
// 평가 순서: deny 우선 → allow 검증
|
|
122
|
+
const denied = scope.deny.length > 0 && matchesAny(rel, scope.deny);
|
|
123
|
+
const allowed = scope.allow.length === 0 || matchesAny(rel, scope.allow);
|
|
124
|
+
|
|
125
|
+
const violated = denied || !allowed;
|
|
126
|
+
if (!violated) process.exit(0);
|
|
127
|
+
|
|
128
|
+
const lines = [];
|
|
129
|
+
lines.push(`🚧 SCOPE GUARD: ${toolName} — out of declared scope`);
|
|
130
|
+
lines.push(` file: ${rel}`);
|
|
131
|
+
if (denied) lines.push(` reason: matches deny pattern`);
|
|
132
|
+
else if (!allowed) lines.push(` reason: not in allow list`);
|
|
133
|
+
if (scope.reason) lines.push(` declared scope: ${scope.reason}`);
|
|
134
|
+
lines.push(` declared in: .claude/vibe/scope.json (mode=${scope.mode})`);
|
|
135
|
+
|
|
136
|
+
const blocking = scope.mode === 'block';
|
|
137
|
+
if (blocking) {
|
|
138
|
+
lines.push('');
|
|
139
|
+
lines.push('🚫 BLOCKED. Edit scope.json or justify to the user before proceeding.');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log(lines.join('\n'));
|
|
143
|
+
logHookDecision('scope-guard', toolName, blocking ? 'block' : 'warn', `${rel} ${denied ? '(deny)' : '(out-of-allow)'}`);
|
|
144
|
+
|
|
145
|
+
process.exit(blocking ? 2 : 0);
|
|
@@ -94,6 +94,15 @@ async function main() {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// Scope sync — 활성 SPEC 기반으로 scope.json 자동 갱신 (수동 관리면 스킵)
|
|
98
|
+
try {
|
|
99
|
+
const { syncScopeFile } = await import('./lib/scope-from-spec.js');
|
|
100
|
+
const result = syncScopeFile(PROJECT_DIR);
|
|
101
|
+
if (result.action === 'created' || result.action === 'updated' || result.action === 'removed') {
|
|
102
|
+
console.log(`\n🚧 Scope ${result.action} from active SPECs (.claude/vibe/scope.json)`);
|
|
103
|
+
}
|
|
104
|
+
} catch { /* scope sync is best-effort */ }
|
|
105
|
+
|
|
97
106
|
// Autonomy status summary
|
|
98
107
|
try {
|
|
99
108
|
const fs = await import('fs');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@su-record/vibe",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.24",
|
|
4
4
|
"description": "AI Coding Framework for Claude Code — 56 agents, 45 skills, multi-LLM orchestration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cli/index.js",
|
|
@@ -64,12 +64,12 @@
|
|
|
64
64
|
"node": ">=18.0.0"
|
|
65
65
|
},
|
|
66
66
|
"optionalDependencies": {
|
|
67
|
-
"@
|
|
68
|
-
"@
|
|
67
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.6",
|
|
68
|
+
"@ast-grep/napi": "^0.40.5"
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
71
|
"@clack/prompts": "^1.0.0",
|
|
72
|
-
"better-sqlite3": "^12.
|
|
72
|
+
"better-sqlite3": "^12.9.0",
|
|
73
73
|
"chalk": "^5.3.0",
|
|
74
74
|
"glob": "^13.0.1",
|
|
75
75
|
"papaparse": "^5.5.3",
|
|
@@ -97,5 +97,10 @@
|
|
|
97
97
|
"CLAUDE.md",
|
|
98
98
|
"README.md",
|
|
99
99
|
"LICENSE"
|
|
100
|
-
]
|
|
100
|
+
],
|
|
101
|
+
"pnpm": {
|
|
102
|
+
"onlyBuiltDependencies": [
|
|
103
|
+
"better-sqlite3"
|
|
104
|
+
]
|
|
105
|
+
}
|
|
101
106
|
}
|