huskies: merge 757

This commit is contained in:
dave
2026-04-27 23:31:57 +00:00
parent dffa05d703
commit 7ee542dd1e
7 changed files with 571 additions and 177 deletions
+3 -41
View File
@@ -77,8 +77,7 @@ impl AgentPool {
"[pipeline] Failed to move '{story_id}' to 4_merge/: {e}"
);
} else {
self.start_mergemaster_or_block(&project_root, story_id)
.await;
self.trigger_server_side_merge(&project_root, story_id);
}
}
crate::io::story_metadata::QaMode::Agent => {
@@ -151,8 +150,7 @@ impl AgentPool {
"[pipeline] Failed to move '{story_id}' to 4_merge/: {e}"
);
} else {
self.start_mergemaster_or_block(&project_root, story_id)
.await;
self.trigger_server_side_merge(&project_root, story_id);
}
}
crate::io::story_metadata::QaMode::Agent => {
@@ -272,8 +270,7 @@ impl AgentPool {
"[pipeline] Failed to move '{story_id}' to 4_merge/: {e}"
);
} else {
self.start_mergemaster_or_block(&project_root, story_id)
.await;
self.trigger_server_side_merge(&project_root, story_id);
}
}
} else if let Some(reason) =
@@ -440,41 +437,6 @@ impl AgentPool {
// become available (bug 295).
self.auto_assign_available_work(&project_root).await;
}
/// Start the mergemaster agent for `story_id`, but only if the feature
/// branch has commits that are not yet on master.
///
/// If the branch has zero commits ahead of master, this logs an error and
/// sends a [`WatcherEvent::StoryBlocked`] instead of spawning a Claude
/// session. A no-op merge session was observed spending $0.82 in the
/// 2026-04-09 incident (story 519).
async fn start_mergemaster_or_block(&self, project_root: &Path, story_id: &str) {
let branch = format!("feature/story-{story_id}");
if !crate::agents::lifecycle::feature_branch_has_unmerged_changes(project_root, story_id) {
slog_error!(
"[mergemaster] Branch '{branch}' has no commits ahead of master — \
refusing to spawn merge session. \
Likely cause: the worktree was reset to master after the feature \
branch's commits were created. Investigate the worktree's git state \
before retrying. Story '{story_id}' stays in 4_merge/ for human review."
);
let _ = self.watcher_tx.send(WatcherEvent::StoryBlocked {
story_id: story_id.to_string(),
reason: format!(
"Feature branch '{branch}' has no commits ahead of master — nothing to merge. \
The worktree may have been reset to master. \
Check the worktree's git state and retry manually."
),
});
return;
}
if let Err(e) = self
.start_agent(project_root, story_id, Some("mergemaster"), None, None)
.await
{
slog_error!("[pipeline] Failed to start mergemaster for '{story_id}': {e}");
}
}
}
/// Spawn pipeline advancement as a background task.
@@ -88,16 +88,23 @@ async fn mergemaster_blocks_and_sends_story_blocked_when_no_commits_ahead() {
"story should remain in content store — not removed"
);
// A StoryBlocked event must have been emitted (triggers chat failure notice,
// not the success 🎉 emoji).
// A StoryBlocked event must be emitted by the background merge task.
// The deterministic merge pipeline runs asynchronously, so poll with a
// timeout instead of a non-blocking try_recv().
let mut got_blocked = false;
while let Ok(evt) = rx.try_recv() {
if let WatcherEvent::StoryBlocked { story_id, .. } = &evt
&& story_id == "9919_story_no_commits"
{
got_blocked = true;
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
while tokio::time::Instant::now() < deadline {
while let Ok(evt) = rx.try_recv() {
if let WatcherEvent::StoryBlocked { story_id, .. } = &evt
&& story_id == "9919_story_no_commits"
{
got_blocked = true;
}
}
if got_blocked {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
assert!(
got_blocked,