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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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-DHHd2dar.mjs";
5
+ import { $ as runtimeStateMessageSchema, B as isReservedAgentName$1, C as createChatSchema, D as defaultRuntimeConfigPayload, E as createOrgFromMeSchema, F as inboxAckFrameSchema, G as notificationQuerySchema, H as listMeChatsQuerySchema, I as inboxDeliverFrameSchema$1, J as patchOnboardingSchema, K as onboardingEventSchema, L as inboxPollQuerySchema, M as githubDevCallbackQuerySchema, N as githubStartQuerySchema, O as delegateFeishuUserSchema, P as imageInlineContentSchema, Q as refreshTokenSchema, R as isOrgSettingNamespace, S as createAgentSchema, T as createMemberSchema, U as loginSchema, V as joinByInvitationSchema, W as messageSourceSchema$1, Z as rebindAgentSchema, _ as clientRegisterSchema, _t as updateMemberSchema, a as AGENT_VISIBILITY, at as sessionCompletionMessageSchema, b as createAdapterConfigSchema, c as ORG_SETTINGS_NAMESPACES$1, ct as sessionReconcileRequestSchema, d as addParticipantSchema, dt as submitQuestionAnswerSchema, et as safeRedirectPath, f as agentBindRequestSchema, ft as updateAdapterConfigSchema, gt as updateClientCapabilitiesSchema, h as agentTypeSchema$1, ht as updateChatSchema, i as AGENT_STATUSES, it as sendToAgentSchema, j as githubCallbackQuerySchema, k as dryRunAgentRuntimeConfigSchema, l as WS_AUTH_FRAME_TIMEOUT_MS, lt as sessionStateMessageSchema, m as agentRuntimeConfigPayloadSchema$1, mt as updateAgentSchema, n as AGENT_NAME_REGEX$1, nt as selfServiceFeishuBotSchema, ot as sessionEventMessageSchema, p as agentPinnedMessageSchema$1, pt as updateAgentRuntimeConfigSchema, q as paginationQuerySchema, r as AGENT_SELECTOR_HEADER$1, rt as sendMessageSchema, s as MENTION_REGEX, st as sessionEventSchema$1, t as AGENT_BIND_REJECT_REASONS, u as addMeChatParticipantsSchema, ut as stripCode, v as connectTokenExchangeSchema, vt as updateOrganizationSchema, w as createMeChatSchema, x as createAdapterMappingSchema, y as contextTreeSnapshotSchema, yt as wsAuthFrameSchema, z as isRedactedEnvValue } from "./dist-CMhywpXB.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-DHCSQ8kg-DjlSmE9q.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_-NV_lkhfI.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";
@@ -1015,7 +1015,6 @@ const messageFormatSchema = z.enum([
1015
1015
  "card",
1016
1016
  "reference",
1017
1017
  "file",
1018
- "task",
1019
1018
  "question",
1020
1019
  "question_answer"
1021
1020
  ]);
@@ -1199,9 +1198,7 @@ const meChatRowSchema = z.object({
1199
1198
  lastMessageAt: z.string().nullable(),
1200
1199
  lastMessagePreview: z.string().nullable(),
1201
1200
  unreadMentionCount: z.number().int(),
1202
- canReply: z.boolean(),
1203
- taskId: z.string().nullable(),
1204
- taskStatus: z.string().nullable()
1201
+ canReply: z.boolean()
1205
1202
  });
1206
1203
  z.object({
1207
1204
  rows: z.array(meChatRowSchema),
@@ -1677,94 +1674,6 @@ z.object({
1677
1674
  totalMessages: z.number(),
1678
1675
  byOrganization: z.array(orgStatsSchema)
1679
1676
  });
1680
- const taskStatusSchema = z.enum([
1681
- "pending",
1682
- "assigned",
1683
- "working",
1684
- "completed",
1685
- "failed",
1686
- "cancelled"
1687
- ]);
1688
- const taskCreatorTypeSchema = z.enum(["agent", "admin"]);
1689
- const taskMessageEventSchema = z.enum([
1690
- "assigned",
1691
- "status_changed",
1692
- "cancelled"
1693
- ]);
1694
- z.object({
1695
- taskId: z.string(),
1696
- event: taskMessageEventSchema,
1697
- title: z.string(),
1698
- body: z.string().default(""),
1699
- status: taskStatusSchema,
1700
- fromStatus: taskStatusSchema.optional(),
1701
- originRef: z.string().nullable().optional()
1702
- });
1703
- z.object({
1704
- title: z.string().min(1).max(500),
1705
- body: z.string().optional(),
1706
- assigneeAgentId: z.string().optional(),
1707
- originRef: z.string().max(500).optional(),
1708
- metadata: z.record(z.string(), z.unknown()).optional()
1709
- }).extend({ organizationId: z.string().optional() });
1710
- z.object({
1711
- status: z.enum([
1712
- "working",
1713
- "completed",
1714
- "failed"
1715
- ]),
1716
- result: z.string().optional()
1717
- });
1718
- z.object({
1719
- assigneeAgentId: z.string().nullable().optional(),
1720
- status: taskStatusSchema.optional(),
1721
- result: z.string().optional()
1722
- });
1723
- z.object({ chatId: z.string().min(1) });
1724
- const taskSchema = z.object({
1725
- id: z.string(),
1726
- organizationId: z.string(),
1727
- title: z.string(),
1728
- body: z.string(),
1729
- status: taskStatusSchema,
1730
- assigneeAgentId: z.string().nullable(),
1731
- createdByType: taskCreatorTypeSchema,
1732
- createdById: z.string(),
1733
- originRef: z.string().nullable(),
1734
- result: z.string().nullable(),
1735
- metadata: z.record(z.string(), z.unknown()),
1736
- createdAt: z.string(),
1737
- updatedAt: z.string(),
1738
- cancelledAt: z.string().nullable(),
1739
- cancelledByType: taskCreatorTypeSchema.nullable(),
1740
- cancelledById: z.string().nullable()
1741
- });
1742
- const taskChatLinkSchema = z.object({
1743
- taskId: z.string(),
1744
- chatId: z.string(),
1745
- linkedByAgentId: z.string().nullable(),
1746
- linkedAt: z.string()
1747
- });
1748
- taskSchema.extend({ chats: z.array(taskChatLinkSchema) });
1749
- z.object({
1750
- status: taskStatusSchema.optional(),
1751
- assigneeAgentId: z.string().optional(),
1752
- originRef: z.string().optional(),
1753
- limit: z.coerce.number().int().min(1).max(100).default(20),
1754
- cursor: z.string().optional()
1755
- });
1756
- const taskHealthSignalSchema = z.enum([
1757
- "normal",
1758
- "idle_island",
1759
- "awaiting_reply",
1760
- "no_chat",
1761
- "not_applicable"
1762
- ]);
1763
- z.object({
1764
- taskId: z.string(),
1765
- signal: taskHealthSignalSchema,
1766
- reason: z.string()
1767
- });
1768
1677
  const userStatusSchema = z.enum(["active", "suspended"]);
1769
1678
  z.object({
1770
1679
  id: z.string(),
@@ -2876,7 +2785,126 @@ function getHandlerFactory(type) {
2876
2785
  }
2877
2786
  return factory;
2878
2787
  }
2879
- join(DEFAULT_DATA_DIR, "context-tree");
2788
+ const CONTEXT_TREE_DIR = join(DEFAULT_DATA_DIR, "context-tree");
2789
+ /**
2790
+ * Sync the shared Context Tree git clone.
2791
+ *
2792
+ * Clones on first run, pulls on subsequent runs.
2793
+ * Returns the binding on success, null on failure (graceful degradation).
2794
+ */
2795
+ async function syncContextTree(serverUrl, getAccessToken, log, userAgent) {
2796
+ try {
2797
+ execFileSync("git", ["--version"], { stdio: "ignore" });
2798
+ } catch {
2799
+ log("Context Tree sync skipped: git is not installed");
2800
+ return null;
2801
+ }
2802
+ let repo;
2803
+ let branch;
2804
+ try {
2805
+ const config = await new FirstTreeHubSDK({
2806
+ serverUrl,
2807
+ getAccessToken,
2808
+ userAgent
2809
+ }).getContextTreeConfig();
2810
+ if (!config.repo) {
2811
+ log("Context Tree sync skipped: not configured on server");
2812
+ return null;
2813
+ }
2814
+ repo = config.repo;
2815
+ branch = config.branch ?? "main";
2816
+ } catch (err) {
2817
+ log(`Context Tree sync skipped: failed to fetch config from server (${err instanceof Error ? err.message : String(err)})`);
2818
+ return null;
2819
+ }
2820
+ try {
2821
+ if (existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
2822
+ if (execFileSync("git", [
2823
+ "rev-parse",
2824
+ "--abbrev-ref",
2825
+ "HEAD"
2826
+ ], {
2827
+ cwd: CONTEXT_TREE_DIR,
2828
+ encoding: "utf-8",
2829
+ timeout: 5e3
2830
+ }).trim() !== branch) {
2831
+ execFileSync("git", ["checkout", branch], {
2832
+ cwd: CONTEXT_TREE_DIR,
2833
+ stdio: "pipe",
2834
+ timeout: 1e4
2835
+ });
2836
+ log(`Context Tree switched to branch ${branch}`);
2837
+ }
2838
+ execFileSync("git", ["pull", "--ff-only"], {
2839
+ cwd: CONTEXT_TREE_DIR,
2840
+ stdio: "pipe",
2841
+ timeout: 3e4
2842
+ });
2843
+ log(`Context Tree updated (pull)`);
2844
+ } else {
2845
+ mkdirSync(CONTEXT_TREE_DIR, { recursive: true });
2846
+ execFileSync("git", [
2847
+ "clone",
2848
+ "--branch",
2849
+ branch,
2850
+ "--single-branch",
2851
+ repo,
2852
+ CONTEXT_TREE_DIR
2853
+ ], {
2854
+ stdio: "pipe",
2855
+ timeout: 6e4
2856
+ });
2857
+ log(`Context Tree cloned from ${repo} (branch: ${branch})`);
2858
+ }
2859
+ return {
2860
+ path: CONTEXT_TREE_DIR,
2861
+ repoUrl: repo,
2862
+ branch
2863
+ };
2864
+ } catch (err) {
2865
+ const msg = err instanceof Error ? err.message : String(err);
2866
+ log(`Context Tree sync failed: ${msg}`);
2867
+ log("Check that git credentials (SSH key or credential helper) are configured for this repo");
2868
+ if ((msg.includes("cannot fast-forward") || msg.includes("not possible to fast-forward") || msg.includes("CONFLICT")) && existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
2869
+ log("Diverged history detected, attempting fresh clone...");
2870
+ try {
2871
+ rmSync(CONTEXT_TREE_DIR, {
2872
+ recursive: true,
2873
+ force: true
2874
+ });
2875
+ mkdirSync(CONTEXT_TREE_DIR, { recursive: true });
2876
+ execFileSync("git", [
2877
+ "clone",
2878
+ "--branch",
2879
+ branch,
2880
+ "--single-branch",
2881
+ repo,
2882
+ CONTEXT_TREE_DIR
2883
+ ], {
2884
+ stdio: "pipe",
2885
+ timeout: 6e4
2886
+ });
2887
+ log("Context Tree re-cloned successfully");
2888
+ return {
2889
+ path: CONTEXT_TREE_DIR,
2890
+ repoUrl: repo,
2891
+ branch
2892
+ };
2893
+ } catch {
2894
+ log("Context Tree re-clone also failed, continuing without context");
2895
+ }
2896
+ }
2897
+ if (existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
2898
+ log("Using existing Context Tree clone despite sync failure");
2899
+ return {
2900
+ path: CONTEXT_TREE_DIR,
2901
+ repoUrl: repo,
2902
+ branch
2903
+ };
2904
+ }
2905
+ return null;
2906
+ }
2907
+ }
2880
2908
  /**
2881
2909
  * Marker file written into every workspace so the Codex CLI's project-root
2882
2910
  * detection (configured via `project_root_markers: ["first-tree-workspace"]`)
@@ -2948,8 +2976,6 @@ function installFirstTreeIntegration(options) {
2948
2976
  const integrateArgs = [
2949
2977
  "tree",
2950
2978
  "integrate",
2951
- "--source-path",
2952
- workspacePath,
2953
2979
  "--tree-path",
2954
2980
  contextTreePath,
2955
2981
  "--mode",
@@ -2984,8 +3010,13 @@ function installFirstTreeIntegration(options) {
2984
3010
  } catch (err) {
2985
3011
  const msg = err instanceof Error ? err.message : String(err);
2986
3012
  const binaryMissing = /ENOENT|not found|command not found/i.test(msg);
3013
+ const unsupportedByThisCli = /unknown (?:option|command|argument)|unrecognized option/i.test(msg);
3014
+ const shouldRetry = binaryMissing || unsupportedByThisCli;
2987
3015
  const isLastAttempt = index === attempts.length - 1;
2988
- if (binaryMissing && !isLastAttempt) continue;
3016
+ if (shouldRetry && !isLastAttempt) {
3017
+ log(`First-tree integration via ${attempt.label} unusable; falling back: ${msg.slice(0, 200)}`);
3018
+ continue;
3019
+ }
2989
3020
  log(`First-tree integration skipped (${attempt.label}): ${msg.slice(0, 200)}`);
2990
3021
  return false;
2991
3022
  }
@@ -4608,6 +4639,8 @@ const createClaudeCodeHandler = (config) => {
4608
4639
  }
4609
4640
  }
4610
4641
  const contextTreePath = config.contextTreePath ?? null;
4642
+ const contextTreeRepoUrl = config.contextTreeRepoUrl ?? null;
4643
+ const agentName = config.agentName ?? null;
4611
4644
  /**
4612
4645
  * Materialise the runtime config's `gitRepos` into worktrees under `cwd`.
4613
4646
  * Idempotent across resumes: reuses an existing Hub-managed worktree if
@@ -4675,7 +4708,8 @@ const createClaudeCodeHandler = (config) => {
4675
4708
  if (contextTreePath) installFirstTreeIntegration({
4676
4709
  workspacePath: workspace,
4677
4710
  contextTreePath,
4678
- workspaceId: sessionCtx.chatId,
4711
+ workspaceId: agentName ?? sessionCtx.chatId,
4712
+ treeRepoUrl: contextTreeRepoUrl ?? void 0,
4679
4713
  log: (msg) => sessionCtx.log(msg)
4680
4714
  });
4681
4715
  }
@@ -4864,6 +4898,8 @@ const createCodexHandler = (config) => {
4864
4898
  const agentConfigCache = config.agentConfigCache ?? null;
4865
4899
  const gitMirrorManager = config.gitMirrorManager ?? null;
4866
4900
  const contextTreePath = config.contextTreePath ?? null;
4901
+ const contextTreeRepoUrl = config.contextTreeRepoUrl ?? null;
4902
+ const agentName = config.agentName ?? null;
4867
4903
  let cwd = null;
4868
4904
  let codex = null;
4869
4905
  let thread = null;
@@ -5135,6 +5171,17 @@ const createCodexHandler = (config) => {
5135
5171
  if (inputs.length === 0) return;
5136
5172
  await runTurn(inputs.join("\n\n"), sessionCtx);
5137
5173
  }
5174
+ /** Install the first-tree skill + binding block; no-op when context tree is unconfigured. */
5175
+ function ensureFirstTreeBinding(workspace, sessionCtx) {
5176
+ if (!contextTreePath) return;
5177
+ installFirstTreeIntegration({
5178
+ workspacePath: workspace,
5179
+ contextTreePath,
5180
+ workspaceId: agentName ?? sessionCtx.chatId,
5181
+ treeRepoUrl: contextTreeRepoUrl ?? void 0,
5182
+ log: (msg) => sessionCtx.log(msg)
5183
+ });
5184
+ }
5138
5185
  return {
5139
5186
  async start(message, sessionCtx) {
5140
5187
  ctx = sessionCtx;
@@ -5160,6 +5207,7 @@ const createCodexHandler = (config) => {
5160
5207
  content: buildAgentBriefing(payload)
5161
5208
  }
5162
5209
  });
5210
+ ensureFirstTreeBinding(cwd, sessionCtx);
5163
5211
  await prepareGitWorktrees(payload, cwd, sessionCtx.chatId);
5164
5212
  codex = new Codex({
5165
5213
  env: buildEnv(sessionCtx),
@@ -5184,17 +5232,20 @@ const createCodexHandler = (config) => {
5184
5232
  env: [],
5185
5233
  gitRepos: []
5186
5234
  };
5187
- bootstrapWorkspace({
5188
- workspacePath: cwd,
5189
- identity: sessionCtx.agent,
5190
- contextTreePath,
5191
- serverUrl: sessionCtx.sdk.serverUrl,
5192
- chatId: sessionCtx.chatId,
5193
- briefing: {
5194
- format: "agents-md",
5195
- content: buildAgentBriefing(payload)
5196
- }
5197
- });
5235
+ if (!existsSync(join(cwd, ".agent", "identity.json"))) {
5236
+ bootstrapWorkspace({
5237
+ workspacePath: cwd,
5238
+ identity: sessionCtx.agent,
5239
+ contextTreePath,
5240
+ serverUrl: sessionCtx.sdk.serverUrl,
5241
+ chatId: sessionCtx.chatId,
5242
+ briefing: {
5243
+ format: "agents-md",
5244
+ content: buildAgentBriefing(payload)
5245
+ }
5246
+ });
5247
+ ensureFirstTreeBinding(cwd, sessionCtx);
5248
+ }
5198
5249
  await prepareGitWorktrees(payload, cwd, sessionCtx.chatId);
5199
5250
  codex = new Codex({
5200
5251
  env: buildEnv(sessionCtx),
@@ -6227,7 +6278,7 @@ var AgentSlot = class {
6227
6278
  lastActivityMs: 0
6228
6279
  };
6229
6280
  }
6230
- async start(contextTreePath) {
6281
+ async start(contextTreeBinding) {
6231
6282
  const sdk = (await this.clientConnection.bindAgent(this.config.agentId, this.config.runtimeType ?? this.config.type, this.config.runtimeVersion)).sdk;
6232
6283
  this.sdk = sdk;
6233
6284
  const agent = await sdk.register();
@@ -6305,7 +6356,9 @@ var AgentSlot = class {
6305
6356
  handlerFactory: this.config.handlerFactory,
6306
6357
  handlerConfig: {
6307
6358
  workspaceRoot: join(DEFAULT_DATA_DIR, "workspaces", this.config.name),
6308
- contextTreePath: contextTreePath ?? void 0,
6359
+ agentName: this.config.name,
6360
+ contextTreePath: contextTreeBinding?.path,
6361
+ contextTreeRepoUrl: contextTreeBinding?.repoUrl,
6309
6362
  gitMirrorManager
6310
6363
  },
6311
6364
  agentIdentity: {
@@ -7188,6 +7241,14 @@ var ClientRuntime = class {
7188
7241
  watcher = null;
7189
7242
  debounceTimer = null;
7190
7243
  /**
7244
+ * Per-org Context Tree binding resolved at `start()`. Threaded through every
7245
+ * `slot.start()` so handlers can copy `AGENT.md` / root `NODE.md` into the
7246
+ * agent workspace's `.agent/context/` and install the first-tree skill.
7247
+ * `null` when the user has no primary org, the org has no tree configured,
7248
+ * or git sync failed — handlers degrade gracefully (empty context dir).
7249
+ */
7250
+ contextTreeBinding = null;
7251
+ /**
7191
7252
  * Directory we write auto-registered agent configs into (same path that
7192
7253
  * `first-tree-hub agent add` uses). Set by `watchAgentsDir` so the
7193
7254
  * `agent:pinned` handler knows where to materialise new configs.
@@ -7247,6 +7308,7 @@ var ClientRuntime = class {
7247
7308
  this.agentIds.add(config.agentId);
7248
7309
  }
7249
7310
  async start() {
7311
+ this.contextTreeBinding = await syncContextTree(this.serverUrl, (opts) => ensureFreshAccessToken(opts), (msg) => print.status("[context-tree]", msg), CLI_USER_AGENT);
7250
7312
  if (this.options.currentVersion && this.options.update) this.updateManager = UpdateManager.attach(this.connection, {
7251
7313
  currentVersion: this.options.currentVersion,
7252
7314
  ...this.options.update,
@@ -7265,7 +7327,7 @@ var ClientRuntime = class {
7265
7327
  }
7266
7328
  await Promise.allSettled(this.agents.map(async (agent) => {
7267
7329
  try {
7268
- const identity = await agent.slot.start();
7330
+ const identity = await agent.slot.start(this.contextTreeBinding);
7269
7331
  print.check(true, `${agent.name}: connected`, `agent: ${identity.displayName ?? identity.agentId}`);
7270
7332
  } catch (error) {
7271
7333
  const msg = error instanceof Error ? error.message : String(error);
@@ -7392,7 +7454,7 @@ var ClientRuntime = class {
7392
7454
  startAgent(name) {
7393
7455
  const entry = this.agents.find((a) => a.name === name);
7394
7456
  if (!entry) return;
7395
- entry.slot.start().then((identity) => {
7457
+ entry.slot.start(this.contextTreeBinding).then((identity) => {
7396
7458
  print.check(true, `${name}: connected`, `agent: ${identity.displayName ?? identity.agentId}`);
7397
7459
  }).catch((err) => {
7398
7460
  const msg = err instanceof Error ? err.message : String(err);
@@ -9029,7 +9091,7 @@ async function onboardCreate(args) {
9029
9091
  }
9030
9092
  const runtimeAgent = args.type === "human" ? args.assistant : args.id;
9031
9093
  if (args.feishuBotAppId && args.feishuBotAppSecret) {
9032
- const { bindFeishuBot } = await import("./feishu-fLnwqCOs.mjs").then((n) => n.r);
9094
+ const { bindFeishuBot } = await import("./feishu-tkZS0vvL.mjs").then((n) => n.r);
9033
9095
  const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
9034
9096
  if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
9035
9097
  else {
@@ -10242,7 +10304,7 @@ function createFeedbackHandler(config) {
10242
10304
  return { handle };
10243
10305
  }
10244
10306
  //#endregion
10245
- //#region ../server/dist/app-CkYiQS_D.mjs
10307
+ //#region ../server/dist/app-D4vCx0C0.mjs
10246
10308
  var import_fastify_opentelemetry = /* @__PURE__ */ __toESM(require_fastify_opentelemetry(), 1);
10247
10309
  init_esm();
10248
10310
  var __defProp = Object.defineProperty;
@@ -10267,50 +10329,6 @@ const adapterAgentMappings = pgTable("adapter_agent_mappings", {
10267
10329
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
10268
10330
  }, (table) => [unique("uq_adapter_agent_mapping").on(table.platform, table.externalUserId)]);
10269
10331
  /**
10270
- * Tasks — lightweight work units. Process descriptors, not tickets.
10271
- * Immutable status state machine: pending → assigned → working → (completed | failed | cancelled).
10272
- * Sub-tasks (parent_task_id) are deferred to a later phase.
10273
- *
10274
- * Referential integrity (org / assignee / chat) is enforced at the service layer,
10275
- * not via DB foreign keys — see `services/task.ts`.
10276
- */
10277
- const tasks = pgTable("tasks", {
10278
- id: text("id").primaryKey(),
10279
- organizationId: text("organization_id").notNull(),
10280
- title: text("title").notNull(),
10281
- body: text("body").notNull().default(""),
10282
- status: text("status").$type().notNull(),
10283
- assigneeAgentId: text("assignee_agent_id"),
10284
- createdByType: text("created_by_type").$type().notNull(),
10285
- createdById: text("created_by_id").notNull(),
10286
- originRef: text("origin_ref"),
10287
- result: text("result"),
10288
- metadata: jsonb("metadata").$type().notNull().default({}),
10289
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
10290
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
10291
- cancelledAt: timestamp("cancelled_at", { withTimezone: true }),
10292
- cancelledByType: text("cancelled_by_type").$type(),
10293
- cancelledById: text("cancelled_by_id")
10294
- }, (table) => [
10295
- index("idx_tasks_org_status").on(table.organizationId, table.status),
10296
- index("idx_tasks_assignee_status").on(table.assigneeAgentId, table.status),
10297
- index("idx_tasks_origin_ref").on(table.originRef),
10298
- index("idx_tasks_org_created_at").on(table.organizationId, table.createdAt)
10299
- ]);
10300
- /**
10301
- * Task ↔ Chat association (M:N). A task may be executed across multiple chats;
10302
- * a chat may host work for multiple tasks over its lifetime.
10303
- *
10304
- * No FK constraints — when a task or chat is deleted, the service layer is
10305
- * responsible for deleting linked rows here first.
10306
- */
10307
- const taskChats = pgTable("task_chats", {
10308
- taskId: text("task_id").notNull(),
10309
- chatId: text("chat_id").notNull(),
10310
- linkedByAgentId: text("linked_by_agent_id"),
10311
- linkedAt: timestamp("linked_at", { withTimezone: true }).notNull().defaultNow()
10312
- }, (table) => [primaryKey({ columns: [table.taskId, table.chatId] }), index("idx_task_chats_chat").on(table.chatId)]);
10313
- /**
10314
10332
  * Pull the JWT-verified `UserScope` off the request. The `userAuthHook`
10315
10333
  * middleware populates `request.user` synchronously before any handler
10316
10334
  * runs; this helper just narrows the optional and throws a clean 401 if
@@ -10445,31 +10463,6 @@ async function assertAgentManageableByUser(db, userId, agentUuid) {
10445
10463
  return scope;
10446
10464
  }
10447
10465
  /**
10448
- * Gate access to a task. Allowed for any active member of the task's org —
10449
- * mirrors the original inline gate in `api/tasks.ts` that this helper
10450
- * replaces. Returns both the task's org row and the caller's resolved
10451
- * `OrgScope`, so handlers can read `scope.memberId` for audit fields.
10452
- */
10453
- async function requireTaskAccess(request, db) {
10454
- const { userId } = requireUser(request);
10455
- const { taskId } = request.params;
10456
- const [task] = await db.select({ organizationId: tasks.organizationId }).from(tasks).where(eq(tasks.id, taskId)).limit(1);
10457
- if (!task) throw new NotFoundError(`Task "${taskId}" not found`);
10458
- const caller = await resolveCallerInOrg(db, userId, task.organizationId);
10459
- const scope = {
10460
- userId,
10461
- organizationId: task.organizationId,
10462
- memberId: caller.memberId,
10463
- role: caller.role,
10464
- humanAgentId: caller.humanAgentId
10465
- };
10466
- stampOrgScope(request, scope);
10467
- return {
10468
- task,
10469
- scope
10470
- };
10471
- }
10472
- /**
10473
10466
  * Assert every agent in `agentIds` is visible to `scope` and lives in
10474
10467
  * `scope.organizationId`. Used by chat-create to keep visibility rules out of
10475
10468
  * the service layer's signature.
@@ -10905,10 +10898,9 @@ async function ensureDefaultOrganization(db) {
10905
10898
  return org ?? existing;
10906
10899
  }
10907
10900
  /**
10908
- * Names beginning with `__` are reserved for Hub-internal pseudo agents
10909
- * (e.g. the task notifier). User-facing creation must not be able to
10910
- * squat on them, otherwise internal traffic could be routed through a
10911
- * real account.
10901
+ * Names beginning with `__` are reserved for Hub-internal pseudo agents.
10902
+ * User-facing creation must not be able to squat on them, otherwise
10903
+ * internal traffic could be routed through a real account.
10912
10904
  */
10913
10905
  const RESERVED_AGENT_NAME_PREFIX = "__";
10914
10906
  /**
@@ -11007,6 +10999,28 @@ async function resolveAgentClient(db, data) {
11007
10999
  return client.id;
11008
11000
  }
11009
11001
  /**
11002
+ * Validate a `delegateMention` write at the service layer. Two checks:
11003
+ * 1. Target uuid must resolve to an existing agent — dangling references
11004
+ * would silently break webhook delegation at runtime.
11005
+ * 2. Target must belong to the same organization as the source agent —
11006
+ * cross-org delegate links are rejected here at the source so the
11007
+ * database never accumulates dirty rows. The webhook router has a
11008
+ * defense-in-depth check that filters them at fan-out time, but this
11009
+ * keeps the data clean and gives the admin UI an immediate 422 instead
11010
+ * of a silent runtime drop.
11011
+ *
11012
+ * `null` clears the field — handled by the caller; we are only invoked when
11013
+ * the caller wrote a non-null uuid.
11014
+ */
11015
+ async function validateDelegateMentionTarget(db, targetUuid, sourceOrgId) {
11016
+ const [target] = await db.select({
11017
+ uuid: agents.uuid,
11018
+ organizationId: agents.organizationId
11019
+ }).from(agents).where(eq(agents.uuid, targetUuid)).limit(1);
11020
+ if (!target) throw new BadRequestError(`delegateMention target "${targetUuid}" not found`);
11021
+ if (target.organizationId !== sourceOrgId) throw new BadRequestError("delegateMention target must belong to the same organization as the agent");
11022
+ }
11023
+ /**
11010
11024
  * Pick the first admin member in the org for internal system agents. Throws
11011
11025
  * if the org has no admin — the caller should surface the error so an admin
11012
11026
  * is created before the system tries to register more agents.
@@ -11046,6 +11060,7 @@ async function createAgent(db, data, options = {}) {
11046
11060
  type: data.type
11047
11061
  });
11048
11062
  await ensureClientSupportsRuntimeProvider(db, clientId, runtimeProvider, { force: options.force });
11063
+ if (data.delegateMention) await validateDelegateMentionTarget(db, data.delegateMention, orgId);
11049
11064
  const [org] = await db.select({ maxAgents: organizations.maxAgents }).from(organizations).where(eq(organizations.id, orgId)).limit(1);
11050
11065
  if (org && org.maxAgents > 0) {
11051
11066
  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.`);
@@ -11192,7 +11207,10 @@ async function updateAgent(db, uuid, data) {
11192
11207
  const updates = { updatedAt: /* @__PURE__ */ new Date() };
11193
11208
  if (data.type !== void 0) updates.type = data.type;
11194
11209
  if (data.displayName !== void 0) updates.displayName = data.displayName;
11195
- if (data.delegateMention !== void 0) updates.delegateMention = data.delegateMention;
11210
+ if (data.delegateMention !== void 0) {
11211
+ if (data.delegateMention !== null) await validateDelegateMentionTarget(db, data.delegateMention, agent.organizationId);
11212
+ updates.delegateMention = data.delegateMention;
11213
+ }
11196
11214
  if (data.visibility !== void 0) updates.visibility = data.visibility;
11197
11215
  if (data.metadata !== void 0) updates.metadata = data.metadata;
11198
11216
  if (data.managerId !== void 0) {
@@ -12092,462 +12110,6 @@ async function agentSendToAgentRoutes(app) {
12092
12110
  });
12093
12111
  });
12094
12112
  }
12095
- /** Legal status transitions. Service enforces; API maps violations to 400. */
12096
- const STATUS_TRANSITIONS = {
12097
- pending: ["assigned", "cancelled"],
12098
- assigned: ["working", "cancelled"],
12099
- working: [
12100
- "completed",
12101
- "failed",
12102
- "cancelled"
12103
- ],
12104
- completed: [],
12105
- failed: [],
12106
- cancelled: []
12107
- };
12108
- function isLegalTransition(from, to) {
12109
- return STATUS_TRANSITIONS[from]?.includes(to) ?? false;
12110
- }
12111
- function isTerminal(status) {
12112
- return TASK_TERMINAL_STATUSES.includes(status);
12113
- }
12114
- /**
12115
- * Reserved name for the hub-owned task notifier pseudo agent. The `__` prefix
12116
- * is rejected by `createAgent`, so real users cannot squat on this identity.
12117
- */
12118
- const SYSTEM_TASKS_AGENT_NAME = "__hub_system_tasks";
12119
- /**
12120
- * Ensure a task-notifier pseudo agent exists in the given organization and
12121
- * return its UUID. Used as the sender for task notification messages so they
12122
- * flow through the normal chat/inbox pipeline. Idempotent under concurrent
12123
- * creation via the unique `(organization_id, name)` constraint.
12124
- */
12125
- async function ensureSystemTasksAgent(db, organizationId) {
12126
- const [existing] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, organizationId), eq(agents.name, SYSTEM_TASKS_AGENT_NAME), ne(agents.status, AGENT_STATUSES.DELETED))).limit(1);
12127
- if (existing) return existing.uuid;
12128
- const uuid = uuidv7();
12129
- const inboxId = `inbox_${uuid}`;
12130
- const [adminMember] = await db.select({ id: members.id }).from(members).where(and(eq(members.organizationId, organizationId), eq(members.role, "admin"))).orderBy(asc(members.createdAt)).limit(1);
12131
- if (!adminMember) throw new ConflictError(`Cannot create system tasks agent in organization "${organizationId}" — no admin member exists.`);
12132
- try {
12133
- const [created] = await db.insert(agents).values({
12134
- uuid,
12135
- name: SYSTEM_TASKS_AGENT_NAME,
12136
- organizationId,
12137
- type: AGENT_TYPES.AUTONOMOUS_AGENT,
12138
- displayName: "System · Tasks",
12139
- inboxId,
12140
- status: AGENT_STATUSES.ACTIVE,
12141
- source: AGENT_SOURCES.ADMIN_API,
12142
- metadata: {
12143
- system: true,
12144
- role: "task-notifier"
12145
- },
12146
- managerId: adminMember.id
12147
- }).returning({ uuid: agents.uuid });
12148
- if (created) return created.uuid;
12149
- } catch (err) {
12150
- if ((err?.code ?? err?.cause?.code ?? "") !== "23505") throw err;
12151
- }
12152
- const [row] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, organizationId), eq(agents.name, SYSTEM_TASKS_AGENT_NAME))).limit(1);
12153
- if (!row) throw new Error("ensureSystemTasksAgent: agent missing after conflict");
12154
- return row.uuid;
12155
- }
12156
- function resolveCreator(actor) {
12157
- if (actor.type === "agent") return {
12158
- type: TASK_CREATOR_TYPES.AGENT,
12159
- id: actor.agentId
12160
- };
12161
- return {
12162
- type: TASK_CREATOR_TYPES.ADMIN,
12163
- id: actor.adminId
12164
- };
12165
- }
12166
- /**
12167
- * Assert the task allows the given agent actor to mutate its chat associations.
12168
- * Only the creator or assignee (for agents) or any admin may do so.
12169
- */
12170
- function assertCanMutateTaskChats(task, actor) {
12171
- if (actor.type === "admin") return;
12172
- const isAssignee = task.assigneeAgentId === actor.agentId;
12173
- const isCreator = task.createdByType === TASK_CREATOR_TYPES.AGENT && task.createdById === actor.agentId;
12174
- if (!isAssignee && !isCreator) throw new ForbiddenError("Only the task creator or assignee may modify its chat associations");
12175
- }
12176
- async function loadAssigneeOrThrow(db, assigneeAgentId, expectedOrgId) {
12177
- const [assignee] = await db.select({
12178
- uuid: agents.uuid,
12179
- organizationId: agents.organizationId,
12180
- status: agents.status
12181
- }).from(agents).where(eq(agents.uuid, assigneeAgentId)).limit(1);
12182
- if (!assignee || assignee.status === AGENT_STATUSES.DELETED) throw new BadRequestError(`Assignee agent "${assigneeAgentId}" not found`);
12183
- if (assignee.organizationId !== expectedOrgId) throw new BadRequestError("Assignee agent belongs to a different organization");
12184
- if (assignee.status === AGENT_STATUSES.SUSPENDED) throw new BadRequestError(`Assignee agent "${assigneeAgentId}" is suspended`);
12185
- return assignee;
12186
- }
12187
- /**
12188
- * Create a task.
12189
- *
12190
- * Initial status is determined by assignee:
12191
- * - no assignee → "pending"
12192
- * - assignee is an agent and equals the creator → "working" (work-first; no notification)
12193
- * - assignee set and differs from creator → "assigned" (task-first; notification dispatched)
12194
- *
12195
- * Task-first notifications go through the regular message+inbox pipeline via a per-org
12196
- * task-notifier pseudo agent. The caller is responsible for triggering notifier fan-out
12197
- * using the returned notification recipients.
12198
- */
12199
- async function createTask(db, actor, input) {
12200
- const [org] = await db.select({ id: organizations.id }).from(organizations).where(eq(organizations.id, input.organizationId)).limit(1);
12201
- if (!org) throw new NotFoundError(`Organization "${input.organizationId}" not found`);
12202
- if (input.assigneeAgentId) await loadAssigneeOrThrow(db, input.assigneeAgentId, input.organizationId);
12203
- if (actor.type === "agent" && actor.organizationId !== input.organizationId) throw new ForbiddenError("Cannot create tasks in a different organization");
12204
- const creator = resolveCreator(actor);
12205
- const selfAssigned = input.assigneeAgentId !== void 0 && actor.type === "agent" && input.assigneeAgentId === actor.agentId;
12206
- let initialStatus;
12207
- if (!input.assigneeAgentId) initialStatus = TASK_STATUSES.PENDING;
12208
- else if (selfAssigned) initialStatus = TASK_STATUSES.WORKING;
12209
- else initialStatus = TASK_STATUSES.ASSIGNED;
12210
- const taskId = uuidv7();
12211
- const [task] = await db.insert(tasks).values({
12212
- id: taskId,
12213
- organizationId: input.organizationId,
12214
- title: input.title,
12215
- body: input.body ?? "",
12216
- status: initialStatus,
12217
- assigneeAgentId: input.assigneeAgentId ?? null,
12218
- createdByType: creator.type,
12219
- createdById: creator.id,
12220
- originRef: input.originRef ?? null,
12221
- metadata: input.metadata ?? {}
12222
- }).returning();
12223
- if (!task) throw new Error("Unexpected: INSERT RETURNING produced no row");
12224
- let notification;
12225
- if (initialStatus === TASK_STATUSES.ASSIGNED && task.assigneeAgentId) notification = await dispatchTaskSystemMessage(db, task, "assigned");
12226
- return {
12227
- task,
12228
- notification
12229
- };
12230
- }
12231
- /** Compose and send a system message describing a task state change to the assignee's chat. */
12232
- async function dispatchTaskSystemMessage(db, task, event, fromStatus) {
12233
- if (!task.assigneeAgentId) return void 0;
12234
- const systemAgentId = await ensureSystemTasksAgent(db, task.organizationId);
12235
- if (systemAgentId === task.assigneeAgentId) return void 0;
12236
- const chat = await findOrCreateDirectChat(db, systemAgentId, task.assigneeAgentId);
12237
- const content = {
12238
- taskId: task.id,
12239
- event,
12240
- title: task.title,
12241
- body: task.body,
12242
- status: task.status,
12243
- ...fromStatus ? { fromStatus } : {},
12244
- originRef: task.originRef
12245
- };
12246
- return sendMessage(db, chat.id, systemAgentId, {
12247
- format: "task",
12248
- content,
12249
- metadata: {
12250
- taskId: task.id,
12251
- event,
12252
- mentions: [task.assigneeAgentId]
12253
- }
12254
- });
12255
- }
12256
- /**
12257
- * Fetch a task, optionally asserting it belongs to `expectedOrgId`. Cross-org
12258
- * access is reported as NotFound so we don't leak existence across tenants.
12259
- */
12260
- async function getTask(db, taskId, expectedOrgId) {
12261
- const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId)).limit(1);
12262
- if (!task) throw new NotFoundError(`Task "${taskId}" not found`);
12263
- if (expectedOrgId !== void 0 && task.organizationId !== expectedOrgId) throw new NotFoundError(`Task "${taskId}" not found`);
12264
- return task;
12265
- }
12266
- async function getTaskDetail(db, taskId, expectedOrgId) {
12267
- const task = await getTask(db, taskId, expectedOrgId);
12268
- const links = await db.select().from(taskChats).where(eq(taskChats.taskId, taskId));
12269
- return {
12270
- ...task,
12271
- chats: links.map((c) => ({
12272
- taskId: c.taskId,
12273
- chatId: c.chatId,
12274
- linkedByAgentId: c.linkedByAgentId,
12275
- linkedAt: c.linkedAt.toISOString()
12276
- }))
12277
- };
12278
- }
12279
- async function listTasks(db, organizationId, query) {
12280
- const conditions = [eq(tasks.organizationId, organizationId)];
12281
- if (query.status) conditions.push(eq(tasks.status, query.status));
12282
- if (query.assigneeAgentId) conditions.push(eq(tasks.assigneeAgentId, query.assigneeAgentId));
12283
- if (query.originRef) conditions.push(eq(tasks.originRef, query.originRef));
12284
- if (query.cursor) conditions.push(lt(tasks.createdAt, new Date(query.cursor)));
12285
- const rows = await db.select().from(tasks).where(and(...conditions)).orderBy(desc(tasks.createdAt)).limit(query.limit + 1);
12286
- const hasMore = rows.length > query.limit;
12287
- const items = hasMore ? rows.slice(0, query.limit) : rows;
12288
- const last = items[items.length - 1];
12289
- return {
12290
- items,
12291
- nextCursor: hasMore && last ? last.createdAt.toISOString() : null
12292
- };
12293
- }
12294
- /** Agent self-report: working / completed / failed. */
12295
- async function updateTaskStatus(db, taskId, actor, data) {
12296
- const existing = await getTask(db, taskId);
12297
- if (actor.type !== "agent") throw new ForbiddenError("updateTaskStatus is for agent self-report; use adminUpdateTask for admin actions");
12298
- if (existing.assigneeAgentId !== actor.agentId) throw new ForbiddenError("Only the assignee may update this task");
12299
- const from = existing.status;
12300
- const to = data.status;
12301
- if (!isLegalTransition(from, to)) throw new BadRequestError(`Illegal status transition: ${from} → ${to}`);
12302
- if (to === TASK_STATUSES.COMPLETED && data.result === void 0) throw new BadRequestError("Completion requires a result (may be an empty string)");
12303
- const updates = {
12304
- status: to,
12305
- updatedAt: /* @__PURE__ */ new Date()
12306
- };
12307
- if (data.result !== void 0) updates.result = data.result;
12308
- const [updated] = await db.update(tasks).set(updates).where(eq(tasks.id, taskId)).returning();
12309
- if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
12310
- return { task: updated };
12311
- }
12312
- /** Admin-facing update: may re-assign while pending, or force a status transition (still gated by state machine). */
12313
- async function adminUpdateTask(db, taskId, actor, data) {
12314
- if (actor.type !== "admin") throw new ForbiddenError("adminUpdateTask requires admin actor");
12315
- const existing = await getTask(db, taskId);
12316
- if (data.status === TASK_STATUSES.CANCELLED) {
12317
- if (isTerminal(existing.status)) throw new ConflictError(`Task is already in terminal state "${existing.status}"`);
12318
- return cancelTask(db, taskId, actor);
12319
- }
12320
- const updates = { updatedAt: /* @__PURE__ */ new Date() };
12321
- let notify = false;
12322
- if (data.assigneeAgentId !== void 0) {
12323
- if (existing.status !== TASK_STATUSES.PENDING && data.assigneeAgentId !== existing.assigneeAgentId) throw new BadRequestError("Cannot reassign a task that is not pending");
12324
- if (data.assigneeAgentId !== null) {
12325
- await loadAssigneeOrThrow(db, data.assigneeAgentId, existing.organizationId);
12326
- updates.assigneeAgentId = data.assigneeAgentId;
12327
- updates.status = TASK_STATUSES.ASSIGNED;
12328
- notify = true;
12329
- } else {
12330
- updates.assigneeAgentId = null;
12331
- updates.status = TASK_STATUSES.PENDING;
12332
- }
12333
- }
12334
- if (data.status !== void 0 && data.status !== existing.status) {
12335
- const from = updates.status ?? existing.status;
12336
- if (!isLegalTransition(from, data.status)) throw new BadRequestError(`Illegal status transition: ${from} → ${data.status}`);
12337
- updates.status = data.status;
12338
- }
12339
- if (data.result !== void 0) updates.result = data.result;
12340
- const resolvedStatus = updates.status ?? existing.status;
12341
- const resolvedAssignee = updates.assigneeAgentId === void 0 ? existing.assigneeAgentId : updates.assigneeAgentId;
12342
- if (resolvedStatus === TASK_STATUSES.ASSIGNED && !resolvedAssignee) throw new BadRequestError("Cannot set status to \"assigned\" without an assignee");
12343
- const [updated] = await db.update(tasks).set(updates).where(eq(tasks.id, taskId)).returning();
12344
- if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
12345
- let notification;
12346
- if (notify && updated.assigneeAgentId) notification = await dispatchTaskSystemMessage(db, updated, "assigned");
12347
- return {
12348
- task: updated,
12349
- notification
12350
- };
12351
- }
12352
- async function cancelTask(db, taskId, actor) {
12353
- const existing = await getTask(db, taskId);
12354
- if (isTerminal(existing.status)) throw new ConflictError(`Task is already in terminal state "${existing.status}"`);
12355
- if (actor.type === "agent") {
12356
- const isAssignee = existing.assigneeAgentId === actor.agentId;
12357
- const isCreator = existing.createdByType === TASK_CREATOR_TYPES.AGENT && existing.createdById === actor.agentId;
12358
- if (!isAssignee && !isCreator) throw new ForbiddenError("Only the assignee or creator may cancel this task");
12359
- }
12360
- const now = /* @__PURE__ */ new Date();
12361
- const { type: cancelType, id: cancelId } = resolveCreator(actor);
12362
- const [updated] = await db.update(tasks).set({
12363
- status: TASK_STATUSES.CANCELLED,
12364
- cancelledAt: now,
12365
- cancelledByType: cancelType,
12366
- cancelledById: cancelId,
12367
- updatedAt: now
12368
- }).where(eq(tasks.id, taskId)).returning();
12369
- if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
12370
- let notification;
12371
- if (updated.assigneeAgentId && !(actor.type === "agent" && actor.agentId === updated.assigneeAgentId)) notification = await dispatchTaskSystemMessage(db, updated, "cancelled", existing.status);
12372
- return {
12373
- task: updated,
12374
- notification
12375
- };
12376
- }
12377
- async function linkChatToTask(db, taskId, chatId, actor) {
12378
- const task = await getTask(db, taskId);
12379
- if (actor.type === "agent" && task.organizationId !== actor.organizationId) throw new NotFoundError(`Task "${taskId}" not found`);
12380
- assertCanMutateTaskChats(task, actor);
12381
- const [chat] = await db.select({ organizationId: chats.organizationId }).from(chats).where(eq(chats.id, chatId)).limit(1);
12382
- if (!chat) throw new NotFoundError(`Chat "${chatId}" not found`);
12383
- if (chat.organizationId !== task.organizationId) throw new BadRequestError("Chat belongs to a different organization");
12384
- if (actor.type === "agent") await assertParticipant(db, chatId, actor.agentId);
12385
- const linkedBy = actor.type === "agent" ? actor.agentId : null;
12386
- await db.insert(taskChats).values({
12387
- taskId,
12388
- chatId,
12389
- linkedByAgentId: linkedBy
12390
- }).onConflictDoNothing();
12391
- }
12392
- async function unlinkChatFromTask(db, taskId, chatId, actor) {
12393
- const task = await getTask(db, taskId);
12394
- if (actor.type === "agent" && task.organizationId !== actor.organizationId) throw new NotFoundError(`Task "${taskId}" not found`);
12395
- assertCanMutateTaskChats(task, actor);
12396
- if ((await db.delete(taskChats).where(and(eq(taskChats.taskId, taskId), eq(taskChats.chatId, chatId))).returning({ chatId: taskChats.chatId })).length === 0) throw new NotFoundError(`Chat "${chatId}" is not linked to task "${taskId}"`);
12397
- }
12398
- /**
12399
- * Derive a health signal for a task. Only meaningful for `working` tasks.
12400
- * See hub-task-design Section 9 for the rules this implements.
12401
- *
12402
- * Algorithm (per linked chat for the assignee):
12403
- * 1. No session row OR state != 'active' → idle_island candidate
12404
- * 2. Session active, last message from assignee → awaiting_reply candidate
12405
- * 3. Session active, last message from other → normal candidate
12406
- * Across all linked chats, normal wins over awaiting_reply, which wins over idle_island.
12407
- */
12408
- async function getTaskHealth(db, taskId, expectedOrgId) {
12409
- const task = await getTask(db, taskId, expectedOrgId);
12410
- if (task.status !== TASK_STATUSES.WORKING) return {
12411
- taskId,
12412
- signal: TASK_HEALTH_SIGNALS.NOT_APPLICABLE,
12413
- reason: `Task status is "${task.status}" — health is only computed for working tasks`
12414
- };
12415
- if (!task.assigneeAgentId) return {
12416
- taskId,
12417
- signal: TASK_HEALTH_SIGNALS.NO_CHAT,
12418
- reason: "Task has no assignee"
12419
- };
12420
- const linked = await db.select({
12421
- chatId: taskChats.chatId,
12422
- sessionState: agentChatSessions.state
12423
- }).from(taskChats).leftJoin(agentChatSessions, and(eq(agentChatSessions.chatId, taskChats.chatId), eq(agentChatSessions.agentId, task.assigneeAgentId))).where(eq(taskChats.taskId, taskId));
12424
- if (linked.length === 0) return {
12425
- taskId,
12426
- signal: TASK_HEALTH_SIGNALS.NO_CHAT,
12427
- reason: "Task has no linked chats"
12428
- };
12429
- const chatSignals = [];
12430
- for (const row of linked) {
12431
- if (row.sessionState !== "active") {
12432
- chatSignals.push(TASK_HEALTH_SIGNALS.IDLE_ISLAND);
12433
- continue;
12434
- }
12435
- const [last] = await db.select({ senderId: messages.senderId }).from(messages).where(eq(messages.chatId, row.chatId)).orderBy(desc(messages.createdAt)).limit(1);
12436
- if (!last) {
12437
- chatSignals.push(TASK_HEALTH_SIGNALS.IDLE_ISLAND);
12438
- continue;
12439
- }
12440
- if (last.senderId === task.assigneeAgentId) chatSignals.push(TASK_HEALTH_SIGNALS.AWAITING_REPLY);
12441
- else chatSignals.push(TASK_HEALTH_SIGNALS.NORMAL);
12442
- }
12443
- if (chatSignals.includes(TASK_HEALTH_SIGNALS.NORMAL)) return {
12444
- taskId,
12445
- signal: TASK_HEALTH_SIGNALS.NORMAL,
12446
- reason: "At least one linked chat is actively progressing"
12447
- };
12448
- if (chatSignals.includes(TASK_HEALTH_SIGNALS.AWAITING_REPLY)) return {
12449
- taskId,
12450
- signal: TASK_HEALTH_SIGNALS.AWAITING_REPLY,
12451
- reason: "Assignee sent the last message and is waiting for a reply"
12452
- };
12453
- return {
12454
- taskId,
12455
- signal: TASK_HEALTH_SIGNALS.IDLE_ISLAND,
12456
- reason: "No active session found for the assignee in any linked chat"
12457
- };
12458
- }
12459
- /** Serialize a task row for API output. */
12460
- function serializeTask(task) {
12461
- return {
12462
- ...task,
12463
- createdAt: task.createdAt.toISOString(),
12464
- updatedAt: task.updatedAt.toISOString(),
12465
- cancelledAt: task.cancelledAt ? task.cancelledAt.toISOString() : null
12466
- };
12467
- }
12468
- function dispatch$2(notifier, result) {
12469
- if (!result) return;
12470
- notifyRecipients(notifier, result.recipients, result.message.id);
12471
- }
12472
- async function agentTaskRoutes(app) {
12473
- /** Create a task. Agent creator; assignee defaults to self (work-first) if omitted. */
12474
- app.post("/", async (request, reply) => {
12475
- const identity = requireAgent(request);
12476
- const body = createTaskSchema.parse(request.body);
12477
- const { task, notification } = await createTask(app.db, {
12478
- type: "agent",
12479
- agentId: identity.uuid,
12480
- organizationId: identity.organizationId
12481
- }, {
12482
- ...body,
12483
- organizationId: identity.organizationId
12484
- });
12485
- dispatch$2(app.notifier, notification);
12486
- return reply.status(201).send(serializeTask(task));
12487
- });
12488
- app.get("/", async (request) => {
12489
- const identity = requireAgent(request);
12490
- const query = taskListQuerySchema.parse(request.query);
12491
- const result = await listTasks(app.db, identity.organizationId, query);
12492
- return {
12493
- items: result.items.map((t) => serializeTask(t)),
12494
- nextCursor: result.nextCursor
12495
- };
12496
- });
12497
- app.get("/:taskId", async (request) => {
12498
- const identity = requireAgent(request);
12499
- const detail = await getTaskDetail(app.db, request.params.taskId, identity.organizationId);
12500
- return {
12501
- ...serializeTask(detail),
12502
- chats: detail.chats
12503
- };
12504
- });
12505
- /** Agent self-report: working / completed / failed. */
12506
- app.patch("/:taskId", async (request) => {
12507
- const identity = requireAgent(request);
12508
- const body = updateTaskStatusSchema.parse(request.body);
12509
- const { task } = await updateTaskStatus(app.db, request.params.taskId, {
12510
- type: "agent",
12511
- agentId: identity.uuid,
12512
- organizationId: identity.organizationId
12513
- }, body);
12514
- return serializeTask(task);
12515
- });
12516
- app.post("/:taskId/cancel", async (request) => {
12517
- const identity = requireAgent(request);
12518
- const { task, notification } = await cancelTask(app.db, request.params.taskId, {
12519
- type: "agent",
12520
- agentId: identity.uuid,
12521
- organizationId: identity.organizationId
12522
- });
12523
- dispatch$2(app.notifier, notification);
12524
- return serializeTask(task);
12525
- });
12526
- app.post("/:taskId/chats", async (request, reply) => {
12527
- const identity = requireAgent(request);
12528
- const body = linkTaskChatSchema.parse(request.body);
12529
- await linkChatToTask(app.db, request.params.taskId, body.chatId, {
12530
- type: "agent",
12531
- agentId: identity.uuid,
12532
- organizationId: identity.organizationId
12533
- });
12534
- return reply.status(204).send();
12535
- });
12536
- app.delete("/:taskId/chats/:chatId", async (request, reply) => {
12537
- const identity = requireAgent(request);
12538
- await unlinkChatFromTask(app.db, request.params.taskId, request.params.chatId, {
12539
- type: "agent",
12540
- agentId: identity.uuid,
12541
- organizationId: identity.organizationId
12542
- });
12543
- return reply.status(204).send();
12544
- });
12545
- /** Task health signal — only meaningful while task.status === "working". */
12546
- app.get("/:taskId/health", async (request) => {
12547
- const identity = requireAgent(request);
12548
- return getTaskHealth(app.db, request.params.taskId, identity.organizationId);
12549
- });
12550
- }
12551
12113
  /** WS close code: agent already connected from another client. */
12552
12114
  const WS_CLOSE_ALREADY_CONNECTED = 4009;
12553
12115
  /** Track active WS connections per agentId. At most one entry per agent. */
@@ -13779,7 +13341,7 @@ async function agentRoutes(app) {
13779
13341
  });
13780
13342
  app.post("/:uuid/test", async (request, reply) => {
13781
13343
  const { uuid } = request.params;
13782
- await requireAgentAccess(request, app.db, "manage");
13344
+ const { agent: targetAgent } = await requireAgentAccess(request, app.db, "manage");
13783
13345
  const presence = await getPresence(app.db, uuid);
13784
13346
  const wsConnected = hasActiveConnection(uuid);
13785
13347
  const clientId = getAgentClientId(uuid) ?? presence?.clientId ?? null;
@@ -13816,15 +13378,15 @@ async function agentRoutes(app) {
13816
13378
  message: "Agent connection is stale — heartbeat lost. The client process may have crashed.",
13817
13379
  connection
13818
13380
  });
13819
- const [owner] = await app.db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.delegateMention, uuid), eq(agents.status, "active"))).limit(1);
13381
+ 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);
13820
13382
  let senderId = owner?.uuid ?? null;
13821
13383
  if (!senderId) {
13822
- const [other] = await app.db.select({ uuid: agents.uuid }).from(agents).where(and(ne(agents.uuid, uuid), eq(agents.status, "active"))).limit(1);
13384
+ 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);
13823
13385
  senderId = other?.uuid ?? null;
13824
13386
  }
13825
13387
  if (!senderId) return reply.status(200).send({
13826
13388
  status: "error",
13827
- message: "No suitable sender found. Need at least one other active agent.",
13389
+ message: "No suitable sender found. Need at least one other active agent in the same organization.",
13828
13390
  connection
13829
13391
  });
13830
13392
  const chat = await findOrCreateDirectChat(app.db, senderId, uuid);
@@ -15017,7 +14579,7 @@ function decodeCursor(cursor) {
15017
14579
  * - Cursor narrows the result to rows STRICTLY before `(cursor.ts, cursor.id)`.
15018
14580
  * - Followed by a small participant-list lookup for the page only.
15019
14581
  */
15020
- async function listMeChats(db, humanAgentId, query) {
14582
+ async function listMeChats(db, humanAgentId, organizationId, query) {
15021
14583
  const limit = query.limit;
15022
14584
  const cursor = query.cursor ? decodeCursor(query.cursor) : null;
15023
14585
  if (query.cursor && !cursor) throw new BadRequestError("Invalid cursor");
@@ -15058,6 +14620,11 @@ async function listMeChats(db, humanAgentId, query) {
15058
14620
  FROM chats c
15059
14621
  JOIN deduped d ON d.chat_id = c.id
15060
14622
  WHERE c.parent_chat_id IS NULL
14623
+ /* Scope to the caller's org. Without this, cross-org dirty chats
14624
+ whose chat_participants still reference the caller's human agent
14625
+ (historical pollution — see fix/cross-org-direct-chat-pollution)
14626
+ would leak into the list and 404 on click via requireChatAccess. */
14627
+ AND c.organization_id = ${organizationId}
15061
14628
  /* Filter: unread / watching */
15062
14629
  AND (${!filterUnreadOnly}::bool OR d.unread_mention_count > 0)
15063
14630
  AND (${!filterWatchingOnly}::bool OR d.membership_kind = 'watching')
@@ -15118,9 +14685,7 @@ async function listMeChats(db, humanAgentId, query) {
15118
14685
  lastMessageAt: toDate(r.last_message_at)?.toISOString() ?? null,
15119
14686
  lastMessagePreview: r.last_message_preview,
15120
14687
  unreadMentionCount: r.unread_mention_count,
15121
- canReply: r.membership_kind === "participant",
15122
- taskId: null,
15123
- taskStatus: null
14688
+ canReply: r.membership_kind === "participant"
15124
14689
  };
15125
14690
  }),
15126
14691
  nextCursor
@@ -16077,17 +15642,17 @@ function isGithubHttpsRepo(repoUrl) {
16077
15642
  }
16078
15643
  function contextStatus(warning) {
16079
15644
  if (warning?.stale) return {
16080
- label: "Team context is stale",
15645
+ label: "Context Tree may be stale",
16081
15646
  detail: warning.detail,
16082
15647
  severity: "warning"
16083
15648
  };
16084
15649
  if (warning) return {
16085
- label: "Team context needs attention",
15650
+ label: "Context Tree needs attention",
16086
15651
  detail: warning.detail,
16087
15652
  severity: "warning"
16088
15653
  };
16089
15654
  return {
16090
- label: "Team context is current",
15655
+ label: "Context Tree is up to date",
16091
15656
  detail: "Agents have a synced team context snapshot available.",
16092
15657
  severity: "ok"
16093
15658
  };
@@ -16838,7 +16403,7 @@ async function healthzRoutes(app) {
16838
16403
  * `api/orgs/invitations.ts` (Class B, admin-gated).
16839
16404
  */
16840
16405
  async function publicInvitationRoutes(app) {
16841
- const { previewInvitation } = await import("./invitation-C299fxkP-B89eqDos.mjs");
16406
+ const { previewInvitation } = await import("./invitation-C299fxkP-CZRV665C.mjs");
16842
16407
  app.get("/:token/preview", async (request, reply) => {
16843
16408
  if (!request.params.token) throw new UnauthorizedError("Token required");
16844
16409
  const preview = await previewInvitation(app.db, request.params.token);
@@ -17018,7 +16583,7 @@ async function meRoutes(app) {
17018
16583
  */
17019
16584
  app.get("/me/pinned-agents", async (request) => {
17020
16585
  const { userId } = requireUser(request);
17021
- const { listMyPinnedAgents } = await import("./client-WubcgX-W-B2bOvgJ1.mjs");
16586
+ const { listMyPinnedAgents } = await import("./client-0RrgrMjR-CylTJGEb.mjs");
17022
16587
  return listMyPinnedAgents(app.db, { userId });
17023
16588
  });
17024
16589
  /**
@@ -17451,7 +17016,7 @@ async function orgChatRoutes(app) {
17451
17016
  }
17452
17017
  if (view === "grouped") return listChatsForMember(app.db, scope.memberId, scope.humanAgentId);
17453
17018
  const query = listMeChatsQuerySchema.parse(request.query);
17454
- return listMeChats(app.db, scope.humanAgentId, query);
17019
+ return listMeChats(app.db, scope.humanAgentId, scope.organizationId, query);
17455
17020
  });
17456
17021
  /**
17457
17022
  * POST /orgs/:orgId/chats — create a new chat. The :orgId path param
@@ -17831,39 +17396,6 @@ function enrichOutput(namespace, out, orgId, publicUrl) {
17831
17396
  }
17832
17397
  return out;
17833
17398
  }
17834
- function dispatch$1(notifier, result) {
17835
- if (!result) return;
17836
- notifyRecipients(notifier, result.recipients, result.message.id);
17837
- }
17838
- /** Class B — `/api/v1/orgs/:orgId/tasks`. Per-task ops live in api/tasks.ts. */
17839
- async function orgTaskRoutes(app) {
17840
- app.get("/", async (request) => {
17841
- const scope = await requireOrgMembership(request, app.db);
17842
- const query = taskListQuerySchema.parse(request.query);
17843
- const result = await listTasks(app.db, scope.organizationId, query);
17844
- return {
17845
- items: result.items.map((t) => serializeTask(t)),
17846
- nextCursor: result.nextCursor
17847
- };
17848
- });
17849
- app.post("/", async (request, reply) => {
17850
- const scope = await requireOrgMembership(request, app.db);
17851
- const body = adminCreateTaskSchema.parse(request.body);
17852
- const { task, notification } = await createTask(app.db, {
17853
- type: "admin",
17854
- adminId: scope.memberId
17855
- }, {
17856
- title: body.title,
17857
- body: body.body,
17858
- ...body.assigneeAgentId !== void 0 ? { assigneeAgentId: body.assigneeAgentId } : {},
17859
- ...body.originRef !== void 0 ? { originRef: body.originRef } : {},
17860
- ...body.metadata !== void 0 ? { metadata: body.metadata } : {},
17861
- organizationId: scope.organizationId
17862
- });
17863
- dispatch$1(app.notifier, notification);
17864
- return reply.status(201).send(serializeTask(task));
17865
- });
17866
- }
17867
17399
  async function loadVisibleAgentIds(db, organizationId, memberId) {
17868
17400
  const rows = await db.select({ id: agents.uuid }).from(agents).where(and(eq(agents.organizationId, organizationId), ne(agents.status, AGENT_STATUSES.DELETED), or(eq(agents.visibility, AGENT_VISIBILITY.ORGANIZATION), eq(agents.managerId, memberId))));
17869
17401
  return new Set(rows.map((r) => r.id));
@@ -18084,44 +17616,6 @@ async function sessionRoutes(app) {
18084
17616
  });
18085
17617
  });
18086
17618
  }
18087
- function dispatch(notifier, result) {
18088
- if (!result) return;
18089
- notifyRecipients(notifier, result.recipients, result.message.id);
18090
- }
18091
- /** Class C — `/api/v1/tasks/:taskId`. The task's `organizationId` locates the org. */
18092
- async function taskRoutes(app) {
18093
- app.get("/:taskId", async (request) => {
18094
- await requireTaskAccess(request, app.db);
18095
- const detail = await getTaskDetail(app.db, request.params.taskId);
18096
- return {
18097
- ...serializeTask(detail),
18098
- chats: detail.chats
18099
- };
18100
- });
18101
- app.patch("/:taskId", async (request) => {
18102
- const { scope } = await requireTaskAccess(request, app.db);
18103
- const body = adminUpdateTaskSchema.parse(request.body);
18104
- const { task, notification } = await adminUpdateTask(app.db, request.params.taskId, {
18105
- type: "admin",
18106
- adminId: scope.memberId
18107
- }, body);
18108
- dispatch(app.notifier, notification);
18109
- return serializeTask(task);
18110
- });
18111
- app.post("/:taskId/cancel", async (request) => {
18112
- const { scope } = await requireTaskAccess(request, app.db);
18113
- const { task, notification } = await cancelTask(app.db, request.params.taskId, {
18114
- type: "admin",
18115
- adminId: scope.memberId
18116
- });
18117
- dispatch(app.notifier, notification);
18118
- return serializeTask(task);
18119
- });
18120
- app.get("/:taskId/health", async (request) => {
18121
- await requireTaskAccess(request, app.db);
18122
- return getTaskHealth(app.db, request.params.taskId);
18123
- });
18124
- }
18125
17619
  const log$1 = createLogger$1("GithubWebhook");
18126
17620
  const GITHUB_ADAPTER_ID = "github-adapter";
18127
17621
  function verifySignature(secret, rawBody, signatureHeader) {
@@ -18187,6 +17681,31 @@ function extractMentions$1(text) {
18187
17681
  }
18188
17682
  return [...names];
18189
17683
  }
17684
+ /** Extract mentions from structural payload fields (not free-form text).
17685
+ * GitHub's `pull_request.review_requested` puts the targeted reviewer in
17686
+ * `requested_reviewer.login`, not in any text body — `extractMentions` would
17687
+ * miss it. Team requests use `requested_team` instead, which we deliberately
17688
+ * skip to stay consistent with `extractMentions` ignoring `@org/team`. */
17689
+ function extractStructuralMentions(eventType, payload) {
17690
+ if (!isRecord(payload)) return [];
17691
+ if (eventType !== "pull_request") return [];
17692
+ if (payload.action !== "review_requested") return [];
17693
+ const reviewer = isRecord(payload.requested_reviewer) ? payload.requested_reviewer : null;
17694
+ const login = typeof reviewer?.login === "string" ? reviewer.login : null;
17695
+ return login ? [login.toLowerCase()] : [];
17696
+ }
17697
+ const DELEGATE_VERDICT_MESSAGES = {
17698
+ ok: "delegate_mention target eligible",
17699
+ not_found: "delegate_mention target not found, skipping",
17700
+ cross_org: "delegate_mention target belongs to another org, skipping",
17701
+ inactive: "delegate_mention target not active, skipping"
17702
+ };
17703
+ function evaluateDelegateTarget(target, sourceOrgId) {
17704
+ if (!target) return "not_found";
17705
+ if (target.organizationId !== sourceOrgId) return "cross_org";
17706
+ if (target.status !== "active") return "inactive";
17707
+ return "ok";
17708
+ }
18190
17709
  /**
18191
17710
  * Route @mentions to delegate agents.
18192
17711
  * For each mentioned user who has delegate_mention configured,
@@ -18205,13 +17724,19 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
18205
17724
  if (agent.status !== "active" || !agent.delegateMention) continue;
18206
17725
  const [target] = await app.db.select({
18207
17726
  id: agents.uuid,
18208
- status: agents.status
17727
+ status: agents.status,
17728
+ organizationId: agents.organizationId
18209
17729
  }).from(agents).where(eq(agents.uuid, agent.delegateMention)).limit(1);
18210
- if (!target || target.status !== "active") {
17730
+ const verdict = evaluateDelegateTarget(target, organizationId);
17731
+ if (verdict !== "ok") {
18211
17732
  log$1.warn({
18212
17733
  targetAgent: agent.delegateMention,
18213
- sourceAgent: agent.name
18214
- }, "delegate_mention target not active, skipping");
17734
+ sourceAgent: agent.name,
17735
+ sourceOrg: organizationId,
17736
+ targetOrg: target?.organizationId,
17737
+ targetStatus: target?.status,
17738
+ verdict
17739
+ }, DELEGATE_VERDICT_MESSAGES[verdict]);
18215
17740
  continue;
18216
17741
  }
18217
17742
  try {
@@ -18222,6 +17747,7 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
18222
17747
  type: "github_mention",
18223
17748
  mentionedUser: agent.name,
18224
17749
  event: ctx.event,
17750
+ action: ctx.action,
18225
17751
  repository: ctx.repository,
18226
17752
  sender: ctx.sender,
18227
17753
  title: ctx.title,
@@ -18231,7 +17757,8 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
18231
17757
  metadata: {
18232
17758
  source: "github",
18233
17759
  event: "mention_delegation",
18234
- mentionedUser: agent.name
17760
+ mentionedUser: agent.name,
17761
+ action: ctx.action
18235
17762
  }
18236
17763
  });
18237
17764
  notifyRecipients(app.notifier, recipients, msg.id);
@@ -18388,12 +17915,14 @@ function extractEventContext(eventType, payload) {
18388
17915
  const sender = isRecord(payload.sender) ? payload.sender : null;
18389
17916
  const repository = typeof repo?.full_name === "string" ? repo.full_name : "";
18390
17917
  const senderLogin = typeof sender?.login === "string" ? sender.login : "";
17918
+ const action = typeof payload.action === "string" ? payload.action : void 0;
18391
17919
  switch (eventType) {
18392
17920
  case "issues": {
18393
17921
  const issue = isRecord(payload.issue) ? payload.issue : null;
18394
17922
  if (!issue) return null;
18395
17923
  return {
18396
17924
  event: "issues",
17925
+ action,
18397
17926
  repository,
18398
17927
  sender: senderLogin,
18399
17928
  title: `Issue #${issue.number}: ${issue.title}`,
@@ -18407,6 +17936,7 @@ function extractEventContext(eventType, payload) {
18407
17936
  if (!issue || !comment) return null;
18408
17937
  return {
18409
17938
  event: "issue_comment",
17939
+ action,
18410
17940
  repository,
18411
17941
  sender: senderLogin,
18412
17942
  title: `Issue #${issue.number}: ${issue.title}`,
@@ -18419,6 +17949,7 @@ function extractEventContext(eventType, payload) {
18419
17949
  if (!pr) return null;
18420
17950
  return {
18421
17951
  event: "pull_request",
17952
+ action,
18422
17953
  repository,
18423
17954
  sender: senderLogin,
18424
17955
  title: `PR #${pr.number}: ${pr.title}`,
@@ -18432,6 +17963,7 @@ function extractEventContext(eventType, payload) {
18432
17963
  if (!pr || !review) return null;
18433
17964
  return {
18434
17965
  event: "pull_request_review",
17966
+ action,
18435
17967
  repository,
18436
17968
  sender: senderLogin,
18437
17969
  title: `PR #${pr.number}: ${pr.title}`,
@@ -18445,6 +17977,7 @@ function extractEventContext(eventType, payload) {
18445
17977
  if (!pr || !comment) return null;
18446
17978
  return {
18447
17979
  event: "pull_request_review_comment",
17980
+ action,
18448
17981
  repository,
18449
17982
  sender: senderLogin,
18450
17983
  title: `PR #${pr.number}: ${pr.title}`,
@@ -18457,6 +17990,7 @@ function extractEventContext(eventType, payload) {
18457
17990
  if (!disc) return null;
18458
17991
  return {
18459
17992
  event: "discussion",
17993
+ action,
18460
17994
  repository,
18461
17995
  sender: senderLogin,
18462
17996
  title: typeof disc.title === "string" ? disc.title : "",
@@ -18470,6 +18004,7 @@ function extractEventContext(eventType, payload) {
18470
18004
  if (!disc || !comment) return null;
18471
18005
  return {
18472
18006
  event: "discussion_comment",
18007
+ action,
18473
18008
  repository,
18474
18009
  sender: senderLogin,
18475
18010
  title: typeof disc.title === "string" ? disc.title : "",
@@ -18482,6 +18017,7 @@ function extractEventContext(eventType, payload) {
18482
18017
  if (!comment) return null;
18483
18018
  return {
18484
18019
  event: "commit_comment",
18020
+ action,
18485
18021
  repository,
18486
18022
  sender: senderLogin,
18487
18023
  title: "Commit comment",
@@ -18497,16 +18033,26 @@ function extractEventContext(eventType, payload) {
18497
18033
  * Only called after action gating confirms this is a "new content" event.
18498
18034
  */
18499
18035
  async function handleMentionDelegation(app, organizationId, eventType, payload) {
18500
- const mentions = extractMentions$1(extractEventText(eventType, payload));
18036
+ const textMentions = extractMentions$1(extractEventText(eventType, payload));
18037
+ const structuralMentions = extractStructuralMentions(eventType, payload);
18038
+ const mentions = [...new Set([...textMentions, ...structuralMentions])];
18501
18039
  const mentionCtx = extractEventContext(eventType, payload);
18502
18040
  if (mentions.length > 0 && mentionCtx) return routeMentionDelegations(app, organizationId, mentions, mentionCtx);
18503
18041
  return 0;
18504
18042
  }
18505
- /** Actions that represent new/changed content (worth scanning for @mentions). */
18043
+ /** Actions that represent new/changed content (worth scanning for @mentions).
18044
+ * Note: `pull_request.review_requested` doesn't carry an @mention in any
18045
+ * text body — the reviewer is in `requested_reviewer.login`. We pick it up
18046
+ * via `extractStructuralMentions`. The complementary `review_request_removed`
18047
+ * is intentionally omitted to avoid notifying the reviewer twice. */
18506
18048
  const MENTION_ACTIONS = {
18507
18049
  issues: ["opened", "edited"],
18508
18050
  issue_comment: ["created"],
18509
- pull_request: ["opened", "edited"],
18051
+ pull_request: [
18052
+ "opened",
18053
+ "edited",
18054
+ "review_requested"
18055
+ ],
18510
18056
  pull_request_review: ["submitted"],
18511
18057
  pull_request_review_comment: ["created"],
18512
18058
  discussion: ["created", "edited"],
@@ -18656,8 +18202,6 @@ var schema_exports = /* @__PURE__ */ __exportAll({
18656
18202
  pendingQuestions: () => pendingQuestions,
18657
18203
  serverInstances: () => serverInstances,
18658
18204
  sessionEvents: () => sessionEvents,
18659
- taskChats: () => taskChats,
18660
- tasks: () => tasks,
18661
18205
  users: () => users
18662
18206
  });
18663
18207
  function connectDatabase(url) {
@@ -20150,7 +19694,6 @@ async function buildApp(config) {
20150
19694
  await scope.register(orgAdapterStatusRoutes, { prefix: "/adapters/status" });
20151
19695
  await scope.register(orgOverviewRoutes, { prefix: "/overview" });
20152
19696
  await scope.register(orgActivityRoutes, { prefix: "/activity" });
20153
- await scope.register(orgTaskRoutes, { prefix: "/tasks" });
20154
19697
  await scope.register(orgSessionRoutes, { prefix: "/sessions" });
20155
19698
  await scope.register(orgNotificationRoutes, { prefix: "/notifications" });
20156
19699
  await scope.register(orgClientRoutes, { prefix: "/clients" });
@@ -20166,7 +19709,6 @@ async function buildApp(config) {
20166
19709
  await scope.register(agentActivityRoutes, { prefix: "/agents" });
20167
19710
  await scope.register(sessionRoutes, { prefix: "/agents" });
20168
19711
  await scope.register(chatRoutes, { prefix: "/chats" });
20169
- await scope.register(taskRoutes, { prefix: "/tasks" });
20170
19712
  await scope.register(adapterRoutes, { prefix: "/adapters" });
20171
19713
  await scope.register(adapterMappingRoutes, { prefix: "/adapter-mappings" });
20172
19714
  await scope.register(clientRoutes, { prefix: "/clients" });
@@ -20178,7 +19720,6 @@ async function buildApp(config) {
20178
19720
  await scope.register(agentSendToAgentRoutes, { prefix: "/agents" });
20179
19721
  await scope.register(agentInboxRoutes, { prefix: "/inbox" });
20180
19722
  await scope.register(agentConfigRoutes$1);
20181
- await scope.register(agentTaskRoutes, { prefix: "/tasks" });
20182
19723
  await scope.register(agentFeishuBotRoutes);
20183
19724
  await scope.register(agentFeishuUserRoutes, { prefix: "/delegated" });
20184
19725
  }), { prefix: "/agent" });