@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,828 @@
1
+ // adapters/mcp-ts/src/tools/vibe_pm/inspect_code.ts
2
+ // vibe_pm.inspect_code - Code review and validation
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, resolveBridgePath, readRunState, getRunsDir, resolveRunDir, invalidateStateCache } from "./context.js";
9
+ import { validateInspectCodeInput } from "../../security/input-validator.js";
10
+ import { preExecutionCheck } from "../../security/path-policy.js";
11
+ import { PATH_ZONES } from "../../security/path-policy.js";
12
+ import { signalToReview, formatMode, formatIssueType, generateRepairPrompt } from "./pm_language.js";
13
+ import { ClinicBridgeFileSchema } from "../../generated/clinic_bridge_file.js";
14
+ import { selectMessageTemplate } from "./types.js";
15
+ import { fixDependencies } from "./modules/fix_dependencies.js";
16
+ import { toInspectionData, runIntentVerification, formatVerificationForPM } from "./intent/index.js";
17
+ import { VibecodingHelperOneLoopSelectionOutputSchema } from "../../generated/vibecoding_helper_one_loop_selection_output.js";
18
+ import { runEntityGate } from "./entity_gate/preflight.js";
19
+ import { updateKceDocUsage } from "./kce/doc_usage.js";
20
+ import { kcePreflight } from "./kce/preflight.js";
21
+ import { loadRulebook } from "./system_design/rulebook.js";
22
+ import { runSemgrepDesignEnforcement } from "./system_design/semgrep.js";
23
+ import { semgrepFindingsToIssues } from "./system_design/issue_mapping.js";
24
+ /**
25
+ * vibe_pm.inspect_code - Code review and validation
26
+ *
27
+ * Internal mapping:
28
+ * → find latest bridge (auto-wiring)
29
+ * → vibecoding-helper one-loop <run_id> --output selection
30
+ * → issue classification (type mapping)
31
+ * → prompt_block generation (if FIX/BLOCK)
32
+ * → result transform (PM language)
33
+ */
34
+ export async function inspectCode(input) {
35
+ const basePath = process.cwd();
36
+ // Security: Validate input first
37
+ validateInspectCodeInput(input);
38
+ // Resolve run_id and bridge path
39
+ const { run_id } = resolveRunId(input.project_id, basePath);
40
+ const project_id = resolveProjectId(run_id, basePath);
41
+ const runState = readRunState(run_id, basePath);
42
+ const bridgePath = resolveBridgePath(input.bridge_path, run_id, basePath);
43
+ // Get mode for context
44
+ const mode = runState?.mode ?? "balanced";
45
+ // P3: Validate and cache the bridge file shape once (fail closed on drift).
46
+ let bridgeDoc = null;
47
+ if (bridgePath) {
48
+ try {
49
+ bridgeDoc = loadBridgeFileValidated(bridgePath);
50
+ }
51
+ catch (e) {
52
+ const issues = [
53
+ {
54
+ type: "RULE_VIOLATION",
55
+ business_risk: "작업 지시서 형식이 예상과 달라 안전한 검수를 진행할 수 없습니다.",
56
+ plain_explanation: "작업 지시서를 다시 발행한 뒤, 다시 검수해주세요.",
57
+ technical_detail: {
58
+ file_path: bridgePath,
59
+ rule_violated: "WORK_ORDER_FILE_SCHEMA_DRIFT"
60
+ }
61
+ }
62
+ ];
63
+ const decisionContext = getDecisionContext(mode, runState);
64
+ const next_action = {
65
+ action_type: "CONTINUE",
66
+ tool: "vibe_pm.create_work_order",
67
+ reason: "작업 지시서를 다시 발행하세요"
68
+ };
69
+ const message_template_id = selectMessageTemplate("BLOCK", issues, true);
70
+ return {
71
+ project_id,
72
+ review_summary: {
73
+ decision_context: decisionContext,
74
+ review_result: "BLOCK",
75
+ one_line_verdict: "작업 지시서를 다시 발행해야 합니다"
76
+ },
77
+ issues_found: issues,
78
+ next_action,
79
+ message_template_id
80
+ };
81
+ }
82
+ }
83
+ // v2.x: P0 Scope Enforcement (Immediate BLOCK)
84
+ // If Spec-High decision_guard.allow_paths expands outside AI GREEN zone, stop before engine execution.
85
+ if (bridgePath) {
86
+ const allowPaths = bridgeDoc ? extractAllowPathsFromBridge(bridgeDoc) : [];
87
+ const invalid = findInvalidAllowPaths(allowPaths);
88
+ if (invalid.length > 0) {
89
+ const issues = [
90
+ {
91
+ type: "RULE_VIOLATION",
92
+ business_risk: "작업 지시서의 허용 범위가 안전 작업 구역 밖으로 확장되어, 설정/시크릿/시스템 파일 훼손 위험이 있습니다.",
93
+ plain_explanation: "이번 작업은 작업 범위부터 다시 정리해야 합니다. 안전 작업 구역(src/tests/lib/scripts) 밖의 범위는 허용되지 않습니다.",
94
+ technical_detail: {
95
+ file_path: bridgePath,
96
+ rule_violated: "SCOPE_INCLUDE_OUTSIDE_SRC_BLOCK"
97
+ }
98
+ }
99
+ ];
100
+ const decisionContext = getDecisionContext(mode, runState);
101
+ const next_action = {
102
+ action_type: "CONTINUE",
103
+ tool: "vibe_pm.create_work_order",
104
+ reason: `작업 범위를 안전 작업 구역으로 다시 제한하세요 (위반: ${invalid.join(", ")})`
105
+ };
106
+ const message_template_id = selectMessageTemplate("BLOCK", issues, true);
107
+ return {
108
+ project_id,
109
+ review_summary: {
110
+ decision_context: decisionContext,
111
+ review_result: "BLOCK",
112
+ one_line_verdict: "작업 범위가 안전 구역을 벗어났습니다"
113
+ },
114
+ issues_found: issues,
115
+ next_action,
116
+ message_template_id
117
+ };
118
+ }
119
+ }
120
+ // OPA deny promotion: if policy_result says "allow=false", hard-block inspect_code (LEVEL_3).
121
+ const opaBlock = buildOpaPolicyBlockOutput({
122
+ basePath,
123
+ run_id,
124
+ project_id,
125
+ mode,
126
+ runState
127
+ });
128
+ if (opaBlock) {
129
+ return opaBlock;
130
+ }
131
+ // KCE preflight (mandatory): verify Evidence/KB substrate is READY before running inspection.
132
+ // Entity Gate runs first to fail-fast on contract drift (schema/tests/static rules).
133
+ try {
134
+ const gate = await runEntityGate({ basePath, run_id, writeEvidence: true });
135
+ if (gate.status !== "OK") {
136
+ const issues = [
137
+ {
138
+ type: "RULE_VIOLATION",
139
+ business_risk: "핵심 계약(엔티티)이 깨져 있어 안전한 검수를 진행할 수 없습니다.",
140
+ plain_explanation: "계약 문서/정적 규칙/테스트 연결을 먼저 복구해야 합니다.",
141
+ technical_detail: {
142
+ file_path: "entities/",
143
+ rule_violated: gate.errors.slice(0, 3).join("; ") || "ENTITY_GATE_FAILED"
144
+ }
145
+ }
146
+ ];
147
+ const decisionContext = getDecisionContext(mode, runState);
148
+ const next_action = {
149
+ action_type: "DONE",
150
+ message: "계약 문서가 깨져 있어 검수를 진행할 수 없습니다. 먼저 계약/규칙/테스트 연결을 복구하세요."
151
+ };
152
+ const message_template_id = selectMessageTemplate("BLOCK", issues, true);
153
+ return {
154
+ project_id,
155
+ review_summary: {
156
+ decision_context: decisionContext,
157
+ review_result: "BLOCK",
158
+ one_line_verdict: "계약 복구가 필요합니다"
159
+ },
160
+ issues_found: issues,
161
+ next_action,
162
+ message_template_id
163
+ };
164
+ }
165
+ }
166
+ catch (e) {
167
+ const msg = e instanceof Error ? e.message : String(e);
168
+ const issues = [
169
+ {
170
+ type: "RULE_VIOLATION",
171
+ business_risk: "계약 검증(엔티티 게이트)을 실행할 수 없어 검수를 진행할 수 없습니다.",
172
+ plain_explanation: "잠시 후 다시 시도해 주세요. 문제가 반복되면 도구 상태 점검이 필요합니다.",
173
+ technical_detail: {
174
+ file_path: "scripts/entity_gate.py",
175
+ rule_violated: msg
176
+ }
177
+ }
178
+ ];
179
+ const decisionContext = getDecisionContext(mode, runState);
180
+ const next_action = {
181
+ action_type: "DONE",
182
+ message: "계약 검증 실행에 실패했습니다. 도구 상태 점검이 필요합니다."
183
+ };
184
+ const message_template_id = selectMessageTemplate("BLOCK", issues, true);
185
+ return {
186
+ project_id,
187
+ review_summary: {
188
+ decision_context: decisionContext,
189
+ review_result: "BLOCK",
190
+ one_line_verdict: "검수 준비가 필요합니다"
191
+ },
192
+ issues_found: issues,
193
+ next_action,
194
+ message_template_id
195
+ };
196
+ }
197
+ // KCE preflight (mandatory): BLOCK when the knowledge substrate is not READY.
198
+ // This prevents "inspect_code" from producing low-quality/low-evidence verdicts.
199
+ try {
200
+ await kcePreflight({ basePath, run_id, purpose: "inspect_code", writeEvidence: true });
201
+ }
202
+ catch (e) {
203
+ const msg = e instanceof Error ? e.message : String(e);
204
+ const issues = [
205
+ {
206
+ type: "RULE_VIOLATION",
207
+ business_risk: "검수에 필요한 근거(지식 인덱스)가 준비되지 않아 안전한 검수를 진행할 수 없습니다.",
208
+ plain_explanation: "도구 상태 점검 후 다시 검수해 주세요.",
209
+ technical_detail: {
210
+ file_path: ".vibe/kce/",
211
+ rule_violated: msg
212
+ }
213
+ }
214
+ ];
215
+ const decisionContext = getDecisionContext(mode, runState);
216
+ const next_action = {
217
+ action_type: "DONE",
218
+ message: "검수 준비가 완료되지 않았습니다. 도구 상태 점검 후 다시 시도해 주세요."
219
+ };
220
+ const message_template_id = selectMessageTemplate("BLOCK", issues, false);
221
+ return {
222
+ project_id,
223
+ review_summary: {
224
+ decision_context: decisionContext,
225
+ review_result: "BLOCK",
226
+ one_line_verdict: "검수 준비가 필요합니다"
227
+ },
228
+ issues_found: issues,
229
+ next_action,
230
+ message_template_id
231
+ };
232
+ }
233
+ // Security: Validate and normalize target paths
234
+ let validatedTargetPaths;
235
+ if (input.target_paths && input.target_paths.length > 0) {
236
+ // Get do_not_touch patterns from bridge (if available)
237
+ const doNotTouchPatterns = bridgeDoc ? extractDoNotTouchPatternsFromBridge(bridgeDoc) : [];
238
+ // Validate paths against security policies
239
+ validatedTargetPaths = preExecutionCheck(input.target_paths, doNotTouchPatterns, basePath);
240
+ }
241
+ // Build command arguments
242
+ const args = ["one-loop", run_id, "--output", "selection"];
243
+ if (bridgePath) {
244
+ args.push("--bridge", bridgePath);
245
+ }
246
+ // Use validated paths instead of raw input
247
+ if (validatedTargetPaths && validatedTargetPaths.length > 0) {
248
+ args.push("--paths", validatedTargetPaths.join(","));
249
+ }
250
+ if (input.mode === "quick") {
251
+ args.push("--quick");
252
+ }
253
+ // Run one-loop validation
254
+ const { code, stdout, stderr } = await runEngine("vibecoding-helper", args, {
255
+ timeoutMs: 180_000
256
+ });
257
+ // Parse result
258
+ const parsed = safeJsonParse(stdout);
259
+ if (!parsed.ok) {
260
+ // If parsing fails, treat as unknown error
261
+ return createErrorOutput(project_id, mode, "검수 결과를 파싱할 수 없습니다", stderr);
262
+ }
263
+ const validated = VibecodingHelperOneLoopSelectionOutputSchema.safeParse(parsed.value);
264
+ if (!validated.success) {
265
+ return createErrorOutput(project_id, mode, "검수 결과 형식이 예상과 다릅니다", stderr);
266
+ }
267
+ let result = validated.data;
268
+ // Determine review status from signal
269
+ const signalLevel = result.signal_level ?? result.signal ?? "LEVEL_1";
270
+ let { result: reviewResult, verdict } = signalToReview(signalLevel);
271
+ // Collect and transform issues
272
+ let issues = transformIssues(result);
273
+ // P2: False Positive recheck loop (BLOCK only, non-critical)
274
+ // If the first run is BLOCK but contains no hard safety issues, re-run once to confirm.
275
+ // - quick → thorough escalation
276
+ // - thorough → one additional confirmation run
277
+ if (reviewResult === "BLOCK" &&
278
+ !issues.some((i) => i.type === "RULE_VIOLATION" || i.type === "RISK_SPIKE")) {
279
+ const recheckArgs = input.mode === "quick" ? args.filter((a) => a !== "--quick") : args;
280
+ try {
281
+ const recheck = await runEngine("vibecoding-helper", recheckArgs, { timeoutMs: 180_000 });
282
+ const reParsed = safeJsonParse(recheck.stdout);
283
+ if (reParsed.ok) {
284
+ const reValidated = VibecodingHelperOneLoopSelectionOutputSchema.safeParse(reParsed.value);
285
+ if (!reValidated.success) {
286
+ throw new Error("recheck_output_schema_invalid");
287
+ }
288
+ const reResult = reValidated.data;
289
+ const reSignalLevel = reResult.signal_level ?? reResult.signal ?? "LEVEL_1";
290
+ const reMapped = signalToReview(reSignalLevel);
291
+ const reIssues = transformIssues(reResult);
292
+ // Never soften if the recheck still reports critical issues.
293
+ const reHasHardIssues = reIssues.some((i) => i.type === "RULE_VIOLATION" || i.type === "RISK_SPIKE");
294
+ // If recheck is less severe, accept it.
295
+ if (!reHasHardIssues && reMapped.result !== "BLOCK") {
296
+ result = reResult;
297
+ reviewResult = reMapped.result;
298
+ verdict = `${reMapped.verdict} (재검증 완료)`;
299
+ issues = reIssues;
300
+ }
301
+ }
302
+ }
303
+ catch {
304
+ // Recheck is best-effort; keep original result on any failure.
305
+ }
306
+ }
307
+ // Get decision context
308
+ const decisionContext = getDecisionContext(mode, runState);
309
+ const modifiedFiles = collectModifiedFiles(result);
310
+ // Evidence (best-effort): record doc usage for deterministic recency signals.
311
+ try {
312
+ const touchedDocs = modifiedFiles
313
+ .map((p) => (typeof p === "string" ? p : ""))
314
+ .map((p) => p.replace(/\\\\/g, "/"))
315
+ .filter((p) => p.endsWith(".md") || p.endsWith(".mdx"));
316
+ const referencedEntities = modifiedFiles
317
+ .map((p) => (typeof p === "string" ? p : ""))
318
+ .map((p) => p.replace(/\\\\/g, "/"))
319
+ .filter((p) => p.startsWith("entities/") && p.endsWith(".entity.json"))
320
+ .map((p) => path.basename(p, ".entity.json"))
321
+ .filter(Boolean);
322
+ updateKceDocUsage({ basePath, run_id, touched_docs: touchedDocs, referenced_entities: referencedEntities });
323
+ }
324
+ catch {
325
+ // ignore
326
+ }
327
+ // Design Enforcement (best-effort): Semgrep findings -> issues_found (mapped via rulebook).
328
+ try {
329
+ const { rulebook } = loadRulebook(basePath);
330
+ const semgrepTargets = computeSemgrepTargets({ basePath, modifiedFiles, validatedTargetPaths });
331
+ const semgrep = await runSemgrepDesignEnforcement({ basePath, run_id, targets: semgrepTargets, timeoutMs: 60_000 });
332
+ if (semgrep.status === "OK" && semgrep.findings.length > 0) {
333
+ const mapped = semgrepFindingsToIssues({
334
+ findings: semgrep.findings,
335
+ rulebook,
336
+ evidence_ref: semgrep.evidence_ref
337
+ });
338
+ issues = [...issues, ...mapped.issues];
339
+ // Only tighten GO -> FIX when Semgrep reports ERROR/CRITICAL findings.
340
+ if (reviewResult === "GO" && mapped.error_count > 0) {
341
+ reviewResult = "FIX";
342
+ verdict = `${verdict} (설계 규칙 위반 ${mapped.error_count}건)`;
343
+ }
344
+ }
345
+ else if (semgrep.status === "ERROR") {
346
+ // Keep the workflow moving: do not downgrade; just annotate the verdict.
347
+ verdict = `${verdict} (설계 규칙 검사를 실행할 수 없습니다)`;
348
+ }
349
+ }
350
+ catch {
351
+ // ignore (best-effort)
352
+ }
353
+ // Run Intent-based verification (if intent document exists)
354
+ let intentCheck;
355
+ try {
356
+ const runsDir = getRunsDir(basePath);
357
+ const inspectionData = toInspectionData({
358
+ status: result.status,
359
+ issues: result.issues?.map((i) => ({
360
+ type: i.type ?? i.issue_type ?? "unknown",
361
+ message: i.message ?? i.description ?? "",
362
+ business_risk: i.business_risk
363
+ })),
364
+ modified_files: modifiedFiles,
365
+ features: result.features ?? []
366
+ });
367
+ const intentResult = await runIntentVerification(runsDir, run_id, inspectionData);
368
+ if (intentResult) {
369
+ intentCheck = formatVerificationForPM(intentResult).intent_check;
370
+ // Downgrade review result if intent verification fails
371
+ if (intentCheck.status === "FAIL" && reviewResult === "GO") {
372
+ reviewResult = "FIX";
373
+ verdict = `Intent 검증 실패: ${intentCheck.summary}`;
374
+ }
375
+ else if (intentCheck.status === "WARN" && reviewResult === "GO") {
376
+ verdict = `${verdict} (Intent 주의: ${intentCheck.summary})`;
377
+ }
378
+ }
379
+ }
380
+ catch {
381
+ // Intent verification is optional - continue if it fails
382
+ }
383
+ // Auto-fix logic (if enabled)
384
+ let fixedIssues;
385
+ if (input.auto_fix !== false) {
386
+ try {
387
+ const fixResult = await fixDependencies(basePath);
388
+ if (fixResult.fixed.length > 0) {
389
+ fixedIssues = fixResult.fixed;
390
+ // If we fixed issues and no critical issues remain, upgrade review result
391
+ const hasCriticalIssues = issues.some((i) => i.type === "RULE_VIOLATION" || i.type === "RISK_SPIKE");
392
+ if (!hasCriticalIssues && reviewResult === "FIX") {
393
+ // Re-evaluate: if only dependency issues were found and we fixed them, GO
394
+ const onlyDependencyIssues = issues.every((i) => i.type === "VERIFY_FAIL" && i.plain_explanation.includes("의존성"));
395
+ if (onlyDependencyIssues) {
396
+ reviewResult = "GO";
397
+ verdict = `자동 수정 완료: ${fixResult.fixed.length}개 의존성 문제 해결`;
398
+ }
399
+ else {
400
+ verdict = `${verdict} (${fixResult.fixed.length}개 의존성 자동 수정됨)`;
401
+ }
402
+ }
403
+ }
404
+ }
405
+ catch {
406
+ // Auto-fix is optional - continue if it fails
407
+ }
408
+ }
409
+ // Determine next action based on review result
410
+ const nextAction = determineNextAction(reviewResult, issues, project_id, mode);
411
+ // P14: Determine message template ID using SSOT rules
412
+ const hasNextWorkOrder = nextAction.action_type === "CONTINUE";
413
+ const message_template_id = selectMessageTemplate(reviewResult, issues, hasNextWorkOrder);
414
+ // Build output with optional intent check
415
+ const output = {
416
+ project_id,
417
+ review_summary: {
418
+ decision_context: decisionContext,
419
+ review_result: reviewResult,
420
+ one_line_verdict: verdict
421
+ },
422
+ issues_found: issues,
423
+ fixed_issues: fixedIssues,
424
+ next_action: nextAction,
425
+ message_template_id
426
+ };
427
+ invalidateEngineCache(new RegExp(escapeRegExp(run_id)));
428
+ invalidateStateCache(run_id, basePath);
429
+ return output;
430
+ }
431
+ function buildOpaPolicyBlockOutput(args) {
432
+ const evidence = readOpaPolicyResult(args.basePath, args.run_id);
433
+ if (!evidence)
434
+ return null;
435
+ const allow = evidence.result.allow;
436
+ if (allow === true)
437
+ return null;
438
+ const classification = classifyOpaPolicyResult(evidence.result);
439
+ const decisionContext = getDecisionContext(args.mode, args.runState);
440
+ const issue = {
441
+ type: "RULE_VIOLATION",
442
+ business_risk: classification.business_risk,
443
+ plain_explanation: classification.plain_explanation,
444
+ technical_detail: {
445
+ file_path: "policy/vibepm/run_app.rego",
446
+ rule_violated: classification.rule_violated,
447
+ evidence_ref: evidence.evidence_path
448
+ }
449
+ };
450
+ return {
451
+ project_id: args.project_id,
452
+ review_summary: {
453
+ decision_context: decisionContext,
454
+ review_result: "BLOCK",
455
+ one_line_verdict: classification.one_line_verdict
456
+ },
457
+ issues_found: [issue],
458
+ next_action: {
459
+ action_type: "DONE",
460
+ message: classification.next_action_message
461
+ },
462
+ message_template_id: "BLOCK-02"
463
+ };
464
+ }
465
+ function readOpaPolicyResult(basePath, run_id) {
466
+ const resolved = resolveRunDir(run_id, basePath);
467
+ if (!resolved)
468
+ return null;
469
+ const evidencePath = path.join(resolved.run_dir, "security", "policy_result.json");
470
+ if (!fs.existsSync(evidencePath))
471
+ return null;
472
+ try {
473
+ const raw = JSON.parse(fs.readFileSync(evidencePath, "utf-8"));
474
+ if (!raw || typeof raw !== "object")
475
+ return null;
476
+ const reasons = Array.isArray(raw.reasons)
477
+ ? raw.reasons.filter((r) => typeof r === "string")
478
+ : [];
479
+ const matched_rules = Array.isArray(raw.matched_rules)
480
+ ? raw.matched_rules.filter((r) => typeof r === "string")
481
+ : [];
482
+ const result = {
483
+ allow: typeof raw.allow === "boolean" ? raw.allow : undefined,
484
+ reasons,
485
+ matched_rules
486
+ };
487
+ return { result, evidence_path: evidencePath };
488
+ }
489
+ catch {
490
+ return null;
491
+ }
492
+ }
493
+ function classifyOpaPolicyResult(result) {
494
+ const reasons = (result.reasons ?? []).join(" ").toLowerCase();
495
+ const matched = (result.matched_rules ?? []).join(" ").toLowerCase();
496
+ const corpus = `${reasons} ${matched}`.trim();
497
+ if (corpus.includes("eval_failed") ||
498
+ corpus.includes("opa_eval_failed") ||
499
+ corpus.includes("opa exception") ||
500
+ corpus.includes("opa_exception") ||
501
+ corpus.includes("eval failed")) {
502
+ return {
503
+ rule_violated: "OPA_POLICY_EVAL_FAILED",
504
+ one_line_verdict: "OPA 평가에 실패하여 안전을 위해 차단했습니다(기본 차단).",
505
+ business_risk: "정책 평가가 실패한 상태에서 실행을 허용하면 통제 불능 상태가 됩니다.",
506
+ plain_explanation: "정책 엔진(OPA) 실행/파싱/타임아웃 문제로 정책 판단을 할 수 없어 기본 차단했습니다.",
507
+ next_action_message: "OPA 설치/정책 파일/실행 환경을 확인한 뒤 다시 시도하세요."
508
+ };
509
+ }
510
+ if (corpus.includes("opa_missing") ||
511
+ corpus.includes("opa not found") ||
512
+ corpus.includes("not found") ||
513
+ corpus.includes("missing")) {
514
+ return {
515
+ rule_violated: "OPA_POLICY_MISSING",
516
+ one_line_verdict: "OPA가 설치되어 있지 않아 안전을 위해 차단했습니다(기본 차단).",
517
+ business_risk: "정책 엔진이 없으면 실행 통제가 불가능해져 보안 경계가 무너질 수 있습니다.",
518
+ plain_explanation: "OPA 바이너리가 없어 정책 판단을 할 수 없어 기본 차단했습니다.",
519
+ next_action_message: "OPA 설치 후 다시 시도하세요."
520
+ };
521
+ }
522
+ return {
523
+ rule_violated: "OPA_POLICY_DENY_RUN_APP",
524
+ one_line_verdict: "정책(OPA)에서 실행이 차단되었습니다. 허용된 커맨드/경로만 사용하세요.",
525
+ business_risk: "로컬 실행(run_app)이 정책 위반으로 이어지면 임의 커맨드 실행/RCE 및 보호 영역 오염 가능성이 있습니다.",
526
+ plain_explanation: "보안 정책이 허용하지 않은 커맨드 또는 경로 접근이 감지되어 실행을 차단했습니다.",
527
+ next_action_message: "OPA 정책을 만족하도록 command/paths를 수정한 뒤 다시 시도하세요."
528
+ };
529
+ }
530
+ function computeSemgrepTargets(args) {
531
+ const out = new Set();
532
+ const candidates = [...(args.validatedTargetPaths ?? []), ...(args.modifiedFiles ?? [])];
533
+ const allowedPrefixes = ["vibecoding_helper/", "adapters/mcp-ts/src/"];
534
+ const allowedExt = new Set([".py", ".ts", ".tsx", ".js", ".jsx"]);
535
+ for (const raw of candidates) {
536
+ if (typeof raw !== "string")
537
+ continue;
538
+ let p = raw.replace(/\\\\/g, "/").trim();
539
+ if (!p)
540
+ continue;
541
+ if (path.isAbsolute(p)) {
542
+ p = path.relative(args.basePath, p).replace(/\\\\/g, "/");
543
+ }
544
+ if (!p || p.startsWith(".."))
545
+ continue;
546
+ if (!allowedPrefixes.some((pref) => p === pref.slice(0, -1) || p.startsWith(pref)))
547
+ continue;
548
+ const ext = path.extname(p).toLowerCase();
549
+ if (!allowedExt.has(ext))
550
+ continue;
551
+ const abs = path.join(args.basePath, p);
552
+ try {
553
+ if (fs.existsSync(abs) && fs.statSync(abs).isFile()) {
554
+ out.add(p);
555
+ }
556
+ }
557
+ catch {
558
+ // ignore
559
+ }
560
+ }
561
+ return Array.from(out.values()).slice(0, 200);
562
+ }
563
+ /**
564
+ * Transform internal issues to user-facing format
565
+ */
566
+ function transformIssues(result) {
567
+ const issues = [];
568
+ // Transform direct issues
569
+ if (result.issues) {
570
+ for (const issue of result.issues) {
571
+ const type = mapIssueType(issue.type ?? issue.issue_type ?? "unknown");
572
+ const { name, description } = formatIssueType(type);
573
+ issues.push({
574
+ type,
575
+ business_risk: issue.business_risk ?? description,
576
+ plain_explanation: issue.message ?? issue.description ?? name,
577
+ technical_detail: issue.file_path
578
+ ? {
579
+ file_path: issue.file_path,
580
+ line_range: issue.line_range,
581
+ rule_violated: issue.rule_violated
582
+ }
583
+ : undefined
584
+ });
585
+ }
586
+ }
587
+ // Transform verification failures
588
+ if (result.verification_results) {
589
+ for (const vr of result.verification_results) {
590
+ if (!vr.passed) {
591
+ issues.push({
592
+ type: "VERIFY_FAIL",
593
+ business_risk: `검증 기준 "${vr.criterion}"을(를) 충족하지 못했습니다`,
594
+ plain_explanation: vr.message ?? "검증에 실패했습니다"
595
+ });
596
+ }
597
+ }
598
+ }
599
+ // Transform scope violations
600
+ if (result.scope_violations) {
601
+ for (const sv of result.scope_violations) {
602
+ issues.push({
603
+ type: "SCOPE_MISMATCH",
604
+ business_risk: "이번 작업 범위를 벗어난 변경이 있습니다",
605
+ plain_explanation: sv.description ?? "범위 이탈",
606
+ technical_detail: sv.file_path
607
+ ? {
608
+ file_path: sv.file_path
609
+ }
610
+ : undefined
611
+ });
612
+ }
613
+ }
614
+ return issues;
615
+ }
616
+ /**
617
+ * Map internal issue type to standardized type
618
+ */
619
+ function mapIssueType(rawType) {
620
+ const normalized = rawType.toLowerCase().replace(/[^a-z_]/g, "");
621
+ if (normalized.includes("rule") || normalized.includes("violation") || normalized.includes("constraint")) {
622
+ return "RULE_VIOLATION";
623
+ }
624
+ if (normalized.includes("verify") || normalized.includes("acceptance") || normalized.includes("criteria")) {
625
+ return "VERIFY_FAIL";
626
+ }
627
+ if (normalized.includes("scope") || normalized.includes("out_of") || normalized.includes("boundary")) {
628
+ return "SCOPE_MISMATCH";
629
+ }
630
+ if (normalized.includes("risk") || normalized.includes("security") || normalized.includes("danger")) {
631
+ return "RISK_SPIKE";
632
+ }
633
+ return "VERIFY_FAIL"; // Default
634
+ }
635
+ /**
636
+ * Generate decision context message
637
+ */
638
+ function getDecisionContext(mode, runState) {
639
+ const modeDisplay = formatMode(mode);
640
+ if (runState?.decisions_made && runState.decisions_made > 0) {
641
+ return `${modeDisplay}로 진행 중 (결재 ${runState.decisions_made}건 완료)`;
642
+ }
643
+ return `${modeDisplay}로 결정하셨습니다`;
644
+ }
645
+ /**
646
+ * Determine next action based on review result
647
+ */
648
+ function determineNextAction(reviewResult, issues, projectId, mode) {
649
+ if (reviewResult === "GO") {
650
+ return {
651
+ action_type: "DONE",
652
+ message: "구현이 완료되었습니다. 다음 단계로 진행하세요."
653
+ };
654
+ }
655
+ if (reviewResult === "FIX" || reviewResult === "BLOCK") {
656
+ // Generate repair prompt
657
+ const mainIssue = issues[0];
658
+ const promptBlock = generateRepairPrompt({
659
+ projectName: projectId,
660
+ mode,
661
+ problem: mainIssue?.plain_explanation ?? "문제가 발견되었습니다",
662
+ cause: mainIssue?.business_risk ?? "원인을 파악 중입니다",
663
+ fixDirection: issues.map((i) => i.plain_explanation),
664
+ verifyCriteria: issues
665
+ .filter((i) => i.type === "VERIFY_FAIL")
666
+ .map((i) => i.plain_explanation.replace(/실패|미충족/g, "충족"))
667
+ });
668
+ return {
669
+ action_type: "COPY_PASTE_TO_AGENT",
670
+ label: reviewResult === "FIX"
671
+ ? "수정이 필요합니다. 아래 내용을 AI에게 전달하세요"
672
+ : "심각한 문제가 있습니다. 아래 내용을 AI에게 전달하세요",
673
+ prompt_block: promptBlock
674
+ };
675
+ }
676
+ // Fallback
677
+ return {
678
+ action_type: "CONTINUE",
679
+ tool: "vibe_pm.create_work_order",
680
+ reason: "다음 작업 지시서를 받으세요"
681
+ };
682
+ }
683
+ function loadBridgeFileValidated(bridgePath) {
684
+ const content = fs.readFileSync(bridgePath, "utf-8");
685
+ const raw = parseBridgeData(content, bridgePath);
686
+ const validated = ClinicBridgeFileSchema.safeParse(raw);
687
+ if (!validated.success) {
688
+ throw new Error("작업 지시서 원본 파일 형식이 예상과 다릅니다. 업데이트 후 다시 시도해주세요.");
689
+ }
690
+ return validated.data;
691
+ }
692
+ function extractDoNotTouchPatternsFromBridge(bridgeDoc) {
693
+ const data = bridgeDoc;
694
+ const dg = data?.decision_guard;
695
+ if (Array.isArray(dg?.read_only_paths)) {
696
+ return dg.read_only_paths.filter((item) => typeof item === "string");
697
+ }
698
+ const legacy = data?.do_not_touch;
699
+ if (Array.isArray(legacy)) {
700
+ return legacy.filter((item) => typeof item === "string");
701
+ }
702
+ return [];
703
+ }
704
+ function extractAllowPathsFromBridge(bridgeDoc) {
705
+ const data = bridgeDoc;
706
+ const dg = data?.decision_guard;
707
+ if (Array.isArray(dg?.allow_paths)) {
708
+ return dg.allow_paths
709
+ .filter((item) => typeof item === "string")
710
+ .map((s) => normalizeAllowPath(s));
711
+ }
712
+ const scope = data?.scope;
713
+ if (scope && typeof scope === "object") {
714
+ const include = scope.include;
715
+ if (Array.isArray(include)) {
716
+ return include
717
+ .filter((item) => typeof item === "string")
718
+ .map((s) => normalizeAllowPath(s));
719
+ }
720
+ }
721
+ return [];
722
+ }
723
+ function normalizeAllowPath(value) {
724
+ let v = (value ?? "").trim().replace(/\\/g, "/");
725
+ if (v.startsWith("./"))
726
+ v = v.slice(2);
727
+ return v;
728
+ }
729
+ function findInvalidAllowPaths(allowPaths) {
730
+ if (!Array.isArray(allowPaths) || allowPaths.length === 0)
731
+ return [];
732
+ const allowedPrefixes = new Set(PATH_ZONES.GREEN.map((p) => {
733
+ const norm = normalizeAllowPath(p);
734
+ const idx = norm.indexOf("**");
735
+ const base = idx >= 0 ? norm.slice(0, idx) : norm;
736
+ return base.endsWith("/") ? base : base + "/";
737
+ }));
738
+ const out = [];
739
+ for (const raw of allowPaths) {
740
+ const p = normalizeAllowPath(raw);
741
+ if (!p)
742
+ continue;
743
+ // Minimal path traversal / absolute-path guard
744
+ if (p.includes("..") || /^[A-Za-z]:[\\/]/.test(p) || p.startsWith("/") || p.startsWith("\\\\")) {
745
+ out.push(p);
746
+ continue;
747
+ }
748
+ let ok = false;
749
+ for (const prefix of allowedPrefixes) {
750
+ if (p === prefix.slice(0, -1) || p.startsWith(prefix)) {
751
+ ok = true;
752
+ break;
753
+ }
754
+ }
755
+ if (!ok)
756
+ out.push(p);
757
+ }
758
+ return out;
759
+ }
760
+ function parseBridgeData(content, bridgePath) {
761
+ if (bridgePath.endsWith(".json")) {
762
+ return JSON.parse(content);
763
+ }
764
+ return parseYaml(content);
765
+ }
766
+ function collectModifiedFiles(result) {
767
+ const files = new Set();
768
+ if (Array.isArray(result.modified_files)) {
769
+ for (const file of result.modified_files) {
770
+ if (typeof file === "string" && file.length > 0) {
771
+ files.add(file);
772
+ }
773
+ }
774
+ }
775
+ if (result.scope_violations) {
776
+ for (const violation of result.scope_violations) {
777
+ if (typeof violation.file_path === "string" && violation.file_path.length > 0) {
778
+ files.add(violation.file_path);
779
+ }
780
+ }
781
+ }
782
+ if (result.issues) {
783
+ for (const issue of result.issues) {
784
+ if (typeof issue.file_path === "string" && issue.file_path.length > 0) {
785
+ files.add(issue.file_path);
786
+ }
787
+ }
788
+ }
789
+ return Array.from(files);
790
+ }
791
+ function escapeRegExp(value) {
792
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
793
+ }
794
+ /**
795
+ * Create error output for parsing/execution failures
796
+ */
797
+ function createErrorOutput(projectId, mode, errorMessage, stderr) {
798
+ const issues = [
799
+ {
800
+ type: "RISK_SPIKE",
801
+ business_risk: "검수를 완료할 수 없습니다",
802
+ plain_explanation: errorMessage,
803
+ technical_detail: stderr
804
+ ? {
805
+ file_path: "system",
806
+ rule_violated: stderr.slice(0, 200)
807
+ }
808
+ : undefined
809
+ }
810
+ ];
811
+ // P14: Error output uses BLOCK-01 (RISK_SPIKE triggers BLOCK-01)
812
+ const message_template_id = selectMessageTemplate("BLOCK", issues, false);
813
+ return {
814
+ project_id: projectId,
815
+ review_summary: {
816
+ decision_context: formatMode(mode),
817
+ review_result: "BLOCK",
818
+ one_line_verdict: "검수 중 오류가 발생했습니다"
819
+ },
820
+ issues_found: issues,
821
+ next_action: {
822
+ action_type: "ASK_DECISION",
823
+ tool: "vibe_pm.get_decision",
824
+ reason: "검수 오류로 인해 추가 결정이 필요합니다"
825
+ },
826
+ message_template_id
827
+ };
828
+ }