story-kit: merge 311_story_server_enforced_retry_limits_for_failed_merge_and_empty_diff_stories

This commit is contained in:
Dave
2026-03-19 16:34:11 +00:00
parent 662e00f94a
commit 3b887e3085
9 changed files with 346 additions and 65 deletions

View File

@@ -1156,7 +1156,7 @@ fn tool_get_pipeline_status(ctx: &AppContext) -> Result<String, String> {
items
.iter()
.map(|s| {
json!({
let mut item = json!({
"story_id": s.story_id,
"name": s.name,
"stage": stage,
@@ -1165,7 +1165,19 @@ fn tool_get_pipeline_status(ctx: &AppContext) -> Result<String, String> {
"model": a.model,
"status": a.status,
})),
})
});
// Include blocked/retry_count when present so callers can
// identify stories stuck in the pipeline.
if let Some(true) = s.blocked {
item["blocked"] = json!(true);
}
if let Some(rc) = s.retry_count {
item["retry_count"] = json!(rc);
}
if let Some(ref mf) = s.merge_failure {
item["merge_failure"] = json!(mf);
}
item
})
.collect()
}

View File

@@ -30,6 +30,12 @@ pub struct UpcomingStory {
/// QA mode for this item: "human", "server", or "agent".
#[serde(skip_serializing_if = "Option::is_none")]
pub qa: Option<String>,
/// Number of retries at the current pipeline stage.
#[serde(skip_serializing_if = "Option::is_none")]
pub retry_count: Option<u32>,
/// True when the story has exceeded its retry limit and will not be auto-assigned.
#[serde(skip_serializing_if = "Option::is_none")]
pub blocked: Option<bool>,
}
pub struct StoryValidationResult {
@@ -123,12 +129,12 @@ fn load_stage_items(
.to_string();
let contents = fs::read_to_string(&path)
.map_err(|e| format!("Failed to read story file {}: {e}", path.display()))?;
let (name, error, merge_failure, review_hold, qa) = match parse_front_matter(&contents) {
Ok(meta) => (meta.name, None, meta.merge_failure, meta.review_hold, meta.qa.map(|m| m.as_str().to_string())),
Err(e) => (None, Some(e.to_string()), None, None, None),
let (name, error, merge_failure, review_hold, qa, retry_count, blocked) = match parse_front_matter(&contents) {
Ok(meta) => (meta.name, None, meta.merge_failure, meta.review_hold, meta.qa.map(|m| m.as_str().to_string()), meta.retry_count, meta.blocked),
Err(e) => (None, Some(e.to_string()), None, None, None, None, None),
};
let agent = agent_map.get(&story_id).cloned();
stories.push(UpcomingStory { story_id, name, error, merge_failure, agent, review_hold, qa });
stories.push(UpcomingStory { story_id, name, error, merge_failure, agent, review_hold, qa, retry_count, blocked });
}
stories.sort_by(|a, b| a.story_id.cmp(&b.story_id));

View File

@@ -739,6 +739,8 @@ mod tests {
agent: None,
review_hold: None,
qa: None,
retry_count: None,
blocked: None,
};
let resp = WsResponse::PipelineState {
backlog: vec![story],
@@ -878,6 +880,8 @@ mod tests {
agent: None,
review_hold: None,
qa: None,
retry_count: None,
blocked: None,
}],
current: vec![UpcomingStory {
story_id: "2_story_b".to_string(),
@@ -887,6 +891,8 @@ mod tests {
agent: None,
review_hold: None,
qa: None,
retry_count: None,
blocked: None,
}],
qa: vec![],
merge: vec![],
@@ -898,6 +904,8 @@ mod tests {
agent: None,
review_hold: None,
qa: None,
retry_count: None,
blocked: None,
}],
};
let resp: WsResponse = state.into();
@@ -1056,6 +1064,8 @@ mod tests {
}),
review_hold: None,
qa: None,
retry_count: None,
blocked: None,
}],
qa: vec![],
merge: vec![],