pub mod caveman;
use crate::deferred::{self, DeferredDoc};
use crate::plan::{Phase, Plan};
const IMPLEMENTER_TEMPLATE: &str = include_str!("templates/implementer.txt");
const AUDITOR_TEMPLATE: &str = include_str!("templates/auditor.txt");
const FIXER_TEMPLATE: &str = include_str!("templates/fixer.txt");
const PLANNER_TEMPLATE: &str = include_str!("templates/planner.txt");
const QUESTIONER_TEMPLATE: &str = include_str!("templates/questioner.txt");
pub const TEMPLATE_STATIC_BUDGET: usize = 8_000;
pub fn implementer(_plan: &Plan, deferred: &DeferredDoc, current: &Phase) -> String {
render(
IMPLEMENTER_TEMPLATE,
&[
("phase_id", current.id.as_str()),
("phase_title", current.title.as_str()),
("phase_body", current.body.as_str()),
("deferred", &serialize_deferred_for_prompt(deferred)),
],
)
}
pub fn auditor(_plan: &Plan, current: &Phase, diff: &str, small_fix_line_limit: u32) -> String {
let limit = small_fix_line_limit.to_string();
render(
AUDITOR_TEMPLATE,
&[
("phase_id", current.id.as_str()),
("phase_title", current.title.as_str()),
("phase_body", current.body.as_str()),
("diff", diff),
("deferred", "(no deferred.md provided yet)"),
("small_fix_line_limit", &limit),
],
)
}
pub fn fixer(_plan: &Plan, current: &Phase, test_output: &str) -> String {
render(
FIXER_TEMPLATE,
&[
("phase_id", current.id.as_str()),
("phase_title", current.title.as_str()),
("phase_body", current.body.as_str()),
("test_output", test_output),
("deferred", "(no deferred.md provided yet)"),
],
)
}
pub fn planner(goal: &str, repo_summary: &str) -> String {
render(
PLANNER_TEMPLATE,
&[("goal", goal), ("repo_summary", repo_summary)],
)
}
pub fn questioner(goal: &str, repo_summary: &str, max_questions: u32) -> String {
let max = max_questions.to_string();
render(
QUESTIONER_TEMPLATE,
&[
("goal", goal),
("repo_summary", repo_summary),
("max_questions", &max),
],
)
}
pub fn auditor_with_deferred(
_plan: &Plan,
current: &Phase,
diff: &str,
deferred: &DeferredDoc,
small_fix_line_limit: u32,
) -> String {
let limit = small_fix_line_limit.to_string();
render(
AUDITOR_TEMPLATE,
&[
("phase_id", current.id.as_str()),
("phase_title", current.title.as_str()),
("phase_body", current.body.as_str()),
("diff", diff),
("deferred", &serialize_deferred_for_prompt(deferred)),
("small_fix_line_limit", &limit),
],
)
}
pub fn fixer_with_deferred(
_plan: &Plan,
current: &Phase,
test_output: &str,
deferred: &DeferredDoc,
) -> String {
render(
FIXER_TEMPLATE,
&[
("phase_id", current.id.as_str()),
("phase_title", current.title.as_str()),
("phase_body", current.body.as_str()),
("test_output", test_output),
("deferred", &serialize_deferred_for_prompt(deferred)),
],
)
}
fn serialize_deferred_for_prompt(doc: &DeferredDoc) -> String {
let s = deferred::serialize(doc);
if s.is_empty() {
"(empty)\n".to_string()
} else {
s
}
}
fn render(template: &str, vars: &[(&str, &str)]) -> String {
let bytes = template.as_bytes();
let mut out = String::with_capacity(template.len() + 256);
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
if c == b'{' && bytes.get(i + 1) == Some(&b'{') {
out.push('{');
i += 2;
} else if c == b'}' && bytes.get(i + 1) == Some(&b'}') {
out.push('}');
i += 2;
} else if c == b'{' {
let rel = template[i + 1..].find('}').unwrap_or_else(|| {
panic!("unterminated placeholder in prompt template at byte {i}")
});
let name = &template[i + 1..i + 1 + rel];
let val = vars
.iter()
.find(|(k, _)| *k == name)
.unwrap_or_else(|| panic!("unknown placeholder {{{name}}} in prompt template"))
.1;
out.push_str(val);
i = i + 1 + rel + 1;
} else if c == b'}' {
panic!("unmatched }} at byte {i} in prompt template");
} else {
let next = template[i..]
.find(['{', '}'])
.map(|d| i + d)
.unwrap_or(template.len());
out.push_str(&template[i..next]);
i = next;
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::deferred::{DeferredItem, DeferredPhase};
use crate::plan::PhaseId;
fn pid(s: &str) -> PhaseId {
PhaseId::parse(s).unwrap()
}
fn fixture_plan() -> Plan {
Plan::new(
pid("02"),
vec![
Phase {
id: pid("01"),
title: "Foundation".into(),
body: "\n**Scope.** stand it up.\n\n**Deliverables.**\n- crate\n\n**Acceptance.**\n- builds\n\n".into(),
},
Phase {
id: pid("02"),
title: "Domain types".into(),
body: "\n**Scope.** type vocabulary.\n\n**Deliverables.**\n- PhaseId\n\n**Acceptance.**\n- ordering tests\n".into(),
},
],
)
}
fn fixture_current() -> Phase {
Phase {
id: pid("02"),
title: "Domain types".into(),
body: "\n**Scope.** type vocabulary.\n\n**Deliverables.**\n- PhaseId\n\n**Acceptance.**\n- ordering tests\n".into(),
}
}
fn fixture_deferred() -> DeferredDoc {
DeferredDoc {
items: vec![
DeferredItem {
text: "polish error message".into(),
done: false,
},
DeferredItem {
text: "remove unused stub".into(),
done: true,
},
],
phases: vec![DeferredPhase {
source_phase: pid("07"),
title: "rework agent trait".into(),
body: "\nbody line\n".into(),
}],
}
}
#[test]
fn render_substitutes_simple_placeholders() {
assert_eq!(
render("hello {name}!", &[("name", "world")]),
"hello world!"
);
}
#[test]
fn render_handles_repeated_and_adjacent_placeholders() {
assert_eq!(render("{a}{b}{a}", &[("a", "X"), ("b", "Y")]), "XYX");
}
#[test]
fn render_double_brace_escapes_literal_braces() {
assert_eq!(
render(
"rust: Result<{T}, {E}> {{ ok }}",
&[("T", "u32"), ("E", "Err")]
),
"rust: Result<u32, Err> { ok }"
);
}
#[test]
fn render_preserves_unicode() {
assert_eq!(render("ünîcødé {x} 漢字", &[("x", "✓")]), "ünîcødé ✓ 漢字");
}
#[test]
#[should_panic(expected = "unknown placeholder")]
fn render_panics_on_unknown_placeholder() {
render("{nope}", &[("name", "v")]);
}
#[test]
#[should_panic(expected = "unterminated placeholder")]
fn render_panics_on_unterminated_placeholder() {
render("oh no {forever", &[("forever", "x")]);
}
#[test]
#[should_panic(expected = "unmatched")]
fn render_panics_on_lone_close_brace() {
render("oh no }", &[]);
}
#[test]
fn implementer_includes_phase_and_deferred() {
let plan = fixture_plan();
let current = fixture_current();
let deferred = fixture_deferred();
let out = implementer(&plan, &deferred, ¤t);
assert!(out.contains("# Phase 02: Domain types"));
assert!(out.contains("PhaseId"));
assert!(out.contains("- [ ] polish error message"));
assert!(out.contains("### From phase 07: rework agent trait"));
assert!(out.contains("Never edit `plan.md`"));
assert!(out.contains(".pitboss/"));
assert!(!out.contains("{phase_id}"));
assert!(!out.contains("{deferred}"));
}
#[test]
fn auditor_renders_threshold_and_diff() {
let plan = fixture_plan();
let current = fixture_current();
let diff = "diff --git a/src/x.rs b/src/x.rs\n@@\n+println!(\"hi\");\n";
let out = auditor(&plan, ¤t, diff, 30);
assert!(out.contains("≤ 30 lines"));
assert!(out.contains("Diff produced by the implementer"));
assert!(out.contains("println!"));
assert!(!out.contains("{small_fix_line_limit}"));
}
#[test]
fn fixer_includes_test_output() {
let plan = fixture_plan();
let current = fixture_current();
let test_output = "running 1 test\ntest foo ... FAILED\nassertion failed: 1 == 2\n";
let out = fixer(&plan, ¤t, test_output);
assert!(out.contains("Tests failed after the implementer ran"));
assert!(out.contains("assertion failed"));
assert!(!out.contains("{test_output}"));
}
#[test]
fn planner_embeds_goal_and_repo_summary() {
let out = planner(
"Build a CLI todo app in Rust",
"Cargo.toml\nsrc/main.rs\nREADME.md",
);
assert!(out.contains("Build a CLI todo app in Rust"));
assert!(out.contains("Cargo.toml"));
assert!(out.contains("YAML frontmatter"));
assert!(out.contains("Output ONLY the file contents."));
assert!(!out.contains("{goal}"));
}
#[test]
fn empty_deferred_renders_as_visible_marker() {
let plan = fixture_plan();
let current = fixture_current();
let out = implementer(&plan, &DeferredDoc::empty(), ¤t);
assert!(
out.contains("(empty)"),
"expected an explicit empty marker so the agent isn't confused by a blank section"
);
}
#[test]
fn auditor_with_deferred_threads_real_doc() {
let plan = fixture_plan();
let current = fixture_current();
let deferred = fixture_deferred();
let out = auditor_with_deferred(&plan, ¤t, "no diff", &deferred, 25);
assert!(out.contains("- [ ] polish error message"));
assert!(out.contains("≤ 25 lines"));
}
#[test]
fn fixer_with_deferred_threads_real_doc() {
let plan = fixture_plan();
let current = fixture_current();
let deferred = fixture_deferred();
let out = fixer_with_deferred(&plan, ¤t, "boom", &deferred);
assert!(out.contains("- [ ] polish error message"));
assert!(out.contains("boom"));
}
#[test]
fn templates_fit_static_budget() {
for (name, body) in [
("implementer", IMPLEMENTER_TEMPLATE),
("auditor", AUDITOR_TEMPLATE),
("fixer", FIXER_TEMPLATE),
("planner", PLANNER_TEMPLATE),
("questioner", QUESTIONER_TEMPLATE),
] {
assert!(
body.len() <= TEMPLATE_STATIC_BUDGET,
"{name} template is {} bytes, exceeding TEMPLATE_STATIC_BUDGET ({} bytes)",
body.len(),
TEMPLATE_STATIC_BUDGET
);
}
}
#[test]
fn snapshot_implementer() {
let plan = fixture_plan();
let current = fixture_current();
let deferred = fixture_deferred();
insta::assert_snapshot!(implementer(&plan, &deferred, ¤t));
}
#[test]
fn snapshot_auditor() {
let plan = fixture_plan();
let current = fixture_current();
let diff = "diff --git a/src/x.rs b/src/x.rs\n@@\n-old\n+new\n";
insta::assert_snapshot!(auditor(&plan, ¤t, diff, 30));
}
#[test]
fn snapshot_fixer() {
let plan = fixture_plan();
let current = fixture_current();
let test_output = "running 2 tests\ntest a ... ok\ntest b ... FAILED\n";
insta::assert_snapshot!(fixer(&plan, ¤t, test_output));
}
#[test]
fn snapshot_planner() {
let goal = "Build a CLI todo app in Rust with JSON persistence.";
let repo_summary = "(empty repository)";
insta::assert_snapshot!(planner(goal, repo_summary));
}
#[test]
fn questioner_embeds_goal_and_max() {
let out = questioner("Build a CLI todo app", "(empty repository)", 20);
assert!(out.contains("Build a CLI todo app"));
assert!(out.contains("20"));
assert!(out.contains("numbered list"));
assert!(!out.contains("{goal}"));
assert!(!out.contains("{max_questions}"));
}
#[test]
fn snapshot_questioner() {
let goal = "Add a --interview flag to pitboss plan.";
let repo_summary = "(empty repository)";
insta::assert_snapshot!(questioner(goal, repo_summary, 25));
}
}