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 Self::new(output_dir.clone(), output_dir.join(AGENTS_FILE_NAME))
82 }
83
84 pub fn with_agents_path(&self, agents_path: impl Into<PathBuf>) -> Self {
85 Self::new(self.output_dir.clone(), agents_path.into())
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct RepairPolicy {
91 repair_all: bool,
92 repair_targets: HashSet<RepairTarget>,
93 pub open_pr_brief: bool,
94 pub open_pr_comment: bool,
95}
96
97impl RepairPolicy {
98 pub fn new(
99 repair_targets: HashSet<RepairTarget>,
100 open_pr_brief: bool,
101 open_pr_comment: bool,
102 ) -> Self {
103 let repair_all = repair_targets.is_empty();
104 Self {
105 repair_all,
106 repair_targets,
107 open_pr_brief,
108 open_pr_comment,
109 }
110 }
111
112 pub fn repair_targets(&self) -> &HashSet<RepairTarget> {
113 &self.repair_targets
114 }
115
116 pub fn repairs_all(&self) -> bool {
117 self.repair_all
118 }
119
120 pub fn should_repair_agents(&self) -> bool {
121 self.repair_all || self.repair_targets.contains(&RepairTarget::Agents)
122 }
123
124 pub fn should_repair_bundle(&self) -> bool {
125 self.repair_all || self.repair_targets.contains(&RepairTarget::AgentBundle)
126 }
127
128 pub fn should_repair_fix_prompt(&self) -> bool {
129 self.repair_all || self.repair_targets.contains(&RepairTarget::FixPrompt)
130 }
131
132 pub fn should_include_pr_brief(&self) -> bool {
133 self.open_pr_brief || self.repair_targets.contains(&RepairTarget::PrBrief)
134 }
135
136 pub fn should_include_pr_comment(&self) -> bool {
137 self.open_pr_comment || self.repair_targets.contains(&RepairTarget::PrComment)
138 }
139
140 pub fn should_repair_pr_brief(&self) -> bool {
141 self.repair_all || self.repair_targets.contains(&RepairTarget::PrBrief)
142 }
143
144 pub fn should_repair_pr_comment(&self) -> bool {
145 self.repair_all || self.repair_targets.contains(&RepairTarget::PrComment)
146 }
147}
148
149pub fn build_repair_plan(layout: &AgentAssetLayout, policy: &RepairPolicy) -> Vec<RepairPlanItem> {
150 let mut plan = Vec::new();
151
152 if policy.should_repair_agents() {
153 plan.push(RepairPlanItem {
154 target: RepairTarget::Agents.key().to_string(),
155 path: layout.agents_path.display().to_string(),
156 reason: "refresh managed AGENTS.md block".to_string(),
157 });
158 }
159
160 if policy.should_repair_bundle() {
161 plan.push(RepairPlanItem {
162 target: RepairTarget::AgentBundle.key().to_string(),
163 path: layout.agent_bundle_dir.display().to_string(),
164 reason: "rebuild agent-pack files and next-steps.sh".to_string(),
165 });
166 }
167
168 if policy.should_repair_fix_prompt() {
169 plan.push(RepairPlanItem {
170 target: RepairTarget::FixPrompt.key().to_string(),
171 path: layout.fix_prompt_path.display().to_string(),
172 reason: "refresh AI fix prompt".to_string(),
173 });
174 }
175
176 if policy.should_include_pr_brief() {
177 plan.push(RepairPlanItem {
178 target: RepairTarget::PrBrief.key().to_string(),
179 path: layout.pr_brief_path.display().to_string(),
180 reason: "refresh PR handoff brief".to_string(),
181 });
182 }
183
184 if policy.should_include_pr_comment() {
185 plan.push(RepairPlanItem {
186 target: RepairTarget::PrComment.key().to_string(),
187 path: layout.pr_comment_path.display().to_string(),
188 reason: "refresh sticky PR comment draft".to_string(),
189 });
190 }
191
192 plan
193}
194
195pub fn relative_to_agents(agents_path: &Path, asset_path: &Path) -> String {
196 agents_path
197 .parent()
198 .and_then(|parent| asset_path.strip_prefix(parent).ok())
199 .unwrap_or(asset_path)
200 .display()
201 .to_string()
202}