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