@loucompanion/forge-bridge 0.1.3-dev.f66395c3ac21 → 0.1.3-dev.fbe15023cdfe

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 (150) hide show
  1. package/README.md +78 -0
  2. package/dist/bridge/bridge.base.d.ts +6 -1
  3. package/dist/bridge/bridge.service.d.ts +60 -3
  4. package/dist/bridge/bridge.service.js +970 -24
  5. package/dist/bridge/bridge.types.d.ts +172 -1
  6. package/dist/cleanup/cleanup.service.d.ts +13 -2
  7. package/dist/cleanup/cleanup.service.js +435 -3
  8. package/dist/cleanup/cleanup.types.d.ts +10 -0
  9. package/dist/cleanup/index.d.ts +1 -1
  10. package/dist/cleanup/index.js +1 -1
  11. package/dist/cleanup/internals.d.ts +2 -2
  12. package/dist/cleanup/internals.js +1 -1
  13. package/dist/cli/cli.help.d.ts +2 -0
  14. package/dist/cli/cli.help.js +217 -0
  15. package/dist/cli/cli.service.d.ts +42 -2
  16. package/dist/cli/cli.service.js +356 -205
  17. package/dist/cli/local-config.d.ts +6 -0
  18. package/dist/cli/local-config.js +47 -3
  19. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  20. package/dist/runtime/attempt-node-runtime-launcher.js +101 -0
  21. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  22. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  23. package/dist/runtime/attempt-process-supervisor.d.ts +86 -0
  24. package/dist/runtime/attempt-process-supervisor.js +350 -0
  25. package/dist/runtime/generated-output-reset.d.ts +10 -0
  26. package/dist/runtime/generated-output-reset.js +103 -0
  27. package/dist/runtime/host-listener-inspector.d.ts +44 -0
  28. package/dist/runtime/host-listener-inspector.js +324 -0
  29. package/dist/runtime/host-port-broker.d.ts +76 -0
  30. package/dist/runtime/host-port-broker.js +333 -0
  31. package/dist/runtime/host-runtime-contract.d.ts +47 -0
  32. package/dist/runtime/host-runtime-contract.js +700 -0
  33. package/dist/runtime/host-runtime-lease.d.ts +133 -0
  34. package/dist/runtime/host-runtime-lease.js +1460 -0
  35. package/dist/runtime/host-runtime-promotion.d.ts +23 -0
  36. package/dist/runtime/host-runtime-promotion.js +159 -0
  37. package/dist/runtime/node-host-runtime-adapter.d.ts +51 -0
  38. package/dist/runtime/node-host-runtime-adapter.js +268 -0
  39. package/dist/runtime/playwright-host-runtime-adapter.d.ts +20 -0
  40. package/dist/runtime/playwright-host-runtime-adapter.js +220 -0
  41. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  42. package/dist/runtime/runtime-isolation-lease.js +158 -0
  43. package/dist/sandbox/command-runner.d.ts +4 -0
  44. package/dist/sandbox/command-runner.js +47 -0
  45. package/dist/sandbox/contract.d.ts +5 -0
  46. package/dist/sandbox/contract.js +276 -0
  47. package/dist/sandbox/event-budget.d.ts +11 -0
  48. package/dist/sandbox/event-budget.js +27 -0
  49. package/dist/sandbox/image-policy.d.ts +35 -0
  50. package/dist/sandbox/image-policy.js +247 -0
  51. package/dist/sandbox/index.d.ts +5 -0
  52. package/dist/sandbox/index.js +5 -0
  53. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  54. package/dist/sandbox/lease-watchdog.js +67 -0
  55. package/dist/sandbox/preflight.d.ts +54 -0
  56. package/dist/sandbox/preflight.js +335 -0
  57. package/dist/sandbox/process.d.ts +20 -0
  58. package/dist/sandbox/process.js +209 -0
  59. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  60. package/dist/sandbox/provider-auth/codex.js +212 -0
  61. package/dist/sandbox/repository-lock.d.ts +5 -0
  62. package/dist/sandbox/repository-lock.js +119 -0
  63. package/dist/sandbox/runtime.d.ts +297 -0
  64. package/dist/sandbox/runtime.js +2338 -0
  65. package/dist/sandbox/sandbox.types.d.ts +86 -0
  66. package/dist/sandbox/sandbox.types.js +1 -0
  67. package/dist/sandbox/smoke.d.ts +76 -0
  68. package/dist/sandbox/smoke.js +380 -0
  69. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  70. package/dist/sandbox/worker-supervisor.js +36 -0
  71. package/dist/sandbox/worker.d.ts +1 -0
  72. package/dist/sandbox/worker.js +225 -0
  73. package/dist/sandbox/workspace.service.d.ts +13 -0
  74. package/dist/sandbox/workspace.service.js +72 -0
  75. package/dist/session/managed-attempt-tools.d.ts +12 -0
  76. package/dist/session/managed-attempt-tools.js +561 -0
  77. package/dist/session/phase-completion.artifacts.d.ts +10 -0
  78. package/dist/session/phase-completion.artifacts.js +627 -0
  79. package/dist/session/phase-completion.d.ts +43 -42
  80. package/dist/session/phase-completion.helpers.d.ts +112 -0
  81. package/dist/session/phase-completion.helpers.js +684 -0
  82. package/dist/session/phase-completion.js +114 -3007
  83. package/dist/session/phase-completion.prompts.d.ts +14 -0
  84. package/dist/session/phase-completion.prompts.js +234 -0
  85. package/dist/session/phase-completion.runtime.d.ts +47 -0
  86. package/dist/session/phase-completion.runtime.js +731 -0
  87. package/dist/session/phase-completion.schemas.d.ts +3 -0
  88. package/dist/session/phase-completion.schemas.js +812 -0
  89. package/dist/session/phase-completion.validation.d.ts +37 -0
  90. package/dist/session/phase-completion.validation.js +828 -0
  91. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  92. package/dist/session/provider-cli/ci-repair-readiness.js +90 -0
  93. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +93 -0
  94. package/dist/session/provider-cli/provider-cli.artifacts.js +778 -0
  95. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  96. package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
  97. package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
  98. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +14 -0
  99. package/dist/session/provider-cli/provider-cli.deterministic.js +375 -0
  100. package/dist/session/provider-cli/provider-cli.process.d.ts +48 -0
  101. package/dist/session/provider-cli/provider-cli.process.js +474 -0
  102. package/dist/session/provider-cli/provider-cli.prompts.d.ts +47 -0
  103. package/dist/session/provider-cli/provider-cli.prompts.js +595 -0
  104. package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
  105. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +66 -0
  106. package/dist/session/provider-cli/provider-cli.runtime.d.ts +9 -0
  107. package/dist/session/provider-cli/provider-cli.runtime.js +41 -0
  108. package/dist/session/provider-cli/provider-cli.service.d.ts +22 -53
  109. package/dist/session/provider-cli/provider-cli.service.js +587 -2530
  110. package/dist/session/provider-cli/provider-cli.types.d.ts +6 -0
  111. package/dist/session/provider-cli/provider-output-state.d.ts +22 -0
  112. package/dist/session/provider-cli/provider-output-state.js +473 -0
  113. package/dist/session/session-redaction.d.ts +3 -0
  114. package/dist/session/session-redaction.js +23 -0
  115. package/dist/session/session.base.d.ts +6 -1
  116. package/dist/session/session.service.d.ts +8 -2
  117. package/dist/session/session.service.js +463 -58
  118. package/dist/session/session.types.d.ts +177 -1
  119. package/dist/session/session.types.js +352 -3
  120. package/dist/shared/path/path.js +3 -1
  121. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  122. package/dist/shared/process/host-process-recovery.js +78 -0
  123. package/dist/shared/process/host-process-supervisor.d.ts +57 -0
  124. package/dist/shared/process/host-process-supervisor.js +282 -0
  125. package/dist/shared/process/process.d.ts +1 -0
  126. package/dist/shared/process/process.js +1 -0
  127. package/dist/shared/process/windows-launch.d.ts +8 -0
  128. package/dist/shared/process/windows-launch.js +41 -0
  129. package/dist/shared/provider-capabilities.d.ts +1 -0
  130. package/dist/shared/provider-capabilities.js +3 -0
  131. package/dist/shared/redaction/redaction.js +10 -3
  132. package/dist/version.d.ts +1 -1
  133. package/dist/version.js +1 -1
  134. package/dist/worktree/artifact-injector.d.ts +3 -2
  135. package/dist/worktree/artifact-injector.js +82 -10
  136. package/dist/worktree/dependency-warmer.d.ts +21 -1
  137. package/dist/worktree/dependency-warmer.js +138 -5
  138. package/dist/worktree/index.d.ts +1 -0
  139. package/dist/worktree/index.js +1 -0
  140. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  141. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  142. package/dist/worktree/worktree-paths.d.ts +4 -0
  143. package/dist/worktree/worktree-paths.js +30 -0
  144. package/dist/worktree/worktree.base.d.ts +3 -0
  145. package/dist/worktree/worktree.service.d.ts +9 -3
  146. package/dist/worktree/worktree.service.js +100 -52
  147. package/dist/worktree/worktree.types.d.ts +3 -1
  148. package/package.json +6 -3
  149. package/runtime/windows-job-supervisor.ps1 +356 -0
  150. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -0,0 +1,282 @@
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
+ fs.writeFileSync(payloadPath, `${JSON.stringify({
26
+ command: resolvedCommand,
27
+ args: resolvedArgs,
28
+ cwd: request.cwd,
29
+ parentPid: process.pid,
30
+ supervisorId,
31
+ readyPath,
32
+ startGrantPath,
33
+ })}\n`, {
34
+ encoding: "utf8",
35
+ flag: "wx",
36
+ mode: 0o600,
37
+ });
38
+ return {
39
+ command: windowsPowerShellPath(),
40
+ args: [
41
+ "-NoLogo",
42
+ "-NoProfile",
43
+ "-NonInteractive",
44
+ "-ExecutionPolicy",
45
+ "Bypass",
46
+ "-File",
47
+ windowsJobSupervisorPath,
48
+ "-Mode",
49
+ "Outer",
50
+ "-PayloadPath",
51
+ payloadPath,
52
+ ],
53
+ detached: false,
54
+ supervisorId,
55
+ strategy: "windows_job_object",
56
+ metadataPath,
57
+ payloadPath,
58
+ readyPath,
59
+ startGrantPath,
60
+ commandDigest,
61
+ };
62
+ }
63
+ return {
64
+ command: resolvedCommand,
65
+ args: resolvedArgs,
66
+ detached: true,
67
+ supervisorId,
68
+ strategy: "unix_process_group",
69
+ metadataPath,
70
+ commandDigest,
71
+ };
72
+ }
73
+ export function persistHostProcessSupervisorMetadata(launch, request, rootPid, rootCreatedAt, providerReady) {
74
+ if (!Number.isSafeInteger(rootPid) || rootPid <= 0) {
75
+ throw new Error("host_process_supervisor_invalid: supervised root PID was not assigned.");
76
+ }
77
+ const env = request.env ?? {};
78
+ const metadata = {
79
+ schemaVersion: 1,
80
+ supervisorId: launch.supervisorId,
81
+ strategy: launch.strategy,
82
+ rootPid,
83
+ rootCreatedAt,
84
+ rootIdentity: providerReady?.rootPid === rootPid
85
+ ? providerReady.rootIdentity ?? null
86
+ : captureHostProcessIdentity(rootPid),
87
+ commandDigest: launch.commandDigest,
88
+ worktreePath: request.cwd,
89
+ runtimeLeaseId: requiredEnvironment(env, "FORGE_RESOURCE_LEASE_ID"),
90
+ dispatchAttemptId: requiredEnvironment(env, "FORGE_DISPATCH_ATTEMPT_ID"),
91
+ fencingToken: requiredEnvironment(env, "FORGE_FENCING_TOKEN"),
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,
98
+ };
99
+ fs.writeFileSync(launch.metadataPath, `${JSON.stringify(metadata)}\n`, {
100
+ encoding: "utf8",
101
+ flag: "wx",
102
+ mode: 0o600,
103
+ });
104
+ return metadata;
105
+ }
106
+ /**
107
+ * Captures an OS-issued process birth marker. Recovery compares this marker
108
+ * before signalling a persisted PID so PID reuse can never redirect cleanup
109
+ * at an unrelated process.
110
+ */
111
+ export function captureHostProcessIdentity(pid, platform = process.platform) {
112
+ if (!Number.isSafeInteger(pid) || pid <= 0)
113
+ return null;
114
+ try {
115
+ if (platform === "linux") {
116
+ const bootId = fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
117
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
118
+ const close = stat.lastIndexOf(")");
119
+ const fields = close >= 0 ? stat.slice(close + 2).trim().split(/\s+/u) : [];
120
+ const startTicks = fields[19];
121
+ return bootId && startTicks && /^\d+$/u.test(startTicks)
122
+ ? `linux:${bootId}:${startTicks}`
123
+ : null;
124
+ }
125
+ if (platform === "win32") {
126
+ const output = execFileSync(windowsPowerShellPath(), [
127
+ "-NoLogo",
128
+ "-NoProfile",
129
+ "-NonInteractive",
130
+ "-Command",
131
+ `(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks`,
132
+ ], {
133
+ encoding: "utf8",
134
+ windowsHide: true,
135
+ timeout: 10_000,
136
+ stdio: ["ignore", "pipe", "ignore"],
137
+ }).trim();
138
+ return /^\d+$/u.test(output) ? `win32:${output}` : null;
139
+ }
140
+ // Other Unix variants do not expose a sufficiently strong birth marker
141
+ // through Node. Fail closed rather than trust a second-resolution ps time.
142
+ return null;
143
+ }
144
+ catch {
145
+ return null;
146
+ }
147
+ }
148
+ export async function terminateHostProcessTree(child, launch, graceMs = 2_000) {
149
+ if (!launch) {
150
+ try {
151
+ child.kill("SIGTERM");
152
+ }
153
+ catch {
154
+ // The process already settled.
155
+ }
156
+ return;
157
+ }
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");
164
+ try {
165
+ child.kill("SIGTERM");
166
+ }
167
+ catch {
168
+ // Closing the launcher is best effort when the Job Object already settled.
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
+ }
187
+ return;
188
+ }
189
+ const processGroupId = child.pid;
190
+ if (!processGroupId || !signalUnixProcessGroup(processGroupId, "SIGTERM"))
191
+ return;
192
+ await new Promise(resolve => setTimeout(resolve, graceMs));
193
+ if (unixProcessGroupExists(processGroupId)) {
194
+ signalUnixProcessGroup(processGroupId, "SIGKILL");
195
+ }
196
+ }
197
+ function requiredEnvironment(env, name) {
198
+ const value = env[name]?.trim();
199
+ if (!value)
200
+ throw new Error(`host_process_supervisor_invalid: ${name} is required.`);
201
+ return value;
202
+ }
203
+ function assertMetadataPath(logDirectory, metadataPath) {
204
+ const root = path.resolve(logDirectory);
205
+ const target = path.resolve(metadataPath);
206
+ if (target !== root && !target.startsWith(`${root}${path.sep}`)) {
207
+ throw new Error("host_process_supervisor_invalid: metadata path must be inside FORGE_LOG_DIR.");
208
+ }
209
+ }
210
+ function windowsPowerShellPath() {
211
+ const systemRoot = process.env.SystemRoot?.trim() || "C:\\Windows";
212
+ const candidate = path.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
213
+ return fs.existsSync(candidate) ? candidate : "powershell.exe";
214
+ }
215
+ function signalUnixProcessGroup(processGroupId, signal) {
216
+ try {
217
+ process.kill(-processGroupId, signal);
218
+ return true;
219
+ }
220
+ catch (error) {
221
+ if (error.code === "ESRCH")
222
+ return false;
223
+ throw error;
224
+ }
225
+ }
226
+ function unixProcessGroupExists(processGroupId) {
227
+ try {
228
+ process.kill(-processGroupId, 0);
229
+ return true;
230
+ }
231
+ catch (error) {
232
+ if (error.code === "ESRCH")
233
+ return false;
234
+ throw error;
235
+ }
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
+ }
@@ -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 = "";
@@ -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;
@@ -0,0 +1,3 @@
1
+ export function supportsDraftImageAttachmentsFor(cliProviders) {
2
+ return cliProviders.some((provider) => provider.trim().toLowerCase() === "codex");
3
+ }
@@ -1,16 +1,23 @@
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;
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;
2
7
  export function redactSecrets(value, exactSecrets = []) {
3
8
  return redactValue(value, exactSecrets);
4
9
  }
5
10
  export function redactText(value, exactSecrets = []) {
6
- let redacted = value;
11
+ let redacted = value.replace(serializedAttachmentContentPattern, "$1[REDACTED]$2");
7
12
  for (const secret of exactSecrets) {
8
13
  if (secret.length > 0) {
9
14
  redacted = redacted.split(secret).join("[REDACTED]");
10
15
  }
11
16
  }
12
17
  return redacted
13
- .replace(/(api[_-]?key|authorization|bearer|client[_-]?secret|password|secret|token)(["':=\s]+)([^"',\s]+)/gi, "$1$2[REDACTED]")
18
+ .replace(authorizationCredentialPattern, "$1[REDACTED]")
19
+ .replace(bearerCredentialPattern, "$1[REDACTED]")
20
+ .replace(secretAssignmentPattern, "$1$2[REDACTED]")
14
21
  .replace(/(sk-[a-zA-Z0-9_-]{12,})/g, "[REDACTED]");
15
22
  }
16
23
  function redactValue(value, exactSecrets) {
@@ -25,6 +32,6 @@ function redactValue(value, exactSecrets) {
25
32
  }
26
33
  return Object.fromEntries(Object.entries(value).map(([key, item]) => [
27
34
  key,
28
- secretKeyPattern.test(key) ? "[REDACTED]" : redactValue(item, exactSecrets)
35
+ secretKeyPattern.test(key) || attachmentContentKeyPattern.test(key) ? "[REDACTED]" : redactValue(item, exactSecrets)
29
36
  ]));
30
37
  }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const forgeBridgeVersion = "0.1.3-dev.f66395c3ac21";
1
+ export declare const forgeBridgeVersion = "0.1.3-dev.fbe15023cdfe";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const forgeBridgeVersion = "0.1.3-dev.f66395c3ac21";
1
+ export const forgeBridgeVersion = "0.1.3-dev.fbe15023cdfe";
@@ -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"]);
@@ -36,22 +37,32 @@ const DRAFT_OPERATIONAL_SKILLS = new Set([
36
37
  "draft-repo-context",
37
38
  "draft-graph-context",
38
39
  "draft-product-scope",
39
- "draft-work-order-plan",
40
40
  "draft-critic",
41
41
  "draft-proposal"
42
42
  ]);
43
+ const DRAFT_WORK_ITEM_OPERATIONAL_SKILLS = new Set([
44
+ "draft-work-item-plan"
45
+ ]);
46
+ const DRAFT_WORK_ORDER_OPERATIONAL_SKILLS = new Set([
47
+ "draft-work-order-plan"
48
+ ]);
43
49
  const DRAFT_OPERATIONAL_HOOKS = new Set([
44
50
  "draft-intent",
45
51
  "draft-repo-context",
46
52
  "draft-graph-context",
47
53
  "draft-product-scope",
48
- "draft-work-order-plan",
49
54
  "draft-critic",
50
55
  "draft-proposal"
51
56
  ]);
52
- export async function injectWorktreeArtifacts(worktreePath, artifacts) {
57
+ const DRAFT_WORK_ITEM_OPERATIONAL_HOOKS = new Set([
58
+ "draft-work-item-plan"
59
+ ]);
60
+ const DRAFT_WORK_ORDER_OPERATIONAL_HOOKS = new Set([
61
+ "draft-work-order-plan"
62
+ ]);
63
+ export async function injectWorktreeArtifacts(worktreePath, artifacts, protectedRelativePaths = new Set()) {
53
64
  for (const artifact of artifacts) {
54
- await writeArtifact(worktreePath, artifact);
65
+ await writeArtifact(worktreePath, artifact, protectedRelativePaths);
55
66
  }
56
67
  }
57
68
  export function filterArtifactsForLane(artifacts, lane, phase) {
@@ -64,11 +75,11 @@ export function filterArtifactsForLane(artifacts, lane, phase) {
64
75
  return artifacts.filter((artifact) => shouldInjectArchitectArtifact(artifact, commandSlug));
65
76
  }
66
77
  if (normalizedLane === "draft") {
67
- return artifacts.filter((artifact) => shouldInjectDraftArtifact(artifact, commandSlug));
78
+ return artifacts.filter((artifact) => shouldInjectDraftArtifact(artifact, commandSlug, phase));
68
79
  }
69
80
  return artifacts;
70
81
  }
71
- export async function writeArtifact(worktreePath, artifact) {
82
+ export async function writeArtifact(worktreePath, artifact, protectedRelativePaths = new Set()) {
72
83
  const writtenHash = sha256(artifact.body);
73
84
  if (writtenHash !== artifact.contentHash.toLowerCase()) {
74
85
  throw new Error(`Injected artifact '${artifact.slug}' content_hash mismatch (expected ${artifact.contentHash}, got ${writtenHash}).`);
@@ -83,6 +94,11 @@ export async function writeArtifact(worktreePath, artifact) {
83
94
  ? path.resolve(scopeDir, slug, "SKILL.md")
84
95
  : path.resolve(scopeDir, `${slug}.md`);
85
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
+ }
86
102
  await fs.mkdir(path.dirname(filePath), { recursive: true });
87
103
  await fs.writeFile(filePath, artifact.body, "utf8");
88
104
  }
@@ -110,6 +126,39 @@ export function resolveCommandSlug(lane, phase) {
110
126
  return "dev-prompt";
111
127
  }
112
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
+ }
113
162
  function shouldInjectDebugArtifact(artifact, commandSlug) {
114
163
  if (artifact.scope.trim().toLowerCase() !== "operational") {
115
164
  return false;
@@ -142,7 +191,7 @@ function shouldInjectArchitectArtifact(artifact, commandSlug) {
142
191
  return false;
143
192
  }
144
193
  }
145
- function shouldInjectDraftArtifact(artifact, commandSlug) {
194
+ function shouldInjectDraftArtifact(artifact, commandSlug, phase) {
146
195
  if (artifact.scope.trim().toLowerCase() !== "operational") {
147
196
  return false;
148
197
  }
@@ -151,15 +200,38 @@ function shouldInjectDraftArtifact(artifact, commandSlug) {
151
200
  case "command":
152
201
  return slug === commandSlug;
153
202
  case "skill":
154
- return DRAFT_OPERATIONAL_SKILLS.has(slug);
203
+ return DRAFT_OPERATIONAL_SKILLS.has(slug) || draftPhaseSkills(phase).has(slug);
155
204
  case "hook":
156
- return DRAFT_OPERATIONAL_HOOKS.has(slug);
205
+ return DRAFT_OPERATIONAL_HOOKS.has(slug) || draftPhaseHooks(phase).has(slug);
157
206
  default:
158
207
  return false;
159
208
  }
160
209
  }
210
+ function draftPhaseSkills(phase) {
211
+ switch (normalizeDraftPhase(phase)) {
212
+ case "work_order":
213
+ return DRAFT_WORK_ORDER_OPERATIONAL_SKILLS;
214
+ case "story":
215
+ return DRAFT_WORK_ITEM_OPERATIONAL_SKILLS;
216
+ default:
217
+ return DRAFT_WORK_ITEM_OPERATIONAL_SKILLS;
218
+ }
219
+ }
220
+ function draftPhaseHooks(phase) {
221
+ switch (normalizeDraftPhase(phase)) {
222
+ case "work_order":
223
+ return DRAFT_WORK_ORDER_OPERATIONAL_HOOKS;
224
+ case "story":
225
+ return DRAFT_WORK_ITEM_OPERATIONAL_HOOKS;
226
+ default:
227
+ return DRAFT_WORK_ITEM_OPERATIONAL_HOOKS;
228
+ }
229
+ }
230
+ function normalizeDraftPhase(phase) {
231
+ return phase.trim().toLowerCase().replaceAll("-", "_");
232
+ }
161
233
  export function buildSpecBody(laneCommand, workItem, phase = "implementation") {
162
- const prompt = laneCommand.body.replaceAll("{workItemId}", workItem.id);
234
+ const prompt = artifactExecutionBody(laneCommand).replaceAll("{workItemId}", workItem.id);
163
235
  return `---
164
236
  work_item_id: ${workItem.id}
165
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>;
@@ -30,3 +33,20 @@ export declare function createDirectoryLink(sourcePath: string, targetPath: stri
30
33
  export declare function lockfilesDiffer(baseCheckoutPath: string, worktreePath: string, packageRootRelative: string): Promise<boolean>;
31
34
  export declare function requiresInstallFallback(entry: WarmPathEntry, baseCheckoutPath: string, worktreePath: string, sourceExists: boolean): Promise<string | null>;
32
35
  export declare function declaresNativeModules(packageRootPath: string): Promise<boolean>;
36
+ export interface NpmLaunchOptions {
37
+ readonly env?: NodeJS.ProcessEnv;
38
+ readonly execPath?: string;
39
+ readonly platform?: NodeJS.Platform;
40
+ readonly homeDir?: string;
41
+ readonly candidates?: readonly string[];
42
+ }
43
+ export declare function resolveNpmLaunch(args: readonly string[], options?: NpmLaunchOptions): {
44
+ command: string;
45
+ args: readonly string[];
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;