summaryrefslogtreecommitdiff
path: root/makima/src/llm/phase_guidance.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-26 20:19:30 +0000
committersoryu <soryu@soryu.co>2026-01-26 20:19:30 +0000
commit04e1e8f0dd85d19917ac5ba0b73cba65ebac8976 (patch)
treee52537dd2a33c10156f1378ffdc6803bc983482d /makima/src/llm/phase_guidance.rs
parent6328477bc459eca0243b685553dbd75b925fdc8a (diff)
downloadsoryu-04e1e8f0dd85d19917ac5ba0b73cba65ebac8976.tar.gz
soryu-04e1e8f0dd85d19917ac5ba0b73cba65ebac8976.zip
Add completion phases
Diffstat (limited to 'makima/src/llm/phase_guidance.rs')
-rw-r--r--makima/src/llm/phase_guidance.rs688
1 files changed, 282 insertions, 406 deletions
diff --git a/makima/src/llm/phase_guidance.rs b/makima/src/llm/phase_guidance.rs
index 03f7c76..379bdca 100644
--- a/makima/src/llm/phase_guidance.rs
+++ b/makima/src/llm/phase_guidance.rs
@@ -21,13 +21,12 @@
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
-use uuid::Uuid;
-/// Priority level for recommended deliverables
+/// Priority level for deliverables
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")]
-pub enum FilePriority {
- /// Must exist before advancing phase
+pub enum DeliverablePriority {
+ /// Must be completed before advancing phase
Required,
/// Strongly suggested for phase completion
Recommended,
@@ -35,49 +34,45 @@ pub enum FilePriority {
Optional,
}
-/// A recommended file for a phase
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct RecommendedFile {
- /// Template ID to create from
- pub template_id: String,
- /// Suggested file name
- pub name_suggestion: String,
+/// A deliverable for a phase
+#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
+pub struct Deliverable {
+ /// Unique identifier for the deliverable
+ pub id: String,
+ /// Display name
+ pub name: String,
/// Priority level
- pub priority: FilePriority,
+ pub priority: DeliverablePriority,
/// Brief description of purpose
pub description: String,
}
/// Expected deliverables for a phase
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct PhaseDeliverables {
/// Phase name
pub phase: String,
- /// Recommended files to create
- pub recommended_files: Vec<RecommendedFile>,
+ /// Deliverables for this phase
+ pub deliverables: Vec<Deliverable>,
/// Whether a repository is required for this phase
pub requires_repository: bool,
- /// Whether tasks should exist in this phase
+ /// Whether tasks should be completed in this phase
pub requires_tasks: bool,
/// Guidance text for this phase
pub guidance: String,
}
-/// Status of a deliverable item
+/// Status of a deliverable
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct DeliverableStatus {
- /// Template ID
- pub template_id: String,
- /// Expected name
+ /// Deliverable ID
+ pub id: String,
+ /// Display name
pub name: String,
/// Priority
- pub priority: FilePriority,
- /// Whether it has been created
+ pub priority: DeliverablePriority,
+ /// Whether it has been completed
pub completed: bool,
- /// File ID if created
- pub file_id: Option<Uuid>,
- /// Actual file name if created
- pub actual_name: Option<String>,
}
/// Checklist for phase completion
@@ -85,8 +80,8 @@ pub struct DeliverableStatus {
pub struct PhaseChecklist {
/// Current phase
pub phase: String,
- /// File deliverables status
- pub file_deliverables: Vec<DeliverableStatus>,
+ /// Deliverable status list
+ pub deliverables: Vec<DeliverableStatus>,
/// Whether repository is configured
pub has_repository: bool,
/// Whether repository was required
@@ -111,16 +106,8 @@ pub struct TaskStats {
pub failed: usize,
}
-/// Minimal file info for checklist building
-pub struct FileInfo {
- pub id: Uuid,
- pub name: String,
- pub contract_phase: Option<String>,
-}
-
/// Minimal task info for checklist building
pub struct TaskInfo {
- pub id: Uuid,
pub name: String,
pub status: String,
}
@@ -131,22 +118,6 @@ pub fn get_phase_deliverables(phase: &str) -> PhaseDeliverables {
}
/// Get phase deliverables configuration for a specific contract type
-///
-/// ## Contract Types
-///
-/// ### Simple
-/// - Plan: Only "Plan" deliverable (required)
-/// - Execute: Only "PR" deliverable (required)
-///
-/// ### Specification
-/// - Research: Only "Research Notes" deliverable (required)
-/// - Specify: Only "Requirements Document" deliverable (required)
-/// - Plan: Only "Plan" deliverable (required)
-/// - Execute: Only "PR" deliverable (required)
-/// - Review: Only "Release Notes" deliverable (required)
-///
-/// ### Execute
-/// - Execute: No deliverables at all
pub fn get_phase_deliverables_for_type(phase: &str, contract_type: &str) -> PhaseDeliverables {
match contract_type {
"execute" => get_execute_type_deliverables(phase),
@@ -156,41 +127,35 @@ pub fn get_phase_deliverables_for_type(phase: &str, contract_type: &str) -> Phas
}
/// Get deliverables for 'simple' contract type
-/// - Plan phase: Only "Plan" deliverable (required)
-/// - Execute phase: Only "PR" deliverable (required)
fn get_simple_type_deliverables(phase: &str) -> PhaseDeliverables {
match phase {
"plan" => PhaseDeliverables {
phase: "plan".to_string(),
- recommended_files: vec![
- RecommendedFile {
- template_id: "plan".to_string(),
- name_suggestion: "Plan".to_string(),
- priority: FilePriority::Required,
- description: "Implementation plan detailing the approach and tasks".to_string(),
- },
- ],
+ deliverables: vec![Deliverable {
+ id: "plan-document".to_string(),
+ name: "Plan".to_string(),
+ priority: DeliverablePriority::Required,
+ description: "Implementation plan detailing the approach and tasks".to_string(),
+ }],
requires_repository: true,
requires_tasks: false,
guidance: "Create a plan document that outlines the implementation approach. A repository must be configured before moving to Execute phase.".to_string(),
},
"execute" => PhaseDeliverables {
phase: "execute".to_string(),
- recommended_files: vec![
- RecommendedFile {
- template_id: "pr".to_string(),
- name_suggestion: "PR".to_string(),
- priority: FilePriority::Required,
- description: "Pull request with the implemented changes".to_string(),
- },
- ],
+ deliverables: vec![Deliverable {
+ id: "pull-request".to_string(),
+ name: "Pull Request".to_string(),
+ priority: DeliverablePriority::Required,
+ description: "Pull request with the implemented changes".to_string(),
+ }],
requires_repository: true,
requires_tasks: true,
guidance: "Execute the plan and create a PR with the implemented changes. Complete all tasks to finish the contract.".to_string(),
},
_ => PhaseDeliverables {
phase: phase.to_string(),
- recommended_files: vec![],
+ deliverables: vec![],
requires_repository: false,
requires_tasks: false,
guidance: "Unknown phase for simple contract type".to_string(),
@@ -199,86 +164,71 @@ fn get_simple_type_deliverables(phase: &str) -> PhaseDeliverables {
}
/// Get deliverables for 'specification' contract type
-/// - Research: Only "Research Notes" deliverable (required)
-/// - Specify: Only "Requirements Document" deliverable (required)
-/// - Plan: Only "Plan" deliverable (required)
-/// - Execute: Only "PR" deliverable (required)
-/// - Review: Only "Release Notes" deliverable (required)
fn get_specification_type_deliverables(phase: &str) -> PhaseDeliverables {
match phase {
"research" => PhaseDeliverables {
phase: "research".to_string(),
- recommended_files: vec![
- RecommendedFile {
- template_id: "research-notes".to_string(),
- name_suggestion: "Research Notes".to_string(),
- priority: FilePriority::Required,
- description: "Document findings and insights during research".to_string(),
- },
- ],
+ deliverables: vec![Deliverable {
+ id: "research-notes".to_string(),
+ name: "Research Notes".to_string(),
+ priority: DeliverablePriority::Required,
+ description: "Document findings and insights during research".to_string(),
+ }],
requires_repository: false,
requires_tasks: false,
guidance: "Focus on understanding the problem space and document your findings in the Research Notes before moving to Specify phase.".to_string(),
},
"specify" => PhaseDeliverables {
phase: "specify".to_string(),
- recommended_files: vec![
- RecommendedFile {
- template_id: "requirements".to_string(),
- name_suggestion: "Requirements Document".to_string(),
- priority: FilePriority::Required,
- description: "Define functional and non-functional requirements".to_string(),
- },
- ],
+ deliverables: vec![Deliverable {
+ id: "requirements-document".to_string(),
+ name: "Requirements Document".to_string(),
+ priority: DeliverablePriority::Required,
+ description: "Define functional and non-functional requirements".to_string(),
+ }],
requires_repository: false,
requires_tasks: false,
guidance: "Define what needs to be built with clear requirements in the Requirements Document. Ensure specifications are detailed enough for planning.".to_string(),
},
"plan" => PhaseDeliverables {
phase: "plan".to_string(),
- recommended_files: vec![
- RecommendedFile {
- template_id: "plan".to_string(),
- name_suggestion: "Plan".to_string(),
- priority: FilePriority::Required,
- description: "Implementation plan detailing the approach and tasks".to_string(),
- },
- ],
+ deliverables: vec![Deliverable {
+ id: "plan-document".to_string(),
+ name: "Plan".to_string(),
+ priority: DeliverablePriority::Required,
+ description: "Implementation plan detailing the approach and tasks".to_string(),
+ }],
requires_repository: true,
requires_tasks: false,
guidance: "Create a plan document that outlines the implementation approach. A repository must be configured before moving to Execute phase.".to_string(),
},
"execute" => PhaseDeliverables {
phase: "execute".to_string(),
- recommended_files: vec![
- RecommendedFile {
- template_id: "pr".to_string(),
- name_suggestion: "PR".to_string(),
- priority: FilePriority::Required,
- description: "Pull request with the implemented changes".to_string(),
- },
- ],
+ deliverables: vec![Deliverable {
+ id: "pull-request".to_string(),
+ name: "Pull Request".to_string(),
+ priority: DeliverablePriority::Required,
+ description: "Pull request with the implemented changes".to_string(),
+ }],
requires_repository: true,
requires_tasks: true,
guidance: "Execute the plan and create a PR with the implemented changes. Complete all tasks before moving to Review phase.".to_string(),
},
"review" => PhaseDeliverables {
phase: "review".to_string(),
- recommended_files: vec![
- RecommendedFile {
- template_id: "release-notes".to_string(),
- name_suggestion: "Release Notes".to_string(),
- priority: FilePriority::Required,
- description: "Document changes for release communication".to_string(),
- },
- ],
+ deliverables: vec![Deliverable {
+ id: "release-notes".to_string(),
+ name: "Release Notes".to_string(),
+ priority: DeliverablePriority::Required,
+ description: "Document changes for release communication".to_string(),
+ }],
requires_repository: false,
requires_tasks: false,
guidance: "Review completed work and document the release in the Release Notes. The contract can be completed after review.".to_string(),
},
_ => PhaseDeliverables {
phase: phase.to_string(),
- recommended_files: vec![],
+ deliverables: vec![],
requires_repository: false,
requires_tasks: false,
guidance: "Unknown phase for specification contract type".to_string(),
@@ -287,19 +237,18 @@ fn get_specification_type_deliverables(phase: &str) -> PhaseDeliverables {
}
/// Get deliverables for 'execute' contract type
-/// - Execute phase only: No deliverables at all
fn get_execute_type_deliverables(phase: &str) -> PhaseDeliverables {
match phase {
"execute" => PhaseDeliverables {
phase: "execute".to_string(),
- recommended_files: vec![], // No deliverables for execute-only contract type
+ deliverables: vec![], // No deliverables for execute-only contract type
requires_repository: true,
requires_tasks: true,
guidance: "Execute the tasks directly. No deliverable documents are required for this contract type.".to_string(),
},
_ => PhaseDeliverables {
phase: phase.to_string(),
- recommended_files: vec![],
+ deliverables: vec![],
requires_repository: false,
requires_tasks: false,
guidance: "The 'execute' contract type only supports the 'execute' phase.".to_string(),
@@ -307,49 +256,25 @@ fn get_execute_type_deliverables(phase: &str) -> PhaseDeliverables {
}
}
-/// Build a phase checklist comparing expected vs actual deliverables (legacy, defaults to "simple")
-pub fn get_phase_checklist(
- phase: &str,
- files: &[FileInfo],
- tasks: &[TaskInfo],
- has_repository: bool,
-) -> PhaseChecklist {
- get_phase_checklist_for_type(phase, files, tasks, has_repository, "simple")
-}
-
-/// Build a phase checklist comparing expected vs actual deliverables for a specific contract type
+/// Build a phase checklist comparing expected vs actual deliverables
pub fn get_phase_checklist_for_type(
phase: &str,
- files: &[FileInfo],
+ completed_deliverables: &[String],
tasks: &[TaskInfo],
has_repository: bool,
contract_type: &str,
) -> PhaseChecklist {
- let deliverables = get_phase_deliverables_for_type(phase, contract_type);
+ let phase_config = get_phase_deliverables_for_type(phase, contract_type);
- // Match files to expected deliverables
- let file_deliverables: Vec<DeliverableStatus> = deliverables
- .recommended_files
+ // Build deliverable status list
+ let deliverables: Vec<DeliverableStatus> = phase_config
+ .deliverables
.iter()
- .map(|rec| {
- // Check if a file with matching template ID or similar name exists
- let matched_file = files.iter().find(|f| {
- // Match by phase first
- f.contract_phase.as_deref() == Some(phase) &&
- // Then by name similarity (case-insensitive contains)
- (f.name.to_lowercase().contains(&rec.name_suggestion.to_lowercase()) ||
- rec.name_suggestion.to_lowercase().contains(&f.name.to_lowercase()) ||
- f.name.to_lowercase().contains(&rec.template_id.replace("-", " ")))
- });
-
- DeliverableStatus {
- template_id: rec.template_id.clone(),
- name: rec.name_suggestion.clone(),
- priority: rec.priority,
- completed: matched_file.is_some(),
- file_id: matched_file.map(|f| f.id),
- actual_name: matched_file.map(|f| f.name.clone()),
- }
+ .map(|d| DeliverableStatus {
+ id: d.id.clone(),
+ name: d.name.clone(),
+ priority: d.priority,
+ completed: completed_deliverables.contains(&d.id),
})
.collect();
@@ -359,9 +284,18 @@ pub fn get_phase_checklist_for_type(
let pending = tasks.iter().filter(|t| t.status == "pending").count();
let running = tasks.iter().filter(|t| t.status == "running").count();
let done = tasks.iter().filter(|t| t.status == "done").count();
- let failed = tasks.iter().filter(|t| t.status == "failed" || t.status == "error").count();
-
- Some(TaskStats { total, pending, running, done, failed })
+ let failed = tasks
+ .iter()
+ .filter(|t| t.status == "failed" || t.status == "error")
+ .count();
+
+ Some(TaskStats {
+ total,
+ pending,
+ running,
+ done,
+ failed,
+ })
} else {
None
};
@@ -370,9 +304,9 @@ pub fn get_phase_checklist_for_type(
let mut completed_items = 0;
let mut total_items = 0;
- // Count required and recommended files (not optional)
- for status in &file_deliverables {
- if status.priority != FilePriority::Optional {
+ // Count required and recommended deliverables (not optional)
+ for status in &deliverables {
+ if status.priority != DeliverablePriority::Optional {
total_items += 1;
if status.completed {
completed_items += 1;
@@ -381,7 +315,7 @@ pub fn get_phase_checklist_for_type(
}
// Count repository if required
- if deliverables.requires_repository {
+ if phase_config.requires_repository {
total_items += 1;
if has_repository {
completed_items += 1;
@@ -407,62 +341,64 @@ pub fn get_phase_checklist_for_type(
// Generate suggestions
let mut suggestions = Vec::new();
- // Suggest missing required files
- for status in &file_deliverables {
+ // Suggest missing deliverables
+ for status in &deliverables {
if !status.completed {
match status.priority {
- FilePriority::Required => {
- suggestions.push(format!("Create {} (required)", status.name));
- }
- FilePriority::Recommended => {
- suggestions.push(format!("Consider creating {} (recommended)", status.name));
+ DeliverablePriority::Required => {
+ suggestions.push(format!(
+ "Mark '{}' as complete using mark_deliverable_complete (required)",
+ status.name
+ ));
}
- FilePriority::Optional => {
- // Don't suggest optional items
+ DeliverablePriority::Recommended => {
+ suggestions.push(format!(
+ "Consider completing '{}' (recommended)",
+ status.name
+ ));
}
+ DeliverablePriority::Optional => {}
}
}
}
// Suggest repository if needed
- if deliverables.requires_repository && !has_repository {
+ if phase_config.requires_repository && !has_repository {
suggestions.push("Configure a repository for task execution".to_string());
}
// Suggest task actions for execute phase
if let Some(ref stats) = task_stats {
if stats.total == 0 {
- suggestions.push("Create tasks from the Task Breakdown document".to_string());
+ suggestions.push("Create tasks to implement the plan".to_string());
} else if stats.pending > 0 {
suggestions.push(format!("Run {} pending task(s)", stats.pending));
} else if stats.running > 0 {
- suggestions.push(format!("Wait for {} running task(s) to complete", stats.running));
+ suggestions.push(format!(
+ "Wait for {} running task(s) to complete",
+ stats.running
+ ));
} else if stats.failed > 0 {
suggestions.push(format!("Address {} failed task(s)", stats.failed));
} else if stats.done == stats.total && stats.total > 0 {
- // All tasks complete - for simple contracts, this is the terminal phase
- // For specification contracts, they should advance to review phase
- suggestions.push("Mark the contract as completed (for simple contracts) or advance to Review phase".to_string());
- }
- }
-
- // Suggest completion for review phase (terminal for specification contracts)
- if phase == "review" {
- let has_release_notes = file_deliverables.iter()
- .any(|d| d.template_id == "release-notes" && d.completed);
- if has_release_notes {
- suggestions.push("Mark the contract as completed".to_string());
+ suggestions.push("All tasks complete. Mark deliverables and advance phase.".to_string());
}
}
// Generate summary
- let summary = generate_phase_summary(phase, &file_deliverables, has_repository, &task_stats, completion_percentage);
+ let summary = generate_phase_summary(
+ phase,
+ &deliverables,
+ has_repository,
+ &task_stats,
+ completion_percentage,
+ );
PhaseChecklist {
phase: phase.to_string(),
- file_deliverables,
+ deliverables,
has_repository,
- repository_required: deliverables.requires_repository,
+ repository_required: phase_config.requires_repository,
task_stats,
completion_percentage,
summary,
@@ -483,32 +419,39 @@ fn generate_phase_summary(
match phase {
"research" => {
if completed_count == 0 {
- "Research phase needs documentation. Create research notes or competitor analysis.".to_string()
+ "Research phase needs documentation. Mark deliverables complete when ready."
+ .to_string()
} else {
- format!("{}/{} research documents created. Consider transitioning to Specify phase.", completed_count, total_count)
+ format!(
+ "{}/{} deliverables complete. Ready to transition to Specify phase.",
+ completed_count, total_count
+ )
}
}
"specify" => {
- let has_required = deliverables.iter()
- .filter(|d| d.priority == FilePriority::Required)
+ let has_required = deliverables
+ .iter()
+ .filter(|d| d.priority == DeliverablePriority::Required)
.all(|d| d.completed);
if !has_required {
- "Specify phase requires a Requirements Document before transitioning.".to_string()
- } else if completion_percentage >= 66 {
- "Specifications are ready. Consider transitioning to Plan phase.".to_string()
+ "Specify phase requires completing the Requirements Document deliverable."
+ .to_string()
} else {
- format!("{}/{} specification documents created.", completed_count, total_count)
+ "Specifications ready. Consider transitioning to Plan phase.".to_string()
}
}
"plan" => {
- let has_task_breakdown = deliverables.iter()
- .any(|d| d.template_id == "task-breakdown" && d.completed);
+ let has_required = deliverables
+ .iter()
+ .filter(|d| d.priority == DeliverablePriority::Required)
+ .all(|d| d.completed);
- if !has_task_breakdown {
- "Plan phase requires a Task Breakdown document.".to_string()
+ if !has_required {
+ "Plan phase requires completing the Plan deliverable.".to_string()
} else if !has_repository {
- "Repository not configured. Configure a repository before Execute phase.".to_string()
+ "Repository not configured. Configure a repository before Execute phase."
+ .to_string()
} else {
"Planning complete. Ready to transition to Execute phase.".to_string()
}
@@ -516,23 +459,33 @@ fn generate_phase_summary(
"execute" => {
if let Some(stats) = task_stats {
if stats.total == 0 {
- "No tasks created. Create tasks from the Task Breakdown document.".to_string()
+ "No tasks created. Create tasks to implement the plan.".to_string()
} else if stats.done == stats.total {
- "All tasks complete! Ready for Review phase.".to_string()
+ "All tasks complete! Mark deliverables and advance to Review phase (or complete contract).".to_string()
} else {
- format!("{}/{} tasks completed ({}% done)", stats.done, stats.total,
- if stats.total > 0 { (stats.done * 100) / stats.total } else { 0 })
+ format!(
+ "{}/{} tasks completed ({}% done)",
+ stats.done,
+ stats.total,
+ if stats.total > 0 {
+ (stats.done * 100) / stats.total
+ } else {
+ 0
+ }
+ )
}
} else {
"Execute phase in progress.".to_string()
}
}
"review" => {
- let has_release_notes = deliverables.iter()
- .any(|d| d.template_id == "release-notes" && d.completed);
+ let has_required = deliverables
+ .iter()
+ .filter(|d| d.priority == DeliverablePriority::Required)
+ .all(|d| d.completed);
- if !has_release_notes {
- "Review phase requires Release Notes before completion.".to_string()
+ if !has_required {
+ "Review phase requires completing the Release Notes deliverable.".to_string()
} else {
"Review documentation complete. Contract can be marked as done.".to_string()
}
@@ -541,44 +494,6 @@ fn generate_phase_summary(
}
}
-/// Check if phase targets are met for transition (legacy, defaults to "simple")
-pub fn check_phase_completion(
- phase: &str,
- files: &[FileInfo],
- tasks: &[TaskInfo],
- has_repository: bool,
-) -> bool {
- check_phase_completion_for_type(phase, files, tasks, has_repository, "simple")
-}
-
-/// Check if phase targets are met for transition for a specific contract type
-pub fn check_phase_completion_for_type(
- phase: &str,
- files: &[FileInfo],
- tasks: &[TaskInfo],
- has_repository: bool,
- contract_type: &str,
-) -> bool {
- let checklist = get_phase_checklist_for_type(phase, files, tasks, has_repository, contract_type);
-
- // Check required files are complete
- let required_files_complete = checklist.file_deliverables.iter()
- .filter(|d| d.priority == FilePriority::Required)
- .all(|d| d.completed);
-
- // Check repository if required
- let repository_ok = !checklist.repository_required || checklist.has_repository;
-
- // Check tasks if in execute phase
- let tasks_ok = if let Some(stats) = &checklist.task_stats {
- stats.total > 0 && stats.done == stats.total
- } else {
- true
- };
-
- required_files_complete && repository_ok && tasks_ok
-}
-
/// Result of checking if deliverables are met for the current phase
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct DeliverableCheckResult {
@@ -603,9 +518,11 @@ pub struct DeliverableCheckResult {
/// A single deliverable item status
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct DeliverableItem {
+ /// ID of the deliverable
+ pub id: String,
/// Name of the deliverable
pub name: String,
- /// Type: "file", "repository", "pr", "tasks"
+ /// Type: "deliverable", "repository", "tasks"
pub deliverable_type: String,
/// Whether it's met
pub met: bool,
@@ -614,51 +531,49 @@ pub struct DeliverableItem {
}
/// Check if all required deliverables for the current phase are met
-/// This is used for both prompts and the check_deliverables_met tool
pub fn check_deliverables_met(
phase: &str,
contract_type: &str,
- files: &[FileInfo],
+ completed_deliverables: &[String],
tasks: &[TaskInfo],
has_repository: bool,
- pr_url: Option<&str>,
) -> DeliverableCheckResult {
- let mut required_deliverables = Vec::new();
+ let mut required_items = Vec::new();
let mut missing = Vec::new();
// Get the deliverables for this contract type and phase
- let deliverables = get_phase_deliverables_for_type(phase, contract_type);
-
- // Check required files for this phase
- for rec in &deliverables.recommended_files {
- if rec.priority == FilePriority::Required {
- let matched = files.iter().any(|f| {
- f.contract_phase.as_deref() == Some(phase) &&
- (f.name.to_lowercase().contains(&rec.name_suggestion.to_lowercase()) ||
- rec.name_suggestion.to_lowercase().contains(&f.name.to_lowercase()) ||
- f.name.to_lowercase().contains(&rec.template_id.replace("-", " ")))
- });
-
- required_deliverables.push(DeliverableItem {
- name: rec.name_suggestion.clone(),
- deliverable_type: "file".to_string(),
- met: matched,
- details: if matched {
- Some("Document exists".to_string())
+ let phase_config = get_phase_deliverables_for_type(phase, contract_type);
+
+ // Check required deliverables for this phase
+ for deliverable in &phase_config.deliverables {
+ if deliverable.priority == DeliverablePriority::Required {
+ let is_complete = completed_deliverables.contains(&deliverable.id);
+
+ required_items.push(DeliverableItem {
+ id: deliverable.id.clone(),
+ name: deliverable.name.clone(),
+ deliverable_type: "deliverable".to_string(),
+ met: is_complete,
+ details: if is_complete {
+ Some("Marked complete".to_string())
} else {
None
},
});
- if !matched {
- missing.push(format!("Create {} (required)", rec.name_suggestion));
+ if !is_complete {
+ missing.push(format!(
+ "Mark '{}' as complete (required)",
+ deliverable.name
+ ));
}
}
}
// Check repository for phases that require it
- if deliverables.requires_repository {
- required_deliverables.push(DeliverableItem {
+ if phase_config.requires_repository {
+ required_items.push(DeliverableItem {
+ id: "repository".to_string(),
name: "Repository".to_string(),
deliverable_type: "repository".to_string(),
met: has_repository,
@@ -675,12 +590,13 @@ pub fn check_deliverables_met(
}
// Check tasks for execute phase
- if deliverables.requires_tasks {
+ if phase_config.requires_tasks {
let total_tasks = tasks.len();
let done_tasks = tasks.iter().filter(|t| t.status == "done").count();
let tasks_complete = total_tasks > 0 && done_tasks == total_tasks;
- required_deliverables.push(DeliverableItem {
+ required_items.push(DeliverableItem {
+ id: "tasks".to_string(),
name: "Tasks Completed".to_string(),
deliverable_type: "tasks".to_string(),
met: tasks_complete,
@@ -691,38 +607,36 @@ pub fn check_deliverables_met(
if total_tasks == 0 {
missing.push("Create and complete tasks".to_string());
} else {
- missing.push(format!("Complete remaining {} task(s)", total_tasks - done_tasks));
+ missing.push(format!(
+ "Complete remaining {} task(s)",
+ total_tasks - done_tasks
+ ));
}
}
}
- // For simple/specification contracts in execute phase, PR is a key deliverable
- if (contract_type == "simple" || contract_type == "specification") && phase == "execute" {
- let has_pr = pr_url.is_some() && !pr_url.unwrap_or("").is_empty();
- required_deliverables.push(DeliverableItem {
- name: "Pull Request".to_string(),
- deliverable_type: "pr".to_string(),
- met: has_pr,
- details: pr_url.map(|u| format!("PR: {}", u)),
- });
-
- if !has_pr {
- missing.push("Create a Pull Request for the completed work".to_string());
- }
- }
-
- let deliverables_met = required_deliverables.iter().all(|d| d.met);
+ let deliverables_met = required_items.iter().all(|d| d.met);
let next_phase = get_next_phase_for_contract(contract_type, phase);
let ready_to_advance = deliverables_met && next_phase.is_some();
let summary = if deliverables_met {
if let Some(ref next) = next_phase {
- format!("All deliverables met for {} phase. Ready to advance to {} phase.", phase, next)
+ format!(
+ "All deliverables met for {} phase. Ready to advance to {} phase.",
+ phase, next
+ )
} else {
- format!("All deliverables met for {} phase. This is the final phase.", phase)
+ format!(
+ "All deliverables met for {} phase. This is the final phase.",
+ phase
+ )
}
} else {
- format!("{} deliverable(s) still needed for {} phase.", missing.len(), phase)
+ format!(
+ "{} deliverable(s) still needed for {} phase.",
+ missing.len(),
+ phase
+ )
};
DeliverableCheckResult {
@@ -730,7 +644,7 @@ pub fn check_deliverables_met(
ready_to_advance,
phase: phase.to_string(),
next_phase,
- required_deliverables,
+ required_deliverables: required_items,
missing,
summary,
auto_progress_recommended: deliverables_met && ready_to_advance,
@@ -758,17 +672,21 @@ pub fn get_next_phase_for_contract(contract_type: &str, current_phase: &str) ->
}
/// Determine if the contract should auto-progress to the next phase
-/// This is called when deliverables are met and autonomous_loop is enabled
pub fn should_auto_progress(
phase: &str,
contract_type: &str,
- files: &[FileInfo],
+ completed_deliverables: &[String],
tasks: &[TaskInfo],
has_repository: bool,
- pr_url: Option<&str>,
autonomous_loop: bool,
) -> AutoProgressDecision {
- let check = check_deliverables_met(phase, contract_type, files, tasks, has_repository, pr_url);
+ let check = check_deliverables_met(
+ phase,
+ contract_type,
+ completed_deliverables,
+ tasks,
+ has_repository,
+ );
if !check.deliverables_met {
return AutoProgressDecision {
@@ -840,15 +758,25 @@ pub fn generate_deliverable_prompt_guidance(
let mut guidance = String::new();
guidance.push_str("\n## Phase Deliverables Status\n\n");
- guidance.push_str(&format!("**Current Phase**: {} | **Contract Type**: {}\n\n",
- capitalize(phase), contract_type));
+ guidance.push_str(&format!(
+ "**Current Phase**: {} | **Contract Type**: {}\n\n",
+ capitalize(phase),
+ contract_type
+ ));
// Show required deliverables checklist
guidance.push_str("### Required Deliverables Checklist\n");
for item in &check_result.required_deliverables {
let status = if item.met { "[x]" } else { "[ ]" };
- let details = item.details.as_ref().map(|d| format!(" - {}", d)).unwrap_or_default();
- guidance.push_str(&format!("{} **{}** ({}){}\n", status, item.name, item.deliverable_type, details));
+ let details = item
+ .details
+ .as_ref()
+ .map(|d| format!(" - {}", d))
+ .unwrap_or_default();
+ guidance.push_str(&format!(
+ "{} **{}** ({}){}\n",
+ status, item.name, item.deliverable_type, details
+ ));
}
// Show status and next actions
@@ -861,7 +789,9 @@ pub fn generate_deliverable_prompt_guidance(
guidance.push_str(&format!("\n**ACTION REQUIRED**: Since all deliverables are met, you should call `advance_phase` with `new_phase=\"{}\"` to progress the contract.\n", next));
}
} else {
- guidance.push_str("This is the terminal phase. The contract can be marked as completed.\n");
+ guidance.push_str(
+ "This is the terminal phase. The contract can be marked as completed.\n",
+ );
}
} else {
guidance.push_str("**Deliverables not yet met.**\n\n");
@@ -869,7 +799,9 @@ pub fn generate_deliverable_prompt_guidance(
for item in &check_result.missing {
guidance.push_str(&format!("- {}\n", item));
}
- guidance.push_str("\nComplete the missing deliverables before advancing to the next phase.\n");
+ guidance.push_str(
+ "\nUse `mark_deliverable_complete` to mark deliverables as complete when ready.\n",
+ );
}
guidance
@@ -877,20 +809,23 @@ pub fn generate_deliverable_prompt_guidance(
/// Format checklist as markdown for LLM context
pub fn format_checklist_markdown(checklist: &PhaseChecklist) -> String {
- let mut md = format!("## Phase Progress ({} Phase)\n\n", capitalize(&checklist.phase));
+ let mut md = format!(
+ "## Phase Progress ({} Phase)\n\n",
+ capitalize(&checklist.phase)
+ );
- // File deliverables
+ // Deliverables
md.push_str("### Deliverables\n");
- for status in &checklist.file_deliverables {
+ for status in &checklist.deliverables {
let check = if status.completed { "+" } else { "-" };
let priority_label = match status.priority {
- FilePriority::Required => " (required)",
- FilePriority::Recommended => " (recommended)",
- FilePriority::Optional => " (optional)",
+ DeliverablePriority::Required => " (required)",
+ DeliverablePriority::Recommended => " (recommended)",
+ DeliverablePriority::Optional => " (optional)",
};
if status.completed {
- md.push_str(&format!("[{}] {} - \"{}\"\n", check, status.name, status.actual_name.as_deref().unwrap_or("created")));
+ md.push_str(&format!("[{}] {} - completed\n", check, status.name));
} else {
md.push_str(&format!("[{}] {}{}\n", check, status.name, priority_label));
}
@@ -904,7 +839,7 @@ pub fn format_checklist_markdown(checklist: &PhaseChecklist) -> String {
// Task stats for execute phase
if let Some(ref stats) = checklist.task_stats {
- md.push_str(&format!("\n### Task Progress\n"));
+ md.push_str("\n### Task Progress\n");
md.push_str(&format!("- Total: {}\n", stats.total));
md.push_str(&format!("- Done: {}\n", stats.done));
if stats.pending > 0 {
@@ -919,7 +854,10 @@ pub fn format_checklist_markdown(checklist: &PhaseChecklist) -> String {
}
// Summary
- md.push_str(&format!("\n**Status**: {} ({}% complete)\n", checklist.summary, checklist.completion_percentage));
+ md.push_str(&format!(
+ "\n**Status**: {} ({}% complete)\n",
+ checklist.summary, checklist.completion_percentage
+ ));
// Suggestions
if !checklist.suggestions.is_empty() {
@@ -946,125 +884,63 @@ mod tests {
#[test]
fn test_get_phase_deliverables_simple() {
- // Simple contract type: Plan phase has only "Plan" deliverable
let plan = get_phase_deliverables_for_type("plan", "simple");
assert_eq!(plan.phase, "plan");
assert!(plan.requires_repository);
- assert_eq!(plan.recommended_files.len(), 1);
- assert_eq!(plan.recommended_files[0].template_id, "plan");
- assert_eq!(plan.recommended_files[0].priority, FilePriority::Required);
+ assert_eq!(plan.deliverables.len(), 1);
+ assert_eq!(plan.deliverables[0].id, "plan-document");
+ assert_eq!(plan.deliverables[0].priority, DeliverablePriority::Required);
- // Simple contract type: Execute phase has only "PR" deliverable
let execute = get_phase_deliverables_for_type("execute", "simple");
assert_eq!(execute.phase, "execute");
assert!(execute.requires_repository);
assert!(execute.requires_tasks);
- assert_eq!(execute.recommended_files.len(), 1);
- assert_eq!(execute.recommended_files[0].template_id, "pr");
- assert_eq!(execute.recommended_files[0].priority, FilePriority::Required);
+ assert_eq!(execute.deliverables.len(), 1);
+ assert_eq!(execute.deliverables[0].id, "pull-request");
}
#[test]
fn test_get_phase_deliverables_specification() {
- // Specification: Research phase has only "Research Notes" deliverable
let research = get_phase_deliverables_for_type("research", "specification");
- assert_eq!(research.phase, "research");
- assert!(!research.requires_repository);
- assert_eq!(research.recommended_files.len(), 1);
- assert_eq!(research.recommended_files[0].template_id, "research-notes");
- assert_eq!(research.recommended_files[0].priority, FilePriority::Required);
+ assert_eq!(research.deliverables.len(), 1);
+ assert_eq!(research.deliverables[0].id, "research-notes");
- // Specification: Specify phase has only "Requirements Document" deliverable
let specify = get_phase_deliverables_for_type("specify", "specification");
- assert_eq!(specify.phase, "specify");
- assert_eq!(specify.recommended_files.len(), 1);
- assert_eq!(specify.recommended_files[0].template_id, "requirements");
- assert_eq!(specify.recommended_files[0].priority, FilePriority::Required);
-
- // Specification: Plan phase has only "Plan" deliverable
- let plan = get_phase_deliverables_for_type("plan", "specification");
- assert_eq!(plan.phase, "plan");
- assert_eq!(plan.recommended_files.len(), 1);
- assert_eq!(plan.recommended_files[0].template_id, "plan");
+ assert_eq!(specify.deliverables.len(), 1);
+ assert_eq!(specify.deliverables[0].id, "requirements-document");
- // Specification: Execute phase has only "PR" deliverable
- let execute = get_phase_deliverables_for_type("execute", "specification");
- assert_eq!(execute.phase, "execute");
- assert_eq!(execute.recommended_files.len(), 1);
- assert_eq!(execute.recommended_files[0].template_id, "pr");
-
- // Specification: Review phase has only "Release Notes" deliverable
let review = get_phase_deliverables_for_type("review", "specification");
- assert_eq!(review.phase, "review");
- assert_eq!(review.recommended_files.len(), 1);
- assert_eq!(review.recommended_files[0].template_id, "release-notes");
- assert_eq!(review.recommended_files[0].priority, FilePriority::Required);
+ assert_eq!(review.deliverables.len(), 1);
+ assert_eq!(review.deliverables[0].id, "release-notes");
}
#[test]
fn test_get_phase_deliverables_execute_type() {
- // Execute contract type: Only execute phase, NO deliverables
let execute = get_phase_deliverables_for_type("execute", "execute");
- assert_eq!(execute.phase, "execute");
+ assert!(execute.deliverables.is_empty());
assert!(execute.requires_repository);
assert!(execute.requires_tasks);
- assert!(execute.recommended_files.is_empty()); // NO deliverables
-
- // Execute contract type: Other phases should return empty deliverables
- let plan = get_phase_deliverables_for_type("plan", "execute");
- assert!(plan.recommended_files.is_empty());
}
#[test]
- fn test_phase_checklist_empty_simple() {
- let checklist = get_phase_checklist_for_type("plan", &[], &[], false, "simple");
- assert_eq!(checklist.completion_percentage, 0);
- assert!(!checklist.suggestions.is_empty());
- }
-
- #[test]
- fn test_phase_checklist_execute_type_no_deliverables() {
- // Execute contract type with no file deliverables
- let checklist = get_phase_checklist_for_type("execute", &[], &[], true, "execute");
- // Should have no file deliverables
- assert!(checklist.file_deliverables.is_empty());
- }
-
- #[test]
- fn test_check_phase_completion_specification() {
- let files = vec![
- FileInfo {
- id: Uuid::new_v4(),
- name: "Requirements Document".to_string(),
- contract_phase: Some("specify".to_string()),
- },
- ];
-
- // Specify phase has required file for specification contract type
- let complete = check_phase_completion_for_type("specify", &files, &[], false, "specification");
- assert!(complete);
- }
-
- #[test]
- fn test_check_phase_completion_simple() {
- let files = vec![
- FileInfo {
- id: Uuid::new_v4(),
- name: "Plan".to_string(),
- contract_phase: Some("plan".to_string()),
- },
- ];
+ fn test_check_deliverables_met() {
+ // No deliverables marked complete
+ let result = check_deliverables_met("plan", "simple", &[], &[], true);
+ assert!(!result.deliverables_met);
+ assert!(!result.missing.is_empty());
- // Plan phase has required "Plan" file for simple contract type
- let complete = check_phase_completion_for_type("plan", &files, &[], true, "simple");
- assert!(complete);
+ // Deliverable marked complete
+ let completed = vec!["plan-document".to_string()];
+ let result = check_deliverables_met("plan", "simple", &completed, &[], true);
+ assert!(result.deliverables_met);
+ assert!(result.ready_to_advance);
}
#[test]
- fn test_legacy_functions_default_to_simple() {
- // Legacy get_phase_deliverables defaults to simple
- let plan = get_phase_deliverables("plan");
- assert_eq!(plan.recommended_files.len(), 1);
- assert_eq!(plan.recommended_files[0].template_id, "plan");
+ fn test_phase_checklist() {
+ let completed = vec!["plan-document".to_string()];
+ let checklist = get_phase_checklist_for_type("plan", &completed, &[], true, "simple");
+ assert_eq!(checklist.completion_percentage, 100);
+ assert!(checklist.deliverables[0].completed);
}
}