huskies: merge 1009
This commit is contained in:
@@ -123,7 +123,7 @@ mod tests {
|
||||
bus.fire(TransitionFired {
|
||||
story_id: StoryId("test".into()),
|
||||
before: Stage::Backlog,
|
||||
after: Stage::Coding,
|
||||
after: Stage::Coding { claim: None },
|
||||
event: PipelineEvent::DepsMet,
|
||||
at: Utc::now(),
|
||||
});
|
||||
@@ -142,6 +142,7 @@ mod tests {
|
||||
let merge = Stage::Merge {
|
||||
feature_branch: BranchName("feature/story-1".into()),
|
||||
commits_ahead: NonZeroU32::new(3).unwrap(),
|
||||
claim: None,
|
||||
};
|
||||
// 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
|
||||
|
||||
@@ -40,8 +40,8 @@ mod tests;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use types::{
|
||||
AgentName, ArchiveReason, BranchName, ExecutionState, GitSha, MergeFailureKind, NodePubkey,
|
||||
PipelineItem, Stage, StoryId, TransitionError, stage_dir_name, stage_label,
|
||||
AgentClaim, AgentName, ArchiveReason, BranchName, ExecutionState, GitSha, MergeFailureKind,
|
||||
NodePubkey, PipelineItem, Stage, StoryId, TransitionError, stage_dir_name, stage_label,
|
||||
};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
|
||||
@@ -122,7 +122,6 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -145,7 +144,6 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let item = PipelineItem::try_from(&view).unwrap();
|
||||
assert_eq!(item.story_id, StoryId("42_story_test".to_string()));
|
||||
@@ -159,7 +157,7 @@ mod tests {
|
||||
fn project_current_item() {
|
||||
let view = PipelineItemView::for_test(
|
||||
"42_story_test",
|
||||
Stage::Coding,
|
||||
Stage::Coding { claim: None },
|
||||
"Test",
|
||||
Some(crate::config::AgentName::Coder1),
|
||||
2u32,
|
||||
@@ -167,10 +165,9 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let item = PipelineItem::try_from(&view).unwrap();
|
||||
assert!(matches!(item.stage, Stage::Coding));
|
||||
assert!(matches!(item.stage, Stage::Coding { .. }));
|
||||
assert_eq!(item.retry_count, 2);
|
||||
}
|
||||
|
||||
@@ -181,6 +178,7 @@ mod tests {
|
||||
Stage::Merge {
|
||||
feature_branch: fb("feature/story-42_story_test"),
|
||||
commits_ahead: nz(1),
|
||||
claim: None,
|
||||
},
|
||||
Some("Test"),
|
||||
);
|
||||
@@ -189,6 +187,7 @@ mod tests {
|
||||
if let Stage::Merge {
|
||||
feature_branch,
|
||||
commits_ahead,
|
||||
..
|
||||
} = &item.stage
|
||||
{
|
||||
assert_eq!(feature_branch.0, "feature/story-42_story_test");
|
||||
@@ -226,7 +225,6 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let item = PipelineItem::try_from(&view).unwrap();
|
||||
assert!(matches!(
|
||||
@@ -253,7 +251,6 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let item = PipelineItem::try_from(&view).unwrap();
|
||||
assert!(matches!(
|
||||
@@ -270,7 +267,7 @@ mod tests {
|
||||
let view = make_view(
|
||||
"42_story_test",
|
||||
Stage::Frozen {
|
||||
resume_to: Box::new(Stage::Coding),
|
||||
resume_to: Box::new(Stage::Coding { claim: None }),
|
||||
},
|
||||
Some("Frozen Story"),
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ fn sid(s: &str) -> StoryId {
|
||||
fn happy_path_backlog_through_archived() {
|
||||
let s = Stage::Backlog;
|
||||
let s = transition(s, PipelineEvent::DepsMet).unwrap();
|
||||
assert!(matches!(s, Stage::Coding));
|
||||
assert!(matches!(s, Stage::Coding { .. }));
|
||||
|
||||
let s = transition(
|
||||
s,
|
||||
@@ -52,7 +52,7 @@ fn happy_path_backlog_through_archived() {
|
||||
|
||||
#[test]
|
||||
fn happy_path_with_qa() {
|
||||
let s = Stage::Coding;
|
||||
let s = Stage::Coding { claim: None };
|
||||
let s = transition(s, PipelineEvent::GatesStarted).unwrap();
|
||||
assert!(matches!(s, Stage::Qa));
|
||||
|
||||
@@ -69,7 +69,7 @@ fn happy_path_with_qa() {
|
||||
|
||||
#[test]
|
||||
fn qa_retry_loop() {
|
||||
let s = Stage::Coding;
|
||||
let s = Stage::Coding { claim: None };
|
||||
let s = transition(s, PipelineEvent::GatesStarted).unwrap();
|
||||
assert!(matches!(s, Stage::Qa));
|
||||
|
||||
@@ -80,7 +80,7 @@ fn qa_retry_loop() {
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(s, Stage::Coding));
|
||||
assert!(matches!(s, Stage::Coding { .. }));
|
||||
}
|
||||
|
||||
// ── Bug 519: Merge with zero commits is unrepresentable ─────────────
|
||||
@@ -154,7 +154,7 @@ fn cannot_start_gates_from_backlog() {
|
||||
|
||||
#[test]
|
||||
fn cannot_accept_from_coding() {
|
||||
let result = transition(Stage::Coding, PipelineEvent::Accepted);
|
||||
let result = transition(Stage::Coding { claim: None }, PipelineEvent::Accepted);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(TransitionError::InvalidTransition { .. })
|
||||
@@ -165,7 +165,7 @@ fn cannot_accept_from_coding() {
|
||||
|
||||
#[test]
|
||||
fn block_from_any_active_stage() {
|
||||
for s in [Stage::Backlog, Stage::Coding, Stage::Qa] {
|
||||
for s in [Stage::Backlog, Stage::Coding { claim: None }, Stage::Qa] {
|
||||
let result = transition(
|
||||
s.clone(),
|
||||
PipelineEvent::Block {
|
||||
@@ -178,6 +178,7 @@ fn block_from_any_active_stage() {
|
||||
let m = Stage::Merge {
|
||||
feature_branch: fb("f"),
|
||||
commits_ahead: nz(1),
|
||||
claim: None,
|
||||
};
|
||||
let result = transition(
|
||||
m,
|
||||
@@ -194,7 +195,7 @@ fn unblock_returns_to_coding() {
|
||||
reason: "test".into(),
|
||||
};
|
||||
let result = transition(s, PipelineEvent::Unblock).unwrap();
|
||||
assert!(matches!(result, Stage::Coding));
|
||||
assert!(matches!(result, Stage::Coding { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -251,7 +252,7 @@ fn legacy_unblock_archived_blocked_returns_to_backlog() {
|
||||
fn abandon_from_any_active_or_done() {
|
||||
for s in [
|
||||
Stage::Backlog,
|
||||
Stage::Coding,
|
||||
Stage::Coding { claim: None },
|
||||
Stage::Qa,
|
||||
Stage::Done {
|
||||
merged_at: chrono::Utc::now(),
|
||||
@@ -267,7 +268,7 @@ fn abandon_from_any_active_or_done() {
|
||||
fn supersede_from_any_active_or_done() {
|
||||
for s in [
|
||||
Stage::Backlog,
|
||||
Stage::Coding,
|
||||
Stage::Coding { claim: None },
|
||||
Stage::Qa,
|
||||
Stage::Done {
|
||||
merged_at: chrono::Utc::now(),
|
||||
@@ -291,7 +292,7 @@ fn review_hold_from_active_stages() {
|
||||
// Story 945: `ReviewHold` transitions to `Stage::ReviewHold { resume_to }`
|
||||
// with the resume_to set to the originating stage, replacing the legacy
|
||||
// boolean flag.
|
||||
for s in [Stage::Backlog, Stage::Coding, Stage::Qa] {
|
||||
for s in [Stage::Backlog, Stage::Coding { claim: None }, Stage::Qa] {
|
||||
let result = transition(
|
||||
s.clone(),
|
||||
PipelineEvent::ReviewHold {
|
||||
@@ -316,6 +317,7 @@ fn merge_failed_final() {
|
||||
let s = Stage::Merge {
|
||||
feature_branch: fb("f"),
|
||||
commits_ahead: nz(1),
|
||||
claim: None,
|
||||
};
|
||||
let result = transition(
|
||||
s,
|
||||
@@ -336,7 +338,7 @@ fn merge_failed_final() {
|
||||
#[test]
|
||||
fn merge_failed_only_from_merge() {
|
||||
let result = transition(
|
||||
Stage::Coding,
|
||||
Stage::Coding { claim: None },
|
||||
PipelineEvent::MergeFailedFinal {
|
||||
reason: "conflicts".into(),
|
||||
},
|
||||
@@ -413,6 +415,7 @@ fn bug_502_agent_not_in_stage() {
|
||||
let merge = Stage::Merge {
|
||||
feature_branch: BranchName("feature/story-1".into()),
|
||||
commits_ahead: NonZeroU32::new(3).unwrap(),
|
||||
claim: None,
|
||||
};
|
||||
// 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
|
||||
@@ -480,7 +483,7 @@ fn cannot_deps_met_from_upcoming() {
|
||||
|
||||
#[test]
|
||||
fn reject_from_active_stages() {
|
||||
for s in [Stage::Backlog, Stage::Coding, Stage::Qa] {
|
||||
for s in [Stage::Backlog, Stage::Coding { claim: None }, Stage::Qa] {
|
||||
let result = transition(
|
||||
s.clone(),
|
||||
PipelineEvent::Reject {
|
||||
@@ -493,6 +496,7 @@ fn reject_from_active_stages() {
|
||||
let m = Stage::Merge {
|
||||
feature_branch: fb("f"),
|
||||
commits_ahead: nz(1),
|
||||
claim: None,
|
||||
};
|
||||
let result = transition(
|
||||
m,
|
||||
@@ -561,7 +565,7 @@ fn freeze_transitions_to_frozen_variant_with_resume_to() {
|
||||
);
|
||||
|
||||
let item = read_typed(story_id).unwrap().unwrap();
|
||||
assert!(matches!(item.stage, Stage::Coding));
|
||||
assert!(matches!(item.stage, Stage::Coding { .. }));
|
||||
assert!(!matches!(item.stage, Stage::Frozen { .. }));
|
||||
|
||||
super::apply::transition_to_frozen(story_id).expect("freeze should succeed");
|
||||
@@ -569,7 +573,7 @@ fn freeze_transitions_to_frozen_variant_with_resume_to() {
|
||||
let item = read_typed(story_id).unwrap().unwrap();
|
||||
match &item.stage {
|
||||
Stage::Frozen { resume_to } => assert!(
|
||||
matches!(**resume_to, Stage::Coding),
|
||||
matches!(**resume_to, Stage::Coding { .. }),
|
||||
"resume_to should preserve the previous stage; got {resume_to:?}"
|
||||
),
|
||||
other => panic!("stage should be Stage::Frozen after freeze; got {other:?}"),
|
||||
@@ -583,7 +587,7 @@ fn freeze_transitions_to_frozen_variant_with_resume_to() {
|
||||
|
||||
let item = read_typed(story_id).unwrap().unwrap();
|
||||
assert!(
|
||||
matches!(item.stage, Stage::Coding),
|
||||
matches!(item.stage, Stage::Coding { .. }),
|
||||
"stage should return to Coding after unfreeze: {:?}",
|
||||
item.stage
|
||||
);
|
||||
@@ -884,10 +888,11 @@ fn merge_aborted_returns_to_coding() {
|
||||
let s = Stage::Merge {
|
||||
feature_branch: fb("feature/story-73"),
|
||||
commits_ahead: nz(2),
|
||||
claim: None,
|
||||
};
|
||||
let result = transition(s, PipelineEvent::MergeAborted).unwrap();
|
||||
assert!(
|
||||
matches!(result, Stage::Coding),
|
||||
matches!(result, Stage::Coding { .. }),
|
||||
"Merge + MergeAborted should return to Coding, got: {result:?}"
|
||||
);
|
||||
}
|
||||
@@ -915,7 +920,7 @@ fn merge_aborted_moves_to_coding_via_crdt() {
|
||||
fired.before
|
||||
);
|
||||
assert!(
|
||||
matches!(fired.after, Stage::Coding),
|
||||
matches!(fired.after, Stage::Coding { .. }),
|
||||
"fired.after should be Coding: {:?}",
|
||||
fired.after
|
||||
);
|
||||
@@ -958,7 +963,7 @@ fn move_story_merge_to_current_succeeds() {
|
||||
.expect("CRDT read should succeed")
|
||||
.expect("item should exist");
|
||||
assert!(
|
||||
matches!(item.stage, Stage::Coding),
|
||||
matches!(item.stage, Stage::Coding { .. }),
|
||||
"story should be in Coding after move_story_to_stage(merge → current): {:?}",
|
||||
item.stage
|
||||
);
|
||||
@@ -974,7 +979,7 @@ fn hotfix_requested_from_done_lands_in_coding() {
|
||||
};
|
||||
let result = transition(done, PipelineEvent::HotfixRequested).unwrap();
|
||||
assert!(
|
||||
matches!(result, Stage::Coding),
|
||||
matches!(result, Stage::Coding { .. }),
|
||||
"Done + HotfixRequested must land in Coding; got: {:?}",
|
||||
result
|
||||
);
|
||||
@@ -984,11 +989,12 @@ fn hotfix_requested_from_done_lands_in_coding() {
|
||||
fn hotfix_requested_rejected_from_non_done_stages() {
|
||||
for stage in [
|
||||
Stage::Backlog,
|
||||
Stage::Coding,
|
||||
Stage::Coding { claim: None },
|
||||
Stage::Qa,
|
||||
Stage::Merge {
|
||||
feature_branch: fb("feature/story-1"),
|
||||
commits_ahead: nz(1),
|
||||
claim: None,
|
||||
},
|
||||
] {
|
||||
let result = transition(stage.clone(), PipelineEvent::HotfixRequested);
|
||||
|
||||
@@ -149,10 +149,10 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
(Upcoming, Triage) => Ok(Backlog),
|
||||
|
||||
// ── Forward path ────────────────────────────────────────────────
|
||||
(Backlog, DepsMet) => Ok(Coding),
|
||||
(Coding, GatesStarted) => Ok(Qa),
|
||||
(Backlog, DepsMet) => Ok(Coding { claim: None }),
|
||||
(Coding { .. }, GatesStarted) => Ok(Qa),
|
||||
(
|
||||
Coding,
|
||||
Coding { .. },
|
||||
QaSkipped {
|
||||
feature_branch,
|
||||
commits_ahead,
|
||||
@@ -160,6 +160,7 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
) => Ok(Merge {
|
||||
feature_branch,
|
||||
commits_ahead,
|
||||
claim: None,
|
||||
}),
|
||||
(
|
||||
Qa,
|
||||
@@ -170,8 +171,9 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
) => Ok(Merge {
|
||||
feature_branch,
|
||||
commits_ahead,
|
||||
claim: None,
|
||||
}),
|
||||
(Qa, GatesFailed { .. }) => Ok(Coding),
|
||||
(Qa, GatesFailed { .. }) => Ok(Coding { claim: None }),
|
||||
(Merge { .. }, MergeSucceeded { merge_commit }) => Ok(Done {
|
||||
merged_at: now,
|
||||
merge_commit,
|
||||
@@ -193,7 +195,7 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
|
||||
// ── Block: any active → Blocked ──────────────────────────────
|
||||
(Backlog, Block { reason })
|
||||
| (Coding, Block { reason })
|
||||
| (Coding { .. }, Block { reason })
|
||||
| (Qa, Block { reason })
|
||||
| (Merge { .. }, Block { reason }) => Ok(Blocked { reason }),
|
||||
|
||||
@@ -201,18 +203,20 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
// story to `Stage::ReviewHold { resume_to, reason }`, preserving the
|
||||
// current stage as the resume target so a reviewer can clear the
|
||||
// hold and continue.
|
||||
(s @ (Backlog | Coding | Qa | Merge { .. }), PipelineEvent::ReviewHold { reason }) => {
|
||||
Ok(Stage::ReviewHold {
|
||||
resume_to: Box::new(s),
|
||||
reason,
|
||||
})
|
||||
}
|
||||
(
|
||||
s @ (Backlog | Coding { .. } | Qa | Merge { .. }),
|
||||
PipelineEvent::ReviewHold { reason },
|
||||
) => Ok(Stage::ReviewHold {
|
||||
resume_to: Box::new(s),
|
||||
reason,
|
||||
}),
|
||||
|
||||
// ── MergeFailed: Merge → MergeFailure (recoverable intermediate) ──
|
||||
(
|
||||
Merge {
|
||||
feature_branch,
|
||||
commits_ahead,
|
||||
..
|
||||
},
|
||||
MergeFailed { kind },
|
||||
) => Ok(MergeFailure {
|
||||
@@ -246,14 +250,14 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
// ── Abandon / supersede from any active or done stage ───────────
|
||||
(Upcoming, Abandon)
|
||||
| (Backlog, Abandon)
|
||||
| (Coding, Abandon)
|
||||
| (Coding { .. }, Abandon)
|
||||
| (Qa, Abandon)
|
||||
| (Merge { .. }, Abandon)
|
||||
| (Done { .. }, Abandon) => Ok(Abandoned { ts: now }),
|
||||
|
||||
(Upcoming, Supersede { by })
|
||||
| (Backlog, Supersede { by })
|
||||
| (Coding, Supersede { by })
|
||||
| (Coding { .. }, Supersede { by })
|
||||
| (Qa, Supersede { by })
|
||||
| (Merge { .. }, Supersede { by })
|
||||
| (Done { .. }, Supersede { by }) => Ok(Superseded {
|
||||
@@ -263,7 +267,7 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
|
||||
// ── Reject from any active stage or QA ──────────────────────────
|
||||
(Backlog, Reject { reason })
|
||||
| (Coding, Reject { reason })
|
||||
| (Coding { .. }, Reject { reason })
|
||||
| (Qa, Reject { reason })
|
||||
| (Merge { .. }, Reject { reason }) => Ok(Rejected { ts: now, reason }),
|
||||
|
||||
@@ -272,21 +276,24 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
// the backlog while waiting on dependent fixes, without losing it to
|
||||
// Archived. Unlike `Unblock` (Blocked → Coding), this does not
|
||||
// re-enter the active flow.
|
||||
(Coding, Demote) | (Qa, Demote) | (Merge { .. }, Demote) | (Blocked { .. }, Demote) => {
|
||||
Ok(Backlog)
|
||||
}
|
||||
(Coding { .. }, Demote)
|
||||
| (Qa, Demote)
|
||||
| (Merge { .. }, Demote)
|
||||
| (Blocked { .. }, Demote) => Ok(Backlog),
|
||||
|
||||
// ── Close: direct completion from any active stage ─────────────
|
||||
(Backlog, Close) | (Coding, Close) | (Qa, Close) | (Merge { .. }, Close) => Ok(Done {
|
||||
merged_at: now,
|
||||
merge_commit: GitSha("closed".to_string()),
|
||||
}),
|
||||
(Backlog, Close) | (Coding { .. }, Close) | (Qa, Close) | (Merge { .. }, Close) => {
|
||||
Ok(Done {
|
||||
merged_at: now,
|
||||
merge_commit: GitSha("closed".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Freeze: any non-terminal stage → Frozen { resume_to } ──────
|
||||
(
|
||||
s @ (Upcoming
|
||||
| Backlog
|
||||
| Coding
|
||||
| Coding { .. }
|
||||
| Qa
|
||||
| Merge { .. }
|
||||
| Blocked { .. }
|
||||
@@ -305,7 +312,7 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
(Stage::ReviewHold { resume_to, .. }, ReviewHoldCleared) => Ok(*resume_to),
|
||||
|
||||
// ── FixupRequested: MergeFailure → Coding (coder fixup) ────────
|
||||
(MergeFailure { .. }, FixupRequested) => Ok(Coding),
|
||||
(MergeFailure { .. }, FixupRequested) => Ok(Coding { claim: None }),
|
||||
|
||||
// ── FixupRequested: MergeFailureFinal → Coding (operator override)
|
||||
//
|
||||
@@ -314,19 +321,19 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
// the gate failure is fixable and send the story back for another
|
||||
// coder attempt. The budget counter is a mergemaster bookkeeping
|
||||
// detail, not a hard ceiling.
|
||||
(MergeFailureFinal { .. }, FixupRequested) => Ok(Coding),
|
||||
(MergeFailureFinal { .. }, FixupRequested) => Ok(Coding { claim: None }),
|
||||
|
||||
// ── ReQueuedForQa: MergeFailure → Qa (re-review) ────────────────
|
||||
(MergeFailure { .. }, ReQueuedForQa) => Ok(Qa),
|
||||
|
||||
// ── MergeAborted: Merge → Coding (abort in-flight merge) ─────────
|
||||
(Merge { .. }, MergeAborted) => Ok(Coding),
|
||||
(Merge { .. }, MergeAborted) => Ok(Coding { claim: None }),
|
||||
|
||||
// ── HotfixRequested: Done → Coding (post-merge hotfix) ───────────
|
||||
// Allows reopening a completed story so a coder can apply a hotfix.
|
||||
// A fresh feature branch is forked from master when auto-assign spawns
|
||||
// the coder.
|
||||
(Done { .. }, HotfixRequested) => Ok(Coding),
|
||||
(Done { .. }, HotfixRequested) => Ok(Coding { claim: None }),
|
||||
|
||||
// ── MergemasterAttempted: MergeFailure → MergeFailureFinal ─────
|
||||
(MergeFailure { kind, .. }, MergemasterAttempted) => Ok(MergeFailureFinal { kind }),
|
||||
@@ -337,7 +344,7 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
(Stage::ReviewHold { resume_to, .. }, Unblock) => Ok(*resume_to),
|
||||
|
||||
// ── Unblock: Blocked → Coding ─────────────────────────────────
|
||||
(Blocked { .. }, Unblock) => Ok(Coding),
|
||||
(Blocked { .. }, Unblock) => Ok(Coding { claim: None }),
|
||||
|
||||
// ── Unblock MergeFailure → Merge (re-attempt) ────────────────────
|
||||
// `unblock_story` on a failed merge re-queues it for merge, restoring
|
||||
@@ -353,6 +360,7 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
) => Ok(Merge {
|
||||
feature_branch,
|
||||
commits_ahead,
|
||||
claim: None,
|
||||
}),
|
||||
|
||||
// ── Demote MergeFailure → Backlog (manual parking) ───────────────
|
||||
|
||||
@@ -109,6 +109,22 @@ impl MergeFailureKind {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Agent claim payload ────────────────────────────────────────────────────
|
||||
|
||||
/// Active claim on a pipeline item by a specific agent.
|
||||
///
|
||||
/// Embedded directly in [`Stage::Coding`] and [`Stage::Merge`] rather than
|
||||
/// stored in separate CRDT registers. Readers access the claim via
|
||||
/// `item.stage()` rather than through a separate `item.claim()` accessor
|
||||
/// (story 1009).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AgentClaim {
|
||||
/// The agent (e.g. `"coder-1"`) that has claimed this work item.
|
||||
pub agent: AgentName,
|
||||
/// When the claim was written.
|
||||
pub claimed_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
// ── Synced pipeline stage (lives in CRDT, converges across nodes) ───────────
|
||||
|
||||
/// The pipeline stage for a work item.
|
||||
@@ -146,17 +162,26 @@ pub enum Stage {
|
||||
Backlog,
|
||||
|
||||
/// Story is being actively coded somewhere in the mesh.
|
||||
Coding,
|
||||
///
|
||||
/// Carries an optional [`AgentClaim`] identifying which agent is currently
|
||||
/// working on this item. `None` means the item is in the coding stage but
|
||||
/// no agent has claimed it yet (e.g. just transitioned from Backlog and
|
||||
/// waiting for an agent to pick it up).
|
||||
Coding { claim: Option<AgentClaim> },
|
||||
|
||||
/// Coder has run; gates are running.
|
||||
Qa,
|
||||
|
||||
/// Gates passed; ready to merge.
|
||||
///
|
||||
/// `commits_ahead: NonZeroU32` makes "Merge with nothing to merge"
|
||||
/// structurally impossible (eliminates bug 519).
|
||||
/// structurally impossible (eliminates bug 519). The optional
|
||||
/// [`AgentClaim`] carries the mergemaster agent that owns this merge.
|
||||
Merge {
|
||||
feature_branch: BranchName,
|
||||
commits_ahead: NonZeroU32,
|
||||
/// Agent currently running the merge, or `None` when unclaimed.
|
||||
claim: Option<AgentClaim>,
|
||||
},
|
||||
|
||||
/// Mergemaster squashed to master. Always carries merge metadata.
|
||||
@@ -274,7 +299,7 @@ impl Stage {
|
||||
match s {
|
||||
"upcoming" => Some(Stage::Upcoming),
|
||||
"backlog" => Some(Stage::Backlog),
|
||||
"coding" => Some(Stage::Coding),
|
||||
"coding" => Some(Stage::Coding { claim: None }),
|
||||
"blocked" => Some(Stage::Blocked {
|
||||
reason: String::new(),
|
||||
}),
|
||||
@@ -282,6 +307,7 @@ impl Stage {
|
||||
"merge" => Some(Stage::Merge {
|
||||
feature_branch: BranchName(String::new()),
|
||||
commits_ahead: NonZeroU32::new(1).expect("1 is non-zero"),
|
||||
claim: None,
|
||||
}),
|
||||
"merge_failure" => Some(Stage::MergeFailure {
|
||||
kind: MergeFailureKind::Other(String::new()),
|
||||
@@ -292,10 +318,10 @@ impl Stage {
|
||||
kind: MergeFailureKind::Other(String::new()),
|
||||
}),
|
||||
"frozen" => Some(Stage::Frozen {
|
||||
resume_to: Box::new(Stage::Coding),
|
||||
resume_to: Box::new(Stage::Coding { claim: None }),
|
||||
}),
|
||||
"review_hold" => Some(Stage::ReviewHold {
|
||||
resume_to: Box::new(Stage::Coding),
|
||||
resume_to: Box::new(Stage::Coding { claim: None }),
|
||||
reason: String::new(),
|
||||
}),
|
||||
"done" => Some(Stage::Done {
|
||||
@@ -391,7 +417,7 @@ pub fn stage_label(s: &Stage) -> &'static str {
|
||||
match s {
|
||||
Stage::Upcoming => "Upcoming",
|
||||
Stage::Backlog => "Backlog",
|
||||
Stage::Coding => "Coding",
|
||||
Stage::Coding { .. } => "Coding",
|
||||
Stage::Qa => "Qa",
|
||||
Stage::Merge { .. } => "Merge",
|
||||
Stage::MergeFailure { .. } => "MergeFailure",
|
||||
@@ -416,7 +442,7 @@ pub fn stage_dir_name(s: &Stage) -> &'static str {
|
||||
match s {
|
||||
Stage::Upcoming => "upcoming",
|
||||
Stage::Backlog => "backlog",
|
||||
Stage::Coding => "coding",
|
||||
Stage::Coding { .. } => "coding",
|
||||
Stage::Blocked { .. } => "blocked",
|
||||
Stage::Qa => "qa",
|
||||
Stage::Merge { .. } => "merge",
|
||||
|
||||
Reference in New Issue
Block a user