runcastle 1.2.1 → 1.2.3

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/bin/runcastle.js CHANGED
@@ -914,6 +914,23 @@ var init_resolve_executable = __esm(() => {
914
914
  };
915
915
  });
916
916
 
917
+ // src/pty/kill-tree.ts
918
+ import { execFile } from "child_process";
919
+ import { promisify } from "util";
920
+ async function killProcessTree(pid) {
921
+ try {
922
+ if (process.platform === "win32") {
923
+ await execFileAsync("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true });
924
+ } else {
925
+ process.kill(-pid, "SIGTERM");
926
+ }
927
+ } catch {}
928
+ }
929
+ var execFileAsync;
930
+ var init_kill_tree = __esm(() => {
931
+ execFileAsync = promisify(execFile);
932
+ });
933
+
917
934
  // src/pty/pty-sidecar.ts
918
935
  import { spawn } from "child_process";
919
936
  import { createRequire } from "module";
@@ -1095,6 +1112,9 @@ ${why.replace(/\n/g, `\r
1095
1112
  }
1096
1113
  }, 500);
1097
1114
  },
1115
+ killTree() {
1116
+ return killProcessTree(process.platform === "win32" ? child.pid ?? pid : pid);
1117
+ },
1098
1118
  get pid() {
1099
1119
  return pid;
1100
1120
  },
@@ -1107,6 +1127,7 @@ var HOST_PATH;
1107
1127
  var init_pty_sidecar = __esm(() => {
1108
1128
  init_asset_paths();
1109
1129
  init_resolve_executable();
1130
+ init_kill_tree();
1110
1131
  HOST_PATH = resolveAsset(ASSET_ENV.ptyHost, fileURLToPath(new URL("./pty-host.cjs", import.meta.url)));
1111
1132
  });
1112
1133
 
@@ -1193,6 +1214,9 @@ function createNativePtySession(cmd, args, opts) {
1193
1214
  proc.kill();
1194
1215
  } catch {}
1195
1216
  },
1217
+ killTree() {
1218
+ return killProcessTree(proc.pid);
1219
+ },
1196
1220
  get pid() {
1197
1221
  return proc.pid;
1198
1222
  },
@@ -1204,6 +1228,7 @@ function createNativePtySession(cmd, args, opts) {
1204
1228
  var cached = null, BACKEND_LOGGED;
1205
1229
  var init_pty = __esm(() => {
1206
1230
  init_resolve_executable();
1231
+ init_kill_tree();
1207
1232
  init_pty_sidecar();
1208
1233
  BACKEND_LOGGED = Symbol.for("runcastle.pty.backend.logged");
1209
1234
  });
@@ -1855,18 +1880,24 @@ function devPaneLive(paneId) {
1855
1880
  const entry = ptyRegistry().get(paneId);
1856
1881
  return !!entry && !entry.exited;
1857
1882
  }
1858
- function stopDevPane(paneId) {
1883
+ function killTreeBounded(pty) {
1884
+ return new Promise((resolve4) => {
1885
+ const timer = setTimeout(resolve4, KILL_TREE_TIMEOUT_MS);
1886
+ pty.killTree().catch(() => {}).then(() => {
1887
+ clearTimeout(timer);
1888
+ resolve4();
1889
+ });
1890
+ });
1891
+ }
1892
+ async function stopDevPane(paneId) {
1859
1893
  const reg = ptyRegistry();
1860
1894
  const entry = reg.get(paneId);
1861
- if (entry && !entry.exited && process.platform !== "win32") {
1862
- try {
1863
- process.kill(-entry.pty.pid, "SIGTERM");
1864
- } catch {}
1865
- }
1895
+ if (entry && !entry.exited)
1896
+ await killTreeBounded(entry.pty);
1866
1897
  reg.kill(paneId);
1867
1898
  reg.remove(paneId);
1868
1899
  }
1869
- var DRIVE_PREFIX = "drive:", SNIFF_WINDOW = 8192;
1900
+ var DRIVE_PREFIX = "drive:", SNIFF_WINDOW = 8192, KILL_TREE_TIMEOUT_MS = 5000;
1870
1901
  var init_dev_pane = __esm(() => {
1871
1902
  init_events();
1872
1903
  init_registry();
@@ -2795,7 +2826,7 @@ async function testDrive(ctx, project, feature, action) {
2795
2826
  const detachedWorktree2 = testDriveState.detachedWorktree;
2796
2827
  const devPaneId = testDriveState.devPaneId;
2797
2828
  if (devPaneId)
2798
- stopDevPane(devPaneId);
2829
+ await stopDevPane(devPaneId);
2799
2830
  const teardown = await runDriveHookStep(ctx, scope, project.repoPath, "teardown", project.driveStopCommand, driveEnvFor(ctx, project, { slug: feature.slug, branch }, scope).env);
2800
2831
  const carriedChanges = await dirtyPaths(g);
2801
2832
  await g.checkout(previousBranch2);
@@ -2935,7 +2966,7 @@ async function startDryRun(ctx, project) {
2935
2966
  async function stopDryRun(ctx, project, state) {
2936
2967
  const scope = { projectId: project.id };
2937
2968
  if (state.devPaneId)
2938
- stopDevPane(state.devPaneId);
2969
+ await stopDevPane(state.devPaneId);
2939
2970
  const teardown = await runDriveHookStep(ctx, scope, project.repoPath, "teardown", project.driveStopCommand, state.env);
2940
2971
  state.observed.teardownOk = teardown?.result.ok;
2941
2972
  testDriveState = undefined;
@@ -3430,7 +3461,7 @@ function landProjectSession(ctx, session) {
3430
3461
  data: { sessionId: session.id, branch: PROJECT_BRANCH, ...data }
3431
3462
  });
3432
3463
  };
3433
- landProjectBranch(project).then((res) => {
3464
+ const landing = landProjectBranch(project).then((res) => {
3434
3465
  if (!res)
3435
3466
  return;
3436
3467
  if (res.landed) {
@@ -3443,6 +3474,8 @@ function landProjectSession(ctx, session) {
3443
3474
  }).catch((e) => {
3444
3475
  report("project.land_failed", `could not land the project session's work onto ${project.mainBranch}: ${e instanceof Error ? e.message : String(e)} \u2014 ${kept}`, { error: e instanceof Error ? e.message : String(e) });
3445
3476
  });
3477
+ inFlightLandings.add(landing);
3478
+ landing.finally(() => inFlightLandings.delete(landing));
3446
3479
  }
3447
3480
  function markSessionEnded(ctx, id) {
3448
3481
  const existing = getSessionRow(ctx, id);
@@ -3471,7 +3504,7 @@ function mostRecentLiveSession(ctx) {
3471
3504
  const row = ctx.db.select().from(sessions).where(eq5(sessions.status, "live")).orderBy(desc3(sql2`rowid`)).limit(1).get();
3472
3505
  return row ? rowToSession(row) : null;
3473
3506
  }
3474
- var KICKOFF_DELAY_MS = 1500, KICKOFF_SUBMIT_DELAY_MS = 350, KICKOFF_CONFIRM_MS = 12000, KICKOFF_MAX_ATTEMPTS = 3, CLEAR_INPUT = "\x15", SESSION_READY_TIMEOUT_MS = 25000, CONVERGE_KICKOFF_LINE, KICKOFF_LINES, RESUME_KICKOFF_PREFIX, pendingKickoffOverrides, deliveries, MATCH_PREFIX = 40, landedProjectSessions;
3507
+ var KICKOFF_DELAY_MS = 1500, KICKOFF_SUBMIT_DELAY_MS = 350, KICKOFF_CONFIRM_MS = 12000, KICKOFF_MAX_ATTEMPTS = 3, CLEAR_INPUT = "\x15", SESSION_READY_TIMEOUT_MS = 25000, CONVERGE_KICKOFF_LINE, KICKOFF_LINES, RESUME_KICKOFF_PREFIX, pendingKickoffOverrides, deliveries, MATCH_PREFIX = 40, landedProjectSessions, inFlightLandings;
3475
3508
  var init_sessions = __esm(() => {
3476
3509
  init_src();
3477
3510
  init_schema();
@@ -3494,6 +3527,7 @@ var init_sessions = __esm(() => {
3494
3527
  pendingKickoffOverrides = new Map;
3495
3528
  deliveries = new Map;
3496
3529
  landedProjectSessions = new Set;
3530
+ inFlightLandings = new Set;
3497
3531
  });
3498
3532
 
3499
3533
  // src/launcher/reconcile.ts
package/index.js CHANGED
@@ -913,6 +913,23 @@ var init_resolve_executable = __esm(() => {
913
913
  };
914
914
  });
915
915
 
916
+ // src/pty/kill-tree.ts
917
+ import { execFile } from "child_process";
918
+ import { promisify } from "util";
919
+ async function killProcessTree(pid) {
920
+ try {
921
+ if (process.platform === "win32") {
922
+ await execFileAsync("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true });
923
+ } else {
924
+ process.kill(-pid, "SIGTERM");
925
+ }
926
+ } catch {}
927
+ }
928
+ var execFileAsync;
929
+ var init_kill_tree = __esm(() => {
930
+ execFileAsync = promisify(execFile);
931
+ });
932
+
916
933
  // src/pty/pty-sidecar.ts
917
934
  import { spawn } from "child_process";
918
935
  import { createRequire } from "module";
@@ -1094,6 +1111,9 @@ ${why.replace(/\n/g, `\r
1094
1111
  }
1095
1112
  }, 500);
1096
1113
  },
1114
+ killTree() {
1115
+ return killProcessTree(process.platform === "win32" ? child.pid ?? pid : pid);
1116
+ },
1097
1117
  get pid() {
1098
1118
  return pid;
1099
1119
  },
@@ -1106,6 +1126,7 @@ var HOST_PATH;
1106
1126
  var init_pty_sidecar = __esm(() => {
1107
1127
  init_asset_paths();
1108
1128
  init_resolve_executable();
1129
+ init_kill_tree();
1109
1130
  HOST_PATH = resolveAsset(ASSET_ENV.ptyHost, fileURLToPath(new URL("./pty-host.cjs", import.meta.url)));
1110
1131
  });
1111
1132
 
@@ -1192,6 +1213,9 @@ function createNativePtySession(cmd, args, opts) {
1192
1213
  proc.kill();
1193
1214
  } catch {}
1194
1215
  },
1216
+ killTree() {
1217
+ return killProcessTree(proc.pid);
1218
+ },
1195
1219
  get pid() {
1196
1220
  return proc.pid;
1197
1221
  },
@@ -1203,6 +1227,7 @@ function createNativePtySession(cmd, args, opts) {
1203
1227
  var cached = null, BACKEND_LOGGED;
1204
1228
  var init_pty = __esm(() => {
1205
1229
  init_resolve_executable();
1230
+ init_kill_tree();
1206
1231
  init_pty_sidecar();
1207
1232
  BACKEND_LOGGED = Symbol.for("runcastle.pty.backend.logged");
1208
1233
  });
@@ -1854,18 +1879,24 @@ function devPaneLive(paneId) {
1854
1879
  const entry = ptyRegistry().get(paneId);
1855
1880
  return !!entry && !entry.exited;
1856
1881
  }
1857
- function stopDevPane(paneId) {
1882
+ function killTreeBounded(pty) {
1883
+ return new Promise((resolve4) => {
1884
+ const timer = setTimeout(resolve4, KILL_TREE_TIMEOUT_MS);
1885
+ pty.killTree().catch(() => {}).then(() => {
1886
+ clearTimeout(timer);
1887
+ resolve4();
1888
+ });
1889
+ });
1890
+ }
1891
+ async function stopDevPane(paneId) {
1858
1892
  const reg = ptyRegistry();
1859
1893
  const entry = reg.get(paneId);
1860
- if (entry && !entry.exited && process.platform !== "win32") {
1861
- try {
1862
- process.kill(-entry.pty.pid, "SIGTERM");
1863
- } catch {}
1864
- }
1894
+ if (entry && !entry.exited)
1895
+ await killTreeBounded(entry.pty);
1865
1896
  reg.kill(paneId);
1866
1897
  reg.remove(paneId);
1867
1898
  }
1868
- var DRIVE_PREFIX = "drive:", SNIFF_WINDOW = 8192;
1899
+ var DRIVE_PREFIX = "drive:", SNIFF_WINDOW = 8192, KILL_TREE_TIMEOUT_MS = 5000;
1869
1900
  var init_dev_pane = __esm(() => {
1870
1901
  init_events();
1871
1902
  init_registry();
@@ -2794,7 +2825,7 @@ async function testDrive(ctx, project, feature, action) {
2794
2825
  const detachedWorktree2 = testDriveState.detachedWorktree;
2795
2826
  const devPaneId = testDriveState.devPaneId;
2796
2827
  if (devPaneId)
2797
- stopDevPane(devPaneId);
2828
+ await stopDevPane(devPaneId);
2798
2829
  const teardown = await runDriveHookStep(ctx, scope, project.repoPath, "teardown", project.driveStopCommand, driveEnvFor(ctx, project, { slug: feature.slug, branch }, scope).env);
2799
2830
  const carriedChanges = await dirtyPaths(g);
2800
2831
  await g.checkout(previousBranch2);
@@ -2934,7 +2965,7 @@ async function startDryRun(ctx, project) {
2934
2965
  async function stopDryRun(ctx, project, state) {
2935
2966
  const scope = { projectId: project.id };
2936
2967
  if (state.devPaneId)
2937
- stopDevPane(state.devPaneId);
2968
+ await stopDevPane(state.devPaneId);
2938
2969
  const teardown = await runDriveHookStep(ctx, scope, project.repoPath, "teardown", project.driveStopCommand, state.env);
2939
2970
  state.observed.teardownOk = teardown?.result.ok;
2940
2971
  testDriveState = undefined;
@@ -3429,7 +3460,7 @@ function landProjectSession(ctx, session) {
3429
3460
  data: { sessionId: session.id, branch: PROJECT_BRANCH, ...data }
3430
3461
  });
3431
3462
  };
3432
- landProjectBranch(project).then((res) => {
3463
+ const landing = landProjectBranch(project).then((res) => {
3433
3464
  if (!res)
3434
3465
  return;
3435
3466
  if (res.landed) {
@@ -3442,6 +3473,8 @@ function landProjectSession(ctx, session) {
3442
3473
  }).catch((e) => {
3443
3474
  report("project.land_failed", `could not land the project session's work onto ${project.mainBranch}: ${e instanceof Error ? e.message : String(e)} \u2014 ${kept}`, { error: e instanceof Error ? e.message : String(e) });
3444
3475
  });
3476
+ inFlightLandings.add(landing);
3477
+ landing.finally(() => inFlightLandings.delete(landing));
3445
3478
  }
3446
3479
  function markSessionEnded(ctx, id) {
3447
3480
  const existing = getSessionRow(ctx, id);
@@ -3470,7 +3503,7 @@ function mostRecentLiveSession(ctx) {
3470
3503
  const row = ctx.db.select().from(sessions).where(eq5(sessions.status, "live")).orderBy(desc3(sql2`rowid`)).limit(1).get();
3471
3504
  return row ? rowToSession(row) : null;
3472
3505
  }
3473
- var KICKOFF_DELAY_MS = 1500, KICKOFF_SUBMIT_DELAY_MS = 350, KICKOFF_CONFIRM_MS = 12000, KICKOFF_MAX_ATTEMPTS = 3, CLEAR_INPUT = "\x15", SESSION_READY_TIMEOUT_MS = 25000, CONVERGE_KICKOFF_LINE, KICKOFF_LINES, RESUME_KICKOFF_PREFIX, pendingKickoffOverrides, deliveries, MATCH_PREFIX = 40, landedProjectSessions;
3506
+ var KICKOFF_DELAY_MS = 1500, KICKOFF_SUBMIT_DELAY_MS = 350, KICKOFF_CONFIRM_MS = 12000, KICKOFF_MAX_ATTEMPTS = 3, CLEAR_INPUT = "\x15", SESSION_READY_TIMEOUT_MS = 25000, CONVERGE_KICKOFF_LINE, KICKOFF_LINES, RESUME_KICKOFF_PREFIX, pendingKickoffOverrides, deliveries, MATCH_PREFIX = 40, landedProjectSessions, inFlightLandings;
3474
3507
  var init_sessions = __esm(() => {
3475
3508
  init_src();
3476
3509
  init_schema();
@@ -3493,6 +3526,7 @@ var init_sessions = __esm(() => {
3493
3526
  pendingKickoffOverrides = new Map;
3494
3527
  deliveries = new Map;
3495
3528
  landedProjectSessions = new Set;
3529
+ inFlightLandings = new Set;
3496
3530
  });
3497
3531
 
3498
3532
  // src/launcher/reconcile.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcastle",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Burn tickets into shipped features with Claude Code.",
5
5
  "license": "MIT",
6
6
  "type": "module",