huskies: merge 866

This commit is contained in:
dave
2026-04-29 22:42:59 +00:00
parent a49f668b5a
commit 9a3f60d5d3
19 changed files with 289 additions and 144 deletions
+10 -23
View File
@@ -119,34 +119,21 @@ impl AgentPool {
}
// AC6: Detect empty-diff stories before starting the merge pipeline.
// If the worktree has no commits on the feature branch, write a
// merge_failure and block the story immediately — no merge job needed.
// If the worktree has no commits on the feature branch, block the
// story immediately via the state machine — no merge job needed.
if let Some(wt_path) = worktree::find_worktree_path(project_root, story_id)
&& !crate::agents::gates::worktree_has_committed_work(&wt_path)
{
slog_warn!(
"[auto-assign] Story '{story_id}' in 4_merge/ has no commits \
on feature branch. Writing merge_failure and blocking."
);
let empty_diff_reason = "Feature branch has no code changes — the coder agent \
did not produce any commits.";
if let Some(contents) = crate::db::read_content(story_id) {
let updated = crate::io::story_metadata::write_merge_failure_in_content(
&contents,
empty_diff_reason,
);
let blocked = crate::io::story_metadata::write_blocked_in_content(&updated);
crate::db::write_content(story_id, &blocked);
crate::db::write_item_with_content(story_id, "4_merge", &blocked);
} else {
let story_path = project_root
.join(".huskies/work/4_merge")
.join(format!("{story_id}.md"));
let _ = crate::io::story_metadata::write_merge_failure(
&story_path,
empty_diff_reason,
);
let _ = crate::io::story_metadata::write_blocked(&story_path);
slog_warn!(
"[auto-assign] Story '{story_id}' in 4_merge/ has no commits \
on feature branch. Blocking via state machine."
);
if let Err(e) =
crate::agents::lifecycle::transition_to_blocked(story_id, empty_diff_reason)
{
slog_error!("[auto-assign] Failed to transition '{story_id}' to Blocked: {e}");
}
let _ = self
.watcher_tx
@@ -34,8 +34,16 @@ pub(super) fn has_review_hold(project_root: &Path, _stage_dir: &str, story_id: &
.unwrap_or(false)
}
/// Return `true` if the story file has `blocked: true` in its front matter.
/// Return `true` if the story is blocked — either via the typed `Stage::Blocked`
/// variant or the legacy `blocked: true` front-matter field.
pub(super) fn is_story_blocked(project_root: &Path, _stage_dir: &str, story_id: &str) -> bool {
// Check the typed stage first (authoritative after story 866).
if let Ok(Some(item)) = crate::pipeline_state::read_typed(story_id)
&& item.stage.is_blocked()
{
return true;
}
// Legacy fallback: check front-matter field for backward compatibility.
use crate::io::story_metadata::parse_front_matter;
let contents = match read_story_contents(project_root, story_id) {
Some(c) => c,
@@ -267,12 +267,13 @@ max_turns = 10
let found = pool.run_watchdog_pass(Some(root));
assert!(found >= 1, "watchdog should detect the over-limit agent");
// With max_retries=1, the first violation blocks immediately.
let updated = crate::db::read_content(story_id)
.expect("story content must still exist after watchdog termination");
assert!(
updated.contains("blocked: true"),
"story must be marked `blocked: true` after limit termination with max_retries=1 — got:\n{updated}"
// With max_retries=1, the first violation blocks immediately via the state machine.
let item = crate::crdt_state::read_item(story_id)
.expect("story must be in CRDT after watchdog termination");
assert_eq!(
item.stage, "2_blocked",
"story stage must be 2_blocked after limit termination with max_retries=1 — got: {}",
item.stage
);
// Sanity: the agent itself is also Failed with the right reason.
@@ -407,11 +408,12 @@ max_turns = 10
let event = rx.try_recv().expect("watchdog must emit an Error event");
assert!(matches!(event, AgentEvent::Error { .. }));
// With max_retries=1, the story is blocked.
let updated = crate::db::read_content(story_id).unwrap();
assert!(
updated.contains("blocked: true"),
"story must be blocked after per-session overrun with max_retries=1"
// With max_retries=1, the story is blocked via the state machine.
let item = crate::crdt_state::read_item(story_id)
.expect("story must be in CRDT after per-session overrun");
assert_eq!(
item.stage, "2_blocked",
"story stage must be 2_blocked after per-session overrun with max_retries=1"
);
}
@@ -470,9 +472,9 @@ max_turns = 10
Some(1),
"after session 1, retry_count should be 1 in CRDT"
);
let content = crate::db::read_content(story_id).unwrap();
assert!(
!content.contains("blocked: true"),
assert_ne!(
item.stage.as_str(),
"2_blocked",
"story should NOT be blocked after session 1"
);
}
@@ -490,9 +492,9 @@ max_turns = 10
Some(2),
"after session 2, retry_count should be 2 in CRDT"
);
let content = crate::db::read_content(story_id).unwrap();
assert!(
!content.contains("blocked: true"),
assert_ne!(
item.stage.as_str(),
"2_blocked",
"story should NOT be blocked after session 2"
);
}
@@ -504,16 +506,18 @@ max_turns = 10
pool.inject_test_agent_with_session(story_id, "coder-1", AgentStatus::Running, "session-3");
pool.run_watchdog_pass(Some(root));
let content = crate::db::read_content(story_id).unwrap();
assert!(
content.contains("blocked: true"),
"story must be blocked after session 3 (retry_count=3 >= max_retries=3) — got:\n{content}"
);
let item = crate::crdt_state::read_item(story_id).expect("story must be in CRDT");
assert_eq!(
item.stage, "2_blocked",
"story must be blocked after session 3 (retry_count=3 >= max_retries=3) — got: {}",
item.stage
);
// retry_count resets to 0 on stage transition (Bug 780) — the fact
// that the story reached 2_blocked proves the retry limit was hit.
assert_eq!(
item.retry_count,
Some(3),
"retry_count should be 3 in CRDT after session 3"
Some(0),
"retry_count should reset to 0 after stage transition to blocked"
);
}
}