|
|
|
@@ -740,22 +740,14 @@ fn load_project_context_with_parents_and_home(
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Auto-generate .codewhale/instructions.md when no context file exists anywhere.
|
|
|
|
|
// This avoids the per-turn filesystem scan fallback in prompts.rs that
|
|
|
|
|
// breaks KV prefix cache stability.
|
|
|
|
|
// Generate a bounded in-memory fallback when no context file exists
|
|
|
|
|
// anywhere. This keeps prompt shape stable without creating project-local
|
|
|
|
|
// `.codewhale/` files merely because CodeWhale was opened in a directory.
|
|
|
|
|
if !ctx.has_instructions()
|
|
|
|
|
&& let Some(generated) = auto_generate_context(workspace)
|
|
|
|
|
&& let Some(generated) = generate_ephemeral_context(workspace)
|
|
|
|
|
{
|
|
|
|
|
let mut warnings = std::mem::take(&mut ctx.warnings);
|
|
|
|
|
ctx = load_project_context(workspace);
|
|
|
|
|
warnings.extend(ctx.warnings.iter().cloned());
|
|
|
|
|
ctx.warnings = warnings;
|
|
|
|
|
if !ctx.has_instructions() {
|
|
|
|
|
// Loaded from the file we just wrote — use the generated content
|
|
|
|
|
// directly as a last resort (shouldn't normally happen).
|
|
|
|
|
ctx.instructions = Some(generated);
|
|
|
|
|
ctx.source_path = None;
|
|
|
|
|
}
|
|
|
|
|
ctx.instructions = Some(generated);
|
|
|
|
|
ctx.source_path = None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load the CodeWhale-specific repo authority policy
|
|
|
|
@@ -920,44 +912,17 @@ fn load_global_agents_context(workspace: &Path, home_dir: Option<&Path>) -> Opti
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Generate a context file from project tree + summary and write it to
|
|
|
|
|
/// `.codewhale/instructions.md` (or `.deepseek/instructions.md` as legacy
|
|
|
|
|
/// fallback). Returns the generated content on success.
|
|
|
|
|
fn auto_generate_context(workspace: &Path) -> Option<String> {
|
|
|
|
|
let codewhale_dir = workspace.join(".codewhale");
|
|
|
|
|
let instructions_path = codewhale_dir.join("instructions.md");
|
|
|
|
|
let legacy_instructions_path = workspace.join(".deepseek/instructions.md");
|
|
|
|
|
|
|
|
|
|
// Don't overwrite an existing file (check both locations)
|
|
|
|
|
if instructions_path.exists() || legacy_instructions_path.exists() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Generate ephemeral context from the project tree. Returns the generated
|
|
|
|
|
/// content on success without writing workspace files.
|
|
|
|
|
fn generate_ephemeral_context(workspace: &Path) -> Option<String> {
|
|
|
|
|
let overview = generate_bounded_project_overview(workspace)?;
|
|
|
|
|
|
|
|
|
|
let content = format!(
|
|
|
|
|
"# Project Context (Auto-generated)\n\n\
|
|
|
|
|
> This file was automatically generated by CodeWhale.\n\
|
|
|
|
|
> You can edit or delete it at any time.\n\n\
|
|
|
|
|
Some(format!(
|
|
|
|
|
"# Project Context (Auto-generated, ephemeral)\n\n\
|
|
|
|
|
> This context was generated in memory by CodeWhale.\n\
|
|
|
|
|
> No .codewhale/instructions.md file was written.\n\n\
|
|
|
|
|
{overview}"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Create .codewhale/ directory
|
|
|
|
|
if let Err(e) = std::fs::create_dir_all(&codewhale_dir) {
|
|
|
|
|
tracing::warn!("Failed to create .codewhale/ directory: {e}");
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match std::fs::write(&instructions_path, &content) {
|
|
|
|
|
Ok(()) => {
|
|
|
|
|
tracing::info!("Auto-generated {}", instructions_path.display());
|
|
|
|
|
Some(content)
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
tracing::warn!("Failed to write {}: {e}", instructions_path.display());
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Load a context file with size checking
|
|
|
|
@@ -1488,7 +1453,7 @@ mod tests {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn auto_generated_context_is_bounded_for_many_file_workspace() {
|
|
|
|
|
fn generated_context_is_bounded_and_ephemeral_for_many_file_workspace() {
|
|
|
|
|
let workspace = tempdir().expect("workspace tempdir");
|
|
|
|
|
let home = tempdir().expect("home tempdir");
|
|
|
|
|
let noisy = workspace.path().join("aaa-many-files");
|
|
|
|
@@ -1513,9 +1478,17 @@ mod tests {
|
|
|
|
|
assert!(ctx.has_instructions());
|
|
|
|
|
|
|
|
|
|
let generated_path = workspace.path().join(".codewhale").join("instructions.md");
|
|
|
|
|
assert_eq!(ctx.source_path.as_deref(), Some(generated_path.as_path()));
|
|
|
|
|
let generated = fs::read_to_string(&generated_path).expect("read generated");
|
|
|
|
|
assert!(generated.contains("Project Context (Auto-generated)"));
|
|
|
|
|
assert_eq!(ctx.source_path, None);
|
|
|
|
|
assert!(
|
|
|
|
|
!generated_path.exists(),
|
|
|
|
|
"generated project context should stay ephemeral"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
!workspace.path().join(".codewhale").exists(),
|
|
|
|
|
"loading context should not create a .codewhale directory"
|
|
|
|
|
);
|
|
|
|
|
let generated = ctx.instructions.as_ref().expect("generated instructions");
|
|
|
|
|
assert!(generated.contains("Project Context (Auto-generated, ephemeral)"));
|
|
|
|
|
assert!(generated.contains("Bounded Project Overview"));
|
|
|
|
|
assert!(!generated.contains("<project_context_pack>"));
|
|
|
|
|
assert!(
|
|
|
|
@@ -1613,7 +1586,7 @@ mod tests {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn cached_context_regenerates_after_auto_generated_context_is_deleted() {
|
|
|
|
|
fn cached_generated_context_stays_ephemeral() {
|
|
|
|
|
crate::project_context_cache::clear();
|
|
|
|
|
let workspace = tempdir().expect("workspace tempdir");
|
|
|
|
|
let home = tempdir().expect("home tempdir");
|
|
|
|
@@ -1622,17 +1595,17 @@ mod tests {
|
|
|
|
|
load_project_context_with_parents_cached_and_home(workspace.path(), Some(home.path()));
|
|
|
|
|
assert!(first.has_instructions());
|
|
|
|
|
let generated_path = workspace.path().join(".codewhale").join("instructions.md");
|
|
|
|
|
assert!(generated_path.is_file(), "expected generated instructions");
|
|
|
|
|
|
|
|
|
|
fs::remove_file(&generated_path).expect("remove generated instructions");
|
|
|
|
|
assert!(!generated_path.exists());
|
|
|
|
|
assert!(
|
|
|
|
|
!generated_path.exists(),
|
|
|
|
|
"first load should not write generated instructions"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let second =
|
|
|
|
|
load_project_context_with_parents_cached_and_home(workspace.path(), Some(home.path()));
|
|
|
|
|
assert!(second.has_instructions());
|
|
|
|
|
assert!(
|
|
|
|
|
generated_path.is_file(),
|
|
|
|
|
"cache hit under the missing-file signature would skip regeneration"
|
|
|
|
|
!generated_path.exists(),
|
|
|
|
|
"cached generated context should remain in memory-only state"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1937,7 +1910,7 @@ mod tests {
|
|
|
|
|
ctx.instructions
|
|
|
|
|
.as_ref()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.contains("Project Context (Auto-generated)")
|
|
|
|
|
.contains("Project Context (Auto-generated, ephemeral)")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|