diff --git a/docs/turn-loop.md b/docs/turn-loop.md index a298ecd1..2c28d7ce 100644 --- a/docs/turn-loop.md +++ b/docs/turn-loop.md @@ -187,6 +187,26 @@ socket at `/run/hive/` once at startup: `services.hyperhive.c0re.operatorPronouns`, default `she/her`). Passed via `--system-prompt-file`. + **Marker grammar.** `` opens a block; matching + `` closes it. Nesting is NOT supported — a stray + opener overrides until its closing tag (or end of file). A + mismatched closer (`` inside a `role:agent` + block) is elided from the output but does NOT pop the active + role: suppression stays conservative so a typo can't dump + wrong-flavor content. Whitespace inside markers is tolerated + (`` parses the same as ``). + Content outside any marker is always shared. + + **`hive_identity` / `swarm_identity` shape.** Each carries a + leading space + backticked name (` on hive \`pr1ma\``, + ` in swarm \`constellat1on\``) when the corresponding env var + is set, otherwise empty string. The independence lets the + template drop one or both into the opener prose without + breaking single-hive deployments that never set the option; + the renderer also treats `Some("")` from a caller as `None` so + empty-string env vars and missing env vars round-trip the + same way. + The shared per-turn plumbing lives in `hive_ag3nt::turn::{write_mcp_config, write_settings, write_system_prompt, run_turn, drive_turn, emit_turn_end, wait_for_login, compact_session}` so the two binaries diff --git a/hive-ag3nt/src/plugins.rs b/hive-ag3nt/src/plugins.rs index 54d505d9..f5b21765 100644 --- a/hive-ag3nt/src/plugins.rs +++ b/hive-ag3nt/src/plugins.rs @@ -100,12 +100,9 @@ async fn update_marketplaces() { /// Install every plugin in `/etc/hyperhive/claude-plugins.json`. /// Returns a list of human-readable failure messages so the caller can /// route them through their own per-role surface (turn-failure-style -/// notification, see `Surface::send_to_parent`). Pre-#692 this function -/// hardcoded `"manager"` as the failure-notification recipient via a -/// `notify_recipient: Option<&str>` arg; mara on #778 wanted the -/// manager-name special case gone. Now plugins.rs is wire-agnostic and -/// the caller picks the recipient via the same `` sentinel -/// failure-notify uses everywhere else (#703). +/// notification, see `Surface::send_to_parent`). Wire-agnostic: the +/// caller picks the recipient via the same `` sentinel that +/// failure-notify uses everywhere else. pub async fn install_configured(socket: &Path) -> Vec { let _ = socket; // Reserved for future telemetry; currently unused. let Ok(raw) = tokio::fs::read_to_string(PLUGINS_PATH).await else { diff --git a/hive-ag3nt/src/prompt.rs b/hive-ag3nt/src/prompt.rs index 08b0ffbd..ac051899 100644 --- a/hive-ag3nt/src/prompt.rs +++ b/hive-ag3nt/src/prompt.rs @@ -1,31 +1,8 @@ -//! System-prompt renderer (closes #519). -//! -//! Both flavors (agent / manager) used to live in separate files -//! (`prompts/agent.md`, `prompts/manager.md`) that drifted in lockstep -//! whenever someone updated only one. Now there's a single -//! `prompts/system.md` with HTML-comment markers gating role-specific -//! blocks; this module assembles the final prompt for a given flavor. -//! -//! Marker syntax (HTML comments — invisible in rendered markdown, -//! distinct from `{label}` / `{operator_pronouns}` placeholders): -//! -//! ```text -//! -//! sub-agent-only paragraph -//! -//! -//! shared paragraph -//! -//! -//! manager-only paragraph -//! -//! ``` -//! -//! Content outside any marker is shared. Nesting is NOT supported; -//! a stray opener overrides until its closing tag (or end of file). -//! When #513 lands the marker grammar can grow `cap:` blocks -//! the same way without touching the renderer surface (the role -//! distinction folds into a per-cap-set lookup). +//! System-prompt renderer. Single `prompts/system.md` with +//! HTML-comment markers gating role-specific blocks; this module +//! assembles the final prompt for a given flavor. Marker grammar + +//! placeholder substitution rules in +//! `docs/turn-loop.md::On-boot files` (`claude-system-prompt.md`). use std::path::{Path, PathBuf}; @@ -34,31 +11,14 @@ use anyhow::{Context, Result}; use crate::mcp::Flavor; /// Assemble the system prompt for a given flavor + label + pronouns + -/// optional hive / swarm display names. -/// Pure function — no I/O. Splits out from [`write_system_prompt`] so -/// the marker logic + substitution is unit-testable in isolation. -/// The caller supplies the template body so tests can pass an inline -/// fixture and production reads it once at harness startup via -/// [`hive_sh4re::assets::prompt_template`] (`$HIVE_ASSETS_DIR/prompts/ -/// system.md`). -/// -/// Substitutions: -/// -/// - `{label}` — short hive-local name (e.g. `iris`). -/// - `{qualified_label}` (#589) — `${label}@${domain}` in federated -/// deployments, same as `{label}` when no hive domain is configured. -/// - `{operator_pronouns}` — `"she/her"` / `"they/them"` / etc. -/// - `{hive_identity}` (#701 / #709) — ` on hive \`pr1ma\`` (with -/// leading space + backticks) when `hive_name` is `Some`, **empty -/// string** when `None`. Lets the template drop the names into the -/// opener prose without breaking single-hive deployments that -/// never set the option. -/// - `{swarm_identity}` (#701 / #709) — same shape for the swarm: -/// ` in swarm \`constellat1on\`` when `Some`, empty when `None`. -/// -/// Both `hive_name` / `swarm_name` are independent: setting just one -/// surfaces only that clause; setting both yields the full prose -/// (`… on hive \`pr1ma\` in swarm \`constellat1on\` …`). +/// optional hive / swarm display names. Pure function — no I/O. Splits +/// out from [`write_system_prompt`] so the marker logic + substitution +/// is unit-testable in isolation. The caller supplies the template body +/// so tests can pass an inline fixture and production reads it once at +/// harness startup via [`hive_sh4re::assets::prompt_template`] +/// (`$HIVE_ASSETS_DIR/prompts/system.md`). Substitution placeholders + +/// marker grammar documented in +/// `docs/turn-loop.md::On-boot files` (`claude-system-prompt.md`). #[must_use] pub fn render( template: &str, @@ -94,7 +54,7 @@ pub fn render( /// 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). +/// content. 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. @@ -166,9 +126,9 @@ pub async fn write_system_prompt(_socket: &Path, label: &str, flavor: Flavor) -> template_path.display() ) })?; - // #701 / #709: surface hive + swarm display names in the prompt - // opener when configured. Both `None` falls back to the pre-#709 - // wording verbatim (single-hive deployments see no diff). + // Surface hive + swarm display names in the prompt opener when + // configured. Both `None` falls back to the non-identity wording + // verbatim (single-hive deployments see no diff). let hive_name = crate::identity::hive_name(); let swarm_name = crate::identity::swarm_name(); let body = render( @@ -190,11 +150,11 @@ mod tests { use super::*; use std::sync::LazyLock; - // #555: the production template lives at - // `$HIVE_ASSETS_DIR/prompts/system.md` and is loaded at runtime. - // The unit tests below want to assert against the actual production - // wording (so the renderer + tool surface stay honest), so they - // resolve the same path at test runtime via two fallbacks: + // The production template lives at `$HIVE_ASSETS_DIR/prompts/system.md` + // and is loaded at runtime. The unit tests below want to assert + // against the actual production wording (so the renderer + tool + // surface stay honest), so they resolve the same path at test + // runtime via two fallbacks: // 1. `$HIVE_ASSETS_DIR/prompts/system.md` — the runtime contract // production uses. The flake's `checks.cargo-test` derivation // sets this to the `hyperhive-assets` output so `cargo test` @@ -278,7 +238,6 @@ shared closer // 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\ @@ -337,7 +296,7 @@ shared closer // Spot-check: the manager-only tool block (request_init_config, // kill, schedule_*) MUST NOT appear in the agent's rendered // prompt. Drift between flavor and tool surface bites every - // time it happens (cf. #511 missing-allow-list bug). + // time it happens. let rendered = render( &PRODUCTION_TEMPLATE, Flavor::Agent, @@ -395,14 +354,15 @@ shared closer assert!(manager.starts_with("You are the hyperhive manager")); } - // Inline fixture for the #709 placeholders. Cargo's `cargo test` - // resolves `PRODUCTION_TEMPLATE` against `$HIVE_ASSETS_DIR/prompts/ - // system.md`, which the flake builds at derivation time — a fresh - // placeholder added on the source side isn't in the shipped asset - // until the flake rebuilds, so PRODUCTION_TEMPLATE can't be the - // fixture here. The string below carries just enough of the - // opener shape to exercise the substitution logic; nothing here - // depends on the production template's flavor markers. + // Inline fixture for the {hive_identity} / {swarm_identity} + // placeholders. Cargo's `cargo test` resolves `PRODUCTION_TEMPLATE` + // against `$HIVE_ASSETS_DIR/prompts/system.md`, which the flake + // builds at derivation time — a fresh placeholder added on the + // source side isn't in the shipped asset until the flake rebuilds, + // so PRODUCTION_TEMPLATE can't be the fixture here. The string + // below carries just enough of the opener shape to exercise the + // substitution logic; nothing here depends on the production + // template's flavor markers. const IDENTITY_FIXTURE: &str = "\ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity}{swarm_identity} in a multi-agent system. Pronouns: **{operator_pronouns}**. "; @@ -456,8 +416,8 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity #[test] fn render_omits_identity_when_unset() { - // None / None must round-trip the pre-#709 opener verbatim — - // single-hive deployments see zero diff. + // None / None must round-trip the non-identity opener verbatim + // — single-hive deployments see zero diff. let rendered = render( IDENTITY_FIXTURE, Flavor::Agent,