summaryrefslogtreecommitdiff
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
parent6328477bc459eca0243b685553dbd75b925fdc8a (diff)
downloadsoryu-04e1e8f0dd85d19917ac5ba0b73cba65ebac8976.tar.gz
soryu-04e1e8f0dd85d19917ac5ba0b73cba65ebac8976.zip
Add completion phases
-rw-r--r--makima/migrations/20250125000000_add_completed_deliverables.sql8
-rw-r--r--makima/src/bin/makima.rs15
-rw-r--r--makima/src/daemon/api/supervisor.rs25
-rw-r--r--makima/src/daemon/cli/mod.rs3
-rw-r--r--makima/src/daemon/cli/supervisor.rs15
-rw-r--r--makima/src/daemon/task/manager.rs22
-rw-r--r--makima/src/db/models.rs24
-rw-r--r--makima/src/db/repository.rs55
-rw-r--r--makima/src/llm/contract_tools.rs107
-rw-r--r--makima/src/llm/mod.rs13
-rw-r--r--makima/src/llm/phase_guidance.rs688
-rw-r--r--makima/src/llm/templates.rs1013
-rw-r--r--makima/src/llm/tools.rs159
-rw-r--r--makima/src/server/handlers/contract_chat.rs225
-rw-r--r--makima/src/server/handlers/contract_daemon.rs38
-rw-r--r--makima/src/server/handlers/contracts.rs130
-rw-r--r--makima/src/server/handlers/mesh_daemon.rs50
-rw-r--r--makima/src/server/handlers/templates.rs106
-rw-r--r--makima/src/server/mod.rs4
19 files changed, 743 insertions, 1957 deletions
diff --git a/makima/migrations/20250125000000_add_completed_deliverables.sql b/makima/migrations/20250125000000_add_completed_deliverables.sql
new file mode 100644
index 0000000..662a2bd
--- /dev/null
+++ b/makima/migrations/20250125000000_add_completed_deliverables.sql
@@ -0,0 +1,8 @@
+-- Add completed_deliverables column to track which phase deliverables have been marked complete
+-- Structure: { "plan": ["plan-document"], "execute": ["pull-request"] }
+
+ALTER TABLE contracts
+ADD COLUMN completed_deliverables JSONB NOT NULL DEFAULT '{}';
+
+-- Add index for efficient querying
+CREATE INDEX idx_contracts_completed_deliverables ON contracts USING gin (completed_deliverables);
diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs
index 96dc252..6ddecab 100644
--- a/makima/src/bin/makima.rs
+++ b/makima/src/bin/makima.rs
@@ -590,6 +590,21 @@ async fn run_supervisor(
"contract": result.0
}))?);
}
+ SupervisorCommand::MarkDeliverable(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ eprintln!(
+ "Marking deliverable '{}' as complete for contract {}...",
+ args.deliverable_id, args.common.contract_id
+ );
+ let result = client
+ .supervisor_mark_deliverable(
+ args.common.contract_id,
+ &args.deliverable_id,
+ args.phase.as_deref(),
+ )
+ .await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
}
Ok(())
diff --git a/makima/src/daemon/api/supervisor.rs b/makima/src/daemon/api/supervisor.rs
index 74c27e0..e79a9bb 100644
--- a/makima/src/daemon/api/supervisor.rs
+++ b/makima/src/daemon/api/supervisor.rs
@@ -299,6 +299,31 @@ impl ApiClient {
self.delete(&format!("/api/v1/mesh/tasks/{}", task_id)).await
}
+ /// Mark a deliverable as complete.
+ pub async fn supervisor_mark_deliverable(
+ &self,
+ contract_id: Uuid,
+ deliverable_id: &str,
+ phase: Option<&str>,
+ ) -> Result<JsonValue, ApiError> {
+ #[derive(Serialize)]
+ #[serde(rename_all = "camelCase")]
+ struct MarkDeliverableRequest {
+ deliverable_id: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ phase: Option<String>,
+ }
+ let req = MarkDeliverableRequest {
+ deliverable_id: deliverable_id.to_string(),
+ phase: phase.map(|s| s.to_string()),
+ };
+ self.post(
+ &format!("/api/v1/contracts/{}/deliverables/complete", contract_id),
+ &req,
+ )
+ .await
+ }
+
/// Update a task.
pub async fn update_task(
&self,
diff --git a/makima/src/daemon/cli/mod.rs b/makima/src/daemon/cli/mod.rs
index 216f281..0805edd 100644
--- a/makima/src/daemon/cli/mod.rs
+++ b/makima/src/daemon/cli/mod.rs
@@ -157,6 +157,9 @@ pub enum SupervisorCommand {
/// Resume a completed contract (reactivate it)
ResumeContract(supervisor::ResumeContractArgs),
+
+ /// Mark a deliverable as complete
+ MarkDeliverable(supervisor::MarkDeliverableArgs),
}
/// Contract subcommands for task-contract interaction.
diff --git a/makima/src/daemon/cli/supervisor.rs b/makima/src/daemon/cli/supervisor.rs
index 3bc8525..4f36fd8 100644
--- a/makima/src/daemon/cli/supervisor.rs
+++ b/makima/src/daemon/cli/supervisor.rs
@@ -220,6 +220,21 @@ pub struct AdvancePhaseArgs {
pub phase: String,
}
+/// Arguments for mark-deliverable command.
+#[derive(Args, Debug)]
+pub struct MarkDeliverableArgs {
+ #[command(flatten)]
+ pub common: SupervisorArgs,
+
+ /// The deliverable ID to mark as complete (e.g., 'plan-document', 'pull-request', 'research-notes')
+ #[arg(index = 1)]
+ pub deliverable_id: String,
+
+ /// Phase the deliverable belongs to. Defaults to current contract phase if not specified.
+ #[arg(long)]
+ pub phase: Option<String>,
+}
+
/// Arguments for task command (get individual task details).
#[derive(Args, Debug)]
pub struct GetTaskArgs {
diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs
index 86d7e05..8abff3f 100644
--- a/makima/src/daemon/task/manager.rs
+++ b/makima/src/daemon/task/manager.rs
@@ -720,6 +720,9 @@ makima supervisor status
# Advance to the next phase (specify, plan, execute, review)
makima supervisor advance-phase <phase>
+
+# Mark a phase deliverable as complete (e.g., 'plan-document', 'pull-request')
+makima supervisor mark-deliverable <deliverable_id> [--phase <phase>]
```
### User Feedback
@@ -781,6 +784,25 @@ makima supervisor advance-phase <phase>
Valid phases: `specify`, `plan`, `execute`, `review`
+### Marking Deliverables Complete
+
+Each phase has deliverables that must be completed before advancing. Use `mark-deliverable` to explicitly mark them as complete when you've verified the requirement is satisfied:
+
+```bash
+# Mark a deliverable complete (defaults to current phase)
+makima supervisor mark-deliverable plan-document
+
+# Mark a deliverable for a specific phase
+makima supervisor mark-deliverable pull-request --phase execute
+```
+
+Common deliverable IDs by phase:
+- **plan**: `plan-document`, `requirements-document`
+- **execute**: `pull-request`
+- **review**: `release-notes`, `retrospective`
+
+**Use `status` to see which deliverables are pending for the current phase.**
+
## When to Advance Phases
**IMPORTANT**: You MUST advance the contract phase as you complete work in each phase!
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs
index 0c1d9f2..95517a1 100644
--- a/makima/src/db/models.rs
+++ b/makima/src/db/models.rs
@@ -1321,6 +1321,11 @@ pub struct Contract {
/// phase outputs (like plans, requirements, etc.) before continuing.
#[serde(default)]
pub phase_guard: bool,
+ /// Completed deliverables per phase.
+ /// Structure: { "plan": ["plan-document"], "execute": ["pull-request"] }
+ #[sqlx(json)]
+ #[serde(default)]
+ pub completed_deliverables: serde_json::Value,
pub version: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
@@ -1374,6 +1379,25 @@ impl Contract {
_ => ContractPhase::Execute, // simple and execute both end at execute
}
}
+
+ /// Get completed deliverable IDs for a specific phase
+ pub fn get_completed_deliverables(&self, phase: &str) -> Vec<String> {
+ self.completed_deliverables
+ .get(phase)
+ .and_then(|v| v.as_array())
+ .map(|arr| {
+ arr.iter()
+ .filter_map(|v| v.as_str().map(String::from))
+ .collect()
+ })
+ .unwrap_or_default()
+ }
+
+ /// Check if a specific deliverable is marked as complete for a phase
+ pub fn is_deliverable_complete(&self, phase: &str, deliverable_id: &str) -> bool {
+ self.get_completed_deliverables(phase)
+ .contains(&deliverable_id.to_string())
+ }
}
/// Contract repository record from the database
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index d3e4c56..b55b05e 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -3018,6 +3018,61 @@ pub async fn update_contract_supervisor(
.await
}
+/// Mark a deliverable as complete for a specific phase.
+/// Uses JSONB operations to append the deliverable_id to the phase's array.
+pub async fn mark_deliverable_complete(
+ pool: &PgPool,
+ contract_id: Uuid,
+ phase: &str,
+ deliverable_id: &str,
+) -> Result<Contract, sqlx::Error> {
+ // Use jsonb_set to add the deliverable to the phase's array
+ // If the phase key doesn't exist, create an empty array first
+ // COALESCE handles the case where the phase array doesn't exist yet
+ sqlx::query_as::<_, Contract>(
+ r#"
+ UPDATE contracts
+ SET completed_deliverables = jsonb_set(
+ completed_deliverables,
+ ARRAY[$2::text],
+ COALESCE(completed_deliverables->$2, '[]'::jsonb) || to_jsonb($3::text),
+ true
+ ),
+ updated_at = NOW()
+ WHERE id = $1
+ AND NOT (COALESCE(completed_deliverables->$2, '[]'::jsonb) ? $3)
+ RETURNING *
+ "#,
+ )
+ .bind(contract_id)
+ .bind(phase)
+ .bind(deliverable_id)
+ .fetch_one(pool)
+ .await
+}
+
+/// Clear all completed deliverables for a specific phase.
+/// Used when phase changes or deliverables need to be reset.
+pub async fn clear_phase_deliverables(
+ pool: &PgPool,
+ contract_id: Uuid,
+ phase: &str,
+) -> Result<Contract, sqlx::Error> {
+ sqlx::query_as::<_, Contract>(
+ r#"
+ UPDATE contracts
+ SET completed_deliverables = completed_deliverables - $2,
+ updated_at = NOW()
+ WHERE id = $1
+ RETURNING *
+ "#,
+ )
+ .bind(contract_id)
+ .bind(phase)
+ .fetch_one(pool)
+ .await
+}
+
/// Get the supervisor task for a contract.
pub async fn get_contract_supervisor_task(
pool: &PgPool,
diff --git a/makima/src/llm/contract_tools.rs b/makima/src/llm/contract_tools.rs
index 44c1e20..0f50132 100644
--- a/makima/src/llm/contract_tools.rs
+++ b/makima/src/llm/contract_tools.rs
@@ -64,30 +64,8 @@ pub static CONTRACT_TOOLS: once_cell::sync::Lazy<Vec<Tool>> = once_cell::sync::L
// File Management Tools
// =============================================================================
Tool {
- name: "create_file_from_template".to_string(),
- description: "Create a new file in the contract from a template. Templates are phase-appropriate document structures.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "template_id": {
- "type": "string",
- "description": "ID of the template to use (e.g., 'research-notes', 'requirements', 'architecture')"
- },
- "name": {
- "type": "string",
- "description": "Name for the new file"
- },
- "description": {
- "type": "string",
- "description": "Optional description for the file"
- }
- },
- "required": ["template_id", "name"]
- }),
- },
- Tool {
name: "create_empty_file".to_string(),
- description: "Create a new empty file in the contract without using a template.".to_string(),
+ description: "Create a new empty file in the contract.".to_string(),
parameters: json!({
"type": "object",
"properties": {
@@ -103,18 +81,26 @@ pub static CONTRACT_TOOLS: once_cell::sync::Lazy<Vec<Tool>> = once_cell::sync::L
"required": ["name"]
}),
},
+ // =============================================================================
+ // Deliverable Management Tools
+ // =============================================================================
Tool {
- name: "list_available_templates".to_string(),
- description: "List all available templates, optionally filtered by phase. Use this to see what templates can be used with create_file_from_template.".to_string(),
+ name: "mark_deliverable_complete".to_string(),
+ description: "Mark a phase deliverable as complete. Use this when you have verified that a deliverable requirement has been satisfied. Use get_phase_info or check_deliverables_met first to see available deliverable IDs.".to_string(),
parameters: json!({
"type": "object",
"properties": {
+ "deliverable_id": {
+ "type": "string",
+ "description": "The ID of the deliverable to mark as complete (e.g., 'plan-document', 'pull-request', 'research-notes')"
+ },
"phase": {
"type": "string",
"enum": ["research", "specify", "plan", "execute", "review"],
- "description": "Optional filter to show only templates for a specific phase"
+ "description": "Phase the deliverable belongs to. Defaults to the current contract phase if not specified."
}
- }
+ },
+ "required": ["deliverable_id"]
}),
},
// =============================================================================
@@ -488,16 +474,16 @@ pub enum ContractToolRequest {
ReadFile { file_id: Uuid },
// File management
- CreateFileFromTemplate {
- template_id: String,
- name: String,
- description: Option<String>,
- },
CreateEmptyFile {
name: String,
description: Option<String>,
},
- ListAvailableTemplates { phase: Option<String> },
+
+ // Deliverable management
+ MarkDeliverableComplete {
+ deliverable_id: String,
+ phase: Option<String>,
+ },
// Task management
CreateContractTask {
@@ -592,9 +578,10 @@ pub fn parse_contract_tool_call(call: &super::tools::ToolCall) -> ContractToolEx
"read_file" => parse_read_file(call),
// File management
- "create_file_from_template" => parse_create_file_from_template(call),
"create_empty_file" => parse_create_empty_file(call),
- "list_available_templates" => parse_list_available_templates(call),
+
+ // Deliverable management
+ "mark_deliverable_complete" => parse_mark_deliverable_complete(call),
// Task management
"create_contract_task" => parse_create_contract_task(call),
@@ -703,13 +690,9 @@ fn parse_read_file(call: &super::tools::ToolCall) -> ContractToolExecutionResult
// File Management Tool Parsing
// =============================================================================
-fn parse_create_file_from_template(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let template_id = call.arguments.get("template_id").and_then(|v| v.as_str());
+fn parse_create_empty_file(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
let name = call.arguments.get("name").and_then(|v| v.as_str());
- let Some(template_id) = template_id else {
- return error_result("Missing required parameter: template_id");
- };
let Some(name) = name else {
return error_result("Missing required parameter: name");
};
@@ -722,10 +705,9 @@ fn parse_create_file_from_template(call: &super::tools::ToolCall) -> ContractToo
ContractToolExecutionResult {
success: true,
- message: format!("Creating file '{}' from template '{}'...", name, template_id),
+ message: format!("Creating empty file '{}'...", name),
data: None,
- request: Some(ContractToolRequest::CreateFileFromTemplate {
- template_id: template_id.to_string(),
+ request: Some(ContractToolRequest::CreateEmptyFile {
name: name.to_string(),
description,
}),
@@ -733,32 +715,20 @@ fn parse_create_file_from_template(call: &super::tools::ToolCall) -> ContractToo
}
}
-fn parse_create_empty_file(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let name = call.arguments.get("name").and_then(|v| v.as_str());
-
- let Some(name) = name else {
- return error_result("Missing required parameter: name");
- };
+// =============================================================================
+// Deliverable Management Tool Parsing
+// =============================================================================
- let description = call
+fn parse_mark_deliverable_complete(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
+ let deliverable_id = call
.arguments
- .get("description")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
+ .get("deliverable_id")
+ .and_then(|v| v.as_str());
- ContractToolExecutionResult {
- success: true,
- message: format!("Creating empty file '{}'...", name),
- data: None,
- request: Some(ContractToolRequest::CreateEmptyFile {
- name: name.to_string(),
- description,
- }),
- pending_questions: None,
- }
-}
+ let Some(deliverable_id) = deliverable_id else {
+ return error_result("Missing required parameter: deliverable_id");
+ };
-fn parse_list_available_templates(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
let phase = call
.arguments
.get("phase")
@@ -767,9 +737,12 @@ fn parse_list_available_templates(call: &super::tools::ToolCall) -> ContractTool
ContractToolExecutionResult {
success: true,
- message: "Listing available templates...".to_string(),
+ message: format!("Marking deliverable '{}' as complete...", deliverable_id),
data: None,
- request: Some(ContractToolRequest::ListAvailableTemplates { phase }),
+ request: Some(ContractToolRequest::MarkDeliverableComplete {
+ deliverable_id: deliverable_id.to_string(),
+ phase,
+ }),
pending_questions: None,
}
}
diff --git a/makima/src/llm/mod.rs b/makima/src/llm/mod.rs
index fc3802b..212876a 100644
--- a/makima/src/llm/mod.rs
+++ b/makima/src/llm/mod.rs
@@ -19,19 +19,18 @@ pub use contract_tools::{
pub use groq::GroqClient;
pub use mesh_tools::{parse_mesh_tool_call, MeshToolExecutionResult, MeshToolRequest, MESH_TOOLS};
pub use phase_guidance::{
- check_deliverables_met, check_phase_completion, check_phase_completion_for_type,
- format_checklist_markdown, generate_deliverable_prompt_guidance, get_next_phase_for_contract,
- get_phase_checklist, get_phase_checklist_for_type, get_phase_deliverables, get_phase_deliverables_for_type,
- should_auto_progress, AutoProgressAction, AutoProgressDecision, DeliverableCheckResult, DeliverableItem,
- DeliverableStatus, FileInfo, FilePriority, PhaseChecklist, PhaseDeliverables, RecommendedFile,
- TaskInfo, TaskStats,
+ check_deliverables_met, format_checklist_markdown, generate_deliverable_prompt_guidance,
+ get_next_phase_for_contract, get_phase_checklist_for_type, get_phase_deliverables,
+ get_phase_deliverables_for_type, should_auto_progress, AutoProgressAction, AutoProgressDecision,
+ Deliverable, DeliverableCheckResult, DeliverableItem, DeliverablePriority, DeliverableStatus,
+ PhaseChecklist, PhaseDeliverables, TaskInfo, TaskStats,
};
pub use task_output::{
analyze_task_output, format_parsed_tasks, parse_tasks_from_breakdown, ParsedTask,
PhaseImpact, SuggestedAction, TaskOutputAnalysis, TaskParseResult,
};
pub use markdown::{body_to_markdown, markdown_to_body};
-pub use templates::{all_templates, templates_for_phase, FileTemplate};
+pub use templates::{all_contract_types, ContractTypeTemplate};
pub use tools::{
execute_tool_call, Tool, ToolCall, ToolResult, UserAnswer, UserQuestion, VersionToolRequest,
AVAILABLE_TOOLS,
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);
}
}
diff --git a/makima/src/llm/templates.rs b/makima/src/llm/templates.rs
index 8d3c04d..48b7515 100644
--- a/makima/src/llm/templates.rs
+++ b/makima/src/llm/templates.rs
@@ -1,13 +1,10 @@
-//! Template definitions for phase-appropriate file structures.
+//! Contract type template definitions.
//!
-//! Templates provide starting structures for files based on the contract phase.
-//! Each phase has templates suited for that stage of work.
+//! Defines the available contract types and their workflow phases.
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
-use crate::db::models::BodyElement;
-
// =============================================================================
// Contract Type Templates (Workflow Definitions)
// =============================================================================
@@ -81,1009 +78,3 @@ fn execute_contract_type() -> ContractTypeTemplate {
is_builtin: true,
}
}
-
-// =============================================================================
-// File Templates
-// =============================================================================
-
-/// A file template with suggested structure
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-pub struct FileTemplate {
- /// Template identifier
- pub id: String,
- /// Display name
- pub name: String,
- /// Contract phase this template is designed for
- pub phase: String,
- /// Brief description of what this template is for
- pub description: String,
- /// Suggested body elements (structure only - content to be filled by LLM)
- pub suggested_body: Vec<BodyElement>,
-}
-
-/// Get templates appropriate for a given contract phase
-pub fn templates_for_phase(phase: &str) -> Vec<FileTemplate> {
- match phase {
- "research" => vec![
- research_notes_template(),
- competitor_analysis_template(),
- user_research_template(),
- ],
- "specify" => vec![
- requirements_template(),
- user_stories_template(),
- acceptance_criteria_template(),
- ],
- "plan" => vec![
- architecture_template(),
- technical_design_template(),
- task_breakdown_template(),
- ],
- "execute" => vec![
- dev_notes_template(),
- test_plan_template(),
- implementation_log_template(),
- ],
- "review" => vec![
- review_checklist_template(),
- release_notes_template(),
- retrospective_template(),
- ],
- _ => vec![],
- }
-}
-
-/// Get all available templates across all phases
-pub fn all_templates() -> Vec<FileTemplate> {
- vec![
- // Research phase
- research_notes_template(),
- competitor_analysis_template(),
- user_research_template(),
- // Specify phase
- requirements_template(),
- user_stories_template(),
- acceptance_criteria_template(),
- // Plan phase
- architecture_template(),
- technical_design_template(),
- task_breakdown_template(),
- // Execute phase
- dev_notes_template(),
- test_plan_template(),
- implementation_log_template(),
- // Review phase
- review_checklist_template(),
- release_notes_template(),
- retrospective_template(),
- ]
-}
-
-// =============================================================================
-// Research Phase Templates
-// =============================================================================
-
-fn research_notes_template() -> FileTemplate {
- FileTemplate {
- id: "research-notes".to_string(),
- name: "Research Notes".to_string(),
- phase: "research".to_string(),
- description: "Document findings, insights, and questions during research".to_string(),
- suggested_body: vec![
- BodyElement::Heading {
- level: 1,
- text: "Research Notes".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Context".to_string(),
- },
- BodyElement::Paragraph {
- text: "Describe the research objective and scope...".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Key Findings".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "Finding 1...".to_string(),
- "Finding 2...".to_string(),
- "Finding 3...".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Open Questions".to_string(),
- },
- BodyElement::List {
- ordered: true,
- items: vec![
- "Question to investigate...".to_string(),
- "Area needing more research...".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Next Steps".to_string(),
- },
- BodyElement::Paragraph {
- text: "Outline follow-up actions...".to_string(),
- },
- ],
- }
-}
-
-fn competitor_analysis_template() -> FileTemplate {
- FileTemplate {
- id: "competitor-analysis".to_string(),
- name: "Competitor Analysis".to_string(),
- phase: "research".to_string(),
- description: "Analyze competitors and market positioning".to_string(),
- suggested_body: vec![
- BodyElement::Heading {
- level: 1,
- text: "Competitor Analysis".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Market Overview".to_string(),
- },
- BodyElement::Paragraph {
- text: "Describe the market landscape...".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Competitor 1: [Name]".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "Strengths: ...".to_string(),
- "Weaknesses: ...".to_string(),
- "Key Features: ...".to_string(),
- "Pricing: ...".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Competitive Advantages".to_string(),
- },
- BodyElement::Paragraph {
- text: "Our differentiation strategy...".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Gaps & Opportunities".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec!["Opportunity 1...".to_string(), "Opportunity 2...".to_string()],
- },
- ],
- }
-}
-
-fn user_research_template() -> FileTemplate {
- FileTemplate {
- id: "user-research".to_string(),
- name: "User Research".to_string(),
- phase: "research".to_string(),
- description: "Document user interviews, surveys, and persona insights".to_string(),
- suggested_body: vec![
- BodyElement::Heading {
- level: 1,
- text: "User Research".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Research Method".to_string(),
- },
- BodyElement::Paragraph {
- text: "Describe the research methodology used...".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "User Personas".to_string(),
- },
- BodyElement::Heading {
- level: 3,
- text: "Persona 1: [Name]".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "Role: ...".to_string(),
- "Goals: ...".to_string(),
- "Pain Points: ...".to_string(),
- "Behaviors: ...".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Key Insights".to_string(),
- },
- BodyElement::List {
- ordered: true,
- items: vec!["Insight from research...".to_string()],
- },
- BodyElement::Heading {
- level: 2,
- text: "Recommendations".to_string(),
- },
- BodyElement::Paragraph {
- text: "Based on research findings...".to_string(),
- },
- ],
- }
-}
-
-// =============================================================================
-// Specify Phase Templates
-// =============================================================================
-
-fn requirements_template() -> FileTemplate {
- FileTemplate {
- id: "requirements".to_string(),
- name: "Requirements Document".to_string(),
- phase: "specify".to_string(),
- description: "Define functional and non-functional requirements".to_string(),
- suggested_body: vec![
- BodyElement::Heading {
- level: 1,
- text: "Requirements Document".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Overview".to_string(),
- },
- BodyElement::Paragraph {
- text: "Brief description of the feature/project...".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Functional Requirements".to_string(),
- },
- BodyElement::List {
- ordered: true,
- items: vec![
- "FR-001: The system shall...".to_string(),
- "FR-002: Users must be able to...".to_string(),
- "FR-003: ...".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Non-Functional Requirements".to_string(),
- },
- BodyElement::List {
- ordered: true,
- items: vec![
- "NFR-001: Performance - ...".to_string(),
- "NFR-002: Security - ...".to_string(),
- "NFR-003: Scalability - ...".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Constraints".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "Technical constraints...".to_string(),
- "Business constraints...".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Dependencies".to_string(),
- },
- BodyElement::Paragraph {
- text: "External dependencies and integrations...".to_string(),
- },
- ],
- }
-}
-
-fn user_stories_template() -> FileTemplate {
- FileTemplate {
- id: "user-stories".to_string(),
- name: "User Stories".to_string(),
- phase: "specify".to_string(),
- description: "Define features from the user's perspective".to_string(),
- suggested_body: vec![
- BodyElement::Heading {
- level: 1,
- text: "User Stories".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Epic: [Feature Name]".to_string(),
- },
- BodyElement::Paragraph {
- text: "High-level description of the epic...".to_string(),
- },
- BodyElement::Heading {
- level: 3,
- text: "US-001: [Story Title]".to_string(),
- },
- BodyElement::Paragraph {
- text: "As a [user type], I want to [action], so that [benefit].".to_string(),
- },
- BodyElement::Heading {
- level: 4,
- text: "Acceptance Criteria".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "Given... When... Then...".to_string(),
- "Given... When... Then...".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 3,
- text: "US-002: [Story Title]".to_string(),
- },
- BodyElement::Paragraph {
- text: "As a [user type], I want to [action], so that [benefit].".to_string(),
- },
- ],
- }
-}
-
-fn acceptance_criteria_template() -> FileTemplate {
- FileTemplate {
- id: "acceptance-criteria".to_string(),
- name: "Acceptance Criteria".to_string(),
- phase: "specify".to_string(),
- description: "Define testable conditions for feature completion".to_string(),
- suggested_body: vec![
- BodyElement::Heading {
- level: 1,
- text: "Acceptance Criteria".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Feature: [Name]".to_string(),
- },
- BodyElement::Paragraph {
- text: "Description of the feature being specified...".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Scenarios".to_string(),
- },
- BodyElement::Heading {
- level: 3,
- text: "Scenario 1: [Happy Path]".to_string(),
- },
- BodyElement::Code {
- language: Some("gherkin".to_string()),
- content: "Given [precondition]\nWhen [action]\nThen [expected result]".to_string(),
- },
- BodyElement::Heading {
- level: 3,
- text: "Scenario 2: [Edge Case]".to_string(),
- },
- BodyElement::Code {
- language: Some("gherkin".to_string()),
- content: "Given [precondition]\nWhen [action]\nThen [expected result]".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Out of Scope".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec!["Items explicitly not included...".to_string()],
- },
- ],
- }
-}
-
-// =============================================================================
-// Plan Phase Templates
-// =============================================================================
-
-fn architecture_template() -> FileTemplate {
- FileTemplate {
- id: "architecture".to_string(),
- name: "Architecture Document".to_string(),
- phase: "plan".to_string(),
- description: "Document system architecture and design decisions".to_string(),
- suggested_body: vec![
- BodyElement::Heading {
- level: 1,
- text: "Architecture Document".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Overview".to_string(),
- },
- BodyElement::Paragraph {
- text: "High-level architecture description...".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "System Components".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "Component A: Description and responsibility".to_string(),
- "Component B: Description and responsibility".to_string(),
- "Component C: Description and responsibility".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Data Flow".to_string(),
- },
- BodyElement::Paragraph {
- text: "Describe how data flows through the system...".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Technology Stack".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "Frontend: ...".to_string(),
- "Backend: ...".to_string(),
- "Database: ...".to_string(),
- "Infrastructure: ...".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Design Decisions".to_string(),
- },
- BodyElement::Heading {
- level: 3,
- text: "ADR-001: [Decision Title]".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "Context: ...".to_string(),
- "Decision: ...".to_string(),
- "Consequences: ...".to_string(),
- ],
- },
- ],
- }
-}
-
-fn technical_design_template() -> FileTemplate {
- FileTemplate {
- id: "technical-design".to_string(),
- name: "Technical Design".to_string(),
- phase: "plan".to_string(),
- description: "Detailed technical specification for implementation".to_string(),
- suggested_body: vec![
- BodyElement::Heading {
- level: 1,
- text: "Technical Design".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Purpose".to_string(),
- },
- BodyElement::Paragraph {
- text: "What this design document covers...".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "API Design".to_string(),
- },
- BodyElement::Code {
- language: Some("typescript".to_string()),
- content: "// Interface definitions\ninterface Example {\n // ...\n}".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Database Schema".to_string(),
- },
- BodyElement::Code {
- language: Some("sql".to_string()),
- content: "-- Table definitions\nCREATE TABLE example (\n -- ...\n);".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Implementation Notes".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "Key implementation consideration...".to_string(),
- "Performance consideration...".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Migration Strategy".to_string(),
- },
- BodyElement::Paragraph {
- text: "How to migrate from current state...".to_string(),
- },
- ],
- }
-}
-
-fn task_breakdown_template() -> FileTemplate {
- FileTemplate {
- id: "task-breakdown".to_string(),
- name: "Task Breakdown".to_string(),
- phase: "plan".to_string(),
- description: "Break down work into implementable tasks".to_string(),
- suggested_body: vec![
- BodyElement::Heading {
- level: 1,
- text: "Task Breakdown".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Overview".to_string(),
- },
- BodyElement::Paragraph {
- text: "Summary of the work to be done...".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Phase 1: Foundation".to_string(),
- },
- BodyElement::List {
- ordered: true,
- items: vec![
- "[ ] Task 1: Set up project structure".to_string(),
- "[ ] Task 2: Configure development environment".to_string(),
- "[ ] Task 3: Create base components".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Phase 2: Core Features".to_string(),
- },
- BodyElement::List {
- ordered: true,
- items: vec![
- "[ ] Task 4: Implement feature A".to_string(),
- "[ ] Task 5: Implement feature B".to_string(),
- "[ ] Task 6: Add tests".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Phase 3: Polish & Deploy".to_string(),
- },
- BodyElement::List {
- ordered: true,
- items: vec![
- "[ ] Task 7: Error handling".to_string(),
- "[ ] Task 8: Documentation".to_string(),
- "[ ] Task 9: Deployment".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Dependencies".to_string(),
- },
- BodyElement::Paragraph {
- text: "Task dependencies and blockers...".to_string(),
- },
- ],
- }
-}
-
-// =============================================================================
-// Execute Phase Templates
-// =============================================================================
-
-fn dev_notes_template() -> FileTemplate {
- FileTemplate {
- id: "dev-notes".to_string(),
- name: "Development Notes".to_string(),
- phase: "execute".to_string(),
- description: "Track implementation details, decisions, and learnings".to_string(),
- suggested_body: vec![
- BodyElement::Heading {
- level: 1,
- text: "Development Notes".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Current Status".to_string(),
- },
- BodyElement::Paragraph {
- text: "Brief summary of implementation progress...".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Implementation Details".to_string(),
- },
- BodyElement::Heading {
- level: 3,
- text: "[Component/Feature Name]".to_string(),
- },
- BodyElement::Paragraph {
- text: "How this was implemented and why...".to_string(),
- },
- BodyElement::Code {
- language: Some("typescript".to_string()),
- content: "// Key code snippet or example".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Challenges & Solutions".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "Challenge: ... | Solution: ...".to_string(),
- "Challenge: ... | Solution: ...".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "TODOs".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "[ ] Remaining item...".to_string(),
- "[ ] Follow-up task...".to_string(),
- ],
- },
- ],
- }
-}
-
-fn test_plan_template() -> FileTemplate {
- FileTemplate {
- id: "test-plan".to_string(),
- name: "Test Plan".to_string(),
- phase: "execute".to_string(),
- description: "Document testing strategy and test cases".to_string(),
- suggested_body: vec![
- BodyElement::Heading {
- level: 1,
- text: "Test Plan".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Test Scope".to_string(),
- },
- BodyElement::Paragraph {
- text: "What is being tested and the testing approach...".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Test Types".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "Unit Tests: Component-level testing".to_string(),
- "Integration Tests: API and service integration".to_string(),
- "E2E Tests: User flow testing".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Test Cases".to_string(),
- },
- BodyElement::Heading {
- level: 3,
- text: "TC-001: [Test Name]".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "Preconditions: ...".to_string(),
- "Steps: ...".to_string(),
- "Expected Result: ...".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Test Data".to_string(),
- },
- BodyElement::Paragraph {
- text: "Required test data and fixtures...".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Test Results".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "[ ] TC-001: Pending".to_string(),
- "[ ] TC-002: Pending".to_string(),
- ],
- },
- ],
- }
-}
-
-fn implementation_log_template() -> FileTemplate {
- FileTemplate {
- id: "implementation-log".to_string(),
- name: "Implementation Log".to_string(),
- phase: "execute".to_string(),
- description: "Chronological log of implementation progress".to_string(),
- suggested_body: vec![
- BodyElement::Heading {
- level: 1,
- text: "Implementation Log".to_string(),
- },
- BodyElement::Paragraph {
- text: "Tracking daily progress and decisions...".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "[Date]".to_string(),
- },
- BodyElement::Heading {
- level: 3,
- text: "Completed".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec!["What was accomplished...".to_string()],
- },
- BodyElement::Heading {
- level: 3,
- text: "In Progress".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec!["Current work...".to_string()],
- },
- BodyElement::Heading {
- level: 3,
- text: "Blockers".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec!["Any blockers or issues...".to_string()],
- },
- BodyElement::Heading {
- level: 3,
- text: "Notes".to_string(),
- },
- BodyElement::Paragraph {
- text: "Additional context or decisions made...".to_string(),
- },
- ],
- }
-}
-
-// =============================================================================
-// Review Phase Templates
-// =============================================================================
-
-fn review_checklist_template() -> FileTemplate {
- FileTemplate {
- id: "review-checklist".to_string(),
- name: "Review Checklist".to_string(),
- phase: "review".to_string(),
- description: "Comprehensive checklist for code and feature review".to_string(),
- suggested_body: vec![
- BodyElement::Heading {
- level: 1,
- text: "Review Checklist".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Code Quality".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "[ ] Code follows style guidelines".to_string(),
- "[ ] No unnecessary complexity".to_string(),
- "[ ] Functions are well-named and focused".to_string(),
- "[ ] No dead code or commented-out code".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Testing".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "[ ] Unit tests pass".to_string(),
- "[ ] Integration tests pass".to_string(),
- "[ ] Edge cases covered".to_string(),
- "[ ] Test coverage acceptable".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Security".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "[ ] No hardcoded credentials".to_string(),
- "[ ] Input validation in place".to_string(),
- "[ ] Authentication/authorization correct".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Documentation".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "[ ] README updated".to_string(),
- "[ ] API documentation complete".to_string(),
- "[ ] Inline comments where needed".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Review Notes".to_string(),
- },
- BodyElement::Paragraph {
- text: "Additional review comments and feedback...".to_string(),
- },
- ],
- }
-}
-
-fn release_notes_template() -> FileTemplate {
- FileTemplate {
- id: "release-notes".to_string(),
- name: "Release Notes".to_string(),
- phase: "review".to_string(),
- description: "Document changes for release communication".to_string(),
- suggested_body: vec![
- BodyElement::Heading {
- level: 1,
- text: "Release Notes - v[X.Y.Z]".to_string(),
- },
- BodyElement::Paragraph {
- text: "Release date: [DATE]".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "Highlights".to_string(),
- },
- BodyElement::Paragraph {
- text: "Key features and improvements in this release...".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "New Features".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "Feature 1: Description".to_string(),
- "Feature 2: Description".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Improvements".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "Improvement 1: Description".to_string(),
- "Improvement 2: Description".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Bug Fixes".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "Fixed: Issue description".to_string(),
- "Fixed: Issue description".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Breaking Changes".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec!["Breaking change description (if any)...".to_string()],
- },
- BodyElement::Heading {
- level: 2,
- text: "Known Issues".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec!["Known issue (if any)...".to_string()],
- },
- ],
- }
-}
-
-fn retrospective_template() -> FileTemplate {
- FileTemplate {
- id: "retrospective".to_string(),
- name: "Retrospective".to_string(),
- phase: "review".to_string(),
- description: "Reflect on the project and capture learnings".to_string(),
- suggested_body: vec![
- BodyElement::Heading {
- level: 1,
- text: "Retrospective".to_string(),
- },
- BodyElement::Paragraph {
- text: "Project: [Name] | Date: [DATE]".to_string(),
- },
- BodyElement::Heading {
- level: 2,
- text: "What Went Well".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "Success 1...".to_string(),
- "Success 2...".to_string(),
- "Success 3...".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "What Could Be Improved".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "Area for improvement 1...".to_string(),
- "Area for improvement 2...".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Lessons Learned".to_string(),
- },
- BodyElement::List {
- ordered: true,
- items: vec![
- "Key lesson from this project...".to_string(),
- "Technical insight gained...".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Action Items".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "[ ] Action to improve future projects...".to_string(),
- "[ ] Process change to implement...".to_string(),
- ],
- },
- BodyElement::Heading {
- level: 2,
- text: "Metrics".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec![
- "Timeline: Planned vs Actual".to_string(),
- "Scope: Delivered vs Planned".to_string(),
- "Quality: Bug count, test coverage".to_string(),
- ],
- },
- ],
- }
-}
diff --git a/makima/src/llm/tools.rs b/makima/src/llm/tools.rs
index 1e43c40..c192398 100644
--- a/makima/src/llm/tools.rs
+++ b/makima/src/llm/tools.rs
@@ -5,7 +5,6 @@ use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::db::models::{BodyElement, ChartType, TranscriptEntry};
-use crate::llm::templates;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tool {
@@ -412,36 +411,6 @@ pub static AVAILABLE_TOOLS: once_cell::sync::Lazy<Vec<Tool>> =
"required": ["target_version"]
}),
},
- // Template tools
- Tool {
- name: "suggest_templates".to_string(),
- description: "Get suggested file templates based on a contract phase. Returns templates with predefined structures appropriate for research, specify, plan, execute, or review phases. Use this to help users start documents with proper structure.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "phase": {
- "type": "string",
- "enum": ["research", "specify", "plan", "execute", "review"],
- "description": "The contract phase to get templates for. If not provided, returns all templates."
- }
- },
- "required": []
- }),
- },
- Tool {
- name: "apply_template".to_string(),
- description: "Apply a template to the current file, replacing the body with the template structure. The template provides a starting structure that should be customized for the user's needs.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "template_id": {
- "type": "string",
- "description": "The template ID to apply (e.g., 'research-notes', 'requirements', 'architecture')"
- }
- },
- "required": ["template_id"]
- }),
- },
]
});
@@ -531,9 +500,6 @@ pub fn execute_tool_call(
"list_versions" => execute_list_versions(),
"read_version" => execute_read_version(call),
"restore_version" => execute_restore_version(call),
- // Template tools
- "suggest_templates" => execute_suggest_templates(call),
- "apply_template" => execute_apply_template(call),
_ => ToolExecutionResult {
result: ToolResult {
success: false,
@@ -1648,131 +1614,6 @@ fn execute_restore_version(call: &ToolCall) -> ToolExecutionResult {
}
}
-// =============================================================================
-// Template Tool Execution Functions
-// =============================================================================
-
-fn execute_suggest_templates(call: &ToolCall) -> ToolExecutionResult {
- let phase = call.arguments.get("phase").and_then(|v| v.as_str());
-
- let template_list = match phase {
- Some(p) => templates::templates_for_phase(p),
- None => templates::all_templates(),
- };
-
- if template_list.is_empty() {
- return ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: format!(
- "No templates available for phase: {}",
- phase.unwrap_or("(none)")
- ),
- },
- new_body: None,
- new_summary: None,
- parsed_data: Some(json!([])),
- version_request: None,
- pending_questions: None,
- };
- }
-
- // Convert templates to JSON (without the full body for display)
- let templates_json: Vec<serde_json::Value> = template_list
- .iter()
- .map(|t| {
- json!({
- "id": t.id,
- "name": t.name,
- "phase": t.phase,
- "description": t.description,
- "elementCount": t.suggested_body.len()
- })
- })
- .collect();
-
- let phase_msg = phase
- .map(|p| format!(" for '{}' phase", p))
- .unwrap_or_default();
-
- ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: format!(
- "Found {} template(s){}. Use apply_template with a template_id to apply one.",
- templates_json.len(),
- phase_msg
- ),
- },
- new_body: None,
- new_summary: None,
- parsed_data: Some(json!(templates_json)),
- version_request: None,
- pending_questions: None,
- }
-}
-
-fn execute_apply_template(call: &ToolCall) -> ToolExecutionResult {
- let template_id = call
- .arguments
- .get("template_id")
- .and_then(|v| v.as_str());
-
- let Some(template_id) = template_id else {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: "Missing template_id parameter".to_string(),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- };
-
- // Find the template
- let all = templates::all_templates();
- let template = all.iter().find(|t| t.id == template_id);
-
- let Some(template) = template else {
- let available: Vec<String> = all.iter().map(|t| t.id.clone()).collect();
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: format!(
- "Template '{}' not found. Available: {}",
- template_id,
- available.join(", ")
- ),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- };
-
- ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: format!(
- "Applied template '{}' ({}) with {} elements. You can now customize the content.",
- template.name,
- template.phase,
- template.suggested_body.len()
- ),
- },
- new_body: Some(template.suggested_body.clone()),
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- }
-}
-
/// Convert serde_json::Value to jaq_interpret::Val
fn json_to_jaq(value: &serde_json::Value) -> jaq_interpret::Val {
match value {
diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs
index 28c3436..e035368 100644
--- a/makima/src/server/handlers/contract_chat.rs
+++ b/makima/src/server/handlers/contract_chat.rs
@@ -19,11 +19,11 @@ use crate::db::{
repository,
};
use crate::llm::{
- all_templates, analyze_task_output, body_to_markdown, format_checklist_markdown,
+ analyze_task_output, body_to_markdown, format_checklist_markdown,
format_parsed_tasks, parse_tasks_from_breakdown,
claude::{self, ClaudeClient, ClaudeError, ClaudeModel},
groq::{GroqClient, GroqError, Message, ToolCallResponse},
- parse_contract_tool_call, templates_for_phase, ContractToolRequest, FileInfo,
+ parse_contract_tool_call, ContractToolRequest,
LlmModel, TaskInfo, ToolCall, ToolResult, UserQuestion, CONTRACT_TOOLS,
format_transcript_for_analysis, calculate_speaker_stats,
build_analysis_prompt, parse_analysis_response,
@@ -441,36 +441,29 @@ fn build_contract_context(contract: &crate::db::models::ContractWithRelations) -
context.push_str(&format!("Description: {}\n", desc));
}
- // Build phase checklist
- let file_infos: Vec<FileInfo> = contract.files.iter().map(|f| FileInfo {
- id: f.id,
- name: f.name.clone(),
- contract_phase: f.contract_phase.clone(),
- }).collect();
+ // Get completed deliverables for the current phase
+ let completed_deliverables = c.get_completed_deliverables(&c.phase);
+ // Build task infos for checklist
let task_infos: Vec<TaskInfo> = contract.tasks.iter().map(|t| TaskInfo {
- id: t.id,
name: t.name.clone(),
status: t.status.clone(),
}).collect();
let has_repository = !contract.repositories.is_empty();
- let phase_checklist = crate::llm::get_phase_checklist_for_type(&c.phase, &file_infos, &task_infos, has_repository, &c.contract_type);
+ let phase_checklist = crate::llm::get_phase_checklist_for_type(&c.phase, &completed_deliverables, &task_infos, has_repository, &c.contract_type);
// Add phase checklist to context
context.push_str("\n");
context.push_str(&format_checklist_markdown(&phase_checklist));
// Add deliverable check result for phase transition readiness
- // Note: pr_url is not available in TaskSummary, so we pass None here
- // Full PR checking should be done via the check_deliverables_met tool
let deliverable_check = crate::llm::check_deliverables_met(
&c.phase,
&c.contract_type,
- &file_infos,
+ &completed_deliverables,
&task_infos,
has_repository,
- None, // pr_url not available in TaskSummary
);
// Add deliverable prompt guidance
@@ -1204,23 +1197,7 @@ async fn handle_contract_request(
}
}
- ContractToolRequest::CreateFileFromTemplate {
- template_id,
- name,
- description,
- } => {
- // Find the template
- let templates = all_templates();
- let template = templates.iter().find(|t| t.id == template_id);
-
- let Some(template) = template else {
- return ContractRequestResult {
- success: false,
- message: format!("Template '{}' not found", template_id),
- data: None,
- };
- };
-
+ ContractToolRequest::CreateEmptyFile { name, description } => {
// Verify contract exists and get current phase
let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
Ok(Some(c)) => c,
@@ -1240,32 +1217,25 @@ async fn handle_contract_request(
}
};
- // Use template's phase if available, otherwise use contract's current phase
- let contract_phase = Some(template.phase.clone()).or(Some(contract.phase.clone()));
-
- // Create the file (contract_id is now required)
+ // Create the file with current contract phase
let create_req = crate::db::models::CreateFileRequest {
contract_id,
name: Some(name.clone()),
description,
- body: template.suggested_body.clone(),
+ body: Vec::new(),
transcript: Vec::new(),
location: None,
repo_file_path: None,
- contract_phase,
+ contract_phase: Some(contract.phase.clone()),
};
match repository::create_file_for_owner(pool, owner_id, create_req).await {
Ok(file) => ContractRequestResult {
success: true,
- message: format!(
- "Created file '{}' from template '{}'",
- name, template.name
- ),
+ message: format!("Created empty file '{}'", name),
data: Some(json!({
"fileId": file.id,
"name": file.name,
- "templateId": template_id,
})),
},
Err(e) => ContractRequestResult {
@@ -1276,8 +1246,11 @@ async fn handle_contract_request(
}
}
- ContractToolRequest::CreateEmptyFile { name, description } => {
- // Verify contract exists and get current phase
+ ContractToolRequest::MarkDeliverableComplete {
+ deliverable_id,
+ phase,
+ } => {
+ // Get the contract to determine current phase and contract type
let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
Ok(Some(c)) => c,
Ok(None) => {
@@ -1296,61 +1269,60 @@ async fn handle_contract_request(
}
};
- // Create the file with current contract phase
- let create_req = crate::db::models::CreateFileRequest {
- contract_id,
- name: Some(name.clone()),
- description,
- body: Vec::new(),
- transcript: Vec::new(),
- location: None,
- repo_file_path: None,
- contract_phase: Some(contract.phase.clone()),
- };
+ // Use specified phase or default to current contract phase
+ let target_phase = phase.unwrap_or_else(|| contract.phase.clone());
- match repository::create_file_for_owner(pool, owner_id, create_req).await {
- Ok(file) => ContractRequestResult {
+ // Validate the deliverable ID exists for this phase/contract type
+ let phase_deliverables = crate::llm::get_phase_deliverables_for_type(&target_phase, &contract.contract_type);
+ let deliverable_exists = phase_deliverables.deliverables.iter().any(|d| d.id == deliverable_id);
+
+ if !deliverable_exists {
+ let valid_ids: Vec<&str> = phase_deliverables.deliverables.iter().map(|d| d.id.as_str()).collect();
+ return ContractRequestResult {
+ success: false,
+ message: format!(
+ "Invalid deliverable_id '{}' for {} phase. Valid IDs: {:?}",
+ deliverable_id, target_phase, valid_ids
+ ),
+ data: None,
+ };
+ }
+
+ // Check if already completed
+ if contract.is_deliverable_complete(&target_phase, &deliverable_id) {
+ return ContractRequestResult {
success: true,
- message: format!("Created empty file '{}'", name),
+ message: format!("Deliverable '{}' is already marked complete for {} phase", deliverable_id, target_phase),
data: Some(json!({
- "fileId": file.id,
- "name": file.name,
+ "deliverableId": deliverable_id,
+ "phase": target_phase,
+ "alreadyComplete": true,
})),
- },
+ };
+ }
+
+ // Mark the deliverable as complete
+ match repository::mark_deliverable_complete(pool, contract_id, &target_phase, &deliverable_id).await {
+ Ok(updated_contract) => {
+ let completed = updated_contract.get_completed_deliverables(&target_phase);
+ ContractRequestResult {
+ success: true,
+ message: format!("Marked deliverable '{}' as complete for {} phase", deliverable_id, target_phase),
+ data: Some(json!({
+ "deliverableId": deliverable_id,
+ "phase": target_phase,
+ "completedDeliverables": completed,
+ })),
+ }
+ }
Err(e) => ContractRequestResult {
success: false,
- message: format!("Failed to create file: {}", e),
+ message: format!("Failed to mark deliverable complete: {}", e),
data: None,
},
}
}
- ContractToolRequest::ListAvailableTemplates { phase } => {
- let templates = if let Some(p) = phase {
- templates_for_phase(&p)
- } else {
- all_templates()
- };
-
- let template_data: Vec<serde_json::Value> = templates
- .iter()
- .map(|t| {
- json!({
- "id": t.id,
- "name": t.name,
- "phase": t.phase,
- "description": t.description,
- })
- })
- .collect();
-
- ContractRequestResult {
- success: true,
- message: format!("Found {} templates", templates.len()),
- data: Some(json!({ "templates": template_data })),
- }
- }
-
ContractToolRequest::CreateContractTask {
name,
plan,
@@ -1666,8 +1638,8 @@ async fn handle_contract_request(
};
let phase_info = get_phase_description(&contract.phase);
- let templates = templates_for_phase(&contract.phase);
- let template_names: Vec<String> = templates.iter().map(|t| t.name.clone()).collect();
+ let phase_deliverables = crate::llm::get_phase_deliverables_for_type(&contract.phase, &contract.contract_type);
+ let deliverable_names: Vec<String> = phase_deliverables.deliverables.iter().map(|d| d.name.clone()).collect();
ContractRequestResult {
success: true,
@@ -1676,7 +1648,8 @@ async fn handle_contract_request(
"phase": contract.phase,
"description": phase_info.0,
"activities": phase_info.1,
- "suggestedTemplates": template_names,
+ "deliverables": deliverable_names,
+ "guidance": phase_deliverables.guidance,
"nextPhase": get_next_phase(&contract.phase),
})),
}
@@ -1764,30 +1737,22 @@ async fn handle_contract_request(
}
};
- let file_infos: Vec<FileInfo> = cwr.files.iter().map(|f| FileInfo {
- id: f.id,
- name: f.name.clone(),
- contract_phase: f.contract_phase.clone(),
- }).collect();
+ // Get completed deliverables for the current phase
+ let completed_deliverables = cwr.contract.get_completed_deliverables(current_phase);
let task_infos: Vec<TaskInfo> = cwr.tasks.iter().map(|t| TaskInfo {
- id: t.id,
name: t.name.clone(),
status: t.status.clone(),
}).collect();
let has_repository = !cwr.repositories.is_empty();
- // Note: pr_url is not available in TaskSummary, so we skip PR checking here
- // For simple contracts, the PR deliverable check will need to be done
- // by fetching full task details if needed
let check_result = crate::llm::check_deliverables_met(
current_phase,
&contract.contract_type,
- &file_infos,
+ &completed_deliverables,
&task_infos,
has_repository,
- None, // pr_url not available in TaskSummary
);
// Block transition if deliverables are not met
@@ -1895,17 +1860,17 @@ async fn handle_contract_request(
match repository::change_contract_phase_for_owner(pool, contract_id, owner_id, &new_phase).await {
Ok(Some(updated)) => {
// Get deliverables for the new phase (using contract type)
- let deliverables = crate::llm::get_phase_deliverables_for_type(&new_phase, &contract.contract_type);
+ let phase_deliverables = crate::llm::get_phase_deliverables_for_type(&new_phase, &contract.contract_type);
- // Build suggested files list
- let suggested_files: Vec<serde_json::Value> = deliverables
- .recommended_files
+ // Build deliverables list
+ let deliverables_list: Vec<serde_json::Value> = phase_deliverables
+ .deliverables
.iter()
- .map(|f| json!({
- "templateId": f.template_id,
- "name": f.name_suggestion,
- "priority": format!("{:?}", f.priority).to_lowercase(),
- "description": f.description,
+ .map(|d| json!({
+ "id": d.id,
+ "name": d.name,
+ "priority": format!("{:?}", d.priority).to_lowercase(),
+ "description": d.description,
}))
.collect();
@@ -1913,16 +1878,16 @@ async fn handle_contract_request(
success: true,
message: format!(
"Advanced contract from '{}' to '{}' phase. {}",
- current_phase, new_phase, deliverables.guidance
+ current_phase, new_phase, phase_deliverables.guidance
),
data: Some(json!({
"status": "advanced",
"previousPhase": current_phase,
"newPhase": updated.phase,
- "phaseGuidance": deliverables.guidance,
- "suggestedFiles": suggested_files,
- "requiresRepository": deliverables.requires_repository,
- "requiresTasks": deliverables.requires_tasks,
+ "phaseGuidance": phase_deliverables.guidance,
+ "deliverables": deliverables_list,
+ "requiresRepository": phase_deliverables.requires_repository,
+ "requiresTasks": phase_deliverables.requires_tasks,
})),
}
},
@@ -2028,20 +1993,15 @@ async fn handle_contract_request(
ContractToolRequest::GetPhaseChecklist => {
match get_contract_with_relations(pool, contract_id, owner_id).await {
Ok(Some(cwr)) => {
- let file_infos: Vec<FileInfo> = cwr.files.iter().map(|f| FileInfo {
- id: f.id,
- name: f.name.clone(),
- contract_phase: f.contract_phase.clone(),
- }).collect();
+ let completed_deliverables = cwr.contract.get_completed_deliverables(&cwr.contract.phase);
let task_infos: Vec<TaskInfo> = cwr.tasks.iter().map(|t| TaskInfo {
- id: t.id,
name: t.name.clone(),
status: t.status.clone(),
}).collect();
let has_repository = !cwr.repositories.is_empty();
- let checklist = crate::llm::get_phase_checklist_for_type(&cwr.contract.phase, &file_infos, &task_infos, has_repository, &cwr.contract.contract_type);
+ let checklist = crate::llm::get_phase_checklist_for_type(&cwr.contract.phase, &completed_deliverables, &task_infos, has_repository, &cwr.contract.contract_type);
ContractRequestResult {
success: true,
@@ -2049,7 +2009,7 @@ async fn handle_contract_request(
data: Some(json!({
"phase": checklist.phase,
"completionPercentage": checklist.completion_percentage,
- "deliverables": checklist.file_deliverables,
+ "deliverables": checklist.deliverables,
"hasRepository": checklist.has_repository,
"repositoryRequired": checklist.repository_required,
"taskStats": checklist.task_stats,
@@ -2074,41 +2034,30 @@ async fn handle_contract_request(
ContractToolRequest::CheckDeliverablesMet => {
match get_contract_with_relations(pool, contract_id, owner_id).await {
Ok(Some(cwr)) => {
- let file_infos: Vec<FileInfo> = cwr.files.iter().map(|f| FileInfo {
- id: f.id,
- name: f.name.clone(),
- contract_phase: f.contract_phase.clone(),
- }).collect();
+ let completed_deliverables = cwr.contract.get_completed_deliverables(&cwr.contract.phase);
let task_infos: Vec<TaskInfo> = cwr.tasks.iter().map(|t| TaskInfo {
- id: t.id,
name: t.name.clone(),
status: t.status.clone(),
}).collect();
let has_repository = !cwr.repositories.is_empty();
- // Note: pr_url is not available in TaskSummary
- // For simple contracts needing PR checking, full task details would need to be fetched
- // For now, we pass None and the LLM can guide the user to ensure a PR exists
-
let check_result = crate::llm::check_deliverables_met(
&cwr.contract.phase,
&cwr.contract.contract_type,
- &file_infos,
+ &completed_deliverables,
&task_infos,
has_repository,
- None, // pr_url not available in TaskSummary
);
// Check if we should auto-progress
let auto_progress = crate::llm::should_auto_progress(
&cwr.contract.phase,
&cwr.contract.contract_type,
- &file_infos,
+ &completed_deliverables,
&task_infos,
has_repository,
- None, // pr_url not available in TaskSummary
cwr.contract.autonomous_loop,
);
diff --git a/makima/src/server/handlers/contract_daemon.rs b/makima/src/server/handlers/contract_daemon.rs
index 5b23831..5f56f06 100644
--- a/makima/src/server/handlers/contract_daemon.rs
+++ b/makima/src/server/handlers/contract_daemon.rs
@@ -15,7 +15,7 @@ use utoipa::ToSchema;
use uuid::Uuid;
use crate::db::{models::FileSummary, repository};
-use crate::llm::phase_guidance::{self, FileInfo, PhaseChecklist, TaskInfo};
+use crate::llm::phase_guidance::{self, PhaseChecklist, TaskInfo};
use crate::server::auth::Authenticated;
use crate::server::messages::ApiError;
use crate::server::state::SharedState;
@@ -242,28 +242,14 @@ pub async fn get_contract_checklist(
}
};
- // Get files for this contract
- let files = match repository::list_files_in_contract(pool, id, auth.owner_id).await {
- Ok(f) => f
- .into_iter()
- .map(|f| FileInfo {
- id: f.id,
- name: f.name,
- contract_phase: f.contract_phase,
- })
- .collect::<Vec<_>>(),
- Err(e) => {
- tracing::warn!("Failed to get files for contract {}: {}", id, e);
- Vec::new()
- }
- };
+ // Get completed deliverables for the current phase
+ let completed_deliverables = contract.get_completed_deliverables(&contract.phase);
// Get tasks for this contract
let tasks = match repository::list_tasks_in_contract(pool, id, auth.owner_id).await {
Ok(t) => t
.into_iter()
.map(|t| TaskInfo {
- id: t.id,
name: t.name,
status: t.status,
})
@@ -280,7 +266,7 @@ pub async fn get_contract_checklist(
Err(_) => false,
};
- let checklist = phase_guidance::get_phase_checklist_for_type(&contract.phase, &files, &tasks, has_repository, &contract.contract_type);
+ let checklist = phase_guidance::get_phase_checklist_for_type(&contract.phase, &completed_deliverables, &tasks, has_repository, &contract.contract_type);
Json(checklist).into_response()
}
@@ -463,24 +449,14 @@ pub async fn get_suggest_action(
}
};
- // Get files and tasks for checklist
- let files = repository::list_files_in_contract(pool, id, auth.owner_id)
- .await
- .unwrap_or_default()
- .into_iter()
- .map(|f| FileInfo {
- id: f.id,
- name: f.name,
- contract_phase: f.contract_phase,
- })
- .collect::<Vec<_>>();
+ // Get completed deliverables and tasks for checklist
+ let completed_deliverables = contract.get_completed_deliverables(&contract.phase);
let tasks = repository::list_tasks_in_contract(pool, id, auth.owner_id)
.await
.unwrap_or_default()
.into_iter()
.map(|t| TaskInfo {
- id: t.id,
name: t.name,
status: t.status,
})
@@ -491,7 +467,7 @@ pub async fn get_suggest_action(
.map(|r| !r.is_empty())
.unwrap_or(false);
- let checklist = phase_guidance::get_phase_checklist_for_type(&contract.phase, &files, &tasks, has_repository, &contract.contract_type);
+ let checklist = phase_guidance::get_phase_checklist_for_type(&contract.phase, &completed_deliverables, &tasks, has_repository, &contract.contract_type);
// Determine suggested action based on checklist
let (action, description) = if !checklist.suggestions.is_empty() {
diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs
index f16f33d..de3164c 100644
--- a/makima/src/server/handlers/contracts.rs
+++ b/makima/src/server/handlers/contracts.rs
@@ -6,6 +6,8 @@ use axum::{
response::IntoResponse,
Json,
};
+use serde::Deserialize;
+use utoipa::ToSchema;
use uuid::Uuid;
use crate::db::models::{
@@ -1423,6 +1425,134 @@ pub async fn change_phase(
}
// =============================================================================
+// Deliverables
+// =============================================================================
+
+/// Request body for marking a deliverable complete
+#[derive(Debug, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct MarkDeliverableRequest {
+ /// The deliverable ID to mark as complete (e.g., 'plan-document', 'pull-request')
+ pub deliverable_id: String,
+ /// Phase the deliverable belongs to. Defaults to current contract phase if not specified.
+ pub phase: Option<String>,
+}
+
+/// Mark a deliverable as complete for a contract phase.
+#[utoipa::path(
+ post,
+ path = "/api/v1/contracts/{id}/deliverables/complete",
+ params(
+ ("id" = Uuid, Path, description = "Contract ID")
+ ),
+ request_body = MarkDeliverableRequest,
+ responses(
+ (status = 200, description = "Deliverable marked complete", body = serde_json::Value),
+ (status = 400, description = "Invalid deliverable ID", body = ApiError),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 404, description = "Contract not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ security(
+ ("bearer_auth" = []),
+ ("api_key" = [])
+ ),
+ tag = "Contracts"
+)]
+pub async fn mark_deliverable_complete(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+ Json(req): Json<MarkDeliverableRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Get contract
+ let contract = match repository::get_contract_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(c)) => c,
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Contract not found")),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ tracing::error!("Failed to get contract {}: {}", id, e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response();
+ }
+ };
+
+ // Use specified phase or default to current contract phase
+ let target_phase = req.phase.unwrap_or_else(|| contract.phase.clone());
+
+ // Validate the deliverable ID exists for this phase/contract type
+ let phase_deliverables = crate::llm::get_phase_deliverables_for_type(&target_phase, &contract.contract_type);
+ let deliverable = phase_deliverables.deliverables.iter().find(|d| d.id == req.deliverable_id);
+
+ if deliverable.is_none() {
+ let valid_ids: Vec<&str> = phase_deliverables.deliverables.iter().map(|d| d.id.as_str()).collect();
+ return (
+ StatusCode::BAD_REQUEST,
+ Json(ApiError::new(
+ "INVALID_DELIVERABLE",
+ format!(
+ "Invalid deliverable_id '{}' for {} phase (contract type: {}). Valid IDs: {:?}",
+ req.deliverable_id, target_phase, contract.contract_type, valid_ids
+ ),
+ )),
+ )
+ .into_response();
+ }
+
+ // Check if already completed
+ if contract.is_deliverable_complete(&target_phase, &req.deliverable_id) {
+ return Json(serde_json::json!({
+ "success": true,
+ "message": format!("Deliverable '{}' is already marked complete for {} phase", req.deliverable_id, target_phase),
+ "deliverableId": req.deliverable_id,
+ "phase": target_phase,
+ "alreadyComplete": true,
+ }))
+ .into_response();
+ }
+
+ // Mark the deliverable as complete
+ match repository::mark_deliverable_complete(pool, id, &target_phase, &req.deliverable_id).await {
+ Ok(updated_contract) => {
+ let completed = updated_contract.get_completed_deliverables(&target_phase);
+ Json(serde_json::json!({
+ "success": true,
+ "message": format!("Marked deliverable '{}' as complete for {} phase", req.deliverable_id, target_phase),
+ "deliverableId": req.deliverable_id,
+ "phase": target_phase,
+ "completedDeliverables": completed,
+ }))
+ .into_response()
+ }
+ Err(e) => {
+ tracing::error!("Failed to mark deliverable complete for contract {}: {}", id, e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+// =============================================================================
// Events
// =============================================================================
diff --git a/makima/src/server/handlers/mesh_daemon.rs b/makima/src/server/handlers/mesh_daemon.rs
index f5a3c10..270118f 100644
--- a/makima/src/server/handlers/mesh_daemon.rs
+++ b/makima/src/server/handlers/mesh_daemon.rs
@@ -23,7 +23,7 @@ use uuid::Uuid;
use crate::db::models::Task;
use crate::db::repository;
-use crate::llm::{check_deliverables_met, FileInfo, TaskInfo};
+use crate::llm::{check_deliverables_met, TaskInfo};
use crate::server::auth::{hash_api_key, API_KEY_HEADER};
use crate::server::messages::ApiError;
use crate::server::state::{
@@ -494,12 +494,6 @@ async fn compute_action_directive(
_ => return None,
};
- // Get files
- let files = match repository::list_files_in_contract(pool, contract_id, owner_id).await {
- Ok(f) => f,
- _ => return None,
- };
-
// Get tasks (non-supervisor only)
let tasks = match repository::list_tasks_by_contract(pool, contract_id, owner_id).await {
Ok(t) => t.into_iter().filter(|t| !t.is_supervisor).collect::<Vec<_>>(),
@@ -512,20 +506,12 @@ async fn compute_action_directive(
_ => return None,
};
- // Convert to FileInfo and TaskInfo for check_deliverables_met
- let file_infos: Vec<FileInfo> = files
- .iter()
- .map(|f| FileInfo {
- id: f.id,
- name: f.name.clone(),
- contract_phase: f.contract_phase.clone(),
- })
- .collect();
+ // Get completed deliverables for the current phase
+ let completed_deliverables = contract.get_completed_deliverables(&contract.phase);
let task_infos: Vec<TaskInfo> = tasks
.iter()
.map(|t| TaskInfo {
- id: t.id,
name: t.name.clone(),
status: t.status.clone(),
})
@@ -533,29 +519,29 @@ async fn compute_action_directive(
let has_repository = !repos.is_empty();
- // Check if any task has a PR URL set
- let pr_url = tasks.iter().find_map(|t| t.pr_url.as_deref());
-
- // Check deliverables
- let check = check_deliverables_met(
+ // Check deliverables (unused, but kept for future reference)
+ let _check = check_deliverables_met(
&contract.phase,
&contract.contract_type,
- &file_infos,
+ &completed_deliverables,
&task_infos,
has_repository,
- pr_url,
);
- // Only generate directive if deliverables are met and we're in execute phase
- if check.deliverables_met && contract.phase == "execute" {
- // All tasks done, need to create PR
- if pr_url.is_none() || pr_url.unwrap_or("").is_empty() {
- let done_count = task_infos.iter().filter(|t| t.status == "done").count();
+ // Generate directive based on deliverable status
+ if contract.phase == "execute" {
+ // Check if all tasks are done but PR deliverable is not marked complete
+ let all_tasks_done = !task_infos.is_empty()
+ && task_infos.iter().all(|t| t.status == "done");
+ let pr_deliverable_complete = completed_deliverables.contains(&"pull-request".to_string());
+
+ if all_tasks_done && !pr_deliverable_complete {
+ let done_count = task_infos.len();
return Some(format!(
"[ACTION REQUIRED] All {} task(s) completed successfully.\n\
- You MUST now create a PR:\n\
- 1. Ensure all changes are merged to your makima branch\n\
- 2. Create PR: `makima supervisor pr \"makima/...\" --title \"...\" --base main`",
+ You MUST now create a PR and mark the 'pull-request' deliverable as complete:\n\
+ 1. Ensure all changes are merged to your branch\n\
+ 2. Create PR and then call mark_deliverable_complete with deliverable_id='pull-request'",
done_count
));
}
diff --git a/makima/src/server/handlers/templates.rs b/makima/src/server/handlers/templates.rs
index 6d95e86..c73007e 100644
--- a/makima/src/server/handlers/templates.rs
+++ b/makima/src/server/handlers/templates.rs
@@ -1,112 +1,12 @@
-//! Templates API handler.
+//! Contract types API handler.
-use axum::{extract::Query, http::StatusCode, response::IntoResponse, Json};
-use serde::{Deserialize, Serialize};
+use axum::{http::StatusCode, response::IntoResponse, Json};
+use serde::Serialize;
use utoipa::ToSchema;
use crate::llm::templates;
use crate::llm::templates::ContractTypeTemplate;
-/// Query parameters for listing templates
-#[derive(Debug, Deserialize, ToSchema)]
-pub struct ListTemplatesQuery {
- /// Filter by contract phase (research, specify, plan, execute, review)
- pub phase: Option<String>,
-}
-
-/// Template summary for API response
-#[derive(Debug, Serialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct TemplateSummary {
- /// Template identifier
- pub id: String,
- /// Display name
- pub name: String,
- /// Contract phase this template is designed for
- pub phase: String,
- /// Brief description
- pub description: String,
- /// Number of body elements in the template
- pub element_count: usize,
-}
-
-/// Response for listing templates
-#[derive(Debug, Serialize, ToSchema)]
-pub struct ListTemplatesResponse {
- pub templates: Vec<TemplateSummary>,
-}
-
-/// List available file templates
-#[utoipa::path(
- get,
- path = "/api/v1/templates",
- params(
- ("phase" = Option<String>, Query, description = "Filter by contract phase")
- ),
- responses(
- (status = 200, description = "Templates retrieved successfully", body = ListTemplatesResponse)
- ),
- tag = "templates"
-)]
-pub async fn list_templates(
- Query(query): Query<ListTemplatesQuery>,
-) -> impl IntoResponse {
- let template_list = match query.phase.as_deref() {
- Some(phase) => templates::templates_for_phase(phase),
- None => templates::all_templates(),
- };
-
- let summaries: Vec<TemplateSummary> = template_list
- .iter()
- .map(|t| TemplateSummary {
- id: t.id.clone(),
- name: t.name.clone(),
- phase: t.phase.clone(),
- description: t.description.clone(),
- element_count: t.suggested_body.len(),
- })
- .collect();
-
- (
- StatusCode::OK,
- Json(ListTemplatesResponse {
- templates: summaries,
- }),
- )
- .into_response()
-}
-
-/// Get a specific template by ID
-#[utoipa::path(
- get,
- path = "/api/v1/templates/{id}",
- params(
- ("id" = String, Path, description = "Template ID")
- ),
- responses(
- (status = 200, description = "Template retrieved successfully", body = templates::FileTemplate),
- (status = 404, description = "Template not found")
- ),
- tag = "templates"
-)]
-pub async fn get_template(
- axum::extract::Path(id): axum::extract::Path<String>,
-) -> impl IntoResponse {
- let all = templates::all_templates();
- let template = all.into_iter().find(|t| t.id == id);
-
- match template {
- Some(t) => (StatusCode::OK, Json(serde_json::json!(t))).into_response(),
- None => (
- StatusCode::NOT_FOUND,
- Json(serde_json::json!({
- "error": format!("Template '{}' not found", id)
- })),
- )
- .into_response(),
- }
-}
-
// =============================================================================
// Contract Type Templates (Workflow Definitions)
// =============================================================================
diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs
index 75f64c6..bf302a5 100644
--- a/makima/src/server/mod.rs
+++ b/makima/src/server/mod.rs
@@ -159,6 +159,7 @@ pub fn make_router(state: SharedState) -> Router {
.delete(contracts::delete_contract),
)
.route("/contracts/{id}/phase", post(contracts::change_phase))
+ .route("/contracts/{id}/deliverables/complete", post(contracts::mark_deliverable_complete))
.route("/contracts/{id}/events", get(contracts::get_events))
.route("/contracts/{id}/chat", post(contract_chat::contract_chat_handler))
.route(
@@ -205,9 +206,6 @@ pub fn make_router(state: SharedState) -> Router {
)
// Timeline endpoint (unified history for user)
.route("/timeline", get(history::get_timeline))
- // Template endpoints
- .route("/templates", get(templates::list_templates))
- .route("/templates/{id}", get(templates::get_template))
// Contract type templates (workflow definitions)
.route("/contract-types", get(templates::list_contract_types))
// Settings endpoints