huskies: merge 871

This commit is contained in:
dave
2026-04-29 15:40:03 +00:00
parent 2655288412
commit 7f8467b068
4 changed files with 127 additions and 1 deletions
@@ -120,6 +120,10 @@ pub async fn handle_assign(
return format!("Failed to assign model to **{story_name}**: {e}");
}
// Mirror the assignment into the CRDT register so the in-memory pipeline
// state stays consistent with the front-matter.
crate::crdt_state::set_agent(&story_id, Some(&agent_name));
// Check whether a coder is already running on this story.
let running_coders: Vec<_> = agents
.list_agents()
+1 -1
View File
@@ -53,7 +53,7 @@ pub use types::{
};
pub use write::{
bump_retry_count, migrate_names_from_slugs, migrate_story_ids_to_numeric, name_from_story_id,
set_qa_mode, set_retry_count, write_item,
set_agent, set_qa_mode, set_retry_count, write_item,
};
#[cfg(test)]
+103
View File
@@ -186,6 +186,36 @@ pub fn migrate_names_from_slugs() {
slog!("[crdt] Migrated names for {count} items from story ID slugs");
}
/// Set the `agent` field for a pipeline item by its story ID.
///
/// `Some(name)` writes the agent name into the CRDT register.
/// `None` clears the register by writing an empty string — use this
/// to unpin an agent without touching the surrounding item.
///
/// This is the typed setter counterpart to [`write_item`]'s `agent` parameter.
/// Callers that only need to update the agent (e.g. the `update_story` MCP tool
/// and the Matrix `!assign` command) should prefer this function over
/// passing `agent` through the full [`write_item`] call, which requires all
/// other fields to be known.
///
/// Returns `true` if the item was found and the write was performed.
pub fn set_agent(story_id: &str, agent: Option<&str>) -> bool {
let Some(state_mutex) = get_crdt() else {
return false;
};
let Ok(mut state) = state_mutex.lock() else {
return false;
};
let Some(&idx) = state.index.get(story_id) else {
return false;
};
let value = agent.unwrap_or("").to_string();
apply_and_persist(&mut state, |s| {
s.crdt.doc.items[idx].agent.set(value.clone())
});
true
}
/// Set the typed `qa_mode` CRDT register for a pipeline item.
///
/// Passing `Some(mode)` writes the mode string (e.g. `"server"`, `"agent"`, `"human"`)
@@ -685,6 +715,79 @@ mod tests {
migrate_names_from_slugs();
}
// ── set_agent tests ──────────────────────────────────────────────────────
#[test]
fn set_agent_some_writes_name() {
init_for_test();
write_item(
"871_story_set_agent_write",
"2_current",
Some("Set Agent Write"),
None,
None,
None,
None,
None,
None,
None,
);
let found = set_agent("871_story_set_agent_write", Some("coder-1"));
assert!(found, "set_agent should return true for an existing item");
let item = read_item("871_story_set_agent_write").expect("item must exist");
assert_eq!(
item.agent.as_deref(),
Some("coder-1"),
"agent should be written to CRDT register"
);
}
#[test]
fn set_agent_none_clears_register() {
init_for_test();
write_item(
"871_story_set_agent_clear",
"2_current",
Some("Set Agent Clear"),
Some("coder-2"),
None,
None,
None,
None,
None,
None,
);
// Confirm agent is set.
let before = read_item("871_story_set_agent_clear").expect("item must exist");
assert_eq!(before.agent.as_deref(), Some("coder-2"));
// Clear it.
let found = set_agent("871_story_set_agent_clear", None);
assert!(found, "set_agent should return true for an existing item");
let after = read_item("871_story_set_agent_clear").expect("item must exist");
assert!(
after.agent.as_deref().unwrap_or("").is_empty(),
"agent should be cleared (empty string) after set_agent(None)"
);
}
#[test]
fn set_agent_returns_false_for_unknown_story() {
init_for_test();
let found = set_agent("999_story_nonexistent", Some("coder-1"));
assert!(
!found,
"set_agent should return false when story is not in the CRDT"
);
}
// ── set_qa_mode regression tests ─────────────────────────────────────────
#[test]
@@ -44,6 +44,19 @@ pub(crate) fn tool_update_story(args: &Value, ctx: &AppContext) -> Result<String
Some(&front_matter)
};
// Capture the agent value before moving front_matter into the file writer,
// so we can mirror it into the CRDT register below.
let agent_for_crdt = args
.get("agent")
.and_then(|v| v.as_str())
.or_else(|| {
args.get("front_matter")
.and_then(|v| v.as_object())
.and_then(|o| o.get("agent"))
.and_then(|v| v.as_str())
})
.map(str::to_string);
let root = ctx.state.get_project_root()?;
// Only call update_story_in_file when there is something left to write.
@@ -51,6 +64,12 @@ pub(crate) fn tool_update_story(args: &Value, ctx: &AppContext) -> Result<String
update_story_in_file(&root, story_id, user_story, description, front_matter_opt)?;
}
// Mirror the agent assignment into the CRDT register so the in-memory
// pipeline state stays consistent with the front-matter.
if let Some(ref a) = agent_for_crdt {
crate::crdt_state::set_agent(story_id, Some(a));
}
// Bug 503: warn if any depends_on in the (now updated) story points at an archived story.
let stage = crate::pipeline_state::read_typed(story_id)
.ok()