Skip to main content

lean_ctx/
instructions.rs

1use crate::tools::CrpMode;
2
3/// Claude Code truncates MCP server instructions at 2048 characters.
4/// Full instructions are installed as `$CLAUDE_CONFIG_DIR/rules/lean-ctx.md`
5/// (defaulting to `~/.claude/rules/lean-ctx.md`) instead.
6const CLAUDE_CODE_INSTRUCTION_CAP: usize = 2048;
7
8/// Universal instruction cap for all MCP clients.
9/// Prioritizes content blocks to fit within this limit.
10const 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    // Avoid loading dynamic on-disk session/knowledge/gotcha blocks in tests, which can
25    // vary across machines and between concurrent test runs.
26    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
36/// Deterministic instruction builder for the Instruction Compiler.
37///
38/// MUST NOT depend on process-global env toggles or on-disk mutable config, because the compiler
39/// output is intended to be stable and diffable across runs and in CI.
40pub 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; // keep signature aligned with other builders
447    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}