[文書] 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!(
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,12 +29,10 @@ 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(|| {
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()
"Repository is bare (no working directory). Miyabi requires a non-bare repository."
.to_string(),
)
}),
Err(e) => Err(Error::Git(format!(

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 {
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 {
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 {
},
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,8 +628,7 @@ mod tests {
let workflow = Workflow {
name: "test-workflow".to_string(),
description: "Test".to_string(),
steps: vec![
WorkflowStep {
steps: vec![WorkflowStep {
id: "step1".to_string(),
name: "First Step".to_string(),
agent: Some("code-reviewer".to_string()),
@ -618,8 +638,7 @@ mod tests {
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,8 +1477,10 @@ 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, '(' | ')' | '[' | ']' | '{' | '}' | '<' | '>') {
if line_idx == self.cursor.line
&& col == self.cursor.col
&& matches!(ch, '(' | ')' | '[' | ']' | '{' | '}' | '<' | '>')
{
return Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD);

View file

@ -42,9 +42,7 @@ pub fn render_diff_widget_with_scroll(
// 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)

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 {
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();

View file

@ -0,0 +1,190 @@
# DTP Playbook v4 — ビジョン完全準拠版
_v4: 原則 1記憶+ Bus ドッキング + ブランチ戦略を統合。全 38 要素をカバー。_
---
## North Star
> 記憶はアタッチメント。ジグで手順を強制。GitHub で事実を確定。
---
## 完了済み
| 内容 | コミット | 行数 |
|------|---------|------|
| Phase A: gate + lock + store + protocol | 986d907 | +1,485 |
| Phase B: CLI サブコマンド | 273c416 | +1,171 |
| clippy 修正 | 437b959 | 0 (既に反映済み) |
---
## Sprint 1: GATE 修正 + Phase C今日
> ビジョン対応: 原則 2 (ジグ) を 58% → 90% に
| # | Issue | タスク | 行数 |
|---|-------|--------|------|
| 1 | #52 | GATE 0: issue=0 拒否 | ~5 |
| 2 | #53 | GATE 5: ブランチ名バリデーション | ~10 |
| 3 | #54 | GATE 3: HIGH risk 承認チェック | ~20 |
| 4 | #55 | GATE 7: merge 後ロック自動解放 + 後続タスク解放 | ~15 |
| 5 | #56 | exit code: GATE 拒否=1, 入力エラー=2 | ~10 |
| 6 | #57 | 依存ブロック assign → exit 1 | ~10 |
| 7 | — | verify_merge: GitHub API で PR merged を検証 | ~50 |
| 8 | — | escape hatch: force_unlock + manual_complete | ~50 |
| 9 | — | E2E テスト: register → done 全シーケンス | ~100 |
**GATE**: cargo test GREEN + clippy ZERO + E2E テスト GREEN
**タグ**: `v1.0-dtp-complete`
---
## Sprint 2: Bus ドッキング + ブランチ戦略(今週)
> ビジョン対応: 原則 3 (SSOT) を 44% → 80% に
| # | Issue | タスク | 行数 |
|---|-------|--------|------|
| 1 | #61 | Bus ドッキング: register → auto enqueue | ~30 |
| 2 | #61 | Bus ドッキング: merge → auto complete | ~20 |
| 3 | #65 | Bus データパス統合 (HAYASHI_SHUNSUKE → 参照) | ~10 |
| 4 | #64 | 並列 Codex ブランチ戦略: assign 時に自動 branch + worktree | ~50 |
| 5 | — | JSON 出力スキーマ統一 | ~30 |
| 6 | — | OpenClaw hooks 連携 | ~50 |
| 7 | — | OpenClaw main 呼び出しテスト | テスト |
| 8 | — | tasks.json memory sync | ~30 |
**GATE**: Bus enqueue/complete 動作 + OpenClaw main が register→merge 完走
**タグ**: `v1.1-bus-docked`
---
## Sprint 3: 記憶アタッチメント + ドリーミング(来週)
> ビジョン対応: 原則 1 (記憶) を 0% → 70% に ← **核心**
| # | Issue | タスク | 行数 |
|---|-------|--------|------|
| 1 | #58 | attach_context: Issue 本文取得 | ~20 |
| 2 | #58 | attach_context: GNI impact 結果をアタッチ | ~20 |
| 3 | #58 | attach_context: ロック対象ファイルの先頭 50 行抜粋 | ~20 |
| 4 | #58 | attach_context: トークン閾値で切り詰め | ~20 |
| 5 | #58 | tasks.json に context_attachments フィールド追加 | ~15 |
| 6 | #58 | CLI: assign 時に attach_context 自動実行 | ~10 |
| 7 | #58 | CLI: --format json で attachments を出力 | ~10 |
| 8 | #59 | dream: event log replay | ~30 |
| 9 | #59 | dream: パターン抽出 (GATE 拒否頻度、ロック競合) | ~40 |
| 10 | #59 | dream: 学び重要度判定 (High/Medium/Low) | ~20 |
| 11 | #59 | dream: High → docs/ にコミット | ~20 |
| 12 | #59 | dream: Medium → Issue コメントに追記 | ~15 |
| 13 | #59 | dream: Low → アーカイブ | ~10 |
| 14 | #59 | CLI: miyabi gate dream [--since 24h] [--auto] | ~20 |
| 15 | #59 | launchd: 毎晩 03:00 に dream --auto 実行 | plist |
| 16 | #63 | Web ダッシュボード: miyabi gate serve (localhost:4848) | ~100 |
| 17 | — | Heartbeat デーモン (launchd 60秒) | ~50 |
| 18 | — | Telegram 通知 (GATE 通過/拒否) | ~50 |
| 19 | — | VOICEBOX 自動化 (hooks.yaml) | ~20 |
| 20 | — | git 自動同期 (merge 後に auto push) | ~30 |
| 21 | — | Maestro Playbook 登録 | 設定 |
**GATE**: attach_context が動作 + dream が docs/ に昇格 + Web ダッシュボード表示
**タグ**: `v1.2-memory-attached`
---
## Sprint 4: シータサイクル + Obsidian + 品質ゲート(今月)
> ビジョン対応: 原則 1 を 70% → 100% + 自己改善 0% → 100%
| # | Issue | タスク | 行数 |
|---|-------|--------|------|
| 1 | #60 | シータ θ1: dream に Knowledge Watcher 差分検知を統合 | ~30 |
| 2 | #60 | シータ θ5: skill-bus record-run を dream 内で自動呼出 | ~10 |
| 3 | #60 | シータ θ6: Self-Improving → SKILL.md 自動更新 | ~30 |
| 4 | #60 | シータ θ6: attach_context の選別ルール自動改善 | ~20 |
| 5 | #62 | Obsidian: dream → Vault ノート生成 | ~50 |
| 6 | #62 | Obsidian: wikilink 自動生成 | ~30 |
| 7 | #62 | Obsidian: MOC 自動更新 | ~20 |
| 8 | #62 | Obsidian: Smart Connections 埋め込み再生成トリガー | ~10 |
| 9 | #62 | attach_context: Smart Connections セマンティック検索追加 | ~30 |
| 10 | #62 | attach_context: wikilink 追跡で関連ノート展開 | ~20 |
| 11 | — | rust-ai-pipeline Phase 1 統合 (GATE 4.5) | ~50 |
| 12 | — | proptest 拡張 (gate/lock/store) | ~100 |
| 13 | — | cargo-mutants (mutation score 80%+) | 設定 |
**GATE**: シータが自律巡回 + Obsidian にノート生成 + mutation 80%+
**タグ**: `v1.3-theta-obsidian`
---
## Sprint 5: 移行 + 公開(来月以降)
| # | タスク |
|---|--------|
| 1 | miyabi-private (TS) → Rust 段階的移行 |
| 2 | OpenClaw プラグインとして公開 |
| 3 | npm パッケージ配布 |
**タグ**: `v2.0-public`
---
## Sprint DAG
```
Sprint 1 (#52-57 + Phase C) ← 今日。GATE を固める
Sprint 2 (#61,64,65) ← 今週。Bus接続 + ブランチ戦略
Sprint 3 (#58,59,63) ← 来週。★記憶アタッチメント + ドリーミング★
Sprint 4 (#60,62) ← 今月。シータ + Obsidian + 品質ゲート
Sprint 5 ← 来月。公開
```
---
## ビジョン達成度の推移
```
原則1 原則2 原則3 可視化 自己改善 全体
現在: 0% 58% 44% 13% 0% 32%
Sprint1後: 0% 90% 50% 13% 0% 38%
Sprint2後: 0% 95% 80% 13% 0% 47%
Sprint3後: 70% 95% 85% 60% 10% 72%
Sprint4後: 100% 100% 90% 70% 100% 93%
Sprint5後: 100% 100% 100% 80% 100% 97%
```
---
## 全 Issue → Sprint マッピング
| Issue | Sprint | 原則 |
|-------|--------|------|
| #52 GATE 0 Issue=0 | 1 | 原則2 |
| #53 GATE 5 ブランチ名 | 1 | 原則2 |
| #54 GATE 3 HIGH承認 | 1 | 原則2 |
| #55 GATE 7 ロック解放 | 1 | 原則2 |
| #56 exit code | 1 | 原則2 |
| #57 依存 exit code | 1 | 原則2 |
| #58 記憶アタッチメント | 3 | **原則1** |
| #59 ドリーミング | 3 | **原則1** |
| #60 シータサイクル | 4 | 自己改善 |
| #61 Bus ドッキング | 2 | 原則3 |
| #62 Obsidian 連携 | 4 | **原則1** |
| #63 Web ダッシュボード | 3 | 可視化 |
| #64 ブランチ戦略 | 2 | 原則2 |
| #65 Bus データパス | 2 | 原則3 |
---
_This is Playbook v4. It covers all 38 elements of the VISION._
_Previous versions (v1-v3) are archived in docs/dtp/._

View file

@ -2,3 +2,6 @@
{"ts":"2026-04-09T21:17:09.658Z","agent":"codex","skill":"context-and-impact","task":"DTP Phase B: miyabi gate CLI サブコマンド追加、protocol API 拡張、cargo test とコミット完了","result":"success","score":0.96,"notes":""}
{"ts":"2026-04-09T21:27:55.420Z","agent":"codex","skill":"context-and-impact","task":"miyabi-core の clippy 7件を解消し clippy/test を通過確認。ただし HEAD 差分ゼロのため指定コミットは未作成","result":"partial","score":0.78,"notes":""}
{"ts":"2026-04-09T21:28:53.022Z","agent":"codex","skill":"context-and-impact","task":"clippy 7件解消の確認後、空コミットで指定メッセージを作成","result":"success","score":0.92,"notes":""}
{"ts":"2026-04-09T21:49:02.984Z","agent":"codex","skill":"rust-development","task":"Fix GitHub Issues #56 and #57: gate exit code mapping and dependency-blocked assign behavior","result":"success","score":0.97,"notes":""}
{"ts":"2026-04-09T21:50:24.425Z","agent":"codex","skill":"rust-llm-pitfalls","task":"Fix GitHub Issues #54 and #55 in protocol.rs with approval and merge lock handling","result":"success","score":1,"notes":""}
{"ts":"2026-04-09T21:52:05.485Z","agent":"codex","skill":"rust-llm-pitfalls","task":"Issue #52/#53 修正: issue=0 拒否とブランチ名バリデーション実装","result":"success","score":0.95,"notes":""}