@loucompanion/forge-bridge 0.1.1-dev.ac486debe210 → 0.1.1-dev.c5b751411a2b

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.
@@ -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
  }
@@ -87,11 +87,20 @@ export class ForgeBridgeService {
87
87
  const normalized = this.normalizeDispatch(dispatch);
88
88
  const result = await this.sessions.runOnce(normalized, {
89
89
  emit: async (event) => {
90
- await this.invokeAck("DispatchEvent", {
91
- leaseId,
92
- fencingToken,
93
- event: redactSecrets(event, redactionSecrets)
94
- });
90
+ try {
91
+ await this.invokeAck("DispatchEvent", {
92
+ leaseId,
93
+ fencingToken,
94
+ event: redactSecrets(event, redactionSecrets)
95
+ });
96
+ }
97
+ catch (error) {
98
+ if (isDispatchNoLongerPending(error)) {
99
+ controller.abort(error instanceof Error ? error.message : "Dispatch is no longer pending.");
100
+ return;
101
+ }
102
+ throw error;
103
+ }
95
104
  }
96
105
  }, controller.signal);
97
106
  await this.sendResult(leaseId, fencingToken, redactSecrets(result, redactionSecrets));
@@ -123,6 +132,7 @@ export class ForgeBridgeService {
123
132
  const mapping = findRepoMapping(dispatch, this.config.repoMappings);
124
133
  const localPath = mapping?.localPath?.trim();
125
134
  const repoRoot = localPath ? path.dirname(path.resolve(localPath)) : this.config.defaultRepoRoot;
135
+ const qaLaunchTarget = normalizeQaAppLaunchTarget(mapping?.qaAppLaunchTarget) ?? dispatch.qaBrowser?.launchTarget ?? null;
126
136
  return {
127
137
  ...dispatch,
128
138
  repo: {
@@ -133,7 +143,13 @@ export class ForgeBridgeService {
133
143
  serverLocal: {
134
144
  repoRoot,
135
145
  cleanupWorktrees: this.config.cleanupWorktrees
136
- }
146
+ },
147
+ qaBrowser: dispatch.qaBrowser
148
+ ? {
149
+ ...dispatch.qaBrowser,
150
+ launchTarget: qaLaunchTarget
151
+ }
152
+ : dispatch.qaBrowser
137
153
  };
138
154
  }
139
155
  async sendResult(leaseId, fencingToken, result) {
@@ -142,7 +158,15 @@ export class ForgeBridgeService {
142
158
  fencingToken,
143
159
  result
144
160
  };
145
- await this.invokeAck("DispatchResult", payload);
161
+ try {
162
+ await this.invokeAck("DispatchResult", payload);
163
+ }
164
+ catch (error) {
165
+ if (isDispatchNoLongerPending(error)) {
166
+ return;
167
+ }
168
+ throw error;
169
+ }
146
170
  }
147
171
  startHeartbeat() {
148
172
  this.stopHeartbeat();
@@ -189,7 +213,7 @@ export class ForgeBridgeService {
189
213
  }
190
214
  const ack = await connection.invoke(methodName, payload);
191
215
  if (!ack.accepted) {
192
- throw new Error(ack.message ?? ack.code ?? `${methodName} callback was rejected.`);
216
+ throw new BridgeCallbackRejectedError(ack.code ?? null, ack.message ?? ack.code ?? `${methodName} callback was rejected.`);
193
217
  }
194
218
  return ack;
195
219
  }
@@ -207,6 +231,20 @@ export class ForgeBridgeService {
207
231
  }
208
232
  }
209
233
  }
234
+ class BridgeCallbackRejectedError extends Error {
235
+ code;
236
+ constructor(code, message) {
237
+ super(message);
238
+ this.code = code;
239
+ this.name = "BridgeCallbackRejectedError";
240
+ }
241
+ }
242
+ function isDispatchNoLongerPending(error) {
243
+ if (error instanceof BridgeCallbackRejectedError) {
244
+ return error.code === "bridge.dispatch_not_pending" || error.code === "bridge.stale_dispatch";
245
+ }
246
+ return error instanceof Error && error.message === "Dispatch is no longer pending.";
247
+ }
210
248
  export class SignalRBridgeConnectionFactory {
211
249
  create(config) {
212
250
  return new SignalRBridgeConnection(config);
@@ -323,6 +361,27 @@ function findRepoMapping(dispatch, mappings) {
323
361
  (!!mapping.remoteUrl?.trim() && equalsIgnoreCase(mapping.remoteUrl, dispatch.repo.remoteUrl)));
324
362
  });
325
363
  }
364
+ function normalizeQaAppLaunchTarget(target) {
365
+ if (!target) {
366
+ return null;
367
+ }
368
+ const routes = [...new Set((target.routes ?? []).map((route) => route.trim()).filter(Boolean))];
369
+ const normalized = {
370
+ url: cleanString(target.url),
371
+ command: cleanString(target.command),
372
+ working_directory: cleanString(target.working_directory),
373
+ ready_url: cleanString(target.ready_url),
374
+ routes,
375
+ notes: cleanString(target.notes)
376
+ };
377
+ return normalized.url || normalized.command || normalized.ready_url || routes.length > 0
378
+ ? normalized
379
+ : null;
380
+ }
381
+ function cleanString(value) {
382
+ const clean = value?.trim();
383
+ return clean ? clean : null;
384
+ }
326
385
  function equalsIgnoreCase(left, right) {
327
386
  return !!left?.trim() && !!right?.trim() && left.trim().toLowerCase() === right.trim().toLowerCase();
328
387
  }
@@ -27,6 +27,15 @@ export interface BridgeRepoMapping {
27
27
  readonly name?: string | null;
28
28
  readonly remoteUrl?: string | null;
29
29
  readonly localPath?: string | null;
30
+ readonly qaAppLaunchTarget?: QaAppLaunchTarget | null;
31
+ }
32
+ export interface QaAppLaunchTarget {
33
+ readonly url?: string | null;
34
+ readonly command?: string | null;
35
+ readonly working_directory?: string | null;
36
+ readonly ready_url?: string | null;
37
+ readonly routes?: readonly string[] | null;
38
+ readonly notes?: string | null;
30
39
  }
31
40
  export interface BridgeProviderStatus {
32
41
  readonly signedIn?: boolean | null;
@@ -159,7 +159,8 @@ function normalizeRepoMappings(values) {
159
159
  repoId: cleanString(mapping.repoId),
160
160
  name: cleanString(mapping.name) ?? label,
161
161
  remoteUrl: cleanString(mapping.remoteUrl),
162
- localPath: path.resolve(mapping.localPath)
162
+ localPath: path.resolve(mapping.localPath),
163
+ qaAppLaunchTarget: normalizeQaAppLaunchTarget(mapping.qaAppLaunchTarget)
163
164
  };
164
165
  })
165
166
  .sort((left, right) => left.label.localeCompare(right.label));
@@ -171,6 +172,22 @@ function normalizeLabel(value) {
171
172
  }
172
173
  return label;
173
174
  }
175
+ function normalizeQaAppLaunchTarget(value) {
176
+ if (!value)
177
+ return undefined;
178
+ const routes = cleanList(value.routes);
179
+ const normalized = {
180
+ url: cleanString(value.url),
181
+ command: cleanString(value.command),
182
+ working_directory: cleanString(value.working_directory),
183
+ ready_url: cleanString(value.ready_url),
184
+ routes,
185
+ notes: cleanString(value.notes)
186
+ };
187
+ return normalized.url || normalized.command || normalized.ready_url || routes?.length
188
+ ? normalized
189
+ : undefined;
190
+ }
174
191
  function cleanString(value) {
175
192
  const clean = value?.trim();
176
193
  return clean ? clean : undefined;
@@ -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";
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;
@@ -17,10 +21,15 @@ export interface CompletionToolRuntimeOptions {
17
21
  export interface CompletionToolServerConfig {
18
22
  readonly worktreePath: string;
19
23
  readonly workItemId: string;
24
+ readonly ownerType?: string;
25
+ readonly ownerId?: string;
20
26
  readonly phase: CompletionPhase;
21
27
  readonly producedBy?: string;
22
28
  readonly redactionSecrets?: readonly string[];
23
29
  readonly developCycle?: boolean;
30
+ readonly qaCycle?: boolean;
31
+ readonly debugCycle?: boolean;
32
+ readonly architectCycle?: boolean;
24
33
  }
25
34
  export interface CompletionToolCallResult {
26
35
  readonly content: readonly {
@@ -30,17 +39,20 @@ export interface CompletionToolCallResult {
30
39
  readonly structuredContent: CompletionArtifactReference & {
31
40
  readonly gate: {
32
41
  readonly phase: CompletionPhase;
42
+ readonly qaGate?: QaCompletionGate;
33
43
  readonly status: string;
34
44
  readonly allowed: boolean;
35
45
  readonly reason: string;
36
46
  };
37
- readonly nextPhase: CompletionPhase | "dev_test_ready";
47
+ readonly nextPhase: CompletionPhase | "dev_test_ready" | "qa_ready" | "sign_off_ready";
38
48
  readonly nextInstruction: string;
39
49
  };
40
50
  }
41
51
  export interface CompletionArtifactReference {
42
52
  readonly phase: CompletionPhase;
43
53
  readonly workItemId: string;
54
+ readonly ownerType?: string;
55
+ readonly ownerId?: string;
44
56
  readonly repoPath: string;
45
57
  readonly path: string;
46
58
  readonly reportPath: string;
@@ -48,11 +60,20 @@ export interface CompletionArtifactReference {
48
60
  readonly status: string;
49
61
  readonly producedBy: string;
50
62
  }
63
+ export declare const debugCompletionPhaseOrder: readonly DebugCompletionPhase[];
64
+ export declare const qaCompletionGateOrder: readonly QaCompletionGate[];
51
65
  export declare function completionPhaseFromDispatch(dispatch: RunOnceDispatchPacket): CompletionPhase;
52
66
  export declare function normalizeCompletionPhase(value: string | null | undefined): CompletionPhase;
53
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;
54
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[];
55
74
  export declare function expectedCompletionToolName(phase: CompletionPhase): string;
75
+ export declare function expectedQaCompletionToolName(gate: QaCompletionGate): string;
76
+ export declare function expectedDebugCompletionToolName(gate: DebugCompletionPhase): string;
56
77
  export declare function artifactRootFor(worktreePath: string, workItemId: string): string;
57
78
  export declare function completionArtifactJsonPath(worktreePath: string, workItemId: string, phase: CompletionPhase): string;
58
79
  export declare function completionArtifactReportPath(worktreePath: string, workItemId: string, phase: CompletionPhase): string;
@@ -62,16 +83,23 @@ export declare function codexMcpConfigArgs(runtime: CompletionToolRuntime): read
62
83
  export declare function cursorMcpServerConfig(runtime: CompletionToolRuntime): unknown;
63
84
  export declare function completionPromptForPhase(phase: CompletionPhase): string;
64
85
  export declare function completionPromptForDevelopCycle(phase: CompletionPhase): string;
86
+ export declare function completionPromptForQaCycle(currentGate?: QaCompletionGate): string;
87
+ export declare function completionPromptForDebugCycle(currentGate?: DebugCompletionPhase): string;
65
88
  export declare function completionToolNamesForServer(config: CompletionToolServerConfig): readonly string[];
66
89
  export declare class CompletionToolServerSession {
67
90
  private readonly config;
68
91
  private currentPhase;
92
+ private currentQaGate;
69
93
  private completed;
70
94
  constructor(config: CompletionToolServerConfig);
71
95
  get instructions(): string;
72
96
  toolDefinitions(): readonly unknown[];
73
97
  callTool(toolName: string, input: unknown): Promise<CompletionToolCallResult>;
98
+ private callDebugGateTool;
99
+ private callQaGateTool;
74
100
  private advanceAfterPass;
101
+ private advanceAfterQaGate;
102
+ private advanceAfterDebugGate;
75
103
  }
76
104
  export declare function writeCompletionArtifact(config: CompletionToolServerConfig, toolName: string, input: unknown): Promise<CompletionArtifactReference>;
77
105
  export declare function readCompletionArtifact(worktreePath: string, workItemId: string, phase: CompletionPhase): Promise<RunnerDispatchArtifact | null>;