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