From 4b765bbc391a4f34eb9c18ab9f07c750b5bf33b4 Mon Sep 17 00:00:00 2001 From: dave Date: Thu, 23 Apr 2026 11:52:09 +0000 Subject: [PATCH] huskies: merge 601_story_project_local_agent_prompt_layer_for_huskies --- .huskies/AGENT.md | 24 ++++++ server/src/agents/local_prompt.rs | 118 ++++++++++++++++++++++++++++++ server/src/agents/mod.rs | 1 + server/src/agents/pool/start.rs | 11 +++ website/docs/configuration.html | 30 ++++++++ 5 files changed, 184 insertions(+) create mode 100644 .huskies/AGENT.md create mode 100644 server/src/agents/local_prompt.rs diff --git a/.huskies/AGENT.md b/.huskies/AGENT.md new file mode 100644 index 00000000..c6b2029d --- /dev/null +++ b/.huskies/AGENT.md @@ -0,0 +1,24 @@ +# Huskies project-local agent guidance + +## Documentation +Docs live in `website/docs/*.html` (static HTML), **not** Markdown files. When a story asks you to document something, edit the relevant `.html` file in `website/docs/`. + +## Configuration files +- Agent config: `.huskies/agents.toml` (preferred) or `[[agent]]` blocks in `.huskies/project.toml` +- Project settings: `.huskies/project.toml` +- Bot credentials: `.huskies/bot.toml` (gitignored — never commit) + +## Frontend build +The frontend is embedded into the Rust binary via `rust-embed`. Run `npm run build` in `frontend/` before testing frontend changes, or the embedded assets will be stale. + +## Quality gates (all enforced by `script/test`) +1. `npm run build` (frontend) +2. `cargo fmt --all --check` +3. `cargo clippy -- -D warnings` +4. `cargo test` +5. `npm test` (frontend Vitest) + +Clippy is zero-tolerance: no warnings allowed. Fix every warning before committing. + +## Runtime validation +The `validate_agents` function in `server/src/config.rs` rejects unknown runtimes. Supported values: `"claude-code"` and `"gemini"`. Adding a new runtime requires updating that function. diff --git a/server/src/agents/local_prompt.rs b/server/src/agents/local_prompt.rs new file mode 100644 index 00000000..19119d21 --- /dev/null +++ b/server/src/agents/local_prompt.rs @@ -0,0 +1,118 @@ +//! Project-local agent prompt layer. +//! +//! Reads `.huskies/AGENT.md` from the project root and appends its content to +//! the baked-in agent prompt at spawn time. This lets projects record +//! non-obvious facts (directory conventions, known traps, etc.) that every +//! agent should know without modifying the shared agent configuration. +//! +//! Behaviour contract: +//! - If the file is missing or empty the caller receives `None`; agents spawn +//! normally with no warnings or errors. +//! - If the file exists and is non-empty, the content is returned and an +//! INFO-level log line is emitted with the file path and byte count. +//! - The file is read fresh on every agent spawn — no caching. + +use std::path::Path; + +/// Attempt to load the project-local agent prompt from `.huskies/AGENT.md`. +/// +/// Returns `Some(content)` when the file exists and is non-empty, or `None` +/// when the file is absent or empty. Never returns an error; any I/O problem +/// is silently treated as "no local prompt". +pub fn read_project_local_prompt(project_root: &Path) -> Option { + let path = project_root.join(".huskies/AGENT.md"); + let content = std::fs::read_to_string(&path).ok()?; + let trimmed = content.trim(); + if trimmed.is_empty() { + return None; + } + crate::slog!( + "[agents] project-local prompt loaded: {} ({} bytes)", + path.display(), + trimmed.len() + ); + Some(trimmed.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn returns_none_when_file_absent() { + let tmp = tempfile::tempdir().unwrap(); + let result = read_project_local_prompt(tmp.path()); + assert!(result.is_none(), "missing file must return None"); + } + + #[test] + fn returns_none_when_file_empty() { + let tmp = tempfile::tempdir().unwrap(); + let huskies_dir = tmp.path().join(".huskies"); + std::fs::create_dir_all(&huskies_dir).unwrap(); + std::fs::write(huskies_dir.join("AGENT.md"), "").unwrap(); + let result = read_project_local_prompt(tmp.path()); + assert!(result.is_none(), "empty file must return None"); + } + + #[test] + fn returns_none_when_file_whitespace_only() { + let tmp = tempfile::tempdir().unwrap(); + let huskies_dir = tmp.path().join(".huskies"); + std::fs::create_dir_all(&huskies_dir).unwrap(); + std::fs::write(huskies_dir.join("AGENT.md"), " \n\n ").unwrap(); + let result = read_project_local_prompt(tmp.path()); + assert!(result.is_none(), "whitespace-only file must return None"); + } + + #[test] + fn returns_content_when_file_non_empty() { + let tmp = tempfile::tempdir().unwrap(); + let huskies_dir = tmp.path().join(".huskies"); + std::fs::create_dir_all(&huskies_dir).unwrap(); + let marker = "DISTINCTIVE_MARKER_XYZ42"; + std::fs::write(huskies_dir.join("AGENT.md"), format!("# Hints\n{marker}\n")).unwrap(); + let result = read_project_local_prompt(tmp.path()); + assert!(result.is_some(), "non-empty file must return Some"); + let content = result.unwrap(); + assert!( + content.contains(marker), + "returned content must include the marker: {content}" + ); + } + + #[test] + fn appended_to_prompt_integration() { + // Simulates the start.rs usage: marker appears in the constructed + // system prompt when the file is present, absent when it is not. + let tmp_with = tempfile::tempdir().unwrap(); + let huskies_dir = tmp_with.path().join(".huskies"); + std::fs::create_dir_all(&huskies_dir).unwrap(); + let marker = "INTEGRATION_MARKER_601"; + std::fs::write(huskies_dir.join("AGENT.md"), marker).unwrap(); + + let base_prompt = "You are a coder agent.".to_string(); + let local = read_project_local_prompt(tmp_with.path()); + let effective = match local { + Some(ref extra) => format!("{base_prompt}\n\n{extra}"), + None => base_prompt.clone(), + }; + assert!( + effective.contains(marker), + "marker must appear in effective prompt when file present: {effective}" + ); + + // Without the file + let tmp_without = tempfile::tempdir().unwrap(); + let local2 = read_project_local_prompt(tmp_without.path()); + assert!(local2.is_none(), "no marker when file absent"); + let effective2 = match local2 { + Some(ref extra) => format!("{base_prompt}\n\n{extra}"), + None => base_prompt.clone(), + }; + assert!( + !effective2.contains(marker), + "marker must NOT appear in effective prompt when file absent: {effective2}" + ); + } +} diff --git a/server/src/agents/mod.rs b/server/src/agents/mod.rs index 7586ba76..44382957 100644 --- a/server/src/agents/mod.rs +++ b/server/src/agents/mod.rs @@ -1,6 +1,7 @@ //! Agent subsystem — types, configuration, and orchestration for coding agents. pub mod gates; pub mod lifecycle; +pub mod local_prompt; pub mod merge; mod pool; pub(crate) mod pty; diff --git a/server/src/agents/pool/start.rs b/server/src/agents/pool/start.rs index bb815bb3..de8f257d 100644 --- a/server/src/agents/pool/start.rs +++ b/server/src/agents/pool/start.rs @@ -410,6 +410,17 @@ impl AgentPool { } }; + // Append project-local prompt content (.huskies/AGENT.md) to the + // baked-in prompt so every agent role sees project-specific guidance + // without any config changes. The file is read fresh each spawn; + // if absent or empty, the prompt is unchanged and no warning is logged. + if let Some(local) = + crate::agents::local_prompt::read_project_local_prompt(&project_root_clone) + { + prompt.push_str("\n\n"); + prompt.push_str(&local); + } + // Build the effective prompt and determine resume session. // // When resuming a previous session, discard the full rendered prompt diff --git a/website/docs/configuration.html b/website/docs/configuration.html index 53370f17..9d3d95ed 100644 --- a/website/docs/configuration.html +++ b/website/docs/configuration.html @@ -200,6 +200,36 @@ prompt = "You are working on story {{story_id}} ..." system_prompt = "You are a senior full-stack engineer ..."

To use this agent for a specific story, add agent: opus to the story's front matter, or run start <number> opus in chat.

+

Project-local agent prompt (.huskies/AGENT.md)

+

Place a file at .huskies/AGENT.md in your project root to append project-specific guidance to every agent's initial prompt at spawn time.

+ +

How it works

+ + +

Ordering

+
    +
  1. Baked-in agent prompt (from agents.toml or project.toml)
  2. +
  3. Project-local content from .huskies/AGENT.md
  4. +
  5. Resume context (only on agent restart after a gate failure)
  6. +
+ +

Example

+
# .huskies/AGENT.md
+
+## Documentation
+Docs live in `website/docs/*.html`, not Markdown files.
+Edit the relevant .html file when a story asks for documentation.
+
+## Quality gates
+Run `cargo clippy -- -D warnings` before committing. Zero warnings allowed.
+

Edit the file at any time — the next agent spawn picks up the latest content automatically.

+

bot.toml

Chat transport configuration. Lives at .huskies/bot.toml. This file is gitignored as it contains credentials. Copy the appropriate example file to get started:

cp .huskies/bot.toml.matrix.example .huskies/bot.toml