@loucompanion/forge-bridge 0.1.1-dev.cada6731ecc7 → 0.1.1-dev.d71662695396
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/session/phase-completion.d.ts +12 -1
- package/dist/session/phase-completion.js +482 -22
- package/dist/session/provider-cli/provider-cli.service.d.ts +7 -0
- package/dist/session/provider-cli/provider-cli.service.js +296 -24
- package/dist/session/session.types.d.ts +4 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.js +39 -0
- package/package.json +1 -1
|
@@ -25,6 +25,12 @@ export interface ProviderProcessHandlers {
|
|
|
25
25
|
onStdoutLine?(line: string): Promise<void> | void;
|
|
26
26
|
onStderrLine?(line: string): Promise<void> | void;
|
|
27
27
|
}
|
|
28
|
+
interface ProviderChildEnvironmentOptions {
|
|
29
|
+
readonly parentEnv?: NodeJS.ProcessEnv;
|
|
30
|
+
readonly platform?: NodeJS.Platform;
|
|
31
|
+
readonly homeDir?: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function buildProviderChildEnvironment(requestEnv?: Readonly<Record<string, string>>, options?: ProviderChildEnvironmentOptions): NodeJS.ProcessEnv;
|
|
28
34
|
export declare class RoutingProviderCliService implements ProviderCliService {
|
|
29
35
|
private readonly realProvider;
|
|
30
36
|
private readonly deterministicProvider;
|
|
@@ -67,3 +73,4 @@ export declare function buildProviderCliCommand(dispatch: RunOnceDispatchPacket,
|
|
|
67
73
|
readonly skipGitRepoCheck?: boolean;
|
|
68
74
|
}): ProviderCliCommand;
|
|
69
75
|
export declare function codexSandboxModeForPlatform(platform?: NodeJS.Platform): "workspace-write" | "danger-full-access";
|
|
76
|
+
export {};
|
|
@@ -9,7 +9,7 @@ import { GitClient } from "../../shared/git/git.js";
|
|
|
9
9
|
import { resolveCursorAgentDirectLaunch } from "../../shared/cursor-agent/cursor-agent.launch.js";
|
|
10
10
|
import { ensureNoSymbolicLinkPathSegments, isPathUnderRoot, sha256File } from "../qa-sidecar-validation.js";
|
|
11
11
|
import { buildSpecBody, selectLaneCommand } from "../../worktree/artifact-injector.js";
|
|
12
|
-
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";
|
|
13
13
|
import { addUsage, buildProviderUsageEvent, emptyUsage, hasUsage, readUsageNumber } from "./provider-usage.js";
|
|
14
14
|
const fakeWorkerModel = "server-local-fake-worker";
|
|
15
15
|
const execFileAsync = promisify(execFile);
|
|
@@ -25,15 +25,31 @@ const allowedProviderParentEnvNames = [
|
|
|
25
25
|
];
|
|
26
26
|
const windowsAllowedProviderParentEnvNames = [
|
|
27
27
|
"SystemRoot",
|
|
28
|
+
"SystemDrive",
|
|
28
29
|
"ComSpec",
|
|
29
30
|
"PATHEXT",
|
|
30
31
|
"APPDATA",
|
|
31
|
-
"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"
|
|
32
46
|
];
|
|
33
|
-
function buildProviderChildEnvironment(requestEnv) {
|
|
47
|
+
export function buildProviderChildEnvironment(requestEnv, options = {}) {
|
|
48
|
+
const parentEnv = options.parentEnv ?? process.env;
|
|
49
|
+
const platform = options.platform ?? process.platform;
|
|
34
50
|
const env = {};
|
|
35
|
-
for (const [key, value] of Object.entries(
|
|
36
|
-
if (typeof value === "string" && isAllowedProviderParentEnv(key)) {
|
|
51
|
+
for (const [key, value] of Object.entries(parentEnv)) {
|
|
52
|
+
if (typeof value === "string" && isAllowedProviderParentEnv(key, platform)) {
|
|
37
53
|
env[key] = value;
|
|
38
54
|
}
|
|
39
55
|
}
|
|
@@ -45,16 +61,88 @@ function buildProviderChildEnvironment(requestEnv) {
|
|
|
45
61
|
}
|
|
46
62
|
}
|
|
47
63
|
}
|
|
64
|
+
if (platform === "win32") {
|
|
65
|
+
normalizeWindowsDotNetEnvironment(env, parentEnv, options.homeDir ?? os.homedir());
|
|
66
|
+
}
|
|
48
67
|
return env;
|
|
49
68
|
}
|
|
50
|
-
function isAllowedProviderParentEnv(key) {
|
|
69
|
+
function isAllowedProviderParentEnv(key, platform = process.platform) {
|
|
51
70
|
return isAllowedProviderParentEnvName(key, allowedProviderParentEnvNames)
|
|
52
71
|
|| key.toUpperCase().startsWith("LC_")
|
|
53
|
-
|| (
|
|
72
|
+
|| (platform === "win32" && isAllowedProviderParentEnvName(key, windowsAllowedProviderParentEnvNames));
|
|
54
73
|
}
|
|
55
74
|
function isAllowedProviderParentEnvName(key, allowedNames) {
|
|
56
75
|
return allowedNames.some((allowed) => allowed.toLowerCase() === key.toLowerCase());
|
|
57
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
|
+
}
|
|
58
146
|
export class RoutingProviderCliService {
|
|
59
147
|
realProvider;
|
|
60
148
|
deterministicProvider;
|
|
@@ -101,20 +189,61 @@ export class ProcessBackedProviderCliService {
|
|
|
101
189
|
throw new Error("Draft dispatch context is missing.");
|
|
102
190
|
}
|
|
103
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 isDecomposePrompt = isDecomposeDraftKind(dispatch.draft.kind);
|
|
204
|
+
const completionTools = isDecomposePrompt
|
|
205
|
+
? null
|
|
206
|
+
: await prepareCompletionToolRuntime({ dispatch, worktree: draftWorktree, phase: "draft" });
|
|
104
207
|
const command = buildProviderCliCommand(dispatch, {
|
|
105
208
|
worktreePath: tempDir,
|
|
106
209
|
prompt: buildDraftPrompt(dispatch),
|
|
210
|
+
completionTools: completionTools ?? undefined,
|
|
107
211
|
skipGitRepoCheck: true
|
|
108
212
|
});
|
|
213
|
+
const draftArtifactSlugs = dispatch.artifacts
|
|
214
|
+
.filter((artifact) => artifact.scope === "operational" && ["command", "skill", "hook"].includes(artifact.kind))
|
|
215
|
+
.map((artifact) => `${artifact.kind}:${artifact.slug}`);
|
|
216
|
+
await context.emit({
|
|
217
|
+
role: "runner",
|
|
218
|
+
kind: "checkpoint",
|
|
219
|
+
content: `${isDecomposePrompt ? "Structured prompt" : "Draft"} artifacts injected: ${draftArtifactSlugs.length}${draftArtifactSlugs.length ? ` (${draftArtifactSlugs.join(", ")})` : ""}. Project: ${dispatch.draft.projectName ?? dispatch.repo.name}. Repo: ${dispatch.draft.repoName ?? dispatch.repo.name}.`,
|
|
220
|
+
eventType: isDecomposePrompt ? "structured_prompt_artifacts_injected" : "draft_artifacts_injected"
|
|
221
|
+
});
|
|
109
222
|
await context.emit({
|
|
110
223
|
role: "runner",
|
|
111
224
|
kind: "checkpoint",
|
|
112
|
-
content: `Starting provider CLI Draft: ${formatProviderCliCommand(command)}`,
|
|
225
|
+
content: `Starting provider CLI ${isDecomposePrompt ? "structured prompt" : "Draft"}: ${formatProviderCliCommand(command)}`,
|
|
113
226
|
eventType: "provider_cli_started"
|
|
114
227
|
});
|
|
228
|
+
if (completionTools) {
|
|
229
|
+
await context.emit({
|
|
230
|
+
role: "runner",
|
|
231
|
+
kind: "checkpoint",
|
|
232
|
+
content: "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.",
|
|
233
|
+
eventType: "completion_tools_configured"
|
|
234
|
+
});
|
|
235
|
+
}
|
|
115
236
|
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
116
237
|
try {
|
|
117
|
-
const
|
|
238
|
+
const providerCancellation = new AbortController();
|
|
239
|
+
const abortProvider = () => providerCancellation.abort();
|
|
240
|
+
if (context.signal?.aborted) {
|
|
241
|
+
abortProvider();
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
context.signal?.addEventListener("abort", abortProvider, { once: true });
|
|
245
|
+
}
|
|
246
|
+
const runPromise = this.processRunner.run({
|
|
118
247
|
command: command.command,
|
|
119
248
|
args: command.args,
|
|
120
249
|
cwd: tempDir,
|
|
@@ -125,27 +254,79 @@ export class ProcessBackedProviderCliService {
|
|
|
125
254
|
await context.emit(event);
|
|
126
255
|
}
|
|
127
256
|
}
|
|
128
|
-
},
|
|
129
|
-
|
|
257
|
+
}, providerCancellation.signal);
|
|
258
|
+
let result;
|
|
259
|
+
let earlyProposalArtifact = null;
|
|
260
|
+
if (completionTools && !isDecomposePrompt) {
|
|
261
|
+
const terminal = await Promise.race([
|
|
262
|
+
runPromise.then(value => ({ kind: "process", value })),
|
|
263
|
+
waitForDraftProposalArtifact(tempDir, dispatch.workItem.id, context.signal).then(value => ({ kind: "artifact", value }))
|
|
264
|
+
]);
|
|
265
|
+
if (terminal.kind === "artifact") {
|
|
266
|
+
earlyProposalArtifact = terminal.value;
|
|
267
|
+
providerCancellation.abort();
|
|
268
|
+
result = await runPromise.catch(() => ({ exitCode: 0, stdout: "", stderr: "" }));
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
result = terminal.value;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
result = await runPromise;
|
|
276
|
+
}
|
|
277
|
+
context.signal?.removeEventListener("abort", abortProvider);
|
|
278
|
+
if (result.exitCode !== 0 && !earlyProposalArtifact) {
|
|
130
279
|
throw new Error(`Provider CLI exited with code ${result.exitCode}${summarizeStderr(result.stderr)}.`);
|
|
131
280
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
281
|
+
await context.emit({
|
|
282
|
+
role: "runner",
|
|
283
|
+
kind: "checkpoint",
|
|
284
|
+
content: `Provider CLI ${isDecomposePrompt ? "structured prompt" : "Draft"} completed: ${command.command} exit ${result.exitCode}${outputState.sessionLabel ? ` ${outputState.sessionLabel}` : ""}.`,
|
|
285
|
+
eventType: "provider_cli_completed"
|
|
286
|
+
});
|
|
287
|
+
await emitProviderUsageEvent(context.emit, dispatch, outputState);
|
|
288
|
+
if (isDecomposePrompt) {
|
|
289
|
+
const structuredJson = parseJsonLine(outputState.finalMessage ?? "");
|
|
290
|
+
if (!structuredJson || typeof structuredJson !== "object" || Array.isArray(structuredJson)) {
|
|
291
|
+
throw new Error("Provider CLI structured prompt completed but did not return a JSON object.");
|
|
292
|
+
}
|
|
293
|
+
await context.emit({
|
|
294
|
+
role: "assistant",
|
|
295
|
+
kind: "message",
|
|
296
|
+
content: JSON.stringify(structuredJson),
|
|
297
|
+
eventType: "draft_complete"
|
|
298
|
+
});
|
|
299
|
+
return {
|
|
300
|
+
summary: "Structured prompt generated JSON through provider CLI."
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
await emitDraftGateArtifacts(context.emit, tempDir, dispatch.workItem.id);
|
|
304
|
+
const proposalArtifact = earlyProposalArtifact ?? await readCompletionArtifact(tempDir, dispatch.workItem.id, "draft");
|
|
305
|
+
if (!proposalArtifact) {
|
|
306
|
+
throw new Error("Provider CLI Draft completed but did not call forge.complete_draft_proposal.");
|
|
307
|
+
}
|
|
308
|
+
const proposal = parseJsonLine(proposalArtifact.verdictJson) ?? {};
|
|
309
|
+
const draftJson = proposal["draft_json"];
|
|
310
|
+
if (!draftJson || typeof draftJson !== "object" || Array.isArray(draftJson)) {
|
|
311
|
+
throw new Error("Provider CLI Draft completed but draft.proposal.json did not contain draft_json.");
|
|
135
312
|
}
|
|
136
|
-
const json =
|
|
313
|
+
const json = JSON.stringify(draftJson);
|
|
137
314
|
await context.emit({
|
|
138
315
|
role: "assistant",
|
|
139
316
|
kind: "message",
|
|
140
317
|
content: json,
|
|
141
318
|
eventType: "draft_complete"
|
|
142
319
|
});
|
|
143
|
-
await emitProviderUsageEvent(context.emit, dispatch, outputState);
|
|
144
320
|
return {
|
|
145
|
-
summary: "Draft generated through Forge
|
|
321
|
+
summary: "Draft generated through Forge Draft completion cycle.",
|
|
322
|
+
artifact: proposalArtifact,
|
|
323
|
+
artifacts: [proposalArtifact]
|
|
146
324
|
};
|
|
147
325
|
}
|
|
148
326
|
finally {
|
|
327
|
+
if (completionTools) {
|
|
328
|
+
await cleanupCompletionTools(completionTools, context.emit);
|
|
329
|
+
}
|
|
149
330
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
150
331
|
}
|
|
151
332
|
}
|
|
@@ -1812,18 +1993,95 @@ function isFakeWorkerModel(model) {
|
|
|
1812
1993
|
function sha256(content) {
|
|
1813
1994
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
1814
1995
|
}
|
|
1996
|
+
function buildDraftSpec(dispatch) {
|
|
1997
|
+
return `# Draft Generation Context
|
|
1998
|
+
|
|
1999
|
+
Project: ${dispatch.draft?.projectName ?? dispatch.repo.name}
|
|
2000
|
+
Repository: ${dispatch.draft?.repoName ?? dispatch.repo.name}
|
|
2001
|
+
Draft kind: ${dispatch.draft?.kind ?? "draft"}
|
|
2002
|
+
|
|
2003
|
+
## User Prompt
|
|
2004
|
+
|
|
2005
|
+
${dispatch.draft?.userPrompt ?? ""}
|
|
2006
|
+
`;
|
|
2007
|
+
}
|
|
2008
|
+
async function emitDraftGateArtifacts(emit, worktreePath, workItemId) {
|
|
2009
|
+
const draftArtifactFiles = {
|
|
2010
|
+
draft_intent: "draft.intent.json",
|
|
2011
|
+
draft_repo_context: "draft.repo-context.json",
|
|
2012
|
+
draft_graph_context: "draft.graph-context.json",
|
|
2013
|
+
draft_product_scope: "draft.product-scope.json",
|
|
2014
|
+
draft_work_order_plan: "draft.work-order-plan.json",
|
|
2015
|
+
draft_critic: "draft.critic.json",
|
|
2016
|
+
draft_proposal: "draft.proposal.json"
|
|
2017
|
+
};
|
|
2018
|
+
for (const gate of draftCompletionGateOrder) {
|
|
2019
|
+
const actualPath = path.join(worktreePath, ".forge", "work-items", workItemId, "artifacts", draftArtifactFiles[gate]);
|
|
2020
|
+
if (!await fileExists(actualPath)) {
|
|
2021
|
+
continue;
|
|
2022
|
+
}
|
|
2023
|
+
const relativePath = path.relative(worktreePath, actualPath).split(path.sep).join("/");
|
|
2024
|
+
await emit({
|
|
2025
|
+
role: "tool",
|
|
2026
|
+
kind: "tool_result",
|
|
2027
|
+
content: `Forge Draft completion tool recorded ${gate} artifact ${relativePath}.`,
|
|
2028
|
+
eventType: "completion_tool_artifact_recorded",
|
|
2029
|
+
data: JSON.stringify({ phase: "draft", draft_gate: gate, artifact_relative_paths: [relativePath] })
|
|
2030
|
+
});
|
|
2031
|
+
await emit({
|
|
2032
|
+
role: "hook",
|
|
2033
|
+
kind: "checkpoint",
|
|
2034
|
+
content: `phase_gate: ${gate} artifact accepted with status pass.`,
|
|
2035
|
+
eventType: "phase_gate"
|
|
2036
|
+
});
|
|
2037
|
+
const next = draftCompletionGateOrder[draftCompletionGateOrder.indexOf(gate) + 1];
|
|
2038
|
+
if (next) {
|
|
2039
|
+
await emit({
|
|
2040
|
+
role: "hook",
|
|
2041
|
+
kind: "checkpoint",
|
|
2042
|
+
content: `phase_progression: ${gate} -> ${next}`,
|
|
2043
|
+
eventType: "phase_gate"
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
async function fileExists(filePath) {
|
|
2049
|
+
try {
|
|
2050
|
+
await fs.access(filePath);
|
|
2051
|
+
return true;
|
|
2052
|
+
}
|
|
2053
|
+
catch {
|
|
2054
|
+
return false;
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
1815
2057
|
function buildDraftPrompt(dispatch) {
|
|
1816
2058
|
const draft = dispatch.draft;
|
|
1817
2059
|
if (!draft) {
|
|
1818
2060
|
throw new Error("Draft prompt is missing.");
|
|
1819
2061
|
}
|
|
1820
|
-
|
|
2062
|
+
const draftArtifacts = dispatch.artifacts
|
|
2063
|
+
.filter((artifact) => artifact.scope === "operational" && ["command", "skill", "hook"].includes(artifact.kind))
|
|
2064
|
+
.map((artifact) => `## ${artifact.kind}: ${artifact.slug}\n${artifact.body.trim()}`)
|
|
2065
|
+
.join("\n\n");
|
|
2066
|
+
const artifactSection = draftArtifacts
|
|
2067
|
+
? `\n\n# Injected Draft command/skills/hooks\n\n${draftArtifacts}`
|
|
2068
|
+
: "";
|
|
2069
|
+
const projectSection = [
|
|
2070
|
+
draft.projectName ? `Project: ${draft.projectName}` : null,
|
|
2071
|
+
draft.repoName ? `Repository: ${draft.repoName}` : null
|
|
2072
|
+
].filter(Boolean).join("\n");
|
|
2073
|
+
const scopedProjectSection = projectSection ? `\n\n# Selected project context\n\n${projectSection}` : "";
|
|
2074
|
+
return `${draft.systemPrompt}${scopedProjectSection}${artifactSection}
|
|
1821
2075
|
|
|
1822
2076
|
Draft kind: ${draft.kind}
|
|
1823
2077
|
|
|
1824
2078
|
${draft.userPrompt}
|
|
1825
2079
|
|
|
1826
|
-
|
|
2080
|
+
${completionPromptForDraftCycle("draft_intent")}
|
|
2081
|
+
|
|
2082
|
+
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.
|
|
2083
|
+
|
|
2084
|
+
Do not modify source files.`;
|
|
1827
2085
|
}
|
|
1828
2086
|
function extractJsonObject(content) {
|
|
1829
2087
|
let trimmed = content.trim();
|
|
@@ -1842,6 +2100,19 @@ function summarizeStderr(stderr) {
|
|
|
1842
2100
|
const trimmed = stderr.trim();
|
|
1843
2101
|
return trimmed ? `: ${clip(trimmed, 1000)}` : "";
|
|
1844
2102
|
}
|
|
2103
|
+
async function waitForDraftProposalArtifact(worktreePath, workItemId, signal) {
|
|
2104
|
+
while (!signal?.aborted) {
|
|
2105
|
+
const artifact = await readCompletionArtifact(worktreePath, workItemId, "draft");
|
|
2106
|
+
if (artifact) {
|
|
2107
|
+
return artifact;
|
|
2108
|
+
}
|
|
2109
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2110
|
+
}
|
|
2111
|
+
throw new Error("Draft proposal artifact wait was cancelled.");
|
|
2112
|
+
}
|
|
2113
|
+
function isDecomposeDraftKind(kind) {
|
|
2114
|
+
return kind === "work_order_decomposition" || kind === "feature_decomposition";
|
|
2115
|
+
}
|
|
1845
2116
|
function deterministicDraftPayload(dispatch) {
|
|
1846
2117
|
const kind = dispatch.draft?.kind ?? "work_item";
|
|
1847
2118
|
if (kind === "work_order") {
|
|
@@ -1926,6 +2197,7 @@ Work item: ${dispatch.workItem.id} — ${dispatch.workItem.title}
|
|
|
1926
2197
|
|
|
1927
2198
|
Plan only. Do not implement source changes, do not create Develop phase artifacts, and do not mark the spec approved.
|
|
1928
2199
|
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.
|
|
2200
|
+
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.
|
|
1929
2201
|
${architectRevisionInstruction(dispatch)}
|
|
1930
2202
|
Forge owns protocol files: write Architect artifacts only through the completion MCP tool.
|
|
1931
2203
|
|
|
@@ -2231,11 +2503,11 @@ class CursorStreamJsonState {
|
|
|
2231
2503
|
if (type === "assistant") {
|
|
2232
2504
|
const text = assistantMessageText(event);
|
|
2233
2505
|
if (text) {
|
|
2234
|
-
this.finalMessage =
|
|
2506
|
+
this.finalMessage = text;
|
|
2235
2507
|
return [{
|
|
2236
2508
|
role: "assistant",
|
|
2237
2509
|
kind: "message",
|
|
2238
|
-
content:
|
|
2510
|
+
content: clip(text, 4000),
|
|
2239
2511
|
eventType: "provider_message"
|
|
2240
2512
|
}];
|
|
2241
2513
|
}
|
|
@@ -2269,7 +2541,7 @@ class CursorStreamJsonState {
|
|
|
2269
2541
|
if (type === "result" && stringField(event, "subtype") === "success") {
|
|
2270
2542
|
const result = stringField(event, "result");
|
|
2271
2543
|
if (result) {
|
|
2272
|
-
this.finalMessage =
|
|
2544
|
+
this.finalMessage = result;
|
|
2273
2545
|
}
|
|
2274
2546
|
this.accumulateUsage(event);
|
|
2275
2547
|
}
|
|
@@ -2450,11 +2722,11 @@ class CodexJsonState {
|
|
|
2450
2722
|
if (itemType === "agent_message") {
|
|
2451
2723
|
const text = stringField(item, "text");
|
|
2452
2724
|
if (text) {
|
|
2453
|
-
this.finalMessage =
|
|
2725
|
+
this.finalMessage = text;
|
|
2454
2726
|
return [{
|
|
2455
2727
|
role: "assistant",
|
|
2456
2728
|
kind: "message",
|
|
2457
|
-
content:
|
|
2729
|
+
content: clip(text, 4000),
|
|
2458
2730
|
eventType: "provider_message"
|
|
2459
2731
|
}];
|
|
2460
2732
|
}
|
|
@@ -23,6 +23,8 @@ export interface DispatchDraftPrompt {
|
|
|
23
23
|
readonly kind: string;
|
|
24
24
|
readonly systemPrompt: string;
|
|
25
25
|
readonly userPrompt: string;
|
|
26
|
+
readonly projectName?: string | null;
|
|
27
|
+
readonly repoName?: string | null;
|
|
26
28
|
}
|
|
27
29
|
export interface DispatchQaBrowserContext {
|
|
28
30
|
readonly required: boolean;
|
|
@@ -224,6 +226,8 @@ export interface RunnerDispatchArtifact {
|
|
|
224
226
|
readonly planJson?: string | null;
|
|
225
227
|
readonly evidenceJson?: string | null;
|
|
226
228
|
readonly findingsJson?: string | null;
|
|
229
|
+
readonly implementationPlanJson?: string | null;
|
|
230
|
+
readonly qaPlanJson?: string | null;
|
|
227
231
|
readonly report: string;
|
|
228
232
|
readonly producedBy: string;
|
|
229
233
|
readonly status: string;
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const forgeBridgeVersion = "0.1.1-dev.
|
|
1
|
+
export declare const forgeBridgeVersion = "0.1.1-dev.d71662695396";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const forgeBridgeVersion = "0.1.1-dev.
|
|
1
|
+
export const forgeBridgeVersion = "0.1.1-dev.d71662695396";
|
|
@@ -31,6 +31,24 @@ const ARCHITECT_OPERATIONAL_HOOKS = new Set([
|
|
|
31
31
|
"architect-complete",
|
|
32
32
|
"architect-revise"
|
|
33
33
|
]);
|
|
34
|
+
const DRAFT_OPERATIONAL_SKILLS = new Set([
|
|
35
|
+
"draft-intent",
|
|
36
|
+
"draft-repo-context",
|
|
37
|
+
"draft-graph-context",
|
|
38
|
+
"draft-product-scope",
|
|
39
|
+
"draft-work-order-plan",
|
|
40
|
+
"draft-critic",
|
|
41
|
+
"draft-proposal"
|
|
42
|
+
]);
|
|
43
|
+
const DRAFT_OPERATIONAL_HOOKS = new Set([
|
|
44
|
+
"draft-intent",
|
|
45
|
+
"draft-repo-context",
|
|
46
|
+
"draft-graph-context",
|
|
47
|
+
"draft-product-scope",
|
|
48
|
+
"draft-work-order-plan",
|
|
49
|
+
"draft-critic",
|
|
50
|
+
"draft-proposal"
|
|
51
|
+
]);
|
|
34
52
|
export async function injectWorktreeArtifacts(worktreePath, artifacts) {
|
|
35
53
|
for (const artifact of artifacts) {
|
|
36
54
|
await writeArtifact(worktreePath, artifact);
|
|
@@ -45,6 +63,9 @@ export function filterArtifactsForLane(artifacts, lane, phase) {
|
|
|
45
63
|
if (normalizedLane === "architect") {
|
|
46
64
|
return artifacts.filter((artifact) => shouldInjectArchitectArtifact(artifact, commandSlug));
|
|
47
65
|
}
|
|
66
|
+
if (normalizedLane === "draft") {
|
|
67
|
+
return artifacts.filter((artifact) => shouldInjectDraftArtifact(artifact, commandSlug));
|
|
68
|
+
}
|
|
48
69
|
return artifacts;
|
|
49
70
|
}
|
|
50
71
|
export async function writeArtifact(worktreePath, artifact) {
|
|
@@ -79,6 +100,8 @@ export function resolveCommandSlug(lane, phase) {
|
|
|
79
100
|
return "rework-prompt";
|
|
80
101
|
}
|
|
81
102
|
switch (normalizedLane) {
|
|
103
|
+
case "draft":
|
|
104
|
+
return "draft-prompt";
|
|
82
105
|
case "architect":
|
|
83
106
|
return "architect-prompt";
|
|
84
107
|
case "qa":
|
|
@@ -119,6 +142,22 @@ function shouldInjectArchitectArtifact(artifact, commandSlug) {
|
|
|
119
142
|
return false;
|
|
120
143
|
}
|
|
121
144
|
}
|
|
145
|
+
function shouldInjectDraftArtifact(artifact, commandSlug) {
|
|
146
|
+
if (artifact.scope.trim().toLowerCase() !== "operational") {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
const slug = artifact.slug.trim().toLowerCase();
|
|
150
|
+
switch (normalizedArtifactKind(artifact.kind)) {
|
|
151
|
+
case "command":
|
|
152
|
+
return slug === commandSlug;
|
|
153
|
+
case "skill":
|
|
154
|
+
return DRAFT_OPERATIONAL_SKILLS.has(slug);
|
|
155
|
+
case "hook":
|
|
156
|
+
return DRAFT_OPERATIONAL_HOOKS.has(slug);
|
|
157
|
+
default:
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
122
161
|
export function buildSpecBody(laneCommand, workItem, phase = "implementation") {
|
|
123
162
|
const prompt = laneCommand.body.replaceAll("{workItemId}", workItem.id);
|
|
124
163
|
return `---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loucompanion/forge-bridge",
|
|
3
|
-
"version": "0.1.1-dev.
|
|
3
|
+
"version": "0.1.1-dev.d71662695396",
|
|
4
4
|
"description": "Forge runner bridge CLI for connecting local developer machines to Forge dispatch.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|