gestura_core_tasks/
tasks.rs

1//! Task management system for tracking agent workflows
2//!
3//! This module provides a task management system that integrates with the agent loop
4//! to track complex workflows, subtasks, and progress throughout conversation sessions.
5//!
6//! # Architecture
7//!
8//! ```text
9//! .gestura/tasks/
10//! ├── {session_id_1}.json    # Tasks for session 1
11//! ├── {session_id_2}.json    # Tasks for session 2
12//! └── ...
13//! ```
14//!
15//! # Usage
16//!
17//! ```rust,ignore
18//! use gestura_core::tasks::{TaskManager, Task, TaskStatus};
19//!
20//! let manager = TaskManager::new("/path/to/workspace");
21//! let task = manager.create_task("session-123", "Implement feature", "Add new API endpoint", None)?;
22//! manager.update_task_status("session-123", &task.id, TaskStatus::InProgress)?;
23//! ```
24
25use chrono::{DateTime, Utc};
26use serde::{Deserialize, Serialize};
27use std::collections::{HashMap, HashSet};
28use std::fs;
29use std::path::PathBuf;
30use uuid::Uuid;
31
32const GESTURA_HOME_DIR_ENV: &str = "GESTURA_HOME_DIR";
33
34fn gestura_home_dir() -> PathBuf {
35    std::env::var_os(GESTURA_HOME_DIR_ENV)
36        .map(PathBuf::from)
37        .or_else(dirs::home_dir)
38        .unwrap_or_else(|| PathBuf::from("."))
39}
40
41/// Status of a task
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43pub enum TaskStatus {
44    /// Task has not been started
45    NotStarted,
46    /// Task is blocked on dependencies, approvals, or other coordination gates
47    Blocked,
48    /// Task is currently in progress
49    InProgress,
50    /// Task has been completed
51    Completed,
52    /// Task has been cancelled
53    Cancelled,
54}
55
56impl std::fmt::Display for TaskStatus {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        match self {
59            Self::NotStarted => write!(f, "not_started"),
60            Self::Blocked => write!(f, "blocked"),
61            Self::InProgress => write!(f, "in_progress"),
62            Self::Completed => write!(f, "completed"),
63            Self::Cancelled => write!(f, "cancelled"),
64        }
65    }
66}
67
68impl std::str::FromStr for TaskStatus {
69    type Err = String;
70
71    /// Parse a task status (case-insensitive, accepts common aliases).
72    fn from_str(s: &str) -> Result<Self, Self::Err> {
73        let norm = s.trim().to_ascii_lowercase().replace('-', "_");
74        match norm.as_str() {
75            "not_started" | "todo" | "new" => Ok(Self::NotStarted),
76            "blocked" | "waiting" | "pending" => Ok(Self::Blocked),
77            "in_progress" | "doing" | "wip" => Ok(Self::InProgress),
78            "completed" | "done" => Ok(Self::Completed),
79            "cancelled" | "canceled" | "dropped" => Ok(Self::Cancelled),
80            _ => Err(format!(
81                "Unknown task status: '{}'. Expected: not_started, blocked, in_progress, completed, cancelled",
82                s
83            )),
84        }
85    }
86}
87
88/// Source of a task (who created it)
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
90pub enum TaskSource {
91    /// Created manually by user via the task panel UI
92    #[default]
93    User,
94    /// Created automatically by the agent during processing
95    Agent,
96    /// Created by the orchestrator for workflow delegation
97    Orchestrator,
98}
99
100/// Background execution status for tasks that represent long-running work.
101///
102/// This is primarily intended for UI dashboards to reflect delegated work
103/// (e.g. orchestrator / agent tasks) that may run asynchronously.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
105pub enum TaskBackgroundStatus {
106    /// The background job has been queued but has not started.
107    Queued,
108    /// The background job is blocked on another dependency or review gate.
109    Blocked,
110    /// The background job is waiting on explicit approval.
111    AwaitingApproval,
112    /// The background job is currently running.
113    Running,
114    /// The background job completed successfully.
115    Succeeded,
116    /// The background job failed.
117    Failed,
118    /// The background job was cancelled.
119    Cancelled,
120}
121
122/// Background job metadata attached to a task.
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct TaskBackgroundJob {
125    /// Current background job status.
126    pub status: TaskBackgroundStatus,
127    /// Optional identifier for the job in an external system.
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub job_id: Option<String>,
130    /// Optional human-readable status message.
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub message: Option<String>,
133}
134
135/// Phase in the task/memory lifecycle.
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
137#[serde(rename_all = "snake_case")]
138pub enum TaskMemoryPhase {
139    /// Task was delegated to a subagent.
140    Delegated,
141    /// Task produced a handoff summary.
142    Handoff,
143    /// Task promoted durable/shared memory.
144    Promoted,
145    /// Task hit a blocker relevant to memory tracking.
146    Blocked,
147}
148
149/// Structured event recorded in task metadata for memory lifecycle tracking.
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct TaskMemoryEvent {
152    /// Lifecycle phase represented by this event.
153    pub phase: TaskMemoryPhase,
154    /// Human-readable summary.
155    pub summary: String,
156    /// Optional scope for the related memory record.
157    #[serde(default, skip_serializing_if = "Option::is_none")]
158    pub scope: Option<String>,
159    /// Optional memory type for the related memory record.
160    #[serde(default, skip_serializing_if = "Option::is_none")]
161    pub memory_type: Option<String>,
162    /// Optional durable memory file path.
163    #[serde(default, skip_serializing_if = "Option::is_none")]
164    pub memory_file_path: Option<String>,
165    /// Timestamp when this event was recorded.
166    pub recorded_at: DateTime<Utc>,
167}
168
169impl TaskMemoryEvent {
170    /// Create a new task memory event.
171    pub fn new(
172        phase: TaskMemoryPhase,
173        summary: impl Into<String>,
174        scope: Option<String>,
175        memory_type: Option<String>,
176        memory_file_path: Option<String>,
177    ) -> Self {
178        Self {
179            phase,
180            summary: summary.into(),
181            scope,
182            memory_type,
183            memory_file_path,
184            recorded_at: Utc::now(),
185        }
186    }
187}
188
189/// Structured task-local memory lifecycle information persisted in metadata.
190#[derive(Debug, Clone, Default, Serialize, Deserialize)]
191pub struct TaskMemoryLifecycle {
192    /// Recorded lifecycle events.
193    #[serde(default, skip_serializing_if = "Vec::is_empty")]
194    pub events: Vec<TaskMemoryEvent>,
195    /// Most recent durable memory file path, if any.
196    #[serde(default, skip_serializing_if = "Option::is_none")]
197    pub last_memory_file_path: Option<String>,
198}
199
200/// Runtime-owned execution kind inferred or assigned for a tracked task.
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
202#[serde(rename_all = "snake_case")]
203pub enum TaskExecutionKind {
204    /// Investigation, analysis, or planning work.
205    Planning,
206    /// Concrete implementation or file mutation work.
207    Implementation,
208    /// Build, test, lint, or other verification work.
209    Verification,
210    /// Fallback when no stronger runtime classification is available.
211    #[default]
212    General,
213}
214
215/// Runtime-authored verification requirements for a tracked task.
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
217pub struct TaskVerificationProfile {
218    /// Primary execution kind for the task.
219    #[serde(default)]
220    pub execution_kind: TaskExecutionKind,
221    /// Whether the task requires a successful source mutation.
222    #[serde(default)]
223    pub requires_mutation: bool,
224    /// Whether the task requires a successful build/check command.
225    #[serde(default)]
226    pub requires_build: bool,
227    /// Whether the task requires a successful test command.
228    #[serde(default)]
229    pub requires_test: bool,
230    /// Whether the task requires external evidence (for example, a source
231    /// cross-check) rather than a local readback alone.
232    #[serde(default)]
233    pub requires_external_evidence: bool,
234    /// Whether the task requires direct runtime launch evidence instead of only
235    /// build or test results.
236    #[serde(default)]
237    pub requires_launch_evidence: bool,
238    /// Whether the runtime considers the task safe to run in parallel with other
239    /// ready tasks.
240    #[serde(default)]
241    pub parallel_safe: bool,
242}
243
244/// Structured runtime evidence recorded for a task.
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
246#[serde(rename_all = "snake_case")]
247pub enum TaskExecutionEvidenceKind {
248    /// Some successful tool work occurred for the task.
249    ToolActivity,
250    /// Diagnostic progress was made without yet proving completion.
251    Diagnostic,
252    /// Observed evidence contradicted the expected outcome.
253    Contradiction,
254    /// Observed evidence surfaced a blocker or unavailable dependency.
255    Blocker,
256    /// A source mutation completed successfully.
257    Mutation,
258    /// A build or compile verification command succeeded.
259    Build,
260    /// A test command succeeded.
261    Test,
262    /// An artifact was produced or discovered.
263    Artifact,
264}
265
266/// A single execution evidence record stored in task metadata.
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
268pub struct TaskExecutionEvidence {
269    /// Kind of runtime evidence observed.
270    pub kind: TaskExecutionEvidenceKind,
271    /// Human-readable summary of what happened.
272    pub summary: String,
273    /// Tool responsible for the evidence, if any.
274    #[serde(default, skip_serializing_if = "Option::is_none")]
275    pub tool_name: Option<String>,
276    /// Command associated with the evidence, if any.
277    #[serde(default, skip_serializing_if = "Option::is_none")]
278    pub command: Option<String>,
279    /// Whether the evidence corresponds to a successful outcome.
280    #[serde(default = "default_true")]
281    pub success: bool,
282    /// Timestamp when the evidence was recorded.
283    pub recorded_at: DateTime<Utc>,
284}
285
286/// Runtime-owned execution state persisted in task metadata.
287#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
288pub struct TaskExecutionState {
289    /// Verification requirements currently assigned to the task.
290    #[serde(default)]
291    pub verification_profile: TaskVerificationProfile,
292    /// Whether any successful tool activity has been observed for the task.
293    #[serde(default)]
294    pub saw_tool_activity: bool,
295    /// Whether successful source mutation evidence has been observed.
296    #[serde(default)]
297    pub saw_mutation: bool,
298    /// Whether diagnostic progress has been observed for the task.
299    #[serde(default)]
300    pub saw_diagnostic_progress: bool,
301    /// Whether contradiction evidence has been observed for the task.
302    #[serde(default)]
303    pub saw_contradiction: bool,
304    /// Whether blocker evidence has been observed for the task.
305    #[serde(default)]
306    pub saw_blocker: bool,
307    /// Whether successful build/check evidence has been observed.
308    #[serde(default)]
309    pub build_succeeded: bool,
310    /// Whether successful test evidence has been observed.
311    #[serde(default)]
312    pub test_succeeded: bool,
313    /// Optional runtime note for UI/status surfaces.
314    #[serde(default, skip_serializing_if = "Option::is_none")]
315    pub last_runtime_note: Option<String>,
316    /// Structured evidence history, capped to a small rolling window.
317    #[serde(default, skip_serializing_if = "Vec::is_empty")]
318    pub evidence: Vec<TaskExecutionEvidence>,
319}
320
321fn default_true() -> bool {
322    true
323}
324
325impl TaskExecutionEvidence {
326    /// Create a new task execution evidence record.
327    pub fn new(
328        kind: TaskExecutionEvidenceKind,
329        summary: impl Into<String>,
330        tool_name: Option<String>,
331        command: Option<String>,
332    ) -> Self {
333        Self {
334            kind,
335            summary: summary.into(),
336            tool_name,
337            command,
338            success: true,
339            recorded_at: Utc::now(),
340        }
341    }
342
343    /// Override whether the evidence represents a successful outcome.
344    pub fn with_success(mut self, success: bool) -> Self {
345        self.success = success;
346        self
347    }
348}
349
350impl TaskExecutionState {
351    fn latest_successful_mutation_index(&self) -> Option<usize> {
352        self.evidence
353            .iter()
354            .enumerate()
355            .filter_map(|(index, evidence)| {
356                (evidence.kind == TaskExecutionEvidenceKind::Mutation && evidence.success)
357                    .then_some(index)
358            })
359            .next_back()
360    }
361
362    fn has_external_verification_evidence_after_latest_mutation(&self) -> bool {
363        let latest_mutation_index = self.latest_successful_mutation_index();
364
365        self.evidence.iter().enumerate().any(|(index, evidence)| {
366            evidence.kind == TaskExecutionEvidenceKind::ToolActivity
367                && evidence.success
368                && evidence
369                    .tool_name
370                    .as_deref()
371                    .is_some_and(|tool_name| matches!(tool_name, "web" | "web_search"))
372                && latest_mutation_index.is_none_or(|mutation_index| index > mutation_index)
373        })
374    }
375
376    fn has_launch_verification_evidence_after_latest_mutation(&self) -> bool {
377        let latest_mutation_index = self.latest_successful_mutation_index();
378
379        self.evidence.iter().enumerate().any(|(index, evidence)| {
380            evidence.kind == TaskExecutionEvidenceKind::ToolActivity
381                && evidence.success
382                && evidence.tool_name.as_deref().is_some_and(|tool_name| {
383                    matches!(tool_name, "shell" | "browser" | "open-browser")
384                })
385                && evidence
386                    .command
387                    .as_deref()
388                    .is_some_and(Self::launch_evidence_matches)
389                && latest_mutation_index.is_none_or(|mutation_index| index > mutation_index)
390        })
391    }
392
393    fn launch_evidence_matches(command: &str) -> bool {
394        let normalized = command.to_ascii_lowercase();
395        [
396            "cargo tauri dev",
397            "tauri dev",
398            "npm run tauri dev",
399            "pnpm tauri dev",
400            "pnpm run tauri dev",
401            "yarn tauri dev",
402            "yarn run tauri dev",
403            "bun tauri dev",
404            "bun run tauri dev",
405            "cargo run",
406            "npm run dev",
407            "pnpm dev",
408            "pnpm run dev",
409            "yarn dev",
410            "yarn run dev",
411            "bun dev",
412            "bun run dev",
413        ]
414        .iter()
415        .any(|marker| normalized.contains(marker))
416    }
417
418    /// Merge a runtime-authored verification profile into the current state.
419    pub fn merge_profile(&mut self, profile: TaskVerificationProfile) {
420        self.verification_profile = profile;
421    }
422
423    /// Record evidence and update the derived boolean summary.
424    pub fn record_evidence(&mut self, evidence: TaskExecutionEvidence) -> bool {
425        let duplicate = self.evidence.iter().any(|existing| {
426            existing.kind == evidence.kind
427                && existing.summary == evidence.summary
428                && existing.tool_name == evidence.tool_name
429                && existing.command == evidence.command
430                && existing.success == evidence.success
431        });
432        if duplicate {
433            return false;
434        }
435
436        match evidence.kind {
437            TaskExecutionEvidenceKind::ToolActivity => self.saw_tool_activity = true,
438            TaskExecutionEvidenceKind::Diagnostic => {
439                self.saw_tool_activity = true;
440                self.saw_diagnostic_progress = true;
441            }
442            TaskExecutionEvidenceKind::Contradiction => {
443                self.saw_tool_activity = true;
444                self.saw_diagnostic_progress = true;
445                self.saw_contradiction = true;
446            }
447            TaskExecutionEvidenceKind::Blocker => {
448                self.saw_tool_activity = true;
449                self.saw_diagnostic_progress = true;
450                self.saw_blocker = true;
451            }
452            TaskExecutionEvidenceKind::Mutation => {
453                self.saw_tool_activity = true;
454                if evidence.success {
455                    self.saw_mutation = true;
456                }
457            }
458            TaskExecutionEvidenceKind::Build => {
459                self.saw_tool_activity = true;
460                if evidence.success {
461                    self.build_succeeded = true;
462                }
463            }
464            TaskExecutionEvidenceKind::Test => {
465                self.saw_tool_activity = true;
466                if evidence.success {
467                    self.test_succeeded = true;
468                }
469            }
470            TaskExecutionEvidenceKind::Artifact => self.saw_tool_activity = true,
471        }
472
473        self.evidence.push(evidence);
474        if self.evidence.len() > 32 {
475            let excess = self.evidence.len() - 32;
476            self.evidence.drain(0..excess);
477        }
478        true
479    }
480
481    /// Return `true` when the observed runtime evidence satisfies the task's
482    /// assigned verification profile.
483    pub fn satisfies_profile(&self) -> bool {
484        let requires_progress = !self.verification_profile.requires_mutation
485            && !self.verification_profile.requires_build
486            && !self.verification_profile.requires_test
487            && !self.verification_profile.requires_external_evidence
488            && !self.verification_profile.requires_launch_evidence;
489
490        (!self.verification_profile.requires_mutation || self.saw_mutation)
491            && (!self.verification_profile.requires_build || self.build_succeeded)
492            && (!self.verification_profile.requires_test || self.test_succeeded)
493            && (!self.verification_profile.requires_external_evidence
494                || self.has_external_verification_evidence_after_latest_mutation())
495            && (!self.verification_profile.requires_launch_evidence
496                || self.has_launch_verification_evidence_after_latest_mutation())
497            && (!requires_progress || self.saw_tool_activity)
498    }
499}
500
501impl TaskBackgroundJob {
502    /// Create a new background job record.
503    pub fn new(
504        status: TaskBackgroundStatus,
505        job_id: Option<String>,
506        message: Option<String>,
507    ) -> Self {
508        Self {
509            status,
510            job_id,
511            message,
512        }
513    }
514}
515
516/// A task represents a unit of work to be tracked
517#[derive(Debug, Clone, Serialize, Deserialize)]
518pub struct Task {
519    /// Unique identifier for this task
520    pub id: String,
521    /// Human-readable name
522    pub name: String,
523    /// Detailed description
524    pub description: String,
525    /// Current status
526    pub status: TaskStatus,
527    /// Parent task ID (for subtasks)
528    pub parent_id: Option<String>,
529
530    /// IDs of tasks that block this task from being started/completed.
531    ///
532    /// This is modeled as a dependency list ("blocked by") so we can derive
533    /// the inverse relationship ("blocks") on demand.
534    #[serde(default, skip_serializing_if = "Vec::is_empty")]
535    pub blocked_by: Vec<String>,
536
537    /// Optional background job state for tasks that run asynchronously.
538    #[serde(default, skip_serializing_if = "Option::is_none")]
539    pub background_job: Option<TaskBackgroundJob>,
540
541    /// Optional ordering hint for UI dashboards (lower first).
542    #[serde(default)]
543    pub sort_order: i32,
544
545    /// Optional phase/group label for UI dashboards.
546    #[serde(default, skip_serializing_if = "Option::is_none")]
547    pub phase: Option<String>,
548    /// When the task was created
549    pub created_at: DateTime<Utc>,
550    /// When the task was last updated
551    pub updated_at: DateTime<Utc>,
552    /// Session ID this task belongs to
553    pub session_id: String,
554    /// Source of the task (user, agent, or orchestrator)
555    #[serde(default)]
556    pub source: TaskSource,
557    /// ID linking to an orchestrator DelegatedTask (for bidirectional sync)
558    #[serde(default)]
559    pub orchestrator_task_id: Option<String>,
560    /// ID of the agent that created/owns this task
561    #[serde(default)]
562    pub agent_id: Option<String>,
563    /// Additional metadata (tool calls, output, context, etc.)
564    #[serde(default)]
565    pub metadata: Option<serde_json::Value>,
566}
567
568/// Result of reconciling a tracked task after an agent turn.
569#[derive(Debug, Clone, PartialEq, Eq)]
570pub enum TrackedTaskFinalization {
571    /// The tracked task and its subtree are complete.
572    Completed,
573    /// The tracked task still has open subtasks and should remain in progress.
574    StillInProgress { open_subtask_ids: Vec<String> },
575}
576
577/// LLM-produced task specification used to materialize an auto-plan breakdown.
578#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
579pub struct RequirementBreakdownTaskSpec {
580    /// Concise task title shown in task-tree UIs.
581    pub name: String,
582    /// Human-readable implementation details for the task.
583    pub description: String,
584    /// Relative importance label captured from the planner.
585    pub priority: String,
586    /// Whether other work should wait on this task.
587    pub is_blocking: bool,
588    /// Exact parent task name when this spec is nested under another spec.
589    #[serde(default)]
590    pub parent_name: Option<String>,
591}
592
593impl RequirementBreakdownTaskSpec {
594    fn render_description(&self) -> String {
595        let mut description = self.description.trim().to_string();
596        if !self.priority.trim().is_empty() {
597            description.push_str(&format!("\n\nPriority: {}", self.priority.trim()));
598        }
599        if self.is_blocking {
600            description.push_str("\nBlocking: yes");
601        }
602        description
603    }
604}
605
606/// Result of materializing requirement-breakdown tasks.
607#[derive(Debug, Clone, Default)]
608pub struct MaterializedTaskBreakdown {
609    /// All persisted tasks created from the supplied breakdown specs.
610    pub created_tasks: Vec<Task>,
611    /// The subset of `created_tasks` that were attached directly to the root.
612    pub root_task_ids: Vec<String>,
613}
614
615/// Shared execution context for a tracked auto-planned request.
616#[derive(Debug, Clone)]
617pub struct AutoTrackedExecutionPlan {
618    /// Tracked root task that owns the request-wide plan.
619    pub root_task: Task,
620    /// Summary labels for the currently open planned subtasks.
621    pub planned_subtasks: Vec<String>,
622    /// Initially focused execution task under the tracked root, if any.
623    pub initial_task_id: Option<String>,
624    /// Human-readable label for the initial task, if any.
625    pub initial_task_name: Option<String>,
626    /// Number of generated descendant tasks created beneath the root.
627    pub generated_task_count: usize,
628}
629
630/// Parse, normalize, and validate a planner response into typed task specs.
631pub fn parse_requirement_breakdown_response(
632    response: &str,
633) -> Result<Vec<RequirementBreakdownTaskSpec>, TaskError> {
634    let trimmed = response.trim();
635    let parsed =
636        serde_json::from_str::<Vec<RequirementBreakdownTaskSpec>>(trimmed).or_else(|_| {
637            let start = trimmed.find('[').ok_or_else(|| {
638                serde_json::Error::io(std::io::Error::new(
639                    std::io::ErrorKind::InvalidData,
640                    "planner response did not contain a JSON array",
641                ))
642            })?;
643            let end = trimmed.rfind(']').ok_or_else(|| {
644                serde_json::Error::io(std::io::Error::new(
645                    std::io::ErrorKind::InvalidData,
646                    "planner response did not contain a closing JSON array bracket",
647                ))
648            })?;
649            serde_json::from_str::<Vec<RequirementBreakdownTaskSpec>>(&trimmed[start..=end])
650        })?;
651
652    normalize_requirement_breakdown_specs(parsed)
653}
654
655fn normalize_requirement_breakdown_specs(
656    specs: Vec<RequirementBreakdownTaskSpec>,
657) -> Result<Vec<RequirementBreakdownTaskSpec>, TaskError> {
658    if specs.is_empty() {
659        return Err(TaskError::InvalidInput(
660            "requirement breakdown did not include any task specs".to_string(),
661        ));
662    }
663
664    let mut normalized = Vec::with_capacity(specs.len());
665    let mut seen_names = HashSet::new();
666
667    for mut spec in specs {
668        spec.name = spec.name.trim().to_string();
669        spec.description = spec.description.trim().to_string();
670        spec.priority = spec.priority.trim().to_ascii_lowercase();
671        spec.parent_name = spec
672            .parent_name
673            .take()
674            .map(|value| value.trim().to_string())
675            .filter(|value| !value.is_empty());
676
677        if spec.name.is_empty() {
678            return Err(TaskError::InvalidInput(
679                "requirement breakdown included a task with an empty name".to_string(),
680            ));
681        }
682        if spec.description.is_empty() {
683            return Err(TaskError::InvalidInput(format!(
684                "requirement breakdown task '{}' is missing a description",
685                spec.name
686            )));
687        }
688        if !seen_names.insert(spec.name.clone()) {
689            return Err(TaskError::InvalidInput(format!(
690                "requirement breakdown contains duplicate task name '{}'",
691                spec.name
692            )));
693        }
694
695        normalized.push(spec);
696    }
697
698    let valid_names = normalized
699        .iter()
700        .map(|spec| spec.name.as_str())
701        .collect::<HashSet<_>>();
702    for spec in &normalized {
703        if let Some(parent_name) = spec.parent_name.as_deref() {
704            if parent_name == spec.name {
705                return Err(TaskError::InvalidInput(format!(
706                    "requirement breakdown task '{}' cannot parent itself",
707                    spec.name
708                )));
709            }
710            if !valid_names.contains(parent_name) {
711                return Err(TaskError::InvalidInput(format!(
712                    "requirement breakdown task '{}' references unknown parent '{}'",
713                    spec.name, parent_name
714                )));
715            }
716        }
717    }
718
719    Ok(normalized)
720}
721
722fn format_auto_tracked_root_description(original_input: &str) -> String {
723    format!(
724        "Autogenerated tracked execution task for request:\n\n{}",
725        original_input.trim()
726    )
727}
728
729fn format_planned_subtask_label(task: &Task) -> String {
730    format!("{} [{}]", task.name, task.status)
731}
732
733fn canonical_materialized_task_name(name: &str) -> String {
734    name.split_whitespace()
735        .collect::<Vec<_>>()
736        .join(" ")
737        .to_ascii_lowercase()
738}
739
740impl Task {
741    /// Create a new task (defaults to User source)
742    pub fn new(
743        session_id: impl Into<String>,
744        name: impl Into<String>,
745        description: impl Into<String>,
746        parent_id: Option<String>,
747    ) -> Self {
748        let now = Utc::now();
749        Self {
750            id: Uuid::new_v4().to_string(),
751            name: name.into(),
752            description: description.into(),
753            status: TaskStatus::NotStarted,
754            parent_id,
755            blocked_by: Vec::new(),
756            background_job: None,
757            sort_order: 0,
758            phase: None,
759            created_at: now,
760            updated_at: now,
761            session_id: session_id.into(),
762            source: TaskSource::User,
763            orchestrator_task_id: None,
764            agent_id: None,
765            metadata: None,
766        }
767    }
768
769    /// Create a new task with a specific source
770    pub fn new_with_source(
771        session_id: impl Into<String>,
772        name: impl Into<String>,
773        description: impl Into<String>,
774        parent_id: Option<String>,
775        source: TaskSource,
776        agent_id: Option<String>,
777    ) -> Self {
778        let now = Utc::now();
779        Self {
780            id: Uuid::new_v4().to_string(),
781            name: name.into(),
782            description: description.into(),
783            status: TaskStatus::NotStarted,
784            parent_id,
785            blocked_by: Vec::new(),
786            background_job: None,
787            sort_order: 0,
788            phase: None,
789            created_at: now,
790            updated_at: now,
791            session_id: session_id.into(),
792            source,
793            orchestrator_task_id: None,
794            agent_id,
795            metadata: None,
796        }
797    }
798
799    /// Create a task from an orchestrator delegated task
800    pub fn from_orchestrator_task(
801        session_id: impl Into<String>,
802        orchestrator_task_id: impl Into<String>,
803        agent_id: impl Into<String>,
804        name: impl Into<String>,
805        description: impl Into<String>,
806        context: Option<serde_json::Value>,
807    ) -> Self {
808        let now = Utc::now();
809        Self {
810            id: Uuid::new_v4().to_string(),
811            name: name.into(),
812            description: description.into(),
813            status: TaskStatus::NotStarted,
814            parent_id: None,
815            blocked_by: Vec::new(),
816            background_job: Some(TaskBackgroundJob::new(
817                TaskBackgroundStatus::Queued,
818                None,
819                Some("Created by orchestrator".to_string()),
820            )),
821            sort_order: 0,
822            phase: None,
823            created_at: now,
824            updated_at: now,
825            session_id: session_id.into(),
826            source: TaskSource::Orchestrator,
827            orchestrator_task_id: Some(orchestrator_task_id.into()),
828            agent_id: Some(agent_id.into()),
829            metadata: context,
830        }
831    }
832
833    /// Set metadata (e.g., tool calls, output)
834    pub fn set_metadata(&mut self, metadata: serde_json::Value) {
835        self.metadata = Some(metadata);
836        self.updated_at = Utc::now();
837    }
838
839    /// Update the task status
840    pub fn set_status(&mut self, status: TaskStatus) {
841        self.status = status;
842        self.updated_at = Utc::now();
843    }
844
845    /// Update the task name
846    pub fn set_name(&mut self, name: impl Into<String>) {
847        self.name = name.into();
848        self.updated_at = Utc::now();
849    }
850
851    /// Update the task description
852    pub fn set_description(&mut self, description: impl Into<String>) {
853        self.description = description.into();
854        self.updated_at = Utc::now();
855    }
856
857    /// Returns `true` when the task is in a terminal (non-active) status.
858    pub fn is_terminal(&self) -> bool {
859        matches!(self.status, TaskStatus::Completed | TaskStatus::Cancelled)
860    }
861
862    /// Replace the background job state and update timestamps.
863    pub fn set_background_job(&mut self, job: Option<TaskBackgroundJob>) {
864        self.background_job = job;
865        self.updated_at = Utc::now();
866    }
867
868    /// Add a dependency ("blocked by") to this task.
869    ///
870    /// This does not validate existence; callers should validate using the session's `TaskList`.
871    pub fn add_blocked_by(&mut self, task_id: impl Into<String>) {
872        let id = task_id.into();
873        if !self.blocked_by.iter().any(|x| x == &id) {
874            self.blocked_by.push(id);
875            self.updated_at = Utc::now();
876        }
877    }
878
879    /// Set the phase/group label for dashboard grouping.
880    pub fn set_phase(&mut self, phase: Option<String>) {
881        self.phase = phase;
882        self.updated_at = Utc::now();
883    }
884}
885
886/// A list of tasks for a session
887#[derive(Debug, Clone, Serialize, Deserialize)]
888pub struct TaskList {
889    /// Session ID
890    pub session_id: String,
891    /// All tasks in this session
892    pub tasks: Vec<Task>,
893    /// Optional "current task" pointer for UI focus and checkpoint/rewind.
894    ///
895    /// This is persisted alongside the task list so the UI can restore the
896    /// user's current focus when resuming a session.
897    #[serde(default, skip_serializing_if = "Option::is_none")]
898    pub current_task_id: Option<String>,
899}
900
901/// A hierarchical node for rendering task trees in dashboards.
902#[derive(Debug, Clone, Serialize)]
903pub struct TaskTreeNode {
904    /// The task for this node.
905    pub task: Task,
906    /// Child tasks.
907    pub children: Vec<TaskTreeNode>,
908}
909
910impl TaskList {
911    /// Create a new task list for a session
912    pub fn new(session_id: impl Into<String>) -> Self {
913        Self {
914            session_id: session_id.into(),
915            tasks: Vec::new(),
916            current_task_id: None,
917        }
918    }
919
920    /// Get the currently focused task id.
921    pub fn current_task_id(&self) -> Option<&str> {
922        self.current_task_id.as_deref()
923    }
924
925    /// Set or clear the current task pointer.
926    ///
927    /// If `task_id` is `Some`, the id must exist in this task list.
928    pub fn set_current_task_id(&mut self, task_id: Option<String>) -> Result<(), TaskError> {
929        if let Some(ref id) = task_id
930            && self.find_task(id.as_str()).is_none()
931        {
932            return Err(TaskError::InvalidInput(format!(
933                "current_task_id '{id}' does not exist in task list"
934            )));
935        }
936
937        self.current_task_id = task_id;
938        Ok(())
939    }
940
941    /// Add a task to the list
942    pub fn add_task(&mut self, task: Task) {
943        self.tasks.push(task);
944    }
945
946    /// Find a task by ID
947    pub fn find_task(&self, task_id: &str) -> Option<&Task> {
948        self.tasks.iter().find(|t| t.id == task_id)
949    }
950
951    /// Find a task by ID (mutable)
952    pub fn find_task_mut(&mut self, task_id: &str) -> Option<&mut Task> {
953        self.tasks.iter_mut().find(|t| t.id == task_id)
954    }
955
956    /// Remove a task by ID
957    pub fn remove_task(&mut self, task_id: &str) -> Option<Task> {
958        if let Some(pos) = self.tasks.iter().position(|t| t.id == task_id) {
959            if self.current_task_id.as_deref() == Some(task_id) {
960                self.current_task_id = None;
961            }
962            Some(self.tasks.remove(pos))
963        } else {
964            None
965        }
966    }
967
968    /// Get all root tasks (tasks without a parent)
969    pub fn root_tasks(&self) -> Vec<&Task> {
970        self.tasks
971            .iter()
972            .filter(|t| t.parent_id.is_none())
973            .collect()
974    }
975
976    /// Get all subtasks of a given task
977    pub fn subtasks(&self, parent_id: &str) -> Vec<&Task> {
978        self.tasks
979            .iter()
980            .filter(|t| t.parent_id.as_deref() == Some(parent_id))
981            .collect()
982    }
983
984    fn sort_task_refs_by_priority(tasks: &mut [&Task]) {
985        tasks.sort_by(|a, b| {
986            a.sort_order
987                .cmp(&b.sort_order)
988                .then_with(|| a.created_at.cmp(&b.created_at))
989        });
990    }
991
992    /// Get all descendant tasks of a given task in depth-first order.
993    pub fn descendants(&self, task_id: &str) -> Vec<&Task> {
994        let mut descendants = Vec::new();
995        let mut root_children = self.subtasks(task_id);
996        Self::sort_task_refs_by_priority(&mut root_children);
997        let mut pending_ids = root_children
998            .into_iter()
999            .rev()
1000            .map(|task| task.id.clone())
1001            .collect::<Vec<_>>();
1002        let mut seen = HashSet::new();
1003
1004        while let Some(current_id) = pending_ids.pop() {
1005            if !seen.insert(current_id.clone()) {
1006                continue;
1007            }
1008
1009            let Some(task) = self.find_task(&current_id) else {
1010                continue;
1011            };
1012            descendants.push(task);
1013
1014            let mut children = self.subtasks(&current_id);
1015            Self::sort_task_refs_by_priority(&mut children);
1016            for child in children.into_iter().rev() {
1017                pending_ids.push(child.id.clone());
1018            }
1019        }
1020
1021        descendants
1022    }
1023
1024    fn ancestor_ids(&self, task_id: &str) -> Vec<String> {
1025        let mut ancestors = Vec::new();
1026        let mut seen = HashSet::new();
1027        let mut current_parent = self
1028            .find_task(task_id)
1029            .and_then(|task| task.parent_id.clone());
1030
1031        while let Some(parent_id) = current_parent {
1032            if !seen.insert(parent_id.clone()) {
1033                break;
1034            }
1035
1036            current_parent = self
1037                .find_task(&parent_id)
1038                .and_then(|task| task.parent_id.clone());
1039            ancestors.push(parent_id);
1040        }
1041
1042        ancestors
1043    }
1044
1045    /// Return `true` when the task is blocked by any dependency that is not terminal.
1046    pub fn is_task_blocked(&self, task_id: &str) -> Result<bool, TaskError> {
1047        let task = self
1048            .find_task(task_id)
1049            .ok_or_else(|| TaskError::NotFound(task_id.to_string()))?;
1050
1051        for dep_id in &task.blocked_by {
1052            // Missing dependency is treated as blocking; it's a data integrity issue.
1053            let Some(dep) = self.find_task(dep_id) else {
1054                return Ok(true);
1055            };
1056            if !dep.is_terminal() {
1057                return Ok(true);
1058            }
1059        }
1060
1061        Ok(false)
1062    }
1063
1064    /// Validate that a task can transition to the requested status.
1065    fn validate_status_transition(
1066        &self,
1067        task_id: &str,
1068        status: TaskStatus,
1069    ) -> Result<(), TaskError> {
1070        if status != TaskStatus::Completed {
1071            return Ok(());
1072        }
1073
1074        let task = self
1075            .find_task(task_id)
1076            .ok_or_else(|| TaskError::NotFound(task_id.to_string()))?;
1077
1078        if self.is_task_blocked(task_id)? {
1079            return Err(TaskError::InvalidInput(format!(
1080                "task '{}' cannot be completed while dependencies remain open",
1081                task.name
1082            )));
1083        }
1084
1085        let open_subtasks: Vec<&Task> = self
1086            .descendants(task_id)
1087            .into_iter()
1088            .filter(|subtask| !subtask.is_terminal())
1089            .collect();
1090        if !open_subtasks.is_empty() {
1091            let names = open_subtasks
1092                .iter()
1093                .map(|subtask| format!("'{}'", subtask.name))
1094                .collect::<Vec<_>>()
1095                .join(", ");
1096            return Err(TaskError::InvalidInput(format!(
1097                "task '{}' cannot be completed while subtasks remain open: {}",
1098                task.name, names
1099            )));
1100        }
1101
1102        if let Some(job) = task.background_job.as_ref()
1103            && matches!(
1104                job.status,
1105                TaskBackgroundStatus::Queued
1106                    | TaskBackgroundStatus::Blocked
1107                    | TaskBackgroundStatus::AwaitingApproval
1108                    | TaskBackgroundStatus::Running
1109            )
1110        {
1111            return Err(TaskError::InvalidInput(format!(
1112                "task '{}' cannot be completed while its background job is still {:?}",
1113                task.name, job.status
1114            )));
1115        }
1116
1117        Ok(())
1118    }
1119
1120    /// Add a dependency relationship: `task_id` is blocked by `blocked_by_id`.
1121    ///
1122    /// This enforces:
1123    /// - both tasks must exist
1124    /// - no self-dependencies
1125    /// - no cycles across `blocked_by` relationships
1126    pub fn add_dependency(&mut self, task_id: &str, blocked_by_id: &str) -> Result<(), TaskError> {
1127        if task_id == blocked_by_id {
1128            return Err(TaskError::InvalidInput(
1129                "task cannot be blocked by itself".to_string(),
1130            ));
1131        }
1132
1133        // Ensure both exist.
1134        if self.find_task(task_id).is_none() {
1135            return Err(TaskError::NotFound(task_id.to_string()));
1136        }
1137        if self.find_task(blocked_by_id).is_none() {
1138            return Err(TaskError::NotFound(blocked_by_id.to_string()));
1139        }
1140
1141        // Reject cycles: if `blocked_by_id` depends on `task_id` already, we'd create a cycle.
1142        if self.depends_on_transitively(blocked_by_id, task_id) {
1143            return Err(TaskError::InvalidInput(
1144                "dependency would create a cycle".to_string(),
1145            ));
1146        }
1147
1148        let task = self
1149            .find_task_mut(task_id)
1150            .ok_or_else(|| TaskError::NotFound(task_id.to_string()))?;
1151        task.add_blocked_by(blocked_by_id.to_string());
1152        Ok(())
1153    }
1154
1155    /// Build a recursive task tree.
1156    ///
1157    /// Nodes are ordered by `sort_order` then creation time.
1158    ///
1159    /// Tasks referencing a missing parent are treated as roots.
1160    pub fn build_tree(&self) -> Vec<TaskTreeNode> {
1161        let ids: HashSet<String> = self.tasks.iter().map(|t| t.id.clone()).collect();
1162        self.build_tree_for_parent(None, &ids)
1163    }
1164
1165    /// Internal helper for building a task tree for a given parent.
1166    ///
1167    /// - `parent_id=None` means "roots".
1168    /// - Any task whose parent is missing from `ids` is treated as a root.
1169    fn build_tree_for_parent(
1170        &self,
1171        parent_id: Option<&str>,
1172        ids: &HashSet<String>,
1173    ) -> Vec<TaskTreeNode> {
1174        let mut children: Vec<Task> = self
1175            .tasks
1176            .iter()
1177            .filter(|t| match parent_id {
1178                Some(pid) => t.parent_id.as_deref() == Some(pid),
1179                None => {
1180                    t.parent_id.is_none()
1181                        || t.parent_id.as_deref().is_some_and(|p| !ids.contains(p))
1182                }
1183            })
1184            .cloned()
1185            .collect();
1186
1187        children.sort_by(|a, b| {
1188            a.sort_order
1189                .cmp(&b.sort_order)
1190                .then_with(|| a.created_at.cmp(&b.created_at))
1191        });
1192
1193        children
1194            .into_iter()
1195            .map(|task| TaskTreeNode {
1196                children: self.build_tree_for_parent(Some(task.id.as_str()), ids),
1197                task,
1198            })
1199            .collect()
1200    }
1201
1202    /// Returns `true` if `start` depends on `target` directly or transitively.
1203    fn depends_on_transitively(&self, start: &str, target: &str) -> bool {
1204        let mut visited: HashSet<String> = HashSet::new();
1205        self.depends_on_transitively_inner(start, target, &mut visited)
1206    }
1207
1208    /// DFS helper for `depends_on_transitively`.
1209    fn depends_on_transitively_inner(
1210        &self,
1211        start: &str,
1212        target: &str,
1213        visited: &mut HashSet<String>,
1214    ) -> bool {
1215        if start == target {
1216            return true;
1217        }
1218
1219        if visited.contains(start) {
1220            return false;
1221        }
1222        visited.insert(start.to_string());
1223
1224        let Some(task) = self.find_task(start) else {
1225            return false;
1226        };
1227
1228        for dep in &task.blocked_by {
1229            if self.depends_on_transitively_inner(dep, target, visited) {
1230                return true;
1231            }
1232        }
1233
1234        false
1235    }
1236}
1237
1238/// Error type for task operations
1239#[derive(Debug, thiserror::Error)]
1240pub enum TaskError {
1241    /// Task not found
1242    #[error("Task not found: {0}")]
1243    NotFound(String),
1244    /// Invalid input
1245    #[error("Invalid input: {0}")]
1246    InvalidInput(String),
1247    /// I/O error
1248    #[error("I/O error: {0}")]
1249    Io(#[from] std::io::Error),
1250    /// Serialization error
1251    #[error("Serialization error: {0}")]
1252    Serialization(#[from] serde_json::Error),
1253}
1254
1255/// Task manager for persisting and managing tasks
1256pub struct TaskManager {
1257    /// Base directory for task files
1258    base_dir: PathBuf,
1259    /// In-memory cache of task lists by session ID
1260    cache: std::sync::RwLock<HashMap<String, TaskList>>,
1261}
1262
1263impl TaskManager {
1264    fn find_existing_materialized_task(
1265        task_list: &TaskList,
1266        parent_id: Option<&str>,
1267        task_name: &str,
1268    ) -> Option<Task> {
1269        let canonical_name = canonical_materialized_task_name(task_name);
1270
1271        task_list
1272            .tasks
1273            .iter()
1274            .filter(|task| task.parent_id.as_deref() == parent_id)
1275            .filter(|task| canonical_materialized_task_name(&task.name) == canonical_name)
1276            .min_by(|left, right| {
1277                left.is_terminal()
1278                    .cmp(&right.is_terminal())
1279                    .then_with(|| left.created_at.cmp(&right.created_at))
1280            })
1281            .cloned()
1282    }
1283
1284    /// Create a new task manager with the given base directory
1285    pub fn new(base_dir: impl Into<PathBuf>) -> Self {
1286        let base_dir = base_dir.into().join(".gestura").join("tasks");
1287        Self {
1288            base_dir,
1289            cache: std::sync::RwLock::new(HashMap::new()),
1290        }
1291    }
1292
1293    /// Get the path to a session's task file
1294    fn task_file_path(&self, session_id: &str) -> PathBuf {
1295        self.base_dir.join(format!("{}.json", session_id))
1296    }
1297
1298    /// Load tasks for a session from disk
1299    fn load_from_disk(&self, session_id: &str) -> Result<TaskList, TaskError> {
1300        let path = self.task_file_path(session_id);
1301        if !path.exists() {
1302            return Ok(TaskList::new(session_id));
1303        }
1304
1305        let content = fs::read_to_string(&path)?;
1306        let task_list: TaskList = serde_json::from_str(&content)?;
1307        Ok(task_list)
1308    }
1309
1310    /// Save tasks for a session to disk
1311    fn save_to_disk(&self, task_list: &TaskList) -> Result<(), TaskError> {
1312        // Ensure directory exists
1313        fs::create_dir_all(&self.base_dir)?;
1314
1315        let path = self.task_file_path(&task_list.session_id);
1316        let content = serde_json::to_string_pretty(task_list)?;
1317        fs::write(&path, content)?;
1318        Ok(())
1319    }
1320
1321    /// Get or load task list for a session
1322    fn get_or_load(&self, session_id: &str) -> Result<TaskList, TaskError> {
1323        // Check cache first
1324        {
1325            let cache = self.cache.read().unwrap();
1326            if let Some(task_list) = cache.get(session_id) {
1327                return Ok(task_list.clone());
1328            }
1329        }
1330
1331        // Load from disk
1332        let task_list = self.load_from_disk(session_id)?;
1333
1334        // Update cache
1335        {
1336            let mut cache = self.cache.write().unwrap();
1337            cache.insert(session_id.to_string(), task_list.clone());
1338        }
1339
1340        Ok(task_list)
1341    }
1342
1343    /// Update cache and save to disk
1344    fn update_and_save(&self, task_list: TaskList) -> Result<(), TaskError> {
1345        // Save to disk first
1346        self.save_to_disk(&task_list)?;
1347
1348        // Update cache
1349        {
1350            let mut cache = self.cache.write().unwrap();
1351            cache.insert(task_list.session_id.clone(), task_list);
1352        }
1353
1354        Ok(())
1355    }
1356
1357    /// Load the full task list for a session.
1358    ///
1359    /// This returns the persisted state (or an empty list if none exists yet).
1360    /// It is used by checkpoint/rewind to snapshot and restore task state.
1361    pub fn load_task_list(&self, session_id: &str) -> Result<TaskList, TaskError> {
1362        self.get_or_load(session_id)
1363    }
1364
1365    /// Replace the persisted task list for a session.
1366    ///
1367    /// This is primarily used by checkpoint/rewind to restore a previous task
1368    /// state.
1369    pub fn replace_task_list(&self, task_list: TaskList) -> Result<(), TaskError> {
1370        self.update_and_save(task_list)
1371    }
1372
1373    /// Set or clear the current task pointer for a session.
1374    ///
1375    /// If `task_id` is `Some`, it must exist in the session's task list.
1376    pub fn set_current_task_id(
1377        &self,
1378        session_id: &str,
1379        task_id: Option<String>,
1380    ) -> Result<(), TaskError> {
1381        let mut task_list = self.get_or_load(session_id)?;
1382        task_list.set_current_task_id(task_id)?;
1383        self.update_and_save(task_list)
1384    }
1385
1386    /// Get the current task pointer for a session.
1387    pub fn get_current_task_id(&self, session_id: &str) -> Result<Option<String>, TaskError> {
1388        let task_list = self.get_or_load(session_id)?;
1389        Ok(task_list.current_task_id.clone())
1390    }
1391
1392    /// Create a new task
1393    pub fn create_task(
1394        &self,
1395        session_id: &str,
1396        name: impl Into<String>,
1397        description: impl Into<String>,
1398        parent_id: Option<String>,
1399    ) -> Result<Task, TaskError> {
1400        let mut task_list = self.get_or_load(session_id)?;
1401        let task = Task::new(session_id, name, description, parent_id);
1402        task_list.add_task(task.clone());
1403        self.update_and_save(task_list)?;
1404        Ok(task)
1405    }
1406
1407    /// Update a task's status
1408    pub fn update_task_status(
1409        &self,
1410        session_id: &str,
1411        task_id: &str,
1412        status: TaskStatus,
1413    ) -> Result<(), TaskError> {
1414        let mut task_list = self.get_or_load(session_id)?;
1415        task_list.validate_status_transition(task_id, status)?;
1416        let task = task_list
1417            .find_task_mut(task_id)
1418            .ok_or_else(|| TaskError::NotFound(task_id.to_string()))?;
1419        task.set_status(status);
1420        Self::clear_current_task_if_terminal(&mut task_list, task_id, status);
1421        Self::reconcile_ancestor_statuses(&mut task_list, task_id)?;
1422        self.update_and_save(task_list)?;
1423        Ok(())
1424    }
1425
1426    fn clear_current_task_if_terminal(task_list: &mut TaskList, task_id: &str, status: TaskStatus) {
1427        if matches!(status, TaskStatus::Completed | TaskStatus::Cancelled)
1428            && task_list.current_task_id.as_deref() == Some(task_id)
1429        {
1430            task_list.current_task_id = None;
1431        }
1432    }
1433
1434    fn reconcile_ancestor_statuses(
1435        task_list: &mut TaskList,
1436        task_id: &str,
1437    ) -> Result<(), TaskError> {
1438        for ancestor_id in task_list.ancestor_ids(task_id) {
1439            let Some(current_status) = task_list.find_task(&ancestor_id).map(|task| task.status)
1440            else {
1441                continue;
1442            };
1443
1444            if current_status == TaskStatus::Cancelled {
1445                continue;
1446            }
1447
1448            let next_status = if task_list
1449                .validate_status_transition(&ancestor_id, TaskStatus::Completed)
1450                .is_ok()
1451            {
1452                TaskStatus::Completed
1453            } else if task_list.is_task_blocked(&ancestor_id)? {
1454                TaskStatus::Blocked
1455            } else {
1456                TaskStatus::InProgress
1457            };
1458
1459            if next_status == current_status {
1460                continue;
1461            }
1462
1463            let ancestor = task_list
1464                .find_task_mut(&ancestor_id)
1465                .ok_or_else(|| TaskError::NotFound(ancestor_id.clone()))?;
1466            ancestor.set_status(next_status);
1467            Self::clear_current_task_if_terminal(task_list, &ancestor_id, next_status);
1468        }
1469
1470        Ok(())
1471    }
1472
1473    /// Update a task's name and description
1474    pub fn update_task(
1475        &self,
1476        session_id: &str,
1477        task_id: &str,
1478        name: Option<String>,
1479        description: Option<String>,
1480    ) -> Result<(), TaskError> {
1481        let mut task_list = self.get_or_load(session_id)?;
1482        let task = task_list
1483            .find_task_mut(task_id)
1484            .ok_or_else(|| TaskError::NotFound(task_id.to_string()))?;
1485
1486        if let Some(name) = name {
1487            task.set_name(name);
1488        }
1489        if let Some(description) = description {
1490            task.set_description(description);
1491        }
1492
1493        self.update_and_save(task_list)?;
1494        Ok(())
1495    }
1496
1497    /// Delete a task
1498    pub fn delete_task(&self, session_id: &str, task_id: &str) -> Result<Task, TaskError> {
1499        let mut task_list = self.get_or_load(session_id)?;
1500        let task = task_list
1501            .remove_task(task_id)
1502            .ok_or_else(|| TaskError::NotFound(task_id.to_string()))?;
1503        self.update_and_save(task_list)?;
1504        Ok(task)
1505    }
1506
1507    /// List all tasks for a session
1508    pub fn list_tasks(&self, session_id: &str) -> Result<Vec<Task>, TaskError> {
1509        let task_list = self.get_or_load(session_id)?;
1510        Ok(task_list.tasks.clone())
1511    }
1512
1513    /// Get task hierarchy for a session
1514    pub fn get_hierarchy(&self, session_id: &str) -> Result<Vec<(Task, Vec<Task>)>, TaskError> {
1515        let task_list = self.get_or_load(session_id)?;
1516        let mut hierarchy = Vec::new();
1517
1518        for root in task_list.root_tasks() {
1519            let subtasks = task_list.subtasks(&root.id).into_iter().cloned().collect();
1520            hierarchy.push((root.clone(), subtasks));
1521        }
1522
1523        Ok(hierarchy)
1524    }
1525
1526    /// List all descendant tasks for the given task.
1527    pub fn list_descendants(
1528        &self,
1529        session_id: &str,
1530        task_id: &str,
1531    ) -> Result<Vec<Task>, TaskError> {
1532        let task_list = self.get_or_load(session_id)?;
1533        if task_list.find_task(task_id).is_none() {
1534            return Err(TaskError::NotFound(task_id.to_string()));
1535        }
1536
1537        Ok(task_list
1538            .descendants(task_id)
1539            .into_iter()
1540            .cloned()
1541            .collect())
1542    }
1543
1544    /// Get a full recursive task tree for a session.
1545    pub fn get_task_tree(&self, session_id: &str) -> Result<Vec<TaskTreeNode>, TaskError> {
1546        let task_list = self.get_or_load(session_id)?;
1547        Ok(task_list.build_tree())
1548    }
1549
1550    /// Add a dependency relationship to a task.
1551    pub fn add_task_dependency(
1552        &self,
1553        session_id: &str,
1554        task_id: &str,
1555        blocked_by_id: &str,
1556    ) -> Result<(), TaskError> {
1557        let mut task_list = self.get_or_load(session_id)?;
1558        task_list.add_dependency(task_id, blocked_by_id)?;
1559        self.update_and_save(task_list)
1560    }
1561
1562    /// Update background job state for a task.
1563    pub fn set_task_background_job(
1564        &self,
1565        session_id: &str,
1566        task_id: &str,
1567        job: Option<TaskBackgroundJob>,
1568    ) -> Result<(), TaskError> {
1569        let mut task_list = self.get_or_load(session_id)?;
1570        let task = task_list
1571            .find_task_mut(task_id)
1572            .ok_or_else(|| TaskError::NotFound(task_id.to_string()))?;
1573        task.set_background_job(job);
1574        self.update_and_save(task_list)
1575    }
1576
1577    /// Create a task from an agent (during LLM processing)
1578    pub fn create_agent_task(
1579        &self,
1580        session_id: &str,
1581        name: impl Into<String>,
1582        description: impl Into<String>,
1583        agent_id: Option<String>,
1584        parent_id: Option<String>,
1585    ) -> Result<Task, TaskError> {
1586        let mut task_list = self.get_or_load(session_id)?;
1587        let task = Task::new_with_source(
1588            session_id,
1589            name,
1590            description,
1591            parent_id,
1592            TaskSource::Agent,
1593            agent_id,
1594        );
1595        task_list.add_task(task.clone());
1596        self.update_and_save(task_list)?;
1597        Ok(task)
1598    }
1599
1600    /// Create a task from an orchestrator delegated task
1601    pub fn create_orchestrator_task(
1602        &self,
1603        session_id: &str,
1604        orchestrator_task_id: impl Into<String>,
1605        agent_id: impl Into<String>,
1606        name: impl Into<String>,
1607        description: impl Into<String>,
1608        context: Option<serde_json::Value>,
1609    ) -> Result<Task, TaskError> {
1610        let mut task_list = self.get_or_load(session_id)?;
1611        let task = Task::from_orchestrator_task(
1612            session_id,
1613            orchestrator_task_id,
1614            agent_id,
1615            name,
1616            description,
1617            context,
1618        );
1619        task_list.add_task(task.clone());
1620        self.update_and_save(task_list)?;
1621        Ok(task)
1622    }
1623
1624    /// Find a task by its orchestrator_task_id
1625    pub fn find_by_orchestrator_id(
1626        &self,
1627        session_id: &str,
1628        orchestrator_task_id: &str,
1629    ) -> Result<Option<Task>, TaskError> {
1630        let task_list = self.get_or_load(session_id)?;
1631        Ok(task_list
1632            .tasks
1633            .iter()
1634            .find(|t| t.orchestrator_task_id.as_deref() == Some(orchestrator_task_id))
1635            .cloned())
1636    }
1637
1638    /// Update a task's metadata
1639    pub fn update_task_metadata(
1640        &self,
1641        session_id: &str,
1642        task_id: &str,
1643        metadata: serde_json::Value,
1644    ) -> Result<(), TaskError> {
1645        let mut task_list = self.get_or_load(session_id)?;
1646        let task = task_list
1647            .find_task_mut(task_id)
1648            .ok_or_else(|| TaskError::NotFound(task_id.to_string()))?;
1649        task.set_metadata(metadata);
1650        self.update_and_save(task_list)?;
1651        Ok(())
1652    }
1653
1654    /// Materialize a requirement breakdown into persisted agent-created tasks.
1655    pub fn materialize_requirement_breakdown(
1656        &self,
1657        session_id: &str,
1658        specs: &[RequirementBreakdownTaskSpec],
1659    ) -> Result<MaterializedTaskBreakdown, TaskError> {
1660        self.materialize_requirement_breakdown_internal(session_id, None, specs)
1661    }
1662
1663    /// Materialize a requirement breakdown beneath an existing tracked root task.
1664    pub fn materialize_requirement_breakdown_under_parent(
1665        &self,
1666        session_id: &str,
1667        parent_task_id: &str,
1668        specs: &[RequirementBreakdownTaskSpec],
1669    ) -> Result<MaterializedTaskBreakdown, TaskError> {
1670        let parent = self
1671            .get_task(session_id, parent_task_id)?
1672            .ok_or_else(|| TaskError::NotFound(parent_task_id.to_string()))?;
1673        if parent.session_id != session_id {
1674            return Err(TaskError::InvalidInput(format!(
1675                "parent task '{}' does not belong to session '{}'",
1676                parent_task_id, session_id
1677            )));
1678        }
1679
1680        self.materialize_requirement_breakdown_internal(
1681            session_id,
1682            Some(parent_task_id.to_string()),
1683            specs,
1684        )
1685    }
1686
1687    /// Create a tracked root task and materialize a shared execution plan under it.
1688    pub fn initialize_auto_tracked_execution_plan(
1689        &self,
1690        session_id: &str,
1691        root_task_name: &str,
1692        original_input: &str,
1693        specs: &[RequirementBreakdownTaskSpec],
1694    ) -> Result<AutoTrackedExecutionPlan, TaskError> {
1695        let root_task = self.create_task(
1696            session_id,
1697            root_task_name,
1698            format_auto_tracked_root_description(original_input),
1699            None,
1700        )?;
1701        self.update_task_status(session_id, &root_task.id, TaskStatus::InProgress)?;
1702
1703        let breakdown =
1704            self.materialize_requirement_breakdown_under_parent(session_id, &root_task.id, specs)?;
1705
1706        let open_descendants = self
1707            .list_descendants(session_id, &root_task.id)?
1708            .into_iter()
1709            .filter(|task| !task.is_terminal())
1710            .collect::<Vec<_>>();
1711        let initial_task = open_descendants.first().cloned();
1712        let current_task_id = initial_task
1713            .as_ref()
1714            .map(|task| task.id.clone())
1715            .unwrap_or_else(|| root_task.id.clone());
1716
1717        if let Some(task) = initial_task.as_ref() {
1718            self.update_task_status(session_id, &task.id, TaskStatus::InProgress)?;
1719        }
1720        self.set_current_task_id(session_id, Some(current_task_id))?;
1721
1722        self.record_memory_event(
1723            session_id,
1724            &root_task.id,
1725            TaskMemoryEvent::new(
1726                TaskMemoryPhase::Handoff,
1727                format!(
1728                    "Initialized tracked request with {} planned tracked subtasks",
1729                    breakdown.created_tasks.len()
1730                ),
1731                Some("session".to_string()),
1732                Some("handoff".to_string()),
1733                None,
1734            ),
1735        )?;
1736
1737        Ok(AutoTrackedExecutionPlan {
1738            root_task,
1739            planned_subtasks: open_descendants
1740                .iter()
1741                .take(8)
1742                .map(format_planned_subtask_label)
1743                .collect(),
1744            initial_task_id: initial_task.as_ref().map(|task| task.id.clone()),
1745            initial_task_name: initial_task.as_ref().map(|task| task.name.clone()),
1746            generated_task_count: breakdown.created_tasks.len(),
1747        })
1748    }
1749
1750    fn materialize_requirement_breakdown_internal(
1751        &self,
1752        session_id: &str,
1753        parent_task_id: Option<String>,
1754        specs: &[RequirementBreakdownTaskSpec],
1755    ) -> Result<MaterializedTaskBreakdown, TaskError> {
1756        let normalized = normalize_requirement_breakdown_specs(specs.to_vec())?;
1757        let mut task_list = self.get_or_load(session_id)?;
1758        let mut created_tasks = Vec::with_capacity(normalized.len());
1759        let mut root_task_ids = Vec::new();
1760        let mut name_to_id: HashMap<String, String> = HashMap::new();
1761        let mut pending = normalized;
1762        let mut modified = false;
1763
1764        while !pending.is_empty() {
1765            let mut progressed = false;
1766            let mut deferred = Vec::new();
1767
1768            for spec in pending {
1769                let resolved_parent_id = match spec.parent_name.as_deref() {
1770                    Some(parent_name) => match name_to_id.get(parent_name) {
1771                        Some(parent_id) => Some(parent_id.clone()),
1772                        None => {
1773                            deferred.push(spec);
1774                            continue;
1775                        }
1776                    },
1777                    None => parent_task_id.clone(),
1778                };
1779
1780                let task = if let Some(existing_task) = Self::find_existing_materialized_task(
1781                    &task_list,
1782                    resolved_parent_id.as_deref(),
1783                    &spec.name,
1784                ) {
1785                    existing_task
1786                } else {
1787                    let task = Task::new_with_source(
1788                        session_id,
1789                        spec.name.clone(),
1790                        spec.render_description(),
1791                        resolved_parent_id,
1792                        TaskSource::Agent,
1793                        None,
1794                    );
1795                    task_list.add_task(task.clone());
1796                    modified = true;
1797                    task
1798                };
1799
1800                if spec.parent_name.is_none() {
1801                    root_task_ids.push(task.id.clone());
1802                }
1803
1804                name_to_id.insert(spec.name, task.id.clone());
1805                created_tasks.push(task);
1806                progressed = true;
1807            }
1808
1809            if !progressed {
1810                let unresolved = deferred
1811                    .into_iter()
1812                    .map(|spec| {
1813                        format!(
1814                            "{} -> {}",
1815                            spec.name,
1816                            spec.parent_name.unwrap_or_else(|| "<none>".to_string())
1817                        )
1818                    })
1819                    .collect::<Vec<_>>()
1820                    .join(", ");
1821                return Err(TaskError::InvalidInput(format!(
1822                    "requirement breakdown contains unresolved parent references: {unresolved}"
1823                )));
1824            }
1825
1826            pending = deferred;
1827        }
1828
1829        if modified {
1830            self.update_and_save(task_list)?;
1831        }
1832
1833        Ok(MaterializedTaskBreakdown {
1834            created_tasks,
1835            root_task_ids,
1836        })
1837    }
1838
1839    /// Cancel any descendant tasks that are still open.
1840    pub fn cancel_open_descendants(
1841        &self,
1842        session_id: &str,
1843        task_id: &str,
1844    ) -> Result<Vec<String>, TaskError> {
1845        let descendants = self.list_descendants(session_id, task_id)?;
1846        let mut cancelled = Vec::new();
1847
1848        for descendant in descendants {
1849            if matches!(
1850                descendant.status,
1851                TaskStatus::Completed | TaskStatus::Cancelled
1852            ) {
1853                continue;
1854            }
1855
1856            self.update_task_status(session_id, &descendant.id, TaskStatus::Cancelled)?;
1857            cancelled.push(descendant.id);
1858        }
1859
1860        Ok(cancelled)
1861    }
1862
1863    /// Record the result metadata for a task and update its status.
1864    pub fn record_task_result(
1865        &self,
1866        session_id: &str,
1867        task_id: &str,
1868        success: bool,
1869        output: String,
1870        tool_calls: i32,
1871        duration_ms: i32,
1872    ) -> Result<TaskStatus, TaskError> {
1873        let task = self
1874            .get_task(session_id, task_id)?
1875            .ok_or_else(|| TaskError::NotFound(task_id.to_string()))?;
1876
1877        let mut metadata_map = match task.metadata {
1878            Some(serde_json::Value::Object(map)) => map,
1879            _ => serde_json::Map::new(),
1880        };
1881        metadata_map.insert("success".to_string(), serde_json::Value::Bool(success));
1882        metadata_map.insert("output".to_string(), serde_json::Value::String(output));
1883        metadata_map.insert(
1884            "tool_calls".to_string(),
1885            serde_json::Value::Number(serde_json::Number::from(tool_calls)),
1886        );
1887        metadata_map.insert(
1888            "duration_ms".to_string(),
1889            serde_json::Value::Number(serde_json::Number::from(duration_ms)),
1890        );
1891
1892        let target_status = if success {
1893            TaskStatus::Completed
1894        } else {
1895            TaskStatus::Cancelled
1896        };
1897
1898        self.update_task_metadata(session_id, task_id, serde_json::Value::Object(metadata_map))?;
1899        self.update_task_status(session_id, task_id, target_status)?;
1900        Ok(target_status)
1901    }
1902
1903    /// Reconcile a tracked task after an agent run by examining descendant state.
1904    pub fn finalize_tracked_task_after_agent_run(
1905        &self,
1906        session_id: &str,
1907        task_id: &str,
1908    ) -> Result<TrackedTaskFinalization, TaskError> {
1909        let current_status = self
1910            .get_task(session_id, task_id)?
1911            .ok_or_else(|| TaskError::NotFound(task_id.to_string()))?
1912            .status;
1913        let descendants = self.list_descendants(session_id, task_id)?;
1914        let open_subtask_ids = descendants
1915            .into_iter()
1916            .filter(|task| !matches!(task.status, TaskStatus::Completed | TaskStatus::Cancelled))
1917            .map(|task| task.id)
1918            .collect::<Vec<_>>();
1919
1920        if open_subtask_ids.is_empty() {
1921            self.update_task_status(session_id, task_id, TaskStatus::Completed)?;
1922            if self.get_current_task_id(session_id)?.as_deref() == Some(task_id) {
1923                self.set_current_task_id(session_id, None)?;
1924            }
1925            Ok(TrackedTaskFinalization::Completed)
1926        } else {
1927            if !matches!(
1928                current_status,
1929                TaskStatus::Completed | TaskStatus::Cancelled
1930            ) {
1931                self.update_task_status(session_id, task_id, TaskStatus::InProgress)?;
1932            }
1933            Ok(TrackedTaskFinalization::StillInProgress { open_subtask_ids })
1934        }
1935    }
1936
1937    /// Record a structured memory lifecycle event in task metadata.
1938    pub fn record_memory_event(
1939        &self,
1940        session_id: &str,
1941        task_id: &str,
1942        event: TaskMemoryEvent,
1943    ) -> Result<(), TaskError> {
1944        let mut task_list = self.get_or_load(session_id)?;
1945        let task = task_list
1946            .find_task_mut(task_id)
1947            .ok_or_else(|| TaskError::NotFound(task_id.to_string()))?;
1948
1949        let metadata = task.metadata.get_or_insert_with(|| serde_json::json!({}));
1950        if !metadata.is_object() {
1951            *metadata = serde_json::json!({});
1952        }
1953
1954        let Some(metadata_map) = metadata.as_object_mut() else {
1955            return Err(TaskError::InvalidInput(
1956                "task metadata could not be represented as an object".to_string(),
1957            ));
1958        };
1959
1960        let mut lifecycle: TaskMemoryLifecycle = metadata_map
1961            .get("memory_lifecycle")
1962            .cloned()
1963            .map(serde_json::from_value)
1964            .transpose()?
1965            .unwrap_or_default();
1966
1967        if let Some(memory_file_path) = event.memory_file_path.clone() {
1968            lifecycle.last_memory_file_path = Some(memory_file_path);
1969        }
1970        lifecycle.events.push(event);
1971        if lifecycle.events.len() > 32 {
1972            let excess = lifecycle.events.len() - 32;
1973            lifecycle.events.drain(0..excess);
1974        }
1975
1976        metadata_map.insert(
1977            "memory_lifecycle".to_string(),
1978            serde_json::to_value(&lifecycle)?,
1979        );
1980        task.updated_at = Utc::now();
1981
1982        self.update_and_save(task_list)?;
1983        Ok(())
1984    }
1985
1986    /// Read structured memory lifecycle data from task metadata.
1987    pub fn get_memory_lifecycle(
1988        &self,
1989        session_id: &str,
1990        task_id: &str,
1991    ) -> Result<Option<TaskMemoryLifecycle>, TaskError> {
1992        let task = self.get_task(session_id, task_id)?;
1993        let Some(task) = task else {
1994            return Ok(None);
1995        };
1996        let Some(metadata) = task.metadata else {
1997            return Ok(None);
1998        };
1999        let Some(value) = metadata.get("memory_lifecycle") else {
2000            return Ok(None);
2001        };
2002
2003        Ok(Some(serde_json::from_value(value.clone())?))
2004    }
2005
2006    /// Update the runtime execution state stored in task metadata.
2007    pub fn update_execution_state<F>(
2008        &self,
2009        session_id: &str,
2010        task_id: &str,
2011        update: F,
2012    ) -> Result<TaskExecutionState, TaskError>
2013    where
2014        F: FnOnce(&mut TaskExecutionState),
2015    {
2016        let mut task_list = self.get_or_load(session_id)?;
2017        let task = task_list
2018            .find_task_mut(task_id)
2019            .ok_or_else(|| TaskError::NotFound(task_id.to_string()))?;
2020
2021        let metadata = task.metadata.get_or_insert_with(|| serde_json::json!({}));
2022        if !metadata.is_object() {
2023            *metadata = serde_json::json!({});
2024        }
2025
2026        let Some(metadata_map) = metadata.as_object_mut() else {
2027            return Err(TaskError::InvalidInput(
2028                "task metadata could not be represented as an object".to_string(),
2029            ));
2030        };
2031
2032        let mut state: TaskExecutionState = metadata_map
2033            .get("execution_state")
2034            .cloned()
2035            .map(serde_json::from_value)
2036            .transpose()?
2037            .unwrap_or_default();
2038
2039        update(&mut state);
2040
2041        metadata_map.insert("execution_state".to_string(), serde_json::to_value(&state)?);
2042        task.updated_at = Utc::now();
2043        self.update_and_save(task_list)?;
2044        Ok(state)
2045    }
2046
2047    /// Read runtime execution state from task metadata.
2048    pub fn get_execution_state(
2049        &self,
2050        session_id: &str,
2051        task_id: &str,
2052    ) -> Result<Option<TaskExecutionState>, TaskError> {
2053        let task = self.get_task(session_id, task_id)?;
2054        let Some(task) = task else {
2055            return Ok(None);
2056        };
2057        let Some(metadata) = task.metadata else {
2058            return Ok(None);
2059        };
2060        let Some(value) = metadata.get("execution_state") else {
2061            return Ok(None);
2062        };
2063
2064        Ok(Some(serde_json::from_value(value.clone())?))
2065    }
2066
2067    /// Get a specific task by ID
2068    pub fn get_task(&self, session_id: &str, task_id: &str) -> Result<Option<Task>, TaskError> {
2069        let task_list = self.get_or_load(session_id)?;
2070        Ok(task_list.find_task(task_id).cloned())
2071    }
2072}
2073
2074/// Process-wide global [`TaskManager`] instance.
2075///
2076/// All subsystems — Tauri commands, orchestrator observer, and the agent tool
2077/// pipeline — **must** use this single instance so they share one in-memory
2078/// cache and avoid stale-read bugs caused by independent `OnceLock` statics.
2079static GLOBAL_TASK_MANAGER: std::sync::OnceLock<TaskManager> = std::sync::OnceLock::new();
2080
2081/// Returns the process-wide shared [`TaskManager`].
2082///
2083/// Initializes with `~/.gestura/tasks/` on first call; subsequent calls return
2084/// the same instance.  Callers must **not** create their own `OnceLock<TaskManager>`
2085/// statics — doing so produces independent in-memory caches that cause stale
2086/// reads when a different subsystem writes tasks to disk.
2087pub fn get_global_task_manager() -> &'static TaskManager {
2088    GLOBAL_TASK_MANAGER.get_or_init(|| TaskManager::new(gestura_home_dir()))
2089}
2090
2091#[cfg(test)]
2092mod tests {
2093    use super::*;
2094    use tempfile::TempDir;
2095
2096    #[test]
2097    fn test_task_creation() {
2098        let task = Task::new("session-123", "Test Task", "Test description", None);
2099        assert_eq!(task.session_id, "session-123");
2100        assert_eq!(task.name, "Test Task");
2101        assert_eq!(task.description, "Test description");
2102        assert_eq!(task.status, TaskStatus::NotStarted);
2103        assert!(task.parent_id.is_none());
2104        assert!(task.blocked_by.is_empty());
2105        assert!(task.background_job.is_none());
2106        assert!(!task.id.is_empty());
2107    }
2108
2109    #[test]
2110    fn test_task_status_update() {
2111        let mut task = Task::new("session-123", "Test Task", "Test description", None);
2112        let original_updated_at = task.updated_at;
2113
2114        // Small delay to ensure timestamp changes
2115        std::thread::sleep(std::time::Duration::from_millis(10));
2116
2117        task.set_status(TaskStatus::InProgress);
2118        assert_eq!(task.status, TaskStatus::InProgress);
2119        assert!(task.updated_at > original_updated_at);
2120    }
2121
2122    #[test]
2123    fn test_task_list_operations() {
2124        let mut list = TaskList::new("session-123");
2125        assert_eq!(list.tasks.len(), 0);
2126        assert!(list.current_task_id().is_none());
2127
2128        let task1 = Task::new("session-123", "Task 1", "Description 1", None);
2129        let task2 = Task::new(
2130            "session-123",
2131            "Task 2",
2132            "Description 2",
2133            Some(task1.id.clone()),
2134        );
2135
2136        list.add_task(task1.clone());
2137        list.add_task(task2.clone());
2138        assert_eq!(list.tasks.len(), 2);
2139
2140        // Setting current task requires it to exist
2141        assert!(list.set_current_task_id(Some(task1.id.clone())).is_ok());
2142        assert_eq!(list.current_task_id(), Some(task1.id.as_str()));
2143        assert!(matches!(
2144            list.set_current_task_id(Some("does-not-exist".to_string())),
2145            Err(TaskError::InvalidInput(_))
2146        ));
2147
2148        // Test find
2149        let found = list.find_task(&task1.id);
2150        assert!(found.is_some());
2151        assert_eq!(found.unwrap().name, "Task 1");
2152
2153        // Test root tasks
2154        let roots = list.root_tasks();
2155        assert_eq!(roots.len(), 1);
2156        assert_eq!(roots[0].id, task1.id);
2157
2158        // Test subtasks
2159        let subtasks = list.subtasks(&task1.id);
2160        assert_eq!(subtasks.len(), 1);
2161        assert_eq!(subtasks[0].id, task2.id);
2162
2163        // Test remove
2164        let removed = list.remove_task(&task1.id);
2165        assert!(removed.is_some());
2166        assert_eq!(list.tasks.len(), 1);
2167        // removing the current task clears the pointer
2168        assert!(list.current_task_id().is_none());
2169    }
2170
2171    #[test]
2172    fn test_task_current_pointer_roundtrip() {
2173        let temp_dir = TempDir::new().unwrap();
2174
2175        let session_id = "session-123";
2176        let task_id = {
2177            let manager = TaskManager::new(temp_dir.path());
2178            let task = manager
2179                .create_task(session_id, "Test Task", "Test description", None)
2180                .unwrap();
2181            manager
2182                .set_current_task_id(session_id, Some(task.id.clone()))
2183                .unwrap();
2184            task.id
2185        };
2186
2187        let manager = TaskManager::new(temp_dir.path());
2188        let loaded = manager.get_current_task_id(session_id).unwrap();
2189        assert_eq!(loaded, Some(task_id));
2190    }
2191
2192    #[test]
2193    fn test_task_replace_task_list() {
2194        let temp_dir = TempDir::new().unwrap();
2195        let manager = TaskManager::new(temp_dir.path());
2196
2197        let session_id = "session-123";
2198        let task = Task::new(session_id, "Task 1", "Description 1", None);
2199
2200        let mut list = TaskList::new(session_id);
2201        list.add_task(task.clone());
2202        list.set_current_task_id(Some(task.id.clone())).unwrap();
2203
2204        manager.replace_task_list(list).unwrap();
2205
2206        let loaded = manager.load_task_list(session_id).unwrap();
2207        assert_eq!(loaded.tasks.len(), 1);
2208        assert_eq!(loaded.current_task_id(), Some(task.id.as_str()));
2209    }
2210
2211    #[test]
2212    fn test_task_manager_create_and_list() {
2213        let temp_dir = TempDir::new().unwrap();
2214        let manager = TaskManager::new(temp_dir.path());
2215
2216        let task = manager
2217            .create_task("session-123", "Test Task", "Test description", None)
2218            .unwrap();
2219
2220        assert_eq!(task.name, "Test Task");
2221
2222        let tasks = manager.list_tasks("session-123").unwrap();
2223        assert_eq!(tasks.len(), 1);
2224        assert_eq!(tasks[0].id, task.id);
2225    }
2226
2227    #[test]
2228    fn test_task_manager_persistence() {
2229        let temp_dir = TempDir::new().unwrap();
2230
2231        // Create task with first manager instance
2232        {
2233            let manager = TaskManager::new(temp_dir.path());
2234            manager
2235                .create_task("session-123", "Test Task", "Test description", None)
2236                .unwrap();
2237        }
2238
2239        // Load with second manager instance
2240        {
2241            let manager = TaskManager::new(temp_dir.path());
2242            let tasks = manager.list_tasks("session-123").unwrap();
2243            assert_eq!(tasks.len(), 1);
2244            assert_eq!(tasks[0].name, "Test Task");
2245        }
2246    }
2247
2248    #[test]
2249    fn test_task_manager_update_status() {
2250        let temp_dir = TempDir::new().unwrap();
2251        let manager = TaskManager::new(temp_dir.path());
2252
2253        let task = manager
2254            .create_task("session-123", "Test Task", "Test description", None)
2255            .unwrap();
2256
2257        manager
2258            .update_task_status("session-123", &task.id, TaskStatus::InProgress)
2259            .unwrap();
2260
2261        let tasks = manager.list_tasks("session-123").unwrap();
2262        assert_eq!(tasks[0].status, TaskStatus::InProgress);
2263    }
2264
2265    #[test]
2266    fn test_task_manager_update_task() {
2267        let temp_dir = TempDir::new().unwrap();
2268        let manager = TaskManager::new(temp_dir.path());
2269
2270        let task = manager
2271            .create_task("session-123", "Test Task", "Test description", None)
2272            .unwrap();
2273
2274        manager
2275            .update_task(
2276                "session-123",
2277                &task.id,
2278                Some("Updated Task".to_string()),
2279                Some("Updated description".to_string()),
2280            )
2281            .unwrap();
2282
2283        let tasks = manager.list_tasks("session-123").unwrap();
2284        assert_eq!(tasks[0].name, "Updated Task");
2285        assert_eq!(tasks[0].description, "Updated description");
2286    }
2287
2288    #[test]
2289    fn test_task_manager_delete() {
2290        let temp_dir = TempDir::new().unwrap();
2291        let manager = TaskManager::new(temp_dir.path());
2292
2293        let task = manager
2294            .create_task("session-123", "Test Task", "Test description", None)
2295            .unwrap();
2296
2297        let deleted = manager.delete_task("session-123", &task.id).unwrap();
2298        assert_eq!(deleted.id, task.id);
2299
2300        let tasks = manager.list_tasks("session-123").unwrap();
2301        assert_eq!(tasks.len(), 0);
2302    }
2303
2304    #[test]
2305    fn test_task_manager_hierarchy() {
2306        let temp_dir = TempDir::new().unwrap();
2307        let manager = TaskManager::new(temp_dir.path());
2308
2309        let root = manager
2310            .create_task("session-123", "Root Task", "Root description", None)
2311            .unwrap();
2312
2313        manager
2314            .create_task(
2315                "session-123",
2316                "Subtask 1",
2317                "Subtask description",
2318                Some(root.id.clone()),
2319            )
2320            .unwrap();
2321
2322        manager
2323            .create_task(
2324                "session-123",
2325                "Subtask 2",
2326                "Subtask description",
2327                Some(root.id.clone()),
2328            )
2329            .unwrap();
2330
2331        let hierarchy = manager.get_hierarchy("session-123").unwrap();
2332        assert_eq!(hierarchy.len(), 1);
2333        assert_eq!(hierarchy[0].0.id, root.id);
2334        assert_eq!(hierarchy[0].1.len(), 2);
2335    }
2336
2337    #[test]
2338    fn test_task_dependencies_blocking() {
2339        let temp_dir = TempDir::new().unwrap();
2340        let manager = TaskManager::new(temp_dir.path());
2341
2342        let session_id = "session-123";
2343        let dep = manager
2344            .create_task(session_id, "Dep", "Dependency", None)
2345            .unwrap();
2346        let task = manager
2347            .create_task(session_id, "Task", "Work", None)
2348            .unwrap();
2349
2350        manager
2351            .add_task_dependency(session_id, &task.id, &dep.id)
2352            .unwrap();
2353
2354        let list = manager.load_task_list(session_id).unwrap();
2355        assert!(list.is_task_blocked(&task.id).unwrap());
2356
2357        manager
2358            .update_task_status(session_id, &dep.id, TaskStatus::Completed)
2359            .unwrap();
2360
2361        let list = manager.load_task_list(session_id).unwrap();
2362        assert!(!list.is_task_blocked(&task.id).unwrap());
2363    }
2364
2365    #[test]
2366    fn test_task_manager_rejects_completion_with_open_subtasks() {
2367        let temp_dir = TempDir::new().unwrap();
2368        let manager = TaskManager::new(temp_dir.path());
2369        let session_id = "session-123";
2370
2371        let parent = manager
2372            .create_task(session_id, "Build hello world", "Create the app", None)
2373            .unwrap();
2374        let child = manager
2375            .create_task(
2376                session_id,
2377                "Implement UI",
2378                "Render hello world in a Tauri window",
2379                Some(parent.id.clone()),
2380            )
2381            .unwrap();
2382
2383        let err = manager
2384            .update_task_status(session_id, &parent.id, TaskStatus::Completed)
2385            .unwrap_err();
2386
2387        assert!(matches!(err, TaskError::InvalidInput(_)));
2388        assert!(err.to_string().contains("Implement UI"));
2389
2390        manager
2391            .update_task_status(session_id, &child.id, TaskStatus::Completed)
2392            .unwrap();
2393        manager
2394            .update_task_status(session_id, &parent.id, TaskStatus::Completed)
2395            .unwrap();
2396    }
2397
2398    #[test]
2399    fn test_task_list_descendants_include_nested_children() {
2400        let mut list = TaskList::new("session-123");
2401        let root = Task::new("session-123", "Root", "Root", None);
2402        let child = Task::new("session-123", "Child", "Child", Some(root.id.clone()));
2403        let grandchild = Task::new(
2404            "session-123",
2405            "Grandchild",
2406            "Grandchild",
2407            Some(child.id.clone()),
2408        );
2409
2410        let root_id = root.id.clone();
2411        let child_id = child.id.clone();
2412        let grandchild_id = grandchild.id.clone();
2413
2414        list.add_task(root);
2415        list.add_task(child);
2416        list.add_task(grandchild);
2417
2418        let descendants = list.descendants(&root_id);
2419        assert_eq!(descendants.len(), 2);
2420        assert!(descendants.iter().any(|task| task.id == child_id));
2421        assert!(descendants.iter().any(|task| task.id == grandchild_id));
2422    }
2423
2424    #[test]
2425    fn test_task_list_descendants_preserve_priority_order_depth_first() {
2426        let mut list = TaskList::new("session-priority-123");
2427        let root = Task::new("session-priority-123", "Root", "Root", None);
2428        let root_id = root.id.clone();
2429
2430        let mut first = Task::new(
2431            "session-priority-123",
2432            "First",
2433            "First",
2434            Some(root.id.clone()),
2435        );
2436        first.sort_order = 0;
2437
2438        let mut second = Task::new(
2439            "session-priority-123",
2440            "Second",
2441            "Second",
2442            Some(root.id.clone()),
2443        );
2444        second.sort_order = 10;
2445
2446        let mut nested = Task::new(
2447            "session-priority-123",
2448            "Nested",
2449            "Nested",
2450            Some(first.id.clone()),
2451        );
2452        nested.sort_order = 0;
2453
2454        let expected = vec![first.id.clone(), nested.id.clone(), second.id.clone()];
2455
2456        list.add_task(root);
2457        list.add_task(first);
2458        list.add_task(second);
2459        list.add_task(nested);
2460
2461        let ordered = list
2462            .descendants(&root_id)
2463            .into_iter()
2464            .map(|task| task.id.clone())
2465            .collect::<Vec<_>>();
2466
2467        assert_eq!(ordered, expected);
2468    }
2469
2470    #[test]
2471    fn test_task_manager_rejects_completion_with_open_nested_descendants() {
2472        let temp_dir = TempDir::new().unwrap();
2473        let manager = TaskManager::new(temp_dir.path());
2474        let session_id = "session-nested-123";
2475
2476        let mut root = Task::new(session_id, "Root", "Root", None);
2477        let mut child = Task::new(session_id, "Child", "Child", Some(root.id.clone()));
2478        let grandchild = Task::new(
2479            session_id,
2480            "Grandchild",
2481            "Grandchild",
2482            Some(child.id.clone()),
2483        );
2484        child.set_status(TaskStatus::Completed);
2485        root.set_status(TaskStatus::InProgress);
2486
2487        let mut task_list = TaskList::new(session_id);
2488        task_list.add_task(root.clone());
2489        task_list.add_task(child);
2490        task_list.add_task(grandchild);
2491        manager.replace_task_list(task_list).unwrap();
2492
2493        let err = manager
2494            .update_task_status(session_id, &root.id, TaskStatus::Completed)
2495            .unwrap_err();
2496
2497        assert!(matches!(err, TaskError::InvalidInput(_)));
2498        assert!(err.to_string().contains("Grandchild"));
2499    }
2500
2501    #[test]
2502    fn test_task_manager_rejects_completion_while_blocked() {
2503        let temp_dir = TempDir::new().unwrap();
2504        let manager = TaskManager::new(temp_dir.path());
2505        let session_id = "session-123";
2506
2507        let plan = manager
2508            .create_task(session_id, "Plan work", "Break down the task", None)
2509            .unwrap();
2510        let implementation = manager
2511            .create_task(session_id, "Implement app", "Build the Tauri app", None)
2512            .unwrap();
2513
2514        manager
2515            .add_task_dependency(session_id, &implementation.id, &plan.id)
2516            .unwrap();
2517
2518        let err = manager
2519            .update_task_status(session_id, &implementation.id, TaskStatus::Completed)
2520            .unwrap_err();
2521
2522        assert!(matches!(err, TaskError::InvalidInput(_)));
2523        assert!(err.to_string().contains("dependencies remain open"));
2524
2525        manager
2526            .update_task_status(session_id, &plan.id, TaskStatus::Completed)
2527            .unwrap();
2528        manager
2529            .update_task_status(session_id, &implementation.id, TaskStatus::Completed)
2530            .unwrap();
2531    }
2532
2533    #[test]
2534    fn test_task_dependency_cycle_rejected() {
2535        let temp_dir = TempDir::new().unwrap();
2536        let manager = TaskManager::new(temp_dir.path());
2537        let session_id = "session-123";
2538
2539        let a = manager.create_task(session_id, "A", "A", None).unwrap();
2540        let b = manager.create_task(session_id, "B", "B", None).unwrap();
2541
2542        manager
2543            .add_task_dependency(session_id, &a.id, &b.id)
2544            .unwrap();
2545
2546        let err = manager
2547            .add_task_dependency(session_id, &b.id, &a.id)
2548            .unwrap_err();
2549
2550        assert!(matches!(err, TaskError::InvalidInput(_)));
2551    }
2552
2553    #[test]
2554    fn test_task_tree_recursive() {
2555        let temp_dir = TempDir::new().unwrap();
2556        let manager = TaskManager::new(temp_dir.path());
2557        let session_id = "session-123";
2558
2559        let root = manager
2560            .create_task(session_id, "Root", "Root", None)
2561            .unwrap();
2562        let child = manager
2563            .create_task(session_id, "Child", "Child", Some(root.id.clone()))
2564            .unwrap();
2565        manager
2566            .create_task(session_id, "Grand", "Grand", Some(child.id.clone()))
2567            .unwrap();
2568
2569        let tree = manager.get_task_tree(session_id).unwrap();
2570        assert_eq!(tree.len(), 1);
2571        assert_eq!(tree[0].task.id, root.id);
2572        assert_eq!(tree[0].children.len(), 1);
2573        assert_eq!(tree[0].children[0].task.id, child.id);
2574        assert_eq!(tree[0].children[0].children.len(), 1);
2575    }
2576
2577    #[test]
2578    fn test_record_memory_event() {
2579        let temp_dir = TempDir::new().unwrap();
2580        let manager = TaskManager::new(temp_dir.path());
2581        let session_id = "session-123";
2582
2583        let task = manager
2584            .create_task(session_id, "Memory Task", "Track delegation", None)
2585            .unwrap();
2586
2587        manager
2588            .record_memory_event(
2589                session_id,
2590                &task.id,
2591                TaskMemoryEvent::new(
2592                    TaskMemoryPhase::Delegated,
2593                    "Delegated to subagent",
2594                    Some("directive".to_string()),
2595                    Some("handoff".to_string()),
2596                    None,
2597                ),
2598            )
2599            .unwrap();
2600
2601        let lifecycle = manager
2602            .get_memory_lifecycle(session_id, &task.id)
2603            .unwrap()
2604            .unwrap();
2605
2606        assert_eq!(lifecycle.events.len(), 1);
2607        assert_eq!(lifecycle.events[0].phase, TaskMemoryPhase::Delegated);
2608        assert_eq!(lifecycle.events[0].summary, "Delegated to subagent");
2609    }
2610
2611    #[test]
2612    fn test_update_execution_state_records_profile_and_evidence() {
2613        let temp_dir = TempDir::new().unwrap();
2614        let manager = TaskManager::new(temp_dir.path());
2615        let session_id = "session-execution-state";
2616
2617        let task = manager
2618            .create_task(session_id, "Implement feature", "Edit and verify", None)
2619            .unwrap();
2620
2621        manager
2622            .update_execution_state(session_id, &task.id, |state| {
2623                state.merge_profile(TaskVerificationProfile {
2624                    execution_kind: TaskExecutionKind::Implementation,
2625                    requires_mutation: true,
2626                    ..TaskVerificationProfile::default()
2627                });
2628                state.record_evidence(TaskExecutionEvidence::new(
2629                    TaskExecutionEvidenceKind::Mutation,
2630                    "Edited src/main.rs",
2631                    Some("file".to_string()),
2632                    None,
2633                ));
2634            })
2635            .unwrap();
2636
2637        let state = manager
2638            .get_execution_state(session_id, &task.id)
2639            .unwrap()
2640            .unwrap();
2641
2642        assert_eq!(
2643            state.verification_profile.execution_kind,
2644            TaskExecutionKind::Implementation
2645        );
2646        assert!(state.verification_profile.requires_mutation);
2647        assert!(state.saw_mutation);
2648        assert!(state.satisfies_profile());
2649        assert_eq!(state.evidence.len(), 1);
2650    }
2651
2652    #[test]
2653    fn generic_verification_profile_requires_observed_progress() {
2654        let mut state = TaskExecutionState::default();
2655        state.merge_profile(TaskVerificationProfile {
2656            execution_kind: TaskExecutionKind::Verification,
2657            ..TaskVerificationProfile::default()
2658        });
2659
2660        assert!(!state.satisfies_profile());
2661
2662        state.record_evidence(TaskExecutionEvidence::new(
2663            TaskExecutionEvidenceKind::ToolActivity,
2664            "Reviewed the generated artifact and cross-checked the facts",
2665            Some("web_search".to_string()),
2666            None,
2667        ));
2668
2669        assert!(state.satisfies_profile());
2670    }
2671
2672    #[test]
2673    fn failed_build_evidence_counts_as_progress_without_counting_as_success() {
2674        let mut state = TaskExecutionState::default();
2675        state.merge_profile(TaskVerificationProfile {
2676            execution_kind: TaskExecutionKind::Verification,
2677            requires_build: true,
2678            ..TaskVerificationProfile::default()
2679        });
2680
2681        state.record_evidence(
2682            TaskExecutionEvidence::new(
2683                TaskExecutionEvidenceKind::Build,
2684                "cargo check failed with compiler errors",
2685                Some("shell".to_string()),
2686                Some("cargo check".to_string()),
2687            )
2688            .with_success(false),
2689        );
2690
2691        assert!(state.saw_tool_activity);
2692        assert!(!state.build_succeeded);
2693        assert!(!state.satisfies_profile());
2694        assert_eq!(state.evidence.len(), 1);
2695        assert!(!state.evidence[0].success);
2696    }
2697
2698    #[test]
2699    fn failed_mutation_evidence_does_not_mark_mutation_requirement_complete() {
2700        let mut state = TaskExecutionState::default();
2701        state.merge_profile(TaskVerificationProfile {
2702            execution_kind: TaskExecutionKind::Implementation,
2703            requires_mutation: true,
2704            ..TaskVerificationProfile::default()
2705        });
2706
2707        state.record_evidence(
2708            TaskExecutionEvidence::new(
2709                TaskExecutionEvidenceKind::Mutation,
2710                "Attempted to edit src/main.rs but the patch failed",
2711                Some("code".to_string()),
2712                None,
2713            )
2714            .with_success(false),
2715        );
2716
2717        assert!(state.saw_tool_activity);
2718        assert!(!state.saw_mutation);
2719        assert!(!state.satisfies_profile());
2720        assert!(!state.evidence[0].success);
2721    }
2722
2723    #[test]
2724    fn contradiction_and_blocker_evidence_are_tracked_as_diagnostic_progress() {
2725        let mut state = TaskExecutionState::default();
2726
2727        state.record_evidence(TaskExecutionEvidence::new(
2728            TaskExecutionEvidenceKind::Contradiction,
2729            "Observed result contradicted the expected outcome",
2730            Some("shell".to_string()),
2731            Some("curl -I http://localhost:3000/missing".to_string()),
2732        ));
2733        state.record_evidence(TaskExecutionEvidence::new(
2734            TaskExecutionEvidenceKind::Blocker,
2735            "Verification is blocked on a missing dependency",
2736            Some("shell".to_string()),
2737            Some("npm test".to_string()),
2738        ));
2739
2740        assert!(state.saw_tool_activity);
2741        assert!(state.saw_diagnostic_progress);
2742        assert!(state.saw_contradiction);
2743        assert!(state.saw_blocker);
2744        assert_eq!(state.evidence.len(), 2);
2745    }
2746
2747    #[test]
2748    fn external_verification_profile_requires_post_mutation_external_evidence() {
2749        let mut state = TaskExecutionState::default();
2750        state.merge_profile(TaskVerificationProfile {
2751            execution_kind: TaskExecutionKind::Verification,
2752            requires_external_evidence: true,
2753            ..TaskVerificationProfile::default()
2754        });
2755
2756        state.record_evidence(TaskExecutionEvidence::new(
2757            TaskExecutionEvidenceKind::ToolActivity,
2758            "Reviewed local output",
2759            Some("read_file".to_string()),
2760            None,
2761        ));
2762        assert!(!state.satisfies_profile());
2763
2764        state.record_evidence(TaskExecutionEvidence::new(
2765            TaskExecutionEvidenceKind::ToolActivity,
2766            "Cross-checked an earlier source",
2767            Some("web_search".to_string()),
2768            None,
2769        ));
2770        assert!(state.satisfies_profile());
2771
2772        state.record_evidence(TaskExecutionEvidence::new(
2773            TaskExecutionEvidenceKind::Mutation,
2774            "Updated the draft after review",
2775            Some("file".to_string()),
2776            None,
2777        ));
2778        assert!(!state.satisfies_profile());
2779
2780        state.record_evidence(TaskExecutionEvidence::new(
2781            TaskExecutionEvidenceKind::ToolActivity,
2782            "Cross-checked the updated draft against outside sources",
2783            Some("web_search".to_string()),
2784            None,
2785        ));
2786        assert!(state.satisfies_profile());
2787    }
2788
2789    #[test]
2790    fn launch_verification_profile_requires_post_mutation_launch_evidence() {
2791        let mut state = TaskExecutionState::default();
2792        state.merge_profile(TaskVerificationProfile {
2793            execution_kind: TaskExecutionKind::Verification,
2794            requires_launch_evidence: true,
2795            ..TaskVerificationProfile::default()
2796        });
2797
2798        state.record_evidence(TaskExecutionEvidence::new(
2799            TaskExecutionEvidenceKind::ToolActivity,
2800            "Reviewed logs only",
2801            Some("read_file".to_string()),
2802            None,
2803        ));
2804        assert!(!state.satisfies_profile());
2805
2806        state.record_evidence(TaskExecutionEvidence::new(
2807            TaskExecutionEvidenceKind::ToolActivity,
2808            "Launched the app in dev mode",
2809            Some("shell".to_string()),
2810            Some("cargo tauri dev".to_string()),
2811        ));
2812        assert!(state.satisfies_profile());
2813
2814        state.record_evidence(TaskExecutionEvidence::new(
2815            TaskExecutionEvidenceKind::Mutation,
2816            "Updated the app after launch verification",
2817            Some("file".to_string()),
2818            None,
2819        ));
2820        assert!(!state.satisfies_profile());
2821
2822        state.record_evidence(TaskExecutionEvidence::new(
2823            TaskExecutionEvidenceKind::ToolActivity,
2824            "Re-launched the app after the update",
2825            Some("shell".to_string()),
2826            Some("cargo tauri dev --no-watch".to_string()),
2827        ));
2828        assert!(state.satisfies_profile());
2829    }
2830
2831    #[test]
2832    fn materialize_requirement_breakdown_creates_parent_child_tasks() {
2833        let temp_dir = TempDir::new().unwrap();
2834        let manager = TaskManager::new(temp_dir.path());
2835
2836        let breakdown = manager
2837            .materialize_requirement_breakdown(
2838                "session-123",
2839                &[
2840                    RequirementBreakdownTaskSpec {
2841                        name: "Root".to_string(),
2842                        description: "Top-level work".to_string(),
2843                        priority: "high".to_string(),
2844                        is_blocking: true,
2845                        parent_name: None,
2846                    },
2847                    RequirementBreakdownTaskSpec {
2848                        name: "Child".to_string(),
2849                        description: "Nested work".to_string(),
2850                        priority: "medium".to_string(),
2851                        is_blocking: false,
2852                        parent_name: Some("Root".to_string()),
2853                    },
2854                ],
2855            )
2856            .unwrap();
2857
2858        assert_eq!(breakdown.created_tasks.len(), 2);
2859        assert_eq!(breakdown.root_task_ids.len(), 1);
2860        assert_eq!(breakdown.created_tasks[0].name, "Root");
2861        assert!(
2862            breakdown.created_tasks[0]
2863                .description
2864                .contains("Priority: high")
2865        );
2866        assert!(
2867            breakdown.created_tasks[0]
2868                .description
2869                .contains("Blocking: yes")
2870        );
2871        assert_eq!(
2872            breakdown.created_tasks[1].parent_id.as_deref(),
2873            Some(breakdown.created_tasks[0].id.as_str())
2874        );
2875    }
2876
2877    #[test]
2878    fn parse_requirement_breakdown_response_rejects_unknown_parent_names() {
2879        let error = parse_requirement_breakdown_response(
2880            r#"[
2881                {"name":"Root","description":"Plan","priority":"high","is_blocking":true,"parent_name":null},
2882                {"name":"Child","description":"Implement","priority":"medium","is_blocking":false,"parent_name":"Missing"}
2883            ]"#,
2884        )
2885        .unwrap_err();
2886
2887        assert!(
2888            error
2889                .to_string()
2890                .contains("references unknown parent 'Missing'")
2891        );
2892    }
2893
2894    #[test]
2895    fn materialize_requirement_breakdown_under_parent_handles_out_of_order_specs() {
2896        let temp_dir = TempDir::new().unwrap();
2897        let manager = TaskManager::new(temp_dir.path());
2898        let root = manager
2899            .create_task("session-123", "Root", "Tracked root", None)
2900            .unwrap();
2901
2902        let breakdown = manager
2903            .materialize_requirement_breakdown_under_parent(
2904                "session-123",
2905                &root.id,
2906                &[
2907                    RequirementBreakdownTaskSpec {
2908                        name: "Child".to_string(),
2909                        description: "Implement child".to_string(),
2910                        priority: "medium".to_string(),
2911                        is_blocking: false,
2912                        parent_name: Some("Parent".to_string()),
2913                    },
2914                    RequirementBreakdownTaskSpec {
2915                        name: "Parent".to_string(),
2916                        description: "Create parent".to_string(),
2917                        priority: "high".to_string(),
2918                        is_blocking: true,
2919                        parent_name: None,
2920                    },
2921                ],
2922            )
2923            .unwrap();
2924
2925        assert_eq!(breakdown.root_task_ids.len(), 1);
2926        let parent = breakdown
2927            .created_tasks
2928            .iter()
2929            .find(|task| task.name == "Parent")
2930            .unwrap();
2931        let child = breakdown
2932            .created_tasks
2933            .iter()
2934            .find(|task| task.name == "Child")
2935            .unwrap();
2936        assert_eq!(parent.parent_id.as_deref(), Some(root.id.as_str()));
2937        assert_eq!(child.parent_id.as_deref(), Some(parent.id.as_str()));
2938    }
2939
2940    #[test]
2941    fn materialize_requirement_breakdown_under_parent_reuses_existing_matching_tasks() {
2942        let temp_dir = TempDir::new().unwrap();
2943        let manager = TaskManager::new(temp_dir.path());
2944        let root = manager
2945            .create_task("session-123", "Root", "Tracked root", None)
2946            .unwrap();
2947        let specs = [
2948            RequirementBreakdownTaskSpec {
2949                name: "Build Tauri application".to_string(),
2950                description: "Build the packaged app".to_string(),
2951                priority: "high".to_string(),
2952                is_blocking: true,
2953                parent_name: None,
2954            },
2955            RequirementBreakdownTaskSpec {
2956                name: "Perform hello-world smoke test".to_string(),
2957                description: "Launch the app and verify it runs".to_string(),
2958                priority: "high".to_string(),
2959                is_blocking: true,
2960                parent_name: Some("Build Tauri application".to_string()),
2961            },
2962        ];
2963
2964        let first = manager
2965            .materialize_requirement_breakdown_under_parent("session-123", &root.id, &specs)
2966            .unwrap();
2967        let second = manager
2968            .materialize_requirement_breakdown_under_parent("session-123", &root.id, &specs)
2969            .unwrap();
2970
2971        assert_eq!(first.created_tasks.len(), 2);
2972        assert_eq!(second.created_tasks.len(), 2);
2973        assert_eq!(
2974            manager
2975                .list_descendants("session-123", &root.id)
2976                .unwrap()
2977                .len(),
2978            2
2979        );
2980
2981        let first_build = first
2982            .created_tasks
2983            .iter()
2984            .find(|task| task.name == "Build Tauri application")
2985            .unwrap();
2986        let second_build = second
2987            .created_tasks
2988            .iter()
2989            .find(|task| task.name == "Build Tauri application")
2990            .unwrap();
2991        let first_smoke = first
2992            .created_tasks
2993            .iter()
2994            .find(|task| task.name == "Perform hello-world smoke test")
2995            .unwrap();
2996        let second_smoke = second
2997            .created_tasks
2998            .iter()
2999            .find(|task| task.name == "Perform hello-world smoke test")
3000            .unwrap();
3001
3002        assert_eq!(first_build.id, second_build.id);
3003        assert_eq!(first_smoke.id, second_smoke.id);
3004        assert_eq!(
3005            second_smoke.parent_id.as_deref(),
3006            Some(second_build.id.as_str())
3007        );
3008    }
3009
3010    #[test]
3011    fn initialize_auto_tracked_execution_plan_creates_single_root_context() {
3012        let temp_dir = TempDir::new().unwrap();
3013        let manager = TaskManager::new(temp_dir.path());
3014
3015        let plan = manager
3016            .initialize_auto_tracked_execution_plan(
3017                "session-123",
3018                "Implement feature",
3019                "Plan and implement the feature",
3020                &[
3021                    RequirementBreakdownTaskSpec {
3022                        name: "Design".to_string(),
3023                        description: "Design the feature".to_string(),
3024                        priority: "high".to_string(),
3025                        is_blocking: true,
3026                        parent_name: None,
3027                    },
3028                    RequirementBreakdownTaskSpec {
3029                        name: "Implement".to_string(),
3030                        description: "Implement the feature".to_string(),
3031                        priority: "high".to_string(),
3032                        is_blocking: false,
3033                        parent_name: None,
3034                    },
3035                ],
3036            )
3037            .unwrap();
3038
3039        assert_eq!(plan.root_task.name, "Implement feature");
3040        assert_eq!(plan.generated_task_count, 2);
3041        assert_eq!(
3042            manager.get_current_task_id("session-123").unwrap(),
3043            plan.initial_task_id
3044        );
3045        assert_eq!(
3046            manager
3047                .list_descendants("session-123", &plan.root_task.id)
3048                .unwrap()
3049                .len(),
3050            2
3051        );
3052    }
3053
3054    #[test]
3055    fn finalize_tracked_task_marks_parent_complete_only_after_descendants_close() {
3056        let temp_dir = TempDir::new().unwrap();
3057        let manager = TaskManager::new(temp_dir.path());
3058        let root = manager
3059            .create_task("session-123", "Root", "Tracked root", None)
3060            .unwrap();
3061        let child = manager
3062            .create_task(
3063                "session-123",
3064                "Child",
3065                "Outstanding child",
3066                Some(root.id.clone()),
3067            )
3068            .unwrap();
3069
3070        let first = manager
3071            .finalize_tracked_task_after_agent_run("session-123", &root.id)
3072            .unwrap();
3073        assert_eq!(
3074            first,
3075            TrackedTaskFinalization::StillInProgress {
3076                open_subtask_ids: vec![child.id.clone()],
3077            }
3078        );
3079        assert_eq!(
3080            manager
3081                .get_task("session-123", &root.id)
3082                .unwrap()
3083                .unwrap()
3084                .status,
3085            TaskStatus::InProgress
3086        );
3087
3088        manager
3089            .update_task_status("session-123", &child.id, TaskStatus::Completed)
3090            .unwrap();
3091        let second = manager
3092            .finalize_tracked_task_after_agent_run("session-123", &root.id)
3093            .unwrap();
3094        assert_eq!(second, TrackedTaskFinalization::Completed);
3095        assert_eq!(
3096            manager
3097                .get_task("session-123", &root.id)
3098                .unwrap()
3099                .unwrap()
3100                .status,
3101            TaskStatus::Completed
3102        );
3103    }
3104
3105    #[test]
3106    fn finalize_tracked_task_does_not_reopen_completed_root_with_open_descendants() {
3107        let temp_dir = TempDir::new().unwrap();
3108        let manager = TaskManager::new(temp_dir.path());
3109        let session_id = "session-finalize-sticky-complete";
3110
3111        let root = manager
3112            .create_task(session_id, "Root", "Tracked root", None)
3113            .unwrap();
3114        let child = manager
3115            .create_task(
3116                session_id,
3117                "Child",
3118                "Outstanding child",
3119                Some(root.id.clone()),
3120            )
3121            .unwrap();
3122
3123        manager
3124            .update_task_status(session_id, &root.id, TaskStatus::Completed)
3125            .unwrap_err();
3126        manager
3127            .update_task_status(session_id, &child.id, TaskStatus::Completed)
3128            .unwrap();
3129        manager
3130            .update_task_status(session_id, &root.id, TaskStatus::Completed)
3131            .unwrap();
3132
3133        let mut task_list = manager.load_task_list(session_id).unwrap();
3134        task_list
3135            .find_task_mut(&child.id)
3136            .expect("stored child task")
3137            .set_status(TaskStatus::NotStarted);
3138        manager.replace_task_list(task_list).unwrap();
3139
3140        let outcome = manager
3141            .finalize_tracked_task_after_agent_run(session_id, &root.id)
3142            .unwrap();
3143
3144        assert_eq!(
3145            outcome,
3146            TrackedTaskFinalization::StillInProgress {
3147                open_subtask_ids: vec![child.id.clone()],
3148            }
3149        );
3150        assert_eq!(
3151            manager
3152                .get_task(session_id, &root.id)
3153                .unwrap()
3154                .unwrap()
3155                .status,
3156            TaskStatus::Completed
3157        );
3158    }
3159
3160    #[test]
3161    fn record_task_result_merges_metadata_and_status() {
3162        let temp_dir = TempDir::new().unwrap();
3163        let manager = TaskManager::new(temp_dir.path());
3164        let task = manager
3165            .create_task("session-123", "Root", "Tracked root", None)
3166            .unwrap();
3167        manager
3168            .update_task_metadata(
3169                "session-123",
3170                &task.id,
3171                serde_json::json!({ "existing": true }),
3172            )
3173            .unwrap();
3174
3175        let status = manager
3176            .record_task_result("session-123", &task.id, true, "done".to_string(), 3, 42)
3177            .unwrap();
3178
3179        assert_eq!(status, TaskStatus::Completed);
3180        let task = manager.get_task("session-123", &task.id).unwrap().unwrap();
3181        let metadata = task.metadata.unwrap();
3182        assert_eq!(
3183            metadata.get("existing").and_then(|v| v.as_bool()),
3184            Some(true)
3185        );
3186        assert_eq!(
3187            metadata.get("success").and_then(|v| v.as_bool()),
3188            Some(true)
3189        );
3190        assert_eq!(metadata.get("tool_calls").and_then(|v| v.as_i64()), Some(3));
3191        assert_eq!(
3192            metadata.get("duration_ms").and_then(|v| v.as_i64()),
3193            Some(42)
3194        );
3195        assert_eq!(task.status, TaskStatus::Completed);
3196    }
3197
3198    #[test]
3199    fn completing_last_child_auto_completes_parent_and_clears_current_task() {
3200        let temp_dir = TempDir::new().unwrap();
3201        let manager = TaskManager::new(temp_dir.path());
3202        let session_id = "session-parent-auto-complete";
3203
3204        let root = manager
3205            .create_task(session_id, "Root", "Root", None)
3206            .unwrap();
3207        let child = manager
3208            .create_task(session_id, "Child", "Child", Some(root.id.clone()))
3209            .unwrap();
3210
3211        manager
3212            .set_current_task_id(session_id, Some(root.id.clone()))
3213            .unwrap();
3214        manager
3215            .update_task_status(session_id, &child.id, TaskStatus::Completed)
3216            .unwrap();
3217
3218        let stored_root = manager.get_task(session_id, &root.id).unwrap().unwrap();
3219        assert_eq!(stored_root.status, TaskStatus::Completed);
3220        assert_eq!(manager.get_current_task_id(session_id).unwrap(), None);
3221    }
3222
3223    #[test]
3224    fn completing_nested_leaf_auto_completes_all_ancestors() {
3225        let temp_dir = TempDir::new().unwrap();
3226        let manager = TaskManager::new(temp_dir.path());
3227        let session_id = "session-nested-parent-auto-complete";
3228
3229        let root = manager
3230            .create_task(session_id, "Root", "Root", None)
3231            .unwrap();
3232        let child = manager
3233            .create_task(session_id, "Child", "Child", Some(root.id.clone()))
3234            .unwrap();
3235        let grandchild = manager
3236            .create_task(
3237                session_id,
3238                "Grandchild",
3239                "Grandchild",
3240                Some(child.id.clone()),
3241            )
3242            .unwrap();
3243
3244        manager
3245            .update_task_status(session_id, &grandchild.id, TaskStatus::Completed)
3246            .unwrap();
3247
3248        assert_eq!(
3249            manager
3250                .get_task(session_id, &child.id)
3251                .unwrap()
3252                .unwrap()
3253                .status,
3254            TaskStatus::Completed
3255        );
3256        assert_eq!(
3257            manager
3258                .get_task(session_id, &root.id)
3259                .unwrap()
3260                .unwrap()
3261                .status,
3262            TaskStatus::Completed
3263        );
3264    }
3265
3266    #[test]
3267    fn reopening_descendant_reopens_completed_ancestors() {
3268        let temp_dir = TempDir::new().unwrap();
3269        let manager = TaskManager::new(temp_dir.path());
3270        let session_id = "session-reopen-ancestor";
3271
3272        let root = manager
3273            .create_task(session_id, "Root", "Root", None)
3274            .unwrap();
3275        let child = manager
3276            .create_task(session_id, "Child", "Child", Some(root.id.clone()))
3277            .unwrap();
3278
3279        manager
3280            .update_task_status(session_id, &child.id, TaskStatus::Completed)
3281            .unwrap();
3282        manager
3283            .update_task_status(session_id, &child.id, TaskStatus::InProgress)
3284            .unwrap();
3285
3286        assert_eq!(
3287            manager
3288                .get_task(session_id, &root.id)
3289                .unwrap()
3290                .unwrap()
3291                .status,
3292            TaskStatus::InProgress
3293        );
3294    }
3295}