@loucompanion/forge-bridge 0.1.1-dev.e52b249f07a8 → 0.1.1-dev.f599637d804e

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.
@@ -15,6 +15,7 @@ export declare class ForgeBridgeService implements BridgeService {
15
15
  run(signal?: AbortSignal): Promise<void>;
16
16
  stop(): Promise<void>;
17
17
  private handleDispatch;
18
+ private parseDispatchOrReport;
18
19
  private handleCancel;
19
20
  private abortActiveSessions;
20
21
  private normalizeDispatch;
@@ -51,7 +51,7 @@ export class ForgeBridgeService {
51
51
  attempt = 0;
52
52
  await this.waitForClose(signal);
53
53
  }
54
- catch (error) {
54
+ catch {
55
55
  if (this.stopped || signal?.aborted) {
56
56
  break;
57
57
  }
@@ -73,10 +73,14 @@ export class ForgeBridgeService {
73
73
  this.abortActiveSessions("bridge stopping");
74
74
  await this.stopConnection();
75
75
  }
76
- async handleDispatch(dispatch) {
76
+ async handleDispatch(rawDispatch) {
77
+ const dispatch = this.parseDispatchOrReport(rawDispatch);
78
+ if (!dispatch) {
79
+ return;
80
+ }
77
81
  const leaseId = dispatch.lease.id;
78
82
  const fencingToken = dispatch.lease.fencingToken;
79
- const redactionSecrets = exactSecrets(dispatch);
83
+ const redactionSecrets = exactSecrets(dispatch, this.config);
80
84
  if (this.activeSessions.has(leaseId)) {
81
85
  await this.sendResult(leaseId, fencingToken, failureResult("Dispatch is already active on this bridge.")).catch(() => undefined);
82
86
  return;
@@ -87,11 +91,20 @@ export class ForgeBridgeService {
87
91
  const normalized = this.normalizeDispatch(dispatch);
88
92
  const result = await this.sessions.runOnce(normalized, {
89
93
  emit: async (event) => {
90
- await this.invokeAck("DispatchEvent", {
91
- leaseId,
92
- fencingToken,
93
- event: redactSecrets(event, redactionSecrets)
94
- });
94
+ try {
95
+ await this.invokeAck("DispatchEvent", {
96
+ leaseId,
97
+ fencingToken,
98
+ event: redactSecrets(event, redactionSecrets)
99
+ });
100
+ }
101
+ catch (error) {
102
+ if (isDispatchNoLongerPending(error)) {
103
+ controller.abort(error instanceof Error ? error.message : "Dispatch is no longer pending.");
104
+ return;
105
+ }
106
+ throw error;
107
+ }
95
108
  }
96
109
  }, controller.signal);
97
110
  await this.sendResult(leaseId, fencingToken, redactSecrets(result, redactionSecrets));
@@ -106,6 +119,27 @@ export class ForgeBridgeService {
106
119
  this.activeSessions.delete(leaseId);
107
120
  }
108
121
  }
122
+ parseDispatchOrReport(rawDispatch) {
123
+ try {
124
+ return parseRunOnceDispatchPacket(rawDispatch);
125
+ }
126
+ catch (error) {
127
+ const identifiers = readDispatchIdentifiers(rawDispatch);
128
+ if (!identifiers) {
129
+ return null;
130
+ }
131
+ const summary = invalidDispatchSummary(error);
132
+ void this.sendResult(identifiers.leaseId, identifiers.fencingToken, failureResult(summary, [
133
+ {
134
+ role: "runner",
135
+ kind: "error",
136
+ content: summary,
137
+ eventType: "invalid_dispatch"
138
+ }
139
+ ])).catch(() => undefined);
140
+ return null;
141
+ }
142
+ }
109
143
  async handleCancel(request) {
110
144
  const active = this.activeSessions.get(request.leaseId);
111
145
  if (!active || active.fencingToken !== request.fencingToken) {
@@ -149,7 +183,15 @@ export class ForgeBridgeService {
149
183
  fencingToken,
150
184
  result
151
185
  };
152
- await this.invokeAck("DispatchResult", payload);
186
+ try {
187
+ await this.invokeAck("DispatchResult", payload);
188
+ }
189
+ catch (error) {
190
+ if (isDispatchNoLongerPending(error)) {
191
+ return;
192
+ }
193
+ throw error;
194
+ }
153
195
  }
154
196
  startHeartbeat() {
155
197
  this.stopHeartbeat();
@@ -196,7 +238,7 @@ export class ForgeBridgeService {
196
238
  }
197
239
  const ack = await connection.invoke(methodName, payload);
198
240
  if (!ack.accepted) {
199
- throw new Error(ack.message ?? ack.code ?? `${methodName} callback was rejected.`);
241
+ throw new BridgeCallbackRejectedError(ack.code ?? null, ack.message ?? ack.code ?? `${methodName} callback was rejected.`);
200
242
  }
201
243
  return ack;
202
244
  }
@@ -214,6 +256,20 @@ export class ForgeBridgeService {
214
256
  }
215
257
  }
216
258
  }
259
+ class BridgeCallbackRejectedError extends Error {
260
+ code;
261
+ constructor(code, message) {
262
+ super(message);
263
+ this.code = code;
264
+ this.name = "BridgeCallbackRejectedError";
265
+ }
266
+ }
267
+ function isDispatchNoLongerPending(error) {
268
+ if (error instanceof BridgeCallbackRejectedError) {
269
+ return error.code === "bridge.dispatch_not_pending" || error.code === "bridge.stale_dispatch";
270
+ }
271
+ return error instanceof Error && error.message === "Dispatch is no longer pending.";
272
+ }
217
273
  export class SignalRBridgeConnectionFactory {
218
274
  create(config) {
219
275
  return new SignalRBridgeConnection(config);
@@ -244,14 +300,7 @@ export class SignalRBridgeConnection {
244
300
  }
245
301
  onDispatch(handler) {
246
302
  this.connection.on("Dispatch", (raw) => {
247
- let dispatch;
248
- try {
249
- dispatch = parseRunOnceDispatchPacket(raw);
250
- }
251
- catch {
252
- return;
253
- }
254
- void handler(dispatch).catch(() => undefined);
303
+ void handler(raw).catch(() => undefined);
255
304
  });
256
305
  }
257
306
  onCancel(handler) {
@@ -278,7 +327,7 @@ export function defaultBridgeServiceConfig(overrides = {}) {
278
327
  projectLabels: splitCsv(process.env.FORGE_PROJECT_LABELS ?? ""),
279
328
  repoMappings: [],
280
329
  os: process.platform,
281
- sandboxProfiles: ["default"],
330
+ sandboxProfiles: overrides.sandboxProfiles,
282
331
  supportsApiSecretGrant: true,
283
332
  supportsManagedWorktrees: true,
284
333
  supportsManagedForgeArtifacts: true,
@@ -292,6 +341,7 @@ export function defaultBridgeServiceConfig(overrides = {}) {
292
341
  };
293
342
  return {
294
343
  ...config,
344
+ sandboxProfiles: config.sandboxProfiles ?? defaultSandboxProfiles(config.cliProviders),
295
345
  heartbeatIntervalMs: positiveFiniteNumber(config.heartbeatIntervalMs, 30_000)
296
346
  };
297
347
  }
@@ -310,6 +360,11 @@ function positiveFiniteNumber(value, fallback) {
310
360
  function providerStatusesFor(providers) {
311
361
  return Object.fromEntries(providers.map((provider) => [provider, { signedIn: true, version: "", models: configuredModelsFor(provider) }]));
312
362
  }
363
+ function defaultSandboxProfiles(providers) {
364
+ return providers.some((provider) => provider.toLowerCase() === "codex")
365
+ ? ["provider-workspace-write"]
366
+ : ["none"];
367
+ }
313
368
  function configuredModelsFor(provider) {
314
369
  const envName = `FORGE_${provider.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase()}_MODELS`;
315
370
  return splitCsv(process.env[envName] ?? "");
@@ -354,6 +409,28 @@ function cleanString(value) {
354
409
  function equalsIgnoreCase(left, right) {
355
410
  return !!left?.trim() && !!right?.trim() && left.trim().toLowerCase() === right.trim().toLowerCase();
356
411
  }
412
+ function readDispatchIdentifiers(rawDispatch) {
413
+ if (!rawDispatch || typeof rawDispatch !== "object" || Array.isArray(rawDispatch)) {
414
+ return null;
415
+ }
416
+ const packet = rawDispatch;
417
+ const rawLease = packet.lease ?? packet.Lease;
418
+ if (!rawLease || typeof rawLease !== "object" || Array.isArray(rawLease)) {
419
+ return null;
420
+ }
421
+ const lease = rawLease;
422
+ const leaseId = readString(lease, "id", "Id");
423
+ const fencingToken = readString(lease, "fencingToken", "FencingToken");
424
+ return leaseId && fencingToken ? { leaseId, fencingToken } : null;
425
+ }
426
+ function readString(source, camelKey, pascalKey) {
427
+ const value = source[camelKey] ?? source[pascalKey];
428
+ return typeof value === "string" && value.trim() ? value : null;
429
+ }
430
+ function invalidDispatchSummary(error) {
431
+ const detail = error instanceof Error ? error.message : String(error);
432
+ return `Invalid bridge dispatch packet: ${detail}`;
433
+ }
357
434
  function delay(ms, signal) {
358
435
  if (signal?.aborted) {
359
436
  return Promise.resolve();
@@ -376,6 +453,9 @@ function abortPromise(signal) {
376
453
  }
377
454
  return new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
378
455
  }
379
- function exactSecrets(dispatch) {
380
- return dispatch.apiKeySecretGrant?.secret ? [dispatch.apiKeySecretGrant.secret] : [];
456
+ function exactSecrets(dispatch, config) {
457
+ return [
458
+ dispatch.apiKeySecretGrant?.secret,
459
+ config.deviceToken
460
+ ].filter((secret) => typeof secret === "string" && secret.length > 0);
381
461
  }
@@ -1,4 +1,4 @@
1
- import type { RunOnceDispatchPacket, RunOnceResultPacket, RunnerDispatchEvent } from "../session/session.types.js";
1
+ import type { RunOnceResultPacket, RunnerDispatchEvent } from "../session/session.types.js";
2
2
  export interface BridgeServiceConfig {
3
3
  readonly serverUrl: string;
4
4
  readonly runnerId: string;
@@ -88,5 +88,5 @@ export interface BridgeCancelRequest {
88
88
  readonly fencingToken: string;
89
89
  readonly reason?: string | null;
90
90
  }
91
- export type BridgeDispatchHandler = (dispatch: RunOnceDispatchPacket) => Promise<void>;
91
+ export type BridgeDispatchHandler = (dispatch: unknown) => Promise<void>;
92
92
  export type BridgeCancelHandler = (request: BridgeCancelRequest) => Promise<void>;
@@ -176,19 +176,23 @@ function parseEnvSecretGrant(raw) {
176
176
  };
177
177
  }
178
178
  function exactSecrets(dispatch) {
179
- return dispatch.apiKeySecretGrant?.secret ? [dispatch.apiKeySecretGrant.secret] : [];
179
+ return exactSecretValues(dispatch.apiKeySecretGrant?.secret, process.env.FORGE_DEVICE_TOKEN);
180
180
  }
181
181
  function readEnvSecretValuesForRedaction() {
182
+ const secrets = [];
182
183
  const raw = process.env[apiKeySecretGrantEnv];
183
- if (!raw) {
184
- return [];
185
- }
186
- try {
187
- return [parseEnvSecretGrant(raw).secret];
188
- }
189
- catch {
190
- return [];
184
+ if (raw) {
185
+ try {
186
+ secrets.push(parseEnvSecretGrant(raw).secret);
187
+ }
188
+ catch {
189
+ // Ignore malformed grant env here; command handling will report the parse error.
190
+ }
191
191
  }
192
+ return exactSecretValues(...secrets, process.env.FORGE_DEVICE_TOKEN);
193
+ }
194
+ function exactSecretValues(...values) {
195
+ return values.filter((value) => typeof value === "string" && value.length > 0);
192
196
  }
193
197
  export function parseArgs(argv) {
194
198
  if (argv[0] === "connect") {
@@ -2,7 +2,7 @@ export { parseRunOnceDispatchPacket, failureResult } from "./session.types.js";
2
2
  export { buildCompletionToolServerArgs, codexMcpConfigArgs, completionPhaseFromDispatch, expectedCompletionToolName, prepareCompletionToolRuntime, readCompletionArtifact, writeCompletionArtifact } from "./phase-completion.js";
3
3
  export type { CompletionArtifactReference, CompletionPhase, CompletionToolRuntime } from "./phase-completion.js";
4
4
  export type { SessionEventSink, SessionService } from "./session.base.js";
5
- export type { ApiKeySecretGrant, DispatchCycleRun, DispatchExecution, DispatchLease, DispatchSession, DispatchRepo, DispatchServerLocalConfig, DispatchWorkItem, RunOnceDispatchPacket, RunOnceResultPacket, RunnerDispatchArtifact, RunnerDispatchEvent, RunnerDispatchSpec, RunnerEventLine } from "./session.types.js";
5
+ export type { ApiKeySecretGrant, DispatchCycleRun, DispatchExecution, DispatchLease, DispatchPullRequestContext, DispatchSession, DispatchRepo, DispatchServerLocalConfig, DispatchWorkItem, RunOnceDispatchPacket, RunOnceResultPacket, RunnerDispatchArtifact, RunnerDispatchEvent, RunnerDispatchSpec, RunnerEventLine } from "./session.types.js";
6
6
  export { DeterministicProviderCliService, ProcessBackedProviderCliService, RoutingProviderCliService, buildProviderCliCommand } from "./provider-cli/provider-cli.service.js";
7
7
  export type { ProviderCliService } from "./provider-cli/provider-cli.base.js";
8
8
  export type { ProviderCliCommand, ProviderProcessRunner, ProviderProcessRunRequest, ProviderProcessRunResult } from "./provider-cli/provider-cli.service.js";
@@ -1,6 +1,10 @@
1
1
  import type { RunOnceDispatchPacket, RunnerDispatchArtifact } from "./session.types.js";
2
2
  import type { WorktreeHandle } from "../worktree/worktree.types.js";
3
- export type CompletionPhase = "implementation" | "self_review" | "validation" | "qa";
3
+ export type DevelopCompletionPhase = "implementation" | "self_review" | "validation";
4
+ export type DebugCompletionPhase = "debug_repro" | "debug_root_cause" | "debug_fix" | "debug_regression";
5
+ export type ArchitectCompletionPhase = "architect";
6
+ export type CompletionPhase = DevelopCompletionPhase | "qa" | DebugCompletionPhase | ArchitectCompletionPhase;
7
+ export type QaCompletionGate = "qa_plan" | "qa_regression" | "qa_api" | "qa_manual_web" | "qa_bug_repro" | "qa_findings" | "qa_verdict";
4
8
  export interface CompletionToolRuntime {
5
9
  readonly serverName: string;
6
10
  readonly command: string;
@@ -23,6 +27,9 @@ export interface CompletionToolServerConfig {
23
27
  readonly producedBy?: string;
24
28
  readonly redactionSecrets?: readonly string[];
25
29
  readonly developCycle?: boolean;
30
+ readonly qaCycle?: boolean;
31
+ readonly debugCycle?: boolean;
32
+ readonly architectCycle?: boolean;
26
33
  }
27
34
  export interface CompletionToolCallResult {
28
35
  readonly content: readonly {
@@ -32,11 +39,12 @@ export interface CompletionToolCallResult {
32
39
  readonly structuredContent: CompletionArtifactReference & {
33
40
  readonly gate: {
34
41
  readonly phase: CompletionPhase;
42
+ readonly qaGate?: QaCompletionGate;
35
43
  readonly status: string;
36
44
  readonly allowed: boolean;
37
45
  readonly reason: string;
38
46
  };
39
- readonly nextPhase: CompletionPhase | "dev_test_ready" | "sign_off_ready";
47
+ readonly nextPhase: CompletionPhase | "dev_test_ready" | "qa_ready" | "sign_off_ready";
40
48
  readonly nextInstruction: string;
41
49
  };
42
50
  }
@@ -52,33 +60,50 @@ export interface CompletionArtifactReference {
52
60
  readonly status: string;
53
61
  readonly producedBy: string;
54
62
  }
63
+ export declare const debugCompletionPhaseOrder: readonly DebugCompletionPhase[];
64
+ export declare const qaCompletionGateOrder: readonly QaCompletionGate[];
55
65
  export declare function completionPhaseFromDispatch(dispatch: RunOnceDispatchPacket): CompletionPhase;
56
66
  export declare function normalizeCompletionPhase(value: string | null | undefined): CompletionPhase;
57
67
  export declare function usesDevelopCycleCompletion(dispatch: RunOnceDispatchPacket): boolean;
68
+ export declare function usesQaCycleCompletion(dispatch: RunOnceDispatchPacket): boolean;
69
+ export declare function usesDebugCycleCompletion(dispatch: RunOnceDispatchPacket): boolean;
70
+ export declare function usesArchitectCycleCompletion(dispatch: RunOnceDispatchPacket): boolean;
58
71
  export declare function completionToolNamesForPhase(phase: CompletionPhase): readonly string[];
72
+ export declare function completionToolNamesForQaGate(gate: QaCompletionGate): readonly string[];
73
+ export declare function completionToolNamesForDebugGate(gate: DebugCompletionPhase): readonly string[];
59
74
  export declare function expectedCompletionToolName(phase: CompletionPhase): string;
75
+ export declare function expectedQaCompletionToolName(gate: QaCompletionGate): string;
76
+ export declare function expectedDebugCompletionToolName(gate: DebugCompletionPhase): string;
60
77
  export declare function artifactRootFor(worktreePath: string, workItemId: string): string;
61
- export declare function completionArtifactJsonPath(worktreePath: string, workItemId: string, phase: CompletionPhase): string;
62
- export declare function completionArtifactReportPath(worktreePath: string, workItemId: string, phase: CompletionPhase): string;
78
+ export declare function artifactRootForOwner(worktreePath: string, ownerType: string, ownerId: string): string;
79
+ export declare function completionArtifactJsonPath(worktreePath: string, workItemId: string, phase: CompletionPhase, ownerType?: string | null, ownerId?: string | null): string;
80
+ export declare function completionArtifactReportPath(worktreePath: string, workItemId: string, phase: CompletionPhase, ownerType?: string | null, ownerId?: string | null): string;
63
81
  export declare function prepareCompletionToolRuntime(options: CompletionToolRuntimeOptions): Promise<CompletionToolRuntime>;
64
82
  export declare function buildCompletionToolServerArgs(config: CompletionToolServerConfig): readonly string[];
65
83
  export declare function codexMcpConfigArgs(runtime: CompletionToolRuntime): readonly string[];
66
84
  export declare function cursorMcpServerConfig(runtime: CompletionToolRuntime): unknown;
67
85
  export declare function completionPromptForPhase(phase: CompletionPhase): string;
68
86
  export declare function completionPromptForDevelopCycle(phase: CompletionPhase): string;
87
+ export declare function completionPromptForQaCycle(currentGate?: QaCompletionGate): string;
88
+ export declare function completionPromptForDebugCycle(currentGate?: DebugCompletionPhase): string;
69
89
  export declare function completionToolNamesForServer(config: CompletionToolServerConfig): readonly string[];
70
90
  export declare class CompletionToolServerSession {
71
91
  private readonly config;
72
92
  private currentPhase;
93
+ private currentQaGate;
73
94
  private completed;
74
95
  constructor(config: CompletionToolServerConfig);
75
96
  get instructions(): string;
76
97
  toolDefinitions(): readonly unknown[];
77
98
  callTool(toolName: string, input: unknown): Promise<CompletionToolCallResult>;
99
+ private callDebugGateTool;
100
+ private callQaGateTool;
78
101
  private advanceAfterPass;
102
+ private advanceAfterQaGate;
103
+ private advanceAfterDebugGate;
79
104
  }
80
105
  export declare function writeCompletionArtifact(config: CompletionToolServerConfig, toolName: string, input: unknown): Promise<CompletionArtifactReference>;
81
- export declare function readCompletionArtifact(worktreePath: string, workItemId: string, phase: CompletionPhase): Promise<RunnerDispatchArtifact | null>;
106
+ export declare function readCompletionArtifact(worktreePath: string, workItemId: string, phase: CompletionPhase, ownerType?: string | null, ownerId?: string | null): Promise<RunnerDispatchArtifact | null>;
82
107
  export declare function runCompletionMcpServer(config: CompletionToolServerConfig): Promise<void>;
83
108
  export declare function parseCompletionMcpArgs(argv: readonly string[]): {
84
109
  readonly ok: true;