huskies: merge 895
This commit is contained in:
@@ -2,9 +2,36 @@
|
||||
|
||||
use crate::agents::{AgentPool, AgentStatus};
|
||||
use crate::config::ProjectConfig;
|
||||
use crate::pipeline_state::{PipelineItem, Stage};
|
||||
use crate::pipeline_state::{ArchiveReason, PipelineItem, Stage};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
/// Map a stage to its display section label, or `None` to skip it entirely.
|
||||
///
|
||||
/// This is the single source of truth for the "where does this item appear"
|
||||
/// decision. It mirrors the bucket routing in `http/workflow/pipeline.rs`
|
||||
/// so that chat output and the web UI are always consistent.
|
||||
///
|
||||
/// `Stage::Frozen { resume_to }` is handled recursively: a frozen story
|
||||
/// appears in the same section its `resume_to` stage would land in.
|
||||
pub(crate) fn display_section(s: &Stage) -> Option<&'static str> {
|
||||
match s {
|
||||
Stage::Upcoming | Stage::Backlog => Some("Backlog"),
|
||||
Stage::Coding
|
||||
| Stage::Blocked { .. }
|
||||
| Stage::Archived {
|
||||
reason: ArchiveReason::Blocked { .. },
|
||||
..
|
||||
} => Some("In Progress"),
|
||||
Stage::Qa | Stage::ReviewHold { .. } => Some("QA"),
|
||||
Stage::Merge { .. } | Stage::MergeFailure { .. } | Stage::MergeFailureFinal { .. } => {
|
||||
Some("Merge")
|
||||
}
|
||||
Stage::Done { .. } => Some("Done"),
|
||||
Stage::Frozen { resume_to } => display_section(resume_to),
|
||||
Stage::Archived { .. } => None, // other archived variants are hidden
|
||||
}
|
||||
}
|
||||
|
||||
/// Check which dependency numbers from `item.depends_on` are unmet.
|
||||
///
|
||||
/// A dependency is considered met if the dep is in `Done` or `Archived` stage
|
||||
@@ -95,26 +122,24 @@ pub(crate) fn build_status_from_items(
|
||||
|
||||
let mut out = String::from("**Pipeline Status**\n\n");
|
||||
|
||||
// Active pipeline stages to display (Archived is handled separately below).
|
||||
type StagePredicate = fn(&Stage) -> bool;
|
||||
let stage_filters: &[(&str, StagePredicate)] = &[
|
||||
("Backlog", |s| matches!(s, Stage::Backlog)),
|
||||
("In Progress", |s| matches!(s, Stage::Coding)),
|
||||
("QA", |s| matches!(s, Stage::Qa)),
|
||||
("Merge", |s| matches!(s, Stage::Merge { .. })),
|
||||
("Done", |s| matches!(s, Stage::Done { .. })),
|
||||
];
|
||||
// Render each display section in order. Blocked items appear in-place
|
||||
// under their stage section (determined by `display_section`); there is
|
||||
// no separate "Blocked" section. Frozen items appear under the section
|
||||
// their `resume_to` stage maps to.
|
||||
let sections = ["Backlog", "In Progress", "QA", "Merge", "Done"];
|
||||
|
||||
for (label, filter) in stage_filters {
|
||||
let mut stage_items: Vec<&PipelineItem> =
|
||||
items.iter().filter(|i| filter(&i.stage)).collect();
|
||||
stage_items.sort_by(|a, b| a.story_id.0.cmp(&b.story_id.0));
|
||||
let count = stage_items.len();
|
||||
for label in sections {
|
||||
let mut section_items: Vec<&PipelineItem> = items
|
||||
.iter()
|
||||
.filter(|i| display_section(&i.stage) == Some(label))
|
||||
.collect();
|
||||
section_items.sort_by(|a, b| a.story_id.0.cmp(&b.story_id.0));
|
||||
let count = section_items.len();
|
||||
out.push_str(&format!("**{label}** ({count})\n"));
|
||||
if stage_items.is_empty() {
|
||||
if section_items.is_empty() {
|
||||
out.push_str(" *(none)*\n");
|
||||
} else {
|
||||
for item in &stage_items {
|
||||
for item in §ion_items {
|
||||
out.push_str(&render_item_line(
|
||||
item,
|
||||
items,
|
||||
@@ -129,26 +154,6 @@ pub(crate) fn build_status_from_items(
|
||||
out.push('\n');
|
||||
}
|
||||
|
||||
// Blocked items: Archived { reason: Blocked } shown with 🔴 indicator.
|
||||
let mut blocked_items: Vec<&PipelineItem> =
|
||||
items.iter().filter(|i| i.stage.is_blocked()).collect();
|
||||
blocked_items.sort_by(|a, b| a.story_id.0.cmp(&b.story_id.0));
|
||||
if !blocked_items.is_empty() {
|
||||
out.push_str(&format!("**Blocked** ({})\n", blocked_items.len()));
|
||||
for item in &blocked_items {
|
||||
out.push_str(&render_item_line(
|
||||
item,
|
||||
items,
|
||||
&active_map,
|
||||
&cost_by_story,
|
||||
&config,
|
||||
&running_merges,
|
||||
&merge_failures,
|
||||
));
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
|
||||
// Free agents: configured agents not currently running or pending.
|
||||
out.push_str("**Free Agents**\n");
|
||||
if let Some(cfg) = &config {
|
||||
@@ -198,7 +203,8 @@ fn render_item_line(
|
||||
} else {
|
||||
Some(item.name.as_str())
|
||||
};
|
||||
let frozen = crate::io::story_metadata::is_story_frozen_in_store(story_id);
|
||||
// Use the typed CRDT stage as the sole source of truth (story 945).
|
||||
let frozen = item.stage.is_frozen();
|
||||
let base_label = super::story_short_label(story_id, name_opt);
|
||||
let display = if frozen {
|
||||
format!("\u{2744}\u{FE0F} {base_label}") // ❄️ prefix
|
||||
@@ -219,9 +225,24 @@ fn render_item_line(
|
||||
format!(" *(waiting on: {})*", nums.join(", "))
|
||||
};
|
||||
|
||||
// Merge-stage items get a dedicated breakdown indicator instead of the
|
||||
// generic traffic-light dot.
|
||||
if matches!(item.stage, Stage::Merge { .. }) {
|
||||
// Merge-stage items get dedicated breakdown indicators instead of the
|
||||
// generic traffic-light dot. MergeFailure / MergeFailureFinal items
|
||||
// now also appear in the Merge section (in-place) so they are handled
|
||||
// here alongside normal Merge items.
|
||||
if matches!(
|
||||
item.stage,
|
||||
Stage::Merge { .. } | Stage::MergeFailure { .. } | Stage::MergeFailureFinal { .. }
|
||||
) {
|
||||
// MergeFailure and MergeFailureFinal carry their reason directly on
|
||||
// the stage variant — always show ⛔ with the failure snippet.
|
||||
match &item.stage {
|
||||
Stage::MergeFailure { reason, .. } | Stage::MergeFailureFinal { reason } => {
|
||||
let snippet = first_non_empty_snippet(reason, 120);
|
||||
return format!(" \u{26D4} {display}{cost_suffix}{dep_suffix} — {snippet}\n");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let in_det_merge = running_merges.contains(story_id);
|
||||
let merge_failure = merge_failures.get(story_id);
|
||||
if in_det_merge {
|
||||
|
||||
Reference in New Issue
Block a user