@loucompanion/forge-bridge 0.1.1-dev.32d4dd5874ee → 0.1.1-dev.40fbc494669b

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.
@@ -6,6 +6,11 @@ export interface ProviderCliRunContext {
6
6
  readonly signal?: AbortSignal;
7
7
  emit(event: RunnerDispatchEvent): Promise<void>;
8
8
  }
9
+ export interface ProviderCliDraftContext {
10
+ readonly dispatch: RunOnceDispatchPacket;
11
+ readonly signal?: AbortSignal;
12
+ emit(event: RunnerDispatchEvent): Promise<void>;
13
+ }
9
14
  export interface ProviderCliRunResult {
10
15
  readonly summary: string;
11
16
  readonly spec?: RunnerDispatchSpec | null;
@@ -0,0 +1,25 @@
1
+ import type { RunOnceDispatchPacket, RunnerDispatchEvent } from "../session.types.js";
2
+ export type UsageSource = "provider_reported" | "cli_reported" | "estimated" | "unknown";
3
+ /**
4
+ * Normalized, provider-agnostic token usage accumulated for a dispatch. Cost is never computed
5
+ * here; the API recomputes cost from these tokens x model rates (ADR-0006).
6
+ */
7
+ export interface ProviderUsage {
8
+ readonly inputTokens: number;
9
+ readonly outputTokens: number;
10
+ readonly cacheReadTokens: number;
11
+ readonly cacheCreationTokens: number;
12
+ readonly reasoningOutputTokens: number;
13
+ readonly providerDurationMs: number | null;
14
+ readonly source: UsageSource;
15
+ }
16
+ export declare function emptyUsage(source?: UsageSource): ProviderUsage;
17
+ export declare function addUsage(base: ProviderUsage, next: Partial<ProviderUsage>): ProviderUsage;
18
+ export declare function totalTokens(usage: ProviderUsage): number;
19
+ export declare function hasUsage(usage: ProviderUsage | null): usage is ProviderUsage;
20
+ /**
21
+ * Builds the durable `provider_usage` event. Token numbers travel as a structured `data` JSON
22
+ * string (snake_case) that the API parses into a usage_event row.
23
+ */
24
+ export declare function buildProviderUsageEvent(dispatch: RunOnceDispatchPacket, usage: ProviderUsage): RunnerDispatchEvent;
25
+ export declare function readUsageNumber(source: Record<string, unknown>, key: string): number;
@@ -0,0 +1,64 @@
1
+ export function emptyUsage(source = "unknown") {
2
+ return {
3
+ inputTokens: 0,
4
+ outputTokens: 0,
5
+ cacheReadTokens: 0,
6
+ cacheCreationTokens: 0,
7
+ reasoningOutputTokens: 0,
8
+ providerDurationMs: null,
9
+ source
10
+ };
11
+ }
12
+ export function addUsage(base, next) {
13
+ return {
14
+ inputTokens: base.inputTokens + nonNegative(next.inputTokens),
15
+ outputTokens: base.outputTokens + nonNegative(next.outputTokens),
16
+ cacheReadTokens: base.cacheReadTokens + nonNegative(next.cacheReadTokens),
17
+ cacheCreationTokens: base.cacheCreationTokens + nonNegative(next.cacheCreationTokens),
18
+ reasoningOutputTokens: base.reasoningOutputTokens + nonNegative(next.reasoningOutputTokens),
19
+ providerDurationMs: next.providerDurationMs ?? base.providerDurationMs,
20
+ source: next.source ?? base.source
21
+ };
22
+ }
23
+ export function totalTokens(usage) {
24
+ return usage.inputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheCreationTokens;
25
+ }
26
+ export function hasUsage(usage) {
27
+ return usage !== null && totalTokens(usage) > 0;
28
+ }
29
+ /**
30
+ * Builds the durable `provider_usage` event. Token numbers travel as a structured `data` JSON
31
+ * string (snake_case) that the API parses into a usage_event row.
32
+ */
33
+ export function buildProviderUsageEvent(dispatch, usage) {
34
+ const data = {
35
+ provider: dispatch.execution.provider,
36
+ model: dispatch.execution.model,
37
+ lane: dispatch.execution.lane,
38
+ phase: dispatch.cycleRun.phase,
39
+ attempt: dispatch.cycleRun.attempt,
40
+ input_tokens: usage.inputTokens,
41
+ output_tokens: usage.outputTokens,
42
+ cache_read_tokens: usage.cacheReadTokens,
43
+ cache_creation_tokens: usage.cacheCreationTokens,
44
+ reasoning_output_tokens: usage.reasoningOutputTokens,
45
+ metered: dispatch.execution.authKind === "api_key",
46
+ usage_source: usage.source,
47
+ provider_duration_ms: usage.providerDurationMs
48
+ };
49
+ const total = totalTokens(usage);
50
+ return {
51
+ role: "runner",
52
+ kind: "checkpoint",
53
+ content: `Provider usage: ${total} tokens (${usage.inputTokens} in / ${usage.outputTokens} out) for ${dispatch.execution.model}.`,
54
+ eventType: "provider_usage",
55
+ data: JSON.stringify(data)
56
+ };
57
+ }
58
+ export function readUsageNumber(source, key) {
59
+ const value = source[key];
60
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
61
+ }
62
+ function nonNegative(value) {
63
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
64
+ }
@@ -0,0 +1,3 @@
1
+ export declare function isPathUnderRoot(root: string, target: string): boolean;
2
+ export declare function ensureNoSymbolicLinkPathSegments(root: string, targetDirectory: string, errorMessage: string): Promise<void>;
3
+ export declare function sha256File(filePath: string): Promise<string>;
@@ -0,0 +1,44 @@
1
+ import crypto from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ export function isPathUnderRoot(root, target) {
6
+ const relative = path.relative(path.resolve(root), path.resolve(target));
7
+ return Boolean(relative)
8
+ && relative !== ".."
9
+ && !relative.startsWith(`..${path.sep}`)
10
+ && !path.isAbsolute(relative);
11
+ }
12
+ export async function ensureNoSymbolicLinkPathSegments(root, targetDirectory, errorMessage) {
13
+ const resolvedRoot = path.resolve(root);
14
+ const resolvedTargetDirectory = path.resolve(targetDirectory);
15
+ const relative = path.relative(resolvedRoot, resolvedTargetDirectory);
16
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
17
+ throw new Error(errorMessage);
18
+ }
19
+ let current = resolvedRoot;
20
+ const rootStat = await fs.lstat(current);
21
+ if (rootStat.isSymbolicLink()) {
22
+ throw new Error(errorMessage);
23
+ }
24
+ if (!relative) {
25
+ return;
26
+ }
27
+ for (const segment of relative.split(path.sep)) {
28
+ if (!segment) {
29
+ continue;
30
+ }
31
+ current = path.join(current, segment);
32
+ const stat = await fs.lstat(current);
33
+ if (stat.isSymbolicLink()) {
34
+ throw new Error(errorMessage);
35
+ }
36
+ }
37
+ }
38
+ export async function sha256File(filePath) {
39
+ const hash = crypto.createHash("sha256");
40
+ for await (const chunk of createReadStream(filePath)) {
41
+ hash.update(chunk);
42
+ }
43
+ return hash.digest("hex");
44
+ }
@@ -13,7 +13,9 @@ export class BridgeSessionService {
13
13
  const redactionSecrets = exactSecrets(dispatch);
14
14
  const events = [];
15
15
  const emit = async (event) => {
16
- throwIfAborted(signal);
16
+ if (signal?.aborted) {
17
+ return;
18
+ }
17
19
  const redacted = redactSecrets(event, redactionSecrets);
18
20
  events.push(redacted);
19
21
  await sink.emit(redacted);
@@ -25,15 +27,39 @@ export class BridgeSessionService {
25
27
  content: "forge-bridge accepted run-once dispatch.",
26
28
  eventType: "dispatch_accepted"
27
29
  });
30
+ if (dispatch.draft) {
31
+ const run = await this.providerCli.runDraft({
32
+ dispatch,
33
+ emit,
34
+ signal
35
+ });
36
+ throwIfAborted(signal);
37
+ return redactSecrets({
38
+ schemaVersion: 1,
39
+ exitCode: 0,
40
+ summary: run.summary,
41
+ events,
42
+ spec: run.spec ?? null,
43
+ artifact: run.artifact ?? null,
44
+ artifacts: run.artifacts?.length ? run.artifacts : run.artifact ? [run.artifact] : []
45
+ }, redactionSecrets);
46
+ }
28
47
  const worktree = await this.worktrees.prepare({
29
48
  repo: dispatch.repo,
30
49
  workItem: dispatch.workItem,
31
50
  baseBranch: dispatch.cycleRun.baseBranch || dispatch.cycleRun.requestedBase,
51
+ worktreeKey: dispatch.lease.worktreeKey,
52
+ ownerType: dispatch.cycleRun.ownerType ?? dispatch.lease.ownerType ?? null,
53
+ ownerId: dispatch.cycleRun.ownerId ?? dispatch.lease.ownerId ?? null,
54
+ lane: dispatch.execution.lane,
55
+ phase: dispatch.cycleRun.phase,
32
56
  allowedRepoRoot: dispatch.serverLocal.repoRoot,
33
57
  cleanupWorktrees: dispatch.serverLocal.cleanupWorktrees,
34
58
  artifacts: dispatch.artifacts,
59
+ qaRework: dispatch.qaRework,
35
60
  warmPathOverrides: dispatch.warmPathOverrides
36
61
  });
62
+ const providerDispatch = withPreparedQaReworkPacket(dispatch, worktree.qaReworkPacketPath);
37
63
  throwIfAborted(signal);
38
64
  await emit({
39
65
  role: "runner",
@@ -49,22 +75,22 @@ export class BridgeSessionService {
49
75
  const lane = dispatch.execution.lane.toLowerCase();
50
76
  const role = dispatch.execution.role.toLowerCase();
51
77
  const isQa = role === "qa" || lane === "qa";
78
+ const isArchitect = role === "architect" || lane === "architect" || dispatch.cycleRun.kind.toLowerCase() === "architect";
52
79
  const isConflictResolve = role === "conflict_resolve" || lane === "conflict_resolve";
80
+ const context = {
81
+ dispatch: providerDispatch,
82
+ worktree,
83
+ emit,
84
+ signal
85
+ };
53
86
  const run = isQa
54
- ? await this.providerCli.runQa(dispatch, emit, signal)
55
- : isConflictResolve
56
- ? await this.providerCli.runConflictResolve({
57
- dispatch,
58
- worktree,
59
- emit,
60
- signal
61
- })
62
- : await this.providerCli.runImplementation({
63
- dispatch,
64
- worktree,
65
- emit,
66
- signal
67
- });
87
+ ? await this.providerCli.runQa(context)
88
+ : isArchitect
89
+ ? await this.providerCli.runArchitect(context)
90
+ : isConflictResolve
91
+ ? await this.providerCli.runConflictResolve(context)
92
+ : await this.providerCli.runImplementation(context);
93
+ throwIfAborted(signal);
68
94
  const phaseArtifacts = run.artifacts?.length ? run.artifacts : run.artifact ? [run.artifact] : [];
69
95
  return redactSecrets({
70
96
  schemaVersion: 1,
@@ -108,6 +134,18 @@ function validateApiKeySecretGrant(dispatch) {
108
134
  function exactSecrets(dispatch) {
109
135
  return dispatch.apiKeySecretGrant?.secret ? [dispatch.apiKeySecretGrant.secret] : [];
110
136
  }
137
+ function withPreparedQaReworkPacket(dispatch, packetPath) {
138
+ if (!dispatch.qaRework || !packetPath) {
139
+ return dispatch;
140
+ }
141
+ return {
142
+ ...dispatch,
143
+ qaRework: {
144
+ ...dispatch.qaRework,
145
+ packetPath
146
+ }
147
+ };
148
+ }
111
149
  async function emitDependencyWarmEvents(warm, emit) {
112
150
  if (warm.linked.length > 0) {
113
151
  await emit({
@@ -8,9 +8,120 @@ export interface RunOnceDispatchPacket {
8
8
  readonly lease: DispatchLease;
9
9
  readonly execution: DispatchExecution;
10
10
  readonly serverLocal: DispatchServerLocalConfig;
11
+ readonly pullRequest?: DispatchPullRequestContext | null;
11
12
  readonly apiKeySecretGrant?: ApiKeySecretGrant | null;
12
13
  readonly artifacts: readonly DispatchResolvedArtifact[];
13
14
  readonly warmPathOverrides?: readonly string[];
15
+ readonly qaBrowser?: DispatchQaBrowserContext | null;
16
+ readonly qaRework?: DispatchQaReworkContext | null;
17
+ readonly architectRevision?: DispatchArchitectRevisionContext | null;
18
+ readonly storyDevelop?: DispatchStoryDevelopContext | null;
19
+ readonly storyQa?: DispatchStoryQaContext | null;
20
+ readonly draft?: DispatchDraftPrompt | null;
21
+ }
22
+ export interface DispatchDraftPrompt {
23
+ readonly kind: string;
24
+ readonly systemPrompt: string;
25
+ readonly userPrompt: string;
26
+ }
27
+ export interface DispatchQaBrowserContext {
28
+ readonly required: boolean;
29
+ readonly launchTarget?: DispatchQaAppLaunchTarget | null;
30
+ readonly artifactsPath: string;
31
+ readonly missingTargetReason?: string | null;
32
+ }
33
+ export interface DispatchQaAppLaunchTarget {
34
+ readonly url?: string | null;
35
+ readonly command?: string | null;
36
+ readonly working_directory?: string | null;
37
+ readonly ready_url?: string | null;
38
+ readonly routes: readonly string[];
39
+ readonly notes?: string | null;
40
+ }
41
+ export interface DispatchQaReworkContext {
42
+ readonly attempt: number;
43
+ readonly maxAttempts: number;
44
+ readonly packetPath: string;
45
+ readonly packetJson: string;
46
+ readonly findingIds: readonly string[];
47
+ readonly findings: readonly Record<string, unknown>[];
48
+ }
49
+ export interface DispatchArchitectRevisionContext {
50
+ readonly priorPlanId: string;
51
+ readonly priorContentHash: string;
52
+ readonly feedback: string;
53
+ }
54
+ export interface DispatchStoryDevelopContext {
55
+ readonly storyWorkItemId: string;
56
+ readonly planId: string;
57
+ readonly planContentHash: string;
58
+ readonly architectTaskId: string;
59
+ readonly phaseId: string;
60
+ readonly dependencyWorkItemIds: readonly string[];
61
+ }
62
+ export interface DispatchStoryQaContext {
63
+ readonly storyWorkItemId: string;
64
+ readonly planId: string;
65
+ readonly planContentHash: string;
66
+ readonly worktreeKey: string;
67
+ readonly branch: string;
68
+ readonly implementationPlanJson?: string | null;
69
+ readonly qaPlanJson?: string | null;
70
+ readonly finalQaChecks: readonly string[];
71
+ readonly childTasks: readonly DispatchStoryQaChildTaskContext[];
72
+ readonly dependencies: readonly DispatchStoryQaDependencyContext[];
73
+ }
74
+ export interface DispatchStoryQaChildTaskContext {
75
+ readonly workItemId: string;
76
+ readonly title: string;
77
+ readonly architectTaskId: string;
78
+ readonly phaseId: string;
79
+ readonly phaseOrder: number;
80
+ readonly dependencyWorkItemIds: readonly string[];
81
+ readonly spec?: DispatchStoryQaChildSpecContext | null;
82
+ readonly implementationArtifact?: DispatchStoryQaArtifactContext | null;
83
+ readonly selfReviewArtifact?: DispatchStoryQaArtifactContext | null;
84
+ readonly validationArtifact?: DispatchStoryQaArtifactContext | null;
85
+ readonly qaArtifact?: DispatchStoryQaArtifactContext | null;
86
+ }
87
+ export interface DispatchStoryQaChildSpecContext {
88
+ readonly version: number;
89
+ readonly status: string;
90
+ readonly path: string;
91
+ readonly contentHash: string;
92
+ readonly body: string;
93
+ }
94
+ export interface DispatchStoryQaArtifactContext {
95
+ readonly artifactId: string;
96
+ readonly cycleRunId: string;
97
+ readonly phase: string;
98
+ readonly status: string;
99
+ readonly producedAt: string;
100
+ readonly path?: string | null;
101
+ readonly verdictJson?: string | null;
102
+ readonly evidenceJson?: string | null;
103
+ readonly findingsJson?: string | null;
104
+ readonly report?: string | null;
105
+ }
106
+ export interface DispatchStoryQaDependencyContext {
107
+ readonly fromWorkItemId: string;
108
+ readonly toWorkItemId: string;
109
+ }
110
+ export interface DispatchPullRequestContext {
111
+ readonly number: number;
112
+ readonly provider: string;
113
+ readonly title: string;
114
+ readonly sourceBranch: string;
115
+ readonly targetBranch: string;
116
+ readonly url: string;
117
+ readonly state: string;
118
+ readonly checkState: string;
119
+ readonly mergeState: string;
120
+ readonly requiredChecksPassed: number;
121
+ readonly requiredChecksTotal: number;
122
+ readonly unresolvedThreads: number;
123
+ readonly humanApproved: boolean;
124
+ readonly hasMergeConflicts: boolean;
14
125
  }
15
126
  export interface DispatchResolvedArtifact {
16
127
  readonly kind: string;
@@ -18,6 +129,7 @@ export interface DispatchResolvedArtifact {
18
129
  readonly scope: string;
19
130
  readonly body: string;
20
131
  readonly contentHash: string;
132
+ readonly version: number;
21
133
  readonly lane?: string | null;
22
134
  readonly event?: string | null;
23
135
  }
@@ -40,6 +152,9 @@ export interface DispatchRepo {
40
152
  }
41
153
  export interface DispatchCycleRun {
42
154
  readonly id: string;
155
+ readonly ownerType?: string | null;
156
+ readonly ownerId?: string | null;
157
+ readonly workItemId?: string | null;
43
158
  readonly kind: string;
44
159
  readonly phase: string;
45
160
  readonly requestedBase: string;
@@ -55,6 +170,9 @@ export interface DispatchSession {
55
170
  }
56
171
  export interface DispatchLease {
57
172
  readonly id: string;
173
+ readonly ownerType?: string | null;
174
+ readonly ownerId?: string | null;
175
+ readonly workItemId?: string | null;
58
176
  readonly worktreeKey: string;
59
177
  readonly lane: string;
60
178
  readonly fencingToken: string;
@@ -70,6 +188,7 @@ export interface DispatchExecution {
70
188
  readonly role: string;
71
189
  readonly lane: string;
72
190
  readonly requiresApiKeySecretGrant: boolean;
191
+ readonly runnerRepoLocalPath?: string | null;
73
192
  }
74
193
  export interface ApiKeySecretGrant {
75
194
  readonly grantId: string;
@@ -86,6 +205,10 @@ export interface RunnerDispatchEvent {
86
205
  readonly kind: string;
87
206
  readonly content: string;
88
207
  readonly eventType: string;
208
+ /** Provider/runner-reported event time (ISO-8601) when available, preserved for accurate durations. */
209
+ readonly occurredAt?: string | null;
210
+ /** Optional structured telemetry payload (JSON string), e.g. token usage for provider_usage events. */
211
+ readonly data?: string | null;
89
212
  }
90
213
  export interface RunnerDispatchSpec {
91
214
  readonly repoPath: string;
@@ -98,6 +221,11 @@ export interface RunnerDispatchArtifact {
98
221
  readonly repoPath: string;
99
222
  readonly path: string;
100
223
  readonly verdictJson: string;
224
+ readonly planJson?: string | null;
225
+ readonly evidenceJson?: string | null;
226
+ readonly findingsJson?: string | null;
227
+ readonly implementationPlanJson?: string | null;
228
+ readonly qaPlanJson?: string | null;
101
229
  readonly report: string;
102
230
  readonly producedBy: string;
103
231
  readonly status: string;