@feigi/fleet-ctl 3.102.2 → 3.102.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feigi/fleet-ctl",
3
- "version": "3.102.2",
3
+ "version": "3.102.4",
4
4
  "description": "Agent fleet: run-team controller, merge bot, PR reviewer, and the ticket pipeline they share",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -64,14 +64,17 @@
64
64
  // forwarded to the running command, no further command starts, and nothing
65
65
  // is written, so an older record stays where it is — settle compares the
66
66
  // record's tree with the reported head's tree, so a stale one cannot pass for
67
- // this run. The forwarded signal reaches the `sh -c` running the command, and
68
- // stops the command's own run only if that shell execs it or relays the
69
- // signal: a compound command that does neither runs on, and this script exits
70
- // only once that run closes its output. A signal received before step 3 takes
71
- // its default action: the script dies of it, printing nothing. A command that
72
- // dies of a signal this script did not receive is a failure like any other,
73
- // recorded red as the signal's number plus 128. A record is a claim, no
74
- // attestation: the member produces it, and nothing here stops a forged one.
67
+ // this run. Each command runs in a process group of its own, and the forwarded
68
+ // signal is delivered to that whole group — the `sh -c`, the commands it
69
+ // started and everything they started — so a compound command stops with it,
70
+ // unless a member of the group ignores the signal, and this script exits once
71
+ // the run closes its output. A SIGKILL aimed at this script, by pid or at its
72
+ // process group, cannot be forwarded and leaves the command's group running.
73
+ // A signal received before step 3 takes its default action: the script dies
74
+ // of it, printing nothing. A command that dies of a signal this script did
75
+ // not receive is a failure like any other, recorded red as the signal's
76
+ // number plus 128. A record is a claim, no attestation: the member produces
77
+ // it, and nothing here stops a forged one.
75
78
  //
76
79
  // Exit status: 0 both commands passed and the record is written; 1 a command
77
80
  // failed — the record says which, and the script's stderr names it; 2 a
@@ -208,16 +211,16 @@ async function cli() {
208
211
  });
209
212
  }
210
213
  // A SIGTERM, SIGINT or SIGHUP this script itself receives is forwarded to
211
- // the running command and marks the run killed: no further command starts
212
- // and nothing is recorded. That holds until the record is decided, so a
213
- // signal during the commands never takes the default action and cuts off
214
- // the output still queued. After that only queued output is left, and a
215
- // signal takes its default action: the handler removes every handler and
216
- // re-raises it, so a reader that stalls on that output cannot leave a
217
- // process no signal stops. The handlers are never removed eagerly at the
218
- // decision: removing them drops a signal caught but not yet dispatched —
219
- // one landing between the record's rename and that removal — which would
220
- // leave the process running with nothing left to stop it.
214
+ // the running command's process group and marks the run killed: no further
215
+ // command starts and nothing is recorded. That holds until the record is
216
+ // decided, so a signal during the commands never takes the default action
217
+ // and cuts off the output still queued. After that only queued output is
218
+ // left, and a signal takes its default action: the handler removes every
219
+ // handler and re-raises it, so a reader that stalls on that output cannot
220
+ // leave a process no signal stops. The handlers are never removed eagerly
221
+ // at the decision: removing them drops a signal caught but not yet
222
+ // dispatched — one landing between the record's rename and that removal —
223
+ // which would leave the process running with nothing left to stop it.
221
224
  const run = { killed: null, child: null, ranMs: 0, decided: false };
222
225
  const handlers = KILL_SIGNALS.map((sig) => [sig, () => {
223
226
  if (run.decided) {
@@ -226,7 +229,7 @@ async function cli() {
226
229
  return;
227
230
  }
228
231
  run.killed ??= sig;
229
- run.child?.kill(sig);
232
+ signalGroup(run.child, sig);
230
233
  }]);
231
234
  for (const [sig, handler] of handlers) process.on(sig, handler);
232
235
  const check = checkCmd.trim() === "true"
@@ -278,17 +281,33 @@ async function cli() {
278
281
 
279
282
  if (isCLI(import.meta.url)) await cli();
280
283
 
281
- // One command, `sh -c` from `cwd`, its output passed through and its tail kept
282
- // for the counts. A signal that stops it is its number plus 128. `run` carries
283
- // the child to the signal handlers, and the command's duration to the kill
284
- // message.
284
+ // The signal goes to the command's whole process group. A group that is
285
+ // already gone has nothing to receive it, so ESRCH is not an error. EPERM is
286
+ // macOS's answer for a group whose members have all exited but are not yet
287
+ // reaped, and for one holding a member this user may not signal: the
288
+ // command's own shell is then signalled directly. Any other failure still
289
+ // surfaces. A spawn that never produced a pid has no group.
290
+ function signalGroup(child, sig) {
291
+ if (child?.pid === undefined) return;
292
+ try {
293
+ process.kill(-child.pid, sig);
294
+ } catch (e) {
295
+ if (e.code === "EPERM") child.kill(sig);
296
+ else if (e.code !== "ESRCH") throw e;
297
+ }
298
+ }
299
+
300
+ // One command, `sh -c` from `cwd`, in a process group of its own, its output
301
+ // passed through and its tail kept for the counts. A signal that stops it is
302
+ // its number plus 128. `run` carries the child to the signal handlers, and the
303
+ // command's duration to the kill message.
285
304
  function runCommand(run, command, cwd, env) {
286
305
  return new Promise((resolve) => {
287
306
  let tail = "";
288
307
  const keep = (chunk) => {
289
308
  tail = (tail + chunk).slice(-TAIL_BYTES);
290
309
  };
291
- const child = spawn("sh", ["-c", command], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
310
+ const child = spawn("sh", ["-c", command], { cwd, env, stdio: ["ignore", "pipe", "pipe"], detached: true });
292
311
  run.child = child;
293
312
  const startedAt = Date.now();
294
313
  child.stdout.setEncoding("utf8");