vibe-gate-mcp 0.1.4 → 0.1.5
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/CHANGELOG.md +12 -0
- package/README.md +3 -0
- package/dist/index.mjs +244 -113
- package/docs/ROADMAP.md +19 -0
- package/docs/SEMANTIC_DIFF_PAYLOAD.md +9 -0
- package/docs/USAGE.md +2 -0
- package/docs/project/api/mcp-tools.md +7 -3
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.5] - 2026-09-23
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Enforce bounded review inputs, close the conflict loop as deadlocked after a rejected third round, and avoid duplicating or persisting source payloads between rounds.
|
|
15
|
+
- Load Critic `REQUEST:` paths from the workspace with path, file-count, file-size, line-count, and total-context limits; reject traversal and symlink escapes in changed-file reads.
|
|
16
|
+
- Use the OpenAI Responses API so current reasoning models work without model-specific request parameters or a Vibe-Gate model allowlist.
|
|
17
|
+
|
|
18
|
+
### Docs
|
|
19
|
+
|
|
20
|
+
- Document the review limits and requested-context behavior, and add a release checklist and scope review.
|
|
21
|
+
|
|
10
22
|
## [0.1.4] - 2026-09-21
|
|
11
23
|
|
|
12
24
|
### Added
|
package/README.md
CHANGED
|
@@ -45,6 +45,8 @@ OPENAI_API_KEY=YOUR_OPENAI_API_KEY
|
|
|
45
45
|
|
|
46
46
|
`opencode` is still the separate Zen/Go HTTP provider and needs `OPENCODE_API_KEY`. `opencode-cli` runs the local CLI and requires a `provider/model` value in `CRITIC_MODEL`; see the CLI guide for details.
|
|
47
47
|
|
|
48
|
+
`CRITIC_MODEL` is passed to the selected provider or CLI without a Vibe-Gate model allowlist; the provider must support that model ID. The OpenAI API provider uses the Responses API. See [provider configuration](docs/USAGE.md#configuration--providers).
|
|
49
|
+
|
|
48
50
|
See [CLI provider setup and alternatives](docs/CLI_PROVIDERS.md) for CLI installation, login, configuration, OpenCode session details, and other candidates we evaluated. Full variable list: [docs/project/VARIABLES.md](docs/project/VARIABLES.md).
|
|
49
51
|
|
|
50
52
|
### 2. Configure Cursor MCP (any consumer repo)
|
|
@@ -122,6 +124,7 @@ Probes: `updateStatus: false` or `phaseId` prefixes `mcp-smoke-` / `vibe-gate-pr
|
|
|
122
124
|
| [docs/USAGE.md](docs/USAGE.md) | First run and providers |
|
|
123
125
|
| [docs/CLI_PROVIDERS.md](docs/CLI_PROVIDERS.md) | Local CLI providers |
|
|
124
126
|
| [docs/SEMANTIC_DIFF_PAYLOAD.md](docs/SEMANTIC_DIFF_PAYLOAD.md) | `files[]` contract |
|
|
127
|
+
| [docs/ROADMAP.md](docs/ROADMAP.md) | Release checklist |
|
|
125
128
|
| [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) | Stale MCP, path errors |
|
|
126
129
|
| [docs/project/VARIABLES.md](docs/project/VARIABLES.md) | Env SSoT |
|
|
127
130
|
| [examples/](examples/) | Cursor mcp.json templates |
|
package/dist/index.mjs
CHANGED
|
@@ -395,6 +395,18 @@ const SEMANTIC_DIFF_SOURCE_FILES = {
|
|
|
395
395
|
MAX_BYTES_PER_FILE: 1048576,
|
|
396
396
|
MAX_TOTAL_BYTES: 5242880
|
|
397
397
|
};
|
|
398
|
+
/** MCP review input bounds; apply size checks again after files are read. */
|
|
399
|
+
const REVIEW_INPUT_LIMITS = {
|
|
400
|
+
MAX_PHASE_ID_CHARS: 256,
|
|
401
|
+
MAX_REPORT_CHARS: 5e4,
|
|
402
|
+
MAX_SEMANTIC_DIFF_CHARS: 5e5,
|
|
403
|
+
MAX_PATH_CHARS: 1024,
|
|
404
|
+
MAX_DEPENDENCIES: 100,
|
|
405
|
+
MAX_DEPENDENCY_NAME_CHARS: 256,
|
|
406
|
+
MAX_DEBT_SUBJECT_CHARS: 200,
|
|
407
|
+
MAX_DEBT_RATIONALE_CHARS: 4e3,
|
|
408
|
+
MAX_REQUESTED_CONTEXT_LINES: 120
|
|
409
|
+
};
|
|
398
410
|
/**
|
|
399
411
|
* Status.json write policy on ACCEPT.
|
|
400
412
|
* Probe phaseIds must not pollute consumer `.vibe/status.json` unless updateStatus:true.
|
|
@@ -693,7 +705,6 @@ const concernVerificationSchema = z.object({
|
|
|
693
705
|
const reviewRoundSchema = z.object({
|
|
694
706
|
round: z.number(),
|
|
695
707
|
report: z.string(),
|
|
696
|
-
semanticDiff: z.string().optional(),
|
|
697
708
|
verdict: z.string(),
|
|
698
709
|
criticResponse: z.string(),
|
|
699
710
|
concerns: z.array(concernSchema).optional(),
|
|
@@ -1847,7 +1858,10 @@ async function verifyCanonicalPathUnderWorkspace(workspaceRoot, absolutePath, pa
|
|
|
1847
1858
|
code: "PATH_OUTSIDE_WORKSPACE",
|
|
1848
1859
|
message: `${pathKind} resolves outside VIBE_WORKSPACE_ROOT after canonical resolution.`
|
|
1849
1860
|
};
|
|
1850
|
-
return {
|
|
1861
|
+
return {
|
|
1862
|
+
ok: true,
|
|
1863
|
+
canonicalPath: fileReal
|
|
1864
|
+
};
|
|
1851
1865
|
}
|
|
1852
1866
|
/**
|
|
1853
1867
|
* Resolve a user-supplied relative path to an absolute path confined to workspaceRoot.
|
|
@@ -1946,7 +1960,7 @@ async function loadSemanticDiffFromWorkspacePath(workspaceRoot, userRelativePath
|
|
|
1946
1960
|
};
|
|
1947
1961
|
let raw;
|
|
1948
1962
|
try {
|
|
1949
|
-
raw = await readFile(
|
|
1963
|
+
raw = await readFile(canonical.canonicalPath, "utf8");
|
|
1950
1964
|
} catch {
|
|
1951
1965
|
return {
|
|
1952
1966
|
ok: false,
|
|
@@ -1998,9 +2012,10 @@ function mapPathError(code, message) {
|
|
|
1998
2012
|
message
|
|
1999
2013
|
};
|
|
2000
2014
|
}
|
|
2001
|
-
function
|
|
2015
|
+
function formatSemanticDiffFileBlock(relativePath, content, truncated = false) {
|
|
2002
2016
|
const body = content.endsWith("\n") ? content : `${content}\n`;
|
|
2003
|
-
|
|
2017
|
+
const note = truncated ? `[Context limited to ${REVIEW_INPUT_LIMITS.MAX_REQUESTED_CONTEXT_LINES} lines.]\n` : "";
|
|
2018
|
+
return `${SEMANTIC_DIFF_PAYLOAD_MARKERS.FILE_LINE_PREFIX}${relativePath}\n${SEMANTIC_DIFF_PAYLOAD_MARKERS.CONTENT_LINE}\n${body}${note}`;
|
|
2004
2019
|
}
|
|
2005
2020
|
function validateFilesArray(paths) {
|
|
2006
2021
|
if (paths.length === 0) return {
|
|
@@ -2061,7 +2076,8 @@ async function assertReadableSourceFile(workspaceRoot, relativePath, absolutePat
|
|
|
2061
2076
|
};
|
|
2062
2077
|
return {
|
|
2063
2078
|
ok: true,
|
|
2064
|
-
nextTotalBytes
|
|
2079
|
+
nextTotalBytes,
|
|
2080
|
+
canonicalPath: canonical.canonicalPath
|
|
2065
2081
|
};
|
|
2066
2082
|
}
|
|
2067
2083
|
async function readSourceFileContent(absolutePath, relativePath) {
|
|
@@ -2104,9 +2120,9 @@ async function buildSemanticDiffFromSourceFiles(workspaceRoot, relativePaths) {
|
|
|
2104
2120
|
const sizeCheck = await assertReadableSourceFile(workspaceRoot, relativePath, pathResult.absolutePath, totalBytes);
|
|
2105
2121
|
if (!sizeCheck.ok) return sizeCheck.result;
|
|
2106
2122
|
totalBytes = sizeCheck.nextTotalBytes;
|
|
2107
|
-
const read = await readSourceFileContent(
|
|
2123
|
+
const read = await readSourceFileContent(sizeCheck.canonicalPath, relativePath);
|
|
2108
2124
|
if (!read.ok) return read.result;
|
|
2109
|
-
blocks.push(
|
|
2125
|
+
blocks.push(formatSemanticDiffFileBlock(relativePath, read.content));
|
|
2110
2126
|
filesLoaded.push(relativePath);
|
|
2111
2127
|
}
|
|
2112
2128
|
return {
|
|
@@ -2120,32 +2136,27 @@ async function buildSemanticDiffFromSourceFiles(workspaceRoot, relativePaths) {
|
|
|
2120
2136
|
/**
|
|
2121
2137
|
* OpenAI API integration.
|
|
2122
2138
|
*/
|
|
2123
|
-
|
|
2124
|
-
user: "user",
|
|
2125
|
-
assistant: "assistant",
|
|
2126
|
-
system: "system"
|
|
2127
|
-
};
|
|
2128
|
-
function toOpenAIMessages$1(messages) {
|
|
2139
|
+
function toOpenAIInput(messages) {
|
|
2129
2140
|
return messages.map((m) => ({
|
|
2130
|
-
role:
|
|
2141
|
+
role: m.role,
|
|
2131
2142
|
content: m.content
|
|
2132
2143
|
}));
|
|
2133
2144
|
}
|
|
2134
2145
|
function createOpenAIProvider(apiKey, model) {
|
|
2135
2146
|
const client = new OpenAI({ apiKey });
|
|
2136
2147
|
return { async complete(messages) {
|
|
2137
|
-
const
|
|
2148
|
+
const response = await client.responses.create({
|
|
2138
2149
|
model,
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
messages: toOpenAIMessages$1(messages)
|
|
2150
|
+
max_output_tokens: LLM_MAX_TOKENS,
|
|
2151
|
+
input: toOpenAIInput(messages)
|
|
2142
2152
|
});
|
|
2153
|
+
const usage = response.usage ? {
|
|
2154
|
+
promptTokens: response.usage.input_tokens,
|
|
2155
|
+
completionTokens: response.usage.output_tokens
|
|
2156
|
+
} : void 0;
|
|
2143
2157
|
return {
|
|
2144
|
-
content:
|
|
2145
|
-
usage
|
|
2146
|
-
promptTokens: completion.usage.prompt_tokens,
|
|
2147
|
-
completionTokens: completion.usage.completion_tokens
|
|
2148
|
-
} : void 0
|
|
2158
|
+
content: response.output_text,
|
|
2159
|
+
usage
|
|
2149
2160
|
};
|
|
2150
2161
|
} };
|
|
2151
2162
|
}
|
|
@@ -3345,6 +3356,57 @@ function parseVerificationsFromResponse(response, existingConcerns) {
|
|
|
3345
3356
|
}
|
|
3346
3357
|
return verifications;
|
|
3347
3358
|
}
|
|
3359
|
+
function splitRequestedPath(part) {
|
|
3360
|
+
let remainder = part.trim();
|
|
3361
|
+
if (!remainder) return null;
|
|
3362
|
+
if (remainder.startsWith("\"") || remainder.startsWith("'")) {
|
|
3363
|
+
const quote = remainder[0];
|
|
3364
|
+
const closingQuote = remainder.indexOf(quote, 1);
|
|
3365
|
+
if (closingQuote < 2) return null;
|
|
3366
|
+
const filePath = remainder.slice(1, closingQuote);
|
|
3367
|
+
remainder = remainder.slice(closingQuote + 1).trim();
|
|
3368
|
+
return {
|
|
3369
|
+
filePath,
|
|
3370
|
+
remainder
|
|
3371
|
+
};
|
|
3372
|
+
}
|
|
3373
|
+
const firstSpace = remainder.search(/\s/);
|
|
3374
|
+
const filePath = firstSpace === -1 ? remainder : remainder.slice(0, firstSpace);
|
|
3375
|
+
remainder = firstSpace === -1 ? "" : remainder.slice(firstSpace).trim();
|
|
3376
|
+
return {
|
|
3377
|
+
filePath,
|
|
3378
|
+
remainder
|
|
3379
|
+
};
|
|
3380
|
+
}
|
|
3381
|
+
function parseFileRequestPart(part) {
|
|
3382
|
+
const split = splitRequestedPath(part);
|
|
3383
|
+
if (!split) return null;
|
|
3384
|
+
let { filePath, remainder } = split;
|
|
3385
|
+
const pathRange = /:(\d+(?:-\d+)?)$/.exec(filePath);
|
|
3386
|
+
const leadingRange = /^:(\d+(?:-\d+)?)(?:\s+|$)/.exec(remainder);
|
|
3387
|
+
const lineRange = pathRange?.[1] ?? leadingRange?.[1];
|
|
3388
|
+
if (pathRange) filePath = filePath.slice(0, -pathRange[0].length);
|
|
3389
|
+
else if (leadingRange) remainder = remainder.slice(leadingRange[0].length).trim();
|
|
3390
|
+
if (!filePath) return null;
|
|
3391
|
+
return {
|
|
3392
|
+
filePath,
|
|
3393
|
+
lineRange,
|
|
3394
|
+
reason: remainder || void 0
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
function parseRequestsFromResponse(response) {
|
|
3398
|
+
const requests = [];
|
|
3399
|
+
const requestRegex = /\bREQUEST:\s*(.+)/gi;
|
|
3400
|
+
let match;
|
|
3401
|
+
while ((match = requestRegex.exec(response)) !== null) {
|
|
3402
|
+
const parts = match[1].trim().split(",").map((p) => p.trim());
|
|
3403
|
+
for (const part of parts) {
|
|
3404
|
+
const req = parseFileRequestPart(part);
|
|
3405
|
+
if (req) requests.push(req);
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
return requests;
|
|
3409
|
+
}
|
|
3348
3410
|
function hasRequestBlocks(response) {
|
|
3349
3411
|
if (response.toUpperCase().includes(RESPONSE_BLOCKS.REQUEST)) return true;
|
|
3350
3412
|
const upper = response.toUpperCase();
|
|
@@ -3578,6 +3640,104 @@ function computeSemanticDiffLineHints(semanticDiff) {
|
|
|
3578
3640
|
return hints;
|
|
3579
3641
|
}
|
|
3580
3642
|
//#endregion
|
|
3643
|
+
//#region src/summarizer/read-changed-files.ts
|
|
3644
|
+
/**
|
|
3645
|
+
* Read contents of changed files with truncation.
|
|
3646
|
+
* Used for relevance filtering - only read files that were actually changed.
|
|
3647
|
+
* Includes auto-import expansion for TypeScript/JavaScript files.
|
|
3648
|
+
*/
|
|
3649
|
+
async function readChangedFileContent(workspaceRoot, filePath, maxLines = CONTEXT_LIMITS.MAX_LINES_PER_FILE, lineRange) {
|
|
3650
|
+
const pathResult = resolveSafePathInWorkspace(workspaceRoot, filePath, WORKSPACE_PATH_KIND.SOURCE_FILE);
|
|
3651
|
+
if (!pathResult.ok) return {
|
|
3652
|
+
path: filePath,
|
|
3653
|
+
content: CONTEXT_LIMITS.FILE_UNREADABLE,
|
|
3654
|
+
truncated: false,
|
|
3655
|
+
error: pathResult.message
|
|
3656
|
+
};
|
|
3657
|
+
try {
|
|
3658
|
+
const canonical = await verifyCanonicalPathUnderWorkspace(workspaceRoot, pathResult.absolutePath, WORKSPACE_PATH_KIND.SOURCE_FILE);
|
|
3659
|
+
if (!canonical.ok) return {
|
|
3660
|
+
path: filePath,
|
|
3661
|
+
content: CONTEXT_LIMITS.FILE_UNREADABLE,
|
|
3662
|
+
truncated: false,
|
|
3663
|
+
error: canonical.message
|
|
3664
|
+
};
|
|
3665
|
+
const fileStat = await stat(canonical.canonicalPath);
|
|
3666
|
+
if (!fileStat.isFile()) return {
|
|
3667
|
+
path: filePath,
|
|
3668
|
+
content: CONTEXT_LIMITS.FILE_UNREADABLE,
|
|
3669
|
+
truncated: false,
|
|
3670
|
+
error: "Not a regular file."
|
|
3671
|
+
};
|
|
3672
|
+
if (fileStat.size > SEMANTIC_DIFF_SOURCE_FILES.MAX_BYTES_PER_FILE) return {
|
|
3673
|
+
path: filePath,
|
|
3674
|
+
content: CONTEXT_LIMITS.FILE_UNREADABLE,
|
|
3675
|
+
truncated: false,
|
|
3676
|
+
error: "File exceeds size limit."
|
|
3677
|
+
};
|
|
3678
|
+
const lines = (await readFile(canonical.canonicalPath, "utf-8")).split("\n");
|
|
3679
|
+
if (lineRange) {
|
|
3680
|
+
const range = /^(\d+)(?:-(\d+))?$/.exec(lineRange);
|
|
3681
|
+
const start = Number(range?.[1]);
|
|
3682
|
+
const end = range?.[2] ? Number(range[2]) : start;
|
|
3683
|
+
if (!range || start < 1 || end < start) return {
|
|
3684
|
+
path: filePath,
|
|
3685
|
+
content: CONTEXT_LIMITS.FILE_UNREADABLE,
|
|
3686
|
+
truncated: false,
|
|
3687
|
+
error: "Invalid line range."
|
|
3688
|
+
};
|
|
3689
|
+
let startIdx = Math.max(0, start - 1);
|
|
3690
|
+
let endIdx = Math.min(lines.length, end);
|
|
3691
|
+
if (!range[2]) {
|
|
3692
|
+
startIdx = Math.max(0, startIdx - 50);
|
|
3693
|
+
endIdx = Math.min(lines.length, startIdx + 100);
|
|
3694
|
+
}
|
|
3695
|
+
const requestedEndIdx = endIdx;
|
|
3696
|
+
endIdx = Math.min(endIdx, startIdx + REVIEW_INPUT_LIMITS.MAX_REQUESTED_CONTEXT_LINES);
|
|
3697
|
+
return {
|
|
3698
|
+
path: filePath,
|
|
3699
|
+
content: lines.slice(startIdx, endIdx).map((l, i) => `${startIdx + i + 1} | ${l}`).join("\n"),
|
|
3700
|
+
truncated: endIdx < requestedEndIdx
|
|
3701
|
+
};
|
|
3702
|
+
}
|
|
3703
|
+
return {
|
|
3704
|
+
path: filePath,
|
|
3705
|
+
content: lines.slice(0, maxLines).join("\n"),
|
|
3706
|
+
truncated: lines.length > maxLines
|
|
3707
|
+
};
|
|
3708
|
+
} catch {
|
|
3709
|
+
return {
|
|
3710
|
+
path: filePath,
|
|
3711
|
+
content: CONTEXT_LIMITS.FILE_UNREADABLE,
|
|
3712
|
+
truncated: false,
|
|
3713
|
+
error: "File could not be read."
|
|
3714
|
+
};
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
async function readRequestedFiles(workspaceRoot, response) {
|
|
3718
|
+
const requests = parseRequestsFromResponse(response);
|
|
3719
|
+
if (requests.length > SEMANTIC_DIFF_SOURCE_FILES.MAX_COUNT) throw new Error(`Critic requested more than ${SEMANTIC_DIFF_SOURCE_FILES.MAX_COUNT} files.`);
|
|
3720
|
+
const uniqueRequests = [...new Map(requests.map((request) => [`${request.filePath}:${request.lineRange ?? ""}`, request])).values()];
|
|
3721
|
+
const blocks = [];
|
|
3722
|
+
const files = /* @__PURE__ */ new Set();
|
|
3723
|
+
let totalChars = 0;
|
|
3724
|
+
for (const request of uniqueRequests) {
|
|
3725
|
+
if (request.filePath.length > REVIEW_INPUT_LIMITS.MAX_PATH_CHARS) throw new Error(`Critic requested a path longer than ${REVIEW_INPUT_LIMITS.MAX_PATH_CHARS} characters.`);
|
|
3726
|
+
const file = await readChangedFileContent(workspaceRoot, request.filePath, REVIEW_INPUT_LIMITS.MAX_REQUESTED_CONTEXT_LINES, request.lineRange);
|
|
3727
|
+
if (file.error) throw new Error(`Could not read requested file ${request.filePath}: ${file.error}`);
|
|
3728
|
+
const block = formatSemanticDiffFileBlock(request.filePath, file.content, file.truncated);
|
|
3729
|
+
const nextLength = totalChars + block.length;
|
|
3730
|
+
if (nextLength > REVIEW_INPUT_LIMITS.MAX_SEMANTIC_DIFF_CHARS) throw new Error(`Critic requested context exceeds ${REVIEW_INPUT_LIMITS.MAX_SEMANTIC_DIFF_CHARS} characters.`);
|
|
3731
|
+
blocks.push(block);
|
|
3732
|
+
totalChars = nextLength;
|
|
3733
|
+
files.add(request.filePath);
|
|
3734
|
+
}
|
|
3735
|
+
return {
|
|
3736
|
+
semanticDiff: blocks.join(SEMANTIC_DIFF_PAYLOAD_MARKERS.FILE_BLOCK_SEPARATOR),
|
|
3737
|
+
filesAnalyzed: files.size
|
|
3738
|
+
};
|
|
3739
|
+
}
|
|
3740
|
+
//#endregion
|
|
3581
3741
|
//#region src/tools/submit-phase-review.ts
|
|
3582
3742
|
/**
|
|
3583
3743
|
* MCP tool: submit_phase_review
|
|
@@ -3620,15 +3780,16 @@ function buildHistorySummary(history, maxTokens = CRITIC_THRESHOLDS.HISTORY_SUMM
|
|
|
3620
3780
|
}
|
|
3621
3781
|
return parts.join("\n\n---\n\n");
|
|
3622
3782
|
}
|
|
3623
|
-
function buildUserContent(args, historySummary) {
|
|
3624
|
-
const parts = [`Phase: ${args.phaseId}
|
|
3783
|
+
function buildUserContent(args, historySummary, requestedContext = "") {
|
|
3784
|
+
const parts = [`Phase: ${args.phaseId}`];
|
|
3625
3785
|
if (historySummary) parts.unshift(`Previous rounds:\n${historySummary}\n`);
|
|
3626
|
-
|
|
3786
|
+
parts.push(`<developer_report>\n${args.report}\n</developer_report>`);
|
|
3787
|
+
const codeContent = [args.semanticDiff, requestedContext].filter(Boolean).join("\n\n");
|
|
3788
|
+
parts.push(`## CHANGED FILES\n<code_content>\n${codeContent}\n</code_content>`);
|
|
3627
3789
|
if (args.dependencies?.length) parts.push(`Dependencies: ${args.dependencies.join(", ")}`);
|
|
3628
|
-
debugLog(`buildUserContent - semanticDiff length: ${args.semanticDiff?.length ?? 0}`);
|
|
3629
3790
|
return parts.join("\n\n");
|
|
3630
3791
|
}
|
|
3631
|
-
async function buildContextBlock(workspaceRoot, newDeps, semanticDiff
|
|
3792
|
+
async function buildContextBlock(workspaceRoot, newDeps, semanticDiff) {
|
|
3632
3793
|
const [blueprint, pkgDeps] = await Promise.all([extractProjectBlueprint(workspaceRoot), parseDependencyListFromPackageJson(workspaceRoot).catch(() => ({
|
|
3633
3794
|
dependencies: [],
|
|
3634
3795
|
devDependencies: []
|
|
@@ -3637,46 +3798,12 @@ async function buildContextBlock(workspaceRoot, newDeps, semanticDiff, report) {
|
|
|
3637
3798
|
const bloatWarn = newDeps.length >= DEPENDENCY_THRESHOLDS.BLOAT_WARNING_NEW_PACKAGES || totalDeps >= DEPENDENCY_THRESHOLDS.BLOAT_WARNING_TOTAL_DEPS;
|
|
3638
3799
|
const parts = [`Project: ${blueprint.framework}. Structures: ${blueprint.structures.join(", ") || "none"}.`, `Current deps: ${totalDeps}. New/updated: ${newDeps.join(", ") || "none"}.`];
|
|
3639
3800
|
if (bloatWarn) parts.push(BLOAT_WARNING_MESSAGE);
|
|
3640
|
-
|
|
3641
|
-
const combinedText = [semanticDiff, report].filter(Boolean).join("\n");
|
|
3642
|
-
if (combinedText) {
|
|
3643
|
-
filesAnalyzed = parseSemanticDiff(combinedText).filesChanged.length;
|
|
3644
|
-
const contentBlock = buildContentBlockFromInput(semanticDiff, report);
|
|
3645
|
-
parts.push(contentBlock);
|
|
3646
|
-
}
|
|
3801
|
+
const filesAnalyzed = semanticDiff ? parseSemanticDiff(semanticDiff).filesChanged.length : 0;
|
|
3647
3802
|
return {
|
|
3648
3803
|
context: parts.join(" "),
|
|
3649
3804
|
filesAnalyzed
|
|
3650
3805
|
};
|
|
3651
3806
|
}
|
|
3652
|
-
function buildContentBlockFromInput(semanticDiff, report) {
|
|
3653
|
-
const sections = [];
|
|
3654
|
-
if (semanticDiff?.trim()) sections.push(`## CHANGED FILES (MCP resolved FILE:...CONTENT: payload from files[], semanticDiffPath, or inline semanticDiff):\n${semanticDiff.trim()}`);
|
|
3655
|
-
if (report?.trim()) sections.push(`## DEVELOPER REPORT:\n${report.trim()}`);
|
|
3656
|
-
return sections.join("\n\n");
|
|
3657
|
-
}
|
|
3658
|
-
async function appendRequestedFilesToContext(workspaceRoot, originalContext, criticResponse) {
|
|
3659
|
-
if (!hasRequestBlocks(criticResponse)) return {
|
|
3660
|
-
context: originalContext,
|
|
3661
|
-
filesAnalyzed: 0
|
|
3662
|
-
};
|
|
3663
|
-
return {
|
|
3664
|
-
context: `${originalContext}\n\n## CRITIC REQUESTED MORE CONTEXT\nNote: Resubmit with files[] (or semanticDiffPath / inline semanticDiff) covering every path the Critic needs. MCP reads those workspace paths when you provide them.`,
|
|
3665
|
-
filesAnalyzed: 0
|
|
3666
|
-
};
|
|
3667
|
-
}
|
|
3668
|
-
async function appendPreviousRoundsFilesToContext(workspaceRoot, originalContext, session) {
|
|
3669
|
-
const previousContents = [];
|
|
3670
|
-
for (const h of session.history) if (h.semanticDiff?.trim()) previousContents.push(`--- Round ${h.round} ---\n${h.semanticDiff.trim()}`);
|
|
3671
|
-
if (previousContents.length === 0) return {
|
|
3672
|
-
context: originalContext,
|
|
3673
|
-
filesAnalyzed: 0
|
|
3674
|
-
};
|
|
3675
|
-
return {
|
|
3676
|
-
context: `${originalContext}\n\n## PREVIOUS ROUNDS CONTENT (preserved):\n${previousContents.join("\n\n")}`,
|
|
3677
|
-
filesAnalyzed: 0
|
|
3678
|
-
};
|
|
3679
|
-
}
|
|
3680
3807
|
function toTextContent(json) {
|
|
3681
3808
|
return {
|
|
3682
3809
|
type: "text",
|
|
@@ -3720,18 +3847,7 @@ async function writeCaseFile(workspaceRoot, caseFile) {
|
|
|
3720
3847
|
await writeFile(path, JSON.stringify(caseFile, null, 2), "utf-8");
|
|
3721
3848
|
}
|
|
3722
3849
|
async function checkDeadlockEarly(workspaceRoot, args, session, semanticDiffHints) {
|
|
3723
|
-
|
|
3724
|
-
if (round > CONFLICT_LOOP.MAX_ROUNDS) {
|
|
3725
|
-
await clearSession(workspaceRoot);
|
|
3726
|
-
await updateConflictCount(workspaceRoot, 1);
|
|
3727
|
-
const caseFile = buildCaseFile(args.phaseId, round, []);
|
|
3728
|
-
await writeCaseFile(workspaceRoot, caseFile);
|
|
3729
|
-
return { content: [toTextContentWithHints({
|
|
3730
|
-
...caseFile,
|
|
3731
|
-
filesAnalyzed: 0
|
|
3732
|
-
}, semanticDiffHints)] };
|
|
3733
|
-
}
|
|
3734
|
-
if (round > 1 && session?.phaseId === args.phaseId && session.round >= CONFLICT_LOOP.MAX_ROUNDS) {
|
|
3850
|
+
if ((args.round ?? 1) > 1 && session?.phaseId === args.phaseId && session.round >= CONFLICT_LOOP.MAX_ROUNDS) {
|
|
3735
3851
|
await clearSession(workspaceRoot);
|
|
3736
3852
|
await updateConflictCount(workspaceRoot, 1);
|
|
3737
3853
|
const caseFile = buildCaseFile(args.phaseId, session.round, session.history);
|
|
@@ -3803,25 +3919,23 @@ async function runCriticReview(workspaceRoot, args, session, config, provider) {
|
|
|
3803
3919
|
const model = getEffectiveModel(config);
|
|
3804
3920
|
const [status, contextBlockResult, rules, preferencesLog] = await Promise.all([
|
|
3805
3921
|
getStatus(workspaceRoot),
|
|
3806
|
-
buildContextBlock(workspaceRoot, args.dependencies ?? [], args.semanticDiff
|
|
3922
|
+
buildContextBlock(workspaceRoot, args.dependencies ?? [], args.semanticDiff),
|
|
3807
3923
|
loadRules(workspaceRoot),
|
|
3808
3924
|
readPreferencesLog(workspaceRoot)
|
|
3809
3925
|
]);
|
|
3810
|
-
|
|
3926
|
+
const enrichedContextBlock = contextBlockResult.context;
|
|
3811
3927
|
let totalFilesRead = contextBlockResult.filesAnalyzed;
|
|
3928
|
+
let requestedContext = "";
|
|
3812
3929
|
if (round > 1 && session?.history && session.history.length > 0) {
|
|
3813
3930
|
const previousCriticResponse = session.history[session.history.length - 1].criticResponse;
|
|
3814
3931
|
if (hasRequestBlocks(previousCriticResponse)) {
|
|
3815
|
-
const requestedBlockResult = await
|
|
3816
|
-
|
|
3932
|
+
const requestedBlockResult = await readRequestedFiles(workspaceRoot, previousCriticResponse);
|
|
3933
|
+
requestedContext = requestedBlockResult.semanticDiff;
|
|
3817
3934
|
totalFilesRead += requestedBlockResult.filesAnalyzed;
|
|
3818
3935
|
}
|
|
3819
|
-
const previousRoundsResult = await appendPreviousRoundsFilesToContext(workspaceRoot, enrichedContextBlock, session);
|
|
3820
|
-
enrichedContextBlock = previousRoundsResult.context;
|
|
3821
|
-
totalFilesRead += previousRoundsResult.filesAnalyzed;
|
|
3822
3936
|
}
|
|
3823
3937
|
const rulesBlock = formatRulesForPrompt(rules);
|
|
3824
|
-
const userContent = buildUserContent(args, session?.phaseId === args.phaseId ? buildHistorySummary(session.history) : void 0);
|
|
3938
|
+
const userContent = buildUserContent(args, session?.phaseId === args.phaseId ? buildHistorySummary(session.history) : void 0, requestedContext);
|
|
3825
3939
|
const messages = [{
|
|
3826
3940
|
role: "system",
|
|
3827
3941
|
content: buildSystemPrompt(personaPrompt, status, enrichedContextBlock, rulesBlock, preferencesLog, round)
|
|
@@ -3849,7 +3963,6 @@ async function runCriticReview(workspaceRoot, args, session, config, provider) {
|
|
|
3849
3963
|
roundData: {
|
|
3850
3964
|
round,
|
|
3851
3965
|
report: args.report,
|
|
3852
|
-
semanticDiff: args.semanticDiff,
|
|
3853
3966
|
verdict: String(verdict),
|
|
3854
3967
|
criticResponse: response.content,
|
|
3855
3968
|
concerns: concerns.length > 0 ? concerns : void 0,
|
|
@@ -3894,7 +4007,7 @@ async function handleRejectOrContinue(result, session, workspaceRoot, args, sema
|
|
|
3894
4007
|
const noActivePriorConcerns = !priorConcerns.some((c) => c.reviewStatus === CONCERN_REVIEW_STATUS.REVIEWED_VALID);
|
|
3895
4008
|
if (allPriorReviewed && noActivePriorConcerns) return handleAcceptVerdict(result, workspaceRoot, args, semanticDiffHints);
|
|
3896
4009
|
}
|
|
3897
|
-
if (round
|
|
4010
|
+
if (round >= CONFLICT_LOOP.MAX_ROUNDS) {
|
|
3898
4011
|
await clearSession(workspaceRoot);
|
|
3899
4012
|
await updateConflictCount(workspaceRoot, 1);
|
|
3900
4013
|
const caseFile = buildCaseFile(args.phaseId, round, nextSession.history, String(result.verdict));
|
|
@@ -3926,17 +4039,17 @@ async function handleRejectOrContinue(result, session, workspaceRoot, args, sema
|
|
|
3926
4039
|
* which advertises `properties: {}` to IDEs (agents then cannot discover `files` / `semanticDiffPath`).
|
|
3927
4040
|
*/
|
|
3928
4041
|
const submitPhaseReviewFieldsSchema = z.object({
|
|
3929
|
-
phaseId: z.string().describe("Phase identifier (e.g., phase-6-§1a or 1.1.1)"),
|
|
3930
|
-
report: z.string().describe("Implementer report. MUST INCLUDE: 1. Specific file paths & line numbers. 2. What changed and why. 3. Confirmation that NO \"future solutions\" or \"TODOs\" remain (instant fixes only)."),
|
|
3931
|
-
files: z.array(z.string()).optional().describe(
|
|
3932
|
-
semanticDiffPath: z.string().optional().describe("Workspace-relative path to a pre-built FILE:...CONTENT: payload file (raw or JSON {\"semanticDiff\":\"...\"}). Exactly one of: files | semanticDiffPath | semanticDiff."),
|
|
3933
|
-
semanticDiff: z.string().optional().describe("Inline FILE:...CONTENT: payload. Prefer files[]. Exactly one of: files | semanticDiffPath | semanticDiff. NOT git diff."),
|
|
4042
|
+
phaseId: z.string().trim().min(1).max(REVIEW_INPUT_LIMITS.MAX_PHASE_ID_CHARS).describe("Phase identifier (e.g., phase-6-§1a or 1.1.1)"),
|
|
4043
|
+
report: z.string().trim().min(1).max(REVIEW_INPUT_LIMITS.MAX_REPORT_CHARS).describe("Implementer report. MUST INCLUDE: 1. Specific file paths & line numbers. 2. What changed and why. 3. Confirmation that NO \"future solutions\" or \"TODOs\" remain (instant fixes only)."),
|
|
4044
|
+
files: z.array(z.string().trim().min(1).max(REVIEW_INPUT_LIMITS.MAX_PATH_CHARS)).min(1).max(SEMANTIC_DIFF_SOURCE_FILES.MAX_COUNT).optional().describe(`PREFERRED. Workspace-relative source paths under VIBE_WORKSPACE_ROOT. MCP reads each file and builds FILE:...CONTENT: payload. Exactly one of: files | semanticDiffPath | semanticDiff. Max ${SEMANTIC_DIFF_SOURCE_FILES.MAX_COUNT} files.`),
|
|
4045
|
+
semanticDiffPath: z.string().trim().min(1).max(REVIEW_INPUT_LIMITS.MAX_PATH_CHARS).optional().describe("Workspace-relative path to a pre-built FILE:...CONTENT: payload file (raw or JSON {\"semanticDiff\":\"...\"}). Exactly one of: files | semanticDiffPath | semanticDiff."),
|
|
4046
|
+
semanticDiff: z.string().trim().min(1).max(REVIEW_INPUT_LIMITS.MAX_SEMANTIC_DIFF_CHARS).optional().describe("Inline FILE:...CONTENT: payload. Prefer files[]. Exactly one of: files | semanticDiffPath | semanticDiff. NOT git diff."),
|
|
3934
4047
|
updateStatus: z.boolean().optional().describe("When false, ACCEPT does not write .vibe/status.json. Default: true except phaseIds with mcp-smoke- / vibe-gate-probe- prefixes."),
|
|
3935
|
-
dependencies: z.array(z.string()).optional().describe("New/updated package names"),
|
|
3936
|
-
round: z.number().optional().describe(
|
|
4048
|
+
dependencies: z.array(z.string().trim().min(1).max(REVIEW_INPUT_LIMITS.MAX_DEPENDENCY_NAME_CHARS)).max(REVIEW_INPUT_LIMITS.MAX_DEPENDENCIES).optional().describe("New/updated package names"),
|
|
4049
|
+
round: z.number().int().min(1).max(CONFLICT_LOOP.MAX_ROUNDS).optional().describe(`Round number (1-${CONFLICT_LOOP.MAX_ROUNDS}), default 1`),
|
|
3937
4050
|
logToDebt: z.object({
|
|
3938
|
-
subject: z.string(),
|
|
3939
|
-
rationale: z.string()
|
|
4051
|
+
subject: z.string().trim().min(1).max(REVIEW_INPUT_LIMITS.MAX_DEBT_SUBJECT_CHARS),
|
|
4052
|
+
rationale: z.string().trim().min(1).max(REVIEW_INPUT_LIMITS.MAX_DEBT_RATIONALE_CHARS)
|
|
3940
4053
|
}).optional().describe("When DEBT verdict and Implementer accepts, log to DEBT.md")
|
|
3941
4054
|
});
|
|
3942
4055
|
function countPayloadSources(data) {
|
|
@@ -4070,35 +4183,53 @@ async function handleAcceptVerdictFlow(reviewResult, session, workspaceRoot, arg
|
|
|
4070
4183
|
if (session && session.concerns.length > 0 && !(allReviewed && noActiveConcernsLeft)) return handleAcceptWithUnverifiedConcerns(reviewResult, updatedSession ?? session, semanticDiffHints);
|
|
4071
4184
|
return handleAcceptVerdict(reviewResult, workspaceRoot, args, semanticDiffHints);
|
|
4072
4185
|
}
|
|
4073
|
-
async function
|
|
4074
|
-
const parsed = submitPhaseReviewInputSchema.safeParse(rawArgs);
|
|
4075
|
-
if (!parsed.success) return { content: [toTextContent({
|
|
4076
|
-
error: "Invalid submit_phase_review arguments",
|
|
4077
|
-
issues: z.flattenError(parsed.error)
|
|
4078
|
-
})] };
|
|
4079
|
-
const input = parsed.data;
|
|
4080
|
-
const config = loadConfig();
|
|
4081
|
-
const provider = createLLMProvider(config);
|
|
4082
|
-
if (!provider) return { content: [toTextContent({ error: ERROR_MESSAGES.NO_LLM_PROVIDER })] };
|
|
4083
|
-
const workspaceRoot = getWorkspaceRoot();
|
|
4186
|
+
async function resolveSemanticDiffInput(workspaceRoot, input) {
|
|
4084
4187
|
let semanticDiff;
|
|
4085
|
-
if (input.files
|
|
4188
|
+
if (input.files) {
|
|
4086
4189
|
const built = await buildSemanticDiffFromSourceFiles(workspaceRoot, input.files);
|
|
4087
|
-
if (!built.ok) return {
|
|
4190
|
+
if (!built.ok) return {
|
|
4191
|
+
ok: false,
|
|
4088
4192
|
error: built.message,
|
|
4089
4193
|
code: built.code
|
|
4090
|
-
}
|
|
4194
|
+
};
|
|
4091
4195
|
semanticDiff = built.semanticDiff;
|
|
4092
4196
|
debugLog(`semanticDiff built from files[]: ${built.filesLoaded.join(", ")}`);
|
|
4093
|
-
} else if (input.semanticDiffPath
|
|
4094
|
-
const loaded = await loadSemanticDiffFromWorkspacePath(workspaceRoot, input.semanticDiffPath
|
|
4095
|
-
if (!loaded.ok) return {
|
|
4197
|
+
} else if (input.semanticDiffPath) {
|
|
4198
|
+
const loaded = await loadSemanticDiffFromWorkspacePath(workspaceRoot, input.semanticDiffPath);
|
|
4199
|
+
if (!loaded.ok) return {
|
|
4200
|
+
ok: false,
|
|
4096
4201
|
error: loaded.message,
|
|
4097
4202
|
code: loaded.code
|
|
4098
|
-
}
|
|
4203
|
+
};
|
|
4099
4204
|
semanticDiff = loaded.semanticDiff;
|
|
4100
4205
|
debugLog(`semanticDiff loaded from file: ${loaded.resolvedFromPath}`);
|
|
4101
4206
|
} else semanticDiff = input.semanticDiff.trim();
|
|
4207
|
+
if (semanticDiff.length > REVIEW_INPUT_LIMITS.MAX_SEMANTIC_DIFF_CHARS) return {
|
|
4208
|
+
ok: false,
|
|
4209
|
+
error: `resolved semanticDiff exceeds the maximum size of ${REVIEW_INPUT_LIMITS.MAX_SEMANTIC_DIFF_CHARS} characters.`
|
|
4210
|
+
};
|
|
4211
|
+
return {
|
|
4212
|
+
ok: true,
|
|
4213
|
+
semanticDiff
|
|
4214
|
+
};
|
|
4215
|
+
}
|
|
4216
|
+
async function handleSubmitPhaseReview(rawArgs) {
|
|
4217
|
+
const parsed = submitPhaseReviewInputSchema.safeParse(rawArgs);
|
|
4218
|
+
if (!parsed.success) return { content: [toTextContent({
|
|
4219
|
+
error: "Invalid submit_phase_review arguments",
|
|
4220
|
+
issues: z.flattenError(parsed.error)
|
|
4221
|
+
})] };
|
|
4222
|
+
const input = parsed.data;
|
|
4223
|
+
const config = loadConfig();
|
|
4224
|
+
const provider = createLLMProvider(config);
|
|
4225
|
+
if (!provider) return { content: [toTextContent({ error: ERROR_MESSAGES.NO_LLM_PROVIDER })] };
|
|
4226
|
+
const workspaceRoot = getWorkspaceRoot();
|
|
4227
|
+
const resolution = await resolveSemanticDiffInput(workspaceRoot, input);
|
|
4228
|
+
if (!resolution.ok) return { content: [toTextContent({
|
|
4229
|
+
error: resolution.error,
|
|
4230
|
+
...resolution.code ? { code: resolution.code } : {}
|
|
4231
|
+
})] };
|
|
4232
|
+
const { semanticDiff } = resolution;
|
|
4102
4233
|
const args = {
|
|
4103
4234
|
phaseId: input.phaseId,
|
|
4104
4235
|
report: input.report,
|
package/docs/ROADMAP.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Roadmap and release checklist
|
|
2
|
+
|
|
3
|
+
## 0.1.5 — review safety and conflict-loop fixes
|
|
4
|
+
|
|
5
|
+
- [x] Bound MCP review inputs and resolved payload size.
|
|
6
|
+
- [x] End a rejected third Critic round with a deadlock case.
|
|
7
|
+
- [x] Keep source payloads out of the system prompt and conflict-session history.
|
|
8
|
+
- [x] Read Critic `REQUEST:` paths within the workspace and enforce file and context limits.
|
|
9
|
+
- [x] Use canonical paths for changed-file reads to reject traversal and symlink escapes.
|
|
10
|
+
- [x] Keep model IDs configurable and use the OpenAI Responses API for current reasoning-model compatibility.
|
|
11
|
+
- [x] Add focused tests and update user documentation.
|
|
12
|
+
- [x] Run typecheck, lint, unit tests, build, MCP tool smoke test, and npm package inspection.
|
|
13
|
+
- [ ] Publish `vibe-gate-mcp@0.1.5` from the maintainer's npm account.
|
|
14
|
+
|
|
15
|
+
## Scope review
|
|
16
|
+
|
|
17
|
+
The earlier release plan included extra diagnostics and automatic provider fallback. Those do not address a demonstrated defect in the current package. Fallback could also select a different account or incur unexpected provider charges, so providers remain explicitly configured. The 0.1.5 scope is limited to behavior that was broken or unbounded in the existing review path; broader provider work should wait for a concrete user need.
|
|
18
|
+
|
|
19
|
+
The npm package is not published by the current GitHub Actions workflow; CI only validates and builds it. After the commit is pushed, publish the version from an authenticated maintainer environment.
|
|
@@ -22,6 +22,7 @@ Limits (SSoT: `SEMANTIC_DIFF_SOURCE_FILES` in `src/constants.ts`):
|
|
|
22
22
|
- Max **10** paths per call
|
|
23
23
|
- Max **1 MiB** per file
|
|
24
24
|
- Max **5 MiB** total
|
|
25
|
+
- The resolved payload must also stay under **500,000 characters**; the same limit applies to inline payloads and payload files.
|
|
25
26
|
|
|
26
27
|
Paths must be **relative to `VIBE_WORKSPACE_ROOT`**. Absolute paths and `..` traversal are rejected.
|
|
27
28
|
|
|
@@ -44,6 +45,14 @@ Write the same FILE:…CONTENT: string to a UTF-8 file under the workspace (raw
|
|
|
44
45
|
|
|
45
46
|
Same FILE:…CONTENT: string as the tool argument. Prefer `files[]`.
|
|
46
47
|
|
|
48
|
+
## Critic-requested context
|
|
49
|
+
|
|
50
|
+
On a later review round, the MCP reads paths from the previous Critic response's `REQUEST:` lines under `VIBE_WORKSPACE_ROOT`. Paths are subject to workspace and symlink checks. Each request is limited to 10 files, 1 MiB per file, and 120 context lines; the combined requested context is limited to 500,000 characters. Use a narrower line range when a file needs more context.
|
|
51
|
+
|
|
52
|
+
The review loop allows three rounds. A rejected or blocked third round produces a deadlock case for human review.
|
|
53
|
+
|
|
54
|
+
Source payloads are submitted again with each review round and are not stored in `.vibe/review-session.json`.
|
|
55
|
+
|
|
47
56
|
## Payload format (what the Critic sees)
|
|
48
57
|
|
|
49
58
|
```
|
package/docs/USAGE.md
CHANGED
|
@@ -70,6 +70,8 @@ OPENAI_API_KEY=YOUR_OPENAI_API_KEY
|
|
|
70
70
|
# CRITIC_MODEL=gpt-5.4 # optional, default
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
Vibe-Gate sends OpenAI requests through the Responses API. `CRITIC_MODEL` is passed through without a Vibe-Gate model allowlist, so you can select a newer model such as `gpt-6-luna` without waiting for a package update. The model must be available to your OpenAI account and support the Responses API.
|
|
74
|
+
|
|
73
75
|
### Anthropic
|
|
74
76
|
|
|
75
77
|
```env
|
|
@@ -22,14 +22,16 @@ exactly one payload source:
|
|
|
22
22
|
semanticDiffPath → loadSemanticDiffFromWorkspacePath
|
|
23
23
|
semanticDiff → use inline string
|
|
24
24
|
↓
|
|
25
|
-
parseSemanticDiff() → filesChanged count
|
|
25
|
+
parseSemanticDiff() → filesChanged count
|
|
26
26
|
↓
|
|
27
|
-
buildContextBlock() →
|
|
27
|
+
buildContextBlock() → project summary and dependencies
|
|
28
28
|
↓
|
|
29
|
-
provider.complete([system, user]) → Critic LLM
|
|
29
|
+
provider.complete([system, user]) → Critic LLM (report and source payload sent once in user message)
|
|
30
30
|
↓
|
|
31
31
|
parseVerdictFromResponse → ACCEPT | REJECT | …
|
|
32
32
|
↓
|
|
33
|
+
on the next round, read workspace paths from prior REQUEST: blocks (bounded and canonical-path checked)
|
|
34
|
+
↓
|
|
33
35
|
if ACCEPT && shouldPersistPhaseStatus(phaseId, updateStatus) → updatePhaseOnAccept → .vibe/status.json
|
|
34
36
|
↓
|
|
35
37
|
Response JSON: { verdict, model, usage, statusUpdated, statusSkipped?, statusError?, … }
|
|
@@ -49,6 +51,8 @@ Response JSON: { verdict, model, usage, statusUpdated, statusSkipped?, statusErr
|
|
|
49
51
|
| round | number | no | Round (1–3), default 1 |
|
|
50
52
|
| logToDebt | object | no | When DEBT: `{ subject, rationale }` |
|
|
51
53
|
|
|
54
|
+
Input limits are enforced by the MCP schema: phase id 256 characters, report 50,000, dependency list 100 names (256 characters each), and paths 1,024 characters. The resolved source payload is rechecked after loading and limited to 500,000 characters. Critic-requested context is limited to 10 files, 1 MiB per file, and 120 lines per request.
|
|
55
|
+
|
|
52
56
|
† **Exactly one** of `files` (non-empty), `semanticDiffPath` (non-empty), or `semanticDiff` (non-empty). See [SEMANTIC_DIFF_PAYLOAD.md](../../SEMANTIC_DIFF_PAYLOAD.md).
|
|
53
57
|
|
|
54
58
|
**ListTools note:** MCP registers `submitPhaseReviewFieldsSchema` (plain ZodObject). Exactly-one rules run in the handler via `submitPhaseReviewInputSchema`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibe-gate-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Adversarial Quality Gate MCP for vibe-coding: IDE AI vs Critic AI, human decides on deadlock",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
".env.example",
|
|
35
35
|
"README.md",
|
|
36
36
|
"CHANGELOG.md",
|
|
37
|
+
"docs/ROADMAP.md",
|
|
37
38
|
"docs/INSTALLATION.md",
|
|
38
39
|
"docs/CLI_PROVIDERS.md",
|
|
39
40
|
"docs/USAGE.md",
|