@vibecodetown/mcp-server 2.2.0 → 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 (70) hide show
  1. package/README.md +10 -10
  2. package/build/auth/index.js +0 -2
  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/dx/activity.js +26 -3
  9. package/build/engine.js +151 -0
  10. package/build/errors.js +107 -0
  11. package/build/generated/bridge_build_seed_input.js +2 -0
  12. package/build/generated/bridge_build_seed_output.js +2 -0
  13. package/build/generated/bridge_confirm_reference_input.js +2 -0
  14. package/build/generated/bridge_confirm_reference_output.js +2 -0
  15. package/build/generated/bridge_confirmed_reference_file.js +2 -0
  16. package/build/generated/bridge_generate_references_input.js +2 -0
  17. package/build/generated/bridge_generate_references_output.js +2 -0
  18. package/build/generated/bridge_references_file.js +2 -0
  19. package/build/generated/bridge_work_order_seed_file.js +2 -0
  20. package/build/generated/contracts_bundle_info.js +3 -3
  21. package/build/generated/index.js +14 -0
  22. package/build/generated/ingress_input.js +2 -0
  23. package/build/generated/ingress_output.js +2 -0
  24. package/build/generated/ingress_resolution_file.js +2 -0
  25. package/build/generated/ingress_summary_file.js +2 -0
  26. package/build/generated/message_template_id_mapping_file.js +2 -0
  27. package/build/generated/run_app_input.js +1 -1
  28. package/build/index.js +4 -3
  29. package/build/local-mode/paths.js +1 -0
  30. package/build/local-mode/setup.js +21 -1
  31. package/build/path-utils.js +68 -0
  32. package/build/runtime/cli_invoker.js +1 -1
  33. package/build/tools/vibe_pm/advisory_review.js +5 -3
  34. package/build/tools/vibe_pm/bridge_build_seed.js +164 -0
  35. package/build/tools/vibe_pm/bridge_confirm_reference.js +91 -0
  36. package/build/tools/vibe_pm/bridge_generate_references.js +258 -0
  37. package/build/tools/vibe_pm/briefing.js +27 -3
  38. package/build/tools/vibe_pm/context.js +79 -0
  39. package/build/tools/vibe_pm/create_work_order.js +200 -3
  40. package/build/tools/vibe_pm/doctor.js +95 -0
  41. package/build/tools/vibe_pm/entity_gate/preflight.js +8 -3
  42. package/build/tools/vibe_pm/export_output.js +14 -13
  43. package/build/tools/vibe_pm/finalize_work.js +78 -40
  44. package/build/tools/vibe_pm/get_decision.js +2 -2
  45. package/build/tools/vibe_pm/index.js +128 -42
  46. package/build/tools/vibe_pm/ingress.js +645 -0
  47. package/build/tools/vibe_pm/ingress_gate.js +116 -0
  48. package/build/tools/vibe_pm/inspect_code.js +90 -20
  49. package/build/tools/vibe_pm/kce/doc_usage.js +4 -9
  50. package/build/tools/vibe_pm/kce/on_finalize.js +2 -2
  51. package/build/tools/vibe_pm/kce/preflight.js +11 -7
  52. package/build/tools/vibe_pm/memory_status.js +11 -8
  53. package/build/tools/vibe_pm/memory_sync.js +11 -8
  54. package/build/tools/vibe_pm/pm_language.js +17 -16
  55. package/build/tools/vibe_pm/python_error.js +115 -0
  56. package/build/tools/vibe_pm/run_app.js +169 -43
  57. package/build/tools/vibe_pm/run_app_podman.js +64 -2
  58. package/build/tools/vibe_pm/search_oss.js +5 -3
  59. package/build/tools/vibe_pm/spec_rag.js +185 -0
  60. package/build/tools/vibe_pm/status.js +50 -3
  61. package/build/tools/vibe_pm/submit_decision.js +2 -2
  62. package/build/tools/vibe_pm/types.js +28 -0
  63. package/build/tools/vibe_pm/undo_last_task.js +9 -2
  64. package/build/tools/vibe_pm/waiter_mapping.js +155 -0
  65. package/build/tools/vibe_pm/zoekt_evidence.js +5 -3
  66. package/build/tools.js +13 -5
  67. package/build/vibe-cli.js +245 -7
  68. package/package.json +5 -4
  69. package/skills/VRIP_INSTALL_MANIFEST_DOCTOR.skill.md +288 -0
  70. package/skills/index.json +14 -0
@@ -0,0 +1,116 @@
1
+ // adapters/mcp-ts/src/tools/vibe_pm/ingress_gate.ts
2
+ // Internal: Ingress v2 runtime gate (ingress_resolution.json)
3
+ import * as fs from "node:fs";
4
+ import * as fsPromises from "node:fs/promises";
5
+ import * as path from "node:path";
6
+ import { resolveProjectId, getRunContext, resolveRunDir } from "./context.js";
7
+ import { IngressResolutionFileSchema } from "../../generated/ingress_resolution_file.js";
8
+ function toRelPath(basePath, absPath) {
9
+ const rel = path.relative(basePath, absPath);
10
+ return rel.replace(/\\\\/g, "/");
11
+ }
12
+ async function writeJsonAtomic(absPath, doc) {
13
+ await fsPromises.mkdir(path.dirname(absPath), { recursive: true });
14
+ const tmp = `${absPath}.tmp`;
15
+ await fsPromises.writeFile(tmp, JSON.stringify(doc, null, 2) + "\n", "utf-8");
16
+ await fsPromises.rename(tmp, absPath);
17
+ }
18
+ function ingressDir(runDir) {
19
+ return path.join(runDir, "ingress");
20
+ }
21
+ export function ingressResolutionPathAbs(runDir) {
22
+ return path.join(ingressDir(runDir), "ingress_resolution.json");
23
+ }
24
+ export function isStrictIngressEnabled() {
25
+ const raw = (process.env.VIBECODE_STRICT_INGRESS ?? "").trim().toLowerCase();
26
+ if (!raw)
27
+ return false;
28
+ return !(raw === "0" || raw === "false" || raw === "no" || raw === "off");
29
+ }
30
+ export function isIngressGateRequiredForTool(toolName) {
31
+ // Keep the gate minimal and focused on "Back 단계" tools that can cause drift/confusion.
32
+ return (toolName === "vibe_pm.get_decision" ||
33
+ toolName === "vibe_pm.submit_decision" ||
34
+ toolName === "vibe_pm.create_work_order" ||
35
+ toolName === "vibe_pm.inspect_code" ||
36
+ toolName === "vibe_pm.run_app" ||
37
+ toolName === "vibe_pm.finalize_work" ||
38
+ toolName === "vibe_pm.undo_last_task");
39
+ }
40
+ export async function checkIngressGate(args) {
41
+ const allowLegacyAuto = args.allowLegacyAuto !== false;
42
+ // If the run directory doesn't exist at all, do not block.
43
+ const resolved = resolveRunDir(args.run_id, args.basePath);
44
+ const maybeLegacyRunDir = path.join(args.basePath, "runs", args.run_id);
45
+ if (!resolved && !fs.existsSync(maybeLegacyRunDir)) {
46
+ return { status: "SKIP_NO_RUN", reason: "run_dir_not_found" };
47
+ }
48
+ const context = getRunContext(args.run_id, args.basePath);
49
+ const absPath = ingressResolutionPathAbs(context.runs_path);
50
+ const relPath = toRelPath(args.basePath, absPath);
51
+ let legacyWritten = false;
52
+ if (!fs.existsSync(absPath)) {
53
+ if (isStrictIngressEnabled() || !allowLegacyAuto) {
54
+ return {
55
+ status: "BLOCK",
56
+ message: "프로젝트 시작 상태가 확정되지 않았습니다. 먼저 시작(ingress) 단계를 완료해주세요.",
57
+ recovery: "vibe_pm.ingress를 먼저 호출해 프로젝트 이어가기/새 작업을 확정하세요.",
58
+ details: { run_id: args.run_id, ingress_resolution_path: relPath }
59
+ };
60
+ }
61
+ const project_id = resolveProjectId(args.run_id, args.basePath);
62
+ const hasVibeState = fs.existsSync(path.join(args.basePath, ".vibe"));
63
+ const doc = IngressResolutionFileSchema.parse({
64
+ schema_version: "ingress.resolution.v2",
65
+ run_id: args.run_id,
66
+ project_id,
67
+ workspace_path: path.resolve(args.basePath),
68
+ resolution: "INGRESS_RESOLVED_NEW_TASK",
69
+ has_vibe_state: hasVibeState,
70
+ context_scan_required: false,
71
+ context_scan_completed: false,
72
+ created_at: new Date().toISOString()
73
+ });
74
+ await writeJsonAtomic(absPath, doc);
75
+ legacyWritten = true;
76
+ }
77
+ let raw;
78
+ try {
79
+ raw = JSON.parse(await fsPromises.readFile(absPath, "utf-8"));
80
+ }
81
+ catch {
82
+ return {
83
+ status: "BLOCK",
84
+ message: "프로젝트 시작 상태 파일을 읽을 수 없습니다.",
85
+ recovery: "vibe_pm.ingress를 다시 실행해 시작 상태를 다시 기록해주세요.",
86
+ details: { run_id: args.run_id, ingress_resolution_path: relPath }
87
+ };
88
+ }
89
+ const parsed = IngressResolutionFileSchema.safeParse(raw);
90
+ if (!parsed.success) {
91
+ return {
92
+ status: "BLOCK",
93
+ message: "프로젝트 시작 상태 파일 형식이 예상과 다릅니다.",
94
+ recovery: "도구 업데이트 후, vibe_pm.ingress로 시작 상태를 다시 생성해주세요.",
95
+ details: { run_id: args.run_id, ingress_resolution_path: relPath }
96
+ };
97
+ }
98
+ const doc = parsed.data;
99
+ if (doc.resolution === "INGRESS_ABORTED") {
100
+ return {
101
+ status: "BLOCK",
102
+ message: "프로젝트가 시작되지 않았습니다(중단됨).",
103
+ recovery: "vibe_pm.ingress에서 다시 시작 상태를 확정한 뒤 진행하세요.",
104
+ details: { run_id: args.run_id, ingress_resolution_path: relPath }
105
+ };
106
+ }
107
+ if (doc.context_scan_required && doc.context_scan_completed !== true) {
108
+ return {
109
+ status: "BLOCK",
110
+ message: "기존 프로젝트 이어가기를 위한 사전 점검(컨텍스트 스캔)이 완료되지 않았습니다.",
111
+ recovery: "vibe_pm.ingress를 context_scan=true로 다시 실행해 스캔을 완료하세요.",
112
+ details: { run_id: args.run_id, ingress_resolution_path: relPath }
113
+ };
114
+ }
115
+ return { status: "OK", doc, resolution_path: relPath, legacy_written: legacyWritten };
116
+ }
@@ -3,7 +3,7 @@
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
5
  import { parse as parseYaml } from "yaml";
6
- import { runEngine, invalidateEngineCache } from "../../engine.js";
6
+ import { runPythonCli, invalidateEngineCache } from "../../engine.js";
7
7
  import { safeJsonParse } from "../../cli.js";
8
8
  import { resolveRunId, resolveProjectId, resolveBridgePath, readRunState, getRunsDir, resolveRunDir, invalidateStateCache } from "./context.js";
9
9
  import { validateInspectCodeInput } from "../../security/input-validator.js";
@@ -21,6 +21,8 @@ import { kcePreflight } from "./kce/preflight.js";
21
21
  import { loadRulebook } from "./system_design/rulebook.js";
22
22
  import { runSemgrepDesignEnforcement } from "./system_design/semgrep.js";
23
23
  import { semgrepFindingsToIssues } from "./system_design/issue_mapping.js";
24
+ import { isOpaEvalFailedError } from "../../errors.js";
25
+ import { normalizeForMatch } from "../../path-utils.js";
24
26
  /**
25
27
  * vibe_pm.inspect_code - Code review and validation
26
28
  *
@@ -251,7 +253,7 @@ export async function inspectCode(input) {
251
253
  args.push("--quick");
252
254
  }
253
255
  // Run one-loop validation
254
- const { code, stdout, stderr } = await runEngine("vibecoding-helper", args, {
256
+ const { code, stdout, stderr } = await runPythonCli(args, {
255
257
  timeoutMs: 180_000
256
258
  });
257
259
  // Parse result
@@ -278,7 +280,7 @@ export async function inspectCode(input) {
278
280
  !issues.some((i) => i.type === "RULE_VIOLATION" || i.type === "RISK_SPIKE")) {
279
281
  const recheckArgs = input.mode === "quick" ? args.filter((a) => a !== "--quick") : args;
280
282
  try {
281
- const recheck = await runEngine("vibecoding-helper", recheckArgs, { timeoutMs: 180_000 });
283
+ const recheck = await runPythonCli(recheckArgs, { timeoutMs: 180_000 });
282
284
  const reParsed = safeJsonParse(recheck.stdout);
283
285
  if (reParsed.ok) {
284
286
  const reValidated = VibecodingHelperOneLoopSelectionOutputSchema.safeParse(reParsed.value);
@@ -350,6 +352,17 @@ export async function inspectCode(input) {
350
352
  catch {
351
353
  // ignore (best-effort)
352
354
  }
355
+ // SRWP: Check for SPEC_UPDATE_MISSING condition
356
+ // Code changed but no spec updated → FIX issue
357
+ const specMissingIssue = checkSpecUpdateMissing(modifiedFiles);
358
+ if (specMissingIssue) {
359
+ issues.push(specMissingIssue);
360
+ // Downgrade GO → FIX when spec update is missing
361
+ if (reviewResult === "GO") {
362
+ reviewResult = "FIX";
363
+ verdict = "스펙 업데이트가 필요합니다";
364
+ }
365
+ }
353
366
  // Run Intent-based verification (if intent document exists)
354
367
  let intentCheck;
355
368
  try {
@@ -491,14 +504,11 @@ function readOpaPolicyResult(basePath, run_id) {
491
504
  }
492
505
  }
493
506
  function classifyOpaPolicyResult(result) {
494
- const reasons = (result.reasons ?? []).join(" ").toLowerCase();
495
- const matched = (result.matched_rules ?? []).join(" ").toLowerCase();
507
+ const reasons = (result.reasons ?? []).join(" ");
508
+ const matched = (result.matched_rules ?? []).join(" ");
496
509
  const corpus = `${reasons} ${matched}`.trim();
497
- if (corpus.includes("eval_failed") ||
498
- corpus.includes("opa_eval_failed") ||
499
- corpus.includes("opa exception") ||
500
- corpus.includes("opa_exception") ||
501
- corpus.includes("eval failed")) {
510
+ // P1-4: Use centralized error pattern for OPA eval failures
511
+ if (isOpaEvalFailedError(corpus)) {
502
512
  return {
503
513
  rule_violated: "OPA_POLICY_EVAL_FAILED",
504
514
  one_line_verdict: "OPA 평가에 실패하여 안전을 위해 차단했습니다(기본 차단).",
@@ -707,7 +717,7 @@ function extractAllowPathsFromBridge(bridgeDoc) {
707
717
  if (Array.isArray(dg?.allow_paths)) {
708
718
  return dg.allow_paths
709
719
  .filter((item) => typeof item === "string")
710
- .map((s) => normalizeAllowPath(s));
720
+ .map((s) => normalizeForMatch(s));
711
721
  }
712
722
  const scope = data?.scope;
713
723
  if (scope && typeof scope === "object") {
@@ -715,29 +725,24 @@ function extractAllowPathsFromBridge(bridgeDoc) {
715
725
  if (Array.isArray(include)) {
716
726
  return include
717
727
  .filter((item) => typeof item === "string")
718
- .map((s) => normalizeAllowPath(s));
728
+ .map((s) => normalizeForMatch(s));
719
729
  }
720
730
  }
721
731
  return [];
722
732
  }
723
- function normalizeAllowPath(value) {
724
- let v = (value ?? "").trim().replace(/\\/g, "/");
725
- if (v.startsWith("./"))
726
- v = v.slice(2);
727
- return v;
728
- }
733
+ // P1-5: normalizeForMatch -> normalizeForMatch (centralized)
729
734
  function findInvalidAllowPaths(allowPaths) {
730
735
  if (!Array.isArray(allowPaths) || allowPaths.length === 0)
731
736
  return [];
732
737
  const allowedPrefixes = new Set(PATH_ZONES.GREEN.map((p) => {
733
- const norm = normalizeAllowPath(p);
738
+ const norm = normalizeForMatch(p);
734
739
  const idx = norm.indexOf("**");
735
740
  const base = idx >= 0 ? norm.slice(0, idx) : norm;
736
741
  return base.endsWith("/") ? base : base + "/";
737
742
  }));
738
743
  const out = [];
739
744
  for (const raw of allowPaths) {
740
- const p = normalizeAllowPath(raw);
745
+ const p = normalizeForMatch(raw);
741
746
  if (!p)
742
747
  continue;
743
748
  // Minimal path traversal / absolute-path guard
@@ -791,6 +796,71 @@ function collectModifiedFiles(result) {
791
796
  function escapeRegExp(value) {
792
797
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
793
798
  }
799
+ // ============================================================
800
+ // SRWP: Spec-Driven Loop Detection
801
+ // ============================================================
802
+ /**
803
+ * Detect if any files are code changes (not docs/config)
804
+ */
805
+ function hasCodeChanges(files) {
806
+ const codePatterns = [
807
+ /^src\//,
808
+ /^lib\//,
809
+ /^tests?\//,
810
+ /^scripts\//,
811
+ /^adapters\//,
812
+ /^engines\//,
813
+ /^vibecoding_helper\//,
814
+ /\.ts$/,
815
+ /\.tsx$/,
816
+ /\.js$/,
817
+ /\.jsx$/,
818
+ /\.py$/,
819
+ /\.rs$/,
820
+ /\.go$/
821
+ ];
822
+ return files.some((f) => {
823
+ const normalized = f.replace(/\\/g, "/");
824
+ return codePatterns.some((p) => p.test(normalized));
825
+ });
826
+ }
827
+ /**
828
+ * Detect if any files are spec/documentation changes
829
+ */
830
+ function hasSpecChanges(files) {
831
+ return files.some((f) => {
832
+ const normalized = f.replace(/\\/g, "/");
833
+ return (normalized === "docs/SSOT.md" ||
834
+ normalized === "docs/CURRENT_SPEC.md" ||
835
+ normalized.startsWith("docs/ssot/") ||
836
+ normalized.startsWith("docs/specs/") ||
837
+ normalized.startsWith("docs/dev_logs/") ||
838
+ normalized.startsWith("docs/planning/"));
839
+ });
840
+ }
841
+ /**
842
+ * Check for SPEC_UPDATE_MISSING condition
843
+ * Returns an Issue if code changed but no spec updated
844
+ */
845
+ function checkSpecUpdateMissing(modifiedFiles) {
846
+ const codeChanged = hasCodeChanges(modifiedFiles);
847
+ const specChanged = hasSpecChanges(modifiedFiles);
848
+ if (codeChanged && !specChanged) {
849
+ const codeFileCount = modifiedFiles.filter((f) => hasCodeChanges([f])).length;
850
+ return {
851
+ type: "RULE_VIOLATION",
852
+ business_risk: "코드는 바뀌었는데 스펙 문서가 업데이트되지 않았습니다. " +
853
+ "이 상태로 배포하면 문서와 코드가 어긋나 유지보수가 어려워집니다.",
854
+ plain_explanation: `${codeFileCount}개 코드 파일이 변경되었지만 스펙 문서(docs/SSOT.md, docs/ssot/*, docs/dev_logs/*) 업데이트가 없습니다. ` +
855
+ "이번 작업의 변경사항을 문서에 반영하세요. (3회 반복 시 BLOCK)",
856
+ technical_detail: {
857
+ file_path: "docs/SSOT.md",
858
+ rule_violated: "SPEC_UPDATE_MISSING"
859
+ }
860
+ };
861
+ }
862
+ return null;
863
+ }
794
864
  /**
795
865
  * Create error output for parsing/execution failures
796
866
  */
@@ -3,9 +3,7 @@
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
5
  import { resolveRunDir } from "../context.js";
6
- function normalizeRelPosix(p) {
7
- return (p ?? "").trim().replace(/\\\\/g, "/");
8
- }
6
+ import { uniquePosixPaths } from "../../../path-utils.js";
9
7
  function tryReadJson(p) {
10
8
  try {
11
9
  if (!fs.existsSync(p))
@@ -16,9 +14,6 @@ function tryReadJson(p) {
16
14
  return null;
17
15
  }
18
16
  }
19
- function uniqSorted(items) {
20
- return Array.from(new Set(items.map(normalizeRelPosix).filter(Boolean))).sort();
21
- }
22
17
  export function kceDocUsagePath(run_id, basePath) {
23
18
  const resolved = resolveRunDir(run_id, basePath);
24
19
  if (!resolved)
@@ -30,9 +25,9 @@ export function updateKceDocUsage(args) {
30
25
  if (!p)
31
26
  return;
32
27
  const existing = tryReadJson(p);
33
- const touched = uniqSorted([...(existing?.touched_docs ?? []), ...(args.touched_docs ?? [])]);
34
- const injected = uniqSorted([...(existing?.injected_docs ?? []), ...(args.injected_docs ?? [])]);
35
- const entities = uniqSorted([...(existing?.referenced_entities ?? []), ...(args.referenced_entities ?? [])]);
28
+ const touched = uniquePosixPaths([...(existing?.touched_docs ?? []), ...(args.touched_docs ?? [])]);
29
+ const injected = uniquePosixPaths([...(existing?.injected_docs ?? []), ...(args.injected_docs ?? [])]);
30
+ const entities = uniquePosixPaths([...(existing?.referenced_entities ?? []), ...(args.referenced_entities ?? [])]);
36
31
  const out = {
37
32
  version: "kce_doc_usage.v1",
38
33
  created_at: new Date().toISOString(),
@@ -1,9 +1,9 @@
1
1
  // adapters/mcp-ts/src/tools/vibe_pm/kce/on_finalize.ts
2
2
  // Internal: best-effort KCE sync after finalize_work (keeps KCE warm).
3
- import { runEngine } from "../../../engine.js";
3
+ import { runPythonCli } from "../../../engine.js";
4
4
  export async function kickoffKceSyncBestEffort() {
5
5
  try {
6
- await runEngine("vibecoding-helper", ["kce-sync", "--prune", "--skip-missing-roots"], { timeoutMs: 300_000 });
6
+ await runPythonCli(["kce-sync", "--prune", "--skip-missing-roots"], { timeoutMs: 300_000 });
7
7
  }
8
8
  catch {
9
9
  // ignore
@@ -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 KceStatusSchema = z.object({
10
11
  version: z.literal("kce_status.v1"),
11
12
  readiness_state: z.enum(["READY", "HEALING", "FAILED"]),
@@ -89,9 +90,10 @@ function kceEventsPath(run_id, basePath) {
89
90
  return path.join(dir, "kce_events.jsonl");
90
91
  }
91
92
  export async function runKceStatus() {
92
- const { code, stdout, stderr } = await runEngine("vibecoding-helper", ["kce-status"], { timeoutMs: 60_000 });
93
+ const { code, stdout, stderr } = await runPythonCli(["kce-status"], { timeoutMs: 60_000 });
93
94
  if (code !== 0) {
94
- throw new Error(`kce-status failed: ${stderr || `exit_code=${code}`}`);
95
+ const classified = classifyPythonError(stderr ?? "", code, "KCE 상태 확인");
96
+ throw new Error(`${classified.issueCode}: ${classified.summary}`);
95
97
  }
96
98
  const parsed = safeJsonParse(stdout);
97
99
  if (!parsed.ok)
@@ -109,11 +111,12 @@ export async function runKceSync(args) {
109
111
  cmd.push("--skip-missing-roots");
110
112
  if (args.forceFull)
111
113
  cmd.push("--force-full");
112
- const { code, stdout, stderr } = await runEngine("vibecoding-helper", cmd, {
114
+ const { code, stdout, stderr } = await runPythonCli(cmd, {
113
115
  timeoutMs: Math.max(60_000, args.timeoutMs ?? 300_000)
114
116
  });
115
117
  if (code !== 0) {
116
- throw new Error(`kce-sync failed: ${stderr || `exit_code=${code}`}`);
118
+ const classified = classifyPythonError(stderr ?? "", code, "KCE 동기화");
119
+ throw new Error(`${classified.issueCode}: ${classified.summary}`);
117
120
  }
118
121
  const parsed = safeJsonParse(stdout);
119
122
  if (!parsed.ok)
@@ -139,9 +142,10 @@ export async function runKceRetrieve(args) {
139
142
  ];
140
143
  if (args.pathPrefix)
141
144
  cmd.push("--path-prefix", args.pathPrefix);
142
- const { code, stdout, stderr } = await runEngine("vibecoding-helper", cmd, { timeoutMs: 60_000 });
145
+ const { code, stdout, stderr } = await runPythonCli(cmd, { timeoutMs: 60_000 });
143
146
  if (code !== 0) {
144
- throw new Error(`kce-retrieve failed: ${stderr || `exit_code=${code}`}`);
147
+ const classified = classifyPythonError(stderr ?? "", code, "KCE 검색");
148
+ throw new Error(`${classified.issueCode}: ${classified.summary}`);
145
149
  }
146
150
  const parsed = safeJsonParse(stdout);
147
151
  if (!parsed.ok)
@@ -1,10 +1,11 @@
1
1
  // adapters/mcp-ts/src/tools/vibe_pm/memory_status.ts
2
2
  // vibe_pm.memory_status - Local Memory status (ChromaDB index readiness)
3
- import { runEngine } from "../../engine.js";
3
+ import { runPythonCliWithCache } from "../../engine.js";
4
4
  import { safeJsonParse } from "../../cli.js";
5
5
  import { validateToolInput } from "../../security/input-validator.js";
6
6
  import { resolveProjectId } from "./context.js";
7
7
  import { MemoryStatusOutputSchema } from "../../generated/memory_status_output.js";
8
+ import { classifyPythonError } from "./python_error.js";
8
9
  function isOfflineMode() {
9
10
  const v = (process.env.VIBECODE_OFFLINE ?? "").trim().toLowerCase();
10
11
  return v === "1" || v === "true" || v === "yes" || v === "on";
@@ -29,20 +30,21 @@ export async function memoryStatus(input) {
29
30
  const persistDir = (input.persist_dir ?? defaultPersistDir()).trim();
30
31
  try {
31
32
  const args = ["memory-status", "--project-id", project_id, "--docs-root", docsRoot, "--persist-dir", persistDir];
32
- const { code, stdout, stderr } = await runEngine("vibecoding-helper", args, { timeoutMs: 60_000 });
33
+ const { code, stdout, stderr } = await runPythonCliWithCache(args, { timeoutMs: 60_000 });
33
34
  if (code !== 0) {
35
+ const classified = classifyPythonError(stderr ?? "", code, "Local Memory 상태 확인");
34
36
  return MemoryStatusOutputSchema.parse({
35
37
  project_id,
36
38
  status: "ERROR",
37
- summary: "Local Memory 상태 확인에 실패했습니다.",
39
+ summary: classified.summary,
38
40
  docs_root: docsRoot,
39
41
  persist_dir: persistDir,
40
42
  collection: "vibe_docs",
41
43
  stats: { document_count: 0, chunk_count: 0, last_sync_at: null },
42
44
  embedding: { backend: "unknown", model: null, ready: false },
43
45
  offline_mode: isOfflineMode(),
44
- issues: [stderr?.trim() ? `engine_error: ${stderr.trim()}` : `engine_exit_code: ${code}`],
45
- next_action: { tool: "vibe_pm.doctor", reason: "설치/환경 상태를 점검하겠습니다." }
46
+ issues: [`${classified.issueCode}: ${stderr?.trim() || `exit_code=${code}`}`],
47
+ next_action: classified.nextAction
46
48
  });
47
49
  }
48
50
  const parsed = safeJsonParse(stdout);
@@ -65,18 +67,19 @@ export async function memoryStatus(input) {
65
67
  }
66
68
  catch (e) {
67
69
  const msg = e instanceof Error ? e.message : String(e);
70
+ const classified = classifyPythonError(msg, 1, "Local Memory 상태 확인");
68
71
  return MemoryStatusOutputSchema.parse({
69
72
  project_id,
70
73
  status: "ERROR",
71
- summary: "Local Memory 상태 확인 중 오류가 발생했습니다.",
74
+ summary: classified.summary,
72
75
  docs_root: docsRoot,
73
76
  persist_dir: persistDir,
74
77
  collection: "vibe_docs",
75
78
  stats: { document_count: 0, chunk_count: 0, last_sync_at: null },
76
79
  embedding: { backend: "unknown", model: null, ready: false },
77
80
  offline_mode: isOfflineMode(),
78
- issues: [msg],
79
- next_action: { tool: "vibe_pm.doctor", reason: "설치/환경 상태를 점검하겠습니다." }
81
+ issues: [`${classified.issueCode}: ${msg}`],
82
+ next_action: classified.nextAction
80
83
  });
81
84
  }
82
85
  }
@@ -1,10 +1,11 @@
1
1
  // adapters/mcp-ts/src/tools/vibe_pm/memory_sync.ts
2
2
  // vibe_pm.memory_sync - Local Memory sync (incremental index update)
3
- import { runEngine } from "../../engine.js";
3
+ import { runPythonCli } from "../../engine.js";
4
4
  import { safeJsonParse } from "../../cli.js";
5
5
  import { validateToolInput } from "../../security/input-validator.js";
6
6
  import { resolveProjectId } from "./context.js";
7
7
  import { MemorySyncOutputSchema } from "../../generated/memory_sync_output.js";
8
+ import { classifyPythonError } from "./python_error.js";
8
9
  const inFlight = new Map();
9
10
  function isOfflineMode() {
10
11
  const v = (process.env.VIBECODE_OFFLINE ?? "").trim().toLowerCase();
@@ -59,13 +60,14 @@ export async function memorySync(input) {
59
60
  if (forceFull)
60
61
  args.push("--force-full");
61
62
  const startedAt = Date.now();
62
- const { code, stdout, stderr } = await runEngine("vibecoding-helper", args, { timeoutMs: 300_000 });
63
+ const { code, stdout, stderr } = await runPythonCli(args, { timeoutMs: 300_000 });
63
64
  const durationMs = Date.now() - startedAt;
64
65
  if (code !== 0) {
66
+ const classified = classifyPythonError(stderr ?? "", code, "Local Memory 동기화");
65
67
  return MemorySyncOutputSchema.parse({
66
68
  project_id,
67
69
  status: "ERROR",
68
- summary: "Local Memory 동기화에 실패했습니다.",
70
+ summary: classified.summary,
69
71
  docs_root: docsRoot,
70
72
  persist_dir: persistDir,
71
73
  collection: "vibe_docs",
@@ -77,8 +79,8 @@ export async function memorySync(input) {
77
79
  duration_ms: durationMs
78
80
  },
79
81
  offline_mode: isOfflineMode(),
80
- issues: [stderr?.trim() ? `engine_error: ${stderr.trim()}` : `engine_exit_code: ${code}`],
81
- next_action: { tool: "vibe_pm.doctor", reason: "설치/환경 상태를 점검하겠습니다." }
82
+ issues: [`${classified.issueCode}: ${stderr?.trim() || `exit_code=${code}`}`],
83
+ next_action: classified.nextAction
82
84
  });
83
85
  }
84
86
  const parsed = safeJsonParse(stdout);
@@ -106,10 +108,11 @@ export async function memorySync(input) {
106
108
  }
107
109
  catch (e) {
108
110
  const msg = e instanceof Error ? e.message : String(e);
111
+ const classified = classifyPythonError(msg, 1, "Local Memory 동기화");
109
112
  return MemorySyncOutputSchema.parse({
110
113
  project_id,
111
114
  status: "ERROR",
112
- summary: "Local Memory 동기화 중 오류가 발생했습니다.",
115
+ summary: classified.summary,
113
116
  docs_root: docsRoot,
114
117
  persist_dir: persistDir,
115
118
  collection: "vibe_docs",
@@ -121,8 +124,8 @@ export async function memorySync(input) {
121
124
  duration_ms: 0
122
125
  },
123
126
  offline_mode: isOfflineMode(),
124
- issues: [msg],
125
- next_action: { tool: "vibe_pm.doctor", reason: "설치/환경 상태를 점검하겠습니다." }
127
+ issues: [`${classified.issueCode}: ${msg}`],
128
+ next_action: classified.nextAction
126
129
  });
127
130
  }
128
131
  })();
@@ -1,6 +1,7 @@
1
1
  // adapters/mcp-ts/src/tools/vibe_pm/pm_language.ts
2
2
  // PM Language transformation utilities
3
3
  // Reference: docs/DEV_SPEC/MCP_TOOL_SCHEMA_SPEC.md Section: Status Mapping
4
+ import { applyTermDictionary, resolveLocaleFromText } from "./waiter_mapping.js";
4
5
  // ============================================================
5
6
  // Signal Level Mapping (Internal → User-facing)
6
7
  // ============================================================
@@ -202,21 +203,21 @@ ${verifyList}
202
203
  * This is a simplified version - full implementation would parse and restructure
203
204
  */
204
205
  export function transformAskQueueText(rawText) {
205
- let transformed = rawText;
206
- // Replace common technical terms with PM-friendly alternatives
207
- const replacements = [
208
- [/run_id/gi, "프로젝트"],
209
- [/bridge/gi, "작업 지시서"],
210
- [/schema/gi, "구조"],
211
- [/gate/gi, "검토"],
212
- [/signal/gi, "상태"],
213
- [/FSM/gi, "워크플로우"],
214
- [/spec_drift/gi, "범위 변경"],
215
- [/decision_card/gi, "결재 안건"],
216
- [/clinic_bridge/gi, "검수 기준"]
217
- ];
218
- for (const [pattern, replacement] of replacements) {
219
- transformed = transformed.replace(pattern, replacement);
206
+ try {
207
+ const locale = resolveLocaleFromText(rawText);
208
+ return applyTermDictionary(rawText, locale);
209
+ }
210
+ catch {
211
+ // Best-effort fallback: preserve previous behavior when mapping is unavailable.
212
+ return rawText
213
+ .replace(/run_id/gi, "프로젝트")
214
+ .replace(/bridge/gi, "작업 지시서")
215
+ .replace(/schema/gi, "구조")
216
+ .replace(/gate/gi, "검토")
217
+ .replace(/signal/gi, "상태")
218
+ .replace(/FSM/gi, "워크플로우")
219
+ .replace(/spec_drift/gi, "범위 변경")
220
+ .replace(/decision_card/gi, "결재 안건")
221
+ .replace(/clinic_bridge/gi, "검수 기준");
220
222
  }
221
- return transformed;
222
223
  }