@loucompanion/forge-bridge 0.1.1-dev.32d4dd5874ee → 0.1.1-dev.615630fb4618
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 +2 -0
- package/dist/session/phase-completion.js +31 -6
- package/dist/session/provider-cli/provider-cli.service.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.service.js +124 -43
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.js +1 -4
- package/package.json +1 -1
|
@@ -19,6 +19,7 @@ export interface CompletionToolServerConfig {
|
|
|
19
19
|
readonly workItemId: string;
|
|
20
20
|
readonly phase: CompletionPhase;
|
|
21
21
|
readonly producedBy?: string;
|
|
22
|
+
readonly redactionSecrets?: readonly string[];
|
|
22
23
|
}
|
|
23
24
|
export interface CompletionArtifactReference {
|
|
24
25
|
readonly phase: CompletionPhase;
|
|
@@ -52,3 +53,4 @@ export declare function parseCompletionMcpArgs(argv: readonly string[]): {
|
|
|
52
53
|
readonly ok: false;
|
|
53
54
|
readonly error: string;
|
|
54
55
|
};
|
|
56
|
+
export declare function completionRedactionEnv(secrets: readonly string[]): Readonly<Record<string, string>>;
|
|
@@ -2,7 +2,9 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { redactSecrets } from "../shared/redaction/redaction.js";
|
|
5
6
|
const toolServerName = "forge";
|
|
7
|
+
const completionRedactionSecretsEnv = "FORGE_COMPLETION_REDACTION_SECRETS";
|
|
6
8
|
const phaseFiles = {
|
|
7
9
|
implementation: {
|
|
8
10
|
jsonFileName: "implementation.completeness.json",
|
|
@@ -122,12 +124,13 @@ export async function writeCompletionArtifact(config, toolName, input) {
|
|
|
122
124
|
const payload = asRecord(input, "completion payload");
|
|
123
125
|
validatePayloadShape(phase, payload);
|
|
124
126
|
validateOptionalWorkItemId(payload, config.workItemId);
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
+
const redactedPayload = redactSecrets(payload, config.redactionSecrets ?? []);
|
|
128
|
+
const producedBy = optionalString(redactedPayload, "produced_by")
|
|
129
|
+
?? optionalString(redactedPayload, "producedBy")
|
|
127
130
|
?? config.producedBy
|
|
128
131
|
?? "forge-completion-tool";
|
|
129
|
-
const status = requiredString(
|
|
130
|
-
const artifactWithoutHash = normalizeArtifactPayload(
|
|
132
|
+
const status = requiredString(redactedPayload, "status").trim().toLowerCase();
|
|
133
|
+
const artifactWithoutHash = normalizeArtifactPayload(redactedPayload, {
|
|
131
134
|
phase,
|
|
132
135
|
workItemId: config.workItemId,
|
|
133
136
|
producedBy,
|
|
@@ -139,7 +142,7 @@ export async function writeCompletionArtifact(config, toolName, input) {
|
|
|
139
142
|
content_hash: contentHash
|
|
140
143
|
};
|
|
141
144
|
const artifactJson = `${JSON.stringify(artifact, null, 2)}\n`;
|
|
142
|
-
const report = reportMarkdown(
|
|
145
|
+
const report = reportMarkdown(redactedPayload, phase, status, producedBy, contentHash);
|
|
143
146
|
const artifactPath = completionArtifactJsonPath(config.worktreePath, config.workItemId, phase);
|
|
144
147
|
const reportPath = completionArtifactReportPath(config.worktreePath, config.workItemId, phase);
|
|
145
148
|
await fs.mkdir(path.dirname(artifactPath), { recursive: true });
|
|
@@ -232,10 +235,17 @@ export function parseCompletionMcpArgs(argv) {
|
|
|
232
235
|
worktreePath: path.resolve(worktreePath),
|
|
233
236
|
workItemId,
|
|
234
237
|
phase,
|
|
235
|
-
producedBy
|
|
238
|
+
producedBy,
|
|
239
|
+
redactionSecrets: parseCompletionRedactionSecrets(process.env[completionRedactionSecretsEnv])
|
|
236
240
|
}
|
|
237
241
|
};
|
|
238
242
|
}
|
|
243
|
+
export function completionRedactionEnv(secrets) {
|
|
244
|
+
const filtered = secrets.filter((secret) => secret.length > 0);
|
|
245
|
+
return filtered.length > 0
|
|
246
|
+
? { [completionRedactionSecretsEnv]: JSON.stringify(filtered) }
|
|
247
|
+
: {};
|
|
248
|
+
}
|
|
239
249
|
async function prepareCursorCompletionToolRuntime(worktreePath, runtime) {
|
|
240
250
|
const cursorDir = path.join(worktreePath, ".cursor");
|
|
241
251
|
const configPath = path.join(cursorDir, "mcp.json");
|
|
@@ -504,6 +514,21 @@ function parseStrictCompletionPhase(value) {
|
|
|
504
514
|
}
|
|
505
515
|
return null;
|
|
506
516
|
}
|
|
517
|
+
function parseCompletionRedactionSecrets(raw) {
|
|
518
|
+
if (!raw?.trim()) {
|
|
519
|
+
return [];
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
const parsed = JSON.parse(raw);
|
|
523
|
+
if (!Array.isArray(parsed)) {
|
|
524
|
+
return [];
|
|
525
|
+
}
|
|
526
|
+
return parsed.filter((value) => typeof value === "string" && value.length > 0);
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
return [];
|
|
530
|
+
}
|
|
531
|
+
}
|
|
507
532
|
function phaseFromToolName(toolName) {
|
|
508
533
|
const normalized = toolName.trim().toLowerCase();
|
|
509
534
|
if (normalized === "forge.complete_implementation" || normalized === "complete_implementation") {
|
|
@@ -11,6 +11,7 @@ export interface ProviderProcessRunRequest {
|
|
|
11
11
|
readonly command: string;
|
|
12
12
|
readonly args: readonly string[];
|
|
13
13
|
readonly cwd: string;
|
|
14
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
14
15
|
}
|
|
15
16
|
export interface ProviderProcessRunResult {
|
|
16
17
|
readonly exitCode: number;
|
|
@@ -7,7 +7,7 @@ import { promisify } from "node:util";
|
|
|
7
7
|
import { GitClient } from "../../shared/git/git.js";
|
|
8
8
|
import { resolveCursorAgentDirectLaunch } from "../../shared/cursor-agent/cursor-agent.launch.js";
|
|
9
9
|
import { buildSpecBody, selectLaneCommand } from "../../worktree/artifact-injector.js";
|
|
10
|
-
import { codexMcpConfigArgs,
|
|
10
|
+
import { codexMcpConfigArgs, completionPhaseFromDispatch, completionPromptForPhase, completionRedactionEnv, expectedCompletionToolName, prepareCompletionToolRuntime, readCompletionArtifact, writeCompletionArtifact } from "../phase-completion.js";
|
|
11
11
|
const fakeWorkerModel = "server-local-fake-worker";
|
|
12
12
|
const execFileAsync = promisify(execFile);
|
|
13
13
|
export class RoutingProviderCliService {
|
|
@@ -77,7 +77,8 @@ export class ProcessBackedProviderCliService {
|
|
|
77
77
|
result = await this.processRunner.run({
|
|
78
78
|
command: command.command,
|
|
79
79
|
args: command.args,
|
|
80
|
-
cwd: worktree.worktreePath
|
|
80
|
+
cwd: worktree.worktreePath,
|
|
81
|
+
env: providerRedactionEnv(dispatch)
|
|
81
82
|
}, {
|
|
82
83
|
onStdoutLine: async (line) => {
|
|
83
84
|
const event = outputState.consume(line);
|
|
@@ -105,12 +106,13 @@ export class ProcessBackedProviderCliService {
|
|
|
105
106
|
if (!completionArtifact) {
|
|
106
107
|
throw new Error(`Provider CLI completed but did not call the Forge ${phase} completion tool.`);
|
|
107
108
|
}
|
|
109
|
+
await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, completionArtifact);
|
|
108
110
|
if (outputState.finalMessage) {
|
|
109
111
|
await context.emit({
|
|
110
112
|
role: "assistant",
|
|
111
113
|
kind: "message",
|
|
112
114
|
content: outputState.finalMessage,
|
|
113
|
-
eventType:
|
|
115
|
+
eventType: phaseCompleteEventType(phase)
|
|
114
116
|
});
|
|
115
117
|
}
|
|
116
118
|
else {
|
|
@@ -118,7 +120,7 @@ export class ProcessBackedProviderCliService {
|
|
|
118
120
|
role: "assistant",
|
|
119
121
|
kind: "message",
|
|
120
122
|
content: `${dispatch.execution.provider} CLI completed for ${worktree.branchName}.`,
|
|
121
|
-
eventType:
|
|
123
|
+
eventType: phaseCompleteEventType(phase)
|
|
122
124
|
});
|
|
123
125
|
}
|
|
124
126
|
return {
|
|
@@ -250,41 +252,26 @@ export class DeterministicProviderCliService {
|
|
|
250
252
|
content: `Created bridge changeset at ${changesetPath}`,
|
|
251
253
|
eventType: "changeset"
|
|
252
254
|
});
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
evidence: `FORGE_PHASE5_CHANGESET.md was committed with content hash ${sha256(changesetBody)}.`,
|
|
266
|
-
status: "satisfied"
|
|
267
|
-
}
|
|
268
|
-
],
|
|
269
|
-
known_gaps: [],
|
|
270
|
-
next_phase_context: "Review FORGE_PHASE5_CHANGESET.md and the bridge commit before validation.",
|
|
271
|
-
produced_by: "forge-bridge",
|
|
272
|
-
content_hash: sha256(changesetBody)
|
|
273
|
-
};
|
|
274
|
-
const verdictJson = JSON.stringify(verdict);
|
|
275
|
-
const report = `# Implementation report\n\nforge-bridge prepared \`${worktree.branchName}\` and created \`${changesetPath}\`.\n`;
|
|
276
|
-
const artifactPath = completionArtifactJsonPath(worktree.worktreePath, dispatch.workItem.id, "implementation");
|
|
277
|
-
const reportPath = completionArtifactReportPath(worktree.worktreePath, dispatch.workItem.id, "implementation");
|
|
278
|
-
await fs.writeFile(artifactPath, verdictJson + "\n", "utf8");
|
|
279
|
-
await fs.writeFile(reportPath, report, "utf8");
|
|
255
|
+
await writeCompletionArtifact({
|
|
256
|
+
worktreePath: worktree.worktreePath,
|
|
257
|
+
workItemId: dispatch.workItem.id,
|
|
258
|
+
phase,
|
|
259
|
+
producedBy: "forge-bridge",
|
|
260
|
+
redactionSecrets: exactRedactionSecrets(dispatch)
|
|
261
|
+
}, expectedCompletionToolName(phase), deterministicCompletionPayload(phase, dispatch, changesetBody));
|
|
262
|
+
const completionArtifact = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, phase);
|
|
263
|
+
if (!completionArtifact) {
|
|
264
|
+
throw new Error(`Deterministic provider did not produce the Forge ${phase} completion artifact.`);
|
|
265
|
+
}
|
|
266
|
+
await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, completionArtifact);
|
|
280
267
|
await context.emit({
|
|
281
268
|
role: "assistant",
|
|
282
269
|
kind: "message",
|
|
283
|
-
content: "
|
|
284
|
-
eventType:
|
|
270
|
+
content: `${phase.replace(/_/g, " ")} complete. forge-bridge produced deterministic phase artifacts.`,
|
|
271
|
+
eventType: phaseCompleteEventType(phase)
|
|
285
272
|
});
|
|
286
273
|
return {
|
|
287
|
-
summary: `forge-bridge completed ${worktree.branchName}.`,
|
|
274
|
+
summary: `forge-bridge completed ${worktree.branchName} (${phase}).`,
|
|
288
275
|
spec: {
|
|
289
276
|
repoPath: worktree.worktreePath,
|
|
290
277
|
path: specPath,
|
|
@@ -292,15 +279,7 @@ export class DeterministicProviderCliService {
|
|
|
292
279
|
producedBy: "forge-lane-command",
|
|
293
280
|
status: "approved"
|
|
294
281
|
},
|
|
295
|
-
artifact:
|
|
296
|
-
repoPath: worktree.worktreePath,
|
|
297
|
-
path: artifactPath,
|
|
298
|
-
verdictJson,
|
|
299
|
-
report,
|
|
300
|
-
producedBy: "forge-bridge",
|
|
301
|
-
status: "pass",
|
|
302
|
-
phase: "implementation"
|
|
303
|
-
}
|
|
282
|
+
artifact: completionArtifact
|
|
304
283
|
};
|
|
305
284
|
}
|
|
306
285
|
async runQa(dispatch, emit) {
|
|
@@ -334,12 +313,114 @@ export class DeterministicProviderCliService {
|
|
|
334
313
|
};
|
|
335
314
|
}
|
|
336
315
|
}
|
|
316
|
+
function phaseCompleteEventType(phase) {
|
|
317
|
+
return phase === "implementation" ? "implementation_complete" : `${phase}_complete`;
|
|
318
|
+
}
|
|
319
|
+
function deterministicCompletionPayload(phase, dispatch, changesetBody) {
|
|
320
|
+
const changesetHash = sha256(changesetBody);
|
|
321
|
+
const criterion = dispatch.workItem.acceptanceCriteria || "Deterministic bridge changeset exists";
|
|
322
|
+
if (phase === "implementation") {
|
|
323
|
+
return {
|
|
324
|
+
status: "pass",
|
|
325
|
+
summary: "forge-bridge produced a deterministic implementation changeset.",
|
|
326
|
+
plan: [{ step: "Create deterministic changeset", status: "done" }],
|
|
327
|
+
changed_files: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Recorded deterministic bridge evidence." }],
|
|
328
|
+
tests_added_or_updated: [{ name: "forge_bridge_run_once", evidence: "The bridge committed FORGE_PHASE5_CHANGESET.md." }],
|
|
329
|
+
docs_added_or_updated: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Deterministic evidence document created for the run." }],
|
|
330
|
+
acceptance_criteria: [
|
|
331
|
+
{
|
|
332
|
+
criterion,
|
|
333
|
+
evidence: `FORGE_PHASE5_CHANGESET.md was committed with content hash ${changesetHash}.`,
|
|
334
|
+
status: "satisfied"
|
|
335
|
+
}
|
|
336
|
+
],
|
|
337
|
+
known_gaps: [],
|
|
338
|
+
next_phase_context: "Review FORGE_PHASE5_CHANGESET.md and the bridge commit before validation."
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
if (phase === "self_review") {
|
|
342
|
+
return {
|
|
343
|
+
status: "pass",
|
|
344
|
+
summary: "forge-bridge fake worker completed deterministic self-review.",
|
|
345
|
+
reviewed_diff: true,
|
|
346
|
+
findings: [],
|
|
347
|
+
security_review: {
|
|
348
|
+
status: "pass",
|
|
349
|
+
notes: "No secret handling, auth, or destructive behavior changes were introduced by the deterministic changeset."
|
|
350
|
+
},
|
|
351
|
+
edge_case_review: {
|
|
352
|
+
status: "pass",
|
|
353
|
+
notes: "The deterministic fake-worker changeset is intentionally narrow and repeatable."
|
|
354
|
+
},
|
|
355
|
+
design_review: {
|
|
356
|
+
status: "not_applicable",
|
|
357
|
+
screenshots: [],
|
|
358
|
+
notes: "No UI changes."
|
|
359
|
+
},
|
|
360
|
+
deterministic_comparison: {
|
|
361
|
+
status: "pass",
|
|
362
|
+
command_or_tool: "git diff --stat HEAD~1..HEAD",
|
|
363
|
+
notes: `Reviewed deterministic changeset with content hash ${changesetHash}.`
|
|
364
|
+
},
|
|
365
|
+
fixes_applied: [],
|
|
366
|
+
remaining_risks: []
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
status: "pass",
|
|
371
|
+
summary: "forge-bridge fake worker completed deterministic validation.",
|
|
372
|
+
commands: [
|
|
373
|
+
{
|
|
374
|
+
command: "git status --short && git diff --check",
|
|
375
|
+
exit_code: 0,
|
|
376
|
+
status: "pass",
|
|
377
|
+
summary: "Deterministic validation command completed for the fake-worker changeset."
|
|
378
|
+
}
|
|
379
|
+
],
|
|
380
|
+
acceptance_criteria: [
|
|
381
|
+
{
|
|
382
|
+
criterion,
|
|
383
|
+
evidence: `FORGE_PHASE5_CHANGESET.md exists with content hash ${changesetHash}.`,
|
|
384
|
+
status: "satisfied"
|
|
385
|
+
}
|
|
386
|
+
],
|
|
387
|
+
build_status: "pass",
|
|
388
|
+
test_status: "pass",
|
|
389
|
+
lint_status: "not_applicable",
|
|
390
|
+
design_status: "not_applicable",
|
|
391
|
+
drift_checks: [
|
|
392
|
+
{
|
|
393
|
+
command_or_tool: "git diff --check",
|
|
394
|
+
status: "pass",
|
|
395
|
+
summary: "No deterministic drift or whitespace errors were reported for the fake-worker changeset."
|
|
396
|
+
}
|
|
397
|
+
],
|
|
398
|
+
ready_for_dev_test: true
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
async function emitCompletionArtifactRecorded(emit, worktreePath, artifact) {
|
|
402
|
+
const relativePath = path.relative(worktreePath, artifact.path) || path.basename(artifact.path);
|
|
403
|
+
await emit({
|
|
404
|
+
role: "tool",
|
|
405
|
+
kind: "tool_result",
|
|
406
|
+
content: `Forge completion tool recorded ${artifact.phase ?? "unknown"} artifact ${relativePath} (${artifact.status}).`,
|
|
407
|
+
eventType: "completion_tool_artifact_recorded"
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
function providerRedactionEnv(dispatch) {
|
|
411
|
+
const env = completionRedactionEnv(exactRedactionSecrets(dispatch));
|
|
412
|
+
return Object.keys(env).length > 0 ? env : undefined;
|
|
413
|
+
}
|
|
414
|
+
function exactRedactionSecrets(dispatch) {
|
|
415
|
+
return dispatch.apiKeySecretGrant?.secret ? [dispatch.apiKeySecretGrant.secret] : [];
|
|
416
|
+
}
|
|
337
417
|
export class StreamingProviderProcessRunner {
|
|
338
418
|
async run(request, handlers = {}, signal) {
|
|
339
419
|
const launch = await resolveProviderProcessLaunch(request.command, request.args);
|
|
340
420
|
return await new Promise((resolve, reject) => {
|
|
341
421
|
const child = spawn(launch.command, launch.args, {
|
|
342
422
|
cwd: request.cwd,
|
|
423
|
+
env: request.env ? { ...process.env, ...request.env } : undefined,
|
|
343
424
|
stdio: ["ignore", "pipe", "pipe"],
|
|
344
425
|
windowsHide: true
|
|
345
426
|
});
|
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.615630fb4618";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const forgeBridgeVersion = "0.1.1-dev.
|
|
1
|
+
export const forgeBridgeVersion = "0.1.1-dev.615630fb4618";
|
|
@@ -29,10 +29,7 @@ export function selectLaneCommand(artifacts, lane, phase) {
|
|
|
29
29
|
return artifacts.find((artifact) => normalizedArtifactKind(artifact.kind) === "command" && artifact.slug.trim().toLowerCase() === slug);
|
|
30
30
|
}
|
|
31
31
|
export function resolveCommandSlug(lane, phase) {
|
|
32
|
-
const normalizedPhase = phase.trim().toLowerCase();
|
|
33
|
-
if (normalizedPhase === "self_review") {
|
|
34
|
-
return "self-review-prompt";
|
|
35
|
-
}
|
|
32
|
+
const normalizedPhase = phase.trim().toLowerCase().replace(/-/g, "_");
|
|
36
33
|
if (normalizedPhase === "rework") {
|
|
37
34
|
return "rework-prompt";
|
|
38
35
|
}
|
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.615630fb4618",
|
|
4
4
|
"description": "Forge runner bridge CLI for connecting local developer machines to Forge dispatch.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|