@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,645 @@
|
|
|
1
|
+
// adapters/mcp-ts/src/tools/vibe_pm/ingress.ts
|
|
2
|
+
// vibe_pm.ingress - Ingress v2 runtime entrypoint (writes ingress_resolution + optional context_scan artifacts)
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as fsPromises from "node:fs/promises";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { encode as encodeMsgpack } from "@msgpack/msgpack";
|
|
7
|
+
import { simpleGit } from "simple-git";
|
|
8
|
+
import { validateToolInput } from "../../security/input-validator.js";
|
|
9
|
+
import { invokeSystem } from "../../runtime/cli_invoker.js";
|
|
10
|
+
import { generateRunId, resolveProjectId, findLatestRunId, getRunContext } from "./context.js";
|
|
11
|
+
import { IngressResolutionFileSchema } from "../../generated/ingress_resolution_file.js";
|
|
12
|
+
import { IngressSummaryFileSchema } from "../../generated/ingress_summary_file.js";
|
|
13
|
+
function toRelPath(basePath, absPath) {
|
|
14
|
+
const rel = path.relative(basePath, absPath);
|
|
15
|
+
return rel.replace(/\\\\/g, "/");
|
|
16
|
+
}
|
|
17
|
+
async function writeJsonAtomic(absPath, doc) {
|
|
18
|
+
await fsPromises.mkdir(path.dirname(absPath), { recursive: true });
|
|
19
|
+
const tmp = `${absPath}.tmp`;
|
|
20
|
+
await fsPromises.writeFile(tmp, JSON.stringify(doc, null, 2) + "\n", "utf-8");
|
|
21
|
+
await fsPromises.rename(tmp, absPath);
|
|
22
|
+
}
|
|
23
|
+
async function writeBinaryAtomic(absPath, bytes) {
|
|
24
|
+
await fsPromises.mkdir(path.dirname(absPath), { recursive: true });
|
|
25
|
+
const tmp = `${absPath}.tmp`;
|
|
26
|
+
await fsPromises.writeFile(tmp, bytes);
|
|
27
|
+
await fsPromises.rename(tmp, absPath);
|
|
28
|
+
}
|
|
29
|
+
async function writeJsonlAtomic(absPath, lines) {
|
|
30
|
+
await fsPromises.mkdir(path.dirname(absPath), { recursive: true });
|
|
31
|
+
const tmp = `${absPath}.tmp`;
|
|
32
|
+
await fsPromises.writeFile(tmp, lines.join("\n") + "\n", "utf-8");
|
|
33
|
+
await fsPromises.rename(tmp, absPath);
|
|
34
|
+
}
|
|
35
|
+
function existsDir(p) {
|
|
36
|
+
try {
|
|
37
|
+
return fs.existsSync(p) && fs.statSync(p).isDirectory();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function safeReadJsonFile(absPath) {
|
|
44
|
+
try {
|
|
45
|
+
if (!fs.existsSync(absPath))
|
|
46
|
+
return null;
|
|
47
|
+
return JSON.parse(fs.readFileSync(absPath, "utf-8"));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function listTopLevelDirs(workspacePath) {
|
|
54
|
+
try {
|
|
55
|
+
const entries = fs.readdirSync(workspacePath, { withFileTypes: true });
|
|
56
|
+
return entries
|
|
57
|
+
.filter((e) => e.isDirectory())
|
|
58
|
+
.map((e) => e.name)
|
|
59
|
+
.filter((name) => name !== ".git" && name !== "node_modules" && name !== ".vibe" && name !== "dist" && name !== "build")
|
|
60
|
+
.slice(0, 50);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function detectStackSignals(workspacePath) {
|
|
67
|
+
const signals = [];
|
|
68
|
+
const has = (rel) => fs.existsSync(path.join(workspacePath, rel));
|
|
69
|
+
const hasAny = (rels) => rels.some((r) => has(r));
|
|
70
|
+
if (has("package.json"))
|
|
71
|
+
signals.push("package.json");
|
|
72
|
+
if (has("tsconfig.json"))
|
|
73
|
+
signals.push("tsconfig.json");
|
|
74
|
+
if (has("pnpm-lock.yaml"))
|
|
75
|
+
signals.push("pnpm-lock.yaml");
|
|
76
|
+
if (has("yarn.lock"))
|
|
77
|
+
signals.push("yarn.lock");
|
|
78
|
+
if (has("pyproject.toml"))
|
|
79
|
+
signals.push("pyproject.toml");
|
|
80
|
+
if (hasAny(["requirements.txt", "requirements-dev.txt"]))
|
|
81
|
+
signals.push("requirements.txt");
|
|
82
|
+
if (has("Cargo.toml"))
|
|
83
|
+
signals.push("Cargo.toml");
|
|
84
|
+
if (has("go.mod"))
|
|
85
|
+
signals.push("go.mod");
|
|
86
|
+
if (hasAny(["pom.xml", "build.gradle", "build.gradle.kts"]))
|
|
87
|
+
signals.push("java_build");
|
|
88
|
+
try {
|
|
89
|
+
if (fs.readdirSync(workspacePath).some((n) => n.endsWith(".csproj")))
|
|
90
|
+
signals.push("dotnet_csproj");
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// ignore
|
|
94
|
+
}
|
|
95
|
+
const languages_hint = [];
|
|
96
|
+
if (signals.includes("tsconfig.json"))
|
|
97
|
+
languages_hint.push({ id: "ts", confidence: 5 });
|
|
98
|
+
if (signals.includes("package.json") && !signals.includes("tsconfig.json"))
|
|
99
|
+
languages_hint.push({ id: "js", confidence: 4 });
|
|
100
|
+
if (signals.includes("pyproject.toml") || signals.includes("requirements.txt"))
|
|
101
|
+
languages_hint.push({ id: "py", confidence: 4 });
|
|
102
|
+
if (signals.includes("Cargo.toml"))
|
|
103
|
+
languages_hint.push({ id: "rs", confidence: 4 });
|
|
104
|
+
if (signals.includes("go.mod"))
|
|
105
|
+
languages_hint.push({ id: "go", confidence: 4 });
|
|
106
|
+
if (signals.includes("java_build"))
|
|
107
|
+
languages_hint.push({ id: "java", confidence: 3 });
|
|
108
|
+
if (signals.includes("dotnet_csproj"))
|
|
109
|
+
languages_hint.push({ id: "dotnet", confidence: 3 });
|
|
110
|
+
const topDirs = listTopLevelDirs(workspacePath);
|
|
111
|
+
const monorepo_hint = languages_hint.length >= 2 ||
|
|
112
|
+
topDirs.includes("apps") ||
|
|
113
|
+
topDirs.includes("packages") ||
|
|
114
|
+
topDirs.includes("services");
|
|
115
|
+
return { signals, languages_hint, monorepo_hint };
|
|
116
|
+
}
|
|
117
|
+
function deriveEntrypoints(workspacePath) {
|
|
118
|
+
const run_candidates = [];
|
|
119
|
+
const test_candidates = [];
|
|
120
|
+
const pkgPath = path.join(workspacePath, "package.json");
|
|
121
|
+
const pkg = safeReadJsonFile(pkgPath);
|
|
122
|
+
const scripts = pkg && typeof pkg === "object" ? (pkg.scripts ?? {}) : {};
|
|
123
|
+
const hasScript = (k) => typeof scripts?.[k] === "string" && scripts[k].trim().length > 0;
|
|
124
|
+
if (fs.existsSync(pkgPath) && scripts && typeof scripts === "object") {
|
|
125
|
+
if (hasScript("dev")) {
|
|
126
|
+
run_candidates.push({
|
|
127
|
+
key: "A",
|
|
128
|
+
label: "개발 서버 켜기",
|
|
129
|
+
kind: "dev_server",
|
|
130
|
+
confidence: 4,
|
|
131
|
+
cwd: ".",
|
|
132
|
+
command: ["npm", "run", "dev"],
|
|
133
|
+
evidence: ["package.json:scripts.dev"]
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (hasScript("start")) {
|
|
137
|
+
run_candidates.push({
|
|
138
|
+
key: run_candidates.length === 0 ? "A" : run_candidates.length === 1 ? "B" : "C",
|
|
139
|
+
label: "앱 실행",
|
|
140
|
+
kind: "start",
|
|
141
|
+
confidence: 3,
|
|
142
|
+
cwd: ".",
|
|
143
|
+
command: ["npm", "run", "start"],
|
|
144
|
+
evidence: ["package.json:scripts.start"]
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (hasScript("test")) {
|
|
148
|
+
test_candidates.push({
|
|
149
|
+
key: "A",
|
|
150
|
+
label: "테스트 실행",
|
|
151
|
+
kind: "test",
|
|
152
|
+
confidence: 4,
|
|
153
|
+
cwd: ".",
|
|
154
|
+
command: ["npm", "test"],
|
|
155
|
+
evidence: ["package.json:scripts.test"]
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Python
|
|
160
|
+
if (run_candidates.length === 0 && (fs.existsSync(path.join(workspacePath, "pyproject.toml")) || fs.existsSync(path.join(workspacePath, "requirements.txt")))) {
|
|
161
|
+
run_candidates.push({
|
|
162
|
+
key: "A",
|
|
163
|
+
label: "파이썬 실행",
|
|
164
|
+
kind: "python_run",
|
|
165
|
+
confidence: 2,
|
|
166
|
+
cwd: ".",
|
|
167
|
+
command: ["python", "-m", "app"],
|
|
168
|
+
evidence: ["pyproject.toml|requirements.txt"]
|
|
169
|
+
});
|
|
170
|
+
test_candidates.push({
|
|
171
|
+
key: "A",
|
|
172
|
+
label: "pytest 실행",
|
|
173
|
+
kind: "test",
|
|
174
|
+
confidence: 3,
|
|
175
|
+
cwd: ".",
|
|
176
|
+
command: ["python", "-m", "pytest"],
|
|
177
|
+
evidence: ["pyproject.toml|requirements.txt"]
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
// Rust
|
|
181
|
+
if (run_candidates.length === 0 && fs.existsSync(path.join(workspacePath, "Cargo.toml"))) {
|
|
182
|
+
test_candidates.push({
|
|
183
|
+
key: "A",
|
|
184
|
+
label: "cargo test",
|
|
185
|
+
kind: "test",
|
|
186
|
+
confidence: 4,
|
|
187
|
+
cwd: ".",
|
|
188
|
+
command: ["cargo", "test"],
|
|
189
|
+
evidence: ["Cargo.toml"]
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
const entrypoint_ambiguous = run_candidates.length >= 2;
|
|
193
|
+
return { run_candidates, test_candidates, entrypoint_ambiguous };
|
|
194
|
+
}
|
|
195
|
+
function resolveIngressScanBinary(basePath) {
|
|
196
|
+
const override = (process.env.VIBECODE_INGRESS_SCAN_BIN ?? "").trim();
|
|
197
|
+
const exe = process.platform === "win32" ? "vpm-back.exe" : "vpm-back";
|
|
198
|
+
const candidates = [];
|
|
199
|
+
if (override) {
|
|
200
|
+
candidates.push(path.isAbsolute(override) ? override : path.resolve(basePath, override));
|
|
201
|
+
}
|
|
202
|
+
// Dev-friendly defaults (repo-local builds)
|
|
203
|
+
candidates.push(path.join(basePath, "engines", "target", "release", exe));
|
|
204
|
+
candidates.push(path.join(basePath, "engines", "target", "debug", exe));
|
|
205
|
+
for (const c of candidates) {
|
|
206
|
+
try {
|
|
207
|
+
if (fs.existsSync(c) && fs.statSync(c).isFile())
|
|
208
|
+
return c;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// ignore
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
async function runIngressScanBinary(args) {
|
|
217
|
+
const warnings = [];
|
|
218
|
+
const res = await invokeSystem(args.binPath, [
|
|
219
|
+
"ingress-scan",
|
|
220
|
+
"--repo-root",
|
|
221
|
+
args.workspacePath,
|
|
222
|
+
"--run-dir",
|
|
223
|
+
args.runDir,
|
|
224
|
+
"--run-id",
|
|
225
|
+
args.run_id
|
|
226
|
+
], args.basePath, { timeoutMs: 120_000 });
|
|
227
|
+
if (res.exitCode !== 0) {
|
|
228
|
+
return { ok: false, error: `ingress_scan_failed: exitCode=${res.exitCode} stderr=${res.stderr.trim()}` };
|
|
229
|
+
}
|
|
230
|
+
for (const rel of args.requiredFiles) {
|
|
231
|
+
const abs = path.join(args.runDir, "ingress", rel);
|
|
232
|
+
if (!fs.existsSync(abs)) {
|
|
233
|
+
return { ok: false, error: `ingress_scan_missing_output: ${rel}` };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Validate summary contract (Front consumption baseline)
|
|
237
|
+
const summaryAbs = path.join(args.runDir, "ingress", "ingress_summary.json");
|
|
238
|
+
try {
|
|
239
|
+
const raw = JSON.parse(fs.readFileSync(summaryAbs, "utf-8"));
|
|
240
|
+
const parsed = IngressSummaryFileSchema.safeParse(raw);
|
|
241
|
+
if (!parsed.success)
|
|
242
|
+
return { ok: false, error: "ingress_scan_invalid_summary" };
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return { ok: false, error: "ingress_scan_invalid_summary" };
|
|
246
|
+
}
|
|
247
|
+
warnings.push("CONTEXT_SCAN_BIN_USED");
|
|
248
|
+
return { ok: true, warnings };
|
|
249
|
+
}
|
|
250
|
+
async function indexDocsJsonl(workspacePath, run_id) {
|
|
251
|
+
const out = [];
|
|
252
|
+
const header = {
|
|
253
|
+
type: "__header__",
|
|
254
|
+
format_version: "ingress.docs_index.v1",
|
|
255
|
+
run_id,
|
|
256
|
+
created_at: new Date().toISOString(),
|
|
257
|
+
producer: { name: "vibe_pm.ingress", version: "v1" }
|
|
258
|
+
};
|
|
259
|
+
out.push(JSON.stringify(header));
|
|
260
|
+
const candidates = ["README.md", "docs/README.md", "docs/SSOT.md", "docs/CURRENT_SPEC.md"].map((p) => path.join(workspacePath, p));
|
|
261
|
+
let docsCount = 0;
|
|
262
|
+
for (const abs of candidates) {
|
|
263
|
+
try {
|
|
264
|
+
if (!fs.existsSync(abs))
|
|
265
|
+
continue;
|
|
266
|
+
const st = fs.statSync(abs);
|
|
267
|
+
if (!st.isFile())
|
|
268
|
+
continue;
|
|
269
|
+
const rel = path.relative(workspacePath, abs).replace(/\\\\/g, "/");
|
|
270
|
+
out.push(JSON.stringify({
|
|
271
|
+
type: "doc",
|
|
272
|
+
path: rel,
|
|
273
|
+
kind: rel.toLowerCase().includes("ssot") ? "ssot" : rel.toLowerCase().includes("readme") ? "readme" : "doc",
|
|
274
|
+
bytes: st.size,
|
|
275
|
+
mtime: st.mtime.toISOString(),
|
|
276
|
+
signals: []
|
|
277
|
+
}));
|
|
278
|
+
docsCount += 1;
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
// ignore
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return { lines: out, docs_count: docsCount };
|
|
285
|
+
}
|
|
286
|
+
async function writeContextScanArtifacts(opts) {
|
|
287
|
+
const warnings = [];
|
|
288
|
+
const ingressDirAbs = path.join(opts.runDir, "ingress");
|
|
289
|
+
const projectFingerprintAbs = path.join(ingressDirAbs, "project_fingerprint.json");
|
|
290
|
+
const repoMapAbs = path.join(ingressDirAbs, "repo_map.msgpack");
|
|
291
|
+
const entrypointsAbs = path.join(ingressDirAbs, "entrypoints.msgpack");
|
|
292
|
+
const docsIndexAbs = path.join(ingressDirAbs, "docs_index.jsonl");
|
|
293
|
+
const riskFlagsAbs = path.join(ingressDirAbs, "risk_flags.json");
|
|
294
|
+
const ingressSummaryAbs = path.join(ingressDirAbs, "ingress_summary.json");
|
|
295
|
+
const artifactsRel = {
|
|
296
|
+
project_fingerprint: toRelPath(opts.basePath, projectFingerprintAbs),
|
|
297
|
+
repo_map: toRelPath(opts.basePath, repoMapAbs),
|
|
298
|
+
entrypoints: toRelPath(opts.basePath, entrypointsAbs),
|
|
299
|
+
docs_index: toRelPath(opts.basePath, docsIndexAbs),
|
|
300
|
+
risk_flags: toRelPath(opts.basePath, riskFlagsAbs),
|
|
301
|
+
ingress_summary: toRelPath(opts.basePath, ingressSummaryAbs)
|
|
302
|
+
};
|
|
303
|
+
if (!opts.write_files) {
|
|
304
|
+
return {
|
|
305
|
+
completed: false,
|
|
306
|
+
warnings: ["WRITE_SKIPPED: write_files=false"],
|
|
307
|
+
artifacts: artifactsRel,
|
|
308
|
+
risk_flags: { entrypoint_ambiguous: true, monorepo_suspected: false }
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
let completed = true;
|
|
312
|
+
// Prefer back binary when available (P28). Fall back to TS scan on failure.
|
|
313
|
+
const binPath = resolveIngressScanBinary(opts.basePath);
|
|
314
|
+
if (binPath) {
|
|
315
|
+
try {
|
|
316
|
+
const required = [
|
|
317
|
+
"project_fingerprint.json",
|
|
318
|
+
"repo_map.msgpack",
|
|
319
|
+
"entrypoints.msgpack",
|
|
320
|
+
"docs_index.jsonl",
|
|
321
|
+
"risk_flags.json",
|
|
322
|
+
"ingress_summary.json"
|
|
323
|
+
];
|
|
324
|
+
const bin = await runIngressScanBinary({
|
|
325
|
+
basePath: opts.basePath,
|
|
326
|
+
binPath,
|
|
327
|
+
workspacePath: opts.workspacePath,
|
|
328
|
+
runDir: opts.runDir,
|
|
329
|
+
run_id: opts.run_id,
|
|
330
|
+
requiredFiles: required
|
|
331
|
+
});
|
|
332
|
+
if (bin.ok) {
|
|
333
|
+
warnings.push(...bin.warnings);
|
|
334
|
+
// Derive minimal risk flags for auto-trigger (cheap read)
|
|
335
|
+
let entrypointAmbiguous = true;
|
|
336
|
+
let monorepoSuspected = false;
|
|
337
|
+
try {
|
|
338
|
+
const raw = JSON.parse(fs.readFileSync(riskFlagsAbs, "utf-8"));
|
|
339
|
+
entrypointAmbiguous = Boolean(raw?.flags?.entrypoint_ambiguous);
|
|
340
|
+
monorepoSuspected = Boolean(raw?.flags?.monorepo_suspected);
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
// keep defaults
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
completed: true,
|
|
347
|
+
warnings,
|
|
348
|
+
artifacts: artifactsRel,
|
|
349
|
+
risk_flags: { entrypoint_ambiguous: entrypointAmbiguous, monorepo_suspected: monorepoSuspected }
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
warnings.push(`CONTEXT_SCAN_BIN_FAILED: ${bin.error}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch (e) {
|
|
357
|
+
warnings.push(`CONTEXT_SCAN_BIN_FAILED: ${e instanceof Error ? e.message : String(e)}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// 1) project_fingerprint.json (best-effort)
|
|
361
|
+
try {
|
|
362
|
+
const git = simpleGit({ baseDir: opts.workspacePath });
|
|
363
|
+
const isRepo = await git.checkIsRepo();
|
|
364
|
+
let remoteUrl = null;
|
|
365
|
+
let branch = null;
|
|
366
|
+
let head = null;
|
|
367
|
+
let dirty = false;
|
|
368
|
+
if (isRepo) {
|
|
369
|
+
try {
|
|
370
|
+
remoteUrl = (await git.raw(["remote", "get-url", "origin"])).trim() || null;
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
remoteUrl = null;
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
const b = await git.branch();
|
|
377
|
+
branch = b.current || null;
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
branch = null;
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
head = (await git.revparse(["HEAD"])).trim() || null;
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
head = null;
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
const st = await git.status();
|
|
390
|
+
dirty = (st.files?.length ?? 0) > 0;
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
dirty = false;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
await writeJsonAtomic(projectFingerprintAbs, {
|
|
397
|
+
format_version: "ingress.project_fingerprint.v1",
|
|
398
|
+
run_id: opts.run_id,
|
|
399
|
+
created_at: new Date().toISOString(),
|
|
400
|
+
producer: { name: "vibe_pm.ingress", version: "v1" },
|
|
401
|
+
repo_root: path.resolve(opts.workspacePath),
|
|
402
|
+
is_git: isRepo,
|
|
403
|
+
git: isRepo
|
|
404
|
+
? { remote_url: remoteUrl, branch, head_commit: head, dirty }
|
|
405
|
+
: null
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
catch (e) {
|
|
409
|
+
completed = false;
|
|
410
|
+
warnings.push(`FINGERPRINT_FAILED: ${e instanceof Error ? e.message : String(e)}`);
|
|
411
|
+
}
|
|
412
|
+
// 2) repo_map.msgpack (best-effort)
|
|
413
|
+
const stack = detectStackSignals(opts.workspacePath);
|
|
414
|
+
try {
|
|
415
|
+
const topDirs = listTopLevelDirs(opts.workspacePath);
|
|
416
|
+
const doc = {
|
|
417
|
+
format_version: "ingress.repo_map.v1",
|
|
418
|
+
run_id: opts.run_id,
|
|
419
|
+
created_at: new Date().toISOString(),
|
|
420
|
+
producer: { name: "vibe_pm.ingress", version: "v1" },
|
|
421
|
+
summary: {
|
|
422
|
+
top_dirs: topDirs,
|
|
423
|
+
languages_hint: stack.languages_hint,
|
|
424
|
+
monorepo_hint: stack.monorepo_hint,
|
|
425
|
+
signals: stack.signals
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
await writeBinaryAtomic(repoMapAbs, encodeMsgpack(doc));
|
|
429
|
+
}
|
|
430
|
+
catch (e) {
|
|
431
|
+
completed = false;
|
|
432
|
+
warnings.push(`REPO_MAP_FAILED: ${e instanceof Error ? e.message : String(e)}`);
|
|
433
|
+
}
|
|
434
|
+
// 3) entrypoints.msgpack (best-effort)
|
|
435
|
+
const entry = deriveEntrypoints(opts.workspacePath);
|
|
436
|
+
try {
|
|
437
|
+
const doc = {
|
|
438
|
+
format_version: "ingress.entrypoints.v1",
|
|
439
|
+
run_id: opts.run_id,
|
|
440
|
+
created_at: new Date().toISOString(),
|
|
441
|
+
producer: { name: "vibe_pm.ingress", version: "v1" },
|
|
442
|
+
run_candidates: entry.run_candidates,
|
|
443
|
+
test_candidates: entry.test_candidates
|
|
444
|
+
};
|
|
445
|
+
await writeBinaryAtomic(entrypointsAbs, encodeMsgpack(doc));
|
|
446
|
+
}
|
|
447
|
+
catch (e) {
|
|
448
|
+
completed = false;
|
|
449
|
+
warnings.push(`ENTRYPOINTS_FAILED: ${e instanceof Error ? e.message : String(e)}`);
|
|
450
|
+
}
|
|
451
|
+
// 4) docs_index.jsonl (best-effort)
|
|
452
|
+
let docsIndexCount = 0;
|
|
453
|
+
try {
|
|
454
|
+
const docs = await indexDocsJsonl(opts.workspacePath, opts.run_id);
|
|
455
|
+
docsIndexCount = docs.docs_count;
|
|
456
|
+
await writeJsonlAtomic(docsIndexAbs, docs.lines);
|
|
457
|
+
}
|
|
458
|
+
catch (e) {
|
|
459
|
+
completed = false;
|
|
460
|
+
warnings.push(`DOCS_INDEX_FAILED: ${e instanceof Error ? e.message : String(e)}`);
|
|
461
|
+
}
|
|
462
|
+
// 5) risk_flags.json (best-effort)
|
|
463
|
+
const hasEnvFiles = fs.existsSync(path.join(opts.workspacePath, ".env")) || fs.existsSync(path.join(opts.workspacePath, ".env.local"));
|
|
464
|
+
let hasSecretsLikeFiles = existsDir(path.join(opts.workspacePath, ".ssh"));
|
|
465
|
+
if (!hasSecretsLikeFiles) {
|
|
466
|
+
try {
|
|
467
|
+
hasSecretsLikeFiles = fs.readdirSync(opts.workspacePath).some((n) => n.endsWith(".pem") || n.endsWith(".key"));
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
hasSecretsLikeFiles = false;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const risk = {
|
|
474
|
+
entrypoint_ambiguous: entry.entrypoint_ambiguous,
|
|
475
|
+
monorepo_suspected: stack.monorepo_hint
|
|
476
|
+
};
|
|
477
|
+
try {
|
|
478
|
+
await writeJsonAtomic(riskFlagsAbs, {
|
|
479
|
+
format_version: "ingress.risk_flags.v1",
|
|
480
|
+
run_id: opts.run_id,
|
|
481
|
+
created_at: new Date().toISOString(),
|
|
482
|
+
producer: { name: "vibe_pm.ingress", version: "v1" },
|
|
483
|
+
flags: {
|
|
484
|
+
has_secrets_like_files: hasSecretsLikeFiles,
|
|
485
|
+
has_env_files: hasEnvFiles,
|
|
486
|
+
monorepo_suspected: stack.monorepo_hint,
|
|
487
|
+
entrypoint_ambiguous: entry.entrypoint_ambiguous,
|
|
488
|
+
huge_repo: fs.existsSync(path.join(opts.workspacePath, "node_modules"))
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
catch (e) {
|
|
493
|
+
completed = false;
|
|
494
|
+
warnings.push(`RISK_FLAGS_FAILED: ${e instanceof Error ? e.message : String(e)}`);
|
|
495
|
+
}
|
|
496
|
+
// 6) ingress_summary.json (best-effort, required for Front consumption)
|
|
497
|
+
try {
|
|
498
|
+
const entrypointAmbiguous = entry.run_candidates.length > 1 || entry.test_candidates.length > 1;
|
|
499
|
+
const entrypointStatus = entry.run_candidates.length === 0 && entry.test_candidates.length === 0
|
|
500
|
+
? "NONE"
|
|
501
|
+
: entrypointAmbiguous
|
|
502
|
+
? "AMBIGUOUS"
|
|
503
|
+
: "OK";
|
|
504
|
+
const clarificationReasons = [];
|
|
505
|
+
if (entrypointAmbiguous)
|
|
506
|
+
clarificationReasons.push("ENTRYPOINT_AMBIGUOUS");
|
|
507
|
+
if (stack.monorepo_hint)
|
|
508
|
+
clarificationReasons.push("MONOREPO_SUSPECTED");
|
|
509
|
+
const repoKindHint = stack.languages_hint.length === 0
|
|
510
|
+
? "unknown"
|
|
511
|
+
: stack.languages_hint
|
|
512
|
+
.slice(0, 3)
|
|
513
|
+
.map((x) => x.id)
|
|
514
|
+
.join("+") + (stack.monorepo_hint ? " monorepo" : "");
|
|
515
|
+
const summaryDoc = IngressSummaryFileSchema.parse({
|
|
516
|
+
format_version: "ingress.summary.v1",
|
|
517
|
+
run_id: opts.run_id,
|
|
518
|
+
created_at: new Date().toISOString(),
|
|
519
|
+
producer: { name: "vibe_pm.ingress", version: "v1" },
|
|
520
|
+
pointers: {
|
|
521
|
+
project_fingerprint: "project_fingerprint.json",
|
|
522
|
+
repo_map: "repo_map.msgpack",
|
|
523
|
+
entrypoints: "entrypoints.msgpack",
|
|
524
|
+
docs_index: "docs_index.jsonl",
|
|
525
|
+
risk_flags: "risk_flags.json"
|
|
526
|
+
},
|
|
527
|
+
high_level: {
|
|
528
|
+
repo_kind_hint: repoKindHint,
|
|
529
|
+
entrypoint_status: entrypointStatus,
|
|
530
|
+
clarification_recommended: clarificationReasons.length > 0,
|
|
531
|
+
clarification_reasons: clarificationReasons,
|
|
532
|
+
docs_index_count: docsIndexCount
|
|
533
|
+
},
|
|
534
|
+
consumption_policy: {
|
|
535
|
+
front_reads: ["ingress_summary.json", "risk_flags.json"],
|
|
536
|
+
bridge_reads: ["repo_map.msgpack", "docs_index.jsonl", "entrypoints.msgpack"],
|
|
537
|
+
back_reads: ["*"]
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
await writeJsonAtomic(ingressSummaryAbs, summaryDoc);
|
|
541
|
+
}
|
|
542
|
+
catch (e) {
|
|
543
|
+
completed = false;
|
|
544
|
+
warnings.push(`INGRESS_SUMMARY_FAILED: ${e instanceof Error ? e.message : String(e)}`);
|
|
545
|
+
}
|
|
546
|
+
return { completed, warnings, artifacts: artifactsRel, risk_flags: risk };
|
|
547
|
+
}
|
|
548
|
+
export async function ingress(input, basePath = process.cwd()) {
|
|
549
|
+
validateToolInput({ project_id: input.project_id, run_id: input.run_id });
|
|
550
|
+
const workspacePath = path.resolve(basePath, input.workspace_path ?? ".");
|
|
551
|
+
if (!existsDir(workspacePath)) {
|
|
552
|
+
throw new Error(`workspace_path not found: ${workspacePath}`);
|
|
553
|
+
}
|
|
554
|
+
const resolution = input.resolution;
|
|
555
|
+
const hasVibeState = fs.existsSync(path.join(workspacePath, ".vibe"));
|
|
556
|
+
const preferredId = input.run_id ?? input.project_id;
|
|
557
|
+
// run_id resolution:
|
|
558
|
+
// - RESUME prefers existing latest run when not provided.
|
|
559
|
+
// - NEW_TASK/ADOPT generates a new run_id when not provided.
|
|
560
|
+
let run_id = null;
|
|
561
|
+
if (typeof preferredId === "string" && preferredId.trim()) {
|
|
562
|
+
run_id = preferredId.trim();
|
|
563
|
+
}
|
|
564
|
+
else if (resolution === "INGRESS_RESOLVED_RESUME") {
|
|
565
|
+
run_id = findLatestRunId(basePath);
|
|
566
|
+
if (!run_id) {
|
|
567
|
+
throw new Error("resume requires an existing run_id");
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
const project_id_guess = resolveProjectId(undefined, basePath);
|
|
572
|
+
run_id = generateRunId(project_id_guess, basePath);
|
|
573
|
+
}
|
|
574
|
+
const project_id = resolveProjectId(run_id, basePath);
|
|
575
|
+
// Ensure Spec-High run root exists so future tools resolve the same run_dir.
|
|
576
|
+
const specHighRunDir = path.join(basePath, "engines", "spec_high", "runs", run_id);
|
|
577
|
+
await fsPromises.mkdir(specHighRunDir, { recursive: true });
|
|
578
|
+
const context = getRunContext(run_id, basePath);
|
|
579
|
+
const ingressDirAbs = path.join(context.runs_path, "ingress");
|
|
580
|
+
const ingressResolutionAbs = path.join(ingressDirAbs, "ingress_resolution.json");
|
|
581
|
+
const ingressResolutionRel = toRelPath(basePath, ingressResolutionAbs);
|
|
582
|
+
const context_scan_required = resolution !== "INGRESS_ABORTED";
|
|
583
|
+
const doScan = input.context_scan !== false && context_scan_required;
|
|
584
|
+
const writeFiles = input.write_files !== false;
|
|
585
|
+
let scanCompleted = false;
|
|
586
|
+
let artifacts;
|
|
587
|
+
const warnings = [];
|
|
588
|
+
if (doScan) {
|
|
589
|
+
const scan = await writeContextScanArtifacts({
|
|
590
|
+
basePath,
|
|
591
|
+
workspacePath,
|
|
592
|
+
run_id,
|
|
593
|
+
runDir: context.runs_path,
|
|
594
|
+
write_files: writeFiles
|
|
595
|
+
});
|
|
596
|
+
scanCompleted = scan.completed;
|
|
597
|
+
artifacts = scan.artifacts;
|
|
598
|
+
warnings.push(...scan.warnings);
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
scanCompleted = !context_scan_required;
|
|
602
|
+
}
|
|
603
|
+
const resolutionDoc = IngressResolutionFileSchema.parse({
|
|
604
|
+
schema_version: "ingress.resolution.v2",
|
|
605
|
+
run_id,
|
|
606
|
+
project_id,
|
|
607
|
+
workspace_path: workspacePath,
|
|
608
|
+
resolution,
|
|
609
|
+
has_vibe_state: hasVibeState,
|
|
610
|
+
context_scan_required,
|
|
611
|
+
context_scan_completed: scanCompleted,
|
|
612
|
+
created_at: new Date().toISOString()
|
|
613
|
+
});
|
|
614
|
+
if (writeFiles) {
|
|
615
|
+
await writeJsonAtomic(ingressResolutionAbs, resolutionDoc);
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
warnings.push("WRITE_SKIPPED: ingress_resolution.json not written (write_files=false)");
|
|
619
|
+
}
|
|
620
|
+
const next_action = resolution === "INGRESS_ABORTED"
|
|
621
|
+
? null
|
|
622
|
+
: scanCompleted
|
|
623
|
+
? { tool: "vibe_pm.get_decision", reason: "이제 결재 단계로 넘어갈 수 있습니다." }
|
|
624
|
+
: { tool: "vibe_pm.ingress", reason: "사전 점검(컨텍스트 스캔)을 완료해야 합니다." };
|
|
625
|
+
const message = resolution === "INGRESS_ABORTED"
|
|
626
|
+
? "Ingress가 중단 상태로 기록되었습니다."
|
|
627
|
+
: scanCompleted
|
|
628
|
+
? "Ingress 및 사전 점검(컨텍스트 스캔)을 완료했습니다."
|
|
629
|
+
: "Ingress 상태를 기록했습니다. 사전 점검(컨텍스트 스캔)이 아직 완료되지 않았습니다.";
|
|
630
|
+
return {
|
|
631
|
+
success: true,
|
|
632
|
+
project_id,
|
|
633
|
+
run_id,
|
|
634
|
+
workspace_path: workspacePath,
|
|
635
|
+
has_vibe_state: hasVibeState,
|
|
636
|
+
resolution,
|
|
637
|
+
context_scan_required,
|
|
638
|
+
context_scan_completed: scanCompleted,
|
|
639
|
+
ingress_resolution_path: ingressResolutionRel,
|
|
640
|
+
artifacts,
|
|
641
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
642
|
+
message,
|
|
643
|
+
next_action
|
|
644
|
+
};
|
|
645
|
+
}
|