@loucompanion/forge-bridge 0.1.3-dev.ba7d36726eda → 0.1.3-dev.bca7d0ccee53
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.base.d.ts +2 -1
- package/dist/bridge/bridge.service.d.ts +8 -0
- package/dist/bridge/bridge.service.js +184 -12
- package/dist/bridge/bridge.types.d.ts +37 -0
- package/dist/cli/cli.help.js +1 -1
- package/dist/cli/cli.service.js +1 -1
- package/dist/cli/local-config.js +2 -1
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +3 -1
- package/dist/runtime/attempt-node-runtime-launcher.js +62 -11
- package/dist/runtime/attempt-process-supervisor-registry.js +1 -1
- package/dist/runtime/attempt-process-supervisor.d.ts +12 -0
- package/dist/runtime/attempt-process-supervisor.js +75 -15
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +103 -0
- package/dist/runtime/host-listener-inspector.d.ts +16 -0
- package/dist/runtime/host-listener-inspector.js +143 -4
- package/dist/runtime/host-runtime-contract.d.ts +9 -1
- package/dist/runtime/host-runtime-contract.js +411 -6
- package/dist/runtime/host-runtime-lease.d.ts +12 -1
- package/dist/runtime/host-runtime-lease.js +271 -50
- package/dist/runtime/host-runtime-promotion.d.ts +2 -0
- package/dist/runtime/host-runtime-promotion.js +35 -4
- package/dist/runtime/node-host-runtime-adapter.d.ts +7 -0
- package/dist/runtime/node-host-runtime-adapter.js +71 -8
- package/dist/runtime/playwright-host-runtime-adapter.js +84 -0
- package/dist/sandbox/preflight.d.ts +9 -0
- package/dist/sandbox/preflight.js +39 -4
- package/dist/sandbox/workspace.service.js +7 -2
- package/dist/session/managed-attempt-tools.d.ts +17 -0
- package/dist/session/managed-attempt-tools.js +712 -0
- package/dist/session/phase-completion.artifacts.js +9 -2
- package/dist/session/phase-completion.d.ts +11 -2
- package/dist/session/phase-completion.helpers.d.ts +5 -0
- package/dist/session/phase-completion.helpers.js +45 -9
- package/dist/session/phase-completion.js +12 -3
- package/dist/session/phase-completion.prompts.d.ts +3 -3
- package/dist/session/phase-completion.prompts.js +21 -6
- package/dist/session/phase-completion.runtime.d.ts +13 -1
- package/dist/session/phase-completion.runtime.js +183 -8
- package/dist/session/phase-completion.schemas.js +108 -3
- package/dist/session/phase-completion.validation.d.ts +4 -1
- package/dist/session/phase-completion.validation.js +144 -18
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +95 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
- package/dist/session/provider-cli/provider-cli.process.d.ts +14 -0
- package/dist/session/provider-cli/provider-cli.process.js +176 -18
- package/dist/session/provider-cli/provider-cli.prompts.js +30 -8
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +19 -2
- package/dist/session/provider-cli/provider-cli.service.js +390 -83
- package/dist/session/provider-cli/provider-cli.types.d.ts +3 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.js +30 -2
- package/dist/session/session.base.d.ts +2 -1
- package/dist/session/session.service.d.ts +2 -1
- package/dist/session/session.service.js +303 -30
- package/dist/session/session.types.d.ts +38 -0
- package/dist/session/session.types.js +17 -0
- package/dist/shared/process/host-process-supervisor.d.ts +18 -1
- package/dist/shared/process/host-process-supervisor.js +94 -7
- package/dist/shared/process/process.d.ts +1 -0
- package/dist/shared/process/process.js +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.d.ts +3 -2
- package/dist/worktree/artifact-injector.js +43 -4
- package/dist/worktree/dependency-warmer.d.ts +10 -1
- package/dist/worktree/dependency-warmer.js +94 -5
- package/dist/worktree/worktree.service.d.ts +1 -0
- package/dist/worktree/worktree.service.js +12 -1
- package/package.json +1 -1
- package/runtime/windows-job-supervisor.ps1 +89 -20
|
@@ -13,8 +13,21 @@ export interface HostProcessSupervisedLaunch {
|
|
|
13
13
|
readonly supervisorId: string;
|
|
14
14
|
readonly strategy: HostProcessSupervisorStrategy;
|
|
15
15
|
readonly metadataPath: string;
|
|
16
|
+
readonly payloadPath?: string;
|
|
17
|
+
readonly readyPath?: string;
|
|
18
|
+
readonly startGrantPath?: string;
|
|
16
19
|
readonly commandDigest: string;
|
|
17
20
|
}
|
|
21
|
+
export interface HostProcessProviderReady {
|
|
22
|
+
readonly schemaVersion: 1;
|
|
23
|
+
readonly supervisorId: string;
|
|
24
|
+
readonly rootPid?: number;
|
|
25
|
+
readonly rootIdentity?: string | null;
|
|
26
|
+
readonly providerPid: number;
|
|
27
|
+
readonly providerCreatedAt: string;
|
|
28
|
+
readonly providerIdentity?: string | null;
|
|
29
|
+
readonly startAttempt: number;
|
|
30
|
+
}
|
|
18
31
|
export interface HostProcessSupervisorMetadata {
|
|
19
32
|
readonly schemaVersion: 1;
|
|
20
33
|
readonly supervisorId: string;
|
|
@@ -28,9 +41,13 @@ export interface HostProcessSupervisorMetadata {
|
|
|
28
41
|
readonly dispatchAttemptId: string;
|
|
29
42
|
readonly fencingToken: string;
|
|
30
43
|
readonly bridgePid: number;
|
|
44
|
+
readonly providerPid?: number;
|
|
45
|
+
readonly providerCreatedAt?: string;
|
|
46
|
+
readonly providerIdentity?: string | null;
|
|
47
|
+
readonly providerStartAttempt?: number;
|
|
31
48
|
}
|
|
32
49
|
export declare function resolveHostProcessSupervisedLaunch(request: HostProcessSupervisionRequest, resolvedCommand: string, resolvedArgs: readonly string[], platform?: NodeJS.Platform): HostProcessSupervisedLaunch | null;
|
|
33
|
-
export declare function persistHostProcessSupervisorMetadata(launch: HostProcessSupervisedLaunch, request: HostProcessSupervisionRequest, rootPid: number, rootCreatedAt: string): HostProcessSupervisorMetadata;
|
|
50
|
+
export declare function persistHostProcessSupervisorMetadata(launch: HostProcessSupervisedLaunch, request: HostProcessSupervisionRequest, rootPid: number, rootCreatedAt: string, providerReady?: HostProcessProviderReady): HostProcessSupervisorMetadata;
|
|
34
51
|
/**
|
|
35
52
|
* Captures an OS-issued process birth marker. Recovery compares this marker
|
|
36
53
|
* before signalling a persisted PID so PID reuse can never redirect cleanup
|
|
@@ -19,13 +19,22 @@ export function resolveHostProcessSupervisedLaunch(request, resolvedCommand, res
|
|
|
19
19
|
if (!fs.existsSync(windowsJobSupervisorPath)) {
|
|
20
20
|
throw new Error(`host_process_supervisor_missing: ${windowsJobSupervisorPath}`);
|
|
21
21
|
}
|
|
22
|
-
const
|
|
22
|
+
const payloadPath = `${metadataPath}.launch.json`;
|
|
23
|
+
const readyPath = `${metadataPath}.provider-ready.json`;
|
|
24
|
+
const startGrantPath = `${metadataPath}.provider-start-granted`;
|
|
25
|
+
fs.writeFileSync(payloadPath, `${JSON.stringify({
|
|
23
26
|
command: resolvedCommand,
|
|
24
27
|
args: resolvedArgs,
|
|
25
28
|
cwd: request.cwd,
|
|
26
29
|
parentPid: process.pid,
|
|
27
30
|
supervisorId,
|
|
28
|
-
|
|
31
|
+
readyPath,
|
|
32
|
+
startGrantPath,
|
|
33
|
+
})}\n`, {
|
|
34
|
+
encoding: "utf8",
|
|
35
|
+
flag: "wx",
|
|
36
|
+
mode: 0o600,
|
|
37
|
+
});
|
|
29
38
|
return {
|
|
30
39
|
command: windowsPowerShellPath(),
|
|
31
40
|
args: [
|
|
@@ -38,13 +47,16 @@ export function resolveHostProcessSupervisedLaunch(request, resolvedCommand, res
|
|
|
38
47
|
windowsJobSupervisorPath,
|
|
39
48
|
"-Mode",
|
|
40
49
|
"Outer",
|
|
41
|
-
"-
|
|
42
|
-
|
|
50
|
+
"-PayloadPath",
|
|
51
|
+
payloadPath,
|
|
43
52
|
],
|
|
44
53
|
detached: false,
|
|
45
54
|
supervisorId,
|
|
46
55
|
strategy: "windows_job_object",
|
|
47
56
|
metadataPath,
|
|
57
|
+
payloadPath,
|
|
58
|
+
readyPath,
|
|
59
|
+
startGrantPath,
|
|
48
60
|
commandDigest,
|
|
49
61
|
};
|
|
50
62
|
}
|
|
@@ -58,7 +70,7 @@ export function resolveHostProcessSupervisedLaunch(request, resolvedCommand, res
|
|
|
58
70
|
commandDigest,
|
|
59
71
|
};
|
|
60
72
|
}
|
|
61
|
-
export function persistHostProcessSupervisorMetadata(launch, request, rootPid, rootCreatedAt) {
|
|
73
|
+
export function persistHostProcessSupervisorMetadata(launch, request, rootPid, rootCreatedAt, providerReady) {
|
|
62
74
|
if (!Number.isSafeInteger(rootPid) || rootPid <= 0) {
|
|
63
75
|
throw new Error("host_process_supervisor_invalid: supervised root PID was not assigned.");
|
|
64
76
|
}
|
|
@@ -69,13 +81,20 @@ export function persistHostProcessSupervisorMetadata(launch, request, rootPid, r
|
|
|
69
81
|
strategy: launch.strategy,
|
|
70
82
|
rootPid,
|
|
71
83
|
rootCreatedAt,
|
|
72
|
-
rootIdentity:
|
|
84
|
+
rootIdentity: providerReady?.rootPid === rootPid
|
|
85
|
+
? providerReady.rootIdentity ?? null
|
|
86
|
+
: captureHostProcessIdentity(rootPid),
|
|
73
87
|
commandDigest: launch.commandDigest,
|
|
74
88
|
worktreePath: request.cwd,
|
|
75
89
|
runtimeLeaseId: requiredEnvironment(env, "FORGE_RESOURCE_LEASE_ID"),
|
|
76
90
|
dispatchAttemptId: requiredEnvironment(env, "FORGE_DISPATCH_ATTEMPT_ID"),
|
|
77
91
|
fencingToken: requiredEnvironment(env, "FORGE_FENCING_TOKEN"),
|
|
78
92
|
bridgePid: process.pid,
|
|
93
|
+
providerPid: providerReady?.providerPid,
|
|
94
|
+
providerCreatedAt: providerReady?.providerCreatedAt,
|
|
95
|
+
providerIdentity: providerReady?.providerIdentity
|
|
96
|
+
?? (providerReady ? captureHostProcessIdentity(providerReady.providerPid) : undefined),
|
|
97
|
+
providerStartAttempt: providerReady?.startAttempt,
|
|
79
98
|
};
|
|
80
99
|
fs.writeFileSync(launch.metadataPath, `${JSON.stringify(metadata)}\n`, {
|
|
81
100
|
encoding: "utf8",
|
|
@@ -113,7 +132,7 @@ export function captureHostProcessIdentity(pid, platform = process.platform) {
|
|
|
113
132
|
], {
|
|
114
133
|
encoding: "utf8",
|
|
115
134
|
windowsHide: true,
|
|
116
|
-
timeout:
|
|
135
|
+
timeout: 10_000,
|
|
117
136
|
stdio: ["ignore", "pipe", "ignore"],
|
|
118
137
|
}).trim();
|
|
119
138
|
return /^\d+$/u.test(output) ? `win32:${output}` : null;
|
|
@@ -137,12 +156,34 @@ export async function terminateHostProcessTree(child, launch, graceMs = 2_000) {
|
|
|
137
156
|
return;
|
|
138
157
|
}
|
|
139
158
|
if (launch.strategy === "windows_job_object") {
|
|
159
|
+
const rootPid = child.pid ?? 0;
|
|
160
|
+
const metadata = readSupervisorMetadata(launch.metadataPath);
|
|
161
|
+
const rootIdentity = metadata?.rootPid === rootPid
|
|
162
|
+
? metadata.rootIdentity ?? null
|
|
163
|
+
: captureHostProcessIdentity(rootPid, "win32");
|
|
140
164
|
try {
|
|
141
165
|
child.kill("SIGTERM");
|
|
142
166
|
}
|
|
143
167
|
catch {
|
|
144
168
|
// Closing the launcher is best effort when the Job Object already settled.
|
|
145
169
|
}
|
|
170
|
+
await waitForExactProcessExit(rootPid, rootIdentity, graceMs, "win32");
|
|
171
|
+
if (exactProcessExists(rootPid, rootIdentity, "win32")) {
|
|
172
|
+
forceWindowsProcessTree(rootPid);
|
|
173
|
+
await waitForExactProcessExit(rootPid, rootIdentity, graceMs, "win32");
|
|
174
|
+
}
|
|
175
|
+
if (exactProcessExists(rootPid, rootIdentity, "win32")) {
|
|
176
|
+
throw new Error("host_process_supervisor_termination_failed: supervised Windows root did not stop.");
|
|
177
|
+
}
|
|
178
|
+
const providerPid = metadata?.providerPid ?? 0;
|
|
179
|
+
const providerIdentity = metadata?.providerIdentity ?? null;
|
|
180
|
+
if (exactProcessExists(providerPid, providerIdentity, "win32")) {
|
|
181
|
+
forceWindowsProcessTree(providerPid);
|
|
182
|
+
await waitForExactProcessExit(providerPid, providerIdentity, graceMs, "win32");
|
|
183
|
+
}
|
|
184
|
+
if (exactProcessExists(providerPid, providerIdentity, "win32")) {
|
|
185
|
+
throw new Error("host_process_supervisor_termination_failed: supervised Windows provider tree did not stop.");
|
|
186
|
+
}
|
|
146
187
|
return;
|
|
147
188
|
}
|
|
148
189
|
const processGroupId = child.pid;
|
|
@@ -193,3 +234,49 @@ function unixProcessGroupExists(processGroupId) {
|
|
|
193
234
|
throw error;
|
|
194
235
|
}
|
|
195
236
|
}
|
|
237
|
+
function readSupervisorMetadata(metadataPath) {
|
|
238
|
+
try {
|
|
239
|
+
return JSON.parse(fs.readFileSync(metadataPath, "utf8"));
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function exactProcessExists(pid, identity, platform) {
|
|
246
|
+
if (!identity || !processExists(pid))
|
|
247
|
+
return false;
|
|
248
|
+
return captureHostProcessIdentity(pid, platform) === identity;
|
|
249
|
+
}
|
|
250
|
+
function processExists(pid) {
|
|
251
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
252
|
+
return false;
|
|
253
|
+
try {
|
|
254
|
+
process.kill(pid, 0);
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
return error.code === "EPERM";
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function waitForExactProcessExit(pid, identity, timeoutMs, platform) {
|
|
262
|
+
const deadline = Date.now() + timeoutMs;
|
|
263
|
+
while (Date.now() < deadline && exactProcessExists(pid, identity, platform)) {
|
|
264
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function forceWindowsProcessTree(pid) {
|
|
268
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
269
|
+
return;
|
|
270
|
+
const systemRoot = process.env.SystemRoot?.trim() || "C:\\Windows";
|
|
271
|
+
const taskkill = path.win32.join(systemRoot, "System32", "taskkill.exe");
|
|
272
|
+
try {
|
|
273
|
+
execFileSync(taskkill, ["/PID", String(pid), "/T", "/F"], {
|
|
274
|
+
windowsHide: true,
|
|
275
|
+
timeout: 5_000,
|
|
276
|
+
stdio: "ignore",
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
// A racing process exit is verified by the caller before it returns.
|
|
281
|
+
}
|
|
282
|
+
}
|
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.bca7d0ccee53";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const forgeBridgeVersion = "0.1.3-dev.
|
|
1
|
+
export const forgeBridgeVersion = "0.1.3-dev.bca7d0ccee53";
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { DispatchResolvedArtifact } from "../session/session.types.js";
|
|
2
|
-
export declare function injectWorktreeArtifacts(worktreePath: string, artifacts: readonly DispatchResolvedArtifact[]): Promise<void>;
|
|
2
|
+
export declare function injectWorktreeArtifacts(worktreePath: string, artifacts: readonly DispatchResolvedArtifact[], protectedRelativePaths?: ReadonlySet<string>): Promise<void>;
|
|
3
3
|
export declare function filterArtifactsForLane(artifacts: readonly DispatchResolvedArtifact[], lane: string, phase: string): readonly DispatchResolvedArtifact[];
|
|
4
|
-
export declare function writeArtifact(worktreePath: string, artifact: DispatchResolvedArtifact): Promise<void>;
|
|
4
|
+
export declare function writeArtifact(worktreePath: string, artifact: DispatchResolvedArtifact, protectedRelativePaths?: ReadonlySet<string>): Promise<void>;
|
|
5
5
|
export declare function selectLaneCommand(artifacts: readonly DispatchResolvedArtifact[], lane: string, phase: string): DispatchResolvedArtifact | undefined;
|
|
6
6
|
export declare function resolveCommandSlug(lane: string, phase: string): string;
|
|
7
|
+
export declare function artifactExecutionBody(artifact: DispatchResolvedArtifact): string;
|
|
7
8
|
export declare function buildSpecBody(laneCommand: DispatchResolvedArtifact, workItem: {
|
|
8
9
|
readonly id: string;
|
|
9
10
|
readonly title: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
const DEBUG_OPERATIONAL_SKILLS = new Set(["debug-repro", "debug-root-cause", "debug-fix", "debug-regression"]);
|
|
@@ -59,9 +60,9 @@ const DRAFT_WORK_ITEM_OPERATIONAL_HOOKS = new Set([
|
|
|
59
60
|
const DRAFT_WORK_ORDER_OPERATIONAL_HOOKS = new Set([
|
|
60
61
|
"draft-work-order-plan"
|
|
61
62
|
]);
|
|
62
|
-
export async function injectWorktreeArtifacts(worktreePath, artifacts) {
|
|
63
|
+
export async function injectWorktreeArtifacts(worktreePath, artifacts, protectedRelativePaths = new Set()) {
|
|
63
64
|
for (const artifact of artifacts) {
|
|
64
|
-
await writeArtifact(worktreePath, artifact);
|
|
65
|
+
await writeArtifact(worktreePath, artifact, protectedRelativePaths);
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
export function filterArtifactsForLane(artifacts, lane, phase) {
|
|
@@ -78,7 +79,7 @@ export function filterArtifactsForLane(artifacts, lane, phase) {
|
|
|
78
79
|
}
|
|
79
80
|
return artifacts;
|
|
80
81
|
}
|
|
81
|
-
export async function writeArtifact(worktreePath, artifact) {
|
|
82
|
+
export async function writeArtifact(worktreePath, artifact, protectedRelativePaths = new Set()) {
|
|
82
83
|
const writtenHash = sha256(artifact.body);
|
|
83
84
|
if (writtenHash !== artifact.contentHash.toLowerCase()) {
|
|
84
85
|
throw new Error(`Injected artifact '${artifact.slug}' content_hash mismatch (expected ${artifact.contentHash}, got ${writtenHash}).`);
|
|
@@ -93,6 +94,11 @@ export async function writeArtifact(worktreePath, artifact) {
|
|
|
93
94
|
? path.resolve(scopeDir, slug, "SKILL.md")
|
|
94
95
|
: path.resolve(scopeDir, `${slug}.md`);
|
|
95
96
|
assertInside(forgeRoot, filePath, "Artifact file");
|
|
97
|
+
const relativePath = path.relative(worktreePath, filePath).replaceAll(path.sep, "/");
|
|
98
|
+
if (protectedRelativePaths.has(relativePath)) {
|
|
99
|
+
await restoreTrackedArtifactIfItMatchesManagedBody(worktreePath, filePath, relativePath, artifact);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
96
102
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
97
103
|
await fs.writeFile(filePath, artifact.body, "utf8");
|
|
98
104
|
}
|
|
@@ -120,6 +126,39 @@ export function resolveCommandSlug(lane, phase) {
|
|
|
120
126
|
return "dev-prompt";
|
|
121
127
|
}
|
|
122
128
|
}
|
|
129
|
+
async function restoreTrackedArtifactIfItMatchesManagedBody(worktreePath, filePath, relativePath, artifact) {
|
|
130
|
+
const current = await fs.readFile(filePath, "utf8").catch(() => null);
|
|
131
|
+
if (current === null
|
|
132
|
+
|| (current !== artifact.body && current !== artifactExecutionBody(artifact)))
|
|
133
|
+
return;
|
|
134
|
+
const result = await new Promise((resolve) => {
|
|
135
|
+
const child = spawn("git", ["show", `HEAD:${relativePath}`], {
|
|
136
|
+
cwd: worktreePath,
|
|
137
|
+
windowsHide: true,
|
|
138
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
139
|
+
});
|
|
140
|
+
let stdout = "";
|
|
141
|
+
child.stdout?.setEncoding("utf8");
|
|
142
|
+
child.stdout?.on("data", chunk => { stdout += chunk; });
|
|
143
|
+
child.on("error", () => resolve({ exitCode: 1, stdout: "" }));
|
|
144
|
+
child.on("close", code => resolve({ exitCode: code ?? 1, stdout }));
|
|
145
|
+
});
|
|
146
|
+
if (result.exitCode === 0)
|
|
147
|
+
await fs.writeFile(filePath, result.stdout, "utf8");
|
|
148
|
+
}
|
|
149
|
+
export function artifactExecutionBody(artifact) {
|
|
150
|
+
const content = artifact.body;
|
|
151
|
+
if (!content.startsWith("---"))
|
|
152
|
+
return content;
|
|
153
|
+
const end = content.indexOf("\n---", 3);
|
|
154
|
+
if (end < 3)
|
|
155
|
+
return content;
|
|
156
|
+
let bodyStart = end + 4;
|
|
157
|
+
while (bodyStart < content.length && (content[bodyStart] === "\n" || content[bodyStart] === "\r")) {
|
|
158
|
+
bodyStart += 1;
|
|
159
|
+
}
|
|
160
|
+
return content.slice(bodyStart);
|
|
161
|
+
}
|
|
123
162
|
function shouldInjectDebugArtifact(artifact, commandSlug) {
|
|
124
163
|
if (artifact.scope.trim().toLowerCase() !== "operational") {
|
|
125
164
|
return false;
|
|
@@ -192,7 +231,7 @@ function normalizeDraftPhase(phase) {
|
|
|
192
231
|
return phase.trim().toLowerCase().replaceAll("-", "_");
|
|
193
232
|
}
|
|
194
233
|
export function buildSpecBody(laneCommand, workItem, phase = "implementation") {
|
|
195
|
-
const prompt = laneCommand.
|
|
234
|
+
const prompt = artifactExecutionBody(laneCommand).replaceAll("{workItemId}", workItem.id);
|
|
196
235
|
return `---
|
|
197
236
|
work_item_id: ${workItem.id}
|
|
198
237
|
status: approved
|
|
@@ -22,7 +22,10 @@ export interface WarmDependenciesResult {
|
|
|
22
22
|
readonly fallbacks: readonly InstallFallbackResult[];
|
|
23
23
|
}
|
|
24
24
|
export interface CommandExecutor {
|
|
25
|
-
run(cwd: string, command: string, args: readonly string[]): Promise<void>;
|
|
25
|
+
run(cwd: string, command: string, args: readonly string[], options?: CommandExecutionOptions): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export interface CommandExecutionOptions {
|
|
28
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
26
29
|
}
|
|
27
30
|
export declare function detectWarmPaths(repoRoot: string, warmPathOverrides?: readonly string[]): Promise<WarmPathEntry[]>;
|
|
28
31
|
export declare function warmWorktreeDependencies(request: WarmDependenciesRequest, executor?: CommandExecutor): Promise<WarmDependenciesResult>;
|
|
@@ -41,3 +44,9 @@ export declare function resolveNpmLaunch(args: readonly string[], options?: NpmL
|
|
|
41
44
|
command: string;
|
|
42
45
|
args: readonly string[];
|
|
43
46
|
};
|
|
47
|
+
export declare function resolvePnpmLaunch(args: readonly string[]): {
|
|
48
|
+
command: string;
|
|
49
|
+
args: readonly string[];
|
|
50
|
+
};
|
|
51
|
+
export declare function formatCommandFailureOutput(stdout: string, stderr: string): string;
|
|
52
|
+
export declare function unattendedPackageManagerEnvironment(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
@@ -23,6 +23,7 @@ const NATIVE_MODULE_HINTS = [
|
|
|
23
23
|
"better-sqlite3",
|
|
24
24
|
"@swc/core"
|
|
25
25
|
];
|
|
26
|
+
const MAX_COMMAND_FAILURE_OUTPUT_LENGTH = 8_000;
|
|
26
27
|
export async function detectWarmPaths(repoRoot, warmPathOverrides = []) {
|
|
27
28
|
const entries = new Map();
|
|
28
29
|
await collectDetectedPaths(repoRoot, repoRoot, entries, 0);
|
|
@@ -50,10 +51,39 @@ export async function warmWorktreeDependencies(request, executor = defaultComman
|
|
|
50
51
|
const linked = [];
|
|
51
52
|
const skipped = [];
|
|
52
53
|
const fallbacks = [];
|
|
54
|
+
const restoredDotNetRoots = new Set();
|
|
55
|
+
const installedNodeRoots = new Set();
|
|
53
56
|
for (const entry of warmPaths) {
|
|
54
57
|
const sourcePath = path.join(baseCheckoutPath, entry.relativePath);
|
|
55
58
|
const targetPath = path.join(worktreePath, entry.relativePath);
|
|
56
59
|
assertWithinRoot(allowedRoot, targetPath, "Warm target");
|
|
60
|
+
if (entry.ecosystem === "node" && path.basename(entry.relativePath) === "node_modules") {
|
|
61
|
+
await removeLegacyDirectoryLink(targetPath, allowedRoot);
|
|
62
|
+
const installPackageRoot = await resolveNodeInstallPackageRoot(worktreePath, entry.packageRoot);
|
|
63
|
+
const packageRoot = path.resolve(worktreePath, installPackageRoot === "." ? "" : installPackageRoot);
|
|
64
|
+
const packageKey = process.platform === "win32" ? packageRoot.toLowerCase() : packageRoot;
|
|
65
|
+
if (installedNodeRoots.has(packageKey)) {
|
|
66
|
+
skipped.push(entry.relativePath);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
installedNodeRoots.add(packageKey);
|
|
70
|
+
const install = await runInstallFallback({ ...entry, packageRoot: installPackageRoot }, worktreePath, "Managed worktrees require isolated Node dependencies.", executor);
|
|
71
|
+
fallbacks.push(install);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (entry.ecosystem === "dotnet") {
|
|
75
|
+
await removeLegacyDirectoryLink(targetPath, allowedRoot);
|
|
76
|
+
const packageRoot = path.resolve(worktreePath, entry.packageRoot === "." ? "" : entry.packageRoot);
|
|
77
|
+
const packageKey = process.platform === "win32" ? packageRoot.toLowerCase() : packageRoot;
|
|
78
|
+
if (restoredDotNetRoots.has(packageKey)) {
|
|
79
|
+
skipped.push(entry.relativePath);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
restoredDotNetRoots.add(packageKey);
|
|
83
|
+
const install = await runInstallFallback(entry, worktreePath, "Managed worktrees require isolated .NET build outputs.", executor);
|
|
84
|
+
fallbacks.push(install);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
57
87
|
const sourceExists = await directoryExists(sourcePath);
|
|
58
88
|
const fallbackReason = await resolveFallbackReason(entry, baseCheckoutPath, worktreePath, sourceExists);
|
|
59
89
|
if (fallbackReason) {
|
|
@@ -70,6 +100,16 @@ export async function warmWorktreeDependencies(request, executor = defaultComman
|
|
|
70
100
|
}
|
|
71
101
|
return { linked, skipped, fallbacks };
|
|
72
102
|
}
|
|
103
|
+
async function removeLegacyDirectoryLink(targetPath, allowedRoot) {
|
|
104
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
105
|
+
const resolvedRoot = path.resolve(allowedRoot);
|
|
106
|
+
assertWithinRoot(resolvedRoot, resolvedTarget, "Legacy warm target");
|
|
107
|
+
const stat = await fs.lstat(resolvedTarget).catch(() => null);
|
|
108
|
+
if (!stat?.isSymbolicLink()) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
await fs.rm(resolvedTarget, { force: true });
|
|
112
|
+
}
|
|
73
113
|
export async function createDirectoryLink(sourcePath, targetPath, allowedRoot) {
|
|
74
114
|
const resolvedSource = path.resolve(sourcePath);
|
|
75
115
|
const resolvedTarget = path.resolve(targetPath);
|
|
@@ -81,7 +121,10 @@ export async function createDirectoryLink(sourcePath, targetPath, allowedRoot) {
|
|
|
81
121
|
if (!stat?.isDirectory()) {
|
|
82
122
|
throw new Error(`Link source must be an existing directory: ${resolvedSource}`);
|
|
83
123
|
}
|
|
84
|
-
await fs.
|
|
124
|
+
const existingTarget = await fs.lstat(resolvedTarget).catch(() => null);
|
|
125
|
+
await fs.rm(resolvedTarget, existingTarget?.isSymbolicLink()
|
|
126
|
+
? { force: true }
|
|
127
|
+
: { recursive: true, force: true });
|
|
85
128
|
await fs.mkdir(path.dirname(resolvedTarget), { recursive: true });
|
|
86
129
|
if (process.platform === "win32") {
|
|
87
130
|
await fs.symlink(resolvedSource, resolvedTarget, "junction");
|
|
@@ -185,10 +228,17 @@ export async function lockfilesDiffer(baseCheckoutPath, worktreePath, packageRoo
|
|
|
185
228
|
async function runInstallFallback(entry, worktreePath, reason, executor) {
|
|
186
229
|
const cwd = path.join(worktreePath, entry.packageRoot === "." ? "" : entry.packageRoot);
|
|
187
230
|
if (entry.ecosystem === "node") {
|
|
231
|
+
const pnpmLockPath = path.join(cwd, "pnpm-lock.yaml");
|
|
232
|
+
if (await pathExists(pnpmLockPath)) {
|
|
233
|
+
const args = ["install", "--frozen-lockfile"];
|
|
234
|
+
const launch = resolvePnpmLaunch(args);
|
|
235
|
+
await executor.run(cwd, launch.command, launch.args, { env: unattendedPackageManagerEnvironment() });
|
|
236
|
+
return { ecosystem: entry.ecosystem, packageRoot: entry.packageRoot, reason, command: `pnpm ${args.join(" ")}` };
|
|
237
|
+
}
|
|
188
238
|
const lockPath = path.join(cwd, "package-lock.json");
|
|
189
239
|
const args = (await pathExists(lockPath)) ? ["ci"] : ["install"];
|
|
190
240
|
const launch = resolveNpmLaunch(args);
|
|
191
|
-
await executor.run(cwd, launch.command, launch.args);
|
|
241
|
+
await executor.run(cwd, launch.command, launch.args, { env: unattendedPackageManagerEnvironment() });
|
|
192
242
|
return { ecosystem: entry.ecosystem, packageRoot: entry.packageRoot, reason, command: `npm ${args.join(" ")}` };
|
|
193
243
|
}
|
|
194
244
|
if (entry.ecosystem === "python") {
|
|
@@ -219,6 +269,23 @@ async function runInstallFallback(entry, worktreePath, reason, executor) {
|
|
|
219
269
|
command: "dotnet restore"
|
|
220
270
|
};
|
|
221
271
|
}
|
|
272
|
+
async function resolveNodeInstallPackageRoot(worktreePath, packageRoot) {
|
|
273
|
+
const resolvedWorktree = path.resolve(worktreePath);
|
|
274
|
+
let current = path.resolve(resolvedWorktree, packageRoot === "." ? "" : packageRoot);
|
|
275
|
+
assertWithinRoot(resolvedWorktree, current, "Node package root");
|
|
276
|
+
while (isSubPath(resolvedWorktree, current)) {
|
|
277
|
+
if (await pathExists(path.join(current, "pnpm-lock.yaml"))
|
|
278
|
+
|| await pathExists(path.join(current, "pnpm-workspace.yaml"))
|
|
279
|
+
|| await pathExists(path.join(current, "package-lock.json"))) {
|
|
280
|
+
const relative = normalizeRelativePath(path.relative(resolvedWorktree, current));
|
|
281
|
+
return relative || ".";
|
|
282
|
+
}
|
|
283
|
+
if (current === resolvedWorktree)
|
|
284
|
+
break;
|
|
285
|
+
current = path.dirname(current);
|
|
286
|
+
}
|
|
287
|
+
return packageRoot;
|
|
288
|
+
}
|
|
222
289
|
export async function requiresInstallFallback(entry, baseCheckoutPath, worktreePath, sourceExists) {
|
|
223
290
|
return resolveFallbackReason(entry, baseCheckoutPath, worktreePath, sourceExists);
|
|
224
291
|
}
|
|
@@ -310,6 +377,26 @@ export function resolveNpmLaunch(args, options = {}) {
|
|
|
310
377
|
}
|
|
311
378
|
return { command: "npm", args };
|
|
312
379
|
}
|
|
380
|
+
export function resolvePnpmLaunch(args) {
|
|
381
|
+
if (process.platform === "win32") {
|
|
382
|
+
return buildWindowsCmdLaunch("pnpm.cmd", args, process.env.ComSpec);
|
|
383
|
+
}
|
|
384
|
+
return { command: "pnpm", args };
|
|
385
|
+
}
|
|
386
|
+
export function formatCommandFailureOutput(stdout, stderr) {
|
|
387
|
+
const sections = [
|
|
388
|
+
stderr.trim().length > 0 ? `stderr:\n${stderr.trim()}` : null,
|
|
389
|
+
stdout.trim().length > 0 ? `stdout:\n${stdout.trim()}` : null,
|
|
390
|
+
].filter((section) => section !== null);
|
|
391
|
+
const output = sections.join("\n");
|
|
392
|
+
if (output.length <= MAX_COMMAND_FAILURE_OUTPUT_LENGTH) {
|
|
393
|
+
return output;
|
|
394
|
+
}
|
|
395
|
+
return `[last ${MAX_COMMAND_FAILURE_OUTPUT_LENGTH} characters]\n${output.slice(-MAX_COMMAND_FAILURE_OUTPUT_LENGTH)}`;
|
|
396
|
+
}
|
|
397
|
+
export function unattendedPackageManagerEnvironment(env = process.env) {
|
|
398
|
+
return { ...env, CI: "true" };
|
|
399
|
+
}
|
|
313
400
|
function tryResolveNpmCandidate(candidate) {
|
|
314
401
|
try {
|
|
315
402
|
if (!existsSync(candidate)) {
|
|
@@ -331,10 +418,12 @@ function defaultNpmCliCandidates(execPath, homeDir) {
|
|
|
331
418
|
return candidates;
|
|
332
419
|
}
|
|
333
420
|
const defaultCommandExecutor = {
|
|
334
|
-
async run(cwd, command, args) {
|
|
335
|
-
const result = await defaultProcessRunner.run({ command, cwd, args });
|
|
421
|
+
async run(cwd, command, args, options) {
|
|
422
|
+
const result = await defaultProcessRunner.run({ command, cwd, args, env: options?.env });
|
|
336
423
|
if (result.exitCode !== 0) {
|
|
337
|
-
|
|
424
|
+
const output = formatCommandFailureOutput(result.stdout, result.stderr);
|
|
425
|
+
const details = output.length > 0 ? `\n${output}` : " no output was captured";
|
|
426
|
+
throw new Error(`${command} ${args.join(" ")} failed (${result.exitCode}) in ${cwd}:${details}`);
|
|
338
427
|
}
|
|
339
428
|
}
|
|
340
429
|
};
|
|
@@ -6,6 +6,7 @@ export declare class GitWorktreeService implements WorktreeService {
|
|
|
6
6
|
private readonly coordinator;
|
|
7
7
|
constructor(git?: GitClient, coordinator?: WorktreeLifecycleCoordinator);
|
|
8
8
|
prepare(request: WorktreePrepareRequest): Promise<WorktreeHandle>;
|
|
9
|
+
private trackedForgePaths;
|
|
9
10
|
private repoCreationCoordinationPath;
|
|
10
11
|
release(handle: WorktreeHandle): Promise<void>;
|
|
11
12
|
private resolveRepoPath;
|
|
@@ -59,7 +59,7 @@ export class GitWorktreeService {
|
|
|
59
59
|
const qaReworkPacketPath = await materializeQaReworkPacket(resolvedWorktreePath, request);
|
|
60
60
|
const artifacts = filterArtifactsForLane(request.artifacts, request.lane, request.phase);
|
|
61
61
|
if (artifacts.length > 0) {
|
|
62
|
-
await injectWorktreeArtifacts(resolvedWorktreePath, artifacts);
|
|
62
|
+
await injectWorktreeArtifacts(resolvedWorktreePath, artifacts, await this.trackedForgePaths(resolvedWorktreePath));
|
|
63
63
|
}
|
|
64
64
|
const dependencyWarm = await warmWorktreeDependencies({
|
|
65
65
|
baseCheckoutPath: repoPath,
|
|
@@ -78,6 +78,10 @@ export class GitWorktreeService {
|
|
|
78
78
|
hydratedPriorPhaseArtifactCount
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
|
+
async trackedForgePaths(worktreePath) {
|
|
82
|
+
const output = await this.git.run(worktreePath, ["ls-files", "-z", "--", ".forge"]);
|
|
83
|
+
return new Set(output.split("\0").filter(Boolean).map(value => value.replaceAll("\\", "/")));
|
|
84
|
+
}
|
|
81
85
|
repoCreationCoordinationPath(repo, allowedRoot) {
|
|
82
86
|
if (repo.serverLocalPath && repo.serverLocalPath.trim().length > 0) {
|
|
83
87
|
return path.resolve(repo.serverLocalPath);
|
|
@@ -191,6 +195,13 @@ export class GitWorktreeService {
|
|
|
191
195
|
return isSubPath(allowedRoot, resolved) ? resolved : null;
|
|
192
196
|
}
|
|
193
197
|
async resolveBaseStartPoint(repoPath, baseBranch) {
|
|
198
|
+
if (baseBranch === "HEAD") {
|
|
199
|
+
const head = (await this.git.run(repoPath, ["rev-parse", "--verify", "HEAD^{commit}"])).trim();
|
|
200
|
+
if (!/^[0-9a-f]{40,64}$/iu.test(head)) {
|
|
201
|
+
throw new Error("The repository checkout does not have a valid committed HEAD revision.");
|
|
202
|
+
}
|
|
203
|
+
return head;
|
|
204
|
+
}
|
|
194
205
|
const localRef = `refs/heads/${baseBranch}`;
|
|
195
206
|
const remoteRef = `refs/remotes/origin/${baseBranch}`;
|
|
196
207
|
const origin = await this.git.run(repoPath, ["remote", "get-url", "origin"]).catch(() => "");
|
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.bca7d0ccee53",
|
|
4
4
|
"description": "Forge runner bridge CLI for connecting local developer machines to Forge dispatch.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|