@vibecodetown/mcp-server 2.1.4 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/build/auth/credential_store.js +146 -0
- package/build/auth/public_key.js +6 -4
- package/build/bootstrap/doctor.js +113 -5
- package/build/bootstrap/installer.js +85 -15
- package/build/bootstrap/registry.js +11 -6
- package/build/bootstrap/skills-installer.js +365 -0
- package/build/control_plane/gate.js +52 -70
- package/build/dx/activity.js +26 -3
- package/build/engine.js +151 -0
- package/build/errors.js +107 -0
- package/build/generated/bridge_build_seed_input.js +2 -0
- package/build/generated/bridge_build_seed_output.js +2 -0
- package/build/generated/bridge_confirm_reference_input.js +2 -0
- package/build/generated/bridge_confirm_reference_output.js +2 -0
- package/build/generated/bridge_confirmed_reference_file.js +2 -0
- package/build/generated/bridge_generate_references_input.js +2 -0
- package/build/generated/bridge_generate_references_output.js +2 -0
- package/build/generated/bridge_references_file.js +2 -0
- package/build/generated/bridge_work_order_seed_file.js +2 -0
- package/build/generated/contracts_bundle_info.js +3 -3
- package/build/generated/index.js +14 -0
- package/build/generated/ingress_input.js +2 -0
- package/build/generated/ingress_output.js +2 -0
- package/build/generated/ingress_resolution_file.js +2 -0
- package/build/generated/ingress_summary_file.js +2 -0
- package/build/generated/message_template_id_mapping_file.js +2 -0
- package/build/generated/run_app_input.js +1 -1
- package/build/index.js +4 -1
- package/build/local-mode/git.js +36 -22
- package/build/local-mode/paths.js +1 -0
- package/build/local-mode/project-state.js +176 -0
- package/build/local-mode/setup.js +21 -1
- package/build/local-mode/templates.js +3 -3
- package/build/path-utils.js +68 -0
- package/build/runtime/cli_invoker.js +416 -0
- package/build/tools/vibe_pm/advisory_review.js +5 -3
- package/build/tools/vibe_pm/bridge_build_seed.js +164 -0
- package/build/tools/vibe_pm/bridge_confirm_reference.js +91 -0
- package/build/tools/vibe_pm/bridge_generate_references.js +258 -0
- package/build/tools/vibe_pm/briefing.js +26 -1
- package/build/tools/vibe_pm/context.js +79 -0
- package/build/tools/vibe_pm/create_work_order.js +200 -3
- package/build/tools/vibe_pm/doctor.js +95 -0
- package/build/tools/vibe_pm/entity_gate/preflight.js +8 -3
- package/build/tools/vibe_pm/export_output.js +14 -13
- package/build/tools/vibe_pm/finalize_work.js +74 -0
- package/build/tools/vibe_pm/force_override.js +104 -0
- package/build/tools/vibe_pm/get_decision.js +2 -2
- package/build/tools/vibe_pm/index.js +160 -3
- package/build/tools/vibe_pm/ingress.js +645 -0
- package/build/tools/vibe_pm/ingress_gate.js +116 -0
- package/build/tools/vibe_pm/inspect_code.js +90 -20
- package/build/tools/vibe_pm/kce/doc_usage.js +4 -9
- package/build/tools/vibe_pm/kce/on_finalize.js +2 -2
- package/build/tools/vibe_pm/kce/preflight.js +11 -7
- package/build/tools/vibe_pm/list_rules.js +135 -0
- package/build/tools/vibe_pm/memory_status.js +11 -8
- package/build/tools/vibe_pm/memory_sync.js +11 -8
- package/build/tools/vibe_pm/pm_language.js +17 -16
- package/build/tools/vibe_pm/pre_commit_analysis.js +292 -0
- package/build/tools/vibe_pm/publish_mcp.js +271 -0
- package/build/tools/vibe_pm/python_error.js +115 -0
- package/build/tools/vibe_pm/run_app.js +215 -86
- package/build/tools/vibe_pm/run_app_podman.js +64 -2
- package/build/tools/vibe_pm/save_rule.js +120 -0
- package/build/tools/vibe_pm/search_oss.js +5 -3
- package/build/tools/vibe_pm/spec_rag.js +185 -0
- package/build/tools/vibe_pm/status.js +50 -3
- package/build/tools/vibe_pm/submit_decision.js +2 -2
- package/build/tools/vibe_pm/types.js +28 -0
- package/build/tools/vibe_pm/undo_last_task.js +23 -20
- package/build/tools/vibe_pm/waiter_mapping.js +155 -0
- package/build/tools/vibe_pm/zoekt_evidence.js +5 -3
- package/build/tools.js +13 -5
- package/build/version-check.js +5 -5
- package/build/vibe-cli.js +742 -39
- package/package.json +5 -4
- package/skills/VRIP_INSTALL_MANIFEST_DOCTOR.skill.md +288 -0
- package/skills/index.json +14 -0
|
@@ -1,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 {
|
|
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
|
|
69
|
+
const { code, stdout, stderr } = await runPythonCli(args, { timeoutMs });
|
|
69
70
|
if (code !== 0) {
|
|
70
|
-
|
|
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) {
|