Skip to content
← Public packages

@kentcdodds/kody-issue-triage

Loop-safe triage for Kody run errors and fleet package-runtime error-rate elevations. Wakes Cole (Grok Bot) to decide; Cursor agents only when Cole escalates.

src/peer-context.ts

352 lines · 10.4 KB · TypeScript
import { agentsWithOpenGithubPrs, listAgents } from 'kody:@kentcdodds/cursor/agents'
import githubRequest from 'kody:@kentcdodds/github/request'
import { kody } from 'kody:runtime'
import {
	agentCreatedRecently,
	agentMatchesRecord,
	agentUrlFrom,
	emptyPeerContext,
	liveSiblingForRecord,
	PEER_AGENT_LIMIT,
	PEER_PR_LIMIT,
	PEER_PUBLISH_LIMIT,
	PEER_SIBLING_LIMIT,
	prMatchesRecord,
	repoSlugFromUrl,
	siblingMatchesRecord,
	siblingWhy,
	type PeerAgent,
	type PeerContext,
	type PeerPublish,
	type PeerPullRequest,
	type PeerSibling,
} from './peer-match.ts'
import type { FingerprintRecord } from './shared.ts'
import { activityUrlFor } from './shared.ts'
import {
	getFingerprint,
	initSchema,
	listRecentFingerprints,
	listSiblingFingerprints,
} from './storage.ts'

export type { PeerContext } from './peer-match.ts'
export { formatPeerContextForPrompt } from './peer-match.ts'

export type GetPeerContextInput = {
	fingerprint?: string
	record?: FingerprintRecord
}

type GithubPull = {
	number?: number
	title?: string
	html_url?: string
	draft?: boolean
	updated_at?: string
	head?: { ref?: string }
	base?: { repo?: { full_name?: string } }
}

type ListedAgent = {
	id?: string
	name?: string
	status?: string
	url?: string
	createdAt?: string
}

type AgentPrRow = {
	agentId?: string
	prUrl?: string
	pr_url?: string
	url?: string
	title?: string
	prTitle?: string
	prState?: string
	repo?: string
	number?: number
	draft?: boolean
	updatedAt?: string
	updated_at?: string
}

async function settled<T>(work: () => Promise<T>, fallback: T): Promise<T> {
	try {
		return await work()
	} catch {
		return fallback
	}
}

function summarizeAgents(items: ListedAgent[], record: FingerprintRecord): PeerAgent[] {
	const matched: PeerAgent[] = []
	for (const item of items) {
		if (!agentMatchesRecord(item, record)) continue
		const createdAt = item.createdAt ? String(item.createdAt) : null
		matched.push({
			id: String(item.id || ''),
			name: String(item.name || 'unnamed'),
			status: String(item.status || 'UNKNOWN'),
			url: agentUrlFrom(item),
			createdAt,
			why: agentCreatedRecently(createdAt)
				? 'recently created triage agent matching owner or error family'
				: 'matching triage agent',
		})
		if (matched.length >= PEER_AGENT_LIMIT) break
	}
	return matched.filter((agent) => agent.id)
}

function pullFromGithub(row: GithubPull): PeerPullRequest | null {
	const url = String(row.html_url || '')
	if (!url) return null
	return {
		repo: String(row.base?.repo?.full_name || repoSlugFromUrl(url) || 'unknown'),
		number: typeof row.number === 'number' ? row.number : null,
		title: String(row.title || 'untitled'),
		url,
		draft: row.draft === true,
		updatedAt: row.updated_at ? String(row.updated_at) : null,
		agentId: null,
		why: 'open PR on a Kody repo matching owner or error family',
	}
}

function pullFromAgentRow(row: AgentPrRow): PeerPullRequest | null {
	const url = String(row.prUrl || row.pr_url || row.url || '')
	if (!url) return null
	return {
		repo: String(row.repo || repoSlugFromUrl(url) || 'unknown'),
		number: typeof row.number === 'number' ? row.number : null,
		title: String(row.title || row.prTitle || 'untitled'),
		url,
		draft: row.draft === true,
		updatedAt: row.updatedAt || row.updated_at ? String(row.updatedAt || row.updated_at) : null,
		agentId: row.agentId ? String(row.agentId) : null,
		why: 'open PR attached to a Cloud agent',
	}
}

function dedupePulls(items: Array<PeerPullRequest | null>) {
	const seen = new Set<string>()
	const next: PeerPullRequest[] = []
	for (const item of items) {
		if (!item?.url || seen.has(item.url)) continue
		seen.add(item.url)
		next.push(item)
		if (next.length >= PEER_PR_LIMIT) break
	}
	return next
}

async function listOpenRepoPulls(repo: 'kody' | 'use-kody') {
	const response = await githubRequest({
		account: 'bot',
		path: `/repos/kentcdodds/${repo}/pulls`,
		query: { state: 'open', per_page: 20, sort: 'updated', direction: 'desc' },
		timeoutMs: 8000,
	})
	const data = Array.isArray(response?.data) ? (response.data as GithubPull[]) : []
	return data
}

async function gatherPullRequests(
	record: FingerprintRecord,
	matchedAgentIds: Set<string>,
): Promise<PeerPullRequest[]> {
	const [agentPrs, kodyPulls, useKodyPulls] = await Promise.all([
		settled(async () => {
			const result = await agentsWithOpenGithubPrs({ limit: 20 })
			const rows = Array.isArray(result?.pullRequests)
				? (result.pullRequests as AgentPrRow[])
				: []
			return rows
		}, []),
		settled(() => listOpenRepoPulls('kody'), []),
		settled(() => listOpenRepoPulls('use-kody'), []),
	])

	const fromAgents = agentPrs
		.map((row) => {
			const pr = pullFromAgentRow(row)
			if (!pr) return null
			const linked = Boolean(pr.agentId && matchedAgentIds.has(pr.agentId))
			if (!prMatchesRecord(pr, record, { linkedToMatchedAgent: linked })) {
				return null
			}
			return {
				...pr,
				why: linked
					? 'open PR attached to a matching triage agent'
					: pr.why,
			}
		})

	const fromGithub = [...kodyPulls, ...useKodyPulls]
		.map(pullFromGithub)
		.filter((pr): pr is PeerPullRequest => Boolean(pr && prMatchesRecord(pr, record)))

	return dedupePulls([...fromAgents, ...fromGithub])
}

async function gatherPublishes(record: FingerprintRecord): Promise<PeerPublish[]> {
	const items: PeerPublish[] = []
	if (record.package_id) {
		const pkg = await settled(
			() => kody.packageGet({ package_id: record.package_id as string }),
			null as { kody_id?: string; updated_at?: string } | null,
		)
		if (pkg?.updated_at) {
			items.push({
				kodyId: pkg.kody_id || record.kody_id,
				packageId: record.package_id,
				name: 'package_get.updated_at',
				status: 'updated',
				startedAt: String(pkg.updated_at),
				url: null,
				why: 'saved package last updated',
			})
		}
		const runs = await settled(
			() =>
				kody.runList({
					package_id: record.package_id as string,
					status: 'success',
					limit: 5,
				}),
			{ runs: [] as Array<Record<string, unknown>> },
		)
		for (const run of runs.runs || []) {
			const name = String(run.name || run.surface || 'run')
			items.push({
				kodyId: record.kody_id,
				packageId: record.package_id,
				name,
				status: String(run.status || 'success'),
				startedAt: run.started_at ? String(run.started_at) : null,
				url: activityUrlFor(run.id ? String(run.id) : null),
				why: 'recent successful owner-package run',
			})
			if (items.length >= PEER_PUBLISH_LIMIT) break
		}
	}

	if (record.kody_id && items.length < PEER_PUBLISH_LIMIT) {
		const workflows = await settled(
			() => kody.workflowRunList({ limit: 25 }),
			{ workflows: [] as Array<Record<string, unknown>> },
		)
		for (const workflow of workflows.workflows || []) {
			if (String(workflow.kody_id || '') !== record.kody_id) continue
			if (String(workflow.status || '') !== 'complete') continue
			items.push({
				kodyId: record.kody_id,
				packageId: record.package_id,
				name: String(workflow.workflow_name || workflow.export_name || 'workflow'),
				status: 'complete',
				startedAt: workflow.completed_at
					? String(workflow.completed_at)
					: workflow.created_at
						? String(workflow.created_at)
						: null,
				url: null,
				why: 'recent completed owner workflow',
			})
			if (items.length >= PEER_PUBLISH_LIMIT) break
		}
	}

	return items.slice(0, PEER_PUBLISH_LIMIT)
}

function siblingsFromRecent(
	recent: FingerprintRecord[],
	record: FingerprintRecord,
): PeerSibling[] {
	const items: PeerSibling[] = []
	for (const sibling of recent) {
		if (!siblingMatchesRecord(sibling, record)) continue
		items.push({
			fingerprint: sibling.fingerprint,
			owner: sibling.owner,
			error_family: sibling.error_family,
			sample_message: sibling.sample_message,
			status: sibling.status,
			outcome: sibling.outcome,
			completedAt: sibling.completed_at,
			agentUrl: sibling.agent_url || sibling.kody_agent_url,
			summary: sibling.summary,
			why: siblingWhy(sibling, record),
		})
		if (items.length >= PEER_SIBLING_LIMIT) break
	}
	return items
}

/**
 * Refresh the spawn-time peer snapshot: matching Cloud agents, related PRs,
 * owner-package activity, and stored sibling fingerprints.
 *
 * @param input.fingerprint - Fingerprint string to look up when `record` is omitted.
 * @param input.record - Optional already-loaded fingerprint record.
 * @returns Peer context (`agents`, `pullRequests`, `publishes`, `siblings`, ...) plus `ok`.
 *
 * @example
 * import getPeerContext from 'kody:@kentcdodds/kody-issue-triage/peer-context'
 * const peers = await getPeerContext({ fingerprint: 'abc123' })
 * // => { ok: true, fingerprint: 'abc123', agents: [...], ... }
 */
export async function gatherPeerContext(record: FingerprintRecord): Promise<PeerContext> {
	const listed = await settled(async () => {
		const page = await listAgents({ limit: 40 })
		return Array.isArray(page?.items) ? (page.items as ListedAgent[]) : []
	}, [] as ListedAgent[])

	const agents = summarizeAgents(listed, record)
	const matchedAgentIds = new Set(agents.map((agent) => agent.id))
	const [pullRequests, publishes, storedSiblings, recent] = await Promise.all([
		settled(() => gatherPullRequests(record, matchedAgentIds), []),
		settled(() => gatherPublishes(record), []),
		settled(() => listSiblingFingerprints(record, 20), [] as FingerprintRecord[]),
		settled(() => listRecentFingerprints(40), [] as FingerprintRecord[]),
	])
	const siblings = siblingsFromRecent(
		storedSiblings.length ? storedSiblings : recent,
		record,
	)

	return {
		agents,
		pullRequests,
		publishes,
		siblings,
		highConfidenceLiveSibling: liveSiblingForRecord(agents, record),
	}
}

/**
 * Refresh the spawn-time peer snapshot: matching Cloud agents, related PRs,
 * owner-package activity, and stored sibling fingerprints.
 *
 * @param input.fingerprint - Fingerprint string to look up when `record` is omitted.
 * @param input.record - Optional already-loaded fingerprint record.
 * @returns Peer context (`agents`, `pullRequests`, `publishes`, `siblings`, ...) plus `ok`.
 *
 * @example
 * import getPeerContext from 'kody:@kentcdodds/kody-issue-triage/peer-context'
 * const peers = await getPeerContext({ fingerprint: 'abc123' })
 * // => { ok: true, fingerprint: 'abc123', agents: [...], ... }
 */
export default async function getPeerContext(input: GetPeerContextInput = {}) {
	await initSchema()
	const record =
		input.record ||
		(input.fingerprint ? await getFingerprint(String(input.fingerprint).trim()) : null)
	if (!record) {
		return { ...emptyPeerContext(), ok: false as const, error: 'unknown fingerprint' }
	}
	const context = await gatherPeerContext(record)
	return { ok: true as const, fingerprint: record.fingerprint, ...context }
}