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/index.js CHANGED
@@ -25,6 +25,7 @@ __export(exports_paths, {
25
25
  projectWorktreesDir: () => projectWorktreesDir,
26
26
  prodDataDir: () => prodDataDir,
27
27
  logsDir: () => logsDir,
28
+ installIdPath: () => installIdPath,
28
29
  featureDocsRel: () => featureDocsRel,
29
30
  envPath: () => envPath,
30
31
  devDataDir: () => devDataDir,
@@ -63,6 +64,9 @@ function configPath() {
63
64
  function envPath() {
64
65
  return join(dataDir(), ".env");
65
66
  }
67
+ function installIdPath() {
68
+ return join(dataDir(), "install-id");
69
+ }
66
70
  function logsDir() {
67
71
  return join(dataDir(), "logs");
68
72
  }
@@ -245,7 +249,7 @@ var init_ids = () => {};
245
249
 
246
250
  // ../core/src/schemas.ts
247
251
  import * as z2 from "zod";
248
- 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;
252
+ 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;
249
253
  var init_schemas = __esm(() => {
250
254
  Phase = z2.enum([
251
255
  "ideation",
@@ -265,6 +269,11 @@ var init_schemas = __esm(() => {
265
269
  "prepare",
266
270
  "project"
267
271
  ]);
272
+ SessionPurpose = z2.enum(["resolve-conflict"]);
273
+ MergeBranchPair = z2.object({
274
+ mergeFrom: z2.string(),
275
+ mergeInto: z2.string()
276
+ });
268
277
  RunStatus = z2.enum(["running", "succeeded", "failed", "cancelled"]);
269
278
  FeatureStatus = z2.enum(["active", "shipped", "archived"]);
270
279
  SessionStatus = z2.enum(["launching", "live", "ended"]);
@@ -381,6 +390,8 @@ var init_schemas = __esm(() => {
381
390
  featureId: z2.string().optional(),
382
391
  projectId: z2.string().optional(),
383
392
  kind: SessionKind,
393
+ purpose: SessionPurpose.optional(),
394
+ purposeData: MergeBranchPair.optional(),
384
395
  ccSessionId: z2.string().optional(),
385
396
  transcriptPath: z2.string().optional(),
386
397
  status: SessionStatus,
@@ -619,6 +630,8 @@ var init_db_schema = __esm(() => {
619
630
  projectId: text("project_id"),
620
631
  lap: integer("lap").notNull().default(1),
621
632
  kind: text("kind").notNull().$type(),
633
+ purpose: text("purpose").$type(),
634
+ purposeData: text("purpose_data", { mode: "json" }).$type(),
622
635
  ccSessionId: text("cc_session_id"),
623
636
  transcriptPath: text("transcript_path"),
624
637
  status: text("status").notNull().$type(),
@@ -1565,6 +1578,8 @@ function rowToSession(row) {
1565
1578
  featureId: row.featureId ?? undefined,
1566
1579
  projectId: row.projectId ?? undefined,
1567
1580
  kind: row.kind,
1581
+ purpose: row.purpose ?? undefined,
1582
+ purposeData: row.purposeData ?? undefined,
1568
1583
  ccSessionId: row.ccSessionId ?? undefined,
1569
1584
  transcriptPath: row.transcriptPath ?? undefined,
1570
1585
  status: row.status,
@@ -3180,10 +3195,24 @@ async function conflictedFiles(g) {
3180
3195
  return [];
3181
3196
  }
3182
3197
  }
3198
+ async function mergeInProgressAt(worktreePath) {
3199
+ try {
3200
+ return await mergeInProgress(git(worktreePath));
3201
+ } catch {
3202
+ return false;
3203
+ }
3204
+ }
3205
+ async function isAncestor(repoPath, ancestor, descendant) {
3206
+ try {
3207
+ const out = await git(repoPath).raw(["rev-list", "--count", ancestor, `^${descendant}`]);
3208
+ return out.trim() === "0";
3209
+ } catch {
3210
+ return false;
3211
+ }
3212
+ }
3183
3213
  async function mergeInProgress(g) {
3184
3214
  try {
3185
- await g.raw(["rev-parse", "--verify", "--quiet", "MERGE_HEAD"]);
3186
- return true;
3215
+ return (await g.raw(["rev-parse", "--verify", "--quiet", "MERGE_HEAD"])).trim() !== "";
3187
3216
  } catch {
3188
3217
  return false;
3189
3218
  }
@@ -3213,6 +3242,8 @@ function createSessionRow(ctx, input) {
3213
3242
  projectId: input.projectId ?? null,
3214
3243
  lap: input.featureId ? getFeatureRow(ctx, input.featureId).lap : 1,
3215
3244
  kind: input.kind,
3245
+ purpose: input.purpose ?? null,
3246
+ purposeData: input.purposeData ?? null,
3216
3247
  ccSessionId: null,
3217
3248
  transcriptPath: null,
3218
3249
  status: "launching",
@@ -6657,6 +6688,8 @@ function guardsEdits(kind) {
6657
6688
  function evaluateEditGuard(input) {
6658
6689
  if (!guardsEdits(input.kind))
6659
6690
  return null;
6691
+ if (input.purpose === "resolve-conflict" && input.mergeInProgress)
6692
+ return null;
6660
6693
  if (!input.toolName || !EDIT_TOOLS.includes(input.toolName))
6661
6694
  return null;
6662
6695
  if (!input.filePath)
@@ -6833,22 +6866,40 @@ function handleUserPrompt(ctx, sessionId, feature, payload) {
6833
6866
  }
6834
6867
  };
6835
6868
  }
6836
- function handleSessionEnd(ctx, sessionId, feature) {
6837
- markSessionEnded(ctx, sessionId);
6838
- releaseForSession(ctx, sessionId);
6869
+ async function handleSessionEnd(ctx, session, feature) {
6870
+ markSessionEnded(ctx, session.id);
6871
+ releaseForSession(ctx, session.id);
6839
6872
  emit(ctx, feature.id, {
6840
6873
  type: "session.ended",
6841
6874
  message: "session ended",
6842
- data: { sessionId }
6875
+ data: { sessionId: session.id }
6843
6876
  });
6877
+ await noteResolvedMerge(ctx, session, feature);
6844
6878
  return {};
6845
6879
  }
6846
- function handlePreToolUse(session, feature, payload) {
6880
+ async function noteResolvedMerge(ctx, session, feature) {
6881
+ const pair = session.purpose === "resolve-conflict" ? session.purposeData : undefined;
6882
+ if (!pair)
6883
+ return;
6884
+ try {
6885
+ if (!await isAncestor(session.worktreePath, pair.mergeFrom, pair.mergeInto))
6886
+ return;
6887
+ emit(ctx, feature.id, {
6888
+ type: "merge.resolved",
6889
+ message: `merge conflict resolved \u2014 ${pair.mergeFrom} is in ${pair.mergeInto}`,
6890
+ data: { sessionId: session.id, ...pair }
6891
+ });
6892
+ } catch {}
6893
+ }
6894
+ async function handlePreToolUse(session, feature, payload) {
6847
6895
  const toolName = typeof payload?.tool_name === "string" ? payload.tool_name : undefined;
6848
6896
  const toolInput = payload?.tool_input ?? {};
6849
6897
  const path = typeof toolInput.file_path === "string" ? toolInput.file_path : typeof toolInput.notebook_path === "string" ? toolInput.notebook_path : undefined;
6898
+ const mergeInProgress2 = session.purpose === "resolve-conflict" ? await mergeInProgressAt(session.worktreePath) : false;
6850
6899
  const denial = evaluateEditGuard({
6851
6900
  kind: session.kind,
6901
+ purpose: session.purpose,
6902
+ mergeInProgress: mergeInProgress2,
6852
6903
  worktreePath: session.worktreePath,
6853
6904
  toolName,
6854
6905
  filePath: path,
@@ -6877,6 +6928,7 @@ var init_hooks = __esm(() => {
6877
6928
  init_edit_guard();
6878
6929
  init_sessions();
6879
6930
  init_events();
6931
+ init_git();
6880
6932
  init_prep();
6881
6933
  init_repo();
6882
6934
  init_tickets();
@@ -6914,7 +6966,7 @@ var init_hooks = __esm(() => {
6914
6966
  case "session-end":
6915
6967
  return c.json(handleProjectScopedSessionEnd(ctx, session));
6916
6968
  case "pre-tool":
6917
- return c.json(handlePreToolUse(session, undefined, body.payload));
6969
+ return c.json(await handlePreToolUse(session, undefined, body.payload));
6918
6970
  default:
6919
6971
  return c.json({});
6920
6972
  }
@@ -6928,9 +6980,9 @@ var init_hooks = __esm(() => {
6928
6980
  case "user-prompt":
6929
6981
  return c.json(handleUserPrompt(ctx, sessionId, feature, body.payload));
6930
6982
  case "session-end":
6931
- return c.json(handleSessionEnd(ctx, sessionId, feature));
6983
+ return c.json(await handleSessionEnd(ctx, session, feature));
6932
6984
  case "pre-tool":
6933
- return c.json(handlePreToolUse(session, feature, body.payload));
6985
+ return c.json(await handlePreToolUse(session, feature, body.payload));
6934
6986
  default:
6935
6987
  return c.json({});
6936
6988
  }
@@ -7072,6 +7124,166 @@ var init_web = __esm(() => {
7072
7124
  };
7073
7125
  });
7074
7126
 
7127
+ // src/version.ts
7128
+ import { readFileSync as readFileSync6 } from "fs";
7129
+ import { dirname as dirname5, join as join12 } from "path";
7130
+ import { fileURLToPath as fileURLToPath4 } from "url";
7131
+ function runcastleVersion() {
7132
+ if (cached2)
7133
+ return cached2;
7134
+ const here = dirname5(fileURLToPath4(import.meta.url));
7135
+ for (const pkgPath of [join12(here, "..", "package.json"), join12(here, "package.json")]) {
7136
+ try {
7137
+ const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
7138
+ if (typeof pkg.version === "string") {
7139
+ cached2 = pkg.version;
7140
+ return cached2;
7141
+ }
7142
+ } catch {}
7143
+ }
7144
+ cached2 = UNKNOWN_VERSION;
7145
+ return cached2;
7146
+ }
7147
+ var cached2, UNKNOWN_VERSION = "0.0.0";
7148
+ var init_version = () => {};
7149
+
7150
+ // src/services/install-id.ts
7151
+ import { randomUUID } from "crypto";
7152
+ import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
7153
+ import { dirname as dirname6 } from "path";
7154
+ function getInstallId() {
7155
+ const file = installIdPath();
7156
+ try {
7157
+ if (existsSync13(file)) {
7158
+ const existing = readFileSync7(file, "utf8").trim();
7159
+ if (UUID_RE.test(existing))
7160
+ return existing;
7161
+ }
7162
+ const id = randomUUID();
7163
+ mkdirSync6(dirname6(file), { recursive: true });
7164
+ writeFileSync2(file, `${id}
7165
+ `);
7166
+ return id;
7167
+ } catch {
7168
+ return randomUUID();
7169
+ }
7170
+ }
7171
+ var UUID_RE;
7172
+ var init_install_id = __esm(() => {
7173
+ init_paths();
7174
+ 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;
7175
+ });
7176
+
7177
+ // src/services/update-check.ts
7178
+ function parse(version) {
7179
+ const cleaned = version.trim().replace(/^v/, "");
7180
+ const [core = "", pre = ""] = cleaned.split("-", 2);
7181
+ const release2 = core.split(".").map((n) => Number.parseInt(n, 10) || 0);
7182
+ return { release: release2, pre: pre ? pre.split(".") : [] };
7183
+ }
7184
+ function compareSemver(a, b) {
7185
+ const pa = parse(a);
7186
+ const pb = parse(b);
7187
+ const len = Math.max(pa.release.length, pb.release.length);
7188
+ for (let i = 0;i < len; i++) {
7189
+ const diff = (pa.release[i] ?? 0) - (pb.release[i] ?? 0);
7190
+ if (diff !== 0)
7191
+ return diff;
7192
+ }
7193
+ if (pa.pre.length === 0 && pb.pre.length === 0)
7194
+ return 0;
7195
+ if (pa.pre.length === 0)
7196
+ return 1;
7197
+ if (pb.pre.length === 0)
7198
+ return -1;
7199
+ const preLen = Math.max(pa.pre.length, pb.pre.length);
7200
+ for (let i = 0;i < preLen; i++) {
7201
+ const x = pa.pre[i];
7202
+ const y = pb.pre[i];
7203
+ if (x === undefined)
7204
+ return -1;
7205
+ if (y === undefined)
7206
+ return 1;
7207
+ const nx = Number(x);
7208
+ const ny = Number(y);
7209
+ const bothNumeric = !Number.isNaN(nx) && !Number.isNaN(ny);
7210
+ const cmp = bothNumeric ? nx - ny : x < y ? -1 : x > y ? 1 : 0;
7211
+ if (cmp !== 0)
7212
+ return cmp;
7213
+ }
7214
+ return 0;
7215
+ }
7216
+ function doNotTrack(env) {
7217
+ const raw = env.DO_NOT_TRACK;
7218
+ return raw !== undefined && raw !== "" && raw !== "0";
7219
+ }
7220
+ async function latestFromNpm(fetchImpl) {
7221
+ try {
7222
+ const res = await fetchImpl(NPM_LATEST_URL);
7223
+ if (!res.ok)
7224
+ return null;
7225
+ const body = await res.json();
7226
+ return typeof body.version === "string" ? body.version : null;
7227
+ } catch {
7228
+ return null;
7229
+ }
7230
+ }
7231
+ async function latestFromPing(fetchImpl, current2) {
7232
+ try {
7233
+ const res = await fetchImpl(PING_URL, {
7234
+ method: "POST",
7235
+ headers: { "content-type": "application/json" },
7236
+ body: JSON.stringify({
7237
+ installId: getInstallId(),
7238
+ version: current2,
7239
+ platform: process.platform
7240
+ })
7241
+ });
7242
+ if (!res.ok)
7243
+ return null;
7244
+ const body = await res.json();
7245
+ return typeof body.latest === "string" ? body.latest : null;
7246
+ } catch {
7247
+ return null;
7248
+ }
7249
+ }
7250
+ async function checkForUpdate(opts) {
7251
+ const { current: current2 } = opts;
7252
+ const fetchImpl = opts.fetchImpl ?? fetch;
7253
+ const env = opts.env ?? process.env;
7254
+ if (current2 === UNKNOWN_VERSION)
7255
+ return NO_UPDATE(current2);
7256
+ const latest = doNotTrack(env) ? await latestFromNpm(fetchImpl) : await latestFromPing(fetchImpl, current2) ?? await latestFromNpm(fetchImpl);
7257
+ if (!latest)
7258
+ return NO_UPDATE(current2);
7259
+ return {
7260
+ current: current2,
7261
+ latest,
7262
+ updateAvailable: compareSemver(latest, current2) > 0,
7263
+ command: UPDATE_COMMAND
7264
+ };
7265
+ }
7266
+ async function getUpdateInfo(current2, fetchImpl) {
7267
+ if (cache)
7268
+ return cache;
7269
+ const info = await checkForUpdate({ current: current2, fetchImpl });
7270
+ if (info.latest !== null)
7271
+ cache = info;
7272
+ return info;
7273
+ }
7274
+ var PACKAGE_NAME = "runcastle", UPDATE_COMMAND, PING_URL = "https://ping.runcastle.dev/ping", NPM_LATEST_URL, NO_UPDATE = (current2) => ({
7275
+ current: current2,
7276
+ latest: null,
7277
+ updateAvailable: false,
7278
+ command: UPDATE_COMMAND
7279
+ }), cache;
7280
+ var init_update_check = __esm(() => {
7281
+ init_version();
7282
+ init_install_id();
7283
+ UPDATE_COMMAND = `bun add -g ${PACKAGE_NAME}@latest`;
7284
+ NPM_LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
7285
+ });
7286
+
7075
7287
  // src/trpc/context.ts
7076
7288
  import { initTRPC } from "@trpc/server";
7077
7289
  var t, errorMapping, router, publicProcedure, createCallerFactory;
@@ -7120,11 +7332,11 @@ var init_events2 = __esm(() => {
7120
7332
  });
7121
7333
 
7122
7334
  // src/launcher/artifacts.ts
7123
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync2 } from "fs";
7124
- import { fileURLToPath as fileURLToPath4 } from "url";
7125
- import { join as join12 } from "path";
7335
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync3 } from "fs";
7336
+ import { fileURLToPath as fileURLToPath5 } from "url";
7337
+ import { join as join13 } from "path";
7126
7338
  function hookClientPath() {
7127
- return resolveAsset(ASSET_ENV.hookClient, fileURLToPath4(new URL("./hook-client.ts", import.meta.url)));
7339
+ return resolveAsset(ASSET_ENV.hookClient, fileURLToPath5(new URL("./hook-client.ts", import.meta.url)));
7128
7340
  }
7129
7341
  function serverUrlFor(config2) {
7130
7342
  return `http://localhost:${config2.serverPort}`;
@@ -7546,16 +7758,16 @@ function renderMcpConfig(session, config2) {
7546
7758
  async function writeSessionArtifacts(input) {
7547
7759
  const { session, feature, config: config2, waypoint, prepare, projectBrief, lap } = input;
7548
7760
  const dir = sessionDir(session.id);
7549
- mkdirSync6(dir, { recursive: true });
7550
- const systemPromptPath = join12(dir, "system-prompt.md");
7551
- const settingsPath = join12(dir, "settings.json");
7552
- const mcpConfigPath = join12(dir, "mcp.json");
7761
+ mkdirSync7(dir, { recursive: true });
7762
+ const systemPromptPath = join13(dir, "system-prompt.md");
7763
+ const settingsPath = join13(dir, "settings.json");
7764
+ const mcpConfigPath = join13(dir, "mcp.json");
7553
7765
  const systemPrompt = feature ? renderSystemPrompt(feature, session.kind, waypoint, lap) : prepare ? renderPreparePrompt(prepare) : projectBrief ? renderProjectPrompt(projectBrief) : (() => {
7554
7766
  throw new Error(`session ${session.id} has no feature and no project-session brief`);
7555
7767
  })();
7556
- writeFileSync2(systemPromptPath, systemPrompt, "utf8");
7557
- writeFileSync2(settingsPath, JSON.stringify(renderSettings(hookClientPath(), session.kind), null, 2), "utf8");
7558
- writeFileSync2(mcpConfigPath, JSON.stringify(renderMcpConfig(session, config2), null, 2), "utf8");
7768
+ writeFileSync3(systemPromptPath, systemPrompt, "utf8");
7769
+ writeFileSync3(settingsPath, JSON.stringify(renderSettings(hookClientPath(), session.kind), null, 2), "utf8");
7770
+ writeFileSync3(mcpConfigPath, JSON.stringify(renderMcpConfig(session, config2), null, 2), "utf8");
7559
7771
  return { systemPromptPath, settingsPath, mcpConfigPath };
7560
7772
  }
7561
7773
  var RUNCASTLE_MCP_ALLOW_RULES, SESSION_BASH_READ_RULES, SESSION_BASH_WRITE_RULES, SESSION_BASH_ALLOW_RULES, SESSION_START_SOURCES;
@@ -7599,9 +7811,9 @@ var init_artifacts = __esm(() => {
7599
7811
  });
7600
7812
 
7601
7813
  // src/launcher/launcher.ts
7602
- import { existsSync as existsSync13 } from "fs";
7603
- import { dirname as dirname5, join as join13, resolve as resolve9 } from "path";
7604
- import { fileURLToPath as fileURLToPath5 } from "url";
7814
+ import { existsSync as existsSync14 } from "fs";
7815
+ import { dirname as dirname7, join as join14, resolve as resolve9 } from "path";
7816
+ import { fileURLToPath as fileURLToPath6 } from "url";
7605
7817
  import { and as and9, eq as eq11 } from "drizzle-orm";
7606
7818
  function ptyExitMessage(exitCode) {
7607
7819
  const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
@@ -7633,23 +7845,23 @@ function resolveClaudeExecutable() {
7633
7845
  function claudeSpawnTarget(claudeArgs) {
7634
7846
  return spawnTargetFor(resolveClaudeExecutable(), claudeArgs);
7635
7847
  }
7636
- function resolvePluginDir(fromDir = dirname5(fileURLToPath5(import.meta.url))) {
7637
- const rel = join13("packages", "skills", "packs", "runcastle");
7848
+ function resolvePluginDir(fromDir = dirname7(fileURLToPath6(import.meta.url))) {
7849
+ const rel = join14("packages", "skills", "packs", "runcastle");
7638
7850
  const override = process.env[SKILLS_DIR_ENV];
7639
7851
  if (override) {
7640
- const dir2 = join13(resolve9(override), "packs", "runcastle");
7641
- if (existsSync13(dir2))
7852
+ const dir2 = join14(resolve9(override), "packs", "runcastle");
7853
+ if (existsSync14(dir2))
7642
7854
  return dir2;
7643
7855
  throw new Error(`${SKILLS_DIR_ENV}=${override} has no plugin dir at ${dir2}`);
7644
7856
  }
7645
7857
  const searched = [];
7646
7858
  let dir = fromDir;
7647
7859
  for (let i = 0;i < 8; i += 1) {
7648
- const candidate = join13(dir, rel);
7860
+ const candidate = join14(dir, rel);
7649
7861
  searched.push(candidate);
7650
- if (existsSync13(candidate))
7862
+ if (existsSync14(candidate))
7651
7863
  return candidate;
7652
- const parent = dirname5(dir);
7864
+ const parent = dirname7(dir);
7653
7865
  if (parent === dir)
7654
7866
  break;
7655
7867
  dir = parent;
@@ -7718,6 +7930,8 @@ async function launchSession(ctx, input, opts = {}) {
7718
7930
  const session = createSessionRow(ctx, {
7719
7931
  featureId: feature.id,
7720
7932
  kind: input.kind,
7933
+ purpose: input.purpose,
7934
+ purposeData: input.purposeData,
7721
7935
  worktreePath
7722
7936
  });
7723
7937
  const plan = planKickoff({ kind: input.kind, lap: feature.lap, kickoffLine: input.kickoffLine });
@@ -8165,7 +8379,9 @@ var init_feature = __esm(() => {
8165
8379
  launchSession: publicProcedure.input(z6.object({
8166
8380
  featureId: z6.string(),
8167
8381
  kind: SessionKind,
8168
- kickoffLine: z6.string().min(1).optional()
8382
+ kickoffLine: z6.string().min(1).optional(),
8383
+ purpose: SessionPurpose.optional(),
8384
+ purposeData: MergeBranchPair.optional()
8169
8385
  })).mutation(({ ctx, input }) => launchSession(ctx, input)),
8170
8386
  workWaypoint: publicProcedure.input(z6.object({ featureId: z6.string(), waypointId: z6.string(), endLive: z6.boolean().optional() })).mutation(({ ctx, input }) => workWaypoint(ctx, input)),
8171
8387
  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 })),
@@ -8216,9 +8432,9 @@ var init_feature = __esm(() => {
8216
8432
  });
8217
8433
 
8218
8434
  // src/services/fsbrowse.ts
8219
- import { existsSync as existsSync14, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
8435
+ import { existsSync as existsSync15, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
8220
8436
  import { homedir as homedir2 } from "os";
8221
- import { basename, dirname as dirname6, isAbsolute as isAbsolute2, join as join14, resolve as resolve10, sep as sep3 } from "path";
8437
+ import { basename, dirname as dirname8, isAbsolute as isAbsolute2, join as join15, resolve as resolve10, sep as sep3 } from "path";
8222
8438
  function expandPath(input, home = homedir2()) {
8223
8439
  const trimmed = input.trim();
8224
8440
  if (trimmed === "")
@@ -8227,12 +8443,12 @@ function expandPath(input, home = homedir2()) {
8227
8443
  if (out === "~")
8228
8444
  out = home;
8229
8445
  else if (out.startsWith("~/") || out.startsWith("~\\"))
8230
- out = join14(home, out.slice(2));
8446
+ out = join15(home, out.slice(2));
8231
8447
  return resolve10(out);
8232
8448
  }
8233
8449
  function looksLikeRepo(dir) {
8234
8450
  try {
8235
- return existsSync14(join14(dir, ".git"));
8451
+ return existsSync15(join15(dir, ".git"));
8236
8452
  } catch {
8237
8453
  return false;
8238
8454
  }
@@ -8243,20 +8459,20 @@ function listRoots(home = homedir2()) {
8243
8459
  for (let code = 67;code <= 90; code++) {
8244
8460
  const drive = `${String.fromCharCode(code)}:${sep3}`;
8245
8461
  try {
8246
- if (existsSync14(drive))
8462
+ if (existsSync15(drive))
8247
8463
  roots.push({ label: drive.replace(/\\$/, ""), path: drive, kind: "drive" });
8248
8464
  } catch {}
8249
8465
  }
8250
8466
  } else {
8251
8467
  roots.push({ label: "/", path: "/", kind: "drive" });
8252
8468
  }
8253
- if (existsSync14(home))
8469
+ if (existsSync15(home))
8254
8470
  roots.push({ label: "Home", path: home, kind: "home" });
8255
8471
  const seen = new Set(roots.map(pathKey));
8256
8472
  for (const name of COMMON_DIRS) {
8257
- const candidate = join14(home, name);
8473
+ const candidate = join15(home, name);
8258
8474
  const key = pathKey({ path: candidate });
8259
- if (seen.has(key) || !existsSync14(candidate))
8475
+ if (seen.has(key) || !existsSync15(candidate))
8260
8476
  continue;
8261
8477
  seen.add(key);
8262
8478
  roots.push({ label: name, path: candidate, kind: "common" });
@@ -8271,7 +8487,7 @@ function crumbsFor(dir) {
8271
8487
  let cur = dir;
8272
8488
  for (;; ) {
8273
8489
  out.unshift({ name: basename(cur) || cur, path: cur });
8274
- const parent = dirname6(cur);
8490
+ const parent = dirname8(cur);
8275
8491
  if (parent === cur)
8276
8492
  break;
8277
8493
  cur = parent;
@@ -8279,7 +8495,7 @@ function crumbsFor(dir) {
8279
8495
  return out;
8280
8496
  }
8281
8497
  function parentOf(dir) {
8282
- const parent = dirname6(dir);
8498
+ const parent = dirname8(dir);
8283
8499
  return parent === dir ? null : parent;
8284
8500
  }
8285
8501
  function browseDir(input, showHidden = false, home = homedir2()) {
@@ -8287,13 +8503,13 @@ function browseDir(input, showHidden = false, home = homedir2()) {
8287
8503
  if (!isAbsolute2(target)) {
8288
8504
  throw new InvalidInputError(`path is not absolute: ${target}`);
8289
8505
  }
8290
- if (!existsSync14(target)) {
8506
+ if (!existsSync15(target)) {
8291
8507
  throw new InvalidInputError(`path does not exist: ${target}`);
8292
8508
  }
8293
8509
  let dir = target;
8294
8510
  try {
8295
8511
  if (!statSync2(target).isDirectory())
8296
- dir = dirname6(target);
8512
+ dir = dirname8(target);
8297
8513
  } catch (e) {
8298
8514
  throw new InvalidInputError(`cannot read path: ${target} (${errMsg4(e)})`);
8299
8515
  }
@@ -8315,7 +8531,7 @@ function browseDir(input, showHidden = false, home = homedir2()) {
8315
8531
  continue;
8316
8532
  if (!showHidden && name.startsWith("."))
8317
8533
  continue;
8318
- const path = join14(dir, name);
8534
+ const path = join15(dir, name);
8319
8535
  const isSymlink = dirent.isSymbolicLink();
8320
8536
  if (isSymlink && !showHidden)
8321
8537
  continue;
@@ -8352,8 +8568,8 @@ var init_fsbrowse = __esm(() => {
8352
8568
  });
8353
8569
 
8354
8570
  // src/services/projects.ts
8355
- import { existsSync as existsSync15 } from "fs";
8356
- import { basename as basename2, join as join15 } from "path";
8571
+ import { existsSync as existsSync16 } from "fs";
8572
+ import { basename as basename2, join as join16 } from "path";
8357
8573
  import { eq as eq12, isNull } from "drizzle-orm";
8358
8574
  function listProjects(ctx) {
8359
8575
  return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
@@ -8434,7 +8650,7 @@ async function assertRepoTolerant(ctx, repoPath) {
8434
8650
  } catch (e) {
8435
8651
  if (!isNotImplemented(e))
8436
8652
  throw e;
8437
- if (!existsSync15(join15(repoPath, ".git"))) {
8653
+ if (!existsSync16(join16(repoPath, ".git"))) {
8438
8654
  throw new InvalidInputError(`not a git repository: ${repoPath}`);
8439
8655
  }
8440
8656
  }
@@ -8507,15 +8723,15 @@ var init_run = __esm(() => {
8507
8723
  });
8508
8724
 
8509
8725
  // src/services/settings.ts
8510
- import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
8511
- import { dirname as dirname7 } from "path";
8726
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
8727
+ import { dirname as dirname9 } from "path";
8512
8728
  import * as z9 from "zod";
8513
8729
  import { eq as eq13 } from "drizzle-orm";
8514
8730
  function readRawConfig(configFile) {
8515
- if (!existsSync16(configFile))
8731
+ if (!existsSync17(configFile))
8516
8732
  return {};
8517
8733
  try {
8518
- const parsed = JSON.parse(readFileSync6(configFile, "utf8"));
8734
+ const parsed = JSON.parse(readFileSync8(configFile, "utf8"));
8519
8735
  if (typeof parsed !== "object" || parsed === null)
8520
8736
  return {};
8521
8737
  return foldLegacyModelConfig(parsed);
@@ -8654,8 +8870,8 @@ function updateSettings(ctx, input, io = {}) {
8654
8870
  function writeGlobal(configFile, configKey, value) {
8655
8871
  const raw = readRawConfig(configFile);
8656
8872
  raw[configKey] = value;
8657
- mkdirSync7(dirname7(configFile), { recursive: true });
8658
- writeFileSync3(configFile, `${JSON.stringify(raw, null, 2)}
8873
+ mkdirSync8(dirname9(configFile), { recursive: true });
8874
+ writeFileSync4(configFile, `${JSON.stringify(raw, null, 2)}
8659
8875
  `);
8660
8876
  }
8661
8877
  function updateStepModel(ctx, input, configFile, io) {
@@ -8693,8 +8909,8 @@ function writeStepModel(configFile, step, value) {
8693
8909
  else
8694
8910
  stepModels[step] = value;
8695
8911
  raw.stepModels = stepModels;
8696
- mkdirSync7(dirname7(configFile), { recursive: true });
8697
- writeFileSync3(configFile, `${JSON.stringify(raw, null, 2)}
8912
+ mkdirSync8(dirname9(configFile), { recursive: true });
8913
+ writeFileSync4(configFile, `${JSON.stringify(raw, null, 2)}
8698
8914
  `);
8699
8915
  }
8700
8916
  function field(view, key) {
@@ -9071,9 +9287,9 @@ var init_system_exec = __esm(() => {
9071
9287
  });
9072
9288
 
9073
9289
  // src/services/setup.ts
9074
- import { copyFileSync, existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync7, readdirSync as readdirSync6, writeFileSync as writeFileSync4 } from "fs";
9075
- import { dirname as dirname8, join as join16 } from "path";
9076
- import { fileURLToPath as fileURLToPath6 } from "url";
9290
+ import { copyFileSync, existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync9, readdirSync as readdirSync6, writeFileSync as writeFileSync5 } from "fs";
9291
+ import { dirname as dirname10, join as join17 } from "path";
9292
+ import { fileURLToPath as fileURLToPath7 } from "url";
9077
9293
  async function writeGitIdentity(exec, identity) {
9078
9294
  const name = identity.name.trim();
9079
9295
  const email = identity.email.trim();
@@ -9148,16 +9364,16 @@ async function resolveRuntime(exec, preferred) {
9148
9364
  function resolveSandcastleBin() {
9149
9365
  try {
9150
9366
  const mainUrl = import.meta.resolve("@ai-hero/sandcastle");
9151
- let dir = dirname8(fileURLToPath6(mainUrl));
9367
+ let dir = dirname10(fileURLToPath7(mainUrl));
9152
9368
  for (let hops = 0;hops < 6; hops++) {
9153
- const manifestPath = join16(dir, "package.json");
9154
- if (existsSync17(manifestPath)) {
9155
- const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
9369
+ const manifestPath = join17(dir, "package.json");
9370
+ if (existsSync18(manifestPath)) {
9371
+ const manifest = JSON.parse(readFileSync9(manifestPath, "utf8"));
9156
9372
  if (manifest.name === "@ai-hero/sandcastle" && manifest.bin?.sandcastle) {
9157
- return join16(dir, manifest.bin.sandcastle);
9373
+ return join17(dir, manifest.bin.sandcastle);
9158
9374
  }
9159
9375
  }
9160
- const parent = dirname8(dir);
9376
+ const parent = dirname10(dir);
9161
9377
  if (parent === dir)
9162
9378
  break;
9163
9379
  dir = parent;
@@ -9179,21 +9395,21 @@ function terminalSpec(kind, opts) {
9179
9395
  };
9180
9396
  }
9181
9397
  function sandcastleTemplateDir() {
9182
- return resolveAsset(ASSET_ENV.sandcastleTemplate, fileURLToPath6(new URL("../assets/sandcastle", import.meta.url)));
9398
+ return resolveAsset(ASSET_ENV.sandcastleTemplate, fileURLToPath7(new URL("../assets/sandcastle", import.meta.url)));
9183
9399
  }
9184
9400
  function scaffoldSandcastleConfig(templateDir, targetDir) {
9185
- const dir = join16(targetDir, ".sandcastle");
9186
- if (existsSync17(dir))
9401
+ const dir = join17(targetDir, ".sandcastle");
9402
+ if (existsSync18(dir))
9187
9403
  return { scaffolded: false, dir };
9188
- mkdirSync8(dir, { recursive: true });
9404
+ mkdirSync9(dir, { recursive: true });
9189
9405
  for (const name of readdirSync6(templateDir)) {
9190
- copyFileSync(join16(templateDir, name), join16(dir, name));
9406
+ copyFileSync(join17(templateDir, name), join17(dir, name));
9191
9407
  }
9192
9408
  return { scaffolded: true, dir };
9193
9409
  }
9194
9410
  function prepareSandboxBuildContext() {
9195
9411
  const target = sandboxBuildDir();
9196
- mkdirSync8(target, { recursive: true });
9412
+ mkdirSync9(target, { recursive: true });
9197
9413
  scaffoldSandcastleConfig(sandcastleTemplateDir(), target);
9198
9414
  return target;
9199
9415
  }
@@ -9229,10 +9445,10 @@ function createTokenVerifier(exec) {
9229
9445
  function fileAfkTokenIo(exec) {
9230
9446
  const path = envPath();
9231
9447
  return {
9232
- read: () => existsSync17(path) ? readFileSync7(path, "utf8") : "",
9448
+ read: () => existsSync18(path) ? readFileSync9(path, "utf8") : "",
9233
9449
  write: (content) => {
9234
- mkdirSync8(dirname8(path), { recursive: true });
9235
- writeFileSync4(path, content, "utf8");
9450
+ mkdirSync9(dirname10(path), { recursive: true });
9451
+ writeFileSync5(path, content, "utf8");
9236
9452
  },
9237
9453
  verify: createTokenVerifier(exec)
9238
9454
  };
@@ -9285,110 +9501,6 @@ var init_setup2 = __esm(() => {
9285
9501
  });
9286
9502
  });
9287
9503
 
9288
- // src/version.ts
9289
- import { readFileSync as readFileSync8 } from "fs";
9290
- import { dirname as dirname9, join as join17 } from "path";
9291
- import { fileURLToPath as fileURLToPath7 } from "url";
9292
- function runcastleVersion() {
9293
- if (cached2)
9294
- return cached2;
9295
- const here = dirname9(fileURLToPath7(import.meta.url));
9296
- for (const pkgPath of [join17(here, "..", "package.json"), join17(here, "package.json")]) {
9297
- try {
9298
- const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
9299
- if (typeof pkg.version === "string") {
9300
- cached2 = pkg.version;
9301
- return cached2;
9302
- }
9303
- } catch {}
9304
- }
9305
- cached2 = UNKNOWN_VERSION;
9306
- return cached2;
9307
- }
9308
- var cached2, UNKNOWN_VERSION = "0.0.0";
9309
- var init_version = () => {};
9310
-
9311
- // src/services/update-check.ts
9312
- function parse(version) {
9313
- const cleaned = version.trim().replace(/^v/, "");
9314
- const [core = "", pre = ""] = cleaned.split("-", 2);
9315
- const release2 = core.split(".").map((n) => Number.parseInt(n, 10) || 0);
9316
- return { release: release2, pre: pre ? pre.split(".") : [] };
9317
- }
9318
- function compareSemver(a, b) {
9319
- const pa = parse(a);
9320
- const pb = parse(b);
9321
- const len = Math.max(pa.release.length, pb.release.length);
9322
- for (let i = 0;i < len; i++) {
9323
- const diff = (pa.release[i] ?? 0) - (pb.release[i] ?? 0);
9324
- if (diff !== 0)
9325
- return diff;
9326
- }
9327
- if (pa.pre.length === 0 && pb.pre.length === 0)
9328
- return 0;
9329
- if (pa.pre.length === 0)
9330
- return 1;
9331
- if (pb.pre.length === 0)
9332
- return -1;
9333
- const preLen = Math.max(pa.pre.length, pb.pre.length);
9334
- for (let i = 0;i < preLen; i++) {
9335
- const x = pa.pre[i];
9336
- const y = pb.pre[i];
9337
- if (x === undefined)
9338
- return -1;
9339
- if (y === undefined)
9340
- return 1;
9341
- const nx = Number(x);
9342
- const ny = Number(y);
9343
- const bothNumeric = !Number.isNaN(nx) && !Number.isNaN(ny);
9344
- const cmp = bothNumeric ? nx - ny : x < y ? -1 : x > y ? 1 : 0;
9345
- if (cmp !== 0)
9346
- return cmp;
9347
- }
9348
- return 0;
9349
- }
9350
- async function checkForUpdate(opts) {
9351
- const { current: current2 } = opts;
9352
- const fetchImpl = opts.fetchImpl ?? fetch;
9353
- if (current2 === UNKNOWN_VERSION)
9354
- return NO_UPDATE(current2);
9355
- try {
9356
- const res = await fetchImpl(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`);
9357
- if (!res.ok)
9358
- return NO_UPDATE(current2);
9359
- const body = await res.json();
9360
- const latest = typeof body.version === "string" ? body.version : null;
9361
- if (!latest)
9362
- return NO_UPDATE(current2);
9363
- return {
9364
- current: current2,
9365
- latest,
9366
- updateAvailable: compareSemver(latest, current2) > 0,
9367
- command: UPDATE_COMMAND
9368
- };
9369
- } catch {
9370
- return NO_UPDATE(current2);
9371
- }
9372
- }
9373
- async function getUpdateInfo(current2, fetchImpl) {
9374
- if (cache)
9375
- return cache;
9376
- const info = await checkForUpdate({ current: current2, fetchImpl });
9377
- if (info.latest !== null)
9378
- cache = info;
9379
- return info;
9380
- }
9381
- var PACKAGE_NAME = "runcastle", UPDATE_COMMAND, NO_UPDATE = (current2) => ({
9382
- current: current2,
9383
- latest: null,
9384
- updateAvailable: false,
9385
- command: UPDATE_COMMAND
9386
- }), cache;
9387
- var init_update_check = __esm(() => {
9388
- init_version();
9389
- UPDATE_COMMAND = `bun add -g ${PACKAGE_NAME}@latest`;
9390
- });
9391
-
9392
9504
  // src/trpc/routers/system.ts
9393
9505
  var systemRouter;
9394
9506
  var init_system = __esm(() => {
@@ -9402,8 +9514,8 @@ var init_system = __esm(() => {
9402
9514
  });
9403
9515
 
9404
9516
  // src/services/test-notes.ts
9405
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync5 } from "fs";
9406
- import { dirname as dirname10 } from "path";
9517
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync6 } from "fs";
9518
+ import { dirname as dirname11 } from "path";
9407
9519
  import { asc as asc4, eq as eq14 } from "drizzle-orm";
9408
9520
  function rowToNote(row) {
9409
9521
  return {
@@ -9548,8 +9660,8 @@ function renderTestNotes(ctx, feature) {
9548
9660
  lines.push(noteLine(ctx, note));
9549
9661
  }
9550
9662
  const path = featureDocPath(projectForFeature(ctx, feature), feature, "test-notes.md");
9551
- mkdirSync9(dirname10(path), { recursive: true });
9552
- writeFileSync5(path, `${lines.join(`
9663
+ mkdirSync10(dirname11(path), { recursive: true });
9664
+ writeFileSync6(path, `${lines.join(`
9553
9665
  `)}
9554
9666
  `, "utf8");
9555
9667
  }
@@ -9748,6 +9860,7 @@ async function startServer() {
9748
9860
  websocket: terminalWebSocket
9749
9861
  });
9750
9862
  console.log(`runcastle${process.env.RUNCASTLE_DEV ? " [dev]" : ""} server listening on ` + `http://localhost:${config2.serverPort} \u2014 data dir: ${dataDir()}`);
9863
+ getUpdateInfo(runcastleVersion()).catch(() => {});
9751
9864
  const SHUTDOWN_KEY = Symbol.for("runcastle.shutdown.wired");
9752
9865
  const g = globalThis;
9753
9866
  if (!g[SHUTDOWN_KEY]) {
@@ -9773,7 +9886,9 @@ var init_src2 = __esm(() => {
9773
9886
  init_hooks();
9774
9887
  init_stream();
9775
9888
  init_web();
9889
+ init_update_check();
9776
9890
  init_router();
9891
+ init_version();
9777
9892
  init_reconcile_runs();
9778
9893
  });
9779
9894
  init_src2();