1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43pub enum TaskStatus {
44 NotStarted,
46 Blocked,
48 InProgress,
50 Completed,
52 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
90pub enum TaskSource {
91 #[default]
93 User,
94 Agent,
96 Orchestrator,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
105pub enum TaskBackgroundStatus {
106 Queued,
108 Blocked,
110 AwaitingApproval,
112 Running,
114 Succeeded,
116 Failed,
118 Cancelled,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct TaskBackgroundJob {
125 pub status: TaskBackgroundStatus,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub job_id: Option<String>,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub message: Option<String>,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
137#[serde(rename_all = "snake_case")]
138pub enum TaskMemoryPhase {
139 Delegated,
141 Handoff,
143 Promoted,
145 Blocked,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct TaskMemoryEvent {
152 pub phase: TaskMemoryPhase,
154 pub summary: String,
156 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub scope: Option<String>,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub memory_type: Option<String>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub memory_file_path: Option<String>,
165 pub recorded_at: DateTime<Utc>,
167}
168
169impl TaskMemoryEvent {
170 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
191pub struct TaskMemoryLifecycle {
192 #[serde(default, skip_serializing_if = "Vec::is_empty")]
194 pub events: Vec<TaskMemoryEvent>,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub last_memory_file_path: Option<String>,
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
202#[serde(rename_all = "snake_case")]
203pub enum TaskExecutionKind {
204 Planning,
206 Implementation,
208 Verification,
210 #[default]
212 General,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
217pub struct TaskVerificationProfile {
218 #[serde(default)]
220 pub execution_kind: TaskExecutionKind,
221 #[serde(default)]
223 pub requires_mutation: bool,
224 #[serde(default)]
226 pub requires_build: bool,
227 #[serde(default)]
229 pub requires_test: bool,
230 #[serde(default)]
233 pub requires_external_evidence: bool,
234 #[serde(default)]
237 pub requires_launch_evidence: bool,
238 #[serde(default)]
241 pub parallel_safe: bool,
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
246#[serde(rename_all = "snake_case")]
247pub enum TaskExecutionEvidenceKind {
248 ToolActivity,
250 Diagnostic,
252 Contradiction,
254 Blocker,
256 Mutation,
258 Build,
260 Test,
262 Artifact,
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
268pub struct TaskExecutionEvidence {
269 pub kind: TaskExecutionEvidenceKind,
271 pub summary: String,
273 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub tool_name: Option<String>,
276 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub command: Option<String>,
279 #[serde(default = "default_true")]
281 pub success: bool,
282 pub recorded_at: DateTime<Utc>,
284}
285
286#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
288pub struct TaskExecutionState {
289 #[serde(default)]
291 pub verification_profile: TaskVerificationProfile,
292 #[serde(default)]
294 pub saw_tool_activity: bool,
295 #[serde(default)]
297 pub saw_mutation: bool,
298 #[serde(default)]
300 pub saw_diagnostic_progress: bool,
301 #[serde(default)]
303 pub saw_contradiction: bool,
304 #[serde(default)]
306 pub saw_blocker: bool,
307 #[serde(default)]
309 pub build_succeeded: bool,
310 #[serde(default)]
312 pub test_succeeded: bool,
313 #[serde(default, skip_serializing_if = "Option::is_none")]
315 pub last_runtime_note: Option<String>,
316 #[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 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 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 pub fn merge_profile(&mut self, profile: TaskVerificationProfile) {
420 self.verification_profile = profile;
421 }
422
423 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
518pub struct Task {
519 pub id: String,
521 pub name: String,
523 pub description: String,
525 pub status: TaskStatus,
527 pub parent_id: Option<String>,
529
530 #[serde(default, skip_serializing_if = "Vec::is_empty")]
535 pub blocked_by: Vec<String>,
536
537 #[serde(default, skip_serializing_if = "Option::is_none")]
539 pub background_job: Option<TaskBackgroundJob>,
540
541 #[serde(default)]
543 pub sort_order: i32,
544
545 #[serde(default, skip_serializing_if = "Option::is_none")]
547 pub phase: Option<String>,
548 pub created_at: DateTime<Utc>,
550 pub updated_at: DateTime<Utc>,
552 pub session_id: String,
554 #[serde(default)]
556 pub source: TaskSource,
557 #[serde(default)]
559 pub orchestrator_task_id: Option<String>,
560 #[serde(default)]
562 pub agent_id: Option<String>,
563 #[serde(default)]
565 pub metadata: Option<serde_json::Value>,
566}
567
568#[derive(Debug, Clone, PartialEq, Eq)]
570pub enum TrackedTaskFinalization {
571 Completed,
573 StillInProgress { open_subtask_ids: Vec<String> },
575}
576
577#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
579pub struct RequirementBreakdownTaskSpec {
580 pub name: String,
582 pub description: String,
584 pub priority: String,
586 pub is_blocking: bool,
588 #[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#[derive(Debug, Clone, Default)]
608pub struct MaterializedTaskBreakdown {
609 pub created_tasks: Vec<Task>,
611 pub root_task_ids: Vec<String>,
613}
614
615#[derive(Debug, Clone)]
617pub struct AutoTrackedExecutionPlan {
618 pub root_task: Task,
620 pub planned_subtasks: Vec<String>,
622 pub initial_task_id: Option<String>,
624 pub initial_task_name: Option<String>,
626 pub generated_task_count: usize,
628}
629
630pub 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 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 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 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 pub fn set_metadata(&mut self, metadata: serde_json::Value) {
835 self.metadata = Some(metadata);
836 self.updated_at = Utc::now();
837 }
838
839 pub fn set_status(&mut self, status: TaskStatus) {
841 self.status = status;
842 self.updated_at = Utc::now();
843 }
844
845 pub fn set_name(&mut self, name: impl Into<String>) {
847 self.name = name.into();
848 self.updated_at = Utc::now();
849 }
850
851 pub fn set_description(&mut self, description: impl Into<String>) {
853 self.description = description.into();
854 self.updated_at = Utc::now();
855 }
856
857 pub fn is_terminal(&self) -> bool {
859 matches!(self.status, TaskStatus::Completed | TaskStatus::Cancelled)
860 }
861
862 pub fn set_background_job(&mut self, job: Option<TaskBackgroundJob>) {
864 self.background_job = job;
865 self.updated_at = Utc::now();
866 }
867
868 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 pub fn set_phase(&mut self, phase: Option<String>) {
881 self.phase = phase;
882 self.updated_at = Utc::now();
883 }
884}
885
886#[derive(Debug, Clone, Serialize, Deserialize)]
888pub struct TaskList {
889 pub session_id: String,
891 pub tasks: Vec<Task>,
893 #[serde(default, skip_serializing_if = "Option::is_none")]
898 pub current_task_id: Option<String>,
899}
900
901#[derive(Debug, Clone, Serialize)]
903pub struct TaskTreeNode {
904 pub task: Task,
906 pub children: Vec<TaskTreeNode>,
908}
909
910impl TaskList {
911 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 pub fn current_task_id(&self) -> Option<&str> {
922 self.current_task_id.as_deref()
923 }
924
925 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 pub fn add_task(&mut self, task: Task) {
943 self.tasks.push(task);
944 }
945
946 pub fn find_task(&self, task_id: &str) -> Option<&Task> {
948 self.tasks.iter().find(|t| t.id == task_id)
949 }
950
951 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 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 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 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 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(¤t_id) else {
1010 continue;
1011 };
1012 descendants.push(task);
1013
1014 let mut children = self.subtasks(¤t_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 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 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 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 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 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 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 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 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 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 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#[derive(Debug, thiserror::Error)]
1240pub enum TaskError {
1241 #[error("Task not found: {0}")]
1243 NotFound(String),
1244 #[error("Invalid input: {0}")]
1246 InvalidInput(String),
1247 #[error("I/O error: {0}")]
1249 Io(#[from] std::io::Error),
1250 #[error("Serialization error: {0}")]
1252 Serialization(#[from] serde_json::Error),
1253}
1254
1255pub struct TaskManager {
1257 base_dir: PathBuf,
1259 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 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 fn task_file_path(&self, session_id: &str) -> PathBuf {
1295 self.base_dir.join(format!("{}.json", session_id))
1296 }
1297
1298 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 fn save_to_disk(&self, task_list: &TaskList) -> Result<(), TaskError> {
1312 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 fn get_or_load(&self, session_id: &str) -> Result<TaskList, TaskError> {
1323 {
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 let task_list = self.load_from_disk(session_id)?;
1333
1334 {
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 fn update_and_save(&self, task_list: TaskList) -> Result<(), TaskError> {
1345 self.save_to_disk(&task_list)?;
1347
1348 {
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 pub fn load_task_list(&self, session_id: &str) -> Result<TaskList, TaskError> {
1362 self.get_or_load(session_id)
1363 }
1364
1365 pub fn replace_task_list(&self, task_list: TaskList) -> Result<(), TaskError> {
1370 self.update_and_save(task_list)
1371 }
1372
1373 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
2074static GLOBAL_TASK_MANAGER: std::sync::OnceLock<TaskManager> = std::sync::OnceLock::new();
2080
2081pub 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 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 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 let found = list.find_task(&task1.id);
2150 assert!(found.is_some());
2151 assert_eq!(found.unwrap().name, "Task 1");
2152
2153 let roots = list.root_tasks();
2155 assert_eq!(roots.len(), 1);
2156 assert_eq!(roots[0].id, task1.id);
2157
2158 let subtasks = list.subtasks(&task1.id);
2160 assert_eq!(subtasks.len(), 1);
2161 assert_eq!(subtasks[0].id, task2.id);
2162
2163 let removed = list.remove_task(&task1.id);
2165 assert!(removed.is_some());
2166 assert_eq!(list.tasks.len(), 1);
2167 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 {
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 {
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}