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