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