use std::fmt::Write as _;
use crate::paths::{routine_compiled_prompt_path, routine_scheduled_log_path};
use super::agents::AgentCommand;
use super::flags::{list_flags, FlagScope};
use super::model::Routine;
pub(crate) fn slugify(title: &str) -> String {
let mut out = String::new();
let mut prev_dash = false;
for ch in title.chars() {
if ch.is_alphanumeric() {
out.extend(ch.to_lowercase());
prev_dash = false;
} else if !prev_dash {
out.push('-');
prev_dash = true;
}
}
let trimmed = out.trim_matches('-').to_string();
if trimmed.is_empty() {
"routine".to_string()
} else {
trimmed
}
}
pub(crate) const TMUX_SESSION_PREFIX: &str = "moadim-";
pub(crate) fn tmux_session_prefix(slug: &str) -> String {
format!("{TMUX_SESSION_PREFIX}{slug}-")
}
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---\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.flag_type, 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}"];
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)
}
#[path = "command_path_resolution.rs"]
mod command_path_resolution;
pub(crate) use command_path_resolution::*;
pub(crate) fn shell_quote(text: &str) -> String {
let mut out = String::from("'");
for ch in text.chars() {
if ch == '\'' {
out.push_str("'\\''");
} else {
out.push(ch);
}
}
out.push('\'');
out
}
#[path = "command_system_prompt.rs"]
mod command_system_prompt;
pub(crate) use command_system_prompt::system_prompt_stmts;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TriggerSource {
Scheduled,
Manual,
}
pub(crate) fn build_routine_command(
routine: &Routine,
agent: &AgentCommand,
source: TriggerSource,
) -> String {
let slug = slugify(&routine.title);
let prompt_path = routine_compiled_prompt_path(&slug)
.to_string_lossy()
.into_owned();
let scheduled_log_path = routine_scheduled_log_path(&slug)
.to_string_lossy()
.into_owned();
let workbenches_base = crate::paths::workbenches_dir()
.to_string_lossy()
.into_owned();
let prompt_file_ref = "prompt.md";
let workbench_ref = ".";
let mut invocation = vec![agent.command.clone()];
for arg in &agent.args {
invocation.push(substitute(arg, workbench_ref, prompt_file_ref));
}
if let Some(model) = &routine.model {
invocation.push("--model".to_string());
invocation.push(shell_quote(model));
}
let invocation = invocation.join(" ");
let mut stmts = vec![
"umask 077".to_string(),
format!(
"export PATH=$PATH:{}",
shell_quote(&cron_path(&agent.command))
),
r#"TS="$(date +%s)""#.to_string(),
];
if source == TriggerSource::Scheduled {
stmts.push(format!(
r#"printf '%s\n' "$TS" >> {} || true"#,
shell_quote(&scheduled_log_path)
));
}
stmts.extend([
format!("SLUG={}", shell_quote(&slug)),
r#"RID="${TS}_$$""#.to_string(),
format!(r#"WB={}/"$SLUG-$RID""#, shell_quote(&workbenches_base)),
format!(r#"SESS="{TMUX_SESSION_PREFIX}$SLUG-$RID""#),
r#"mkdir -p "$WB""#.to_string(),
]);
let mut inner_stmts = Vec::new();
inner_stmts.extend(system_prompt_stmts(
&crate::paths::user_prompt_path().to_string_lossy(),
&routine.title,
&agent.instructions_file,
));
inner_stmts.extend([
format!(
r#"cp {src} "$WB/prompt.md" || {{ echo "moadim: missing routine prompt {src}; aborting launch" | tee -a "$WB/agent.log" >&2; exit 1; }}"#,
src = shell_quote(&prompt_path)
),
]);
if let Some(setup) = &agent.setup {
inner_stmts.push(format!(
r#"{{ {setup}; }} || {{ echo "moadim: agent setup failed; aborting launch" | tee -a "$WB/agent.log" >&2; exit 1; }}"#
));
}
let invocation_with_exit_code = format!(r#"{invocation}; printf '%s' "$?" > exit_code"#);
inner_stmts.push(format!(
r#"tmux new-session -d -s "$SESS" -c "$WB" {} \; pipe-pane -o -t "$SESS" "cat >> \"$WB\"/agent.log" || {{ echo "moadim: failed to start tmux session $SESS (already exists?); aborting launch" | tee -a "$WB/agent.log" >&2; exit 1; }}"#,
shell_quote(&invocation_with_exit_code)
));
stmts.push(format!(
r#"{{ {} ; }} >> "$WB/launch.log" 2>&1"#,
inner_stmts.join("; ")
));
stmts.join("; ")
}
#[cfg(test)]
#[path = "command_tests.rs"]
mod command_tests;
#[cfg(test)]
#[path = "command_bin_resolution_tests.rs"]
mod command_bin_resolution_tests;
#[cfg(test)]
#[path = "command_placeholder_tests.rs"]
mod command_placeholder_tests;