Skip to main content

oxios_kernel/token_maxing/
planner.rs

1//! WorkPlanner — three-source task synthesis (RFC-031 §7).
2//!
3//! Selects the next task for the TokenMaxer from three prioritized sources,
4//! each filtered to non-destructive, bounded work (maxing runs unattended — no
5//! `rm`, no deploys, no outbound network beyond read):
6//!
7//! - **Source A — autonomous skills**: skills with frontmatter `autonomous:
8//!   true`. This is the "자주 실행되던 스킬" axis.
9//! - **Source B — registered projects/mounts**: a bounded, read-mostly review
10//!   task synthesized from each project's paths.
11//! - **Source C — recurring patterns**: stubbed (lowest priority; avoids
12//!   inventing work).
13
14use std::collections::HashSet;
15use std::path::PathBuf;
16use std::sync::Arc;
17
18use super::session::TaskSource;
19
20/// A unit of work the TokenMaxer can dispatch on one provider.
21#[derive(Debug, Clone)]
22pub struct PlannedTask {
23    /// Which source produced this task.
24    pub source: TaskSource,
25    /// Human label (skill/project name) — for the status panel + report.
26    pub source_name: String,
27    /// The synthesized goal prompt handed to the agent.
28    pub goal: String,
29    /// Workspace paths to sandbox the agent to (project tasks only).
30    pub mount_paths: Vec<PathBuf>,
31}
32
33/// Selects the next task for a maxing run.
34pub struct WorkPlanner {
35    skills: Arc<crate::skill::SkillManager>,
36    projects: Option<Arc<crate::project::ProjectManager>>,
37    /// Mount manager — resolves a project's `mount_ids` into filesystem
38    /// paths (RFC-025: paths live on Mounts, not Projects).
39    mounts: Option<Arc<crate::mount::MountManager>>,
40}
41
42impl WorkPlanner {
43    pub fn new(
44        skills: Arc<crate::skill::SkillManager>,
45        projects: Option<Arc<crate::project::ProjectManager>>,
46        mounts: Option<Arc<crate::mount::MountManager>>,
47    ) -> Self {
48        Self {
49            skills,
50            projects,
51            mounts,
52        }
53    }
54
55    /// Pick the next task not already done this session (`done_goals`).
56    ///
57    /// Returns `None` when no eligible work remains — the maxer then terminates
58    /// the window early ("stopped: nothing to do") rather than fabricating work.
59    pub async fn next_task(&self, done_goals: &HashSet<String>) -> Option<PlannedTask> {
60        // Source A — autonomous-eligible skills.
61        for entry in self.skills.list_skills().await {
62            let autonomous = entry
63                .metadata
64                .as_ref()
65                .map(|m| m.autonomous)
66                .unwrap_or(false);
67            if !autonomous {
68                continue;
69            }
70            let goal = format!(
71                "[Skill: {}] {}. Perform this skill's routine work autonomously and \
72                 summarize the outcome. Do not make destructive changes or run deploys.",
73                entry.skill.name, entry.skill.description
74            );
75            if done_goals.contains(&goal) {
76                continue;
77            }
78            return Some(PlannedTask {
79                source: TaskSource::Skill,
80                source_name: entry.skill.name.clone(),
81                goal,
82                mount_paths: Vec::new(),
83            });
84        }
85
86        // Source B — registered projects (read-mostly review).
87        if let Some(pm) = &self.projects {
88            for proj in pm.list_projects() {
89                // RFC-025: a project's filesystem scope is its referenced
90                // Mounts. Skip projects with no Mounts (non-code, or
91                // pre-migration) — there is nothing on disk to review.
92                if proj.mount_ids.is_empty() {
93                    continue;
94                }
95                let mount_paths = self.resolve_mount_paths(&proj.mount_ids);
96                if mount_paths.is_empty() {
97                    continue;
98                }
99                let goal = format!(
100                    "[Project: {}] Review the recent state of this project: summarize open \
101                     work, recent changes, and any TODOs/FIXMEs. Read-only — do not modify \
102                     files or run deploys.",
103                    proj.name
104                );
105                if done_goals.contains(&goal) {
106                    continue;
107                }
108                return Some(PlannedTask {
109                    source: TaskSource::Project,
110                    source_name: proj.name.clone(),
111                    goal,
112                    mount_paths,
113                });
114            }
115        }
116
117        // Source C — recurring patterns: intentionally stubbed (RFC-031 §7).
118        None
119    }
120
121    /// Resolve a project's `mount_ids` into deduplicated filesystem paths via
122    /// the Mount manager (RFC-025: paths live on Mounts, not Projects).
123    fn resolve_mount_paths(&self, mount_ids: &[crate::mount::MountId]) -> Vec<PathBuf> {
124        let Some(mm) = &self.mounts else {
125            return Vec::new();
126        };
127        let mut paths = Vec::new();
128        for mount in mm.get_mounts_ordered(mount_ids) {
129            for p in &mount.paths {
130                if !paths.contains(p) {
131                    paths.push(p.clone());
132                }
133            }
134        }
135        paths
136    }
137}