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";
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}