Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened | 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(review|reviewing|reviews)\b.*\b(pull requests?|prs?|changes?|code)\b|\b(pull requests?|prs?)\b.*\breview/i, | |
| 36 | routine: (duty) => ({ | |
| 37 | name: "Review pull requests", | |
| 38 | instructions: `When a pull request is ready for review, review it (${trimmed(duty)}). Read the change and its checks, then post a short review: what it changes, the risks, which tests cover it and what they miss, and a verdict: looks good, or what to fix first. Link the pull request.`, | |
| 39 | schedule: null, | |
| 40 | events: ["pull_ready"], | |
| 41 | repos: [], | |
| 42 | }), | |
| 43 | }, | |
| 44 | { | |
| 45 | match: /\b(flaky|failing|broken)\b.*\b(checks?|tests?|builds?|ci)\b|\b(checks?|ci|builds?)\b.*\b(flaky|fail)/i, | |
| 46 | routine: (duty) => ({ | |
| 47 | name: "Chase failing checks", | |
| 48 | 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.`, | |
| 49 | schedule: null, | |
| 50 | events: ["checks_failed"], | |
| 51 | repos: [], | |
| 52 | }), | |
| 53 | }, | |
| 54 | { | |
| 55 | match: /\btest plans?\b|\bnew features?\b.*\btest/i, | |
| 56 | routine: (duty) => ({ | |
| 57 | name: "Test plans for new work", | |
| 58 | 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. Skip bug reports and questions.`, | |
| 59 | schedule: null, | |
| 60 | events: ["issue_opened"], | |
| 61 | repos: [], | |
| 62 | }), | |
| 63 | }, | |
| 64 | { | |
| 65 | match: /\b(triage|triaging)\b|\bincoming (issues|bugs|requests)\b/i, | |
| 66 | routine: (duty) => ({ | |
| 67 | name: "Triage new issues", | |
| 68 | 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.`, | |
| 69 | schedule: null, | |
| 70 | events: ["issue_opened"], | |
| 71 | repos: [], | |
| 72 | }), | |
| 73 | }, | |
| 74 | { | |
| 75 | match: /\b(deploys?|deployments?|incidents?|on-?call|outages?|rollbacks?)\b/i, | |
| 76 | routine: (duty) => ({ | |
| 77 | name: "Watch deploys", | |
| 78 | 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.`, | |
| 79 | schedule: null, | |
| 80 | events: ["deploy_failed"], | |
| 81 | repos: [], | |
| 82 | }), | |
| 83 | }, | |
| 84 | { | |
| 85 | match: /\b(release notes|changelog|what shipped|weekly (summary|update|digest)|digests?)\b/i, | |
| 86 | routine: (duty) => ({ | |
| 87 | name: "Weekly summary", | |
| 88 | 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.`, | |
| 89 | schedule: { every: "week", minute: 0, hour: 16, weekday: 5 }, | |
| 90 | events: [], | |
| 91 | repos: [], | |
| 92 | }), | |
| 93 | }, | |
| 94 | { | |
| 95 | match: /\b(support|customers?|tickets?|complaints?|feedback)\b/i, | |
| 96 | routine: (duty) => ({ | |
| 97 | name: "Support themes", | |
| 98 | 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.`, | |
| 99 | schedule: { every: "weekday", minute: 0, hour: 9, weekday: 1 }, | |
| 100 | events: [], | |
| 101 | repos: [], | |
| 102 | }), | |
| 103 | }, | |
| 104 | ]; | |
| 105 | ||
| 106 | /** | |
| 107 | * Routines for an agent's responsibilities that it doesn't have yet: one | |
| 108 | * per kind, skipping those whose name it already uses or whose events its | |
| 109 | * routines already run on. | |
| 110 | */ | |
| 111 | export function suggestRoutines(responsibilities: string[], existing: { name: string; events: RoutineEvent[] }[]): RoutineSuggestion[] { | |
| 112 | const out: RoutineSuggestion[] = []; | |
| 113 | const taken = new Set(existing.map((r) => r.name.toLowerCase())); | |
| 114 | const covered = new Set(existing.flatMap((r) => r.events)); | |
| 115 | for (const duty of responsibilities) { | |
| 116 | const rule = RULES.find((r) => r.match.test(duty)); | |
| 117 | if (!rule) continue; | |
| 118 | const routine = rule.routine(duty); | |
| 119 | if (taken.has(routine.name.toLowerCase())) continue; | |
| 120 | if (routine.events?.length && routine.events.every((e) => covered.has(e))) continue; | |
| 121 | taken.add(routine.name.toLowerCase()); | |
| 122 | out.push({ responsibility: duty, routine }); | |
| 123 | } | |
| 124 | return out; | |
| 125 | } |