1use crate::core::config::CompressionLevel;
2use crate::core::rules_canonical::{self as rc, Wrapper};
3use crate::tools::CrpMode;
4
5const INSTRUCTION_CAP_TOKENS: usize = 800;
7
8#[cfg(test)]
20const STATIC_INSTRUCTION_BUDGET_TOKENS: usize = 545;
21#[cfg(test)]
22const STATIC_INSTRUCTION_BUDGET_TDD_TOKENS: usize = 675;
23#[cfg(all(test, windows))]
25const STATIC_INSTRUCTION_SHELL_HINT_TOKENS: usize = 25;
26#[cfg(all(test, not(windows)))]
27const STATIC_INSTRUCTION_SHELL_HINT_TOKENS: usize = 0;
28
29#[must_use]
30pub fn build_instructions(crp_mode: CrpMode) -> String {
31 build_instructions_with_client(crp_mode, "")
32}
33
34#[must_use]
35pub fn build_instructions_with_client(crp_mode: CrpMode, client_name: &str) -> String {
36 let cfg = crate::core::config::Config::load();
37 let minimal = cfg.minimal_overhead_effective_for_client(client_name);
38 let shadow = cfg.shadow_mode;
39 let level = if client_loads_compression_from_file(client_name) {
42 CompressionLevel::Off
43 } else {
44 CompressionLevel::effective(&cfg)
45 };
46 let persona_block = crate::core::persona::Persona::resolve(&cfg).prompt_block();
50 build_full_instructions(
51 crp_mode,
52 client_name,
53 minimal,
54 level,
55 shadow,
56 &persona_block,
57 )
58}
59
60#[must_use]
69pub fn claude_code_static_instructions_for_test() -> String {
70 build_full_instructions(CrpMode::Off, "", true, CompressionLevel::Off, false, "")
71}
72
73#[must_use]
75pub fn build_instructions_for_test(crp_mode: CrpMode) -> String {
76 let shadow = false;
77 let level = CompressionLevel::effective(&crate::core::config::Config::load());
80 let tp =
81 crate::core::tool_profiles::ToolProfile::from_config(&crate::core::config::Config::load());
82 let skeleton = rc::render(shadow, Wrapper::Bare, level, &tp);
83 let shell_hint = build_shell_hint();
84
85 let base = format!(
86 "{skeleton}\n\
87 {shell_hint}\n\
88 {decoder_block}\n\
89 {origin}",
90 decoder_block =
91 crate::core::protocol::instruction_decoder_block(matches!(crp_mode, CrpMode::Tdd)),
92 origin = crate::core::integrity::origin_line(),
93 );
94
95 match crp_mode_suffix(crp_mode) {
96 "" => format!("{base}\n\n{}", rc::INTELLIGENCE),
97 crp => format!("{base}\n\n{crp}\n\n{}", rc::INTELLIGENCE),
98 }
99}
100
101#[must_use]
105pub fn build_instructions_with_client_for_compiler(
106 crp_mode: CrpMode,
107 client_name: &str,
108 _unified_tool_mode: bool,
109) -> String {
110 let tp =
111 crate::core::tool_profiles::ToolProfile::from_config(&crate::core::config::Config::load());
112 let skeleton = rc::render(true, Wrapper::Bare, CompressionLevel::Off, &tp);
113 let shell_hint = build_shell_hint();
114
115 let base = format!(
116 "{skeleton}\n\
117 {shell_hint}\n\
118 {decoder_block}\n\
119 {origin}",
120 decoder_block =
121 crate::core::protocol::instruction_decoder_block(matches!(crp_mode, CrpMode::Tdd)),
122 origin = crate::core::integrity::origin_line(),
123 );
124
125 let _ = client_name;
126
127 match crp_mode_suffix(crp_mode) {
128 "" => format!("{base}\n\n{}", rc::INTELLIGENCE),
129 crp => format!("{base}\n\n{crp}\n\n{}", rc::INTELLIGENCE),
130 }
131}
132
133fn rotate_wakeup_manifest(session: &crate::core::session::SessionState, profile_name: &str) {
135 use crate::core::litm_calibration::{Position, record_outcome};
136 use crate::core::session::ManifestEntry;
137
138 let mut updated = session.clone();
139
140 for entry in &updated.wakeup_manifest {
141 if !entry.missed
142 && let Some(pos) = Position::parse(&entry.position)
143 {
144 record_outcome(&entry.profile, pos, true);
145 }
146 }
147
148 let mut manifest: Vec<ManifestEntry> = Vec::new();
149 let mut push = |key: &str, position: &str| {
150 let key = key.trim();
151 if !key.is_empty() {
152 manifest.push(ManifestEntry {
153 key: key.chars().take(80).collect(),
154 position: position.to_string(),
155 profile: profile_name.to_string(),
156 missed: false,
157 });
158 }
159 };
160
161 if let Some(ref task) = updated.task {
162 push(&task.description, "begin");
163 }
164 for d in updated.decisions.iter().rev().take(5) {
165 push(&d.summary, "begin");
166 }
167 for f in updated.findings.iter().rev().take(8) {
168 push(&f.summary, "end");
169 }
170 for n in updated.next_steps.iter().take(3) {
171 push(n, "end");
172 }
173
174 updated.wakeup_manifest = manifest;
175 let _ = updated.save();
176}
177
178#[must_use]
180pub fn claude_config_dir_display() -> String {
181 match std::env::var("CLAUDE_CONFIG_DIR") {
182 Ok(dir) if !dir.trim().is_empty() => {
183 let dir = dir.trim().to_string();
184 if dir.starts_with('~') {
185 dir
186 } else if let Some(home) = dirs::home_dir() {
187 let home_str = home.to_string_lossy();
188 if let Some(rest) = dir.strip_prefix(home_str.as_ref()) {
189 format!("~{rest}")
190 } else {
191 dir
192 }
193 } else {
194 dir
195 }
196 }
197 _ => "~/.claude".to_string(),
198 }
199}
200
201fn build_full_instructions(
204 crp_mode: CrpMode,
205 client_name: &str,
206 minimal: bool,
207 level: CompressionLevel,
208 shadow: bool,
209 persona_block: &str,
210) -> String {
211 let profile = crate::core::litm::LitmProfile::from_client_name(client_name);
212 let loaded_session = if minimal {
213 None
214 } else {
215 crate::core::session::SessionState::load_latest()
216 };
217
218 let (session_block, litm_end_block) = match loaded_session {
219 Some(ref session) => {
220 rotate_wakeup_manifest(session, profile.name);
221 let share = crate::core::litm_calibration::begin_share(profile.name);
222 let mut positioned = crate::core::litm::position_optimize_with_share(session, share);
223 positioned.enforce_token_budget(crate::core::litm::active_session_budget());
226 let begin = format!(
227 "\n\n--- ACTIVE SESSION (LITM P1: begin position, profile: {}) ---\n{}\n---\n",
228 profile.name, positioned.begin_block
229 );
230 let end = if positioned.end_block.is_empty() {
231 String::new()
232 } else {
233 format!(
234 "\n--- SESSION RESUME (post-compaction) ---\n{}\n---\n",
235 positioned.end_block
236 )
237 };
238 (begin, end)
239 }
240 None => (String::new(), String::new()),
241 };
242
243 let project_root_for_blocks = if minimal {
244 None
245 } else {
246 loaded_session
247 .as_ref()
248 .and_then(|s| s.project_root.clone())
249 .or_else(|| {
250 std::env::current_dir()
251 .ok()
252 .map(|p| p.to_string_lossy().to_string())
253 })
254 };
255
256 let knowledge_block = match &project_root_for_blocks {
257 Some(root) => {
258 let knowledge = crate::core::knowledge::ProjectKnowledge::load(root);
259 match knowledge {
260 Some(k) if !k.facts.is_empty() || !k.patterns.is_empty() => {
261 let aaak = k.format_aaak();
262 if aaak.is_empty() {
263 String::new()
264 } else {
265 format!("\n--- PROJECT MEMORY (AAAK) ---\n{}\n---\n", aaak.trim())
266 }
267 }
268 _ => String::new(),
269 }
270 }
271 None => String::new(),
272 };
273
274 let gotcha_block = match &project_root_for_blocks {
275 Some(root) => {
276 let store = crate::core::gotcha_tracker::GotchaStore::load(root);
277 let files: Vec<String> = loaded_session
278 .as_ref()
279 .map(|s| s.files_touched.iter().map(|ft| ft.path.clone()).collect())
280 .unwrap_or_default();
281 let block = store.format_injection_block(&files);
282 if block.is_empty() {
283 String::new()
284 } else {
285 format!("\n{block}\n")
286 }
287 }
288 None => String::new(),
289 };
290
291 let health_block = match &project_root_for_blocks {
292 Some(root) => {
293 let block = crate::core::code_health::persist::format_session_block(root);
294 if block.is_empty() {
295 String::new()
296 } else {
297 format!("\n{block}\n")
298 }
299 }
300 None => String::new(),
301 };
302
303 let shell_hint = build_shell_hint();
304
305 let cfg = crate::core::config::Config::load();
319 let tool_profile = crate::core::tool_profiles::ToolProfile::from_config(&cfg);
320 let skeleton = if client_loads_rules_from_file(client_name) {
321 let anchor = if client_is_hook_covered(client_name) {
322 hook_covered_anchor(&tool_profile)
323 } else {
324 skeleton_anchor(&tool_profile)
325 };
326 let compression = rc::compression_text(level);
327 if compression.is_empty() {
328 anchor
329 } else {
330 format!("{anchor}\n{compression}")
331 }
332 } else {
333 rc::render(shadow, Wrapper::Bare, level, &tool_profile)
334 };
335
336 let config_dir = claude_config_dir_display();
339
340 let persona_section = if persona_block.is_empty() {
343 String::new()
344 } else {
345 format!("\n{persona_block}")
346 };
347
348 let base = format!(
349 "{skeleton}\n\
350 {persona_section}\
351 {shell_hint}\n\
352 {decoder_block}\n\
353 Full instructions at {config_dir}/CLAUDE.md\n\
354 {session_block}\n\
355 {knowledge_block}\n\
356 {gotcha_block}\n\
357 {health_block}\n\
358 {origin}\n\
359 {litm_end_block}",
360 decoder_block =
361 crate::core::protocol::instruction_decoder_block(matches!(crp_mode, CrpMode::Tdd)),
362 origin = crate::core::integrity::origin_line(),
363 litm_end_block = litm_end_block
364 );
365
366 let guidance_suffix = match crp_mode_suffix(crp_mode) {
369 "" => rc::INTELLIGENCE.to_string(),
370 crp => format!("{crp}\n\n{}", rc::INTELLIGENCE),
371 };
372
373 assemble_within_cap(&base, &guidance_suffix, INSTRUCTION_CAP_TOKENS)
374}
375
376fn crp_mode_suffix(crp_mode: CrpMode) -> &'static str {
377 match crp_mode {
378 CrpMode::Off => "",
379 CrpMode::Compact => {
380 "CRP MODE: compact — omit filler; abbreviate fn,cfg,impl,deps,req,res; \
381 diff lines (+/-) only; <=200 tok; trust tool outputs."
382 }
383 CrpMode::Tdd => {
384 "CRP MODE: tdd — max density; Fn refs + diff lines only \
385 (+F1:42 | -F1:10-15 | ~F1:42 old->new); <=150 tok; zero narration."
386 }
387 }
388}
389
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 let Some(base_budget) = cap_tokens.checked_sub(suffix_tokens + 1) else {
409 return truncate_to_token_cap(&full, cap_tokens);
410 };
411 let trimmed_base = truncate_to_token_cap(base, base_budget);
412 format!("{trimmed_base}\n\n{suffix}")
413}
414
415fn truncate_to_token_cap(s: &str, cap_tokens: usize) -> String {
416 use crate::core::tokens::count_tokens;
417 if count_tokens(s) <= cap_tokens {
418 return s.to_string();
419 }
420 let cuts: Vec<usize> = s.match_indices('\n').map(|(i, _)| i).collect();
421 let (mut lo, mut hi) = (0usize, cuts.len());
422 let mut best: Option<usize> = None;
423 while lo < hi {
424 let mid = lo + (hi - lo) / 2;
425 let end = cuts[mid];
426 if end > 0 && count_tokens(&s[..end]) <= cap_tokens {
427 best = Some(end);
428 lo = mid + 1;
429 } else {
430 hi = mid;
431 }
432 }
433 if let Some(end) = best {
434 return s[..end].to_string();
435 }
436 let byte_approx = cap_tokens * 4;
437 let safe = s.floor_char_boundary(byte_approx.min(s.len()));
438 s[..safe].to_string()
439}
440
441#[must_use]
443pub fn claude_code_instructions() -> String {
444 build_instructions(CrpMode::Off)
445}
446
447fn skeleton_anchor(tp: &crate::core::tool_profiles::ToolProfile) -> String {
450 if tp.is_tool_enabled("ctx_compose") {
451 "lean-ctx active — your auto-loaded lean-ctx rules apply: \
452 ctx_* tools replace native Read/Grep/Shell/Glob (ctx_compose first)."
453 .into()
454 } else {
455 "lean-ctx active — your auto-loaded lean-ctx rules apply: \
456 ctx_* tools replace native Read/Grep/Shell/Glob."
457 .into()
458 }
459}
460
461fn hook_covered_anchor(tp: &crate::core::tool_profiles::ToolProfile) -> String {
463 let mut s =
464 String::from("lean-ctx active — hooks compress native Shell/Read/Grep transparently");
465 if tp.is_tool_enabled("ctx_compose") {
466 s.push_str("; call ctx_compose to orient");
467 }
468 if tp.is_tool_enabled("ctx_session") || tp.is_tool_enabled("ctx_knowledge") {
470 s.push_str(", ctx_search(action=semantic) / ctx_knowledge for meaning & memory");
471 }
472 s.push('.');
473 s
474}
475
476#[cfg(test)]
478const SKELETON_ANCHOR: &str = "lean-ctx active — your auto-loaded lean-ctx rules apply: \
479 ctx_* tools replace native Read/Grep/Shell/Glob (ctx_compose first).";
480
481fn client_loads_compression_from_file(client_name: &str) -> bool {
482 crate::core::home::resolve_home_dir().is_some_and(|home| {
483 crate::core::rules_channel::client_autoloads_compression(client_name, &home)
484 })
485}
486
487fn client_loads_rules_from_file(client_name: &str) -> bool {
488 crate::core::home::resolve_home_dir()
489 .is_some_and(|home| crate::core::rules_channel::client_autoloads_rules(client_name, &home))
490}
491
492fn client_is_hook_covered(client_name: &str) -> bool {
493 crate::core::home::resolve_home_dir()
494 .is_some_and(|home| crate::core::rules_channel::client_hook_covered(client_name, &home))
495}
496
497fn build_shell_hint() -> String {
498 if !cfg!(windows) {
499 return String::new();
500 }
501 let name = crate::shell::shell_name();
502 let is_posix = matches!(name.as_str(), "bash" | "sh" | "zsh" | "fish");
503 if is_posix {
504 format!("\nSHELL: {name} (POSIX) — no PowerShell cmdlets.\n")
505 } else if name.contains("powershell") || name.contains("pwsh") {
506 format!("\nSHELL: {name}. Use PowerShell cmdlets.\n")
507 } else {
508 format!("\nSHELL: {name}.\n")
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515 use crate::core::tokens::count_tokens;
516
517 #[test]
518 fn guidance_suffix_survives_oversized_base() {
519 let base = "SESSION LINE\n".repeat(4000);
520 let suffix = "OUTPUT STYLE: expert-terse\nFn refs only, diff lines only.";
521 let out = assemble_within_cap(&base, suffix, INSTRUCTION_CAP_TOKENS);
522 assert!(out.contains("OUTPUT STYLE: expert-terse"));
523 assert!(count_tokens(&out) <= INSTRUCTION_CAP_TOKENS);
524 assert!(out.len() < base.len());
525 }
526
527 #[test]
528 fn empty_client_never_dedups_compression() {
529 assert!(!client_loads_compression_from_file(""));
530 assert!(!client_loads_compression_from_file("totally-unknown-agent"));
531 }
532
533 #[test]
534 fn covered_client_gets_anchor_instead_of_skeleton() {
535 let _guard = crate::core::data_dir::test_env_lock();
538 let tmp = tempfile::tempdir().unwrap();
539 let home = tmp.path();
540 std::fs::create_dir_all(home.join(".cursor/rules")).unwrap();
541 let cfg = crate::core::config::Config::load();
542 let shadow = cfg.shadow_mode;
543 let tp = crate::core::tool_profiles::ToolProfile::Power;
544 std::fs::write(
545 home.join(".cursor/rules/lean-ctx.mdc"),
546 rc::render(
547 shadow,
548 Wrapper::Dedicated,
549 crate::core::config::CompressionLevel::Standard,
550 &tp,
551 ),
552 )
553 .unwrap();
554 let old_home = std::env::var("HOME").ok();
555 crate::test_env::set_var("HOME", home);
556 crate::test_env::set_var("LEAN_CTX_MINIMAL", "1");
557
558 let covered = build_instructions_with_client(CrpMode::Off, "cursor");
559 let uncovered = build_instructions_with_client(CrpMode::Off, "some-other-agent");
560
561 if let Some(h) = old_home {
562 crate::test_env::set_var("HOME", h);
563 } else {
564 crate::test_env::remove_var("HOME");
565 }
566 crate::test_env::remove_var("LEAN_CTX_MINIMAL");
567
568 assert!(
569 covered.contains(SKELETON_ANCHOR),
570 "covered client must get the anchor:\n{covered}"
571 );
572 if shadow {
573 assert!(
574 !covered.contains("auto-route"),
575 "covered shadow client must not re-pay the shadow nudge:\n{covered}"
576 );
577 } else {
578 assert!(
579 !covered.contains("MANDATORY MAPPING"),
580 "covered client must not re-pay the skeleton:\n{covered}"
581 );
582 }
583 assert!(
585 !covered.contains("OUTPUT STYLE:"),
586 "covered client must not re-pay the compression prompt:\n{covered}"
587 );
588 if shadow {
589 assert!(
590 uncovered.contains("auto-route"),
591 "uncovered shadow client gets the shadow nudge:\n{uncovered}"
592 );
593 } else {
594 assert!(
595 uncovered.contains("MANDATORY MAPPING"),
596 "uncovered client keeps the full skeleton:\n{uncovered}"
597 );
598 }
599 eprintln!(
600 "instructions footprint: covered={} tok, uncovered={} tok",
601 count_tokens(&covered),
602 count_tokens(&uncovered)
603 );
604 assert!(
605 count_tokens(&covered) < count_tokens(&uncovered),
606 "anchor path must be strictly cheaper"
607 );
608 }
609
610 #[test]
611 fn hook_covered_client_gets_hook_aware_anchor() {
612 let _guard = crate::core::data_dir::test_env_lock();
616 let tmp = tempfile::tempdir().unwrap();
617 let home = tmp.path();
618 std::fs::create_dir_all(home.join(".cursor/rules")).unwrap();
619 let tp = crate::core::tool_profiles::ToolProfile::Power;
620 std::fs::write(
621 home.join(".cursor/rules/lean-ctx.mdc"),
622 rc::render(
623 false,
624 Wrapper::HookCovered,
625 crate::core::config::CompressionLevel::Off,
626 &tp,
627 ),
628 )
629 .unwrap();
630 std::fs::write(
631 home.join(".cursor/hooks.json"),
632 r#"{"version":1,"hooks":{"preToolUse":[
633 {"matcher":"Shell","command":"/usr/local/bin/lean-ctx hook rewrite"},
634 {"matcher":"Read|Grep","command":"/usr/local/bin/lean-ctx hook redirect"}
635 ]}}"#,
636 )
637 .unwrap();
638 let old_home = std::env::var("HOME").ok();
639 crate::test_env::set_var("HOME", home);
640 crate::test_env::set_var("LEAN_CTX_MINIMAL", "1");
641
642 let covered = build_instructions_with_client(CrpMode::Off, "cursor");
643
644 if let Some(h) = old_home {
645 crate::test_env::set_var("HOME", h);
646 } else {
647 crate::test_env::remove_var("HOME");
648 }
649 crate::test_env::remove_var("LEAN_CTX_MINIMAL");
650
651 assert!(
652 covered.contains("hooks compress native Shell/Read/Grep"),
653 "hook-covered client must get the hook-aware anchor:\n{covered}"
654 );
655 assert!(
656 !covered.contains("ctx_* tools replace native")
657 && !covered.contains("MANDATORY MAPPING"),
658 "hook-covered client must not carry the replace-native wording:\n{covered}"
659 );
660 }
661
662 #[test]
663 fn non_coding_persona_block_lands_in_instructions() {
664 let _guard = crate::core::data_dir::test_env_lock();
665 crate::test_env::set_var("LEAN_CTX_MINIMAL", "1");
666
667 crate::test_env::set_var("LEAN_CTX_PERSONA", "research");
668 let research = build_instructions_with_client(CrpMode::Off, "");
669
670 crate::test_env::set_var("LEAN_CTX_PERSONA", "coding");
671 let coding = build_instructions_with_client(CrpMode::Off, "");
672
673 crate::test_env::remove_var("LEAN_CTX_PERSONA");
674 crate::test_env::remove_var("LEAN_CTX_MINIMAL");
675
676 assert!(
677 research.contains("PERSONA: research"),
678 "research persona must announce its domain block:\n{research}"
679 );
680 assert!(
681 research.contains("INTENTS: explore, summarize, compare, cite, synthesize"),
682 "research persona must carry its intent vocabulary:\n{research}"
683 );
684 assert!(
685 !coding.contains("PERSONA:"),
686 "the coding default must keep the instructions byte-stable (#498):\n{coding}"
687 );
688 }
689
690 #[test]
691 fn under_cap_keeps_everything() {
692 let base = "tool mapping block";
693 let suffix = "OUTPUT STYLE: dense";
694 let out = assemble_within_cap(base, suffix, INSTRUCTION_CAP_TOKENS);
695 assert!(out.contains(base));
696 assert!(out.contains(suffix));
697 }
698
699 #[test]
700 fn empty_suffix_caps_base_only() {
701 let base = "x\n".repeat(4000);
702 let out = assemble_within_cap(&base, "", INSTRUCTION_CAP_TOKENS);
703 assert!(count_tokens(&out) <= INSTRUCTION_CAP_TOKENS);
704 }
705
706 #[cfg(windows)]
707 #[test]
708 fn shell_hint_stays_within_its_budget() {
709 let hint = build_shell_hint();
710 let tokens = count_tokens(&hint);
711 assert!(
712 tokens <= STATIC_INSTRUCTION_SHELL_HINT_TOKENS,
713 "shell hint = {tokens} tok, budget {STATIC_INSTRUCTION_SHELL_HINT_TOKENS}: {hint}"
714 );
715 }
716
717 #[test]
718 fn minimal_overhead_instructions_stay_within_budget() {
719 const MINIMAL_INSTRUCTION_BUDGET_TOKENS: usize =
720 STATIC_INSTRUCTION_BUDGET_TDD_TOKENS + STATIC_INSTRUCTION_SHELL_HINT_TOKENS;
721 let _iso = crate::core::data_dir::isolated_data_dir();
722 crate::test_env::set_var("LEAN_CTX_MINIMAL", "1");
723 let out = build_instructions(CrpMode::Compact);
724 crate::test_env::remove_var("LEAN_CTX_MINIMAL");
725 let tokens = count_tokens(&out);
726 assert!(
727 tokens <= MINIMAL_INSTRUCTION_BUDGET_TOKENS,
728 "minimal-overhead instructions = {tokens} tok, budget {MINIMAL_INSTRUCTION_BUDGET_TOKENS}\n---\n{out}\n---"
729 );
730 }
731
732 #[test]
733 fn static_skeleton_stays_within_budget() {
734 let _iso = crate::core::data_dir::isolated_data_dir();
735 crate::test_env::set_var("LEAN_CTX_COMPRESSION", "off");
738 for (mode, base_budget) in [
739 (CrpMode::Off, STATIC_INSTRUCTION_BUDGET_TOKENS),
740 (CrpMode::Compact, STATIC_INSTRUCTION_BUDGET_TOKENS),
741 (CrpMode::Tdd, STATIC_INSTRUCTION_BUDGET_TDD_TOKENS),
742 ] {
743 let budget = base_budget + STATIC_INSTRUCTION_SHELL_HINT_TOKENS;
744 let out = build_instructions_for_test(mode);
745 let tokens = count_tokens(&out);
746 assert!(
747 tokens <= budget,
748 "static instructions for {mode:?} = {tokens} tok, budget {budget}\n---\n{out}\n---"
749 );
750 }
751 crate::test_env::remove_var("LEAN_CTX_COMPRESSION");
752 }
753}