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