@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,258 @@
|
|
|
1
|
+
// adapters/mcp-ts/src/tools/vibe_pm/bridge_generate_references.ts
|
|
2
|
+
// vibe_pm.bridge_generate_references - Create runs/<run_id>/bridge/references.json (A/B/C)
|
|
3
|
+
import * as fsSync from "node:fs";
|
|
4
|
+
import * as fs from "node:fs/promises";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { decode as decodeMsgpack } from "@msgpack/msgpack";
|
|
7
|
+
import { validateToolInput } from "../../security/input-validator.js";
|
|
8
|
+
import { resolveProjectId, resolveRunId, getRunContext } from "./context.js";
|
|
9
|
+
import { BridgeReferencesFileSchema } from "../../generated/bridge_references_file.js";
|
|
10
|
+
import { searchOss } from "./search_oss.js";
|
|
11
|
+
function sanitizeStrings(values, maxItems) {
|
|
12
|
+
if (!Array.isArray(values))
|
|
13
|
+
return [];
|
|
14
|
+
return values
|
|
15
|
+
.map((v) => (typeof v === "string" ? v.trim() : ""))
|
|
16
|
+
.filter(Boolean)
|
|
17
|
+
.slice(0, maxItems);
|
|
18
|
+
}
|
|
19
|
+
function keyForIndex(i) {
|
|
20
|
+
return i === 0 ? "A" : i === 1 ? "B" : "C";
|
|
21
|
+
}
|
|
22
|
+
function toRelPath(basePath, absPath) {
|
|
23
|
+
const rel = path.relative(basePath, absPath);
|
|
24
|
+
return rel.replace(/\\\\/g, "/");
|
|
25
|
+
}
|
|
26
|
+
export async function bridgeGenerateReferences(input, basePath = process.cwd()) {
|
|
27
|
+
validateToolInput({ project_id: input.project_id, run_id: input.run_id });
|
|
28
|
+
const resolvedRun = resolveRunId(input.run_id ?? input.project_id, basePath);
|
|
29
|
+
const run_id = resolvedRun.run_id;
|
|
30
|
+
const project_id = resolveProjectId(run_id, basePath);
|
|
31
|
+
const context = getRunContext(run_id, basePath);
|
|
32
|
+
const rawUserIntent = String(input.raw_user_intent ?? "").trim();
|
|
33
|
+
let keywords = sanitizeStrings(input?.keywords, 12);
|
|
34
|
+
if (!rawUserIntent)
|
|
35
|
+
throw new Error("raw_user_intent is required");
|
|
36
|
+
if (keywords.length === 0) {
|
|
37
|
+
keywords = deriveKeywordsFromContextScan({ basePath, runDirAbs: context.runs_path });
|
|
38
|
+
if (keywords.length === 0) {
|
|
39
|
+
keywords = ["reference"];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const constraints = sanitizeStrings(input?.constraints, 12);
|
|
43
|
+
const mustHave = sanitizeStrings(input?.must_have, 12);
|
|
44
|
+
const mustNot = sanitizeStrings(input?.must_not, 12);
|
|
45
|
+
const maxCandidates = typeof input?.max_candidates === "number" ? Math.trunc(input.max_candidates) : 3;
|
|
46
|
+
const maxK = maxCandidates >= 2 && maxCandidates <= 3 ? maxCandidates : 3;
|
|
47
|
+
const warnings = [];
|
|
48
|
+
// Candidate sources:
|
|
49
|
+
// 1) candidates_seed (explicit) - deterministic + test-friendly
|
|
50
|
+
// 2) best-effort OSS search (vibe_pm.search_oss) - may fail offline
|
|
51
|
+
// 3) fallback patterns (always available)
|
|
52
|
+
const seedCandidates = Array.isArray(input?.candidates_seed) ? input.candidates_seed : [];
|
|
53
|
+
const candidates = [];
|
|
54
|
+
if (seedCandidates.length > 0) {
|
|
55
|
+
warnings.push("OSS_SEARCH_SKIPPED: candidates_seed provided");
|
|
56
|
+
for (const c of seedCandidates.slice(0, maxK)) {
|
|
57
|
+
const obj = c;
|
|
58
|
+
const title = typeof obj?.title === "string" ? obj.title.trim() : "";
|
|
59
|
+
const kind = typeof obj?.kind === "string" ? obj.kind : "doc";
|
|
60
|
+
const primary_link = typeof obj?.primary_link === "string" ? obj.primary_link.trim() : "";
|
|
61
|
+
if (!title || !primary_link)
|
|
62
|
+
continue;
|
|
63
|
+
candidates.push({
|
|
64
|
+
title,
|
|
65
|
+
kind,
|
|
66
|
+
why_similar: typeof obj?.why_similar === "string" ? obj.why_similar : "유사 레퍼런스",
|
|
67
|
+
fit_summary: typeof obj?.fit_summary === "string" ? obj.fit_summary : "",
|
|
68
|
+
confidence: 4,
|
|
69
|
+
links: { primary: primary_link, secondary: [] },
|
|
70
|
+
evidence: { source: "manual", highlights: [] }
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
try {
|
|
76
|
+
const oss = await searchOss({
|
|
77
|
+
project_id,
|
|
78
|
+
run_id,
|
|
79
|
+
intent: rawUserIntent.slice(0, 200),
|
|
80
|
+
keywords,
|
|
81
|
+
write_evidence: true
|
|
82
|
+
});
|
|
83
|
+
for (const c of (oss.candidates ?? []).slice(0, maxK)) {
|
|
84
|
+
const repo = typeof c?.repo === "string" ? c.repo.trim() : "";
|
|
85
|
+
const url = typeof c?.url === "string" ? c.url.trim() : "";
|
|
86
|
+
const desc = typeof c?.description === "string" ? c.description : "";
|
|
87
|
+
if (!repo || !url)
|
|
88
|
+
continue;
|
|
89
|
+
candidates.push({
|
|
90
|
+
title: repo,
|
|
91
|
+
kind: "oss_repo",
|
|
92
|
+
why_similar: desc || "유사 기능을 가진 OSS 레퍼런스",
|
|
93
|
+
fit_summary: desc || "",
|
|
94
|
+
confidence: 3,
|
|
95
|
+
links: { primary: url, secondary: [] },
|
|
96
|
+
evidence: { source: "github", highlights: [] }
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
102
|
+
warnings.push(`OSS_SEARCH_FAILED: ${msg}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Ensure at least 2 candidates by adding deterministic fallbacks.
|
|
106
|
+
const fallback = [
|
|
107
|
+
{
|
|
108
|
+
title: "Minimal Bridge Protocol",
|
|
109
|
+
kind: "doc",
|
|
110
|
+
why_similar: "A/B/C 선택 → 확인 기록 → seed 생성의 최소 프로토콜",
|
|
111
|
+
fit_summary: "도입 난이도 낮음",
|
|
112
|
+
confidence: 3,
|
|
113
|
+
links: { primary: "https://example.com/bridge-protocol", secondary: [] },
|
|
114
|
+
evidence: { source: "manual", highlights: [] }
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
title: "Local Evidence Pattern (Zoekt/Ripgrep)",
|
|
118
|
+
kind: "pattern",
|
|
119
|
+
why_similar: "로컬 코드베이스 근거를 모아 구현 방향을 고정",
|
|
120
|
+
fit_summary: "증거 기반 정렬",
|
|
121
|
+
confidence: 3,
|
|
122
|
+
links: { primary: "https://example.com/zoekt", secondary: [] },
|
|
123
|
+
evidence: { source: "manual", highlights: [] }
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
title: "Template Pin Pattern",
|
|
127
|
+
kind: "pattern",
|
|
128
|
+
why_similar: "레퍼런스를 핀으로 고정하고 작업 범위를 줄이는 방식",
|
|
129
|
+
fit_summary: "합의/반복 비용 감소",
|
|
130
|
+
confidence: 3,
|
|
131
|
+
links: { primary: "https://example.com/pattern-template-pin", secondary: [] },
|
|
132
|
+
evidence: { source: "manual", highlights: [] }
|
|
133
|
+
}
|
|
134
|
+
];
|
|
135
|
+
for (const f of fallback) {
|
|
136
|
+
if (candidates.length >= 2)
|
|
137
|
+
break;
|
|
138
|
+
candidates.push(f);
|
|
139
|
+
}
|
|
140
|
+
const picked = candidates.slice(0, maxK).map((c, i) => ({
|
|
141
|
+
key: keyForIndex(i),
|
|
142
|
+
...c
|
|
143
|
+
}));
|
|
144
|
+
const nowIso = new Date().toISOString();
|
|
145
|
+
const referencesDoc = BridgeReferencesFileSchema.parse({
|
|
146
|
+
schema_version: "bridge.references.v1",
|
|
147
|
+
run_id,
|
|
148
|
+
project_id,
|
|
149
|
+
created_at: nowIso,
|
|
150
|
+
query: {
|
|
151
|
+
raw_user_intent: rawUserIntent,
|
|
152
|
+
keywords,
|
|
153
|
+
constraints,
|
|
154
|
+
must_have: mustHave,
|
|
155
|
+
must_not: mustNot
|
|
156
|
+
},
|
|
157
|
+
candidates: picked
|
|
158
|
+
});
|
|
159
|
+
const bridgeDir = path.join(context.runs_path, "bridge");
|
|
160
|
+
const referencesPathAbs = path.join(bridgeDir, "references.json");
|
|
161
|
+
if (input?.write_files !== false) {
|
|
162
|
+
await fs.mkdir(bridgeDir, { recursive: true });
|
|
163
|
+
await fs.writeFile(referencesPathAbs, JSON.stringify(referencesDoc, null, 2) + "\n", "utf-8");
|
|
164
|
+
}
|
|
165
|
+
const out = {
|
|
166
|
+
success: true,
|
|
167
|
+
project_id,
|
|
168
|
+
run_id,
|
|
169
|
+
references_path: toRelPath(basePath, referencesPathAbs),
|
|
170
|
+
candidates: picked.map((c) => ({ key: c.key, title: c.title, primary_link: c.links.primary })),
|
|
171
|
+
warnings,
|
|
172
|
+
next_action: {
|
|
173
|
+
tool: "vibe_pm.bridge_confirm_reference",
|
|
174
|
+
reason: "A/B/C 중 하나를 선택해 레퍼런스를 확정하세요."
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
function deriveKeywordsFromContextScan(args) {
|
|
180
|
+
const ingressDir = path.join(args.runDirAbs, "ingress");
|
|
181
|
+
const repoMapAbs = path.join(ingressDir, "repo_map.msgpack");
|
|
182
|
+
const riskFlagsAbs = path.join(ingressDir, "risk_flags.json");
|
|
183
|
+
const summaryAbs = path.join(ingressDir, "ingress_summary.json");
|
|
184
|
+
const out = [];
|
|
185
|
+
const push = (s) => {
|
|
186
|
+
const v = (s ?? "").trim();
|
|
187
|
+
if (!v)
|
|
188
|
+
return;
|
|
189
|
+
out.push(v);
|
|
190
|
+
};
|
|
191
|
+
// 1) ingress_summary.json (cheap JSON)
|
|
192
|
+
try {
|
|
193
|
+
if (fsSync.existsSync(summaryAbs)) {
|
|
194
|
+
const raw = JSON.parse(fsSync.readFileSync(summaryAbs, "utf-8"));
|
|
195
|
+
const hint = typeof raw?.high_level?.repo_kind_hint === "string" ? raw.high_level.repo_kind_hint : "";
|
|
196
|
+
hint.split(/[\s+]+/).forEach(push);
|
|
197
|
+
const reasons = Array.isArray(raw?.high_level?.clarification_reasons) ? raw.high_level.clarification_reasons : [];
|
|
198
|
+
for (const r of reasons) {
|
|
199
|
+
if (typeof r === "string" && r.trim())
|
|
200
|
+
push(r.trim().toLowerCase());
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// ignore
|
|
206
|
+
}
|
|
207
|
+
// 2) risk_flags.json (cheap JSON)
|
|
208
|
+
try {
|
|
209
|
+
if (fsSync.existsSync(riskFlagsAbs)) {
|
|
210
|
+
const raw = JSON.parse(fsSync.readFileSync(riskFlagsAbs, "utf-8"));
|
|
211
|
+
const flags = raw?.flags ?? {};
|
|
212
|
+
if (flags?.monorepo_suspected)
|
|
213
|
+
push("monorepo");
|
|
214
|
+
if (flags?.entrypoint_ambiguous)
|
|
215
|
+
push("entrypoint");
|
|
216
|
+
if (flags?.has_env_files)
|
|
217
|
+
push("env");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// ignore
|
|
222
|
+
}
|
|
223
|
+
// 3) repo_map.msgpack (best-effort, still deterministic)
|
|
224
|
+
try {
|
|
225
|
+
if (fsSync.existsSync(repoMapAbs)) {
|
|
226
|
+
const decoded = decodeMsgpack(fsSync.readFileSync(repoMapAbs));
|
|
227
|
+
const summary = decoded?.summary ?? {};
|
|
228
|
+
const topDirs = Array.isArray(summary?.top_dirs) ? summary.top_dirs : [];
|
|
229
|
+
for (const d of topDirs.slice(0, 8)) {
|
|
230
|
+
if (typeof d === "string" && d.trim())
|
|
231
|
+
push(d.trim());
|
|
232
|
+
}
|
|
233
|
+
const signals = Array.isArray(summary?.signals) ? summary.signals : [];
|
|
234
|
+
for (const s of signals.slice(0, 8)) {
|
|
235
|
+
if (typeof s === "string" && s.trim())
|
|
236
|
+
push(s.trim());
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// ignore
|
|
242
|
+
}
|
|
243
|
+
// Normalize + dedupe + clamp
|
|
244
|
+
const seen = new Set();
|
|
245
|
+
const normalized = [];
|
|
246
|
+
for (const k of out) {
|
|
247
|
+
const v = k.toLowerCase().replace(/[^\w-]+/g, " ").trim().split(/\s+/)[0] ?? "";
|
|
248
|
+
if (!v)
|
|
249
|
+
continue;
|
|
250
|
+
if (seen.has(v))
|
|
251
|
+
continue;
|
|
252
|
+
seen.add(v);
|
|
253
|
+
normalized.push(v);
|
|
254
|
+
if (normalized.length >= 12)
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
return normalized;
|
|
258
|
+
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
// adapters/mcp-ts/src/tools/vibe_pm/briefing.ts
|
|
2
2
|
// vibe_pm.briefing - Project intake and structuring
|
|
3
|
+
import * as fsPromises from "node:fs/promises";
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
3
6
|
import { runEngine } from "../../engine.js";
|
|
4
7
|
import { safeJsonParse } from "../../cli.js";
|
|
5
|
-
import { generateRunId, resolveProjectId, ensureRunsDir, getRunsDir } from "./context.js";
|
|
8
|
+
import { generateRunId, resolveProjectId, ensureRunsDir, getRunsDir, getRunContext } from "./context.js";
|
|
6
9
|
import { validateBriefingInput } from "../../security/input-validator.js";
|
|
7
10
|
import { ensureResearchForBriefing } from "./modules/ensure.js";
|
|
8
11
|
import { memorySync } from "./memory_sync.js";
|
|
9
12
|
import { getAuthGate } from "../../auth/index.js";
|
|
10
13
|
import { SpecHighDecisionDraftOutputSchema } from "../../generated/spec_high_decision_draft_output.js";
|
|
14
|
+
import { IngressResolutionFileSchema } from "../../generated/ingress_resolution_file.js";
|
|
11
15
|
import { applyPromptDensityHysteresis, computePromptDensity, loadLatestPromptDensityForProject, savePromptDensity } from "./intent/prompt_density.js";
|
|
12
16
|
import { generateIntentFromBriefing, saveIntentWithMarkdown } from "./intent/index.js";
|
|
13
17
|
/**
|
|
@@ -35,11 +39,31 @@ export async function briefing(input) {
|
|
|
35
39
|
// Mode defaults to balanced
|
|
36
40
|
const mode = input.mode ?? "balanced";
|
|
37
41
|
// Step 1: Initialize the run
|
|
38
|
-
|
|
39
|
-
const initResult = await runEngine("spec-high", ["--root", "engines/spec_high", "init", run_id], { timeoutMs: 60_000 });
|
|
42
|
+
const initResult = await runEngine("spec-high", ["--root", "engines/spec_high", "init", run_id, "--mode", mode], { timeoutMs: 60_000 });
|
|
40
43
|
if (initResult.code !== 0) {
|
|
41
44
|
throw new Error(`프로젝트 초기화 실패: ${initResult.stderr || "알 수 없는 오류"}`);
|
|
42
45
|
}
|
|
46
|
+
// Ingress v2 (best-effort): record ingress_resolution.json so downstream tools can gate deterministically.
|
|
47
|
+
try {
|
|
48
|
+
const ctx = getRunContext(run_id, basePath);
|
|
49
|
+
const ingressDir = path.join(ctx.runs_path, "ingress");
|
|
50
|
+
await fsPromises.mkdir(ingressDir, { recursive: true });
|
|
51
|
+
const doc = IngressResolutionFileSchema.parse({
|
|
52
|
+
schema_version: "ingress.resolution.v2",
|
|
53
|
+
run_id,
|
|
54
|
+
project_id,
|
|
55
|
+
workspace_path: path.resolve(basePath),
|
|
56
|
+
resolution: "INGRESS_RESOLVED_NEW_TASK",
|
|
57
|
+
has_vibe_state: fs.existsSync(path.join(basePath, ".vibe")),
|
|
58
|
+
context_scan_required: false,
|
|
59
|
+
context_scan_completed: false,
|
|
60
|
+
created_at: new Date().toISOString()
|
|
61
|
+
});
|
|
62
|
+
await fsPromises.writeFile(path.join(ingressDir, "ingress_resolution.json"), JSON.stringify(doc, null, 2) + "\n", "utf-8");
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// ignore (best-effort)
|
|
66
|
+
}
|
|
43
67
|
// Step 2: Set the project brief as input
|
|
44
68
|
const inputSetResult = await runEngine("spec-high", ["--root", "engines/spec_high", "input-set", run_id, "--text", input.project_brief], { timeoutMs: 60_000 });
|
|
45
69
|
if (inputSetResult.code !== 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);
|