1use std::fs;
9use std::io;
10use std::path::Path;
11
12use crate::flow::{PANEL_PROMPT_ROOT, Panel};
13
14const WORKER_BOOTSTRAP_PROMPT: &str = include_str!("instructions.md").trim_ascii();
15
16pub const REVIEW_PROMPT_PATH: &str = ".agents/sloop/prompts/review.md";
17
18pub const PANEL_REVIEWER_INSTRUCTION: &str = "You are one reviewer on a panel judging the work in this git worktree. Run `sloop brief` to read the assignment under review. Read and run whatever you need, but change nothing. Report your verdict with `sloop verdict pass|fail --reason <text> [--confidence low|medium|high]` exactly once; that call, not your prose, is your report, and finishing without it counts as a fail.";
21
22pub const BACKWARD_CONTEXT_LINES: usize = 100;
26
27pub fn compose_worker_prompt(root: &Path) -> Result<String, String> {
28 let path = root.join(".agents/sloop/instructions.md");
29 match fs::read_to_string(&path) {
30 Ok(instructions) => Ok(format!("{WORKER_BOOTSTRAP_PROMPT}\n\n{instructions}")),
31 Err(error) if error.kind() == io::ErrorKind::NotFound => {
32 Ok(WORKER_BOOTSTRAP_PROMPT.to_owned())
33 }
34 Err(error) => Err(format!("cannot read {}: {error}", path.display())),
35 }
36}
37
38pub fn panel_prompt(root: &Path, panel: &Panel) -> Result<String, String> {
43 let path = root.join(PANEL_PROMPT_ROOT).join(&panel.prompt);
44 let text = fs::read_to_string(&path)
45 .map_err(|error| format!("cannot read panel prompt {}: {error}", path.display()))?;
46 Ok(format!("{PANEL_REVIEWER_INSTRUCTION}\n\n{text}"))
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct FailureContext {
59 pub stage: String,
60 pub attempt: u32,
61 pub reason: String,
62 pub output: String,
64}
65
66pub fn previous_attempt_block(context: &FailureContext) -> String {
70 let mut block = format!(
71 "--- previous attempt failed ---\n\
72 Stage `{}` (attempt {}) failed: {}\n\
73 You are running again to address that failure.",
74 context.stage, context.attempt, context.reason,
75 );
76 if !context.output.is_empty() {
77 block.push_str(&format!(
78 "\n\nThe last {BACKWARD_CONTEXT_LINES} lines of that stage's output:\n{}",
79 context.output,
80 ));
81 }
82 block.push_str("\n--- end previous attempt ---");
83 block
84}
85
86#[cfg(test)]
87mod tests {
88 use std::fs;
89
90 use tempfile::tempdir;
91
92 use super::{
93 FailureContext, WORKER_BOOTSTRAP_PROMPT, compose_worker_prompt, previous_attempt_block,
94 };
95
96 #[test]
97 fn worker_prompt_uses_the_builtin_when_instructions_are_absent() {
98 let root = tempdir().unwrap();
99
100 assert_eq!(
101 compose_worker_prompt(root.path()).unwrap(),
102 WORKER_BOOTSTRAP_PROMPT
103 );
104 }
105
106 #[test]
107 fn worker_prompt_appends_repository_instructions() {
108 let root = tempdir().unwrap();
109 fs::create_dir_all(root.path().join(".agents/sloop")).unwrap();
110 fs::write(
111 root.path().join(".agents/sloop/instructions.md"),
112 "Use repository conventions.\n",
113 )
114 .unwrap();
115
116 assert_eq!(
117 compose_worker_prompt(root.path()).unwrap(),
118 format!("{WORKER_BOOTSTRAP_PROMPT}\n\nUse repository conventions.\n")
119 );
120 }
121
122 #[test]
125 fn the_previous_attempt_block_names_the_failure_and_fences_its_output() {
126 let block = previous_attempt_block(&FailureContext {
127 stage: "test".into(),
128 attempt: 2,
129 reason: "exit 101".into(),
130 output: "assertion failed\n left: 1".into(),
131 });
132
133 assert_eq!(
134 block,
135 concat!(
136 "--- previous attempt failed ---\n",
137 "Stage `test` (attempt 2) failed: exit 101\n",
138 "You are running again to address that failure.\n",
139 "\n",
140 "The last 100 lines of that stage's output:\n",
141 "assertion failed\n",
142 " left: 1\n",
143 "--- end previous attempt ---",
144 )
145 );
146 }
147
148 #[test]
151 fn the_previous_attempt_block_omits_an_empty_output_section() {
152 let block = previous_attempt_block(&FailureContext {
153 stage: "review".into(),
154 attempt: 1,
155 reason: "no verdict reported".into(),
156 output: String::new(),
157 });
158
159 assert_eq!(
160 block,
161 concat!(
162 "--- previous attempt failed ---\n",
163 "Stage `review` (attempt 1) failed: no verdict reported\n",
164 "You are running again to address that failure.\n",
165 "--- end previous attempt ---",
166 )
167 );
168 }
169}