runcastle 1.2.3 → 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(),
@@ -1566,6 +1579,8 @@ function rowToSession(row) {
1566
1579
  featureId: row.featureId ?? undefined,
1567
1580
  projectId: row.projectId ?? undefined,
1568
1581
  kind: row.kind,
1582
+ purpose: row.purpose ?? undefined,
1583
+ purposeData: row.purposeData ?? undefined,
1569
1584
  ccSessionId: row.ccSessionId ?? undefined,
1570
1585
  transcriptPath: row.transcriptPath ?? undefined,
1571
1586
  status: row.status,
@@ -3181,10 +3196,24 @@ async function conflictedFiles(g) {
3181
3196
  return [];
3182
3197
  }
3183
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
+ }
3184
3214
  async function mergeInProgress(g) {
3185
3215
  try {
3186
- await g.raw(["rev-parse", "--verify", "--quiet", "MERGE_HEAD"]);
3187
- return true;
3216
+ return (await g.raw(["rev-parse", "--verify", "--quiet", "MERGE_HEAD"])).trim() !== "";
3188
3217
  } catch {
3189
3218
  return false;
3190
3219
  }
@@ -3214,6 +3243,8 @@ function createSessionRow(ctx, input) {
3214
3243
  projectId: input.projectId ?? null,
3215
3244
  lap: input.featureId ? getFeatureRow(ctx, input.featureId).lap : 1,
3216
3245
  kind: input.kind,
3246
+ purpose: input.purpose ?? null,
3247
+ purposeData: input.purposeData ?? null,
3217
3248
  ccSessionId: null,
3218
3249
  transcriptPath: null,
3219
3250
  status: "launching",
@@ -6658,6 +6689,8 @@ function guardsEdits(kind) {
6658
6689
  function evaluateEditGuard(input) {
6659
6690
  if (!guardsEdits(input.kind))
6660
6691
  return null;
6692
+ if (input.purpose === "resolve-conflict" && input.mergeInProgress)
6693
+ return null;
6661
6694
  if (!input.toolName || !EDIT_TOOLS.includes(input.toolName))
6662
6695
  return null;
6663
6696
  if (!input.filePath)
@@ -6834,22 +6867,40 @@ function handleUserPrompt(ctx, sessionId, feature, payload) {
6834
6867
  }
6835
6868
  };
6836
6869
  }
6837
- function handleSessionEnd(ctx, sessionId, feature) {
6838
- markSessionEnded(ctx, sessionId);
6839
- releaseForSession(ctx, sessionId);
6870
+ async function handleSessionEnd(ctx, session, feature) {
6871
+ markSessionEnded(ctx, session.id);
6872
+ releaseForSession(ctx, session.id);
6840
6873
  emit(ctx, feature.id, {
6841
6874
  type: "session.ended",
6842
6875
  message: "session ended",
6843
- data: { sessionId }
6876
+ data: { sessionId: session.id }
6844
6877
  });
6878
+ await noteResolvedMerge(ctx, session, feature);
6845
6879
  return {};
6846
6880
  }
6847
- 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) {
6848
6896
  const toolName = typeof payload?.tool_name === "string" ? payload.tool_name : undefined;
6849
6897
  const toolInput = payload?.tool_input ?? {};
6850
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;
6851
6900
  const denial = evaluateEditGuard({
6852
6901
  kind: session.kind,
6902
+ purpose: session.purpose,
6903
+ mergeInProgress: mergeInProgress2,
6853
6904
  worktreePath: session.worktreePath,
6854
6905
  toolName,
6855
6906
  filePath: path,
@@ -6878,6 +6929,7 @@ var init_hooks = __esm(() => {
6878
6929
  init_edit_guard();
6879
6930
  init_sessions();
6880
6931
  init_events();
6932
+ init_git();
6881
6933
  init_prep();
6882
6934
  init_repo();
6883
6935
  init_tickets();
@@ -6915,7 +6967,7 @@ var init_hooks = __esm(() => {
6915
6967
  case "session-end":
6916
6968
  return c.json(handleProjectScopedSessionEnd(ctx, session));
6917
6969
  case "pre-tool":
6918
- return c.json(handlePreToolUse(session, undefined, body.payload));
6970
+ return c.json(await handlePreToolUse(session, undefined, body.payload));
6919
6971
  default:
6920
6972
  return c.json({});
6921
6973
  }
@@ -6929,9 +6981,9 @@ var init_hooks = __esm(() => {
6929
6981
  case "user-prompt":
6930
6982
  return c.json(handleUserPrompt(ctx, sessionId, feature, body.payload));
6931
6983
  case "session-end":
6932
- return c.json(handleSessionEnd(ctx, sessionId, feature));
6984
+ return c.json(await handleSessionEnd(ctx, session, feature));
6933
6985
  case "pre-tool":
6934
- return c.json(handlePreToolUse(session, feature, body.payload));
6986
+ return c.json(await handlePreToolUse(session, feature, body.payload));
6935
6987
  default:
6936
6988
  return c.json({});
6937
6989
  }
@@ -7073,6 +7125,166 @@ var init_web = __esm(() => {
7073
7125
  };
7074
7126
  });
7075
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
+
7076
7288
  // src/trpc/context.ts
7077
7289
  import { initTRPC } from "@trpc/server";
7078
7290
  var t, errorMapping, router, publicProcedure, createCallerFactory;
@@ -7121,11 +7333,11 @@ var init_events2 = __esm(() => {
7121
7333
  });
7122
7334
 
7123
7335
  // src/launcher/artifacts.ts
7124
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync2 } from "fs";
7125
- import { fileURLToPath as fileURLToPath4 } from "url";
7126
- 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";
7127
7339
  function hookClientPath() {
7128
- 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)));
7129
7341
  }
7130
7342
  function serverUrlFor(config2) {
7131
7343
  return `http://localhost:${config2.serverPort}`;
@@ -7547,16 +7759,16 @@ function renderMcpConfig(session, config2) {
7547
7759
  async function writeSessionArtifacts(input) {
7548
7760
  const { session, feature, config: config2, waypoint, prepare, projectBrief, lap } = input;
7549
7761
  const dir = sessionDir(session.id);
7550
- mkdirSync6(dir, { recursive: true });
7551
- const systemPromptPath = join12(dir, "system-prompt.md");
7552
- const settingsPath = join12(dir, "settings.json");
7553
- 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");
7554
7766
  const systemPrompt = feature ? renderSystemPrompt(feature, session.kind, waypoint, lap) : prepare ? renderPreparePrompt(prepare) : projectBrief ? renderProjectPrompt(projectBrief) : (() => {
7555
7767
  throw new Error(`session ${session.id} has no feature and no project-session brief`);
7556
7768
  })();
7557
- writeFileSync2(systemPromptPath, systemPrompt, "utf8");
7558
- writeFileSync2(settingsPath, JSON.stringify(renderSettings(hookClientPath(), session.kind), null, 2), "utf8");
7559
- 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");
7560
7772
  return { systemPromptPath, settingsPath, mcpConfigPath };
7561
7773
  }
7562
7774
  var RUNCASTLE_MCP_ALLOW_RULES, SESSION_BASH_READ_RULES, SESSION_BASH_WRITE_RULES, SESSION_BASH_ALLOW_RULES, SESSION_START_SOURCES;
@@ -7600,9 +7812,9 @@ var init_artifacts = __esm(() => {
7600
7812
  });
7601
7813
 
7602
7814
  // src/launcher/launcher.ts
7603
- import { existsSync as existsSync13 } from "fs";
7604
- import { dirname as dirname5, join as join13, resolve as resolve9 } from "path";
7605
- 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";
7606
7818
  import { and as and9, eq as eq11 } from "drizzle-orm";
7607
7819
  function ptyExitMessage(exitCode) {
7608
7820
  const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
@@ -7634,23 +7846,23 @@ function resolveClaudeExecutable() {
7634
7846
  function claudeSpawnTarget(claudeArgs) {
7635
7847
  return spawnTargetFor(resolveClaudeExecutable(), claudeArgs);
7636
7848
  }
7637
- function resolvePluginDir(fromDir = dirname5(fileURLToPath5(import.meta.url))) {
7638
- const rel = join13("packages", "skills", "packs", "runcastle");
7849
+ function resolvePluginDir(fromDir = dirname7(fileURLToPath6(import.meta.url))) {
7850
+ const rel = join14("packages", "skills", "packs", "runcastle");
7639
7851
  const override = process.env[SKILLS_DIR_ENV];
7640
7852
  if (override) {
7641
- const dir2 = join13(resolve9(override), "packs", "runcastle");
7642
- if (existsSync13(dir2))
7853
+ const dir2 = join14(resolve9(override), "packs", "runcastle");
7854
+ if (existsSync14(dir2))
7643
7855
  return dir2;
7644
7856
  throw new Error(`${SKILLS_DIR_ENV}=${override} has no plugin dir at ${dir2}`);
7645
7857
  }
7646
7858
  const searched = [];
7647
7859
  let dir = fromDir;
7648
7860
  for (let i = 0;i < 8; i += 1) {
7649
- const candidate = join13(dir, rel);
7861
+ const candidate = join14(dir, rel);
7650
7862
  searched.push(candidate);
7651
- if (existsSync13(candidate))
7863
+ if (existsSync14(candidate))
7652
7864
  return candidate;
7653
- const parent = dirname5(dir);
7865
+ const parent = dirname7(dir);
7654
7866
  if (parent === dir)
7655
7867
  break;
7656
7868
  dir = parent;
@@ -7719,6 +7931,8 @@ async function launchSession(ctx, input, opts = {}) {
7719
7931
  const session = createSessionRow(ctx, {
7720
7932
  featureId: feature.id,
7721
7933
  kind: input.kind,
7934
+ purpose: input.purpose,
7935
+ purposeData: input.purposeData,
7722
7936
  worktreePath
7723
7937
  });
7724
7938
  const plan = planKickoff({ kind: input.kind, lap: feature.lap, kickoffLine: input.kickoffLine });
@@ -8166,7 +8380,9 @@ var init_feature = __esm(() => {
8166
8380
  launchSession: publicProcedure.input(z6.object({
8167
8381
  featureId: z6.string(),
8168
8382
  kind: SessionKind,
8169
- kickoffLine: z6.string().min(1).optional()
8383
+ kickoffLine: z6.string().min(1).optional(),
8384
+ purpose: SessionPurpose.optional(),
8385
+ purposeData: MergeBranchPair.optional()
8170
8386
  })).mutation(({ ctx, input }) => launchSession(ctx, input)),
8171
8387
  workWaypoint: publicProcedure.input(z6.object({ featureId: z6.string(), waypointId: z6.string(), endLive: z6.boolean().optional() })).mutation(({ ctx, input }) => workWaypoint(ctx, input)),
8172
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 })),
@@ -8217,9 +8433,9 @@ var init_feature = __esm(() => {
8217
8433
  });
8218
8434
 
8219
8435
  // src/services/fsbrowse.ts
8220
- 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";
8221
8437
  import { homedir as homedir2 } from "os";
8222
- 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";
8223
8439
  function expandPath(input, home = homedir2()) {
8224
8440
  const trimmed = input.trim();
8225
8441
  if (trimmed === "")
@@ -8228,12 +8444,12 @@ function expandPath(input, home = homedir2()) {
8228
8444
  if (out === "~")
8229
8445
  out = home;
8230
8446
  else if (out.startsWith("~/") || out.startsWith("~\\"))
8231
- out = join14(home, out.slice(2));
8447
+ out = join15(home, out.slice(2));
8232
8448
  return resolve10(out);
8233
8449
  }
8234
8450
  function looksLikeRepo(dir) {
8235
8451
  try {
8236
- return existsSync14(join14(dir, ".git"));
8452
+ return existsSync15(join15(dir, ".git"));
8237
8453
  } catch {
8238
8454
  return false;
8239
8455
  }
@@ -8244,20 +8460,20 @@ function listRoots(home = homedir2()) {
8244
8460
  for (let code = 67;code <= 90; code++) {
8245
8461
  const drive = `${String.fromCharCode(code)}:${sep3}`;
8246
8462
  try {
8247
- if (existsSync14(drive))
8463
+ if (existsSync15(drive))
8248
8464
  roots.push({ label: drive.replace(/\\$/, ""), path: drive, kind: "drive" });
8249
8465
  } catch {}
8250
8466
  }
8251
8467
  } else {
8252
8468
  roots.push({ label: "/", path: "/", kind: "drive" });
8253
8469
  }
8254
- if (existsSync14(home))
8470
+ if (existsSync15(home))
8255
8471
  roots.push({ label: "Home", path: home, kind: "home" });
8256
8472
  const seen = new Set(roots.map(pathKey));
8257
8473
  for (const name of COMMON_DIRS) {
8258
- const candidate = join14(home, name);
8474
+ const candidate = join15(home, name);
8259
8475
  const key = pathKey({ path: candidate });
8260
- if (seen.has(key) || !existsSync14(candidate))
8476
+ if (seen.has(key) || !existsSync15(candidate))
8261
8477
  continue;
8262
8478
  seen.add(key);
8263
8479
  roots.push({ label: name, path: candidate, kind: "common" });
@@ -8272,7 +8488,7 @@ function crumbsFor(dir) {
8272
8488
  let cur = dir;
8273
8489
  for (;; ) {
8274
8490
  out.unshift({ name: basename(cur) || cur, path: cur });
8275
- const parent = dirname6(cur);
8491
+ const parent = dirname8(cur);
8276
8492
  if (parent === cur)
8277
8493
  break;
8278
8494
  cur = parent;
@@ -8280,7 +8496,7 @@ function crumbsFor(dir) {
8280
8496
  return out;
8281
8497
  }
8282
8498
  function parentOf(dir) {
8283
- const parent = dirname6(dir);
8499
+ const parent = dirname8(dir);
8284
8500
  return parent === dir ? null : parent;
8285
8501
  }
8286
8502
  function browseDir(input, showHidden = false, home = homedir2()) {
@@ -8288,13 +8504,13 @@ function browseDir(input, showHidden = false, home = homedir2()) {
8288
8504
  if (!isAbsolute2(target)) {
8289
8505
  throw new InvalidInputError(`path is not absolute: ${target}`);
8290
8506
  }
8291
- if (!existsSync14(target)) {
8507
+ if (!existsSync15(target)) {
8292
8508
  throw new InvalidInputError(`path does not exist: ${target}`);
8293
8509
  }
8294
8510
  let dir = target;
8295
8511
  try {
8296
8512
  if (!statSync2(target).isDirectory())
8297
- dir = dirname6(target);
8513
+ dir = dirname8(target);
8298
8514
  } catch (e) {
8299
8515
  throw new InvalidInputError(`cannot read path: ${target} (${errMsg4(e)})`);
8300
8516
  }
@@ -8316,7 +8532,7 @@ function browseDir(input, showHidden = false, home = homedir2()) {
8316
8532
  continue;
8317
8533
  if (!showHidden && name.startsWith("."))
8318
8534
  continue;
8319
- const path = join14(dir, name);
8535
+ const path = join15(dir, name);
8320
8536
  const isSymlink = dirent.isSymbolicLink();
8321
8537
  if (isSymlink && !showHidden)
8322
8538
  continue;
@@ -8353,8 +8569,8 @@ var init_fsbrowse = __esm(() => {
8353
8569
  });
8354
8570
 
8355
8571
  // src/services/projects.ts
8356
- import { existsSync as existsSync15 } from "fs";
8357
- 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";
8358
8574
  import { eq as eq12, isNull } from "drizzle-orm";
8359
8575
  function listProjects(ctx) {
8360
8576
  return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
@@ -8435,7 +8651,7 @@ async function assertRepoTolerant(ctx, repoPath) {
8435
8651
  } catch (e) {
8436
8652
  if (!isNotImplemented(e))
8437
8653
  throw e;
8438
- if (!existsSync15(join15(repoPath, ".git"))) {
8654
+ if (!existsSync16(join16(repoPath, ".git"))) {
8439
8655
  throw new InvalidInputError(`not a git repository: ${repoPath}`);
8440
8656
  }
8441
8657
  }
@@ -8508,15 +8724,15 @@ var init_run = __esm(() => {
8508
8724
  });
8509
8725
 
8510
8726
  // src/services/settings.ts
8511
- import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
8512
- 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";
8513
8729
  import * as z9 from "zod";
8514
8730
  import { eq as eq13 } from "drizzle-orm";
8515
8731
  function readRawConfig(configFile) {
8516
- if (!existsSync16(configFile))
8732
+ if (!existsSync17(configFile))
8517
8733
  return {};
8518
8734
  try {
8519
- const parsed = JSON.parse(readFileSync6(configFile, "utf8"));
8735
+ const parsed = JSON.parse(readFileSync8(configFile, "utf8"));
8520
8736
  if (typeof parsed !== "object" || parsed === null)
8521
8737
  return {};
8522
8738
  return foldLegacyModelConfig(parsed);
@@ -8655,8 +8871,8 @@ function updateSettings(ctx, input, io = {}) {
8655
8871
  function writeGlobal(configFile, configKey, value) {
8656
8872
  const raw = readRawConfig(configFile);
8657
8873
  raw[configKey] = value;
8658
- mkdirSync7(dirname7(configFile), { recursive: true });
8659
- writeFileSync3(configFile, `${JSON.stringify(raw, null, 2)}
8874
+ mkdirSync8(dirname9(configFile), { recursive: true });
8875
+ writeFileSync4(configFile, `${JSON.stringify(raw, null, 2)}
8660
8876
  `);
8661
8877
  }
8662
8878
  function updateStepModel(ctx, input, configFile, io) {
@@ -8694,8 +8910,8 @@ function writeStepModel(configFile, step, value) {
8694
8910
  else
8695
8911
  stepModels[step] = value;
8696
8912
  raw.stepModels = stepModels;
8697
- mkdirSync7(dirname7(configFile), { recursive: true });
8698
- writeFileSync3(configFile, `${JSON.stringify(raw, null, 2)}
8913
+ mkdirSync8(dirname9(configFile), { recursive: true });
8914
+ writeFileSync4(configFile, `${JSON.stringify(raw, null, 2)}
8699
8915
  `);
8700
8916
  }
8701
8917
  function field(view, key) {
@@ -9072,9 +9288,9 @@ var init_system_exec = __esm(() => {
9072
9288
  });
9073
9289
 
9074
9290
  // src/services/setup.ts
9075
- import { copyFileSync, existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync7, readdirSync as readdirSync6, writeFileSync as writeFileSync4 } from "fs";
9076
- import { dirname as dirname8, join as join16 } from "path";
9077
- 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";
9078
9294
  async function writeGitIdentity(exec, identity) {
9079
9295
  const name = identity.name.trim();
9080
9296
  const email = identity.email.trim();
@@ -9149,16 +9365,16 @@ async function resolveRuntime(exec, preferred) {
9149
9365
  function resolveSandcastleBin() {
9150
9366
  try {
9151
9367
  const mainUrl = import.meta.resolve("@ai-hero/sandcastle");
9152
- let dir = dirname8(fileURLToPath6(mainUrl));
9368
+ let dir = dirname10(fileURLToPath7(mainUrl));
9153
9369
  for (let hops = 0;hops < 6; hops++) {
9154
- const manifestPath = join16(dir, "package.json");
9155
- if (existsSync17(manifestPath)) {
9156
- 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"));
9157
9373
  if (manifest.name === "@ai-hero/sandcastle" && manifest.bin?.sandcastle) {
9158
- return join16(dir, manifest.bin.sandcastle);
9374
+ return join17(dir, manifest.bin.sandcastle);
9159
9375
  }
9160
9376
  }
9161
- const parent = dirname8(dir);
9377
+ const parent = dirname10(dir);
9162
9378
  if (parent === dir)
9163
9379
  break;
9164
9380
  dir = parent;
@@ -9180,21 +9396,21 @@ function terminalSpec(kind, opts) {
9180
9396
  };
9181
9397
  }
9182
9398
  function sandcastleTemplateDir() {
9183
- 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)));
9184
9400
  }
9185
9401
  function scaffoldSandcastleConfig(templateDir, targetDir) {
9186
- const dir = join16(targetDir, ".sandcastle");
9187
- if (existsSync17(dir))
9402
+ const dir = join17(targetDir, ".sandcastle");
9403
+ if (existsSync18(dir))
9188
9404
  return { scaffolded: false, dir };
9189
- mkdirSync8(dir, { recursive: true });
9405
+ mkdirSync9(dir, { recursive: true });
9190
9406
  for (const name of readdirSync6(templateDir)) {
9191
- copyFileSync(join16(templateDir, name), join16(dir, name));
9407
+ copyFileSync(join17(templateDir, name), join17(dir, name));
9192
9408
  }
9193
9409
  return { scaffolded: true, dir };
9194
9410
  }
9195
9411
  function prepareSandboxBuildContext() {
9196
9412
  const target = sandboxBuildDir();
9197
- mkdirSync8(target, { recursive: true });
9413
+ mkdirSync9(target, { recursive: true });
9198
9414
  scaffoldSandcastleConfig(sandcastleTemplateDir(), target);
9199
9415
  return target;
9200
9416
  }
@@ -9230,10 +9446,10 @@ function createTokenVerifier(exec) {
9230
9446
  function fileAfkTokenIo(exec) {
9231
9447
  const path = envPath();
9232
9448
  return {
9233
- read: () => existsSync17(path) ? readFileSync7(path, "utf8") : "",
9449
+ read: () => existsSync18(path) ? readFileSync9(path, "utf8") : "",
9234
9450
  write: (content) => {
9235
- mkdirSync8(dirname8(path), { recursive: true });
9236
- writeFileSync4(path, content, "utf8");
9451
+ mkdirSync9(dirname10(path), { recursive: true });
9452
+ writeFileSync5(path, content, "utf8");
9237
9453
  },
9238
9454
  verify: createTokenVerifier(exec)
9239
9455
  };
@@ -9286,110 +9502,6 @@ var init_setup2 = __esm(() => {
9286
9502
  });
9287
9503
  });
9288
9504
 
9289
- // src/version.ts
9290
- import { readFileSync as readFileSync8 } from "fs";
9291
- import { dirname as dirname9, join as join17 } from "path";
9292
- import { fileURLToPath as fileURLToPath7 } from "url";
9293
- function runcastleVersion() {
9294
- if (cached2)
9295
- return cached2;
9296
- const here = dirname9(fileURLToPath7(import.meta.url));
9297
- for (const pkgPath of [join17(here, "..", "package.json"), join17(here, "package.json")]) {
9298
- try {
9299
- const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
9300
- if (typeof pkg.version === "string") {
9301
- cached2 = pkg.version;
9302
- return cached2;
9303
- }
9304
- } catch {}
9305
- }
9306
- cached2 = UNKNOWN_VERSION;
9307
- return cached2;
9308
- }
9309
- var cached2, UNKNOWN_VERSION = "0.0.0";
9310
- var init_version = () => {};
9311
-
9312
- // src/services/update-check.ts
9313
- function parse(version) {
9314
- const cleaned = version.trim().replace(/^v/, "");
9315
- const [core = "", pre = ""] = cleaned.split("-", 2);
9316
- const release2 = core.split(".").map((n) => Number.parseInt(n, 10) || 0);
9317
- return { release: release2, pre: pre ? pre.split(".") : [] };
9318
- }
9319
- function compareSemver(a, b) {
9320
- const pa = parse(a);
9321
- const pb = parse(b);
9322
- const len = Math.max(pa.release.length, pb.release.length);
9323
- for (let i = 0;i < len; i++) {
9324
- const diff = (pa.release[i] ?? 0) - (pb.release[i] ?? 0);
9325
- if (diff !== 0)
9326
- return diff;
9327
- }
9328
- if (pa.pre.length === 0 && pb.pre.length === 0)
9329
- return 0;
9330
- if (pa.pre.length === 0)
9331
- return 1;
9332
- if (pb.pre.length === 0)
9333
- return -1;
9334
- const preLen = Math.max(pa.pre.length, pb.pre.length);
9335
- for (let i = 0;i < preLen; i++) {
9336
- const x = pa.pre[i];
9337
- const y = pb.pre[i];
9338
- if (x === undefined)
9339
- return -1;
9340
- if (y === undefined)
9341
- return 1;
9342
- const nx = Number(x);
9343
- const ny = Number(y);
9344
- const bothNumeric = !Number.isNaN(nx) && !Number.isNaN(ny);
9345
- const cmp = bothNumeric ? nx - ny : x < y ? -1 : x > y ? 1 : 0;
9346
- if (cmp !== 0)
9347
- return cmp;
9348
- }
9349
- return 0;
9350
- }
9351
- async function checkForUpdate(opts) {
9352
- const { current: current2 } = opts;
9353
- const fetchImpl = opts.fetchImpl ?? fetch;
9354
- if (current2 === UNKNOWN_VERSION)
9355
- return NO_UPDATE(current2);
9356
- try {
9357
- const res = await fetchImpl(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`);
9358
- if (!res.ok)
9359
- return NO_UPDATE(current2);
9360
- const body = await res.json();
9361
- const latest = typeof body.version === "string" ? body.version : null;
9362
- if (!latest)
9363
- return NO_UPDATE(current2);
9364
- return {
9365
- current: current2,
9366
- latest,
9367
- updateAvailable: compareSemver(latest, current2) > 0,
9368
- command: UPDATE_COMMAND
9369
- };
9370
- } catch {
9371
- return NO_UPDATE(current2);
9372
- }
9373
- }
9374
- async function getUpdateInfo(current2, fetchImpl) {
9375
- if (cache)
9376
- return cache;
9377
- const info = await checkForUpdate({ current: current2, fetchImpl });
9378
- if (info.latest !== null)
9379
- cache = info;
9380
- return info;
9381
- }
9382
- var PACKAGE_NAME = "runcastle", UPDATE_COMMAND, NO_UPDATE = (current2) => ({
9383
- current: current2,
9384
- latest: null,
9385
- updateAvailable: false,
9386
- command: UPDATE_COMMAND
9387
- }), cache;
9388
- var init_update_check = __esm(() => {
9389
- init_version();
9390
- UPDATE_COMMAND = `bun add -g ${PACKAGE_NAME}@latest`;
9391
- });
9392
-
9393
9505
  // src/trpc/routers/system.ts
9394
9506
  var systemRouter;
9395
9507
  var init_system = __esm(() => {
@@ -9403,8 +9515,8 @@ var init_system = __esm(() => {
9403
9515
  });
9404
9516
 
9405
9517
  // src/services/test-notes.ts
9406
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync5 } from "fs";
9407
- import { dirname as dirname10 } from "path";
9518
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync6 } from "fs";
9519
+ import { dirname as dirname11 } from "path";
9408
9520
  import { asc as asc4, eq as eq14 } from "drizzle-orm";
9409
9521
  function rowToNote(row) {
9410
9522
  return {
@@ -9549,8 +9661,8 @@ function renderTestNotes(ctx, feature) {
9549
9661
  lines.push(noteLine(ctx, note));
9550
9662
  }
9551
9663
  const path = featureDocPath(projectForFeature(ctx, feature), feature, "test-notes.md");
9552
- mkdirSync9(dirname10(path), { recursive: true });
9553
- writeFileSync5(path, `${lines.join(`
9664
+ mkdirSync10(dirname11(path), { recursive: true });
9665
+ writeFileSync6(path, `${lines.join(`
9554
9666
  `)}
9555
9667
  `, "utf8");
9556
9668
  }
@@ -9749,6 +9861,7 @@ async function startServer() {
9749
9861
  websocket: terminalWebSocket
9750
9862
  });
9751
9863
  console.log(`runcastle${process.env.RUNCASTLE_DEV ? " [dev]" : ""} server listening on ` + `http://localhost:${config2.serverPort} \u2014 data dir: ${dataDir()}`);
9864
+ getUpdateInfo(runcastleVersion()).catch(() => {});
9752
9865
  const SHUTDOWN_KEY = Symbol.for("runcastle.shutdown.wired");
9753
9866
  const g = globalThis;
9754
9867
  if (!g[SHUTDOWN_KEY]) {
@@ -9774,7 +9887,9 @@ var init_src2 = __esm(() => {
9774
9887
  init_hooks();
9775
9888
  init_stream();
9776
9889
  init_web();
9890
+ init_update_check();
9777
9891
  init_router();
9892
+ init_version();
9778
9893
  init_reconcile_runs();
9779
9894
  });
9780
9895
 
@@ -9832,7 +9947,7 @@ __export(exports_cli, {
9832
9947
  resolveDoctorEnv: () => resolveDoctorEnv,
9833
9948
  parseMode: () => parseMode
9834
9949
  });
9835
- import { existsSync as existsSync18, readFileSync as readFileSync9 } from "fs";
9950
+ import { existsSync as existsSync19, readFileSync as readFileSync10 } from "fs";
9836
9951
  function parseMode(argv) {
9837
9952
  return argv.includes("--gate") || argv.includes("--boot") ? "gate" : "diagnostic";
9838
9953
  }
@@ -9840,8 +9955,8 @@ function envWithToken() {
9840
9955
  const merged = { ...process.env };
9841
9956
  try {
9842
9957
  const path = envPath();
9843
- if (existsSync18(path)) {
9844
- 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;
9845
9960
  if (fromFile && fromFile.length > 0)
9846
9961
  merged.CLAUDE_CODE_OAUTH_TOKEN = fromFile;
9847
9962
  }
@@ -9883,7 +9998,7 @@ var init_cli = __esm(() => {
9883
9998
  // src/bin/runcastle.ts
9884
9999
  init_asset_paths();
9885
10000
  init_version();
9886
- import { dirname as dirname11, resolve as resolve11 } from "path";
10001
+ import { dirname as dirname12, resolve as resolve11 } from "path";
9887
10002
  import { fileURLToPath as fileURLToPath8 } from "url";
9888
10003
  function parseCommand(argv) {
9889
10004
  const [first, ...rest] = argv;
@@ -9908,7 +10023,7 @@ Usage:
9908
10023
  runcastle --help Show this help
9909
10024
  `;
9910
10025
  async function main(argv) {
9911
- applyInstalledAssetEnv(resolve11(dirname11(fileURLToPath8(import.meta.url)), ".."));
10026
+ applyInstalledAssetEnv(resolve11(dirname12(fileURLToPath8(import.meta.url)), ".."));
9912
10027
  const { command, args } = parseCommand(argv);
9913
10028
  switch (command) {
9914
10029
  case "version":