@vibecodetown/mcp-server 2.1.3 → 2.2.0

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.
@@ -0,0 +1,53 @@
1
+ // adapters/mcp-ts/src/local-mode/version-lock.ts
2
+ // Version lock management for unified installation
3
+ import * as fs from "node:fs";
4
+ import { z } from "zod";
5
+ import { getVibeRepoPaths } from "./paths.js";
6
+ export const VersionLockSchema = z.object({
7
+ schema_version: z.literal(1),
8
+ created_at: z.string(),
9
+ updated_at: z.string(),
10
+ cli: z.object({
11
+ name: z.string(),
12
+ version: z.string(),
13
+ }),
14
+ engines: z.record(z.string(), z.object({
15
+ version: z.string(),
16
+ installed_at: z.string(),
17
+ })),
18
+ });
19
+ /**
20
+ * Read version lock file from repo
21
+ */
22
+ export function readVersionLock(repoRoot) {
23
+ const paths = getVibeRepoPaths(repoRoot);
24
+ if (!fs.existsSync(paths.versionLockFile))
25
+ return null;
26
+ try {
27
+ const data = JSON.parse(fs.readFileSync(paths.versionLockFile, "utf-8"));
28
+ return VersionLockSchema.parse(data);
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ /**
35
+ * Write version lock file to repo
36
+ */
37
+ export function writeVersionLock(repoRoot, cliVersion, engines) {
38
+ const paths = getVibeRepoPaths(repoRoot);
39
+ const now = new Date().toISOString();
40
+ const existing = readVersionLock(repoRoot);
41
+ const lock = {
42
+ schema_version: 1,
43
+ created_at: existing?.created_at ?? now,
44
+ updated_at: now,
45
+ cli: { name: "@vibecode/mcp-server", version: cliVersion },
46
+ engines: Object.fromEntries(Object.entries(engines).map(([name, version]) => [
47
+ name,
48
+ { version, installed_at: now }
49
+ ])),
50
+ };
51
+ fs.writeFileSync(paths.versionLockFile, JSON.stringify(lock, null, 2) + "\n");
52
+ return paths.versionLockFile;
53
+ }
@@ -0,0 +1,416 @@
1
+ /**
2
+ * cli_invoker.ts - Single CLI Invocation Point (SSOT)
3
+ *
4
+ * All MCP tools MUST use this module for CLI invocations.
5
+ * Direct usage of child_process outside this file is forbidden.
6
+ *
7
+ * This module provides:
8
+ * - Centralized CLI invocation
9
+ * - Invocation logging for test verification
10
+ * - Environment sanitization
11
+ * - Timeout handling
12
+ *
13
+ * @module runtime/cli_invoker
14
+ */
15
+ import { spawn, spawnSync } from "node:child_process";
16
+ import { sanitizeEnv } from "../cli.js";
17
+ import { getEngineCtx } from "../engine.js";
18
+ // ============================================================================
19
+ // Invocation Log (for test verification)
20
+ // ============================================================================
21
+ const invocationLog = [];
22
+ /**
23
+ * Get a copy of the invocation log
24
+ * Used by tests to verify CLI calls
25
+ */
26
+ export function getInvocationLog() {
27
+ return [...invocationLog];
28
+ }
29
+ /**
30
+ * Clear the invocation log
31
+ * Should be called in test beforeEach()
32
+ */
33
+ export function clearInvocationLog() {
34
+ invocationLog.length = 0;
35
+ }
36
+ /**
37
+ * Check if a specific CLI prefix was invoked
38
+ * Used by enforcement tests
39
+ */
40
+ export function wasInvoked(bin, argsPrefix) {
41
+ return invocationLog.some((record) => record.bin === bin &&
42
+ argsPrefix.every((prefix, i) => record.args[i] === prefix));
43
+ }
44
+ // ============================================================================
45
+ // Binary Resolution
46
+ // ============================================================================
47
+ let vibeBinPath = null;
48
+ /**
49
+ * Get the vibe CLI binary path
50
+ * This is the path to the vibe-cli.ts entry point
51
+ */
52
+ async function getVibeBinPath() {
53
+ if (vibeBinPath)
54
+ return vibeBinPath;
55
+ // In production, vibe is invoked via npx or global install
56
+ // For now, we use the current process argv
57
+ // TODO: Resolve proper vibe binary path from package
58
+ vibeBinPath = process.argv[1] ?? "vibe";
59
+ return vibeBinPath;
60
+ }
61
+ /** Known system binaries that should be resolved via PATH */
62
+ const SYSTEM_BINS = new Set([
63
+ "git", "semgrep", "python", "python3", "opa", "node", "npm", "npx",
64
+ // Common utilities that might be needed
65
+ "tar", "curl", "wget", "which", "bash", "sh",
66
+ ]);
67
+ /** Engine binary names (for type checking) */
68
+ const ENGINE_BINS = new Set([
69
+ "spec-high", "vibecoding-helper", "clinic", "execution-engine",
70
+ ]);
71
+ /**
72
+ * Resolve binary path for a given CLI bin
73
+ */
74
+ async function resolveBinPath(bin) {
75
+ if (bin === "vibe") {
76
+ return getVibeBinPath();
77
+ }
78
+ // System binary - use the name directly (resolved via PATH)
79
+ if (SYSTEM_BINS.has(bin)) {
80
+ return bin;
81
+ }
82
+ // Engine binary - use cached path from bootstrap
83
+ if (ENGINE_BINS.has(bin)) {
84
+ const ctx = await getEngineCtx();
85
+ const enginePath = ctx.bins[bin];
86
+ if (!enginePath) {
87
+ throw new Error(`Engine binary not installed: ${bin}`);
88
+ }
89
+ return enginePath;
90
+ }
91
+ // Unknown binary - assume it's a system command (PATH resolution)
92
+ // This allows flexibility for arbitrary commands while logging them
93
+ return bin;
94
+ }
95
+ // ============================================================================
96
+ // CLI Invocation
97
+ // ============================================================================
98
+ /**
99
+ * Invoke a CLI command
100
+ *
101
+ * This is the ONLY function that should spawn CLI processes.
102
+ * All other code should use this function.
103
+ *
104
+ * @param inv - Invocation request
105
+ * @returns CLI result with exit code, stdout, stderr, and duration
106
+ *
107
+ * @example
108
+ * ```typescript
109
+ * const result = await invokeCli({
110
+ * bin: "vibe",
111
+ * args: ["inspect", "--run-id", "abc123"],
112
+ * cwd: "/path/to/project",
113
+ * timeoutMs: 60000
114
+ * });
115
+ * ```
116
+ */
117
+ export async function invokeCli(inv) {
118
+ const startTime = Date.now();
119
+ const timeoutMs = inv.timeoutMs ?? 120_000;
120
+ // Create log record
121
+ const record = {
122
+ ...inv,
123
+ timestamp: new Date().toISOString(),
124
+ };
125
+ invocationLog.push(record);
126
+ // Resolve binary path
127
+ const binPath = await resolveBinPath(inv.bin);
128
+ return new Promise((resolve) => {
129
+ let resolved = false;
130
+ const p = spawn(binPath, inv.args, {
131
+ cwd: inv.cwd,
132
+ env: sanitizeEnv(inv.env),
133
+ shell: false,
134
+ stdio: ["ignore", "pipe", "pipe"],
135
+ });
136
+ let stdout = "";
137
+ let stderr = "";
138
+ const finish = (exitCode) => {
139
+ if (resolved)
140
+ return;
141
+ resolved = true;
142
+ const result = {
143
+ exitCode,
144
+ stdout,
145
+ stderr,
146
+ durationMs: Date.now() - startTime,
147
+ };
148
+ // Update log record with result
149
+ record.result = result;
150
+ resolve(result);
151
+ };
152
+ const killTimer = setTimeout(() => {
153
+ try {
154
+ p.kill("SIGKILL");
155
+ }
156
+ catch {
157
+ // Ignore kill errors
158
+ }
159
+ stderr += "\n[TIMEOUT]";
160
+ finish(124);
161
+ }, timeoutMs);
162
+ p.stdout.on("data", (d) => (stdout += d.toString("utf-8")));
163
+ p.stderr.on("data", (d) => (stderr += d.toString("utf-8")));
164
+ p.on("error", (e) => {
165
+ clearTimeout(killTimer);
166
+ const msg = e instanceof Error ? e.message : String(e);
167
+ stderr += `\n[SPAWN_ERROR] ${msg}`;
168
+ finish(127);
169
+ });
170
+ p.on("close", (code) => {
171
+ clearTimeout(killTimer);
172
+ finish(code ?? 1);
173
+ });
174
+ });
175
+ }
176
+ // ============================================================================
177
+ // Convenience Functions
178
+ // ============================================================================
179
+ /**
180
+ * Invoke vibe CLI command (convenience wrapper)
181
+ *
182
+ * @example
183
+ * ```typescript
184
+ * const result = await invokeVibe(["inspect", "--run-id", "abc123"], "/project");
185
+ * ```
186
+ */
187
+ export async function invokeVibe(args, cwd, opts) {
188
+ return invokeCli({
189
+ bin: "vibe",
190
+ args,
191
+ cwd,
192
+ env: opts?.env,
193
+ timeoutMs: opts?.timeoutMs,
194
+ });
195
+ }
196
+ /**
197
+ * Invoke engine binary directly (legacy, will be deprecated)
198
+ *
199
+ * Prefer using invokeVibe() with appropriate vibe subcommand instead.
200
+ *
201
+ * @deprecated Use invokeVibe() instead
202
+ */
203
+ export async function invokeEngine(engine, args, cwd, opts) {
204
+ return invokeCli({
205
+ bin: engine,
206
+ args,
207
+ cwd,
208
+ env: opts?.env,
209
+ timeoutMs: opts?.timeoutMs,
210
+ });
211
+ }
212
+ /**
213
+ * Invoke a system binary (git, semgrep, python, etc.)
214
+ *
215
+ * These commands are resolved via PATH.
216
+ *
217
+ * @example
218
+ * ```typescript
219
+ * // Run git status
220
+ * const result = await invokeSystem("git", ["status"], "/project");
221
+ *
222
+ * // Run semgrep scan
223
+ * const result = await invokeSystem("semgrep", ["scan", "--config", "auto"], "/project");
224
+ * ```
225
+ */
226
+ export async function invokeSystem(bin, args, cwd, opts) {
227
+ return invokeCli({
228
+ bin,
229
+ args,
230
+ cwd,
231
+ env: opts?.env,
232
+ timeoutMs: opts?.timeoutMs,
233
+ });
234
+ }
235
+ /**
236
+ * Invoke git command (convenience wrapper)
237
+ *
238
+ * @example
239
+ * ```typescript
240
+ * const result = await invokeGit(["status"], "/project");
241
+ * const result = await invokeGit(["add", "."], "/project");
242
+ * ```
243
+ */
244
+ export async function invokeGit(args, cwd, opts) {
245
+ return invokeSystem("git", args, cwd, opts);
246
+ }
247
+ /**
248
+ * Invoke a system binary synchronously
249
+ *
250
+ * Use sparingly - prefer async versions. Only use for:
251
+ * - Startup/bootstrap code
252
+ * - Simple checks that must block
253
+ *
254
+ * @example
255
+ * ```typescript
256
+ * const result = invokeSystemSync("git", ["rev-parse", "--show-toplevel"], "/project");
257
+ *
258
+ * // With stdin input
259
+ * const result = invokeSystemSync("opa", ["eval", "-I", ...], "/project", {
260
+ * input: JSON.stringify(data)
261
+ * });
262
+ * ```
263
+ */
264
+ export function invokeSystemSync(bin, args, cwd, opts) {
265
+ const timeoutMs = opts?.timeoutMs ?? 30_000;
266
+ // Log the invocation (sync version also logs for consistency)
267
+ const record = {
268
+ bin,
269
+ args,
270
+ cwd,
271
+ env: opts?.env,
272
+ timeoutMs,
273
+ timestamp: new Date().toISOString(),
274
+ };
275
+ invocationLog.push(record);
276
+ const result = spawnSync(bin, args, {
277
+ cwd,
278
+ encoding: "utf-8",
279
+ env: sanitizeEnv(opts?.env),
280
+ input: opts?.input,
281
+ stdio: opts?.input ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
282
+ timeout: timeoutMs,
283
+ });
284
+ const cliResult = {
285
+ exitCode: result.status ?? (result.signal ? 124 : 1),
286
+ stdout: result.stdout ?? "",
287
+ stderr: result.stderr ?? "",
288
+ };
289
+ // Check for spawn errors (e.g., binary not found)
290
+ if (result.error) {
291
+ const err = result.error;
292
+ cliResult.exitCode = err.code === "ENOENT" ? 127 : 1;
293
+ cliResult.stderr = `[SPAWN_ERROR] ${err.message}`;
294
+ }
295
+ // Update log record
296
+ record.result = { ...cliResult, durationMs: 0 };
297
+ return cliResult;
298
+ }
299
+ /**
300
+ * Invoke git command synchronously
301
+ *
302
+ * @example
303
+ * ```typescript
304
+ * const result = invokeGitSync(["rev-parse", "--show-toplevel"], "/project");
305
+ * ```
306
+ */
307
+ export function invokeGitSync(args, cwd, opts) {
308
+ return invokeSystemSync("git", args, cwd, opts);
309
+ }
310
+ // ============================================================================
311
+ // Detached Process (for background services)
312
+ // ============================================================================
313
+ /**
314
+ * Spawn a detached process that continues running after parent exits
315
+ *
316
+ * Use for background services like web servers.
317
+ * The process is spawned with `detached: true` and `unref()`.
318
+ *
319
+ * @returns PID of the spawned process, or null if spawn failed
320
+ *
321
+ * @example
322
+ * ```typescript
323
+ * const pid = spawnDetached("npm", ["run", "dev"], "/project", {
324
+ * PORT: "3000"
325
+ * });
326
+ * ```
327
+ */
328
+ export function spawnDetached(bin, args, cwd, env) {
329
+ // Log the invocation
330
+ const record = {
331
+ bin,
332
+ args,
333
+ cwd,
334
+ env,
335
+ timestamp: new Date().toISOString(),
336
+ };
337
+ invocationLog.push(record);
338
+ try {
339
+ const child = spawn(bin, args, {
340
+ cwd,
341
+ detached: true,
342
+ stdio: "ignore",
343
+ env: sanitizeEnv(env),
344
+ });
345
+ // Unref to allow parent to exit independently
346
+ child.unref();
347
+ const pid = child.pid ?? null;
348
+ // Update log record
349
+ record.result = {
350
+ exitCode: 0,
351
+ stdout: "",
352
+ stderr: "",
353
+ durationMs: 0,
354
+ };
355
+ return pid;
356
+ }
357
+ catch {
358
+ record.result = {
359
+ exitCode: 127,
360
+ stdout: "",
361
+ stderr: "[SPAWN_ERROR]",
362
+ durationMs: 0,
363
+ };
364
+ return null;
365
+ }
366
+ }
367
+ /**
368
+ * Parse VRIP control response from CLI result
369
+ *
370
+ * @param result - CLI invocation result
371
+ * @returns Parsed control response or error
372
+ */
373
+ export function parseVripResponse(result) {
374
+ if (result.exitCode > 1) {
375
+ // System crash - no JSON expected
376
+ return { ok: false, error: `CLI crashed (code ${result.exitCode}): ${result.stderr}` };
377
+ }
378
+ try {
379
+ const ctrl = JSON.parse(result.stdout.trim());
380
+ if (typeof ctrl.ok !== "boolean" || typeof ctrl.run_id !== "string") {
381
+ return { ok: false, error: "Invalid VRIP response: missing ok or run_id" };
382
+ }
383
+ return { ok: true, ctrl };
384
+ }
385
+ catch (e) {
386
+ const msg = e instanceof Error ? e.message : String(e);
387
+ return { ok: false, error: `JSON parse failed: ${msg}\nRaw: ${result.stdout}` };
388
+ }
389
+ }
390
+ /**
391
+ * Invoke vibe CLI with VRIP protocol
392
+ *
393
+ * Automatically adds --json flag and parses VRIP response.
394
+ *
395
+ * @example
396
+ * ```typescript
397
+ * const { ctrl, result } = await invokeVibeVrip(
398
+ * ["inspect", "--target", "src/"],
399
+ * "/project"
400
+ * );
401
+ * if (ctrl.ok) {
402
+ * const runDir = `.vibe/runs/${ctrl.run_id}`;
403
+ * // Read actual results from run_dir
404
+ * }
405
+ * ```
406
+ */
407
+ export async function invokeVibeVrip(args, cwd, opts) {
408
+ // Always add --json for VRIP protocol
409
+ const jsonArgs = args.includes("--json") ? args : [...args, "--json"];
410
+ const result = await invokeVibe(jsonArgs, cwd, opts);
411
+ const parsed = parseVripResponse(result);
412
+ if (!parsed.ok) {
413
+ throw new Error(parsed.error);
414
+ }
415
+ return { ctrl: parsed.ctrl, result };
416
+ }
@@ -35,7 +35,8 @@ export async function briefing(input) {
35
35
  // Mode defaults to balanced
36
36
  const mode = input.mode ?? "balanced";
37
37
  // Step 1: Initialize the run
38
- const initResult = await runEngine("spec-high", ["--root", "engines/spec_high", "init", run_id, "--mode", mode], { timeoutMs: 60_000 });
38
+ // Note: spec-high init doesn't support --mode flag, mode is handled at MCP layer
39
+ const initResult = await runEngine("spec-high", ["--root", "engines/spec_high", "init", run_id], { timeoutMs: 60_000 });
39
40
  if (initResult.code !== 0) {
40
41
  throw new Error(`프로젝트 초기화 실패: ${initResult.stderr || "알 수 없는 오류"}`);
41
42
  }
@@ -10,6 +10,7 @@ import { runDocStatusTriage } from "./doc_status_gate.js";
10
10
  import { kickoffKceSyncBestEffort } from "./kce/on_finalize.js";
11
11
  import { updateVersionFile } from "../../version-check.js";
12
12
  import { CONTRACTS_VERSION, CONTRACTS_BUNDLE_SHA256 } from "../../generated/contracts_bundle_info.js";
13
+ import { updateLastCommit, checkAndAddMcpBuildAction, formatReminders, } from "../../local-mode/project-state.js";
13
14
  /**
14
15
  * vibe_pm.finalize_work - Complete work with documentation and Git
15
16
  *
@@ -161,6 +162,13 @@ export async function finalizeWork(input, basePath = process.cwd()) {
161
162
  await git.commit(commitMessage);
162
163
  commitHash = (await git.revparse(["HEAD"])).trim() || "unknown";
163
164
  commitCreated = true;
165
+ // Update project state with commit info
166
+ updateLastCommit({
167
+ hash: commitHash,
168
+ message: commitMessage.split("\n")[0],
169
+ timestamp: new Date().toISOString(),
170
+ pushed: false, // Will be updated after push
171
+ }, basePath);
164
172
  }
165
173
  catch (err) {
166
174
  warning = appendWarning(warning, `Git commit 실패: ${err instanceof Error ? err.message : String(err)}`);
@@ -222,6 +230,13 @@ export async function finalizeWork(input, basePath = process.cwd()) {
222
230
  try {
223
231
  await git.push("origin", branchName);
224
232
  pushedToRemote = true;
233
+ // Update project state - mark as pushed
234
+ updateLastCommit({
235
+ hash: commitHash,
236
+ message: commitMessage.split("\n")[0],
237
+ timestamp: new Date().toISOString(),
238
+ pushed: true,
239
+ }, basePath);
225
240
  }
226
241
  catch (err) {
227
242
  warning = appendWarning(warning, `Git push 실패: ${err instanceof Error ? err.message : String(err)}`);
@@ -257,6 +272,22 @@ export async function finalizeWork(input, basePath = process.cwd()) {
257
272
  catch {
258
273
  // Best-effort: do not block finalize_work
259
274
  }
275
+ // Best-effort: Check if MCP package needs rebuild and update project state
276
+ let mcpBuildNeeded = false;
277
+ try {
278
+ mcpBuildNeeded = await checkAndAddMcpBuildAction(basePath);
279
+ if (mcpBuildNeeded) {
280
+ warning = appendWarning(warning, "MCP_BUILD_NEEDED: adapters/mcp-ts 소스 변경됨. vibe_pm.publish_mcp로 빌드 필요");
281
+ }
282
+ // Add reminder summary to warning if there are pending actions
283
+ const reminders = formatReminders(basePath);
284
+ if (reminders) {
285
+ warning = appendWarning(warning, `\n${reminders}`);
286
+ }
287
+ }
288
+ catch {
289
+ // ignore (best-effort)
290
+ }
260
291
  return {
261
292
  success: commitHash !== "no-commit" || updatedFiles.length > 0,
262
293
  updated_files: updatedFiles,
@@ -264,10 +295,15 @@ export async function finalizeWork(input, basePath = process.cwd()) {
264
295
  pushed_to_remote: pushedToRemote,
265
296
  remote_url: remoteUrl,
266
297
  warning,
267
- next_action: {
268
- tool: "vibe_pm.status",
269
- reason: "작업 완료 상태를 확인하세요"
270
- }
298
+ next_action: mcpBuildNeeded
299
+ ? {
300
+ tool: "vibe_pm.publish_mcp",
301
+ reason: "MCP 패키지 빌드 필요 (소스 변경 감지)"
302
+ }
303
+ : {
304
+ tool: "vibe_pm.status",
305
+ reason: "작업 완료 상태를 확인하세요"
306
+ }
271
307
  };
272
308
  }
273
309
  /**
@@ -0,0 +1,104 @@
1
+ // adapters/mcp-ts/src/tools/vibe_pm/force_override.ts
2
+ // vibe_pm.force_override - Emergency escape hatch for blocked operations
3
+ // P2-1: 탈출 경로 마련
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+ import { z } from "zod";
7
+ // ============================================================
8
+ // Schemas
9
+ // ============================================================
10
+ export const forceOverrideInputSchema = z.object({
11
+ action: z.string().describe("차단된 작업 설명 (예: 'npm run build')"),
12
+ reason: z.string().describe("강제 실행이 필요한 이유"),
13
+ acknowledge_risks: z.literal(true).describe("위험을 인지했음을 확인 (true 필수)"),
14
+ });
15
+ export const forceOverrideOutputSchema = z.object({
16
+ success: z.boolean(),
17
+ override_id: z.string().describe("강제 실행 ID (감사 로그용)"),
18
+ action: z.string(),
19
+ reason: z.string(),
20
+ timestamp: z.string(),
21
+ audit_log_path: z.string().optional(),
22
+ warning: z.string(),
23
+ next_action: z.object({
24
+ tool: z.string(),
25
+ reason: z.string(),
26
+ }),
27
+ });
28
+ function generateOverrideId() {
29
+ const now = new Date();
30
+ const ts = now.toISOString().replace(/[-:T.]/g, "").slice(0, 15);
31
+ const rand = Math.random().toString(36).slice(2, 8);
32
+ return `FORCE_${ts}_${rand}`;
33
+ }
34
+ function writeAuditLog(basePath, entry) {
35
+ try {
36
+ const auditDir = path.join(basePath, ".vibe", "audit");
37
+ fs.mkdirSync(auditDir, { recursive: true });
38
+ const logFile = path.join(auditDir, "force_override.jsonl");
39
+ const line = JSON.stringify(entry) + "\n";
40
+ fs.appendFileSync(logFile, line, "utf-8");
41
+ return logFile;
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ // ============================================================
48
+ // Tool Implementation
49
+ // ============================================================
50
+ /**
51
+ * vibe_pm.force_override
52
+ *
53
+ * Emergency escape hatch for blocked operations.
54
+ * Requires explicit acknowledgment of risks.
55
+ * All uses are logged for audit purposes.
56
+ *
57
+ * This does NOT actually execute any command - it only:
58
+ * 1. Validates the acknowledgment
59
+ * 2. Creates an audit log entry
60
+ * 3. Returns an override ID that can be used to bypass gates
61
+ *
62
+ * The AI agent can then use this override_id when re-attempting
63
+ * the blocked operation.
64
+ */
65
+ export async function forceOverride(input) {
66
+ const basePath = process.cwd();
67
+ // Validate input (fail-closed)
68
+ const parsed = forceOverrideInputSchema.parse(input);
69
+ // acknowledge_risks must be true (enforced by schema, but double-check)
70
+ if (parsed.acknowledge_risks !== true) {
71
+ throw new Error("acknowledge_risks must be true to use force_override");
72
+ }
73
+ // Generate override ID
74
+ const overrideId = generateOverrideId();
75
+ const timestamp = new Date().toISOString();
76
+ // Create audit log entry
77
+ const auditEntry = {
78
+ override_id: overrideId,
79
+ timestamp,
80
+ action: parsed.action,
81
+ reason: parsed.reason,
82
+ user_acknowledged_risks: true,
83
+ cwd: basePath,
84
+ env: {
85
+ user: process.env.USER ?? process.env.USERNAME,
86
+ hostname: process.env.HOSTNAME ?? process.env.COMPUTERNAME,
87
+ },
88
+ };
89
+ // Write audit log
90
+ const auditLogPath = writeAuditLog(basePath, auditEntry);
91
+ return {
92
+ success: true,
93
+ override_id: overrideId,
94
+ action: parsed.action,
95
+ reason: parsed.reason,
96
+ timestamp,
97
+ audit_log_path: auditLogPath ?? undefined,
98
+ warning: "⚠ 강제 실행이 기록되었습니다. 이 작업은 감사 로그에 저장됩니다.",
99
+ next_action: {
100
+ tool: "vibe_pm.run_app",
101
+ reason: "override_id를 사용해 차단된 작업을 다시 시도할 수 있습니다.",
102
+ },
103
+ };
104
+ }