Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/config/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@ describe('FactoryConfigSchema', () => {
expect(parsed.environments).toEqual({})
})

it('rejects ticket-dispatch notification targets that cannot deliver', () => {
const withTarget = (target: Record<string, unknown>) => FactoryConfigSchema.safeParse({
repos: { default: 'AgentWorkforce/factory' },
hooks: { onTicketDispatch: { notify: [target] } },
})

expect(withTarget({ surface: 'slack' }).success).toBe(false)
expect(withTarget({ surface: 'linear', commentOnIssue: false }).success).toBe(false)
expect(withTarget({ surface: 'slack', dm: 'U123' }).success).toBe(true)
expect(withTarget({ surface: 'linear', commentOnIssue: true }).success).toBe(true)
})

it('accepts secret-reference-only Kubernetes BYOC and managed connections', () => {
const parsed = FactoryConfigSchema.parse({
repos: { default: 'AgentWorkforce/factory' },
Expand Down
37 changes: 37 additions & 0 deletions src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,40 @@ const reportingSchema = z.object({
requestTimeoutMs: z.number().int().min(100).max(60_000).default(15_000),
}).default({})

const ticketDispatchNotificationSchema = z.discriminatedUnion('surface', [
z.object({
surface: z.literal('relay'),
channel: z.string().trim().min(1),
}).strict(),
z.object({
surface: z.literal('slack'),
Comment thread
khaliqgant marked this conversation as resolved.
channel: z.string().trim().min(1).optional(),
dm: z.string().trim().min(1).optional(),
}).strict(),
z.object({
surface: z.literal('telegram'),
chatId: z.string().trim().min(1),
}).strict(),
z.object({
surface: z.literal('linear'),
commentOnIssue: z.literal(true),
}).strict(),
Comment thread
khaliqgant marked this conversation as resolved.
]).superRefine((target, ctx) => {
if (target.surface === 'slack' && !target.channel && !target.dm) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['channel'],
message: 'Slack ticket-dispatch notifications require channel and/or dm',
})
}
})

const hooksSchema = z.object({
onTicketDispatch: z.object({
notify: z.array(ticketDispatchNotificationSchema).min(1),
}).strict().optional(),
}).strict().optional()

const previewServiceSchema = z.object({
/** Local HTTP port the repository's development server listens on. */
port: z.number().int().min(1).max(65_535),
Expand Down Expand Up @@ -283,6 +317,9 @@ const WorkspaceConfigObjectSchema = z.object({
// analytics. It defaults on for real CLI sessions and remains no-op when no
// Cloud account is available; delivery failure never changes orchestration.
reporting: reportingSchema,
// Optional fan-out for the point an agent team has been successfully
// dispatched. Every configured surface is attempted independently.
hooks: hooksSchema,
preview: previewSchema,
github: githubSchema,
verification: verificationSchema,
Expand Down
29 changes: 29 additions & 0 deletions src/delivery/ticket-dispatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { slackClient, telegramClient } from '@relayfile/relay-helpers'

export interface TicketDispatchDelivery {
slack(input: { channel?: string; dm?: string; text: string }): Promise<void>
telegram(input: { chatId: string; text: string }): Promise<void>
}

export function createTicketDispatchDelivery(options: {
mountRoot?: string
} = {}): TicketDispatchDelivery {
const clientOptions = options.mountRoot ? { mountRoot: options.mountRoot } : undefined

return {
async slack({ channel, dm, text }): Promise<void> {
const client = slackClient(clientOptions)
await Promise.all([
...(channel ? [client.post(channel, text)] : []),
...(dm ? [client.dm(dm, text)] : []),
])
},

async telegram({ chatId, text }): Promise<void> {
const result = await telegramClient(clientOptions).sendMessage(chatId, text)
if (!result.ok || !result.messageId) {
throw new Error(`Telegram delivery to ${chatId} returned no receipt`)
}
},
}
}
Loading