@codyswann/lisa 4.60.10 → 4.60.12

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 (64) hide show
  1. package/all/copy-overwrite/scripts/lib/process-tree-runner.mjs +15 -13
  2. package/all/copy-overwrite/scripts/lib/windows-process-job.cs +249 -0
  3. package/all/copy-overwrite/scripts/lib/windows-process-job.mjs +119 -0
  4. package/all/copy-overwrite/scripts/lib/windows-process-job.ps1 +25 -0
  5. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  6. package/dist/core/lisa-owned-hash-ledger.js +42 -0
  7. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  8. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  9. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  10. package/dist/core/upstream-evidence-manifest.js +13 -3
  11. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  12. package/expo/copy-overwrite/scripts/check-e2e-coverage.mjs +15 -14
  13. package/package.json +4 -4
  14. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  16. package/plugins/lisa-agy/plugin.json +1 -1
  17. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  20. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  26. package/plugins/lisa-expo-agy/plugin.json +1 -1
  27. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  36. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  37. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  41. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  42. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  47. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  51. package/plugins/lisa-rails-agy/plugin.json +1 -1
  52. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  56. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  57. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  61. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  62. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  64. package/scripts/two-channel-couplings.json +1 -1
@@ -3,14 +3,18 @@
3
3
  // Do not edit directly — durable changes belong upstream in Lisa.
4
4
 
5
5
  /**
6
- * Run one shell command in its own process group and reap the whole group.
6
+ * Run one shell command in a process boundary and reap every descendant.
7
7
  *
8
8
  * Node's synchronous timeout signals only the direct child. A gate command is
9
9
  * a tree (shell, package manager, test workers), so killing only the shell
10
10
  * leaves descendants running against scratch state the caller then removes.
11
11
  * This small asynchronous supervisor is invoked through a synchronous parent:
12
12
  * it owns the process-group id, applies the deadline, terminates the group, and
13
- * only then lets the parent continue.
13
+ * only then lets the parent continue. On Windows, the common PowerShell helper
14
+ * owns a native Job Object because a shell PID cannot identify descendants
15
+ * after the shell exits. Windows 10 / Server 2016 or newer and the installed
16
+ * Windows PowerShell execution policy must permit that helper; startup failure
17
+ * rejects the command without an uncontained fallback.
14
18
  *
15
19
  * ## Interrupting a run
16
20
  *
@@ -31,6 +35,7 @@
31
35
  import { spawn, spawnSync } from "node:child_process";
32
36
 
33
37
  import { invokedAsScript } from "./invoked-as-script.mjs";
38
+ import { startWindowsProcessJob } from "./windows-process-job.mjs";
34
39
 
35
40
  const KILL_GRACE_MS = 750;
36
41
  const REAP_POLL_MS = 25;
@@ -407,7 +412,7 @@ export async function reapTree(pid, controls = DEFAULT_REAP_CONTROLS) {
407
412
  * lands on a recycled pid.
408
413
  * @param {string} command Shell source to supervise.
409
414
  * @param {number} timeoutMs Deadline for the whole tree.
410
- * @param {typeof reapTree} [reap] Injectable group reaper.
415
+ * @param {typeof reapTree} [reap] Injectable POSIX group reaper. Windows retains its native job owner.
411
416
  * @param {object} [options] Interrupt-watch seams, injectable for tests.
412
417
  * @param {readonly number[]} [options.watchPids] Pids whose death ends this run.
413
418
  * @param {typeof interruptionReason} [options.detect] Interrupt predicate.
@@ -425,14 +430,7 @@ export function supervise(command, timeoutMs, reap = reapTree, options = {}) {
425
430
  launchParentPid = process.ppid,
426
431
  } = options;
427
432
  return new Promise((resolve, reject) => {
428
- const shell =
429
- process.platform === "win32"
430
- ? process.env.ComSpec || "cmd.exe"
431
- : "/bin/sh";
432
- const shellArgs =
433
- process.platform === "win32"
434
- ? ["/d", "/s", "/c", command]
435
- : ["-c", command];
433
+ let windowsJob;
436
434
  let pendingSignal;
437
435
  let dispatchSignal = signal => {
438
436
  pendingSignal ??= signal;
@@ -455,7 +453,11 @@ export function supervise(command, timeoutMs, reap = reapTree, options = {}) {
455
453
 
456
454
  const child = (() => {
457
455
  try {
458
- return spawn(shell, shellArgs, {
456
+ if (process.platform === "win32") {
457
+ windowsJob = startWindowsProcessJob(command);
458
+ return windowsJob.child;
459
+ }
460
+ return spawn("/bin/sh", ["-c", command], {
459
461
  detached: true,
460
462
  env: process.env,
461
463
  stdio: "inherit",
@@ -501,7 +503,7 @@ export function supervise(command, timeoutMs, reap = reapTree, options = {}) {
501
503
  // shell's close handler is reaping must join that operation instead of
502
504
  // starting a second kill sequence or taking the default signal action.
503
505
  const reapOnce = () => {
504
- reapPromise ??= reap(pid);
506
+ reapPromise ??= windowsJob ? windowsJob.reap() : reap(pid);
505
507
  return reapPromise;
506
508
  };
507
509
  const finish = async (code, signal) => {
@@ -0,0 +1,249 @@
1
+ // This file is managed by Lisa and IS replaced on each `lisa` run.
2
+ // Do not edit directly — durable changes belong upstream in Lisa.
3
+ // Windows 10 / Server 2016 or newer: create the shell inside its Job Object.
4
+ using System;
5
+ using System.ComponentModel;
6
+ using System.Diagnostics;
7
+ using System.IO;
8
+ using System.Runtime.InteropServices;
9
+ using System.Text;
10
+ using System.Threading;
11
+
12
+ /// <summary>Runs a shell inside a retained job and confirms its descendants have exited.</summary>
13
+ public static class LisaWindowsProcessJob
14
+ {
15
+ [StructLayout(LayoutKind.Sequential)]
16
+ struct BasicLimits
17
+ {
18
+ public long ProcessTime, JobTime;
19
+ public uint Flags;
20
+ public UIntPtr MinimumWorkingSet, MaximumWorkingSet;
21
+ public uint ActiveProcessLimit;
22
+ public UIntPtr Affinity;
23
+ public uint Priority, SchedulingClass;
24
+ }
25
+
26
+ [StructLayout(LayoutKind.Sequential)]
27
+ struct ExtendedLimits
28
+ {
29
+ public BasicLimits Basic;
30
+ public ulong ReadOperations, WriteOperations, OtherOperations;
31
+ public ulong ReadBytes, WriteBytes, OtherBytes;
32
+ public UIntPtr ProcessMemory, JobMemory, PeakProcessMemory, PeakJobMemory;
33
+ }
34
+
35
+ [StructLayout(LayoutKind.Sequential)]
36
+ struct Accounting
37
+ {
38
+ public long UserTime, KernelTime, PeriodUserTime, PeriodKernelTime;
39
+ public uint PageFaults, TotalProcesses, ActiveProcesses, TerminatedProcesses;
40
+ }
41
+
42
+ [StructLayout(LayoutKind.Sequential)]
43
+ struct StartupInfo
44
+ {
45
+ public uint Size;
46
+ public IntPtr Reserved, Desktop, Title;
47
+ public uint X, Y, Width, Height, XCount, YCount, Fill, Flags;
48
+ public ushort ShowWindow, ReservedLength;
49
+ public IntPtr ReservedData, Input, Output, Error;
50
+ }
51
+
52
+ [StructLayout(LayoutKind.Sequential)]
53
+ struct StartupInfoEx
54
+ {
55
+ public StartupInfo Startup;
56
+ public IntPtr Attributes;
57
+ }
58
+
59
+ [StructLayout(LayoutKind.Sequential)]
60
+ struct ProcessInformation
61
+ {
62
+ public IntPtr Process, Thread;
63
+ public uint ProcessId, ThreadId;
64
+ }
65
+
66
+ /// <summary>Creates the helper-owned job handle, which is never inherited by the shell.</summary>
67
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
68
+ static extern IntPtr CreateJobObjectW(IntPtr attributes, string name);
69
+ /// <summary>Applies the job limits used to kill members when its final handle closes.</summary>
70
+ [DllImport("kernel32.dll", SetLastError = true)]
71
+ static extern bool SetInformationJobObject(IntPtr job, int kind, ref ExtendedLimits value, uint size);
72
+ /// <summary>Reads job accounting so cleanup can confirm zero active processes.</summary>
73
+ [DllImport("kernel32.dll", SetLastError = true)]
74
+ static extern bool QueryInformationJobObject(IntPtr job, int kind, out Accounting value, uint size, IntPtr returned);
75
+ /// <summary>Requests termination of every process retained in the job.</summary>
76
+ [DllImport("kernel32.dll", SetLastError = true)]
77
+ static extern bool TerminateJobObject(IntPtr job, uint code);
78
+ /// <summary>Measures or initializes storage for the two process-creation attributes.</summary>
79
+ [DllImport("kernel32.dll", SetLastError = true)]
80
+ static extern bool InitializeProcThreadAttributeList(IntPtr list, int count, uint flags, ref IntPtr size);
81
+ /// <summary>Adds the job binding or restricted inherited-handle list before process creation.</summary>
82
+ [DllImport("kernel32.dll", SetLastError = true)]
83
+ static extern bool UpdateProcThreadAttribute(IntPtr list, uint flags, IntPtr attribute, IntPtr value, IntPtr size, IntPtr previous, IntPtr returned);
84
+ /// <summary>Releases initialized attribute-list contents before their backing memory is freed.</summary>
85
+ [DllImport("kernel32.dll")]
86
+ static extern void DeleteProcThreadAttributeList(IntPtr list);
87
+ /// <summary>Creates the shell with job membership and explicit standard handles already assigned.</summary>
88
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
89
+ static extern bool CreateProcessW(string application, StringBuilder command, IntPtr processAttributes, IntPtr threadAttributes, bool inheritHandles, uint flags, IntPtr environment, string directory, ref StartupInfoEx startup, out ProcessInformation process);
90
+ /// <summary>Polls the shell handle while allowing stop-file and owner-exit checks.</summary>
91
+ [DllImport("kernel32.dll", SetLastError = true)]
92
+ static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
93
+ /// <summary>Reads the shell exit code after its process handle signals completion.</summary>
94
+ [DllImport("kernel32.dll", SetLastError = true)]
95
+ static extern bool GetExitCodeProcess(IntPtr process, out uint code);
96
+ /// <summary>Releases an owned native handle without closing borrowed caller handles.</summary>
97
+ [DllImport("kernel32.dll", SetLastError = true)]
98
+ static extern bool CloseHandle(IntPtr handle);
99
+ /// <summary>Supplies the current-process pseudo handle for standard-handle duplication.</summary>
100
+ [DllImport("kernel32.dll")]
101
+ static extern IntPtr GetCurrentProcess();
102
+ /// <summary>Reads a borrowed standard handle, which may be absent in a service process.</summary>
103
+ [DllImport("kernel32.dll", SetLastError = true)]
104
+ static extern IntPtr GetStdHandle(int kind);
105
+ /// <summary>Opens a temporary NUL device handle when a standard stream is absent.</summary>
106
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
107
+ static extern IntPtr CreateFileW(string name, uint access, uint share, IntPtr attributes, uint creation, uint flags, IntPtr template);
108
+ /// <summary>Creates an inheritable duplicate while retaining ownership of the original handle.</summary>
109
+ [DllImport("kernel32.dll", SetLastError = true)]
110
+ static extern bool DuplicateHandle(IntPtr sourceProcess, IntPtr source, IntPtr targetProcess, out IntPtr target, uint access, bool inherit, uint options);
111
+
112
+ /// <summary>Throws with the captured native error when an operation reports failure.</summary>
113
+ static void Check(bool ok, string operation)
114
+ {
115
+ if (!ok) throw new Win32Exception(Marshal.GetLastWin32Error(), operation);
116
+ }
117
+
118
+ /// <summary>Returns an owned inheritable standard handle, substituting NUL only when absent.</summary>
119
+ /// <param name="kind">The native standard-input, output, or error handle identifier.</param>
120
+ /// <returns>A duplicate that Run must close after the contained command finishes.</returns>
121
+ static IntPtr StandardHandle(int kind)
122
+ {
123
+ IntPtr source = GetStdHandle(kind);
124
+ bool absent = source == IntPtr.Zero || source == new IntPtr(-1);
125
+ if (absent)
126
+ {
127
+ // Services may have no standard handles. Supply EOF/discard devices
128
+ // while preserving any real redirected handles supplied by callers.
129
+ source = CreateFileW("NUL", kind == -10 ? 0x80000000u : 0x40000000u, 3, IntPtr.Zero, 3, 0, IntPtr.Zero);
130
+ Check(source != new IntPtr(-1), "Open absent gate standard handle");
131
+ }
132
+ try
133
+ {
134
+ IntPtr result;
135
+ Check(DuplicateHandle(GetCurrentProcess(), source, GetCurrentProcess(), out result, 0, true, 2), "Duplicate gate standard handle");
136
+ return result;
137
+ }
138
+ finally
139
+ {
140
+ if (absent) CloseHandle(source);
141
+ }
142
+ }
143
+
144
+ /// <summary>Terminates job members and waits up to five seconds for empty membership.</summary>
145
+ /// <param name="job">The retained job handle; this method does not close it.</param>
146
+ static void TerminateAndWait(IntPtr job)
147
+ {
148
+ Check(TerminateJobObject(job, 255), "Terminate gate job");
149
+ Stopwatch deadline = Stopwatch.StartNew();
150
+ for (;;)
151
+ {
152
+ Accounting state;
153
+ Check(QueryInformationJobObject(job, 1, out state, (uint)Marshal.SizeOf(typeof(Accounting)), IntPtr.Zero), "Read gate job membership");
154
+ if (state.ActiveProcesses == 0) return;
155
+ if (deadline.ElapsedMilliseconds >= 5000) throw new TimeoutException("Gate job still has processes after termination");
156
+ Thread.Sleep(25);
157
+ }
158
+ }
159
+
160
+ /// <summary>Runs a contained command and reaps its descendants before returning.</summary>
161
+ /// <remarks>
162
+ /// The helper owns the job outside its membership. A helper crash closes
163
+ /// its non-inherited job handle and kills the job as a backstop.
164
+ /// </remarks>
165
+ /// <param name="command">The shell command, forwarded without rewriting its arguments.</param>
166
+ /// <param name="controlDirectory">The caller-owned directory containing the stop marker.</param>
167
+ /// <param name="parent">The retained owner process whose exit requests cancellation.</param>
168
+ /// <returns>The shell exit code, or 255 when interrupted before normal completion.</returns>
169
+ public static int Run(string command, string controlDirectory, Process parent)
170
+ {
171
+ IntPtr job = IntPtr.Zero, attributes = IntPtr.Zero;
172
+ IntPtr jobValue = IntPtr.Zero, handleValues = IntPtr.Zero;
173
+ bool attributesInitialized = false;
174
+ IntPtr[] standard = new IntPtr[3];
175
+ ProcessInformation child = new ProcessInformation();
176
+ uint result = 255;
177
+ try
178
+ {
179
+ job = CreateJobObjectW(IntPtr.Zero, null);
180
+ Check(job != IntPtr.Zero, "Create gate job");
181
+ ExtendedLimits limits = new ExtendedLimits();
182
+ limits.Basic.Flags = 0x2000; // JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE.
183
+ Check(SetInformationJobObject(job, 9, ref limits, (uint)Marshal.SizeOf(typeof(ExtendedLimits))), "Set gate job lifetime");
184
+
185
+ IntPtr size = IntPtr.Zero;
186
+ InitializeProcThreadAttributeList(IntPtr.Zero, 2, 0, ref size);
187
+ Check(size != IntPtr.Zero, "Measure gate process attributes");
188
+ attributes = Marshal.AllocHGlobal(size);
189
+ Check(InitializeProcThreadAttributeList(attributes, 2, 0, ref size), "Initialize gate process attributes");
190
+ attributesInitialized = true;
191
+ jobValue = Marshal.AllocHGlobal(IntPtr.Size);
192
+ Marshal.WriteIntPtr(jobValue, job);
193
+ // PROC_THREAD_ATTRIBUTE_JOB_LIST binds before the first instruction;
194
+ // suspended-create followed by assignment has an orphaning window.
195
+ Check(UpdateProcThreadAttribute(attributes, 0, new IntPtr(0x2000D), jobValue, new IntPtr(IntPtr.Size), IntPtr.Zero, IntPtr.Zero), "Bind gate to job at creation");
196
+
197
+ handleValues = Marshal.AllocHGlobal(3 * IntPtr.Size);
198
+ for (int i = 0; i < 3; ++i)
199
+ {
200
+ standard[i] = StandardHandle(-10 - i);
201
+ Marshal.WriteIntPtr(handleValues, i * IntPtr.Size, standard[i]);
202
+ }
203
+ Check(UpdateProcThreadAttribute(attributes, 0, new IntPtr(0x20002), handleValues, new IntPtr(3 * IntPtr.Size), IntPtr.Zero, IntPtr.Zero), "Limit inherited gate handles");
204
+ StartupInfoEx startup = new StartupInfoEx();
205
+ startup.Startup.Size = (uint)Marshal.SizeOf(typeof(StartupInfoEx));
206
+ startup.Startup.Flags = 0x100; // STARTF_USESTDHANDLES.
207
+ startup.Startup.Input = standard[0];
208
+ startup.Startup.Output = standard[1];
209
+ startup.Startup.Error = standard[2];
210
+ startup.Attributes = attributes;
211
+ string shell = Environment.GetEnvironmentVariable("ComSpec") ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe");
212
+ StringBuilder line = new StringBuilder("\"" + shell + "\" /d /s /c \"" + command + "\"");
213
+ // Never launch a command after a stop or owner exit during startup.
214
+ if (File.Exists(Path.Combine(controlDirectory, "stop")) || parent.WaitForExit(0)) return 255;
215
+ Check(CreateProcessW(shell, line, IntPtr.Zero, IntPtr.Zero, true, 0x80000, IntPtr.Zero, Environment.CurrentDirectory, ref startup, out child), "Create contained gate command");
216
+ for (;;)
217
+ {
218
+ uint wait = WaitForSingleObject(child.Process, 25);
219
+ if (wait == 0)
220
+ {
221
+ Check(GetExitCodeProcess(child.Process, out result), "Read gate command exit");
222
+ break;
223
+ }
224
+ Check(wait == 258, "Wait for gate command");
225
+ if (File.Exists(Path.Combine(controlDirectory, "stop")) || parent.WaitForExit(0)) break;
226
+ }
227
+ }
228
+ finally
229
+ {
230
+ // Release our process handles before checking membership accounting.
231
+ if (child.Thread != IntPtr.Zero) CloseHandle(child.Thread);
232
+ if (child.Process != IntPtr.Zero) CloseHandle(child.Process);
233
+ try
234
+ {
235
+ if (job != IntPtr.Zero) TerminateAndWait(job);
236
+ }
237
+ finally
238
+ {
239
+ if (job != IntPtr.Zero) CloseHandle(job);
240
+ foreach (IntPtr handle in standard) if (handle != IntPtr.Zero) CloseHandle(handle);
241
+ if (attributesInitialized) DeleteProcThreadAttributeList(attributes);
242
+ if (attributes != IntPtr.Zero) Marshal.FreeHGlobal(attributes);
243
+ if (jobValue != IntPtr.Zero) Marshal.FreeHGlobal(jobValue);
244
+ if (handleValues != IntPtr.Zero) Marshal.FreeHGlobal(handleValues);
245
+ }
246
+ }
247
+ return unchecked((int)result);
248
+ }
249
+ }
@@ -0,0 +1,119 @@
1
+ // This file is managed by Lisa and IS replaced on each `lisa` run.
2
+ // Do not edit directly — durable changes belong upstream in Lisa.
3
+ // The Windows-only helper owns a Job Object until all of its processes exit.
4
+ import { spawn } from "node:child_process";
5
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import path from "node:path";
8
+ import { env } from "node:process";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const CLEANUP_TIMEOUT_MS = 30000;
12
+
13
+ /**
14
+ * Launch the Windows shell in a native job and return its cleanup operation.
15
+ * @param {string} command Shell source to run.
16
+ * @returns {{child: import("node:child_process").ChildProcess, reap: () => Promise<void>}} Native process boundary.
17
+ */
18
+ export function startWindowsProcessJob(command) {
19
+ const directory = mkdtempSync(path.join(tmpdir(), "lisa-windows-job-"));
20
+ const powershell = path.join(
21
+ env.SystemRoot || "C:\\Windows",
22
+ "System32",
23
+ "WindowsPowerShell",
24
+ "v1.0",
25
+ "powershell.exe"
26
+ );
27
+ const script = fileURLToPath(
28
+ new URL("./windows-process-job.ps1", import.meta.url)
29
+ );
30
+ let child;
31
+ try {
32
+ child = spawn(
33
+ powershell,
34
+ ["-NoLogo", "-NoProfile", "-NonInteractive", "-File", script],
35
+ {
36
+ // The native job supplies isolation. Keep the helper attached so
37
+ // Windows PowerShell retains the caller's standard handles.
38
+ stdio: "inherit",
39
+ env: {
40
+ ...env,
41
+ TEMP: directory,
42
+ TMP: directory,
43
+ LISA_WINDOWS_PROCESS_JOB: JSON.stringify({
44
+ command,
45
+ directory,
46
+ parentPid: process.pid,
47
+ cwd: process.cwd(),
48
+ originalTemp: env.TEMP ?? null,
49
+ originalTmp: env.TMP ?? null,
50
+ }),
51
+ },
52
+ }
53
+ );
54
+ } catch (error) {
55
+ rmSync(directory, { recursive: true, force: true });
56
+ throw error;
57
+ }
58
+ let closed = false;
59
+ const completion = new Promise(resolve => {
60
+ child.once("close", () => {
61
+ closed = true;
62
+ resolve();
63
+ });
64
+ child.once("error", () => {
65
+ if (child.pid === undefined)
66
+ rmSync(directory, { recursive: true, force: true });
67
+ });
68
+ });
69
+ const reap = async () => {
70
+ let timer;
71
+ try {
72
+ if (!closed) writeFileSync(path.join(directory, "stop"), "stop");
73
+ await Promise.race([
74
+ completion,
75
+ new Promise((_, reject) => {
76
+ timer = setTimeout(() => {
77
+ // Killing the owner closes its job handle as a last resort, but
78
+ // cannot supply a completed wait: reject instead of claiming reap.
79
+ child.kill("SIGKILL");
80
+ reject(
81
+ new Error("Windows gate helper did not complete its cleanup wait")
82
+ );
83
+ }, CLEANUP_TIMEOUT_MS);
84
+ }),
85
+ ]);
86
+ if (readFileSync(path.join(directory, "reaped"), "utf8") !== "reaped") {
87
+ throw new Error("Windows gate helper did not confirm an empty job");
88
+ }
89
+ } catch (error) {
90
+ // A failed stop-file write must still close the job owner. Its native
91
+ // KILL_ON_JOB_CLOSE limit stops contained processes; the finally block
92
+ // waits for helper close before removing scratch. Keep this a failure:
93
+ // forced termination cannot provide the normal empty-job receipt.
94
+ if (!closed && !child.killed) child.kill("SIGKILL");
95
+ throw new Error(
96
+ `Windows gate cleanup was not verified (helper exit ${child.exitCode ?? child.signalCode ?? "pending"}): ${error.message}`
97
+ );
98
+ } finally {
99
+ clearTimeout(timer);
100
+ if (!closed) {
101
+ // Give a killed helper time to close before a CLI rejection exits Node.
102
+ // Still bound failure when the OS cannot stop the helper promptly.
103
+ await Promise.race([
104
+ completion,
105
+ new Promise(resolve => {
106
+ timer = setTimeout(resolve, 1000);
107
+ }),
108
+ ]);
109
+ clearTimeout(timer);
110
+ }
111
+ if (closed) rmSync(directory, { recursive: true, force: true });
112
+ else
113
+ child.once("close", () =>
114
+ rmSync(directory, { recursive: true, force: true })
115
+ );
116
+ }
117
+ };
118
+ return { child, reap };
119
+ }
@@ -0,0 +1,25 @@
1
+ # This file is managed by Lisa and IS replaced on each `lisa` run.
2
+ # Do not edit directly — durable changes belong upstream in Lisa.
3
+ # Use the installed Windows PowerShell policy; never override execution policy.
4
+ $ErrorActionPreference = 'Stop'
5
+ $parent = $null
6
+ try {
7
+ $config = $env:LISA_WINDOWS_PROCESS_JOB | ConvertFrom-Json
8
+ Remove-Item Env:LISA_WINDOWS_PROCESS_JOB
9
+ $parent = [Diagnostics.Process]::GetProcessById([int]$config.parentPid)
10
+ $null = $parent.Handle
11
+ Add-Type -Path (Join-Path $PSScriptRoot 'windows-process-job.cs')
12
+ # Compiler scratch belongs to this helper; the gate retains its original env.
13
+ [Environment]::SetEnvironmentVariable('TEMP', $config.originalTemp)
14
+ [Environment]::SetEnvironmentVariable('TMP', $config.originalTmp)
15
+ [Environment]::CurrentDirectory = $config.cwd
16
+ $code = [LisaWindowsProcessJob]::Run($config.command, $config.directory, $parent)
17
+ # Only a completed native membership wait may authorize a successful reap.
18
+ [IO.File]::WriteAllText((Join-Path $config.directory 'reaped'), 'reaped')
19
+ exit $code
20
+ } catch {
21
+ [Console]::Error.WriteLine('Windows gate containment failed: ' + $_.Exception.Message)
22
+ exit 1
23
+ } finally {
24
+ if ($null -ne $parent) { $parent.Dispose() }
25
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"lisa-owned-hash-ledger.d.ts","sourceRoot":"","sources":["../../src/core/lisa-owned-hash-ledger.ts"],"names":[],"mappings":"AACA,6EAA6E;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAC3C,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CA20CjC,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,+BAA+B,EAAE,QAAQ,CACpD,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CA8uCjC,CAAC"}
1
+ {"version":3,"file":"lisa-owned-hash-ledger.d.ts","sourceRoot":"","sources":["../../src/core/lisa-owned-hash-ledger.ts"],"names":[],"mappings":"AACA,6EAA6E;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAC3C,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAg2CjC,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,+BAA+B,EAAE,QAAQ,CACpD,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAmwCjC,CAAC"}
@@ -153,6 +153,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
153
153
  "4a5dce9c10d75b1027c9cc8606aba99cfd1fbcca3fa52f12a8a2433eac3e55bd",
154
154
  "5c666bc36267bb4008e074638be96856676957ff79e0506c7a6e073bce5319e6",
155
155
  "880f37a8cc95ed051d7cfbdd4560d3e01cc99b1b9d01631681cadf0e33d37bd2",
156
+ "99764b136eaa7bba3b7abf78f60bc6805f8c008a9cac59c78c5eadba35cecdc7",
156
157
  "9d570347fb1177be2381fe3f119882762be08a1a23c0a9486bdf5a6a97e02b5b",
157
158
  "b284a59b3ce5dfa8fa4e6b9c5789c6c9c08120eaf957a91453fc659322feab28",
158
159
  "b3c89213aab541483cf50c9c7ccbd4dd931fb5ef4d079e970ea82520f22bd01c",
@@ -472,7 +473,9 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
472
473
  "cffebe7feca1b5cce7b514bd1b5e991375cbc20607dff70385699e474e36b4d3",
473
474
  "d9ce218e754bf28a8d3d9b3b92c34ae9ce2e7b22182d31d9a87302ba33cc0151",
474
475
  "eb73088635fddf236fca92d1c94324a889c503843155d436835b96d3ed6fa1f1",
476
+ "f248672f91e4f1b7a604278762e1b6a037ea05356ff9ce70d31057ae3c257d83",
475
477
  "fb68902c68b4cd156f26d2f743364f611a54b833bc445b5144b4c844f553f188",
478
+ "fe045e89990248b735281275a736ab54861fdb003af3267cd1166c4f1489b91b",
476
479
  ]),
477
480
  "scripts/lib/reusable-workflow-load-adapter.d.mts": Object.freeze([
478
481
  "289c57b7cf1de7c2b5a2e89de8e706d53fc590592b8f3ba08ba41e04f97fd343",
@@ -501,6 +504,24 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
501
504
  "6203064d0403906016f6700b092551ff561b811fb9c02cbbdec59b85b0af81b0",
502
505
  "66fb27cc1de6baf48acf908715624c04b7738f831944a6c1c9daff50383026be",
503
506
  ]),
507
+ "scripts/lib/windows-process-job.cs": Object.freeze([
508
+ "198c23dfee840abaad516d84e70c943eca565baf13d54bc143c8e7f925be038c",
509
+ "7bbe3b13bfc35ca2a0b85fbee9a8f0a6549bf8b05997f6ce4b361bd09d924218",
510
+ "8de9cd7b38c7afd0b837de9baa4cb8d3ddceeb0a1b434862e2ad3549044a6790",
511
+ "bf32dc865bd16c03181df1564e2576ce308e952d21bc7a71571b2138532763f9",
512
+ ]),
513
+ "scripts/lib/windows-process-job.mjs": Object.freeze([
514
+ "109224c7c0d98447117f78a794a70df07925370a1ab01945bf0369f5bfe498b8",
515
+ "33fb198c1c5f32e6a615741eb21ee22a86d38701c6137bc00c72a273c2ad4b76",
516
+ "599628647c03ac22fe7cd5df22219a70b67e449cc893cc85564a93d658df2c2d",
517
+ "bc7cbab9ff259b4858c35b74803f962375277eb56b57d7b7cb05ddbd8aa73063",
518
+ "d7362a646468f87805b6d9e87fe61f2a2a97b4d481b31627a634edb66229f19d",
519
+ "e448bb4c092d6ae5c868352f355c78e6d8d549cdd6a12c505ca0a5923e0cae62",
520
+ ]),
521
+ "scripts/lib/windows-process-job.ps1": Object.freeze([
522
+ "5b978f201be9a035001f0cc4a7e8da76a67f26e6f99da91907747c4d65d58f1b",
523
+ "d73b15152703d7f195d1e06f7d7cee5437373237fcd74fe68afbd79fccebc1ba",
524
+ ]),
504
525
  "scripts/lib/worktree-dependencies.d.mts": Object.freeze([
505
526
  "66f77b767869f57185b819ca6f294ad0706348b50eba12f78af2430d9c8775df",
506
527
  "acc2b7a00f1e6f806359c6609e58743b6293f17f3a5ac30abd18b1a22fab8bfc",
@@ -1519,6 +1540,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
1519
1540
  "43558ba985967a3710e75361845afa987e84b17dbc264bb3ff68d8bfdf463318",
1520
1541
  "4a5dce9c10d75b1027c9cc8606aba99cfd1fbcca3fa52f12a8a2433eac3e55bd",
1521
1542
  "880f37a8cc95ed051d7cfbdd4560d3e01cc99b1b9d01631681cadf0e33d37bd2",
1543
+ "99764b136eaa7bba3b7abf78f60bc6805f8c008a9cac59c78c5eadba35cecdc7",
1522
1544
  "9d570347fb1177be2381fe3f119882762be08a1a23c0a9486bdf5a6a97e02b5b",
1523
1545
  "b284a59b3ce5dfa8fa4e6b9c5789c6c9c08120eaf957a91453fc659322feab28",
1524
1546
  "b3c89213aab541483cf50c9c7ccbd4dd931fb5ef4d079e970ea82520f22bd01c",
@@ -1821,7 +1843,9 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
1821
1843
  "cffebe7feca1b5cce7b514bd1b5e991375cbc20607dff70385699e474e36b4d3",
1822
1844
  "d9ce218e754bf28a8d3d9b3b92c34ae9ce2e7b22182d31d9a87302ba33cc0151",
1823
1845
  "eb73088635fddf236fca92d1c94324a889c503843155d436835b96d3ed6fa1f1",
1846
+ "f248672f91e4f1b7a604278762e1b6a037ea05356ff9ce70d31057ae3c257d83",
1824
1847
  "fb68902c68b4cd156f26d2f743364f611a54b833bc445b5144b4c844f553f188",
1848
+ "fe045e89990248b735281275a736ab54861fdb003af3267cd1166c4f1489b91b",
1825
1849
  ]),
1826
1850
  "scripts/lib/reusable-workflow-load-adapter.d.mts": Object.freeze([
1827
1851
  "289c57b7cf1de7c2b5a2e89de8e706d53fc590592b8f3ba08ba41e04f97fd343",
@@ -1850,6 +1874,24 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
1850
1874
  "6203064d0403906016f6700b092551ff561b811fb9c02cbbdec59b85b0af81b0",
1851
1875
  "66fb27cc1de6baf48acf908715624c04b7738f831944a6c1c9daff50383026be",
1852
1876
  ]),
1877
+ "scripts/lib/windows-process-job.cs": Object.freeze([
1878
+ "198c23dfee840abaad516d84e70c943eca565baf13d54bc143c8e7f925be038c",
1879
+ "7bbe3b13bfc35ca2a0b85fbee9a8f0a6549bf8b05997f6ce4b361bd09d924218",
1880
+ "8de9cd7b38c7afd0b837de9baa4cb8d3ddceeb0a1b434862e2ad3549044a6790",
1881
+ "bf32dc865bd16c03181df1564e2576ce308e952d21bc7a71571b2138532763f9",
1882
+ ]),
1883
+ "scripts/lib/windows-process-job.mjs": Object.freeze([
1884
+ "109224c7c0d98447117f78a794a70df07925370a1ab01945bf0369f5bfe498b8",
1885
+ "33fb198c1c5f32e6a615741eb21ee22a86d38701c6137bc00c72a273c2ad4b76",
1886
+ "599628647c03ac22fe7cd5df22219a70b67e449cc893cc85564a93d658df2c2d",
1887
+ "bc7cbab9ff259b4858c35b74803f962375277eb56b57d7b7cb05ddbd8aa73063",
1888
+ "d7362a646468f87805b6d9e87fe61f2a2a97b4d481b31627a634edb66229f19d",
1889
+ "e448bb4c092d6ae5c868352f355c78e6d8d549cdd6a12c505ca0a5923e0cae62",
1890
+ ]),
1891
+ "scripts/lib/windows-process-job.ps1": Object.freeze([
1892
+ "5b978f201be9a035001f0cc4a7e8da76a67f26e6f99da91907747c4d65d58f1b",
1893
+ "d73b15152703d7f195d1e06f7d7cee5437373237fcd74fe68afbd79fccebc1ba",
1894
+ ]),
1853
1895
  "scripts/lib/worktree-dependencies.d.mts": Object.freeze([
1854
1896
  "66f77b767869f57185b819ca6f294ad0706348b50eba12f78af2430d9c8775df",
1855
1897
  "acc2b7a00f1e6f806359c6609e58743b6293f17f3a5ac30abd18b1a22fab8bfc",