1use std::sync::Arc;
13
14#[derive(Clone)]
16pub enum SectionContent {
17 Static(&'static str),
19 Dynamic(Arc<dyn Fn() -> String + Send + Sync>),
21 Cached(String),
23}
24
25impl SectionContent {
26 pub fn static_content(s: &'static str) -> Self {
28 Self::Static(s)
29 }
30
31 pub fn dynamic<F>(f: F) -> Self
33 where
34 F: Fn() -> String + Send + Sync + 'static,
35 {
36 Self::Dynamic(Arc::new(f))
37 }
38
39 pub fn compute(&self) -> String {
41 match self {
42 Self::Static(s) => s.to_string(),
43 Self::Dynamic(f) => f(),
44 Self::Cached(s) => s.clone(),
45 }
46 }
47
48 pub fn is_cacheable(&self) -> bool {
50 matches!(self, Self::Static(_) | Self::Cached(_))
51 }
52
53 pub fn cache(self, content: String) -> Self {
55 Self::Cached(content)
56 }
57}
58
59impl std::fmt::Debug for SectionContent {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 match self {
62 Self::Static(s) => f.debug_tuple("Static").field(&s.len()).finish(),
63 Self::Dynamic(_) => f.write_str("Dynamic(<function>)"),
64 Self::Cached(s) => f.debug_tuple("Cached").field(&s.len()).finish(),
65 }
66 }
67}
68
69#[derive(Clone, Debug)]
71pub struct PromptSection {
72 pub name: String,
74 pub content: SectionContent,
76 pub cacheable: bool,
78 pub order: usize,
80}
81
82pub fn red_flags_section() -> PromptSection {
91 PromptSection::static_section("red_flags", RED_FLAGS_CONTENT).with_order(10)
92}
93
94pub fn skill_priority_section() -> PromptSection {
99 PromptSection::static_section("skill_priority", SKILL_PRIORITY_CONTENT).with_order(15)
100}
101
102pub fn skill_rules_section() -> PromptSection {
107 PromptSection::static_section("skill_rules", SKILL_RULES_CONTENT).with_order(5)
108}
109
110pub fn tool_guidelines_section() -> PromptSection {
112 PromptSection::static_section("tool_guidelines", TOOL_GUIDELINES_CONTENT).with_order(20)
113}
114
115const RED_FLAGS_CONTENT: &'static str = "
117## Red Flags - STOP and reconsider
118
119These thoughts mean STOP — you're rationalizing:
120
121| Thought | Reality |
122|---------|---------|
123| \"This is just a simple question\" | Questions are tasks. Check for skills. |
124| \"I need more context first\" | Skill check comes BEFORE clarifying questions. |
125| \"Let me explore the codebase first\" | Skills tell you HOW to explore. Check first. |
126| \"I can check git/files quickly\" | Files lack conversation context. Check for skills. |
127| \"Let me gather information first\" | Skills tell you HOW to gather information. |
128| \"This doesn't need a formal skill\" | If a skill exists, use it. |
129| \"I remember this skill\" | Skills evolve. Read current version. |
130| \"This doesn't count as a task\" | Action = task. Check for skills. |
131| \"The skill is overkill\" | Simple things become complex. Use it. |
132| \"I'll just do this one thing first\" | Check BEFORE doing anything. |
133| \"This feels productive\" | Undisciplined action wastes time. Skills prevent this. |
134| \"I know what that means\" | Knowing the concept ≠ using the skill. Invoke it. |
135
136**When you catch yourself thinking these, STOP and invoke the relevant skill.**
137";
138
139const SKILL_PRIORITY_CONTENT: &'static str = "
141## Skill Priority
142
143When multiple skills could apply, use this order:
144
1451. **Process skills first** (brainstorming, debugging, planning) - these determine HOW to approach the task
1462. **Implementation skills second** (frontend-design, mcp-builder, code-review) - these guide execution
147
148Examples:
149- \"Let's build X\" → brainstorming first, then implementation skills
150- \"Fix this bug\" → debugging first, then domain-specific skills
151- \"Review this code\" → code-review skill directly (domain-specific)
152
153## Skill Types
154
155- **Rigid skills** (TDD, debugging): Follow exactly. Don't adapt away discipline.
156- **Flexible skills** (patterns): Adapt principles to context.
157
158The skill itself tells you which type it is.
159";
160
161const SKILL_RULES_CONTENT: &'static str = "
163<EXTREMELY-IMPORTANT>
164If you think there is even a **1% chance** a skill might apply to what you are doing,
165you **ABSOLUTELY MUST** invoke the skill.
166
167IF A SKILL APPLIES TO YOUR TASK, YOU DO NOT HAVE A CHOICE. YOU MUST USE IT.
168
169This is not negotiable. This is not optional. You cannot rationalize your way out of this.
170</EXTREMELY-IMPORTANT>
171
172## The Rule
173
174**Invoke relevant or requested skills BEFORE any response or action.**
175Even a 1% chance a skill might apply means that you should invoke the skill to check.
176If an invoked skill turns out to be wrong for the situation, you don't need to use it.
177";
178
179const TOOL_GUIDELINES_CONTENT: &'static str = "
181## Tool Usage Guidelines
182
183### When to use tools
184
185| Task | Preferred Tool |
186|------|----------------|
187| Find files by pattern | `Glob` (not `find` command) |
188| Search file contents | `Grep` (not `grep` command) |
189| Read a specific file | `Read` (not `cat` command) |
190| Search code symbols | `code_search` (not `grep`) |
191| Find function callers | `code_callers` (not manual search) |
192| Find function callees | `code_callees` (not manual search) |
193
194### CodeGraph vs Native Search
195
196Use **CodeGraph** (`code_*` tools) for structural questions:
197- \"Where is X defined?\" → `code_search`
198- \"What calls Y?\" → `code_callers`
199- \"What does Y call?\" → `code_callees`
200
201Use **native grep/read** for literal text:
202- String contents, comments, log messages
203- After you already have a specific file open
204
205### Rules of thumb
206
207- **Don't grep first** when looking up a symbol by name
208- **Trust CodeGraph results** — they come from full AST parse
209- **Don't re-verify** CodeGraph results with grep (slower, less accurate)
210";
211
212impl PromptSection {
213 pub fn static_section(name: impl Into<String>, content: &'static str) -> Self {
215 Self {
216 name: name.into(),
217 content: SectionContent::static_content(content),
218 cacheable: true,
219 order: 0,
220 }
221 }
222
223 pub fn dynamic_section<F>(name: impl Into<String>, compute: F) -> Self
225 where
226 F: Fn() -> String + Send + Sync + 'static,
227 {
228 Self {
229 name: name.into(),
230 content: SectionContent::dynamic(compute),
231 cacheable: false,
232 order: 0,
233 }
234 }
235
236 pub fn cached_section(name: impl Into<String>, content: String) -> Self {
238 Self {
239 name: name.into(),
240 content: SectionContent::Cached(content),
241 cacheable: true,
242 order: 0,
243 }
244 }
245
246 pub fn with_order(self, order: usize) -> Self {
248 Self { order, ..self }
249 }
250
251 pub fn with_cacheable(self, cacheable: bool) -> Self {
253 Self { cacheable, ..self }
254 }
255
256 pub fn render(&self) -> String {
258 let content = self.content.compute();
259 if content.is_empty() {
260 String::new()
261 } else {
262 format!("[{}]\n{}", self.name, content)
263 }
264 }
265
266 pub fn compute_content(&self) -> String {
268 self.content.compute()
269 }
270
271 pub fn estimated_tokens(&self) -> usize {
273 let content = self.compute_content();
275 let chinese_chars = content
276 .chars()
277 .filter(|c| c.is_alphabetic() && c.len_utf8() > 1)
278 .count();
279 let english_words = content.split_whitespace().count();
280 chinese_chars / 3 + english_words + (content.len() - chinese_chars) / 4
281 }
282}
283
284pub struct SectionBuilder {
286 sections: Vec<PromptSection>,
287}
288
289impl SectionBuilder {
290 pub fn new() -> Self {
291 Self {
292 sections: Vec::new(),
293 }
294 }
295
296 pub fn add_static(self, name: impl Into<String>, content: &'static str) -> Self {
298 self.add_section(PromptSection::static_section(name, content))
299 }
300
301 pub fn add_dynamic<F>(self, name: impl Into<String>, compute: F) -> Self
303 where
304 F: Fn() -> String + Send + Sync + 'static,
305 {
306 self.add_section(PromptSection::dynamic_section(name, compute))
307 }
308
309 pub fn add_section(mut self, section: PromptSection) -> Self {
311 self.sections.push(section);
312 self
313 }
314
315 pub fn build(self) -> Vec<PromptSection> {
317 let mut sections = self.sections;
318 sections.sort_by_key(|s| s.order);
319 sections
320 }
321}
322
323impl Default for SectionBuilder {
324 fn default() -> Self {
325 Self::new()
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn test_static_section() {
335 let section = PromptSection::static_section("identity", "You are an AI assistant.");
336 assert!(section.cacheable);
337 assert_eq!(section.compute_content(), "You are an AI assistant.");
338 }
339
340 #[test]
341 fn test_dynamic_section() {
342 let section = PromptSection::dynamic_section("date", || {
343 format!("Current date: {}", chrono::Local::now().format("%Y-%m-%d"))
344 });
345 assert!(!section.cacheable);
346 let content = section.compute_content();
347 assert!(content.starts_with("Current date:"));
348 }
349
350 #[test]
351 fn test_render_with_header() {
352 let section = PromptSection::static_section("test", "Hello");
353 let rendered = section.render();
354 assert_eq!(rendered, "[test]\nHello");
355 }
356
357 #[test]
358 fn test_section_builder() {
359 let sections = SectionBuilder::new()
360 .add_static("a", "content a")
361 .add_static("b", "content b")
362 .build();
363 assert_eq!(sections.len(), 2);
364 }
365
366 #[test]
367 fn test_order_sorting() {
368 let sections = SectionBuilder::new()
369 .add_section(PromptSection::static_section("last", "c").with_order(10))
370 .add_section(PromptSection::static_section("first", "a").with_order(1))
371 .add_section(PromptSection::static_section("middle", "b").with_order(5))
372 .build();
373
374 assert_eq!(sections[0].name, "first");
375 assert_eq!(sections[1].name, "middle");
376 assert_eq!(sections[2].name, "last");
377 }
378
379 #[test]
380 fn test_predefined_red_flags_section() {
381 let section = red_flags_section();
382 assert_eq!(section.name, "red_flags");
383 assert!(section.cacheable);
384 assert!(section.order == 10);
385 let content = section.compute_content();
386 assert!(content.contains("Red Flags"));
387 assert!(content.contains("STOP"));
388 assert!(content.contains("This is just a simple question"));
389 }
390
391 #[test]
392 fn test_predefined_skill_priority_section() {
393 let section = skill_priority_section();
394 assert_eq!(section.name, "skill_priority");
395 assert!(section.cacheable);
396 assert!(section.order == 15);
397 let content = section.compute_content();
398 assert!(content.contains("Skill Priority"));
399 assert!(content.contains("Process skills first"));
400 }
401
402 #[test]
403 fn test_predefined_skill_rules_section() {
404 let section = skill_rules_section();
405 assert_eq!(section.name, "skill_rules");
406 assert!(section.cacheable);
407 assert!(section.order == 5); let content = section.compute_content();
409 assert!(content.contains("1%"));
410 assert!(content.contains("MUST"));
411 }
412
413 #[test]
414 fn test_predefined_tool_guidelines_section() {
415 let section = tool_guidelines_section();
416 assert_eq!(section.name, "tool_guidelines");
417 assert!(section.cacheable);
418 let content = section.compute_content();
419 assert!(content.contains("Glob"));
420 assert!(content.contains("Grep"));
421 assert!(content.contains("CodeGraph"));
422 }
423
424 #[test]
425 fn test_predefined_sections_order() {
426 let rules = skill_rules_section();
428 let flags = red_flags_section();
429 let priority = skill_priority_section();
430 let tools = tool_guidelines_section();
431
432 assert!(rules.order < flags.order);
434 assert!(flags.order < priority.order);
435 assert!(priority.order < tools.order);
436 }
437}