fix: drop source-map.json from agent orientation bundle
The orientation bundle was 96 KB per coder spawn with 85 KB of that being source-map.json — a static symbol listing that drowned out the workflow rules in AGENT.md and likely explains why PLAN.md ceremony is being skipped (the instruction is ~5% of the bundle, buried under a wall of symbols). Agents are excellent at grep on demand, so the source map adds little value as a preloaded cheat sheet. File stays on disk for the merge-time source-map-check doc-coverage gate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,10 +10,12 @@
|
|||||||
//! - `.huskies/README.md`
|
//! - `.huskies/README.md`
|
||||||
//! - `.huskies/specs/00_CONTEXT.md`
|
//! - `.huskies/specs/00_CONTEXT.md`
|
||||||
//! - `.huskies/AGENT.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
|
//! `STACK.md` and `.huskies/source-map.json` are intentionally excluded — they
|
||||||
//! should grep it on demand.
|
//! are large and change often; agents should grep on demand instead. Earlier
|
||||||
|
//! versions of this bundle inlined the source map, which ballooned the orientation
|
||||||
|
//! to ~96 KB and drowned out the workflow rules in AGENT.md; the file is still
|
||||||
|
//! kept on disk for the merge-time `source-map-check` doc-coverage gate.
|
||||||
//!
|
//!
|
||||||
//! Behaviour contract:
|
//! Behaviour contract:
|
||||||
//! - Files that are missing or empty are skipped silently (no error, no section).
|
//! - Files that are missing or empty are skipped silently (no error, no section).
|
||||||
@@ -33,12 +35,6 @@ const ORIENTATION_FILES: &[&str] = &[
|
|||||||
".huskies/AGENT.md",
|
".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
|
/// Attempt to load the project-local agent prompt by concatenating orientation
|
||||||
/// files from the project root.
|
/// files from the project root.
|
||||||
///
|
///
|
||||||
@@ -60,14 +56,11 @@ pub fn read_project_local_prompt(project_root: &Path) -> Option<String> {
|
|||||||
sections.push((rel_path, trimmed.to_string()));
|
sections.push((rel_path, trimmed.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read source-map.json (after AGENT.md) with a byte cap.
|
if sections.is_empty() {
|
||||||
let source_map_content = read_source_map_section(project_root);
|
|
||||||
|
|
||||||
if sections.is_empty() && source_map_content.is_none() {
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut included_files: Vec<&str> = sections.iter().map(|(name, _)| *name).collect();
|
let included_files: Vec<&str> = sections.iter().map(|(name, _)| *name).collect();
|
||||||
let mut bundle = String::new();
|
let mut bundle = String::new();
|
||||||
for (i, (name, content)) in sections.iter().enumerate() {
|
for (i, (name, content)) in sections.iter().enumerate() {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
@@ -77,15 +70,6 @@ pub fn read_project_local_prompt(project_root: &Path) -> Option<String> {
|
|||||||
bundle.push_str(content);
|
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!(
|
crate::slog!(
|
||||||
"[agents] orientation bundle: {} bytes, files: [{}]",
|
"[agents] orientation bundle: {} bytes, files: [{}]",
|
||||||
bundle.len(),
|
bundle.len(),
|
||||||
@@ -95,39 +79,6 @@ pub fn read_project_local_prompt(project_root: &Path) -> Option<String> {
|
|||||||
Some(bundle)
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -310,10 +261,13 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── source-map.json tests ────────────────────────────────────────────────
|
// ── source-map.json must NOT be inlined into the bundle ──────────────────
|
||||||
|
// The file is kept on disk for the merge-time source-map-check gate, but
|
||||||
|
// inlining it into every agent spawn ballooned the orientation past 96 KB
|
||||||
|
// and drowned out the workflow rules in AGENT.md.
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn source_map_included_after_agent_md() {
|
fn source_map_not_included_even_when_present() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
write_file(tmp.path(), ".huskies/AGENT.md", "agent content");
|
write_file(tmp.path(), ".huskies/AGENT.md", "agent content");
|
||||||
write_file(
|
write_file(
|
||||||
@@ -324,92 +278,12 @@ mod tests {
|
|||||||
|
|
||||||
let result = read_project_local_prompt(tmp.path()).unwrap();
|
let result = read_project_local_prompt(tmp.path()).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
result.contains("=== .huskies/source-map.json ==="),
|
!result.contains("=== .huskies/source-map.json ==="),
|
||||||
"source-map delimiter must be present: {result}"
|
"source-map must not appear as an orientation section: {result}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
result.contains(r#""src/lib.rs""#),
|
!result.contains("src/lib.rs"),
|
||||||
"source-map content must be present: {result}"
|
"source-map content must not be inlined: {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()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user