1use std::cmp::Ordering;
10use std::path::PathBuf;
11
12use anyhow::Result;
13use console::style;
14use serde::{Deserialize, Serialize};
15
16use crate::cache::Cache;
17
18#[derive(Debug, Clone)]
20pub struct PrimerOptions {
21 pub budget: u32,
23 pub capabilities: Vec<String>,
25 pub format: PrimerFormat,
27 pub cache: Option<PathBuf>,
29 pub json: bool,
31}
32
33#[derive(Debug, Clone, Copy, Default)]
35pub enum PrimerFormat {
36 #[default]
37 Text,
38 Json,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum Tier {
44 Minimal,
45 Standard,
46 Full,
47}
48
49impl Tier {
50 pub fn from_budget(remaining: u32) -> Self {
52 if remaining < 80 {
53 Tier::Minimal
54 } else if remaining < 300 {
55 Tier::Standard
56 } else {
57 Tier::Full
58 }
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct Bootstrap {
65 pub awareness: String,
66 pub workflow: String,
67 pub expansion: String,
68 pub tokens: u32,
69}
70
71impl Default for Bootstrap {
72 fn default() -> Self {
73 Self {
74 awareness: "This project uses ACP. @acp:* comments are directives for you.".to_string(),
75 workflow: "Before editing: acp constraints <path>".to_string(),
76 expansion: "More: acp primer --budget N".to_string(),
77 tokens: 20,
78 }
79 }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct Command {
85 pub name: String,
86 pub critical: bool,
87 pub priority: u32,
88 pub capabilities: Vec<String>,
89 pub tiers: TierContent,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct TierContent {
95 pub minimal: TierLevel,
96 pub standard: Option<TierLevel>,
97 pub full: Option<TierLevel>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct TierLevel {
103 pub tokens: u32,
104 pub template: String,
105}
106
107#[derive(Debug, Clone, Serialize)]
109pub struct PrimerOutput {
110 pub total_tokens: u32,
111 pub tier: String,
112 pub commands_included: usize,
113 pub content: String,
114}
115
116pub fn execute_primer(options: PrimerOptions) -> Result<()> {
118 let primer = generate_primer(&options)?;
119
120 if options.json || matches!(options.format, PrimerFormat::Json) {
121 println!("{}", serde_json::to_string_pretty(&primer)?);
122 } else {
123 println!("{}", primer.content);
124 }
125
126 Ok(())
127}
128
129pub fn generate_primer(options: &PrimerOptions) -> Result<PrimerOutput> {
131 let bootstrap = Bootstrap::default();
132 let commands = get_default_commands();
133
134 let filtered_commands: Vec<&Command> = if options.capabilities.is_empty() {
136 commands.iter().collect()
137 } else {
138 commands
139 .iter()
140 .filter(|cmd| {
141 cmd.capabilities.is_empty()
142 || cmd
143 .capabilities
144 .iter()
145 .any(|cap| options.capabilities.contains(cap))
146 })
147 .collect()
148 };
149
150 let mut sorted_commands = filtered_commands;
152 sorted_commands.sort_by(|a, b| match (a.critical, b.critical) {
153 (true, false) => Ordering::Less,
154 (false, true) => Ordering::Greater,
155 _ => a.priority.cmp(&b.priority),
156 });
157
158 let remaining_budget = options.budget.saturating_sub(bootstrap.tokens);
160 let tier = Tier::from_budget(remaining_budget);
161
162 let mut used_tokens = bootstrap.tokens;
164 let mut selected_commands: Vec<(&Command, &TierLevel)> = Vec::new();
165
166 for cmd in sorted_commands {
167 let tier_level = match tier {
169 Tier::Full => cmd
170 .tiers
171 .full
172 .as_ref()
173 .or(cmd.tiers.standard.as_ref())
174 .unwrap_or(&cmd.tiers.minimal),
175 Tier::Standard => cmd.tiers.standard.as_ref().unwrap_or(&cmd.tiers.minimal),
176 Tier::Minimal => &cmd.tiers.minimal,
177 };
178
179 let cmd_tokens = tier_level.tokens;
180
181 if cmd.critical || used_tokens + cmd_tokens <= options.budget {
183 used_tokens += cmd_tokens;
184 selected_commands.push((cmd, tier_level));
185 }
186 }
187
188 let mut content = String::new();
190
191 content.push_str(&bootstrap.awareness);
193 content.push('\n');
194 content.push_str(&bootstrap.workflow);
195 content.push('\n');
196 content.push_str(&bootstrap.expansion);
197 content.push_str("\n\n");
198
199 for (cmd, tier_level) in &selected_commands {
201 content.push_str(&format!("{}\n", style(&cmd.name).bold()));
202 content.push_str(&tier_level.template);
203 content.push_str("\n\n");
204 }
205
206 if let Some(cache_path) = &options.cache {
208 if cache_path.exists() && used_tokens + 30 < options.budget {
209 if let Ok(cache) = Cache::from_json(cache_path) {
210 let warnings = get_project_warnings(&cache);
211 if !warnings.is_empty() {
212 content.push_str(&format!("{}\n", style("Project Warnings").bold()));
213 for warning in warnings.iter().take(3) {
214 content.push_str(&format!(" - {}\n", warning));
215 used_tokens += 15;
216 if used_tokens >= options.budget {
217 break;
218 }
219 }
220 }
221 }
222 }
223 }
224
225 let tier_name = match tier {
226 Tier::Minimal => "minimal",
227 Tier::Standard => "standard",
228 Tier::Full => "full",
229 };
230
231 Ok(PrimerOutput {
232 total_tokens: used_tokens,
233 tier: tier_name.to_string(),
234 commands_included: selected_commands.len(),
235 content: content.trim().to_string(),
236 })
237}
238
239fn get_project_warnings(cache: &Cache) -> Vec<String> {
241 let mut warnings = Vec::new();
242
243 for (name, symbol) in &cache.symbols {
245 if let Some(ref constraints) = symbol.constraints {
246 if constraints.level == "frozen" || constraints.level == "restricted" {
247 warnings.push(format!(
248 "{}: {} ({})",
249 name,
250 constraints.level,
251 constraints.directive.chars().take(50).collect::<String>()
252 ));
253 }
254 }
255 }
256
257 warnings.truncate(5);
259 warnings
260}
261
262fn get_default_commands() -> Vec<Command> {
264 vec![
265 Command {
266 name: "acp constraints <path>".to_string(),
267 critical: true,
268 priority: 1,
269 capabilities: vec!["shell".to_string()],
270 tiers: TierContent {
271 minimal: TierLevel {
272 tokens: 8,
273 template: " Returns: lock level + directive".to_string(),
274 },
275 standard: Some(TierLevel {
276 tokens: 25,
277 template: " Returns: lock level + directive
278 Levels: frozen (refuse), restricted (ask), normal (proceed)
279 Use: Check before ANY file modification"
280 .to_string(),
281 }),
282 full: Some(TierLevel {
283 tokens: 45,
284 template: " Returns: lock level + directive
285 Levels: frozen (refuse), restricted (ask), normal (proceed)
286 Use: Check before ANY file modification
287 Example:
288 $ acp constraints src/auth/session.ts
289 frozen - Core auth logic; security-critical"
290 .to_string(),
291 }),
292 },
293 },
294 Command {
295 name: "acp query file <path>".to_string(),
296 critical: false,
297 priority: 2,
298 capabilities: vec!["shell".to_string()],
299 tiers: TierContent {
300 minimal: TierLevel {
301 tokens: 6,
302 template: " Returns: purpose, constraints, symbols".to_string(),
303 },
304 standard: Some(TierLevel {
305 tokens: 20,
306 template: " Returns: purpose, constraints, symbols, dependencies
307 Options: --json for machine-readable output
308 Use: Understand file context before working with it"
309 .to_string(),
310 }),
311 full: Some(TierLevel {
312 tokens: 35,
313 template: " Returns: purpose, constraints, symbols, dependencies
314 Options: --json for machine-readable output
315 Use: Understand file context before working with it
316 Example:
317 $ acp query file src/payments/processor.ts"
318 .to_string(),
319 }),
320 },
321 },
322 Command {
323 name: "acp query symbol <name>".to_string(),
324 critical: false,
325 priority: 3,
326 capabilities: vec!["shell".to_string()],
327 tiers: TierContent {
328 minimal: TierLevel {
329 tokens: 6,
330 template: " Returns: signature, purpose, constraints, callers".to_string(),
331 },
332 standard: Some(TierLevel {
333 tokens: 18,
334 template: " Returns: signature, purpose, constraints, callers/callees
335 Options: --json for machine-readable output
336 Use: Understand function/method before modifying"
337 .to_string(),
338 }),
339 full: None,
340 },
341 },
342 Command {
343 name: "acp query domain <name>".to_string(),
344 critical: false,
345 priority: 4,
346 capabilities: vec!["shell".to_string()],
347 tiers: TierContent {
348 minimal: TierLevel {
349 tokens: 5,
350 template: " Returns: domain files, cross-cutting concerns".to_string(),
351 },
352 standard: Some(TierLevel {
353 tokens: 15,
354 template: " Returns: domain files, cross-cutting concerns
355 Options: --json for machine-readable output
356 Use: Understand architectural boundaries"
357 .to_string(),
358 }),
359 full: None,
360 },
361 },
362 Command {
363 name: "acp map [path]".to_string(),
364 critical: false,
365 priority: 5,
366 capabilities: vec!["shell".to_string()],
367 tiers: TierContent {
368 minimal: TierLevel {
369 tokens: 5,
370 template: " Returns: directory tree with purposes".to_string(),
371 },
372 standard: Some(TierLevel {
373 tokens: 15,
374 template: " Returns: directory tree with purposes and constraints
375 Options: --depth N, --inline (show todos/hacks)
376 Use: Navigate unfamiliar codebase"
377 .to_string(),
378 }),
379 full: None,
380 },
381 },
382 Command {
383 name: "acp expand <text>".to_string(),
384 critical: false,
385 priority: 6,
386 capabilities: vec!["shell".to_string()],
387 tiers: TierContent {
388 minimal: TierLevel {
389 tokens: 5,
390 template: " Expands $variable references to full paths".to_string(),
391 },
392 standard: Some(TierLevel {
393 tokens: 12,
394 template: " Expands $variable references to full paths
395 Options: --mode inline|annotated
396 Use: Resolve variable shortcuts in instructions"
397 .to_string(),
398 }),
399 full: None,
400 },
401 },
402 Command {
403 name: "acp attempt start <id>".to_string(),
404 critical: false,
405 priority: 7,
406 capabilities: vec!["shell".to_string()],
407 tiers: TierContent {
408 minimal: TierLevel {
409 tokens: 5,
410 template: " Creates checkpoint for safe experimentation".to_string(),
411 },
412 standard: Some(TierLevel {
413 tokens: 15,
414 template: " Creates checkpoint for safe experimentation
415 Related: acp attempt fail <id>, acp attempt verify <id>
416 Use: Track and revert failed approaches"
417 .to_string(),
418 }),
419 full: None,
420 },
421 },
422 Command {
423 name: "acp primer --budget <N>".to_string(),
424 critical: false,
425 priority: 8,
426 capabilities: vec!["shell".to_string()],
427 tiers: TierContent {
428 minimal: TierLevel {
429 tokens: 5,
430 template: " Get more context (this command)".to_string(),
431 },
432 standard: Some(TierLevel {
433 tokens: 10,
434 template: " Get more context within token budget
435 Options: --capabilities shell,mcp
436 Use: Request more detailed primer"
437 .to_string(),
438 }),
439 full: None,
440 },
441 },
442 ]
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448
449 #[test]
450 fn test_tier_from_budget() {
451 assert_eq!(Tier::from_budget(50), Tier::Minimal);
452 assert_eq!(Tier::from_budget(79), Tier::Minimal);
453 assert_eq!(Tier::from_budget(80), Tier::Standard);
454 assert_eq!(Tier::from_budget(200), Tier::Standard);
455 assert_eq!(Tier::from_budget(299), Tier::Standard);
456 assert_eq!(Tier::from_budget(300), Tier::Full);
457 assert_eq!(Tier::from_budget(500), Tier::Full);
458 }
459
460 #[test]
461 fn test_generate_minimal_primer() {
462 let options = PrimerOptions {
463 budget: 60,
464 capabilities: vec![],
465 format: PrimerFormat::Text,
466 cache: None,
467 json: false,
468 };
469
470 let result = generate_primer(&options).unwrap();
471 assert!(result.total_tokens <= 60 || result.commands_included == 1); assert_eq!(result.tier, "minimal");
473 assert!(result.content.contains("constraints"));
474 }
475
476 #[test]
477 fn test_generate_standard_primer() {
478 let options = PrimerOptions {
479 budget: 200,
480 capabilities: vec![],
481 format: PrimerFormat::Text,
482 cache: None,
483 json: false,
484 };
485
486 let result = generate_primer(&options).unwrap();
487 assert_eq!(result.tier, "standard");
488 assert!(result.commands_included >= 3);
489 }
490
491 #[test]
492 fn test_critical_commands_always_included() {
493 let options = PrimerOptions {
494 budget: 30, capabilities: vec![],
496 format: PrimerFormat::Text,
497 cache: None,
498 json: false,
499 };
500
501 let result = generate_primer(&options).unwrap();
502 assert!(result.content.contains("constraints"));
504 }
505
506 #[test]
507 fn test_capability_filtering() {
508 let options = PrimerOptions {
509 budget: 200,
510 capabilities: vec!["mcp".to_string()], format: PrimerFormat::Text,
512 cache: None,
513 json: false,
514 };
515
516 let result = generate_primer(&options).unwrap();
517 assert!(result.content.contains("constraints")); }
521}