export const SELF_KODY_ID = 'friction-log' export const owner = 'kody-hq' export const repo = 'kody' export const repoSlug = `${owner}/${repo}` export const repository = `https://github.com/${repoSlug}` export const ref = 'main' export const timezone = 'America/Denver' export const frictionLabel = 'friction' export const skippedLabel = 'friction-skipped' export const skippedLabelDescription = 'Daily sweep will not re-investigate until this label is removed.' export const skippedLabelColor = '6B5D52' export const maxIssuesInPrompt = 10 export const agentModel = { id: 'grok-4.6', params: [ { id: 'effort', value: 'high' }, { id: 'fast', value: 'true' }, ], } as const export type IssueComment = { id: number userLogin: string createdAt: string body: string } export type FrictionIssue = { number: number title: string body: string htmlUrl: string userLogin: string createdAt: string updatedAt: string labels: Array comments: Array } export type EligibilityReason = 'open' | 'skipped' export type Eligibility = { eligible: boolean reason: EligibilityReason } export type SweepRecord = { day: string scanned_at: string eligible_count: number issue_numbers_json: string spawned: number agent_id: string | null agent_url: string | null skipped: string | null dry_run: number } export type OutcomeRecord = { agent_id: string day: string outcome: string summary: string pr_url: string | null completed_at: string } export function truncate(text: unknown, maxLength: number) { const value = String(text ?? '') return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}…` } /** * Neutralize untrusted text before it goes into an outbound Cursor prompt. * * Kody expands `{{secret:…}}` placeholders in outbound request bodies at fetch * time, so a literal placeholder quoted in an issue would otherwise trigger a * host-approval error for api.cursor.com (or, if approved, leak that secret * into the agent prompt). Breaking the double brace keeps the text readable * without being a placeholder. */ export function sanitizeForPrompt(text: unknown) { return String(text ?? '') .replaceAll('`', 'ˋ') .replaceAll('«', '<') .replaceAll('»', '>') .replace(/\{\{(\s*secret\s*:)/gi, '{ {$1') } export function agentUrlOf(agent: { id?: string; url?: string } | null) { if (!agent) return null return agent.url || (agent.id ? `https://cursor.com/agents/${agent.id}` : null) } export function denverDayKey(now = new Date()) { return new Intl.DateTimeFormat('en-CA', { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit', }).format(now) } export function normalizeFrictionTitle(title: string) { const cleaned = String(title ?? '').trim().replace(/\s+/g, " ") if (!cleaned) return '' if (/^friction:\s*/i.test(cleaned)) { return `Friction: ${cleaned.replace(/^friction:\s*/i, "")}` } return `Friction: ${cleaned}` } export const preliminaryInvestigationMaxChars = 1200 export const relevantFilesMaxCount = 20 export function normalizeRelevantFiles( files: Array | undefined, ): Array { if (!Array.isArray(files)) return [] const seen = new Set() const out: Array = [] for (const entry of files) { const path = String(entry ?? '').trim() if (!path || seen.has(path)) continue seen.add(path) out.push(path) if (out.length >= relevantFilesMaxCount) break } return out } /** * Build the friction issue / reuse-comment body from structured fields. * When `body` is set, it wins as-is. Otherwise sections are joined; include a * short preliminary investigation (what was already looked at) and optional * relevant files. Do not invent a root cause. Omit the files list when empty. */ export function buildFrictionBody(input: { body?: string whatHappened?: string whatYouWanted?: string howToReproduce?: string cost?: string preliminaryInvestigation?: string relevantFiles?: Array }) { const provided = String(input.body ?? '').trim() if (provided) return provided const parts: string[] = [] const sections = [ ['What happened', input.whatHappened], ['What you wanted', input.whatYouWanted], ['How to reproduce', input.howToReproduce], ['Cost', input.cost], ] as const for (const [heading, value] of sections) { const text = String(value ?? '').trim() if (!text) continue parts.push(`## ${heading}\n\n${text}`) } const investigation = truncate( String(input.preliminaryInvestigation ?? '').trim(), preliminaryInvestigationMaxChars, ).trim() const files = normalizeRelevantFiles(input.relevantFiles) if (investigation || files.length > 0) { const block = ['## Preliminary investigation', ''] if (investigation) block.push(investigation) if (files.length > 0) { if (investigation) block.push('') block.push('Relevant files:') for (const file of files) { block.push(`- \`${file}\``) } } parts.push(block.join('\n')) } return parts.join('\n\n') } function normalizeBodyForCompare(text: unknown) { return String(text ?? '') .replace(/\r\n/g, '\n') .replace(/\s+/g, ' ') .trim() } /** * True when a reuse comment would only repeat text the issue already has * (same body as the issue itself). Re-filing the identical papercut adds no * new information, so `./file` skips the "Seen again" comment. */ export function isSameFrictionBody(newBody: unknown, existingBody: unknown) { const next = normalizeBodyForCompare(newBody) if (!next) return false return next === normalizeBodyForCompare(existingBody) } export type ReuseCommentSkipped = 'same-as-issue-body' | 'disabled' | 'empty-body' /** * Decide whether a reuse should skip its "Seen again" comment. Returns the * reason to skip, or undefined to post the comment. */ export function reuseCommentSkipReason(input: { commentOnReuse: boolean body: string sameBodyAsExisting: boolean }): ReuseCommentSkipped | undefined { if (!input.commentOnReuse) return 'disabled' if (!String(input.body ?? '').trim()) return 'empty-body' if (input.sameBodyAsExisting) return 'same-as-issue-body' return undefined }