runcastle 1.2.2 → 1.2.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/bin/runcastle.js CHANGED
@@ -26,6 +26,7 @@ __export(exports_paths, {
26
26
  projectWorktreesDir: () => projectWorktreesDir,
27
27
  prodDataDir: () => prodDataDir,
28
28
  logsDir: () => logsDir,
29
+ installIdPath: () => installIdPath,
29
30
  featureDocsRel: () => featureDocsRel,
30
31
  envPath: () => envPath,
31
32
  devDataDir: () => devDataDir,
@@ -64,6 +65,9 @@ function configPath() {
64
65
  function envPath() {
65
66
  return join(dataDir(), ".env");
66
67
  }
68
+ function installIdPath() {
69
+ return join(dataDir(), "install-id");
70
+ }
67
71
  function logsDir() {
68
72
  return join(dataDir(), "logs");
69
73
  }
@@ -246,7 +250,7 @@ var init_ids = () => {};
246
250
 
247
251
  // ../core/src/schemas.ts
248
252
  import * as z2 from "zod";
249
- var Phase, TicketStatus, SessionKind, RunStatus, FeatureStatus, SessionStatus, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, TestNoteStatus, TestNote, PREPARED_KEYS, PreparedKey, DRIVE_LOOP_KEYS, FindingSource, PROJECT_NAME_MAX = 80, ProjectName, Project, ProjectFinding, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow;
253
+ var Phase, TicketStatus, SessionKind, SessionPurpose, MergeBranchPair, RunStatus, FeatureStatus, SessionStatus, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, TestNoteStatus, TestNote, PREPARED_KEYS, PreparedKey, DRIVE_LOOP_KEYS, FindingSource, PROJECT_NAME_MAX = 80, ProjectName, Project, ProjectFinding, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow;
250
254
  var init_schemas = __esm(() => {
251
255
  Phase = z2.enum([
252
256
  "ideation",
@@ -266,6 +270,11 @@ var init_schemas = __esm(() => {
266
270
  "prepare",
267
271
  "project"
268
272
  ]);
273
+ SessionPurpose = z2.enum(["resolve-conflict"]);
274
+ MergeBranchPair = z2.object({
275
+ mergeFrom: z2.string(),
276
+ mergeInto: z2.string()
277
+ });
269
278
  RunStatus = z2.enum(["running", "succeeded", "failed", "cancelled"]);
270
279
  FeatureStatus = z2.enum(["active", "shipped", "archived"]);
271
280
  SessionStatus = z2.enum(["launching", "live", "ended"]);
@@ -382,6 +391,8 @@ var init_schemas = __esm(() => {
382
391
  featureId: z2.string().optional(),
383
392
  projectId: z2.string().optional(),
384
393
  kind: SessionKind,
394
+ purpose: SessionPurpose.optional(),
395
+ purposeData: MergeBranchPair.optional(),
385
396
  ccSessionId: z2.string().optional(),
386
397
  transcriptPath: z2.string().optional(),
387
398
  status: SessionStatus,
@@ -620,6 +631,8 @@ var init_db_schema = __esm(() => {
620
631
  projectId: text("project_id"),
621
632
  lap: integer("lap").notNull().default(1),
622
633
  kind: text("kind").notNull().$type(),
634
+ purpose: text("purpose").$type(),
635
+ purposeData: text("purpose_data", { mode: "json" }).$type(),
623
636
  ccSessionId: text("cc_session_id"),
624
637
  transcriptPath: text("transcript_path"),
625
638
  status: text("status").notNull().$type(),
@@ -914,6 +927,23 @@ var init_resolve_executable = __esm(() => {
914
927
  };
915
928
  });
916
929
 
930
+ // src/pty/kill-tree.ts
931
+ import { execFile } from "child_process";
932
+ import { promisify } from "util";
933
+ async function killProcessTree(pid) {
934
+ try {
935
+ if (process.platform === "win32") {
936
+ await execFileAsync("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true });
937
+ } else {
938
+ process.kill(-pid, "SIGTERM");
939
+ }
940
+ } catch {}
941
+ }
942
+ var execFileAsync;
943
+ var init_kill_tree = __esm(() => {
944
+ execFileAsync = promisify(execFile);
945
+ });
946
+
917
947
  // src/pty/pty-sidecar.ts
918
948
  import { spawn } from "child_process";
919
949
  import { createRequire } from "module";
@@ -1095,6 +1125,9 @@ ${why.replace(/\n/g, `\r
1095
1125
  }
1096
1126
  }, 500);
1097
1127
  },
1128
+ killTree() {
1129
+ return killProcessTree(process.platform === "win32" ? child.pid ?? pid : pid);
1130
+ },
1098
1131
  get pid() {
1099
1132
  return pid;
1100
1133
  },
@@ -1107,6 +1140,7 @@ var HOST_PATH;
1107
1140
  var init_pty_sidecar = __esm(() => {
1108
1141
  init_asset_paths();
1109
1142
  init_resolve_executable();
1143
+ init_kill_tree();
1110
1144
  HOST_PATH = resolveAsset(ASSET_ENV.ptyHost, fileURLToPath(new URL("./pty-host.cjs", import.meta.url)));
1111
1145
  });
1112
1146
 
@@ -1193,6 +1227,9 @@ function createNativePtySession(cmd, args, opts) {
1193
1227
  proc.kill();
1194
1228
  } catch {}
1195
1229
  },
1230
+ killTree() {
1231
+ return killProcessTree(proc.pid);
1232
+ },
1196
1233
  get pid() {
1197
1234
  return proc.pid;
1198
1235
  },
@@ -1204,6 +1241,7 @@ function createNativePtySession(cmd, args, opts) {
1204
1241
  var cached = null, BACKEND_LOGGED;
1205
1242
  var init_pty = __esm(() => {
1206
1243
  init_resolve_executable();
1244
+ init_kill_tree();
1207
1245
  init_pty_sidecar();
1208
1246
  BACKEND_LOGGED = Symbol.for("runcastle.pty.backend.logged");
1209
1247
  });
@@ -1541,6 +1579,8 @@ function rowToSession(row) {
1541
1579
  featureId: row.featureId ?? undefined,
1542
1580
  projectId: row.projectId ?? undefined,
1543
1581
  kind: row.kind,
1582
+ purpose: row.purpose ?? undefined,
1583
+ purposeData: row.purposeData ?? undefined,
1544
1584
  ccSessionId: row.ccSessionId ?? undefined,
1545
1585
  transcriptPath: row.transcriptPath ?? undefined,
1546
1586
  status: row.status,
@@ -1796,7 +1836,6 @@ var init_waypoints = __esm(() => {
1796
1836
  });
1797
1837
 
1798
1838
  // src/pty/dev-pane.ts
1799
- import { execFileSync } from "child_process";
1800
1839
  function drivePaneId(ownerId) {
1801
1840
  return `${DRIVE_PREFIX}${ownerId}`;
1802
1841
  }
@@ -1856,24 +1895,24 @@ function devPaneLive(paneId) {
1856
1895
  const entry = ptyRegistry().get(paneId);
1857
1896
  return !!entry && !entry.exited;
1858
1897
  }
1859
- function killProcessTree(pid) {
1860
- try {
1861
- if (process.platform === "win32") {
1862
- execFileSync("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
1863
- } else {
1864
- process.kill(-pid, "SIGTERM");
1865
- }
1866
- } catch {}
1898
+ function killTreeBounded(pty) {
1899
+ return new Promise((resolve4) => {
1900
+ const timer = setTimeout(resolve4, KILL_TREE_TIMEOUT_MS);
1901
+ pty.killTree().catch(() => {}).then(() => {
1902
+ clearTimeout(timer);
1903
+ resolve4();
1904
+ });
1905
+ });
1867
1906
  }
1868
- function stopDevPane(paneId) {
1907
+ async function stopDevPane(paneId) {
1869
1908
  const reg = ptyRegistry();
1870
1909
  const entry = reg.get(paneId);
1871
1910
  if (entry && !entry.exited)
1872
- killProcessTree(entry.pty.pid);
1911
+ await killTreeBounded(entry.pty);
1873
1912
  reg.kill(paneId);
1874
1913
  reg.remove(paneId);
1875
1914
  }
1876
- var DRIVE_PREFIX = "drive:", SNIFF_WINDOW = 8192;
1915
+ var DRIVE_PREFIX = "drive:", SNIFF_WINDOW = 8192, KILL_TREE_TIMEOUT_MS = 5000;
1877
1916
  var init_dev_pane = __esm(() => {
1878
1917
  init_events();
1879
1918
  init_registry();
@@ -2802,7 +2841,7 @@ async function testDrive(ctx, project, feature, action) {
2802
2841
  const detachedWorktree2 = testDriveState.detachedWorktree;
2803
2842
  const devPaneId = testDriveState.devPaneId;
2804
2843
  if (devPaneId)
2805
- stopDevPane(devPaneId);
2844
+ await stopDevPane(devPaneId);
2806
2845
  const teardown = await runDriveHookStep(ctx, scope, project.repoPath, "teardown", project.driveStopCommand, driveEnvFor(ctx, project, { slug: feature.slug, branch }, scope).env);
2807
2846
  const carriedChanges = await dirtyPaths(g);
2808
2847
  await g.checkout(previousBranch2);
@@ -2942,7 +2981,7 @@ async function startDryRun(ctx, project) {
2942
2981
  async function stopDryRun(ctx, project, state) {
2943
2982
  const scope = { projectId: project.id };
2944
2983
  if (state.devPaneId)
2945
- stopDevPane(state.devPaneId);
2984
+ await stopDevPane(state.devPaneId);
2946
2985
  const teardown = await runDriveHookStep(ctx, scope, project.repoPath, "teardown", project.driveStopCommand, state.env);
2947
2986
  state.observed.teardownOk = teardown?.result.ok;
2948
2987
  testDriveState = undefined;
@@ -3157,10 +3196,24 @@ async function conflictedFiles(g) {
3157
3196
  return [];
3158
3197
  }
3159
3198
  }
3199
+ async function mergeInProgressAt(worktreePath) {
3200
+ try {
3201
+ return await mergeInProgress(git(worktreePath));
3202
+ } catch {
3203
+ return false;
3204
+ }
3205
+ }
3206
+ async function isAncestor(repoPath, ancestor, descendant) {
3207
+ try {
3208
+ const out = await git(repoPath).raw(["rev-list", "--count", ancestor, `^${descendant}`]);
3209
+ return out.trim() === "0";
3210
+ } catch {
3211
+ return false;
3212
+ }
3213
+ }
3160
3214
  async function mergeInProgress(g) {
3161
3215
  try {
3162
- await g.raw(["rev-parse", "--verify", "--quiet", "MERGE_HEAD"]);
3163
- return true;
3216
+ return (await g.raw(["rev-parse", "--verify", "--quiet", "MERGE_HEAD"])).trim() !== "";
3164
3217
  } catch {
3165
3218
  return false;
3166
3219
  }
@@ -3190,6 +3243,8 @@ function createSessionRow(ctx, input) {
3190
3243
  projectId: input.projectId ?? null,
3191
3244
  lap: input.featureId ? getFeatureRow(ctx, input.featureId).lap : 1,
3192
3245
  kind: input.kind,
3246
+ purpose: input.purpose ?? null,
3247
+ purposeData: input.purposeData ?? null,
3193
3248
  ccSessionId: null,
3194
3249
  transcriptPath: null,
3195
3250
  status: "launching",
@@ -3437,7 +3492,7 @@ function landProjectSession(ctx, session) {
3437
3492
  data: { sessionId: session.id, branch: PROJECT_BRANCH, ...data }
3438
3493
  });
3439
3494
  };
3440
- landProjectBranch(project).then((res) => {
3495
+ const landing = landProjectBranch(project).then((res) => {
3441
3496
  if (!res)
3442
3497
  return;
3443
3498
  if (res.landed) {
@@ -3450,6 +3505,8 @@ function landProjectSession(ctx, session) {
3450
3505
  }).catch((e) => {
3451
3506
  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) });
3452
3507
  });
3508
+ inFlightLandings.add(landing);
3509
+ landing.finally(() => inFlightLandings.delete(landing));
3453
3510
  }
3454
3511
  function markSessionEnded(ctx, id) {
3455
3512
  const existing = getSessionRow(ctx, id);
@@ -3478,7 +3535,7 @@ function mostRecentLiveSession(ctx) {
3478
3535
  const row = ctx.db.select().from(sessions).where(eq5(sessions.status, "live")).orderBy(desc3(sql2`rowid`)).limit(1).get();
3479
3536
  return row ? rowToSession(row) : null;
3480
3537
  }
3481
- 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;
3538
+ 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;
3482
3539
  var init_sessions = __esm(() => {
3483
3540
  init_src();
3484
3541
  init_schema();
@@ -3501,6 +3558,7 @@ var init_sessions = __esm(() => {
3501
3558
  pendingKickoffOverrides = new Map;
3502
3559
  deliveries = new Map;
3503
3560
  landedProjectSessions = new Set;
3561
+ inFlightLandings = new Set;
3504
3562
  });
3505
3563
 
3506
3564
  // src/launcher/reconcile.ts
@@ -6631,6 +6689,8 @@ function guardsEdits(kind) {
6631
6689
  function evaluateEditGuard(input) {
6632
6690
  if (!guardsEdits(input.kind))
6633
6691
  return null;
6692
+ if (input.purpose === "resolve-conflict" && input.mergeInProgress)
6693
+ return null;
6634
6694
  if (!input.toolName || !EDIT_TOOLS.includes(input.toolName))
6635
6695
  return null;
6636
6696
  if (!input.filePath)
@@ -6807,22 +6867,40 @@ function handleUserPrompt(ctx, sessionId, feature, payload) {
6807
6867
  }
6808
6868
  };
6809
6869
  }
6810
- function handleSessionEnd(ctx, sessionId, feature) {
6811
- markSessionEnded(ctx, sessionId);
6812
- releaseForSession(ctx, sessionId);
6870
+ async function handleSessionEnd(ctx, session, feature) {
6871
+ markSessionEnded(ctx, session.id);
6872
+ releaseForSession(ctx, session.id);
6813
6873
  emit(ctx, feature.id, {
6814
6874
  type: "session.ended",
6815
6875
  message: "session ended",
6816
- data: { sessionId }
6876
+ data: { sessionId: session.id }
6817
6877
  });
6878
+ await noteResolvedMerge(ctx, session, feature);
6818
6879
  return {};
6819
6880
  }
6820
- function handlePreToolUse(session, feature, payload) {
6881
+ async function noteResolvedMerge(ctx, session, feature) {
6882
+ const pair = session.purpose === "resolve-conflict" ? session.purposeData : undefined;
6883
+ if (!pair)
6884
+ return;
6885
+ try {
6886
+ if (!await isAncestor(session.worktreePath, pair.mergeFrom, pair.mergeInto))
6887
+ return;
6888
+ emit(ctx, feature.id, {
6889
+ type: "merge.resolved",
6890
+ message: `merge conflict resolved \u2014 ${pair.mergeFrom} is in ${pair.mergeInto}`,
6891
+ data: { sessionId: session.id, ...pair }
6892
+ });
6893
+ } catch {}
6894
+ }
6895
+ async function handlePreToolUse(session, feature, payload) {
6821
6896
  const toolName = typeof payload?.tool_name === "string" ? payload.tool_name : undefined;
6822
6897
  const toolInput = payload?.tool_input ?? {};
6823
6898
  const path = typeof toolInput.file_path === "string" ? toolInput.file_path : typeof toolInput.notebook_path === "string" ? toolInput.notebook_path : undefined;
6899
+ const mergeInProgress2 = session.purpose === "resolve-conflict" ? await mergeInProgressAt(session.worktreePath) : false;
6824
6900
  const denial = evaluateEditGuard({
6825
6901
  kind: session.kind,
6902
+ purpose: session.purpose,
6903
+ mergeInProgress: mergeInProgress2,
6826
6904
  worktreePath: session.worktreePath,
6827
6905
  toolName,
6828
6906
  filePath: path,
@@ -6851,6 +6929,7 @@ var init_hooks = __esm(() => {
6851
6929
  init_edit_guard();
6852
6930
  init_sessions();
6853
6931
  init_events();
6932
+ init_git();
6854
6933
  init_prep();
6855
6934
  init_repo();
6856
6935
  init_tickets();
@@ -6888,7 +6967,7 @@ var init_hooks = __esm(() => {
6888
6967
  case "session-end":
6889
6968
  return c.json(handleProjectScopedSessionEnd(ctx, session));
6890
6969
  case "pre-tool":
6891
- return c.json(handlePreToolUse(session, undefined, body.payload));
6970
+ return c.json(await handlePreToolUse(session, undefined, body.payload));
6892
6971
  default:
6893
6972
  return c.json({});
6894
6973
  }
@@ -6902,9 +6981,9 @@ var init_hooks = __esm(() => {
6902
6981
  case "user-prompt":
6903
6982
  return c.json(handleUserPrompt(ctx, sessionId, feature, body.payload));
6904
6983
  case "session-end":
6905
- return c.json(handleSessionEnd(ctx, sessionId, feature));
6984
+ return c.json(await handleSessionEnd(ctx, session, feature));
6906
6985
  case "pre-tool":
6907
- return c.json(handlePreToolUse(session, feature, body.payload));
6986
+ return c.json(await handlePreToolUse(session, feature, body.payload));
6908
6987
  default:
6909
6988
  return c.json({});
6910
6989
  }
@@ -7046,6 +7125,166 @@ var init_web = __esm(() => {
7046
7125
  };
7047
7126
  });
7048
7127
 
7128
+ // src/version.ts
7129
+ import { readFileSync as readFileSync6 } from "fs";
7130
+ import { dirname as dirname5, join as join12 } from "path";
7131
+ import { fileURLToPath as fileURLToPath4 } from "url";
7132
+ function runcastleVersion() {
7133
+ if (cached2)
7134
+ return cached2;
7135
+ const here = dirname5(fileURLToPath4(import.meta.url));
7136
+ for (const pkgPath of [join12(here, "..", "package.json"), join12(here, "package.json")]) {
7137
+ try {
7138
+ const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
7139
+ if (typeof pkg.version === "string") {
7140
+ cached2 = pkg.version;
7141
+ return cached2;
7142
+ }
7143
+ } catch {}
7144
+ }
7145
+ cached2 = UNKNOWN_VERSION;
7146
+ return cached2;
7147
+ }
7148
+ var cached2, UNKNOWN_VERSION = "0.0.0";
7149
+ var init_version = () => {};
7150
+
7151
+ // src/services/install-id.ts
7152
+ import { randomUUID } from "crypto";
7153
+ import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
7154
+ import { dirname as dirname6 } from "path";
7155
+ function getInstallId() {
7156
+ const file = installIdPath();
7157
+ try {
7158
+ if (existsSync13(file)) {
7159
+ const existing = readFileSync7(file, "utf8").trim();
7160
+ if (UUID_RE.test(existing))
7161
+ return existing;
7162
+ }
7163
+ const id = randomUUID();
7164
+ mkdirSync6(dirname6(file), { recursive: true });
7165
+ writeFileSync2(file, `${id}
7166
+ `);
7167
+ return id;
7168
+ } catch {
7169
+ return randomUUID();
7170
+ }
7171
+ }
7172
+ var UUID_RE;
7173
+ var init_install_id = __esm(() => {
7174
+ init_paths();
7175
+ UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
7176
+ });
7177
+
7178
+ // src/services/update-check.ts
7179
+ function parse(version) {
7180
+ const cleaned = version.trim().replace(/^v/, "");
7181
+ const [core = "", pre = ""] = cleaned.split("-", 2);
7182
+ const release2 = core.split(".").map((n) => Number.parseInt(n, 10) || 0);
7183
+ return { release: release2, pre: pre ? pre.split(".") : [] };
7184
+ }
7185
+ function compareSemver(a, b) {
7186
+ const pa = parse(a);
7187
+ const pb = parse(b);
7188
+ const len = Math.max(pa.release.length, pb.release.length);
7189
+ for (let i = 0;i < len; i++) {
7190
+ const diff = (pa.release[i] ?? 0) - (pb.release[i] ?? 0);
7191
+ if (diff !== 0)
7192
+ return diff;
7193
+ }
7194
+ if (pa.pre.length === 0 && pb.pre.length === 0)
7195
+ return 0;
7196
+ if (pa.pre.length === 0)
7197
+ return 1;
7198
+ if (pb.pre.length === 0)
7199
+ return -1;
7200
+ const preLen = Math.max(pa.pre.length, pb.pre.length);
7201
+ for (let i = 0;i < preLen; i++) {
7202
+ const x = pa.pre[i];
7203
+ const y = pb.pre[i];
7204
+ if (x === undefined)
7205
+ return -1;
7206
+ if (y === undefined)
7207
+ return 1;
7208
+ const nx = Number(x);
7209
+ const ny = Number(y);
7210
+ const bothNumeric = !Number.isNaN(nx) && !Number.isNaN(ny);
7211
+ const cmp = bothNumeric ? nx - ny : x < y ? -1 : x > y ? 1 : 0;
7212
+ if (cmp !== 0)
7213
+ return cmp;
7214
+ }
7215
+ return 0;
7216
+ }
7217
+ function doNotTrack(env) {
7218
+ const raw = env.DO_NOT_TRACK;
7219
+ return raw !== undefined && raw !== "" && raw !== "0";
7220
+ }
7221
+ async function latestFromNpm(fetchImpl) {
7222
+ try {
7223
+ const res = await fetchImpl(NPM_LATEST_URL);
7224
+ if (!res.ok)
7225
+ return null;
7226
+ const body = await res.json();
7227
+ return typeof body.version === "string" ? body.version : null;
7228
+ } catch {
7229
+ return null;
7230
+ }
7231
+ }
7232
+ async function latestFromPing(fetchImpl, current2) {
7233
+ try {
7234
+ const res = await fetchImpl(PING_URL, {
7235
+ method: "POST",
7236
+ headers: { "content-type": "application/json" },
7237
+ body: JSON.stringify({
7238
+ installId: getInstallId(),
7239
+ version: current2,
7240
+ platform: process.platform
7241
+ })
7242
+ });
7243
+ if (!res.ok)
7244
+ return null;
7245
+ const body = await res.json();
7246
+ return typeof body.latest === "string" ? body.latest : null;
7247
+ } catch {
7248
+ return null;
7249
+ }
7250
+ }
7251
+ async function checkForUpdate(opts) {
7252
+ const { current: current2 } = opts;
7253
+ const fetchImpl = opts.fetchImpl ?? fetch;
7254
+ const env = opts.env ?? process.env;
7255
+ if (current2 === UNKNOWN_VERSION)
7256
+ return NO_UPDATE(current2);
7257
+ const latest = doNotTrack(env) ? await latestFromNpm(fetchImpl) : await latestFromPing(fetchImpl, current2) ?? await latestFromNpm(fetchImpl);
7258
+ if (!latest)
7259
+ return NO_UPDATE(current2);
7260
+ return {
7261
+ current: current2,
7262
+ latest,
7263
+ updateAvailable: compareSemver(latest, current2) > 0,
7264
+ command: UPDATE_COMMAND
7265
+ };
7266
+ }
7267
+ async function getUpdateInfo(current2, fetchImpl) {
7268
+ if (cache)
7269
+ return cache;
7270
+ const info = await checkForUpdate({ current: current2, fetchImpl });
7271
+ if (info.latest !== null)
7272
+ cache = info;
7273
+ return info;
7274
+ }
7275
+ var PACKAGE_NAME = "runcastle", UPDATE_COMMAND, PING_URL = "https://ping.runcastle.dev/ping", NPM_LATEST_URL, NO_UPDATE = (current2) => ({
7276
+ current: current2,
7277
+ latest: null,
7278
+ updateAvailable: false,
7279
+ command: UPDATE_COMMAND
7280
+ }), cache;
7281
+ var init_update_check = __esm(() => {
7282
+ init_version();
7283
+ init_install_id();
7284
+ UPDATE_COMMAND = `bun add -g ${PACKAGE_NAME}@latest`;
7285
+ NPM_LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
7286
+ });
7287
+
7049
7288
  // src/trpc/context.ts
7050
7289
  import { initTRPC } from "@trpc/server";
7051
7290
  var t, errorMapping, router, publicProcedure, createCallerFactory;
@@ -7094,11 +7333,11 @@ var init_events2 = __esm(() => {
7094
7333
  });
7095
7334
 
7096
7335
  // src/launcher/artifacts.ts
7097
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync2 } from "fs";
7098
- import { fileURLToPath as fileURLToPath4 } from "url";
7099
- import { join as join12 } from "path";
7336
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync3 } from "fs";
7337
+ import { fileURLToPath as fileURLToPath5 } from "url";
7338
+ import { join as join13 } from "path";
7100
7339
  function hookClientPath() {
7101
- return resolveAsset(ASSET_ENV.hookClient, fileURLToPath4(new URL("./hook-client.ts", import.meta.url)));
7340
+ return resolveAsset(ASSET_ENV.hookClient, fileURLToPath5(new URL("./hook-client.ts", import.meta.url)));
7102
7341
  }
7103
7342
  function serverUrlFor(config2) {
7104
7343
  return `http://localhost:${config2.serverPort}`;
@@ -7520,16 +7759,16 @@ function renderMcpConfig(session, config2) {
7520
7759
  async function writeSessionArtifacts(input) {
7521
7760
  const { session, feature, config: config2, waypoint, prepare, projectBrief, lap } = input;
7522
7761
  const dir = sessionDir(session.id);
7523
- mkdirSync6(dir, { recursive: true });
7524
- const systemPromptPath = join12(dir, "system-prompt.md");
7525
- const settingsPath = join12(dir, "settings.json");
7526
- const mcpConfigPath = join12(dir, "mcp.json");
7762
+ mkdirSync7(dir, { recursive: true });
7763
+ const systemPromptPath = join13(dir, "system-prompt.md");
7764
+ const settingsPath = join13(dir, "settings.json");
7765
+ const mcpConfigPath = join13(dir, "mcp.json");
7527
7766
  const systemPrompt = feature ? renderSystemPrompt(feature, session.kind, waypoint, lap) : prepare ? renderPreparePrompt(prepare) : projectBrief ? renderProjectPrompt(projectBrief) : (() => {
7528
7767
  throw new Error(`session ${session.id} has no feature and no project-session brief`);
7529
7768
  })();
7530
- writeFileSync2(systemPromptPath, systemPrompt, "utf8");
7531
- writeFileSync2(settingsPath, JSON.stringify(renderSettings(hookClientPath(), session.kind), null, 2), "utf8");
7532
- writeFileSync2(mcpConfigPath, JSON.stringify(renderMcpConfig(session, config2), null, 2), "utf8");
7769
+ writeFileSync3(systemPromptPath, systemPrompt, "utf8");
7770
+ writeFileSync3(settingsPath, JSON.stringify(renderSettings(hookClientPath(), session.kind), null, 2), "utf8");
7771
+ writeFileSync3(mcpConfigPath, JSON.stringify(renderMcpConfig(session, config2), null, 2), "utf8");
7533
7772
  return { systemPromptPath, settingsPath, mcpConfigPath };
7534
7773
  }
7535
7774
  var RUNCASTLE_MCP_ALLOW_RULES, SESSION_BASH_READ_RULES, SESSION_BASH_WRITE_RULES, SESSION_BASH_ALLOW_RULES, SESSION_START_SOURCES;
@@ -7573,9 +7812,9 @@ var init_artifacts = __esm(() => {
7573
7812
  });
7574
7813
 
7575
7814
  // src/launcher/launcher.ts
7576
- import { existsSync as existsSync13 } from "fs";
7577
- import { dirname as dirname5, join as join13, resolve as resolve9 } from "path";
7578
- import { fileURLToPath as fileURLToPath5 } from "url";
7815
+ import { existsSync as existsSync14 } from "fs";
7816
+ import { dirname as dirname7, join as join14, resolve as resolve9 } from "path";
7817
+ import { fileURLToPath as fileURLToPath6 } from "url";
7579
7818
  import { and as and9, eq as eq11 } from "drizzle-orm";
7580
7819
  function ptyExitMessage(exitCode) {
7581
7820
  const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
@@ -7607,23 +7846,23 @@ function resolveClaudeExecutable() {
7607
7846
  function claudeSpawnTarget(claudeArgs) {
7608
7847
  return spawnTargetFor(resolveClaudeExecutable(), claudeArgs);
7609
7848
  }
7610
- function resolvePluginDir(fromDir = dirname5(fileURLToPath5(import.meta.url))) {
7611
- const rel = join13("packages", "skills", "packs", "runcastle");
7849
+ function resolvePluginDir(fromDir = dirname7(fileURLToPath6(import.meta.url))) {
7850
+ const rel = join14("packages", "skills", "packs", "runcastle");
7612
7851
  const override = process.env[SKILLS_DIR_ENV];
7613
7852
  if (override) {
7614
- const dir2 = join13(resolve9(override), "packs", "runcastle");
7615
- if (existsSync13(dir2))
7853
+ const dir2 = join14(resolve9(override), "packs", "runcastle");
7854
+ if (existsSync14(dir2))
7616
7855
  return dir2;
7617
7856
  throw new Error(`${SKILLS_DIR_ENV}=${override} has no plugin dir at ${dir2}`);
7618
7857
  }
7619
7858
  const searched = [];
7620
7859
  let dir = fromDir;
7621
7860
  for (let i = 0;i < 8; i += 1) {
7622
- const candidate = join13(dir, rel);
7861
+ const candidate = join14(dir, rel);
7623
7862
  searched.push(candidate);
7624
- if (existsSync13(candidate))
7863
+ if (existsSync14(candidate))
7625
7864
  return candidate;
7626
- const parent = dirname5(dir);
7865
+ const parent = dirname7(dir);
7627
7866
  if (parent === dir)
7628
7867
  break;
7629
7868
  dir = parent;
@@ -7692,6 +7931,8 @@ async function launchSession(ctx, input, opts = {}) {
7692
7931
  const session = createSessionRow(ctx, {
7693
7932
  featureId: feature.id,
7694
7933
  kind: input.kind,
7934
+ purpose: input.purpose,
7935
+ purposeData: input.purposeData,
7695
7936
  worktreePath
7696
7937
  });
7697
7938
  const plan = planKickoff({ kind: input.kind, lap: feature.lap, kickoffLine: input.kickoffLine });
@@ -8139,7 +8380,9 @@ var init_feature = __esm(() => {
8139
8380
  launchSession: publicProcedure.input(z6.object({
8140
8381
  featureId: z6.string(),
8141
8382
  kind: SessionKind,
8142
- kickoffLine: z6.string().min(1).optional()
8383
+ kickoffLine: z6.string().min(1).optional(),
8384
+ purpose: SessionPurpose.optional(),
8385
+ purposeData: MergeBranchPair.optional()
8143
8386
  })).mutation(({ ctx, input }) => launchSession(ctx, input)),
8144
8387
  workWaypoint: publicProcedure.input(z6.object({ featureId: z6.string(), waypointId: z6.string(), endLive: z6.boolean().optional() })).mutation(({ ctx, input }) => workWaypoint(ctx, input)),
8145
8388
  converge: publicProcedure.input(z6.object({ featureId: z6.string(), overrideReason: z6.string().min(1).optional() })).mutation(({ ctx, input }) => converge(ctx, { featureId: input.featureId, overrideReason: input.overrideReason })),
@@ -8190,9 +8433,9 @@ var init_feature = __esm(() => {
8190
8433
  });
8191
8434
 
8192
8435
  // src/services/fsbrowse.ts
8193
- import { existsSync as existsSync14, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
8436
+ import { existsSync as existsSync15, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
8194
8437
  import { homedir as homedir2 } from "os";
8195
- import { basename, dirname as dirname6, isAbsolute as isAbsolute2, join as join14, resolve as resolve10, sep as sep3 } from "path";
8438
+ import { basename, dirname as dirname8, isAbsolute as isAbsolute2, join as join15, resolve as resolve10, sep as sep3 } from "path";
8196
8439
  function expandPath(input, home = homedir2()) {
8197
8440
  const trimmed = input.trim();
8198
8441
  if (trimmed === "")
@@ -8201,12 +8444,12 @@ function expandPath(input, home = homedir2()) {
8201
8444
  if (out === "~")
8202
8445
  out = home;
8203
8446
  else if (out.startsWith("~/") || out.startsWith("~\\"))
8204
- out = join14(home, out.slice(2));
8447
+ out = join15(home, out.slice(2));
8205
8448
  return resolve10(out);
8206
8449
  }
8207
8450
  function looksLikeRepo(dir) {
8208
8451
  try {
8209
- return existsSync14(join14(dir, ".git"));
8452
+ return existsSync15(join15(dir, ".git"));
8210
8453
  } catch {
8211
8454
  return false;
8212
8455
  }
@@ -8217,20 +8460,20 @@ function listRoots(home = homedir2()) {
8217
8460
  for (let code = 67;code <= 90; code++) {
8218
8461
  const drive = `${String.fromCharCode(code)}:${sep3}`;
8219
8462
  try {
8220
- if (existsSync14(drive))
8463
+ if (existsSync15(drive))
8221
8464
  roots.push({ label: drive.replace(/\\$/, ""), path: drive, kind: "drive" });
8222
8465
  } catch {}
8223
8466
  }
8224
8467
  } else {
8225
8468
  roots.push({ label: "/", path: "/", kind: "drive" });
8226
8469
  }
8227
- if (existsSync14(home))
8470
+ if (existsSync15(home))
8228
8471
  roots.push({ label: "Home", path: home, kind: "home" });
8229
8472
  const seen = new Set(roots.map(pathKey));
8230
8473
  for (const name of COMMON_DIRS) {
8231
- const candidate = join14(home, name);
8474
+ const candidate = join15(home, name);
8232
8475
  const key = pathKey({ path: candidate });
8233
- if (seen.has(key) || !existsSync14(candidate))
8476
+ if (seen.has(key) || !existsSync15(candidate))
8234
8477
  continue;
8235
8478
  seen.add(key);
8236
8479
  roots.push({ label: name, path: candidate, kind: "common" });
@@ -8245,7 +8488,7 @@ function crumbsFor(dir) {
8245
8488
  let cur = dir;
8246
8489
  for (;; ) {
8247
8490
  out.unshift({ name: basename(cur) || cur, path: cur });
8248
- const parent = dirname6(cur);
8491
+ const parent = dirname8(cur);
8249
8492
  if (parent === cur)
8250
8493
  break;
8251
8494
  cur = parent;
@@ -8253,7 +8496,7 @@ function crumbsFor(dir) {
8253
8496
  return out;
8254
8497
  }
8255
8498
  function parentOf(dir) {
8256
- const parent = dirname6(dir);
8499
+ const parent = dirname8(dir);
8257
8500
  return parent === dir ? null : parent;
8258
8501
  }
8259
8502
  function browseDir(input, showHidden = false, home = homedir2()) {
@@ -8261,13 +8504,13 @@ function browseDir(input, showHidden = false, home = homedir2()) {
8261
8504
  if (!isAbsolute2(target)) {
8262
8505
  throw new InvalidInputError(`path is not absolute: ${target}`);
8263
8506
  }
8264
- if (!existsSync14(target)) {
8507
+ if (!existsSync15(target)) {
8265
8508
  throw new InvalidInputError(`path does not exist: ${target}`);
8266
8509
  }
8267
8510
  let dir = target;
8268
8511
  try {
8269
8512
  if (!statSync2(target).isDirectory())
8270
- dir = dirname6(target);
8513
+ dir = dirname8(target);
8271
8514
  } catch (e) {
8272
8515
  throw new InvalidInputError(`cannot read path: ${target} (${errMsg4(e)})`);
8273
8516
  }
@@ -8289,7 +8532,7 @@ function browseDir(input, showHidden = false, home = homedir2()) {
8289
8532
  continue;
8290
8533
  if (!showHidden && name.startsWith("."))
8291
8534
  continue;
8292
- const path = join14(dir, name);
8535
+ const path = join15(dir, name);
8293
8536
  const isSymlink = dirent.isSymbolicLink();
8294
8537
  if (isSymlink && !showHidden)
8295
8538
  continue;
@@ -8326,8 +8569,8 @@ var init_fsbrowse = __esm(() => {
8326
8569
  });
8327
8570
 
8328
8571
  // src/services/projects.ts
8329
- import { existsSync as existsSync15 } from "fs";
8330
- import { basename as basename2, join as join15 } from "path";
8572
+ import { existsSync as existsSync16 } from "fs";
8573
+ import { basename as basename2, join as join16 } from "path";
8331
8574
  import { eq as eq12, isNull } from "drizzle-orm";
8332
8575
  function listProjects(ctx) {
8333
8576
  return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
@@ -8408,7 +8651,7 @@ async function assertRepoTolerant(ctx, repoPath) {
8408
8651
  } catch (e) {
8409
8652
  if (!isNotImplemented(e))
8410
8653
  throw e;
8411
- if (!existsSync15(join15(repoPath, ".git"))) {
8654
+ if (!existsSync16(join16(repoPath, ".git"))) {
8412
8655
  throw new InvalidInputError(`not a git repository: ${repoPath}`);
8413
8656
  }
8414
8657
  }
@@ -8481,15 +8724,15 @@ var init_run = __esm(() => {
8481
8724
  });
8482
8725
 
8483
8726
  // src/services/settings.ts
8484
- import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
8485
- import { dirname as dirname7 } from "path";
8727
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
8728
+ import { dirname as dirname9 } from "path";
8486
8729
  import * as z9 from "zod";
8487
8730
  import { eq as eq13 } from "drizzle-orm";
8488
8731
  function readRawConfig(configFile) {
8489
- if (!existsSync16(configFile))
8732
+ if (!existsSync17(configFile))
8490
8733
  return {};
8491
8734
  try {
8492
- const parsed = JSON.parse(readFileSync6(configFile, "utf8"));
8735
+ const parsed = JSON.parse(readFileSync8(configFile, "utf8"));
8493
8736
  if (typeof parsed !== "object" || parsed === null)
8494
8737
  return {};
8495
8738
  return foldLegacyModelConfig(parsed);
@@ -8628,8 +8871,8 @@ function updateSettings(ctx, input, io = {}) {
8628
8871
  function writeGlobal(configFile, configKey, value) {
8629
8872
  const raw = readRawConfig(configFile);
8630
8873
  raw[configKey] = value;
8631
- mkdirSync7(dirname7(configFile), { recursive: true });
8632
- writeFileSync3(configFile, `${JSON.stringify(raw, null, 2)}
8874
+ mkdirSync8(dirname9(configFile), { recursive: true });
8875
+ writeFileSync4(configFile, `${JSON.stringify(raw, null, 2)}
8633
8876
  `);
8634
8877
  }
8635
8878
  function updateStepModel(ctx, input, configFile, io) {
@@ -8667,8 +8910,8 @@ function writeStepModel(configFile, step, value) {
8667
8910
  else
8668
8911
  stepModels[step] = value;
8669
8912
  raw.stepModels = stepModels;
8670
- mkdirSync7(dirname7(configFile), { recursive: true });
8671
- writeFileSync3(configFile, `${JSON.stringify(raw, null, 2)}
8913
+ mkdirSync8(dirname9(configFile), { recursive: true });
8914
+ writeFileSync4(configFile, `${JSON.stringify(raw, null, 2)}
8672
8915
  `);
8673
8916
  }
8674
8917
  function field(view, key) {
@@ -9045,9 +9288,9 @@ var init_system_exec = __esm(() => {
9045
9288
  });
9046
9289
 
9047
9290
  // src/services/setup.ts
9048
- import { copyFileSync, existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync7, readdirSync as readdirSync6, writeFileSync as writeFileSync4 } from "fs";
9049
- import { dirname as dirname8, join as join16 } from "path";
9050
- import { fileURLToPath as fileURLToPath6 } from "url";
9291
+ import { copyFileSync, existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync9, readdirSync as readdirSync6, writeFileSync as writeFileSync5 } from "fs";
9292
+ import { dirname as dirname10, join as join17 } from "path";
9293
+ import { fileURLToPath as fileURLToPath7 } from "url";
9051
9294
  async function writeGitIdentity(exec, identity) {
9052
9295
  const name = identity.name.trim();
9053
9296
  const email = identity.email.trim();
@@ -9122,16 +9365,16 @@ async function resolveRuntime(exec, preferred) {
9122
9365
  function resolveSandcastleBin() {
9123
9366
  try {
9124
9367
  const mainUrl = import.meta.resolve("@ai-hero/sandcastle");
9125
- let dir = dirname8(fileURLToPath6(mainUrl));
9368
+ let dir = dirname10(fileURLToPath7(mainUrl));
9126
9369
  for (let hops = 0;hops < 6; hops++) {
9127
- const manifestPath = join16(dir, "package.json");
9128
- if (existsSync17(manifestPath)) {
9129
- const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
9370
+ const manifestPath = join17(dir, "package.json");
9371
+ if (existsSync18(manifestPath)) {
9372
+ const manifest = JSON.parse(readFileSync9(manifestPath, "utf8"));
9130
9373
  if (manifest.name === "@ai-hero/sandcastle" && manifest.bin?.sandcastle) {
9131
- return join16(dir, manifest.bin.sandcastle);
9374
+ return join17(dir, manifest.bin.sandcastle);
9132
9375
  }
9133
9376
  }
9134
- const parent = dirname8(dir);
9377
+ const parent = dirname10(dir);
9135
9378
  if (parent === dir)
9136
9379
  break;
9137
9380
  dir = parent;
@@ -9153,21 +9396,21 @@ function terminalSpec(kind, opts) {
9153
9396
  };
9154
9397
  }
9155
9398
  function sandcastleTemplateDir() {
9156
- return resolveAsset(ASSET_ENV.sandcastleTemplate, fileURLToPath6(new URL("../assets/sandcastle", import.meta.url)));
9399
+ return resolveAsset(ASSET_ENV.sandcastleTemplate, fileURLToPath7(new URL("../assets/sandcastle", import.meta.url)));
9157
9400
  }
9158
9401
  function scaffoldSandcastleConfig(templateDir, targetDir) {
9159
- const dir = join16(targetDir, ".sandcastle");
9160
- if (existsSync17(dir))
9402
+ const dir = join17(targetDir, ".sandcastle");
9403
+ if (existsSync18(dir))
9161
9404
  return { scaffolded: false, dir };
9162
- mkdirSync8(dir, { recursive: true });
9405
+ mkdirSync9(dir, { recursive: true });
9163
9406
  for (const name of readdirSync6(templateDir)) {
9164
- copyFileSync(join16(templateDir, name), join16(dir, name));
9407
+ copyFileSync(join17(templateDir, name), join17(dir, name));
9165
9408
  }
9166
9409
  return { scaffolded: true, dir };
9167
9410
  }
9168
9411
  function prepareSandboxBuildContext() {
9169
9412
  const target = sandboxBuildDir();
9170
- mkdirSync8(target, { recursive: true });
9413
+ mkdirSync9(target, { recursive: true });
9171
9414
  scaffoldSandcastleConfig(sandcastleTemplateDir(), target);
9172
9415
  return target;
9173
9416
  }
@@ -9203,10 +9446,10 @@ function createTokenVerifier(exec) {
9203
9446
  function fileAfkTokenIo(exec) {
9204
9447
  const path = envPath();
9205
9448
  return {
9206
- read: () => existsSync17(path) ? readFileSync7(path, "utf8") : "",
9449
+ read: () => existsSync18(path) ? readFileSync9(path, "utf8") : "",
9207
9450
  write: (content) => {
9208
- mkdirSync8(dirname8(path), { recursive: true });
9209
- writeFileSync4(path, content, "utf8");
9451
+ mkdirSync9(dirname10(path), { recursive: true });
9452
+ writeFileSync5(path, content, "utf8");
9210
9453
  },
9211
9454
  verify: createTokenVerifier(exec)
9212
9455
  };
@@ -9259,110 +9502,6 @@ var init_setup2 = __esm(() => {
9259
9502
  });
9260
9503
  });
9261
9504
 
9262
- // src/version.ts
9263
- import { readFileSync as readFileSync8 } from "fs";
9264
- import { dirname as dirname9, join as join17 } from "path";
9265
- import { fileURLToPath as fileURLToPath7 } from "url";
9266
- function runcastleVersion() {
9267
- if (cached2)
9268
- return cached2;
9269
- const here = dirname9(fileURLToPath7(import.meta.url));
9270
- for (const pkgPath of [join17(here, "..", "package.json"), join17(here, "package.json")]) {
9271
- try {
9272
- const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
9273
- if (typeof pkg.version === "string") {
9274
- cached2 = pkg.version;
9275
- return cached2;
9276
- }
9277
- } catch {}
9278
- }
9279
- cached2 = UNKNOWN_VERSION;
9280
- return cached2;
9281
- }
9282
- var cached2, UNKNOWN_VERSION = "0.0.0";
9283
- var init_version = () => {};
9284
-
9285
- // src/services/update-check.ts
9286
- function parse(version) {
9287
- const cleaned = version.trim().replace(/^v/, "");
9288
- const [core = "", pre = ""] = cleaned.split("-", 2);
9289
- const release2 = core.split(".").map((n) => Number.parseInt(n, 10) || 0);
9290
- return { release: release2, pre: pre ? pre.split(".") : [] };
9291
- }
9292
- function compareSemver(a, b) {
9293
- const pa = parse(a);
9294
- const pb = parse(b);
9295
- const len = Math.max(pa.release.length, pb.release.length);
9296
- for (let i = 0;i < len; i++) {
9297
- const diff = (pa.release[i] ?? 0) - (pb.release[i] ?? 0);
9298
- if (diff !== 0)
9299
- return diff;
9300
- }
9301
- if (pa.pre.length === 0 && pb.pre.length === 0)
9302
- return 0;
9303
- if (pa.pre.length === 0)
9304
- return 1;
9305
- if (pb.pre.length === 0)
9306
- return -1;
9307
- const preLen = Math.max(pa.pre.length, pb.pre.length);
9308
- for (let i = 0;i < preLen; i++) {
9309
- const x = pa.pre[i];
9310
- const y = pb.pre[i];
9311
- if (x === undefined)
9312
- return -1;
9313
- if (y === undefined)
9314
- return 1;
9315
- const nx = Number(x);
9316
- const ny = Number(y);
9317
- const bothNumeric = !Number.isNaN(nx) && !Number.isNaN(ny);
9318
- const cmp = bothNumeric ? nx - ny : x < y ? -1 : x > y ? 1 : 0;
9319
- if (cmp !== 0)
9320
- return cmp;
9321
- }
9322
- return 0;
9323
- }
9324
- async function checkForUpdate(opts) {
9325
- const { current: current2 } = opts;
9326
- const fetchImpl = opts.fetchImpl ?? fetch;
9327
- if (current2 === UNKNOWN_VERSION)
9328
- return NO_UPDATE(current2);
9329
- try {
9330
- const res = await fetchImpl(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`);
9331
- if (!res.ok)
9332
- return NO_UPDATE(current2);
9333
- const body = await res.json();
9334
- const latest = typeof body.version === "string" ? body.version : null;
9335
- if (!latest)
9336
- return NO_UPDATE(current2);
9337
- return {
9338
- current: current2,
9339
- latest,
9340
- updateAvailable: compareSemver(latest, current2) > 0,
9341
- command: UPDATE_COMMAND
9342
- };
9343
- } catch {
9344
- return NO_UPDATE(current2);
9345
- }
9346
- }
9347
- async function getUpdateInfo(current2, fetchImpl) {
9348
- if (cache)
9349
- return cache;
9350
- const info = await checkForUpdate({ current: current2, fetchImpl });
9351
- if (info.latest !== null)
9352
- cache = info;
9353
- return info;
9354
- }
9355
- var PACKAGE_NAME = "runcastle", UPDATE_COMMAND, NO_UPDATE = (current2) => ({
9356
- current: current2,
9357
- latest: null,
9358
- updateAvailable: false,
9359
- command: UPDATE_COMMAND
9360
- }), cache;
9361
- var init_update_check = __esm(() => {
9362
- init_version();
9363
- UPDATE_COMMAND = `bun add -g ${PACKAGE_NAME}@latest`;
9364
- });
9365
-
9366
9505
  // src/trpc/routers/system.ts
9367
9506
  var systemRouter;
9368
9507
  var init_system = __esm(() => {
@@ -9376,8 +9515,8 @@ var init_system = __esm(() => {
9376
9515
  });
9377
9516
 
9378
9517
  // src/services/test-notes.ts
9379
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync5 } from "fs";
9380
- import { dirname as dirname10 } from "path";
9518
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync6 } from "fs";
9519
+ import { dirname as dirname11 } from "path";
9381
9520
  import { asc as asc4, eq as eq14 } from "drizzle-orm";
9382
9521
  function rowToNote(row) {
9383
9522
  return {
@@ -9522,8 +9661,8 @@ function renderTestNotes(ctx, feature) {
9522
9661
  lines.push(noteLine(ctx, note));
9523
9662
  }
9524
9663
  const path = featureDocPath(projectForFeature(ctx, feature), feature, "test-notes.md");
9525
- mkdirSync9(dirname10(path), { recursive: true });
9526
- writeFileSync5(path, `${lines.join(`
9664
+ mkdirSync10(dirname11(path), { recursive: true });
9665
+ writeFileSync6(path, `${lines.join(`
9527
9666
  `)}
9528
9667
  `, "utf8");
9529
9668
  }
@@ -9722,6 +9861,7 @@ async function startServer() {
9722
9861
  websocket: terminalWebSocket
9723
9862
  });
9724
9863
  console.log(`runcastle${process.env.RUNCASTLE_DEV ? " [dev]" : ""} server listening on ` + `http://localhost:${config2.serverPort} \u2014 data dir: ${dataDir()}`);
9864
+ getUpdateInfo(runcastleVersion()).catch(() => {});
9725
9865
  const SHUTDOWN_KEY = Symbol.for("runcastle.shutdown.wired");
9726
9866
  const g = globalThis;
9727
9867
  if (!g[SHUTDOWN_KEY]) {
@@ -9747,7 +9887,9 @@ var init_src2 = __esm(() => {
9747
9887
  init_hooks();
9748
9888
  init_stream();
9749
9889
  init_web();
9890
+ init_update_check();
9750
9891
  init_router();
9892
+ init_version();
9751
9893
  init_reconcile_runs();
9752
9894
  });
9753
9895
 
@@ -9805,7 +9947,7 @@ __export(exports_cli, {
9805
9947
  resolveDoctorEnv: () => resolveDoctorEnv,
9806
9948
  parseMode: () => parseMode
9807
9949
  });
9808
- import { existsSync as existsSync18, readFileSync as readFileSync9 } from "fs";
9950
+ import { existsSync as existsSync19, readFileSync as readFileSync10 } from "fs";
9809
9951
  function parseMode(argv) {
9810
9952
  return argv.includes("--gate") || argv.includes("--boot") ? "gate" : "diagnostic";
9811
9953
  }
@@ -9813,8 +9955,8 @@ function envWithToken() {
9813
9955
  const merged = { ...process.env };
9814
9956
  try {
9815
9957
  const path = envPath();
9816
- if (existsSync18(path)) {
9817
- const fromFile = parseEnvFile(readFileSync9(path, "utf8")).CLAUDE_CODE_OAUTH_TOKEN;
9958
+ if (existsSync19(path)) {
9959
+ const fromFile = parseEnvFile(readFileSync10(path, "utf8")).CLAUDE_CODE_OAUTH_TOKEN;
9818
9960
  if (fromFile && fromFile.length > 0)
9819
9961
  merged.CLAUDE_CODE_OAUTH_TOKEN = fromFile;
9820
9962
  }
@@ -9856,7 +9998,7 @@ var init_cli = __esm(() => {
9856
9998
  // src/bin/runcastle.ts
9857
9999
  init_asset_paths();
9858
10000
  init_version();
9859
- import { dirname as dirname11, resolve as resolve11 } from "path";
10001
+ import { dirname as dirname12, resolve as resolve11 } from "path";
9860
10002
  import { fileURLToPath as fileURLToPath8 } from "url";
9861
10003
  function parseCommand(argv) {
9862
10004
  const [first, ...rest] = argv;
@@ -9881,7 +10023,7 @@ Usage:
9881
10023
  runcastle --help Show this help
9882
10024
  `;
9883
10025
  async function main(argv) {
9884
- applyInstalledAssetEnv(resolve11(dirname11(fileURLToPath8(import.meta.url)), ".."));
10026
+ applyInstalledAssetEnv(resolve11(dirname12(fileURLToPath8(import.meta.url)), ".."));
9885
10027
  const { command, args } = parseCommand(argv);
9886
10028
  switch (command) {
9887
10029
  case "version":