huskies: merge 757
This commit is contained in:
@@ -83,10 +83,9 @@ impl AgentPool {
|
||||
};
|
||||
|
||||
// Process each active pipeline stage in order.
|
||||
let stages: [(&str, PipelineStage); 3] = [
|
||||
let stages: [(&str, PipelineStage); 2] = [
|
||||
("2_current", PipelineStage::Coder),
|
||||
("3_qa", PipelineStage::Qa),
|
||||
("4_merge", PipelineStage::Mergemaster),
|
||||
];
|
||||
|
||||
for (stage_dir, stage) in &stages {
|
||||
@@ -121,58 +120,6 @@ impl AgentPool {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip stories in 4_merge/ that already have a reported merge failure.
|
||||
// These need human intervention — auto-assigning a new mergemaster
|
||||
// would just waste tokens on the same broken merge.
|
||||
if *stage == PipelineStage::Mergemaster
|
||||
&& has_merge_failure(project_root, stage_dir, story_id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// AC6: Detect empty-diff stories in 4_merge/ before starting a
|
||||
// mergemaster. If the worktree has no commits on the feature branch,
|
||||
// write a merge_failure and block the story immediately.
|
||||
if *stage == PipelineStage::Mergemaster
|
||||
&& 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.";
|
||||
// Write merge_failure and blocked to content store.
|
||||
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, stage_dir, &blocked);
|
||||
} else {
|
||||
// Fallback: filesystem.
|
||||
let story_path = project_root
|
||||
.join(".huskies/work")
|
||||
.join(stage_dir)
|
||||
.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);
|
||||
}
|
||||
let _ = self
|
||||
.watcher_tx
|
||||
.send(crate::io::watcher::WatcherEvent::StoryBlocked {
|
||||
story_id: story_id.to_string(),
|
||||
reason: empty_diff_reason.to_string(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Re-acquire the lock on each iteration to see state changes
|
||||
// from previous start_agent calls in the same pass.
|
||||
let preferred_agent =
|
||||
@@ -287,6 +234,114 @@ impl AgentPool {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 4_merge: deterministic server-side merge (no LLM agent) ──────────
|
||||
//
|
||||
// Stories in 4_merge/ are handled directly by the server rather than by
|
||||
// a mergemaster agent. For each eligible story, trigger start_merge_agent_work
|
||||
// which runs the squash-merge pipeline in a background task. On success
|
||||
// the story advances to 5_done automatically. On failure merge_failure is
|
||||
// written to the CRDT and a notification is emitted; the story stays in
|
||||
// 4_merge/ until a human intervenes or an explicit `start_agent mergemaster`
|
||||
// call invokes the LLM-driven recovery path.
|
||||
let merge_items = scan_stage_items(project_root, "4_merge");
|
||||
for story_id in &merge_items {
|
||||
// Skip stories with an already-recorded merge failure — they need
|
||||
// human intervention (operator can call start_agent mergemaster).
|
||||
if has_merge_failure(project_root, "4_merge", story_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if has_review_hold(project_root, "4_merge", story_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_story_frozen(project_root, "4_merge", story_id) {
|
||||
slog!("[auto-assign] Story '{story_id}' in 4_merge/ is frozen; skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_story_blocked(project_root, "4_merge", story_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if has_unmet_dependencies(project_root, "4_merge", story_id) {
|
||||
slog!("[auto-assign] Story '{story_id}' in 4_merge/ has unmet deps; skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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 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);
|
||||
}
|
||||
let _ = self
|
||||
.watcher_tx
|
||||
.send(crate::io::watcher::WatcherEvent::StoryBlocked {
|
||||
story_id: story_id.to_string(),
|
||||
reason: empty_diff_reason.to_string(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if a merge job is already running for this story (e.g. triggered
|
||||
// by a previous auto-assign pass or by pipeline advancement).
|
||||
let already_running = self
|
||||
.merge_jobs
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|jobs| jobs.get(story_id.as_str()).cloned())
|
||||
.is_some_and(|job| {
|
||||
matches!(job.status, crate::agents::merge::MergeJobStatus::Running)
|
||||
});
|
||||
if already_running {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if an explicit mergemaster LLM agent is already running
|
||||
// (operator-driven failure recovery path).
|
||||
let has_mergemaster = {
|
||||
let agents = match self.agents.lock() {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
slog_error!("[auto-assign] Failed to lock agents: {e}");
|
||||
break;
|
||||
}
|
||||
};
|
||||
is_story_assigned_for_stage(&config, &agents, story_id, &PipelineStage::Mergemaster)
|
||||
};
|
||||
if has_mergemaster {
|
||||
continue;
|
||||
}
|
||||
|
||||
slog!("[auto-assign] Triggering server-side merge for '{story_id}' in 4_merge/");
|
||||
self.trigger_server_side_merge(project_root, story_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user