Server-owned agent completion: remove report_completion dependency
When an agent process exits normally, the server now automatically runs acceptance gates (uncommitted changes check + cargo clippy + tests) and advances the pipeline based on results. This replaces the previous model where agents had to explicitly call report_completion as an MCP tool. Changes: - Add run_server_owned_completion() free function in agents.rs that runs gates on process exit, stores a CompletionReport, and advances pipeline - Wire it into start_agent's spawned task (replaces simple status setting) - Remove report_completion from MCP tools list and handler (mcp.rs) - Update default_agent_prompt() to not reference report_completion - Update all agent prompts in project.toml (supervisor, coders, qa, mergemaster) to reflect server-owned completion - Add guard: skip gates if completion was already recorded (legacy path) - Add 4 new tests for server-owned completion behavior - Update tools_list test (26 tools, report_completion excluded) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,9 +35,9 @@ You have these tools via the story-kit MCP server:
|
|||||||
2. Read the story file from .story_kit/work/ to understand requirements
|
2. Read the story file from .story_kit/work/ to understand requirements
|
||||||
3. Move it to work/2_current/ if it is in work/1_upcoming/
|
3. Move it to work/2_current/ if it is in work/1_upcoming/
|
||||||
4. Start coder-1 on the story: call start_agent with story_id="{{story_id}}" and agent_name="coder-1"
|
4. Start coder-1 on the story: call start_agent with story_id="{{story_id}}" and agent_name="coder-1"
|
||||||
5. Wait for completion: call wait_for_agent with story_id="{{story_id}}" and agent_name="coder-1". The coder will call report_completion when done, which runs acceptance gates automatically. wait_for_agent returns when the coder reports completion.
|
5. Wait for completion: call wait_for_agent with story_id="{{story_id}}" and agent_name="coder-1". The server automatically runs acceptance gates (cargo clippy + tests) when the coder process exits. wait_for_agent returns when the coder reaches a terminal state.
|
||||||
6. Check the result: inspect the "completion" field in the wait_for_agent response — if gates_passed is true, the work is done; if false, review the gate_output and decide whether to start a fresh coder.
|
6. Check the result: inspect the "completion" field in the wait_for_agent response — if gates_passed is true, the work is done; if false, review the gate_output and decide whether to start a fresh coder.
|
||||||
7. If the agent gets stuck or fails without calling report_completion, stop it and start a fresh agent.
|
7. If the agent gets stuck, stop it and start a fresh agent.
|
||||||
8. STOP here. Do NOT accept the story or merge to master. Report the status to the human for final review and acceptance.
|
8. STOP here. Do NOT accept the story or merge to master. Report the status to the human for final review and acceptance.
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
@@ -46,7 +46,7 @@ You have these tools via the story-kit MCP server:
|
|||||||
- Focus on coordination, monitoring, and quality review
|
- Focus on coordination, monitoring, and quality review
|
||||||
- Never accept stories or merge to master - that is the human's job
|
- Never accept stories or merge to master - that is the human's job
|
||||||
- Your job ends when the coder's completion report shows gates_passed=true and you have reported the result"""
|
- Your job ends when the coder's completion report shows gates_passed=true and you have reported the result"""
|
||||||
system_prompt = "You are a supervisor agent. Read CLAUDE.md and .story_kit/README.md first to understand the project dev process. Use MCP tools to coordinate sub-agents. Never implement code directly - always delegate to coder agents and monitor their progress. Use wait_for_agent to block until the coder calls report_completion (which runs acceptance gates automatically). Never accept stories or merge to master - get all gates green and report to the human."
|
system_prompt = "You are a supervisor agent. Read CLAUDE.md and .story_kit/README.md first to understand the project dev process. Use MCP tools to coordinate sub-agents. Never implement code directly - always delegate to coder agents and monitor their progress. Use wait_for_agent to block until the coder finishes — the server automatically runs acceptance gates when the agent process exits. Never accept stories or merge to master - get all gates green and report to the human."
|
||||||
|
|
||||||
[[agent]]
|
[[agent]]
|
||||||
name = "coder-1"
|
name = "coder-1"
|
||||||
@@ -54,8 +54,8 @@ role = "Full-stack engineer. Implements features across all components."
|
|||||||
model = "sonnet"
|
model = "sonnet"
|
||||||
max_turns = 50
|
max_turns = 50
|
||||||
max_budget_usd = 5.00
|
max_budget_usd = 5.00
|
||||||
prompt = "You are working in a git worktree on story {{story_id}}. Read CLAUDE.md first, then .story_kit/README.md to understand the dev process. Pick up the story from .story_kit/work/ - move it to work/2_current/ if needed. Follow the SDTW process through implementation and verification (Steps 1-3). The worktree and feature branch already exist - do not create them. Check .mcp.json for MCP tools. Do NOT accept the story or merge - commit your work and stop. If the user asks to review your changes, tell them to run: cd \"{{worktree_path}}\" && git difftool {{base_branch}}...HEAD\n\nIMPORTANT: When all your work is committed, call report_completion as your FINAL action: report_completion(story_id='{{story_id}}', agent_name='{{agent_name}}', summary='<brief summary of what you implemented>'). The server will run cargo clippy and tests automatically to verify your work."
|
prompt = "You are working in a git worktree on story {{story_id}}. Read CLAUDE.md first, then .story_kit/README.md to understand the dev process. Pick up the story from .story_kit/work/ - move it to work/2_current/ if needed. Follow the SDTW process through implementation and verification (Steps 1-3). The worktree and feature branch already exist - do not create them. Check .mcp.json for MCP tools. Do NOT accept the story or merge - commit your work and stop. If the user asks to review your changes, tell them to run: cd \"{{worktree_path}}\" && git difftool {{base_branch}}...HEAD\n\nIMPORTANT: Commit all your work before your process exits. The server will automatically run acceptance gates (cargo clippy + tests) when your process exits and advance the pipeline based on the results."
|
||||||
system_prompt = "You are a full-stack engineer working autonomously in a git worktree. Follow the Story-Driven Test Workflow strictly. Run cargo clippy and biome checks before considering work complete. Commit all your work before finishing - use a descriptive commit message. Do not accept stories, move them to archived, or merge to master - a human will do that. Do not coordinate with other agents - focus on your assigned story. ALWAYS call report_completion as your absolute final action after committing."
|
system_prompt = "You are a full-stack engineer working autonomously in a git worktree. Follow the Story-Driven Test Workflow strictly. Run cargo clippy and biome checks before considering work complete. Commit all your work before finishing - use a descriptive commit message. Do not accept stories, move them to archived, or merge to master - a human will do that. Do not coordinate with other agents - focus on your assigned story. The server automatically runs acceptance gates when your process exits."
|
||||||
|
|
||||||
[[agent]]
|
[[agent]]
|
||||||
name = "coder-2"
|
name = "coder-2"
|
||||||
@@ -63,8 +63,8 @@ role = "Full-stack engineer. Implements features across all components."
|
|||||||
model = "sonnet"
|
model = "sonnet"
|
||||||
max_turns = 50
|
max_turns = 50
|
||||||
max_budget_usd = 5.00
|
max_budget_usd = 5.00
|
||||||
prompt = "You are working in a git worktree on story {{story_id}}. Read CLAUDE.md first, then .story_kit/README.md to understand the dev process. Pick up the story from .story_kit/work/ - move it to work/2_current/ if needed. Follow the SDTW process through implementation and verification (Steps 1-3). The worktree and feature branch already exist - do not create them. Check .mcp.json for MCP tools. Do NOT accept the story or merge - commit your work and stop. If the user asks to review your changes, tell them to run: cd \"{{worktree_path}}\" && git difftool {{base_branch}}...HEAD\n\nIMPORTANT: When all your work is committed, call report_completion as your FINAL action: report_completion(story_id='{{story_id}}', agent_name='{{agent_name}}', summary='<brief summary of what you implemented>'). The server will run cargo clippy and tests automatically to verify your work."
|
prompt = "You are working in a git worktree on story {{story_id}}. Read CLAUDE.md first, then .story_kit/README.md to understand the dev process. Pick up the story from .story_kit/work/ - move it to work/2_current/ if needed. Follow the SDTW process through implementation and verification (Steps 1-3). The worktree and feature branch already exist - do not create them. Check .mcp.json for MCP tools. Do NOT accept the story or merge - commit your work and stop. If the user asks to review your changes, tell them to run: cd \"{{worktree_path}}\" && git difftool {{base_branch}}...HEAD\n\nIMPORTANT: Commit all your work before your process exits. The server will automatically run acceptance gates (cargo clippy + tests) when your process exits and advance the pipeline based on the results."
|
||||||
system_prompt = "You are a full-stack engineer working autonomously in a git worktree. Follow the Story-Driven Test Workflow strictly. Run cargo clippy and biome checks before considering work complete. Commit all your work before finishing - use a descriptive commit message. Do not accept stories, move them to archived, or merge to master - a human will do that. Do not coordinate with other agents - focus on your assigned story. ALWAYS call report_completion as your absolute final action after committing."
|
system_prompt = "You are a full-stack engineer working autonomously in a git worktree. Follow the Story-Driven Test Workflow strictly. Run cargo clippy and biome checks before considering work complete. Commit all your work before finishing - use a descriptive commit message. Do not accept stories, move them to archived, or merge to master - a human will do that. Do not coordinate with other agents - focus on your assigned story. The server automatically runs acceptance gates when your process exits."
|
||||||
|
|
||||||
[[agent]]
|
[[agent]]
|
||||||
name = "coder-3"
|
name = "coder-3"
|
||||||
@@ -72,8 +72,8 @@ role = "Full-stack engineer. Implements features across all components."
|
|||||||
model = "sonnet"
|
model = "sonnet"
|
||||||
max_turns = 50
|
max_turns = 50
|
||||||
max_budget_usd = 5.00
|
max_budget_usd = 5.00
|
||||||
prompt = "You are working in a git worktree on story {{story_id}}. Read CLAUDE.md first, then .story_kit/README.md to understand the dev process. Pick up the story from .story_kit/work/ - move it to work/2_current/ if needed. Follow the SDTW process through implementation and verification (Steps 1-3). The worktree and feature branch already exist - do not create them. Check .mcp.json for MCP tools. Do NOT accept the story or merge - commit your work and stop. If the user asks to review your changes, tell them to run: cd \"{{worktree_path}}\" && git difftool {{base_branch}}...HEAD\n\nIMPORTANT: When all your work is committed, call report_completion as your FINAL action: report_completion(story_id='{{story_id}}', agent_name='{{agent_name}}', summary='<brief summary of what you implemented>'). The server will run cargo clippy and tests automatically to verify your work."
|
prompt = "You are working in a git worktree on story {{story_id}}. Read CLAUDE.md first, then .story_kit/README.md to understand the dev process. Pick up the story from .story_kit/work/ - move it to work/2_current/ if needed. Follow the SDTW process through implementation and verification (Steps 1-3). The worktree and feature branch already exist - do not create them. Check .mcp.json for MCP tools. Do NOT accept the story or merge - commit your work and stop. If the user asks to review your changes, tell them to run: cd \"{{worktree_path}}\" && git difftool {{base_branch}}...HEAD\n\nIMPORTANT: Commit all your work before your process exits. The server will automatically run acceptance gates (cargo clippy + tests) when your process exits and advance the pipeline based on the results."
|
||||||
system_prompt = "You are a full-stack engineer working autonomously in a git worktree. Follow the Story-Driven Test Workflow strictly. Run cargo clippy and biome checks before considering work complete. Commit all your work before finishing - use a descriptive commit message. Do not accept stories, move them to archived, or merge to master - a human will do that. Do not coordinate with other agents - focus on your assigned story. ALWAYS call report_completion as your absolute final action after committing."
|
system_prompt = "You are a full-stack engineer working autonomously in a git worktree. Follow the Story-Driven Test Workflow strictly. Run cargo clippy and biome checks before considering work complete. Commit all your work before finishing - use a descriptive commit message. Do not accept stories, move them to archived, or merge to master - a human will do that. Do not coordinate with other agents - focus on your assigned story. The server automatically runs acceptance gates when your process exits."
|
||||||
|
|
||||||
[[agent]]
|
[[agent]]
|
||||||
name = "qa"
|
name = "qa"
|
||||||
@@ -110,7 +110,7 @@ Read CLAUDE.md first, then .story_kit/README.md to understand the dev process.
|
|||||||
- Kill the test server when done: `pkill -f story-kit-server || true`
|
- Kill the test server when done: `pkill -f story-kit-server || true`
|
||||||
|
|
||||||
### 4. Produce Structured Report
|
### 4. Produce Structured Report
|
||||||
Call report_completion as your FINAL action with a summary in this format:
|
Print your QA report to stdout before your process exits. The server will automatically run acceptance gates. Use this format:
|
||||||
|
|
||||||
```
|
```
|
||||||
## QA Report for {{story_id}}
|
## QA Report for {{story_id}}
|
||||||
@@ -138,8 +138,8 @@ Call report_completion as your FINAL action with a summary in this format:
|
|||||||
## Rules
|
## Rules
|
||||||
- Do NOT modify any code — read-only review only
|
- Do NOT modify any code — read-only review only
|
||||||
- If the server fails to start, still provide the testing plan with curl commands
|
- If the server fails to start, still provide the testing plan with curl commands
|
||||||
- Call report_completion as your FINAL action"""
|
- The server automatically runs acceptance gates when your process exits"""
|
||||||
system_prompt = "You are a QA agent. Your job is read-only: review code quality, run tests, try to start the server, and produce a structured QA report. Do not modify code. Call report_completion as your final action."
|
system_prompt = "You are a QA agent. Your job is read-only: review code quality, run tests, try to start the server, and produce a structured QA report. Do not modify code. The server automatically runs acceptance gates when your process exits."
|
||||||
|
|
||||||
[[agent]]
|
[[agent]]
|
||||||
name = "mergemaster"
|
name = "mergemaster"
|
||||||
@@ -162,5 +162,5 @@ Read CLAUDE.md first, then .story_kit/README.md to understand the dev process.
|
|||||||
- Do NOT implement code yourself
|
- Do NOT implement code yourself
|
||||||
- Do NOT resolve complex conflicts yourself - report them clearly
|
- Do NOT resolve complex conflicts yourself - report them clearly
|
||||||
- Your job is to trigger the merge pipeline and report results
|
- Your job is to trigger the merge pipeline and report results
|
||||||
- Call report_completion as your final action with a summary of what happened"""
|
- The server automatically runs acceptance gates when your process exits"""
|
||||||
system_prompt = "You are the mergemaster agent. Your sole responsibility is to trigger the merge_agent_work MCP tool and report the results. Do not write code. Do not resolve conflicts manually. Report success or failure clearly so the human can act."
|
system_prompt = "You are the mergemaster agent. Your sole responsibility is to trigger the merge_agent_work MCP tool and report the results. Do not write code. Do not resolve conflicts manually. Report success or failure clearly so the human can act."
|
||||||
|
|||||||
@@ -93,7 +93,10 @@ pub fn pipeline_stage(agent_name: &str) -> PipelineStage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Report produced by an agent calling `report_completion`.
|
/// Completion report produced when acceptance gates are run.
|
||||||
|
///
|
||||||
|
/// Created automatically by the server when an agent process exits normally,
|
||||||
|
/// or via the internal `report_completion` method.
|
||||||
#[derive(Debug, Serialize, Clone)]
|
#[derive(Debug, Serialize, Clone)]
|
||||||
pub struct CompletionReport {
|
pub struct CompletionReport {
|
||||||
pub summary: String,
|
pub summary: String,
|
||||||
@@ -263,6 +266,7 @@ impl AgentPool {
|
|||||||
let cwd = wt_path_str.clone();
|
let cwd = wt_path_str.clone();
|
||||||
let key_clone = key.clone();
|
let key_clone = key.clone();
|
||||||
let log_clone = event_log.clone();
|
let log_clone = event_log.clone();
|
||||||
|
let port_for_task = self.port;
|
||||||
|
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
let _ = tx_clone.send(AgentEvent::Status {
|
let _ = tx_clone.send(AgentEvent::Status {
|
||||||
@@ -277,17 +281,16 @@ impl AgentPool {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(session_id) => {
|
Ok(session_id) => {
|
||||||
if let Ok(mut agents) = agents_ref.lock()
|
// Server-owned completion: run acceptance gates automatically
|
||||||
&& let Some(agent) = agents.get_mut(&key_clone)
|
// when the agent process exits normally.
|
||||||
{
|
run_server_owned_completion(
|
||||||
agent.status = AgentStatus::Completed;
|
&agents_ref,
|
||||||
agent.session_id = session_id.clone();
|
port_for_task,
|
||||||
}
|
&sid,
|
||||||
let _ = tx_clone.send(AgentEvent::Done {
|
&aname,
|
||||||
story_id: sid.clone(),
|
|
||||||
agent_name: aname.clone(),
|
|
||||||
session_id,
|
session_id,
|
||||||
});
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if let Ok(mut agents) = agents_ref.lock()
|
if let Ok(mut agents) = agents_ref.lock()
|
||||||
@@ -747,13 +750,19 @@ impl AgentPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Report that an agent has finished work on a story.
|
/// Internal: report that an agent has finished work on a story.
|
||||||
|
///
|
||||||
|
/// **Note:** This is no longer exposed as an MCP tool. The server now
|
||||||
|
/// automatically runs completion gates when an agent process exits
|
||||||
|
/// (see `run_server_owned_completion`). This method is retained for
|
||||||
|
/// backwards compatibility and testing.
|
||||||
///
|
///
|
||||||
/// - Rejects with an error if the worktree has uncommitted changes.
|
/// - Rejects with an error if the worktree has uncommitted changes.
|
||||||
/// - Runs acceptance gates (cargo clippy + cargo nextest run / cargo test).
|
/// - Runs acceptance gates (cargo clippy + cargo nextest run / cargo test).
|
||||||
/// - Stores the `CompletionReport` on the agent record.
|
/// - Stores the `CompletionReport` on the agent record.
|
||||||
/// - Transitions status to `Completed` (gates passed) or `Failed` (gates failed).
|
/// - Transitions status to `Completed` (gates passed) or `Failed` (gates failed).
|
||||||
/// - Emits a `Done` event so `wait_for_agent` unblocks.
|
/// - Emits a `Done` event so `wait_for_agent` unblocks.
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn report_completion(
|
pub async fn report_completion(
|
||||||
&self,
|
&self,
|
||||||
story_id: &str,
|
story_id: &str,
|
||||||
@@ -1040,6 +1049,134 @@ impl AgentPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Server-owned completion: runs acceptance gates when an agent process exits
|
||||||
|
/// normally, and advances the pipeline based on results.
|
||||||
|
///
|
||||||
|
/// This is a **free function** (not a method on `AgentPool`) to break the
|
||||||
|
/// opaque type cycle that would otherwise arise: `start_agent` → spawned task
|
||||||
|
/// → server-owned completion → pipeline advance → `start_agent`.
|
||||||
|
///
|
||||||
|
/// If the agent already has a completion report (e.g. from a legacy
|
||||||
|
/// `report_completion` call), this is a no-op to avoid double-running gates.
|
||||||
|
async fn run_server_owned_completion(
|
||||||
|
agents: &Arc<Mutex<HashMap<String, StoryAgent>>>,
|
||||||
|
port: u16,
|
||||||
|
story_id: &str,
|
||||||
|
agent_name: &str,
|
||||||
|
session_id: Option<String>,
|
||||||
|
) {
|
||||||
|
let key = composite_key(story_id, agent_name);
|
||||||
|
|
||||||
|
// Guard: skip if completion was already recorded (legacy path).
|
||||||
|
{
|
||||||
|
let lock = match agents.lock() {
|
||||||
|
Ok(a) => a,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
match lock.get(&key) {
|
||||||
|
Some(agent) if agent.completion.is_some() => {
|
||||||
|
eprintln!(
|
||||||
|
"[agents] Completion already recorded for '{story_id}:{agent_name}'; \
|
||||||
|
skipping server-owned gates."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Some(_) => {}
|
||||||
|
None => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get worktree path for running gates.
|
||||||
|
let worktree_path = {
|
||||||
|
let lock = match agents.lock() {
|
||||||
|
Ok(a) => a,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
lock.get(&key)
|
||||||
|
.and_then(|a| a.worktree_info.as_ref().map(|wt| wt.path.clone()))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run acceptance gates.
|
||||||
|
let (gates_passed, gate_output) = if let Some(wt_path) = worktree_path {
|
||||||
|
let path = wt_path;
|
||||||
|
match tokio::task::spawn_blocking(move || {
|
||||||
|
check_uncommitted_changes(&path)?;
|
||||||
|
run_acceptance_gates(&path)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Ok(result)) => result,
|
||||||
|
Ok(Err(e)) => (false, e),
|
||||||
|
Err(e) => (false, format!("Gate check task panicked: {e}")),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
false,
|
||||||
|
"No worktree path available to run acceptance gates".to_string(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"[agents] Server-owned completion for '{story_id}:{agent_name}': gates_passed={gates_passed}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let report = CompletionReport {
|
||||||
|
summary: "Agent process exited normally".to_string(),
|
||||||
|
gates_passed,
|
||||||
|
gate_output,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store completion report and set status.
|
||||||
|
let tx = {
|
||||||
|
let mut lock = match agents.lock() {
|
||||||
|
Ok(a) => a,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
let agent = match lock.get_mut(&key) {
|
||||||
|
Some(a) => a,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
agent.completion = Some(report);
|
||||||
|
agent.session_id = session_id.clone();
|
||||||
|
agent.status = if gates_passed {
|
||||||
|
AgentStatus::Completed
|
||||||
|
} else {
|
||||||
|
AgentStatus::Failed
|
||||||
|
};
|
||||||
|
agent.tx.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Emit Done so wait_for_agent unblocks.
|
||||||
|
let _ = tx.send(AgentEvent::Done {
|
||||||
|
story_id: story_id.to_string(),
|
||||||
|
agent_name: agent_name.to_string(),
|
||||||
|
session_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Advance the pipeline state machine in a background task.
|
||||||
|
// Uses a non-async helper to break the opaque type cycle.
|
||||||
|
spawn_pipeline_advance(Arc::clone(agents), port, story_id, agent_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn pipeline advancement as a background task.
|
||||||
|
///
|
||||||
|
/// This is a **non-async** function so it does not participate in the opaque
|
||||||
|
/// type cycle between `start_agent` and `run_server_owned_completion`.
|
||||||
|
fn spawn_pipeline_advance(
|
||||||
|
agents: Arc<Mutex<HashMap<String, StoryAgent>>>,
|
||||||
|
port: u16,
|
||||||
|
story_id: &str,
|
||||||
|
agent_name: &str,
|
||||||
|
) {
|
||||||
|
let sid = story_id.to_string();
|
||||||
|
let aname = agent_name.to_string();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let pool = AgentPool { agents, port };
|
||||||
|
pool.run_pipeline_advance_for_completed_agent(&sid, &aname)
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Result of a mergemaster merge operation.
|
/// Result of a mergemaster merge operation.
|
||||||
#[derive(Debug, Serialize, Clone)]
|
#[derive(Debug, Serialize, Clone)]
|
||||||
pub struct MergeReport {
|
pub struct MergeReport {
|
||||||
@@ -1287,8 +1424,8 @@ fn check_uncommitted_changes(path: &Path) -> Result<(), String> {
|
|||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
if !stdout.trim().is_empty() {
|
if !stdout.trim().is_empty() {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Worktree has uncommitted changes. Commit your work before calling \
|
"Worktree has uncommitted changes. Please commit all work before \
|
||||||
report_completion:\n{stdout}"
|
the agent exits:\n{stdout}"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1916,6 +2053,148 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── server-owned completion tests ───────────────────────────────────────────
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn server_owned_completion_skips_when_already_completed() {
|
||||||
|
let pool = AgentPool::new(3001);
|
||||||
|
let report = CompletionReport {
|
||||||
|
summary: "Already done".to_string(),
|
||||||
|
gates_passed: true,
|
||||||
|
gate_output: String::new(),
|
||||||
|
};
|
||||||
|
pool.inject_test_agent_with_completion(
|
||||||
|
"s10",
|
||||||
|
"coder-1",
|
||||||
|
AgentStatus::Completed,
|
||||||
|
PathBuf::from("/tmp/nonexistent"),
|
||||||
|
report,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Subscribe before calling so we can check if Done event was emitted.
|
||||||
|
let mut rx = pool.subscribe("s10", "coder-1").unwrap();
|
||||||
|
|
||||||
|
run_server_owned_completion(&pool.agents, pool.port, "s10", "coder-1", Some("sess-1".to_string()))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Status should remain Completed (unchanged) — no gate re-run.
|
||||||
|
let agents = pool.agents.lock().unwrap();
|
||||||
|
let key = composite_key("s10", "coder-1");
|
||||||
|
let agent = agents.get(&key).unwrap();
|
||||||
|
assert_eq!(agent.status, AgentStatus::Completed);
|
||||||
|
// Summary should still be the original, not overwritten.
|
||||||
|
assert_eq!(
|
||||||
|
agent.completion.as_ref().unwrap().summary,
|
||||||
|
"Already done"
|
||||||
|
);
|
||||||
|
drop(agents);
|
||||||
|
|
||||||
|
// No Done event should have been emitted.
|
||||||
|
assert!(
|
||||||
|
rx.try_recv().is_err(),
|
||||||
|
"should not emit Done when completion already exists"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn server_owned_completion_runs_gates_on_clean_worktree() {
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
let tmp = tempdir().unwrap();
|
||||||
|
let repo = tmp.path();
|
||||||
|
init_git_repo(repo);
|
||||||
|
|
||||||
|
let pool = AgentPool::new(3001);
|
||||||
|
pool.inject_test_agent_with_path(
|
||||||
|
"s11",
|
||||||
|
"coder-1",
|
||||||
|
AgentStatus::Running,
|
||||||
|
repo.to_path_buf(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut rx = pool.subscribe("s11", "coder-1").unwrap();
|
||||||
|
|
||||||
|
run_server_owned_completion(&pool.agents, pool.port, "s11", "coder-1", Some("sess-2".to_string()))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Completion report should exist (gates were run, though they may fail
|
||||||
|
// because this is not a real Cargo project).
|
||||||
|
let agents = pool.agents.lock().unwrap();
|
||||||
|
let key = composite_key("s11", "coder-1");
|
||||||
|
let agent = agents.get(&key).unwrap();
|
||||||
|
assert!(
|
||||||
|
agent.completion.is_some(),
|
||||||
|
"completion report should be created"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
agent.completion.as_ref().unwrap().summary,
|
||||||
|
"Agent process exited normally"
|
||||||
|
);
|
||||||
|
// Session ID should be stored.
|
||||||
|
assert_eq!(agent.session_id, Some("sess-2".to_string()));
|
||||||
|
// Status should be terminal (Completed or Failed depending on gate results).
|
||||||
|
assert!(
|
||||||
|
agent.status == AgentStatus::Completed || agent.status == AgentStatus::Failed,
|
||||||
|
"status should be terminal, got: {:?}",
|
||||||
|
agent.status
|
||||||
|
);
|
||||||
|
drop(agents);
|
||||||
|
|
||||||
|
// A Done event should have been emitted.
|
||||||
|
let event = rx.try_recv().expect("should emit Done event");
|
||||||
|
assert!(
|
||||||
|
matches!(event, AgentEvent::Done { .. }),
|
||||||
|
"expected Done event, got: {event:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn server_owned_completion_fails_on_dirty_worktree() {
|
||||||
|
use std::fs;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
let tmp = tempdir().unwrap();
|
||||||
|
let repo = tmp.path();
|
||||||
|
init_git_repo(repo);
|
||||||
|
// Create an uncommitted file.
|
||||||
|
fs::write(repo.join("dirty.txt"), "not committed").unwrap();
|
||||||
|
|
||||||
|
let pool = AgentPool::new(3001);
|
||||||
|
pool.inject_test_agent_with_path(
|
||||||
|
"s12",
|
||||||
|
"coder-1",
|
||||||
|
AgentStatus::Running,
|
||||||
|
repo.to_path_buf(),
|
||||||
|
);
|
||||||
|
|
||||||
|
run_server_owned_completion(&pool.agents, pool.port, "s12", "coder-1", None)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let agents = pool.agents.lock().unwrap();
|
||||||
|
let key = composite_key("s12", "coder-1");
|
||||||
|
let agent = agents.get(&key).unwrap();
|
||||||
|
assert!(agent.completion.is_some());
|
||||||
|
assert!(!agent.completion.as_ref().unwrap().gates_passed);
|
||||||
|
assert_eq!(agent.status, AgentStatus::Failed);
|
||||||
|
assert!(
|
||||||
|
agent
|
||||||
|
.completion
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.gate_output
|
||||||
|
.contains("uncommitted"),
|
||||||
|
"gate_output should mention uncommitted changes"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn server_owned_completion_nonexistent_agent_is_noop() {
|
||||||
|
let pool = AgentPool::new(3001);
|
||||||
|
// Should not panic or error — just silently return.
|
||||||
|
run_server_owned_completion(&pool.agents, pool.port, "nonexistent", "bot", None)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
// ── move_story_to_current tests ────────────────────────────────────────────
|
// ── move_story_to_current tests ────────────────────────────────────────────
|
||||||
// No git repo needed: the watcher handles commits asynchronously.
|
// No git repo needed: the watcher handles commits asynchronously.
|
||||||
|
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ fn default_agent_command() -> String {
|
|||||||
fn default_agent_prompt() -> String {
|
fn default_agent_prompt() -> String {
|
||||||
"You are working in a git worktree on story {{story_id}}. \
|
"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. \
|
Read .story_kit/README.md to understand the dev process, then pick up the story. \
|
||||||
When all work is committed, call report_completion with story_id='{{story_id}}', \
|
Commit all your work when done — the server will automatically run acceptance \
|
||||||
agent_name='{{agent_name}}', and a brief summary as your final action."
|
gates (cargo clippy + tests) when your process exits."
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -582,28 +582,6 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
|||||||
"required": ["worktree_path"]
|
"required": ["worktree_path"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "report_completion",
|
|
||||||
"description": "Report that the agent has finished work on a story. Rejects if the worktree has uncommitted changes. Runs acceptance gates (cargo clippy + tests) automatically. Stores the completion status and gate results on the agent record for retrieval by wait_for_agent or the supervisor. Call this as your final action after committing all changes.",
|
|
||||||
"inputSchema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"story_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Story identifier (e.g. '44_my_story')"
|
|
||||||
},
|
|
||||||
"agent_name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Agent name (as configured in project.toml)"
|
|
||||||
},
|
|
||||||
"summary": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Brief summary of the work completed"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["story_id", "agent_name", "summary"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "accept_story",
|
"name": "accept_story",
|
||||||
"description": "Accept a story: moves it from current/ to archived/ and auto-commits to master.",
|
"description": "Accept a story: moves it from current/ to archived/ and auto-commits to master.",
|
||||||
@@ -805,8 +783,6 @@ async fn handle_tools_call(
|
|||||||
"remove_worktree" => tool_remove_worktree(&args, ctx).await,
|
"remove_worktree" => tool_remove_worktree(&args, ctx).await,
|
||||||
// Editor tools
|
// Editor tools
|
||||||
"get_editor_command" => tool_get_editor_command(&args, ctx),
|
"get_editor_command" => tool_get_editor_command(&args, ctx),
|
||||||
// Completion reporting
|
|
||||||
"report_completion" => tool_report_completion(&args, ctx).await,
|
|
||||||
// Lifecycle tools
|
// Lifecycle tools
|
||||||
"accept_story" => tool_accept_story(&args, ctx),
|
"accept_story" => tool_accept_story(&args, ctx),
|
||||||
// Story mutation tools (auto-commit to master)
|
// Story mutation tools (auto-commit to master)
|
||||||
@@ -1185,40 +1161,6 @@ fn tool_get_editor_command(args: &Value, ctx: &AppContext) -> Result<String, Str
|
|||||||
Ok(format!("{editor} {worktree_path}"))
|
Ok(format!("{editor} {worktree_path}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn tool_report_completion(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
|
||||||
let story_id = args
|
|
||||||
.get("story_id")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or("Missing required argument: story_id")?;
|
|
||||||
let agent_name = args
|
|
||||||
.get("agent_name")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or("Missing required argument: agent_name")?;
|
|
||||||
let summary = args
|
|
||||||
.get("summary")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or("Missing required argument: summary")?;
|
|
||||||
|
|
||||||
let report = ctx
|
|
||||||
.agents
|
|
||||||
.report_completion(story_id, agent_name, summary)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
serde_json::to_string_pretty(&json!({
|
|
||||||
"story_id": story_id,
|
|
||||||
"agent_name": agent_name,
|
|
||||||
"summary": report.summary,
|
|
||||||
"gates_passed": report.gates_passed,
|
|
||||||
"gate_output": report.gate_output,
|
|
||||||
"message": if report.gates_passed {
|
|
||||||
"Completion accepted. All acceptance gates passed."
|
|
||||||
} else {
|
|
||||||
"Completion recorded but acceptance gates failed. Review gate_output for details."
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.map_err(|e| format!("Serialization error: {e}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tool_accept_story(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
fn tool_accept_story(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||||
let story_id = args
|
let story_id = args
|
||||||
.get("story_id")
|
.get("story_id")
|
||||||
@@ -1591,7 +1533,7 @@ mod tests {
|
|||||||
assert!(names.contains(&"list_worktrees"));
|
assert!(names.contains(&"list_worktrees"));
|
||||||
assert!(names.contains(&"remove_worktree"));
|
assert!(names.contains(&"remove_worktree"));
|
||||||
assert!(names.contains(&"get_editor_command"));
|
assert!(names.contains(&"get_editor_command"));
|
||||||
assert!(names.contains(&"report_completion"));
|
assert!(!names.contains(&"report_completion"));
|
||||||
assert!(names.contains(&"accept_story"));
|
assert!(names.contains(&"accept_story"));
|
||||||
assert!(names.contains(&"check_criterion"));
|
assert!(names.contains(&"check_criterion"));
|
||||||
assert!(names.contains(&"set_test_plan"));
|
assert!(names.contains(&"set_test_plan"));
|
||||||
@@ -1601,7 +1543,7 @@ mod tests {
|
|||||||
assert!(names.contains(&"merge_agent_work"));
|
assert!(names.contains(&"merge_agent_work"));
|
||||||
assert!(names.contains(&"move_story_to_merge"));
|
assert!(names.contains(&"move_story_to_merge"));
|
||||||
assert!(names.contains(&"request_qa"));
|
assert!(names.contains(&"request_qa"));
|
||||||
assert_eq!(tools.len(), 27);
|
assert_eq!(tools.len(), 26);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1850,71 +1792,6 @@ mod tests {
|
|||||||
assert!(parsed.get("completion").is_some());
|
assert!(parsed.get("completion").is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── report_completion tool tests ──────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn report_completion_in_tools_list() {
|
|
||||||
let resp = handle_tools_list(Some(json!(1)));
|
|
||||||
let tools = resp.result.unwrap()["tools"].as_array().unwrap().clone();
|
|
||||||
let tool = tools
|
|
||||||
.iter()
|
|
||||||
.find(|t| t["name"] == "report_completion")
|
|
||||||
.expect("report_completion missing from tools list");
|
|
||||||
// Schema has required fields
|
|
||||||
let required = tool["inputSchema"]["required"].as_array().unwrap();
|
|
||||||
let req_names: Vec<&str> = required.iter().map(|v| v.as_str().unwrap()).collect();
|
|
||||||
assert!(req_names.contains(&"story_id"));
|
|
||||||
assert!(req_names.contains(&"agent_name"));
|
|
||||||
assert!(req_names.contains(&"summary"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn report_completion_tool_missing_story_id() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let ctx = test_ctx(tmp.path());
|
|
||||||
let result =
|
|
||||||
tool_report_completion(&json!({"agent_name": "bot", "summary": "done"}), &ctx).await;
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result.unwrap_err().contains("story_id"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn report_completion_tool_missing_agent_name() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let ctx = test_ctx(tmp.path());
|
|
||||||
let result =
|
|
||||||
tool_report_completion(&json!({"story_id": "44_test", "summary": "done"}), &ctx).await;
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result.unwrap_err().contains("agent_name"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn report_completion_tool_missing_summary() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let ctx = test_ctx(tmp.path());
|
|
||||||
let result = tool_report_completion(
|
|
||||||
&json!({"story_id": "44_test", "agent_name": "bot"}),
|
|
||||||
&ctx,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result.unwrap_err().contains("summary"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn report_completion_tool_nonexistent_agent() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let ctx = test_ctx(tmp.path());
|
|
||||||
let result = tool_report_completion(
|
|
||||||
&json!({"story_id": "99_nope", "agent_name": "bot", "summary": "done"}),
|
|
||||||
&ctx,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert!(result.is_err());
|
|
||||||
let msg = result.unwrap_err();
|
|
||||||
assert!(msg.contains("No agent"), "unexpected: {msg}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Editor command tool tests ─────────────────────────────────
|
// ── Editor command tool tests ─────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user