diff --git a/crates/source-map-gen/src/regen_main.rs b/crates/source-map-gen/src/regen_main.rs index 586676c5..c93aa8e7 100644 --- a/crates/source-map-gen/src/regen_main.rs +++ b/crates/source-map-gen/src/regen_main.rs @@ -1,4 +1,4 @@ -//! CLI binary that regenerates `.huskies/source-map.json` from scratch. +//! CLI binary for manual regeneration of `.huskies/source-map.json`. //! //! Usage: `source-map-regen [--project-root ]` //! @@ -6,8 +6,10 @@ //! extracts public item signatures, and writes a fresh sorted JSON map. The output //! is byte-identical across runs on the same source tree (deterministic). //! -//! Intended to be called from the pre-commit quality gate (`script/check`) so that -//! every commit captures an accurate, stale-entry-free snapshot of the source map. +//! The pre-commit gate (`script/check`) no longer calls this binary directly — map +//! regeneration is now inlined into the coder spawn path (`local_prompt.rs`) so every +//! agent session starts with a fresh snapshot. This binary is kept as an escape hatch +//! for manual out-of-band regeneration (e.g. after bulk refactors outside the pipeline). use source_map_gen::regenerate_source_map; use std::path::Path; diff --git a/script/check b/script/check index a0eefb24..b9fd69cd 100755 --- a/script/check +++ b/script/check @@ -13,9 +13,5 @@ cargo fmt --manifest-path "$PROJECT_ROOT/Cargo.toml" --all --check echo "=== Running cargo clippy ===" cargo clippy --manifest-path "$PROJECT_ROOT/Cargo.toml" --workspace --all-targets -- -D warnings -echo "=== Regenerating source map ===" -cargo run --manifest-path "$PROJECT_ROOT/Cargo.toml" -p source-map-gen --bin source-map-regen --quiet -- --project-root "$PROJECT_ROOT" -git -C "$PROJECT_ROOT" add .huskies/source-map.json - echo "=== Checking doc coverage on changed files ===" cargo run --manifest-path "$PROJECT_ROOT/Cargo.toml" -p source-map-gen --bin source-map-check --quiet -- --worktree "$PROJECT_ROOT" --base master diff --git a/server/src/agents/local_prompt.rs b/server/src/agents/local_prompt.rs index e848617e..ee9867d0 100644 --- a/server/src/agents/local_prompt.rs +++ b/server/src/agents/local_prompt.rs @@ -60,6 +60,13 @@ pub fn read_project_local_prompt(project_root: &Path) -> Option { sections.push((rel_path, trimmed.to_string())); } + // Regenerate the source map so agents always start from a fresh snapshot. + // Failure is non-fatal: log it and fall through to whatever is on disk. + let map_path = project_root.join(SOURCE_MAP_REL); + if let Err(e) = source_map_gen::regenerate_source_map(project_root, &map_path) { + crate::slog!("[agents] source-map regen failed (non-fatal): {}", e); + } + // Read source-map.json (after AGENT.md) with a byte cap. let source_map_content = read_source_map_section(project_root); @@ -387,6 +394,86 @@ mod tests { ); } + // ── Regen-on-spawn tests ───────────────────────────────────────────────── + + fn init_git_repo(dir: &Path) { + let run = |args: &[&str]| { + std::process::Command::new("git") + .args(args) + .current_dir(dir) + .output() + .unwrap(); + }; + run(&["init"]); + run(&["config", "user.email", "test@test.com"]); + run(&["config", "user.name", "Test"]); + run(&["commit", "--allow-empty", "-m", "init"]); + } + + /// Happy path: regen runs successfully and the fresh map is included in the bundle. + #[test] + fn regen_creates_map_on_coder_spawn() { + let tmp = tempfile::tempdir().unwrap(); + init_git_repo(tmp.path()); + + // Write a tracked Rust file so git ls-files has something to index. + write_file( + tmp.path(), + "lib.rs", + "//! Module doc.\n\n/// A function.\npub fn hello() {}\n", + ); + std::process::Command::new("git") + .args(["add", "lib.rs"]) + .current_dir(tmp.path()) + .output() + .unwrap(); + std::process::Command::new("git") + .args(["commit", "-m", "add lib.rs"]) + .current_dir(tmp.path()) + .output() + .unwrap(); + + // Write an orientation file so we get Some back. + write_file(tmp.path(), "CLAUDE.md", "agent hints"); + + // Map does not exist yet. + let map_path = tmp.path().join(SOURCE_MAP_REL); + assert!(!map_path.exists(), "map must not exist before spawn"); + + let result = read_project_local_prompt(tmp.path()); + assert!( + result.is_some(), + "bundle must be Some when CLAUDE.md present" + ); + + // Regen should have written the map. + assert!( + map_path.exists(), + "regen must have written source-map.json during spawn" + ); + } + + /// Fallback: regen fails (no git repo) but a stale map on disk is still read. + #[test] + fn regen_fails_stale_map_still_readable() { + let tmp = tempfile::tempdir().unwrap(); + // No git repo — regen will fail with "git ls-files" error. + + write_file(tmp.path(), "CLAUDE.md", "agent hints"); + // Write a stale map manually. + write_file( + tmp.path(), + SOURCE_MAP_REL, + r#"{"stale/entry.rs": ["fn old"]}"#, + ); + + let result = read_project_local_prompt(tmp.path()).unwrap(); + assert!( + result.contains("stale/entry.rs"), + "stale map must still be readable after regen failure: {result}" + ); + } + #[test] #[allow(clippy::string_slice)] // sm_start is derived from str::find — always a char boundary fn source_map_truncated_at_byte_cap() {