import { createAgent, listAgents } from 'kody:@kentcdodds/cursor/agents' import { createRun } from 'kody:@kentcdodds/cursor/runs' import { buildFrictionAgentPrompt } from './agent-prompt.ts' import { eligibleIssues, isEligibleIssue } from './eligibility.ts' import { loadFrictionIssues } from './issues.ts' import { agentModel, agentUrlOf, denverDayKey, maxIssuesInPrompt, ref, repository, type FrictionIssue, } from './shared.ts' import { getSweep, isEnabled, latestLiveSweep, listRecentOutcomes, upsertSweep, } from './storage.ts' export type SweepInput = { dryRun?: boolean force?: boolean } function isLiveStatus(status: unknown) { const value = String(status || '').toUpperCase() return value === 'ACTIVE' || value === 'CREATING' || value === 'RUNNING' } async function findLiveHunterAgent() { try { const { items } = await listAgents({ limit: 50 }) return ( (items || []).find((agent) => { const name = String(agent.name || '') return /friction-log/i.test(name) && isLiveStatus(agent.status) }) || null ) } catch { return null } } function compactIssues(issues: Array) { return issues.slice(0, maxIssuesInPrompt).map((issue) => ({ number: issue.number, title: issue.title, htmlUrl: issue.htmlUrl, labels: issue.labels, eligibility: isEligibleIssue(issue).reason, })) } /** * List open friction issues and spawn one Cloud Agent when any are eligible. */ export default async function sweep(input: SweepInput = {}) { const dryRun = input.dryRun === true const force = input.force === true const day = denverDayKey() if (!(await isEnabled()) && !dryRun) { return { ok: true, skipped: 'paused', day } } const existing = await getSweep(day) if (existing && existing.spawned === 1 && existing.dry_run === 0 && !dryRun && !force) { return { ok: true, skipped: 'already-spawned-today', day, agentId: existing.agent_id, agentUrl: existing.agent_url, } } const liveSweep = await latestLiveSweep() if (liveSweep?.agent_id && !dryRun && !force) { const outcomes = await listRecentOutcomes(5) const finished = outcomes.some((row) => row.agent_id === liveSweep.agent_id) if (!finished) { const live = await findLiveHunterAgent() if (live) { return { ok: true, skipped: 'live-agent', day, agentId: live.id, agentUrl: agentUrlOf(live), } } } } const loaded = await loadFrictionIssues() const eligible = eligibleIssues(loaded) const queued = eligible.slice(0, maxIssuesInPrompt) const issueNumbers = queued.map((issue) => issue.number) if (queued.length === 0) { if (!dryRun) { await upsertSweep({ day, scanned_at: new Date().toISOString(), eligible_count: 0, issue_numbers_json: '[]', spawned: 0, agent_id: null, agent_url: null, skipped: 'no-eligible-issues', dry_run: 0, }) } return { ok: true, day, dryRun, openCount: loaded.length, eligibleCount: 0, spawned: false, } } const prompt = buildFrictionAgentPrompt({ issues: queued, day }) if (dryRun) { return { ok: true, day, dryRun: true, openCount: loaded.length, eligibleCount: eligible.length, issues: compactIssues(queued), wouldSpawn: true, promptChars: prompt.length, } } const created = await createAgent({ prompt: { text: prompt }, model: agentModel, name: `friction-log ${day}`.slice(0, 60), repository, ref, }) const agent = (created as { agent?: { id?: string; url?: string } })?.agent ?? (created as { id?: string; url?: string }) const agentId = agent?.id || null const agentUrl = agentUrlOf(agent) if (agentId && agentUrl) { try { await createRun({ agentId, prompt: { text: `Your public agent URL is ${agentUrl}. Continue the original friction-log investigation.`, }, }) } catch { // The initial prompt already tells the agent how to recover the URL. } } await upsertSweep({ day, scanned_at: new Date().toISOString(), eligible_count: queued.length, issue_numbers_json: JSON.stringify(issueNumbers), spawned: 1, agent_id: agentId, agent_url: agentUrl, skipped: null, dry_run: 0, }) return { ok: true, day, openCount: loaded.length, eligibleCount: eligible.length, issues: compactIssues(queued), spawned: true, agentId, agentUrl, } }