@loucompanion/forge-bridge 0.1.3-dev.04a84914da09 → 0.1.3-dev.09aeeb7ccadf
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 +24 -4
- package/dist/bridge/bridge.types.d.ts +4 -0
- package/dist/cli/cli.help.d.ts +2 -0
- package/dist/cli/cli.help.js +171 -0
- package/dist/cli/cli.service.js +8 -183
- package/dist/cli/local-config.d.ts +2 -0
- package/dist/cli/local-config.js +6 -2
- package/dist/session/phase-completion.artifacts.d.ts +10 -0
- package/dist/session/phase-completion.artifacts.js +620 -0
- package/dist/session/phase-completion.d.ts +32 -44
- package/dist/session/phase-completion.helpers.d.ts +112 -0
- package/dist/session/phase-completion.helpers.js +660 -0
- package/dist/session/phase-completion.js +60 -3173
- package/dist/session/phase-completion.prompts.d.ts +14 -0
- package/dist/session/phase-completion.prompts.js +220 -0
- package/dist/session/phase-completion.runtime.d.ts +36 -0
- package/dist/session/phase-completion.runtime.js +609 -0
- package/dist/session/phase-completion.schemas.d.ts +3 -0
- package/dist/session/phase-completion.schemas.js +707 -0
- package/dist/session/phase-completion.validation.d.ts +34 -0
- package/dist/session/phase-completion.validation.js +724 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +93 -0
- package/dist/session/provider-cli/provider-cli.artifacts.js +754 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +12 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +318 -0
- package/dist/session/provider-cli/provider-cli.process.d.ts +32 -0
- package/dist/session/provider-cli/provider-cli.process.js +287 -0
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +42 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +497 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +65 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +9 -0
- package/dist/session/provider-cli/provider-cli.runtime.js +40 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +6 -53
- package/dist/session/provider-cli/provider-cli.service.js +132 -2507
- package/dist/session/provider-cli/provider-cli.types.d.ts +1 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +20 -0
- package/dist/session/provider-cli/provider-output-state.js +445 -0
- package/dist/session/session.service.js +20 -17
- package/dist/session/session.types.d.ts +81 -0
- package/dist/session/session.types.js +163 -1
- package/dist/shared/process/windows-launch.d.ts +8 -0
- package/dist/shared/process/windows-launch.js +41 -0
- package/dist/shared/provider-capabilities.d.ts +1 -0
- package/dist/shared/provider-capabilities.js +3 -0
- package/dist/shared/redaction/redaction.js +4 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.js +39 -6
- package/dist/worktree/dependency-warmer.d.ts +11 -0
- package/dist/worktree/dependency-warmer.js +45 -1
- package/dist/worktree/worktree.service.d.ts +2 -1
- package/dist/worktree/worktree.service.js +32 -12
- package/package.json +1 -1
|
@@ -20,10 +20,13 @@ export function parseRunOnceDispatchPacket(value) {
|
|
|
20
20
|
artifacts: parseArtifacts(packet.artifacts),
|
|
21
21
|
warmPathOverrides: parseWarmPathOverrides(packet.warmPathOverrides),
|
|
22
22
|
qaBrowser: parseQaBrowser(packet.qaBrowser),
|
|
23
|
+
qaScope: parseQaScope(packet.qaScope ?? packet.qa_scope),
|
|
23
24
|
qaRework: parseQaRework(packet.qaRework),
|
|
24
25
|
architectRevision: parseArchitectRevision(packet.architectRevision),
|
|
26
|
+
architectDependencies: parseArchitectDependencies(packet.architectDependencies),
|
|
25
27
|
storyDevelop: parseStoryDevelop(packet.storyDevelop),
|
|
26
28
|
storyQa: parseStoryQa(packet.storyQa),
|
|
29
|
+
designerRefs: parseDesignerRefs(packet.designerRefs ?? packet.designer_refs),
|
|
27
30
|
draft: parseDraftPrompt(packet.draft)
|
|
28
31
|
};
|
|
29
32
|
}
|
|
@@ -118,9 +121,45 @@ function parseDraftPrompt(value) {
|
|
|
118
121
|
return {
|
|
119
122
|
kind: requiredString(draft, "kind"),
|
|
120
123
|
systemPrompt: requiredString(draft, "systemPrompt"),
|
|
121
|
-
userPrompt: requiredString(draft, "userPrompt")
|
|
124
|
+
userPrompt: requiredString(draft, "userPrompt"),
|
|
125
|
+
projectName: optionalString(draft, "projectName"),
|
|
126
|
+
repoName: optionalString(draft, "repoName"),
|
|
127
|
+
attachments: parseDraftAttachments(draft.attachments)
|
|
122
128
|
};
|
|
123
129
|
}
|
|
130
|
+
function parseDraftAttachments(value) {
|
|
131
|
+
if (value === undefined || value === null) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
if (!Array.isArray(value)) {
|
|
135
|
+
throw new Error("draft.attachments must be an array when provided.");
|
|
136
|
+
}
|
|
137
|
+
return value.map((entry, index) => {
|
|
138
|
+
const attachment = asObject(entry, `draft.attachments[${index}]`);
|
|
139
|
+
const kind = requiredString(attachment, "kind").trim().toLowerCase();
|
|
140
|
+
if (kind !== "text" && kind !== "image") {
|
|
141
|
+
throw new Error(`draft.attachments[${index}].kind must be text or image.`);
|
|
142
|
+
}
|
|
143
|
+
const textContent = optionalString(attachment, "textContent");
|
|
144
|
+
const contentBase64 = optionalString(attachment, "contentBase64");
|
|
145
|
+
if (kind === "text" && textContent === null) {
|
|
146
|
+
throw new Error(`draft.attachments[${index}].textContent is required for text attachments.`);
|
|
147
|
+
}
|
|
148
|
+
if (kind === "image" && contentBase64 === null) {
|
|
149
|
+
throw new Error(`draft.attachments[${index}].contentBase64 is required for image attachments.`);
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
index: optionalNonNegativeInteger(attachment, "index") ?? index,
|
|
153
|
+
fileName: requiredString(attachment, "fileName"),
|
|
154
|
+
contentType: optionalString(attachment, "contentType") ?? "",
|
|
155
|
+
kind,
|
|
156
|
+
byteCount: optionalNonNegativeInteger(attachment, "byteCount") ?? 0,
|
|
157
|
+
summary: optionalString(attachment, "summary") ?? "",
|
|
158
|
+
textContent,
|
|
159
|
+
contentBase64
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
}
|
|
124
163
|
function parseApiKeySecretGrant(value) {
|
|
125
164
|
if (value === undefined || value === null) {
|
|
126
165
|
return null;
|
|
@@ -197,6 +236,27 @@ function parseQaBrowser(value) {
|
|
|
197
236
|
missingTargetReason: optionalString(qaBrowser, "missingTargetReason") ?? optionalString(qaBrowser, "missing_target_reason")
|
|
198
237
|
};
|
|
199
238
|
}
|
|
239
|
+
function parseQaScope(value) {
|
|
240
|
+
if (value === undefined || value === null) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
const qaScope = asObject(value, "qaScope");
|
|
244
|
+
return {
|
|
245
|
+
schema_version: requiredNumber(qaScope, "schema_version"),
|
|
246
|
+
surface: parseOptionalStringArray(qaScope.surface, "qaScope.surface"),
|
|
247
|
+
required_evidence: parseOptionalStringArray(qaScope.required_evidence, "qaScope.required_evidence"),
|
|
248
|
+
not_applicable: parseOptionalStringArray(qaScope.not_applicable, "qaScope.not_applicable"),
|
|
249
|
+
browser_required: optionalBoolean(qaScope, "browser_required") ?? optionalBoolean(qaScope, "browserRequired") ?? false,
|
|
250
|
+
api_required: optionalBoolean(qaScope, "api_required") ?? optionalBoolean(qaScope, "apiRequired") ?? false,
|
|
251
|
+
source: optionalString(qaScope, "source"),
|
|
252
|
+
authority: optionalString(qaScope, "authority"),
|
|
253
|
+
confidence: optionalString(qaScope, "confidence"),
|
|
254
|
+
confirmed_by_user: optionalBoolean(qaScope, "confirmed_by_user") ?? optionalBoolean(qaScope, "confirmedByUser"),
|
|
255
|
+
locked: optionalBoolean(qaScope, "locked"),
|
|
256
|
+
derived_from: parseOptionalObject(qaScope.derived_from ?? qaScope.derivedFrom, "qaScope.derived_from"),
|
|
257
|
+
rationale: optionalString(qaScope, "rationale")
|
|
258
|
+
};
|
|
259
|
+
}
|
|
200
260
|
function parseQaAppLaunchTarget(value) {
|
|
201
261
|
if (value === undefined || value === null) {
|
|
202
262
|
return null;
|
|
@@ -260,6 +320,34 @@ function parseArchitectRevision(value) {
|
|
|
260
320
|
feedback: requiredString(revision, "feedback")
|
|
261
321
|
};
|
|
262
322
|
}
|
|
323
|
+
function parseArchitectDependencies(value) {
|
|
324
|
+
if (value === undefined || value === null) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
const context = asObject(value, "architectDependencies");
|
|
328
|
+
const prerequisites = context.prerequisites;
|
|
329
|
+
if (!Array.isArray(prerequisites)) {
|
|
330
|
+
throw new Error("architectDependencies.prerequisites must be an array.");
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
baseBranch: requiredString(context, "baseBranch").trim(),
|
|
334
|
+
prerequisites: prerequisites.map((entry, index) => {
|
|
335
|
+
const prerequisite = asObject(entry, `architectDependencies.prerequisites[${index}]`);
|
|
336
|
+
return {
|
|
337
|
+
storyId: requiredString(prerequisite, "storyId").trim(),
|
|
338
|
+
title: requiredString(prerequisite, "title").trim(),
|
|
339
|
+
scheduleIndex: optionalNonNegativeInteger(prerequisite, "scheduleIndex"),
|
|
340
|
+
waveIndex: optionalNonNegativeInteger(prerequisite, "waveIndex"),
|
|
341
|
+
featureScheduleIndex: optionalNonNegativeInteger(prerequisite, "featureScheduleIndex"),
|
|
342
|
+
expectedOutput: optionalString(prerequisite, "expectedOutput"),
|
|
343
|
+
handoffSummary: optionalString(prerequisite, "handoffSummary"),
|
|
344
|
+
dependencyReason: optionalString(prerequisite, "dependencyReason"),
|
|
345
|
+
pullRequestNumber: optionalPositiveInteger(prerequisite, "pullRequestNumber"),
|
|
346
|
+
targetBranch: requiredString(prerequisite, "targetBranch").trim()
|
|
347
|
+
};
|
|
348
|
+
})
|
|
349
|
+
};
|
|
350
|
+
}
|
|
263
351
|
function parseStoryDevelop(value) {
|
|
264
352
|
if (value === undefined || value === null) {
|
|
265
353
|
return null;
|
|
@@ -365,6 +453,58 @@ function parseStoryQaDependencies(value) {
|
|
|
365
453
|
};
|
|
366
454
|
});
|
|
367
455
|
}
|
|
456
|
+
function parseDesignerRefs(value) {
|
|
457
|
+
if (value === undefined || value === null) {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
const context = asObject(value, "designerRefs");
|
|
461
|
+
const refs = context.refs;
|
|
462
|
+
if (!Array.isArray(refs)) {
|
|
463
|
+
throw new Error("designerRefs.refs must be an array.");
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
schemaVersion: requiredNumber(context, "schemaVersion"),
|
|
467
|
+
kind: requiredString(context, "kind"),
|
|
468
|
+
ownerType: requiredString(context, "ownerType"),
|
|
469
|
+
ownerId: requiredString(context, "ownerId"),
|
|
470
|
+
projectId: requiredString(context, "projectId"),
|
|
471
|
+
repoId: requiredString(context, "repoId"),
|
|
472
|
+
title: requiredString(context, "title"),
|
|
473
|
+
refs: refs.map((entry, index) => parseDesignerRefAttachment(entry, index))
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
function parseDesignerRefAttachment(value, index) {
|
|
477
|
+
const attachment = asObject(value, `designerRefs.refs[${index}]`);
|
|
478
|
+
return {
|
|
479
|
+
ref: parseDesignerRef(attachment.ref, index),
|
|
480
|
+
reason: optionalString(attachment, "reason") ?? "",
|
|
481
|
+
required: requiredBoolean(attachment, "required"),
|
|
482
|
+
source: optionalString(attachment, "source") ?? "",
|
|
483
|
+
lineageKind: optionalString(attachment, "lineageKind") ?? optionalString(attachment, "lineage_kind") ?? "",
|
|
484
|
+
inheritedFromItemType: optionalString(attachment, "inheritedFromItemType") ?? optionalString(attachment, "inherited_from_item_type"),
|
|
485
|
+
inheritedFromItemId: optionalString(attachment, "inheritedFromItemId") ?? optionalString(attachment, "inherited_from_item_id"),
|
|
486
|
+
inheritedFromDesignerRefId: optionalString(attachment, "inheritedFromDesignerRefId") ?? optionalString(attachment, "inherited_from_designer_ref_id"),
|
|
487
|
+
addedByLane: optionalString(attachment, "addedByLane") ?? optionalString(attachment, "added_by_lane") ?? "",
|
|
488
|
+
sortOrder: requiredNumber(attachment, "sortOrder"),
|
|
489
|
+
createdAt: requiredString(attachment, "createdAt")
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
function parseDesignerRef(value, index) {
|
|
493
|
+
const designerRef = asObject(value, `designerRefs.refs[${index}].ref`);
|
|
494
|
+
return {
|
|
495
|
+
id: requiredString(designerRef, "id"),
|
|
496
|
+
projectId: requiredString(designerRef, "projectId"),
|
|
497
|
+
repoId: requiredString(designerRef, "repoId"),
|
|
498
|
+
refKey: requiredString(designerRef, "refKey"),
|
|
499
|
+
kind: requiredString(designerRef, "kind"),
|
|
500
|
+
path: requiredString(designerRef, "path"),
|
|
501
|
+
anchor: optionalString(designerRef, "anchor"),
|
|
502
|
+
title: optionalString(designerRef, "title") ?? "",
|
|
503
|
+
contentHash: requiredString(designerRef, "contentHash"),
|
|
504
|
+
commitSha: requiredString(designerRef, "commitSha"),
|
|
505
|
+
createdAt: requiredString(designerRef, "createdAt")
|
|
506
|
+
};
|
|
507
|
+
}
|
|
368
508
|
function parseStringArray(value, label) {
|
|
369
509
|
if (!Array.isArray(value)) {
|
|
370
510
|
throw new Error(`${label} must be an array.`);
|
|
@@ -376,6 +516,18 @@ function parseStringArray(value, label) {
|
|
|
376
516
|
return entry.trim();
|
|
377
517
|
});
|
|
378
518
|
}
|
|
519
|
+
function parseOptionalStringArray(value, label) {
|
|
520
|
+
if (value === undefined || value === null) {
|
|
521
|
+
return [];
|
|
522
|
+
}
|
|
523
|
+
return parseStringArray(value, label);
|
|
524
|
+
}
|
|
525
|
+
function parseOptionalObject(value, label) {
|
|
526
|
+
if (value === undefined || value === null) {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
return asObject(value, label);
|
|
530
|
+
}
|
|
379
531
|
function parseArtifacts(value) {
|
|
380
532
|
if (value === undefined || value === null) {
|
|
381
533
|
return [];
|
|
@@ -447,6 +599,16 @@ function optionalPositiveInteger(source, key) {
|
|
|
447
599
|
}
|
|
448
600
|
return value;
|
|
449
601
|
}
|
|
602
|
+
function optionalNonNegativeInteger(source, key) {
|
|
603
|
+
const value = source[key];
|
|
604
|
+
if (value === undefined || value === null) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
608
|
+
throw new Error(`${key} must be a non-negative integer when provided.`);
|
|
609
|
+
}
|
|
610
|
+
return value;
|
|
611
|
+
}
|
|
450
612
|
function requiredNumber(source, key) {
|
|
451
613
|
const value = source[key];
|
|
452
614
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface LaunchCommand {
|
|
2
|
+
readonly command: string;
|
|
3
|
+
readonly args: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
export declare function buildWindowsCmdLaunch(command: string, args: readonly string[], comSpec?: string | undefined): LaunchCommand;
|
|
6
|
+
export declare function buildWindowsLaunchCommand(commandPath: string, args: readonly string[]): LaunchCommand;
|
|
7
|
+
export declare function windowsCommandLine(command: string, args: readonly string[]): string;
|
|
8
|
+
export declare function quoteWindowsCommandLineArg(value: string): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function buildWindowsCmdLaunch(command, args, comSpec = process.env.ComSpec) {
|
|
3
|
+
const resolvedComSpec = comSpec?.trim();
|
|
4
|
+
const commandLine = windowsCommandLine(command, args);
|
|
5
|
+
const cmdPayload = commandLine.startsWith('"') ? `"${commandLine}"` : commandLine;
|
|
6
|
+
return {
|
|
7
|
+
command: resolvedComSpec || "cmd.exe",
|
|
8
|
+
args: ["/d", "/s", "/v:off", "/c", cmdPayload]
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function buildWindowsLaunchCommand(commandPath, args) {
|
|
12
|
+
const extension = path.extname(commandPath).toLowerCase();
|
|
13
|
+
if (extension === ".ps1") {
|
|
14
|
+
return {
|
|
15
|
+
command: powershellExecutable(),
|
|
16
|
+
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", commandPath, ...args]
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (extension === ".cmd" || extension === ".bat") {
|
|
20
|
+
return buildWindowsCmdLaunch(commandPath, args);
|
|
21
|
+
}
|
|
22
|
+
return { command: commandPath, args };
|
|
23
|
+
}
|
|
24
|
+
export function windowsCommandLine(command, args) {
|
|
25
|
+
return [command, ...args].map(quoteWindowsCommandLineArg).join(" ");
|
|
26
|
+
}
|
|
27
|
+
export function quoteWindowsCommandLineArg(value) {
|
|
28
|
+
const escaped = value
|
|
29
|
+
.replace(/\^/g, "^^")
|
|
30
|
+
.replace(/%/g, "^%");
|
|
31
|
+
if (escaped.length > 0 && !/[\s"&|<>()^%!]/.test(escaped)) {
|
|
32
|
+
return escaped;
|
|
33
|
+
}
|
|
34
|
+
return `"${escaped.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, "$1$1")}"`;
|
|
35
|
+
}
|
|
36
|
+
function powershellExecutable() {
|
|
37
|
+
const systemRoot = process.env.SystemRoot;
|
|
38
|
+
return systemRoot
|
|
39
|
+
? path.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
|
|
40
|
+
: "powershell.exe";
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function supportsDraftImageAttachmentsFor(cliProviders: readonly string[]): boolean;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
const secretKeyPattern = /(api[_-]?key|authorization|bearer|client[_-]?secret|password|secret|token)/i;
|
|
2
|
+
const attachmentContentKeyPattern = /^(textContent|contentBase64)$/i;
|
|
3
|
+
const serializedAttachmentContentPattern = /("(?:textContent|contentBase64)"\s*:\s*")(?:(?:\\.)|[^"\\])*(")/gi;
|
|
2
4
|
export function redactSecrets(value, exactSecrets = []) {
|
|
3
5
|
return redactValue(value, exactSecrets);
|
|
4
6
|
}
|
|
5
7
|
export function redactText(value, exactSecrets = []) {
|
|
6
|
-
let redacted = value;
|
|
8
|
+
let redacted = value.replace(serializedAttachmentContentPattern, "$1[REDACTED]$2");
|
|
7
9
|
for (const secret of exactSecrets) {
|
|
8
10
|
if (secret.length > 0) {
|
|
9
11
|
redacted = redacted.split(secret).join("[REDACTED]");
|
|
@@ -25,6 +27,6 @@ function redactValue(value, exactSecrets) {
|
|
|
25
27
|
}
|
|
26
28
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
27
29
|
key,
|
|
28
|
-
secretKeyPattern.test(key) ? "[REDACTED]" : redactValue(item, exactSecrets)
|
|
30
|
+
secretKeyPattern.test(key) || attachmentContentKeyPattern.test(key) ? "[REDACTED]" : redactValue(item, exactSecrets)
|
|
29
31
|
]));
|
|
30
32
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const forgeBridgeVersion = "0.1.3-dev.
|
|
1
|
+
export declare const forgeBridgeVersion = "0.1.3-dev.09aeeb7ccadf";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const forgeBridgeVersion = "0.1.3-dev.
|
|
1
|
+
export const forgeBridgeVersion = "0.1.3-dev.09aeeb7ccadf";
|
|
@@ -36,19 +36,29 @@ const DRAFT_OPERATIONAL_SKILLS = new Set([
|
|
|
36
36
|
"draft-repo-context",
|
|
37
37
|
"draft-graph-context",
|
|
38
38
|
"draft-product-scope",
|
|
39
|
-
"draft-work-order-plan",
|
|
40
39
|
"draft-critic",
|
|
41
40
|
"draft-proposal"
|
|
42
41
|
]);
|
|
42
|
+
const DRAFT_WORK_ITEM_OPERATIONAL_SKILLS = new Set([
|
|
43
|
+
"draft-work-item-plan"
|
|
44
|
+
]);
|
|
45
|
+
const DRAFT_WORK_ORDER_OPERATIONAL_SKILLS = new Set([
|
|
46
|
+
"draft-work-order-plan"
|
|
47
|
+
]);
|
|
43
48
|
const DRAFT_OPERATIONAL_HOOKS = new Set([
|
|
44
49
|
"draft-intent",
|
|
45
50
|
"draft-repo-context",
|
|
46
51
|
"draft-graph-context",
|
|
47
52
|
"draft-product-scope",
|
|
48
|
-
"draft-work-order-plan",
|
|
49
53
|
"draft-critic",
|
|
50
54
|
"draft-proposal"
|
|
51
55
|
]);
|
|
56
|
+
const DRAFT_WORK_ITEM_OPERATIONAL_HOOKS = new Set([
|
|
57
|
+
"draft-work-item-plan"
|
|
58
|
+
]);
|
|
59
|
+
const DRAFT_WORK_ORDER_OPERATIONAL_HOOKS = new Set([
|
|
60
|
+
"draft-work-order-plan"
|
|
61
|
+
]);
|
|
52
62
|
export async function injectWorktreeArtifacts(worktreePath, artifacts) {
|
|
53
63
|
for (const artifact of artifacts) {
|
|
54
64
|
await writeArtifact(worktreePath, artifact);
|
|
@@ -64,7 +74,7 @@ export function filterArtifactsForLane(artifacts, lane, phase) {
|
|
|
64
74
|
return artifacts.filter((artifact) => shouldInjectArchitectArtifact(artifact, commandSlug));
|
|
65
75
|
}
|
|
66
76
|
if (normalizedLane === "draft") {
|
|
67
|
-
return artifacts.filter((artifact) => shouldInjectDraftArtifact(artifact, commandSlug));
|
|
77
|
+
return artifacts.filter((artifact) => shouldInjectDraftArtifact(artifact, commandSlug, phase));
|
|
68
78
|
}
|
|
69
79
|
return artifacts;
|
|
70
80
|
}
|
|
@@ -142,7 +152,7 @@ function shouldInjectArchitectArtifact(artifact, commandSlug) {
|
|
|
142
152
|
return false;
|
|
143
153
|
}
|
|
144
154
|
}
|
|
145
|
-
function shouldInjectDraftArtifact(artifact, commandSlug) {
|
|
155
|
+
function shouldInjectDraftArtifact(artifact, commandSlug, phase) {
|
|
146
156
|
if (artifact.scope.trim().toLowerCase() !== "operational") {
|
|
147
157
|
return false;
|
|
148
158
|
}
|
|
@@ -151,13 +161,36 @@ function shouldInjectDraftArtifact(artifact, commandSlug) {
|
|
|
151
161
|
case "command":
|
|
152
162
|
return slug === commandSlug;
|
|
153
163
|
case "skill":
|
|
154
|
-
return DRAFT_OPERATIONAL_SKILLS.has(slug);
|
|
164
|
+
return DRAFT_OPERATIONAL_SKILLS.has(slug) || draftPhaseSkills(phase).has(slug);
|
|
155
165
|
case "hook":
|
|
156
|
-
return DRAFT_OPERATIONAL_HOOKS.has(slug);
|
|
166
|
+
return DRAFT_OPERATIONAL_HOOKS.has(slug) || draftPhaseHooks(phase).has(slug);
|
|
157
167
|
default:
|
|
158
168
|
return false;
|
|
159
169
|
}
|
|
160
170
|
}
|
|
171
|
+
function draftPhaseSkills(phase) {
|
|
172
|
+
switch (normalizeDraftPhase(phase)) {
|
|
173
|
+
case "work_order":
|
|
174
|
+
return DRAFT_WORK_ORDER_OPERATIONAL_SKILLS;
|
|
175
|
+
case "story":
|
|
176
|
+
return DRAFT_WORK_ITEM_OPERATIONAL_SKILLS;
|
|
177
|
+
default:
|
|
178
|
+
return DRAFT_WORK_ITEM_OPERATIONAL_SKILLS;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function draftPhaseHooks(phase) {
|
|
182
|
+
switch (normalizeDraftPhase(phase)) {
|
|
183
|
+
case "work_order":
|
|
184
|
+
return DRAFT_WORK_ORDER_OPERATIONAL_HOOKS;
|
|
185
|
+
case "story":
|
|
186
|
+
return DRAFT_WORK_ITEM_OPERATIONAL_HOOKS;
|
|
187
|
+
default:
|
|
188
|
+
return DRAFT_WORK_ITEM_OPERATIONAL_HOOKS;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function normalizeDraftPhase(phase) {
|
|
192
|
+
return phase.trim().toLowerCase().replaceAll("-", "_");
|
|
193
|
+
}
|
|
161
194
|
export function buildSpecBody(laneCommand, workItem, phase = "implementation") {
|
|
162
195
|
const prompt = laneCommand.body.replaceAll("{workItemId}", workItem.id);
|
|
163
196
|
return `---
|
|
@@ -30,3 +30,14 @@ export declare function createDirectoryLink(sourcePath: string, targetPath: stri
|
|
|
30
30
|
export declare function lockfilesDiffer(baseCheckoutPath: string, worktreePath: string, packageRootRelative: string): Promise<boolean>;
|
|
31
31
|
export declare function requiresInstallFallback(entry: WarmPathEntry, baseCheckoutPath: string, worktreePath: string, sourceExists: boolean): Promise<string | null>;
|
|
32
32
|
export declare function declaresNativeModules(packageRootPath: string): Promise<boolean>;
|
|
33
|
+
export interface NpmLaunchOptions {
|
|
34
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
35
|
+
readonly execPath?: string;
|
|
36
|
+
readonly platform?: NodeJS.Platform;
|
|
37
|
+
readonly homeDir?: string;
|
|
38
|
+
readonly candidates?: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
export declare function resolveNpmLaunch(args: readonly string[], options?: NpmLaunchOptions): {
|
|
41
|
+
command: string;
|
|
42
|
+
args: readonly string[];
|
|
43
|
+
};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import { NodeProcessRunner } from "../shared/process/process.js";
|
|
6
|
+
import { buildWindowsCmdLaunch } from "../shared/process/windows-launch.js";
|
|
4
7
|
import { isSubPath } from "../shared/path/path.js";
|
|
5
8
|
const SKIP_DIR_NAMES = new Set([
|
|
6
9
|
".git",
|
|
@@ -184,7 +187,8 @@ async function runInstallFallback(entry, worktreePath, reason, executor) {
|
|
|
184
187
|
if (entry.ecosystem === "node") {
|
|
185
188
|
const lockPath = path.join(cwd, "package-lock.json");
|
|
186
189
|
const args = (await pathExists(lockPath)) ? ["ci"] : ["install"];
|
|
187
|
-
|
|
190
|
+
const launch = resolveNpmLaunch(args);
|
|
191
|
+
await executor.run(cwd, launch.command, launch.args);
|
|
188
192
|
return { ecosystem: entry.ecosystem, packageRoot: entry.packageRoot, reason, command: `npm ${args.join(" ")}` };
|
|
189
193
|
}
|
|
190
194
|
if (entry.ecosystem === "python") {
|
|
@@ -286,6 +290,46 @@ async function sha256File(filePath) {
|
|
|
286
290
|
const content = await fs.readFile(filePath);
|
|
287
291
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
288
292
|
}
|
|
293
|
+
export function resolveNpmLaunch(args, options = {}) {
|
|
294
|
+
const env = options.env ?? process.env;
|
|
295
|
+
const execPath = options.execPath ?? process.execPath;
|
|
296
|
+
const npmExecPath = env.npm_execpath;
|
|
297
|
+
if (npmExecPath) {
|
|
298
|
+
return { command: execPath, args: [npmExecPath, ...args] };
|
|
299
|
+
}
|
|
300
|
+
const platform = options.platform ?? process.platform;
|
|
301
|
+
if (platform === "win32") {
|
|
302
|
+
const comSpec = options.env ? env.ComSpec ?? "cmd.exe" : env.ComSpec;
|
|
303
|
+
return buildWindowsCmdLaunch("npm.cmd", args, comSpec);
|
|
304
|
+
}
|
|
305
|
+
for (const candidate of options.candidates ?? defaultNpmCliCandidates(execPath, options.homeDir ?? os.homedir())) {
|
|
306
|
+
const resolvedCandidate = tryResolveNpmCandidate(candidate);
|
|
307
|
+
if (resolvedCandidate) {
|
|
308
|
+
return { command: execPath, args: [resolvedCandidate, ...args] };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return { command: "npm", args };
|
|
312
|
+
}
|
|
313
|
+
function tryResolveNpmCandidate(candidate) {
|
|
314
|
+
try {
|
|
315
|
+
if (!existsSync(candidate)) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
return realpathSync(candidate);
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function defaultNpmCliCandidates(execPath, homeDir) {
|
|
325
|
+
const candidates = [];
|
|
326
|
+
const execDir = path.dirname(execPath);
|
|
327
|
+
if (path.isAbsolute(execDir)) {
|
|
328
|
+
candidates.push(path.join(execDir, "npm"), path.join(execDir, "npm-cli.js"), path.join(execDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"));
|
|
329
|
+
}
|
|
330
|
+
candidates.push("/usr/bin/npm", "/usr/local/bin/npm", path.join(homeDir, ".npm-global", "bin", "npm"), path.join(homeDir, ".npm-global", "lib", "node_modules", "npm", "bin", "npm-cli.js"));
|
|
331
|
+
return candidates;
|
|
332
|
+
}
|
|
289
333
|
const defaultCommandExecutor = {
|
|
290
334
|
async run(cwd, command, args) {
|
|
291
335
|
const result = await defaultProcessRunner.run({ command, cwd, args });
|
|
@@ -9,7 +9,8 @@ export declare class GitWorktreeService implements WorktreeService {
|
|
|
9
9
|
private resolveRepoPath;
|
|
10
10
|
private findExistingRepoPath;
|
|
11
11
|
private resolveMatchingRepo;
|
|
12
|
-
private
|
|
12
|
+
private resolveBaseStartPoint;
|
|
13
|
+
private resetWorktreeToBase;
|
|
13
14
|
private gitRefExists;
|
|
14
15
|
private currentBranch;
|
|
15
16
|
private ensureGitIdentity;
|
|
@@ -14,7 +14,7 @@ export class GitWorktreeService {
|
|
|
14
14
|
const baseBranch = normalizeBranchName(request.baseBranch, "develop");
|
|
15
15
|
const repoPath = await this.resolveRepoPath(request.repo, allowedRoot, baseBranch);
|
|
16
16
|
await this.ensureGitIdentity(repoPath);
|
|
17
|
-
await this.
|
|
17
|
+
const baseStartPoint = await this.resolveBaseStartPoint(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
20
|
const fallbackWorktreeKey = `${kind}/${shortId}`;
|
|
@@ -30,7 +30,7 @@ export class GitWorktreeService {
|
|
|
30
30
|
const exists = await directoryExists(worktreePath);
|
|
31
31
|
let resolvedWorktreePath;
|
|
32
32
|
if (!exists) {
|
|
33
|
-
await this.git.run(repoPath, ["worktree", "add", "-B", branchName, worktreePath,
|
|
33
|
+
await this.git.run(repoPath, ["worktree", "add", "-B", branchName, worktreePath, baseStartPoint]);
|
|
34
34
|
resolvedWorktreePath = await resolveExistingDirectoryPath(worktreePath);
|
|
35
35
|
}
|
|
36
36
|
else {
|
|
@@ -42,6 +42,9 @@ export class GitWorktreeService {
|
|
|
42
42
|
if (currentBranch !== branchName) {
|
|
43
43
|
throw new Error(`Existing worktree ${resolvedWorktreePath} is on branch '${currentBranch}', expected '${branchName}'.`);
|
|
44
44
|
}
|
|
45
|
+
if (shouldResetExistingWorktree(request)) {
|
|
46
|
+
await this.resetWorktreeToBase(resolvedWorktreePath, baseStartPoint);
|
|
47
|
+
}
|
|
45
48
|
}
|
|
46
49
|
await this.ensureGitIdentity(resolvedWorktreePath);
|
|
47
50
|
await clearRunArtifactRoots(resolvedWorktreePath, request);
|
|
@@ -164,21 +167,27 @@ export class GitWorktreeService {
|
|
|
164
167
|
const resolved = await resolveExistingDirectoryPath(candidate);
|
|
165
168
|
return isSubPath(allowedRoot, resolved) ? resolved : null;
|
|
166
169
|
}
|
|
167
|
-
async
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
170
|
+
async resolveBaseStartPoint(repoPath, baseBranch) {
|
|
171
|
+
const localRef = `refs/heads/${baseBranch}`;
|
|
172
|
+
const remoteRef = `refs/remotes/origin/${baseBranch}`;
|
|
171
173
|
const origin = await this.git.run(repoPath, ["remote", "get-url", "origin"]).catch(() => "");
|
|
172
|
-
if (origin.trim().length
|
|
173
|
-
|
|
174
|
+
if (origin.trim().length > 0) {
|
|
175
|
+
const fetched = await this.git.run(repoPath, ["fetch", "--prune", "origin"])
|
|
176
|
+
.then(() => true)
|
|
177
|
+
.catch(() => false);
|
|
178
|
+
if (fetched && await this.gitRefExists(repoPath, remoteRef)) {
|
|
179
|
+
return remoteRef;
|
|
180
|
+
}
|
|
174
181
|
}
|
|
175
|
-
await this.
|
|
176
|
-
|
|
177
|
-
await this.git.run(repoPath, ["branch", baseBranch, `refs/remotes/origin/${baseBranch}`]);
|
|
178
|
-
return;
|
|
182
|
+
if (await this.gitRefExists(repoPath, localRef)) {
|
|
183
|
+
return localRef;
|
|
179
184
|
}
|
|
180
185
|
throw new Error(`Base branch '${baseBranch}' was not found locally or on origin.`);
|
|
181
186
|
}
|
|
187
|
+
async resetWorktreeToBase(worktreePath, baseStartPoint) {
|
|
188
|
+
await this.git.run(worktreePath, ["reset", "--hard", baseStartPoint]);
|
|
189
|
+
await this.git.run(worktreePath, ["clean", "-fd"]);
|
|
190
|
+
}
|
|
182
191
|
async gitRefExists(repoPath, gitRef) {
|
|
183
192
|
return this.git.run(repoPath, ["show-ref", "--verify", "--quiet", gitRef]).then(() => true).catch(() => false);
|
|
184
193
|
}
|
|
@@ -269,6 +278,17 @@ function normalizeOwnerType(value) {
|
|
|
269
278
|
}
|
|
270
279
|
return null;
|
|
271
280
|
}
|
|
281
|
+
function shouldResetExistingWorktree(request) {
|
|
282
|
+
const lane = request.lane.trim().toLowerCase();
|
|
283
|
+
const phase = request.phase.trim().toLowerCase().replace(/-/g, "_");
|
|
284
|
+
if (lane === "develop" && (phase === "implementation" || phase === "self_review" || phase === "validation")) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
if (lane === "qa" || phase === "qa") {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
272
292
|
function qaReworkPacketFileName(packetPath) {
|
|
273
293
|
const normalized = packetPath.trim().replace(/\\/g, "/");
|
|
274
294
|
const fileName = normalized.split("/").filter(Boolean).at(-1) ?? "";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loucompanion/forge-bridge",
|
|
3
|
-
"version": "0.1.3-dev.
|
|
3
|
+
"version": "0.1.3-dev.09aeeb7ccadf",
|
|
4
4
|
"description": "Forge runner bridge CLI for connecting local developer machines to Forge dispatch.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|