huskies: merge 535_bug_chat_status_number_and_mcp_tool_status_still_read_from_filesystem_broken_after_530

This commit is contained in:
dave
2026-04-10 19:01:31 +00:00
parent bc2b1e244c
commit 40893a8cb1
2 changed files with 105 additions and 60 deletions
+83 -50
View File
@@ -1,8 +1,12 @@
//! Handler for the story triage dump subcommand of `status`. //! Handler for the story triage dump subcommand of `status`.
//! //!
//! Produces a triage dump for a story that is currently in-progress //! Produces a triage dump for a story: metadata, acceptance criteria,
//! (`work/2_current/`): metadata, acceptance criteria, worktree/branch state, //! worktree/branch state, git diff, recent commits, and the tail of the
//! git diff, recent commits, and the tail of the agent log. //! agent log.
//!
//! Reads from the CRDT pipeline state and the in-memory content store — no
//! filesystem access for story content. Works for stories in any pipeline
//! stage, not just `2_current`.
//! //!
//! The command is handled entirely at the bot level — no LLM invocation. //! The command is handled entirely at the bot level — no LLM invocation.
@@ -26,39 +30,31 @@ pub(super) fn handle_triage(ctx: &CommandContext) -> Option<String> {
)); ));
} }
let current_dir = ctx match find_story_by_number(num_str) {
.project_root Some((story_id, item)) => Some(build_triage_dump(ctx, &story_id, &item, num_str)),
.join(".huskies")
.join("work")
.join("2_current");
match find_story_in_dir(&current_dir, num_str) {
Some((path, stem)) => Some(build_triage_dump(ctx, &path, &stem, num_str)),
None => Some(format!( None => Some(format!(
"Story **{num_str}** is not currently in progress (not found in `work/2_current/`)." "Story **{num_str}** not found in the pipeline."
)), )),
} }
} }
/// Find a `.md` file whose numeric prefix matches `num_str` in `dir`. /// Find a pipeline item whose numeric prefix matches `num_str` by querying the
/// /// CRDT state. Returns `(story_id, PipelineItem)` for the first match.
/// Returns `(path, file_stem)` for the first match. fn find_story_by_number(
fn find_story_in_dir(dir: &Path, num_str: &str) -> Option<(PathBuf, String)> { num_str: &str,
let entries = std::fs::read_dir(dir).ok()?; ) -> Option<(String, crate::pipeline_state::PipelineItem)> {
for entry in entries.flatten() { let items = crate::pipeline_state::read_all_typed();
let path = entry.path(); for item in items {
if path.extension().and_then(|e| e.to_str()) != Some("md") { let file_num = item
continue; .story_id
} .0
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) { .split('_')
let file_num = stem .next()
.split('_') .filter(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()))
.next() .unwrap_or("");
.filter(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit())) if file_num == num_str {
.unwrap_or(""); let story_id = item.story_id.0.clone();
if file_num == num_str { return Some((story_id, item));
return Some((path.clone(), stem.to_string()));
}
} }
} }
None None
@@ -67,13 +63,13 @@ fn find_story_in_dir(dir: &Path, num_str: &str) -> Option<(PathBuf, String)> {
/// Build the full triage dump for a story. /// Build the full triage dump for a story.
fn build_triage_dump( fn build_triage_dump(
ctx: &CommandContext, ctx: &CommandContext,
story_path: &Path,
story_id: &str, story_id: &str,
item: &crate::pipeline_state::PipelineItem,
num_str: &str, num_str: &str,
) -> String { ) -> String {
let contents = match std::fs::read_to_string(story_path) { let contents = match crate::db::read_content(story_id) {
Ok(c) => c, Some(c) => c,
Err(e) => return format!("Failed to read story {num_str}: {e}"), None => return format!("Story {num_str}: content not found in content store."),
}; };
let meta = crate::io::story_metadata::parse_front_matter(&contents).ok(); let meta = crate::io::story_metadata::parse_front_matter(&contents).ok();
@@ -83,7 +79,9 @@ fn build_triage_dump(
// ---- Header ---- // ---- Header ----
out.push_str(&format!("## Story {num_str}{name}\n")); out.push_str(&format!("## Story {num_str}{name}\n"));
out.push_str("**Stage:** In Progress (`2_current`)\n\n"); let stage_name = crate::pipeline_state::stage_label(&item.stage);
let dir_name = crate::pipeline_state::stage_dir_name(&item.stage);
out.push_str(&format!("**Stage:** {stage_name} (`{dir_name}`)\n\n"));
// ---- Front matter fields ---- // ---- Front matter fields ----
if let Some(ref m) = meta { if let Some(ref m) = meta {
@@ -351,27 +349,24 @@ mod tests {
// -- not found ---------------------------------------------------------- // -- not found ----------------------------------------------------------
#[test] #[test]
fn whatsup_story_not_in_current_returns_friendly_message() { fn whatsup_story_not_in_pipeline_returns_friendly_message() {
// Initialize the content store so read_all_typed() returns nothing for
// this number without panicking.
crate::db::ensure_content_store();
let tmp = tempfile::TempDir::new().unwrap(); let tmp = tempfile::TempDir::new().unwrap();
// Create the directory but put the story in backlog, not current // Use a number unlikely to collide with other tests' CRDT entries.
write_story_file( let output = status_triage_cmd(tmp.path(), "99997").unwrap();
tmp.path(),
"1_backlog",
"42_story_not_in_current.md",
"---\nname: Not in current\n---\n",
);
let output = status_triage_cmd(tmp.path(), "42").unwrap();
assert!( assert!(
output.contains("42"), output.contains("99997"),
"message should include story number: {output}" "message should include story number: {output}"
); );
assert!( assert!(
output.contains("not") || output.contains("Not"), output.contains("not found") || output.contains("Not found"),
"message should say not found/in progress: {output}" "message should say not found: {output}"
); );
} }
// -- found in 2_current ------------------------------------------------- // -- found in any pipeline stage ----------------------------------------
#[test] #[test]
fn whatsup_shows_story_name_and_stage() { fn whatsup_shows_story_name_and_stage() {
@@ -389,11 +384,49 @@ mod tests {
"should show story name: {output}" "should show story name: {output}"
); );
assert!( assert!(
output.contains("In Progress") || output.contains("2_current"), output.contains("Coding") || output.contains("2_current"),
"should show pipeline stage: {output}" "should show pipeline stage: {output}"
); );
} }
#[test]
fn whatsup_works_for_story_in_backlog() {
let tmp = tempfile::TempDir::new().unwrap();
write_story_file(
tmp.path(),
"1_backlog",
"9901_story_backlog_item.md",
"---\nname: Backlog Item\n---\n",
);
let output = status_triage_cmd(tmp.path(), "9901").unwrap();
assert!(output.contains("9901"), "should show story number: {output}");
assert!(
output.contains("Backlog Item"),
"should show story name: {output}"
);
assert!(
output.contains("Backlog") || output.contains("1_backlog"),
"should show backlog stage: {output}"
);
}
#[test]
fn whatsup_works_for_story_in_qa() {
let tmp = tempfile::TempDir::new().unwrap();
write_story_file(
tmp.path(),
"3_qa",
"9902_story_qa_item.md",
"---\nname: QA Item\n---\n",
);
let output = status_triage_cmd(tmp.path(), "9902").unwrap();
assert!(output.contains("9902"), "should show story number: {output}");
assert!(
output.contains("QA Item"),
"should show story name: {output}"
);
}
#[test] #[test]
fn whatsup_shows_acceptance_criteria() { fn whatsup_shows_acceptance_criteria() {
let tmp = tempfile::TempDir::new().unwrap(); let tmp = tempfile::TempDir::new().unwrap();
+22 -10
View File
@@ -158,19 +158,13 @@ pub(super) async fn tool_status(args: &Value, ctx: &AppContext) -> Result<String
let root = ctx.state.get_project_root()?; let root = ctx.state.get_project_root()?;
// Read from CRDT/DB content store — verify the item is in 2_current. // Read from CRDT/DB content store — works for stories in any pipeline stage.
let typed_item = crate::pipeline_state::read_typed(story_id) let _typed_item = crate::pipeline_state::read_typed(story_id)
.map_err(|e| format!("Failed to read pipeline state: {e}"))? .map_err(|e| format!("Failed to read pipeline state: {e}"))?
.ok_or_else(|| format!( .ok_or_else(|| format!(
"Story '{story_id}' not found in work/2_current/. Check the story_id and ensure it is in the current stage." "Story '{story_id}' not found in the pipeline."
))?; ))?;
if typed_item.stage.dir_name() != "2_current" {
return Err(format!(
"Story '{story_id}' not found in work/2_current/. Check the story_id and ensure it is in the current stage."
));
}
let contents = crate::db::read_content(story_id).ok_or_else(|| { let contents = crate::db::read_content(story_id).ok_or_else(|| {
format!("Story '{story_id}' has no content in the content store.") format!("Story '{story_id}' has no content in the content store.")
})?; })?;
@@ -334,7 +328,7 @@ mod tests {
let ctx = crate::http::context::AppContext::new_test(tmp.path().to_path_buf()); let ctx = crate::http::context::AppContext::new_test(tmp.path().to_path_buf());
let result = tool_status(&json!({"story_id": "999_story_nonexistent"}), &ctx).await; let result = tool_status(&json!({"story_id": "999_story_nonexistent"}), &ctx).await;
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().contains("not found in work/2_current/")); assert!(result.unwrap_err().contains("not found in the pipeline"));
} }
#[tokio::test] #[tokio::test]
@@ -362,4 +356,22 @@ mod tests {
assert_eq!(ac[1]["text"], "Second criterion"); assert_eq!(ac[1]["text"], "Second criterion");
assert_eq!(ac[1]["checked"], true); assert_eq!(ac[1]["checked"], true);
} }
#[tokio::test]
async fn tool_status_works_for_story_in_backlog() {
let tmp = tempdir().unwrap();
crate::db::ensure_content_store();
let story_content = "---\nname: Backlog Story\n---\n\n## Acceptance Criteria\n\n- [ ] One thing\n";
crate::db::write_item_with_content("9887_story_backlog_test", "1_backlog", story_content);
let ctx = crate::http::context::AppContext::new_test(tmp.path().to_path_buf());
let result = tool_status(&json!({"story_id": "9887_story_backlog_test"}), &ctx)
.await
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["story_id"], "9887_story_backlog_test");
assert_eq!(parsed["front_matter"]["name"], "Backlog Story");
}
} }