@vibecodetown/mcp-server 2.2.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +10 -10
  2. package/build/auth/index.js +0 -2
  3. package/build/auth/public_key.js +6 -4
  4. package/build/bootstrap/doctor.js +113 -5
  5. package/build/bootstrap/installer.js +85 -15
  6. package/build/bootstrap/registry.js +11 -6
  7. package/build/bootstrap/skills-installer.js +365 -0
  8. package/build/dx/activity.js +26 -3
  9. package/build/engine.js +151 -0
  10. package/build/errors.js +107 -0
  11. package/build/generated/bridge_build_seed_input.js +2 -0
  12. package/build/generated/bridge_build_seed_output.js +2 -0
  13. package/build/generated/bridge_confirm_reference_input.js +2 -0
  14. package/build/generated/bridge_confirm_reference_output.js +2 -0
  15. package/build/generated/bridge_confirmed_reference_file.js +2 -0
  16. package/build/generated/bridge_generate_references_input.js +2 -0
  17. package/build/generated/bridge_generate_references_output.js +2 -0
  18. package/build/generated/bridge_references_file.js +2 -0
  19. package/build/generated/bridge_work_order_seed_file.js +2 -0
  20. package/build/generated/contracts_bundle_info.js +3 -3
  21. package/build/generated/index.js +14 -0
  22. package/build/generated/ingress_input.js +2 -0
  23. package/build/generated/ingress_output.js +2 -0
  24. package/build/generated/ingress_resolution_file.js +2 -0
  25. package/build/generated/ingress_summary_file.js +2 -0
  26. package/build/generated/message_template_id_mapping_file.js +2 -0
  27. package/build/generated/run_app_input.js +1 -1
  28. package/build/index.js +4 -3
  29. package/build/local-mode/paths.js +1 -0
  30. package/build/local-mode/setup.js +21 -1
  31. package/build/path-utils.js +68 -0
  32. package/build/runtime/cli_invoker.js +1 -1
  33. package/build/tools/vibe_pm/advisory_review.js +5 -3
  34. package/build/tools/vibe_pm/bridge_build_seed.js +164 -0
  35. package/build/tools/vibe_pm/bridge_confirm_reference.js +91 -0
  36. package/build/tools/vibe_pm/bridge_generate_references.js +258 -0
  37. package/build/tools/vibe_pm/briefing.js +27 -3
  38. package/build/tools/vibe_pm/context.js +79 -0
  39. package/build/tools/vibe_pm/create_work_order.js +200 -3
  40. package/build/tools/vibe_pm/doctor.js +95 -0
  41. package/build/tools/vibe_pm/entity_gate/preflight.js +8 -3
  42. package/build/tools/vibe_pm/export_output.js +14 -13
  43. package/build/tools/vibe_pm/finalize_work.js +78 -40
  44. package/build/tools/vibe_pm/get_decision.js +2 -2
  45. package/build/tools/vibe_pm/index.js +128 -42
  46. package/build/tools/vibe_pm/ingress.js +645 -0
  47. package/build/tools/vibe_pm/ingress_gate.js +116 -0
  48. package/build/tools/vibe_pm/inspect_code.js +90 -20
  49. package/build/tools/vibe_pm/kce/doc_usage.js +4 -9
  50. package/build/tools/vibe_pm/kce/on_finalize.js +2 -2
  51. package/build/tools/vibe_pm/kce/preflight.js +11 -7
  52. package/build/tools/vibe_pm/memory_status.js +11 -8
  53. package/build/tools/vibe_pm/memory_sync.js +11 -8
  54. package/build/tools/vibe_pm/pm_language.js +17 -16
  55. package/build/tools/vibe_pm/python_error.js +115 -0
  56. package/build/tools/vibe_pm/run_app.js +169 -43
  57. package/build/tools/vibe_pm/run_app_podman.js +64 -2
  58. package/build/tools/vibe_pm/search_oss.js +5 -3
  59. package/build/tools/vibe_pm/spec_rag.js +185 -0
  60. package/build/tools/vibe_pm/status.js +50 -3
  61. package/build/tools/vibe_pm/submit_decision.js +2 -2
  62. package/build/tools/vibe_pm/types.js +28 -0
  63. package/build/tools/vibe_pm/undo_last_task.js +9 -2
  64. package/build/tools/vibe_pm/waiter_mapping.js +155 -0
  65. package/build/tools/vibe_pm/zoekt_evidence.js +5 -3
  66. package/build/tools.js +13 -5
  67. package/build/vibe-cli.js +245 -7
  68. package/package.json +5 -4
  69. package/skills/VRIP_INSTALL_MANIFEST_DOCTOR.skill.md +288 -0
  70. package/skills/index.json +14 -0
@@ -0,0 +1,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
- // Note: spec-high init doesn't support --mode flag, mode is handled at MCP layer
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
- const scopeInclude = Array.isArray(dg?.allow_paths)
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
- const scopeExclude = Array.isArray(dg?.deny_paths)
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
- const doNotTouch = Array.isArray(dg?.read_only_paths)
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);