@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.
@@ -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(process.env)) {
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
- || (process.platform === "win32" && isAllowedProviderParentEnvName(key, windowsAllowedProviderParentEnvNames));
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 result = await this.processRunner.run({
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
- }, context.signal);
129
- if (result.exitCode !== 0) {
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
- const content = outputState.finalMessage ?? result.stdout.trim();
133
- if (!content) {
134
- throw new Error("Provider CLI completed without returning a draft.");
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 = extractJsonObject(content);
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 Runner."
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
- return `${draft.systemPrompt}
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
- Return ONLY the requested JSON object. Do not wrap it in markdown and do not modify files.`;
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 = clip(text, 4000);
2506
+ this.finalMessage = text;
2235
2507
  return [{
2236
2508
  role: "assistant",
2237
2509
  kind: "message",
2238
- content: this.finalMessage,
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 = clip(result, 4000);
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 = clip(text, 4000);
2725
+ this.finalMessage = text;
2454
2726
  return [{
2455
2727
  role: "assistant",
2456
2728
  kind: "message",
2457
- content: this.finalMessage,
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.cada6731ecc7";
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.cada6731ecc7";
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.cada6731ecc7",
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",