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