← 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/update-card.ts
181 lines · 4.5 KB · TypeScriptimport editMessage from 'kody:@kentcdodds/discord/edit-message'
import { formatKodyIssueReport } from './format-discord-report.ts'
import { discordChannelId, truncate } from './shared.ts'
import {
getFingerprint,
initSchema,
updateFingerprint,
} from './storage.ts'
export type CardStage = 'started' | 'pr' | 'merged' | 'deployed'
export type UpdateCardInput = {
fingerprint: string
stage: CardStage
/** Convenience URL mapped by stage when stage-specific fields are omitted. */
url?: string
agentUrl?: string
prUrl?: string
mergeCommitUrl?: string
deployUrl?: string
status?: string
summary?: string
discordMessageId?: string
}
const STAGES = new Set<CardStage>(['started', 'pr', 'merged', 'deployed'])
const STAGE_STATUS: Record<CardStage, string> = {
started: 'in-progress',
pr: 'pr-open',
merged: 'merged',
deployed: 'deployed',
}
function trimUrl(value: unknown): string | null {
if (typeof value !== 'string') return null
const trimmed = value.trim()
return trimmed ? trimmed : null
}
/**
* Progressive Discord card updater for an in-flight Kody issue fingerprint.
* Edits the stored Discord message in place and accumulates lifecycle links
* (agent → PR → merge commit → deploy) without releasing the lease or marking
* a terminal outcome. Use `./record-outcome` for the final report.
*
* @example
* import updateCard from 'kody:@kentcdodds/kody-issue-triage/update-card'
* await updateCard({
* fingerprint: 'workflow:skills:timeout-90s',
* stage: 'started',
* agentUrl: 'https://cursor.com/agents/…',
* })
*
* @example
* import updateCard from 'kody:@kentcdodds/kody-issue-triage/update-card'
* await updateCard({
* fingerprint: 'workflow:skills:timeout-90s',
* stage: 'pr',
* prUrl: 'https://github.com/kentcdodds/kody/pull/1',
* })
*/
export default async function updateCard(input: UpdateCardInput) {
const fingerprint = String(input?.fingerprint || '').trim()
const stage = String(input?.stage || '').trim() as CardStage
if (!fingerprint) {
return {
ok: true as const,
skipped: 'invalid-input',
error: 'fingerprint is required',
}
}
if (!STAGES.has(stage)) {
return {
ok: true as const,
skipped: 'invalid-input',
error: `stage must be one of: ${[...STAGES].join(', ')}`,
}
}
await initSchema()
const record = await getFingerprint(fingerprint)
if (!record) {
return {
ok: true as const,
skipped: 'unknown-fingerprint',
error: `unknown fingerprint: ${fingerprint}`,
}
}
const aliasUrl = trimUrl(input.url)
const agentUrl =
trimUrl(input.agentUrl) ||
(stage === 'started' ? aliasUrl : null) ||
record.agent_url
const prUrl =
trimUrl(input.prUrl) ||
(stage === 'pr' ? aliasUrl : null) ||
record.pr_url
const mergeCommitUrl =
trimUrl(input.mergeCommitUrl) ||
(stage === 'merged' ? aliasUrl : null) ||
record.merge_commit_url
const deployUrl =
trimUrl(input.deployUrl) ||
(stage === 'deployed' ? aliasUrl : null) ||
record.deploy_url
const summary =
typeof input.summary === 'string' && input.summary.trim()
? truncate(input.summary.trim(), 1800)
: record.summary
const terminal = new Set([
'fixed',
'ignored',
'resolved_noise',
'recommendation',
'loop_detected',
'failed',
])
const status =
(typeof input.status === 'string' && input.status.trim()) ||
(terminal.has(record.status) ? record.status : STAGE_STATUS[stage])
await updateFingerprint(fingerprint, {
agent_url: agentUrl,
pr_url: prUrl,
merge_commit_url: mergeCommitUrl,
deploy_url: deployUrl,
card_stage: stage,
summary,
status,
})
const updated = {
...record,
agent_url: agentUrl,
pr_url: prUrl,
merge_commit_url: mergeCommitUrl,
deploy_url: deployUrl,
card_stage: stage,
summary,
status,
}
const discordMessageId =
trimUrl(input.discordMessageId) || record.discord_message_id
const report = formatKodyIssueReport({
status,
record: updated,
summary,
prUrl,
mergeCommitUrl,
deployUrl,
})
if (discordMessageId) {
try {
await editMessage({
channelId: discordChannelId,
messageId: discordMessageId,
content: report.content,
flags: report.flags,
})
} catch {
// Best-effort Discord edit; storage already updated.
}
}
return {
ok: true as const,
fingerprint,
stage,
discordMessageId,
links: {
activity: updated.activity_url,
agent: updated.agent_url,
pr: updated.pr_url,
merge: updated.merge_commit_url,
deploy: updated.deploy_url,
},
...(discordMessageId ? {} : { skipped: 'no-discord-message' }),
}
}