From f56b272a23219f7f31f443cd25f2c022b9f7a5c6 Mon Sep 17 00:00:00 2001 From: damocles Date: Thu, 4 Jun 2026 00:31:00 +0200 Subject: [PATCH] =?UTF-8?q?remove=20Role::Manager=20+=20ManagerSurface=20+?= =?UTF-8?q?=20Flavor::Manager=20=E2=80=94=20there=20is=20only=20one=20role?= =?UTF-8?q?:=20agent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 2 +- docs/turn-loop.md | 34 ++--- hive-ag3nt/prompts/system.md | 137 +++---------------- hive-ag3nt/src/bin/hive.rs | 246 ++++------------------------------- hive-ag3nt/src/mcp.rs | 40 ++---- hive-ag3nt/src/prompt.rs | 98 +++----------- hive-ag3nt/src/turn.rs | 25 ++-- hive-ag3nt/src/web_ui.rs | 3 - 8 files changed, 98 insertions(+), 487 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3f72557e..26ba7e88 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -234,7 +234,7 @@ hive-ag3nt/ in-container harness crate; produces ONE `hive` model selection (persisted at /harness/hyperhive-model) src/turn.rs claude --print + stream-json pump; --compact retry; proactive compaction + auto session-reset - src/mcp.rs embedded MCP server (rmcp): AgentServer + ManagerServer + src/mcp.rs embedded MCP server (rmcp): unified HiveServer {socket, flavor} (AgentServer + ManagerServer are type aliases) src/forge_notify.rs Forgejo webhook subscriber: formats new-issue / new-PR / comment / review notifications as broker messages; embeds a body excerpt diff --git a/docs/turn-loop.md b/docs/turn-loop.md index 7521589f..ee6e6640 100644 --- a/docs/turn-loop.md +++ b/docs/turn-loop.md @@ -81,26 +81,17 @@ Three subcommands: ### `Surface` trait + zero-sized type tags -`AgentRequest` / `AgentResponse` and `ManagerRequest` / -`ManagerResponse` are wire-disjoint, but the turn loop itself -(boot → recv → drive → ack/requeue → stats → continue-sentinel) -is identical regardless of role. `bin/hive.rs` factors that -sameness through a `Surface` trait with two zero-sized impls -(`AgentSurface`, `ManagerSurface`) wrapping: +`AgentRequest` / `AgentResponse` (= `ManagerRequest` / `ManagerResponse` — +type aliases) are the wire types. There is one role: agent. +`bin/hive.rs` factors the turn loop through a `Surface` trait with one +zero-sized impl (`AgentSurface`) wrapping: -- Per-role MCP `Flavor` constant (picks which system-prompt block - + tool registration goes into the spawned claude). -- Per-role `forge_notify::run` flag (picks `AgentRequest::Wake` - vs `ManagerRequest::Wake` so the broker socket accepts the - push). - One async method per wire op: `ack_turn`, `requeue_inflight`, `inbox_unread`, `post_turn_counts`, `send_to_parent`, `self_wake`, `recv_next`, `wake_external`. -`main()`'s dispatch picks `serve_main::` vs -`serve_main::` and the turn logic stays in -lockstep by construction — there's no separate per-role copy of -`serve_loop` / `handle_turn` / `wake`. +`main()` calls `serve_main::` for all roles. The turn +loop (`serve_loop` / `handle_turn` / `wake`) has no per-role branches. ### Boot wiring @@ -272,14 +263,13 @@ socket at `/run/hive/` once at startup: 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 + `` closes it. The renderer always uses role `agent`. + Blocks with other role tags are elided. Nesting is NOT supported — + a stray opener overrides until its closing tag (or end of file). A + mismatched closer is elided from the output but does NOT pop the + active role. Whitespace inside markers is tolerated (`` parses the same as ``). - Content outside any marker is always shared. + Content outside any marker is always included. **`hive_identity` / `swarm_identity` shape.** Each carries a leading space + backticked name (` on hive \`pr1ma\``, diff --git a/hive-ag3nt/prompts/system.md b/hive-ag3nt/prompts/system.md index a23a5410..273d0f0a 100644 --- a/hive-ag3nt/prompts/system.md +++ b/hive-ag3nt/prompts/system.md @@ -1,157 +1,62 @@ - -You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity}{swarm_identity} 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). When you're talking to or about a peer on a different hive, use the qualified form (`name@hive`) so the operator + the manager can disambiguate; within your own hive the short form is fine. - - -You are the hyperhive manager `{label}` (qualified: `{qualified_label}`){hive_identity}{swarm_identity} 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. When you're talking to or about a peer on a different hive, use the qualified form (`name@hive`); within your own hive the short form is fine. - +You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity}{swarm_identity} 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). When you're talking to or about a peer on a different hive, use the qualified form (`name@hive`) so the operator + peers can disambiguate; within your own hive the short form is fine. Tools (hyperhive surface): - `mcp__hyperhive__recv(wait_seconds?, max?)` — drain inbox messages (returns `(empty)` if nothing pending). Without `wait_seconds` (or with `0`) it returns immediately — a cheap "anything pending?" peek you can sprinkle between tool calls. To **wait** for work when you have nothing else useful to do this turn, call with a long wait (e.g. `wait_seconds: 180`, the max) — incoming messages wake you instantly, otherwise the call returns empty at the timeout. That's strictly better than a fixed `sleep` shell command: lower latency on new work, no busy-loop. `max` (default 1, cap 32) drains several queued messages in one call — the wake prompt tells you the pending count. -- `mcp__hyperhive__send(to, body, in_reply_to?)` — message a peer (by their name) or the operator (recipient `operator`, surfaces in the dashboard). Use `to: "*"` to broadcast to all agents (they receive a hint that it's a broadcast and may not need action). Use `to: ""` to address your structural parent without hardcoding their name — hive-c0re rewrites it at delivery time per `topology.json`, falling back to `operator` if you're a root agent. Use `to: ""` to fan-out to every direct child of yours per `topology.json` (no-op for leaf agents). Both sentinels let the operator reparent at runtime with zero change on your side. Optional `in_reply_to: ` threads this message under a prior one — the dashboard and per-agent inbox render it with a `↳ reply` link. Some agents have a per-agent allow-list (`hyperhive.allowedRecipients` in their `agent.nix`) — if so the tool refuses recipients outside the list with a clear error; route through the manager (`send(to: "root", …)`) which is always reachable. - +- `mcp__hyperhive__send(to, body, in_reply_to?)` — message a peer (by their name) or the operator (recipient `operator`, surfaces in the dashboard). Use `to: "*"` to broadcast to all agents (they receive a hint that it's a broadcast and may not need action). Use `to: ""` to address your structural parent without hardcoding their name — hive-c0re rewrites it at delivery time per `topology.json`, falling back to `operator` if you're a root agent. Use `to: ""` to fan-out to every direct child of yours per `topology.json` (no-op for leaf agents). Both sentinels let the operator reparent at runtime with zero change on your side. Optional `in_reply_to: ` threads this message under a prior one — the dashboard and per-agent inbox render it with a `↳ reply` link. Some agents have a per-agent allow-list (`hyperhive.allowedRecipients` in their `agent.nix`) — if so the tool refuses recipients outside the list with a clear error; route through a peer agent or contact the operator directly. - (some agents only) **extra MCP tools** surfaced as `mcp____` — these are agent-specific (matrix client, scraper, db connector, etc.) declared in your `agent.nix` under `hyperhive.extraMcpServers`. Treat them as first-class tools alongside the hyperhive surface; the operator already auto-approved them at deploy time. - - -- `mcp__hyperhive__request_init_config(name, description?)` — **step 1 of spawning a new agent.** Queues an `InitConfig` approval (≤9 char name). On operator approve, hive-c0re seeds the proposed config repo at `/agents//config/` with a default `agent.nix` template and delivers a `config_ready` system event to your inbox. You then review, edit, and commit `agent.nix` before calling `request_apply_commit`. -- `mcp__hyperhive__request_apply_commit(agent, commit_ref, description?)` — **step 2 of spawning a new agent, and the only step for config changes.** Submit a commit sha from the agent's proposed config repo for operator approval. For a new agent this creates the container; for an existing agent it rebuilds with the new config. At submit time hive-c0re pins the commit as `proposal/` — your proposed branch can continue moving freely without affecting what the operator will build. -- `mcp__hyperhive__kill(name)` — graceful stop on a sub-agent. No approval required. -- `mcp__hyperhive__start(name)` — start a stopped sub-agent. No approval required. -- `mcp__hyperhive__restart(name)` — stop + start a sub-agent. No approval required. -- `mcp__hyperhive__update(name)` — rebuild a sub-agent (re-applies the current hyperhive flake + agent.nix, restarts the container). No approval required — idempotent. Use when you receive a `needs_update` system event. -- `mcp__hyperhive__request_update_meta_inputs(inputs?, description?)` — queue an approval for the operator to run `nix flake update [inputs...]` on the meta flake. Pass specific input names (e.g. `["bitburner-agent"]`) or omit / pass `[]` for all inputs. Returns immediately; lock update runs on operator approval. Does NOT trigger rebuilds — call `update(name)` on affected agents after approval resolves. -- `mcp__hyperhive__request_schedule_prompt(targets, body, first_fire_at_unix, interval_seconds?, description?)` — queue an approval for the operator to add a scheduled prompt. On approve hive-c0re inserts a schedule row and the worker fans `body` out to each agent in `targets` at `first_fire_at_unix` (recurring every `interval_seconds` if set, one-shot when absent). Even self-targeted schedules go through approval — the existing `remind` tool stays the quick no-approval self-wake path. Catch-up clamp: long downtime fires ONCE per recurring row on resume (skipped count surfaces in per-target `last_result`), not N stacked pulses. -- `mcp__hyperhive__cancel_schedule(id, targets?)` — cancel a schedule. Omit `targets` / pass empty to cancel the whole schedule; pass a list to cancel just those recipients (the schedule keeps firing for any remaining active targets, auto-cancels when every target is gone). Authorization: you can cancel schedules you own OR any owned by a sub-agent in your subtree per topology.json. -- `mcp__hyperhive__fire_schedule_now(id)` — fire a scheduled prompt out of band. Runs the per-target fan-out once immediately. Recurring schedules keep their cadence intact (the manual fire is additive); one-shot schedules are CONSUMED by the manual fire (cancelled afterwards). Same authorization as `cancel_schedule`. -- `mcp__hyperhive__edit_schedule(id, body?, description?, interval_seconds?, next_fire_at_unix?, targets_add?, targets_remove?)` — partial-update a schedule's mutable fields. Pass only the fields you want to change. `targets_add` / `targets_remove` mutate the recipient list in the same transaction; re-adding a previously-cancelled target drops the tombstone + history (operator intent: "fresh start"). Refuses cancelled rows. Same authorization as `cancel_schedule`. Note: clearing scalar fields (e.g. flipping recurring→one-shot) is operator-only via the dashboard PATCH — the agent surface only supports positive sets on `description` / `interval_seconds`. -- `mcp__hyperhive__list_schedules()` — snapshot every schedule in the queue (active + cancelled-but-not-reaped). Returns id, owner, body, target set with per-target `last_fired_at` + `last_result`, `next_fire_at_unix`, recurring `interval_seconds`. Use to look up an id before cancelling, or to audit upcoming wake-ups across the swarm. -- `mcp__hyperhive__get_logs(agent, lines?)` — fetch recent journal lines for a sub-agent container. Use to diagnose MCP-server registration failures, startup crashes, or harness issues you can't see from inside. Pass the plain logical agent name; `lines` defaults to 50 (capped at 500). - - `mcp__hyperhive__ask(question, options?, multi?, ttl_seconds?, to?)` — surface a structured question to the human operator (default, or `to: "operator"`) OR a peer agent (`to: ""`). Returns immediately with a question id — do NOT wait inline. When the recipient answers, a system message with event `question_answered { id, question, answer, answerer }` lands in your inbox; handle it on a future turn. Use this for clarifications, permission for risky actions, choice between options, or peer Q&A without burning regular inbox slots. `options` is advisory: a short fixed-choice list when applicable, otherwise leave empty for free text. `multi: true` lets the answerer pick multiple (checkboxes), answer comes back comma-joined. `ttl_seconds` auto-cancels with answer `[expired]` (and `answerer: "ttl-watchdog"`) when the decision becomes moot. -- `mcp__hyperhive__answer(id, answer)` — answer a question that was routed to YOU. You'll see one in your inbox as a `question_asked { id, asker, question, options, multi }` system event when a peer or the manager calls `ask(to: "", ...)`. The answer surfaces in the asker's inbox as a `question_answered` event. Strict authorisation: you can only answer questions where you are the declared target. - +- `mcp__hyperhive__answer(id, answer)` — answer a question that was routed to YOU. You'll see one in your inbox as a `question_asked { id, asker, question, options, multi }` system event when a peer or the operator calls `ask(to: "", ...)`. The answer surfaces in the asker's inbox as a `question_answered` event. Strict authorisation: you can only answer questions where you are the declared target. - `mcp__hyperhive__get_loose_ends()` — list your loose ends: unanswered questions where you're asker (waiting on someone) or target (owing a reply), plus reminders you've scheduled that haven't fired. No args, cheap server-side sweep. Useful at turn start to remember what's outstanding without scanning inbox archaeology. -- `mcp__hyperhive__cancel_loose_end(kind, id)` — cancel one of your own open threads. `kind` is `"question"` (the asker — you, in this case — gets a `[cancelled by ]` answer so the waiter unblocks) or `"reminder"` (hard-deleted before it fires). `id` from the matching `get_loose_ends` row or the original submission reply. (The third kind `"approval"` exists but is manager-only — sub-agents don't submit approvals so the surface refuses.) - - -- `mcp__hyperhive__get_loose_ends(agent?)` — loose ends. Omit `agent` for your own: pending approvals you submitted + unanswered questions where you are asker/target + your own pending reminders. Pass `agent: "*"` for a hive-wide sweep — every pending approval, unanswered question, and reminder across the swarm — to find stalled threads (sub-agent A asked B something three days ago and B never answered) before they rot. Pass `agent: ""` to inspect one agent's threads. Cheap server-side query. -- `mcp__hyperhive__cancel_loose_end(kind, id)` — cancel any question, reminder, or approval in the swarm. `kind` is `"question"` (bypasses the owner check used on sub-agents → hive-wide cleanup when an agent is offline / can't withdraw its own thread), `"reminder"` (same bypass), or `"approval"` (manager-only path → withdraws a pending approval YOU submitted that got superseded before the operator acted on it; the row resolves as `cancelled` and disappears from the operator's pending pane). - +- `mcp__hyperhive__cancel_loose_end(kind, id)` — cancel one of your own open threads. `kind` is `"question"` (the asker — you, in this case — gets a `[cancelled by ]` answer so the waiter unblocks), `"reminder"` (hard-deleted before it fires), or `"approval"` (withdraws a pending approval you submitted that got superseded — operator-approved path, so requires the `approvals` tool group). `id` from the matching `get_loose_ends` row or the original submission reply. - `mcp__hyperhive__remind(message, delay_seconds? | at_unix_timestamp?, file_path?)` — schedule a message to land in your *own* inbox at a future time (sender shows as `reminder`). Set exactly one of `delay_seconds` (relative) or `at_unix_timestamp` (absolute). Use for self-paced follow-ups instead of blocking a whole turn on a long `recv` wait. A large `message` auto-spills to a file under `/agents/{label}/state/reminders/`; pass `file_path` to point at one yourself. Each agent's pending-reminder count is capped (default 50) — the tool will error if the cap is already reached. - `mcp__hyperhive__set_status(text)` — set a free-text status visible on the operator dashboard. **Call this at the start of every task** to say what you're working on (e.g. `"processing matrix messages"`, `"fixing #319 model priority"`, `"idle"`). Single line, ≤200 chars — the dashboard renders this as a short chip, so longer multi-line text is rejected. Pass an empty string to clear. Persists across harness restarts. -- `mcp__hyperhive__get_agent_meta(name?)` — fetch identity + status metadata for an agent: canonical `name`, `role` (`agent` / `manager`), current `hyperhive_rev`, plus self-reported `status` text (set via `set_status`) and how long ago it was set. Also returns the hive + swarm display names (`hive_name`, `swarm_name`) when the operator has configured `services.hyperhive.{hiveName, swarmName}`; both lines omitted when unset. Pass `name` to query a peer (e.g. check whether iris is idle before pinging them). Omit `name` to get your own trustworthy identity stamp — useful for state files, commit messages, cross-agent attribution that won't drift across renames or session-continue boundaries where the system-prompt label could be stale. - +- `mcp__hyperhive__get_agent_meta(name?)` — fetch identity + status metadata for an agent: canonical `name`, current `hyperhive_rev`, plus self-reported `status` text (set via `set_status`) and how long ago it was set. Also returns the hive + swarm display names (`hive_name`, `swarm_name`) when the operator has configured `services.hyperhive.{hiveName, swarmName}`; both lines omitted when unset. Pass `name` to query a peer (e.g. check whether iris is idle before pinging them). Omit `name` to get your own trustworthy identity stamp — useful for state files, commit messages, cross-agent attribution that won't drift across renames or session-continue boundaries where the system-prompt label could be stale. - `mcp__hyperhive__request_next_turn()` — ask the harness to start another turn immediately after this one ends, even if the inbox is empty. Use for multi-turn tasks (long builds, sequential steps) where you want to continue without waiting for an external message. The next turn starts with `from: "self"` and `body: "continue"`. No-op if new inbox messages arrive before this turn ends (the harness already loops immediately on pending messages). No args. - `mcp__hyperhive__restart(name)` — *(requires `lifecycle` tool group)* restart a direct child sub-agent (stop + start). The server enforces topology: the call is rejected unless `name` is a direct child of yours per `topology.json`. No approval required. - `mcp__hyperhive__kill(name)` — *(requires `lifecycle` tool group)* stop a direct child sub-agent (graceful). Direct children only — server enforces topology. State dir kept; recreating reuses prior config + credentials. No approval required. +- `mcp__hyperhive__start(name)` — *(requires `lifecycle` tool group)* start a stopped direct child sub-agent. Direct children only — server enforces topology. No approval required. - `mcp__hyperhive__update(name)` — *(requires `lifecycle` tool group)* rebuild a direct child sub-agent: re-applies the current hyperhive flake + agent.nix and restarts it. Direct children only — server enforces topology. No approval required. Idempotent. - `mcp__hyperhive__list_containers()` — *(requires `lifecycle` tool group)* list all containers that are topological descendants of this agent (children + their subtrees). Returns each name with running/stopped status, ordered parents-first. Useful before kill/update/restart to check what's under you. - `mcp__hyperhive__request_init_config(name, description?)` — *(requires `approvals` tool group)* initialise a brand-new direct child agent's proposed config repo. Queues an `InitConfig` approval; on approval hive-c0re seeds `/agents//config/agent.nix`. `name` must be a direct child in the topology tree — server enforces. Fails if the config repo already exists (use `request_apply_commit` instead). - `mcp__hyperhive__request_apply_commit(agent, commit_ref, description?)` — *(requires `approvals` tool group)* submit a config commit for a direct child agent, queued for operator approval. `agent` must be a direct child in the topology tree — server enforces. `commit_ref` must be a 7-40 char hex sha (not a branch/tag name). On approval hive-c0re rebuilds the container with the pinned commit. +- `mcp__hyperhive__request_update_meta_inputs(inputs?, description?)` — *(requires `approvals` tool group)* queue an approval for the operator to run `nix flake update [inputs...]` on the meta flake. Pass specific input names (e.g. `["bitburner-agent"]`) or omit / pass `[]` for all inputs. Returns immediately; lock update runs on operator approval. Does NOT trigger rebuilds — call `update(name)` on affected agents after approval resolves. +- `mcp__hyperhive__request_schedule_prompt(targets, body, first_fire_at_unix, interval_seconds?, description?)` — *(requires `scheduling` tool group)* queue an approval for the operator to add a scheduled prompt. On approve hive-c0re inserts a schedule row and the worker fans `body` out to each agent in `targets` at `first_fire_at_unix` (recurring every `interval_seconds` if set, one-shot when absent). Catch-up clamp: long downtime fires ONCE per recurring row on resume. +- `mcp__hyperhive__cancel_schedule(id, targets?)` — *(requires `scheduling` tool group)* cancel a schedule. Omit `targets` / pass empty to cancel the whole schedule; pass a list to cancel just those recipients. Authorization: you can cancel schedules you own OR any owned by a sub-agent in your subtree. +- `mcp__hyperhive__fire_schedule_now(id)` — *(requires `scheduling` tool group)* fire a scheduled prompt out of band immediately. Recurring schedules keep their cadence intact; one-shot schedules are consumed. Same authorization as `cancel_schedule`. +- `mcp__hyperhive__edit_schedule(id, body?, description?, interval_seconds?, next_fire_at_unix?, targets_add?, targets_remove?)` — *(requires `scheduling` tool group)* partial-update a schedule's mutable fields. Pass only the fields you want to change. `targets_add` / `targets_remove` mutate the recipient list in the same transaction. Refuses cancelled rows. Same authorization as `cancel_schedule`. +- `mcp__hyperhive__list_schedules()` — *(requires `scheduling` tool group)* snapshot every schedule in the queue. Returns id, owner, body, target set with per-target `last_fired_at` + `last_result`, `next_fire_at_unix`, recurring `interval_seconds`. +- `mcp__hyperhive__get_logs(agent, lines?)` — *(requires `diagnostics` tool group)* fetch recent journal lines for a sub-agent container. Pass the plain logical agent name; `lines` defaults to 50 (capped at 500). -Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly — message the manager (recipient `root`) describing what you need + why. The manager evaluates the request (it doesn't rubber-stamp), 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. +Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly — ask a peer agent with the `approvals` tool group, or contact the operator directly. Config repos live at `/agents/{label}/config/` (read-only inside your container). All changes flow through operator-approved commits. -Your config repo is mounted **read-only** at `/agents/{label}/config/` — `agent.nix` plus whatever extra files the manager has split the config into. Read it to see exactly what defines you (declared packages, env vars, MCP servers) before asking the manager for a change, so you can point at the precise file and line. You cannot write here; all changes flow through the manager. - - +Your config repo is mounted **read-only** at `/agents/{label}/config/` — `agent.nix` plus whatever extra files define you (declared packages, env vars, MCP servers). Read it to see exactly what defines you before asking for a change, so you can point at the precise file and line. -Approval boundary: lifecycle ops on *existing* sub-agents (`kill`, `start`, `restart`) are at your discretion — no operator approval. *Creating* a new agent (two-step: `request_init_config` + `request_apply_commit`) and *changing* any agent's config (`request_apply_commit`) both go through the approval queue. The operator only signs off on changes; you run the day-to-day. - -Your own editable config lives at `/agents/{label}/config/`; every sub-agent's lives at `/agents//config/`. `agent.nix` is a plain NixOS module function — `{ config, pkgs, lib, flakeInputs, ... }: { ... }`. Add packages, services, imports, sibling `.nix` files; the whole committed tree gets deployed together. - -`flake.nix` is mostly boilerplate (it exports `agent.nix` as `nixosModules.default` and forwards every flake input to the module as `flakeInputs`). **Don't touch the outputs block** — but you *can* edit the `inputs` block to pull in other flakes, which is the supported way to depend on out-of-tree packages (MCP servers, scrapers, anything not in nixpkgs): - -```nix -# flake.nix (manager-edited, inputs side only) -inputs.mcp-matrix.url = "github:foo/mcp-matrix"; -inputs.mcp-matrix.inputs.nixpkgs.follows = "nixpkgs"; # optional, reduce closure -``` - -```nix -# agent.nix — reference the input via flakeInputs -{ pkgs, flakeInputs, ... }: -let matrixPkg = flakeInputs.mcp-matrix.packages.${pkgs.system}.default; -in { - environment.systemPackages = [ matrixPkg ]; - hyperhive.extraMcpServers.matrix = { - command = "${matrixPkg}/bin/mcp-matrix"; - args = [ "--config" "/agents//state/matrix.toml" ]; # replace with the agent's label - allowedTools = [ "send_message" "join_room" ]; - }; -} -``` - -The new input's pinned sha lands in the agent's `flake.lock` (also tracked + part of the proposal). Build failures from a broken `flake.nix` surface as a `failed/` annotated tag, so the worst case is a rejected deploy — not a silently-broken agent. - -Each proposed repo has an `applied` git remote pre-configured pointing at the read-only mirror of what's deployed. Useful patterns: - -- `git -C /agents//config fetch applied` — refresh the local copy of every deployed/failed/denied tag. -- `git -C /agents//config log applied/main --oneline` — every successful deploy of this agent. -- `git -C /agents//config show applied/refs/tags/deployed/` — the tree that was deployed for approval ``. -- `git -C /agents//config show applied/refs/tags/failed/` — annotated tag body is the build error from a rejected rebuild. -- `git -C /agents//config show applied/refs/tags/denied/` — annotated tag body is the operator's reason for denial. -- `git -C /agents//config rebase applied/main` — base your in-flight work on whatever's actually deployed (useful after a failed/denied pile-up). - -System-wide view: `/meta/` is a read-only mirror of the deployed-agents flake. `git -C /meta log --oneline` is the deploy log for every agent across the swarm; `cat /meta/flake.lock` shows which sha each agent is pinned at right now. - -Tag scheme on every approval id: `proposal → approved → building → deployed | failed`, plus `denied` as a terminal alternative to `approved`. `applied/main` only advances on `deployed/*`, so a failed build does not corrupt the agent — submit a fix as a new commit and a fresh `request_apply_commit`. - -Sub-agents are NOT trusted by default. When one asks for a config change (new packages, env vars, etc.), verify the request before staging: - -- Does it match what the agent actually needs to do its declared role? -- Is the package legitimate (no obviously-malicious names, no overly broad permissions)? -- Are there cheaper / safer alternatives that don't need a config edit? -- If the change has any ambiguity or could affect other agents / the host, surface the question to the operator (see below) instead of staging it yourself. - -You're the policy gate between sub-agents and the operator's approval queue — the operator clicks ◆ APPR0VE on your commits, so don't submit changes you wouldn't defend. - -Two ways to talk to the operator: `send(to: "operator", ...)` for fire-and-forget status / pointers (surfaces in the operator inbox), or `ask(question, options?)` when you need a decision (omit `to`, or pass `to: "operator"`). `ask` is non-blocking — it queues the question and returns an id immediately; the answer arrives on a future turn as a `question_answered` system event. Prefer `ask` over an open-ended `send` for anything you actually need to wait on. Same primitive can target a sub-agent (`to: ""`) when you need a structured answer from a peer rather than free-form chat. +Approval boundary: lifecycle ops on *existing* direct children (`kill`, `start`, `restart`) are at your discretion — no operator approval needed (requires `lifecycle` tool group). *Creating* a new agent (two-step: `request_init_config` + `request_apply_commit`) and *changing* any agent's config (`request_apply_commit`) both go through the approval queue (requires `approvals` tool group). The operator only signs off on changes; you run the day-to-day. Messages from sender `system` are hyperhive helper events (JSON body, `event` field discriminates): `approval_resolved`, `config_ready`, `spawned`, `rebuilt`, `killed`, `destroyed`, `container_crash`, `needs_login`, `logged_in`, `needs_update`, `question_asked`, `question_answered`. Use these to react to lifecycle changes: -- `config_ready` — the proposed config repo for a new agent was just seeded (post-`InitConfig` approval). Review and edit `/agents//config/agent.nix`, commit your changes, then call `request_apply_commit` with the commit sha — this will create the container on approval (first spawn) and rebuild on every subsequent deploy. -- `needs_login` — agent has no claude session yet. You can't help directly (login is interactive OAuth on the operator side); flag the operator if it's been long. -- `logged_in` — agent just completed login; first useful turn is imminent. Good time to brief them on what to do. +- `config_ready` — the proposed config repo for a new agent was just seeded (post-`InitConfig` approval). Review and edit `/agents//config/agent.nix`, commit your changes, then call `request_apply_commit` with the commit sha. +- `needs_login` — agent has no claude session yet. Flag the operator if it's been long. +- `logged_in` — agent just completed login; first useful turn is imminent. - `needs_update` — agent's flake rev is stale. Call `update(name)` to rebuild — it's idempotent and doesn't need approval. - `container_crash` — restart with `start(name)`. If it crashes again, ask the operator. -- otherwise greet freshly-spawned agents, retry failed rebuilds, pick up the operator's answer to questions you asked. - +- otherwise greet freshly-spawned agents, retry failed rebuilds, pick up answers to questions you asked. Durable knowledge: - - write to `/agents/{label}/state/notes.md` (free-form) or any other path under `/agents/{label}/state/`. That directory is bind-mounted from the host and persists across container destroy/recreate — claude's `--continue` session only carries short-term context, but `/agents/{label}/state/` is forever. Read it back at the start of relevant turns to remember things across resets. - - - -- Your own: `/state/notes.md` (free-form) or anything else under `/state/`. Bind-mounted from the host — survives destroy/recreate. Claude's `--continue` session only carries short-term context; `/state/` is forever. Good place for a roster of active sub-agents, ongoing initiatives, decisions you've made. -- Sub-agents': every sub-agent has its own `/state/` too. From your container that's `/agents//state/` (your `/agents` mount is RW), so you can read what they've recorded and write notes for them when you need to leave a heads-up or task list. - Claude session (OAuth credentials) lives at `/root/.claude/` and persists across restarts. - **Shared space**: `/shared` is accessible to all agents (read/write). Only put things here you're willing to lose — other agents may delete them. Use for explicit cross-agent communication or shared artifacts when appropriate. **Hive knowledge**: `/knowledge` is a read-only bind-mount of the `internal/knowledge` repo on the forge. It contains hive-wide reference documents (conventions, runbooks, shared notes). Read files there for context; to contribute, fork `internal/knowledge` on the forge and open a PR. - **Code forge**: a private Forgejo at `http://localhost:3000` is available when `/agents/{label}/state/forge-token` exists. You have your own user account (named `{label}`). Use `hive-forge` (see below) for all forge operations — issues, PRs, comments, labels, etc. For git operations use plain `git` directly against `http://localhost:3000//.git` (credentials are pre-configured). The `hive-forge` CLI helper wraps common Forgejo API operations: `view`, `issue`, `issue-create`, `issue-edit`, `pr`, `pr-create`, `comment`, `comments`, `comment-show`, `comment-edit`, `assign`, `close`, `labels`, `lint`, `list`, `milestone`, `pr-reviews`, `branches`, `tree-sha`, `diff`, `subscription`, `attach-issue`, `attach-comment`. `lint ` runs triage queries (`unassigned`, `no-reviewer --reviewer NAME`, `stale-branches [--days N]`, `assignments [--user NAME]`). Default repo comes from `HIVE_FORGE_REPO`; pass `-r ` (global flag, works before or after the verb) to target a different repo. Every verb takes `--help` for its full signature. To create a PR: `hive-forge pr-create --title "..." --head [--base main] [--body "..." | --body-file ] [--draft] [--push [--remote forge]]` — prints the PR URL. Add `--push` to also `git push` the head branch before the API call (default remote: `forge`); the noisy post-push "Create a pull request" hint is suppressed since we print the canonical URL ourselves. To create an issue: `hive-forge issue-create --title "..." [--body "..." | --body-file ] [--assignee ]`. `--body-file -` means stdin, so a HEREDOC body works naturally: `hive-forge comment --body-file - < ` / `hive-forge attach-comment ` — both print the `browser_download_url`. Key ops: `hive-forge diff ` prints the unified diff; `hive-forge subscription [--watch|--ignore|--unwatch]` manages repo watch state. Note: forge notifications are delivered via the internal message daemon. - -Keep messages short — a few sentences each. For anything big (file listings, long diffs, transcripts, analysis): write the payload to `/agents/{label}/state/` and `send` a short pointer ("dropped the cluster audit in /agents/{label}/state/cluster-audit-2026-05.md, headline: 3 nodes over 80% mem"). The manager + operator can read your state from the host as `/agents/{label}/state/`. Sub-agent peers can't read each other's state directly — go through the manager if a payload needs to reach another sub-agent. - - - -Keep messages short — a few sentences each. For anything big (digests, agent rosters, plans, transcripts) write the payload to a file and `send` a short pointer: - -- To a sub-agent X: write to `/agents/X/state/` and tell them "see /agents/X/state/". -- To the operator: write to your own `/state/` (host path `/var/lib/hyperhive/agents/{label}/state/`) and tell them where to look. -- For shared artifacts (coordination, common reference data): write to `/shared/`. Only put things here you're willing to lose — other agents may delete them. - -A one-line headline + the file path beats a wall-of-text every time — it survives context compaction and the operator can read it in their own time. - +Keep messages short — a few sentences each. For anything big (file listings, long diffs, transcripts, analysis): write the payload to `/agents/{label}/state/` and `send` a short pointer ("dropped the cluster audit in /agents/{label}/state/cluster-audit-2026-05.md, headline: 3 nodes over 80% mem"). The operator can read your state from the host as `/agents/{label}/state/`. Sub-agent peers can't read each other's state directly — coordinate through shared space or a common parent. When your inbox has a message, handle it and stop. Don't narrate intent — act. diff --git a/hive-ag3nt/src/bin/hive.rs b/hive-ag3nt/src/bin/hive.rs index bcd7c71e..b90494ca 100644 --- a/hive-ag3nt/src/bin/hive.rs +++ b/hive-ag3nt/src/bin/hive.rs @@ -1,10 +1,7 @@ -//! Unified hyperhive harness binary. Picks role from `HIVE_ROLE` -//! (`"agent"` | `"manager"`), dispatches one of three subcommands -//! (`serve` / `mcp` / `wake`), and runs the turn loop through a -//! generic `Surface` trait so both wire surfaces stay in lockstep. -//! -//! Architecture (single-binary rationale, Surface-trait + zero-sized -//! type tags, boot wiring, turn-outcome branch) lives in +//! Unified hyperhive harness binary. Dispatches one of three subcommands +//! (`serve` / `mcp` / `wake`). There is one role: agent. The `Surface` +//! trait + `AgentSurface` zero-sized type tag keeps the turn loop +//! generic and testable. Architecture lives in //! [`docs/turn-loop.md::Harness binary shape`](../../../docs/turn-loop.md). use std::path::{Path, PathBuf}; @@ -13,7 +10,7 @@ use std::time::Duration; use hive_ag3nt::web_ui::TurnLock; -use anyhow::{Result, bail}; +use anyhow::Result; use clap::{Parser, Subcommand}; use hive_ag3nt::events::{Bus, LiveEvent, TurnState}; use hive_ag3nt::login::{self, LoginState}; @@ -21,15 +18,10 @@ use hive_ag3nt::turn_stats::TurnStats; use hive_ag3nt::{ DEFAULT_SOCKET, DEFAULT_WEB_PORT, client, mcp, plugins, serve_common, turn, web_ui, }; -use hive_sh4re::{ - AgentRequest, AgentResponse, HelperEvent, ManagerRequest, ManagerResponse, SYSTEM_SENDER, -}; +use hive_sh4re::{AgentRequest, AgentResponse, HelperEvent, SYSTEM_SENDER}; #[derive(Parser)] -#[command( - name = "hive", - about = "hyperhive harness — role from $HIVE_ROLE (agent|manager)" -)] +#[command(name = "hive", about = "hyperhive harness")] struct Cli { /// Path to the per-agent MCP socket (bind-mounted from the host). #[arg(long, global = true, default_value = DEFAULT_SOCKET)] @@ -48,16 +40,14 @@ enum Cmd { #[arg(long, default_value_t = 1000)] poll_ms: u64, }, - /// Run this role's MCP server on stdio. Spawned by `claude` via + /// Run the MCP server on stdio. Spawned by `claude` via /// `--mcp-config`; tools dispatch through `/run/hive/mcp.sock` back /// into the hyperhive broker. Mcp, /// Inject a wake-up event into this harness's inbox so the next /// turn fires with the given body. Intended for extra MCP servers /// / helpers (matrix bridge, scraper, webhook listener, etc.) that - /// need to nudge claude on external events. Available on both - /// agent and manager roles; mirrors the `AgentRequest::Wake` / - /// `ManagerRequest::Wake` pair already on the wire. + /// need to nudge claude on external events. Wake { #[arg(long)] from: String, @@ -67,20 +57,6 @@ enum Cmd { }, } -#[derive(Copy, Clone)] -enum Role { - Agent, - Manager, -} - -fn resolve_role() -> Result { - match std::env::var("HIVE_ROLE").as_deref() { - Ok("agent") | Err(_) => Ok(Role::Agent), - Ok("manager") => Ok(Role::Manager), - Ok(other) => bail!("unknown HIVE_ROLE={other:?}; expected 'agent' or 'manager'"), - } -} - #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt() @@ -91,26 +67,11 @@ async fn main() -> Result<()> { .init(); let cli = Cli::parse(); - let role = resolve_role()?; - // Generic dispatch: one `serve_main` / `wake` body, two - // monomorphisations driven by the `Surface` type parameter. See - // `docs/turn-loop.md::Surface trait + zero-sized type tags`. - match (role, cli.cmd) { - (Role::Agent, Cmd::Serve { poll_ms }) => { - serve_main::(&cli.socket, poll_ms).await - } - (Role::Manager, Cmd::Serve { poll_ms }) => { - serve_main::(&cli.socket, poll_ms).await - } - (Role::Agent, Cmd::Mcp) => mcp::serve_agent_stdio(cli.socket).await, - (Role::Manager, Cmd::Mcp) => mcp::serve_agent_stdio(cli.socket).await, - (Role::Agent, Cmd::Wake { from, body }) => { - wake::(&cli.socket, from, body).await - } - (Role::Manager, Cmd::Wake { from, body }) => { - wake::(&cli.socket, from, body).await - } + match cli.cmd { + Cmd::Serve { poll_ms } => serve_main::(&cli.socket, poll_ms).await, + Cmd::Mcp => mcp::serve_agent_stdio(cli.socket).await, + Cmd::Wake { from, body } => wake::(&cli.socket, from, body).await, } } @@ -186,16 +147,11 @@ enum RecvOutcome { TransportError, } -/// Per-role wire surface. Two impls — `AgentSurface`, `ManagerSurface` -/// — wrap the disjoint `Request`/`Response` enums plus a handful of -/// boot-time constants that vary by role. Every other function in this -/// binary that talks to the broker goes through this trait so the turn -/// loop itself has zero per-role branches. +/// Wire surface abstraction. `AgentSurface` is the only impl — the trait +/// exists to keep the turn loop generic and testable. Every function that +/// talks to the broker goes through this so there are zero hard-coded +/// `AgentRequest` / `AgentResponse` references in the turn loop itself. trait Surface { - /// MCP flavor passed to `TurnFiles::prepare`. Picks which static - /// system-prompt block + tool registration goes into the spawned - /// `claude` process. - const FLAVOR: mcp::Flavor; /// Ack the in-flight turn. Logs warnings on transport/broker /// errors but never propagates — turn loop continues either way. fn ack_turn(socket: &Path) -> impl Future; @@ -237,12 +193,11 @@ trait Surface { // ---------- AgentSurface ---------- -/// Zero-sized type tag for the sub-agent wire surface. +/// Zero-sized type tag for the agent wire surface. /// Talks `AgentRequest` / `AgentResponse`. struct AgentSurface; impl Surface for AgentSurface { - const FLAVOR: mcp::Flavor = mcp::Flavor::Agent; async fn ack_turn(socket: &Path) { match client::request::<_, AgentResponse>(socket, &AgentRequest::AckTurn).await { @@ -382,159 +337,11 @@ impl Surface for AgentSurface { } } -// ---------- ManagerSurface ---------- - -/// Zero-sized type tag for the manager wire surface. -/// Talks `ManagerRequest` / `ManagerResponse`. -struct ManagerSurface; - -impl Surface for ManagerSurface { - const FLAVOR: mcp::Flavor = mcp::Flavor::Manager; - - async fn ack_turn(socket: &Path) { - match client::request::<_, ManagerResponse>(socket, &ManagerRequest::AckTurn).await { - Ok(ManagerResponse::Ok) => {} - Ok(ManagerResponse::Err { message }) => { - tracing::warn!(%message, "ack_turn rejected by broker"); - } - Ok(other) => tracing::warn!(?other, "ack_turn unexpected response"), - Err(e) => tracing::warn!(error = ?e, "ack_turn transport error"), - } - } - - async fn requeue_inflight(socket: &Path) { - match client::request::<_, ManagerResponse>(socket, &ManagerRequest::RequeueInflight).await - { - Ok(ManagerResponse::Ok) => {} - Ok(ManagerResponse::Err { message }) => { - tracing::warn!(%message, "requeue_inflight rejected by broker"); - } - Ok(other) => tracing::warn!(?other, "requeue_inflight unexpected response"), - Err(e) => tracing::warn!(error = ?e, "requeue_inflight transport error"), - } - } - - async fn inbox_unread(socket: &Path) -> u64 { - match client::request::<_, ManagerResponse>(socket, &ManagerRequest::Status).await { - Ok(ManagerResponse::Status { unread }) => unread, - _ => 0, - } - } - - async fn post_turn_counts(socket: &Path) -> (Option, Option) { - let threads = match client::request::<_, ManagerResponse>( - socket, - &ManagerRequest::GetLooseEnds { agent: None }, - ) - .await - { - Ok(ManagerResponse::LooseEnds { loose_ends }) => u64::try_from(loose_ends.len()).ok(), - _ => None, - }; - let reminders = match client::request::<_, ManagerResponse>( - socket, - &ManagerRequest::CountPendingReminders { agent: None }, - ) - .await - { - Ok(ManagerResponse::PendingRemindersCount { count }) => Some(count), - _ => None, - }; - (threads, reminders) - } - - async fn send_to_parent(socket: &Path, body: String) { - let res = client::request::<_, ManagerResponse>( - socket, - &ManagerRequest::Send { - to: hive_sh4re::PARENT_RECIPIENT.into(), - body, - in_reply_to: None, - }, - ) - .await; - if let Err(e) = res { - tracing::warn!(error = ?e, "failed to notify parent of turn failure"); - } - } - - async fn self_wake(socket: &Path) { - let res = client::request::<_, ManagerResponse>( - socket, - &ManagerRequest::Wake { - from: "self".into(), - body: "continue".into(), - transient: false, - }, - ) - .await; - match res { - Ok(ManagerResponse::Ok) => { - tracing::info!("request_next_turn: injected self-continue wake"); - } - Ok(ManagerResponse::Err { message }) => { - tracing::warn!(%message, "check_and_inject_continue: wake rejected"); - } - Err(e) => { - tracing::warn!(error = ?e, "check_and_inject_continue: wake transport error"); - } - _ => {} - } - } - - async fn recv_next(socket: &Path) -> RecvOutcome { - let recv: Result = client::request( - socket, - &ManagerRequest::Recv { - wait_seconds: Some(180), - max: None, - }, - ) - .await; - match recv { - Ok(ManagerResponse::Messages { messages }) if !messages.is_empty() => { - let first = messages.into_iter().next().expect("checked non-empty"); - RecvOutcome::Message(first) - } - Ok(ManagerResponse::Messages { .. }) => RecvOutcome::Empty, - Ok(ManagerResponse::Err { message }) => { - tracing::warn!(%message, "recv error"); - RecvOutcome::TransportError - } - Ok(other) => { - tracing::warn!(?other, "recv produced unexpected response kind"); - RecvOutcome::TransportError - } - Err(e) => { - tracing::warn!(error = ?e, "recv failed; retrying"); - RecvOutcome::TransportError - } - } - } - - async fn wake_external(socket: &Path, from: String, body: String) -> Result<()> { - let resp: ManagerResponse = client::request( - socket, - &ManagerRequest::Wake { - from, - body, - transient: false, - }, - ) - .await?; - match resp { - ManagerResponse::Ok => Ok(()), - ManagerResponse::Err { message } => anyhow::bail!("wake: {message}"), - other => anyhow::bail!("wake: unexpected response {other:?}"), - } - } -} - // ---------- generic turn loop ---------- -/// Per-role boot — wires up the web UI, login state, stats, plugins, -/// forge notifier, and either drops into `serve_loop` directly -/// (`Online`) or parks on the login flow first (`NeedsLogin`). See +/// Boot — wires up the web UI, login state, stats, plugins, forge +/// notifier, and either drops into `serve_loop` directly (`Online`) or +/// parks on the login flow first (`NeedsLogin`). See /// `docs/turn-loop.md::Boot wiring`. async fn serve_main(socket: &Path, poll_ms: u64) -> Result<()> { let port = std::env::var("HIVE_PORT") @@ -559,13 +366,12 @@ async fn serve_main(socket: &Path, poll_ms: u64) -> Result<()> { bus.seed_usage(ctx, cost); } } - let files = turn::TurnFiles::prepare(socket, &label, S::FLAVOR).await?; + let files = turn::TurnFiles::prepare(socket, &label).await?; let turn_lock: TurnLock = Arc::new(tokio::sync::Mutex::new(())); - // Plugin install runs role-agnostic: failures come back as a - // Vec and we route each through `` via the same - // `send_to_parent` failure-notify path the turn loop uses. The - // broker resolves `` per `topology::parent_of`; root - // agents and the manager fall through to operator. + // Plugin install failures come back as a Vec — route each + // through `` via the `send_to_parent` failure-notify path. + // The broker resolves `` per `topology::parent_of`; + // root agents fall through to operator. for failure in plugins::install_configured(socket).await { S::send_to_parent(socket, failure).await; } diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index dee9bee4..74afc384 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -1744,13 +1744,11 @@ pub const SERVER_NAME: &str = "hyperhive"; /// should plan in /state notes instead. pub const ALLOWED_BUILTIN_TOOLS: &[&str] = &["Edit", "Glob", "Grep", "Read", "Write"]; -/// Which MCP tool surface to advertise via `--allowedTools`. The agent -/// list is the strict subset of the manager list, so we just thread the -/// flavor through. +/// Kept for API stability — was two-variant (Agent/Manager) but the manager +/// role no longer exists. Only `Agent` remains; everything is perms + caps. #[derive(Debug, Clone, Copy)] pub enum Flavor { Agent, - Manager, } /// Env var written by the meta renderer with a comma-separated list of @@ -1800,18 +1798,11 @@ fn allowed_capability_tools() -> Vec { /// token is matched (case-insensitive) against the `ToolGroup` serde names /// (`messaging`, `meta`, `inbox`, `lifecycle`, `approvals`, `scheduling`, /// `diagnostics`, `execution`). Unrecognised tokens are logged and skipped. -/// Falls back to the flavor default when the env var is absent or empty. -fn effective_tool_groups(flavor: Flavor) -> Vec { +/// Falls back to `AGENT_DEFAULT` when the env var is absent or empty. +fn effective_tool_groups() -> Vec { let raw = match std::env::var(TOOL_GROUPS_ENV) { Ok(v) if !v.trim().is_empty() => v, - _ => { - // No env var — use the flavor default unchanged. - let defaults = match flavor { - Flavor::Agent => hive_sh4re::ToolGroup::AGENT_DEFAULT, - Flavor::Manager => hive_sh4re::ToolGroup::MANAGER_DEFAULT, - }; - return defaults.to_vec(); - } + _ => return hive_sh4re::ToolGroup::AGENT_DEFAULT.to_vec(), }; let mut groups = Vec::new(); for token in raw.split(',') { @@ -1829,12 +1820,9 @@ fn effective_tool_groups(flavor: Flavor) -> Vec { if groups.is_empty() { tracing::warn!( "{TOOL_GROUPS_ENV} set but contained no recognised groups; \ - falling back to flavor default" + falling back to AGENT_DEFAULT" ); - return match flavor { - Flavor::Agent => hive_sh4re::ToolGroup::AGENT_DEFAULT.to_vec(), - Flavor::Manager => hive_sh4re::ToolGroup::MANAGER_DEFAULT.to_vec(), - }; + return hive_sh4re::ToolGroup::AGENT_DEFAULT.to_vec(); } groups } @@ -1872,8 +1860,8 @@ pub fn allowed_mcp_tools(groups: &[hive_sh4re::ToolGroup]) -> Vec { /// Combined allow-list passed to `--allowedTools` (auto-approve) — covers /// both the built-ins and the MCP surface. #[must_use] -pub fn allowed_tools_arg(flavor: Flavor) -> String { - let groups = effective_tool_groups(flavor); +pub fn allowed_tools_arg() -> String { + let groups = effective_tool_groups(); // Base built-ins always present. let mut all: Vec = ALLOWED_BUILTIN_TOOLS .iter() @@ -1903,15 +1891,7 @@ pub fn allowed_tools_arg(flavor: Flavor) -> String { /// `WebFetch`/`WebSearch` when the `web_tools` group is active). #[must_use] pub fn builtin_tools_arg() -> String { - builtin_tools_arg_for_flavor(Flavor::Agent) -} - -/// Flavor-aware variant used by `turn.rs` via `builtin_tools_arg`. Reads -/// the effective tool groups for `flavor` so `--tools` matches what -/// `--allowedTools` includes for the same session. -#[must_use] -pub fn builtin_tools_arg_for_flavor(flavor: Flavor) -> String { - let groups = effective_tool_groups(flavor); + let groups = effective_tool_groups(); let mut tools: Vec<&str> = ALLOWED_BUILTIN_TOOLS.to_vec(); for group in &groups { for t in group.builtin_tools() { diff --git a/hive-ag3nt/src/prompt.rs b/hive-ag3nt/src/prompt.rs index e4e88d30..8173fc7e 100644 --- a/hive-ag3nt/src/prompt.rs +++ b/hive-ag3nt/src/prompt.rs @@ -1,38 +1,31 @@ //! 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 +//! assembles the final prompt (always "agent" role — there is only one +//! role). Marker grammar + placeholder substitution rules in //! `docs/turn-loop.md::On-boot files` (`claude-system-prompt.md`). use std::path::{Path, PathBuf}; 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`] +/// Assemble the system prompt for a given 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`). Substitution placeholders + /// marker grammar documented in /// `docs/turn-loop.md::On-boot files` (`claude-system-prompt.md`). #[must_use] pub fn render( template: &str, - flavor: Flavor, label: &str, operator_pronouns: &str, hive_name: Option<&str>, swarm_name: Option<&str>, ) -> String { - let target = match flavor { - Flavor::Agent => "agent", - Flavor::Manager => "manager", - }; - let body = filter_role_blocks(template, target); + let body = filter_role_blocks(template, "agent"); let qualified = crate::identity::qualify(label); let hive_identity = hive_name .filter(|n| !n.is_empty()) @@ -113,7 +106,7 @@ fn parse_close_marker(line: &str) -> Option<&str> { /// # Errors /// /// Returns an error if the system prompt file cannot be written. -pub async fn write_system_prompt(_socket: &Path, label: &str, flavor: Flavor) -> Result { +pub async fn write_system_prompt(_socket: &Path, label: &str) -> Result { let parent = crate::paths::config_dir(); tokio::fs::create_dir_all(&parent).await.ok(); let pronouns = std::env::var("HIVE_OPERATOR_PRONOUNS").unwrap_or_else(|_| "she/her".to_owned()); @@ -133,7 +126,6 @@ pub async fn write_system_prompt(_socket: &Path, label: &str, flavor: Flavor) -> let swarm_name = crate::identity::swarm_name(); let body = render( &template, - flavor, label, &pronouns, hive_name.as_deref(), @@ -279,7 +271,6 @@ shared closer // harness already relied on. let rendered = render( &PRODUCTION_TEMPLATE, - Flavor::Agent, "alice", "they/them", None, @@ -292,66 +283,32 @@ shared closer } #[test] - fn render_agent_excludes_manager_only_tools() { - // 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. + fn render_no_role_markers_in_output() { + // No raw role markers should survive into the rendered prompt. let rendered = render( &PRODUCTION_TEMPLATE, - Flavor::Agent, "alice", "she/her", None, None, ); - assert!(!rendered.contains("request_init_config")); - assert!(!rendered.contains("request_apply_commit")); - assert!(!rendered.contains("get_logs")); - // Sanity: shared tools DO appear. + assert!(!rendered.contains("