harness_loop_engine/
patterns.rs1use crate::budget::TokenBudget;
23use crate::level::LoopLevel;
24use crate::spec::LoopSpec;
25
26pub fn daily_triage() -> LoopSpec {
29 LoopSpec::new(
30 "daily-triage",
31 "Surface anything in the project that needs human attention today.",
32 LoopLevel::L1Report,
33 )
34 .with_cadence("daily 09:00")
35 .with_budget(TokenBudget::iters(10))
36 .with_action_kind("report")
37 .with_maker_prompt(
38 "Review recent activity (open issues, failing checks, stale branches, \
39 TODOs) and list the few items that most need attention today, with a \
40 one-line reason each. Be terse.",
41 )
42 .with_checker_prompt(
43 "Confirm each surfaced item is real and not already resolved. Drop \
44 anything stale.",
45 )
46}
47
48pub fn pr_babysitter() -> LoopSpec {
52 LoopSpec::new(
53 "pr-babysitter",
54 "Keep open PRs moving; flag any that are stuck.",
55 LoopLevel::L1Report,
56 )
57 .with_cadence("every 10m")
58 .with_budget(TokenBudget::iters(8))
59 .with_action_kind("comment")
60 .with_maker_prompt(
61 "For each open PR, determine if it is blocked (red CI, conflicts, \
62 unanswered review, idle > 24h). List only the blocked ones and the \
63 single next action each needs.",
64 )
65 .with_checker_prompt("Verify each flagged PR is genuinely blocked right now.")
66}
67
68pub fn ci_sweeper() -> LoopSpec {
72 LoopSpec::new(
73 "ci-sweeper",
74 "Keep the default branch green by proposing fixes for CI failures.",
75 LoopLevel::L2Assisted,
76 )
77 .with_cadence("every 10m")
78 .with_budget(TokenBudget::iters(20).with_max_total_tokens(400_000))
79 .with_action_kind("apply-patch")
80 .with_maker_prompt(
81 "If CI is failing, reproduce the failure, find the root cause, and \
82 make the smallest change that fixes it. If you cannot fix it \
83 confidently, explain what you found and stop.",
84 )
85 .with_checker_prompt(
86 "Run the build and the failing tests. Confirm they now pass and that \
87 nothing else regressed. Report DoneWithConcerns if the fix looks \
88 risky or broad.",
89 )
90}
91
92pub fn dependency_sweeper() -> LoopSpec {
95 LoopSpec::new(
96 "dependency-sweeper",
97 "Keep dependencies current via small, verified update PRs.",
98 LoopLevel::L2Assisted,
99 )
100 .with_cadence("daily 04:00")
101 .with_budget(TokenBudget::iters(16).with_max_total_tokens(300_000))
102 .with_action_kind("open-pr")
103 .with_maker_prompt(
104 "Identify outdated dependencies with low-risk updates (patch/minor). \
105 Update them and adjust any code the update requires. One coherent \
106 batch only.",
107 )
108 .with_checker_prompt(
109 "Build and test against the updated dependencies. Confirm green. Flag \
110 any major-version or behaviour-changing update for human review.",
111 )
112}
113
114pub fn changelog_drafter() -> LoopSpec {
117 LoopSpec::new(
118 "changelog-drafter",
119 "Draft accurate, readable changelog entries from recent changes.",
120 LoopLevel::L1Report,
121 )
122 .with_cadence("daily 18:00")
123 .with_budget(TokenBudget::iters(10))
124 .with_action_kind("draft")
125 .with_maker_prompt(
126 "Summarize changes merged since the last release into changelog \
127 entries grouped by Added / Changed / Fixed. User-facing language.",
128 )
129 .with_checker_prompt(
130 "Confirm each entry maps to a real change and nothing significant is \
131 missing.",
132 )
133}
134
135pub fn post_merge_cleanup() -> LoopSpec {
138 LoopSpec::new(
139 "post-merge-cleanup",
140 "Catch loose ends left behind by recent merges.",
141 LoopLevel::L1Report,
142 )
143 .with_cadence("every 6h")
144 .with_budget(TokenBudget::iters(10))
145 .with_action_kind("report")
146 .with_maker_prompt(
147 "Look for cleanup opportunities from recently merged work: merged \
148 branches not deleted, newly dead code, TODOs introduced, docs that \
149 drifted. List concrete items.",
150 )
151 .with_checker_prompt("Confirm each cleanup item is still applicable.")
152}
153
154pub fn issue_triage() -> LoopSpec {
156 LoopSpec::new(
157 "issue-triage",
158 "Label, prioritize, and route incoming issues consistently.",
159 LoopLevel::L1Report,
160 )
161 .with_cadence("every 2h")
162 .with_budget(TokenBudget::iters(8))
163 .with_action_kind("comment")
164 .with_maker_prompt(
165 "For each new, untriaged issue, propose labels, a priority, and an \
166 owner/area, with a one-line justification. Propose only.",
167 )
168 .with_checker_prompt(
169 "Sanity-check the proposed triage against the issue text; flag any \
170 that need a human.",
171 )
172}
173
174pub fn catalogue() -> Vec<LoopSpec> {
177 vec![
178 daily_triage(),
179 pr_babysitter(),
180 ci_sweeper(),
181 dependency_sweeper(),
182 changelog_drafter(),
183 post_merge_cleanup(),
184 issue_triage(),
185 ]
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn catalogue_has_seven_uniquely_named_loops() {
194 let cat = catalogue();
195 assert_eq!(cat.len(), 7);
196 let mut names: Vec<_> = cat.iter().map(|s| s.name.clone()).collect();
197 names.sort();
198 names.dedup();
199 assert_eq!(names.len(), 7, "loop names must be unique");
200 }
201
202 #[test]
203 fn sweepers_are_assisted_everything_else_reports() {
204 assert_eq!(ci_sweeper().level, LoopLevel::L2Assisted);
205 assert_eq!(dependency_sweeper().level, LoopLevel::L2Assisted);
206 assert_eq!(daily_triage().level, LoopLevel::L1Report);
207 assert_eq!(issue_triage().level, LoopLevel::L1Report);
208 }
209
210 #[test]
211 fn every_pattern_cadence_parses() {
212 for spec in catalogue() {
213 assert!(
214 harness_daemon::Schedule::parse(&spec.cadence).is_ok(),
215 "cadence `{}` for `{}` must parse",
216 spec.cadence,
217 spec.name
218 );
219 }
220 }
221}