@agent-team-foundation/first-tree-hub 0.12.0 → 0.12.2

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.
@@ -2,10 +2,10 @@ import { a as __toCommonJS, o as __toESM, t as __commonJSMin } from "./chunk-BSw
2
2
  import { A as FIRST_TREE_HUB_ATTR, C as stampOrgScope, D as untrustedAttrs, E as startWsConnectionSpan, M as require_pino, O as withSpan, S as stampChatResource, _ as rootLogger$1, a as buildRateLimitError, c as currentTraceId, g as reportErrorToRoot, i as bodyCaptureOnSendHook, j as redactUrl, k as withWsMessageSpan, l as decodeJwtForTrace, m as observabilityPlugin, n as applyLoggerConfig, o as classifyJoseError, r as attachRequestContext, s as createLogger$1, t as adapterAttrs, u as endWsConnectionSpan, x as stampAgentResource, y as setWsConnectionAttrs } from "./observability-BAScT_5S-BcW9HgkG.mjs";
3
3
  import { C as resetConfigMeta, E as setConfigValue, S as resetConfig, T as serverConfigSchema, b as migrateLegacyHome, c as resolveServerUrl, d as DEFAULT_CONFIG_DIR, f as DEFAULT_DATA_DIR$1, g as collectMissingPrompts, h as clientConfigSchema, i as ensureFreshAccessToken, l as saveAgentConfig, m as agentConfigSchema, o as loadCredentials, p as DEFAULT_HOME_DIR$1, u as saveCredentials, v as initConfig, w as resolveConfigReadonly, y as loadAgents } from "./bootstrap-C_K2CKXC.mjs";
4
4
  import { a as print, i as blank, n as CLI_USER_AGENT, r as COMMAND_VERSION, s as status, t as cliFetch } from "./cli-fetch--tiwKm5S.mjs";
5
- import { $ as loginSchema, A as createAgentSchema, At as updateTaskStatusSchema, B as githubCallbackQuerySchema, C as agentTypeSchema$1, Ct as updateAdapterConfigSchema, D as contextTreeSnapshotSchema, Dt as updateClientCapabilitiesSchema, E as connectTokenExchangeSchema, Et as updateChatSchema, F as createTaskSchema, G as inboxDeliverFrameSchema$1, H as githubStartQuerySchema, I as defaultRuntimeConfigPayload, J as isRedactedEnvValue, K as inboxPollQuerySchema, L as delegateFeishuUserSchema, M as createMeChatSchema, N as createMemberSchema, O as createAdapterConfigSchema, Ot as updateMemberSchema, P as createOrgFromMeSchema, Q as listMeChatsQuerySchema, R as dryRunAgentRuntimeConfigSchema, S as agentRuntimeConfigPayloadSchema$1, St as taskListQuerySchema, T as clientRegisterSchema, Tt as updateAgentSchema, U as imageInlineContentSchema, V as githubDevCallbackQuerySchema, W as inboxAckFrameSchema, X as joinByInvitationSchema, Y as isReservedAgentName$1, Z as linkTaskChatSchema, _ as addParticipantSchema, _t as sessionEventSchema$1, a as AGENT_STATUSES, b as agentBindRequestSchema, bt as stripCode, ct as refreshTokenSchema, d as TASK_CREATOR_TYPES, et as messageSourceSchema$1, f as TASK_HEALTH_SIGNALS, ft as selfServiceFeishuBotSchema, g as addMeChatParticipantsSchema, gt as sessionEventMessageSchema, h as WS_AUTH_FRAME_TIMEOUT_MS, ht as sessionCompletionMessageSchema, i as AGENT_SOURCES, it as patchOnboardingSchema, j as createChatSchema, jt as wsAuthFrameSchema, k as createAdapterMappingSchema, kt as updateOrganizationSchema, l as MENTION_REGEX, lt as runtimeStateMessageSchema, m as TASK_TERMINAL_STATUSES, mt as sendToAgentSchema, n as AGENT_NAME_REGEX$1, nt as onboardingEventSchema, o as AGENT_TYPES, p as TASK_STATUSES, pt as sendMessageSchema, q as isOrgSettingNamespace, r as AGENT_SELECTOR_HEADER$1, rt as paginationQuerySchema, s as AGENT_VISIBILITY, st as rebindAgentSchema, t as AGENT_BIND_REJECT_REASONS, tt as notificationQuerySchema, u as ORG_SETTINGS_NAMESPACES$1, ut as safeRedirectPath, v as adminCreateTaskSchema, vt as sessionReconcileRequestSchema, wt as updateAgentRuntimeConfigSchema, x as agentPinnedMessageSchema$1, xt as submitQuestionAnswerSchema, y as adminUpdateTaskSchema, yt as sessionStateMessageSchema } from "./dist-UOZ6vMUW.mjs";
5
+ import { $ as loginSchema, A as createAgentSchema, At as updateTaskStatusSchema, B as githubCallbackQuerySchema, C as agentTypeSchema$1, Ct as updateAdapterConfigSchema, D as contextTreeSnapshotSchema, Dt as updateClientCapabilitiesSchema, E as connectTokenExchangeSchema, Et as updateChatSchema, F as createTaskSchema, G as inboxDeliverFrameSchema$1, H as githubStartQuerySchema, I as defaultRuntimeConfigPayload, J as isRedactedEnvValue, K as inboxPollQuerySchema, L as delegateFeishuUserSchema, M as createMeChatSchema, N as createMemberSchema, O as createAdapterConfigSchema, Ot as updateMemberSchema, P as createOrgFromMeSchema, Q as listMeChatsQuerySchema, R as dryRunAgentRuntimeConfigSchema, S as agentRuntimeConfigPayloadSchema$1, St as taskListQuerySchema, T as clientRegisterSchema, Tt as updateAgentSchema, U as imageInlineContentSchema, V as githubDevCallbackQuerySchema, W as inboxAckFrameSchema, X as joinByInvitationSchema, Y as isReservedAgentName$1, Z as linkTaskChatSchema, _ as addParticipantSchema, _t as sessionEventSchema$1, a as AGENT_STATUSES, b as agentBindRequestSchema, bt as stripCode, ct as refreshTokenSchema, d as TASK_CREATOR_TYPES, et as messageSourceSchema$1, f as TASK_HEALTH_SIGNALS, ft as selfServiceFeishuBotSchema, g as addMeChatParticipantsSchema, gt as sessionEventMessageSchema, h as WS_AUTH_FRAME_TIMEOUT_MS, ht as sessionCompletionMessageSchema, i as AGENT_SOURCES, it as patchOnboardingSchema, j as createChatSchema, jt as wsAuthFrameSchema, k as createAdapterMappingSchema, kt as updateOrganizationSchema, l as MENTION_REGEX, lt as runtimeStateMessageSchema, m as TASK_TERMINAL_STATUSES, mt as sendToAgentSchema, n as AGENT_NAME_REGEX$1, nt as onboardingEventSchema, o as AGENT_TYPES, p as TASK_STATUSES, pt as sendMessageSchema, q as isOrgSettingNamespace, r as AGENT_SELECTOR_HEADER$1, rt as paginationQuerySchema, s as AGENT_VISIBILITY, st as rebindAgentSchema, t as AGENT_BIND_REJECT_REASONS, tt as notificationQuerySchema, u as ORG_SETTINGS_NAMESPACES$1, ut as safeRedirectPath, v as adminCreateTaskSchema, vt as sessionReconcileRequestSchema, wt as updateAgentRuntimeConfigSchema, x as agentPinnedMessageSchema$1, xt as submitQuestionAnswerSchema, y as adminUpdateTaskSchema, yt as sessionStateMessageSchema } from "./dist-DHHd2dar.mjs";
6
6
  import { a as ConflictError, c as UnauthorizedError, i as ClientUserMismatchError$1, l as organizations, n as BadRequestError, o as ForbiddenError, r as ClientOrgMismatchError$1, s as NotFoundError, t as AppError, u as users } from "./errors-CF5evtJt-B0NTIVPt.mjs";
7
7
  import { n as init_esm, r as trace, t as esm_exports } from "./esm-iadMkGbV.mjs";
8
- import { $ as pendingQuestions, A as heartbeatClient, B as listAgentsWithRuntime, C as findOrCreateDirectChat, D as getClient, E as getChatDetail, F as joinChat, G as listClientsForOrgAdmin, H as listChats, I as leaveAsParticipant, J as markStaleAgents, K as listMessages, L as leaveChat, M as inboxEntries, N as invalidateChatAudience, O as getOnlineCount, P as joinAsParticipant, Q as notifyRecipients, R as listActiveAgentsPinnedToClient, S as ensureParticipant$1, T as getCachedAudience, U as listChatsForMember, V as listChatParticipantsWithNames, W as listClients, X as members, Y as markSupersededByChat, Z as messages, _ as createNotifier, _t as updateClientCapabilities, a as agents, at as removeParticipant, b as editMessage, c as bindAgent, ct as retireClient, d as chats, dt as serverInstances, et as recomputeChatWatchers, f as claimClient, ft as setOffline, g as createChat, gt as unbindAgent, h as clients, ht as touchAgent, i as agentVisibilityCondition, it as registerClient, j as heartbeatInstance, k as getPresence, l as chatParticipants, lt as sendMessage, m as cleanupStalePresence, mt as submitAnswer, n as agentChatSessions, nt as recomputeWatchersForMember, o as assertClientOwner, ot as resetActivity, p as cleanupStaleClients, pt as setRuntimeState, r as agentPresence, rt as registerChatMessageDispatcher, s as assertParticipant, st as resolveChatMembership, t as addParticipant, tt as recomputeWatchersForAgent, u as chatSubscriptions, ut as sendToAgent$1, v as deriveAuthState, vt as upsertSessionState, w as getActivityOverview, x as ensureCanJoin, y as disconnectClient, z as listAgentsManagedByUser } from "./client-BPUdUaZT-CyCrpCTP.mjs";
8
+ import { $ as pendingQuestions, A as heartbeatClient, B as listAgentsWithRuntime, C as findOrCreateDirectChat, D as getClient, E as getChatDetail, F as joinChat, G as listClientsForOrgAdmin, H as listChats, I as leaveAsParticipant, J as markStaleAgents, K as listMessages, L as leaveChat, M as inboxEntries, N as invalidateChatAudience, O as getOnlineCount, P as joinAsParticipant, Q as notifyRecipients, R as listActiveAgentsPinnedToClient, S as ensureParticipant$1, T as getCachedAudience, U as listChatsForMember, V as listChatParticipantsWithNames, W as listClients, X as members, Y as markSupersededByChat, Z as messages, _ as createNotifier, _t as updateClientCapabilities, a as agents, at as removeParticipant, b as editMessage, c as bindAgent, ct as retireClient, d as chats, dt as serverInstances, et as recomputeChatWatchers, f as claimClient, ft as setOffline, g as createChat, gt as unbindAgent, h as clients, ht as touchAgent, i as agentVisibilityCondition, it as registerClient, j as heartbeatInstance, k as getPresence, l as chatParticipants, lt as sendMessage, m as cleanupStalePresence, mt as submitAnswer, n as agentChatSessions, nt as recomputeWatchersForMember, o as assertClientOwner, ot as resetActivity, p as cleanupStaleClients, pt as setRuntimeState, r as agentPresence, rt as registerChatMessageDispatcher, s as assertParticipant, st as resolveChatMembership, t as addParticipant, tt as recomputeWatchersForAgent, u as chatSubscriptions, ut as sendToAgent$1, v as deriveAuthState, vt as upsertSessionState, w as getActivityOverview, x as ensureCanJoin, y as disconnectClient, z as listAgentsManagedByUser } from "./client-DHCSQ8kg-DjlSmE9q.mjs";
9
9
  import { a as invitationRedemptions, c as recordRedemption, i as getActiveInvitation, l as rotateInvitation, n as ensureActiveInvitation, o as invitations, r as findActiveByToken, t as buildInviteUrl, u as uuidv7 } from "./invitation-Bg0TRiyx-BsZH4GCS.mjs";
10
10
  import { createRequire } from "node:module";
11
11
  import { ZodError, z } from "zod";
@@ -1365,26 +1365,51 @@ z.object({
1365
1365
  * 2. Add a key to `ORG_SETTINGS_NAMESPACES`.
1366
1366
  * 3. Done. No DB migration, no new API route.
1367
1367
  */
1368
- const httpsRepoUrlSchema = z.string().url().refine((value) => {
1368
+ const SCP_LIKE_SSH_RE = /^(?:[A-Za-z0-9_.-]+@)?[A-Za-z0-9.-]+:(?!\d+(?:\/|$))[^/:@\s][^:@\s]*$/;
1369
+ function isScpLikeSshUrl(value) {
1370
+ if (value.includes("://")) return false;
1371
+ return SCP_LIKE_SSH_RE.test(value);
1372
+ }
1373
+ const repoUrlSchema = z.string().min(1).superRefine((value, ctx) => {
1374
+ if (isScpLikeSshUrl(value)) return;
1375
+ let url;
1369
1376
  try {
1370
- return new URL(value).protocol === "https:";
1377
+ url = new URL(value);
1371
1378
  } catch {
1372
- return false;
1379
+ ctx.addIssue({
1380
+ code: z.ZodIssueCode.custom,
1381
+ message: "Repo URL must be HTTPS, SSH (ssh://...), or scp-like (git@host:path)."
1382
+ });
1383
+ return;
1373
1384
  }
1374
- }, { message: "Repo URL must use HTTPS." }).refine((value) => {
1375
- try {
1376
- const url = new URL(value);
1377
- return url.username.length === 0 && url.password.length === 0;
1378
- } catch {
1379
- return false;
1385
+ if (url.protocol !== "https:" && url.protocol !== "ssh:") {
1386
+ ctx.addIssue({
1387
+ code: z.ZodIssueCode.custom,
1388
+ message: "Repo URL must use HTTPS or SSH."
1389
+ });
1390
+ return;
1391
+ }
1392
+ if (url.password.length > 0) {
1393
+ ctx.addIssue({
1394
+ code: z.ZodIssueCode.custom,
1395
+ message: "Repo URL must not include credentials."
1396
+ });
1397
+ return;
1398
+ }
1399
+ if (url.protocol === "https:" && url.username.length > 0) {
1400
+ ctx.addIssue({
1401
+ code: z.ZodIssueCode.custom,
1402
+ message: "Repo URL must not include credentials."
1403
+ });
1404
+ return;
1380
1405
  }
1381
- }, { message: "Repo URL must not include credentials." });
1406
+ });
1382
1407
  const orgContextTreeStorageSchema = z.object({
1383
- repo: httpsRepoUrlSchema.optional(),
1408
+ repo: repoUrlSchema.optional(),
1384
1409
  branch: z.string().default("main")
1385
1410
  });
1386
1411
  const orgContextTreeInputSchema = z.object({
1387
- repo: httpsRepoUrlSchema.nullish(),
1412
+ repo: repoUrlSchema.nullish(),
1388
1413
  branch: z.string().min(1).nullish()
1389
1414
  });
1390
1415
  const orgContextTreeOutputSchema = z.object({
@@ -1398,11 +1423,11 @@ const orgGithubIntegrationOutputSchema = z.object({
1398
1423
  webhookUrl: z.string()
1399
1424
  });
1400
1425
  const orgSourceReposStorageSchema = z.object({ repos: z.array(z.object({
1401
- url: httpsRepoUrlSchema,
1426
+ url: repoUrlSchema,
1402
1427
  defaultBranch: z.string().optional()
1403
1428
  })).default([]) });
1404
1429
  const orgSourceReposInputSchema = z.object({ repos: z.array(z.object({
1405
- url: httpsRepoUrlSchema,
1430
+ url: repoUrlSchema,
1406
1431
  defaultBranch: z.string().min(1).optional()
1407
1432
  })).optional() });
1408
1433
  const orgSourceReposOutputSchema = z.object({ repos: z.array(z.object({
@@ -3029,8 +3054,65 @@ For content with quotes, \`$\`, backticks, or newlines, prefer stdin to avoid sh
3029
3054
  const DEFAULT_CLONE_TIMEOUT_MS = 300 * 1e3;
3030
3055
  const FETCH_REFSPEC = "+refs/heads/*:refs/remotes/origin/*";
3031
3056
  const SESSION_BRANCH_PREFIX = "hub-session";
3057
+ /**
3058
+ * Hash a repo URL into the mirror directory name.
3059
+ *
3060
+ * The hash is computed over a *canonical* form (host + path, lowercased
3061
+ * host, no `.git` suffix, no leading/trailing slash, no protocol, no
3062
+ * `user@` prefix), so the same upstream repo addressed via any of the
3063
+ * accepted forms (HTTPS, `ssh://`, or scp-like) all land in the same
3064
+ * mirror dir. That matters because:
3065
+ * - admins may write any of those three forms into `source_repos[].url`
3066
+ * (the schema accepts all of them)
3067
+ * - the `fetchOrigin` fallback below transparently swaps protocols when
3068
+ * credentials are missing — without canonical hashing, the fallback
3069
+ * would silently maintain a second mirror dir
3070
+ *
3071
+ * Migration cost: pre-existing mirrors created before this change use the
3072
+ * raw-URL hash and will be orphaned after the upgrade. `gcMirrors` removes
3073
+ * them on the next run; the next fetch repopulates the canonical-keyed
3074
+ * mirror. No data loss — mirror is a cache, not state.
3075
+ */
3032
3076
  function hashUrl(url) {
3033
- return createHash("sha256").update(url).digest("hex").slice(0, 32);
3077
+ return createHash("sha256").update(canonicalizeRepoUrl(url)).digest("hex").slice(0, 32);
3078
+ }
3079
+ /**
3080
+ * Reduce a repo URL to `<host[:port]>/<path-without-.git>`. Used as the
3081
+ * input to the mirror dir hash and exposed for unit testing.
3082
+ *
3083
+ * `https://github.com/foo/bar.git` → `github.com/foo/bar`
3084
+ * `git@github.com:foo/bar.git` → `github.com/foo/bar`
3085
+ * `ssh://git@github.com/foo/bar.git` → `github.com/foo/bar`
3086
+ * `ssh://git@gitlab.example.com:2222/x/y` → `gitlab.example.com:2222/x/y`
3087
+ *
3088
+ * Falls back to the raw input for un-parseable strings — better to keep
3089
+ * a stable mirror than to throw mid-bootstrap.
3090
+ */
3091
+ function canonicalizeRepoUrl(url) {
3092
+ if (!url.includes("://")) {
3093
+ const m = url.match(/^(?:[A-Za-z0-9_.-]+@)?([A-Za-z0-9.-]+):([^/@:\s][^@:\s]*)$/);
3094
+ const host = m?.[1];
3095
+ const rawPath = m?.[2];
3096
+ if (host && rawPath && !/^\d+(?:\/|$)/.test(rawPath)) {
3097
+ const path = normalizePath(rawPath);
3098
+ return `${host.toLowerCase()}/${path}`;
3099
+ }
3100
+ }
3101
+ try {
3102
+ const parsed = new URL(url);
3103
+ const host = parsed.hostname.toLowerCase();
3104
+ return `${parsed.port === "" || parsed.protocol === "https:" && parsed.port === "443" || parsed.protocol === "ssh:" && parsed.port === "22" ? host : `${host}:${parsed.port}`}/${normalizePath(parsed.pathname)}`;
3105
+ } catch {
3106
+ return url;
3107
+ }
3108
+ }
3109
+ /**
3110
+ * Strip leading slashes, trailing slashes, and a trailing `.git`. Used by
3111
+ * `canonicalizeRepoUrl` so that `…/foo/bar`, `…/foo/bar/`, and `…/foo/bar.git`
3112
+ * all collapse to the same canonical path (and therefore the same mirror dir).
3113
+ */
3114
+ function normalizePath(rawPath) {
3115
+ return rawPath.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "");
3034
3116
  }
3035
3117
  function shortHash(input) {
3036
3118
  return createHash("sha256").update(input).digest("hex").slice(0, 8);
@@ -3067,10 +3149,14 @@ function createGitMirrorManager(opts) {
3067
3149
  }
3068
3150
  async function git(args, cwd, timeoutMs, env) {
3069
3151
  const start = Date.now();
3152
+ const finalEnv = {
3153
+ GIT_TERMINAL_PROMPT: "0",
3154
+ ...env ?? process.env
3155
+ };
3070
3156
  return await new Promise((resolveExec, rejectExec) => {
3071
3157
  const proc = spawn("git", args, {
3072
3158
  cwd: cwd ?? void 0,
3073
- env: env ?? process.env,
3159
+ env: finalEnv,
3074
3160
  stdio: [
3075
3161
  "ignore",
3076
3162
  "pipe",
@@ -3114,6 +3200,116 @@ function createGitMirrorManager(opts) {
3114
3200
  }
3115
3201
  }
3116
3202
  /**
3203
+ * `git fetch --prune origin` with one-shot bidirectional protocol fallback.
3204
+ *
3205
+ * Decides direction from `originUrl`'s protocol:
3206
+ * - HTTPS origin → on credential-shaped failure, retry as SSH
3207
+ * - SSH origin → on credential-shaped failure, retry as HTTPS
3208
+ * The fallback fires only when **all** hold:
3209
+ * 1. The first attempt failed with a credential-shaped error in the
3210
+ * direction-appropriate sense (`isLikelyHttpsAuthFailure` /
3211
+ * `isLikelySshAuthFailure`). Network failures, missing-ref errors,
3212
+ * and TLS/host-key surprises propagate as-is — those won't be cured
3213
+ * by switching transports and silently retrying would mask real bugs.
3214
+ * 2. The origin URL maps to a usable peer-protocol form
3215
+ * (see `httpsToSshBaseRewrite` / `sshToHttpsBaseRewrite`). Custom
3216
+ * ports beyond the protocol default disable the rewrite — there's no
3217
+ * universal mapping between `https://host:8443` and an SSH peer.
3218
+ *
3219
+ * Implementation: the retry uses `git -c url.<peer-base>.insteadOf=<origin-base>`
3220
+ * — git resolves origin's URL through that rewrite for the duration of
3221
+ * one subprocess and never persists anything to disk. The mirror's
3222
+ * `remote.origin.url` stays as configured, so the next fetch starts back
3223
+ * at the original protocol unless this fallback fires again.
3224
+ */
3225
+ /**
3226
+ * `remote set-head --auto` is a remote-talking op too — under the same
3227
+ * credential rules as `fetch`. If we don't apply the same fallback,
3228
+ * mirrors whose HTTPS creds are missing end up with `refs/remotes/origin/HEAD`
3229
+ * unset, which then breaks `resolveBase()` for callers without an explicit
3230
+ * `ref` (the default-branch lookup throws).
3231
+ *
3232
+ * Strategy mirrors `fetchOrigin`: try the configured protocol first, fall
3233
+ * back to the peer protocol once via `-c url.<peer>.insteadOf=<origin>`.
3234
+ * Returns true on either success, false if both attempts failed (which
3235
+ * mirrors the historical `gitOk` semantics — non-fatal). No `GitMirrorAuthError`
3236
+ * here: callers that need origin/HEAD already get a clear
3237
+ * `GitMirrorError("Cannot resolve default branch …")` if both attempts fail.
3238
+ */
3239
+ async function setHeadAuto(mirrorPath, originUrl) {
3240
+ if (await gitOk([
3241
+ "remote",
3242
+ "set-head",
3243
+ "origin",
3244
+ "--auto"
3245
+ ], mirrorPath, 3e4)) return true;
3246
+ const direction = pickFallbackDirection(originUrl);
3247
+ if (!direction) return false;
3248
+ return await gitOk([
3249
+ "-c",
3250
+ `url.${direction.peerBase}.insteadOf=${direction.originBase}`,
3251
+ "remote",
3252
+ "set-head",
3253
+ "origin",
3254
+ "--auto"
3255
+ ], mirrorPath, 3e4);
3256
+ }
3257
+ async function readOriginUrl(mirrorPath) {
3258
+ try {
3259
+ const { stdout } = await git([
3260
+ "config",
3261
+ "--get",
3262
+ "remote.origin.url"
3263
+ ], mirrorPath, 1e4);
3264
+ return stdout.trim() || null;
3265
+ } catch {
3266
+ return null;
3267
+ }
3268
+ }
3269
+ async function fetchOrigin(mirrorPath, originUrl) {
3270
+ const direction = pickFallbackDirection(originUrl);
3271
+ try {
3272
+ const { elapsedMs } = await git([
3273
+ "fetch",
3274
+ "--prune",
3275
+ "origin"
3276
+ ], mirrorPath, cloneTimeoutMs);
3277
+ return {
3278
+ elapsedMs,
3279
+ usedFallback: false
3280
+ };
3281
+ } catch (primaryErr) {
3282
+ const primaryMessage = primaryErr instanceof Error ? primaryErr.message : String(primaryErr);
3283
+ if (!direction || !direction.shouldRetry(primaryMessage)) throw primaryErr;
3284
+ log?.info({
3285
+ gitUrl: originUrl,
3286
+ fromProtocol: direction.fromProtocol,
3287
+ toProtocol: direction.toProtocol,
3288
+ peerBase: direction.peerBase
3289
+ }, "fetch failed with credential-shaped error; retrying with peer-protocol insteadOf rewrite");
3290
+ try {
3291
+ const { elapsedMs } = await git([
3292
+ "-c",
3293
+ `url.${direction.peerBase}.insteadOf=${direction.originBase}`,
3294
+ "fetch",
3295
+ "--prune",
3296
+ "origin"
3297
+ ], mirrorPath, cloneTimeoutMs);
3298
+ log?.info({
3299
+ gitUrl: originUrl,
3300
+ toProtocol: direction.toProtocol
3301
+ }, "protocol-fallback fetch succeeded");
3302
+ return {
3303
+ elapsedMs,
3304
+ usedFallback: true
3305
+ };
3306
+ } catch (peerErr) {
3307
+ const peerMessage = peerErr instanceof Error ? peerErr.message : String(peerErr);
3308
+ throw new GitMirrorAuthError(`Could not fetch ${originUrl} over ${direction.fromProtocol.toUpperCase()} or ${direction.toProtocol.toUpperCase()}. ${direction.fromProtocol.toUpperCase()} attempt failed: ${truncate(primaryMessage)} ${direction.toProtocol.toUpperCase()} retry (${direction.peerBase}) failed: ${truncate(peerMessage)}`);
3309
+ }
3310
+ }
3311
+ }
3312
+ /**
3117
3313
  * Bring the mirror's config to the invariant expected by this module:
3118
3314
  * fetch refspec = `+refs/heads/*:refs/remotes/origin/*`, `remote.origin.mirror`
3119
3315
  * absent, `refs/remotes/origin/HEAD` resolvable.
@@ -3181,17 +3377,8 @@ function createGitMirrorManager(opts) {
3181
3377
  migrated = true;
3182
3378
  }
3183
3379
  if (migrated) {
3184
- await git([
3185
- "fetch",
3186
- "--prune",
3187
- "origin"
3188
- ], mirrorPath, cloneTimeoutMs);
3189
- await gitOk([
3190
- "remote",
3191
- "set-head",
3192
- "origin",
3193
- "--auto"
3194
- ], mirrorPath, 3e4);
3380
+ await fetchOrigin(mirrorPath, url);
3381
+ await setHeadAuto(mirrorPath, url);
3195
3382
  log?.info({ gitUrl: url }, "mirror config migrated");
3196
3383
  }
3197
3384
  return { migrated };
@@ -3221,17 +3408,8 @@ function createGitMirrorManager(opts) {
3221
3408
  "remote.origin.fetch",
3222
3409
  FETCH_REFSPEC
3223
3410
  ], mirrorPath, 1e4);
3224
- await git([
3225
- "fetch",
3226
- "--prune",
3227
- "origin"
3228
- ], mirrorPath, cloneTimeoutMs);
3229
- await gitOk([
3230
- "remote",
3231
- "set-head",
3232
- "origin",
3233
- "--auto"
3234
- ], mirrorPath, 3e4);
3411
+ await fetchOrigin(mirrorPath, url);
3412
+ await setHeadAuto(mirrorPath, url);
3235
3413
  }
3236
3414
  async function branchExists(mirrorPath, branchName) {
3237
3415
  return await gitOk([
@@ -3257,6 +3435,21 @@ function createGitMirrorManager(opts) {
3257
3435
  "--quiet",
3258
3436
  "refs/remotes/origin/HEAD"
3259
3437
  ], mirrorPath, 1e4)) return "refs/remotes/origin/HEAD";
3438
+ const url = await readOriginUrl(mirrorPath);
3439
+ if (url && await setHeadAuto(mirrorPath, url)) {
3440
+ if (await gitOk([
3441
+ "rev-parse",
3442
+ "--verify",
3443
+ "--quiet",
3444
+ "refs/remotes/origin/HEAD"
3445
+ ], mirrorPath, 1e4)) {
3446
+ log?.info({
3447
+ mirrorPath,
3448
+ gitUrl: url
3449
+ }, "origin/HEAD self-healed via setHeadAuto");
3450
+ return "refs/remotes/origin/HEAD";
3451
+ }
3452
+ }
3260
3453
  throw new GitMirrorError("Cannot resolve default branch: refs/remotes/origin/HEAD is missing. Re-run with an explicit `ref`.");
3261
3454
  }
3262
3455
  if (looksLikeCommitSha(ref)) {
@@ -3325,16 +3518,12 @@ function createGitMirrorManager(opts) {
3325
3518
  const path = mirrorDir(url);
3326
3519
  if (!existsSync(join(path, "HEAD"))) throw new GitMirrorError(`Cannot fetch — no mirror exists for "${url}"`);
3327
3520
  try {
3328
- const { elapsedMs } = await git([
3329
- "fetch",
3330
- "--prune",
3331
- "origin"
3332
- ], path, cloneTimeoutMs);
3521
+ const { elapsedMs } = await fetchOrigin(path, url);
3333
3522
  return { elapsedMs };
3334
3523
  } catch (err) {
3335
3524
  log?.warn({
3336
3525
  gitUrl: url,
3337
- errorCode: err instanceof GitMirrorError ? "git-failed" : "unknown",
3526
+ errorCode: err instanceof GitMirrorAuthError ? "auth-failed" : err instanceof GitMirrorTimeoutError ? "timeout" : err instanceof GitMirrorError ? "git-failed" : "unknown",
3338
3527
  stderr: err instanceof Error ? err.message.slice(0, 1024) : String(err).slice(0, 1024)
3339
3528
  }, "mirror fetch failed");
3340
3529
  throw err;
@@ -3472,6 +3661,166 @@ var GitMirrorWorktreeConflictError = class extends GitMirrorError {
3472
3661
  }
3473
3662
  };
3474
3663
  /**
3664
+ * Thrown when both the HTTPS fetch and the SSH fallback fail. The message
3665
+ * carries trimmed stderr from both attempts so the operator can see whether
3666
+ * the host's HTTPS credentials are missing, the SSH key is missing, or both.
3667
+ */
3668
+ var GitMirrorAuthError = class extends GitMirrorError {
3669
+ constructor(message) {
3670
+ super(message);
3671
+ this.name = "GitMirrorAuthError";
3672
+ }
3673
+ };
3674
+ /**
3675
+ * Heuristic for HTTPS-side credential failures. Matches the indicators git
3676
+ * itself emits over libcurl / git-credential helpers, regardless of platform.
3677
+ *
3678
+ * Negative space (intentionally NOT matched): network errors
3679
+ * (`Could not resolve host`, `connection refused`), repo errors
3680
+ * (`Repository not found`, `couldn't find remote ref`), TLS errors
3681
+ * (`SSL certificate problem`). Those won't be cured by switching transports
3682
+ * and silently retrying would mask the real bug.
3683
+ *
3684
+ * Exported for unit testing.
3685
+ */
3686
+ function isLikelyHttpsAuthFailure(message) {
3687
+ if (!message) return false;
3688
+ return /could not read Username/i.test(message) || /could not read Password/i.test(message) || /Authentication failed/i.test(message) || /terminal prompts disabled/i.test(message) || /HTTP\s*(?:Basic:\s*)?Access denied/i.test(message) || /\bHTTP[/ ]?(1\.[01]|2|2\.0)?\s*40[13]\b/i.test(message) || /\bfatal:\s*unable to access\b.*\b(401|403)\b/i.test(message) || /\bremote: Invalid username or password\b/i.test(message);
3689
+ }
3690
+ /**
3691
+ * Heuristic for SSH-side credential failures (no key on disk, key not
3692
+ * accepted by remote, agent has nothing usable, host key mismatch).
3693
+ *
3694
+ * Negative space (intentionally NOT matched): SSH-level network errors
3695
+ * (`Could not resolve hostname`, `Connection refused`, `Connection timed out`).
3696
+ * Those are network reachability issues — switching to HTTPS won't help
3697
+ * unless the network policy specifically blocks port 22, which is rare
3698
+ * enough that we'd rather surface the original error than guess.
3699
+ *
3700
+ * Exported for unit testing.
3701
+ */
3702
+ function isLikelySshAuthFailure(message) {
3703
+ if (!message) return false;
3704
+ return /Permission denied\s*(?:\(|,)/i.test(message) || /Could not read from remote repository/i.test(message) || /Host key verification failed/i.test(message) || /no matching host key type/i.test(message) || /no mutual signature algorithm/i.test(message);
3705
+ }
3706
+ /**
3707
+ * Map an HTTPS git URL to the `insteadOf` rewrite needed to make git resolve
3708
+ * it through SSH. Returns *base* strings (suitable for
3709
+ * `git -c url.<sshBase>.insteadOf=<httpsBase>`) — git's `insteadOf` is a
3710
+ * prefix match, so we only need the host segment.
3711
+ *
3712
+ * `https://github.com/owner/repo.git` → `git@github.com:` / `https://github.com/`
3713
+ *
3714
+ * Returns `null` for inputs that should NOT trigger fallback:
3715
+ * - non-HTTPS URLs (already SSH, `git://`, `file://`, etc.)
3716
+ * - URLs with embedded credentials (schema rejects these on input;
3717
+ * belt-and-braces — never silently downgrade auth strength)
3718
+ * - HTTPS URLs with a non-default port — there is no portable mapping to
3719
+ * an SSH port (HTTPS:8443 ↔ SSH:???), so we refuse to guess
3720
+ * - URLs that fail to parse
3721
+ *
3722
+ * Exported for unit testing.
3723
+ */
3724
+ function httpsToSshBaseRewrite(url) {
3725
+ if (!url || !/^https:\/\//i.test(url)) return null;
3726
+ let parsed;
3727
+ try {
3728
+ parsed = new URL(url);
3729
+ } catch {
3730
+ return null;
3731
+ }
3732
+ if (parsed.protocol !== "https:") return null;
3733
+ if (parsed.username.length > 0 || parsed.password.length > 0) return null;
3734
+ if (!parsed.hostname) return null;
3735
+ if (parsed.port && parsed.port !== "443") return null;
3736
+ return {
3737
+ httpsBase: `https://${parsed.hostname}/`,
3738
+ sshBase: `git@${parsed.hostname}:`
3739
+ };
3740
+ }
3741
+ /**
3742
+ * Map an SSH git URL (either `ssh://` or scp-like `[user@]host:path`) to the
3743
+ * `insteadOf` rewrite for resolving via HTTPS. Mirror of
3744
+ * `httpsToSshBaseRewrite`.
3745
+ *
3746
+ * `git@github.com:owner/repo.git` → `git@github.com:` / `https://github.com/`
3747
+ * `ssh://git@github.com/owner/repo.git` → `ssh://git@github.com/` / `https://github.com/`
3748
+ * `ssh://git@gitlab.example.com:22/x/y.git` → `ssh://git@gitlab.example.com:22/` / `https://gitlab.example.com/`
3749
+ *
3750
+ * Returns `null` when:
3751
+ * - URL is not SSH-shaped
3752
+ * - URL has an embedded password (`user:pass@`)
3753
+ * - SSH URL has a non-default port (≠ 22) — no portable mapping to HTTPS
3754
+ *
3755
+ * Exported for unit testing.
3756
+ */
3757
+ function sshToHttpsBaseRewrite(url) {
3758
+ if (!url) return null;
3759
+ if (!url.includes("://")) {
3760
+ const m = url.match(/^((?:[A-Za-z0-9_.-]+@)?)([A-Za-z0-9.-]+):([^/@:\s][^@:\s]*)$/);
3761
+ const userAt = m?.[1];
3762
+ const host = m?.[2];
3763
+ const path = m?.[3];
3764
+ if (userAt === void 0 || !host || !path) return null;
3765
+ if (/^\d+(?:\/|$)/.test(path)) return null;
3766
+ return {
3767
+ sshBase: `${userAt}${host}:`,
3768
+ httpsBase: `https://${host}/`
3769
+ };
3770
+ }
3771
+ let parsed;
3772
+ try {
3773
+ parsed = new URL(url);
3774
+ } catch {
3775
+ return null;
3776
+ }
3777
+ if (parsed.protocol !== "ssh:") return null;
3778
+ if (parsed.password.length > 0) return null;
3779
+ if (!parsed.hostname) return null;
3780
+ if (parsed.port && parsed.port !== "22") return null;
3781
+ const userAt = parsed.username.length > 0 ? `${parsed.username}@` : "";
3782
+ return {
3783
+ sshBase: parsed.port ? `ssh://${userAt}${parsed.hostname}:${parsed.port}/` : `ssh://${userAt}${parsed.hostname}/`,
3784
+ httpsBase: `https://${parsed.hostname}/`
3785
+ };
3786
+ }
3787
+ /**
3788
+ * Same shape as `SCP_LIKE_SSH_RE` in the shared schema — kept in sync so
3789
+ * what the schema accepts is exactly what we route through the SSH-side
3790
+ * fallback. Single source of truth would be ideal, but cross-package import
3791
+ * for a regex isn't worth the build coupling.
3792
+ */
3793
+ const SCP_LIKE_RE = /^(?:[A-Za-z0-9_.-]+@)?[A-Za-z0-9.-]+:(?!\d+(?:\/|$))[^/:@\s][^:@\s]*$/;
3794
+ function pickFallbackDirection(originUrl) {
3795
+ if (/^https:\/\//i.test(originUrl)) {
3796
+ const r = httpsToSshBaseRewrite(originUrl);
3797
+ if (!r) return null;
3798
+ return {
3799
+ fromProtocol: "https",
3800
+ toProtocol: "ssh",
3801
+ originBase: r.httpsBase,
3802
+ peerBase: r.sshBase,
3803
+ shouldRetry: isLikelyHttpsAuthFailure
3804
+ };
3805
+ }
3806
+ if (/^ssh:\/\//i.test(originUrl) || SCP_LIKE_RE.test(originUrl)) {
3807
+ const r = sshToHttpsBaseRewrite(originUrl);
3808
+ if (!r) return null;
3809
+ return {
3810
+ fromProtocol: "ssh",
3811
+ toProtocol: "https",
3812
+ originBase: r.sshBase,
3813
+ peerBase: r.httpsBase,
3814
+ shouldRetry: isLikelySshAuthFailure
3815
+ };
3816
+ }
3817
+ return null;
3818
+ }
3819
+ function truncate(text, max = 512) {
3820
+ if (text.length <= max) return text;
3821
+ return `${text.slice(0, max)}…[truncated]`;
3822
+ }
3823
+ /**
3475
3824
  * InputController — push-based async iterable bridge.
3476
3825
  *
3477
3826
  * Bridges imperative `push()` calls to the `AsyncIterable` that
@@ -8680,7 +9029,7 @@ async function onboardCreate(args) {
8680
9029
  }
8681
9030
  const runtimeAgent = args.type === "human" ? args.assistant : args.id;
8682
9031
  if (args.feishuBotAppId && args.feishuBotAppSecret) {
8683
- const { bindFeishuBot } = await import("./feishu-C6qlhju2.mjs").then((n) => n.r);
9032
+ const { bindFeishuBot } = await import("./feishu-fLnwqCOs.mjs").then((n) => n.r);
8684
9033
  const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
8685
9034
  if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
8686
9035
  else {
@@ -9893,7 +10242,7 @@ function createFeedbackHandler(config) {
9893
10242
  return { handle };
9894
10243
  }
9895
10244
  //#endregion
9896
- //#region ../server/dist/app-CVe_gn9M.mjs
10245
+ //#region ../server/dist/app-CkYiQS_D.mjs
9897
10246
  var import_fastify_opentelemetry = /* @__PURE__ */ __toESM(require_fastify_opentelemetry(), 1);
9898
10247
  init_esm();
9899
10248
  var __defProp = Object.defineProperty;
@@ -15388,19 +15737,26 @@ async function deleteOrgSetting(db, orgId, namespace) {
15388
15737
  await db.delete(organizationSettings).where(and(eq(organizationSettings.organizationId, orgId), eq(organizationSettings.namespace, namespace)));
15389
15738
  }
15390
15739
  /**
15391
- * Resolve the caller's "primary org" the earliest-joined active
15392
- * membership for the given user. Used by user-scoped routes that
15393
- * historically didn't take an `:orgId` (e.g. `/context-tree/info`) so
15394
- * the SDK call shape doesn't have to change while the per-tenant lookup
15395
- * still happens correctly.
15740
+ * Resolve the caller's "primary org" for user-scoped routes that
15741
+ * historically didn't take an `:orgId` (e.g. `/context-tree/info`,
15742
+ * `/context-tree/snapshot`).
15743
+ *
15744
+ * Uses the same `pickDefaultMembership` helper that `/me` uses to compute
15745
+ * `defaultOrganizationId` (most-recently-active membership, id desc tie-break).
15746
+ * That guarantees the org `/me` reports as the default is the same org these
15747
+ * server-internal lookups read from — earlier the two sides used opposite
15748
+ * orderings (`/me` desc, this fn asc), so multi-org users saw `/info`
15749
+ * resolve to a different (often unconfigured) org than the one Team Settings
15750
+ * was edited for.
15396
15751
  *
15397
- * Returns `null` for users with no active membership. Tightening to
15398
- * "explicit org selector" is a future change-once-multi-org-clients-arrive
15399
- * concern. (#7)
15752
+ * Returns `null` for users with no active membership.
15400
15753
  */
15401
15754
  async function resolveUserPrimaryOrgId(db, userId) {
15402
- const [row] = await db.select({ organizationId: members.organizationId }).from(members).where(and(eq(members.userId, userId), eq(members.status, "active"))).orderBy(asc(members.createdAt)).limit(1);
15403
- return row?.organizationId ?? null;
15755
+ return pickDefaultMembership(await db.select({
15756
+ id: members.id,
15757
+ organizationId: members.organizationId,
15758
+ createdAt: members.createdAt
15759
+ }).from(members).where(and(eq(members.userId, userId), eq(members.status, "active"))))?.organizationId ?? null;
15404
15760
  }
15405
15761
  async function contextTreeInfoRoutes(app) {
15406
15762
  /**
@@ -16333,7 +16689,7 @@ function ghostNodeId(path) {
16333
16689
  function toPosix(path) {
16334
16690
  return sep === "/" ? path : path.split(sep).join("/");
16335
16691
  }
16336
- const querySchema = z.object({ window: z.enum([
16692
+ const querySchema$1 = z.object({ window: z.enum([
16337
16693
  "1d",
16338
16694
  "7d",
16339
16695
  "30d"
@@ -16344,7 +16700,7 @@ async function contextTreeSnapshotRoutes(app) {
16344
16700
  timeWindow: "1 minute",
16345
16701
  keyGenerator: (request) => request.user?.userId ?? request.ip
16346
16702
  } } }, async (request) => {
16347
- const query = querySchema.parse(request.query);
16703
+ const query = querySchema$1.parse(request.query);
16348
16704
  const { userId } = requireUser(request);
16349
16705
  const orgId = await resolveUserPrimaryOrgId(app.db, userId);
16350
16706
  const binding = orgId ? await getOrgContextTree(app.db, orgId) : {};
@@ -16482,7 +16838,7 @@ async function healthzRoutes(app) {
16482
16838
  * `api/orgs/invitations.ts` (Class B, admin-gated).
16483
16839
  */
16484
16840
  async function publicInvitationRoutes(app) {
16485
- const { previewInvitation } = await import("./invitation-C299fxkP-KyCNax4T.mjs");
16841
+ const { previewInvitation } = await import("./invitation-C299fxkP-B89eqDos.mjs");
16486
16842
  app.get("/:token/preview", async (request, reply) => {
16487
16843
  if (!request.params.token) throw new UnauthorizedError("Token required");
16488
16844
  const preview = await previewInvitation(app.db, request.params.token);
@@ -16662,7 +17018,7 @@ async function meRoutes(app) {
16662
17018
  */
16663
17019
  app.get("/me/pinned-agents", async (request) => {
16664
17020
  const { userId } = requireUser(request);
16665
- const { listMyPinnedAgents } = await import("./client-BhCtO2df-BGOu-rRN.mjs");
17021
+ const { listMyPinnedAgents } = await import("./client-WubcgX-W-B2bOvgJ1.mjs");
16666
17022
  return listMyPinnedAgents(app.db, { userId });
16667
17023
  });
16668
17024
  /**
@@ -17136,6 +17492,34 @@ async function orgClientRoutes(app) {
17136
17492
  }));
17137
17493
  });
17138
17494
  }
17495
+ const querySchema = z.object({ window: z.enum([
17496
+ "1d",
17497
+ "7d",
17498
+ "30d"
17499
+ ]).optional() }).strict();
17500
+ async function orgContextTreeSnapshotRoutes(app) {
17501
+ app.get("/snapshot", { config: { rateLimit: {
17502
+ max: app.config.rateLimit?.contextTreeSnapshotMax ?? 6,
17503
+ timeWindow: "1 minute",
17504
+ keyGenerator: (request) => `${request.user?.userId ?? request.ip}:${orgIdParam(request.params) ?? "unknown-org"}`
17505
+ } } }, async (request) => {
17506
+ const query = querySchema.parse(request.query);
17507
+ const scope = await requireOrgMembership(request, app.db);
17508
+ const binding = await getOrgContextTree(app.db, scope.organizationId);
17509
+ const githubToken = contextTreeGithubTokenForRepo(binding.repo, app.config.contextTreeSync);
17510
+ const snapshot = await getContextTreeSnapshot({
17511
+ ...binding,
17512
+ githubToken
17513
+ }, query.window ?? "7d");
17514
+ return contextTreeSnapshotSchema.parse(snapshot);
17515
+ });
17516
+ }
17517
+ function orgIdParam(params) {
17518
+ if (!params || typeof params !== "object") return null;
17519
+ if (!("orgId" in params)) return null;
17520
+ const orgId = params.orgId;
17521
+ return typeof orgId === "string" ? orgId : null;
17522
+ }
17139
17523
  /**
17140
17524
  * Class B — `/api/v1/orgs/:orgId` itself: read & rename the org row.
17141
17525
  * Replaces the deleted `/admin/organizations/:id` GET/PATCH pair.
@@ -17928,18 +18312,43 @@ async function githubWebhookRoutes(app) {
17928
18312
  ok: true,
17929
18313
  event: "ping"
17930
18314
  });
17931
- if (eventType === "issues") return handleIssuesEvent(app, orgId, eventType, payload, reply);
17932
- if (eventType === "issue_comment") return handleIssueCommentEvent(app, orgId, eventType, payload, reply);
17933
- let mentionsRouted = 0;
17934
- const allowedActions = MENTION_ACTIONS[eventType];
17935
- const action = isRecord(payload) && typeof payload.action === "string" ? payload.action : void 0;
17936
- if (allowedActions && action && allowedActions.includes(action)) mentionsRouted = await handleMentionDelegation(app, orgId, eventType, payload);
17937
- return reply.status(200).send({
17938
- ok: true,
17939
- event: eventType,
17940
- handled: mentionsRouted > 0,
17941
- mentionsRouted
17942
- });
18315
+ const deliveryHeader = request.headers["x-github-delivery"];
18316
+ const deliveryId = typeof deliveryHeader === "string" && deliveryHeader.length > 0 ? deliveryHeader : null;
18317
+ if (deliveryId) {
18318
+ if (!await claimEvent(app.db, deliveryId, "github")) {
18319
+ log$1.info({
18320
+ deliveryId,
18321
+ eventType
18322
+ }, "duplicate GitHub delivery, skipping");
18323
+ return reply.status(200).send({
18324
+ ok: true,
18325
+ event: eventType,
18326
+ deduped: true
18327
+ });
18328
+ }
18329
+ }
18330
+ try {
18331
+ if (eventType === "issues") return await handleIssuesEvent(app, orgId, eventType, payload, reply);
18332
+ if (eventType === "issue_comment") return await handleIssueCommentEvent(app, orgId, eventType, payload, reply);
18333
+ let mentionsRouted = 0;
18334
+ const allowedActions = MENTION_ACTIONS[eventType];
18335
+ const action = isRecord(payload) && typeof payload.action === "string" ? payload.action : void 0;
18336
+ if (allowedActions && action && allowedActions.includes(action)) mentionsRouted = await handleMentionDelegation(app, orgId, eventType, payload);
18337
+ return reply.status(200).send({
18338
+ ok: true,
18339
+ event: eventType,
18340
+ handled: mentionsRouted > 0,
18341
+ mentionsRouted
18342
+ });
18343
+ } catch (err) {
18344
+ if (deliveryId) await unclaimEvent(app.db, deliveryId, "github").catch((unclaimErr) => {
18345
+ log$1.error({
18346
+ err: unclaimErr,
18347
+ deliveryId
18348
+ }, "failed to unclaim GitHub delivery after handler error");
18349
+ });
18350
+ throw err;
18351
+ }
17943
18352
  });
17944
18353
  }
17945
18354
  /** Extract text body from any GitHub webhook event for @mention scanning. */
@@ -19748,6 +20157,7 @@ async function buildApp(config) {
19748
20157
  await scope.register(orgInvitationRoutes, { prefix: "/invitations" });
19749
20158
  await scope.register(orgMemberRoutes, { prefix: "/members" });
19750
20159
  await scope.register(orgSettingsRoutes, { prefix: "/settings" });
20160
+ await scope.register(orgContextTreeSnapshotRoutes, { prefix: "/context-tree" });
19751
20161
  }), { prefix: "/orgs/:orgId" });
19752
20162
  await api.register(orgWsRoutes(notifier, config.secrets.jwtSecret), { prefix: "/orgs/:orgId/ws" });
19753
20163
  await api.register(userScope("resourcesScope", async (scope) => {