@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.
- package/LICENSE +21 -0
- package/README.md +269 -0
- package/build/auth/gate.js +225 -0
- package/build/auth/index.js +55 -0
- package/build/auth/public_key.js +27 -0
- package/build/auth/token_cache.js +122 -0
- package/build/auth/token_verifier.js +103 -0
- package/build/bootstrap/doctor.js +115 -0
- package/build/bootstrap/installer.js +673 -0
- package/build/bootstrap/lock.js +37 -0
- package/build/bootstrap/platform.js +26 -0
- package/build/bootstrap/registry.js +37 -0
- package/build/cache/index.js +147 -0
- package/build/cli.js +101 -0
- package/build/contracts.js +22 -0
- package/build/control_plane/gate.js +161 -0
- package/build/control_plane/index.js +6 -0
- package/build/dx/activity.js +139 -0
- package/build/engine.js +106 -0
- package/build/errors.js +171 -0
- package/build/generated/activate_input.js +2 -0
- package/build/generated/activate_output.js +57 -0
- package/build/generated/advisory_review_input.js +2 -0
- package/build/generated/advisory_review_output.js +35 -0
- package/build/generated/auth_token_file.js +2 -0
- package/build/generated/briefing_input.js +2 -0
- package/build/generated/briefing_output.js +2 -0
- package/build/generated/clinic_bridge_file.js +13 -0
- package/build/generated/contracts_bundle_info.js +5 -0
- package/build/generated/create_work_order_input.js +2 -0
- package/build/generated/create_work_order_output.js +2 -0
- package/build/generated/current_work_order_file.js +2 -0
- package/build/generated/doctor_input.js +2 -0
- package/build/generated/doctor_output.js +24 -0
- package/build/generated/execution_result.js +2 -0
- package/build/generated/execution_task.js +2 -0
- package/build/generated/export_output_input.js +2 -0
- package/build/generated/export_output_output.js +2 -0
- package/build/generated/finalize_work_input.js +2 -0
- package/build/generated/finalize_work_output.js +2 -0
- package/build/generated/gate_input.js +2 -0
- package/build/generated/gate_output.js +2 -0
- package/build/generated/gate_result_v1.js +2 -0
- package/build/generated/get_decision_input.js +2 -0
- package/build/generated/get_decision_output.js +13 -0
- package/build/generated/handoff_to_clinic.js +2 -0
- package/build/generated/index.js +75 -0
- package/build/generated/inspect_code_input.js +2 -0
- package/build/generated/inspect_code_output.js +13 -0
- package/build/generated/memory_retrieve_output.js +2 -0
- package/build/generated/memory_state_file.js +2 -0
- package/build/generated/memory_status_input.js +2 -0
- package/build/generated/memory_status_output.js +13 -0
- package/build/generated/memory_sync_input.js +2 -0
- package/build/generated/memory_sync_output.js +13 -0
- package/build/generated/plugin_result.js +2 -0
- package/build/generated/react_perf_check_patterns_input.js +2 -0
- package/build/generated/react_perf_check_patterns_output.js +2 -0
- package/build/generated/react_perf_generate_report_input.js +2 -0
- package/build/generated/react_perf_generate_report_output.js +2 -0
- package/build/generated/repair_plan_input.js +2 -0
- package/build/generated/repair_plan_output.js +2 -0
- package/build/generated/run_app_input.js +2 -0
- package/build/generated/run_app_output.js +2 -0
- package/build/generated/run_state_file.js +13 -0
- package/build/generated/scaffold_input.js +2 -0
- package/build/generated/scaffold_output.js +2 -0
- package/build/generated/search_oss_input.js +2 -0
- package/build/generated/search_oss_output.js +2 -0
- package/build/generated/selection_validation_result.js +2 -0
- package/build/generated/signal_agent_input.js +2 -0
- package/build/generated/spec_high_ask_queue_items_file.js +2 -0
- package/build/generated/spec_high_clinic_bridge_output.js +2 -0
- package/build/generated/spec_high_decision_draft_output.js +2 -0
- package/build/generated/spec_high_validate_output.js +2 -0
- package/build/generated/status_input.js +2 -0
- package/build/generated/status_output.js +2 -0
- package/build/generated/submit_decision_input.js +2 -0
- package/build/generated/submit_decision_output.js +2 -0
- package/build/generated/tool_error_output.js +2 -0
- package/build/generated/undo_last_task_input.js +2 -0
- package/build/generated/undo_last_task_output.js +2 -0
- package/build/generated/update_input.js +2 -0
- package/build/generated/update_output.js +2 -0
- package/build/generated/vibe_pm_inspection_result.js +2 -0
- package/build/generated/vibe_pm_report_markdown.js +2 -0
- package/build/generated/vibe_pm_verdict.js +2 -0
- package/build/generated/vibe_repo_config.js +2 -0
- package/build/generated/vibecoding_helper_answer_output.js +2 -0
- package/build/generated/vibecoding_helper_one_loop_selection_output.js +2 -0
- package/build/generated/vibecoding_helper_show_ask_queue_output.js +2 -0
- package/build/generated/work_order_v1.js +2 -0
- package/build/generated/zoekt_evidence_input.js +2 -0
- package/build/generated/zoekt_evidence_output.js +2 -0
- package/build/index.js +111 -0
- package/build/legacy_alias.js +65 -0
- package/build/local-mode/bash.js +61 -0
- package/build/local-mode/config.js +171 -0
- package/build/local-mode/git.js +33 -0
- package/build/local-mode/init.js +110 -0
- package/build/local-mode/paths.js +24 -0
- package/build/local-mode/templates.js +856 -0
- package/build/local-mode/work-order.js +41 -0
- package/build/resources/index.js +246 -0
- package/build/security/input-validator.js +119 -0
- package/build/security/path-policy.js +289 -0
- package/build/security/sandbox.js +228 -0
- package/build/tools/react_perf/check_patterns.js +172 -0
- package/build/tools/react_perf/generate_report.js +337 -0
- package/build/tools/react_perf/index.js +119 -0
- package/build/tools/react_perf/rules/advanced.js +325 -0
- package/build/tools/react_perf/rules/async.js +104 -0
- package/build/tools/react_perf/rules/bundle.js +101 -0
- package/build/tools/react_perf/rules/client.js +186 -0
- package/build/tools/react_perf/rules/index.js +74 -0
- package/build/tools/react_perf/rules/js.js +148 -0
- package/build/tools/react_perf/rules/rendering.js +166 -0
- package/build/tools/react_perf/rules/rerender.js +161 -0
- package/build/tools/react_perf/rules/server.js +141 -0
- package/build/tools/react_perf/types.js +127 -0
- package/build/tools/vibe_pm/activate.js +102 -0
- package/build/tools/vibe_pm/advisory_review.js +77 -0
- package/build/tools/vibe_pm/briefing.js +178 -0
- package/build/tools/vibe_pm/context.js +439 -0
- package/build/tools/vibe_pm/create_work_order.js +271 -0
- package/build/tools/vibe_pm/doc_status_gate.js +370 -0
- package/build/tools/vibe_pm/doctor.js +262 -0
- package/build/tools/vibe_pm/entity_gate/preflight.js +78 -0
- package/build/tools/vibe_pm/export_output.js +135 -0
- package/build/tools/vibe_pm/finalize_work.js +393 -0
- package/build/tools/vibe_pm/gate.js +33 -0
- package/build/tools/vibe_pm/get_decision.js +281 -0
- package/build/tools/vibe_pm/index.js +593 -0
- package/build/tools/vibe_pm/inspect_code.js +828 -0
- package/build/tools/vibe_pm/intent/generator.js +294 -0
- package/build/tools/vibe_pm/intent/index.js +5 -0
- package/build/tools/vibe_pm/intent/prompt_density.js +227 -0
- package/build/tools/vibe_pm/intent/types.js +70 -0
- package/build/tools/vibe_pm/intent/verifier.js +237 -0
- package/build/tools/vibe_pm/kce/doc_usage.js +51 -0
- package/build/tools/vibe_pm/kce/on_finalize.js +11 -0
- package/build/tools/vibe_pm/kce/preflight.js +232 -0
- package/build/tools/vibe_pm/local_memory.js +26 -0
- package/build/tools/vibe_pm/memory_status.js +82 -0
- package/build/tools/vibe_pm/memory_sync.js +134 -0
- package/build/tools/vibe_pm/modules/decision_snapshot.js +29 -0
- package/build/tools/vibe_pm/modules/ensure.js +100 -0
- package/build/tools/vibe_pm/modules/fingerprint.js +30 -0
- package/build/tools/vibe_pm/modules/fix_dependencies.js +394 -0
- package/build/tools/vibe_pm/modules/planning_v1.js +110 -0
- package/build/tools/vibe_pm/modules/repo_context.js +56 -0
- package/build/tools/vibe_pm/modules/research_v1.js +114 -0
- package/build/tools/vibe_pm/modules/skills_v1.js +100 -0
- package/build/tools/vibe_pm/pm_language.js +222 -0
- package/build/tools/vibe_pm/repair_plan.js +199 -0
- package/build/tools/vibe_pm/run_app.js +597 -0
- package/build/tools/vibe_pm/run_app_podman.js +64 -0
- package/build/tools/vibe_pm/scaffold.js +550 -0
- package/build/tools/vibe_pm/search_oss.js +124 -0
- package/build/tools/vibe_pm/status.js +153 -0
- package/build/tools/vibe_pm/submit_decision.js +87 -0
- package/build/tools/vibe_pm/system_design/issue_mapping.js +47 -0
- package/build/tools/vibe_pm/system_design/rulebook.js +112 -0
- package/build/tools/vibe_pm/system_design/semgrep.js +132 -0
- package/build/tools/vibe_pm/types.js +229 -0
- package/build/tools/vibe_pm/undo_last_task.js +163 -0
- package/build/tools/vibe_pm/update.js +146 -0
- package/build/tools/vibe_pm/zoekt_evidence.js +96 -0
- package/build/tools.js +269 -0
- package/build/version-check.js +239 -0
- package/build/vibe-cli.js +631 -0
- 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
|
+
}
|