@loucompanion/forge-bridge 0.1.3-dev.ab8510b71f9b → 0.1.3-dev.af261f369eae
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/dist/bridge/bridge.service.js +24 -4
- package/dist/bridge/bridge.types.d.ts +4 -0
- package/dist/cli/cli.help.d.ts +2 -0
- package/dist/cli/cli.help.js +171 -0
- package/dist/cli/cli.service.js +8 -183
- package/dist/cli/local-config.d.ts +2 -0
- package/dist/cli/local-config.js +6 -2
- package/dist/session/phase-completion.artifacts.d.ts +10 -0
- package/dist/session/phase-completion.artifacts.js +620 -0
- package/dist/session/phase-completion.d.ts +32 -44
- package/dist/session/phase-completion.helpers.d.ts +112 -0
- package/dist/session/phase-completion.helpers.js +660 -0
- package/dist/session/phase-completion.js +28 -3497
- package/dist/session/phase-completion.prompts.d.ts +14 -0
- package/dist/session/phase-completion.prompts.js +220 -0
- package/dist/session/phase-completion.runtime.d.ts +36 -0
- package/dist/session/phase-completion.runtime.js +609 -0
- package/dist/session/phase-completion.schemas.d.ts +3 -0
- package/dist/session/phase-completion.schemas.js +707 -0
- package/dist/session/phase-completion.validation.d.ts +34 -0
- package/dist/session/phase-completion.validation.js +724 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +93 -0
- package/dist/session/provider-cli/provider-cli.artifacts.js +754 -0
- package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +14 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +329 -0
- package/dist/session/provider-cli/provider-cli.process.d.ts +32 -0
- package/dist/session/provider-cli/provider-cli.process.js +287 -0
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +44 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +528 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +65 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +9 -0
- package/dist/session/provider-cli/provider-cli.runtime.js +40 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +9 -53
- package/dist/session/provider-cli/provider-cli.service.js +161 -2561
- package/dist/session/provider-cli/provider-cli.types.d.ts +1 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +20 -0
- package/dist/session/provider-cli/provider-output-state.js +445 -0
- package/dist/session/session.service.js +30 -23
- package/dist/session/session.types.d.ts +48 -0
- package/dist/session/session.types.js +90 -1
- package/dist/shared/process/windows-launch.d.ts +8 -0
- package/dist/shared/process/windows-launch.js +41 -0
- package/dist/shared/provider-capabilities.d.ts +1 -0
- package/dist/shared/provider-capabilities.js +3 -0
- package/dist/shared/redaction/redaction.js +4 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/dependency-warmer.d.ts +11 -0
- package/dist/worktree/dependency-warmer.js +45 -1
- package/package.json +1 -1
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { completionPhaseFromDispatch, completionPhaseOrder, completionRedactionSecretsEnv, completionToolNamesForDebugGate, completionToolNamesForDecomposeGate, completionToolNamesForDraftGate, completionToolNamesForPhase, completionToolNamesForQaGate, debugCompletionPhaseOrder, decomposeCompletionGateOrder, draftCompletionGateOrder, qaCompletionGateOrder, toolServerName, usesArchitectCycleCompletion, usesDebugCycleCompletion, usesDevelopCycleCompletion, usesQaCycleCompletion } from "./phase-completion.js";
|
|
4
|
+
import { asDebugCompletionPhase, asJsonRpcMessage, asMutableRecord, asRecord, decomposeGateFromToolName, draftGateFromToolName, errorMessage, forgeBridgeCliBinPath, isDebugCompletionPhase, isDecomposeDispatch, isDevelopCompletionPhase, normalizedOwnerType, parseCompletionRedactionSecrets, parseOptionalJsonObject, parseStrictCompletionPhase, phaseFromToolName, qaGateFromToolName, requiredString, rpcError, tomlString, tomlStringArray, writeFileAtomic, writeJsonRpc } from "./phase-completion.validation.js";
|
|
5
|
+
import { completionPromptForDebugCycle, completionPromptForDecomposeCycle, completionPromptForDevelopCycle, completionPromptForDraftCycle, completionPromptForPhase, completionPromptForQaCycle, completionToolNextInstruction, completionToolNextInstructionForDebugGate, completionToolNextInstructionForDecomposeGate, completionToolNextInstructionForDraftGate, completionToolNextInstructionForQaGate, nextDebugPhaseAfter, nextPhaseAfter } from "./phase-completion.prompts.js";
|
|
6
|
+
import { toolDescription, toolInputSchema } from "./phase-completion.schemas.js";
|
|
7
|
+
import { writeCompletionArtifact, writeDecomposeGateCompletionArtifact, writeDraftGateCompletionArtifact } from "./phase-completion.artifacts.js";
|
|
8
|
+
export async function prepareCompletionToolRuntime(options) {
|
|
9
|
+
const phase = options.phase ?? completionPhaseFromDispatch(options.dispatch);
|
|
10
|
+
const worktreePath = path.resolve(options.worktree.worktreePath);
|
|
11
|
+
const workItemId = options.dispatch.workItem.id;
|
|
12
|
+
const ownerType = normalizedOwnerType(options.dispatch.cycleRun.ownerType) ?? "work_item";
|
|
13
|
+
const ownerId = options.dispatch.cycleRun.ownerId ?? workItemId;
|
|
14
|
+
const mcpArgs = buildCompletionToolServerArgs({
|
|
15
|
+
worktreePath,
|
|
16
|
+
workItemId,
|
|
17
|
+
ownerType,
|
|
18
|
+
ownerId,
|
|
19
|
+
phase,
|
|
20
|
+
producedBy: `${options.dispatch.execution.provider}-cli`,
|
|
21
|
+
developCycle: usesDevelopCycleCompletion(options.dispatch),
|
|
22
|
+
qaCycle: usesQaCycleCompletion(options.dispatch),
|
|
23
|
+
debugCycle: usesDebugCycleCompletion(options.dispatch),
|
|
24
|
+
architectCycle: usesArchitectCycleCompletion(options.dispatch),
|
|
25
|
+
draftCycle: (phase === "draft"
|
|
26
|
+
|| options.dispatch.execution.lane.trim().toLowerCase() === "draft"
|
|
27
|
+
|| options.dispatch.cycleRun.kind.trim().toLowerCase() === "draft")
|
|
28
|
+
&& !isDecomposeDispatch(options.dispatch),
|
|
29
|
+
decomposeCycle: isDecomposeDispatch(options.dispatch)
|
|
30
|
+
});
|
|
31
|
+
const runtime = {
|
|
32
|
+
serverName: toolServerName,
|
|
33
|
+
command: process.execPath,
|
|
34
|
+
args: [forgeBridgeCliBinPath(), ...mcpArgs],
|
|
35
|
+
env: {},
|
|
36
|
+
cleanup: async () => undefined
|
|
37
|
+
};
|
|
38
|
+
if (options.dispatch.execution.provider.trim().toLowerCase() !== "cursor") {
|
|
39
|
+
return runtime;
|
|
40
|
+
}
|
|
41
|
+
return await prepareCursorCompletionToolRuntime(worktreePath, runtime);
|
|
42
|
+
}
|
|
43
|
+
export function buildCompletionToolServerArgs(config) {
|
|
44
|
+
const args = [
|
|
45
|
+
"mcp",
|
|
46
|
+
"--worktree",
|
|
47
|
+
path.resolve(config.worktreePath),
|
|
48
|
+
"--work-item-id",
|
|
49
|
+
config.workItemId,
|
|
50
|
+
"--phase",
|
|
51
|
+
config.phase,
|
|
52
|
+
"--produced-by",
|
|
53
|
+
config.producedBy ?? "forge-completion-tool"
|
|
54
|
+
];
|
|
55
|
+
if (config.ownerType || config.ownerId) {
|
|
56
|
+
args.push("--owner-type", normalizedOwnerType(config.ownerType) ?? "work_item");
|
|
57
|
+
args.push("--owner-id", config.ownerId ?? config.workItemId);
|
|
58
|
+
}
|
|
59
|
+
if (config.developCycle) {
|
|
60
|
+
args.push("--develop-cycle");
|
|
61
|
+
}
|
|
62
|
+
if (config.qaCycle) {
|
|
63
|
+
args.push("--qa-cycle");
|
|
64
|
+
}
|
|
65
|
+
if (config.debugCycle) {
|
|
66
|
+
args.push("--debug-cycle");
|
|
67
|
+
}
|
|
68
|
+
if (config.architectCycle) {
|
|
69
|
+
args.push("--architect-cycle");
|
|
70
|
+
}
|
|
71
|
+
if (config.draftCycle) {
|
|
72
|
+
args.push("--draft-cycle");
|
|
73
|
+
}
|
|
74
|
+
if (config.decomposeCycle) {
|
|
75
|
+
args.push("--decompose-cycle");
|
|
76
|
+
}
|
|
77
|
+
return args;
|
|
78
|
+
}
|
|
79
|
+
export function codexMcpConfigArgs(runtime) {
|
|
80
|
+
const prefix = `mcp_servers.${runtime.serverName}`;
|
|
81
|
+
const args = [
|
|
82
|
+
"-c",
|
|
83
|
+
`${prefix}.command=${tomlString(runtime.command)}`,
|
|
84
|
+
"-c",
|
|
85
|
+
`${prefix}.args=${tomlStringArray(runtime.args)}`
|
|
86
|
+
];
|
|
87
|
+
for (const [key, value] of Object.entries(runtime.env)) {
|
|
88
|
+
args.push("-c", `${prefix}.env.${key}=${tomlString(value)}`);
|
|
89
|
+
}
|
|
90
|
+
return args;
|
|
91
|
+
}
|
|
92
|
+
export function cursorMcpServerConfig(runtime) {
|
|
93
|
+
return {
|
|
94
|
+
command: runtime.command,
|
|
95
|
+
args: [...runtime.args],
|
|
96
|
+
env: runtime.env
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export function completionToolNamesForServer(config) {
|
|
100
|
+
return config.decomposeCycle
|
|
101
|
+
? decomposeCompletionGateOrder.flatMap((gate) => completionToolNamesForDecomposeGate(gate))
|
|
102
|
+
: config.draftCycle
|
|
103
|
+
? draftCompletionGateOrder.flatMap((gate) => completionToolNamesForDraftGate(gate))
|
|
104
|
+
: config.debugCycle
|
|
105
|
+
? debugCompletionPhaseOrder.flatMap((gate) => completionToolNamesForDebugGate(gate))
|
|
106
|
+
: config.qaCycle
|
|
107
|
+
? qaCompletionGateOrder.flatMap((gate) => completionToolNamesForQaGate(gate))
|
|
108
|
+
: config.developCycle
|
|
109
|
+
? completionPhaseOrder.flatMap((phase) => completionToolNamesForPhase(phase))
|
|
110
|
+
: config.architectCycle
|
|
111
|
+
? completionToolNamesForPhase("architect")
|
|
112
|
+
: completionToolNamesForPhase(config.phase);
|
|
113
|
+
}
|
|
114
|
+
export class CompletionToolServerSession {
|
|
115
|
+
config;
|
|
116
|
+
currentPhase;
|
|
117
|
+
currentQaGate = "qa_plan";
|
|
118
|
+
currentDraftGate = "draft_intent";
|
|
119
|
+
currentDecomposeGate = "decompose_subject_intake";
|
|
120
|
+
completed = false;
|
|
121
|
+
constructor(config) {
|
|
122
|
+
this.config = config;
|
|
123
|
+
this.currentPhase = config.phase;
|
|
124
|
+
}
|
|
125
|
+
get instructions() {
|
|
126
|
+
return this.config.decomposeCycle
|
|
127
|
+
? completionPromptForDecomposeCycle(this.currentDecomposeGate)
|
|
128
|
+
: this.config.draftCycle
|
|
129
|
+
? completionPromptForDraftCycle(this.currentDraftGate)
|
|
130
|
+
: this.config.debugCycle
|
|
131
|
+
? completionPromptForDebugCycle(asDebugCompletionPhase(this.currentPhase))
|
|
132
|
+
: this.config.qaCycle
|
|
133
|
+
? completionPromptForQaCycle(this.currentQaGate)
|
|
134
|
+
: this.config.developCycle
|
|
135
|
+
? completionPromptForDevelopCycle(this.currentPhase)
|
|
136
|
+
: completionPromptForPhase(this.currentPhase);
|
|
137
|
+
}
|
|
138
|
+
toolDefinitions() {
|
|
139
|
+
return completionToolNamesForServer(this.config).map((name) => toolDefinition(name, phaseFromToolName(name), qaGateFromToolName(name), decomposeGateFromToolName(name)));
|
|
140
|
+
}
|
|
141
|
+
async callTool(toolName, input) {
|
|
142
|
+
const requestedPhase = phaseFromToolName(toolName);
|
|
143
|
+
if (this.completed) {
|
|
144
|
+
throw new Error("Forge completion cycle is already complete; no more completion calls are accepted.");
|
|
145
|
+
}
|
|
146
|
+
if (this.config.decomposeCycle) {
|
|
147
|
+
return await this.callDecomposeGateTool(toolName, requestedPhase, input);
|
|
148
|
+
}
|
|
149
|
+
if (this.config.draftCycle) {
|
|
150
|
+
return await this.callDraftGateTool(toolName, requestedPhase, input);
|
|
151
|
+
}
|
|
152
|
+
if (this.config.debugCycle) {
|
|
153
|
+
return await this.callDebugGateTool(toolName, requestedPhase, input);
|
|
154
|
+
}
|
|
155
|
+
if (this.config.qaCycle) {
|
|
156
|
+
return await this.callQaGateTool(toolName, requestedPhase, input);
|
|
157
|
+
}
|
|
158
|
+
if (requestedPhase !== this.currentPhase) {
|
|
159
|
+
throw new Error(`Tool ${toolName} is out of order. Current Forge phase is ${this.currentPhase}.`);
|
|
160
|
+
}
|
|
161
|
+
const completedPhase = this.currentPhase;
|
|
162
|
+
const reference = await writeCompletionArtifact({ ...this.config, phase: completedPhase }, toolName, input);
|
|
163
|
+
const status = reference.status.trim().toLowerCase();
|
|
164
|
+
const pass = status === "pass";
|
|
165
|
+
const nextPhase = pass
|
|
166
|
+
? this.advanceAfterPass(completedPhase)
|
|
167
|
+
: completedPhase;
|
|
168
|
+
const nextInstruction = completionToolNextInstruction(completedPhase, status, nextPhase);
|
|
169
|
+
const reason = pass
|
|
170
|
+
? `${completedPhase} completion artifact accepted.`
|
|
171
|
+
: `${completedPhase} completion artifact recorded with status ${status}; rework is required before phase progression.`;
|
|
172
|
+
return {
|
|
173
|
+
content: [
|
|
174
|
+
{
|
|
175
|
+
type: "text",
|
|
176
|
+
text: `Forge recorded ${reference.phase} completion artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}`
|
|
177
|
+
}
|
|
178
|
+
],
|
|
179
|
+
structuredContent: {
|
|
180
|
+
...reference,
|
|
181
|
+
gate: {
|
|
182
|
+
phase: completedPhase,
|
|
183
|
+
status,
|
|
184
|
+
allowed: pass,
|
|
185
|
+
reason
|
|
186
|
+
},
|
|
187
|
+
nextPhase,
|
|
188
|
+
nextInstruction
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
async callDecomposeGateTool(toolName, requestedPhase, input) {
|
|
193
|
+
const requestedGate = decomposeGateFromToolName(toolName);
|
|
194
|
+
if (requestedPhase !== "decompose" || !requestedGate) {
|
|
195
|
+
throw new Error(`Tool ${toolName} is not available in Decompose-cycle mode. Current Forge Decompose gate is ${this.currentDecomposeGate}.`);
|
|
196
|
+
}
|
|
197
|
+
if (requestedGate !== this.currentDecomposeGate) {
|
|
198
|
+
throw new Error(`Tool ${toolName} is out of order. Current Forge Decompose gate is ${this.currentDecomposeGate}.`);
|
|
199
|
+
}
|
|
200
|
+
const completedGate = this.currentDecomposeGate;
|
|
201
|
+
const reference = await writeDecomposeGateCompletionArtifact(this.config, completedGate, toolName, input);
|
|
202
|
+
const status = reference.status.trim().toLowerCase();
|
|
203
|
+
const pass = status === "pass";
|
|
204
|
+
const nextGate = pass ? this.advanceAfterDecomposeGate(completedGate) : completedGate;
|
|
205
|
+
const nextPhase = pass
|
|
206
|
+
? (nextGate ? "decompose" : "sign_off_ready")
|
|
207
|
+
: "decompose";
|
|
208
|
+
const nextInstruction = completionToolNextInstructionForDecomposeGate(completedGate, status, nextGate);
|
|
209
|
+
const reason = pass
|
|
210
|
+
? nextGate
|
|
211
|
+
? `${completedGate} artifact accepted; continue to ${nextGate}.`
|
|
212
|
+
: `Decompose proposal artifact accepted with status ${status}.`
|
|
213
|
+
: `${completedGate} artifact recorded with status ${status}; rework is required before continuing.`;
|
|
214
|
+
return {
|
|
215
|
+
content: [{ type: "text", text: `Forge recorded ${completedGate} artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}` }],
|
|
216
|
+
structuredContent: {
|
|
217
|
+
...reference,
|
|
218
|
+
gate: { phase: "decompose", decomposeGate: completedGate, status, allowed: pass, reason },
|
|
219
|
+
nextPhase,
|
|
220
|
+
nextInstruction
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
async callDraftGateTool(toolName, requestedPhase, input) {
|
|
225
|
+
const requestedGate = draftGateFromToolName(toolName);
|
|
226
|
+
if (requestedPhase !== "draft" || !requestedGate) {
|
|
227
|
+
throw new Error(`Tool ${toolName} is not available in Draft-cycle mode. Current Forge Draft gate is ${this.currentDraftGate}.`);
|
|
228
|
+
}
|
|
229
|
+
if (requestedGate !== this.currentDraftGate) {
|
|
230
|
+
throw new Error(`Tool ${toolName} is out of order. Current Forge Draft gate is ${this.currentDraftGate}.`);
|
|
231
|
+
}
|
|
232
|
+
const completedGate = this.currentDraftGate;
|
|
233
|
+
const reference = await writeDraftGateCompletionArtifact(this.config, completedGate, toolName, input);
|
|
234
|
+
const status = reference.status.trim().toLowerCase();
|
|
235
|
+
const nextGate = this.advanceAfterDraftGate(completedGate);
|
|
236
|
+
const nextPhase = nextGate ? "draft" : "sign_off_ready";
|
|
237
|
+
const nextInstruction = completionToolNextInstructionForDraftGate(completedGate, status, nextGate);
|
|
238
|
+
return {
|
|
239
|
+
content: [{ type: "text", text: `Forge recorded ${completedGate} artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}` }],
|
|
240
|
+
structuredContent: {
|
|
241
|
+
...reference,
|
|
242
|
+
gate: { phase: "draft", draftGate: completedGate, status, allowed: status !== "blocked", reason: nextGate ? `${completedGate} artifact accepted; continue to ${nextGate}.` : `Draft proposal artifact accepted with status ${status}.` },
|
|
243
|
+
nextPhase,
|
|
244
|
+
nextInstruction
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
async callDebugGateTool(toolName, requestedPhase, input) {
|
|
249
|
+
const currentDebugGate = asDebugCompletionPhase(this.currentPhase);
|
|
250
|
+
if (!isDebugCompletionPhase(requestedPhase)) {
|
|
251
|
+
throw new Error(`Tool ${toolName} is not available in Debug-cycle mode. Current Forge Debug gate is ${currentDebugGate}.`);
|
|
252
|
+
}
|
|
253
|
+
if (requestedPhase !== currentDebugGate) {
|
|
254
|
+
throw new Error(`Tool ${toolName} is out of order. Current Forge Debug gate is ${currentDebugGate}.`);
|
|
255
|
+
}
|
|
256
|
+
const completedGate = currentDebugGate;
|
|
257
|
+
const reference = await writeCompletionArtifact({ ...this.config, phase: completedGate }, toolName, input);
|
|
258
|
+
const status = reference.status.trim().toLowerCase();
|
|
259
|
+
const pass = status === "pass";
|
|
260
|
+
const nextGate = pass ? this.advanceAfterDebugGate(completedGate) : completedGate;
|
|
261
|
+
const nextPhase = nextGate ?? "qa_ready";
|
|
262
|
+
const nextInstruction = completionToolNextInstructionForDebugGate(completedGate, status, nextGate);
|
|
263
|
+
const reason = pass
|
|
264
|
+
? `${completedGate} artifact accepted${nextGate ? `; continue to ${nextGate}.` : "; Debug regression is QA-ready."}`
|
|
265
|
+
: `${completedGate} artifact recorded with status ${status}; rework is required before Debug progression.`;
|
|
266
|
+
return {
|
|
267
|
+
content: [
|
|
268
|
+
{
|
|
269
|
+
type: "text",
|
|
270
|
+
text: `Forge recorded ${completedGate} artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}`
|
|
271
|
+
}
|
|
272
|
+
],
|
|
273
|
+
structuredContent: {
|
|
274
|
+
...reference,
|
|
275
|
+
gate: {
|
|
276
|
+
phase: completedGate,
|
|
277
|
+
status,
|
|
278
|
+
allowed: pass,
|
|
279
|
+
reason
|
|
280
|
+
},
|
|
281
|
+
nextPhase,
|
|
282
|
+
nextInstruction
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
async callQaGateTool(toolName, requestedPhase, input) {
|
|
287
|
+
const requestedGate = qaGateFromToolName(toolName);
|
|
288
|
+
if (requestedPhase !== "qa" || !requestedGate) {
|
|
289
|
+
throw new Error(`Tool ${toolName} is not available in QA-cycle mode. Current Forge QA gate is ${this.currentQaGate}.`);
|
|
290
|
+
}
|
|
291
|
+
if (requestedGate !== this.currentQaGate) {
|
|
292
|
+
throw new Error(`Tool ${toolName} is out of order. Current Forge QA gate is ${this.currentQaGate}.`);
|
|
293
|
+
}
|
|
294
|
+
const completedGate = this.currentQaGate;
|
|
295
|
+
const reference = await writeCompletionArtifact({ ...this.config, phase: "qa" }, toolName, input);
|
|
296
|
+
const status = reference.status.trim().toLowerCase();
|
|
297
|
+
const nextGate = this.advanceAfterQaGate(completedGate);
|
|
298
|
+
const nextPhase = nextGate ? "qa" : "sign_off_ready";
|
|
299
|
+
const nextInstruction = completionToolNextInstructionForQaGate(completedGate, status, nextGate);
|
|
300
|
+
const allowed = status !== "blocked";
|
|
301
|
+
const reason = nextGate
|
|
302
|
+
? `${completedGate} artifact accepted; continue to ${nextGate}.`
|
|
303
|
+
: `QA verdict artifact accepted with status ${status}.`;
|
|
304
|
+
return {
|
|
305
|
+
content: [
|
|
306
|
+
{
|
|
307
|
+
type: "text",
|
|
308
|
+
text: `Forge recorded ${completedGate} artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}`
|
|
309
|
+
}
|
|
310
|
+
],
|
|
311
|
+
structuredContent: {
|
|
312
|
+
...reference,
|
|
313
|
+
gate: {
|
|
314
|
+
phase: "qa",
|
|
315
|
+
qaGate: completedGate,
|
|
316
|
+
status,
|
|
317
|
+
allowed,
|
|
318
|
+
reason
|
|
319
|
+
},
|
|
320
|
+
nextPhase,
|
|
321
|
+
nextInstruction
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
advanceAfterPass(phase) {
|
|
326
|
+
if (phase === "qa") {
|
|
327
|
+
this.completed = true;
|
|
328
|
+
return "sign_off_ready";
|
|
329
|
+
}
|
|
330
|
+
if (isDebugCompletionPhase(phase)) {
|
|
331
|
+
return nextDebugPhaseAfter(phase) ?? "qa_ready";
|
|
332
|
+
}
|
|
333
|
+
const next = nextPhaseAfter(phase);
|
|
334
|
+
if (!this.config.developCycle) {
|
|
335
|
+
return next ?? "dev_test_ready";
|
|
336
|
+
}
|
|
337
|
+
if (next) {
|
|
338
|
+
this.currentPhase = next;
|
|
339
|
+
return next;
|
|
340
|
+
}
|
|
341
|
+
this.completed = true;
|
|
342
|
+
return "dev_test_ready";
|
|
343
|
+
}
|
|
344
|
+
advanceAfterDecomposeGate(gate) {
|
|
345
|
+
const index = decomposeCompletionGateOrder.indexOf(gate);
|
|
346
|
+
const next = index >= 0 ? decomposeCompletionGateOrder[index + 1] ?? null : null;
|
|
347
|
+
if (next) {
|
|
348
|
+
this.currentDecomposeGate = next;
|
|
349
|
+
return next;
|
|
350
|
+
}
|
|
351
|
+
this.completed = true;
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
advanceAfterDraftGate(gate) {
|
|
355
|
+
const index = draftCompletionGateOrder.indexOf(gate);
|
|
356
|
+
const next = index >= 0 ? draftCompletionGateOrder[index + 1] ?? null : null;
|
|
357
|
+
if (next) {
|
|
358
|
+
this.currentDraftGate = next;
|
|
359
|
+
return next;
|
|
360
|
+
}
|
|
361
|
+
this.completed = true;
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
advanceAfterQaGate(gate) {
|
|
365
|
+
const index = qaCompletionGateOrder.indexOf(gate);
|
|
366
|
+
const next = index >= 0 ? qaCompletionGateOrder[index + 1] ?? null : null;
|
|
367
|
+
if (next) {
|
|
368
|
+
this.currentQaGate = next;
|
|
369
|
+
return next;
|
|
370
|
+
}
|
|
371
|
+
this.completed = true;
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
advanceAfterDebugGate(gate) {
|
|
375
|
+
const index = debugCompletionPhaseOrder.indexOf(gate);
|
|
376
|
+
const next = index >= 0 ? debugCompletionPhaseOrder[index + 1] ?? null : null;
|
|
377
|
+
if (next) {
|
|
378
|
+
this.currentPhase = next;
|
|
379
|
+
return next;
|
|
380
|
+
}
|
|
381
|
+
this.completed = true;
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
export async function runCompletionMcpServer(config) {
|
|
386
|
+
const session = new CompletionToolServerSession(config);
|
|
387
|
+
process.stdin.setEncoding("utf8");
|
|
388
|
+
let buffer = "";
|
|
389
|
+
for await (const chunk of process.stdin) {
|
|
390
|
+
buffer += chunk;
|
|
391
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
392
|
+
while (newlineIndex >= 0) {
|
|
393
|
+
const rawLine = buffer.slice(0, newlineIndex).trim();
|
|
394
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
395
|
+
if (rawLine.length > 0) {
|
|
396
|
+
await handleJsonRpcLine(session, rawLine);
|
|
397
|
+
}
|
|
398
|
+
newlineIndex = buffer.indexOf("\n");
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const trailing = buffer.trim();
|
|
402
|
+
if (trailing.length > 0) {
|
|
403
|
+
await handleJsonRpcLine(session, trailing);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
export function parseCompletionMcpArgs(argv) {
|
|
407
|
+
let worktreePath = null;
|
|
408
|
+
let workItemId = null;
|
|
409
|
+
let phase = "implementation";
|
|
410
|
+
let ownerType;
|
|
411
|
+
let ownerId;
|
|
412
|
+
let producedBy;
|
|
413
|
+
let developCycle = false;
|
|
414
|
+
let qaCycle = false;
|
|
415
|
+
let debugCycle = false;
|
|
416
|
+
let architectCycle = false;
|
|
417
|
+
let draftCycle = false;
|
|
418
|
+
let decomposeCycle = false;
|
|
419
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
420
|
+
const arg = argv[index];
|
|
421
|
+
if (arg === "--worktree") {
|
|
422
|
+
worktreePath = argv[++index] ?? null;
|
|
423
|
+
}
|
|
424
|
+
else if (arg === "--work-item-id") {
|
|
425
|
+
workItemId = argv[++index] ?? null;
|
|
426
|
+
}
|
|
427
|
+
else if (arg === "--owner-type") {
|
|
428
|
+
ownerType = normalizedOwnerType(argv[++index]);
|
|
429
|
+
if (!ownerType) {
|
|
430
|
+
return { ok: false, error: "mcp --owner-type must be work_item or story." };
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else if (arg === "--owner-id") {
|
|
434
|
+
ownerId = argv[++index] ?? undefined;
|
|
435
|
+
}
|
|
436
|
+
else if (arg === "--phase") {
|
|
437
|
+
const rawPhase = argv[++index];
|
|
438
|
+
const parsedPhase = parseStrictCompletionPhase(rawPhase);
|
|
439
|
+
if (!parsedPhase) {
|
|
440
|
+
return { ok: false, error: "mcp --phase must be one of: implementation, self-review, validation, qa, architect, draft, debug-repro, debug-root-cause, debug-fix, debug-regression." };
|
|
441
|
+
}
|
|
442
|
+
phase = parsedPhase;
|
|
443
|
+
}
|
|
444
|
+
else if (arg === "--produced-by") {
|
|
445
|
+
producedBy = argv[++index] ?? undefined;
|
|
446
|
+
}
|
|
447
|
+
else if (arg === "--develop-cycle") {
|
|
448
|
+
developCycle = true;
|
|
449
|
+
}
|
|
450
|
+
else if (arg === "--qa-cycle") {
|
|
451
|
+
qaCycle = true;
|
|
452
|
+
}
|
|
453
|
+
else if (arg === "--debug-cycle") {
|
|
454
|
+
debugCycle = true;
|
|
455
|
+
}
|
|
456
|
+
else if (arg === "--architect-cycle") {
|
|
457
|
+
architectCycle = true;
|
|
458
|
+
}
|
|
459
|
+
else if (arg === "--draft-cycle") {
|
|
460
|
+
draftCycle = true;
|
|
461
|
+
}
|
|
462
|
+
else if (arg === "--decompose-cycle") {
|
|
463
|
+
decomposeCycle = true;
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
return { ok: false, error: `Unknown mcp argument: ${arg}` };
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (!worktreePath?.trim() || !workItemId?.trim()) {
|
|
470
|
+
return { ok: false, error: "mcp requires --worktree and --work-item-id." };
|
|
471
|
+
}
|
|
472
|
+
if ([developCycle, qaCycle, debugCycle, architectCycle, draftCycle, decomposeCycle].filter(Boolean).length > 1) {
|
|
473
|
+
return { ok: false, error: "mcp accepts only one cycle mode." };
|
|
474
|
+
}
|
|
475
|
+
if (developCycle && !isDevelopCompletionPhase(phase)) {
|
|
476
|
+
return { ok: false, error: "mcp --develop-cycle requires --phase implementation, self-review, or validation." };
|
|
477
|
+
}
|
|
478
|
+
if (qaCycle && phase !== "qa") {
|
|
479
|
+
return { ok: false, error: "mcp --qa-cycle requires --phase qa." };
|
|
480
|
+
}
|
|
481
|
+
if (debugCycle && !isDebugCompletionPhase(phase)) {
|
|
482
|
+
return { ok: false, error: "mcp --debug-cycle requires --phase debug-repro, debug-root-cause, debug-fix, or debug-regression." };
|
|
483
|
+
}
|
|
484
|
+
if (architectCycle && phase !== "architect") {
|
|
485
|
+
return { ok: false, error: "mcp --architect-cycle requires --phase architect." };
|
|
486
|
+
}
|
|
487
|
+
if (draftCycle && phase !== "draft") {
|
|
488
|
+
return { ok: false, error: "mcp --draft-cycle requires --phase draft." };
|
|
489
|
+
}
|
|
490
|
+
if (decomposeCycle && phase !== "decompose") {
|
|
491
|
+
return { ok: false, error: "mcp --decompose-cycle requires --phase decompose." };
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
ok: true,
|
|
495
|
+
config: {
|
|
496
|
+
worktreePath: path.resolve(worktreePath),
|
|
497
|
+
workItemId,
|
|
498
|
+
ownerType,
|
|
499
|
+
ownerId,
|
|
500
|
+
phase,
|
|
501
|
+
producedBy,
|
|
502
|
+
redactionSecrets: parseCompletionRedactionSecrets(process.env[completionRedactionSecretsEnv]),
|
|
503
|
+
developCycle,
|
|
504
|
+
qaCycle,
|
|
505
|
+
debugCycle,
|
|
506
|
+
architectCycle,
|
|
507
|
+
draftCycle,
|
|
508
|
+
decomposeCycle
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
export function completionRedactionEnv(secrets) {
|
|
513
|
+
const filtered = secrets.filter((secret) => secret.length > 0);
|
|
514
|
+
return filtered.length > 0
|
|
515
|
+
? { [completionRedactionSecretsEnv]: JSON.stringify(filtered) }
|
|
516
|
+
: {};
|
|
517
|
+
}
|
|
518
|
+
async function prepareCursorCompletionToolRuntime(worktreePath, runtime) {
|
|
519
|
+
const cursorDir = path.join(worktreePath, ".cursor");
|
|
520
|
+
const configPath = path.join(cursorDir, "mcp.json");
|
|
521
|
+
const original = await fs.readFile(configPath, "utf8").then((content) => ({ existed: true, content }), () => ({ existed: false, content: "" }));
|
|
522
|
+
const existing = original.existed ? parseOptionalJsonObject(original.content, configPath) : {};
|
|
523
|
+
const servers = asMutableRecord(existing.mcpServers);
|
|
524
|
+
servers[runtime.serverName] = cursorMcpServerConfig(runtime);
|
|
525
|
+
const next = {
|
|
526
|
+
...existing,
|
|
527
|
+
mcpServers: servers
|
|
528
|
+
};
|
|
529
|
+
await fs.mkdir(cursorDir, { recursive: true });
|
|
530
|
+
await writeFileAtomic(configPath, `${JSON.stringify(next, null, 2)}\n`);
|
|
531
|
+
return {
|
|
532
|
+
...runtime,
|
|
533
|
+
cursorMcpConfigPath: configPath,
|
|
534
|
+
cleanup: async () => {
|
|
535
|
+
if (original.existed) {
|
|
536
|
+
await writeFileAtomic(configPath, original.content);
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
await fs.rm(configPath, { force: true });
|
|
540
|
+
await fs.rmdir(cursorDir).catch(() => undefined);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
async function handleJsonRpcLine(session, rawLine) {
|
|
546
|
+
let message;
|
|
547
|
+
try {
|
|
548
|
+
message = JSON.parse(rawLine);
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
writeJsonRpc({ jsonrpc: "2.0", id: null, error: rpcError(-32700, errorMessage(error)) });
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const request = asJsonRpcMessage(message);
|
|
555
|
+
if (!request) {
|
|
556
|
+
writeJsonRpc({ jsonrpc: "2.0", id: null, error: rpcError(-32600, "Invalid JSON-RPC message.") });
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (request.id === undefined) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
const result = await handleJsonRpcRequest(session, request.method, request.params);
|
|
564
|
+
writeJsonRpc({ jsonrpc: "2.0", id: request.id, result });
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
writeJsonRpc({ jsonrpc: "2.0", id: request.id, error: rpcError(-32000, errorMessage(error)) });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
async function handleJsonRpcRequest(session, method, params) {
|
|
571
|
+
if (method === "initialize") {
|
|
572
|
+
const initializeParams = params && typeof params === "object" && !Array.isArray(params)
|
|
573
|
+
? params
|
|
574
|
+
: {};
|
|
575
|
+
return {
|
|
576
|
+
protocolVersion: typeof initializeParams.protocolVersion === "string" ? initializeParams.protocolVersion : "2025-06-18",
|
|
577
|
+
capabilities: {
|
|
578
|
+
tools: {}
|
|
579
|
+
},
|
|
580
|
+
serverInfo: {
|
|
581
|
+
name: "forge-completion-tools",
|
|
582
|
+
version: "0.1.0"
|
|
583
|
+
},
|
|
584
|
+
instructions: session.instructions
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
if (method === "ping") {
|
|
588
|
+
return {};
|
|
589
|
+
}
|
|
590
|
+
if (method === "tools/list") {
|
|
591
|
+
return {
|
|
592
|
+
tools: session.toolDefinitions()
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
if (method === "tools/call") {
|
|
596
|
+
const call = asRecord(params, "tools/call params");
|
|
597
|
+
const name = requiredString(call, "name");
|
|
598
|
+
const args = call.arguments ?? {};
|
|
599
|
+
return await session.callTool(name, args);
|
|
600
|
+
}
|
|
601
|
+
throw new Error(`Unsupported MCP method: ${method}`);
|
|
602
|
+
}
|
|
603
|
+
function toolDefinition(name, phase, qaGate = null, decomposeGate = null) {
|
|
604
|
+
return {
|
|
605
|
+
name,
|
|
606
|
+
description: toolDescription(phase, qaGate, decomposeGate),
|
|
607
|
+
inputSchema: toolInputSchema(phase, qaGate, decomposeGate)
|
|
608
|
+
};
|
|
609
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type CompletionPhase, type DecomposeCompletionGate, type QaCompletionGate } from "./phase-completion.js";
|
|
2
|
+
export declare function toolDescription(phase: CompletionPhase, qaGate?: QaCompletionGate | null, decomposeGate?: DecomposeCompletionGate | null): string;
|
|
3
|
+
export declare function toolInputSchema(phase: CompletionPhase, qaGate?: QaCompletionGate | null, decomposeGate?: DecomposeCompletionGate | null): unknown;
|