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.
|
//! Each module handles a specific document type or workflow.
|
||||||
|
|
||||||
pub mod adr;
|
pub mod adr;
|
||||||
|
pub mod audit;
|
||||||
pub mod decision;
|
pub mod decision;
|
||||||
pub mod pr;
|
pub mod pr;
|
||||||
pub mod release;
|
pub mod release;
|
||||||
pub mod reminder;
|
pub mod reminder;
|
||||||
|
pub mod rfc;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod spike;
|
pub mod spike;
|
||||||
pub mod staging;
|
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
|
/// Handle blue_worktree_remove
|
||||||
pub fn handle_remove(state: &ProjectState, args: &Value) -> Result<Value, ServerError> {
|
pub fn handle_remove(state: &ProjectState, args: &Value) -> Result<Value, ServerError> {
|
||||||
let title = args
|
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_unlock" => self.handle_staging_unlock(&call.arguments),
|
||||||
"blue_staging_status" => self.handle_staging_status(&call.arguments),
|
"blue_staging_status" => self.handle_staging_status(&call.arguments),
|
||||||
"blue_staging_cleanup" => self.handle_staging_cleanup(&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)),
|
_ => Err(ServerError::ToolNotFound(call.name)),
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
|
|
@ -1422,6 +1475,25 @@ impl BlueServer {
|
||||||
let state = self.ensure_state()?;
|
let state = self.ensure_state()?;
|
||||||
crate::handlers::staging::handle_cleanup(state, args)
|
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 {
|
impl Default for BlueServer {
|
||||||
|
|
|
||||||
|
|
@ -241,11 +241,22 @@ blue/
|
||||||
- [x] Blue's voice in all error messages
|
- [x] Blue's voice in all error messages
|
||||||
- [x] 24 tests passing
|
- [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):
|
Remaining tools to port (if needed):
|
||||||
- Code search/indexing (requires tree-sitter)
|
- Code search/indexing (requires tree-sitter)
|
||||||
- IaC detection and staging deployment tracking
|
- IaC detection and staging deployment tracking
|
||||||
|
- PRD tools (5): create, get, approve, complete, list
|
||||||
|
|
||||||
## Test Plan
|
## Test Plan
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue