@loucompanion/forge-bridge 0.1.3-dev.f66395c3ac21 → 0.1.3-dev.fbe15023cdfe

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.
Files changed (150) hide show
  1. package/README.md +78 -0
  2. package/dist/bridge/bridge.base.d.ts +6 -1
  3. package/dist/bridge/bridge.service.d.ts +60 -3
  4. package/dist/bridge/bridge.service.js +970 -24
  5. package/dist/bridge/bridge.types.d.ts +172 -1
  6. package/dist/cleanup/cleanup.service.d.ts +13 -2
  7. package/dist/cleanup/cleanup.service.js +435 -3
  8. package/dist/cleanup/cleanup.types.d.ts +10 -0
  9. package/dist/cleanup/index.d.ts +1 -1
  10. package/dist/cleanup/index.js +1 -1
  11. package/dist/cleanup/internals.d.ts +2 -2
  12. package/dist/cleanup/internals.js +1 -1
  13. package/dist/cli/cli.help.d.ts +2 -0
  14. package/dist/cli/cli.help.js +217 -0
  15. package/dist/cli/cli.service.d.ts +42 -2
  16. package/dist/cli/cli.service.js +356 -205
  17. package/dist/cli/local-config.d.ts +6 -0
  18. package/dist/cli/local-config.js +47 -3
  19. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  20. package/dist/runtime/attempt-node-runtime-launcher.js +101 -0
  21. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  22. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  23. package/dist/runtime/attempt-process-supervisor.d.ts +86 -0
  24. package/dist/runtime/attempt-process-supervisor.js +350 -0
  25. package/dist/runtime/generated-output-reset.d.ts +10 -0
  26. package/dist/runtime/generated-output-reset.js +103 -0
  27. package/dist/runtime/host-listener-inspector.d.ts +44 -0
  28. package/dist/runtime/host-listener-inspector.js +324 -0
  29. package/dist/runtime/host-port-broker.d.ts +76 -0
  30. package/dist/runtime/host-port-broker.js +333 -0
  31. package/dist/runtime/host-runtime-contract.d.ts +47 -0
  32. package/dist/runtime/host-runtime-contract.js +700 -0
  33. package/dist/runtime/host-runtime-lease.d.ts +133 -0
  34. package/dist/runtime/host-runtime-lease.js +1460 -0
  35. package/dist/runtime/host-runtime-promotion.d.ts +23 -0
  36. package/dist/runtime/host-runtime-promotion.js +159 -0
  37. package/dist/runtime/node-host-runtime-adapter.d.ts +51 -0
  38. package/dist/runtime/node-host-runtime-adapter.js +268 -0
  39. package/dist/runtime/playwright-host-runtime-adapter.d.ts +20 -0
  40. package/dist/runtime/playwright-host-runtime-adapter.js +220 -0
  41. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  42. package/dist/runtime/runtime-isolation-lease.js +158 -0
  43. package/dist/sandbox/command-runner.d.ts +4 -0
  44. package/dist/sandbox/command-runner.js +47 -0
  45. package/dist/sandbox/contract.d.ts +5 -0
  46. package/dist/sandbox/contract.js +276 -0
  47. package/dist/sandbox/event-budget.d.ts +11 -0
  48. package/dist/sandbox/event-budget.js +27 -0
  49. package/dist/sandbox/image-policy.d.ts +35 -0
  50. package/dist/sandbox/image-policy.js +247 -0
  51. package/dist/sandbox/index.d.ts +5 -0
  52. package/dist/sandbox/index.js +5 -0
  53. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  54. package/dist/sandbox/lease-watchdog.js +67 -0
  55. package/dist/sandbox/preflight.d.ts +54 -0
  56. package/dist/sandbox/preflight.js +335 -0
  57. package/dist/sandbox/process.d.ts +20 -0
  58. package/dist/sandbox/process.js +209 -0
  59. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  60. package/dist/sandbox/provider-auth/codex.js +212 -0
  61. package/dist/sandbox/repository-lock.d.ts +5 -0
  62. package/dist/sandbox/repository-lock.js +119 -0
  63. package/dist/sandbox/runtime.d.ts +297 -0
  64. package/dist/sandbox/runtime.js +2338 -0
  65. package/dist/sandbox/sandbox.types.d.ts +86 -0
  66. package/dist/sandbox/sandbox.types.js +1 -0
  67. package/dist/sandbox/smoke.d.ts +76 -0
  68. package/dist/sandbox/smoke.js +380 -0
  69. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  70. package/dist/sandbox/worker-supervisor.js +36 -0
  71. package/dist/sandbox/worker.d.ts +1 -0
  72. package/dist/sandbox/worker.js +225 -0
  73. package/dist/sandbox/workspace.service.d.ts +13 -0
  74. package/dist/sandbox/workspace.service.js +72 -0
  75. package/dist/session/managed-attempt-tools.d.ts +12 -0
  76. package/dist/session/managed-attempt-tools.js +561 -0
  77. package/dist/session/phase-completion.artifacts.d.ts +10 -0
  78. package/dist/session/phase-completion.artifacts.js +627 -0
  79. package/dist/session/phase-completion.d.ts +43 -42
  80. package/dist/session/phase-completion.helpers.d.ts +112 -0
  81. package/dist/session/phase-completion.helpers.js +684 -0
  82. package/dist/session/phase-completion.js +114 -3007
  83. package/dist/session/phase-completion.prompts.d.ts +14 -0
  84. package/dist/session/phase-completion.prompts.js +234 -0
  85. package/dist/session/phase-completion.runtime.d.ts +47 -0
  86. package/dist/session/phase-completion.runtime.js +731 -0
  87. package/dist/session/phase-completion.schemas.d.ts +3 -0
  88. package/dist/session/phase-completion.schemas.js +812 -0
  89. package/dist/session/phase-completion.validation.d.ts +37 -0
  90. package/dist/session/phase-completion.validation.js +828 -0
  91. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  92. package/dist/session/provider-cli/ci-repair-readiness.js +90 -0
  93. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +93 -0
  94. package/dist/session/provider-cli/provider-cli.artifacts.js +778 -0
  95. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  96. package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
  97. package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
  98. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +14 -0
  99. package/dist/session/provider-cli/provider-cli.deterministic.js +375 -0
  100. package/dist/session/provider-cli/provider-cli.process.d.ts +48 -0
  101. package/dist/session/provider-cli/provider-cli.process.js +474 -0
  102. package/dist/session/provider-cli/provider-cli.prompts.d.ts +47 -0
  103. package/dist/session/provider-cli/provider-cli.prompts.js +595 -0
  104. package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
  105. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +66 -0
  106. package/dist/session/provider-cli/provider-cli.runtime.d.ts +9 -0
  107. package/dist/session/provider-cli/provider-cli.runtime.js +41 -0
  108. package/dist/session/provider-cli/provider-cli.service.d.ts +22 -53
  109. package/dist/session/provider-cli/provider-cli.service.js +587 -2530
  110. package/dist/session/provider-cli/provider-cli.types.d.ts +6 -0
  111. package/dist/session/provider-cli/provider-output-state.d.ts +22 -0
  112. package/dist/session/provider-cli/provider-output-state.js +473 -0
  113. package/dist/session/session-redaction.d.ts +3 -0
  114. package/dist/session/session-redaction.js +23 -0
  115. package/dist/session/session.base.d.ts +6 -1
  116. package/dist/session/session.service.d.ts +8 -2
  117. package/dist/session/session.service.js +463 -58
  118. package/dist/session/session.types.d.ts +177 -1
  119. package/dist/session/session.types.js +352 -3
  120. package/dist/shared/path/path.js +3 -1
  121. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  122. package/dist/shared/process/host-process-recovery.js +78 -0
  123. package/dist/shared/process/host-process-supervisor.d.ts +57 -0
  124. package/dist/shared/process/host-process-supervisor.js +282 -0
  125. package/dist/shared/process/process.d.ts +1 -0
  126. package/dist/shared/process/process.js +1 -0
  127. package/dist/shared/process/windows-launch.d.ts +8 -0
  128. package/dist/shared/process/windows-launch.js +41 -0
  129. package/dist/shared/provider-capabilities.d.ts +1 -0
  130. package/dist/shared/provider-capabilities.js +3 -0
  131. package/dist/shared/redaction/redaction.js +10 -3
  132. package/dist/version.d.ts +1 -1
  133. package/dist/version.js +1 -1
  134. package/dist/worktree/artifact-injector.d.ts +3 -2
  135. package/dist/worktree/artifact-injector.js +82 -10
  136. package/dist/worktree/dependency-warmer.d.ts +21 -1
  137. package/dist/worktree/dependency-warmer.js +138 -5
  138. package/dist/worktree/index.d.ts +1 -0
  139. package/dist/worktree/index.js +1 -0
  140. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  141. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  142. package/dist/worktree/worktree-paths.d.ts +4 -0
  143. package/dist/worktree/worktree-paths.js +30 -0
  144. package/dist/worktree/worktree.base.d.ts +3 -0
  145. package/dist/worktree/worktree.service.d.ts +9 -3
  146. package/dist/worktree/worktree.service.js +100 -52
  147. package/dist/worktree/worktree.types.d.ts +3 -1
  148. package/package.json +6 -3
  149. package/runtime/windows-job-supervisor.ps1 +356 -0
  150. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -0,0 +1,731 @@
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, artifactOwnerRelativeRoot, 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
+ import { callManagedAttemptTool, isManagedAttemptTool, managedAttemptToolDefinition, managedAttemptToolNames, } from "./managed-attempt-tools.js";
9
+ export async function prepareCompletionToolRuntime(options) {
10
+ const phase = options.phase ?? completionPhaseFromDispatch(options.dispatch);
11
+ const worktreePath = path.resolve(options.worktree.worktreePath);
12
+ const workItemId = options.dispatch.workItem.id;
13
+ const ownerType = normalizedOwnerType(options.dispatch.cycleRun.ownerType) ?? "work_item";
14
+ const ownerId = options.dispatch.cycleRun.ownerId ?? workItemId;
15
+ const mcpArgs = buildCompletionToolServerArgs({
16
+ worktreePath,
17
+ workItemId,
18
+ ownerType,
19
+ ownerId,
20
+ phase,
21
+ producedBy: `${options.dispatch.execution.provider}-cli`,
22
+ developCycle: usesDevelopCycleCompletion(options.dispatch),
23
+ qaCycle: usesQaCycleCompletion(options.dispatch),
24
+ debugCycle: usesDebugCycleCompletion(options.dispatch),
25
+ architectCycle: usesArchitectCycleCompletion(options.dispatch),
26
+ draftCycle: (phase === "draft"
27
+ || options.dispatch.execution.lane.trim().toLowerCase() === "draft"
28
+ || options.dispatch.cycleRun.kind.trim().toLowerCase() === "draft")
29
+ && !isDecomposeDispatch(options.dispatch),
30
+ decomposeCycle: isDecomposeDispatch(options.dispatch)
31
+ });
32
+ const runtime = {
33
+ serverName: toolServerName,
34
+ command: process.execPath,
35
+ args: [forgeBridgeCliBinPath(), ...mcpArgs],
36
+ env: completionToolRuntimeEnvironment(options.runtimeEnvironment),
37
+ cleanup: async () => undefined
38
+ };
39
+ if (options.dispatch.execution.provider.trim().toLowerCase() !== "cursor") {
40
+ return runtime;
41
+ }
42
+ return await prepareCursorCompletionToolRuntime(worktreePath, runtime);
43
+ }
44
+ export function buildCompletionToolServerArgs(config) {
45
+ const args = [
46
+ "mcp",
47
+ "--worktree",
48
+ path.resolve(config.worktreePath),
49
+ "--work-item-id",
50
+ config.workItemId,
51
+ "--phase",
52
+ config.phase,
53
+ "--produced-by",
54
+ config.producedBy ?? "forge-completion-tool"
55
+ ];
56
+ if (config.ownerType || config.ownerId) {
57
+ args.push("--owner-type", normalizedOwnerType(config.ownerType) ?? "work_item");
58
+ args.push("--owner-id", config.ownerId ?? config.workItemId);
59
+ }
60
+ if (config.developCycle) {
61
+ args.push("--develop-cycle");
62
+ }
63
+ if (config.qaCycle) {
64
+ args.push("--qa-cycle");
65
+ }
66
+ if (config.debugCycle) {
67
+ args.push("--debug-cycle");
68
+ }
69
+ if (config.architectCycle) {
70
+ args.push("--architect-cycle");
71
+ }
72
+ if (config.draftCycle) {
73
+ args.push("--draft-cycle");
74
+ }
75
+ if (config.decomposeCycle) {
76
+ args.push("--decompose-cycle");
77
+ }
78
+ return args;
79
+ }
80
+ export function codexMcpConfigArgs(runtime) {
81
+ const prefix = `mcp_servers.${runtime.serverName}`;
82
+ const args = [
83
+ "-c",
84
+ `${prefix}.command=${tomlString(runtime.command)}`,
85
+ "-c",
86
+ `${prefix}.args=${tomlStringArray(runtime.args)}`,
87
+ "-c",
88
+ `${prefix}.default_tools_approval_mode=${tomlString("approve")}`,
89
+ "-c",
90
+ `${prefix}.tool_timeout_sec=7200`
91
+ ];
92
+ for (const [key, value] of Object.entries(runtime.env)) {
93
+ args.push("-c", `${prefix}.env.${key}=${tomlString(value)}`);
94
+ }
95
+ return args;
96
+ }
97
+ export function cursorMcpServerConfig(runtime) {
98
+ return {
99
+ command: runtime.command,
100
+ args: [...runtime.args],
101
+ env: runtime.env
102
+ };
103
+ }
104
+ export function completionToolNamesForServer(config) {
105
+ return config.decomposeCycle
106
+ ? decomposeCompletionGateOrder.flatMap((gate) => completionToolNamesForDecomposeGate(gate))
107
+ : config.draftCycle
108
+ ? draftCompletionGateOrder.flatMap((gate) => completionToolNamesForDraftGate(gate))
109
+ : config.debugCycle
110
+ ? debugCompletionPhaseOrder.flatMap((gate) => completionToolNamesForDebugGate(gate))
111
+ : config.qaCycle
112
+ ? qaCompletionGateOrder.flatMap((gate) => completionToolNamesForQaGate(gate))
113
+ : config.developCycle
114
+ ? completionPhaseOrder.flatMap((phase) => completionToolNamesForPhase(phase))
115
+ : config.architectCycle
116
+ ? completionToolNamesForPhase("architect")
117
+ : completionToolNamesForPhase(config.phase);
118
+ }
119
+ export class CompletionToolServerSession {
120
+ config;
121
+ currentPhase;
122
+ currentQaGate = "qa_plan";
123
+ currentDraftGate = "draft_intent";
124
+ currentDecomposeGate = "decompose_subject_intake";
125
+ managedCommandReceipts = [];
126
+ completed = false;
127
+ constructor(config) {
128
+ this.config = config;
129
+ this.currentPhase = config.phase;
130
+ }
131
+ get instructions() {
132
+ return this.config.decomposeCycle
133
+ ? completionPromptForDecomposeCycle(this.currentDecomposeGate)
134
+ : this.config.draftCycle
135
+ ? completionPromptForDraftCycle(this.currentDraftGate)
136
+ : this.config.debugCycle
137
+ ? completionPromptForDebugCycle(asDebugCompletionPhase(this.currentPhase))
138
+ : this.config.qaCycle
139
+ ? completionPromptForQaCycle(this.currentQaGate, artifactOwnerRelativeRoot(this.config.ownerType, this.config.ownerId, this.config.workItemId))
140
+ : this.config.developCycle
141
+ ? completionPromptForDevelopCycle(this.currentPhase)
142
+ : completionPromptForPhase(this.currentPhase, artifactOwnerRelativeRoot(this.config.ownerType, this.config.ownerId, this.config.workItemId));
143
+ }
144
+ toolDefinitions() {
145
+ const completionTools = completionToolNamesForServer(this.config).map((name) => toolDefinition(name, phaseFromToolName(name), qaGateFromToolName(name), decomposeGateFromToolName(name)));
146
+ return managedAttemptToolsAvailable()
147
+ ? [...completionTools, ...managedAttemptToolNames.map(managedAttemptToolDefinition)]
148
+ : completionTools;
149
+ }
150
+ async callTool(toolName, input) {
151
+ if (isManagedAttemptTool(toolName)) {
152
+ if (!managedAttemptToolsAvailable()) {
153
+ throw new Error("Forge managed-attempt tools require an active managed host runtime.");
154
+ }
155
+ const result = await callManagedAttemptTool(toolName, input, this.config.worktreePath);
156
+ if (Array.isArray(result.structuredContent.command)) {
157
+ this.managedCommandReceipts.push(result.structuredContent);
158
+ }
159
+ return result;
160
+ }
161
+ const requestedPhase = phaseFromToolName(toolName);
162
+ if (this.completed) {
163
+ throw new Error("Forge completion cycle is already complete; no more completion calls are accepted.");
164
+ }
165
+ if (this.config.decomposeCycle) {
166
+ return await this.callDecomposeGateTool(toolName, requestedPhase, input);
167
+ }
168
+ if (this.config.draftCycle) {
169
+ return await this.callDraftGateTool(toolName, requestedPhase, input);
170
+ }
171
+ if (this.config.debugCycle) {
172
+ return await this.callDebugGateTool(toolName, requestedPhase, input);
173
+ }
174
+ if (this.config.qaCycle) {
175
+ return await this.callQaGateTool(toolName, requestedPhase, input);
176
+ }
177
+ if (requestedPhase !== this.currentPhase) {
178
+ throw new Error(`Tool ${toolName} is out of order. Current Forge phase is ${this.currentPhase}.`);
179
+ }
180
+ const completedPhase = this.currentPhase;
181
+ const reference = await writeCompletionArtifact({ ...this.config, phase: completedPhase }, toolName, input);
182
+ const status = reference.status.trim().toLowerCase();
183
+ const pass = status === "pass";
184
+ const nextPhase = pass
185
+ ? this.advanceAfterPass(completedPhase)
186
+ : completedPhase;
187
+ const nextInstruction = completionToolNextInstruction(completedPhase, status, nextPhase);
188
+ const reason = pass
189
+ ? `${completedPhase} completion artifact accepted.`
190
+ : `${completedPhase} completion artifact recorded with status ${status}; rework is required before phase progression.`;
191
+ return {
192
+ content: [
193
+ {
194
+ type: "text",
195
+ text: `Forge recorded ${reference.phase} completion artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}`
196
+ }
197
+ ],
198
+ structuredContent: {
199
+ ...reference,
200
+ gate: {
201
+ phase: completedPhase,
202
+ status,
203
+ allowed: pass,
204
+ reason
205
+ },
206
+ nextPhase,
207
+ nextInstruction
208
+ }
209
+ };
210
+ }
211
+ async callDecomposeGateTool(toolName, requestedPhase, input) {
212
+ const requestedGate = decomposeGateFromToolName(toolName);
213
+ if (requestedPhase !== "decompose" || !requestedGate) {
214
+ throw new Error(`Tool ${toolName} is not available in Decompose-cycle mode. Current Forge Decompose gate is ${this.currentDecomposeGate}.`);
215
+ }
216
+ if (requestedGate !== this.currentDecomposeGate) {
217
+ throw new Error(`Tool ${toolName} is out of order. Current Forge Decompose gate is ${this.currentDecomposeGate}.`);
218
+ }
219
+ const completedGate = this.currentDecomposeGate;
220
+ const reference = await writeDecomposeGateCompletionArtifact(this.config, completedGate, toolName, input);
221
+ const status = reference.status.trim().toLowerCase();
222
+ const pass = status === "pass";
223
+ const nextGate = pass ? this.advanceAfterDecomposeGate(completedGate) : completedGate;
224
+ const nextPhase = pass
225
+ ? (nextGate ? "decompose" : "sign_off_ready")
226
+ : "decompose";
227
+ const nextInstruction = completionToolNextInstructionForDecomposeGate(completedGate, status, nextGate);
228
+ const reason = pass
229
+ ? nextGate
230
+ ? `${completedGate} artifact accepted; continue to ${nextGate}.`
231
+ : `Decompose proposal artifact accepted with status ${status}.`
232
+ : `${completedGate} artifact recorded with status ${status}; rework is required before continuing.`;
233
+ return {
234
+ content: [{ type: "text", text: `Forge recorded ${completedGate} artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}` }],
235
+ structuredContent: {
236
+ ...reference,
237
+ gate: { phase: "decompose", decomposeGate: completedGate, status, allowed: pass, reason },
238
+ nextPhase,
239
+ nextInstruction
240
+ }
241
+ };
242
+ }
243
+ async callDraftGateTool(toolName, requestedPhase, input) {
244
+ const requestedGate = draftGateFromToolName(toolName);
245
+ if (requestedPhase !== "draft" || !requestedGate) {
246
+ throw new Error(`Tool ${toolName} is not available in Draft-cycle mode. Current Forge Draft gate is ${this.currentDraftGate}.`);
247
+ }
248
+ if (requestedGate !== this.currentDraftGate) {
249
+ throw new Error(`Tool ${toolName} is out of order. Current Forge Draft gate is ${this.currentDraftGate}.`);
250
+ }
251
+ const completedGate = this.currentDraftGate;
252
+ const reference = await writeDraftGateCompletionArtifact(this.config, completedGate, toolName, input);
253
+ const status = reference.status.trim().toLowerCase();
254
+ const nextGate = this.advanceAfterDraftGate(completedGate);
255
+ const nextPhase = nextGate ? "draft" : "sign_off_ready";
256
+ const nextInstruction = completionToolNextInstructionForDraftGate(completedGate, status, nextGate);
257
+ return {
258
+ content: [{ type: "text", text: `Forge recorded ${completedGate} artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}` }],
259
+ structuredContent: {
260
+ ...reference,
261
+ gate: { phase: "draft", draftGate: completedGate, status, allowed: status !== "blocked", reason: nextGate ? `${completedGate} artifact accepted; continue to ${nextGate}.` : `Draft proposal artifact accepted with status ${status}.` },
262
+ nextPhase,
263
+ nextInstruction
264
+ }
265
+ };
266
+ }
267
+ async callDebugGateTool(toolName, requestedPhase, input) {
268
+ const currentDebugGate = asDebugCompletionPhase(this.currentPhase);
269
+ if (!isDebugCompletionPhase(requestedPhase)) {
270
+ throw new Error(`Tool ${toolName} is not available in Debug-cycle mode. Current Forge Debug gate is ${currentDebugGate}.`);
271
+ }
272
+ if (requestedPhase !== currentDebugGate) {
273
+ throw new Error(`Tool ${toolName} is out of order. Current Forge Debug gate is ${currentDebugGate}.`);
274
+ }
275
+ const completedGate = currentDebugGate;
276
+ const reference = await writeCompletionArtifact({ ...this.config, phase: completedGate }, toolName, input);
277
+ const status = reference.status.trim().toLowerCase();
278
+ const pass = status === "pass";
279
+ const nextGate = pass ? this.advanceAfterDebugGate(completedGate) : completedGate;
280
+ const nextPhase = nextGate ?? "qa_ready";
281
+ const nextInstruction = completionToolNextInstructionForDebugGate(completedGate, status, nextGate);
282
+ const reason = pass
283
+ ? `${completedGate} artifact accepted${nextGate ? `; continue to ${nextGate}.` : "; Debug regression is QA-ready."}`
284
+ : `${completedGate} artifact recorded with status ${status}; rework is required before Debug progression.`;
285
+ return {
286
+ content: [
287
+ {
288
+ type: "text",
289
+ text: `Forge recorded ${completedGate} artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}`
290
+ }
291
+ ],
292
+ structuredContent: {
293
+ ...reference,
294
+ gate: {
295
+ phase: completedGate,
296
+ status,
297
+ allowed: pass,
298
+ reason
299
+ },
300
+ nextPhase,
301
+ nextInstruction
302
+ }
303
+ };
304
+ }
305
+ async callQaGateTool(toolName, requestedPhase, input) {
306
+ const requestedGate = qaGateFromToolName(toolName);
307
+ if (requestedPhase !== "qa" || !requestedGate) {
308
+ throw new Error(`Tool ${toolName} is not available in QA-cycle mode. Current Forge QA gate is ${this.currentQaGate}.`);
309
+ }
310
+ if (requestedGate !== this.currentQaGate) {
311
+ throw new Error(`Tool ${toolName} is out of order. Current Forge QA gate is ${this.currentQaGate}.`);
312
+ }
313
+ const completedGate = this.currentQaGate;
314
+ if (completedGate === "qa_regression" && managedAttemptToolsAvailable()) {
315
+ assertManagedQaRegressionEvidence(input, this.managedCommandReceipts);
316
+ }
317
+ const reference = await writeCompletionArtifact({ ...this.config, phase: "qa" }, toolName, input);
318
+ const status = reference.status.trim().toLowerCase();
319
+ const nextGate = this.advanceAfterQaGate(completedGate);
320
+ const nextPhase = nextGate ? "qa" : "sign_off_ready";
321
+ const nextInstruction = completionToolNextInstructionForQaGate(completedGate, status, nextGate);
322
+ const allowed = status !== "blocked";
323
+ const reason = nextGate
324
+ ? `${completedGate} artifact accepted; continue to ${nextGate}.`
325
+ : `QA verdict artifact accepted with status ${status}.`;
326
+ return {
327
+ content: [
328
+ {
329
+ type: "text",
330
+ text: `Forge recorded ${completedGate} artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}`
331
+ }
332
+ ],
333
+ structuredContent: {
334
+ ...reference,
335
+ gate: {
336
+ phase: "qa",
337
+ qaGate: completedGate,
338
+ status,
339
+ allowed,
340
+ reason
341
+ },
342
+ nextPhase,
343
+ nextInstruction
344
+ }
345
+ };
346
+ }
347
+ advanceAfterPass(phase) {
348
+ if (phase === "qa") {
349
+ this.completed = true;
350
+ return "sign_off_ready";
351
+ }
352
+ if (phase === "ci_repair") {
353
+ this.completed = true;
354
+ return "provider_checks_ready";
355
+ }
356
+ if (isDebugCompletionPhase(phase)) {
357
+ return nextDebugPhaseAfter(phase) ?? "qa_ready";
358
+ }
359
+ const next = nextPhaseAfter(phase);
360
+ if (!this.config.developCycle) {
361
+ return next ?? "dev_test_ready";
362
+ }
363
+ if (next) {
364
+ this.currentPhase = next;
365
+ return next;
366
+ }
367
+ this.completed = true;
368
+ return "dev_test_ready";
369
+ }
370
+ advanceAfterDecomposeGate(gate) {
371
+ const index = decomposeCompletionGateOrder.indexOf(gate);
372
+ const next = index >= 0 ? decomposeCompletionGateOrder[index + 1] ?? null : null;
373
+ if (next) {
374
+ this.currentDecomposeGate = next;
375
+ return next;
376
+ }
377
+ this.completed = true;
378
+ return null;
379
+ }
380
+ advanceAfterDraftGate(gate) {
381
+ const index = draftCompletionGateOrder.indexOf(gate);
382
+ const next = index >= 0 ? draftCompletionGateOrder[index + 1] ?? null : null;
383
+ if (next) {
384
+ this.currentDraftGate = next;
385
+ return next;
386
+ }
387
+ this.completed = true;
388
+ return null;
389
+ }
390
+ advanceAfterQaGate(gate) {
391
+ const index = qaCompletionGateOrder.indexOf(gate);
392
+ const next = index >= 0 ? qaCompletionGateOrder[index + 1] ?? null : null;
393
+ if (next) {
394
+ this.currentQaGate = next;
395
+ return next;
396
+ }
397
+ this.completed = true;
398
+ return null;
399
+ }
400
+ advanceAfterDebugGate(gate) {
401
+ const index = debugCompletionPhaseOrder.indexOf(gate);
402
+ const next = index >= 0 ? debugCompletionPhaseOrder[index + 1] ?? null : null;
403
+ if (next) {
404
+ this.currentPhase = next;
405
+ return next;
406
+ }
407
+ this.completed = true;
408
+ return null;
409
+ }
410
+ }
411
+ const completionToolRuntimeEnvironmentNames = [
412
+ "FORGE_RUNTIME_MODE",
413
+ "FORGE_RESOURCE_LEASE_ID",
414
+ "FORGE_DISPATCH_ATTEMPT_ID",
415
+ "FORGE_LOG_DIR",
416
+ "FORGE_TEST_OUTPUT_DIR",
417
+ "FORGE_TMPDIR",
418
+ "TMPDIR",
419
+ "TMP",
420
+ "TEMP",
421
+ ];
422
+ export function completionToolRuntimeEnvironment(runtimeEnvironment) {
423
+ if (!runtimeEnvironment)
424
+ return {};
425
+ const env = {};
426
+ for (const name of completionToolRuntimeEnvironmentNames) {
427
+ const value = runtimeEnvironment[name]?.trim();
428
+ if (value)
429
+ env[name] = value;
430
+ }
431
+ return env;
432
+ }
433
+ const minimumManagedSolutionTestTimeoutMs = 45 * 60 * 1000;
434
+ /**
435
+ * A solution-wide Forge test is long enough that a generic provider shell can
436
+ * expire while VSTest is still healthy. On managed host attempts, only the
437
+ * attempt-owned command runner can prove the exact process outcome and clean
438
+ * up its process tree. Keep this check at the completion gate so prompt drift
439
+ * cannot turn an arbitrary shell timeout into a QA blocker.
440
+ */
441
+ export function assertManagedQaRegressionEvidence(input, receipts) {
442
+ const payload = asRecord(input, "QA regression completion payload");
443
+ const nestedEvidence = asMutableRecord(payload.evidence);
444
+ const commands = payload.commands ?? nestedEvidence.commands;
445
+ if (!Array.isArray(commands))
446
+ return;
447
+ for (const [index, entry] of commands.entries()) {
448
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
449
+ continue;
450
+ const command = entry.command;
451
+ if (typeof command !== "string" || !isForgeSolutionTestCommand(command))
452
+ continue;
453
+ const managedCommandId = entry.managed_command_id;
454
+ if (typeof managedCommandId !== "string" || !managedCommandId.trim()) {
455
+ throw new Error(`qa_regression_managed_command_id_required: commands[${index}] runs the solution-wide Forge test. `
456
+ + "Run it with forge.start_managed_command, continue independent work, call forge.wait_managed_command once for the terminal receipt, and include the returned managed_command_id in this command evidence.");
457
+ }
458
+ const matchingReceipts = receipts.filter((receipt) => {
459
+ const tokens = receipt.command;
460
+ return receipt.command_id === managedCommandId
461
+ && Array.isArray(tokens)
462
+ && tokens.every((token) => typeof token === "string")
463
+ && isForgeSolutionTestCommand(tokens.join(" "));
464
+ });
465
+ if (matchingReceipts.length === 0) {
466
+ throw new Error(`qa_regression_managed_command_required: commands[${index}] runs the solution-wide Forge test. `
467
+ + "Run it with forge.start_managed_command, continue independent work, call forge.wait_managed_command once for the terminal receipt, and include its matching managed_command_id; generic-shell timeout evidence is not accepted on a managed host.");
468
+ }
469
+ const terminalReceipts = matchingReceipts.filter((receipt) => typeof receipt.status === "string" && [
470
+ "pass",
471
+ "fail",
472
+ "timed_out",
473
+ "timeout_termination_failed",
474
+ "runner_error",
475
+ ].includes(receipt.status));
476
+ if (terminalReceipts.length === 0) {
477
+ throw new Error(`qa_regression_managed_command_in_progress: commands[${index}] has not produced a terminal managed receipt. `
478
+ + "Call forge.wait_managed_command once for its terminal receipt, then resubmit this gate.");
479
+ }
480
+ const adequateReceipt = terminalReceipts.some((receipt) => {
481
+ if (receipt.timed_out !== true)
482
+ return true;
483
+ return typeof receipt.timeout_ms === "number"
484
+ && receipt.timeout_ms >= minimumManagedSolutionTestTimeoutMs
485
+ && receipt.process_tree_terminated === true;
486
+ });
487
+ if (!adequateReceipt) {
488
+ throw new Error(`qa_regression_managed_timeout_too_short: commands[${index}] timed out before the minimum `
489
+ + `${minimumManagedSolutionTestTimeoutMs}ms solution-test budget. Rerun it with forge.start_managed_command `
490
+ + "using at least a 45-minute timeout and resubmit this gate.");
491
+ }
492
+ }
493
+ }
494
+ function isForgeSolutionTestCommand(command) {
495
+ const normalized = command.trim().toLowerCase().replaceAll("\\", "/");
496
+ return normalized.includes("dotnet")
497
+ && /(?:^|\s)test(?:\s|$)/u.test(normalized)
498
+ && /(?:^|[\s/"'])forge\.sln(?:[\s"']|$)/u.test(normalized);
499
+ }
500
+ function managedAttemptToolsAvailable() {
501
+ return process.env.FORGE_RUNTIME_MODE === "host"
502
+ && Boolean(process.env.FORGE_RESOURCE_LEASE_ID?.trim())
503
+ && Boolean(process.env.FORGE_DISPATCH_ATTEMPT_ID?.trim())
504
+ && Boolean(process.env.FORGE_LOG_DIR?.trim())
505
+ && Boolean(process.env.FORGE_TEST_OUTPUT_DIR?.trim());
506
+ }
507
+ export async function runCompletionMcpServer(config) {
508
+ const session = new CompletionToolServerSession(config);
509
+ process.stdin.setEncoding("utf8");
510
+ let buffer = "";
511
+ for await (const chunk of process.stdin) {
512
+ buffer += chunk;
513
+ let newlineIndex = buffer.indexOf("\n");
514
+ while (newlineIndex >= 0) {
515
+ const rawLine = buffer.slice(0, newlineIndex).trim();
516
+ buffer = buffer.slice(newlineIndex + 1);
517
+ if (rawLine.length > 0) {
518
+ await handleJsonRpcLine(session, rawLine);
519
+ }
520
+ newlineIndex = buffer.indexOf("\n");
521
+ }
522
+ }
523
+ const trailing = buffer.trim();
524
+ if (trailing.length > 0) {
525
+ await handleJsonRpcLine(session, trailing);
526
+ }
527
+ }
528
+ export function parseCompletionMcpArgs(argv) {
529
+ let worktreePath = null;
530
+ let workItemId = null;
531
+ let phase = "implementation";
532
+ let ownerType;
533
+ let ownerId;
534
+ let producedBy;
535
+ let developCycle = false;
536
+ let qaCycle = false;
537
+ let debugCycle = false;
538
+ let architectCycle = false;
539
+ let draftCycle = false;
540
+ let decomposeCycle = false;
541
+ for (let index = 1; index < argv.length; index += 1) {
542
+ const arg = argv[index];
543
+ if (arg === "--worktree") {
544
+ worktreePath = argv[++index] ?? null;
545
+ }
546
+ else if (arg === "--work-item-id") {
547
+ workItemId = argv[++index] ?? null;
548
+ }
549
+ else if (arg === "--owner-type") {
550
+ ownerType = normalizedOwnerType(argv[++index]);
551
+ if (!ownerType) {
552
+ return { ok: false, error: "mcp --owner-type must be work_item or story." };
553
+ }
554
+ }
555
+ else if (arg === "--owner-id") {
556
+ ownerId = argv[++index] ?? undefined;
557
+ }
558
+ else if (arg === "--phase") {
559
+ const rawPhase = argv[++index];
560
+ const parsedPhase = parseStrictCompletionPhase(rawPhase);
561
+ if (!parsedPhase) {
562
+ return { ok: false, error: "mcp --phase must be one of: implementation, self-review, validation, qa, architect, draft, decompose, ci-repair, debug-repro, debug-root-cause, debug-fix, debug-regression." };
563
+ }
564
+ phase = parsedPhase;
565
+ }
566
+ else if (arg === "--produced-by") {
567
+ producedBy = argv[++index] ?? undefined;
568
+ }
569
+ else if (arg === "--develop-cycle") {
570
+ developCycle = true;
571
+ }
572
+ else if (arg === "--qa-cycle") {
573
+ qaCycle = true;
574
+ }
575
+ else if (arg === "--debug-cycle") {
576
+ debugCycle = true;
577
+ }
578
+ else if (arg === "--architect-cycle") {
579
+ architectCycle = true;
580
+ }
581
+ else if (arg === "--draft-cycle") {
582
+ draftCycle = true;
583
+ }
584
+ else if (arg === "--decompose-cycle") {
585
+ decomposeCycle = true;
586
+ }
587
+ else {
588
+ return { ok: false, error: `Unknown mcp argument: ${arg}` };
589
+ }
590
+ }
591
+ if (!worktreePath?.trim() || !workItemId?.trim()) {
592
+ return { ok: false, error: "mcp requires --worktree and --work-item-id." };
593
+ }
594
+ if ([developCycle, qaCycle, debugCycle, architectCycle, draftCycle, decomposeCycle].filter(Boolean).length > 1) {
595
+ return { ok: false, error: "mcp accepts only one cycle mode." };
596
+ }
597
+ if (developCycle && !isDevelopCompletionPhase(phase)) {
598
+ return { ok: false, error: "mcp --develop-cycle requires --phase implementation, self-review, or validation." };
599
+ }
600
+ if (qaCycle && phase !== "qa") {
601
+ return { ok: false, error: "mcp --qa-cycle requires --phase qa." };
602
+ }
603
+ if (debugCycle && !isDebugCompletionPhase(phase)) {
604
+ return { ok: false, error: "mcp --debug-cycle requires --phase debug-repro, debug-root-cause, debug-fix, or debug-regression." };
605
+ }
606
+ if (architectCycle && phase !== "architect") {
607
+ return { ok: false, error: "mcp --architect-cycle requires --phase architect." };
608
+ }
609
+ if (draftCycle && phase !== "draft") {
610
+ return { ok: false, error: "mcp --draft-cycle requires --phase draft." };
611
+ }
612
+ if (decomposeCycle && phase !== "decompose") {
613
+ return { ok: false, error: "mcp --decompose-cycle requires --phase decompose." };
614
+ }
615
+ return {
616
+ ok: true,
617
+ config: {
618
+ worktreePath: path.resolve(worktreePath),
619
+ workItemId,
620
+ ownerType,
621
+ ownerId,
622
+ phase,
623
+ producedBy,
624
+ redactionSecrets: parseCompletionRedactionSecrets(process.env[completionRedactionSecretsEnv]),
625
+ developCycle,
626
+ qaCycle,
627
+ debugCycle,
628
+ architectCycle,
629
+ draftCycle,
630
+ decomposeCycle
631
+ }
632
+ };
633
+ }
634
+ export function completionRedactionEnv(secrets) {
635
+ const filtered = secrets.filter((secret) => secret.length > 0);
636
+ return filtered.length > 0
637
+ ? { [completionRedactionSecretsEnv]: JSON.stringify(filtered) }
638
+ : {};
639
+ }
640
+ async function prepareCursorCompletionToolRuntime(worktreePath, runtime) {
641
+ const cursorDir = path.join(worktreePath, ".cursor");
642
+ const configPath = path.join(cursorDir, "mcp.json");
643
+ const original = await fs.readFile(configPath, "utf8").then((content) => ({ existed: true, content }), () => ({ existed: false, content: "" }));
644
+ const existing = original.existed ? parseOptionalJsonObject(original.content, configPath) : {};
645
+ const servers = asMutableRecord(existing.mcpServers);
646
+ servers[runtime.serverName] = cursorMcpServerConfig(runtime);
647
+ const next = {
648
+ ...existing,
649
+ mcpServers: servers
650
+ };
651
+ await fs.mkdir(cursorDir, { recursive: true });
652
+ await writeFileAtomic(configPath, `${JSON.stringify(next, null, 2)}\n`);
653
+ return {
654
+ ...runtime,
655
+ cursorMcpConfigPath: configPath,
656
+ cleanup: async () => {
657
+ if (original.existed) {
658
+ await writeFileAtomic(configPath, original.content);
659
+ }
660
+ else {
661
+ await fs.rm(configPath, { force: true });
662
+ await fs.rmdir(cursorDir).catch(() => undefined);
663
+ }
664
+ }
665
+ };
666
+ }
667
+ async function handleJsonRpcLine(session, rawLine) {
668
+ let message;
669
+ try {
670
+ message = JSON.parse(rawLine);
671
+ }
672
+ catch (error) {
673
+ writeJsonRpc({ jsonrpc: "2.0", id: null, error: rpcError(-32700, errorMessage(error)) });
674
+ return;
675
+ }
676
+ const request = asJsonRpcMessage(message);
677
+ if (!request) {
678
+ writeJsonRpc({ jsonrpc: "2.0", id: null, error: rpcError(-32600, "Invalid JSON-RPC message.") });
679
+ return;
680
+ }
681
+ if (request.id === undefined) {
682
+ return;
683
+ }
684
+ try {
685
+ const result = await handleJsonRpcRequest(session, request.method, request.params);
686
+ writeJsonRpc({ jsonrpc: "2.0", id: request.id, result });
687
+ }
688
+ catch (error) {
689
+ writeJsonRpc({ jsonrpc: "2.0", id: request.id, error: rpcError(-32000, errorMessage(error)) });
690
+ }
691
+ }
692
+ async function handleJsonRpcRequest(session, method, params) {
693
+ if (method === "initialize") {
694
+ const initializeParams = params && typeof params === "object" && !Array.isArray(params)
695
+ ? params
696
+ : {};
697
+ return {
698
+ protocolVersion: typeof initializeParams.protocolVersion === "string" ? initializeParams.protocolVersion : "2025-06-18",
699
+ capabilities: {
700
+ tools: {}
701
+ },
702
+ serverInfo: {
703
+ name: "forge-completion-tools",
704
+ version: "0.1.0"
705
+ },
706
+ instructions: session.instructions
707
+ };
708
+ }
709
+ if (method === "ping") {
710
+ return {};
711
+ }
712
+ if (method === "tools/list") {
713
+ return {
714
+ tools: session.toolDefinitions()
715
+ };
716
+ }
717
+ if (method === "tools/call") {
718
+ const call = asRecord(params, "tools/call params");
719
+ const name = requiredString(call, "name");
720
+ const args = call.arguments ?? {};
721
+ return await session.callTool(name, args);
722
+ }
723
+ throw new Error(`Unsupported MCP method: ${method}`);
724
+ }
725
+ function toolDefinition(name, phase, qaGate = null, decomposeGate = null) {
726
+ return {
727
+ name,
728
+ description: toolDescription(phase, qaGate, decomposeGate),
729
+ inputSchema: toolInputSchema(phase, qaGate, decomposeGate)
730
+ };
731
+ }