diff --git a/hive-ag3nt/assets/agent.css b/hive-ag3nt/assets/agent.css index 9a78466e..1943405a 100644 --- a/hive-ag3nt/assets/agent.css +++ b/hive-ag3nt/assets/agent.css @@ -1,15 +1,11 @@ :root { - /* 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 */ + --bg: #0a0014; + --fg: #e0d4ff; + --muted: #6c5c8c; + --purple: #cc66ff; + --purple-dim: #4a1a6a; + --amber: #ffb84d; + --green: #66ff99; } body { background: var(--bg); @@ -25,12 +21,14 @@ 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); @@ -39,11 +37,10 @@ h2, h3 { margin-bottom: 0.5em; } .meta { color: var(--muted); font-size: 0.85em; } -.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); } +.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; } .btn { font-family: inherit; font-size: 1em; @@ -54,9 +51,9 @@ a:hover { color: var(--fg); } cursor: pointer; letter-spacing: 0.1em; } -.btn:hover { background: rgba(205, 214, 244, 0.06); } +.btn:hover { background: rgba(204, 102, 255, 0.1); } .btn-login { color: var(--amber); border-color: var(--amber); } -.btn-cancel { color: var(--red); border-color: var(--red); font-size: 0.85em; padding: 0.15em 0.6em; } +.btn-cancel { color: #ff6b6b; border-color: #ff6b6b; font-size: 0.85em; padding: 0.15em 0.6em; } .btn-rebuild { color: var(--amber); border: 1px solid var(--amber); @@ -69,7 +66,7 @@ a:hover { color: var(--fg); } vertical-align: middle; cursor: pointer; } -.btn-rebuild:hover { background: rgba(250, 179, 135, 0.1); } +.btn-rebuild:hover { background: rgba(255, 184, 77, 0.1); } .btn-send { color: var(--green); border-color: var(--green); } .sendform { display: flex; gap: 0.6em; margin-top: 0.5em; } .sendform input { @@ -133,11 +130,11 @@ pre.diff { padding-left: 1.2em; opacity: 0.85; } -.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 .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 .text { color: var(--fg); padding-left: 1.2em; } .live .thinking { color: var(--muted); font-style: italic; padding-left: 1.2em; } -.live .tool-use { color: var(--cyan); padding-left: 1.2em; } +.live .tool-use { color: #66e0ff; 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 deleted file mode 100644 index 80994207..00000000 --- a/hive-ag3nt/prompts/agent.md +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 2ffcac73..00000000 --- a/hive-ag3nt/prompts/manager.md +++ /dev/null @@ -1,15 +0,0 @@ -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 7fa08b9b..81e05c5f 100644 --- a/hive-ag3nt/src/bin/hive-ag3nt.rs +++ b/hive-ag3nt/src/bin/hive-ag3nt.rs @@ -128,7 +128,6 @@ 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 { @@ -138,15 +137,9 @@ async fn serve( from: from.clone(), body: body.clone(), }); - let prompt = format_wake_prompt(&from, &body); - let outcome = turn::drive_turn( - &prompt, - &mcp_config, - &system_prompt, - &bus, - mcp::Flavor::Agent, - ) - .await; + let prompt = format_wake_prompt(&label, &from, &body); + let outcome = + turn::drive_turn(&prompt, &mcp_config, &bus, mcp::Flavor::Agent).await; turn::emit_turn_end(&bus, &outcome); } Ok(AgentResponse::Empty) => {} @@ -164,11 +157,38 @@ async fn serve( } } -/// 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---") +/// 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." + ) } fn render(resp: &AgentResponse) -> Result<()> { diff --git a/hive-ag3nt/src/bin/hive-m1nd.rs b/hive-ag3nt/src/bin/hive-m1nd.rs index 9c8eead5..bf107987 100644 --- a/hive-ag3nt/src/bin/hive-m1nd.rs +++ b/hive-ag3nt/src/bin/hive-m1nd.rs @@ -127,7 +127,6 @@ 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 { @@ -154,15 +153,9 @@ async fn serve(socket: &Path, interval: Duration, bus: Bus) -> Result<()> { from: from.clone(), body: body.clone(), }); - let prompt = format_wake_prompt(&from, &body); - let outcome = turn::drive_turn( - &prompt, - &mcp_config, - &system_prompt, - &bus, - mcp::Flavor::Manager, - ) - .await; + let prompt = format_wake_prompt(&label, &from, &body); + let outcome = + turn::drive_turn(&prompt, &mcp_config, &bus, mcp::Flavor::Manager).await; turn::emit_turn_end(&bus, &outcome); } Ok(ManagerResponse::Empty) => {} @@ -180,10 +173,43 @@ async fn serve(socket: &Path, interval: Duration, bus: Bus) -> Result<()> { } } - -/// 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---") +/// 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." + ) } diff --git a/hive-ag3nt/src/turn.rs b/hive-ag3nt/src/turn.rs index 4cb77213..387fe199 100644 --- a/hive-ag3nt/src/turn.rs +++ b/hive-ag3nt/src/turn.rs @@ -50,29 +50,6 @@ 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)] @@ -89,17 +66,16 @@ 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, system_prompt, bus, flavor).await { + match run_turn(prompt, mcp_config, 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, system_prompt, bus, flavor).await + run_turn(prompt, mcp_config, bus, flavor).await } other => other, } @@ -155,20 +131,10 @@ 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, - Some(system_prompt), - bus, - flavor, - ClaudeMode::Turn, - ) - .await - { + match run_claude(prompt, mcp_config, bus, flavor, ClaudeMode::Turn).await { Ok(too_long) if too_long => TurnOutcome::PromptTooLong, Ok(_) => TurnOutcome::Ok, Err(e) => TurnOutcome::Failed(e), @@ -185,7 +151,6 @@ 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, @@ -204,7 +169,6 @@ enum ClaudeMode { async fn run_claude( prompt: &str, mcp_config: &Path, - system_prompt: Option<&Path>, bus: &Bus, flavor: mcp::Flavor, mode: ClaudeMode, @@ -219,9 +183,6 @@ 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 79dfdd45..fa38a138 100644 --- a/hive-c0re/assets/dashboard.css +++ b/hive-c0re/assets/dashboard.css @@ -1,18 +1,16 @@ :root { - /* 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 */ + --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; } body { background: var(--bg); @@ -28,6 +26,7 @@ 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 { @@ -35,6 +34,7 @@ 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,9 +48,13 @@ 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: var(--fg); } +a:hover { + color: #fff; + text-shadow: 0 0 12px rgba(0, 255, 255, 0.9); +} .role { display: inline-block; margin-left: 0.4em; @@ -61,8 +65,8 @@ a:hover { color: var(--fg); } letter-spacing: 0.1em; text-transform: uppercase; } -.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); } +.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); } .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; } @@ -87,8 +91,9 @@ 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(205, 214, 244, 0.06); } +.btn:hover { background: rgba(255,255,255,0.05); text-shadow: 0 0 12px currentColor; } .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; }