← Public packages
@kentcdodds/friction-log
Qualify and file friction papercuts to GitHub (any owner/repo) or wake Patch for Kody packages; daily sweep of kody-hq/kody friction issues.
src/sweep.ts
190 lines · 4.3 KB · TypeScriptimport { 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<FrictionIssue>) {
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,
}
}