operator pronouns: configurable free-text, threaded into prompts
new NixOS module option services.hive-c0re.operatorPronouns
(free text, default 'she/her', example 'they/them'). hive-c0re
takes it as a CLI flag (--operator-pronouns, lib.escapeShellArg'd
in the systemd unit), stores it on Coordinator, threads it into
the meta flake's mkAgent so each agent's systemd service gets
HIVE_OPERATOR_PRONOUNS set. the harness reads the env at boot
and substitutes {operator_pronouns} into the agent / manager
system prompt alongside {label}. nix string is escaped against
backslash + double-quote so non-ascii / quoted values
round-trip safely. prompt addendum: both agent.md and
manager.md mention the operator's pronouns up front so claude
uses them naturally in third-person reference. propagates on
next ↻ R3BU1LD (meta lock bump, no per-agent approval).
This commit is contained in:
parent
5208b0112a
commit
50ef806266
12 changed files with 90 additions and 13 deletions
|
|
@ -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):
|
Tools (hyperhive surface):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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):
|
Tools (hyperhive surface):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,11 @@ pub async fn write_system_prompt(
|
||||||
mcp::Flavor::Agent => include_str!("../prompts/agent.md"),
|
mcp::Flavor::Agent => include_str!("../prompts/agent.md"),
|
||||||
mcp::Flavor::Manager => include_str!("../prompts/manager.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");
|
let path = parent.join("claude-system-prompt.md");
|
||||||
tokio::fs::write(&path, body).await?;
|
tokio::fs::write(&path, body).await?;
|
||||||
tracing::info!(path = %path.display(), "wrote claude system prompt");
|
tracing::info!(path = %path.display(), "wrote claude system prompt");
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ pub async fn approve(coord: Arc<Coordinator>, id: i64) -> Result<()> {
|
||||||
&claude_dir,
|
&claude_dir,
|
||||||
¬es_dir,
|
¬es_dir,
|
||||||
coord_bg.dashboard_port,
|
coord_bg.dashboard_port,
|
||||||
|
&coord_bg.operator_pronouns,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
coord_bg.clear_transient(&agent_bg);
|
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.
|
/// destroy). Idempotent — a no-op when nothing changed.
|
||||||
async fn sync_meta_after_lifecycle(coord: &Coordinator) -> Result<()> {
|
async fn sync_meta_after_lifecycle(coord: &Coordinator) -> Result<()> {
|
||||||
let agents = lifecycle::agents_for_meta_listing().await?;
|
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<()> {
|
pub async fn deny(coord: &Coordinator, id: i64, note: Option<&str>) -> Result<()> {
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ pub async fn rebuild_agent(coord: &Arc<Coordinator>, name: &str, current_rev: &s
|
||||||
&claude_dir,
|
&claude_dir,
|
||||||
¬es_dir,
|
¬es_dir,
|
||||||
coord.dashboard_port,
|
coord.dashboard_port,
|
||||||
|
&coord.operator_pronouns,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
coord.clear_transient(name);
|
coord.clear_transient(name);
|
||||||
|
|
@ -144,6 +145,7 @@ pub async fn ensure_manager(coord: &Arc<Coordinator>) -> Result<()> {
|
||||||
&claude_dir,
|
&claude_dir,
|
||||||
¬es_dir,
|
¬es_dir,
|
||||||
coord.dashboard_port,
|
coord.dashboard_port,
|
||||||
|
&coord.operator_pronouns,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
if let Some(rev) = current_rev {
|
if let Some(rev) = current_rev {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,13 @@ pub struct Coordinator {
|
||||||
/// each per-agent flake so the agent's web UI can build the right
|
/// each per-agent flake so the agent's web UI can build the right
|
||||||
/// rebuild-button URL pointing back at the dashboard.
|
/// rebuild-button URL pointing back at the dashboard.
|
||||||
pub dashboard_port: u16,
|
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.<harness>.environment by the
|
||||||
|
/// meta flake); the harness substitutes it into the agent /
|
||||||
|
/// manager system prompt at boot.
|
||||||
|
pub operator_pronouns: String,
|
||||||
agents: Mutex<HashMap<String, AgentSocket>>,
|
agents: Mutex<HashMap<String, AgentSocket>>,
|
||||||
/// Agents whose lifecycle action (currently just spawn) is in flight.
|
/// Agents whose lifecycle action (currently just spawn) is in flight.
|
||||||
/// Read by the dashboard to render a spinner; cleared when the action
|
/// Read by the dashboard to render a spinner; cleared when the action
|
||||||
|
|
@ -67,7 +74,12 @@ pub enum TransientKind {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Coordinator {
|
impl Coordinator {
|
||||||
pub fn open(db_path: &Path, hyperhive_flake: String, dashboard_port: u16) -> Result<Self> {
|
pub fn open(
|
||||||
|
db_path: &Path,
|
||||||
|
hyperhive_flake: String,
|
||||||
|
dashboard_port: u16,
|
||||||
|
operator_pronouns: String,
|
||||||
|
) -> Result<Self> {
|
||||||
let broker = Broker::open(db_path).context("open broker")?;
|
let broker = Broker::open(db_path).context("open broker")?;
|
||||||
let approvals = Approvals::open(db_path).context("open approvals")?;
|
let approvals = Approvals::open(db_path).context("open approvals")?;
|
||||||
let questions = OperatorQuestions::open(db_path).context("open operator_questions")?;
|
let questions = OperatorQuestions::open(db_path).context("open operator_questions")?;
|
||||||
|
|
@ -77,6 +89,7 @@ impl Coordinator {
|
||||||
questions: Arc::new(questions),
|
questions: Arc::new(questions),
|
||||||
hyperhive_flake,
|
hyperhive_flake,
|
||||||
dashboard_port,
|
dashboard_port,
|
||||||
|
operator_pronouns,
|
||||||
agents: Mutex::new(HashMap::new()),
|
agents: Mutex::new(HashMap::new()),
|
||||||
transient: Mutex::new(HashMap::new()),
|
transient: Mutex::new(HashMap::new()),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,7 @@ pub async fn spawn(
|
||||||
claude_dir: &Path,
|
claude_dir: &Path,
|
||||||
notes_dir: &Path,
|
notes_dir: &Path,
|
||||||
dashboard_port: u16,
|
dashboard_port: u16,
|
||||||
|
operator_pronouns: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
validate(name)?;
|
validate(name)?;
|
||||||
if let Some(other) = port_collision(name).await {
|
if let Some(other) = port_collision(name).await {
|
||||||
|
|
@ -148,7 +149,7 @@ pub async fn spawn(
|
||||||
// before `nixos-container create` so the `--flake meta#<name>`
|
// before `nixos-container create` so the `--flake meta#<name>`
|
||||||
// ref resolves.
|
// ref resolves.
|
||||||
let agents = agents_after_spawn(name).await?;
|
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 container = container_name(name);
|
||||||
let flake_ref = format!("{}#{name}", crate::meta::meta_dir().display());
|
let flake_ref = format!("{}#{name}", crate::meta::meta_dir().display());
|
||||||
run(&["create", &container, "--flake", &flake_ref]).await?;
|
run(&["create", &container, "--flake", &flake_ref]).await?;
|
||||||
|
|
@ -257,6 +258,7 @@ pub async fn destroy(name: &str) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn rebuild(
|
pub async fn rebuild(
|
||||||
name: &str,
|
name: &str,
|
||||||
hyperhive_flake: &str,
|
hyperhive_flake: &str,
|
||||||
|
|
@ -265,6 +267,7 @@ pub async fn rebuild(
|
||||||
claude_dir: &Path,
|
claude_dir: &Path,
|
||||||
notes_dir: &Path,
|
notes_dir: &Path,
|
||||||
dashboard_port: u16,
|
dashboard_port: u16,
|
||||||
|
operator_pronouns: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Sync the meta flake (idempotent — no-op when the rendered
|
// Sync the meta flake (idempotent — no-op when the rendered
|
||||||
// flake matches disk) so a manual rebuild from the dashboard
|
// 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
|
// got added directly via `nixos-container create` outside
|
||||||
// hive-c0re).
|
// hive-c0re).
|
||||||
let agents = agents_for_meta(None).await?;
|
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
|
// Then bump just this agent's input — picks up whatever
|
||||||
// `applied/<n>/main` currently points at (deployed/<latest>).
|
// `applied/<n>/main` currently points at (deployed/<latest>).
|
||||||
// Commits the lock if it changed.
|
// Commits the lock if it changed.
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,11 @@ enum Cmd {
|
||||||
/// Dashboard HTTP port.
|
/// Dashboard HTTP port.
|
||||||
#[arg(long, default_value_t = 7000)]
|
#[arg(long, default_value_t = 7000)]
|
||||||
dashboard_port: u16,
|
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-<name>`). Bypasses
|
/// Spawn a new agent container directly (`hive-agent-<name>`). Bypasses
|
||||||
/// the approval queue — use only as an operator on the host. For
|
/// the approval queue — use only as an operator on the host. For
|
||||||
|
|
@ -95,8 +100,14 @@ async fn main() -> Result<()> {
|
||||||
hyperhive_flake,
|
hyperhive_flake,
|
||||||
db,
|
db,
|
||||||
dashboard_port,
|
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())?;
|
manager_server::start(coord.clone())?;
|
||||||
// Idempotent pre-flight: rewrite pre-meta-layout applied
|
// Idempotent pre-flight: rewrite pre-meta-layout applied
|
||||||
// repos, ensure proposed repos carry the `applied`
|
// repos, ensure proposed repos carry the `applied`
|
||||||
|
|
|
||||||
|
|
@ -53,12 +53,13 @@ pub fn meta_dir() -> PathBuf {
|
||||||
pub async fn sync_agents(
|
pub async fn sync_agents(
|
||||||
hyperhive_flake: &str,
|
hyperhive_flake: &str,
|
||||||
dashboard_port: u16,
|
dashboard_port: u16,
|
||||||
|
operator_pronouns: &str,
|
||||||
agents: &[AgentSpec],
|
agents: &[AgentSpec],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let dir = meta_dir();
|
let dir = meta_dir();
|
||||||
std::fs::create_dir_all(&dir).with_context(|| format!("create {}", dir.display()))?;
|
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 flake_path = dir.join("flake.nix");
|
||||||
let on_disk = std::fs::read_to_string(&flake_path).unwrap_or_default();
|
let on_disk = std::fs::read_to_string(&flake_path).unwrap_or_default();
|
||||||
let initial = !dir.join(".git").exists();
|
let initial = !dir.join(".git").exists();
|
||||||
|
|
@ -180,7 +181,12 @@ pub async fn lock_update_hyperhive() -> Result<()> {
|
||||||
git_commit(&dir, "bump hyperhive").await
|
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 _;
|
use std::fmt::Write as _;
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
out.push_str("{\n description = \"hyperhive deployed agents\";\n inputs = {\n");
|
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");
|
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!(
|
let _ = writeln!(
|
||||||
out,
|
out,
|
||||||
" dashboardPort = {dashboard_port};\n mkAgent = {{ name, isManager, port }}:"
|
" dashboardPort = {dashboard_port};\n operatorPronouns = \"{pronouns_escaped}\";\n mkAgent = {{ name, isManager, port }}:"
|
||||||
);
|
);
|
||||||
out.push_str(
|
out.push_str(
|
||||||
r#" let
|
r#" let
|
||||||
|
|
@ -217,6 +229,7 @@ fn render_flake(hyperhive_flake: &str, dashboard_port: u16, agents: &[AgentSpec]
|
||||||
HIVE_PORT = toString port;
|
HIVE_PORT = toString port;
|
||||||
HIVE_LABEL = name;
|
HIVE_LABEL = name;
|
||||||
HIVE_DASHBOARD_PORT = toString dashboardPort;
|
HIVE_DASHBOARD_PORT = toString dashboardPort;
|
||||||
|
HIVE_OPERATOR_PRONOUNS = operatorPronouns;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,13 @@ pub async fn run(coord: &Arc<Coordinator>) -> Result<()> {
|
||||||
// Phase 3: meta repo.
|
// Phase 3: meta repo.
|
||||||
let agents = lifecycle::agents_for_meta_listing().await.unwrap_or_default();
|
let agents = lifecycle::agents_for_meta_listing().await.unwrap_or_default();
|
||||||
if let Err(e) =
|
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");
|
tracing::warn!(error = ?e, "migration: meta sync_agents failed");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ async fn handle(stream: UnixStream, coord: Arc<Coordinator>) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
|
async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
|
||||||
let result: anyhow::Result<HostResponse> = async {
|
let result: anyhow::Result<HostResponse> = async {
|
||||||
Ok(match req {
|
Ok(match req {
|
||||||
|
|
@ -75,6 +76,7 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
|
||||||
&claude_dir,
|
&claude_dir,
|
||||||
¬es_dir,
|
¬es_dir,
|
||||||
coord.dashboard_port,
|
coord.dashboard_port,
|
||||||
|
&coord.operator_pronouns,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|
@ -135,6 +137,7 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
|
||||||
&claude_dir,
|
&claude_dir,
|
||||||
¬es_dir,
|
¬es_dir,
|
||||||
coord.dashboard_port,
|
coord.dashboard_port,
|
||||||
|
&coord.operator_pronouns,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
HostResponse::success()
|
HostResponse::success()
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,21 @@ in
|
||||||
default = 7000;
|
default = 7000;
|
||||||
description = "TCP port the hive-c0re dashboard listens on.";
|
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 {
|
config = lib.mkIf cfg.enable {
|
||||||
|
|
@ -69,7 +84,7 @@ in
|
||||||
];
|
];
|
||||||
environment.HYPERHIVE_GIT = "${pkgs.git}/bin/git";
|
environment.HYPERHIVE_GIT = "${pkgs.git}/bin/git";
|
||||||
serviceConfig = {
|
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";
|
Restart = "on-failure";
|
||||||
RestartSec = 2;
|
RestartSec = 2;
|
||||||
RuntimeDirectory = "hyperhive";
|
RuntimeDirectory = "hyperhive";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue