@loucompanion/forge-bridge 0.1.1-dev.03b8ba168599

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.
Files changed (80) hide show
  1. package/README.md +64 -0
  2. package/dist/bridge/bridge.base.d.ts +17 -0
  3. package/dist/bridge/bridge.base.js +1 -0
  4. package/dist/bridge/bridge.service.d.ts +45 -0
  5. package/dist/bridge/bridge.service.js +353 -0
  6. package/dist/bridge/bridge.types.d.ts +83 -0
  7. package/dist/bridge/bridge.types.js +1 -0
  8. package/dist/bridge/index.d.ts +2 -0
  9. package/dist/bridge/index.js +1 -0
  10. package/dist/bridge/internals.d.ts +3 -0
  11. package/dist/bridge/internals.js +1 -0
  12. package/dist/cleanup/cleanup.base.d.ts +4 -0
  13. package/dist/cleanup/cleanup.base.js +1 -0
  14. package/dist/cleanup/cleanup.service.d.ts +5 -0
  15. package/dist/cleanup/cleanup.service.js +5 -0
  16. package/dist/cleanup/cleanup.types.d.ts +6 -0
  17. package/dist/cleanup/cleanup.types.js +1 -0
  18. package/dist/cleanup/index.d.ts +2 -0
  19. package/dist/cleanup/index.js +1 -0
  20. package/dist/cleanup/internals.d.ts +3 -0
  21. package/dist/cleanup/internals.js +1 -0
  22. package/dist/cli/bin.d.ts +2 -0
  23. package/dist/cli/bin.js +4 -0
  24. package/dist/cli/cli.base.d.ts +7 -0
  25. package/dist/cli/cli.base.js +1 -0
  26. package/dist/cli/cli.service.d.ts +66 -0
  27. package/dist/cli/cli.service.js +770 -0
  28. package/dist/cli/index.d.ts +2 -0
  29. package/dist/cli/index.js +1 -0
  30. package/dist/cli/internals.d.ts +1 -0
  31. package/dist/cli/internals.js +1 -0
  32. package/dist/cli/local-config.d.ts +34 -0
  33. package/dist/cli/local-config.js +181 -0
  34. package/dist/config.d.ts +5 -0
  35. package/dist/config.js +8 -0
  36. package/dist/index.d.ts +7 -0
  37. package/dist/index.js +7 -0
  38. package/dist/session/index.d.ts +2 -0
  39. package/dist/session/index.js +1 -0
  40. package/dist/session/internals.d.ts +7 -0
  41. package/dist/session/internals.js +2 -0
  42. package/dist/session/provider-cli/index.d.ts +3 -0
  43. package/dist/session/provider-cli/index.js +1 -0
  44. package/dist/session/provider-cli/provider-cli.base.d.ts +7 -0
  45. package/dist/session/provider-cli/provider-cli.base.js +1 -0
  46. package/dist/session/provider-cli/provider-cli.service.d.ts +58 -0
  47. package/dist/session/provider-cli/provider-cli.service.js +705 -0
  48. package/dist/session/provider-cli/provider-cli.types.d.ts +13 -0
  49. package/dist/session/provider-cli/provider-cli.types.js +1 -0
  50. package/dist/session/session.base.d.ts +7 -0
  51. package/dist/session/session.base.js +1 -0
  52. package/dist/session/session.service.d.ts +10 -0
  53. package/dist/session/session.service.js +134 -0
  54. package/dist/session/session.types.d.ts +118 -0
  55. package/dist/session/session.types.js +197 -0
  56. package/dist/shared/git/git.d.ts +6 -0
  57. package/dist/shared/git/git.js +18 -0
  58. package/dist/shared/path/path.d.ts +4 -0
  59. package/dist/shared/path/path.js +29 -0
  60. package/dist/shared/process/process.d.ts +16 -0
  61. package/dist/shared/process/process.js +32 -0
  62. package/dist/shared/redaction/redaction.d.ts +2 -0
  63. package/dist/shared/redaction/redaction.js +30 -0
  64. package/dist/version.d.ts +1 -0
  65. package/dist/version.js +1 -0
  66. package/dist/worktree/artifact-injector.d.ts +11 -0
  67. package/dist/worktree/artifact-injector.js +104 -0
  68. package/dist/worktree/dependency-warmer.d.ts +32 -0
  69. package/dist/worktree/dependency-warmer.js +297 -0
  70. package/dist/worktree/index.d.ts +4 -0
  71. package/dist/worktree/index.js +2 -0
  72. package/dist/worktree/internals.d.ts +3 -0
  73. package/dist/worktree/internals.js +1 -0
  74. package/dist/worktree/worktree.base.d.ts +5 -0
  75. package/dist/worktree/worktree.base.js +1 -0
  76. package/dist/worktree/worktree.service.d.ts +16 -0
  77. package/dist/worktree/worktree.service.js +301 -0
  78. package/dist/worktree/worktree.types.d.ts +19 -0
  79. package/dist/worktree/worktree.types.js +1 -0
  80. package/package.json +52 -0
@@ -0,0 +1,13 @@
1
+ import type { RunOnceDispatchPacket, RunnerDispatchArtifact, RunnerDispatchEvent, RunnerDispatchSpec } from "../session.types.js";
2
+ import type { WorktreeHandle } from "../../worktree/worktree.types.js";
3
+ export interface ProviderCliRunContext {
4
+ readonly dispatch: RunOnceDispatchPacket;
5
+ readonly worktree: WorktreeHandle;
6
+ readonly signal?: AbortSignal;
7
+ emit(event: RunnerDispatchEvent): Promise<void>;
8
+ }
9
+ export interface ProviderCliRunResult {
10
+ readonly summary: string;
11
+ readonly spec?: RunnerDispatchSpec | null;
12
+ readonly artifact?: RunnerDispatchArtifact | null;
13
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import type { RunOnceDispatchPacket, RunOnceResultPacket, RunnerDispatchEvent } from "./session.types.js";
2
+ export interface SessionEventSink {
3
+ emit(event: RunnerDispatchEvent): Promise<void>;
4
+ }
5
+ export interface SessionService {
6
+ runOnce(dispatch: RunOnceDispatchPacket, sink: SessionEventSink, signal?: AbortSignal): Promise<RunOnceResultPacket>;
7
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import type { ProviderCliService } from "./provider-cli/provider-cli.base.js";
2
+ import type { SessionEventSink, SessionService } from "./session.base.js";
3
+ import type { RunOnceDispatchPacket, RunOnceResultPacket } from "./session.types.js";
4
+ import type { WorktreeService } from "../worktree/worktree.base.js";
5
+ export declare class BridgeSessionService implements SessionService {
6
+ private readonly worktrees;
7
+ private readonly providerCli;
8
+ constructor(worktrees?: WorktreeService, providerCli?: ProviderCliService);
9
+ runOnce(dispatch: RunOnceDispatchPacket, sink: SessionEventSink, signal?: AbortSignal): Promise<RunOnceResultPacket>;
10
+ }
@@ -0,0 +1,134 @@
1
+ import { redactSecrets } from "../shared/redaction/redaction.js";
2
+ import { RoutingProviderCliService } from "./provider-cli/provider-cli.service.js";
3
+ import { GitWorktreeService } from "../worktree/worktree.service.js";
4
+ export class BridgeSessionService {
5
+ worktrees;
6
+ providerCli;
7
+ constructor(worktrees = new GitWorktreeService(), providerCli = new RoutingProviderCliService()) {
8
+ this.worktrees = worktrees;
9
+ this.providerCli = providerCli;
10
+ }
11
+ async runOnce(dispatch, sink, signal) {
12
+ validateApiKeySecretGrant(dispatch);
13
+ const redactionSecrets = exactSecrets(dispatch);
14
+ const events = [];
15
+ const emit = async (event) => {
16
+ throwIfAborted(signal);
17
+ const redacted = redactSecrets(event, redactionSecrets);
18
+ events.push(redacted);
19
+ await sink.emit(redacted);
20
+ };
21
+ throwIfAborted(signal);
22
+ await emit({
23
+ role: "runner",
24
+ kind: "checkpoint",
25
+ content: "forge-bridge accepted run-once dispatch.",
26
+ eventType: "dispatch_accepted"
27
+ });
28
+ const worktree = await this.worktrees.prepare({
29
+ repo: dispatch.repo,
30
+ workItem: dispatch.workItem,
31
+ baseBranch: dispatch.cycleRun.baseBranch || dispatch.cycleRun.requestedBase,
32
+ allowedRepoRoot: dispatch.serverLocal.repoRoot,
33
+ cleanupWorktrees: dispatch.serverLocal.cleanupWorktrees,
34
+ artifacts: dispatch.artifacts,
35
+ warmPathOverrides: dispatch.warmPathOverrides
36
+ });
37
+ throwIfAborted(signal);
38
+ await emit({
39
+ role: "runner",
40
+ kind: "checkpoint",
41
+ content: `Prepared worktree ${worktree.branchName}.`,
42
+ eventType: "worktree_prepared"
43
+ });
44
+ if (worktree.dependencyWarm) {
45
+ await emitDependencyWarmEvents(worktree.dependencyWarm, emit);
46
+ }
47
+ try {
48
+ throwIfAborted(signal);
49
+ const lane = dispatch.execution.lane.toLowerCase();
50
+ const role = dispatch.execution.role.toLowerCase();
51
+ const isQa = role === "qa" || lane === "qa";
52
+ const isConflictResolve = role === "conflict_resolve" || lane === "conflict_resolve";
53
+ 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
+ });
68
+ return redactSecrets({
69
+ schemaVersion: 1,
70
+ exitCode: 0,
71
+ summary: run.summary,
72
+ events,
73
+ spec: run.spec ?? null,
74
+ artifact: run.artifact ?? null
75
+ }, redactionSecrets);
76
+ }
77
+ finally {
78
+ await this.worktrees.release(worktree);
79
+ }
80
+ }
81
+ }
82
+ function throwIfAborted(signal) {
83
+ if (signal?.aborted) {
84
+ throw new Error(String(signal.reason ?? "Dispatch cancelled."));
85
+ }
86
+ }
87
+ function validateApiKeySecretGrant(dispatch) {
88
+ if (!dispatch.execution.requiresApiKeySecretGrant) {
89
+ return;
90
+ }
91
+ if (dispatch.execution.authKind !== "api_key") {
92
+ throw new Error("API-key secret grant requires api_key execution.");
93
+ }
94
+ const grant = dispatch.apiKeySecretGrant;
95
+ if (!grant?.secret) {
96
+ throw new Error("API-key secret grant is missing.");
97
+ }
98
+ if (grant.provider.toLowerCase() !== dispatch.execution.provider.toLowerCase()) {
99
+ throw new Error("API-key secret grant provider does not match dispatch provider.");
100
+ }
101
+ const expiresAt = Date.parse(grant.expiresAt);
102
+ if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) {
103
+ throw new Error("API-key secret grant is expired.");
104
+ }
105
+ }
106
+ function exactSecrets(dispatch) {
107
+ return dispatch.apiKeySecretGrant?.secret ? [dispatch.apiKeySecretGrant.secret] : [];
108
+ }
109
+ async function emitDependencyWarmEvents(warm, emit) {
110
+ if (warm.linked.length > 0) {
111
+ await emit({
112
+ role: "runner",
113
+ kind: "checkpoint",
114
+ content: `Warmed dependencies via link: ${warm.linked.join(", ")}.`,
115
+ eventType: "dependencies_warmed"
116
+ });
117
+ }
118
+ for (const fallback of warm.fallbacks) {
119
+ await emit({
120
+ role: "runner",
121
+ kind: "checkpoint",
122
+ content: `Dependency install fallback (${fallback.ecosystem}): ${fallback.reason} Ran \`${fallback.command}\` in ${fallback.packageRoot}.`,
123
+ eventType: "dependencies_install_fallback"
124
+ });
125
+ }
126
+ if (warm.skipped.length > 0) {
127
+ await emit({
128
+ role: "runner",
129
+ kind: "checkpoint",
130
+ content: `Skipped warming missing dependency paths: ${warm.skipped.join(", ")}.`,
131
+ eventType: "dependencies_warm_skipped"
132
+ });
133
+ }
134
+ }
@@ -0,0 +1,118 @@
1
+ export interface RunOnceDispatchPacket {
2
+ readonly schemaVersion: 1;
3
+ readonly userId: string;
4
+ readonly workItem: DispatchWorkItem;
5
+ readonly repo: DispatchRepo;
6
+ readonly cycleRun: DispatchCycleRun;
7
+ readonly session: DispatchSession;
8
+ readonly lease: DispatchLease;
9
+ readonly execution: DispatchExecution;
10
+ readonly serverLocal: DispatchServerLocalConfig;
11
+ readonly apiKeySecretGrant?: ApiKeySecretGrant | null;
12
+ readonly artifacts: readonly DispatchResolvedArtifact[];
13
+ readonly warmPathOverrides?: readonly string[];
14
+ }
15
+ export interface DispatchResolvedArtifact {
16
+ readonly kind: string;
17
+ readonly slug: string;
18
+ readonly scope: string;
19
+ readonly body: string;
20
+ readonly contentHash: string;
21
+ readonly lane?: string | null;
22
+ readonly event?: string | null;
23
+ }
24
+ export interface DispatchWorkItem {
25
+ readonly id: string;
26
+ readonly kind: string;
27
+ readonly title: string;
28
+ readonly description: string;
29
+ readonly acceptanceCriteria: string;
30
+ readonly reproSteps: string;
31
+ readonly systemInfo: string;
32
+ }
33
+ export interface DispatchRepo {
34
+ readonly id: string;
35
+ readonly name: string;
36
+ readonly remoteUrl: string;
37
+ readonly defaultBranch: string;
38
+ readonly cloneOnDemand: boolean;
39
+ readonly serverLocalPath?: string | null;
40
+ }
41
+ export interface DispatchCycleRun {
42
+ readonly id: string;
43
+ readonly kind: string;
44
+ readonly phase: string;
45
+ readonly requestedBase: string;
46
+ readonly baseBranch: string;
47
+ readonly expectedCurrentBranch?: string | null;
48
+ readonly attempt: number;
49
+ readonly model: string;
50
+ }
51
+ export interface DispatchSession {
52
+ readonly id: string;
53
+ readonly model: string;
54
+ readonly lane: string;
55
+ }
56
+ export interface DispatchLease {
57
+ readonly id: string;
58
+ readonly worktreeKey: string;
59
+ readonly lane: string;
60
+ readonly fencingToken: string;
61
+ readonly expiresAt: string;
62
+ }
63
+ export interface DispatchExecution {
64
+ readonly provider: string;
65
+ readonly model: string;
66
+ readonly authKind: string;
67
+ readonly location: string;
68
+ readonly runnerId?: string | null;
69
+ readonly repoId: string;
70
+ readonly role: string;
71
+ readonly lane: string;
72
+ readonly requiresApiKeySecretGrant: boolean;
73
+ }
74
+ export interface ApiKeySecretGrant {
75
+ readonly grantId: string;
76
+ readonly provider: string;
77
+ readonly secret: string;
78
+ readonly expiresAt: string;
79
+ }
80
+ export interface DispatchServerLocalConfig {
81
+ readonly repoRoot: string;
82
+ readonly cleanupWorktrees: boolean;
83
+ }
84
+ export interface RunnerDispatchEvent {
85
+ readonly role: string;
86
+ readonly kind: string;
87
+ readonly content: string;
88
+ readonly eventType: string;
89
+ }
90
+ export interface RunnerDispatchSpec {
91
+ readonly repoPath: string;
92
+ readonly path: string;
93
+ readonly body: string;
94
+ readonly producedBy: string;
95
+ readonly status: string;
96
+ }
97
+ export interface RunnerDispatchArtifact {
98
+ readonly repoPath: string;
99
+ readonly path: string;
100
+ readonly verdictJson: string;
101
+ readonly report: string;
102
+ readonly producedBy: string;
103
+ readonly status: string;
104
+ }
105
+ export interface RunOnceResultPacket {
106
+ readonly schemaVersion: 1;
107
+ readonly exitCode: number;
108
+ readonly summary: string;
109
+ readonly events: readonly RunnerDispatchEvent[];
110
+ readonly spec?: RunnerDispatchSpec | null;
111
+ readonly artifact?: RunnerDispatchArtifact | null;
112
+ }
113
+ export interface RunnerEventLine {
114
+ readonly type: "runner_event";
115
+ readonly event: RunnerDispatchEvent;
116
+ }
117
+ export declare function parseRunOnceDispatchPacket(value: unknown): RunOnceDispatchPacket;
118
+ export declare function failureResult(summary: string, events?: readonly RunnerDispatchEvent[]): RunOnceResultPacket;
@@ -0,0 +1,197 @@
1
+ import path from "node:path";
2
+ export function parseRunOnceDispatchPacket(value) {
3
+ const packet = asObject(value, "dispatch packet");
4
+ const schemaVersion = requiredNumber(packet, "schemaVersion");
5
+ if (schemaVersion !== 1) {
6
+ throw new Error(`Unsupported dispatch schemaVersion: ${schemaVersion}`);
7
+ }
8
+ return {
9
+ schemaVersion: 1,
10
+ userId: requiredString(packet, "userId"),
11
+ workItem: parseWorkItem(packet.workItem),
12
+ repo: parseRepo(packet.repo),
13
+ cycleRun: parseCycleRun(packet.cycleRun),
14
+ session: parseSession(packet.session),
15
+ lease: parseLease(packet.lease),
16
+ execution: parseExecution(packet.execution),
17
+ serverLocal: parseServerLocal(packet.serverLocal),
18
+ apiKeySecretGrant: parseApiKeySecretGrant(packet.apiKeySecretGrant),
19
+ artifacts: parseArtifacts(packet.artifacts),
20
+ warmPathOverrides: parseWarmPathOverrides(packet.warmPathOverrides)
21
+ };
22
+ }
23
+ export function failureResult(summary, events = []) {
24
+ return {
25
+ schemaVersion: 1,
26
+ exitCode: 1,
27
+ summary,
28
+ events
29
+ };
30
+ }
31
+ function parseWorkItem(value) {
32
+ const item = asObject(value, "workItem");
33
+ return {
34
+ id: requiredString(item, "id"),
35
+ kind: requiredString(item, "kind"),
36
+ title: requiredString(item, "title"),
37
+ description: optionalString(item, "description") ?? "",
38
+ acceptanceCriteria: optionalString(item, "acceptanceCriteria") ?? "",
39
+ reproSteps: optionalString(item, "reproSteps") ?? "",
40
+ systemInfo: optionalString(item, "systemInfo") ?? ""
41
+ };
42
+ }
43
+ function parseRepo(value) {
44
+ const repo = asObject(value, "repo");
45
+ return {
46
+ id: requiredString(repo, "id"),
47
+ name: requiredString(repo, "name"),
48
+ remoteUrl: optionalString(repo, "remoteUrl") ?? "",
49
+ defaultBranch: requiredString(repo, "defaultBranch"),
50
+ cloneOnDemand: requiredBoolean(repo, "cloneOnDemand"),
51
+ serverLocalPath: optionalString(repo, "serverLocalPath")
52
+ };
53
+ }
54
+ function parseCycleRun(value) {
55
+ const cycle = asObject(value, "cycleRun");
56
+ return {
57
+ id: requiredString(cycle, "id"),
58
+ kind: requiredString(cycle, "kind"),
59
+ phase: requiredString(cycle, "phase"),
60
+ requestedBase: optionalString(cycle, "requestedBase") ?? "develop",
61
+ baseBranch: optionalString(cycle, "baseBranch") ?? "develop",
62
+ expectedCurrentBranch: optionalString(cycle, "expectedCurrentBranch"),
63
+ attempt: requiredNumber(cycle, "attempt"),
64
+ model: requiredString(cycle, "model")
65
+ };
66
+ }
67
+ function parseSession(value) {
68
+ const session = asObject(value, "session");
69
+ return {
70
+ id: requiredString(session, "id"),
71
+ model: requiredString(session, "model"),
72
+ lane: requiredString(session, "lane")
73
+ };
74
+ }
75
+ function parseLease(value) {
76
+ const lease = asObject(value, "lease");
77
+ return {
78
+ id: requiredString(lease, "id"),
79
+ worktreeKey: requiredString(lease, "worktreeKey"),
80
+ lane: requiredString(lease, "lane"),
81
+ fencingToken: requiredString(lease, "fencingToken"),
82
+ expiresAt: requiredString(lease, "expiresAt")
83
+ };
84
+ }
85
+ function parseExecution(value) {
86
+ const execution = asObject(value, "execution");
87
+ return {
88
+ provider: requiredString(execution, "provider"),
89
+ model: requiredString(execution, "model"),
90
+ authKind: requiredString(execution, "authKind"),
91
+ location: requiredString(execution, "location"),
92
+ runnerId: optionalString(execution, "runnerId"),
93
+ repoId: requiredString(execution, "repoId"),
94
+ role: requiredString(execution, "role"),
95
+ lane: requiredString(execution, "lane"),
96
+ requiresApiKeySecretGrant: requiredBoolean(execution, "requiresApiKeySecretGrant")
97
+ };
98
+ }
99
+ function parseApiKeySecretGrant(value) {
100
+ if (value === undefined || value === null) {
101
+ return null;
102
+ }
103
+ const grant = asObject(value, "apiKeySecretGrant");
104
+ return {
105
+ grantId: requiredString(grant, "grantId"),
106
+ provider: requiredString(grant, "provider"),
107
+ secret: requiredString(grant, "secret"),
108
+ expiresAt: requiredString(grant, "expiresAt")
109
+ };
110
+ }
111
+ function parseServerLocal(value) {
112
+ const serverLocal = asObject(value, "serverLocal");
113
+ return {
114
+ repoRoot: requiredString(serverLocal, "repoRoot"),
115
+ cleanupWorktrees: requiredBoolean(serverLocal, "cleanupWorktrees")
116
+ };
117
+ }
118
+ function parseWarmPathOverrides(value) {
119
+ if (value === undefined || value === null) {
120
+ return [];
121
+ }
122
+ if (!Array.isArray(value)) {
123
+ throw new Error("warmPathOverrides must be an array when provided.");
124
+ }
125
+ return value.map((entry, index) => parseWarmPathOverride(entry, index));
126
+ }
127
+ function parseWarmPathOverride(entry, index) {
128
+ if (typeof entry !== "string" || entry.trim().length === 0) {
129
+ throw new Error(`warmPathOverrides[${index}] must be a non-empty string.`);
130
+ }
131
+ const trimmed = entry.trim();
132
+ if (path.isAbsolute(trimmed) || path.posix.isAbsolute(trimmed) || path.win32.isAbsolute(trimmed)) {
133
+ throw new Error(`warmPathOverrides[${index}] must be a relative path.`);
134
+ }
135
+ const segments = trimmed.split(/[\\/]+/).filter(Boolean);
136
+ if (segments.length === 0 || segments.some((segment) => segment === "." || segment === "..")) {
137
+ throw new Error(`warmPathOverrides[${index}] must not contain path traversal.`);
138
+ }
139
+ return segments.join("/");
140
+ }
141
+ function parseArtifacts(value) {
142
+ if (value === undefined || value === null) {
143
+ return [];
144
+ }
145
+ if (!Array.isArray(value)) {
146
+ throw new Error("artifacts must be an array when provided.");
147
+ }
148
+ return value.map((entry, index) => {
149
+ const artifact = asObject(entry, `artifacts[${index}]`);
150
+ return {
151
+ kind: requiredString(artifact, "kind"),
152
+ slug: requiredString(artifact, "slug"),
153
+ scope: requiredString(artifact, "scope"),
154
+ body: optionalString(artifact, "body") ?? "",
155
+ contentHash: requiredString(artifact, "contentHash"),
156
+ lane: optionalString(artifact, "lane"),
157
+ event: optionalString(artifact, "event")
158
+ };
159
+ });
160
+ }
161
+ function asObject(value, label) {
162
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
163
+ throw new Error(`${label} must be an object.`);
164
+ }
165
+ return value;
166
+ }
167
+ function requiredString(source, key) {
168
+ const value = source[key];
169
+ if (typeof value !== "string" || value.trim().length === 0) {
170
+ throw new Error(`${key} must be a non-empty string.`);
171
+ }
172
+ return value;
173
+ }
174
+ function optionalString(source, key) {
175
+ const value = source[key];
176
+ if (value === undefined || value === null) {
177
+ return null;
178
+ }
179
+ if (typeof value !== "string") {
180
+ throw new Error(`${key} must be a string when provided.`);
181
+ }
182
+ return value;
183
+ }
184
+ function requiredBoolean(source, key) {
185
+ const value = source[key];
186
+ if (typeof value !== "boolean") {
187
+ throw new Error(`${key} must be a boolean.`);
188
+ }
189
+ return value;
190
+ }
191
+ function requiredNumber(source, key) {
192
+ const value = source[key];
193
+ if (typeof value !== "number" || !Number.isFinite(value)) {
194
+ throw new Error(`${key} must be a finite number.`);
195
+ }
196
+ return value;
197
+ }
@@ -0,0 +1,6 @@
1
+ import { type ProcessRunner } from "../process/process.js";
2
+ export declare class GitClient {
3
+ private readonly processRunner;
4
+ constructor(processRunner?: ProcessRunner);
5
+ run(cwd: string, args: readonly string[]): Promise<string>;
6
+ }
@@ -0,0 +1,18 @@
1
+ import { NodeProcessRunner } from "../process/process.js";
2
+ export class GitClient {
3
+ processRunner;
4
+ constructor(processRunner = new NodeProcessRunner()) {
5
+ this.processRunner = processRunner;
6
+ }
7
+ async run(cwd, args) {
8
+ const result = await this.processRunner.run({
9
+ command: "git",
10
+ args,
11
+ cwd
12
+ });
13
+ if (result.exitCode !== 0) {
14
+ throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim()}`);
15
+ }
16
+ return result.stdout;
17
+ }
18
+ }
@@ -0,0 +1,4 @@
1
+ export declare function isSubPath(root: string, candidate: string): boolean;
2
+ export declare function resolveExistingDirectoryPath(inputPath: string): Promise<string>;
3
+ export declare function ensureDirectory(inputPath: string): Promise<string>;
4
+ export declare function safeSegment(value: string, fallback: string): string;
@@ -0,0 +1,29 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ export function isSubPath(root, candidate) {
4
+ const normalizedRoot = path.resolve(root);
5
+ const normalizedCandidate = path.resolve(candidate);
6
+ const relative = path.relative(normalizedRoot, normalizedCandidate);
7
+ return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative));
8
+ }
9
+ export async function resolveExistingDirectoryPath(inputPath) {
10
+ const stat = await fs.stat(inputPath);
11
+ if (!stat.isDirectory()) {
12
+ throw new Error(`Path is not a directory: ${inputPath}`);
13
+ }
14
+ return await fs.realpath(inputPath);
15
+ }
16
+ export async function ensureDirectory(inputPath) {
17
+ await fs.mkdir(inputPath, { recursive: true });
18
+ return await resolveExistingDirectoryPath(inputPath);
19
+ }
20
+ export function safeSegment(value, fallback) {
21
+ const segment = value
22
+ .trim()
23
+ .split("")
24
+ .map((ch) => (/[a-zA-Z0-9_-]/.test(ch) ? ch : "-"))
25
+ .join("")
26
+ .replace(/-+/g, "-")
27
+ .replace(/^-|-$/g, "");
28
+ return segment.length > 0 ? segment : fallback;
29
+ }
@@ -0,0 +1,16 @@
1
+ export interface ProcessRunRequest {
2
+ readonly command: string;
3
+ readonly args: readonly string[];
4
+ readonly cwd: string;
5
+ }
6
+ export interface ProcessRunResult {
7
+ readonly exitCode: number;
8
+ readonly stdout: string;
9
+ readonly stderr: string;
10
+ }
11
+ export interface ProcessRunner {
12
+ run(request: ProcessRunRequest): Promise<ProcessRunResult>;
13
+ }
14
+ export declare class NodeProcessRunner implements ProcessRunner {
15
+ run(request: ProcessRunRequest): Promise<ProcessRunResult>;
16
+ }
@@ -0,0 +1,32 @@
1
+ import { spawn } from "node:child_process";
2
+ import readline from "node:readline";
3
+ export class NodeProcessRunner {
4
+ async run(request) {
5
+ return await new Promise((resolve, reject) => {
6
+ const child = spawn(request.command, request.args, {
7
+ cwd: request.cwd,
8
+ stdio: ["ignore", "pipe", "pipe"]
9
+ });
10
+ let stdout = "";
11
+ let stderr = "";
12
+ child.on("error", reject);
13
+ const stdoutReader = readline.createInterface({ input: child.stdout });
14
+ const stderrReader = readline.createInterface({ input: child.stderr });
15
+ stdoutReader.on("line", (line) => {
16
+ stdout += `${line}\n`;
17
+ });
18
+ stderrReader.on("line", (line) => {
19
+ stderr += `${line}\n`;
20
+ });
21
+ child.on("close", (code) => {
22
+ stdoutReader.close();
23
+ stderrReader.close();
24
+ resolve({
25
+ exitCode: code ?? 1,
26
+ stdout,
27
+ stderr
28
+ });
29
+ });
30
+ });
31
+ }
32
+ }
@@ -0,0 +1,2 @@
1
+ export declare function redactSecrets<T>(value: T, exactSecrets?: readonly string[]): T;
2
+ export declare function redactText(value: string, exactSecrets?: readonly string[]): string;
@@ -0,0 +1,30 @@
1
+ const secretKeyPattern = /(api[_-]?key|authorization|bearer|client[_-]?secret|password|secret|token)/i;
2
+ export function redactSecrets(value, exactSecrets = []) {
3
+ return redactValue(value, exactSecrets);
4
+ }
5
+ export function redactText(value, exactSecrets = []) {
6
+ let redacted = value;
7
+ for (const secret of exactSecrets) {
8
+ if (secret.length > 0) {
9
+ redacted = redacted.split(secret).join("[REDACTED]");
10
+ }
11
+ }
12
+ return redacted
13
+ .replace(/(api[_-]?key|authorization|bearer|client[_-]?secret|password|secret|token)(["':=\s]+)([^"',\s]+)/gi, "$1$2[REDACTED]")
14
+ .replace(/(sk-[a-zA-Z0-9_-]{12,})/g, "[REDACTED]");
15
+ }
16
+ function redactValue(value, exactSecrets) {
17
+ if (typeof value === "string") {
18
+ return redactText(value, exactSecrets);
19
+ }
20
+ if (Array.isArray(value)) {
21
+ return value.map((item) => redactValue(item, exactSecrets));
22
+ }
23
+ if (!value || typeof value !== "object") {
24
+ return value;
25
+ }
26
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
27
+ key,
28
+ secretKeyPattern.test(key) ? "[REDACTED]" : redactValue(item, exactSecrets)
29
+ ]));
30
+ }
@@ -0,0 +1 @@
1
+ export declare const forgeBridgeVersion = "0.1.1-dev.03b8ba168599";
@@ -0,0 +1 @@
1
+ export const forgeBridgeVersion = "0.1.1-dev.03b8ba168599";
@@ -0,0 +1,11 @@
1
+ import type { DispatchResolvedArtifact } from "../session/session.types.js";
2
+ export declare function injectWorktreeArtifacts(worktreePath: string, artifacts: readonly DispatchResolvedArtifact[]): Promise<void>;
3
+ export declare function writeArtifact(worktreePath: string, artifact: DispatchResolvedArtifact): Promise<void>;
4
+ export declare function selectLaneCommand(artifacts: readonly DispatchResolvedArtifact[], lane: string, phase: string): DispatchResolvedArtifact | undefined;
5
+ export declare function resolveCommandSlug(lane: string, phase: string): string;
6
+ export declare function buildSpecBody(laneCommand: DispatchResolvedArtifact, workItem: {
7
+ readonly id: string;
8
+ readonly title: string;
9
+ readonly description: string;
10
+ readonly acceptanceCriteria: string;
11
+ }): string;