huskies: merge 997
This commit is contained in:
@@ -152,6 +152,7 @@ mod tests {
|
||||
after: Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
},
|
||||
event: PipelineEvent::DepsMet,
|
||||
at: Utc::now(),
|
||||
@@ -172,6 +173,7 @@ mod tests {
|
||||
feature_branch: BranchName("feature/story-1".into()),
|
||||
commits_ahead: NonZeroU32::new(3).unwrap(),
|
||||
claim: None,
|
||||
retries: 0,
|
||||
};
|
||||
// Stage::Merge has exactly two fields: feature_branch and commits_ahead.
|
||||
// There is no way to attach an agent name to it. The type system
|
||||
|
||||
@@ -52,14 +52,11 @@ impl TryFrom<&PipelineItemView> for PipelineItem {
|
||||
.map(|d| StoryId(d.to_string()))
|
||||
.collect();
|
||||
|
||||
let retry_count = view.retry_count();
|
||||
|
||||
Ok(PipelineItem {
|
||||
story_id,
|
||||
name,
|
||||
stage: view.stage().clone(),
|
||||
depends_on,
|
||||
retry_count,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -117,7 +114,6 @@ mod tests {
|
||||
stage,
|
||||
name.unwrap_or("(unnamed)"),
|
||||
None,
|
||||
0u32,
|
||||
vec![],
|
||||
None,
|
||||
None,
|
||||
@@ -139,7 +135,6 @@ mod tests {
|
||||
Stage::Backlog,
|
||||
"Test Story",
|
||||
None,
|
||||
0u32,
|
||||
vec![10, 20],
|
||||
None,
|
||||
None,
|
||||
@@ -150,7 +145,6 @@ mod tests {
|
||||
assert_eq!(item.name, "Test Story");
|
||||
assert!(matches!(item.stage, Stage::Backlog));
|
||||
assert_eq!(item.depends_on.len(), 2);
|
||||
assert_eq!(item.retry_count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -160,10 +154,10 @@ mod tests {
|
||||
Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 2,
|
||||
},
|
||||
"Test",
|
||||
Some(crate::config::AgentName::Coder1),
|
||||
2u32,
|
||||
vec![],
|
||||
None,
|
||||
None,
|
||||
@@ -171,7 +165,7 @@ mod tests {
|
||||
);
|
||||
let item = PipelineItem::try_from(&view).unwrap();
|
||||
assert!(matches!(item.stage, Stage::Coding { .. }));
|
||||
assert_eq!(item.retry_count, 2);
|
||||
assert_eq!(item.retry_count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -182,6 +176,7 @@ mod tests {
|
||||
feature_branch: fb("feature/story-42_story_test"),
|
||||
commits_ahead: nz(1),
|
||||
claim: None,
|
||||
retries: 0,
|
||||
},
|
||||
Some("Test"),
|
||||
);
|
||||
@@ -223,7 +218,6 @@ mod tests {
|
||||
},
|
||||
"Test",
|
||||
None,
|
||||
0u32,
|
||||
vec![],
|
||||
None,
|
||||
None,
|
||||
@@ -249,7 +243,6 @@ mod tests {
|
||||
},
|
||||
"Test",
|
||||
None,
|
||||
0u32,
|
||||
vec![],
|
||||
None,
|
||||
None,
|
||||
@@ -273,6 +266,7 @@ mod tests {
|
||||
resume_to: Box::new(Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
}),
|
||||
},
|
||||
Some("Frozen Story"),
|
||||
@@ -308,6 +302,7 @@ mod tests {
|
||||
Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
},
|
||||
Some("Test"),
|
||||
);
|
||||
@@ -328,6 +323,7 @@ mod tests {
|
||||
Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Drafted,
|
||||
retries: 0,
|
||||
},
|
||||
Some("Test"),
|
||||
);
|
||||
@@ -348,6 +344,7 @@ mod tests {
|
||||
Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Confirmed,
|
||||
retries: 0,
|
||||
},
|
||||
Some("Test"),
|
||||
);
|
||||
|
||||
@@ -55,6 +55,7 @@ fn happy_path_with_qa() {
|
||||
let s = Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
};
|
||||
let s = transition(s, PipelineEvent::GatesStarted).unwrap();
|
||||
assert!(matches!(s, Stage::Qa));
|
||||
@@ -75,6 +76,7 @@ fn qa_retry_loop() {
|
||||
let s = Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
};
|
||||
let s = transition(s, PipelineEvent::GatesStarted).unwrap();
|
||||
assert!(matches!(s, Stage::Qa));
|
||||
@@ -164,6 +166,7 @@ fn cannot_accept_from_coding() {
|
||||
Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
},
|
||||
PipelineEvent::Accepted,
|
||||
);
|
||||
@@ -182,6 +185,7 @@ fn block_from_any_active_stage() {
|
||||
Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
},
|
||||
Stage::Qa,
|
||||
] {
|
||||
@@ -198,6 +202,7 @@ fn block_from_any_active_stage() {
|
||||
feature_branch: fb("f"),
|
||||
commits_ahead: nz(1),
|
||||
claim: None,
|
||||
retries: 0,
|
||||
};
|
||||
let result = transition(
|
||||
m,
|
||||
@@ -274,6 +279,7 @@ fn abandon_from_any_active_or_done() {
|
||||
Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
},
|
||||
Stage::Qa,
|
||||
Stage::Done {
|
||||
@@ -293,6 +299,7 @@ fn supersede_from_any_active_or_done() {
|
||||
Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
},
|
||||
Stage::Qa,
|
||||
Stage::Done {
|
||||
@@ -322,6 +329,7 @@ fn review_hold_from_active_stages() {
|
||||
Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
},
|
||||
Stage::Qa,
|
||||
] {
|
||||
@@ -350,6 +358,7 @@ fn merge_failed_final() {
|
||||
feature_branch: fb("f"),
|
||||
commits_ahead: nz(1),
|
||||
claim: None,
|
||||
retries: 0,
|
||||
};
|
||||
let result = transition(
|
||||
s,
|
||||
@@ -373,6 +382,7 @@ fn merge_failed_only_from_merge() {
|
||||
Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
},
|
||||
PipelineEvent::MergeFailedFinal {
|
||||
reason: "conflicts".into(),
|
||||
@@ -451,6 +461,7 @@ fn bug_502_agent_not_in_stage() {
|
||||
feature_branch: BranchName("feature/story-1".into()),
|
||||
commits_ahead: NonZeroU32::new(3).unwrap(),
|
||||
claim: None,
|
||||
retries: 0,
|
||||
};
|
||||
// Stage::Merge has exactly two fields: feature_branch and commits_ahead.
|
||||
// There is no way to attach an agent name to it. The type system
|
||||
@@ -523,6 +534,7 @@ fn reject_from_active_stages() {
|
||||
Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
},
|
||||
Stage::Qa,
|
||||
] {
|
||||
@@ -539,6 +551,7 @@ fn reject_from_active_stages() {
|
||||
feature_branch: fb("f"),
|
||||
commits_ahead: nz(1),
|
||||
claim: None,
|
||||
retries: 0,
|
||||
};
|
||||
let result = transition(
|
||||
m,
|
||||
@@ -931,6 +944,7 @@ fn merge_aborted_returns_to_coding() {
|
||||
feature_branch: fb("feature/story-73"),
|
||||
commits_ahead: nz(2),
|
||||
claim: None,
|
||||
retries: 0,
|
||||
};
|
||||
let result = transition(s, PipelineEvent::MergeAborted).unwrap();
|
||||
assert!(
|
||||
@@ -1034,12 +1048,14 @@ fn hotfix_requested_rejected_from_non_done_stages() {
|
||||
Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
},
|
||||
Stage::Qa,
|
||||
Stage::Merge {
|
||||
feature_branch: fb("feature/story-1"),
|
||||
commits_ahead: nz(1),
|
||||
claim: None,
|
||||
retries: 0,
|
||||
},
|
||||
] {
|
||||
let result = transition(stage.clone(), PipelineEvent::HotfixRequested);
|
||||
@@ -1064,6 +1080,7 @@ fn audit_entry_backlog_to_coding_exact_format() {
|
||||
after: Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
},
|
||||
event: PipelineEvent::DepsMet,
|
||||
at,
|
||||
@@ -1083,6 +1100,7 @@ fn audit_entry_is_single_line_with_all_fields() {
|
||||
feature_branch: fb("feature/story-42"),
|
||||
commits_ahead: nz(3),
|
||||
claim: None,
|
||||
retries: 0,
|
||||
},
|
||||
event: PipelineEvent::GatesPassed {
|
||||
feature_branch: fb("feature/story-42"),
|
||||
@@ -1120,6 +1138,7 @@ fn audit_entry_merge_to_done() {
|
||||
feature_branch: fb("f"),
|
||||
commits_ahead: nz(1),
|
||||
claim: None,
|
||||
retries: 0,
|
||||
},
|
||||
after: Stage::Done {
|
||||
merged_at: chrono::Utc::now(),
|
||||
@@ -1167,6 +1186,7 @@ fn audit_entry_coding_to_blocked() {
|
||||
before: Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
},
|
||||
after: Stage::Blocked {
|
||||
reason: "waiting".into(),
|
||||
@@ -1192,6 +1212,7 @@ fn audit_entry_blocked_to_coding() {
|
||||
after: Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
},
|
||||
event: PipelineEvent::Unblock,
|
||||
at: chrono::Utc::now(),
|
||||
@@ -1210,6 +1231,7 @@ fn audit_entry_merge_to_merge_failure() {
|
||||
feature_branch: fb("f"),
|
||||
commits_ahead: nz(1),
|
||||
claim: None,
|
||||
retries: 0,
|
||||
},
|
||||
after: Stage::MergeFailure {
|
||||
kind: MergeFailureKind::Other("conflicts".into()),
|
||||
@@ -1234,11 +1256,13 @@ fn audit_entry_coding_to_frozen() {
|
||||
before: Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
},
|
||||
after: Stage::Frozen {
|
||||
resume_to: Box::new(Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
}),
|
||||
},
|
||||
event: PipelineEvent::Freeze,
|
||||
@@ -1257,6 +1281,7 @@ fn audit_entry_coding_to_abandoned() {
|
||||
before: Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
},
|
||||
after: Stage::Abandoned {
|
||||
ts: chrono::Utc::now(),
|
||||
|
||||
@@ -152,6 +152,7 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
(Backlog, DepsMet) => Ok(Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
}),
|
||||
(Coding { .. }, GatesStarted) => Ok(Qa),
|
||||
(
|
||||
@@ -164,6 +165,7 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
feature_branch,
|
||||
commits_ahead,
|
||||
claim: None,
|
||||
retries: 0,
|
||||
}),
|
||||
(
|
||||
Qa,
|
||||
@@ -175,10 +177,12 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
feature_branch,
|
||||
commits_ahead,
|
||||
claim: None,
|
||||
retries: 0,
|
||||
}),
|
||||
(Qa, GatesFailed { .. }) => Ok(Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
}),
|
||||
(Merge { .. }, MergeSucceeded { merge_commit }) => Ok(Done {
|
||||
merged_at: now,
|
||||
@@ -323,6 +327,7 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
(MergeFailure { .. }, FixupRequested) => Ok(Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
}),
|
||||
|
||||
// ── FixupRequested: MergeFailureFinal → Coding (operator override)
|
||||
@@ -335,6 +340,7 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
(MergeFailureFinal { .. }, FixupRequested) => Ok(Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
}),
|
||||
|
||||
// ── ReQueuedForQa: MergeFailure → Qa (re-review) ────────────────
|
||||
@@ -344,6 +350,7 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
(Merge { .. }, MergeAborted) => Ok(Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
}),
|
||||
|
||||
// ── HotfixRequested: Done → Coding (post-merge hotfix) ───────────
|
||||
@@ -353,6 +360,7 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
(Done { .. }, HotfixRequested) => Ok(Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
}),
|
||||
|
||||
// ── MergemasterAttempted: MergeFailure → MergeFailureFinal ─────
|
||||
@@ -367,6 +375,7 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
(Blocked { .. }, Unblock) => Ok(Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
}),
|
||||
|
||||
// ── Unblock MergeFailure → Merge (re-attempt) ────────────────────
|
||||
@@ -384,6 +393,7 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
feature_branch,
|
||||
commits_ahead,
|
||||
claim: None,
|
||||
retries: 0,
|
||||
}),
|
||||
|
||||
// ── Demote MergeFailure → Backlog (manual parking) ───────────────
|
||||
|
||||
@@ -212,9 +212,14 @@ pub enum Stage {
|
||||
///
|
||||
/// `plan` tracks the lifecycle of the `PLAN.md` file in the worktree,
|
||||
/// updated by the filesystem watcher on create/modify/remove events.
|
||||
///
|
||||
/// `retries` counts how many times the coder agent has been restarted for
|
||||
/// this item. Replaces the separate `retry_count` CRDT register (story 997).
|
||||
Coding {
|
||||
claim: Option<AgentClaim>,
|
||||
plan: PlanState,
|
||||
/// Number of coder restarts for this item. Zero on the first attempt.
|
||||
retries: u32,
|
||||
},
|
||||
|
||||
/// Coder has run; gates are running.
|
||||
@@ -225,11 +230,16 @@ pub enum Stage {
|
||||
/// `commits_ahead: NonZeroU32` makes "Merge with nothing to merge"
|
||||
/// structurally impossible (eliminates bug 519). The optional
|
||||
/// [`AgentClaim`] carries the mergemaster agent that owns this merge.
|
||||
///
|
||||
/// `retries` counts how many times the mergemaster agent has been restarted
|
||||
/// for this item. Replaces the separate `retry_count` CRDT register (story 997).
|
||||
Merge {
|
||||
feature_branch: BranchName,
|
||||
commits_ahead: NonZeroU32,
|
||||
/// Agent currently running the merge, or `None` when unclaimed.
|
||||
claim: Option<AgentClaim>,
|
||||
/// Number of mergemaster restarts for this item. Zero on the first attempt.
|
||||
retries: u32,
|
||||
},
|
||||
|
||||
/// Mergemaster squashed to master. Always carries merge metadata.
|
||||
@@ -350,6 +360,7 @@ impl Stage {
|
||||
"coding" => Some(Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
}),
|
||||
"blocked" => Some(Stage::Blocked {
|
||||
reason: String::new(),
|
||||
@@ -359,6 +370,7 @@ impl Stage {
|
||||
feature_branch: BranchName(String::new()),
|
||||
commits_ahead: NonZeroU32::new(1).expect("1 is non-zero"),
|
||||
claim: None,
|
||||
retries: 0,
|
||||
}),
|
||||
"merge_failure" => Some(Stage::MergeFailure {
|
||||
kind: MergeFailureKind::Other(String::new()),
|
||||
@@ -372,12 +384,14 @@ impl Stage {
|
||||
resume_to: Box::new(Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
}),
|
||||
}),
|
||||
"review_hold" => Some(Stage::ReviewHold {
|
||||
resume_to: Box::new(Stage::Coding {
|
||||
claim: None,
|
||||
plan: PlanState::Missing,
|
||||
retries: 0,
|
||||
}),
|
||||
reason: String::new(),
|
||||
}),
|
||||
@@ -438,13 +452,26 @@ pub enum ExecutionState {
|
||||
// ── Pipeline item (the aggregate) ───────────────────────────────────────────
|
||||
|
||||
/// A fully typed pipeline item. Every field is validated by construction.
|
||||
///
|
||||
/// The retry count is no longer a top-level field — callers read it from the
|
||||
/// Stage variant (`Stage::Coding { retries }` / `Stage::Merge { retries }`).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PipelineItem {
|
||||
pub story_id: StoryId,
|
||||
pub name: String,
|
||||
pub stage: Stage,
|
||||
pub depends_on: Vec<StoryId>,
|
||||
pub retry_count: u32,
|
||||
}
|
||||
|
||||
impl PipelineItem {
|
||||
/// Returns the retry count embedded in the stage payload.
|
||||
pub fn retry_count(&self) -> u32 {
|
||||
match &self.stage {
|
||||
Stage::Coding { retries, .. } => *retries,
|
||||
Stage::Merge { retries, .. } => *retries,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Transition errors ───────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user