harness: unify agent + manager prompts into single template (closes #519)
This commit is contained in:
parent
88476976b7
commit
01af5003d1
9 changed files with 325 additions and 75 deletions
|
|
@ -156,9 +156,12 @@ in `migrate.rs` (idempotent, marker-guarded).
|
|||
|
||||
### E — prompt + tools
|
||||
|
||||
- `prompts/manager.md` vs `prompts/agent.md` — two separate system
|
||||
prompts. **Per-agent cap list** of what the agent can do, rendered
|
||||
into a single parametrised prompt at boot.
|
||||
- `prompts/system.md` with `<!-- role:agent -->` / `<!-- role:manager -->`
|
||||
marker blocks, assembled by `hive_ag3nt::prompt::render` based on
|
||||
flavor (closes #519). **Per-agent cap list** of what the agent can
|
||||
do — already a single parametrised prompt; once #513 lands the
|
||||
marker grammar grows `cap:<group>` blocks the renderer reads from
|
||||
the per-agent ToolGroup set.
|
||||
- `mcp.rs::Flavor::{Agent, Manager}` controls which MCP tools claude
|
||||
sees. Already structured this way internally — the per-flavour
|
||||
allow-list becomes a per-cap-set lookup.
|
||||
|
|
|
|||
|
|
@ -360,7 +360,8 @@ updates the manager itself.
|
|||
|
||||
## Manager policy
|
||||
|
||||
From `hive-ag3nt/prompts/manager.md`: the manager does NOT
|
||||
From `hive-ag3nt/prompts/system.md` (`<!-- role:manager -->` block,
|
||||
rendered via `hive_ag3nt::prompt::render`): the manager does NOT
|
||||
rubber-stamp sub-agent config requests. It verifies (role match,
|
||||
package legitimacy, cheaper alternative, blast radius) before
|
||||
committing and calling `request_apply_commit`.
|
||||
|
|
@ -447,7 +448,8 @@ regular claude turn so the manager can react. Variants
|
|||
asker sees the matching `QuestionAnswered`.
|
||||
|
||||
To add a new event: new `HelperEvent` variant + call sites + update
|
||||
`prompts/manager.md` so the manager knows the new shape.
|
||||
`prompts/system.md` (`<!-- role:manager -->` block, the lifecycle-
|
||||
event list) so the manager knows the new shape.
|
||||
|
||||
## Auto-update on startup
|
||||
|
||||
|
|
|
|||
|
|
@ -163,11 +163,14 @@ socket at `/run/hive/` once at startup:
|
|||
- `claude-settings.json` — the `--settings` blob (auto-compact and
|
||||
auto-memory off, effortLevel medium).
|
||||
- `claude-system-prompt.md` — rendered from
|
||||
`hive-ag3nt/prompts/{agent,manager}.md` with `{label}` and
|
||||
`{operator_pronouns}` substituted. Pronouns come from
|
||||
`HIVE_OPERATOR_PRONOUNS` env (set by the meta flake from
|
||||
`services.hive-c0re.operatorPronouns`, default `she/her`).
|
||||
Passed via `--system-prompt-file`.
|
||||
`hive-ag3nt/prompts/system.md` by `hive_ag3nt::prompt::render`:
|
||||
HTML-comment markers (`<!-- role:agent -->...<!-- /role:agent -->`,
|
||||
same for `role:manager`) gate the role-specific blocks (closes
|
||||
#519); everything else is shared. Then `{label}` and
|
||||
`{operator_pronouns}` get substituted in the assembled output.
|
||||
Pronouns come from `HIVE_OPERATOR_PRONOUNS` env (set by the meta
|
||||
flake from `services.hive-c0re.operatorPronouns`, default
|
||||
`she/her`). Passed via `--system-prompt-file`.
|
||||
|
||||
The shared per-turn plumbing lives in `hive_ag3nt::turn::{write_mcp_config,
|
||||
write_settings, write_system_prompt, run_turn, drive_turn,
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
You are hyperhive agent `{label}` in a multi-agent system. The operator (recipient `operator` in `send`, the human at the dashboard) uses **{operator_pronouns}** pronouns — use them naturally when you refer to them in third person (e.g. when relaying to a peer or the manager).
|
||||
|
||||
Tools (hyperhive surface):
|
||||
|
||||
- `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). Optional `in_reply_to: <message-id>` 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: "manager", …)`) which is always reachable.
|
||||
- (some agents only) **extra MCP tools** surfaced as `mcp__<server>__<tool>` — 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__ask(question, options?, multi?, ttl_seconds?, to?)` — surface a structured question to the human operator (default, or `to: "operator"`) OR a peer agent (`to: "<agent-name>"`). 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: "<your-name>", ...)`. 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 <you>]` 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__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"`). 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. 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.
|
||||
|
||||
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 + 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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
**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}`); credentials for the `tea` CLI are pre-configured at boot. Use `tea repos create`, `tea pulls create --base main --head <branch>`, `tea pulls list`, `tea issues create`, etc. for any persistent code work — git repos that should outlive a single turn, code you want a peer or the operator to review, anything you'd otherwise jam into `/shared`. Falls back to plain `git`/`curl` if `tea` doesn't fit; the REST API is at `http://localhost:3000/api/v1/` with the same token (`Authorization: token $(cat /agents/{label}/state/forge-token)`).
|
||||
|
||||
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`, `milestone`, `pr-reviews`, `branches`, `tree-sha`, `diff`, `subscription`, `attach-issue`, `attach-comment`. `lint <sub>` runs triage queries (`unassigned`, `no-reviewer --reviewer NAME`, `stale-branches`, `assignments`). Default repo comes from `HIVE_FORGE_REPO`; pass `-r <repo>` (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 <branch> [--base main] [--body "..." | --body-file <path>] [--draft]` — prints the PR URL. To create an issue: `hive-forge issue-create --title "..." [--body "..." | --body-file <path>] [--assignee <user>]`. `--body-file -` means stdin, so a HEREDOC body works naturally: `hive-forge comment <num> --body-file - <<EOF ... EOF`. To attach a file: `hive-forge attach-issue <number> <file>` / `hive-forge attach-comment <comment-id> <file>` — both print the `browser_download_url`. Key ops: `hive-forge diff <pr>` 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/<descriptive-name>` 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.
|
||||
|
||||
When your inbox has a message, handle it and stop. Don't narrate intent — act.
|
||||
|
|
@ -1,9 +1,18 @@
|
|||
<!-- role:agent -->
|
||||
You are hyperhive agent `{label}` in a multi-agent system. The operator (recipient `operator` in `send`, the human at the dashboard) uses **{operator_pronouns}** pronouns — use them naturally when you refer to them in third person (e.g. when relaying to a peer or the manager).
|
||||
<!-- /role:agent -->
|
||||
<!-- role:manager -->
|
||||
You are the hyperhive manager `{label}` in a multi-agent system. You coordinate sub-agents and relay between them and the operator. The operator (recipient `operator`, the human at the dashboard) uses **{operator_pronouns}** pronouns — use them naturally when you refer to them in third person.
|
||||
<!-- /role:manager -->
|
||||
|
||||
Tools (hyperhive surface):
|
||||
|
||||
- `mcp__hyperhive__recv(wait_seconds?, max?)` — drain inbox messages. Without `wait_seconds` (or with `0`) it returns immediately — a cheap inbox peek you can drop between actions. To **wait** when you have nothing else to do, call with a long wait (e.g. `wait_seconds: 180`, the max) — you'll wake instantly on new work, otherwise return after the timeout. Use that instead of ending the turn or sleeping in a Bash command. `max` (default 1, cap 32) drains several queued messages in one call.
|
||||
- `mcp__hyperhive__send(to, body)` — message an agent (by name), another peer, or the operator (`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).
|
||||
- `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). Optional `in_reply_to: <message-id>` 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: "manager", …)`) which is always reachable.
|
||||
<!-- role:agent -->
|
||||
- (some agents only) **extra MCP tools** surfaced as `mcp__<server>__<tool>` — 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.
|
||||
<!-- /role:agent -->
|
||||
<!-- role:manager -->
|
||||
- `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/<name>/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/<id>` — 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.
|
||||
|
|
@ -17,17 +26,32 @@ Tools (hyperhive surface):
|
|||
- `mcp__hyperhive__edit_schedule(id, body?, description?, interval_seconds?, next_fire_at_unix?, targets_add?, targets_remove?)` — partial-update a schedule's mutable fields (#474). 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 operator (default, or `to: "operator"`) OR a sub-agent (`to: "<agent-name>"`). Returns immediately with a question id; the answer arrives later as a system `question_answered { id, question, answer, answerer }` event in your inbox. Options are advisory: the dashboard always lets the operator type a free-text answer in addition. Set `multi: true` to render options as checkboxes (operator can pick multiple); the answer comes back as `, `-separated. Set `ttl_seconds` to auto-cancel after a deadline (capped at 6h server-side) — on expiry the answer is `[expired]` and `answerer` is `"ttl-watchdog"`. Do not poll inside the same turn — finish the current work and react when the event lands.
|
||||
- `mcp__hyperhive__answer(id, answer)` — answer a question that was routed to YOU (a sub-agent did `ask(to: "manager", ...)`). The triggering event in your inbox is `question_asked { id, asker, question, options, multi }`. The answer surfaces in the asker's inbox as a `question_answered` event.
|
||||
<!-- /role:manager -->
|
||||
- `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: "<agent-name>"`). 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: "<your-name>", ...)`. 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.
|
||||
<!-- role:agent -->
|
||||
- `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 <you>]` 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.)
|
||||
<!-- /role:agent -->
|
||||
<!-- role:manager -->
|
||||
- `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: "<name>"` 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__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). Good for deadline follow-ups — "check whether agent X answered the question I relayed". Large payloads auto-spill to a file under `/state/reminders/`; pass `file_path` to control the destination.
|
||||
- `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 doing (e.g. `"reviewing argus's #341 proposal"`, `"approving lifecycle changes"`, `"idle"`). 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 the target's self-reported `status` text (set via `set_status`) and how long ago it was set. Pass `name` to check on a sub-agent (idle? still working on the task you assigned?) without scrolling the dashboard. Omit `name` for your own identity stamp — useful for boot announcements, state-file headers, cross-agent attribution that won't drift across config reloads.
|
||||
<!-- /role:manager -->
|
||||
- `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"`). 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. 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.
|
||||
<!-- role:agent -->
|
||||
- `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.
|
||||
|
||||
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 + 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.
|
||||
|
||||
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.
|
||||
<!-- /role:agent -->
|
||||
<!-- role:manager -->
|
||||
|
||||
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/hm1nd/config/`; every sub-agent's lives at `/agents/<name>/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.
|
||||
Your own editable config lives at `/agents/{label}/config/`; every sub-agent's lives at `/agents/<name>/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):
|
||||
|
||||
|
|
@ -85,20 +109,41 @@ Messages from sender `system` are hyperhive helper events (JSON body, `event` fi
|
|||
- `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.
|
||||
<!-- /role:manager -->
|
||||
|
||||
Durable knowledge:
|
||||
<!-- role:agent -->
|
||||
|
||||
- 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.
|
||||
<!-- /role:agent -->
|
||||
<!-- role:manager -->
|
||||
|
||||
- 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/<name>/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.
|
||||
<!-- /role:manager -->
|
||||
|
||||
Claude session (OAuth credentials) lives at `/root/.claude/` and persists across restarts.
|
||||
<!-- role:agent -->
|
||||
|
||||
**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.
|
||||
<!-- /role:agent -->
|
||||
|
||||
**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}`); credentials for the `tea` CLI are pre-configured at boot. Use `tea repos create`, `tea pulls create --base main --head <branch>`, `tea pulls list`, `tea issues create`, etc. for any persistent code work — git repos that should outlive a single turn, code you want a peer or the operator to review, anything you'd otherwise jam into `/shared`. Falls back to plain `git`/`curl` if `tea` doesn't fit; the REST API is at `http://localhost:3000/api/v1/` with the same token (`Authorization: token $(cat /agents/{label}/state/forge-token)`).
|
||||
|
||||
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`, `milestone`, `pr-reviews`, `branches`, `tree-sha`, `diff`, `subscription`, `attach-issue`, `attach-comment`. `lint <sub>` runs triage queries (`unassigned`, `no-reviewer --reviewer NAME`, `stale-branches [--days N]`, `assignments [--user NAME]`). Default repo comes from `HIVE_FORGE_REPO`; pass `-r <repo>` (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 <branch> [--base main] [--body "..." | --body-file <path>] [--draft]` — prints the PR URL. To create an issue: `hive-forge issue-create --title "..." [--body "..." | --body-file <path>] [--assignee <user>]`. `--body-file -` means stdin, so a HEREDOC body works naturally: `hive-forge comment <num> --body-file - <<EOF ... EOF`. To attach a file: `hive-forge attach-issue <number> <file>` / `hive-forge attach-comment <comment-id> <file>` — both print the `browser_download_url`. Key ops: `hive-forge diff <pr>` prints the unified diff; `hive-forge subscription [--watch|--ignore|--unwatch]` manages repo watch state. Note: forge notifications are delivered via the internal message daemon.
|
||||
<!-- role:agent -->
|
||||
|
||||
Keep messages short — a few sentences each. For anything big (file listings, long diffs, transcripts, analysis): write the payload to `/agents/{label}/state/<descriptive-name>` 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.
|
||||
<!-- /role:agent -->
|
||||
<!-- role:manager -->
|
||||
|
||||
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/<descriptive-name>` and tell them "see /agents/X/state/<descriptive-name>".
|
||||
- To the operator: write to your own `/state/<descriptive-name>` (host path `/var/lib/hyperhive/agents/hm1nd/state/`) and tell them where to look.
|
||||
- To the operator: write to your own `/state/<descriptive-name>` (host path `/var/lib/hyperhive/agents/{label}/state/`) and tell them where to look.
|
||||
- For shared artifacts (coordination, common reference data): write to `/shared/<descriptive-name>`. Only put things here you're willing to lose — other agents may delete them.
|
||||
|
||||
**Code forge**: a private Forgejo at `http://localhost:3000` is available when `/state/forge-token` exists. You have your own user (`hm1nd`) and so does every sub-agent (one per name). The `tea` CLI is pre-configured at boot. Use it for code work that should survive a turn — a proposed refactor across sub-agents, scratch repos, PRs you want a sub-agent or the operator to review (`tea pulls create --base main --head <branch>`, `tea pulls list`, `tea issues create`). REST API at `http://localhost:3000/api/v1/` with `Authorization: token $(cat /state/forge-token)` for anything `tea` can't express. The `hive-forge` CLI helper wraps common operations: `view`, `issue`, `issue-create`, `issue-edit`, `pr`, `pr-create`, `comment`, `comments`, `comment-show`, `comment-edit`, `assign`, `close`, `labels`, `lint`, `milestone`, `pr-reviews`, `branches`, `tree-sha`, `diff`, `subscription`, `attach-issue`, `attach-comment`. `lint <sub>` runs triage queries (`unassigned`, `no-reviewer --reviewer NAME`, `stale-branches [--days N]`, `assignments [--user NAME]`) — handy for sweeping the backlog without ad-hoc curl + jq. Default repo from `HIVE_FORGE_REPO`; `-r <repo>` (global flag, works before or after the verb) targets a different repo. Each verb takes `--help`. Use `hive-forge pr-create --title "..." --head <branch> [--base main] [--body "..." | --body-file <path>] [--draft]` to open a PR; `hive-forge issue-create --title "..." [--body "..." | --body-file <path>] [--assignee <user>]` to file an issue. `--body-file -` reads stdin, so a HEREDOC body works: `hive-forge comment <n> --body-file - <<EOF ... EOF`. `diff <pr>` prints the unified diff; `subscription [--watch|--ignore|--unwatch]` manages watch state. Forge notifications arrive via the internal message daemon.
|
||||
|
||||
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.
|
||||
<!-- /role:manager -->
|
||||
|
||||
When your inbox has a message, handle it and stop. Don't narrate intent — act.
|
||||
|
|
@ -318,8 +318,9 @@ async fn handle_agent_turn(
|
|||
}
|
||||
|
||||
// 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. `unread` is the count of *other*
|
||||
// (`prompts/system.md` filtered to this agent's role-block via
|
||||
// `hive_ag3nt::prompt::render` → `claude --system-prompt-file`); this
|
||||
// is just the wake signal claude reacts to. `unread` is the count of *other*
|
||||
// messages in the inbox right after this one was popped.
|
||||
// `redelivered` flags messages that were popped in a prior harness
|
||||
// session, never acked, and resurfaced after a restart — a banner
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ pub mod login_session;
|
|||
pub mod mcp;
|
||||
pub mod paths;
|
||||
pub mod plugins;
|
||||
pub mod prompt;
|
||||
pub mod serve_common;
|
||||
pub mod stats;
|
||||
pub mod turn;
|
||||
|
|
|
|||
242
hive-ag3nt/src/prompt.rs
Normal file
242
hive-ag3nt/src/prompt.rs
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
//! System-prompt renderer (closes #519).
|
||||
//!
|
||||
//! Both flavors (agent / manager) used to live in separate files
|
||||
//! (`prompts/agent.md`, `prompts/manager.md`) that drifted in lockstep
|
||||
//! whenever someone updated only one. Now there's a single
|
||||
//! `prompts/system.md` with HTML-comment markers gating role-specific
|
||||
//! blocks; this module assembles the final prompt for a given flavor.
|
||||
//!
|
||||
//! Marker syntax (HTML comments — invisible in rendered markdown,
|
||||
//! distinct from `{label}` / `{operator_pronouns}` placeholders):
|
||||
//!
|
||||
//! ```text
|
||||
//! <!-- role:agent -->
|
||||
//! sub-agent-only paragraph
|
||||
//! <!-- /role:agent -->
|
||||
//!
|
||||
//! shared paragraph
|
||||
//!
|
||||
//! <!-- role:manager -->
|
||||
//! manager-only paragraph
|
||||
//! <!-- /role:manager -->
|
||||
//! ```
|
||||
//!
|
||||
//! Content outside any marker is shared. Nesting is NOT supported;
|
||||
//! a stray opener overrides until its closing tag (or end of file).
|
||||
//! When #513 lands the marker grammar can grow `cap:<group>` blocks
|
||||
//! the same way without touching the renderer surface (the role
|
||||
//! distinction folds into a per-cap-set lookup).
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::mcp::Flavor;
|
||||
|
||||
const TEMPLATE: &str = include_str!("../prompts/system.md");
|
||||
|
||||
/// Assemble the system prompt for a given flavor + label + pronouns.
|
||||
/// Pure function — no I/O. Splits out from [`write_system_prompt`] so
|
||||
/// the marker logic + substitution is unit-testable in isolation.
|
||||
#[must_use]
|
||||
pub fn render(flavor: Flavor, label: &str, operator_pronouns: &str) -> String {
|
||||
let target = match flavor {
|
||||
Flavor::Agent => "agent",
|
||||
Flavor::Manager => "manager",
|
||||
};
|
||||
let body = filter_role_blocks(TEMPLATE, target);
|
||||
body.replace("{label}", label)
|
||||
.replace("{operator_pronouns}", operator_pronouns)
|
||||
}
|
||||
|
||||
/// Walk `template` line-by-line. Inside a `<!-- role:X -->` block,
|
||||
/// suppress all lines unless `X == target`. Marker lines themselves are
|
||||
/// always elided from the output. Unbalanced openers (no matching
|
||||
/// closer) hold the suppression state until end-of-file.
|
||||
fn filter_role_blocks(template: &str, target: &str) -> String {
|
||||
let mut out = String::with_capacity(template.len());
|
||||
// None = outside any block; Some(role) = inside role-tagged block.
|
||||
let mut active_role: Option<&str> = None;
|
||||
for line in template.lines() {
|
||||
let trimmed = line.trim();
|
||||
if let Some(role) = parse_open_marker(trimmed) {
|
||||
active_role = Some(role);
|
||||
continue;
|
||||
}
|
||||
if parse_close_marker(trimmed).is_some() {
|
||||
active_role = None;
|
||||
continue;
|
||||
}
|
||||
let include = match active_role {
|
||||
None => true,
|
||||
Some(role) => role == target,
|
||||
};
|
||||
if include {
|
||||
out.push_str(line);
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `<!-- role:agent -->` → `Some("agent")`. Anything else returns
|
||||
/// None. Whitespace inside the marker is tolerated so a future
|
||||
/// author's `<!--role:foo-->` (no spaces) still parses; the dashboard
|
||||
/// markdown renderer is equally lenient.
|
||||
fn parse_open_marker(line: &str) -> Option<&str> {
|
||||
let inside = line.strip_prefix("<!--")?.strip_suffix("-->")?.trim();
|
||||
let role = inside.strip_prefix("role:")?.trim();
|
||||
// Reject closer-looking content ("/role:..." would start with "/")
|
||||
// so `/role:agent` doesn't accidentally match here.
|
||||
if role.starts_with('/') {
|
||||
return None;
|
||||
}
|
||||
Some(role)
|
||||
}
|
||||
|
||||
/// `<!-- /role:agent -->` → `Some("agent")`. Mirror of
|
||||
/// [`parse_open_marker`] for the closing tag.
|
||||
fn parse_close_marker(line: &str) -> Option<&str> {
|
||||
let inside = line.strip_prefix("<!--")?.strip_suffix("-->")?.trim();
|
||||
inside.strip_prefix("/role:").map(str::trim)
|
||||
}
|
||||
|
||||
/// Write the assembled prompt to a stable path next to the harness
|
||||
/// socket and return the path. The Rust harness passes this path to
|
||||
/// `claude --system-prompt-file` so the per-turn prompts only carry
|
||||
/// the role + tools instructions in the system slot; per-turn prompts
|
||||
/// become much smaller (just the wake-message body).
|
||||
///
|
||||
/// # 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<PathBuf> {
|
||||
let parent = socket.parent().unwrap_or_else(|| Path::new("/run/hive"));
|
||||
tokio::fs::create_dir_all(parent).await.ok();
|
||||
let pronouns = std::env::var("HIVE_OPERATOR_PRONOUNS").unwrap_or_else(|_| "she/her".to_owned());
|
||||
let body = render(flavor, label, &pronouns);
|
||||
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)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const SAMPLE: &str = "\
|
||||
shared opener
|
||||
<!-- role:agent -->
|
||||
agent-only line
|
||||
<!-- /role:agent -->
|
||||
<!-- role:manager -->
|
||||
manager-only line
|
||||
<!-- /role:manager -->
|
||||
shared closer
|
||||
";
|
||||
|
||||
#[test]
|
||||
fn filter_keeps_shared_and_target_role() {
|
||||
let agent = filter_role_blocks(SAMPLE, "agent");
|
||||
assert!(agent.contains("shared opener"));
|
||||
assert!(agent.contains("agent-only line"));
|
||||
assert!(!agent.contains("manager-only line"));
|
||||
assert!(agent.contains("shared closer"));
|
||||
// Marker lines themselves are stripped — no `<!--` left behind.
|
||||
assert!(!agent.contains("<!--"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_for_manager_picks_manager_block() {
|
||||
let manager = filter_role_blocks(SAMPLE, "manager");
|
||||
assert!(manager.contains("shared opener"));
|
||||
assert!(!manager.contains("agent-only line"));
|
||||
assert!(manager.contains("manager-only line"));
|
||||
assert!(manager.contains("shared closer"));
|
||||
assert!(!manager.contains("<!--"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_open_marker_handles_whitespace_variants() {
|
||||
assert_eq!(parse_open_marker("<!-- role:agent -->"), Some("agent"));
|
||||
assert_eq!(parse_open_marker("<!--role:agent-->"), Some("agent"));
|
||||
assert_eq!(parse_open_marker("<!-- role:manager -->"), Some("manager"));
|
||||
// Close tags must NOT match open-tag parser.
|
||||
assert_eq!(parse_open_marker("<!-- /role:agent -->"), None);
|
||||
// Non-markers pass through (return None).
|
||||
assert_eq!(parse_open_marker("just text"), None);
|
||||
assert_eq!(parse_open_marker("<!-- not a role -->"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_close_marker_handles_whitespace_variants() {
|
||||
assert_eq!(parse_close_marker("<!-- /role:agent -->"), Some("agent"));
|
||||
assert_eq!(parse_close_marker("<!--/role:manager-->"), Some("manager"));
|
||||
// Open tags must NOT match close-tag parser.
|
||||
assert_eq!(parse_close_marker("<!-- role:agent -->"), None);
|
||||
assert_eq!(parse_close_marker("just text"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbalanced_opener_suppresses_until_eof() {
|
||||
// Stray opener with no closer — content stays suppressed for
|
||||
// the wrong-role target right through to end-of-file. Real-
|
||||
// file safety net: a typo in a closer doesn't accidentally
|
||||
// dump wrong-flavor content into the active prompt.
|
||||
let template = "shared\n<!-- role:manager -->\nm-only\nstill m-only\n";
|
||||
let agent = filter_role_blocks(template, "agent");
|
||||
assert_eq!(agent, "shared\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_substitutes_label_and_pronouns() {
|
||||
// Real template's first agent line — keeps the renderer
|
||||
// honest about the {label} / {operator_pronouns} pair the
|
||||
// harness already relied on.
|
||||
let rendered = render(Flavor::Agent, "alice", "they/them");
|
||||
assert!(rendered.contains("hyperhive agent `alice`"));
|
||||
assert!(rendered.contains("**they/them** pronouns"));
|
||||
assert!(!rendered.contains("{label}"));
|
||||
assert!(!rendered.contains("{operator_pronouns}"));
|
||||
}
|
||||
|
||||
#[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 (cf. #511 missing-allow-list bug).
|
||||
let rendered = render(Flavor::Agent, "alice", "she/her");
|
||||
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("mcp__hyperhive__recv"));
|
||||
assert!(rendered.contains("mcp__hyperhive__ask"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_manager_includes_manager_only_tools() {
|
||||
let rendered = render(Flavor::Manager, "hm1nd", "she/her");
|
||||
assert!(rendered.contains("request_init_config"));
|
||||
assert!(rendered.contains("request_apply_commit"));
|
||||
assert!(rendered.contains("get_logs"));
|
||||
assert!(rendered.contains("request_schedule_prompt"));
|
||||
assert!(rendered.contains("cancel_schedule"));
|
||||
// Sub-agent-only sections must NOT appear in manager prompt.
|
||||
assert!(!rendered.contains("request_next_turn"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_uses_correct_role_opener() {
|
||||
let agent = render(Flavor::Agent, "alice", "she/her");
|
||||
assert!(agent.starts_with("You are hyperhive agent"));
|
||||
let manager = render(Flavor::Manager, "hm1nd", "she/her");
|
||||
assert!(manager.starts_with("You are the hyperhive manager"));
|
||||
}
|
||||
}
|
||||
|
|
@ -164,11 +164,10 @@ pub async fn write_settings(socket: &Path) -> Result<PathBuf> {
|
|||
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).
|
||||
/// Thin re-export of [`crate::prompt::write_system_prompt`] for
|
||||
/// callers that already import this module. The actual rendering +
|
||||
/// marker-block logic lives in `prompt.rs` (closes #519); this is
|
||||
/// just the public entry point the binaries call.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
|
|
@ -178,20 +177,7 @@ pub async fn write_system_prompt(
|
|||
label: &str,
|
||||
flavor: mcp::Flavor,
|
||||
) -> Result<PathBuf> {
|
||||
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 pronouns = std::env::var("HIVE_OPERATOR_PRONOUNS").unwrap_or_else(|_| "she/her".to_owned());
|
||||
let body = template
|
||||
.replace("{label}", label)
|
||||
.replace("{operator_pronouns}", &pronouns);
|
||||
let path = parent.join("claude-system-prompt.md");
|
||||
tokio::fs::write(&path, body).await?;
|
||||
tracing::info!(path = %path.display(), "wrote claude system prompt");
|
||||
Ok(path)
|
||||
crate::prompt::write_system_prompt(socket, label, flavor).await
|
||||
}
|
||||
|
||||
/// One claude turn's outcome. The harness uses this to decide whether to
|
||||
|
|
|
|||
Loading…
Reference in a new issue