import { App } from "@slack/bolt"; import { canActOnTask, canCreateTask, createSlackAccessPolicy, isDirectMessage, isUserAllowed, type SlackAccessPolicy, stripBotMention, } from "./access"; import { taskCorrelationId } from "./actors"; import { FrontalCodeApiClient } from "./api-client"; import { OcpBridge, type SlackUpdateClient } from "./bridge"; import { config } from "./config"; import { logger } from "./log"; import { createOcpStore, type OcpStoreHandle } from "./store"; import type { FrontalCodeAppliedOrphanPolicy, FrontalCodeApprovalAction, FrontalCodeCreateTaskResponse, FrontalCodeEventEnvelope, FrontalCodeEventPayload, FrontalCodeEventTaskSummary, FrontalCodeOrphanPolicyQuery, FrontalCodeOrphanPolicyResponse, FrontalCodeTask, FrontalCodeTrackedTask, SlackBlock, SlackBody, } from "./types"; import { FrontalCodeEventsClient } from "./websocket"; // Type definitions for Slack Bolt handlers interface SlackMessage { text: string; user: string; channel: string; ts: string; thread_ts?: string; bot_id?: string; subtype?: string; channel_type?: string; } interface SlackAction { action_id: string; value?: string; action_ts?: string; } interface SlackCommand { text: string; user_id: string; channel_id: string; trigger_id?: string; } export interface SlackInterfaceDeps { // Event store for the OCP host; defaults to Postgres at OCP_DATABASE_URL. ocpStore?: OcpStoreHandle; } interface SlackMessagePayload { text: string; blocks?: SlackBlock[]; } // Slack interface: Socket Mode connection to Slack, hosted as an OpenChannel Protocol agent that // talks to the Frontal Code control plane. Slack requests and bot messages flow through `ocp`. export class SlackInterface { private readonly app: App; private readonly ocp: OcpBridge; private readonly frontalCodeApi: FrontalCodeApiClient; private readonly frontalCodeEvents: FrontalCodeEventsClient; private readonly trackedTasks = new Map(); private readonly approvalMessageTsByTask = new Map(); private readonly approvalInFlight = new Set(); private readonly approvalResolved = new Set(); private readonly access: SlackAccessPolicy; constructor(deps: SlackInterfaceDeps = {}) { // Initialize Slack WebSocket connection this.app = new App({ token: config.slack.botToken, appToken: config.slack.appToken, signingSecret: config.slack.signingSecret, socketMode: true, // WebSocket connection logLevel: undefined, // Minimal logging }); this.access = createSlackAccessPolicy({ allowedUserIds: config.slack.allowedUserIds, allowedChannelIds: config.slack.allowedChannelIds, adminUserIds: config.slack.adminUserIds, }); this.frontalCodeApi = new FrontalCodeApiClient(); this.ocp = new OcpBridge({ agentId: config.ocp.agentId, api: this.frontalCodeApi, slack: this.app.client as unknown as SlackUpdateClient, store: deps.ocpStore ?? createOcpStore({ databaseUrl: config.ocp.databaseUrl, tableName: config.ocp.eventsTable, }), runMigrations: config.ocp.runMigrations, logger, onTaskCreated: (task, routing, prompt) => this.registerTask(task, routing, prompt), }); this.frontalCodeEvents = new FrontalCodeEventsClient( (query) => this.frontalCodeApi.getEventsWebSocketUrl(query), () => this.frontalCodeApi.getEventsWebSocketHeaders(), ); this.frontalCodeEvents.onTrackedTaskEvent(async (event, task) => { await this.handleFrontalCodeTaskEvent(event, task); }); this.setupEventHandlers(); } private setupEventHandlers(): void { // Direct messages are the only non-mention path that can start a task. Channel and group // traffic is ignored here; channels are handled through explicit @mentions (app_mention). // Allowed DMs go through the `@openchannelprotocol/sdk/slack` normalizer, which dedupes Slack retries by event_id. this.app.message(async ({ message, body }) => { const slackMessage = message as SlackMessage; if (!slackMessage.text || !slackMessage.user || slackMessage.bot_id || slackMessage.subtype) { return; } if (!isDirectMessage(slackMessage.channel, slackMessage.channel_type)) { return; } if (!this.canStartTaskFrom(slackMessage)) { return; } // Bolt always passes the event_callback envelope; without one, wrap the bare event. await this.ocp.receiveSlackEvent(body ?? { event: { type: "message", ...slackMessage } }); }); // Channel tasks start only when the bot is explicitly mentioned. The chain continues so the // generic connector event forwarder below still sees the mention. this.app.event("app_mention", async ({ event, body, next }) => { const mention = event as unknown as SlackMessage; if (mention.user && mention.channel) { const prompt = stripBotMention(mention.text || ""); if (prompt && this.canStartTaskFrom(mention)) { await this.ocp.receiveSlackPrompt({ eventId: (body as { event_id?: string } | undefined)?.event_id, userId: mention.user, channelId: mention.channel, threadTs: mention.thread_ts || mention.ts, text: prompt, }); } } await next(); }); // Handle slash commands this.app.command("/code", async ({ command: rawCommand, ack }) => { const command = rawCommand as SlackCommand; if ( !canCreateTask(this.access, { userId: command.user_id, channelId: command.channel_id, }) ) { logger.info("Denied /code command", { userId: command.user_id, channelId: command.channel_id, }); await ack({ response_type: "ephemeral", text: "You are not authorized to run Code tasks in this channel.", }); return; } const policyQuery = this.parseOrphanPolicyCommand(command.text); if (policyQuery) { try { const policy = await this.frontalCodeApi.getOrphanPolicy(policyQuery); await ack(this.buildOrphanPolicyCommandResponse(policy)); } catch (error) { await ack({ response_type: "ephemeral", text: `Failed to load orphan policy: ${(error as Error).message}`, }); } return; } if (!command.text.trim()) { await ack({ response_type: "ephemeral", text: "Usage: /code " }); return; } await ack(); await this.ocp.receiveSlackPrompt({ eventId: command.trigger_id ? `slack-command:${command.trigger_id}` : undefined, userId: command.user_id, channelId: command.channel_id, text: command.text, }); }); // Handle interactions (buttons, modals, etc.) this.app.action({ action_id: /^.*$/ }, async ({ action, ack, body }) => { await ack(); await this.handleSlackAction(action as SlackAction, body as unknown as SlackBody); }); // Handle events (user joins, reactions, etc.) this.app.event(/.*/, async ({ event }) => { // Forward events to Frontal Code AI for processing const slackEvent = event as { type: string; user?: string }; await this.frontalCodeApi.sendConnectorEvent("slack", { type: slackEvent.type, userId: slackEvent.user || "", data: event, }); }); } // Start WebSocket connection to Slack async connect(): Promise { try { // Postgres is required: fail fast before Slack starts delivering events. await this.ocp.start(); await this.syncTrackedTasksFromFrontalCode(); await this.app.start(); await this.frontalCodeEvents.connect(); logger.info("Slack WebSocket interface connected"); } catch (error) { logger.error("Failed to connect to Slack", error as Error); throw error; } } // Disconnect WebSocket async disconnect(): Promise { try { await this.frontalCodeEvents.disconnect(); await this.app.stop(); await this.ocp.stop(); logger.info("Slack WebSocket interface disconnected"); } catch (error) { logger.error("Failed to disconnect from Slack", error as Error); throw error; } } // Health check async healthCheck(): Promise<{ slack: boolean; frontal_code: boolean; ocp: boolean }> { try { const slackConnected = (this.app as unknown as { isListening?: () => boolean }).isListening?.() || false; const [frontalCodeConnected, ocpStoreReachable] = await Promise.all([ this.frontalCodeApi.healthCheck(), this.ocp.ping(), ]); return { slack: slackConnected, frontal_code: frontalCodeConnected, ocp: ocpStoreReachable, }; } catch (error) { logger.error("Health check failed", error as Error); return { slack: false, frontal_code: false, ocp: false, }; } } private canStartTaskFrom(message: SlackMessage): boolean { const allowed = canCreateTask(this.access, { userId: message.user, channelId: message.channel, channelType: message.channel_type, }); if (!allowed) { logger.info("Ignored Slack task request from unauthorized user or channel", { userId: message.user, channelId: message.channel, }); } return allowed; } private async registerTask( task: FrontalCodeCreateTaskResponse, trackedTask: FrontalCodeTrackedTask, prompt: string, ): Promise { const message = this.formatTaskCreatedMessage(task, prompt); const response = await this.ocp.sendToThread({ channelId: trackedTask.channelId, threadTs: trackedTask.threadTs, text: message, correlationId: taskCorrelationId(trackedTask.taskId), }); const nextTask = { ...trackedTask, threadTs: trackedTask.threadTs || response.ts, }; this.upsertTrackedTask(nextTask); try { await this.frontalCodeApi.updateTaskContext({ taskId: nextTask.taskId, source: "slack", user_id: nextTask.userId, channel_id: nextTask.channelId, thread_ts: nextTask.threadTs, }); } catch (error) { logger.warn("Failed to persist Slack task thread anchor to Frontal Code", { taskId: nextTask.taskId, error: (error as Error).message, }); } this.frontalCodeEvents.trackTask(nextTask); } private async handleSlackAction(action: SlackAction, body: SlackBody): Promise { if (this.isOrphanApprovalAction(action)) { await this.handleOrphanApprovalAction(action, body); return; } if (this.isGithubFollowupApprovalAction(action)) { await this.handleGithubFollowupApprovalAction(action, body); return; } if (!isUserAllowed(this.access, body.user.id)) { logger.info("Ignored Slack action from unauthorized user", { userId: body.user.id, action: action.action_id, }); return; } const response = await this.frontalCodeApi.sendConnectorInteraction("slack", { action: action.action_id, value: action.value, userId: body.user.id, context: body, }); await this.ocp.updateMessage({ channelId: body.channel.id, ts: body.message.ts, blocks: response.blocks, }); } private isOrphanApprovalAction(action: SlackAction): boolean { return ( Boolean(action.value) && (action.action_id === "orphaned_hosted_agent.retry" || action.action_id === "orphaned_hosted_agent.cancel") ); } private isGithubFollowupApprovalAction(action: SlackAction): boolean { return ( Boolean(action.value) && (action.action_id === "github_review_followup.ack" || action.action_id === "github_review_followup.retry") ); } // Only the task's creator or an admin, and only when allowlisted, may resolve its approvals. private async canResolveTaskApproval(taskId: string, actorId: string): Promise { if (!isUserAllowed(this.access, actorId)) { return false; } let ownerId = this.trackedTasks.get(taskId)?.userId; if (!ownerId && !this.access.adminUserIds.has(actorId)) { try { ownerId = (await this.frontalCodeApi.getTask(taskId))?.user_id; } catch (error) { logger.warn("Failed to load task owner for approval check", { taskId, error: (error as Error).message, }); return false; } } return canActOnTask(this.access, { actorId, ownerId }); } private async denyApprovalAction(body: SlackBody, taskId: string): Promise { logger.info("Denied Slack approval action", { taskId, userId: body.user.id }); try { await this.app.client.chat.postEphemeral({ channel: body.channel.id, user: body.user.id, text: `You are not authorized to act on task ${taskId}. Only the task creator or a Code admin can.`, }); } catch (error) { logger.warn("Failed to send approval denial notice", { taskId, error: (error as Error).message, }); } } private async handleOrphanApprovalAction(action: SlackAction, body: SlackBody): Promise { const taskId = action.value; if (!taskId) { return; } if (!(await this.canResolveTaskApproval(taskId, body.user.id))) { await this.denyApprovalAction(body, taskId); return; } const actionName: FrontalCodeApprovalAction = action.action_id.endsWith(".retry") ? "retry" : "cancel"; const approvalTs = this.approvalMessageTsByTask.get(taskId) || body.message.ts; if (this.approvalResolved.has(taskId)) { await this.updateApprovalMessage( body.channel.id, approvalTs, `Approval for task ${taskId} was already resolved.`, this.buildApprovalResolvedBlocks(taskId, "already_resolved"), ); return; } if (this.approvalInFlight.has(taskId)) { await this.updateApprovalMessage( body.channel.id, approvalTs, `Approval for task ${taskId} is already being processed.`, this.buildApprovalProcessingBlocks(taskId, actionName), ); return; } this.approvalInFlight.add(taskId); this.approvalMessageTsByTask.set(taskId, approvalTs); await this.updateApprovalMessage( body.channel.id, approvalTs, `Processing approval for task ${taskId}: ${actionName}.`, this.buildApprovalProcessingBlocks(taskId, actionName), ); try { const task = await this.ocp.resolveApproval({ eventId: approvalEventId(action, body), taskId, approvalKind: "orphaned_hosted_agent", action: actionName, slackUserId: body.user.id, resolvedBy: body.user.id, reason: "resolved from Slack approval action", channelId: body.channel.id, threadTs: this.trackedTasks.get(taskId)?.threadTs, }); this.approvalInFlight.delete(taskId); this.approvalResolved.add(taskId); await this.updateApprovalMessage( body.channel.id, approvalTs, `Approval resolved for task ${task.task_id}: ${actionName}.`, this.buildApprovalResolvedBlocks( task.task_id, actionName, task.worker_status, task.worker_id, ), ); } catch (error) { this.approvalInFlight.delete(taskId); await this.updateApprovalMessage( body.channel.id, approvalTs, `Approval failed for task ${taskId}.`, this.buildApprovalErrorBlocks(taskId, error as Error), ); throw error; } } private async handleGithubFollowupApprovalAction( action: SlackAction, body: SlackBody, ): Promise { const taskId = action.value; if (!taskId) { return; } if (!(await this.canResolveTaskApproval(taskId, body.user.id))) { await this.denyApprovalAction(body, taskId); return; } const actionName: FrontalCodeApprovalAction = action.action_id.endsWith(".retry") ? "retry" : "ack"; const approvalTs = this.approvalMessageTsByTask.get(taskId) || body.message.ts; if (this.approvalResolved.has(taskId)) { await this.updateApprovalMessage( body.channel.id, approvalTs, `Follow-up for task ${taskId} was already resolved.`, this.buildApprovalResolvedBlocks(taskId, "already_resolved"), ); return; } if (this.approvalInFlight.has(taskId)) { await this.updateApprovalMessage( body.channel.id, approvalTs, `Follow-up for task ${taskId} is already being processed.`, this.buildApprovalProcessingBlocks(taskId, actionName), ); return; } this.approvalInFlight.add(taskId); try { const result = await this.ocp.resolveApproval({ eventId: approvalEventId(action, body), taskId, approvalKind: "github_review_followup", action: actionName, slackUserId: body.user.id, resolvedBy: body.user.name || body.user.id, channelId: body.channel.id, threadTs: this.trackedTasks.get(taskId)?.threadTs, }); this.approvalResolved.add(taskId); this.approvalInFlight.delete(taskId); await this.updateApprovalMessage( body.channel.id, approvalTs, `Follow-up resolved for task ${taskId}.`, this.buildApprovalResolvedBlocks(taskId, actionName), ); const trackedTask = this.toTrackedTask(result); if (trackedTask) { this.upsertTrackedTask(trackedTask); this.frontalCodeEvents.trackTask(trackedTask); } } catch (error) { this.approvalInFlight.delete(taskId); await this.updateApprovalMessage( body.channel.id, approvalTs, `Follow-up for task ${taskId} failed: ${(error as Error).message}`, this.buildApprovalErrorBlocks(taskId, error as Error), ); } } private async updateApprovalMessage( channel: string, ts: string, text: string, blocks: SlackBlock[], ): Promise { await this.ocp.updateMessage({ channelId: channel, ts, text, blocks }); } private async handleFrontalCodeTaskEvent( event: FrontalCodeEventEnvelope, task?: FrontalCodeTrackedTask, ): Promise { const resolvedTask = await this.resolveTrackedTaskForEvent(event, task); if (!resolvedTask) { return; } const nextTask = resolvedTask; const message = this.formatFrontalCodeEvent(event, nextTask); if (!message) { return; } await this.postExternalStatusUpdates(event, message.text); if (event.event === "approval.requested" && this.approvalResolved.has(nextTask.taskId)) { return; } if (event.event === "approval.resolved") { this.approvalInFlight.delete(nextTask.taskId); this.approvalResolved.add(nextTask.taskId); const approvalTs = this.approvalMessageTsByTask.get(nextTask.taskId); if (approvalTs) { await this.ocp.updateMessage({ channelId: nextTask.channelId, text: message.text, blocks: message.blocks, ts: approvalTs, }); if (this.isTerminalFrontalCodeEvent(event)) { await this.cleanupTaskState(nextTask.taskId); } return; } } const response = await this.ocp.sendToThread({ channelId: nextTask.channelId, threadTs: nextTask.threadTs, text: message.text, blocks: message.blocks, correlationId: taskCorrelationId(nextTask.taskId), }); if (event.event === "approval.requested") { this.approvalMessageTsByTask.set(nextTask.taskId, response.ts as string); try { await this.frontalCodeApi.updateTaskContext({ taskId: nextTask.taskId, source: "slack", user_id: nextTask.userId, channel_id: nextTask.channelId, thread_ts: nextTask.threadTs, approval_message_ts: response.ts as string, }); } catch (error) { logger.warn("Failed to persist Slack approval message linkage to Frontal Code", { taskId: nextTask.taskId, error: (error as Error).message, }); } } if (this.isTerminalFrontalCodeEvent(event)) { await this.cleanupTaskState(nextTask.taskId); } } private async postExternalStatusUpdates( event: FrontalCodeEventEnvelope, fallbackText: string, ): Promise { const payload = event.payload; if (!payload) { return; } const hasLinear = payload.linear_issue_id || payload.linear_issue_identifier || payload.linear_issue_url; const hasGraphite = payload.graphite_stack_id || payload.graphite_head_branch || payload.graphite_base_branch; if (!hasLinear && !hasGraphite) { return; } const message = this.buildExternalStatusMessage(event) ?? fallbackText ?? undefined; if (!message) { return; } if (hasLinear) { try { await this.frontalCodeApi.postLinearStatus({ issueId: payload.linear_issue_id, identifier: payload.linear_issue_identifier, url: payload.linear_issue_url, state: payload.linear_issue_state, taskId: event.task_id, message, }); } catch (error) { logger.warn("Failed to post Linear status", { error: (error as Error).message, }); } } if (hasGraphite) { try { await this.frontalCodeApi.postGraphiteStatus({ stackId: payload.graphite_stack_id, headBranch: payload.graphite_head_branch, baseBranch: payload.graphite_base_branch, taskId: event.task_id, message, }); } catch (error) { logger.warn("Failed to post Graphite status", { error: (error as Error).message, }); } } } private buildExternalStatusMessage(event: FrontalCodeEventEnvelope): string | undefined { const summary = this.readEventTaskSummary(event); const syntheticTask: FrontalCodeTrackedTask = { taskId: event.task_id || summary?.work_item_id || "task", channelId: summary?.channel_id || "external", threadTs: summary?.thread_ts || undefined, userId: summary?.user_id || "", }; const formatted = this.formatFrontalCodeEvent(event, syntheticTask); return formatted?.text; } private async resolveTrackedTaskForEvent( event: FrontalCodeEventEnvelope, task?: FrontalCodeTrackedTask, ): Promise { const taskId = event.task_id; if (!taskId) { return undefined; } const knownTask = task || this.trackedTasks.get(taskId); if (knownTask) { const hydratedTask = this.hydrateTrackedTaskFromEvent(knownTask, event); this.upsertTrackedTask(hydratedTask); return hydratedTask; } const eventTask = this.readTrackedTaskFromEventSummary(taskId, event); if (eventTask) { this.upsertTrackedTask(eventTask); this.frontalCodeEvents.trackTask(eventTask); return eventTask; } try { const snapshot = await this.frontalCodeApi.getTask(taskId); const snapshotTask = this.toTrackedTask(snapshot); if (!snapshotTask) { return undefined; } this.syncDerivedApprovalState(snapshot); if (snapshot.approval_message_ts) { this.approvalMessageTsByTask.set(snapshotTask.taskId, snapshot.approval_message_ts); } this.upsertTrackedTask(snapshotTask); this.frontalCodeEvents.trackTask(snapshotTask); return snapshotTask; } catch (error) { logger.warn("Failed to resolve Slack task routing from Frontal Code event", { taskId, error: (error as Error).message, }); return undefined; } } private formatTaskCreatedMessage(task: FrontalCodeCreateTaskResponse, prompt: string): string { const parts = [`Task created: ${task.task_id}`, `Prompt: ${prompt}`]; if (task.plan_kind) { parts.push(`Lane: ${task.plan_kind}`); } if (task.worker_status) { parts.push(`Worker: ${task.worker_status}${task.worker_id ? ` (${task.worker_id})` : ""}`); } return parts.join("\n"); } private formatFrontalCodeEvent( event: FrontalCodeEventEnvelope, task: FrontalCodeTrackedTask, ): SlackMessagePayload | null { const summary = this.readEventTaskSummary(event); const taskLabel = this.describeTask(task.taskId, summary); switch (event.event) { case "task.created": return null; case "task.routed": return this.formatTaskRoutedEvent(taskLabel, event, summary); case "task.cancelled": return this.formatTaskCancelledEvent(taskLabel, event); case "lane.started": return this.formatLaneStartedEvent(taskLabel, event, summary); case "lane.failed": return this.formatLaneFailedEvent(taskLabel, event, summary); case "lane.blocked": return this.formatLaneBlockedEvent(taskLabel, event); case "lane.green": { const payload = event.payload as Record | undefined; const resultText = (payload?.result as string) || task.result || ""; const truncated = resultText && resultText.length > 2800 ? `${resultText.substring(0, 2800)}\n\n... (truncated)` : resultText || ""; const text = truncated ? `${taskLabel} completed:\n\n${truncated}` : `${taskLabel} reported a green lane.`; return { text }; } case "approval.requested": return this.formatApprovalRequestedEvent(event, task); case "approval.resolved": return this.formatApprovalResolvedEvent(task, taskLabel, event, summary); case "memory.captured": return { text: `Memory captured for ${taskLabel.toLowerCase()}.` }; case "connector.event.received": return this.formatConnectorEvent(taskLabel, event); default: return { text: `${taskLabel} updated: ${event.event}` }; } } private formatConnectorEvent( taskLabel: string, event: FrontalCodeEventEnvelope, ): SlackMessagePayload | null { const payload = event.payload; if (payload?.connector !== "github") { return null; } const connectorType = payload.type; const data = this.readConnectorEventData(payload.data); const actor = this.readConnectorString(data, "user_id", "sender_login"); const prNumber = this.readConnectorNumber(data, "pr_number") ?? this.readConnectorNumber(data, "number"); const htmlUrl = this.readConnectorString(data, "html_url"); const commentBody = this.readConnectorString(data, "comment_body"); const reviewBody = this.readConnectorString(data, "review_body"); const reviewState = this.readConnectorString(data, "review_state"); const prMerged = data?.pr_merged === true; const prSuffix = prNumber ? ` #${prNumber}` : ""; const prOnSuffix = prNumber ? ` on PR #${prNumber}` : ""; const actorSuffix = actor ? ` by ${actor}` : ""; const actorFromSuffix = actor ? ` from ${actor}` : ""; const urlSuffix = htmlUrl ? ` ${htmlUrl}` : ""; const reviewSuffix = reviewState ? ` (${reviewState.toLowerCase()})` : ""; switch (connectorType) { case "pull_request.synchronize": return { text: `${taskLabel} received a GitHub PR update${prNumber ? ` (#${prNumber})` : ""}${actorFromSuffix}.${urlSuffix}`, }; case "pull_request.closed": return { text: `${taskLabel} linked GitHub PR${prSuffix} was ${prMerged ? "merged" : "closed"}${actorSuffix}.${urlSuffix}`, }; case "pull_request.reopened": return { text: `${taskLabel} linked GitHub PR${prSuffix} was reopened${actorSuffix}.${urlSuffix}`, }; case "pull_request_review.submitted": return { text: `${taskLabel} received a GitHub review${prOnSuffix}${actorFromSuffix}${reviewSuffix}: ${reviewBody || "new review feedback"}`, }; case "issue_comment.created": return { text: `${taskLabel} received a GitHub comment${prOnSuffix}${actorFromSuffix}: ${commentBody || "new review feedback"}`, }; default: return null; } } private formatTaskRoutedEvent( taskLabel: string, event: FrontalCodeEventEnvelope, summary: FrontalCodeEventTaskSummary, ): SlackMessagePayload { const planKind = summary.plan_kind || event.payload?.plan_kind || "assigned"; const laneCount = event.payload?.lane_count; return { text: `${taskLabel} routed to ${planKind}${laneCount ? ` (${laneCount} lanes)` : ""}.`, }; } private formatTaskCancelledEvent( taskLabel: string, event: FrontalCodeEventEnvelope, ): SlackMessagePayload { const policyText = this.describeOrphanPolicy(this.readOrphanPolicy(event.payload)); return { text: `${taskLabel} was cancelled.${policyText ? ` ${policyText}` : ""}`, }; } private formatLaneStartedEvent( taskLabel: string, event: FrontalCodeEventEnvelope, summary: FrontalCodeEventTaskSummary, ): SlackMessagePayload { const role = event.payload?.role || "lane"; const workerStatus = summary.worker_status || event.payload?.worker_status || event.status; const workerId = summary.worker_id || event.payload?.worker_id; return { text: `${taskLabel} ${role} started with worker ${workerStatus}${workerId ? ` (${workerId})` : ""}.`, }; } private formatLaneFailedEvent( taskLabel: string, event: FrontalCodeEventEnvelope, summary: FrontalCodeEventTaskSummary, ): SlackMessagePayload { const error = summary.error || event.payload?.error || "lane execution failed"; return { text: `${taskLabel} failed: ${error}`, }; } private formatLaneBlockedEvent( taskLabel: string, event: FrontalCodeEventEnvelope, ): SlackMessagePayload { const reason = event.payload?.reason || event.payload?.detail || "waiting for input"; const policyText = this.describeOrphanPolicy(this.readOrphanPolicy(event.payload)); return { text: `${taskLabel} is blocked: ${reason}${policyText ? ` ${policyText}` : ""}`, }; } private formatApprovalResolvedEvent( task: FrontalCodeTrackedTask, taskLabel: string, event: FrontalCodeEventEnvelope, summary: FrontalCodeEventTaskSummary, ): SlackMessagePayload { const action = event.payload?.action || "updated"; const workerStatus = summary.worker_status || event.payload?.worker_status; const workerId = summary.worker_id || event.payload?.worker_id; if (event.payload?.approval_kind === "github_review_followup") { return { text: `GitHub follow-up cleared for ${taskLabel.toLowerCase()}.`, blocks: this.buildApprovalResolvedBlocks(task.taskId, action), }; } return { text: `Approval resolved for ${taskLabel.toLowerCase()}: ${action}.`, blocks: this.buildApprovalResolvedBlocks(task.taskId, action, workerStatus, workerId), }; } private formatApprovalRequestedEvent( event: FrontalCodeEventEnvelope, task: FrontalCodeTrackedTask, ): SlackMessagePayload { const summary = this.readEventTaskSummary(event); const payload = event.payload; const taskLabel = this.describeTask(task.taskId, summary); const approvalKind = payload?.approval_kind; const reason = payload?.reason || "Task requires approval."; const policyText = this.describeOrphanPolicy(this.readOrphanPolicy(payload)); if (approvalKind === "orphaned_hosted_agent") { return { text: `${taskLabel} needs approval: ${reason}`, blocks: [ { type: "section", text: { type: "mrkdwn", text: `*${taskLabel} needs approval*\n${reason}${policyText ? `\n${policyText}` : ""}`, }, }, { type: "actions", elements: [ { type: "button", text: { type: "plain_text", text: "Retry Lane", emoji: true, }, style: "primary", action_id: "orphaned_hosted_agent.retry", value: task.taskId, }, { type: "button", text: { type: "plain_text", text: "Cancel Task", emoji: true, }, style: "danger", action_id: "orphaned_hosted_agent.cancel", value: task.taskId, }, ], }, ], }; } if (approvalKind === "github_review_followup") { return { text: `${taskLabel} needs follow-up: ${reason}`, blocks: [ { type: "section", text: { type: "mrkdwn", text: `*${taskLabel} needs follow-up*\n${reason}`, }, }, { type: "actions", elements: [ { type: "button", text: { type: "plain_text", text: "Mark done", emoji: true, }, style: "primary", action_id: "github_review_followup.ack", value: task.taskId, }, { type: "button", text: { type: "plain_text", text: "Retry lane", emoji: true, }, action_id: "github_review_followup.retry", value: task.taskId, }, ], }, ], }; } return { text: `${taskLabel} is waiting for approval.`, }; } private readEventTaskSummary(event: FrontalCodeEventEnvelope): FrontalCodeEventTaskSummary { const payload = event.payload; if (!payload) { return {}; } const taskStatus = payload.task_status; return { task_status: taskStatus === "pending" || taskStatus === "running" || taskStatus === "completed" || taskStatus === "failed" || taskStatus === "cancelled" ? taskStatus : undefined, source: payload.source, user_id: payload.user_id, channel_id: payload.channel_id, thread_ts: payload.thread_ts, approval_message_ts: payload.approval_message_ts, repository: payload.repository, branch: payload.branch, priority: payload.priority, plan_id: payload.plan_id, plan_kind: payload.plan_kind, work_item_id: payload.work_item_id, worker_id: payload.worker_id, worker_status: payload.worker_status, result: payload.result, error: payload.error, }; } private readConnectorEventData( data: FrontalCodeEventPayload["data"], ): Record | undefined { return data && typeof data === "object" && !Array.isArray(data) ? (data as Record) : undefined; } private readConnectorString( data: Record | undefined, ...keys: string[] ): string | undefined { for (const key of keys) { const value = data?.[key]; if (typeof value === "string" && value.trim()) { return value; } } return undefined; } private readConnectorNumber( data: Record | undefined, ...keys: string[] ): number | undefined { for (const key of keys) { const value = data?.[key]; if (typeof value === "number") { return value; } } return undefined; } private hydrateTrackedTaskFromEvent( task: FrontalCodeTrackedTask, event: FrontalCodeEventEnvelope, ): FrontalCodeTrackedTask { const summary = this.readEventTaskSummary(event); const nextChannelId = summary.channel_id || task.channelId; const nextThreadTs = summary.thread_ts || task.threadTs; const nextUserId = summary.user_id || task.userId; const payload = event.payload as Record | undefined; const eventResult = (payload?.result as string) || task.result || undefined; if ( nextChannelId === task.channelId && nextThreadTs === task.threadTs && nextUserId === task.userId && eventResult === task.result ) { return task; } return { ...task, channelId: nextChannelId, threadTs: nextThreadTs, userId: nextUserId, result: eventResult, }; } private readTrackedTaskFromEventSummary( taskId: string, event: FrontalCodeEventEnvelope, ): FrontalCodeTrackedTask | undefined { const summary = this.readEventTaskSummary(event); if (!summary.channel_id) { return undefined; } return { taskId, channelId: summary.channel_id, threadTs: summary.thread_ts, userId: summary.user_id, }; } private describeTask(taskId: string, summary: FrontalCodeEventTaskSummary): string { if (summary.repository) { return `Task ${taskId} (${summary.repository})`; } return `Task ${taskId}`; } private parseOrphanPolicyCommand(text: string): FrontalCodeOrphanPolicyQuery | null { const trimmed = text.trim(); if (!trimmed) { return null; } const parts = trimmed.split(/\s+/); if (parts.length < 2 || parts[0] !== "policy" || parts[1] !== "orphans") { return null; } const query: FrontalCodeOrphanPolicyQuery = {}; for (const token of parts.slice(2)) { const [rawKey, ...rest] = token.split("="); const rawValue = rest.join("=").trim(); if (!rawKey || !rawValue) { continue; } const key = rawKey.trim().toLowerCase(); if (key === "repository" || key === "repo") { query.repository = rawValue; } else if (key === "source") { query.source = rawValue; } else if (key === "priority") { query.priority = rawValue; } } return query; } private buildOrphanPolicyCommandResponse(policy: FrontalCodeOrphanPolicyResponse): { response_type: "ephemeral"; text: string; blocks: SlackBlock[]; } { const preview = policy.preview ? [ policy.preview.repository ? `repo=${policy.preview.repository}` : undefined, policy.preview.source ? `source=${policy.preview.source}` : undefined, policy.preview.priority ? `priority=${policy.preview.priority}` : undefined, ] .filter((value): value is string => Boolean(value)) .join(", ") : undefined; const ruleLines = this.formatOrphanPolicyRuleLines(policy); const effectivePolicyText = this.describeOrphanPolicy(policy.effective_policy) || "Policy unavailable."; const defaultPolicyText = this.describeOrphanPolicy(policy.default_policy) || "Policy unavailable."; return { response_type: "ephemeral", text: `Orphan policy${preview ? ` preview for ${preview}` : ""}: ${effectivePolicyText}`, blocks: [ { type: "section", text: { type: "mrkdwn", text: `*Orphan policy${preview ? " preview" : ""}*\n${preview ? `Selectors: ${preview}\n` : ""}Effective: ${effectivePolicyText}\nDefault: ${defaultPolicyText}`, }, }, { type: "section", text: { type: "mrkdwn", text: `*Configured rules*\n${ruleLines.join("\n")}`, }, }, ], }; } private readOrphanPolicy( payload?: FrontalCodeEventPayload, ): FrontalCodeAppliedOrphanPolicy | undefined { return payload?.orphan_policy; } private describeOrphanPolicy(policy?: FrontalCodeAppliedOrphanPolicy): string | undefined { if (!policy) { return undefined; } const selectors = [ policy.match_repository ? `repo=${policy.match_repository}` : undefined, policy.match_source ? `source=${policy.match_source}` : undefined, policy.match_priority ? `priority=${policy.match_priority}` : undefined, ].filter((value): value is string => Boolean(value)); const policyScope = selectors.length > 0 ? `${policy.source} (${selectors.join(", ")})` : policy.source; const steps = [ `approval ${policy.approval_delay_secs}s`, policy.auto_retry_after_secs !== undefined ? `auto-retry ${policy.auto_retry_after_secs}s` : undefined, policy.auto_cancel_after_secs !== undefined ? `auto-cancel ${policy.auto_cancel_after_secs}s` : undefined, ].filter((value): value is string => Boolean(value)); return `Policy: ${policyScope}; ${steps.join(", ")}.`; } private formatOrphanPolicyRuleLines(policy: FrontalCodeOrphanPolicyResponse): string[] { if (policy.configured_rules.length === 0) { return ["No scoped rules configured."]; } return policy.configured_rules .slice(0, 5) .map((rule, index) => this.formatOrphanPolicyRuleLine(rule, index)); } private formatOrphanPolicyRuleLine( rule: FrontalCodeOrphanPolicyResponse["configured_rules"][number], index: number, ): string { const selectors = [ rule.repository ? `repo=${rule.repository}` : undefined, rule.source ? `source=${rule.source}` : undefined, rule.priority ? `priority=${rule.priority}` : undefined, ] .filter((value): value is string => Boolean(value)) .join(", "); const timing = [ rule.approval_delay_secs !== undefined ? `approval ${rule.approval_delay_secs}s` : undefined, rule.auto_retry_after_secs !== undefined ? `retry ${rule.auto_retry_after_secs}s` : undefined, rule.auto_cancel_after_secs !== undefined ? `cancel ${rule.auto_cancel_after_secs}s` : undefined, ] .filter((value): value is string => Boolean(value)) .join(", "); return `${index + 1}. ${selectors || "match any"} -> ${timing || "inherit defaults"}`; } private buildApprovalProcessingBlocks( taskId: string, action: FrontalCodeApprovalAction, ): SlackBlock[] { return [ { type: "section", text: { type: "mrkdwn", text: `*Processing approval for task ${taskId}*\nSelected action: \`${action}\``, }, }, ]; } private buildApprovalResolvedBlocks( taskId: string, action: string, workerStatus?: string, workerId?: string, ): SlackBlock[] { const detail = workerStatus && workerId ? `Worker: ${workerStatus} (${workerId})` : workerStatus ? `Worker: ${workerStatus}` : undefined; return [ { type: "section", text: { type: "mrkdwn", text: `*Approval resolved for task ${taskId}*\nAction: \`${action}\`${detail ? `\n${detail}` : ""}`, }, }, ]; } private buildApprovalErrorBlocks(taskId: string, error: Error): SlackBlock[] { return [ { type: "section", text: { type: "mrkdwn", text: `*Approval failed for task ${taskId}*\n${error.message}`, }, }, { type: "actions", elements: [ { type: "button", text: { type: "plain_text", text: "Retry Lane", emoji: true, }, style: "primary", action_id: "orphaned_hosted_agent.retry", value: taskId, }, { type: "button", text: { type: "plain_text", text: "Cancel Task", emoji: true, }, style: "danger", action_id: "orphaned_hosted_agent.cancel", value: taskId, }, ], }, ]; } private async syncTrackedTasksFromFrontalCode(): Promise { try { const activeTasks = await this.frontalCodeApi.listTasks({ source: "slack", status: "pending,running", }); let merged = 0; for (const task of activeTasks) { const trackedTask = this.toTrackedTask(task); if (!trackedTask) { continue; } if (!this.trackedTasks.has(trackedTask.taskId)) { merged += 1; } this.upsertTrackedTask(trackedTask); if (task.approval_message_ts) { this.approvalMessageTsByTask.set(trackedTask.taskId, task.approval_message_ts); } this.syncDerivedApprovalState(task); this.frontalCodeEvents.trackTask(trackedTask); } logger.info("Synchronized Slack tasks from Frontal Code", { discoveredTasks: activeTasks.length, mergedTasks: merged, trackedTasks: this.trackedTasks.size, }); } catch (error) { logger.warn("Failed to synchronize Slack tasks from Frontal Code", { error: (error as Error).message, }); } } private toTrackedTask(task: FrontalCodeTask): FrontalCodeTrackedTask | null { if (!task.channel_id) { return null; } return { taskId: task.task_id, channelId: task.channel_id, threadTs: task.thread_ts, userId: task.user_id, result: task.result, }; } private isTerminalFrontalCodeEvent(event: FrontalCodeEventEnvelope): boolean { return ( event.event === "task.cancelled" || event.event === "lane.green" || event.event === "lane.failed" ); } private upsertTrackedTask(task: FrontalCodeTrackedTask): void { const existing = this.trackedTasks.get(task.taskId); const mergedTask = this.sanitizeTrackedTask({ ...existing, ...task, threadTs: task.threadTs || existing?.threadTs, }); this.trackedTasks.set(task.taskId, mergedTask); } private sanitizeTrackedTask(task: FrontalCodeTrackedTask): FrontalCodeTrackedTask { return { taskId: task.taskId, channelId: task.channelId, threadTs: task.threadTs, userId: task.userId, }; } private syncDerivedApprovalState(task: FrontalCodeTask): void { const hasApprovalMessage = Boolean(task.approval_message_ts); const awaitingApproval = task.worker_status === "orphaned"; if (hasApprovalMessage && !awaitingApproval) { this.approvalResolved.add(task.task_id); return; } this.approvalResolved.delete(task.task_id); } private cleanupTaskState(taskId: string): void { this.trackedTasks.delete(taskId); this.frontalCodeEvents.untrackTask(taskId); this.approvalMessageTsByTask.delete(taskId); this.approvalInFlight.delete(taskId); this.approvalResolved.delete(taskId); } } // Slack stamps each click with action_ts; with the user it identifies one decision, so a redelivered // click is recorded once. function approvalEventId(action: SlackAction, body: SlackBody): string | undefined { return action.action_ts ? `slack-action:${body.user.id}:${action.action_ts}` : undefined; } export default SlackInterface;