Skip to main content

verifyos_cli/
agent_assets.rs

1use clap::ValueEnum;
2use serde::{Deserialize, Serialize};
3use std::collections::HashSet;
4use std::path::{Path, PathBuf};
5
6pub const AGENTS_FILE_NAME: &str = "AGENTS.md";
7pub const AGENT_BUNDLE_DIR_NAME: &str = ".verifyos-agent";
8pub const AGENT_PACK_JSON_NAME: &str = "agent-pack.json";
9pub const AGENT_PACK_MARKDOWN_NAME: &str = "agent-pack.md";
10pub const NEXT_STEPS_SCRIPT_NAME: &str = "next-steps.sh";
11pub const FIX_PROMPT_NAME: &str = "fix-prompt.md";
12pub const REPAIR_PLAN_NAME: &str = "repair-plan.md";
13pub const PR_BRIEF_NAME: &str = "pr-brief.md";
14pub const PR_COMMENT_NAME: &str = "pr-comment.md";
15pub const HANDOFF_MANIFEST_NAME: &str = "handoff.json";
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, ValueEnum)]
18pub enum RepairTarget {
19    Agents,
20    AgentBundle,
21    FixPrompt,
22    PrBrief,
23    PrComment,
24}
25
26impl RepairTarget {
27    pub fn key(self) -> &'static str {
28        match self {
29            RepairTarget::Agents => "agents",
30            RepairTarget::AgentBundle => "agent-bundle",
31            RepairTarget::FixPrompt => "fix-prompt",
32            RepairTarget::PrBrief => "pr-brief",
33            RepairTarget::PrComment => "pr-comment",
34        }
35    }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39pub struct RepairPlanItem {
40    pub target: String,
41    pub path: String,
42    pub reason: String,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct AgentAssetLayout {
47    pub output_dir: PathBuf,
48    pub agents_path: PathBuf,
49    pub agent_bundle_dir: PathBuf,
50    pub agent_pack_json_path: PathBuf,
51    pub agent_pack_markdown_path: PathBuf,
52    pub next_steps_script_path: PathBuf,
53    pub fix_prompt_path: PathBuf,
54    pub repair_plan_path: PathBuf,
55    pub pr_brief_path: PathBuf,
56    pub pr_comment_path: PathBuf,
57}
58
59impl AgentAssetLayout {
60    pub fn new(output_dir: impl Into<PathBuf>, agents_path: impl Into<PathBuf>) -> Self {
61        let output_dir = output_dir.into();
62        let agents_path = agents_path.into();
63        let agent_bundle_dir = output_dir.join(AGENT_BUNDLE_DIR_NAME);
64
65        Self {
66            output_dir: output_dir.clone(),
67            agents_path,
68            agent_pack_json_path: agent_bundle_dir.join(AGENT_PACK_JSON_NAME),
69            agent_pack_markdown_path: agent_bundle_dir.join(AGENT_PACK_MARKDOWN_NAME),
70            next_steps_script_path: agent_bundle_dir.join(NEXT_STEPS_SCRIPT_NAME),
71            fix_prompt_path: output_dir.join(FIX_PROMPT_NAME),
72            repair_plan_path: output_dir.join(REPAIR_PLAN_NAME),
73            pr_brief_path: output_dir.join(PR_BRIEF_NAME),
74            pr_comment_path: output_dir.join(PR_COMMENT_NAME),
75            agent_bundle_dir,
76        }
77    }
78
79    pub fn from_output_dir(output_dir: impl Into<PathBuf>) -> Self {
80        let output_dir = output_dir.into();
81        let agents_path = if output_dir.file_name().and_then(|n| n.to_str()) == Some(".verifyos") {
82            output_dir
83                .parent()
84                .map(|p| p.join(AGENTS_FILE_NAME))
85                .unwrap_or_else(|| output_dir.join(AGENTS_FILE_NAME))
86        } else {
87            output_dir.join(AGENTS_FILE_NAME)
88        };
89        Self::new(output_dir, agents_path)
90    }
91
92    pub fn with_agents_path(&self, agents_path: impl Into<PathBuf>) -> Self {
93        Self::new(self.output_dir.clone(), agents_path.into())
94    }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct RepairPolicy {
99    repair_all: bool,
100    repair_targets: HashSet<RepairTarget>,
101    pub open_pr_brief: bool,
102    pub open_pr_comment: bool,
103}
104
105impl RepairPolicy {
106    pub fn new(
107        repair_targets: HashSet<RepairTarget>,
108        open_pr_brief: bool,
109        open_pr_comment: bool,
110    ) -> Self {
111        let repair_all = repair_targets.is_empty();
112        Self {
113            repair_all,
114            repair_targets,
115            open_pr_brief,
116            open_pr_comment,
117        }
118    }
119
120    pub fn repair_targets(&self) -> &HashSet<RepairTarget> {
121        &self.repair_targets
122    }
123
124    pub fn repairs_all(&self) -> bool {
125        self.repair_all
126    }
127
128    pub fn should_repair_agents(&self) -> bool {
129        self.repair_all || self.repair_targets.contains(&RepairTarget::Agents)
130    }
131
132    pub fn should_repair_bundle(&self) -> bool {
133        self.repair_all || self.repair_targets.contains(&RepairTarget::AgentBundle)
134    }
135
136    pub fn should_repair_fix_prompt(&self) -> bool {
137        self.repair_all || self.repair_targets.contains(&RepairTarget::FixPrompt)
138    }
139
140    pub fn should_include_pr_brief(&self) -> bool {
141        self.open_pr_brief || self.repair_targets.contains(&RepairTarget::PrBrief)
142    }
143
144    pub fn should_include_pr_comment(&self) -> bool {
145        self.open_pr_comment || self.repair_targets.contains(&RepairTarget::PrComment)
146    }
147
148    pub fn should_repair_pr_brief(&self) -> bool {
149        self.repair_all || self.repair_targets.contains(&RepairTarget::PrBrief)
150    }
151
152    pub fn should_repair_pr_comment(&self) -> bool {
153        self.repair_all || self.repair_targets.contains(&RepairTarget::PrComment)
154    }
155}
156
157pub fn build_repair_plan(layout: &AgentAssetLayout, policy: &RepairPolicy) -> Vec<RepairPlanItem> {
158    let mut plan = Vec::new();
159
160    if policy.should_repair_agents() {
161        plan.push(RepairPlanItem {
162            target: RepairTarget::Agents.key().to_string(),
163            path: layout.agents_path.display().to_string(),
164            reason: "refresh managed AGENTS.md block".to_string(),
165        });
166    }
167
168    if policy.should_repair_bundle() {
169        plan.push(RepairPlanItem {
170            target: RepairTarget::AgentBundle.key().to_string(),
171            path: layout.agent_bundle_dir.display().to_string(),
172            reason: "rebuild agent-pack files and next-steps.sh".to_string(),
173        });
174    }
175
176    if policy.should_repair_fix_prompt() {
177        plan.push(RepairPlanItem {
178            target: RepairTarget::FixPrompt.key().to_string(),
179            path: layout.fix_prompt_path.display().to_string(),
180            reason: "refresh AI fix prompt".to_string(),
181        });
182    }
183
184    if policy.should_include_pr_brief() {
185        plan.push(RepairPlanItem {
186            target: RepairTarget::PrBrief.key().to_string(),
187            path: layout.pr_brief_path.display().to_string(),
188            reason: "refresh PR handoff brief".to_string(),
189        });
190    }
191
192    if policy.should_include_pr_comment() {
193        plan.push(RepairPlanItem {
194            target: RepairTarget::PrComment.key().to_string(),
195            path: layout.pr_comment_path.display().to_string(),
196            reason: "refresh sticky PR comment draft".to_string(),
197        });
198    }
199
200    plan
201}
202
203pub fn relative_to_agents(agents_path: &Path, asset_path: &Path) -> String {
204    agents_path
205        .parent()
206        .and_then(|parent| asset_path.strip_prefix(parent).ok())
207        .unwrap_or(asset_path)
208        .display()
209        .to_string()
210}