@loucompanion/forge-bridge 0.1.1-dev.f599637d804e → 0.1.3-dev.2c3feac3af65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge/bridge.service.js +89 -9
- package/dist/bridge/bridge.types.d.ts +2 -0
- package/dist/cli/cli.service.js +212 -1
- package/dist/cli/local-config.d.ts +1 -0
- package/dist/cli/local-config.js +18 -3
- package/dist/config.js +1 -1
- package/dist/session/phase-completion.d.ts +23 -1
- package/dist/session/phase-completion.js +1046 -31
- package/dist/session/provider-cli/provider-cli.base.d.ts +2 -1
- package/dist/session/provider-cli/provider-cli.service.d.ts +13 -1
- package/dist/session/provider-cli/provider-cli.service.js +602 -19
- package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
- package/dist/session/session.service.js +17 -0
- package/dist/session/session.types.d.ts +26 -0
- package/dist/session/session.types.js +47 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.js +72 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { execFile, spawn } from "node:child_process";
|
|
5
6
|
import readline from "node:readline";
|
|
@@ -8,7 +9,7 @@ import { GitClient } from "../../shared/git/git.js";
|
|
|
8
9
|
import { resolveCursorAgentDirectLaunch } from "../../shared/cursor-agent/cursor-agent.launch.js";
|
|
9
10
|
import { ensureNoSymbolicLinkPathSegments, isPathUnderRoot, sha256File } from "../qa-sidecar-validation.js";
|
|
10
11
|
import { buildSpecBody, selectLaneCommand } from "../../worktree/artifact-injector.js";
|
|
11
|
-
import { codexMcpConfigArgs, completionPhaseFromDispatch, completionPromptForDebugCycle, completionPromptForDevelopCycle, completionPromptForPhase, completionPromptForQaCycle, completionRedactionEnv, expectedCompletionToolName, expectedDebugCompletionToolName, expectedQaCompletionToolName, prepareCompletionToolRuntime, readCompletionArtifact, usesDebugCycleCompletion, usesDevelopCycleCompletion, writeCompletionArtifact, artifactRootFor, artifactRootForOwner, debugCompletionPhaseOrder, qaCompletionGateOrder } from "../phase-completion.js";
|
|
12
|
+
import { codexMcpConfigArgs, completionPhaseFromDispatch, completionPromptForDebugCycle, completionPromptForDevelopCycle, completionPromptForDraftCycle, completionPromptForPhase, completionPromptForQaCycle, completionRedactionEnv, expectedCompletionToolName, expectedDebugCompletionToolName, expectedQaCompletionToolName, prepareCompletionToolRuntime, readCompletionArtifact, usesDebugCycleCompletion, usesDevelopCycleCompletion, writeCompletionArtifact, artifactRootFor, artifactRootForOwner, debugCompletionPhaseOrder, draftCompletionGateOrder, qaCompletionGateOrder } from "../phase-completion.js";
|
|
12
13
|
import { addUsage, buildProviderUsageEvent, emptyUsage, hasUsage, readUsageNumber } from "./provider-usage.js";
|
|
13
14
|
const fakeWorkerModel = "server-local-fake-worker";
|
|
14
15
|
const execFileAsync = promisify(execFile);
|
|
@@ -24,15 +25,31 @@ const allowedProviderParentEnvNames = [
|
|
|
24
25
|
];
|
|
25
26
|
const windowsAllowedProviderParentEnvNames = [
|
|
26
27
|
"SystemRoot",
|
|
28
|
+
"SystemDrive",
|
|
27
29
|
"ComSpec",
|
|
28
30
|
"PATHEXT",
|
|
29
31
|
"APPDATA",
|
|
30
|
-
"LOCALAPPDATA"
|
|
32
|
+
"LOCALAPPDATA",
|
|
33
|
+
"USERNAME",
|
|
34
|
+
"USERDOMAIN",
|
|
35
|
+
"HOMEDRIVE",
|
|
36
|
+
"HOMEPATH",
|
|
37
|
+
"ProgramData",
|
|
38
|
+
"ProgramFiles",
|
|
39
|
+
"ProgramFiles(x86)",
|
|
40
|
+
"ProgramW6432",
|
|
41
|
+
"DOTNET_ROOT",
|
|
42
|
+
"DOTNET_ROOT(x86)",
|
|
43
|
+
"DOTNET_CLI_HOME",
|
|
44
|
+
"NUGET_PACKAGES",
|
|
45
|
+
"NUGET_HTTP_CACHE_PATH"
|
|
31
46
|
];
|
|
32
|
-
function buildProviderChildEnvironment(requestEnv) {
|
|
47
|
+
export function buildProviderChildEnvironment(requestEnv, options = {}) {
|
|
48
|
+
const parentEnv = options.parentEnv ?? process.env;
|
|
49
|
+
const platform = options.platform ?? process.platform;
|
|
33
50
|
const env = {};
|
|
34
|
-
for (const [key, value] of Object.entries(
|
|
35
|
-
if (typeof value === "string" && isAllowedProviderParentEnv(key)) {
|
|
51
|
+
for (const [key, value] of Object.entries(parentEnv)) {
|
|
52
|
+
if (typeof value === "string" && isAllowedProviderParentEnv(key, platform)) {
|
|
36
53
|
env[key] = value;
|
|
37
54
|
}
|
|
38
55
|
}
|
|
@@ -44,16 +61,88 @@ function buildProviderChildEnvironment(requestEnv) {
|
|
|
44
61
|
}
|
|
45
62
|
}
|
|
46
63
|
}
|
|
64
|
+
if (platform === "win32") {
|
|
65
|
+
normalizeWindowsDotNetEnvironment(env, parentEnv, options.homeDir ?? os.homedir());
|
|
66
|
+
}
|
|
47
67
|
return env;
|
|
48
68
|
}
|
|
49
|
-
function isAllowedProviderParentEnv(key) {
|
|
69
|
+
function isAllowedProviderParentEnv(key, platform = process.platform) {
|
|
50
70
|
return isAllowedProviderParentEnvName(key, allowedProviderParentEnvNames)
|
|
51
71
|
|| key.toUpperCase().startsWith("LC_")
|
|
52
|
-
|| (
|
|
72
|
+
|| (platform === "win32" && isAllowedProviderParentEnvName(key, windowsAllowedProviderParentEnvNames));
|
|
53
73
|
}
|
|
54
74
|
function isAllowedProviderParentEnvName(key, allowedNames) {
|
|
55
75
|
return allowedNames.some((allowed) => allowed.toLowerCase() === key.toLowerCase());
|
|
56
76
|
}
|
|
77
|
+
function normalizeWindowsDotNetEnvironment(env, parentEnv, homeDir) {
|
|
78
|
+
const systemRoot = readEnvironmentValue(env, "SystemRoot") ?? readEnvironmentValue(parentEnv, "SystemRoot") ?? "C:\\Windows";
|
|
79
|
+
const systemDrive = path.win32.parse(systemRoot).root.replace(/\\$/, "") || "C:";
|
|
80
|
+
setEnvironmentValueIfMissing(env, "SystemRoot", systemRoot);
|
|
81
|
+
setEnvironmentValueIfMissing(env, "SystemDrive", readEnvironmentValue(parentEnv, "SystemDrive") ?? systemDrive);
|
|
82
|
+
setEnvironmentValueIfMissing(env, "ComSpec", readEnvironmentValue(parentEnv, "ComSpec") ?? path.win32.join(systemRoot, "System32", "cmd.exe"));
|
|
83
|
+
setEnvironmentValueIfMissing(env, "PATHEXT", readEnvironmentValue(parentEnv, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.CPL");
|
|
84
|
+
const userProfile = resolveWindowsUserProfile(env, parentEnv, homeDir);
|
|
85
|
+
if (userProfile) {
|
|
86
|
+
setEnvironmentValueIfMissing(env, "USERPROFILE", userProfile);
|
|
87
|
+
setEnvironmentValueIfMissing(env, "HOME", userProfile);
|
|
88
|
+
setEnvironmentValueIfMissing(env, "DOTNET_CLI_HOME", userProfile);
|
|
89
|
+
setEnvironmentValueIfMissing(env, "NUGET_PACKAGES", path.win32.join(userProfile, ".nuget", "packages"));
|
|
90
|
+
const parsed = path.win32.parse(userProfile);
|
|
91
|
+
if (parsed.root) {
|
|
92
|
+
setEnvironmentValueIfMissing(env, "HOMEDRIVE", parsed.root.replace(/\\$/, ""));
|
|
93
|
+
setEnvironmentValueIfMissing(env, "HOMEPATH", userProfile.slice(parsed.root.length - (parsed.root.endsWith("\\") ? 1 : 0)) || "\\");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const appData = readEnvironmentValue(env, "APPDATA")
|
|
97
|
+
?? readEnvironmentValue(parentEnv, "APPDATA")
|
|
98
|
+
?? (userProfile ? path.win32.join(userProfile, "AppData", "Roaming") : undefined);
|
|
99
|
+
const localAppData = readEnvironmentValue(env, "LOCALAPPDATA")
|
|
100
|
+
?? readEnvironmentValue(parentEnv, "LOCALAPPDATA")
|
|
101
|
+
?? (userProfile ? path.win32.join(userProfile, "AppData", "Local") : undefined);
|
|
102
|
+
if (appData) {
|
|
103
|
+
setEnvironmentValueIfMissing(env, "APPDATA", appData);
|
|
104
|
+
}
|
|
105
|
+
if (localAppData) {
|
|
106
|
+
setEnvironmentValueIfMissing(env, "LOCALAPPDATA", localAppData);
|
|
107
|
+
setEnvironmentValueIfMissing(env, "NUGET_HTTP_CACHE_PATH", path.win32.join(localAppData, "NuGet", "v3-cache"));
|
|
108
|
+
}
|
|
109
|
+
setEnvironmentValueIfMissing(env, "DOTNET_NOLOGO", "1");
|
|
110
|
+
setEnvironmentValueIfMissing(env, "DOTNET_CLI_TELEMETRY_OPTOUT", "1");
|
|
111
|
+
setEnvironmentValueIfMissing(env, "DOTNET_SKIP_FIRST_TIME_EXPERIENCE", "1");
|
|
112
|
+
setEnvironmentValueIfMissing(env, "NUGET_XMLDOC_MODE", "skip");
|
|
113
|
+
}
|
|
114
|
+
function resolveWindowsUserProfile(env, parentEnv, homeDir) {
|
|
115
|
+
const existing = readEnvironmentValue(env, "USERPROFILE") ?? readEnvironmentValue(parentEnv, "USERPROFILE");
|
|
116
|
+
if (existing) {
|
|
117
|
+
return existing;
|
|
118
|
+
}
|
|
119
|
+
const home = readEnvironmentValue(env, "HOME") ?? readEnvironmentValue(parentEnv, "HOME");
|
|
120
|
+
if (home && path.win32.isAbsolute(home)) {
|
|
121
|
+
return home;
|
|
122
|
+
}
|
|
123
|
+
const homeDrive = readEnvironmentValue(env, "HOMEDRIVE") ?? readEnvironmentValue(parentEnv, "HOMEDRIVE");
|
|
124
|
+
const homePath = readEnvironmentValue(env, "HOMEPATH") ?? readEnvironmentValue(parentEnv, "HOMEPATH");
|
|
125
|
+
if (homeDrive && homePath) {
|
|
126
|
+
return `${homeDrive}${homePath}`;
|
|
127
|
+
}
|
|
128
|
+
if (homeDir && path.win32.isAbsolute(homeDir)) {
|
|
129
|
+
return homeDir;
|
|
130
|
+
}
|
|
131
|
+
const systemDrive = readEnvironmentValue(env, "SystemDrive") ?? readEnvironmentValue(parentEnv, "SystemDrive") ?? "C:";
|
|
132
|
+
const username = readEnvironmentValue(env, "USERNAME") ?? readEnvironmentValue(parentEnv, "USERNAME");
|
|
133
|
+
return username ? path.win32.join(systemDrive, "Users", username) : undefined;
|
|
134
|
+
}
|
|
135
|
+
function readEnvironmentValue(env, name) {
|
|
136
|
+
const entry = Object.entries(env).find(([key]) => key.toLowerCase() === name.toLowerCase());
|
|
137
|
+
const value = entry?.[1];
|
|
138
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
139
|
+
}
|
|
140
|
+
function setEnvironmentValueIfMissing(env, name, value) {
|
|
141
|
+
if (readEnvironmentValue(env, name)) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
env[name] = value;
|
|
145
|
+
}
|
|
57
146
|
export class RoutingProviderCliService {
|
|
58
147
|
realProvider;
|
|
59
148
|
deterministicProvider;
|
|
@@ -81,6 +170,11 @@ export class RoutingProviderCliService {
|
|
|
81
170
|
? await this.deterministicProvider.runQa(context)
|
|
82
171
|
: await this.realProvider.runQa(context);
|
|
83
172
|
}
|
|
173
|
+
async runDraft(context) {
|
|
174
|
+
return isFakeWorkerModel(context.dispatch.execution.model)
|
|
175
|
+
? await this.deterministicProvider.runDraft(context)
|
|
176
|
+
: await this.realProvider.runDraft(context);
|
|
177
|
+
}
|
|
84
178
|
}
|
|
85
179
|
export class ProcessBackedProviderCliService {
|
|
86
180
|
processRunner;
|
|
@@ -89,6 +183,174 @@ export class ProcessBackedProviderCliService {
|
|
|
89
183
|
this.processRunner = processRunner;
|
|
90
184
|
this.git = git;
|
|
91
185
|
}
|
|
186
|
+
async runDraft(context) {
|
|
187
|
+
const { dispatch } = context;
|
|
188
|
+
if (!dispatch.draft) {
|
|
189
|
+
throw new Error("Draft dispatch context is missing.");
|
|
190
|
+
}
|
|
191
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "forge-bridge-draft-"));
|
|
192
|
+
const draftWorktree = {
|
|
193
|
+
repoPath: tempDir,
|
|
194
|
+
worktreePath: tempDir,
|
|
195
|
+
branchName: "draft",
|
|
196
|
+
baseBranch: dispatch.cycleRun.baseBranch || dispatch.repo.defaultBranch || "draft",
|
|
197
|
+
cleanupWorktrees: true
|
|
198
|
+
};
|
|
199
|
+
const metadataRoot = path.join(tempDir, ".forge", "work-items", dispatch.workItem.id);
|
|
200
|
+
await fs.mkdir(metadataRoot, { recursive: true });
|
|
201
|
+
await fs.writeFile(path.join(metadataRoot, "draft-input.json"), JSON.stringify(dispatch.draft, null, 2) + "\n", "utf8");
|
|
202
|
+
await fs.writeFile(path.join(metadataRoot, "spec.md"), buildDraftSpec(dispatch), "utf8");
|
|
203
|
+
const decomposeDraft = isDecomposeDraftDispatch(dispatch);
|
|
204
|
+
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree: draftWorktree, phase: decomposeDraft ? "decompose" : "draft" });
|
|
205
|
+
const command = buildProviderCliCommand(dispatch, {
|
|
206
|
+
worktreePath: tempDir,
|
|
207
|
+
prompt: buildDraftPrompt(dispatch),
|
|
208
|
+
completionTools: completionTools ?? undefined,
|
|
209
|
+
skipGitRepoCheck: true
|
|
210
|
+
});
|
|
211
|
+
const draftArtifactSlugs = dispatch.artifacts
|
|
212
|
+
.filter((artifact) => artifact.scope === "operational" && ["command", "skill", "hook"].includes(artifact.kind))
|
|
213
|
+
.map((artifact) => `${artifact.kind}:${artifact.slug}`);
|
|
214
|
+
await context.emit({
|
|
215
|
+
role: "runner",
|
|
216
|
+
kind: "checkpoint",
|
|
217
|
+
content: `Draft artifacts injected: ${draftArtifactSlugs.length}${draftArtifactSlugs.length ? ` (${draftArtifactSlugs.join(", ")})` : ""}. Project: ${dispatch.draft.projectName ?? dispatch.repo.name}. Repo: ${dispatch.draft.repoName ?? dispatch.repo.name}.`,
|
|
218
|
+
eventType: "draft_artifacts_injected"
|
|
219
|
+
});
|
|
220
|
+
await context.emit({
|
|
221
|
+
role: "runner",
|
|
222
|
+
kind: "checkpoint",
|
|
223
|
+
content: `Starting provider CLI Draft: ${formatProviderCliCommand(command)}`,
|
|
224
|
+
eventType: "provider_cli_started"
|
|
225
|
+
});
|
|
226
|
+
await context.emit({
|
|
227
|
+
role: "runner",
|
|
228
|
+
kind: "checkpoint",
|
|
229
|
+
content: decomposeDraft
|
|
230
|
+
? "Forge Decompose completion tools configured for decompose_subject_intake -> decompose_context_review -> decompose_breakdown_strategy -> decompose_child_items -> decompose_dependency_graph -> decompose_critic -> decompose_proposal; provider must call each MCP completion tool before exit."
|
|
231
|
+
: "Forge Draft completion tools configured for draft_intent -> draft_repo_context -> draft_graph_context -> draft_product_scope -> draft_work_order_plan -> draft_critic -> draft_proposal; provider must call each MCP completion tool before exit.",
|
|
232
|
+
eventType: "completion_tools_configured"
|
|
233
|
+
});
|
|
234
|
+
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
235
|
+
try {
|
|
236
|
+
const providerCancellation = new AbortController();
|
|
237
|
+
const abortProvider = () => providerCancellation.abort();
|
|
238
|
+
if (context.signal?.aborted) {
|
|
239
|
+
abortProvider();
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
context.signal?.addEventListener("abort", abortProvider, { once: true });
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const runPromise = this.processRunner.run({
|
|
246
|
+
command: command.command,
|
|
247
|
+
args: command.args,
|
|
248
|
+
cwd: tempDir,
|
|
249
|
+
env: providerProcessEnv(dispatch)
|
|
250
|
+
}, {
|
|
251
|
+
onStdoutLine: async (line) => {
|
|
252
|
+
for (const event of outputState.consume(line)) {
|
|
253
|
+
await context.emit(event);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}, providerCancellation.signal);
|
|
257
|
+
const artifactWaitCancellation = new AbortController();
|
|
258
|
+
const abortArtifactWait = () => artifactWaitCancellation.abort();
|
|
259
|
+
if (context.signal?.aborted) {
|
|
260
|
+
abortArtifactWait();
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
context.signal?.addEventListener("abort", abortArtifactWait, { once: true });
|
|
264
|
+
}
|
|
265
|
+
const artifactWaitPromise = waitForCompletionArtifact(tempDir, dispatch.workItem.id, decomposeDraft ? "decompose" : "draft", artifactWaitCancellation.signal);
|
|
266
|
+
let result;
|
|
267
|
+
let earlyProposalArtifact = null;
|
|
268
|
+
try {
|
|
269
|
+
const terminal = await Promise.race([
|
|
270
|
+
runPromise.then(value => ({ kind: "process", value })),
|
|
271
|
+
artifactWaitPromise.then(value => ({ kind: "artifact", value }))
|
|
272
|
+
]);
|
|
273
|
+
artifactWaitCancellation.abort();
|
|
274
|
+
await artifactWaitPromise.catch(() => undefined);
|
|
275
|
+
if (terminal.kind === "artifact") {
|
|
276
|
+
earlyProposalArtifact = terminal.value;
|
|
277
|
+
providerCancellation.abort();
|
|
278
|
+
result = await runPromise.catch(() => ({ exitCode: 0, stdout: "", stderr: "" }));
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
result = terminal.value;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
artifactWaitCancellation.abort();
|
|
286
|
+
context.signal?.removeEventListener("abort", abortArtifactWait);
|
|
287
|
+
}
|
|
288
|
+
if (result.exitCode !== 0 && !earlyProposalArtifact) {
|
|
289
|
+
throw new Error(`Provider CLI exited with code ${result.exitCode}${summarizeStderr(result.stderr)}.`);
|
|
290
|
+
}
|
|
291
|
+
await context.emit({
|
|
292
|
+
role: "runner",
|
|
293
|
+
kind: "checkpoint",
|
|
294
|
+
content: `Provider CLI Draft completed: ${command.command} exit ${result.exitCode}${outputState.sessionLabel ? ` ${outputState.sessionLabel}` : ""}.`,
|
|
295
|
+
eventType: "provider_cli_completed"
|
|
296
|
+
});
|
|
297
|
+
await emitProviderUsageEvent(context.emit, dispatch, outputState);
|
|
298
|
+
if (decomposeDraft) {
|
|
299
|
+
await emitDecomposeGateArtifacts(context.emit, tempDir, dispatch.workItem.id);
|
|
300
|
+
const proposalArtifact = earlyProposalArtifact ?? await readCompletionArtifact(tempDir, dispatch.workItem.id, "decompose");
|
|
301
|
+
if (!proposalArtifact) {
|
|
302
|
+
throw new Error("Provider CLI Decompose completed but did not call forge.complete_decompose_proposal.");
|
|
303
|
+
}
|
|
304
|
+
const proposal = parseJsonLine(proposalArtifact.verdictJson) ?? {};
|
|
305
|
+
const proposalJson = proposal["proposal_json"] ?? proposal["decomposition_json"];
|
|
306
|
+
if (!proposalJson || typeof proposalJson !== "object" || Array.isArray(proposalJson)) {
|
|
307
|
+
throw new Error("Provider CLI Decompose completed but decompose.proposal.json did not contain proposal_json.");
|
|
308
|
+
}
|
|
309
|
+
const json = JSON.stringify(proposalJson);
|
|
310
|
+
await context.emit({
|
|
311
|
+
role: "assistant",
|
|
312
|
+
kind: "message",
|
|
313
|
+
content: json,
|
|
314
|
+
eventType: "decompose_proposal"
|
|
315
|
+
});
|
|
316
|
+
return {
|
|
317
|
+
summary: "Decomposition proposal generated through Forge Decompose completion cycle.",
|
|
318
|
+
artifact: proposalArtifact,
|
|
319
|
+
artifacts: [proposalArtifact]
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
await emitDraftGateArtifacts(context.emit, tempDir, dispatch.workItem.id);
|
|
323
|
+
const proposalArtifact = earlyProposalArtifact ?? await readCompletionArtifact(tempDir, dispatch.workItem.id, "draft");
|
|
324
|
+
if (!proposalArtifact) {
|
|
325
|
+
throw new Error("Provider CLI Draft completed but did not call forge.complete_draft_proposal.");
|
|
326
|
+
}
|
|
327
|
+
const proposal = parseJsonLine(proposalArtifact.verdictJson) ?? {};
|
|
328
|
+
const draftJson = proposal["draft_json"];
|
|
329
|
+
if (!draftJson || typeof draftJson !== "object" || Array.isArray(draftJson)) {
|
|
330
|
+
throw new Error("Provider CLI Draft completed but draft.proposal.json did not contain draft_json.");
|
|
331
|
+
}
|
|
332
|
+
const json = JSON.stringify(draftJson);
|
|
333
|
+
await context.emit({
|
|
334
|
+
role: "assistant",
|
|
335
|
+
kind: "message",
|
|
336
|
+
content: json,
|
|
337
|
+
eventType: "draft_complete"
|
|
338
|
+
});
|
|
339
|
+
return {
|
|
340
|
+
summary: "Draft generated through Forge Draft completion cycle.",
|
|
341
|
+
artifact: proposalArtifact,
|
|
342
|
+
artifacts: [proposalArtifact]
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
finally {
|
|
346
|
+
context.signal?.removeEventListener("abort", abortProvider);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
await cleanupCompletionTools(completionTools, context.emit);
|
|
351
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
92
354
|
async runImplementation(context) {
|
|
93
355
|
const { dispatch, worktree } = context;
|
|
94
356
|
const phase = completionPhaseFromDispatch(dispatch);
|
|
@@ -164,6 +426,15 @@ export class ProcessBackedProviderCliService {
|
|
|
164
426
|
});
|
|
165
427
|
await emitProviderUsageEvent(context.emit, dispatch, outputState);
|
|
166
428
|
const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
|
|
429
|
+
if (developCycle || debugCycle || phase === "implementation") {
|
|
430
|
+
await this.git.run(worktree.worktreePath, ["push", "origin", `HEAD:${worktree.branchName}`]);
|
|
431
|
+
await context.emit({
|
|
432
|
+
role: "runner",
|
|
433
|
+
kind: "checkpoint",
|
|
434
|
+
content: `Pushed ${worktree.branchName} to origin after ${phase} completion.`,
|
|
435
|
+
eventType: "worktree_branch_pushed"
|
|
436
|
+
});
|
|
437
|
+
}
|
|
167
438
|
const completionArtifacts = await readCompletionArtifactsForRun(worktree.worktreePath, dispatch.workItem.id, phase, developCycle, debugCycle);
|
|
168
439
|
for (const artifact of completionArtifacts) {
|
|
169
440
|
if (!liveCompletionEvents.hasEmitted(completionPhaseFromArtifact(artifact))) {
|
|
@@ -204,6 +475,16 @@ export class ProcessBackedProviderCliService {
|
|
|
204
475
|
}
|
|
205
476
|
async runArchitect(context) {
|
|
206
477
|
const { dispatch, worktree } = context;
|
|
478
|
+
const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
|
|
479
|
+
await fs.mkdir(metadataRoot, { recursive: true });
|
|
480
|
+
const laneCommand = selectLaneCommand(dispatch.artifacts, dispatch.execution.lane, dispatch.cycleRun.phase);
|
|
481
|
+
if (!laneCommand) {
|
|
482
|
+
throw new Error(`Lane command for lane '${dispatch.execution.lane}' phase '${dispatch.cycleRun.phase}' was not resolved.`);
|
|
483
|
+
}
|
|
484
|
+
const specBody = buildSpecBody(laneCommand, dispatch.workItem, "architect");
|
|
485
|
+
const specPath = path.join(metadataRoot, "spec.md");
|
|
486
|
+
await fs.writeFile(specPath, specBody, "utf8");
|
|
487
|
+
await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "pending_approval", produced_by: "forge-lane-command" }) + "\n", "utf8");
|
|
207
488
|
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "architect" });
|
|
208
489
|
const command = buildProviderCliCommand(dispatch, {
|
|
209
490
|
worktreePath: worktree.worktreePath,
|
|
@@ -270,15 +551,13 @@ export class ProcessBackedProviderCliService {
|
|
|
270
551
|
content: outputState.finalMessage || `${dispatch.execution.provider} CLI produced an Architect plan for ${worktree.branchName}.`,
|
|
271
552
|
eventType: "architect_complete"
|
|
272
553
|
});
|
|
273
|
-
const specPath = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id, "spec.md");
|
|
274
|
-
const specBody = await fs.readFile(specPath, "utf8").catch(() => `# ${dispatch.workItem.title}\n\nArchitect plan pending approval.\n`);
|
|
275
554
|
return {
|
|
276
555
|
summary: `${dispatch.execution.provider} CLI Architect plan produced.`,
|
|
277
556
|
spec: {
|
|
278
557
|
repoPath: worktree.worktreePath,
|
|
279
558
|
path: specPath,
|
|
280
559
|
body: specBody,
|
|
281
|
-
producedBy:
|
|
560
|
+
producedBy: "forge-lane-command",
|
|
282
561
|
status: "pending_approval"
|
|
283
562
|
},
|
|
284
563
|
artifact: architectArtifact,
|
|
@@ -435,6 +714,17 @@ export class DeterministicProviderCliService {
|
|
|
435
714
|
constructor(git = new GitClient()) {
|
|
436
715
|
this.git = git;
|
|
437
716
|
}
|
|
717
|
+
async runDraft(context) {
|
|
718
|
+
const content = JSON.stringify(deterministicDraftPayload(context.dispatch));
|
|
719
|
+
await context.emit({
|
|
720
|
+
role: "assistant",
|
|
721
|
+
kind: "message",
|
|
722
|
+
content,
|
|
723
|
+
eventType: "draft_complete"
|
|
724
|
+
});
|
|
725
|
+
await context.emit(buildProviderUsageEvent(context.dispatch, estimatedDeterministicUsage()));
|
|
726
|
+
return { summary: "forge-bridge generated deterministic draft." };
|
|
727
|
+
}
|
|
438
728
|
async runImplementation(context) {
|
|
439
729
|
const { dispatch, worktree } = context;
|
|
440
730
|
const phase = completionPhaseFromDispatch(dispatch);
|
|
@@ -1741,6 +2031,283 @@ function isFakeWorkerModel(model) {
|
|
|
1741
2031
|
function sha256(content) {
|
|
1742
2032
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
1743
2033
|
}
|
|
2034
|
+
function buildDraftSpec(dispatch) {
|
|
2035
|
+
return `# Draft Generation Context
|
|
2036
|
+
|
|
2037
|
+
Project: ${dispatch.draft?.projectName ?? dispatch.repo.name}
|
|
2038
|
+
Repository: ${dispatch.draft?.repoName ?? dispatch.repo.name}
|
|
2039
|
+
Draft kind: ${dispatch.draft?.kind ?? "draft"}
|
|
2040
|
+
|
|
2041
|
+
## User Prompt
|
|
2042
|
+
|
|
2043
|
+
${dispatch.draft?.userPrompt ?? ""}
|
|
2044
|
+
`;
|
|
2045
|
+
}
|
|
2046
|
+
async function emitDraftGateArtifacts(emit, worktreePath, workItemId) {
|
|
2047
|
+
const draftArtifactFiles = {
|
|
2048
|
+
draft_intent: "draft.intent.json",
|
|
2049
|
+
draft_repo_context: "draft.repo-context.json",
|
|
2050
|
+
draft_graph_context: "draft.graph-context.json",
|
|
2051
|
+
draft_product_scope: "draft.product-scope.json",
|
|
2052
|
+
draft_work_order_plan: "draft.work-order-plan.json",
|
|
2053
|
+
draft_critic: "draft.critic.json",
|
|
2054
|
+
draft_proposal: "draft.proposal.json"
|
|
2055
|
+
};
|
|
2056
|
+
for (const gate of draftCompletionGateOrder) {
|
|
2057
|
+
const actualPath = path.join(worktreePath, ".forge", "work-items", workItemId, "artifacts", draftArtifactFiles[gate]);
|
|
2058
|
+
if (!await fileExists(actualPath)) {
|
|
2059
|
+
continue;
|
|
2060
|
+
}
|
|
2061
|
+
const relativePath = path.relative(worktreePath, actualPath).split(path.sep).join("/");
|
|
2062
|
+
await emit({
|
|
2063
|
+
role: "tool",
|
|
2064
|
+
kind: "tool_result",
|
|
2065
|
+
content: `Forge Draft completion tool recorded ${gate} artifact ${relativePath}.`,
|
|
2066
|
+
eventType: "completion_tool_artifact_recorded",
|
|
2067
|
+
data: JSON.stringify({ phase: "draft", draft_gate: gate, artifact_relative_paths: [relativePath] })
|
|
2068
|
+
});
|
|
2069
|
+
await emit({
|
|
2070
|
+
role: "hook",
|
|
2071
|
+
kind: "checkpoint",
|
|
2072
|
+
content: `phase_gate: ${gate} artifact accepted with status pass.`,
|
|
2073
|
+
eventType: "phase_gate"
|
|
2074
|
+
});
|
|
2075
|
+
const next = draftCompletionGateOrder[draftCompletionGateOrder.indexOf(gate) + 1];
|
|
2076
|
+
if (next) {
|
|
2077
|
+
await emit({
|
|
2078
|
+
role: "hook",
|
|
2079
|
+
kind: "checkpoint",
|
|
2080
|
+
content: `phase_progression: ${gate} -> ${next}`,
|
|
2081
|
+
eventType: "phase_gate"
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
async function emitDecomposeGateArtifacts(emit, worktreePath, workItemId) {
|
|
2087
|
+
const decomposeArtifactFiles = {
|
|
2088
|
+
decompose_subject_intake: "decompose.subject-intake.json",
|
|
2089
|
+
decompose_context_review: "decompose.context-review.json",
|
|
2090
|
+
decompose_breakdown_strategy: "decompose.breakdown-strategy.json",
|
|
2091
|
+
decompose_child_items: "decompose.child-items.json",
|
|
2092
|
+
decompose_dependency_graph: "decompose.dependency-graph.json",
|
|
2093
|
+
decompose_critic: "decompose.critic.json",
|
|
2094
|
+
decompose_proposal: "decompose.proposal.json"
|
|
2095
|
+
};
|
|
2096
|
+
const gates = Object.keys(decomposeArtifactFiles);
|
|
2097
|
+
for (const gate of gates) {
|
|
2098
|
+
const actualPath = path.join(worktreePath, ".forge", "work-items", workItemId, "artifacts", decomposeArtifactFiles[gate]);
|
|
2099
|
+
if (!await fileExists(actualPath)) {
|
|
2100
|
+
continue;
|
|
2101
|
+
}
|
|
2102
|
+
const relativePath = path.relative(worktreePath, actualPath).split(path.sep).join("/");
|
|
2103
|
+
await emit({
|
|
2104
|
+
role: "tool",
|
|
2105
|
+
kind: "tool_result",
|
|
2106
|
+
content: `Forge Decompose completion tool recorded ${gate} artifact ${relativePath}.`,
|
|
2107
|
+
eventType: "completion_tool_artifact_recorded",
|
|
2108
|
+
data: JSON.stringify({ phase: "decompose", decompose_gate: gate, artifact_relative_paths: [relativePath] })
|
|
2109
|
+
});
|
|
2110
|
+
await emit({
|
|
2111
|
+
role: "hook",
|
|
2112
|
+
kind: "checkpoint",
|
|
2113
|
+
content: `phase_gate: ${gate} artifact accepted with status pass.`,
|
|
2114
|
+
eventType: "phase_gate"
|
|
2115
|
+
});
|
|
2116
|
+
const next = gates[gates.indexOf(gate) + 1];
|
|
2117
|
+
if (next) {
|
|
2118
|
+
await emit({
|
|
2119
|
+
role: "hook",
|
|
2120
|
+
kind: "checkpoint",
|
|
2121
|
+
content: `phase_progression: ${gate} -> ${next}`,
|
|
2122
|
+
eventType: "phase_gate"
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
async function fileExists(filePath) {
|
|
2128
|
+
try {
|
|
2129
|
+
await fs.access(filePath);
|
|
2130
|
+
return true;
|
|
2131
|
+
}
|
|
2132
|
+
catch {
|
|
2133
|
+
return false;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
function buildDraftPrompt(dispatch) {
|
|
2137
|
+
const draft = dispatch.draft;
|
|
2138
|
+
if (!draft) {
|
|
2139
|
+
throw new Error("Draft prompt is missing.");
|
|
2140
|
+
}
|
|
2141
|
+
const decomposeDraft = isDecomposeDraftDispatch(dispatch);
|
|
2142
|
+
const draftArtifacts = dispatch.artifacts
|
|
2143
|
+
.filter((artifact) => artifact.scope === "operational" && ["command", "skill", "hook"].includes(artifact.kind))
|
|
2144
|
+
.map((artifact) => `## ${artifact.kind}: ${artifact.slug}\n${artifact.body.trim()}`)
|
|
2145
|
+
.join("\n\n");
|
|
2146
|
+
const artifactSection = draftArtifacts
|
|
2147
|
+
? `\n\n# Injected Draft command/skills/hooks\n\n${draftArtifacts}`
|
|
2148
|
+
: "";
|
|
2149
|
+
const projectSection = [
|
|
2150
|
+
draft.projectName ? `Project: ${draft.projectName}` : null,
|
|
2151
|
+
draft.repoName ? `Repository: ${draft.repoName}` : null
|
|
2152
|
+
].filter(Boolean).join("\n");
|
|
2153
|
+
const scopedProjectSection = projectSection ? `\n\n# Selected project context\n\n${projectSection}` : "";
|
|
2154
|
+
return `${draft.systemPrompt}${scopedProjectSection}${artifactSection}
|
|
2155
|
+
|
|
2156
|
+
Draft kind: ${draft.kind}
|
|
2157
|
+
|
|
2158
|
+
${draft.userPrompt}
|
|
2159
|
+
|
|
2160
|
+
${decomposeDraft
|
|
2161
|
+
? `Use the injected command/skills/hooks as lane instructions. You must call each Forge Decompose MCP completion tool in order. Do not claim Decompose is complete until forge.complete_decompose_proposal accepts the final proposal_json.
|
|
2162
|
+
|
|
2163
|
+
Complete these gates in order: decompose_subject_intake -> decompose_context_review -> decompose_breakdown_strategy -> decompose_child_items -> decompose_dependency_graph -> decompose_critic -> decompose_proposal.
|
|
2164
|
+
|
|
2165
|
+
The final proposal_json object must contain summary, optional blockedReason, and candidates.`
|
|
2166
|
+
: `${completionPromptForDraftCycle("draft_intent")}
|
|
2167
|
+
|
|
2168
|
+
Use the injected command/skills/hooks as lane instructions. You must call each Forge Draft MCP completion tool in order. Do not claim the Draft is complete until forge.complete_draft_proposal accepts the final draft_json.`}
|
|
2169
|
+
|
|
2170
|
+
Do not modify source files.`;
|
|
2171
|
+
}
|
|
2172
|
+
function isDecomposeDraftKind(kind) {
|
|
2173
|
+
return kind === "work_order_decomposition" || kind === "feature_decomposition";
|
|
2174
|
+
}
|
|
2175
|
+
function isDecomposeDraftDispatch(dispatch) {
|
|
2176
|
+
return dispatch.execution.lane.trim().toLowerCase() === "decompose"
|
|
2177
|
+
|| dispatch.session.lane.trim().toLowerCase() === "decompose"
|
|
2178
|
+
|| isDecomposeDraftKind(dispatch.draft?.kind?.trim().toLowerCase());
|
|
2179
|
+
}
|
|
2180
|
+
function summarizeStderr(stderr) {
|
|
2181
|
+
const trimmed = stderr.trim();
|
|
2182
|
+
return trimmed ? `: ${clip(trimmed, 1000)}` : "";
|
|
2183
|
+
}
|
|
2184
|
+
async function waitForCompletionArtifact(worktreePath, workItemId, phase, signal) {
|
|
2185
|
+
while (!signal?.aborted) {
|
|
2186
|
+
const artifact = await readCompletionArtifact(worktreePath, workItemId, phase);
|
|
2187
|
+
if (artifact) {
|
|
2188
|
+
return artifact;
|
|
2189
|
+
}
|
|
2190
|
+
await delay(1000, signal);
|
|
2191
|
+
}
|
|
2192
|
+
throw new Error(`${phase} proposal artifact wait was cancelled.`);
|
|
2193
|
+
}
|
|
2194
|
+
async function delay(ms, signal) {
|
|
2195
|
+
if (!signal) {
|
|
2196
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2199
|
+
const abortSignal = signal;
|
|
2200
|
+
if (abortSignal.aborted) {
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
await new Promise((resolve) => {
|
|
2204
|
+
const timeout = setTimeout(done, ms);
|
|
2205
|
+
const abort = () => done();
|
|
2206
|
+
function done() {
|
|
2207
|
+
clearTimeout(timeout);
|
|
2208
|
+
abortSignal.removeEventListener("abort", abort);
|
|
2209
|
+
resolve();
|
|
2210
|
+
}
|
|
2211
|
+
abortSignal.addEventListener("abort", abort, { once: true });
|
|
2212
|
+
if (abortSignal.aborted) {
|
|
2213
|
+
done();
|
|
2214
|
+
}
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2217
|
+
function deterministicDraftPayload(dispatch) {
|
|
2218
|
+
const kind = dispatch.draft?.kind ?? "work_item";
|
|
2219
|
+
if (kind === "work_order_decomposition") {
|
|
2220
|
+
return {
|
|
2221
|
+
summary: "Fake decompose proposed a Feature DAG for the Work Order.",
|
|
2222
|
+
blockedReason: "",
|
|
2223
|
+
candidates: [
|
|
2224
|
+
{
|
|
2225
|
+
origin: "agent",
|
|
2226
|
+
key: "intake-submission",
|
|
2227
|
+
title: "Customer intake submission",
|
|
2228
|
+
summary: "Collect, validate, de-duplicate, and confirm incoming customer requests.",
|
|
2229
|
+
planningPacket: "# Customer intake submission\n\nPlan Stories for request form validation, attachment support, duplicate prevention, and submission confirmation.",
|
|
2230
|
+
sortOrder: 10,
|
|
2231
|
+
dependsOn: []
|
|
2232
|
+
},
|
|
2233
|
+
{
|
|
2234
|
+
origin: "agent",
|
|
2235
|
+
key: "review-queue",
|
|
2236
|
+
title: "Reviewer triage queue",
|
|
2237
|
+
summary: "Provide reviewer triage, assignment, notes, status transitions, and audit trail.",
|
|
2238
|
+
planningPacket: "# Reviewer triage queue\n\nPlan Stories for queue filtering, assignment, reviewer notes, status changes, and immutable audit trail.",
|
|
2239
|
+
sortOrder: 20,
|
|
2240
|
+
dependsOn: ["intake-submission"]
|
|
2241
|
+
}
|
|
2242
|
+
]
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
2245
|
+
if (kind === "feature_decomposition") {
|
|
2246
|
+
return {
|
|
2247
|
+
summary: "Fake decompose proposed a Story DAG for the Feature.",
|
|
2248
|
+
blockedReason: "",
|
|
2249
|
+
candidates: [
|
|
2250
|
+
{
|
|
2251
|
+
origin: "agent",
|
|
2252
|
+
key: "foundation",
|
|
2253
|
+
title: "Build the feature foundation",
|
|
2254
|
+
description: "Create the persistence, API, and UI shell needed by the feature.",
|
|
2255
|
+
acceptanceCriteria: "Scenario: Foundation exists\nGiven the Feature is selected\nWhen the foundation is delivered\nThen downstream Stories can build on stable contracts",
|
|
2256
|
+
points: 3,
|
|
2257
|
+
sortOrder: 10,
|
|
2258
|
+
dependsOn: []
|
|
2259
|
+
},
|
|
2260
|
+
{
|
|
2261
|
+
origin: "agent",
|
|
2262
|
+
key: "workflow-behavior",
|
|
2263
|
+
title: "Implement the primary workflow behavior",
|
|
2264
|
+
description: "Implement the main user-visible behavior for the feature after the foundation is ready.",
|
|
2265
|
+
acceptanceCriteria: "Scenario: Workflow behavior works\nGiven the foundation Story is complete\nWhen the user performs the primary workflow\nThen the expected state change and feedback are visible",
|
|
2266
|
+
points: 5,
|
|
2267
|
+
sortOrder: 20,
|
|
2268
|
+
dependsOn: ["foundation"]
|
|
2269
|
+
},
|
|
2270
|
+
{
|
|
2271
|
+
origin: "agent",
|
|
2272
|
+
key: "audit-and-tests",
|
|
2273
|
+
title: "Add audit coverage and regression tests",
|
|
2274
|
+
description: "Add audit/evidence behavior and automated regression coverage for the feature.",
|
|
2275
|
+
acceptanceCriteria: "Scenario: Feature is verifiable\nGiven the workflow behavior is complete\nWhen validation runs\nThen audit evidence and tests cover the expected outcomes",
|
|
2276
|
+
points: 3,
|
|
2277
|
+
sortOrder: 30,
|
|
2278
|
+
dependsOn: ["workflow-behavior"]
|
|
2279
|
+
}
|
|
2280
|
+
]
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
if (kind === "work_order") {
|
|
2284
|
+
return {
|
|
2285
|
+
title: "Deterministic runner Work Order draft",
|
|
2286
|
+
summary: "Forge Runner generated this deterministic Work Order draft.",
|
|
2287
|
+
objective: "Validate runner-backed Draft generation.",
|
|
2288
|
+
planningPacket: "Runner-backed draft generation returned a Work Order JSON payload.",
|
|
2289
|
+
decompositionContext: "Use this deterministic fixture only in bridge tests.",
|
|
2290
|
+
assumptions: ["Forge Runner dispatch is available."],
|
|
2291
|
+
warnings: []
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
if (kind === "story") {
|
|
2295
|
+
return {
|
|
2296
|
+
title: "Deterministic runner Story draft",
|
|
2297
|
+
description: "Forge Runner generated this deterministic Story draft.",
|
|
2298
|
+
acceptanceCriteria: "Given runner-backed Draft generation, when Forge requests a Story draft, then JSON is returned.",
|
|
2299
|
+
points: 3
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
return {
|
|
2303
|
+
title: "Deterministic runner Work Item draft",
|
|
2304
|
+
description: "Forge Runner generated this deterministic Work Item draft.",
|
|
2305
|
+
acceptanceCriteria: "Given runner-backed Draft generation, when Forge requests a draft, then JSON is returned.",
|
|
2306
|
+
reproSteps: "Run Draft with a Forge Runner provider selected.",
|
|
2307
|
+
systemInfo: "forge-bridge deterministic provider",
|
|
2308
|
+
points: 1
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
1744
2311
|
export function formatProviderCliCommand(command) {
|
|
1745
2312
|
return [command.command, ...command.args].map(singleLine).join(" ");
|
|
1746
2313
|
}
|
|
@@ -1752,7 +2319,7 @@ export function buildProviderCliCommand(dispatch, run) {
|
|
|
1752
2319
|
const normalizedProvider = provider.toLowerCase();
|
|
1753
2320
|
const model = dispatch.execution.model.trim();
|
|
1754
2321
|
if (normalizedProvider === "codex") {
|
|
1755
|
-
const args = run ? ["-s",
|
|
2322
|
+
const args = run ? ["-s", codexSandboxModeForPlatform(), "-a", "never", "exec"] : ["exec"];
|
|
1756
2323
|
if (run?.completionTools) {
|
|
1757
2324
|
args.push(...codexMcpConfigArgs(run.completionTools));
|
|
1758
2325
|
}
|
|
@@ -1762,6 +2329,9 @@ export function buildProviderCliCommand(dispatch, run) {
|
|
|
1762
2329
|
if (run.worktreePath) {
|
|
1763
2330
|
args.push("--cd", run.worktreePath);
|
|
1764
2331
|
}
|
|
2332
|
+
if (run.skipGitRepoCheck) {
|
|
2333
|
+
args.push("--skip-git-repo-check");
|
|
2334
|
+
}
|
|
1765
2335
|
args.push(run.prompt ?? "");
|
|
1766
2336
|
}
|
|
1767
2337
|
return { command: "codex", args };
|
|
@@ -1782,6 +2352,9 @@ export function buildProviderCliCommand(dispatch, run) {
|
|
|
1782
2352
|
}
|
|
1783
2353
|
return { command: provider, args };
|
|
1784
2354
|
}
|
|
2355
|
+
export function codexSandboxModeForPlatform(platform = process.platform) {
|
|
2356
|
+
return platform === "win32" ? "danger-full-access" : "workspace-write";
|
|
2357
|
+
}
|
|
1785
2358
|
function buildArchitectPrompt(dispatch) {
|
|
1786
2359
|
return `You are Forge's Architect lane running locally in a prepared git worktree.
|
|
1787
2360
|
|
|
@@ -1789,6 +2362,7 @@ Work item: ${dispatch.workItem.id} — ${dispatch.workItem.title}
|
|
|
1789
2362
|
|
|
1790
2363
|
Plan only. Do not implement source changes, do not create Develop phase artifacts, and do not mark the spec approved.
|
|
1791
2364
|
Read injected Forge context under .forge plus the repository shape, then produce a repo-grounded implementation DAG with task tests, phase gates, final QA coverage, and a pending-approval spec.
|
|
2365
|
+
Implementation DAG tasks are Develop child tasks only: include focused repo/product implementation changes. Do not create child Develop tasks for parent-story QA, sign-off, materialized child inventory, child-count evidence, parent plan artifact checks, or final integrated acceptance proof; put those checks in qa_plan.final_qa.
|
|
1792
2366
|
${architectRevisionInstruction(dispatch)}
|
|
1793
2367
|
Forge owns protocol files: write Architect artifacts only through the completion MCP tool.
|
|
1794
2368
|
|
|
@@ -1895,18 +2469,27 @@ function phaseWorkInstruction(phase) {
|
|
|
1895
2469
|
}
|
|
1896
2470
|
function qaBrowserInstruction(dispatch) {
|
|
1897
2471
|
const qaBrowser = dispatch.qaBrowser;
|
|
2472
|
+
const qaScope = dispatch.qaScope;
|
|
1898
2473
|
if (!qaBrowser) {
|
|
1899
2474
|
return "Browser/app launch context: not provided by Forge.";
|
|
1900
2475
|
}
|
|
2476
|
+
const scopeAuthority = qaScope
|
|
2477
|
+
? qaScope.source === "heuristic"
|
|
2478
|
+
? "Forge's transient heuristic fallback"
|
|
2479
|
+
: "persisted QA scope"
|
|
2480
|
+
: "Forge heuristics";
|
|
2481
|
+
const scopeEvidence = qaScope
|
|
2482
|
+
? `\n- QA scope required evidence: ${qaScope.required_evidence.length > 0 ? qaScope.required_evidence.join(", ") : "none"}\n- QA scope not applicable: ${qaScope.not_applicable.length > 0 ? qaScope.not_applicable.join(", ") : "none"}${qaScope.rationale ? `\n- QA scope rationale: ${qaScope.rationale}` : ""}`
|
|
2483
|
+
: "";
|
|
1901
2484
|
const launchTarget = qaBrowser.launchTarget;
|
|
1902
2485
|
if (qaBrowser.required && !hasLaunchTarget(launchTarget ?? null)) {
|
|
1903
|
-
return `Browser/app launch context:\n- UI/browser QA is required for this item.\n- No app launch target was provided.\n- Return blocked with blocker_reason: missing_app_launch_target instead of pretending browser testing occurred
|
|
2486
|
+
return `Browser/app launch context:\n- UI/browser QA is required for this item by ${scopeAuthority}.\n- No app launch target was provided.\n- Return blocked with blocker_reason: missing_app_launch_target instead of pretending browser testing occurred.${scopeEvidence}\n- Artifact sidecars path when available: ${qaBrowser.artifactsPath || `.forge/work-items/${dispatch.workItem.id}/artifacts/qa`}`;
|
|
1904
2487
|
}
|
|
1905
2488
|
if (!launchTarget) {
|
|
1906
|
-
return `Browser/app launch context:\n- UI/browser QA is not required by
|
|
2489
|
+
return `Browser/app launch context:\n- UI/browser QA is not required by ${scopeAuthority} for this item.\n- If you determine browser testing is needed, return blocked with blocker_reason: missing_app_launch_target unless you can safely identify a launch target from the repo.${scopeEvidence}\n- Browser sidecars path: ${qaBrowser.artifactsPath || `.forge/work-items/${dispatch.workItem.id}/artifacts/qa`}`;
|
|
1907
2490
|
}
|
|
1908
2491
|
const routes = launchTarget.routes.length > 0 ? launchTarget.routes.join(", ") : "not specified";
|
|
1909
|
-
return `Browser/app launch context:\n- Browser QA required: ${qaBrowser.required ?
|
|
2492
|
+
return `Browser/app launch context:\n- Browser QA required: ${qaBrowser.required ? `yes, by ${scopeAuthority}` : `no by ${scopeAuthority}, unless the diff/AC requires it`}\n- URL: ${launchTarget.url || "not specified"}\n- Ready URL: ${launchTarget.ready_url || "not specified"}\n- Launch command: ${launchTarget.command || "not specified"}\n- Working directory: ${launchTarget.working_directory || "repo root"}\n- Routes to cover: ${routes}\n- Notes: ${launchTarget.notes || "none"}${scopeEvidence}\n- Browser sidecars path: ${qaBrowser.artifactsPath || `.forge/work-items/${dispatch.workItem.id}/artifacts/qa`}`;
|
|
1910
2493
|
}
|
|
1911
2494
|
function storyQaInstruction(dispatch) {
|
|
1912
2495
|
const storyQa = dispatch.storyQa;
|
|
@@ -2094,11 +2677,11 @@ class CursorStreamJsonState {
|
|
|
2094
2677
|
if (type === "assistant") {
|
|
2095
2678
|
const text = assistantMessageText(event);
|
|
2096
2679
|
if (text) {
|
|
2097
|
-
this.finalMessage =
|
|
2680
|
+
this.finalMessage = text;
|
|
2098
2681
|
return [{
|
|
2099
2682
|
role: "assistant",
|
|
2100
2683
|
kind: "message",
|
|
2101
|
-
content:
|
|
2684
|
+
content: clip(text, 4000),
|
|
2102
2685
|
eventType: "provider_message"
|
|
2103
2686
|
}];
|
|
2104
2687
|
}
|
|
@@ -2132,7 +2715,7 @@ class CursorStreamJsonState {
|
|
|
2132
2715
|
if (type === "result" && stringField(event, "subtype") === "success") {
|
|
2133
2716
|
const result = stringField(event, "result");
|
|
2134
2717
|
if (result) {
|
|
2135
|
-
this.finalMessage =
|
|
2718
|
+
this.finalMessage = result;
|
|
2136
2719
|
}
|
|
2137
2720
|
this.accumulateUsage(event);
|
|
2138
2721
|
}
|
|
@@ -2313,11 +2896,11 @@ class CodexJsonState {
|
|
|
2313
2896
|
if (itemType === "agent_message") {
|
|
2314
2897
|
const text = stringField(item, "text");
|
|
2315
2898
|
if (text) {
|
|
2316
|
-
this.finalMessage =
|
|
2899
|
+
this.finalMessage = text;
|
|
2317
2900
|
return [{
|
|
2318
2901
|
role: "assistant",
|
|
2319
2902
|
kind: "message",
|
|
2320
|
-
content:
|
|
2903
|
+
content: clip(text, 4000),
|
|
2321
2904
|
eventType: "provider_message"
|
|
2322
2905
|
}];
|
|
2323
2906
|
}
|