use std::fmt::Write as _;
use super::slugify;
use crate::routines::agents::AgentCommand;
use crate::routines::flags::{list_flags, FlagScope};
use crate::routines::model::Routine;
pub(crate) fn compose_prompt(routine: &Routine) -> String {
let mut body = String::from("# Workbench\n");
if routine.repositories.is_empty() {
body.push_str("You are working in an empty directory.\n");
} else {
body.push_str(
"You are working in an empty directory. These repositories are relevant — clone any you need:\n",
);
for repo in &routine.repositories {
match &repo.branch {
Some(branch) => {
let _ = writeln!(body, "- {} (branch {})", repo.repository, branch);
}
None => {
let _ = writeln!(body, "- {}", repo.repository);
}
}
}
}
if let Some(goal) = routine
.goal
.as_deref()
.map(str::trim)
.filter(|text| !text.is_empty())
{
body.push_str("\n## Goal\n");
body.push_str(goal);
body.push('\n');
}
body.push_str("\n## Routine origin disclosure\n\n");
body.push_str("You act on behalf of the moadim routine named below. In every external, outward-facing communication you produce — GitHub issues, pull requests and comments; Slack messages; emails; any channel a human or third-party system receives — you MUST disclose that the action originates from this moadim routine, naming it.\n\n");
let _ = writeln!(body, "Routine name: {}", routine.title);
body.push_str("\n---\n");
body.push_str(&routine.prompt);
body.push('\n');
let flags = list_flags(&slugify(&routine.title));
if !flags.is_empty() {
body.push_str("\n---\n# Open flags\n\nRaised on a previous run and not yet resolved:\n\n");
for flag in &flags {
let scope = match flag.scope {
FlagScope::General => "general",
FlagScope::Local => "local",
};
let _ = writeln!(
body,
"- **{}** ({scope}): {}",
flag.category, flag.description
);
}
}
body
}
#[allow(
clippy::literal_string_with_formatting_args,
reason = "these are literal `String::replace` placeholder tokens, not `format!`-family arguments — there is no formatting macro here to move them into"
)]
pub(crate) fn substitute(template: &str, workbench: &str, prompt_file: &str) -> String {
template
.replace("{workbench}", workbench)
.replace("{prompt_file}", prompt_file)
.replace("{prompt}", r#""$(cat prompt.md)""#)
}
const KNOWN_PLACEHOLDERS: [&str; 3] = ["{workbench}", "{prompt_file}", "{prompt}"];
pub(crate) fn placeholder_tokens(arg: &str) -> Vec<String> {
let bytes = arg.as_bytes();
let mut out = Vec::new();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'{' && (i == 0 || bytes[i - 1] != b'$') {
if let Some(rel) = arg[i + 1..].find('}') {
let inner = &arg[i + 1..i + 1 + rel];
if inner.starts_with(|ch: char| ch.is_ascii_lowercase())
&& inner.chars().all(|ch| ch.is_ascii_lowercase() || ch == '_')
{
out.push(format!("{{{inner}}}"));
}
i += 1 + rel + 1;
continue;
}
}
i += 1;
}
out
}
pub(crate) fn validate_placeholders(args: &[String]) -> Result<(), String> {
for arg in args {
for token in placeholder_tokens(arg) {
if !KNOWN_PLACEHOLDERS.contains(&token.as_str()) {
return Err(format!(
"unknown placeholder {token} in args; supported placeholders are {}",
KNOWN_PLACEHOLDERS.join(", ")
));
}
}
}
let delivers_prompt = args
.iter()
.any(|arg| arg.contains("{prompt}") || arg.contains("{prompt_file}"));
if !delivers_prompt {
return Err(
"args must include a prompt placeholder ({prompt} or {prompt_file}); \
otherwise the agent launches with no task"
.to_string(),
);
}
Ok(())
}
pub(crate) const MAX_INLINE_PROMPT_BYTES: usize = 128 * 1024;
pub(crate) fn inline_prompt_overflow(routine: &Routine, agent: &AgentCommand) -> Option<usize> {
if !agent.args.iter().any(|arg| arg.contains("{prompt}")) {
return None;
}
let len = compose_prompt(routine).len();
(len > MAX_INLINE_PROMPT_BYTES).then_some(len)
}