story-kit: start 70_story_server_owned_agent_completion_remove_report_completion_dependency
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
|
||||
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"
|
||||
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.
|
||||
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.
|
||||
|
||||
## Rules
|
||||
@@ -46,7 +46,7 @@ You have these tools via the story-kit MCP server:
|
||||
- Focus on coordination, monitoring, and quality review
|
||||
- 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"""
|
||||
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]]
|
||||
name = "coder-1"
|
||||
@@ -54,8 +54,8 @@ role = "Full-stack engineer. Implements features across all components."
|
||||
model = "sonnet"
|
||||
max_turns = 50
|
||||
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."
|
||||
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."
|
||||
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. The server automatically runs acceptance gates when your process exits."
|
||||
|
||||
[[agent]]
|
||||
name = "coder-2"
|
||||
@@ -63,8 +63,8 @@ role = "Full-stack engineer. Implements features across all components."
|
||||
model = "sonnet"
|
||||
max_turns = 50
|
||||
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."
|
||||
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."
|
||||
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. The server automatically runs acceptance gates when your process exits."
|
||||
|
||||
[[agent]]
|
||||
name = "coder-3"
|
||||
@@ -72,8 +72,8 @@ role = "Full-stack engineer. Implements features across all components."
|
||||
model = "sonnet"
|
||||
max_turns = 50
|
||||
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."
|
||||
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."
|
||||
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. The server automatically runs acceptance gates when your process exits."
|
||||
|
||||
[[agent]]
|
||||
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`
|
||||
|
||||
### 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}}
|
||||
@@ -138,8 +138,8 @@ Call report_completion as your FINAL action with a summary in this format:
|
||||
## Rules
|
||||
- Do NOT modify any code — read-only review only
|
||||
- If the server fails to start, still provide the testing plan with curl commands
|
||||
- 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. 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. The server automatically runs acceptance gates when your process exits."
|
||||
|
||||
[[agent]]
|
||||
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 resolve complex conflicts yourself - report them clearly
|
||||
- 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."
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: Server-owned agent completion: remove report_completion dependency
|
||||
test_plan: approved
|
||||
---
|
||||
|
||||
# Story 70: Server-owned agent completion: remove report_completion dependency
|
||||
|
||||
## User Story
|
||||
|
||||
As a developer using story-kit, I want the server to automatically run acceptance
|
||||
gates and advance the pipeline when an agent process exits, so that the pipeline
|
||||
does not stall when an agent forgets or fails to call `report_completion`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] When an agent process exits normally, the server automatically runs acceptance gates (uncommitted changes check + cargo clippy + tests)
|
||||
- [ ] When acceptance gates pass on natural process exit, the agent status is set to `Completed` and the pipeline advances
|
||||
- [ ] When acceptance gates fail on natural process exit, the agent status is set to `Failed` and the pipeline handles the failure appropriately
|
||||
- [ ] `report_completion` is removed from the MCP tools list (agents no longer see or call it)
|
||||
- [ ] The default agent prompt no longer instructs agents to call `report_completion`
|
||||
- [ ] If a completion was already recorded before the process exits (legacy path), the server does not run gates a second time
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Removing the concept of a `CompletionReport` struct (still used internally)
|
||||
- Changing the pipeline advancement logic itself
|
||||
- Frontend changes
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: "Work item creation does not quote YAML special characters or include front matter"
|
||||
test_plan: pending
|
||||
---
|
||||
|
||||
# Bug 72: Work item creation does not quote YAML special characters or include front matter
|
||||
|
||||
## Description
|
||||
|
||||
Two related issues in server-side work item creation:
|
||||
|
||||
1. **create_story** writes the `name` value into YAML front matter without quoting. If the name contains YAML-special characters like colons, the resulting front matter is invalid YAML and fails to parse.
|
||||
|
||||
2. **create_bug** does not write YAML front matter at all (no `---` block with `name` and `test_plan`). The UI/parser expects front matter on all work items in the pipeline, so bug files show parse errors.
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
### Issue 1 (create_story)
|
||||
1. Call create_story with a name containing a colon, e.g. "Server-owned agent completion: remove report_completion dependency"
|
||||
2. Open the generated .md file
|
||||
3. Observe the front matter parser rejects it: "mapping values are not allowed in this context"
|
||||
|
||||
### Issue 2 (create_bug)
|
||||
1. Call create_bug with any name
|
||||
2. Open the generated .md file
|
||||
3. Observe there is no YAML front matter block — just a markdown heading
|
||||
|
||||
## Actual Result
|
||||
|
||||
- create_story: Invalid front matter when name contains colons
|
||||
- create_bug: No front matter at all
|
||||
|
||||
## Expected Result
|
||||
|
||||
- All work item creation methods should write valid YAML front matter with quoted `name` values
|
||||
- create_bug should include `---` front matter block with `name` and `test_plan` fields, consistent with create_story
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] create_story quotes name values containing YAML-special characters
|
||||
- [ ] create_bug writes YAML front matter with name and test_plan fields
|
||||
- [ ] Existing tests updated to cover special character handling
|
||||
@@ -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)]
|
||||
pub struct CompletionReport {
|
||||
pub summary: String,
|
||||
@@ -263,6 +266,7 @@ impl AgentPool {
|
||||
let cwd = wt_path_str.clone();
|
||||
let key_clone = key.clone();
|
||||
let log_clone = event_log.clone();
|
||||
let port_for_task = self.port;
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let _ = tx_clone.send(AgentEvent::Status {
|
||||
@@ -277,17 +281,16 @@ impl AgentPool {
|
||||
.await
|
||||
{
|
||||
Ok(session_id) => {
|
||||
if let Ok(mut agents) = agents_ref.lock()
|
||||
&& let Some(agent) = agents.get_mut(&key_clone)
|
||||
{
|
||||
agent.status = AgentStatus::Completed;
|
||||
agent.session_id = session_id.clone();
|
||||
}
|
||||
let _ = tx_clone.send(AgentEvent::Done {
|
||||
story_id: sid.clone(),
|
||||
agent_name: aname.clone(),
|
||||
// Server-owned completion: run acceptance gates automatically
|
||||
// when the agent process exits normally.
|
||||
run_server_owned_completion(
|
||||
&agents_ref,
|
||||
port_for_task,
|
||||
&sid,
|
||||
&aname,
|
||||
session_id,
|
||||
});
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Err(e) => {
|
||||
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.
|
||||
/// - Runs acceptance gates (cargo clippy + cargo nextest run / cargo test).
|
||||
/// - Stores the `CompletionReport` on the agent record.
|
||||
/// - Transitions status to `Completed` (gates passed) or `Failed` (gates failed).
|
||||
/// - Emits a `Done` event so `wait_for_agent` unblocks.
|
||||
#[allow(dead_code)]
|
||||
pub async fn report_completion(
|
||||
&self,
|
||||
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.
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub struct MergeReport {
|
||||
@@ -1287,8 +1424,8 @@ fn check_uncommitted_changes(path: &Path) -> Result<(), String> {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
if !stdout.trim().is_empty() {
|
||||
return Err(format!(
|
||||
"Worktree has uncommitted changes. Commit your work before calling \
|
||||
report_completion:\n{stdout}"
|
||||
"Worktree has uncommitted changes. Please commit all work before \
|
||||
the agent exits:\n{stdout}"
|
||||
));
|
||||
}
|
||||
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 ────────────────────────────────────────────
|
||||
// No git repo needed: the watcher handles commits asynchronously.
|
||||
|
||||
|
||||
@@ -61,8 +61,8 @@ fn default_agent_command() -> String {
|
||||
fn default_agent_prompt() -> String {
|
||||
"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. \
|
||||
When all work is committed, call report_completion with story_id='{{story_id}}', \
|
||||
agent_name='{{agent_name}}', and a brief summary as your final action."
|
||||
Commit all your work when done — the server will automatically run acceptance \
|
||||
gates (cargo clippy + tests) when your process exits."
|
||||
.to_string()
|
||||
}
|
||||
|
||||
|
||||
@@ -582,28 +582,6 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
||||
"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",
|
||||
"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,
|
||||
// Editor tools
|
||||
"get_editor_command" => tool_get_editor_command(&args, ctx),
|
||||
// Completion reporting
|
||||
"report_completion" => tool_report_completion(&args, ctx).await,
|
||||
// Lifecycle tools
|
||||
"accept_story" => tool_accept_story(&args, ctx),
|
||||
// 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}"))
|
||||
}
|
||||
|
||||
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> {
|
||||
let story_id = args
|
||||
.get("story_id")
|
||||
@@ -1591,7 +1533,7 @@ mod tests {
|
||||
assert!(names.contains(&"list_worktrees"));
|
||||
assert!(names.contains(&"remove_worktree"));
|
||||
assert!(names.contains(&"get_editor_command"));
|
||||
assert!(names.contains(&"report_completion"));
|
||||
assert!(!names.contains(&"report_completion"));
|
||||
assert!(names.contains(&"accept_story"));
|
||||
assert!(names.contains(&"check_criterion"));
|
||||
assert!(names.contains(&"set_test_plan"));
|
||||
@@ -1601,7 +1543,7 @@ mod tests {
|
||||
assert!(names.contains(&"merge_agent_work"));
|
||||
assert!(names.contains(&"move_story_to_merge"));
|
||||
assert!(names.contains(&"request_qa"));
|
||||
assert_eq!(tools.len(), 27);
|
||||
assert_eq!(tools.len(), 26);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1850,71 +1792,6 @@ mod tests {
|
||||
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 ─────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user