use std::fs;
use std::io;
use std::path::Path;
use crate::flow::{PANEL_PROMPT_ROOT, Panel};
const WORKER_BOOTSTRAP_PROMPT: &str = include_str!("instructions.md").trim_ascii();
pub const REVIEW_PROMPT_PATH: &str = ".agents/sloop/prompts/review.md";
pub 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.";
pub const BACKWARD_CONTEXT_LINES: usize = 100;
pub fn compose_worker_prompt(root: &Path) -> Result<String, String> {
let path = root.join(".agents/sloop/instructions.md");
match fs::read_to_string(&path) {
Ok(instructions) => Ok(format!("{WORKER_BOOTSTRAP_PROMPT}\n\n{instructions}")),
Err(error) if error.kind() == io::ErrorKind::NotFound => {
Ok(WORKER_BOOTSTRAP_PROMPT.to_owned())
}
Err(error) => Err(format!("cannot read {}: {error}", path.display())),
}
}
pub fn panel_prompt(root: &Path, panel: &Panel) -> Result<String, String> {
let path = root.join(PANEL_PROMPT_ROOT).join(&panel.prompt);
let text = fs::read_to_string(&path)
.map_err(|error| format!("cannot read panel prompt {}: {error}", path.display()))?;
Ok(format!("{PANEL_REVIEWER_INSTRUCTION}\n\n{text}"))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FailureContext {
pub stage: String,
pub attempt: u32,
pub reason: String,
pub output: String,
}
pub fn previous_attempt_block(context: &FailureContext) -> String {
let mut block = format!(
"--- previous attempt failed ---\n\
Stage `{}` (attempt {}) failed: {}\n\
You are running again to address that failure.",
context.stage, context.attempt, context.reason,
);
if !context.output.is_empty() {
block.push_str(&format!(
"\n\nThe last {BACKWARD_CONTEXT_LINES} lines of that stage's output:\n{}",
context.output,
));
}
block.push_str("\n--- end previous attempt ---");
block
}
#[cfg(test)]
mod tests {
use std::fs;
use tempfile::tempdir;
use super::{
FailureContext, WORKER_BOOTSTRAP_PROMPT, compose_worker_prompt, previous_attempt_block,
};
#[test]
fn worker_prompt_uses_the_builtin_when_instructions_are_absent() {
let root = tempdir().unwrap();
assert_eq!(
compose_worker_prompt(root.path()).unwrap(),
WORKER_BOOTSTRAP_PROMPT
);
}
#[test]
fn worker_prompt_appends_repository_instructions() {
let root = tempdir().unwrap();
fs::create_dir_all(root.path().join(".agents/sloop")).unwrap();
fs::write(
root.path().join(".agents/sloop/instructions.md"),
"Use repository conventions.\n",
)
.unwrap();
assert_eq!(
compose_worker_prompt(root.path()).unwrap(),
format!("{WORKER_BOOTSTRAP_PROMPT}\n\nUse repository conventions.\n")
);
}
#[test]
fn the_previous_attempt_block_names_the_failure_and_fences_its_output() {
let block = previous_attempt_block(&FailureContext {
stage: "test".into(),
attempt: 2,
reason: "exit 101".into(),
output: "assertion failed\n left: 1".into(),
});
assert_eq!(
block,
concat!(
"--- previous attempt failed ---\n",
"Stage `test` (attempt 2) failed: exit 101\n",
"You are running again to address that failure.\n",
"\n",
"The last 100 lines of that stage's output:\n",
"assertion failed\n",
" left: 1\n",
"--- end previous attempt ---",
)
);
}
#[test]
fn the_previous_attempt_block_omits_an_empty_output_section() {
let block = previous_attempt_block(&FailureContext {
stage: "review".into(),
attempt: 1,
reason: "no verdict reported".into(),
output: String::new(),
});
assert_eq!(
block,
concat!(
"--- previous attempt failed ---\n",
"Stage `review` (attempt 1) failed: no verdict reported\n",
"You are running again to address that failure.\n",
"--- end previous attempt ---",
)
);
}
}