1use crate::tools::CrpMode;
2
3const INSTRUCTION_CAP_TOKENS: usize = 1200;
13
14pub fn build_instructions(crp_mode: CrpMode) -> String {
15 build_instructions_with_client(crp_mode, "")
16}
17
18pub fn build_instructions_with_client(crp_mode: CrpMode, client_name: &str) -> String {
19 if is_claude_code_client(client_name) {
20 return build_claude_code_instructions();
21 }
22 build_full_instructions(crp_mode, client_name)
23}
24
25pub fn build_instructions_for_test(crp_mode: CrpMode) -> String {
26 build_full_instructions_for_test(crp_mode, "")
29}
30
31pub fn build_instructions_with_client_for_test(crp_mode: CrpMode, client_name: &str) -> String {
32 if is_claude_code_client(client_name) {
33 return build_claude_code_instructions();
34 }
35 build_full_instructions_for_test(crp_mode, client_name)
36}
37
38pub fn build_instructions_with_client_for_compiler(
43 crp_mode: CrpMode,
44 client_name: &str,
45 unified_tool_mode: bool,
46) -> String {
47 if is_claude_code_client(client_name) {
48 return build_claude_code_instructions();
49 }
50 build_full_instructions_for_compiler(crp_mode, client_name, unified_tool_mode)
51}
52
53fn is_claude_code_client(client_name: &str) -> bool {
54 let lower = client_name.to_lowercase();
55 lower.contains("claude") && !lower.contains("cursor")
56}
57
58fn rotate_wakeup_manifest(session: &crate::core::session::SessionState, profile_name: &str) {
65 use crate::core::litm_calibration::{record_outcome, Position};
66 use crate::core::session::ManifestEntry;
67
68 let mut updated = session.clone();
69
70 for entry in &updated.wakeup_manifest {
71 if !entry.missed {
72 if let Some(pos) = Position::parse(&entry.position) {
73 record_outcome(&entry.profile, pos, true);
74 }
75 }
76 }
77
78 let mut manifest: Vec<ManifestEntry> = Vec::new();
79 let mut push = |key: &str, position: &str| {
80 let key = key.trim();
81 if !key.is_empty() {
82 manifest.push(ManifestEntry {
83 key: key.chars().take(80).collect(),
84 position: position.to_string(),
85 profile: profile_name.to_string(),
86 missed: false,
87 });
88 }
89 };
90
91 if let Some(ref task) = updated.task {
92 push(&task.description, "begin");
93 }
94 for d in updated.decisions.iter().rev().take(5) {
95 push(&d.summary, "begin");
96 }
97 for f in updated.findings.iter().rev().take(8) {
98 push(&f.summary, "end");
99 }
100 for n in updated.next_steps.iter().take(3) {
101 push(n, "end");
102 }
103
104 updated.wakeup_manifest = manifest;
105 let _ = updated.save();
106}
107
108pub fn claude_config_dir_display() -> String {
109 match std::env::var("CLAUDE_CONFIG_DIR") {
110 Ok(dir) if !dir.trim().is_empty() => {
111 let dir = dir.trim().to_string();
112 if dir.starts_with('~') {
113 dir
114 } else if let Some(home) = dirs::home_dir() {
115 let home_str = home.to_string_lossy();
116 if let Some(rest) = dir.strip_prefix(home_str.as_ref()) {
117 format!("~{rest}")
118 } else {
119 dir
120 }
121 } else {
122 dir
123 }
124 }
125 _ => "~/.claude".to_string(),
126 }
127}
128
129fn build_claude_code_instructions() -> String {
130 let shell_hint = build_shell_hint();
131 let config_dir = claude_config_dir_display();
132
133 let session_block = match crate::core::session::SessionState::load_latest() {
135 Some(session) => {
136 let mut parts = Vec::new();
137 if let Some(ref task) = session.task {
138 let pct = task
139 .progress_pct
140 .map_or(String::new(), |p| format!(" [{p}%]"));
141 parts.push(format!("Task: {}{pct}", task.description));
142 }
143 if !session.decisions.is_empty() {
144 let items: Vec<&str> = session
145 .decisions
146 .iter()
147 .rev()
148 .take(3)
149 .map(|d| d.summary.as_str())
150 .collect();
151 parts.push(format!("Decisions: {}", items.join("; ")));
152 }
153 if !session.files_touched.is_empty() {
154 let modified: Vec<&str> = session
155 .files_touched
156 .iter()
157 .filter(|f| f.modified)
158 .take(5)
159 .map(|f| f.path.as_str())
160 .collect();
161 if !modified.is_empty() {
162 parts.push(format!("Modified: {}", modified.join(", ")));
163 }
164 }
165 if !session.findings.is_empty() {
166 let recent: Vec<&str> = session
167 .findings
168 .iter()
169 .rev()
170 .take(3)
171 .map(|f| f.summary.as_str())
172 .collect();
173 parts.push(format!("Recent: {}", recent.join("; ")));
174 }
175 if parts.is_empty() {
176 String::new()
177 } else {
178 format!("\n\n--- SESSION ---\n{}\n---", parts.join("\n"))
179 }
180 }
181 None => String::new(),
182 };
183
184 let cfg = crate::core::config::Config::load();
185 let shadow_preamble = if cfg.shadow_mode {
186 "SHADOW MODE ACTIVE: ALL reads/searches/shell MUST use ctx_* tools. Native equivalents are intercepted.\n\n"
187 } else {
188 ""
189 };
190
191 let instr = format!("\
192{shadow_preamble}\
193ALWAYS use lean-ctx MCP tools instead of native equivalents.
194
195Tool mapping (MANDATORY):
196• Read/cat/head/tail -> ctx_read(path, mode)
197• Shell/bash -> ctx_shell(command)
198• Grep/rg -> ctx_search(pattern, path)
199• ls/find -> ctx_tree(path, depth)
200• Edit/StrReplace -> native (lean-ctx=READ only). If Edit needs Read and Read is unavailable, use ctx_edit.
201• Write, Delete, Glob -> normal. NEVER loop on Edit failures — use ctx_edit.
202
203ctx_read modes: full|map|signatures|diff|task|reference|aggressive|entropy|lines:N-M
204Auto-selects mode. Re-reads ~13 tok. File refs F1,F2.. persist.
205
206Workflow: Orient(ctx_overview) → Locate(ctx_search) → Read(ctx_read) → Edit(ctx_edit/native) → Verify(ctx_read diff + ctx_shell test) → Record(ctx_knowledge).
207
208CEP: 1.ACT FIRST 2.DELTA ONLY 3.STRUCTURED(+/-/~) 4.ONE LINE 5.QUALITY
209{shell_hint}\
210Prefer: ctx_read>Read | ctx_shell>Shell | ctx_search>Grep | ctx_tree>ls
211Edit: native Edit/StrReplace preferred, ctx_edit if Edit unavailable.
212Never echo tool output. Never narrate. Show only changed code.
213Full instructions at {config_dir}/CLAUDE.md (imports rules/lean-ctx.md){session_block}");
214
215 instr
216}
217
218fn build_full_instructions(crp_mode: CrpMode, client_name: &str) -> String {
219 let cfg = crate::core::config::Config::load();
220 let minimal = cfg.minimal_overhead_effective_for_client(client_name);
221
222 let profile = crate::core::litm::LitmProfile::from_client_name(client_name);
223 let loaded_session = if minimal {
224 None
225 } else {
226 crate::core::session::SessionState::load_latest()
227 };
228
229 let (session_block, litm_end_block) = match loaded_session {
230 Some(ref session) => {
231 rotate_wakeup_manifest(session, profile.name);
235 let share = crate::core::litm_calibration::begin_share(profile.name);
236 let positioned = crate::core::litm::position_optimize_with_share(session, share);
237 let begin = format!(
238 "\n\n--- ACTIVE SESSION (LITM P1: begin position, profile: {}) ---\n{}\n---\n",
239 profile.name, positioned.begin_block
240 );
241 let end = if positioned.end_block.is_empty() {
242 String::new()
243 } else {
244 format!(
245 "\n--- SESSION RESUME (post-compaction) ---\n{}\n---\n",
246 positioned.end_block
247 )
248 };
249 (begin, end)
250 }
251 None => (String::new(), String::new()),
252 };
253
254 let project_root_for_blocks = if minimal {
255 None
256 } else {
257 loaded_session
258 .as_ref()
259 .and_then(|s| s.project_root.clone())
260 .or_else(|| {
261 std::env::current_dir()
262 .ok()
263 .map(|p| p.to_string_lossy().to_string())
264 })
265 };
266
267 let knowledge_block = match &project_root_for_blocks {
268 Some(root) => {
269 let knowledge = crate::core::knowledge::ProjectKnowledge::load(root);
270 match knowledge {
271 Some(k) if !k.facts.is_empty() || !k.patterns.is_empty() => {
272 let aaak = k.format_aaak();
273 if aaak.is_empty() {
274 String::new()
275 } else {
276 format!("\n--- PROJECT MEMORY (AAAK) ---\n{}\n---\n", aaak.trim())
277 }
278 }
279 _ => String::new(),
280 }
281 }
282 None => String::new(),
283 };
284
285 let gotcha_block = match &project_root_for_blocks {
286 Some(root) => {
287 let store = crate::core::gotcha_tracker::GotchaStore::load(root);
288 let files: Vec<String> = loaded_session
289 .as_ref()
290 .map(|s| s.files_touched.iter().map(|ft| ft.path.clone()).collect())
291 .unwrap_or_default();
292 let block = store.format_injection_block(&files);
293 if block.is_empty() {
294 String::new()
295 } else {
296 format!("\n{block}\n")
297 }
298 }
299 None => String::new(),
300 };
301
302 let shell_hint = build_shell_hint();
303
304 use crate::core::rules_canonical as rc;
305 let tool_bullets = rc::tool_mapping_bullets(rc::Mode::Mcp);
306 let compat = rc::compatibility_block();
307 let read_modes = rc::ctx_read_modes_block();
308 let auto_block = rc::automation_block();
309 let cep = rc::cep_block();
310 let litm_pref = rc::litm_end_block(rc::Mode::Mcp);
311
312 let shadow_preamble = if cfg.shadow_mode {
313 "SHADOW MODE ACTIVE: ALL file reads, searches, and shell commands MUST go through ctx_* tools. \
314 Native Read/Grep/Shell are intercepted and redirected — using ctx_* directly is faster and more reliable.\n\n"
315 } else {
316 ""
317 };
318
319 let mut base = format!(
320 "\
321{shadow_preamble}\
322CRITICAL: ALWAYS use lean-ctx MCP tools instead of native equivalents for token savings.\n\
323\n\
324{tool_bullets}\n\
325\n\
326{compat}\n\
327{shell_hint}\
328\n\
329{read_modes}\n\
330\n\
331{auto_block}\n\
332\n\
333{cep}\n\
334\n\
335{decoder_block}\n\
336\n\
337{session_block}\
338{knowledge_block}\
339{gotcha_block}\
340\n\
341--- ORIGIN ---\n\
342{origin}\n\
343\n\
344{litm_pref}\
345{litm_end_block}",
346 decoder_block = crate::core::protocol::instruction_decoder_block(),
347 origin = crate::core::integrity::origin_line(),
348 litm_end_block = &litm_end_block
349 );
350
351 if should_use_unified(client_name) {
352 base.push_str("\n\n");
353 base.push_str(rc::unified_tool_mode_block());
354 base.push('\n');
355 }
356
357 let intelligence_block = build_intelligence_block();
358 let terse_block = build_terse_agent_block_for_client(&crp_mode, client_name);
359
360 let guidance_suffix = match crp_mode {
366 CrpMode::Off => format!("{terse_block}{intelligence_block}"),
367 CrpMode::Compact => format!(
368 "CRP MODE: compact\n\
369Omit filler. Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
370Diff lines (+/-) only. TARGET: <=200 tok. Trust tool outputs.\n\n\
371{terse_block}{intelligence_block}"
372 ),
373 CrpMode::Tdd => format!(
374 "CRP MODE: tdd\n\
375Max density. Every token carries meaning. Fn refs only, diff lines (+/-) only.\n\
376Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
377+F1:42 param(timeout:Duration) | -F1:10-15 | ~F1:42 old->new\n\
378BUDGET: <=150 tok. ZERO NARRATION. Trust tool outputs.\n\n\
379{terse_block}{intelligence_block}"
380 ),
381 };
382
383 assemble_within_cap(&base, &guidance_suffix, INSTRUCTION_CAP_TOKENS)
384}
385
386fn 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 {
411 return truncate_to_token_cap(&full, cap_tokens);
412 };
413 let trimmed_base = truncate_to_token_cap(base, base_budget);
414 format!("{trimmed_base}\n\n{suffix}")
415}
416
417fn truncate_to_token_cap(s: &str, cap_tokens: usize) -> String {
418 use crate::core::tokens::count_tokens;
419 if count_tokens(s) <= cap_tokens {
420 return s.to_string();
421 }
422 let cuts: Vec<usize> = s.match_indices('\n').map(|(i, _)| i).collect();
429 let (mut lo, mut hi) = (0usize, cuts.len());
430 let mut best: Option<usize> = None;
431 while lo < hi {
432 let mid = lo + (hi - lo) / 2;
433 let end = cuts[mid];
434 if end > 0 && count_tokens(&s[..end]) <= cap_tokens {
435 best = Some(end);
436 lo = mid + 1;
437 } else {
438 hi = mid;
439 }
440 }
441 if let Some(end) = best {
442 return s[..end].to_string();
443 }
444 let byte_approx = cap_tokens * 4;
446 let safe = s.floor_char_boundary(byte_approx.min(s.len()));
447 s[..safe].to_string()
448}
449
450fn build_full_instructions_for_test(crp_mode: CrpMode, client_name: &str) -> String {
451 use crate::core::rules_canonical as rc;
452 let shell_hint = build_shell_hint();
453 let session_block = String::new();
454 let knowledge_block = String::new();
455 let gotcha_block = String::new();
456 let litm_end_block = String::new();
457
458 let tool_bullets = rc::tool_mapping_bullets(rc::Mode::Mcp);
459 let compat = rc::compatibility_block();
460 let read_modes = rc::ctx_read_modes_block();
461 let auto_block = rc::automation_block();
462 let cep = rc::cep_block();
463 let litm_pref = rc::litm_end_block(rc::Mode::Mcp);
464
465 let mut base = format!(
466 "\
467CRITICAL: ALWAYS use lean-ctx MCP tools instead of native equivalents for token savings.\n\
468\n\
469{tool_bullets}\n\
470\n\
471{compat}\n\
472{shell_hint}\
473\n\
474{read_modes}\n\
475\n\
476{auto_block}\n\
477\n\
478{cep}\n\
479\n\
480{decoder_block}\n\
481\n\
482{session_block}\
483{knowledge_block}\
484{gotcha_block}\
485\n\
486--- ORIGIN ---\n\
487{origin}\n\
488\n\
489{litm_pref}\
490{litm_end_block}",
491 decoder_block = crate::core::protocol::instruction_decoder_block(),
492 origin = crate::core::integrity::origin_line(),
493 litm_end_block = &litm_end_block
494 );
495
496 if should_use_unified(client_name) {
497 base.push_str("\n\n");
498 base.push_str(rc::unified_tool_mode_block());
499 base.push('\n');
500 }
501
502 let intelligence_block = build_intelligence_block();
503 let terse_block = build_terse_agent_block_for_client(&crp_mode, client_name);
504
505 match crp_mode {
506 CrpMode::Off => format!("{base}\n\n{terse_block}{intelligence_block}"),
507 CrpMode::Compact => {
508 format!(
509 "{base}\n\n\
510CRP MODE: compact\n\
511Omit filler. Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
512Diff lines (+/-) only. TARGET: <=200 tok. Trust tool outputs.\n\n\
513{terse_block}{intelligence_block}"
514 )
515 }
516 CrpMode::Tdd => {
517 format!(
518 "{base}\n\n\
519CRP MODE: tdd\n\
520Max density. Every token carries meaning. Fn refs only, diff lines (+/-) only.\n\
521Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
522+F1:42 param(timeout:Duration) | -F1:10-15 | ~F1:42 old->new\n\
523BUDGET: <=150 tok. ZERO NARRATION. Trust tool outputs.\n\n\
524{terse_block}{intelligence_block}"
525 )
526 }
527 }
528}
529
530fn build_full_instructions_for_compiler(
531 crp_mode: CrpMode,
532 client_name: &str,
533 unified_tool_mode: bool,
534) -> String {
535 let shell_hint = build_shell_hint();
536 let session_block = String::new();
537 let knowledge_block = String::new();
538 let gotcha_block = String::new();
539 let litm_end_block = String::new();
540
541 use crate::core::rules_canonical as rc;
542 let tool_bullets = rc::tool_mapping_bullets(rc::Mode::Mcp);
543 let compat = rc::compatibility_block();
544 let read_modes = rc::ctx_read_modes_block();
545 let auto_blk = rc::automation_block();
546 let cep = rc::cep_block();
547 let litm_pref = rc::litm_end_block(rc::Mode::Mcp);
548
549 let mut base = format!(
550 "\
551CRITICAL: ALWAYS use lean-ctx MCP tools instead of native equivalents for token savings.\n\
552\n\
553{tool_bullets}\n\
554\n\
555{compat}\n\
556{shell_hint}\
557\n\
558{read_modes}\n\
559\n\
560{auto_blk}\n\
561\n\
562{cep}\n\
563\n\
564{decoder_block}\n\
565\n\
566{session_block}\
567{knowledge_block}\
568{gotcha_block}\
569\n\
570--- ORIGIN ---\n\
571{origin}\n\
572\n\
573{litm_pref}\
574{litm_end_block}",
575 decoder_block = crate::core::protocol::instruction_decoder_block(),
576 origin = crate::core::integrity::origin_line(),
577 litm_end_block = &litm_end_block
578 );
579
580 if unified_tool_mode {
581 base.push_str("\n\n");
582 base.push_str(rc::unified_tool_mode_block());
583 base.push('\n');
584 }
585
586 let _ = client_name; let intelligence_block = build_intelligence_block();
588
589 match crp_mode {
590 CrpMode::Off => format!("{base}\n\n{intelligence_block}"),
591 CrpMode::Compact => {
592 format!(
593 "{base}\n\n\
594CRP MODE: compact\n\
595Omit filler. Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
596Diff lines (+/-) only. TARGET: <=200 tok. Trust tool outputs.\n\n\
597{intelligence_block}"
598 )
599 }
600 CrpMode::Tdd => {
601 format!(
602 "{base}\n\n\
603CRP MODE: tdd\n\
604Max density. Every token carries meaning. Fn refs only, diff lines (+/-) only.\n\
605Abbreviate: fn,cfg,impl,deps,req,res,ctx,err,ret,arg,val,ty,mod.\n\
606+F1:42 param(timeout:Duration) | -F1:10-15 | ~F1:42 old->new\n\
607BUDGET: <=150 tok. ZERO NARRATION. Trust tool outputs.\n\n\
608{intelligence_block}"
609 )
610 }
611 }
612}
613
614pub fn claude_code_instructions() -> String {
615 build_claude_code_instructions()
616}
617
618fn build_terse_agent_block_for_client(_crp_mode: &CrpMode, client_name: &str) -> String {
619 use crate::core::config::{CompressionLevel, Config};
620 let cfg = Config::load();
621 let compression = CompressionLevel::effective(&cfg);
622 if compression.is_active() {
623 let persona = crate::core::persona::Persona::resolve(&cfg);
624 return crate::core::terse::agent_prompts::build_prompt_block_for_persona(
625 &compression,
626 client_name,
627 &persona,
628 );
629 }
630 String::new()
631}
632
633fn build_intelligence_block() -> String {
634 "\
635OUTPUT EFFICIENCY:\n\
636• Never echo tool output code. Never add narration comments. Show only changed code.\n\
637• [TASK:type] and SCOPE hints included. Architecture=thorough, generate=code."
638 .to_string()
639}
640
641fn build_shell_hint() -> String {
642 if !cfg!(windows) {
643 return String::new();
644 }
645 let name = crate::shell::shell_name();
646 let is_posix = matches!(name.as_str(), "bash" | "sh" | "zsh" | "fish");
647 if is_posix {
648 format!(
649 "\nSHELL: {name} (POSIX). Use POSIX commands (cat, head, grep, find, ls). \
650 Do NOT use PowerShell cmdlets (Get-Content, Select-Object, Get-ChildItem).\n"
651 )
652 } else if name.contains("powershell") || name.contains("pwsh") {
653 format!("\nSHELL: {name}. Use PowerShell cmdlets.\n")
654 } else {
655 format!("\nSHELL: {name}.\n")
656 }
657}
658
659fn should_use_unified(client_name: &str) -> bool {
660 if std::env::var("LEAN_CTX_FULL_TOOLS").is_ok() {
661 return false;
662 }
663 if std::env::var("LEAN_CTX_UNIFIED").is_ok() {
664 return true;
665 }
666 let _ = client_name;
667 false
668}
669
670#[cfg(test)]
671mod tests {
672 use super::*;
673 use crate::core::tokens::count_tokens;
674
675 #[test]
676 fn guidance_suffix_survives_oversized_base() {
677 let base = "SESSION LINE\n".repeat(4000);
679 let suffix = "OUTPUT STYLE: expert-terse\nFn refs only, diff lines only.";
680 let out = assemble_within_cap(&base, suffix, INSTRUCTION_CAP_TOKENS);
681
682 assert!(
683 out.contains("OUTPUT STYLE: expert-terse"),
684 "protected guidance suffix must survive truncation"
685 );
686 assert!(
687 count_tokens(&out) <= INSTRUCTION_CAP_TOKENS,
688 "assembled output must respect the token cap"
689 );
690 assert!(
691 out.len() < base.len(),
692 "oversized base must have been truncated"
693 );
694 }
695
696 #[test]
697 fn under_cap_keeps_everything() {
698 let base = "tool mapping block";
699 let suffix = "OUTPUT STYLE: dense";
700 let out = assemble_within_cap(base, suffix, INSTRUCTION_CAP_TOKENS);
701 assert!(out.contains(base));
702 assert!(out.contains(suffix));
703 }
704
705 #[test]
706 fn empty_suffix_caps_base_only() {
707 let base = "x\n".repeat(4000);
708 let out = assemble_within_cap(&base, "", INSTRUCTION_CAP_TOKENS);
709 assert!(count_tokens(&out) <= INSTRUCTION_CAP_TOKENS);
710 }
711}