huskies: merge 986

This commit is contained in:
dave
2026-05-13 15:57:24 +00:00
parent 91fbad568a
commit 430079ecbc
13 changed files with 377 additions and 81 deletions
@@ -56,10 +56,10 @@ impl AgentPool {
let path = worktree_path.clone();
// Run gate checks in a blocking thread to avoid stalling the async runtime.
let (gates_passed, gate_output) = tokio::task::spawn_blocking(move || {
let outcome = tokio::task::spawn_blocking(move || {
// Step 1: Reject if worktree is dirty.
crate::agents::gates::check_uncommitted_changes(&path)?;
// Step 2: Run clippy + tests and return (passed, output).
// Step 2: Run acceptance gates and return a typed GateOutcome.
crate::agents::gates::run_acceptance_gates(&path)
})
.await
@@ -67,8 +67,8 @@ impl AgentPool {
let report = CompletionReport {
summary: summary.to_string(),
gates_passed,
gate_output,
gates_passed: outcome.passed,
gate_output: outcome.output,
needs_commit_recovery: false,
};
@@ -161,7 +161,7 @@ pub(in crate::agents::pool) async fn run_server_owned_completion(
false,
));
}
let (passed, output) = crate::agents::gates::run_acceptance_gates(&path)?;
let outcome = crate::agents::gates::run_acceptance_gates(&path)?;
// Restore stashed uncommitted changes.
if stashed {
let _ = std::process::Command::new("git")
@@ -169,7 +169,7 @@ pub(in crate::agents::pool) async fn run_server_owned_completion(
.current_dir(&path)
.output();
}
Ok((passed, output, false))
Ok((outcome.passed, outcome.output, false))
})
.await
{
+22 -39
View File
@@ -5,24 +5,6 @@ use crate::worktree;
use std::path::Path;
use std::sync::Arc;
/// Return `true` when `gate_output` matches a self-evident-fix class of failure
/// that a short fixup coder session can resolve without human intervention.
///
/// Patterns covered: fmt drift (`cargo fmt --check`), clippy warnings promoted
/// to errors (`-D warnings`), and missing doc comments detected by clippy or
/// the source-map-check gate.
fn is_self_evident_fix(gate_output: &str) -> bool {
let patterns: &[&str] = &[
"Diff in ", // cargo fmt --check output
"would reformat", // rustfmt --check output
"error[clippy::", // clippy error
"warning[clippy::", // clippy warning (treated as error via -D warnings)
"missing_doc_comments", // clippy missing-doc lint
"missing-docs direction", // source-map-check gate
];
patterns.iter().any(|p| gate_output.contains(p))
}
use super::super::super::AgentPool;
use super::time::{
decode_server_start_time, encode_server_start_time, server_start_time, unix_now,
@@ -130,31 +112,28 @@ impl AgentPool {
// On any failure: record merge_failure in CRDT and emit notification.
if !success {
let kind = match &report {
Ok(r) if r.no_commits => crate::pipeline_state::MergeFailureKind::NoCommits,
Ok(r) if r.had_conflicts => {
crate::pipeline_state::MergeFailureKind::ConflictDetected(
r.conflict_details.clone(),
)
}
Ok(r) => {
if r.had_conflicts {
crate::pipeline_state::MergeFailureKind::ConflictDetected(
r.conflict_details.clone(),
)
} else {
crate::pipeline_state::MergeFailureKind::GatesFailed(
r.gate_output.clone(),
)
}
crate::pipeline_state::MergeFailureKind::GatesFailed(r.gate_output.clone())
}
Err(e) => crate::pipeline_state::MergeFailureKind::Other(e.clone()),
};
let is_no_commits = matches!(
&kind,
crate::pipeline_state::MergeFailureKind::Other(r) if r.contains("no commits to merge")
);
// Self-evident fix: gate-only failure whose output matches a pattern
// a fixup coder can resolve in one short session (story 981).
let fixup_output = match &kind {
crate::pipeline_state::MergeFailureKind::GatesFailed(o) => o.as_str(),
_ => "",
};
let is_fixup =
!is_no_commits && !fixup_output.is_empty() && is_self_evident_fix(fixup_output);
let is_no_commits =
matches!(&kind, crate::pipeline_state::MergeFailureKind::NoCommits);
// Self-evident fix: gate-only failure whose typed kind a fixup coder
// can resolve in one short session (story 981, 986).
let is_fixup = !is_no_commits
&& report
.as_ref()
.ok()
.and_then(|r| r.gate_failure_kind.as_ref())
.map(|k| k.is_self_evident_fix())
.unwrap_or(false);
if is_no_commits {
let reason = kind.display_reason();
@@ -301,6 +280,8 @@ impl AgentPool {
conflict_details: merge_result.conflict_details,
gates_passed: merge_result.gates_passed,
gate_output: merge_result.output,
gate_failure_kind: merge_result.gate_failure_kind,
no_commits: merge_result.no_commits,
worktree_cleaned_up: false,
story_archived: false,
});
@@ -330,6 +311,8 @@ impl AgentPool {
conflict_details: merge_result.conflict_details,
gates_passed: true,
gate_output: merge_result.output,
gate_failure_kind: None,
no_commits: false,
worktree_cleaned_up,
story_archived,
})
@@ -29,6 +29,8 @@ impl AgentPool {
conflict_details: None,
gates_passed: false,
gate_output: String::new(),
gate_failure_kind: None,
no_commits: false,
worktree_cleaned_up: false,
story_archived: false,
});
+15 -11
View File
@@ -590,23 +590,27 @@ async fn merge_agent_work_zero_commits_ahead_stays_in_merge_stage() {
let pool = Arc::new(AgentPool::new_test(3001));
let job = run_merge_to_completion(&pool, repo, "675_zero_commits").await;
// The job must have failed with a "no commits to merge" error.
// The job must have completed with success=false and no_commits=true.
// Since story 986 the "no commits" path returns Ok(result) not Err, so the
// job status is Completed (not Failed).
match &job.status {
MergeJobStatus::Failed(e) => {
MergeJobStatus::Completed(report) => {
assert!(
e.contains("no commits to merge"),
"error must contain 'no commits to merge', got: {e}"
!report.success,
"merge must not have succeeded when feature branch is empty"
);
assert!(
e.contains("675_zero_commits"),
"error must name the story_id, got: {e}"
report.no_commits,
"report.no_commits must be true for a zero-ahead branch"
);
assert!(
report.gate_output.contains("no commits to merge"),
"gate_output must contain 'no commits to merge', got: {}",
report.gate_output
);
}
MergeJobStatus::Completed(report) => {
panic!(
"expected Failed status, got Completed with success={}: {}",
report.success, report.gate_output
);
MergeJobStatus::Failed(e) => {
panic!("expected Completed(success=false) status, got Failed: {e}");
}
MergeJobStatus::Running => panic!("should not still be running"),
}