huskies: merge 871
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user