@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.
- package/README.md +10 -10
- package/build/auth/index.js +0 -2
- package/build/auth/public_key.js +6 -4
- package/build/bootstrap/doctor.js +113 -5
- package/build/bootstrap/installer.js +85 -15
- package/build/bootstrap/registry.js +11 -6
- package/build/bootstrap/skills-installer.js +365 -0
- package/build/dx/activity.js +26 -3
- package/build/engine.js +151 -0
- package/build/errors.js +107 -0
- package/build/generated/bridge_build_seed_input.js +2 -0
- package/build/generated/bridge_build_seed_output.js +2 -0
- package/build/generated/bridge_confirm_reference_input.js +2 -0
- package/build/generated/bridge_confirm_reference_output.js +2 -0
- package/build/generated/bridge_confirmed_reference_file.js +2 -0
- package/build/generated/bridge_generate_references_input.js +2 -0
- package/build/generated/bridge_generate_references_output.js +2 -0
- package/build/generated/bridge_references_file.js +2 -0
- package/build/generated/bridge_work_order_seed_file.js +2 -0
- package/build/generated/contracts_bundle_info.js +3 -3
- package/build/generated/index.js +14 -0
- package/build/generated/ingress_input.js +2 -0
- package/build/generated/ingress_output.js +2 -0
- package/build/generated/ingress_resolution_file.js +2 -0
- package/build/generated/ingress_summary_file.js +2 -0
- package/build/generated/message_template_id_mapping_file.js +2 -0
- package/build/generated/run_app_input.js +1 -1
- package/build/index.js +4 -3
- package/build/local-mode/paths.js +1 -0
- package/build/local-mode/setup.js +21 -1
- package/build/path-utils.js +68 -0
- package/build/runtime/cli_invoker.js +1 -1
- package/build/tools/vibe_pm/advisory_review.js +5 -3
- package/build/tools/vibe_pm/bridge_build_seed.js +164 -0
- package/build/tools/vibe_pm/bridge_confirm_reference.js +91 -0
- package/build/tools/vibe_pm/bridge_generate_references.js +258 -0
- package/build/tools/vibe_pm/briefing.js +27 -3
- package/build/tools/vibe_pm/context.js +79 -0
- package/build/tools/vibe_pm/create_work_order.js +200 -3
- package/build/tools/vibe_pm/doctor.js +95 -0
- package/build/tools/vibe_pm/entity_gate/preflight.js +8 -3
- package/build/tools/vibe_pm/export_output.js +14 -13
- package/build/tools/vibe_pm/finalize_work.js +78 -40
- package/build/tools/vibe_pm/get_decision.js +2 -2
- package/build/tools/vibe_pm/index.js +128 -42
- package/build/tools/vibe_pm/ingress.js +645 -0
- package/build/tools/vibe_pm/ingress_gate.js +116 -0
- package/build/tools/vibe_pm/inspect_code.js +90 -20
- package/build/tools/vibe_pm/kce/doc_usage.js +4 -9
- package/build/tools/vibe_pm/kce/on_finalize.js +2 -2
- package/build/tools/vibe_pm/kce/preflight.js +11 -7
- package/build/tools/vibe_pm/memory_status.js +11 -8
- package/build/tools/vibe_pm/memory_sync.js +11 -8
- package/build/tools/vibe_pm/pm_language.js +17 -16
- package/build/tools/vibe_pm/python_error.js +115 -0
- package/build/tools/vibe_pm/run_app.js +169 -43
- package/build/tools/vibe_pm/run_app_podman.js +64 -2
- package/build/tools/vibe_pm/search_oss.js +5 -3
- package/build/tools/vibe_pm/spec_rag.js +185 -0
- package/build/tools/vibe_pm/status.js +50 -3
- package/build/tools/vibe_pm/submit_decision.js +2 -2
- package/build/tools/vibe_pm/types.js +28 -0
- package/build/tools/vibe_pm/undo_last_task.js +9 -2
- package/build/tools/vibe_pm/waiter_mapping.js +155 -0
- package/build/tools/vibe_pm/zoekt_evidence.js +5 -3
- package/build/tools.js +13 -5
- package/build/vibe-cli.js +245 -7
- package/package.json +5 -4
- package/skills/VRIP_INSTALL_MANIFEST_DOCTOR.skill.md +288 -0
- 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
|
-
|
|
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("
|
|
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
|
-
|
|
59
|
-
|
|
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 {
|
|
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
|
|
47
|
+
const res = await runPythonCli(cmd, { timeoutMs: 300_000 });
|
|
47
48
|
if (res.code !== 0) {
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|