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