@mandujs/mcp 0.38.3 → 0.38.4
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/package.json +2 -2
- package/src/executor/error-handler.ts +76 -2
- package/src/profiles.ts +112 -50
- package/src/tools/guard.ts +35 -0
- package/src/tools/index.ts +18 -4
- package/src/tools/transaction.ts +18 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/mcp",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.4",
|
|
4
4
|
"description": "Mandu MCP Server - Agent-native interface for Mandu framework operations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@mandujs/core": "^0.54.
|
|
37
|
+
"@mandujs/core": "^0.54.10",
|
|
38
38
|
"@mandujs/ate": "^0.26.1",
|
|
39
39
|
"@mandujs/skills": "^0.20.1",
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.25.3"
|
|
@@ -35,6 +35,23 @@ export interface McpErrorResponse {
|
|
|
35
35
|
export interface McpToolResponse {
|
|
36
36
|
content: Array<{ type: string; text: string }>;
|
|
37
37
|
isError?: boolean;
|
|
38
|
+
_meta?: McpResponseMeta;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type McpNextActionKind = "none" | "inspect" | "verify" | "repair" | "retry";
|
|
42
|
+
|
|
43
|
+
export interface McpNextAction {
|
|
44
|
+
kind: McpNextActionKind;
|
|
45
|
+
reason: string;
|
|
46
|
+
command?: string;
|
|
47
|
+
tool?: string;
|
|
48
|
+
input?: unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface McpResponseMeta {
|
|
52
|
+
toolName: string;
|
|
53
|
+
ok: boolean;
|
|
54
|
+
nextAction: McpNextAction;
|
|
38
55
|
}
|
|
39
56
|
|
|
40
57
|
/**
|
|
@@ -186,6 +203,51 @@ function isSoftErrorResult(result: unknown): boolean {
|
|
|
186
203
|
return false;
|
|
187
204
|
}
|
|
188
205
|
|
|
206
|
+
function inferNextAction(toolName: string, result: unknown, errorResponse?: McpErrorResponse): McpNextAction {
|
|
207
|
+
if (errorResponse) {
|
|
208
|
+
return {
|
|
209
|
+
kind: errorResponse.retryable ? "retry" : "repair",
|
|
210
|
+
reason: errorResponse.suggestion ?? errorResponse.error,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (result && typeof result === "object") {
|
|
215
|
+
const obj = result as Record<string, unknown>;
|
|
216
|
+
if (isSoftErrorResult(result)) {
|
|
217
|
+
return { kind: "repair", reason: String(obj.error ?? "Tool returned an error result.") };
|
|
218
|
+
}
|
|
219
|
+
if (typeof obj.nextVerifyCommand === "string") {
|
|
220
|
+
return { kind: "verify", reason: "Tool returned an explicit verification command.", command: obj.nextVerifyCommand };
|
|
221
|
+
}
|
|
222
|
+
if (typeof obj.nextRepairInput === "string") {
|
|
223
|
+
return {
|
|
224
|
+
kind: "repair",
|
|
225
|
+
reason: "Tool returned a repair input artifact.",
|
|
226
|
+
command: `mandu agent repair --from ${obj.nextRepairInput}`,
|
|
227
|
+
input: obj.nextRepairInput,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (obj.dryRun === true) {
|
|
231
|
+
return { kind: "inspect", reason: "Dry-run completed; review the preview before applying changes." };
|
|
232
|
+
}
|
|
233
|
+
if (Array.isArray(obj.nextSteps) && obj.nextSteps.length > 0) {
|
|
234
|
+
return { kind: "inspect", reason: "Tool returned follow-up steps.", input: obj.nextSteps };
|
|
235
|
+
}
|
|
236
|
+
if (typeof obj.tip === "string") {
|
|
237
|
+
return { kind: "inspect", reason: obj.tip };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return { kind: "none", reason: `${toolName} completed successfully.` };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function attachMeta(payload: unknown, meta: McpResponseMeta): unknown {
|
|
245
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
246
|
+
return { ...(payload as Record<string, unknown>), _meta: meta };
|
|
247
|
+
}
|
|
248
|
+
return { data: payload, _meta: meta };
|
|
249
|
+
}
|
|
250
|
+
|
|
189
251
|
export function createToolResponse(
|
|
190
252
|
toolName: string,
|
|
191
253
|
result: unknown,
|
|
@@ -193,28 +255,40 @@ export function createToolResponse(
|
|
|
193
255
|
): McpToolResponse {
|
|
194
256
|
if (error) {
|
|
195
257
|
const errorResponse = formatMcpError(error, toolName);
|
|
258
|
+
const meta: McpResponseMeta = {
|
|
259
|
+
toolName,
|
|
260
|
+
ok: false,
|
|
261
|
+
nextAction: inferNextAction(toolName, null, errorResponse),
|
|
262
|
+
};
|
|
196
263
|
return {
|
|
197
264
|
content: [
|
|
198
265
|
{
|
|
199
266
|
type: "text",
|
|
200
|
-
text: JSON.stringify(errorResponse, null, 2),
|
|
267
|
+
text: JSON.stringify(attachMeta(errorResponse, meta), null, 2),
|
|
201
268
|
},
|
|
202
269
|
],
|
|
203
270
|
isError: true,
|
|
271
|
+
_meta: meta,
|
|
204
272
|
};
|
|
205
273
|
}
|
|
206
274
|
|
|
207
275
|
// Detect soft errors returned by handlers (e.g. { error: "Route not found" })
|
|
208
276
|
const softError = isSoftErrorResult(result);
|
|
277
|
+
const meta: McpResponseMeta = {
|
|
278
|
+
toolName,
|
|
279
|
+
ok: !softError,
|
|
280
|
+
nextAction: inferNextAction(toolName, result),
|
|
281
|
+
};
|
|
209
282
|
|
|
210
283
|
return {
|
|
211
284
|
content: [
|
|
212
285
|
{
|
|
213
286
|
type: "text",
|
|
214
|
-
text: JSON.stringify(result, null, 2),
|
|
287
|
+
text: JSON.stringify(attachMeta(result, meta), null, 2),
|
|
215
288
|
},
|
|
216
289
|
],
|
|
217
290
|
...(softError && { isError: true }),
|
|
291
|
+
_meta: meta,
|
|
218
292
|
};
|
|
219
293
|
}
|
|
220
294
|
|
package/src/profiles.ts
CHANGED
|
@@ -1,57 +1,119 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP Tool Profiles
|
|
3
|
-
*
|
|
4
|
-
* Controls how many tool categories are exposed to AI agents.
|
|
5
|
-
* - agent-core: Canonical agent workflow plus docs grounding (default)
|
|
6
|
-
* - agent-full: Agent workflow plus Mandu domain tools
|
|
7
|
-
* - internal: All categories, no filtering
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export type McpProfile = "agent-core" | "agent-full" | "internal";
|
|
11
|
-
|
|
12
|
-
export const PROFILE_CATEGORIES: Record<McpProfile, string[] | null> = {
|
|
13
|
-
"agent-core": ["agent", "docs"],
|
|
14
|
-
"agent-full": [
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool Profiles
|
|
3
|
+
*
|
|
4
|
+
* Controls how many tool categories are exposed to AI agents.
|
|
5
|
+
* - agent-core: Canonical agent workflow plus docs grounding (default)
|
|
6
|
+
* - agent-full: Agent workflow plus Mandu domain tools
|
|
7
|
+
* - internal: All categories, no filtering
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type McpProfile = "agent-core" | "agent-full" | "internal";
|
|
11
|
+
|
|
12
|
+
export const PROFILE_CATEGORIES: Record<McpProfile, string[] | null> = {
|
|
13
|
+
"agent-core": ["agent", "docs"],
|
|
14
|
+
"agent-full": [
|
|
15
|
+
// Official agent loop
|
|
16
|
+
"agent",
|
|
17
|
+
"docs",
|
|
18
|
+
// Code generation / scaffolding
|
|
19
|
+
"spec",
|
|
20
|
+
"generate",
|
|
21
|
+
"composite",
|
|
22
|
+
// Domain primitives
|
|
23
|
+
"slot",
|
|
24
|
+
"slot-validation",
|
|
25
|
+
"hydration",
|
|
26
|
+
"contract",
|
|
27
|
+
"design",
|
|
28
|
+
"seo",
|
|
29
|
+
// Quality / validation
|
|
30
|
+
"guard",
|
|
31
|
+
"lint",
|
|
32
|
+
"run-tests",
|
|
33
|
+
"ate",
|
|
34
|
+
// Deploy
|
|
35
|
+
"deploy-plan",
|
|
36
|
+
"deploy-preview",
|
|
37
|
+
// AI refactor
|
|
38
|
+
"refactor-barrel",
|
|
39
|
+
"refactor-routes",
|
|
40
|
+
"refactor-contract",
|
|
41
|
+
],
|
|
42
|
+
internal: null,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Categories intentionally hidden from every agent-facing profile.
|
|
47
|
+
*
|
|
48
|
+
* Every new tool category in `TOOL_MODULES` MUST be classified into one of:
|
|
49
|
+
* - `PROFILE_CATEGORIES["agent-core"]` — canonical agent loop
|
|
50
|
+
* - `PROFILE_CATEGORIES["agent-full"]` — domain work for agents
|
|
51
|
+
* - `EXPERT_ONLY_CATEGORIES` — internal plumbing
|
|
52
|
+
*
|
|
53
|
+
* `profile-coverage.test.ts` fails CI if a new category is left unclassified,
|
|
54
|
+
* preventing silent default-profile bloat over time.
|
|
55
|
+
*/
|
|
56
|
+
export const EXPERT_ONLY_CATEGORIES: ReadonlySet<string> = new Set([
|
|
57
|
+
// Transactional state / framework internals
|
|
58
|
+
"transaction",
|
|
59
|
+
"history",
|
|
60
|
+
"decisions",
|
|
61
|
+
"negotiate",
|
|
62
|
+
// Runtime / project introspection
|
|
63
|
+
"brain",
|
|
64
|
+
"runtime",
|
|
65
|
+
"project",
|
|
66
|
+
"resource",
|
|
67
|
+
// Devtools / kitchen
|
|
68
|
+
"kitchen",
|
|
69
|
+
"component",
|
|
70
|
+
// Agent loop internal helpers (used by mandu.agent.*, not by agents directly)
|
|
71
|
+
"ai-brief",
|
|
72
|
+
"loop-close",
|
|
73
|
+
// Specialized ATE phases (core "ate" is exposed in agent-full)
|
|
74
|
+
"ate-phase5",
|
|
75
|
+
"ate-context",
|
|
76
|
+
"ate-run",
|
|
77
|
+
"ate-flakes",
|
|
78
|
+
"ate-prompt",
|
|
79
|
+
"ate-exemplar",
|
|
80
|
+
"ate-save",
|
|
81
|
+
"ate-boundary-probe",
|
|
82
|
+
"ate-recall",
|
|
83
|
+
"ate-remember",
|
|
84
|
+
"ate-coverage",
|
|
85
|
+
"ate-mutate",
|
|
86
|
+
"ate-mutation-report",
|
|
87
|
+
"ate-oracle-pending",
|
|
88
|
+
"ate-oracle-verdict",
|
|
89
|
+
"ate-oracle-replay",
|
|
90
|
+
]);
|
|
29
91
|
|
|
30
92
|
/**
|
|
31
93
|
* Returns allowed category names for a profile, or null if all categories are allowed.
|
|
32
94
|
*/
|
|
33
|
-
export function getProfileCategories(profile: McpProfile): string[] | null {
|
|
34
|
-
return PROFILE_CATEGORIES[profile] ?? null;
|
|
35
|
-
}
|
|
36
|
-
|
|
95
|
+
export function getProfileCategories(profile: McpProfile): string[] | null {
|
|
96
|
+
return PROFILE_CATEGORIES[profile] ?? null;
|
|
97
|
+
}
|
|
98
|
+
|
|
37
99
|
/**
|
|
38
100
|
* Type guard for valid profile strings.
|
|
39
101
|
*/
|
|
40
|
-
export function isValidProfile(value: string): value is McpProfile {
|
|
41
|
-
return value === "agent-core" || value === "agent-full" || value === "internal";
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Resolve current and legacy profile names to the new official profile set.
|
|
46
|
-
*/
|
|
47
|
-
export function resolveMcpProfile(
|
|
48
|
-
value: string | undefined,
|
|
49
|
-
fallback: McpProfile = "agent-core",
|
|
50
|
-
): McpProfile {
|
|
51
|
-
if (!value) return fallback;
|
|
52
|
-
if (isValidProfile(value)) return value;
|
|
53
|
-
if (value === "minimal") return "agent-core";
|
|
54
|
-
if (value === "standard") return "agent-full";
|
|
55
|
-
if (value === "full") return "internal";
|
|
56
|
-
return fallback;
|
|
57
|
-
}
|
|
102
|
+
export function isValidProfile(value: string): value is McpProfile {
|
|
103
|
+
return value === "agent-core" || value === "agent-full" || value === "internal";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve current and legacy profile names to the new official profile set.
|
|
108
|
+
*/
|
|
109
|
+
export function resolveMcpProfile(
|
|
110
|
+
value: string | undefined,
|
|
111
|
+
fallback: McpProfile = "agent-core",
|
|
112
|
+
): McpProfile {
|
|
113
|
+
if (!value) return fallback;
|
|
114
|
+
if (isValidProfile(value)) return value;
|
|
115
|
+
if (value === "minimal") return "agent-core";
|
|
116
|
+
if (value === "standard") return "agent-full";
|
|
117
|
+
if (value === "full") return "internal";
|
|
118
|
+
return fallback;
|
|
119
|
+
}
|
package/src/tools/guard.ts
CHANGED
|
@@ -138,7 +138,25 @@ export function guardTools(projectRoot: string) {
|
|
|
138
138
|
}
|
|
139
139
|
};
|
|
140
140
|
|
|
141
|
+
const guardViolationCode = (ruleId: string | undefined, type?: string) =>
|
|
142
|
+
`MANDU_GUARD_${(ruleId || type || "VIOLATION").toUpperCase().replace(/[^A-Z0-9]+/g, "_")}`;
|
|
143
|
+
|
|
144
|
+
const explainViolation = (input: {
|
|
145
|
+
type?: string;
|
|
146
|
+
ruleId?: string;
|
|
147
|
+
fromLayer?: string;
|
|
148
|
+
toLayer?: string;
|
|
149
|
+
suggestion?: string;
|
|
150
|
+
}): string => {
|
|
151
|
+
const source = input.fromLayer ? ` from ${input.fromLayer}` : "";
|
|
152
|
+
const target = input.toLayer ? ` to ${input.toLayer}` : "";
|
|
153
|
+
const rule = input.ruleId ?? input.type ?? "architecture rule";
|
|
154
|
+
const fix = input.suggestion ? ` Suggested fix: ${input.suggestion}` : "";
|
|
155
|
+
return `Violation of ${rule}${source}${target}.${fix}`.trim();
|
|
156
|
+
};
|
|
157
|
+
|
|
141
158
|
const summarizeArchitectureViolation = (violation: Violation) => ({
|
|
159
|
+
code: guardViolationCode(violation.ruleName, violation.type),
|
|
142
160
|
ruleId: violation.ruleName,
|
|
143
161
|
type: violation.type,
|
|
144
162
|
file: path.relative(projectRoot, violation.filePath).replace(/\\/g, "/") || violation.filePath,
|
|
@@ -146,16 +164,25 @@ export function guardTools(projectRoot: string) {
|
|
|
146
164
|
column: violation.column,
|
|
147
165
|
message: violation.ruleDescription,
|
|
148
166
|
suggestion: violation.suggestions[0],
|
|
167
|
+
explanation: explainViolation({
|
|
168
|
+
type: violation.type,
|
|
169
|
+
ruleId: violation.ruleName,
|
|
170
|
+
fromLayer: violation.fromLayer,
|
|
171
|
+
toLayer: violation.toLayer,
|
|
172
|
+
suggestion: violation.suggestions[0],
|
|
173
|
+
}),
|
|
149
174
|
fromLayer: violation.fromLayer,
|
|
150
175
|
toLayer: violation.toLayer,
|
|
151
176
|
importStatement: violation.importStatement,
|
|
152
177
|
});
|
|
153
178
|
|
|
154
179
|
const summarizeLegacyViolation = (v: Awaited<ReturnType<typeof runGuardCheck>>["violations"][number]) => ({
|
|
180
|
+
code: guardViolationCode(v.ruleId),
|
|
155
181
|
ruleId: v.ruleId,
|
|
156
182
|
file: v.file,
|
|
157
183
|
message: v.message,
|
|
158
184
|
suggestion: v.suggestion,
|
|
185
|
+
explanation: explainViolation({ ruleId: v.ruleId, suggestion: v.suggestion }),
|
|
159
186
|
});
|
|
160
187
|
|
|
161
188
|
const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
|
|
@@ -521,10 +548,18 @@ export function guardTools(projectRoot: string) {
|
|
|
521
548
|
preset: config.preset,
|
|
522
549
|
violations: items.map((item) => ({
|
|
523
550
|
// Violation info
|
|
551
|
+
code: guardViolationCode(item.violation.ruleName, item.violation.type),
|
|
524
552
|
type: item.violation.type,
|
|
525
553
|
file: item.violation.filePath,
|
|
526
554
|
line: item.violation.line,
|
|
527
555
|
message: item.violation.ruleDescription,
|
|
556
|
+
explanation: explainViolation({
|
|
557
|
+
type: item.violation.type,
|
|
558
|
+
ruleId: item.violation.ruleName,
|
|
559
|
+
fromLayer: item.violation.fromLayer,
|
|
560
|
+
toLayer: item.violation.toLayer,
|
|
561
|
+
suggestion: item.healing.primary.explanation,
|
|
562
|
+
}),
|
|
528
563
|
fromLayer: item.violation.fromLayer,
|
|
529
564
|
toLayer: item.violation.toLayer,
|
|
530
565
|
importStatement: item.violation.importStatement,
|
package/src/tools/index.ts
CHANGED
|
@@ -290,9 +290,10 @@ export const TOOL_MODULES: ToolModule[] = [
|
|
|
290
290
|
export function validateBuiltinToolModules(
|
|
291
291
|
modules: readonly ToolModule[] = TOOL_MODULES
|
|
292
292
|
): string[] {
|
|
293
|
-
const issues: string[] = [];
|
|
294
|
-
const categories = new Set<string>();
|
|
295
|
-
const toolNames = new Map<string, string>();
|
|
293
|
+
const issues: string[] = [];
|
|
294
|
+
const categories = new Set<string>();
|
|
295
|
+
const toolNames = new Map<string, string>();
|
|
296
|
+
const descriptions = new Map<string, string>();
|
|
296
297
|
|
|
297
298
|
for (const module of modules) {
|
|
298
299
|
if (categories.has(module.category)) {
|
|
@@ -305,7 +306,20 @@ export function validateBuiltinToolModules(
|
|
|
305
306
|
}
|
|
306
307
|
|
|
307
308
|
for (const definition of module.definitions) {
|
|
308
|
-
|
|
309
|
+
if (!definition.description?.trim()) {
|
|
310
|
+
issues.push(`tool definition is missing description: ${definition.name} in ${module.category}`);
|
|
311
|
+
}
|
|
312
|
+
const normalizedDescription = definition.description?.trim().replace(/\s+/g, " ").toLowerCase();
|
|
313
|
+
if (normalizedDescription) {
|
|
314
|
+
const previousTool = descriptions.get(normalizedDescription);
|
|
315
|
+
if (previousTool) {
|
|
316
|
+
issues.push(
|
|
317
|
+
`duplicate tool description: ${definition.name} and ${previousTool} both describe the same action`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
descriptions.set(normalizedDescription, definition.name);
|
|
321
|
+
}
|
|
322
|
+
const previousCategory = toolNames.get(definition.name);
|
|
309
323
|
if (previousCategory) {
|
|
310
324
|
issues.push(
|
|
311
325
|
`duplicate tool definition: ${definition.name} in ${previousCategory} and ${module.category}`
|
package/src/tools/transaction.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getTransactionStatus,
|
|
7
7
|
hasActiveTransaction,
|
|
8
8
|
} from "@mandujs/core";
|
|
9
|
-
import { acquireLock, releaseLock, checkLock } from "../tx-lock.js";
|
|
9
|
+
import { acquireLock, releaseLock, checkLock, requireLock } from "../tx-lock.js";
|
|
10
10
|
|
|
11
11
|
export const transactionToolDefinitions: Tool[] = [
|
|
12
12
|
{
|
|
@@ -99,9 +99,15 @@ export function transactionTools(projectRoot: string) {
|
|
|
99
99
|
return { error: lock.error };
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
let change: Awaited<ReturnType<typeof beginChange>>;
|
|
103
|
+
try {
|
|
104
|
+
change = await beginChange(projectRoot, {
|
|
105
|
+
message: message || "MCP transaction",
|
|
106
|
+
});
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (lock.lockId) releaseLock(lock.lockId);
|
|
109
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
110
|
+
}
|
|
105
111
|
|
|
106
112
|
return {
|
|
107
113
|
success: true,
|
|
@@ -122,6 +128,10 @@ export function transactionTools(projectRoot: string) {
|
|
|
122
128
|
error: "No active transaction to commit",
|
|
123
129
|
};
|
|
124
130
|
}
|
|
131
|
+
const lockCheck = requireLock(lockId);
|
|
132
|
+
if (!lockCheck.allowed) {
|
|
133
|
+
return { error: lockCheck.error };
|
|
134
|
+
}
|
|
125
135
|
|
|
126
136
|
const result = await commitChange(projectRoot);
|
|
127
137
|
if (lockId) releaseLock(lockId);
|
|
@@ -142,6 +152,10 @@ export function transactionTools(projectRoot: string) {
|
|
|
142
152
|
error: "No active transaction to rollback. Provide a changeId to rollback a specific change.",
|
|
143
153
|
};
|
|
144
154
|
}
|
|
155
|
+
const lockCheck = requireLock(lockId);
|
|
156
|
+
if (!lockCheck.allowed) {
|
|
157
|
+
return { error: lockCheck.error };
|
|
158
|
+
}
|
|
145
159
|
|
|
146
160
|
const result = await rollbackChange(projectRoot, changeId);
|
|
147
161
|
if (lockId) releaseLock(lockId);
|