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