Hermes direct-worker profile (B-OG-48, #2912)

This is the lightweight CLI profile for dispatching hermes as a direct worker (not the session-import tooling documented in hermes-sessions.md).

Hermes direct-worker profile (B-OG-48, #2912)

This is the lightweight CLI profile for dispatching `hermes` as a direct worker (not the session-import tooling documented in `hermes-sessions.md`).

The bundled `direct-shell` invocation profile in `agentic/code/providers/model-capabilities.v1.json` is version-observed against the installed `hermes 0.21.6+387.gdce1e9b`, per-user install at `~/.hermes/hermes-agent` (launcher `~/.local/bin/hermes`), re-verified 2026-10-10 on host titan: `hermes chat --help` and the installed `hermes_cli` source (`H:`/`D:` evidence grades). The root `/usr/local/lib/hermes-agent` copy is a stale 0.20.6 install and is never cited. It stays `unqualified` for dispatch until a human qualifier reviews a live run. The adapter (`tools/ralph-external/lib/hermes-adapter.mjs`) renders:

hermes chat --query-file - --oneshot --quiet --ignore-user-config --ignore-rules --source tool --provider <provider> -m <model> --reasoning <effort>

with the brief piped on stdin (never argv), and `<provider>` always explicit (see "The provider is always explicit" below).

Why `chat --query-file - --oneshot --quiet`, not the top-level `-z`

Reading the installed source (never running it) rules `-z` out as the baseline for a restricted task: it is argv-only for the brief (`hermes_cli/_parser.py:148-153`) and `hermes_cli/oneshot.py` unconditionally sets `HERMES_YOLO_MODE=1` (module docstring + line 276) with no disabling flag at all.

`chat --query-file - --oneshot --quiet` reads the brief from stdin to EOF (`hermes_cli/main.py:1711-1712`) and routes through `_run_quiet_single_query` (`hermes_cli/cli_single_query.py:199-343`), whose shared exit mapper `_single_query_exit_code` (`cli_single_query.py:152-194`) returns 1 whenever `result.get("failed") or result.get("partial") or result.get("completed") is False` — a turn that printed partial text before failing still exits 1 (unlike `-z`).

Exit 0 is still not treated as completion evidence (even though 0.21.6 fixed the specific bug this guarded against)

This profile's `parseWorkerFinal` never trusts exit 0 alone. Historically, on hermes 0.20.6, a specific case made that necessary: reasoning-budget exhaustion (then `agent/conversation_loop.py:3902`) returned `{"final_response": "⚠️ Thinking Budget Exhausted...", "completed": False, "partial": True, "error": "..."}` with no `"failed"` key, and that build's exit mapper (`1 if result.get("failed") else 0`) stayed at `0` — exit 0, non-empty stdout, genuinely incomplete.

On hermes 0.21.6 this exact case is fixed upstream. The same reasoning-budget-exhaustion result is now built by `partial_result()` (`agent/turn_truncation.py:164-180`, still `"completed": False, "partial": True`), and `_single_query_exit_code` (`cli_single_query.py:152-194`) now checks `partial`/`completed is False` too, so this case correctly exits 1 on 0.21.6. A 2026-10-10 conductor smoke run (`--provider openrouter -m anthropic/claude-sonnet-4.6 --reasoning low`) confirmed the fixed-exit-code contract: exit 0, stdout exactly the instructed answer.

`HermesAdapter.parseWorkerFinal` still does not stop at the exit-derived status, now for a narrower but still-live reason: a genuinely `completed` turn's `final_response` is the model's own text, and nothing in the CLI guarantees the model actually emitted the instructed `workerClaims` JSON document. So the final text must also parse as a strict `workerClaims` JSON document (#2890, `ProviderAdapter.parseWorkerFinal`/ `parseWorkerOutput`, `format: 'json'`) — a warning message, an error string, or any other free text fails that parse regardless of exit code. The repository's test fixture keeps a synthetic `budget-exhausted` scenario (exit 0 + free text) as a defense-in-depth regression test of this strict parse, independent of what the CLI's own exit code says.

Approval policy: not emitted-flag-only, and NOT just `--yolo`

Approval policy is host config (`~/.hermes/config.yaml`), not a single argv flag, and `--yolo` is one of several bypasses, not the only one:

  • `approvals.single_query_mode` (`tools/approval_context.py:298-304`

`_binary_approval_mode()`, used by `_get_single_query_approval_mode()` at `tools/approval_context.py:313-315`) accepts `approve/off/allow/yes` as config values, treated as auto-approve. The built-in default is `"deny"` (`hermes_cli/config_defaults.py:1682`), but a host's `config.yaml` can override it to `approve`.

  • `approvals.mode` (default `"smart"`, `config_defaults.py:1679`) can be set

to `"off"` in config.yaml, bypassing approval prompts the same way `--yolo` does (`tools/approval.py:967`, `approval_context._get_approval_mode() == "off"`).

  • `HERMES_YOLO_MODE` is a process env var frozen at Python import

(`tools/approval.py:46`) — if present in the worker's environment, every approval gate auto-approves unconditionally, with no argv involved at all.

This adapter always emits `--ignore-user-config` (verified on `hermes chat --help`), which makes the session fall back to the safe built-in defaults (`single_query_mode: deny`, `approvals.mode: smart`, never `off`) regardless of the host's `config.yaml` — session/auth credentials still resolve via `.env`, so this never blocks dispatch. `HERMES_YOLO_MODE` (and any other `HERMES_*` override) is defeated structurally, not by a flag: the bundled profile's `envAllowlist` is `[]` and the shared shell renderer clears the child environment (`env -i <allowlist-only>`), so an ambient `HERMES_YOLO_MODE` in the conductor's own environment can never reach the child — proven by an explicit assertion (`hermes-direct.test.ts` HD10). The `approval` control still stays `unqualified` on the bundled registry: `--ignore-user-config`'s effect is real and source-evidenced, but it is not live-qualified.

There is no `--usage-file`/JSON-report equivalent for `chat`: that flag is declared only at the top level (`_parser.py:156-160`) and read only at the `-z`/`--oneshot PROMPT` dispatch sites (`hermes_cli/oneshot.py:215-324`). Final-output extraction for this profile is stdout text on exit 0 PLUS the strict workerClaims parse above, never exit code or stdout non-emptiness alone.

Manual smoke run

The conductor runs this once, by hand, after `hermes` is installed, to record a pass/fail and the observed CLI version on the tracking issue. It is not part of default CI and performs exactly one live, billable call.

The provider is always explicit. Under `--ignore-user-config`, an omitted `--provider` falls back to `auto` (`hermes_cli/runtime_provider.py:490-499`, `resolve_requested_provider`), which picks whatever credentials happen to be present. The profile refuses a missing or `auto` provider. Put the provider key in the child process's environment only (for example `OPENROUTER_API_KEY`, read from the operator's vault at call time), never on argv or in a file:

echo 'Reply with exactly: hermes-smoke-ok' | OPENROUTER_API_KEY="$KEY" hermes chat \
  --query-file - --oneshot --quiet --ignore-user-config --ignore-rules --source tool \
  --provider openrouter -m anthropic/claude-sonnet-4.6 --reasoning low ; echo "exit=$?"

Reading the result

  • Exit code: `echo "exit=$?"` must print `exit=0`. Any non-zero exit is

a fail, even if some text was printed to stdout. Missing credentials exit 1 with "No inference provider configured" or "No usable credentials found".

  • stdout: after any leading Hermes startup diagnostics, it must contain

exactly `hermes-smoke-ok` and nothing else. The tolerated diagnostics are lines rendered by `_cprint` that start with a bare `⚠`. On hermes 0.20.6 that included "⚠ tirith security scanner enabled but not available" when the tirith binary was missing on the host (that build's `_ensure_tirith_security`, `cli.py:8805-8822`); on hermes 0.21.6 the tirith scanner was removed entirely (`hermes_cli/config_migrations.py:686-702`, "the bundled tirith scanner left Hermes") and a 2026-10-10 smoke run confirmed the line no longer appears. The parser strips only matching leading lines (`stripHermesStartupDiagnostics`) regardless of version. A diagnostic after the answer, any other preamble, or a model-facing `⚠️` budget warning fails. A non-empty but different message is still a fail even though it exits 0; see "Exit 0 is still not treated as completion evidence" above.

Record the installed `hermes --version` output, the command above (without the key), and the pass/fail outcome (exit code + stdout text) on the tracking issue — the result of each run is posted there, not duplicated into this doc. This manual run is evidence only; it never marks the registry profile as qualified for automated dispatch.

Isolation and caps (`hermes chat --help`, v0.21.6)

  • `--ignore-rules`: "Skip auto-injection of AGENTS.md, SOUL.md, .cursorrules, memory, and preloaded skills. Combine with --ignore-user-config for a fully isolated run." It is always emitted with `--ignore-user-config`, so a worker never loads repository rules or user memory into its brief.
  • `--source tool`: session source tag. Help: "Use 'tool' for third-party integrations". It keeps worker sessions apart from the user's own sessions.
  • `--max-turns N`: "Maximum tool-calling iterations per conversation turn (default: 500, or agent.max_turns in config)". This is the evidenced step-cap mapping. The step-cap control stays `unqualified` until it is wired to an invocation-selected limit.
  • `--run-budget SECONDS`: an optional wall-clock budget per run. It is complementary to the conductor's own timeout.