1use crate::tools::CrpMode;
2
3const INSTRUCTION_CAP_TOKENS: usize = 1200;
12
13pub fn build_instructions(crp_mode: CrpMode) -> String {
14 build_instructions_with_client(crp_mode, "")
15}
16
17pub fn build_instructions_with_client(crp_mode: CrpMode, client_name: &str) -> String {
18 if is_claude_code_client(client_name) {
19 return build_claude_code_instructions();
20 }
21 build_full_instructions(crp_mode, client_name)
22}
23
24pub fn build_instructions_for_test(crp_mode: CrpMode) -> String {
25 build_full_instructions_for_test(crp_mode, "")
28}
29
30pub fn build_instructions_with_client_for_test(crp_mode: CrpMode, client_name: &str) -> String {
31 if is_claude_code_client(client_name) {
32 return build_claude_code_instructions();
33 }
34 build_full_instructions_for_test(crp_mode, client_name)
35}
36
37pub fn build_instructions_with_client_for_compiler(
42 crp_mode: CrpMode,
43 client_name: &str,
44 unified_tool_mode: bool,
45) -> String {
46 if is_claude_code_client(client_name) {
47 return build_claude_code_instructions();
48 }
49 build_full_instructions_for_compiler(crp_mode, client_name, unified_tool_mode)
50}
51
52fn is_claude_code_client(client_name: &str) -> bool {
53 let lower = client_name.to_lowercase();
54 lower.contains("claude") && !lower.contains("cursor")
55}
56
57pub fn claude_config_dir_display() -> String {
58 match std::env::var("CLAUDE_CONFIG_DIR") {
59 Ok(dir) if !dir.trim().is_empty() => {
60 let dir = dir.trim().to_string();
61 if dir.starts_with('~') {
62 dir
63 } else if let Some(home) = dirs::home_dir() {
64 let home_str = home.to_string_lossy();
65 if let Some(rest) = dir.strip_prefix(home_str.as_ref()) {
66 format!("~{rest}")
67 } else {
68 dir
69 }
70 } else {
71 dir
72 }
73 }
74 _ => "~/.claude".to_string(),
75 }
76}
77
78fn build_claude_code_instructions() -> String {
79 let shell_hint = build_shell_hint();
80 let config_dir = claude_config_dir_display();
81
82 let session_block = match crate::core::session::SessionState::load_latest() {
84 Some(session) => {
85 let mut parts = Vec::new();
86 if let Some(ref task) = session.task {
87 let pct = task
88 .progress_pct
89 .map_or(String::new(), |p| format!(" [{p}%]"));
90 parts.push(format!("Task: {}{pct}", task.description));
91 }
92 if !session.decisions.is_empty() {
93 let items: Vec<&str> = session
94 .decisions
95 .iter()
96 .rev()
97 .take(3)
98 .map(|d| d.summary.as_str())
99 .collect();
100 parts.push(format!("Decisions: {}", items.join("; ")));
101 }
102 if !session.files_touched.is_empty() {
103 let modified: Vec<&str> = session
104 .files_touched
105 .iter()
106 .filter(|f| f.modified)
107 .take(5)
108 .map(|f| f.path.as_str())
109 .collect();
110 if !modified.is_empty() {
111 parts.push(format!("Modified: {}", modified.join(", ")));
112 }
113 }
114 if !session.findings.is_empty() {
115 let recent: Vec<&str> = session
116 .findings
117 .iter()
118 .rev()
119 .take(3)
120 .map(|f| f.summary.as_str())
121 .collect();
122 parts.push(format!("Recent: {}", recent.join("; ")));
123 }
124 if parts.is_empty() {
125 String::new()
126 } else {
127 format!("\n\n--- SESSION ---\n{}\n---", parts.join("\n"))
128 }
129 }
130 None => String::new(),
131 };
132
133 let instr = format!("\
134ALWAYS use lean-ctx MCP tools instead of native equivalents.
135
136Tool mapping (MANDATORY):
137• Read/cat/head/tail -> ctx_read(path, mode)
138• Shell/bash -> ctx_shell(command)
139• Grep/rg -> ctx_search(pattern, path)
140• ls/find -> ctx_tree(path, depth)
141• Edit/StrReplace -> native (lean-ctx=READ only). If Edit needs Read and Read is unavailable, use ctx_edit.
142• Write, Delete, Glob -> normal. NEVER loop on Edit failures — use ctx_edit.
143
144ctx_read modes: full|map|signatures|diff|task|reference|aggressive|entropy|lines:N-M
145Auto-selects mode. Re-reads ~13 tok. File refs F1,F2.. persist.
146Cache auto-validates via file mtime. Use fresh=true (or start_line / lines:N-M) to force a disk re-read.
147
148Auto: ctx_overview, ctx_preload, ctx_dedup, ctx_compress behind the scenes.
149Multi-agent: ctx_agent(action=handoff|sync|diary).
150ctx_semantic_search for meaning search. ctx_session for memory.
151ctx_knowledge: remember|recall|timeline|rooms|search|wakeup.
152ctx_shell raw=true for uncompressed.
153
154CEP: 1.ACT FIRST 2.DELTA ONLY 3.STRUCTURED(+/-/~) 4.ONE LINE 5.QUALITY
155{shell_hint}\
156Prefer: ctx_read>Read | ctx_shell>Shell | ctx_search>Grep | ctx_tree>ls
157Edit: native Edit/StrReplace preferred, ctx_edit if Edit unavailable.
158Never echo tool output. Never narrate. Show only changed code.
159Full instructions at {config_dir}/CLAUDE.md (imports rules/lean-ctx.md){session_block}");
160
161 instr
162}
163
164fn build_full_instructions(crp_mode: CrpMode, client_name: &str) -> String {
165 let cfg = crate::core::config::Config::load();
166 let minimal = cfg.minimal_overhead_effective_for_client(client_name);
167
168 let profile = crate::core::litm::LitmProfile::from_client_name(client_name);
169 let loaded_session = if minimal {
170 None
171 } else {
172 crate::core::session::SessionState::load_latest()
173 };
174
175 let (session_block, litm_end_block) = match loaded_session {
176 Some(ref session) => {
177 let positioned = crate::core::litm::position_optimize(session);
178 let begin = format!(
179 "\n\n--- ACTIVE SESSION (LITM P1: begin position, profile: {}) ---\n{}\n---\n",
180 profile.name, positioned.begin_block
181 );
182 let end = if positioned.end_block.is_empty() {
183 String::new()
184 } else {
185 format!(
186 "\n--- SESSION RESUME (post-compaction) ---\n{}\n---\n",
187 positioned.end_block
188 )
189 };
190 (begin, end)
191 }
192 None => (String::new(), String::new()),
193 };
194
195 let project_root_for_blocks = if minimal {
196 None
197 } else {
198 loaded_session
199 .as_ref()
200 .and_then(|s| s.project_root.clone())
201 .or_else(|| {
202 std::env::current_dir()
203 .ok()
204 .map(|p| p.to_string_lossy().to_string())
205 })
206 };
207
208 let knowledge_block = match &project_root_for_blocks {
209 Some(root) => {
210 let knowledge = crate::core::knowledge::ProjectKnowledge::load(root);
211 match knowledge {
212 Some(k) if !k.facts.is_empty() || !k.patterns.is_empty() => {
213 let aaak = k.format_aaak();
214 if aaak.is_empty() {
215 String::new()
216 } else {
217 format!("\n--- PROJECT MEMORY (AAAK) ---\n{}\n---\n", aaak.trim())
218 }
219 }
220 _ => String::new(),
221 }
222 }
223 None => String::new(),
224 };
225
226 let gotcha_block = match &project_root_for_blocks {
227 Some(root) => {
228 let store = crate::core::gotcha_tracker::GotchaStore::load(root);
229 let files: Vec<String> = loaded_session
230 .as_ref()
231 .map(|s| s.files_touched.iter().map(|ft| ft.path.clone()).collect())
232 .unwrap_or_default();
233 let block = store.format_injection_block(&files);
234 if block.is_empty() {
235 String::new()
236 } else {
237 format!("\n{block}\n")
238 }
239 }
240 None => String::new(),
241 };
242
243 let shell_hint = build_shell_hint();
244
245 use crate::core::rules_canonical as rc;
246 let tool_bullets = rc::tool_mapping_bullets(rc::Mode::Mcp);
247 let compat = rc::compatibility_block();
248 let read_modes = rc::ctx_read_modes_block();
249 let auto_block = rc::automation_block();
250 let cep = rc::cep_block();
251 let litm_pref = rc::litm_end_block(rc::Mode::Mcp);
252
253 let mut base = format!(
254 "\
255CRITICAL: ALWAYS use lean-ctx MCP tools instead of native equivalents for token savings.\n\
256\n\
257{tool_bullets}\n\
258\n\
259{compat}\n\
260{shell_hint}\
261\n\
262{read_modes}\n\
263\n\
264{auto_block}\n\
265\n\
266{cep}\n\
267\n\
268{decoder_block}\n\
269\n\
270{session_block}\
271{knowledge_block}\
272{gotcha_block}\
273\n\
274--- ORIGIN ---\n\
275{origin}\n\
276\n\
277{litm_pref}\
278{litm_end_block}",
279 decoder_block = crate::core::protocol::instruction_decoder_block(),
280 origin = crate::core::integrity::origin_line(),
281 litm_end_block = &litm_end_block
282 );
283
284 if should_use_unified(client_name) {
285 base.push_str("\n\n");
286 base.push_str(rc::unified_tool_mode_block());
287 base.push('\n');
288 }
289
290 let intelligence_block = build_intelligence_block();
291 let terse_block = build_terse_agent_block_for_client(&crp_mode, client_name);
292
293 let guidance_suffix = match crp_mode {
299 CrpMode::Off => format!("{terse_block}{intelligence_block}"),
300 CrpMode::Compact => format!(
301 "CRP MODE: compact\n\
302Omit filler. Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
303Diff lines (+/-) only. TARGET: <=200 tok. Trust tool outputs.\n\n\
304{terse_block}{intelligence_block}"
305 ),
306 CrpMode::Tdd => format!(
307 "CRP MODE: tdd\n\
308Max density. Every token carries meaning. Fn refs only, diff lines (+/-) only.\n\
309Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
310+F1:42 param(timeout:Duration) | -F1:10-15 | ~F1:42 old->new\n\
311BUDGET: <=150 tok. ZERO NARRATION. Trust tool outputs.\n\n\
312{terse_block}{intelligence_block}"
313 ),
314 };
315
316 assemble_within_cap(&base, &guidance_suffix, INSTRUCTION_CAP_TOKENS)
317}
318
319fn assemble_within_cap(base: &str, suffix: &str, cap_tokens: usize) -> String {
324 use crate::core::tokens::count_tokens;
325 let suffix = suffix.trim_end_matches('\n');
326 if suffix.is_empty() {
327 let full = base.to_string();
328 return if count_tokens(&full) > cap_tokens {
329 truncate_to_token_cap(&full, cap_tokens)
330 } else {
331 full
332 };
333 }
334
335 let full = format!("{base}\n\n{suffix}");
336 if count_tokens(&full) <= cap_tokens {
337 return full;
338 }
339
340 let suffix_tokens = count_tokens(suffix);
341 let Some(base_budget) = cap_tokens.checked_sub(suffix_tokens + 1) else {
344 return truncate_to_token_cap(&full, cap_tokens);
345 };
346 let trimmed_base = truncate_to_token_cap(base, base_budget);
347 format!("{trimmed_base}\n\n{suffix}")
348}
349
350fn truncate_to_token_cap(s: &str, cap_tokens: usize) -> String {
351 use crate::core::tokens::count_tokens;
352 if count_tokens(s) <= cap_tokens {
353 return s.to_string();
354 }
355 let cuts: Vec<usize> = s.match_indices('\n').map(|(i, _)| i).collect();
362 let (mut lo, mut hi) = (0usize, cuts.len());
363 let mut best: Option<usize> = None;
364 while lo < hi {
365 let mid = lo + (hi - lo) / 2;
366 let end = cuts[mid];
367 if end > 0 && count_tokens(&s[..end]) <= cap_tokens {
368 best = Some(end);
369 lo = mid + 1;
370 } else {
371 hi = mid;
372 }
373 }
374 if let Some(end) = best {
375 return s[..end].to_string();
376 }
377 let byte_approx = cap_tokens * 4;
379 let safe = s.floor_char_boundary(byte_approx.min(s.len()));
380 s[..safe].to_string()
381}
382
383fn build_full_instructions_for_test(crp_mode: CrpMode, client_name: &str) -> String {
384 use crate::core::rules_canonical as rc;
385 let shell_hint = build_shell_hint();
386 let session_block = String::new();
387 let knowledge_block = String::new();
388 let gotcha_block = String::new();
389 let litm_end_block = String::new();
390
391 let tool_bullets = rc::tool_mapping_bullets(rc::Mode::Mcp);
392 let compat = rc::compatibility_block();
393 let read_modes = rc::ctx_read_modes_block();
394 let auto_block = rc::automation_block();
395 let cep = rc::cep_block();
396 let litm_pref = rc::litm_end_block(rc::Mode::Mcp);
397
398 let mut base = format!(
399 "\
400CRITICAL: ALWAYS use lean-ctx MCP tools instead of native equivalents for token savings.\n\
401\n\
402{tool_bullets}\n\
403\n\
404{compat}\n\
405{shell_hint}\
406\n\
407{read_modes}\n\
408\n\
409{auto_block}\n\
410\n\
411{cep}\n\
412\n\
413{decoder_block}\n\
414\n\
415{session_block}\
416{knowledge_block}\
417{gotcha_block}\
418\n\
419--- ORIGIN ---\n\
420{origin}\n\
421\n\
422{litm_pref}\
423{litm_end_block}",
424 decoder_block = crate::core::protocol::instruction_decoder_block(),
425 origin = crate::core::integrity::origin_line(),
426 litm_end_block = &litm_end_block
427 );
428
429 if should_use_unified(client_name) {
430 base.push_str("\n\n");
431 base.push_str(rc::unified_tool_mode_block());
432 base.push('\n');
433 }
434
435 let intelligence_block = build_intelligence_block();
436 let terse_block = build_terse_agent_block_for_client(&crp_mode, client_name);
437
438 match crp_mode {
439 CrpMode::Off => format!("{base}\n\n{terse_block}{intelligence_block}"),
440 CrpMode::Compact => {
441 format!(
442 "{base}\n\n\
443CRP MODE: compact\n\
444Omit filler. Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
445Diff lines (+/-) only. TARGET: <=200 tok. Trust tool outputs.\n\n\
446{terse_block}{intelligence_block}"
447 )
448 }
449 CrpMode::Tdd => {
450 format!(
451 "{base}\n\n\
452CRP MODE: tdd\n\
453Max density. Every token carries meaning. Fn refs only, diff lines (+/-) only.\n\
454Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
455+F1:42 param(timeout:Duration) | -F1:10-15 | ~F1:42 old->new\n\
456BUDGET: <=150 tok. ZERO NARRATION. Trust tool outputs.\n\n\
457{terse_block}{intelligence_block}"
458 )
459 }
460 }
461}
462
463fn build_full_instructions_for_compiler(
464 crp_mode: CrpMode,
465 client_name: &str,
466 unified_tool_mode: bool,
467) -> String {
468 let shell_hint = build_shell_hint();
469 let session_block = String::new();
470 let knowledge_block = String::new();
471 let gotcha_block = String::new();
472 let litm_end_block = String::new();
473
474 use crate::core::rules_canonical as rc;
475 let tool_bullets = rc::tool_mapping_bullets(rc::Mode::Mcp);
476 let compat = rc::compatibility_block();
477 let read_modes = rc::ctx_read_modes_block();
478 let auto_blk = rc::automation_block();
479 let cep = rc::cep_block();
480 let litm_pref = rc::litm_end_block(rc::Mode::Mcp);
481
482 let mut base = format!(
483 "\
484CRITICAL: ALWAYS use lean-ctx MCP tools instead of native equivalents for token savings.\n\
485\n\
486{tool_bullets}\n\
487\n\
488{compat}\n\
489{shell_hint}\
490\n\
491{read_modes}\n\
492\n\
493{auto_blk}\n\
494\n\
495{cep}\n\
496\n\
497{decoder_block}\n\
498\n\
499{session_block}\
500{knowledge_block}\
501{gotcha_block}\
502\n\
503--- ORIGIN ---\n\
504{origin}\n\
505\n\
506{litm_pref}\
507{litm_end_block}",
508 decoder_block = crate::core::protocol::instruction_decoder_block(),
509 origin = crate::core::integrity::origin_line(),
510 litm_end_block = &litm_end_block
511 );
512
513 if unified_tool_mode {
514 base.push_str("\n\n");
515 base.push_str(rc::unified_tool_mode_block());
516 base.push('\n');
517 }
518
519 let _ = client_name; let intelligence_block = build_intelligence_block();
521
522 match crp_mode {
523 CrpMode::Off => format!("{base}\n\n{intelligence_block}"),
524 CrpMode::Compact => {
525 format!(
526 "{base}\n\n\
527CRP MODE: compact\n\
528Omit filler. Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
529Diff lines (+/-) only. TARGET: <=200 tok. Trust tool outputs.\n\n\
530{intelligence_block}"
531 )
532 }
533 CrpMode::Tdd => {
534 format!(
535 "{base}\n\n\
536CRP MODE: tdd\n\
537Max density. Every token carries meaning. Fn refs only, diff lines (+/-) only.\n\
538Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
539+F1:42 param(timeout:Duration) | -F1:10-15 | ~F1:42 old->new\n\
540BUDGET: <=150 tok. ZERO NARRATION. Trust tool outputs.\n\n\
541{intelligence_block}"
542 )
543 }
544 }
545}
546
547pub fn claude_code_instructions() -> String {
548 build_claude_code_instructions()
549}
550
551fn build_terse_agent_block_for_client(_crp_mode: &CrpMode, client_name: &str) -> String {
552 use crate::core::config::{CompressionLevel, Config};
553 let cfg = Config::load();
554 let compression = CompressionLevel::effective(&cfg);
555 if compression.is_active() {
556 return crate::core::terse::agent_prompts::build_prompt_block_for_client(
557 &compression,
558 client_name,
559 );
560 }
561 String::new()
562}
563
564fn build_intelligence_block() -> String {
565 "\
566OUTPUT EFFICIENCY:\n\
567• Never echo tool output code. Never add narration comments. Show only changed code.\n\
568• [TASK:type] and SCOPE hints included. Architecture=thorough, generate=code."
569 .to_string()
570}
571
572fn build_shell_hint() -> String {
573 if !cfg!(windows) {
574 return String::new();
575 }
576 let name = crate::shell::shell_name();
577 let is_posix = matches!(name.as_str(), "bash" | "sh" | "zsh" | "fish");
578 if is_posix {
579 format!(
580 "\nSHELL: {name} (POSIX). Use POSIX commands (cat, head, grep, find, ls). \
581 Do NOT use PowerShell cmdlets (Get-Content, Select-Object, Get-ChildItem).\n"
582 )
583 } else if name.contains("powershell") || name.contains("pwsh") {
584 format!("\nSHELL: {name}. Use PowerShell cmdlets.\n")
585 } else {
586 format!("\nSHELL: {name}.\n")
587 }
588}
589
590fn should_use_unified(client_name: &str) -> bool {
591 if std::env::var("LEAN_CTX_FULL_TOOLS").is_ok() {
592 return false;
593 }
594 if std::env::var("LEAN_CTX_UNIFIED").is_ok() {
595 return true;
596 }
597 let _ = client_name;
598 false
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604 use crate::core::tokens::count_tokens;
605
606 #[test]
607 fn guidance_suffix_survives_oversized_base() {
608 let base = "SESSION LINE\n".repeat(4000);
610 let suffix = "OUTPUT STYLE: expert-terse\nFn refs only, diff lines only.";
611 let out = assemble_within_cap(&base, suffix, INSTRUCTION_CAP_TOKENS);
612
613 assert!(
614 out.contains("OUTPUT STYLE: expert-terse"),
615 "protected guidance suffix must survive truncation"
616 );
617 assert!(
618 count_tokens(&out) <= INSTRUCTION_CAP_TOKENS,
619 "assembled output must respect the token cap"
620 );
621 assert!(
622 out.len() < base.len(),
623 "oversized base must have been truncated"
624 );
625 }
626
627 #[test]
628 fn under_cap_keeps_everything() {
629 let base = "tool mapping block";
630 let suffix = "OUTPUT STYLE: dense";
631 let out = assemble_within_cap(base, suffix, INSTRUCTION_CAP_TOKENS);
632 assert!(out.contains(base));
633 assert!(out.contains(suffix));
634 }
635
636 #[test]
637 fn empty_suffix_caps_base_only() {
638 let base = "x\n".repeat(4000);
639 let out = assemble_within_cap(&base, "", INSTRUCTION_CAP_TOKENS);
640 assert!(count_tokens(&out) <= INSTRUCTION_CAP_TOKENS);
641 }
642}