Hermes Agent: who decides what
How to read this page
A seat is a judgment the agent makes over and over: may this sender's message run, what goes in the prompt, may this command run, is the turn over. Each card records the question, how it is answered, and who or what holds it, with the code that implements or exposes that decision. Seat ids (A1, B11 …) are links.
Each answer describes what Hermes does at its default settings: the code's defaults plus the config.yaml the installers start from, a copy of the repository's cli-config.yaml.example. Running hermes setup or editing config.yaml changes some of these; where an answer depends on such a value, it names the setting. Platforms and opt-in plugins that change an answer are named where they matter; exceptions named are the main ones, not a complete list.
The chip on each card has two words. The first is about the question: a RULE has one right answer given its inputs, so a run that answers differently is a bug; a RULING is one that competent people could answer differently. Enforcing a limit someone set is a RULE; choosing what to keep, what to trust or when the work is done is a RULING, even when code holds it.
The second word is who holds it: llm = a language-model call; model = another learned model; code = code in the repository; human = a person; external = code outside the repository that the agent hands the decision to.
Paths are from the repository root. A bare :120-140 or (323) means lines in the file named just before it.
Counts
276 seats across 9 stages: A 16, B 37, C 34, D 64, E 28, F 41, G 19, H 21, I 16.
| Kind of question · holder | Seats |
|---|---|
| RULE · code | 104 |
| RULING · code | 94 |
| RULING · llm | 31 |
| RULING · human | 26 |
| RULING · external | 18 |
| RULE · external | 2 |
| RULE · model | 1 |
What this edition does not have
There is no list of gaps. No coverage figure: this edition does not say how many of the agent's model calls and decision points the seats leave out. Each card shows the question, the answer and its holder; for most seats we also recorded what checks the judgment and how it has failed, but those notes are not shown here.
What is left out of this edition
Nothing is left out: all 276 cards are shown.
A. Intake and routing
A1 May this sender's message dispatch agent work? RULE · code
- Answered how
Code walks a fixed order of grants on every inbound gateway message (
_principal_authorized): a local session is decided by its own adapter, webhook and trusted-inbound platforms pass, then chat-scoped grants, per-platform allow-all, role auth, the pairing store and the env/config allowlists. With no allowlist configured andGATEWAY_ALLOW_ALL_USERSunset (the fresh-install case), an unknown sender is denied unless an operator approved their pairing code. A bot-authored message that passes is still refused while its chat's loop guard is cooling down.- Held by
gateway/authz_mixin.py:707-790(_is_user_authorized→_principal_authorized): a LOCAL-platform source is decided first byauthorize_local_source(724-727;gateway/session_local.py:155-172, None for any other platform); webhook /trusted_inboundplatforms pass (731-732); chat-scoped grants and relay delegation (615-659); per-platform allow-all (746-747); Discord role auth (749-750); pairing store (753-755); env/config allowlists (757-790); with no env allowlist, own-policy adapters (762-767,_own_policy_authorizes542-562) or adapterextra.allow_from(564-593); elseGATEWAY_ALLOW_ALL_USERS(770), default deny.
A2 Does an unrecognised DM sender get a pairing code, silence, or a decline? RULE · code
- Answered how
Code picks pair, ignore or decline (
_get_unauthorized_dm_behavior): a per-platform setting wins, Email ignores, then a non-default globalunauthorized_dm_behavior, then the adapter's DM policy; after that, a setGATEWAY_ALLOWED_USERSmeans ignore, and a platform's own user or group allowlist does so only on adapters that opt in. On a fresh install with no allowlist the global default ispair, so the stranger gets a pairing code with the approve command; an ignored sender triggers one notice per sender per process to the owner's home channel, if one is set.- Held by
gateway/authz_mixin.py:792-834(_get_unauthorized_dm_behavior); stranger/owner copy ingateway/run_inbound_unauthorized.py:40-113(owner notice once per sender per process, 75-94).
A3 Should this stranger be let in? (pairing approval) RULING · human
- Answered how
A person answers: the stranger is sent a code and the operator decides by running
hermes pairing approve <platform> <code>; code then records the approval and, if an allowlist env var is configured, adds the user to it. Code bounds the question: codes expire after one hour, at most 3 are pending per platform, and 5 failed approvals lock the platform out for an hour. This only arises when the DM behaviour ispair, which is the default with no allowlist.- Held by
gateway/run_inbound_unauthorized.py:40-46: the operator, running the approve command printed bypairing_code_reply; recorded bygateway/pairing.py:500(approve_code), mirrored into the allowlist (gateway/pairing.py:153-161). Code limits: 1 h TTL (gateway/pairing.py:32), 3 pending per platform (35), lockout after 5 failed approvals (36, 633-646).
A4 In a group chat, is this message addressed to the bot? RULE · code
- Answered how
Each messaging adapter's code decides, and the default differs by platform. On Telegram
require_mentionandbots_require_mentionboth default to false, so an authorized sender's group message needs no mention; it can still be dropped when the chat is outsideallowed_chats, by topic settings, or when it @mentions only other bots (exclusive_bot_mentions, on by default). Withrequire_mentionon, only replies to the bot, @mentions and configured wake-word patterns get through. Discord, Slack, Matrix, Mattermost and Feishu default to requiring a mention; DingTalk and Signal default to not requiring one.- Held by
per adapter. Exemplar:
plugins/platforms/telegram/adapter.py:5783-5785(_telegram_require_mention), 6439 (gate), 6164-6195 (bots must mention). Same question in the Slack, Discord, Matrix, WhatsApp, Mattermost, Feishu, DingTalk, Teams, Photon, Buzz adapters (122 files in the tree matchrequire_mention: 56 intests/, 31 inwebsite/docs, 35 elsewhere).
A5 Which profile, and which conversation, does this message belong to? RULE · code
- Answered how
Code answers deterministically.
resolve_identitypins once per inbound event which bot received it and which profile it runs in (with profile multiplexing off, the default, that is the one launch profile).build_session_keythen builds the conversation key: a DM gets one session per chat, a group gets one session per participant outside threads, and a thread is shared by everyone in it, becausegroup_sessions_per_userdefaults to true andthread_sessions_per_userto false.- Held by
identity once per event,
gateway/session_identity.py:201(resolve_identity); session keygateway/session.py:691-734(build_session_key): DMs per chat, groups per user unless in a thread, threads shared by default (719-721).
A6 Is this input a slash command, a skill invocation, or a message for the model? RULE · code
- Answered how
Code answers by lookup. A leading slash word is looked up in the shared command registry (
resolve_command, name or alias); in the gateway a built-in command keeps precedence, an alias quick-command is expanded next, and only then is the word tried as a skill name (resolve_skill_command_key, underscores and hyphens interchangeable), with up to 5 leading skills stacked. An unknown slash word gets an unknown-command reply; anything without a leading slash goes to the model.- Held by
shared registry
hermes_cli/commands.py:399(resolve_command), used by the gateway (gateway/run_inbound.py), TUI (tui_gateway/server.py) and CLI (hermes_cli/slash_exec.py); skill commandsagent/skill_commands.py:822-843(resolve_skill_command_key,build_skill_invocation_message), up to 5 stacked skills (859).
A7 May this sender run this slash command? RULE · code
- Answered how
Off by default: with no admin list set for a platform's DM or group scope, code lets every authorized sender run every slash command. When an operator sets that scope's admin list, admins run anything and everyone else runs only the always-allowed floor plus that scope's
user_allowed_commands; under profile multiplexing the policy comes from the profile owning the bot, and a served profile whose config is not cached fails closed. Plain chat is never gated here.- Held by
gateway/slash_access.py:38-49(can_run),86-130(policy per DM/group scope),137-158(multiplex: policy from the bot-owning profile; uncached served profile fails closed).
A8 While the agent is busy, does a new message interrupt, queue, steer or redirect the run? RULE · code
- Answered how
Code applies the configured
display.busy_input_mode, which defaults tointerrupt. In interrupt mode a plain-text message redirects the running turn when the running agent supports redirect, and the mode is demoted to queue while subagents are running or context compression is in flight; insteermode the text is injected into the running turn, falling back to queue when it cannot be steered; inqueuemode the message waits for the turn to end.- Held by
gateway/run_busy.py:706-758(_resolve_busy_steer_or_redirect); interrupt demoted to queue while subagents run (714-718, #30170) or compression is in flight (719-723, #56391); steer falls back to queue when not steerable (729-746).
A9 Which model handles this turn (voice turns)? RULE · code
- Answered how
At the defaults the main model handles voice turns too:
auxiliary.voice_chatships with providerautoand an empty model, so_route_targetreturns nothing and no swap happens. When a voice model (or a provider with no model, which picks that provider's fast auxiliary model) is configured, a voice turn runs on it only if the request's rough token estimate fits under the compression threshold; otherwise the turn stays on the main model.- Held by
agent/voice_turn_route.py:37-60(_route_targetfromauxiliary.voice_chat), fit check 63-70 (_fits: request must fit under the voice model's compression threshold, else main model).
A10 Should a scheduled job wake the model at all this run? RULING · external
- Answered how
Only for a scheduled job that has a pre-run script. The script, which the user wrote, decides: if the last non-empty line of its output is the JSON
{"wakeAgent": false}, the run is skipped with no model call and no delivery. Any other output, including non-JSON or a missing flag, wakes the model normally.- Held by
the job's script, read by
cron/scheduler_prompt.py:21-35(_parse_wake_gate: last non-empty stdout line{"wakeAgent": false}skips the run).
A11 Did the pre-run script find nothing to report? RULE · code
- Answered how
Code answers for jobs with a pre-run script: if the script succeeded and printed nothing,
_build_job_promptreturns no prompt and the AI call is skipped. A failed script's output is instead put in front of the prompt as a 'Script Error' block for the model to report, and non-empty output is put in as context.- Held by
cron/scheduler_prompt.py:336-337(successful script with empty stdout → no AI call).
A12 Does an internal event (background-process completion, wake) start a new turn? RULE · code
- Answered how
Code answers: an internal event becomes a turn only when the receiving adapter actually accepts it (
admit_internal_eventchecks an acceptance flag, not just that the handler returned). For a push-capable adapter the wake is first offered to the session's queue, which never interrupts a turn already admitted; otherwise a synthetic message event is built and admitted. A gateway that is draining refuses the event so the producer retries it after restart.- Held by
gateway/wake.py:97-120(admit_internal_eventrequires a concrete adapter acceptance); synthetic event built atgateway/wake.py:149-151, after a session-FIFO admission (admit_route_wake, 139-148) is tried first on the push path; watcher arminggateway/run_notifications.py(arm_process_watcher, pergateway/AGENTS.md:59-66, not re-traced).
A13 In the classic terminal app, while the agent is busy, does a new message interrupt, queue, steer or redirect the run? RULE · code
- Answered how
Code applies
display.busy_input_mode, read once at start-up and defaulting tointerrupt(changeable with/busy). In interrupt mode text redirects the running turn when the agent supports redirect, and otherwise is put on an interrupt queue that the chat loop delivers by stopping the turn withagent.interrupt(...);steerinjects text mid-run, falling back to queue for images or a rejected steer;queueholds it for the next turn. Slash and!commands typed while busy are never routed this way; they queue and run after the turn.- Held by
hermes_cli/cli_tui_mixin.py:1475-1532(_tui_enter_while_busy), reached from:1437-1441; the fallback interrupt is delivered by the chat loop athermes_cli/cli_chat_turn_mixin.py:415-452(agent.interrupt(...)at:439). Mode read once at start-up (hermes_cli/cli_init_mixin.py:150-152) and changed by/busy(hermes_cli/cli_commands_mixin.py:2045-2051, from grep).
A14 In the TUI and desktop backend, while the agent is busy, does a newly submitted prompt redirect, steer, interrupt or queue? RULE · code
- Answered how
Code applies
display.busy_input_mode, defaulting tointerrupt. Text-only input in interrupt mode redirects the running turn when the agent supports redirect; otherwise the prompt is queued durably and the turn is hard-interrupted (input with images is queued without interrupting).steerinjects the text mid-run or falls back to the queue and never escalates to an interrupt;queue, a client queue drain, or compression in flight all mean queue only.- Held by
tui_gateway/session_auto_continue.py:400-454(_handle_busy_submit), called fromtui_gateway/methods_prompt.py:894-913; explicit steer/redirect requests taketui_gateway/methods_session_interrupt.py:133-176; mode fromtui_gateway/server.py:249-251; compression probetui_gateway/session_auto_continue.py:262-289; interrupt worker:224-243.
A15 Does first-run onboarding open on this machine, and can its chat be started? RULE · code
- Answered how
Code answers. Onboarding is due when the root profile's
onboarding.runis true, or when it is unset and this machine has never used Hermes;falsemeans done. A first run starts only on the desktop app's local backend and only when the welcome trial can be claimed or a provider is configured; a rerun from Settings skips that claim.- Held by
hermes_cli/onboarding_run.py:20-29(should_run): the root config's explicitonboarding.runwhen it is a bool, else whether this machine never used Hermes (install_state.probe().fresh)._run_state(tui_gateway/methods_onboarding.py:29-42) addseligible: only the desktop's local backend, and only when something can carry the first chat (_due_now:45-53). At start,_claim_before_runrefuses outside the local desktop backend and claims the welcome trial, refusing when there is neither a trial nor a configured provider (gateway/session_onboarding.py:132-146; twintui_gateway/methods_onboarding.py:149-165). On the session authority, starting needssession:create(gateway/session_onboarding.py:60-61); the sidecar's start handler has no such check (tui_gateway/methods_onboarding.py:168-180).
A16 When onboarding starts and an unfinished onboarding chat already exists, is that chat resumed, ended, or deleted? RULE · code
- Answered how
Code answers. A second window or a reconnect resumes the newest open run; a fresh app launch or a rerun starts a new run and ends every other open run as replaced. On a fresh launch, unfinished chats no window is watching are deleted instead, unless a window reopens one in time.
- Held by
gateway/session_onboarding.py:100-118(_choose_run): the newest open run is resumed unless the start is a rerun or a fresh app launch (:109-112; newest because open runs are listed bystarted_at DESC,hermes_state_sessions_model_config.py:36-42,agent/onboarding_record.py:107-118). Every other open run ends asreplaced(_settle_runsgateway/session_onboarding.py:199-219), except that on a fresh launch chats no window is watching are deleted (_discard_unfinished_runs:222-242); a window that reopens one in time keeps it (keep_from_discard:181-196). The sidecar twin closes live sessions and deletes every replaced chat, ending a run asreplacedonly when the delete is refused (tui_gateway/methods_onboarding.py:276-294).
B. Context: what the model sees
B1 What goes in the system prompt, and in what order? RULE · code
- Answered how
Code assembles it in a fixed order of three tiers: stable (identity, help and model guidance, pinned skills, the coding brief), context (the caller's system message, project context files, the workspace snapshot), then volatile (skills index, memory, plugin sections, profile line, timestamp, runtime environment hints). The result is cached for the session and rebuilt after context compression, on an operator-requested refresh, or when the stored prompt is stale or unusable (for example a changed Bot Chat capability epoch).
- Held by
agent/system_prompt.py:738-801(build_system_prompt_parts: stable → context → volatile), joined at 804-813; built once per session and rebuilt after compression (816-834) or an operator-requested refresh (agent/prompt_refresh.py:80-96).
B2 Reuse this session's stored system prompt, or rebuild it? RULE · code
- Answered how
Code answers when a session with history resumes: the stored prompt is reused if its recorded model, provider, session id (only when the session-id line is on) and working directory still match the running agent; a mismatch, a missing or empty stored prompt, or a changed Bot Chat capability epoch rebuilds it. A change of platform alone does not rebuild. Saved tool definitions keep their stored order and bytes, but tools no longer available drop out and new ones are appended.
- Held by
agent/conversation_loop.py:802-953(_restore_or_build_system_prompt), identity test 957-984 (_stored_prompt_matches_runtime: model, provider, session id, cwd; platform deliberately excluded, #104414); tools pinned to the stored array 749-768 (_restore_pinned_tools), with carried tools filtered by current toolset config (tools/mcp_tool_agent.py:325-369, 345).
B3 Which identity: the user's SOUL.md or the default? RULE · code
- Answered how
Code answers: when the agent loads context files (or is set to load its identity anyway, as cron does), the profile home's SOUL.md is the identity, after a threat scan and truncation; if it is missing or empty, or the process is a safe-mode worker, the built-in default identity is used. A fresh install seeds a default SOUL.md into the home, so at the defaults the identity comes from that seeded file.
- Held by
agent/system_prompt.py:550-556(_identity_parts; SOUL.md whenload_soul_identityor notskip_context_files), loaderagent/prompt_builder.py:1512-1560(load_soul_md: returns nothing in a safe-mode worker (1523-1526), own-profile home, legacy bot protocol stripped, distribution-owned SOUL.md scanned as non-user-authored 1548-1555).
B4 Which project instruction files load into the prompt? RULE · code
- Answered how
Code answers with a fixed precedence: only one project file type loads, the first found of
.hermes.md/HERMES.md(nearest, up to the git root), the AGENTS.md chain from git root to the working directory,CLAUDE.md, then.cursorrulesplus.cursor/rules/*.mdc. Nothing loads when the agent skips context files or runs as a safe-mode worker, and on surfaces other than CLI and TUI a working directory that fell back to the Hermes install tree loads no project file.- Held by
agent/prompt_builder.py:1720-1750(build_context_files_prompt): first non-empty type wins among.hermes.md/HERMES.md(walk to git root, 141-150), AGENTS chain git-root→cwd withAGENTS.override.mdpreferred per directory (1589-1614, 1676-1697),CLAUDE.md(1617-1628),.cursorrules+.cursor/rules/*.mdc(1631-1637); install-tree fallback suppressed (1658-1665, #64590); skipped entirely whenskip_context_files(agent/system_prompt.py:724-725) or in a safe-mode worker (1730-1733).
B5 Is this context file safe to put in the prompt? RULING · code
- Answered how
Code answers with a pattern scan (
scan_for_threatsin thecontextscope, plus invisible-Unicode checks) before each file enters the prompt. A hit in a project file (.hermes.md, AGENTS.md, CLAUDE.md,.cursorrules) replaces the whole file with a one-line BLOCKED notice; a hit in the user's own SOUL.md is only logged and the file still loads, unless a profile distribution owns that SOUL.md.- Held by
agent/prompt_builder.py:84-110(_scan_context_content) overtools/threat_patterns.py:131-145(scan_for_threats(scope="context")); called for SOUL.md (1556), each context section (1576, 1711) and subdirectory hints (agent/subdirectory_hints.py:305).
B6 How much of a long context file does the model see? RULING · code
- Answered how
Code answers with a size cap. At the default (
context_file_max_charsunset) the cap scales with the model's context window, about 6% of it in characters, never below 20,000 or above 500,000, and 20,000 when the window is unknown; a longer file keeps its first 70% and last 20% of the cap with a marker naming the omitted headings and telling the model toread_filethe rest. A merged AGENTS chain is capped again as a whole, and subdirectory hints use a fixed 32,000-character cap.- Held by
agent/prompt_builder.py:1043-1054(cap = explicitcontext_file_max_chars, else 6 % of the window, 20k floor, 500k ceiling),1462-1493(_truncate_content: 70 % head, 20 % tail, marker naming omitted headings); merged AGENTS chain capped again (1693-1697); subdirectory hints fixed at 32,000 chars (agent/subdirectory_hints.py:26, 307-309).
B7 Which behaviour guidance does this model get? RULING · code
- Answered how
Code answers by model-name substring match. With tools present, every model gets the task-completion and parallel-tool-call guidance; at the default
autosetting foragent.tool_use_enforcementandagent.execution_guidance, extra blocks go only to models whose name contains a listed substring (for example gpt, codex, gemini, qwen, deepseek), Gemini and Gemma get a Google-specific block, and Claude-family names get neither. Setting either key to true, false or a list overrides the built-in lists.- Held by
agent/system_prompt.py:41-52(_model_gate), 559-587 (_guidance_parts); listsTOOL_USE_ENFORCEMENT_MODELS(agent/prompt_builder.py:362),EXECUTION_GUIDANCE_MODELS(376); Gemini/ Gemma extra block (agent/system_prompt.py:582-583);DEVELOPER_ROLE_MODELS(agent/prompt_builder.py:659).
B8 Which platform hint does the model get? RULE · code
- Answered how
Code answers by platform name: the built-in hint for core surfaces, else the hint shipped by the messaging platform's plugin, else none. A
platform_hints.<platform>entry in config can append to or replace it (none are set by default); a scheduled job also gets its delivery channel's hint, and Telegram adds a rich-messages note only when that option is on (off by default).- Held by
agent/system_prompt.py:451-462(platform_hint), built-in or plugin registry hint (413-431), configplatform_hintsappend/replace (55-68), cron delivery destination hint (434-448), Telegram rich-messages add-on (agent/prompt_builder.py:725,agent/system_prompt.py:429-430).
B9 Which skills are offered in the index, and how? RULE · code
- Answered how
Code answers, and only when the agent has a skills tool: it lists every skill not disabled for the session's platform, not excluded by its host, environment or app gates, and not hidden by its conditional rules (needs a tool or toolset that is missing, or is a fallback for one that is present); a name found in several places resolves as project over local over created over external, and plugin-registered skills are added. In coding focus mode, non-coding categories are shown by name only. At the defaults no project skills are listed, because no project folder is trusted.
- Held by
agent/system_prompt.py:301-314(_skills_prompt, only with skill tools) →agent/prompt_builder.py:1367-1459(_build_skills_system_prompt_inner): disabled per platform (1373), offer-time host/env/app gates (1154-1165), conditional activation (1168-1186), tier precedence project > local > create_dir > external (1417-1433), plugin skills (1189-1218, 1435-1442), focus-mode names-only demotion (1303-1310); render 1295-1359.
B10 Is this project skill too dangerous to offer? RULING · code
- Answered how
Off by default: project skills are read only from repos listed in
skills.trusted_project_dirs, which is empty on a fresh install. For a trusted repo, code scans each project skill folder with the same scanner hub installs use (cached by content hash); adangerousverdict, or any scanner failure, quarantines the skill from the index, the skill list, skill viewing and slash commands, while acautionverdict still loads.- Held by
agent/skill_utils.py:635-665(is_quarantined_project_skill, the single chokepointiter_project_skill_files); only repos inskills.trusted_project_dirsare read at all (556-600).
B11 Has the skill list changed since the prompt was built? RULE · code
- Answered how
Code answers every turn by comparing the skills index stored in the system prompt (as amended by the newest earlier note) with the index it would build now. If they differ, a one-shot note listing added and removed skills is attached to the current user message, leaving the cached system prompt untouched. A prompt with no skills index, and MoA or codex_app_server turns, are never compared.
- Held by
agent/skills_index_delta.py:102-138(stage_skills_index_note), called every turn fromagent/turn_context.py:1254, delivered on the user-message note channel (913-939).
B12 Has the interface changed since the prompt was built? RULE · code
- Answered how
Code answers when the system prompt is restored or rebuilt: it compares the running platform with what the model was last told (the newest switch note, else the prompt's
Platform:line). If they differ, a one-shot note is attached to the current user message saying the earlier interface is superseded, with the new platform's hint when the prompt itself is out of date; the cached prompt is not rebuilt for this. MoA and codex_app_server turns skip it.- Held by
agent/surface_switch.py:103(stage_surface_switch_note), called fromagent/conversation_loop.py:846, 871, 926.
B13 Which skills are pinned into every session? RULE · code
- Answered how
Off by default:
skills.auto_loadis an empty list on a fresh install, so no skill is pinned. When the operator lists skills there, code loads each named skill in full into the stable part of the system prompt once per agent, marked as active guidance for the session; missing or disabled names are skipped with a warning. Nothing is pinned for agents without skills tools, for agents that skip context files (subagents, review forks), or whenHERMES_IGNORE_RULESis set.- Held by
agent/system_prompt.py:317-341(_auto_load_parts, resolved once per agent, skipped for agents that skip context files and whenHERMES_IGNORE_RULESis set),agent/skill_commands.py:983-1028(resolve_auto_load_skills,build_auto_load_prompt).
B14 What does an invoked skill inject into the turn? RULE · code
- Answered how
Code builds the message: an activation note, the skill body after template substitution of
${HERMES_*}tokens (on by default), the skill's directory, its resolved config values, any setup note, a list of supporting files, then the user's own instruction. Inline!cmd`shell expansion runs only whenskills.inline_shell` is on, which it is not by default, and never for a community-trust hub install.- Held by
agent/skill_commands.py:366-416(_build_skill_message: preprocessed body, skill dir, config values 311-326, setup note 345-350, supporting files 353-363); preprocessingagent/skill_preprocessing.py:142-156:${HERMES_*}tokens (default on) and inline!cmd`shell expansion whenskills.inline_shell` is on, never for community hub installs (99-139).
B15 Which persistent-memory snapshot goes into the prompt? RULE · code
- Answered how
Code answers: the built-in MEMORY.md and USER.md stores are both on by default, loaded from disk when the agent starts (capped at 2,200 and 1,375 characters), and each is rendered into the prompt's volatile tier. An external memory provider's block is added only when
memory.providernames one (empty, meaning built-in only, by default) and its tools are exposed under the agent's toolsets. The snapshot is held for the session and reloaded from disk when the prompt is rebuilt, for example after compression or an operator-requested refresh.- Held by
agent/system_prompt.py:522-547(_memory_parts: MEMORY.md and USER.md viaformat_for_system_prompt, plus the external provider'ssystem_prompt_block, gated on the same check as provider tools, #81014); store built atagent/agent_init.py:1230-1252(2,200 / 1,375 char limits).
B16 Is this prompt worth a memory recall? RULING · code
- Answered how
Only when an external memory provider is configured (none by default). Code then skips recall for an empty prompt, a slash command, or a bare acknowledgement or greeting such as 'ok', 'thanks' or 'continue' matched by a fixed pattern; every other prompt triggers recall, both at turn start and for the next-turn prefetch queued after the reply.
- Held by
agent/memory_provider.py:66-81(TRIVIAL_PROMPT_RE,is_trivial_prompt); gate atagent/turn_context.py:985-986; next-turn prefetch gaterun_agent.py:916-917.
B17 What does the external memory provider recall for this turn? RULING · external
- Answered how
Off by default: no external memory provider is configured on a fresh install. When
memory.providernames one, that provider's own code (outside Hermes' decision) chooses what to recall for the redacted user query; Hermes bounds each external call with a timeout, may skip later calls while a stuck one is still running, and refuses recall outside the profile the providers were set up for. The result is wrapped in a<memory-context>block, labelled as recalled reference data rather than new user input, and attached to the current user turn.- Held by
agent/memory_manager.py:349-410: providerprefetch()viaprefetch_all+_prefetch_provider(refused outside the profile the providers were initialised for (352); external provider bounded by a timeout, and a gate may skip later calls while a stuck call runs, 366-398); wrapped bybuild_memory_context_block(agent/memory_context.py:169-185) and injected into the current user turn (agent/turn_context.py:966-994, 997-1041).
B18 What extra context do plugins add to the user turn? RULING · external
- Answered how
Each registered
pre_llm_callhook decides, so the answer comes from plugin or hook code outside the core: whatever context strings the hooks return are joined and added to the current user message, never to the system prompt, and a hook may instead return a block veto with a message. Oversized pieces are spilled to disk under the spill setting. With no plugin or shell hook registeringpre_llm_call(the Raft platform adapter is one in-tree registrant), nothing is added.- Held by
agent/turn_context.py:849-910(_collect_pre_llm_call_context); oversized pieces are spilled to disk when the spill setting allows (876-906); merged with gateway notes (913-939).
B19 Which plugin system-prompt sections are included? RULING · external
- Answered how
Each enabled plugin that registers a system-prompt section decides its own content; core renders all registered sections once per session, freezes them, and places them after memory in the volatile tier. A resumed session parses them back from the stored prompt bytes instead of re-running plugin code, and a render that fails at a rebuild keeps the previous bytes. With no such plugin enabled, no section is added.
- Held by
agent/system_prompt.py:113-135(_frozen_plugin_prompt_sections, rendered once, restored from persisted bytes 138-162), placed at 790.
B20 Is this a coding session? RULING · code
- Answered how
Code answers with file checks at the default
coding_context: auto: on an interactive surface (CLI, TUI, ACP, desktop) a session counts as coding when the working directory, or an ancestor up to six levels (skipping home and the temp folder), holds a project marker such aspackage.json,pyproject.tomlorAGENTS.md, or sits in a git repo outside home that holds source files. Messaging platforms are never coding underauto. A coding session gets the coding brief and a workspace snapshot pinned for the session;focusalso narrows the toolsets and shows non-coding skill categories by name only, andon/offforce the answer.- Held by
agent/coding_context.py:262-277(_detect_profile), marker walk 220-233, code-file probe 236-259; interactive surfaces only (30); applied atagent/system_prompt.py:668-697(_coding_parts, workspace snapshot pinned per session 604-666); focus mode also demotes skill categories (365-371) and collapses toolsets (328-333).
B21 Which tools does the model see? RULE · code
- Answered how
Code answers: the tools named by the session's enabled toolsets (every non-platform toolset when none are given), minus disabled toolsets subtracted last, then minus any tool whose availability check fails. Some schemas are rewritten or dropped per session, for example
browser_execis removed when the terminal tool is absent; memory-provider tools are appended, MCP tools are refreshed between turns, and a resumed session keeps its stored tool list.- Held by
model_tools.py:227-554(get_tool_definitions→_select_tool_names333-355: enabled toolsets, disabled subtracted last;check_fnavailability in the registry; dynamic schema rewrites 479-501, e.g.browser_execdropped without terminal 395-399); memory-provider tools appended (agent/memory_manager_tools.py:63-106); MCP tools refreshed between turns (agent/turn_context.py:556-574); pinned for resumed sessions (B2).
B22 Should MCP and plugin tools be hidden behind tool search? RULE · code
- Answered how
At the defaults (
tools.tool_search.enabled: auto),assemble_tool_defsreplaces every deferrable tool with the three bridge tools whenever at least one deferrable tool is present: MCP tools, plugin tools outside the core set, and a curated list of cold built-ins such assession_search,todo_listandcronjob_manage. The 5 %threshold_pctdoes not gate this; it only caps how large the catalog listing in the bridge description may be (the smaller of 5 % of the context window and 4,000 tokens), degrading to names only or a bare bridge. Withenabled: offevery tool is sent directly.- Held by
tools/tool_search.py:344-379(assemble_tool_defs;threshold_pctdefault 5 %, 78; listing budget 217-219); called last inmodel_tools.py:540-552.
B23 What does a subdirectory's AGENTS.md add when the agent touches it? RULE · code
- Answered how
After each tool call,
check_tool_callcollects the directories named inpath/file_path/workdirarguments and in a terminal command's paths andcd/pushdtargets, walking up to 5 ancestors, and keeps only directories inside the working dir, not yet visited and not in an excluded copy tree. For each, the first hint file found (AGENTS.override.md,AGENTS.md,CLAUDE.md,.cursorrulesand lower-case variants) whose resolved target stays inside the working dir and passes the read deny-list is scanned, cut to 32,000 characters and appended to the tool result. Nothing is added whenskip_context_filesis set, safe-worker mode is on, or the working dir is a home-like directory.- Held by
agent/subdirectory_hints.py:187-314(check_tool_call): directories from path args andcd/pushdtargets (198-247), up to 5 ancestors (210-230), inside the working dir only (249-260), symlink targets resolved and checked against the read deny-list (40-68), excluded copy dirs (271-278), home-like working dirs disabled (117-133); appended to the tool result atagent/tool_executor.py:1135-1141; disabled withskip_context_files(agent/agent_init.py:2074-2075) and when safe-worker mode is on (agent/subdirectory_hints.py:191).
B24 What do @-references expand into, and is the expansion too large? RULING · code
- Answered how
When a user message carries
@file:,@folder:,@diff,@staged,@git,@url:or a plugin's reference kind,preprocess_context_references_asyncexpands up to 16 of them, keeping paths inside the session's working dir and off the credential deny-list, and appends the results under an "Attached Context" block. If the expansions together exceed 50 % of the model's context window the whole injection is refused and the user gets the refusal; above 25 % a warning is added. These percentages are fixed in code; if the expansion step itself throws, the gateway sends the text as typed.- Held by
agent/context_references.py:202-251(preprocess_context_references_async); path containment 511-523; credential deny-list 526-549; git refs 254-259;@url:fetch 457-468; plugin prefixes 279-286. Called from the gateway (gateway/session_context_refs.py:18-31, reached fromgateway/run_inbound.py:1720-1721), which also expands the CLI's turns (comment athermes_cli/cli_chat_turn_mixin.py:94;gateway/session_context_refs.py:3-5), and from the TUI (tui_gateway/prompt_turn.py:731) (C5).
B25 Does an image reach the model as pixels or as a text description? RULE · code
- Answered how
At the default
agent.image_input_mode: auto,decide_image_input_modesends the image as pixels only when the capability lookup says the active model supports vision (a per-model config override first, then probes such as the models.dev catalog); an explicitly configuredauxiliary.visionbackend, or an unknown answer, sends it as a text description instead. Settingimage_input_modetonativeortextfixes the answer for every turn.- Held by
agent/image_routing.py:375-391(decide_image_input_mode: config override; an explicitauxiliary.visionbackend forces text; else catalogsupports_vision); native parts built at 533-563; read guard 501-514.
B26 What does the user's image show? (text mode) RULING · llm
- Answered how
Only on a gateway turn in text mode (the active model is not known to support vision, or an explicit
auxiliary.visionbackend is set):_enrich_message_with_visionasks the auxiliary vision model, throughvision_analyze_tool, for a 2-4 sentence description of each attached image. Whatever description comes back, of any length, is stripped of memory fences and prepended to the user's text in the agent's first-person voice together with the image's local path; if the call fails, a note pointing the model atvision_analyzewith that path is prepended instead.- Held by
gateway/run_inbound.py:1842-1885(_enrich_message_with_vision) →tools/vision_tools.py:888-926(vision_analyze_tool) on theauxiliary.visionmodel (async_call_llm, 913).
B27 What did the user say in the voice note? RULE · model
- Answered how
On a messaging gateway with
stt.enabledon (the default), each voice clip goes totranscribe_audio. With nostt.providerchosen in config.yaml, the provider is local faster-whisper (or a local whisper command) when installed, otherwise the first cloud speech-to-text service that has a key; if that fails, local transcription is tried. A transcript is prepended to the user's text as a quoted line, an empty one becomes a "do not guess, ask the user" note, and a failure becomes a neutral note giving the audio path; with STT off, the model gets only the file path and duration.- Held by
tools/transcription_tools.py:566-588(transcribe_audio: configured STT, provider picked by_get_provider249-277, with local fallback), called atgateway/run_inbound_transcription.py:163-187; wrapper 19-67; disabled bystt_enabled(gateway/config.py:611;gateway/run_inbound_transcription.py:28-35).
B28 What session context does a gateway turn carry? RULE · code
- Answered how
On each messaging-gateway turn,
build_session_context_promptwrites a "Current Session Context" block: the source platform and chat, channel topic, user name or ID, platform notes, connected platforms, home channels and cron delivery options, with chat names and other labels quoted as untrusted metadata. User and chat IDs are sent raw by default; they are hashed only withredact_piion and on a platform marked PII-safe. The block rides the per-turn ephemeral prompt together with any channel prompt and channel override, except on an API-server execution turn, which sends only that execution's own ephemeral prompt.- Held by
gateway/session.py:383-466(build_session_context_prompt: untrusted metadata JSON-quoted 274-280, PII hashing on safe platforms 390-393); combined ephemeral prompt with channel prompts and channel overridesgateway/run_turn_runner.py:1161-1179, except on anapi_executionturn, which sends only its own settings'ephemeral_system_prompt(1165-1168).
B29 What does a scheduled run's model see? RULE · code
- Answered how
_build_job_promptassembles a scheduled run's message from the job's stored prompt plus any per-run context, the pre-run script's output (a script that succeeds with no output skips the model call entirely), earlier jobs' outputs named incontext_from, the job's notepad, then a fixed cron hint and a current-run-time line in front; the job's skills are loaded ahead of it when set. The result passes an injection scan before the run. The agent itself runs with memory on, SOUL.md loaded, and project context files only when the job has aworkdir.- Held by
cron/scheduler_prompt.py:304-385(_build_job_prompt): user prompt and run context (318-327), pre-run script output (329-345), previous outputs viacontext_fromclipped to 8,000 chars (139-200), notepad (351-355),_CRON_HINT(270-290) followed by a current-run-time line (_run_time_line, 293-301, prepended at 357), job skills loaded raw (203-267); the agent runs withskip_context_files=not workdir,load_soul_identity=True,skip_memory=False(cron/scheduler.py:2508-2510).
B30 Do plugins rewrite the request the model receives? RULING · external
- Answered how
Inert on a fresh install: no plugin shipped in the repository registers
llm_requestmiddleware, soapply_llm_request_middlewarereturns the request untouched. When an installed plugin registers one, each callback in turn may return a replacement request dict, which is sent to the provider in place of the original; non-dict results are ignored, and a callback that raises is logged and skipped while the other callbacks' rewrites still apply.- Held by
registered
llm_requestmiddleware, applied atagent/turn_api_request.py:155-170viahermes_cli/middleware.py:88-99(_apply_request_chain:63-85: each callback may return a replacementrequestdict).
B31 Is the stored transcript valid for the provider, and how is it repaired before sending? RULE · code
- Answered how
Always on, before every model request.
prepare_iterationrewrites any stored tool-call arguments that do not parse to"{}"(logging the original at WARNING and adding a corruption-marker tool result), neutralises copies of old interrupt-placeholder rows, and merges rows that break role alternation. The request copy then has orphaned tool results removed, thinking-only assistant turns dropped and adjacent user rows merged, whitespace and tool-call JSON normalised, and lone surrogate characters stripped; every step is a fixed rule.- Held by
agent/turn_iteration_prep.py:194-254(tool-call argument sanitize, interrupt-ghost neutralise:119-141, alternation repair + re-anchor),agent/agent_runtime_helpers.py:233-310(sanitize_tool_call_arguments: unparseable historical arguments ->"{}"+ corruption-marker tool result),agent/turn_request_assembly.py:155-198(_sanitize_api_messages, thinking-only drop / user merge, whitespace strip, canonical tool-call JSONagent/conversation_loop.py:1103-1123, surrogate strip).
B32 The person sent a steering note mid-run: when and where does the model see it, and is it ever deferred to the next turn or dropped? RULE · code
- Answered how
A steering note is buffered by
steerand delivered as its own marked user row placed right after the newest tool result, either at the end of a tool batch or just before the next model request. If there is no tool result to follow (for example a turn with no tool calls), it is put back; at turn end any ticketless leftover is handed back aspending_steerfor the surface to send as the next user turn, while a ticketed gateway transfer stays buffered for the gateway to settle. Only a hard stop drops it.- Held by
Two drains of the same steer buffer (
agent/interrupt_control.py:310-321steer,:398-408_drain_pending_steer; the buffer is a list of(ticket, text)parts whose newline join is_pending_steer,:90-102), both delivering the same row built bysteer_user_row(agent/prompt_builder.py:586-592): after a tool batch,agent/agent_runtime_helpers.py:3668-3706(apply_pending_steer_to_tool_results, called fromagent/tool_executor.py:1210-1221); and before each model request,agent/turn_iteration_prep.py:175-181with_inject_steer_after_newest_tool_result:284-295. With no tool row to follow, the text is put back (agent/agent_runtime_helpers.py:3662-3665viarequeue_drained_steer,agent/interrupt_control.py:111-120;agent/agent_runtime_helpers.py:3696-3701;agent/turn_iteration_prep.py:294-295). At turn end the leftover ticketless text is handed back asresult["pending_steer"](agent/turn_finalizer.py:844-849,_drain_leftover_steeragent/interrupt_control.py:410-417); a ticketed steer (a gateway transfer) stays buffered for the gateway to settle withwithdraw_steer(:419-432). A hard stop drops it (agent/interrupt_control.py:299-307).
B33 Does a plugged-in context engine replace the list of messages sent to the model on this request? RULING · external
- Answered how
Off by default: the default
context.engine: compressoris the built-in compressor, which does not override the hook, so the request is left alone. When an operator setscontext.engineto a user-installed engine (and safe-worker mode is off), that engine'sselect_context()runs before every provider request, retries included, and may return a non-empty list of messages that replaces the request for that call only; None, an empty or malformed list, or an exception leaves the request unchanged.- Held by
agent/context_engine.py:121-141(theselect_context()contract, implemented by the active context-engine plugin), called byagent/conversation_loop.py:1351-1398(_apply_context_engine_selection) fromagent/turn_request_assembly.py:140-147on every provider request. The engine is chosen by thecontext.enginesetting (agent/agent_init.py:1803-1852; defaultcompressorathermes_cli/config_defaults.py:1288).
B34 This model already refused images once in this session: are all images removed from the request it is about to get? RULE · code
- Answered how
On every request attempt,
strip_images_for_rejecting_modelchecks whether the current provider and model pair is in the agent's set of models that have rejected images; if it is, every image part (user uploads included) is removed from that request's copy of the messages, leaving stored history untouched. A model is added to the set only after a provider error matching a known image-rejection phrase, and nothing removes it for the life of the agent.- Held by
agent/message_sanitization.py:443-454(strip_images_for_rejecting_model: lookup of(provider, model)inagent._image_rejecting_models, then_strip_images_from_messages:364-397), called on every attempt atagent/turn_api_request.py:133-134.
B35 What is the model told at the start of the onboarding chat (the rules for the whole run, and what it knows about the machine)? RULE · code
- Answered how
Code answers with fixed text. When the onboarding chat has no messages yet,
opening_promptis sent as a hidden user turn, not the system prompt, carrying the rules for the whole run, the machine facts read at the start (OS, architecture, machine kind, locale), and seven profile dimensions the model is to infer without saying them.- Held by
agent/onboarding_text.py:116-146(opening_prompt) withfacts_block(:102-113) andPROFILE_DIMENSIONS(:85-93). It is sent as a hidden user turn, not the system prompt (:5-6), when the onboarding chat has no messages yet:gateway/session_onboarding.py:91-95, 115-116, 268-274(session authority) andtui_gateway/methods_onboarding.py:215, 235-239(sidecar).
B36 Once a first task is picked in onboarding, what is the model told to build and how? RULE · code
- Answered how
Code answers with fixed text. When a first task is picked, the reminder carries that row's goal, steps, output, quality bar and done test (or a generic brief for a task named in words), the skills to load first, work rules, page and connector rules where they apply, lines for this OS, and the instruction to close with a 'How does this look?' question and then call
setup_beat(beat="done").- Held by
agent/onboarding_text.py:250-264(build_guidance), reached through thebuild_<id>reminder key (agent/onboarding_flow.py:167-171;agent/onboarding_text.py:371-374). It assembles the picked row's goal, steps, output, quality bar and done test (_task_prompt:237-243overagent/onboarding_first_tasks.py:59-249, orOWN_TASK:251-263for a task named in words), the skills to load first (task_skills:318-328), work rules_HOW_TO_WORK(agent/onboarding_text.py:174-181), the preview-pane line (:186-193), page rules for page tasks (:197-211), the connectors playbook for the brief and week tasks (:213-220), host lines per OS (:222-234, 256-262) and the close card instruction_CLOSE(:170-172).
B37 Does this chat's agent get the onboarding tool (setup_beat)? RULE · code
- Answered how
Code answers. On the session authority, a chat that has an onboarding run record, open or ended, gets the
onboardingtoolset on every turn; on the older sidecar only while the run is open, decided when the agent is built. A chat whose toolset list is explicitly empty gets none, and the toolset exists only for desktop sessions.- Held by
agent/onboarding_session.py:39-48(onboarding_chat_toolsets): the session authority folds theonboardingtoolset in for any chat whose run record exists, open or ended, called atgateway/session_policy.py:1031-1033); the sidecar folds it in at agent build only while the run is open (onboarding_toolsetsagent/onboarding_session.py:28-36, called attui_gateway/agent_factory.py:668); a tool refresh keeps it (carry_onboarding_toolsetagent/onboarding_session.py:51-55,tui_gateway/methods_tools.py:321-332). An explicit empty toolset list gets no fold-in, and a list that already namesonboardingis kept as is; any other explicit list getsonboardingadded (keeps_toolsetsagent/onboarding_session.py:14-16). The toolset exists only for desktop sessions (toolsets.py:143-144).
C. Choosing the next action
C1 What should the agent do next: call which tools with which arguments, or answer? RULING · llm
- Answered how
The configured main model decides, on every loop iteration: it gets the system prompt, the conversation and the offered tool schemas, and returns either provider-native tool calls or a text answer. Hermes accepts a tool call whose name it knows (after name repair) and whose arguments parse as a JSON object, then coerces string values to the schema's types; required fields and enum values are not checked before dispatch except for deferred tools behind the tool-search bridge, and no strict schema mode is set on the main request. A text answer is accepted only after the later finish checks.
- Held by
agent/turn_api_call.py:139-145(the main modelagent.model, withagent.provider/api_mode, called throughrun_llm_execution_middleware->_perform_api_call:93-126: streamingagent._interruptible_streaming_api_callorrelay_llm.execute(... agent._interruptible_api_call ...)); request built atagent/turn_api_request.py:136-139.
C2 Before the first model call, which tool calls does a built-in script make? RULE · code
- Answered how
Only when the caller starting the turn passes a prelude; the one in the repository is the onboarding opening. While an onboarding record is active and its name step has not been shown,
onboarding_preludeyields one scripted step, a greeting (skipped if already said) plus asetup_beatcall for the name card, andrun_scripted_preludeplays it through the ordinary tool round as if the model had made the call, before any model request. Ordinary turns have no prelude and go straight to the model.- Held by
agent/turn_scripted_prelude.py:33-54(run_scripted_prelude), entered atagent/conversation_loop.py:1742-1744; the one prelude found isagent/onboarding_session.py:58-74(onboarding_prelude, yields onesetup_beatname card), passed in fromtui_gateway/prompt_turn.py:815-816andgateway/session_onboarding.py:312-321.
C3 (opt-in) Does an external Codex app-server process run the whole turn instead of Hermes' loop? RULING · external
- Answered how
Off by default. Only when the model's
api_modeiscodex_app_serverdoes_turn_without_loophand the whole turn to an external Codex app-server process, which chooses and runs the turn's actions itself; Hermes then splices in the rows it reports. If that run fails in a way that activates a fallback model, the same user turn is re-run on Hermes' own loop.- Held by
agent/conversation_loop.py:1611-1619(_turn_without_loop, called at:1739-1740) and:1622-1637hand the turn toagent._run_codex_app_server_turn; session adapteragent/transports/codex_app_server_session.py:1-60.
C4 Do plugins wrap or replace the provider call itself? RULING · external
- Answered how
Inert on a fresh install: with no
llm_executionmiddleware registered,_run_execution_chaincalls the provider directly. When an installed plugin registers one, each callback receives the request and a single-usenext_call; it may pass a changed request on, wrap the call, or return its own response without the provider ever being called. A callback that raises before callingnext_callis skipped and the chain continues; an error from the provider itself is re-raised.- Held by
agent/turn_api_call.py:128-145(llm_executionmiddleware) ->hermes_cli/middleware.py:136-142,162-215(a callback may return without callingnext_call).
C5 Is the provider's response well-formed enough to use? RULE · code
- Answered how
After every model call,
validate_response_shapeasks the active transport whether the raw response has the expected shape (for example non-emptychoices, or a non-empty content list on the Anthropic wire); a Codex response with statusfailedorcancelled, or empty output with no output text, counts as invalid. An invalid response leads, in order, to credential rotation for Codex soft failures, an immediate switch to a fallback model when one is configured, otherwise jittered retries of 5 to 120 seconds up to the retry limit, and finally a failed turn.- Held by
agent/turn_recovery.py:1572-1621(validate_response_shape), acted on byretry_invalid_responseagent/turn_response_check.py:236-352.
C6 Did the model finish, or was it cut off (finish-reason derivation)? RULING · code
- Answered how
After each response,
_derive_finish_reasontakes the provider's own finish reason through its transport, with two rewrites. On the Codex Responses wire anincompletestatus for the output limit becomes a continuation, or a length retry when a tool call was cut off. For GLM models served by a local Ollama endpoint only, astopafter tool use with at least 20 characters of visible text that does not end in closing punctuation, a code fence or an emoji is treated as cut off; every other provider'sstopis taken as given.- Held by
agent/turn_response_check.py:67-89(_derive_finish_reason), incl. the Ollama-GLM stop->length rewriterun_agent.py:703-742(_should_treat_stop_as_truncated: visible text >=20 chars without a "natural ending" after a tool turn is treated as truncated) and the Codex incomplete mappingagent/turn_response_check.py:47-64,73-74.
C7 After a truncation, continue the text, retry the tool call with a bigger budget, or stop? RULING · code
- Answered how
When a response is cut off,
recover_from_truncationfollows a fixed order. It ends the turn if the stream was cut for runaway repetition, if a context overflow followed partial delivery, or if the reasoning budget was exhausted or the text repeats; on API modes that support continuation, a content-filter stall goes to the fallback model, a cut-off text answer is continued with a user-role nudge up to 4 times, and a cut-off tool call is retried with a larger output budget up to 4 times. Otherwise it rolls back to the last complete assistant turn, or fails the turn when the very first response was cut.- Held by
agent/turn_truncation.py:454-548(recover_from_truncation): abort on thinking- exhaustion or repetition:256-268, content-filter stall -> fallback:271-308, text continuation up to 4 (:311-376, appends a user-role nudge:344-347), truncated-tool-call retry up to 4 withboosted_output_cap(:388-451), window-filled exit:149-158, rollback:540-548.
C8 Is this model output a degenerate repetition loop? RULING · code
- Answered how
Always on, with fixed thresholds:
is_repetition_dominatedcalls text a loop when at least five exact repeats of a line, a 60-character window or a longer unit cover at least half of it, and the stricter runaway test also requires that at most half of its non-empty lines are distinct. A completed answer is checked only at 16,000 characters or more, a live stream from 16,000 characters on, and the same test is applied to truncated replies, interrupt checkpoints and streamed tool arguments. Text judged a loop is dropped and the reply is treated as truncated; for looping tool arguments the call is retried (up to four times), so the turn need not end.- Held by
Consolidated (one detector family,
agent/repetition_guard.py:53-160,RunawayStreamWatch:163-194), five call sites: final textagent/turn_final_response.py:313-324(>=16 000 chars), truncation abortagent/turn_truncation.py:265-267and stream-cut:474-479, interrupt checkpointagent/turn_api_call.py:203-208, streamed tool argumentsagent/chat_completion_helpers.py:3277-3281.
C9 Did the model stop mid-way through a REASONING_SCRATCHPAD block? RULE · code
- Answered how
On every response, a plain string check (
has_incomplete_scratchpad) asks whether the text opens<REASONING_SCRATCHPAD>without ever closing it. If so, the reply is discarded and the call is retried up to 2 times; after that the turn ends with a partial result rolled back to the last complete assistant turn.- Held by
agent/turn_response_intake.py:207-224+agent/trajectory.py:19-21.
C10 A Codex/Responses turn came back incomplete or reasoning-only: continue, nudge, fall back, or stop? RULING · code
- Answered how
Only on the Codex Responses wire, when a response's status is
incomplete:continue_codex_incompletekeeps the interim reply and continues the turn, up to 3 continuations, adding a fixed user-role nudge when a bare retry would resend identical bytes and, if reasoning used up the whole output budget, turning reasoning off and doubling the output cap for the next attempt. Three reasoning-only responses in a row switch to a configured fallback model; if none is left, or the continuations run out, the turn ends with a partial result.- Held by
agent/turn_truncation.py:561-701(continue_codex_incomplete), entered atagent/turn_response_intake.py:226-243.
C11 A provider returned an HTTP-200 safety refusal: try another provider, or show the refusal? RULING · code
- Answered how
When a provider answers normally but with a safety-refusal finish reason,
handle_content_policy_refusalnever retries the same model: if a fallback model is configured and still available it switches to it and restarts the request, otherwise the user gets the refusal, with the provider's own explanation when there is one. The same refusal reached as an HTTP error goes through the error classifier, which aborts and also tries the fallback.- Held by
agent/turn_truncation.py:716-776(handle_content_policy_refusal), routed fromagent/turn_response_check.py:155-171; error-path twinagent/error_classifier.py:487,890-891(_V_CONTENT_BLOCKED: abort + fallback).
C12 The model stopped with only reasoning and no visible text: is the reasoning the answer? RULING · code
- Answered how
Normally no: a reasoning-only reply goes to the empty-response recovery path instead. The reasoning is returned as the answer only on a clean
stopwith no tool calls and no visible text, on a chat-completions route the operator has marked as answering in reasoning (ananswer_in_reasoningcapability on a custom provider or model, or the local Nemotron parser route), never on OpenRouter and never for signed Anthropic thinking. A promoted answer is logged at WARNING and kept out of the stored row's content.- Held by
agent/turn_final_response.py:137-174(promotion gated byanswer_in_reasoning_capabilityagent/reasoning_promotion.py, excluded for signed Anthropic thinkingagent/turn_final_response.py:155-161).
C13 The model returned nothing visible: reuse, nudge, prefill, retry, fall back, or give up? RULING · code
- Answered how
recover_empty_responsewalks a fixed ladder when a reply has no visible text: text already streamed before a dropped connection becomes the answer; earlier visible text followed only by housekeeping tools becomes the answer; after other tool calls the model is nudged once; a thinking-only reply is fed back as a prefill up to 2 times; then empty retries follow, cut short when the empties look deterministic or expensive. After that a configured fallback model is tried, and otherwise the turn ends with an(empty)answer.- Held by
Consolidated: ladder
agent/turn_empty_response.py:168-315+ cost/determinism guardagent/empty_response_guard.py:173-237; entered atagent/turn_final_response.py:179-193.
C14 finish_reason says "tool_calls" but no call arrived: re-prompt or end? RULE · code
- Answered how
When the finish reason is
tool_callsbut the reply carries no tool call, the loop re-prompts the model with a fixed nudge to emit the call, up to 3 consecutive times; both the empty reply and the nudge are marked as scaffolding and not stored. On the fourth such reply, or as soon as a reply ends normally, the counter resets and the reply's text is treated as the answer.- Held by
agent/turn_final_response.py:332-364(max 3 consecutive).
C15 The model named a tool that does not exist: was it a misspelling, and of which tool? RULING · code
- Answered how
When the model names a tool that is not offered,
repair_tool_callfirst trims leaked XML or quote fragments and tries lower-casing, separator and CamelCase normalisation and stripping atoolsuffix; if none of those is a known tool, it takes the single closest offered name with a string similarity of at least 0.7 (difflib). The repaired name replaces the original in place and the call runs as that tool; with no match the call goes to the unknown-tool handling.- Held by
agent/agent_runtime_helpers.py:2555-2602(repair_tool_call: XML-trim, case/separator/ CamelCase/suffix normalisation, thendifflib.get_close_matches(..., valid_tool_names, cutoff=0.7):2601), applied in place atagent/turn_tool_validation.py:101-110.
C16 Some or all tool names are unknown after repair: run the valid ones, retry, or stop? RULE · code
- Answered how
After name repair, if a batch mixes known and unknown tool names, the known calls run and each unknown one gets an error result, and the strike count resets. If every call in the batch is unknown, every call gets an error result (naming the problem and the available tools) and a strike is counted; the third strike in a row ends the turn with a partial result.
- Held by
agent/turn_tool_validation.py:114-155(mixed batch: error only the invalid calls; all-invalid: error results + strike, stop as partial at 3),agent/turn_tool_round.py:104-128, error textagent/conversation_loop.py:1126-1137.
C17 (non-streamed path) Tool arguments are not valid JSON: retry, answer with errors, or refuse as truncated? RULING · code
- Answered how
Before running tools, every call's arguments are checked with
json.loads; dict or list arguments are re-serialised and empty ones become{}. If invalid arguments do not end in}or]they are treated as cut off and the turn ends without running anything; otherwise the model call is silently repeated up to 2 times, and on the third failure each call gets an error result asking for valid JSON so the model can recover. Arguments that parse but are not a JSON object are refused later, at dispatch.- Held by
agent/turn_tool_validation.py:157-236(dict/list -> JSON, empty ->{}, invalid: "truncated" if not ending in}/]-> refuse and end:183-203; else retry the call up to 2 times with nothing appended:205-212, then error results on the third failure:213-232); non-object JSON refused at dispatchagent/tool_executor.py:175-186.
C18 (streamed path) Tool arguments are not valid JSON: repair and execute, or flag as truncated? RULING · code
- Answered how
On a streamed chat-completions reply,
_assemble_tool_callsparses each call's arguments. If they are not valid JSON and the stream ended with a finish reason, a fixed repair (trailing commas, missing closers, control characters) is tried and the repaired JSON is used; if the repair gives nothing usable, or the stream ended with no finish reason at all, the call is flagged as cut off and handed to truncation recovery. With no finish reason, empty arguments or arguments dominated by repetition are flagged the same way.- Held by
agent/chat_completion_helpers.py:3251-3291(_assemble_tool_calls): with a finish_reason,_repair_tool_call_argumentsagent/message_sanitization.py:230-285(trailing commas, rebalance closers, control chars) and the repaired JSON is used;"{}"or no finish_reason -> flagged truncated.
C19 The model issued the same call twice in one batch: run both? RULING · code
- Answered how
No. Before the assistant row is staged,
_deduplicate_tool_callsdrops any call in the batch whose tool name and arguments match an earlier one, comparing valid JSON arguments in canonical form so key order and whitespace do not matter; the first copy runs and each dropped duplicate is logged at WARNING.- Held by
run_agent.py:1202-1219(_deduplicate_tool_calls, canonical-JSON key, first wins), appliedagent/turn_tool_round.py:96-99before the assistant row is staged.
C20 Mixture of Agents: what does each reference model advise (the state, what next, what is wrong)? RULING · llm
- Answered how
Off by default (no MoA preset is active on a fresh install). On a
/moaturn, or when a MoA preset is the model,_run_referencesends each enabled reference model of the preset the conversation, trimmed to that model's window, under a fixed advisory system prompt, in parallel. Each returns free prose that is passed on unchecked; a call that fails becomes a[failed: ...]note, and an empty reply becomes(empty response)and is not counted as a failure.- Held by
agent/moa_loop.py:414-465(_run_reference, the configured reference models on aux taskmoa_reference; calls at:446,:453); fan-out:619-700
C21 Mixture of Agents (one-shot /moa): what private guidance does the aggregator synthesize from the reference answers? RULING · llm
- Answered how
Off by default; only on a one-shot
/moaturn. After the reference models answer,aggregate_moa_contextasks the preset's aggregator model, with a fixed synthesis prompt, to turn the user's prompt and the reference responses into concise guidance on next steps, tool use, risks and disagreements. The free-prose result is attached unchecked as private guidance for the normal agent loop; if every reference failed the aggregator is skipped, and if the aggregator call fails the raw references are passed on instead.- Held by
agent/moa_loop.py:914-988(aggregate_moa_context, the preset's aggregator model on aux taskmoa_aggregator; call at:973)
C22 Mixture of Agents preset used as the model: what does the aggregator do next (tools passed), given the references' guidance? RULING · llm
- Answered how
Off by default; only when a MoA preset is selected as the model. On each model call the facade's
createruns the reference models (reusing the previous results when the conversation has not changed), attaches their answers as private guidance to the conversation, and sends it with the agent's tools to the preset's aggregator model, whose reply is used as the main model's response for that step, tool calls included. If every reference failed, the aggregator acts alone; a disabled preset sends straight to the aggregator.- Held by
agent/moa_loop.py:1395-1483(_build_guidance,create: the facade in front of the preset's aggregator model on aux taskmoa_aggregator) →:1207-1258_call_prepared_aggregator(call built at:1239, sent at:1245)
C23 (gateway proxy mode) What is the reply to this chat message? The whole turn is handed to a remote server instead of the local agent. RULING · external
- Answered how
Off by default: it applies only when
GATEWAY_PROXY_URLorgateway.proxy_urlis set. When it is, the first thing_run_agent_innerdoes is post the turn to{proxy_url}/v1/chat/completionsand the reply is whatever text the remote server streams back; the local agent never runs, and no tool list is sent.- Held by
gateway/run_turn.py:763: the server at the proxy URL, reached byPOST {proxy_url}/v1/chat/completionsinside_run_agent_via_proxy(:661-832). Sent (:704-716): bodymodel: "hermes-agent",stream: true, messages = the gateway'scontext_promptas a system message when non-empty, then every history row whose role is user or assistant and whose content is truthy (tool rows and tool-call-only rows are dropped), then the new user message; headersAuthorization: Bearer <GATEWAY_PROXY_KEY>when a key is set andX-Hermes-Session-Id. No tool list is sent. Taken back (:735-756): onlychoices[0].delta.contenttext; tool calls, reasoning, usage and finish reason in the stream are ignored. The switch that hands the turn over is code:gateway/run_turn.py:2148-2160(if self._get_proxy_url():awaitsself._run_agent_via_proxy(...)and returns its result), the first statement of_run_agent_inner.
C24 Someone commented on a Feishu/Lark document and mentioned the bot: what should the reply say, or should it stay silent? RULING · llm
- Answered how
Only on the Feishu/Lark platform, and only for a comment event addressed to the bot that the document's comment rules allow. A separate agent on the gateway's configured model, limited to the Feishu doc and drive tools and 15 iterations, writes the reply; if it returns nothing or the text
NO_REPLY, nothing is posted.- Held by
the gateway's configured model (
plugins/platforms/feishu/feishu_comment.py:447-462_resolve_model_and_runtime), run as its ownAIAgentat:504-508(_run_comment_agent) with toolsetsfeishu_doc+feishu_drive, 15 iterations, context files and memory skipped
C25 The desktop preview pane cannot load a local URL: which server should be running there, and which commands restart it? RULING · llm
- Answered how
Only when the desktop preview pane sends a
preview.restartrequest for a URL it cannot load. Hermes then starts a hidden one-off agent on the parent session's model, limited to the terminal and file tools and given the parent's recent history; that agent decides which server to start and runs the commands itself.- Held by
the parent session's model (
tui_gateway/agent_callbacks.py:413), run as a hidden one-offAIAgentattui_gateway/methods_prompt.py:1256-1260(preview.restartRPC:1227-1271) with toolsetsterminal+fileonly (tui_gateway/agent_callbacks.py:427-429)
C26 /btw with no live agent (or after the fork failed): what is the answer to the side question, given only a transcript digest? RULING · llm
- Answered how
When
/btwhas no live agent to fork, or the fork fails or returns an empty answer, one stateless model call answers from a text digest of the transcript. It goes to the main provider and model unlessauxiliary.side_questionnames another; it has no tools.- Held by
agent/side_question.py:19: aux taskside_question, one stateless call atagent/oneshot.py:104(run_oneshot), reached fromagent/side_question.py:136-144(_answer_via_oneshot). Model: the main provider and model first unlessauxiliary.side_questionoverrides (agent/auxiliary_client.py:4571-4581)
C29 During onboarding, which card comes next, and what is the model told to do after each card is settled and before each user turn? RULE · code
- Answered how
Code answers. The cards run in a fixed order (name, theme, layout, connectors, plugins, tour, build), the opening name card is shown by a scripted step, and
setup_beat(beat="next")shows the first card still pending or open; plugins and tour are skipped when this machine has no onboarding plugins or tours are off. After a card settles, times out or is skipped, code attaches a fixed reminder naming the pick and the next card, and before each user turn while a card is closed it adds a note on how to apply it or bring it back.- Held by
Order:
agent/onboarding_flow.py:174-186: the fixed beat orderBEATS(name, theme, layout, connectors, plugins, tour, build);next_beatreturns the first beat still pending or shown (:210-212). Beats this machine does not get are marked skipped when the run starts (_settle_unfit:203-207) and again if the model tries to show one (:245-248): plugins only when the OS has onboarding catalog plugins (:180-181), the tour only when tours are on (:182-183). Those facts are read once at the start (agent/onboarding_facts.py:15-27).setup_beat(beat="next")uses this order (tools/setup_beat_tool.py:151-155). After each card:apply(agent/onboarding_flow.py:412-424) returns a reminder key when a card settles, times out or is skipped, and on some refusals (a card that is simply shown gets none);Beat.afterpicksafter_<beat>,tour_<preset>orbuild_<id>(:167-171).reminderrenders the key (agent/onboarding_text.py:363-377) from_REMINDERS(:318-336),_wrote(:345-353),_timed_out(:356-360) and_already(:339-342);_nextnames the next card and, before the build card, the menu of first tasks (:282-287)._resultattaches the text to the tool result asreminder(tools/setup_beat_tool.py:88-93). Before each user turn:onboarding_turn_note(agent/onboarding_session.py:77-86): no note when the history is empty, a card is waiting, or the run is not open; otherwiseturn_note(agent/onboarding_text.py:380-388), empty once every card is settled (:383-385). Appended to the turn's API-only notes (agent/onboarding_session.py:89-92) byarm_onboarding_turn(gateway/session_onboarding.py:309-321, called atgateway/run_turn_runner.py:1011-1012) andtui_gateway/methods_onboarding.py:303-308.
C30 Which first tasks go on the onboarding build card, and how are they worded? RULING · llm
- Answered how
A model call decides. Before the build card, the reminder lists the first-task rows this machine qualifies for (gated only on installed plugins, connected apps and the OS) and tells the model to pick up to four that fit the person and reword each label. Code refuses unknown, repeated, empty or over-32-character picks and the fixed row, which it always adds last.
- Held by
the model, calling
setup_beat(beat="build", options=[...])with up to four picks (tools/setup_beat_tool.py:199-202), told to choose rows 'that fit what you learned about them' and reword each label (agent/onboarding_text.py:271-279). Code bounds the choice: the rows it may pick are filtered by hard facts only (agent/onboarding_first_tasks.py:3-5, 268-270: a plugin row needs that plugin installed:46-48, the brief and week rows need an app connected:51-52, the Mac row needs macOS:55-56), andpick_errorrefuses unknown, repeated or over-long picks and the fixed row (:278-315). The fixed row 'Explain how Hermes works' is always added last by code (:19-20, 273-275).
C31 Before the connectors card in onboarding, does the model offer to look around the computer, and which apps does it suggest connecting? RULING · llm
- Answered how
A model call decides. Code refuses the first attempt to show the connectors card without suggestions and tells the model to offer once to look around the computer; whether to look is the person's yes or no in chat, and code does not check that an offer was made or answered. Suggested slugs must be on the hosted connector list when that list can be read.
- Held by
the model, which passes connector slugs in
suggestwhen it shows the connectors card (tools/setup_beat_tool.py:197-198; card data:130-131). Code refuses only the first try to show the card without suggestions, answering 'offer first' with the_LOOK_AROUNDtext (agent/onboarding_flow.py:252-256;agent/onboarding_text.py:151-156, 323); suggestions are taken to mean it already looked (agent/onboarding_flow.py:241-242, 253), so code does not check that an offer was made or answered. When the hosted connector list can be read, suggestions must be slugs on it, or the card is not shown (tools/setup_beat_tool.py:96-119, 158-159;tools/connectors/targets.py:134-145). Whether to look is the person's yes or no, asked by the model in chat.
C32 In onboarding, when the person types a message instead of answering the card, did that message answer or skip the card, and with what value? RULING · llm
- Answered how
A model call decides on the person's next turn, prompted by a note naming the closed card: it records a typed answer with
setup_beat(value=...)or a skip withskip=true, and applies theme and layout throughset_theme. Code refuses a value outside the card's ids and a theme or layout recorded in words. On the name card only, typed words are taken as the name by code, without the model.- Held by
the model, on the person's next turn. It records an answer given in words with
setup_beat(beat=..., value=...)or a skip withskip=true(tools/setup_beat_tool.py:54-62), or applies theme and layout throughset_theme(agent/onboarding_text.py:158-168). It is told how by the turn note (agent/onboarding_text.py:380-388, C29) and thewrotereminder (:345-353); for the tour it is told to bucket their words into quick, full or skip (:165). Code checks the value against the card's ids (agent/onboarding_flow.py:324-335) and refuses a theme or layout recorded in words, pointing toset_theme(:319-328).
C33 Does the person end onboarding early (the Skip setup button)? RULING · human
- Answered how
The person decides, by pressing Skip setup in the desktop app; the model is told only the person can end it that way. Code then ends the run as skipped, marks every unsettled card skipped and withdraws the waiting card; on the session authority this needs
session:controlon the chat.- Held by
the person, pressing Skip setup in the desktop app, which sends
onboarding.skip:gateway/session_onboarding.py:282-306(session authority; requiressession:controlon the chat,:298) andtui_gateway/methods_onboarding.py:243-258(sidecar). It ends the run asskipped(agent/onboarding_flow.py:110-115, 344-349) and withdraws the waiting card (gateway/session_onboarding.py:303-305).
C34 When does an onboarding run count as finished, and does the install stop opening onboarding? RULE · code
- Answered how
Code answers. The run completes as soon as the first-task (build) card is answered or recorded;
setup_beat(beat="done")is refused only while the build beat is still pending, and otherwise completes the run. When a run ends (completed, skipped, replaced or deleted) and no other run is open, code writesonboarding.run: falsein the root profile's config so onboarding stops opening, and tells every window.- Held by
agent/onboarding_flow.py:412-424(apply): the run completes when the build beat settles, that is when the first-task card is answered (:413-414, 419-421);doneis refused only while the build beat is still pending, that is neither shown, answered nor skipped (:353); otherwisedonecompletes the run even with no answer (:352-358).agent/onboarding_record.py:47-70(_transition) then closes the install-levelonboarding.runflag unless another run is still open (:68-69,_close_install_flag:73-84), writing it in the ROOT profile's config (hermes_cli/onboarding_run.py:1-6, 54-70). Every window is told the run ended (agent/onboarding_record.py:27-44, 66-67;tui_gateway/methods_onboarding.py:311-316).
C35 In the onboarding chat, does a general tool's result (set_theme, manage_connections display, gui_tour) settle a setup card, and what is the model then told? RULE · code
- Answered how
Code answers. In a chat whose agent has
setup_beat, every tool result that was neither blocked nor an error is checked: an applied or picked accent or mode fromset_themesettles the theme card, a layout settles layout, amanage_connectionsdisplay with a connected list settles connectors, and a startedgui_toursettles the tour. The model then gets anafter_<card>reminder, oralready_<card>for a repeated theme or layout.- Held by
agent/onboarding_record.py:161-178(observe_tool_result), run on every tool result that was neither blocked nor an error (agent/tool_executor.py:1092-1093) and acting only in a chat whose agent carriessetup_beat(agent/onboarding_record.py:166).agent/onboarding_flow.py:361-374(_applied_beats) maps a result to beats: an applied or picked accent or mode settles theme, a layout settles layout, amanage_connectionsdisplay withconnectedsettles connectors, a started tour settles tour;_applied(:377-390) returnsafter_<beat>or, for a repeat,already_<beat>.
D59 Do plugins rewrite a tool call's arguments before any check sees them? RULING · external
- Answered how
Off by default: with no plugin enabled, no tool-request middleware is registered and the model's arguments pass through unchanged. When an enabled plugin registers
tool_requestmiddleware, each callback in turn may return replacement arguments, and the last replacement is what plugin hooks, guardrails, approvals and execution then see; the rewrite is recorded in a trace but nothing in Hermes judges it.- Held by
plugin callbacks registered with
ctx.register_middleware("tool_request", cb)(hermes_cli/plugins.py:939-944), run byhermes_cli/middleware.py:102-133(apply_tool_request_middleware) through_apply_request_chain:63-85andhermes_cli/plugins_dispatch.py:733-763(invoke_middleware). Call sites:agent/tool_executor.py:799-809(agent loop),model_tools.py:766-776called at:946-948(direct dispatch),agent/agent_runtime_helpers.py:2487-2494.
D. Gatekeeping: may this action run?
D1 Is this shell command catastrophic enough to refuse outright, whatever the settings (yolo, approvals off, cron approve)? RULING · code
- Answered how
Fixed code patterns decide. On the default local terminal backend, every command is first matched against the hardline list (for example deleting the filesystem root), and a command too complex to parse or with malformed quoting counts as a match too; a match is refused before yolo,
approvals.mode: off, the allowlist or any prompt is consulted. On an isolated backend (Docker without host mounts, Modal, Daytona, Vercel sandbox) this check is skipped and only the operator's deny rules apply.- Held by
tools/approval_detection.py:216-241detect_hardline_commandoverHARDLINE_PATTERNS:124-157; applied first intools/approval.py:1099-1122_floor_block, before yolo/mode-off (tools/approval.py:1193-1195,:1133-1135)
D2 Does this command match one of the operator's approvals.deny globs ("never, even under yolo")? RULE · code
- Answered how
Off in effect by default:
approvals.denyships as an empty list, so nothing matches. When the operator adds globs, code matches each case-insensitively against normalised variants of the command and refuses on a hit, before yolo,approvals.mode: offand the isolated-container shortcut.- Held by
tools/approval_floors.py:23-42_match_user_deny_rule(fnmatch over deobfuscated variants); called fromtools/approval.py:1088-1096, also on the isolated-container fast pathtools/approval.py:1131-1132,:1190-1191
D3 Would this command or write delete or overwrite the Python interpreter/venv this Hermes process boots from? RULE · code
- Answered how
Code decides, with no setting to turn it off. A terminal command that would delete the interpreter or venv Hermes runs from is refused alongside the hardline floor, before yolo or any prompt; a file-tool write to those paths is refused as a protected path. On an isolated terminal backend the command check is skipped with the rest of the floor.
- Held by
agent/runtime_self_protection.py:245command_deletes_runtime(commands, viatools/approval.py:1113-1116) and:166is_protected_path(file writes, viaagent/file_safety.py:271-274). Consolidated: same question, same module.
D4 Is the model piping a guessed password into sudo -S? RULING · code
- Answered how
A regular expression decides: when
SUDO_PASSWORDis not set, a terminal command containing an explicitsudo -Sis refused outright, before yolo or any prompt. It runs only on the terminal tool's path, not incheck_dangerous_command, and not on an isolated terminal backend.- Held by
tools/approval_detection.py:204-213_check_sudo_stdin_guard(only whenSUDO_PASSWORDis unset); applied in the terminal path only (tools/approval.py:1193passessudo_guard=True;check_dangerous_commanddoes not)
D5 Does this shell command need a person's OK before it runs? RULING · code
- Answered how
Fixed patterns decide: the command (and normalised variants of it) is matched against the dangerous-pattern list, and the first match flags it. A flagged command that is not already approved goes to the approval step; at the default
approvals.mode: smarta model call looks at it before any person does. Under yolo,approvals.mode: offor an isolated terminal backend, flagged commands run without that step.- Held by
tools/approval_detection.py:1680-1685detect_dangerous_command(first match from_iter_dangerous_descriptions:1635-1677) overDANGEROUS_PATTERNS:245-500(about 115 descriptions), plus execution-flag findings:1020and the gateway-lifecycle splice check:1555-1572
D6 Is the terminal backend isolated enough that dangerous-command prompts can be skipped? RULING · code
- Answered how
Code decides from the terminal backend name. At the default
terminal.backend: localthe answer is no, so the normal checks run. For Docker without bind-mounted host paths, Modal, Daytona or a Vercel sandbox (or a plugin backend that declares it) the answer is yes: only the operator's deny rules are checked and everything else is approved.- Held by
tools/approval.py:1071-1085_should_skip_container_guards(docker without host mounts, modal, daytona, vercel_sandbox; plugin backends viaprovider_flag)
D7 Has a person already approved this command (pattern key, exact text or glob) for this session or permanently? RULE · code
- Answered how
Code looks the command up in two places: the session's approved pattern keys plus the profile's permanent
command_allowlist, re-read from disk on every check so hand edits take effect at once. The allowlist also matches the exact command text or a glob, but never for a compound command (pipes,;,$(...)and similar). A match skips the prompt; on a fresh install both are empty.- Held by
tools/approval.py:382-388is_approved;tools/approval_floors.py:184-200_command_matches_permanent_allowlist(refuses compound commands:140-181); persistencetools/approval.py:412-425, reconcile-with-disk:481-512and on every check:364-379(_sync_permanent_from_disk)
D8 Should this session skip approvals from now on? RULING · human
- Answered how
Off by default. The person turns it on for one session (Shift+Tab in the CLI, the Desktop toggle,
/yoloin a messaging gateway), which is stored and survives a restart, or for one launched process with--yolo, which must be given again next time. While it is on, approval prompts are skipped, but the hardline list, deny rules and protected instruction-file writes still apply.- Held by
the person, via
tools/approval_yolo.py:31-43toggle_session_yolo(CLI Shift+Tab, TUI, Desktop zap, gateway/yolo); read bytools/approval.py:338-344_yolo_active
D9 Is there anyone who can answer an approval prompt in this context, and if not, does a flagged action run or stop? RULE · code
- Answered how
Code decides from the run context. If an interactive CLI, a gateway session or an ask bridge is present, the flagged command goes to the approval step. A single-query (
-q) run, a cron job or an unattended platform such as a webhook refuses it under the shippeddenymodes. Any other headless run with none of these markers lets a flagged shell command run without asking; plugin-escalated tool calls fail closed there instead.- Held by
tools/approval.py:961-993_presence;tools/approval_context.py:167-260predicates;tools/approval.py:650-666_unattended_contexts; the headless defaulttools/approval.py:1209-1216(no CLI, no gateway, no unattended marker → approved)
D10 Is this flagged command actually safe to run, should it be refused, or does a person need to look? RULING · llm
- Answered how
On by default:
approvals.modeships assmart. When a flagged command orexecute_codescript reaches the approval step with someone able to answer, one auxiliary model call (temperature 0, a one-word reply) returns APPROVE, DENY or ESCALATE. APPROVE runs that command only; DENY blocks it, though a present owner may still allow that one operation; ESCALATE, an empty reply or an error goes to the person.- Held by
tools/approval_smart.py:88-146_smart_approveviacall_llm(task="approval"):123-127; wrapped by_smart_verdict:149-170; consumed bytools/approval.py:792-821_smart_gate. Active only whenapprovals.mode: smartand only for the command gate and the execute_code gate (tools/approval.py:1228,:1319), not the action gate.
D11 Do I allow this flagged action, and for how long (once / session / always / deny)? RULING · human
- Answered how
At the default
approvals.mode: smarta model call answers first, and the person is asked only when it escalates or fails, or to override a model DENY for one operation. The person answers at the CLI prompt or through gateway buttons or/approve, choosing once, session, always or deny; an unanswered or undeliverable prompt is a denial. Withapprovals.mode: manualthe person is asked directly.- Held by
the person at the active surface; engine
tools/approval.py:824-958_human_decision→ selected plugin transport (D18) → gateway round-triptools/approval_gateway_wait.py:140-264→ pending fallbacktools/approval.py:578-611→ CLI prompttools/approval_prompt.py:20-184(prompt_dangerous_approval, body in_ask_human:110-184)
D12 Has the guardian refused enough commands in a row that the model must stop trying variations? RULING · code
- Answered how
Code counts consecutive DENY verdicts from the smart-approval model in one session; at the default threshold of 3 the denial message gains a hard-stop instruction telling the model to stop trying variations and report to the user. It changes only the message text, it does not block anything further; any approval resets the count, and a person's own denials are not counted.
0turns it off.- Held by
tools/approval.py:63-116(_record_denial,_denial_breaker_addendum); thresholdapprovals.denial_breaker_threshold:73-78
D13 Is an approval collected ahead of time for a batch of terminal calls still valid for this command now? RULE · code
- Answered how
Applies only in Desktop sessions with an approval notifier, when the model issues two or more terminal calls together. Code then hands back the prepared decision once, only if the call id and the exact command, backend and host-access match. It is dropped after any earlier command in the batch failed. A decision no person answered (yolo, approvals off, allowlist) is re-checked against the policy in force now, so the live guard runs instead, except that an approval from the smart model stays usable while
approvals.modeis stillsmart(the default).- Held by
agent/terminal_approval_batch.py:219-246consume_prepared_guard; argument-change refusal:268-276; batching only for Desktop sessions with a notifier:292-308
D14 Does this whole Python script need a person's OK before it runs? RULING · code
- Answered how
Code decides from the context. In the interactive CLI or a local viewer's own turn the script is not gated as a whole (its terminal calls are checked one by one); cron,
-qand unattended platforms refuse it under the shippeddenymodes. In a messaging gateway or ask context the whole script goes to the approval step, where at the defaultapprovals.mode: smarta model call answers before any person.- Held by
tools/approval.py:1251-1321check_execute_code_guard; calledtools/code_execution_tool.py:776-777
D15 Should this tool call be vetoed, rewritten, or escalated to a person before it runs? RULING · external
- Answered how
Off by default: with no plugin or shell hook enabled, no hook returns anything and every tool call proceeds. When hooks are installed, each
pre_tool_callresult is collected: any validblockvetoes the call with its message, otherwise the firstapprovesends the call to the approval step (an error or timeout there blocks it), andmodifyresults are merged into the arguments.- Held by
hermes_cli/plugins.py:1907-1959_get_pre_tool_call_directive_details(block > approve > modify); resolution:1987-2016; dispatchhermes_cli/plugins_dispatch.py:275-324(invoke_hook,_run_hook_callbacks); shell scripts bridged byagent/shell_hooks.py:540-615(_make_callback,_evaluate_result). Called atagent/tool_executor.py:661-675,model_tools.py:790-801,agent/agent_runtime_helpers.py:2454-2467
D16 Do I allow this configured hook script to run on these events? RULING · human
- Answered how
Applies only when the config lists shell hooks; none ship by default. The first time a new event and command pair is seen, the person at a terminal answers a y/N prompt and a yes is recorded. Without a terminal the hook is not registered; with
--accept-hooks,HERMES_ACCEPT_HOOKSorhooks_auto_accept: true(default false) it is approved without asking.- Held by
the person at a TTY,
agent/shell_hooks.py:764-784_prompt_and_record; skipped by--accept-hooks/HERMES_ACCEPT_HOOKS/hooks_auto_accept(:816-823); non-TTY → not registered (:770)
D17 May this background-review or side-question fork call this tool? RULE · code
- Answered how
Code checks a per-thread allow set before any plugin hook runs; a tool not in it is blocked with a fixed message. A
/btwfork gets an empty set, so every tool is refused. A background-review fork may use the skills tools,read_fileandsearch_files, the memory tool only on memory-triggered reviews with memory on, plus any names inauxiliary.background_review.extra_tools.- Held by
thread-local
hermes_cli/plugins.py:1884_thread_tool_whitelist, checked at:1918-1921; set byagent/background_review.py:1198-1206(whitelist from:1102-1142) andagent/side_question.py:119-121(empty set)
D18 If the operator's chosen approval channel (a plugin) is unavailable or fails, may a built-in prompt be used instead? RULE · code
- Answered how
Does not arise by default:
security.approval.transportships asbuiltin, so the built-in prompts are used directly. When an operator names a plugin transport and it is missing, errors or times out, code denies the action unlesstransport_fallback: builtinis set; an unreadable setting also denies.- Held by
tools/approval_prompt.py:199-277; configtools/approval_context.py:374-385
D19 Is this write aimed at a file that steers future agent behaviour (AGENTS.md, CLAUDE.md, SOUL.md, .cursorrules, project .hermes/)? RULING · code
- Answered how
Code decides, on by default (
security.protected_instruction_filesdefaults to true, and an unreadable config keeps it on). Awrite_fileorpatchwhose target, or its resolved symlink target, is named AGENTS.md, CLAUDE.md, SOUL.md or .cursorrules in any folder (any letter case), or sits directly inside a project.hermesfolder, is sent for a person's approval. Files under the Hermes home itself are left to other guards.- Held by
tools/file_tools_write_guards.py:192-193list,:214-256_protected_instruction_reason; called fromtools/file_tools.py:788,:875(write_file, patch only)
D20 Do I allow this one write to an agent-instruction file? RULING · human
- Answered how
The person decides, every time, with no model call first: yolo,
approvals.mode: off, smart mode and the allowlists do not apply. They answer through the gateway's buttons or the CLI panel, and any yes covers this one write only. With no channel, or in-q, cron or unattended runs, the write is refused, as are a timeout, a withdrawn prompt and a failed delivery.- Held by
the person,
tools/file_tools_write_guards.py:263-346_request_protected_instruction_approval(gateway button or CLI panel; deliberately NOT_run_approval_gate, so yolo and allowlists do not apply,:266-268)
D21 Is this file-tool write aimed at a system path or the active profile's config.yaml? RULING · code
- Answered how
Code refuses, with no approval option, any
write_fileorpatchto a Windows device-namespace path, to/etc/,/boot/,/usr/lib/systemd/and their macOS mirrors, to the Docker socket, to the active profile'sconfig.yaml, or to a path the skill lock refuses. On an SSH backend whose home cannot be detected, a path climbing above it is refused too. The message points the model to the terminal with sudo for system files.- Held by
tools/file_tools_write_guards.py:147-177_check_sensitive_path(NT/device-namespace paths:154-156; a path climbing above an undetectable SSH home:158-161; the prefixes and exact paths at:33-37, among them/etc/,/boot/and the docker socket; the activeconfig.yaml:168-173; and paths refused bytools.skill_lock.path_refusalat:174-176);tools/file_tools.py:780,:873
D22 Is this path a credential store, an application-owned Hermes store, or outside HERMES_WRITE_SAFE_ROOT? RULING · code
- Answered how
Code refuses the write outright, without an approval option, if the path is a credential file or directory (SSH keys,
~/.aws,.netrc, Hermes.envand OAuth stores,/etc/shadow), the Hermes runtime itself, or Hermes-owned state (state.db, sessions, MCP tokens, vault, browser profile).HERMES_WRITE_SAFE_ROOTis unset by default, so the outside-the-root check does not apply unless an operator sets it.~/.ssh/configis left to the approval step instead.- Held by
agent/file_safety.py:255-307_classify_write_denialwith lists:178-252; consumed bytools/file_operations.py:1310,:1349,:1530,:1630
D23 Is this write to a path that needs approval rather than a block (~/.ssh/config)? RULE · code
- Answered how
A fixed list decides: today only
~/.ssh/config. A file-tool write there goes to the ordinary approval step, which yolo andapprovals.mode: offskip and which no model call answers; otherwise the person chooses once, session or always. With no one to ask (headless,-q, cron, unattended) it is refused.- Held by
agent/file_safety.py:232-240,:330-334;tools/file_tools_write_guards.py:362-407hands it to D11
D24 Would this text write destroy a binary document (docx/xlsx/pdf/sqlite)? RULE · code
- Answered how
Code decides by file extension. A text write to an Office-style document (docx, xlsx, pptx and similar) or a SQLite sidecar file is always refused. Overwriting an existing PDF or other binary file is refused, creating a new one is allowed, and if existence cannot be checked where the write would land, the write is refused.
- Held by
tools/file_tools_write_guards.py:486-550
D25 May write_file replace this file when this task never saw its full current content? RULE · code
- Answered how
Code refuses a
write_filethat would replace an existing file this task has not seen in full (never read, read only in part or redacted, or patched without a full read), or that changed on disk since it was read. New files, files the task read in full or wrote itself, and unresolvable paths proceed. The model is told to read the whole file or usepatch.- Held by
tools/file_tools_write_guards.py:560-597;tools/file_tools.py:897
D26 May the agent read this file (credential stores, MCP tokens, vault, browser profile, project .env, skills-hub cache)? RULING · code
- Answered how
Code refuses a file-tool read of the skills-hub cache, the named Hermes credential stores, anything under the Hermes
mcp-tokens,browser-profile,vaultorwebappfolders, or any.env-style file anywhere on disk (.env.exampleis suggested instead). There is no setting or approval to allow it.- Held by
agent/file_safety.py:375-421get_read_block_error; consumedtools/file_tools.py:244,:648,:1092,tools/credential_files.py:93,tools/transcription_tools.py:429
D27 Would this command (or a script it runs) stop, restart or uninstall the supervised gateway it is running inside? RULE · code
- Answered how
Applies only when Hermes runs as a supervised gateway service. There, before any other terminal check, code scans the command and any script it runs, and refuses a gateway restart, stop or uninstall,
launchctl submit/bootstrap, or a broad kill of the host interpreter; a referenced script it cannot scan is refused too. Nothing, includingforce, bypasses it; in the CLI or an unsupervised gateway it does not run.- Held by
tools/terminal_tool_guards.py:255-340gateway_lifecycle_block→cron/lifecycle_guard.pyscan_gateway_lifecycle; run first intools/terminal_tool.py:1413-1437, unconditionally (forcecannot bypass,:1535)
D28 Would this git command rewrite the live checkout this Hermes process runs from? RULE · code
- Answered how
Applies only on Windows with the local terminal backend. There, code refuses a git command that would rewrite the source checkout this Hermes process is running from; on macOS, Linux or a remote backend nothing is checked.
- Held by
tools/self_repo_guard.py:545-553; active only whenos.name == "nt"(:538-542);tools/terminal_tool_guards.py:343-368
D29 Does the requested workdir contain characters that could act as shell syntax? RULE · code
- Answered how
Code checks every character of a requested
workdiragainst an allow list (letters, digits and a fixed set of safe punctuation; control characters never pass) and refuses the command on the first other character. It runs only when the model passes aworkdir, after the gateway lifecycle check.- Held by
tools/terminal_tool_guards.py:38-47;tools/terminal_tool.py:1427-1432
D30 When a subagent's command is flagged and nobody can be asked on its thread, is it denied or allowed? RULE · code
- Answered how
Denied by default: each subagent worker thread is given an approval callback that answers
denyand logs a warning, unlessdelegation.subagent_auto_approveis true, in which case it answersonce. At the defaultapprovals.mode: smartthe approval model still looks first and can approve on its own; gateway sessions use their own approval queue instead.- Held by
tools/delegate_tool_config.py:44-61; installed per child workertools/delegate_tool_child_run.py:921-923
D31 Which tools may a scheduled job's agent never have? RULING · code
- Answered how
Code removes the messaging and clarify toolsets from every scheduled job's agent, and the cronjob toolset too unless
cron.allow_agent_schedulingis set (default false). Anything inagent.disabled_toolsetsis added, so a job's own tool list cannot widen past it.- Held by
cron/scheduler.py:415-436(messaging, clarify always; cronjob unlesscron.allow_agent_scheduling)
D32 Does this cron prompt (or the prompt assembled with its skills) carry an injection or exfiltration payload? RULING · code
- Answered how
Code answers with fixed regex lists. The prompt a person or the model writes is scanned strictly at create, update and run-now (invisible unicode blocks it, then eight injection/secret-reading/destructive patterns and five curl/wget secret-exfiltration patterns); a match refuses the job. At fire time the prompt assembled with skill bodies gets only the four injection-directive patterns, and invisible unicode there is stripped and logged instead of blocking.
- Held by
tools/cronjob_prompt_scan.py:121-130(strict, user prompt),:133-144(loose, assembled); calledtools/cronjob_tools.py:696,:797,:856
D33 May this script run on schedule (pre-run data script or no_agent job)? RULE · code
- Answered how
Code answers twice. When the job is created, the script path must be relative (absolute,
~and drive-letter paths are refused), must not escapeHERMES_HOME/scriptsand must already exist there. Each time the job fires the path is resolved again, this time also accepting absolute and~paths as long as they resolve inside that folder; the script then runs as a subprocess whose environment comes frombuild_subprocess_env.- Held by
create time
tools/cronjob_job_args.py:416-440(relative only); run timecron/scheduler_script.py:323-366(relative, absolute and~accepted if insideHERMES_HOME/scripts); executed:432-455viabuild_subprocess_env, never through the command guards
D34 Which environment variables (credentials) may a shell, execute_code, MCP or cron child see? RULING · code
- Answered how
Code answers by filtering the parent environment each time a child is spawned: Hermes-internal secrets and Hermes-managed provider credentials are always dropped; other blocklisted names pass only when they are on the passthrough allowlist, and first-party terminal variables have their own exception. The allowlist holds names a loaded skill declared or the operator listed under
terminal.env_passthroughin config.yaml (empty by default); a Hermes-managed provider credential is refused from it and checked again at use.- Held by
tools/environments/local.py:266-323(_filter_secret_env,_scrubbed_env),:381build_subprocess_env; passthrough allowlisttools/env_passthrough.py:46-133(skill-declared names refused if Hermes-managed)
D35 Which profile's secrets may this task read, and what happens when none is bound? RULE · code
- Answered how
Code answers in
get_secreton every credential read. Global names always come from the process environment; otherwise a bound profile scope is read first, and a miss falls back to the process environment unless the scope serves a routed profile. With no scope bound, a single-profile install reads the process environment; when multiplexing is active (one gateway serving several profiles) it reads the launch-body scope if one is set and otherwise raisesUnscopedSecretErrorrather than guess.- Held by
agent/secret_scope.py:230-261get_secret(raisesUnscopedSecretErrorin multiplex mode with no scope, unless a launch-body scope is set, which it then reads,:248-252),:154-162set_secret_scope
D36 Does this memory or skill write commit now, wait for a person, or get staged for later review? RULE · code
- Answered how
Off by default:
memory.write_approvalandskills.write_approvalboth ship false, so every memory and skill write commits at once. When the subsystem's setting is on, code decides: skill writes and any write from the background review are staged as a pending file; a foreground memory write asks the person inline when an interactive channel can answer, commits on yes, is refused on no, and is staged if nobody can answer or the prompt fails.- Held by
tools/write_approval.py:201-216evaluate_gate; enabled check:44-52; staging:74-89
D37 Do I allow this memory/skill write? RULING · human
- Answered how
Off by default, because
memory.write_approvalandskills.write_approvalship false and writes commit without asking. When the setting is on, the person answers: a foreground memory write in an interactive session shows a "Save to memory?" prompt (once or session approves, deny refuses), and every other gated write waits as a pending record until the person approves or rejects it with/memory pendingor/skills pendingand the approve/reject commands.- Held by
the person: inline CLI callback
tools/write_approval.py:219-248(memory, foreground, interactive only) or out-of-band/memory pending,/skills pendingreview of staged records (:190-198) Also: the person via/memory pending|approve|reject,/skills approve(hermes_cli/write_approval_commands.py:136-139→tools/memory_tool.py:353,tools/skill_manager_tool.py:716) or the inline prompt (tools/write_approval.py:201-245).
D38 Is this MCP tool read-only, so an untrusted server's call may skip the prompt? RULING · external
- Answered how
Only matters for a server configured
trust: untrusted; a server with no trust setting is treated as fully trusted and never prompts. For an untrusted server, the MCP server itself answers: a tool counts as read-only only when the server's own tool listing marked itreadOnlyHint=trueat discovery, and missing or malformed hints are treated as write-capable.- Held by
tools/mcp_tool_handlers.py:52-57reads_tool_read_only_hintscaptured at discovery; used by:123-147
D39 Do I accept what this MCP server's elicitation asks? RULING · human
- Answered how
Elicitation is on for every MCP server by default, and the person answers an approve/deny prompt on whatever surface owns the session (a gateway button or the CLI/TUI panel), covering this one request. Code declines first without asking when nobody can answer (a single-query, cron or unattended session, or a gateway session with no notify callback), and URL-mode requests are always declined; a timeout returns cancel.
- Held by
the person, via
tools/approval_prompt.py:296-376request_elicitation_consent; callertools/mcp_tool_sampling.py:285-321(elicitation)
D40 Do I allow this write-capable tool on an untrusted MCP server to run? RULING · human
- Answered how
Only for a server configured
trust: untrusted(unset means fully trusted, no prompt). For such a server's write-capable tool, code answers first: YOLO,approvals.mode: off, or a remembered Session/Always answer for that server and tool lets the call run. Otherwise the person is shown the redacted arguments and answers once, for the session, or always (once only when the call also passes the chat's spend limit); anything but accept, including an approval-system error, blocks the call.- Held by
the person, via
tools/approval_prompt.py:296-376request_elicitation_consent; callertools/mcp_tool_handlers.py:150-208(untrusted write tool), asked only when no remembered Session/Always answer, YOLO orapprovals.mode: offcovers the call (:67-73,:134-135)
D41 Do I allow a saved payment card to be filled into this checkout page? RULING · human
- Answered how
Each time the browser vault is about to write a saved card into a page, the person is asked "Fill payment card '<label>' on <origin>" through the session's approval surface (gateway button or CLI panel), for this fill only. Only an explicit accept fills the card; a session where nobody can answer, a timeout or an error refuses it.
- Held by
the person, via
tools/approval_prompt.py:296-376request_elicitation_consent; callertools/browser_vault_tool.py:622-632(card fill)
D42 May this MCP server make the user's model generate text for it (and with which model, how many tokens, how often)? RULING · code
- Answered how
On by default for every MCP server, with no prompt to the person. Code admits each sampling request against the server's settings: at most
max_rpmrequests in a sliding minute (default 10), the requested model only if it is inallowed_models(empty by default, so any), tokens capped atmax_tokens_cap(default 4096), and at mostmax_tool_roundsconsecutive tool-use rounds (default 5).- Held by
tools/mcp_tool_sampling.py:184-196_admit(sliding 60 s rate limit, default 10/min;allowed_models), token cap:206(default 4096), tool-loop cap:150-157; on by defaulttools/mcp_tool_server_run.py:268-270(sampling.enableddefault True)
D43 Does this npx/uvx MCP package have a known malware advisory? RULE · external
- Answered how
Before a stdio MCP server launched through npx, uvx or pipx is started, Hermes asks Google's public OSV database about that package and blocks the launch only if it has a malware advisory (an id starting
MAL-); ordinary CVEs are ignored. Verdicts are cached for an hour, and a network error or a check slower than 12 seconds lets the server start (fail-open).- Held by
tools/osv_check.py:166-192(check_package_for_malware; endpoint:19, docstring:1-7); called before a stdio MCP server is spawned,tools/mcp_tool.py:38-53
D44 May a tool fetch or a browser script reach this URL (private ranges, cloud metadata)? RULING · code
- Answered how
Code answers by resolving the host and checking every address: cloud-metadata hostnames and addresses are always refused, and private, loopback, link-local, reserved and CGNAT addresses are refused because
security.allow_private_urlsships false. Non-http(s) schemes, ambiguous authorities and DNS failures are refused too, except that a DNS failure on a hostname is let through when a proxy is configured. For page scripts, the browser pre-screen checks URL literals only when the browser is not local and the browser's private-URL setting is off.- Held by
tools/url_safety.py:402-458(is_safe_url; policy:1-11;security.allow_private_urls), browser eval pre-screentools/browser_url_policy.py:145-202
D45 Is this domain on the operator's blocklist? RULE · code
- Answered how
Off by default: the shipped blocklist is disabled with no domains, so nothing is blocked. When the operator enables
security.website_blocklistand lists domains (or shared list files), code matches the URL's host against each rule (*.x.comas a glob, a bare domain exactly or as a parent) and blocks on the first match; the policy is re-read at most every 30 seconds, and a broken policy fails open.- Held by
tools/website_policy.py:163-198(check_website_access;security.website_blocklist, default disabled:23; 30 s cache:26)
D46 May this skill (or plugin) be installed, given its scan findings and where it came from? RULING · code
- Answered how
Code answers from a fixed table: the scan's worst finding sets the verdict (critical is dangerous, high is caution, otherwise safe) and the source sets the trust level (official is builtin, a listed repo is trusted, anything else community). Builtin always installs; trusted installs unless dangerous; community installs only when safe.
--forceoverrides a block except a dangerous verdict on a community or trusted source, and an agent-created skill with a dangerous verdict gets a retry-able error only whenskills.guard_agent_createdis on (off by default).- Held by
tools/skills_guard.py:821-834should_allow_installoverINSTALL_POLICY:29-36, trust:941-949, verdict:952-955; plugins reuse the enginetools/plugin_guard.py:2-8,:28-30
D47 Do I install this skill or plugin anyway despite a caution verdict? RULING · human
- Answered how
The person answers, and only by asking for it: force is off unless they tick
forceon the desktop install card the model's catalog request raises, type--forceon/skills installor the admin CLI, or answer y to the[y/N]prompt thathermes plugins installandhermes plugins updateshow for a caution verdict in a terminal (no terminal means no). Force still cannot install a dangerous verdict from a community or trusted source.- Held by
the person, mid-session: the
forcekey on the desktop approval card during the model'smanage_catalogcall (tools/connectors/catalog.py:34, read at:220, passed at:272-274; the answer arrives by theconnection.respondRPC,tui_gateway/methods_connectors.py:382-391), or--forcetyped on/skills installin the CLI (hermes_cli/skills_hub.py:1646). Outside a run:--forceon the admin CLI, or the terminal y/N prompt, which exists only onhermes plugins installandhermes plugins update(hermes_cli/plugins_cmd_install.py:334-342, used at:654andhermes_cli/plugins_cmd_update.py:104).
D48 Do I accept this file edit shown as a diff in my editor (Zed via ACP)? RULING · human
- Answered how
Only in an ACP session (an editor such as Zed driving Hermes); the CLI and gateways never bind the requester, so their edits are not asked about here. In an ACP session, before a file edit is dispatched Hermes sends the proposed diff to the editor and the person accepts or rejects it there; a rejection, a requester error or a diff that cannot be built blocks the edit.
- Held by
the person in the ACP client;
acp_adapter/edit_approval.py:177-197; called before write_file/patch dispatchmodel_tools.py:803-813
D49 Is this desktop action destructive enough to need a person's OK (and does raising a window count)? RULING · code
- Answered how
Code answers from a fixed action table: clicks, drags, scrolls, typing, key presses,
set_valueandfocus_appare marked destructive and need approval, while capture, wait and the app/window listings do not; raising a window (bring_to_front, orfocus_appwithraise_window) is asked separately. Typed shell patterns and system key combos are hard-blocked before any prompt, and the approval itself goes through Hermes's shared approval gate under a key per action and foreground/background mode. The tool is in the default tool list but only works when its desktop driver is available.- Held by
tools/computer_use/tool.py:336-341(_reject_unsafe,_ACTIONS[action].destructive,bring_to_front), approval via D11:382-404
D50 Has a person taken control of the desktop since this action was admitted? RULE · code
- Answered how
Code answers from a control lease with an epoch counter: the action is refused if a person holds the desktop at admission, checked again once the dispatch lock is held, and the result is discarded if the lease's epoch has moved since admission, before and after the device operation (even if the person already handed control back). The model is told a person took over and to re-capture after hand-back.
- Held by
tools/computer_use/tool.py:355-377(assert_agent_may_act, epoch fence)
D51 May this plugin's call on the host's model override the task slot, provider, model, agent id or auth profile, or must it run on the host's active model as configured? RULE · code
- Answered how
Code answers on every plugin model call from the operator's
plugins.entries.<plugin>.llmsettings, re-read each call; with none set (the default) every override is refused, so the plugin runs on the host's active model. A task key the plugin registered itself is always allowed; a built-in task, a provider, a model, an agent id or an auth profile each needs its ownallow_*_overrideflag, and a provider or model must also be in its allowlist when one is given. Any refusal raises an error rather than quietly dropping the override.- Held by
agent/plugin_llm.py:498-519PluginLlm._gate: loads the plugin's policy (:508, default loader_resolve_trust_policy:121-141), then_check_task(:509, defined:207-236), then_check_overrides(:510-512, defined:170-181, using_gate_ref_override:153-161). Checks, in order: task (unset, blank orauto-> main-model path:219-221; a slot the plugin registered itself -> allowed; a built-in slot -> only withallow_task_override; any other key -> refused,:222-236); provider (flagallow_provider_override, then theallowed_providerslist unless it holds*); model (flag, thenallowed_models); agent id (flag only); auth profile (flag only). Every refusal raisesPluginLlmTrustError(:144); nothing is silently narrowed. A call with no overrides is never refused. The model call follows in_invoke_sync(:564-572,call_llmat:572) or_invoke_async(:574-580,async_call_llmat:580).
D52 (Codex app-server runtime) Codex asks to run a command, apply a patch, widen its permissions, or pass on an MCP server's elicitation: accept, ask the person, or decline? RULE · code
- Answered how
Applies only when a turn runs through the Codex app-server runtime. Code routes each request: a command or patch is accepted outright when an approval bypass is on (
approvals.mode: offor YOLO), declined when no one can answer (gateway, cron,chat -q, unattended), and otherwise put to the person through the interactive approval callback, where once becomes accept and session or always becomes accept-for-session. Permission escalations are always declined, and an MCP elicitation is accepted only from Hermes's ownhermes-toolsserver.- Held by
agent/transports/codex_app_server_session.py:698-715(_handle_server_request) dispatching through:723-728(_SERVER_REQUEST_HANDLERS); exec and patch go to:730-746(_run_approval_callback: bypass flag -> accept:736-737; no callback -> decline:738-739; else the approval callback, its choice mapped at:829-834); permission escalation is a constant decline:726; elicitation:717-721(_respond_elicitation) accepts only whenserverNameequals the constanthermes-tools(agent/transports/hermes_tools_mcp_server.py:32). The two inputs are fixed when the session object is built:agent/codex_runtime.py:624-629(callback) and:632-637,:658(bypass flags).
D54 A flagged command inside the background-review fork: denied, or put to a person? RULE · code
- Answered how
Code answers: while the background review runs, its thread's approval callback is a function that returns "deny" for every command and logs it, so any flagged command whose approval reaches the point of asking a person is refused instead of prompting. Whatever answers earlier in the shared approval gate is not changed by it, and the callback is cleared when the review ends.
- Held by
agent/background_review.py:1065-1069(_bg_review_auto_deny: returns "deny" for every call, whatever the command), installed in the review thread's approval slot at:1262through:1072-1076(_set_thread_approval_callback), cleared at:1273and:1328. It is consulted only where the command gate reaches the CLI prompt path (tools/approval.py:942-943->tools/approval_prompt.py:120-127).
D55 Does the person supply the sudo password this command needs? RULING · human
- Answered how
The person answers, but only when a sudo command has no configured or cached password, the session is interactive (or has a sudo callback) with a user who can answer, and a passwordless-sudo probe fails. They are asked through the registered sudo callback (CLI panel, TUI/desktop, or attached viewers) or else a hidden terminal read with a 45-second limit; an empty answer, timeout or error runs the command without sudo's password, and a given password is cached for the session.
- Held by
the person at the classic CLI or the TUI/desktop client, asked by
tools/terminal_tool_sudo.py:185-254(_prompt_for_sudo_password): the registered sudo callback when there is one (:193-206; CLI panelhermes_cli/cli_modal_mixin.py:1045-1069, TUI/desktop requesttui_gateway/agent_callbacks.py:256-257, or, for a local viewer's turn run by the gateway session authority, asudorequest to the attached viewersgateway/session_sudo_prompt.py:30-53), else a hidden read from/dev/ttyor the Windows console (tools/terminal_tool_sudo.py:208-254,:156-182). The prompt is raised by_transform_sudo_commandat:487-496.
D56 Is this memory content safe to put in the system prompt (injection, exfiltration, secret-shaped)? RULING · code
- Answered how
Code answers with the shared threat-pattern list at its strict scope: a memory write whose text contains invisible unicode or matches a pattern (after full-width characters are folded) is refused with the first hit. When memory is loaded at session start, each stored entry is scanned the same way; a hit is replaced by a
[BLOCKED: ...]note in the system prompt only, while the raw entry stays in the file so the person can see and remove it.- Held by
tools/memory_tool_store.py:29_scan_memory_content→tools/threat_patterns.py:131-159(strict scope); load-timetools/memory_tool_store.py:223-255merged.
D57 May this scheduled job send a stored provider credential to the endpoint it names? RULE · code
- Answered how
Code answers whenever a job carries a
base_urloverride, at create, on every update and again before each run. Abase_urlwith no provider is refused; a named provider's override must have the same scheme, host and port as that provider's own endpoint;provider="custom"passes when no stored key would be attached for that host, or when the URL matches a configured endpoint. Anything that cannot be matched, and any validator error at run time, is refused (fail closed); jobs without an override are not affected.- Held by
tools/cronjob_job_args.py:364-413(_validate_cron_base_url; helpers_same_origin:311-320,_custom_stored_key_error:328-361). Run-time wrappercron/scheduler_prompt.py:419-441(_guard_job_credential_exfil), called once per fire before provider resolution atcron/scheduler.py:2455. Also called at createtools/cronjob_tools.py:701and on every updatetools/cronjob_tools.py:900-903.
D58 May this skill's inline shell snippet run when the skill is loaded? RULE · code
- Answered how
Off by default:
skills.inline_shellships false, so no snippet runs. When the operator turns it on, code runs every !cmdsnippet in a loaded skill with bash in the skill's folder (10-second default timeout, output capped), unless the skill sits inside a community-trust hub install or its hub lock cannot be read; there is no approval prompt or command check.- Held by
agent/skill_preprocessing.py:154(the settingskills.inline_shellAND NOT_is_community_hub_skill(skill_dir),:99-139); the command then runs at:47-86(run_inline_shell,subprocess.run([bash, "-c", command]):60-70) with a per-snippet timeout and a 4000-character output cap. No approval, blocklist or deny-rule check.
D60 The model copied a context-compression placeholder into a tool call's arguments: may the call run? RULE · code
- Answered how
Code answers once per call, after plugin hooks may have rewritten the arguments and before guardrails or dispatch: every string anywhere in the arguments is searched for a context-compression marker (its prefix followed by its first count). Any hit blocks the call and tells the model which argument paths carried it; a fixed list of read-only tools (file reads, searches, web reads and similar) is exempt, and every other tool, including plugin and MCP tools, is checked.
- Held by
agent/tool_dispatch_helpers.py:69-99(_context_pruned_argument_paths: regex_COMPRESSION_MARKER_ARTIFACT_RE,agent/compression_marker.py:42-44, over every string in the arguments; tools inNO_EFFECT_TOOL_NAMES,agent/tool_result_classification.py:16-24, are exempt). Applied once per call atagent/tool_executor.py:709-720, after plugin pre_tool_call modify hooks and before guardrails and dispatch; the blocked result is built at:647-658.
D61 The model asked to call a tool hidden behind tool search: is that tool inside this session's toolset grant? RULE · code
- Answered how
Code answers each time the model reaches a deferred tool through the tool-search bridge: the named tool must be among the deferrable tools in this session's own toolset grant, computed fresh from the session's enabled and disabled toolsets. Out of scope, the agent loop turns the call into a blocked result before plugin hooks, guardrails or dispatch, and the direct dispatcher returns an error; in scope, the arguments are still checked against the tool's real schema. Connector batches are scoped separately.
- Held by
tools/tool_search.py:545-551(scoped_deferrable_names: the deferrable names in this session's pre-assembly tool list), applied at two call sites: the agent-loop unwrapagent/tool_executor.py:414-417(set built on each call by_tool_search_scoped_names:360-379, through theget_tool_definitionsmemo; it is no longer cached on the agent) and the dispatchermodel_tools.py:756-757(_dispatch_bridge_tool, catalog built:731-735). A miss in the agent loop becomesscope_block, which_dispatch_authorized_onceturns into a blocked result before plugin hooks, guardrails or dispatch (agent/tool_executor.py:701-734, error typetool_scope_block).
D62 May this session use hosted connectors at all (is the connections toolset part of its grant)? RULE · code
- Answered how
Code answers on each connector call by checking that
manage_connectionsis among the tools this session's enabled and disabled toolsets select: a directmanage_connectionsorconnectors__*call is refused otherwise, and so is a connector batch sent through the tool-search bridge.manage_connectionsis in the shared core tool list, so ordinary CLI and messaging sessions pass at defaults, while sessions whose toolsets leave it out are refused.- Held by
model_tools.py:938-942(directconnectors__*ormanage_connectionscall: refused unlessmanage_connectionsis in_select_tool_names(enabled_toolsets, disabled_toolsets)) andmodel_tools.py:748-750(a connector batch sent throughtool_call: refused unlessconnections_in_scope(current_defs),tools/connectors/search.py:14-15, which tests for the samemanage_connectionsname).
D63 Does this foreground command detach itself or start a long-lived server, so that it must be re-sent as a tracked background process instead? RULING · code
- Answered how
Code answers with fixed regexes on every foreground terminal call, after quoted text, backticks and inert heredoc bodies are blanked and unless the command looks like a help or version call. The first hit among
nohup/disown/setsid, a spaced&, or one of eight dev-server/watch patterns refuses the call and tells the model to re-send it withbackground=true. A command whose only backgrounding is a single trailing&is instead run as a tracked background process.- Held by
tools/terminal_tool_guards.py:135-141(_foreground_background_guidance), first hit wins over the ordered table:101-121: (1)nohup/disown/setsidat a command position, regex:67-69; (2) a spaced&inline or trailing, regexes:70-71; (3) one of eight long-lived patterns (npm|pnpm|yarn|bun [run] dev|start|serve|watch,docker compose up,next dev,vite,nodemon,uvicorn,gunicorn,python -m http.server),:89-98. Run on the command after quotes, backticks and inert heredoc bodies are blanked (_strip_quotes:74-86); skipped entirely when the command looks like a help or version call (:124-132,:138-139). Applied to non-background calls attools/terminal_tool.py:1240-1254, except whentrailing_amp_background_command(tools/terminal_tool_guards.py:187-218) finds that a lone trailing&is the command's only backgrounding and stripping it leaves the approval identity unchanged (:170-184,:216-217): that command is run as background=true instead of refused (tools/terminal_tool.py:1250-1251,:1263).
D64 While an approval is waiting, is this plain chat message ("yes", "ok", "always", "no", a thumbs-up) the answer to it, and with what scope? RULING · code
- Answered how
On messaging gateways, code answers with an exact word table: only while a command approval is actually blocking that session and the sender is allowed gateway control, a message whose whole trimmed, lower-cased text is an approve, deny, always or session word (English always, plus the active language's synonyms) or a thumbs-up/down is treated as the reply. It is rewritten to
/approve,/approve always,/approve sessionor/denyand handled exactly as that command; any other text falls through to the normal busy handling.- Held by
gateway/run_busy.py:654-704(_route_plaintext_approval_while_busy), word table:639-652built from:33-46(approval_input_words, the i18n lists underapproval.inputs.*); called from:961inside_handle_active_session_busy_message(:930-962). A match is rewritten to/approve [always|session]or/deny(:687) and handed togateway/slash_commands.py:1206-1250, which callstools.approval.resolve_gateway_approval(tools/approval.py:143-177).
D65 When an MCP server declares a per-chat spend limit, does a paid call that would take this chat past it run? RULING · human
- Answered how
Off unless a server's entry declares tool costs and a
spend_limit. Then a paid call that stays under the chat's limit is booked silently, and one that would pass it asks the person once-or-deny on any server,trust: fullincluded; remembered trust grants,/yoloandapprovals.mode: offdo not skip this prompt. Where nobody can answer (cron, single-query runs) or the prompt fails or goes unanswered, the call is refused.- Held by
the person in the chat, answering a one-call prompt raised by
_ask_for_call(tools/mcp_tool_handlers.py:150-208; the spend-only wording is:180-186). The prompt comes on any server,trust: fullincluded, when the call's declared cost would take the chat's running total past the limit (tools/mcp_tool_costs.py:3-7;over_limit:172-173; the silent reservation below the limit:175-183; gatetools/mcp_tool_handlers.py:136-147). The limit itself is set by whoever writes the server's entry:mcp_servers.<name>.spend_limitwithtool_costsandcost_unitin config.yaml (tools/mcp_tool_costs.py:23-24, 87-91), or a portable package'scosts/spend_limit/cost_unit, translated to the native keys (hermes_cli/agent_plugins.py:93-95, 182-195). A same-name native entry keeps the package's costs unless it sets them itself, andspend_limit: 0turns the limit off (tools/mcp_tool_config.py:672-676). The policy is recorded at discovery (tools/mcp_tool_registration.py:64-73, 85).
D66 Has the person locked this skill, so that the agent may not change it? RULING · human
- Answered how
The person decides; no skill is locked at install. They lock one with
hermes curator lock <skill>or the desktop lock switch, which writeslocked: truein the profile'sskills/.usage.json; code then refuses the agent's changes throughskill_manage(except create),write_file/patchand the terminal writes its parser recognises, and the curator skips the skill. The agent cannot unlock it through those paths, but the terminal check is a policy check, not a sandbox. If the lock file is unreadable, onlyskill_managecalled from a background review refuses; the other paths see no lock.- Held by
the person, through
hermes curator lock <skill>(hermes_cli/curator.py:243-269, 682-683) or the Desktop lock switch, which sendsPUT /api/skills/lock(apps/desktop/src/app/capabilities/skills/skill-detail.tsx:53-65;apps/desktop/src/api/skills.ts:46-57; routehermes_cli/web_routers/skills.py:395-415). Both resolve the name in the active profile and writelocked: truein that profile'sskills/.usage.json, keyed by directory name (hermes_cli/curator.py:250-256;tools/skill_usage.py:580-585;tools/skill_lock.py:3-4). The guarded agent paths cannot unlock: the local terminal guard refuseshermes curator unlock(tools/skill_lock.py:104-105, 268-272), andwrite_file,patchand the terminal guard protect the lock record like the skill (:102-103, 113-119); the guard is 'a policy check, not a sandbox' (:10-12).
E. Execution and observation
E1 (Nous provider) Should this call be sent now, given another session's recorded rate limit? RULE · code
- Answered how
Only when the provider is Nous. Before each model call, code checks whether any session has recorded a Nous rate limit that has not yet reset; if so the call is not sent, and Hermes switches to a configured fallback provider if one exists or else ends the turn with a message saying when the limit resets (free-tier wording for anonymous use). Any error inside the check lets the call go ahead.
- Held by
agent/turn_api_call.py:232-295(nous_rate_limit_guard).
E2 Has the model stream stalled, so the call should be killed and retried? RULING · code
- Answered how
Code answers with a watchdog that checks about every 0.3 seconds while a streamed call runs: if no chunk has arrived for longer than the stale threshold, the stream is killed so the call can be retried. An explicit provider
stale_timeout_secondsis normally used as set; otherwise the threshold is 180 seconds (900 for a local server), raised to at least 240 or 300 seconds for prompts over 50k or 100k tokens and to a floor for known reasoning models. When the server is on this machine, time the computer spent asleep is not counted.- Held by
agent/chat_completion_stream_monitor.py:293-297(kill when no chunk for_stream_stale_timeout; when the endpoint is on this machine, time the computer slept is not counted,:271-272,:278-279,_skip_sleep:174-184); thresholdagent/chat_completion_helpers.py:621-669(provider config, else 180 s; 900 s local; scaled 240/300 s for >50k/>100k tokens; reasoning-model floor). Streaming chosen atagent/turn_api_call.py:46-69.
E3 The user interrupted or redirected while the model was answering: what happens to that answer? RULE · code
- Answered how
Code answers. If the person sent a correction (redirect) while the model was answering, the answer is thrown away and the same step is re-run with the correction, up to the retry limit, after which the turn ends and the correction is handed back as the next message. If the person simply interrupted, any text already streamed is kept in the conversation marked as interrupted, unless it is runaway repetition, in which case a hidden placeholder is stored instead.
- Held by
agent/turn_api_call.py:130-163(response crossed by a redirect is discarded and rebuilt) and:177-217(handle_api_interrupt: keep streamed partial unless runaway repetition). Bound on redirect restartsagent/turn_iteration_prep.py:467-490.
E4 What kind of failure is this API error, and what does it recommend (retry, compress, rotate, fall back)? RULING · code
- Answered how
Code answers it:
classify_api_errorruns a fixed series of checks (plugin and provider-profile hooks, provider special cases, HTTP status, error codes, message patterns, transport type) and the first one that matches gives the failure kind and its recovery hints. With no plugin enabled and no built-in provider profile defining the hook, the first two checks return nothing; if nothing matches, the error isunknownand retried with backoff.- Held by
agent/error_classifier.py:1021-1054(classify_api_error) over stages:1012-1018(provider special cases, HTTP status, MoA, error codes, ~25 message-pattern tuples:103-471, transport heuristics), verdict table:473-506.
E5 Do plugins or the provider's own profile reclassify the error first? RULING · external
- Answered how
Off by default: no plugin is enabled on a fresh install and no built-in provider profile defines
classify_api_error, so both checks return nothing and the built-in checks decide. When a plugin registerstransform_api_error_classification, or the active provider's profile definesclassify_api_error, the first valid verdict wins before any built-in check; a hook that fails to load or raises is skipped.- Held by
agent/error_classifier.py:747-764(_plugin_verdict) and:767-807(_profile_verdict), run before every built-in stage (:1015-1016).
E6 Given the classification, which recovery runs: refresh/rotate credential, strip images, drop reasoning, back off, fall back, or end the turn? RULE · code
- Answered how
Code runs a fixed order: recoveries that need no classification first (for example the fast-mode 429), then
classify_api_error, then the specific recovery chains and context-overflow handling, and finallysettle_unrecovered_error. That last step tries the fallback chain only when the classification allows it (or a spent recovery permits it) and otherwise ends the turn, sends exhausted retries (agent.api_max_retries, default 3) to fallback, automatic recovery or a terminal result, and otherwise backs off and retries. The fallback chain is empty by default, so on a fresh install no fallback model takes over.- Held by
Consolidated (one pipeline):
agent/turn_api_error.py:52-234(handle_api_error) and:266-438(settle_unrecovered_error),agent/turn_recovery.py:235(recover_before_classification),:650(recover_after_classification),:1815(route_classified_error),:1449(compute_error_backoff),:1396(interruptible_backoff_sleep).
E7 Which fallback provider/model takes over? RULE · code
- Answered how
With the shipped defaults nothing takes over:
fallback_providersis an empty list, so the chain is exhausted at once and the turn stays on the primary. When the operator lists fallback providers, code walks them in order, skips any marked unavailable or cooling down or that fail to bind, switches to the first that binds and rewrites the prompt's model identity.- Held by
agent/chat_completion_helpers.py:2053-2112(try_activate_fallback: chain order, skip unavailable or cooling candidates, rewrite the prompt's model identity:2084).
E8 Retries and fallbacks are spent on a transient outage: park and wait, or end the turn? RULING · code
- Answered how
On by default (
auto_recovery_cycles: 5). After retries and the fallback chain are spent, code parks the turn only for an overloaded, server-error or timeout verdict and only if no answer text has reached the user, waits 15, 30, then 60 s (jittered; a provider Retry-After up to 120 s wins) with a visible countdown, and gives up with a status line after five cycles. Setting it to 0 ends the turn instead.- Held by
agent/turn_recovery_autorecover.py:59-116(5 cycles, 15/30/60 s, Retry-After up to 120 s; only overloaded/server_error/timeout; never after text reached the user:61-65).
E9 An exception escaped the turn: deterministic local bug (stop now) or transient (retry)? RULING · code
- Answered how
Code reads the traceback: if the exception passed through local processing modules and never through an API-call module, it is a deterministic local bug and the turn ends at once; an interpreter shutdown also ends it. Otherwise the turn retries until 8 escaped exceptions (
min(8, max_iterations)) or until the step limit is one call away.- Held by
agent/turn_loop_errors.py:35-171; module setsagent/conversation_loop.py:229-239.
E10 Can these tool calls run in parallel, or must they run in order? RULE · code
- Answered how
Code splits each batch in call order: interactive tools, unparseable arguments, tools with a registered timeout above the batch default, and anything not on the parallel-safe list (or an MCP server that opted in) run in order;
read_file/search_files/write_file/patchjoin a parallel run only when their paths do not overlap a writer. A parallel run of two or more calls uses up to 8 worker threads.- Held by
agent/tool_dispatch_helpers.py:182-258(_batch_admission,_plan_tool_batch_segments): never-parallel set:30, parallel-safe set:33-44, path-overlap reservations:48-50,278-316, MCP server opt-in:124-130; worker capagent/tool_executor.py:245-252.
E11 A tool-search bridge call (tool_call) names a deferred tool: which tool, and are its arguments valid? RULE · code
- Answered how
Tool search is on by default (
tools.tool_search.enabled: autowith a default list of deferred tools). When the model callstool_call, code resolves the named tool, refuses it with a tool error if it is not in this session's scoped catalog, and checks the arguments against the deferred tool's own schema before dispatching it as the real tool.- Held by
model_tools.py:714-763(_dispatch_bridge_tool: resolve:745, session scope check:756-757, schema check:758-762).
E12 Are string-typed arguments coerced to the schema's types? RULE · code
- Answered how
Code answers it on every registry tool call:
coerce_tool_argslooks up the tool's schema and converts string arguments to the declared types; a value that does not convert is left as the model sent it, and a tool with no schema properties is left untouched.- Held by
tools/arg_coercion.py:21-230, calledmodel_tools.py:902.
E13 Should a filesystem snapshot be taken before this action? RULING · code
- Answered how
Off by default (
checkpoints.enabled: False; opt in withhermes chat --checkpointsor the setting). When on, code snapshots beforewrite_fileandpatch, and before aterminalcommand that a regex reads as destructive (rm, cp, mv, sed -i, dd, git reset, an overwriting>and similar) when the task has no container backend; any checkpoint error is swallowed and the action runs anyway.- Held by
agent/tool_executor.py:1032-1044(only when checkpoints are enabled,:1032; off by default,checkpoints.enabled: Falseathermes_cli/config_defaults.py:486-487; then write_file/patch always; terminal only when_is_destructive_commandagent/tool_dispatch_helpers.py:52-67,102-104matches rm/cp/mv/sed -i/dd/git reset... or>).
E14 When is a running tool abandoned (timeout or interrupt), and what does the model get back? RULING · code
- Answered how
Code answers it: a sequential tool call is abandoned after 420 s by default (
timeouts.tools.sequential_calloverrides; 0 disables), with time spent waiting on a human approval added back, anddelegate_taskandmanage_connectionsare exempt; an interrupt abandons it sooner. The model gets a fixed "timed out after N s" or "cancelled ... abandoned" text, and the worker thread is interrupted, not joined.- Held by
agent/tool_executor.py:838-995(sequential) (420 s default, extended by human approval wait:875;delegate_task/manage_connectionsexempt:856); concurrent batch:135-142,1438-1515; result text:960,971.
E15 Did this tool call fail? RULING · code
- Answered how
Code answers it with fixed tests: a terminal result fails on a non-zero exit code; a JSON-object result fails on its top-level shape (
success: false, anerrorormessagealongside a failure, or a failed-shape marker); any other string fails if it carries a non-empty quoted "error"/"failed" near the start or begins with Error. Guardrail refusals and landed file writes never count as failures.- Held by
agent/display.py:1030-1073(_detect_tool_failure), consumed by the executors (agent/tool_executor.py:1382,1831) and handed to the loop guardrails asfailed(run_agent.py:1268); fallback twinagent/tool_guardrails.py:219-246used only whenfailedis not passed (:386-387). Also:agent/tool_guardrails.py:219-246classify_tool_failure: - terminal non-zero exit - memory "exceed the limit" - a result that parses as a JSON object: a top-levelerror/messagewithsuccess: falseor anerrorkey, or a failed shape (shape_failure_error,agent/tool_result_classification.py:86-101) - otherwise a non-empty quoted"error"/"failed"starting in the first 500 chars, or a leadingError(has_failure_marker,agent/tool_result_classification.py:45-57) - it mirrorsagent.display._detect_tool_failure(agent/tool_guardrails.py:220-221)
E16 A tool result is too big: what does the model see instead (preview + file path, or truncation)? RULING · code
- Answered how
Code answers it: a result over the per-tool threshold (100,000 chars by default, 50,000 for MCP, scaled down for small context windows) is saved to a spillover file and the model sees a 1,500-char preview plus the file path; a turn whose results together exceed 200,000 chars gets the same treatment. If no location can be written, the model gets the preview with a truncation note.
- Held by
tools/tool_result_storage.py:395-436(per result) and:461-493(turn aggregate), with context-window-scaled budgetagent/tool_executor.py:119-133; multimodal text parts:1179-1207.
E17 Is this tool's output untrusted, to be wrapped as data-not-instructions? RULING · code
- Answered how
Code answers it from a fixed name list: results of
web_extract,web_searchand anybrowser_*ormcp_*tool, 32 characters or longer, are wrapped in anuntrusted_tool_resultblock telling the model to treat the content as data, with look-alike delimiter tags defanged. Every other tool's output goes in unwrapped.- Held by
agent/tool_dispatch_helpers.py:483-493(web_extract,web_search,browser_*,mcp_*; >=32 chars) and:566-598(wrap, delimiter defang), applied inmake_tool_result_message:448-480.
E18 Does a result carry provider-side elision markers, so the model should be told it is incomplete? RULING · code
- Answered how
Code answers it for the untrusted tools only (
web_extract,web_search,browser_*,mcp_*): if a result of at least 1,000 characters shows one of four markers ("... N more items","has_more": true, "saved to sandbox", "data_preview") in its first 64 KB, a fixed note saying the data is incomplete is appended inside the wrapped block.- Held by
agent/tool_dispatch_helpers.py:500-541(4 marker regexes, untrusted tools only, 1 KB-64 KB window).
E19 The same call returned the same result again: replace it with a reference stub and add a loop notice? RULING · code
- Answered how
On by default (
stall_guards: true). From the second consecutive call with the same tool, arguments and byte-identical string result, code replaces a result of 512 characters or more that is not a failure with a short reference stub pointing at the first one; from the third, a loop notice is appended for tools that are not pollers.- Held by
agent/tool_guardrails.py:450-501(observe_call) and:542-557(stub), appliedrun_agent.py:1266-1296before the result is appended. Thresholdsagent/tool_guardrails.py:35-52. Also:agent/tool_guardrails.py:498-500,542-557. It applies from the 2nd identical repeat, for results of 512 chars or more, never to errors. Applied atrun_agent.py:1283-1284.
E20 Do plugins rewrite a tool's result before the model sees it? RULING · external
- Answered how
Off by default: no plugin is enabled on a fresh install, so the hook check finds no listener and the result passes unchanged. When an enabled plugin registers
transform_tool_result(the bundled, opt-insecurity-guidanceplugin does), the first string any callback returns replaces the result; a hook error keeps the original.- Held by
transform_tool_resulthookmodel_tools.py:862-879(registry tools) andagent/inline_tool_executors.py:63-85(inline tools); first string wins.
E21 The active model can't take images in tool results: what does it get instead? RULE · code
- Answered how
Code answers it: if the model supports vision and the provider accepts image parts in tool results, the images go through; if the provider rejects them (or rejected them earlier this session), the model gets a text summary. A model without vision gets the text summary, except that a
computer_usescreenshot becomes an error telling it to switch to a vision-capable model.- Held by
agent/vision_message_prep.py:207-245(_tool_result_content_for_active_model), calledagent/tool_executor.py:1145.
E22 Do plugins rewrite the final reply before it is stored and shown? RULING · external
- Answered how
Off by default: with no plugin enabled, no
transform_llm_outputhook runs and the reply is stored and shown as written. When a plugin registers it, the first non-empty string returned replaces the reply once per turn before it is first saved; a plugin that registered withhold_streamruns on a separate path that shows a fixed "withheld" notice if its transform fails.- Held by
agent/turn_finalizer.py:509-547(apply_llm_output_transform), calledagent/turn_final_response.py:387-401before the durable flush.
E23 Is a tool's output high-risk (prompt-injection patterns), and who is told? RULING · code
- Answered how
Code answers it for the untrusted tools only (
web_extract,web_search,browser_*,mcp_*):scan_for_threatsruns over the text and any finding marks the output high-risk. A non-low result is sent to the surface as atool.output_riskprogress event; nothing is blocked or redacted, and this check adds nothing to what the model sees.- Held by
agent/tool_dispatch_helpers.py:544-563(scan_for_threats, untrusted tools only), attached:471-477, emitted astool.output_riskagent/tool_executor.py:1056-1060.
E24 The model asked the person a question (clarify): what do they answer? RULING · human
- Answered how
The person answers, through the form their surface shows (CLI, desktop/TUI or a messaging gateway), with the questions and choices shown in a redacted copy. If the surface wired no answer callback the model gets an "unavailable" tool error; a cancel, timeout or undelivered form comes back to the model as that outcome and the model continues.
- Held by
the person at the surface's callback (
tools/clarify_tool.py:115-151; inline executoragent/inline_tool_executors.py:266); runs inline, never parallel (agent/tool_dispatch_helpers.py:30).
E25 A stream died on a 5xx before any text arrived: re-issue the request once without streaming to see the real error? RULING · code
- Answered how
Code answers it on a streaming chat-completions call: when the stream fails with a status of 500 or above before any visible text, no stop is pending, the stream is not a llama.cpp router and no probe ran in the last 60 s, it re-sends the same request once without streaming. A probe answer is used for the turn; a probe error below 500 replaces the opaque 5xx; any other failure keeps the original error.
- Held by
agent/chat_completion_helpers.py:3642-3710_unmask_server_error_with_nonstreaming, called from one place,_handle_stream_errorat:3636(itself called from the stream retry loop_callat:3742). Conditions, all required: the request was not cancelled by Hermes (:3571-3573); the error is not astream_optionsrejection (:3586-3594); no deltas were sent, or deltas fired but nothing visible was delivered and no tool call was in flight (:3596-3597, through_forget_invisible_deltasagent/chat_completion_stream_monitor.py:83-98, andagent/chat_completion_helpers.py:3668); the error is not a timeout, connection, SSE-connection or stream-parse error and not an empty stream, because those take the retry branch (:3616-3632) and the probe sits in theelse(:3633-3637); no interrupt is pending (:3662-3663); the stream is not a llama.cpp router stream (_streams_llamacpp,:3664-3666); the error carries an HTTP status of 500 or above (:3667-3669); the wire is chat-completions (:3670-3671); and no probe ran on this agent in the last 60 s (_STREAM_5XX_PROBE_WINDOW_S = 60.0at:75, checked:3672-3675, stamped at:3676before the probe is sent, initial valueagent/agent_init.py:1160). The probe is the same request minusstreamandstream_options(agent/chat_completion_helpers.py:3677) sent throughinterruptible_api_call(:3681, defined:1260-1273).
E26 Do plugins wrap, replace or skip the tool call itself? RULING · external
- Answered how
Off by default: no plugin is enabled on a fresh install, so no
tool_executionmiddleware is registered and the tool runs as called. When a plugin registers one, each callback can pass changed arguments on, wrap the call, or return without calling the tool, in which case its return value becomes the result.- Held by
plugin callbacks registered with
ctx.register_middleware("tool_execution", cb)(hermes_cli/plugins.py:939-944), run byhermes_cli/middleware.py:144-150(run_tool_execution_middleware) ->_run_execution_chain:162-215. A callback may callnext_callwith different arguments (:180-194) or return without calling it, in which case the tool does not run and the callback's return value is the result (:201-202). Call sites:agent/tool_executor.py:810-816,model_tools.py:854-859,agent/agent_runtime_helpers.py:2545-2552.
E27 The model asked a foreground command to wait longer than the foreground cap: is it run in the background with a completion notice instead? RULING · code
- Answered how
Code answers it: when the model passes a
timeoutabove the foreground cap (600 s by default,TERMINAL_MAX_FOREGROUND_TIMEOUToverrides) on a command without a lone trailing&, the command runs in the background and a note saying so is added to the result. A completion notice is requested; on surfaces that cannot deliver one, the note tells the model to poll instead. A command that needs detachment guidance is refused before this check.- Held by
tools/terminal_tool.py:1255-1256(_plan_execution: when the command is not a lone trailing-&command (:1250-1251),timeout > FOREGROUND_MAX_TIMEOUTsetspromoted_from_foreground_timeout), acted on at:1565-1568(forcesbackground=True,notify_on_complete=True, dropswatch_patterns) and:1582-1583(note added to the result by_with_promoted_note:1274-1286). Cap:FOREGROUND_MAX_TIMEOUT, default 600 s, envTERMINAL_MAX_FOREGROUND_TIMEOUT(tools/terminal_tool.py:78).
E28 During first-run onboarding in the desktop app, what does the person pick on each card (name, accent colour, layout, apps to connect, plugins, tour, first task)? RULING · human
- Answered how
Only in the desktop app's first-run onboarding chat. The person picks on each card the model shows with
setup_beat, and the call waits up to 300 s; an unanswered card times out, and a message typed instead closes it (on the name card, typed words are taken as the name). Code accepts only the card's own pick keys and offered ids; the name card is pre-filled with the OS account's real name, which goes to the card only.- Held by
the person at the desktop app's setup card. The card is raised by the
setup_beattool (tools/setup_beat_tool.py:135-176; the blocking ask is:165-166) through the agent'ssetup_beat_callback, which runs inline (agent/inline_tool_executors.py:229-240). Two hosts supply the callback, both with the 300 s card deadlineCARD_TIMEOUT_S(agent/onboarding_flow.py:19): the session authority (gateway/local_desktop_ui.py:205-207, 276) and the older sidecar (tui_gateway/methods_onboarding.py:297-300, wired attui_gateway/agent_callbacks.py:184). The beats and their options are code (agent/onboarding_flow.py:22-24, 174-186); the name card is pre-filled with the OS account's real name, sent to the card only (tools/setup_beat_tool.py:125-129,agent/onboarding_facts.py:30-35).
F. Limits and memory
F1 Has this turn used up its step allowance? RULE · code
- Answered how
The parent's step cap is
agent.max_turns: the config.yaml the installers seed sets 500,hermes setupsets 150 (90 for its Blank Slate option), and with no finite value code applies no cap; a delegated child has its own cap (delegation.max_iterations: 250). Code counts one step per model call under a lock and, once the count reaches the cap, ends the turn with reasonmax_iterations_reached, except for a single granted grace call.- Held by
-
agent/turn_iteration_prep.py:408-416(begin_iteration: consume or break withbudget_exhausted) - loop conditionagent/conversation_loop.py:1746- counteragent/iteration_budget.py:34-40
F2 Should the model be told it is near the step limit? RULE · code
- Answered how
Off by default:
agent.budget_warning_ratiois null. When a ratio is set (or, for a dispatcher-owned kanban worker, forced to 0.9) and the step cap is finite (agent.max_turnsfor the parent,delegation.max_iterationsfor a child), code appends a one-time notice to the newest tool result once the steps used reach that share of the cap; if the newest message is not an unsaved tool result, no notice is sent.- Held by
agent/turn_iteration_prep.py:48-99_maybe_inject_iteration_budget_warning. - It appends to the newest un-persisted tool result. - It is opt-in viaagent.budget_warning_ratio(hermes_cli/config_defaults.py:69, readagent/agent_init.py:1291). - For kanban workers it is forced to 0.9 (agent/turn_iteration_prep.py:58-64). - It is off when the cap is unlimited (:70).
F3 The step budget is spent: what does the agent hand back? (parent and delegated child, merged) RULING · llm
- Answered how
Runs when a turn reaches its step cap (
agent.max_turnsfor the parent: 500 in the config.yaml the installers seed, 150 afterhermes setup;delegation.max_iterations, 250, for a delegated child). Then, if a stop gate had already withheld a composed answer, that answer is handed back; otherwise a summary call to the session's model writes the reply (on the chat-completions path the tool list is still sent, for cache parity, and any tool call in the reply is discarded), retried once if the reply comes back empty, and canned text is used if both are empty. A dispatcher-owned kanban worker also records the task as timed out.- Held by
agent/turn_finalizer.py:124-195_resolve_budget_fallback→agent/chat_completion_helpers.py:2279-2331handle_max_iterations. - If a stop gate already withheld a composed answer, that answer is reused instead and no call is made (agent/turn_finalizer.py:139-149).
F4 Does this iteration count against the budget? RULING · code
- Answered how
Code answers it: restarts for a redirect, compression or a fallback rebuild, compaction and the preflight gate give their step back on both counters the loop checks, with restarts bounded by
max_retries. A round whose only tool isexecute_codegives back only the shared iteration budget, so it still counts against the API-call cap (agent.max_turnsfor the parent,delegation.max_iterationsfor a child).- Held by
refund sites: - execute_code-only rounds
agent/turn_tool_round.py:205-208- redirect, compression and rebuilt-for-fallback restartsagent/turn_iteration_prep.py:487-498,540-541- compactionagent/turn_context_compaction.py:112- preflightagent/turn_preflight_gate.py:56- restarts bounded bymax_retries(agent/turn_iteration_prep.py:467-484,525-537)
F5 Has this turn silently stalled? (agent watchdog) RULING · code
- Answered how
On by default (
turn_liveness.timeout_s: 600, polled every 15 s) for a turn that takes the session-database turn lease. Code aborts the turn when its activity clock shows no progress for 600 s of awake time, re-checking under the activity lock that nothing moved before committing; a tool still running stamps activity every 30 s, so it reads as alive.- Held by
agent/turn_liveness.py:90-157TurnLivenessWatchdog._tick, scheduled fromagent/turn_facade_lease.py:73. - Default 600 s idle, polled every 15 s (agent/turn_liveness.py:23-24). - Host sleep time is excluded (:113-114). - The abort is revalidated under the activity lock (:6-8,:141).
F6 Has this gateway turn been idle too long? (gateway watchdog) RULING · code
- Answered how
Messaging gateway only. A watchdog thread abandons the turn once the agent reports no activity for
agent.gateway_timeoutseconds (1800 by default; 0 = unlimited), not counting host sleep; before the agent reports any activity, time since the worker started counts.- Held by
gateway/run.py:2723-2755_watch_gateway_turn_inactivity, started atgateway/run_turn.py:1371-1380. - Default 1800 s viaagent.gateway_timeout, bridged toHERMES_AGENT_TIMEOUT(gateway/run.py:1949;gateway/run_turn.py:1328-1331).
F7 Has this cron job been idle too long? RULING · code
- Answered how
Only for scheduled cron jobs. When a job's agent shows no activity for
HERMES_CRON_TIMEOUTseconds (600 by default or on bad input; 0 = unlimited), code logs its last activity, hard-interrupts it and raises a timeout.- Held by
cron/scheduler.py:1186-1196_cron_inactivity_seconds(HERMES_CRON_TIMEOUT, default 600 s), then:1916-1930_raise_inactivity_timeout→request_hard_interrupt, with the runner at:1941-1943.
F8 Should the model be told to wrap up because the wall-clock budget is 80% gone? RULE · code
- Answered how
Off by default (
agent.run_budget_seconds: null). When a run budget is set (orhermes chat --run-budget N), code appends a one-time wrap-up notice to the newest tool result once 80% of the time has passed; if that result is already saved, no notice is sent. Nothing stops the run at 100%.- Held by
agent/conversation_loop.py:126-158_maybe_inject_run_budget_wrapup, called fromagent/turn_iteration_prep.py:183-186andagent/tool_executor.py:204-209.
F9 How long may one provider call wait before it counts as hung, under a run budget? RULING · code
- Answered how
Off by default, because no run budget is set. When
agent.run_budget_secondsis set, code caps an implicit stale timeout for a provider call at half the remaining budget, never below 60 s; an explicit per-provider timeout setting is left as configured.- Held by
agent/chat_completion_helpers.py:678-687cap_to_run_budget: half the remaining budget, floor 60 s, and only for implicit timeouts (:690-700;run_agent.py:568-587).
F10 Has a detached review fork spent its input-token allowance? RULE · code
- Answered how
Background review is on by default, and each review fork gets an input-token budget taken from its model's context window unless
max_input_tokensis set (0 or less = unlimited). Code checks it at the top of each review iteration and stops the review's tool loop once the input tokens used reach the budget.- Held by
agent/conversation_loop.py:114-123_review_input_budget_exhausted, checked atagent/turn_iteration_prep.py:390-400. The budget is set atagent/background_review.py:1058.
F11 Is the agent stuck in a tool-call loop: warn, block or halt the turn? RULING · code
- Answered how
Code answers it with counters and fixed thresholds: repeated failures with the same arguments, repeated failures of one tool, no-progress repeats, identical call streaks and repeating 2-4 call cycles. At the defaults it warns (appending guidance to the result) on the CLI, desktop, TUI, ACP, subagents and the API server, and on unattended platforms such as messaging gateways and cron it also blocks or halts the turn with a fixed reply.
- Held by
agent/tool_guardrails.py:312-535ToolCallGuardrailController: - exact-args failure streak - same-tool failure count - idempotent no-progress - consecutive identical (tool, args, result) streak - repeating cycles of period 2-4 - Applied atrun_agent.py:1266-1306. A halt ends the turn with the synthetic replyrun_agent.py:1257-1264, exitguardrail_halt(agent/turn_tool_round.py:184-198). - Hard stops are off on attended platforms and on for unattended ones (agent/tool_guardrails.py:84-86,118-124,144-145).
F12 Has this turn made too many web searches or spawned too many subagents? RULING · code
- Answered how
Code decides on every surface, whatever the hard-stop setting: before each
web_searchcall or subagent-spawning call,_check_loop_caprefuses it once this turn's count has reached the cap (tool_loop_guardrails.loop_caps, default 50 each; 0 means unlimited), and the refusal also halts the turn. The counters reset every turn.- Held by
agent/tool_guardrails.py:559-573_check_loop_cap. The default is 50 each (:81-82). It applies regardless ofhard_stop_enabled(:367-370). A block also halts the turn (run_agent.py:1304-1306).
F13 Should automatic compression run now? (threshold trigger) RULING · code
- Answered how
On by default (
compression.enabled: true). Code decides:should_compress_infosays compress once the prompt token count reachesthreshold_tokens, unless a summary-failure cooldown, a structural no-op backoff or the ineffective-compaction breaker blocks it. The trigger is 50% (compression.threshold) of the window minus the output reservation, raised to at least 75% on windows under 512K; a per-model percentage replaces the global one (up or down), andthreshold_tokenscan only lower it.- Held by
-
agent/context_compressor.py:3007-3016should_compress_info- thresholdagent/context_compressor.py:2660_derive_triggerandagent/context_compressor.py:2778-2810_compute_threshold_tokens: window minus max_tokens times the percent, floored, 85% cap atagent/context_compressor.py:2727, 75% floor under 512K atagent/context_compressor.py:2764,agent/context_compressor.py:1149-1150- per-model longest-substring overridesagent/context_compressor.py:2037,2828
F14 Is a rough estimate over threshold enough, or wait one request for the provider's count? RULING · code
- Answered how
Code decides at the turn-start preflight: when a rough estimate not anchored on real usage is at or over the trigger,
should_defer_preflight_to_real_usagewaits one request for the provider's own count if a compaction just happened, or if the last real prompt count was under the trigger. It never defers when the last real count was already over the trigger or the provider has shown it omits usage; a provider overflow error still compacts reactively.- Held by
agent/context_compressor.py:2984-3001should_defer_preflight_to_real_usage, used atagent/turn_context_compaction.py:268-292.
F15 Was the last compaction ineffective, so auto-compaction should be benched? (anti-thrash) RULING · code
- Answered how
Code decides from the provider's real count: the first real prompt count after a compaction is compared with the trigger, and still being over it adds a strike while being under clears them; a compaction refused before commit because it would grow the transcript also adds one. Two strikes block automatic compaction; after 300 seconds blocked, one probe is allowed with the count dropped to a single strike.
- Held by
agent/context_compressor.py:2916-2948_apply_real_prompt_verdict; tripagent/context_compressor.py:3028-3035; probationagent/context_compressor.py:3072-3120; a "would grow" refusal counts as a strike (agent/context_compressor.py:2517, fromagent/conversation_compression.py:3194).
F16 Nothing was eligible to compact: back off? RULING · code
- Answered how
Code decides: when a compaction finds nothing eligible, or returns the transcript unchanged,
_record_structural_no_opholds off automatic compaction for 300 seconds without counting an ineffectiveness strike. The hold lifts early at the next turn-start preflight once the transcript has more messages than recorded (the unchanged-transcript path records 0), on any completed compaction, or on a manual/compress.- Held by
agent/context_compressor.py:2503-2515_record_structural_no_op, set fromagent/context_compressor.py:5193-5198andagent/conversation_compression.py:3494-3506, honoured atagent/context_compressor.py:3061-3069. Lifted before it lapses once the transcript has more messages than the count recorded at the no-op (agent/context_compressor.py:5200-5203, called fromagent/turn_context_compaction.py:246-248); the CONV setter passes no count, so it records 0 (agent/conversation_compression.py:3503-3504,agent/context_compressor.py:2510).
F17 After a summary failure, how long before trying again? (cooldown ladder) RULING · code
- Answered how
Code decides after a failed summary call: timeouts (a 429 is classed as a timeout) and truncated summaries climb a 60, 300, 900 second ladder on separate counters; invalid JSON, a closed stream or empty content wait 30 seconds; anything else waits 60. A new cooldown never shortens a longer live one and is persisted on the session row, and while it runs automatic summaries are skipped; a manual
/compressand a provider overflow error get past it.- Held by
agent/context_compressor.py:4147-4155: - ladderagent/context_compressor.py:799,814: 60/300/900 s; 30 s for JSON, closed-stream and empty; 600 s with no provider - persistence never shortens (agent/context_compressor.py:2607-2619) - stallagent/context_compressor.py:2621- gateagent/context_compressor.py:3056-3069- durable read ignores hygiene rows (agent/context_compressor.py:2554-2605) - overflow bypass (agent/context_compressor.py:3894,agent/turn_overflow.py:156)
F18 The summary call failed: abort and keep everything, or commit a lossy fallback? RULING · code
- Answered how
Code decides by failure class, after any main-model retry: access or quota errors, network or closed-stream failures, truncated and empty-content summaries always abort and keep every message (a rate limit is not treated as an access error), and an overload aborts until three in a row, after which it degrades. Any other failure commits the deterministic fallback summary at the defaults; with
compression.abort_on_summary_failure: trueevery failure aborts instead.- Held by
agent/context_compressor.py:718-752_classify_summary_failure(re-read by PART-4);agent/context_compressor.py:221-247(rate limits deliberately not terminal,:238-239); terminal tableagent/context_compressor.py:757-793; decisionagent/context_compressor.py:5225-5256; overload escalates after 3 (agent/context_compressor.py:811,4169-4181);compression.abort_on_summary_failureforces abort.
F19 A separate summary model failed: retry on the main model? RULE · code
- Answered how
Code decides: when the summary model that failed (a configured one, or the model an
autoroute resolved) is not the main model and no fallback has happened yet,_fallback_to_main_for_compressionclears the cooldown and the summary is retried once, immediately, on the main model, with the route pinned to it. At the defaults (auxiliary.compression.provider: auto, no model) the retry fires only when the auto route resolved a different model.- Held by
agent/context_compressor.py:4136-4142→agent/context_compressor.py:3773-3796_fallback_to_main_for_compression; route pinagent/context_compressor_summary.py:30-63.
F20 Write the handoff summary of the middle turns (the LLM summary) RULING · llm
- Answered how
A language model writes it:
_generate_summarybuilds a structured prompt from the middle turns (updating any previous summary) and_call_summary_llmsends onecall_llm(task="compression")request with no output cap, on theauxiliary.compressionroute (autoby default). Empty, refused or length-truncated replies count as failures; a good reply is redacted, stripped of think blocks and has pruned-skill markers restored. While a failure cooldown runs, automatic summaries make no call, but a provider-reported overflow still gets one attempt.- Held by
agent/context_compressor.py:3885-3947_generate_summary→agent/context_compressor.py:3798-3883_call_summary_llm(call_llm(task="compression")agent/context_compressor.py:3805; nomax_tokens,agent/context_compressor.py:3811).
F21 With no LLM summary, which anchors survive? (deterministic fallback) RULING · code
- Answered how
Code builds it with no model call:
_build_static_fallback_summarykeeps the last real user ask as the active task, up to 12 completed actions, file paths, tool results matching an error regex as blockers, the last 8 dropped turns and an elided copy of the previous summary, then redacts and size-caps it; lean tail mode (the default) appends its anchor index and verbatim user messages. The result also becomes the previous summary that the next compaction builds on.- Held by
agent/context_compressor.py:3514-3580_build_static_fallback_summary, anchorsagent/context_compressor.py:3453-3512(regexagent/context_compressor.py:3504), committedagent/context_compressor.py:5277-5303. It becomes_previous_summary(agent/context_compressor.py:5302).
F22 Skip the summary LLM before calling it? (feasibility skip, benched model) RULING · code
- Answered how
Code decides before an automatic summary call: the summary model is skipped and the deterministic fallback used when the last two compactions both fell back (one probe of the model is allowed every 300 seconds), or when an ineffectiveness strike is on record and the middle section is under 10% of the trigger. A manual
/compressor an already prepared summary never skips.- Held by
agent/context_compressor_prellm.py:39-92, wiredagent/context_compressor.py:5527-5530.
F23 A summary route stalled: retry elsewhere, go deterministic, or continue uncompressed? RULING · code
- Answered how
Code decides when the summary stalls past its no-progress or total budget: it retries once on the first complete entry of
auxiliary.compression.fallback_chain(none is configured by default) within what is left of the shared pre-commit budget, and once a stall-class failure is already on record this session it then commits the deterministic fallback summary with no model call. If nothing compacts, the turn continues uncompressed with a warning and the timeout cooldown ladder is stepped.- Held by
agent/conversation_compression.py:857-895resolve_compression_fallback_route;agent/conversation_compression.py:898-905;agent/conversation_compression.py:970-1060; deterministic pinagent/context_compressor_summary.py:73-86; host timeout copyagent/compression_facade.py:48-96.
F24 Which old tool results become one-liners before summarising? (phase-1 prune) RULING · code
- Answered how
Code decides, with no model call, when a compaction plans its window:
_prune_old_tool_resultsreplaces older identical tool results with a back-reference, turns tool results before the protected tail that exceed a minimum size into one-line summaries (sparing skill bodies the tail still relies on), retires stale images in the tail, and, when the tail alone is over its soft budget, demotes tool bodies inside it too. The newest unread tool round is spared unless it alone exceeds the tail's hard share of the input window.- Held by
agent/context_compressor.py:3286-3324_prune_old_tool_results: - dedupeagent/context_compressor.py:3167- one-linersagent/context_compressor.py:3185-3213/agent/context_compressor.py:1652,1945- protected skillsagent/context_compressor.py:1102- image retirementagent/context_compressor.py:1460- pressure demotionagent/context_compressor.py:3225-3273- the pending round is spared (agent/context_compressor.py:3275-3303)
F25 Proactive tool-result prune outside full compaction: commit now? RULING · code
- Answered how
Off by default (
compression.proactive_prune_tokens: 0). When set, code decides: once the request reaches that many tokens,prune_tool_results_onlyruns the deterministic tool-result prune (results overproactive_prune_min_result_chars, default 8,000 characters, with the lastprotect_last_nmessages protected) and commits it only if it reclaims at least 4,096 tokens and the transcript has regrown past the previous runway; otherwise nothing changes.- Held by
agent/context_compressor_prune.py:28-135prune_tool_results_only(moved out ofCCintoProactivePruneMixin, mixed intoContextCompressoratagent/context_compressor.py:36,:2149-2151).
F26 Where are the boundaries: what stays verbatim and what is summarised? RULING · code
- Answered how
Code decides:
_compress_windowkeeps the system prompt plus the first 3 messages (protect_first_n, which drops to 0 after the first compaction), and_find_tail_cut_by_tokenswalks back from the newest message keeping rows up to the tail token budget with a 1.5x soft ceiling, normally keeping the last user and assistant anchors, and keeping whole tool groups. Everything between is summarised. At the default lean tail mode the budget is, per the config comment, about 2.5% of the window (10K floor, 25K cap).- Held by
-
agent/context_compressor_window.py:19-37_compress_window- headagent/context_compressor.py:4583(decays after the first compaction,agent/context_compressor.py:4571-4581) - tailagent/context_compressor.py:4985-5107_find_tail_cut_by_tokens: soft ceilingagent/context_compressor.py:3215, flooragent/context_compressor.py:5000, floorless walkagent/context_compressor.py:5010, anchorsagent/context_compressor.py:4647,4663,5080-5103- lean demotionagent/context_compressor.py:3582-3587→agent/context_compressor_tail.py:71-107- in-flight restatementagent/context_compressor.py:4777
F27 What may the summariser see? (per-record caps, sampling, elision) RULING · code
- Answered how
Code decides with fixed limits: each record is redacted, media directives and assistant think blocks are removed, bodies over 6,000 characters keep the first 4,000 and last 1,500, and tool arguments over 1,500 characters are cut to 1,200. In lean tail mode (the default) whole records are then sampled in up to 8 slices to fit the total character cap; legacy mode keeps the head and tail of the whole text and marks the gap.
- Held by
agent/context_compressor.py:3403-3447;agent/context_compressor.py:3652-3771_sample_summary_records;agent/context_compressor.py:3603_bound_summary_input.
F28 Where does the summary row go: its own role, or merged into a tail row? RULE · code
- Answered how
Code decides by role alternation:
_summary_placementmakes the summary its own row, user or assistant, whichever alternates with the last visible head row and then the first visible tail row (user when it opens the conversation or no real user text survives). When no standalone role alternates, the summary is folded into the first tail row with a delimiter and an end marker.- Held by
agent/context_compressor.py:5319-5374.
F29 Strip historical images at commit, and evict stale outbound images per request? RULING · code
- Answered how
Code decides on two paths. When a compaction commits,
_strip_historical_mediareplaces image parts with placeholders in everything before the newest image-bearing user message and keeps only the newest tool-result image (a pending tool round is spared). Separately,evict_stale_outbound_tool_imagesdrops older tool-result images from the outgoing copy of a request only, in batches, to stay under an image-count ceiling and a byte budget, and never drops images the person uploaded; on a 413 that compression could not fix, tool images are stripped as a last resort.- Held by
agent/context_compressor.py:1570, calledagent/context_compressor.py:5395;agent/context_compressor.py:1510evict_stale_outbound_tool_images;agent/image_eviction_policy.py:143-288; the 413 last resortagent/turn_overflow.py:264.
F30 The compacted transcript would be larger: salvage it or refuse? RULING · code
- Answered how
Code decides at the commit site: if the compacted transcript's rough token estimate is larger than the original's,
salvage_grown_transcripttries one mechanical shrink (older large tool results to placeholders, oversized summary rows elided, stale reasoning replay pruned, the todo snapshot reduced) and commits it only if strictly smaller. Otherwise the compaction is refused, the original kept, the user warned, and an ineffectiveness strike recorded.- Held by
agent/conversation_compression.py:3140-3197;agent/context_compressor.py:565-598salvage_grown_transcript.
F31 The provider says the request is too large: compress, clamp output, strip images, or refuse? RULING · code
- Answered how
Code decides from the provider's error: an output-cap error that states a budget clamps
max_tokensto fit and also compresses, and one that states none ends the turn; a local server rejecting a request under half the known window without quoting a count ends the turn as retryable, with no compression. Otherwise a limit the provider reports is adopted only when it is lower than the current window, and history is compressed with the cooldown bypassed, up tocompression.max_attempts(default 3), ending the turn when nothing shrinks. A 413 is compressed and scored in bytes, then tool images are stripped as a last resort.- Held by
agent/turn_overflow.py:359-450_recover_context_length(unexplained-rejection branch:392-422, fraction:39);:219-275;:278-312;:140-178(bypasses the cooldown);:119-138.
F32 Preflight could not shrink an over-window request: stop the turn, or send? RULING · code
- Answered how
Code decides after preflight compaction: if compaction timed out, the turn is stopped before the provider call unless the request is known to fit the model's window. If it ran without progress (neither fewer rows nor a token cut over 5%), the turn is stopped only when the request is known to exceed the window and no cooldown deferred the pass. Otherwise the turn goes ahead with the compacted request, or the original when nothing was compacted.
- Held by
agent/turn_context.py:362-431, wiredagent/turn_context_compaction.py:409-433.
F33 Gateway session hygiene: compress before the agent even runs? RULING · code
- Answered how
Messaging gateway only, on by default. Before the agent runs on a transcript of 4 or more messages,
_hmwa_hygiene_plancompresses when the token count (the last real prompt count, else a persisted usage anchor, else a rough estimate) reaches 85% of the model's window, or the transcript reachescompression.hygiene_hard_message_limit(default 5,000) messages. It skips when a persisted failure cooldown is active or another compression is already in flight.- Held by
gateway/run_turn_hygiene.py:156-228_hmwa_hygiene_plan; driver:765-814; cooldowngateway/run.py:110-158.
F34 Hygiene did not land: which rows are dropped from this turn's payload? RULE · code
- Answered how
Messaging gateway only. When hygiene did not compress this turn (compression off, under both thresholds, cooldown, already in flight, timed out or failed) and the transcript is longer than
compression.hygiene_hard_message_limit(default 5,000) messages,bound_model_input_without_hygienesends only the leading system and session-meta rows plus the newest rows up to that count, never starting on a tool result. The stored transcript is untouched.- Held by
gateway/run_turn.py:113-136bound_model_input_without_hygiene, appliedgateway/run_turn_hygiene.py:817-828.
F35 Compact when a session resumes after an idle gap? RULING · code
- Answered how
Off by default (
compression.idle_compact_after_seconds: 0). When set, code decides at turn start: if the session has been idle at least that long, it is compacted before the first reply, unless a failure cooldown is active, a post-compaction usage reading is still pending, or the estimate is at or under the floor (trigger timestarget_ratio, raised by what the last compaction left).- Held by
agent/turn_context_compaction.py:144-214;agent/turn_context.py:465-491.
F36 Native provider-side compaction (OpenAI Responses): send it, at what threshold, what survives? RULING · code
- Answered how
Off by default (
compression.codex_responses_native: false). When on, and only for a native-compaction model on api.openai.com, the Codex backend or a trusted proxy, Hermes code decides to send acontext_managementcompaction entry, sets its threshold a safety margin under its own trigger (an explicitcodex_responses_compact_thresholdis held between a floor and that margin), and keeps the person's plain messages and its own summaries within fixed budgets. The provider then decides how and when its opaque server context is compacted. It is never sent while compression is disabled orcompression.checkpoint_requiredis on.- Held by
agent/native_compaction.py:109-142(gates),:76-91,:242-365,:375-397.
F37 Is the next model call driven only by a reference handoff, so skip it? RULE · code
- Answered how
Code decides:
reference_handoff_would_drive_next_model_callis true when, after the last summary row carrying no live ask, there is no tool row, tool call, real user turn or summary with a live ask. If so and the turn's own user message cannot be restored after the handoff, the model call is skipped and the turn ends with a short 'Context was compacted' status instead of a reply.- Held by
agent/context_compressor.py:5703-5732, wiredagent/conversation_loop.py:179-199; alsoagent/turn_iteration_prep.py:503-518.
F38 Micro-compaction: fold one finished exchange into a rolling summary each turn RULING · llm
- Answered how
Off by default (
compression.micro_compact: false). When on, after every Nth finished turn (micro_compact_every_n_turns, default 1) a language model folds the oldest unabsorbed exchange into a rolling summary that replaces it in the transcript; once that summary passes 2,000 tokens the next pass re-summarises it instead. After 3 failures in a row on the same exchange, that exchange is skipped and left for full compaction.- Held by
agent/micro_compaction.py:205-300,:119-162, defrag:168-199, skip after 3 failures:302-317.
F39 Should all new work (cron ticks, kanban dispatch, new gateway turns) pause now? RULING · human
- Answered how
A person decides: the operator runs
hermes pause, which writes anESTOPfile in the Hermes home, andhermes resume, which removes it. While the file exists (even empty or corrupt), the cron scheduler, kanban dispatcher and new gateway turns skip work; both the profile home and the fleet root are checked, and work already running is not stopped.- Held by
the operator via
hermes pause/hermes resume; sentinel checkagent/estop.py:1-8,:31-40
F40 Has a bot-to-bot conversation gone on too long? RULING · code
- Answered how
Messaging gateway, on by default. Code counts each authorised bot-authored message per conversation (profile, platform, chat): once 20 have arrived within 300 seconds, the next one trips a 600-second cooldown during which that chat's bot messages are dropped. The numbers come from
gateway.bot_loop_guard, re-read on every call; human messages are never counted.- Held by
gateway/bot_loop_guard.py:82-131(sliding window; defaults 20 messages / 300 s, cooldown 600 s at 22-27); counted once per inbound atgateway/authz_mixin.py:693-705, peeked at 717-720.
F41 The agent is mid-run: should it stop, or change course, right now? RULING · human
- Answered how
A person decides, at whichever surface is active: in the classic CLI, a message entered while the agent runs goes to the busy-input path and
/steeror/stopis applied at once on the UI thread; in the TUI and desktop, an interrupt, steer or redirect request, or a prompt submitted mid-turn; in the messaging gateway, a message sent while the agent is busy. What a plain mid-run message does followsdisplay.busy_input_mode, which defaults tointerrupt; on the gateway an interrupt is turned into a queued message while subagents run or compression is in flight.- Held by
the person at the active surface. Entry points read: classic CLI, typing and pressing Enter while the agent runs (
hermes_cli/cli_tui_mixin.py:1437-1439) or/steerhandled on the UI thread (:1465-1467); TUI and desktop, thesession.interrupt,session.steerandsession.redirectrequests (tui_gateway/methods_session_interrupt.py:70-105, 133-176) or a prompt submitted while a turn runs (tui_gateway/methods_prompt.py:894-913); messaging gateway, any message sent while the agent is busy (gateway/run_busy.py:930,gateway/run_inbound.py:748).
G. Judging the result
G1 Goal judge: is the standing goal done, blocked, continue, or wait? RULING · llm
- Answered how
Only when the user has set a standing goal with
/goal. After each turn, any user-declared gate commands run first; if they pass,judge_goalasks a language model on theauxiliary.goal_judgeroute (autoby default, temperature 0) whether the goal is done, blocked, continue or wait. Done ends the goal, blocked pauses it, wait parks it, and continue sends the next continuation prompt until the turn budget runs out; an unreachable judge counts as continue, and repeated judge errors or unparseable replies pause the goal.- Held by
hermes_cli/goals.py:894-959judge_goal→:878-891_call_goal_judge_llm; consumer:1484-1568evaluate_after_turn.
G2 What does "done" mean for this objective? (goal drafting) RULING · llm
- Answered how
Only on
/goal draft <objective>. A language model on theauxiliary.goal_judgeroute (autoby default, temperature 0) expands the plain-language objective into a completion contract that the per-turn judge later checks against. If the call fails, the reply is not JSON or the contract is empty, the goal is still set, as a free-form goal with no contract.- Held by
hermes_cli/goals.py:1044-1073draft_contract, from/goal draft(hermes_cli/goal_command.py:138-147).
G3 Does every user-declared quality-gate command pass? RULE · external
- Answered how
Only when the user has added gate commands to a goal. After each goal turn and before the judge,
_check_gatesruns each command through the shell in the session workspace, and exit code 0 passes. The first failing gate skips the judge and feeds its output back as the next continuation prompt; past its retry limit, or when the declared workspace is not a directory here, the goal pauses.- Held by
hermes_cli/goals.py:1306-1359_check_gates;:396-414run_gate(shell=True); workspace:374-393. It runs before the judge (:1505-1509).
G4 Kanban goal-mode worker loop: is the card's work done? RULING · llm
- Answered how
Only for a kanban card run in goal mode. Each round, code first stops the loop if the worker has already completed, blocked or handed the card to review; otherwise
judge_goalasks the goal-judge model whether the worker's latest reply meets the card's title and body. Blocked blocks the card; done sends one nudge to callkanban_completeand blocks the card if the worker still does not; continue or wait sends a continuation prompt until the turn budget is spent, which blocks the card; a judge transport failure stops the loop.- Held by
hermes_cli/goals.py:1630-1753run_kanban_goal_loop, judge call:1693, terminal-status short-circuit:1621-1684.
G5 May this goal-mode card be completed or sent to review? (kanban completion gate) RULING · llm
- Answered how
Only for a goal-mode kanban card. When the worker tries to complete the card or request review,
_goal_gateasks the goal-judge model whether the worker's evidence meets the card's title and body: done lets the handoff through, and blocked or any other verdict rejects it with guidance. If no judge model is configured or reachable, or the check raises, the handoff is allowed.- Held by
tools/kanban_tools.py:486-516_goal_gate(called:748,900) andhermes_cli/kanban.py:830-878.
G6 /loop: did the acting agent declare the loop finished? (LOOP_COMPLETE marker) RULING · llm
- Answered how
After each wakeup turn of an active
/loop,complete_tickfirst checks the agent's reply for a line that is onlyLOOP_COMPLETE(optionally followed by.or!). The acting model decides by writing that line; code then ends the loop as done. The pattern matches such a line anywhere in the reply, not only at the end.- Held by
hermes_cli/loops.py:370-372response_signals_complete, regex:35-37((?im)on any line). It is checked before G7 (:562-565).
G7 /loop --until: is the stop condition met? RULING · llm
- Answered how
Only when
/loopwas started with--untiland the reply is not empty: after the marker check,complete_ticksends the stop condition and the reply to the goal judge (judge_goal, an auxiliary model call). "done" ends the loop, "blocked" pauses it for the person to re-scope, and anything else, including a judge error, keeps looping.- Held by
hermes_cli/loops.py:567-582(reusesjudge_goal, withs.untilas the goal).
G8 Verify-on-stop: may a turn that edited code end without fresh passing evidence? RULING · code
- Answered how
Off by default (
agent.verify_on_stop: False;HERMES_VERIFY_ON_STOPoverrides, and"auto"turns it on except on messaging surfaces). When on and the turn edited non-prose files, code looks up the session's verification ledger; unless its status ispassed, the turn is sent back to the model with a nudge to verify, at most twice per turn.- Held by
-
agent/verification_stop.py:169-241build_verify_on_stop_nudge-:103-123_verification_snapshot- ledgeragent/verification_evidence.py:763-805- classifier:551-595- gate wiringagent/turn_stop_gates.py:36-50,178-188- Default OFF (hermes_cli/config_defaults.py:209;agent/verification_stop.py:62-88). Two attempts (:170).
G9 pre_verify plugin hook: should the turn continue after code edits? RULING · external
- Answered how
Off by default: it runs only when a plugin has registered a
pre_verifyhook. Then, after a turn that edited files and belowmax_verify_nudges(3 when unset), Hermes calls the hooks and the first one returningcontinueorblockwith a non-empty message sends the turn back to the model with that message; any other return lets the turn finish.- Held by
hermes_cli/plugins.py:2030-2048get_pre_verify_continue_message, viaagent/turn_stop_gates.py:53-77. Runs only with edits and a registered hook, belowmax_verify_nudges(default 3,agent/verify_hooks.py:16,29-34).
G10 Kanban stop guard: has the worker handed the card off? RULE · code
- Answered how
Only in a dispatcher-owned kanban worker (
HERMES_KANBAN_TASKset for a task the worker owns;HERMES_KANBAN_STOP_NUDGE=0turns it off). When such a worker tries to end, code scans the conversation for any call tokanban_complete,kanban_block,kanban_schedule,kanban_request_revieworkanban_request_changes; if none, it sends a nudge back to the model, at most twice.- Held by
agent/kanban_stop.py:64-98build_kanban_stop_nudge;:51-61session_called_kanban_terminal; terminal set:22-28. Max 2 nudges.
G11 /review: is the presented work correct and complete? RULING · llm
- Answered how
When the person types
/review,start_reviewsnapshots the recent messages and hands them to a background subagent (delegate_task(background=True)) whose model is asked for a structured review ending in a final verdict. Its answer comes back into the chat as an ordinary background-delegation result; no code reads the verdict, so the person and the main model take it as text. On channels without background completions it runs synchronously instead.- Held by
agent/review_engine.py:158-181start_review, viadelegate_task(background=True).
G12 HUMAN: accept or tighten the drafted goal contract RULING · human
- Answered how
With
/goal draft, an auxiliary model drafts a completion contract and Hermes shows it and tells the person how to tighten it. The goal starts working at once in the same step, so the person's review is optional and happens, if at all, by re-setting the goal with inline lines. If the draft fails, the goal runs free-form.- Held by
the person, at
hermes_cli/goal_command.py:161-166. Nothing forces review before kickoff (:172).
G13 HUMAN: override a BLOCKED or exhausted goal (/goal resume) RULING · human
- Answered how
When the goal judge rules a goal unachievable, or the turn budget runs out, the goal is paused with a message naming
/goal resumeand/goal clear. Only the person decides what happens next;/goal resumesets it active again and by default resets the turns used to zero.- Held by
the person. Pause messages
hermes_cli/goals.py:1477-1482,1533-1536; resume resets the turn budget (:1198-1206).
G14 HUMAN: add or remove quality gates and subgoals RULING · human
- Answered how
The person adds, lists, removes or clears quality gates with
/goal gateand success criteria with/subgoal; code only stores them on the active goal. Adding a gate needs the surface's own authorization callback to allow it, because gates run shell commands with no later approval; removing and clearing do not.- Held by
the person, at
hermes_cli/goal_command.py:118-134;hermes_cli/goals.py:1225-1288. Gate creation needs adapter authorization (hermes_cli/goal_command.py:126-129).
G15 HUMAN: act on a kanban card blocked by the judge or the budget RULING · human
- Answered how
In a kanban goal-mode worker, code blocks the card when the goal judge rules it unachievable, when the worker is judged done but never calls
kanban_completeafter one finalize nudge, or when the turn budget runs out. The card then waits on the board for a person (the board operator) to re-scope, unblock or close it; nothing in the loop decides that.- Held by
the board operator; block sites
hermes_cli/goals.py:1709,1716-1719,1729-1733;tools/kanban_tools.py:470-476.
G16 The reply ends by announcing a next action but makes no tool call: is the model stalled? RULING · code
- Answered how
On by default (
agent.stall_guards: True) whenever tools are offered. When a reply with no tool calls is at most 400 characters and its last 160 characters end on an announced next step ("let me now", "I'll now", "next, I"...), code treats the model as stalled, keeps the reply as interim and sends a nudge to act. Promoted reasoning text gets a broader first-person-plan check. At most two such re-prompts per turn, shared with the two guards below.- Held by
agent/turn_final_response.py:221-237,250-298withtrailing_continue_intentagent/agent_runtime_helpers.py:3342-3357(andpromoted_reasoning_announces_action:3373-3395for promoted reasoning).
G17 After real tool work the reply is a short fragment: collapse, or a terse answer? RULING · code
- Answered how
Only when the intent-ack mode is not off: at the default
agent.intent_ack_continuation: "auto"that means only on thecodex_responsesAPI mode, so on other providers this guard does not run. Then, after a turn that produced tool results, a reply of 24 characters or fewer with no sentence ending that starts with stray punctuation before a letter, or is in a non-Latin script the person did not use, is treated as a collapse and re-prompted (shared limit of two per turn).- Held by
agent/turn_final_response.py:238-248+looks_like_degenerate_finalagent/agent_runtime_helpers.py:3297-3319(<=24 chars, wrong-script or leading-punctuation shapes).
G18 Before any tool ran, is a short "I'll do X" reply an acknowledgement that should keep going? RULING · code
- Answered how
At the default
agent.intent_ack_continuation: "auto"this runs only on thecodex_responsesAPI mode. There, before any tool has run in the conversation, a reply of up to 1,200 characters that promises a future action with an action word (or a narrow "now"-style action clause), and that refers to a file path or workspace, is treated as an acknowledgement: code keeps it as interim and nudges the model to continue, at most twice per turn (shared limit). Setting the mode to all drops the workspace requirement and applies it on every API mode.- Held by
agent/turn_final_response.py:254-265+looks_like_codex_intermediate_ackagent/agent_runtime_helpers.py:3131-3285; modeintent_ack_continuation_mode:3402-3415(default codex-only).
G19 When the model gives its final answer, is the turn held open because the language server has since reported problems in files it edited? RULE · code
- Answered how
Code answers. With language-server checks on (
lsp.enabled: true, the default, active inside a git worktree), when awrite_file/patchcheck outlasted the write's own wait, code waits up to 5 s at the model's final answer for this agent's pending checks; if any problems came back, the answer is kept as an interim message and the model is told to fix them first. This happens at most 3 times per turn.- Held by
agent/turn_stop_gates.py:98-117(_late_lsp_verdicts), the first gate inapply_stop_gates(:161-176), which the final-response phase calls (agent/turn_final_response.py:375-385). It waits up toTURN_END_WAIT= 5.0 s for this agent's edit checks still running, then takes every undelivered verdict queued under the agent's session id or task id (agent/lsp/deferred.py:31-32, 125-138). Verdicts are queued when awrite_file/patchlanguage-server check outlasts the write's own short wait (tools/file_operations_lint.py:353-360, 385-417).
H. Delegation
H1 Is this spawn request accepted? RULE · code
- Answered how
Code checks each
delegate_taskspawn in order and refuses it with a tool error at the first failure: spawning paused by the operator; the parent already atdelegation.max_spawn_depth(default 1, so only the top-level agent may spawn); more tasks thanmax_concurrent_children(default 10); a bad task, schema or image; no task routable to a model (a task that alone cannot be routed is refused and the rest run); and, in a one-shot run only, more thandelegation.oneshot_max_children(default 2) children in the session.- Held by
tools/delegate_tool.py:573-627in order: - pause:573-577- depth:584-591- counttools/delegate_tool_tasks.py:80-86- schema and imagetools/delegate_tool.py:604-607- credentials, one route per task (_routes_for,tools/delegate_tool.py:507-518,608-613;tools/delegate_tool_routes.py:309-332) - one-shot captools/delegate_tool.py:532-549,625-627
H2 HUMAN: should new spawns be paused right now? RULING · human
- Answered how
Not paused at start: the flag is an in-memory
False. Only the operator sets it, through the TUI (pin /agents) or thedelegation.pausecall; while it is set every new spawn is refused, and children already running keep going.- Held by
the operator; stored at
tools/delegate_tool_registry.py:16,39-48.
H3 Is this goal concrete enough to hand off? RULING · code
- Answered how
Code, and only for the
tasks=[...]form: a goal that is just "todo" or "task N", that contains an unexpanded template marker such as<feature_name>, or that is under 10 characters when there are two or more tasks is refused with a message telling the model to rewrite it. The single-goalform only needs a non-empty goal.- Held by
tools/delegate_tool_tasks.py:36-65_validate_batch_tasks(placeholders:15, templates:16-18, <10 chars with ≥2 tasks:59).
H4 May this child itself delegate (orchestrator or leaf)? RULE · code
- Answered how
Code decides from depth alone; the
rolethe model passes is ignored. A child is an orchestrator only whendelegation.orchestrator_enabledis on (default true) and its depth is belowdelegation.max_spawn_depth; at the default depth of 1 every child is a leaf and cannot delegate.- Held by
tools/delegate_tool.py:227-245(the model'sroleis ignored; orchestrator iff enabled and depth < max).
H5 Which tools does the child get? RULING · code
- Answered how
Code: the child inherits the parent's enabled toolsets, or, if the model named toolsets, only those the parent also has (parent MCP toolsets may be added back by setting). The blocked tools
delegate_task,clarify,memory,send_messageandcronjob_manageand the kanban toolset are then removed; an orchestrator child getsdelegationback. A plugin request through the subagent API is refused if its toolsets go beyond the parent's.- Held by
tools/delegate_tool_toolsets.py:77-123_resolve_child_toolsets(blocked list:14-22). The pluginallowed_toolsetscheck is merged here (agent/subagent_lifecycle.py:463-472). Also:tools/delegate_tool_toolsets.py:14-22DELEGATE_BLOCKED_TOOLS(delegate_task, clarify, memory, send_message, cronjob_manage),:77-123_resolve_child_toolsets(children never gain parent-absent tools)
H6 What background and instructions does the child need? RULING · llm
- Answered how
The parent model decides, by what it writes in each task's
goalandcontext. Code puts the context verbatim into a short child system prompt with fixed completion instructions, sends the goal as the child's first message, and skips the parent's memory; when a workspace path is known it also adds that workspace's project context files (not SOUL.md). Nothing checks that the context is complete.- Held by
parent model output; assembled by
tools/delegate_tool_progress.py:180-219_build_child_system_promptandtools/delegate_tool_child_run.py:725-769,840-843,928-938. Memory and context files are skipped (tools/delegate_tool.py:298).
H7 Which model, provider and credentials does the child run on? RULE · code
- Answered how
Code, from the
delegationconfig section. At the defaults (model,providerandbase_urlall empty) the child inherits the parent's model, provider, endpoint and key. A setbase_urluses that endpoint directly, a setproviderresolves a full credential bundle, and a pinned provider re-derives the wire format instead of inheriting it; if the child has a credential pool, it leases an entry that serves its endpoint.- Held by
tools/delegate_tool_config.py:469-488,563-656,279-338,226-238;tools/delegate_tool_child_run.py:496-524.
H8 Is the child stuck, so stop waiting? RULING · code
- Answered how
Code. For a child the parent waits on, a heartbeat every 30 seconds checks for progress (a new iteration, a tool change or fresh activity); after 15 quiet cycles between tools (450 s) or 40 on the same tool (1,200 s) the wait ends, the child is told to stop, and its real result is still collected if it lands within a short grace window.
delegation.child_timeout_secondsadds an inactivity cap, off by default (0). Background delegations get a separate monitor with the same 450/1,200 s thresholds and a 120 s grace before they are marked stalled.- Held by
sync heartbeat
tools/delegate_tool_child_run.py:357-400(15 idle / 40 in-tool cycles × 30 s,tools/delegate_tool.py:107-113); optional captools/delegate_tool_child_run.py:864-900; grace:951-978; async monitortools/async_delegation.py:1203-1272(450/1200/120 s,:94-97).
H9 What status does the child's run get? RULE · code
- Answered how
Code, by fixed rules: an interrupted run is
interrupted(carrying the last real assistant text); a run flagged failed or with an error isfailed; otherwise it iscompletedif there is a usable final answer (not empty and not the(empty)give-up marker), elsefailed. A run that hit its iteration limit stayscompletedbut is markedtruncated, and a schema still violated after the retry does not fail the run. Timeouts and exceptions get their own entries.- Held by
tools/delegate_tool_child_run.py:618-718_build_result_entry; timeout and exception entries:985-1044;tools/delegate_tool.py:416-424.
H10 Does the child's answer satisfy the declared output_schema? RULE · code
- Answered how
Only when the parent attached an
output_schema. Code strips fences and prose around the outermost JSON, parses it and checks it with the jsonschema library (if that library is missing, any parseable JSON passes). On failure the child gets exactly one retry turn with the errors; if it still fails, the run still counts as completed and the raw text goes back markedschema_valid: falsewith a notice.- Held by
tools/delegation_output_schema.py:87-106validate_output;tools/delegate_tool_child_run.py:543-584.
H11 What goes in the child's final summary? RULING · llm
- Answered how
The child model decides: its system prompt asks for a tight summary of what it did, what it found, files touched and issues. Without an
output_schema, code takes its final reply as the summary with no parsing or check of its content, then redacts secrets (on by default) and trims it to the parent's context budget before delivery; the parent is warned in the tool description that summaries are self-reports to verify.- Held by
the child model under
_COMPLETION_INSTRUCTIONS(tools/delegate_tool_progress.py:148-157); taken verbatim (tools/delegate_tool_child_run.py:624).
H12 How much of the child's answer enters the parent's context? RULING · code
- Answered how
Code: each summary is capped at the smaller of
delegation.max_summary_chars(24,000 by default) and a share of the parent's remaining context (half of the headroom split across the batch at about 4 characters per token, never below 2,000). A longer summary is cut to a head and tail with a footer, and the full text is written to a file under the delegation cache that the parent can read.- Held by
tools/delegate_tool_results.py:281-309_apply_summary_budget: min(max_summary_chars24000, 50% of remaining headroom per summary, floor 2000),:165-171,254-279; 75/25 head-tail plus footer plus spill:194-238.
H13 Detached or inline, and how does the result come back? RULE · code
- Answered how
Code, not the model: a
delegate_taskcall from the top-level agent asks for background execution, and one from a subagent runs inline. A top-level call still runs inline when the session joins its children (delegation.gateway_mode: inline, or the defaultautoon a human chat session with interim messages off), when the session cannot receive a detached result later (one-shot runs, cron, kanban workers, stateless HTTP), or when the background pool is full (max_concurrent_children), with a note saying why; otherwise the result re-enters the parent's chat as one message when the whole batch finishes.- Held by
- depth 0 → background (
run_agent.py:1355;tools/delegate_tool.py:903-909) -tools/delegate_tool_dispatch.py:251-288_resolve_async_wake_sid- capacity rejected, then sync fallback (tools/async_delegation.py:954-956;tools/delegate_tool_dispatch.py:228-247,471-477) -_units_of(:383-398) - re-entry texttools/process_registry_notifications.py:280-317
H14 Which of the child's background processes outlive it? RULING · llm
- Answered how
The child model decides, by handing a background process off to the parent with a tool call; code records the handed-off processes on the result. Any other process the child still has running is listed as orphaned on the result and then closed with the child. By default (
delegation.surface_child_process_notifications: False) completion notices from processes owned by a subagent are dropped rather than shown to the parent.- Held by
the child model. Accounting
tools/delegate_tool_child_run.py:1067-1085before cleanup:1124-1157. Notifications are suppressed unlesssurface_child_process_notifications(tools/process_registry.py:1863-1873; policytools/process_registry_notifications.py:55-69).
H15 Did the child change files the parent already read? RULE · code
- Answered how
Code: when the child finishes, it compares files written since the child started by any task other than the parent against the files the parent had read. If any match, a note asking the parent to re-read them (up to 8 named) is appended to the child's summary, or listed as
stale_pathswhen there is no summary.- Held by
tools/delegate_tool_child_run.py:1046-1065append_sibling_write_reminder.
H16 Does a parent stop reach this child, and who may steer or stop it? RULE · code
- Answered how
Code: every child is registered on its parent so a parent stop fans out to it; a child attached after a stop was already requested gets the stop at once (hard stop cancels, soft stop redirects). Through
delegate_tasklist, steer and stop, only the conversation that owns the child may act on it, judged by the parent link or the parent's session id (followed across context-compression renames). A steer that arrives after the child's last turn is reported as missed.- Held by
tools/delegate_tool_child_run.py:125-144_attach_child;tools/delegate_tool_registry.py_owns_subagent_record;tools/delegate_tool_child_run.py:526-533,711-717.
H17 Plugin API: what terminal state does the plugin see? RULE · code
- Answered how
Code maps the child run's status by fixed rules:
completedbecomes SUCCEEDED,interruptedbecomes CANCELLED if the plugin had asked to cancel and INTERRUPTED otherwise, and anything else (or an exception) becomes FAILED. The plugin sees only a clipped summary, the error, the API-call count and the duration, plus a hash of the result.- Held by
agent/subagent_lifecycle.py:415-445_run: completed → SUCCEEDED; it keeps only summary, error, api_calls and duration (:430-435).
H18 /btw side question: is the fork's answer usable, or fall back? RULE · code
- Answered how
Code: when a live agent exists,
/btwfirst asks a fork of it, with every tool call denied and at most 3 iterations; any non-empty answer is used. If the fork fails or answers empty (and the caller has not stopped it), one auxiliary call answers from a trimmed transcript, and that answer is returned without a check.- Held by
agent/side_question.py:147-175answer_side_question; fork:22,102-133; one-shot:136-144.
H19 The model asked for more subagent spawns in one response than the concurrency limit: how many of those calls are kept? RULING · code
- Answered how
Code, before the assistant message is recorded: if one model response holds more
delegate_taskcalls thandelegation.max_concurrent_children(default 10; the env var is read only if the config has none), it keeps the first ones up to the limit in order, keeps every other tool call, and silently drops the rest apart from a log warning.- Held by
run_agent.py:1182-1200(AIAgent._cap_delegate_task_calls: keeps the firstmax_concurrent_childrendelegate_taskcalls in order, keeps every other call, drops the rest), applied atagent/turn_tool_round.py:96-99, before the assistant row is staged at:110-113. Limit fromtools/delegate_tool_config.py:95-110(_get_max_concurrent_children: configdelegation.max_concurrent_children, then envDELEGATION_MAX_CONCURRENT_CHILDREN, default 10 at:19, floor 1, no ceiling).
H20 Kanban decomposer: how should this triage card be split into child tasks, and which profile takes each? (runs unattended by default) RULING · llm
- Answered how
An auxiliary model call decides. On by default wherever the gateway runs the kanban dispatcher (
kanban.dispatch_in_gateway: true,kanban.auto_decompose: true): each dispatcher tick, unless dispatch is paused, it decomposes up to 3 triage cards (kanban.auto_decompose_per_tick) with no person involved. Code parses the reply, replaces unknown assignees with the default and drops bad parent links; a person can also run it on one card from the CLI or dashboard.- Held by
hermes_cli/kanban_decompose.py:303-336decompose_task→hermes_cli/kanban_specify.py:146-187_call_aux(call_llm, aux taskkanban_decomposer,:170). Unattended caller: gateway dispatcher tickgateway/kanban_watchers.py:304-307→gateway/kanban_watchers_dispatcher.py:247-303auto_decompose_tick(:291, author "auto-decomposer"). Person-initiated: CLIhermes_cli/kanban.py:1312-1317_cmd_decompose, dashboardplugins/kanban/dashboard/plugin_api.py:1606.
H21 Kanban triage specifier: what title and spec should this rough card have before a worker picks it up? RULING · llm
- Answered how
An auxiliary model call decides, only when a person runs
hermes kanban specifyor the dashboard's specify action. Its JSON title and body replace the card's; if no JSON parses, the whole reply becomes the body, and the card moves from triage to todo where the dispatcher can hand it to a worker. At the defaults, a gateway's auto-decomposer may take a triage card before anyone specifies it.- Held by
hermes_cli/kanban_specify.py:190-235specify_task→:146-187_call_aux(call_llm, aux tasktriage_specifier,:170). Person-initiated only: CLIhermes_cli/kanban.py:1296-1302_cmd_specify(hermes kanban specify), dashboardplugins/kanban/dashboard/plugin_api.py:1029.
I. Learning and feedback
I1 Has enough conversation happened to review it for memory? RULING · code
- Answered how
Code counts user turns: with the
memorytool available and a memory store present, every turn adds one, and atmemory.nudge_interval(default 10) it signals a memory review and resets. A directmemorytool call resets the count, and a resumed session restarts it from the number of earlier user turns.- Held by
agent/turn_context.py:765_tick_memory_nudge(hydrated:757-762; reset on a directmemorycall,agent/tool_executor.py:736-737).
I2 Did this turn do enough tool work to review it for skills? RULING · code
- Answered how
Code counts tool-calling iterations while
skill_manageis available; at the end of a turn, if the count has reached the skill nudge interval (skills.creation_nudge_interval: 15 in the config.yaml the installers seed, 10 when the setting is absent), it asks for a skill review and resets. Usingskill_manageresets the count.- Held by
agent/turn_finalizer.py:863-869; counteragent/turn_iteration_prep.py:166-167; resetagent/tool_executor.py:738-739.
I3 Should a review fork run now at all? RULE · code
- Answered how
Code, after a memory or skill review is due: it runs only for a turn with a final answer that was not interrupted, not skipped for background review (cron), and whose prompt no hook vetoed. Automatic reviews also need a top-level agent and
background_review.enabled(true by default); on a managed local model server they wait for idle, a second review is dropped while one is in flight, and the review is skipped if the provider cannot make tool calls.- Held by
-
agent/turn_finalizer.py:606-616(final response, not interrupted, not cron); the whole step is skipped for a turn whose prompt a hook vetoed (:596-597) -run_agent.py:762-769(automatic reviews only: not a delegate; enabled) -:779(defer on a managed local runtime) -agent/background_review.py:75-89(drop if one is in flight) -:1266(provider cannot do tool calls)
I4 Is anything worth remembering, and is it about the user or the environment? RULING · llm
- Answered how
On by default: every 10 user turns (
memory.nudge_interval: 10), when the memory tool and store are available, a background fork of the agent reads the conversation and a model call decides whether to add a memory entry and to which store (user or environment). Code then checks the target, content, size caps and duplicates, but nothing checks the user-versus-environment choice; withmemory.write_approval: truethe adds are staged for the person instead of written.- Held by
agent/background_review.py:1175_run_review_fork→run_conversation:1214.
I5 What should the skill library learn: patch, add a support file, or create a class-level skill? RULING · llm
- Answered how
On by default: once a turn brings the tool-calling iterations counted since the last skill review (or
skill_manageuse) to the skill nudge interval (15 in the config.yaml the installers seed, 10 whenskills.creation_nudge_intervalis absent), andskill_manageis available, a background fork runs and a model call decides which skills to patch, extend or create. Code validates names, frontmatter, size and read-before-write, but not whether the lesson is worth a skill; withskills.write_approval: truethe changes are staged for the person instead.- Held by
agent/background_review.py:1175with_SKILL_REVIEW_PROMPT(:456) or_COMBINED_REVIEW_PROMPT(:529).; the fork's model callagent/background_review.py:1212-1221
I6 May an unattended review delete or replace a memory entry? RULE · code
- Answered how
Code answers no. In an unattended background review, a memory
patchorremove(alone or in a batch) is not applied but staged for the person's approval (/memory pending), and if staging fails it is denied; older stagedreplacerecords are gated the same way, andaddpasses. A review the person asked for with/refineis marked attended and may delete.- Held by
tools/memory_tool.py:179_background_delete_gate(staged;/refineexempt,tools/skill_provenance.py:29-44).
I7 Is this agent-written skill safe? (security scan) RULING · code
- Answered how
Off by default (
skills.guard_agent_created: false): agent-written skills are not scanned. When it is on, each skill write is scanned after it lands; a blocking verdict rolls the file back and returns an error the agent can retry, and a scanner exception lets the write stand.- Held by
tools/skill_manager_tool.py:50-64→tools/skills_guard.scan_skill, called:422.
I8 Is it time to run the curator? RULE · code
- Answered how
On by default (
curator.enabled: true). Code says yes when the last run is at leastcurator.interval_hours(168) old and the curator is not paused; the very first check only records the time and defers one interval. The 2-hour idle requirement (curator.min_idle_hours) is enforced only where the caller measures idle (the web server); CLI startup and the gateway's housekeeping poll pass infinite idle, and a lock file stops two processes running the same pass.- Held by
agent/curator.py:160should_run_now(168 h);:1200maybe_run_curator(idle ≥ 2 h); claim:1171-1192.
I9 Is this skill still worth keeping, or should it go stale or be archived? RULING · code
- Answered how
Code answers on each curator pass from activity dates: a skill unused for 14 days (
curator.stale_after_days) is marked stale and one unused for 30 days (curator.archive_after_days) is moved to the recoverable archive. Pinned, cron-referenced and locked skills are skipped, and a never-used skill younger than the stale cutoff is left active; at the defaults only agent-created skills are managed (bundled ones only withcurator.prune_builtins: true).- Held by
agent/curator.py:209apply_automatic_transitions(pinned, cron-referenced and locked skills exempt:220,227-232; young never-used exempt:252; agent-created skills, plus bundled ones whencurator.prune_builtinsis on,tools/skill_usage.py:186-194,:232-237).
I10 Which agent-created skills overlap, so merge into an umbrella or prune? RULING · llm
- Answered how
Off by default (
curator.consolidate: false): a scheduled curator pass is prune-only and never forks a model. When it is on, or once withhermes curator run --consolidate, a model-run agent limited to the skills toolset decides which skills to merge into umbrellas or prune and makes the changes throughskill_manage.- Held by
agent/curator.py:1087_run_llm_review(toolset["skills"],max_iterations=9999,:1118-1120, re-read by PART-4).
I11 Should cron jobs that named an archived skill load the umbrella or drop it? RULE · code
- Answered how
Code answers after every curator pass while it writes the run report: a cron job that listed a skill classified as consolidated is pointed at the umbrella, and one that listed a pruned skill has it dropped, keeping order. With consolidation off (the default) the pass forks no model, so this normally has nothing to rewrite; a failure here is logged and never stops the curator.
- Held by
agent/curator.py:677_rewrite_cron_refs, called from inside the report writer_write_run_report(:723), which also runs on dry-run (:977).
I12 What reusable skill should be distilled from what the user pointed at? (/learn) RULING · llm
- Answered how
Only when the person types
/learn. The main model gets a prompt built from their request (or, with no text, 'the workflow we just went through') and decides what skill to write and saves it withskill_manage; the usual skill-write checks apply, and the authorship and source-hygiene rules are prompt text only.- Held by
agent/learn_prompt.py:136build_learn_prompt, sent as a user turn.
I13 HUMAN: is this learned memory or skill right: keep, edit, or remove? (Journey) RULING · human
- Answered how
The person decides, when they open Journey (
hermes journey, TUI/journeyor the desktop app); nothing prompts them. Code then applies the choice: an edit rewrites the memory entry or skill, and a delete removes a memory entry or archives a skill (recoverable withhermes curator restore), refusing a pinned skill until it is unpinned.- Held by
the person, via
agent/learning_mutations.py:164,189(hermes journey, TUI/journey, desktop).
I14 HUMAN: learn from this session now, with this focus? (/refine) RULING · human
- Answered how
The person decides by typing
/refine, optionally with a focus. That review spawns at once, bypassing theenabledswitch and the idle-defer queue that hold automatic reviews, its focus text is appended to the review prompt with priority, and it is marked attended so it may also replace or remove memory entries.- Held by
the person;
run_agent.py:752-783withexplicit=True; focusagent/background_review.py:1355-1359.
I15 What from this session should the external memory provider keep? (end-of-session extraction) RULING · external
- Answered how
Off by default (
memory.provideris empty, built-in memory only). When an external provider is configured, at session end (shutdown,/new, gateway eviction) Hermes redacts the transcript and hands it to the provider'son_session_end; what to keep is decided by the provider's own code.- Held by
-
agent/memory_manager.py:629on_session_end-:636commit_session_boundary_async- shutdownrun_agent.py:871-886-/newhermes_cli/cli_session_mixin.py:534-560- eviction under the owning profile's scope (gateway/AGENTS.md, Profile scope)
I16 A background review was held back for the local machine: is the machine quiet enough to run it now, or has it waited long enough to run anyway? RULING · code
- Answered how
Only for automatic reviews whose model is the managed local runtime, with
auxiliary.background_review.deferat its defaultauto. A poller every 5 s releases the oldest held review once no turn has run in the process for 15 s and the local server reports no slot processing (an unreadable server counts as idle), or releases any review held 30 minutes regardless; at release it re-checks that reviews are still enabled.- Held by
agent/review_idle_queue.py:131-147(ReviewIdleQueue._pop_dispatchable) with:127-129(_quiet_for, viaquiet_period:119-125),:190-214(_managed_server_idle) and the constants:26-28; polled by:149-166(_run); dispatch-time re-check of the enabled flag:168-187.
How this map was made
Readers each took a part of the agent and wrote seat cards from the source. Most cards were first written at an earlier commit and re-read against the commit above; the rest were written at that commit, and every card's short answer was then written and checked at it. The map's author re-read the cards against the cited lines and corrected what the source did not support. Reviews by a model from another family (read-only, at the same commit) checked the cards against the code; the author checked a sample of their findings against the source before rewriting, and every rewritten answer went back to a further cross-family review. A checker confirms that each seat's recorded holder line exists at the pinned commit and carries its anchor text. Nothing on this page was produced by running their code.