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

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 refreshTokenSchema, A as dryRunAgentRuntimeConfigSchema, B as isRedactedEnvValue, C as createAgentSchema, D as createOrgFromMeSchema, E as createMemberSchema, F as imageInlineContentSchema, G as messageSourceSchema$1, H as joinByInvitationSchema, I as inboxAckFrameSchema, J as paginationQuerySchema, K as notificationQuerySchema, L as inboxDeliverFrameSchema$1, M as githubCallbackQuerySchema, N as githubDevCallbackQuerySchema, O as defaultRuntimeConfigPayload, P as githubStartQuerySchema, Q as rebindAgentSchema, R as inboxPollQuerySchema, S as createAdapterMappingSchema, T as createMeChatSchema, U as listMeChatsQuerySchema, V as isReservedAgentName$1, W as loginSchema, Y as patchOnboardingSchema, _t as updateClientCapabilitiesSchema, a as AGENT_VISIBILITY, at as sendToAgentSchema, b as contextTreeSnapshotSchema, bt as wsAuthFrameSchema, c as ORG_SETTINGS_NAMESPACES$1, ct as sessionEventSchema$1, d as addParticipantSchema, dt as stripCode, et as runtimeStateMessageSchema, f as agentBindRequestSchema, ft as submitQuestionAnswerSchema, g as chatMetadataSchema$1, gt as updateChatSchema, h as agentTypeSchema$1, ht as updateAgentSchema, i as AGENT_STATUSES, it as sendMessageSchema, k as delegateFeishuUserSchema, l as WS_AUTH_FRAME_TIMEOUT_MS, lt as sessionReconcileRequestSchema, m as agentRuntimeConfigPayloadSchema$1, mt as updateAgentRuntimeConfigSchema, n as AGENT_NAME_REGEX$1, ot as sessionCompletionMessageSchema, p as agentPinnedMessageSchema$1, pt as updateAdapterConfigSchema, q as onboardingEventSchema, r as AGENT_SELECTOR_HEADER$1, rt as selfServiceFeishuBotSchema, s as MENTION_REGEX, st as sessionEventMessageSchema, t as AGENT_BIND_REJECT_REASONS, tt as safeRedirectPath, u as addMeChatParticipantsSchema, ut as sessionStateMessageSchema, v as clientRegisterSchema, vt as updateMemberSchema, w as createChatSchema, x as createAdapterConfigSchema, y as connectTokenExchangeSchema, yt as updateOrganizationSchema, z as isOrgSettingNamespace } from "./dist-BwPlBZWi.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-D1TDiik_-gxtXN9bj.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-DL5vHhvQ-CnYGq2x-.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";
@@ -730,6 +730,30 @@ z.object({
730
730
  expiresIn: z.number(),
731
731
  command: z.string()
732
732
  });
733
+ const githubEntityTypeSchema = z.enum([
734
+ "issue",
735
+ "pull_request",
736
+ "discussion",
737
+ "commit"
738
+ ]);
739
+ const githubChatMetadataSchema = z.object({
740
+ source: z.literal("github"),
741
+ entityType: githubEntityTypeSchema,
742
+ entityKey: z.string().min(1),
743
+ entityUrl: z.string().url().optional()
744
+ });
745
+ const feishuChatMetadataSchema = z.object({
746
+ source: z.literal("feishu"),
747
+ externalChannelId: z.string().min(1)
748
+ });
749
+ const chatMetadataSchema = z.discriminatedUnion("source", [githubChatMetadataSchema, feishuChatMetadataSchema]);
750
+ /**
751
+ * `createChat` callers may not set metadata at all (admin-created group chats,
752
+ * me-chats, …), so the input schema accepts either an empty object or one of
753
+ * the typed variants. The empty `{}` arm is `.strict()` so a caller cannot
754
+ * sneak through `{ source: "github" }` without the required fields.
755
+ */
756
+ const optionalChatMetadataSchema = z.union([z.object({}).strict(), chatMetadataSchema]);
733
757
  const chatTypeSchema = z.enum([
734
758
  "direct",
735
759
  "group",
@@ -739,7 +763,7 @@ z.object({
739
763
  type: chatTypeSchema,
740
764
  topic: z.string().max(500).optional(),
741
765
  participantIds: z.array(z.string()).min(1),
742
- metadata: z.record(z.string(), z.unknown()).optional()
766
+ metadata: optionalChatMetadataSchema.optional()
743
767
  });
744
768
  const chatParticipantSchema = z.object({
745
769
  agentId: z.string(),
@@ -1015,7 +1039,6 @@ const messageFormatSchema = z.enum([
1015
1039
  "card",
1016
1040
  "reference",
1017
1041
  "file",
1018
- "task",
1019
1042
  "question",
1020
1043
  "question_answer"
1021
1044
  ]);
@@ -1199,9 +1222,7 @@ const meChatRowSchema = z.object({
1199
1222
  lastMessageAt: z.string().nullable(),
1200
1223
  lastMessagePreview: z.string().nullable(),
1201
1224
  unreadMentionCount: z.number().int(),
1202
- canReply: z.boolean(),
1203
- taskId: z.string().nullable(),
1204
- taskStatus: z.string().nullable()
1225
+ canReply: z.boolean()
1205
1226
  });
1206
1227
  z.object({
1207
1228
  rows: z.array(meChatRowSchema),
@@ -1677,94 +1698,6 @@ z.object({
1677
1698
  totalMessages: z.number(),
1678
1699
  byOrganization: z.array(orgStatsSchema)
1679
1700
  });
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
1701
  const userStatusSchema = z.enum(["active", "suspended"]);
1769
1702
  z.object({
1770
1703
  id: z.string(),
@@ -2876,7 +2809,126 @@ function getHandlerFactory(type) {
2876
2809
  }
2877
2810
  return factory;
2878
2811
  }
2879
- join(DEFAULT_DATA_DIR, "context-tree");
2812
+ const CONTEXT_TREE_DIR = join(DEFAULT_DATA_DIR, "context-tree");
2813
+ /**
2814
+ * Sync the shared Context Tree git clone.
2815
+ *
2816
+ * Clones on first run, pulls on subsequent runs.
2817
+ * Returns the binding on success, null on failure (graceful degradation).
2818
+ */
2819
+ async function syncContextTree(serverUrl, getAccessToken, log, userAgent) {
2820
+ try {
2821
+ execFileSync("git", ["--version"], { stdio: "ignore" });
2822
+ } catch {
2823
+ log("Context Tree sync skipped: git is not installed");
2824
+ return null;
2825
+ }
2826
+ let repo;
2827
+ let branch;
2828
+ try {
2829
+ const config = await new FirstTreeHubSDK({
2830
+ serverUrl,
2831
+ getAccessToken,
2832
+ userAgent
2833
+ }).getContextTreeConfig();
2834
+ if (!config.repo) {
2835
+ log("Context Tree sync skipped: not configured on server");
2836
+ return null;
2837
+ }
2838
+ repo = config.repo;
2839
+ branch = config.branch ?? "main";
2840
+ } catch (err) {
2841
+ log(`Context Tree sync skipped: failed to fetch config from server (${err instanceof Error ? err.message : String(err)})`);
2842
+ return null;
2843
+ }
2844
+ try {
2845
+ if (existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
2846
+ if (execFileSync("git", [
2847
+ "rev-parse",
2848
+ "--abbrev-ref",
2849
+ "HEAD"
2850
+ ], {
2851
+ cwd: CONTEXT_TREE_DIR,
2852
+ encoding: "utf-8",
2853
+ timeout: 5e3
2854
+ }).trim() !== branch) {
2855
+ execFileSync("git", ["checkout", branch], {
2856
+ cwd: CONTEXT_TREE_DIR,
2857
+ stdio: "pipe",
2858
+ timeout: 1e4
2859
+ });
2860
+ log(`Context Tree switched to branch ${branch}`);
2861
+ }
2862
+ execFileSync("git", ["pull", "--ff-only"], {
2863
+ cwd: CONTEXT_TREE_DIR,
2864
+ stdio: "pipe",
2865
+ timeout: 3e4
2866
+ });
2867
+ log(`Context Tree updated (pull)`);
2868
+ } else {
2869
+ mkdirSync(CONTEXT_TREE_DIR, { recursive: true });
2870
+ execFileSync("git", [
2871
+ "clone",
2872
+ "--branch",
2873
+ branch,
2874
+ "--single-branch",
2875
+ repo,
2876
+ CONTEXT_TREE_DIR
2877
+ ], {
2878
+ stdio: "pipe",
2879
+ timeout: 6e4
2880
+ });
2881
+ log(`Context Tree cloned from ${repo} (branch: ${branch})`);
2882
+ }
2883
+ return {
2884
+ path: CONTEXT_TREE_DIR,
2885
+ repoUrl: repo,
2886
+ branch
2887
+ };
2888
+ } catch (err) {
2889
+ const msg = err instanceof Error ? err.message : String(err);
2890
+ log(`Context Tree sync failed: ${msg}`);
2891
+ log("Check that git credentials (SSH key or credential helper) are configured for this repo");
2892
+ if ((msg.includes("cannot fast-forward") || msg.includes("not possible to fast-forward") || msg.includes("CONFLICT")) && existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
2893
+ log("Diverged history detected, attempting fresh clone...");
2894
+ try {
2895
+ rmSync(CONTEXT_TREE_DIR, {
2896
+ recursive: true,
2897
+ force: true
2898
+ });
2899
+ mkdirSync(CONTEXT_TREE_DIR, { recursive: true });
2900
+ execFileSync("git", [
2901
+ "clone",
2902
+ "--branch",
2903
+ branch,
2904
+ "--single-branch",
2905
+ repo,
2906
+ CONTEXT_TREE_DIR
2907
+ ], {
2908
+ stdio: "pipe",
2909
+ timeout: 6e4
2910
+ });
2911
+ log("Context Tree re-cloned successfully");
2912
+ return {
2913
+ path: CONTEXT_TREE_DIR,
2914
+ repoUrl: repo,
2915
+ branch
2916
+ };
2917
+ } catch {
2918
+ log("Context Tree re-clone also failed, continuing without context");
2919
+ }
2920
+ }
2921
+ if (existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
2922
+ log("Using existing Context Tree clone despite sync failure");
2923
+ return {
2924
+ path: CONTEXT_TREE_DIR,
2925
+ repoUrl: repo,
2926
+ branch
2927
+ };
2928
+ }
2929
+ return null;
2930
+ }
2931
+ }
2880
2932
  /**
2881
2933
  * Marker file written into every workspace so the Codex CLI's project-root
2882
2934
  * detection (configured via `project_root_markers: ["first-tree-workspace"]`)
@@ -3003,7 +3055,8 @@ function generateToolsDoc() {
3003
3055
  You are running inside **Agent Hub**, a messaging platform for agent teams.
3004
3056
 
3005
3057
  - Messages from other team members arrive as your prompt input
3006
- - Each message includes a \`[From: sender-id]\` header so you know who sent it
3058
+ - Each message includes a \`[From: <agent-name>]\` header that name is also
3059
+ what you pass back to \`agent send\` to reply to or address that agent
3007
3060
  - **Your final text response is automatically delivered** to the chat — just respond normally
3008
3061
  - For **proactive communication** (sending to other agents, other chats, or structured data),
3009
3062
  use the \`first-tree-hub\` CLI below
@@ -3018,8 +3071,8 @@ These are injected automatically when the agent process starts:
3018
3071
  |----------|-------------|
3019
3072
  | \`FIRST_TREE_HUB_SERVER_URL\` | Server address for API calls |
3020
3073
  | \`FIRST_TREE_HUB_ACCESS_TOKEN\` | User member access JWT (short-lived) |
3021
- | \`FIRST_TREE_HUB_AGENT_ID\` | Your agent UUID — send as \`X-Agent-Id\` |
3022
- | \`FIRST_TREE_HUB_CHAT_ID\` | Current chat context ID |
3074
+ | \`FIRST_TREE_HUB_AGENT_ID\` | YOUR own agent UUID. The CLI reads it to identify you as the sender never pass it as a \`send\` target. |
3075
+ | \`FIRST_TREE_HUB_CHAT_ID\` | The chat this session is currently bound to. The CLI uses it to route messages — you don't need to pass it manually. |
3023
3076
 
3024
3077
  The \`first-tree-hub\` CLI reads these automatically — no extra setup needed.
3025
3078
 
@@ -3029,13 +3082,18 @@ Use the \`first-tree-hub agent send\` CLI — it reads the env vars above and
3029
3082
  attaches the \`Authorization\` + \`X-Agent-Id\` headers automatically:
3030
3083
 
3031
3084
  \`\`\`bash
3032
- # Send to another agent — target is the agent NAME, NOT a uuid.
3033
- # Names are stable (set on creation, immutable, unique in the org).
3085
+ # Send to another agent — first positional argument is the recipient's NAME
3086
+ # (NOT a uuid; uuids in chat history / participant lists are not accepted).
3034
3087
  # Run \`first-tree-hub agent list\` to see available names.
3088
+ #
3089
+ # Routing: if the recipient is a participant of your current chat (typically
3090
+ # the case in a group chat where someone @-mentioned you to talk to them),
3091
+ # the message stays in that chat. Otherwise it falls back to a direct chat
3092
+ # between you and the recipient. You don't need to think about which.
3035
3093
  first-tree-hub agent send <agentName> "your message"
3036
3094
 
3037
- # Send to a chat (target is a chat UUID; use this when replying into a
3038
- # specific chat, e.g. a group where you were mentioned)
3095
+ # Send into a specific chat by id use this only when you explicitly want
3096
+ # to address a chat your current session is NOT bound to.
3039
3097
  first-tree-hub agent send --chat <chatId> "your message"
3040
3098
 
3041
3099
  # Send markdown (default format is text)
@@ -6894,6 +6952,36 @@ function resolveReplyToFromEnv(env, override) {
6894
6952
  replyToChat: override.replyToChat ?? (envComplete ? envChatId : void 0)
6895
6953
  };
6896
6954
  }
6955
+ function resolveSenderName(input) {
6956
+ const { override, envAgentId, agents } = input;
6957
+ if (agents.size === 0) return { kind: "none" };
6958
+ if (override) return {
6959
+ kind: "ok",
6960
+ name: override
6961
+ };
6962
+ if (envAgentId) {
6963
+ for (const [name, cfg] of agents) if (cfg.agentId === envAgentId) return {
6964
+ kind: "ok",
6965
+ name
6966
+ };
6967
+ return {
6968
+ kind: "envMismatch",
6969
+ envAgentId,
6970
+ available: [...agents.keys()]
6971
+ };
6972
+ }
6973
+ if (agents.size === 1) {
6974
+ const [only] = [...agents.keys()];
6975
+ if (only) return {
6976
+ kind: "ok",
6977
+ name: only
6978
+ };
6979
+ }
6980
+ return {
6981
+ kind: "ambiguous",
6982
+ available: [...agents.keys()]
6983
+ };
6984
+ }
6897
6985
  //#endregion
6898
6986
  //#region src/core/admin.ts
6899
6987
  /**
@@ -7213,6 +7301,14 @@ var ClientRuntime = class {
7213
7301
  watcher = null;
7214
7302
  debounceTimer = null;
7215
7303
  /**
7304
+ * Per-org Context Tree binding resolved at `start()`. Threaded through every
7305
+ * `slot.start()` so handlers can copy `AGENT.md` / root `NODE.md` into the
7306
+ * agent workspace's `.agent/context/` and install the first-tree skill.
7307
+ * `null` when the user has no primary org, the org has no tree configured,
7308
+ * or git sync failed — handlers degrade gracefully (empty context dir).
7309
+ */
7310
+ contextTreeBinding = null;
7311
+ /**
7216
7312
  * Directory we write auto-registered agent configs into (same path that
7217
7313
  * `first-tree-hub agent add` uses). Set by `watchAgentsDir` so the
7218
7314
  * `agent:pinned` handler knows where to materialise new configs.
@@ -7272,6 +7368,7 @@ var ClientRuntime = class {
7272
7368
  this.agentIds.add(config.agentId);
7273
7369
  }
7274
7370
  async start() {
7371
+ this.contextTreeBinding = await syncContextTree(this.serverUrl, (opts) => ensureFreshAccessToken(opts), (msg) => print.status("[context-tree]", msg), CLI_USER_AGENT);
7275
7372
  if (this.options.currentVersion && this.options.update) this.updateManager = UpdateManager.attach(this.connection, {
7276
7373
  currentVersion: this.options.currentVersion,
7277
7374
  ...this.options.update,
@@ -7290,7 +7387,7 @@ var ClientRuntime = class {
7290
7387
  }
7291
7388
  await Promise.allSettled(this.agents.map(async (agent) => {
7292
7389
  try {
7293
- const identity = await agent.slot.start();
7390
+ const identity = await agent.slot.start(this.contextTreeBinding);
7294
7391
  print.check(true, `${agent.name}: connected`, `agent: ${identity.displayName ?? identity.agentId}`);
7295
7392
  } catch (error) {
7296
7393
  const msg = error instanceof Error ? error.message : String(error);
@@ -7417,7 +7514,7 @@ var ClientRuntime = class {
7417
7514
  startAgent(name) {
7418
7515
  const entry = this.agents.find((a) => a.name === name);
7419
7516
  if (!entry) return;
7420
- entry.slot.start().then((identity) => {
7517
+ entry.slot.start(this.contextTreeBinding).then((identity) => {
7421
7518
  print.check(true, `${name}: connected`, `agent: ${identity.displayName ?? identity.agentId}`);
7422
7519
  }).catch((err) => {
7423
7520
  const msg = err instanceof Error ? err.message : String(err);
@@ -9054,7 +9151,7 @@ async function onboardCreate(args) {
9054
9151
  }
9055
9152
  const runtimeAgent = args.type === "human" ? args.assistant : args.id;
9056
9153
  if (args.feishuBotAppId && args.feishuBotAppSecret) {
9057
- const { bindFeishuBot } = await import("./feishu-fLnwqCOs.mjs").then((n) => n.r);
9154
+ const { bindFeishuBot } = await import("./feishu-CKGzIamp.mjs").then((n) => n.r);
9058
9155
  const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
9059
9156
  if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
9060
9157
  else {
@@ -10267,7 +10364,7 @@ function createFeedbackHandler(config) {
10267
10364
  return { handle };
10268
10365
  }
10269
10366
  //#endregion
10270
- //#region ../server/dist/app-BXdU2BzM.mjs
10367
+ //#region ../server/dist/app-kJNM9Cf1.mjs
10271
10368
  var import_fastify_opentelemetry = /* @__PURE__ */ __toESM(require_fastify_opentelemetry(), 1);
10272
10369
  init_esm();
10273
10370
  var __defProp = Object.defineProperty;
@@ -10292,50 +10389,6 @@ const adapterAgentMappings = pgTable("adapter_agent_mappings", {
10292
10389
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
10293
10390
  }, (table) => [unique("uq_adapter_agent_mapping").on(table.platform, table.externalUserId)]);
10294
10391
  /**
10295
- * Tasks — lightweight work units. Process descriptors, not tickets.
10296
- * Immutable status state machine: pending → assigned → working → (completed | failed | cancelled).
10297
- * Sub-tasks (parent_task_id) are deferred to a later phase.
10298
- *
10299
- * Referential integrity (org / assignee / chat) is enforced at the service layer,
10300
- * not via DB foreign keys — see `services/task.ts`.
10301
- */
10302
- const tasks = pgTable("tasks", {
10303
- id: text("id").primaryKey(),
10304
- organizationId: text("organization_id").notNull(),
10305
- title: text("title").notNull(),
10306
- body: text("body").notNull().default(""),
10307
- status: text("status").$type().notNull(),
10308
- assigneeAgentId: text("assignee_agent_id"),
10309
- createdByType: text("created_by_type").$type().notNull(),
10310
- createdById: text("created_by_id").notNull(),
10311
- originRef: text("origin_ref"),
10312
- result: text("result"),
10313
- metadata: jsonb("metadata").$type().notNull().default({}),
10314
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
10315
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
10316
- cancelledAt: timestamp("cancelled_at", { withTimezone: true }),
10317
- cancelledByType: text("cancelled_by_type").$type(),
10318
- cancelledById: text("cancelled_by_id")
10319
- }, (table) => [
10320
- index("idx_tasks_org_status").on(table.organizationId, table.status),
10321
- index("idx_tasks_assignee_status").on(table.assigneeAgentId, table.status),
10322
- index("idx_tasks_origin_ref").on(table.originRef),
10323
- index("idx_tasks_org_created_at").on(table.organizationId, table.createdAt)
10324
- ]);
10325
- /**
10326
- * Task ↔ Chat association (M:N). A task may be executed across multiple chats;
10327
- * a chat may host work for multiple tasks over its lifetime.
10328
- *
10329
- * No FK constraints — when a task or chat is deleted, the service layer is
10330
- * responsible for deleting linked rows here first.
10331
- */
10332
- const taskChats = pgTable("task_chats", {
10333
- taskId: text("task_id").notNull(),
10334
- chatId: text("chat_id").notNull(),
10335
- linkedByAgentId: text("linked_by_agent_id"),
10336
- linkedAt: timestamp("linked_at", { withTimezone: true }).notNull().defaultNow()
10337
- }, (table) => [primaryKey({ columns: [table.taskId, table.chatId] }), index("idx_task_chats_chat").on(table.chatId)]);
10338
- /**
10339
10392
  * Pull the JWT-verified `UserScope` off the request. The `userAuthHook`
10340
10393
  * middleware populates `request.user` synchronously before any handler
10341
10394
  * runs; this helper just narrows the optional and throws a clean 401 if
@@ -10470,31 +10523,6 @@ async function assertAgentManageableByUser(db, userId, agentUuid) {
10470
10523
  return scope;
10471
10524
  }
10472
10525
  /**
10473
- * Gate access to a task. Allowed for any active member of the task's org —
10474
- * mirrors the original inline gate in `api/tasks.ts` that this helper
10475
- * replaces. Returns both the task's org row and the caller's resolved
10476
- * `OrgScope`, so handlers can read `scope.memberId` for audit fields.
10477
- */
10478
- async function requireTaskAccess(request, db) {
10479
- const { userId } = requireUser(request);
10480
- const { taskId } = request.params;
10481
- const [task] = await db.select({ organizationId: tasks.organizationId }).from(tasks).where(eq(tasks.id, taskId)).limit(1);
10482
- if (!task) throw new NotFoundError(`Task "${taskId}" not found`);
10483
- const caller = await resolveCallerInOrg(db, userId, task.organizationId);
10484
- const scope = {
10485
- userId,
10486
- organizationId: task.organizationId,
10487
- memberId: caller.memberId,
10488
- role: caller.role,
10489
- humanAgentId: caller.humanAgentId
10490
- };
10491
- stampOrgScope(request, scope);
10492
- return {
10493
- task,
10494
- scope
10495
- };
10496
- }
10497
- /**
10498
10526
  * Assert every agent in `agentIds` is visible to `scope` and lives in
10499
10527
  * `scope.organizationId`. Used by chat-create to keep visibility rules out of
10500
10528
  * the service layer's signature.
@@ -10930,10 +10958,9 @@ async function ensureDefaultOrganization(db) {
10930
10958
  return org ?? existing;
10931
10959
  }
10932
10960
  /**
10933
- * Names beginning with `__` are reserved for Hub-internal pseudo agents
10934
- * (e.g. the task notifier). User-facing creation must not be able to
10935
- * squat on them, otherwise internal traffic could be routed through a
10936
- * real account.
10961
+ * Names beginning with `__` are reserved for Hub-internal pseudo agents.
10962
+ * User-facing creation must not be able to squat on them, otherwise
10963
+ * internal traffic could be routed through a real account.
10937
10964
  */
10938
10965
  const RESERVED_AGENT_NAME_PREFIX = "__";
10939
10966
  /**
@@ -11503,16 +11530,17 @@ async function findOrCreateChatForChannel(db, data) {
11503
11530
  return db.transaction(async (tx) => {
11504
11531
  const [botAgent] = await tx.select({ organizationId: agents.organizationId }).from(agents).where(eq(agents.uuid, data.botAgentId)).limit(1);
11505
11532
  const orgId = botAgent?.organizationId ?? await resolveDefaultOrgId(db);
11533
+ const metadata = chatMetadataSchema$1.parse({
11534
+ source: data.platform,
11535
+ externalChannelId: data.externalChannelId
11536
+ });
11506
11537
  await tx.insert(chats).values({
11507
11538
  id: chatId,
11508
11539
  organizationId: orgId,
11509
11540
  type: internalType,
11510
11541
  topic: data.topic ?? null,
11511
11542
  lifecyclePolicy: "adapter_managed",
11512
- metadata: {
11513
- source: data.platform,
11514
- externalChannelId: data.externalChannelId
11515
- }
11543
+ metadata
11516
11544
  });
11517
11545
  const participants = data.botAgentId === data.senderAgentId ? [{
11518
11546
  chatId,
@@ -12143,462 +12171,6 @@ async function agentSendToAgentRoutes(app) {
12143
12171
  });
12144
12172
  });
12145
12173
  }
12146
- /** Legal status transitions. Service enforces; API maps violations to 400. */
12147
- const STATUS_TRANSITIONS = {
12148
- pending: ["assigned", "cancelled"],
12149
- assigned: ["working", "cancelled"],
12150
- working: [
12151
- "completed",
12152
- "failed",
12153
- "cancelled"
12154
- ],
12155
- completed: [],
12156
- failed: [],
12157
- cancelled: []
12158
- };
12159
- function isLegalTransition(from, to) {
12160
- return STATUS_TRANSITIONS[from]?.includes(to) ?? false;
12161
- }
12162
- function isTerminal(status) {
12163
- return TASK_TERMINAL_STATUSES.includes(status);
12164
- }
12165
- /**
12166
- * Reserved name for the hub-owned task notifier pseudo agent. The `__` prefix
12167
- * is rejected by `createAgent`, so real users cannot squat on this identity.
12168
- */
12169
- const SYSTEM_TASKS_AGENT_NAME = "__hub_system_tasks";
12170
- /**
12171
- * Ensure a task-notifier pseudo agent exists in the given organization and
12172
- * return its UUID. Used as the sender for task notification messages so they
12173
- * flow through the normal chat/inbox pipeline. Idempotent under concurrent
12174
- * creation via the unique `(organization_id, name)` constraint.
12175
- */
12176
- async function ensureSystemTasksAgent(db, organizationId) {
12177
- 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);
12178
- if (existing) return existing.uuid;
12179
- const uuid = uuidv7();
12180
- const inboxId = `inbox_${uuid}`;
12181
- 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);
12182
- if (!adminMember) throw new ConflictError(`Cannot create system tasks agent in organization "${organizationId}" — no admin member exists.`);
12183
- try {
12184
- const [created] = await db.insert(agents).values({
12185
- uuid,
12186
- name: SYSTEM_TASKS_AGENT_NAME,
12187
- organizationId,
12188
- type: AGENT_TYPES.AUTONOMOUS_AGENT,
12189
- displayName: "System · Tasks",
12190
- inboxId,
12191
- status: AGENT_STATUSES.ACTIVE,
12192
- source: AGENT_SOURCES.ADMIN_API,
12193
- metadata: {
12194
- system: true,
12195
- role: "task-notifier"
12196
- },
12197
- managerId: adminMember.id
12198
- }).returning({ uuid: agents.uuid });
12199
- if (created) return created.uuid;
12200
- } catch (err) {
12201
- if ((err?.code ?? err?.cause?.code ?? "") !== "23505") throw err;
12202
- }
12203
- 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);
12204
- if (!row) throw new Error("ensureSystemTasksAgent: agent missing after conflict");
12205
- return row.uuid;
12206
- }
12207
- function resolveCreator(actor) {
12208
- if (actor.type === "agent") return {
12209
- type: TASK_CREATOR_TYPES.AGENT,
12210
- id: actor.agentId
12211
- };
12212
- return {
12213
- type: TASK_CREATOR_TYPES.ADMIN,
12214
- id: actor.adminId
12215
- };
12216
- }
12217
- /**
12218
- * Assert the task allows the given agent actor to mutate its chat associations.
12219
- * Only the creator or assignee (for agents) or any admin may do so.
12220
- */
12221
- function assertCanMutateTaskChats(task, actor) {
12222
- if (actor.type === "admin") return;
12223
- const isAssignee = task.assigneeAgentId === actor.agentId;
12224
- const isCreator = task.createdByType === TASK_CREATOR_TYPES.AGENT && task.createdById === actor.agentId;
12225
- if (!isAssignee && !isCreator) throw new ForbiddenError("Only the task creator or assignee may modify its chat associations");
12226
- }
12227
- async function loadAssigneeOrThrow(db, assigneeAgentId, expectedOrgId) {
12228
- const [assignee] = await db.select({
12229
- uuid: agents.uuid,
12230
- organizationId: agents.organizationId,
12231
- status: agents.status
12232
- }).from(agents).where(eq(agents.uuid, assigneeAgentId)).limit(1);
12233
- if (!assignee || assignee.status === AGENT_STATUSES.DELETED) throw new BadRequestError(`Assignee agent "${assigneeAgentId}" not found`);
12234
- if (assignee.organizationId !== expectedOrgId) throw new BadRequestError("Assignee agent belongs to a different organization");
12235
- if (assignee.status === AGENT_STATUSES.SUSPENDED) throw new BadRequestError(`Assignee agent "${assigneeAgentId}" is suspended`);
12236
- return assignee;
12237
- }
12238
- /**
12239
- * Create a task.
12240
- *
12241
- * Initial status is determined by assignee:
12242
- * - no assignee → "pending"
12243
- * - assignee is an agent and equals the creator → "working" (work-first; no notification)
12244
- * - assignee set and differs from creator → "assigned" (task-first; notification dispatched)
12245
- *
12246
- * Task-first notifications go through the regular message+inbox pipeline via a per-org
12247
- * task-notifier pseudo agent. The caller is responsible for triggering notifier fan-out
12248
- * using the returned notification recipients.
12249
- */
12250
- async function createTask(db, actor, input) {
12251
- const [org] = await db.select({ id: organizations.id }).from(organizations).where(eq(organizations.id, input.organizationId)).limit(1);
12252
- if (!org) throw new NotFoundError(`Organization "${input.organizationId}" not found`);
12253
- if (input.assigneeAgentId) await loadAssigneeOrThrow(db, input.assigneeAgentId, input.organizationId);
12254
- if (actor.type === "agent" && actor.organizationId !== input.organizationId) throw new ForbiddenError("Cannot create tasks in a different organization");
12255
- const creator = resolveCreator(actor);
12256
- const selfAssigned = input.assigneeAgentId !== void 0 && actor.type === "agent" && input.assigneeAgentId === actor.agentId;
12257
- let initialStatus;
12258
- if (!input.assigneeAgentId) initialStatus = TASK_STATUSES.PENDING;
12259
- else if (selfAssigned) initialStatus = TASK_STATUSES.WORKING;
12260
- else initialStatus = TASK_STATUSES.ASSIGNED;
12261
- const taskId = uuidv7();
12262
- const [task] = await db.insert(tasks).values({
12263
- id: taskId,
12264
- organizationId: input.organizationId,
12265
- title: input.title,
12266
- body: input.body ?? "",
12267
- status: initialStatus,
12268
- assigneeAgentId: input.assigneeAgentId ?? null,
12269
- createdByType: creator.type,
12270
- createdById: creator.id,
12271
- originRef: input.originRef ?? null,
12272
- metadata: input.metadata ?? {}
12273
- }).returning();
12274
- if (!task) throw new Error("Unexpected: INSERT RETURNING produced no row");
12275
- let notification;
12276
- if (initialStatus === TASK_STATUSES.ASSIGNED && task.assigneeAgentId) notification = await dispatchTaskSystemMessage(db, task, "assigned");
12277
- return {
12278
- task,
12279
- notification
12280
- };
12281
- }
12282
- /** Compose and send a system message describing a task state change to the assignee's chat. */
12283
- async function dispatchTaskSystemMessage(db, task, event, fromStatus) {
12284
- if (!task.assigneeAgentId) return void 0;
12285
- const systemAgentId = await ensureSystemTasksAgent(db, task.organizationId);
12286
- if (systemAgentId === task.assigneeAgentId) return void 0;
12287
- const chat = await findOrCreateDirectChat(db, systemAgentId, task.assigneeAgentId);
12288
- const content = {
12289
- taskId: task.id,
12290
- event,
12291
- title: task.title,
12292
- body: task.body,
12293
- status: task.status,
12294
- ...fromStatus ? { fromStatus } : {},
12295
- originRef: task.originRef
12296
- };
12297
- return sendMessage(db, chat.id, systemAgentId, {
12298
- format: "task",
12299
- content,
12300
- metadata: {
12301
- taskId: task.id,
12302
- event,
12303
- mentions: [task.assigneeAgentId]
12304
- }
12305
- });
12306
- }
12307
- /**
12308
- * Fetch a task, optionally asserting it belongs to `expectedOrgId`. Cross-org
12309
- * access is reported as NotFound so we don't leak existence across tenants.
12310
- */
12311
- async function getTask(db, taskId, expectedOrgId) {
12312
- const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId)).limit(1);
12313
- if (!task) throw new NotFoundError(`Task "${taskId}" not found`);
12314
- if (expectedOrgId !== void 0 && task.organizationId !== expectedOrgId) throw new NotFoundError(`Task "${taskId}" not found`);
12315
- return task;
12316
- }
12317
- async function getTaskDetail(db, taskId, expectedOrgId) {
12318
- const task = await getTask(db, taskId, expectedOrgId);
12319
- const links = await db.select().from(taskChats).where(eq(taskChats.taskId, taskId));
12320
- return {
12321
- ...task,
12322
- chats: links.map((c) => ({
12323
- taskId: c.taskId,
12324
- chatId: c.chatId,
12325
- linkedByAgentId: c.linkedByAgentId,
12326
- linkedAt: c.linkedAt.toISOString()
12327
- }))
12328
- };
12329
- }
12330
- async function listTasks(db, organizationId, query) {
12331
- const conditions = [eq(tasks.organizationId, organizationId)];
12332
- if (query.status) conditions.push(eq(tasks.status, query.status));
12333
- if (query.assigneeAgentId) conditions.push(eq(tasks.assigneeAgentId, query.assigneeAgentId));
12334
- if (query.originRef) conditions.push(eq(tasks.originRef, query.originRef));
12335
- if (query.cursor) conditions.push(lt(tasks.createdAt, new Date(query.cursor)));
12336
- const rows = await db.select().from(tasks).where(and(...conditions)).orderBy(desc(tasks.createdAt)).limit(query.limit + 1);
12337
- const hasMore = rows.length > query.limit;
12338
- const items = hasMore ? rows.slice(0, query.limit) : rows;
12339
- const last = items[items.length - 1];
12340
- return {
12341
- items,
12342
- nextCursor: hasMore && last ? last.createdAt.toISOString() : null
12343
- };
12344
- }
12345
- /** Agent self-report: working / completed / failed. */
12346
- async function updateTaskStatus(db, taskId, actor, data) {
12347
- const existing = await getTask(db, taskId);
12348
- if (actor.type !== "agent") throw new ForbiddenError("updateTaskStatus is for agent self-report; use adminUpdateTask for admin actions");
12349
- if (existing.assigneeAgentId !== actor.agentId) throw new ForbiddenError("Only the assignee may update this task");
12350
- const from = existing.status;
12351
- const to = data.status;
12352
- if (!isLegalTransition(from, to)) throw new BadRequestError(`Illegal status transition: ${from} → ${to}`);
12353
- if (to === TASK_STATUSES.COMPLETED && data.result === void 0) throw new BadRequestError("Completion requires a result (may be an empty string)");
12354
- const updates = {
12355
- status: to,
12356
- updatedAt: /* @__PURE__ */ new Date()
12357
- };
12358
- if (data.result !== void 0) updates.result = data.result;
12359
- const [updated] = await db.update(tasks).set(updates).where(eq(tasks.id, taskId)).returning();
12360
- if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
12361
- return { task: updated };
12362
- }
12363
- /** Admin-facing update: may re-assign while pending, or force a status transition (still gated by state machine). */
12364
- async function adminUpdateTask(db, taskId, actor, data) {
12365
- if (actor.type !== "admin") throw new ForbiddenError("adminUpdateTask requires admin actor");
12366
- const existing = await getTask(db, taskId);
12367
- if (data.status === TASK_STATUSES.CANCELLED) {
12368
- if (isTerminal(existing.status)) throw new ConflictError(`Task is already in terminal state "${existing.status}"`);
12369
- return cancelTask(db, taskId, actor);
12370
- }
12371
- const updates = { updatedAt: /* @__PURE__ */ new Date() };
12372
- let notify = false;
12373
- if (data.assigneeAgentId !== void 0) {
12374
- if (existing.status !== TASK_STATUSES.PENDING && data.assigneeAgentId !== existing.assigneeAgentId) throw new BadRequestError("Cannot reassign a task that is not pending");
12375
- if (data.assigneeAgentId !== null) {
12376
- await loadAssigneeOrThrow(db, data.assigneeAgentId, existing.organizationId);
12377
- updates.assigneeAgentId = data.assigneeAgentId;
12378
- updates.status = TASK_STATUSES.ASSIGNED;
12379
- notify = true;
12380
- } else {
12381
- updates.assigneeAgentId = null;
12382
- updates.status = TASK_STATUSES.PENDING;
12383
- }
12384
- }
12385
- if (data.status !== void 0 && data.status !== existing.status) {
12386
- const from = updates.status ?? existing.status;
12387
- if (!isLegalTransition(from, data.status)) throw new BadRequestError(`Illegal status transition: ${from} → ${data.status}`);
12388
- updates.status = data.status;
12389
- }
12390
- if (data.result !== void 0) updates.result = data.result;
12391
- const resolvedStatus = updates.status ?? existing.status;
12392
- const resolvedAssignee = updates.assigneeAgentId === void 0 ? existing.assigneeAgentId : updates.assigneeAgentId;
12393
- if (resolvedStatus === TASK_STATUSES.ASSIGNED && !resolvedAssignee) throw new BadRequestError("Cannot set status to \"assigned\" without an assignee");
12394
- const [updated] = await db.update(tasks).set(updates).where(eq(tasks.id, taskId)).returning();
12395
- if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
12396
- let notification;
12397
- if (notify && updated.assigneeAgentId) notification = await dispatchTaskSystemMessage(db, updated, "assigned");
12398
- return {
12399
- task: updated,
12400
- notification
12401
- };
12402
- }
12403
- async function cancelTask(db, taskId, actor) {
12404
- const existing = await getTask(db, taskId);
12405
- if (isTerminal(existing.status)) throw new ConflictError(`Task is already in terminal state "${existing.status}"`);
12406
- if (actor.type === "agent") {
12407
- const isAssignee = existing.assigneeAgentId === actor.agentId;
12408
- const isCreator = existing.createdByType === TASK_CREATOR_TYPES.AGENT && existing.createdById === actor.agentId;
12409
- if (!isAssignee && !isCreator) throw new ForbiddenError("Only the assignee or creator may cancel this task");
12410
- }
12411
- const now = /* @__PURE__ */ new Date();
12412
- const { type: cancelType, id: cancelId } = resolveCreator(actor);
12413
- const [updated] = await db.update(tasks).set({
12414
- status: TASK_STATUSES.CANCELLED,
12415
- cancelledAt: now,
12416
- cancelledByType: cancelType,
12417
- cancelledById: cancelId,
12418
- updatedAt: now
12419
- }).where(eq(tasks.id, taskId)).returning();
12420
- if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
12421
- let notification;
12422
- if (updated.assigneeAgentId && !(actor.type === "agent" && actor.agentId === updated.assigneeAgentId)) notification = await dispatchTaskSystemMessage(db, updated, "cancelled", existing.status);
12423
- return {
12424
- task: updated,
12425
- notification
12426
- };
12427
- }
12428
- async function linkChatToTask(db, taskId, chatId, actor) {
12429
- const task = await getTask(db, taskId);
12430
- if (actor.type === "agent" && task.organizationId !== actor.organizationId) throw new NotFoundError(`Task "${taskId}" not found`);
12431
- assertCanMutateTaskChats(task, actor);
12432
- const [chat] = await db.select({ organizationId: chats.organizationId }).from(chats).where(eq(chats.id, chatId)).limit(1);
12433
- if (!chat) throw new NotFoundError(`Chat "${chatId}" not found`);
12434
- if (chat.organizationId !== task.organizationId) throw new BadRequestError("Chat belongs to a different organization");
12435
- if (actor.type === "agent") await assertParticipant(db, chatId, actor.agentId);
12436
- const linkedBy = actor.type === "agent" ? actor.agentId : null;
12437
- await db.insert(taskChats).values({
12438
- taskId,
12439
- chatId,
12440
- linkedByAgentId: linkedBy
12441
- }).onConflictDoNothing();
12442
- }
12443
- async function unlinkChatFromTask(db, taskId, chatId, actor) {
12444
- const task = await getTask(db, taskId);
12445
- if (actor.type === "agent" && task.organizationId !== actor.organizationId) throw new NotFoundError(`Task "${taskId}" not found`);
12446
- assertCanMutateTaskChats(task, actor);
12447
- 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}"`);
12448
- }
12449
- /**
12450
- * Derive a health signal for a task. Only meaningful for `working` tasks.
12451
- * See hub-task-design Section 9 for the rules this implements.
12452
- *
12453
- * Algorithm (per linked chat for the assignee):
12454
- * 1. No session row OR state != 'active' → idle_island candidate
12455
- * 2. Session active, last message from assignee → awaiting_reply candidate
12456
- * 3. Session active, last message from other → normal candidate
12457
- * Across all linked chats, normal wins over awaiting_reply, which wins over idle_island.
12458
- */
12459
- async function getTaskHealth(db, taskId, expectedOrgId) {
12460
- const task = await getTask(db, taskId, expectedOrgId);
12461
- if (task.status !== TASK_STATUSES.WORKING) return {
12462
- taskId,
12463
- signal: TASK_HEALTH_SIGNALS.NOT_APPLICABLE,
12464
- reason: `Task status is "${task.status}" — health is only computed for working tasks`
12465
- };
12466
- if (!task.assigneeAgentId) return {
12467
- taskId,
12468
- signal: TASK_HEALTH_SIGNALS.NO_CHAT,
12469
- reason: "Task has no assignee"
12470
- };
12471
- const linked = await db.select({
12472
- chatId: taskChats.chatId,
12473
- sessionState: agentChatSessions.state
12474
- }).from(taskChats).leftJoin(agentChatSessions, and(eq(agentChatSessions.chatId, taskChats.chatId), eq(agentChatSessions.agentId, task.assigneeAgentId))).where(eq(taskChats.taskId, taskId));
12475
- if (linked.length === 0) return {
12476
- taskId,
12477
- signal: TASK_HEALTH_SIGNALS.NO_CHAT,
12478
- reason: "Task has no linked chats"
12479
- };
12480
- const chatSignals = [];
12481
- for (const row of linked) {
12482
- if (row.sessionState !== "active") {
12483
- chatSignals.push(TASK_HEALTH_SIGNALS.IDLE_ISLAND);
12484
- continue;
12485
- }
12486
- const [last] = await db.select({ senderId: messages.senderId }).from(messages).where(eq(messages.chatId, row.chatId)).orderBy(desc(messages.createdAt)).limit(1);
12487
- if (!last) {
12488
- chatSignals.push(TASK_HEALTH_SIGNALS.IDLE_ISLAND);
12489
- continue;
12490
- }
12491
- if (last.senderId === task.assigneeAgentId) chatSignals.push(TASK_HEALTH_SIGNALS.AWAITING_REPLY);
12492
- else chatSignals.push(TASK_HEALTH_SIGNALS.NORMAL);
12493
- }
12494
- if (chatSignals.includes(TASK_HEALTH_SIGNALS.NORMAL)) return {
12495
- taskId,
12496
- signal: TASK_HEALTH_SIGNALS.NORMAL,
12497
- reason: "At least one linked chat is actively progressing"
12498
- };
12499
- if (chatSignals.includes(TASK_HEALTH_SIGNALS.AWAITING_REPLY)) return {
12500
- taskId,
12501
- signal: TASK_HEALTH_SIGNALS.AWAITING_REPLY,
12502
- reason: "Assignee sent the last message and is waiting for a reply"
12503
- };
12504
- return {
12505
- taskId,
12506
- signal: TASK_HEALTH_SIGNALS.IDLE_ISLAND,
12507
- reason: "No active session found for the assignee in any linked chat"
12508
- };
12509
- }
12510
- /** Serialize a task row for API output. */
12511
- function serializeTask(task) {
12512
- return {
12513
- ...task,
12514
- createdAt: task.createdAt.toISOString(),
12515
- updatedAt: task.updatedAt.toISOString(),
12516
- cancelledAt: task.cancelledAt ? task.cancelledAt.toISOString() : null
12517
- };
12518
- }
12519
- function dispatch$2(notifier, result) {
12520
- if (!result) return;
12521
- notifyRecipients(notifier, result.recipients, result.message.id);
12522
- }
12523
- async function agentTaskRoutes(app) {
12524
- /** Create a task. Agent creator; assignee defaults to self (work-first) if omitted. */
12525
- app.post("/", async (request, reply) => {
12526
- const identity = requireAgent(request);
12527
- const body = createTaskSchema.parse(request.body);
12528
- const { task, notification } = await createTask(app.db, {
12529
- type: "agent",
12530
- agentId: identity.uuid,
12531
- organizationId: identity.organizationId
12532
- }, {
12533
- ...body,
12534
- organizationId: identity.organizationId
12535
- });
12536
- dispatch$2(app.notifier, notification);
12537
- return reply.status(201).send(serializeTask(task));
12538
- });
12539
- app.get("/", async (request) => {
12540
- const identity = requireAgent(request);
12541
- const query = taskListQuerySchema.parse(request.query);
12542
- const result = await listTasks(app.db, identity.organizationId, query);
12543
- return {
12544
- items: result.items.map((t) => serializeTask(t)),
12545
- nextCursor: result.nextCursor
12546
- };
12547
- });
12548
- app.get("/:taskId", async (request) => {
12549
- const identity = requireAgent(request);
12550
- const detail = await getTaskDetail(app.db, request.params.taskId, identity.organizationId);
12551
- return {
12552
- ...serializeTask(detail),
12553
- chats: detail.chats
12554
- };
12555
- });
12556
- /** Agent self-report: working / completed / failed. */
12557
- app.patch("/:taskId", async (request) => {
12558
- const identity = requireAgent(request);
12559
- const body = updateTaskStatusSchema.parse(request.body);
12560
- const { task } = await updateTaskStatus(app.db, request.params.taskId, {
12561
- type: "agent",
12562
- agentId: identity.uuid,
12563
- organizationId: identity.organizationId
12564
- }, body);
12565
- return serializeTask(task);
12566
- });
12567
- app.post("/:taskId/cancel", async (request) => {
12568
- const identity = requireAgent(request);
12569
- const { task, notification } = await cancelTask(app.db, request.params.taskId, {
12570
- type: "agent",
12571
- agentId: identity.uuid,
12572
- organizationId: identity.organizationId
12573
- });
12574
- dispatch$2(app.notifier, notification);
12575
- return serializeTask(task);
12576
- });
12577
- app.post("/:taskId/chats", async (request, reply) => {
12578
- const identity = requireAgent(request);
12579
- const body = linkTaskChatSchema.parse(request.body);
12580
- await linkChatToTask(app.db, request.params.taskId, body.chatId, {
12581
- type: "agent",
12582
- agentId: identity.uuid,
12583
- organizationId: identity.organizationId
12584
- });
12585
- return reply.status(204).send();
12586
- });
12587
- app.delete("/:taskId/chats/:chatId", async (request, reply) => {
12588
- const identity = requireAgent(request);
12589
- await unlinkChatFromTask(app.db, request.params.taskId, request.params.chatId, {
12590
- type: "agent",
12591
- agentId: identity.uuid,
12592
- organizationId: identity.organizationId
12593
- });
12594
- return reply.status(204).send();
12595
- });
12596
- /** Task health signal — only meaningful while task.status === "working". */
12597
- app.get("/:taskId/health", async (request) => {
12598
- const identity = requireAgent(request);
12599
- return getTaskHealth(app.db, request.params.taskId, identity.organizationId);
12600
- });
12601
- }
12602
12174
  /** WS close code: agent already connected from another client. */
12603
12175
  const WS_CLOSE_ALREADY_CONNECTED = 4009;
12604
12176
  /** Track active WS connections per agentId. At most one entry per agent. */
@@ -15174,9 +14746,7 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
15174
14746
  lastMessageAt: toDate(r.last_message_at)?.toISOString() ?? null,
15175
14747
  lastMessagePreview: r.last_message_preview,
15176
14748
  unreadMentionCount: r.unread_mention_count,
15177
- canReply: r.membership_kind === "participant",
15178
- taskId: null,
15179
- taskStatus: null
14749
+ canReply: r.membership_kind === "participant"
15180
14750
  };
15181
14751
  }),
15182
14752
  nextCursor
@@ -16894,7 +16464,7 @@ async function healthzRoutes(app) {
16894
16464
  * `api/orgs/invitations.ts` (Class B, admin-gated).
16895
16465
  */
16896
16466
  async function publicInvitationRoutes(app) {
16897
- const { previewInvitation } = await import("./invitation-C299fxkP-B89eqDos.mjs");
16467
+ const { previewInvitation } = await import("./invitation-C299fxkP-Dts66QTU.mjs");
16898
16468
  app.get("/:token/preview", async (request, reply) => {
16899
16469
  if (!request.params.token) throw new UnauthorizedError("Token required");
16900
16470
  const preview = await previewInvitation(app.db, request.params.token);
@@ -17074,7 +16644,7 @@ async function meRoutes(app) {
17074
16644
  */
17075
16645
  app.get("/me/pinned-agents", async (request) => {
17076
16646
  const { userId } = requireUser(request);
17077
- const { listMyPinnedAgents } = await import("./client-0RrgrMjR-DPyuu6Ls.mjs");
16647
+ const { listMyPinnedAgents } = await import("./client-DSM_opoz-BH5eegXb.mjs");
17078
16648
  return listMyPinnedAgents(app.db, { userId });
17079
16649
  });
17080
16650
  /**
@@ -17887,39 +17457,6 @@ function enrichOutput(namespace, out, orgId, publicUrl) {
17887
17457
  }
17888
17458
  return out;
17889
17459
  }
17890
- function dispatch$1(notifier, result) {
17891
- if (!result) return;
17892
- notifyRecipients(notifier, result.recipients, result.message.id);
17893
- }
17894
- /** Class B — `/api/v1/orgs/:orgId/tasks`. Per-task ops live in api/tasks.ts. */
17895
- async function orgTaskRoutes(app) {
17896
- app.get("/", async (request) => {
17897
- const scope = await requireOrgMembership(request, app.db);
17898
- const query = taskListQuerySchema.parse(request.query);
17899
- const result = await listTasks(app.db, scope.organizationId, query);
17900
- return {
17901
- items: result.items.map((t) => serializeTask(t)),
17902
- nextCursor: result.nextCursor
17903
- };
17904
- });
17905
- app.post("/", async (request, reply) => {
17906
- const scope = await requireOrgMembership(request, app.db);
17907
- const body = adminCreateTaskSchema.parse(request.body);
17908
- const { task, notification } = await createTask(app.db, {
17909
- type: "admin",
17910
- adminId: scope.memberId
17911
- }, {
17912
- title: body.title,
17913
- body: body.body,
17914
- ...body.assigneeAgentId !== void 0 ? { assigneeAgentId: body.assigneeAgentId } : {},
17915
- ...body.originRef !== void 0 ? { originRef: body.originRef } : {},
17916
- ...body.metadata !== void 0 ? { metadata: body.metadata } : {},
17917
- organizationId: scope.organizationId
17918
- });
17919
- dispatch$1(app.notifier, notification);
17920
- return reply.status(201).send(serializeTask(task));
17921
- });
17922
- }
17923
17460
  async function loadVisibleAgentIds(db, organizationId, memberId) {
17924
17461
  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))));
17925
17462
  return new Set(rows.map((r) => r.id));
@@ -18140,97 +17677,380 @@ async function sessionRoutes(app) {
18140
17677
  });
18141
17678
  });
18142
17679
  }
18143
- function dispatch(notifier, result) {
18144
- if (!result) return;
18145
- notifyRecipients(notifier, result.recipients, result.message.id);
17680
+ function isRecord(value) {
17681
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17682
+ }
17683
+ /** Pull `repository.full_name` ("owner/repo") from a webhook payload, or null. */
17684
+ function repoFullName(payload) {
17685
+ if (!isRecord(payload)) return null;
17686
+ const repo = isRecord(payload.repository) ? payload.repository : null;
17687
+ return typeof repo?.full_name === "string" && repo.full_name.length > 0 ? repo.full_name : null;
17688
+ }
17689
+ function readNumber(value) {
17690
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
17691
+ }
17692
+ function readString(value) {
17693
+ return typeof value === "string" && value.length > 0 ? value : null;
17694
+ }
17695
+ /**
17696
+ * Resolve the entity that a GitHub webhook event belongs to.
17697
+ *
17698
+ * Returns `null` when the event isn't a clustering candidate (event type
17699
+ * outside the §4.1 "core" list, malformed payload). Caller is expected to
17700
+ * skip such events.
17701
+ *
17702
+ * Notes
17703
+ * - `commit_comment` falls back to a `commit` entity keyed on `<repo>@<sha>`
17704
+ * when no associated PR is in the payload — the design hedges on "optionally
17705
+ * resolve to a PR", but doing so requires an extra GitHub API call which we
17706
+ * defer to Phase 1+.
17707
+ */
17708
+ function extractEventEntity(eventType, payload) {
17709
+ if (!isRecord(payload)) return null;
17710
+ const repo = repoFullName(payload);
17711
+ if (!repo) return null;
17712
+ switch (eventType) {
17713
+ case "issues":
17714
+ case "issue_comment": {
17715
+ const issue = isRecord(payload.issue) ? payload.issue : null;
17716
+ const number = readNumber(issue?.number);
17717
+ if (number === null) return null;
17718
+ return {
17719
+ type: "issue",
17720
+ key: `${repo}#${number}`,
17721
+ title: readString(issue?.title) ?? void 0,
17722
+ url: readString(issue?.html_url) ?? void 0
17723
+ };
17724
+ }
17725
+ case "pull_request":
17726
+ case "pull_request_review":
17727
+ case "pull_request_review_comment": {
17728
+ const pr = isRecord(payload.pull_request) ? payload.pull_request : null;
17729
+ const number = readNumber(pr?.number);
17730
+ if (number === null) return null;
17731
+ return {
17732
+ type: "pull_request",
17733
+ key: `${repo}#${number}`,
17734
+ title: readString(pr?.title) ?? void 0,
17735
+ url: readString(pr?.html_url) ?? void 0
17736
+ };
17737
+ }
17738
+ case "discussion":
17739
+ case "discussion_comment": {
17740
+ const disc = isRecord(payload.discussion) ? payload.discussion : null;
17741
+ const number = readNumber(disc?.number);
17742
+ if (number === null) return null;
17743
+ return {
17744
+ type: "discussion",
17745
+ key: `${repo}#discussion-${number}`,
17746
+ title: readString(disc?.title) ?? void 0,
17747
+ url: readString(disc?.html_url) ?? void 0
17748
+ };
17749
+ }
17750
+ case "commit_comment": {
17751
+ const comment = isRecord(payload.comment) ? payload.comment : null;
17752
+ const sha = readString(comment?.commit_id);
17753
+ if (!sha) return null;
17754
+ return {
17755
+ type: "commit",
17756
+ key: `${repo}@${sha}`,
17757
+ url: readString(comment?.html_url) ?? void 0
17758
+ };
17759
+ }
17760
+ default: return null;
17761
+ }
17762
+ }
17763
+ /**
17764
+ * Closing-keyword regex from
17765
+ * https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue
17766
+ * — `close[sd]? | fix(es|ed)? | resolve[sd]?`. Cross-repo `org/repo#N` is
17767
+ * deliberately excluded (out of scope for Phase 0; see §4.5).
17768
+ */
17769
+ const FIXES_KEYWORDS_RE = /\b(?:close[sd]?|fix(?:es|ed)?|resolve[sd]?)\s+#(\d+)\b/gi;
17770
+ /**
17771
+ * Parse `Fixes #N` / `Closes #N` / `Resolves #N` references out of a PR body.
17772
+ * Returns ordered, deduplicated entity references for issues in the same repo
17773
+ * (cross-repo refs ignored per §4.5).
17774
+ *
17775
+ * Caller is expected to pass `repoFullName` so we can build the entity key.
17776
+ */
17777
+ function parseFixesRefs(text, repoFullName) {
17778
+ if (!text) return [];
17779
+ const seen = /* @__PURE__ */ new Set();
17780
+ const out = [];
17781
+ for (const match of text.matchAll(FIXES_KEYWORDS_RE)) {
17782
+ const num = match[1];
17783
+ if (!num) continue;
17784
+ const key = `${repoFullName}#${num}`;
17785
+ if (seen.has(key)) continue;
17786
+ seen.add(key);
17787
+ out.push({
17788
+ type: "issue",
17789
+ key
17790
+ });
17791
+ }
17792
+ return out;
17793
+ }
17794
+ /**
17795
+ * Pick a chat-title prefix from (entity, eventType, action).
17796
+ *
17797
+ * PR review-flow events (`pull_request.review_requested`,
17798
+ * `pull_request_review.*`, `pull_request_review_comment.*`) collapse into a
17799
+ * single "PR Review" prefix so a chat first-touched by a review event is
17800
+ * visibly distinct from one first-touched by `pull_request.opened`. Everything
17801
+ * else just renders the entity type.
17802
+ *
17803
+ * Note: chat titles are written once at chat creation (see
17804
+ * `github-entity-chat.ts::createEntityChat`) — subsequent events for the same
17805
+ * entity reuse the existing title even if their (event, action) maps to a
17806
+ * different prefix. This matches the "entity is the container" semantic.
17807
+ */
17808
+ function entityTitlePrefix(entity, eventType, action) {
17809
+ if (eventType === "pull_request" && action === "review_requested") return "PR Review";
17810
+ if (eventType === "pull_request_review") return "PR Review";
17811
+ if (eventType === "pull_request_review_comment") return "PR Review";
17812
+ switch (entity.type) {
17813
+ case "issue": return "Issue";
17814
+ case "pull_request": return "PR";
17815
+ case "discussion": return "Discussion";
17816
+ case "commit": return "Commit";
17817
+ }
17818
+ }
17819
+ /**
17820
+ * Strip the leading `owner/` segment from an entity key so the chat title
17821
+ * stays compact. `owner/repo#42` → `repo#42`; `owner/repo@abc1234` →
17822
+ * `repo@abc1234`. The full `owner/repo#N` form is still used as the
17823
+ * clustering primary key (`github_entity_chat_mappings.entity_key`); only the
17824
+ * display string is shortened.
17825
+ */
17826
+ function shortEntityKey(key) {
17827
+ const slash = key.indexOf("/");
17828
+ return slash === -1 ? key : key.slice(slash + 1);
17829
+ }
17830
+ /**
17831
+ * Render a chat topic from an entity. Used as the chat title; kept short so
17832
+ * the chat-list row doesn't truncate aggressively.
17833
+ *
17834
+ * formatEntityTitle({ type: "pull_request", key: "owner/repo#307", title: "Improve overview" }, "pull_request", "opened")
17835
+ * → "PR repo#307: Improve overview"
17836
+ * formatEntityTitle(<same>, "pull_request", "review_requested")
17837
+ * → "PR Review repo#307: Improve overview"
17838
+ */
17839
+ function formatEntityTitle(entity, eventType, action) {
17840
+ const head = `${entityTitlePrefix(entity, eventType, action)} ${shortEntityKey(entity.key)}`;
17841
+ if (entity.title && entity.title.length > 0) return `${head}: ${entity.title}`;
17842
+ return head;
17843
+ }
17844
+ const SILENT_EVENT_TYPES = new Set([
17845
+ "workflow_run",
17846
+ "workflow_job",
17847
+ "check_run",
17848
+ "check_suite",
17849
+ "status",
17850
+ "push",
17851
+ "create",
17852
+ "delete",
17853
+ "fork",
17854
+ "watch",
17855
+ "release",
17856
+ "label",
17857
+ "label_created",
17858
+ "label_deleted",
17859
+ "reaction",
17860
+ "member",
17861
+ "membership",
17862
+ "team",
17863
+ "team_add",
17864
+ "organization",
17865
+ "org_block",
17866
+ "project",
17867
+ "project_card",
17868
+ "project_column"
17869
+ ]);
17870
+ /**
17871
+ * Per-event-type action-level filters. Frequent low-signal actions that would
17872
+ * otherwise spam an entity chat. `synchronize` (PR branch push) is the most
17873
+ * common offender — it fires on every commit push to a PR branch and never
17874
+ * carries new conversation.
17875
+ */
17876
+ const SILENT_ACTIONS = {
17877
+ issues: new Set([
17878
+ "labeled",
17879
+ "unlabeled",
17880
+ "milestoned",
17881
+ "demilestoned",
17882
+ "pinned",
17883
+ "unpinned"
17884
+ ]),
17885
+ pull_request: new Set([
17886
+ "labeled",
17887
+ "unlabeled",
17888
+ "auto_merge_enabled",
17889
+ "auto_merge_disabled",
17890
+ "synchronize"
17891
+ ])
17892
+ };
17893
+ /** True iff the event should be silently 200-OKed without further routing. */
17894
+ function shouldSilent(eventType, payload) {
17895
+ if (SILENT_EVENT_TYPES.has(eventType)) return true;
17896
+ if (!isRecord(payload)) return false;
17897
+ if (readString((isRecord(payload.sender) ? payload.sender : null)?.type) === "Bot") return true;
17898
+ const action = readString(payload.action);
17899
+ if (!action) return false;
17900
+ return SILENT_ACTIONS[eventType]?.has(action) ?? false;
18146
17901
  }
18147
- /** Class C — `/api/v1/tasks/:taskId`. The task's `organizationId` locates the org. */
18148
- async function taskRoutes(app) {
18149
- app.get("/:taskId", async (request) => {
18150
- await requireTaskAccess(request, app.db);
18151
- const detail = await getTaskDetail(app.db, request.params.taskId);
17902
+ /**
17903
+ * GitHub-specific webhook entity → chat clustering (Phase 0).
17904
+ *
17905
+ * Each `(organization, human_agent, delegate_agent, entity)` tuple resolves to
17906
+ * exactly one chat. Future external sources (Linear, Slack, …) get their own
17907
+ * tables — their entity models differ enough that a generic table would slip
17908
+ * back into untyped jsonb.
17909
+ *
17910
+ * `bound_via` distinguishes the first-touch row (`direct`) from a row written
17911
+ * by the `Fixes #N` linker (`fixes_link`). Routing logic ignores the
17912
+ * distinction; it exists for audit and future strategy tweaks.
17913
+ */
17914
+ const githubEntityChatMappings = pgTable("github_entity_chat_mappings", {
17915
+ organizationId: text("organization_id").notNull().references(() => organizations.id),
17916
+ humanAgentId: text("human_agent_id").notNull().references(() => agents.uuid, { onDelete: "cascade" }),
17917
+ delegateAgentId: text("delegate_agent_id").notNull().references(() => agents.uuid, { onDelete: "cascade" }),
17918
+ entityType: text("entity_type").notNull(),
17919
+ entityKey: text("entity_key").notNull(),
17920
+ chatId: text("chat_id").notNull().references(() => chats.id, { onDelete: "cascade" }),
17921
+ boundAt: timestamp("bound_at", { withTimezone: true }).notNull().defaultNow(),
17922
+ boundVia: text("bound_via").notNull()
17923
+ }, (table) => [primaryKey({ columns: [
17924
+ table.organizationId,
17925
+ table.humanAgentId,
17926
+ table.delegateAgentId,
17927
+ table.entityType,
17928
+ table.entityKey
17929
+ ] }), index("idx_github_entity_chat_mappings_chat").on(table.chatId)]);
17930
+ /**
17931
+ * Resolve which chat a GitHub event for (human, delegate, entity) belongs to.
17932
+ *
17933
+ * Three-step strategy from docs/webhook-routing-design.md §4.4:
17934
+ * a. Direct hit — entity already bound; reuse that chat.
17935
+ * b. Fixes-link — any related entity (parsed from `Fixes #N` in a PR body)
17936
+ * already bound; write a `fixes_link` row for this entity pointing at
17937
+ * the same chat, return it.
17938
+ * c. Miss — create a fresh chat via the canonical `createChat` entrypoint
17939
+ * and write a `direct` mapping row.
17940
+ *
17941
+ * Concurrent webhook deliveries for a never-before-seen entity race on (c);
17942
+ * the composite primary key + ON CONFLICT DO NOTHING ensures only one row
17943
+ * survives. The losing caller falls back to a re-read so the chat stays
17944
+ * unique.
17945
+ */
17946
+ async function resolveTargetChat(db, params) {
17947
+ const { organizationId, humanAgentId, delegateAgentId, entity, relatedEntities, eventType, action } = params;
17948
+ const direct = await lookupMapping(db, organizationId, humanAgentId, delegateAgentId, entity);
17949
+ if (direct) return {
17950
+ chatId: direct.chatId,
17951
+ created: false,
17952
+ boundVia: direct.boundVia
17953
+ };
17954
+ for (const ref of relatedEntities) {
17955
+ const linked = await lookupMapping(db, organizationId, humanAgentId, delegateAgentId, ref);
17956
+ if (!linked) continue;
17957
+ const inserted = await insertMappingIfAbsent(db, {
17958
+ organizationId,
17959
+ humanAgentId,
17960
+ delegateAgentId,
17961
+ entity,
17962
+ chatId: linked.chatId,
17963
+ boundVia: "fixes_link"
17964
+ });
18152
17965
  return {
18153
- ...serializeTask(detail),
18154
- chats: detail.chats
17966
+ chatId: inserted.chatId,
17967
+ created: false,
17968
+ boundVia: inserted.boundVia
18155
17969
  };
17970
+ }
17971
+ const chat = await createEntityChat(db, humanAgentId, delegateAgentId, entity, eventType, action);
17972
+ const inserted = await insertMappingIfAbsent(db, {
17973
+ organizationId,
17974
+ humanAgentId,
17975
+ delegateAgentId,
17976
+ entity,
17977
+ chatId: chat.id,
17978
+ boundVia: "direct"
18156
17979
  });
18157
- app.patch("/:taskId", async (request) => {
18158
- const { scope } = await requireTaskAccess(request, app.db);
18159
- const body = adminUpdateTaskSchema.parse(request.body);
18160
- const { task, notification } = await adminUpdateTask(app.db, request.params.taskId, {
18161
- type: "admin",
18162
- adminId: scope.memberId
18163
- }, body);
18164
- dispatch(app.notifier, notification);
18165
- return serializeTask(task);
18166
- });
18167
- app.post("/:taskId/cancel", async (request) => {
18168
- const { scope } = await requireTaskAccess(request, app.db);
18169
- const { task, notification } = await cancelTask(app.db, request.params.taskId, {
18170
- type: "admin",
18171
- adminId: scope.memberId
18172
- });
18173
- dispatch(app.notifier, notification);
18174
- return serializeTask(task);
17980
+ return {
17981
+ chatId: inserted.chatId,
17982
+ created: inserted.chatId === chat.id,
17983
+ boundVia: inserted.boundVia
17984
+ };
17985
+ }
17986
+ async function lookupMapping(db, organizationId, humanAgentId, delegateAgentId, entity) {
17987
+ const [row] = await db.select({
17988
+ chatId: githubEntityChatMappings.chatId,
17989
+ boundVia: githubEntityChatMappings.boundVia
17990
+ }).from(githubEntityChatMappings).where(and(eq(githubEntityChatMappings.organizationId, organizationId), eq(githubEntityChatMappings.humanAgentId, humanAgentId), eq(githubEntityChatMappings.delegateAgentId, delegateAgentId), eq(githubEntityChatMappings.entityType, entity.type), eq(githubEntityChatMappings.entityKey, entity.key))).limit(1);
17991
+ if (!row) return null;
17992
+ return {
17993
+ chatId: row.chatId,
17994
+ boundVia: row.boundVia === "fixes_link" ? "fixes_link" : "direct"
17995
+ };
17996
+ }
17997
+ async function insertMappingIfAbsent(db, params) {
17998
+ const [inserted] = await db.insert(githubEntityChatMappings).values({
17999
+ organizationId: params.organizationId,
18000
+ humanAgentId: params.humanAgentId,
18001
+ delegateAgentId: params.delegateAgentId,
18002
+ entityType: params.entity.type,
18003
+ entityKey: params.entity.key,
18004
+ chatId: params.chatId,
18005
+ boundVia: params.boundVia
18006
+ }).onConflictDoNothing({ target: [
18007
+ githubEntityChatMappings.organizationId,
18008
+ githubEntityChatMappings.humanAgentId,
18009
+ githubEntityChatMappings.delegateAgentId,
18010
+ githubEntityChatMappings.entityType,
18011
+ githubEntityChatMappings.entityKey
18012
+ ] }).returning({
18013
+ chatId: githubEntityChatMappings.chatId,
18014
+ boundVia: githubEntityChatMappings.boundVia
18175
18015
  });
18176
- app.get("/:taskId/health", async (request) => {
18177
- await requireTaskAccess(request, app.db);
18178
- return getTaskHealth(app.db, request.params.taskId);
18016
+ if (inserted) return {
18017
+ chatId: inserted.chatId,
18018
+ boundVia: inserted.boundVia === "fixes_link" ? "fixes_link" : "direct"
18019
+ };
18020
+ const winner = await lookupMapping(db, params.organizationId, params.humanAgentId, params.delegateAgentId, params.entity);
18021
+ if (!winner) throw new Error("Unexpected: mapping insert conflicted but row not visible on re-read");
18022
+ return winner;
18023
+ }
18024
+ /**
18025
+ * Create a fresh chat for a (human, delegate, entity) tuple. Goes through the
18026
+ * canonical `createChat` so:
18027
+ * - cross-org participants are rejected (BadRequestError)
18028
+ * - direct agent-only chats automatically get `mode=mention_only`
18029
+ * - watcher rows are recomputed
18030
+ * - a future addParticipant call would upgrade the chat to `group` via
18031
+ * `maybeUpgradeDirectToGroup` instead of raw INSERT shortcuts
18032
+ */
18033
+ async function createEntityChat(db, humanAgentId, delegateAgentId, entity, eventType, action) {
18034
+ const metadata = chatMetadataSchema$1.parse({
18035
+ source: "github",
18036
+ entityType: entity.type,
18037
+ entityKey: entity.key,
18038
+ ...entity.url ? { entityUrl: entity.url } : {}
18179
18039
  });
18040
+ return { id: (await createChat(db, humanAgentId, {
18041
+ type: "direct",
18042
+ participantIds: [delegateAgentId],
18043
+ topic: formatEntityTitle(entity, eventType, action),
18044
+ metadata
18045
+ })).id };
18180
18046
  }
18181
18047
  const log$1 = createLogger$1("GithubWebhook");
18182
- const GITHUB_ADAPTER_ID = "github-adapter";
18183
18048
  function verifySignature(secret, rawBody, signatureHeader) {
18184
18049
  const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`;
18185
18050
  const expectedBuf = Buffer.from(expected, "utf8");
18186
18051
  const receivedBuf = Buffer.from(signatureHeader, "utf8");
18187
18052
  if (expectedBuf.length !== receivedBuf.length || !timingSafeEqual(expectedBuf, receivedBuf)) throw new UnauthorizedError("Invalid webhook signature");
18188
18053
  }
18189
- async function ensureGitHubAdapterAgent(db, organizationId) {
18190
- const [existing] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, organizationId), eq(agents.name, GITHUB_ADAPTER_ID))).limit(1);
18191
- if (existing) return existing.uuid;
18192
- try {
18193
- return (await createAgent(db, {
18194
- name: GITHUB_ADAPTER_ID,
18195
- type: "autonomous_agent",
18196
- displayName: "GitHub Adapter",
18197
- organizationId,
18198
- metadata: {
18199
- source: "github",
18200
- managed: true
18201
- }
18202
- })).uuid;
18203
- } catch (err) {
18204
- if (err instanceof ConflictError) {
18205
- const [created] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, organizationId), eq(agents.name, GITHUB_ADAPTER_ID))).limit(1);
18206
- if (created) return created.uuid;
18207
- }
18208
- throw err;
18209
- }
18210
- }
18211
- async function findTargetAgent(db, organizationId, repoFullName) {
18212
- const allAgents = await db.select({
18213
- id: agents.uuid,
18214
- name: agents.name,
18215
- metadata: agents.metadata,
18216
- type: agents.type
18217
- }).from(agents).where(and(eq(agents.organizationId, organizationId), eq(agents.status, "active")));
18218
- for (const agent of allAgents) {
18219
- if (agent.name === GITHUB_ADAPTER_ID) continue;
18220
- const meta = agent.metadata;
18221
- if (meta && typeof meta === "object" && "github" in meta) {
18222
- const github = meta.github;
18223
- if (isRecord(github) && "repos" in github) {
18224
- const repos = github.repos;
18225
- if (Array.isArray(repos) && repos.includes(repoFullName)) return agent.id;
18226
- }
18227
- }
18228
- }
18229
- return null;
18230
- }
18231
- function isRecord(value) {
18232
- return typeof value === "object" && value !== null && !Array.isArray(value);
18233
- }
18234
18054
  /** Extract unique @mentions from text. Returns lowercase usernames.
18235
18055
  * Excludes email patterns (user@example.com) and team mentions (@org/team). */
18236
18056
  function extractMentions$1(text) {
@@ -18270,10 +18090,18 @@ function evaluateDelegateTarget(target, sourceOrgId) {
18270
18090
  }
18271
18091
  /**
18272
18092
  * Route @mentions to delegate agents.
18273
- * For each mentioned user who has delegate_mention configured,
18274
- * send a card message from the mentioned user to their delegate.
18093
+ *
18094
+ * For each mentioned GitHub user who maps to an agent with `delegate_mention`
18095
+ * configured, resolve which chat the event belongs to (via §4.4's
18096
+ * entity-clustering rules) and post a card from the human-bound agent to its
18097
+ * delegate.
18098
+ *
18099
+ * The entity argument is the §4.2 entity for the current event; `relatedRefs`
18100
+ * is the parsed `Fixes #N` list (empty for non-PR events). Both are
18101
+ * pre-computed by the caller so the heavy parsing doesn't run once per
18102
+ * mention.
18275
18103
  */
18276
- async function routeMentionDelegations(app, organizationId, mentionedNames, ctx) {
18104
+ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx, entity, relatedRefs) {
18277
18105
  if (mentionedNames.length === 0) return 0;
18278
18106
  const delegates = await app.db.select({
18279
18107
  id: agents.uuid,
@@ -18302,8 +18130,24 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
18302
18130
  continue;
18303
18131
  }
18304
18132
  try {
18305
- const chat = await findOrCreateDirectChat(app.db, agent.id, agent.delegateMention);
18306
- const { message: msg, recipients } = await sendMessage(app.db, chat.id, agent.id, {
18133
+ const resolved = await resolveTargetChat(app.db, {
18134
+ organizationId,
18135
+ humanAgentId: agent.id,
18136
+ delegateAgentId: agent.delegateMention,
18137
+ entity,
18138
+ relatedEntities: relatedRefs,
18139
+ eventType: ctx.event,
18140
+ action: ctx.action ?? ""
18141
+ });
18142
+ log$1.info({
18143
+ chatId: resolved.chatId,
18144
+ entityType: entity.type,
18145
+ entityKey: entity.key,
18146
+ boundVia: resolved.boundVia,
18147
+ created: resolved.created,
18148
+ humanAgent: agent.name
18149
+ }, "resolved entity chat");
18150
+ const { message: msg, recipients } = await sendMessage(app.db, resolved.chatId, agent.id, {
18307
18151
  format: "card",
18308
18152
  content: {
18309
18153
  type: "github_mention",
@@ -18314,13 +18158,20 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
18314
18158
  sender: ctx.sender,
18315
18159
  title: ctx.title,
18316
18160
  body: ctx.body,
18317
- url: ctx.url
18161
+ url: ctx.url,
18162
+ entity: {
18163
+ type: entity.type,
18164
+ key: entity.key,
18165
+ url: entity.url ?? null
18166
+ }
18318
18167
  },
18319
18168
  metadata: {
18320
18169
  source: "github",
18321
18170
  event: "mention_delegation",
18322
18171
  mentionedUser: agent.name,
18323
- action: ctx.action
18172
+ action: ctx.action,
18173
+ entityType: entity.type,
18174
+ entityKey: entity.key
18324
18175
  }
18325
18176
  });
18326
18177
  notifyRecipients(app.notifier, recipients, msg.id);
@@ -18335,43 +18186,6 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
18335
18186
  }
18336
18187
  return routed;
18337
18188
  }
18338
- function parseIssuesPayload(body) {
18339
- if (!isRecord(body)) throw new BadRequestError("Invalid payload: expected object");
18340
- if (typeof body.action !== "string") throw new BadRequestError("Invalid payload: missing action");
18341
- if (!isRecord(body.issue)) throw new BadRequestError("Invalid payload: missing issue");
18342
- if (!isRecord(body.repository)) throw new BadRequestError("Invalid payload: missing repository");
18343
- if (!isRecord(body.sender)) throw new BadRequestError("Invalid payload: missing sender");
18344
- const issue = body.issue;
18345
- const labels = Array.isArray(issue.labels) ? issue.labels.filter((l) => isRecord(l) && typeof l.name === "string") : [];
18346
- return {
18347
- action: body.action,
18348
- issue: {
18349
- number: typeof issue.number === "number" ? issue.number : 0,
18350
- title: typeof issue.title === "string" ? issue.title : "",
18351
- body: typeof issue.body === "string" ? issue.body : null,
18352
- html_url: typeof issue.html_url === "string" ? issue.html_url : "",
18353
- labels,
18354
- state: typeof issue.state === "string" ? issue.state : "open"
18355
- },
18356
- repository: { full_name: typeof body.repository.full_name === "string" ? body.repository.full_name : "" },
18357
- sender: { login: typeof body.sender.login === "string" ? body.sender.login : "" }
18358
- };
18359
- }
18360
- function parseIssueCommentPayload(body) {
18361
- const base = parseIssuesPayload(body);
18362
- if (!isRecord(body)) throw new BadRequestError("Invalid payload: expected object");
18363
- if (!isRecord(body.comment)) throw new BadRequestError("Invalid payload: missing comment");
18364
- const comment = body.comment;
18365
- const commentUser = isRecord(comment.user) ? comment.user : { login: "" };
18366
- return {
18367
- ...base,
18368
- comment: {
18369
- body: typeof comment.body === "string" ? comment.body : "",
18370
- html_url: typeof comment.html_url === "string" ? comment.html_url : "",
18371
- user: { login: typeof commentUser.login === "string" ? commentUser.login : "" }
18372
- }
18373
- };
18374
- }
18375
18189
  async function githubWebhookRoutes(app) {
18376
18190
  app.addContentTypeParser("application/json", { parseAs: "buffer" }, (_request, body, done) => {
18377
18191
  done(null, body);
@@ -18401,6 +18215,11 @@ async function githubWebhookRoutes(app) {
18401
18215
  ok: true,
18402
18216
  event: "ping"
18403
18217
  });
18218
+ if (shouldSilent(eventType, payload)) return reply.status(200).send({
18219
+ ok: true,
18220
+ event: eventType,
18221
+ silent: true
18222
+ });
18404
18223
  const deliveryHeader = request.headers["x-github-delivery"];
18405
18224
  const deliveryId = typeof deliveryHeader === "string" && deliveryHeader.length > 0 ? deliveryHeader : null;
18406
18225
  if (deliveryId) {
@@ -18417,16 +18236,17 @@ async function githubWebhookRoutes(app) {
18417
18236
  }
18418
18237
  }
18419
18238
  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
18239
  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);
18240
+ const allowedActions = MENTION_ACTIONS[eventType];
18241
+ if (!allowedActions || !action || !allowedActions.includes(action)) return reply.status(200).send({
18242
+ ok: true,
18243
+ event: eventType,
18244
+ handled: false
18245
+ });
18246
+ const mentionsRouted = await handleMentionDelegation(app, orgId, eventType, payload);
18426
18247
  return reply.status(200).send({
18427
18248
  ok: true,
18428
18249
  event: eventType,
18429
- handled: mentionsRouted > 0,
18430
18250
  mentionsRouted
18431
18251
  });
18432
18252
  } catch (err) {
@@ -18598,9 +18418,15 @@ async function handleMentionDelegation(app, organizationId, eventType, payload)
18598
18418
  const textMentions = extractMentions$1(extractEventText(eventType, payload));
18599
18419
  const structuralMentions = extractStructuralMentions(eventType, payload);
18600
18420
  const mentions = [...new Set([...textMentions, ...structuralMentions])];
18601
- const mentionCtx = extractEventContext(eventType, payload);
18602
- if (mentions.length > 0 && mentionCtx) return routeMentionDelegations(app, organizationId, mentions, mentionCtx);
18603
- return 0;
18421
+ if (mentions.length === 0) return 0;
18422
+ const ctx = extractEventContext(eventType, payload);
18423
+ if (!ctx) return 0;
18424
+ const entity = extractEventEntity(eventType, payload);
18425
+ if (!entity) {
18426
+ log$1.warn({ eventType }, "mention extracted but no entity resolvable; skipping fan-out");
18427
+ return 0;
18428
+ }
18429
+ return routeMentionDelegations(app, organizationId, mentions, ctx, entity, eventType === "pull_request" && ctx.repository.length > 0 ? parseFixesRefs(ctx.body, ctx.repository) : []);
18604
18430
  }
18605
18431
  /** Actions that represent new/changed content (worth scanning for @mentions).
18606
18432
  * Note: `pull_request.review_requested` doesn't carry an @mention in any
@@ -18621,124 +18447,6 @@ const MENTION_ACTIONS = {
18621
18447
  discussion_comment: ["created"],
18622
18448
  commit_comment: ["created"]
18623
18449
  };
18624
- async function handleIssuesEvent(app, organizationId, eventType, payload, reply) {
18625
- const data = parseIssuesPayload(payload);
18626
- if (MENTION_ACTIONS.issues?.includes(data.action)) await handleMentionDelegation(app, organizationId, eventType, payload);
18627
- if (![
18628
- "opened",
18629
- "edited",
18630
- "labeled"
18631
- ].includes(data.action)) return reply.status(200).send({
18632
- ok: true,
18633
- event: "issues",
18634
- action: data.action,
18635
- handled: false
18636
- });
18637
- const [senderId, targetAgentId] = await Promise.all([ensureGitHubAdapterAgent(app.db, organizationId), findTargetAgent(app.db, organizationId, data.repository.full_name)]);
18638
- if (!targetAgentId) {
18639
- log$1.warn({
18640
- repo: data.repository.full_name,
18641
- event: "issue"
18642
- }, "no target agent found for GitHub event");
18643
- return reply.status(200).send({
18644
- ok: true,
18645
- event: "issues",
18646
- action: data.action,
18647
- routed: false
18648
- });
18649
- }
18650
- const content = {
18651
- type: "github_issue",
18652
- action: data.action,
18653
- issue: {
18654
- number: data.issue.number,
18655
- title: data.issue.title,
18656
- body: data.issue.body,
18657
- url: data.issue.html_url,
18658
- labels: data.issue.labels.map((l) => l.name),
18659
- state: data.issue.state
18660
- },
18661
- repository: data.repository.full_name,
18662
- sender: data.sender.login
18663
- };
18664
- const metadata = {
18665
- source: "github",
18666
- event: "issues",
18667
- action: data.action
18668
- };
18669
- const chat = await findOrCreateDirectChat(app.db, senderId, targetAgentId);
18670
- const { message: msg, recipients } = await sendMessage(app.db, chat.id, senderId, {
18671
- format: "card",
18672
- content,
18673
- metadata
18674
- });
18675
- notifyRecipients(app.notifier, recipients, msg.id);
18676
- return reply.status(200).send({
18677
- ok: true,
18678
- event: "issues",
18679
- action: data.action,
18680
- routed: true
18681
- });
18682
- }
18683
- async function handleIssueCommentEvent(app, organizationId, eventType, payload, reply) {
18684
- const data = parseIssueCommentPayload(payload);
18685
- if (MENTION_ACTIONS.issue_comment?.includes(data.action)) await handleMentionDelegation(app, organizationId, eventType, payload);
18686
- if (data.action !== "created") return reply.status(200).send({
18687
- ok: true,
18688
- event: "issue_comment",
18689
- action: data.action,
18690
- handled: false
18691
- });
18692
- const [senderId, targetAgentId] = await Promise.all([ensureGitHubAdapterAgent(app.db, organizationId), findTargetAgent(app.db, organizationId, data.repository.full_name)]);
18693
- if (!targetAgentId) {
18694
- log$1.warn({
18695
- repo: data.repository.full_name,
18696
- event: "issue_comment"
18697
- }, "no target agent found for GitHub event");
18698
- return reply.status(200).send({
18699
- ok: true,
18700
- event: "issue_comment",
18701
- action: data.action,
18702
- routed: false
18703
- });
18704
- }
18705
- const content = {
18706
- type: "github_issue_comment",
18707
- action: data.action,
18708
- issue: {
18709
- number: data.issue.number,
18710
- title: data.issue.title,
18711
- url: data.issue.html_url,
18712
- labels: data.issue.labels.map((l) => l.name),
18713
- state: data.issue.state
18714
- },
18715
- comment: {
18716
- body: data.comment.body,
18717
- url: data.comment.html_url,
18718
- author: data.comment.user.login
18719
- },
18720
- repository: data.repository.full_name,
18721
- sender: data.sender.login
18722
- };
18723
- const metadata = {
18724
- source: "github",
18725
- event: "issue_comment",
18726
- action: data.action
18727
- };
18728
- const chat = await findOrCreateDirectChat(app.db, senderId, targetAgentId);
18729
- const { message: msg, recipients } = await sendMessage(app.db, chat.id, senderId, {
18730
- format: "card",
18731
- content,
18732
- metadata
18733
- });
18734
- notifyRecipients(app.notifier, recipients, msg.id);
18735
- return reply.status(200).send({
18736
- ok: true,
18737
- event: "issue_comment",
18738
- action: data.action,
18739
- routed: true
18740
- });
18741
- }
18742
18450
  var schema_exports = /* @__PURE__ */ __exportAll({
18743
18451
  adapterAgentMappings: () => adapterAgentMappings,
18744
18452
  adapterChatMappings: () => adapterChatMappings,
@@ -18753,6 +18461,7 @@ var schema_exports = /* @__PURE__ */ __exportAll({
18753
18461
  chatSubscriptions: () => chatSubscriptions,
18754
18462
  chats: () => chats,
18755
18463
  clients: () => clients,
18464
+ githubEntityChatMappings: () => githubEntityChatMappings,
18756
18465
  inboxEntries: () => inboxEntries,
18757
18466
  invitationRedemptions: () => invitationRedemptions,
18758
18467
  invitations: () => invitations,
@@ -18764,8 +18473,6 @@ var schema_exports = /* @__PURE__ */ __exportAll({
18764
18473
  pendingQuestions: () => pendingQuestions,
18765
18474
  serverInstances: () => serverInstances,
18766
18475
  sessionEvents: () => sessionEvents,
18767
- taskChats: () => taskChats,
18768
- tasks: () => tasks,
18769
18476
  users: () => users
18770
18477
  });
18771
18478
  function connectDatabase(url) {
@@ -20258,7 +19965,6 @@ async function buildApp(config) {
20258
19965
  await scope.register(orgAdapterStatusRoutes, { prefix: "/adapters/status" });
20259
19966
  await scope.register(orgOverviewRoutes, { prefix: "/overview" });
20260
19967
  await scope.register(orgActivityRoutes, { prefix: "/activity" });
20261
- await scope.register(orgTaskRoutes, { prefix: "/tasks" });
20262
19968
  await scope.register(orgSessionRoutes, { prefix: "/sessions" });
20263
19969
  await scope.register(orgNotificationRoutes, { prefix: "/notifications" });
20264
19970
  await scope.register(orgClientRoutes, { prefix: "/clients" });
@@ -20274,7 +19980,6 @@ async function buildApp(config) {
20274
19980
  await scope.register(agentActivityRoutes, { prefix: "/agents" });
20275
19981
  await scope.register(sessionRoutes, { prefix: "/agents" });
20276
19982
  await scope.register(chatRoutes, { prefix: "/chats" });
20277
- await scope.register(taskRoutes, { prefix: "/tasks" });
20278
19983
  await scope.register(adapterRoutes, { prefix: "/adapters" });
20279
19984
  await scope.register(adapterMappingRoutes, { prefix: "/adapter-mappings" });
20280
19985
  await scope.register(clientRoutes, { prefix: "/clients" });
@@ -20286,7 +19991,6 @@ async function buildApp(config) {
20286
19991
  await scope.register(agentSendToAgentRoutes, { prefix: "/agents" });
20287
19992
  await scope.register(agentInboxRoutes, { prefix: "/inbox" });
20288
19993
  await scope.register(agentConfigRoutes$1);
20289
- await scope.register(agentTaskRoutes, { prefix: "/tasks" });
20290
19994
  await scope.register(agentFeishuBotRoutes);
20291
19995
  await scope.register(agentFeishuUserRoutes, { prefix: "/delegated" });
20292
19996
  }), { prefix: "/agent" });
@@ -20897,4 +20601,4 @@ function registerSaaSConnectCommand(program) {
20897
20601
  });
20898
20602
  }
20899
20603
  //#endregion
20900
- export { formatStaleReason as $, checkDocker as A, isServiceSupported as B, createApiNameResolver as C, checkBackgroundService as D, checkAgentConfigs as E, checkWebSocket as F, uninstallClientService as G, restartClientService as H, printResults as I, stopPostgres as J, ensurePostgres as K, reconcileAgentConfigs as L, checkServerConfig as M, checkServerHealth as N, checkClientConfig as O, checkServerReachable as P, findStaleAliases as Q, getClientServiceStatus as R, runHomeMigration as S, runMigrations as T, startClientService as U, resolveCliInvocation as V, stopClientService as W, handleClientOrgMismatch as X, ClientRuntime as Y, rotateClientIdWithBackup as Z, formatCheckReport as _, declineUpdate as a, success as at, onboardCreate as b, detectInstallMode as c, FirstTreeHubSDK as ct, startServer as d, cleanWorkspaces as dt, removeLocalAgent as et, reconcileLocalRuntimeProviders as f, probeCapabilities as ft, promptMissingFields as g, promptAddAgent as h, createExecuteUpdate as i, fail as it, checkNodeVersion as j, checkDatabase as k, fetchLatestVersion as l, SdkError as lt, isInteractive as m, configureClientLoggerForService as mt, deriveHubUrlFromToken as n, hasUser as nt, promptUpdate as o, ClientOrgMismatchError as ot, uploadClientCapabilities as p, applyClientLoggerConfig as pt, isDockerAvailable as q, registerSaaSConnectCommand as r, resolveReplyToFromEnv as rt, PACKAGE_NAME as s, ClientUserMismatchError as st, HubUrlDerivationError as t, createOwner as tt, installGlobalLatest as u, SessionRegistry as ut, loadOnboardState as v, migrateLocalAgentDirs as w, saveOnboardState as x, onboardCheck as y, installClientService as z };
20604
+ export { formatStaleReason as $, checkDocker as A, isServiceSupported as B, createApiNameResolver as C, checkBackgroundService as D, checkAgentConfigs as E, checkWebSocket as F, uninstallClientService as G, restartClientService as H, printResults as I, stopPostgres as J, ensurePostgres as K, reconcileAgentConfigs as L, checkServerConfig as M, checkServerHealth as N, checkClientConfig as O, checkServerReachable as P, findStaleAliases as Q, getClientServiceStatus as R, runHomeMigration as S, runMigrations as T, startClientService as U, resolveCliInvocation as V, stopClientService as W, handleClientOrgMismatch as X, ClientRuntime as Y, rotateClientIdWithBackup as Z, formatCheckReport as _, declineUpdate as a, fail as at, onboardCreate as b, detectInstallMode as c, ClientUserMismatchError as ct, startServer as d, SessionRegistry as dt, removeLocalAgent as et, reconcileLocalRuntimeProviders as f, cleanWorkspaces as ft, promptMissingFields as g, promptAddAgent as h, configureClientLoggerForService as ht, createExecuteUpdate as i, resolveSenderName as it, checkNodeVersion as j, checkDatabase as k, fetchLatestVersion as l, FirstTreeHubSDK as lt, isInteractive as m, applyClientLoggerConfig as mt, deriveHubUrlFromToken as n, hasUser as nt, promptUpdate as o, success as ot, uploadClientCapabilities as p, probeCapabilities as pt, isDockerAvailable as q, registerSaaSConnectCommand as r, resolveReplyToFromEnv as rt, PACKAGE_NAME as s, ClientOrgMismatchError as st, HubUrlDerivationError as t, createOwner as tt, installGlobalLatest as u, SdkError as ut, loadOnboardState as v, migrateLocalAgentDirs as w, saveOnboardState as x, onboardCheck as y, installClientService as z };