@loucompanion/forge-bridge 0.1.3-dev.f2b8822ce953 → 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.
- package/README.md +78 -0
- package/dist/bridge/bridge.base.d.ts +6 -1
- package/dist/bridge/bridge.service.d.ts +60 -3
- package/dist/bridge/bridge.service.js +970 -24
- package/dist/bridge/bridge.types.d.ts +172 -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 +47 -1
- package/dist/cli/cli.service.d.ts +42 -2
- package/dist/cli/cli.service.js +351 -22
- package/dist/cli/local-config.d.ts +6 -0
- package/dist/cli/local-config.js +47 -3
- 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 +350 -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/command-runner.d.ts +4 -0
- package/dist/sandbox/command-runner.js +47 -0
- package/dist/sandbox/contract.d.ts +5 -0
- package/dist/sandbox/contract.js +276 -0
- package/dist/sandbox/event-budget.d.ts +11 -0
- package/dist/sandbox/event-budget.js +27 -0
- package/dist/sandbox/image-policy.d.ts +35 -0
- package/dist/sandbox/image-policy.js +247 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/lease-watchdog.d.ts +18 -0
- package/dist/sandbox/lease-watchdog.js +67 -0
- package/dist/sandbox/preflight.d.ts +54 -0
- package/dist/sandbox/preflight.js +335 -0
- package/dist/sandbox/process.d.ts +20 -0
- package/dist/sandbox/process.js +209 -0
- package/dist/sandbox/provider-auth/codex.d.ts +26 -0
- package/dist/sandbox/provider-auth/codex.js +212 -0
- package/dist/sandbox/repository-lock.d.ts +5 -0
- package/dist/sandbox/repository-lock.js +119 -0
- package/dist/sandbox/runtime.d.ts +297 -0
- package/dist/sandbox/runtime.js +2338 -0
- package/dist/sandbox/sandbox.types.d.ts +86 -0
- package/dist/sandbox/sandbox.types.js +1 -0
- package/dist/sandbox/smoke.d.ts +76 -0
- package/dist/sandbox/smoke.js +380 -0
- package/dist/sandbox/worker-supervisor.d.ts +13 -0
- package/dist/sandbox/worker-supervisor.js +36 -0
- package/dist/sandbox/worker.d.ts +1 -0
- package/dist/sandbox/worker.js +225 -0
- package/dist/sandbox/workspace.service.d.ts +13 -0
- package/dist/sandbox/workspace.service.js +72 -0
- package/dist/session/managed-attempt-tools.d.ts +12 -0
- package/dist/session/managed-attempt-tools.js +561 -0
- package/dist/session/phase-completion.artifacts.js +32 -4
- 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 +20 -6
- package/dist/session/phase-completion.runtime.d.ts +12 -1
- package/dist/session/phase-completion.runtime.js +129 -7
- package/dist/session/phase-completion.schemas.js +132 -5
- 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.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +66 -9
- package/dist/session/provider-cli/provider-cli.process.d.ts +17 -0
- package/dist/session/provider-cli/provider-cli.process.js +223 -14
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +17 -2
- package/dist/session/provider-cli/provider-cli.prompts.js +226 -18
- 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 +19 -3
- package/dist/session/provider-cli/provider-cli.service.js +503 -98
- package/dist/session/provider-cli/provider-cli.types.d.ts +6 -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 +463 -58
- package/dist/session/session.types.d.ts +144 -1
- package/dist/session/session.types.js +279 -3
- 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/process/windows-launch.d.ts +1 -1
- package/dist/shared/process/windows-launch.js +6 -3
- package/dist/shared/provider-capabilities.d.ts +1 -0
- package/dist/shared/provider-capabilities.js +3 -0
- package/dist/shared/redaction/redaction.js +10 -3
- 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 +21 -1
- package/dist/worktree/dependency-warmer.js +138 -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 +6 -3
- package/runtime/windows-job-supervisor.ps1 +356 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -0,0 +1,561 @@
|
|
|
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 waitManagedCommandNames = new Set(["forge.wait_managed_command", "wait_managed_command"]);
|
|
10
|
+
const resetGeneratedOutputNames = new Set(["forge.reset_generated_outputs", "reset_generated_outputs"]);
|
|
11
|
+
const maximumCapturedCharacters = 200_000;
|
|
12
|
+
const maximumReturnedOutputCharacters = 12_000;
|
|
13
|
+
const maximumReportedFailedTests = 50;
|
|
14
|
+
const maximumSynchronousWaitMs = 240_000;
|
|
15
|
+
const managedCommandLaunchTimeoutMs = 60_000;
|
|
16
|
+
const managedCommandProgressPersistIntervalMs = 5_000;
|
|
17
|
+
const managedCommandReceiptPollIntervalMs = 1_000;
|
|
18
|
+
const managedCommandReceiptReplaceAttempts = 40;
|
|
19
|
+
const managedCommandReceiptReplaceRetryMs = 25;
|
|
20
|
+
const terminalManagedCommandStatuses = new Set([
|
|
21
|
+
"pass",
|
|
22
|
+
"fail",
|
|
23
|
+
"timed_out",
|
|
24
|
+
"timeout_termination_failed",
|
|
25
|
+
"runner_error",
|
|
26
|
+
]);
|
|
27
|
+
const managedCommandTasks = new Map();
|
|
28
|
+
export const managedAttemptToolNames = [
|
|
29
|
+
"forge.run_managed_command",
|
|
30
|
+
"run_managed_command",
|
|
31
|
+
"forge.start_managed_command",
|
|
32
|
+
"start_managed_command",
|
|
33
|
+
"forge.get_managed_command",
|
|
34
|
+
"get_managed_command",
|
|
35
|
+
"forge.wait_managed_command",
|
|
36
|
+
"wait_managed_command",
|
|
37
|
+
"forge.reset_generated_outputs",
|
|
38
|
+
"reset_generated_outputs",
|
|
39
|
+
];
|
|
40
|
+
export function isManagedAttemptTool(name) {
|
|
41
|
+
return managedCommandNames.has(name)
|
|
42
|
+
|| startManagedCommandNames.has(name)
|
|
43
|
+
|| getManagedCommandNames.has(name)
|
|
44
|
+
|| waitManagedCommandNames.has(name)
|
|
45
|
+
|| resetGeneratedOutputNames.has(name);
|
|
46
|
+
}
|
|
47
|
+
export function managedAttemptToolDefinition(name) {
|
|
48
|
+
if (managedCommandNames.has(name) || startManagedCommandNames.has(name)) {
|
|
49
|
+
return {
|
|
50
|
+
name,
|
|
51
|
+
description: startManagedCommandNames.has(name)
|
|
52
|
+
? "Start a long command in an attempt-owned process tree and return a durable command ID immediately. Continue independent work, then call forge.wait_managed_command once for its terminal receipt."
|
|
53
|
+
: "Run a short command in an attempt-owned process tree. Commands with a timeout above four minutes automatically start asynchronously; continue independent work, then call forge.wait_managed_command once for the durable result.",
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: "object",
|
|
56
|
+
additionalProperties: false,
|
|
57
|
+
required: ["command", "args"],
|
|
58
|
+
properties: {
|
|
59
|
+
command: { type: "string", minLength: 1 },
|
|
60
|
+
args: { type: "array", items: { type: "string" } },
|
|
61
|
+
cwd: { type: "string", description: "Worktree-relative directory; defaults to the worktree root." },
|
|
62
|
+
timeout_ms: { type: "integer", minimum: 1000, maximum: 7200000, default: 2700000 },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (getManagedCommandNames.has(name)) {
|
|
68
|
+
return {
|
|
69
|
+
name,
|
|
70
|
+
description: "Read the durable status and final receipt for a command started by Forge's managed command runner.",
|
|
71
|
+
inputSchema: {
|
|
72
|
+
type: "object",
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
required: ["command_id"],
|
|
75
|
+
properties: {
|
|
76
|
+
command_id: { type: "string", minLength: 1 },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (waitManagedCommandNames.has(name)) {
|
|
82
|
+
return {
|
|
83
|
+
name,
|
|
84
|
+
description: "Wait for a command started by Forge's managed command runner to reach a terminal state. Use this once after completing work that is independent of the command; Forge streams durable progress while this call waits.",
|
|
85
|
+
inputSchema: {
|
|
86
|
+
type: "object",
|
|
87
|
+
additionalProperties: false,
|
|
88
|
+
required: ["command_id"],
|
|
89
|
+
properties: {
|
|
90
|
+
command_id: { type: "string", minLength: 1 },
|
|
91
|
+
wait_timeout_ms: { type: "integer", minimum: 1000, maximum: 7200000, default: 7200000 },
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
name,
|
|
98
|
+
description: "Quarantine known untracked generated test-output directories into this attempt's private output directory.",
|
|
99
|
+
inputSchema: { type: "object", additionalProperties: false, properties: {} },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export async function callManagedAttemptTool(name, input, worktreePath) {
|
|
103
|
+
if (resetGeneratedOutputNames.has(name)) {
|
|
104
|
+
const outputDirectory = requiredAttemptEnvironment("FORGE_TEST_OUTPUT_DIR");
|
|
105
|
+
const result = await resetGeneratedOutputs(worktreePath, outputDirectory);
|
|
106
|
+
return {
|
|
107
|
+
content: [{
|
|
108
|
+
type: "text",
|
|
109
|
+
text: result.quarantinedPaths.length === 0
|
|
110
|
+
? "Forge found no stale generated output in the source worktree."
|
|
111
|
+
: `Forge quarantined ${result.quarantinedPaths.length} generated-output director${result.quarantinedPaths.length === 1 ? "y" : "ies"}: ${result.quarantinedPaths.join(", ")}.`,
|
|
112
|
+
}],
|
|
113
|
+
structuredContent: {
|
|
114
|
+
status: "pass",
|
|
115
|
+
quarantined_paths: result.quarantinedPaths,
|
|
116
|
+
quarantine_directory: result.quarantineDirectory,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (getManagedCommandNames.has(name)) {
|
|
121
|
+
return await readManagedCommand(input);
|
|
122
|
+
}
|
|
123
|
+
if (waitManagedCommandNames.has(name)) {
|
|
124
|
+
return await waitManagedCommand(input);
|
|
125
|
+
}
|
|
126
|
+
if (!managedCommandNames.has(name) && !startManagedCommandNames.has(name)) {
|
|
127
|
+
throw new Error(`Unknown managed attempt tool: ${name}`);
|
|
128
|
+
}
|
|
129
|
+
const record = asRecord(input);
|
|
130
|
+
const command = requiredInputString(record, "command");
|
|
131
|
+
const args = requiredStringArray(record, "args");
|
|
132
|
+
const timeoutMs = optionalTimeout(record.timeout_ms);
|
|
133
|
+
const cwd = await containedWorkingDirectory(worktreePath, record.cwd);
|
|
134
|
+
const logDirectory = requiredAttemptEnvironment("FORGE_LOG_DIR");
|
|
135
|
+
await fs.mkdir(path.join(logDirectory, "managed-commands"), { recursive: true, mode: 0o700 });
|
|
136
|
+
const commandId = randomUUID();
|
|
137
|
+
const metadataPath = path.join(logDirectory, "managed-commands", `${commandId}.json`);
|
|
138
|
+
const receiptPath = managedCommandReceiptPath(logDirectory, commandId);
|
|
139
|
+
const invocation = {
|
|
140
|
+
commandId,
|
|
141
|
+
command,
|
|
142
|
+
args,
|
|
143
|
+
cwd,
|
|
144
|
+
timeoutMs,
|
|
145
|
+
logDirectory,
|
|
146
|
+
metadataPath,
|
|
147
|
+
receiptPath,
|
|
148
|
+
startedAt: Date.now(),
|
|
149
|
+
};
|
|
150
|
+
if (startManagedCommandNames.has(name) || timeoutMs > maximumSynchronousWaitMs) {
|
|
151
|
+
const started = managedCommandResult({
|
|
152
|
+
...invocation,
|
|
153
|
+
timedOut: false,
|
|
154
|
+
status: "starting",
|
|
155
|
+
exitCode: null,
|
|
156
|
+
stdout: "",
|
|
157
|
+
stderr: "",
|
|
158
|
+
});
|
|
159
|
+
await persistManagedCommandReceipt(receiptPath, started.structuredContent);
|
|
160
|
+
const task = executeManagedCommand(invocation);
|
|
161
|
+
managedCommandTasks.set(commandId, task);
|
|
162
|
+
void task.finally(() => managedCommandTasks.delete(commandId)).catch(() => undefined);
|
|
163
|
+
return {
|
|
164
|
+
...started,
|
|
165
|
+
content: [{
|
|
166
|
+
type: "text",
|
|
167
|
+
text: `Forge managed command ${commandId} started asynchronously. Continue independent work, then call forge.wait_managed_command once with this command_id for the terminal receipt.`,
|
|
168
|
+
}],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return await executeManagedCommand(invocation);
|
|
172
|
+
}
|
|
173
|
+
async function executeManagedCommand(invocation) {
|
|
174
|
+
const environment = inheritedEnvironment({
|
|
175
|
+
FORGE_RUNTIME_MODE: "host",
|
|
176
|
+
FORGE_RESOURCE_LEASE_ID: requiredAttemptEnvironment("FORGE_RESOURCE_LEASE_ID"),
|
|
177
|
+
FORGE_DISPATCH_ATTEMPT_ID: requiredAttemptEnvironment("FORGE_DISPATCH_ATTEMPT_ID"),
|
|
178
|
+
// This nested command owns a distinct supervisor identity. A command-local
|
|
179
|
+
// fence avoids exposing the parent attempt's fencing capability to Codex's
|
|
180
|
+
// MCP subprocess configuration while preserving exact-tree cleanup proof.
|
|
181
|
+
FORGE_FENCING_TOKEN: randomUUID(),
|
|
182
|
+
FORGE_LOG_DIR: invocation.logDirectory,
|
|
183
|
+
FORGE_TEST_OUTPUT_DIR: requiredAttemptEnvironment("FORGE_TEST_OUTPUT_DIR"),
|
|
184
|
+
FORGE_PROCESS_SUPERVISOR_ID: `managed-${invocation.commandId.replaceAll("-", "")}`,
|
|
185
|
+
FORGE_PROCESS_SUPERVISOR_METADATA_PATH: invocation.metadataPath,
|
|
186
|
+
});
|
|
187
|
+
let stdout = "";
|
|
188
|
+
let stderr = "";
|
|
189
|
+
const controller = new AbortController();
|
|
190
|
+
let timedOut = false;
|
|
191
|
+
let timer = null;
|
|
192
|
+
let outputRevision = 0;
|
|
193
|
+
let lastOutputAt = null;
|
|
194
|
+
let lastProgressPersistedAt = 0;
|
|
195
|
+
let progressPersistence = Promise.resolve();
|
|
196
|
+
const launchTimer = setTimeout(() => {
|
|
197
|
+
timedOut = true;
|
|
198
|
+
controller.abort(new Error(`managed_command_launch_timeout: process supervisor was not ready within ${managedCommandLaunchTimeoutMs}ms.`));
|
|
199
|
+
}, managedCommandLaunchTimeoutMs);
|
|
200
|
+
try {
|
|
201
|
+
const result = await new StreamingProviderProcessRunner().run({
|
|
202
|
+
command: invocation.command,
|
|
203
|
+
args: invocation.args,
|
|
204
|
+
cwd: invocation.cwd,
|
|
205
|
+
env: environment,
|
|
206
|
+
captureOutput: false,
|
|
207
|
+
}, {
|
|
208
|
+
onStarted: async () => {
|
|
209
|
+
clearTimeout(launchTimer);
|
|
210
|
+
const running = managedCommandResult({
|
|
211
|
+
...invocation,
|
|
212
|
+
timedOut: false,
|
|
213
|
+
status: "running",
|
|
214
|
+
exitCode: null,
|
|
215
|
+
stdout,
|
|
216
|
+
stderr,
|
|
217
|
+
outputRevision,
|
|
218
|
+
lastOutputAt,
|
|
219
|
+
});
|
|
220
|
+
await persistManagedCommandReceipt(invocation.receiptPath, running.structuredContent);
|
|
221
|
+
timer = setTimeout(() => {
|
|
222
|
+
timedOut = true;
|
|
223
|
+
controller.abort(new Error(`managed_command_timeout: exceeded ${invocation.timeoutMs}ms.`));
|
|
224
|
+
}, invocation.timeoutMs);
|
|
225
|
+
},
|
|
226
|
+
onStdoutLine: (line) => {
|
|
227
|
+
stdout = appendBounded(stdout, line);
|
|
228
|
+
outputRevision += 1;
|
|
229
|
+
lastOutputAt = Date.now();
|
|
230
|
+
progressPersistence = persistManagedCommandProgressIfDue(invocation, () => ({ stdout, stderr, outputRevision, lastOutputAt }), progressPersistence, () => lastProgressPersistedAt, value => { lastProgressPersistedAt = value; });
|
|
231
|
+
return progressPersistence;
|
|
232
|
+
},
|
|
233
|
+
onStderrLine: (line) => {
|
|
234
|
+
stderr = appendBounded(stderr, line);
|
|
235
|
+
outputRevision += 1;
|
|
236
|
+
lastOutputAt = Date.now();
|
|
237
|
+
progressPersistence = persistManagedCommandProgressIfDue(invocation, () => ({ stdout, stderr, outputRevision, lastOutputAt }), progressPersistence, () => lastProgressPersistedAt, value => { lastProgressPersistedAt = value; });
|
|
238
|
+
return progressPersistence;
|
|
239
|
+
},
|
|
240
|
+
}, controller.signal);
|
|
241
|
+
await progressPersistence;
|
|
242
|
+
const status = timedOut ? "timed_out" : result.exitCode === 0 ? "pass" : "fail";
|
|
243
|
+
const receipt = managedCommandResult({
|
|
244
|
+
...invocation,
|
|
245
|
+
timedOut,
|
|
246
|
+
status,
|
|
247
|
+
exitCode: result.exitCode,
|
|
248
|
+
stdout,
|
|
249
|
+
stderr,
|
|
250
|
+
outputRevision,
|
|
251
|
+
lastOutputAt,
|
|
252
|
+
});
|
|
253
|
+
await persistManagedCommandReceipt(invocation.receiptPath, receipt.structuredContent);
|
|
254
|
+
return receipt;
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
258
|
+
stderr = appendBounded(stderr, message);
|
|
259
|
+
const receipt = managedCommandResult({
|
|
260
|
+
...invocation,
|
|
261
|
+
timedOut,
|
|
262
|
+
status: timedOut ? "timeout_termination_failed" : "runner_error",
|
|
263
|
+
exitCode: null,
|
|
264
|
+
stdout,
|
|
265
|
+
stderr,
|
|
266
|
+
outputRevision,
|
|
267
|
+
lastOutputAt,
|
|
268
|
+
});
|
|
269
|
+
await persistManagedCommandReceipt(invocation.receiptPath, receipt.structuredContent);
|
|
270
|
+
return receipt;
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
clearTimeout(launchTimer);
|
|
274
|
+
if (timer)
|
|
275
|
+
clearTimeout(timer);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async function readManagedCommand(input) {
|
|
279
|
+
const commandId = validatedCommandId(asRecord(input));
|
|
280
|
+
const receiptPath = managedCommandReceiptPath(requiredAttemptEnvironment("FORGE_LOG_DIR"), commandId);
|
|
281
|
+
let receipt;
|
|
282
|
+
try {
|
|
283
|
+
receipt = JSON.parse(await fs.readFile(receiptPath, "utf8"));
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
if (error.code === "ENOENT") {
|
|
287
|
+
throw new Error(`managed_command_not_found: no durable receipt exists for ${commandId}.`, { cause: error });
|
|
288
|
+
}
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
const status = typeof receipt.status === "string" ? receipt.status : "unknown";
|
|
292
|
+
const stdout = typeof receipt.stdout === "string" ? receipt.stdout : "";
|
|
293
|
+
const stderr = typeof receipt.stderr === "string" ? receipt.stderr : "";
|
|
294
|
+
return {
|
|
295
|
+
content: [{
|
|
296
|
+
type: "text",
|
|
297
|
+
text: terminalManagedCommandStatuses.has(status)
|
|
298
|
+
? terminalManagedCommandText({
|
|
299
|
+
commandId,
|
|
300
|
+
status,
|
|
301
|
+
exitCode: typeof receipt.exit_code === "number" ? receipt.exit_code : null,
|
|
302
|
+
elapsedMs: typeof receipt.elapsed_ms === "number" ? receipt.elapsed_ms : null,
|
|
303
|
+
stdout,
|
|
304
|
+
stderr,
|
|
305
|
+
})
|
|
306
|
+
: `Forge managed command ${commandId} is ${status}. Continue independent work and use forge.wait_managed_command when ready for its terminal receipt.`,
|
|
307
|
+
}],
|
|
308
|
+
structuredContent: receipt,
|
|
309
|
+
isError: terminalManagedCommandStatuses.has(status) && status !== "pass",
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
async function waitManagedCommand(input) {
|
|
313
|
+
const record = asRecord(input);
|
|
314
|
+
const commandId = validatedCommandId(record);
|
|
315
|
+
const waitTimeoutMs = optionalWaitTimeout(record.wait_timeout_ms);
|
|
316
|
+
const deadline = Date.now() + waitTimeoutMs;
|
|
317
|
+
const receiptPath = managedCommandReceiptPath(requiredAttemptEnvironment("FORGE_LOG_DIR"), commandId);
|
|
318
|
+
const task = managedCommandTasks.get(commandId);
|
|
319
|
+
if (task) {
|
|
320
|
+
const result = await Promise.race([
|
|
321
|
+
task,
|
|
322
|
+
delay(waitTimeoutMs).then(() => null),
|
|
323
|
+
]);
|
|
324
|
+
if (result)
|
|
325
|
+
return result;
|
|
326
|
+
}
|
|
327
|
+
while (Date.now() < deadline) {
|
|
328
|
+
const result = await readManagedCommand({ command_id: commandId });
|
|
329
|
+
if (terminalManagedCommandStatuses.has(String(result.structuredContent.status))) {
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
await delay(Math.min(managedCommandReceiptPollIntervalMs, Math.max(1, deadline - Date.now())));
|
|
333
|
+
}
|
|
334
|
+
const receipt = await readManagedCommand({ command_id: commandId });
|
|
335
|
+
return {
|
|
336
|
+
...receipt,
|
|
337
|
+
content: [{
|
|
338
|
+
type: "text",
|
|
339
|
+
text: `Forge is still supervising managed command ${commandId} after waiting ${waitTimeoutMs}ms. Its durable receipt remains at ${receiptPath}.`,
|
|
340
|
+
}],
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function managedCommandReceiptPath(logDirectory, commandId) {
|
|
344
|
+
return path.join(logDirectory, "managed-commands", `${commandId}.receipt.json`);
|
|
345
|
+
}
|
|
346
|
+
async function persistManagedCommandReceipt(receiptPath, receipt) {
|
|
347
|
+
const temporaryPath = `${receiptPath}.${randomUUID()}.tmp`;
|
|
348
|
+
await fs.writeFile(temporaryPath, `${JSON.stringify(receipt, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
349
|
+
try {
|
|
350
|
+
for (let attempt = 1;; attempt += 1) {
|
|
351
|
+
try {
|
|
352
|
+
await fs.rename(temporaryPath, receiptPath);
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
const code = error.code;
|
|
357
|
+
if (attempt >= managedCommandReceiptReplaceAttempts
|
|
358
|
+
|| (code !== "EPERM" && code !== "EBUSY" && code !== "EACCES")) {
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
await delay(managedCommandReceiptReplaceRetryMs);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function managedCommandResult(values) {
|
|
370
|
+
const elapsedMs = Date.now() - values.startedAt;
|
|
371
|
+
const isTerminal = terminalManagedCommandStatuses.has(values.status);
|
|
372
|
+
const diagnostics = isTerminal
|
|
373
|
+
? managedCommandDiagnostics(values.stdout, values.stderr)
|
|
374
|
+
: undefined;
|
|
375
|
+
return {
|
|
376
|
+
content: [{
|
|
377
|
+
type: "text",
|
|
378
|
+
text: isTerminal
|
|
379
|
+
? terminalManagedCommandText({
|
|
380
|
+
commandId: values.commandId,
|
|
381
|
+
status: values.status,
|
|
382
|
+
exitCode: values.exitCode,
|
|
383
|
+
elapsedMs,
|
|
384
|
+
stdout: values.stdout,
|
|
385
|
+
stderr: values.stderr,
|
|
386
|
+
}, diagnostics)
|
|
387
|
+
: `Forge managed command ${values.commandId} is ${values.status} after ${elapsedMs}ms.`,
|
|
388
|
+
}],
|
|
389
|
+
structuredContent: {
|
|
390
|
+
command_id: values.commandId,
|
|
391
|
+
status: values.status,
|
|
392
|
+
command: [values.command, ...values.args],
|
|
393
|
+
cwd: values.cwd,
|
|
394
|
+
timeout_ms: values.timeoutMs,
|
|
395
|
+
elapsed_ms: elapsedMs,
|
|
396
|
+
started_at: new Date(values.startedAt).toISOString(),
|
|
397
|
+
timed_out: values.timedOut,
|
|
398
|
+
exit_code: values.exitCode,
|
|
399
|
+
stdout: values.stdout,
|
|
400
|
+
stderr: values.stderr,
|
|
401
|
+
...(diagnostics ? { diagnostics } : {}),
|
|
402
|
+
output_truncated: values.stdout.length >= maximumCapturedCharacters
|
|
403
|
+
|| values.stderr.length >= maximumCapturedCharacters,
|
|
404
|
+
output_revision: values.outputRevision ?? 0,
|
|
405
|
+
last_output_at: values.lastOutputAt ? new Date(values.lastOutputAt).toISOString() : null,
|
|
406
|
+
supervisor_metadata_path: values.metadataPath,
|
|
407
|
+
receipt_path: values.receiptPath,
|
|
408
|
+
process_tree_terminated: values.status === "timed_out" ? true : values.timedOut ? false : undefined,
|
|
409
|
+
},
|
|
410
|
+
isError: isTerminal && values.status !== "pass",
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function persistManagedCommandProgressIfDue(invocation, snapshot, previousPersistence, lastPersistedAt, setLastPersistedAt) {
|
|
414
|
+
const now = Date.now();
|
|
415
|
+
if (now - lastPersistedAt() < managedCommandProgressPersistIntervalMs)
|
|
416
|
+
return previousPersistence;
|
|
417
|
+
setLastPersistedAt(now);
|
|
418
|
+
const current = snapshot();
|
|
419
|
+
return previousPersistence.then(async () => {
|
|
420
|
+
const receipt = managedCommandResult({
|
|
421
|
+
...invocation,
|
|
422
|
+
timedOut: false,
|
|
423
|
+
status: "running",
|
|
424
|
+
exitCode: null,
|
|
425
|
+
...current,
|
|
426
|
+
});
|
|
427
|
+
await persistManagedCommandReceipt(invocation.receiptPath, receipt.structuredContent);
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
async function containedWorkingDirectory(worktreePath, input) {
|
|
431
|
+
const root = await fs.realpath(path.resolve(worktreePath));
|
|
432
|
+
const relative = input === undefined ? "." : requiredStandaloneString(input, "cwd");
|
|
433
|
+
if (path.isAbsolute(relative))
|
|
434
|
+
throw new Error("managed_command_invalid: cwd must be worktree-relative.");
|
|
435
|
+
const target = await fs.realpath(path.resolve(root, relative));
|
|
436
|
+
const containment = path.relative(root, target);
|
|
437
|
+
if (containment.startsWith(`..${path.sep}`) || path.isAbsolute(containment)) {
|
|
438
|
+
throw new Error("managed_command_invalid: cwd resolves outside the source worktree.");
|
|
439
|
+
}
|
|
440
|
+
return target;
|
|
441
|
+
}
|
|
442
|
+
function inheritedEnvironment(overrides) {
|
|
443
|
+
const env = {};
|
|
444
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
445
|
+
if (typeof value === "string" && !looksSensitiveEnvironmentName(key))
|
|
446
|
+
env[key] = value;
|
|
447
|
+
}
|
|
448
|
+
return { ...env, ...overrides };
|
|
449
|
+
}
|
|
450
|
+
function looksSensitiveEnvironmentName(name) {
|
|
451
|
+
const normalized = name.trim().toUpperCase();
|
|
452
|
+
return normalized.includes("SECRET")
|
|
453
|
+
|| normalized.includes("TOKEN")
|
|
454
|
+
|| normalized.includes("PASSWORD")
|
|
455
|
+
|| normalized.includes("CREDENTIAL")
|
|
456
|
+
|| normalized.endsWith("_API_KEY")
|
|
457
|
+
|| normalized === "OPENAI_API_KEY";
|
|
458
|
+
}
|
|
459
|
+
function requiredAttemptEnvironment(name) {
|
|
460
|
+
const value = process.env[name]?.trim();
|
|
461
|
+
if (!value)
|
|
462
|
+
throw new Error(`managed_attempt_tool_unavailable: ${name} is not configured.`);
|
|
463
|
+
return value;
|
|
464
|
+
}
|
|
465
|
+
function asRecord(value) {
|
|
466
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
467
|
+
throw new Error("managed_command_invalid: input must be an object.");
|
|
468
|
+
return value;
|
|
469
|
+
}
|
|
470
|
+
function requiredInputString(record, name) {
|
|
471
|
+
return requiredStandaloneString(record[name], name);
|
|
472
|
+
}
|
|
473
|
+
function validatedCommandId(record) {
|
|
474
|
+
const commandId = requiredInputString(record, "command_id");
|
|
475
|
+
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)) {
|
|
476
|
+
throw new Error("managed_command_invalid: command_id must be a UUID returned by Forge.");
|
|
477
|
+
}
|
|
478
|
+
return commandId;
|
|
479
|
+
}
|
|
480
|
+
function requiredStandaloneString(value, name) {
|
|
481
|
+
if (typeof value !== "string" || !value.trim())
|
|
482
|
+
throw new Error(`managed_command_invalid: ${name} is required.`);
|
|
483
|
+
return value.trim();
|
|
484
|
+
}
|
|
485
|
+
function requiredStringArray(record, name) {
|
|
486
|
+
const value = record[name];
|
|
487
|
+
if (!Array.isArray(value) || value.some(entry => typeof entry !== "string")) {
|
|
488
|
+
throw new Error(`managed_command_invalid: ${name} must be a string array.`);
|
|
489
|
+
}
|
|
490
|
+
return value;
|
|
491
|
+
}
|
|
492
|
+
function optionalTimeout(value) {
|
|
493
|
+
if (value === undefined)
|
|
494
|
+
return 2_700_000;
|
|
495
|
+
if (!Number.isSafeInteger(value) || value < 1_000 || value > 7_200_000) {
|
|
496
|
+
throw new Error("managed_command_invalid: timeout_ms must be an integer from 1000 through 7200000.");
|
|
497
|
+
}
|
|
498
|
+
return value;
|
|
499
|
+
}
|
|
500
|
+
function optionalWaitTimeout(value) {
|
|
501
|
+
if (value === undefined)
|
|
502
|
+
return 7_200_000;
|
|
503
|
+
if (!Number.isSafeInteger(value) || value < 1_000 || value > 7_200_000) {
|
|
504
|
+
throw new Error("managed_command_invalid: wait_timeout_ms must be an integer from 1000 through 7200000.");
|
|
505
|
+
}
|
|
506
|
+
return value;
|
|
507
|
+
}
|
|
508
|
+
async function delay(ms) {
|
|
509
|
+
await new Promise(resolve => setTimeout(resolve, ms));
|
|
510
|
+
}
|
|
511
|
+
function appendBounded(current, line) {
|
|
512
|
+
const next = `${current}${line}\n`;
|
|
513
|
+
return next.length <= maximumCapturedCharacters
|
|
514
|
+
? next
|
|
515
|
+
: next.slice(-maximumCapturedCharacters);
|
|
516
|
+
}
|
|
517
|
+
function managedCommandDiagnostics(stdout, stderr) {
|
|
518
|
+
const lines = `${stdout}\n${stderr}`
|
|
519
|
+
.split(/\r?\n/u)
|
|
520
|
+
.map(line => line.trim())
|
|
521
|
+
.filter(Boolean);
|
|
522
|
+
const failedTests = new Set();
|
|
523
|
+
for (const line of lines) {
|
|
524
|
+
const xunit = line.match(/^\[xUnit\.net[^\]]*\]\s+(.+?)\s+\[FAIL\]$/u);
|
|
525
|
+
if (xunit?.[1])
|
|
526
|
+
failedTests.add(xunit[1].trim());
|
|
527
|
+
const vstest = line.match(/^Failed\s+(.+?)(?:\s+\[(?:\d|<).*)?$/u);
|
|
528
|
+
if (vstest?.[1] && !vstest[1].startsWith("!"))
|
|
529
|
+
failedTests.add(vstest[1].trim());
|
|
530
|
+
}
|
|
531
|
+
const summaryLines = lines
|
|
532
|
+
.filter(line => /(?:Failed!|Passed!|Test Run|Tests?:\s|Test Files|\[FAIL\]|error\s+[A-Z]+\d+)/iu.test(line))
|
|
533
|
+
.slice(-20);
|
|
534
|
+
return {
|
|
535
|
+
failed_tests: [...failedTests].slice(0, maximumReportedFailedTests),
|
|
536
|
+
failed_test_count: failedTests.size,
|
|
537
|
+
summary_lines: summaryLines,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function terminalManagedCommandText(values, diagnostics = managedCommandDiagnostics(values.stdout, values.stderr)) {
|
|
541
|
+
const failedTests = diagnostics.failed_tests;
|
|
542
|
+
const summaryLines = diagnostics.summary_lines;
|
|
543
|
+
const duration = values.elapsedMs === null ? "an unknown duration" : `${values.elapsedMs}ms`;
|
|
544
|
+
const sections = [
|
|
545
|
+
`Forge managed command ${values.commandId} ${values.status} after ${duration} (exit ${values.exitCode ?? "unavailable"}).`,
|
|
546
|
+
];
|
|
547
|
+
if (failedTests.length > 0) {
|
|
548
|
+
sections.push(`Failed tests (${diagnostics.failed_test_count}):\n${failedTests.map(test => `- ${test}`).join("\n")}`);
|
|
549
|
+
}
|
|
550
|
+
if (summaryLines.length > 0) {
|
|
551
|
+
sections.push(`Test summary:\n${summaryLines.join("\n")}`);
|
|
552
|
+
}
|
|
553
|
+
const stdoutTail = tail(values.stdout, maximumReturnedOutputCharacters);
|
|
554
|
+
const stderrTail = tail(values.stderr, maximumReturnedOutputCharacters);
|
|
555
|
+
sections.push(`stdout tail:\n${stdoutTail || "(empty)"}`);
|
|
556
|
+
sections.push(`stderr tail:\n${stderrTail || "(empty)"}`);
|
|
557
|
+
return sections.join("\n\n");
|
|
558
|
+
}
|
|
559
|
+
function tail(value, maximumCharacters) {
|
|
560
|
+
return value.length <= maximumCharacters ? value : value.slice(-maximumCharacters);
|
|
561
|
+
}
|
|
@@ -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);
|
|
@@ -424,7 +431,7 @@ async function synthesizeQaCycleVerdictPayload(config, payload, status) {
|
|
|
424
431
|
]);
|
|
425
432
|
merged.browser_checks = dedupeUnknownArray([
|
|
426
433
|
...qaArray(payload, "browser_checks", payloadEvidence),
|
|
427
|
-
...evidenceGates.flatMap((gate) => qaArray(gate, "browser_checks"))
|
|
434
|
+
...evidenceGates.flatMap((gate) => qaArray(gate, "browser_checks").map(normalizeInheritedQaBrowserCheck))
|
|
428
435
|
]);
|
|
429
436
|
merged.sidecars = dedupeUnknownArray([
|
|
430
437
|
...qaArray(payload, "sidecars", payloadEvidence),
|
|
@@ -516,6 +523,27 @@ function dedupeStrings(values) {
|
|
|
516
523
|
}
|
|
517
524
|
return deduped;
|
|
518
525
|
}
|
|
526
|
+
function normalizeInheritedQaBrowserCheck(browserCheck) {
|
|
527
|
+
if (!browserCheck || typeof browserCheck !== "object" || Array.isArray(browserCheck)) {
|
|
528
|
+
return browserCheck;
|
|
529
|
+
}
|
|
530
|
+
const record = browserCheck;
|
|
531
|
+
if (optionalString(record, "summary")) {
|
|
532
|
+
return record;
|
|
533
|
+
}
|
|
534
|
+
const target = optionalString(record, "url") ?? optionalString(record, "route") ?? "the configured browser target";
|
|
535
|
+
const status = optionalString(record, "status") ?? "recorded";
|
|
536
|
+
return {
|
|
537
|
+
...record,
|
|
538
|
+
summary: `Inherited browser check ${status} for ${target}.`
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function normalizeSha256ContentHash(contentHash) {
|
|
542
|
+
const normalized = contentHash.trim().toLowerCase();
|
|
543
|
+
return normalized.startsWith("sha256:")
|
|
544
|
+
? normalized.slice("sha256:".length)
|
|
545
|
+
: normalized;
|
|
546
|
+
}
|
|
519
547
|
async function validateQaSidecarReferences(worktreePath, workItemId, sidecars, ownerType, ownerId) {
|
|
520
548
|
if (sidecars.length === 0) {
|
|
521
549
|
return [];
|
|
@@ -557,7 +585,7 @@ async function validateQaSidecarReferences(worktreePath, workItemId, sidecars, o
|
|
|
557
585
|
throw new Error(`sidecars[${index}].bytes does not match the referenced file.`);
|
|
558
586
|
}
|
|
559
587
|
const actualHash = await sha256File(fullPath);
|
|
560
|
-
if (actualHash !== contentHash
|
|
588
|
+
if (actualHash !== normalizeSha256ContentHash(contentHash)) {
|
|
561
589
|
throw new Error(`sidecars[${index}].content_hash does not match the referenced file.`);
|
|
562
590
|
}
|
|
563
591
|
validated.push({
|
|
@@ -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
|
}
|