// ═══════════════════════════════════════════════════════════════════ // YOU WRITE THIS FILE. These four functions shape the bot's entire // behavior. Defaults below are safe and intentionally conservative — // redline to taste. Each function is ~5-10 lines. // ═══════════════════════════════════════════════════════════════════ import type { CycleContext, CycleResult, Gap, Proposal } from "./types.ts"; // Should this cycle even fire right now? // Called AFTER the pause-file check + cost-cap check have already passed. // Use this for judgment calls (cooldown, shared-resource conflicts). export function shouldRunCycle(ctx: CycleContext): { run: boolean; reason: string } { if (ctx.workingTreeDirty) { return { run: false, reason: "dirty working tree — commit or stash your work first" }; } if (ctx.lastCycleAt) { const minsAgo = (Date.now() - new Date(ctx.lastCycleAt).getTime()) / 60000; if (minsAgo < 10) return { run: false, reason: `last cycle ${minsAgo.toFixed(1)}min ago (<10min cooldown)` }; } if (ctx.autotuneBusy) { return { run: false, reason: "autotune agent is running — avoid Ollama contention" }; } return { run: true, reason: "ready" }; } // Which [bot-eligible] gap from the PRD does this cycle attack? // Default: first in document order. Alternatives: random for exploration, // smallest-context, tag-priority (e.g. [bot-eligible:high] wins). export function pickGap(gaps: Gap[]): { gap: Gap | null; reason: string } { if (gaps.length === 0) return { gap: null, reason: "no [bot-eligible] gaps in PRD" }; return { gap: gaps[0], reason: "first eligible gap in document order" }; } // Cheap gate BEFORE burning test cycles. Reject if proposal smells wrong. // This catches runaway cloud proposals before we apply+test them. export function scoreProposal(p: Proposal): { accept: boolean; reason: string } { if (p.files.length === 0) return { accept: false, reason: "empty proposal" }; if (p.files.length > 5) return { accept: false, reason: `>5 files (${p.files.length})` }; if (p.estimated_loc > 200) return { accept: false, reason: `>200 LOC (${p.estimated_loc})` }; // Paths the bot is NEVER allowed to touch. Extend cautiously. const forbidden = [ ".git/", "secrets", "lakehouse.toml", "docs/ADR-", "docs/DECISIONS.md", "docs/PRD.md", "/etc/", "/root/", "Cargo.lock", ]; for (const f of p.files) { const hit = forbidden.find(prefix => f.path.includes(prefix)); if (hit) return { accept: false, reason: `touches forbidden path (${hit}): ${f.path}` }; } return { accept: true, reason: "passes size + path filters" }; } // Tests are green and changes applied. Do we actually open a PR? // Mem0-aligned: the cycle already short-circuited with `cycle_noop` if // every file was identical to current state, so by the time we're here // at least one file was actually added or updated. // // Stricter options below — uncomment to require a test file touched, etc. export function shouldOpenPR(c: CycleResult): { open: boolean; reason: string } { if (!c.testsGreen) return { open: false, reason: "tests red" }; const realChanges = c.filesAdded.length + c.filesUpdated.length; if (realChanges === 0) return { open: false, reason: "nothing actually changed on disk" }; // Stricter option — require at least one test file in the diff: // const hasTest = [...c.filesAdded, ...c.filesUpdated].some(f => /test|_tests/i.test(f)); // if (!hasTest) return { open: false, reason: "no test file touched" }; return { open: true, reason: `green + ${c.filesAdded.length} add / ${c.filesUpdated.length} update` }; }