feat(story-93): expose server logs to agents via get_server_logs MCP tool
- Add log_buffer module: bounded 1000-line ring buffer with push/get_recent API
- Add slog! macro: drop-in for eprintln! that also captures to ring buffer
- Replace all eprintln! calls across agents, watcher, search, chat, worktree, claude_code with slog!
- Add get_server_logs MCP tool: accepts count (1-500) and optional filter params
- 5 unit tests for log_buffer covering push/retrieve, eviction, filtering, count limits, empty buffer
- 262 tests passing, clippy clean
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 20:38:19 +00:00
|
|
|
use crate::slog;
|
2026-02-19 17:58:53 +00:00
|
|
|
use serde::Deserialize;
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
use std::collections::HashSet;
|
2026-02-19 17:58:53 +00:00
|
|
|
use std::path::Path;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
|
|
|
pub struct ProjectConfig {
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub component: Vec<ComponentConfig>,
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
#[serde(default)]
|
|
|
|
|
pub agent: Vec<AgentConfig>,
|
2026-02-26 12:16:07 +00:00
|
|
|
#[serde(default)]
|
|
|
|
|
pub watcher: WatcherConfig,
|
2026-03-19 11:56:39 +00:00
|
|
|
/// Project-wide default QA mode: "server", "agent", or "human".
|
|
|
|
|
/// Per-story `qa` front matter overrides this. Default: "server".
|
|
|
|
|
#[serde(default = "default_qa")]
|
|
|
|
|
pub default_qa: String,
|
2026-02-26 12:16:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Configuration for the filesystem watcher's sweep behaviour.
|
|
|
|
|
///
|
|
|
|
|
/// Controls how often the watcher checks `5_done/` for items to promote to
|
|
|
|
|
/// `6_archived/`, and how long items must remain in `5_done/` before promotion.
|
|
|
|
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
|
|
|
|
pub struct WatcherConfig {
|
|
|
|
|
/// How often (in seconds) to check `5_done/` for items to archive.
|
|
|
|
|
/// Default: 60 seconds.
|
|
|
|
|
#[serde(default = "default_sweep_interval_secs")]
|
|
|
|
|
pub sweep_interval_secs: u64,
|
|
|
|
|
/// How long (in seconds) an item must remain in `5_done/` before being
|
|
|
|
|
/// moved to `6_archived/`. Default: 14400 (4 hours).
|
|
|
|
|
#[serde(default = "default_done_retention_secs")]
|
|
|
|
|
pub done_retention_secs: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for WatcherConfig {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
sweep_interval_secs: default_sweep_interval_secs(),
|
|
|
|
|
done_retention_secs: default_done_retention_secs(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_sweep_interval_secs() -> u64 {
|
|
|
|
|
60
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_done_retention_secs() -> u64 {
|
|
|
|
|
4 * 60 * 60 // 4 hours
|
2026-02-19 17:58:53 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-19 11:56:39 +00:00
|
|
|
fn default_qa() -> String {
|
|
|
|
|
"server".to_string()
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 17:58:53 +00:00
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
2026-02-20 11:57:25 +00:00
|
|
|
#[allow(dead_code)]
|
2026-02-19 17:58:53 +00:00
|
|
|
pub struct ComponentConfig {
|
|
|
|
|
pub name: String,
|
|
|
|
|
#[serde(default = "default_path")]
|
|
|
|
|
pub path: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub setup: Vec<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub teardown: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
|
|
|
pub struct AgentConfig {
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
#[serde(default = "default_agent_name")]
|
|
|
|
|
pub name: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub role: String,
|
2026-02-19 17:58:53 +00:00
|
|
|
#[serde(default = "default_agent_command")]
|
|
|
|
|
pub command: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub args: Vec<String>,
|
|
|
|
|
#[serde(default = "default_agent_prompt")]
|
|
|
|
|
pub prompt: String,
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
#[serde(default)]
|
|
|
|
|
pub model: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub allowed_tools: Option<Vec<String>>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub max_turns: Option<u32>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub max_budget_usd: Option<f64>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub system_prompt: Option<String>,
|
2026-02-24 15:50:34 +00:00
|
|
|
/// Pipeline stage this agent belongs to. Supported values: "coder", "qa",
|
|
|
|
|
/// "mergemaster", "other". When set, overrides the legacy name-based
|
|
|
|
|
/// detection used by `pipeline_stage()`.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub stage: Option<String>,
|
2026-02-24 13:13:16 +00:00
|
|
|
/// Inactivity timeout in seconds for the PTY read loop.
|
|
|
|
|
/// If no output is received within this duration, the agent process is killed
|
|
|
|
|
/// and marked as Failed. Default: 300 (5 minutes). Set to 0 to disable.
|
|
|
|
|
#[serde(default = "default_inactivity_timeout_secs")]
|
|
|
|
|
pub inactivity_timeout_secs: u64,
|
2026-02-19 17:58:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_path() -> String {
|
|
|
|
|
".".to_string()
|
|
|
|
|
}
|
|
|
|
|
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
fn default_agent_name() -> String {
|
|
|
|
|
"default".to_string()
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 13:13:16 +00:00
|
|
|
fn default_inactivity_timeout_secs() -> u64 {
|
|
|
|
|
300
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 17:58:53 +00:00
|
|
|
fn default_agent_command() -> String {
|
|
|
|
|
"claude".to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_agent_prompt() -> String {
|
2026-02-20 15:02:34 +00:00
|
|
|
"You are working in a git worktree on story {{story_id}}. \
|
|
|
|
|
Read .story_kit/README.md to understand the dev process, then pick up the story. \
|
2026-02-23 15:00:10 +00:00
|
|
|
Commit all your work when done — the server will automatically run acceptance \
|
|
|
|
|
gates (cargo clippy + tests) when your process exits."
|
2026-02-20 15:02:34 +00:00
|
|
|
.to_string()
|
2026-02-19 17:58:53 +00:00
|
|
|
}
|
|
|
|
|
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
/// Legacy config format with `agent` as an optional single table (`[agent]`).
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
struct LegacyProjectConfig {
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
component: Vec<ComponentConfig>,
|
|
|
|
|
agent: Option<AgentConfig>,
|
2026-02-26 12:16:07 +00:00
|
|
|
#[serde(default)]
|
|
|
|
|
watcher: WatcherConfig,
|
2026-03-19 11:56:39 +00:00
|
|
|
#[serde(default = "default_qa")]
|
|
|
|
|
default_qa: String,
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-19 17:58:53 +00:00
|
|
|
impl Default for ProjectConfig {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
component: Vec::new(),
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
agent: vec![AgentConfig {
|
|
|
|
|
name: default_agent_name(),
|
|
|
|
|
role: String::new(),
|
2026-02-19 17:58:53 +00:00
|
|
|
command: default_agent_command(),
|
|
|
|
|
args: vec![],
|
|
|
|
|
prompt: default_agent_prompt(),
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
model: None,
|
|
|
|
|
allowed_tools: None,
|
|
|
|
|
max_turns: None,
|
|
|
|
|
max_budget_usd: None,
|
|
|
|
|
system_prompt: None,
|
2026-02-24 15:50:34 +00:00
|
|
|
stage: None,
|
2026-02-24 13:13:16 +00:00
|
|
|
inactivity_timeout_secs: default_inactivity_timeout_secs(),
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
}],
|
2026-02-26 12:16:07 +00:00
|
|
|
watcher: WatcherConfig::default(),
|
2026-03-19 11:56:39 +00:00
|
|
|
default_qa: default_qa(),
|
2026-02-19 17:58:53 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ProjectConfig {
|
|
|
|
|
/// Load from `.story_kit/project.toml` relative to the given root.
|
|
|
|
|
/// Falls back to sensible defaults if the file doesn't exist.
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
///
|
|
|
|
|
/// Supports both the new `[[agent]]` array format and the legacy
|
|
|
|
|
/// `[agent]` single-table format (with a deprecation warning).
|
2026-02-19 17:58:53 +00:00
|
|
|
pub fn load(project_root: &Path) -> Result<Self, String> {
|
|
|
|
|
let config_path = project_root.join(".story_kit/project.toml");
|
|
|
|
|
if !config_path.exists() {
|
|
|
|
|
return Ok(Self::default());
|
|
|
|
|
}
|
|
|
|
|
let content =
|
|
|
|
|
std::fs::read_to_string(&config_path).map_err(|e| format!("Read config: {e}"))?;
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
Self::parse(&content)
|
2026-02-19 17:58:53 +00:00
|
|
|
}
|
|
|
|
|
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
/// Parse config from a TOML string, supporting both new and legacy formats.
|
|
|
|
|
pub fn parse(content: &str) -> Result<Self, String> {
|
|
|
|
|
// Try new format first (agent as array of tables)
|
|
|
|
|
match toml::from_str::<ProjectConfig>(content) {
|
|
|
|
|
Ok(config) if !config.agent.is_empty() => {
|
|
|
|
|
validate_agents(&config.agent)?;
|
|
|
|
|
Ok(config)
|
|
|
|
|
}
|
|
|
|
|
Ok(config) => {
|
|
|
|
|
// Parsed successfully but no agents — could be legacy or no agent section.
|
|
|
|
|
// Try legacy format.
|
|
|
|
|
if let Ok(legacy) = toml::from_str::<LegacyProjectConfig>(content)
|
|
|
|
|
&& let Some(agent) = legacy.agent {
|
feat(story-93): expose server logs to agents via get_server_logs MCP tool
- Add log_buffer module: bounded 1000-line ring buffer with push/get_recent API
- Add slog! macro: drop-in for eprintln! that also captures to ring buffer
- Replace all eprintln! calls across agents, watcher, search, chat, worktree, claude_code with slog!
- Add get_server_logs MCP tool: accepts count (1-500) and optional filter params
- 5 unit tests for log_buffer covering push/retrieve, eviction, filtering, count limits, empty buffer
- 262 tests passing, clippy clean
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 20:38:19 +00:00
|
|
|
slog!(
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
"[config] Warning: [agent] table is deprecated. \
|
|
|
|
|
Use [[agent]] array format instead."
|
|
|
|
|
);
|
|
|
|
|
let config = ProjectConfig {
|
|
|
|
|
component: legacy.component,
|
|
|
|
|
agent: vec![agent],
|
2026-02-26 12:16:07 +00:00
|
|
|
watcher: legacy.watcher,
|
2026-03-19 11:56:39 +00:00
|
|
|
default_qa: legacy.default_qa,
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
};
|
|
|
|
|
validate_agents(&config.agent)?;
|
|
|
|
|
return Ok(config);
|
|
|
|
|
}
|
|
|
|
|
// No agent section at all
|
|
|
|
|
Ok(config)
|
|
|
|
|
}
|
|
|
|
|
Err(_) => {
|
|
|
|
|
// New format failed — try legacy
|
|
|
|
|
let legacy: LegacyProjectConfig =
|
|
|
|
|
toml::from_str(content).map_err(|e| format!("Parse config: {e}"))?;
|
|
|
|
|
if let Some(agent) = legacy.agent {
|
feat(story-93): expose server logs to agents via get_server_logs MCP tool
- Add log_buffer module: bounded 1000-line ring buffer with push/get_recent API
- Add slog! macro: drop-in for eprintln! that also captures to ring buffer
- Replace all eprintln! calls across agents, watcher, search, chat, worktree, claude_code with slog!
- Add get_server_logs MCP tool: accepts count (1-500) and optional filter params
- 5 unit tests for log_buffer covering push/retrieve, eviction, filtering, count limits, empty buffer
- 262 tests passing, clippy clean
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 20:38:19 +00:00
|
|
|
slog!(
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
"[config] Warning: [agent] table is deprecated. \
|
|
|
|
|
Use [[agent]] array format instead."
|
|
|
|
|
);
|
|
|
|
|
let config = ProjectConfig {
|
|
|
|
|
component: legacy.component,
|
|
|
|
|
agent: vec![agent],
|
2026-02-26 12:16:07 +00:00
|
|
|
watcher: legacy.watcher,
|
2026-03-19 11:56:39 +00:00
|
|
|
default_qa: legacy.default_qa,
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
};
|
|
|
|
|
validate_agents(&config.agent)?;
|
|
|
|
|
Ok(config)
|
|
|
|
|
} else {
|
|
|
|
|
Ok(ProjectConfig {
|
|
|
|
|
component: legacy.component,
|
|
|
|
|
agent: Vec::new(),
|
2026-02-26 12:16:07 +00:00
|
|
|
watcher: legacy.watcher,
|
2026-03-19 11:56:39 +00:00
|
|
|
default_qa: legacy.default_qa,
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 11:56:39 +00:00
|
|
|
/// Return the project-wide default QA mode parsed from `default_qa`.
|
|
|
|
|
/// Falls back to `Server` if the value is unrecognised.
|
|
|
|
|
pub fn default_qa_mode(&self) -> crate::io::story_metadata::QaMode {
|
|
|
|
|
crate::io::story_metadata::QaMode::from_str(&self.default_qa)
|
|
|
|
|
.unwrap_or(crate::io::story_metadata::QaMode::Server)
|
|
|
|
|
}
|
|
|
|
|
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
/// Look up an agent config by name.
|
|
|
|
|
pub fn find_agent(&self, name: &str) -> Option<&AgentConfig> {
|
|
|
|
|
self.agent.iter().find(|a| a.name == name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get the default (first) agent config.
|
|
|
|
|
pub fn default_agent(&self) -> Option<&AgentConfig> {
|
|
|
|
|
self.agent.first()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Render template variables in agent args and prompt for the given agent.
|
|
|
|
|
/// If `agent_name` is None, uses the first (default) agent.
|
2026-02-19 17:58:53 +00:00
|
|
|
pub fn render_agent_args(
|
|
|
|
|
&self,
|
|
|
|
|
worktree_path: &str,
|
|
|
|
|
story_id: &str,
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
agent_name: Option<&str>,
|
2026-02-20 12:48:50 +00:00
|
|
|
base_branch: Option<&str>,
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
) -> Result<(String, Vec<String>, String), String> {
|
|
|
|
|
let agent = match agent_name {
|
|
|
|
|
Some(name) => self
|
|
|
|
|
.find_agent(name)
|
|
|
|
|
.ok_or_else(|| format!("No agent named '{name}' in config"))?,
|
|
|
|
|
None => self
|
|
|
|
|
.default_agent()
|
|
|
|
|
.ok_or_else(|| "No agents configured".to_string())?,
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-20 12:48:50 +00:00
|
|
|
let bb = base_branch.unwrap_or("master");
|
2026-02-20 15:02:34 +00:00
|
|
|
let aname = agent.name.as_str();
|
2026-02-19 17:58:53 +00:00
|
|
|
let render = |s: &str| {
|
|
|
|
|
s.replace("{{worktree_path}}", worktree_path)
|
|
|
|
|
.replace("{{story_id}}", story_id)
|
2026-02-20 12:48:50 +00:00
|
|
|
.replace("{{base_branch}}", bb)
|
2026-02-20 15:02:34 +00:00
|
|
|
.replace("{{agent_name}}", aname)
|
2026-02-19 17:58:53 +00:00
|
|
|
};
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
|
2026-02-19 17:58:53 +00:00
|
|
|
let command = render(&agent.command);
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
let mut args: Vec<String> = agent.args.iter().map(|a| render(a)).collect();
|
2026-02-19 17:58:53 +00:00
|
|
|
let prompt = render(&agent.prompt);
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
|
|
|
|
|
// Append structured CLI flags
|
|
|
|
|
if let Some(ref model) = agent.model {
|
|
|
|
|
args.push("--model".to_string());
|
|
|
|
|
args.push(model.clone());
|
|
|
|
|
}
|
|
|
|
|
if let Some(ref tools) = agent.allowed_tools
|
|
|
|
|
&& !tools.is_empty() {
|
|
|
|
|
args.push("--allowedTools".to_string());
|
|
|
|
|
args.push(tools.join(","));
|
|
|
|
|
}
|
|
|
|
|
if let Some(turns) = agent.max_turns {
|
|
|
|
|
args.push("--max-turns".to_string());
|
|
|
|
|
args.push(turns.to_string());
|
|
|
|
|
}
|
|
|
|
|
if let Some(budget) = agent.max_budget_usd {
|
|
|
|
|
args.push("--max-budget-usd".to_string());
|
|
|
|
|
args.push(budget.to_string());
|
|
|
|
|
}
|
|
|
|
|
if let Some(ref sp) = agent.system_prompt {
|
|
|
|
|
args.push("--append-system-prompt".to_string());
|
|
|
|
|
args.push(render(sp));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok((command, args, prompt))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Validate agent configs: no duplicate names, no empty names, positive budgets/turns.
|
|
|
|
|
fn validate_agents(agents: &[AgentConfig]) -> Result<(), String> {
|
|
|
|
|
let mut names = HashSet::new();
|
|
|
|
|
for agent in agents {
|
|
|
|
|
if agent.name.trim().is_empty() {
|
|
|
|
|
return Err("Agent name must not be empty".to_string());
|
|
|
|
|
}
|
|
|
|
|
if !names.insert(&agent.name) {
|
|
|
|
|
return Err(format!("Duplicate agent name: '{}'", agent.name));
|
|
|
|
|
}
|
|
|
|
|
if let Some(budget) = agent.max_budget_usd
|
|
|
|
|
&& budget <= 0.0 {
|
|
|
|
|
return Err(format!(
|
|
|
|
|
"Agent '{}': max_budget_usd must be positive, got {budget}",
|
|
|
|
|
agent.name
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
if let Some(turns) = agent.max_turns
|
|
|
|
|
&& turns == 0 {
|
|
|
|
|
return Err(format!(
|
|
|
|
|
"Agent '{}': max_turns must be positive, got 0",
|
|
|
|
|
agent.name
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-02-19 17:58:53 +00:00
|
|
|
}
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
Ok(())
|
2026-02-19 17:58:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
use std::fs;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn default_config_when_missing() {
|
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
|
|
|
let config = ProjectConfig::load(tmp.path()).unwrap();
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
assert_eq!(config.agent.len(), 1);
|
|
|
|
|
assert_eq!(config.agent[0].name, "default");
|
2026-02-19 17:58:53 +00:00
|
|
|
assert!(config.component.is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
fn parse_multi_agent_toml() {
|
|
|
|
|
let toml_str = r#"
|
|
|
|
|
[[component]]
|
|
|
|
|
name = "server"
|
|
|
|
|
path = "."
|
|
|
|
|
setup = ["cargo check"]
|
|
|
|
|
|
|
|
|
|
[[agent]]
|
|
|
|
|
name = "supervisor"
|
|
|
|
|
role = "Coordinates work"
|
|
|
|
|
model = "opus"
|
|
|
|
|
max_turns = 50
|
|
|
|
|
max_budget_usd = 10.00
|
|
|
|
|
system_prompt = "You are a senior engineer"
|
|
|
|
|
|
|
|
|
|
[[agent]]
|
|
|
|
|
name = "coder-1"
|
|
|
|
|
role = "Full-stack engineer"
|
|
|
|
|
model = "sonnet"
|
|
|
|
|
max_turns = 30
|
|
|
|
|
max_budget_usd = 5.00
|
|
|
|
|
"#;
|
|
|
|
|
|
|
|
|
|
let config = ProjectConfig::parse(toml_str).unwrap();
|
|
|
|
|
assert_eq!(config.agent.len(), 2);
|
|
|
|
|
assert_eq!(config.agent[0].name, "supervisor");
|
|
|
|
|
assert_eq!(config.agent[0].role, "Coordinates work");
|
|
|
|
|
assert_eq!(config.agent[0].model, Some("opus".to_string()));
|
|
|
|
|
assert_eq!(config.agent[0].max_turns, Some(50));
|
|
|
|
|
assert_eq!(config.agent[0].max_budget_usd, Some(10.0));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
config.agent[0].system_prompt,
|
|
|
|
|
Some("You are a senior engineer".to_string())
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(config.agent[1].name, "coder-1");
|
|
|
|
|
assert_eq!(config.agent[1].model, Some("sonnet".to_string()));
|
|
|
|
|
assert_eq!(config.component.len(), 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn parse_legacy_single_agent() {
|
|
|
|
|
let toml_str = r#"
|
|
|
|
|
[[component]]
|
|
|
|
|
name = "server"
|
|
|
|
|
path = "."
|
|
|
|
|
|
|
|
|
|
[agent]
|
|
|
|
|
command = "claude"
|
|
|
|
|
args = ["--print", "--directory", "{{worktree_path}}"]
|
|
|
|
|
prompt = "Pick up story {{story_id}}"
|
|
|
|
|
"#;
|
|
|
|
|
|
|
|
|
|
let config = ProjectConfig::parse(toml_str).unwrap();
|
|
|
|
|
assert_eq!(config.agent.len(), 1);
|
|
|
|
|
assert_eq!(config.agent[0].name, "default");
|
|
|
|
|
assert_eq!(config.agent[0].command, "claude");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn validate_duplicate_names() {
|
|
|
|
|
let toml_str = r#"
|
|
|
|
|
[[agent]]
|
|
|
|
|
name = "coder"
|
|
|
|
|
role = "Engineer"
|
|
|
|
|
|
|
|
|
|
[[agent]]
|
|
|
|
|
name = "coder"
|
|
|
|
|
role = "Another engineer"
|
|
|
|
|
"#;
|
|
|
|
|
|
|
|
|
|
let err = ProjectConfig::parse(toml_str).unwrap_err();
|
|
|
|
|
assert!(err.contains("Duplicate agent name: 'coder'"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn validate_empty_name() {
|
|
|
|
|
let toml_str = r#"
|
|
|
|
|
[[agent]]
|
|
|
|
|
name = ""
|
|
|
|
|
role = "Engineer"
|
|
|
|
|
"#;
|
|
|
|
|
|
|
|
|
|
let err = ProjectConfig::parse(toml_str).unwrap_err();
|
|
|
|
|
assert!(err.contains("Agent name must not be empty"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn validate_non_positive_budget() {
|
|
|
|
|
let toml_str = r#"
|
|
|
|
|
[[agent]]
|
|
|
|
|
name = "coder"
|
|
|
|
|
max_budget_usd = -1.0
|
|
|
|
|
"#;
|
|
|
|
|
|
|
|
|
|
let err = ProjectConfig::parse(toml_str).unwrap_err();
|
|
|
|
|
assert!(err.contains("must be positive"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn validate_zero_max_turns() {
|
|
|
|
|
let toml_str = r#"
|
|
|
|
|
[[agent]]
|
|
|
|
|
name = "coder"
|
|
|
|
|
max_turns = 0
|
|
|
|
|
"#;
|
|
|
|
|
|
|
|
|
|
let err = ProjectConfig::parse(toml_str).unwrap_err();
|
|
|
|
|
assert!(err.contains("max_turns must be positive"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn render_agent_args_default() {
|
|
|
|
|
let config = ProjectConfig::default();
|
|
|
|
|
let (cmd, args, prompt) = config
|
2026-02-20 12:48:50 +00:00
|
|
|
.render_agent_args("/tmp/wt", "42_foo", None, None)
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(cmd, "claude");
|
2026-02-23 19:26:37 +00:00
|
|
|
assert!(args.is_empty());
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
assert!(prompt.contains("42_foo"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn render_agent_args_by_name() {
|
|
|
|
|
let toml_str = r#"
|
|
|
|
|
[[agent]]
|
|
|
|
|
name = "supervisor"
|
|
|
|
|
model = "opus"
|
|
|
|
|
max_turns = 50
|
|
|
|
|
max_budget_usd = 10.00
|
|
|
|
|
system_prompt = "You lead story {{story_id}}"
|
|
|
|
|
allowed_tools = ["Read", "Write", "Bash"]
|
|
|
|
|
|
|
|
|
|
[[agent]]
|
|
|
|
|
name = "coder"
|
|
|
|
|
model = "sonnet"
|
|
|
|
|
max_turns = 30
|
|
|
|
|
"#;
|
|
|
|
|
|
|
|
|
|
let config = ProjectConfig::parse(toml_str).unwrap();
|
|
|
|
|
let (cmd, args, prompt) = config
|
2026-02-20 12:48:50 +00:00
|
|
|
.render_agent_args("/tmp/wt", "42_foo", Some("supervisor"), Some("master"))
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(cmd, "claude");
|
|
|
|
|
assert!(args.contains(&"--model".to_string()));
|
|
|
|
|
assert!(args.contains(&"opus".to_string()));
|
|
|
|
|
assert!(args.contains(&"--max-turns".to_string()));
|
|
|
|
|
assert!(args.contains(&"50".to_string()));
|
|
|
|
|
assert!(args.contains(&"--max-budget-usd".to_string()));
|
|
|
|
|
assert!(args.contains(&"10".to_string()));
|
|
|
|
|
assert!(args.contains(&"--allowedTools".to_string()));
|
|
|
|
|
assert!(args.contains(&"Read,Write,Bash".to_string()));
|
|
|
|
|
assert!(args.contains(&"--append-system-prompt".to_string()));
|
|
|
|
|
// System prompt should have template rendered
|
|
|
|
|
assert!(args.contains(&"You lead story 42_foo".to_string()));
|
|
|
|
|
assert!(prompt.contains("42_foo"));
|
|
|
|
|
|
|
|
|
|
// Render for coder
|
|
|
|
|
let (_, coder_args, _) = config
|
2026-02-20 12:48:50 +00:00
|
|
|
.render_agent_args("/tmp/wt", "42_foo", Some("coder"), Some("master"))
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
.unwrap();
|
|
|
|
|
assert!(coder_args.contains(&"sonnet".to_string()));
|
|
|
|
|
assert!(coder_args.contains(&"30".to_string()));
|
|
|
|
|
assert!(!coder_args.contains(&"--max-budget-usd".to_string()));
|
|
|
|
|
assert!(!coder_args.contains(&"--append-system-prompt".to_string()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn render_agent_args_not_found() {
|
|
|
|
|
let config = ProjectConfig::default();
|
2026-02-20 12:48:50 +00:00
|
|
|
let result = config.render_agent_args("/tmp/wt", "42_foo", Some("nonexistent"), None);
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
assert!(result.is_err());
|
|
|
|
|
assert!(result.unwrap_err().contains("No agent named 'nonexistent'"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn find_agent_and_default() {
|
|
|
|
|
let toml_str = r#"
|
|
|
|
|
[[agent]]
|
|
|
|
|
name = "first"
|
|
|
|
|
|
|
|
|
|
[[agent]]
|
|
|
|
|
name = "second"
|
|
|
|
|
"#;
|
|
|
|
|
|
|
|
|
|
let config = ProjectConfig::parse(toml_str).unwrap();
|
|
|
|
|
assert_eq!(config.default_agent().unwrap().name, "first");
|
|
|
|
|
assert_eq!(config.find_agent("second").unwrap().name, "second");
|
|
|
|
|
assert!(config.find_agent("missing").is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn parse_project_toml_from_file() {
|
2026-02-19 17:58:53 +00:00
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
|
|
|
let sk = tmp.path().join(".story_kit");
|
|
|
|
|
fs::create_dir_all(&sk).unwrap();
|
|
|
|
|
fs::write(
|
|
|
|
|
sk.join("project.toml"),
|
|
|
|
|
r#"
|
|
|
|
|
[[component]]
|
|
|
|
|
name = "server"
|
|
|
|
|
path = "."
|
|
|
|
|
setup = ["cargo check"]
|
|
|
|
|
teardown = []
|
|
|
|
|
|
|
|
|
|
[[component]]
|
|
|
|
|
name = "frontend"
|
|
|
|
|
path = "frontend"
|
|
|
|
|
setup = ["pnpm install"]
|
|
|
|
|
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
[[agent]]
|
|
|
|
|
name = "main"
|
2026-02-19 17:58:53 +00:00
|
|
|
command = "claude"
|
|
|
|
|
args = ["--print", "--directory", "{{worktree_path}}"]
|
|
|
|
|
prompt = "Pick up story {{story_id}}"
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
model = "sonnet"
|
2026-02-19 17:58:53 +00:00
|
|
|
"#,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let config = ProjectConfig::load(tmp.path()).unwrap();
|
|
|
|
|
assert_eq!(config.component.len(), 2);
|
|
|
|
|
assert_eq!(config.component[0].name, "server");
|
|
|
|
|
assert_eq!(config.component[1].setup, vec!["pnpm install"]);
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
assert_eq!(config.agent.len(), 1);
|
|
|
|
|
assert_eq!(config.agent[0].name, "main");
|
|
|
|
|
assert_eq!(config.agent[0].model, Some("sonnet".to_string()));
|
2026-02-19 17:58:53 +00:00
|
|
|
}
|
2026-02-26 12:16:07 +00:00
|
|
|
|
|
|
|
|
// ── WatcherConfig ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn watcher_config_defaults_when_omitted() {
|
|
|
|
|
let toml_str = r#"
|
|
|
|
|
[[agent]]
|
|
|
|
|
name = "coder"
|
|
|
|
|
"#;
|
|
|
|
|
let config = ProjectConfig::parse(toml_str).unwrap();
|
|
|
|
|
assert_eq!(config.watcher.sweep_interval_secs, 60);
|
|
|
|
|
assert_eq!(config.watcher.done_retention_secs, 4 * 60 * 60);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn watcher_config_custom_values() {
|
|
|
|
|
let toml_str = r#"
|
|
|
|
|
[watcher]
|
|
|
|
|
sweep_interval_secs = 30
|
|
|
|
|
done_retention_secs = 7200
|
|
|
|
|
|
|
|
|
|
[[agent]]
|
|
|
|
|
name = "coder"
|
|
|
|
|
"#;
|
|
|
|
|
let config = ProjectConfig::parse(toml_str).unwrap();
|
|
|
|
|
assert_eq!(config.watcher.sweep_interval_secs, 30);
|
|
|
|
|
assert_eq!(config.watcher.done_retention_secs, 7200);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn watcher_config_partial_override() {
|
|
|
|
|
let toml_str = r#"
|
|
|
|
|
[watcher]
|
|
|
|
|
sweep_interval_secs = 10
|
|
|
|
|
|
|
|
|
|
[[agent]]
|
|
|
|
|
name = "coder"
|
|
|
|
|
"#;
|
|
|
|
|
let config = ProjectConfig::parse(toml_str).unwrap();
|
|
|
|
|
assert_eq!(config.watcher.sweep_interval_secs, 10);
|
|
|
|
|
// done_retention_secs should fall back to the default (4 hours).
|
|
|
|
|
assert_eq!(config.watcher.done_retention_secs, 4 * 60 * 60);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn watcher_config_from_file() {
|
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
|
|
|
let sk = tmp.path().join(".story_kit");
|
|
|
|
|
fs::create_dir_all(&sk).unwrap();
|
|
|
|
|
fs::write(
|
|
|
|
|
sk.join("project.toml"),
|
|
|
|
|
r#"
|
|
|
|
|
[watcher]
|
|
|
|
|
sweep_interval_secs = 120
|
|
|
|
|
done_retention_secs = 3600
|
|
|
|
|
|
|
|
|
|
[[agent]]
|
|
|
|
|
name = "coder"
|
|
|
|
|
"#,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let config = ProjectConfig::load(tmp.path()).unwrap();
|
|
|
|
|
assert_eq!(config.watcher.sweep_interval_secs, 120);
|
|
|
|
|
assert_eq!(config.watcher.done_retention_secs, 3600);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn watcher_config_default_when_no_file() {
|
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
|
|
|
let config = ProjectConfig::load(tmp.path()).unwrap();
|
|
|
|
|
assert_eq!(config.watcher, WatcherConfig::default());
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 19:33:54 +00:00
|
|
|
#[test]
|
|
|
|
|
fn coder_agents_have_root_cause_guidance() {
|
|
|
|
|
// Load the actual project.toml and verify all coder-stage agents
|
|
|
|
|
// include root cause investigation guidance for bugs.
|
|
|
|
|
let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
|
|
|
|
|
let project_root = manifest_dir.parent().unwrap();
|
|
|
|
|
let config = ProjectConfig::load(project_root).unwrap();
|
|
|
|
|
|
|
|
|
|
let coder_agents: Vec<_> = config
|
|
|
|
|
.agent
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|a| a.stage.as_deref() == Some("coder"))
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
assert!(
|
|
|
|
|
!coder_agents.is_empty(),
|
|
|
|
|
"Expected at least one coder-stage agent in project.toml"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
for agent in coder_agents {
|
|
|
|
|
let prompt = &agent.prompt;
|
|
|
|
|
let system_prompt = agent.system_prompt.as_deref().unwrap_or("");
|
|
|
|
|
let combined = format!("{prompt} {system_prompt}");
|
|
|
|
|
|
|
|
|
|
assert!(
|
|
|
|
|
combined.contains("root cause"),
|
|
|
|
|
"Coder agent '{}' must mention 'root cause' in prompt or system_prompt",
|
|
|
|
|
agent.name
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
combined.contains("git bisect") || combined.contains("git log"),
|
|
|
|
|
"Coder agent '{}' must mention 'git bisect' or 'git log' for bug investigation",
|
|
|
|
|
agent.name
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
combined.to_lowercase().contains("do not") || combined.contains("surgical"),
|
|
|
|
|
"Coder agent '{}' must discourage adding abstractions/workarounds",
|
|
|
|
|
agent.name
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 12:16:07 +00:00
|
|
|
#[test]
|
|
|
|
|
fn watcher_config_preserved_in_legacy_format() {
|
|
|
|
|
let toml_str = r#"
|
|
|
|
|
[watcher]
|
|
|
|
|
sweep_interval_secs = 15
|
|
|
|
|
done_retention_secs = 900
|
|
|
|
|
|
|
|
|
|
[agent]
|
|
|
|
|
command = "claude"
|
|
|
|
|
"#;
|
|
|
|
|
let config = ProjectConfig::parse(toml_str).unwrap();
|
|
|
|
|
assert_eq!(config.watcher.sweep_interval_secs, 15);
|
|
|
|
|
assert_eq!(config.watcher.done_retention_secs, 900);
|
|
|
|
|
assert_eq!(config.agent.len(), 1);
|
|
|
|
|
}
|
2026-02-19 17:58:53 +00:00
|
|
|
}
|