@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.
@@ -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);
@@ -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.718bdb0018b9";
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.718bdb0018b9";
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 (lane.trim().toLowerCase()) {
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
- if (request.artifacts.length > 0) {
41
- await injectWorktreeArtifacts(resolvedWorktreePath, request.artifacts);
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.718bdb0018b9",
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.0.0"
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
  }