1use crate::tools::CrpMode;
2
3const CLAUDE_CODE_INSTRUCTION_CAP: usize = 2048;
6
7pub fn build_instructions(crp_mode: CrpMode) -> String {
8 build_instructions_with_client(crp_mode, "")
9}
10
11pub fn build_instructions_with_client(crp_mode: CrpMode, client_name: &str) -> String {
12 if is_claude_code_client(client_name) {
13 return build_claude_code_instructions();
14 }
15 build_full_instructions(crp_mode, client_name)
16}
17
18pub fn build_instructions_for_test(crp_mode: CrpMode) -> String {
19 build_full_instructions_for_test(crp_mode, "")
22}
23
24pub fn build_instructions_with_client_for_test(crp_mode: CrpMode, client_name: &str) -> String {
25 if is_claude_code_client(client_name) {
26 return build_claude_code_instructions();
27 }
28 build_full_instructions_for_test(crp_mode, client_name)
29}
30
31pub fn build_instructions_with_client_for_compiler(
36 crp_mode: CrpMode,
37 client_name: &str,
38 unified_tool_mode: bool,
39) -> String {
40 if is_claude_code_client(client_name) {
41 return build_claude_code_instructions();
42 }
43 build_full_instructions_for_compiler(crp_mode, client_name, unified_tool_mode)
44}
45
46fn is_claude_code_client(client_name: &str) -> bool {
47 let lower = client_name.to_lowercase();
48 lower.contains("claude") && !lower.contains("cursor")
49}
50
51fn build_claude_code_instructions() -> String {
52 let shell_hint = build_shell_hint();
53 let instr = format!("\
54ALWAYS use lean-ctx MCP tools instead of native equivalents.
55
56Tool mapping (MANDATORY):
57• Read/cat/head/tail -> ctx_read(path, mode)
58• Shell/bash -> ctx_shell(command)
59• Grep/rg -> ctx_search(pattern, path)
60• ls/find -> ctx_tree(path, depth)
61• Edit/StrReplace -> native (lean-ctx=READ only). If Edit needs Read and Read is unavailable, use ctx_edit.
62• Write, Delete, Glob -> normal. NEVER loop on Edit failures — use ctx_edit.
63
64ctx_read modes: full|map|signatures|diff|task|reference|aggressive|entropy|lines:N-M
65Auto-selects mode. Re-reads ~13 tok. File refs F1,F2.. persist.
66Cache auto-validates via file mtime. Use fresh=true (or start_line / lines:N-M) to force a disk re-read.
67
68Auto: ctx_overview, ctx_preload, ctx_dedup, ctx_compress behind the scenes.
69Multi-agent: ctx_agent(action=handoff|sync|diary).
70ctx_semantic_search for meaning search. ctx_session for memory.
71ctx_knowledge: remember|recall|timeline|rooms|search|wakeup.
72ctx_shell raw=true for uncompressed.
73
74CEP: 1.ACT FIRST 2.DELTA ONLY 3.STRUCTURED(+/-/~) 4.ONE LINE 5.QUALITY
75{shell_hint}\
76Prefer: ctx_read>Read | ctx_shell>Shell | ctx_search>Grep | ctx_tree>ls
77Edit: native Edit/StrReplace preferred, ctx_edit if Edit unavailable.
78Never echo tool output. Never narrate. Show only changed code.
79Full instructions at ~/.claude/CLAUDE.md (imports rules/lean-ctx.md)");
80
81 if shell_hint.is_empty() {
82 debug_assert!(
83 instr.len() <= CLAUDE_CODE_INSTRUCTION_CAP,
84 "Claude Code instructions exceed {CLAUDE_CODE_INSTRUCTION_CAP} chars: {} chars",
85 instr.len()
86 );
87 }
88 instr
89}
90
91fn build_full_instructions(crp_mode: CrpMode, client_name: &str) -> String {
92 let cfg = crate::core::config::Config::load();
93 let minimal = cfg.minimal_overhead_effective_for_client(client_name);
94
95 let profile = crate::core::litm::LitmProfile::from_client_name(client_name);
96 let loaded_session = if minimal {
97 None
98 } else {
99 crate::core::session::SessionState::load_latest()
100 };
101
102 let session_block = match loaded_session {
103 Some(ref session) => {
104 let positioned = crate::core::litm::position_optimize(session);
105 let resume = if session.stats.total_tool_calls > 0 {
106 format!("\n{}", session.build_resume_block())
107 } else {
108 String::new()
109 };
110 format!(
111 "\n\n--- ACTIVE SESSION (LITM P1: begin position, profile: {}) ---\n{}{resume}\n---\n",
112 profile.name, positioned.begin_block
113 )
114 }
115 None => String::new(),
116 };
117
118 let project_root_for_blocks = if minimal {
119 None
120 } else {
121 loaded_session
122 .as_ref()
123 .and_then(|s| s.project_root.clone())
124 .or_else(|| {
125 std::env::current_dir()
126 .ok()
127 .map(|p| p.to_string_lossy().to_string())
128 })
129 };
130
131 let knowledge_block = match &project_root_for_blocks {
132 Some(root) => {
133 let knowledge = crate::core::knowledge::ProjectKnowledge::load(root);
134 match knowledge {
135 Some(k) if !k.facts.is_empty() || !k.patterns.is_empty() => {
136 let aaak = k.format_aaak();
137 if aaak.is_empty() {
138 String::new()
139 } else {
140 format!("\n--- PROJECT MEMORY (AAAK) ---\n{}\n---\n", aaak.trim())
141 }
142 }
143 _ => String::new(),
144 }
145 }
146 None => String::new(),
147 };
148
149 let gotcha_block = match &project_root_for_blocks {
150 Some(root) => {
151 let store = crate::core::gotcha_tracker::GotchaStore::load(root);
152 let files: Vec<String> = loaded_session
153 .as_ref()
154 .map(|s| s.files_touched.iter().map(|ft| ft.path.clone()).collect())
155 .unwrap_or_default();
156 let block = store.format_injection_block(&files);
157 if block.is_empty() {
158 String::new()
159 } else {
160 format!("\n{block}\n")
161 }
162 }
163 None => String::new(),
164 };
165
166 let shell_hint = build_shell_hint();
167
168 let mut base = format!("\
169CRITICAL: ALWAYS use lean-ctx MCP tools instead of native equivalents for token savings.\n\
170\n\
171lean-ctx MCP — MANDATORY tool mapping:\n\
172• Read/cat/head/tail -> ctx_read(path, mode) [NEVER use native Read]\n\
173• Shell/bash -> ctx_shell(command) [NEVER use native Shell]\n\
174• Grep/rg -> ctx_search(pattern, path) [NEVER use native Grep]\n\
175• ls/find -> ctx_tree(path, depth)\n\
176• Edit/StrReplace -> use native (lean-ctx only replaces READ, not WRITE)\n\
177• Write, Delete, Glob -> use normally\n\
178\n\
179COMPATIBILITY: lean-ctx replaces READ operations only. Edit/Write/StrReplace stay native.\n\
180FILE EDITING: Native Edit/StrReplace preferred. If Edit fails, use ctx_edit immediately.\n\
181{shell_hint}\
182\n\
183ctx_read modes: full|map|signatures|diff|task|reference|aggressive|entropy|lines:N-M. Auto-selects. Cached re-reads can be ~13 tok when unchanged. Fn refs F1,F2.. persist.\n\
184Cache auto-validates via file mtime. Use fresh=true (or start_line / lines:N-M) to force a disk re-read.\n\
185\n\
186Auto: ctx_overview, ctx_preload, ctx_dedup, ctx_compress run behind the scenes. Checkpoint every 15 calls.\n\
187Multi-agent: ctx_agent(action=handoff|sync). Diary: ctx_agent(action=diary, category=discovery|decision|blocker|progress|insight).\n\
188ctx_semantic_search for meaning-based search. ctx_session for memory. ctx_knowledge: remember|recall|timeline|rooms|search|wakeup.\n\
189ctx_shell raw=true for uncompressed output.\n\
190\n\
191CEP v1: 1.ACT FIRST 2.DELTA ONLY (Fn refs) 3.STRUCTURED (+/-/~) 4.ONE LINE PER ACTION 5.QUALITY ANCHOR\n\
192\n\
193{decoder_block}\n\
194\n\
195{session_block}\
196{knowledge_block}\
197{gotcha_block}\
198\n\
199--- ORIGIN ---\n\
200{origin}\n\
201\n\
202--- TOOL PREFERENCE (LITM-END) ---\n\
203ctx_read>Read ctx_shell>Shell ctx_search>Grep ctx_tree>ls | Edit/Write/Glob=native",
204 decoder_block = crate::core::protocol::instruction_decoder_block(),
205 origin = crate::core::integrity::origin_line()
206 );
207
208 if should_use_unified(client_name) {
209 base.push_str(
210 "\n\n\
211UNIFIED TOOL MODE (active):\n\
212Additional tools are accessed via ctx() meta-tool: ctx(tool=\"<name>\", ...params).\n\
213See the ctx() tool description for available sub-tools.\n",
214 );
215 }
216
217 let intelligence_block = build_intelligence_block();
218 let terse_block = build_terse_agent_block(&crp_mode);
219
220 let base = base;
221 match crp_mode {
222 CrpMode::Off => format!("{base}\n\n{terse_block}{intelligence_block}"),
223 CrpMode::Compact => {
224 format!(
225 "{base}\n\n\
226CRP MODE: compact\n\
227Omit filler. Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
228Diff lines (+/-) only. TARGET: <=200 tok. Trust tool outputs.\n\n\
229{terse_block}{intelligence_block}"
230 )
231 }
232 CrpMode::Tdd => {
233 format!(
234 "{base}\n\n\
235CRP MODE: tdd\n\
236Max density. Every token carries meaning. Fn refs only, diff lines (+/-) only.\n\
237Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
238+F1:42 param(timeout:Duration) | -F1:10-15 | ~F1:42 old->new\n\
239BUDGET: <=150 tok. ZERO NARRATION. Trust tool outputs.\n\n\
240{terse_block}{intelligence_block}"
241 )
242 }
243 }
244}
245
246fn build_full_instructions_for_test(crp_mode: CrpMode, client_name: &str) -> String {
247 let shell_hint = build_shell_hint();
248 let session_block = String::new();
249 let knowledge_block = String::new();
250 let gotcha_block = String::new();
251
252 let mut base = format!(
253 "\
254CRITICAL: ALWAYS use lean-ctx MCP tools instead of native equivalents for token savings.\n\
255\n\
256lean-ctx MCP — MANDATORY tool mapping:\n\
257• Read/cat/head/tail -> ctx_read(path, mode) [NEVER use native Read]\n\
258• Shell/bash -> ctx_shell(command) [NEVER use native Shell]\n\
259• Grep/rg -> ctx_search(pattern, path) [NEVER use native Grep]\n\
260• ls/find -> ctx_tree(path, depth)\n\
261• Edit/StrReplace -> use native (lean-ctx only replaces READ, not WRITE)\n\
262• Write, Delete, Glob -> use normally\n\
263\n\
264COMPATIBILITY: lean-ctx replaces READ operations only. Edit/Write/StrReplace stay native.\n\
265FILE EDITING: Native Edit/StrReplace preferred. If Edit fails, use ctx_edit immediately.\n\
266{shell_hint}\
267\n\
268ctx_read modes: full|map|signatures|diff|task|reference|aggressive|entropy|lines:N-M. Auto-selects. Cached re-reads can be ~13 tok when unchanged. Fn refs F1,F2.. persist.\n\
269Cache auto-validates via file mtime. Use fresh=true (or start_line / lines:N-M) to force a disk re-read.\n\
270\n\
271Auto: ctx_overview, ctx_preload, ctx_dedup, ctx_compress run behind the scenes. Checkpoint every 15 calls.\n\
272Multi-agent: ctx_agent(action=handoff|sync). Diary: ctx_agent(action=diary, category=discovery|decision|blocker|progress|insight).\n\
273ctx_semantic_search for meaning-based search. ctx_session for memory. ctx_knowledge: remember|recall|timeline|rooms|search|wakeup.\n\
274ctx_shell raw=true for uncompressed output.\n\
275\n\
276CEP v1: 1.ACT FIRST 2.DELTA ONLY (Fn refs) 3.STRUCTURED (+/-/~) 4.ONE LINE PER ACTION 5.QUALITY ANCHOR\n\
277\n\
278{decoder_block}\n\
279\n\
280{session_block}\
281{knowledge_block}\
282{gotcha_block}\
283\n\
284--- ORIGIN ---\n\
285{origin}\n\
286\n\
287--- TOOL PREFERENCE (LITM-END) ---\n\
288ctx_read>Read ctx_shell>Shell ctx_search>Grep ctx_tree>ls | Edit/Write/Glob=native",
289 decoder_block = crate::core::protocol::instruction_decoder_block(),
290 origin = crate::core::integrity::origin_line(),
291 );
292
293 if should_use_unified(client_name) {
294 base.push_str(
295 "\n\n\
296UNIFIED TOOL MODE (active):\n\
297Additional tools are accessed via ctx() meta-tool: ctx(tool=\"<name>\", ...params).\n\
298See the ctx() tool description for available sub-tools.\n",
299 );
300 }
301
302 let intelligence_block = build_intelligence_block();
303 let terse_block = build_terse_agent_block(&crp_mode);
304
305 match crp_mode {
306 CrpMode::Off => format!("{base}\n\n{terse_block}{intelligence_block}"),
307 CrpMode::Compact => {
308 format!(
309 "{base}\n\n\
310CRP MODE: compact\n\
311Omit filler. Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
312Diff lines (+/-) only. TARGET: <=200 tok. Trust tool outputs.\n\n\
313{terse_block}{intelligence_block}"
314 )
315 }
316 CrpMode::Tdd => {
317 format!(
318 "{base}\n\n\
319CRP MODE: tdd\n\
320Max density. Every token carries meaning. Fn refs only, diff lines (+/-) only.\n\
321Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
322+F1:42 param(timeout:Duration) | -F1:10-15 | ~F1:42 old->new\n\
323BUDGET: <=150 tok. ZERO NARRATION. Trust tool outputs.\n\n\
324{terse_block}{intelligence_block}"
325 )
326 }
327 }
328}
329
330fn build_full_instructions_for_compiler(
331 crp_mode: CrpMode,
332 client_name: &str,
333 unified_tool_mode: bool,
334) -> String {
335 let shell_hint = build_shell_hint();
336 let session_block = String::new();
337 let knowledge_block = String::new();
338 let gotcha_block = String::new();
339
340 let mut base = format!(
341 "\
342CRITICAL: ALWAYS use lean-ctx MCP tools instead of native equivalents for token savings.\n\
343\n\
344lean-ctx MCP — MANDATORY tool mapping:\n\
345• Read/cat/head/tail -> ctx_read(path, mode) [NEVER use native Read]\n\
346• Shell/bash -> ctx_shell(command) [NEVER use native Shell]\n\
347• Grep/rg -> ctx_search(pattern, path) [NEVER use native Grep]\n\
348• ls/find -> ctx_tree(path, depth)\n\
349• Edit/StrReplace -> use native (lean-ctx only replaces READ, not WRITE)\n\
350• Write, Delete, Glob -> use normally\n\
351\n\
352COMPATIBILITY: lean-ctx replaces READ operations only. Edit/Write/StrReplace stay native.\n\
353FILE EDITING: Native Edit/StrReplace preferred. If Edit fails, use ctx_edit immediately.\n\
354{shell_hint}\
355\n\
356ctx_read modes: full|map|signatures|diff|task|reference|aggressive|entropy|lines:N-M. Auto-selects. Cached re-reads can be ~13 tok when unchanged. Fn refs F1,F2.. persist.\n\
357Cache auto-validates via file mtime. Use fresh=true (or start_line / lines:N-M) to force a disk re-read.\n\
358\n\
359Auto: ctx_overview, ctx_preload, ctx_dedup, ctx_compress run behind the scenes. Checkpoint every 15 calls.\n\
360Multi-agent: ctx_agent(action=handoff|sync). Diary: ctx_agent(action=diary, category=discovery|decision|blocker|progress|insight).\n\
361ctx_semantic_search for meaning-based search. ctx_session for memory. ctx_knowledge: remember|recall|timeline|rooms|search|wakeup.\n\
362ctx_shell raw=true for uncompressed output.\n\
363\n\
364CEP v1: 1.ACT FIRST 2.DELTA ONLY (Fn refs) 3.STRUCTURED (+/-/~) 4.ONE LINE PER ACTION 5.QUALITY ANCHOR\n\
365\n\
366{decoder_block}\n\
367\n\
368{session_block}\
369{knowledge_block}\
370{gotcha_block}\
371\n\
372--- ORIGIN ---\n\
373{origin}\n\
374\n\
375--- TOOL PREFERENCE (LITM-END) ---\n\
376ctx_read>Read ctx_shell>Shell ctx_search>Grep ctx_tree>ls | Edit/Write/Glob=native",
377 decoder_block = crate::core::protocol::instruction_decoder_block(),
378 origin = crate::core::integrity::origin_line(),
379 );
380
381 if unified_tool_mode {
382 base.push_str(
383 "\n\n\
384UNIFIED TOOL MODE (active):\n\
385Additional tools are accessed via ctx() meta-tool: ctx(tool=\"<name>\", ...params).\n\
386See the ctx() tool description for available sub-tools.\n",
387 );
388 }
389
390 let _ = client_name; let intelligence_block = build_intelligence_block();
392
393 match crp_mode {
394 CrpMode::Off => format!("{base}\n\n{intelligence_block}"),
395 CrpMode::Compact => {
396 format!(
397 "{base}\n\n\
398CRP MODE: compact\n\
399Omit filler. Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
400Diff lines (+/-) only. TARGET: <=200 tok. Trust tool outputs.\n\n\
401{intelligence_block}"
402 )
403 }
404 CrpMode::Tdd => {
405 format!(
406 "{base}\n\n\
407CRP MODE: tdd\n\
408Max density. Every token carries meaning. Fn refs only, diff lines (+/-) only.\n\
409Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
410+F1:42 param(timeout:Duration) | -F1:10-15 | ~F1:42 old->new\n\
411BUDGET: <=150 tok. ZERO NARRATION. Trust tool outputs.\n\n\
412{intelligence_block}"
413 )
414 }
415 }
416}
417
418pub fn claude_code_instructions() -> String {
419 build_claude_code_instructions()
420}
421
422pub fn build_cli_redirect_instructions() -> String {
423 let base = "\
424PREFER lean-ctx CLI commands over MCP tools (no schema overhead):\n\
425\n\
426Via Shell/Bash:\n\
427• lean-ctx read <path> -> replaces ctx_read\n\
428• lean-ctx read <path> --mode map -> replaces ctx_read(mode=\"map\")\n\
429• lean-ctx shell \"<cmd>\" -> replaces ctx_shell\n\
430• lean-ctx search <pattern> <path> -> replaces ctx_search\n\
431• lean-ctx tree <path> -> replaces ctx_tree\n\
432\n\
433Edit files: native Edit/StrReplace. Write, Delete, Glob → use normally.\n\
434Read modes: auto|full|map|signatures|diff|aggressive|entropy|task|reference|lines:N-M";
435
436 let config = crate::core::config::Config::load();
437 let level = crate::core::config::CompressionLevel::effective(&config);
438 let terse_block = crate::core::terse::agent_prompts::build_prompt_block(&level);
439
440 if terse_block.is_empty() {
441 base.to_string()
442 } else {
443 format!("{base}\n\n{terse_block}")
444 }
445}
446
447pub fn build_hybrid_instructions() -> String {
448 let base = "\
449Hybrid mode: MCP for reads (cache), CLI for everything else (no schema overhead):\n\
450\n\
451MCP (keep using): ctx_read(path, mode) — in-process cache, re-reads ~13 tokens.\n\
452\n\
453Via Shell/Bash:\n\
454• lean-ctx shell \"<cmd>\" -> replaces ctx_shell\n\
455• lean-ctx search <pattern> <path> -> replaces ctx_search\n\
456• lean-ctx tree <path> -> replaces ctx_tree\n\
457\n\
458Edit files: native Edit/StrReplace. Write, Delete, Glob → use normally.";
459
460 let config = crate::core::config::Config::load();
461 let level = crate::core::config::CompressionLevel::effective(&config);
462 let terse_block = crate::core::terse::agent_prompts::build_prompt_block(&level);
463
464 if terse_block.is_empty() {
465 base.to_string()
466 } else {
467 format!("{base}\n\n{terse_block}")
468 }
469}
470
471pub fn full_instructions_for_rules_file(crp_mode: CrpMode) -> String {
472 build_full_instructions(crp_mode, "")
473}
474
475fn build_terse_agent_block(crp_mode: &CrpMode) -> String {
476 use crate::core::config::{CompressionLevel, Config, TerseAgent};
477 let cfg = Config::load();
478
479 let compression = CompressionLevel::effective(&cfg);
480 if compression.is_active() {
481 return crate::core::terse::agent_prompts::build_prompt_block(&compression);
482 }
483
484 let level = TerseAgent::effective(&cfg.terse_agent);
485 if !level.is_active() {
486 return String::new();
487 }
488 if matches!(crp_mode, CrpMode::Tdd) && !matches!(level, TerseAgent::Ultra) {
490 return String::new();
491 }
492 let text = match level {
493 TerseAgent::Off => return String::new(),
494 TerseAgent::Lite => {
495 "\
496OUTPUT STYLE: Prefer concise responses. Skip narration, explain only when asked.\n\
497Use bullet points over paragraphs. Code > words. Diff > full file."
498 }
499 TerseAgent::Full => {
500 "\
501OUTPUT STYLE: Maximum density. Every token carries meaning.\n\
502Code changes: diff only (+/-), no full blocks. Explanations: 1 sentence max unless asked.\n\
503Lists: no filler words. Never repeat what the user said. Never explain what you're about to do."
504 }
505 TerseAgent::Ultra => {
506 "\
507OUTPUT STYLE: Ultra-terse. Expert pair programmer mode.\n\
508Skip: greetings, transitions, summaries, \"I'll\", \"Let me\", \"Here's\".\n\
509Max 2 sentences per explanation. Code speaks. Act, don't narrate. When uncertain: ask 1 question."
510 }
511 };
512 format!("{text}\n\n")
513}
514
515fn build_intelligence_block() -> String {
516 "\
517OUTPUT EFFICIENCY:\n\
518• Never echo tool output code. Never add narration comments. Show only changed code.\n\
519• [TASK:type] and SCOPE hints included. Architecture=thorough, generate=code."
520 .to_string()
521}
522
523fn build_shell_hint() -> String {
524 if !cfg!(windows) {
525 return String::new();
526 }
527 let name = crate::shell::shell_name();
528 let is_posix = matches!(name.as_str(), "bash" | "sh" | "zsh" | "fish");
529 if is_posix {
530 format!(
531 "\nSHELL: {name} (POSIX). Use POSIX commands (cat, head, grep, find, ls). \
532 Do NOT use PowerShell cmdlets (Get-Content, Select-Object, Get-ChildItem).\n"
533 )
534 } else if name.contains("powershell") || name.contains("pwsh") {
535 format!("\nSHELL: {name}. Use PowerShell cmdlets.\n")
536 } else {
537 format!("\nSHELL: {name}.\n")
538 }
539}
540
541fn should_use_unified(client_name: &str) -> bool {
542 if std::env::var("LEAN_CTX_FULL_TOOLS").is_ok() {
543 return false;
544 }
545 if std::env::var("LEAN_CTX_UNIFIED").is_ok() {
546 return true;
547 }
548 let _ = client_name;
549 false
550}