story-kit: merge 247_story_human_qa_gate_with_rejection_flow

This commit is contained in:
Dave
2026-03-18 15:45:45 +00:00
parent 1faacd7812
commit 9352443555
11 changed files with 557 additions and 26 deletions

View File

@@ -889,25 +889,37 @@ impl AgentPool {
};
if coverage_passed {
// Spikes skip merge — they stay in 3_qa/ for human review.
if super::lifecycle::item_type_from_id(story_id) == "spike" {
// Mark the spike as held for review so auto-assign won't
// restart QA on it.
// Check whether this item needs human review before merging.
let needs_human_review = {
let item_type = super::lifecycle::item_type_from_id(story_id);
if item_type == "spike" {
true // Spikes always need human review.
} else {
// Stories/bugs: check the manual_qa front matter field (defaults to true).
let qa_dir = project_root.join(".story_kit/work/3_qa");
let story_path = qa_dir.join(format!("{story_id}.md"));
crate::io::story_metadata::requires_manual_qa(&story_path)
}
};
if needs_human_review {
// Hold in 3_qa/ for human review.
let qa_dir = project_root.join(".story_kit/work/3_qa");
let spike_path = qa_dir.join(format!("{story_id}.md"));
if let Err(e) = crate::io::story_metadata::write_review_hold(&spike_path) {
let story_path = qa_dir.join(format!("{story_id}.md"));
if let Err(e) = crate::io::story_metadata::write_review_hold(&story_path) {
slog_error!("[pipeline] Failed to set review_hold on '{story_id}': {e}");
}
slog!(
"[pipeline] QA passed for spike '{story_id}'. \
Stopping for human review (skipping merge). \
"[pipeline] QA passed for '{story_id}'. \
Holding for human review. \
Worktree preserved at: {worktree_path:?}"
);
// Free up the QA slot without advancing the spike.
// Free up the QA slot without advancing.
self.auto_assign_available_work(&project_root).await;
} else {
slog!(
"[pipeline] QA passed gates and coverage for '{story_id}'. Moving to merge."
"[pipeline] QA passed gates and coverage for '{story_id}'. \
manual_qa: false — moving directly to merge."
);
if let Err(e) = super::lifecycle::move_story_to_merge(&project_root, story_id) {
slog_error!("[pipeline] Failed to move '{story_id}' to 4_merge/: {e}");
@@ -1746,23 +1758,35 @@ impl AgentPool {
};
if coverage_passed {
// Spikes skip the merge stage — stay in 3_qa/ for human review.
if super::lifecycle::item_type_from_id(story_id) == "spike" {
let spike_path = project_root
// Check whether this item needs human review before merging.
let needs_human_review = {
let item_type = super::lifecycle::item_type_from_id(story_id);
if item_type == "spike" {
true
} else {
let story_path = project_root
.join(".story_kit/work/3_qa")
.join(format!("{story_id}.md"));
crate::io::story_metadata::requires_manual_qa(&story_path)
}
};
if needs_human_review {
let story_path = project_root
.join(".story_kit/work/3_qa")
.join(format!("{story_id}.md"));
if let Err(e) = crate::io::story_metadata::write_review_hold(&spike_path) {
if let Err(e) = crate::io::story_metadata::write_review_hold(&story_path) {
eprintln!(
"[startup:reconcile] Failed to set review_hold on spike '{story_id}': {e}"
"[startup:reconcile] Failed to set review_hold on '{story_id}': {e}"
);
}
eprintln!(
"[startup:reconcile] Spike '{story_id}' passed QA — holding for human review."
"[startup:reconcile] '{story_id}' passed QA — holding for human review."
);
let _ = progress_tx.send(ReconciliationEvent {
story_id: story_id.clone(),
status: "review_hold".to_string(),
message: "Spike passed QA — waiting for human review.".to_string(),
message: "Passed QA — waiting for human review.".to_string(),
});
} else if let Err(e) = super::lifecycle::move_story_to_merge(project_root, story_id) {
eprintln!(
@@ -2655,7 +2679,12 @@ mod tests {
// Set up story in 3_qa/
let qa_dir = root.join(".story_kit/work/3_qa");
fs::create_dir_all(&qa_dir).unwrap();
fs::write(qa_dir.join("51_story_test.md"), "test").unwrap();
// manual_qa: false so the story skips human review and goes straight to merge.
fs::write(
qa_dir.join("51_story_test.md"),
"---\nname: Test\nmanual_qa: false\n---\ntest",
)
.unwrap();
let pool = AgentPool::new_test(3001);
pool.run_pipeline_advance(