1use std::path::Path;
49
50const INSTRUCTION_FILES: [&str; 2] = ["CLAUDE.md", "AGENTS.md"];
54
55pub const MAX_INSTRUCTIONS_BYTES: usize = 64_000;
76
77const MAX_NESTED_INSTRUCTIONS_BYTES: usize = 12_000;
81const MAX_NESTED_FILE_BYTES: usize = 6_000;
82
83const MAX_NESTED_FILES: usize = 8;
86
87const MIN_USEFUL_NESTED_BYTES: usize = 400;
91
92pub const MAX_KNOWLEDGE_BYTES: usize = 8_000;
94
95const MAX_KNOWLEDGE_ENTRIES: usize = 40;
97
98pub fn agent_instructions(worktree: &Path) -> Option<(String, String)> {
103 for name in INSTRUCTION_FILES {
104 let path = worktree.join(name);
105 let Ok(raw) = std::fs::read_to_string(&path) else {
106 continue;
107 };
108 if raw.trim().is_empty() {
109 continue;
110 }
111 return Some((
112 name.to_string(),
113 truncate_note(&raw, MAX_INSTRUCTIONS_BYTES),
114 ));
115 }
116 None
117}
118
119pub fn nested_instructions(worktree: &Path) -> Option<String> {
162 let paths = tracked_instruction_files(worktree)?;
163 if paths.is_empty() {
164 return None;
165 }
166
167 let mut out = String::new();
168 let mut used = 0usize;
169 let mut deferred: Vec<String> = Vec::new();
170
171 for (i, rel) in paths.iter().enumerate() {
172 let dir = rel.rsplit_once('/').map(|(d, _)| d).unwrap_or(".");
173 let over_budget = i >= MAX_NESTED_FILES || used >= MAX_NESTED_INSTRUCTIONS_BYTES;
174 let raw = if over_budget {
175 String::new()
176 } else {
177 std::fs::read_to_string(worktree.join(rel)).unwrap_or_default()
178 };
179 if over_budget || raw.trim().is_empty() {
180 if over_budget {
181 deferred.push(rel.clone());
182 }
183 continue;
184 }
185 let remaining = MAX_NESTED_INSTRUCTIONS_BYTES.saturating_sub(used);
186 if remaining < MIN_USEFUL_NESTED_BYTES && raw.trim().len() > remaining {
191 deferred.push(rel.clone());
192 continue;
193 }
194 let body = truncate_note(raw.trim(), MAX_NESTED_FILE_BYTES.min(remaining));
195 used += body.len();
196 out.push_str(&format!(
197 "--- {rel} — applies to everything under `{dir}/` ---\n{body}\n\n"
198 ));
199 }
200
201 if !deferred.is_empty() {
202 out.push_str(
203 "Not shown, over budget. Read the file before editing anything under its \
204 directory:\n",
205 );
206 for rel in &deferred {
207 out.push_str(&format!("- {rel}\n"));
208 }
209 }
210
211 let trimmed = out.trim();
212 (!trimmed.is_empty()).then(|| trimmed.to_string())
213}
214
215fn tracked_instruction_files(worktree: &Path) -> Option<Vec<String>> {
220 let out = std::process::Command::new("git")
221 .arg("-C")
222 .arg(worktree)
223 .args(["ls-files", "-z", "--", "*CLAUDE.md", "*AGENTS.md"])
224 .output()
225 .ok()?;
226 if !out.status.success() {
227 return None;
228 }
229 let mut paths: Vec<String> = String::from_utf8_lossy(&out.stdout)
230 .split('\0')
231 .filter(|p| !p.is_empty())
232 .filter(|p| p.contains('/'))
236 .filter(|p| {
237 let name = p.rsplit('/').next().unwrap_or(p);
238 INSTRUCTION_FILES.contains(&name)
239 })
240 .map(str::to_string)
241 .collect();
242 paths.sort_by_key(|p| (p.matches('/').count(), p.clone()));
245 Some(paths)
246}
247
248pub fn dot_car_knowledge(worktree: &Path) -> Option<String> {
250 let car_dir = car_memgine::project::discover_project(worktree)?;
251 let project = car_memgine::project::load_project(&car_dir).ok()?;
252
253 let mut out = String::new();
254 if let Some(identity) = project.identity.as_deref().map(str::trim) {
255 if !identity.is_empty() {
256 out.push_str(identity);
257 out.push_str("\n\n");
258 }
259 }
260 if !project.knowledge.is_empty() {
261 out.push_str("Recorded project knowledge:\n");
262 for entry in project.knowledge.iter().take(MAX_KNOWLEDGE_ENTRIES) {
263 let kind = if entry.entry_type.is_empty() {
264 "note"
265 } else {
266 &entry.entry_type
267 };
268 out.push_str(&format!("- [{kind}] {}", entry.fact.trim()));
269 let recommendation = entry.recommendation.trim();
270 if !recommendation.is_empty() {
271 out.push_str(&format!(" — {recommendation}"));
272 }
273 out.push('\n');
274 }
275 }
276 let trimmed = out.trim();
277 (!trimmed.is_empty()).then(|| truncate_note(trimmed, MAX_KNOWLEDGE_BYTES))
278}
279
280pub fn project_context(worktree: &Path) -> Option<String> {
290 let instructions = agent_instructions(worktree);
291 let nested = nested_instructions(worktree);
292 let knowledge = dot_car_knowledge(worktree);
293 let skills = instructions
296 .is_some()
297 .then(|| available_skills(worktree))
298 .flatten();
299 if instructions.is_none() && nested.is_none() && knowledge.is_none() {
300 return None;
301 }
302
303 let mut out = String::new();
304 if let Some((name, body)) = instructions {
305 out.push_str(&format!(
306 "PROJECT INSTRUCTIONS (from {name}, written by this repository's maintainers).\n\
307 These are review-time rules. Your outcome contract does NOT check them, so a \
308 diff can pass every check and still be rejected for breaking one. Follow them \
309 as constraints on HOW you implement, and never weaken or edit a contract check \
310 to satisfy one — the contract decides whether the work is done, these decide \
311 whether it is acceptable. Where a rule points at another document you have not \
312 been given, say so in your summary rather than guessing at its contents.\n\n\
313 {body}\n"
314 ));
315 }
316 if let Some(nested) = nested {
317 out.push_str(&format!(
318 "\nDIRECTORY-SCOPED INSTRUCTIONS.\n\
319 Each block below governs one subtree and carries the same weight as the \
320 instructions above while you are working inside it. Where a scoped rule is \
321 stricter than a root one, the scoped rule wins for that subtree.\n\n{nested}\n"
322 ));
323 }
324 if let Some(skills) = skills {
325 out.push_str(&format!("\nPROJECT SKILLS.\n{skills}\n"));
326 }
327 if let Some(knowledge) = knowledge {
328 if !out.is_empty() {
329 out.push('\n');
330 }
331 out.push_str(&format!(
332 "PROJECT KNOWLEDGE (from .car/, recorded by the team).\n\n{knowledge}\n"
333 ));
334 }
335 Some(out)
336}
337
338pub fn available_skills(worktree: &Path) -> Option<String> {
353 let skills_dir = worktree.join(".claude").join("skills");
354 let mut entries: Vec<(String, String)> = std::fs::read_dir(&skills_dir)
355 .ok()?
356 .flatten()
357 .filter_map(|entry| {
358 let manifest = entry.path().join("SKILL.md");
359 let raw = std::fs::read_to_string(&manifest).ok()?;
360 let (name, description) = parse_frontmatter(&raw)?;
361 let rel = format!(
362 ".claude/skills/{}/SKILL.md",
363 entry.file_name().to_string_lossy()
364 );
365 Some((rel, format!("**{name}** — {description}")))
366 })
367 .collect();
368 if entries.is_empty() {
369 return None;
370 }
371 entries.sort();
372
373 let mut out = String::from(
374 "The instructions above delegate to these skill documents. They are files in this \
375 worktree: when your work touches an area one of them covers, read it with \
376 `read_file` BEFORE editing, rather than guessing at what it says.\n\n",
377 );
378 for (path, summary) in entries {
379 out.push_str(&format!("- `{path}` — {summary}\n"));
380 }
381 Some(truncate_note(out.trim(), MAX_KNOWLEDGE_BYTES))
382}
383
384fn parse_frontmatter(raw: &str) -> Option<(String, String)> {
392 let body = raw.strip_prefix("---")?;
393 let end = body.find("\n---")?;
394 let block = &body[..end];
395
396 let mut name = None;
397 let mut description: Option<String> = None;
398 let mut in_description = false;
399 for line in block.lines() {
400 if let Some(rest) = line.strip_prefix("name:") {
401 name = Some(rest.trim().to_string());
402 in_description = false;
403 } else if let Some(rest) = line.strip_prefix("description:") {
404 let first = rest.trim().trim_start_matches(['>', '|', '-']).trim();
405 description = Some(first.to_string());
406 in_description = true;
407 } else if in_description {
408 let indented = line.starts_with(' ') || line.starts_with('\t');
409 if indented && !line.trim().is_empty() {
410 let existing = description.get_or_insert_with(String::new);
411 if !existing.is_empty() {
412 existing.push(' ');
413 }
414 existing.push_str(line.trim());
415 } else if !line.trim().is_empty() {
416 in_description = false;
417 }
418 }
419 }
420 let name = name.filter(|n| !n.is_empty())?;
421 let description = description
422 .map(|d| first_sentence(&d))
423 .filter(|d| !d.is_empty())?;
424 Some((name, description))
425}
426
427fn first_sentence(text: &str) -> String {
430 match text.find(". ") {
431 Some(i) => text[..=i].trim().to_string(),
432 None => text.trim().to_string(),
433 }
434}
435
436fn truncate_note(text: &str, max: usize) -> String {
438 if text.len() <= max {
439 return text.to_string();
440 }
441 let mut end = max;
442 while end > 0 && !text.is_char_boundary(end) {
443 end -= 1;
444 }
445 format!(
446 "{}\n\n[truncated: {} of {} bytes shown]",
447 &text[..end],
448 end,
449 text.len()
450 )
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456
457 #[test]
458 fn reads_claude_md_and_prefers_it_over_agents_md() {
459 let dir = tempfile::tempdir().unwrap();
460 std::fs::write(dir.path().join("CLAUDE.md"), "no feature flags, ever").unwrap();
461 std::fs::write(dir.path().join("AGENTS.md"), "something else").unwrap();
462 let (name, body) = agent_instructions(dir.path()).expect("instructions found");
463 assert_eq!(name, "CLAUDE.md");
464 assert!(body.contains("no feature flags"));
465 }
466
467 #[test]
468 fn falls_back_to_agents_md() {
469 let dir = tempfile::tempdir().unwrap();
470 std::fs::write(dir.path().join("AGENTS.md"), "house rules").unwrap();
471 let (name, _) = agent_instructions(dir.path()).expect("instructions found");
472 assert_eq!(name, "AGENTS.md");
473 }
474
475 #[test]
476 fn an_empty_instruction_file_is_not_instructions() {
477 let dir = tempfile::tempdir().unwrap();
478 std::fs::write(dir.path().join("CLAUDE.md"), " \n\n").unwrap();
479 assert!(agent_instructions(dir.path()).is_none());
480 }
481
482 #[test]
483 fn a_repo_with_nothing_yields_no_block() {
484 let dir = tempfile::tempdir().unwrap();
485 assert!(project_context(dir.path()).is_none());
486 }
487
488 #[test]
489 fn the_block_says_the_contract_does_not_check_these_rules() {
490 let dir = tempfile::tempdir().unwrap();
493 std::fs::write(dir.path().join("CLAUDE.md"), "rule one").unwrap();
494 let block = project_context(dir.path()).expect("block");
495 assert!(block.contains("does NOT check them"));
496 assert!(block.contains("never weaken or edit a contract check"));
497 assert!(block.contains("rule one"));
498 }
499
500 #[test]
501 fn truncation_is_announced_not_silent() {
502 let dir = tempfile::tempdir().unwrap();
503 let huge = "x".repeat(MAX_INSTRUCTIONS_BYTES + 500);
504 std::fs::write(dir.path().join("CLAUDE.md"), &huge).unwrap();
505 let (_, body) = agent_instructions(dir.path()).expect("instructions");
506 assert!(
507 body.contains("[truncated:"),
508 "a silent cut hides missing rules"
509 );
510 assert!(body.len() < huge.len() + 200);
511 }
512
513 #[test]
514 fn truncation_lands_on_a_char_boundary() {
515 let text = "é".repeat(100);
517 let cut = truncate_note(&text, 51);
518 assert!(cut.contains("[truncated:"));
519 }
520
521 fn write_skill(root: &std::path::Path, dir: &str, frontmatter: &str) {
522 let d = root.join(".claude").join("skills").join(dir);
523 std::fs::create_dir_all(&d).unwrap();
524 std::fs::write(d.join("SKILL.md"), frontmatter).unwrap();
525 }
526
527 #[test]
528 fn skills_are_indexed_by_name_and_first_sentence() {
529 let dir = tempfile::tempdir().unwrap();
530 std::fs::write(dir.path().join("CLAUDE.md"), "see the skills").unwrap();
531 write_skill(
532 dir.path(),
533 "car-bindings-api",
534 "---\nname: car-bindings-api\ndescription: >-\n The complete CAR bindings API\n surface. Use it whenever work touches the FFI boundary.\n---\nbody text here\n",
535 );
536 let block = project_context(dir.path()).expect("block");
537 assert!(block.contains("car-bindings-api"));
538 assert!(block.contains("The complete CAR bindings API surface."));
539 assert!(block.contains(".claude/skills/car-bindings-api/SKILL.md"));
541 assert!(block.contains("read_file"));
542 assert!(!block.contains("body text here"));
544 }
545
546 #[test]
547 fn a_folded_description_is_joined_not_truncated_at_the_newline() {
548 let (name, desc) = parse_frontmatter(
549 "---\nname: thing\ndescription: >-\n first part\n second part. Rest.\n---\n",
550 )
551 .expect("parsed");
552 assert_eq!(name, "thing");
553 assert_eq!(desc, "first part second part.");
554 }
555
556 #[test]
557 fn a_skill_without_usable_frontmatter_is_skipped_not_guessed() {
558 let dir = tempfile::tempdir().unwrap();
559 std::fs::write(dir.path().join("CLAUDE.md"), "rules").unwrap();
560 write_skill(dir.path(), "broken", "no frontmatter at all\n");
561 write_skill(
562 dir.path(),
563 "good",
564 "---\nname: good\ndescription: Does a thing.\n---\n",
565 );
566 let block = project_context(dir.path()).expect("block");
567 assert!(block.contains("good"));
568 assert!(!block.contains("broken"));
569 }
570
571 #[test]
572 fn skills_are_not_volunteered_without_instructions_that_delegate() {
573 let dir = tempfile::tempdir().unwrap();
574 write_skill(
575 dir.path(),
576 "lonely",
577 "---\nname: lonely\ndescription: Nobody points here.\n---\n",
578 );
579 assert!(project_context(dir.path()).is_none());
581 }
582
583 #[test]
584 fn dot_car_knowledge_is_rendered_with_recommendations() {
585 let dir = tempfile::tempdir().unwrap();
586 let car = dir.path().join(".car");
587 std::fs::create_dir_all(car.join("knowledge")).unwrap();
588 std::fs::write(car.join("identity.md"), "The CAR runtime.").unwrap();
589 std::fs::write(
590 car.join("knowledge").join("gotchas.jsonl"),
591 r#"{"id":"g1","type":"gotcha","fact":"cargo config follows cwd","recommendation":"run from car-rs"}"#,
592 )
593 .unwrap();
594 let rendered = dot_car_knowledge(dir.path()).expect("knowledge loaded");
595 assert!(rendered.contains("The CAR runtime."));
596 assert!(rendered.contains("cargo config follows cwd"));
597 assert!(rendered.contains("run from car-rs"));
598 assert!(rendered.contains("[gotcha]"));
599 }
600
601 #[test]
602 fn discovery_walks_up_from_a_nested_worktree() {
603 let dir = tempfile::tempdir().unwrap();
606 std::fs::create_dir_all(dir.path().join(".car")).unwrap();
607 std::fs::write(dir.path().join(".car").join("identity.md"), "root project").unwrap();
608 let nested = dir.path().join("crates").join("thing");
609 std::fs::create_dir_all(&nested).unwrap();
610 let rendered = dot_car_knowledge(&nested).expect("found by walking up");
611 assert!(rendered.contains("root project"));
612 }
613
614 fn repo_with(files: &[(&str, &str)]) -> tempfile::TempDir {
619 let dir = tempfile::tempdir().unwrap();
620 let git = |args: &[&str]| {
621 let ok = std::process::Command::new("git")
622 .arg("-C")
623 .arg(dir.path())
624 .args(args)
625 .output()
626 .unwrap()
627 .status
628 .success();
629 assert!(ok, "git {args:?} failed");
630 };
631 git(&["init", "-q"]);
632 git(&["config", "user.email", "t@example.com"]);
633 git(&["config", "user.name", "t"]);
634 for (rel, body) in files {
635 let path = dir.path().join(rel);
636 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
637 std::fs::write(&path, body).unwrap();
638 }
639 git(&["add", "-A"]);
640 git(&["commit", "-qm", "seed"]);
641 dir
642 }
643
644 #[test]
645 fn a_nested_instruction_file_is_inlined_with_the_directory_it_governs() {
646 let dir = repo_with(&[
647 ("CLAUDE.md", "root rules"),
648 ("crates/napi/CLAUDE.md", "do not reintroduce the five bugs"),
649 ]);
650
651 let nested = nested_instructions(dir.path()).expect("a nested file is found");
652 assert!(
653 nested.contains("do not reintroduce the five bugs"),
654 "the nested rule must be inlined, not merely pointed at: {nested}"
655 );
656 assert!(
657 nested.contains("crates/napi/CLAUDE.md") && nested.contains("`crates/napi/`"),
658 "a scoped rule shown without its scope reads as a global one: {nested}"
659 );
660 }
661
662 #[test]
663 fn the_root_file_is_not_repeated_in_the_nested_block() {
664 let dir = repo_with(&[("CLAUDE.md", "root rules"), ("sub/CLAUDE.md", "sub rules")]);
665 let nested = nested_instructions(dir.path()).unwrap();
666 assert!(
667 !nested.contains("root rules"),
668 "agent_instructions already loads the root file in full: {nested}"
669 );
670 }
671
672 #[test]
676 fn an_untracked_instruction_file_is_ignored() {
677 let dir = repo_with(&[("CLAUDE.md", "root rules"), ("sub/CLAUDE.md", "tracked")]);
678 std::fs::create_dir_all(dir.path().join("target/scratch")).unwrap();
681 std::fs::write(
682 dir.path().join("target/scratch/CLAUDE.md"),
683 "not maintainer intent",
684 )
685 .unwrap();
686
687 let nested = nested_instructions(dir.path()).unwrap();
688 assert!(nested.contains("tracked"));
689 assert!(
690 !nested.contains("not maintainer intent"),
691 "untracked text must not reach the system prompt — a session could \
692 otherwise write its own rules mid-run: {nested}"
693 );
694 }
695
696 #[test]
697 fn a_worktree_that_is_not_a_git_checkout_yields_nothing_rather_than_failing() {
698 let dir = tempfile::tempdir().unwrap();
699 std::fs::write(dir.path().join("CLAUDE.md"), "rules").unwrap();
700 assert!(nested_instructions(dir.path()).is_none());
701 assert!(project_context(dir.path()).unwrap().contains("rules"));
703 }
704
705 #[test]
706 fn past_the_file_budget_the_rest_become_readable_pointers() {
707 let mut files: Vec<(String, String)> = vec![("CLAUDE.md".into(), "root".into())];
708 for i in 0..(MAX_NESTED_FILES + 3) {
709 files.push((format!("d{i:02}/CLAUDE.md"), format!("rule {i}")));
710 }
711 let refs: Vec<(&str, &str)> = files
712 .iter()
713 .map(|(a, b)| (a.as_str(), b.as_str()))
714 .collect();
715 let dir = repo_with(&refs);
716
717 let nested = nested_instructions(dir.path()).unwrap();
718 assert!(
719 nested.contains("Not shown, over budget"),
720 "a repo past the budget must be TOLD it is seeing pointers: {nested}"
721 );
722 assert!(
723 nested.contains("rule 0"),
724 "the first files are still inlined"
725 );
726 assert!(
727 nested.contains(&format!("d{:02}/CLAUDE.md", MAX_NESTED_FILES + 2)),
728 "an over-budget file is still named so the model can read it"
729 );
730 }
731
732 #[test]
733 fn nested_instructions_reach_the_prompt_block_with_their_precedence_stated() {
734 let dir = repo_with(&[
735 ("CLAUDE.md", "root rules"),
736 ("crates/napi/CLAUDE.md", "napi gotchas"),
737 ]);
738 let block = project_context(dir.path()).expect("a block");
739 assert!(block.contains("DIRECTORY-SCOPED INSTRUCTIONS"));
740 assert!(block.contains("napi gotchas"));
741 assert!(
742 block.contains("the scoped rule wins for that subtree"),
743 "precedence between root and scoped rules must be stated, not guessed: {block}"
744 );
745 }
746
747 #[test]
752 fn car_s_own_repo_yields_both_its_hard_rules_and_its_napi_gotchas() {
753 let root = Path::new(env!("CARGO_MANIFEST_DIR"))
754 .ancestors()
755 .nth(3)
756 .unwrap();
757 if !root.join("car-rs/crates/car-ffi-napi/CLAUDE.md").exists() {
758 return;
759 }
760 let block = project_context(root).expect("CAR has instructions");
761
762 assert!(
765 block.contains("No cargo feature flags"),
766 "the hard rules must survive the root budget"
767 );
768 assert!(block.contains("Keep all FFI bindings in sync"));
769 assert!(
770 !block.contains("[truncated:"),
771 "CAR's own instructions must not be truncated at all"
772 );
773
774 assert!(
776 block.contains("Do not reintroduce them"),
777 "car-ffi-napi/CLAUDE.md must reach the prompt"
778 );
779 assert!(block.contains("car-rs/crates/car-ffi-napi/CLAUDE.md"));
780 }
781
782 #[test]
785 fn a_file_that_would_only_fit_as_a_fragment_is_deferred_instead() {
786 let big = "r".repeat(MAX_NESTED_FILE_BYTES);
787 let mut files: Vec<(String, String)> = vec![("CLAUDE.md".into(), "root".into())];
788 for i in 0..2 {
790 files.push((format!("d{i}/CLAUDE.md"), big.clone()));
791 }
792 files.push(("zz/CLAUDE.md".into(), "z".repeat(5_000)));
793 let refs: Vec<(&str, &str)> = files
794 .iter()
795 .map(|(a, b)| (a.as_str(), b.as_str()))
796 .collect();
797 let dir = repo_with(&refs);
798
799 let nested = nested_instructions(dir.path()).unwrap();
800 assert!(
801 nested.contains("zz/CLAUDE.md"),
802 "the deferred file must still be named: {}",
803 &nested[nested.len().saturating_sub(400)..]
804 );
805 assert!(
806 !nested.contains(&"z".repeat(200)),
807 "a sliver of the deferred file must not be inlined"
808 );
809 }
810
811 #[test]
825 fn the_repos_own_instructions_fit_the_cap() {
826 let root = Path::new(env!("CARGO_MANIFEST_DIR"))
827 .ancestors()
828 .nth(3)
829 .expect("crates/car-server-core is three levels below the repo root");
830 let claude_md = root.join("CLAUDE.md");
831 let Ok(raw) = std::fs::read_to_string(&claude_md) else {
832 return;
834 };
835 assert!(
836 raw.len() <= MAX_INSTRUCTIONS_BYTES,
837 "CAR's own CLAUDE.md is {} bytes and MAX_INSTRUCTIONS_BYTES is {}, so the \
838 coder working on this repo is silently losing the tail of its own rules. \
839 Raise the constant (and read its doc comment first) — do not delete this test.",
840 raw.len(),
841 MAX_INSTRUCTIONS_BYTES
842 );
843 }
844
845 #[test]
849 fn truncation_drops_the_tail_it_claims_to() {
850 let text = format!(
851 "{}\n## Project conventions (hard rules)\nno feature flags",
852 "x".repeat(100)
853 );
854 let cut = truncate_note(&text, 50);
855 assert!(!cut.contains("hard rules"), "the tail really is discarded");
856 assert!(
857 cut.contains("[truncated: 50 of"),
858 "and the loss is announced"
859 );
860 }
861}