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.
| Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked | 1 | /** |
| 2 | * When routines run, and what a routine must have: pure, so it is tested | |
| 3 | * on its own (routines.ts runs them). | |
| 4 | */ | |
| Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened | 5 | import type { NewRoutine, RoutineEvent, RoutineSchedule } from "@g1t/contracts"; |
| 6 | ||
| 7 | import { EVENT_KEYS } from "./suggest.ts"; | |
| Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked | 8 | |
| 9 | const EVERY = ["hour", "day", "weekday", "week"] as const; | |
| 10 | ||
| 11 | /** A schedule as given, checked; a message when it isn't one. */ | |
| 12 | export function checkSchedule(input: unknown): { ok: true; value: RoutineSchedule } | { ok: false; message: string } { | |
| 13 | if (!input || typeof input !== "object") return { ok: false, message: "Say when it runs." }; | |
| 14 | const s = input as Record<string, unknown>; | |
| 15 | const every = EVERY.find((e) => e === s.every); | |
| 16 | if (!every) return { ok: false, message: "A routine runs every hour, day, weekday or week." }; | |
| 17 | const int = (value: unknown, min: number, max: number) => (typeof value === "number" && Number.isInteger(value) && value >= min && value <= max ? value : null); | |
| 18 | const minute = int(s.minute ?? 0, 0, 59); | |
| 19 | const hour = int(s.hour ?? 9, 0, 23); | |
| 20 | const weekday = int(s.weekday ?? 1, 0, 6); | |
| 21 | if (minute === null || hour === null || weekday === null) return { ok: false, message: "The minute is 0 to 59, the hour 0 to 23 and the day 0 (Sunday) to 6." }; | |
| 22 | return { ok: true, value: { every, minute, hour, weekday } }; | |
| 23 | } | |
| 24 | ||
| 25 | /** When a routine next runs after `after`, in UTC. */ | |
| 26 | export function nextRun(schedule: RoutineSchedule, after: Date): Date { | |
| 27 | const next = new Date(after.getTime()); | |
| 28 | next.setUTCSeconds(0, 0); | |
| 29 | if (schedule.every === "hour") { | |
| 30 | next.setUTCMinutes(schedule.minute); | |
| 31 | if (next <= after) next.setUTCHours(next.getUTCHours() + 1); | |
| 32 | return next; | |
| 33 | } | |
| 34 | next.setUTCHours(schedule.hour, schedule.minute); | |
| 35 | if (next <= after) next.setUTCDate(next.getUTCDate() + 1); | |
| 36 | for (let i = 0; i < 8; i++) { | |
| 37 | const day = next.getUTCDay(); | |
| 38 | const fits = schedule.every === "day" || (schedule.every === "weekday" ? day >= 1 && day <= 5 : day === schedule.weekday); | |
| 39 | if (fits) return next; | |
| 40 | next.setUTCDate(next.getUTCDate() + 1); | |
| 41 | } | |
| 42 | return next; | |
| 43 | } | |
| 44 | ||
| 45 | /** A schedule in words: "Every weekday at 09:00 UTC". */ | |
| 46 | export function describeSchedule(schedule: RoutineSchedule): string { | |
| 47 | const pad = (n: number) => String(n).padStart(2, "0"); | |
| 48 | const at = `${pad(schedule.hour)}:${pad(schedule.minute)} UTC`; | |
| 49 | const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; | |
| 50 | switch (schedule.every) { | |
| 51 | case "hour": | |
| 52 | return `Every hour at :${pad(schedule.minute)}`; | |
| 53 | case "day": | |
| 54 | return `Every day at ${at}`; | |
| 55 | case "weekday": | |
| 56 | return `Every weekday at ${at}`; | |
| 57 | case "week": | |
| 58 | return `Every ${days[schedule.weekday]} at ${at}`; | |
| 59 | } | |
| 60 | } | |
| 61 | ||
| Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened | 62 | export type CheckedRoutine = NewRoutine & { schedule: RoutineSchedule | null; events: RoutineEvent[]; repos: string[] }; |
| 63 | ||
| 64 | /** A routine as given, checked: a name, instructions, and a schedule, events, or both. */ | |
| 65 | export function checkRoutine(input: NewRoutine): { ok: true; value: CheckedRoutine } | { ok: false; message: string } { | |
| Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked | 66 | const name = typeof input?.name === "string" ? input.name.trim().slice(0, 80) : ""; |
| 67 | const instructions = typeof input?.instructions === "string" ? input.instructions.trim().slice(0, 8000) : ""; | |
| 68 | if (!name) return { ok: false, message: "Give the routine a name." }; | |
| 69 | if (instructions.length < 10) return { ok: false, message: "Say what the routine does, in a sentence or more." }; | |
| 70 | if (typeof input.channel_id !== "string" || !input.channel_id) return { ok: false, message: "Choose the channel it posts in." }; | |
| Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened | 71 | const given = Array.isArray(input.events) ? input.events : []; |
| 72 | if (given.some((e) => !EVENT_KEYS.includes(e))) return { ok: false, message: "That isn't something a routine can run on." }; | |
| 73 | const events = [...new Set(given)]; | |
| 74 | const repos = [...new Set((Array.isArray(input.repos) ? input.repos : []).filter((r): r is string => typeof r === "string").map((r) => r.trim().toLowerCase()).filter(Boolean))]; | |
| 75 | if (repos.some((r) => !/^[a-z0-9._-]+\/[a-z0-9._-]+$/.test(r))) return { ok: false, message: "Name repositories as workspace/name." }; | |
| 76 | if (repos.length > 20) return { ok: false, message: "A routine follows at most 20 repositories; leave it empty for all of them." }; | |
| 77 | let schedule: RoutineSchedule | null = null; | |
| 78 | if (input.schedule) { | |
| 79 | const checked = checkSchedule(input.schedule); | |
| 80 | if (!checked.ok) return checked; | |
| 81 | schedule = checked.value; | |
| 82 | } | |
| 83 | if (!schedule && !events.length) return { ok: false, message: "A routine runs on a schedule, when something happens, or both." }; | |
| 84 | return { ok: true, value: { ...input, name, instructions, schedule, events, repos, enabled: input.enabled !== false } }; | |
| Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked | 85 | } |