ask_operator: multi-select + free-text fallback

ask_operator now accepts a multi: bool. when true and options is
non-empty, the dashboard renders the choices as checkboxes — operator
picks any subset, answer comes back as a ', '-joined string. when
false (default), options are radio buttons.

independent of multi, a free-text input ('or type your own…') is
always rendered alongside options so the operator is never trapped
by an incomplete list. submit merges checked options + free text into
the single 'answer' field.

schema migration: operator_questions grows a multi INTEGER column
with a one-shot ALTER TABLE on open. backward compatible — old rows
default to 0 (not multi).

prompt + mcp tool description updated; existing dashboard css for
.qform was rewritten around the new vertical layout.
This commit is contained in:
müde 2026-05-15 19:52:44 +02:00
parent c337cc06f8
commit 8344dd9ab7
7 changed files with 130 additions and 35 deletions

View file

@ -9,7 +9,7 @@ Tools (hyperhive surface):
- `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__request_apply_commit(agent, commit_ref)` — submit a config change for any agent (`hm1nd` for self) for operator approval.
- `mcp__hyperhive__ask_operator(question, options?)` — surface a question on the dashboard. Returns immediately with a question id; the operator's answer arrives later as a system `operator_answered` event in your inbox. Do not poll inside the same turn — finish the current work and react when the event lands.
- `mcp__hyperhive__ask_operator(question, options?, multi?)` — surface a question on the dashboard. Returns immediately with a question id; the operator's answer arrives later as a system `operator_answered` 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. Do not poll inside the same turn — finish the current work and react when the event lands.
Approval boundary: lifecycle ops on *existing* sub-agents (`kill`, `start`, `restart`) are at your discretion — no operator approval. *Creating* a new agent (`request_spawn`) and *changing* any agent's config (`request_apply_commit`) still go through the approval queue. The operator only signs off on changes; you run the day-to-day.

View file

@ -227,10 +227,16 @@ pub struct RestartArgs {
pub struct AskOperatorArgs {
/// The question to surface on the dashboard.
pub question: String,
/// Optional fixed-choice answers. If empty, the dashboard renders a
/// free-text input. Otherwise renders a select list of these options.
/// Optional fixed-choice answers. The dashboard always renders a
/// free-text fallback ("Other…") so the operator is never trapped
/// by an incomplete list.
#[serde(default)]
pub options: Vec<String>,
/// When true, options are rendered as checkboxes — operator can pick
/// any subset. The answer comes back as a single string with
/// selections joined by ", ". Ignored when `options` is empty.
#[serde(default)]
pub multi: bool,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
@ -369,7 +375,9 @@ impl ManagerServer {
with event `operator_answered { id, question, answer }` lands in your inbox; handle it \
on a future turn. Use this when a decision needs human signal (ambiguous sub-agent \
request, policy call, scope clarification). `options` is advisory: pass a short \
fixed-choice list when applicable, otherwise leave empty for free text."
fixed-choice list when applicable, otherwise leave empty for free text. Set \
`multi: true` to let the operator pick multiple options (checkboxes); the answer \
comes back as a comma-separated string."
)]
async fn ask_operator(&self, Parameters(args): Parameters<AskOperatorArgs>) -> String {
let log = format!("{args:?}");
@ -378,6 +386,7 @@ impl ManagerServer {
.dispatch(hive_sh4re::ManagerRequest::AskOperator {
question: args.question,
options: args.options,
multi: args.multi,
})
.await;
match resp {