| 1 | /** |
| 2 | * What a routine can run on, in words, and routines that fit an agent's |
| 3 | * responsibilities, for owners to add in one step: Margo's "Reviewing pull |
| 4 | * requests for risk and test coverage" runs when a pull request is ready |
| 5 | * for review. Matched on words, never by a model, so it is instant and the |
| 6 | * same every time. Pure, so it is tested on its own. |
| 7 | */ |
| 8 | import type { NewRoutine, RoutineEvent, RoutineSuggestion } from "@g1t/contracts"; |
| 9 | |
| 10 | /** The events a routine can run on, by key. */ |
| 11 | export const EVENT_KEYS: RoutineEvent[] = ["pull_ready", "pull_merged", "checks_failed", "issue_opened", "deploy_failed"]; |
| 12 | |
| 13 | const WORDS: Record<RoutineEvent, string> = { |
| 14 | pull_ready: "a pull request is ready for review", |
| 15 | pull_merged: "a pull request is merged", |
| 16 | checks_failed: "checks fail on a pull request", |
| 17 | issue_opened: "an issue is opened", |
| 18 | deploy_failed: "a deploy fails", |
| 19 | }; |
| 20 | |
| 21 | /** A routine's events in words: "When a pull request is ready for review or checks fail on a pull request". */ |
| 22 | export function describeEvents(events: RoutineEvent[]): string { |
| 23 | const list = events.map((e) => WORDS[e]).filter(Boolean); |
| 24 | if (!list.length) return ""; |
| 25 | const joined = list.length === 1 ? list[0] : `${list.slice(0, -1).join(", ")} or ${list.at(-1)}`; |
| 26 | return `When ${joined}`; |
| 27 | } |
| 28 | |
| 29 | const trimmed = (duty: string) => duty.trim().replace(/\.$/, ""); |
| 30 | |
| 31 | type Rule = { match: RegExp; routine: (duty: string) => Omit<NewRoutine, "channel_id"> }; |
| 32 | |
| 33 | const RULES: Rule[] = [ |
| 34 | { |
| 35 | match: /\b(docs|documentation|pages|runbooks?)\b.*\b(change|changes|merge|merges|wrong|current|up to date|stale)\b/i, |
| 36 | routine: (duty) => ({ |
| 37 | name: "Keep the docs current", |
| 38 | instructions: `When a pull request is merged, keep the docs true (${trimmed(duty)}). Find the pages it makes wrong or incomplete: stale_pages for this repository first (pages citing code it changed), then search_docs for what it changed. Update each with edit_page (it becomes a suggestion where you can't edit), citing the pull request, with marks_current when it brings a stale page up to date. Post a short list of what you changed here; say so if nothing needed changing.`, |
| 39 | schedule: null, |
| 40 | events: ["pull_merged"], |
| 41 | repos: [], |
| 42 | }), |
| 43 | }, |
| 44 | { |
| 45 | match: /\b(review|reviewing|reviews)\b.*\b(pull requests?|prs?|changes?|code)\b|\b(pull requests?|prs?)\b.*\breview/i, |
| 46 | routine: (duty) => ({ |
| 47 | name: "Review pull requests", |
| 48 | instructions: `When a pull request is ready for review, review it (${trimmed(duty)}). Read the change and its checks, then post your review on the pull request with review_pull: what it changes, the risks, which tests cover it and what they miss, and a verdict (approve, or request changes with what to fix first). Then post a two-line summary here with a link.`, |
| 49 | schedule: null, |
| 50 | events: ["pull_ready"], |
| 51 | repos: [], |
| 52 | }), |
| 53 | }, |
| 54 | { |
| 55 | match: /\b(flaky|failing|broken)\b.*\b(checks?|tests?|builds?|ci)\b|\b(checks?|ci|builds?)\b.*\b(flaky|fail)/i, |
| 56 | routine: (duty) => ({ |
| 57 | name: "Chase failing checks", |
| 58 | instructions: `When checks fail on a pull request, look into it (${trimmed(duty)}). Read the failure, say whether it looks real or flaky and why, and what to do next. If a flaky check keeps coming back, offer to file an issue.`, |
| 59 | schedule: null, |
| 60 | events: ["checks_failed"], |
| 61 | repos: [], |
| 62 | }), |
| 63 | }, |
| 64 | { |
| 65 | match: /\btest plans?\b|\bnew features?\b.*\btest/i, |
| 66 | routine: (duty) => ({ |
| 67 | name: "Test plans for new work", |
| 68 | instructions: `When an issue is opened for new work, draft a test plan (${trimmed(duty)}): what to test, the edge cases, and what can be automated. Post it on the issue with comment, and a one-line note here. Skip bug reports and questions.`, |
| 69 | schedule: null, |
| 70 | events: ["issue_opened"], |
| 71 | repos: [], |
| 72 | }), |
| 73 | }, |
| 74 | { |
| 75 | match: /\b(triage|triaging)\b|\bincoming (issues|bugs|requests)\b/i, |
| 76 | routine: (duty) => ({ |
| 77 | name: "Triage new issues", |
| 78 | instructions: `When an issue is opened, triage it (${trimmed(duty)}): what it is (bug, feature, question), how urgent, which area of the code it touches, and who should own it.`, |
| 79 | schedule: null, |
| 80 | events: ["issue_opened"], |
| 81 | repos: [], |
| 82 | }), |
| 83 | }, |
| 84 | { |
| 85 | match: /\b(deploys?|deployments?|incidents?|on-?call|outages?|rollbacks?)\b/i, |
| 86 | routine: (duty) => ({ |
| 87 | name: "Watch deploys", |
| 88 | instructions: `When a deploy fails, find out why (${trimmed(duty)}): what failed, what changed since the last good deploy, and whether to retry, roll back or fix forward.`, |
| 89 | schedule: null, |
| 90 | events: ["deploy_failed"], |
| 91 | repos: [], |
| 92 | }), |
| 93 | }, |
| 94 | { |
| 95 | match: /\b(release notes|changelog|what shipped|weekly (summary|update|digest)|digests?)\b/i, |
| 96 | routine: (duty) => ({ |
| 97 | name: "Weekly summary", |
| 98 | instructions: `Every Friday (${trimmed(duty)}): what merged and shipped this week, what's still open and what's at risk, in a short post people can skim.`, |
| 99 | schedule: { every: "week", minute: 0, hour: 16, weekday: 5 }, |
| 100 | events: [], |
| 101 | repos: [], |
| 102 | }), |
| 103 | }, |
| 104 | { |
| 105 | match: /\b(support|customers?|tickets?|complaints?|feedback)\b/i, |
| 106 | routine: (duty) => ({ |
| 107 | name: "Support themes", |
| 108 | instructions: `Every weekday morning (${trimmed(duty)}): read yesterday's conversations you can see, group questions and complaints into themes, and post the top ones with how often each came up and whether an issue already covers it.`, |
| 109 | schedule: { every: "weekday", minute: 0, hour: 9, weekday: 1 }, |
| 110 | events: [], |
| 111 | repos: [], |
| 112 | }), |
| 113 | }, |
| 114 | ]; |
| 115 | |
| 116 | /** |
| 117 | * Routines for an agent's responsibilities that it doesn't have yet: one |
| 118 | * per kind, skipping those whose name it already uses or whose events its |
| 119 | * routines already run on. |
| 120 | */ |
| 121 | export function suggestRoutines(responsibilities: string[], existing: { name: string; events: RoutineEvent[] }[]): RoutineSuggestion[] { |
| 122 | const out: RoutineSuggestion[] = []; |
| 123 | const taken = new Set(existing.map((r) => r.name.toLowerCase())); |
| 124 | const covered = new Set(existing.flatMap((r) => r.events)); |
| 125 | for (const duty of responsibilities) { |
| 126 | const rule = RULES.find((r) => r.match.test(duty)); |
| 127 | if (!rule) continue; |
| 128 | const routine = rule.routine(duty); |
| 129 | if (taken.has(routine.name.toLowerCase())) continue; |
| 130 | if (routine.events?.length && routine.events.every((e) => covered.has(e))) continue; |
| 131 | taken.add(routine.name.toLowerCase()); |
| 132 | out.push({ responsibility: duty, routine }); |
| 133 | } |
| 134 | return out; |
| 135 | } |