From 800d9925a5ec561cdc8a5226795b66658aba114b Mon Sep 17 00:00:00 2001 From: damocles Date: Wed, 27 May 2026 23:40:13 +0200 Subject: [PATCH] prompt: matched-close check + drop dead-code guard (argus #527 nits) --- hive-ag3nt/src/prompt.rs | 53 +++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/hive-ag3nt/src/prompt.rs b/hive-ag3nt/src/prompt.rs index 6a6416e2..c82b343f 100644 --- a/hive-ag3nt/src/prompt.rs +++ b/hive-ag3nt/src/prompt.rs @@ -52,7 +52,11 @@ pub fn render(flavor: Flavor, label: &str, operator_pronouns: &str) -> String { /// Walk `template` line-by-line. Inside a `` block, /// suppress all lines unless `X == target`. Marker lines themselves are /// always elided from the output. Unbalanced openers (no matching -/// closer) hold the suppression state until end-of-file. +/// closer) hold the suppression state until end-of-file. A mismatched +/// closer (`` inside a `role:agent` block) is +/// elided from the output but does NOT reset the active role — keeps +/// the suppression conservative so a typo can't dump wrong-flavor +/// content (per argus #527 review nit). fn filter_role_blocks(template: &str, target: &str) -> String { let mut out = String::with_capacity(template.len()); // None = outside any block; Some(role) = inside role-tagged block. @@ -63,8 +67,12 @@ fn filter_role_blocks(template: &str, target: &str) -> String { active_role = Some(role); continue; } - if parse_close_marker(trimmed).is_some() { - active_role = None; + if let Some(close_role) = parse_close_marker(trimmed) { + if active_role == Some(close_role) { + active_role = None; + } + // Mismatched close: elide the marker line but keep the + // active role intact so wrong-flavor content stays gated. continue; } let include = match active_role { @@ -82,15 +90,12 @@ fn filter_role_blocks(template: &str, target: &str) -> String { /// `` → `Some("agent")`. Anything else returns /// None. Whitespace inside the marker is tolerated so a future /// author's `` (no spaces) still parses; the dashboard -/// markdown renderer is equally lenient. +/// markdown renderer is equally lenient. Close tags (`/role:...`) +/// can't accidentally match — the `strip_prefix("role:")` rejects +/// the leading slash before we'd ever see it. fn parse_open_marker(line: &str) -> Option<&str> { let inside = line.strip_prefix("")?.trim(); let role = inside.strip_prefix("role:")?.trim(); - // Reject closer-looking content ("/role:..." would start with "/") - // so `/role:agent` doesn't accidentally match here. - if role.starts_with('/') { - return None; - } Some(role) } @@ -182,6 +187,36 @@ shared closer assert_eq!(parse_close_marker("just text"), None); } + #[test] + fn mismatched_close_keeps_active_role() { + // `` block with a stray `` + // closer inside: the manager-tagged close must NOT pop the agent + // gate, else manager-target output would leak the agent block's + // text (or vice-versa). Stray marker line itself is still elided. + // Per argus #527 review nit. + let template = "shared\n\ + \n\ + agent line 1\n\ + \n\ + agent line 2\n\ + \n\ + shared end\n"; + let manager = filter_role_blocks(template, "manager"); + // Both agent lines stay gated out for the manager target; the + // stray close didn't accidentally pop the role. Stray marker + // itself elided from the output. + assert!(!manager.contains("agent line 1")); + assert!(!manager.contains("agent line 2")); + assert!(!manager.contains("