Add document lifecycle handlers for spike, ADR, decision, and worktree operations. This brings the tool count from 9 to 16. New tools: - blue_spike_create: Start time-boxed investigation - blue_spike_complete: Complete investigation with outcome - blue_adr_create: Create Architecture Decision Record - blue_decision_create: Create lightweight Decision Note - blue_worktree_create: Create isolated git worktree for RFC - blue_worktree_list: List active worktrees - blue_worktree_remove: Remove worktree after merge All handlers use Blue's voice for consistent messaging. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
98 lines
2.9 KiB
Rust
98 lines
2.9 KiB
Rust
//! Decision tool handlers
|
|
//!
|
|
//! Handles lightweight Decision Note creation.
|
|
|
|
use std::fs;
|
|
|
|
use blue_core::{Decision, DocType, Document, ProjectState};
|
|
use serde_json::{json, Value};
|
|
|
|
use crate::error::ServerError;
|
|
|
|
/// Handle blue_decision_create
|
|
pub fn handle_create(state: &ProjectState, args: &Value) -> Result<Value, ServerError> {
|
|
let title = args
|
|
.get("title")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(ServerError::InvalidParams)?;
|
|
|
|
let decision_text = args
|
|
.get("decision")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(ServerError::InvalidParams)?;
|
|
|
|
let rationale = args.get("rationale").and_then(|v| v.as_str());
|
|
let alternatives: Vec<String> = args
|
|
.get("alternatives")
|
|
.and_then(|v| v.as_array())
|
|
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
|
|
.unwrap_or_default();
|
|
|
|
// Create decision
|
|
let mut decision = Decision::new(title, decision_text);
|
|
if let Some(rat) = rationale {
|
|
decision.rationale = Some(rat.to_string());
|
|
}
|
|
decision.alternatives = alternatives;
|
|
|
|
// Generate markdown
|
|
let markdown = decision.to_markdown();
|
|
|
|
// Compute file path (date-based)
|
|
let today = chrono::Utc::now().format("%Y-%m-%d").to_string();
|
|
let file_name = format!("{}-{}.md", today, to_kebab_case(title));
|
|
let file_path = format!("decisions/{}", file_name);
|
|
|
|
// Write the file
|
|
let docs_path = state.home.docs_path(&state.project);
|
|
let decision_path = docs_path.join(&file_path);
|
|
|
|
// Check if already exists
|
|
if decision_path.exists() {
|
|
return Ok(json!({
|
|
"status": "error",
|
|
"message": blue_core::voice::error(
|
|
&format!("Decision '{}' already exists for today", title),
|
|
"Use a different title or update the existing one"
|
|
)
|
|
}));
|
|
}
|
|
|
|
if let Some(parent) = decision_path.parent() {
|
|
fs::create_dir_all(parent).map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
|
}
|
|
fs::write(&decision_path, &markdown).map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
|
|
|
// Add to store
|
|
let mut doc = Document::new(DocType::Decision, title, "recorded");
|
|
doc.file_path = Some(file_path.clone());
|
|
|
|
let id = state
|
|
.store
|
|
.add_document(&doc)
|
|
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
|
|
|
Ok(json!({
|
|
"status": "success",
|
|
"id": id,
|
|
"title": title,
|
|
"file": decision_path.display().to_string(),
|
|
"markdown": markdown,
|
|
"message": blue_core::voice::success(
|
|
&format!("Recorded decision: '{}'", title),
|
|
None
|
|
)
|
|
}))
|
|
}
|
|
|
|
/// Convert a string to kebab-case
|
|
fn to_kebab_case(s: &str) -> String {
|
|
s.to_lowercase()
|
|
.chars()
|
|
.map(|c| if c.is_alphanumeric() { c } else { '-' })
|
|
.collect::<String>()
|
|
.split('-')
|
|
.filter(|s| !s.is_empty())
|
|
.collect::<Vec<_>>()
|
|
.join("-")
|
|
}
|