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:
Dave
2026-03-16 23:06:14 +00:00
parent 225137fbdc
commit 7c3a756a5c
7 changed files with 4331 additions and 4149 deletions

View File

@@ -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";