huskies: merge 601_story_project_local_agent_prompt_layer_for_huskies

This commit is contained in:
dave
2026-04-23 11:52:09 +00:00
parent c9e8ed030e
commit 4b765bbc39
5 changed files with 184 additions and 0 deletions
+24
View File
@@ -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.
+118
View File
@@ -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<String> {
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}"
);
}
}
+1
View File
@@ -1,6 +1,7 @@
//! Agent subsystem — types, configuration, and orchestration for coding agents. //! Agent subsystem — types, configuration, and orchestration for coding agents.
pub mod gates; pub mod gates;
pub mod lifecycle; pub mod lifecycle;
pub mod local_prompt;
pub mod merge; pub mod merge;
mod pool; mod pool;
pub(crate) mod pty; pub(crate) mod pty;
+11
View File
@@ -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. // Build the effective prompt and determine resume session.
// //
// When resuming a previous session, discard the full rendered prompt // When resuming a previous session, discard the full rendered prompt
+30
View File
@@ -200,6 +200,36 @@ prompt = "You are working on story {{story_id}} ..."
system_prompt = "You are a senior full-stack engineer ..."</code></pre> system_prompt = "You are a senior full-stack engineer ..."</code></pre>
<p>To use this agent for a specific story, add <code>agent: opus</code> to the story's front matter, or run <code>start &lt;number&gt; opus</code> in chat.</p> <p>To use this agent for a specific story, add <code>agent: opus</code> to the story's front matter, or run <code>start &lt;number&gt; opus</code> in chat.</p>
<h2 id="agent-md">Project-local agent prompt (<code>.huskies/AGENT.md</code>)</h2>
<p>Place a file at <code>.huskies/AGENT.md</code> in your project root to append project-specific guidance to every agent's initial prompt at spawn time.</p>
<h3>How it works</h3>
<ul>
<li>Huskies reads <code>.huskies/AGENT.md</code> each time an agent is spawned — no caching, no restart required.</li>
<li>The file content is appended <em>after</em> the baked-in agent prompt, so project guidance refines core instructions without overriding them.</li>
<li>Applies to all agent roles: coder, QA, mergemaster, and supervisor.</li>
<li>If the file is missing or empty, agents spawn normally — no warnings, no errors.</li>
<li>When the file exists and is non-empty, a single <code>INFO</code> log line is emitted showing the file path and byte count.</li>
</ul>
<h3>Ordering</h3>
<ol>
<li>Baked-in agent prompt (from <code>agents.toml</code> or <code>project.toml</code>)</li>
<li>Project-local content from <code>.huskies/AGENT.md</code></li>
<li>Resume context (only on agent restart after a gate failure)</li>
</ol>
<h3>Example</h3>
<pre><code># .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.</code></pre>
<p>Edit the file at any time — the next agent spawn picks up the latest content automatically.</p>
<h2 id="bot-toml">bot.toml</h2> <h2 id="bot-toml">bot.toml</h2>
<p>Chat transport configuration. Lives at <code>.huskies/bot.toml</code>. This file is gitignored as it contains credentials. Copy the appropriate example file to get started:</p> <p>Chat transport configuration. Lives at <code>.huskies/bot.toml</code>. This file is gitignored as it contains credentials. Copy the appropriate example file to get started:</p>
<pre><code>cp .huskies/bot.toml.matrix.example .huskies/bot.toml</code></pre> <pre><code>cp .huskies/bot.toml.matrix.example .huskies/bot.toml</code></pre>