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/rules/lean-ctx.md` instead.
5const 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
18fn is_claude_code_client(client_name: &str) -> bool {
19    let lower = client_name.to_lowercase();
20    lower.contains("claude") && !lower.contains("cursor")
21}
22
23fn build_claude_code_instructions() -> String {
24    let shell_hint = build_shell_hint();
25    let instr = format!("\
26ALWAYS use lean-ctx MCP tools instead of native equivalents.
27
28Tool mapping (MANDATORY):
29• Read/cat/head/tail -> ctx_read(path, mode)
30• Shell/bash -> ctx_shell(command)
31• Grep/rg -> ctx_search(pattern, path)
32• ls/find -> ctx_tree(path, depth)
33• Edit/StrReplace -> native (lean-ctx=READ only). If Edit needs Read and Read is unavailable, use ctx_edit.
34• Write, Delete, Glob -> normal. NEVER loop on Edit failures — use ctx_edit.
35
36ctx_read modes: full|map|signatures|diff|task|reference|aggressive|entropy|lines:N-M
37Auto-selects mode. Re-reads ~13 tok. File refs F1,F2.. persist.
38Cached? fresh=true or lines:N-M.
39
40Auto: ctx_overview, ctx_preload, ctx_dedup, ctx_compress behind the scenes.
41Multi-agent: ctx_agent(action=handoff|sync|diary).
42ctx_semantic_search for meaning search. ctx_session for memory.
43ctx_knowledge: remember|recall|timeline|rooms|search|wakeup.
44ctx_shell raw=true for uncompressed.
45
46CEP: 1.ACT FIRST 2.DELTA ONLY 3.STRUCTURED(+/-/~) 4.ONE LINE 5.QUALITY
47{shell_hint}\
48Prefer: ctx_read>Read | ctx_shell>Shell | ctx_search>Grep | ctx_tree>ls
49Edit: native Edit/StrReplace preferred, ctx_edit if Edit unavailable.
50Never echo tool output. Never narrate. Show only changed code.
51Full instructions at ~/.claude/CLAUDE.md (imports rules/lean-ctx.md)");
52
53    if shell_hint.is_empty() {
54        debug_assert!(
55            instr.len() <= CLAUDE_CODE_INSTRUCTION_CAP,
56            "Claude Code instructions exceed {CLAUDE_CODE_INSTRUCTION_CAP} chars: {} chars",
57            instr.len()
58        );
59    }
60    instr
61}
62
63fn build_full_instructions(crp_mode: CrpMode, client_name: &str) -> String {
64    let cfg = crate::core::config::Config::load();
65    let minimal = cfg.minimal_overhead_effective();
66
67    let profile = crate::core::litm::LitmProfile::from_client_name(client_name);
68    let loaded_session = if minimal {
69        None
70    } else {
71        crate::core::session::SessionState::load_latest()
72    };
73
74    let session_block = match loaded_session {
75        Some(ref session) => {
76            let positioned = crate::core::litm::position_optimize(session);
77            let resume = if session.stats.total_tool_calls > 0 {
78                format!("\n{}", session.build_resume_block())
79            } else {
80                String::new()
81            };
82            format!(
83                "\n\n--- ACTIVE SESSION (LITM P1: begin position, profile: {}) ---\n{}{resume}\n---\n",
84                profile.name, positioned.begin_block
85            )
86        }
87        None => String::new(),
88    };
89
90    let project_root_for_blocks = if minimal {
91        None
92    } else {
93        loaded_session
94            .as_ref()
95            .and_then(|s| s.project_root.clone())
96            .or_else(|| {
97                std::env::current_dir()
98                    .ok()
99                    .map(|p| p.to_string_lossy().to_string())
100            })
101    };
102
103    let knowledge_block = match &project_root_for_blocks {
104        Some(root) => {
105            let knowledge = crate::core::knowledge::ProjectKnowledge::load(root);
106            match knowledge {
107                Some(k) if !k.facts.is_empty() || !k.patterns.is_empty() => {
108                    let aaak = k.format_aaak();
109                    if aaak.is_empty() {
110                        String::new()
111                    } else {
112                        format!("\n--- PROJECT MEMORY (AAAK) ---\n{}\n---\n", aaak.trim())
113                    }
114                }
115                _ => String::new(),
116            }
117        }
118        None => String::new(),
119    };
120
121    let gotcha_block = match &project_root_for_blocks {
122        Some(root) => {
123            let store = crate::core::gotcha_tracker::GotchaStore::load(root);
124            let files: Vec<String> = loaded_session
125                .as_ref()
126                .map(|s| s.files_touched.iter().map(|ft| ft.path.clone()).collect())
127                .unwrap_or_default();
128            let block = store.format_injection_block(&files);
129            if block.is_empty() {
130                String::new()
131            } else {
132                format!("\n{block}\n")
133            }
134        }
135        None => String::new(),
136    };
137
138    let shell_hint = build_shell_hint();
139
140    let mut base = format!("\
141CRITICAL: ALWAYS use lean-ctx MCP tools instead of native equivalents for token savings.\n\
142\n\
143lean-ctx MCP — MANDATORY tool mapping:\n\
144• Read/cat/head/tail -> ctx_read(path, mode)  [NEVER use native Read]\n\
145• Shell/bash -> ctx_shell(command)  [NEVER use native Shell]\n\
146• Grep/rg -> ctx_search(pattern, path)  [NEVER use native Grep]\n\
147• ls/find -> ctx_tree(path, depth)\n\
148• Edit/StrReplace -> use native (lean-ctx only replaces READ, not WRITE)\n\
149• Write, Delete, Glob -> use normally\n\
150\n\
151COMPATIBILITY: lean-ctx replaces READ operations only. Edit/Write/StrReplace stay native.\n\
152FILE EDITING: Native Edit/StrReplace preferred. If Edit fails, use ctx_edit immediately.\n\
153{shell_hint}\
154\n\
155ctx_read modes: full|map|signatures|diff|task|reference|aggressive|entropy|lines:N-M. Auto-selects. Re-reads ~13 tok. Fn refs F1,F2.. persist.\n\
156Cached? Use fresh=true, start_line=N, or lines:N-M.\n\
157\n\
158Auto: ctx_overview, ctx_preload, ctx_dedup, ctx_compress run behind the scenes. Checkpoint every 15 calls.\n\
159Multi-agent: ctx_agent(action=handoff|sync). Diary: ctx_agent(action=diary, category=discovery|decision|blocker|progress|insight).\n\
160ctx_semantic_search for meaning-based search. ctx_session for memory. ctx_knowledge: remember|recall|timeline|rooms|search|wakeup.\n\
161ctx_shell raw=true for uncompressed output.\n\
162\n\
163CEP v1: 1.ACT FIRST 2.DELTA ONLY (Fn refs) 3.STRUCTURED (+/-/~) 4.ONE LINE PER ACTION 5.QUALITY ANCHOR\n\
164\n\
165{decoder_block}\n\
166\n\
167{session_block}\
168{knowledge_block}\
169{gotcha_block}\
170\n\
171--- ORIGIN ---\n\
172{origin}\n\
173\n\
174--- TOOL PREFERENCE (LITM-END) ---\n\
175Prefer: ctx_read over Read | ctx_shell over Shell | ctx_search over Grep | ctx_tree over ls\n\
176Edit files: native Edit/StrReplace if available, ctx_edit if Edit requires unavailable Read.\n\
177Write, Delete, Glob -> use normally. NEVER loop on Edit failures — use ctx_edit.",
178        decoder_block = crate::core::protocol::instruction_decoder_block(),
179        origin = crate::core::integrity::origin_line()
180    );
181
182    if should_use_unified(client_name) {
183        base.push_str(
184            "\n\n\
185UNIFIED TOOL MODE (active):\n\
186Additional tools are accessed via ctx() meta-tool: ctx(tool=\"<name>\", ...params).\n\
187See the ctx() tool description for available sub-tools.\n",
188        );
189    }
190
191    let intelligence_block = build_intelligence_block();
192    let terse_block = build_terse_agent_block(&crp_mode);
193
194    let base = base;
195    match crp_mode {
196        CrpMode::Off => format!("{base}\n\n{terse_block}{intelligence_block}"),
197        CrpMode::Compact => {
198            format!(
199                "{base}\n\n\
200CRP MODE: compact\n\
201Omit filler. Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
202Diff lines (+/-) only. TARGET: <=200 tok. Trust tool outputs.\n\n\
203{terse_block}{intelligence_block}"
204            )
205        }
206        CrpMode::Tdd => {
207            format!(
208                "{base}\n\n\
209CRP MODE: tdd\n\
210Max density. Every token carries meaning. Fn refs only, diff lines (+/-) only.\n\
211Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
212+F1:42 param(timeout:Duration) | -F1:10-15 | ~F1:42 old->new\n\
213BUDGET: <=150 tok. ZERO NARRATION. Trust tool outputs.\n\n\
214{terse_block}{intelligence_block}"
215            )
216        }
217    }
218}
219
220pub fn claude_code_instructions() -> String {
221    build_claude_code_instructions()
222}
223
224pub fn full_instructions_for_rules_file(crp_mode: CrpMode) -> String {
225    build_full_instructions(crp_mode, "")
226}
227
228fn build_terse_agent_block(crp_mode: &CrpMode) -> String {
229    use crate::core::config::{Config, TerseAgent};
230    let cfg = Config::load();
231    let level = TerseAgent::effective(&cfg.terse_agent);
232    if !level.is_active() {
233        return String::new();
234    }
235    // CRP Tdd already enforces extreme density — only Ultra adds value on top
236    if matches!(crp_mode, CrpMode::Tdd) && !matches!(level, TerseAgent::Ultra) {
237        return String::new();
238    }
239    let text = match level {
240        TerseAgent::Off => return String::new(),
241        TerseAgent::Lite => {
242            "\
243OUTPUT STYLE: Prefer concise responses. Skip narration, explain only when asked.\n\
244Use bullet points over paragraphs. Code > words. Diff > full file."
245        }
246        TerseAgent::Full => {
247            "\
248OUTPUT STYLE: Maximum density. Every token carries meaning.\n\
249Code changes: diff only (+/-), no full blocks. Explanations: 1 sentence max unless asked.\n\
250Lists: no filler words. Never repeat what the user said. Never explain what you're about to do."
251        }
252        TerseAgent::Ultra => {
253            "\
254OUTPUT STYLE: Ultra-terse. Expert pair programmer mode.\n\
255Skip: greetings, transitions, summaries, \"I'll\", \"Let me\", \"Here's\".\n\
256Max 2 sentences per explanation. Code speaks. Act, don't narrate. When uncertain: ask 1 question."
257        }
258    };
259    format!("{text}\n\n")
260}
261
262fn build_intelligence_block() -> String {
263    "\
264OUTPUT EFFICIENCY:\n\
265• Never echo tool output code. Never add narration comments. Show only changed code.\n\
266• [TASK:type] and SCOPE hints included. Architecture=thorough, generate=code."
267        .to_string()
268}
269
270fn build_shell_hint() -> String {
271    if !cfg!(windows) {
272        return String::new();
273    }
274    let name = crate::shell::shell_name();
275    let is_posix = matches!(name.as_str(), "bash" | "sh" | "zsh" | "fish");
276    if is_posix {
277        format!(
278            "\nSHELL: {name} (POSIX). Use POSIX commands (cat, head, grep, find, ls). \
279             Do NOT use PowerShell cmdlets (Get-Content, Select-Object, Get-ChildItem).\n"
280        )
281    } else if name.contains("powershell") || name.contains("pwsh") {
282        format!("\nSHELL: {name}. Use PowerShell cmdlets.\n")
283    } else {
284        format!("\nSHELL: {name}.\n")
285    }
286}
287
288fn should_use_unified(client_name: &str) -> bool {
289    if std::env::var("LEAN_CTX_FULL_TOOLS").is_ok() {
290        return false;
291    }
292    if std::env::var("LEAN_CTX_UNIFIED").is_ok() {
293        return true;
294    }
295    let _ = client_name;
296    false
297}