huskies: merge 945

This commit is contained in:
dave
2026-05-13 06:05:01 +00:00
parent 3a8894ea8f
commit 9ce5a8df0c
53 changed files with 497 additions and 654 deletions
-1
View File
@@ -92,7 +92,6 @@ pub(crate) fn tool_dump_crdt(args: &Value) -> Result<String, String> {
"name": item.name,
"agent": item.agent,
"retry_count": item.retry_count,
"blocked": item.blocked,
"depends_on": item.depends_on,
"claimed_by": item.claimed_by,
"claimed_at": item.claimed_at,
-2
View File
@@ -291,7 +291,6 @@ mod tests {
None,
None,
None,
None,
);
let tmp = tempfile::tempdir().unwrap();
let ctx = test_ctx(tmp.path());
@@ -318,7 +317,6 @@ mod tests {
None,
None,
None,
None,
);
let tmp = tempfile::tempdir().unwrap();
let ctx = test_ctx(tmp.path());
+10 -2
View File
@@ -54,8 +54,16 @@ pub(super) async fn tool_approve_qa(args: &Value, ctx: &AppContext) -> Result<St
let project_root = ctx.services.agents.get_project_root(&ctx.state)?;
// Clear review_hold before moving (story 932: CRDT register).
crate::crdt_state::set_review_hold(story_id, false);
// Clear review_hold before moving (story 945: Stage::ReviewHold variant).
if let Ok(Some(item)) = crate::pipeline_state::read_typed(story_id)
&& matches!(item.stage, crate::pipeline_state::Stage::ReviewHold { .. })
{
let _ = crate::pipeline_state::apply_transition_str(
story_id,
crate::pipeline_state::PipelineEvent::ReviewHoldCleared,
None,
);
}
if is_spike(story_id) {
// Spikes skip the merge stage entirely: merge the feature branch to master
+1 -14
View File
@@ -183,9 +183,6 @@ pub(super) async fn tool_status(args: &Value, ctx: &AppContext) -> Result<String
if let Some(agent) = view.agent() {
front_matter.insert("agent".to_string(), json!(agent));
}
if view.blocked() {
front_matter.insert("blocked".to_string(), json!(true));
}
if let Some(qa) = view.qa_mode() {
front_matter.insert("qa".to_string(), json!(qa));
}
@@ -216,14 +213,6 @@ pub(super) async fn tool_status(args: &Value, ctx: &AppContext) -> Result<String
front_matter.insert("merge_failure".to_string(), json!(mf));
}
// review_hold is a typed CRDT register (story 932).
if crate::crdt_state::read_item(story_id)
.map(|v| v.review_hold())
.unwrap_or(false)
{
front_matter.insert("review_hold".to_string(), json!(true));
}
// --- AC checklist ---
let ac_items: Vec<Value> = parse_ac_items(&contents)
.into_iter()
@@ -357,7 +346,7 @@ mod tests {
}
#[tokio::test]
async fn tool_status_returns_blocked_retry_count_and_depends_on() {
async fn tool_status_returns_retry_count_and_depends_on() {
let tmp = tempdir().unwrap();
crate::crdt_state::init_for_test();
@@ -369,7 +358,6 @@ mod tests {
story_content,
crate::db::ItemMeta::named("Blocked Story"),
);
crate::crdt_state::set_blocked("9887_story_blocked_test", true);
crate::crdt_state::set_retry_count("9887_story_blocked_test", 3);
crate::crdt_state::set_depends_on("9887_story_blocked_test", &[100, 200]);
@@ -379,7 +367,6 @@ mod tests {
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["front_matter"]["blocked"], true);
assert_eq!(parsed["front_matter"]["retry_count"], 3);
let depends_on = parsed["front_matter"]["depends_on"].as_array().unwrap();
assert_eq!(depends_on.len(), 2);
+14 -14
View File
@@ -127,20 +127,20 @@ pub(crate) fn tool_show_epic(args: &Value, _ctx: &AppContext) -> Result<String,
continue;
};
if member_view.epic() == Some(epic_id) {
// Frozen is now an orthogonal CRDT flag (story 934, stage 4).
let stage_name = if member_view.frozen() {
"frozen"
} else {
match &item.stage {
Stage::Upcoming | Stage::Backlog => "backlog",
Stage::Coding => "current",
Stage::Qa => "qa",
Stage::Merge { .. } => "merge",
Stage::Done { .. } => "done",
Stage::Archived { .. } => "archived",
Stage::MergeFailure { .. } => "merge_failure",
Stage::Blocked { .. } => "blocked",
}
// Story 945: Frozen / ReviewHold / MergeFailureFinal are first-class
// Stage variants — no more orthogonal boolean flags.
let stage_name = match &item.stage {
Stage::Upcoming | Stage::Backlog => "backlog",
Stage::Coding => "current",
Stage::Qa => "qa",
Stage::Merge { .. } => "merge",
Stage::Done { .. } => "done",
Stage::Archived { .. } => "archived",
Stage::MergeFailure { .. } => "merge_failure",
Stage::MergeFailureFinal { .. } => "merge_failure_final",
Stage::Blocked { .. } => "blocked",
Stage::Frozen { .. } => "frozen",
Stage::ReviewHold { .. } => "review_hold",
};
if matches!(item.stage, Stage::Done { .. }) {
done += 1;
@@ -91,12 +91,30 @@ pub(crate) fn tool_update_story(args: &Value, ctx: &AppContext) -> Result<String
}
}
"review_hold" => {
if let Some(b) = value.as_bool() {
crate::crdt_state::set_review_hold(story_id, b);
} else if value.as_str() == Some("true") {
crate::crdt_state::set_review_hold(story_id, true);
} else if value.as_str() == Some("false") {
crate::crdt_state::set_review_hold(story_id, false);
let want_hold = match value {
Value::Bool(b) => Some(*b),
Value::String(s) if s == "true" => Some(true),
Value::String(s) if s == "false" => Some(false),
_ => None,
};
match want_hold {
Some(true) => {
crate::pipeline_state::apply_transition_str(
story_id,
crate::pipeline_state::PipelineEvent::ReviewHold {
reason: "Set via update_story".to_string(),
},
None,
)?;
}
Some(false) => {
crate::pipeline_state::apply_transition_str(
story_id,
crate::pipeline_state::PipelineEvent::ReviewHoldCleared,
None,
)?;
}
None => {}
}
}
"frozen" => {
@@ -126,8 +144,12 @@ pub(crate) fn tool_update_story(args: &Value, ctx: &AppContext) -> Result<String
}
}
"mergemaster_attempted" => {
if let Some(b) = value.as_bool() {
crate::crdt_state::set_mergemaster_attempted(story_id, b);
if let Some(true) = value.as_bool() {
crate::pipeline_state::apply_transition_str(
story_id,
crate::pipeline_state::PipelineEvent::MergemasterAttempted,
None,
)?;
}
}
other => {