@loucompanion/forge-bridge 0.1.3-dev.d46a8e73d03d → 0.1.3-dev.e7b62d8fdd33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +83 -0
  2. package/dist/bridge/bridge.base.d.ts +4 -1
  3. package/dist/bridge/bridge.service.d.ts +55 -3
  4. package/dist/bridge/bridge.service.js +834 -87
  5. package/dist/bridge/bridge.types.d.ts +89 -1
  6. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  7. package/dist/bridge/runner-host-capabilities.js +93 -0
  8. package/dist/cleanup/cleanup.service.d.ts +14 -2
  9. package/dist/cleanup/cleanup.service.js +496 -3
  10. package/dist/cleanup/cleanup.types.d.ts +10 -0
  11. package/dist/cleanup/index.d.ts +1 -1
  12. package/dist/cleanup/index.js +1 -1
  13. package/dist/cleanup/internals.d.ts +2 -2
  14. package/dist/cleanup/internals.js +1 -1
  15. package/dist/cli/cli.help.js +37 -3
  16. package/dist/cli/cli.service.d.ts +33 -2
  17. package/dist/cli/cli.service.js +209 -27
  18. package/dist/cli/local-config.d.ts +5 -0
  19. package/dist/cli/local-config.js +45 -1
  20. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  21. package/dist/runtime/aspnet-runtime-prebuild.js +340 -0
  22. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  23. package/dist/runtime/attempt-node-runtime-launcher.js +105 -0
  24. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  25. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  26. package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
  27. package/dist/runtime/attempt-process-supervisor.js +386 -0
  28. package/dist/runtime/docker-engine-probe.d.ts +83 -0
  29. package/dist/runtime/docker-engine-probe.js +300 -0
  30. package/dist/runtime/generated-output-reset.d.ts +10 -0
  31. package/dist/runtime/generated-output-reset.js +103 -0
  32. package/dist/runtime/host-listener-inspector.d.ts +46 -0
  33. package/dist/runtime/host-listener-inspector.js +349 -0
  34. package/dist/runtime/host-port-broker.d.ts +76 -0
  35. package/dist/runtime/host-port-broker.js +333 -0
  36. package/dist/runtime/host-runtime-contract.d.ts +47 -0
  37. package/dist/runtime/host-runtime-contract.js +700 -0
  38. package/dist/runtime/host-runtime-lease.d.ts +197 -0
  39. package/dist/runtime/host-runtime-lease.js +2203 -0
  40. package/dist/runtime/host-runtime-promotion.d.ts +23 -0
  41. package/dist/runtime/host-runtime-promotion.js +159 -0
  42. package/dist/runtime/memory-capacity.d.ts +68 -0
  43. package/dist/runtime/memory-capacity.js +121 -0
  44. package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
  45. package/dist/runtime/node-host-runtime-adapter.js +284 -0
  46. package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
  47. package/dist/runtime/playwright-host-runtime-adapter.js +231 -0
  48. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  49. package/dist/runtime/runtime-isolation-lease.js +158 -0
  50. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  51. package/dist/runtime/workspace-resource-estimator.js +174 -0
  52. package/dist/sandbox/preflight.d.ts +45 -1
  53. package/dist/sandbox/preflight.js +299 -34
  54. package/dist/sandbox/process.js +125 -40
  55. package/dist/sandbox/repository-lock.js +41 -3
  56. package/dist/sandbox/runtime.d.ts +66 -3
  57. package/dist/sandbox/runtime.js +670 -105
  58. package/dist/sandbox/sandbox.types.d.ts +10 -0
  59. package/dist/sandbox/smoke.d.ts +76 -0
  60. package/dist/sandbox/smoke.js +380 -0
  61. package/dist/sandbox/worker-supervisor.js +1 -1
  62. package/dist/sandbox/worker.js +2 -5
  63. package/dist/sandbox/workspace.service.d.ts +1 -0
  64. package/dist/sandbox/workspace.service.js +28 -5
  65. package/dist/session/managed-attempt-tools.d.ts +21 -0
  66. package/dist/session/managed-attempt-tools.js +912 -0
  67. package/dist/session/managed-build-coordinator.d.ts +5 -0
  68. package/dist/session/managed-build-coordinator.js +105 -0
  69. package/dist/session/managed-build-receipt.d.ts +21 -0
  70. package/dist/session/managed-build-receipt.js +335 -0
  71. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  72. package/dist/session/phase-completion.artifacts.js +233 -24
  73. package/dist/session/phase-completion.d.ts +12 -2
  74. package/dist/session/phase-completion.helpers.d.ts +5 -0
  75. package/dist/session/phase-completion.helpers.js +51 -10
  76. package/dist/session/phase-completion.js +14 -3
  77. package/dist/session/phase-completion.prompts.d.ts +3 -3
  78. package/dist/session/phase-completion.prompts.js +24 -8
  79. package/dist/session/phase-completion.runtime.d.ts +13 -1
  80. package/dist/session/phase-completion.runtime.js +189 -8
  81. package/dist/session/phase-completion.schemas.js +117 -4
  82. package/dist/session/phase-completion.validation.d.ts +4 -1
  83. package/dist/session/phase-completion.validation.js +153 -22
  84. package/dist/session/pre-publication-validation.d.ts +46 -0
  85. package/dist/session/pre-publication-validation.js +208 -0
  86. package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
  87. package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
  88. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +3 -2
  89. package/dist/session/provider-cli/provider-cli.artifacts.js +65 -7
  90. package/dist/session/provider-cli/provider-cli.deterministic.js +49 -1
  91. package/dist/session/provider-cli/provider-cli.process.d.ts +25 -0
  92. package/dist/session/provider-cli/provider-cli.process.js +463 -34
  93. package/dist/session/provider-cli/provider-cli.prompts.d.ts +4 -1
  94. package/dist/session/provider-cli/provider-cli.prompts.js +108 -25
  95. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  96. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  97. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  98. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  99. package/dist/session/provider-cli/provider-cli.runtime.js +6 -3
  100. package/dist/session/provider-cli/provider-cli.service.d.ts +22 -2
  101. package/dist/session/provider-cli/provider-cli.service.js +524 -100
  102. package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
  103. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  104. package/dist/session/provider-cli/provider-output-state.js +91 -16
  105. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  106. package/dist/session/provider-cli/provider-usage.js +28 -5
  107. package/dist/session/session-redaction.d.ts +3 -0
  108. package/dist/session/session-redaction.js +38 -0
  109. package/dist/session/session.base.d.ts +6 -1
  110. package/dist/session/session.service.d.ts +8 -2
  111. package/dist/session/session.service.js +522 -41
  112. package/dist/session/session.types.d.ts +72 -0
  113. package/dist/session/session.types.js +74 -3
  114. package/dist/shared/format/format-bytes.d.ts +1 -0
  115. package/dist/shared/format/format-bytes.js +9 -0
  116. package/dist/shared/path/path.d.ts +1 -0
  117. package/dist/shared/path/path.js +21 -1
  118. package/dist/shared/process/dotnet-toolchain.d.ts +14 -0
  119. package/dist/shared/process/dotnet-toolchain.js +179 -0
  120. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  121. package/dist/shared/process/host-process-recovery.js +78 -0
  122. package/dist/shared/process/host-process-supervisor.d.ts +59 -0
  123. package/dist/shared/process/host-process-supervisor.js +330 -0
  124. package/dist/shared/process/process.d.ts +1 -0
  125. package/dist/shared/process/process.js +1 -0
  126. package/dist/shared/redaction/redaction.js +12 -2
  127. package/dist/version.d.ts +1 -1
  128. package/dist/version.js +1 -1
  129. package/dist/worktree/artifact-injector.d.ts +3 -2
  130. package/dist/worktree/artifact-injector.js +43 -4
  131. package/dist/worktree/dependency-warmer.d.ts +28 -1
  132. package/dist/worktree/dependency-warmer.js +308 -9
  133. package/dist/worktree/index.d.ts +2 -1
  134. package/dist/worktree/index.js +1 -0
  135. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  136. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  137. package/dist/worktree/worktree-paths.d.ts +4 -0
  138. package/dist/worktree/worktree-paths.js +30 -0
  139. package/dist/worktree/worktree.base.d.ts +3 -0
  140. package/dist/worktree/worktree.service.d.ts +13 -2
  141. package/dist/worktree/worktree.service.js +238 -47
  142. package/dist/worktree/worktree.types.d.ts +5 -1
  143. package/package.json +5 -3
  144. package/runtime/windows-job-supervisor.ps1 +444 -0
  145. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -0,0 +1,59 @@
1
+ import type { ChildProcess } from "node:child_process";
2
+ export type HostProcessSupervisorStrategy = "windows_job_object" | "unix_process_group";
3
+ export interface HostProcessSupervisionRequest {
4
+ readonly command: string;
5
+ readonly args: readonly string[];
6
+ readonly cwd: string;
7
+ readonly env?: Readonly<Record<string, string>>;
8
+ }
9
+ export interface HostProcessSupervisedLaunch {
10
+ readonly command: string;
11
+ readonly args: readonly string[];
12
+ readonly detached: boolean;
13
+ readonly supervisorId: string;
14
+ readonly strategy: HostProcessSupervisorStrategy;
15
+ readonly metadataPath: string;
16
+ readonly memoryTelemetryPath?: string;
17
+ readonly payloadPath?: string;
18
+ readonly payloadSha256?: string;
19
+ readonly readyPath?: string;
20
+ readonly startGrantPath?: string;
21
+ readonly commandDigest: string;
22
+ }
23
+ export interface HostProcessProviderReady {
24
+ readonly schemaVersion: 1;
25
+ readonly supervisorId: string;
26
+ readonly rootPid?: number;
27
+ readonly rootIdentity?: string | null;
28
+ readonly providerPid: number;
29
+ readonly providerCreatedAt: string;
30
+ readonly providerIdentity?: string | null;
31
+ readonly startAttempt: number;
32
+ }
33
+ export interface HostProcessSupervisorMetadata {
34
+ readonly schemaVersion: 1;
35
+ readonly supervisorId: string;
36
+ readonly strategy: HostProcessSupervisorStrategy;
37
+ readonly rootPid: number;
38
+ readonly rootCreatedAt: string;
39
+ readonly rootIdentity?: string | null;
40
+ readonly commandDigest: string;
41
+ readonly worktreePath: string;
42
+ readonly runtimeLeaseId: string;
43
+ readonly dispatchAttemptId: string;
44
+ readonly fencingToken: string;
45
+ readonly bridgePid: number;
46
+ readonly providerPid?: number;
47
+ readonly providerCreatedAt?: string;
48
+ readonly providerIdentity?: string | null;
49
+ readonly providerStartAttempt?: number;
50
+ }
51
+ export declare function resolveHostProcessSupervisedLaunch(request: HostProcessSupervisionRequest, resolvedCommand: string, resolvedArgs: readonly string[], platform?: NodeJS.Platform): HostProcessSupervisedLaunch | null;
52
+ export declare function persistHostProcessSupervisorMetadata(launch: HostProcessSupervisedLaunch, request: HostProcessSupervisionRequest, rootPid: number, rootCreatedAt: string, providerReady?: HostProcessProviderReady): HostProcessSupervisorMetadata;
53
+ /**
54
+ * Captures an OS-issued process birth marker. Recovery compares this marker
55
+ * before signalling a persisted PID so PID reuse can never redirect cleanup
56
+ * at an unrelated process.
57
+ */
58
+ export declare function captureHostProcessIdentity(pid: number, platform?: NodeJS.Platform): string | null;
59
+ export declare function terminateHostProcessTree(child: ChildProcess, launch: HostProcessSupervisedLaunch | null, graceMs?: number): Promise<void>;
@@ -0,0 +1,330 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFileSync } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ const windowsJobSupervisorPath = fileURLToPath(new URL("../../../runtime/windows-job-supervisor.ps1", import.meta.url));
7
+ export function resolveHostProcessSupervisedLaunch(request, resolvedCommand, resolvedArgs, platform = process.platform) {
8
+ const env = request.env;
9
+ if (env?.FORGE_RUNTIME_MODE !== "host")
10
+ return null;
11
+ const supervisorId = requiredEnvironment(env, "FORGE_PROCESS_SUPERVISOR_ID");
12
+ const metadataPath = requiredEnvironment(env, "FORGE_PROCESS_SUPERVISOR_METADATA_PATH");
13
+ const logDirectory = requiredEnvironment(env, "FORGE_LOG_DIR");
14
+ assertMetadataPath(logDirectory, metadataPath);
15
+ const commandDigest = createHash("sha256")
16
+ .update(JSON.stringify([resolvedCommand, ...resolvedArgs]))
17
+ .digest("hex");
18
+ if (platform === "win32") {
19
+ if (!fs.existsSync(windowsJobSupervisorPath)) {
20
+ throw new Error(`host_process_supervisor_missing: ${windowsJobSupervisorPath}`);
21
+ }
22
+ const payloadPath = `${metadataPath}.launch.json`;
23
+ const readyPath = `${metadataPath}.provider-ready.json`;
24
+ const startGrantPath = `${metadataPath}.provider-start-granted`;
25
+ const memoryTelemetryPath = `${metadataPath}.memory.json`;
26
+ const serializedPayload = `${JSON.stringify({
27
+ command: resolvedCommand,
28
+ args: resolvedArgs,
29
+ cwd: request.cwd,
30
+ parentPid: process.pid,
31
+ supervisorId,
32
+ readyPath,
33
+ startGrantPath,
34
+ memoryTelemetryPath,
35
+ })}\n`;
36
+ const payloadSha256 = createHash("sha256").update(serializedPayload).digest("hex");
37
+ fs.writeFileSync(payloadPath, serializedPayload, {
38
+ encoding: "utf8",
39
+ flag: "wx",
40
+ mode: 0o600,
41
+ });
42
+ return {
43
+ command: windowsPowerShellPath(),
44
+ args: [
45
+ "-NoLogo",
46
+ "-NoProfile",
47
+ "-NonInteractive",
48
+ "-ExecutionPolicy",
49
+ "Bypass",
50
+ "-File",
51
+ windowsJobSupervisorPath,
52
+ "-Mode",
53
+ "Outer",
54
+ "-PayloadPath",
55
+ payloadPath,
56
+ "-PayloadSha256",
57
+ payloadSha256,
58
+ ],
59
+ detached: false,
60
+ supervisorId,
61
+ strategy: "windows_job_object",
62
+ metadataPath,
63
+ memoryTelemetryPath,
64
+ payloadPath,
65
+ payloadSha256,
66
+ readyPath,
67
+ startGrantPath,
68
+ commandDigest,
69
+ };
70
+ }
71
+ return {
72
+ command: resolvedCommand,
73
+ args: resolvedArgs,
74
+ detached: true,
75
+ supervisorId,
76
+ strategy: "unix_process_group",
77
+ metadataPath,
78
+ commandDigest,
79
+ };
80
+ }
81
+ export function persistHostProcessSupervisorMetadata(launch, request, rootPid, rootCreatedAt, providerReady) {
82
+ if (!Number.isSafeInteger(rootPid) || rootPid <= 0) {
83
+ throw new Error("host_process_supervisor_invalid: supervised root PID was not assigned.");
84
+ }
85
+ const env = request.env ?? {};
86
+ const metadata = {
87
+ schemaVersion: 1,
88
+ supervisorId: launch.supervisorId,
89
+ strategy: launch.strategy,
90
+ rootPid,
91
+ rootCreatedAt,
92
+ rootIdentity: providerReady?.rootPid === rootPid
93
+ ? providerReady.rootIdentity ?? null
94
+ : captureHostProcessIdentity(rootPid),
95
+ commandDigest: launch.commandDigest,
96
+ worktreePath: request.cwd,
97
+ runtimeLeaseId: requiredEnvironment(env, "FORGE_RESOURCE_LEASE_ID"),
98
+ dispatchAttemptId: requiredEnvironment(env, "FORGE_DISPATCH_ATTEMPT_ID"),
99
+ fencingToken: requiredEnvironment(env, "FORGE_FENCING_TOKEN"),
100
+ bridgePid: process.pid,
101
+ providerPid: providerReady?.providerPid,
102
+ providerCreatedAt: providerReady?.providerCreatedAt,
103
+ providerIdentity: providerReady?.providerIdentity
104
+ ?? (providerReady ? captureHostProcessIdentity(providerReady.providerPid) : undefined),
105
+ providerStartAttempt: providerReady?.startAttempt,
106
+ };
107
+ fs.writeFileSync(launch.metadataPath, `${JSON.stringify(metadata)}\n`, {
108
+ encoding: "utf8",
109
+ flag: "wx",
110
+ mode: 0o600,
111
+ });
112
+ return metadata;
113
+ }
114
+ /**
115
+ * Captures an OS-issued process birth marker. Recovery compares this marker
116
+ * before signalling a persisted PID so PID reuse can never redirect cleanup
117
+ * at an unrelated process.
118
+ */
119
+ export function captureHostProcessIdentity(pid, platform = process.platform) {
120
+ if (!Number.isSafeInteger(pid) || pid <= 0)
121
+ return null;
122
+ try {
123
+ if (platform === "linux") {
124
+ const bootId = fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
125
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
126
+ const close = stat.lastIndexOf(")");
127
+ const fields = close >= 0 ? stat.slice(close + 2).trim().split(/\s+/u) : [];
128
+ const startTicks = fields[19];
129
+ return bootId && startTicks && /^\d+$/u.test(startTicks)
130
+ ? `linux:${bootId}:${startTicks}`
131
+ : null;
132
+ }
133
+ if (platform === "win32") {
134
+ const output = execFileSync(windowsPowerShellPath(), [
135
+ "-NoLogo",
136
+ "-NoProfile",
137
+ "-NonInteractive",
138
+ "-Command",
139
+ `(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks`,
140
+ ], {
141
+ encoding: "utf8",
142
+ windowsHide: true,
143
+ timeout: 10_000,
144
+ stdio: ["ignore", "pipe", "ignore"],
145
+ }).trim();
146
+ return /^\d+$/u.test(output) ? `win32:${output}` : null;
147
+ }
148
+ // Other Unix variants do not expose a sufficiently strong birth marker
149
+ // through Node. Fail closed rather than trust a second-resolution ps time.
150
+ return null;
151
+ }
152
+ catch {
153
+ return null;
154
+ }
155
+ }
156
+ export async function terminateHostProcessTree(child, launch, graceMs = 2_000) {
157
+ if (!launch) {
158
+ await terminateUnsupervisedProcessTree(child, graceMs);
159
+ return;
160
+ }
161
+ if (launch.strategy === "windows_job_object") {
162
+ const rootPid = child.pid ?? 0;
163
+ const metadata = readSupervisorMetadata(launch.metadataPath);
164
+ const rootIdentity = metadata?.rootPid === rootPid
165
+ ? metadata.rootIdentity ?? null
166
+ : captureHostProcessIdentity(rootPid, "win32");
167
+ try {
168
+ child.kill("SIGTERM");
169
+ }
170
+ catch {
171
+ // Closing the launcher is best effort when the Job Object already settled.
172
+ }
173
+ await waitForExactProcessExit(rootPid, rootIdentity, graceMs, "win32");
174
+ if (exactProcessExists(rootPid, rootIdentity, "win32")) {
175
+ forceWindowsProcessTree(rootPid);
176
+ await waitForExactProcessExit(rootPid, rootIdentity, graceMs, "win32");
177
+ }
178
+ if (exactProcessExists(rootPid, rootIdentity, "win32")) {
179
+ throw new Error("host_process_supervisor_termination_failed: supervised Windows root did not stop.");
180
+ }
181
+ const providerPid = metadata?.providerPid ?? 0;
182
+ const providerIdentity = metadata?.providerIdentity ?? null;
183
+ if (exactProcessExists(providerPid, providerIdentity, "win32")) {
184
+ forceWindowsProcessTree(providerPid);
185
+ await waitForExactProcessExit(providerPid, providerIdentity, graceMs, "win32");
186
+ }
187
+ if (exactProcessExists(providerPid, providerIdentity, "win32")) {
188
+ throw new Error("host_process_supervisor_termination_failed: supervised Windows provider tree did not stop.");
189
+ }
190
+ return;
191
+ }
192
+ const processGroupId = child.pid;
193
+ if (!processGroupId || !signalUnixProcessGroup(processGroupId, "SIGTERM"))
194
+ return;
195
+ await waitForUnixProcessGroupExit(processGroupId, graceMs);
196
+ if (unixProcessGroupExists(processGroupId)) {
197
+ signalUnixProcessGroup(processGroupId, "SIGKILL");
198
+ await waitForUnixProcessGroupExit(processGroupId, graceMs);
199
+ }
200
+ if (unixProcessGroupExists(processGroupId)) {
201
+ throw new Error("host_process_supervisor_termination_failed: supervised Unix process group did not stop.");
202
+ }
203
+ }
204
+ async function terminateUnsupervisedProcessTree(child, graceMs) {
205
+ const rootPid = child.pid ?? 0;
206
+ if (!Number.isSafeInteger(rootPid) || rootPid <= 0) {
207
+ try {
208
+ child.kill("SIGTERM");
209
+ }
210
+ catch {
211
+ // The process already settled before a PID was assigned.
212
+ }
213
+ return;
214
+ }
215
+ if (process.platform === "win32") {
216
+ // Without a Job Object there is no trustworthy ancestry record after the
217
+ // root has already exited. Never target a potentially reused Windows PID.
218
+ if (child.exitCode !== null || child.signalCode !== null)
219
+ return;
220
+ const rootIdentity = captureHostProcessIdentity(rootPid, "win32");
221
+ forceWindowsProcessTree(rootPid);
222
+ await waitForExactProcessExit(rootPid, rootIdentity, graceMs, "win32");
223
+ if (exactProcessExists(rootPid, rootIdentity, "win32")) {
224
+ throw new Error("host_process_supervisor_termination_failed: unsupervised Windows provider tree did not stop.");
225
+ }
226
+ return;
227
+ }
228
+ if (!signalUnixProcessGroup(rootPid, "SIGTERM"))
229
+ return;
230
+ await waitForUnixProcessGroupExit(rootPid, graceMs);
231
+ if (unixProcessGroupExists(rootPid)) {
232
+ signalUnixProcessGroup(rootPid, "SIGKILL");
233
+ await waitForUnixProcessGroupExit(rootPid, graceMs);
234
+ }
235
+ if (unixProcessGroupExists(rootPid)) {
236
+ throw new Error("host_process_supervisor_termination_failed: unsupervised Unix provider tree did not stop.");
237
+ }
238
+ }
239
+ function requiredEnvironment(env, name) {
240
+ const value = env[name]?.trim();
241
+ if (!value)
242
+ throw new Error(`host_process_supervisor_invalid: ${name} is required.`);
243
+ return value;
244
+ }
245
+ function assertMetadataPath(logDirectory, metadataPath) {
246
+ const root = path.resolve(logDirectory);
247
+ const target = path.resolve(metadataPath);
248
+ if (target !== root && !target.startsWith(`${root}${path.sep}`)) {
249
+ throw new Error("host_process_supervisor_invalid: metadata path must be inside FORGE_LOG_DIR.");
250
+ }
251
+ }
252
+ function windowsPowerShellPath() {
253
+ const systemRoot = process.env.SystemRoot?.trim() || "C:\\Windows";
254
+ const candidate = path.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
255
+ return fs.existsSync(candidate) ? candidate : "powershell.exe";
256
+ }
257
+ function signalUnixProcessGroup(processGroupId, signal) {
258
+ try {
259
+ process.kill(-processGroupId, signal);
260
+ return true;
261
+ }
262
+ catch (error) {
263
+ if (error.code === "ESRCH")
264
+ return false;
265
+ throw error;
266
+ }
267
+ }
268
+ function unixProcessGroupExists(processGroupId) {
269
+ try {
270
+ process.kill(-processGroupId, 0);
271
+ return true;
272
+ }
273
+ catch (error) {
274
+ if (error.code === "ESRCH")
275
+ return false;
276
+ throw error;
277
+ }
278
+ }
279
+ async function waitForUnixProcessGroupExit(processGroupId, timeoutMs) {
280
+ const deadline = Date.now() + timeoutMs;
281
+ while (Date.now() < deadline && unixProcessGroupExists(processGroupId)) {
282
+ await new Promise(resolve => setTimeout(resolve, 25));
283
+ }
284
+ }
285
+ function readSupervisorMetadata(metadataPath) {
286
+ try {
287
+ return JSON.parse(fs.readFileSync(metadataPath, "utf8"));
288
+ }
289
+ catch {
290
+ return null;
291
+ }
292
+ }
293
+ function exactProcessExists(pid, identity, platform) {
294
+ if (!identity || !processExists(pid))
295
+ return false;
296
+ return captureHostProcessIdentity(pid, platform) === identity;
297
+ }
298
+ function processExists(pid) {
299
+ if (!Number.isSafeInteger(pid) || pid <= 0)
300
+ return false;
301
+ try {
302
+ process.kill(pid, 0);
303
+ return true;
304
+ }
305
+ catch (error) {
306
+ return error.code === "EPERM";
307
+ }
308
+ }
309
+ async function waitForExactProcessExit(pid, identity, timeoutMs, platform) {
310
+ const deadline = Date.now() + timeoutMs;
311
+ while (Date.now() < deadline && exactProcessExists(pid, identity, platform)) {
312
+ await new Promise(resolve => setTimeout(resolve, 25));
313
+ }
314
+ }
315
+ function forceWindowsProcessTree(pid) {
316
+ if (!Number.isSafeInteger(pid) || pid <= 0)
317
+ return;
318
+ const systemRoot = process.env.SystemRoot?.trim() || "C:\\Windows";
319
+ const taskkill = path.win32.join(systemRoot, "System32", "taskkill.exe");
320
+ try {
321
+ execFileSync(taskkill, ["/PID", String(pid), "/T", "/F"], {
322
+ windowsHide: true,
323
+ timeout: 5_000,
324
+ stdio: "ignore",
325
+ });
326
+ }
327
+ catch {
328
+ // A racing process exit is verified by the caller before it returns.
329
+ }
330
+ }
@@ -2,6 +2,7 @@ export interface ProcessRunRequest {
2
2
  readonly command: string;
3
3
  readonly args: readonly string[];
4
4
  readonly cwd: string;
5
+ readonly env?: NodeJS.ProcessEnv;
5
6
  }
6
7
  export interface ProcessRunResult {
7
8
  readonly exitCode: number;
@@ -5,6 +5,7 @@ export class NodeProcessRunner {
5
5
  return await new Promise((resolve, reject) => {
6
6
  const child = spawn(request.command, request.args, {
7
7
  cwd: request.cwd,
8
+ env: request.env,
8
9
  stdio: ["ignore", "pipe", "pipe"]
9
10
  });
10
11
  let stdout = "";
@@ -1,6 +1,14 @@
1
1
  const secretKeyPattern = /(api[_-]?key|authorization|bearer|client[_-]?secret|password|secret|token)/i;
2
2
  const attachmentContentKeyPattern = /^(textContent|contentBase64)$/i;
3
3
  const serializedAttachmentContentPattern = /("(?:textContent|contentBase64)"\s*:\s*")(?:(?:\\.)|[^"\\])*(")/gi;
4
+ const authorizationCredentialPattern = /(\bauthorization\s*[:=]\s*(?:bearer|basic)?\s*)([^"',\s]+)/gi;
5
+ const bearerCredentialPattern = /(\bbearer\s+)([^"',\s]+)/gi;
6
+ const secretAssignmentPattern = /(api[_-]?key|client[_-]?secret|password|secret|token)(\s*["']?\s*[:=]\s*["']?\s*)([^"',\s]+)/gi;
7
+ // Keep this boundary in parity with Forge.Api's SecretRedactor. Without it, the
8
+ // `sk-` substring in a structural identifier such as `task-skill-contract`
9
+ // is mistaken for a provider credential and corrupts the artifact after it has
10
+ // already passed schema validation.
11
+ const providerKeyPattern = /(?<![a-zA-Z0-9])sk-[a-zA-Z0-9_-]{12,}/g;
4
12
  export function redactSecrets(value, exactSecrets = []) {
5
13
  return redactValue(value, exactSecrets);
6
14
  }
@@ -12,8 +20,10 @@ export function redactText(value, exactSecrets = []) {
12
20
  }
13
21
  }
14
22
  return redacted
15
- .replace(/(api[_-]?key|authorization|bearer|client[_-]?secret|password|secret|token)(["':=\s]+)([^"',\s]+)/gi, "$1$2[REDACTED]")
16
- .replace(/(sk-[a-zA-Z0-9_-]{12,})/g, "[REDACTED]");
23
+ .replace(authorizationCredentialPattern, "$1[REDACTED]")
24
+ .replace(bearerCredentialPattern, "$1[REDACTED]")
25
+ .replace(secretAssignmentPattern, "$1$2[REDACTED]")
26
+ .replace(providerKeyPattern, "[REDACTED]");
17
27
  }
18
28
  function redactValue(value, exactSecrets) {
19
29
  if (typeof value === "string") {
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const forgeBridgeVersion = "0.1.3-dev.d46a8e73d03d";
1
+ export declare const forgeBridgeVersion = "0.1.3-dev.e7b62d8fdd33";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const forgeBridgeVersion = "0.1.3-dev.d46a8e73d03d";
1
+ export const forgeBridgeVersion = "0.1.3-dev.e7b62d8fdd33";
@@ -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.body.replaceAll("{workItemId}", workItem.id);
234
+ const prompt = artifactExecutionBody(laneCommand).replaceAll("{workItemId}", workItem.id);
196
235
  return `---
197
236
  work_item_id: ${workItem.id}
198
237
  status: approved
@@ -9,6 +9,13 @@ export interface WarmDependenciesRequest {
9
9
  readonly worktreePath: string;
10
10
  readonly allowedRoot: string;
11
11
  readonly warmPathOverrides?: readonly string[];
12
+ readonly preparationPolicy?: DependencyPreparationPolicy;
13
+ }
14
+ export interface DependencyPreparationPolicy {
15
+ readonly isolation?: string;
16
+ readonly trustDomain?: string;
17
+ readonly invalidationEpoch?: string;
18
+ readonly toolchainVersions?: Readonly<Partial<Record<DependencyEcosystem | "npm" | "pnpm", string>>>;
12
19
  }
13
20
  export interface InstallFallbackResult {
14
21
  readonly ecosystem: DependencyEcosystem;
@@ -20,9 +27,23 @@ export interface WarmDependenciesResult {
20
27
  readonly linked: readonly string[];
21
28
  readonly skipped: readonly string[];
22
29
  readonly fallbacks: readonly InstallFallbackResult[];
30
+ readonly receipts: readonly DependencyPreparationReceiptResult[];
31
+ }
32
+ export interface DependencyPreparationReceiptResult {
33
+ readonly ecosystem: DependencyEcosystem;
34
+ readonly packageRoot: string;
35
+ readonly command: string;
36
+ readonly cacheHit: boolean;
37
+ readonly fingerprint: string;
38
+ readonly receiptPath: string;
39
+ /** End-to-end receipt lookup/preparation time, including coordinator wait. */
40
+ readonly durationMs: number;
23
41
  }
24
42
  export interface CommandExecutor {
25
- run(cwd: string, command: string, args: readonly string[]): Promise<void>;
43
+ run(cwd: string, command: string, args: readonly string[], options?: CommandExecutionOptions): Promise<void>;
44
+ }
45
+ export interface CommandExecutionOptions {
46
+ readonly env?: NodeJS.ProcessEnv;
26
47
  }
27
48
  export declare function detectWarmPaths(repoRoot: string, warmPathOverrides?: readonly string[]): Promise<WarmPathEntry[]>;
28
49
  export declare function warmWorktreeDependencies(request: WarmDependenciesRequest, executor?: CommandExecutor): Promise<WarmDependenciesResult>;
@@ -41,3 +62,9 @@ export declare function resolveNpmLaunch(args: readonly string[], options?: NpmL
41
62
  command: string;
42
63
  args: readonly string[];
43
64
  };
65
+ export declare function resolvePnpmLaunch(args: readonly string[]): {
66
+ command: string;
67
+ args: readonly string[];
68
+ };
69
+ export declare function formatCommandFailureOutput(stdout: string, stderr: string): string;
70
+ export declare function unattendedPackageManagerEnvironment(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;