@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,271 @@
|
|
|
1
|
+
// adapters/mcp-ts/src/tools/vibe_pm/create_work_order.ts
|
|
2
|
+
// vibe_pm.create_work_order - Issue work order for implementation
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { parse as parseYaml } from "yaml";
|
|
6
|
+
import { runEngine, invalidateEngineCache } from "../../engine.js";
|
|
7
|
+
import { safeJsonParse } from "../../cli.js";
|
|
8
|
+
import { resolveRunId, resolveProjectId, getRunContext, readRunState, invalidateStateCache, getRunsDir } from "./context.js";
|
|
9
|
+
import { generateWorkOrderText, transformAskQueueText } from "./pm_language.js";
|
|
10
|
+
import { validateToolInput } from "../../security/input-validator.js";
|
|
11
|
+
import { checkGate, formatViolations } from "../../control_plane/gate.js";
|
|
12
|
+
import { ensureModulesForWorkOrder, readFileIfExists } from "./modules/ensure.js";
|
|
13
|
+
import { ClinicBridgeFileSchema } from "../../generated/clinic_bridge_file.js";
|
|
14
|
+
import { loadPromptDensity } from "./intent/prompt_density.js";
|
|
15
|
+
import { runKceRetrieve, runKceStatus } from "./kce/preflight.js";
|
|
16
|
+
import { updateKceDocUsage } from "./kce/doc_usage.js";
|
|
17
|
+
/**
|
|
18
|
+
* vibe_pm.create_work_order - Issue work order for implementation
|
|
19
|
+
*
|
|
20
|
+
* Internal mapping:
|
|
21
|
+
* → spec-high clinic-bridge <run_id> --write --format yaml
|
|
22
|
+
* → paste_to_agent text generation
|
|
23
|
+
*/
|
|
24
|
+
export async function createWorkOrder(input) {
|
|
25
|
+
const basePath = process.cwd();
|
|
26
|
+
// Security: Validate input first
|
|
27
|
+
validateToolInput({
|
|
28
|
+
project_id: input.project_id,
|
|
29
|
+
additional_instructions: input.additional_instructions,
|
|
30
|
+
});
|
|
31
|
+
// Resolve run_id
|
|
32
|
+
const { run_id } = resolveRunId(input.project_id, basePath);
|
|
33
|
+
const project_id = resolveProjectId(run_id, basePath);
|
|
34
|
+
const context = getRunContext(run_id, basePath);
|
|
35
|
+
const runState = readRunState(run_id, basePath);
|
|
36
|
+
const promptDensityMode = loadPromptDensity(getRunsDir(basePath), run_id)?.mode ?? "mode_0";
|
|
37
|
+
// Generate clinic bridge
|
|
38
|
+
const bridgeResult = await runEngine("spec-high", ["--root", "engines/spec_high", "clinic-bridge", run_id, "--write", "--format", "yaml"], { timeoutMs: 120_000 });
|
|
39
|
+
if (bridgeResult.code !== 0) {
|
|
40
|
+
throw new Error(`작업 지시서 생성 실패: ${bridgeResult.stderr || "알 수 없는 오류"}`);
|
|
41
|
+
}
|
|
42
|
+
// Generate intent documents from resolved decision cards
|
|
43
|
+
try {
|
|
44
|
+
await runEngine("spec-high", ["--root", "engines/spec_high", "intent-generate", run_id, "--write", "--force"], { timeoutMs: 60_000 });
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Intent generation is optional - continue if it fails
|
|
48
|
+
}
|
|
49
|
+
// Invalidate caches after state mutation
|
|
50
|
+
invalidateEngineCache(new RegExp(run_id));
|
|
51
|
+
invalidateStateCache(run_id, basePath);
|
|
52
|
+
// Get bridge path from response or use default
|
|
53
|
+
let bridgePath = `engines/spec_high/runs/${run_id}/handoff/clinic_bridge.yaml`;
|
|
54
|
+
const bridgeParsed = safeJsonParse(bridgeResult.stdout);
|
|
55
|
+
if (bridgeParsed.ok && bridgeParsed.value.bridge_path) {
|
|
56
|
+
bridgePath = bridgeParsed.value.bridge_path;
|
|
57
|
+
}
|
|
58
|
+
// Read and parse bridge file
|
|
59
|
+
const fullBridgePath = path.isAbsolute(bridgePath)
|
|
60
|
+
? bridgePath
|
|
61
|
+
: path.join(basePath, bridgePath);
|
|
62
|
+
let bridgeData = {};
|
|
63
|
+
try {
|
|
64
|
+
if (fs.existsSync(fullBridgePath)) {
|
|
65
|
+
const content = fs.readFileSync(fullBridgePath, "utf-8");
|
|
66
|
+
bridgeData = fullBridgePath.endsWith(".json") ? JSON.parse(content) : parseYaml(content);
|
|
67
|
+
const validated = ClinicBridgeFileSchema.safeParse(bridgeData);
|
|
68
|
+
if (!validated.success) {
|
|
69
|
+
throw new Error("작업 지시서 원본 파일 형식이 예상과 다릅니다. 업데이트 후 다시 시도해주세요.");
|
|
70
|
+
}
|
|
71
|
+
bridgeData = validated.data;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
// Fail closed (contract drift): do not issue a work order from an unknown/unsafe file shape.
|
|
76
|
+
throw e instanceof Error
|
|
77
|
+
? e
|
|
78
|
+
: new Error("작업 지시서 원본 파일을 읽을 수 없습니다. 업데이트 후 다시 시도해주세요.");
|
|
79
|
+
}
|
|
80
|
+
// Extract data from bridge
|
|
81
|
+
const projectName = project_id;
|
|
82
|
+
const mode = runState?.mode ?? "balanced";
|
|
83
|
+
const headline = "프로젝트 구현";
|
|
84
|
+
const dg = bridgeData?.decision_guard;
|
|
85
|
+
const legacyScope = bridgeData?.scope;
|
|
86
|
+
const scopeInclude = Array.isArray(dg?.allow_paths)
|
|
87
|
+
? dg.allow_paths
|
|
88
|
+
: Array.isArray(legacyScope?.include)
|
|
89
|
+
? legacyScope.include
|
|
90
|
+
: [];
|
|
91
|
+
const scopeExclude = Array.isArray(dg?.deny_paths)
|
|
92
|
+
? dg.deny_paths
|
|
93
|
+
: Array.isArray(legacyScope?.exclude)
|
|
94
|
+
? legacyScope.exclude
|
|
95
|
+
: [];
|
|
96
|
+
const doNotTouch = Array.isArray(dg?.read_only_paths)
|
|
97
|
+
? dg.read_only_paths
|
|
98
|
+
: Array.isArray(bridgeData?.do_not_touch)
|
|
99
|
+
? bridgeData.do_not_touch
|
|
100
|
+
: [];
|
|
101
|
+
const verifyCriteria = Array.isArray(bridgeData?.verification?.required)
|
|
102
|
+
? bridgeData.verification.required
|
|
103
|
+
.map((c) => (typeof c?.cmd === "string" ? c.cmd.trim() : ""))
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
: Array.isArray(bridgeData?.verify_criteria)
|
|
106
|
+
? bridgeData.verify_criteria.filter((s) => typeof s === "string" && s.trim())
|
|
107
|
+
: [];
|
|
108
|
+
// Generate paste_to_agent text
|
|
109
|
+
let pasteToAgent = generateWorkOrderText({
|
|
110
|
+
projectName,
|
|
111
|
+
mode,
|
|
112
|
+
headline,
|
|
113
|
+
includes: scopeInclude,
|
|
114
|
+
excludes: scopeExclude,
|
|
115
|
+
doNotTouch,
|
|
116
|
+
verifyCriteria
|
|
117
|
+
});
|
|
118
|
+
if (input.additional_instructions && input.additional_instructions.trim()) {
|
|
119
|
+
pasteToAgent += `\n\n[추가 지시]\n- ${input.additional_instructions.trim()}\n`;
|
|
120
|
+
}
|
|
121
|
+
// Ensure versioned modules (Research/Skills/Planning) exist and link them to output
|
|
122
|
+
const modules = ensureModulesForWorkOrder({ basePath, runId: run_id });
|
|
123
|
+
// Append SKILLS_PACK / PLAN_STEPS into paste_to_agent (still user-friendly, no engine terms)
|
|
124
|
+
const skillsPack = readFileIfExists(modules.skill_bundle_prompt_path);
|
|
125
|
+
if (skillsPack) {
|
|
126
|
+
pasteToAgent += `\n\n[SKILLS_PACK]\n${skillsPack.trim()}\n`;
|
|
127
|
+
}
|
|
128
|
+
const planRaw = readFileIfExists(modules.planning.path);
|
|
129
|
+
if (planRaw) {
|
|
130
|
+
const planSummary = formatPlanSteps(planRaw);
|
|
131
|
+
if (planSummary) {
|
|
132
|
+
pasteToAgent += `\n\n[PLAN_STEPS]\n${planSummary.trim()}\n`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Attach KCE references (best-effort; unless prompt density explicitly disables it)
|
|
136
|
+
if (promptDensityMode !== "mode_2") {
|
|
137
|
+
const query = (input.additional_instructions ?? "").trim() ||
|
|
138
|
+
`${projectName}\n${headline}\n${verifyCriteria.slice(0, 3).join("\n")}`.trim();
|
|
139
|
+
try {
|
|
140
|
+
// Free policy: only inject when KCE is READY. Never auto-heal (no sync) from core tools.
|
|
141
|
+
const status = await runKceStatus();
|
|
142
|
+
if (status.readiness_state !== "READY") {
|
|
143
|
+
throw new Error("KCE_NOT_READY");
|
|
144
|
+
}
|
|
145
|
+
const out = await runKceRetrieve({ query, maxItems: 3, limit: 10, maxTotalChars: 2000, maxChunkChars: 800 });
|
|
146
|
+
const refsRaw = out.context_block.trim();
|
|
147
|
+
if (refsRaw) {
|
|
148
|
+
const sanitized = transformAskQueueText(refsRaw);
|
|
149
|
+
const clipped = sanitized.length > 2000 ? `${sanitized.slice(0, 2000).trimEnd()}…` : sanitized;
|
|
150
|
+
pasteToAgent += `\n\n[참고 문헌]\n${clipped}\n`;
|
|
151
|
+
}
|
|
152
|
+
// Evidence (best-effort): record what was injected
|
|
153
|
+
try {
|
|
154
|
+
const kceDir = path.join(context.runs_path, "kce");
|
|
155
|
+
fs.mkdirSync(kceDir, { recursive: true });
|
|
156
|
+
fs.writeFileSync(path.join(kceDir, "kb_query_evidence.json"), JSON.stringify({
|
|
157
|
+
version: "kce_query_evidence.v1",
|
|
158
|
+
created_at: new Date().toISOString(),
|
|
159
|
+
query,
|
|
160
|
+
hits: out.hits,
|
|
161
|
+
kce_status: status
|
|
162
|
+
}, null, 2) + "\n", "utf-8");
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// ignore
|
|
166
|
+
}
|
|
167
|
+
// Evidence (best-effort): record doc usage for deterministic recency signals.
|
|
168
|
+
try {
|
|
169
|
+
const injectedDocs = (out.hits ?? [])
|
|
170
|
+
.map((h) => (typeof h.path === "string" ? h.path : ""))
|
|
171
|
+
.map((p) => p.replace(/\\\\/g, "/"))
|
|
172
|
+
.filter((p) => p.endsWith(".md") || p.endsWith(".mdx"));
|
|
173
|
+
const referencedEntities = injectedDocs
|
|
174
|
+
.filter((p) => p.startsWith("entities/") && p.endsWith(".entity.json"))
|
|
175
|
+
.map((p) => path.basename(p, ".entity.json"))
|
|
176
|
+
.filter(Boolean);
|
|
177
|
+
updateKceDocUsage({ basePath, run_id, injected_docs: injectedDocs, referenced_entities: referencedEntities });
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// ignore
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Best-effort: never block work order issuance when local memory is unavailable.
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Build work order
|
|
188
|
+
const workOrder = {
|
|
189
|
+
headline,
|
|
190
|
+
scope: {
|
|
191
|
+
include: scopeInclude,
|
|
192
|
+
exclude: scopeExclude
|
|
193
|
+
},
|
|
194
|
+
do_not_touch: doNotTouch,
|
|
195
|
+
verify_criteria: verifyCriteria,
|
|
196
|
+
paste_to_agent: pasteToAgent
|
|
197
|
+
};
|
|
198
|
+
// Run System Design Gate check
|
|
199
|
+
// Convert to WorkOrderV1 format for gate validation
|
|
200
|
+
const workOrderV1 = {
|
|
201
|
+
intent: `${headline}: ${pasteToAgent.slice(0, 100)}...`,
|
|
202
|
+
scope: {
|
|
203
|
+
include: scopeInclude.length > 0 ? scopeInclude : ["src/**/*"],
|
|
204
|
+
exclude: scopeExclude
|
|
205
|
+
},
|
|
206
|
+
tool_style: "EXEC",
|
|
207
|
+
idempotency_key: `${run_id}-${Date.now()}`
|
|
208
|
+
};
|
|
209
|
+
try {
|
|
210
|
+
const gateResult = await checkGate(workOrderV1, { repoRoot: basePath });
|
|
211
|
+
if (!gateResult.allowed && gateResult.result) {
|
|
212
|
+
const violations = formatViolations(gateResult.result);
|
|
213
|
+
throw new Error(`작업 지시서가 Gate 검사를 통과하지 못했습니다:\n${violations}`);
|
|
214
|
+
}
|
|
215
|
+
if (gateResult.error) {
|
|
216
|
+
// Log but don't block - gate check failure shouldn't prevent work order issuance
|
|
217
|
+
console.warn(`Gate check warning: ${gateResult.error}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (gateError) {
|
|
221
|
+
// If gate throws (not just returns blocked), log warning but continue
|
|
222
|
+
// This ensures backwards compatibility when Python CLI is not available
|
|
223
|
+
if (gateError instanceof Error && gateError.message.includes("Gate 검사")) {
|
|
224
|
+
throw gateError; // Re-throw actual gate violations
|
|
225
|
+
}
|
|
226
|
+
console.warn(`Gate check skipped: ${gateError instanceof Error ? gateError.message : String(gateError)}`);
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
project_id,
|
|
230
|
+
run_id,
|
|
231
|
+
work_order: workOrder,
|
|
232
|
+
paths: {
|
|
233
|
+
bridge_path: bridgePath,
|
|
234
|
+
intent_dir: `engines/spec_high/runs/${run_id}/intent`
|
|
235
|
+
},
|
|
236
|
+
modules: {
|
|
237
|
+
research: modules.research,
|
|
238
|
+
skills: modules.skills,
|
|
239
|
+
planning: modules.planning
|
|
240
|
+
},
|
|
241
|
+
next_action: {
|
|
242
|
+
tool: "vibe_pm.inspect_code",
|
|
243
|
+
reason: "구현 후 검수를 요청하세요",
|
|
244
|
+
when: "after_implementation"
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function formatPlanSteps(planJsonText) {
|
|
249
|
+
try {
|
|
250
|
+
const doc = JSON.parse(planJsonText);
|
|
251
|
+
const steps = doc.plan ?? [];
|
|
252
|
+
if (!Array.isArray(steps) || steps.length === 0)
|
|
253
|
+
return null;
|
|
254
|
+
const lines = [];
|
|
255
|
+
for (const s of steps) {
|
|
256
|
+
const n = s.step ?? 0;
|
|
257
|
+
const title = (s.title ?? "").trim();
|
|
258
|
+
const v = Array.isArray(s.verification) ? s.verification.filter(Boolean) : [];
|
|
259
|
+
if (title) {
|
|
260
|
+
lines.push(`${n}. ${title}`);
|
|
261
|
+
}
|
|
262
|
+
if (v.length > 0) {
|
|
263
|
+
lines.push(` - 확인: ${v[0]}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return lines.join("\n");
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
// adapters/mcp-ts/src/tools/vibe_pm/doc_status_gate.ts
|
|
2
|
+
// Internal: policy-based doc status tagging + safe archiving (no delete).
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { minimatch } from "minimatch";
|
|
6
|
+
function readJsonIfExists(p) {
|
|
7
|
+
try {
|
|
8
|
+
if (!fs.existsSync(p))
|
|
9
|
+
return null;
|
|
10
|
+
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function defaultPolicy() {
|
|
17
|
+
return {
|
|
18
|
+
version: "doc_status_policy.v1",
|
|
19
|
+
scope: { include_globs: ["docs/**/*.md", "docs/**/*.mdx"], exclude_globs: ["docs/archive/**"] },
|
|
20
|
+
signals: {
|
|
21
|
+
entity_link: { enabled: true, weight: 50, link_markers: ["<!-- ENTITY:", "entities/", "entity_id:"] },
|
|
22
|
+
evidence_link: { enabled: true, weight: 25, evidence_markers: ["runs/", "fixtures/", "schemas/"] },
|
|
23
|
+
recent_usage: { enabled: true, lookback_days: 30, weight: 25 }
|
|
24
|
+
},
|
|
25
|
+
classification: {
|
|
26
|
+
ACTIVE: { min_score: 70, required_any: ["entity_link", "evidence_link"] },
|
|
27
|
+
DORMANT: { min_score: 35, required_any: ["entity_link", "evidence_link"] },
|
|
28
|
+
ORPHAN: { max_score: 34, required_any: [], required_none: ["entity_link"] }
|
|
29
|
+
},
|
|
30
|
+
actions: {
|
|
31
|
+
tag_header: {
|
|
32
|
+
enabled: true,
|
|
33
|
+
format: "<!-- DOC_STATUS: {status} | score={score} | reason={reason} | last_seen={last_seen} -->"
|
|
34
|
+
},
|
|
35
|
+
archive: {
|
|
36
|
+
enabled: false,
|
|
37
|
+
only_status: ["DORMANT", "ORPHAN"],
|
|
38
|
+
grace_days: 14,
|
|
39
|
+
archive_root: "docs/archive",
|
|
40
|
+
archive_prefix: "{yyyy}-{mm}-{dd}__",
|
|
41
|
+
write_tombstone: true,
|
|
42
|
+
tombstone_mode: "overwrite_source",
|
|
43
|
+
tombstone_content: "# Moved\n\nThis document has been archived to: [{archive_path}]({archive_relative_path})\n\nReason: {reason}\nDate: {yyyy}-{mm}-{dd}\n"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function nowParts(now) {
|
|
49
|
+
const yyyy = String(now.getFullYear()).padStart(4, "0");
|
|
50
|
+
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
|
51
|
+
const dd = String(now.getDate()).padStart(2, "0");
|
|
52
|
+
return { yyyy, mm, dd };
|
|
53
|
+
}
|
|
54
|
+
function renderTemplate(tpl, vars) {
|
|
55
|
+
let out = tpl;
|
|
56
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
57
|
+
out = out.split(`{${k}}`).join(v);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function prependArchiveObsoleteMarker(body) {
|
|
62
|
+
const marker = "<!-- OBSOLETE: This document is archived and no longer authoritative -->\n" +
|
|
63
|
+
"<!-- Reference: docs/SSOT.md is the ultimate authority -->\n\n" +
|
|
64
|
+
"> **ARCHIVED** - 이 문서는 더 이상 기준이 아닙니다.\n" +
|
|
65
|
+
"> 기준: docs/SSOT.md\n\n" +
|
|
66
|
+
"---\n\n";
|
|
67
|
+
return marker + body;
|
|
68
|
+
}
|
|
69
|
+
function ensureArchiveMarker(absPath) {
|
|
70
|
+
const raw = fs.readFileSync(absPath, "utf-8");
|
|
71
|
+
const head = raw.split("\n").slice(0, 12).join("\n");
|
|
72
|
+
const hasObsolete = /OBSOLETE/i.test(head);
|
|
73
|
+
const hasRef = head.includes("docs/SSOT.md");
|
|
74
|
+
if (hasObsolete && hasRef)
|
|
75
|
+
return;
|
|
76
|
+
fs.writeFileSync(absPath, prependArchiveObsoleteMarker(raw), "utf-8");
|
|
77
|
+
}
|
|
78
|
+
function upsertStatusHeader(absPath, headerLine) {
|
|
79
|
+
const raw = fs.readFileSync(absPath, "utf-8");
|
|
80
|
+
const lines = raw.split("\n");
|
|
81
|
+
const first = lines[0] ?? "";
|
|
82
|
+
const isExisting = first.trim().startsWith("<!-- DOC_STATUS:");
|
|
83
|
+
const next = isExisting ? [headerLine, ...lines.slice(1)] : [headerLine, ...lines];
|
|
84
|
+
const out = next.join("\n");
|
|
85
|
+
if (out === raw)
|
|
86
|
+
return { changed: false, existed: isExisting };
|
|
87
|
+
fs.writeFileSync(absPath, out, "utf-8");
|
|
88
|
+
return { changed: true, existed: isExisting };
|
|
89
|
+
}
|
|
90
|
+
function collectDocsUnder(basePath) {
|
|
91
|
+
const docsRoot = path.join(basePath, "docs");
|
|
92
|
+
if (!fs.existsSync(docsRoot))
|
|
93
|
+
return [];
|
|
94
|
+
const out = [];
|
|
95
|
+
const stack = [docsRoot];
|
|
96
|
+
while (stack.length > 0) {
|
|
97
|
+
const cur = stack.pop();
|
|
98
|
+
let entries = [];
|
|
99
|
+
try {
|
|
100
|
+
entries = fs.readdirSync(cur, { withFileTypes: true });
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
for (const e of entries) {
|
|
106
|
+
const abs = path.join(cur, e.name);
|
|
107
|
+
if (e.isDirectory()) {
|
|
108
|
+
// Hard safety: skip common heavy dirs even if policy is wrong.
|
|
109
|
+
if (e.name === "node_modules" || e.name === ".git" || e.name === "target" || e.name === ".vibe")
|
|
110
|
+
continue;
|
|
111
|
+
stack.push(abs);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (!e.isFile())
|
|
115
|
+
continue;
|
|
116
|
+
if (!abs.toLowerCase().endsWith(".md") && !abs.toLowerCase().endsWith(".mdx"))
|
|
117
|
+
continue;
|
|
118
|
+
out.push(abs);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
function matchesAny(relPosix, globs) {
|
|
124
|
+
for (const g of globs) {
|
|
125
|
+
if (minimatch(relPosix, g, { dot: true }))
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
function readHeadBlock(absPath, maxLines) {
|
|
131
|
+
try {
|
|
132
|
+
const raw = fs.readFileSync(absPath, "utf-8");
|
|
133
|
+
return raw.split("\n").slice(0, maxLines).join("\n");
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return "";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function addRunDirsFromRoot(runsRoot, out) {
|
|
140
|
+
try {
|
|
141
|
+
if (!fs.existsSync(runsRoot))
|
|
142
|
+
return;
|
|
143
|
+
const entries = fs.readdirSync(runsRoot, { withFileTypes: true });
|
|
144
|
+
for (const e of entries) {
|
|
145
|
+
if (!e.isDirectory())
|
|
146
|
+
continue;
|
|
147
|
+
const abs = path.join(runsRoot, e.name);
|
|
148
|
+
if (e.name === "examples" || e.name === "fixtures") {
|
|
149
|
+
addRunDirsFromRoot(abs, out);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
out.push(abs);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// ignore
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function collectRecentUsedDocs(basePath, lookbackDays, now) {
|
|
160
|
+
const out = new Set();
|
|
161
|
+
const lookbackMs = Math.max(0, lookbackDays) * 24 * 60 * 60 * 1000;
|
|
162
|
+
const cutoff = now.getTime() - lookbackMs;
|
|
163
|
+
const runDirs = [];
|
|
164
|
+
addRunDirsFromRoot(path.join(basePath, "runs"), runDirs);
|
|
165
|
+
addRunDirsFromRoot(path.join(basePath, "engines", "spec_high", "runs"), runDirs);
|
|
166
|
+
for (const runDir of runDirs) {
|
|
167
|
+
const p = path.join(runDir, "kce", "kce_doc_usage.json");
|
|
168
|
+
try {
|
|
169
|
+
if (!fs.existsSync(p))
|
|
170
|
+
continue;
|
|
171
|
+
const st = fs.statSync(p);
|
|
172
|
+
if (st.mtime.getTime() < cutoff)
|
|
173
|
+
continue;
|
|
174
|
+
const doc = readJsonIfExists(p);
|
|
175
|
+
const touched = Array.isArray(doc?.touched_docs) ? doc.touched_docs : [];
|
|
176
|
+
const injected = Array.isArray(doc?.injected_docs) ? doc.injected_docs : [];
|
|
177
|
+
for (const v of [...touched, ...injected]) {
|
|
178
|
+
if (typeof v !== "string")
|
|
179
|
+
continue;
|
|
180
|
+
const rel = v.replace(/\\\\/g, "/").trim();
|
|
181
|
+
if (rel)
|
|
182
|
+
out.add(rel);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// ignore
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
function classifyDoc(args) {
|
|
192
|
+
const { policy, absPath, relPosix, now, touchedNow, recentUsedDocs } = args;
|
|
193
|
+
const head = readHeadBlock(absPath, 80);
|
|
194
|
+
const signals = {
|
|
195
|
+
entity_link: false,
|
|
196
|
+
evidence_link: false,
|
|
197
|
+
recent_usage: false
|
|
198
|
+
};
|
|
199
|
+
if (policy.signals.entity_link.enabled) {
|
|
200
|
+
const markers = policy.signals.entity_link.link_markers ?? [];
|
|
201
|
+
signals.entity_link = markers.some((m) => m && head.includes(m));
|
|
202
|
+
}
|
|
203
|
+
if (policy.signals.evidence_link.enabled) {
|
|
204
|
+
const markers = policy.signals.evidence_link.evidence_markers ?? [];
|
|
205
|
+
signals.evidence_link = markers.some((m) => m && head.includes(m));
|
|
206
|
+
}
|
|
207
|
+
if (policy.signals.recent_usage.enabled) {
|
|
208
|
+
const lookbackMs = policy.signals.recent_usage.lookback_days * 24 * 60 * 60 * 1000;
|
|
209
|
+
let recent = false;
|
|
210
|
+
if (touchedNow.has(relPosix)) {
|
|
211
|
+
recent = true;
|
|
212
|
+
}
|
|
213
|
+
else if (recentUsedDocs.has(relPosix)) {
|
|
214
|
+
recent = true;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
try {
|
|
218
|
+
const mtime = fs.statSync(absPath).mtime.getTime();
|
|
219
|
+
recent = now.getTime() - mtime <= lookbackMs;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
recent = false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
signals.recent_usage = recent;
|
|
226
|
+
}
|
|
227
|
+
const score = (signals.entity_link ? policy.signals.entity_link.weight : 0) +
|
|
228
|
+
(signals.evidence_link ? policy.signals.evidence_link.weight : 0) +
|
|
229
|
+
(signals.recent_usage ? policy.signals.recent_usage.weight : 0);
|
|
230
|
+
const active = policy.classification.ACTIVE;
|
|
231
|
+
const dormant = policy.classification.DORMANT;
|
|
232
|
+
const orphan = policy.classification.ORPHAN;
|
|
233
|
+
const any = (keys) => keys.length === 0 || keys.some((k) => signals[k] === true);
|
|
234
|
+
const none = (keys) => keys.length === 0 || keys.every((k) => signals[k] !== true);
|
|
235
|
+
let status = "ORPHAN";
|
|
236
|
+
if (score >= active.min_score && any(active.required_any))
|
|
237
|
+
status = "ACTIVE";
|
|
238
|
+
else if (score >= dormant.min_score && any(dormant.required_any))
|
|
239
|
+
status = "DORMANT";
|
|
240
|
+
else if (orphan.required_none && !none(orphan.required_none))
|
|
241
|
+
status = "DORMANT";
|
|
242
|
+
else
|
|
243
|
+
status = "ORPHAN";
|
|
244
|
+
const reason = Object.entries(signals)
|
|
245
|
+
.filter(([, v]) => v)
|
|
246
|
+
.map(([k]) => k)
|
|
247
|
+
.join(",");
|
|
248
|
+
const lastSeen = touchedNow.has(relPosix) || recentUsedDocs.has(relPosix) ? now.toISOString() : "";
|
|
249
|
+
return { status, score, reason: reason || "none", lastSeen };
|
|
250
|
+
}
|
|
251
|
+
export function runDocStatusTriage(args) {
|
|
252
|
+
const now = args.now ?? new Date();
|
|
253
|
+
const policyPath = path.join(args.basePath, "schemas", "doc_status_policy.v1.json");
|
|
254
|
+
const loaded = readJsonIfExists(policyPath);
|
|
255
|
+
const policy = loaded ?? defaultPolicy();
|
|
256
|
+
const include = policy.scope.include_globs ?? ["docs/**/*.md"];
|
|
257
|
+
const exclude = policy.scope.exclude_globs ?? [];
|
|
258
|
+
const touchedNow = new Set((args.touched_paths ?? []).map((p) => p.replace(/\\\\/g, "/")));
|
|
259
|
+
const recentUsedDocs = policy.signals.recent_usage.enabled
|
|
260
|
+
? collectRecentUsedDocs(args.basePath, policy.signals.recent_usage.lookback_days, now)
|
|
261
|
+
: new Set();
|
|
262
|
+
const tagged = [];
|
|
263
|
+
const archived = [];
|
|
264
|
+
const tombstoned = [];
|
|
265
|
+
const hadStatusTagBefore = new Set();
|
|
266
|
+
let active = 0;
|
|
267
|
+
let dormant = 0;
|
|
268
|
+
let orphan = 0;
|
|
269
|
+
const candidates = collectDocsUnder(args.basePath);
|
|
270
|
+
for (const abs of candidates) {
|
|
271
|
+
const rel = path.relative(args.basePath, abs).replace(/\\\\/g, "/");
|
|
272
|
+
if (!matchesAny(rel, include))
|
|
273
|
+
continue;
|
|
274
|
+
if (matchesAny(rel, exclude))
|
|
275
|
+
continue;
|
|
276
|
+
const { status, score, reason, lastSeen } = classifyDoc({
|
|
277
|
+
relPosix: rel,
|
|
278
|
+
absPath: abs,
|
|
279
|
+
policy,
|
|
280
|
+
now,
|
|
281
|
+
touchedNow,
|
|
282
|
+
recentUsedDocs
|
|
283
|
+
});
|
|
284
|
+
if (status === "ACTIVE")
|
|
285
|
+
active += 1;
|
|
286
|
+
else if (status === "DORMANT")
|
|
287
|
+
dormant += 1;
|
|
288
|
+
else
|
|
289
|
+
orphan += 1;
|
|
290
|
+
if (policy.actions.tag_header.enabled) {
|
|
291
|
+
const header = renderTemplate(policy.actions.tag_header.format, {
|
|
292
|
+
status,
|
|
293
|
+
score: String(score),
|
|
294
|
+
reason,
|
|
295
|
+
last_seen: lastSeen || "-"
|
|
296
|
+
});
|
|
297
|
+
try {
|
|
298
|
+
const res = upsertStatusHeader(abs, header);
|
|
299
|
+
if (res.existed)
|
|
300
|
+
hadStatusTagBefore.add(rel);
|
|
301
|
+
if (res.changed)
|
|
302
|
+
tagged.push(rel);
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
// ignore
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Archive is intentionally conservative; v1 uses grace_days only with mtime heuristic.
|
|
309
|
+
if (policy.actions.archive.enabled && policy.actions.archive.only_status.includes(status)) {
|
|
310
|
+
// Safety: do not archive a doc the same run it gets tagged the first time.
|
|
311
|
+
// This prevents mass moves on first enable and keeps the pipeline idempotent.
|
|
312
|
+
if (!hadStatusTagBefore.has(rel))
|
|
313
|
+
continue;
|
|
314
|
+
const graceMs = policy.actions.archive.grace_days * 24 * 60 * 60 * 1000;
|
|
315
|
+
let eligible = false;
|
|
316
|
+
try {
|
|
317
|
+
const mtime = fs.statSync(abs).mtime.getTime();
|
|
318
|
+
eligible = now.getTime() - mtime >= graceMs;
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
eligible = false;
|
|
322
|
+
}
|
|
323
|
+
if (!eligible)
|
|
324
|
+
continue;
|
|
325
|
+
const { yyyy, mm, dd } = nowParts(now);
|
|
326
|
+
const archiveRootAbs = path.join(args.basePath, policy.actions.archive.archive_root);
|
|
327
|
+
const prefix = renderTemplate(policy.actions.archive.archive_prefix, { yyyy, mm, dd });
|
|
328
|
+
const fileName = path.basename(abs);
|
|
329
|
+
const archiveName = `${prefix}${fileName}`;
|
|
330
|
+
const archiveAbs = path.join(archiveRootAbs, archiveName);
|
|
331
|
+
try {
|
|
332
|
+
fs.mkdirSync(path.dirname(archiveAbs), { recursive: true });
|
|
333
|
+
fs.renameSync(abs, archiveAbs);
|
|
334
|
+
ensureArchiveMarker(archiveAbs);
|
|
335
|
+
archived.push(path.relative(args.basePath, archiveAbs).replace(/\\\\/g, "/"));
|
|
336
|
+
if (policy.actions.archive.write_tombstone) {
|
|
337
|
+
const relArchiveFromRepo = path.relative(args.basePath, archiveAbs).replace(/\\\\/g, "/");
|
|
338
|
+
const relFromSourceDir = path
|
|
339
|
+
.relative(path.dirname(abs), archiveAbs)
|
|
340
|
+
.replace(/\\\\/g, "/");
|
|
341
|
+
const tombTpl = policy.actions.archive.tombstone_content ??
|
|
342
|
+
policy.actions.archive.tombstone_format ??
|
|
343
|
+
"# Moved\n\nThis document has been archived.\n";
|
|
344
|
+
const tomb = renderTemplate(tombTpl, {
|
|
345
|
+
yyyy,
|
|
346
|
+
mm,
|
|
347
|
+
dd,
|
|
348
|
+
status,
|
|
349
|
+
reason,
|
|
350
|
+
path: rel,
|
|
351
|
+
archive_path: relArchiveFromRepo,
|
|
352
|
+
archive_relative_path: relFromSourceDir
|
|
353
|
+
});
|
|
354
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
355
|
+
fs.writeFileSync(abs, tomb + "\n", "utf-8");
|
|
356
|
+
tombstoned.push(rel);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// ignore
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
tagged,
|
|
366
|
+
archived,
|
|
367
|
+
tombstoned,
|
|
368
|
+
summary: { active, dormant, orphan }
|
|
369
|
+
};
|
|
370
|
+
}
|