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 live in the `<!-- lean-ctx -->` block in `~/.claude/CLAUDE.md`
5/// plus the on-demand skill (`~/.claude/skills/lean-ctx/SKILL.md`); the legacy
6/// always-loaded `~/.claude/rules/lean-ctx.md` was retired in 3.8 (GL #555).
7/// Session state is dynamically appended to the MCP instructions for continuity.
8///
9/// Universal instruction cap for all MCP clients (in tokens, not bytes).
10/// Enforced via `count_tokens` so truncation is accurate regardless of
11/// character mix (ASCII, CJK, emoji).
12const INSTRUCTION_CAP_TOKENS: usize = 1200;
13
14pub fn build_instructions(crp_mode: CrpMode) -> String {
15    build_instructions_with_client(crp_mode, "")
16}
17
18pub fn build_instructions_with_client(crp_mode: CrpMode, client_name: &str) -> String {
19    if is_claude_code_client(client_name) {
20        return build_claude_code_instructions();
21    }
22    build_full_instructions(crp_mode, client_name)
23}
24
25pub fn build_instructions_for_test(crp_mode: CrpMode) -> String {
26    // Avoid loading dynamic on-disk session/knowledge/gotcha blocks in tests, which can
27    // vary across machines and between concurrent test runs.
28    build_full_instructions_for_test(crp_mode, "")
29}
30
31pub fn build_instructions_with_client_for_test(crp_mode: CrpMode, client_name: &str) -> String {
32    if is_claude_code_client(client_name) {
33        return build_claude_code_instructions();
34    }
35    build_full_instructions_for_test(crp_mode, client_name)
36}
37
38/// Deterministic instruction builder for the Instruction Compiler.
39///
40/// MUST NOT depend on process-global env toggles or on-disk mutable config, because the compiler
41/// output is intended to be stable and diffable across runs and in CI.
42pub fn build_instructions_with_client_for_compiler(
43    crp_mode: CrpMode,
44    client_name: &str,
45    unified_tool_mode: bool,
46) -> String {
47    if is_claude_code_client(client_name) {
48        return build_claude_code_instructions();
49    }
50    build_full_instructions_for_compiler(crp_mode, client_name, unified_tool_mode)
51}
52
53fn is_claude_code_client(client_name: &str) -> bool {
54    let lower = client_name.to_lowercase();
55    lower.contains("claude") && !lower.contains("cursor")
56}
57
58/// LITM calibration manifest rotation (#539).
59///
60/// Settles the previous manifest — every entry the agent never re-recalled is
61/// a placement *hit* (misses were already recorded by the recall hook) — then
62/// stores the manifest for the injection built from `session` right now:
63/// task + decisions go to the begin block, findings + next steps to the end.
64fn rotate_wakeup_manifest(session: &crate::core::session::SessionState, profile_name: &str) {
65    use crate::core::litm_calibration::{record_outcome, Position};
66    use crate::core::session::ManifestEntry;
67
68    let mut updated = session.clone();
69
70    for entry in &updated.wakeup_manifest {
71        if !entry.missed {
72            if let Some(pos) = Position::parse(&entry.position) {
73                record_outcome(&entry.profile, pos, true);
74            }
75        }
76    }
77
78    let mut manifest: Vec<ManifestEntry> = Vec::new();
79    let mut push = |key: &str, position: &str| {
80        let key = key.trim();
81        if !key.is_empty() {
82            manifest.push(ManifestEntry {
83                key: key.chars().take(80).collect(),
84                position: position.to_string(),
85                profile: profile_name.to_string(),
86                missed: false,
87            });
88        }
89    };
90
91    if let Some(ref task) = updated.task {
92        push(&task.description, "begin");
93    }
94    for d in updated.decisions.iter().rev().take(5) {
95        push(&d.summary, "begin");
96    }
97    for f in updated.findings.iter().rev().take(8) {
98        push(&f.summary, "end");
99    }
100    for n in updated.next_steps.iter().take(3) {
101        push(n, "end");
102    }
103
104    updated.wakeup_manifest = manifest;
105    let _ = updated.save();
106}
107
108pub fn claude_config_dir_display() -> String {
109    match std::env::var("CLAUDE_CONFIG_DIR") {
110        Ok(dir) if !dir.trim().is_empty() => {
111            let dir = dir.trim().to_string();
112            if dir.starts_with('~') {
113                dir
114            } else if let Some(home) = dirs::home_dir() {
115                let home_str = home.to_string_lossy();
116                if let Some(rest) = dir.strip_prefix(home_str.as_ref()) {
117                    format!("~{rest}")
118                } else {
119                    dir
120                }
121            } else {
122                dir
123            }
124        }
125        _ => "~/.claude".to_string(),
126    }
127}
128
129fn build_claude_code_instructions() -> String {
130    let shell_hint = build_shell_hint();
131    let config_dir = claude_config_dir_display();
132
133    // Load session state for continuity (compact version for Claude Code's char limit)
134    let session_block = match crate::core::session::SessionState::load_latest() {
135        Some(session) => {
136            let mut parts = Vec::new();
137            if let Some(ref task) = session.task {
138                let pct = task
139                    .progress_pct
140                    .map_or(String::new(), |p| format!(" [{p}%]"));
141                parts.push(format!("Task: {}{pct}", task.description));
142            }
143            if !session.decisions.is_empty() {
144                let items: Vec<&str> = session
145                    .decisions
146                    .iter()
147                    .rev()
148                    .take(3)
149                    .map(|d| d.summary.as_str())
150                    .collect();
151                parts.push(format!("Decisions: {}", items.join("; ")));
152            }
153            if !session.files_touched.is_empty() {
154                let modified: Vec<&str> = session
155                    .files_touched
156                    .iter()
157                    .filter(|f| f.modified)
158                    .take(5)
159                    .map(|f| f.path.as_str())
160                    .collect();
161                if !modified.is_empty() {
162                    parts.push(format!("Modified: {}", modified.join(", ")));
163                }
164            }
165            if !session.findings.is_empty() {
166                let recent: Vec<&str> = session
167                    .findings
168                    .iter()
169                    .rev()
170                    .take(3)
171                    .map(|f| f.summary.as_str())
172                    .collect();
173                parts.push(format!("Recent: {}", recent.join("; ")));
174            }
175            if parts.is_empty() {
176                String::new()
177            } else {
178                format!("\n\n--- SESSION ---\n{}\n---", parts.join("\n"))
179            }
180        }
181        None => String::new(),
182    };
183
184    let cfg = crate::core::config::Config::load();
185    let shadow_preamble = if cfg.shadow_mode {
186        "SHADOW MODE ACTIVE: ALL reads/searches/shell MUST use ctx_* tools. Native equivalents are intercepted.\n\n"
187    } else {
188        ""
189    };
190
191    let instr = format!("\
192{shadow_preamble}\
193ALWAYS use lean-ctx MCP tools instead of native equivalents.
194
195Tool mapping (MANDATORY):
196• Read/cat/head/tail -> ctx_read(path, mode)
197• Shell/bash -> ctx_shell(command)
198• Grep/rg -> ctx_search(pattern, path)
199• ls/find -> ctx_tree(path, depth)
200• Edit/StrReplace -> native (lean-ctx=READ only). If Edit needs Read and Read is unavailable, use ctx_edit.
201• Write, Delete, Glob -> normal. NEVER loop on Edit failures — use ctx_edit.
202
203ctx_read modes: full|map|signatures|diff|task|reference|aggressive|entropy|lines:N-M
204Auto-selects mode. Re-reads ~13 tok. File refs F1,F2.. persist.
205
206Workflow: Orient(ctx_overview) → Locate(ctx_search) → Read(ctx_read) → Edit(ctx_edit/native) → Verify(ctx_read diff + ctx_shell test) → Record(ctx_knowledge).
207
208CEP: 1.ACT FIRST 2.DELTA ONLY 3.STRUCTURED(+/-/~) 4.ONE LINE 5.QUALITY
209{shell_hint}\
210Prefer: ctx_read>Read | ctx_shell>Shell | ctx_search>Grep | ctx_tree>ls
211Edit: native Edit/StrReplace preferred, ctx_edit if Edit unavailable.
212Never echo tool output. Never narrate. Show only changed code.
213Full instructions at {config_dir}/CLAUDE.md (imports rules/lean-ctx.md){session_block}");
214
215    instr
216}
217
218fn build_full_instructions(crp_mode: CrpMode, client_name: &str) -> String {
219    let cfg = crate::core::config::Config::load();
220    let minimal = cfg.minimal_overhead_effective_for_client(client_name);
221
222    let profile = crate::core::litm::LitmProfile::from_client_name(client_name);
223    let loaded_session = if minimal {
224        None
225    } else {
226        crate::core::session::SessionState::load_latest()
227    };
228
229    let (session_block, litm_end_block) = match loaded_session {
230        Some(ref session) => {
231            // LITM calibration (#539): rotate the placement manifest — every
232            // entry the agent never re-recalled counts as a placement hit —
233            // then rebuild it for this injection and apply the learned share.
234            rotate_wakeup_manifest(session, profile.name);
235            let share = crate::core::litm_calibration::begin_share(profile.name);
236            let positioned = crate::core::litm::position_optimize_with_share(session, share);
237            let begin = format!(
238                "\n\n--- ACTIVE SESSION (LITM P1: begin position, profile: {}) ---\n{}\n---\n",
239                profile.name, positioned.begin_block
240            );
241            let end = if positioned.end_block.is_empty() {
242                String::new()
243            } else {
244                format!(
245                    "\n--- SESSION RESUME (post-compaction) ---\n{}\n---\n",
246                    positioned.end_block
247                )
248            };
249            (begin, end)
250        }
251        None => (String::new(), String::new()),
252    };
253
254    let project_root_for_blocks = if minimal {
255        None
256    } else {
257        loaded_session
258            .as_ref()
259            .and_then(|s| s.project_root.clone())
260            .or_else(|| {
261                std::env::current_dir()
262                    .ok()
263                    .map(|p| p.to_string_lossy().to_string())
264            })
265    };
266
267    let knowledge_block = match &project_root_for_blocks {
268        Some(root) => {
269            let knowledge = crate::core::knowledge::ProjectKnowledge::load(root);
270            match knowledge {
271                Some(k) if !k.facts.is_empty() || !k.patterns.is_empty() => {
272                    let aaak = k.format_aaak();
273                    if aaak.is_empty() {
274                        String::new()
275                    } else {
276                        format!("\n--- PROJECT MEMORY (AAAK) ---\n{}\n---\n", aaak.trim())
277                    }
278                }
279                _ => String::new(),
280            }
281        }
282        None => String::new(),
283    };
284
285    let gotcha_block = match &project_root_for_blocks {
286        Some(root) => {
287            let store = crate::core::gotcha_tracker::GotchaStore::load(root);
288            let files: Vec<String> = loaded_session
289                .as_ref()
290                .map(|s| s.files_touched.iter().map(|ft| ft.path.clone()).collect())
291                .unwrap_or_default();
292            let block = store.format_injection_block(&files);
293            if block.is_empty() {
294                String::new()
295            } else {
296                format!("\n{block}\n")
297            }
298        }
299        None => String::new(),
300    };
301
302    let shell_hint = build_shell_hint();
303
304    use crate::core::rules_canonical as rc;
305    let tool_bullets = rc::tool_mapping_bullets(rc::Mode::Mcp);
306    let compat = rc::compatibility_block();
307    let read_modes = rc::ctx_read_modes_block();
308    let auto_block = rc::automation_block();
309    let cep = rc::cep_block();
310    let litm_pref = rc::litm_end_block(rc::Mode::Mcp);
311
312    let shadow_preamble = if cfg.shadow_mode {
313        "SHADOW MODE ACTIVE: ALL file reads, searches, and shell commands MUST go through ctx_* tools. \
314         Native Read/Grep/Shell are intercepted and redirected — using ctx_* directly is faster and more reliable.\n\n"
315    } else {
316        ""
317    };
318
319    let mut base = format!(
320        "\
321{shadow_preamble}\
322CRITICAL: ALWAYS use lean-ctx MCP tools instead of native equivalents for token savings.\n\
323\n\
324{tool_bullets}\n\
325\n\
326{compat}\n\
327{shell_hint}\
328\n\
329{read_modes}\n\
330\n\
331{auto_block}\n\
332\n\
333{cep}\n\
334\n\
335{decoder_block}\n\
336\n\
337{session_block}\
338{knowledge_block}\
339{gotcha_block}\
340\n\
341--- ORIGIN ---\n\
342{origin}\n\
343\n\
344{litm_pref}\
345{litm_end_block}",
346        decoder_block = crate::core::protocol::instruction_decoder_block(),
347        origin = crate::core::integrity::origin_line(),
348        litm_end_block = &litm_end_block
349    );
350
351    if should_use_unified(client_name) {
352        base.push_str("\n\n");
353        base.push_str(rc::unified_tool_mode_block());
354        base.push('\n');
355    }
356
357    let intelligence_block = build_intelligence_block();
358    let terse_block = build_terse_agent_block_for_client(&crp_mode, client_name);
359
360    // The guidance suffix (CRP-mode rules + compression/output-style + the
361    // intelligence block) is the operational contract for the agent and must
362    // survive the token cap. The variable session/knowledge/gotcha blocks live
363    // inside `base` and are the right thing to shed under pressure (H3). So we
364    // protect the suffix and truncate only `base` to fit the budget.
365    let guidance_suffix = match crp_mode {
366        CrpMode::Off => format!("{terse_block}{intelligence_block}"),
367        CrpMode::Compact => format!(
368            "CRP MODE: compact\n\
369Omit filler. Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
370Diff lines (+/-) only. TARGET: <=200 tok. Trust tool outputs.\n\n\
371{terse_block}{intelligence_block}"
372        ),
373        CrpMode::Tdd => format!(
374            "CRP MODE: tdd\n\
375Max density. Every token carries meaning. Fn refs only, diff lines (+/-) only.\n\
376Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
377+F1:42 param(timeout:Duration) | -F1:10-15 | ~F1:42 old->new\n\
378BUDGET: <=150 tok. ZERO NARRATION. Trust tool outputs.\n\n\
379{terse_block}{intelligence_block}"
380        ),
381    };
382
383    assemble_within_cap(&base, &guidance_suffix, INSTRUCTION_CAP_TOKENS)
384}
385
386/// Join `base` and a protected `suffix` so the result fits `cap_tokens`,
387/// truncating only `base` if needed. The suffix is the agent's operational
388/// contract (compression/output-style guidance) and is preserved verbatim as
389/// long as it fits on its own; otherwise we fall back to capping the whole.
390fn assemble_within_cap(base: &str, suffix: &str, cap_tokens: usize) -> String {
391    use crate::core::tokens::count_tokens;
392    let suffix = suffix.trim_end_matches('\n');
393    if suffix.is_empty() {
394        let full = base.to_string();
395        return if count_tokens(&full) > cap_tokens {
396            truncate_to_token_cap(&full, cap_tokens)
397        } else {
398            full
399        };
400    }
401
402    let full = format!("{base}\n\n{suffix}");
403    if count_tokens(&full) <= cap_tokens {
404        return full;
405    }
406
407    let suffix_tokens = count_tokens(suffix);
408    // Reserve room for the suffix plus the "\n\n" join. If the suffix alone is
409    // already at/over budget, degrade to a plain tail-cap of the whole text.
410    let Some(base_budget) = cap_tokens.checked_sub(suffix_tokens + 1) else {
411        return truncate_to_token_cap(&full, cap_tokens);
412    };
413    let trimmed_base = truncate_to_token_cap(base, base_budget);
414    format!("{trimmed_base}\n\n{suffix}")
415}
416
417fn truncate_to_token_cap(s: &str, cap_tokens: usize) -> String {
418    use crate::core::tokens::count_tokens;
419    if count_tokens(s) <= cap_tokens {
420        return s.to_string();
421    }
422    // Keep whole lines: candidate cut points are the byte offsets of each
423    // newline. Token count is monotonic in prefix length, so binary-search for
424    // the longest whole-line prefix within the cap. This costs O(log lines)
425    // tokenizations instead of O(lines) — the per-line loop was pathologically
426    // slow on large session blocks (and timed out under coverage's ptrace
427    // instrumentation).
428    let cuts: Vec<usize> = s.match_indices('\n').map(|(i, _)| i).collect();
429    let (mut lo, mut hi) = (0usize, cuts.len());
430    let mut best: Option<usize> = None;
431    while lo < hi {
432        let mid = lo + (hi - lo) / 2;
433        let end = cuts[mid];
434        if end > 0 && count_tokens(&s[..end]) <= cap_tokens {
435            best = Some(end);
436            lo = mid + 1;
437        } else {
438            hi = mid;
439        }
440    }
441    if let Some(end) = best {
442        return s[..end].to_string();
443    }
444    // No line boundary fits — fall back to a char-boundary byte approximation.
445    let byte_approx = cap_tokens * 4;
446    let safe = s.floor_char_boundary(byte_approx.min(s.len()));
447    s[..safe].to_string()
448}
449
450fn build_full_instructions_for_test(crp_mode: CrpMode, client_name: &str) -> String {
451    use crate::core::rules_canonical as rc;
452    let shell_hint = build_shell_hint();
453    let session_block = String::new();
454    let knowledge_block = String::new();
455    let gotcha_block = String::new();
456    let litm_end_block = String::new();
457
458    let tool_bullets = rc::tool_mapping_bullets(rc::Mode::Mcp);
459    let compat = rc::compatibility_block();
460    let read_modes = rc::ctx_read_modes_block();
461    let auto_block = rc::automation_block();
462    let cep = rc::cep_block();
463    let litm_pref = rc::litm_end_block(rc::Mode::Mcp);
464
465    let mut base = format!(
466        "\
467CRITICAL: ALWAYS use lean-ctx MCP tools instead of native equivalents for token savings.\n\
468\n\
469{tool_bullets}\n\
470\n\
471{compat}\n\
472{shell_hint}\
473\n\
474{read_modes}\n\
475\n\
476{auto_block}\n\
477\n\
478{cep}\n\
479\n\
480{decoder_block}\n\
481\n\
482{session_block}\
483{knowledge_block}\
484{gotcha_block}\
485\n\
486--- ORIGIN ---\n\
487{origin}\n\
488\n\
489{litm_pref}\
490{litm_end_block}",
491        decoder_block = crate::core::protocol::instruction_decoder_block(),
492        origin = crate::core::integrity::origin_line(),
493        litm_end_block = &litm_end_block
494    );
495
496    if should_use_unified(client_name) {
497        base.push_str("\n\n");
498        base.push_str(rc::unified_tool_mode_block());
499        base.push('\n');
500    }
501
502    let intelligence_block = build_intelligence_block();
503    let terse_block = build_terse_agent_block_for_client(&crp_mode, client_name);
504
505    match crp_mode {
506        CrpMode::Off => format!("{base}\n\n{terse_block}{intelligence_block}"),
507        CrpMode::Compact => {
508            format!(
509                "{base}\n\n\
510CRP MODE: compact\n\
511Omit filler. Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
512Diff lines (+/-) only. TARGET: <=200 tok. Trust tool outputs.\n\n\
513{terse_block}{intelligence_block}"
514            )
515        }
516        CrpMode::Tdd => {
517            format!(
518                "{base}\n\n\
519CRP MODE: tdd\n\
520Max density. Every token carries meaning. Fn refs only, diff lines (+/-) only.\n\
521Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
522+F1:42 param(timeout:Duration) | -F1:10-15 | ~F1:42 old->new\n\
523BUDGET: <=150 tok. ZERO NARRATION. Trust tool outputs.\n\n\
524{terse_block}{intelligence_block}"
525            )
526        }
527    }
528}
529
530fn build_full_instructions_for_compiler(
531    crp_mode: CrpMode,
532    client_name: &str,
533    unified_tool_mode: bool,
534) -> String {
535    let shell_hint = build_shell_hint();
536    let session_block = String::new();
537    let knowledge_block = String::new();
538    let gotcha_block = String::new();
539    let litm_end_block = String::new();
540
541    use crate::core::rules_canonical as rc;
542    let tool_bullets = rc::tool_mapping_bullets(rc::Mode::Mcp);
543    let compat = rc::compatibility_block();
544    let read_modes = rc::ctx_read_modes_block();
545    let auto_blk = rc::automation_block();
546    let cep = rc::cep_block();
547    let litm_pref = rc::litm_end_block(rc::Mode::Mcp);
548
549    let mut base = format!(
550        "\
551CRITICAL: ALWAYS use lean-ctx MCP tools instead of native equivalents for token savings.\n\
552\n\
553{tool_bullets}\n\
554\n\
555{compat}\n\
556{shell_hint}\
557\n\
558{read_modes}\n\
559\n\
560{auto_blk}\n\
561\n\
562{cep}\n\
563\n\
564{decoder_block}\n\
565\n\
566{session_block}\
567{knowledge_block}\
568{gotcha_block}\
569\n\
570--- ORIGIN ---\n\
571{origin}\n\
572\n\
573{litm_pref}\
574{litm_end_block}",
575        decoder_block = crate::core::protocol::instruction_decoder_block(),
576        origin = crate::core::integrity::origin_line(),
577        litm_end_block = &litm_end_block
578    );
579
580    if unified_tool_mode {
581        base.push_str("\n\n");
582        base.push_str(rc::unified_tool_mode_block());
583        base.push('\n');
584    }
585
586    let _ = client_name; // keep signature aligned with other builders
587    let intelligence_block = build_intelligence_block();
588
589    match crp_mode {
590        CrpMode::Off => format!("{base}\n\n{intelligence_block}"),
591        CrpMode::Compact => {
592            format!(
593                "{base}\n\n\
594CRP MODE: compact\n\
595Omit filler. Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
596Diff lines (+/-) only. TARGET: <=200 tok. Trust tool outputs.\n\n\
597{intelligence_block}"
598            )
599        }
600        CrpMode::Tdd => {
601            format!(
602                "{base}\n\n\
603CRP MODE: tdd\n\
604Max density. Every token carries meaning. Fn refs only, diff lines (+/-) only.\n\
605Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
606+F1:42 param(timeout:Duration) | -F1:10-15 | ~F1:42 old->new\n\
607BUDGET: <=150 tok. ZERO NARRATION. Trust tool outputs.\n\n\
608{intelligence_block}"
609            )
610        }
611    }
612}
613
614pub fn claude_code_instructions() -> String {
615    build_claude_code_instructions()
616}
617
618fn build_terse_agent_block_for_client(_crp_mode: &CrpMode, client_name: &str) -> String {
619    use crate::core::config::{CompressionLevel, Config};
620    let cfg = Config::load();
621    let compression = CompressionLevel::effective(&cfg);
622    if compression.is_active() {
623        let persona = crate::core::persona::Persona::resolve(&cfg);
624        return crate::core::terse::agent_prompts::build_prompt_block_for_persona(
625            &compression,
626            client_name,
627            &persona,
628        );
629    }
630    String::new()
631}
632
633fn build_intelligence_block() -> String {
634    "\
635OUTPUT EFFICIENCY:\n\
636• Never echo tool output code. Never add narration comments. Show only changed code.\n\
637• [TASK:type] and SCOPE hints included. Architecture=thorough, generate=code."
638        .to_string()
639}
640
641fn build_shell_hint() -> String {
642    if !cfg!(windows) {
643        return String::new();
644    }
645    let name = crate::shell::shell_name();
646    let is_posix = matches!(name.as_str(), "bash" | "sh" | "zsh" | "fish");
647    if is_posix {
648        format!(
649            "\nSHELL: {name} (POSIX). Use POSIX commands (cat, head, grep, find, ls). \
650             Do NOT use PowerShell cmdlets (Get-Content, Select-Object, Get-ChildItem).\n"
651        )
652    } else if name.contains("powershell") || name.contains("pwsh") {
653        format!("\nSHELL: {name}. Use PowerShell cmdlets.\n")
654    } else {
655        format!("\nSHELL: {name}.\n")
656    }
657}
658
659fn should_use_unified(client_name: &str) -> bool {
660    if std::env::var("LEAN_CTX_FULL_TOOLS").is_ok() {
661        return false;
662    }
663    if std::env::var("LEAN_CTX_UNIFIED").is_ok() {
664        return true;
665    }
666    let _ = client_name;
667    false
668}
669
670#[cfg(test)]
671mod tests {
672    use super::*;
673    use crate::core::tokens::count_tokens;
674
675    #[test]
676    fn guidance_suffix_survives_oversized_base() {
677        // Simulate a bloated session/knowledge `base` that alone exceeds the cap.
678        let base = "SESSION LINE\n".repeat(4000);
679        let suffix = "OUTPUT STYLE: expert-terse\nFn refs only, diff lines only.";
680        let out = assemble_within_cap(&base, suffix, INSTRUCTION_CAP_TOKENS);
681
682        assert!(
683            out.contains("OUTPUT STYLE: expert-terse"),
684            "protected guidance suffix must survive truncation"
685        );
686        assert!(
687            count_tokens(&out) <= INSTRUCTION_CAP_TOKENS,
688            "assembled output must respect the token cap"
689        );
690        assert!(
691            out.len() < base.len(),
692            "oversized base must have been truncated"
693        );
694    }
695
696    #[test]
697    fn under_cap_keeps_everything() {
698        let base = "tool mapping block";
699        let suffix = "OUTPUT STYLE: dense";
700        let out = assemble_within_cap(base, suffix, INSTRUCTION_CAP_TOKENS);
701        assert!(out.contains(base));
702        assert!(out.contains(suffix));
703    }
704
705    #[test]
706    fn empty_suffix_caps_base_only() {
707        let base = "x\n".repeat(4000);
708        let out = assemble_within_cap(&base, "", INSTRUCTION_CAP_TOKENS);
709        assert!(count_tokens(&out) <= INSTRUCTION_CAP_TOKENS);
710    }
711}