@vibecodetown/mcp-server 2.1.4 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +10 -10
  2. package/build/auth/credential_store.js +146 -0
  3. package/build/auth/public_key.js +6 -4
  4. package/build/bootstrap/doctor.js +113 -5
  5. package/build/bootstrap/installer.js +85 -15
  6. package/build/bootstrap/registry.js +11 -6
  7. package/build/bootstrap/skills-installer.js +365 -0
  8. package/build/control_plane/gate.js +52 -70
  9. package/build/dx/activity.js +26 -3
  10. package/build/engine.js +151 -0
  11. package/build/errors.js +107 -0
  12. package/build/generated/bridge_build_seed_input.js +2 -0
  13. package/build/generated/bridge_build_seed_output.js +2 -0
  14. package/build/generated/bridge_confirm_reference_input.js +2 -0
  15. package/build/generated/bridge_confirm_reference_output.js +2 -0
  16. package/build/generated/bridge_confirmed_reference_file.js +2 -0
  17. package/build/generated/bridge_generate_references_input.js +2 -0
  18. package/build/generated/bridge_generate_references_output.js +2 -0
  19. package/build/generated/bridge_references_file.js +2 -0
  20. package/build/generated/bridge_work_order_seed_file.js +2 -0
  21. package/build/generated/contracts_bundle_info.js +3 -3
  22. package/build/generated/index.js +14 -0
  23. package/build/generated/ingress_input.js +2 -0
  24. package/build/generated/ingress_output.js +2 -0
  25. package/build/generated/ingress_resolution_file.js +2 -0
  26. package/build/generated/ingress_summary_file.js +2 -0
  27. package/build/generated/message_template_id_mapping_file.js +2 -0
  28. package/build/generated/run_app_input.js +1 -1
  29. package/build/index.js +4 -1
  30. package/build/local-mode/git.js +36 -22
  31. package/build/local-mode/paths.js +1 -0
  32. package/build/local-mode/project-state.js +176 -0
  33. package/build/local-mode/setup.js +21 -1
  34. package/build/local-mode/templates.js +3 -3
  35. package/build/path-utils.js +68 -0
  36. package/build/runtime/cli_invoker.js +416 -0
  37. package/build/tools/vibe_pm/advisory_review.js +5 -3
  38. package/build/tools/vibe_pm/bridge_build_seed.js +164 -0
  39. package/build/tools/vibe_pm/bridge_confirm_reference.js +91 -0
  40. package/build/tools/vibe_pm/bridge_generate_references.js +258 -0
  41. package/build/tools/vibe_pm/briefing.js +26 -1
  42. package/build/tools/vibe_pm/context.js +79 -0
  43. package/build/tools/vibe_pm/create_work_order.js +200 -3
  44. package/build/tools/vibe_pm/doctor.js +95 -0
  45. package/build/tools/vibe_pm/entity_gate/preflight.js +8 -3
  46. package/build/tools/vibe_pm/export_output.js +14 -13
  47. package/build/tools/vibe_pm/finalize_work.js +74 -0
  48. package/build/tools/vibe_pm/force_override.js +104 -0
  49. package/build/tools/vibe_pm/get_decision.js +2 -2
  50. package/build/tools/vibe_pm/index.js +160 -3
  51. package/build/tools/vibe_pm/ingress.js +645 -0
  52. package/build/tools/vibe_pm/ingress_gate.js +116 -0
  53. package/build/tools/vibe_pm/inspect_code.js +90 -20
  54. package/build/tools/vibe_pm/kce/doc_usage.js +4 -9
  55. package/build/tools/vibe_pm/kce/on_finalize.js +2 -2
  56. package/build/tools/vibe_pm/kce/preflight.js +11 -7
  57. package/build/tools/vibe_pm/list_rules.js +135 -0
  58. package/build/tools/vibe_pm/memory_status.js +11 -8
  59. package/build/tools/vibe_pm/memory_sync.js +11 -8
  60. package/build/tools/vibe_pm/pm_language.js +17 -16
  61. package/build/tools/vibe_pm/pre_commit_analysis.js +292 -0
  62. package/build/tools/vibe_pm/publish_mcp.js +271 -0
  63. package/build/tools/vibe_pm/python_error.js +115 -0
  64. package/build/tools/vibe_pm/run_app.js +215 -86
  65. package/build/tools/vibe_pm/run_app_podman.js +64 -2
  66. package/build/tools/vibe_pm/save_rule.js +120 -0
  67. package/build/tools/vibe_pm/search_oss.js +5 -3
  68. package/build/tools/vibe_pm/spec_rag.js +185 -0
  69. package/build/tools/vibe_pm/status.js +50 -3
  70. package/build/tools/vibe_pm/submit_decision.js +2 -2
  71. package/build/tools/vibe_pm/types.js +28 -0
  72. package/build/tools/vibe_pm/undo_last_task.js +23 -20
  73. package/build/tools/vibe_pm/waiter_mapping.js +155 -0
  74. package/build/tools/vibe_pm/zoekt_evidence.js +5 -3
  75. package/build/tools.js +13 -5
  76. package/build/version-check.js +5 -5
  77. package/build/vibe-cli.js +742 -39
  78. package/package.json +5 -4
  79. package/skills/VRIP_INSTALL_MANIFEST_DOCTOR.skill.md +288 -0
  80. package/skills/index.json +14 -0
@@ -1,12 +1,53 @@
1
1
  // adapters/mcp-ts/src/tools/vibe_pm/context.ts
2
2
  // Run context auto-discovery utilities
3
3
  import * as fs from "node:fs";
4
+ import * as fsPromises from "node:fs/promises";
4
5
  import * as path from "node:path";
5
6
  import { TieredCache, createCacheKey } from "../../cache/index.js";
6
7
  import { validatePath } from "../../security/path-policy.js";
7
8
  import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
8
9
  import { RunStateFileSchema } from "../../generated/run_state_file.js";
9
10
  // ============================================================
11
+ // P2-19: Async File System Utilities
12
+ // NOTE: Gradual migration from sync to async is in progress.
13
+ // New code should prefer these async helpers.
14
+ // ============================================================
15
+ /**
16
+ * Check if a path exists (async)
17
+ */
18
+ async function existsAsync(p) {
19
+ try {
20
+ await fsPromises.access(p);
21
+ return true;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ /**
28
+ * Check if a path is a directory (async)
29
+ */
30
+ async function isDirAsync(p) {
31
+ try {
32
+ const stat = await fsPromises.stat(p);
33
+ return stat.isDirectory();
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ /**
40
+ * Read file content (async)
41
+ */
42
+ async function readFileAsync(p) {
43
+ try {
44
+ return await fsPromises.readFile(p, "utf-8");
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ // ============================================================
10
51
  // Constants
11
52
  // ============================================================
12
53
  const RUNS_DIR = "runs";
@@ -51,6 +92,8 @@ function normalizeRunState(raw) {
51
92
  const parsed = RunStateFileSchema.safeParse(raw);
52
93
  if (!parsed.success)
53
94
  return null;
95
+ // RunStateFileSchema uses z.any().superRefine() for union validation
96
+ // Type assertion is safe here because safeParse succeeded
54
97
  const value = parsed.data;
55
98
  const out = {};
56
99
  if (typeof value?.phase === "string")
@@ -241,8 +284,38 @@ export function resolveRunId(providedId, basePath = process.cwd()) {
241
284
  }
242
285
  return { run_id: generateRunId(undefined, basePath), is_new: true };
243
286
  }
287
+ /**
288
+ * Find the .vibe folder in the directory tree (like Git finds .git)
289
+ * Priority:
290
+ * 1. VIBE_PROJECT_ROOT environment variable (if set and has .vibe)
291
+ * 2. Search upward from startPath
292
+ * @param startPath Starting directory (default: process.cwd())
293
+ * @returns Path to the directory containing .vibe, or null if not found
294
+ */
295
+ export function findVibeRoot(startPath = process.cwd()) {
296
+ // Check VIBE_PROJECT_ROOT environment variable first
297
+ const envRoot = process.env.VIBE_PROJECT_ROOT;
298
+ if (envRoot && fs.existsSync(path.join(envRoot, ".vibe"))) {
299
+ return envRoot;
300
+ }
301
+ // Search upward from startPath
302
+ let current = path.resolve(startPath);
303
+ const root = path.parse(current).root;
304
+ while (current !== root) {
305
+ if (fs.existsSync(path.join(current, ".vibe"))) {
306
+ return current;
307
+ }
308
+ current = path.dirname(current);
309
+ }
310
+ // Check root as well
311
+ if (fs.existsSync(path.join(root, ".vibe"))) {
312
+ return root;
313
+ }
314
+ return null;
315
+ }
244
316
  /**
245
317
  * Resolve project_id from run_id or folder name
318
+ * If .vibe folder exists in parent directories, uses that folder's name
246
319
  */
247
320
  export function resolveProjectId(runId, basePath = process.cwd()) {
248
321
  if (runId) {
@@ -258,6 +331,12 @@ export function resolveProjectId(runId, basePath = process.cwd()) {
258
331
  }
259
332
  return runId;
260
333
  }
334
+ // Find the project root by looking for .vibe folder
335
+ const vibeRoot = findVibeRoot(basePath);
336
+ if (vibeRoot) {
337
+ return sanitizeForId(path.basename(vibeRoot));
338
+ }
339
+ // Fallback: use current folder name
261
340
  return sanitizeForId(path.basename(basePath));
262
341
  }
263
342
  // ============================================================
@@ -14,6 +14,7 @@ import { ClinicBridgeFileSchema } from "../../generated/clinic_bridge_file.js";
14
14
  import { loadPromptDensity } from "./intent/prompt_density.js";
15
15
  import { runKceRetrieve, runKceStatus } from "./kce/preflight.js";
16
16
  import { updateKceDocUsage } from "./kce/doc_usage.js";
17
+ import { getSpecContext } from "./spec_rag.js";
17
18
  /**
18
19
  * vibe_pm.create_work_order - Issue work order for implementation
19
20
  *
@@ -83,17 +84,17 @@ export async function createWorkOrder(input) {
83
84
  const headline = "프로젝트 구현";
84
85
  const dg = bridgeData?.decision_guard;
85
86
  const legacyScope = bridgeData?.scope;
86
- const scopeInclude = Array.isArray(dg?.allow_paths)
87
+ let scopeInclude = Array.isArray(dg?.allow_paths)
87
88
  ? dg.allow_paths
88
89
  : Array.isArray(legacyScope?.include)
89
90
  ? legacyScope.include
90
91
  : [];
91
- const scopeExclude = Array.isArray(dg?.deny_paths)
92
+ let scopeExclude = Array.isArray(dg?.deny_paths)
92
93
  ? dg.deny_paths
93
94
  : Array.isArray(legacyScope?.exclude)
94
95
  ? legacyScope.exclude
95
96
  : [];
96
- const doNotTouch = Array.isArray(dg?.read_only_paths)
97
+ let doNotTouch = Array.isArray(dg?.read_only_paths)
97
98
  ? dg.read_only_paths
98
99
  : Array.isArray(bridgeData?.do_not_touch)
99
100
  ? bridgeData.do_not_touch
@@ -105,6 +106,27 @@ export async function createWorkOrder(input) {
105
106
  : Array.isArray(bridgeData?.verify_criteria)
106
107
  ? bridgeData.verify_criteria.filter((s) => typeof s === "string" && s.trim())
107
108
  : [];
109
+ // Add SSOT compliance verification criterion
110
+ verifyCriteria.push("변경 사항이 기존 SSOT 문서(docs/ssot/*.md)와 충돌하지 않는지 확인");
111
+ // Bridge protocol (seed consumption):
112
+ // - If work_order_seed.json exists, it must be consumed (no ignore)
113
+ // - confirmed_reference.json must exist when seed exists (fail-closed)
114
+ //
115
+ // Minimal structured consumption policy:
116
+ // - Never broaden scope beyond the clinic bridge. Only:
117
+ // - Fill missing include list from seed.include (when clinic bridge include is empty)
118
+ // - Union in seed.exclude / seed.do_not_touch as additional restrictions
119
+ const seed = loadBridgeWorkOrderSeedOrThrow({ runDir: context.runs_path });
120
+ if (seed) {
121
+ const seedInclude = sanitizePatternList(seed.scope_seed.include);
122
+ const seedExclude = sanitizePatternList(seed.scope_seed.exclude);
123
+ const seedDoNotTouch = sanitizePatternList(seed.scope_seed.do_not_touch ?? []);
124
+ if (scopeInclude.length === 0 && seedInclude.length > 0) {
125
+ scopeInclude = seedInclude;
126
+ }
127
+ scopeExclude = mergeUnique(scopeExclude, seedExclude);
128
+ doNotTouch = mergeUnique(doNotTouch, seedDoNotTouch);
129
+ }
108
130
  // Generate paste_to_agent text
109
131
  let pasteToAgent = generateWorkOrderText({
110
132
  projectName,
@@ -118,6 +140,35 @@ export async function createWorkOrder(input) {
118
140
  if (input.additional_instructions && input.additional_instructions.trim()) {
119
141
  pasteToAgent += `\n\n[추가 지시]\n- ${input.additional_instructions.trim()}\n`;
120
142
  }
143
+ if (seed) {
144
+ pasteToAgent = applyBridgeSeedToPasteToAgent(pasteToAgent, seed);
145
+ }
146
+ // Inject SPEC_CONTEXT from SSOT documents (best-effort)
147
+ if (promptDensityMode !== "mode_2") {
148
+ try {
149
+ const specQuery = [headline, seed?.intent?.goal, verifyCriteria.slice(0, 2).join(" ")]
150
+ .filter(Boolean)
151
+ .join(" ");
152
+ const specResult = await getSpecContext({
153
+ query: specQuery,
154
+ maxChars: 2500,
155
+ maxItems: 4,
156
+ basePath
157
+ });
158
+ if (specResult.context_block) {
159
+ // Insert SPEC_CONTEXT right after the header line
160
+ const headerEnd = pasteToAgent.indexOf("\n\n");
161
+ if (headerEnd > 0) {
162
+ const header = pasteToAgent.slice(0, headerEnd);
163
+ const rest = pasteToAgent.slice(headerEnd);
164
+ pasteToAgent = `${header}\n\n${specResult.context_block}${rest}`;
165
+ }
166
+ }
167
+ }
168
+ catch {
169
+ // Best-effort: never block work order issuance when spec context is unavailable
170
+ }
171
+ }
121
172
  // Ensure versioned modules (Research/Skills/Planning) exist and link them to output
122
173
  const modules = ensureModulesForWorkOrder({ basePath, runId: run_id });
123
174
  // Append SKILLS_PACK / PLAN_STEPS into paste_to_agent (still user-friendly, no engine terms)
@@ -245,6 +296,152 @@ export async function createWorkOrder(input) {
245
296
  }
246
297
  };
247
298
  }
299
+ export function loadBridgeWorkOrderSeedOrThrow(opts) {
300
+ const bridgeDir = path.join(opts.runDir, "bridge");
301
+ const seedPath = path.join(bridgeDir, "work_order_seed.json");
302
+ const confirmedPath = path.join(bridgeDir, "confirmed_reference.json");
303
+ if (!fs.existsSync(seedPath))
304
+ return null;
305
+ if (!fs.existsSync(confirmedPath)) {
306
+ throw new Error("레퍼런스 선택 확인 파일이 없어 작업 지시서를 생성할 수 없습니다. 먼저 레퍼런스를 확정해주세요.");
307
+ }
308
+ let raw;
309
+ try {
310
+ raw = JSON.parse(fs.readFileSync(seedPath, "utf-8"));
311
+ }
312
+ catch {
313
+ throw new Error("work_order_seed.json을 읽을 수 없습니다. 파일 내용을 확인해주세요.");
314
+ }
315
+ if (!raw || typeof raw !== "object") {
316
+ throw new Error("work_order_seed.json 형식이 올바르지 않습니다.");
317
+ }
318
+ const seed = raw;
319
+ if (seed.schema_version !== "bridge.work_order_seed.v1") {
320
+ throw new Error("work_order_seed.json 버전이 올바르지 않습니다.");
321
+ }
322
+ if (typeof seed.created_at !== "string" || !seed.created_at) {
323
+ throw new Error("work_order_seed.json 필수 필드(created_at)가 누락되었습니다.");
324
+ }
325
+ if (typeof seed.intent?.goal !== "string" || !seed.intent.goal.trim()) {
326
+ throw new Error("work_order_seed.json 필수 필드(intent.goal)가 누락되었습니다.");
327
+ }
328
+ if (typeof seed.intent?.user_story !== "string" || !seed.intent.user_story.trim()) {
329
+ throw new Error("work_order_seed.json 필수 필드(intent.user_story)가 누락되었습니다.");
330
+ }
331
+ if (!Array.isArray(seed.intent?.acceptance) || seed.intent.acceptance.length < 3) {
332
+ throw new Error("work_order_seed.json 필수 필드(intent.acceptance)가 부족합니다(최소 3개).");
333
+ }
334
+ if (!Array.isArray(seed.scope_seed?.include) || seed.scope_seed.include.length < 1) {
335
+ throw new Error("work_order_seed.json 필수 필드(scope_seed.include)가 누락되었습니다.");
336
+ }
337
+ if (!Array.isArray(seed.scope_seed?.exclude)) {
338
+ throw new Error("work_order_seed.json 필수 필드(scope_seed.exclude)가 누락되었습니다.");
339
+ }
340
+ if (typeof seed.reference?.key !== "string" || !["A", "B", "C"].includes(seed.reference.key)) {
341
+ throw new Error("work_order_seed.json 필수 필드(reference.key)가 올바르지 않습니다.");
342
+ }
343
+ if (typeof seed.reference?.title !== "string" || !seed.reference.title.trim()) {
344
+ throw new Error("work_order_seed.json 필수 필드(reference.title)가 누락되었습니다.");
345
+ }
346
+ if (typeof seed.reference?.primary_link !== "string" || !seed.reference.primary_link.trim()) {
347
+ throw new Error("work_order_seed.json 필수 필드(reference.primary_link)가 누락되었습니다.");
348
+ }
349
+ return seed;
350
+ }
351
+ export function applyBridgeSeedToPasteToAgent(pasteToAgent, seed) {
352
+ const lines = [];
353
+ lines.push("[레퍼런스 기준]");
354
+ lines.push(`- 선택: ${seed.reference.title}`);
355
+ lines.push(`- 링크: ${seed.reference.primary_link}`);
356
+ if (Array.isArray(seed.reference.takeaways) && seed.reference.takeaways.length > 0) {
357
+ lines.push("- 핵심 포인트:");
358
+ for (const t of seed.reference.takeaways.slice(0, 10)) {
359
+ if (typeof t === "string" && t.trim())
360
+ lines.push(` - ${t.trim()}`);
361
+ }
362
+ }
363
+ lines.push("");
364
+ lines.push("[사용자 목표]");
365
+ lines.push(seed.intent.goal.trim());
366
+ lines.push("");
367
+ lines.push("[사용자 관점]");
368
+ lines.push(seed.intent.user_story.trim());
369
+ lines.push("");
370
+ lines.push("[완료 기준]");
371
+ for (const a of seed.intent.acceptance) {
372
+ if (typeof a === "string" && a.trim())
373
+ lines.push(`- ${a.trim()}`);
374
+ }
375
+ if (Array.isArray(seed.intent.constraints) && seed.intent.constraints.length > 0) {
376
+ lines.push("");
377
+ lines.push("[제약]");
378
+ for (const c of seed.intent.constraints.slice(0, 12)) {
379
+ if (typeof c === "string" && c.trim())
380
+ lines.push(`- ${c.trim()}`);
381
+ }
382
+ }
383
+ if (Array.isArray(seed.intent.non_goals) && seed.intent.non_goals.length > 0) {
384
+ lines.push("");
385
+ lines.push("[하지 않을 것]");
386
+ for (const n of seed.intent.non_goals.slice(0, 8)) {
387
+ if (typeof n === "string" && n.trim())
388
+ lines.push(`- ${n.trim()}`);
389
+ }
390
+ }
391
+ lines.push("");
392
+ lines.push("[권장 작업 구역]");
393
+ for (const p of seed.scope_seed.include.slice(0, 20)) {
394
+ if (typeof p === "string" && p.trim())
395
+ lines.push(`- 포함: ${p.trim()}`);
396
+ }
397
+ for (const p of seed.scope_seed.exclude.slice(0, 20)) {
398
+ if (typeof p === "string" && p.trim())
399
+ lines.push(`- 제외: ${p.trim()}`);
400
+ }
401
+ if (Array.isArray(seed.scope_seed.do_not_touch) && seed.scope_seed.do_not_touch.length > 0) {
402
+ for (const p of seed.scope_seed.do_not_touch.slice(0, 20)) {
403
+ if (typeof p === "string" && p.trim())
404
+ lines.push(`- 건드리지 말 것: ${p.trim()}`);
405
+ }
406
+ }
407
+ const seedBlock = lines.join("\n").trimEnd();
408
+ // Insert seed guidance near the top of the work order so it becomes the primary grounding context.
409
+ // Prefer placing it right before the first "[목표]" section if present; otherwise prepend.
410
+ const marker = "\n\n[목표]\n";
411
+ const idx = pasteToAgent.indexOf(marker);
412
+ if (idx !== -1) {
413
+ const head = pasteToAgent.slice(0, idx).trimEnd();
414
+ const tail = pasteToAgent.slice(idx).trimStart();
415
+ return `${head}\n\n${seedBlock}\n\n${tail}\n`;
416
+ }
417
+ return `${seedBlock}\n\n${pasteToAgent.trimEnd()}\n`;
418
+ }
419
+ function sanitizePatternList(list) {
420
+ if (!Array.isArray(list))
421
+ return [];
422
+ return list
423
+ .map((p) => (typeof p === "string" ? p.trim() : ""))
424
+ .filter(Boolean);
425
+ }
426
+ function mergeUnique(base, extra) {
427
+ const seen = new Set();
428
+ const out = [];
429
+ for (const p of base) {
430
+ const v = typeof p === "string" ? p.trim() : "";
431
+ if (!v || seen.has(v))
432
+ continue;
433
+ seen.add(v);
434
+ out.push(v);
435
+ }
436
+ for (const p of extra) {
437
+ const v = typeof p === "string" ? p.trim() : "";
438
+ if (!v || seen.has(v))
439
+ continue;
440
+ seen.add(v);
441
+ out.push(v);
442
+ }
443
+ return out;
444
+ }
248
445
  function formatPlanSteps(planJsonText) {
249
446
  try {
250
447
  const doc = JSON.parse(planJsonText);
@@ -2,11 +2,13 @@
2
2
  // vibe_pm.doctor - Installation health check and auto-repair
3
3
  import { DoctorInputSchema } from "../../generated/doctor_input.js";
4
4
  import { doctor as runDoctor, healthCheck, checkForUpdates } from "../../bootstrap/doctor.js";
5
+ import { getSkillsHealth } from "../../bootstrap/skills-installer.js";
5
6
  import { CONTRACTS_BUNDLE_SHA256, CONTRACTS_SCHEMA_COUNT, CONTRACTS_VERSION } from "../../generated/contracts_bundle_info.js";
6
7
  import { getAuthGate } from "../../auth/index.js";
7
8
  import { resolveProjectId } from "./context.js";
8
9
  import { memoryStatus } from "./memory_status.js";
9
10
  import { memorySync } from "./memory_sync.js";
11
+ import { runPythonCli, findPythonRepoRoot } from "../../engine.js";
10
12
  // ============================================================
11
13
  // Input/Output Types
12
14
  // ============================================================
@@ -26,6 +28,36 @@ function mapEngineStatus(status) {
26
28
  return "손상됨";
27
29
  }
28
30
  }
31
+ /**
32
+ * Check Python CLI health
33
+ * Verifies that `python -m vibecoding_helper` can be executed
34
+ */
35
+ async function checkPythonCli() {
36
+ const cwd = process.cwd();
37
+ const detectedRoot = findPythonRepoRoot(cwd);
38
+ try {
39
+ const result = await runPythonCli(["--help"], { timeoutMs: 10_000 });
40
+ if (result.code === 0) {
41
+ return {
42
+ status: "정상",
43
+ message: "Python CLI 정상 동작",
44
+ pythonpath_detected: detectedRoot
45
+ };
46
+ }
47
+ return {
48
+ status: "오류",
49
+ message: `Python CLI 오류: ${result.stderr?.slice(0, 100) ?? "알 수 없는 오류"}`,
50
+ pythonpath_detected: detectedRoot
51
+ };
52
+ }
53
+ catch (e) {
54
+ return {
55
+ status: "오류",
56
+ message: e instanceof Error ? e.message : "알 수 없는 오류",
57
+ pythonpath_detected: detectedRoot
58
+ };
59
+ }
60
+ }
29
61
  // ============================================================
30
62
  // Doctor Implementation
31
63
  // ============================================================
@@ -57,6 +89,23 @@ export async function doctor(input) {
57
89
  return null;
58
90
  }
59
91
  }
92
+ function getSkillBundleStatus() {
93
+ const skillsHealth = getSkillsHealth();
94
+ let status = "OK";
95
+ if (!skillsHealth.installed || skillsHealth.summary.missing > 0 || skillsHealth.summary.corrupted > 0) {
96
+ status = "ERROR";
97
+ }
98
+ else if (skillsHealth.version !== skillsHealth.version) {
99
+ // Note: In a real scenario, compare with expected version from registry
100
+ status = "WARN";
101
+ }
102
+ return {
103
+ status,
104
+ version: skillsHealth.version,
105
+ installed: skillsHealth.summary.ok,
106
+ total: skillsHealth.summary.total
107
+ };
108
+ }
60
109
  try {
61
110
  // Step 1: Quick health check
62
111
  const quickCheck = await healthCheck();
@@ -68,6 +117,8 @@ export async function doctor(input) {
68
117
  version: e.version,
69
118
  current_version: e.currentVersion
70
119
  }));
120
+ // Python CLI check
121
+ const pythonCliStatus = await checkPythonCli();
71
122
  // Local Memory check (best-effort)
72
123
  let localMemory = await getLocalMemoryStatus();
73
124
  if (autoFix && localMemory?.status === "NEEDS_SYNC") {
@@ -91,6 +142,23 @@ export async function doctor(input) {
91
142
  // ignore
92
143
  }
93
144
  }
145
+ // Check if Python CLI has issues
146
+ if (pythonCliStatus.status === "오류") {
147
+ return {
148
+ status: "NEEDS_ATTENTION",
149
+ contracts,
150
+ summary: "Python CLI에 문제가 있습니다.",
151
+ engines,
152
+ python_cli: pythonCliStatus,
153
+ skills: getSkillBundleStatus(),
154
+ local_memory: localMemory,
155
+ actions_taken: actionsTaken.length > 0 ? actionsTaken : undefined,
156
+ next_action: {
157
+ type: "MANUAL_FIX",
158
+ message: `Python CLI 오류: ${pythonCliStatus.message}. PYTHONPATH 설정을 확인하세요.`
159
+ }
160
+ };
161
+ }
94
162
  if (localMemory && localMemory.status !== "READY") {
95
163
  const msg = localMemory.status === "NEEDS_SYNC"
96
164
  ? "Local Memory 갱신이 필요합니다. vibe_pm.memory_sync를 실행해 보세요."
@@ -102,6 +170,8 @@ export async function doctor(input) {
102
170
  contracts,
103
171
  summary: "엔진은 정상입니다. 다만 Local Memory 준비가 필요합니다.",
104
172
  engines,
173
+ python_cli: pythonCliStatus,
174
+ skills: getSkillBundleStatus(),
105
175
  local_memory: localMemory,
106
176
  actions_taken: actionsTaken.length > 0 ? actionsTaken : undefined,
107
177
  next_action: {
@@ -115,6 +185,8 @@ export async function doctor(input) {
115
185
  contracts,
116
186
  summary: "모든 엔진이 정상적으로 설치되어 있습니다.",
117
187
  engines,
188
+ python_cli: pythonCliStatus,
189
+ skills: getSkillBundleStatus(),
118
190
  local_memory: localMemory,
119
191
  actions_taken: actionsTaken.length > 0 ? actionsTaken : undefined,
120
192
  next_action: {
@@ -136,6 +208,8 @@ export async function doctor(input) {
136
208
  version: info.version,
137
209
  current_version: info.currentVersion
138
210
  }));
211
+ // Python CLI check (after auto-fix)
212
+ const pythonCliStatus = await checkPythonCli();
139
213
  if (doctorResult.status === "OK") {
140
214
  actionsTaken.push("문제 자동 수정 완료");
141
215
  // Local Memory check (best-effort)
@@ -172,6 +246,8 @@ export async function doctor(input) {
172
246
  contracts,
173
247
  summary: "엔진은 정상입니다. 다만 Local Memory 준비가 필요합니다.",
174
248
  engines,
249
+ python_cli: pythonCliStatus,
250
+ skills: getSkillBundleStatus(),
175
251
  actions_taken: actionsTaken,
176
252
  local_memory: localMemory,
177
253
  next_action: {
@@ -185,6 +261,8 @@ export async function doctor(input) {
185
261
  contracts,
186
262
  summary: "문제를 발견하고 자동으로 수정했습니다.",
187
263
  engines,
264
+ python_cli: pythonCliStatus,
265
+ skills: getSkillBundleStatus(),
188
266
  actions_taken: actionsTaken,
189
267
  local_memory: localMemory,
190
268
  next_action: {
@@ -201,6 +279,8 @@ export async function doctor(input) {
201
279
  contracts,
202
280
  summary: "일부 엔진 업데이트가 필요합니다.",
203
281
  engines,
282
+ python_cli: pythonCliStatus,
283
+ skills: getSkillBundleStatus(),
204
284
  actions_taken: actionsTaken,
205
285
  local_memory: localMemory,
206
286
  next_action: {
@@ -216,6 +296,8 @@ export async function doctor(input) {
216
296
  contracts,
217
297
  summary: doctorResult.error ?? "알 수 없는 오류가 발생했습니다.",
218
298
  engines,
299
+ python_cli: pythonCliStatus,
300
+ skills: getSkillBundleStatus(),
219
301
  actions_taken: actionsTaken,
220
302
  local_memory: localMemory,
221
303
  next_action: {
@@ -231,6 +313,7 @@ export async function doctor(input) {
231
313
  version: e.version,
232
314
  current_version: e.currentVersion
233
315
  }));
316
+ const pythonCliStatus = await checkPythonCli();
234
317
  const issueCount = engines.filter((e) => e.status !== "정상").length;
235
318
  const localMemory = await getLocalMemoryStatus();
236
319
  return {
@@ -238,6 +321,8 @@ export async function doctor(input) {
238
321
  contracts,
239
322
  summary: `${issueCount}개의 엔진에 문제가 있습니다.`,
240
323
  engines,
324
+ python_cli: pythonCliStatus,
325
+ skills: getSkillBundleStatus(),
241
326
  local_memory: localMemory,
242
327
  next_action: {
243
328
  type: "MANUAL_FIX",
@@ -247,11 +332,21 @@ export async function doctor(input) {
247
332
  }
248
333
  catch (e) {
249
334
  const localMemory = await getLocalMemoryStatus();
335
+ // Try to get Python CLI status even in error state
336
+ let pythonCliStatus;
337
+ try {
338
+ pythonCliStatus = await checkPythonCli();
339
+ }
340
+ catch {
341
+ // ignore
342
+ }
250
343
  return {
251
344
  status: "ERROR",
252
345
  contracts,
253
346
  summary: e instanceof Error ? e.message : "알 수 없는 오류",
254
347
  engines: [],
348
+ python_cli: pythonCliStatus,
349
+ skills: getSkillBundleStatus(),
255
350
  local_memory: localMemory,
256
351
  next_action: {
257
352
  type: "CONTACT_SUPPORT",
@@ -3,9 +3,10 @@
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
5
  import { z } from "zod";
6
- import { runEngine } from "../../../engine.js";
6
+ import { runPythonCli } from "../../../engine.js";
7
7
  import { safeJsonParse } from "../../../cli.js";
8
8
  import { resolveRunDir } from "../context.js";
9
+ import { classifyPythonError } from "../python_error.js";
9
10
  const EntityGateReportSchema = z.object({
10
11
  status: z.enum(["OK", "FAIL"]),
11
12
  errors: z.array(z.string()),
@@ -38,9 +39,13 @@ function kceEventsPath(run_id, basePath) {
38
39
  export async function runEntityGate(args) {
39
40
  const eventsPath = args.writeEvidence && args.run_id ? kceEventsPath(args.run_id, args.basePath) : null;
40
41
  try {
41
- const { code, stdout, stderr } = await runEngine("vibecoding-helper", ["entity-gate"], { timeoutMs: 60_000 });
42
+ const { code, stdout, stderr } = await runPythonCli(["entity-gate"], { timeoutMs: 60_000 });
42
43
  if (code !== 0 && !stdout.trim()) {
43
- throw new Error(`entity-gate failed: ${stderr || `exit_code=${code}`}`);
44
+ const classified = classifyPythonError(stderr ?? "", code, "Entity Gate");
45
+ const hint = classified.issueCode === "python_module_not_found"
46
+ ? " (PYTHONPATH 문제 - vibe_pm.doctor 실행 권장)"
47
+ : "";
48
+ throw new Error(`${classified.issueCode}: ${classified.summary}${hint}`);
44
49
  }
45
50
  const parsed = safeJsonParse(stdout);
46
51
  if (!parsed.ok)
@@ -2,14 +2,13 @@
2
2
  // vibe_pm.export_output - Export a feature into a chosen output format via Output Adapters
3
3
  import * as path from "node:path";
4
4
  import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
5
- import { runEngine } from "../../engine.js";
5
+ import { runPythonCli } from "../../engine.js";
6
6
  import { safeJsonParse } from "../../cli.js";
7
7
  import { validateToolInput } from "../../security/input-validator.js";
8
8
  import { isPathForbidden, validatePath } from "../../security/path-policy.js";
9
9
  import { resolveProjectId, resolveRunId, resolveRunDir } from "./context.js";
10
- function normalizeRelPosix(p) {
11
- return p.replace(/\\/g, "/").replace(/^\/+/, "");
12
- }
10
+ import { classifyPythonError } from "./python_error.js";
11
+ import { normalizeRelativePosix } from "../../path-utils.js";
13
12
  function resolveDefaultOutputDir(target) {
14
13
  const t = (target ?? "").trim();
15
14
  return t ? `export/${t}` : "export/out";
@@ -41,7 +40,7 @@ export async function exportOutput(input) {
41
40
  }
42
41
  const outputDirInput = (input.output_dir ?? "").trim() || resolveDefaultOutputDir(target);
43
42
  const outputDirAbs = validatePath(outputDirInput, basePath);
44
- const outputDirRel = normalizeRelPosix(path.relative(basePath, outputDirAbs));
43
+ const outputDirRel = normalizeRelativePosix(path.relative(basePath, outputDirAbs));
45
44
  const forbid = isPathForbidden(outputDirRel);
46
45
  if (forbid.forbidden) {
47
46
  throw new McpError(ErrorCode.InvalidParams, "Forbidden export output_dir.", {
@@ -64,9 +63,10 @@ export async function exportOutput(input) {
64
63
  }
65
64
  if (dryRun)
66
65
  planCmd.push("--dry-run");
67
- const planRes = await runEngine("vibecoding-helper", planCmd, { timeoutMs: 120_000 });
66
+ const planRes = await runPythonCli(planCmd, { timeoutMs: 120_000 });
68
67
  if (planRes.code !== 0) {
69
- throw new Error(`export-plan failed: ${planRes.stderr || `exit_code=${planRes.code}`}`);
68
+ const classified = classifyPythonError(planRes.stderr ?? "", planRes.code, "export-plan");
69
+ throw new Error(`${classified.issueCode}: ${classified.summary}`);
70
70
  }
71
71
  const planParsed = safeJsonParse(planRes.stdout);
72
72
  if (!planParsed.ok) {
@@ -94,9 +94,10 @@ export async function exportOutput(input) {
94
94
  ];
95
95
  if (needsTemplatePin)
96
96
  genCmd.push("--template-repo", templateRepo, "--template-ref", templateRef);
97
- const genRes = await runEngine("vibecoding-helper", genCmd, { timeoutMs: 300_000 });
97
+ const genRes = await runPythonCli(genCmd, { timeoutMs: 300_000 });
98
98
  if (genRes.code !== 0) {
99
- throw new Error(`export-generate failed: ${genRes.stderr || `exit_code=${genRes.code}`}`);
99
+ const classified = classifyPythonError(genRes.stderr ?? "", genRes.code, "export-generate");
100
+ throw new Error(`${classified.issueCode}: ${classified.summary}`);
100
101
  }
101
102
  const genParsed = safeJsonParse(genRes.stdout);
102
103
  if (!genParsed.ok) {
@@ -107,10 +108,10 @@ export async function exportOutput(input) {
107
108
  for (const f of files) {
108
109
  const p = typeof f?.path === "string" ? f.path : "";
109
110
  if (p)
110
- createdPaths.push(normalizeRelPosix(p));
111
+ createdPaths.push(normalizeRelativePosix(p));
111
112
  }
112
113
  }
113
- artifactsPathRel = normalizeRelPosix(path.relative(basePath, exportArtifactsPath(runDirAbs)));
114
+ artifactsPathRel = normalizeRelativePosix(path.relative(basePath, exportArtifactsPath(runDirAbs)));
114
115
  }
115
116
  const out = {
116
117
  success: true,
@@ -120,8 +121,8 @@ export async function exportOutput(input) {
120
121
  output_dir: outputDirRel,
121
122
  dry_run: dryRun,
122
123
  paths: {
123
- feature_output_spec_path: normalizeRelPosix(path.relative(basePath, specAbs)),
124
- export_plan_path: normalizeRelPosix(path.relative(basePath, exportPlanPath(runDirAbs))),
124
+ feature_output_spec_path: normalizeRelativePosix(path.relative(basePath, specAbs)),
125
+ export_plan_path: normalizeRelativePosix(path.relative(basePath, exportPlanPath(runDirAbs))),
125
126
  export_artifacts_path: artifactsPathRel
126
127
  },
127
128
  created_paths: createdPaths,