Skip to main content

track/services/
agent_context.rs

1use crate::models::{
2    build_git_context, build_jj_context, build_next_action, compute_workflow_phase,
3    oldest_pending_todo, workspace_lifecycle, AgentGuardrails, GitAgentContext, JjAgentContext,
4    Task, TaskRepo, Todo, TodoAction, TodoAgentView, TodoStatus, VcsMode, WorkflowContext,
5    WorkspaceAgentView, Worktree,
6};
7use crate::services::WorktreeService;
8use serde::Serialize;
9
10/// Agent-oriented fields shared by `track status --json` and `/api/status`.
11#[derive(Debug, Clone, Serialize)]
12pub struct AgentStatusExtensions {
13    pub vcs_mode: VcsMode,
14    pub workflow: WorkflowContext,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub jj: Option<JjAgentContext>,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub git: Option<GitAgentContext>,
19    pub todos_agent: Vec<TodoAgentView>,
20    pub guardrails: AgentGuardrails,
21}
22
23/// Builds agent-oriented JSON extensions for status endpoints.
24pub fn build_agent_extensions(
25    vcs_mode: VcsMode,
26    task: &Task,
27    todos: &[Todo],
28    worktrees: &[Worktree],
29    repos: &[TaskRepo],
30    worktree_service: &WorktreeService<'_>,
31) -> AgentStatusExtensions {
32    let phase = compute_workflow_phase(vcs_mode, task, todos, worktrees, repos);
33    let next_todo_id = oldest_pending_todo(todos).map(|todo| todo.id);
34    let legacy_merge_required = vcs_mode == VcsMode::Jj
35        && todos
36            .iter()
37            .any(|todo| todo.status == TodoStatus::Pending && todo.worktree_requested);
38
39    let workflow = WorkflowContext {
40        phase,
41        next_action: build_next_action(vcs_mode, phase, task, todos, worktrees, repos),
42    };
43
44    let git_ctx = build_git_context(task, repos);
45    let git_worktree_path = git_ctx.workspace_path.clone();
46
47    let todos_agent: Vec<TodoAgentView> = todos
48        .iter()
49        .map(|todo| {
50            let lifecycle = workspace_lifecycle(todo, worktrees);
51            let bookmark = if todo.worktree_requested {
52                worktree_service
53                    .get_todo_branch_name(task.id, task.ticket_id.as_deref(), todo.task_index)
54                    .ok()
55            } else {
56                None
57            };
58            let path = worktrees
59                .iter()
60                .find(|wt| wt.todo_id == Some(todo.id))
61                .map(|wt| wt.path.clone())
62                .or_else(|| {
63                    if vcs_mode == VcsMode::Git && todo.status == TodoStatus::Pending {
64                        git_worktree_path.clone()
65                    } else {
66                        None
67                    }
68                });
69
70            TodoAgentView {
71                todo_id: todo.task_index,
72                content: todo.content.clone(),
73                status: todo.status,
74                is_next: next_todo_id == Some(todo.id),
75                allowed_actions: TodoAction::allowed_for(todo)
76                    .into_iter()
77                    .map(|action| action.as_str().to_string())
78                    .chain(if todo.status == TodoStatus::Pending {
79                        vec!["delete".to_string()]
80                    } else {
81                        Vec::new()
82                    })
83                    .collect(),
84                workspace: WorkspaceAgentView {
85                    lifecycle,
86                    path,
87                    bookmark,
88                },
89            }
90        })
91        .collect();
92
93    let guardrails = AgentGuardrails::for_mode(vcs_mode, legacy_merge_required);
94
95    let (jj, git) = match vcs_mode {
96        VcsMode::Jj => (Some(build_jj_context(task, repos)), None),
97        VcsMode::Git => (None, Some(git_ctx)),
98    };
99
100    AgentStatusExtensions {
101        vcs_mode,
102        workflow,
103        jj,
104        git,
105        todos_agent,
106        guardrails,
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::db::Database;
114    use crate::models::WorkflowPhase;
115    use crate::services::{TaskService, TodoService};
116
117    #[test]
118    fn agent_extensions_include_workflow_and_jj() {
119        let db = Database::new_in_memory().unwrap();
120        let task = TaskService::new(&db)
121            .create_task("Task", None, None, None)
122            .unwrap();
123        let todo_service = TodoService::new(&db);
124        todo_service.add_todo(task.id, "Work", false).unwrap();
125
126        let todos = todo_service.list_todos(task.id).unwrap();
127        let worktree_service = WorktreeService::new(&db);
128        let extensions =
129            build_agent_extensions(VcsMode::Jj, &task, &todos, &[], &[], &worktree_service);
130
131        assert_eq!(extensions.workflow.phase, WorkflowPhase::Setup);
132        assert_eq!(extensions.vcs_mode, VcsMode::Jj);
133        assert_eq!(extensions.jj.as_ref().unwrap().skill, "jj");
134        assert!(extensions.git.is_none());
135        assert!(extensions.guardrails.must_use_jj_skill);
136        assert!(extensions.guardrails.reopen_forbidden);
137    }
138
139    #[test]
140    fn agent_extensions_use_git_context_in_git_mode() {
141        let db = Database::new_in_memory().unwrap();
142        db.set_vcs_mode(VcsMode::Git).unwrap();
143        let task = TaskService::new(&db)
144            .create_task("Task", None, None, None)
145            .unwrap();
146        let todo_service = TodoService::new(&db);
147        todo_service.add_todo(task.id, "Work", false).unwrap();
148
149        let todos = todo_service.list_todos(task.id).unwrap();
150        let worktree_service = WorktreeService::new(&db);
151        let extensions = build_agent_extensions(
152            db.get_vcs_mode().unwrap(),
153            &task,
154            &todos,
155            &[],
156            &[],
157            &worktree_service,
158        );
159
160        assert_eq!(extensions.vcs_mode, VcsMode::Git);
161        assert!(extensions.jj.is_none());
162        assert_eq!(extensions.git.as_ref().unwrap().branch, "track/task-1");
163        assert!(!extensions.guardrails.must_use_jj_skill);
164    }
165}