verifyos_cli/
agent_assets.rs1use 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}