@loucompanion/forge-bridge 0.1.3-dev.d46a8e73d03d → 0.1.3-dev.eeefdb0d19c1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/dist/bridge/bridge.base.d.ts +2 -1
- package/dist/bridge/bridge.service.d.ts +22 -3
- package/dist/bridge/bridge.service.js +416 -40
- package/dist/bridge/bridge.types.d.ts +59 -1
- package/dist/cleanup/cleanup.service.d.ts +13 -2
- package/dist/cleanup/cleanup.service.js +435 -3
- package/dist/cleanup/cleanup.types.d.ts +10 -0
- package/dist/cleanup/index.d.ts +1 -1
- package/dist/cleanup/index.js +1 -1
- package/dist/cleanup/internals.d.ts +2 -2
- package/dist/cleanup/internals.js +1 -1
- package/dist/cli/cli.help.js +37 -3
- package/dist/cli/cli.service.d.ts +30 -2
- package/dist/cli/cli.service.js +182 -23
- package/dist/cli/local-config.d.ts +4 -0
- package/dist/cli/local-config.js +41 -1
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +101 -0
- package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
- package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +86 -0
- package/dist/runtime/attempt-process-supervisor.js +343 -0
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +103 -0
- package/dist/runtime/host-listener-inspector.d.ts +44 -0
- package/dist/runtime/host-listener-inspector.js +324 -0
- package/dist/runtime/host-port-broker.d.ts +76 -0
- package/dist/runtime/host-port-broker.js +333 -0
- package/dist/runtime/host-runtime-contract.d.ts +47 -0
- package/dist/runtime/host-runtime-contract.js +700 -0
- package/dist/runtime/host-runtime-lease.d.ts +133 -0
- package/dist/runtime/host-runtime-lease.js +1460 -0
- package/dist/runtime/host-runtime-promotion.d.ts +23 -0
- package/dist/runtime/host-runtime-promotion.js +159 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +51 -0
- package/dist/runtime/node-host-runtime-adapter.js +268 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +20 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +220 -0
- package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +158 -0
- package/dist/sandbox/preflight.d.ts +14 -0
- package/dist/sandbox/preflight.js +91 -12
- package/dist/sandbox/process.js +125 -40
- package/dist/sandbox/repository-lock.js +37 -1
- package/dist/sandbox/runtime.d.ts +66 -3
- package/dist/sandbox/runtime.js +670 -105
- package/dist/sandbox/sandbox.types.d.ts +3 -0
- package/dist/sandbox/smoke.d.ts +76 -0
- package/dist/sandbox/smoke.js +380 -0
- package/dist/sandbox/worker-supervisor.js +1 -1
- package/dist/sandbox/worker.js +2 -5
- package/dist/sandbox/workspace.service.d.ts +1 -0
- package/dist/sandbox/workspace.service.js +28 -5
- package/dist/session/managed-attempt-tools.d.ts +12 -0
- package/dist/session/managed-attempt-tools.js +350 -0
- package/dist/session/phase-completion.artifacts.js +9 -2
- package/dist/session/phase-completion.d.ts +4 -2
- package/dist/session/phase-completion.helpers.js +27 -3
- package/dist/session/phase-completion.js +12 -3
- package/dist/session/phase-completion.prompts.d.ts +3 -3
- package/dist/session/phase-completion.prompts.js +19 -5
- package/dist/session/phase-completion.runtime.d.ts +12 -1
- package/dist/session/phase-completion.runtime.js +127 -7
- package/dist/session/phase-completion.schemas.js +108 -3
- package/dist/session/phase-completion.validation.d.ts +4 -1
- package/dist/session/phase-completion.validation.js +106 -2
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +90 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +1 -1
- package/dist/session/provider-cli/provider-cli.artifacts.js +28 -4
- package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
- package/dist/session/provider-cli/provider-cli.process.d.ts +16 -0
- package/dist/session/provider-cli/provider-cli.process.js +212 -25
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +4 -1
- package/dist/session/provider-cli/provider-cli.prompts.js +84 -17
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +3 -2
- package/dist/session/provider-cli/provider-cli.service.d.ts +3 -2
- package/dist/session/provider-cli/provider-cli.service.js +254 -71
- package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.js +30 -2
- package/dist/session/session-redaction.d.ts +3 -0
- package/dist/session/session-redaction.js +23 -0
- package/dist/session/session.base.d.ts +6 -1
- package/dist/session/session.service.d.ts +8 -2
- package/dist/session/session.service.js +436 -41
- package/dist/session/session.types.d.ts +55 -0
- package/dist/session/session.types.js +69 -2
- package/dist/shared/path/path.js +3 -1
- package/dist/shared/process/host-process-recovery.d.ts +12 -0
- package/dist/shared/process/host-process-recovery.js +78 -0
- package/dist/shared/process/host-process-supervisor.d.ts +57 -0
- package/dist/shared/process/host-process-supervisor.js +282 -0
- package/dist/shared/process/process.d.ts +1 -0
- package/dist/shared/process/process.js +1 -0
- package/dist/shared/redaction/redaction.js +6 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.d.ts +3 -2
- package/dist/worktree/artifact-injector.js +43 -4
- package/dist/worktree/dependency-warmer.d.ts +10 -1
- package/dist/worktree/dependency-warmer.js +94 -5
- package/dist/worktree/index.d.ts +1 -0
- package/dist/worktree/index.js +1 -0
- package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
- package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
- package/dist/worktree/worktree-paths.d.ts +4 -0
- package/dist/worktree/worktree-paths.js +30 -0
- package/dist/worktree/worktree.base.d.ts +3 -0
- package/dist/worktree/worktree.service.d.ts +7 -2
- package/dist/worktree/worktree.service.js +72 -44
- package/dist/worktree/worktree.types.d.ts +3 -1
- package/package.json +3 -2
- package/runtime/windows-job-supervisor.ps1 +356 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { resetGeneratedOutputs } from "../runtime/generated-output-reset.js";
|
|
5
|
+
import { StreamingProviderProcessRunner } from "./provider-cli/provider-cli.process.js";
|
|
6
|
+
const managedCommandNames = new Set(["forge.run_managed_command", "run_managed_command"]);
|
|
7
|
+
const startManagedCommandNames = new Set(["forge.start_managed_command", "start_managed_command"]);
|
|
8
|
+
const getManagedCommandNames = new Set(["forge.get_managed_command", "get_managed_command"]);
|
|
9
|
+
const resetGeneratedOutputNames = new Set(["forge.reset_generated_outputs", "reset_generated_outputs"]);
|
|
10
|
+
const maximumCapturedCharacters = 1_000_000;
|
|
11
|
+
const maximumSynchronousWaitMs = 240_000;
|
|
12
|
+
const managedCommandLaunchTimeoutMs = 60_000;
|
|
13
|
+
const terminalManagedCommandStatuses = new Set([
|
|
14
|
+
"pass",
|
|
15
|
+
"fail",
|
|
16
|
+
"timed_out",
|
|
17
|
+
"timeout_termination_failed",
|
|
18
|
+
"runner_error",
|
|
19
|
+
]);
|
|
20
|
+
const managedCommandTasks = new Map();
|
|
21
|
+
export const managedAttemptToolNames = [
|
|
22
|
+
"forge.run_managed_command",
|
|
23
|
+
"run_managed_command",
|
|
24
|
+
"forge.start_managed_command",
|
|
25
|
+
"start_managed_command",
|
|
26
|
+
"forge.get_managed_command",
|
|
27
|
+
"get_managed_command",
|
|
28
|
+
"forge.reset_generated_outputs",
|
|
29
|
+
"reset_generated_outputs",
|
|
30
|
+
];
|
|
31
|
+
export function isManagedAttemptTool(name) {
|
|
32
|
+
return managedCommandNames.has(name)
|
|
33
|
+
|| startManagedCommandNames.has(name)
|
|
34
|
+
|| getManagedCommandNames.has(name)
|
|
35
|
+
|| resetGeneratedOutputNames.has(name);
|
|
36
|
+
}
|
|
37
|
+
export function managedAttemptToolDefinition(name) {
|
|
38
|
+
if (managedCommandNames.has(name) || startManagedCommandNames.has(name)) {
|
|
39
|
+
return {
|
|
40
|
+
name,
|
|
41
|
+
description: startManagedCommandNames.has(name)
|
|
42
|
+
? "Start a long command in an attempt-owned process tree and return a durable command ID immediately. Poll forge.get_managed_command until it reaches a terminal status."
|
|
43
|
+
: "Run a short command in an attempt-owned process tree. Commands with a timeout above four minutes automatically start asynchronously; poll forge.get_managed_command for their durable result.",
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: "object",
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
required: ["command", "args"],
|
|
48
|
+
properties: {
|
|
49
|
+
command: { type: "string", minLength: 1 },
|
|
50
|
+
args: { type: "array", items: { type: "string" } },
|
|
51
|
+
cwd: { type: "string", description: "Worktree-relative directory; defaults to the worktree root." },
|
|
52
|
+
timeout_ms: { type: "integer", minimum: 1000, maximum: 7200000, default: 2700000 },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (getManagedCommandNames.has(name)) {
|
|
58
|
+
return {
|
|
59
|
+
name,
|
|
60
|
+
description: "Read the durable status and final receipt for a command started by Forge's managed command runner.",
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: "object",
|
|
63
|
+
additionalProperties: false,
|
|
64
|
+
required: ["command_id"],
|
|
65
|
+
properties: {
|
|
66
|
+
command_id: { type: "string", minLength: 1 },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
name,
|
|
73
|
+
description: "Quarantine known untracked generated test-output directories into this attempt's private output directory.",
|
|
74
|
+
inputSchema: { type: "object", additionalProperties: false, properties: {} },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export async function callManagedAttemptTool(name, input, worktreePath) {
|
|
78
|
+
if (resetGeneratedOutputNames.has(name)) {
|
|
79
|
+
const outputDirectory = requiredAttemptEnvironment("FORGE_TEST_OUTPUT_DIR");
|
|
80
|
+
const result = await resetGeneratedOutputs(worktreePath, outputDirectory);
|
|
81
|
+
return {
|
|
82
|
+
content: [{
|
|
83
|
+
type: "text",
|
|
84
|
+
text: result.quarantinedPaths.length === 0
|
|
85
|
+
? "Forge found no stale generated output in the source worktree."
|
|
86
|
+
: `Forge quarantined ${result.quarantinedPaths.length} generated-output director${result.quarantinedPaths.length === 1 ? "y" : "ies"}: ${result.quarantinedPaths.join(", ")}.`,
|
|
87
|
+
}],
|
|
88
|
+
structuredContent: {
|
|
89
|
+
status: "pass",
|
|
90
|
+
quarantined_paths: result.quarantinedPaths,
|
|
91
|
+
quarantine_directory: result.quarantineDirectory,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (getManagedCommandNames.has(name)) {
|
|
96
|
+
return await readManagedCommand(input);
|
|
97
|
+
}
|
|
98
|
+
if (!managedCommandNames.has(name) && !startManagedCommandNames.has(name)) {
|
|
99
|
+
throw new Error(`Unknown managed attempt tool: ${name}`);
|
|
100
|
+
}
|
|
101
|
+
const record = asRecord(input);
|
|
102
|
+
const command = requiredInputString(record, "command");
|
|
103
|
+
const args = requiredStringArray(record, "args");
|
|
104
|
+
const timeoutMs = optionalTimeout(record.timeout_ms);
|
|
105
|
+
const cwd = await containedWorkingDirectory(worktreePath, record.cwd);
|
|
106
|
+
const logDirectory = requiredAttemptEnvironment("FORGE_LOG_DIR");
|
|
107
|
+
await fs.mkdir(path.join(logDirectory, "managed-commands"), { recursive: true, mode: 0o700 });
|
|
108
|
+
const commandId = randomUUID();
|
|
109
|
+
const metadataPath = path.join(logDirectory, "managed-commands", `${commandId}.json`);
|
|
110
|
+
const receiptPath = managedCommandReceiptPath(logDirectory, commandId);
|
|
111
|
+
const invocation = {
|
|
112
|
+
commandId,
|
|
113
|
+
command,
|
|
114
|
+
args,
|
|
115
|
+
cwd,
|
|
116
|
+
timeoutMs,
|
|
117
|
+
logDirectory,
|
|
118
|
+
metadataPath,
|
|
119
|
+
receiptPath,
|
|
120
|
+
startedAt: Date.now(),
|
|
121
|
+
};
|
|
122
|
+
if (startManagedCommandNames.has(name) || timeoutMs > maximumSynchronousWaitMs) {
|
|
123
|
+
const started = managedCommandResult({
|
|
124
|
+
...invocation,
|
|
125
|
+
timedOut: false,
|
|
126
|
+
status: "starting",
|
|
127
|
+
exitCode: null,
|
|
128
|
+
stdout: "",
|
|
129
|
+
stderr: "",
|
|
130
|
+
});
|
|
131
|
+
await persistManagedCommandReceipt(receiptPath, started.structuredContent);
|
|
132
|
+
const task = executeManagedCommand(invocation);
|
|
133
|
+
managedCommandTasks.set(commandId, task);
|
|
134
|
+
void task.finally(() => managedCommandTasks.delete(commandId)).catch(() => undefined);
|
|
135
|
+
return {
|
|
136
|
+
...started,
|
|
137
|
+
content: [{
|
|
138
|
+
type: "text",
|
|
139
|
+
text: `Forge managed command ${commandId} started asynchronously. Poll forge.get_managed_command with this command_id until status is terminal.`,
|
|
140
|
+
}],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return await executeManagedCommand(invocation);
|
|
144
|
+
}
|
|
145
|
+
async function executeManagedCommand(invocation) {
|
|
146
|
+
const environment = inheritedEnvironment({
|
|
147
|
+
FORGE_RUNTIME_MODE: "host",
|
|
148
|
+
FORGE_RESOURCE_LEASE_ID: requiredAttemptEnvironment("FORGE_RESOURCE_LEASE_ID"),
|
|
149
|
+
FORGE_DISPATCH_ATTEMPT_ID: requiredAttemptEnvironment("FORGE_DISPATCH_ATTEMPT_ID"),
|
|
150
|
+
// This nested command owns a distinct supervisor identity. A command-local
|
|
151
|
+
// fence avoids exposing the parent attempt's fencing capability to Codex's
|
|
152
|
+
// MCP subprocess configuration while preserving exact-tree cleanup proof.
|
|
153
|
+
FORGE_FENCING_TOKEN: randomUUID(),
|
|
154
|
+
FORGE_LOG_DIR: invocation.logDirectory,
|
|
155
|
+
FORGE_TEST_OUTPUT_DIR: requiredAttemptEnvironment("FORGE_TEST_OUTPUT_DIR"),
|
|
156
|
+
FORGE_PROCESS_SUPERVISOR_ID: `managed-${invocation.commandId.replaceAll("-", "")}`,
|
|
157
|
+
FORGE_PROCESS_SUPERVISOR_METADATA_PATH: invocation.metadataPath,
|
|
158
|
+
});
|
|
159
|
+
let stdout = "";
|
|
160
|
+
let stderr = "";
|
|
161
|
+
const controller = new AbortController();
|
|
162
|
+
let timedOut = false;
|
|
163
|
+
let timer = null;
|
|
164
|
+
const launchTimer = setTimeout(() => {
|
|
165
|
+
timedOut = true;
|
|
166
|
+
controller.abort(new Error(`managed_command_launch_timeout: process supervisor was not ready within ${managedCommandLaunchTimeoutMs}ms.`));
|
|
167
|
+
}, managedCommandLaunchTimeoutMs);
|
|
168
|
+
try {
|
|
169
|
+
const result = await new StreamingProviderProcessRunner().run({
|
|
170
|
+
command: invocation.command,
|
|
171
|
+
args: invocation.args,
|
|
172
|
+
cwd: invocation.cwd,
|
|
173
|
+
env: environment,
|
|
174
|
+
captureOutput: false,
|
|
175
|
+
}, {
|
|
176
|
+
onStarted: async () => {
|
|
177
|
+
clearTimeout(launchTimer);
|
|
178
|
+
const running = managedCommandResult({
|
|
179
|
+
...invocation,
|
|
180
|
+
timedOut: false,
|
|
181
|
+
status: "running",
|
|
182
|
+
exitCode: null,
|
|
183
|
+
stdout,
|
|
184
|
+
stderr,
|
|
185
|
+
});
|
|
186
|
+
await persistManagedCommandReceipt(invocation.receiptPath, running.structuredContent);
|
|
187
|
+
timer = setTimeout(() => {
|
|
188
|
+
timedOut = true;
|
|
189
|
+
controller.abort(new Error(`managed_command_timeout: exceeded ${invocation.timeoutMs}ms.`));
|
|
190
|
+
}, invocation.timeoutMs);
|
|
191
|
+
},
|
|
192
|
+
onStdoutLine: (line) => { stdout = appendBounded(stdout, line); },
|
|
193
|
+
onStderrLine: (line) => { stderr = appendBounded(stderr, line); },
|
|
194
|
+
}, controller.signal);
|
|
195
|
+
const status = timedOut ? "timed_out" : result.exitCode === 0 ? "pass" : "fail";
|
|
196
|
+
const receipt = managedCommandResult({ ...invocation, timedOut, status, exitCode: result.exitCode, stdout, stderr });
|
|
197
|
+
await persistManagedCommandReceipt(invocation.receiptPath, receipt.structuredContent);
|
|
198
|
+
return receipt;
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
202
|
+
stderr = appendBounded(stderr, message);
|
|
203
|
+
const receipt = managedCommandResult({
|
|
204
|
+
...invocation,
|
|
205
|
+
timedOut,
|
|
206
|
+
status: timedOut ? "timeout_termination_failed" : "runner_error",
|
|
207
|
+
exitCode: null,
|
|
208
|
+
stdout,
|
|
209
|
+
stderr,
|
|
210
|
+
});
|
|
211
|
+
await persistManagedCommandReceipt(invocation.receiptPath, receipt.structuredContent);
|
|
212
|
+
return receipt;
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
clearTimeout(launchTimer);
|
|
216
|
+
if (timer)
|
|
217
|
+
clearTimeout(timer);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function readManagedCommand(input) {
|
|
221
|
+
const commandId = requiredInputString(asRecord(input), "command_id");
|
|
222
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(commandId)) {
|
|
223
|
+
throw new Error("managed_command_invalid: command_id must be a UUID returned by Forge.");
|
|
224
|
+
}
|
|
225
|
+
const receiptPath = managedCommandReceiptPath(requiredAttemptEnvironment("FORGE_LOG_DIR"), commandId);
|
|
226
|
+
let receipt;
|
|
227
|
+
try {
|
|
228
|
+
receipt = JSON.parse(await fs.readFile(receiptPath, "utf8"));
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
if (error.code === "ENOENT") {
|
|
232
|
+
throw new Error(`managed_command_not_found: no durable receipt exists for ${commandId}.`, { cause: error });
|
|
233
|
+
}
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
const status = typeof receipt.status === "string" ? receipt.status : "unknown";
|
|
237
|
+
const stdout = typeof receipt.stdout === "string" ? receipt.stdout : "";
|
|
238
|
+
const stderr = typeof receipt.stderr === "string" ? receipt.stderr : "";
|
|
239
|
+
return {
|
|
240
|
+
content: [{
|
|
241
|
+
type: "text",
|
|
242
|
+
text: terminalManagedCommandStatuses.has(status)
|
|
243
|
+
? `Forge managed command ${commandId} is ${status} (exit ${receipt.exit_code ?? "unavailable"}).\nstdout:\n${stdout || "(empty)"}\nstderr:\n${stderr || "(empty)"}`
|
|
244
|
+
: `Forge managed command ${commandId} is ${status}. Poll this tool again after the command advances.`,
|
|
245
|
+
}],
|
|
246
|
+
structuredContent: receipt,
|
|
247
|
+
isError: terminalManagedCommandStatuses.has(status) && status !== "pass",
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function managedCommandReceiptPath(logDirectory, commandId) {
|
|
251
|
+
return path.join(logDirectory, "managed-commands", `${commandId}.receipt.json`);
|
|
252
|
+
}
|
|
253
|
+
async function persistManagedCommandReceipt(receiptPath, receipt) {
|
|
254
|
+
const temporaryPath = `${receiptPath}.${randomUUID()}.tmp`;
|
|
255
|
+
await fs.writeFile(temporaryPath, `${JSON.stringify(receipt, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
256
|
+
await fs.rename(temporaryPath, receiptPath);
|
|
257
|
+
}
|
|
258
|
+
function managedCommandResult(values) {
|
|
259
|
+
const elapsedMs = Date.now() - values.startedAt;
|
|
260
|
+
return {
|
|
261
|
+
content: [{
|
|
262
|
+
type: "text",
|
|
263
|
+
text: `Forge managed command ${values.commandId} ${values.status} after ${elapsedMs}ms (exit ${values.exitCode ?? "unavailable"}).\nstdout:\n${values.stdout || "(empty)"}\nstderr:\n${values.stderr || "(empty)"}`,
|
|
264
|
+
}],
|
|
265
|
+
structuredContent: {
|
|
266
|
+
command_id: values.commandId,
|
|
267
|
+
status: values.status,
|
|
268
|
+
command: [values.command, ...values.args],
|
|
269
|
+
cwd: values.cwd,
|
|
270
|
+
timeout_ms: values.timeoutMs,
|
|
271
|
+
elapsed_ms: elapsedMs,
|
|
272
|
+
timed_out: values.timedOut,
|
|
273
|
+
exit_code: values.exitCode,
|
|
274
|
+
stdout: values.stdout,
|
|
275
|
+
stderr: values.stderr,
|
|
276
|
+
supervisor_metadata_path: values.metadataPath,
|
|
277
|
+
receipt_path: values.receiptPath,
|
|
278
|
+
process_tree_terminated: values.status === "timed_out" ? true : values.timedOut ? false : undefined,
|
|
279
|
+
},
|
|
280
|
+
isError: terminalManagedCommandStatuses.has(values.status) && values.status !== "pass",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async function containedWorkingDirectory(worktreePath, input) {
|
|
284
|
+
const root = await fs.realpath(path.resolve(worktreePath));
|
|
285
|
+
const relative = input === undefined ? "." : requiredStandaloneString(input, "cwd");
|
|
286
|
+
if (path.isAbsolute(relative))
|
|
287
|
+
throw new Error("managed_command_invalid: cwd must be worktree-relative.");
|
|
288
|
+
const target = await fs.realpath(path.resolve(root, relative));
|
|
289
|
+
const containment = path.relative(root, target);
|
|
290
|
+
if (containment.startsWith(`..${path.sep}`) || path.isAbsolute(containment)) {
|
|
291
|
+
throw new Error("managed_command_invalid: cwd resolves outside the source worktree.");
|
|
292
|
+
}
|
|
293
|
+
return target;
|
|
294
|
+
}
|
|
295
|
+
function inheritedEnvironment(overrides) {
|
|
296
|
+
const env = {};
|
|
297
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
298
|
+
if (typeof value === "string" && !looksSensitiveEnvironmentName(key))
|
|
299
|
+
env[key] = value;
|
|
300
|
+
}
|
|
301
|
+
return { ...env, ...overrides };
|
|
302
|
+
}
|
|
303
|
+
function looksSensitiveEnvironmentName(name) {
|
|
304
|
+
const normalized = name.trim().toUpperCase();
|
|
305
|
+
return normalized.includes("SECRET")
|
|
306
|
+
|| normalized.includes("TOKEN")
|
|
307
|
+
|| normalized.includes("PASSWORD")
|
|
308
|
+
|| normalized.includes("CREDENTIAL")
|
|
309
|
+
|| normalized.endsWith("_API_KEY")
|
|
310
|
+
|| normalized === "OPENAI_API_KEY";
|
|
311
|
+
}
|
|
312
|
+
function requiredAttemptEnvironment(name) {
|
|
313
|
+
const value = process.env[name]?.trim();
|
|
314
|
+
if (!value)
|
|
315
|
+
throw new Error(`managed_attempt_tool_unavailable: ${name} is not configured.`);
|
|
316
|
+
return value;
|
|
317
|
+
}
|
|
318
|
+
function asRecord(value) {
|
|
319
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
320
|
+
throw new Error("managed_command_invalid: input must be an object.");
|
|
321
|
+
return value;
|
|
322
|
+
}
|
|
323
|
+
function requiredInputString(record, name) {
|
|
324
|
+
return requiredStandaloneString(record[name], name);
|
|
325
|
+
}
|
|
326
|
+
function requiredStandaloneString(value, name) {
|
|
327
|
+
if (typeof value !== "string" || !value.trim())
|
|
328
|
+
throw new Error(`managed_command_invalid: ${name} is required.`);
|
|
329
|
+
return value.trim();
|
|
330
|
+
}
|
|
331
|
+
function requiredStringArray(record, name) {
|
|
332
|
+
const value = record[name];
|
|
333
|
+
if (!Array.isArray(value) || value.some(entry => typeof entry !== "string")) {
|
|
334
|
+
throw new Error(`managed_command_invalid: ${name} must be a string array.`);
|
|
335
|
+
}
|
|
336
|
+
return value;
|
|
337
|
+
}
|
|
338
|
+
function optionalTimeout(value) {
|
|
339
|
+
if (value === undefined)
|
|
340
|
+
return 2_700_000;
|
|
341
|
+
if (!Number.isSafeInteger(value) || value < 1_000 || value > 7_200_000) {
|
|
342
|
+
throw new Error("managed_command_invalid: timeout_ms must be an integer from 1000 through 7200000.");
|
|
343
|
+
}
|
|
344
|
+
return value;
|
|
345
|
+
}
|
|
346
|
+
function appendBounded(current, line) {
|
|
347
|
+
if (current.length >= maximumCapturedCharacters)
|
|
348
|
+
return current;
|
|
349
|
+
return `${current}${line}\n`.slice(0, maximumCapturedCharacters);
|
|
350
|
+
}
|
|
@@ -237,12 +237,19 @@ export async function writeQaGateCompletionArtifact(config, gate, input) {
|
|
|
237
237
|
const status = normalizeQaGateStatus(requiredString(payload, "status"));
|
|
238
238
|
validateQaGatePayloadShape(gate, payload, status);
|
|
239
239
|
validateOptionalWorkItemId(payload, config.workItemId);
|
|
240
|
-
const
|
|
240
|
+
const ownerFields = artifactOwnerFields(config);
|
|
241
|
+
const validatedSidecars = gate === "qa_manual_web"
|
|
242
|
+
? await validateQaSidecarReferences(config.worktreePath, config.workItemId, qaArray(payload, "sidecars", qaEvidenceSource(payload)), ownerFields.owner_type, ownerFields.owner_id)
|
|
243
|
+
: null;
|
|
244
|
+
const redactedPayload = redactSecrets({
|
|
245
|
+
...payload,
|
|
246
|
+
status,
|
|
247
|
+
...(validatedSidecars ? { sidecars: validatedSidecars } : {})
|
|
248
|
+
}, config.redactionSecrets ?? []);
|
|
241
249
|
const producedBy = optionalString(redactedPayload, "produced_by")
|
|
242
250
|
?? optionalString(redactedPayload, "producedBy")
|
|
243
251
|
?? config.producedBy
|
|
244
252
|
?? "forge-completion-tool";
|
|
245
|
-
const ownerFields = artifactOwnerFields(config);
|
|
246
253
|
const artifactRoot = ownerFields.owner_type && ownerFields.owner_id
|
|
247
254
|
? artifactRootForOwner(config.worktreePath, ownerFields.owner_type, ownerFields.owner_id)
|
|
248
255
|
: artifactRootFor(config.worktreePath, config.workItemId);
|
|
@@ -9,7 +9,8 @@ export type DebugCompletionPhase = "debug_repro" | "debug_root_cause" | "debug_f
|
|
|
9
9
|
export type ArchitectCompletionPhase = "architect";
|
|
10
10
|
export type DraftCompletionPhase = "draft";
|
|
11
11
|
export type DecomposeCompletionPhase = "decompose";
|
|
12
|
-
export type
|
|
12
|
+
export type CiRepairCompletionPhase = "ci_repair";
|
|
13
|
+
export type CompletionPhase = DevelopCompletionPhase | "qa" | DebugCompletionPhase | ArchitectCompletionPhase | DraftCompletionPhase | DecomposeCompletionPhase | CiRepairCompletionPhase;
|
|
13
14
|
export type QaCompletionGate = "qa_plan" | "qa_regression" | "qa_api" | "qa_manual_web" | "qa_bug_repro" | "qa_findings" | "qa_verdict";
|
|
14
15
|
export type DraftCompletionGate = "draft_intent" | "draft_repo_context" | "draft_graph_context" | "draft_product_scope" | "draft_work_order_plan" | "draft_critic" | "draft_proposal";
|
|
15
16
|
export type DecomposeCompletionGate = "decompose_subject_intake" | "decompose_context_review" | "decompose_breakdown_strategy" | "decompose_child_items" | "decompose_dependency_graph" | "decompose_critic" | "decompose_proposal";
|
|
@@ -25,6 +26,7 @@ export interface CompletionToolRuntimeOptions {
|
|
|
25
26
|
readonly dispatch: RunOnceDispatchPacket;
|
|
26
27
|
readonly worktree: WorktreeHandle;
|
|
27
28
|
readonly phase?: CompletionPhase;
|
|
29
|
+
readonly runtimeEnvironment?: Readonly<Record<string, string>>;
|
|
28
30
|
}
|
|
29
31
|
export interface CompletionToolServerConfig {
|
|
30
32
|
readonly worktreePath: string;
|
|
@@ -56,7 +58,7 @@ export interface CompletionToolCallResult {
|
|
|
56
58
|
readonly allowed: boolean;
|
|
57
59
|
readonly reason: string;
|
|
58
60
|
};
|
|
59
|
-
readonly nextPhase: CompletionPhase | "dev_test_ready" | "qa_ready" | "sign_off_ready";
|
|
61
|
+
readonly nextPhase: CompletionPhase | "dev_test_ready" | "qa_ready" | "sign_off_ready" | "provider_checks_ready";
|
|
60
62
|
readonly nextInstruction: string;
|
|
61
63
|
};
|
|
62
64
|
}
|
|
@@ -85,6 +85,9 @@ export function parseStrictCompletionPhase(value) {
|
|
|
85
85
|
if (normalized === "decompose" || normalized === "decomposition" || normalized === "work_order_decomposition" || normalized === "feature_decomposition") {
|
|
86
86
|
return "decompose";
|
|
87
87
|
}
|
|
88
|
+
if (normalized === "ci_repair" || normalized === "pr_checks" || normalized === "repair_pr_checks") {
|
|
89
|
+
return "ci_repair";
|
|
90
|
+
}
|
|
88
91
|
return null;
|
|
89
92
|
}
|
|
90
93
|
export function normalizeDebugCompletionPhase(value) {
|
|
@@ -156,6 +159,9 @@ export function phaseFromToolName(toolName) {
|
|
|
156
159
|
if (normalized === "forge.complete_architect_plan" || normalized === "complete_architect_plan") {
|
|
157
160
|
return "architect";
|
|
158
161
|
}
|
|
162
|
+
if (normalized === "forge.complete_ci_repair" || normalized === "complete_ci_repair") {
|
|
163
|
+
return "ci_repair";
|
|
164
|
+
}
|
|
159
165
|
if (draftGateFromToolName(toolName)) {
|
|
160
166
|
return "draft";
|
|
161
167
|
}
|
|
@@ -600,9 +606,22 @@ export async function readOptionalFile(filePath) {
|
|
|
600
606
|
}
|
|
601
607
|
export async function writeFileAtomic(filePath, content) {
|
|
602
608
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
603
|
-
const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${
|
|
609
|
+
const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
604
610
|
await fs.writeFile(tempPath, content, "utf8");
|
|
605
|
-
|
|
611
|
+
try {
|
|
612
|
+
await fs.rename(tempPath, filePath);
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
if (process.platform !== "win32" || !isNodeError(error) || (error.code !== "EPERM" && error.code !== "EEXIST")) {
|
|
616
|
+
await fs.rm(tempPath, { force: true });
|
|
617
|
+
throw error;
|
|
618
|
+
}
|
|
619
|
+
// Windows rename does not reliably replace an existing destination. QA-cycle gates
|
|
620
|
+
// intentionally refresh canonical bundle files, so remove only the exact destination
|
|
621
|
+
// after the replacement temp file is durable, then complete the rename.
|
|
622
|
+
await fs.rm(filePath, { force: true });
|
|
623
|
+
await fs.rename(tempPath, filePath);
|
|
624
|
+
}
|
|
606
625
|
}
|
|
607
626
|
export function stableStringify(value) {
|
|
608
627
|
return JSON.stringify(sortJson(value));
|
|
@@ -626,7 +645,12 @@ export function forgeBridgeCliBinPath() {
|
|
|
626
645
|
return fileURLToPath(new URL("../cli/bin.js", import.meta.url));
|
|
627
646
|
}
|
|
628
647
|
export function tomlString(value) {
|
|
629
|
-
|
|
648
|
+
if (value.includes("'''")) {
|
|
649
|
+
throw new Error("Codex MCP configuration values cannot contain three consecutive apostrophes.");
|
|
650
|
+
}
|
|
651
|
+
// Codex config overrides pass through npm's PowerShell shim on Windows.
|
|
652
|
+
// Literal TOML strings survive that boundary; basic-string double quotes do not.
|
|
653
|
+
return `'''${value}'''`;
|
|
630
654
|
}
|
|
631
655
|
export function tomlStringArray(values) {
|
|
632
656
|
return `[${values.map(tomlString).join(", ")}]`;
|
|
@@ -195,6 +195,10 @@ export const phaseFiles = {
|
|
|
195
195
|
decompose: {
|
|
196
196
|
jsonFileName: "decompose.proposal.json",
|
|
197
197
|
reportFileName: "decompose.report.md"
|
|
198
|
+
},
|
|
199
|
+
ci_repair: {
|
|
200
|
+
jsonFileName: "ci-repair.handoff.json",
|
|
201
|
+
reportFileName: "ci-repair.report.md"
|
|
198
202
|
}
|
|
199
203
|
};
|
|
200
204
|
export function completionPhaseFromDispatch(dispatch) {
|
|
@@ -224,6 +228,9 @@ export function normalizeCompletionPhase(value) {
|
|
|
224
228
|
if (normalized === "decompose" || normalized === "decomposition" || normalized === "work_order_decomposition" || normalized === "feature_decomposition") {
|
|
225
229
|
return "decompose";
|
|
226
230
|
}
|
|
231
|
+
if (normalized === "ci_repair" || normalized === "pr_checks" || normalized === "repair_pr_checks") {
|
|
232
|
+
return "ci_repair";
|
|
233
|
+
}
|
|
227
234
|
return "implementation";
|
|
228
235
|
}
|
|
229
236
|
export function usesDevelopCycleCompletion(dispatch) {
|
|
@@ -275,9 +282,11 @@ export function completionToolNamesForPhase(phase) {
|
|
|
275
282
|
? "complete_self_review"
|
|
276
283
|
: phase === "validation"
|
|
277
284
|
? "complete_validation"
|
|
278
|
-
: phase === "
|
|
279
|
-
? "
|
|
280
|
-
: "
|
|
285
|
+
: phase === "ci_repair"
|
|
286
|
+
? "complete_ci_repair"
|
|
287
|
+
: phase === "architect"
|
|
288
|
+
? "complete_architect_plan"
|
|
289
|
+
: "complete_qa";
|
|
281
290
|
return [`forge.${base}`, base];
|
|
282
291
|
}
|
|
283
292
|
export function completionToolNamesForQaGate(gate) {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { type CompletionPhase, type DebugCompletionPhase, type DecomposeCompletionGate, type DevelopCompletionPhase, type DraftCompletionGate, type QaCompletionGate } from "./phase-completion.js";
|
|
2
|
-
export declare function completionPromptForPhase(phase: CompletionPhase): string;
|
|
2
|
+
export declare function completionPromptForPhase(phase: CompletionPhase, artifactRoot?: string): string;
|
|
3
3
|
export declare function completionPromptForDevelopCycle(phase: CompletionPhase): string;
|
|
4
|
-
export declare function completionPromptForQaCycle(currentGate?: QaCompletionGate): string;
|
|
4
|
+
export declare function completionPromptForQaCycle(currentGate?: QaCompletionGate, artifactRoot?: string): string;
|
|
5
5
|
export declare function completionPromptForDraftCycle(currentGate?: DraftCompletionGate): string;
|
|
6
6
|
export declare function completionPromptForDecomposeCycle(currentGate?: DecomposeCompletionGate): string;
|
|
7
7
|
export declare function completionPromptForDebugCycle(currentGate?: DebugCompletionPhase): string;
|
|
8
8
|
export declare function nextPhaseAfter(phase: CompletionPhase): DevelopCompletionPhase | null;
|
|
9
9
|
export declare function nextDebugPhaseAfter(phase: DebugCompletionPhase): DebugCompletionPhase | null;
|
|
10
|
-
export declare function completionToolNextInstruction(completedPhase: CompletionPhase, status: string, nextPhase: CompletionPhase | "dev_test_ready" | "qa_ready" | "sign_off_ready"): string;
|
|
10
|
+
export declare function completionToolNextInstruction(completedPhase: CompletionPhase, status: string, nextPhase: CompletionPhase | "dev_test_ready" | "qa_ready" | "sign_off_ready" | "provider_checks_ready"): string;
|
|
11
11
|
export declare function completionToolNextInstructionForDraftGate(completedGate: DraftCompletionGate, status: string, nextGate: DraftCompletionGate | null): string;
|
|
12
12
|
export declare function completionToolNextInstructionForQaGate(completedGate: QaCompletionGate, status: string, nextGate: QaCompletionGate | null): string;
|
|
13
13
|
export declare function completionToolNextInstructionForDecomposeGate(completedGate: DecomposeCompletionGate, status: string, nextGate: DecomposeCompletionGate | null): string;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { completionPhaseOrder, completionToolNamesForPhase, debugCompletionPhaseOrder, expectedCompletionToolName, expectedDebugCompletionToolName, expectedDecomposeCompletionToolName, phaseFiles, toolServerName } from "./phase-completion.js";
|
|
2
2
|
import { isDebugCompletionPhase, isDevelopCompletionPhase } from "./phase-completion.validation.js";
|
|
3
|
-
export function completionPromptForPhase(phase) {
|
|
3
|
+
export function completionPromptForPhase(phase, artifactRoot = ".forge/work-items/{workItemId}/artifacts") {
|
|
4
4
|
const toolName = expectedCompletionToolName(phase);
|
|
5
5
|
const alias = completionToolNamesForPhase(phase).find((name) => name !== toolName);
|
|
6
6
|
const artifactName = phaseFiles[phase].jsonFileName;
|
|
7
7
|
if (phase === "qa") {
|
|
8
8
|
return `Forge completion tools are available through the MCP server named \`${toolServerName}\`.
|
|
9
9
|
Do not write QA artifacts by hand. When QA is complete, call \`${toolName}(...)\`${alias ? ` (or \`${alias}\` if your client shows unqualified MCP tool names)` : ""} with the required structured QA payload.
|
|
10
|
-
That tool writes the durable QA bundle under
|
|
10
|
+
That tool writes the durable QA bundle under \`${artifactRoot}/\`: \`qa.plan.json\`, \`qa.evidence.json\`, \`qa.findings.json\`, \`${artifactName}\`, and \`qa.report.md\`.
|
|
11
11
|
Use \`status: "pass"\` only when the validated changes satisfy acceptance criteria with concrete evidence. Use \`status: "needs_rework"\` with actionable in-scope findings, or \`status: "blocked"\` with a concrete blocker_reason.
|
|
12
12
|
Include \`policy_warnings\` for non-blocking Forge policy or scope mismatches that should remain visible after QA.
|
|
13
13
|
Forge will mirror only the tool-written QA artifacts; reporting QA completion in prose is not enough.`;
|
|
@@ -22,6 +22,14 @@ The payload must put \`phases\`, \`tasks\`, \`dependencies\`, and \`phase_gates\
|
|
|
22
22
|
Dependency entries use canonical \`before\` and \`after\` task ids, for example \`{ "before": "task-1", "after": "task-2" }\`. Forge accepts \`from\`/\`to\` only as aliases and normalizes them before writing artifacts.
|
|
23
23
|
Use \`status: "pass"\` for a valid pending-approval plan, \`needs_revision\` when story/design input must change, or \`blocked\` with a concrete blocker.
|
|
24
24
|
Forge will mirror only the tool-written Architect artifacts; reporting the plan in prose is not enough.`;
|
|
25
|
+
}
|
|
26
|
+
if (phase === "ci_repair") {
|
|
27
|
+
return `Forge completion tools are available through the MCP server named \`${toolServerName}\`.
|
|
28
|
+
Read the generated \`.forge/work-items/{workItemId}/ci-repair.readiness.json\` before running validation.
|
|
29
|
+
Do not write the CI-repair handoff by hand. After the repair and locally equivalent validation are complete, call \`${toolName}(...)\`${alias ? ` (or \`${alias}\` if your client shows unqualified MCP tool names)` : ""}.
|
|
30
|
+
That tool validates failed-check inspection, root cause, scoped changes, narrow proof, every failed-job guard/test/drift step, provider-only classifications, broader validation, remaining risk, provider state, and push readiness.
|
|
31
|
+
It writes \`.forge/work-items/{workItemId}/artifacts/${artifactName}\`. Forge Bridge will not commit, push, or report CI repair success until this gate passes.
|
|
32
|
+
For a step classified \`provider_only\`, include the exact reason and provider check that will supply the missing proof. Do not weaken or delete the provider check.`;
|
|
25
33
|
}
|
|
26
34
|
return `Forge completion tools are available through the MCP server named \`${toolServerName}\`.
|
|
27
35
|
Do not write phase artifacts by hand. When the phase is complete, call \`${toolName}(...)\`${alias ? ` (or \`${alias}\` if your client shows unqualified MCP tool names)` : ""} with the required evidence payload.
|
|
@@ -38,7 +46,7 @@ Complete the dev cycle in order inside this same provider session:
|
|
|
38
46
|
Each tool writes the canonical artifact under \`.forge/work-items/{workItemId}/artifacts/\` and returns the next-phase instruction Forge expects you to follow.
|
|
39
47
|
If the current phase cannot pass, call the current phase tool with a non-pass status and explicit blockers instead of reporting completion in prose only.`;
|
|
40
48
|
}
|
|
41
|
-
export function completionPromptForQaCycle(currentGate = "qa_plan") {
|
|
49
|
+
export function completionPromptForQaCycle(currentGate = "qa_plan", artifactRoot = ".forge/work-items/{workItemId}/artifacts") {
|
|
42
50
|
return `Forge completion tools are available through the MCP server named \`${toolServerName}\`.
|
|
43
51
|
Do not write QA artifacts by hand. The tool server enforces QA gate order and starts this run at \`${currentGate}\`.
|
|
44
52
|
Complete the QA cycle in order inside this same provider session:
|
|
@@ -50,7 +58,8 @@ Complete the QA cycle in order inside this same provider session:
|
|
|
50
58
|
6. Follow the returned findings instruction, then call \`forge.complete_qa_findings(...)\`.
|
|
51
59
|
7. Follow the returned verdict instruction, then call \`forge.complete_qa_verdict(...)\`.
|
|
52
60
|
Use \`status: "skipped"\` with a reason for gates that are not applicable. If a gate is blocked, keep moving through later gates as skipped due to the blocker and finish with a blocked verdict.
|
|
53
|
-
|
|
61
|
+
On a managed host, start solution-wide \`dotnet test Forge.sln\` evidence through \`forge.start_managed_command\` with at least a 45-minute timeout, poll \`forge.get_managed_command\` until it returns a terminal receipt, and include the returned \`managed_command_id\` in its command evidence. The regression gate rejects generic-shell timeouts and non-terminal receipts for that command.
|
|
62
|
+
The final verdict tool writes the canonical QA bundle under \`${artifactRoot}/\`: \`qa.plan.json\`, \`qa.evidence.json\`, \`qa.findings.json\`, \`qa.verdict.json\`, and \`qa.report.md\`.
|
|
54
63
|
Include \`policy_warnings\` in the final verdict for non-blocking Forge policy or scope mismatches that should remain visible after QA.
|
|
55
64
|
The legacy \`forge.complete_qa\` one-shot tool is not available in normal QA-cycle mode. Reporting QA completion in prose is not enough.`;
|
|
56
65
|
}
|
|
@@ -115,6 +124,11 @@ export function completionToolNextInstruction(completedPhase, status, nextPhase)
|
|
|
115
124
|
}
|
|
116
125
|
return "QA accepted. Forge has the structured evidence needed to move this work item to Sign-Off.";
|
|
117
126
|
}
|
|
127
|
+
if (completedPhase === "ci_repair") {
|
|
128
|
+
return status === "pass"
|
|
129
|
+
? "CI repair handoff accepted. Forge Bridge may now commit, push, and return the PR to provider checks."
|
|
130
|
+
: "Stay in CI repair, address the exact missing evidence returned by this gate, then call forge.complete_ci_repair again.";
|
|
131
|
+
}
|
|
118
132
|
if (status !== "pass") {
|
|
119
133
|
return `Stay in ${completedPhase}, address the recorded blockers, then call ${expectedCompletionToolName(completedPhase)} again.`;
|
|
120
134
|
}
|
|
@@ -203,7 +217,7 @@ export function completionToolNextInstructionForDebugGate(completedGate, status,
|
|
|
203
217
|
function qaGateInstruction(gate) {
|
|
204
218
|
switch (gate) {
|
|
205
219
|
case "qa_regression":
|
|
206
|
-
return "Run or explicitly skip applicable regression/build/lint/typecheck/e2e checks, then call forge.complete_qa_regression.";
|
|
220
|
+
return "Run or explicitly skip applicable regression/build/lint/typecheck/e2e checks, then call forge.complete_qa_regression. On a managed host, start solution-wide dotnet test Forge.sln with forge.start_managed_command using at least a 45-minute timeout, poll forge.get_managed_command to a terminal receipt, and include its managed_command_id in the command evidence. A reproduced failure outside the current diff and acceptance criteria may remain non-blocking only when the current item has independent passing evidence: preserve it as an out_of_scope_failure with candidate_bug_id, scope_evidence, acceptance_criteria_independent: true, and a failing focused repro command/exit code. Never classify an unexplained, in-scope, or infrastructure failure this way.";
|
|
207
221
|
case "qa_api":
|
|
208
222
|
return "Run or explicitly skip applicable API/backend contract checks, then call forge.complete_qa_api.";
|
|
209
223
|
case "qa_manual_web":
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type CompletionToolCallResult, type CompletionToolRuntime, type CompletionToolRuntimeOptions, type CompletionToolServerConfig } from "./phase-completion.js";
|
|
2
|
+
import { type ManagedAttemptToolResult } from "./managed-attempt-tools.js";
|
|
2
3
|
export declare function prepareCompletionToolRuntime(options: CompletionToolRuntimeOptions): Promise<CompletionToolRuntime>;
|
|
3
4
|
export declare function buildCompletionToolServerArgs(config: CompletionToolServerConfig): readonly string[];
|
|
4
5
|
export declare function codexMcpConfigArgs(runtime: CompletionToolRuntime): readonly string[];
|
|
@@ -10,11 +11,12 @@ export declare class CompletionToolServerSession {
|
|
|
10
11
|
private currentQaGate;
|
|
11
12
|
private currentDraftGate;
|
|
12
13
|
private currentDecomposeGate;
|
|
14
|
+
private readonly managedCommandReceipts;
|
|
13
15
|
private completed;
|
|
14
16
|
constructor(config: CompletionToolServerConfig);
|
|
15
17
|
get instructions(): string;
|
|
16
18
|
toolDefinitions(): readonly unknown[];
|
|
17
|
-
callTool(toolName: string, input: unknown): Promise<CompletionToolCallResult>;
|
|
19
|
+
callTool(toolName: string, input: unknown): Promise<CompletionToolCallResult | ManagedAttemptToolResult>;
|
|
18
20
|
private callDecomposeGateTool;
|
|
19
21
|
private callDraftGateTool;
|
|
20
22
|
private callDebugGateTool;
|
|
@@ -25,6 +27,15 @@ export declare class CompletionToolServerSession {
|
|
|
25
27
|
private advanceAfterQaGate;
|
|
26
28
|
private advanceAfterDebugGate;
|
|
27
29
|
}
|
|
30
|
+
export declare function completionToolRuntimeEnvironment(runtimeEnvironment?: Readonly<Record<string, string>>): Readonly<Record<string, string>>;
|
|
31
|
+
/**
|
|
32
|
+
* A solution-wide Forge test is long enough that a generic provider shell can
|
|
33
|
+
* expire while VSTest is still healthy. On managed host attempts, only the
|
|
34
|
+
* attempt-owned command runner can prove the exact process outcome and clean
|
|
35
|
+
* up its process tree. Keep this check at the completion gate so prompt drift
|
|
36
|
+
* cannot turn an arbitrary shell timeout into a QA blocker.
|
|
37
|
+
*/
|
|
38
|
+
export declare function assertManagedQaRegressionEvidence(input: unknown, receipts: readonly Readonly<Record<string, unknown>>[]): void;
|
|
28
39
|
export declare function runCompletionMcpServer(config: CompletionToolServerConfig): Promise<void>;
|
|
29
40
|
export declare function parseCompletionMcpArgs(argv: readonly string[]): {
|
|
30
41
|
readonly ok: true;
|