@loucompanion/forge-bridge 0.1.1-dev.9219f8477b8a → 0.1.1-dev.a7b57476b69b.dirty1
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.
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { defaultForgeBridgeConfig } from "./config.js";
|
|
5
|
+
describe("defaultForgeBridgeConfig", () => {
|
|
6
|
+
it("uses a non-phase-numbered repo root", () => {
|
|
7
|
+
expect(defaultForgeBridgeConfig().defaultRepoRoot).toBe(path.join(os.tmpdir(), "forge-repos"));
|
|
8
|
+
});
|
|
9
|
+
});
|
|
@@ -235,7 +235,15 @@ export class ProcessBackedProviderCliService {
|
|
|
235
235
|
}
|
|
236
236
|
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
237
237
|
try {
|
|
238
|
-
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({
|
|
239
247
|
command: command.command,
|
|
240
248
|
args: command.args,
|
|
241
249
|
cwd: tempDir,
|
|
@@ -246,8 +254,28 @@ export class ProcessBackedProviderCliService {
|
|
|
246
254
|
await context.emit(event);
|
|
247
255
|
}
|
|
248
256
|
}
|
|
249
|
-
},
|
|
250
|
-
|
|
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) {
|
|
251
279
|
throw new Error(`Provider CLI exited with code ${result.exitCode}${summarizeStderr(result.stderr)}.`);
|
|
252
280
|
}
|
|
253
281
|
await context.emit({
|
|
@@ -273,7 +301,7 @@ export class ProcessBackedProviderCliService {
|
|
|
273
301
|
};
|
|
274
302
|
}
|
|
275
303
|
await emitDraftGateArtifacts(context.emit, tempDir, dispatch.workItem.id);
|
|
276
|
-
const proposalArtifact = await readCompletionArtifact(tempDir, dispatch.workItem.id, "draft");
|
|
304
|
+
const proposalArtifact = earlyProposalArtifact ?? await readCompletionArtifact(tempDir, dispatch.workItem.id, "draft");
|
|
277
305
|
if (!proposalArtifact) {
|
|
278
306
|
throw new Error("Provider CLI Draft completed but did not call forge.complete_draft_proposal.");
|
|
279
307
|
}
|
|
@@ -417,6 +445,16 @@ export class ProcessBackedProviderCliService {
|
|
|
417
445
|
}
|
|
418
446
|
async runArchitect(context) {
|
|
419
447
|
const { dispatch, worktree } = context;
|
|
448
|
+
const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
|
|
449
|
+
await fs.mkdir(metadataRoot, { recursive: true });
|
|
450
|
+
const laneCommand = selectLaneCommand(dispatch.artifacts, dispatch.execution.lane, dispatch.cycleRun.phase);
|
|
451
|
+
if (!laneCommand) {
|
|
452
|
+
throw new Error(`Lane command for lane '${dispatch.execution.lane}' phase '${dispatch.cycleRun.phase}' was not resolved.`);
|
|
453
|
+
}
|
|
454
|
+
const specBody = buildSpecBody(laneCommand, dispatch.workItem, "architect");
|
|
455
|
+
const specPath = path.join(metadataRoot, "spec.md");
|
|
456
|
+
await fs.writeFile(specPath, specBody, "utf8");
|
|
457
|
+
await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "pending_approval", produced_by: "forge-lane-command" }) + "\n", "utf8");
|
|
420
458
|
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "architect" });
|
|
421
459
|
const command = buildProviderCliCommand(dispatch, {
|
|
422
460
|
worktreePath: worktree.worktreePath,
|
|
@@ -483,15 +521,13 @@ export class ProcessBackedProviderCliService {
|
|
|
483
521
|
content: outputState.finalMessage || `${dispatch.execution.provider} CLI produced an Architect plan for ${worktree.branchName}.`,
|
|
484
522
|
eventType: "architect_complete"
|
|
485
523
|
});
|
|
486
|
-
const specPath = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id, "spec.md");
|
|
487
|
-
const specBody = await fs.readFile(specPath, "utf8").catch(() => `# ${dispatch.workItem.title}\n\nArchitect plan pending approval.\n`);
|
|
488
524
|
return {
|
|
489
525
|
summary: `${dispatch.execution.provider} CLI Architect plan produced.`,
|
|
490
526
|
spec: {
|
|
491
527
|
repoPath: worktree.worktreePath,
|
|
492
528
|
path: specPath,
|
|
493
529
|
body: specBody,
|
|
494
|
-
producedBy:
|
|
530
|
+
producedBy: "forge-lane-command",
|
|
495
531
|
status: "pending_approval"
|
|
496
532
|
},
|
|
497
533
|
artifact: architectArtifact,
|
|
@@ -2072,11 +2108,85 @@ function summarizeStderr(stderr) {
|
|
|
2072
2108
|
const trimmed = stderr.trim();
|
|
2073
2109
|
return trimmed ? `: ${clip(trimmed, 1000)}` : "";
|
|
2074
2110
|
}
|
|
2111
|
+
async function waitForDraftProposalArtifact(worktreePath, workItemId, signal) {
|
|
2112
|
+
while (!signal?.aborted) {
|
|
2113
|
+
const artifact = await readCompletionArtifact(worktreePath, workItemId, "draft");
|
|
2114
|
+
if (artifact) {
|
|
2115
|
+
return artifact;
|
|
2116
|
+
}
|
|
2117
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2118
|
+
}
|
|
2119
|
+
throw new Error("Draft proposal artifact wait was cancelled.");
|
|
2120
|
+
}
|
|
2075
2121
|
function isDecomposeDraftKind(kind) {
|
|
2076
2122
|
return kind === "work_order_decomposition" || kind === "feature_decomposition";
|
|
2077
2123
|
}
|
|
2078
2124
|
function deterministicDraftPayload(dispatch) {
|
|
2079
2125
|
const kind = dispatch.draft?.kind ?? "work_item";
|
|
2126
|
+
if (kind === "work_order_decomposition") {
|
|
2127
|
+
return {
|
|
2128
|
+
summary: "Fake decompose proposed a Feature DAG for the Work Order.",
|
|
2129
|
+
blockedReason: "",
|
|
2130
|
+
candidates: [
|
|
2131
|
+
{
|
|
2132
|
+
origin: "agent",
|
|
2133
|
+
key: "intake-submission",
|
|
2134
|
+
title: "Customer intake submission",
|
|
2135
|
+
summary: "Collect, validate, de-duplicate, and confirm incoming customer requests.",
|
|
2136
|
+
planningPacket: "# Customer intake submission\n\nPlan Stories for request form validation, attachment support, duplicate prevention, and submission confirmation.",
|
|
2137
|
+
sortOrder: 10,
|
|
2138
|
+
dependsOn: []
|
|
2139
|
+
},
|
|
2140
|
+
{
|
|
2141
|
+
origin: "agent",
|
|
2142
|
+
key: "review-queue",
|
|
2143
|
+
title: "Reviewer triage queue",
|
|
2144
|
+
summary: "Provide reviewer triage, assignment, notes, status transitions, and audit trail.",
|
|
2145
|
+
planningPacket: "# Reviewer triage queue\n\nPlan Stories for queue filtering, assignment, reviewer notes, status changes, and immutable audit trail.",
|
|
2146
|
+
sortOrder: 20,
|
|
2147
|
+
dependsOn: ["intake-submission"]
|
|
2148
|
+
}
|
|
2149
|
+
]
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
if (kind === "feature_decomposition") {
|
|
2153
|
+
return {
|
|
2154
|
+
summary: "Fake decompose proposed a Story DAG for the Feature.",
|
|
2155
|
+
blockedReason: "",
|
|
2156
|
+
candidates: [
|
|
2157
|
+
{
|
|
2158
|
+
origin: "agent",
|
|
2159
|
+
key: "foundation",
|
|
2160
|
+
title: "Build the feature foundation",
|
|
2161
|
+
description: "Create the persistence, API, and UI shell needed by the feature.",
|
|
2162
|
+
acceptanceCriteria: "Scenario: Foundation exists\nGiven the Feature is selected\nWhen the foundation is delivered\nThen downstream Stories can build on stable contracts",
|
|
2163
|
+
points: 3,
|
|
2164
|
+
sortOrder: 10,
|
|
2165
|
+
dependsOn: []
|
|
2166
|
+
},
|
|
2167
|
+
{
|
|
2168
|
+
origin: "agent",
|
|
2169
|
+
key: "workflow-behavior",
|
|
2170
|
+
title: "Implement the primary workflow behavior",
|
|
2171
|
+
description: "Implement the main user-visible behavior for the feature after the foundation is ready.",
|
|
2172
|
+
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",
|
|
2173
|
+
points: 5,
|
|
2174
|
+
sortOrder: 20,
|
|
2175
|
+
dependsOn: ["foundation"]
|
|
2176
|
+
},
|
|
2177
|
+
{
|
|
2178
|
+
origin: "agent",
|
|
2179
|
+
key: "audit-and-tests",
|
|
2180
|
+
title: "Add audit coverage and regression tests",
|
|
2181
|
+
description: "Add audit/evidence behavior and automated regression coverage for the feature.",
|
|
2182
|
+
acceptanceCriteria: "Scenario: Feature is verifiable\nGiven the workflow behavior is complete\nWhen validation runs\nThen audit evidence and tests cover the expected outcomes",
|
|
2183
|
+
points: 3,
|
|
2184
|
+
sortOrder: 30,
|
|
2185
|
+
dependsOn: ["workflow-behavior"]
|
|
2186
|
+
}
|
|
2187
|
+
]
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2080
2190
|
if (kind === "work_order") {
|
|
2081
2191
|
return {
|
|
2082
2192
|
title: "Deterministic runner Work Order draft",
|
|
@@ -2465,11 +2575,11 @@ class CursorStreamJsonState {
|
|
|
2465
2575
|
if (type === "assistant") {
|
|
2466
2576
|
const text = assistantMessageText(event);
|
|
2467
2577
|
if (text) {
|
|
2468
|
-
this.finalMessage =
|
|
2578
|
+
this.finalMessage = text;
|
|
2469
2579
|
return [{
|
|
2470
2580
|
role: "assistant",
|
|
2471
2581
|
kind: "message",
|
|
2472
|
-
content:
|
|
2582
|
+
content: clip(text, 4000),
|
|
2473
2583
|
eventType: "provider_message"
|
|
2474
2584
|
}];
|
|
2475
2585
|
}
|
|
@@ -2503,7 +2613,7 @@ class CursorStreamJsonState {
|
|
|
2503
2613
|
if (type === "result" && stringField(event, "subtype") === "success") {
|
|
2504
2614
|
const result = stringField(event, "result");
|
|
2505
2615
|
if (result) {
|
|
2506
|
-
this.finalMessage =
|
|
2616
|
+
this.finalMessage = result;
|
|
2507
2617
|
}
|
|
2508
2618
|
this.accumulateUsage(event);
|
|
2509
2619
|
}
|
|
@@ -2684,11 +2794,11 @@ class CodexJsonState {
|
|
|
2684
2794
|
if (itemType === "agent_message") {
|
|
2685
2795
|
const text = stringField(item, "text");
|
|
2686
2796
|
if (text) {
|
|
2687
|
-
this.finalMessage =
|
|
2797
|
+
this.finalMessage = text;
|
|
2688
2798
|
return [{
|
|
2689
2799
|
role: "assistant",
|
|
2690
2800
|
kind: "message",
|
|
2691
|
-
content:
|
|
2801
|
+
content: clip(text, 4000),
|
|
2692
2802
|
eventType: "provider_message"
|
|
2693
2803
|
}];
|
|
2694
2804
|
}
|
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.a7b57476b69b.dirty1";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const forgeBridgeVersion = "0.1.1-dev.
|
|
1
|
+
export const forgeBridgeVersion = "0.1.1-dev.a7b57476b69b.dirty1";
|
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.a7b57476b69b.dirty1",
|
|
4
4
|
"description": "Forge runner bridge CLI for connecting local developer machines to Forge dispatch.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|