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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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-D1TDiik_-gxtXN9bj.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;
1380
1391
  }
1381
- }, { message: "Repo URL must not include credentials." });
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;
1405
+ }
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({
@@ -2923,8 +2948,6 @@ function installFirstTreeIntegration(options) {
2923
2948
  const integrateArgs = [
2924
2949
  "tree",
2925
2950
  "integrate",
2926
- "--source-path",
2927
- workspacePath,
2928
2951
  "--tree-path",
2929
2952
  contextTreePath,
2930
2953
  "--mode",
@@ -2959,8 +2982,13 @@ function installFirstTreeIntegration(options) {
2959
2982
  } catch (err) {
2960
2983
  const msg = err instanceof Error ? err.message : String(err);
2961
2984
  const binaryMissing = /ENOENT|not found|command not found/i.test(msg);
2985
+ const unsupportedByThisCli = /unknown (?:option|command|argument)|unrecognized option/i.test(msg);
2986
+ const shouldRetry = binaryMissing || unsupportedByThisCli;
2962
2987
  const isLastAttempt = index === attempts.length - 1;
2963
- if (binaryMissing && !isLastAttempt) continue;
2988
+ if (shouldRetry && !isLastAttempt) {
2989
+ log(`First-tree integration via ${attempt.label} unusable; falling back: ${msg.slice(0, 200)}`);
2990
+ continue;
2991
+ }
2964
2992
  log(`First-tree integration skipped (${attempt.label}): ${msg.slice(0, 200)}`);
2965
2993
  return false;
2966
2994
  }
@@ -3029,8 +3057,65 @@ For content with quotes, \`$\`, backticks, or newlines, prefer stdin to avoid sh
3029
3057
  const DEFAULT_CLONE_TIMEOUT_MS = 300 * 1e3;
3030
3058
  const FETCH_REFSPEC = "+refs/heads/*:refs/remotes/origin/*";
3031
3059
  const SESSION_BRANCH_PREFIX = "hub-session";
3060
+ /**
3061
+ * Hash a repo URL into the mirror directory name.
3062
+ *
3063
+ * The hash is computed over a *canonical* form (host + path, lowercased
3064
+ * host, no `.git` suffix, no leading/trailing slash, no protocol, no
3065
+ * `user@` prefix), so the same upstream repo addressed via any of the
3066
+ * accepted forms (HTTPS, `ssh://`, or scp-like) all land in the same
3067
+ * mirror dir. That matters because:
3068
+ * - admins may write any of those three forms into `source_repos[].url`
3069
+ * (the schema accepts all of them)
3070
+ * - the `fetchOrigin` fallback below transparently swaps protocols when
3071
+ * credentials are missing — without canonical hashing, the fallback
3072
+ * would silently maintain a second mirror dir
3073
+ *
3074
+ * Migration cost: pre-existing mirrors created before this change use the
3075
+ * raw-URL hash and will be orphaned after the upgrade. `gcMirrors` removes
3076
+ * them on the next run; the next fetch repopulates the canonical-keyed
3077
+ * mirror. No data loss — mirror is a cache, not state.
3078
+ */
3032
3079
  function hashUrl(url) {
3033
- return createHash("sha256").update(url).digest("hex").slice(0, 32);
3080
+ return createHash("sha256").update(canonicalizeRepoUrl(url)).digest("hex").slice(0, 32);
3081
+ }
3082
+ /**
3083
+ * Reduce a repo URL to `<host[:port]>/<path-without-.git>`. Used as the
3084
+ * input to the mirror dir hash and exposed for unit testing.
3085
+ *
3086
+ * `https://github.com/foo/bar.git` → `github.com/foo/bar`
3087
+ * `git@github.com:foo/bar.git` → `github.com/foo/bar`
3088
+ * `ssh://git@github.com/foo/bar.git` → `github.com/foo/bar`
3089
+ * `ssh://git@gitlab.example.com:2222/x/y` → `gitlab.example.com:2222/x/y`
3090
+ *
3091
+ * Falls back to the raw input for un-parseable strings — better to keep
3092
+ * a stable mirror than to throw mid-bootstrap.
3093
+ */
3094
+ function canonicalizeRepoUrl(url) {
3095
+ if (!url.includes("://")) {
3096
+ const m = url.match(/^(?:[A-Za-z0-9_.-]+@)?([A-Za-z0-9.-]+):([^/@:\s][^@:\s]*)$/);
3097
+ const host = m?.[1];
3098
+ const rawPath = m?.[2];
3099
+ if (host && rawPath && !/^\d+(?:\/|$)/.test(rawPath)) {
3100
+ const path = normalizePath(rawPath);
3101
+ return `${host.toLowerCase()}/${path}`;
3102
+ }
3103
+ }
3104
+ try {
3105
+ const parsed = new URL(url);
3106
+ const host = parsed.hostname.toLowerCase();
3107
+ return `${parsed.port === "" || parsed.protocol === "https:" && parsed.port === "443" || parsed.protocol === "ssh:" && parsed.port === "22" ? host : `${host}:${parsed.port}`}/${normalizePath(parsed.pathname)}`;
3108
+ } catch {
3109
+ return url;
3110
+ }
3111
+ }
3112
+ /**
3113
+ * Strip leading slashes, trailing slashes, and a trailing `.git`. Used by
3114
+ * `canonicalizeRepoUrl` so that `…/foo/bar`, `…/foo/bar/`, and `…/foo/bar.git`
3115
+ * all collapse to the same canonical path (and therefore the same mirror dir).
3116
+ */
3117
+ function normalizePath(rawPath) {
3118
+ return rawPath.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "");
3034
3119
  }
3035
3120
  function shortHash(input) {
3036
3121
  return createHash("sha256").update(input).digest("hex").slice(0, 8);
@@ -3067,10 +3152,14 @@ function createGitMirrorManager(opts) {
3067
3152
  }
3068
3153
  async function git(args, cwd, timeoutMs, env) {
3069
3154
  const start = Date.now();
3155
+ const finalEnv = {
3156
+ GIT_TERMINAL_PROMPT: "0",
3157
+ ...env ?? process.env
3158
+ };
3070
3159
  return await new Promise((resolveExec, rejectExec) => {
3071
3160
  const proc = spawn("git", args, {
3072
3161
  cwd: cwd ?? void 0,
3073
- env: env ?? process.env,
3162
+ env: finalEnv,
3074
3163
  stdio: [
3075
3164
  "ignore",
3076
3165
  "pipe",
@@ -3114,6 +3203,116 @@ function createGitMirrorManager(opts) {
3114
3203
  }
3115
3204
  }
3116
3205
  /**
3206
+ * `git fetch --prune origin` with one-shot bidirectional protocol fallback.
3207
+ *
3208
+ * Decides direction from `originUrl`'s protocol:
3209
+ * - HTTPS origin → on credential-shaped failure, retry as SSH
3210
+ * - SSH origin → on credential-shaped failure, retry as HTTPS
3211
+ * The fallback fires only when **all** hold:
3212
+ * 1. The first attempt failed with a credential-shaped error in the
3213
+ * direction-appropriate sense (`isLikelyHttpsAuthFailure` /
3214
+ * `isLikelySshAuthFailure`). Network failures, missing-ref errors,
3215
+ * and TLS/host-key surprises propagate as-is — those won't be cured
3216
+ * by switching transports and silently retrying would mask real bugs.
3217
+ * 2. The origin URL maps to a usable peer-protocol form
3218
+ * (see `httpsToSshBaseRewrite` / `sshToHttpsBaseRewrite`). Custom
3219
+ * ports beyond the protocol default disable the rewrite — there's no
3220
+ * universal mapping between `https://host:8443` and an SSH peer.
3221
+ *
3222
+ * Implementation: the retry uses `git -c url.<peer-base>.insteadOf=<origin-base>`
3223
+ * — git resolves origin's URL through that rewrite for the duration of
3224
+ * one subprocess and never persists anything to disk. The mirror's
3225
+ * `remote.origin.url` stays as configured, so the next fetch starts back
3226
+ * at the original protocol unless this fallback fires again.
3227
+ */
3228
+ /**
3229
+ * `remote set-head --auto` is a remote-talking op too — under the same
3230
+ * credential rules as `fetch`. If we don't apply the same fallback,
3231
+ * mirrors whose HTTPS creds are missing end up with `refs/remotes/origin/HEAD`
3232
+ * unset, which then breaks `resolveBase()` for callers without an explicit
3233
+ * `ref` (the default-branch lookup throws).
3234
+ *
3235
+ * Strategy mirrors `fetchOrigin`: try the configured protocol first, fall
3236
+ * back to the peer protocol once via `-c url.<peer>.insteadOf=<origin>`.
3237
+ * Returns true on either success, false if both attempts failed (which
3238
+ * mirrors the historical `gitOk` semantics — non-fatal). No `GitMirrorAuthError`
3239
+ * here: callers that need origin/HEAD already get a clear
3240
+ * `GitMirrorError("Cannot resolve default branch …")` if both attempts fail.
3241
+ */
3242
+ async function setHeadAuto(mirrorPath, originUrl) {
3243
+ if (await gitOk([
3244
+ "remote",
3245
+ "set-head",
3246
+ "origin",
3247
+ "--auto"
3248
+ ], mirrorPath, 3e4)) return true;
3249
+ const direction = pickFallbackDirection(originUrl);
3250
+ if (!direction) return false;
3251
+ return await gitOk([
3252
+ "-c",
3253
+ `url.${direction.peerBase}.insteadOf=${direction.originBase}`,
3254
+ "remote",
3255
+ "set-head",
3256
+ "origin",
3257
+ "--auto"
3258
+ ], mirrorPath, 3e4);
3259
+ }
3260
+ async function readOriginUrl(mirrorPath) {
3261
+ try {
3262
+ const { stdout } = await git([
3263
+ "config",
3264
+ "--get",
3265
+ "remote.origin.url"
3266
+ ], mirrorPath, 1e4);
3267
+ return stdout.trim() || null;
3268
+ } catch {
3269
+ return null;
3270
+ }
3271
+ }
3272
+ async function fetchOrigin(mirrorPath, originUrl) {
3273
+ const direction = pickFallbackDirection(originUrl);
3274
+ try {
3275
+ const { elapsedMs } = await git([
3276
+ "fetch",
3277
+ "--prune",
3278
+ "origin"
3279
+ ], mirrorPath, cloneTimeoutMs);
3280
+ return {
3281
+ elapsedMs,
3282
+ usedFallback: false
3283
+ };
3284
+ } catch (primaryErr) {
3285
+ const primaryMessage = primaryErr instanceof Error ? primaryErr.message : String(primaryErr);
3286
+ if (!direction || !direction.shouldRetry(primaryMessage)) throw primaryErr;
3287
+ log?.info({
3288
+ gitUrl: originUrl,
3289
+ fromProtocol: direction.fromProtocol,
3290
+ toProtocol: direction.toProtocol,
3291
+ peerBase: direction.peerBase
3292
+ }, "fetch failed with credential-shaped error; retrying with peer-protocol insteadOf rewrite");
3293
+ try {
3294
+ const { elapsedMs } = await git([
3295
+ "-c",
3296
+ `url.${direction.peerBase}.insteadOf=${direction.originBase}`,
3297
+ "fetch",
3298
+ "--prune",
3299
+ "origin"
3300
+ ], mirrorPath, cloneTimeoutMs);
3301
+ log?.info({
3302
+ gitUrl: originUrl,
3303
+ toProtocol: direction.toProtocol
3304
+ }, "protocol-fallback fetch succeeded");
3305
+ return {
3306
+ elapsedMs,
3307
+ usedFallback: true
3308
+ };
3309
+ } catch (peerErr) {
3310
+ const peerMessage = peerErr instanceof Error ? peerErr.message : String(peerErr);
3311
+ 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)}`);
3312
+ }
3313
+ }
3314
+ }
3315
+ /**
3117
3316
  * Bring the mirror's config to the invariant expected by this module:
3118
3317
  * fetch refspec = `+refs/heads/*:refs/remotes/origin/*`, `remote.origin.mirror`
3119
3318
  * absent, `refs/remotes/origin/HEAD` resolvable.
@@ -3181,17 +3380,8 @@ function createGitMirrorManager(opts) {
3181
3380
  migrated = true;
3182
3381
  }
3183
3382
  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);
3383
+ await fetchOrigin(mirrorPath, url);
3384
+ await setHeadAuto(mirrorPath, url);
3195
3385
  log?.info({ gitUrl: url }, "mirror config migrated");
3196
3386
  }
3197
3387
  return { migrated };
@@ -3221,17 +3411,8 @@ function createGitMirrorManager(opts) {
3221
3411
  "remote.origin.fetch",
3222
3412
  FETCH_REFSPEC
3223
3413
  ], 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);
3414
+ await fetchOrigin(mirrorPath, url);
3415
+ await setHeadAuto(mirrorPath, url);
3235
3416
  }
3236
3417
  async function branchExists(mirrorPath, branchName) {
3237
3418
  return await gitOk([
@@ -3257,6 +3438,21 @@ function createGitMirrorManager(opts) {
3257
3438
  "--quiet",
3258
3439
  "refs/remotes/origin/HEAD"
3259
3440
  ], mirrorPath, 1e4)) return "refs/remotes/origin/HEAD";
3441
+ const url = await readOriginUrl(mirrorPath);
3442
+ if (url && await setHeadAuto(mirrorPath, url)) {
3443
+ if (await gitOk([
3444
+ "rev-parse",
3445
+ "--verify",
3446
+ "--quiet",
3447
+ "refs/remotes/origin/HEAD"
3448
+ ], mirrorPath, 1e4)) {
3449
+ log?.info({
3450
+ mirrorPath,
3451
+ gitUrl: url
3452
+ }, "origin/HEAD self-healed via setHeadAuto");
3453
+ return "refs/remotes/origin/HEAD";
3454
+ }
3455
+ }
3260
3456
  throw new GitMirrorError("Cannot resolve default branch: refs/remotes/origin/HEAD is missing. Re-run with an explicit `ref`.");
3261
3457
  }
3262
3458
  if (looksLikeCommitSha(ref)) {
@@ -3325,16 +3521,12 @@ function createGitMirrorManager(opts) {
3325
3521
  const path = mirrorDir(url);
3326
3522
  if (!existsSync(join(path, "HEAD"))) throw new GitMirrorError(`Cannot fetch — no mirror exists for "${url}"`);
3327
3523
  try {
3328
- const { elapsedMs } = await git([
3329
- "fetch",
3330
- "--prune",
3331
- "origin"
3332
- ], path, cloneTimeoutMs);
3524
+ const { elapsedMs } = await fetchOrigin(path, url);
3333
3525
  return { elapsedMs };
3334
3526
  } catch (err) {
3335
3527
  log?.warn({
3336
3528
  gitUrl: url,
3337
- errorCode: err instanceof GitMirrorError ? "git-failed" : "unknown",
3529
+ errorCode: err instanceof GitMirrorAuthError ? "auth-failed" : err instanceof GitMirrorTimeoutError ? "timeout" : err instanceof GitMirrorError ? "git-failed" : "unknown",
3338
3530
  stderr: err instanceof Error ? err.message.slice(0, 1024) : String(err).slice(0, 1024)
3339
3531
  }, "mirror fetch failed");
3340
3532
  throw err;
@@ -3472,6 +3664,166 @@ var GitMirrorWorktreeConflictError = class extends GitMirrorError {
3472
3664
  }
3473
3665
  };
3474
3666
  /**
3667
+ * Thrown when both the HTTPS fetch and the SSH fallback fail. The message
3668
+ * carries trimmed stderr from both attempts so the operator can see whether
3669
+ * the host's HTTPS credentials are missing, the SSH key is missing, or both.
3670
+ */
3671
+ var GitMirrorAuthError = class extends GitMirrorError {
3672
+ constructor(message) {
3673
+ super(message);
3674
+ this.name = "GitMirrorAuthError";
3675
+ }
3676
+ };
3677
+ /**
3678
+ * Heuristic for HTTPS-side credential failures. Matches the indicators git
3679
+ * itself emits over libcurl / git-credential helpers, regardless of platform.
3680
+ *
3681
+ * Negative space (intentionally NOT matched): network errors
3682
+ * (`Could not resolve host`, `connection refused`), repo errors
3683
+ * (`Repository not found`, `couldn't find remote ref`), TLS errors
3684
+ * (`SSL certificate problem`). Those won't be cured by switching transports
3685
+ * and silently retrying would mask the real bug.
3686
+ *
3687
+ * Exported for unit testing.
3688
+ */
3689
+ function isLikelyHttpsAuthFailure(message) {
3690
+ if (!message) return false;
3691
+ 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);
3692
+ }
3693
+ /**
3694
+ * Heuristic for SSH-side credential failures (no key on disk, key not
3695
+ * accepted by remote, agent has nothing usable, host key mismatch).
3696
+ *
3697
+ * Negative space (intentionally NOT matched): SSH-level network errors
3698
+ * (`Could not resolve hostname`, `Connection refused`, `Connection timed out`).
3699
+ * Those are network reachability issues — switching to HTTPS won't help
3700
+ * unless the network policy specifically blocks port 22, which is rare
3701
+ * enough that we'd rather surface the original error than guess.
3702
+ *
3703
+ * Exported for unit testing.
3704
+ */
3705
+ function isLikelySshAuthFailure(message) {
3706
+ if (!message) return false;
3707
+ 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);
3708
+ }
3709
+ /**
3710
+ * Map an HTTPS git URL to the `insteadOf` rewrite needed to make git resolve
3711
+ * it through SSH. Returns *base* strings (suitable for
3712
+ * `git -c url.<sshBase>.insteadOf=<httpsBase>`) — git's `insteadOf` is a
3713
+ * prefix match, so we only need the host segment.
3714
+ *
3715
+ * `https://github.com/owner/repo.git` → `git@github.com:` / `https://github.com/`
3716
+ *
3717
+ * Returns `null` for inputs that should NOT trigger fallback:
3718
+ * - non-HTTPS URLs (already SSH, `git://`, `file://`, etc.)
3719
+ * - URLs with embedded credentials (schema rejects these on input;
3720
+ * belt-and-braces — never silently downgrade auth strength)
3721
+ * - HTTPS URLs with a non-default port — there is no portable mapping to
3722
+ * an SSH port (HTTPS:8443 ↔ SSH:???), so we refuse to guess
3723
+ * - URLs that fail to parse
3724
+ *
3725
+ * Exported for unit testing.
3726
+ */
3727
+ function httpsToSshBaseRewrite(url) {
3728
+ if (!url || !/^https:\/\//i.test(url)) return null;
3729
+ let parsed;
3730
+ try {
3731
+ parsed = new URL(url);
3732
+ } catch {
3733
+ return null;
3734
+ }
3735
+ if (parsed.protocol !== "https:") return null;
3736
+ if (parsed.username.length > 0 || parsed.password.length > 0) return null;
3737
+ if (!parsed.hostname) return null;
3738
+ if (parsed.port && parsed.port !== "443") return null;
3739
+ return {
3740
+ httpsBase: `https://${parsed.hostname}/`,
3741
+ sshBase: `git@${parsed.hostname}:`
3742
+ };
3743
+ }
3744
+ /**
3745
+ * Map an SSH git URL (either `ssh://` or scp-like `[user@]host:path`) to the
3746
+ * `insteadOf` rewrite for resolving via HTTPS. Mirror of
3747
+ * `httpsToSshBaseRewrite`.
3748
+ *
3749
+ * `git@github.com:owner/repo.git` → `git@github.com:` / `https://github.com/`
3750
+ * `ssh://git@github.com/owner/repo.git` → `ssh://git@github.com/` / `https://github.com/`
3751
+ * `ssh://git@gitlab.example.com:22/x/y.git` → `ssh://git@gitlab.example.com:22/` / `https://gitlab.example.com/`
3752
+ *
3753
+ * Returns `null` when:
3754
+ * - URL is not SSH-shaped
3755
+ * - URL has an embedded password (`user:pass@`)
3756
+ * - SSH URL has a non-default port (≠ 22) — no portable mapping to HTTPS
3757
+ *
3758
+ * Exported for unit testing.
3759
+ */
3760
+ function sshToHttpsBaseRewrite(url) {
3761
+ if (!url) return null;
3762
+ if (!url.includes("://")) {
3763
+ const m = url.match(/^((?:[A-Za-z0-9_.-]+@)?)([A-Za-z0-9.-]+):([^/@:\s][^@:\s]*)$/);
3764
+ const userAt = m?.[1];
3765
+ const host = m?.[2];
3766
+ const path = m?.[3];
3767
+ if (userAt === void 0 || !host || !path) return null;
3768
+ if (/^\d+(?:\/|$)/.test(path)) return null;
3769
+ return {
3770
+ sshBase: `${userAt}${host}:`,
3771
+ httpsBase: `https://${host}/`
3772
+ };
3773
+ }
3774
+ let parsed;
3775
+ try {
3776
+ parsed = new URL(url);
3777
+ } catch {
3778
+ return null;
3779
+ }
3780
+ if (parsed.protocol !== "ssh:") return null;
3781
+ if (parsed.password.length > 0) return null;
3782
+ if (!parsed.hostname) return null;
3783
+ if (parsed.port && parsed.port !== "22") return null;
3784
+ const userAt = parsed.username.length > 0 ? `${parsed.username}@` : "";
3785
+ return {
3786
+ sshBase: parsed.port ? `ssh://${userAt}${parsed.hostname}:${parsed.port}/` : `ssh://${userAt}${parsed.hostname}/`,
3787
+ httpsBase: `https://${parsed.hostname}/`
3788
+ };
3789
+ }
3790
+ /**
3791
+ * Same shape as `SCP_LIKE_SSH_RE` in the shared schema — kept in sync so
3792
+ * what the schema accepts is exactly what we route through the SSH-side
3793
+ * fallback. Single source of truth would be ideal, but cross-package import
3794
+ * for a regex isn't worth the build coupling.
3795
+ */
3796
+ const SCP_LIKE_RE = /^(?:[A-Za-z0-9_.-]+@)?[A-Za-z0-9.-]+:(?!\d+(?:\/|$))[^/:@\s][^:@\s]*$/;
3797
+ function pickFallbackDirection(originUrl) {
3798
+ if (/^https:\/\//i.test(originUrl)) {
3799
+ const r = httpsToSshBaseRewrite(originUrl);
3800
+ if (!r) return null;
3801
+ return {
3802
+ fromProtocol: "https",
3803
+ toProtocol: "ssh",
3804
+ originBase: r.httpsBase,
3805
+ peerBase: r.sshBase,
3806
+ shouldRetry: isLikelyHttpsAuthFailure
3807
+ };
3808
+ }
3809
+ if (/^ssh:\/\//i.test(originUrl) || SCP_LIKE_RE.test(originUrl)) {
3810
+ const r = sshToHttpsBaseRewrite(originUrl);
3811
+ if (!r) return null;
3812
+ return {
3813
+ fromProtocol: "ssh",
3814
+ toProtocol: "https",
3815
+ originBase: r.sshBase,
3816
+ peerBase: r.httpsBase,
3817
+ shouldRetry: isLikelySshAuthFailure
3818
+ };
3819
+ }
3820
+ return null;
3821
+ }
3822
+ function truncate(text, max = 512) {
3823
+ if (text.length <= max) return text;
3824
+ return `${text.slice(0, max)}…[truncated]`;
3825
+ }
3826
+ /**
3475
3827
  * InputController — push-based async iterable bridge.
3476
3828
  *
3477
3829
  * Bridges imperative `push()` calls to the `AsyncIterable` that
@@ -4259,6 +4611,8 @@ const createClaudeCodeHandler = (config) => {
4259
4611
  }
4260
4612
  }
4261
4613
  const contextTreePath = config.contextTreePath ?? null;
4614
+ const contextTreeRepoUrl = config.contextTreeRepoUrl ?? null;
4615
+ const agentName = config.agentName ?? null;
4262
4616
  /**
4263
4617
  * Materialise the runtime config's `gitRepos` into worktrees under `cwd`.
4264
4618
  * Idempotent across resumes: reuses an existing Hub-managed worktree if
@@ -4326,7 +4680,8 @@ const createClaudeCodeHandler = (config) => {
4326
4680
  if (contextTreePath) installFirstTreeIntegration({
4327
4681
  workspacePath: workspace,
4328
4682
  contextTreePath,
4329
- workspaceId: sessionCtx.chatId,
4683
+ workspaceId: agentName ?? sessionCtx.chatId,
4684
+ treeRepoUrl: contextTreeRepoUrl ?? void 0,
4330
4685
  log: (msg) => sessionCtx.log(msg)
4331
4686
  });
4332
4687
  }
@@ -4515,6 +4870,8 @@ const createCodexHandler = (config) => {
4515
4870
  const agentConfigCache = config.agentConfigCache ?? null;
4516
4871
  const gitMirrorManager = config.gitMirrorManager ?? null;
4517
4872
  const contextTreePath = config.contextTreePath ?? null;
4873
+ const contextTreeRepoUrl = config.contextTreeRepoUrl ?? null;
4874
+ const agentName = config.agentName ?? null;
4518
4875
  let cwd = null;
4519
4876
  let codex = null;
4520
4877
  let thread = null;
@@ -4786,6 +5143,17 @@ const createCodexHandler = (config) => {
4786
5143
  if (inputs.length === 0) return;
4787
5144
  await runTurn(inputs.join("\n\n"), sessionCtx);
4788
5145
  }
5146
+ /** Install the first-tree skill + binding block; no-op when context tree is unconfigured. */
5147
+ function ensureFirstTreeBinding(workspace, sessionCtx) {
5148
+ if (!contextTreePath) return;
5149
+ installFirstTreeIntegration({
5150
+ workspacePath: workspace,
5151
+ contextTreePath,
5152
+ workspaceId: agentName ?? sessionCtx.chatId,
5153
+ treeRepoUrl: contextTreeRepoUrl ?? void 0,
5154
+ log: (msg) => sessionCtx.log(msg)
5155
+ });
5156
+ }
4789
5157
  return {
4790
5158
  async start(message, sessionCtx) {
4791
5159
  ctx = sessionCtx;
@@ -4811,6 +5179,7 @@ const createCodexHandler = (config) => {
4811
5179
  content: buildAgentBriefing(payload)
4812
5180
  }
4813
5181
  });
5182
+ ensureFirstTreeBinding(cwd, sessionCtx);
4814
5183
  await prepareGitWorktrees(payload, cwd, sessionCtx.chatId);
4815
5184
  codex = new Codex({
4816
5185
  env: buildEnv(sessionCtx),
@@ -4835,17 +5204,20 @@ const createCodexHandler = (config) => {
4835
5204
  env: [],
4836
5205
  gitRepos: []
4837
5206
  };
4838
- bootstrapWorkspace({
4839
- workspacePath: cwd,
4840
- identity: sessionCtx.agent,
4841
- contextTreePath,
4842
- serverUrl: sessionCtx.sdk.serverUrl,
4843
- chatId: sessionCtx.chatId,
4844
- briefing: {
4845
- format: "agents-md",
4846
- content: buildAgentBriefing(payload)
4847
- }
4848
- });
5207
+ if (!existsSync(join(cwd, ".agent", "identity.json"))) {
5208
+ bootstrapWorkspace({
5209
+ workspacePath: cwd,
5210
+ identity: sessionCtx.agent,
5211
+ contextTreePath,
5212
+ serverUrl: sessionCtx.sdk.serverUrl,
5213
+ chatId: sessionCtx.chatId,
5214
+ briefing: {
5215
+ format: "agents-md",
5216
+ content: buildAgentBriefing(payload)
5217
+ }
5218
+ });
5219
+ ensureFirstTreeBinding(cwd, sessionCtx);
5220
+ }
4849
5221
  await prepareGitWorktrees(payload, cwd, sessionCtx.chatId);
4850
5222
  codex = new Codex({
4851
5223
  env: buildEnv(sessionCtx),
@@ -5878,7 +6250,7 @@ var AgentSlot = class {
5878
6250
  lastActivityMs: 0
5879
6251
  };
5880
6252
  }
5881
- async start(contextTreePath) {
6253
+ async start(contextTreeBinding) {
5882
6254
  const sdk = (await this.clientConnection.bindAgent(this.config.agentId, this.config.runtimeType ?? this.config.type, this.config.runtimeVersion)).sdk;
5883
6255
  this.sdk = sdk;
5884
6256
  const agent = await sdk.register();
@@ -5956,7 +6328,9 @@ var AgentSlot = class {
5956
6328
  handlerFactory: this.config.handlerFactory,
5957
6329
  handlerConfig: {
5958
6330
  workspaceRoot: join(DEFAULT_DATA_DIR, "workspaces", this.config.name),
5959
- contextTreePath: contextTreePath ?? void 0,
6331
+ agentName: this.config.name,
6332
+ contextTreePath: contextTreeBinding?.path,
6333
+ contextTreeRepoUrl: contextTreeBinding?.repoUrl,
5960
6334
  gitMirrorManager
5961
6335
  },
5962
6336
  agentIdentity: {
@@ -8680,7 +9054,7 @@ async function onboardCreate(args) {
8680
9054
  }
8681
9055
  const runtimeAgent = args.type === "human" ? args.assistant : args.id;
8682
9056
  if (args.feishuBotAppId && args.feishuBotAppSecret) {
8683
- const { bindFeishuBot } = await import("./feishu-C6qlhju2.mjs").then((n) => n.r);
9057
+ const { bindFeishuBot } = await import("./feishu-fLnwqCOs.mjs").then((n) => n.r);
8684
9058
  const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
8685
9059
  if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
8686
9060
  else {
@@ -9893,7 +10267,7 @@ function createFeedbackHandler(config) {
9893
10267
  return { handle };
9894
10268
  }
9895
10269
  //#endregion
9896
- //#region ../server/dist/app-CVe_gn9M.mjs
10270
+ //#region ../server/dist/app-BXdU2BzM.mjs
9897
10271
  var import_fastify_opentelemetry = /* @__PURE__ */ __toESM(require_fastify_opentelemetry(), 1);
9898
10272
  init_esm();
9899
10273
  var __defProp = Object.defineProperty;
@@ -10658,6 +11032,28 @@ async function resolveAgentClient(db, data) {
10658
11032
  return client.id;
10659
11033
  }
10660
11034
  /**
11035
+ * Validate a `delegateMention` write at the service layer. Two checks:
11036
+ * 1. Target uuid must resolve to an existing agent — dangling references
11037
+ * would silently break webhook delegation at runtime.
11038
+ * 2. Target must belong to the same organization as the source agent —
11039
+ * cross-org delegate links are rejected here at the source so the
11040
+ * database never accumulates dirty rows. The webhook router has a
11041
+ * defense-in-depth check that filters them at fan-out time, but this
11042
+ * keeps the data clean and gives the admin UI an immediate 422 instead
11043
+ * of a silent runtime drop.
11044
+ *
11045
+ * `null` clears the field — handled by the caller; we are only invoked when
11046
+ * the caller wrote a non-null uuid.
11047
+ */
11048
+ async function validateDelegateMentionTarget(db, targetUuid, sourceOrgId) {
11049
+ const [target] = await db.select({
11050
+ uuid: agents.uuid,
11051
+ organizationId: agents.organizationId
11052
+ }).from(agents).where(eq(agents.uuid, targetUuid)).limit(1);
11053
+ if (!target) throw new BadRequestError(`delegateMention target "${targetUuid}" not found`);
11054
+ if (target.organizationId !== sourceOrgId) throw new BadRequestError("delegateMention target must belong to the same organization as the agent");
11055
+ }
11056
+ /**
10661
11057
  * Pick the first admin member in the org for internal system agents. Throws
10662
11058
  * if the org has no admin — the caller should surface the error so an admin
10663
11059
  * is created before the system tries to register more agents.
@@ -10697,6 +11093,7 @@ async function createAgent(db, data, options = {}) {
10697
11093
  type: data.type
10698
11094
  });
10699
11095
  await ensureClientSupportsRuntimeProvider(db, clientId, runtimeProvider, { force: options.force });
11096
+ if (data.delegateMention) await validateDelegateMentionTarget(db, data.delegateMention, orgId);
10700
11097
  const [org] = await db.select({ maxAgents: organizations.maxAgents }).from(organizations).where(eq(organizations.id, orgId)).limit(1);
10701
11098
  if (org && org.maxAgents > 0) {
10702
11099
  if (((await db.select({ value: count() }).from(agents).where(and(eq(agents.organizationId, orgId), ne(agents.status, AGENT_STATUSES.DELETED))))[0]?.value ?? 0) >= org.maxAgents) throw new ForbiddenError(`Organization "${orgId}" has reached its agent limit (${org.maxAgents}). Upgrade your plan or delete unused agents.`);
@@ -10843,7 +11240,10 @@ async function updateAgent(db, uuid, data) {
10843
11240
  const updates = { updatedAt: /* @__PURE__ */ new Date() };
10844
11241
  if (data.type !== void 0) updates.type = data.type;
10845
11242
  if (data.displayName !== void 0) updates.displayName = data.displayName;
10846
- if (data.delegateMention !== void 0) updates.delegateMention = data.delegateMention;
11243
+ if (data.delegateMention !== void 0) {
11244
+ if (data.delegateMention !== null) await validateDelegateMentionTarget(db, data.delegateMention, agent.organizationId);
11245
+ updates.delegateMention = data.delegateMention;
11246
+ }
10847
11247
  if (data.visibility !== void 0) updates.visibility = data.visibility;
10848
11248
  if (data.metadata !== void 0) updates.metadata = data.metadata;
10849
11249
  if (data.managerId !== void 0) {
@@ -13430,7 +13830,7 @@ async function agentRoutes(app) {
13430
13830
  });
13431
13831
  app.post("/:uuid/test", async (request, reply) => {
13432
13832
  const { uuid } = request.params;
13433
- await requireAgentAccess(request, app.db, "manage");
13833
+ const { agent: targetAgent } = await requireAgentAccess(request, app.db, "manage");
13434
13834
  const presence = await getPresence(app.db, uuid);
13435
13835
  const wsConnected = hasActiveConnection(uuid);
13436
13836
  const clientId = getAgentClientId(uuid) ?? presence?.clientId ?? null;
@@ -13467,15 +13867,15 @@ async function agentRoutes(app) {
13467
13867
  message: "Agent connection is stale — heartbeat lost. The client process may have crashed.",
13468
13868
  connection
13469
13869
  });
13470
- const [owner] = await app.db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.delegateMention, uuid), eq(agents.status, "active"))).limit(1);
13870
+ const [owner] = await app.db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.delegateMention, uuid), eq(agents.status, "active"), eq(agents.organizationId, targetAgent.organizationId))).limit(1);
13471
13871
  let senderId = owner?.uuid ?? null;
13472
13872
  if (!senderId) {
13473
- const [other] = await app.db.select({ uuid: agents.uuid }).from(agents).where(and(ne(agents.uuid, uuid), eq(agents.status, "active"))).limit(1);
13873
+ const [other] = await app.db.select({ uuid: agents.uuid }).from(agents).where(and(ne(agents.uuid, uuid), eq(agents.status, "active"), eq(agents.organizationId, targetAgent.organizationId))).limit(1);
13474
13874
  senderId = other?.uuid ?? null;
13475
13875
  }
13476
13876
  if (!senderId) return reply.status(200).send({
13477
13877
  status: "error",
13478
- message: "No suitable sender found. Need at least one other active agent.",
13878
+ message: "No suitable sender found. Need at least one other active agent in the same organization.",
13479
13879
  connection
13480
13880
  });
13481
13881
  const chat = await findOrCreateDirectChat(app.db, senderId, uuid);
@@ -14668,7 +15068,7 @@ function decodeCursor(cursor) {
14668
15068
  * - Cursor narrows the result to rows STRICTLY before `(cursor.ts, cursor.id)`.
14669
15069
  * - Followed by a small participant-list lookup for the page only.
14670
15070
  */
14671
- async function listMeChats(db, humanAgentId, query) {
15071
+ async function listMeChats(db, humanAgentId, organizationId, query) {
14672
15072
  const limit = query.limit;
14673
15073
  const cursor = query.cursor ? decodeCursor(query.cursor) : null;
14674
15074
  if (query.cursor && !cursor) throw new BadRequestError("Invalid cursor");
@@ -14709,6 +15109,11 @@ async function listMeChats(db, humanAgentId, query) {
14709
15109
  FROM chats c
14710
15110
  JOIN deduped d ON d.chat_id = c.id
14711
15111
  WHERE c.parent_chat_id IS NULL
15112
+ /* Scope to the caller's org. Without this, cross-org dirty chats
15113
+ whose chat_participants still reference the caller's human agent
15114
+ (historical pollution — see fix/cross-org-direct-chat-pollution)
15115
+ would leak into the list and 404 on click via requireChatAccess. */
15116
+ AND c.organization_id = ${organizationId}
14712
15117
  /* Filter: unread / watching */
14713
15118
  AND (${!filterUnreadOnly}::bool OR d.unread_mention_count > 0)
14714
15119
  AND (${!filterWatchingOnly}::bool OR d.membership_kind = 'watching')
@@ -15388,19 +15793,26 @@ async function deleteOrgSetting(db, orgId, namespace) {
15388
15793
  await db.delete(organizationSettings).where(and(eq(organizationSettings.organizationId, orgId), eq(organizationSettings.namespace, namespace)));
15389
15794
  }
15390
15795
  /**
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.
15796
+ * Resolve the caller's "primary org" for user-scoped routes that
15797
+ * historically didn't take an `:orgId` (e.g. `/context-tree/info`,
15798
+ * `/context-tree/snapshot`).
15396
15799
  *
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)
15800
+ * Uses the same `pickDefaultMembership` helper that `/me` uses to compute
15801
+ * `defaultOrganizationId` (most-recently-active membership, id desc tie-break).
15802
+ * That guarantees the org `/me` reports as the default is the same org these
15803
+ * server-internal lookups read from — earlier the two sides used opposite
15804
+ * orderings (`/me` desc, this fn asc), so multi-org users saw `/info`
15805
+ * resolve to a different (often unconfigured) org than the one Team Settings
15806
+ * was edited for.
15807
+ *
15808
+ * Returns `null` for users with no active membership.
15400
15809
  */
15401
15810
  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;
15811
+ return pickDefaultMembership(await db.select({
15812
+ id: members.id,
15813
+ organizationId: members.organizationId,
15814
+ createdAt: members.createdAt
15815
+ }).from(members).where(and(eq(members.userId, userId), eq(members.status, "active"))))?.organizationId ?? null;
15404
15816
  }
15405
15817
  async function contextTreeInfoRoutes(app) {
15406
15818
  /**
@@ -15721,17 +16133,17 @@ function isGithubHttpsRepo(repoUrl) {
15721
16133
  }
15722
16134
  function contextStatus(warning) {
15723
16135
  if (warning?.stale) return {
15724
- label: "Team context is stale",
16136
+ label: "Context Tree may be stale",
15725
16137
  detail: warning.detail,
15726
16138
  severity: "warning"
15727
16139
  };
15728
16140
  if (warning) return {
15729
- label: "Team context needs attention",
16141
+ label: "Context Tree needs attention",
15730
16142
  detail: warning.detail,
15731
16143
  severity: "warning"
15732
16144
  };
15733
16145
  return {
15734
- label: "Team context is current",
16146
+ label: "Context Tree is up to date",
15735
16147
  detail: "Agents have a synced team context snapshot available.",
15736
16148
  severity: "ok"
15737
16149
  };
@@ -16333,7 +16745,7 @@ function ghostNodeId(path) {
16333
16745
  function toPosix(path) {
16334
16746
  return sep === "/" ? path : path.split(sep).join("/");
16335
16747
  }
16336
- const querySchema = z.object({ window: z.enum([
16748
+ const querySchema$1 = z.object({ window: z.enum([
16337
16749
  "1d",
16338
16750
  "7d",
16339
16751
  "30d"
@@ -16344,7 +16756,7 @@ async function contextTreeSnapshotRoutes(app) {
16344
16756
  timeWindow: "1 minute",
16345
16757
  keyGenerator: (request) => request.user?.userId ?? request.ip
16346
16758
  } } }, async (request) => {
16347
- const query = querySchema.parse(request.query);
16759
+ const query = querySchema$1.parse(request.query);
16348
16760
  const { userId } = requireUser(request);
16349
16761
  const orgId = await resolveUserPrimaryOrgId(app.db, userId);
16350
16762
  const binding = orgId ? await getOrgContextTree(app.db, orgId) : {};
@@ -16482,7 +16894,7 @@ async function healthzRoutes(app) {
16482
16894
  * `api/orgs/invitations.ts` (Class B, admin-gated).
16483
16895
  */
16484
16896
  async function publicInvitationRoutes(app) {
16485
- const { previewInvitation } = await import("./invitation-C299fxkP-KyCNax4T.mjs");
16897
+ const { previewInvitation } = await import("./invitation-C299fxkP-B89eqDos.mjs");
16486
16898
  app.get("/:token/preview", async (request, reply) => {
16487
16899
  if (!request.params.token) throw new UnauthorizedError("Token required");
16488
16900
  const preview = await previewInvitation(app.db, request.params.token);
@@ -16662,7 +17074,7 @@ async function meRoutes(app) {
16662
17074
  */
16663
17075
  app.get("/me/pinned-agents", async (request) => {
16664
17076
  const { userId } = requireUser(request);
16665
- const { listMyPinnedAgents } = await import("./client-BhCtO2df-BGOu-rRN.mjs");
17077
+ const { listMyPinnedAgents } = await import("./client-0RrgrMjR-DPyuu6Ls.mjs");
16666
17078
  return listMyPinnedAgents(app.db, { userId });
16667
17079
  });
16668
17080
  /**
@@ -17095,7 +17507,7 @@ async function orgChatRoutes(app) {
17095
17507
  }
17096
17508
  if (view === "grouped") return listChatsForMember(app.db, scope.memberId, scope.humanAgentId);
17097
17509
  const query = listMeChatsQuerySchema.parse(request.query);
17098
- return listMeChats(app.db, scope.humanAgentId, query);
17510
+ return listMeChats(app.db, scope.humanAgentId, scope.organizationId, query);
17099
17511
  });
17100
17512
  /**
17101
17513
  * POST /orgs/:orgId/chats — create a new chat. The :orgId path param
@@ -17136,6 +17548,34 @@ async function orgClientRoutes(app) {
17136
17548
  }));
17137
17549
  });
17138
17550
  }
17551
+ const querySchema = z.object({ window: z.enum([
17552
+ "1d",
17553
+ "7d",
17554
+ "30d"
17555
+ ]).optional() }).strict();
17556
+ async function orgContextTreeSnapshotRoutes(app) {
17557
+ app.get("/snapshot", { config: { rateLimit: {
17558
+ max: app.config.rateLimit?.contextTreeSnapshotMax ?? 6,
17559
+ timeWindow: "1 minute",
17560
+ keyGenerator: (request) => `${request.user?.userId ?? request.ip}:${orgIdParam(request.params) ?? "unknown-org"}`
17561
+ } } }, async (request) => {
17562
+ const query = querySchema.parse(request.query);
17563
+ const scope = await requireOrgMembership(request, app.db);
17564
+ const binding = await getOrgContextTree(app.db, scope.organizationId);
17565
+ const githubToken = contextTreeGithubTokenForRepo(binding.repo, app.config.contextTreeSync);
17566
+ const snapshot = await getContextTreeSnapshot({
17567
+ ...binding,
17568
+ githubToken
17569
+ }, query.window ?? "7d");
17570
+ return contextTreeSnapshotSchema.parse(snapshot);
17571
+ });
17572
+ }
17573
+ function orgIdParam(params) {
17574
+ if (!params || typeof params !== "object") return null;
17575
+ if (!("orgId" in params)) return null;
17576
+ const orgId = params.orgId;
17577
+ return typeof orgId === "string" ? orgId : null;
17578
+ }
17139
17579
  /**
17140
17580
  * Class B — `/api/v1/orgs/:orgId` itself: read & rename the org row.
17141
17581
  * Replaces the deleted `/admin/organizations/:id` GET/PATCH pair.
@@ -17803,6 +18243,31 @@ function extractMentions$1(text) {
17803
18243
  }
17804
18244
  return [...names];
17805
18245
  }
18246
+ /** Extract mentions from structural payload fields (not free-form text).
18247
+ * GitHub's `pull_request.review_requested` puts the targeted reviewer in
18248
+ * `requested_reviewer.login`, not in any text body — `extractMentions` would
18249
+ * miss it. Team requests use `requested_team` instead, which we deliberately
18250
+ * skip to stay consistent with `extractMentions` ignoring `@org/team`. */
18251
+ function extractStructuralMentions(eventType, payload) {
18252
+ if (!isRecord(payload)) return [];
18253
+ if (eventType !== "pull_request") return [];
18254
+ if (payload.action !== "review_requested") return [];
18255
+ const reviewer = isRecord(payload.requested_reviewer) ? payload.requested_reviewer : null;
18256
+ const login = typeof reviewer?.login === "string" ? reviewer.login : null;
18257
+ return login ? [login.toLowerCase()] : [];
18258
+ }
18259
+ const DELEGATE_VERDICT_MESSAGES = {
18260
+ ok: "delegate_mention target eligible",
18261
+ not_found: "delegate_mention target not found, skipping",
18262
+ cross_org: "delegate_mention target belongs to another org, skipping",
18263
+ inactive: "delegate_mention target not active, skipping"
18264
+ };
18265
+ function evaluateDelegateTarget(target, sourceOrgId) {
18266
+ if (!target) return "not_found";
18267
+ if (target.organizationId !== sourceOrgId) return "cross_org";
18268
+ if (target.status !== "active") return "inactive";
18269
+ return "ok";
18270
+ }
17806
18271
  /**
17807
18272
  * Route @mentions to delegate agents.
17808
18273
  * For each mentioned user who has delegate_mention configured,
@@ -17821,13 +18286,19 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
17821
18286
  if (agent.status !== "active" || !agent.delegateMention) continue;
17822
18287
  const [target] = await app.db.select({
17823
18288
  id: agents.uuid,
17824
- status: agents.status
18289
+ status: agents.status,
18290
+ organizationId: agents.organizationId
17825
18291
  }).from(agents).where(eq(agents.uuid, agent.delegateMention)).limit(1);
17826
- if (!target || target.status !== "active") {
18292
+ const verdict = evaluateDelegateTarget(target, organizationId);
18293
+ if (verdict !== "ok") {
17827
18294
  log$1.warn({
17828
18295
  targetAgent: agent.delegateMention,
17829
- sourceAgent: agent.name
17830
- }, "delegate_mention target not active, skipping");
18296
+ sourceAgent: agent.name,
18297
+ sourceOrg: organizationId,
18298
+ targetOrg: target?.organizationId,
18299
+ targetStatus: target?.status,
18300
+ verdict
18301
+ }, DELEGATE_VERDICT_MESSAGES[verdict]);
17831
18302
  continue;
17832
18303
  }
17833
18304
  try {
@@ -17838,6 +18309,7 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
17838
18309
  type: "github_mention",
17839
18310
  mentionedUser: agent.name,
17840
18311
  event: ctx.event,
18312
+ action: ctx.action,
17841
18313
  repository: ctx.repository,
17842
18314
  sender: ctx.sender,
17843
18315
  title: ctx.title,
@@ -17847,7 +18319,8 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
17847
18319
  metadata: {
17848
18320
  source: "github",
17849
18321
  event: "mention_delegation",
17850
- mentionedUser: agent.name
18322
+ mentionedUser: agent.name,
18323
+ action: ctx.action
17851
18324
  }
17852
18325
  });
17853
18326
  notifyRecipients(app.notifier, recipients, msg.id);
@@ -17928,18 +18401,43 @@ async function githubWebhookRoutes(app) {
17928
18401
  ok: true,
17929
18402
  event: "ping"
17930
18403
  });
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
- });
18404
+ const deliveryHeader = request.headers["x-github-delivery"];
18405
+ const deliveryId = typeof deliveryHeader === "string" && deliveryHeader.length > 0 ? deliveryHeader : null;
18406
+ if (deliveryId) {
18407
+ if (!await claimEvent(app.db, deliveryId, "github")) {
18408
+ log$1.info({
18409
+ deliveryId,
18410
+ eventType
18411
+ }, "duplicate GitHub delivery, skipping");
18412
+ return reply.status(200).send({
18413
+ ok: true,
18414
+ event: eventType,
18415
+ deduped: true
18416
+ });
18417
+ }
18418
+ }
18419
+ try {
18420
+ if (eventType === "issues") return await handleIssuesEvent(app, orgId, eventType, payload, reply);
18421
+ if (eventType === "issue_comment") return await handleIssueCommentEvent(app, orgId, eventType, payload, reply);
18422
+ let mentionsRouted = 0;
18423
+ const allowedActions = MENTION_ACTIONS[eventType];
18424
+ const action = isRecord(payload) && typeof payload.action === "string" ? payload.action : void 0;
18425
+ if (allowedActions && action && allowedActions.includes(action)) mentionsRouted = await handleMentionDelegation(app, orgId, eventType, payload);
18426
+ return reply.status(200).send({
18427
+ ok: true,
18428
+ event: eventType,
18429
+ handled: mentionsRouted > 0,
18430
+ mentionsRouted
18431
+ });
18432
+ } catch (err) {
18433
+ if (deliveryId) await unclaimEvent(app.db, deliveryId, "github").catch((unclaimErr) => {
18434
+ log$1.error({
18435
+ err: unclaimErr,
18436
+ deliveryId
18437
+ }, "failed to unclaim GitHub delivery after handler error");
18438
+ });
18439
+ throw err;
18440
+ }
17943
18441
  });
17944
18442
  }
17945
18443
  /** Extract text body from any GitHub webhook event for @mention scanning. */
@@ -17979,12 +18477,14 @@ function extractEventContext(eventType, payload) {
17979
18477
  const sender = isRecord(payload.sender) ? payload.sender : null;
17980
18478
  const repository = typeof repo?.full_name === "string" ? repo.full_name : "";
17981
18479
  const senderLogin = typeof sender?.login === "string" ? sender.login : "";
18480
+ const action = typeof payload.action === "string" ? payload.action : void 0;
17982
18481
  switch (eventType) {
17983
18482
  case "issues": {
17984
18483
  const issue = isRecord(payload.issue) ? payload.issue : null;
17985
18484
  if (!issue) return null;
17986
18485
  return {
17987
18486
  event: "issues",
18487
+ action,
17988
18488
  repository,
17989
18489
  sender: senderLogin,
17990
18490
  title: `Issue #${issue.number}: ${issue.title}`,
@@ -17998,6 +18498,7 @@ function extractEventContext(eventType, payload) {
17998
18498
  if (!issue || !comment) return null;
17999
18499
  return {
18000
18500
  event: "issue_comment",
18501
+ action,
18001
18502
  repository,
18002
18503
  sender: senderLogin,
18003
18504
  title: `Issue #${issue.number}: ${issue.title}`,
@@ -18010,6 +18511,7 @@ function extractEventContext(eventType, payload) {
18010
18511
  if (!pr) return null;
18011
18512
  return {
18012
18513
  event: "pull_request",
18514
+ action,
18013
18515
  repository,
18014
18516
  sender: senderLogin,
18015
18517
  title: `PR #${pr.number}: ${pr.title}`,
@@ -18023,6 +18525,7 @@ function extractEventContext(eventType, payload) {
18023
18525
  if (!pr || !review) return null;
18024
18526
  return {
18025
18527
  event: "pull_request_review",
18528
+ action,
18026
18529
  repository,
18027
18530
  sender: senderLogin,
18028
18531
  title: `PR #${pr.number}: ${pr.title}`,
@@ -18036,6 +18539,7 @@ function extractEventContext(eventType, payload) {
18036
18539
  if (!pr || !comment) return null;
18037
18540
  return {
18038
18541
  event: "pull_request_review_comment",
18542
+ action,
18039
18543
  repository,
18040
18544
  sender: senderLogin,
18041
18545
  title: `PR #${pr.number}: ${pr.title}`,
@@ -18048,6 +18552,7 @@ function extractEventContext(eventType, payload) {
18048
18552
  if (!disc) return null;
18049
18553
  return {
18050
18554
  event: "discussion",
18555
+ action,
18051
18556
  repository,
18052
18557
  sender: senderLogin,
18053
18558
  title: typeof disc.title === "string" ? disc.title : "",
@@ -18061,6 +18566,7 @@ function extractEventContext(eventType, payload) {
18061
18566
  if (!disc || !comment) return null;
18062
18567
  return {
18063
18568
  event: "discussion_comment",
18569
+ action,
18064
18570
  repository,
18065
18571
  sender: senderLogin,
18066
18572
  title: typeof disc.title === "string" ? disc.title : "",
@@ -18073,6 +18579,7 @@ function extractEventContext(eventType, payload) {
18073
18579
  if (!comment) return null;
18074
18580
  return {
18075
18581
  event: "commit_comment",
18582
+ action,
18076
18583
  repository,
18077
18584
  sender: senderLogin,
18078
18585
  title: "Commit comment",
@@ -18088,16 +18595,26 @@ function extractEventContext(eventType, payload) {
18088
18595
  * Only called after action gating confirms this is a "new content" event.
18089
18596
  */
18090
18597
  async function handleMentionDelegation(app, organizationId, eventType, payload) {
18091
- const mentions = extractMentions$1(extractEventText(eventType, payload));
18598
+ const textMentions = extractMentions$1(extractEventText(eventType, payload));
18599
+ const structuralMentions = extractStructuralMentions(eventType, payload);
18600
+ const mentions = [...new Set([...textMentions, ...structuralMentions])];
18092
18601
  const mentionCtx = extractEventContext(eventType, payload);
18093
18602
  if (mentions.length > 0 && mentionCtx) return routeMentionDelegations(app, organizationId, mentions, mentionCtx);
18094
18603
  return 0;
18095
18604
  }
18096
- /** Actions that represent new/changed content (worth scanning for @mentions). */
18605
+ /** Actions that represent new/changed content (worth scanning for @mentions).
18606
+ * Note: `pull_request.review_requested` doesn't carry an @mention in any
18607
+ * text body — the reviewer is in `requested_reviewer.login`. We pick it up
18608
+ * via `extractStructuralMentions`. The complementary `review_request_removed`
18609
+ * is intentionally omitted to avoid notifying the reviewer twice. */
18097
18610
  const MENTION_ACTIONS = {
18098
18611
  issues: ["opened", "edited"],
18099
18612
  issue_comment: ["created"],
18100
- pull_request: ["opened", "edited"],
18613
+ pull_request: [
18614
+ "opened",
18615
+ "edited",
18616
+ "review_requested"
18617
+ ],
18101
18618
  pull_request_review: ["submitted"],
18102
18619
  pull_request_review_comment: ["created"],
18103
18620
  discussion: ["created", "edited"],
@@ -19748,6 +20265,7 @@ async function buildApp(config) {
19748
20265
  await scope.register(orgInvitationRoutes, { prefix: "/invitations" });
19749
20266
  await scope.register(orgMemberRoutes, { prefix: "/members" });
19750
20267
  await scope.register(orgSettingsRoutes, { prefix: "/settings" });
20268
+ await scope.register(orgContextTreeSnapshotRoutes, { prefix: "/context-tree" });
19751
20269
  }), { prefix: "/orgs/:orgId" });
19752
20270
  await api.register(orgWsRoutes(notifier, config.secrets.jwtSecret), { prefix: "/orgs/:orgId/ws" });
19753
20271
  await api.register(userScope("resourcesScope", async (scope) => {