intent_engine/
tasks.rs

1use crate::db::models::{
2    DoneTaskResponse, Event, EventsSummary, NextStepSuggestion, PaginatedTasks, ParentTaskInfo,
3    PickNextResponse, SpawnSubtaskResponse, SubtaskInfo, Task, TaskSortBy, TaskWithEvents,
4    WorkspaceStats, WorkspaceStatus,
5};
6use crate::error::{IntentError, Result};
7use chrono::Utc;
8use sqlx::SqlitePool;
9use std::sync::Arc;
10
11pub use crate::db::models::TaskContext;
12pub struct TaskManager<'a> {
13    pool: &'a SqlitePool,
14    notifier: crate::notifications::NotificationSender,
15    cli_notifier: Option<crate::dashboard::cli_notifier::CliNotifier>,
16    project_path: Option<String>,
17}
18
19impl<'a> TaskManager<'a> {
20    pub fn new(pool: &'a SqlitePool) -> Self {
21        Self {
22            pool,
23            notifier: crate::notifications::NotificationSender::new(None),
24            cli_notifier: Some(crate::dashboard::cli_notifier::CliNotifier::new()),
25            project_path: None,
26        }
27    }
28
29    /// Create a TaskManager with project path for CLI notifications
30    pub fn with_project_path(pool: &'a SqlitePool, project_path: String) -> Self {
31        Self {
32            pool,
33            notifier: crate::notifications::NotificationSender::new(None),
34            cli_notifier: Some(crate::dashboard::cli_notifier::CliNotifier::new()),
35            project_path: Some(project_path),
36        }
37    }
38
39    /// Create a TaskManager with WebSocket notification support
40    pub fn with_websocket(
41        pool: &'a SqlitePool,
42        ws_state: Arc<crate::dashboard::websocket::WebSocketState>,
43        project_path: String,
44    ) -> Self {
45        Self {
46            pool,
47            notifier: crate::notifications::NotificationSender::new(Some(ws_state)),
48            cli_notifier: None, // Dashboard context doesn't need CLI notifier
49            project_path: Some(project_path),
50        }
51    }
52
53    /// Internal helper: Notify UI about task creation
54    async fn notify_task_created(&self, task: &Task) {
55        use crate::dashboard::websocket::DatabaseOperationPayload;
56
57        // WebSocket notification (Dashboard context)
58        if let Some(project_path) = &self.project_path {
59            let task_json = match serde_json::to_value(task) {
60                Ok(json) => json,
61                Err(e) => {
62                    tracing::warn!(error = %e, "Failed to serialize task for notification");
63                    return;
64                },
65            };
66
67            let payload =
68                DatabaseOperationPayload::task_created(task.id, task_json, project_path.clone());
69            self.notifier.send(payload).await;
70        }
71
72        // CLI → Dashboard HTTP notification (CLI context)
73        if let Some(cli_notifier) = &self.cli_notifier {
74            cli_notifier
75                .notify_task_changed(Some(task.id), "created", self.project_path.clone())
76                .await;
77        }
78    }
79
80    /// Internal helper: Notify UI about task update
81    async fn notify_task_updated(&self, task: &Task) {
82        use crate::dashboard::websocket::DatabaseOperationPayload;
83
84        // WebSocket notification (Dashboard context)
85        if let Some(project_path) = &self.project_path {
86            let task_json = match serde_json::to_value(task) {
87                Ok(json) => json,
88                Err(e) => {
89                    tracing::warn!(error = %e, "Failed to serialize task for notification");
90                    return;
91                },
92            };
93
94            let payload =
95                DatabaseOperationPayload::task_updated(task.id, task_json, project_path.clone());
96            self.notifier.send(payload).await;
97        }
98
99        // CLI → Dashboard HTTP notification (CLI context)
100        if let Some(cli_notifier) = &self.cli_notifier {
101            cli_notifier
102                .notify_task_changed(Some(task.id), "updated", self.project_path.clone())
103                .await;
104        }
105    }
106
107    /// Internal helper: Notify UI about task deletion
108    async fn notify_task_deleted(&self, task_id: i64) {
109        use crate::dashboard::websocket::DatabaseOperationPayload;
110
111        // WebSocket notification (Dashboard context)
112        if let Some(project_path) = &self.project_path {
113            let payload = DatabaseOperationPayload::task_deleted(task_id, project_path.clone());
114            self.notifier.send(payload).await;
115        }
116
117        // CLI → Dashboard HTTP notification (CLI context)
118        if let Some(cli_notifier) = &self.cli_notifier {
119            cli_notifier
120                .notify_task_changed(Some(task_id), "deleted", self.project_path.clone())
121                .await;
122        }
123    }
124
125    /// Add a new task
126    /// owner: 'human' (created via CLI/Dashboard) or 'ai' (created via MCP)
127    #[tracing::instrument(skip(self), fields(task_name = %name))]
128    pub async fn add_task(
129        &self,
130        name: &str,
131        spec: Option<&str>,
132        parent_id: Option<i64>,
133        owner: Option<&str>,
134    ) -> Result<Task> {
135        // Check for circular dependency if parent_id is provided
136        if let Some(pid) = parent_id {
137            self.check_task_exists(pid).await?;
138        }
139
140        let now = Utc::now();
141        let owner = owner.unwrap_or("human");
142
143        let result = sqlx::query(
144            r#"
145            INSERT INTO tasks (name, spec, parent_id, status, first_todo_at, owner)
146            VALUES (?, ?, ?, 'todo', ?, ?)
147            "#,
148        )
149        .bind(name)
150        .bind(spec)
151        .bind(parent_id)
152        .bind(now)
153        .bind(owner)
154        .execute(self.pool)
155        .await?;
156
157        let id = result.last_insert_rowid();
158        let task = self.get_task(id).await?;
159
160        // Notify WebSocket clients about the new task
161        self.notify_task_created(&task).await;
162
163        Ok(task)
164    }
165
166    // =========================================================================
167    // Transaction-aware methods (for batch operations like PlanExecutor)
168    // These methods do NOT notify - caller is responsible for notifications
169    // =========================================================================
170
171    /// Create a task within a transaction (no notification)
172    ///
173    /// This is used by PlanExecutor for batch operations where:
174    /// - Multiple tasks need atomic creation
175    /// - Notification should happen after all tasks are committed
176    ///
177    /// # Arguments
178    /// * `tx` - The active transaction
179    /// * `name` - Task name
180    /// * `spec` - Optional task specification
181    /// * `priority` - Optional priority (1=critical, 2=high, 3=medium, 4=low)
182    /// * `status` - Optional status string ("todo", "doing", "done")
183    /// * `active_form` - Optional active form description
184    /// * `owner` - Task owner ("human" or "ai")
185    ///
186    /// # Returns
187    /// The ID of the created task
188    #[allow(clippy::too_many_arguments)]
189    pub async fn create_task_in_tx(
190        &self,
191        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
192        name: &str,
193        spec: Option<&str>,
194        priority: Option<i32>,
195        status: Option<&str>,
196        active_form: Option<&str>,
197        owner: &str,
198    ) -> Result<i64> {
199        let now = Utc::now();
200        let status = status.unwrap_or("todo");
201        let priority = priority.unwrap_or(3); // Default: medium
202
203        let result = sqlx::query(
204            r#"
205            INSERT INTO tasks (name, spec, priority, status, active_form, first_todo_at, owner)
206            VALUES (?, ?, ?, ?, ?, ?, ?)
207            "#,
208        )
209        .bind(name)
210        .bind(spec)
211        .bind(priority)
212        .bind(status)
213        .bind(active_form)
214        .bind(now)
215        .bind(owner)
216        .execute(&mut **tx)
217        .await?;
218
219        Ok(result.last_insert_rowid())
220    }
221
222    /// Update a task within a transaction (no notification)
223    ///
224    /// Only updates fields that are Some - supports partial updates.
225    /// Does NOT update name (used for identity) or timestamps.
226    ///
227    /// # Arguments
228    /// * `tx` - The active transaction
229    /// * `task_id` - ID of the task to update
230    /// * `spec` - New spec (if Some)
231    /// * `priority` - New priority (if Some)
232    /// * `status` - New status (if Some)
233    /// * `active_form` - New active form (if Some)
234    pub async fn update_task_in_tx(
235        &self,
236        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
237        task_id: i64,
238        spec: Option<&str>,
239        priority: Option<i32>,
240        status: Option<&str>,
241        active_form: Option<&str>,
242    ) -> Result<()> {
243        // Update spec if provided
244        if let Some(spec) = spec {
245            sqlx::query("UPDATE tasks SET spec = ? WHERE id = ?")
246                .bind(spec)
247                .bind(task_id)
248                .execute(&mut **tx)
249                .await?;
250        }
251
252        // Update priority if provided
253        if let Some(priority) = priority {
254            sqlx::query("UPDATE tasks SET priority = ? WHERE id = ?")
255                .bind(priority)
256                .bind(task_id)
257                .execute(&mut **tx)
258                .await?;
259        }
260
261        // Update status if provided
262        if let Some(status) = status {
263            sqlx::query("UPDATE tasks SET status = ? WHERE id = ?")
264                .bind(status)
265                .bind(task_id)
266                .execute(&mut **tx)
267                .await?;
268        }
269
270        // Update active_form if provided
271        if let Some(active_form) = active_form {
272            sqlx::query("UPDATE tasks SET active_form = ? WHERE id = ?")
273                .bind(active_form)
274                .bind(task_id)
275                .execute(&mut **tx)
276                .await?;
277        }
278
279        Ok(())
280    }
281
282    /// Set parent_id for a task within a transaction (no notification)
283    ///
284    /// Used to establish parent-child relationships after tasks are created.
285    pub async fn set_parent_in_tx(
286        &self,
287        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
288        task_id: i64,
289        parent_id: i64,
290    ) -> Result<()> {
291        sqlx::query("UPDATE tasks SET parent_id = ? WHERE id = ?")
292            .bind(parent_id)
293            .bind(task_id)
294            .execute(&mut **tx)
295            .await?;
296
297        Ok(())
298    }
299
300    /// Clear parent_id for a task in a transaction (make it a root task)
301    ///
302    /// Used when explicitly setting parent_id to null in JSON.
303    pub async fn clear_parent_in_tx(
304        &self,
305        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
306        task_id: i64,
307    ) -> Result<()> {
308        sqlx::query("UPDATE tasks SET parent_id = NULL WHERE id = ?")
309            .bind(task_id)
310            .execute(&mut **tx)
311            .await?;
312
313        Ok(())
314    }
315
316    /// Notify Dashboard about a batch operation
317    ///
318    /// Call this after committing a transaction that created/updated multiple tasks.
319    /// Sends a single "batch_update" notification instead of per-task notifications.
320    pub async fn notify_batch_changed(&self) {
321        if let Some(cli_notifier) = &self.cli_notifier {
322            cli_notifier
323                .notify_task_changed(None, "batch_update", self.project_path.clone())
324                .await;
325        }
326    }
327
328    // =========================================================================
329    // End of transaction-aware methods
330    // =========================================================================
331
332    /// Get a task by ID
333    #[tracing::instrument(skip(self))]
334    pub async fn get_task(&self, id: i64) -> Result<Task> {
335        let task = sqlx::query_as::<_, Task>(
336            r#"
337            SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner
338            FROM tasks
339            WHERE id = ?
340            "#,
341        )
342        .bind(id)
343        .fetch_optional(self.pool)
344        .await?
345        .ok_or(IntentError::TaskNotFound(id))?;
346
347        Ok(task)
348    }
349
350    /// Get a task with events summary
351    pub async fn get_task_with_events(&self, id: i64) -> Result<TaskWithEvents> {
352        let task = self.get_task(id).await?;
353        let events_summary = self.get_events_summary(id).await?;
354
355        Ok(TaskWithEvents {
356            task,
357            events_summary: Some(events_summary),
358        })
359    }
360
361    /// Get full ancestry chain for a task
362    ///
363    /// Returns a vector of tasks from the given task up to the root:
364    /// [task itself, parent, grandparent, ..., root]
365    ///
366    /// Example:
367    /// - Task 42 (parent_id: 55) → [Task 42, Task 55, ...]
368    /// - Task 100 (parent_id: null) → [Task 100]
369    pub async fn get_task_ancestry(&self, task_id: i64) -> Result<Vec<Task>> {
370        let mut chain = Vec::new();
371        let mut current_id = Some(task_id);
372
373        while let Some(id) = current_id {
374            let task = self.get_task(id).await?;
375            current_id = task.parent_id;
376            chain.push(task);
377        }
378
379        Ok(chain)
380    }
381
382    /// Get task context - the complete family tree of a task
383    ///
384    /// Returns:
385    /// - task: The requested task
386    /// - ancestors: Parent chain up to root (ordered from immediate parent to root)
387    /// - siblings: Other tasks at the same level (same parent_id)
388    /// - children: Direct subtasks of this task
389    pub async fn get_task_context(&self, id: i64) -> Result<TaskContext> {
390        // Get the main task
391        let task = self.get_task(id).await?;
392
393        // Get ancestors (walk up parent chain)
394        let mut ancestors = Vec::new();
395        let mut current_parent_id = task.parent_id;
396
397        while let Some(parent_id) = current_parent_id {
398            let parent = self.get_task(parent_id).await?;
399            current_parent_id = parent.parent_id;
400            ancestors.push(parent);
401        }
402
403        // Get siblings (tasks with same parent_id)
404        let siblings = if let Some(parent_id) = task.parent_id {
405            sqlx::query_as::<_, Task>(
406                r#"
407                SELECT id, parent_id, name, spec, status, complexity, priority,
408                       first_todo_at, first_doing_at, first_done_at, active_form, owner
409                FROM tasks
410                WHERE parent_id = ? AND id != ?
411                ORDER BY priority ASC NULLS LAST, id ASC
412                "#,
413            )
414            .bind(parent_id)
415            .bind(id)
416            .fetch_all(self.pool)
417            .await?
418        } else {
419            // For root tasks, get other root tasks as siblings
420            sqlx::query_as::<_, Task>(
421                r#"
422                SELECT id, parent_id, name, spec, status, complexity, priority,
423                       first_todo_at, first_doing_at, first_done_at, active_form, owner
424                FROM tasks
425                WHERE parent_id IS NULL AND id != ?
426                ORDER BY priority ASC NULLS LAST, id ASC
427                "#,
428            )
429            .bind(id)
430            .fetch_all(self.pool)
431            .await?
432        };
433
434        // Get children (direct subtasks)
435        let children = sqlx::query_as::<_, Task>(
436            r#"
437            SELECT id, parent_id, name, spec, status, complexity, priority,
438                   first_todo_at, first_doing_at, first_done_at, active_form, owner
439            FROM tasks
440            WHERE parent_id = ?
441            ORDER BY priority ASC NULLS LAST, id ASC
442            "#,
443        )
444        .bind(id)
445        .fetch_all(self.pool)
446        .await?;
447
448        // Get blocking tasks (tasks that this task depends on)
449        let blocking_tasks = sqlx::query_as::<_, Task>(
450            r#"
451            SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
452                   t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form, t.owner
453            FROM tasks t
454            JOIN dependencies d ON t.id = d.blocking_task_id
455            WHERE d.blocked_task_id = ?
456            ORDER BY t.priority ASC NULLS LAST, t.id ASC
457            "#,
458        )
459        .bind(id)
460        .fetch_all(self.pool)
461        .await?;
462
463        // Get blocked_by tasks (tasks that depend on this task)
464        let blocked_by_tasks = sqlx::query_as::<_, Task>(
465            r#"
466            SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
467                   t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form, t.owner
468            FROM tasks t
469            JOIN dependencies d ON t.id = d.blocked_task_id
470            WHERE d.blocking_task_id = ?
471            ORDER BY t.priority ASC NULLS LAST, t.id ASC
472            "#,
473        )
474        .bind(id)
475        .fetch_all(self.pool)
476        .await?;
477
478        Ok(TaskContext {
479            task,
480            ancestors,
481            siblings,
482            children,
483            dependencies: crate::db::models::TaskDependencies {
484                blocking_tasks,
485                blocked_by_tasks,
486            },
487        })
488    }
489
490    /// Get all descendants of a task recursively (children, grandchildren, etc.)
491    /// Uses recursive CTE for efficient querying
492    pub async fn get_descendants(&self, task_id: i64) -> Result<Vec<Task>> {
493        let descendants = sqlx::query_as::<_, Task>(
494            r#"
495            WITH RECURSIVE descendants AS (
496                SELECT id, parent_id, name, spec, status, complexity, priority,
497                       first_todo_at, first_doing_at, first_done_at, active_form, owner
498                FROM tasks
499                WHERE parent_id = ?
500
501                UNION ALL
502
503                SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
504                       t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form, t.owner
505                FROM tasks t
506                INNER JOIN descendants d ON t.parent_id = d.id
507            )
508            SELECT * FROM descendants
509            ORDER BY parent_id NULLS FIRST, priority ASC NULLS LAST, id ASC
510            "#,
511        )
512        .bind(task_id)
513        .fetch_all(self.pool)
514        .await?;
515
516        Ok(descendants)
517    }
518
519    /// Get status response for a task (the "spotlight" view)
520    /// This is the main method for `ie status` command
521    pub async fn get_status(
522        &self,
523        task_id: i64,
524        with_events: bool,
525    ) -> Result<crate::db::models::StatusResponse> {
526        use crate::db::models::{StatusResponse, TaskBrief};
527
528        // Get task context (reuse existing method)
529        let context = self.get_task_context(task_id).await?;
530
531        // Get all descendants recursively
532        let descendants_full = self.get_descendants(task_id).await?;
533
534        // Convert siblings and descendants to brief format
535        let siblings: Vec<TaskBrief> = context.siblings.iter().map(TaskBrief::from).collect();
536        let descendants: Vec<TaskBrief> = descendants_full.iter().map(TaskBrief::from).collect();
537
538        // Get events if requested
539        let events = if with_events {
540            let event_mgr = crate::events::EventManager::new(self.pool);
541            Some(
542                event_mgr
543                    .list_events(Some(task_id), Some(50), None, None)
544                    .await?,
545            )
546        } else {
547            None
548        };
549
550        Ok(StatusResponse {
551            focused_task: context.task,
552            ancestors: context.ancestors,
553            siblings,
554            descendants,
555            events,
556        })
557    }
558
559    /// Get root tasks (tasks with no parent) for NoFocusResponse
560    pub async fn get_root_tasks(&self) -> Result<Vec<Task>> {
561        let tasks = sqlx::query_as::<_, Task>(
562            r#"
563            SELECT id, parent_id, name, spec, status, complexity, priority,
564                   first_todo_at, first_doing_at, first_done_at, active_form, owner
565            FROM tasks
566            WHERE parent_id IS NULL
567            ORDER BY
568                CASE status
569                    WHEN 'doing' THEN 0
570                    WHEN 'todo' THEN 1
571                    WHEN 'done' THEN 2
572                END,
573                priority ASC NULLS LAST,
574                id ASC
575            "#,
576        )
577        .fetch_all(self.pool)
578        .await?;
579
580        Ok(tasks)
581    }
582
583    /// Get events summary for a task
584    async fn get_events_summary(&self, task_id: i64) -> Result<EventsSummary> {
585        let total_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM events WHERE task_id = ?")
586            .bind(task_id)
587            .fetch_one(self.pool)
588            .await?;
589
590        let recent_events = sqlx::query_as::<_, Event>(
591            r#"
592            SELECT id, task_id, timestamp, log_type, discussion_data
593            FROM events
594            WHERE task_id = ?
595            ORDER BY timestamp DESC
596            LIMIT 10
597            "#,
598        )
599        .bind(task_id)
600        .fetch_all(self.pool)
601        .await?;
602
603        Ok(EventsSummary {
604            total_count,
605            recent_events,
606        })
607    }
608
609    /// Update a task
610    #[allow(clippy::too_many_arguments)]
611    pub async fn update_task(
612        &self,
613        id: i64,
614        name: Option<&str>,
615        spec: Option<&str>,
616        parent_id: Option<Option<i64>>,
617        status: Option<&str>,
618        complexity: Option<i32>,
619        priority: Option<i32>,
620    ) -> Result<Task> {
621        // Check task exists
622        let task = self.get_task(id).await?;
623
624        // Validate status if provided
625        if let Some(s) = status {
626            if !["todo", "doing", "done"].contains(&s) {
627                return Err(IntentError::InvalidInput(format!("Invalid status: {}", s)));
628            }
629        }
630
631        // Check for circular dependency if parent_id is being changed
632        if let Some(Some(pid)) = parent_id {
633            if pid == id {
634                return Err(IntentError::CircularDependency {
635                    blocking_task_id: pid,
636                    blocked_task_id: id,
637                });
638            }
639            self.check_task_exists(pid).await?;
640            self.check_circular_dependency(id, pid).await?;
641        }
642
643        // Build dynamic update query using QueryBuilder for SQL injection safety
644        let mut builder: sqlx::QueryBuilder<sqlx::Sqlite> =
645            sqlx::QueryBuilder::new("UPDATE tasks SET ");
646        let mut has_updates = false;
647
648        if let Some(n) = name {
649            if has_updates {
650                builder.push(", ");
651            }
652            builder.push("name = ").push_bind(n);
653            has_updates = true;
654        }
655
656        if let Some(s) = spec {
657            if has_updates {
658                builder.push(", ");
659            }
660            builder.push("spec = ").push_bind(s);
661            has_updates = true;
662        }
663
664        if let Some(pid) = parent_id {
665            if has_updates {
666                builder.push(", ");
667            }
668            match pid {
669                Some(p) => {
670                    builder.push("parent_id = ").push_bind(p);
671                },
672                None => {
673                    builder.push("parent_id = NULL");
674                },
675            }
676            has_updates = true;
677        }
678
679        if let Some(c) = complexity {
680            if has_updates {
681                builder.push(", ");
682            }
683            builder.push("complexity = ").push_bind(c);
684            has_updates = true;
685        }
686
687        if let Some(p) = priority {
688            if has_updates {
689                builder.push(", ");
690            }
691            builder.push("priority = ").push_bind(p);
692            has_updates = true;
693        }
694
695        if let Some(s) = status {
696            if has_updates {
697                builder.push(", ");
698            }
699            builder.push("status = ").push_bind(s);
700            has_updates = true;
701
702            // Update timestamp fields based on status
703            let now = Utc::now();
704            let timestamp = now.to_rfc3339();
705            match s {
706                "todo" if task.first_todo_at.is_none() => {
707                    builder.push(", first_todo_at = ").push_bind(timestamp);
708                },
709                "doing" if task.first_doing_at.is_none() => {
710                    builder.push(", first_doing_at = ").push_bind(timestamp);
711                },
712                "done" if task.first_done_at.is_none() => {
713                    builder.push(", first_done_at = ").push_bind(timestamp);
714                },
715                _ => {},
716            }
717        }
718
719        if !has_updates {
720            return Ok(task);
721        }
722
723        builder.push(" WHERE id = ").push_bind(id);
724
725        builder.build().execute(self.pool).await?;
726
727        let task = self.get_task(id).await?;
728
729        // Notify WebSocket clients about the task update
730        self.notify_task_updated(&task).await;
731
732        Ok(task)
733    }
734
735    /// Delete a task
736    pub async fn delete_task(&self, id: i64) -> Result<()> {
737        self.check_task_exists(id).await?;
738
739        sqlx::query("DELETE FROM tasks WHERE id = ?")
740            .bind(id)
741            .execute(self.pool)
742            .await?;
743
744        // Notify WebSocket clients about the task deletion
745        self.notify_task_deleted(id).await;
746
747        Ok(())
748    }
749
750    /// Find tasks with optional filters, sorting, and pagination
751    pub async fn find_tasks(
752        &self,
753        status: Option<&str>,
754        parent_id: Option<Option<i64>>,
755        sort_by: Option<TaskSortBy>,
756        limit: Option<i64>,
757        offset: Option<i64>,
758    ) -> Result<PaginatedTasks> {
759        // Apply defaults
760        let sort_by = sort_by.unwrap_or_default(); // Default: FocusAware
761        let limit = limit.unwrap_or(100);
762        let offset = offset.unwrap_or(0);
763
764        // Resolve session_id for FocusAware sorting
765        let session_id = crate::workspace::resolve_session_id(None);
766
767        // Build WHERE clause
768        let mut where_clause = String::from("WHERE 1=1");
769        let mut conditions = Vec::new();
770
771        if let Some(s) = status {
772            where_clause.push_str(" AND status = ?");
773            conditions.push(s.to_string());
774        }
775
776        if let Some(pid) = parent_id {
777            if let Some(p) = pid {
778                where_clause.push_str(" AND parent_id = ?");
779                conditions.push(p.to_string());
780            } else {
781                where_clause.push_str(" AND parent_id IS NULL");
782            }
783        }
784
785        // Track if FocusAware mode needs session_id bind
786        let uses_session_bind = matches!(sort_by, TaskSortBy::FocusAware);
787
788        // Build ORDER BY clause based on sort mode
789        let order_clause = match sort_by {
790            TaskSortBy::Id => {
791                // Legacy: simple ORDER BY id ASC
792                "ORDER BY id ASC".to_string()
793            },
794            TaskSortBy::Priority => {
795                // ORDER BY priority ASC, complexity ASC, id ASC
796                "ORDER BY COALESCE(priority, 999) ASC, COALESCE(complexity, 5) ASC, id ASC"
797                    .to_string()
798            },
799            TaskSortBy::Time => {
800                // ORDER BY timestamp based on status
801                r#"ORDER BY
802                    CASE status
803                        WHEN 'doing' THEN first_doing_at
804                        WHEN 'todo' THEN first_todo_at
805                        WHEN 'done' THEN first_done_at
806                    END ASC NULLS LAST,
807                    id ASC"#
808                    .to_string()
809            },
810            TaskSortBy::FocusAware => {
811                // Focus-aware: current focused task → doing tasks → todo tasks
812                r#"ORDER BY
813                    CASE
814                        WHEN t.id = (SELECT current_task_id FROM sessions WHERE session_id = ?) THEN 0
815                        WHEN t.status = 'doing' THEN 1
816                        WHEN t.status = 'todo' THEN 2
817                        ELSE 3
818                    END ASC,
819                    COALESCE(t.priority, 999) ASC,
820                    t.id ASC"#
821                    .to_string()
822            },
823        };
824
825        // Get total count
826        let count_query = format!("SELECT COUNT(*) FROM tasks {}", where_clause);
827        let mut count_q = sqlx::query_scalar::<_, i64>(&count_query);
828        for cond in &conditions {
829            count_q = count_q.bind(cond);
830        }
831        let total_count = count_q.fetch_one(self.pool).await?;
832
833        // Build main query with pagination
834        let main_query = format!(
835            "SELECT id, parent_id, name, NULL as spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner FROM tasks t {} {} LIMIT ? OFFSET ?",
836            where_clause, order_clause
837        );
838
839        let mut q = sqlx::query_as::<_, Task>(&main_query);
840        for cond in conditions {
841            q = q.bind(cond);
842        }
843        // Bind session_id for FocusAware ORDER BY clause
844        if uses_session_bind {
845            q = q.bind(&session_id);
846        }
847        q = q.bind(limit);
848        q = q.bind(offset);
849
850        let tasks = q.fetch_all(self.pool).await?;
851
852        // Calculate has_more
853        let has_more = offset + (tasks.len() as i64) < total_count;
854
855        Ok(PaginatedTasks {
856            tasks,
857            total_count,
858            has_more,
859            limit,
860            offset,
861        })
862    }
863
864    /// Get workspace statistics using SQL aggregation (no data loading)
865    ///
866    /// This is much more efficient than loading all tasks just to count them.
867    /// Used by session restore when there's no focused task.
868    pub async fn get_stats(&self) -> Result<WorkspaceStats> {
869        let row = sqlx::query_as::<_, (i64, i64, i64, i64)>(
870            r#"SELECT
871                COUNT(*) as total,
872                COALESCE(SUM(CASE WHEN status = 'todo' THEN 1 ELSE 0 END), 0),
873                COALESCE(SUM(CASE WHEN status = 'doing' THEN 1 ELSE 0 END), 0),
874                COALESCE(SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END), 0)
875            FROM tasks"#,
876        )
877        .fetch_one(self.pool)
878        .await?;
879
880        Ok(WorkspaceStats {
881            total_tasks: row.0,
882            todo: row.1,
883            doing: row.2,
884            done: row.3,
885        })
886    }
887
888    /// Start a task (atomic: update status + set current)
889    #[tracing::instrument(skip(self))]
890    pub async fn start_task(&self, id: i64, with_events: bool) -> Result<TaskWithEvents> {
891        // Check if task exists first
892        let task_exists: bool =
893            sqlx::query_scalar::<_, bool>(crate::sql_constants::CHECK_TASK_EXISTS)
894                .bind(id)
895                .fetch_one(self.pool)
896                .await?;
897
898        if !task_exists {
899            return Err(IntentError::TaskNotFound(id));
900        }
901
902        // Check if task is blocked by incomplete dependencies
903        use crate::dependencies::get_incomplete_blocking_tasks;
904        if let Some(blocking_tasks) = get_incomplete_blocking_tasks(self.pool, id).await? {
905            return Err(IntentError::TaskBlocked {
906                task_id: id,
907                blocking_task_ids: blocking_tasks,
908            });
909        }
910
911        let mut tx = self.pool.begin().await?;
912
913        let now = Utc::now();
914
915        // Update task status to doing
916        sqlx::query(
917            r#"
918            UPDATE tasks
919            SET status = 'doing', first_doing_at = COALESCE(first_doing_at, ?)
920            WHERE id = ?
921            "#,
922        )
923        .bind(now)
924        .bind(id)
925        .execute(&mut *tx)
926        .await?;
927
928        // Set as current task in sessions table
929        // Use session_id from environment if available
930        let session_id = crate::workspace::resolve_session_id(None);
931        sqlx::query(
932            r#"
933            INSERT INTO sessions (session_id, current_task_id, created_at, last_active_at)
934            VALUES (?, ?, datetime('now'), datetime('now'))
935            ON CONFLICT(session_id) DO UPDATE SET
936                current_task_id = excluded.current_task_id,
937                last_active_at = datetime('now')
938            "#,
939        )
940        .bind(&session_id)
941        .bind(id)
942        .execute(&mut *tx)
943        .await?;
944
945        tx.commit().await?;
946
947        if with_events {
948            let result = self.get_task_with_events(id).await?;
949            self.notify_task_updated(&result.task).await;
950            Ok(result)
951        } else {
952            let task = self.get_task(id).await?;
953            self.notify_task_updated(&task).await;
954            Ok(TaskWithEvents {
955                task,
956                events_summary: None,
957            })
958        }
959    }
960
961    /// Complete the current focused task (atomic: check children + update status + clear current)
962    /// This command only operates on the current_task_id.
963    /// Prerequisites: A task must be set as current
964    ///
965    /// # Arguments
966    /// * `is_ai_caller` - Whether this is called from AI (MCP) or human (CLI/Dashboard).
967    ///   When true and task is human-owned, the operation will fail.
968    ///   Human tasks can only be completed via CLI or Dashboard.
969    #[tracing::instrument(skip(self))]
970    pub async fn done_task(&self, is_ai_caller: bool) -> Result<DoneTaskResponse> {
971        let session_id = crate::workspace::resolve_session_id(None);
972        let mut tx = self.pool.begin().await?;
973
974        // Get the current task ID from sessions table
975        let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
976            "SELECT current_task_id FROM sessions WHERE session_id = ?",
977        )
978        .bind(&session_id)
979        .fetch_optional(&mut *tx)
980        .await?
981        .flatten();
982
983        let id = current_task_id.ok_or(IntentError::InvalidInput(
984            "No current task is set. Use 'current --set <ID>' to set a task first.".to_string(),
985        ))?;
986
987        // Get the task info before completing it (including owner)
988        let task_info: (String, Option<i64>, String) =
989            sqlx::query_as("SELECT name, parent_id, owner FROM tasks WHERE id = ?")
990                .bind(id)
991                .fetch_one(&mut *tx)
992                .await?;
993        let (task_name, parent_id, owner) = task_info;
994
995        // Human Task Protection: AI cannot complete human-owned tasks
996        // Human must complete their own tasks via CLI or Dashboard
997        if owner == "human" && is_ai_caller {
998            return Err(IntentError::HumanTaskCannotBeCompletedByAI {
999                task_id: id,
1000                task_name: task_name.clone(),
1001            });
1002        }
1003
1004        // Check if all children are done
1005        let uncompleted_children: i64 = sqlx::query_scalar::<_, i64>(
1006            "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done'",
1007        )
1008        .bind(id)
1009        .fetch_one(&mut *tx)
1010        .await?;
1011
1012        if uncompleted_children > 0 {
1013            return Err(IntentError::UncompletedChildren);
1014        }
1015
1016        let now = Utc::now();
1017
1018        // Update task status to done
1019        sqlx::query(
1020            r#"
1021            UPDATE tasks
1022            SET status = 'done', first_done_at = COALESCE(first_done_at, ?)
1023            WHERE id = ?
1024            "#,
1025        )
1026        .bind(now)
1027        .bind(id)
1028        .execute(&mut *tx)
1029        .await?;
1030
1031        // Clear the current task in sessions table for this session
1032        sqlx::query("UPDATE sessions SET current_task_id = NULL, last_active_at = datetime('now') WHERE session_id = ?")
1033            .bind(&session_id)
1034            .execute(&mut *tx)
1035            .await?;
1036
1037        // Determine next step suggestion based on context
1038        let next_step_suggestion = if let Some(parent_task_id) = parent_id {
1039            // Task has a parent - check sibling status
1040            let remaining_siblings: i64 = sqlx::query_scalar::<_, i64>(
1041                "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done' AND id != ?",
1042            )
1043            .bind(parent_task_id)
1044            .bind(id)
1045            .fetch_one(&mut *tx)
1046            .await?;
1047
1048            if remaining_siblings == 0 {
1049                // All siblings are done - parent is ready
1050                let parent_name: String =
1051                    sqlx::query_scalar::<_, String>(crate::sql_constants::SELECT_TASK_NAME)
1052                        .bind(parent_task_id)
1053                        .fetch_one(&mut *tx)
1054                        .await?;
1055
1056                NextStepSuggestion::ParentIsReady {
1057                    message: format!(
1058                        "All sub-tasks of parent #{} '{}' are now complete. The parent task is ready for your attention.",
1059                        parent_task_id, parent_name
1060                    ),
1061                    parent_task_id,
1062                    parent_task_name: parent_name,
1063                }
1064            } else {
1065                // Siblings remain
1066                let parent_name: String =
1067                    sqlx::query_scalar::<_, String>(crate::sql_constants::SELECT_TASK_NAME)
1068                        .bind(parent_task_id)
1069                        .fetch_one(&mut *tx)
1070                        .await?;
1071
1072                NextStepSuggestion::SiblingTasksRemain {
1073                    message: format!(
1074                        "Task #{} completed. Parent task #{} '{}' has other sub-tasks remaining.",
1075                        id, parent_task_id, parent_name
1076                    ),
1077                    parent_task_id,
1078                    parent_task_name: parent_name,
1079                    remaining_siblings_count: remaining_siblings,
1080                }
1081            }
1082        } else {
1083            // No parent - check if this was a top-level task with children or standalone
1084            let child_count: i64 =
1085                sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_CHILDREN_TOTAL)
1086                    .bind(id)
1087                    .fetch_one(&mut *tx)
1088                    .await?;
1089
1090            if child_count > 0 {
1091                // Top-level task with children completed
1092                NextStepSuggestion::TopLevelTaskCompleted {
1093                    message: format!(
1094                        "Top-level task #{} '{}' has been completed. Well done!",
1095                        id, task_name
1096                    ),
1097                    completed_task_id: id,
1098                    completed_task_name: task_name.clone(),
1099                }
1100            } else {
1101                // Check if workspace is clear
1102                let remaining_tasks: i64 = sqlx::query_scalar::<_, i64>(
1103                    "SELECT COUNT(*) FROM tasks WHERE status != 'done' AND id != ?",
1104                )
1105                .bind(id)
1106                .fetch_one(&mut *tx)
1107                .await?;
1108
1109                if remaining_tasks == 0 {
1110                    NextStepSuggestion::WorkspaceIsClear {
1111                        message: format!(
1112                            "Project complete! Task #{} was the last remaining task. There are no more 'todo' or 'doing' tasks.",
1113                            id
1114                        ),
1115                        completed_task_id: id,
1116                    }
1117                } else {
1118                    NextStepSuggestion::NoParentContext {
1119                        message: format!("Task #{} '{}' has been completed.", id, task_name),
1120                        completed_task_id: id,
1121                        completed_task_name: task_name.clone(),
1122                    }
1123                }
1124            }
1125        };
1126
1127        tx.commit().await?;
1128
1129        // Fetch the completed task to notify UI
1130        let completed_task = self.get_task(id).await?;
1131        self.notify_task_updated(&completed_task).await;
1132
1133        Ok(DoneTaskResponse {
1134            completed_task,
1135            workspace_status: WorkspaceStatus {
1136                current_task_id: None,
1137            },
1138            next_step_suggestion,
1139        })
1140    }
1141
1142    /// Check if a task exists
1143    async fn check_task_exists(&self, id: i64) -> Result<()> {
1144        let exists: bool = sqlx::query_scalar::<_, bool>(crate::sql_constants::CHECK_TASK_EXISTS)
1145            .bind(id)
1146            .fetch_one(self.pool)
1147            .await?;
1148
1149        if !exists {
1150            return Err(IntentError::TaskNotFound(id));
1151        }
1152
1153        Ok(())
1154    }
1155
1156    /// Check for circular dependencies
1157    async fn check_circular_dependency(&self, task_id: i64, new_parent_id: i64) -> Result<()> {
1158        let mut current_id = new_parent_id;
1159
1160        loop {
1161            if current_id == task_id {
1162                return Err(IntentError::CircularDependency {
1163                    blocking_task_id: new_parent_id,
1164                    blocked_task_id: task_id,
1165                });
1166            }
1167
1168            let parent: Option<i64> =
1169                sqlx::query_scalar::<_, Option<i64>>(crate::sql_constants::SELECT_TASK_PARENT_ID)
1170                    .bind(current_id)
1171                    .fetch_optional(self.pool)
1172                    .await?
1173                    .flatten();
1174
1175            match parent {
1176                Some(pid) => current_id = pid,
1177                None => break,
1178            }
1179        }
1180
1181        Ok(())
1182    }
1183    /// Create a subtask under the current task and switch to it (atomic operation)
1184    /// Returns error if there is no current task
1185    /// Returns response with subtask info and parent task info
1186    pub async fn spawn_subtask(
1187        &self,
1188        name: &str,
1189        spec: Option<&str>,
1190    ) -> Result<SpawnSubtaskResponse> {
1191        // Get current task from sessions table for this session
1192        let session_id = crate::workspace::resolve_session_id(None);
1193        let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1194            "SELECT current_task_id FROM sessions WHERE session_id = ?",
1195        )
1196        .bind(&session_id)
1197        .fetch_optional(self.pool)
1198        .await?
1199        .flatten();
1200
1201        let parent_id = current_task_id.ok_or(IntentError::InvalidInput(
1202            "No current task to create subtask under".to_string(),
1203        ))?;
1204
1205        // Get parent task info
1206        let parent_name: String =
1207            sqlx::query_scalar::<_, String>(crate::sql_constants::SELECT_TASK_NAME)
1208                .bind(parent_id)
1209                .fetch_one(self.pool)
1210                .await?;
1211
1212        // Create the subtask with AI ownership (CLI operation)
1213        let subtask = self
1214            .add_task(name, spec, Some(parent_id), Some("ai"))
1215            .await?;
1216
1217        // Start the new subtask (sets status to doing and updates current_task_id)
1218        // This keeps the parent task in 'doing' status (multi-doing design)
1219        self.start_task(subtask.id, false).await?;
1220
1221        Ok(SpawnSubtaskResponse {
1222            subtask: SubtaskInfo {
1223                id: subtask.id,
1224                name: subtask.name,
1225                parent_id,
1226                status: "doing".to_string(),
1227            },
1228            parent_task: ParentTaskInfo {
1229                id: parent_id,
1230                name: parent_name,
1231            },
1232        })
1233    }
1234
1235    /// Intelligently pick tasks from 'todo' and transition them to 'doing'
1236    /// Returns tasks that were successfully transitioned
1237    ///
1238    /// # Arguments
1239    /// * `max_count` - Maximum number of tasks to pick
1240    /// * `capacity_limit` - Maximum total number of tasks allowed in 'doing' status
1241    ///
1242    /// # Logic
1243    /// 1. Check current 'doing' task count
1244    /// 2. Calculate available capacity
1245    /// 3. Select tasks from 'todo' (prioritized by: priority DESC, complexity ASC)
1246    /// 4. Transition selected tasks to 'doing'
1247    pub async fn pick_next_tasks(
1248        &self,
1249        max_count: usize,
1250        capacity_limit: usize,
1251    ) -> Result<Vec<Task>> {
1252        let mut tx = self.pool.begin().await?;
1253
1254        // Get current doing count
1255        let doing_count: i64 =
1256            sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_DOING)
1257                .fetch_one(&mut *tx)
1258                .await?;
1259
1260        // Calculate available capacity
1261        let available = capacity_limit.saturating_sub(doing_count as usize);
1262        if available == 0 {
1263            return Ok(vec![]);
1264        }
1265
1266        let limit = std::cmp::min(max_count, available);
1267
1268        // Select tasks from todo, prioritizing by priority DESC, complexity ASC
1269        let todo_tasks = sqlx::query_as::<_, Task>(
1270            r#"
1271                        SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner
1272                        FROM tasks
1273                        WHERE status = 'todo'
1274                        ORDER BY
1275                            COALESCE(priority, 0) ASC,
1276                            COALESCE(complexity, 5) ASC,
1277                            id ASC
1278                        LIMIT ?
1279                        "#,
1280        )
1281        .bind(limit as i64)
1282        .fetch_all(&mut *tx)
1283        .await?;
1284
1285        if todo_tasks.is_empty() {
1286            return Ok(vec![]);
1287        }
1288
1289        let now = Utc::now();
1290
1291        // Transition selected tasks to 'doing'
1292        for task in &todo_tasks {
1293            sqlx::query(
1294                r#"
1295                UPDATE tasks
1296                SET status = 'doing',
1297                    first_doing_at = COALESCE(first_doing_at, ?)
1298                WHERE id = ?
1299                "#,
1300            )
1301            .bind(now)
1302            .bind(task.id)
1303            .execute(&mut *tx)
1304            .await?;
1305        }
1306
1307        tx.commit().await?;
1308
1309        // Fetch and return updated tasks in the same order
1310        let task_ids: Vec<i64> = todo_tasks.iter().map(|t| t.id).collect();
1311        let placeholders = vec!["?"; task_ids.len()].join(",");
1312        let query = format!(
1313            "SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner
1314                         FROM tasks WHERE id IN ({})
1315                         ORDER BY
1316                             COALESCE(priority, 0) ASC,
1317                             COALESCE(complexity, 5) ASC,
1318                             id ASC",
1319            placeholders
1320        );
1321
1322        let mut q = sqlx::query_as::<_, Task>(&query);
1323        for id in task_ids {
1324            q = q.bind(id);
1325        }
1326
1327        let updated_tasks = q.fetch_all(self.pool).await?;
1328        Ok(updated_tasks)
1329    }
1330
1331    /// Intelligently recommend the next task to work on based on context-aware priority model.
1332    ///
1333    /// Priority logic:
1334    /// 1. First priority: Subtasks of the current focused task (depth-first)
1335    /// 2. Second priority: Top-level tasks (breadth-first)
1336    /// 3. No recommendation: Return appropriate empty state
1337    ///
1338    /// This command does NOT modify task status.
1339    pub async fn pick_next(&self) -> Result<PickNextResponse> {
1340        // Step 1: Check if there's a current focused task for this session
1341        let session_id = crate::workspace::resolve_session_id(None);
1342        let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1343            "SELECT current_task_id FROM sessions WHERE session_id = ?",
1344        )
1345        .bind(&session_id)
1346        .fetch_optional(self.pool)
1347        .await?
1348        .flatten();
1349
1350        if let Some(current_id) = current_task_id {
1351            // Step 1a: First priority - Get **doing** subtasks of current focused task
1352            // Exclude tasks blocked by incomplete dependencies
1353            let doing_subtasks = sqlx::query_as::<_, Task>(
1354                r#"
1355                        SELECT id, parent_id, name, spec, status, complexity, priority,
1356                               first_todo_at, first_doing_at, first_done_at, active_form, owner
1357                        FROM tasks
1358                        WHERE parent_id = ? AND status = 'doing'
1359                          AND NOT EXISTS (
1360                            SELECT 1 FROM dependencies d
1361                            JOIN tasks bt ON d.blocking_task_id = bt.id
1362                            WHERE d.blocked_task_id = tasks.id
1363                              AND bt.status != 'done'
1364                          )
1365                        ORDER BY COALESCE(priority, 999999) ASC, id ASC
1366                        LIMIT 1
1367                        "#,
1368            )
1369            .bind(current_id)
1370            .fetch_optional(self.pool)
1371            .await?;
1372
1373            if let Some(task) = doing_subtasks {
1374                return Ok(PickNextResponse::focused_subtask(task));
1375            }
1376
1377            // Step 1b: Second priority - Get **todo** subtasks if no doing subtasks
1378            let todo_subtasks = sqlx::query_as::<_, Task>(
1379                r#"
1380                            SELECT id, parent_id, name, spec, status, complexity, priority,
1381                                   first_todo_at, first_doing_at, first_done_at, active_form, owner
1382                            FROM tasks
1383                            WHERE parent_id = ? AND status = 'todo'
1384                              AND NOT EXISTS (
1385                                SELECT 1 FROM dependencies d
1386                                JOIN tasks bt ON d.blocking_task_id = bt.id
1387                                WHERE d.blocked_task_id = tasks.id
1388                                  AND bt.status != 'done'
1389                              )
1390                            ORDER BY COALESCE(priority, 999999) ASC, id ASC
1391                            LIMIT 1
1392                            "#,
1393            )
1394            .bind(current_id)
1395            .fetch_optional(self.pool)
1396            .await?;
1397
1398            if let Some(task) = todo_subtasks {
1399                return Ok(PickNextResponse::focused_subtask(task));
1400            }
1401        }
1402
1403        // Step 2a: Third priority - Get top-level **doing** tasks (excluding current task)
1404        // Exclude tasks blocked by incomplete dependencies
1405        let doing_top_level = if let Some(current_id) = current_task_id {
1406            sqlx::query_as::<_, Task>(
1407                r#"
1408                SELECT id, parent_id, name, spec, status, complexity, priority,
1409                       first_todo_at, first_doing_at, first_done_at, active_form, owner
1410                FROM tasks
1411                WHERE parent_id IS NULL AND status = 'doing' AND id != ?
1412                  AND NOT EXISTS (
1413                    SELECT 1 FROM dependencies d
1414                    JOIN tasks bt ON d.blocking_task_id = bt.id
1415                    WHERE d.blocked_task_id = tasks.id
1416                      AND bt.status != 'done'
1417                  )
1418                ORDER BY COALESCE(priority, 999999) ASC, id ASC
1419                LIMIT 1
1420                "#,
1421            )
1422            .bind(current_id)
1423            .fetch_optional(self.pool)
1424            .await?
1425        } else {
1426            sqlx::query_as::<_, Task>(
1427                r#"
1428                SELECT id, parent_id, name, spec, status, complexity, priority,
1429                       first_todo_at, first_doing_at, first_done_at, active_form, owner
1430                FROM tasks
1431                WHERE parent_id IS NULL AND status = 'doing'
1432                  AND NOT EXISTS (
1433                    SELECT 1 FROM dependencies d
1434                    JOIN tasks bt ON d.blocking_task_id = bt.id
1435                    WHERE d.blocked_task_id = tasks.id
1436                      AND bt.status != 'done'
1437                  )
1438                ORDER BY COALESCE(priority, 999999) ASC, id ASC
1439                LIMIT 1
1440                "#,
1441            )
1442            .fetch_optional(self.pool)
1443            .await?
1444        };
1445
1446        if let Some(task) = doing_top_level {
1447            return Ok(PickNextResponse::top_level_task(task));
1448        }
1449
1450        // Step 2b: Fourth priority - Get top-level **todo** tasks
1451        // Exclude tasks blocked by incomplete dependencies
1452        let todo_top_level = sqlx::query_as::<_, Task>(
1453            r#"
1454            SELECT id, parent_id, name, spec, status, complexity, priority,
1455                   first_todo_at, first_doing_at, first_done_at, active_form, owner
1456            FROM tasks
1457            WHERE parent_id IS NULL AND status = 'todo'
1458              AND NOT EXISTS (
1459                SELECT 1 FROM dependencies d
1460                JOIN tasks bt ON d.blocking_task_id = bt.id
1461                WHERE d.blocked_task_id = tasks.id
1462                  AND bt.status != 'done'
1463              )
1464            ORDER BY COALESCE(priority, 999999) ASC, id ASC
1465            LIMIT 1
1466            "#,
1467        )
1468        .fetch_optional(self.pool)
1469        .await?;
1470
1471        if let Some(task) = todo_top_level {
1472            return Ok(PickNextResponse::top_level_task(task));
1473        }
1474
1475        // Step 3: No recommendation - determine why
1476        // Check if there are any tasks at all
1477        let total_tasks: i64 =
1478            sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_TOTAL)
1479                .fetch_one(self.pool)
1480                .await?;
1481
1482        if total_tasks == 0 {
1483            return Ok(PickNextResponse::no_tasks_in_project());
1484        }
1485
1486        // Check if all tasks are completed
1487        let todo_or_doing_count: i64 = sqlx::query_scalar::<_, i64>(
1488            "SELECT COUNT(*) FROM tasks WHERE status IN ('todo', 'doing')",
1489        )
1490        .fetch_one(self.pool)
1491        .await?;
1492
1493        if todo_or_doing_count == 0 {
1494            return Ok(PickNextResponse::all_tasks_completed());
1495        }
1496
1497        // Otherwise, there are tasks but none available based on current context
1498        Ok(PickNextResponse::no_available_todos())
1499    }
1500}
1501
1502#[cfg(test)]
1503mod tests {
1504    use super::*;
1505    use crate::events::EventManager;
1506    use crate::test_utils::test_helpers::TestContext;
1507    use crate::workspace::WorkspaceManager;
1508
1509    #[tokio::test]
1510    async fn test_get_stats_empty() {
1511        let ctx = TestContext::new().await;
1512        let manager = TaskManager::new(ctx.pool());
1513
1514        let stats = manager.get_stats().await.unwrap();
1515
1516        assert_eq!(stats.total_tasks, 0);
1517        assert_eq!(stats.todo, 0);
1518        assert_eq!(stats.doing, 0);
1519        assert_eq!(stats.done, 0);
1520    }
1521
1522    #[tokio::test]
1523    async fn test_get_stats_with_tasks() {
1524        let ctx = TestContext::new().await;
1525        let manager = TaskManager::new(ctx.pool());
1526
1527        // Create tasks with different statuses
1528        let task1 = manager.add_task("Task 1", None, None, None).await.unwrap();
1529        let task2 = manager.add_task("Task 2", None, None, None).await.unwrap();
1530        let _task3 = manager.add_task("Task 3", None, None, None).await.unwrap();
1531
1532        // Update statuses
1533        manager
1534            .update_task(task1.id, None, None, None, Some("doing"), None, None)
1535            .await
1536            .unwrap();
1537        manager
1538            .update_task(task2.id, None, None, None, Some("done"), None, None)
1539            .await
1540            .unwrap();
1541        // task3 stays as todo
1542
1543        let stats = manager.get_stats().await.unwrap();
1544
1545        assert_eq!(stats.total_tasks, 3);
1546        assert_eq!(stats.todo, 1);
1547        assert_eq!(stats.doing, 1);
1548        assert_eq!(stats.done, 1);
1549    }
1550
1551    #[tokio::test]
1552    async fn test_add_task() {
1553        let ctx = TestContext::new().await;
1554        let manager = TaskManager::new(ctx.pool());
1555
1556        let task = manager
1557            .add_task("Test task", None, None, None)
1558            .await
1559            .unwrap();
1560
1561        assert_eq!(task.name, "Test task");
1562        assert_eq!(task.status, "todo");
1563        assert!(task.first_todo_at.is_some());
1564        assert!(task.first_doing_at.is_none());
1565        assert!(task.first_done_at.is_none());
1566    }
1567
1568    #[tokio::test]
1569    async fn test_add_task_with_spec() {
1570        let ctx = TestContext::new().await;
1571        let manager = TaskManager::new(ctx.pool());
1572
1573        let spec = "This is a task specification";
1574        let task = manager
1575            .add_task("Test task", Some(spec), None, None)
1576            .await
1577            .unwrap();
1578
1579        assert_eq!(task.name, "Test task");
1580        assert_eq!(task.spec.as_deref(), Some(spec));
1581    }
1582
1583    #[tokio::test]
1584    async fn test_add_task_with_parent() {
1585        let ctx = TestContext::new().await;
1586        let manager = TaskManager::new(ctx.pool());
1587
1588        let parent = manager
1589            .add_task("Parent task", None, None, None)
1590            .await
1591            .unwrap();
1592        let child = manager
1593            .add_task("Child task", None, Some(parent.id), None)
1594            .await
1595            .unwrap();
1596
1597        assert_eq!(child.parent_id, Some(parent.id));
1598    }
1599
1600    #[tokio::test]
1601    async fn test_get_task() {
1602        let ctx = TestContext::new().await;
1603        let manager = TaskManager::new(ctx.pool());
1604
1605        let created = manager
1606            .add_task("Test task", None, None, None)
1607            .await
1608            .unwrap();
1609        let retrieved = manager.get_task(created.id).await.unwrap();
1610
1611        assert_eq!(created.id, retrieved.id);
1612        assert_eq!(created.name, retrieved.name);
1613    }
1614
1615    #[tokio::test]
1616    async fn test_get_task_not_found() {
1617        let ctx = TestContext::new().await;
1618        let manager = TaskManager::new(ctx.pool());
1619
1620        let result = manager.get_task(999).await;
1621        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1622    }
1623
1624    #[tokio::test]
1625    async fn test_update_task_name() {
1626        let ctx = TestContext::new().await;
1627        let manager = TaskManager::new(ctx.pool());
1628
1629        let task = manager
1630            .add_task("Original name", None, None, None)
1631            .await
1632            .unwrap();
1633        let updated = manager
1634            .update_task(task.id, Some("New name"), None, None, None, None, None)
1635            .await
1636            .unwrap();
1637
1638        assert_eq!(updated.name, "New name");
1639    }
1640
1641    #[tokio::test]
1642    async fn test_update_task_status() {
1643        let ctx = TestContext::new().await;
1644        let manager = TaskManager::new(ctx.pool());
1645
1646        let task = manager
1647            .add_task("Test task", None, None, None)
1648            .await
1649            .unwrap();
1650        let updated = manager
1651            .update_task(task.id, None, None, None, Some("doing"), None, None)
1652            .await
1653            .unwrap();
1654
1655        assert_eq!(updated.status, "doing");
1656        assert!(updated.first_doing_at.is_some());
1657    }
1658
1659    #[tokio::test]
1660    async fn test_delete_task() {
1661        let ctx = TestContext::new().await;
1662        let manager = TaskManager::new(ctx.pool());
1663
1664        let task = manager
1665            .add_task("Test task", None, None, None)
1666            .await
1667            .unwrap();
1668        manager.delete_task(task.id).await.unwrap();
1669
1670        let result = manager.get_task(task.id).await;
1671        assert!(result.is_err());
1672    }
1673
1674    #[tokio::test]
1675    async fn test_find_tasks_by_status() {
1676        let ctx = TestContext::new().await;
1677        let manager = TaskManager::new(ctx.pool());
1678
1679        manager
1680            .add_task("Todo task", None, None, None)
1681            .await
1682            .unwrap();
1683        let doing_task = manager
1684            .add_task("Doing task", None, None, None)
1685            .await
1686            .unwrap();
1687        manager
1688            .update_task(doing_task.id, None, None, None, Some("doing"), None, None)
1689            .await
1690            .unwrap();
1691
1692        let todo_result = manager
1693            .find_tasks(Some("todo"), None, None, None, None)
1694            .await
1695            .unwrap();
1696        let doing_result = manager
1697            .find_tasks(Some("doing"), None, None, None, None)
1698            .await
1699            .unwrap();
1700
1701        assert_eq!(todo_result.tasks.len(), 1);
1702        assert_eq!(doing_result.tasks.len(), 1);
1703        assert_eq!(doing_result.tasks[0].status, "doing");
1704    }
1705
1706    #[tokio::test]
1707    async fn test_find_tasks_by_parent() {
1708        let ctx = TestContext::new().await;
1709        let manager = TaskManager::new(ctx.pool());
1710
1711        let parent = manager.add_task("Parent", None, None, None).await.unwrap();
1712        manager
1713            .add_task("Child 1", None, Some(parent.id), None)
1714            .await
1715            .unwrap();
1716        manager
1717            .add_task("Child 2", None, Some(parent.id), None)
1718            .await
1719            .unwrap();
1720
1721        let result = manager
1722            .find_tasks(None, Some(Some(parent.id)), None, None, None)
1723            .await
1724            .unwrap();
1725
1726        assert_eq!(result.tasks.len(), 2);
1727    }
1728
1729    #[tokio::test]
1730    async fn test_start_task() {
1731        let ctx = TestContext::new().await;
1732        let manager = TaskManager::new(ctx.pool());
1733
1734        let task = manager
1735            .add_task("Test task", None, None, None)
1736            .await
1737            .unwrap();
1738        let started = manager.start_task(task.id, false).await.unwrap();
1739
1740        assert_eq!(started.task.status, "doing");
1741        assert!(started.task.first_doing_at.is_some());
1742
1743        // Verify it's set as current task
1744        let session_id = crate::workspace::resolve_session_id(None);
1745        let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1746            "SELECT current_task_id FROM sessions WHERE session_id = ?",
1747        )
1748        .bind(&session_id)
1749        .fetch_optional(ctx.pool())
1750        .await
1751        .unwrap()
1752        .flatten();
1753
1754        assert_eq!(current, Some(task.id));
1755    }
1756
1757    #[tokio::test]
1758    async fn test_start_task_with_events() {
1759        let ctx = TestContext::new().await;
1760        let manager = TaskManager::new(ctx.pool());
1761
1762        let task = manager
1763            .add_task("Test task", None, None, None)
1764            .await
1765            .unwrap();
1766
1767        // Add an event
1768        sqlx::query("INSERT INTO events (task_id, log_type, discussion_data) VALUES (?, ?, ?)")
1769            .bind(task.id)
1770            .bind("test")
1771            .bind("test event")
1772            .execute(ctx.pool())
1773            .await
1774            .unwrap();
1775
1776        let started = manager.start_task(task.id, true).await.unwrap();
1777
1778        assert!(started.events_summary.is_some());
1779        let summary = started.events_summary.unwrap();
1780        assert_eq!(summary.total_count, 1);
1781    }
1782
1783    #[tokio::test]
1784    async fn test_done_task() {
1785        let ctx = TestContext::new().await;
1786        let manager = TaskManager::new(ctx.pool());
1787
1788        let task = manager
1789            .add_task("Test task", None, None, None)
1790            .await
1791            .unwrap();
1792        manager.start_task(task.id, false).await.unwrap();
1793        let response = manager.done_task(false).await.unwrap();
1794
1795        assert_eq!(response.completed_task.status, "done");
1796        assert!(response.completed_task.first_done_at.is_some());
1797        assert_eq!(response.workspace_status.current_task_id, None);
1798
1799        // Should be WORKSPACE_IS_CLEAR since it's the only task
1800        match response.next_step_suggestion {
1801            NextStepSuggestion::WorkspaceIsClear { .. } => {},
1802            _ => panic!("Expected WorkspaceIsClear suggestion"),
1803        }
1804
1805        // Verify current task is cleared
1806        let session_id = crate::workspace::resolve_session_id(None);
1807        let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1808            "SELECT current_task_id FROM sessions WHERE session_id = ?",
1809        )
1810        .bind(&session_id)
1811        .fetch_optional(ctx.pool())
1812        .await
1813        .unwrap()
1814        .flatten();
1815
1816        assert!(current.is_none());
1817    }
1818
1819    #[tokio::test]
1820    async fn test_done_task_with_uncompleted_children() {
1821        let ctx = TestContext::new().await;
1822        let manager = TaskManager::new(ctx.pool());
1823
1824        let parent = manager.add_task("Parent", None, None, None).await.unwrap();
1825        manager
1826            .add_task("Child", None, Some(parent.id), None)
1827            .await
1828            .unwrap();
1829
1830        // Set parent as current task
1831        manager.start_task(parent.id, false).await.unwrap();
1832
1833        let result = manager.done_task(false).await;
1834        assert!(matches!(result, Err(IntentError::UncompletedChildren)));
1835    }
1836
1837    #[tokio::test]
1838    async fn test_done_task_with_completed_children() {
1839        let ctx = TestContext::new().await;
1840        let manager = TaskManager::new(ctx.pool());
1841
1842        let parent = manager.add_task("Parent", None, None, None).await.unwrap();
1843        let child = manager
1844            .add_task("Child", None, Some(parent.id), None)
1845            .await
1846            .unwrap();
1847
1848        // Complete child first
1849        manager.start_task(child.id, false).await.unwrap();
1850        let child_response = manager.done_task(false).await.unwrap();
1851
1852        // Child completion should suggest parent is ready
1853        match child_response.next_step_suggestion {
1854            NextStepSuggestion::ParentIsReady { parent_task_id, .. } => {
1855                assert_eq!(parent_task_id, parent.id);
1856            },
1857            _ => panic!("Expected ParentIsReady suggestion"),
1858        }
1859
1860        // Now parent can be completed
1861        manager.start_task(parent.id, false).await.unwrap();
1862        let parent_response = manager.done_task(false).await.unwrap();
1863        assert_eq!(parent_response.completed_task.status, "done");
1864
1865        // Parent completion should indicate top-level task completed (since it had children)
1866        match parent_response.next_step_suggestion {
1867            NextStepSuggestion::TopLevelTaskCompleted { .. } => {},
1868            _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1869        }
1870    }
1871
1872    #[tokio::test]
1873    async fn test_circular_dependency() {
1874        let ctx = TestContext::new().await;
1875        let manager = TaskManager::new(ctx.pool());
1876
1877        let task1 = manager.add_task("Task 1", None, None, None).await.unwrap();
1878        let task2 = manager
1879            .add_task("Task 2", None, Some(task1.id), None)
1880            .await
1881            .unwrap();
1882
1883        // Try to make task1 a child of task2 (circular)
1884        let result = manager
1885            .update_task(task1.id, None, None, Some(Some(task2.id)), None, None, None)
1886            .await;
1887
1888        assert!(matches!(
1889            result,
1890            Err(IntentError::CircularDependency { .. })
1891        ));
1892    }
1893
1894    #[tokio::test]
1895    async fn test_invalid_parent_id() {
1896        let ctx = TestContext::new().await;
1897        let manager = TaskManager::new(ctx.pool());
1898
1899        let result = manager.add_task("Test", None, Some(999), None).await;
1900        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1901    }
1902
1903    #[tokio::test]
1904    async fn test_update_task_complexity_and_priority() {
1905        let ctx = TestContext::new().await;
1906        let manager = TaskManager::new(ctx.pool());
1907
1908        let task = manager
1909            .add_task("Test task", None, None, None)
1910            .await
1911            .unwrap();
1912        let updated = manager
1913            .update_task(task.id, None, None, None, None, Some(8), Some(10))
1914            .await
1915            .unwrap();
1916
1917        assert_eq!(updated.complexity, Some(8));
1918        assert_eq!(updated.priority, Some(10));
1919    }
1920
1921    #[tokio::test]
1922    async fn test_spawn_subtask() {
1923        let ctx = TestContext::new().await;
1924        let manager = TaskManager::new(ctx.pool());
1925
1926        // Create and start a parent task
1927        let parent = manager
1928            .add_task("Parent task", None, None, None)
1929            .await
1930            .unwrap();
1931        manager.start_task(parent.id, false).await.unwrap();
1932
1933        // Spawn a subtask
1934        let response = manager
1935            .spawn_subtask("Child task", Some("Details"))
1936            .await
1937            .unwrap();
1938
1939        assert_eq!(response.subtask.parent_id, parent.id);
1940        assert_eq!(response.subtask.name, "Child task");
1941        assert_eq!(response.subtask.status, "doing");
1942        assert_eq!(response.parent_task.id, parent.id);
1943        assert_eq!(response.parent_task.name, "Parent task");
1944
1945        // Verify subtask is now the current task
1946        let session_id = crate::workspace::resolve_session_id(None);
1947        let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1948            "SELECT current_task_id FROM sessions WHERE session_id = ?",
1949        )
1950        .bind(&session_id)
1951        .fetch_optional(ctx.pool())
1952        .await
1953        .unwrap()
1954        .flatten();
1955
1956        assert_eq!(current, Some(response.subtask.id));
1957
1958        // Verify subtask is in doing status
1959        let retrieved = manager.get_task(response.subtask.id).await.unwrap();
1960        assert_eq!(retrieved.status, "doing");
1961    }
1962
1963    #[tokio::test]
1964    async fn test_spawn_subtask_no_current_task() {
1965        let ctx = TestContext::new().await;
1966        let manager = TaskManager::new(ctx.pool());
1967
1968        // Try to spawn subtask without a current task
1969        let result = manager.spawn_subtask("Child", None).await;
1970        assert!(result.is_err());
1971    }
1972
1973    #[tokio::test]
1974    async fn test_pick_next_tasks_basic() {
1975        let ctx = TestContext::new().await;
1976        let manager = TaskManager::new(ctx.pool());
1977
1978        // Create 10 todo tasks
1979        for i in 1..=10 {
1980            manager
1981                .add_task(&format!("Task {}", i), None, None, None)
1982                .await
1983                .unwrap();
1984        }
1985
1986        // Pick 5 tasks with capacity limit of 5
1987        let picked = manager.pick_next_tasks(5, 5).await.unwrap();
1988
1989        assert_eq!(picked.len(), 5);
1990        for task in &picked {
1991            assert_eq!(task.status, "doing");
1992            assert!(task.first_doing_at.is_some());
1993        }
1994
1995        // Verify total doing count
1996        let doing_count: i64 =
1997            sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_DOING)
1998                .fetch_one(ctx.pool())
1999                .await
2000                .unwrap();
2001
2002        assert_eq!(doing_count, 5);
2003    }
2004
2005    #[tokio::test]
2006    async fn test_pick_next_tasks_with_existing_doing() {
2007        let ctx = TestContext::new().await;
2008        let manager = TaskManager::new(ctx.pool());
2009
2010        // Create 10 todo tasks
2011        for i in 1..=10 {
2012            manager
2013                .add_task(&format!("Task {}", i), None, None, None)
2014                .await
2015                .unwrap();
2016        }
2017
2018        // Start 2 tasks
2019        let result = manager
2020            .find_tasks(Some("todo"), None, None, None, None)
2021            .await
2022            .unwrap();
2023        manager.start_task(result.tasks[0].id, false).await.unwrap();
2024        manager.start_task(result.tasks[1].id, false).await.unwrap();
2025
2026        // Pick more tasks with capacity limit of 5
2027        let picked = manager.pick_next_tasks(10, 5).await.unwrap();
2028
2029        // Should only pick 3 more (5 - 2 = 3)
2030        assert_eq!(picked.len(), 3);
2031
2032        // Verify total doing count
2033        let doing_count: i64 =
2034            sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_DOING)
2035                .fetch_one(ctx.pool())
2036                .await
2037                .unwrap();
2038
2039        assert_eq!(doing_count, 5);
2040    }
2041
2042    #[tokio::test]
2043    async fn test_pick_next_tasks_at_capacity() {
2044        let ctx = TestContext::new().await;
2045        let manager = TaskManager::new(ctx.pool());
2046
2047        // Create 10 tasks
2048        for i in 1..=10 {
2049            manager
2050                .add_task(&format!("Task {}", i), None, None, None)
2051                .await
2052                .unwrap();
2053        }
2054
2055        // Fill capacity
2056        let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
2057        assert_eq!(first_batch.len(), 5);
2058
2059        // Try to pick more (should return empty)
2060        let second_batch = manager.pick_next_tasks(5, 5).await.unwrap();
2061        assert_eq!(second_batch.len(), 0);
2062    }
2063
2064    #[tokio::test]
2065    async fn test_pick_next_tasks_priority_ordering() {
2066        let ctx = TestContext::new().await;
2067        let manager = TaskManager::new(ctx.pool());
2068
2069        // Create tasks with different priorities
2070        let low = manager
2071            .add_task("Low priority", None, None, None)
2072            .await
2073            .unwrap();
2074        manager
2075            .update_task(low.id, None, None, None, None, None, Some(1))
2076            .await
2077            .unwrap();
2078
2079        let high = manager
2080            .add_task("High priority", None, None, None)
2081            .await
2082            .unwrap();
2083        manager
2084            .update_task(high.id, None, None, None, None, None, Some(10))
2085            .await
2086            .unwrap();
2087
2088        let medium = manager
2089            .add_task("Medium priority", None, None, None)
2090            .await
2091            .unwrap();
2092        manager
2093            .update_task(medium.id, None, None, None, None, None, Some(5))
2094            .await
2095            .unwrap();
2096
2097        // Pick tasks
2098        let picked = manager.pick_next_tasks(3, 5).await.unwrap();
2099
2100        // Should be ordered by priority ASC (lower number = higher priority)
2101        assert_eq!(picked.len(), 3);
2102        assert_eq!(picked[0].priority, Some(1)); // lowest number = highest priority
2103        assert_eq!(picked[1].priority, Some(5)); // medium
2104        assert_eq!(picked[2].priority, Some(10)); // highest number = lowest priority
2105    }
2106
2107    #[tokio::test]
2108    async fn test_pick_next_tasks_complexity_ordering() {
2109        let ctx = TestContext::new().await;
2110        let manager = TaskManager::new(ctx.pool());
2111
2112        // Create tasks with different complexities (same priority)
2113        let complex = manager.add_task("Complex", None, None, None).await.unwrap();
2114        manager
2115            .update_task(complex.id, None, None, None, None, Some(9), Some(5))
2116            .await
2117            .unwrap();
2118
2119        let simple = manager.add_task("Simple", None, None, None).await.unwrap();
2120        manager
2121            .update_task(simple.id, None, None, None, None, Some(1), Some(5))
2122            .await
2123            .unwrap();
2124
2125        let medium = manager.add_task("Medium", None, None, None).await.unwrap();
2126        manager
2127            .update_task(medium.id, None, None, None, None, Some(5), Some(5))
2128            .await
2129            .unwrap();
2130
2131        // Pick tasks
2132        let picked = manager.pick_next_tasks(3, 5).await.unwrap();
2133
2134        // Should be ordered by complexity ASC (simple first)
2135        assert_eq!(picked.len(), 3);
2136        assert_eq!(picked[0].complexity, Some(1)); // simple
2137        assert_eq!(picked[1].complexity, Some(5)); // medium
2138        assert_eq!(picked[2].complexity, Some(9)); // complex
2139    }
2140
2141    #[tokio::test]
2142    async fn test_done_task_sibling_tasks_remain() {
2143        let ctx = TestContext::new().await;
2144        let manager = TaskManager::new(ctx.pool());
2145
2146        // Create parent with multiple children
2147        let parent = manager
2148            .add_task("Parent Task", None, None, None)
2149            .await
2150            .unwrap();
2151        let child1 = manager
2152            .add_task("Child 1", None, Some(parent.id), None)
2153            .await
2154            .unwrap();
2155        let child2 = manager
2156            .add_task("Child 2", None, Some(parent.id), None)
2157            .await
2158            .unwrap();
2159        let _child3 = manager
2160            .add_task("Child 3", None, Some(parent.id), None)
2161            .await
2162            .unwrap();
2163
2164        // Complete first child
2165        manager.start_task(child1.id, false).await.unwrap();
2166        let response = manager.done_task(false).await.unwrap();
2167
2168        // Should indicate siblings remain
2169        match response.next_step_suggestion {
2170            NextStepSuggestion::SiblingTasksRemain {
2171                parent_task_id,
2172                remaining_siblings_count,
2173                ..
2174            } => {
2175                assert_eq!(parent_task_id, parent.id);
2176                assert_eq!(remaining_siblings_count, 2); // child2 and child3
2177            },
2178            _ => panic!("Expected SiblingTasksRemain suggestion"),
2179        }
2180
2181        // Complete second child
2182        manager.start_task(child2.id, false).await.unwrap();
2183        let response2 = manager.done_task(false).await.unwrap();
2184
2185        // Should still indicate siblings remain
2186        match response2.next_step_suggestion {
2187            NextStepSuggestion::SiblingTasksRemain {
2188                remaining_siblings_count,
2189                ..
2190            } => {
2191                assert_eq!(remaining_siblings_count, 1); // only child3
2192            },
2193            _ => panic!("Expected SiblingTasksRemain suggestion"),
2194        }
2195    }
2196
2197    #[tokio::test]
2198    async fn test_done_task_top_level_with_children() {
2199        let ctx = TestContext::new().await;
2200        let manager = TaskManager::new(ctx.pool());
2201
2202        // Create top-level task with children
2203        let parent = manager
2204            .add_task("Epic Task", None, None, None)
2205            .await
2206            .unwrap();
2207        let child = manager
2208            .add_task("Sub Task", None, Some(parent.id), None)
2209            .await
2210            .unwrap();
2211
2212        // Complete child first
2213        manager.start_task(child.id, false).await.unwrap();
2214        manager.done_task(false).await.unwrap();
2215
2216        // Complete parent
2217        manager.start_task(parent.id, false).await.unwrap();
2218        let response = manager.done_task(false).await.unwrap();
2219
2220        // Should be TOP_LEVEL_TASK_COMPLETED
2221        match response.next_step_suggestion {
2222            NextStepSuggestion::TopLevelTaskCompleted {
2223                completed_task_id,
2224                completed_task_name,
2225                ..
2226            } => {
2227                assert_eq!(completed_task_id, parent.id);
2228                assert_eq!(completed_task_name, "Epic Task");
2229            },
2230            _ => panic!("Expected TopLevelTaskCompleted suggestion"),
2231        }
2232    }
2233
2234    #[tokio::test]
2235    async fn test_done_task_no_parent_context() {
2236        let ctx = TestContext::new().await;
2237        let manager = TaskManager::new(ctx.pool());
2238
2239        // Create multiple standalone tasks
2240        let task1 = manager
2241            .add_task("Standalone Task 1", None, None, None)
2242            .await
2243            .unwrap();
2244        let _task2 = manager
2245            .add_task("Standalone Task 2", None, None, None)
2246            .await
2247            .unwrap();
2248
2249        // Complete first task
2250        manager.start_task(task1.id, false).await.unwrap();
2251        let response = manager.done_task(false).await.unwrap();
2252
2253        // Should be NO_PARENT_CONTEXT since task2 is still pending
2254        match response.next_step_suggestion {
2255            NextStepSuggestion::NoParentContext {
2256                completed_task_id,
2257                completed_task_name,
2258                ..
2259            } => {
2260                assert_eq!(completed_task_id, task1.id);
2261                assert_eq!(completed_task_name, "Standalone Task 1");
2262            },
2263            _ => panic!("Expected NoParentContext suggestion"),
2264        }
2265    }
2266
2267    #[tokio::test]
2268    async fn test_pick_next_focused_subtask() {
2269        let ctx = TestContext::new().await;
2270        let manager = TaskManager::new(ctx.pool());
2271
2272        // Create parent task and set as current
2273        let parent = manager
2274            .add_task("Parent task", None, None, None)
2275            .await
2276            .unwrap();
2277        manager.start_task(parent.id, false).await.unwrap();
2278
2279        // Create subtasks with different priorities
2280        let subtask1 = manager
2281            .add_task("Subtask 1", None, Some(parent.id), None)
2282            .await
2283            .unwrap();
2284        let subtask2 = manager
2285            .add_task("Subtask 2", None, Some(parent.id), None)
2286            .await
2287            .unwrap();
2288
2289        // Set priorities: subtask1 = 2, subtask2 = 1 (lower number = higher priority)
2290        manager
2291            .update_task(subtask1.id, None, None, None, None, None, Some(2))
2292            .await
2293            .unwrap();
2294        manager
2295            .update_task(subtask2.id, None, None, None, None, None, Some(1))
2296            .await
2297            .unwrap();
2298
2299        // Pick next should recommend subtask2 (priority 1)
2300        let response = manager.pick_next().await.unwrap();
2301
2302        assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
2303        assert!(response.task.is_some());
2304        assert_eq!(response.task.as_ref().unwrap().id, subtask2.id);
2305        assert_eq!(response.task.as_ref().unwrap().name, "Subtask 2");
2306    }
2307
2308    #[tokio::test]
2309    async fn test_pick_next_top_level_task() {
2310        let ctx = TestContext::new().await;
2311        let manager = TaskManager::new(ctx.pool());
2312
2313        // Create top-level tasks with different priorities
2314        let task1 = manager.add_task("Task 1", None, None, None).await.unwrap();
2315        let task2 = manager.add_task("Task 2", None, None, None).await.unwrap();
2316
2317        // Set priorities: task1 = 5, task2 = 3 (lower number = higher priority)
2318        manager
2319            .update_task(task1.id, None, None, None, None, None, Some(5))
2320            .await
2321            .unwrap();
2322        manager
2323            .update_task(task2.id, None, None, None, None, None, Some(3))
2324            .await
2325            .unwrap();
2326
2327        // Pick next should recommend task2 (priority 3)
2328        let response = manager.pick_next().await.unwrap();
2329
2330        assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2331        assert!(response.task.is_some());
2332        assert_eq!(response.task.as_ref().unwrap().id, task2.id);
2333        assert_eq!(response.task.as_ref().unwrap().name, "Task 2");
2334    }
2335
2336    #[tokio::test]
2337    async fn test_pick_next_no_tasks() {
2338        let ctx = TestContext::new().await;
2339        let manager = TaskManager::new(ctx.pool());
2340
2341        // No tasks created
2342        let response = manager.pick_next().await.unwrap();
2343
2344        assert_eq!(response.suggestion_type, "NONE");
2345        assert_eq!(response.reason_code.as_deref(), Some("NO_TASKS_IN_PROJECT"));
2346        assert!(response.message.is_some());
2347    }
2348
2349    #[tokio::test]
2350    async fn test_pick_next_all_completed() {
2351        let ctx = TestContext::new().await;
2352        let manager = TaskManager::new(ctx.pool());
2353
2354        // Create task and mark as done
2355        let task = manager.add_task("Task 1", None, None, None).await.unwrap();
2356        manager.start_task(task.id, false).await.unwrap();
2357        manager.done_task(false).await.unwrap();
2358
2359        // Pick next should indicate all tasks completed
2360        let response = manager.pick_next().await.unwrap();
2361
2362        assert_eq!(response.suggestion_type, "NONE");
2363        assert_eq!(response.reason_code.as_deref(), Some("ALL_TASKS_COMPLETED"));
2364        assert!(response.message.is_some());
2365    }
2366
2367    #[tokio::test]
2368    async fn test_pick_next_no_available_todos() {
2369        let ctx = TestContext::new().await;
2370        let manager = TaskManager::new(ctx.pool());
2371
2372        // Create a parent task that's in "doing" status
2373        let parent = manager
2374            .add_task("Parent task", None, None, None)
2375            .await
2376            .unwrap();
2377        manager.start_task(parent.id, false).await.unwrap();
2378
2379        // Create a subtask also in "doing" status (no "todo" subtasks)
2380        let subtask = manager
2381            .add_task("Subtask", None, Some(parent.id), None)
2382            .await
2383            .unwrap();
2384        // Switch to subtask (this will set parent back to todo, so we need to manually set subtask to doing)
2385        sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
2386            .bind(subtask.id)
2387            .execute(ctx.pool())
2388            .await
2389            .unwrap();
2390
2391        // Set subtask as current
2392        let session_id = crate::workspace::resolve_session_id(None);
2393        sqlx::query(
2394            r#"
2395            INSERT INTO sessions (session_id, current_task_id, created_at, last_active_at)
2396            VALUES (?, ?, datetime('now'), datetime('now'))
2397            ON CONFLICT(session_id) DO UPDATE SET
2398                current_task_id = excluded.current_task_id,
2399                last_active_at = datetime('now')
2400            "#,
2401        )
2402        .bind(&session_id)
2403        .bind(subtask.id)
2404        .execute(ctx.pool())
2405        .await
2406        .unwrap();
2407
2408        // Set parent to doing (not todo)
2409        sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
2410            .bind(parent.id)
2411            .execute(ctx.pool())
2412            .await
2413            .unwrap();
2414
2415        // With multi-doing semantics, pick next should recommend the doing parent
2416        // (it's a valid top-level doing task that's not current)
2417        let response = manager.pick_next().await.unwrap();
2418
2419        assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2420        assert_eq!(response.task.as_ref().unwrap().id, parent.id);
2421        assert_eq!(response.task.as_ref().unwrap().status, "doing");
2422    }
2423
2424    #[tokio::test]
2425    async fn test_pick_next_priority_ordering() {
2426        let ctx = TestContext::new().await;
2427        let manager = TaskManager::new(ctx.pool());
2428
2429        // Create parent and set as current
2430        let parent = manager.add_task("Parent", None, None, None).await.unwrap();
2431        manager.start_task(parent.id, false).await.unwrap();
2432
2433        // Create multiple subtasks with various priorities
2434        let sub1 = manager
2435            .add_task("Priority 10", None, Some(parent.id), None)
2436            .await
2437            .unwrap();
2438        manager
2439            .update_task(sub1.id, None, None, None, None, None, Some(10))
2440            .await
2441            .unwrap();
2442
2443        let sub2 = manager
2444            .add_task("Priority 1", None, Some(parent.id), None)
2445            .await
2446            .unwrap();
2447        manager
2448            .update_task(sub2.id, None, None, None, None, None, Some(1))
2449            .await
2450            .unwrap();
2451
2452        let sub3 = manager
2453            .add_task("Priority 5", None, Some(parent.id), None)
2454            .await
2455            .unwrap();
2456        manager
2457            .update_task(sub3.id, None, None, None, None, None, Some(5))
2458            .await
2459            .unwrap();
2460
2461        // Pick next should recommend the task with priority 1 (lowest number)
2462        let response = manager.pick_next().await.unwrap();
2463
2464        assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
2465        assert_eq!(response.task.as_ref().unwrap().id, sub2.id);
2466        assert_eq!(response.task.as_ref().unwrap().name, "Priority 1");
2467    }
2468
2469    #[tokio::test]
2470    async fn test_pick_next_falls_back_to_top_level_when_no_subtasks() {
2471        let ctx = TestContext::new().await;
2472        let manager = TaskManager::new(ctx.pool());
2473
2474        // Create parent without subtasks and set as current
2475        let parent = manager.add_task("Parent", None, None, None).await.unwrap();
2476        manager.start_task(parent.id, false).await.unwrap();
2477
2478        // Create another top-level task
2479        let top_level = manager
2480            .add_task("Top level task", None, None, None)
2481            .await
2482            .unwrap();
2483
2484        // Pick next should fall back to top-level task since parent has no todo subtasks
2485        let response = manager.pick_next().await.unwrap();
2486
2487        assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2488        assert_eq!(response.task.as_ref().unwrap().id, top_level.id);
2489    }
2490
2491    // ===== Missing coverage tests =====
2492
2493    #[tokio::test]
2494    async fn test_get_task_with_events() {
2495        let ctx = TestContext::new().await;
2496        let task_mgr = TaskManager::new(ctx.pool());
2497        let event_mgr = EventManager::new(ctx.pool());
2498
2499        let task = task_mgr.add_task("Test", None, None, None).await.unwrap();
2500
2501        // Add some events
2502        event_mgr
2503            .add_event(task.id, "progress", "Event 1")
2504            .await
2505            .unwrap();
2506        event_mgr
2507            .add_event(task.id, "decision", "Event 2")
2508            .await
2509            .unwrap();
2510
2511        let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2512
2513        assert_eq!(result.task.id, task.id);
2514        assert!(result.events_summary.is_some());
2515
2516        let summary = result.events_summary.unwrap();
2517        assert_eq!(summary.total_count, 2);
2518        assert_eq!(summary.recent_events.len(), 2);
2519        assert_eq!(summary.recent_events[0].log_type, "decision"); // Most recent first
2520        assert_eq!(summary.recent_events[1].log_type, "progress");
2521    }
2522
2523    #[tokio::test]
2524    async fn test_get_task_with_events_nonexistent() {
2525        let ctx = TestContext::new().await;
2526        let task_mgr = TaskManager::new(ctx.pool());
2527
2528        let result = task_mgr.get_task_with_events(999).await;
2529        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2530    }
2531
2532    #[tokio::test]
2533    async fn test_get_task_with_many_events() {
2534        let ctx = TestContext::new().await;
2535        let task_mgr = TaskManager::new(ctx.pool());
2536        let event_mgr = EventManager::new(ctx.pool());
2537
2538        let task = task_mgr.add_task("Test", None, None, None).await.unwrap();
2539
2540        // Add 20 events
2541        for i in 0..20 {
2542            event_mgr
2543                .add_event(task.id, "test", &format!("Event {}", i))
2544                .await
2545                .unwrap();
2546        }
2547
2548        let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2549        let summary = result.events_summary.unwrap();
2550
2551        assert_eq!(summary.total_count, 20);
2552        assert_eq!(summary.recent_events.len(), 10); // Limited to 10
2553    }
2554
2555    #[tokio::test]
2556    async fn test_get_task_with_no_events() {
2557        let ctx = TestContext::new().await;
2558        let task_mgr = TaskManager::new(ctx.pool());
2559
2560        let task = task_mgr.add_task("Test", None, None, None).await.unwrap();
2561
2562        let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2563        let summary = result.events_summary.unwrap();
2564
2565        assert_eq!(summary.total_count, 0);
2566        assert_eq!(summary.recent_events.len(), 0);
2567    }
2568
2569    #[tokio::test]
2570    async fn test_pick_next_tasks_zero_capacity() {
2571        let ctx = TestContext::new().await;
2572        let task_mgr = TaskManager::new(ctx.pool());
2573
2574        task_mgr.add_task("Task 1", None, None, None).await.unwrap();
2575
2576        // capacity_limit = 0 means no capacity available
2577        let results = task_mgr.pick_next_tasks(10, 0).await.unwrap();
2578        assert_eq!(results.len(), 0);
2579    }
2580
2581    #[tokio::test]
2582    async fn test_pick_next_tasks_capacity_exceeds_available() {
2583        let ctx = TestContext::new().await;
2584        let task_mgr = TaskManager::new(ctx.pool());
2585
2586        task_mgr.add_task("Task 1", None, None, None).await.unwrap();
2587        task_mgr.add_task("Task 2", None, None, None).await.unwrap();
2588
2589        // Request 10 tasks but only 2 available, capacity = 100
2590        let results = task_mgr.pick_next_tasks(10, 100).await.unwrap();
2591        assert_eq!(results.len(), 2); // Only returns available tasks
2592    }
2593
2594    // ========== task_context tests ==========
2595
2596    #[tokio::test]
2597    async fn test_get_task_context_root_task_no_relations() {
2598        let ctx = TestContext::new().await;
2599        let task_mgr = TaskManager::new(ctx.pool());
2600
2601        // Create a single root task with no relations
2602        let task = task_mgr
2603            .add_task("Root task", None, None, None)
2604            .await
2605            .unwrap();
2606
2607        let context = task_mgr.get_task_context(task.id).await.unwrap();
2608
2609        // Verify task itself
2610        assert_eq!(context.task.id, task.id);
2611        assert_eq!(context.task.name, "Root task");
2612
2613        // No ancestors (root task)
2614        assert_eq!(context.ancestors.len(), 0);
2615
2616        // No siblings
2617        assert_eq!(context.siblings.len(), 0);
2618
2619        // No children
2620        assert_eq!(context.children.len(), 0);
2621    }
2622
2623    #[tokio::test]
2624    async fn test_get_task_context_with_siblings() {
2625        let ctx = TestContext::new().await;
2626        let task_mgr = TaskManager::new(ctx.pool());
2627
2628        // Create multiple root tasks (siblings)
2629        let task1 = task_mgr.add_task("Task 1", None, None, None).await.unwrap();
2630        let task2 = task_mgr.add_task("Task 2", None, None, None).await.unwrap();
2631        let task3 = task_mgr.add_task("Task 3", None, None, None).await.unwrap();
2632
2633        let context = task_mgr.get_task_context(task2.id).await.unwrap();
2634
2635        // Verify task itself
2636        assert_eq!(context.task.id, task2.id);
2637
2638        // No ancestors (root task)
2639        assert_eq!(context.ancestors.len(), 0);
2640
2641        // Should have 2 siblings
2642        assert_eq!(context.siblings.len(), 2);
2643        let sibling_ids: Vec<i64> = context.siblings.iter().map(|t| t.id).collect();
2644        assert!(sibling_ids.contains(&task1.id));
2645        assert!(sibling_ids.contains(&task3.id));
2646        assert!(!sibling_ids.contains(&task2.id)); // Should not include itself
2647
2648        // No children
2649        assert_eq!(context.children.len(), 0);
2650    }
2651
2652    #[tokio::test]
2653    async fn test_get_task_context_with_parent() {
2654        let ctx = TestContext::new().await;
2655        let task_mgr = TaskManager::new(ctx.pool());
2656
2657        // Create parent-child relationship
2658        let parent = task_mgr
2659            .add_task("Parent task", None, None, None)
2660            .await
2661            .unwrap();
2662        let child = task_mgr
2663            .add_task("Child task", None, Some(parent.id), None)
2664            .await
2665            .unwrap();
2666
2667        let context = task_mgr.get_task_context(child.id).await.unwrap();
2668
2669        // Verify task itself
2670        assert_eq!(context.task.id, child.id);
2671        assert_eq!(context.task.parent_id, Some(parent.id));
2672
2673        // Should have 1 ancestor (the parent)
2674        assert_eq!(context.ancestors.len(), 1);
2675        assert_eq!(context.ancestors[0].id, parent.id);
2676        assert_eq!(context.ancestors[0].name, "Parent task");
2677
2678        // No siblings
2679        assert_eq!(context.siblings.len(), 0);
2680
2681        // No children
2682        assert_eq!(context.children.len(), 0);
2683    }
2684
2685    #[tokio::test]
2686    async fn test_get_task_context_with_children() {
2687        let ctx = TestContext::new().await;
2688        let task_mgr = TaskManager::new(ctx.pool());
2689
2690        // Create parent with multiple children
2691        let parent = task_mgr
2692            .add_task("Parent task", None, None, None)
2693            .await
2694            .unwrap();
2695        let child1 = task_mgr
2696            .add_task("Child 1", None, Some(parent.id), None)
2697            .await
2698            .unwrap();
2699        let child2 = task_mgr
2700            .add_task("Child 2", None, Some(parent.id), None)
2701            .await
2702            .unwrap();
2703        let child3 = task_mgr
2704            .add_task("Child 3", None, Some(parent.id), None)
2705            .await
2706            .unwrap();
2707
2708        let context = task_mgr.get_task_context(parent.id).await.unwrap();
2709
2710        // Verify task itself
2711        assert_eq!(context.task.id, parent.id);
2712
2713        // No ancestors (root task)
2714        assert_eq!(context.ancestors.len(), 0);
2715
2716        // No siblings
2717        assert_eq!(context.siblings.len(), 0);
2718
2719        // Should have 3 children
2720        assert_eq!(context.children.len(), 3);
2721        let child_ids: Vec<i64> = context.children.iter().map(|t| t.id).collect();
2722        assert!(child_ids.contains(&child1.id));
2723        assert!(child_ids.contains(&child2.id));
2724        assert!(child_ids.contains(&child3.id));
2725    }
2726
2727    #[tokio::test]
2728    async fn test_get_task_context_multi_level_hierarchy() {
2729        let ctx = TestContext::new().await;
2730        let task_mgr = TaskManager::new(ctx.pool());
2731
2732        // Create 3-level hierarchy: grandparent -> parent -> child
2733        let grandparent = task_mgr
2734            .add_task("Grandparent", None, None, None)
2735            .await
2736            .unwrap();
2737        let parent = task_mgr
2738            .add_task("Parent", None, Some(grandparent.id), None)
2739            .await
2740            .unwrap();
2741        let child = task_mgr
2742            .add_task("Child", None, Some(parent.id), None)
2743            .await
2744            .unwrap();
2745
2746        let context = task_mgr.get_task_context(child.id).await.unwrap();
2747
2748        // Verify task itself
2749        assert_eq!(context.task.id, child.id);
2750
2751        // Should have 2 ancestors (parent and grandparent, ordered from immediate to root)
2752        assert_eq!(context.ancestors.len(), 2);
2753        assert_eq!(context.ancestors[0].id, parent.id);
2754        assert_eq!(context.ancestors[0].name, "Parent");
2755        assert_eq!(context.ancestors[1].id, grandparent.id);
2756        assert_eq!(context.ancestors[1].name, "Grandparent");
2757
2758        // No siblings
2759        assert_eq!(context.siblings.len(), 0);
2760
2761        // No children
2762        assert_eq!(context.children.len(), 0);
2763    }
2764
2765    #[tokio::test]
2766    async fn test_get_task_context_complex_family_tree() {
2767        let ctx = TestContext::new().await;
2768        let task_mgr = TaskManager::new(ctx.pool());
2769
2770        // Create complex structure:
2771        // Root
2772        //  ├─ Child1
2773        //  │   ├─ Grandchild1
2774        //  │   └─ Grandchild2 (target)
2775        //  └─ Child2
2776
2777        let root = task_mgr.add_task("Root", None, None, None).await.unwrap();
2778        let child1 = task_mgr
2779            .add_task("Child1", None, Some(root.id), None)
2780            .await
2781            .unwrap();
2782        let child2 = task_mgr
2783            .add_task("Child2", None, Some(root.id), None)
2784            .await
2785            .unwrap();
2786        let grandchild1 = task_mgr
2787            .add_task("Grandchild1", None, Some(child1.id), None)
2788            .await
2789            .unwrap();
2790        let grandchild2 = task_mgr
2791            .add_task("Grandchild2", None, Some(child1.id), None)
2792            .await
2793            .unwrap();
2794
2795        // Get context for grandchild2
2796        let context = task_mgr.get_task_context(grandchild2.id).await.unwrap();
2797
2798        // Verify task itself
2799        assert_eq!(context.task.id, grandchild2.id);
2800
2801        // Should have 2 ancestors: child1 and root
2802        assert_eq!(context.ancestors.len(), 2);
2803        assert_eq!(context.ancestors[0].id, child1.id);
2804        assert_eq!(context.ancestors[1].id, root.id);
2805
2806        // Should have 1 sibling: grandchild1
2807        assert_eq!(context.siblings.len(), 1);
2808        assert_eq!(context.siblings[0].id, grandchild1.id);
2809
2810        // No children
2811        assert_eq!(context.children.len(), 0);
2812
2813        // Now get context for child1 to verify it sees both grandchildren
2814        let context_child1 = task_mgr.get_task_context(child1.id).await.unwrap();
2815        assert_eq!(context_child1.ancestors.len(), 1);
2816        assert_eq!(context_child1.ancestors[0].id, root.id);
2817        assert_eq!(context_child1.siblings.len(), 1);
2818        assert_eq!(context_child1.siblings[0].id, child2.id);
2819        assert_eq!(context_child1.children.len(), 2);
2820    }
2821
2822    #[tokio::test]
2823    async fn test_get_task_context_respects_priority_ordering() {
2824        let ctx = TestContext::new().await;
2825        let task_mgr = TaskManager::new(ctx.pool());
2826
2827        // Create parent with children having different priorities
2828        let parent = task_mgr.add_task("Parent", None, None, None).await.unwrap();
2829
2830        // Add children with priorities (lower number = higher priority)
2831        let child_low = task_mgr
2832            .add_task("Low priority", None, Some(parent.id), None)
2833            .await
2834            .unwrap();
2835        let _ = task_mgr
2836            .update_task(child_low.id, None, None, None, None, None, Some(10))
2837            .await
2838            .unwrap();
2839
2840        let child_high = task_mgr
2841            .add_task("High priority", None, Some(parent.id), None)
2842            .await
2843            .unwrap();
2844        let _ = task_mgr
2845            .update_task(child_high.id, None, None, None, None, None, Some(1))
2846            .await
2847            .unwrap();
2848
2849        let child_medium = task_mgr
2850            .add_task("Medium priority", None, Some(parent.id), None)
2851            .await
2852            .unwrap();
2853        let _ = task_mgr
2854            .update_task(child_medium.id, None, None, None, None, None, Some(5))
2855            .await
2856            .unwrap();
2857
2858        let context = task_mgr.get_task_context(parent.id).await.unwrap();
2859
2860        // Children should be ordered by priority (1, 5, 10)
2861        assert_eq!(context.children.len(), 3);
2862        assert_eq!(context.children[0].priority, Some(1));
2863        assert_eq!(context.children[1].priority, Some(5));
2864        assert_eq!(context.children[2].priority, Some(10));
2865    }
2866
2867    #[tokio::test]
2868    async fn test_get_task_context_nonexistent_task() {
2869        let ctx = TestContext::new().await;
2870        let task_mgr = TaskManager::new(ctx.pool());
2871
2872        let result = task_mgr.get_task_context(99999).await;
2873        assert!(result.is_err());
2874        assert!(matches!(result, Err(IntentError::TaskNotFound(99999))));
2875    }
2876
2877    #[tokio::test]
2878    async fn test_get_task_context_handles_null_priority() {
2879        let ctx = TestContext::new().await;
2880        let task_mgr = TaskManager::new(ctx.pool());
2881
2882        // Create siblings with mixed null and set priorities
2883        let task1 = task_mgr.add_task("Task 1", None, None, None).await.unwrap();
2884        let _ = task_mgr
2885            .update_task(task1.id, None, None, None, None, None, Some(1))
2886            .await
2887            .unwrap();
2888
2889        let task2 = task_mgr.add_task("Task 2", None, None, None).await.unwrap();
2890        // task2 has NULL priority
2891
2892        let task3 = task_mgr.add_task("Task 3", None, None, None).await.unwrap();
2893        let _ = task_mgr
2894            .update_task(task3.id, None, None, None, None, None, Some(5))
2895            .await
2896            .unwrap();
2897
2898        let context = task_mgr.get_task_context(task2.id).await.unwrap();
2899
2900        // Should have 2 siblings, ordered by priority (non-null first, then null)
2901        assert_eq!(context.siblings.len(), 2);
2902        // Task with priority 1 should come first
2903        assert_eq!(context.siblings[0].id, task1.id);
2904        assert_eq!(context.siblings[0].priority, Some(1));
2905        // Task with priority 5 should come second
2906        assert_eq!(context.siblings[1].id, task3.id);
2907        assert_eq!(context.siblings[1].priority, Some(5));
2908    }
2909
2910    #[tokio::test]
2911    async fn test_pick_next_tasks_priority_order() {
2912        let ctx = TestContext::new().await;
2913        let task_mgr = TaskManager::new(ctx.pool());
2914
2915        // Create 4 tasks with different priorities
2916        let critical = task_mgr
2917            .add_task("Critical Task", None, None, None)
2918            .await
2919            .unwrap();
2920        task_mgr
2921            .update_task(critical.id, None, None, None, None, None, Some(1))
2922            .await
2923            .unwrap();
2924
2925        let low = task_mgr
2926            .add_task("Low Task", None, None, None)
2927            .await
2928            .unwrap();
2929        task_mgr
2930            .update_task(low.id, None, None, None, None, None, Some(4))
2931            .await
2932            .unwrap();
2933
2934        let high = task_mgr
2935            .add_task("High Task", None, None, None)
2936            .await
2937            .unwrap();
2938        task_mgr
2939            .update_task(high.id, None, None, None, None, None, Some(2))
2940            .await
2941            .unwrap();
2942
2943        let medium = task_mgr
2944            .add_task("Medium Task", None, None, None)
2945            .await
2946            .unwrap();
2947        task_mgr
2948            .update_task(medium.id, None, None, None, None, None, Some(3))
2949            .await
2950            .unwrap();
2951
2952        // Pick next tasks should return them in priority order: critical > high > medium > low
2953        let tasks = task_mgr.pick_next_tasks(10, 10).await.unwrap();
2954
2955        assert_eq!(tasks.len(), 4);
2956        assert_eq!(tasks[0].id, critical.id); // Priority 1
2957        assert_eq!(tasks[1].id, high.id); // Priority 2
2958        assert_eq!(tasks[2].id, medium.id); // Priority 3
2959        assert_eq!(tasks[3].id, low.id); // Priority 4
2960    }
2961
2962    #[tokio::test]
2963    async fn test_pick_next_prefers_doing_over_todo() {
2964        let ctx = TestContext::new().await;
2965        let task_mgr = TaskManager::new(ctx.pool());
2966        let workspace_mgr = WorkspaceManager::new(ctx.pool());
2967
2968        // Create a parent task and set it as current
2969        let parent = task_mgr.add_task("Parent", None, None, None).await.unwrap();
2970        let parent_started = task_mgr.start_task(parent.id, false).await.unwrap();
2971        workspace_mgr
2972            .set_current_task(parent_started.task.id, None)
2973            .await
2974            .unwrap();
2975
2976        // Create two subtasks with same priority: one doing, one todo
2977        let doing_subtask = task_mgr
2978            .add_task("Doing Subtask", None, Some(parent.id), None)
2979            .await
2980            .unwrap();
2981        task_mgr.start_task(doing_subtask.id, false).await.unwrap();
2982        // Switch back to parent so doing_subtask is "pending" (doing but not current)
2983        workspace_mgr
2984            .set_current_task(parent.id, None)
2985            .await
2986            .unwrap();
2987
2988        let _todo_subtask = task_mgr
2989            .add_task("Todo Subtask", None, Some(parent.id), None)
2990            .await
2991            .unwrap();
2992
2993        // Both have same priority (default), but doing should be picked first
2994        let result = task_mgr.pick_next().await.unwrap();
2995
2996        if let Some(task) = result.task {
2997            assert_eq!(
2998                task.id, doing_subtask.id,
2999                "Should recommend doing subtask over todo subtask"
3000            );
3001            assert_eq!(task.status, "doing");
3002        } else {
3003            panic!("Expected a task recommendation");
3004        }
3005    }
3006
3007    #[tokio::test]
3008    async fn test_multiple_doing_tasks_allowed() {
3009        let ctx = TestContext::new().await;
3010        let task_mgr = TaskManager::new(ctx.pool());
3011        let workspace_mgr = WorkspaceManager::new(ctx.pool());
3012
3013        // Create and start task A
3014        let task_a = task_mgr.add_task("Task A", None, None, None).await.unwrap();
3015        let task_a_started = task_mgr.start_task(task_a.id, false).await.unwrap();
3016        assert_eq!(task_a_started.task.status, "doing");
3017
3018        // Verify task A is current
3019        let current = workspace_mgr.get_current_task(None).await.unwrap();
3020        assert_eq!(current.current_task_id, Some(task_a.id));
3021
3022        // Create and start task B
3023        let task_b = task_mgr.add_task("Task B", None, None, None).await.unwrap();
3024        let task_b_started = task_mgr.start_task(task_b.id, false).await.unwrap();
3025        assert_eq!(task_b_started.task.status, "doing");
3026
3027        // Verify task B is now current
3028        let current = workspace_mgr.get_current_task(None).await.unwrap();
3029        assert_eq!(current.current_task_id, Some(task_b.id));
3030
3031        // Verify task A is still doing (not reverted to todo)
3032        let task_a_after = task_mgr.get_task(task_a.id).await.unwrap();
3033        assert_eq!(
3034            task_a_after.status, "doing",
3035            "Task A should remain doing even though it is not current"
3036        );
3037
3038        // Verify both tasks are in doing status
3039        let doing_tasks: Vec<Task> = sqlx::query_as(
3040            r#"SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner
3041             FROM tasks WHERE status = 'doing' ORDER BY id"#
3042        )
3043        .fetch_all(ctx.pool())
3044        .await
3045        .unwrap();
3046
3047        assert_eq!(doing_tasks.len(), 2, "Should have 2 doing tasks");
3048        assert_eq!(doing_tasks[0].id, task_a.id);
3049        assert_eq!(doing_tasks[1].id, task_b.id);
3050    }
3051    #[tokio::test]
3052    async fn test_find_tasks_pagination() {
3053        let ctx = TestContext::new().await;
3054        let task_mgr = TaskManager::new(ctx.pool());
3055
3056        // Create 15 tasks
3057        for i in 0..15 {
3058            task_mgr
3059                .add_task(&format!("Task {}", i), None, None, None)
3060                .await
3061                .unwrap();
3062        }
3063
3064        // Page 1: Limit 10, Offset 0
3065        let page1 = task_mgr
3066            .find_tasks(None, None, None, Some(10), Some(0))
3067            .await
3068            .unwrap();
3069        assert_eq!(page1.tasks.len(), 10);
3070        assert_eq!(page1.total_count, 15);
3071        assert!(page1.has_more);
3072        assert_eq!(page1.offset, 0);
3073
3074        // Page 2: Limit 10, Offset 10
3075        let page2 = task_mgr
3076            .find_tasks(None, None, None, Some(10), Some(10))
3077            .await
3078            .unwrap();
3079        assert_eq!(page2.tasks.len(), 5);
3080        assert_eq!(page2.total_count, 15);
3081        assert!(!page2.has_more);
3082        assert_eq!(page2.offset, 10);
3083    }
3084}
3085
3086// Re-export TaskContext for cli_handlers