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)]
21const STATIC_INSTRUCTION_BUDGET_TOKENS: usize = 615;
22#[cfg(test)]
23const STATIC_INSTRUCTION_BUDGET_TDD_TOKENS: usize = 712;
24#[cfg(all(test, windows))]
26const STATIC_INSTRUCTION_SHELL_HINT_TOKENS: usize = 25;
27#[cfg(all(test, not(windows)))]
28const STATIC_INSTRUCTION_SHELL_HINT_TOKENS: usize = 0;
29
30#[must_use]
31pub fn build_instructions(crp_mode: CrpMode) -> String {
32 build_instructions_with_client(crp_mode, "")
33}
34
35#[must_use]
36pub fn build_instructions_with_client(crp_mode: CrpMode, client_name: &str) -> String {
37 let cfg = crate::core::config::Config::load();
38 let minimal = cfg.minimal_overhead_effective_for_client(client_name);
39 let shadow = cfg.shadow_mode;
40 let level = if client_loads_compression_from_file(client_name) {
43 CompressionLevel::Off
44 } else {
45 CompressionLevel::effective(&cfg)
46 };
47 build_full_instructions(crp_mode, client_name, minimal, level, shadow)
48}
49
50#[must_use]
58pub fn claude_code_static_instructions_for_test() -> String {
59 build_full_instructions(CrpMode::Off, "", true, CompressionLevel::Off, false)
60}
61
62#[must_use]
64pub fn build_instructions_for_test(crp_mode: CrpMode) -> String {
65 let shadow = false;
66 let level = CompressionLevel::effective(&crate::core::config::Config::load());
69 let skeleton = rc::render(shadow, Wrapper::Bare, level);
70 let shell_hint = build_shell_hint();
71
72 let base = format!(
73 "{skeleton}\n\
74 {shell_hint}\n\
75 {decoder_block}\n\
76 {origin}",
77 decoder_block =
78 crate::core::protocol::instruction_decoder_block(matches!(crp_mode, CrpMode::Tdd)),
79 origin = crate::core::integrity::origin_line(),
80 );
81
82 match crp_mode_suffix(crp_mode) {
83 "" => format!("{base}\n\n{}", rc::INTELLIGENCE),
84 crp => format!("{base}\n\n{crp}\n\n{}", rc::INTELLIGENCE),
85 }
86}
87
88#[must_use]
92pub fn build_instructions_with_client_for_compiler(
93 crp_mode: CrpMode,
94 client_name: &str,
95 _unified_tool_mode: bool,
96) -> String {
97 let skeleton = rc::render(true, Wrapper::Bare, CompressionLevel::Off);
98 let shell_hint = build_shell_hint();
99
100 let base = format!(
101 "{skeleton}\n\
102 {shell_hint}\n\
103 {decoder_block}\n\
104 {origin}",
105 decoder_block =
106 crate::core::protocol::instruction_decoder_block(matches!(crp_mode, CrpMode::Tdd)),
107 origin = crate::core::integrity::origin_line(),
108 );
109
110 let _ = client_name;
111
112 match crp_mode_suffix(crp_mode) {
113 "" => format!("{base}\n\n{}", rc::INTELLIGENCE),
114 crp => format!("{base}\n\n{crp}\n\n{}", rc::INTELLIGENCE),
115 }
116}
117
118fn rotate_wakeup_manifest(session: &crate::core::session::SessionState, profile_name: &str) {
120 use crate::core::litm_calibration::{Position, record_outcome};
121 use crate::core::session::ManifestEntry;
122
123 let mut updated = session.clone();
124
125 for entry in &updated.wakeup_manifest {
126 if !entry.missed
127 && let Some(pos) = Position::parse(&entry.position)
128 {
129 record_outcome(&entry.profile, pos, true);
130 }
131 }
132
133 let mut manifest: Vec<ManifestEntry> = Vec::new();
134 let mut push = |key: &str, position: &str| {
135 let key = key.trim();
136 if !key.is_empty() {
137 manifest.push(ManifestEntry {
138 key: key.chars().take(80).collect(),
139 position: position.to_string(),
140 profile: profile_name.to_string(),
141 missed: false,
142 });
143 }
144 };
145
146 if let Some(ref task) = updated.task {
147 push(&task.description, "begin");
148 }
149 for d in updated.decisions.iter().rev().take(5) {
150 push(&d.summary, "begin");
151 }
152 for f in updated.findings.iter().rev().take(8) {
153 push(&f.summary, "end");
154 }
155 for n in updated.next_steps.iter().take(3) {
156 push(n, "end");
157 }
158
159 updated.wakeup_manifest = manifest;
160 let _ = updated.save();
161}
162
163#[must_use]
165pub fn claude_config_dir_display() -> String {
166 match std::env::var("CLAUDE_CONFIG_DIR") {
167 Ok(dir) if !dir.trim().is_empty() => {
168 let dir = dir.trim().to_string();
169 if dir.starts_with('~') {
170 dir
171 } else if let Some(home) = dirs::home_dir() {
172 let home_str = home.to_string_lossy();
173 if let Some(rest) = dir.strip_prefix(home_str.as_ref()) {
174 format!("~{rest}")
175 } else {
176 dir
177 }
178 } else {
179 dir
180 }
181 }
182 _ => "~/.claude".to_string(),
183 }
184}
185
186fn build_full_instructions(
189 crp_mode: CrpMode,
190 client_name: &str,
191 minimal: bool,
192 level: CompressionLevel,
193 shadow: bool,
194) -> String {
195 let profile = crate::core::litm::LitmProfile::from_client_name(client_name);
196 let loaded_session = if minimal {
197 None
198 } else {
199 crate::core::session::SessionState::load_latest()
200 };
201
202 let (session_block, litm_end_block) = match loaded_session {
203 Some(ref session) => {
204 rotate_wakeup_manifest(session, profile.name);
205 let share = crate::core::litm_calibration::begin_share(profile.name);
206 let mut positioned = crate::core::litm::position_optimize_with_share(session, share);
207 positioned.enforce_token_budget(crate::core::litm::active_session_budget());
210 let begin = format!(
211 "\n\n--- ACTIVE SESSION (LITM P1: begin position, profile: {}) ---\n{}\n---\n",
212 profile.name, positioned.begin_block
213 );
214 let end = if positioned.end_block.is_empty() {
215 String::new()
216 } else {
217 format!(
218 "\n--- SESSION RESUME (post-compaction) ---\n{}\n---\n",
219 positioned.end_block
220 )
221 };
222 (begin, end)
223 }
224 None => (String::new(), String::new()),
225 };
226
227 let project_root_for_blocks = if minimal {
228 None
229 } else {
230 loaded_session
231 .as_ref()
232 .and_then(|s| s.project_root.clone())
233 .or_else(|| {
234 std::env::current_dir()
235 .ok()
236 .map(|p| p.to_string_lossy().to_string())
237 })
238 };
239
240 let knowledge_block = match &project_root_for_blocks {
241 Some(root) => {
242 let knowledge = crate::core::knowledge::ProjectKnowledge::load(root);
243 match knowledge {
244 Some(k) if !k.facts.is_empty() || !k.patterns.is_empty() => {
245 let aaak = k.format_aaak();
246 if aaak.is_empty() {
247 String::new()
248 } else {
249 format!("\n--- PROJECT MEMORY (AAAK) ---\n{}\n---\n", aaak.trim())
250 }
251 }
252 _ => String::new(),
253 }
254 }
255 None => String::new(),
256 };
257
258 let gotcha_block = match &project_root_for_blocks {
259 Some(root) => {
260 let store = crate::core::gotcha_tracker::GotchaStore::load(root);
261 let files: Vec<String> = loaded_session
262 .as_ref()
263 .map(|s| s.files_touched.iter().map(|ft| ft.path.clone()).collect())
264 .unwrap_or_default();
265 let block = store.format_injection_block(&files);
266 if block.is_empty() {
267 String::new()
268 } else {
269 format!("\n{block}\n")
270 }
271 }
272 None => String::new(),
273 };
274
275 let health_block = match &project_root_for_blocks {
276 Some(root) => {
277 let block = crate::core::code_health::persist::format_session_block(root);
278 if block.is_empty() {
279 String::new()
280 } else {
281 format!("\n{block}\n")
282 }
283 }
284 None => String::new(),
285 };
286
287 let shell_hint = build_shell_hint();
288
289 let skeleton = rc::render(shadow, Wrapper::Bare, level);
292
293 let config_dir = claude_config_dir_display();
296
297 let base = format!(
298 "{skeleton}\n\
299 {shell_hint}\n\
300 {decoder_block}\n\
301 Full instructions at {config_dir}/CLAUDE.md\n\
302 {session_block}\n\
303 {knowledge_block}\n\
304 {gotcha_block}\n\
305 {health_block}\n\
306 {origin}\n\
307 {litm_end_block}",
308 decoder_block =
309 crate::core::protocol::instruction_decoder_block(matches!(crp_mode, CrpMode::Tdd)),
310 origin = crate::core::integrity::origin_line(),
311 litm_end_block = litm_end_block
312 );
313
314 let guidance_suffix = match crp_mode_suffix(crp_mode) {
317 "" => rc::INTELLIGENCE.to_string(),
318 crp => format!("{crp}\n\n{}", rc::INTELLIGENCE),
319 };
320
321 assemble_within_cap(&base, &guidance_suffix, INSTRUCTION_CAP_TOKENS)
322}
323
324fn crp_mode_suffix(crp_mode: CrpMode) -> &'static str {
325 match crp_mode {
326 CrpMode::Off => "",
327 CrpMode::Compact => {
328 "CRP MODE: compact — omit filler; abbreviate fn,cfg,impl,deps,req,res; \
329 diff lines (+/-) only; <=200 tok; trust tool outputs."
330 }
331 CrpMode::Tdd => {
332 "CRP MODE: tdd — max density; Fn refs + diff lines only \
333 (+F1:42 | -F1:10-15 | ~F1:42 old->new); <=150 tok; zero narration."
334 }
335 }
336}
337
338fn assemble_within_cap(base: &str, suffix: &str, cap_tokens: usize) -> String {
339 use crate::core::tokens::count_tokens;
340 let suffix = suffix.trim_end_matches('\n');
341 if suffix.is_empty() {
342 let full = base.to_string();
343 return if count_tokens(&full) > cap_tokens {
344 truncate_to_token_cap(&full, cap_tokens)
345 } else {
346 full
347 };
348 }
349
350 let full = format!("{base}\n\n{suffix}");
351 if count_tokens(&full) <= cap_tokens {
352 return full;
353 }
354
355 let suffix_tokens = count_tokens(suffix);
356 let Some(base_budget) = cap_tokens.checked_sub(suffix_tokens + 1) else {
357 return truncate_to_token_cap(&full, cap_tokens);
358 };
359 let trimmed_base = truncate_to_token_cap(base, base_budget);
360 format!("{trimmed_base}\n\n{suffix}")
361}
362
363fn truncate_to_token_cap(s: &str, cap_tokens: usize) -> String {
364 use crate::core::tokens::count_tokens;
365 if count_tokens(s) <= cap_tokens {
366 return s.to_string();
367 }
368 let cuts: Vec<usize> = s.match_indices('\n').map(|(i, _)| i).collect();
369 let (mut lo, mut hi) = (0usize, cuts.len());
370 let mut best: Option<usize> = None;
371 while lo < hi {
372 let mid = lo + (hi - lo) / 2;
373 let end = cuts[mid];
374 if end > 0 && count_tokens(&s[..end]) <= cap_tokens {
375 best = Some(end);
376 lo = mid + 1;
377 } else {
378 hi = mid;
379 }
380 }
381 if let Some(end) = best {
382 return s[..end].to_string();
383 }
384 let byte_approx = cap_tokens * 4;
385 let safe = s.floor_char_boundary(byte_approx.min(s.len()));
386 s[..safe].to_string()
387}
388
389#[must_use]
391pub fn claude_code_instructions() -> String {
392 build_instructions(CrpMode::Off)
393}
394
395fn client_loads_compression_from_file(client_name: &str) -> bool {
396 crate::core::home::resolve_home_dir().is_some_and(|home| {
397 crate::core::rules_channel::client_autoloads_compression(client_name, &home)
398 })
399}
400
401fn build_shell_hint() -> String {
402 if !cfg!(windows) {
403 return String::new();
404 }
405 let name = crate::shell::shell_name();
406 let is_posix = matches!(name.as_str(), "bash" | "sh" | "zsh" | "fish");
407 if is_posix {
408 format!("\nSHELL: {name} (POSIX) — no PowerShell cmdlets.\n")
409 } else if name.contains("powershell") || name.contains("pwsh") {
410 format!("\nSHELL: {name}. Use PowerShell cmdlets.\n")
411 } else {
412 format!("\nSHELL: {name}.\n")
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use crate::core::tokens::count_tokens;
420
421 #[test]
422 fn guidance_suffix_survives_oversized_base() {
423 let base = "SESSION LINE\n".repeat(4000);
424 let suffix = "OUTPUT STYLE: expert-terse\nFn refs only, diff lines only.";
425 let out = assemble_within_cap(&base, suffix, INSTRUCTION_CAP_TOKENS);
426 assert!(out.contains("OUTPUT STYLE: expert-terse"));
427 assert!(count_tokens(&out) <= INSTRUCTION_CAP_TOKENS);
428 assert!(out.len() < base.len());
429 }
430
431 #[test]
432 fn empty_client_never_dedups_compression() {
433 assert!(!client_loads_compression_from_file(""));
434 assert!(!client_loads_compression_from_file("totally-unknown-agent"));
435 }
436
437 #[test]
438 fn under_cap_keeps_everything() {
439 let base = "tool mapping block";
440 let suffix = "OUTPUT STYLE: dense";
441 let out = assemble_within_cap(base, suffix, INSTRUCTION_CAP_TOKENS);
442 assert!(out.contains(base));
443 assert!(out.contains(suffix));
444 }
445
446 #[test]
447 fn empty_suffix_caps_base_only() {
448 let base = "x\n".repeat(4000);
449 let out = assemble_within_cap(&base, "", INSTRUCTION_CAP_TOKENS);
450 assert!(count_tokens(&out) <= INSTRUCTION_CAP_TOKENS);
451 }
452
453 #[cfg(windows)]
454 #[test]
455 fn shell_hint_stays_within_its_budget() {
456 let hint = build_shell_hint();
457 let tokens = count_tokens(&hint);
458 assert!(
459 tokens <= STATIC_INSTRUCTION_SHELL_HINT_TOKENS,
460 "shell hint = {tokens} tok, budget {STATIC_INSTRUCTION_SHELL_HINT_TOKENS}: {hint}"
461 );
462 }
463
464 #[test]
465 fn minimal_overhead_instructions_stay_within_budget() {
466 const MINIMAL_INSTRUCTION_BUDGET_TOKENS: usize =
467 STATIC_INSTRUCTION_BUDGET_TDD_TOKENS + STATIC_INSTRUCTION_SHELL_HINT_TOKENS;
468 let _iso = crate::core::data_dir::isolated_data_dir();
469 crate::test_env::set_var("LEAN_CTX_MINIMAL", "1");
470 let out = build_instructions(CrpMode::Compact);
471 crate::test_env::remove_var("LEAN_CTX_MINIMAL");
472 let tokens = count_tokens(&out);
473 assert!(
474 tokens <= MINIMAL_INSTRUCTION_BUDGET_TOKENS,
475 "minimal-overhead instructions = {tokens} tok, budget {MINIMAL_INSTRUCTION_BUDGET_TOKENS}\n---\n{out}\n---"
476 );
477 }
478
479 #[test]
480 fn static_skeleton_stays_within_budget() {
481 let _iso = crate::core::data_dir::isolated_data_dir();
482 crate::test_env::set_var("LEAN_CTX_COMPRESSION", "off");
485 for (mode, base_budget) in [
486 (CrpMode::Off, STATIC_INSTRUCTION_BUDGET_TOKENS),
487 (CrpMode::Compact, STATIC_INSTRUCTION_BUDGET_TOKENS),
488 (CrpMode::Tdd, STATIC_INSTRUCTION_BUDGET_TDD_TOKENS),
489 ] {
490 let budget = base_budget + STATIC_INSTRUCTION_SHELL_HINT_TOKENS;
491 let out = build_instructions_for_test(mode);
492 let tokens = count_tokens(&out);
493 assert!(
494 tokens <= budget,
495 "static instructions for {mode:?} = {tokens} tok, budget {budget}\n---\n{out}\n---"
496 );
497 }
498 crate::test_env::remove_var("LEAN_CTX_COMPRESSION");
499 }
500}