@loucompanion/forge-bridge 0.1.1-dev.718bdb0018b9 → 0.1.1-dev.895607f4402a
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 +39 -8
- package/dist/session/internals.d.ts +1 -1
- package/dist/session/phase-completion.d.ts +27 -2
- package/dist/session/phase-completion.js +831 -25
- package/dist/session/provider-cli/provider-cli.service.js +339 -131
- package/dist/session/session.service.js +6 -1
- package/dist/session/session.types.d.ts +23 -0
- package/dist/session/session.types.js +29 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.d.ts +1 -0
- package/dist/worktree/artifact-injector.js +30 -1
- package/dist/worktree/worktree.service.js +5 -3
- package/dist/worktree/worktree.types.d.ts +2 -0
- package/package.json +8 -3
|
@@ -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);
|
|
@@ -29,6 +31,8 @@ export class BridgeSessionService {
|
|
|
29
31
|
repo: dispatch.repo,
|
|
30
32
|
workItem: dispatch.workItem,
|
|
31
33
|
baseBranch: dispatch.cycleRun.baseBranch || dispatch.cycleRun.requestedBase,
|
|
34
|
+
lane: dispatch.execution.lane,
|
|
35
|
+
phase: dispatch.cycleRun.phase,
|
|
32
36
|
allowedRepoRoot: dispatch.serverLocal.repoRoot,
|
|
33
37
|
cleanupWorktrees: dispatch.serverLocal.cleanupWorktrees,
|
|
34
38
|
artifacts: dispatch.artifacts,
|
|
@@ -61,6 +65,7 @@ export class BridgeSessionService {
|
|
|
61
65
|
: isConflictResolve
|
|
62
66
|
? await this.providerCli.runConflictResolve(context)
|
|
63
67
|
: await this.providerCli.runImplementation(context);
|
|
68
|
+
throwIfAborted(signal);
|
|
64
69
|
const phaseArtifacts = run.artifacts?.length ? run.artifacts : run.artifact ? [run.artifact] : [];
|
|
65
70
|
return redactSecrets({
|
|
66
71
|
schemaVersion: 1,
|
|
@@ -8,6 +8,7 @@ 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[];
|
|
@@ -36,6 +37,22 @@ export interface DispatchQaReworkContext {
|
|
|
36
37
|
readonly findingIds: readonly string[];
|
|
37
38
|
readonly findings: readonly Record<string, unknown>[];
|
|
38
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;
|
|
55
|
+
}
|
|
39
56
|
export interface DispatchResolvedArtifact {
|
|
40
57
|
readonly kind: string;
|
|
41
58
|
readonly slug: string;
|
|
@@ -65,6 +82,9 @@ export interface DispatchRepo {
|
|
|
65
82
|
}
|
|
66
83
|
export interface DispatchCycleRun {
|
|
67
84
|
readonly id: string;
|
|
85
|
+
readonly ownerType?: string | null;
|
|
86
|
+
readonly ownerId?: string | null;
|
|
87
|
+
readonly workItemId?: string | null;
|
|
68
88
|
readonly kind: string;
|
|
69
89
|
readonly phase: string;
|
|
70
90
|
readonly requestedBase: string;
|
|
@@ -80,6 +100,9 @@ export interface DispatchSession {
|
|
|
80
100
|
}
|
|
81
101
|
export interface DispatchLease {
|
|
82
102
|
readonly id: string;
|
|
103
|
+
readonly ownerType?: string | null;
|
|
104
|
+
readonly ownerId?: string | null;
|
|
105
|
+
readonly workItemId?: string | null;
|
|
83
106
|
readonly worktreeKey: string;
|
|
84
107
|
readonly lane: string;
|
|
85
108
|
readonly fencingToken: string;
|
|
@@ -15,6 +15,7 @@ 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
21
|
warmPathOverrides: parseWarmPathOverrides(packet.warmPathOverrides),
|
|
@@ -57,6 +58,9 @@ function parseCycleRun(value) {
|
|
|
57
58
|
const cycle = asObject(value, "cycleRun");
|
|
58
59
|
return {
|
|
59
60
|
id: requiredString(cycle, "id"),
|
|
61
|
+
ownerType: optionalString(cycle, "ownerType"),
|
|
62
|
+
ownerId: optionalString(cycle, "ownerId"),
|
|
63
|
+
workItemId: optionalString(cycle, "workItemId"),
|
|
60
64
|
kind: requiredString(cycle, "kind"),
|
|
61
65
|
phase: requiredString(cycle, "phase"),
|
|
62
66
|
requestedBase: optionalString(cycle, "requestedBase") ?? "develop",
|
|
@@ -78,6 +82,9 @@ function parseLease(value) {
|
|
|
78
82
|
const lease = asObject(value, "lease");
|
|
79
83
|
return {
|
|
80
84
|
id: requiredString(lease, "id"),
|
|
85
|
+
ownerType: optionalString(lease, "ownerType"),
|
|
86
|
+
ownerId: optionalString(lease, "ownerId"),
|
|
87
|
+
workItemId: optionalString(lease, "workItemId"),
|
|
81
88
|
worktreeKey: requiredString(lease, "worktreeKey"),
|
|
82
89
|
lane: requiredString(lease, "lane"),
|
|
83
90
|
fencingToken: requiredString(lease, "fencingToken"),
|
|
@@ -118,6 +125,28 @@ function parseServerLocal(value) {
|
|
|
118
125
|
cleanupWorktrees: requiredBoolean(serverLocal, "cleanupWorktrees")
|
|
119
126
|
};
|
|
120
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
|
+
}
|
|
121
150
|
function parseWarmPathOverrides(value) {
|
|
122
151
|
if (value === undefined || value === null) {
|
|
123
152
|
return [];
|
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.895607f4402a";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const forgeBridgeVersion = "0.1.1-dev.
|
|
1
|
+
export const forgeBridgeVersion = "0.1.1-dev.895607f4402a";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DispatchResolvedArtifact } from "../session/session.types.js";
|
|
2
2
|
export declare function injectWorktreeArtifacts(worktreePath: string, artifacts: readonly DispatchResolvedArtifact[]): Promise<void>;
|
|
3
|
+
export declare function filterArtifactsForLane(artifacts: readonly DispatchResolvedArtifact[], lane: string, phase: string): readonly DispatchResolvedArtifact[];
|
|
3
4
|
export declare function writeArtifact(worktreePath: string, artifact: DispatchResolvedArtifact): Promise<void>;
|
|
4
5
|
export declare function selectLaneCommand(artifacts: readonly DispatchResolvedArtifact[], lane: string, phase: string): DispatchResolvedArtifact | undefined;
|
|
5
6
|
export declare function resolveCommandSlug(lane: string, phase: string): string;
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
const DEBUG_OPERATIONAL_SKILLS = new Set(["debug-repro", "debug-root-cause", "debug-fix", "debug-regression"]);
|
|
5
|
+
const DEBUG_OPERATIONAL_HOOKS = new Set(["debug-gate", "debug-tool-hook", "debug-rework"]);
|
|
4
6
|
export async function injectWorktreeArtifacts(worktreePath, artifacts) {
|
|
5
7
|
for (const artifact of artifacts) {
|
|
6
8
|
await writeArtifact(worktreePath, artifact);
|
|
7
9
|
}
|
|
8
10
|
}
|
|
11
|
+
export function filterArtifactsForLane(artifacts, lane, phase) {
|
|
12
|
+
if (lane.trim().toLowerCase() !== "debug") {
|
|
13
|
+
return artifacts;
|
|
14
|
+
}
|
|
15
|
+
const commandSlug = resolveCommandSlug(lane, phase).toLowerCase();
|
|
16
|
+
return artifacts.filter((artifact) => shouldInjectDebugArtifact(artifact, commandSlug));
|
|
17
|
+
}
|
|
9
18
|
export async function writeArtifact(worktreePath, artifact) {
|
|
10
19
|
const writtenHash = sha256(artifact.body);
|
|
11
20
|
if (writtenHash !== artifact.contentHash.toLowerCase()) {
|
|
@@ -29,11 +38,15 @@ export function selectLaneCommand(artifacts, lane, phase) {
|
|
|
29
38
|
return artifacts.find((artifact) => normalizedArtifactKind(artifact.kind) === "command" && artifact.slug.trim().toLowerCase() === slug);
|
|
30
39
|
}
|
|
31
40
|
export function resolveCommandSlug(lane, phase) {
|
|
41
|
+
const normalizedLane = lane.trim().toLowerCase();
|
|
42
|
+
if (normalizedLane === "debug") {
|
|
43
|
+
return "debug-prompt";
|
|
44
|
+
}
|
|
32
45
|
const normalizedPhase = phase.trim().toLowerCase().replace(/-/g, "_");
|
|
33
46
|
if (normalizedPhase === "rework") {
|
|
34
47
|
return "rework-prompt";
|
|
35
48
|
}
|
|
36
|
-
switch (
|
|
49
|
+
switch (normalizedLane) {
|
|
37
50
|
case "architect":
|
|
38
51
|
return "architect-prompt";
|
|
39
52
|
case "qa":
|
|
@@ -42,6 +55,22 @@ export function resolveCommandSlug(lane, phase) {
|
|
|
42
55
|
return "dev-prompt";
|
|
43
56
|
}
|
|
44
57
|
}
|
|
58
|
+
function shouldInjectDebugArtifact(artifact, commandSlug) {
|
|
59
|
+
if (artifact.scope.trim().toLowerCase() !== "operational") {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const slug = artifact.slug.trim().toLowerCase();
|
|
63
|
+
switch (normalizedArtifactKind(artifact.kind)) {
|
|
64
|
+
case "command":
|
|
65
|
+
return slug === commandSlug;
|
|
66
|
+
case "skill":
|
|
67
|
+
return DEBUG_OPERATIONAL_SKILLS.has(slug);
|
|
68
|
+
case "hook":
|
|
69
|
+
return DEBUG_OPERATIONAL_HOOKS.has(slug);
|
|
70
|
+
default:
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
45
74
|
export function buildSpecBody(laneCommand, workItem, phase = "implementation") {
|
|
46
75
|
const prompt = laneCommand.body.replaceAll("{workItemId}", workItem.id);
|
|
47
76
|
return `---
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { GitClient } from "../shared/git/git.js";
|
|
4
4
|
import { ensureDirectory, isSubPath, resolveExistingDirectoryPath, safeSegment } from "../shared/path/path.js";
|
|
5
|
-
import { injectWorktreeArtifacts } from "./artifact-injector.js";
|
|
5
|
+
import { filterArtifactsForLane, injectWorktreeArtifacts } from "./artifact-injector.js";
|
|
6
6
|
import { warmWorktreeDependencies } from "./dependency-warmer.js";
|
|
7
7
|
export class GitWorktreeService {
|
|
8
8
|
git;
|
|
@@ -37,8 +37,9 @@ export class GitWorktreeService {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
await this.ensureGitIdentity(resolvedWorktreePath);
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const artifacts = filterArtifactsForLane(request.artifacts, request.lane, request.phase);
|
|
41
|
+
if (artifacts.length > 0) {
|
|
42
|
+
await injectWorktreeArtifacts(resolvedWorktreePath, artifacts);
|
|
42
43
|
}
|
|
43
44
|
const dependencyWarm = await warmWorktreeDependencies({
|
|
44
45
|
baseCheckoutPath: repoPath,
|
|
@@ -204,6 +205,7 @@ export function normalizeBranchName(value, fallback) {
|
|
|
204
205
|
branch.endsWith(".") ||
|
|
205
206
|
branch.toLowerCase().endsWith(".lock") ||
|
|
206
207
|
/\s/.test(branch) ||
|
|
208
|
+
// eslint-disable-next-line no-control-regex -- Git branch validation must reject ASCII control characters.
|
|
207
209
|
/[\u0000-\u001f~^:?*[\\]/.test(branch) ||
|
|
208
210
|
branch.split("/").some((part) => part.length === 0 || part.startsWith(".") || part.toLowerCase().endsWith(".lock"))) {
|
|
209
211
|
throw new Error("Git branch name must be valid, without whitespace, and cannot start with '-'.");
|
|
@@ -4,6 +4,8 @@ export interface WorktreePrepareRequest {
|
|
|
4
4
|
readonly repo: DispatchRepo;
|
|
5
5
|
readonly workItem: DispatchWorkItem;
|
|
6
6
|
readonly baseBranch: string;
|
|
7
|
+
readonly lane: string;
|
|
8
|
+
readonly phase: string;
|
|
7
9
|
readonly allowedRepoRoot: string;
|
|
8
10
|
readonly cleanupWorktrees: boolean;
|
|
9
11
|
readonly artifacts: readonly DispatchResolvedArtifact[];
|
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.895607f4402a",
|
|
4
4
|
"description": "Forge runner bridge CLI for connecting local developer machines to Forge dispatch.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"url": "https://github.com/coleman399/Forge/issues"
|
|
31
31
|
},
|
|
32
32
|
"engines": {
|
|
33
|
-
"node": ">=22.
|
|
33
|
+
"node": ">=22.13.0"
|
|
34
34
|
},
|
|
35
35
|
"publishConfig": {
|
|
36
36
|
"access": "public"
|
|
@@ -39,14 +39,19 @@
|
|
|
39
39
|
"build": "tsc -p tsconfig.json",
|
|
40
40
|
"test": "vitest run",
|
|
41
41
|
"test:watch": "vitest",
|
|
42
|
-
"smoke:package": "npm run build && node scripts/smoke-package.mjs"
|
|
42
|
+
"smoke:package": "npm run build && node scripts/smoke-package.mjs",
|
|
43
|
+
"lint": "eslint \"src/**/*.ts\" \"scripts/**/*.mjs\" \"vitest.config.ts\""
|
|
43
44
|
},
|
|
44
45
|
"dependencies": {
|
|
45
46
|
"@microsoft/signalr": "^8.0.17"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
49
|
+
"@eslint/js": "^10.0.1",
|
|
48
50
|
"@types/node": "^22.15.32",
|
|
51
|
+
"eslint": "^10.5.0",
|
|
52
|
+
"globals": "^17.6.0",
|
|
49
53
|
"typescript": "^5.8.3",
|
|
54
|
+
"typescript-eslint": "^8.61.1",
|
|
50
55
|
"vitest": "^3.2.3"
|
|
51
56
|
}
|
|
52
57
|
}
|