//! Protocol types for daemon-server communication. //! //! These types mirror the server's protocol exactly for compatibility. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; /// Message from daemon to server. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "camelCase")] pub enum DaemonMessage { /// Authentication request (first message required). Authenticate { #[serde(rename = "apiKey")] api_key: String, #[serde(rename = "machineId")] machine_id: String, hostname: String, #[serde(rename = "maxConcurrentTasks")] max_concurrent_tasks: i32, }, /// Periodic heartbeat with current status. Heartbeat { #[serde(rename = "activeTasks")] active_tasks: Vec, }, /// Enhanced supervisor heartbeat with detailed state. /// Sent periodically by supervisor tasks to report their current state. SupervisorHeartbeat { #[serde(rename = "taskId")] task_id: Uuid, #[serde(rename = "contractId")] contract_id: Uuid, /// Supervisor state: initializing, idle, working, waiting_for_user, waiting_for_tasks, blocked, completed, failed, interrupted state: String, /// Current contract phase phase: String, /// Description of current activity #[serde(rename = "currentActivity")] current_activity: Option, /// Progress percentage (0-100) progress: u8, /// Task IDs the supervisor is waiting on #[serde(rename = "pendingTaskIds")] pending_task_ids: Vec, /// Timestamp of this heartbeat timestamp: DateTime, }, /// Task output streaming (stdout/stderr from Claude Code). TaskOutput { #[serde(rename = "taskId")] task_id: Uuid, output: String, #[serde(rename = "isPartial")] is_partial: bool, }, /// Task status change notification. TaskStatusChange { #[serde(rename = "taskId")] task_id: Uuid, #[serde(rename = "oldStatus")] old_status: String, #[serde(rename = "newStatus")] new_status: String, }, /// Task progress update with summary. TaskProgress { #[serde(rename = "taskId")] task_id: Uuid, summary: String, }, /// Task completion notification. TaskComplete { #[serde(rename = "taskId")] task_id: Uuid, success: bool, error: Option, }, /// Task recovery detected after daemon restart. /// Sent when daemon finds orphaned tasks that can be recovered. TaskRecoveryDetected { #[serde(rename = "taskId")] task_id: Uuid, /// Previous state of the task before daemon restart. #[serde(rename = "previousState")] previous_state: String, /// Whether the worktree is still intact. #[serde(rename = "worktreeIntact")] worktree_intact: bool, /// Path to the worktree if available. #[serde(rename = "worktreePath")] worktree_path: Option, /// Whether the task needs a checkpoint patch for recovery. #[serde(rename = "needsPatch")] needs_patch: bool, }, /// Register a tool key for orchestrator API access. RegisterToolKey { #[serde(rename = "taskId")] task_id: Uuid, /// The API key for this orchestrator to use when calling mesh endpoints. key: String, }, /// Revoke a tool key when task completes. RevokeToolKey { #[serde(rename = "taskId")] task_id: Uuid, }, /// Authentication required - OAuth token expired, provides login URL. AuthenticationRequired { /// Task ID that triggered the auth error (if any). #[serde(rename = "taskId")] task_id: Option, /// OAuth login URL for remote authentication. #[serde(rename = "loginUrl")] login_url: String, /// Hostname of the daemon requiring auth. hostname: Option, }, /// Reauth status update (response to TriggerReauth/SubmitAuthCode commands). ReauthStatus { #[serde(rename = "requestId")] request_id: Uuid, /// Status: "url_ready", "completed", "failed" status: String, /// OAuth login URL (present when status is "url_ready") #[serde(rename = "loginUrl")] login_url: Option, /// Error message (present when status is "failed") error: Option, }, // ========================================================================= // Merge Response Messages (sent by daemon after processing merge commands) // ========================================================================= /// Response to ListBranches command. BranchList { #[serde(rename = "taskId")] task_id: Uuid, branches: Vec, }, /// Response to MergeStatus command. MergeStatusResponse { #[serde(rename = "taskId")] task_id: Uuid, #[serde(rename = "inProgress")] in_progress: bool, #[serde(rename = "sourceBranch")] source_branch: Option, #[serde(rename = "conflictedFiles")] conflicted_files: Vec, }, /// Response to merge operations (MergeStart, MergeResolve, MergeCommit, MergeAbort). MergeResult { #[serde(rename = "taskId")] task_id: Uuid, success: bool, message: String, #[serde(rename = "commitSha")] commit_sha: Option, /// Present only when conflicts occurred. conflicts: Option>, }, /// Response to CheckMergeComplete command. MergeCompleteCheck { #[serde(rename = "taskId")] task_id: Uuid, #[serde(rename = "canComplete")] can_complete: bool, #[serde(rename = "unmergedBranches")] unmerged_branches: Vec, #[serde(rename = "mergedCount")] merged_count: u32, #[serde(rename = "skippedCount")] skipped_count: u32, }, // ========================================================================= // Completion Action Response Messages // ========================================================================= /// Response to RetryCompletionAction command. CompletionActionResult { #[serde(rename = "taskId")] task_id: Uuid, success: bool, message: String, /// PR URL if action was "pr" and successful. #[serde(rename = "prUrl")] pr_url: Option, }, /// Report daemon's available directories for task output. DaemonDirectories { /// Current working directory of the daemon. #[serde(rename = "workingDirectory")] working_directory: String, /// Path to ~/.makima/home directory (for cloning completed work). #[serde(rename = "homeDirectory")] home_directory: String, /// Path to worktrees directory (~/.makima/worktrees). #[serde(rename = "worktreesDirectory")] worktrees_directory: String, }, /// Response to CloneWorktree command. CloneWorktreeResult { #[serde(rename = "taskId")] task_id: Uuid, success: bool, message: String, /// The path where the worktree was cloned. #[serde(rename = "targetDir")] target_dir: Option, }, /// Response to CheckTargetExists command. CheckTargetExistsResult { #[serde(rename = "taskId")] task_id: Uuid, /// Whether the target directory exists. exists: bool, /// The path that was checked. #[serde(rename = "targetDir")] target_dir: String, }, // ========================================================================= // Contract File Response Messages // ========================================================================= /// Response to ReadRepoFile command. RepoFileContent { /// Request ID from the original command. #[serde(rename = "requestId")] request_id: Uuid, /// Path to the file that was read. #[serde(rename = "filePath")] file_path: String, /// File content (None if error occurred). content: Option, /// Whether the operation succeeded. success: bool, /// Error message if operation failed. error: Option, }, // ========================================================================= // Supervisor Git Response Messages // ========================================================================= /// Response to CreateBranch command. BranchCreated { #[serde(rename = "taskId")] task_id: Uuid, success: bool, #[serde(rename = "branchName")] branch_name: String, message: String, }, /// Response to MergeTaskToTarget command. MergeToTargetResult { #[serde(rename = "taskId")] task_id: Uuid, success: bool, message: String, #[serde(rename = "commitSha")] commit_sha: Option, conflicts: Option>, }, /// Response to CreatePR command. PRCreated { #[serde(rename = "taskId")] task_id: Uuid, success: bool, message: String, #[serde(rename = "prUrl")] pr_url: Option, #[serde(rename = "prNumber")] pr_number: Option, }, /// Response to GetTaskDiff command. TaskDiff { #[serde(rename = "taskId")] task_id: Uuid, success: bool, diff: Option, error: Option, }, /// Response to GetWorktreeInfo command. WorktreeInfoResult { #[serde(rename = "taskId")] task_id: Uuid, success: bool, /// Path to the worktree directory #[serde(rename = "worktreePath")] worktree_path: Option, /// Whether the worktree exists exists: bool, /// Number of files changed #[serde(rename = "filesChanged")] files_changed: i32, /// Total lines inserted insertions: i32, /// Total lines deleted deletions: i32, /// Changed files list: [{path, status, linesAdded, linesRemoved}] files: Option, /// Current branch name branch: Option, /// Current HEAD commit SHA #[serde(rename = "headSha")] head_sha: Option, /// Error message if failed error: Option, }, /// Response to CreateCheckpoint command. CheckpointCreated { #[serde(rename = "taskId")] task_id: Uuid, success: bool, /// Commit SHA if successful #[serde(rename = "commitSha")] commit_sha: Option, /// Branch name where checkpoint was created #[serde(rename = "branchName")] branch_name: Option, /// Checkpoint number in sequence (assigned by server on DB insert) #[serde(rename = "checkpointNumber")] checkpoint_number: Option, /// Files changed in this checkpoint: [{path, action}] #[serde(rename = "filesChanged")] files_changed: Option, /// Lines added #[serde(rename = "linesAdded")] lines_added: Option, /// Lines removed #[serde(rename = "linesRemoved")] lines_removed: Option, /// Error message if failed error: Option, /// User-provided checkpoint message message: String, /// Base64-encoded gzip-compressed patch data for recovery #[serde(rename = "patchData", skip_serializing_if = "Option::is_none")] patch_data: Option, /// Commit SHA to apply patch on top of (for recovery) #[serde(rename = "patchBaseSha", skip_serializing_if = "Option::is_none")] patch_base_sha: Option, /// Number of files in the patch #[serde(rename = "patchFilesCount", skip_serializing_if = "Option::is_none")] patch_files_count: Option, }, /// Response to CleanupWorktree command. CleanupWorktreeResult { #[serde(rename = "taskId")] task_id: Uuid, success: bool, message: String, }, /// Response to CreateExportPatch command. ExportPatchCreated { #[serde(rename = "taskId")] task_id: Uuid, success: bool, /// The uncompressed, human-readable patch content. #[serde(rename = "patchContent")] patch_content: Option, /// Number of files changed. #[serde(rename = "filesCount")] files_count: Option, /// Lines added. #[serde(rename = "linesAdded")] lines_added: Option, /// Lines removed. #[serde(rename = "linesRemoved")] lines_removed: Option, /// The base commit SHA that the patch is diffed against. #[serde(rename = "baseCommitSha")] base_commit_sha: Option, /// Error message if failed. error: Option, }, /// Response to InheritGitConfig command. GitConfigInherited { success: bool, /// Git user.email that was inherited #[serde(rename = "userEmail")] user_email: Option, /// Git user.name that was inherited #[serde(rename = "userName")] user_name: Option, /// Error message if failed error: Option, }, /// Request to merge a task's patch to supervisor's worktree (cross-daemon case). /// Sent when a task completes on a different daemon than its supervisor. MergePatchToSupervisor { /// The task that completed. #[serde(rename = "taskId")] task_id: Uuid, /// The supervisor task to merge into. #[serde(rename = "supervisorTaskId")] supervisor_task_id: Uuid, /// Base64-gzipped patch data. #[serde(rename = "patchData")] patch_data: String, /// Base commit SHA for the patch. #[serde(rename = "baseSha")] base_sha: String, }, } /// Information about a branch (used in BranchList message). #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BranchInfo { /// Full branch name. pub name: String, /// Task ID extracted from branch name (if parseable). #[serde(rename = "taskId")] pub task_id: Option, /// Whether this branch has been merged. #[serde(rename = "isMerged")] pub is_merged: bool, /// Short SHA of the last commit. #[serde(rename = "lastCommit")] pub last_commit: String, /// Subject line of the last commit. #[serde(rename = "lastCommitMessage")] pub last_commit_message: String, } /// Command from server to daemon. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "camelCase")] pub enum DaemonCommand { /// Confirm successful authentication. Authenticated { #[serde(rename = "daemonId")] daemon_id: Uuid, }, /// Spawn a new task in a container. SpawnTask { #[serde(rename = "taskId")] task_id: Uuid, /// Human-readable task name (used for commit messages). #[serde(rename = "taskName")] task_name: String, plan: String, #[serde(rename = "repoUrl")] repo_url: Option, #[serde(rename = "baseBranch")] base_branch: Option, /// Target branch to merge into (used for completion actions). #[serde(rename = "targetBranch")] target_branch: Option, /// Parent task ID if this is a subtask. #[serde(rename = "parentTaskId")] parent_task_id: Option, /// Depth in task hierarchy (0=top-level, 1=subtask, 2=sub-subtask). depth: i32, /// Whether this task should run as an orchestrator (true if depth==0 and has subtasks). #[serde(rename = "isOrchestrator")] is_orchestrator: bool, /// Path to user's local repository (outside ~/.makima) for completion actions. #[serde(rename = "targetRepoPath")] target_repo_path: Option, /// Action on completion: "none", "branch", "merge", "pr". #[serde(rename = "completionAction")] completion_action: Option, /// Task ID to continue from (copy worktree from this task). #[serde(rename = "continueFromTaskId")] continue_from_task_id: Option, /// Files to copy from parent task's worktree. #[serde(rename = "copyFiles")] copy_files: Option>, /// Contract ID if this task is associated with a contract. #[serde(rename = "contractId")] contract_id: Option, /// Whether this task is a supervisor (long-running contract orchestrator). #[serde(rename = "isSupervisor", default)] is_supervisor: bool, /// Whether to run in autonomous loop mode. /// When enabled, task will automatically restart with --continue if it exits /// without a COMPLETION_GATE indicating ready: true. #[serde(rename = "autonomousLoop", default)] autonomous_loop: bool, /// Whether to resume from a previous session using --continue flag. /// When enabled, the daemon will reuse the existing worktree and call /// Claude with --continue to maintain conversation history. #[serde(rename = "resumeSession", default)] resume_session: bool, /// Conversation history for fallback when worktree doesn't exist. /// Used to inject previous conversation context into the prompt. #[serde(rename = "conversationHistory", default)] conversation_history: Option, /// Base64-encoded gzip-compressed patch for worktree recovery. /// Used when resume_session=true and the local worktree is missing. #[serde(rename = "patchData", default, skip_serializing_if = "Option::is_none")] patch_data: Option, /// Commit SHA to apply the patch on top of. #[serde(rename = "patchBaseSha", default, skip_serializing_if = "Option::is_none")] patch_base_sha: Option, /// Whether the contract is in local-only mode (skips automatic completion actions). #[serde(rename = "localOnly", default)] local_only: bool, /// Whether to auto-merge to target branch locally when local_only mode is enabled. #[serde(rename = "autoMergeLocal", default)] auto_merge_local: bool, /// Task ID to share worktree with (supervisor's task ID). If Some, use that task's worktree instead of creating a new one. #[serde(rename = "supervisorWorktreeTaskId", default, skip_serializing_if = "Option::is_none")] supervisor_worktree_task_id: Option, /// Directive ID if this task is associated with a directive. #[serde(rename = "directiveId", default, skip_serializing_if = "Option::is_none")] directive_id: Option, }, /// Pause a running task. PauseTask { #[serde(rename = "taskId")] task_id: Uuid, }, /// Resume a paused task. ResumeTask { #[serde(rename = "taskId")] task_id: Uuid, }, /// Interrupt a task (gracefully or forced). InterruptTask { #[serde(rename = "taskId")] task_id: Uuid, graceful: bool, }, /// Send a message to a running task. SendMessage { #[serde(rename = "taskId")] task_id: Uuid, message: String, }, /// Inject context about sibling task progress. InjectSiblingContext { #[serde(rename = "taskId")] task_id: Uuid, #[serde(rename = "siblingTaskId")] sibling_task_id: Uuid, #[serde(rename = "siblingName")] sibling_name: String, #[serde(rename = "siblingStatus")] sibling_status: String, #[serde(rename = "progressSummary")] progress_summary: Option, #[serde(rename = "changedFiles")] changed_files: Vec, }, // ========================================================================= // Merge Commands (for orchestrators to merge subtask branches) // ========================================================================= /// List all subtask branches for a task. ListBranches { #[serde(rename = "taskId")] task_id: Uuid, }, /// Start merging a subtask branch. MergeStart { #[serde(rename = "taskId")] task_id: Uuid, #[serde(rename = "sourceBranch")] source_branch: String, }, /// Get current merge status. MergeStatus { #[serde(rename = "taskId")] task_id: Uuid, }, /// Resolve a merge conflict. MergeResolve { #[serde(rename = "taskId")] task_id: Uuid, file: String, /// "ours" or "theirs" strategy: String, }, /// Commit the current merge. MergeCommit { #[serde(rename = "taskId")] task_id: Uuid, message: String, }, /// Abort the current merge. MergeAbort { #[serde(rename = "taskId")] task_id: Uuid, }, /// Skip merging a subtask branch (mark as intentionally not merged). MergeSkip { #[serde(rename = "taskId")] task_id: Uuid, #[serde(rename = "subtaskId")] subtask_id: Uuid, reason: String, }, /// Check if all subtask branches have been merged or skipped (completion gate). CheckMergeComplete { #[serde(rename = "taskId")] task_id: Uuid, }, // ========================================================================= // Completion Action Commands // ========================================================================= /// Retry a completion action for a completed task. RetryCompletionAction { #[serde(rename = "taskId")] task_id: Uuid, /// Human-readable task name (used for commit messages). #[serde(rename = "taskName")] task_name: String, /// The action to execute: "branch", "merge", or "pr". action: String, /// Path to the target repository. #[serde(rename = "targetRepoPath")] target_repo_path: String, /// Target branch to merge into (for merge/pr actions). #[serde(rename = "targetBranch")] target_branch: Option, }, /// Clone worktree to a target directory. CloneWorktree { #[serde(rename = "taskId")] task_id: Uuid, /// Path to the target directory. #[serde(rename = "targetDir")] target_dir: String, }, /// Check if a target directory exists. CheckTargetExists { #[serde(rename = "taskId")] task_id: Uuid, /// Path to check. #[serde(rename = "targetDir")] target_dir: String, }, // ========================================================================= // Contract File Commands // ========================================================================= /// Read a file from a repository linked to a contract. ReadRepoFile { /// Request ID for correlating response. #[serde(rename = "requestId")] request_id: Uuid, /// Contract ID (used for logging/context). #[serde(rename = "contractId")] contract_id: Uuid, /// Path to the file within the repository. #[serde(rename = "filePath")] file_path: String, /// Full repository path on daemon's filesystem. #[serde(rename = "repoPath")] repo_path: String, }, // ========================================================================= // Supervisor Git Commands // ========================================================================= /// Create a new branch in the supervisor's worktree. CreateBranch { #[serde(rename = "taskId")] task_id: Uuid, #[serde(rename = "branchName")] branch_name: String, /// Optional reference to create branch from (task_id or SHA). #[serde(rename = "fromRef")] from_ref: Option, }, /// Merge a task's changes to a target branch. MergeTaskToTarget { #[serde(rename = "taskId")] task_id: Uuid, /// Target branch to merge into (default: task's base branch). #[serde(rename = "targetBranch")] target_branch: Option, /// Whether to squash commits. squash: bool, }, /// Create a pull request for a task's changes. CreatePR { #[serde(rename = "taskId")] task_id: Uuid, title: String, body: Option, /// Base branch for the PR. If None, will be auto-detected from the repo. #[serde(rename = "baseBranch")] base_branch: Option, /// Source branch name to push and create PR from. branch: String, }, /// Get the diff for a task's changes. GetTaskDiff { #[serde(rename = "taskId")] task_id: Uuid, }, /// Get worktree information (files, stats, branch) for a task. GetWorktreeInfo { #[serde(rename = "taskId")] task_id: Uuid, }, /// Create a checkpoint (stage changes, commit, get stats). CreateCheckpoint { #[serde(rename = "taskId")] task_id: Uuid, /// Commit message for the checkpoint. message: String, }, /// Clean up a task's worktree (used when contract is completed/deleted). CleanupWorktree { #[serde(rename = "taskId")] task_id: Uuid, /// Whether to delete the associated branch. #[serde(rename = "deleteBranch")] delete_branch: bool, }, /// Create an uncompressed git patch for export. /// Returns a human-readable patch that can be applied manually or shared. CreateExportPatch { #[serde(rename = "taskId")] task_id: Uuid, /// Optional base SHA to diff against. If not provided, will try to find /// the merge-base with the default branch. #[serde(rename = "baseSha")] base_sha: Option, }, /// Inherit git config (user.email, user.name) from a directory. /// This config will be applied to all future worktrees. InheritGitConfig { /// Directory to read git config from (defaults to daemon's working directory). #[serde(rename = "sourceDir")] source_dir: Option, }, /// Error response. Error { code: String, message: String, }, /// Restart the daemon process. RestartDaemon, /// Trigger OAuth re-authentication on this daemon. TriggerReauth { #[serde(rename = "requestId")] request_id: Uuid, }, /// Submit auth code for pending reauth. SubmitAuthCode { #[serde(rename = "requestId")] request_id: Uuid, code: String, }, /// Apply a patch to a task's worktree (for cross-daemon merge). /// Sent by server when routing MergePatchToSupervisor to the supervisor's daemon. ApplyPatchToWorktree { /// Target task whose worktree should be patched. #[serde(rename = "targetTaskId")] target_task_id: Uuid, /// Source task that generated the patch (for logging). #[serde(rename = "sourceTaskId")] source_task_id: Uuid, /// Base64-gzipped patch data. #[serde(rename = "patchData")] patch_data: String, /// Base commit SHA for the patch. #[serde(rename = "baseSha")] base_sha: String, }, } impl DaemonMessage { /// Create an authentication message. pub fn authenticate( api_key: &str, machine_id: &str, hostname: &str, max_concurrent_tasks: i32, ) -> Self { Self::Authenticate { api_key: api_key.to_string(), machine_id: machine_id.to_string(), hostname: hostname.to_string(), max_concurrent_tasks, } } /// Create a heartbeat message. pub fn heartbeat(active_tasks: Vec) -> Self { Self::Heartbeat { active_tasks } } /// Create a task output message. pub fn task_output(task_id: Uuid, output: String, is_partial: bool) -> Self { Self::TaskOutput { task_id, output, is_partial, } } /// Create a task status change message. pub fn task_status_change(task_id: Uuid, old_status: &str, new_status: &str) -> Self { Self::TaskStatusChange { task_id, old_status: old_status.to_string(), new_status: new_status.to_string(), } } /// Create a task progress message. pub fn task_progress(task_id: Uuid, summary: String) -> Self { Self::TaskProgress { task_id, summary } } /// Create a task complete message. pub fn task_complete(task_id: Uuid, success: bool, error: Option) -> Self { Self::TaskComplete { task_id, success, error, } } /// Create a task recovery detected message. pub fn task_recovery_detected( task_id: Uuid, previous_state: &str, worktree_intact: bool, worktree_path: Option, needs_patch: bool, ) -> Self { Self::TaskRecoveryDetected { task_id, previous_state: previous_state.to_string(), worktree_intact, worktree_path, needs_patch, } } /// Create a register tool key message. pub fn register_tool_key(task_id: Uuid, key: String) -> Self { Self::RegisterToolKey { task_id, key } } /// Create a revoke tool key message. pub fn revoke_tool_key(task_id: Uuid) -> Self { Self::RevokeToolKey { task_id } } /// Create a supervisor heartbeat message. pub fn supervisor_heartbeat( task_id: Uuid, contract_id: Uuid, state: &str, phase: &str, current_activity: Option, progress: u8, pending_task_ids: Vec, ) -> Self { Self::SupervisorHeartbeat { task_id, contract_id, state: state.to_string(), phase: phase.to_string(), current_activity, progress, pending_task_ids, timestamp: Utc::now(), } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_daemon_message_serialization() { let msg = DaemonMessage::authenticate("key123", "machine-abc", "worker-1", 4); let json = serde_json::to_string(&msg).unwrap(); assert!(json.contains("\"type\":\"authenticate\"")); assert!(json.contains("\"apiKey\":\"key123\"")); assert!(json.contains("\"machineId\":\"machine-abc\"")); } #[test] fn test_daemon_command_deserialization() { let json = r#"{"type":"spawnTask","taskId":"550e8400-e29b-41d4-a716-446655440000","taskName":"Build Feature","plan":"Build the feature","repoUrl":"https://github.com/test/repo","baseBranch":"main","parentTaskId":null,"depth":0,"isOrchestrator":false}"#; let cmd: DaemonCommand = serde_json::from_str(json).unwrap(); match cmd { DaemonCommand::SpawnTask { plan, repo_url, base_branch, parent_task_id, depth, is_orchestrator, .. } => { assert_eq!(plan, "Build the feature"); assert_eq!(repo_url, Some("https://github.com/test/repo".to_string())); assert_eq!(base_branch, Some("main".to_string())); assert_eq!(parent_task_id, None); assert_eq!(depth, 0); assert!(!is_orchestrator); } _ => panic!("Expected SpawnTask"), } } #[test] fn test_orchestrator_spawn_deserialization() { let json = r#"{"type":"spawnTask","taskId":"550e8400-e29b-41d4-a716-446655440000","taskName":"Coordinate","plan":"Coordinate subtasks","repoUrl":"https://github.com/test/repo","baseBranch":"main","parentTaskId":null,"depth":0,"isOrchestrator":true}"#; let cmd: DaemonCommand = serde_json::from_str(json).unwrap(); match cmd { DaemonCommand::SpawnTask { is_orchestrator, depth, .. } => { assert!(is_orchestrator); assert_eq!(depth, 0); } _ => panic!("Expected SpawnTask"), } } }