feat: Phase 6 - audit and completion tools
Add three high-priority tools from coherence-mcp: - blue_audit: Project health check with issues and recommendations - Checks for stalled RFCs (in-progress without worktrees) - Finds implemented RFCs without ADRs - Detects overdue reminders and expired staging locks - blue_rfc_complete: Mark RFC as implemented - Requires 70% task completion minimum - Auto-advances from accepted to in-progress if needed - Identifies ADR graduation candidates - Returns remaining tasks for follow-up - blue_worktree_cleanup: Post-merge cleanup - Verifies PR is merged - Removes git worktree - Deletes local branch - Returns commands for syncing with develop Total: 35 MCP tools, 28 tests passing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8977b30e63
commit
ddaa1cfca8
6 changed files with 605 additions and 1 deletions
174
crates/blue-mcp/src/handlers/audit.rs
Normal file
174
crates/blue-mcp/src/handlers/audit.rs
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
//! Audit tool handler
|
||||
//!
|
||||
//! Checks project health and finds issues.
|
||||
|
||||
use blue_core::{DocType, ProjectState};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::error::ServerError;
|
||||
|
||||
/// Issue found during audit
|
||||
#[derive(Debug)]
|
||||
struct AuditIssue {
|
||||
category: &'static str,
|
||||
title: String,
|
||||
issue: String,
|
||||
severity: &'static str,
|
||||
}
|
||||
|
||||
/// Handle blue_audit
|
||||
pub fn handle_audit(state: &ProjectState) -> Result<Value, ServerError> {
|
||||
let mut issues: Vec<AuditIssue> = Vec::new();
|
||||
let mut recommendations: Vec<String> = Vec::new();
|
||||
|
||||
// Check 1: In-progress RFCs without worktrees (stalled)
|
||||
if let Ok(docs) = state
|
||||
.store
|
||||
.list_documents_by_status(DocType::Rfc, "in-progress")
|
||||
{
|
||||
let worktrees = state.store.list_worktrees().unwrap_or_default();
|
||||
for doc in docs {
|
||||
let has_worktree = worktrees.iter().any(|wt| wt.document_id == doc.id.unwrap_or(0));
|
||||
if !has_worktree {
|
||||
issues.push(AuditIssue {
|
||||
category: "rfc",
|
||||
title: doc.title.clone(),
|
||||
issue: "In-progress but no active worktree (possibly stalled)".into(),
|
||||
severity: "warning",
|
||||
});
|
||||
recommendations.push(format!(
|
||||
"Check on '{}' - marked in-progress but no worktree found",
|
||||
doc.title
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check 2: Implemented RFCs without ADRs
|
||||
if let Ok(implemented) = state
|
||||
.store
|
||||
.list_documents_by_status(DocType::Rfc, "implemented")
|
||||
{
|
||||
if let Ok(adrs) = state.store.list_documents(DocType::Adr) {
|
||||
for rfc in implemented {
|
||||
let has_adr = adrs
|
||||
.iter()
|
||||
.any(|adr| adr.title == rfc.title || adr.title.contains(&rfc.title));
|
||||
if !has_adr {
|
||||
issues.push(AuditIssue {
|
||||
category: "rfc",
|
||||
title: rfc.title.clone(),
|
||||
issue: "Implemented but no ADR created".into(),
|
||||
severity: "info",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check 3: Draft RFCs (potential backlog)
|
||||
if let Ok(drafts) = state.store.list_documents_by_status(DocType::Rfc, "draft") {
|
||||
let draft_count = drafts.len();
|
||||
if draft_count > 5 {
|
||||
recommendations.push(format!(
|
||||
"{} draft RFCs - consider reviewing and accepting or archiving",
|
||||
draft_count
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Check 4: Stale reminders (overdue by more than 7 days)
|
||||
if let Ok(reminders) = state.store.list_reminders(Some(blue_core::ReminderStatus::Pending), false) {
|
||||
let today = chrono::Utc::now().format("%Y-%m-%d").to_string();
|
||||
for reminder in reminders {
|
||||
if let Some(due) = &reminder.due_date {
|
||||
if due < &today {
|
||||
issues.push(AuditIssue {
|
||||
category: "reminder",
|
||||
title: reminder.title.clone(),
|
||||
issue: format!("Overdue since {}", due),
|
||||
severity: "warning",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check 5: Expired staging locks
|
||||
if let Ok(locks) = state.store.list_staging_locks() {
|
||||
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||
for lock in locks {
|
||||
if lock.expires_at < now {
|
||||
issues.push(AuditIssue {
|
||||
category: "staging",
|
||||
title: lock.resource.clone(),
|
||||
issue: format!("Lock expired at {} (held by '{}')", lock.expires_at, lock.locked_by),
|
||||
severity: "warning",
|
||||
});
|
||||
recommendations.push(format!(
|
||||
"Run blue_staging_cleanup to clear expired lock on '{}'",
|
||||
lock.resource
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate summary
|
||||
let error_count = issues.iter().filter(|i| i.severity == "error").count();
|
||||
let warning_count = issues.iter().filter(|i| i.severity == "warning").count();
|
||||
let info_count = issues.iter().filter(|i| i.severity == "info").count();
|
||||
|
||||
let hint = if error_count > 0 {
|
||||
format!(
|
||||
"{} errors, {} warnings found - attention needed",
|
||||
error_count, warning_count
|
||||
)
|
||||
} else if warning_count > 0 {
|
||||
format!("{} warnings found - review recommended", warning_count)
|
||||
} else if info_count > 0 {
|
||||
format!("{} items noted - project is healthy", info_count)
|
||||
} else {
|
||||
"No issues found - project is healthy".into()
|
||||
};
|
||||
|
||||
// Format issues for response
|
||||
let issues_json: Vec<_> = issues
|
||||
.iter()
|
||||
.map(|i| {
|
||||
json!({
|
||||
"category": i.category,
|
||||
"title": i.title,
|
||||
"issue": i.issue,
|
||||
"severity": i.severity,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(json!({
|
||||
"status": "success",
|
||||
"message": blue_core::voice::info(
|
||||
&format!("{} issues found", issues.len()),
|
||||
Some(&hint)
|
||||
),
|
||||
"issues": issues_json,
|
||||
"recommendations": recommendations,
|
||||
"summary": {
|
||||
"errors": error_count,
|
||||
"warnings": warning_count,
|
||||
"info": info_count,
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_audit_empty_project() {
|
||||
let state = ProjectState::for_test();
|
||||
let result = handle_audit(&state).unwrap();
|
||||
assert_eq!(result["status"], "success");
|
||||
assert_eq!(result["issues"].as_array().unwrap().len(), 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,12 @@
|
|||
//! Each module handles a specific document type or workflow.
|
||||
|
||||
pub mod adr;
|
||||
pub mod audit;
|
||||
pub mod decision;
|
||||
pub mod pr;
|
||||
pub mod release;
|
||||
pub mod reminder;
|
||||
pub mod rfc;
|
||||
pub mod session;
|
||||
pub mod spike;
|
||||
pub mod staging;
|
||||
|
|
|
|||
247
crates/blue-mcp/src/handlers/rfc.rs
Normal file
247
crates/blue-mcp/src/handlers/rfc.rs
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
//! RFC tool handlers
|
||||
//!
|
||||
//! Handles RFC lifecycle operations like marking complete.
|
||||
|
||||
use blue_core::{DocType, ProjectState};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::error::ServerError;
|
||||
|
||||
/// Handle blue_rfc_complete
|
||||
///
|
||||
/// Marks an RFC as implemented based on plan progress.
|
||||
/// - 100%: Plan complete, ready for PR
|
||||
/// - 70-99%: Core complete, follow-up tasks identified
|
||||
/// - <70%: Not ready - complete more tasks first
|
||||
pub fn handle_complete(state: &ProjectState, args: &Value) -> Result<Value, ServerError> {
|
||||
let title = args
|
||||
.get("title")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or(ServerError::InvalidParams)?;
|
||||
|
||||
// Find the RFC
|
||||
let doc = state
|
||||
.store
|
||||
.find_document(DocType::Rfc, title)
|
||||
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
||||
|
||||
let doc_id = doc.id.ok_or(ServerError::InvalidParams)?;
|
||||
|
||||
// Check current status
|
||||
match doc.status.as_str() {
|
||||
"draft" => {
|
||||
return Ok(json!({
|
||||
"status": "error",
|
||||
"message": blue_core::voice::error(
|
||||
"Can't complete a draft RFC",
|
||||
"Accept it first with blue_rfc_update_status"
|
||||
)
|
||||
}));
|
||||
}
|
||||
"implemented" => {
|
||||
return Ok(json!({
|
||||
"status": "success",
|
||||
"title": title,
|
||||
"already_implemented": true,
|
||||
"message": blue_core::voice::info(
|
||||
&format!("'{}' is already implemented", title),
|
||||
None::<&str>
|
||||
)
|
||||
}));
|
||||
}
|
||||
"superseded" => {
|
||||
return Ok(json!({
|
||||
"status": "error",
|
||||
"message": blue_core::voice::error(
|
||||
"Can't complete a superseded RFC",
|
||||
"This RFC was replaced by another"
|
||||
)
|
||||
}));
|
||||
}
|
||||
_ => {} // accepted or in-progress - continue
|
||||
}
|
||||
|
||||
// Check plan progress
|
||||
let progress = state
|
||||
.store
|
||||
.get_task_progress(doc_id)
|
||||
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
||||
|
||||
// No tasks = assume complete
|
||||
let (completed, total, percentage) = if progress.total == 0 {
|
||||
(1, 1, 100)
|
||||
} else {
|
||||
(progress.completed, progress.total, progress.percentage)
|
||||
};
|
||||
|
||||
// Check progress thresholds
|
||||
if percentage < 70 {
|
||||
return Ok(json!({
|
||||
"status": "error",
|
||||
"message": blue_core::voice::error(
|
||||
&format!("Only {}/{} tasks done ({}%)", completed, total, percentage),
|
||||
"Need at least 70% to mark as implemented"
|
||||
),
|
||||
"progress": {
|
||||
"completed": completed,
|
||||
"total": total,
|
||||
"percentage": percentage
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Auto-advance from accepted to in-progress if needed
|
||||
let status_auto_advanced = doc.status == "accepted";
|
||||
if status_auto_advanced {
|
||||
state
|
||||
.store
|
||||
.update_document_status(DocType::Rfc, title, "in-progress")
|
||||
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
||||
}
|
||||
|
||||
// Update to implemented
|
||||
state
|
||||
.store
|
||||
.update_document_status(DocType::Rfc, title, "implemented")
|
||||
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
||||
|
||||
// Determine follow-up needs
|
||||
let followup_needed = percentage < 100;
|
||||
let remaining_count = total - completed;
|
||||
|
||||
// Get remaining tasks if any
|
||||
let remaining_tasks: Vec<String> = if followup_needed {
|
||||
state
|
||||
.store
|
||||
.get_tasks(doc_id)
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.filter(|t| !t.completed)
|
||||
.map(|t| t.description.clone())
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
// Check for ADR potential
|
||||
let adr_candidate = check_adr_potential(state, title);
|
||||
|
||||
let hint = if followup_needed {
|
||||
format!(
|
||||
"Core work done ({}%). {} tasks remain for follow-up.",
|
||||
percentage, remaining_count
|
||||
)
|
||||
} else {
|
||||
"All tasks complete. Ready for PR.".to_string()
|
||||
};
|
||||
|
||||
let adr_hint = if adr_candidate {
|
||||
Some(format!(
|
||||
"This RFC may warrant an ADR. Use blue_adr_create with rfc='{}' to graduate.",
|
||||
title
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(json!({
|
||||
"status": "success",
|
||||
"title": title,
|
||||
"new_status": "implemented",
|
||||
"message": blue_core::voice::success(
|
||||
&format!("Marked '{}' as implemented", title),
|
||||
Some(&hint)
|
||||
),
|
||||
"status_auto_advanced": status_auto_advanced,
|
||||
"followup_needed": followup_needed,
|
||||
"remaining_tasks": remaining_tasks,
|
||||
"progress": {
|
||||
"completed": completed,
|
||||
"total": total,
|
||||
"percentage": percentage
|
||||
},
|
||||
"adr_candidate": adr_candidate,
|
||||
"adr_hint": adr_hint,
|
||||
"next_steps": [
|
||||
"Create PR: blue_pr_create",
|
||||
"After merge: blue_worktree_cleanup"
|
||||
]
|
||||
}))
|
||||
}
|
||||
|
||||
/// Check if an RFC is a good ADR candidate based on architectural indicators
|
||||
fn check_adr_potential(state: &ProjectState, title: &str) -> bool {
|
||||
// Look for architectural keywords in the RFC title/metadata
|
||||
let indicators = [
|
||||
"architecture",
|
||||
"pattern",
|
||||
"framework",
|
||||
"infrastructure",
|
||||
"system",
|
||||
"design",
|
||||
"structure",
|
||||
];
|
||||
|
||||
let title_lower = title.to_lowercase();
|
||||
let score = indicators
|
||||
.iter()
|
||||
.filter(|&ind| title_lower.contains(ind))
|
||||
.count();
|
||||
|
||||
// Also check if there are linked ADRs already
|
||||
if let Ok(adrs) = state.store.list_documents(DocType::Adr) {
|
||||
let has_adr = adrs.iter().any(|adr| adr.title.contains(title));
|
||||
if has_adr {
|
||||
return false; // Already has an ADR
|
||||
}
|
||||
}
|
||||
|
||||
score >= 1
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use blue_core::Document;
|
||||
|
||||
#[test]
|
||||
fn test_complete_requires_title() {
|
||||
let state = ProjectState::for_test();
|
||||
let args = json!({});
|
||||
|
||||
let result = handle_complete(&state, &args);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complete_draft_fails() {
|
||||
let state = ProjectState::for_test();
|
||||
|
||||
// Create a draft RFC
|
||||
let mut doc = Document::new(DocType::Rfc, "test-rfc", "draft");
|
||||
doc.number = Some(1);
|
||||
state.store.add_document(&doc).unwrap();
|
||||
|
||||
let args = json!({ "title": "test-rfc" });
|
||||
let result = handle_complete(&state, &args).unwrap();
|
||||
|
||||
assert_eq!(result["status"], "error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complete_accepted_rfc() {
|
||||
let state = ProjectState::for_test();
|
||||
|
||||
// Create an accepted RFC
|
||||
let mut doc = Document::new(DocType::Rfc, "test-rfc", "accepted");
|
||||
doc.number = Some(1);
|
||||
state.store.add_document(&doc).unwrap();
|
||||
|
||||
let args = json!({ "title": "test-rfc" });
|
||||
let result = handle_complete(&state, &args).unwrap();
|
||||
|
||||
assert_eq!(result["status"], "success");
|
||||
assert_eq!(result["new_status"], "implemented");
|
||||
assert_eq!(result["status_auto_advanced"], true);
|
||||
}
|
||||
}
|
||||
|
|
@ -136,6 +136,104 @@ pub fn handle_list(state: &ProjectState) -> Result<Value, ServerError> {
|
|||
}))
|
||||
}
|
||||
|
||||
/// Handle blue_worktree_cleanup
|
||||
///
|
||||
/// Full cleanup after PR merge:
|
||||
/// 1. Verify PR is merged
|
||||
/// 2. Remove worktree
|
||||
/// 3. Delete local branch
|
||||
/// 4. Return commands for switching to develop
|
||||
pub fn handle_cleanup(state: &ProjectState, args: &Value) -> Result<Value, ServerError> {
|
||||
let title = args
|
||||
.get("title")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or(ServerError::InvalidParams)?;
|
||||
|
||||
let branch_name = format!("rfc/{}", title);
|
||||
|
||||
// Find the RFC to get worktree info
|
||||
let doc = state
|
||||
.store
|
||||
.find_document(DocType::Rfc, title)
|
||||
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
||||
|
||||
let doc_id = doc.id.ok_or(ServerError::InvalidParams)?;
|
||||
|
||||
// Get worktree info
|
||||
let worktree = state.store.get_worktree(doc_id).ok().flatten();
|
||||
|
||||
// Try to open the repository
|
||||
let repo_path = state.home.repos_path.join(&state.project);
|
||||
let repo = match git2::Repository::open(&repo_path) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Ok(json!({
|
||||
"status": "error",
|
||||
"message": blue_core::voice::error(
|
||||
&format!("Couldn't open repository: {}", e),
|
||||
"Make sure you're in a git repository"
|
||||
)
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Check if branch is merged
|
||||
let is_merged = blue_core::repo::is_branch_merged(&repo, &branch_name, "develop")
|
||||
.or_else(|_| blue_core::repo::is_branch_merged(&repo, &branch_name, "main"))
|
||||
.unwrap_or(false);
|
||||
|
||||
if !is_merged {
|
||||
return Ok(json!({
|
||||
"status": "error",
|
||||
"message": blue_core::voice::error(
|
||||
"PR doesn't appear to be merged yet",
|
||||
"Complete the merge first with blue_pr_merge"
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
// Remove worktree from git
|
||||
let worktree_removed = if worktree.is_some() {
|
||||
blue_core::repo::remove_worktree(&repo, &branch_name).is_ok()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// Delete local branch
|
||||
let branch_deleted = if let Ok(mut branch) = repo.find_branch(&branch_name, git2::BranchType::Local) {
|
||||
branch.delete().is_ok()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// Remove from store
|
||||
if worktree.is_some() {
|
||||
let _ = state.store.remove_worktree(doc_id);
|
||||
}
|
||||
|
||||
let hint = format!(
|
||||
"Worktree {}removed, branch {}deleted. Run the commands to complete cleanup.",
|
||||
if worktree_removed { "" } else { "not " },
|
||||
if branch_deleted { "" } else { "not " }
|
||||
);
|
||||
|
||||
Ok(json!({
|
||||
"status": "success",
|
||||
"title": title,
|
||||
"worktree_removed": worktree_removed,
|
||||
"branch_deleted": branch_deleted,
|
||||
"message": blue_core::voice::success(
|
||||
&format!("Cleaned up after '{}'", title),
|
||||
Some(&hint)
|
||||
),
|
||||
"commands": [
|
||||
"git checkout develop",
|
||||
"git pull"
|
||||
],
|
||||
"next_action": "Execute the commands to sync with develop"
|
||||
}))
|
||||
}
|
||||
|
||||
/// Handle blue_worktree_remove
|
||||
pub fn handle_remove(state: &ProjectState, args: &Value) -> Result<Value, ServerError> {
|
||||
let title = args
|
||||
|
|
|
|||
|
|
@ -831,6 +831,55 @@ impl BlueServer {
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "blue_audit",
|
||||
"description": "Check project health and find issues. Returns stalled work, missing ADRs, and recommendations.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"type": "string",
|
||||
"description": "Current working directory"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "blue_rfc_complete",
|
||||
"description": "Mark RFC as implemented based on plan progress. Requires at least 70% completion.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"type": "string",
|
||||
"description": "Current working directory"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "RFC title"
|
||||
}
|
||||
},
|
||||
"required": ["title"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "blue_worktree_cleanup",
|
||||
"description": "Clean up after PR merge. Removes worktree, deletes local branch, and provides commands to sync.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"type": "string",
|
||||
"description": "Current working directory"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "RFC title"
|
||||
}
|
||||
},
|
||||
"required": ["title"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}))
|
||||
|
|
@ -887,6 +936,10 @@ impl BlueServer {
|
|||
"blue_staging_unlock" => self.handle_staging_unlock(&call.arguments),
|
||||
"blue_staging_status" => self.handle_staging_status(&call.arguments),
|
||||
"blue_staging_cleanup" => self.handle_staging_cleanup(&call.arguments),
|
||||
// Phase 6: Audit and completion handlers
|
||||
"blue_audit" => self.handle_audit(&call.arguments),
|
||||
"blue_rfc_complete" => self.handle_rfc_complete(&call.arguments),
|
||||
"blue_worktree_cleanup" => self.handle_worktree_cleanup(&call.arguments),
|
||||
_ => Err(ServerError::ToolNotFound(call.name)),
|
||||
}?;
|
||||
|
||||
|
|
@ -1422,6 +1475,25 @@ impl BlueServer {
|
|||
let state = self.ensure_state()?;
|
||||
crate::handlers::staging::handle_cleanup(state, args)
|
||||
}
|
||||
|
||||
// Phase 6: Audit and completion handlers
|
||||
|
||||
fn handle_audit(&mut self, _args: &Option<Value>) -> Result<Value, ServerError> {
|
||||
let state = self.ensure_state()?;
|
||||
crate::handlers::audit::handle_audit(state)
|
||||
}
|
||||
|
||||
fn handle_rfc_complete(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
||||
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
||||
let state = self.ensure_state()?;
|
||||
crate::handlers::rfc::handle_complete(state, args)
|
||||
}
|
||||
|
||||
fn handle_worktree_cleanup(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
||||
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
||||
let state = self.ensure_state()?;
|
||||
crate::handlers::worktree::handle_cleanup(state, args)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BlueServer {
|
||||
|
|
|
|||
|
|
@ -241,11 +241,22 @@ blue/
|
|||
- [x] Blue's voice in all error messages
|
||||
- [x] 24 tests passing
|
||||
|
||||
### Phase 6: Pending (Future)
|
||||
### Phase 6: Audit and Completion - COMPLETE
|
||||
|
||||
- [x] handlers/audit.rs - Project health check with issues and recommendations
|
||||
- [x] handlers/rfc.rs - RFC completion with progress validation
|
||||
- [x] handlers/worktree.rs - Added cleanup handler for post-merge workflow
|
||||
- [x] 3 new MCP tools: blue_audit, blue_rfc_complete, blue_worktree_cleanup
|
||||
- [x] Total: 35 MCP tools
|
||||
- [x] Blue's voice in all error messages
|
||||
- [x] 28 tests passing
|
||||
|
||||
### Phase 7: Pending (Future)
|
||||
|
||||
Remaining tools to port (if needed):
|
||||
- Code search/indexing (requires tree-sitter)
|
||||
- IaC detection and staging deployment tracking
|
||||
- PRD tools (5): create, get, approve, complete, list
|
||||
|
||||
## Test Plan
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue