diff --git a/hive-ag3nt/assets/agent.css b/hive-ag3nt/assets/agent.css index 1943405a..9a78466e 100644 --- a/hive-ag3nt/assets/agent.css +++ b/hive-ag3nt/assets/agent.css @@ -1,11 +1,15 @@ :root { - --bg: #0a0014; - --fg: #e0d4ff; - --muted: #6c5c8c; - --purple: #cc66ff; - --purple-dim: #4a1a6a; - --amber: #ffb84d; - --green: #66ff99; + /* Catppuccin Mocha — mirrors the dashboard palette. */ + --bg: #1e1e2e; /* base */ + --bg-elev: #181825; /* mantle */ + --fg: #cdd6f4; /* text */ + --muted: #7f849c; /* overlay1 */ + --purple: #cba6f7; /* mauve */ + --purple-dim: #45475a; /* surface1 */ + --cyan: #89dceb; /* sky */ + --amber: #fab387; /* peach */ + --green: #a6e3a1; /* green */ + --red: #f38ba8; /* red */ } body { background: var(--bg); @@ -21,14 +25,12 @@ body { text-align: center; margin: 0 0 1em 0; font-size: 0.95em; - text-shadow: 0 0 6px rgba(204, 102, 255, 0.5); overflow-x: auto; } h2, h3 { color: var(--purple); text-transform: uppercase; letter-spacing: 0.15em; - text-shadow: 0 0 8px rgba(204, 102, 255, 0.4); } .divider { color: var(--purple-dim); @@ -37,10 +39,11 @@ h2, h3 { margin-bottom: 0.5em; } .meta { color: var(--muted); font-size: 0.85em; } -.status-online { color: var(--green); text-shadow: 0 0 6px rgba(102, 255, 153, 0.5); } -.status-needs-login { color: var(--amber); text-shadow: 0 0 6px rgba(255, 184, 77, 0.6); } -code { background: rgba(204, 102, 255, 0.1); padding: 0.05em 0.3em; border-radius: 2px; } -a { color: #66e0ff; } +.status-online { color: var(--green); } +.status-needs-login { color: var(--amber); } +code { background: rgba(203, 166, 247, 0.12); padding: 0.05em 0.3em; border-radius: 2px; } +a { color: var(--cyan); } +a:hover { color: var(--fg); } .btn { font-family: inherit; font-size: 1em; @@ -51,9 +54,9 @@ a { color: #66e0ff; } cursor: pointer; letter-spacing: 0.1em; } -.btn:hover { background: rgba(204, 102, 255, 0.1); } +.btn:hover { background: rgba(205, 214, 244, 0.06); } .btn-login { color: var(--amber); border-color: var(--amber); } -.btn-cancel { color: #ff6b6b; border-color: #ff6b6b; font-size: 0.85em; padding: 0.15em 0.6em; } +.btn-cancel { color: var(--red); border-color: var(--red); font-size: 0.85em; padding: 0.15em 0.6em; } .btn-rebuild { color: var(--amber); border: 1px solid var(--amber); @@ -66,7 +69,7 @@ a { color: #66e0ff; } vertical-align: middle; cursor: pointer; } -.btn-rebuild:hover { background: rgba(255, 184, 77, 0.1); } +.btn-rebuild:hover { background: rgba(250, 179, 135, 0.1); } .btn-send { color: var(--green); border-color: var(--green); } .sendform { display: flex; gap: 0.6em; margin-top: 0.5em; } .sendform input { @@ -130,11 +133,11 @@ pre.diff { padding-left: 1.2em; opacity: 0.85; } -.live .turn-end-ok { color: #66ff99; border-left-color: #66ff99; margin-bottom: 0.4em; } -.live .turn-end-fail { color: #ff6b6b; border-left-color: #ff6b6b; margin-bottom: 0.4em; } +.live .turn-end-ok { color: var(--green); border-left-color: var(--green); margin-bottom: 0.4em; } +.live .turn-end-fail { color: var(--red); border-left-color: var(--red); margin-bottom: 0.4em; } .live .text { color: var(--fg); padding-left: 1.2em; } .live .thinking { color: var(--muted); font-style: italic; padding-left: 1.2em; } -.live .tool-use { color: #66e0ff; padding-left: 1.2em; } +.live .tool-use { color: var(--cyan); padding-left: 1.2em; } .live .tool-result { color: var(--muted); padding-left: 1.2em; } .live .result { color: var(--green); padding-left: 0.5em; } .live .sys, .live .note { color: var(--muted); } diff --git a/hive-ag3nt/prompts/agent.md b/hive-ag3nt/prompts/agent.md new file mode 100644 index 00000000..80994207 --- /dev/null +++ b/hive-ag3nt/prompts/agent.md @@ -0,0 +1,10 @@ +You are hyperhive agent `{label}` in a multi-agent system. + +Tools (hyperhive surface): + +- `mcp__hyperhive__recv()` — drain one more message from your inbox (returns `(empty)` if nothing pending). +- `mcp__hyperhive__send(to, body)` — message a peer (by their name) or the operator (recipient `operator`, surfaces in the dashboard). + +Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly — message the manager (recipient `manager`) describing what you need. The manager edits `/agents/{label}/config/agent.nix` on your behalf, commits, and submits an approval that the operator can accept on the dashboard; on approve hive-c0re rebuilds your container with the new config. + +When your inbox has a message, handle it and stop. Don't narrate intent — act. diff --git a/hive-ag3nt/prompts/manager.md b/hive-ag3nt/prompts/manager.md new file mode 100644 index 00000000..2ffcac73 --- /dev/null +++ b/hive-ag3nt/prompts/manager.md @@ -0,0 +1,15 @@ +You are the hyperhive manager `{label}` in a multi-agent system. You coordinate sub-agents and relay between them and the operator. + +Tools (hyperhive surface): + +- `mcp__hyperhive__recv()` — drain one more message from your inbox. +- `mcp__hyperhive__send(to, body)` — message an agent (by name), another peer, or the operator (`operator` surfaces in the dashboard). +- `mcp__hyperhive__request_spawn(name)` — queue a brand-new sub-agent for operator approval (≤9 char name). +- `mcp__hyperhive__kill(name)` — graceful stop on a sub-agent. +- `mcp__hyperhive__request_apply_commit(agent, commit_ref)` — submit a config change for any agent (`hm1nd` for self) for operator approval. + +Your own editable config lives at `/agents/hm1nd/config/agent.nix`; every sub-agent's lives at `/agents//config/agent.nix`. Use file/git tools to edit + commit, then `request_apply_commit`. + +Messages from sender `system` are hyperhive helper events (JSON body, `event` field discriminates): `approval_resolved`, `spawned`, `rebuilt`, `killed`, `destroyed`. Use these to react to lifecycle changes — e.g. greet a freshly-spawned agent, retry a failed rebuild, or note the change to the operator. + +When your inbox has a message, handle it and stop. Don't narrate intent — act. diff --git a/hive-ag3nt/src/bin/hive-ag3nt.rs b/hive-ag3nt/src/bin/hive-ag3nt.rs index 81e05c5f..7fa08b9b 100644 --- a/hive-ag3nt/src/bin/hive-ag3nt.rs +++ b/hive-ag3nt/src/bin/hive-ag3nt.rs @@ -128,6 +128,7 @@ async fn serve( let _ = state; // reserved for future state transitions (turn-loop -> needs-login) let mcp_config = turn::write_mcp_config(socket).await?; let label = std::env::var("HIVE_LABEL").unwrap_or_else(|_| "hive-ag3nt".into()); + let system_prompt = turn::write_system_prompt(socket, &label, mcp::Flavor::Agent).await?; loop { let recv: Result = client::request(socket, &AgentRequest::Recv).await; match recv { @@ -137,9 +138,15 @@ async fn serve( from: from.clone(), body: body.clone(), }); - let prompt = format_wake_prompt(&label, &from, &body); - let outcome = - turn::drive_turn(&prompt, &mcp_config, &bus, mcp::Flavor::Agent).await; + let prompt = format_wake_prompt(&from, &body); + let outcome = turn::drive_turn( + &prompt, + &mcp_config, + &system_prompt, + &bus, + mcp::Flavor::Agent, + ) + .await; turn::emit_turn_end(&bus, &outcome); } Ok(AgentResponse::Empty) => {} @@ -157,38 +164,11 @@ async fn serve( } } -/// System prompt handed to claude on each turn. The harness has already -/// popped one message off the inbox (the wake signal); claude is told -/// about it and the MCP tools, and is expected to drive any further -/// recv/send itself. -fn format_wake_prompt(label: &str, from: &str, body: &str) -> String { - // Manager broker name. Lifecycle calls it `hm1nd` (container), broker - // calls it `manager`. Sub-agents address the manager via `manager`. - let manager = hive_sh4re::MANAGER_AGENT; - format!( - "You are hyperhive agent `{label}` in a multi-agent system.\n\ - \n\ - Incoming message from `{from}`:\n\ - ---\n\ - {body}\n\ - ---\n\ - \n\ - Tools:\n\ - - `mcp__hyperhive__recv()` — drain one more message from your inbox \ - (returns `(empty)` if nothing pending).\n\ - - `mcp__hyperhive__send(to, body)` — message a peer (by their name) \ - or the operator (recipient `operator`, surfaces in the dashboard).\n\ - \n\ - Need new packages, env vars, or other NixOS config for yourself? \ - You can't edit your own config directly — message the manager \ - (recipient `{manager}`) describing what you need. The manager \ - edits `/agents/{label}/config/agent.nix` on your behalf, commits, \ - and submits an approval that the operator can accept on the \ - dashboard; on approve hive-c0re rebuilds your container with the \ - new config.\n\ - \n\ - Handle the inbox, then stop. Don't narrate intent — act." - ) +/// Per-turn user prompt. The role/tools/etc. is in the system prompt +/// (`prompts/agent.md` → `claude --system-prompt-file`); this is just the +/// wake signal claude reacts to. +fn format_wake_prompt(from: &str, body: &str) -> String { + format!("Incoming message from `{from}`:\n---\n{body}\n---") } fn render(resp: &AgentResponse) -> Result<()> { diff --git a/hive-ag3nt/src/bin/hive-m1nd.rs b/hive-ag3nt/src/bin/hive-m1nd.rs index bf107987..9c8eead5 100644 --- a/hive-ag3nt/src/bin/hive-m1nd.rs +++ b/hive-ag3nt/src/bin/hive-m1nd.rs @@ -127,6 +127,7 @@ async fn serve(socket: &Path, interval: Duration, bus: Bus) -> Result<()> { tracing::info!(socket = %socket.display(), "hive-m1nd serve"); let mcp_config = turn::write_mcp_config(socket).await?; let label = std::env::var("HIVE_LABEL").unwrap_or_else(|_| "hm1nd".into()); + let system_prompt = turn::write_system_prompt(socket, &label, mcp::Flavor::Manager).await?; loop { let recv: Result = client::request(socket, &ManagerRequest::Recv).await; match recv { @@ -153,9 +154,15 @@ async fn serve(socket: &Path, interval: Duration, bus: Bus) -> Result<()> { from: from.clone(), body: body.clone(), }); - let prompt = format_wake_prompt(&label, &from, &body); - let outcome = - turn::drive_turn(&prompt, &mcp_config, &bus, mcp::Flavor::Manager).await; + let prompt = format_wake_prompt(&from, &body); + let outcome = turn::drive_turn( + &prompt, + &mcp_config, + &system_prompt, + &bus, + mcp::Flavor::Manager, + ) + .await; turn::emit_turn_end(&bus, &outcome); } Ok(ManagerResponse::Empty) => {} @@ -173,43 +180,10 @@ async fn serve(socket: &Path, interval: Duration, bus: Bus) -> Result<()> { } } -/// Manager-flavored wake prompt. Mentions the privileged tools the sub-agent -/// prompt doesn't have access to, and points the manager at its own -/// editable config repo for self-modification. -fn format_wake_prompt(label: &str, from: &str, body: &str) -> String { - let from_note = if from == SYSTEM_SENDER { - "\n The sender `system` means this is a hyperhive helper event \ - (JSON body, `event` field discriminates): `approval_resolved`, \ - `spawned`, `rebuilt`, `killed`, `destroyed`. Use these to react to \ - lifecycle changes — e.g. greet a freshly-spawned agent, retry a \ - failed rebuild, or note the change to the operator.\n" - } else { - "" - }; - format!( - "You are the hyperhive manager `{label}` in a multi-agent system. You \ - coordinate sub-agents and relay between them and the operator.\n\ - \n\ - Incoming message from `{from}`:\n\ - ---\n\ - {body}\n\ - ---\n{from_note} - \n\ - Tools (hyperhive surface):\n\ - - `mcp__hyperhive__recv()` — drain one more message from your inbox.\n\ - - `mcp__hyperhive__send(to, body)` — message an agent (by name), \ - another peer, or the operator (`operator` surfaces in the dashboard).\n\ - - `mcp__hyperhive__request_spawn(name)` — queue a brand-new sub-agent \ - for operator approval (≤9 char name).\n\ - - `mcp__hyperhive__kill(name)` — graceful stop on a sub-agent.\n\ - - `mcp__hyperhive__request_apply_commit(agent, commit_ref)` — submit \ - a config change for any agent (`hm1nd` for self) for operator \ - approval.\n\ - \n\ - Your own editable config lives at `/agents/hm1nd/config/agent.nix`; \ - every sub-agent's lives at `/agents//config/agent.nix`. Use \ - file/git tools to edit + commit, then `request_apply_commit`.\n\ - \n\ - Handle the inbox, then stop. Don't narrate intent — act." - ) + +/// Per-turn user prompt. The role/tools/etc. is in the system prompt +/// (`prompts/manager.md` → `claude --system-prompt-file`); this is just +/// the wake signal. +fn format_wake_prompt(from: &str, body: &str) -> String { + format!("Incoming message from `{from}`:\n---\n{body}\n---") } diff --git a/hive-ag3nt/src/turn.rs b/hive-ag3nt/src/turn.rs index 387fe199..4cb77213 100644 --- a/hive-ag3nt/src/turn.rs +++ b/hive-ag3nt/src/turn.rs @@ -50,6 +50,29 @@ pub async fn write_mcp_config(socket: &Path) -> Result { Ok(path) } +/// Write the agent's / manager's static system prompt to a file next to +/// the MCP config and return the path. Passed to claude via +/// `--system-prompt-file`, replacing claude's default system prompt with +/// the role + tools instructions. Per-turn prompts become much smaller +/// (just the wake message body). +pub async fn write_system_prompt( + socket: &Path, + label: &str, + flavor: mcp::Flavor, +) -> Result { + let parent = socket.parent().unwrap_or_else(|| Path::new("/run/hive")); + tokio::fs::create_dir_all(parent).await.ok(); + let template = match flavor { + mcp::Flavor::Agent => include_str!("../prompts/agent.md"), + mcp::Flavor::Manager => include_str!("../prompts/manager.md"), + }; + let body = template.replace("{label}", label); + let path = parent.join("claude-system-prompt.md"); + tokio::fs::write(&path, body).await?; + tracing::info!(path = %path.display(), "wrote claude system prompt"); + Ok(path) +} + /// One claude turn's outcome. The harness uses this to decide whether to /// transparently kick off a compaction and retry. #[derive(Debug)] @@ -66,16 +89,17 @@ pub enum TurnOutcome { pub async fn drive_turn( prompt: &str, mcp_config: &Path, + system_prompt: &Path, bus: &Bus, flavor: mcp::Flavor, ) -> TurnOutcome { - match run_turn(prompt, mcp_config, bus, flavor).await { + match run_turn(prompt, mcp_config, system_prompt, bus, flavor).await { TurnOutcome::PromptTooLong => { if let Err(e) = compact_session(bus).await { tracing::warn!(error = %format!("{e:#}"), "compact failed"); return TurnOutcome::Failed(e); } - run_turn(prompt, mcp_config, bus, flavor).await + run_turn(prompt, mcp_config, system_prompt, bus, flavor).await } other => other, } @@ -131,10 +155,20 @@ pub async fn wait_for_login(claude_dir: &Path, state: Arc>, po pub async fn run_turn( prompt: &str, mcp_config: &Path, + system_prompt: &Path, bus: &Bus, flavor: mcp::Flavor, ) -> TurnOutcome { - match run_claude(prompt, mcp_config, bus, flavor, ClaudeMode::Turn).await { + match run_claude( + prompt, + mcp_config, + Some(system_prompt), + bus, + flavor, + ClaudeMode::Turn, + ) + .await + { Ok(too_long) if too_long => TurnOutcome::PromptTooLong, Ok(_) => TurnOutcome::Ok, Err(e) => TurnOutcome::Failed(e), @@ -151,6 +185,7 @@ pub async fn compact_session(bus: &Bus) -> Result<()> { let _ = run_claude( "/compact", Path::new("/dev/null"), + None, bus, mcp::Flavor::Agent, // tool surface unused for /compact ClaudeMode::Compact, @@ -169,6 +204,7 @@ enum ClaudeMode { async fn run_claude( prompt: &str, mcp_config: &Path, + system_prompt: Option<&Path>, bus: &Bus, flavor: mcp::Flavor, mode: ClaudeMode, @@ -183,6 +219,9 @@ async fn run_claude( .arg("--continue") .arg("--settings") .arg(CLAUDE_SETTINGS); + if let Some(p) = system_prompt { + cmd.arg("--system-prompt-file").arg(p); + } if let ClaudeMode::Turn = mode { cmd.arg("--mcp-config") .arg(mcp_config) diff --git a/hive-c0re/assets/dashboard.css b/hive-c0re/assets/dashboard.css index fa38a138..79dfdd45 100644 --- a/hive-c0re/assets/dashboard.css +++ b/hive-c0re/assets/dashboard.css @@ -1,16 +1,18 @@ :root { - --bg: #0a0014; - --bg-elev: #18002a; - --fg: #e0d4ff; - --muted: #6c5c8c; - --purple: #cc66ff; - --purple-dim: #4a1a6a; - --cyan: #00ffff; - --pink: #ff3399; - --amber: #ffaa00; - --green: #00ff88; - --red: #ff4466; - --border: #2a0a4a; + /* Catppuccin Mocha. Keep the legacy variable names so per-class styles + don't need to be rewritten — only the values change. */ + --bg: #1e1e2e; /* base */ + --bg-elev: #181825; /* mantle */ + --fg: #cdd6f4; /* text */ + --muted: #7f849c; /* overlay1 */ + --purple: #cba6f7; /* mauve */ + --purple-dim: #45475a; /* surface1 */ + --cyan: #89dceb; /* sky */ + --pink: #f5c2e7; /* pink */ + --amber: #fab387; /* peach */ + --green: #a6e3a1; /* green */ + --red: #f38ba8; /* red */ + --border: #313244; /* surface0 */ } body { background: var(--bg); @@ -26,7 +28,6 @@ body { text-align: center; margin: 0 0 1em 0; font-size: 0.95em; - text-shadow: 0 0 6px rgba(204, 102, 255, 0.5); overflow-x: auto; } h1, h2 { @@ -34,7 +35,6 @@ h1, h2 { text-transform: uppercase; letter-spacing: 0.15em; margin-top: 2em; - text-shadow: 0 0 8px rgba(204, 102, 255, 0.4); } .divider { color: var(--purple-dim); @@ -48,13 +48,9 @@ li { padding: 0.5em 0; } a { color: var(--cyan); text-decoration: none; - text-shadow: 0 0 4px rgba(0, 255, 255, 0.5); font-weight: bold; } -a:hover { - color: #fff; - text-shadow: 0 0 12px rgba(0, 255, 255, 0.9); -} +a:hover { color: var(--fg); } .role { display: inline-block; margin-left: 0.4em; @@ -65,8 +61,8 @@ a:hover { letter-spacing: 0.1em; text-transform: uppercase; } -.role-m1nd { color: var(--pink); border-color: var(--pink); background: rgba(255, 51, 153, 0.1); } -.role-ag3nt { color: var(--amber); border-color: var(--amber); background: rgba(255, 170, 0, 0.1); } +.role-m1nd { color: var(--pink); border-color: var(--pink); background: rgba(245, 194, 231, 0.08); } +.role-ag3nt { color: var(--amber); border-color: var(--amber); background: rgba(250, 179, 135, 0.08); } .meta { color: var(--muted); font-size: 0.85em; margin-left: 0.4em; } .id { color: var(--pink); font-weight: bold; margin-right: 0.4em; } .agent { color: var(--amber); font-weight: bold; margin-right: 0.6em; } @@ -91,9 +87,8 @@ ul form.inline { display: inline-block; } border: 1px solid; padding: 0.25em 0.8em; cursor: pointer; - text-shadow: 0 0 4px currentColor; } -.btn:hover { background: rgba(255,255,255,0.05); text-shadow: 0 0 12px currentColor; } +.btn:hover { background: rgba(205, 214, 244, 0.06); } .btn-approve { color: var(--green); border-color: var(--green); } .btn-deny { color: var(--red); border-color: var(--red); } .btn-destroy { color: var(--red); border-color: var(--red); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; }