hive-subagent-mcp: name a role at dispatch, load it as the system prompt

A `start` may now name a role: `role: "reviewer"` loads the spawning
agent's own `subagent_roles/reviewer.md` and renders it, alone, into one
per-session file that `--append-system-prompt-file` points at. The role is
the system prompt; the task is the turn, never the other way round — a
task baked into the system prompt would re-assert itself as an
instruction on every later turn of a continued session, not just the one
it was written for. The task instructions (`prompt_file`) are read and
folded ahead of the turn's own prompt instead, the same channel that
carries them to the subagent without a role.

The argument is optional, so every existing call is unchanged — pinned by
a test that a pre-role payload still deserializes with `role` absent from
the schema's required set, and another that the no-role path reaches
claude with the caller's own file, unrendered, and the trigger untouched.
With a role, one test pins the system-prompt file to the role's text and
nothing of the task, and another pins the task still reaching the
subagent as the turn's prompt.

A role name with no file fails the call, before the session name is even
reserved, and the error lists the roles the directory does hold. No agent
ships roles yet, so named-but-missing is the ordinary first-run state; a
fallback there would spawn a subagent under a prompt missing every clause
the role existed to carry. An empty file and a name that is not a plain
identifier refuse the same way.
This commit is contained in:
atlas 2026-09-20 22:50:07 +02:00 committed by mara
commit 657875b2fa
6 changed files with 545 additions and 11 deletions

View file

@ -71,6 +71,48 @@ after a restart has to re-supply `dir` when the session lives anywhere
other than the daemon's own working directory — and a restart is the
situation you reach for `continue` in most often.
## Roles
A `start` may name a **role**: `role: "reviewer"` runs that subagent under
the spawning agent's own `reviewer.md`. Role files live in one directory
per agent, `subagent_roles/` inside that agent's state directory, one
markdown file per role, and the `role` argument is the filename without
its extension.
One convention directory of named files, because a named set enumerates.
Anything that watches roles — a check for a safety clause that has gone
stale, an operator asking what this agent can dispatch — lists the
directory and has the answer. Per-agent rather than hive-wide: the clauses
a role carries come out of that one agent's own prompt files, and the
daemon only ever spawns on that agent's behalf.
**The role is the system prompt; the task is the turn — never the other
way round.** The daemon writes the named role's text, alone, into a
per-session file under the harness directory and points
`--append-system-prompt-file` at it — nothing about the task reaches that
file. It reads the task instructions (`prompt_file`) and folds them ahead
of the turn's own prompt instead, the same channel that carries them to
the subagent without a role. A task baked into the system prompt would
re-assert itself as an instruction on every later turn of the session, not
just the one the caller wrote it for — the system prompt is the
subagent's standing identity, not a one-shot channel. The caller's own
task file stays untouched either way.
### A role with no file refuses the call
A named role with no file **fails the `start`** and names the roles the
directory does hold. No fallback, no spawn, no reservation — the refusal
lands before the daemon claims the session name, so a caller that fixes
the name may retry it immediately.
The reason is the state every agent starts in. The directory starts
empty: hyperhive ships no role files, and nothing creates one until an
agent writes its own, so a name with no file behind it counts as the
ordinary first-run answer rather than a rare corruption. Falling back to a spawn
without the role would hand that subagent a prompt missing every clause
the role existed to carry, and would do it quietly. An empty role file
draws the same refusal, for the same reason.
## Goals, and turns toward them
`start` takes an optional `goal`. Without one a session is a single turn,