huskies: merge 963
This commit is contained in:
@@ -134,7 +134,11 @@ pub struct Claim {
|
|||||||
/// The numeric prefix of the epic's story_id (e.g. `EpicId(9990)` for the
|
/// The numeric prefix of the epic's story_id (e.g. `EpicId(9990)` for the
|
||||||
/// epic whose CRDT story_id register holds `"9990"`). Epics are always
|
/// epic whose CRDT story_id register holds `"9990"`). Epics are always
|
||||||
/// created with a pure-numeric story_id by `create_epic_file`.
|
/// created with a pure-numeric story_id by `create_epic_file`.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
///
|
||||||
|
/// Serialises as a decimal string (`"9990"`) so JSON consumers see a stable
|
||||||
|
/// string identifier regardless of whether the inner value fits in a JS integer.
|
||||||
|
/// Deserialises from a decimal string.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct EpicId(pub u32);
|
pub struct EpicId(pub u32);
|
||||||
|
|
||||||
impl EpicId {
|
impl EpicId {
|
||||||
@@ -158,6 +162,21 @@ impl std::fmt::Display for EpicId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl serde::Serialize for EpicId {
|
||||||
|
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
serializer.serialize_str(&self.0.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> serde::Deserialize<'de> for EpicId {
|
||||||
|
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
s.parse::<u32>()
|
||||||
|
.map(EpicId)
|
||||||
|
.map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A typed snapshot of a single pipeline work item derived from the CRDT document.
|
/// A typed snapshot of a single pipeline work item derived from the CRDT document.
|
||||||
///
|
///
|
||||||
/// Access fields exclusively through the typed accessor methods — raw field access is
|
/// Access fields exclusively through the typed accessor methods — raw field access is
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ use std::path::Path;
|
|||||||
/// Agent assignment embedded in a pipeline stage item.
|
/// Agent assignment embedded in a pipeline stage item.
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub struct AgentAssignment {
|
pub struct AgentAssignment {
|
||||||
pub agent_name: String,
|
pub agent_name: crate::config::AgentName,
|
||||||
pub model: Option<String>,
|
pub model: Option<String>,
|
||||||
pub status: String,
|
pub status: crate::agents::AgentStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A story/bug/spike item as it appears in a pipeline stage listing.
|
/// A story/bug/spike item as it appears in a pipeline stage listing.
|
||||||
@@ -27,9 +27,9 @@ pub struct UpcomingStory {
|
|||||||
/// True when the item is held in QA for human review.
|
/// True when the item is held in QA for human review.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub review_hold: Option<bool>,
|
pub review_hold: Option<bool>,
|
||||||
/// QA mode for this item: "human", "server", or "agent".
|
/// QA mode for this item.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub qa: Option<String>,
|
pub qa: Option<crate::io::story_metadata::QaMode>,
|
||||||
/// Number of retries at the current pipeline stage.
|
/// Number of retries at the current pipeline stage.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub retry_count: Option<u32>,
|
pub retry_count: Option<u32>,
|
||||||
@@ -42,9 +42,9 @@ pub struct UpcomingStory {
|
|||||||
/// Story numbers this story depends on.
|
/// Story numbers this story depends on.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub depends_on: Option<Vec<u32>>,
|
pub depends_on: Option<Vec<u32>>,
|
||||||
/// Epic this item belongs to (numeric ID as string, e.g. "880").
|
/// Epic this item belongs to.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub epic_id: Option<String>,
|
pub epic_id: Option<crate::crdt_state::EpicId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validation outcome for a single story.
|
/// Validation outcome for a single story.
|
||||||
@@ -117,10 +117,8 @@ pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let qa = view
|
let qa = view.as_ref().and_then(|v| v.qa_mode());
|
||||||
.as_ref()
|
let epic_id = view.as_ref().and_then(|v| v.epic());
|
||||||
.and_then(|v| v.qa_mode().map(|q| q.as_str().to_string()));
|
|
||||||
let epic_id = view.as_ref().and_then(|v| v.epic().map(|e| e.to_string()));
|
|
||||||
let merge_failure = crate::crdt_state::read_merge_job(sid).and_then(|j| j.error);
|
let merge_failure = crate::crdt_state::read_merge_job(sid).and_then(|j| j.error);
|
||||||
|
|
||||||
let story = UpcomingStory {
|
let story = UpcomingStory {
|
||||||
@@ -217,16 +215,19 @@ fn build_active_agent_map(ctx: &AppContext) -> HashMap<String, AgentAssignment>
|
|||||||
if !matches!(agent.status, AgentStatus::Pending | AgentStatus::Running) {
|
if !matches!(agent.status, AgentStatus::Pending | AgentStatus::Running) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let Ok(agent_name) = agent.agent_name.parse::<crate::config::AgentName>() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
let model = config_opt
|
let model = config_opt
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|cfg| cfg.find_agent(&agent.agent_name))
|
.and_then(|cfg| cfg.find_agent(agent_name.as_str()))
|
||||||
.and_then(|ac| ac.model.clone());
|
.and_then(|ac| ac.model.clone());
|
||||||
map.insert(
|
map.insert(
|
||||||
agent.story_id.clone(),
|
agent.story_id.clone(),
|
||||||
AgentAssignment {
|
AgentAssignment {
|
||||||
agent_name: agent.agent_name,
|
agent_name,
|
||||||
model,
|
model,
|
||||||
status: agent.status.to_string(),
|
status: agent.status,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -244,8 +245,7 @@ pub fn load_upcoming_stories(_ctx: &AppContext) -> Result<Vec<UpcomingStory>, St
|
|||||||
.filter(|item| matches!(item.stage, Stage::Backlog))
|
.filter(|item| matches!(item.stage, Stage::Backlog))
|
||||||
.map(|item| {
|
.map(|item| {
|
||||||
let sid = &item.story_id.0;
|
let sid = &item.story_id.0;
|
||||||
let epic_id =
|
let epic_id = crate::crdt_state::read_item(sid).and_then(|v| v.epic());
|
||||||
crate::crdt_state::read_item(sid).and_then(|v| v.epic().map(|e| e.to_string()));
|
|
||||||
UpcomingStory {
|
UpcomingStory {
|
||||||
story_id: item.story_id.0.clone(),
|
story_id: item.story_id.0.clone(),
|
||||||
name: if item.name.is_empty() {
|
name: if item.name.is_empty() {
|
||||||
@@ -412,8 +412,8 @@ mod tests {
|
|||||||
"running agent should appear on work item"
|
"running agent should appear on work item"
|
||||||
);
|
);
|
||||||
let agent = item.agent.as_ref().unwrap();
|
let agent = item.agent.as_ref().unwrap();
|
||||||
assert_eq!(agent.agent_name, "coder-1");
|
assert_eq!(agent.agent_name, crate::config::AgentName::Coder1);
|
||||||
assert_eq!(agent.status, "running");
|
assert_eq!(agent.status, crate::agents::AgentStatus::Running);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -480,7 +480,10 @@ mod tests {
|
|||||||
item.agent.is_some(),
|
item.agent.is_some(),
|
||||||
"pending agent should appear on work item"
|
"pending agent should appear on work item"
|
||||||
);
|
);
|
||||||
assert_eq!(item.agent.as_ref().unwrap().status, "pending");
|
assert_eq!(
|
||||||
|
item.agent.as_ref().unwrap().status,
|
||||||
|
crate::agents::AgentStatus::Pending
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ pub struct WorkItemContent {
|
|||||||
pub content: String,
|
pub content: String,
|
||||||
pub stage: crate::pipeline_state::Stage,
|
pub stage: crate::pipeline_state::Stage,
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub agent: Option<String>,
|
pub agent: Option<crate::config::AgentName>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A single entry in the project's configured agent roster.
|
/// A single entry in the project's configured agent roster.
|
||||||
@@ -163,9 +163,7 @@ pub fn get_work_item_content(
|
|||||||
|
|
||||||
let crdt_view = crate::crdt_state::read_item(story_id);
|
let crdt_view = crate::crdt_state::read_item(story_id);
|
||||||
let crdt_name = crdt_view.as_ref().map(|v| v.name().to_string());
|
let crdt_name = crdt_view.as_ref().map(|v| v.name().to_string());
|
||||||
let crdt_agent = crdt_view
|
let crdt_agent = crdt_view.as_ref().and_then(|v| v.agent());
|
||||||
.as_ref()
|
|
||||||
.and_then(|v| v.agent().map(|a| a.to_string()));
|
|
||||||
|
|
||||||
for (stage_dir, stage) in &stages {
|
for (stage_dir, stage) in &stages {
|
||||||
if let Some(content) = io::read_work_item_from_stage(&work_dir, stage_dir, &filename)? {
|
if let Some(content) = io::read_work_item_from_stage(&work_dir, stage_dir, &filename)? {
|
||||||
@@ -173,7 +171,7 @@ pub fn get_work_item_content(
|
|||||||
content,
|
content,
|
||||||
stage: stage.clone(),
|
stage: stage.clone(),
|
||||||
name: crdt_name.clone(),
|
name: crdt_name.clone(),
|
||||||
agent: crdt_agent.clone(),
|
agent: crdt_agent,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,9 +293,9 @@ mod tests {
|
|||||||
error: None,
|
error: None,
|
||||||
merge_failure: None,
|
merge_failure: None,
|
||||||
agent: Some(crate::http::workflow::pipeline::AgentAssignment {
|
agent: Some(crate::http::workflow::pipeline::AgentAssignment {
|
||||||
agent_name: "coder-1".to_string(),
|
agent_name: crate::config::AgentName::Coder1,
|
||||||
model: Some("claude-3-5-sonnet".to_string()),
|
model: Some("claude-3-5-sonnet".to_string()),
|
||||||
status: "running".to_string(),
|
status: crate::agents::AgentStatus::Running,
|
||||||
}),
|
}),
|
||||||
review_hold: None,
|
review_hold: None,
|
||||||
qa: None,
|
qa: None,
|
||||||
|
|||||||
Reference in New Issue
Block a user