use crate::config::StackConfig;
use crate::state::Status;
pub struct PromptContext {
pub story_id: String,
pub stories_dir: String,
pub decisions_dir: String,
pub last_rejection: Option<String>,
pub from: Status,
pub to: Status,
pub stack: StackConfig,
}
impl StackConfig {
pub fn render(&self) -> String {
let mut parts: Vec<String> = Vec::new();
if let Some(ref cmd) = self.build_command {
parts.push(format!("- Compilación/build: `{cmd}`"));
}
if let Some(ref cmd) = self.test_command {
parts.push(format!("- Tests: `{cmd}`"));
}
if let Some(ref cmd) = self.lint_command {
parts.push(format!("- Linting: `{cmd}`"));
}
if let Some(ref cmd) = self.fmt_command {
parts.push(format!("- Formato: `{cmd}`"));
}
if parts.is_empty() {
"Compila/construye el proyecto, ejecuta los tests, y aplica \
linting y formato según las convenciones del proyecto."
.into()
} else {
format!(
"Ejecuta los siguientes comandos para verificar tu trabajo:\n{}",
parts.join("\n")
)
}
}
}
impl PromptContext {
fn header(&self, action: &str) -> String {
format!(
"{action} {id}. Lee {dir}/{id}.md.",
action = action,
id = self.story_id,
dir = self.stories_dir,
)
}
fn suffix(&self, role_short: &str) -> String {
format!(
"Añade entrada en Activity Log: YYYY-MM-DD | {short} | descripción.\n\
Documenta decisiones en {dec}/.\n\
NO preguntes. 100% autónomo.",
short = role_short,
dec = self.decisions_dir,
)
}
pub fn po_plan(&self) -> String {
format!(
"{}\n\
ValÃdala contra el Definition of Ready. Si está lista, muévela de {from} → {to}.\n\
Si necesitas tomar decisiones, documéntalas en {dec}/.\n\
{}",
self.header("Refina"),
self.suffix("PO"),
from = self.from,
to = self.to,
dec = self.decisions_dir,
)
}
pub fn po_validate(&self) -> String {
format!(
"{}\n\
Verifica que el valor de negocio se cumple. Si OK → {to}.\n\
Si no: rechaza a In Review o In Progress según gravedad. Detalla el motivo.\n\
{}",
self.header("Valida"),
self.suffix("PO"),
to = self.to,
)
}
pub fn qa_tests(&self) -> String {
let placeholder = if let Some(ref dir) = self.stack.src_dir {
format!(
"Si necesitas crear placeholders en {dir}/ para que los tests compilen, hazlo.\n",
)
} else {
String::new()
};
let stack_block = self.stack.render();
format!(
"{}\n\
Escribe los tests necesarios según los criterios de aceptación.\n\
{}Ejecútalos para verificar que compilan y pasan:\n\
{}\n\
Mueve el estado de {from} → {to}.\n\
{}",
self.header("Escribe tests para"),
placeholder,
stack_block,
self.suffix("QA"),
from = self.from,
to = self.to,
)
}
pub fn qa_fix_tests(&self) -> String {
let stack_block = self.stack.render();
format!(
"{}\n\
El Developer reportó problemas con los tests actuales.\n\
Lee especialmente el Activity Log para entender el feedback.\n\
Corrige los tests y verifica que compilan y pasan:\n\
{}\n\
El estado se mantiene en {to}.\n\
{}",
self.header("Corrige los tests de"),
stack_block,
self.suffix("QA"),
to = self.to,
)
}
pub fn dev_implement(&self) -> String {
let stack_block = self.stack.render();
format!(
"{}\n\
Los tests ya existen (QA los escribió). Búscalos y haz que pasen.\n\
Implementa en el código fuente.\n\
{}\n\
Mueve de {from} → {to}.\n\
{}",
self.header("Implementa"),
stack_block,
self.suffix("Dev"),
from = self.from,
to = self.to,
)
}
pub fn dev_fix(&self) -> String {
let rejection = self
.last_rejection
.as_deref()
.unwrap_or("(revisa el Activity Log para los detalles)");
let stack_block = self.stack.render();
format!(
"{}\n\
El Reviewer/PO rechazó la implementación anterior:\n\
\n {rejection}\n\
\n\
Lee especialmente el Activity Log para el contexto completo.\n\
Corrige la implementación.\n\
{}\n\
Mueve de {from} → {to}.\n\
{}",
self.header("Corrige"),
stack_block,
self.suffix("Dev"),
from = self.from,
to = self.to,
)
}
pub fn reviewer(&self) -> String {
let stack_block = self.stack.render();
format!(
"{}\n\
Verifica el DoD técnico.\n\
{}\n\
Si TODO OK → Business Review.\n\
Si algo falla → In Progress, con detalles CONCRETOS de archivo, lÃnea y problema.\n\
{}",
self.header("Revisa"),
stack_block,
self.suffix("Reviewer"),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ctx() -> PromptContext {
PromptContext {
story_id: "STORY-042".into(),
stories_dir: "product/stories".into(),
decisions_dir: "product/decisions".into(),
last_rejection: Some("Falta test para edge case CA3".into()),
from: Status::InProgress,
to: Status::InReview,
stack: StackConfig::default(),
}
}
#[test]
fn stack_render_empty_is_generic() {
let stack = StackConfig::default();
let out = stack.render();
assert!(out.contains("Compila/construye"));
assert!(out.contains("linting"));
assert!(!out.contains('`'), "sin comandos = sin backticks");
}
#[test]
fn stack_render_all_commands() {
let stack = StackConfig {
build_command: Some("npm run build".into()),
test_command: Some("npm test".into()),
lint_command: Some("eslint .".into()),
fmt_command: Some("prettier --check .".into()),
src_dir: Some("src/".into()),
};
let out = stack.render();
assert!(out.contains("npm run build"));
assert!(out.contains("npm test"));
assert!(out.contains("eslint"));
assert!(out.contains("prettier"));
assert!(!out.contains("src/"));
}
#[test]
fn stack_render_partial_omits_missing() {
let stack = StackConfig {
test_command: Some("pytest".into()),
..Default::default()
};
let out = stack.render();
assert!(out.contains("pytest"));
assert!(!out.contains("build"), "sin build_command no se menciona");
assert!(!out.contains("lint"), "sin lint_command no se menciona");
}
#[test]
fn po_plan_contains_story_id() {
let plan_ctx = PromptContext {
from: Status::Draft,
to: Status::Ready,
..ctx()
};
let prompt = plan_ctx.po_plan();
assert!(prompt.contains("STORY-042"));
assert!(prompt.contains("Draft"));
assert!(prompt.contains("Ready"));
}
#[test]
fn dev_fix_includes_rejection() {
let prompt = ctx().dev_fix();
assert!(prompt.contains("Falta test para edge case CA3"));
assert!(prompt.contains("In Progress"));
assert!(prompt.contains("In Review"));
}
#[test]
fn all_prompts_contain_story_id() {
for prompt in [
ctx().po_plan(),
ctx().po_validate(),
ctx().qa_tests(),
ctx().qa_fix_tests(),
ctx().dev_implement(),
ctx().dev_fix(),
ctx().reviewer(),
] {
assert!(
prompt.contains("STORY-042"),
"prompt should mention STORY-042"
);
}
}
#[test]
fn all_prompts_contain_no_preguntes() {
for prompt in [
ctx().po_plan(),
ctx().po_validate(),
ctx().qa_tests(),
ctx().qa_fix_tests(),
ctx().dev_implement(),
ctx().dev_fix(),
ctx().reviewer(),
] {
assert!(
prompt.contains("NO preguntes"),
"prompt should tell agent not to ask user"
);
}
}
fn ctx_with_stack() -> PromptContext {
PromptContext {
story_id: "STORY-007".into(),
stories_dir: "docs/stories".into(),
decisions_dir: "docs/decisions".into(),
last_rejection: None,
from: Status::TestsReady,
to: Status::InReview,
stack: StackConfig {
build_command: Some("make".into()),
test_command: Some("make test".into()),
lint_command: Some("golangci-lint run".into()),
fmt_command: Some("gofmt -l .".into()),
src_dir: Some("pkg/".into()),
},
}
}
#[test]
fn dev_implement_includes_stack_commands() {
let prompt = ctx_with_stack().dev_implement();
assert!(prompt.contains("make"));
assert!(prompt.contains("make test"));
assert!(prompt.contains("golangci-lint"));
assert!(prompt.contains("gofmt"));
}
#[test]
fn dev_fix_includes_stack_commands() {
let mut c = ctx_with_stack();
c.last_rejection = Some("bug en edge case".into());
c.from = Status::InProgress;
let prompt = c.dev_fix();
assert!(prompt.contains("make"));
assert!(prompt.contains("bug en edge case"));
assert!(prompt.contains("In Progress"));
assert!(prompt.contains("In Review"));
}
#[test]
fn reviewer_includes_stack_commands() {
let mut c = ctx_with_stack();
c.from = Status::InReview;
c.to = Status::BusinessReview;
let prompt = c.reviewer();
assert!(prompt.contains("make"));
assert!(prompt.contains("make test"));
assert!(prompt.contains("Business Review"));
}
#[test]
fn qa_tests_uses_src_dir_when_defined() {
let prompt = ctx_with_stack().qa_tests();
assert!(prompt.contains("pkg/"));
assert!(prompt.contains("placeholders"));
}
#[test]
fn qa_tests_sin_src_dir_no_menciona_placeholders() {
let c = ctx(); let prompt = c.qa_tests();
assert!(!prompt.contains("placeholders"));
}
#[test]
fn qa_tests_includes_stack_commands() {
let prompt = ctx_with_stack().qa_tests();
assert!(prompt.contains("make"));
assert!(prompt.contains("make test"));
assert!(prompt.contains("golangci-lint"));
}
#[test]
fn qa_fix_tests_includes_stack_commands() {
let mut c = ctx_with_stack();
c.from = Status::TestsReady;
c.to = Status::TestsReady;
let prompt = c.qa_fix_tests();
assert!(prompt.contains("make"));
assert!(prompt.contains("Activity Log"));
assert!(prompt.contains("Tests Ready"));
}
#[test]
fn qa_tests_sin_stack_no_menciona_comandos() {
let c = ctx(); let prompt = c.qa_tests();
assert!(prompt.contains("Compila/construye"));
assert!(!prompt.contains('`'), "sin comandos = sin backticks");
}
#[test]
fn po_plan_sin_stack_commands() {
let mut c = ctx();
c.from = Status::Draft;
c.to = Status::Ready;
let prompt = c.po_plan();
assert!(!prompt.contains('`'), "PO plan no lleva comandos de stack");
assert!(prompt.contains("Draft"));
assert!(prompt.contains("Ready"));
}
#[test]
fn po_validate_sin_stack_commands() {
let mut c = ctx();
c.from = Status::BusinessReview;
c.to = Status::Done;
let prompt = c.po_validate();
assert!(
!prompt.contains('`'),
"PO validate no lleva comandos de stack"
);
assert!(prompt.contains("Done"));
assert!(prompt.contains("In Review"));
assert!(prompt.contains("In Progress"));
}
}