huskies: merge 572_story_edit_criterion_mcp_tool_to_update_acceptance_criteria_text

This commit is contained in:
dave
2026-04-15 12:59:49 +00:00
parent 52b21c22b1
commit ec40b4771b
4 changed files with 109 additions and 4 deletions
+24 -1
View File
@@ -513,6 +513,28 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
"required": ["story_id", "criterion_index"] "required": ["story_id", "criterion_index"]
} }
}, },
{
"name": "edit_criterion",
"description": "Update the text of an existing acceptance criterion in place, preserving its checked/unchecked state. Uses a 0-based index counting all criteria (both checked and unchecked).",
"inputSchema": {
"type": "object",
"properties": {
"story_id": {
"type": "string",
"description": "Story identifier (filename stem, e.g. '28_my_story')"
},
"criterion_index": {
"type": "integer",
"description": "0-based index of the criterion to edit (counts all criteria)"
},
"new_text": {
"type": "string",
"description": "New text for the criterion (without the '- [ ] ' or '- [x] ' prefix)"
}
},
"required": ["story_id", "criterion_index", "new_text"]
}
},
{ {
"name": "add_criterion", "name": "add_criterion",
"description": "Add an acceptance criterion to an existing story file. Appends '- [ ] {criterion}' after the last existing criterion in the '## Acceptance Criteria' section. Auto-commits via the filesystem watcher.", "description": "Add an acceptance criterion to an existing story file. Appends '- [ ] {criterion}' after the last existing criterion in the '## Acceptance Criteria' section. Auto-commits via the filesystem watcher.",
@@ -1242,6 +1264,7 @@ async fn handle_tools_call(id: Option<Value>, params: &Value, ctx: &AppContext)
"accept_story" => story_tools::tool_accept_story(&args, ctx), "accept_story" => story_tools::tool_accept_story(&args, ctx),
// Story mutation tools (auto-commit to master) // Story mutation tools (auto-commit to master)
"check_criterion" => story_tools::tool_check_criterion(&args, ctx), "check_criterion" => story_tools::tool_check_criterion(&args, ctx),
"edit_criterion" => story_tools::tool_edit_criterion(&args, ctx),
"add_criterion" => story_tools::tool_add_criterion(&args, ctx), "add_criterion" => story_tools::tool_add_criterion(&args, ctx),
"update_story" => story_tools::tool_update_story(&args, ctx), "update_story" => story_tools::tool_update_story(&args, ctx),
// Spike lifecycle tools // Spike lifecycle tools
@@ -1426,7 +1449,7 @@ mod tests {
assert!(names.contains(&"loc_file")); assert!(names.contains(&"loc_file"));
assert!(names.contains(&"dump_crdt")); assert!(names.contains(&"dump_crdt"));
assert!(names.contains(&"get_version")); assert!(names.contains(&"get_version"));
assert_eq!(tools.len(), 63); assert_eq!(tools.len(), 64);
} }
#[test] #[test]
+25 -2
View File
@@ -5,8 +5,9 @@ use crate::agents::{
use crate::http::context::AppContext; use crate::http::context::AppContext;
use crate::http::workflow::{ use crate::http::workflow::{
add_criterion_to_file, check_criterion_in_file, create_bug_file, create_refactor_file, add_criterion_to_file, check_criterion_in_file, create_bug_file, create_refactor_file,
create_spike_file, create_story_file, list_bug_files, list_refactor_files, load_pipeline_state, create_spike_file, create_story_file, edit_criterion_in_file, list_bug_files,
load_upcoming_stories, update_story_in_file, validate_story_dirs, list_refactor_files, load_pipeline_state, load_upcoming_stories, update_story_in_file,
validate_story_dirs,
}; };
use crate::io::story_metadata::{ use crate::io::story_metadata::{
check_archived_deps, check_archived_deps_from_list, parse_front_matter, parse_unchecked_todos, check_archived_deps, check_archived_deps_from_list, parse_front_matter, parse_unchecked_todos,
@@ -331,6 +332,28 @@ pub(super) fn tool_check_criterion(args: &Value, ctx: &AppContext) -> Result<Str
)) ))
} }
pub(super) fn tool_edit_criterion(args: &Value, ctx: &AppContext) -> Result<String, String> {
let story_id = args
.get("story_id")
.and_then(|v| v.as_str())
.ok_or("Missing required argument: story_id")?;
let criterion_index = args
.get("criterion_index")
.and_then(|v| v.as_u64())
.ok_or("Missing required argument: criterion_index")? as usize;
let new_text = args
.get("new_text")
.and_then(|v| v.as_str())
.ok_or("Missing required argument: new_text")?;
let root = ctx.state.get_project_root()?;
edit_criterion_in_file(&root, story_id, criterion_index, new_text)?;
Ok(format!(
"Criterion {criterion_index} updated for story '{story_id}'."
))
}
pub(super) fn tool_add_criterion(args: &Value, ctx: &AppContext) -> Result<String, String> { pub(super) fn tool_add_criterion(args: &Value, ctx: &AppContext) -> Result<String, String> {
let story_id = args let story_id = args
.get("story_id") .get("story_id")
+2 -1
View File
@@ -7,7 +7,8 @@ pub use bug_ops::{
create_bug_file, create_refactor_file, create_spike_file, list_bug_files, list_refactor_files, create_bug_file, create_refactor_file, create_spike_file, list_bug_files, list_refactor_files,
}; };
pub use story_ops::{ pub use story_ops::{
add_criterion_to_file, check_criterion_in_file, create_story_file, update_story_in_file, add_criterion_to_file, check_criterion_in_file, create_story_file, edit_criterion_in_file,
update_story_in_file,
}; };
pub use test_results::{ pub use test_results::{
read_test_results_from_story_file, write_coverage_baseline_to_story_file, read_test_results_from_story_file, write_coverage_baseline_to_story_file,
+58
View File
@@ -126,6 +126,64 @@ pub fn check_criterion_in_file(
Ok(()) Ok(())
} }
/// Edit the text of an existing acceptance criterion without changing its checked state.
///
/// Finds the criterion at `criterion_index` (0-based, counting all criteria regardless
/// of checked state) and replaces its text with `new_text`.
pub fn edit_criterion_in_file(
project_root: &Path,
story_id: &str,
criterion_index: usize,
new_text: &str,
) -> Result<(), String> {
let contents = read_story_content(project_root, story_id)?;
let mut count: usize = 0;
let mut found = false;
let new_lines: Vec<String> = contents
.lines()
.map(|line| {
let trimmed = line.trim();
let prefix = if trimmed.starts_with("- [ ] ") {
Some("- [ ] ")
} else if trimmed.starts_with("- [x] ") {
Some("- [x] ")
} else {
None
};
if let Some(p) = prefix {
if count == criterion_index {
count += 1;
found = true;
let indent_len = line.len() - trimmed.len();
let indent = &line[..indent_len];
return format!("{indent}{p}{new_text}");
}
count += 1;
}
line.to_string()
})
.collect();
if !found {
return Err(format!(
"Criterion index {criterion_index} out of range. Story '{story_id}' has \
{count} criteria (indices 0..{}).",
count.saturating_sub(1)
));
}
let mut new_str = new_lines.join("\n");
if contents.ends_with('\n') {
new_str.push('\n');
}
let stage = story_stage(story_id).unwrap_or_else(|| "2_current".to_string());
write_story_content(project_root, story_id, &stage, &new_str);
Ok(())
}
/// Add a new acceptance criterion to a story. /// Add a new acceptance criterion to a story.
/// ///
/// Appends `- [ ] {criterion}` after the last existing criterion line in the /// Appends `- [ ] {criterion}` after the last existing criterion line in the