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