@vibecodetown/mcp-server 2.1.0

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 (172) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +269 -0
  3. package/build/auth/gate.js +225 -0
  4. package/build/auth/index.js +55 -0
  5. package/build/auth/public_key.js +27 -0
  6. package/build/auth/token_cache.js +122 -0
  7. package/build/auth/token_verifier.js +103 -0
  8. package/build/bootstrap/doctor.js +115 -0
  9. package/build/bootstrap/installer.js +673 -0
  10. package/build/bootstrap/lock.js +37 -0
  11. package/build/bootstrap/platform.js +26 -0
  12. package/build/bootstrap/registry.js +37 -0
  13. package/build/cache/index.js +147 -0
  14. package/build/cli.js +101 -0
  15. package/build/contracts.js +22 -0
  16. package/build/control_plane/gate.js +161 -0
  17. package/build/control_plane/index.js +6 -0
  18. package/build/dx/activity.js +139 -0
  19. package/build/engine.js +106 -0
  20. package/build/errors.js +171 -0
  21. package/build/generated/activate_input.js +2 -0
  22. package/build/generated/activate_output.js +57 -0
  23. package/build/generated/advisory_review_input.js +2 -0
  24. package/build/generated/advisory_review_output.js +35 -0
  25. package/build/generated/auth_token_file.js +2 -0
  26. package/build/generated/briefing_input.js +2 -0
  27. package/build/generated/briefing_output.js +2 -0
  28. package/build/generated/clinic_bridge_file.js +13 -0
  29. package/build/generated/contracts_bundle_info.js +5 -0
  30. package/build/generated/create_work_order_input.js +2 -0
  31. package/build/generated/create_work_order_output.js +2 -0
  32. package/build/generated/current_work_order_file.js +2 -0
  33. package/build/generated/doctor_input.js +2 -0
  34. package/build/generated/doctor_output.js +24 -0
  35. package/build/generated/execution_result.js +2 -0
  36. package/build/generated/execution_task.js +2 -0
  37. package/build/generated/export_output_input.js +2 -0
  38. package/build/generated/export_output_output.js +2 -0
  39. package/build/generated/finalize_work_input.js +2 -0
  40. package/build/generated/finalize_work_output.js +2 -0
  41. package/build/generated/gate_input.js +2 -0
  42. package/build/generated/gate_output.js +2 -0
  43. package/build/generated/gate_result_v1.js +2 -0
  44. package/build/generated/get_decision_input.js +2 -0
  45. package/build/generated/get_decision_output.js +13 -0
  46. package/build/generated/handoff_to_clinic.js +2 -0
  47. package/build/generated/index.js +75 -0
  48. package/build/generated/inspect_code_input.js +2 -0
  49. package/build/generated/inspect_code_output.js +13 -0
  50. package/build/generated/memory_retrieve_output.js +2 -0
  51. package/build/generated/memory_state_file.js +2 -0
  52. package/build/generated/memory_status_input.js +2 -0
  53. package/build/generated/memory_status_output.js +13 -0
  54. package/build/generated/memory_sync_input.js +2 -0
  55. package/build/generated/memory_sync_output.js +13 -0
  56. package/build/generated/plugin_result.js +2 -0
  57. package/build/generated/react_perf_check_patterns_input.js +2 -0
  58. package/build/generated/react_perf_check_patterns_output.js +2 -0
  59. package/build/generated/react_perf_generate_report_input.js +2 -0
  60. package/build/generated/react_perf_generate_report_output.js +2 -0
  61. package/build/generated/repair_plan_input.js +2 -0
  62. package/build/generated/repair_plan_output.js +2 -0
  63. package/build/generated/run_app_input.js +2 -0
  64. package/build/generated/run_app_output.js +2 -0
  65. package/build/generated/run_state_file.js +13 -0
  66. package/build/generated/scaffold_input.js +2 -0
  67. package/build/generated/scaffold_output.js +2 -0
  68. package/build/generated/search_oss_input.js +2 -0
  69. package/build/generated/search_oss_output.js +2 -0
  70. package/build/generated/selection_validation_result.js +2 -0
  71. package/build/generated/signal_agent_input.js +2 -0
  72. package/build/generated/spec_high_ask_queue_items_file.js +2 -0
  73. package/build/generated/spec_high_clinic_bridge_output.js +2 -0
  74. package/build/generated/spec_high_decision_draft_output.js +2 -0
  75. package/build/generated/spec_high_validate_output.js +2 -0
  76. package/build/generated/status_input.js +2 -0
  77. package/build/generated/status_output.js +2 -0
  78. package/build/generated/submit_decision_input.js +2 -0
  79. package/build/generated/submit_decision_output.js +2 -0
  80. package/build/generated/tool_error_output.js +2 -0
  81. package/build/generated/undo_last_task_input.js +2 -0
  82. package/build/generated/undo_last_task_output.js +2 -0
  83. package/build/generated/update_input.js +2 -0
  84. package/build/generated/update_output.js +2 -0
  85. package/build/generated/vibe_pm_inspection_result.js +2 -0
  86. package/build/generated/vibe_pm_report_markdown.js +2 -0
  87. package/build/generated/vibe_pm_verdict.js +2 -0
  88. package/build/generated/vibe_repo_config.js +2 -0
  89. package/build/generated/vibecoding_helper_answer_output.js +2 -0
  90. package/build/generated/vibecoding_helper_one_loop_selection_output.js +2 -0
  91. package/build/generated/vibecoding_helper_show_ask_queue_output.js +2 -0
  92. package/build/generated/work_order_v1.js +2 -0
  93. package/build/generated/zoekt_evidence_input.js +2 -0
  94. package/build/generated/zoekt_evidence_output.js +2 -0
  95. package/build/index.js +111 -0
  96. package/build/legacy_alias.js +65 -0
  97. package/build/local-mode/bash.js +61 -0
  98. package/build/local-mode/config.js +171 -0
  99. package/build/local-mode/git.js +33 -0
  100. package/build/local-mode/init.js +110 -0
  101. package/build/local-mode/paths.js +24 -0
  102. package/build/local-mode/templates.js +856 -0
  103. package/build/local-mode/work-order.js +41 -0
  104. package/build/resources/index.js +246 -0
  105. package/build/security/input-validator.js +119 -0
  106. package/build/security/path-policy.js +289 -0
  107. package/build/security/sandbox.js +228 -0
  108. package/build/tools/react_perf/check_patterns.js +172 -0
  109. package/build/tools/react_perf/generate_report.js +337 -0
  110. package/build/tools/react_perf/index.js +119 -0
  111. package/build/tools/react_perf/rules/advanced.js +325 -0
  112. package/build/tools/react_perf/rules/async.js +104 -0
  113. package/build/tools/react_perf/rules/bundle.js +101 -0
  114. package/build/tools/react_perf/rules/client.js +186 -0
  115. package/build/tools/react_perf/rules/index.js +74 -0
  116. package/build/tools/react_perf/rules/js.js +148 -0
  117. package/build/tools/react_perf/rules/rendering.js +166 -0
  118. package/build/tools/react_perf/rules/rerender.js +161 -0
  119. package/build/tools/react_perf/rules/server.js +141 -0
  120. package/build/tools/react_perf/types.js +127 -0
  121. package/build/tools/vibe_pm/activate.js +102 -0
  122. package/build/tools/vibe_pm/advisory_review.js +77 -0
  123. package/build/tools/vibe_pm/briefing.js +178 -0
  124. package/build/tools/vibe_pm/context.js +439 -0
  125. package/build/tools/vibe_pm/create_work_order.js +271 -0
  126. package/build/tools/vibe_pm/doc_status_gate.js +370 -0
  127. package/build/tools/vibe_pm/doctor.js +262 -0
  128. package/build/tools/vibe_pm/entity_gate/preflight.js +78 -0
  129. package/build/tools/vibe_pm/export_output.js +135 -0
  130. package/build/tools/vibe_pm/finalize_work.js +393 -0
  131. package/build/tools/vibe_pm/gate.js +33 -0
  132. package/build/tools/vibe_pm/get_decision.js +281 -0
  133. package/build/tools/vibe_pm/index.js +593 -0
  134. package/build/tools/vibe_pm/inspect_code.js +828 -0
  135. package/build/tools/vibe_pm/intent/generator.js +294 -0
  136. package/build/tools/vibe_pm/intent/index.js +5 -0
  137. package/build/tools/vibe_pm/intent/prompt_density.js +227 -0
  138. package/build/tools/vibe_pm/intent/types.js +70 -0
  139. package/build/tools/vibe_pm/intent/verifier.js +237 -0
  140. package/build/tools/vibe_pm/kce/doc_usage.js +51 -0
  141. package/build/tools/vibe_pm/kce/on_finalize.js +11 -0
  142. package/build/tools/vibe_pm/kce/preflight.js +232 -0
  143. package/build/tools/vibe_pm/local_memory.js +26 -0
  144. package/build/tools/vibe_pm/memory_status.js +82 -0
  145. package/build/tools/vibe_pm/memory_sync.js +134 -0
  146. package/build/tools/vibe_pm/modules/decision_snapshot.js +29 -0
  147. package/build/tools/vibe_pm/modules/ensure.js +100 -0
  148. package/build/tools/vibe_pm/modules/fingerprint.js +30 -0
  149. package/build/tools/vibe_pm/modules/fix_dependencies.js +394 -0
  150. package/build/tools/vibe_pm/modules/planning_v1.js +110 -0
  151. package/build/tools/vibe_pm/modules/repo_context.js +56 -0
  152. package/build/tools/vibe_pm/modules/research_v1.js +114 -0
  153. package/build/tools/vibe_pm/modules/skills_v1.js +100 -0
  154. package/build/tools/vibe_pm/pm_language.js +222 -0
  155. package/build/tools/vibe_pm/repair_plan.js +199 -0
  156. package/build/tools/vibe_pm/run_app.js +597 -0
  157. package/build/tools/vibe_pm/run_app_podman.js +64 -0
  158. package/build/tools/vibe_pm/scaffold.js +550 -0
  159. package/build/tools/vibe_pm/search_oss.js +124 -0
  160. package/build/tools/vibe_pm/status.js +153 -0
  161. package/build/tools/vibe_pm/submit_decision.js +87 -0
  162. package/build/tools/vibe_pm/system_design/issue_mapping.js +47 -0
  163. package/build/tools/vibe_pm/system_design/rulebook.js +112 -0
  164. package/build/tools/vibe_pm/system_design/semgrep.js +132 -0
  165. package/build/tools/vibe_pm/types.js +229 -0
  166. package/build/tools/vibe_pm/undo_last_task.js +163 -0
  167. package/build/tools/vibe_pm/update.js +146 -0
  168. package/build/tools/vibe_pm/zoekt_evidence.js +96 -0
  169. package/build/tools.js +269 -0
  170. package/build/version-check.js +239 -0
  171. package/build/vibe-cli.js +631 -0
  172. package/package.json +76 -0
@@ -0,0 +1,178 @@
1
+ // adapters/mcp-ts/src/tools/vibe_pm/briefing.ts
2
+ // vibe_pm.briefing - Project intake and structuring
3
+ import { runEngine } from "../../engine.js";
4
+ import { safeJsonParse } from "../../cli.js";
5
+ import { generateRunId, resolveProjectId, ensureRunsDir, getRunsDir } from "./context.js";
6
+ import { validateBriefingInput } from "../../security/input-validator.js";
7
+ import { ensureResearchForBriefing } from "./modules/ensure.js";
8
+ import { memorySync } from "./memory_sync.js";
9
+ import { getAuthGate } from "../../auth/index.js";
10
+ import { SpecHighDecisionDraftOutputSchema } from "../../generated/spec_high_decision_draft_output.js";
11
+ import { applyPromptDensityHysteresis, computePromptDensity, loadLatestPromptDensityForProject, savePromptDensity } from "./intent/prompt_density.js";
12
+ import { generateIntentFromBriefing, saveIntentWithMarkdown } from "./intent/index.js";
13
+ /**
14
+ * vibe_pm.briefing - Project intake and structuring
15
+ *
16
+ * Internal mapping:
17
+ * → spec-high init <auto_run_id>
18
+ * → spec-high input-set <run_id> --text <project_brief>
19
+ * → spec-high decision-draft <run_id>
20
+ * → summary generation
21
+ */
22
+ export async function briefing(input) {
23
+ const basePath = process.cwd();
24
+ // Security: Validate input first
25
+ validateBriefingInput(input);
26
+ // Ensure runs directory exists
27
+ ensureRunsDir(basePath);
28
+ // Generate or use provided project_id/run_id
29
+ const project_id = input.project_id ?? resolveProjectId(undefined, basePath);
30
+ const run_id = generateRunId(project_id, basePath);
31
+ // Best-effort: keep Local Memory up-to-date (PRO only; do not block briefing on failure)
32
+ if (shouldKickoffAutoMemorySync()) {
33
+ void kickoffAutoMemorySyncProOnly(project_id);
34
+ }
35
+ // Mode defaults to balanced
36
+ const mode = input.mode ?? "balanced";
37
+ // Step 1: Initialize the run
38
+ const initResult = await runEngine("spec-high", ["--root", "engines/spec_high", "init", run_id, "--mode", mode], { timeoutMs: 60_000 });
39
+ if (initResult.code !== 0) {
40
+ throw new Error(`프로젝트 초기화 실패: ${initResult.stderr || "알 수 없는 오류"}`);
41
+ }
42
+ // Step 2: Set the project brief as input
43
+ const inputSetResult = await runEngine("spec-high", ["--root", "engines/spec_high", "input-set", run_id, "--text", input.project_brief], { timeoutMs: 60_000 });
44
+ if (inputSetResult.code !== 0) {
45
+ throw new Error(`프로젝트 설명 입력 실패: ${inputSetResult.stderr || "알 수 없는 오류"}`);
46
+ }
47
+ // Step 3: Generate decision draft (creates ask_queue)
48
+ let summary = {
49
+ goal: extractGoal(input.project_brief),
50
+ target_user: "일반 사용자",
51
+ first_milestone: `MVP: ${extractFirstFeature(input.project_brief)}`,
52
+ top_risk: "범위가 명확하지 않음"
53
+ };
54
+ try {
55
+ const draftResult = await runEngine("spec-high", ["--root", "engines/spec_high", "decision-draft", run_id], { timeoutMs: 120_000 });
56
+ if (draftResult.code === 0) {
57
+ const parsed = safeJsonParse(draftResult.stdout);
58
+ if (parsed.ok) {
59
+ const validated = SpecHighDecisionDraftOutputSchema.safeParse(parsed.value);
60
+ if (validated.success && validated.data.summary) {
61
+ summary = {
62
+ goal: validated.data.summary.goal ?? summary.goal,
63
+ target_user: validated.data.summary.target_user ?? summary.target_user,
64
+ first_milestone: validated.data.summary.first_milestone ?? summary.first_milestone,
65
+ top_risk: validated.data.summary.top_risk ?? summary.top_risk
66
+ };
67
+ }
68
+ }
69
+ }
70
+ }
71
+ catch {
72
+ // Use default summary if decision-draft fails
73
+ }
74
+ // Generate and save Intent document
75
+ try {
76
+ const runsDir = getRunsDir(basePath);
77
+ const intentData = {
78
+ runId: run_id,
79
+ projectName: extractGoal(input.project_brief),
80
+ goal: summary.goal,
81
+ targetUser: summary.target_user,
82
+ mode: mode,
83
+ firstMilestone: summary.first_milestone,
84
+ topRisk: summary.top_risk
85
+ };
86
+ const intent = generateIntentFromBriefing(intentData);
87
+ await saveIntentWithMarkdown(runsDir, intent);
88
+ }
89
+ catch {
90
+ // Intent generation is optional - continue if it fails
91
+ }
92
+ // Research module is best-effort (do not block briefing)
93
+ try {
94
+ ensureResearchForBriefing({ basePath, runId: run_id, projectBrief: input.project_brief });
95
+ }
96
+ catch {
97
+ // ignore
98
+ }
99
+ // Prompt density is best-effort (do not block briefing)
100
+ try {
101
+ const runsDir = getRunsDir(basePath);
102
+ const previous = loadLatestPromptDensityForProject(runsDir, project_id);
103
+ const current = computePromptDensity(input.project_brief, run_id);
104
+ const density = applyPromptDensityHysteresis({ current, previous });
105
+ savePromptDensity(runsDir, run_id, density);
106
+ }
107
+ catch {
108
+ // ignore
109
+ }
110
+ return {
111
+ project_id,
112
+ run_id,
113
+ pm_summary: summary,
114
+ next_action: {
115
+ tool: "vibe_pm.get_decision",
116
+ reason: "범위를 정하기 위해 결재가 필요합니다"
117
+ }
118
+ };
119
+ }
120
+ /**
121
+ * Extract project goal from brief text
122
+ */
123
+ function extractGoal(brief) {
124
+ // Take first sentence or up to 50 characters
125
+ const firstSentence = brief.split(/[.!?]/)[0].trim();
126
+ if (firstSentence.length <= 50) {
127
+ return firstSentence;
128
+ }
129
+ return firstSentence.slice(0, 47) + "...";
130
+ }
131
+ /**
132
+ * Extract first feature mention from brief
133
+ */
134
+ function extractFirstFeature(brief) {
135
+ // Look for common feature-related keywords
136
+ const featurePatterns = [
137
+ /(?:만들|구현|개발|추가).+?(?:기능|페이지|화면|버튼|폼)/,
138
+ /(.+?)\s*(?:앱|웹|사이트|서비스)/,
139
+ /MVP.+/
140
+ ];
141
+ for (const pattern of featurePatterns) {
142
+ const match = brief.match(pattern);
143
+ if (match) {
144
+ const feature = match[0].trim();
145
+ if (feature.length <= 30) {
146
+ return feature;
147
+ }
148
+ return feature.slice(0, 27) + "...";
149
+ }
150
+ }
151
+ // Default: first few words
152
+ const words = brief.split(/\s+/).slice(0, 5);
153
+ return words.join(" ") + " 구현";
154
+ }
155
+ function shouldKickoffAutoMemorySync() {
156
+ const raw = (process.env.VIBECODE_AUTOMEMORY ?? "").trim().toLowerCase();
157
+ if (raw && (raw === "0" || raw === "false" || raw === "no" || raw === "off"))
158
+ return false;
159
+ const nodeEnv = (process.env.NODE_ENV ?? "").trim().toLowerCase();
160
+ if (nodeEnv === "test")
161
+ return false;
162
+ const vitest = (process.env.VITEST ?? "").trim().toLowerCase();
163
+ if (vitest && vitest !== "0" && vitest !== "false" && vitest !== "no" && vitest !== "off")
164
+ return false;
165
+ return true;
166
+ }
167
+ async function kickoffAutoMemorySyncProOnly(project_id) {
168
+ try {
169
+ const gate = getAuthGate();
170
+ const allowed = await gate.check("local_memory_pro_tools");
171
+ if (!allowed.allowed)
172
+ return;
173
+ await memorySync({ project_id, docs_root: "docs", mode: "on_demand", force_full: false });
174
+ }
175
+ catch {
176
+ // ignore (best-effort)
177
+ }
178
+ }
@@ -0,0 +1,439 @@
1
+ // adapters/mcp-ts/src/tools/vibe_pm/context.ts
2
+ // Run context auto-discovery utilities
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import { TieredCache, createCacheKey } from "../../cache/index.js";
6
+ import { validatePath } from "../../security/path-policy.js";
7
+ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
8
+ import { RunStateFileSchema } from "../../generated/run_state_file.js";
9
+ // ============================================================
10
+ // Constants
11
+ // ============================================================
12
+ const RUNS_DIR = "runs";
13
+ const ENGINES_DIR = "engines";
14
+ const SPEC_HIGH_DIRNAME = "spec_high";
15
+ const HANDOFF_DIR = "handoff";
16
+ const BRIDGE_FILENAME_YAML = "clinic_bridge.yaml";
17
+ const BRIDGE_FILENAME_JSON = "clinic_bridge.json";
18
+ const STATE_FILENAME = "state.json";
19
+ // ============================================================
20
+ // Caches
21
+ // ============================================================
22
+ /**
23
+ * Cache for run state (state.json)
24
+ * TTL: 5 minutes, max 20 entries
25
+ * Validates against file mtime
26
+ */
27
+ const stateCache = new TieredCache({
28
+ ttlMs: 300_000,
29
+ maxEntries: 20,
30
+ name: "run-state",
31
+ });
32
+ /**
33
+ * Cache for bridge path resolution
34
+ * TTL: 5 minutes, max 30 entries
35
+ */
36
+ const bridgePathCache = new TieredCache({
37
+ ttlMs: 300_000,
38
+ maxEntries: 30,
39
+ name: "bridge-path",
40
+ });
41
+ /**
42
+ * Cache for run context
43
+ * TTL: 15 seconds, max 20 entries
44
+ */
45
+ const runContextCache = new TieredCache({
46
+ ttlMs: 15_000,
47
+ maxEntries: 20,
48
+ name: "run-context",
49
+ });
50
+ function normalizeRunState(raw) {
51
+ const parsed = RunStateFileSchema.safeParse(raw);
52
+ if (!parsed.success)
53
+ return null;
54
+ const value = parsed.data;
55
+ const out = {};
56
+ if (typeof value?.phase === "string")
57
+ out.phase = value.phase;
58
+ if (typeof value?.mode === "string")
59
+ out.mode = value.mode;
60
+ if (typeof value?.created_at === "string")
61
+ out.created_at = value.created_at;
62
+ if (typeof value?.decisions_made === "number")
63
+ out.decisions_made = value.decisions_made;
64
+ if (typeof value?.decisions_pending === "number")
65
+ out.decisions_pending = value.decisions_pending;
66
+ if (typeof value?.last_review_status === "string")
67
+ out.last_review_status = value.last_review_status;
68
+ // Spec-High state.json (v1) doesn't expose decisions_made/pending directly.
69
+ // Derive stable counts from required_slots when present.
70
+ if ((out.decisions_made === undefined || out.decisions_pending === undefined) &&
71
+ value?.required_slots &&
72
+ typeof value.required_slots === "object") {
73
+ const slots = Object.values(value.required_slots);
74
+ const filled = slots.filter((s) => s === "filled").length;
75
+ const missing = slots.filter((s) => s === "missing").length;
76
+ if (out.decisions_made === undefined)
77
+ out.decisions_made = filled;
78
+ if (out.decisions_pending === undefined)
79
+ out.decisions_pending = missing;
80
+ }
81
+ return out;
82
+ }
83
+ // ============================================================
84
+ // Auto-Discovery Functions
85
+ // ============================================================
86
+ /**
87
+ * Get the runs directory path
88
+ */
89
+ export function getRunsDir(basePath = process.cwd()) {
90
+ return path.join(basePath, RUNS_DIR);
91
+ }
92
+ /**
93
+ * Get Spec-High runs directory path (preferred v1 run root)
94
+ */
95
+ export function getSpecHighRunsDir(basePath = process.cwd()) {
96
+ return path.join(basePath, ENGINES_DIR, SPEC_HIGH_DIRNAME, RUNS_DIR);
97
+ }
98
+ /**
99
+ * Resolve Spec-High run directory with v1 priority:
100
+ * 1) engines/spec_high/runs/examples/<run_id>
101
+ * 2) engines/spec_high/runs/fixtures/<run_id>
102
+ * 3) engines/spec_high/runs/<run_id>
103
+ *
104
+ * Returns null if not found.
105
+ */
106
+ export function resolveSpecHighRunDir(runId, basePath = process.cwd()) {
107
+ assertSafeId(runId, "run_id");
108
+ const specRuns = getSpecHighRunsDir(basePath);
109
+ const candidates = [
110
+ ["spec_high_examples", path.join(specRuns, "examples", runId)],
111
+ ["spec_high_fixtures", path.join(specRuns, "fixtures", runId)],
112
+ ["spec_high_legacy", path.join(specRuns, runId)],
113
+ ];
114
+ for (const [source, p] of candidates) {
115
+ if (fs.existsSync(p) && fs.statSync(p).isDirectory()) {
116
+ return { run_id: runId, run_dir: p, source };
117
+ }
118
+ }
119
+ return null;
120
+ }
121
+ /**
122
+ * Resolve run directory (preferred Spec-High run dir, fallback legacy runs/<run_id>)
123
+ */
124
+ export function resolveRunDir(runId, basePath = process.cwd()) {
125
+ const resolved = resolveSpecHighRunDir(runId, basePath);
126
+ if (resolved)
127
+ return resolved;
128
+ const legacy = path.join(getRunsDir(basePath), runId);
129
+ if (fs.existsSync(legacy) && fs.statSync(legacy).isDirectory()) {
130
+ return { run_id: runId, run_dir: legacy, source: "legacy" };
131
+ }
132
+ return null;
133
+ }
134
+ /**
135
+ * List all available run IDs
136
+ */
137
+ export function listRunIds(basePath = process.cwd()) {
138
+ const out = new Set();
139
+ const specRunsDir = getSpecHighRunsDir(basePath);
140
+ if (fs.existsSync(specRunsDir)) {
141
+ try {
142
+ const entries = fs.readdirSync(specRunsDir, { withFileTypes: true });
143
+ for (const e of entries) {
144
+ if (!e.isDirectory())
145
+ continue;
146
+ if (e.name === "examples" || e.name === "fixtures")
147
+ continue;
148
+ out.add(e.name);
149
+ }
150
+ }
151
+ catch {
152
+ // ignore
153
+ }
154
+ }
155
+ const runsDir = getRunsDir(basePath);
156
+ if (fs.existsSync(runsDir)) {
157
+ try {
158
+ const entries = fs.readdirSync(runsDir, { withFileTypes: true });
159
+ for (const e of entries) {
160
+ if (!e.isDirectory())
161
+ continue;
162
+ out.add(e.name);
163
+ }
164
+ }
165
+ catch {
166
+ // ignore
167
+ }
168
+ }
169
+ return Array.from(out.values()).sort();
170
+ }
171
+ /**
172
+ * Find the most recent run_id based on directory modification time
173
+ */
174
+ export function findLatestRunId(basePath = process.cwd()) {
175
+ const runIds = listRunIds(basePath);
176
+ if (runIds.length === 0) {
177
+ return null;
178
+ }
179
+ // Sort by modification time (most recent first), preferring Spec-High run roots
180
+ const sorted = runIds
181
+ .map((id) => {
182
+ const resolved = resolveRunDir(id, basePath);
183
+ const runPath = resolved?.run_dir ?? path.join(getRunsDir(basePath), id);
184
+ try {
185
+ const stat = fs.statSync(runPath);
186
+ return { id, mtime: stat.mtime.getTime() };
187
+ }
188
+ catch {
189
+ return { id, mtime: 0 };
190
+ }
191
+ })
192
+ .sort((a, b) => b.mtime - a.mtime);
193
+ return sorted[0]?.id ?? null;
194
+ }
195
+ /**
196
+ * Generate a new run_id based on project folder name and timestamp
197
+ */
198
+ export function generateRunId(projectId, basePath = process.cwd()) {
199
+ const base = projectId ?? path.basename(basePath);
200
+ const timestamp = new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14);
201
+ return `${sanitizeForId(base)}_${timestamp}`;
202
+ }
203
+ /**
204
+ * Sanitize a string for use as an ID
205
+ */
206
+ function sanitizeForId(str) {
207
+ return str
208
+ .toLowerCase()
209
+ .replace(/[^a-z0-9_-]/g, "_")
210
+ .replace(/_+/g, "_")
211
+ .replace(/^_|_$/g, "")
212
+ .slice(0, 32);
213
+ }
214
+ function assertSafeId(value, field) {
215
+ if (!value || value.trim().length === 0) {
216
+ throw new McpError(ErrorCode.InvalidParams, `[SECURITY] ${field} is required`, {
217
+ vibeCode: -32_064,
218
+ category: "SECURITY",
219
+ context: { field },
220
+ });
221
+ }
222
+ if (value.includes("..") || value.includes("/") || value.includes("\\") || value.includes("\0")) {
223
+ throw new McpError(ErrorCode.InvalidParams, `[SECURITY] Invalid ${field}: path traversal detected`, {
224
+ vibeCode: -32_065,
225
+ category: "SECURITY",
226
+ context: { field },
227
+ });
228
+ }
229
+ }
230
+ /**
231
+ * Resolve run_id: use provided value, or find latest, or generate new
232
+ */
233
+ export function resolveRunId(providedId, basePath = process.cwd()) {
234
+ if (providedId) {
235
+ assertSafeId(providedId, "run_id");
236
+ return { run_id: providedId, is_new: false };
237
+ }
238
+ const latestId = findLatestRunId(basePath);
239
+ if (latestId) {
240
+ return { run_id: latestId, is_new: false };
241
+ }
242
+ return { run_id: generateRunId(undefined, basePath), is_new: true };
243
+ }
244
+ /**
245
+ * Resolve project_id from run_id or folder name
246
+ */
247
+ export function resolveProjectId(runId, basePath = process.cwd()) {
248
+ if (runId) {
249
+ assertSafeId(runId, "run_id");
250
+ // Extract project name from run_id (before the timestamp)
251
+ const parts = runId.split("_");
252
+ if (parts.length > 1) {
253
+ // Remove the timestamp part (last part if it looks like a timestamp)
254
+ const lastPart = parts[parts.length - 1];
255
+ if (/^\d{14}$/.test(lastPart)) {
256
+ return parts.slice(0, -1).join("_");
257
+ }
258
+ }
259
+ return runId;
260
+ }
261
+ return sanitizeForId(path.basename(basePath));
262
+ }
263
+ // ============================================================
264
+ // Run Context Functions
265
+ // ============================================================
266
+ /**
267
+ * Get full context for a run (with caching)
268
+ */
269
+ export function getRunContext(runId, basePath = process.cwd()) {
270
+ const cacheKey = createCacheKey("run-context", runId, basePath);
271
+ // Check cache first
272
+ const cached = runContextCache.get(cacheKey);
273
+ if (cached) {
274
+ return cached;
275
+ }
276
+ const projectId = resolveProjectId(runId, basePath);
277
+ const resolved = resolveRunDir(runId, basePath);
278
+ const runsPath = resolved?.run_dir ?? path.join(basePath, RUNS_DIR, runId);
279
+ const handoffPath = path.join(runsPath, HANDOFF_DIR);
280
+ // Find bridge file (prefer YAML)
281
+ let bridgePath = null;
282
+ const yamlBridge = path.join(handoffPath, BRIDGE_FILENAME_YAML);
283
+ const jsonBridge = path.join(handoffPath, BRIDGE_FILENAME_JSON);
284
+ if (fs.existsSync(yamlBridge)) {
285
+ bridgePath = yamlBridge;
286
+ }
287
+ else if (fs.existsSync(jsonBridge)) {
288
+ bridgePath = jsonBridge;
289
+ }
290
+ // Find state file
291
+ const statePath = path.join(runsPath, STATE_FILENAME);
292
+ const stateExists = fs.existsSync(statePath);
293
+ const context = {
294
+ run_id: runId,
295
+ project_id: projectId,
296
+ runs_path: runsPath,
297
+ handoff_path: handoffPath,
298
+ bridge_path: bridgePath,
299
+ state_path: stateExists ? statePath : null
300
+ };
301
+ // Cache the result
302
+ runContextCache.set(cacheKey, context);
303
+ return context;
304
+ }
305
+ /**
306
+ * Read run state from state.json (with mtime-based caching)
307
+ */
308
+ export function readRunState(runId, basePath = process.cwd()) {
309
+ const context = getRunContext(runId, basePath);
310
+ if (!context.state_path) {
311
+ return null;
312
+ }
313
+ const cacheKey = createCacheKey("state", runId, basePath);
314
+ const statePath = context.state_path;
315
+ // Create mtime getter for validation
316
+ const getMtime = () => {
317
+ try {
318
+ return fs.statSync(statePath).mtime.getTime();
319
+ }
320
+ catch {
321
+ return null;
322
+ }
323
+ };
324
+ // Check cache with mtime validation
325
+ const cached = stateCache.get(cacheKey, getMtime);
326
+ if (cached) {
327
+ return cached;
328
+ }
329
+ try {
330
+ const content = fs.readFileSync(statePath, "utf-8");
331
+ const raw = JSON.parse(content);
332
+ const state = normalizeRunState(raw);
333
+ if (!state)
334
+ return null;
335
+ const mtime = getMtime();
336
+ // Cache with mtime
337
+ if (mtime !== null) {
338
+ stateCache.set(cacheKey, state, mtime);
339
+ }
340
+ return state;
341
+ }
342
+ catch {
343
+ return null;
344
+ }
345
+ }
346
+ /**
347
+ * Check if a run exists
348
+ */
349
+ export function runExists(runId, basePath = process.cwd()) {
350
+ return resolveRunDir(runId, basePath) !== null;
351
+ }
352
+ /**
353
+ * Ensure runs directory exists
354
+ */
355
+ export function ensureRunsDir(basePath = process.cwd()) {
356
+ const runsDir = getRunsDir(basePath);
357
+ if (!fs.existsSync(runsDir)) {
358
+ fs.mkdirSync(runsDir, { recursive: true });
359
+ }
360
+ }
361
+ // ============================================================
362
+ // Bridge Auto-Discovery
363
+ // ============================================================
364
+ /**
365
+ * Find the latest bridge file across all runs
366
+ */
367
+ export function findLatestBridge(basePath = process.cwd()) {
368
+ const runIds = listRunIds(basePath);
369
+ let latestBridge = null;
370
+ for (const runId of runIds) {
371
+ const context = getRunContext(runId, basePath);
372
+ if (context.bridge_path) {
373
+ try {
374
+ const stat = fs.statSync(context.bridge_path);
375
+ if (!latestBridge || stat.mtime.getTime() > latestBridge.mtime) {
376
+ latestBridge = { path: context.bridge_path, mtime: stat.mtime.getTime() };
377
+ }
378
+ }
379
+ catch {
380
+ // Skip if stat fails
381
+ }
382
+ }
383
+ }
384
+ return latestBridge?.path ?? null;
385
+ }
386
+ /**
387
+ * Resolve bridge path: use provided value, find from run_id, or find latest (with caching)
388
+ */
389
+ export function resolveBridgePath(providedPath, runId, basePath = process.cwd()) {
390
+ if (providedPath) {
391
+ return validatePath(providedPath, basePath);
392
+ }
393
+ // Check cache for run_id-based lookup
394
+ if (runId) {
395
+ const cacheKey = createCacheKey("bridge-path", runId, basePath);
396
+ const cached = bridgePathCache.get(cacheKey);
397
+ if (cached !== null) {
398
+ return cached;
399
+ }
400
+ const context = getRunContext(runId, basePath);
401
+ if (context.bridge_path) {
402
+ bridgePathCache.set(cacheKey, context.bridge_path);
403
+ return context.bridge_path;
404
+ }
405
+ }
406
+ return findLatestBridge(basePath);
407
+ }
408
+ // ============================================================
409
+ // Cache Invalidation
410
+ // ============================================================
411
+ /**
412
+ * Invalidate all state-related caches for a run
413
+ */
414
+ export function invalidateStateCache(runId, basePath) {
415
+ if (runId && basePath) {
416
+ const stateKey = createCacheKey("state", runId, basePath);
417
+ const contextKey = createCacheKey("run-context", runId, basePath);
418
+ const bridgeKey = createCacheKey("bridge-path", runId, basePath);
419
+ stateCache.invalidate(stateKey);
420
+ runContextCache.invalidate(contextKey);
421
+ bridgePathCache.invalidate(bridgeKey);
422
+ }
423
+ else {
424
+ // Clear all caches
425
+ stateCache.clear();
426
+ runContextCache.clear();
427
+ bridgePathCache.clear();
428
+ }
429
+ }
430
+ /**
431
+ * Get cache statistics for debugging
432
+ */
433
+ export function getContextCacheStats() {
434
+ return {
435
+ state: stateCache.stats(),
436
+ context: runContextCache.stats(),
437
+ bridge: bridgePathCache.stats(),
438
+ };
439
+ }