@vibecodetown/mcp-server 2.1.4 → 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.
- package/build/auth/credential_store.js +146 -0
- package/build/auth/index.js +2 -0
- package/build/control_plane/gate.js +52 -70
- package/build/index.js +2 -0
- package/build/local-mode/git.js +36 -22
- package/build/local-mode/project-state.js +176 -0
- package/build/local-mode/templates.js +3 -3
- package/build/runtime/cli_invoker.js +416 -0
- package/build/tools/vibe_pm/briefing.js +2 -1
- package/build/tools/vibe_pm/finalize_work.js +40 -4
- package/build/tools/vibe_pm/force_override.js +104 -0
- package/build/tools/vibe_pm/index.js +73 -2
- package/build/tools/vibe_pm/list_rules.js +135 -0
- package/build/tools/vibe_pm/pre_commit_analysis.js +292 -0
- package/build/tools/vibe_pm/publish_mcp.js +271 -0
- package/build/tools/vibe_pm/run_app.js +48 -45
- package/build/tools/vibe_pm/save_rule.js +120 -0
- package/build/tools/vibe_pm/undo_last_task.js +16 -20
- package/build/version-check.js +5 -5
- package/build/vibe-cli.js +497 -32
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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
|
+
}
|