diff --git a/Cargo.lock b/Cargo.lock index 19ea449a..a1386811 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -509,7 +509,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cli-sub-agent" -version = "0.1.1090" +version = "0.1.1091" dependencies = [ "anyhow", "chrono", @@ -704,7 +704,7 @@ dependencies = [ [[package]] name = "csa-acp" -version = "0.1.1090" +version = "0.1.1091" dependencies = [ "agent-client-protocol", "anyhow", @@ -725,7 +725,7 @@ dependencies = [ [[package]] name = "csa-config" -version = "0.1.1090" +version = "0.1.1091" dependencies = [ "anyhow", "chrono", @@ -743,7 +743,7 @@ dependencies = [ [[package]] name = "csa-core" -version = "0.1.1090" +version = "0.1.1091" dependencies = [ "agent-teams", "chrono", @@ -761,7 +761,7 @@ dependencies = [ [[package]] name = "csa-eval" -version = "0.1.1090" +version = "0.1.1091" dependencies = [ "anyhow", "chrono", @@ -775,7 +775,7 @@ dependencies = [ [[package]] name = "csa-executor" -version = "0.1.1090" +version = "0.1.1091" dependencies = [ "agent-teams", "anyhow", @@ -804,7 +804,7 @@ dependencies = [ [[package]] name = "csa-hooks" -version = "0.1.1090" +version = "0.1.1091" dependencies = [ "anyhow", "chrono", @@ -823,7 +823,7 @@ dependencies = [ [[package]] name = "csa-lock" -version = "0.1.1090" +version = "0.1.1091" dependencies = [ "anyhow", "chrono", @@ -838,7 +838,7 @@ dependencies = [ [[package]] name = "csa-mcp-hub" -version = "0.1.1090" +version = "0.1.1091" dependencies = [ "anyhow", "axum", @@ -861,7 +861,7 @@ dependencies = [ [[package]] name = "csa-memory" -version = "0.1.1090" +version = "0.1.1091" dependencies = [ "anyhow", "async-trait", @@ -880,7 +880,7 @@ dependencies = [ [[package]] name = "csa-process" -version = "0.1.1090" +version = "0.1.1091" dependencies = [ "anyhow", "chrono", @@ -899,7 +899,7 @@ dependencies = [ [[package]] name = "csa-resource" -version = "0.1.1090" +version = "0.1.1091" dependencies = [ "anyhow", "csa-core", @@ -915,7 +915,7 @@ dependencies = [ [[package]] name = "csa-scheduler" -version = "0.1.1090" +version = "0.1.1091" dependencies = [ "anyhow", "chrono", @@ -933,7 +933,7 @@ dependencies = [ [[package]] name = "csa-session" -version = "0.1.1090" +version = "0.1.1091" dependencies = [ "anyhow", "chrono", @@ -959,7 +959,7 @@ dependencies = [ [[package]] name = "csa-todo" -version = "0.1.1090" +version = "0.1.1091" dependencies = [ "anyhow", "chrono", @@ -4562,7 +4562,7 @@ dependencies = [ [[package]] name = "weave" -version = "0.1.1090" +version = "0.1.1091" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 709df4be..91e8575b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ default-members = [ resolver = "2" [workspace.package] -version = "0.1.1090" +version = "0.1.1091" edition = "2024" rust-version = "1.88" license = "Apache-2.0" diff --git a/crates/cli-sub-agent/src/review_cmd_prose_findings.rs b/crates/cli-sub-agent/src/review_cmd_prose_findings.rs index 98072b6e..01a0aa5b 100644 --- a/crates/cli-sub-agent/src/review_cmd_prose_findings.rs +++ b/crates/cli-sub-agent/src/review_cmd_prose_findings.rs @@ -333,11 +333,6 @@ fn parse_severity_prefixed_finding( allow_description_only: bool, ) -> Option { let (label, rest) = body.split_once(':')?; - // Reject compound priority specs like "P1/P2/P3" — these are acceptance-criteria - // descriptions, not single severity labels. - if label.contains('/') { - return None; - } let severity = severity_from_label(label).or_else(|| leading_severity_from_title(label))?; let rest = rest.trim(); if severity_prefixed_rest_is_zero_count(rest) { @@ -393,16 +388,24 @@ fn strip_unordered_list_prefix(line: &str) -> &str { } fn leading_severity_from_title(title: &str) -> Option { - // Reject compound specs like "P1/P2/P3" — the first alphanumeric run - // (e.g., "P1") is part of a compound, not a standalone severity label. - if title.contains('/') { + let mut segments = title.split('/'); + let severity = leading_severity_from_title_segment(segments.next()?)?; + + // Reject compound specs like "P1/P2/P3" and + // "High-severity/Medium-severity", while allowing descriptive segments + // such as "High correctness / sandbox violation". + if segments.any(|segment| leading_severity_from_title_segment(segment).is_some()) { return None; } - let first_word = title - .trim_start() + + Some(severity) +} + +fn leading_severity_from_title_segment(segment: &str) -> Option { + segment .split(|ch: char| !ch.is_ascii_alphanumeric()) - .find(|word| !word.is_empty())?; - severity_from_label(first_word) + .find(|word| !word.is_empty()) + .and_then(severity_from_label) } fn severity_prefixed_description(label: &str, rest: &str) -> String { diff --git a/crates/cli-sub-agent/src/review_cmd_prose_findings_tests.rs b/crates/cli-sub-agent/src/review_cmd_prose_findings_tests.rs index 68a8a18c..56caef67 100644 --- a/crates/cli-sub-agent/src/review_cmd_prose_findings_tests.rs +++ b/crates/cli-sub-agent/src/review_cmd_prose_findings_tests.rs @@ -262,3 +262,54 @@ fn issue_2637_compound_priority_not_blocking_signal() { "P1/P2/P3: code paths now classify status into bounded cause labels." )); } + +#[test] +fn issue_2652_title_leading_severities_allow_slashes_in_descriptions() { + for (title, expected_severity) in [ + ("Critical security / isolation failure", Severity::Critical), + ("High correctness / sandbox violation", Severity::High), + ("Medium regression / test gap", Severity::Medium), + ("Low docs / help mismatch", Severity::Low), + ("Info docs / help mismatch", Severity::Low), + ("P0 security / isolation failure", Severity::Critical), + ("P1 correctness / lock race", Severity::High), + ("P2 regression / test gap", Severity::Medium), + ("P3 docs / help mismatch", Severity::Low), + ] { + let text = format!("## Findings\n1. {title}: active problem remains\n"); + let findings = extract_review_findings_from_prose(&text); + assert_eq!(findings.len(), 1, "title should parse: {title}"); + assert_eq!(findings[0].severity, expected_severity, "title: {title}"); + } +} + +#[test] +fn issue_2652_decorated_title_leading_severities_still_parse() { + for (title, expected_severity) in [ + ("**High correctness / sandbox violation**", Severity::High), + ("`P1` correctness / lock race", Severity::High), + ("_Medium_ regression / test gap", Severity::Medium), + ] { + let text = format!("## Findings\n1. {title}: active problem remains\n"); + let findings = extract_review_findings_from_prose(&text); + assert_eq!(findings.len(), 1, "decorated title should parse: {title}"); + assert_eq!(findings[0].severity, expected_severity, "title: {title}"); + } +} + +#[test] +fn issue_2652_compound_severity_prefix_stays_rejected() { + for title in [ + "High/Medium", + "P1/P2/P3", + "High-severity/Medium-severity", + "**High** / **Medium**", + ] { + let text = format!("## Findings\n{title}: acceptance criteria summary\n"); + let findings = extract_review_findings_from_prose(&text); + assert!( + findings.is_empty(), + "compound severity prefix should not parse: {title}" + ); + } +} diff --git a/weave.lock b/weave.lock index bc69c310..6e861565 100644 --- a/weave.lock +++ b/weave.lock @@ -1,6 +1,6 @@ [versions] -csa = "0.1.1090" -weave = "0.1.1090" +csa = "0.1.1091" +weave = "0.1.1091" last_migrated_at = "2026-03-08T12:08:01.820964091Z" [migrations]