//! 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<Uuid>,
},
/// 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<String>,
/// Progress percentage (0-100)
progress: u8,
/// Task IDs the supervisor is waiting on
#[serde(rename = "pendingTaskIds")]
pending_task_ids: Vec<Uuid>,
/// Timestamp of this heartbeat
timestamp: DateTime<Utc>,
},
/// 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<String>,
},
/// 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<String>,
/// 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<Uuid>,
/// OAuth login URL for remote authentication.
#[serde(rename = "loginUrl")]
login_url: String,
/// Hostname of the daemon requiring auth.
hostname: Option<String>,
},
// =========================================================================
// Merge Response Messages (sent by daemon after processing merge commands)
// =========================================================================
/// Response to ListBranches command.
BranchList {
#[serde(rename = "taskId")]
task_id: Uuid,
branches: Vec<BranchInfo>,
},
/// Response to MergeStatus command.
MergeStatusResponse {
#[serde(rename = "taskId")]
task_id: Uuid,
#[serde(rename = "inProgress")]
in_progress: bool,
#[serde(rename = "sourceBranch")]
source_branch: Option<String>,
#[serde(rename = "conflictedFiles")]
conflicted_files: Vec<String>,
},
/// 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<String>,
/// Present only when conflicts occurred.
conflicts: Option<Vec<String>>,
},
/// Response to CheckMergeComplete command.
MergeCompleteCheck {
#[serde(rename = "taskId")]
task_id: Uuid,
#[serde(rename = "canComplete")]
can_complete: bool,
#[serde(rename = "unmergedBranches")]
unmerged_branches: Vec<String>,
#[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<String>,
},
/// 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<String>,
},
/// 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<String>,
/// Whether the operation succeeded.
success: bool,
/// Error message if operation failed.
error: Option<String>,
},
// =========================================================================
// 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<String>,
conflicts: Option<Vec<String>>,
},
/// Response to CreatePR command.
PRCreated {
#[serde(rename = "taskId")]
task_id: Uuid,
success: bool,
message: String,
#[serde(rename = "prUrl")]
pr_url: Option<String>,
#[serde(rename = "prNumber")]
pr_number: Option<i32>,
},
/// Response to GetTaskDiff command.
TaskDiff {
#[serde(rename = "taskId")]
task_id: Uuid,
success: bool,
diff: Option<String>,
error: Option<String>,
},
/// Response to GetWorktreeInfo command.
WorktreeInfoResult {
#[serde(rename = "taskId")]
task_id: Uuid,
success: bool,
/// Path to the worktree directory
#[serde(rename = "worktreePath")]
worktree_path: Option<String>,
/// 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<serde_json::Value>,
/// Current branch name
branch: Option<String>,
/// Current HEAD commit SHA
#[serde(rename = "headSha")]
head_sha: Option<String>,
/// Error message if failed
error: Option<String>,
},
/// Response to CreateCheckpoint command.
CheckpointCreated {
#[serde(rename = "taskId")]
task_id: Uuid,
success: bool,
/// Commit SHA if successful
#[serde(rename = "commitSha")]
commit_sha: Option<String>,
/// Branch name where checkpoint was created
#[serde(rename = "branchName")]
branch_name: Option<String>,
/// Checkpoint number in sequence (assigned by server on DB insert)
#[serde(rename = "checkpointNumber")]
checkpoint_number: Option<i32>,
/// Files changed in this checkpoint: [{path, action}]
#[serde(rename = "filesChanged")]
files_changed: Option<serde_json::Value>,
/// Lines added
#[serde(rename = "linesAdded")]
lines_added: Option<i32>,
/// Lines removed
#[serde(rename = "linesRemoved")]
lines_removed: Option<i32>,
/// Error message if failed
error: Option<String>,
/// 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<String>,
/// Commit SHA to apply patch on top of (for recovery)
#[serde(rename = "patchBaseSha", skip_serializing_if = "Option::is_none")]
patch_base_sha: Option<String>,
/// Number of files in the patch
#[serde(rename = "patchFilesCount", skip_serializing_if = "Option::is_none")]
patch_files_count: Option<i32>,
},
/// 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<String>,
/// Number of files changed.
#[serde(rename = "filesCount")]
files_count: Option<usize>,
/// Lines added.
#[serde(rename = "linesAdded")]
lines_added: Option<usize>,
/// Lines removed.
#[serde(rename = "linesRemoved")]
lines_removed: Option<usize>,
/// The base commit SHA that the patch is diffed against.
#[serde(rename = "baseCommitSha")]
base_commit_sha: Option<String>,
/// Error message if failed.
error: Option<String>,
},
/// Response to InheritGitConfig command.
GitConfigInherited {
success: bool,
/// Git user.email that was inherited
#[serde(rename = "userEmail")]
user_email: Option<String>,
/// Git user.name that was inherited
#[serde(rename = "userName")]
user_name: Option<String>,
/// Error message if failed
error: Option<String>,
},
/// 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<Uuid>,
/// 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<String>,
#[serde(rename = "baseBranch")]
base_branch: Option<String>,
/// Target branch to merge into (used for completion actions).
#[serde(rename = "targetBranch")]
target_branch: Option<String>,
/// Parent task ID if this is a subtask.
#[serde(rename = "parentTaskId")]
parent_task_id: Option<Uuid>,
/// 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<String>,
/// Action on completion: "none", "branch", "merge", "pr".
#[serde(rename = "completionAction")]
completion_action: Option<String>,
/// Task ID to continue from (copy worktree from this task).
#[serde(rename = "continueFromTaskId")]
continue_from_task_id: Option<Uuid>,
/// Files to copy from parent task's worktree.
#[serde(rename = "copyFiles")]
copy_files: Option<Vec<String>>,
/// Contract ID if this task is associated with a contract.
#[serde(rename = "contractId")]
contract_id: Option<Uuid>,
/// 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<serde_json::Value>,
/// 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<String>,
/// Commit SHA to apply the patch on top of.
#[serde(rename = "patchBaseSha", default, skip_serializing_if = "Option::is_none")]
patch_base_sha: Option<String>,
/// 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<Uuid>,
},
/// 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<String>,
#[serde(rename = "changedFiles")]
changed_files: Vec<String>,
},
// =========================================================================
// 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<String>,
},
/// 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<String>,
},
/// 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<String>,
/// 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<String>,
/// Base branch for the PR. If None, will be auto-detected from the repo.
#[serde(rename = "baseBranch")]
base_branch: Option<String>,
/// 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<String>,
},
/// 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<String>,
},
/// Error response.
Error {
code: String,
message: String,
},
/// Restart the daemon process.
RestartDaemon,
/// 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<Uuid>) -> 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<String>) -> 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<String>,
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<String>,
progress: u8,
pending_task_ids: Vec<Uuid>,
) -> 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 crate::daemon::*;
#[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","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","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"),
}
}
}