@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,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
+ }