@vibecodetown/mcp-server 2.1.4 → 2.2.1
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/README.md +10 -10
- package/build/auth/credential_store.js +146 -0
- package/build/auth/public_key.js +6 -4
- package/build/bootstrap/doctor.js +113 -5
- package/build/bootstrap/installer.js +85 -15
- package/build/bootstrap/registry.js +11 -6
- package/build/bootstrap/skills-installer.js +365 -0
- package/build/control_plane/gate.js +52 -70
- package/build/dx/activity.js +26 -3
- package/build/engine.js +151 -0
- package/build/errors.js +107 -0
- package/build/generated/bridge_build_seed_input.js +2 -0
- package/build/generated/bridge_build_seed_output.js +2 -0
- package/build/generated/bridge_confirm_reference_input.js +2 -0
- package/build/generated/bridge_confirm_reference_output.js +2 -0
- package/build/generated/bridge_confirmed_reference_file.js +2 -0
- package/build/generated/bridge_generate_references_input.js +2 -0
- package/build/generated/bridge_generate_references_output.js +2 -0
- package/build/generated/bridge_references_file.js +2 -0
- package/build/generated/bridge_work_order_seed_file.js +2 -0
- package/build/generated/contracts_bundle_info.js +3 -3
- package/build/generated/index.js +14 -0
- package/build/generated/ingress_input.js +2 -0
- package/build/generated/ingress_output.js +2 -0
- package/build/generated/ingress_resolution_file.js +2 -0
- package/build/generated/ingress_summary_file.js +2 -0
- package/build/generated/message_template_id_mapping_file.js +2 -0
- package/build/generated/run_app_input.js +1 -1
- package/build/index.js +4 -1
- package/build/local-mode/git.js +36 -22
- package/build/local-mode/paths.js +1 -0
- package/build/local-mode/project-state.js +176 -0
- package/build/local-mode/setup.js +21 -1
- package/build/local-mode/templates.js +3 -3
- package/build/path-utils.js +68 -0
- package/build/runtime/cli_invoker.js +416 -0
- package/build/tools/vibe_pm/advisory_review.js +5 -3
- package/build/tools/vibe_pm/bridge_build_seed.js +164 -0
- package/build/tools/vibe_pm/bridge_confirm_reference.js +91 -0
- package/build/tools/vibe_pm/bridge_generate_references.js +258 -0
- package/build/tools/vibe_pm/briefing.js +26 -1
- package/build/tools/vibe_pm/context.js +79 -0
- package/build/tools/vibe_pm/create_work_order.js +200 -3
- package/build/tools/vibe_pm/doctor.js +95 -0
- package/build/tools/vibe_pm/entity_gate/preflight.js +8 -3
- package/build/tools/vibe_pm/export_output.js +14 -13
- package/build/tools/vibe_pm/finalize_work.js +74 -0
- package/build/tools/vibe_pm/force_override.js +104 -0
- package/build/tools/vibe_pm/get_decision.js +2 -2
- package/build/tools/vibe_pm/index.js +160 -3
- package/build/tools/vibe_pm/ingress.js +645 -0
- package/build/tools/vibe_pm/ingress_gate.js +116 -0
- package/build/tools/vibe_pm/inspect_code.js +90 -20
- package/build/tools/vibe_pm/kce/doc_usage.js +4 -9
- package/build/tools/vibe_pm/kce/on_finalize.js +2 -2
- package/build/tools/vibe_pm/kce/preflight.js +11 -7
- package/build/tools/vibe_pm/list_rules.js +135 -0
- package/build/tools/vibe_pm/memory_status.js +11 -8
- package/build/tools/vibe_pm/memory_sync.js +11 -8
- package/build/tools/vibe_pm/pm_language.js +17 -16
- package/build/tools/vibe_pm/pre_commit_analysis.js +292 -0
- package/build/tools/vibe_pm/publish_mcp.js +271 -0
- package/build/tools/vibe_pm/python_error.js +115 -0
- package/build/tools/vibe_pm/run_app.js +215 -86
- package/build/tools/vibe_pm/run_app_podman.js +64 -2
- package/build/tools/vibe_pm/save_rule.js +120 -0
- package/build/tools/vibe_pm/search_oss.js +5 -3
- package/build/tools/vibe_pm/spec_rag.js +185 -0
- package/build/tools/vibe_pm/status.js +50 -3
- package/build/tools/vibe_pm/submit_decision.js +2 -2
- package/build/tools/vibe_pm/types.js +28 -0
- package/build/tools/vibe_pm/undo_last_task.js +23 -20
- package/build/tools/vibe_pm/waiter_mapping.js +155 -0
- package/build/tools/vibe_pm/zoekt_evidence.js +5 -3
- package/build/tools.js +13 -5
- package/build/version-check.js +5 -5
- package/build/vibe-cli.js +742 -39
- package/package.json +5 -4
- package/skills/VRIP_INSTALL_MANIFEST_DOCTOR.skill.md +288 -0
- package/skills/index.json +14 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// adapters/mcp-ts/src/tools/vibe_pm/save_rule.ts
|
|
2
|
+
// vibe_pm.save_rule - 프로젝트 워크플로우 규칙 저장
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import * as fs from "fs/promises";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import { getVibeRepoPaths } from "../../local-mode/paths.js";
|
|
7
|
+
// ============================================================
|
|
8
|
+
// Schema
|
|
9
|
+
// ============================================================
|
|
10
|
+
export const saveRuleInputSchema = z.object({
|
|
11
|
+
base_path: z.string().describe("프로젝트 루트 경로"),
|
|
12
|
+
rule_title: z.string().min(1).describe("규칙 제목 (예: 'API 변경 시 문서 갱신')"),
|
|
13
|
+
trigger: z.string().min(1).describe("언제 적용되는지 (예: 'API 엔드포인트를 수정할 때마다')"),
|
|
14
|
+
action: z.string().min(1).describe("무엇을 해야 하는지 (예: 'docs/api.md를 함께 갱신한다')"),
|
|
15
|
+
example: z.string().optional().describe("구체적인 예시 (선택)")
|
|
16
|
+
}).strict();
|
|
17
|
+
export const saveRuleOutputSchema = z.object({
|
|
18
|
+
status: z.enum(["saved", "error"]),
|
|
19
|
+
message: z.string(),
|
|
20
|
+
rules_file: z.string().describe(".vibe/project_rules.md 경로"),
|
|
21
|
+
rule_count: z.number().describe("현재 저장된 규칙 수")
|
|
22
|
+
}).strict();
|
|
23
|
+
// ============================================================
|
|
24
|
+
// Implementation
|
|
25
|
+
// ============================================================
|
|
26
|
+
const RULES_FILENAME = "project_rules.md";
|
|
27
|
+
/**
|
|
28
|
+
* 규칙 파일 경로 반환
|
|
29
|
+
*/
|
|
30
|
+
function getRulesFilePath(basePath) {
|
|
31
|
+
const { vibeDir } = getVibeRepoPaths(basePath);
|
|
32
|
+
return path.join(vibeDir, RULES_FILENAME);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 규칙 파일 읽기 (없으면 기본 템플릿 반환)
|
|
36
|
+
*/
|
|
37
|
+
async function readRulesFile(filePath) {
|
|
38
|
+
try {
|
|
39
|
+
return await fs.readFile(filePath, "utf-8");
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// 파일이 없으면 기본 템플릿 반환
|
|
43
|
+
return `# Project Workflow Rules
|
|
44
|
+
|
|
45
|
+
> 이 파일은 Vibe PM이 자동으로 관리합니다.
|
|
46
|
+
> 프로젝트별 워크플로우 규칙이 여기에 저장됩니다.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 규칙 수 계산
|
|
55
|
+
*/
|
|
56
|
+
function countRules(content) {
|
|
57
|
+
const lines = content.split("\n");
|
|
58
|
+
let count = 0;
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
if (line.startsWith("## ") && !line.includes("Project Workflow Rules")) {
|
|
61
|
+
count++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return count;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 새 규칙을 마크다운 형식으로 포맷
|
|
68
|
+
*/
|
|
69
|
+
function formatRule(input) {
|
|
70
|
+
const timestamp = new Date().toISOString().split("T")[0];
|
|
71
|
+
let rule = `## ${input.rule_title}
|
|
72
|
+
|
|
73
|
+
- **언제**: ${input.trigger}
|
|
74
|
+
- **무엇을**: ${input.action}
|
|
75
|
+
`;
|
|
76
|
+
if (input.example) {
|
|
77
|
+
rule += `- **예시**: ${input.example}
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
rule += `- **등록일**: ${timestamp}
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
`;
|
|
85
|
+
return rule;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 규칙 저장
|
|
89
|
+
*/
|
|
90
|
+
export async function saveRule(input) {
|
|
91
|
+
const rulesFile = getRulesFilePath(input.base_path);
|
|
92
|
+
try {
|
|
93
|
+
// .vibe 디렉토리 확인/생성
|
|
94
|
+
const vibeDir = path.dirname(rulesFile);
|
|
95
|
+
await fs.mkdir(vibeDir, { recursive: true });
|
|
96
|
+
// 기존 규칙 읽기
|
|
97
|
+
let content = await readRulesFile(rulesFile);
|
|
98
|
+
// 새 규칙 추가
|
|
99
|
+
const newRule = formatRule(input);
|
|
100
|
+
content += newRule;
|
|
101
|
+
// 파일 저장
|
|
102
|
+
await fs.writeFile(rulesFile, content, "utf-8");
|
|
103
|
+
const ruleCount = countRules(content);
|
|
104
|
+
return {
|
|
105
|
+
status: "saved",
|
|
106
|
+
message: `규칙 '${input.rule_title}'이(가) 저장되었습니다.`,
|
|
107
|
+
rules_file: rulesFile,
|
|
108
|
+
rule_count: ruleCount
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
113
|
+
return {
|
|
114
|
+
status: "error",
|
|
115
|
+
message: `규칙 저장 실패: ${msg}`,
|
|
116
|
+
rules_file: rulesFile,
|
|
117
|
+
rule_count: 0
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// adapters/mcp-ts/src/tools/vibe_pm/search_oss.ts
|
|
2
2
|
// vibe_pm.search_oss - Search OSS repos (gh) and write evidence under runs/<run_id>/export/
|
|
3
3
|
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
-
import {
|
|
4
|
+
import { runPythonCli } from "../../engine.js";
|
|
5
5
|
import { safeJsonParse } from "../../cli.js";
|
|
6
6
|
import { validateToolInput } from "../../security/input-validator.js";
|
|
7
7
|
import { resolveProjectId, resolveRunId } from "./context.js";
|
|
8
|
+
import { classifyPythonError } from "./python_error.js";
|
|
8
9
|
function toCsv(values) {
|
|
9
10
|
return values
|
|
10
11
|
.map((v) => (typeof v === "string" ? v.trim() : ""))
|
|
@@ -64,9 +65,10 @@ export async function searchOss(input) {
|
|
|
64
65
|
const writeEvidence = input?.write_evidence !== false;
|
|
65
66
|
if (!writeEvidence)
|
|
66
67
|
cmd.push("--no-write-evidence");
|
|
67
|
-
const res = await
|
|
68
|
+
const res = await runPythonCli(cmd, { timeoutMs: 180_000 });
|
|
68
69
|
if (res.code !== 0) {
|
|
69
|
-
|
|
70
|
+
const classified = classifyPythonError(res.stderr ?? "", res.code, "OSS 검색");
|
|
71
|
+
throw new Error(`${classified.issueCode}: ${classified.summary}`);
|
|
70
72
|
}
|
|
71
73
|
const parsed = safeJsonParse(res.stdout);
|
|
72
74
|
if (!parsed.ok) {
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// adapters/mcp-ts/src/tools/vibe_pm/spec_rag.ts
|
|
2
|
+
// Spec RAG: Auto-inject SSOT context into work orders
|
|
3
|
+
// Reference: docs/DEV_SPEC/0124/SPEC_RAG_WORK_PROTOCOL
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { runKceRetrieve, runKceStatus } from "./kce/preflight.js";
|
|
7
|
+
/**
|
|
8
|
+
* Priority-ordered list of SSOT documents to inject
|
|
9
|
+
*/
|
|
10
|
+
const SPEC_DOCS = [
|
|
11
|
+
"docs/ssot/DECISION_SSOT.md",
|
|
12
|
+
"docs/ssot/QUALITY_SSOT.md",
|
|
13
|
+
"docs/ssot/MCP_SSOT.md",
|
|
14
|
+
"docs/SSOT.md"
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* Get spec context for injection into work orders.
|
|
18
|
+
*
|
|
19
|
+
* Strategy:
|
|
20
|
+
* 1. Try KCE retrieval (if status=READY)
|
|
21
|
+
* 2. Fallback to direct file reading
|
|
22
|
+
* 3. Return empty if both fail
|
|
23
|
+
*/
|
|
24
|
+
export async function getSpecContext(opts) {
|
|
25
|
+
const maxChars = opts.maxChars ?? 2500;
|
|
26
|
+
const maxItems = opts.maxItems ?? 4;
|
|
27
|
+
const basePath = opts.basePath ?? process.cwd();
|
|
28
|
+
// Strategy 1: Try KCE retrieval
|
|
29
|
+
try {
|
|
30
|
+
const status = await runKceStatus();
|
|
31
|
+
if (status.readiness_state === "READY") {
|
|
32
|
+
const result = await runKceRetrieve({
|
|
33
|
+
query: opts.query,
|
|
34
|
+
maxItems,
|
|
35
|
+
limit: 10,
|
|
36
|
+
maxTotalChars: maxChars,
|
|
37
|
+
maxChunkChars: Math.min(800, Math.floor(maxChars / maxItems)),
|
|
38
|
+
pathPrefix: "docs/ssot"
|
|
39
|
+
});
|
|
40
|
+
if (result.context_block.trim()) {
|
|
41
|
+
const referencedDocs = (result.hits ?? [])
|
|
42
|
+
.map((h) => h.path)
|
|
43
|
+
.filter((p) => p.endsWith(".md"));
|
|
44
|
+
return {
|
|
45
|
+
context_block: buildSpecBlock(result.context_block, referencedDocs),
|
|
46
|
+
source: "kce",
|
|
47
|
+
referenced_docs: referencedDocs
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// KCE unavailable, fall through to fallback
|
|
54
|
+
}
|
|
55
|
+
// Strategy 2: Fallback to direct file reading
|
|
56
|
+
try {
|
|
57
|
+
const { content, docs } = readSpecDocsFallback(basePath, maxChars);
|
|
58
|
+
if (content.trim()) {
|
|
59
|
+
return {
|
|
60
|
+
context_block: buildSpecBlock(content, docs),
|
|
61
|
+
source: "fallback",
|
|
62
|
+
referenced_docs: docs
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Fallback also failed
|
|
68
|
+
}
|
|
69
|
+
// Strategy 3: Return empty
|
|
70
|
+
return {
|
|
71
|
+
context_block: "",
|
|
72
|
+
source: "empty",
|
|
73
|
+
referenced_docs: []
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Build the [SPEC_CONTEXT] block for injection
|
|
78
|
+
*/
|
|
79
|
+
export function buildSpecBlock(content, docs) {
|
|
80
|
+
if (!content.trim())
|
|
81
|
+
return "";
|
|
82
|
+
const lines = [];
|
|
83
|
+
lines.push("[SPEC_CONTEXT]");
|
|
84
|
+
lines.push(content.trim());
|
|
85
|
+
if (docs.length > 0) {
|
|
86
|
+
lines.push("");
|
|
87
|
+
lines.push("[참조 문서]");
|
|
88
|
+
for (const doc of docs.slice(0, 6)) {
|
|
89
|
+
const basename = path.basename(doc);
|
|
90
|
+
lines.push(`- ${basename}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Fallback: Read spec docs directly from filesystem
|
|
97
|
+
*/
|
|
98
|
+
function readSpecDocsFallback(basePath, maxChars) {
|
|
99
|
+
const chunks = [];
|
|
100
|
+
const docs = [];
|
|
101
|
+
let totalChars = 0;
|
|
102
|
+
const perDocLimit = Math.floor(maxChars / SPEC_DOCS.length);
|
|
103
|
+
for (const docPath of SPEC_DOCS) {
|
|
104
|
+
if (totalChars >= maxChars)
|
|
105
|
+
break;
|
|
106
|
+
const fullPath = path.join(basePath, docPath);
|
|
107
|
+
if (!fs.existsSync(fullPath))
|
|
108
|
+
continue;
|
|
109
|
+
try {
|
|
110
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
111
|
+
const extracted = extractRelevantSections(content, perDocLimit);
|
|
112
|
+
if (extracted.trim()) {
|
|
113
|
+
const basename = path.basename(docPath);
|
|
114
|
+
chunks.push(`### ${basename}\n${extracted.trim()}`);
|
|
115
|
+
docs.push(docPath);
|
|
116
|
+
totalChars += extracted.length;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Skip unreadable files
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
content: chunks.join("\n\n"),
|
|
125
|
+
docs
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Extract relevant sections from a markdown document
|
|
130
|
+
* Focus on rules, constraints, and key definitions
|
|
131
|
+
*/
|
|
132
|
+
function extractRelevantSections(content, maxChars) {
|
|
133
|
+
// Priority headers to extract
|
|
134
|
+
const priorityPatterns = [
|
|
135
|
+
/^#+\s*(?:규칙|Rules|제약|Constraints|필수|Required|금지|Prohibited)/im,
|
|
136
|
+
/^#+\s*(?:개요|Overview|핵심|Core|원칙|Principles)/im,
|
|
137
|
+
/^#+\s*(?:정의|Definitions|용어|Terms)/im
|
|
138
|
+
];
|
|
139
|
+
const lines = content.split("\n");
|
|
140
|
+
const sections = [];
|
|
141
|
+
let currentSection = [];
|
|
142
|
+
let inPrioritySection = false;
|
|
143
|
+
let sectionHeader = "";
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
// Check if this is a header
|
|
146
|
+
const isHeader = /^#+\s+/.test(line);
|
|
147
|
+
if (isHeader) {
|
|
148
|
+
// Save previous section if it was a priority section
|
|
149
|
+
if (inPrioritySection && currentSection.length > 0) {
|
|
150
|
+
sections.push([sectionHeader, ...currentSection].join("\n"));
|
|
151
|
+
}
|
|
152
|
+
// Check if this header is a priority section
|
|
153
|
+
inPrioritySection = priorityPatterns.some((p) => p.test(line));
|
|
154
|
+
sectionHeader = line;
|
|
155
|
+
currentSection = [];
|
|
156
|
+
}
|
|
157
|
+
else if (inPrioritySection) {
|
|
158
|
+
currentSection.push(line);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Don't forget the last section
|
|
162
|
+
if (inPrioritySection && currentSection.length > 0) {
|
|
163
|
+
sections.push([sectionHeader, ...currentSection].join("\n"));
|
|
164
|
+
}
|
|
165
|
+
// If no priority sections found, extract the first N characters as overview
|
|
166
|
+
if (sections.length === 0) {
|
|
167
|
+
const overview = content.slice(0, Math.min(maxChars, 500)).trim();
|
|
168
|
+
if (overview) {
|
|
169
|
+
// Try to end at a sentence boundary
|
|
170
|
+
const lastPeriod = overview.lastIndexOf(".");
|
|
171
|
+
const lastNewline = overview.lastIndexOf("\n");
|
|
172
|
+
const cutoff = Math.max(lastPeriod, lastNewline);
|
|
173
|
+
return cutoff > 100 ? overview.slice(0, cutoff + 1) : overview;
|
|
174
|
+
}
|
|
175
|
+
return "";
|
|
176
|
+
}
|
|
177
|
+
// Combine sections up to maxChars
|
|
178
|
+
let result = "";
|
|
179
|
+
for (const section of sections) {
|
|
180
|
+
if (result.length + section.length > maxChars)
|
|
181
|
+
break;
|
|
182
|
+
result += (result ? "\n\n" : "") + section;
|
|
183
|
+
}
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// adapters/mcp-ts/src/tools/vibe_pm/status.ts
|
|
2
2
|
// vibe_pm.status - Project status query
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
3
5
|
import { runEngineWithCache } from "../../engine.js";
|
|
4
6
|
import { safeJsonParse } from "../../cli.js";
|
|
5
7
|
import { resolveRunId, resolveProjectId, getRunContext, readRunState } from "./context.js";
|
|
6
8
|
import { formatMode, formatPhase, signalToReview } from "./pm_language.js";
|
|
7
9
|
import { validateToolInput } from "../../security/input-validator.js";
|
|
8
10
|
import { SpecHighValidateOutputSchema } from "../../generated/spec_high_validate_output.js";
|
|
11
|
+
import { IngressSummaryFileSchema } from "../../generated/ingress_summary_file.js";
|
|
9
12
|
/**
|
|
10
13
|
* vibe_pm.status - Query current project status
|
|
11
14
|
*
|
|
@@ -13,8 +16,7 @@ import { SpecHighValidateOutputSchema } from "../../generated/spec_high_validate
|
|
|
13
16
|
* → spec-high validate <run_id>
|
|
14
17
|
* → status summary generation
|
|
15
18
|
*/
|
|
16
|
-
export async function status(input) {
|
|
17
|
-
const basePath = process.cwd();
|
|
19
|
+
export async function status(input, basePath = process.cwd()) {
|
|
18
20
|
validateToolInput({ project_id: input.project_id });
|
|
19
21
|
// Resolve run_id
|
|
20
22
|
const { run_id, is_new } = resolveRunId(input.project_id, basePath);
|
|
@@ -77,7 +79,17 @@ export async function status(input) {
|
|
|
77
79
|
// Collect risks
|
|
78
80
|
const risks = validateResult?.risks ?? [];
|
|
79
81
|
// Determine next action based on state
|
|
80
|
-
|
|
82
|
+
let nextAction = determineNextAction(phase, decisionsPending, lastReviewStatus);
|
|
83
|
+
// P27: deterministic auto-routing for "pre-work clarification" (based on ingress signals)
|
|
84
|
+
const routed = maybeAutoRouteForClarification({
|
|
85
|
+
runDirAbs: context.runs_path,
|
|
86
|
+
phase,
|
|
87
|
+
decisionsPending
|
|
88
|
+
});
|
|
89
|
+
if (routed) {
|
|
90
|
+
risks.push(routed.risk_note);
|
|
91
|
+
nextAction = routed.next_action;
|
|
92
|
+
}
|
|
81
93
|
return {
|
|
82
94
|
project_id,
|
|
83
95
|
project: {
|
|
@@ -95,6 +107,41 @@ export async function status(input) {
|
|
|
95
107
|
next_action: nextAction
|
|
96
108
|
};
|
|
97
109
|
}
|
|
110
|
+
function maybeAutoRouteForClarification(args) {
|
|
111
|
+
// Never override when user still has pending approvals.
|
|
112
|
+
if (args.decisionsPending > 0)
|
|
113
|
+
return null;
|
|
114
|
+
// Only relevant before work order is issued.
|
|
115
|
+
if (!(args.phase === "decision" || args.phase === "briefing" || args.phase === "init"))
|
|
116
|
+
return null;
|
|
117
|
+
const summaryAbs = path.join(args.runDirAbs, "ingress", "ingress_summary.json");
|
|
118
|
+
if (!fs.existsSync(summaryAbs))
|
|
119
|
+
return null;
|
|
120
|
+
let raw;
|
|
121
|
+
try {
|
|
122
|
+
raw = JSON.parse(fs.readFileSync(summaryAbs, "utf-8"));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const parsed = IngressSummaryFileSchema.safeParse(raw);
|
|
128
|
+
if (!parsed.success)
|
|
129
|
+
return null;
|
|
130
|
+
const recommended = parsed.data.high_level.clarification_recommended === true;
|
|
131
|
+
if (!recommended)
|
|
132
|
+
return null;
|
|
133
|
+
// Treat as completed if seed exists (protocol done).
|
|
134
|
+
const seedAbs = path.join(args.runDirAbs, "bridge", "work_order_seed.json");
|
|
135
|
+
if (fs.existsSync(seedAbs))
|
|
136
|
+
return null;
|
|
137
|
+
return {
|
|
138
|
+
next_action: {
|
|
139
|
+
tool: "vibe_pm.bridge_generate_references",
|
|
140
|
+
label: "다음 할 일: 예시를 고르고 작업 방향을 고정하기"
|
|
141
|
+
},
|
|
142
|
+
risk_note: "작업 방향이 아직 충분히 고정되지 않았습니다. 예시를 한 번 선택하면 다음 단계가 빨라집니다."
|
|
143
|
+
};
|
|
144
|
+
}
|
|
98
145
|
function determineNextAction(phase, decisionsPending, lastReviewStatus) {
|
|
99
146
|
// If there are pending decisions
|
|
100
147
|
if (decisionsPending > 0) {
|
|
@@ -20,8 +20,8 @@ export async function submitDecision(input) {
|
|
|
20
20
|
const project_id = resolveProjectId(run_id, basePath);
|
|
21
21
|
// Format answer text (combine choice with note if provided)
|
|
22
22
|
const answerText = input.note ? `${input.choice}: ${input.note}` : input.choice;
|
|
23
|
-
// Step 1: Submit answer
|
|
24
|
-
const answerResult = await runEngine("
|
|
23
|
+
// Step 1: Submit answer via spec-high
|
|
24
|
+
const answerResult = await runEngine("spec-high", ["--root", "engines/spec_high", "answer", run_id, "--text", answerText], { timeoutMs: 120_000 });
|
|
25
25
|
if (answerResult.code !== 0) {
|
|
26
26
|
throw new Error(`결재 제출 실패: ${answerResult.stderr || "알 수 없는 오류"}`);
|
|
27
27
|
}
|
|
@@ -183,6 +183,14 @@ import { SearchOssInputSchema } from "../../generated/search_oss_input.js";
|
|
|
183
183
|
import { SearchOssOutputSchema } from "../../generated/search_oss_output.js";
|
|
184
184
|
import { ZoektEvidenceInputSchema } from "../../generated/zoekt_evidence_input.js";
|
|
185
185
|
import { ZoektEvidenceOutputSchema } from "../../generated/zoekt_evidence_output.js";
|
|
186
|
+
import { IngressInputSchema } from "../../generated/ingress_input.js";
|
|
187
|
+
import { IngressOutputSchema } from "../../generated/ingress_output.js";
|
|
188
|
+
import { BridgeGenerateReferencesInputSchema } from "../../generated/bridge_generate_references_input.js";
|
|
189
|
+
import { BridgeGenerateReferencesOutputSchema } from "../../generated/bridge_generate_references_output.js";
|
|
190
|
+
import { BridgeConfirmReferenceInputSchema } from "../../generated/bridge_confirm_reference_input.js";
|
|
191
|
+
import { BridgeConfirmReferenceOutputSchema } from "../../generated/bridge_confirm_reference_output.js";
|
|
192
|
+
import { BridgeBuildSeedInputSchema } from "../../generated/bridge_build_seed_input.js";
|
|
193
|
+
import { BridgeBuildSeedOutputSchema } from "../../generated/bridge_build_seed_output.js";
|
|
186
194
|
export const finalizeWorkInputSchema = FinalizeWorkInputSchema;
|
|
187
195
|
// Output schema is generated from JSON Schema SSOT.
|
|
188
196
|
export const finalizeWorkOutputSchema = FinalizeWorkOutputSchema;
|
|
@@ -222,6 +230,26 @@ export const zoektEvidenceInputSchema = ZoektEvidenceInputSchema;
|
|
|
222
230
|
// Output schema is generated from JSON Schema SSOT.
|
|
223
231
|
export const zoektEvidenceOutputSchema = ZoektEvidenceOutputSchema;
|
|
224
232
|
// ============================================================
|
|
233
|
+
// vibe_pm.ingress
|
|
234
|
+
// ============================================================
|
|
235
|
+
export const ingressInputSchema = IngressInputSchema;
|
|
236
|
+
export const ingressOutputSchema = IngressOutputSchema;
|
|
237
|
+
// ============================================================
|
|
238
|
+
// vibe_pm.bridge_generate_references
|
|
239
|
+
// ============================================================
|
|
240
|
+
export const bridgeGenerateReferencesInputSchema = BridgeGenerateReferencesInputSchema;
|
|
241
|
+
export const bridgeGenerateReferencesOutputSchema = BridgeGenerateReferencesOutputSchema;
|
|
242
|
+
// ============================================================
|
|
243
|
+
// vibe_pm.bridge_confirm_reference
|
|
244
|
+
// ============================================================
|
|
245
|
+
export const bridgeConfirmReferenceInputSchema = BridgeConfirmReferenceInputSchema;
|
|
246
|
+
export const bridgeConfirmReferenceOutputSchema = BridgeConfirmReferenceOutputSchema;
|
|
247
|
+
// ============================================================
|
|
248
|
+
// vibe_pm.bridge_build_seed
|
|
249
|
+
// ============================================================
|
|
250
|
+
export const bridgeBuildSeedInputSchema = BridgeBuildSeedInputSchema;
|
|
251
|
+
export const bridgeBuildSeedOutputSchema = BridgeBuildSeedOutputSchema;
|
|
252
|
+
// ============================================================
|
|
225
253
|
// vibe_pm.doctor / vibe_pm.update (Output SSOT only)
|
|
226
254
|
// ============================================================
|
|
227
255
|
export const doctorOutputSchema = DoctorOutputSchema;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// adapters/mcp-ts/src/tools/vibe_pm/undo_last_task.ts
|
|
2
2
|
// vibe_pm.undo_last_task - Time machine rollback via git revert
|
|
3
|
+
//
|
|
4
|
+
// All subprocess invocations go through cli_invoker for centralized management.
|
|
3
5
|
import * as fs from "node:fs";
|
|
4
6
|
import * as path from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
const execAsync = promisify(exec);
|
|
7
|
+
import { invokeGit } from "../../runtime/cli_invoker.js";
|
|
8
|
+
import { isGitConflictError } from "../../errors.js";
|
|
8
9
|
/**
|
|
9
10
|
* vibe_pm.undo_last_task - Rollback recent commits via git revert
|
|
10
11
|
*
|
|
@@ -43,24 +44,25 @@ export async function undoLastTask(input) {
|
|
|
43
44
|
const revertedTasks = [];
|
|
44
45
|
const revertErrors = [];
|
|
45
46
|
for (const commit of commits) {
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
const result = await invokeGit(["revert", "--no-edit", commit.hash], basePath);
|
|
48
|
+
if (result.exitCode === 0) {
|
|
48
49
|
revertedTasks.push({
|
|
49
50
|
commit_hash: commit.shortHash,
|
|
50
51
|
task_summary: commit.subject,
|
|
51
52
|
timestamp: commit.timestamp
|
|
52
53
|
});
|
|
53
54
|
}
|
|
54
|
-
|
|
55
|
-
const errMsg =
|
|
55
|
+
else {
|
|
56
|
+
const errMsg = result.stderr || result.stdout;
|
|
56
57
|
revertErrors.push(`${commit.shortHash}: ${errMsg}`);
|
|
57
58
|
// If revert fails due to conflicts, abort and stop
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
//
|
|
59
|
+
// Check for conflict indicators (exit code 1 with conflict message)
|
|
60
|
+
// P1-4: Use centralized error pattern
|
|
61
|
+
if (isGitConflictError(errMsg) || result.exitCode === 1) {
|
|
62
|
+
const abortResult = await invokeGit(["revert", "--abort"], basePath);
|
|
63
|
+
if (abortResult.exitCode !== 0) {
|
|
64
|
+
// Abort failed - critical state inconsistency
|
|
65
|
+
revertErrors.push(`[CRITICAL] git revert --abort 실패. 수동 복구 필요: git status 확인 후 git revert --abort 또는 git revert --continue 실행`);
|
|
64
66
|
}
|
|
65
67
|
break;
|
|
66
68
|
}
|
|
@@ -99,9 +101,12 @@ export async function undoLastTask(input) {
|
|
|
99
101
|
* Get recent commits from git log
|
|
100
102
|
*/
|
|
101
103
|
async function getRecentCommits(basePath, count) {
|
|
102
|
-
const
|
|
104
|
+
const result = await invokeGit(["log", "--oneline", "-n", String(count), "--format=%H|%h|%s|%ci"], basePath);
|
|
105
|
+
if (result.exitCode !== 0) {
|
|
106
|
+
throw new Error(result.stderr || "git log failed");
|
|
107
|
+
}
|
|
103
108
|
const commits = [];
|
|
104
|
-
const lines = stdout.trim().split("\n").filter(Boolean);
|
|
109
|
+
const lines = result.stdout.trim().split("\n").filter(Boolean);
|
|
105
110
|
for (const line of lines) {
|
|
106
111
|
const [hash, shortHash, subject, timestamp] = line.split("|");
|
|
107
112
|
if (hash && shortHash && subject) {
|
|
@@ -119,13 +124,11 @@ async function getRecentCommits(basePath, count) {
|
|
|
119
124
|
* Get current HEAD commit hash
|
|
120
125
|
*/
|
|
121
126
|
async function getCurrentCommit(basePath) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return stdout.trim();
|
|
125
|
-
}
|
|
126
|
-
catch {
|
|
127
|
+
const result = await invokeGit(["rev-parse", "--short", "HEAD"], basePath);
|
|
128
|
+
if (result.exitCode !== 0) {
|
|
127
129
|
return "unknown";
|
|
128
130
|
}
|
|
131
|
+
return result.stdout.trim();
|
|
129
132
|
}
|
|
130
133
|
/**
|
|
131
134
|
* Add rollback entry to DEV_LOG
|