← Public packages
@kentcdodds/sentry-triage
Sentry triage wakes Cole (Grok Bot) per issue: one active wake per repo (lease + queue), loop-safe Discord. Cole may spawn Cursor for isolated repo work.
src/types.ts
401 lines · 9.3 KB · TypeScript/** Terminal / in-progress statuses stored on a triage issue record. */
export type TriageOutcome =
| 'fixed'
| 'filtered'
| 'ignored'
| 'recommendation'
| 'loop_detected'
| 'failed'
export type TriageIssueRecord = {
issueId?: string
projectSlug?: string
shortId?: string | null
title?: string
link?: string
status?: string
summary?: string
prUrl?: string | null
mergeCommitUrl?: string | null
deployUrl?: string | null
activityUrl?: string | null
cardStage?: string | null
agentId?: string
agentUrl?: string | null
discordMessageId?: string
seenAt?: string
spawnedAt?: string
completedAt?: string
hourBucket?: string
backfill?: boolean
[key: string]: unknown
}
export type GetIssueStateInput = {
/** Sentry issue id (numeric string). */
issueId: string
}
export type GetIssueStateResult = {
record: TriageIssueRecord | null
spawnedThisHour: number
issuesSeenThisHour: number
capRemaining: number
}
export type RecordOutcomeInput = {
/** Sentry issue id (numeric string). */
issueId: string
/** Final triage outcome. */
outcome: TriageOutcome
/** Short human summary written into Discord. Prefer title/context/recommendation/options for `recommendation`. */
summary?: string
/**
* Glanceable decision title for `recommendation` Discord. Recovery stubs
* still use this as the Sentry issue title when the spawn record is missing.
*/
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
/** PR URL when outcome is `fixed`. */
prUrl?: string
/** Merge commit URL after the PR lands (kept on the Discord card). */
mergeCommitUrl?: string
/** Deploy / CI job URL after production ships. */
deployUrl?: string
/**
* Recovery only: Discord status message id from the agent prompt when the
* spawn record is missing from package storage.
*/
discordMessageId?: string
/** Recovery metadata used with `discordMessageId` when seeding a stub record. */
projectSlug?: string
shortId?: string
link?: string
agentUrl?: string
}
export type RecordOutcomeResult = {
ok: true
issueId: string
outcome: TriageOutcome
/** Present when a per-repo queue flush was attempted after releasing the lease. */
queueFlush?: {
ok?: boolean
flushed?: number
agentId?: string
issueIds?: string[]
skipped?: string
error?: string
projectSlug?: string
[key: string]: unknown
}
}
export type ResetIssueInput = {
/** Sentry issue id (numeric string). */
issueId: string
}
export type ResetIssueResult = {
ok: true
issueId: string
hadRecord: boolean
}
export type ReconcileInput = {
/** How far back to look for missed issues (hours, 1–72; default 30). */
lookbackHours?: number
}
export type SweepStaleAwaitingInput = {
/** Max stale awaiting records to redeliver (1–20; default 8). */
limit?: number
/** Preview stale records without dispatching rescue workflows. */
dryRun?: boolean
}
export type SweepStaleAwaitingAction = {
issueId: string
shortId?: string | null
projectSlug: string
status?: string
delivery?: unknown
error?: string
}
export type SweepStaleAwaitingResult = {
ok: true
dryRun?: boolean
swept: number
actions: SweepStaleAwaitingAction[]
}
export type ReconcileResult = {
ok: true
lookbackHours: number
reconciled: number
actions: Array<{
project: string
issueId?: string
shortId?: string | null
reason?: 'missing-record' | 'awaiting-seer-stale' | 'spawn-failed-stale'
delivery?: unknown
error?: string
}>
}
export type FlushQueueInput = {
/** Triage project slug (see `triageProjects` in shared.ts). */
projectSlug: string
}
export type FlushQueueResult = {
ok: boolean
projectSlug: string
flushed?: number
agentId?: string
issueIds?: string[]
skipped?: string
error?: string
[key: string]: unknown
}
export type TriageReportResult = {
count: number
records: TriageIssueRecord[]
counters: unknown
debugShape?: string
/** Snapshot of `repo_leases` rows when the table exists. */
repoLeases?: unknown
/** Per-project queue depths from `repo_queue` when the table exists. */
repoQueue?: unknown
}
export type SentryWebhookBackfillSibling = {
id: string
shortId?: string | null
title?: string
culprit?: string
permalink?: string | null
}
/** Sentry Seer autofix root-cause analysis (from seer.root_cause_completed). */
export type SeerRootCause = {
one_line_description?: string | null
five_whys?: string[]
reproduction_steps?: string[]
relevant_repo?: string | null
fixability?: string | null
runId?: string | number | null
}
export type SentryWebhookPayload = {
action?: string
data?: {
issue?: {
id: string | number
shortId?: string | null
title?: string
culprit?: string | null
level?: string
permalink?: string | null
project?: { slug?: string; id?: string | number } | null
}
event?: {
issue_id?: string | number
title?: string
message?: string
culprit?: string | null
level?: string
web_url?: string | null
project?: string | number
}
/** Present on Seer lifecycle webhooks (same ingress URL). */
group_id?: string | number
run_id?: string | number
root_cause?: SeerRootCause
pull_requests?: Array<{
pull_request?: { pr_number?: number; pr_url?: string; pr_id?: number }
repo_name?: string
provider?: string
}>
}
/** Direct-invocation only; Sentry never sends this key. */
kodyBackfill?: {
siblings?: Array<{
id?: string | number
shortId?: string | null
title?: string
culprit?: string
permalink?: string | null
}>
note?: string
}
/** Direct-invocation only; Sentry never sends this key. */
kodyDryRun?: boolean
/** Internal sweeper-rescue redelivery marker; bounds recursion to depth one. */
kodySweeperRescue?: boolean
}
export type HandleSentryWebhookInput = {
request?: {
json?: SentryWebhookPayload
headers?: Record<string, string | undefined>
}
}
/**
* Staged webhook body written to `packageStorage` before `workflows.create`.
* The durable processor loads via `payloadKey` so workflow params stay tiny.
*/
export type StagedWebhookPayload = {
headers?: Record<string, string | undefined>
json?: SentryWebhookPayload
stagedAt?: string
}
/**
* Durable processor input. Workflow dispatches pass a bounded storage
* reference (`payloadKey` + identity fields); direct / dry-run invocations
* still use the full `request` envelope.
*/
export type ProcessSentryWebhookInput = HandleSentryWebhookInput & {
/** packageStorage key for a staged webhook body (`webhook-payload:…`). */
payloadKey?: string
issueId?: string
resource?: string | null
action?: string | null
}
export type ProcessSentryWebhookResult =
| {
ok: true
skipped: string
resource?: string | null
action?: string | null
issueId?: string
projectSlug?: string | null
issuesSeen?: number
spawned?: number
}
| {
ok: true
issueId: string
projectSlug: string
agentId: string
}
| {
ok: true
queued: true
issueId: string
projectSlug: string
}
| {
ok: true
dryRun: true
issueId: string
projectSlug: string
seerFirstEnabled: boolean
existingStatus: string | null
leaseHeld: boolean
queueDepth: number
wouldDeferToSeer: boolean
seerAutomationOwnsRun?: boolean
}
| {
ok: true
issueId: string
projectSlug: string
deferredToSeer: true
seerRunId?: string | number | null
}
| {
ok: true
issueId: string
projectSlug: string
deferredToSeerAutomation: true
}
| SeerWebhookResult
export type HandleSentryWebhookResult =
| {
ok: true
skipped: string
resource?: string | null
action?: string | null
issueId?: string
projectSlug?: string | null
issuesSeen?: number
spawned?: number
}
| {
ok: true
accepted: true
dispatched: true
issueId?: string
resource?: string | null
action?: string | null
workflowId: string
workflowStatus?: string | null
workflowName?: string
idempotencyKey: string
exportName: './process-sentry-webhook'
}
| ProcessSentryWebhookResult
export type SeerWebhookPayload = {
action?: string
data?: {
run_id?: string | number
group_id?: string | number
root_cause?: SeerRootCause
pull_requests?: Array<{
pull_request?: { pr_number?: number; pr_url?: string; pr_id?: number }
repo_name?: string
provider?: string
}>
}
/** Direct-invocation only; Sentry never sends this key. */
kodyDryRun?: boolean
}
export type HandleSeerWebhookInput = {
request?: {
json?: SeerWebhookPayload
headers?: Record<string, string | undefined>
}
}
export type SeerWebhookResult = {
ok: boolean
action?: string | null
skipped?: string
issueId?: string
projectSlug?: string | null
agentId?: string
seededBy?: string
seerPrUrls?: string[]
dryRun?: boolean
error?: string
[key: string]: unknown
}
export type DescribeSentryTriageResult = {
name: string
webhooks: string[]
projects: Array<{
sentryProject: string
repository: string
}>
exports: Record<string, string>
discordChannel: string
}