@loucompanion/forge-bridge 0.1.1-dev.03b8ba168599 → 0.1.1-dev.2ac58b6f94ce
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/bridge/bridge.service.js +67 -8
- package/dist/bridge/bridge.types.d.ts +9 -0
- package/dist/cli/cli.service.d.ts +4 -0
- package/dist/cli/cli.service.js +21 -1
- package/dist/cli/local-config.js +18 -1
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +1 -0
- package/dist/session/internals.d.ts +3 -1
- package/dist/session/internals.js +1 -0
- package/dist/session/phase-completion.d.ts +101 -0
- package/dist/session/phase-completion.js +1985 -0
- package/dist/session/provider-cli/provider-cli.base.d.ts +1 -2
- package/dist/session/provider-cli/provider-cli.service.d.ts +7 -4
- package/dist/session/provider-cli/provider-cli.service.js +1333 -194
- package/dist/session/provider-cli/provider-cli.types.d.ts +1 -0
- package/dist/session/provider-cli/provider-usage.d.ts +25 -0
- package/dist/session/provider-cli/provider-usage.js +64 -0
- package/dist/session/qa-sidecar-validation.d.ts +3 -0
- package/dist/session/qa-sidecar-validation.js +44 -0
- package/dist/session/session.service.js +16 -15
- package/dist/session/session.types.d.ts +55 -0
- package/dist/session/session.types.js +119 -2
- package/dist/shared/cursor-agent/cursor-agent.launch.d.ts +9 -0
- package/dist/shared/cursor-agent/cursor-agent.launch.js +66 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.d.ts +1 -1
- package/dist/worktree/artifact-injector.js +9 -9
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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);
|
|
@@ -50,28 +52,27 @@ export class BridgeSessionService {
|
|
|
50
52
|
const role = dispatch.execution.role.toLowerCase();
|
|
51
53
|
const isQa = role === "qa" || lane === "qa";
|
|
52
54
|
const isConflictResolve = role === "conflict_resolve" || lane === "conflict_resolve";
|
|
55
|
+
const context = {
|
|
56
|
+
dispatch,
|
|
57
|
+
worktree,
|
|
58
|
+
emit,
|
|
59
|
+
signal
|
|
60
|
+
};
|
|
53
61
|
const run = isQa
|
|
54
|
-
? await this.providerCli.runQa(
|
|
62
|
+
? await this.providerCli.runQa(context)
|
|
55
63
|
: isConflictResolve
|
|
56
|
-
? await this.providerCli.runConflictResolve(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
signal
|
|
61
|
-
})
|
|
62
|
-
: await this.providerCli.runImplementation({
|
|
63
|
-
dispatch,
|
|
64
|
-
worktree,
|
|
65
|
-
emit,
|
|
66
|
-
signal
|
|
67
|
-
});
|
|
64
|
+
? await this.providerCli.runConflictResolve(context)
|
|
65
|
+
: await this.providerCli.runImplementation(context);
|
|
66
|
+
throwIfAborted(signal);
|
|
67
|
+
const phaseArtifacts = run.artifacts?.length ? run.artifacts : run.artifact ? [run.artifact] : [];
|
|
68
68
|
return redactSecrets({
|
|
69
69
|
schemaVersion: 1,
|
|
70
70
|
exitCode: 0,
|
|
71
71
|
summary: run.summary,
|
|
72
72
|
events,
|
|
73
73
|
spec: run.spec ?? null,
|
|
74
|
-
artifact: run.artifact ?? null
|
|
74
|
+
artifact: run.artifact ?? null,
|
|
75
|
+
artifacts: phaseArtifacts
|
|
75
76
|
}, redactionSecrets);
|
|
76
77
|
}
|
|
77
78
|
finally {
|
|
@@ -8,9 +8,50 @@ 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
|
+
}
|
|
18
|
+
export interface DispatchQaBrowserContext {
|
|
19
|
+
readonly required: boolean;
|
|
20
|
+
readonly launchTarget?: DispatchQaAppLaunchTarget | null;
|
|
21
|
+
readonly artifactsPath: string;
|
|
22
|
+
readonly missingTargetReason?: string | null;
|
|
23
|
+
}
|
|
24
|
+
export interface DispatchQaAppLaunchTarget {
|
|
25
|
+
readonly url?: string | null;
|
|
26
|
+
readonly command?: string | null;
|
|
27
|
+
readonly working_directory?: string | null;
|
|
28
|
+
readonly ready_url?: string | null;
|
|
29
|
+
readonly routes: readonly string[];
|
|
30
|
+
readonly notes?: string | null;
|
|
31
|
+
}
|
|
32
|
+
export interface DispatchQaReworkContext {
|
|
33
|
+
readonly attempt: number;
|
|
34
|
+
readonly maxAttempts: number;
|
|
35
|
+
readonly packetPath: string;
|
|
36
|
+
readonly packetJson: string;
|
|
37
|
+
readonly findingIds: readonly string[];
|
|
38
|
+
readonly findings: readonly Record<string, unknown>[];
|
|
39
|
+
}
|
|
40
|
+
export interface DispatchPullRequestContext {
|
|
41
|
+
readonly number: number;
|
|
42
|
+
readonly provider: string;
|
|
43
|
+
readonly title: string;
|
|
44
|
+
readonly sourceBranch: string;
|
|
45
|
+
readonly targetBranch: string;
|
|
46
|
+
readonly url: string;
|
|
47
|
+
readonly state: string;
|
|
48
|
+
readonly checkState: string;
|
|
49
|
+
readonly mergeState: string;
|
|
50
|
+
readonly requiredChecksPassed: number;
|
|
51
|
+
readonly requiredChecksTotal: number;
|
|
52
|
+
readonly unresolvedThreads: number;
|
|
53
|
+
readonly humanApproved: boolean;
|
|
54
|
+
readonly hasMergeConflicts: boolean;
|
|
14
55
|
}
|
|
15
56
|
export interface DispatchResolvedArtifact {
|
|
16
57
|
readonly kind: string;
|
|
@@ -18,6 +59,7 @@ export interface DispatchResolvedArtifact {
|
|
|
18
59
|
readonly scope: string;
|
|
19
60
|
readonly body: string;
|
|
20
61
|
readonly contentHash: string;
|
|
62
|
+
readonly version: number;
|
|
21
63
|
readonly lane?: string | null;
|
|
22
64
|
readonly event?: string | null;
|
|
23
65
|
}
|
|
@@ -40,6 +82,9 @@ export interface DispatchRepo {
|
|
|
40
82
|
}
|
|
41
83
|
export interface DispatchCycleRun {
|
|
42
84
|
readonly id: string;
|
|
85
|
+
readonly ownerType?: string | null;
|
|
86
|
+
readonly ownerId?: string | null;
|
|
87
|
+
readonly workItemId?: string | null;
|
|
43
88
|
readonly kind: string;
|
|
44
89
|
readonly phase: string;
|
|
45
90
|
readonly requestedBase: string;
|
|
@@ -55,6 +100,9 @@ export interface DispatchSession {
|
|
|
55
100
|
}
|
|
56
101
|
export interface DispatchLease {
|
|
57
102
|
readonly id: string;
|
|
103
|
+
readonly ownerType?: string | null;
|
|
104
|
+
readonly ownerId?: string | null;
|
|
105
|
+
readonly workItemId?: string | null;
|
|
58
106
|
readonly worktreeKey: string;
|
|
59
107
|
readonly lane: string;
|
|
60
108
|
readonly fencingToken: string;
|
|
@@ -70,6 +118,7 @@ export interface DispatchExecution {
|
|
|
70
118
|
readonly role: string;
|
|
71
119
|
readonly lane: string;
|
|
72
120
|
readonly requiresApiKeySecretGrant: boolean;
|
|
121
|
+
readonly runnerRepoLocalPath?: string | null;
|
|
73
122
|
}
|
|
74
123
|
export interface ApiKeySecretGrant {
|
|
75
124
|
readonly grantId: string;
|
|
@@ -86,6 +135,10 @@ export interface RunnerDispatchEvent {
|
|
|
86
135
|
readonly kind: string;
|
|
87
136
|
readonly content: string;
|
|
88
137
|
readonly eventType: string;
|
|
138
|
+
/** Provider/runner-reported event time (ISO-8601) when available, preserved for accurate durations. */
|
|
139
|
+
readonly occurredAt?: string | null;
|
|
140
|
+
/** Optional structured telemetry payload (JSON string), e.g. token usage for provider_usage events. */
|
|
141
|
+
readonly data?: string | null;
|
|
89
142
|
}
|
|
90
143
|
export interface RunnerDispatchSpec {
|
|
91
144
|
readonly repoPath: string;
|
|
@@ -101,6 +154,7 @@ export interface RunnerDispatchArtifact {
|
|
|
101
154
|
readonly report: string;
|
|
102
155
|
readonly producedBy: string;
|
|
103
156
|
readonly status: string;
|
|
157
|
+
readonly phase?: string | null;
|
|
104
158
|
}
|
|
105
159
|
export interface RunOnceResultPacket {
|
|
106
160
|
readonly schemaVersion: 1;
|
|
@@ -109,6 +163,7 @@ export interface RunOnceResultPacket {
|
|
|
109
163
|
readonly events: readonly RunnerDispatchEvent[];
|
|
110
164
|
readonly spec?: RunnerDispatchSpec | null;
|
|
111
165
|
readonly artifact?: RunnerDispatchArtifact | null;
|
|
166
|
+
readonly artifacts?: readonly RunnerDispatchArtifact[] | null;
|
|
112
167
|
}
|
|
113
168
|
export interface RunnerEventLine {
|
|
114
169
|
readonly type: "runner_event";
|
|
@@ -15,9 +15,12 @@ export function parseRunOnceDispatchPacket(value) {
|
|
|
15
15
|
lease: parseLease(packet.lease),
|
|
16
16
|
execution: parseExecution(packet.execution),
|
|
17
17
|
serverLocal: parseServerLocal(packet.serverLocal),
|
|
18
|
+
pullRequest: parsePullRequest(packet.pullRequest),
|
|
18
19
|
apiKeySecretGrant: parseApiKeySecretGrant(packet.apiKeySecretGrant),
|
|
19
20
|
artifacts: parseArtifacts(packet.artifacts),
|
|
20
|
-
warmPathOverrides: parseWarmPathOverrides(packet.warmPathOverrides)
|
|
21
|
+
warmPathOverrides: parseWarmPathOverrides(packet.warmPathOverrides),
|
|
22
|
+
qaBrowser: parseQaBrowser(packet.qaBrowser),
|
|
23
|
+
qaRework: parseQaRework(packet.qaRework)
|
|
21
24
|
};
|
|
22
25
|
}
|
|
23
26
|
export function failureResult(summary, events = []) {
|
|
@@ -55,6 +58,9 @@ function parseCycleRun(value) {
|
|
|
55
58
|
const cycle = asObject(value, "cycleRun");
|
|
56
59
|
return {
|
|
57
60
|
id: requiredString(cycle, "id"),
|
|
61
|
+
ownerType: optionalString(cycle, "ownerType"),
|
|
62
|
+
ownerId: optionalString(cycle, "ownerId"),
|
|
63
|
+
workItemId: optionalString(cycle, "workItemId"),
|
|
58
64
|
kind: requiredString(cycle, "kind"),
|
|
59
65
|
phase: requiredString(cycle, "phase"),
|
|
60
66
|
requestedBase: optionalString(cycle, "requestedBase") ?? "develop",
|
|
@@ -76,6 +82,9 @@ function parseLease(value) {
|
|
|
76
82
|
const lease = asObject(value, "lease");
|
|
77
83
|
return {
|
|
78
84
|
id: requiredString(lease, "id"),
|
|
85
|
+
ownerType: optionalString(lease, "ownerType"),
|
|
86
|
+
ownerId: optionalString(lease, "ownerId"),
|
|
87
|
+
workItemId: optionalString(lease, "workItemId"),
|
|
79
88
|
worktreeKey: requiredString(lease, "worktreeKey"),
|
|
80
89
|
lane: requiredString(lease, "lane"),
|
|
81
90
|
fencingToken: requiredString(lease, "fencingToken"),
|
|
@@ -93,7 +102,8 @@ function parseExecution(value) {
|
|
|
93
102
|
repoId: requiredString(execution, "repoId"),
|
|
94
103
|
role: requiredString(execution, "role"),
|
|
95
104
|
lane: requiredString(execution, "lane"),
|
|
96
|
-
requiresApiKeySecretGrant: requiredBoolean(execution, "requiresApiKeySecretGrant")
|
|
105
|
+
requiresApiKeySecretGrant: requiredBoolean(execution, "requiresApiKeySecretGrant"),
|
|
106
|
+
runnerRepoLocalPath: optionalString(execution, "runnerRepoLocalPath")
|
|
97
107
|
};
|
|
98
108
|
}
|
|
99
109
|
function parseApiKeySecretGrant(value) {
|
|
@@ -115,6 +125,28 @@ function parseServerLocal(value) {
|
|
|
115
125
|
cleanupWorktrees: requiredBoolean(serverLocal, "cleanupWorktrees")
|
|
116
126
|
};
|
|
117
127
|
}
|
|
128
|
+
function parsePullRequest(value) {
|
|
129
|
+
if (value === undefined || value === null) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const pullRequest = asObject(value, "pullRequest");
|
|
133
|
+
return {
|
|
134
|
+
number: requiredNumber(pullRequest, "number"),
|
|
135
|
+
provider: requiredString(pullRequest, "provider"),
|
|
136
|
+
title: optionalString(pullRequest, "title") ?? "",
|
|
137
|
+
sourceBranch: requiredString(pullRequest, "sourceBranch"),
|
|
138
|
+
targetBranch: requiredString(pullRequest, "targetBranch"),
|
|
139
|
+
url: optionalString(pullRequest, "url") ?? "",
|
|
140
|
+
state: requiredString(pullRequest, "state"),
|
|
141
|
+
checkState: requiredString(pullRequest, "checkState"),
|
|
142
|
+
mergeState: requiredString(pullRequest, "mergeState"),
|
|
143
|
+
requiredChecksPassed: requiredNumber(pullRequest, "requiredChecksPassed"),
|
|
144
|
+
requiredChecksTotal: requiredNumber(pullRequest, "requiredChecksTotal"),
|
|
145
|
+
unresolvedThreads: requiredNumber(pullRequest, "unresolvedThreads"),
|
|
146
|
+
humanApproved: requiredBoolean(pullRequest, "humanApproved"),
|
|
147
|
+
hasMergeConflicts: requiredBoolean(pullRequest, "hasMergeConflicts")
|
|
148
|
+
};
|
|
149
|
+
}
|
|
118
150
|
function parseWarmPathOverrides(value) {
|
|
119
151
|
if (value === undefined || value === null) {
|
|
120
152
|
return [];
|
|
@@ -138,6 +170,70 @@ function parseWarmPathOverride(entry, index) {
|
|
|
138
170
|
}
|
|
139
171
|
return segments.join("/");
|
|
140
172
|
}
|
|
173
|
+
function parseQaBrowser(value) {
|
|
174
|
+
if (value === undefined || value === null) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
const qaBrowser = asObject(value, "qaBrowser");
|
|
178
|
+
return {
|
|
179
|
+
required: optionalBoolean(qaBrowser, "required") ?? false,
|
|
180
|
+
launchTarget: parseQaAppLaunchTarget(qaBrowser.launchTarget ?? qaBrowser.launch_target),
|
|
181
|
+
artifactsPath: optionalString(qaBrowser, "artifactsPath") ?? optionalString(qaBrowser, "artifacts_path") ?? "",
|
|
182
|
+
missingTargetReason: optionalString(qaBrowser, "missingTargetReason") ?? optionalString(qaBrowser, "missing_target_reason")
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function parseQaAppLaunchTarget(value) {
|
|
186
|
+
if (value === undefined || value === null) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const launchTarget = asObject(value, "qaBrowser.launchTarget");
|
|
190
|
+
const routes = launchTarget.routes;
|
|
191
|
+
if (routes !== undefined && routes !== null && !Array.isArray(routes)) {
|
|
192
|
+
throw new Error("qaBrowser.launchTarget.routes must be an array when provided.");
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
url: optionalString(launchTarget, "url"),
|
|
196
|
+
command: optionalString(launchTarget, "command"),
|
|
197
|
+
working_directory: optionalString(launchTarget, "working_directory"),
|
|
198
|
+
ready_url: optionalString(launchTarget, "ready_url"),
|
|
199
|
+
routes: Array.isArray(routes)
|
|
200
|
+
? routes.map((entry, index) => {
|
|
201
|
+
if (typeof entry !== "string") {
|
|
202
|
+
throw new Error(`qaBrowser.launchTarget.routes[${index}] must be a string.`);
|
|
203
|
+
}
|
|
204
|
+
return entry;
|
|
205
|
+
}).filter((entry) => entry.trim().length > 0)
|
|
206
|
+
: [],
|
|
207
|
+
notes: optionalString(launchTarget, "notes")
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function parseQaRework(value) {
|
|
211
|
+
if (value === undefined || value === null) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
const qaRework = asObject(value, "qaRework");
|
|
215
|
+
const findingIdsValue = qaRework.findingIds;
|
|
216
|
+
if (!Array.isArray(findingIdsValue)) {
|
|
217
|
+
throw new Error("qaRework.findingIds must be an array.");
|
|
218
|
+
}
|
|
219
|
+
const findingsValue = qaRework.findings;
|
|
220
|
+
if (!Array.isArray(findingsValue)) {
|
|
221
|
+
throw new Error("qaRework.findings must be an array.");
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
attempt: requiredNumber(qaRework, "attempt"),
|
|
225
|
+
maxAttempts: requiredNumber(qaRework, "maxAttempts"),
|
|
226
|
+
packetPath: requiredString(qaRework, "packetPath"),
|
|
227
|
+
packetJson: requiredString(qaRework, "packetJson"),
|
|
228
|
+
findingIds: findingIdsValue.map((entry, index) => {
|
|
229
|
+
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
230
|
+
throw new Error(`qaRework.findingIds[${index}] must be a non-empty string.`);
|
|
231
|
+
}
|
|
232
|
+
return entry;
|
|
233
|
+
}),
|
|
234
|
+
findings: findingsValue.map((entry, index) => asObject(entry, `qaRework.findings[${index}]`))
|
|
235
|
+
};
|
|
236
|
+
}
|
|
141
237
|
function parseArtifacts(value) {
|
|
142
238
|
if (value === undefined || value === null) {
|
|
143
239
|
return [];
|
|
@@ -153,6 +249,7 @@ function parseArtifacts(value) {
|
|
|
153
249
|
scope: requiredString(artifact, "scope"),
|
|
154
250
|
body: optionalString(artifact, "body") ?? "",
|
|
155
251
|
contentHash: requiredString(artifact, "contentHash"),
|
|
252
|
+
version: optionalPositiveInteger(artifact, "version") ?? 1,
|
|
156
253
|
lane: optionalString(artifact, "lane"),
|
|
157
254
|
event: optionalString(artifact, "event")
|
|
158
255
|
};
|
|
@@ -188,6 +285,26 @@ function requiredBoolean(source, key) {
|
|
|
188
285
|
}
|
|
189
286
|
return value;
|
|
190
287
|
}
|
|
288
|
+
function optionalBoolean(source, key) {
|
|
289
|
+
const value = source[key];
|
|
290
|
+
if (value === undefined || value === null) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
if (typeof value !== "boolean") {
|
|
294
|
+
throw new Error(`${key} must be a boolean when provided.`);
|
|
295
|
+
}
|
|
296
|
+
return value;
|
|
297
|
+
}
|
|
298
|
+
function optionalPositiveInteger(source, key) {
|
|
299
|
+
const value = source[key];
|
|
300
|
+
if (value === undefined || value === null) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
|
304
|
+
throw new Error(`${key} must be a positive integer when provided.`);
|
|
305
|
+
}
|
|
306
|
+
return value;
|
|
307
|
+
}
|
|
191
308
|
function requiredNumber(source, key) {
|
|
192
309
|
const value = source[key];
|
|
193
310
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface CursorAgentLaunch {
|
|
2
|
+
readonly command: string;
|
|
3
|
+
readonly args: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
export declare function isCursorAgentCommand(command: string): boolean;
|
|
6
|
+
export declare function resolveCursorAgentDirectLaunch(command: string, args: readonly string[]): Promise<CursorAgentLaunch | null>;
|
|
7
|
+
export declare function findLatestCursorAgentVersionDir(versionsRoot: string): Promise<string | null>;
|
|
8
|
+
export declare function compareCursorAgentVersionDirNames(left: string, right: string): number;
|
|
9
|
+
export declare function parseCursorAgentVersionSortKey(name: string): number;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const cursorAgentVersionDirPattern = /^\d{4}\.\d{1,2}\.\d{1,2}-.+/;
|
|
4
|
+
export function isCursorAgentCommand(command) {
|
|
5
|
+
const normalized = command.trim().toLowerCase();
|
|
6
|
+
return normalized === "cursor-agent" || normalized === "agent";
|
|
7
|
+
}
|
|
8
|
+
export async function resolveCursorAgentDirectLaunch(command, args) {
|
|
9
|
+
if (!isCursorAgentCommand(command)) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
13
|
+
if (!localAppData) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const versionDir = await findLatestCursorAgentVersionDir(path.join(localAppData, "cursor-agent", "versions"));
|
|
17
|
+
if (!versionDir) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const nodePath = path.join(versionDir, "node.exe");
|
|
21
|
+
const indexPath = path.join(versionDir, "index.js");
|
|
22
|
+
if (!(await fileExists(nodePath)) || !(await fileExists(indexPath))) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
command: nodePath,
|
|
27
|
+
args: [indexPath, ...args]
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export async function findLatestCursorAgentVersionDir(versionsRoot) {
|
|
31
|
+
let entries;
|
|
32
|
+
try {
|
|
33
|
+
entries = await fs.readdir(versionsRoot);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const latestName = entries
|
|
39
|
+
.filter((name) => cursorAgentVersionDirPattern.test(name))
|
|
40
|
+
.sort(compareCursorAgentVersionDirNames)
|
|
41
|
+
.at(-1);
|
|
42
|
+
return latestName ? path.join(versionsRoot, latestName) : null;
|
|
43
|
+
}
|
|
44
|
+
export function compareCursorAgentVersionDirNames(left, right) {
|
|
45
|
+
const leftKey = parseCursorAgentVersionSortKey(left);
|
|
46
|
+
const rightKey = parseCursorAgentVersionSortKey(right);
|
|
47
|
+
if (leftKey !== rightKey) {
|
|
48
|
+
return leftKey - rightKey;
|
|
49
|
+
}
|
|
50
|
+
return left.localeCompare(right);
|
|
51
|
+
}
|
|
52
|
+
export function parseCursorAgentVersionSortKey(name) {
|
|
53
|
+
const datePart = name.split("-")[0] ?? "";
|
|
54
|
+
const parts = datePart.split(".");
|
|
55
|
+
if (parts.length !== 3) {
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
const year = parts[0] ?? "";
|
|
59
|
+
const month = (parts[1] ?? "").padStart(2, "0");
|
|
60
|
+
const day = (parts[2] ?? "").padStart(2, "0");
|
|
61
|
+
const numeric = Number(`${year}${month}${day}`);
|
|
62
|
+
return Number.isFinite(numeric) ? numeric : 0;
|
|
63
|
+
}
|
|
64
|
+
async function fileExists(inputPath) {
|
|
65
|
+
return fs.stat(inputPath).then((stat) => stat.isFile()).catch(() => false);
|
|
66
|
+
}
|
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.2ac58b6f94ce";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const forgeBridgeVersion = "0.1.1-dev.
|
|
1
|
+
export const forgeBridgeVersion = "0.1.1-dev.2ac58b6f94ce";
|
|
@@ -15,11 +15,13 @@ export async function writeArtifact(worktreePath, artifact) {
|
|
|
15
15
|
const scope = safeArtifactSegment("scope", artifact.scope);
|
|
16
16
|
const slug = safeArtifactSegment("slug", artifact.slug);
|
|
17
17
|
const forgeRoot = path.resolve(worktreePath, ".forge");
|
|
18
|
-
const
|
|
19
|
-
assertInside(forgeRoot,
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
const scopeDir = path.resolve(forgeRoot, kindFolder, scope);
|
|
19
|
+
assertInside(forgeRoot, scopeDir, "Artifact directory");
|
|
20
|
+
const filePath = normalizedArtifactKind(artifact.kind) === "skill"
|
|
21
|
+
? path.resolve(scopeDir, slug, "SKILL.md")
|
|
22
|
+
: path.resolve(scopeDir, `${slug}.md`);
|
|
22
23
|
assertInside(forgeRoot, filePath, "Artifact file");
|
|
24
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
23
25
|
await fs.writeFile(filePath, artifact.body, "utf8");
|
|
24
26
|
}
|
|
25
27
|
export function selectLaneCommand(artifacts, lane, phase) {
|
|
@@ -27,10 +29,7 @@ export function selectLaneCommand(artifacts, lane, phase) {
|
|
|
27
29
|
return artifacts.find((artifact) => normalizedArtifactKind(artifact.kind) === "command" && artifact.slug.trim().toLowerCase() === slug);
|
|
28
30
|
}
|
|
29
31
|
export function resolveCommandSlug(lane, phase) {
|
|
30
|
-
const normalizedPhase = phase.trim().toLowerCase();
|
|
31
|
-
if (normalizedPhase === "self_review") {
|
|
32
|
-
return "self-review-prompt";
|
|
33
|
-
}
|
|
32
|
+
const normalizedPhase = phase.trim().toLowerCase().replace(/-/g, "_");
|
|
34
33
|
if (normalizedPhase === "rework") {
|
|
35
34
|
return "rework-prompt";
|
|
36
35
|
}
|
|
@@ -43,12 +42,13 @@ export function resolveCommandSlug(lane, phase) {
|
|
|
43
42
|
return "dev-prompt";
|
|
44
43
|
}
|
|
45
44
|
}
|
|
46
|
-
export function buildSpecBody(laneCommand, workItem) {
|
|
45
|
+
export function buildSpecBody(laneCommand, workItem, phase = "implementation") {
|
|
47
46
|
const prompt = laneCommand.body.replaceAll("{workItemId}", workItem.id);
|
|
48
47
|
return `---
|
|
49
48
|
work_item_id: ${workItem.id}
|
|
50
49
|
status: approved
|
|
51
50
|
lane: ${laneCommand.lane ?? "develop"}
|
|
51
|
+
phase: ${phase}
|
|
52
52
|
command_slug: ${laneCommand.slug}
|
|
53
53
|
---
|
|
54
54
|
|
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.2ac58b6f94ce",
|
|
4
4
|
"description": "Forge runner bridge CLI for connecting local developer machines to Forge dispatch.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|