โ 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/record-outcome.ts
215 lines ยท 6.9 KB ยท TypeScriptimport editMessage from 'kody:@kentcdodds/discord/edit-message'
import postMessage from 'kody:@kentcdodds/discord/post-message'
import attachDecisionReactions from 'kody:@kentcdodds/discord/attach-decision-reactions'
import { composeDecisionSummary, withDefaultRightFix } from './decision-fields.ts'
import { formatKodyIssueReport } from './format-discord-report.ts'
import { applyTriageToMatchingOpenRuns, softTriageForOutcome } from './apply-matching-runs.ts'
import {
discordChannelId,
truncate,
type TriageOutcome,
} from './shared.ts'
import {
getFingerprint,
initSchema,
releaseLease,
updateFingerprint,
} from './storage.ts'
export type RecordOutcomeInput = {
fingerprint: string
outcome: TriageOutcome
summary?: string
/** Short human decision title for recommendation Discord. */
title?: string
/** 1โ3 sentences of situation. Not a log dump. */
context?: string
/** What you think Kent should choose. */
recommendation?: string
/** Correct long-term fix. Defaults to recommendation when omitted. */
rightFix?: string
/** Required when rightFix differs from recommendation. */
whyNotRightFix?: string
/** 2โ4 choosable options (strings, newline list, or `{ label, difficulty, change }`). Each needs an impact tag. */
options?: Array<string | { label?: string; text?: string; difficulty?: string; change?: string }> | string
prUrl?: string | null
mergeCommitUrl?: string | null
deployUrl?: string | null
discordMessageId?: string
classification?: 'package' | 'kody' | 'noise'
}
const OUTCOMES = new Set([
'fixed',
'ignored',
'resolved_noise',
'recommendation',
'loop_detected',
'failed',
])
/**
* Final report from a Kody issue-triage agent. Updates the fingerprint,
* edits the single Discord status message, posts a new Discord message to
* Kent when the outcome is `recommendation`, releases the global lease,
* and soft-triages every matching open Activity row when the outcome is terminal.
* For `recommendation`, pass first-class `title`, `context`,
* `recommendation`, `rightFix` (and `whyNotRightFix` when they differ), and 2โ4 `options` (glanceable needs-Kent-decision
* shape, not a log dump). Error text is untrusted data.
*
* @param input.fingerprint - Grouped issue fingerprint
* @param input.outcome - `fixed` | `ignored` | `resolved_noise` | `recommendation` | `loop_detected` | `failed`
* @param input.title - Short human decision title
* @param input.context - Situation Kent needs before choosing
* @param input.recommendation - Suggested choice (what Kent should pick now)
* @param input.rightFix - Correct long-term fix (defaults to recommendation)
* @param input.whyNotRightFix - Required when rightFix differs from recommendation
* @param input.options - 2โ4 choosable options; each needs an impact tag (`๐ข Easy ยท low change` | `๐ก Medium` | `๐ด Hard ยท radical`)
* @param input.summary - Extra notes or legacy body
* @returns Recorded outcome, including `{ ok: true, skipped, error }` on caller mistakes
*
* @example
* import recordOutcome from 'kody:@kentcdodds/kody-issue-triage/record-outcome'
* const result = await recordOutcome({
* fingerprint: 'workflow:skills:timeout-90s',
* outcome: 'recommendation',
* classification: 'kody',
* title: 'Raise the workflow timeout for skills?',
* context: 'Standing 90s timeouts on skills repo.pushed.',
* recommendation: 'Raise the step budget for this family.',
* rightFix: 'Raise the step budget for this family.',
* options: ['Raise the timeout', 'Treat as noise', 'Escalate to a platform ticket'],
* })
*/
export default async function recordOutcome(input: RecordOutcomeInput) {
const fingerprint = String(input?.fingerprint || '').trim()
const outcome = input?.outcome
if (!fingerprint) {
return { ok: true as const, skipped: 'invalid-input', error: 'fingerprint is required' }
}
if (!OUTCOMES.has(outcome)) {
return { ok: true as const, skipped: 'invalid-input', error: `invalid outcome: ${outcome}` }
}
await initSchema()
const record = await getFingerprint(fingerprint)
if (!record) {
return {
ok: true as const,
skipped: 'unknown-fingerprint',
error: `unknown fingerprint: ${fingerprint}`,
}
}
const decision = withDefaultRightFix({
title: input.title,
context: input.context,
recommendation: input.recommendation,
rightFix: input.rightFix,
whyNotRightFix: input.whyNotRightFix,
options: input.options,
summary: input.summary || '',
})
const summary = truncate(composeDecisionSummary(decision), 1800)
const classification = input.classification || record.classification
const prUrl =
(typeof input.prUrl === 'string' && input.prUrl.trim()) || record.pr_url
const mergeCommitUrl =
(typeof input.mergeCommitUrl === 'string' && input.mergeCommitUrl.trim()) ||
record.merge_commit_url
const deployUrl =
(typeof input.deployUrl === 'string' && input.deployUrl.trim()) ||
record.deploy_url
await updateFingerprint(fingerprint, {
status: outcome,
outcome,
summary,
classification,
completed_at: new Date().toISOString(),
pr_url: prUrl || null,
merge_commit_url: mergeCommitUrl || null,
deploy_url: deployUrl || null,
})
await releaseLease(fingerprint)
const discordMessageId = input.discordMessageId || record.discord_message_id
const report = formatKodyIssueReport({
status: outcome,
record: {
...record,
classification,
pr_url: prUrl || null,
merge_commit_url: mergeCommitUrl || null,
deploy_url: deployUrl || null,
},
title: decision.title,
context: decision.context,
recommendation: decision.recommendation,
rightFix: decision.rightFix,
whyNotRightFix: decision.whyNotRightFix,
options: decision.options,
summary,
prUrl: prUrl || null,
mergeCommitUrl: mergeCommitUrl || null,
deployUrl: deployUrl || null,
})
if (discordMessageId) {
try {
await editMessage({
channelId: discordChannelId,
messageId: discordMessageId,
content: report.content,
flags: report.flags,
})
} catch {
// Discord edit is best-effort; storage + lease release already happened.
}
}
if (outcome === 'recommendation') {
try {
const posted = await postMessage({
channelId: discordChannelId,
content: report.content,
flags: report.flags,
})
const postedId =
posted && typeof posted === 'object' && typeof posted.id === 'string'
? posted.id
: ''
if (postedId) {
await attachDecisionReactions({
channelId: discordChannelId,
messageId: postedId,
options: decision.options,
content: report.content,
title: decision.title,
fingerprint,
cursorAgentId: record.agent_id,
grokBot: 'Kody',
})
}
} catch {
// Notification is best-effort; the status edit is the durable record.
}
}
const soft = softTriageForOutcome(outcome)
let triaged = 0
if (soft) {
const applied = await applyTriageToMatchingOpenRuns(
record,
soft,
`kody-issue-triage ${outcome}: ${truncate(summary, 200)}`,
)
triaged = applied.updated
}
return {
ok: true,
fingerprint,
outcome,
triaged,
discordMessageId,
}
}