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