[文書] Playbook v4: ビジョン全38要素をカバーする完全版

v3 から追加:
  Sprint 3: 記憶アタッチメント (#58) + ドリーミング (#59) + Web ダッシュボード (#63)
  Sprint 4: シータサイクル (#60) + Obsidian (#62)
  Sprint 2: Bus データパス統合 (#65) + ブランチ戦略 (#64)

新規 Issue:
  #64: 並列Codexブランチ戦略 (worktree + PR)
  #65: Bus データパス統合

ビジョン達成度推移:
  現在 32% → Sprint1後 38% → Sprint2後 47% → Sprint3後 72% → Sprint4後 93%

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
林 駿甫 (Shunsuke Hayashi) 2026-04-10 06:54:20 +09:00
parent a1febb67eb
commit ec1d25887e
29 changed files with 471 additions and 196 deletions

View file

@ -92,13 +92,11 @@ impl ApprovalCallback for RejectHighRisk {
async fn request_approval(&self, request: &ApprovalRequest) -> ApprovalDecision {
match request.risk_level {
RiskLevel::Low | RiskLevel::Medium => ApprovalDecision::Approved,
RiskLevel::High | RiskLevel::Critical => {
ApprovalDecision::Rejected(Some(format!(
"Tool {} requires manual approval (risk level: {})",
request.name,
request.risk_level.as_str()
)))
}
RiskLevel::High | RiskLevel::Critical => ApprovalDecision::Rejected(Some(format!(
"Tool {} requires manual approval (risk level: {})",
request.name,
request.risk_level.as_str()
))),
}
}
}
@ -187,7 +185,10 @@ mod tests {
risk_level: RiskLevel::Low,
description: "Read file".to_string(),
};
assert_eq!(approver.request_approval(&low_risk).await, ApprovalDecision::Approved);
assert_eq!(
approver.request_approval(&low_risk).await,
ApprovalDecision::Approved
);
// High risk should be rejected
let high_risk = ApprovalRequest {

View file

@ -153,9 +153,10 @@ impl Agent {
/// Main agent execution loop
pub async fn run(&self, prompt: &str) -> Result<AgentResult, AgentError> {
// Execute SessionStart hooks
let session_context = HookContext::new()
.with_data("prompt", prompt);
self.hook_manager.execute(&HookEvent::SessionStart, &session_context).await;
let session_context = HookContext::new().with_data("prompt", prompt);
self.hook_manager
.execute(&HookEvent::SessionStart, &session_context)
.await;
self.emit_event(AgentEvent::Started {
prompt: prompt.to_string(),
@ -220,7 +221,9 @@ impl Agent {
let end_context = HookContext::new()
.with_data("iterations", &result.iterations.to_string())
.with_data("tool_calls", &result.tool_calls.to_string());
self.hook_manager.execute(&HookEvent::SessionEnd, &end_context).await;
self.hook_manager
.execute(&HookEvent::SessionEnd, &end_context)
.await;
return Ok(result);
}
@ -302,7 +305,9 @@ impl Agent {
let pre_tool_context = HookContext::new()
.with_tool(&tool_use.name)
.with_data("input", &tool_use.input.to_string());
self.hook_manager.execute(&HookEvent::PreTool, &pre_tool_context).await;
self.hook_manager
.execute(&HookEvent::PreTool, &pre_tool_context)
.await;
// Execute tool
self.emit_event(AgentEvent::ToolExecuting {
@ -327,7 +332,9 @@ impl Agent {
let post_tool_context = HookContext::new()
.with_tool(&tool_use.name)
.with_result(&output.content.to_string());
self.hook_manager.execute(&HookEvent::PostTool, &post_tool_context).await;
self.hook_manager
.execute(&HookEvent::PostTool, &post_tool_context)
.await;
// Create tool result
let content = serde_json::to_string(&output.content)
@ -349,7 +356,9 @@ impl Agent {
let error_context = HookContext::new()
.with_tool(&tool_use.name)
.with_error(&e.to_string());
self.hook_manager.execute(&HookEvent::OnError, &error_context).await;
self.hook_manager
.execute(&HookEvent::OnError, &error_context)
.await;
// Add error as tool result
results.push(ContentBlock::ToolResult {
@ -397,7 +406,9 @@ impl Agent {
let end_context = HookContext::new()
.with_data("iterations", &result.iterations.to_string())
.with_data("tool_calls", &result.tool_calls.to_string());
self.hook_manager.execute(&HookEvent::SessionEnd, &end_context).await;
self.hook_manager
.execute(&HookEvent::SessionEnd, &end_context)
.await;
return Ok(result);
}

View file

@ -134,11 +134,7 @@ pub struct McpToolExecutor {
}
impl McpToolExecutor {
pub fn new(
server_name: String,
tool: McpTool,
manager: Arc<AsyncMutex<McpManager>>,
) -> Self {
pub fn new(server_name: String, tool: McpTool, manager: Arc<AsyncMutex<McpManager>>) -> Self {
Self {
server_name,
tool_name: tool.name,
@ -178,10 +174,13 @@ impl ToolExecutor for McpToolExecutor {
async fn execute(&self, input: Value) -> Result<ToolOutput, ToolError> {
let mut manager = self.manager.lock().await;
match manager.call_tool(&self.server_name, &self.tool_name, input).await {
match manager
.call_tool(&self.server_name, &self.tool_name, input)
.await
{
Ok(result) => {
let content = serde_json::to_string_pretty(&result)
.unwrap_or_else(|_| result.to_string());
let content =
serde_json::to_string_pretty(&result).unwrap_or_else(|_| result.to_string());
Ok(ToolOutput::success(content))
}
Err(e) => Err(ToolError::ExecutionFailed(e.to_string())),
@ -270,7 +269,10 @@ impl ExecutorRegistry {
}
/// Register MCP tools from an MCP manager
pub async fn register_mcp_tools(&mut self, manager: Arc<AsyncMutex<McpManager>>) -> Result<usize, ToolError> {
pub async fn register_mcp_tools(
&mut self,
manager: Arc<AsyncMutex<McpManager>>,
) -> Result<usize, ToolError> {
let mut count = 0;
// Get all tools from all servers
@ -283,11 +285,7 @@ impl ExecutorRegistry {
for (server_name, tools) in all_tools {
for tool in tools {
let executor = McpToolExecutor::new(
server_name.clone(),
tool,
manager.clone(),
);
let executor = McpToolExecutor::new(server_name.clone(), tool, manager.clone());
self.register(executor);
count += 1;
}

View file

@ -13,8 +13,7 @@ mod approval_integration_tests {
let registry = ExecutorRegistry::with_standard_tools();
// Agent should be created successfully with AutoApproveAll callback
let _agent = Agent::new(client, registry)
.with_approval_callback(AutoApproveAll);
let _agent = Agent::new(client, registry).with_approval_callback(AutoApproveAll);
}
#[test]
@ -23,8 +22,7 @@ mod approval_integration_tests {
let registry = ExecutorRegistry::with_standard_tools();
// Agent should be created successfully with RejectHighRisk callback
let _agent = Agent::new(client, registry)
.with_approval_callback(RejectHighRisk);
let _agent = Agent::new(client, registry).with_approval_callback(RejectHighRisk);
}
#[test]

View file

@ -257,7 +257,9 @@ mod tests {
let cache = TTLCache::new(Duration::from_secs(3600)); // default 1 hour
// Insert with shorter custom TTL
cache.insert_with_ttl("key1", "value1", Duration::from_millis(50)).await;
cache
.insert_with_ttl("key1", "value1", Duration::from_millis(50))
.await;
assert_eq!(cache.get(&"key1").await, Some("value1"));
sleep(Duration::from_millis(100)).await;
@ -301,7 +303,9 @@ mod tests {
let cache = TTLCache::new(Duration::from_millis(50));
cache.insert("key1", "value1").await;
cache.insert_with_ttl("key2", "value2", Duration::from_secs(3600)).await;
cache
.insert_with_ttl("key2", "value2", Duration::from_secs(3600))
.await;
sleep(Duration::from_millis(100)).await;
@ -437,7 +441,9 @@ mod tests {
assert!(cache.get(&key).await.is_none());
// Insert
cache.insert(key.clone(), "Response from LLM".to_string()).await;
cache
.insert(key.clone(), "Response from LLM".to_string())
.await;
// Hit
let cached = cache.get(&key).await;

View file

@ -21,7 +21,6 @@ pub struct Config {
pub tools: ToolConfig,
}
/// API configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]

View file

@ -568,8 +568,7 @@ mod tests {
#[test]
fn test_task_with_priority() {
let task = Task::new("priority-task", "Task with priority")
.with_priority(10);
let task = Task::new("priority-task", "Task with priority").with_priority(10);
assert_eq!(task.priority, 10);
assert_eq!(task.name, "priority-task");
@ -577,8 +576,7 @@ mod tests {
#[test]
fn test_task_with_estimated_time() {
let task = Task::new("timed-task", "Task with time estimate")
.with_estimated_time(120);
let task = Task::new("timed-task", "Task with time estimate").with_estimated_time(120);
assert_eq!(task.estimated_time, Some(120));
}

View file

@ -315,7 +315,9 @@ mod tests {
fn test_fallback_strategy_lower_temperature() {
let strategy = FallbackStrategy::lower_temperature();
match strategy {
FallbackStrategy::RetryWithLowerTemperature { temperature_reduction } => {
FallbackStrategy::RetryWithLowerTemperature {
temperature_reduction,
} => {
assert_eq!(temperature_reduction, 0.2);
}
_ => panic!("Expected RetryWithLowerTemperature"),

View file

@ -66,7 +66,10 @@ impl FeatureFlagManager {
/// `true` if the flag exists and is enabled, `false` otherwise
pub fn is_enabled(&self, flag_name: &str) -> bool {
let flags = self.flags.read().unwrap();
flags.get(flag_name).map(|flag| flag.enabled).unwrap_or(false)
flags
.get(flag_name)
.map(|flag| flag.enabled)
.unwrap_or(false)
}
/// Set a feature flag
@ -190,7 +193,10 @@ impl FeatureFlagManager {
/// HashMap of flag names to enabled status
pub fn export_to_map(&self) -> HashMap<String, bool> {
let flags = self.flags.read().unwrap();
flags.iter().map(|(name, flag)| (name.clone(), flag.enabled)).collect()
flags
.iter()
.map(|(name, flag)| (name.clone(), flag.enabled))
.collect()
}
/// Clear all feature flags

View file

@ -29,14 +29,12 @@ pub fn find_git_root(start_path: Option<&Path>) -> Result<PathBuf> {
};
match git2::Repository::discover(&search_path) {
Ok(repo) => repo
.workdir()
.map(|p| p.to_path_buf())
.ok_or_else(|| {
Error::Git(
"Repository is bare (no working directory). Miyabi requires a non-bare repository.".to_string()
)
}),
Ok(repo) => repo.workdir().map(|p| p.to_path_buf()).ok_or_else(|| {
Error::Git(
"Repository is bare (no working directory). Miyabi requires a non-bare repository."
.to_string(),
)
}),
Err(e) => Err(Error::Git(format!(
"Not in a Git repository. Please run miyabi from within a Git repository.\n\
Searched from: {:?}\n\

View file

@ -139,8 +139,7 @@ impl GitHubClient {
/// Create from environment variables
pub fn from_env() -> Result<Self> {
let token = std::env::var("GITHUB_TOKEN")
.map_err(|_| anyhow!("GITHUB_TOKEN not set"))?;
let token = std::env::var("GITHUB_TOKEN").map_err(|_| anyhow!("GITHUB_TOKEN not set"))?;
let repo = std::env::var("GITHUB_REPOSITORY")
.or_else(|_| std::env::var("REPOSITORY"))
.map_err(|_| anyhow!("GITHUB_REPOSITORY or REPOSITORY not set"))?;
@ -156,7 +155,11 @@ impl GitHubClient {
// ========== Issues ==========
/// List issues
pub async fn list_issues(&self, state: Option<&str>, labels: Option<&str>) -> Result<Vec<Issue>> {
pub async fn list_issues(
&self,
state: Option<&str>,
labels: Option<&str>,
) -> Result<Vec<Issue>> {
let mut url = format!(
"{}/repos/{}/{}/issues",
self.base_url, self.owner, self.repo
@ -230,7 +233,8 @@ impl GitHubClient {
self.base_url, self.owner, self.repo, issue_number
);
let response = self.client
let response = self
.client
.post(&url)
.json(&serde_json::json!({ "labels": labels }))
.send()
@ -253,7 +257,8 @@ impl GitHubClient {
self.base_url, self.owner, self.repo, number
);
let response = self.client
let response = self
.client
.patch(&url)
.json(&serde_json::json!({ "state": "closed" }))
.send()
@ -273,10 +278,7 @@ impl GitHubClient {
/// List pull requests
pub async fn list_pull_requests(&self, state: Option<&str>) -> Result<Vec<PullRequest>> {
let mut url = format!(
"{}/repos/{}/{}/pulls",
self.base_url, self.owner, self.repo
);
let mut url = format!("{}/repos/{}/{}/pulls", self.base_url, self.owner, self.repo);
if let Some(s) = state {
url = format!("{}?state={}", url, s);
@ -314,11 +316,11 @@ impl GitHubClient {
}
/// Create a pull request
pub async fn create_pull_request(&self, request: CreatePullRequestRequest) -> Result<PullRequest> {
let url = format!(
"{}/repos/{}/{}/pulls",
self.base_url, self.owner, self.repo
);
pub async fn create_pull_request(
&self,
request: CreatePullRequestRequest,
) -> Result<PullRequest> {
let url = format!("{}/repos/{}/{}/pulls", self.base_url, self.owner, self.repo);
let response = self.client.post(&url).json(&request).send().await?;
@ -333,7 +335,11 @@ impl GitHubClient {
}
/// Merge a pull request
pub async fn merge_pull_request(&self, number: u64, commit_message: Option<&str>) -> Result<()> {
pub async fn merge_pull_request(
&self,
number: u64,
commit_message: Option<&str>,
) -> Result<()> {
let url = format!(
"{}/repos/{}/{}/pulls/{}/merge",
self.base_url, self.owner, self.repo, number
@ -403,10 +409,7 @@ impl GitHubClient {
/// Get repository information
pub async fn get_repo(&self) -> Result<serde_json::Value> {
let url = format!(
"{}/repos/{}/{}",
self.base_url, self.owner, self.repo
);
let url = format!("{}/repos/{}/{}", self.base_url, self.owner, self.repo);
let response = self.client.get(&url).send().await?;

View file

@ -270,7 +270,11 @@ impl HookManager {
let expanded_title = self.expand_variables(title, context);
let expanded_message = self.expand_variables(message, context);
// For now, just log the notification
tracing::info!("Hook notification: {} - {}", expanded_title, expanded_message);
tracing::info!(
"Hook notification: {} - {}",
expanded_title,
expanded_message
);
HookResult {
hook_name: hook.name.clone(),
success: true,

View file

@ -136,7 +136,11 @@ impl McpServer {
}
/// Send a request and get a response
pub async fn request(&mut self, method: &str, params: Option<serde_json::Value>) -> Result<McpResponse> {
pub async fn request(
&mut self,
method: &str,
params: Option<serde_json::Value>,
) -> Result<McpResponse> {
self.request_id += 1;
let request = McpRequest {
jsonrpc: "2.0".to_string(),
@ -145,9 +149,15 @@ impl McpServer {
params,
};
let stdin = self.process.stdin.as_mut()
let stdin = self
.process
.stdin
.as_mut()
.ok_or_else(|| anyhow::anyhow!("Failed to get stdin"))?;
let stdout = self.process.stdout.as_mut()
let stdout = self
.process
.stdout
.as_mut()
.ok_or_else(|| anyhow::anyhow!("Failed to get stdout"))?;
// Send request
@ -183,7 +193,9 @@ impl McpServer {
return Err(anyhow::anyhow!("MCP error: {}", error.message));
}
response.result.ok_or_else(|| anyhow::anyhow!("No result in response"))
response
.result
.ok_or_else(|| anyhow::anyhow!("No result in response"))
}
/// List available tools
@ -193,16 +205,25 @@ impl McpServer {
return Err(anyhow::anyhow!("MCP error: {}", error.message));
}
let result = response.result.ok_or_else(|| anyhow::anyhow!("No result"))?;
let result = response
.result
.ok_or_else(|| anyhow::anyhow!("No result"))?;
let tools: Vec<McpTool> = serde_json::from_value(
result.get("tools").cloned().unwrap_or(serde_json::Value::Array(vec![]))
result
.get("tools")
.cloned()
.unwrap_or(serde_json::Value::Array(vec![])),
)?;
Ok(tools)
}
/// Call a tool
pub async fn call_tool(&mut self, name: &str, arguments: serde_json::Value) -> Result<serde_json::Value> {
pub async fn call_tool(
&mut self,
name: &str,
arguments: serde_json::Value,
) -> Result<serde_json::Value> {
let params = serde_json::json!({
"name": name,
"arguments": arguments
@ -424,7 +445,10 @@ command: node
let config = McpServerConfig {
name: "filesystem".to_string(),
command: "npx".to_string(),
args: vec!["-y".to_string(), "@anthropic/mcp-server-filesystem".to_string()],
args: vec![
"-y".to_string(),
"@anthropic/mcp-server-filesystem".to_string(),
],
env: {
let mut env = HashMap::new();
env.insert("HOME".to_string(), "/tmp".to_string());
@ -616,15 +640,13 @@ command: node
#[test]
fn test_mcp_manager_is_running() {
let config = McpConfig {
servers: vec![
McpServerConfig {
name: "server1".to_string(),
command: "cmd".to_string(),
args: vec![],
env: HashMap::new(),
auto_start: false,
},
],
servers: vec![McpServerConfig {
name: "server1".to_string(),
command: "cmd".to_string(),
args: vec![],
env: HashMap::new(),
auto_start: false,
}],
};
let manager = McpManager::with_config(config);

View file

@ -36,7 +36,8 @@ impl OpenClawClient {
message: message.to_string(),
};
let response = self.client
let response = self
.client
.post(format!("{}/api/message", self.gateway_url))
.header("Authorization", format!("Bearer {}", self.token))
.json(&payload)

View file

@ -327,8 +327,8 @@ mod tests {
#[test]
fn test_orchestrator_task_with_system_prompt() {
let task = OrchestratorTask::new("task-1", "Do something")
.with_system_prompt("You are helpful");
let task =
OrchestratorTask::new("task-1", "Do something").with_system_prompt("You are helpful");
assert_eq!(task.system_prompt, Some("You are helpful".to_string()));
}

View file

@ -178,8 +178,9 @@ impl PluginManager {
pub fn unregister(&self, name: &str) -> Result<()> {
let mut plugins = self.plugins.write().unwrap();
let mut entry =
plugins.remove(name).ok_or_else(|| anyhow!("Plugin '{}' not found", name))?;
let mut entry = plugins
.remove(name)
.ok_or_else(|| anyhow!("Plugin '{}' not found", name))?;
entry.plugin.shutdown()?;
entry.state = PluginState::Shutdown;
@ -198,10 +199,16 @@ impl PluginManager {
pub fn execute(&self, name: &str, context: &PluginContext) -> Result<PluginResult> {
let plugins = self.plugins.read().unwrap();
let entry = plugins.get(name).ok_or_else(|| anyhow!("Plugin '{}' not found", name))?;
let entry = plugins
.get(name)
.ok_or_else(|| anyhow!("Plugin '{}' not found", name))?;
if entry.state != PluginState::Initialized {
return Err(anyhow!("Plugin '{}' is not initialized (state: {:?})", name, entry.state));
return Err(anyhow!(
"Plugin '{}' is not initialized (state: {:?})",
name,
entry.state
));
}
entry.plugin.execute(context)
@ -210,7 +217,10 @@ impl PluginManager {
/// Lists all registered plugins
pub fn list_plugins(&self) -> Vec<PluginMetadata> {
let plugins = self.plugins.read().unwrap();
plugins.values().map(|entry| entry.plugin.metadata()).collect()
plugins
.values()
.map(|entry| entry.plugin.metadata())
.collect()
}
/// Gets plugin metadata

View file

@ -305,10 +305,7 @@ mod tests {
async fn test_retry_returns_correct_value() {
let config = RetryConfig::new(1, 10, 100);
let result = retry_with_backoff(config, || async {
Ok::<i32, Error>(42)
})
.await;
let result = retry_with_backoff(config, || async { Ok::<i32, Error>(42) }).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), 42);

View file

@ -649,7 +649,11 @@ settings:
let loader = RulesLoader::new(temp_dir.path().to_path_buf());
// Invalid YAML
std::fs::write(temp_dir.path().join(".miyabirules"), "invalid: yaml: content:").unwrap();
std::fs::write(
temp_dir.path().join(".miyabirules"),
"invalid: yaml: content:",
)
.unwrap();
let result = loader.load();
assert!(result.is_err());

View file

@ -353,9 +353,11 @@ mod tests {
#[test]
fn test_session_with_system_prompt() {
let session = Session::new("Test")
.system_prompt("You are a helpful assistant");
assert_eq!(session.system_prompt, Some("You are a helpful assistant".to_string()));
let session = Session::new("Test").system_prompt("You are a helpful assistant");
assert_eq!(
session.system_prompt,
Some("You are a helpful assistant".to_string())
);
}
#[test]

View file

@ -500,14 +500,22 @@ impl LegacyTasksFile {
version: self.version,
generated_at: Utc::now(),
generated_from_event_id: None,
tasks: self.tasks.into_iter().map(LegacyTask::into_execution_task).collect(),
tasks: self
.tasks
.into_iter()
.map(LegacyTask::into_execution_task)
.collect(),
file_locks: HashMap::new(),
};
for task in &snapshot.tasks {
if let Some(lock) = &task.lock {
let owner_parts: Vec<&str> = lock.locked_by.split('@').collect();
let agent = owner_parts.first().copied().unwrap_or("unknown").to_string();
let agent = owner_parts
.first()
.copied()
.unwrap_or("unknown")
.to_string();
let node = owner_parts.get(1).copied().unwrap_or("unknown").to_string();
let expires_at = lease_expiry(lock.last_heartbeat, lock.lease_duration_sec);

View file

@ -238,12 +238,8 @@ fn convert_agent_event(event: crate::agent::AgentEvent) -> AgentStreamEvent {
crate::agent::AgentEvent::TokenUsage { input, output } => {
AgentStreamEvent::TokenUsage { input, output }
}
crate::agent::AgentEvent::Completed { result } => {
AgentStreamEvent::Completed { result }
}
crate::agent::AgentEvent::Failed { error } => {
AgentStreamEvent::Error { error }
}
crate::agent::AgentEvent::Completed { result } => AgentStreamEvent::Completed { result },
crate::agent::AgentEvent::Failed { error } => AgentStreamEvent::Error { error },
crate::agent::AgentEvent::ToolApproved { .. } => {
// Approval events are internal, map to Started
AgentStreamEvent::Started
@ -254,9 +250,7 @@ fn convert_agent_event(event: crate::agent::AgentEvent) -> AgentStreamEvent {
crate::agent::AgentEvent::ToolProgress { name, .. } => {
AgentStreamEvent::ToolStarted { name }
}
crate::agent::AgentEvent::TextDelta { text } => {
AgentStreamEvent::TextChunk { text }
}
crate::agent::AgentEvent::TextDelta { text } => AgentStreamEvent::TextChunk { text },
}
}
@ -353,8 +347,7 @@ mod tests {
let client = AnthropicClient::new("test-key".to_string()).unwrap();
let registry = ExecutorRegistry::with_standard_tools();
let agent = StreamingAgent::new(client, registry)
.with_system_prompt("You are helpful");
let agent = StreamingAgent::new(client, registry).with_system_prompt("You are helpful");
assert_eq!(agent.system_prompt, Some("You are helpful".to_string()));
}

View file

@ -77,7 +77,6 @@ pub enum StepCondition {
If { expression: String },
}
/// Retry configuration for a step
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetryConfig {
@ -120,7 +119,6 @@ pub enum FailurePolicy {
Cleanup,
}
/// Step execution result
#[derive(Debug, Clone)]
pub struct StepResult {
@ -238,7 +236,11 @@ impl WorkflowManager {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().map(|e| e == "yml" || e == "yaml").unwrap_or(false) {
if path
.extension()
.map(|e| e == "yml" || e == "yaml")
.unwrap_or(false)
{
if let Ok(name) = self.load_workflow(&path) {
loaded.push(name);
}
@ -264,7 +266,11 @@ impl WorkflowManager {
}
/// Execute a workflow
pub async fn execute(&self, name: &str, initial_vars: HashMap<String, String>) -> Result<WorkflowResult> {
pub async fn execute(
&self,
name: &str,
initial_vars: HashMap<String, String>,
) -> Result<WorkflowResult> {
let workflow = self
.workflows
.get(name)
@ -361,7 +367,10 @@ impl WorkflowManager {
let status = if all_succeeded {
WorkflowStatus::Completed
} else if step_results.iter().any(|r| r.status == StepStatus::Completed) {
} else if step_results
.iter()
.any(|r| r.status == StepStatus::Completed)
{
WorkflowStatus::PartiallyCompleted
} else {
WorkflowStatus::Failed
@ -453,24 +462,20 @@ impl WorkflowManager {
// Execute the task
let result = match agent.run(&expanded_task).await {
Ok(agent_result) => {
StepResult {
step_id: step.id.clone(),
status: StepStatus::Completed,
output: Some(agent_result.output),
error: None,
duration_ms: step_start.elapsed().as_millis() as u64,
}
}
Err(e) => {
StepResult {
step_id: step.id.clone(),
status: StepStatus::Failed,
output: None,
error: Some(e.to_string()),
duration_ms: step_start.elapsed().as_millis() as u64,
}
}
Ok(agent_result) => StepResult {
step_id: step.id.clone(),
status: StepStatus::Completed,
output: Some(agent_result.output),
error: None,
duration_ms: step_start.elapsed().as_millis() as u64,
},
Err(e) => StepResult {
step_id: step.id.clone(),
status: StepStatus::Failed,
output: None,
error: Some(e.to_string()),
duration_ms: step_start.elapsed().as_millis() as u64,
},
};
// Store output variable if specified
@ -495,7 +500,10 @@ impl WorkflowManager {
let status = if all_succeeded {
WorkflowStatus::Completed
} else if step_results.iter().any(|r| r.status == StepStatus::Completed) {
} else if step_results
.iter()
.any(|r| r.status == StepStatus::Completed)
{
WorkflowStatus::PartiallyCompleted
} else {
WorkflowStatus::Failed
@ -517,7 +525,13 @@ impl WorkflowManager {
for step in &workflow.steps {
if !visited.contains_key(&step.id) {
self.visit_step(workflow, &step.id, &mut visited, &mut temp_visited, &mut order)?;
self.visit_step(
workflow,
&step.id,
&mut visited,
&mut temp_visited,
&mut order,
)?;
}
}
@ -534,7 +548,10 @@ impl WorkflowManager {
order: &mut Vec<String>,
) -> Result<()> {
if temp_visited.get(step_id).copied().unwrap_or(false) {
return Err(anyhow::anyhow!("Circular dependency detected at step: {}", step_id));
return Err(anyhow::anyhow!(
"Circular dependency detected at step: {}",
step_id
));
}
if visited.get(step_id).copied().unwrap_or(false) {
@ -561,7 +578,11 @@ impl WorkflowManager {
}
/// Evaluate a step condition
fn evaluate_condition(&self, condition: &Option<StepCondition>, context: &WorkflowContext) -> bool {
fn evaluate_condition(
&self,
condition: &Option<StepCondition>,
context: &WorkflowContext,
) -> bool {
match condition {
None | Some(StepCondition::Always) => true,
Some(StepCondition::OnSuccess) => context
@ -607,19 +628,17 @@ mod tests {
let workflow = Workflow {
name: "test-workflow".to_string(),
description: "Test".to_string(),
steps: vec![
WorkflowStep {
id: "step1".to_string(),
name: "First Step".to_string(),
agent: Some("code-reviewer".to_string()),
task: "Review code".to_string(),
depends_on: vec![],
condition: None,
timeout: 300,
retry: RetryConfig::default(),
output: Some("review_result".to_string()),
},
],
steps: vec![WorkflowStep {
id: "step1".to_string(),
name: "First Step".to_string(),
agent: Some("code-reviewer".to_string()),
task: "Review code".to_string(),
depends_on: vec![],
condition: None,
timeout: 300,
retry: RetryConfig::default(),
output: Some("review_result".to_string()),
}],
variables: HashMap::new(),
on_failure: FailurePolicy::Stop,
};
@ -815,7 +834,10 @@ mod tests {
};
manager.register(workflow);
let result = manager.execute("output-test", HashMap::new()).await.unwrap();
let result = manager
.execute("output-test", HashMap::new())
.await
.unwrap();
assert_eq!(result.status, WorkflowStatus::Completed);
assert_eq!(result.steps.len(), 2);
@ -834,7 +856,10 @@ mod tests {
on_failure: FailurePolicy::Stop,
};
assert_eq!(workflow.variables.get("project"), Some(&"miyabi".to_string()));
assert_eq!(
workflow.variables.get("project"),
Some(&"miyabi".to_string())
);
}
#[test]

View file

@ -145,16 +145,15 @@ impl App {
Ok(Some(rules)) => {
let count = rules.rules.len();
if count > 0 {
view.notifications.info(
"Rules Loaded",
format!("{} project rules active", count),
);
view.notifications
.info("Rules Loaded", format!("{} project rules active", count));
}
Some(rules)
}
Ok(None) => None,
Err(e) => {
view.notifications.error("Rules Error", format!("Failed to load: {}", e));
view.notifications
.error("Rules Error", format!("Failed to load: {}", e));
None
}
};
@ -280,9 +279,10 @@ impl App {
.notifications
.error("Clipboard Error", e.to_string());
} else {
self.view
.notifications
.info("Copied", format!("{} chars", text.len()));
self.view.notifications.info(
"Copied",
format!("{} chars", text.len()),
);
}
}
Err(e) => {
@ -305,7 +305,8 @@ impl App {
#[cfg(target_os = "macos")]
let result = std::process::Command::new("open").arg(&path).spawn();
#[cfg(target_os = "linux")]
let result = std::process::Command::new("xdg-open").arg(&path).spawn();
let result =
std::process::Command::new("xdg-open").arg(&path).spawn();
#[cfg(target_os = "windows")]
let result = std::process::Command::new("cmd")
.args(["/C", "start", "", &path])
@ -316,9 +317,7 @@ impl App {
self.view.notifications.info("Opened", &path);
}
Err(e) => {
self.view
.notifications
.error("Open Error", e.to_string());
self.view.notifications.error("Open Error", e.to_string());
}
}
}

View file

@ -40,8 +40,7 @@ pub enum VimMode {
}
/// Keybinding style preference
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Default)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum KeybindingStyle {
/// Standard keybindings
#[default]
@ -52,7 +51,6 @@ pub enum KeybindingStyle {
Emacs,
}
/// Edit operation for undo/redo
#[derive(Debug, Clone)]
pub enum EditOperation {
@ -1479,12 +1477,14 @@ impl ChatComposer {
return Style::default().bg(Color::Rgb(68, 71, 90)).fg(Color::Cyan);
}
}
if line_idx == self.cursor.line && col == self.cursor.col
&& matches!(ch, '(' | ')' | '[' | ']' | '{' | '}' | '<' | '>') {
return Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD);
}
if line_idx == self.cursor.line
&& col == self.cursor.col
&& matches!(ch, '(' | ')' | '[' | ']' | '{' | '}' | '<' | '>')
{
return Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD);
}
// Basic syntax highlighting
match ch {

View file

@ -36,27 +36,25 @@ pub fn render_diff_widget_with_scroll(
// Create a DiffRender with the single file
let mut renderer = DiffRender::new();
renderer.files.push(diff.clone());
// Get rendered lines
let lines = renderer.render();
// Create block with optional title
let block = if let Some(t) = title {
Block::default()
.borders(Borders::ALL)
.title(t.to_string())
Block::default().borders(Borders::ALL).title(t.to_string())
} else {
Block::default()
.borders(Borders::ALL)
.title(format!("{}{}", diff.old_path, diff.new_path))
};
// Create paragraph with scroll
let paragraph = Paragraph::new(lines)
.block(block)
.wrap(Wrap { trim: false })
.scroll((scroll, 0));
frame.render_widget(paragraph, area);
}

View file

@ -65,10 +65,7 @@ impl HistoryList {
}
}
let list_items: Vec<ListItem> = visible_lines
.into_iter()
.map(ListItem::new)
.collect();
let list_items: Vec<ListItem> = visible_lines.into_iter().map(ListItem::new).collect();
let list = List::new(list_items).style(Style::default().fg(colors::FG));
frame.render_widget(list, inner);

View file

@ -392,9 +392,11 @@ impl MainView {
}
_ => {}
},
ActiveOverlay::Help => if self.help_viewer.handle_key(key) == HelpAction::Close {
self.close_overlay();
},
ActiveOverlay::Help => {
if self.help_viewer.handle_key(key) == HelpAction::Close {
self.close_overlay();
}
}
ActiveOverlay::Approval => match self.approval_overlay.handle_key(key) {
ApprovalAction::Approve(id) | ApprovalAction::ApproveAll(id) => {
self.close_overlay();