diff --git a/hive-ag3nt/prompts/agent.md b/hive-ag3nt/prompts/agent.md index 66b0243..4647e4e 100644 --- a/hive-ag3nt/prompts/agent.md +++ b/hive-ag3nt/prompts/agent.md @@ -1,4 +1,4 @@ -You are hyperhive agent `{label}` in a multi-agent system. +You are hyperhive agent `{label}` in a multi-agent system. The operator (recipient `operator` in `send`, the human at the dashboard) uses **{operator_pronouns}** pronouns — use them naturally when you refer to them in third person (e.g. when relaying to a peer or the manager). Tools (hyperhive surface): diff --git a/hive-ag3nt/prompts/manager.md b/hive-ag3nt/prompts/manager.md index 123b959..74e774c 100644 --- a/hive-ag3nt/prompts/manager.md +++ b/hive-ag3nt/prompts/manager.md @@ -1,4 +1,4 @@ -You are the hyperhive manager `{label}` in a multi-agent system. You coordinate sub-agents and relay between them and the operator. +You are the hyperhive manager `{label}` in a multi-agent system. You coordinate sub-agents and relay between them and the operator. The operator (recipient `operator`, the human at the dashboard) uses **{operator_pronouns}** pronouns — use them naturally when you refer to them in third person. Tools (hyperhive surface): diff --git a/hive-ag3nt/src/turn.rs b/hive-ag3nt/src/turn.rs index d85ef8c..6b1ee0c 100644 --- a/hive-ag3nt/src/turn.rs +++ b/hive-ag3nt/src/turn.rs @@ -107,7 +107,11 @@ pub async fn write_system_prompt( mcp::Flavor::Agent => include_str!("../prompts/agent.md"), mcp::Flavor::Manager => include_str!("../prompts/manager.md"), }; - let body = template.replace("{label}", label); + let pronouns = + std::env::var("HIVE_OPERATOR_PRONOUNS").unwrap_or_else(|_| "she/her".to_owned()); + let body = template + .replace("{label}", label) + .replace("{operator_pronouns}", &pronouns); let path = parent.join("claude-system-prompt.md"); tokio::fs::write(&path, body).await?; tracing::info!(path = %path.display(), "wrote claude system prompt"); diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index 4596743..c7cc5f4 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -69,6 +69,7 @@ pub async fn approve(coord: Arc, id: i64) -> Result<()> { &claude_dir, ¬es_dir, coord_bg.dashboard_port, + &coord_bg.operator_pronouns, ) .await; coord_bg.clear_transient(&agent_bg); @@ -337,7 +338,13 @@ pub async fn destroy(coord: &Coordinator, name: &str, purge: bool) -> Result<()> /// destroy). Idempotent — a no-op when nothing changed. async fn sync_meta_after_lifecycle(coord: &Coordinator) -> Result<()> { let agents = lifecycle::agents_for_meta_listing().await?; - crate::meta::sync_agents(&coord.hyperhive_flake, coord.dashboard_port, &agents).await + crate::meta::sync_agents( + &coord.hyperhive_flake, + coord.dashboard_port, + &coord.operator_pronouns, + &agents, + ) + .await } pub async fn deny(coord: &Coordinator, id: i64, note: Option<&str>) -> Result<()> { diff --git a/hive-c0re/src/auto_update.rs b/hive-c0re/src/auto_update.rs index 537da80..02c6126 100644 --- a/hive-c0re/src/auto_update.rs +++ b/hive-c0re/src/auto_update.rs @@ -72,6 +72,7 @@ pub async fn rebuild_agent(coord: &Arc, name: &str, current_rev: &s &claude_dir, ¬es_dir, coord.dashboard_port, + &coord.operator_pronouns, ) .await; coord.clear_transient(name); @@ -144,6 +145,7 @@ pub async fn ensure_manager(coord: &Arc) -> Result<()> { &claude_dir, ¬es_dir, coord.dashboard_port, + &coord.operator_pronouns, ) .await?; if let Some(rev) = current_rev { diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index 96b2d75..ad151a8 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -35,6 +35,13 @@ pub struct Coordinator { /// each per-agent flake so the agent's web UI can build the right /// rebuild-button URL pointing back at the dashboard. pub dashboard_port: u16, + /// Operator pronouns (free text) — `she/her` by default, set via + /// the NixOS module option `services.hive-c0re.operatorPronouns`. + /// Reaches each container as the `HIVE_OPERATOR_PRONOUNS` env var + /// (injected into systemd.services..environment by the + /// meta flake); the harness substitutes it into the agent / + /// manager system prompt at boot. + pub operator_pronouns: String, agents: Mutex>, /// Agents whose lifecycle action (currently just spawn) is in flight. /// Read by the dashboard to render a spinner; cleared when the action @@ -67,7 +74,12 @@ pub enum TransientKind { } impl Coordinator { - pub fn open(db_path: &Path, hyperhive_flake: String, dashboard_port: u16) -> Result { + pub fn open( + db_path: &Path, + hyperhive_flake: String, + dashboard_port: u16, + operator_pronouns: String, + ) -> Result { let broker = Broker::open(db_path).context("open broker")?; let approvals = Approvals::open(db_path).context("open approvals")?; let questions = OperatorQuestions::open(db_path).context("open operator_questions")?; @@ -77,6 +89,7 @@ impl Coordinator { questions: Arc::new(questions), hyperhive_flake, dashboard_port, + operator_pronouns, agents: Mutex::new(HashMap::new()), transient: Mutex::new(HashMap::new()), }) diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index f3e72da..96a1bd9 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -132,6 +132,7 @@ pub async fn spawn( claude_dir: &Path, notes_dir: &Path, dashboard_port: u16, + operator_pronouns: &str, ) -> Result<()> { validate(name)?; if let Some(other) = port_collision(name).await { @@ -148,7 +149,7 @@ pub async fn spawn( // before `nixos-container create` so the `--flake meta#` // ref resolves. let agents = agents_after_spawn(name).await?; - crate::meta::sync_agents(hyperhive_flake, dashboard_port, &agents).await?; + crate::meta::sync_agents(hyperhive_flake, dashboard_port, operator_pronouns, &agents).await?; let container = container_name(name); let flake_ref = format!("{}#{name}", crate::meta::meta_dir().display()); run(&["create", &container, "--flake", &flake_ref]).await?; @@ -257,6 +258,7 @@ pub async fn destroy(name: &str) -> Result<()> { Ok(()) } +#[allow(clippy::too_many_arguments)] pub async fn rebuild( name: &str, hyperhive_flake: &str, @@ -265,6 +267,7 @@ pub async fn rebuild( claude_dir: &Path, notes_dir: &Path, dashboard_port: u16, + operator_pronouns: &str, ) -> Result<()> { // Sync the meta flake (idempotent — no-op when the rendered // flake matches disk) so a manual rebuild from the dashboard @@ -272,7 +275,7 @@ pub async fn rebuild( // got added directly via `nixos-container create` outside // hive-c0re). let agents = agents_for_meta(None).await?; - crate::meta::sync_agents(hyperhive_flake, dashboard_port, &agents).await?; + crate::meta::sync_agents(hyperhive_flake, dashboard_port, operator_pronouns, &agents).await?; // Then bump just this agent's input — picks up whatever // `applied//main` currently points at (deployed/). // Commits the lock if it changed. diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index e6b5893..663f0ad 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -49,6 +49,11 @@ enum Cmd { /// Dashboard HTTP port. #[arg(long, default_value_t = 7000)] dashboard_port: u16, + /// Operator pronouns (free text). Threaded into each + /// container's harness via `HIVE_OPERATOR_PRONOUNS` so the + /// system prompt can mention them. Default: `she/her`. + #[arg(long, default_value = "she/her")] + operator_pronouns: String, }, /// Spawn a new agent container directly (`hive-agent-`). Bypasses /// the approval queue — use only as an operator on the host. For @@ -95,8 +100,14 @@ async fn main() -> Result<()> { hyperhive_flake, db, dashboard_port, + operator_pronouns, } => { - let coord = Arc::new(Coordinator::open(&db, hyperhive_flake, dashboard_port)?); + let coord = Arc::new(Coordinator::open( + &db, + hyperhive_flake, + dashboard_port, + operator_pronouns, + )?); manager_server::start(coord.clone())?; // Idempotent pre-flight: rewrite pre-meta-layout applied // repos, ensure proposed repos carry the `applied` diff --git a/hive-c0re/src/meta.rs b/hive-c0re/src/meta.rs index afb30b9..bc94862 100644 --- a/hive-c0re/src/meta.rs +++ b/hive-c0re/src/meta.rs @@ -53,12 +53,13 @@ pub fn meta_dir() -> PathBuf { pub async fn sync_agents( hyperhive_flake: &str, dashboard_port: u16, + operator_pronouns: &str, agents: &[AgentSpec], ) -> Result<()> { let dir = meta_dir(); std::fs::create_dir_all(&dir).with_context(|| format!("create {}", dir.display()))?; - let new_flake = render_flake(hyperhive_flake, dashboard_port, agents); + let new_flake = render_flake(hyperhive_flake, dashboard_port, operator_pronouns, agents); let flake_path = dir.join("flake.nix"); let on_disk = std::fs::read_to_string(&flake_path).unwrap_or_default(); let initial = !dir.join(".git").exists(); @@ -180,7 +181,12 @@ pub async fn lock_update_hyperhive() -> Result<()> { git_commit(&dir, "bump hyperhive").await } -fn render_flake(hyperhive_flake: &str, dashboard_port: u16, agents: &[AgentSpec]) -> String { +fn render_flake( + hyperhive_flake: &str, + dashboard_port: u16, + operator_pronouns: &str, + agents: &[AgentSpec], +) -> String { use std::fmt::Write as _; let mut out = String::new(); out.push_str("{\n description = \"hyperhive deployed agents\";\n inputs = {\n"); @@ -193,9 +199,15 @@ fn render_flake(hyperhive_flake: &str, dashboard_port: u16, agents: &[AgentSpec] ); } out.push_str(" };\n outputs =\n { self, hyperhive, ... }@inputs:\n let\n"); + // Free-text operator string — escape backslash + double-quote so a + // pronouns value like `he/him \ "rare"` round-trips into a valid + // nix string literal without breaking the flake. + let pronouns_escaped = operator_pronouns + .replace('\\', "\\\\") + .replace('"', "\\\""); let _ = writeln!( out, - " dashboardPort = {dashboard_port};\n mkAgent = {{ name, isManager, port }}:" + " dashboardPort = {dashboard_port};\n operatorPronouns = \"{pronouns_escaped}\";\n mkAgent = {{ name, isManager, port }}:" ); out.push_str( r#" let @@ -217,6 +229,7 @@ fn render_flake(hyperhive_flake: &str, dashboard_port: u16, agents: &[AgentSpec] HIVE_PORT = toString port; HIVE_LABEL = name; HIVE_DASHBOARD_PORT = toString dashboardPort; + HIVE_OPERATOR_PRONOUNS = operatorPronouns; }; } ]; diff --git a/hive-c0re/src/migrate.rs b/hive-c0re/src/migrate.rs index b613755..48e4169 100644 --- a/hive-c0re/src/migrate.rs +++ b/hive-c0re/src/migrate.rs @@ -62,7 +62,13 @@ pub async fn run(coord: &Arc) -> Result<()> { // Phase 3: meta repo. let agents = lifecycle::agents_for_meta_listing().await.unwrap_or_default(); if let Err(e) = - meta::sync_agents(&coord.hyperhive_flake, coord.dashboard_port, &agents).await + meta::sync_agents( + &coord.hyperhive_flake, + coord.dashboard_port, + &coord.operator_pronouns, + &agents, + ) + .await { tracing::warn!(error = ?e, "migration: meta sync_agents failed"); } diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index 69f5d72..df86861 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -56,6 +56,7 @@ async fn handle(stream: UnixStream, coord: Arc) -> Result<()> { } } +#[allow(clippy::too_many_lines)] async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { let result: anyhow::Result = async { Ok(match req { @@ -75,6 +76,7 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { &claude_dir, ¬es_dir, coord.dashboard_port, + &coord.operator_pronouns, ) .await { @@ -135,6 +137,7 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { &claude_dir, ¬es_dir, coord.dashboard_port, + &coord.operator_pronouns, ) .await?; HostResponse::success() diff --git a/nix/modules/hive-c0re.nix b/nix/modules/hive-c0re.nix index 8612223..b6bdbcb 100644 --- a/nix/modules/hive-c0re.nix +++ b/nix/modules/hive-c0re.nix @@ -37,6 +37,21 @@ in default = 7000; description = "TCP port the hive-c0re dashboard listens on."; }; + operatorPronouns = lib.mkOption { + type = lib.types.str; + default = "she/her"; + example = "they/them"; + description = '' + Operator pronouns, free text. Threaded into every agent + container as the `HIVE_OPERATOR_PRONOUNS` env var; the + harness substitutes it into the agent / manager system + prompt at boot so claude refers to the operator naturally + in third person ("ask her", "tell them", etc.). Changes + propagate to running agents on the next `↻ R3BU1LD` — + forwards as a meta flake env-var bump, no per-agent + approval needed. + ''; + }; }; config = lib.mkIf cfg.enable { @@ -69,7 +84,7 @@ in ]; environment.HYPERHIVE_GIT = "${pkgs.git}/bin/git"; serviceConfig = { - ExecStart = "${cfg.package}/bin/hive-c0re --socket /run/hyperhive/host.sock serve --hyperhive-flake ${cfg.hyperhiveFlake} --dashboard-port ${toString cfg.dashboardPort}"; + ExecStart = "${cfg.package}/bin/hive-c0re --socket /run/hyperhive/host.sock serve --hyperhive-flake ${cfg.hyperhiveFlake} --dashboard-port ${toString cfg.dashboardPort} --operator-pronouns ${lib.escapeShellArg cfg.operatorPronouns}"; Restart = "on-failure"; RestartSec = 2; RuntimeDirectory = "hyperhive";