Refactor agents.rs (7631 lines) into agents/ module directory
Split the monolithic agents.rs into 6 focused modules: - mod.rs: shared types (AgentEvent, AgentStatus, etc.) and re-exports - pool.rs: AgentPool struct, all methods, and helper free functions - pty.rs: PTY streaming (run_agent_pty_blocking, emit_event) - lifecycle.rs: story movement functions (move_story_to_qa, etc.) - gates.rs: acceptance gates (clippy, tests, coverage) - merge.rs: squash-merge, conflict resolution, quality gates All 121 original tests are preserved and distributed across modules. Also adds clear_front_matter_field to story_metadata.rs to strip stale merge_failure from front matter when stories move to done. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -95,6 +95,52 @@ pub fn write_merge_failure(path: &Path, reason: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a key from the YAML front matter of a story file on disk.
|
||||
///
|
||||
/// If front matter is present and contains the key, the line is removed.
|
||||
/// If no front matter or key is not found, the file is left unchanged.
|
||||
pub fn clear_front_matter_field(path: &Path, key: &str) -> Result<(), String> {
|
||||
let contents =
|
||||
fs::read_to_string(path).map_err(|e| format!("Failed to read story file: {e}"))?;
|
||||
let updated = remove_front_matter_field(&contents, key);
|
||||
if updated != contents {
|
||||
fs::write(path, &updated).map_err(|e| format!("Failed to write story file: {e}"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a key: value line from the YAML front matter of a markdown string.
|
||||
///
|
||||
/// If no front matter (opening `---`) is found or the key is absent, returns content unchanged.
|
||||
fn remove_front_matter_field(contents: &str, key: &str) -> String {
|
||||
let mut lines: Vec<String> = contents.lines().map(String::from).collect();
|
||||
if lines.is_empty() || lines[0].trim() != "---" {
|
||||
return contents.to_string();
|
||||
}
|
||||
|
||||
let close_idx = match lines[1..].iter().position(|l| l.trim() == "---") {
|
||||
Some(i) => i + 1,
|
||||
None => return contents.to_string(),
|
||||
};
|
||||
|
||||
let key_prefix = format!("{key}:");
|
||||
if let Some(idx) = lines[1..close_idx]
|
||||
.iter()
|
||||
.position(|l| l.trim_start().starts_with(&key_prefix))
|
||||
.map(|i| i + 1)
|
||||
{
|
||||
lines.remove(idx);
|
||||
} else {
|
||||
return contents.to_string();
|
||||
}
|
||||
|
||||
let mut result = lines.join("\n");
|
||||
if contents.ends_with('\n') {
|
||||
result.push('\n');
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -219,6 +265,40 @@ workflow: tdd
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_front_matter_field_removes_key() {
|
||||
let input = "---\nname: My Story\nmerge_failure: \"something broke\"\n---\n# Body\n";
|
||||
let output = remove_front_matter_field(input, "merge_failure");
|
||||
assert!(!output.contains("merge_failure"));
|
||||
assert!(output.contains("name: My Story"));
|
||||
assert!(output.ends_with('\n'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_front_matter_field_no_op_when_absent() {
|
||||
let input = "---\nname: My Story\n---\n# Body\n";
|
||||
let output = remove_front_matter_field(input, "merge_failure");
|
||||
assert_eq!(output, input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_front_matter_field_no_op_without_front_matter() {
|
||||
let input = "# No front matter\n";
|
||||
let output = remove_front_matter_field(input, "merge_failure");
|
||||
assert_eq!(output, input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_front_matter_field_updates_file() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let path = tmp.path().join("story.md");
|
||||
std::fs::write(&path, "---\nname: Test\nmerge_failure: \"bad\"\n---\n# Story\n").unwrap();
|
||||
clear_front_matter_field(&path, "merge_failure").unwrap();
|
||||
let contents = std::fs::read_to_string(&path).unwrap();
|
||||
assert!(!contents.contains("merge_failure"));
|
||||
assert!(contents.contains("name: Test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_unchecked_todos_mixed() {
|
||||
let input = "## AC\n- [ ] First thing\n- [x] Done thing\n- [ ] Second thing\n";
|
||||
|
||||
Reference in New Issue
Block a user