@loucompanion/forge-bridge 0.1.1-dev.2bc1e7f19b3a → 0.1.1-dev.2d1a116ca78d
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.d.ts +1 -0
- package/dist/bridge/bridge.service.js +213 -25
- package/dist/bridge/bridge.types.d.ts +13 -2
- package/dist/cli/cli.service.js +225 -10
- package/dist/cli/local-config.d.ts +1 -0
- package/dist/cli/local-config.js +36 -4
- package/dist/config.js +1 -1
- package/dist/session/internals.d.ts +1 -1
- package/dist/session/phase-completion.d.ts +34 -5
- package/dist/session/phase-completion.js +2108 -75
- package/dist/session/provider-cli/provider-cli.base.d.ts +4 -3
- package/dist/session/provider-cli/provider-cli.service.d.ts +20 -5
- package/dist/session/provider-cli/provider-cli.service.js +1873 -173
- package/dist/session/provider-cli/provider-cli.types.d.ts +5 -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 +53 -15
- package/dist/session/session.types.d.ts +129 -0
- package/dist/session/session.types.js +250 -2
- 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 +117 -1
- package/dist/worktree/worktree.service.d.ts +1 -0
- package/dist/worktree/worktree.service.js +132 -6
- package/dist/worktree/worktree.types.d.ts +8 -1
- package/package.json +8 -3
|
@@ -15,9 +15,16 @@ 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),
|
|
24
|
+
architectRevision: parseArchitectRevision(packet.architectRevision),
|
|
25
|
+
storyDevelop: parseStoryDevelop(packet.storyDevelop),
|
|
26
|
+
storyQa: parseStoryQa(packet.storyQa),
|
|
27
|
+
draft: parseDraftPrompt(packet.draft)
|
|
21
28
|
};
|
|
22
29
|
}
|
|
23
30
|
export function failureResult(summary, events = []) {
|
|
@@ -55,6 +62,9 @@ function parseCycleRun(value) {
|
|
|
55
62
|
const cycle = asObject(value, "cycleRun");
|
|
56
63
|
return {
|
|
57
64
|
id: requiredString(cycle, "id"),
|
|
65
|
+
ownerType: optionalString(cycle, "ownerType"),
|
|
66
|
+
ownerId: optionalString(cycle, "ownerId"),
|
|
67
|
+
workItemId: optionalString(cycle, "workItemId"),
|
|
58
68
|
kind: requiredString(cycle, "kind"),
|
|
59
69
|
phase: requiredString(cycle, "phase"),
|
|
60
70
|
requestedBase: optionalString(cycle, "requestedBase") ?? "develop",
|
|
@@ -76,6 +86,9 @@ function parseLease(value) {
|
|
|
76
86
|
const lease = asObject(value, "lease");
|
|
77
87
|
return {
|
|
78
88
|
id: requiredString(lease, "id"),
|
|
89
|
+
ownerType: optionalString(lease, "ownerType"),
|
|
90
|
+
ownerId: optionalString(lease, "ownerId"),
|
|
91
|
+
workItemId: optionalString(lease, "workItemId"),
|
|
79
92
|
worktreeKey: requiredString(lease, "worktreeKey"),
|
|
80
93
|
lane: requiredString(lease, "lane"),
|
|
81
94
|
fencingToken: requiredString(lease, "fencingToken"),
|
|
@@ -93,7 +106,19 @@ function parseExecution(value) {
|
|
|
93
106
|
repoId: requiredString(execution, "repoId"),
|
|
94
107
|
role: requiredString(execution, "role"),
|
|
95
108
|
lane: requiredString(execution, "lane"),
|
|
96
|
-
requiresApiKeySecretGrant: requiredBoolean(execution, "requiresApiKeySecretGrant")
|
|
109
|
+
requiresApiKeySecretGrant: requiredBoolean(execution, "requiresApiKeySecretGrant"),
|
|
110
|
+
runnerRepoLocalPath: optionalString(execution, "runnerRepoLocalPath")
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function parseDraftPrompt(value) {
|
|
114
|
+
if (value === undefined || value === null) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const draft = asObject(value, "draft");
|
|
118
|
+
return {
|
|
119
|
+
kind: requiredString(draft, "kind"),
|
|
120
|
+
systemPrompt: requiredString(draft, "systemPrompt"),
|
|
121
|
+
userPrompt: requiredString(draft, "userPrompt")
|
|
97
122
|
};
|
|
98
123
|
}
|
|
99
124
|
function parseApiKeySecretGrant(value) {
|
|
@@ -115,6 +140,28 @@ function parseServerLocal(value) {
|
|
|
115
140
|
cleanupWorktrees: requiredBoolean(serverLocal, "cleanupWorktrees")
|
|
116
141
|
};
|
|
117
142
|
}
|
|
143
|
+
function parsePullRequest(value) {
|
|
144
|
+
if (value === undefined || value === null) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const pullRequest = asObject(value, "pullRequest");
|
|
148
|
+
return {
|
|
149
|
+
number: requiredNumber(pullRequest, "number"),
|
|
150
|
+
provider: requiredString(pullRequest, "provider"),
|
|
151
|
+
title: optionalString(pullRequest, "title") ?? "",
|
|
152
|
+
sourceBranch: requiredString(pullRequest, "sourceBranch"),
|
|
153
|
+
targetBranch: requiredString(pullRequest, "targetBranch"),
|
|
154
|
+
url: optionalString(pullRequest, "url") ?? "",
|
|
155
|
+
state: requiredString(pullRequest, "state"),
|
|
156
|
+
checkState: requiredString(pullRequest, "checkState"),
|
|
157
|
+
mergeState: requiredString(pullRequest, "mergeState"),
|
|
158
|
+
requiredChecksPassed: requiredNumber(pullRequest, "requiredChecksPassed"),
|
|
159
|
+
requiredChecksTotal: requiredNumber(pullRequest, "requiredChecksTotal"),
|
|
160
|
+
unresolvedThreads: requiredNumber(pullRequest, "unresolvedThreads"),
|
|
161
|
+
humanApproved: requiredBoolean(pullRequest, "humanApproved"),
|
|
162
|
+
hasMergeConflicts: requiredBoolean(pullRequest, "hasMergeConflicts")
|
|
163
|
+
};
|
|
164
|
+
}
|
|
118
165
|
function parseWarmPathOverrides(value) {
|
|
119
166
|
if (value === undefined || value === null) {
|
|
120
167
|
return [];
|
|
@@ -138,6 +185,197 @@ function parseWarmPathOverride(entry, index) {
|
|
|
138
185
|
}
|
|
139
186
|
return segments.join("/");
|
|
140
187
|
}
|
|
188
|
+
function parseQaBrowser(value) {
|
|
189
|
+
if (value === undefined || value === null) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const qaBrowser = asObject(value, "qaBrowser");
|
|
193
|
+
return {
|
|
194
|
+
required: optionalBoolean(qaBrowser, "required") ?? false,
|
|
195
|
+
launchTarget: parseQaAppLaunchTarget(qaBrowser.launchTarget ?? qaBrowser.launch_target),
|
|
196
|
+
artifactsPath: optionalString(qaBrowser, "artifactsPath") ?? optionalString(qaBrowser, "artifacts_path") ?? "",
|
|
197
|
+
missingTargetReason: optionalString(qaBrowser, "missingTargetReason") ?? optionalString(qaBrowser, "missing_target_reason")
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function parseQaAppLaunchTarget(value) {
|
|
201
|
+
if (value === undefined || value === null) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
const launchTarget = asObject(value, "qaBrowser.launchTarget");
|
|
205
|
+
const routes = launchTarget.routes;
|
|
206
|
+
if (routes !== undefined && routes !== null && !Array.isArray(routes)) {
|
|
207
|
+
throw new Error("qaBrowser.launchTarget.routes must be an array when provided.");
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
url: optionalString(launchTarget, "url"),
|
|
211
|
+
command: optionalString(launchTarget, "command"),
|
|
212
|
+
working_directory: optionalString(launchTarget, "working_directory"),
|
|
213
|
+
ready_url: optionalString(launchTarget, "ready_url"),
|
|
214
|
+
routes: Array.isArray(routes)
|
|
215
|
+
? routes.map((entry, index) => {
|
|
216
|
+
if (typeof entry !== "string") {
|
|
217
|
+
throw new Error(`qaBrowser.launchTarget.routes[${index}] must be a string.`);
|
|
218
|
+
}
|
|
219
|
+
return entry;
|
|
220
|
+
}).filter((entry) => entry.trim().length > 0)
|
|
221
|
+
: [],
|
|
222
|
+
notes: optionalString(launchTarget, "notes")
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function parseQaRework(value) {
|
|
226
|
+
if (value === undefined || value === null) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
const qaRework = asObject(value, "qaRework");
|
|
230
|
+
const findingIdsValue = qaRework.findingIds;
|
|
231
|
+
if (!Array.isArray(findingIdsValue)) {
|
|
232
|
+
throw new Error("qaRework.findingIds must be an array.");
|
|
233
|
+
}
|
|
234
|
+
const findingsValue = qaRework.findings;
|
|
235
|
+
if (!Array.isArray(findingsValue)) {
|
|
236
|
+
throw new Error("qaRework.findings must be an array.");
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
attempt: requiredNumber(qaRework, "attempt"),
|
|
240
|
+
maxAttempts: requiredNumber(qaRework, "maxAttempts"),
|
|
241
|
+
packetPath: requiredString(qaRework, "packetPath"),
|
|
242
|
+
packetJson: requiredString(qaRework, "packetJson"),
|
|
243
|
+
findingIds: findingIdsValue.map((entry, index) => {
|
|
244
|
+
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
245
|
+
throw new Error(`qaRework.findingIds[${index}] must be a non-empty string.`);
|
|
246
|
+
}
|
|
247
|
+
return entry;
|
|
248
|
+
}),
|
|
249
|
+
findings: findingsValue.map((entry, index) => asObject(entry, `qaRework.findings[${index}]`))
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function parseArchitectRevision(value) {
|
|
253
|
+
if (value === undefined || value === null) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const revision = asObject(value, "architectRevision");
|
|
257
|
+
return {
|
|
258
|
+
priorPlanId: requiredString(revision, "priorPlanId"),
|
|
259
|
+
priorContentHash: requiredString(revision, "priorContentHash"),
|
|
260
|
+
feedback: requiredString(revision, "feedback")
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function parseStoryDevelop(value) {
|
|
264
|
+
if (value === undefined || value === null) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const storyDevelop = asObject(value, "storyDevelop");
|
|
268
|
+
const dependencyWorkItemIds = storyDevelop.dependencyWorkItemIds;
|
|
269
|
+
if (!Array.isArray(dependencyWorkItemIds)) {
|
|
270
|
+
throw new Error("storyDevelop.dependencyWorkItemIds must be an array.");
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
storyWorkItemId: requiredString(storyDevelop, "storyWorkItemId"),
|
|
274
|
+
planId: requiredString(storyDevelop, "planId"),
|
|
275
|
+
planContentHash: requiredString(storyDevelop, "planContentHash"),
|
|
276
|
+
architectTaskId: requiredString(storyDevelop, "architectTaskId"),
|
|
277
|
+
phaseId: requiredString(storyDevelop, "phaseId"),
|
|
278
|
+
dependencyWorkItemIds: dependencyWorkItemIds.map((entry, index) => {
|
|
279
|
+
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
280
|
+
throw new Error(`storyDevelop.dependencyWorkItemIds[${index}] must be a non-empty string.`);
|
|
281
|
+
}
|
|
282
|
+
return entry.trim();
|
|
283
|
+
})
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function parseStoryQa(value) {
|
|
287
|
+
if (value === undefined || value === null) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
const storyQa = asObject(value, "storyQa");
|
|
291
|
+
return {
|
|
292
|
+
storyWorkItemId: requiredString(storyQa, "storyWorkItemId"),
|
|
293
|
+
planId: requiredString(storyQa, "planId"),
|
|
294
|
+
planContentHash: requiredString(storyQa, "planContentHash"),
|
|
295
|
+
worktreeKey: requiredString(storyQa, "worktreeKey"),
|
|
296
|
+
branch: requiredString(storyQa, "branch"),
|
|
297
|
+
implementationPlanJson: optionalString(storyQa, "implementationPlanJson"),
|
|
298
|
+
qaPlanJson: optionalString(storyQa, "qaPlanJson"),
|
|
299
|
+
finalQaChecks: parseStringArray(storyQa.finalQaChecks, "storyQa.finalQaChecks"),
|
|
300
|
+
childTasks: parseStoryQaChildTasks(storyQa.childTasks),
|
|
301
|
+
dependencies: parseStoryQaDependencies(storyQa.dependencies)
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function parseStoryQaChildTasks(value) {
|
|
305
|
+
if (!Array.isArray(value)) {
|
|
306
|
+
throw new Error("storyQa.childTasks must be an array.");
|
|
307
|
+
}
|
|
308
|
+
return value.map((entry, index) => {
|
|
309
|
+
const child = asObject(entry, `storyQa.childTasks[${index}]`);
|
|
310
|
+
return {
|
|
311
|
+
workItemId: requiredString(child, "workItemId"),
|
|
312
|
+
title: requiredString(child, "title"),
|
|
313
|
+
architectTaskId: requiredString(child, "architectTaskId"),
|
|
314
|
+
phaseId: requiredString(child, "phaseId"),
|
|
315
|
+
phaseOrder: requiredNumber(child, "phaseOrder"),
|
|
316
|
+
dependencyWorkItemIds: parseStringArray(child.dependencyWorkItemIds, `storyQa.childTasks[${index}].dependencyWorkItemIds`),
|
|
317
|
+
spec: parseStoryQaSpec(child.spec, `storyQa.childTasks[${index}].spec`),
|
|
318
|
+
implementationArtifact: parseStoryQaArtifact(child.implementationArtifact, `storyQa.childTasks[${index}].implementationArtifact`),
|
|
319
|
+
selfReviewArtifact: parseStoryQaArtifact(child.selfReviewArtifact, `storyQa.childTasks[${index}].selfReviewArtifact`),
|
|
320
|
+
validationArtifact: parseStoryQaArtifact(child.validationArtifact, `storyQa.childTasks[${index}].validationArtifact`),
|
|
321
|
+
qaArtifact: parseStoryQaArtifact(child.qaArtifact, `storyQa.childTasks[${index}].qaArtifact`)
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
function parseStoryQaSpec(value, label) {
|
|
326
|
+
if (value === undefined || value === null) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
const spec = asObject(value, label);
|
|
330
|
+
return {
|
|
331
|
+
version: requiredNumber(spec, "version"),
|
|
332
|
+
status: requiredString(spec, "status"),
|
|
333
|
+
path: requiredString(spec, "path"),
|
|
334
|
+
contentHash: requiredString(spec, "contentHash"),
|
|
335
|
+
body: requiredString(spec, "body")
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function parseStoryQaArtifact(value, label) {
|
|
339
|
+
if (value === undefined || value === null) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
const artifact = asObject(value, label);
|
|
343
|
+
return {
|
|
344
|
+
artifactId: requiredString(artifact, "artifactId"),
|
|
345
|
+
cycleRunId: requiredString(artifact, "cycleRunId"),
|
|
346
|
+
phase: requiredString(artifact, "phase"),
|
|
347
|
+
status: requiredString(artifact, "status"),
|
|
348
|
+
producedAt: requiredString(artifact, "producedAt"),
|
|
349
|
+
path: optionalString(artifact, "path"),
|
|
350
|
+
verdictJson: optionalString(artifact, "verdictJson"),
|
|
351
|
+
evidenceJson: optionalString(artifact, "evidenceJson"),
|
|
352
|
+
findingsJson: optionalString(artifact, "findingsJson"),
|
|
353
|
+
report: optionalString(artifact, "report")
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function parseStoryQaDependencies(value) {
|
|
357
|
+
if (!Array.isArray(value)) {
|
|
358
|
+
throw new Error("storyQa.dependencies must be an array.");
|
|
359
|
+
}
|
|
360
|
+
return value.map((entry, index) => {
|
|
361
|
+
const dependency = asObject(entry, `storyQa.dependencies[${index}]`);
|
|
362
|
+
return {
|
|
363
|
+
fromWorkItemId: requiredString(dependency, "fromWorkItemId").trim(),
|
|
364
|
+
toWorkItemId: requiredString(dependency, "toWorkItemId").trim()
|
|
365
|
+
};
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
function parseStringArray(value, label) {
|
|
369
|
+
if (!Array.isArray(value)) {
|
|
370
|
+
throw new Error(`${label} must be an array.`);
|
|
371
|
+
}
|
|
372
|
+
return value.map((entry, index) => {
|
|
373
|
+
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
374
|
+
throw new Error(`${label}[${index}] must be a non-empty string.`);
|
|
375
|
+
}
|
|
376
|
+
return entry.trim();
|
|
377
|
+
});
|
|
378
|
+
}
|
|
141
379
|
function parseArtifacts(value) {
|
|
142
380
|
if (value === undefined || value === null) {
|
|
143
381
|
return [];
|
|
@@ -189,6 +427,16 @@ function requiredBoolean(source, key) {
|
|
|
189
427
|
}
|
|
190
428
|
return value;
|
|
191
429
|
}
|
|
430
|
+
function optionalBoolean(source, key) {
|
|
431
|
+
const value = source[key];
|
|
432
|
+
if (value === undefined || value === null) {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
if (typeof value !== "boolean") {
|
|
436
|
+
throw new Error(`${key} must be a boolean when provided.`);
|
|
437
|
+
}
|
|
438
|
+
return value;
|
|
439
|
+
}
|
|
192
440
|
function optionalPositiveInteger(source, key) {
|
|
193
441
|
const value = source[key];
|
|
194
442
|
if (value === undefined || value === null) {
|
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.2d1a116ca78d";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const forgeBridgeVersion = "0.1.1-dev.
|
|
1
|
+
export const forgeBridgeVersion = "0.1.1-dev.2d1a116ca78d";
|
|
@@ -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,73 @@
|
|
|
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"]);
|
|
6
|
+
const ARCHITECT_OPERATIONAL_SKILLS = new Set([
|
|
7
|
+
"architect-repo-map",
|
|
8
|
+
"architect-acceptance-map",
|
|
9
|
+
"architect-contract-designer",
|
|
10
|
+
"architect-dag-planner",
|
|
11
|
+
"architect-test-plan-designer",
|
|
12
|
+
"architect-risk-review",
|
|
13
|
+
"architect-integration-strategy",
|
|
14
|
+
"architect-handoff-writer",
|
|
15
|
+
"architect-mcp-contract",
|
|
16
|
+
"architect-semantic-validator"
|
|
17
|
+
]);
|
|
18
|
+
const ARCHITECT_OPERATIONAL_HOOKS = new Set([
|
|
19
|
+
"architect-preflight",
|
|
20
|
+
"architect-context",
|
|
21
|
+
"architect-repo-map",
|
|
22
|
+
"architect-acceptance-map",
|
|
23
|
+
"architect-contracts",
|
|
24
|
+
"architect-plan",
|
|
25
|
+
"architect-qa-seed",
|
|
26
|
+
"architect-integration",
|
|
27
|
+
"architect-mcp-contract",
|
|
28
|
+
"architect-validate-plan",
|
|
29
|
+
"architect-risk-gate",
|
|
30
|
+
"architect-mcp-complete",
|
|
31
|
+
"architect-complete",
|
|
32
|
+
"architect-revise"
|
|
33
|
+
]);
|
|
34
|
+
const DRAFT_OPERATIONAL_SKILLS = new Set([
|
|
35
|
+
"draft-intent",
|
|
36
|
+
"draft-repo-context",
|
|
37
|
+
"draft-graph-context",
|
|
38
|
+
"draft-product-scope",
|
|
39
|
+
"draft-work-order-plan",
|
|
40
|
+
"draft-critic",
|
|
41
|
+
"draft-proposal"
|
|
42
|
+
]);
|
|
43
|
+
const DRAFT_OPERATIONAL_HOOKS = new Set([
|
|
44
|
+
"draft-intent",
|
|
45
|
+
"draft-repo-context",
|
|
46
|
+
"draft-graph-context",
|
|
47
|
+
"draft-product-scope",
|
|
48
|
+
"draft-work-order-plan",
|
|
49
|
+
"draft-critic",
|
|
50
|
+
"draft-proposal"
|
|
51
|
+
]);
|
|
4
52
|
export async function injectWorktreeArtifacts(worktreePath, artifacts) {
|
|
5
53
|
for (const artifact of artifacts) {
|
|
6
54
|
await writeArtifact(worktreePath, artifact);
|
|
7
55
|
}
|
|
8
56
|
}
|
|
57
|
+
export function filterArtifactsForLane(artifacts, lane, phase) {
|
|
58
|
+
const normalizedLane = lane.trim().toLowerCase();
|
|
59
|
+
const commandSlug = resolveCommandSlug(lane, phase).toLowerCase();
|
|
60
|
+
if (normalizedLane === "debug") {
|
|
61
|
+
return artifacts.filter((artifact) => shouldInjectDebugArtifact(artifact, commandSlug));
|
|
62
|
+
}
|
|
63
|
+
if (normalizedLane === "architect") {
|
|
64
|
+
return artifacts.filter((artifact) => shouldInjectArchitectArtifact(artifact, commandSlug));
|
|
65
|
+
}
|
|
66
|
+
if (normalizedLane === "draft") {
|
|
67
|
+
return artifacts.filter((artifact) => shouldInjectDraftArtifact(artifact, commandSlug));
|
|
68
|
+
}
|
|
69
|
+
return artifacts;
|
|
70
|
+
}
|
|
9
71
|
export async function writeArtifact(worktreePath, artifact) {
|
|
10
72
|
const writtenHash = sha256(artifact.body);
|
|
11
73
|
if (writtenHash !== artifact.contentHash.toLowerCase()) {
|
|
@@ -29,11 +91,17 @@ export function selectLaneCommand(artifacts, lane, phase) {
|
|
|
29
91
|
return artifacts.find((artifact) => normalizedArtifactKind(artifact.kind) === "command" && artifact.slug.trim().toLowerCase() === slug);
|
|
30
92
|
}
|
|
31
93
|
export function resolveCommandSlug(lane, phase) {
|
|
94
|
+
const normalizedLane = lane.trim().toLowerCase();
|
|
95
|
+
if (normalizedLane === "debug") {
|
|
96
|
+
return "debug-prompt";
|
|
97
|
+
}
|
|
32
98
|
const normalizedPhase = phase.trim().toLowerCase().replace(/-/g, "_");
|
|
33
99
|
if (normalizedPhase === "rework") {
|
|
34
100
|
return "rework-prompt";
|
|
35
101
|
}
|
|
36
|
-
switch (
|
|
102
|
+
switch (normalizedLane) {
|
|
103
|
+
case "draft":
|
|
104
|
+
return "draft-prompt";
|
|
37
105
|
case "architect":
|
|
38
106
|
return "architect-prompt";
|
|
39
107
|
case "qa":
|
|
@@ -42,6 +110,54 @@ export function resolveCommandSlug(lane, phase) {
|
|
|
42
110
|
return "dev-prompt";
|
|
43
111
|
}
|
|
44
112
|
}
|
|
113
|
+
function shouldInjectDebugArtifact(artifact, commandSlug) {
|
|
114
|
+
if (artifact.scope.trim().toLowerCase() !== "operational") {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const slug = artifact.slug.trim().toLowerCase();
|
|
118
|
+
switch (normalizedArtifactKind(artifact.kind)) {
|
|
119
|
+
case "command":
|
|
120
|
+
return slug === commandSlug;
|
|
121
|
+
case "skill":
|
|
122
|
+
return DEBUG_OPERATIONAL_SKILLS.has(slug);
|
|
123
|
+
case "hook":
|
|
124
|
+
return DEBUG_OPERATIONAL_HOOKS.has(slug);
|
|
125
|
+
default:
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function shouldInjectArchitectArtifact(artifact, commandSlug) {
|
|
130
|
+
if (artifact.scope.trim().toLowerCase() !== "operational") {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
const slug = artifact.slug.trim().toLowerCase();
|
|
134
|
+
switch (normalizedArtifactKind(artifact.kind)) {
|
|
135
|
+
case "command":
|
|
136
|
+
return slug === commandSlug;
|
|
137
|
+
case "skill":
|
|
138
|
+
return ARCHITECT_OPERATIONAL_SKILLS.has(slug);
|
|
139
|
+
case "hook":
|
|
140
|
+
return ARCHITECT_OPERATIONAL_HOOKS.has(slug);
|
|
141
|
+
default:
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function shouldInjectDraftArtifact(artifact, commandSlug) {
|
|
146
|
+
if (artifact.scope.trim().toLowerCase() !== "operational") {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
const slug = artifact.slug.trim().toLowerCase();
|
|
150
|
+
switch (normalizedArtifactKind(artifact.kind)) {
|
|
151
|
+
case "command":
|
|
152
|
+
return slug === commandSlug;
|
|
153
|
+
case "skill":
|
|
154
|
+
return DRAFT_OPERATIONAL_SKILLS.has(slug);
|
|
155
|
+
case "hook":
|
|
156
|
+
return DRAFT_OPERATIONAL_HOOKS.has(slug);
|
|
157
|
+
default:
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
45
161
|
export function buildSpecBody(laneCommand, workItem, phase = "implementation") {
|
|
46
162
|
const prompt = laneCommand.body.replaceAll("{workItemId}", workItem.id);
|
|
47
163
|
return `---
|
|
@@ -11,6 +11,7 @@ export declare class GitWorktreeService implements WorktreeService {
|
|
|
11
11
|
private resolveMatchingRepo;
|
|
12
12
|
private ensureBaseBranch;
|
|
13
13
|
private gitRefExists;
|
|
14
|
+
private currentBranch;
|
|
14
15
|
private ensureGitIdentity;
|
|
15
16
|
}
|
|
16
17
|
export declare function normalizeBranchName(value: string | null | undefined, fallback: string): string;
|
|
@@ -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;
|
|
@@ -17,9 +17,12 @@ export class GitWorktreeService {
|
|
|
17
17
|
await this.ensureBaseBranch(repoPath, baseBranch);
|
|
18
18
|
const shortId = request.workItem.id.replace(/-/g, "").slice(0, 12);
|
|
19
19
|
const kind = safeSegment(request.workItem.kind, "work-item");
|
|
20
|
-
const
|
|
20
|
+
const fallbackWorktreeKey = `${kind}/${shortId}`;
|
|
21
|
+
const worktreeKey = request.worktreeKey?.trim() || fallbackWorktreeKey;
|
|
22
|
+
const worktreeSegments = worktreeKeySegments(worktreeKey, kind, shortId);
|
|
23
|
+
const branchName = normalizeBranchName(worktreeSegments.join("/"), "work-item/unknown");
|
|
21
24
|
const worktreeRoot = resolveWorktreeRoot(allowedRoot, repoPath);
|
|
22
|
-
const worktreePath = path.join(worktreeRoot,
|
|
25
|
+
const worktreePath = path.join(worktreeRoot, ...worktreeSegments);
|
|
23
26
|
const resolvedWorktreeParent = await ensureDirectory(path.dirname(worktreePath));
|
|
24
27
|
if (!isSubPath(allowedRoot, resolvedWorktreeParent)) {
|
|
25
28
|
throw new Error(`Worktree path must stay under ${allowedRoot}.`);
|
|
@@ -35,10 +38,17 @@ export class GitWorktreeService {
|
|
|
35
38
|
if (!isSubPath(allowedRoot, resolvedWorktreePath)) {
|
|
36
39
|
throw new Error(`Worktree path must stay under ${allowedRoot}.`);
|
|
37
40
|
}
|
|
41
|
+
const currentBranch = await this.currentBranch(resolvedWorktreePath);
|
|
42
|
+
if (currentBranch !== branchName) {
|
|
43
|
+
throw new Error(`Existing worktree ${resolvedWorktreePath} is on branch '${currentBranch}', expected '${branchName}'.`);
|
|
44
|
+
}
|
|
38
45
|
}
|
|
39
46
|
await this.ensureGitIdentity(resolvedWorktreePath);
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
await clearRunArtifactRoots(resolvedWorktreePath, request);
|
|
48
|
+
const qaReworkPacketPath = await materializeQaReworkPacket(resolvedWorktreePath, request);
|
|
49
|
+
const artifacts = filterArtifactsForLane(request.artifacts, request.lane, request.phase);
|
|
50
|
+
if (artifacts.length > 0) {
|
|
51
|
+
await injectWorktreeArtifacts(resolvedWorktreePath, artifacts);
|
|
42
52
|
}
|
|
43
53
|
const dependencyWarm = await warmWorktreeDependencies({
|
|
44
54
|
baseCheckoutPath: repoPath,
|
|
@@ -52,7 +62,8 @@ export class GitWorktreeService {
|
|
|
52
62
|
branchName,
|
|
53
63
|
baseBranch,
|
|
54
64
|
cleanupWorktrees: request.cleanupWorktrees,
|
|
55
|
-
dependencyWarm
|
|
65
|
+
dependencyWarm,
|
|
66
|
+
qaReworkPacketPath
|
|
56
67
|
};
|
|
57
68
|
}
|
|
58
69
|
async release(handle) {
|
|
@@ -171,6 +182,9 @@ export class GitWorktreeService {
|
|
|
171
182
|
async gitRefExists(repoPath, gitRef) {
|
|
172
183
|
return this.git.run(repoPath, ["show-ref", "--verify", "--quiet", gitRef]).then(() => true).catch(() => false);
|
|
173
184
|
}
|
|
185
|
+
async currentBranch(repoPath) {
|
|
186
|
+
return (await this.git.run(repoPath, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
|
|
187
|
+
}
|
|
174
188
|
async ensureGitIdentity(repoPath) {
|
|
175
189
|
await this.git.run(repoPath, ["config", "user.email", "forge-bridge@forge.local"]);
|
|
176
190
|
await this.git.run(repoPath, ["config", "user.name", "Forge Bridge"]);
|
|
@@ -192,6 +206,76 @@ function isCloneableRemote(remoteUrl) {
|
|
|
192
206
|
}
|
|
193
207
|
return trimmed.startsWith("git@") || trimmed.includes("://") || path.isAbsolute(trimmed);
|
|
194
208
|
}
|
|
209
|
+
async function clearRunArtifactRoots(worktreePath, request) {
|
|
210
|
+
const roots = new Set();
|
|
211
|
+
roots.add(workItemArtifactRoot(worktreePath, request.workItem.id));
|
|
212
|
+
const ownerType = normalizeOwnerType(request.ownerType);
|
|
213
|
+
const ownerId = request.ownerId?.trim();
|
|
214
|
+
if (ownerType && ownerId) {
|
|
215
|
+
roots.add(ownerArtifactRoot(worktreePath, ownerType, ownerId));
|
|
216
|
+
}
|
|
217
|
+
for (const root of roots) {
|
|
218
|
+
const resolvedRoot = path.resolve(root);
|
|
219
|
+
if (!isSubPath(worktreePath, resolvedRoot)) {
|
|
220
|
+
throw new Error(`Artifact root must stay under ${worktreePath}.`);
|
|
221
|
+
}
|
|
222
|
+
await ensureNoSymlinkPathSegments(worktreePath, resolvedRoot, "Artifact root must not traverse symlinked path segments.");
|
|
223
|
+
await fs.rm(resolvedRoot, { recursive: true, force: true });
|
|
224
|
+
await fs.mkdir(resolvedRoot, { recursive: true });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async function materializeQaReworkPacket(worktreePath, request) {
|
|
228
|
+
const qaRework = request.qaRework;
|
|
229
|
+
if (!qaRework) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
const artifactRoot = artifactRootForRequest(worktreePath, request);
|
|
233
|
+
const packetFileName = qaReworkPacketFileName(qaRework.packetPath);
|
|
234
|
+
const packetPath = path.join(artifactRoot, packetFileName);
|
|
235
|
+
const paths = new Set([packetPath, path.join(artifactRoot, "qa-rework-packet.json")]);
|
|
236
|
+
const packetJson = qaRework.packetJson.endsWith("\n") ? qaRework.packetJson : `${qaRework.packetJson}\n`;
|
|
237
|
+
for (const target of paths) {
|
|
238
|
+
const resolvedTarget = path.resolve(target);
|
|
239
|
+
if (!isSubPath(worktreePath, resolvedTarget)) {
|
|
240
|
+
throw new Error(`QA rework packet path must stay under ${worktreePath}.`);
|
|
241
|
+
}
|
|
242
|
+
await ensureNoSymlinkPathSegments(worktreePath, path.dirname(resolvedTarget), "QA rework packet path must not traverse symlinked path segments.");
|
|
243
|
+
await fs.mkdir(path.dirname(resolvedTarget), { recursive: true });
|
|
244
|
+
await fs.writeFile(resolvedTarget, packetJson, "utf8");
|
|
245
|
+
}
|
|
246
|
+
return path.resolve(packetPath);
|
|
247
|
+
}
|
|
248
|
+
function artifactRootForRequest(worktreePath, request) {
|
|
249
|
+
const ownerType = normalizeOwnerType(request.ownerType);
|
|
250
|
+
const ownerId = request.ownerId?.trim();
|
|
251
|
+
if (ownerType && ownerId) {
|
|
252
|
+
return ownerArtifactRoot(worktreePath, ownerType, ownerId);
|
|
253
|
+
}
|
|
254
|
+
return workItemArtifactRoot(worktreePath, request.workItem.id);
|
|
255
|
+
}
|
|
256
|
+
function workItemArtifactRoot(worktreePath, workItemId) {
|
|
257
|
+
return path.join(worktreePath, ".forge", "work-items", safeSegment(workItemId, "work item id"), "artifacts");
|
|
258
|
+
}
|
|
259
|
+
function ownerArtifactRoot(worktreePath, ownerType, ownerId) {
|
|
260
|
+
return path.join(worktreePath, ".forge", ownerType === "story" ? "stories" : "work-items", safeSegment(ownerId, "owner id"), "artifacts");
|
|
261
|
+
}
|
|
262
|
+
function normalizeOwnerType(value) {
|
|
263
|
+
const normalized = (value ?? "").trim().toLowerCase().replace(/-/g, "_");
|
|
264
|
+
if (normalized === "story") {
|
|
265
|
+
return "story";
|
|
266
|
+
}
|
|
267
|
+
if (normalized === "work_item" || normalized === "workitem" || normalized === "work item") {
|
|
268
|
+
return "work_item";
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
function qaReworkPacketFileName(packetPath) {
|
|
273
|
+
const normalized = packetPath.trim().replace(/\\/g, "/");
|
|
274
|
+
const fileName = normalized.split("/").filter(Boolean).at(-1) ?? "";
|
|
275
|
+
return /^qa-rework-packet(?:\.attempt-\d+)?\.json$/i.test(fileName)
|
|
276
|
+
? fileName
|
|
277
|
+
: "qa-rework-packet.json";
|
|
278
|
+
}
|
|
195
279
|
export function normalizeBranchName(value, fallback) {
|
|
196
280
|
const branch = (value?.trim() || fallback).trim();
|
|
197
281
|
if (branch.length === 0 ||
|
|
@@ -204,6 +288,7 @@ export function normalizeBranchName(value, fallback) {
|
|
|
204
288
|
branch.endsWith(".") ||
|
|
205
289
|
branch.toLowerCase().endsWith(".lock") ||
|
|
206
290
|
/\s/.test(branch) ||
|
|
291
|
+
// eslint-disable-next-line no-control-regex -- Git branch validation must reject ASCII control characters.
|
|
207
292
|
/[\u0000-\u001f~^:?*[\\]/.test(branch) ||
|
|
208
293
|
branch.split("/").some((part) => part.length === 0 || part.startsWith(".") || part.toLowerCase().endsWith(".lock"))) {
|
|
209
294
|
throw new Error("Git branch name must be valid, without whitespace, and cannot start with '-'.");
|
|
@@ -240,6 +325,39 @@ async function gitMetadataExists(repoPath, allowedRoot) {
|
|
|
240
325
|
const resolvedGitDir = await resolveExistingDirectoryPath(gitDirPath);
|
|
241
326
|
return isSubPath(allowedRoot, resolvedGitDir);
|
|
242
327
|
}
|
|
328
|
+
async function ensureNoSymlinkPathSegments(root, targetPath, errorMessage) {
|
|
329
|
+
const resolvedRoot = path.resolve(root);
|
|
330
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
331
|
+
const relative = path.relative(resolvedRoot, resolvedTarget);
|
|
332
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
333
|
+
throw new Error(errorMessage);
|
|
334
|
+
}
|
|
335
|
+
let current = resolvedRoot;
|
|
336
|
+
const rootStat = await fs.lstat(current);
|
|
337
|
+
if (rootStat.isSymbolicLink()) {
|
|
338
|
+
throw new Error(errorMessage);
|
|
339
|
+
}
|
|
340
|
+
if (!relative) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
for (const segment of relative.split(path.sep)) {
|
|
344
|
+
if (!segment) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
current = path.join(current, segment);
|
|
348
|
+
const stat = await fs.lstat(current).catch((error) => {
|
|
349
|
+
if (error.code === "ENOENT")
|
|
350
|
+
return null;
|
|
351
|
+
throw error;
|
|
352
|
+
});
|
|
353
|
+
if (!stat) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (stat.isSymbolicLink()) {
|
|
357
|
+
throw new Error(errorMessage);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
243
361
|
async function pathExists(inputPath) {
|
|
244
362
|
return fs.stat(inputPath).then(() => true).catch(() => false);
|
|
245
363
|
}
|
|
@@ -259,6 +377,14 @@ function uniqueStrings(values) {
|
|
|
259
377
|
}
|
|
260
378
|
return result;
|
|
261
379
|
}
|
|
380
|
+
function worktreeKeySegments(worktreeKey, fallbackKind, fallbackShortId) {
|
|
381
|
+
const segments = worktreeKey
|
|
382
|
+
.split("/")
|
|
383
|
+
.map((segment) => segment.trim())
|
|
384
|
+
.filter((segment) => segment.length > 0)
|
|
385
|
+
.map((segment) => safeSegment(segment, "worktree"));
|
|
386
|
+
return segments.length > 0 ? segments : [fallbackKind, fallbackShortId];
|
|
387
|
+
}
|
|
262
388
|
function isSinglePathSegment(value) {
|
|
263
389
|
return value.length > 0 && value !== "." && value !== ".." && !value.includes("/") && !value.includes("\\");
|
|
264
390
|
}
|