@vibecodetown/mcp-server 2.1.4 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +10 -10
  2. package/build/auth/credential_store.js +146 -0
  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/control_plane/gate.js +52 -70
  9. package/build/dx/activity.js +26 -3
  10. package/build/engine.js +151 -0
  11. package/build/errors.js +107 -0
  12. package/build/generated/bridge_build_seed_input.js +2 -0
  13. package/build/generated/bridge_build_seed_output.js +2 -0
  14. package/build/generated/bridge_confirm_reference_input.js +2 -0
  15. package/build/generated/bridge_confirm_reference_output.js +2 -0
  16. package/build/generated/bridge_confirmed_reference_file.js +2 -0
  17. package/build/generated/bridge_generate_references_input.js +2 -0
  18. package/build/generated/bridge_generate_references_output.js +2 -0
  19. package/build/generated/bridge_references_file.js +2 -0
  20. package/build/generated/bridge_work_order_seed_file.js +2 -0
  21. package/build/generated/contracts_bundle_info.js +3 -3
  22. package/build/generated/index.js +14 -0
  23. package/build/generated/ingress_input.js +2 -0
  24. package/build/generated/ingress_output.js +2 -0
  25. package/build/generated/ingress_resolution_file.js +2 -0
  26. package/build/generated/ingress_summary_file.js +2 -0
  27. package/build/generated/message_template_id_mapping_file.js +2 -0
  28. package/build/generated/run_app_input.js +1 -1
  29. package/build/index.js +4 -1
  30. package/build/local-mode/git.js +36 -22
  31. package/build/local-mode/paths.js +1 -0
  32. package/build/local-mode/project-state.js +176 -0
  33. package/build/local-mode/setup.js +21 -1
  34. package/build/local-mode/templates.js +3 -3
  35. package/build/path-utils.js +68 -0
  36. package/build/runtime/cli_invoker.js +416 -0
  37. package/build/tools/vibe_pm/advisory_review.js +5 -3
  38. package/build/tools/vibe_pm/bridge_build_seed.js +164 -0
  39. package/build/tools/vibe_pm/bridge_confirm_reference.js +91 -0
  40. package/build/tools/vibe_pm/bridge_generate_references.js +258 -0
  41. package/build/tools/vibe_pm/briefing.js +26 -1
  42. package/build/tools/vibe_pm/context.js +79 -0
  43. package/build/tools/vibe_pm/create_work_order.js +200 -3
  44. package/build/tools/vibe_pm/doctor.js +95 -0
  45. package/build/tools/vibe_pm/entity_gate/preflight.js +8 -3
  46. package/build/tools/vibe_pm/export_output.js +14 -13
  47. package/build/tools/vibe_pm/finalize_work.js +74 -0
  48. package/build/tools/vibe_pm/force_override.js +104 -0
  49. package/build/tools/vibe_pm/get_decision.js +2 -2
  50. package/build/tools/vibe_pm/index.js +160 -3
  51. package/build/tools/vibe_pm/ingress.js +645 -0
  52. package/build/tools/vibe_pm/ingress_gate.js +116 -0
  53. package/build/tools/vibe_pm/inspect_code.js +90 -20
  54. package/build/tools/vibe_pm/kce/doc_usage.js +4 -9
  55. package/build/tools/vibe_pm/kce/on_finalize.js +2 -2
  56. package/build/tools/vibe_pm/kce/preflight.js +11 -7
  57. package/build/tools/vibe_pm/list_rules.js +135 -0
  58. package/build/tools/vibe_pm/memory_status.js +11 -8
  59. package/build/tools/vibe_pm/memory_sync.js +11 -8
  60. package/build/tools/vibe_pm/pm_language.js +17 -16
  61. package/build/tools/vibe_pm/pre_commit_analysis.js +292 -0
  62. package/build/tools/vibe_pm/publish_mcp.js +271 -0
  63. package/build/tools/vibe_pm/python_error.js +115 -0
  64. package/build/tools/vibe_pm/run_app.js +215 -86
  65. package/build/tools/vibe_pm/run_app_podman.js +64 -2
  66. package/build/tools/vibe_pm/save_rule.js +120 -0
  67. package/build/tools/vibe_pm/search_oss.js +5 -3
  68. package/build/tools/vibe_pm/spec_rag.js +185 -0
  69. package/build/tools/vibe_pm/status.js +50 -3
  70. package/build/tools/vibe_pm/submit_decision.js +2 -2
  71. package/build/tools/vibe_pm/types.js +28 -0
  72. package/build/tools/vibe_pm/undo_last_task.js +23 -20
  73. package/build/tools/vibe_pm/waiter_mapping.js +155 -0
  74. package/build/tools/vibe_pm/zoekt_evidence.js +5 -3
  75. package/build/tools.js +13 -5
  76. package/build/version-check.js +5 -5
  77. package/build/vibe-cli.js +742 -39
  78. package/package.json +5 -4
  79. package/skills/VRIP_INSTALL_MANIFEST_DOCTOR.skill.md +288 -0
  80. package/skills/index.json +14 -0
@@ -1,10 +1,11 @@
1
1
  // adapters/mcp-ts/src/tools/vibe_pm/advisory_review.ts
2
2
  // vibe_pm.advisory_review - Advisory review (triage/quick/thorough) with routing + cache
3
- import { runEngine } from "../../engine.js";
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 { AdvisoryReviewOutputSchema } from "../../generated/advisory_review_output.js";
8
+ import { classifyPythonError } from "./python_error.js";
8
9
  function clipArg(value, maxChars) {
9
10
  const v = value.trim();
10
11
  if (v.length <= maxChars)
@@ -65,9 +66,10 @@ export async function advisoryReview(input) {
65
66
  args.push("--product-intent-refs", input.product_intent_refs.join(","));
66
67
  }
67
68
  const timeoutMs = mode === "triage" ? 30_000 : 180_000;
68
- const { code, stdout, stderr } = await runEngine("vibecoding-helper", args, { timeoutMs });
69
+ const { code, stdout, stderr } = await runPythonCli(args, { timeoutMs });
69
70
  if (code !== 0) {
70
- throw new Error(stderr?.trim() ? `advisory_review_engine_error: ${stderr.trim()}` : `advisory_review_exit_code: ${code}`);
71
+ const classified = classifyPythonError(stderr ?? "", code, "Advisory Review");
72
+ throw new Error(`${classified.issueCode}: ${classified.summary}`);
71
73
  }
72
74
  const parsed = safeJsonParse(stdout);
73
75
  if (!parsed.ok) {
@@ -0,0 +1,164 @@
1
+ // adapters/mcp-ts/src/tools/vibe_pm/bridge_build_seed.ts
2
+ // vibe_pm.bridge_build_seed - Create runs/<run_id>/bridge/work_order_seed.json
3
+ import * as fs from "node:fs/promises";
4
+ import * as path from "node:path";
5
+ import { validateToolInput } from "../../security/input-validator.js";
6
+ import { resolveProjectId, resolveRunId, getRunContext } from "./context.js";
7
+ import { BridgeConfirmedReferenceFileSchema } from "../../generated/bridge_confirmed_reference_file.js";
8
+ import { BridgeReferencesFileSchema } from "../../generated/bridge_references_file.js";
9
+ import { BridgeWorkOrderSeedFileSchema } from "../../generated/bridge_work_order_seed_file.js";
10
+ function sanitizeStrings(values, maxItems) {
11
+ if (!Array.isArray(values))
12
+ return [];
13
+ return values
14
+ .map((v) => (typeof v === "string" ? v.trim() : ""))
15
+ .filter(Boolean)
16
+ .slice(0, maxItems);
17
+ }
18
+ function toRelPath(basePath, absPath) {
19
+ const rel = path.relative(basePath, absPath);
20
+ return rel.replace(/\\\\/g, "/");
21
+ }
22
+ function looksKorean(text) {
23
+ return /[가-힣]/.test(text);
24
+ }
25
+ export async function bridgeBuildSeed(input, basePath = process.cwd()) {
26
+ validateToolInput({ project_id: input.project_id, run_id: input.run_id });
27
+ const resolvedRun = resolveRunId(input.run_id ?? input.project_id, basePath);
28
+ const run_id = resolvedRun.run_id;
29
+ const project_id = resolveProjectId(run_id, basePath);
30
+ const context = getRunContext(run_id, basePath);
31
+ const bridgeDir = path.join(context.runs_path, "bridge");
32
+ const confirmedPathAbs = path.join(bridgeDir, "confirmed_reference.json");
33
+ const referencesPathAbs = path.join(bridgeDir, "references.json");
34
+ const seedPathAbs = path.join(bridgeDir, "work_order_seed.json");
35
+ // Rule C: confirmed_reference 없이는 seed 생성 불가
36
+ let confirmedRaw;
37
+ try {
38
+ confirmedRaw = JSON.parse(await fs.readFile(confirmedPathAbs, "utf-8"));
39
+ }
40
+ catch {
41
+ throw new Error("confirmed_reference.json이 없어 seed를 만들 수 없습니다. 먼저 레퍼런스를 확정하세요.");
42
+ }
43
+ const confirmed = BridgeConfirmedReferenceFileSchema.parse(confirmedRaw);
44
+ const overwrite = input?.overwrite !== false;
45
+ if (!overwrite) {
46
+ try {
47
+ await fs.access(seedPathAbs);
48
+ throw new Error("work_order_seed.json이 이미 존재합니다. overwrite=true로 다시 시도하세요.");
49
+ }
50
+ catch {
51
+ // ok (does not exist)
52
+ }
53
+ }
54
+ let rawIntent = typeof input?.raw_user_intent === "string" ? input.raw_user_intent.trim() : "";
55
+ if (!rawIntent) {
56
+ try {
57
+ const refsRaw = JSON.parse(await fs.readFile(referencesPathAbs, "utf-8"));
58
+ const refs = BridgeReferencesFileSchema.parse(refsRaw);
59
+ rawIntent = typeof refs.query?.raw_user_intent === "string" ? refs.query.raw_user_intent.trim() : "";
60
+ }
61
+ catch {
62
+ // ignore
63
+ }
64
+ }
65
+ if (!rawIntent)
66
+ rawIntent = "Clarify requirements using the selected reference.";
67
+ const isKo = looksKorean(rawIntent);
68
+ const goal = typeof input?.goal === "string" && input.goal.trim()
69
+ ? input.goal.trim()
70
+ : isKo
71
+ ? `레퍼런스(${confirmed.selected.title}) 기준으로 요구사항을 정규화한다: ${rawIntent}`
72
+ : `Normalize requirements using reference (${confirmed.selected.title}): ${rawIntent}`;
73
+ const userStory = typeof input?.user_story === "string" && input.user_story.trim()
74
+ ? input.user_story.trim()
75
+ : isKo
76
+ ? `사용자로서, 나는 ${rawIntent}을(를) 원한다. 그래서 원하는 결과를 빠르게 얻을 수 있다.`
77
+ : `As a user, I want ${rawIntent} so I can get the intended result quickly.`;
78
+ const acceptance = sanitizeStrings(input?.acceptance, 12);
79
+ const acceptanceFinal = acceptance.length >= 3
80
+ ? acceptance
81
+ : isKo
82
+ ? [
83
+ "레퍼런스 확정(confirmed_reference.json)이 기록된다",
84
+ "seed(work_order_seed.json)이 생성된다",
85
+ "create_work_order가 seed를 소비한다"
86
+ ]
87
+ : [
88
+ "Confirmed reference is written (confirmed_reference.json)",
89
+ "Seed is written (work_order_seed.json)",
90
+ "create_work_order consumes the seed when it exists"
91
+ ];
92
+ const constraints = mergeOverrides(sanitizeStrings(input?.constraints, 12), sanitizeStrings(confirmed?.user_confirmation?.must_have_overrides, 12), 12);
93
+ const nonGoals = mergeOverrides(sanitizeStrings(input?.non_goals, 8), sanitizeStrings(confirmed?.user_confirmation?.must_not_overrides, 8), 8);
94
+ const scopeSeedRaw = input?.scope_seed;
95
+ const scope_seed = scopeSeedRaw && typeof scopeSeedRaw === "object"
96
+ ? {
97
+ include: sanitizeStrings(scopeSeedRaw?.include, 20),
98
+ exclude: sanitizeStrings(scopeSeedRaw?.exclude, 20),
99
+ do_not_touch: sanitizeStrings(scopeSeedRaw?.do_not_touch, 20)
100
+ }
101
+ : {
102
+ include: ["src/**", "tests/**"],
103
+ exclude: ["**/.vibe/**", "config/**"],
104
+ do_not_touch: ["config/**", ".env*", "*.pem", "*.key", ".vibe/**"]
105
+ };
106
+ if (scope_seed.include.length === 0)
107
+ scope_seed.include = ["src/**"];
108
+ const preferredStackHint = typeof input?.preferred_stack_hint === "string" ? input.preferred_stack_hint : "";
109
+ const riskNotes = sanitizeStrings(input?.risk_notes, 8);
110
+ const nowIso = new Date().toISOString();
111
+ const seedDoc = BridgeWorkOrderSeedFileSchema.parse({
112
+ schema_version: "bridge.work_order_seed.v1",
113
+ run_id,
114
+ project_id,
115
+ created_at: nowIso,
116
+ intent: {
117
+ goal,
118
+ user_story: userStory,
119
+ acceptance: acceptanceFinal,
120
+ constraints,
121
+ non_goals: nonGoals
122
+ },
123
+ scope_seed,
124
+ reference: {
125
+ key: confirmed.selected.key,
126
+ title: confirmed.selected.title,
127
+ primary_link: confirmed.selected.primary_link,
128
+ takeaways: []
129
+ },
130
+ handoff: {
131
+ preferred_stack_hint: preferredStackHint,
132
+ risk_notes: riskNotes
133
+ }
134
+ });
135
+ if (input?.write_files !== false) {
136
+ await fs.mkdir(bridgeDir, { recursive: true });
137
+ await fs.writeFile(seedPathAbs, JSON.stringify(seedDoc, null, 2) + "\n", "utf-8");
138
+ }
139
+ const out = {
140
+ success: true,
141
+ project_id,
142
+ run_id,
143
+ seed_path: toRelPath(basePath, seedPathAbs),
144
+ next_action: {
145
+ tool: "vibe_pm.create_work_order",
146
+ reason: "seed를 기반으로 작업 지시서를 발행하세요."
147
+ }
148
+ };
149
+ return out;
150
+ }
151
+ function mergeOverrides(base, overrides, maxItems) {
152
+ const seen = new Set();
153
+ const out = [];
154
+ for (const x of [...base, ...overrides]) {
155
+ const v = typeof x === "string" ? x.trim() : "";
156
+ if (!v || seen.has(v))
157
+ continue;
158
+ seen.add(v);
159
+ out.push(v);
160
+ if (out.length >= maxItems)
161
+ break;
162
+ }
163
+ return out;
164
+ }
@@ -0,0 +1,91 @@
1
+ // adapters/mcp-ts/src/tools/vibe_pm/bridge_confirm_reference.ts
2
+ // vibe_pm.bridge_confirm_reference - Create runs/<run_id>/bridge/confirmed_reference.json
3
+ import * as fs from "node:fs/promises";
4
+ import * as path from "node:path";
5
+ import { validateToolInput } from "../../security/input-validator.js";
6
+ import { resolveProjectId, resolveRunId, getRunContext } from "./context.js";
7
+ import { BridgeReferencesFileSchema } from "../../generated/bridge_references_file.js";
8
+ import { BridgeConfirmedReferenceFileSchema } from "../../generated/bridge_confirmed_reference_file.js";
9
+ function sanitizeStrings(values, maxItems) {
10
+ if (!Array.isArray(values))
11
+ return [];
12
+ return values
13
+ .map((v) => (typeof v === "string" ? v.trim() : ""))
14
+ .filter(Boolean)
15
+ .slice(0, maxItems);
16
+ }
17
+ function toRelPath(basePath, absPath) {
18
+ const rel = path.relative(basePath, absPath);
19
+ return rel.replace(/\\\\/g, "/");
20
+ }
21
+ export async function bridgeConfirmReference(input, basePath = process.cwd()) {
22
+ validateToolInput({ project_id: input.project_id, run_id: input.run_id });
23
+ const resolvedRun = resolveRunId(input.run_id ?? input.project_id, basePath);
24
+ const run_id = resolvedRun.run_id;
25
+ const project_id = resolveProjectId(run_id, basePath);
26
+ const context = getRunContext(run_id, basePath);
27
+ const bridgeDir = path.join(context.runs_path, "bridge");
28
+ const referencesPathAbs = path.join(bridgeDir, "references.json");
29
+ const confirmedPathAbs = path.join(bridgeDir, "confirmed_reference.json");
30
+ let referencesRaw;
31
+ try {
32
+ referencesRaw = JSON.parse(await fs.readFile(referencesPathAbs, "utf-8"));
33
+ }
34
+ catch {
35
+ throw new Error("references.json이 없어 레퍼런스를 확정할 수 없습니다. 먼저 레퍼런스 후보를 생성하세요.");
36
+ }
37
+ const references = BridgeReferencesFileSchema.parse(referencesRaw);
38
+ const selectedKey = String(input?.selected_key ?? "").trim();
39
+ if (!["A", "B", "C"].includes(selectedKey)) {
40
+ throw new Error("selected_key는 A/B/C 중 하나여야 합니다.");
41
+ }
42
+ const selected = (references.candidates ?? []).find((c) => c?.key === selectedKey);
43
+ if (!selected) {
44
+ throw new Error("선택한 레퍼런스를 찾을 수 없습니다. A/B/C 중 다시 선택해주세요.");
45
+ }
46
+ const rawAnswer = String(input?.raw_answer ?? "").trim();
47
+ if (!rawAnswer)
48
+ throw new Error("raw_answer is required");
49
+ const notes = typeof input?.notes === "string" ? input.notes : "";
50
+ const mustHave = sanitizeStrings(input?.must_have_overrides, 12);
51
+ const mustNot = sanitizeStrings(input?.must_not_overrides, 12);
52
+ const nowIso = new Date().toISOString();
53
+ const confirmedDoc = BridgeConfirmedReferenceFileSchema.parse({
54
+ schema_version: "bridge.confirmed_reference.v1",
55
+ run_id,
56
+ project_id,
57
+ created_at: nowIso,
58
+ selected: {
59
+ key: selectedKey,
60
+ title: selected.title,
61
+ kind: selected.kind,
62
+ primary_link: selected.links.primary
63
+ },
64
+ user_confirmation: {
65
+ raw_answer: rawAnswer,
66
+ notes,
67
+ must_have_overrides: mustHave,
68
+ must_not_overrides: mustNot
69
+ }
70
+ });
71
+ if (input?.write_files !== false) {
72
+ await fs.mkdir(bridgeDir, { recursive: true });
73
+ await fs.writeFile(confirmedPathAbs, JSON.stringify(confirmedDoc, null, 2) + "\n", "utf-8");
74
+ }
75
+ const out = {
76
+ success: true,
77
+ project_id,
78
+ run_id,
79
+ confirmed_reference_path: toRelPath(basePath, confirmedPathAbs),
80
+ selected: {
81
+ key: confirmedDoc.selected.key,
82
+ title: confirmedDoc.selected.title,
83
+ primary_link: confirmedDoc.selected.primary_link
84
+ },
85
+ next_action: {
86
+ tool: "vibe_pm.bridge_build_seed",
87
+ reason: "확정된 레퍼런스를 기반으로 seed를 생성하세요."
88
+ }
89
+ };
90
+ return out;
91
+ }
@@ -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
  /**
@@ -39,6 +43,27 @@ export async function briefing(input) {
39
43
  if (initResult.code !== 0) {
40
44
  throw new Error(`프로젝트 초기화 실패: ${initResult.stderr || "알 수 없는 오류"}`);
41
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
+ }
42
67
  // Step 2: Set the project brief as input
43
68
  const inputSetResult = await runEngine("spec-high", ["--root", "engines/spec_high", "input-set", run_id, "--text", input.project_brief], { timeoutMs: 60_000 });
44
69
  if (inputSetResult.code !== 0) {