@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.
@@ -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 producedBy = optionalString(payload, "produced_by")
126
- ?? optionalString(payload, "producedBy")
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(payload, "status").trim().toLowerCase();
130
- const artifactWithoutHash = normalizeArtifactPayload(payload, {
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(payload, phase, status, producedBy, contentHash);
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, completionArtifactJsonPath, completionArtifactReportPath, completionPhaseFromDispatch, completionPromptForPhase, prepareCompletionToolRuntime, readCompletionArtifact } from "../phase-completion.js";
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: "implementation_complete"
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: "implementation_complete"
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
- const verdict = {
254
- schema_version: 1,
255
- phase: "implementation",
256
- status: "pass",
257
- summary: "forge-bridge produced a deterministic implementation changeset.",
258
- plan: [{ step: "Create deterministic changeset", status: "done" }],
259
- changed_files: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Recorded deterministic bridge evidence." }],
260
- tests_added_or_updated: [{ name: "forge_bridge_run_once", evidence: "The bridge committed FORGE_PHASE5_CHANGESET.md." }],
261
- docs_added_or_updated: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Deterministic evidence document created for the run." }],
262
- acceptance_criteria: [
263
- {
264
- criterion: dispatch.workItem.acceptanceCriteria || "Deterministic bridge changeset exists",
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: "Implementation complete. forge-bridge produced deterministic phase artifacts.",
284
- eventType: "implementation_complete"
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.32d4dd5874ee";
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.32d4dd5874ee";
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.32d4dd5874ee",
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",