huskies: merge 671_refactor_migrate_pipeline_state_consumers_from_string_comparisons_to_typed_pipelinestage_enum

This commit is contained in:
dave
2026-04-27 16:35:25 +00:00
parent 39a9766d7d
commit 4a0f57478c
15 changed files with 161 additions and 103 deletions
@@ -3,6 +3,7 @@
use std::path::Path;
use tokio::sync::broadcast;
use crate::pipeline_state::Stage;
use crate::worktree;
use super::super::super::ReconciliationEvent;
@@ -52,20 +53,20 @@ impl AgentPool {
let wt_path = wt_entry.path.clone();
// Determine which active stage the story is in.
let stage_dir = match find_active_story_stage(project_root, story_id) {
let stage = match find_active_story_stage(project_root, story_id) {
Some(s) => s,
None => continue, // Not in any active stage (backlog/archived or unknown).
};
// 4_merge/ is left for auto_assign to handle with a fresh mergemaster.
if stage_dir == "4_merge" {
if matches!(stage, Stage::Merge { .. }) {
continue;
}
let _ = progress_tx.send(ReconciliationEvent {
story_id: story_id.clone(),
status: "checking".to_string(),
message: format!("Checking for committed work in {stage_dir}/"),
message: format!("Checking for committed work in {}/", stage.dir_name()),
});
// Check whether the worktree has commits ahead of the base branch.
@@ -78,7 +79,8 @@ impl AgentPool {
if !has_work {
eprintln!(
"[startup:reconcile] No committed work for '{story_id}' in {stage_dir}/; skipping."
"[startup:reconcile] No committed work for '{story_id}' in {}/; skipping.",
stage.dir_name()
);
let _ = progress_tx.send(ReconciliationEvent {
story_id: story_id.clone(),
@@ -89,7 +91,8 @@ impl AgentPool {
}
eprintln!(
"[startup:reconcile] Found committed work for '{story_id}' in {stage_dir}/. Running acceptance gates."
"[startup:reconcile] Found committed work for '{story_id}' in {}/. Running acceptance gates.",
stage.dir_name()
);
let _ = progress_tx.send(ReconciliationEvent {
story_id: story_id.clone(),
@@ -130,7 +133,8 @@ impl AgentPool {
if !gates_passed {
eprintln!(
"[startup:reconcile] Gates failed for '{story_id}': {gate_output}\n\
Leaving in {stage_dir}/ for auto-assign to restart the agent."
Leaving in {}/ for auto-assign to restart the agent.",
stage.dir_name()
);
let _ = progress_tx.send(ReconciliationEvent {
story_id: story_id.clone(),
@@ -140,9 +144,12 @@ impl AgentPool {
continue;
}
eprintln!("[startup:reconcile] Gates passed for '{story_id}' (stage: {stage_dir}/).");
eprintln!(
"[startup:reconcile] Gates passed for '{story_id}' (stage: {}/).",
stage.dir_name()
);
if stage_dir == "2_current" {
if matches!(stage, Stage::Coding) {
// Coder stage — determine qa mode to decide next step.
let qa_mode = {
let item_type = crate::agents::lifecycle::item_type_from_id(story_id);
@@ -232,7 +239,7 @@ impl AgentPool {
}
}
}
} else if stage_dir == "3_qa" {
} else if matches!(stage, Stage::Qa) {
// QA stage → run coverage gate before advancing to merge.
let wt_path_for_cov = wt_path.clone();
let coverage_result = tokio::task::spawn_blocking(move || {
+14 -10
View File
@@ -342,17 +342,21 @@ impl AgentPool {
// has already reached done or archived (e.g. a previous mergemaster
// succeeded), this advance is a zombie — skip it entirely to avoid
// phantom notifications and redundant post-merge test runs.
if let Ok(Some(typed_item)) = crate::pipeline_state::read_typed(story_id) {
if let Ok(Some(typed_item)) = crate::pipeline_state::read_typed(story_id)
&& matches!(
typed_item.stage,
crate::pipeline_state::Stage::Done { .. }
| crate::pipeline_state::Stage::Archived { .. }
)
{
let current_dir = typed_item.stage.dir_name();
if current_dir == "5_done" || current_dir == "6_archived" {
slog!(
"[pipeline] Skipping stale mergemaster advance for '{story_id}': \
story is already in work/{current_dir}/"
);
// Skip pipeline advancement — do not run post-merge tests,
// do not emit notifications, do not restart agents.
return;
}
slog!(
"[pipeline] Skipping stale mergemaster advance for '{story_id}': \
story is already in work/{current_dir}/"
);
// Skip pipeline advancement — do not run post-merge tests,
// do not emit notifications, do not restart agents.
return;
}
// Block advancement if the mergemaster explicitly reported a failure.
+8 -6
View File
@@ -3,6 +3,7 @@
use std::path::Path;
use crate::config::ProjectConfig;
use crate::pipeline_state::Stage;
use super::super::super::{PipelineStage, agent_config_stage, pipeline_stage};
use super::super::worktree::find_active_story_stage;
@@ -30,19 +31,20 @@ pub(super) fn validate_agent_stage(
if agent_stage == PipelineStage::Other {
return Ok(());
}
let Some(story_stage_dir) = find_active_story_stage(project_root, story_id) else {
let Some(story_stage) = find_active_story_stage(project_root, story_id) else {
return Ok(());
};
let expected_stage = match story_stage_dir {
"2_current" => PipelineStage::Coder,
"3_qa" => PipelineStage::Qa,
"4_merge" => PipelineStage::Mergemaster,
let expected_stage = match story_stage {
Stage::Coding => PipelineStage::Coder,
Stage::Qa => PipelineStage::Qa,
Stage::Merge { .. } => PipelineStage::Mergemaster,
_ => PipelineStage::Other,
};
if expected_stage != PipelineStage::Other && expected_stage != agent_stage {
return Err(format!(
"Agent '{name}' (stage: {agent_stage:?}) cannot be assigned to \
story '{story_id}' in {story_stage_dir}/ (requires stage: {expected_stage:?})"
story '{story_id}' in {}/ (requires stage: {expected_stage:?})",
story_stage.dir_name()
));
}
Ok(())
+13 -13
View File
@@ -21,16 +21,16 @@ impl AgentPool {
}
}
/// Return the active pipeline stage directory name for `story_id`, or `None` if the
/// story is not in any active stage (`2_current/`, `3_qa/`, `4_merge/`).
/// Return the active pipeline stage for `story_id`, or `None` if the story is not
/// in any active stage (`2_current/`, `3_qa/`, `4_merge/`).
pub(super) fn find_active_story_stage(
_project_root: &Path,
story_id: &str,
) -> Option<&'static str> {
) -> Option<crate::pipeline_state::Stage> {
if let Ok(Some(item)) = crate::pipeline_state::read_typed(story_id)
&& item.stage.is_active()
{
return Some(item.stage.dir_name());
return Some(item.stage);
}
None
}
@@ -44,10 +44,10 @@ mod tests {
crate::db::ensure_content_store();
crate::db::write_item_with_content("10_story_test", "2_current", "---\nname: Test\n---\n");
let tmp = tempfile::tempdir().unwrap();
assert_eq!(
assert!(matches!(
find_active_story_stage(tmp.path(), "10_story_test"),
Some("2_current")
);
Some(crate::pipeline_state::Stage::Coding)
));
}
#[test]
@@ -55,10 +55,10 @@ mod tests {
crate::db::ensure_content_store();
crate::db::write_item_with_content("11_story_test", "3_qa", "---\nname: Test\n---\n");
let tmp = tempfile::tempdir().unwrap();
assert_eq!(
assert!(matches!(
find_active_story_stage(tmp.path(), "11_story_test"),
Some("3_qa")
);
Some(crate::pipeline_state::Stage::Qa)
));
}
#[test]
@@ -66,10 +66,10 @@ mod tests {
crate::db::ensure_content_store();
crate::db::write_item_with_content("12_story_test", "4_merge", "---\nname: Test\n---\n");
let tmp = tempfile::tempdir().unwrap();
assert_eq!(
assert!(matches!(
find_active_story_stage(tmp.path(), "12_story_test"),
Some("4_merge")
);
Some(crate::pipeline_state::Stage::Merge { .. })
));
}
#[test]