story-kit: support agent assignment via story front matter (story 249)
Adds an optional `agent:` field to story file front matter so that a specific agent can be requested for a story. The auto-assign loop now: 1. Reads the front-matter `agent` field for each story before picking a free agent. 2. If a preferred agent is named, uses it when free; skips the story (without falling back) when that agent is busy. 3. Falls back to the existing `find_free_agent_for_stage` behaviour when no preference is specified. Ported from feature branch that predated the agents.rs module refactoring. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1377,7 +1377,13 @@ impl AgentPool {
|
||||
for story_id in &items {
|
||||
// Re-acquire the lock on each iteration to see state changes
|
||||
// from previous start_agent calls in the same pass.
|
||||
let (already_assigned, free_agent) = {
|
||||
let preferred_agent =
|
||||
read_story_front_matter_agent(project_root, stage_dir, story_id);
|
||||
|
||||
// Outcome: (already_assigned, chosen_agent, preferred_busy)
|
||||
// preferred_busy=true means the story has a specific agent requested but it is
|
||||
// currently occupied — the story should wait rather than fall back.
|
||||
let (already_assigned, free_agent, preferred_busy) = {
|
||||
let agents = match self.agents.lock() {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
@@ -1386,13 +1392,20 @@ impl AgentPool {
|
||||
}
|
||||
};
|
||||
let assigned = is_story_assigned_for_stage(&config, &agents, story_id, stage);
|
||||
let free = if assigned {
|
||||
None
|
||||
if assigned {
|
||||
(true, None, false)
|
||||
} else if let Some(ref pref) = preferred_agent {
|
||||
// Story has a front-matter agent preference.
|
||||
if is_agent_free(&agents, pref) {
|
||||
(false, Some(pref.clone()), false)
|
||||
} else {
|
||||
(false, None, true)
|
||||
}
|
||||
} else {
|
||||
find_free_agent_for_stage(&config, &agents, stage)
|
||||
.map(|s| s.to_string())
|
||||
};
|
||||
(assigned, free)
|
||||
let free = find_free_agent_for_stage(&config, &agents, stage)
|
||||
.map(|s| s.to_string());
|
||||
(false, free, false)
|
||||
}
|
||||
};
|
||||
|
||||
if already_assigned {
|
||||
@@ -1400,6 +1413,16 @@ impl AgentPool {
|
||||
continue;
|
||||
}
|
||||
|
||||
if preferred_busy {
|
||||
// The story requests a specific agent that is currently busy.
|
||||
// Do not fall back to a different agent; let this story wait.
|
||||
slog!(
|
||||
"[auto-assign] Preferred agent '{}' busy for '{story_id}'; story will wait.",
|
||||
preferred_agent.as_deref().unwrap_or("?")
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
match free_agent {
|
||||
Some(agent_name) => {
|
||||
slog!(
|
||||
@@ -1815,6 +1838,29 @@ fn find_active_story_stage(project_root: &Path, story_id: &str) -> Option<&'stat
|
||||
|
||||
/// Scan a work pipeline stage directory and return story IDs, sorted alphabetically.
|
||||
/// Returns an empty `Vec` if the directory does not exist.
|
||||
/// Read the optional `agent:` field from the front matter of a story file.
|
||||
///
|
||||
/// Returns `Some(agent_name)` if the front matter specifies an agent, or `None`
|
||||
/// if the field is absent or the file cannot be read / parsed.
|
||||
fn read_story_front_matter_agent(project_root: &Path, stage_dir: &str, story_id: &str) -> Option<String> {
|
||||
use crate::io::story_metadata::parse_front_matter;
|
||||
let path = project_root
|
||||
.join(".story_kit")
|
||||
.join("work")
|
||||
.join(stage_dir)
|
||||
.join(format!("{story_id}.md"));
|
||||
let contents = std::fs::read_to_string(path).ok()?;
|
||||
parse_front_matter(&contents).ok()?.agent
|
||||
}
|
||||
|
||||
/// Return `true` if `agent_name` has no active (pending/running) entry in the pool.
|
||||
fn is_agent_free(agents: &HashMap<String, StoryAgent>, agent_name: &str) -> bool {
|
||||
!agents.values().any(|a| {
|
||||
a.agent_name == agent_name
|
||||
&& matches!(a.status, AgentStatus::Running | AgentStatus::Pending)
|
||||
})
|
||||
}
|
||||
|
||||
fn scan_stage_items(project_root: &Path, stage_dir: &str) -> Vec<String> {
|
||||
let dir = project_root
|
||||
.join(".story_kit")
|
||||
|
||||
Reference in New Issue
Block a user