Skip to content

chore(fork): merge upstream main b3746e35ddd3 (RIG-8527) - #50

Merged
mattwilkinsonn merged 8909 commits into
rigel-releasefrom
hermes-sync/rig-8527-merge-upstream-main-b3746e35ddd3
Nov 10, 2026
Merged

mattwilkinsonn merged 8909 commits into
rigel-releasefrom
hermes-sync/rig-8527-merge-upstream-main-b3746e35ddd3

Conversation

@rigel-mintaka

@rigel-mintaka rigel-mintaka commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Merges upstream NousResearch/hermes-agent main (b3746e3) into rigel-release (b8d4599, after #46–#49) through rjc upstream-sync. Keep it a merge commit, so rigel-release keeps upstream history.

Related Issue

RIG-8527. Card-format ruling: RIG-8797 (upstream bounded JSON).

Type of Change

  • ♻️ Refactor (no behavior change)

Changes Made

How to Test

  1. Focused suite at -j 3, 1,437 files: 12,454 passed. The remaining failures also fail on upstream (cron interpreter cases, approval_boundary), or they are e2e tests for connections we don't use.
  2. Hosted, clarify and plugin-origin regression checks: 57 passed. Approval and Discord gateway tests: 775 passed.
  3. Ruff passes.
  4. Full upstream suite not run because of host load (RIG-8772).

Checklist

Code

  • Conventional Commits
  • Only the sync and its conflict resolutions
  • Focused tests pass, apart from failures shared with upstream

Documentation & Housekeeping

  • Documentation: N/A

For New Skills

N/A.

Screenshots / Logs

N/A.

Refs RIG-8527

Co-authored-by: Matt Wilkinson matt@rigel.build

Siddharth Balyan and others added 30 commits November 9, 2026 18:06
…d changes

A provider skipped because plugins.disabled names it stayed missing after
the user enabled it in a running gateway or Desktop backend. The home layer
only rescanned when the plugin directories' mtimes moved, and enabling a
plugin edits config.yaml alone. The layer now remembers the disabled set it
scanned with, so a changed selection triggers the rescan on the next
periodic check. Answers Greptile r4511588226 on NousResearch#151816.
…ipped a plugin

1ee5734 loaded config.yaml on every periodic plugin-dir check. That
broke two CI tests: a bypass session that must never open the profile's
config.yaml, and a remote config backend whose poll raced the extra read.
The layer now records the disabled set only when a scan actually skipped
a plugin it names, and the refresh compares against config only then.
Every other home does no config reads, as before.
…only

The /model picker for a Claude subscription (DirectSDK) profile lost its
cached live list every time Claude Code refreshed its OAuth token, because
~/.claude/.credentials.json (and the other vendor token files) were hashed
into every non-Codex provider's catalog fingerprint. An external-process
provider now fingerprints its command env overrides, the resolved binary
and its declared catalog_credential_files, and skips the hard-coded vendor
files and the Hermes auth stores, which its catalog never reads.

Closes NousResearch#129094.
…aunches

The catalog fingerprint resolved the profile's default process_command even
when a process_command_env_vars override picked another executable. An
upgrade that repointed an override symlink changed neither the override text
nor the default binary, so the picker kept the old executable's model list.
The launch path and the fingerprint now share external_process_command, so
both select the same command. Answers Greptile r4511687631 on NousResearch#151817.
…e URL

A dashboard or TUI session that runs on a provider other than the
profile default (an OpenAI Codex session on a profile whose default is
the Claude subscription DirectSDK provider) got its /fast ultrafast
refused: the slash worker's HermesCLI seeded base_url from
model.base_url (process://...) while --provider said openai-codex, and
the route gate rejected that mix before any entitlement check.

Both spawn sites (first use in slash.exec and _restart_slash_worker)
now pass the live agent's base_url as --base-url, the same way
--provider already travels (NousResearch#57283). The worker sets it as the CLI's
seed only, not as an explicit launch base_url, so the gateway resume
policy for the worker's session is unchanged.

Mechanism and test shape from @686f6c61's handoff on NousResearch#141953 (branch
fix/141953-slash-worker-base-url, commit d9e5bad); the fork is no
longer reachable, so this is a reimplementation with credit.

Closes NousResearch#141953.
…ion endpoint

The base-URL test drove the worker with a fake HermesCLI, so it showed the
assignment but not that the command reads it. The new test runs the
worker's main() with a real HermesCLI, a conflicting process:// profile
default and a stubbed Codex entitlement lookup, then sends /fast ultrafast.
The lookup must receive the session's chatgpt.com URL and
_explicit_base_url must stay unset. It fails without the worker's seed.
The fake-CLI test goes back to covering --provider only. Answers Greptile
r4511615523 on NousResearch#151818.
hermes chat -q / -z / --oneshot fired the background title model call
on the first turn like an interactive session. Nothing ever shows that
title (one-shot sessions are hidden from every picker), and on a process
provider such as the Claude subscription DirectSDK plugin the call is a
second paid CLI spawn per run.

_maybe_title_session_at_turn_start now returns early when
agent_is_oneshot(agent), next to the cron skip; the oneshot_footprint
docstring lists it with the other things a one-shot does not do.
…core

ci: lint, tests and js-tests run their commands through just
…port

The catalog field reader printed shell assignments and the gate parsed them
back with shlex, a shell layer between two Python programs. It now prints
one JSON object, and the gate rejects the entry unless it gets exactly
REPO, SHA, SUBDIR and BAD as strings.

verify_uv_lock.py let TimeoutExpired escape after 600 s, so the job lost the
attempt's log and published no review status. A timed-out attempt now prints
its partial log, reports "uv.lock check timed out" and stops: retrying three
more 600 s attempts would outlive the job.
…scripts

ci: repo-guards, plugin-catalog and skills-index steps run scripts instead of inline bash
README rule 17 lets a catalog entry ship a native binary listed under
attested_binaries, if `gh attestation verify` proves the plugin repo built it
at the pinned commit. The old inline step fed its parser as
`python3 -I - "$entry" <<PYEOF </dev/null`; the later </dev/null replaced the
heredoc, so the parser ran an empty program, the list always came back empty
and `gh attestation verify` never ran. NousResearch#151877 moved the step into
scripts/ci/ and kept that dead branch out, so behaviour stayed the same.

The field reader now returns the attested paths beside the clone source and
rejects any that is not a plain relative path. The gate verifies each one
against the entry's repo at the pinned sha, then removes it so the payload
scan ignores it, and any other binary still fails. No catalog entry declares
attested_binaries today, so nothing admitted now changes.
_check_attested removes each verified file from the clone so the payload scan
ignores it, and it ran before _check_self_updater. A plugin could list a JS
updater under attested_binaries: once its provenance checked out the file was
gone, the self-updater grep never saw it, and users still got it from the
pin. Provenance shows where a file came from, not what it does, so the
self-updater gate now runs before anything is removed.
A provider profile's classify_api_error can mark an auth failure as one no
other provider can fix (no CLI login where an external-process provider
runs). The 401/403 escalation in route_classified_error ignored that flag
and switched to the fallback chain anyway, so a subscription route silently
billed a different provider (NousResearch#132337). Built-in auth verdicts keep
should_fallback=True, so their behavior is unchanged.
New recipes, each one line over the script CI already ran:

- `just docs-generate`: extract-skills.py, then generate-skill-docs.py.
- `just docs-check-generated`: the "committed skill docs match the
  generator" block, moved verbatim into scripts/ci/check_generated_skill_docs.sh
  (same `git add -N`, same diff paths, same ::error:: line and exit 1).
- `just docs-check-links`, `just docs-lint-diagrams`.
- `just docs-check`: generate, then the three checks, in CI's order.

docs-site-checks.yml calls docs-install, docs-generate, the three checks
and docs-build. deploy-site.yml calls docs-generate and docs-install; its
generate-skill-docs.py step moves up beside extract-skills.py because it
reads neither the plugin stars nor the plugin catalog the steps between
them write. Its full bilingual `npm run build`, staging and Pages deploy
steps stay inline. Python recipes run through scripts/hermes-python, which
uses the interpreter setup-pm exports as HERMES_PYTHON, as python3 did.
Two new recipes, one line each:

- `just electron-runtime`: node scripts/build/electron-runtime.mjs --source .
- `just js-install-tests`: the tests-js-only, production, no-scripts pnpm
  install the bundle smoke drives its chat with.

e2e-desktop.yml and e2e-desktop-core.yml call `just js-install` and
`just electron-runtime`; e2e-desktop-update.yml calls `just js-install`.
desktop-bundle-smoke.yml calls `just js-install-tests` on both its macOS
and Windows legs (they share one step list), each after setup-pm with
toolchain all, which installs just. js-tests.yml,
windows-desktop-attach-e2e.yml and the install-e2e-*-run.yml workflows
still spell the same commands out.
…hrough just

`just termux-payload <payload> <package>...` calls the new
scripts/termux/stage_payload.sh, which runs termux_pkg_build.sh for each
package in order, then stage_runtime_libs.py: the same four commands the
"Stage the pinned bionic runtimes" step ran (build_cpython.sh and
build_uv.sh are one-line wrappers over termux_pkg_build.sh with the
package name as the subdir). The archive_inputs step before it stays a
separate step: it needs R2 credentials and is not a developer command.

The native linker contracts step runs `just test -j 2 <files>`, which is
scripts/run_tests.sh --require-paths with the same arguments.
desktop-bundled-release.yml stages the same payload in separate steps and
is unchanged.
…cs job

docs-lint-diagrams called the website's lint:diagrams script, which needs
ascii-guard on PATH; only CI installed it, so `just docs-check` stopped
there locally. The recipe now runs the pinned ascii-guard through
scripts.ci.python_packages, the same pm environment CI uses, with the
same arguments: 442 files checked locally, and a malformed box fails it
as it fails the npm script.

check_generated_skill_docs.sh moves to website/scripts/, beside the
generator it checks, so a change to it starts the docs job.
…x alone

docs-check-generated compared the working tree with the index, so on a
laptop any unstaged docs edit failed as generator drift, just stopped before
the links and diagram checks, and `git add -N` left untracked drafts
intent-to-add in the real index. The check now works in a scratch copy of
the index, and given a generator command it snapshots the docs before
running it. CI's no-argument call compares against the committed docs as
before.

Co-authored-by: Bear Huddleston <196404565+BearHuddleston@users.noreply.github.com>
value.get(type) in the image_type set raises TypeError (unhashable type) when a message part's type field holds a dict, list, or set. Guard the membership test with isinstance(str) so only string types are compared and non-string parts fall through to the existing recursion.
…nvariants

A non-string type neither raises nor stops the descent: the nested image under a
union-typed node is still found, which a skip-the-dict fix would miss (danfye,
KCAYAAI on NousResearch#119827). The other cases pinned behaviour main already had.
…he provider's process:// address

legacy_launch_params replayed a pre-authority row's saved base_url as an
explicit launch override. For an external-process provider that value is the
provider profile's own process://<name> address, which the runtime reads from
the profile and never from model.base_url; _apply_launch_overrides accepts
only http(s), so adoption refused with invalid_params field=base_url and a
canonical Bot Chat holding such a route refused every bot-to-bot delivery.

Keep the provider, drop the address.
…only process://

The salvaged fix keyed the drop on external-process providers. A row saved on
the virtual MoA provider carries `moa://local` the same way and is refused by
the same http(s) launch check (NousResearch#147676). The rule is general: when the saved
route names a provider (not bare custom) and its base_url is not http(s), the
address belongs to the provider, so the provider alone is the route.

A bare-custom route keeps its URL, so the custom-endpoint paths are unchanged.
…attestation-gate

fix(ci): the plugin catalog gate verifies attested binaries
Ravi Tharuma and others added 16 commits November 9, 2026 15:33
A plain group reply carries message_thread_id as a reply anchor, and a
forum_topic_created service message must stay ahead of the thread-less
opener that adopts its topic, so both keep the chat lane.
A message outside allowed_topics was dropped with no log line, so a topic
the operator meant to allow (often a thread-less General message read as
topic 1) looked like a dead gateway (NousResearch#52635). The first drop per
(chat, topic) is now named at INFO with the allowlist. _topic_gates_pass
moves from adapter.py into the topic-routing mixin.
The handoff repointed the route but left the persisted topic binding on the
topic's old session, so the binding heal switched the synthetic handoff turn
and every later message straight back to it: the handed-off conversation
never reached the topic. Same class as /branch --here and /resume (NousResearch#58850).
…ollow-ups

Greptile finding "Follow-ups overtake the topic opener": a thread-less DM
opener the adapter adopts into a just-created topic stayed on the chat
queue, while later explicitly-threaded messages used the topic queue, so a
slow opener (photo download) could be overtaken. The update processor now
mirrors the creation record and queues such an opener on both the chat lane
and the new topic's lane.
…/resume

Review finding (NousResearch#151772, P1): after a handoff repointed a Telegram DM
topic and was suspended, a /resume that finished meanwhile was undone:
the handoff resumed and wrote its topic binding unconditionally (route
newer, binding cli-session), so the next message's heal moved the topic
onto the handed-off session, and the synthetic handoff turn still ran in
the newer conversation.

The handoff's repoint and binding write now run inside the same per-topic
publication /resume and /branch --here use, so the binding heal never acts
between them. When another switch overlapped it, the binding is written
only while the route still names the handoff, under the shared bind lock;
a superseded handoff writes nothing, skips its synthetic turn and fails
(the CLI keeps its session and can retry).
… the new topic

Review finding (NousResearch#151772, P1, Greptile 4515985243): the processor and the
adapter kept separate creation records. A slow root DM update held the chat
lane, so the forum_topic_created handler (the adapter's record) ran after
the first explicitly threaded message had already ended the follow, and a
later thread-less root message within five seconds adopted the topic.

Creation and adoption are now one ordered decision: update admission calls
the adapter's _admit_topic_follow in arrival order (recording an authorized,
non-bot creation; adopting, extending or ending the follow), and the
handlers reuse that per-message result instead of deciding again.
…s suspended

Review finding (NousResearch#151772, P1, Greptile 4515985269): the handoff re-checked
the route only when another topic publication overlapped it, but /new and
/reset move the route and write their binding without one. A /new landing
between the handoff's switch and its bind was overwritten, and the next
turn followed the binding back to the handed-off session.

The handoff now binds through _follow_telegram_topic_switch, which checks
the route unconditionally under the shared bind lock, so a superseded
handoff writes nothing, skips its synthetic turn and fails.
…Research#75416, salvage NousResearch#76685)

DoH routinely answers api.telegram.org with a single A record. The adapter
discovers fallback IPs once per connect, so that lone IP was the whole
fallback path: a DNS blip plus that edge going dark stranded polling until
a restart. The seed IPs now always follow the DoH answers.

Salvages NousResearch#76685 (@duclucky), re-applied onto the current discovery code.
_replacement_telegram_adapter() looked the reconnect replacement up in runner.adapters, which only ever holds the PRIMARY profile's map. A torn-down secondary adapter therefore delegated its in-flight sends - mid-turn progress, notices - to the primary profile's bot, delivering one profile's traffic into another bot's chat (NousResearch#143364, tracker row MSG-43).

Resolve through runner._adapters_for_profile(self._owner_profile) instead: a named profile with no live adapter gets {} and fails closed, the primary keeps its own map. _wait_for_reconnection() shares the helper, so a stale secondary now actually waits for its own reconnect instead of seeing the primary's live bot.

Closes NousResearch#143364.
…ofile replacement (NousResearch#98228, NousResearch#143364)

After a replacement reconnect an in-flight turn keeps the retired adapter.
send() already delegated to the replacement, but edit_message() and
delete_message() returned "Not connected"/False, so progress fragmented into
new bubbles and cleanup left them behind. Both now delegate the same way.

The replacement lookup (moved with the reconnect wait into
telegram_replacement.py, shrinking adapter.py) resolves through the
runner's _adapters_for_profile only, so a secondary with no live
replacement fails closed instead of borrowing the primary bot.
…sResearch#136412, salvage NousResearch#136427)

The fallback transport walks sticky -> IPv4 literals -> hostname inside one
send, and every path got the client's full 10s connect timeout. With the two
seed IPs that is 3 x 10s = 30s, exactly the telegram-send deadline. The
deadline fired mid-walk as a bare TimeoutError, which _classify_send_exception
must treat as possibly delivered (retryable=False), so the reply was dropped:
no retry and no plain fallback.

Both transports built by build_ptb_requests now share a 15s connect budget per
request walk. The first path keeps its full connect timeout and later paths
split the rest. The walk therefore ends with its own ConnectTimeout, which is
classified as a safe retry. A tighter configured connect is never widened.

Salvages NousResearch#136427 (@liuhao1024). The budget split is ported. The adapter.py
hunks (moving _shutdown_abandoned_app) are dropped: the budget now lives in
the shared request builder.
…view

5948ab5 made a typing keep-alive take the chat's shared send+edit
slot when it fired. The stream consumer SKIPS an interim edit while that
slot is held, and `_keep_typing` ticks every 2 s with a 1 s slot, so in a
group (which streams by editing one message) typing kept stealing the
slot from the preview edits: the preview stayed on its first chunk. When
Telegram then refused the finalize edit, the continuation was computed
from that stale prefix and the first message read "<<A-fin-stre", so the
answer's opening never appeared whole.

Typing still checks the slot and the flood window (it stays quiet while
a send or edit holds the chat) and still sends at most one call per chat
per 2 s, so NousResearch#99643's fix (N DM topics multiplying sendChatAction) is
kept. It just no longer claims the slot.

Release-only e2e red on v0.23.1 rc.2 (run 42720042886):
test_telegram_contract.py::test_contract[stream_finalize_rejected_group],
8/8 locally on 4f6ebb9. Bisected (DM cell + group cell together, as
CI runs the file) to 5948ab5. With this change: the pair passes 3/3
and the whole contract file passes (14/14).
…ir requester

NousResearch#152125 bound shared-chat exec approvals to the requester (or
approvals.approvers / the platform admins), but the other prompts a chat
member can answer for someone else stayed open: another member could press
"Always" on a relay or native /reload-mcp confirmation and run its handler,
or answer the agent's clarify question (a choice button, "Other", or a typed
reply) for the person who asked.

Both prompts now record the same authority an exec approval records when
raised (tools.approval_context.prompt_authority: the turn's actor, else the
command's sender, plus approvals.approvers and the chat type), and every
chat answer path checks it through gateway.approval_actor.may_answer:

- relay prompt_response (gateway/relay/prompt_resolvers.prompt_refusal),
  with the bot-owning profile's admins as for exec approvals;
- Telegram, Discord, Slack, WhatsApp Cloud and Feishu button callbacks
  (slash_confirm_refusal / clarify_refusal, published on the plugin API
  beside approval_refusal);
- the runner's typed-reply intercepts (/always, "2", free text after
  "Other"); another member's text is routed as their own message.

A refused tap gets the existing "Only <name> can answer this approval."
and the prompt stays pending. A turn merged from two senders records no
requester, so only approvers and admins can answer, as for approvals.
The default approvers ["*"] keeps today's behaviour.

Supersedes the slash_confirm/clarify half of NousResearch#72008, whose session-owner
mechanism no longer applies on main (main records the requester itself).

Co-authored-by: necoweb3 <65560494+necoweb3@users.noreply.github.com>
…checked against

Review of NousResearch#152310 (blocking): _hm_clarify_reply checked the sender against the
session's oldest clarify, awaited transcription, then answered whichever clarify
was oldest by then. If Alice's question expired mid-transcription and Bob's turn
asked its own, Alice's transcript answered Bob's question unchecked. The reply now
resolves the checked clarify_id (clarify_gateway.attempt_text_response); if that
entry is gone, the transcript falls through as an ordinary message. Every other
answer path (relay, Telegram/Discord/Slack/WhatsApp Cloud/Feishu taps, typed
slash confirm) already resolves by exact id with no await between check and resolve.

P3s from the same review:
- Feishu HTTP-webhook card replies keep the refusal toast, as long-connection
  mode does (a refused tap answered {} before).
- _prompt_refused_to's string dispatch becomes two direct helpers.
- _request_slash_confirm reads event.sender_id(); the decline-fallback test
  now passes a real MessageEvent instead of a SimpleNamespace.

Co-authored-by: necoweb3 <65560494+necoweb3@users.noreply.github.com>
@rigel-mintaka rigel-mintaka changed the title fix(nix): module extraPythonPackages/extraDependencyGroups extend the package's own chore(fork): merge upstream main b3746e35ddd3 (RIG-8527) Nov 10, 2026
@linear-code

ghost commented Nov 10, 2026

Copy link
Copy Markdown

RIG-8527

@rigel-mintaka

ghost commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Rigel patch inventory

PR title class evidence (symbol/test checked) note
#4 feat(vault): unlock 1Password through the desktop app CLI integration KEPT test_browser_vault_unlock_uses_the_app_without_a_prompt present —
#5 feat(approval): show a plugin's request and review it before the human SUPERSEDED test_reviewer_deny_reaches_human_and_final_choice_controls_result present #43 replaced the earlier deny-before-human behaviour.
#6 feat(vault): unlock several 1Password accounts with one master password KEPT test_unlock_is_all_or_nothing present —
#7 feat(vault): vault_secret tool for any 1Password field KEPT vault_secret_run present —
#8 fix(discord): allow approval prompts to stay open past 15 minutes SUPERSEDED _DISCORD_PROMPT_TIMEOUT_MAX identical in merge and upstream Upstream supplies the same broader timeout bound.
#9 fix(vault): refuse vault_secret run when a value is too short to redact KEPT test_run_refuses_a_value_too_short_to_redact present —
#10 feat(approvals): require a reason for vault_secret run, and have the reviewer explain KEPT test_run_requires_a_reason present Deny-message test removed on rigel-release by #43 (DENY now goes to the human).
#11 feat(approvals): show the real tool call in plugin approval prompts KEPT test_without_display_the_call_and_its_arguments_are_shown present —
#14 fix(gateway): admit watch wakes in bot-created Discord threads, drop final refusals KEPT test_watch_wake_admits_in_a_bot_created_thread present —
#13 feat(vault): approval-gated vault_secret create and edit KEPT test_create_from_file_and_literal_returns_handle_and_labels_only present —
#12 feat(discord): forum post actions for the Discord tool KEPT test_create_forum_post_supports_required_tags present —
#16 fix(discord): scope, rate-limit and chunking fixes for forum actions KEPT test_discord_request_retries_short_rate_limits present —
#15 test(gateway): cover every watch-wake refusal class, document the drop SUPERSEDED test_drain_requeues_only_owner_state_refusals present in merge and upstream Refusal policy evolved upstream.
#17 feat(cron): allow the core discord toolset in cron jobs KEPT test_discord_toolset_allowed_in_cron_when_configured present —
#18 fix(vault): allow https subdomains of a saved login site KEPT origin_match_rule present; upstream lacks it —
#19 feat(memory): config to apply background-review memory consolidations directly KEPT _background_review_destructive_mode present —
#20 fix(gateway): route-match wakes on routing keys, bound final durable refusals SUPERSEDED test_durable_completion_refusal_settles_by_refusal_kind present Upstream route enrichment replaces the Rigel helper.
#23 refactor(gateway): share relay admission readiness with completion delivery KEPT admission_ready present —
#25 ci: add a Rigel CI gate on GitHub-hosted runners KEPT .github/workflows/rigel-ci.yml contains name: Rigel CI; absent upstream —
#26 fix(cron): re-dispatch an unstarted unknown execution and raise an incident KEPT test_never_started_oneshot_runs_once_after_restart present —
#27 fix(approval): fail closed on Cotal mesh turns KEPT _is_peer_mesh_approval_context present in merge, absent upstream —
#28 fix(discord): allow approval prompts to stay live for up to a day SUPERSEDED _DISCORD_PROMPT_TIMEOUT_MAX shared in merge and upstream Upstream's shared timeout ceiling supersedes the one-day cap.
#29 fix(cron): fail the run and open an incident when a pre-run script times out KEPT test_pre_run_script_timeout_creates_incident present —
#30 test(cron): pin the pre-run timeout notice and de-race the restart worker test KEPT test_pre_run_timeout_is_identified_as_a_script_timeout present —
#31 feat(discord): daily approvals index thread KEPT test_approval_index_creates_a_forum_post_for_the_day present Escape/mention test replaced on rigel-release by #33's ping test.
#34 feat(plugins): reset a gateway session by id from a plugin KEPT test_plugin_reset_by_session_id_rotates_the_session present —
#40 fix(gateway): read the drain cap from Home Manager launchd jobs KEPT _HOME_MANAGER_LABEL_PREFIX in merge, absent upstream —
#36 fix(approval): raise reviewer budget and reject truncation KEPT test_budget_leaves_room_for_a_reason present (test_smart_approval_injection.py) —
#35 feat(session): plugin-origin session turns with operator-only approval KEPT gateway/plugin_origin_session.py present; test_plugin_origin_session present —
#38 fix(cron): tell cron runs to use terminal when execute_code is denied KEPT test_prompt_mentions_terminal_when_cron_mode_denies_execute_code present —
#33 feat(approvals): show vault targets and MCP args; ping from the index line PARTIAL test_describe_ref, test_index_pending_line_pings_only_allowed_users_and_settle_edit_does_not_ping present MCP args use upstream bounded JSON, not call-style; secret redaction kept.
#43 feat(approval): send a reviewer DENY to the human instead of blocking KEPT test_reviewer_deny_reaches_human_and_final_choice_controls_result present —
#37 feat(approval): give the smart reviewer the working directory KEPT test_working_directory_is_limited_to_300_characters present —
#44 fix(approval): a reviewer DENY needs its own human answer KEPT test_approve_stops_at_denial_and_reports_card present —
#45 feat(cron): keep a run cut off by a gateway restart as pending replay KEPT test_manual_run_preserves_pending_replay present —

Needs attention

Counts: KEPT 29, SUPERSEDED 5, PARTIAL 1, MISSING 0.

@mattwilkinsonn
mattwilkinsonn marked this pull request as ready for review November 10, 2026 14:37
@rigel-mintaka
rigel-mintaka force-pushed the hermes-sync/rig-8527-merge-upstream-main-b3746e35ddd3 branch from 2cc28d5 to 0a781c7 Compare November 10, 2026 15:34
@rigel-mintaka

ghost commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 1 (one all-lens review, at 2cc28d5), then re-sync

Re-synced onto rigel-release b8d4599 (after #46–#49), via rjc sync on the recorded upstream merge. There were 3 new conflicts:

Findings at 2cc28d5 (0 high, 1 medium, 7 low):

Sev Finding Action
medium The dead RelayAdapter._resolve_exec_approval skipped the approver check, and a test used it Deleted. The test now drives the live prompt_resolvers.resolve_exec_approval.
low _drop_entry called settle(None) when only observers were set Guarded.
low Trailing whitespace; missing PEP 8 blank lines Fixed.
low Test name said "fresh context" Renamed to test_each_row_runs_in_a_copy_of_the_drain_start_context.
low /approve own-card has no test Already covered by test_approve_stops_at_denial_and_reports_card and test_approve_all_reports_remaining_denial.
low Telegram legacy string-state fallback Kept on purpose for legacy state; harmless.
low Truncation flag computed in two places (base vs Discord); duplicated notice literal; double origin check in vault fill Fail-closed and cosmetic. Left as is in the sync, to keep this diff to conflict resolution.

Tests on the re-synced tree:

  • Delegation, lifecycle and review: 56 files, 447 passed.
  • Relay, approval, Discord and plugin-origin after the fixes: 33 files, 684 passed.
  • Broad suite (1,396 files): 11,701 passed, 2 failed. Both are test_cron_script interpreter cases, which also fail on upstream.
  • Ruff is clean.

@rigel-mintaka

ghost commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 2 (interdiff 2cc28d5 → 0a781c7)

0 high, 1 medium, 2 low.

Sev Finding Action
medium With tiers, the schema still offered delegation.backend's per-task params, and a tier route then dropped them silently Tiers take precedence: the schema omits backend params when tiers are set, and a tiered spawn carrying one returns a tool error. Two new tests.
low A typo in a tier's reasoning_effort fell back to the parent level It is now refused, as on backend routes. New test.
low _with_tier_effort docstring Reworded to "resolved tier route's effort".

Fixed at aedfe38 (squashed into the recorded merge). Tier tests: 24 passed. They fail without the fix: 3 failed. Delegation, review and lifecycle suites: 161 passed. Ruff is clean.

Co-authored-by: Matt Wilkinson <matt@rigel.build>
@rigel-mintaka
rigel-mintaka force-pushed the hermes-sync/rig-8527-merge-upstream-main-b3746e35ddd3 branch 2 times, most recently from aedfe38 to dc1ff60 Compare November 10, 2026 16:24
@rigel-mintaka

ghost commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 3 (interdiff 0a781c7 → aedfe38): 0 high, 0 medium, 2 low

  • low: the schema used truthy tiers while spawn used is not None, and _load_config ran twice. Fixed: one load and one predicate.
  • low: the backend-param error hint said "name a tier" on a call that already did. It now says to remove the param.

Fixed at dc1ff60. Tier, backend and delegate suites: 111 passed. Ruff is clean. The interdiff left is wording plus one predicate, verified by tests; all-clear on dc1ff60.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.