story-kit: merge 225_story_surface_merge_conflicts_and_failures_in_the_web_ui
This commit is contained in:
@@ -11,7 +11,7 @@ use crate::http::workflow::{
|
||||
validate_story_dirs,
|
||||
};
|
||||
use crate::worktree;
|
||||
use crate::io::story_metadata::{parse_front_matter, parse_unchecked_todos};
|
||||
use crate::io::story_metadata::{parse_front_matter, parse_unchecked_todos, write_merge_failure};
|
||||
use crate::workflow::{evaluate_acceptance_with_coverage, TestCaseResult, TestStatus};
|
||||
use poem::handler;
|
||||
use poem::http::StatusCode;
|
||||
@@ -1668,6 +1668,28 @@ fn tool_report_merge_failure(args: &Value, ctx: &AppContext) -> Result<String, S
|
||||
slog!("[mergemaster] Merge failure reported for '{story_id}': {reason}");
|
||||
ctx.agents.set_merge_failure_reported(story_id);
|
||||
|
||||
// Persist the failure reason to the story file's front matter so it
|
||||
// survives server restarts and is visible in the web UI.
|
||||
if let Ok(project_root) = ctx.state.get_project_root() {
|
||||
let story_file = project_root
|
||||
.join(".story_kit")
|
||||
.join("work")
|
||||
.join("4_merge")
|
||||
.join(format!("{story_id}.md"));
|
||||
if story_file.exists() {
|
||||
if let Err(e) = write_merge_failure(&story_file, reason) {
|
||||
slog_warn!(
|
||||
"[mergemaster] Failed to persist merge_failure to story file for '{story_id}': {e}"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
slog_warn!(
|
||||
"[mergemaster] Story file not found in 4_merge/ for '{story_id}'; \
|
||||
merge_failure not persisted to front matter"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(format!(
|
||||
"Merge failure for '{story_id}' recorded. Story remains in work/4_merge/. Reason: {reason}"
|
||||
))
|
||||
|
||||
@@ -20,6 +20,8 @@ pub struct UpcomingStory {
|
||||
pub story_id: String,
|
||||
pub name: Option<String>,
|
||||
pub error: Option<String>,
|
||||
/// Merge failure reason persisted to front matter by the mergemaster agent.
|
||||
pub merge_failure: Option<String>,
|
||||
/// Active agent working on this item, if any.
|
||||
pub agent: Option<AgentAssignment>,
|
||||
}
|
||||
@@ -115,12 +117,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) = match parse_front_matter(&contents) {
|
||||
Ok(meta) => (meta.name, None),
|
||||
Err(e) => (None, Some(e.to_string())),
|
||||
let (name, error, merge_failure) = match parse_front_matter(&contents) {
|
||||
Ok(meta) => (meta.name, None, meta.merge_failure),
|
||||
Err(e) => (None, Some(e.to_string()), None),
|
||||
};
|
||||
let agent = agent_map.get(&story_id).cloned();
|
||||
stories.push(UpcomingStory { story_id, name, error, agent });
|
||||
stories.push(UpcomingStory { story_id, name, error, merge_failure, agent });
|
||||
}
|
||||
|
||||
stories.sort_by(|a, b| a.story_id.cmp(&b.story_id));
|
||||
|
||||
@@ -612,6 +612,7 @@ mod tests {
|
||||
story_id: "10_story_test".to_string(),
|
||||
name: Some("Test".to_string()),
|
||||
error: None,
|
||||
merge_failure: None,
|
||||
agent: None,
|
||||
};
|
||||
let resp = WsResponse::PipelineState {
|
||||
@@ -748,12 +749,14 @@ mod tests {
|
||||
story_id: "1_story_a".to_string(),
|
||||
name: Some("Story A".to_string()),
|
||||
error: None,
|
||||
merge_failure: None,
|
||||
agent: None,
|
||||
}],
|
||||
current: vec![UpcomingStory {
|
||||
story_id: "2_story_b".to_string(),
|
||||
name: Some("Story B".to_string()),
|
||||
error: None,
|
||||
merge_failure: None,
|
||||
agent: None,
|
||||
}],
|
||||
qa: vec![],
|
||||
@@ -762,6 +765,7 @@ mod tests {
|
||||
story_id: "50_story_done".to_string(),
|
||||
name: Some("Done Story".to_string()),
|
||||
error: None,
|
||||
merge_failure: None,
|
||||
agent: None,
|
||||
}],
|
||||
};
|
||||
@@ -913,6 +917,7 @@ mod tests {
|
||||
story_id: "10_story_x".to_string(),
|
||||
name: Some("Story X".to_string()),
|
||||
error: None,
|
||||
merge_failure: None,
|
||||
agent: Some(crate::http::workflow::AgentAssignment {
|
||||
agent_name: "coder-1".to_string(),
|
||||
model: Some("claude-3-5-sonnet".to_string()),
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::path::Path;
|
||||
pub struct StoryMetadata {
|
||||
pub name: Option<String>,
|
||||
pub coverage_baseline: Option<String>,
|
||||
pub merge_failure: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -27,6 +28,7 @@ impl std::fmt::Display for StoryMetaError {
|
||||
struct FrontMatter {
|
||||
name: Option<String>,
|
||||
coverage_baseline: Option<String>,
|
||||
merge_failure: Option<String>,
|
||||
}
|
||||
|
||||
pub fn parse_front_matter(contents: &str) -> Result<StoryMetadata, StoryMetaError> {
|
||||
@@ -58,6 +60,7 @@ fn build_metadata(front: FrontMatter) -> StoryMetadata {
|
||||
StoryMetadata {
|
||||
name: front.name,
|
||||
coverage_baseline: front.coverage_baseline,
|
||||
merge_failure: front.merge_failure,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +77,24 @@ pub fn write_coverage_baseline(path: &Path, coverage_pct: f64) -> Result<(), Str
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write or update a `merge_failure:` field in the YAML front matter of a story file.
|
||||
///
|
||||
/// The reason is stored as a quoted YAML string so that colons, hashes, and newlines
|
||||
/// in the failure message do not break front-matter parsing.
|
||||
/// If no front matter is present, this is a no-op (returns Ok).
|
||||
pub fn write_merge_failure(path: &Path, reason: &str) -> Result<(), String> {
|
||||
let contents =
|
||||
fs::read_to_string(path).map_err(|e| format!("Failed to read story file: {e}"))?;
|
||||
|
||||
// Produce a YAML-safe inline quoted string: collapse newlines, escape inner quotes.
|
||||
let escaped = reason.replace('"', "\\\"").replace('\n', " ").replace('\r', "");
|
||||
let yaml_value = format!("\"{escaped}\"");
|
||||
|
||||
let updated = set_front_matter_field(&contents, "merge_failure", &yaml_value);
|
||||
fs::write(path, &updated).map_err(|e| format!("Failed to write story file: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Insert or update a key: value pair in the YAML front matter of a markdown string.
|
||||
///
|
||||
/// If no front matter (opening `---`) is found, returns the content unchanged.
|
||||
|
||||
Reference in New Issue
Block a user