import { loadRepoById } from "../repos"; import type { WebhookEvent } from "../events/emit"; import { repoHub } from "./hub-do"; import { toJsonText } from "./protocol"; const MAX_TEXT = 200; const MAX_COMMITS = 20; /** Event types agents can subscribe to: webhook events plus coordination changes. */ export const AGENT_EVENT_TYPES = [ "push", "create", "delete", "issues", "issue_comment", "pull_request", "pull_request_review", "pull_request_review_comment", "release", "workflow_run", "claims", ] as const; function pick(source: unknown, keys: string[]): Record { if (typeof source !== "object" || source === null) return {}; const out: Record = {}; for (const key of keys) { const value: unknown = Reflect.get(source, key); if (typeof value === "string") out[key] = value.slice(0, MAX_TEXT); else if (typeof value === "number" || typeof value === "boolean" || value === null) out[key] = value; } return out; } /** A bounded summary of a webhook payload: enough to react to, never whole bodies or diffs. */ export function summarizeEvent(event: WebhookEvent, payload: Record): Record { const sender = pick(payload.sender, ["login"]); const base = { action: typeof payload.action === "string" ? payload.action : null, sender: sender.login ?? null }; switch (event) { case "push": { const commits = Array.isArray(payload.commits) ? payload.commits.slice(0, MAX_COMMITS) : []; return { ...base, ...pick(payload, ["ref", "before", "after", "created", "deleted"]), commits: commits.map((commit: unknown) => pick(commit, ["id", "message"])), }; } case "pull_request": return { ...base, pull_request: pick(payload.pull_request, ["number", "title", "state", "merged", "html_url"]) }; case "pull_request_review": return { ...base, review: pick(payload.review, ["state", "body", "commit_id"]), pull_request: pick(payload.pull_request, ["number", "html_url"]), }; case "workflow_run": return { ...base, workflow_run: pick(payload.workflow_run, [ "run_number", "name", "event", "head_sha", "head_branch", "status", "conclusion", "html_url", ]), }; case "issues": case "issue_comment": return { ...base, issue: pick(payload.issue, ["number", "title", "html_url"]), comment: pick(payload.comment, ["body"]), }; default: return { ...base, ...pick(payload, ["ref", "ref_type"]) }; } } /** Queue-consumer hook: forwards a repository's webhook event to its MCP coordination hub. */ export async function publishRepoEvent( repoId: string, event: WebhookEvent, payload: Record, ): Promise { if (!(AGENT_EVENT_TYPES as readonly string[]).includes(event)) return; const repo = await loadRepoById(repoId); if (!repo) return; try { await repoHub(repo.id).publish( repo.id, `${repo.owner_name}/${repo.name}`, event, toJsonText(summarizeEvent(event, payload)), ); } catch (error) { console.error("MCP event fan-out failed", error); } }