Story 53: Add QA agent role with request_qa MCP tool
- Add `qa` agent entry to `.story_kit/project.toml` with a detailed prompt covering code quality scan, test verification, manual testing support, and structured report generation - Add `move_story_to_qa` function in `agents.rs` that moves a work item from `work/2_current/` to `work/3_qa/` and auto-commits (idempotent) - Add `request_qa` MCP tool in `mcp.rs` that moves the story to `work/3_qa/` and starts the QA agent on the existing worktree - Add unit tests for `move_story_to_qa` (moves, idempotent, error cases) - Update `tools_list_returns_all_tools` test to expect 27 tools Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -943,6 +943,42 @@ pub fn move_story_to_merge(project_root: &Path, story_id: &str) -> Result<(), St
|
||||
)
|
||||
}
|
||||
|
||||
/// Move a story/bug from `work/2_current/` to `work/3_qa/` and auto-commit.
|
||||
///
|
||||
/// This stages a work item for QA review before merging to master.
|
||||
/// Idempotent: if already in `3_qa/`, returns Ok without committing.
|
||||
pub fn move_story_to_qa(project_root: &Path, story_id: &str) -> Result<(), String> {
|
||||
let sk = project_root.join(".story_kit").join("work");
|
||||
let current_path = sk.join("2_current").join(format!("{story_id}.md"));
|
||||
let qa_dir = sk.join("3_qa");
|
||||
let qa_path = qa_dir.join(format!("{story_id}.md"));
|
||||
|
||||
if qa_path.exists() {
|
||||
// Already in 3_qa/ — idempotent, nothing to do.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !current_path.exists() {
|
||||
return Err(format!(
|
||||
"Work item '{story_id}' not found in work/2_current/. Cannot move to 3_qa/."
|
||||
));
|
||||
}
|
||||
|
||||
std::fs::create_dir_all(&qa_dir)
|
||||
.map_err(|e| format!("Failed to create work/3_qa/ directory: {e}"))?;
|
||||
std::fs::rename(¤t_path, &qa_path)
|
||||
.map_err(|e| format!("Failed to move '{story_id}' to 3_qa/: {e}"))?;
|
||||
|
||||
eprintln!("[lifecycle] Moved '{story_id}' from work/2_current/ to work/3_qa/");
|
||||
|
||||
let msg = format!("story-kit: queue {story_id} for QA");
|
||||
git_stage_and_commit(
|
||||
project_root,
|
||||
&[qa_path.as_path(), current_path.as_path()],
|
||||
&msg,
|
||||
)
|
||||
}
|
||||
|
||||
/// Move a bug from `work/2_current/` or `work/1_upcoming/` to `work/5_archived/` and auto-commit.
|
||||
///
|
||||
/// * If the bug is in `2_current/`, it is moved to `5_archived/` and committed.
|
||||
@@ -1901,6 +1937,75 @@ mod tests {
|
||||
assert!(result.unwrap_err().contains("not found in work/2_current/"));
|
||||
}
|
||||
|
||||
// ── move_story_to_qa tests ────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn move_story_to_qa_moves_file_and_commits() {
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
let tmp = tempdir().unwrap();
|
||||
let repo = tmp.path();
|
||||
init_git_repo(repo);
|
||||
|
||||
let current_dir = repo.join(".story_kit/work/2_current");
|
||||
fs::create_dir_all(¤t_dir).unwrap();
|
||||
let story_file = current_dir.join("30_story_qa_test.md");
|
||||
fs::write(&story_file, "---\nname: QA Test\ntest_plan: approved\n---\n").unwrap();
|
||||
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "add story"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
move_story_to_qa(repo, "30_story_qa_test").unwrap();
|
||||
|
||||
let qa_path = repo.join(".story_kit/work/3_qa/30_story_qa_test.md");
|
||||
assert!(!story_file.exists(), "2_current file should be gone");
|
||||
assert!(qa_path.exists(), "3_qa file should exist");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_story_to_qa_idempotent_when_already_in_qa() {
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
let tmp = tempdir().unwrap();
|
||||
let repo = tmp.path();
|
||||
init_git_repo(repo);
|
||||
|
||||
let qa_dir = repo.join(".story_kit/work/3_qa");
|
||||
fs::create_dir_all(&qa_dir).unwrap();
|
||||
fs::write(
|
||||
qa_dir.join("31_story_test.md"),
|
||||
"---\nname: Test\ntest_plan: approved\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Should succeed without error even though there's nothing to move
|
||||
move_story_to_qa(repo, "31_story_test").unwrap();
|
||||
assert!(qa_dir.join("31_story_test.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_story_to_qa_errors_when_not_in_current() {
|
||||
use tempfile::tempdir;
|
||||
|
||||
let tmp = tempdir().unwrap();
|
||||
let repo = tmp.path();
|
||||
init_git_repo(repo);
|
||||
|
||||
let result = move_story_to_qa(repo, "99_nonexistent");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("not found in work/2_current/"));
|
||||
}
|
||||
|
||||
// ── move_story_to_archived with 4_merge source ────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user