huskies: merge 979

This commit is contained in:
dave
2026-05-13 14:08:58 +00:00
parent 4b18c01835
commit 7854fbd78a
2 changed files with 157 additions and 10 deletions
+157 -2
View File
@@ -10,6 +10,7 @@
//! - `.huskies/README.md`
//! - `.huskies/specs/00_CONTEXT.md`
//! - `.huskies/AGENT.md`
//! - `.huskies/source-map.json` (up to 200 KB; truncated with a log if larger)
//!
//! `STACK.md` is intentionally excluded — it is large and changes often; agents
//! should grep it on demand.
@@ -32,6 +33,12 @@ const ORIENTATION_FILES: &[&str] = &[
".huskies/AGENT.md",
];
/// Path to the source map (relative to project root), appended after AGENT.md.
const SOURCE_MAP_REL: &str = ".huskies/source-map.json";
/// Maximum bytes of source-map content to embed in the prompt.
const SOURCE_MAP_BYTE_CAP: usize = 200 * 1024;
/// Attempt to load the project-local agent prompt by concatenating orientation
/// files from the project root.
///
@@ -53,11 +60,14 @@ pub fn read_project_local_prompt(project_root: &Path) -> Option<String> {
sections.push((rel_path, trimmed.to_string()));
}
if sections.is_empty() {
// Read source-map.json (after AGENT.md) with a byte cap.
let source_map_content = read_source_map_section(project_root);
if sections.is_empty() && source_map_content.is_none() {
return None;
}
let included_files: Vec<&str> = sections.iter().map(|(name, _)| *name).collect();
let mut included_files: Vec<&str> = sections.iter().map(|(name, _)| *name).collect();
let mut bundle = String::new();
for (i, (name, content)) in sections.iter().enumerate() {
if i > 0 {
@@ -67,6 +77,15 @@ pub fn read_project_local_prompt(project_root: &Path) -> Option<String> {
bundle.push_str(content);
}
if let Some(sm) = source_map_content {
if !bundle.is_empty() {
bundle.push('\n');
}
bundle.push_str(&format!("=== {SOURCE_MAP_REL} ===\n"));
bundle.push_str(&sm);
included_files.push(SOURCE_MAP_REL);
}
crate::slog!(
"[agents] orientation bundle: {} bytes, files: [{}]",
bundle.len(),
@@ -76,6 +95,39 @@ pub fn read_project_local_prompt(project_root: &Path) -> Option<String> {
Some(bundle)
}
/// Read `.huskies/source-map.json` from `project_root`, applying a byte cap.
///
/// Returns `None` when the file is absent, unreadable, or empty.
/// When the content exceeds [`SOURCE_MAP_BYTE_CAP`], truncates at a char
/// boundary and logs the truncation.
#[allow(clippy::string_slice)] // cap is walked back to a char boundary before slicing
fn read_source_map_section(project_root: &Path) -> Option<String> {
let path = project_root.join(SOURCE_MAP_REL);
let Ok(content) = std::fs::read_to_string(&path) else {
return None;
};
let trimmed = content.trim();
if trimmed.is_empty() {
return None;
}
if trimmed.len() > SOURCE_MAP_BYTE_CAP {
let mut cap = SOURCE_MAP_BYTE_CAP;
while cap > 0 && !trimmed.is_char_boundary(cap) {
cap -= 1;
}
crate::slog!(
"[agents] source-map.json truncated: {} bytes > {} byte cap; \
including first {} bytes",
trimmed.len(),
SOURCE_MAP_BYTE_CAP,
cap
);
Some(trimmed[..cap].to_string())
} else {
Some(trimmed.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -257,4 +309,107 @@ mod tests {
"STACK.md must not appear in bundle: {result}"
);
}
// ── source-map.json tests ────────────────────────────────────────────────
#[test]
fn source_map_included_after_agent_md() {
let tmp = tempfile::tempdir().unwrap();
write_file(tmp.path(), ".huskies/AGENT.md", "agent content");
write_file(
tmp.path(),
".huskies/source-map.json",
r#"{"src/lib.rs": {"symbols": ["foo"]}}"#,
);
let result = read_project_local_prompt(tmp.path()).unwrap();
assert!(
result.contains("=== .huskies/source-map.json ==="),
"source-map delimiter must be present: {result}"
);
assert!(
result.contains(r#""src/lib.rs""#),
"source-map content must be present: {result}"
);
// source-map section must appear after AGENT.md section
let agent_pos = result.find("=== .huskies/AGENT.md ===").unwrap();
let sm_pos = result.find("=== .huskies/source-map.json ===").unwrap();
assert!(
sm_pos > agent_pos,
"source-map section must come after AGENT.md section"
);
}
#[test]
fn source_map_missing_skipped_silently() {
let tmp = tempfile::tempdir().unwrap();
write_file(tmp.path(), ".huskies/AGENT.md", "agent content");
// source-map.json intentionally absent
let result = read_project_local_prompt(tmp.path()).unwrap();
assert!(
!result.contains("source-map.json"),
"absent source-map must not create a section: {result}"
);
}
#[test]
fn source_map_empty_skipped_silently() {
let tmp = tempfile::tempdir().unwrap();
write_file(tmp.path(), ".huskies/AGENT.md", "agent content");
write_file(tmp.path(), ".huskies/source-map.json", "");
let result = read_project_local_prompt(tmp.path()).unwrap();
assert!(
!result.contains("source-map.json"),
"empty source-map must not create a section: {result}"
);
}
#[test]
fn source_map_only_returns_some() {
let tmp = tempfile::tempdir().unwrap();
// Only source-map.json present; all orientation files absent.
write_file(
tmp.path(),
".huskies/source-map.json",
r#"{"src/main.rs": {}}"#,
);
let result = read_project_local_prompt(tmp.path());
assert!(
result.is_some(),
"source-map alone must produce Some bundle"
);
assert!(
result.unwrap().contains("=== .huskies/source-map.json ==="),
"bundle must contain source-map section"
);
}
#[test]
#[allow(clippy::string_slice)] // sm_start is derived from str::find — always a char boundary
fn source_map_truncated_at_byte_cap() {
let tmp = tempfile::tempdir().unwrap();
write_file(tmp.path(), ".huskies/AGENT.md", "agent");
// Build content larger than SOURCE_MAP_BYTE_CAP (200 KB).
let big = "x".repeat(SOURCE_MAP_BYTE_CAP + 1024);
write_file(tmp.path(), ".huskies/source-map.json", &big);
let result = read_project_local_prompt(tmp.path()).unwrap();
assert!(
result.contains("=== .huskies/source-map.json ==="),
"truncated source-map must still produce a section: {result}"
);
// The content length of just the source-map section must be <= SOURCE_MAP_BYTE_CAP.
let sm_start = result.find("=== .huskies/source-map.json ===").unwrap()
+ "=== .huskies/source-map.json ===\n".len();
let sm_content = &result[sm_start..];
assert!(
sm_content.len() <= SOURCE_MAP_BYTE_CAP,
"source-map section content must be <= {} bytes, got {}",
SOURCE_MAP_BYTE_CAP,
sm_content.len()
);
}
}
-8
View File
@@ -284,14 +284,6 @@ pub(super) async fn run_agent_spawn(
prompt = format!("{block}\n\n{prompt}");
}
// Append a reference to the source map if the file was written.
let source_map_path = project_root_clone.join(".huskies").join("source-map.json");
if source_map_path.exists() {
prompt.push_str(
"\n\nA source map of well-documented changed files is at `.huskies/source-map.json`.",
);
}
match &session_id_to_resume_owned {
Some(sess_id) => slog!("[agent:{sid}:{aname}] spawn mode=warm session_id={sess_id}"),
None => slog!("[agent:{sid}:{aname}] spawn mode=cold"),