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