@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,185 @@
1
+ // adapters/mcp-ts/src/tools/vibe_pm/spec_rag.ts
2
+ // Spec RAG: Auto-inject SSOT context into work orders
3
+ // Reference: docs/DEV_SPEC/0124/SPEC_RAG_WORK_PROTOCOL
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+ import { runKceRetrieve, runKceStatus } from "./kce/preflight.js";
7
+ /**
8
+ * Priority-ordered list of SSOT documents to inject
9
+ */
10
+ const SPEC_DOCS = [
11
+ "docs/ssot/DECISION_SSOT.md",
12
+ "docs/ssot/QUALITY_SSOT.md",
13
+ "docs/ssot/MCP_SSOT.md",
14
+ "docs/SSOT.md"
15
+ ];
16
+ /**
17
+ * Get spec context for injection into work orders.
18
+ *
19
+ * Strategy:
20
+ * 1. Try KCE retrieval (if status=READY)
21
+ * 2. Fallback to direct file reading
22
+ * 3. Return empty if both fail
23
+ */
24
+ export async function getSpecContext(opts) {
25
+ const maxChars = opts.maxChars ?? 2500;
26
+ const maxItems = opts.maxItems ?? 4;
27
+ const basePath = opts.basePath ?? process.cwd();
28
+ // Strategy 1: Try KCE retrieval
29
+ try {
30
+ const status = await runKceStatus();
31
+ if (status.readiness_state === "READY") {
32
+ const result = await runKceRetrieve({
33
+ query: opts.query,
34
+ maxItems,
35
+ limit: 10,
36
+ maxTotalChars: maxChars,
37
+ maxChunkChars: Math.min(800, Math.floor(maxChars / maxItems)),
38
+ pathPrefix: "docs/ssot"
39
+ });
40
+ if (result.context_block.trim()) {
41
+ const referencedDocs = (result.hits ?? [])
42
+ .map((h) => h.path)
43
+ .filter((p) => p.endsWith(".md"));
44
+ return {
45
+ context_block: buildSpecBlock(result.context_block, referencedDocs),
46
+ source: "kce",
47
+ referenced_docs: referencedDocs
48
+ };
49
+ }
50
+ }
51
+ }
52
+ catch {
53
+ // KCE unavailable, fall through to fallback
54
+ }
55
+ // Strategy 2: Fallback to direct file reading
56
+ try {
57
+ const { content, docs } = readSpecDocsFallback(basePath, maxChars);
58
+ if (content.trim()) {
59
+ return {
60
+ context_block: buildSpecBlock(content, docs),
61
+ source: "fallback",
62
+ referenced_docs: docs
63
+ };
64
+ }
65
+ }
66
+ catch {
67
+ // Fallback also failed
68
+ }
69
+ // Strategy 3: Return empty
70
+ return {
71
+ context_block: "",
72
+ source: "empty",
73
+ referenced_docs: []
74
+ };
75
+ }
76
+ /**
77
+ * Build the [SPEC_CONTEXT] block for injection
78
+ */
79
+ export function buildSpecBlock(content, docs) {
80
+ if (!content.trim())
81
+ return "";
82
+ const lines = [];
83
+ lines.push("[SPEC_CONTEXT]");
84
+ lines.push(content.trim());
85
+ if (docs.length > 0) {
86
+ lines.push("");
87
+ lines.push("[참조 문서]");
88
+ for (const doc of docs.slice(0, 6)) {
89
+ const basename = path.basename(doc);
90
+ lines.push(`- ${basename}`);
91
+ }
92
+ }
93
+ return lines.join("\n");
94
+ }
95
+ /**
96
+ * Fallback: Read spec docs directly from filesystem
97
+ */
98
+ function readSpecDocsFallback(basePath, maxChars) {
99
+ const chunks = [];
100
+ const docs = [];
101
+ let totalChars = 0;
102
+ const perDocLimit = Math.floor(maxChars / SPEC_DOCS.length);
103
+ for (const docPath of SPEC_DOCS) {
104
+ if (totalChars >= maxChars)
105
+ break;
106
+ const fullPath = path.join(basePath, docPath);
107
+ if (!fs.existsSync(fullPath))
108
+ continue;
109
+ try {
110
+ const content = fs.readFileSync(fullPath, "utf-8");
111
+ const extracted = extractRelevantSections(content, perDocLimit);
112
+ if (extracted.trim()) {
113
+ const basename = path.basename(docPath);
114
+ chunks.push(`### ${basename}\n${extracted.trim()}`);
115
+ docs.push(docPath);
116
+ totalChars += extracted.length;
117
+ }
118
+ }
119
+ catch {
120
+ // Skip unreadable files
121
+ }
122
+ }
123
+ return {
124
+ content: chunks.join("\n\n"),
125
+ docs
126
+ };
127
+ }
128
+ /**
129
+ * Extract relevant sections from a markdown document
130
+ * Focus on rules, constraints, and key definitions
131
+ */
132
+ function extractRelevantSections(content, maxChars) {
133
+ // Priority headers to extract
134
+ const priorityPatterns = [
135
+ /^#+\s*(?:규칙|Rules|제약|Constraints|필수|Required|금지|Prohibited)/im,
136
+ /^#+\s*(?:개요|Overview|핵심|Core|원칙|Principles)/im,
137
+ /^#+\s*(?:정의|Definitions|용어|Terms)/im
138
+ ];
139
+ const lines = content.split("\n");
140
+ const sections = [];
141
+ let currentSection = [];
142
+ let inPrioritySection = false;
143
+ let sectionHeader = "";
144
+ for (const line of lines) {
145
+ // Check if this is a header
146
+ const isHeader = /^#+\s+/.test(line);
147
+ if (isHeader) {
148
+ // Save previous section if it was a priority section
149
+ if (inPrioritySection && currentSection.length > 0) {
150
+ sections.push([sectionHeader, ...currentSection].join("\n"));
151
+ }
152
+ // Check if this header is a priority section
153
+ inPrioritySection = priorityPatterns.some((p) => p.test(line));
154
+ sectionHeader = line;
155
+ currentSection = [];
156
+ }
157
+ else if (inPrioritySection) {
158
+ currentSection.push(line);
159
+ }
160
+ }
161
+ // Don't forget the last section
162
+ if (inPrioritySection && currentSection.length > 0) {
163
+ sections.push([sectionHeader, ...currentSection].join("\n"));
164
+ }
165
+ // If no priority sections found, extract the first N characters as overview
166
+ if (sections.length === 0) {
167
+ const overview = content.slice(0, Math.min(maxChars, 500)).trim();
168
+ if (overview) {
169
+ // Try to end at a sentence boundary
170
+ const lastPeriod = overview.lastIndexOf(".");
171
+ const lastNewline = overview.lastIndexOf("\n");
172
+ const cutoff = Math.max(lastPeriod, lastNewline);
173
+ return cutoff > 100 ? overview.slice(0, cutoff + 1) : overview;
174
+ }
175
+ return "";
176
+ }
177
+ // Combine sections up to maxChars
178
+ let result = "";
179
+ for (const section of sections) {
180
+ if (result.length + section.length > maxChars)
181
+ break;
182
+ result += (result ? "\n\n" : "") + section;
183
+ }
184
+ return result;
185
+ }
@@ -1,11 +1,14 @@
1
1
  // adapters/mcp-ts/src/tools/vibe_pm/status.ts
2
2
  // vibe_pm.status - Project status query
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
3
5
  import { runEngineWithCache } from "../../engine.js";
4
6
  import { safeJsonParse } from "../../cli.js";
5
7
  import { resolveRunId, resolveProjectId, getRunContext, readRunState } from "./context.js";
6
8
  import { formatMode, formatPhase, signalToReview } from "./pm_language.js";
7
9
  import { validateToolInput } from "../../security/input-validator.js";
8
10
  import { SpecHighValidateOutputSchema } from "../../generated/spec_high_validate_output.js";
11
+ import { IngressSummaryFileSchema } from "../../generated/ingress_summary_file.js";
9
12
  /**
10
13
  * vibe_pm.status - Query current project status
11
14
  *
@@ -13,8 +16,7 @@ import { SpecHighValidateOutputSchema } from "../../generated/spec_high_validate
13
16
  * → spec-high validate <run_id>
14
17
  * → status summary generation
15
18
  */
16
- export async function status(input) {
17
- const basePath = process.cwd();
19
+ export async function status(input, basePath = process.cwd()) {
18
20
  validateToolInput({ project_id: input.project_id });
19
21
  // Resolve run_id
20
22
  const { run_id, is_new } = resolveRunId(input.project_id, basePath);
@@ -77,7 +79,17 @@ export async function status(input) {
77
79
  // Collect risks
78
80
  const risks = validateResult?.risks ?? [];
79
81
  // Determine next action based on state
80
- const nextAction = determineNextAction(phase, decisionsPending, lastReviewStatus);
82
+ let nextAction = determineNextAction(phase, decisionsPending, lastReviewStatus);
83
+ // P27: deterministic auto-routing for "pre-work clarification" (based on ingress signals)
84
+ const routed = maybeAutoRouteForClarification({
85
+ runDirAbs: context.runs_path,
86
+ phase,
87
+ decisionsPending
88
+ });
89
+ if (routed) {
90
+ risks.push(routed.risk_note);
91
+ nextAction = routed.next_action;
92
+ }
81
93
  return {
82
94
  project_id,
83
95
  project: {
@@ -95,6 +107,41 @@ export async function status(input) {
95
107
  next_action: nextAction
96
108
  };
97
109
  }
110
+ function maybeAutoRouteForClarification(args) {
111
+ // Never override when user still has pending approvals.
112
+ if (args.decisionsPending > 0)
113
+ return null;
114
+ // Only relevant before work order is issued.
115
+ if (!(args.phase === "decision" || args.phase === "briefing" || args.phase === "init"))
116
+ return null;
117
+ const summaryAbs = path.join(args.runDirAbs, "ingress", "ingress_summary.json");
118
+ if (!fs.existsSync(summaryAbs))
119
+ return null;
120
+ let raw;
121
+ try {
122
+ raw = JSON.parse(fs.readFileSync(summaryAbs, "utf-8"));
123
+ }
124
+ catch {
125
+ return null;
126
+ }
127
+ const parsed = IngressSummaryFileSchema.safeParse(raw);
128
+ if (!parsed.success)
129
+ return null;
130
+ const recommended = parsed.data.high_level.clarification_recommended === true;
131
+ if (!recommended)
132
+ return null;
133
+ // Treat as completed if seed exists (protocol done).
134
+ const seedAbs = path.join(args.runDirAbs, "bridge", "work_order_seed.json");
135
+ if (fs.existsSync(seedAbs))
136
+ return null;
137
+ return {
138
+ next_action: {
139
+ tool: "vibe_pm.bridge_generate_references",
140
+ label: "다음 할 일: 예시를 고르고 작업 방향을 고정하기"
141
+ },
142
+ risk_note: "작업 방향이 아직 충분히 고정되지 않았습니다. 예시를 한 번 선택하면 다음 단계가 빨라집니다."
143
+ };
144
+ }
98
145
  function determineNextAction(phase, decisionsPending, lastReviewStatus) {
99
146
  // If there are pending decisions
100
147
  if (decisionsPending > 0) {
@@ -20,8 +20,8 @@ export async function submitDecision(input) {
20
20
  const project_id = resolveProjectId(run_id, basePath);
21
21
  // Format answer text (combine choice with note if provided)
22
22
  const answerText = input.note ? `${input.choice}: ${input.note}` : input.choice;
23
- // Step 1: Submit answer
24
- const answerResult = await runEngine("vibecoding-helper", ["answer", run_id, "--text", answerText], { timeoutMs: 120_000 });
23
+ // Step 1: Submit answer via spec-high
24
+ const answerResult = await runEngine("spec-high", ["--root", "engines/spec_high", "answer", run_id, "--text", answerText], { timeoutMs: 120_000 });
25
25
  if (answerResult.code !== 0) {
26
26
  throw new Error(`결재 제출 실패: ${answerResult.stderr || "알 수 없는 오류"}`);
27
27
  }
@@ -183,6 +183,14 @@ import { SearchOssInputSchema } from "../../generated/search_oss_input.js";
183
183
  import { SearchOssOutputSchema } from "../../generated/search_oss_output.js";
184
184
  import { ZoektEvidenceInputSchema } from "../../generated/zoekt_evidence_input.js";
185
185
  import { ZoektEvidenceOutputSchema } from "../../generated/zoekt_evidence_output.js";
186
+ import { IngressInputSchema } from "../../generated/ingress_input.js";
187
+ import { IngressOutputSchema } from "../../generated/ingress_output.js";
188
+ import { BridgeGenerateReferencesInputSchema } from "../../generated/bridge_generate_references_input.js";
189
+ import { BridgeGenerateReferencesOutputSchema } from "../../generated/bridge_generate_references_output.js";
190
+ import { BridgeConfirmReferenceInputSchema } from "../../generated/bridge_confirm_reference_input.js";
191
+ import { BridgeConfirmReferenceOutputSchema } from "../../generated/bridge_confirm_reference_output.js";
192
+ import { BridgeBuildSeedInputSchema } from "../../generated/bridge_build_seed_input.js";
193
+ import { BridgeBuildSeedOutputSchema } from "../../generated/bridge_build_seed_output.js";
186
194
  export const finalizeWorkInputSchema = FinalizeWorkInputSchema;
187
195
  // Output schema is generated from JSON Schema SSOT.
188
196
  export const finalizeWorkOutputSchema = FinalizeWorkOutputSchema;
@@ -222,6 +230,26 @@ export const zoektEvidenceInputSchema = ZoektEvidenceInputSchema;
222
230
  // Output schema is generated from JSON Schema SSOT.
223
231
  export const zoektEvidenceOutputSchema = ZoektEvidenceOutputSchema;
224
232
  // ============================================================
233
+ // vibe_pm.ingress
234
+ // ============================================================
235
+ export const ingressInputSchema = IngressInputSchema;
236
+ export const ingressOutputSchema = IngressOutputSchema;
237
+ // ============================================================
238
+ // vibe_pm.bridge_generate_references
239
+ // ============================================================
240
+ export const bridgeGenerateReferencesInputSchema = BridgeGenerateReferencesInputSchema;
241
+ export const bridgeGenerateReferencesOutputSchema = BridgeGenerateReferencesOutputSchema;
242
+ // ============================================================
243
+ // vibe_pm.bridge_confirm_reference
244
+ // ============================================================
245
+ export const bridgeConfirmReferenceInputSchema = BridgeConfirmReferenceInputSchema;
246
+ export const bridgeConfirmReferenceOutputSchema = BridgeConfirmReferenceOutputSchema;
247
+ // ============================================================
248
+ // vibe_pm.bridge_build_seed
249
+ // ============================================================
250
+ export const bridgeBuildSeedInputSchema = BridgeBuildSeedInputSchema;
251
+ export const bridgeBuildSeedOutputSchema = BridgeBuildSeedOutputSchema;
252
+ // ============================================================
225
253
  // vibe_pm.doctor / vibe_pm.update (Output SSOT only)
226
254
  // ============================================================
227
255
  export const doctorOutputSchema = DoctorOutputSchema;
@@ -5,6 +5,7 @@
5
5
  import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
7
  import { invokeGit } from "../../runtime/cli_invoker.js";
8
+ import { isGitConflictError } from "../../errors.js";
8
9
  /**
9
10
  * vibe_pm.undo_last_task - Rollback recent commits via git revert
10
11
  *
@@ -55,8 +56,14 @@ export async function undoLastTask(input) {
55
56
  const errMsg = result.stderr || result.stdout;
56
57
  revertErrors.push(`${commit.shortHash}: ${errMsg}`);
57
58
  // If revert fails due to conflicts, abort and stop
58
- if (errMsg.includes("conflict") || errMsg.includes("CONFLICT")) {
59
- await invokeGit(["revert", "--abort"], basePath);
59
+ // Check for conflict indicators (exit code 1 with conflict message)
60
+ // P1-4: Use centralized error pattern
61
+ if (isGitConflictError(errMsg) || result.exitCode === 1) {
62
+ const abortResult = await invokeGit(["revert", "--abort"], basePath);
63
+ if (abortResult.exitCode !== 0) {
64
+ // Abort failed - critical state inconsistency
65
+ revertErrors.push(`[CRITICAL] git revert --abort 실패. 수동 복구 필요: git status 확인 후 git revert --abort 또는 git revert --continue 실행`);
66
+ }
60
67
  break;
61
68
  }
62
69
  }
@@ -0,0 +1,155 @@
1
+ // adapters/mcp-ts/src/tools/vibe_pm/waiter_mapping.ts
2
+ // Waiter(Front) rendering: message_template_id mapping + term dictionary (data-driven)
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { MessageTemplateIdMappingFileSchema } from "../../generated/message_template_id_mapping_file.js";
7
+ let cache = null;
8
+ function packageRoot() {
9
+ const here = path.dirname(fileURLToPath(import.meta.url));
10
+ // src/tools/vibe_pm → package root
11
+ return path.resolve(here, "..", "..", "..");
12
+ }
13
+ function repoRoot(pkgRoot) {
14
+ // adapters/mcp-ts → repo root
15
+ return path.resolve(pkgRoot, "..", "..");
16
+ }
17
+ function existsFile(p) {
18
+ try {
19
+ return fs.existsSync(p) && fs.statSync(p).isFile();
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ function readJsonFile(p) {
26
+ const raw = fs.readFileSync(p, "utf-8");
27
+ return JSON.parse(raw);
28
+ }
29
+ function safeLocaleList(mapping) {
30
+ const locales = mapping?.supported_locales;
31
+ return Array.isArray(locales) ? locales.map((x) => String(x).trim()).filter(Boolean) : [];
32
+ }
33
+ function safeDefaultLocale(mapping) {
34
+ const d = mapping?.default_locale;
35
+ return typeof d === "string" && d.trim() ? d.trim() : "ko";
36
+ }
37
+ function safeFallbackLocale(mapping) {
38
+ const f = mapping?.human_language_policy?.fallback_locale;
39
+ const s = typeof f === "string" && f.trim() ? f.trim() : "";
40
+ return s || safeDefaultLocale(mapping);
41
+ }
42
+ export function resetWaiterMappingCacheForTest() {
43
+ cache = null;
44
+ }
45
+ export function getWaiterMapping() {
46
+ const pkgRoot = packageRoot();
47
+ const candidates = [
48
+ path.join(pkgRoot, "assets", "message_template_id_mapping.v1.json"),
49
+ path.join(repoRoot(pkgRoot), "schemas", "message_template_id_mapping.v1.json")
50
+ ];
51
+ for (const p of candidates) {
52
+ if (!existsFile(p))
53
+ continue;
54
+ const mtimeMs = fs.statSync(p).mtimeMs;
55
+ if (cache && cache.mappingPath === p && cache.mtimeMs === mtimeMs) {
56
+ return cache.mapping;
57
+ }
58
+ const raw = readJsonFile(p);
59
+ const parsed = MessageTemplateIdMappingFileSchema.parse(raw);
60
+ cache = { mappingPath: p, mtimeMs, mapping: parsed };
61
+ return parsed;
62
+ }
63
+ // Fail-closed is too harsh for user-facing rendering; fall back to a minimal internal default.
64
+ const fallback = MessageTemplateIdMappingFileSchema.parse({
65
+ version: "message_template_id_mapping.v1",
66
+ default_locale: "ko",
67
+ supported_locales: ["ko", "en"],
68
+ templates: {
69
+ "GO-01": { category: "GO", verdict: { ko: "진행 가능", en: "OK to proceed" }, next_action_prefix: { ko: "다음 단계:", en: "Next:" } },
70
+ "GO-02": { category: "GO", verdict: { ko: "진행 가능", en: "OK to proceed" }, next_action_prefix: { ko: "다음 단계:", en: "Next:" } },
71
+ "FIX-01": { category: "FIX", verdict: { ko: "보완 필요", en: "Needs changes" }, next_action_prefix: { ko: "다음 단계:", en: "Next:" } },
72
+ "FIX-02": { category: "FIX", verdict: { ko: "보완 필요", en: "Needs changes" }, next_action_prefix: { ko: "다음 단계:", en: "Next:" } },
73
+ "BLOCK-01": { category: "BLOCK", verdict: { ko: "반려", en: "Blocked" }, next_action_prefix: { ko: "다음 단계:", en: "Next:" } },
74
+ "BLOCK-02": { category: "BLOCK", verdict: { ko: "반려", en: "Blocked" }, next_action_prefix: { ko: "다음 단계:", en: "Next:" } },
75
+ "TRIAGE-SKIP-01": { category: "TRIAGE", verdict: { ko: "사전 점검 생략", en: "Pre-check skipped" }, next_action_prefix: { ko: "다음 단계:", en: "Next:" } },
76
+ "TRIAGE-RUN-01": { category: "TRIAGE", verdict: { ko: "사전 점검 실행", en: "Pre-check running" }, next_action_prefix: { ko: "다음 단계:", en: "Next:" } },
77
+ "TRIAGE-RUN-02": { category: "TRIAGE", verdict: { ko: "사전 점검 실행", en: "Pre-check running" }, next_action_prefix: { ko: "다음 단계:", en: "Next:" } },
78
+ "ADV-01": { category: "ADVICE", verdict: { ko: "참고", en: "Advice" }, next_action_prefix: { ko: "다음 단계:", en: "Next:" } },
79
+ "ADV-02": { category: "ADVICE", verdict: { ko: "참고", en: "Advice" }, next_action_prefix: { ko: "다음 단계:", en: "Next:" } },
80
+ "WARN-01": { category: "WARN", verdict: { ko: "주의", en: "Warning" }, next_action_prefix: { ko: "다음 단계:", en: "Next:" } },
81
+ "WARN-02": { category: "WARN", verdict: { ko: "주의", en: "Warning" }, next_action_prefix: { ko: "다음 단계:", en: "Next:" } },
82
+ "SOFTBLOCK-01": { category: "SOFTBLOCK", verdict: { ko: "잠깐 멈춤", en: "Pause" }, next_action_prefix: { ko: "다음 단계:", en: "Next:" } }
83
+ },
84
+ term_dictionary: { entries: [{ id: "ENGINE_RUN_ID", aliases: ["run_id"], replacement: { ko: "프로젝트", en: "project" }, category: "engine_term" }] }
85
+ });
86
+ cache = { mappingPath: "<fallback>", mtimeMs: 0, mapping: fallback };
87
+ return fallback;
88
+ }
89
+ export function resolveLocaleFromText(text, mapping = getWaiterMapping()) {
90
+ const env = (process.env.VIBECODE_LOCALE ?? "").trim();
91
+ const supported = safeLocaleList(mapping);
92
+ if (env && supported.includes(env))
93
+ return env;
94
+ const hasKo = /[가-힣]/.test(text);
95
+ if (hasKo && supported.includes("ko"))
96
+ return "ko";
97
+ if (!hasKo && supported.includes("en"))
98
+ return "en";
99
+ return safeFallbackLocale(mapping);
100
+ }
101
+ export function resolveLocaleFromData(data, mapping = getWaiterMapping()) {
102
+ const env = (process.env.VIBECODE_LOCALE ?? "").trim();
103
+ const supported = safeLocaleList(mapping);
104
+ if (env && supported.includes(env))
105
+ return env;
106
+ if (data && typeof data === "object") {
107
+ const obj = data;
108
+ const oneLine = obj.review_summary?.one_line_verdict;
109
+ const msg = typeof oneLine === "string" ? oneLine : typeof obj.message === "string" ? obj.message : "";
110
+ if (msg)
111
+ return resolveLocaleFromText(msg, mapping);
112
+ }
113
+ return safeFallbackLocale(mapping);
114
+ }
115
+ export function renderVerdictFromTemplateId(templateId, locale, mapping = getWaiterMapping()) {
116
+ const templates = mapping?.templates ?? {};
117
+ const t = templates[templateId];
118
+ if (!t || typeof t !== "object")
119
+ return null;
120
+ const verdict = t?.verdict;
121
+ if (!verdict || typeof verdict !== "object")
122
+ return null;
123
+ const v = verdict[locale];
124
+ if (typeof v === "string" && v.trim())
125
+ return v.trim();
126
+ const fallback = safeFallbackLocale(mapping);
127
+ const vf = verdict[fallback];
128
+ if (typeof vf === "string" && vf.trim())
129
+ return vf.trim();
130
+ return null;
131
+ }
132
+ function escapeRegex(s) {
133
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
134
+ }
135
+ export function applyTermDictionary(text, locale, mapping = getWaiterMapping()) {
136
+ let out = text;
137
+ const entries = mapping?.term_dictionary?.entries;
138
+ if (!Array.isArray(entries) || entries.length === 0)
139
+ return out;
140
+ for (const e of entries) {
141
+ const obj = e;
142
+ const aliases = Array.isArray(obj?.aliases) ? obj.aliases.map((a) => String(a)).filter(Boolean) : [];
143
+ const rep = obj?.replacement;
144
+ const replacement = typeof rep === "object" && rep !== null ? String(rep[locale] ?? "").trim() : "";
145
+ if (!replacement || aliases.length === 0)
146
+ continue;
147
+ for (const a of aliases) {
148
+ const alias = String(a).trim();
149
+ if (!alias)
150
+ continue;
151
+ out = out.replace(new RegExp(escapeRegex(alias), "gi"), replacement);
152
+ }
153
+ }
154
+ return out;
155
+ }
@@ -1,10 +1,11 @@
1
1
  // adapters/mcp-ts/src/tools/vibe_pm/zoekt_evidence.ts
2
2
  // vibe_pm.zoekt_evidence - Collect pattern evidence across local roots (zoekt/rg/python_scan)
3
3
  import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
4
- import { runEngine } from "../../engine.js";
4
+ import { runPythonCli } from "../../engine.js";
5
5
  import { safeJsonParse } from "../../cli.js";
6
6
  import { validateToolInput } from "../../security/input-validator.js";
7
7
  import { resolveProjectId, resolveRunId } from "./context.js";
8
+ import { classifyPythonError } from "./python_error.js";
8
9
  function toCsv(values) {
9
10
  return values
10
11
  .map((v) => (typeof v === "string" ? v.trim() : ""))
@@ -43,9 +44,10 @@ export async function zoektEvidence(input) {
43
44
  const writeEvidence = input?.write_evidence !== false;
44
45
  if (!writeEvidence)
45
46
  cmd.push("--no-write-evidence");
46
- const res = await runEngine("vibecoding-helper", cmd, { timeoutMs: 300_000 });
47
+ const res = await runPythonCli(cmd, { timeoutMs: 300_000 });
47
48
  if (res.code !== 0) {
48
- throw new Error(`zoekt-evidence failed: ${res.stderr || res.stdout || `exit_code=${res.code}`}`);
49
+ const classified = classifyPythonError(res.stderr ?? "", res.code, "패턴 증거 수집");
50
+ throw new Error(`${classified.issueCode}: ${classified.summary}`);
49
51
  }
50
52
  const parsed = safeJsonParse(res.stdout);
51
53
  if (!parsed.ok) {
package/build/tools.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import { z } from "zod";
4
4
  import { safeJsonParse } from "./cli.js";
5
5
  import { parseSelectionValidationResult } from "./contracts.js";
6
- import { runEngine } from "./engine.js";
6
+ import { runEngine, runPythonCli } from "./engine.js";
7
7
  import { doctor as doctorImpl } from "./bootstrap/doctor.js";
8
8
  import { ToolErrorOutputSchema } from "./generated/tool_error_output.js";
9
9
  function toolText(obj) {
@@ -45,7 +45,9 @@ export function defineTools() {
45
45
  });
46
46
  async function vibecodeOneLoop(input) {
47
47
  try {
48
- const { code, stdout, stderr } = await runEngine("vibecoding-helper", ["one-loop", input.run_id, "--output", input.output], { timeoutMs: input.timeout_ms ?? 120_000 });
48
+ // [DEPRECATED] Use vibe_pm.inspect_code instead
49
+ // Changed from runEngine("vibecoding-helper") to runPythonCli for correct routing
50
+ const { code, stdout, stderr } = await runPythonCli(["one-loop", input.run_id, "--output", input.output], { timeoutMs: input.timeout_ms ?? 120_000 });
49
51
  const parsed = safeJsonParse(stdout);
50
52
  if (!parsed.ok) {
51
53
  return err("invalid_json", {
@@ -148,7 +150,9 @@ export function defineTools() {
148
150
  });
149
151
  async function vibecodeShowAskQueue(input) {
150
152
  try {
151
- const { code, stdout, stderr } = await runEngine("vibecoding-helper", ["show-ask-queue", input.run_id], { timeoutMs: input.timeout_ms ?? 60_000 });
153
+ // [DEPRECATED] Use vibe_pm.get_decision instead
154
+ // Changed from runEngine("vibecoding-helper") to runPythonCli for correct routing
155
+ const { code, stdout, stderr } = await runPythonCli(["show-ask-queue", input.run_id], { timeoutMs: input.timeout_ms ?? 60_000 });
152
156
  return toolText({
153
157
  status: code === 0 ? "OK" : "FAIL",
154
158
  exit_code: code,
@@ -168,7 +172,9 @@ export function defineTools() {
168
172
  });
169
173
  async function vibecodeShowDecisions(input) {
170
174
  try {
171
- const { code, stdout, stderr } = await runEngine("vibecoding-helper", ["show-decisions", input.run_id], { timeoutMs: input.timeout_ms ?? 60_000 });
175
+ // [DEPRECATED] Use vibe_pm.status instead
176
+ // Changed from runEngine("vibecoding-helper") to runPythonCli for correct routing
177
+ const { code, stdout, stderr } = await runPythonCli(["show-decisions", input.run_id], { timeoutMs: input.timeout_ms ?? 60_000 });
172
178
  return toolText({
173
179
  status: code === 0 ? "OK" : "FAIL",
174
180
  exit_code: code,
@@ -189,7 +195,9 @@ export function defineTools() {
189
195
  });
190
196
  async function vibecodeAnswer(input) {
191
197
  try {
192
- const { code, stdout, stderr } = await runEngine("vibecoding-helper", ["answer", input.run_id, "--text", input.text], { timeoutMs: input.timeout_ms ?? 120_000 });
198
+ // [DEPRECATED] Use vibe_pm.submit_decision instead
199
+ // Changed from runEngine("vibecoding-helper") to runPythonCli for correct routing
200
+ const { code, stdout, stderr } = await runPythonCli(["answer", input.run_id, "--text", input.text], { timeoutMs: input.timeout_ms ?? 120_000 });
193
201
  return toolText({
194
202
  status: code === 0 ? "OK" : "FAIL",
195
203
  exit_code: code,