Asking a human
You may be running unattended. When you hit something you must not decide alone, do not guess and do not stop silently in a terminal nobody is watching. Ask, block, and continue with the answer.
When to ask
Ask when the decision is genuinely not yours:
- Irreversible or destructive. Force-push, drop a table, delete a branch, rewrite history,
rm -rfoutside the workspace, publish, deploy. - A real tradeoff with no repo-visible answer. Two designs that both work and differ in cost the repo does not encode.
- Something only the human has. A credential, an API key, an account id, a production hostname.
- An ambiguous or contradictory requirement. The issue says one thing, the code says another, and picking wrong wastes the whole run.
- Scope you were not given. The fix requires touching a system outside the assignment.
When NOT to ask
Every unnecessary question costs the human an interruption and costs you minutes. Do not ask when:
- The repo, tools, or context can answer it. Read the code, run the test, check the config, grep the history first. "Which test runner?" is in
package.json. - One composite question would do. Never fire three questions in a row. Bundle them: state the decision once, list the options, ask once.
- The human already answered it. Anything in the conversation, the issue, an ADR, or
CONTEXT.mdis answered. Do not re-ask for confirmation of an instruction you were already given. - You are asking for reassurance. "Shall I continue?" is not a decision. Continue.
- It is a style or naming detail. Follow the surrounding code and move on.
- It is cheap and reversible. Just do it; report it afterwards.
If you cannot state a concrete consequence for each option, you do not have a question yet. Investigate more.
Resolve the binary
herdr-hitlonPATH— use it.- Else
"$HERDR_PLUGIN_ROOT/bin/herdr-hitl"whenHERDR_PLUGIN_ROOTis set. - Else run
herdr plugin action invoke huketo.hitl.install-cli, then retry step 1.
HITL=$(command -v herdr-hitl || echo "${HERDR_PLUGIN_ROOT:-}/bin/herdr-hitl")
[ -x "$HITL" ] || { herdr plugin action invoke huketo.hitl.install-cli; HITL=$(command -v herdr-hitl); }
Before every question, resolve the channel with herdr-hitl channel:
messenger— deliver it withherdr-hitl ask.terminal— the human is at your own interface. Ask there and do not callherdr-hitl ask.afk— the human declared unavailable mode. Do not callherdr-hitl askorherdr-hitl notify. Apply the AFK decision routing policy below: decide autonomously, run quorum, or defer dependent work.
The human toggles presence with herdr-hitl afk, herdr-hitl away, and herdr-hitl here. These are human controls; agents must never run them or toggle presence. The ask examples below apply only to the messenger branch.
Decision routing when AFK
AFK is human-declared unavailable mode. When the human is AFK, questions and notifications are refused before the daemon or network with exit 6.
AFK authorizes no actions. It never constitutes approval, and it never invents synthetic approval. Explicitly preauthorized effects remain authorized.
When AFK is active:
- No human questions or notifications: Do not attempt to page or notify the human.
- No busy retry: Do not poll, loop, or retry
askornotifywhile AFK. - No presence toggling: Agents must never run
afk,away, orhere. - CLI does not run models: Autonomous and quorum reasoning is performed by the agent and harness, not by
herdr-hitl.
Route decisions according to this table:
| Decision class | Applicable situations | Required action |
|---|---|---|
| Autonomous | In-scope, reversible, evidence-resolvable matters (e.g. implementation details, test design, following existing conventions, bug fixes with clear evidence). | Decide autonomously using codebase evidence, tests, and documentation. Proceed with execution. |
| Quorum | Material technical alternatives with costly reversal and unresolved evidence (e.g. significant architectural fork where evidence is split). | Seek evidence-backed consensus via quorum across independent models (herdr-quorum). If quorum is unavailable or fails, choose the evidence-supported safe reversible alternative; otherwise defer. |
| Defer | Human-only decisions: unapproved destructive or publication actions (force-push, drop tables, deploy, delete branch, rewrite history), missing credentials/secrets only the human has, conflicting human requirements, out-of-scope work. | Defer only dependent work. Continue independent authorized work. Leave the workspace clean and record the blocked work and required human decision clearly. |
Command surface
herdr-hitl ask [flags]
-t, --title string one-line summary
-m, --message string question body, Markdown; "-" reads stdin
--message-file PATH read the body from a file
-c, --choice strings repeatable, "id=Label" or bare "Label"
--primary strings choice ids rendered as the primary/affirmative button
--danger strings choice ids rendered as the destructive button
--free allow a free-text answer (default true; --free=false forces a choice)
-a, --attach strings repeatable path to an image or document
--timeout duration default 30m; 0 waits forever
--transport strings telegram | discord (default: config)
--agent string label shown to the human (default $HITL_AGENT, else "agent")
--default string text to print if the deadline passes, instead of failing
--channel string messenger | terminal | auto (default: config)
-o, --format string text | json (default text)
herdr-hitl notify [-t|-m|--message-file|-a|--transport|--agent|--channel]
herdr-hitl channel [-o text|json]
herdr-hitl afk [--for duration]
herdr-hitl away [--for duration]
herdr-hitl here
herdr-hitl pending [-o text|json]
herdr-hitl answer <request-id> [--choice ID] [--text TEXT]
herdr-hitl cancel <request-id> [--reason TEXT]
herdr-hitl doctor [-o text|json]
ask -o text prints only the answer on stdout; logs go to stderr. So ANSWER=$(herdr-hitl ask …) is the idiomatic call. With -c, the answer text is the chosen label; use -o json and read .choice_id when you need to branch on a stable id.
Examples
Yes/no approval before something irreversible.
ANSWER=$(herdr-hitl ask -o json \
-t "Force-push to main?" \
-m "The rebase dropped 2 merge commits (a1b2c3d, e4f5g6h). Force-pushing rewrites main for everyone who pulled today." \
-c "push=Force-push" -c "abort=Abort and leave main alone" \
--danger push --primary abort --free=false --timeout 20m)
case "$(printf '%s' "$ANSWER" | jq -r .choice_id)" in
push) git push --force-with-lease ;;
abort) echo "leaving main alone" ;;
esac
Multi-choice design decision with consequences spelled out.
herdr-hitl ask -o json --free=false --timeout 1h \
-t "Session storage for the new auth flow" \
-m 'Three options, all implementable today:
- **Redis** — fastest, but adds a service to deploy and to the dev setup.
- **Postgres table** — no new infra, ~4ms slower per request, needs a cleanup job.
- **Signed cookies** — no storage at all, but sessions cannot be revoked server-side.
No ADR covers this. Revocation is not in the issue requirements.' \
-c "redis=Redis" -c "pg=Postgres table" -c "cookie=Signed cookies" \
--primary pg --danger cookie
Free-text question asking for a value.
HOST=$(herdr-hitl ask --free \
-t "Staging database host" \
-m "Migration is ready. I need the staging Postgres host — it is not in the repo or the env. Reply with the hostname only." \
--timeout 30m) || exit $?
Question with a screenshot attached.
herdr-hitl ask -o json --timeout 15m \
-t "Is this layout right?" \
-m "The sidebar collapses below 900px instead of 768px as the issue asked. Screenshot at 880px attached. Keep 900px or change to 768px?" \
-a /tmp/sidebar-880.png \
-c "keep=Keep 900px" -c "fix=Change to 768px" --primary fix --free=false
Question with a Markdown plan attached — attach, do not paste.
herdr-hitl ask -o json --timeout 2h \
-t "Approve the 9-step refactor plan?" \
-m "Full plan attached (9 commits, touches 34 files, no behaviour change intended). Steps 6 and 7 change the public API of \`internal/store\`." \
-a /tmp/refactor-plan.md \
-c "go=Approved, start" -c "revise=Revise it" -c "drop=Do not do this" \
--primary go --danger drop --free
Fire-and-forget at the end of a long run. Never blocks, no exit-code branching.
herdr-hitl notify -t "Migration finished" \
-m "42 tables migrated, 0 errors, 6m12s. Report attached." -a /tmp/report.md
Exit codes
| Code | Meaning | What you do |
|---|---|---|
0 |
Answered | Use the answer on stdout. |
1 |
Error | Delivery or config failure. Run doctor. Do not retry blindly. |
2 |
Usage error | Your command was wrong. Fix the flags. |
3 |
Timeout | Nobody answered. Take the safe path or stop; do not proceed as if approved. |
4 |
Canceled or declined | The human said no. Stop that line of work. |
5 |
Terminal channel | Nothing was sent. Ask in your own interface; do not retry. |
6 |
AFK channel | Human declared unavailable mode. Nothing was sent; questions and notifications refused. Do not retry; follow autonomous/quorum/defer policy. Defer dependent work and proceed with independent authorized work. |
set +e
ANSWER=$(herdr-hitl ask -t "Deploy to prod?" -m "…" -c "go=Deploy" --free=false --timeout 30m)
CODE=$?
set -e
case $CODE in
0) echo "proceeding: $ANSWER" ;;
3) echo "no answer in 30m — skipping the deploy and reporting instead" ;;
4) echo "declined — stopping" ;;
5) echo "nothing sent — ask in your own interface; do not retry" ;;
6) echo "human is AFK — do not retry; defer dependent work and continue independent work" ;;
*) echo "hitl failed ($CODE)" >&2; exit "$CODE" ;;
esac
Never treat 3, 4, 5, or 6 as approval. AFK authorizes no actions and provides no synthetic approval. Exit 5 is not a failure to retry; it means ask in your own interface. Exit 6 means the human is unavailable; do not retry to page the human. Follow the AFK decision table to decide autonomously, run quorum, or defer only dependent work while continuing independent authorized work. If a timeout has a safe default, encode it with --default so the command exits 0 and prints that value.
Writing a good question
The human may be on a phone, in a queue, with ten seconds of attention.
- Title: the decision, not the topic. "Force-push to main?" beats "Question about git".
- State the decision in the first sentence. No preamble, no recap of what you have been doing.
- Give each option its consequence. An option without a cost is not a choice, it is a quiz.
- Attach the evidence, do not paste it. Diff, plan, log, screenshot —
-a FILE. Pasting 300 lines into the body makes it unreadable on a phone. Up to 10 attachments, 10 MiB each. - Keep the body under ~1500 characters. If it does not fit, the excess belongs in an attachment.
- Set
--timeoutdeliberately. Match it to how long you can afford to wait and to how likely the human is nearby: 15–30m for something blocking the run, 1–2h for a design decision,0only when waiting forever is genuinely correct. Never leave it to the default without thinking. - Use
--dangerfor the destructive option and--primaryfor the safe one. The colours are the only cue the human gets before tapping. - Use
--free=falsewhen a free-text answer is not actionable. Otherwise leave free text on so the human can say something you did not anticipate. - Give
--agenta useful label so the human knows which of your runs is asking.
Troubleshooting
herdr-hitl doctorfirst — it checks config, credentials, daemon reachability, and each transport, and it never prints tokens.- Exit
1with "daemon unavailable":herdr-hitl daemon start, then retry once. - Question delivered but no answer ever arrives:
herdr-hitl pendingto confirm it is still open. The human may not be allowed to answer (allowed_user_ids), or the messenger is misconfigured — seedocs/setup-telegram.md/docs/setup-discord.md. - You were killed mid-ask: nothing to clean up. The daemon sees your connection close and withdraws the question automatically.
- You changed your mind:
herdr-hitl cancel <request-id> --reason "…".