RFC 0016: Context Injection Architecture
- Add blue:// URI scheme for document addressing
- Add manifest.rs for three-tier context configuration
- Implement MCP resources/list and resources/read handlers
- Add `blue context` CLI command for visibility
- Add context_injections audit table (schema v5)
RFC 0017: Dynamic Context Activation (Phase 1)
- Add relevance_edges table for explicit links (schema v6)
- Implement composite session ID: {repo}-{realm}-{random12}
- Add content-hash based staleness detection
- Add tiered refresh policies (SessionStart/OnChange/OnRequest/Never)
- Add rate limiting with 30s cooldown
- Add blue_context_status MCP tool
Drafted from 12-expert alignment dialogue achieving 95% convergence.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
3371 lines
144 KiB
Rust
3371 lines
144 KiB
Rust
//! MCP Server implementation
|
|
//!
|
|
//! Handles JSON-RPC requests and routes to appropriate tool handlers.
|
|
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
|
|
use serde::Deserialize;
|
|
use serde_json::{json, Value};
|
|
use tracing::{debug, info};
|
|
|
|
use blue_core::{detect_blue, DocType, Document, ProjectState, Rfc, RfcStatus, validate_rfc_transition};
|
|
|
|
use crate::error::ServerError;
|
|
|
|
/// Blue MCP Server state
|
|
pub struct BlueServer {
|
|
/// Current working directory
|
|
cwd: Option<PathBuf>,
|
|
/// Cached project state
|
|
state: Option<ProjectState>,
|
|
}
|
|
|
|
impl BlueServer {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
cwd: None,
|
|
state: None,
|
|
}
|
|
}
|
|
|
|
/// Try to load project state for the current directory
|
|
fn ensure_state(&mut self) -> Result<&ProjectState, ServerError> {
|
|
if self.state.is_none() {
|
|
let cwd = self.cwd.as_ref().ok_or(ServerError::BlueNotDetected)?;
|
|
let home = detect_blue(cwd).map_err(|_| ServerError::BlueNotDetected)?;
|
|
|
|
// Try to get project name from the current path
|
|
let project = home.project_name.clone().unwrap_or_else(|| "default".to_string());
|
|
|
|
let state = ProjectState::load(home, &project)
|
|
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
|
|
|
self.state = Some(state);
|
|
}
|
|
|
|
self.state.as_ref().ok_or(ServerError::BlueNotDetected)
|
|
}
|
|
|
|
fn ensure_state_mut(&mut self) -> Result<&mut ProjectState, ServerError> {
|
|
if self.state.is_none() {
|
|
let cwd = self.cwd.as_ref().ok_or(ServerError::BlueNotDetected)?;
|
|
let home = detect_blue(cwd).map_err(|_| ServerError::BlueNotDetected)?;
|
|
|
|
// Try to get project name from the current path
|
|
let project = home.project_name.clone().unwrap_or_else(|| "default".to_string());
|
|
|
|
let state = ProjectState::load(home, &project)
|
|
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
|
|
|
self.state = Some(state);
|
|
}
|
|
|
|
self.state.as_mut().ok_or(ServerError::BlueNotDetected)
|
|
}
|
|
|
|
/// Handle a JSON-RPC request
|
|
pub fn handle_request(&mut self, request: &str) -> String {
|
|
let result = self.handle_request_inner(request);
|
|
match result {
|
|
Ok(response) => response,
|
|
Err(e) => {
|
|
let error_response = json!({
|
|
"jsonrpc": "2.0",
|
|
"error": {
|
|
"code": e.code(),
|
|
"message": e.to_string()
|
|
},
|
|
"id": null
|
|
});
|
|
serde_json::to_string(&error_response).unwrap_or_default()
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_request_inner(&mut self, request: &str) -> Result<String, ServerError> {
|
|
let req: JsonRpcRequest = serde_json::from_str(request)?;
|
|
|
|
debug!("Received request: {} (id: {:?})", req.method, req.id);
|
|
|
|
let result = match req.method.as_str() {
|
|
"initialize" => self.handle_initialize(&req.params),
|
|
"tools/list" => self.handle_tools_list(),
|
|
"tools/call" => self.handle_tool_call(&req.params),
|
|
"resources/list" => self.handle_resources_list(),
|
|
"resources/read" => self.handle_resources_read(&req.params),
|
|
_ => Err(ServerError::MethodNotFound(req.method.clone())),
|
|
};
|
|
|
|
let response = match result {
|
|
Ok(value) => json!({
|
|
"jsonrpc": "2.0",
|
|
"result": value,
|
|
"id": req.id
|
|
}),
|
|
Err(e) => json!({
|
|
"jsonrpc": "2.0",
|
|
"error": {
|
|
"code": e.code(),
|
|
"message": e.to_string()
|
|
},
|
|
"id": req.id
|
|
}),
|
|
};
|
|
|
|
Ok(serde_json::to_string(&response)?)
|
|
}
|
|
|
|
/// Handle initialize request
|
|
fn handle_initialize(&mut self, _params: &Option<Value>) -> Result<Value, ServerError> {
|
|
info!("MCP initialize");
|
|
Ok(json!({
|
|
"protocolVersion": "2024-11-05",
|
|
"capabilities": {
|
|
"tools": {},
|
|
"resources": {
|
|
"listChanged": true
|
|
}
|
|
},
|
|
"serverInfo": {
|
|
"name": "blue",
|
|
"version": env!("CARGO_PKG_VERSION")
|
|
}
|
|
}))
|
|
}
|
|
|
|
/// Handle tools/list request
|
|
fn handle_tools_list(&self) -> Result<Value, ServerError> {
|
|
Ok(json!({
|
|
"tools": [
|
|
{
|
|
"name": "blue_status",
|
|
"description": "Get project status. Returns active work, ready items, stalled items, and recommendations.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_next",
|
|
"description": "Get recommended next actions based on project state.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_rfc_create",
|
|
"description": "Create a new RFC (design document) for a feature.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "RFC title in kebab-case"
|
|
},
|
|
"problem": {
|
|
"type": "string",
|
|
"description": "Problem statement or summary"
|
|
},
|
|
"source_spike": {
|
|
"type": "string",
|
|
"description": "Source spike title that led to this RFC"
|
|
}
|
|
},
|
|
"required": ["title"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_rfc_get",
|
|
"description": "Get an RFC by title or number.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "RFC title or number"
|
|
}
|
|
},
|
|
"required": ["title"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_rfc_update_status",
|
|
"description": "Update an RFC's status (draft -> accepted -> in-progress -> implemented).",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "RFC title"
|
|
},
|
|
"status": {
|
|
"type": "string",
|
|
"description": "New status: accepted, in-progress, implemented, or superseded",
|
|
"enum": ["accepted", "in-progress", "implemented", "superseded"]
|
|
}
|
|
},
|
|
"required": ["title", "status"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_rfc_plan",
|
|
"description": "Create or update an implementation plan with checkboxes for an RFC.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "RFC title"
|
|
},
|
|
"tasks": {
|
|
"type": "array",
|
|
"items": { "type": "string" },
|
|
"description": "List of implementation tasks"
|
|
}
|
|
},
|
|
"required": ["title", "tasks"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_rfc_task_complete",
|
|
"description": "Mark a task as complete in an RFC plan.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "RFC title"
|
|
},
|
|
"task": {
|
|
"type": "string",
|
|
"description": "Task index (1-based) or substring to match"
|
|
}
|
|
},
|
|
"required": ["title", "task"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_rfc_validate",
|
|
"description": "Check RFC status and plan completion.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "RFC title"
|
|
}
|
|
},
|
|
"required": ["title"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_search",
|
|
"description": "Search documents using full-text search.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"query": {
|
|
"type": "string",
|
|
"description": "Search query"
|
|
},
|
|
"doc_type": {
|
|
"type": "string",
|
|
"description": "Filter by document type",
|
|
"enum": ["rfc", "spike", "adr", "decision"]
|
|
},
|
|
"limit": {
|
|
"type": "number",
|
|
"description": "Maximum results to return (default: 10)"
|
|
}
|
|
},
|
|
"required": ["query"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_spike_create",
|
|
"description": "Start a time-boxed investigation.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Investigation title"
|
|
},
|
|
"question": {
|
|
"type": "string",
|
|
"description": "What we're trying to learn"
|
|
},
|
|
"time_box": {
|
|
"type": "string",
|
|
"description": "Time limit (e.g., '2 hours')"
|
|
}
|
|
},
|
|
"required": ["title", "question"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_spike_complete",
|
|
"description": "Complete an investigation with findings.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Investigation title"
|
|
},
|
|
"outcome": {
|
|
"type": "string",
|
|
"description": "Investigation outcome",
|
|
"enum": ["no-action", "decision-made", "recommends-implementation"]
|
|
},
|
|
"summary": {
|
|
"type": "string",
|
|
"description": "Summary of findings"
|
|
}
|
|
},
|
|
"required": ["title", "outcome"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_adr_create",
|
|
"description": "Create an Architecture Decision Record.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "ADR title (kebab-case)"
|
|
},
|
|
"rfc": {
|
|
"type": "string",
|
|
"description": "RFC title this ADR documents (must be implemented)"
|
|
},
|
|
"context": {
|
|
"type": "string",
|
|
"description": "Decision context"
|
|
},
|
|
"decision": {
|
|
"type": "string",
|
|
"description": "The decision that was made"
|
|
},
|
|
"consequences": {
|
|
"type": "array",
|
|
"items": { "type": "string" },
|
|
"description": "Consequences of the decision"
|
|
}
|
|
},
|
|
"required": ["title"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_adr_list",
|
|
"description": "List all ADRs with summaries.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_adr_get",
|
|
"description": "Get full ADR content with referenced_by information.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"number": {
|
|
"type": "number",
|
|
"description": "ADR number to retrieve"
|
|
}
|
|
},
|
|
"required": ["number"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_adr_relevant",
|
|
"description": "Find relevant ADRs based on context. Uses keyword matching (AI matching when LLM available).",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"context": {
|
|
"type": "string",
|
|
"description": "Context to match against (e.g., 'testing strategy', 'deleting old code')"
|
|
}
|
|
},
|
|
"required": ["context"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_adr_audit",
|
|
"description": "Scan for potential ADR violations. Only checks testable ADRs (Evidence, Single Source, No Dead Code).",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_decision_create",
|
|
"description": "Create a lightweight Decision Note.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Decision title"
|
|
},
|
|
"decision": {
|
|
"type": "string",
|
|
"description": "The decision made"
|
|
},
|
|
"rationale": {
|
|
"type": "string",
|
|
"description": "Why this decision was made"
|
|
},
|
|
"alternatives": {
|
|
"type": "array",
|
|
"items": { "type": "string" },
|
|
"description": "Alternatives that were considered"
|
|
}
|
|
},
|
|
"required": ["title", "decision"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_worktree_create",
|
|
"description": "Create an isolated git worktree for RFC implementation. Use after an RFC is accepted, before starting work. Creates a feature branch and isolated directory.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "RFC title to implement"
|
|
}
|
|
},
|
|
"required": ["title"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_worktree_list",
|
|
"description": "List active git worktrees.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_worktree_remove",
|
|
"description": "Remove a worktree after PR merge.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "RFC title"
|
|
},
|
|
"force": {
|
|
"type": "boolean",
|
|
"description": "Remove even if branch not merged"
|
|
}
|
|
},
|
|
"required": ["title"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_pr_create",
|
|
"description": "Create a PR with enforced base branch (develop, not main). Use after implementation is complete and blue_rfc_complete succeeds. If rfc is provided, title is formatted as 'RFC NNNN: Title Case Name'.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"rfc": {
|
|
"type": "string",
|
|
"description": "RFC title (e.g., '0007-consistent-branch-naming'). If provided, PR title is formatted as 'RFC 0007: Consistent Branch Naming'"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "PR title (used if rfc not provided)"
|
|
},
|
|
"base": {
|
|
"type": "string",
|
|
"description": "Base branch (default: develop)"
|
|
},
|
|
"body": {
|
|
"type": "string",
|
|
"description": "PR body (markdown)"
|
|
},
|
|
"draft": {
|
|
"type": "boolean",
|
|
"description": "Create as draft PR"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_pr_verify",
|
|
"description": "Verify test plan checkboxes in a PR.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"pr_number": {
|
|
"type": "number",
|
|
"description": "PR number (auto-detect from branch if not provided)"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_pr_check_item",
|
|
"description": "Mark a test plan item as verified in the PR.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"pr_number": {
|
|
"type": "number",
|
|
"description": "PR number"
|
|
},
|
|
"item": {
|
|
"type": "string",
|
|
"description": "Item index (1-based) or substring to match"
|
|
},
|
|
"verified_by": {
|
|
"type": "string",
|
|
"description": "How the item was verified"
|
|
}
|
|
},
|
|
"required": ["item"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_pr_check_approvals",
|
|
"description": "Check if PR has been approved by reviewers.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"pr_number": {
|
|
"type": "number",
|
|
"description": "PR number"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_pr_merge",
|
|
"description": "Squash-merge a PR after verification and approval.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"pr_number": {
|
|
"type": "number",
|
|
"description": "PR number"
|
|
},
|
|
"squash": {
|
|
"type": "boolean",
|
|
"description": "Use squash merge (default: true)"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_release_create",
|
|
"description": "Create a release with semantic versioning.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"version": {
|
|
"type": "string",
|
|
"description": "Override suggested version (e.g., '2.1.0')"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_session_ping",
|
|
"description": "Register or update session activity for an RFC.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "RFC title being worked on"
|
|
},
|
|
"action": {
|
|
"type": "string",
|
|
"description": "Session action to perform",
|
|
"enum": ["start", "heartbeat", "end"]
|
|
},
|
|
"session_type": {
|
|
"type": "string",
|
|
"description": "Type of work being done (default: implementation)",
|
|
"enum": ["implementation", "review", "testing"]
|
|
}
|
|
},
|
|
"required": ["title", "action"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_session_list",
|
|
"description": "List active sessions on RFCs.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_reminder_create",
|
|
"description": "Create a gated reminder with optional time and condition triggers.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Short reminder title"
|
|
},
|
|
"context": {
|
|
"type": "string",
|
|
"description": "Additional notes/context"
|
|
},
|
|
"gate": {
|
|
"type": "string",
|
|
"description": "Condition that must be met (optional)"
|
|
},
|
|
"due_date": {
|
|
"type": "string",
|
|
"description": "Target date YYYY-MM-DD (optional)"
|
|
},
|
|
"snooze_until": {
|
|
"type": "string",
|
|
"description": "Don't show until this date YYYY-MM-DD (optional)"
|
|
},
|
|
"link_to": {
|
|
"type": "string",
|
|
"description": "RFC/spike/decision title to link"
|
|
}
|
|
},
|
|
"required": ["title"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_reminder_list",
|
|
"description": "List reminders with optional filters.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"status": {
|
|
"type": "string",
|
|
"description": "Filter by status (default: pending)",
|
|
"enum": ["pending", "snoozed", "cleared", "all"]
|
|
},
|
|
"include_future": {
|
|
"type": "boolean",
|
|
"description": "Include snoozed items not yet due (default: false)"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_reminder_snooze",
|
|
"description": "Snooze a reminder until a specific date.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"id": {
|
|
"type": "number",
|
|
"description": "Reminder ID"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Or match by title (partial match)"
|
|
},
|
|
"until": {
|
|
"type": "string",
|
|
"description": "New snooze date (YYYY-MM-DD)"
|
|
}
|
|
},
|
|
"required": ["until"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_reminder_clear",
|
|
"description": "Clear a reminder (mark gate as resolved).",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"id": {
|
|
"type": "number",
|
|
"description": "Reminder ID"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Or match by title (partial match)"
|
|
},
|
|
"resolution": {
|
|
"type": "string",
|
|
"description": "How the gate was resolved"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_staging_lock",
|
|
"description": "Acquire exclusive access to a staging resource.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"resource": {
|
|
"type": "string",
|
|
"description": "Resource to lock (e.g., 'migration', 'staging-db')"
|
|
},
|
|
"locked_by": {
|
|
"type": "string",
|
|
"description": "Identifier for lock holder (RFC title or PR number)"
|
|
},
|
|
"agent_id": {
|
|
"type": "string",
|
|
"description": "Blue agent ID (from .env.isolated)"
|
|
},
|
|
"duration_minutes": {
|
|
"type": "number",
|
|
"description": "Lock duration in minutes (default 30)"
|
|
}
|
|
},
|
|
"required": ["resource", "locked_by"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_staging_unlock",
|
|
"description": "Release a staging lock.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"resource": {
|
|
"type": "string",
|
|
"description": "Resource to unlock"
|
|
},
|
|
"locked_by": {
|
|
"type": "string",
|
|
"description": "Identifier that acquired the lock"
|
|
}
|
|
},
|
|
"required": ["resource", "locked_by"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_staging_status",
|
|
"description": "Check staging lock status.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"resource": {
|
|
"type": "string",
|
|
"description": "Specific resource to check (omit for all locks)"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_staging_cleanup",
|
|
"description": "Clean up expired staging resources.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_staging_deployments",
|
|
"description": "List staging environment deployments. Shows deployed, destroyed, or expired environments. Use check_expired=true to mark expired deployments.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"status": {
|
|
"type": "string",
|
|
"enum": ["deployed", "destroyed", "expired"],
|
|
"description": "Filter by deployment status"
|
|
},
|
|
"check_expired": {
|
|
"type": "boolean",
|
|
"description": "Check for and mark expired deployments (default: false)"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_health_check",
|
|
"description": "Check project health and find issues. Returns stalled work, missing ADRs, overdue reminders, and recommendations.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_audit_create",
|
|
"description": "Create a new audit document (repository, security, rfc-verification, adr-adherence, or custom).",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Audit title in kebab-case"
|
|
},
|
|
"audit_type": {
|
|
"type": "string",
|
|
"description": "Type of audit",
|
|
"enum": ["repository", "security", "rfc-verification", "adr-adherence", "custom"]
|
|
},
|
|
"scope": {
|
|
"type": "string",
|
|
"description": "What is being audited"
|
|
}
|
|
},
|
|
"required": ["title"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_audit_list",
|
|
"description": "List all audit documents.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_audit_get",
|
|
"description": "Get an audit document by title.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Audit title"
|
|
}
|
|
},
|
|
"required": ["title"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_audit_complete",
|
|
"description": "Mark an audit as complete.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Audit title"
|
|
}
|
|
},
|
|
"required": ["title"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_rfc_complete",
|
|
"description": "Mark RFC as implemented based on plan progress. Use after completing tasks in the worktree. Requires at least 70% completion. Follow with blue_pr_create.",
|
|
"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"]
|
|
}
|
|
},
|
|
// Phase 7: PRD tools
|
|
{
|
|
"name": "blue_prd_create",
|
|
"description": "Create a Product Requirements Document (PRD). Use when: user-facing features, business requirements, stakeholder sign-off needed.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Feature title"
|
|
},
|
|
"problem": {
|
|
"type": "string",
|
|
"description": "What problem are users experiencing?"
|
|
},
|
|
"users": {
|
|
"type": "string",
|
|
"description": "Who are the target users?"
|
|
},
|
|
"goals": {
|
|
"type": "array",
|
|
"items": { "type": "string" },
|
|
"description": "Business goals"
|
|
},
|
|
"non_goals": {
|
|
"type": "array",
|
|
"items": { "type": "string" },
|
|
"description": "What this feature explicitly won't do"
|
|
},
|
|
"stakeholders": {
|
|
"type": "array",
|
|
"items": { "type": "string" },
|
|
"description": "Who requested this, who benefits"
|
|
}
|
|
},
|
|
"required": ["title"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_prd_get",
|
|
"description": "Get the content of a PRD.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "PRD title"
|
|
}
|
|
},
|
|
"required": ["title"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_prd_approve",
|
|
"description": "Mark PRD as approved by stakeholders. Transitions: draft -> approved.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "PRD title"
|
|
}
|
|
},
|
|
"required": ["title"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_prd_complete",
|
|
"description": "Mark PRD as implemented after verification. Checks acceptance criteria before allowing completion.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "PRD title"
|
|
}
|
|
},
|
|
"required": ["title"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_prd_list",
|
|
"description": "List PRDs by status.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"status": {
|
|
"type": "string",
|
|
"description": "Filter by status (draft, approved, implemented)"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
// Phase 7: Lint tool
|
|
{
|
|
"name": "blue_lint",
|
|
"description": "Run code quality checks. Detects project type (Rust, JS, Python, CDK) and runs appropriate linters.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"check": {
|
|
"type": "string",
|
|
"description": "Specific check to run (default: all)",
|
|
"enum": ["format", "lint", "all"]
|
|
},
|
|
"fix": {
|
|
"type": "boolean",
|
|
"description": "Auto-fix issues where possible (default: false)"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
// Phase 7: Environment isolation tools
|
|
{
|
|
"name": "blue_env_detect",
|
|
"description": "Detect external dependencies in a project. Returns S3, database, Redis, IaC configs found.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_env_mock",
|
|
"description": "Generate isolated environment configuration. Creates .env.isolated with agent ID and mock configs.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"worktree_path": {
|
|
"type": "string",
|
|
"description": "Path to worktree (defaults to cwd)"
|
|
},
|
|
"agent_id": {
|
|
"type": "string",
|
|
"description": "Custom agent ID (auto-generated if not provided)"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
// Phase 7: Onboarding guide
|
|
{
|
|
"name": "blue_guide",
|
|
"description": "Interactive onboarding guide for new Blue users.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"action": {
|
|
"type": "string",
|
|
"description": "Guide action to perform",
|
|
"enum": ["start", "resume", "next", "skip", "reset", "status"]
|
|
},
|
|
"choice": {
|
|
"type": "string",
|
|
"description": "User's choice from the previous prompt"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
// Phase 7: Staging IaC tools
|
|
{
|
|
"name": "blue_staging_create",
|
|
"description": "Prepare staging environment deployment from IaC. Detects CDK/Terraform/Pulumi and generates deploy commands.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"stack": {
|
|
"type": "string",
|
|
"description": "Specific stack to deploy (defaults to all)"
|
|
},
|
|
"ttl_hours": {
|
|
"type": "number",
|
|
"description": "TTL in hours for auto-cleanup (default: 24)"
|
|
},
|
|
"dry_run": {
|
|
"type": "boolean",
|
|
"description": "Just show command without running (default: true)"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_staging_destroy",
|
|
"description": "Destroy a staging environment. Generates destroy command for detected IaC type.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Deployment name to destroy"
|
|
},
|
|
"dry_run": {
|
|
"type": "boolean",
|
|
"description": "Just show command without executing (default: true)"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_staging_cost",
|
|
"description": "Estimate costs for staging environment. Uses Infracost for Terraform/CDK.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"duration_hours": {
|
|
"type": "number",
|
|
"description": "Duration in hours for cost calculation (default: 24)"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
// Phase 8: Dialogue tools
|
|
{
|
|
"name": "blue_dialogue_lint",
|
|
"description": "Validate dialogue documents against the blue-dialogue-pattern. Returns weighted consistency score.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"file_path": {
|
|
"type": "string",
|
|
"description": "Path to the .dialogue.md file"
|
|
}
|
|
},
|
|
"required": ["file_path"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_extract_dialogue",
|
|
"description": "Extract dialogue content from spawned agent JSONL outputs.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"task_id": {
|
|
"type": "string",
|
|
"description": "Task ID (e.g., 'a6dc70c') - resolves via symlink in /tmp/claude/.../tasks/"
|
|
},
|
|
"file_path": {
|
|
"type": "string",
|
|
"description": "Absolute path to JSONL file"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_dialogue_create",
|
|
"description": "Create a new dialogue document with SQLite metadata. Dialogues capture agent conversations and can be linked to RFCs.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Dialogue title"
|
|
},
|
|
"rfc_title": {
|
|
"type": "string",
|
|
"description": "RFC title to link this dialogue to"
|
|
},
|
|
"summary": {
|
|
"type": "string",
|
|
"description": "Brief summary of the dialogue"
|
|
},
|
|
"content": {
|
|
"type": "string",
|
|
"description": "Full dialogue content"
|
|
}
|
|
},
|
|
"required": ["title"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_dialogue_get",
|
|
"description": "Get a dialogue document by title.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Dialogue title or number"
|
|
}
|
|
},
|
|
"required": ["title"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_dialogue_list",
|
|
"description": "List all dialogue documents, optionally filtered by RFC.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"rfc_title": {
|
|
"type": "string",
|
|
"description": "Filter dialogues by RFC title"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_dialogue_save",
|
|
"description": "Extract dialogue from JSONL and save as a dialogue document with metadata.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Dialogue title"
|
|
},
|
|
"task_id": {
|
|
"type": "string",
|
|
"description": "Task ID to extract dialogue from"
|
|
},
|
|
"file_path": {
|
|
"type": "string",
|
|
"description": "Path to JSONL file (alternative to task_id)"
|
|
},
|
|
"rfc_title": {
|
|
"type": "string",
|
|
"description": "RFC title to link this dialogue to"
|
|
},
|
|
"summary": {
|
|
"type": "string",
|
|
"description": "Brief summary of the dialogue"
|
|
}
|
|
},
|
|
"required": ["title"]
|
|
}
|
|
},
|
|
// Phase 8: Playwright verification
|
|
{
|
|
"name": "blue_playwright_verify",
|
|
"description": "Generate a verification plan for browser-based testing using Playwright MCP.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"task": {
|
|
"type": "string",
|
|
"description": "Description of the verification task"
|
|
},
|
|
"base_url": {
|
|
"type": "string",
|
|
"description": "Base URL for the application (e.g., 'http://localhost:3000')"
|
|
},
|
|
"path": {
|
|
"type": "string",
|
|
"description": "Specific path to navigate to (e.g., '/login')"
|
|
},
|
|
"expected_outcomes": {
|
|
"type": "array",
|
|
"items": { "type": "string" },
|
|
"description": "Expected outcomes to verify"
|
|
},
|
|
"allow_staging": {
|
|
"type": "boolean",
|
|
"description": "Allow staging URLs (default: false, only localhost allowed)"
|
|
}
|
|
},
|
|
"required": ["task", "base_url"]
|
|
}
|
|
},
|
|
// Phase 9: Post-mortem tools
|
|
{
|
|
"name": "blue_postmortem_create",
|
|
"description": "Create a post-mortem document for incident tracking.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Post-mortem title"
|
|
},
|
|
"severity": {
|
|
"type": "string",
|
|
"description": "Severity level (P1, P2, P3, P4)"
|
|
},
|
|
"summary": {
|
|
"type": "string",
|
|
"description": "Brief incident summary"
|
|
},
|
|
"root_cause": {
|
|
"type": "string",
|
|
"description": "Root cause of the incident"
|
|
},
|
|
"duration": {
|
|
"type": "string",
|
|
"description": "Incident duration"
|
|
},
|
|
"impact": {
|
|
"type": "array",
|
|
"items": { "type": "string" },
|
|
"description": "Impact items"
|
|
}
|
|
},
|
|
"required": ["title", "severity"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_postmortem_action_to_rfc",
|
|
"description": "Convert a post-mortem action item into an RFC with bidirectional linking.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"postmortem_title": {
|
|
"type": "string",
|
|
"description": "Title of the post-mortem"
|
|
},
|
|
"action": {
|
|
"type": "string",
|
|
"description": "Action item index (1-based) or substring to match"
|
|
},
|
|
"rfc_title": {
|
|
"type": "string",
|
|
"description": "Optional RFC title (defaults to action item text)"
|
|
}
|
|
},
|
|
"required": ["postmortem_title", "action"]
|
|
}
|
|
},
|
|
// Phase 9: Runbook tools
|
|
{
|
|
"name": "blue_runbook_create",
|
|
"description": "Create a runbook document for operational procedures.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Runbook title"
|
|
},
|
|
"source_rfc": {
|
|
"type": "string",
|
|
"description": "Source RFC title to link"
|
|
},
|
|
"service_name": {
|
|
"type": "string",
|
|
"description": "Service or feature name"
|
|
},
|
|
"owner": {
|
|
"type": "string",
|
|
"description": "Owner team or person"
|
|
},
|
|
"operations": {
|
|
"type": "array",
|
|
"items": { "type": "string" },
|
|
"description": "Initial operations to document"
|
|
},
|
|
"actions": {
|
|
"type": "array",
|
|
"items": { "type": "string" },
|
|
"description": "Action tags for lookup (e.g., ['docker build', 'build image'])"
|
|
}
|
|
},
|
|
"required": ["title"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_runbook_update",
|
|
"description": "Update an existing runbook with new operations or troubleshooting.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Runbook title"
|
|
},
|
|
"add_operation": {
|
|
"type": "string",
|
|
"description": "New operation to add"
|
|
},
|
|
"add_troubleshooting": {
|
|
"type": "string",
|
|
"description": "Troubleshooting section to add"
|
|
}
|
|
},
|
|
"required": ["title"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_runbook_lookup",
|
|
"description": "Find a runbook by action query. Uses word-based matching to find the best runbook for a given action like 'docker build' or 'deploy staging'.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"action": {
|
|
"type": "string",
|
|
"description": "Action to look up (e.g., 'docker build', 'deploy staging')"
|
|
}
|
|
},
|
|
"required": ["action"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_runbook_actions",
|
|
"description": "List all registered actions across runbooks. Use this to discover what runbooks are available.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_realm_status",
|
|
"description": "Get realm overview including repos, domains, contracts, and bindings. Returns pending notifications.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory (must be in a realm repo)"
|
|
}
|
|
},
|
|
"required": ["cwd"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_realm_check",
|
|
"description": "Validate realm contracts and bindings. Returns errors and warnings including schema-without-version changes.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory (must be in a realm repo)"
|
|
},
|
|
"realm": {
|
|
"type": "string",
|
|
"description": "Specific realm to check (defaults to current repo's realm)"
|
|
}
|
|
},
|
|
"required": ["cwd"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_contract_get",
|
|
"description": "Get contract details including schema, value, version, owner, and bindings.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory (must be in a realm repo)"
|
|
},
|
|
"domain": {
|
|
"type": "string",
|
|
"description": "Domain name containing the contract"
|
|
},
|
|
"contract": {
|
|
"type": "string",
|
|
"description": "Contract name"
|
|
}
|
|
},
|
|
"required": ["cwd", "domain", "contract"]
|
|
}
|
|
},
|
|
// Phase 2: Session tools (RFC 0002)
|
|
{
|
|
"name": "blue_session_start",
|
|
"description": "Begin a work session. Tracks active realm, repo, domains, and contracts being modified or watched. Returns session ID and context.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory (must be in a realm repo)"
|
|
},
|
|
"active_rfc": {
|
|
"type": "string",
|
|
"description": "Optional RFC title being worked on"
|
|
}
|
|
},
|
|
"required": ["cwd"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_session_stop",
|
|
"description": "End the current work session. Returns summary including duration, domains touched, and contracts modified.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory (must be in a realm repo)"
|
|
}
|
|
},
|
|
"required": ["cwd"]
|
|
}
|
|
},
|
|
// Phase 3: Workflow tools (RFC 0002)
|
|
{
|
|
"name": "blue_realm_worktree_create",
|
|
"description": "Create git worktrees for coordinated multi-repo development. Auto-selects domain peers (repos sharing domains) by default.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory (must be in a realm repo)"
|
|
},
|
|
"rfc": {
|
|
"type": "string",
|
|
"description": "RFC name for branch naming (creates rfc/<name> branches)"
|
|
},
|
|
"repos": {
|
|
"type": "array",
|
|
"items": { "type": "string" },
|
|
"description": "Specific repos to create worktrees for (defaults to domain peers)"
|
|
}
|
|
},
|
|
"required": ["cwd", "rfc"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_realm_pr_status",
|
|
"description": "Get PR readiness across realm repos. Shows uncommitted changes, commits ahead, and PR status for coordinated releases.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory (must be in a realm repo)"
|
|
},
|
|
"rfc": {
|
|
"type": "string",
|
|
"description": "RFC name to check specific branches (rfc/<name>)"
|
|
}
|
|
},
|
|
"required": ["cwd"]
|
|
}
|
|
},
|
|
// Phase 4: Notifications (RFC 0002)
|
|
{
|
|
"name": "blue_notifications_list",
|
|
"description": "List notifications with state filters. States: pending (unseen), seen (acknowledged), expired (7+ days old). Auto-cleans expired notifications.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory (must be in a realm repo)"
|
|
},
|
|
"state": {
|
|
"type": "string",
|
|
"enum": ["pending", "seen", "expired", "all"],
|
|
"description": "Filter by notification state (default: all)"
|
|
}
|
|
},
|
|
"required": ["cwd"]
|
|
}
|
|
},
|
|
// RFC 0005: Local LLM Integration
|
|
{
|
|
"name": "blue_llm_start",
|
|
"description": "Start the Ollama LLM server. Manages an embedded Ollama instance or uses an external one.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"port": {
|
|
"type": "number",
|
|
"description": "Port to run on (default: 11434)"
|
|
},
|
|
"model": {
|
|
"type": "string",
|
|
"description": "Default model to use (default: qwen2.5:7b)"
|
|
},
|
|
"backend": {
|
|
"type": "string",
|
|
"enum": ["auto", "cuda", "mps", "cpu"],
|
|
"description": "Backend to use (default: auto)"
|
|
},
|
|
"use_external": {
|
|
"type": "boolean",
|
|
"description": "Use external Ollama instead of embedded (default: false)"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_llm_stop",
|
|
"description": "Stop the managed Ollama LLM server.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_llm_status",
|
|
"description": "Check LLM server status. Returns running state, version, and GPU info.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_llm_providers",
|
|
"description": "Show LLM provider fallback chain status. Returns availability of: Ollama (local) → API (Anthropic/OpenAI) → Keywords (always available).",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_model_list",
|
|
"description": "List available models in the Ollama instance.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_model_pull",
|
|
"description": "Pull a model from the Ollama registry.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Model name (e.g., 'qwen2.5:7b', 'llama3.2:3b')"
|
|
}
|
|
},
|
|
"required": ["name"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_model_remove",
|
|
"description": "Remove a model from the Ollama instance.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Model name to remove"
|
|
}
|
|
},
|
|
"required": ["name"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_model_warmup",
|
|
"description": "Warm up a model by loading it into memory.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Model name to warm up"
|
|
}
|
|
},
|
|
"required": ["name"]
|
|
}
|
|
},
|
|
// RFC 0006: Delete tools
|
|
{
|
|
"name": "blue_delete",
|
|
"description": "Delete a document (RFC, spike, decision, etc.) with safety checks. Supports dry_run, force, and permanent options. Default is soft-delete with 7-day retention.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"doc_type": {
|
|
"type": "string",
|
|
"description": "Document type",
|
|
"enum": ["rfc", "spike", "adr", "decision", "prd", "postmortem", "runbook"]
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Document title or number"
|
|
},
|
|
"dry_run": {
|
|
"type": "boolean",
|
|
"description": "Preview what would be deleted without making changes"
|
|
},
|
|
"force": {
|
|
"type": "boolean",
|
|
"description": "Skip confirmation for non-draft documents or active sessions"
|
|
},
|
|
"permanent": {
|
|
"type": "boolean",
|
|
"description": "Permanently delete (skip soft-delete retention)"
|
|
}
|
|
},
|
|
"required": ["doc_type", "title"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_restore",
|
|
"description": "Restore a soft-deleted document within the 7-day retention period.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"doc_type": {
|
|
"type": "string",
|
|
"description": "Document type",
|
|
"enum": ["rfc", "spike", "adr", "decision", "prd", "postmortem", "runbook"]
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Document title to restore"
|
|
}
|
|
},
|
|
"required": ["doc_type", "title"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_deleted_list",
|
|
"description": "List soft-deleted documents that can be restored.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"doc_type": {
|
|
"type": "string",
|
|
"description": "Filter by document type (optional)",
|
|
"enum": ["rfc", "spike", "adr", "decision", "prd", "postmortem", "runbook"]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_purge_deleted",
|
|
"description": "Permanently remove soft-deleted documents older than specified days.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"days": {
|
|
"type": "number",
|
|
"description": "Documents deleted more than this many days ago will be purged (default: 7)"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
// RFC 0010: Semantic Index Tools
|
|
{
|
|
"name": "blue_index_status",
|
|
"description": "Get semantic index status. Shows indexed file count, symbol count, and prompt version.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_index_search",
|
|
"description": "Search the semantic index. Returns files or symbols matching the query.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"query": {
|
|
"type": "string",
|
|
"description": "Search query"
|
|
},
|
|
"symbols_only": {
|
|
"type": "boolean",
|
|
"description": "Search symbols only (default: false, searches files)"
|
|
},
|
|
"limit": {
|
|
"type": "number",
|
|
"description": "Maximum results to return (default: 10)"
|
|
}
|
|
},
|
|
"required": ["query"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_index_impact",
|
|
"description": "Analyze impact of changing a file. Shows what depends on it and its relationships.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"file": {
|
|
"type": "string",
|
|
"description": "File path to analyze"
|
|
}
|
|
},
|
|
"required": ["file"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_index_file",
|
|
"description": "Index a single file with AI-generated summary, relationships, and symbols.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
},
|
|
"file_path": {
|
|
"type": "string",
|
|
"description": "File path to index"
|
|
},
|
|
"file_hash": {
|
|
"type": "string",
|
|
"description": "Hash of file contents for staleness detection"
|
|
},
|
|
"summary": {
|
|
"type": "string",
|
|
"description": "One-sentence summary of what the file does"
|
|
},
|
|
"relationships": {
|
|
"type": "string",
|
|
"description": "Description of relationships to other files"
|
|
},
|
|
"symbols": {
|
|
"type": "array",
|
|
"description": "List of symbols in the file",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": { "type": "string" },
|
|
"kind": { "type": "string" },
|
|
"start_line": { "type": "number" },
|
|
"end_line": { "type": "number" },
|
|
"description": { "type": "string" }
|
|
},
|
|
"required": ["name", "kind"]
|
|
}
|
|
}
|
|
},
|
|
"required": ["file_path", "file_hash"]
|
|
}
|
|
},
|
|
{
|
|
"name": "blue_index_realm",
|
|
"description": "List all indexed files in the current realm.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
// RFC 0017: Context Activation tools
|
|
{
|
|
"name": "blue_context_status",
|
|
"description": "Get context injection status: session ID, active injections, staleness, and relevance graph summary.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Current working directory"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}))
|
|
}
|
|
|
|
// ==================== Resources Handlers (RFC 0016) ====================
|
|
|
|
/// Handle resources/list request
|
|
fn handle_resources_list(&mut self) -> Result<Value, ServerError> {
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::resources::handle_resources_list(state)
|
|
}
|
|
|
|
/// Handle resources/read request
|
|
fn handle_resources_read(&mut self, params: &Option<Value>) -> Result<Value, ServerError> {
|
|
let params = params.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let uri = params
|
|
.get("uri")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(ServerError::InvalidParams)?;
|
|
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::resources::handle_resources_read(state, uri)
|
|
}
|
|
|
|
/// Handle tools/call request
|
|
fn handle_tool_call(&mut self, params: &Option<Value>) -> Result<Value, ServerError> {
|
|
let params = params.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let call: ToolCallParams = serde_json::from_value(params.clone())?;
|
|
|
|
// Extract cwd from arguments if present
|
|
if let Some(ref args) = call.arguments {
|
|
if let Some(cwd) = args.get("cwd").and_then(|v| v.as_str()) {
|
|
self.cwd = Some(PathBuf::from(cwd));
|
|
// Reset state when cwd changes
|
|
self.state = None;
|
|
}
|
|
}
|
|
|
|
let result = match call.name.as_str() {
|
|
"blue_status" => self.handle_status(&call.arguments),
|
|
"blue_next" => self.handle_next(&call.arguments),
|
|
"blue_rfc_create" => self.handle_rfc_create(&call.arguments),
|
|
"blue_rfc_get" => self.handle_rfc_get(&call.arguments),
|
|
"blue_rfc_update_status" => self.handle_rfc_update_status(&call.arguments),
|
|
"blue_rfc_plan" => self.handle_rfc_plan(&call.arguments),
|
|
"blue_rfc_task_complete" => self.handle_rfc_task_complete(&call.arguments),
|
|
"blue_rfc_validate" => self.handle_rfc_validate(&call.arguments),
|
|
"blue_search" => self.handle_search(&call.arguments),
|
|
// Phase 2: Workflow handlers
|
|
"blue_spike_create" => self.handle_spike_create(&call.arguments),
|
|
"blue_spike_complete" => self.handle_spike_complete(&call.arguments),
|
|
"blue_adr_create" => self.handle_adr_create(&call.arguments),
|
|
"blue_adr_list" => self.handle_adr_list(),
|
|
"blue_adr_get" => self.handle_adr_get(&call.arguments),
|
|
"blue_adr_relevant" => self.handle_adr_relevant(&call.arguments),
|
|
"blue_adr_audit" => self.handle_adr_audit(),
|
|
"blue_decision_create" => self.handle_decision_create(&call.arguments),
|
|
"blue_worktree_create" => self.handle_worktree_create(&call.arguments),
|
|
"blue_worktree_list" => self.handle_worktree_list(&call.arguments),
|
|
"blue_worktree_remove" => self.handle_worktree_remove(&call.arguments),
|
|
// Phase 3: PR and Release handlers
|
|
"blue_pr_create" => self.handle_pr_create(&call.arguments),
|
|
"blue_pr_verify" => self.handle_pr_verify(&call.arguments),
|
|
"blue_pr_check_item" => self.handle_pr_check_item(&call.arguments),
|
|
"blue_pr_check_approvals" => self.handle_pr_check_approvals(&call.arguments),
|
|
"blue_pr_merge" => self.handle_pr_merge(&call.arguments),
|
|
"blue_release_create" => self.handle_release_create(&call.arguments),
|
|
// Phase 4: Session and Reminder handlers
|
|
"blue_session_ping" => self.handle_session_ping(&call.arguments),
|
|
"blue_session_list" => self.handle_session_list(&call.arguments),
|
|
"blue_reminder_create" => self.handle_reminder_create(&call.arguments),
|
|
"blue_reminder_list" => self.handle_reminder_list(&call.arguments),
|
|
"blue_reminder_snooze" => self.handle_reminder_snooze(&call.arguments),
|
|
"blue_reminder_clear" => self.handle_reminder_clear(&call.arguments),
|
|
// Phase 5: Staging handlers
|
|
"blue_staging_lock" => self.handle_staging_lock(&call.arguments),
|
|
"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),
|
|
"blue_staging_deployments" => self.handle_staging_deployments(&call.arguments),
|
|
// Phase 6: Health check, audit documents, and completion handlers
|
|
"blue_health_check" => self.handle_health_check(&call.arguments),
|
|
"blue_audit_create" => self.handle_audit_create(&call.arguments),
|
|
"blue_audit_list" => self.handle_audit_list(&call.arguments),
|
|
"blue_audit_get" => self.handle_audit_get(&call.arguments),
|
|
"blue_audit_complete" => self.handle_audit_complete(&call.arguments),
|
|
"blue_rfc_complete" => self.handle_rfc_complete(&call.arguments),
|
|
"blue_worktree_cleanup" => self.handle_worktree_cleanup(&call.arguments),
|
|
// Phase 7: PRD handlers
|
|
"blue_prd_create" => self.handle_prd_create(&call.arguments),
|
|
"blue_prd_get" => self.handle_prd_get(&call.arguments),
|
|
"blue_prd_approve" => self.handle_prd_approve(&call.arguments),
|
|
"blue_prd_complete" => self.handle_prd_complete(&call.arguments),
|
|
"blue_prd_list" => self.handle_prd_list(&call.arguments),
|
|
// Phase 7: Lint handler
|
|
"blue_lint" => self.handle_lint(&call.arguments),
|
|
// Phase 7: Environment handlers
|
|
"blue_env_detect" => self.handle_env_detect(&call.arguments),
|
|
"blue_env_mock" => self.handle_env_mock(&call.arguments),
|
|
// Phase 7: Guide handler
|
|
"blue_guide" => self.handle_guide(&call.arguments),
|
|
// Phase 7: Staging IaC handlers
|
|
"blue_staging_create" => self.handle_staging_create(&call.arguments),
|
|
"blue_staging_destroy" => self.handle_staging_destroy(&call.arguments),
|
|
"blue_staging_cost" => self.handle_staging_cost(&call.arguments),
|
|
// Phase 8: Dialogue handlers
|
|
"blue_dialogue_lint" => self.handle_dialogue_lint(&call.arguments),
|
|
"blue_extract_dialogue" => self.handle_extract_dialogue(&call.arguments),
|
|
"blue_dialogue_create" => self.handle_dialogue_create(&call.arguments),
|
|
"blue_dialogue_get" => self.handle_dialogue_get(&call.arguments),
|
|
"blue_dialogue_list" => self.handle_dialogue_list(&call.arguments),
|
|
"blue_dialogue_save" => self.handle_dialogue_save(&call.arguments),
|
|
// Phase 8: Playwright handler
|
|
"blue_playwright_verify" => self.handle_playwright_verify(&call.arguments),
|
|
// Phase 9: Post-mortem handlers
|
|
"blue_postmortem_create" => self.handle_postmortem_create(&call.arguments),
|
|
"blue_postmortem_action_to_rfc" => self.handle_postmortem_action_to_rfc(&call.arguments),
|
|
// Phase 9: Runbook handlers
|
|
"blue_runbook_create" => self.handle_runbook_create(&call.arguments),
|
|
"blue_runbook_update" => self.handle_runbook_update(&call.arguments),
|
|
"blue_runbook_lookup" => self.handle_runbook_lookup(&call.arguments),
|
|
"blue_runbook_actions" => self.handle_runbook_actions(),
|
|
// Phase 10: Realm tools (RFC 0002)
|
|
"blue_realm_status" => self.handle_realm_status(&call.arguments),
|
|
"blue_realm_check" => self.handle_realm_check(&call.arguments),
|
|
"blue_contract_get" => self.handle_contract_get(&call.arguments),
|
|
"blue_session_start" => self.handle_session_start(&call.arguments),
|
|
"blue_session_stop" => self.handle_session_stop(&call.arguments),
|
|
"blue_realm_worktree_create" => self.handle_realm_worktree_create(&call.arguments),
|
|
"blue_realm_pr_status" => self.handle_realm_pr_status(&call.arguments),
|
|
"blue_notifications_list" => self.handle_notifications_list(&call.arguments),
|
|
// RFC 0005: LLM tools
|
|
"blue_llm_start" => crate::handlers::llm::handle_start(&call.arguments.unwrap_or_default()),
|
|
"blue_llm_stop" => crate::handlers::llm::handle_stop(),
|
|
"blue_llm_status" => crate::handlers::llm::handle_status(),
|
|
"blue_llm_providers" => crate::handlers::llm::handle_providers(),
|
|
"blue_model_list" => crate::handlers::llm::handle_model_list(),
|
|
"blue_model_pull" => crate::handlers::llm::handle_model_pull(&call.arguments.unwrap_or_default()),
|
|
"blue_model_remove" => crate::handlers::llm::handle_model_remove(&call.arguments.unwrap_or_default()),
|
|
"blue_model_warmup" => crate::handlers::llm::handle_model_warmup(&call.arguments.unwrap_or_default()),
|
|
// RFC 0006: Delete tools
|
|
"blue_delete" => self.handle_delete(&call.arguments),
|
|
"blue_restore" => self.handle_restore(&call.arguments),
|
|
"blue_deleted_list" => self.handle_deleted_list(&call.arguments),
|
|
"blue_purge_deleted" => self.handle_purge_deleted(&call.arguments),
|
|
// RFC 0010: Semantic Index tools
|
|
"blue_index_status" => self.handle_index_status(),
|
|
"blue_index_search" => self.handle_index_search(&call.arguments),
|
|
"blue_index_impact" => self.handle_index_impact(&call.arguments),
|
|
"blue_index_file" => self.handle_index_file(&call.arguments),
|
|
"blue_index_realm" => self.handle_index_realm(&call.arguments),
|
|
// RFC 0017: Context Activation tools
|
|
"blue_context_status" => self.handle_context_status(&call.arguments),
|
|
_ => Err(ServerError::ToolNotFound(call.name)),
|
|
}?;
|
|
|
|
// Wrap result in MCP tool call response format
|
|
Ok(json!({
|
|
"content": [{
|
|
"type": "text",
|
|
"text": serde_json::to_string_pretty(&result)?
|
|
}]
|
|
}))
|
|
}
|
|
|
|
fn handle_status(&mut self, _args: &Option<Value>) -> Result<Value, ServerError> {
|
|
match self.ensure_state() {
|
|
Ok(state) => {
|
|
let summary = state.status_summary();
|
|
Ok(json!({
|
|
"active": summary.active,
|
|
"ready": summary.ready,
|
|
"stalled": summary.stalled,
|
|
"drafts": summary.drafts,
|
|
"hint": summary.hint
|
|
}))
|
|
}
|
|
Err(_) => {
|
|
// Fall back to a simple message if not in a Blue project
|
|
Ok(json!({
|
|
"message": blue_core::voice::error(
|
|
"Can't find Blue here",
|
|
"Run 'blue init' to set up this project"
|
|
),
|
|
"active": [],
|
|
"ready": [],
|
|
"stalled": [],
|
|
"drafts": []
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_next(&mut self, _args: &Option<Value>) -> Result<Value, ServerError> {
|
|
match self.ensure_state() {
|
|
Ok(state) => {
|
|
let summary = state.status_summary();
|
|
|
|
let recommendations = if !summary.stalled.is_empty() {
|
|
vec![format!(
|
|
"'{}' might be stalled. Check if work is still in progress.",
|
|
summary.stalled[0].title
|
|
)]
|
|
} else if !summary.ready.is_empty() {
|
|
vec![format!(
|
|
"'{}' is ready to implement. Use blue_worktree_create with title='{}' to start.",
|
|
summary.ready[0].title, summary.ready[0].title
|
|
)]
|
|
} else if !summary.drafts.is_empty() {
|
|
vec![format!(
|
|
"'{}' is in draft. Review and accept it when ready.",
|
|
summary.drafts[0].title
|
|
)]
|
|
} else if !summary.active.is_empty() {
|
|
vec![format!(
|
|
"{} item(s) in progress. Keep at it.",
|
|
summary.active.len()
|
|
)]
|
|
} else {
|
|
vec!["Nothing pressing. Good time to plan something new.".to_string()]
|
|
};
|
|
|
|
Ok(json!({
|
|
"recommendations": recommendations,
|
|
"hint": summary.hint
|
|
}))
|
|
}
|
|
Err(_) => {
|
|
Ok(json!({
|
|
"recommendations": [
|
|
"Run 'blue init' to set up this project first."
|
|
],
|
|
"hint": "Can't find Blue here."
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_rfc_create(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
|
|
let title = args
|
|
.get("title")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(ServerError::InvalidParams)?;
|
|
|
|
let problem = args.get("problem").and_then(|v| v.as_str());
|
|
let source_spike = args.get("source_spike").and_then(|v| v.as_str());
|
|
|
|
match self.ensure_state() {
|
|
Ok(state) => {
|
|
// Get next RFC number
|
|
let number = state.store.next_number(DocType::Rfc)
|
|
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
|
|
|
// Generate markdown
|
|
let mut rfc = Rfc::new(title);
|
|
if let Some(p) = problem {
|
|
rfc.problem = Some(p.to_string());
|
|
}
|
|
if let Some(s) = source_spike {
|
|
rfc.source_spike = Some(s.to_string());
|
|
}
|
|
|
|
let markdown = rfc.to_markdown(number as u32);
|
|
|
|
// Generate filename and write file
|
|
let filename = format!("rfcs/{:04}-{}.md", number, title);
|
|
let docs_path = state.home.docs_path.clone();
|
|
let rfc_path = docs_path.join(&filename);
|
|
if let Some(parent) = rfc_path.parent() {
|
|
fs::create_dir_all(parent)
|
|
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
|
}
|
|
fs::write(&rfc_path, &markdown)
|
|
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
|
|
|
// Create document in store with file path
|
|
let mut doc = Document::new(DocType::Rfc, title, "draft");
|
|
doc.number = Some(number);
|
|
doc.file_path = Some(filename.clone());
|
|
|
|
let id = state.store.add_document(&doc)
|
|
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
|
|
|
Ok(json!({
|
|
"status": "success",
|
|
"id": id,
|
|
"number": number,
|
|
"title": title,
|
|
"file": rfc_path.display().to_string(),
|
|
"markdown": markdown,
|
|
"message": blue_core::voice::success(
|
|
&format!("Created RFC {:04}: '{}'", number, title),
|
|
Some("Want me to help fill in the details?")
|
|
)
|
|
}))
|
|
}
|
|
Err(_) => {
|
|
// Create RFC without persistence (just generate markdown)
|
|
let rfc = Rfc::new(title);
|
|
let markdown = rfc.to_markdown(1);
|
|
|
|
Ok(json!({
|
|
"status": "success",
|
|
"number": 1,
|
|
"title": title,
|
|
"markdown": markdown,
|
|
"message": blue_core::voice::success(
|
|
&format!("Created RFC '{}'", title),
|
|
Some("Note: Not persisted - run 'blue init' to enable storage.")
|
|
)
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_rfc_get(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
|
|
let title = args
|
|
.get("title")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(ServerError::InvalidParams)?;
|
|
|
|
let state = self.ensure_state()?;
|
|
|
|
let doc = state.store.find_document(DocType::Rfc, title)
|
|
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
|
|
|
// Get tasks if any
|
|
let tasks = if let Some(id) = doc.id {
|
|
state.store.get_tasks(id).unwrap_or_default()
|
|
} else {
|
|
vec![]
|
|
};
|
|
|
|
let progress = if let Some(id) = doc.id {
|
|
state.store.get_task_progress(id).ok()
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Ok(json!({
|
|
"id": doc.id,
|
|
"number": doc.number,
|
|
"title": doc.title,
|
|
"status": doc.status,
|
|
"file_path": doc.file_path,
|
|
"created_at": doc.created_at,
|
|
"updated_at": doc.updated_at,
|
|
"tasks": tasks.iter().map(|t| json!({
|
|
"index": t.task_index,
|
|
"description": t.description,
|
|
"completed": t.completed
|
|
})).collect::<Vec<_>>(),
|
|
"progress": progress.map(|p| json!({
|
|
"completed": p.completed,
|
|
"total": p.total,
|
|
"percentage": p.percentage
|
|
}))
|
|
}))
|
|
}
|
|
|
|
fn handle_rfc_update_status(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
|
|
let title = args
|
|
.get("title")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(ServerError::InvalidParams)?;
|
|
|
|
let status_str = args
|
|
.get("status")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(ServerError::InvalidParams)?;
|
|
|
|
let state = self.ensure_state()?;
|
|
|
|
// Find the document to get its file path and current status
|
|
let doc = state.store.find_document(DocType::Rfc, title)
|
|
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
|
|
|
// Parse statuses and validate transition (RFC 0014)
|
|
let current_status = RfcStatus::parse(&doc.status)
|
|
.map_err(|e| ServerError::Workflow(e.to_string()))?;
|
|
let target_status = RfcStatus::parse(status_str)
|
|
.map_err(|e| ServerError::Workflow(e.to_string()))?;
|
|
|
|
// Validate the transition
|
|
validate_rfc_transition(current_status, target_status)
|
|
.map_err(|e| ServerError::Workflow(e.to_string()))?;
|
|
|
|
// Check for worktree if going to in-progress (RFC 0011)
|
|
let has_worktree = state.has_worktree(title);
|
|
let worktree_warning = if status_str == "in-progress" && !has_worktree {
|
|
Some("No worktree exists for this RFC. Consider using blue_worktree_create for isolated development.")
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// Update database
|
|
state.store.update_document_status(DocType::Rfc, title, status_str)
|
|
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
|
|
|
// Update markdown file (RFC 0008)
|
|
let file_updated = if let Some(ref file_path) = doc.file_path {
|
|
let full_path = state.home.docs_path.join(file_path);
|
|
blue_core::update_markdown_status(&full_path, status_str).unwrap_or(false)
|
|
} else {
|
|
false
|
|
};
|
|
|
|
// Conversational hints guide Claude to next action (RFC 0014)
|
|
let hint = match target_status {
|
|
RfcStatus::Accepted => Some(
|
|
"RFC accepted. Ask the user: 'Ready to begin implementation? \
|
|
I'll create a worktree and set up the environment.'"
|
|
),
|
|
RfcStatus::InProgress => Some(
|
|
"Implementation started. Work in the worktree, mark plan tasks \
|
|
as you complete them."
|
|
),
|
|
RfcStatus::Implemented => Some(
|
|
"Implementation complete. Ask the user: 'Ready to create a PR?'"
|
|
),
|
|
RfcStatus::Superseded => Some(
|
|
"RFC superseded. The newer RFC takes precedence."
|
|
),
|
|
RfcStatus::Draft => None,
|
|
};
|
|
|
|
// Build next_action for accepted status (RFC 0011)
|
|
let next_action = if status_str == "accepted" {
|
|
Some(json!({
|
|
"tool": "blue_worktree_create",
|
|
"args": { "title": title },
|
|
"hint": "Create a worktree to start implementation"
|
|
}))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let mut response = json!({
|
|
"status": "success",
|
|
"title": title,
|
|
"new_status": status_str,
|
|
"file_updated": file_updated,
|
|
"message": blue_core::voice::success(
|
|
&format!("Updated '{}' to {}", title, status_str),
|
|
hint
|
|
)
|
|
});
|
|
|
|
// Add optional fields
|
|
if let Some(action) = next_action {
|
|
response["next_action"] = action;
|
|
}
|
|
if let Some(warning) = worktree_warning {
|
|
response["warning"] = json!(warning);
|
|
}
|
|
|
|
Ok(response)
|
|
}
|
|
|
|
fn handle_rfc_plan(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
|
|
let title = args
|
|
.get("title")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(ServerError::InvalidParams)?;
|
|
|
|
let tasks: Vec<String> = args
|
|
.get("tasks")
|
|
.and_then(|v| v.as_array())
|
|
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
|
|
.unwrap_or_default();
|
|
|
|
let state = self.ensure_state()?;
|
|
|
|
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)?;
|
|
|
|
state.store.set_tasks(doc_id, &tasks)
|
|
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
|
|
|
Ok(json!({
|
|
"status": "success",
|
|
"title": title,
|
|
"task_count": tasks.len(),
|
|
"message": blue_core::voice::success(
|
|
&format!("Set {} tasks for '{}'", tasks.len(), title),
|
|
Some("Mark them complete as you go with blue_rfc_task_complete.")
|
|
)
|
|
}))
|
|
}
|
|
|
|
fn handle_rfc_task_complete(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
|
|
let title = args
|
|
.get("title")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(ServerError::InvalidParams)?;
|
|
|
|
let task = args
|
|
.get("task")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(ServerError::InvalidParams)?;
|
|
|
|
let state = self.ensure_state()?;
|
|
|
|
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)?;
|
|
|
|
// Parse task index or find by substring
|
|
let task_index = if let Ok(idx) = task.parse::<i32>() {
|
|
idx
|
|
} else {
|
|
// Find task by substring
|
|
let tasks = state.store.get_tasks(doc_id)
|
|
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
|
|
|
tasks.iter()
|
|
.find(|t| t.description.to_lowercase().contains(&task.to_lowercase()))
|
|
.map(|t| t.task_index)
|
|
.ok_or(ServerError::InvalidParams)?
|
|
};
|
|
|
|
state.store.complete_task(doc_id, task_index)
|
|
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
|
|
|
let progress = state.store.get_task_progress(doc_id)
|
|
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
|
|
|
Ok(json!({
|
|
"status": "success",
|
|
"title": title,
|
|
"task_index": task_index,
|
|
"progress": {
|
|
"completed": progress.completed,
|
|
"total": progress.total,
|
|
"percentage": progress.percentage
|
|
},
|
|
"message": blue_core::voice::success(
|
|
&format!("Task {} complete. {} of {} done ({}%)",
|
|
task_index, progress.completed, progress.total, progress.percentage),
|
|
None
|
|
)
|
|
}))
|
|
}
|
|
|
|
fn handle_rfc_validate(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
|
|
let title = args
|
|
.get("title")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(ServerError::InvalidParams)?;
|
|
|
|
let state = self.ensure_state()?;
|
|
|
|
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)?;
|
|
|
|
let progress = state.store.get_task_progress(doc_id)
|
|
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
|
|
|
let message = if progress.total == 0 {
|
|
"No plan defined yet. Use blue_rfc_plan to add tasks.".to_string()
|
|
} else if progress.percentage == 100 {
|
|
format!("All {} tasks complete. Ready to mark as implemented.", progress.total)
|
|
} else if progress.percentage >= 70 {
|
|
format!("{}% done ({}/{}). Getting close.", progress.percentage, progress.completed, progress.total)
|
|
} else {
|
|
format!("{}% done ({}/{}). Keep going.", progress.percentage, progress.completed, progress.total)
|
|
};
|
|
|
|
Ok(json!({
|
|
"title": doc.title,
|
|
"status": doc.status,
|
|
"progress": {
|
|
"completed": progress.completed,
|
|
"total": progress.total,
|
|
"percentage": progress.percentage
|
|
},
|
|
"message": message
|
|
}))
|
|
}
|
|
|
|
fn handle_search(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
|
|
let query = args
|
|
.get("query")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(ServerError::InvalidParams)?;
|
|
|
|
let doc_type = args.get("doc_type").and_then(|v| v.as_str()).and_then(DocType::from_str);
|
|
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
|
|
|
|
let state = self.ensure_state()?;
|
|
|
|
// Check for adr: prefix query (RFC 0004)
|
|
if let Some(adr_num_str) = query.strip_prefix("adr:") {
|
|
if let Ok(adr_num) = adr_num_str.trim().parse::<i32>() {
|
|
// Find documents that cite this ADR
|
|
return Self::search_adr_citations(state, adr_num, limit);
|
|
}
|
|
}
|
|
|
|
let results = state.store.search_documents(query, doc_type, limit)
|
|
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
|
|
|
Ok(json!({
|
|
"query": query,
|
|
"count": results.len(),
|
|
"results": results.iter().map(|r| json!({
|
|
"title": r.document.title,
|
|
"type": r.document.doc_type.as_str(),
|
|
"status": r.document.status,
|
|
"score": r.score
|
|
})).collect::<Vec<_>>()
|
|
}))
|
|
}
|
|
|
|
/// Search for documents citing a specific ADR (RFC 0004)
|
|
fn search_adr_citations(state: &ProjectState, adr_num: i32, limit: usize) -> Result<Value, ServerError> {
|
|
// Find the ADR document first
|
|
let adrs = state.store.list_documents(DocType::Adr)
|
|
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
|
|
|
let adr_doc = adrs.into_iter().find(|d| d.number == Some(adr_num));
|
|
|
|
let Some(adr) = adr_doc else {
|
|
return Ok(json!({
|
|
"query": format!("adr:{}", adr_num),
|
|
"count": 0,
|
|
"results": [],
|
|
"message": format!("ADR {} not found", adr_num)
|
|
}));
|
|
};
|
|
|
|
let Some(adr_id) = adr.id else {
|
|
return Ok(json!({
|
|
"query": format!("adr:{}", adr_num),
|
|
"count": 0,
|
|
"results": []
|
|
}));
|
|
};
|
|
|
|
// Find documents that link to this ADR
|
|
let query = "SELECT d.id, d.doc_type, d.title, d.status
|
|
FROM documents d
|
|
JOIN document_links l ON l.source_id = d.id
|
|
WHERE l.target_id = ?1
|
|
LIMIT ?2";
|
|
|
|
let conn = state.store.conn();
|
|
let mut stmt = conn.prepare(query)
|
|
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
|
|
|
let rows = stmt.query_map(rusqlite::params![adr_id, limit], |row| {
|
|
Ok((
|
|
row.get::<_, String>(1)?, // doc_type
|
|
row.get::<_, String>(2)?, // title
|
|
row.get::<_, String>(3)?, // status
|
|
))
|
|
}).map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
|
|
|
|
let mut results = Vec::new();
|
|
for row in rows.flatten() {
|
|
let (doc_type, title, status) = row;
|
|
results.push(json!({
|
|
"title": title,
|
|
"type": doc_type,
|
|
"status": status,
|
|
"score": 1.0
|
|
}));
|
|
}
|
|
|
|
Ok(json!({
|
|
"query": format!("adr:{}", adr_num),
|
|
"adr_title": adr.title,
|
|
"count": results.len(),
|
|
"results": results
|
|
}))
|
|
}
|
|
|
|
// Phase 2: Workflow handlers
|
|
|
|
fn handle_spike_create(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::spike::handle_create(state, args)
|
|
}
|
|
|
|
fn handle_spike_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::spike::handle_complete(state, args)
|
|
}
|
|
|
|
fn handle_adr_create(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::adr::handle_create(state, args)
|
|
}
|
|
|
|
fn handle_adr_list(&mut self) -> Result<Value, ServerError> {
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::adr::handle_list(state)
|
|
}
|
|
|
|
fn handle_adr_get(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::adr::handle_get(state, args)
|
|
}
|
|
|
|
fn handle_adr_relevant(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::adr::handle_relevant(state, args)
|
|
}
|
|
|
|
fn handle_adr_audit(&mut self) -> Result<Value, ServerError> {
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::adr::handle_audit(state)
|
|
}
|
|
|
|
fn handle_decision_create(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::decision::handle_create(state, args)
|
|
}
|
|
|
|
fn handle_worktree_create(&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_create(state, args)
|
|
}
|
|
|
|
fn handle_worktree_list(&mut self, _args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::worktree::handle_list(state)
|
|
}
|
|
|
|
fn handle_worktree_remove(&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_remove(state, args)
|
|
}
|
|
|
|
// Phase 3: PR and Release handlers
|
|
|
|
fn handle_pr_create(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::pr::handle_create(state, args)
|
|
}
|
|
|
|
fn handle_pr_verify(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::pr::handle_verify(state, args)
|
|
}
|
|
|
|
fn handle_pr_check_item(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::pr::handle_check_item(state, args)
|
|
}
|
|
|
|
fn handle_pr_check_approvals(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::pr::handle_check_approvals(state, args)
|
|
}
|
|
|
|
fn handle_pr_merge(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::pr::handle_merge(state, args)
|
|
}
|
|
|
|
fn handle_release_create(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::release::handle_create(state, args)
|
|
}
|
|
|
|
// Phase 4: Session and Reminder handlers
|
|
|
|
fn handle_session_ping(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::session::handle_ping(state, args)
|
|
}
|
|
|
|
fn handle_session_list(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let empty = json!({});
|
|
let args = args.as_ref().unwrap_or(&empty);
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::session::handle_list(state, args)
|
|
}
|
|
|
|
fn handle_reminder_create(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::reminder::handle_create(state, args)
|
|
}
|
|
|
|
fn handle_reminder_list(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let empty = json!({});
|
|
let args = args.as_ref().unwrap_or(&empty);
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::reminder::handle_list(state, args)
|
|
}
|
|
|
|
fn handle_reminder_snooze(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::reminder::handle_snooze(state, args)
|
|
}
|
|
|
|
fn handle_reminder_clear(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::reminder::handle_clear(state, args)
|
|
}
|
|
|
|
// Phase 5: Staging handlers
|
|
|
|
fn handle_staging_lock(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::staging::handle_lock(state, args)
|
|
}
|
|
|
|
fn handle_staging_unlock(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::staging::handle_unlock(state, args)
|
|
}
|
|
|
|
fn handle_staging_status(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let empty = json!({});
|
|
let args = args.as_ref().unwrap_or(&empty);
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::staging::handle_status(state, args)
|
|
}
|
|
|
|
fn handle_staging_cleanup(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let empty = json!({});
|
|
let args = args.as_ref().unwrap_or(&empty);
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::staging::handle_cleanup(state, args)
|
|
}
|
|
|
|
fn handle_staging_deployments(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let empty = json!({});
|
|
let args = args.as_ref().unwrap_or(&empty);
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::staging::handle_deployments(state, args)
|
|
}
|
|
|
|
// Phase 6: Health check, audit documents, and completion handlers
|
|
|
|
fn handle_health_check(&mut self, _args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::audit::handle_audit(state)
|
|
}
|
|
|
|
fn handle_audit_create(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::audit_doc::handle_create(state, args)
|
|
}
|
|
|
|
fn handle_audit_list(&mut self, _args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::audit_doc::handle_list(state)
|
|
}
|
|
|
|
fn handle_audit_get(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::audit_doc::handle_get(state, args)
|
|
}
|
|
|
|
fn handle_audit_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::audit_doc::handle_complete(state, args)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// Phase 7: PRD handlers
|
|
|
|
fn handle_prd_create(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::prd::handle_create(state, args)
|
|
}
|
|
|
|
fn handle_prd_get(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::prd::handle_get(state, args)
|
|
}
|
|
|
|
fn handle_prd_approve(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::prd::handle_approve(state, args)
|
|
}
|
|
|
|
fn handle_prd_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::prd::handle_complete(state, args)
|
|
}
|
|
|
|
fn handle_prd_list(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let empty = json!({});
|
|
let args = args.as_ref().unwrap_or(&empty);
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::prd::handle_list(state, args)
|
|
}
|
|
|
|
// Phase 7: Lint handler
|
|
|
|
fn handle_lint(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let empty = json!({});
|
|
let args = args.as_ref().unwrap_or(&empty);
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::lint::handle_lint(args, &state.home.root)
|
|
}
|
|
|
|
// Phase 7: Environment handlers
|
|
|
|
fn handle_env_detect(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let empty = json!({});
|
|
let args = args.as_ref().unwrap_or(&empty);
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::env::handle_detect(args, &state.home.root)
|
|
}
|
|
|
|
fn handle_env_mock(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let empty = json!({});
|
|
let args = args.as_ref().unwrap_or(&empty);
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::env::handle_mock(args, &state.home.root)
|
|
}
|
|
|
|
// Phase 7: Guide handler
|
|
|
|
fn handle_guide(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let empty = json!({});
|
|
let args = args.as_ref().unwrap_or(&empty);
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::guide::handle_guide(args, &state.home.blue_dir)
|
|
}
|
|
|
|
// Phase 7: Staging IaC handlers
|
|
|
|
fn handle_staging_create(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let empty = json!({});
|
|
let args = args.as_ref().unwrap_or(&empty);
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::staging::handle_create(args, &state.home.root)
|
|
}
|
|
|
|
fn handle_staging_destroy(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let empty = json!({});
|
|
let args = args.as_ref().unwrap_or(&empty);
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::staging::handle_destroy(args, &state.home.root)
|
|
}
|
|
|
|
fn handle_staging_cost(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let empty = json!({});
|
|
let args = args.as_ref().unwrap_or(&empty);
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::staging::handle_cost(args, &state.home.root)
|
|
}
|
|
|
|
// Phase 8: Dialogue and Playwright handlers
|
|
|
|
fn handle_dialogue_lint(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
crate::handlers::dialogue_lint::handle_dialogue_lint(args)
|
|
}
|
|
|
|
fn handle_extract_dialogue(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
crate::handlers::dialogue::handle_extract_dialogue(args)
|
|
}
|
|
|
|
fn handle_dialogue_create(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state_mut()?;
|
|
crate::handlers::dialogue::handle_create(state, args)
|
|
}
|
|
|
|
fn handle_dialogue_get(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::dialogue::handle_get(state, args)
|
|
}
|
|
|
|
fn handle_dialogue_list(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let empty = json!({});
|
|
let args = args.as_ref().unwrap_or(&empty);
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::dialogue::handle_list(state, args)
|
|
}
|
|
|
|
fn handle_dialogue_save(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state_mut()?;
|
|
crate::handlers::dialogue::handle_save(state, args)
|
|
}
|
|
|
|
fn handle_playwright_verify(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
crate::handlers::playwright::handle_verify(args)
|
|
}
|
|
|
|
// Phase 9: Post-mortem handlers
|
|
|
|
fn handle_postmortem_create(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state_mut()?;
|
|
crate::handlers::postmortem::handle_create(state, args)
|
|
}
|
|
|
|
fn handle_postmortem_action_to_rfc(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state_mut()?;
|
|
crate::handlers::postmortem::handle_action_to_rfc(state, args)
|
|
}
|
|
|
|
// Phase 9: Runbook handlers
|
|
|
|
fn handle_runbook_create(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state_mut()?;
|
|
crate::handlers::runbook::handle_create(state, args)
|
|
}
|
|
|
|
fn handle_runbook_update(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state_mut()?;
|
|
crate::handlers::runbook::handle_update(state, args)
|
|
}
|
|
|
|
fn handle_runbook_lookup(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::runbook::handle_lookup(state, args)
|
|
}
|
|
|
|
fn handle_runbook_actions(&mut self) -> Result<Value, ServerError> {
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::runbook::handle_actions(state)
|
|
}
|
|
|
|
// Phase 10: Realm handlers (RFC 0002)
|
|
|
|
fn handle_realm_status(&mut self, _args: &Option<Value>) -> Result<Value, ServerError> {
|
|
crate::handlers::realm::handle_status(self.cwd.as_deref())
|
|
}
|
|
|
|
fn handle_realm_check(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let realm = args
|
|
.as_ref()
|
|
.and_then(|a| a.get("realm"))
|
|
.and_then(|v| v.as_str());
|
|
crate::handlers::realm::handle_check(self.cwd.as_deref(), realm)
|
|
}
|
|
|
|
fn handle_contract_get(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let domain = args
|
|
.get("domain")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(ServerError::InvalidParams)?;
|
|
let contract = args
|
|
.get("contract")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(ServerError::InvalidParams)?;
|
|
crate::handlers::realm::handle_contract_get(self.cwd.as_deref(), domain, contract)
|
|
}
|
|
|
|
// Phase 2: Session handlers (RFC 0002)
|
|
|
|
fn handle_session_start(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let active_rfc = args
|
|
.as_ref()
|
|
.and_then(|a| a.get("active_rfc"))
|
|
.and_then(|v| v.as_str());
|
|
crate::handlers::realm::handle_session_start(self.cwd.as_deref(), active_rfc)
|
|
}
|
|
|
|
fn handle_session_stop(&mut self, _args: &Option<Value>) -> Result<Value, ServerError> {
|
|
crate::handlers::realm::handle_session_stop(self.cwd.as_deref())
|
|
}
|
|
|
|
// Phase 3: Workflow handlers (RFC 0002)
|
|
|
|
fn handle_realm_worktree_create(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let rfc = args
|
|
.get("rfc")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(ServerError::InvalidParams)?;
|
|
let repos: Option<Vec<&str>> = args
|
|
.get("repos")
|
|
.and_then(|v| v.as_array())
|
|
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect());
|
|
crate::handlers::realm::handle_worktree_create(self.cwd.as_deref(), rfc, repos)
|
|
}
|
|
|
|
fn handle_realm_pr_status(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let rfc = args
|
|
.as_ref()
|
|
.and_then(|a| a.get("rfc"))
|
|
.and_then(|v| v.as_str());
|
|
crate::handlers::realm::handle_pr_status(self.cwd.as_deref(), rfc)
|
|
}
|
|
|
|
// Phase 4: Notifications handler (RFC 0002)
|
|
|
|
fn handle_notifications_list(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let state = args
|
|
.as_ref()
|
|
.and_then(|a| a.get("state"))
|
|
.and_then(|v| v.as_str());
|
|
crate::handlers::realm::handle_notifications_list(self.cwd.as_deref(), state)
|
|
}
|
|
|
|
// RFC 0006: Delete handlers
|
|
|
|
fn handle_delete(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
|
|
let doc_type_str = args
|
|
.get("doc_type")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(ServerError::InvalidParams)?;
|
|
let doc_type = DocType::from_str(doc_type_str)
|
|
.ok_or(ServerError::InvalidParams)?;
|
|
|
|
let title = args
|
|
.get("title")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(ServerError::InvalidParams)?;
|
|
|
|
let dry_run = args
|
|
.get("dry_run")
|
|
.and_then(|v| v.as_bool())
|
|
.unwrap_or(false);
|
|
|
|
let force = args
|
|
.get("force")
|
|
.and_then(|v| v.as_bool())
|
|
.unwrap_or(false);
|
|
|
|
let permanent = args
|
|
.get("permanent")
|
|
.and_then(|v| v.as_bool())
|
|
.unwrap_or(false);
|
|
|
|
if dry_run {
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::delete::handle_delete_dry_run(state, doc_type, title)
|
|
} else {
|
|
let state = self.ensure_state_mut()?;
|
|
crate::handlers::delete::handle_delete(state, doc_type, title, force, permanent)
|
|
}
|
|
}
|
|
|
|
fn handle_restore(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
|
|
let doc_type_str = args
|
|
.get("doc_type")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(ServerError::InvalidParams)?;
|
|
let doc_type = DocType::from_str(doc_type_str)
|
|
.ok_or(ServerError::InvalidParams)?;
|
|
|
|
let title = args
|
|
.get("title")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(ServerError::InvalidParams)?;
|
|
|
|
let state = self.ensure_state_mut()?;
|
|
crate::handlers::delete::handle_restore(state, doc_type, title)
|
|
}
|
|
|
|
fn handle_deleted_list(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let doc_type = args
|
|
.as_ref()
|
|
.and_then(|a| a.get("doc_type"))
|
|
.and_then(|v| v.as_str())
|
|
.and_then(DocType::from_str);
|
|
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::delete::handle_list_deleted(state, doc_type)
|
|
}
|
|
|
|
fn handle_purge_deleted(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let days = args
|
|
.as_ref()
|
|
.and_then(|a| a.get("days"))
|
|
.and_then(|v| v.as_i64())
|
|
.unwrap_or(7);
|
|
|
|
let state = self.ensure_state_mut()?;
|
|
crate::handlers::delete::handle_purge_deleted(state, days)
|
|
}
|
|
|
|
// RFC 0010: Semantic Index handlers
|
|
|
|
fn handle_index_status(&mut self) -> Result<Value, ServerError> {
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::index::handle_status(state)
|
|
}
|
|
|
|
fn handle_index_search(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::index::handle_search(state, args)
|
|
}
|
|
|
|
fn handle_index_impact(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::index::handle_impact(state, args)
|
|
}
|
|
|
|
fn handle_index_file(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let args = args.as_ref().ok_or(ServerError::InvalidParams)?;
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::index::handle_index_file(state, args)
|
|
}
|
|
|
|
fn handle_index_realm(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let default_args = serde_json::json!({});
|
|
let args = args.as_ref().unwrap_or(&default_args);
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::index::handle_index_realm(state, args)
|
|
}
|
|
|
|
// RFC 0017: Context Activation handlers
|
|
fn handle_context_status(&mut self, _args: &Option<Value>) -> Result<Value, ServerError> {
|
|
let state = self.ensure_state()?;
|
|
crate::handlers::resources::handle_context_status(state)
|
|
}
|
|
}
|
|
|
|
impl Default for BlueServer {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct JsonRpcRequest {
|
|
#[allow(dead_code)]
|
|
jsonrpc: String,
|
|
method: String,
|
|
params: Option<Value>,
|
|
id: Option<Value>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct ToolCallParams {
|
|
name: String,
|
|
arguments: Option<Value>,
|
|
}
|