huskies: merge 945
This commit is contained in:
@@ -594,13 +594,15 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
crate::db::ensure_content_store();
|
||||
// Story 945: "blocked AND in 4_merge" is no longer representable as
|
||||
// separate states. A blocked story lives in `Stage::Blocked` (which
|
||||
// maps to wire-form "blocked"), so auto-assign won't see it in 4_merge.
|
||||
crate::db::write_item_with_content(
|
||||
"9863_story_blocked_conflict",
|
||||
"4_merge",
|
||||
"blocked",
|
||||
"CONFLICT (content): foo.rs",
|
||||
crate::db::ItemMeta {
|
||||
name: Some("Blocked conflict".to_string()),
|
||||
blocked: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
@@ -633,13 +635,13 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
crate::db::ensure_content_store();
|
||||
// Story 945: "mergemaster attempted" is now `Stage::MergeFailureFinal`.
|
||||
crate::db::write_item_with_content(
|
||||
"9862_story_attempted",
|
||||
"4_merge",
|
||||
"merge_failure_final",
|
||||
"CONFLICT (content): foo.rs",
|
||||
crate::db::ItemMeta::named("Already tried"),
|
||||
);
|
||||
crate::crdt_state::set_mergemaster_attempted("9862_story_attempted", true);
|
||||
|
||||
let pool = AgentPool::new_test(3001);
|
||||
pool.auto_assign_available_work(tmp.path()).await;
|
||||
@@ -712,16 +714,13 @@ mod tests {
|
||||
.unwrap();
|
||||
crate::crdt_state::init_for_test();
|
||||
crate::db::ensure_content_store();
|
||||
// mergemaster_attempted is set by the exit path when genuine give-up occurs.
|
||||
// Story 945: the genuine give-up state is now `Stage::MergeFailureFinal`.
|
||||
crate::db::write_item_with_content(
|
||||
"920_story_genuine",
|
||||
"4_merge_failure",
|
||||
"merge_failure_final",
|
||||
"CONFLICT (content): bar.rs",
|
||||
crate::db::ItemMeta::named("Genuine"),
|
||||
);
|
||||
// The CRDT register is the sole authority; set it explicitly as the
|
||||
// spawn exit path would after report_merge_failure.
|
||||
crate::crdt_state::set_mergemaster_attempted("920_story_genuine", true);
|
||||
|
||||
let pool = AgentPool::new_test(3001);
|
||||
pool.auto_assign_available_work(tmp.path()).await;
|
||||
|
||||
@@ -215,10 +215,16 @@ impl AgentPool {
|
||||
message: format!("Failed to advance to QA: {e}"),
|
||||
});
|
||||
} else {
|
||||
// Story 932: review_hold is a typed CRDT register.
|
||||
crate::crdt_state::set_review_hold(story_id, true);
|
||||
// Story 945: ReviewHold is a typed Stage variant.
|
||||
let _ = crate::pipeline_state::apply_transition(
|
||||
story_id,
|
||||
crate::pipeline_state::PipelineEvent::ReviewHold {
|
||||
reason: "qa: human — gates passed, awaiting review".to_string(),
|
||||
},
|
||||
None,
|
||||
);
|
||||
eprintln!(
|
||||
"[startup:reconcile] Moved '{story_id}' → 3_qa/ (qa: human — holding for review)."
|
||||
"[startup:reconcile] Moved '{story_id}' → review_hold (qa: human — holding for review)."
|
||||
);
|
||||
let _ = progress_tx.send(ReconciliationEvent {
|
||||
story_id: story_id.clone(),
|
||||
@@ -278,8 +284,14 @@ impl AgentPool {
|
||||
};
|
||||
|
||||
if needs_human_review {
|
||||
// Story 932: review_hold is a typed CRDT register.
|
||||
crate::crdt_state::set_review_hold(story_id, true);
|
||||
// Story 945: ReviewHold is a typed Stage variant.
|
||||
let _ = crate::pipeline_state::apply_transition(
|
||||
story_id,
|
||||
crate::pipeline_state::PipelineEvent::ReviewHold {
|
||||
reason: "Passed QA — waiting for human review.".to_string(),
|
||||
},
|
||||
None,
|
||||
);
|
||||
eprintln!(
|
||||
"[startup:reconcile] '{story_id}' passed QA — holding for human review."
|
||||
);
|
||||
|
||||
@@ -21,15 +21,16 @@ pub(super) fn read_story_front_matter_agent(
|
||||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
/// Return `true` if the story has its `review_hold` CRDT register set.
|
||||
/// Return `true` if the story is in `Stage::ReviewHold`.
|
||||
///
|
||||
/// Sub-story 932: `review_hold` is now a dedicated CRDT register on
|
||||
/// `PipelineItemCrdt`, distinct from `Stage::Frozen`. The auto-assigner uses
|
||||
/// this to keep human-QA items / spikes parked after gates pass until a
|
||||
/// reviewer explicitly clears the hold (e.g. via `tool_approve_qa`).
|
||||
/// Story 945: `Stage::ReviewHold { resume_to, reason }` is the single source
|
||||
/// of truth — the legacy `review_hold: bool` CRDT register has been deleted.
|
||||
/// The auto-assigner uses this to keep human-QA items / spikes parked after
|
||||
/// gates pass until a reviewer explicitly clears the hold (e.g. via
|
||||
/// `tool_approve_qa`).
|
||||
pub(super) fn has_review_hold(_project_root: &Path, _stage_dir: &str, story_id: &str) -> bool {
|
||||
crate::crdt_state::read_item(story_id)
|
||||
.map(|w| w.review_hold())
|
||||
.map(|w| w.stage().is_review_hold())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -80,18 +81,19 @@ pub(super) fn has_content_conflict_failure(
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Return `true` if the CRDT `mergemaster_attempted` register is set for this story.
|
||||
/// Return `true` if the story is in `Stage::MergeFailureFinal`.
|
||||
///
|
||||
/// Story 945: `Stage::MergeFailureFinal` is the single source of truth —
|
||||
/// the legacy `mergemaster_attempted: bool` CRDT register has been deleted.
|
||||
/// Used to prevent the auto-assigner from repeatedly spawning mergemaster for
|
||||
/// the same story after a failed mergemaster session. The CRDT register is the
|
||||
/// only source consulted — the legacy YAML field is no longer checked.
|
||||
/// the same story after a failed mergemaster session.
|
||||
pub(super) fn has_mergemaster_attempted(
|
||||
_project_root: &Path,
|
||||
_stage_dir: &str,
|
||||
story_id: &str,
|
||||
) -> bool {
|
||||
crate::crdt_state::read_item(story_id)
|
||||
.map(|view| view.mergemaster_attempted())
|
||||
.map(|view| view.stage().is_mergemaster_attempted())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -116,13 +118,15 @@ pub(super) fn check_archived_dependencies(
|
||||
crate::crdt_state::check_archived_deps_crdt(story_id)
|
||||
}
|
||||
|
||||
/// Return `true` if the story's `frozen` CRDT flag is set (story 934, stage 4).
|
||||
/// Return `true` if the story is in `Stage::Frozen`.
|
||||
///
|
||||
/// `frozen` is orthogonal to [`Stage`]: a frozen story keeps its current stage
|
||||
/// register but is skipped by the auto-assigner.
|
||||
/// Story 945: `Stage::Frozen { resume_to }` is the single source of truth —
|
||||
/// the legacy `frozen: bool` CRDT register has been deleted. Frozen stories
|
||||
/// are skipped by the auto-assigner until `Unfreeze` returns them to
|
||||
/// `resume_to`.
|
||||
pub(super) fn is_story_frozen(_project_root: &Path, _stage_dir: &str, story_id: &str) -> bool {
|
||||
crate::crdt_state::read_item(story_id)
|
||||
.map(|view| view.frozen())
|
||||
.map(|view| view.stage().is_frozen())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -139,9 +143,11 @@ mod tests {
|
||||
crate::crdt_state::init_for_test();
|
||||
crate::db::ensure_content_store();
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
// Story 945: review_hold is now a typed Stage variant, seeded via
|
||||
// the wire-form stage register directly.
|
||||
crate::crdt_state::write_item_str(
|
||||
"890_spike_held",
|
||||
"3_qa",
|
||||
"review_hold",
|
||||
Some("Held Spike"),
|
||||
None,
|
||||
None,
|
||||
@@ -149,9 +155,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
crate::crdt_state::set_review_hold("890_spike_held", true);
|
||||
assert!(has_review_hold(tmp.path(), "3_qa", "890_spike_held"));
|
||||
}
|
||||
|
||||
@@ -170,7 +174,6 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(!has_review_hold(tmp.path(), "3_qa", "890_spike_active_qa"));
|
||||
}
|
||||
@@ -258,7 +261,6 @@ mod tests {
|
||||
Some("Blocked"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some("[999]"),
|
||||
None,
|
||||
None,
|
||||
@@ -285,7 +287,6 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
crate::crdt_state::write_item_str(
|
||||
"10_story_ok",
|
||||
@@ -293,7 +294,6 @@ mod tests {
|
||||
Some("Ok"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some("[999]"),
|
||||
None,
|
||||
None,
|
||||
@@ -320,7 +320,6 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(!has_unmet_dependencies(
|
||||
tmp.path(),
|
||||
@@ -346,7 +345,6 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
crate::crdt_state::write_item_str(
|
||||
"503_story_dependent",
|
||||
@@ -354,7 +352,6 @@ mod tests {
|
||||
Some("Dependent"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some("[500]"),
|
||||
None,
|
||||
None,
|
||||
@@ -380,7 +377,6 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
crate::crdt_state::write_item_str(
|
||||
"503_story_waiting",
|
||||
@@ -388,7 +384,6 @@ mod tests {
|
||||
Some("Waiting"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some("[490]"),
|
||||
None,
|
||||
None,
|
||||
|
||||
@@ -253,7 +253,6 @@ max_turns = 10
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
// 12 turns in a single session exceeds the configured max of 10.
|
||||
@@ -381,7 +380,6 @@ max_turns = 10
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
// Prior session with 5 turns (under limit alone).
|
||||
@@ -461,7 +459,6 @@ max_turns = 10
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
// Session 1: exceeds limit → retry_count=1 in CRDT, NOT blocked.
|
||||
|
||||
@@ -66,10 +66,21 @@ pub(super) fn resolve_qa_mode_from_store(
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
/// Mark a story as held for human review (story 932: CRDT register).
|
||||
/// Mark a story as held for human review (story 945: `Stage::ReviewHold`).
|
||||
///
|
||||
/// The caller has just moved the story to QA via `move_story_to_qa`, so the
|
||||
/// story is in `Stage::Qa`. We transition to `Stage::ReviewHold { resume_to:
|
||||
/// Qa, reason }` so the auto-assigner skips it while preserving the resume
|
||||
/// target.
|
||||
pub(super) fn write_review_hold_to_store(story_id: &str) {
|
||||
if !crate::crdt_state::set_review_hold(story_id, true) {
|
||||
slog_error!("[pipeline] Cannot set review_hold for '{story_id}': no CRDT entry");
|
||||
if let Err(e) = crate::pipeline_state::apply_transition_str(
|
||||
story_id,
|
||||
crate::pipeline_state::PipelineEvent::ReviewHold {
|
||||
reason: "Held for human review".to_string(),
|
||||
},
|
||||
None,
|
||||
) {
|
||||
slog_error!("[pipeline] Cannot set review_hold for '{story_id}': {e}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -574,7 +574,11 @@ pub(super) async fn run_agent_spawn(
|
||||
}
|
||||
};
|
||||
if is_genuine {
|
||||
crate::crdt_state::set_mergemaster_attempted(&sid, true);
|
||||
let _ = crate::pipeline_state::apply_transition_str(
|
||||
&sid,
|
||||
crate::pipeline_state::PipelineEvent::MergemasterAttempted,
|
||||
None,
|
||||
);
|
||||
}
|
||||
let _ = tx_done.send(AgentEvent::Done {
|
||||
story_id: sid.clone(),
|
||||
|
||||
@@ -293,7 +293,6 @@ stage = "coder"
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let pool = AgentPool::new_test(3011);
|
||||
|
||||
Reference in New Issue
Block a user