1use std::path::PathBuf;
11
12pub const FOUNDATION_PROMPT: &str = r#"# System Instruction:
16You are an AI coding assistant. Your primary objective is to help the user produce correct, maintainable, secure software. Prefer quality, testability, and clear reasoning over speed or verbosity.
17
18## Operating principles
19- Clarify intent: If requirements are ambiguous or conflicting, ask the minimum number of targeted questions. If you can proceed with reasonable assumptions, state them explicitly and continue.
20- Plan before code: Briefly outline the approach, constraints, and tradeoffs, then implement.
21- Correctness first: Favor simple, reliable solutions. Avoid cleverness that reduces readability or increases risk.
22- Verification mindset: Provide ways to validate (tests, edge cases, invariants, quick checks, sample inputs/outputs). If uncertain, say so and propose a validation path.
23- Security and safety: Avoid insecure defaults. Highlight risky patterns (injection, authz/authn, secrets, SSRF, deserialization, unsafe file ops). Use least privilege and safe parsing.
24- Action over documentation: Code change requests (fix, update, migrate, implement) require code changes, not documentation.
25
26## Interaction contract
27- Start by confirming: language, runtime/versions, target environment, constraints (performance, memory, latency), and any style/architecture preferences. Only ask when missing details materially affect the solution.
28- Before modifying code: Read the file first to understand existing patterns, then make minimal, coherent changes that preserve conventions.
29- When proposing dependencies: keep them minimal; justify each; offer a standard-library alternative when feasible.
30- When giving commands or scripts: make them copy/paste-ready and note OS assumptions.
31- Never fabricate: If you don't know a detail (API, library behavior, version), say so and offer how to check.
32
33## Output format
34- Prefer structured responses:
35 1) Understanding (what you think the user wants + assumptions)
36 2) Approach (short plan + key tradeoffs)
37 3) Implementation (code)
38 4) Validation (tests/checks + edge cases)
39 5) Next steps (optional improvements)
40- Keep explanations concise, but include enough rationale for review and maintenance.
41
42## Code quality rules
43- Write idiomatic code for the requested language.
44- Include error handling, input validation, and clear naming.
45- Avoid premature optimization; note where optimization would be justified.
46- Add tests (unit/integration) when applicable and show how to run them.
47- For performance-sensitive tasks, analyze complexity and propose benchmarks.
48
49## Context handling
50- Use only the information provided in the conversation. If critical context is missing, ask. If a file or snippet is referenced but not included, request it.
51- Remember user-stated preferences (style, tools, constraints) within the session and apply them consistently.
52- ACP context usage: Use provided ACP metadata to navigate to relevant files quickly. Before modifying any file, read it first to verify your understanding matches reality. The metadata helps you find files faster—but you must still read what you'll change.
53
54You are a collaborative partner: be direct, careful, and review-oriented."#;
55
56pub const FOUNDATION_TOKENS: u32 = 620;
59
60use anyhow::Result;
61use serde::Serialize;
62
63use crate::cache::Cache;
64use crate::primer::{
65 self, load_primer_config, render_primer_with_tier, select_sections, CliOverrides,
66 IdeEnvironment, OutputFormat, PrimerTier, ProjectState,
67};
68
69#[derive(Debug, Clone)]
71pub struct PrimerOptions {
72 pub budget: u32,
74 pub capabilities: Vec<String>,
76 pub cache: Option<PathBuf>,
78 pub primer_config: Option<PathBuf>,
80 pub format: OutputFormat,
82 pub json: bool,
84 pub preset: Option<String>,
86 pub include: Vec<String>,
88 pub exclude: Vec<String>,
90 pub categories: Vec<String>,
92 pub no_dynamic: bool,
94 pub explain: bool,
96 pub list_sections: bool,
98 pub list_presets: bool,
100 pub preview: bool,
102 pub standalone: bool,
104 pub foundation_only: bool,
106 pub mcp: bool,
108}
109
110impl Default for PrimerOptions {
111 fn default() -> Self {
112 Self {
113 budget: 200,
114 capabilities: vec![],
115 cache: None,
116 primer_config: None,
117 format: OutputFormat::Markdown,
118 json: false,
119 preset: None,
120 include: vec![],
121 exclude: vec![],
122 categories: vec![],
123 no_dynamic: false,
124 explain: false,
125 list_sections: false,
126 list_presets: false,
127 preview: false,
128 standalone: false,
129 foundation_only: false,
130 mcp: false,
131 }
132 }
133}
134
135#[derive(Debug, Clone, Serialize)]
137pub struct PrimerOutput {
138 pub total_tokens: u32,
139 pub tier: PrimerTier,
141 pub sections_included: usize,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub selection_reasoning: Option<Vec<SelectionReason>>,
144 pub content: String,
145}
146
147#[derive(Debug, Clone, Serialize)]
148pub struct SelectionReason {
149 pub section_id: String,
150 pub phase: String,
151 pub value: f64,
152 pub tokens: u32,
153}
154
155pub fn execute_primer(options: PrimerOptions) -> Result<()> {
157 if options.foundation_only {
159 if options.json {
160 let output = serde_json::json!({
161 "foundation": FOUNDATION_PROMPT,
162 "tokens": FOUNDATION_TOKENS
163 });
164 println!("{}", serde_json::to_string_pretty(&output)?);
165 } else {
166 println!("{}", FOUNDATION_PROMPT);
167 }
168 return Ok(());
169 }
170
171 if options.list_presets {
173 println!("Available presets:\n");
174 for (name, description, weights) in primer::scoring::list_presets() {
175 println!(" {} - {}", console::style(name).bold(), description);
176 println!(
177 " safety={:.1} efficiency={:.1} accuracy={:.1} base={:.1}\n",
178 weights.safety, weights.efficiency, weights.accuracy, weights.base
179 );
180 }
181 return Ok(());
182 }
183
184 if options.list_sections {
185 let cli_overrides = CliOverrides::default();
186 let config = load_primer_config(options.primer_config.as_deref(), &cli_overrides)?;
187
188 println!("Available sections ({}):\n", config.sections.len());
189 for section in primer::selector::list_sections(&config) {
190 let required = if section.required { " [required]" } else { "" };
191 let caps = if section.capabilities.is_empty() {
192 String::new()
193 } else {
194 format!(" ({})", section.capabilities.join(","))
195 };
196 println!(
197 " {:30} {:15} ~{} tokens{}{}",
198 section.id, section.category, section.tokens, required, caps
199 );
200 }
201 return Ok(());
202 }
203
204 if options.standalone {
206 let ide = IdeEnvironment::detect_with_override();
207 if ide.is_ide() && !matches!(ide, IdeEnvironment::ClaudeCode) {
208 eprintln!(
209 "{}: Using --standalone in {} context. \
210 IDE integrations typically provide their own system prompts. \
211 Consider removing --standalone or set ACP_NO_IDE_DETECT=1 to suppress.",
212 console::style("warning").yellow().bold(),
213 ide.name()
214 );
215 }
216 }
217
218 let primer = generate_primer(&options)?;
220
221 if options.json {
222 if options.standalone {
224 let output = serde_json::json!({
225 "foundation": FOUNDATION_PROMPT,
226 "foundation_tokens": FOUNDATION_TOKENS,
227 "primer": primer
228 });
229 println!("{}", serde_json::to_string_pretty(&output)?);
230 } else {
231 println!("{}", serde_json::to_string_pretty(&primer)?);
232 }
233 } else if options.preview {
234 if options.standalone {
236 println!(
237 "Preview: {} tokens (foundation) + {} tokens (primer), {} sections",
238 FOUNDATION_TOKENS, primer.total_tokens, primer.sections_included
239 );
240 } else {
241 println!(
242 "Preview: {} tokens, {} sections",
243 primer.total_tokens, primer.sections_included
244 );
245 }
246 if let Some(reasons) = &primer.selection_reasoning {
247 println!("\nSelection:");
248 for reason in reasons {
249 println!(
250 " [{:12}] {:30} value={:.1} tokens={}",
251 reason.phase, reason.section_id, reason.value, reason.tokens
252 );
253 }
254 }
255 } else {
256 if options.standalone {
258 println!("{}\n\n---\n\n{}", FOUNDATION_PROMPT, primer.content);
259 } else {
260 println!("{}", primer.content);
261 }
262 }
263
264 Ok(())
265}
266
267pub fn generate_primer(options: &PrimerOptions) -> Result<PrimerOutput> {
269 let cli_overrides = CliOverrides {
271 include: options.include.clone(),
272 exclude: options.exclude.clone(),
273 preset: options.preset.clone(),
274 categories: options.categories.clone(),
275 no_dynamic: options.no_dynamic,
276 };
277
278 let config = load_primer_config(options.primer_config.as_deref(), &cli_overrides)?;
280
281 let project_state = if let Some(ref cache_path) = options.cache {
283 if cache_path.exists() {
284 let cache = Cache::from_json(cache_path)?;
285 ProjectState::from_cache(&cache)
286 } else {
287 ProjectState::default()
288 }
289 } else {
290 ProjectState::default()
291 };
292
293 let mut capabilities = options.capabilities.clone();
297 if options.mcp {
298 if !capabilities.contains(&"mcp".to_string()) {
299 capabilities.push("mcp".to_string());
300 }
301 } else if capabilities.is_empty() {
302 capabilities.push("shell".to_string());
304 }
305
306 let selected = select_sections(&config, options.budget, &capabilities, &project_state);
308
309 let total_tokens: u32 = selected.iter().map(|s| s.tokens).sum();
311
312 let selection_reasoning = if options.explain || options.preview {
314 Some(
315 selected
316 .iter()
317 .map(|s| SelectionReason {
318 section_id: s.id.clone(),
319 phase: determine_phase(&s.section),
320 value: s.value,
321 tokens: s.tokens,
322 })
323 .collect(),
324 )
325 } else {
326 None
327 };
328
329 let tier = PrimerTier::from_budget(options.budget);
331
332 let content = render_primer_with_tier(&selected, options.format, &project_state, Some(tier))?;
334
335 Ok(PrimerOutput {
336 total_tokens,
337 tier,
338 sections_included: selected.len(),
339 selection_reasoning,
340 content,
341 })
342}
343
344fn determine_phase(section: &primer::types::Section) -> String {
345 if section.required {
346 "required".to_string()
347 } else if section.required_if.is_some() {
348 "conditional".to_string()
349 } else if section.value.safety >= 80 {
350 "safety".to_string()
351 } else {
352 "value".to_string()
353 }
354}
355
356#[derive(Debug, Clone, Copy, Default)]
362pub enum PrimerFormat {
363 #[default]
364 Text,
365 Json,
366}
367
368#[derive(Debug, Clone, Copy, PartialEq, Eq)]
370pub enum Tier {
371 Minimal,
372 Standard,
373 Full,
374}
375
376impl Tier {
377 pub fn from_budget(remaining: u32) -> Self {
380 if remaining < 300 {
382 Tier::Minimal
383 } else if remaining < 700 {
384 Tier::Standard
385 } else {
386 Tier::Full
387 }
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 #[test]
396 fn test_primer_tier_from_budget() {
397 assert_eq!(PrimerTier::from_budget(50), PrimerTier::Micro);
399 assert_eq!(PrimerTier::from_budget(299), PrimerTier::Micro);
400 assert_eq!(PrimerTier::from_budget(300), PrimerTier::Minimal);
401 assert_eq!(PrimerTier::from_budget(449), PrimerTier::Minimal);
402 assert_eq!(PrimerTier::from_budget(450), PrimerTier::Standard);
403 assert_eq!(PrimerTier::from_budget(699), PrimerTier::Standard);
404 assert_eq!(PrimerTier::from_budget(700), PrimerTier::Full);
405 assert_eq!(PrimerTier::from_budget(1000), PrimerTier::Full);
406 }
407
408 #[test]
409 fn test_legacy_tier_from_budget() {
410 assert_eq!(Tier::from_budget(50), Tier::Minimal);
412 assert_eq!(Tier::from_budget(299), Tier::Minimal);
413 assert_eq!(Tier::from_budget(300), Tier::Standard);
414 assert_eq!(Tier::from_budget(699), Tier::Standard);
415 assert_eq!(Tier::from_budget(700), Tier::Full);
416 }
417
418 #[test]
419 fn test_generate_micro_primer() {
420 let options = PrimerOptions {
421 budget: 60,
422 ..Default::default()
423 };
424
425 let result = generate_primer(&options).unwrap();
426 assert!(result.total_tokens <= 300 || result.sections_included >= 1);
427 assert_eq!(result.tier, PrimerTier::Micro);
428 }
429
430 #[test]
431 fn test_generate_minimal_primer() {
432 let options = PrimerOptions {
433 budget: 350,
434 ..Default::default()
435 };
436
437 let result = generate_primer(&options).unwrap();
438 assert_eq!(result.tier, PrimerTier::Minimal);
439 assert!(result.sections_included >= 1);
440 }
441
442 #[test]
443 fn test_generate_standard_primer() {
444 let options = PrimerOptions {
445 budget: 500,
446 ..Default::default()
447 };
448
449 let result = generate_primer(&options).unwrap();
450 assert_eq!(result.tier, PrimerTier::Standard);
451 assert!(result.sections_included >= 1);
452 }
453
454 #[test]
455 fn test_generate_full_primer() {
456 let options = PrimerOptions {
457 budget: 800,
458 ..Default::default()
459 };
460
461 let result = generate_primer(&options).unwrap();
462 assert_eq!(result.tier, PrimerTier::Full);
463 assert!(result.sections_included >= 1);
464 }
465
466 #[test]
467 fn test_critical_commands_always_included() {
468 let options = PrimerOptions {
469 budget: 30,
470 ..Default::default()
471 };
472
473 let result = generate_primer(&options).unwrap();
474 assert!(result.sections_included >= 1);
476 }
477
478 #[test]
479 fn test_capability_filtering() {
480 let options = PrimerOptions {
481 budget: 500,
482 capabilities: vec!["mcp".to_string()],
483 ..Default::default()
484 };
485
486 let result = generate_primer(&options).unwrap();
487 assert!(result.sections_included >= 1);
489 }
490
491 #[test]
492 fn test_primer_tier_names() {
493 assert_eq!(PrimerTier::Micro.name(), "micro");
494 assert_eq!(PrimerTier::Minimal.name(), "minimal");
495 assert_eq!(PrimerTier::Standard.name(), "standard");
496 assert_eq!(PrimerTier::Full.name(), "full");
497 }
498
499 #[test]
500 fn test_primer_tier_tokens() {
501 assert_eq!(PrimerTier::Micro.cli_tokens(), 250);
502 assert_eq!(PrimerTier::Micro.mcp_tokens(), 178);
503 assert_eq!(PrimerTier::Standard.cli_tokens(), 600);
504 assert_eq!(PrimerTier::Full.cli_tokens(), 1400);
505 }
506
507 #[test]
512 fn test_foundation_prompt_content() {
513 assert!(FOUNDATION_PROMPT.starts_with("# System Instruction:"));
515 assert!(FOUNDATION_PROMPT.contains("## Operating principles"));
517 assert!(FOUNDATION_PROMPT.contains("## Interaction contract"));
518 assert!(FOUNDATION_PROMPT.contains("## Output format"));
519 assert!(FOUNDATION_PROMPT.contains("## Code quality rules"));
520 assert!(FOUNDATION_PROMPT.contains("## Context handling"));
521 assert!(FOUNDATION_PROMPT.contains("collaborative partner"));
523 }
524
525 #[test]
526 fn test_foundation_prompt_token_count() {
527 assert_eq!(FOUNDATION_TOKENS, 620);
529 assert!(FOUNDATION_PROMPT.len() > 2000);
531 }
532
533 #[test]
534 fn test_foundation_only_flag_default() {
535 let options = PrimerOptions::default();
536 assert!(!options.foundation_only);
537 assert!(!options.standalone);
538 }
539
540 #[test]
541 fn test_foundation_only_option() {
542 let options = PrimerOptions {
543 foundation_only: true,
544 ..Default::default()
545 };
546 assert!(options.foundation_only);
547 }
548
549 #[test]
550 fn test_standalone_option() {
551 let options = PrimerOptions {
552 standalone: true,
553 ..Default::default()
554 };
555 assert!(options.standalone);
556 }
557
558 #[test]
563 fn test_mcp_flag_default() {
564 let options = PrimerOptions::default();
565 assert!(!options.mcp);
566 }
567
568 #[test]
569 fn test_mcp_option() {
570 let options = PrimerOptions {
571 mcp: true,
572 budget: 300,
573 ..Default::default()
574 };
575 assert!(options.mcp);
576 let result = generate_primer(&options).unwrap();
578 assert!(result.sections_included >= 1);
579 }
580
581 #[test]
582 fn test_mcp_mode_adds_capability() {
583 let mcp_options = PrimerOptions {
585 mcp: true,
586 budget: 500,
587 ..Default::default()
588 };
589 let mcp_result = generate_primer(&mcp_options).unwrap();
590
591 let shell_options = PrimerOptions {
593 budget: 500,
594 ..Default::default()
595 };
596 let shell_result = generate_primer(&shell_options).unwrap();
597
598 assert!(mcp_result.sections_included >= 1);
600 assert!(shell_result.sections_included >= 1);
601 }
602
603 #[test]
604 fn test_default_shell_capability() {
605 let options = PrimerOptions {
607 budget: 500,
608 ..Default::default()
609 };
610 let result = generate_primer(&options).unwrap();
611 assert!(result.content.contains("acp") || result.sections_included >= 1);
613 }
614}