Commit Graph

2859 Commits

Author SHA1 Message Date
teknium1
cca83848ec chore(memory): holographic leaves core for its community catalog plugin (re-land #136424)
Holographic has a standalone third-party maintainer (itpartypattaya) and a
reviewed catalog entry, so it leaves core now rather than on Oct 15. Restores
9e827685f2 exactly: reverts the revert (435b7bd3a7) and its health waivers
(b9ecc727e9). byterover and retaindb have no maintainer and stay bundled until
the announced date.
2026-10-10 21:18:25 -07:00
teknium1
b9ecc727e9 chore(health): waive pre-existing broad excepts in the restored holographic provider
The revert re-adds plugins/memory/holographic verbatim; the code-health ratchet
measures it against the post-removal tree, so its long-standing broad excepts
read as new. Comment-only waivers, no code change; the provider leaves core on
Oct 15.
2026-10-10 20:58:02 -07:00
teknium1
435b7bd3a7 Revert "chore(memory): holographic leaves core for its community catalog plugin"
This reverts commit 9e827685f2 (#136424). It landed before the announced
October 15, 2026 date for holographic leaving core; it re-lands on or after
that date. Restores plugins/memory/holographic and its tests/docs.
2026-10-10 20:58:02 -07:00
Teknium
9e827685f2 chore(memory): holographic leaves core for its community catalog plugin
Holographic now ships from itpartypattaya/hermes-plugin-holographic through the
plugin catalog (plugin-catalog/holographic.yaml, v0.6.1 @ 46b6f908, a fork of
Nous's archived handoff copy). It keeps the provider name, memory_store.db and
plugins.hermes-memory-store, and memory_provider_migration already lists
holographic in _LEFT_CORE. Deleting the in-tree copy and its tests lets
`hermes update` and agent start install the catalog plugin for every home that
has memory.provider: holographic.

Host code that still releases memory-store handles on profile unserve/delete
resolves the store through import_provider_module, the same way profiles.py
already does, and no longer imports plugins.memory.holographic. Tests that used
holographic as "a bundled provider" now use byterover. The journal-mode
contract drops the holographic row because the plugin owns that store now.

People who installed the archived Nous repo by URL have no catalog record, so
`hermes plugins update` kept pulling the archive. read_catalog_sidecar now
adopts an install from that repo as the holographic catalog entry, and update
re-pins it to the maintained repo at the reviewed sha. That catalog re-pin
also needed the reviewed-pin scan trust that catalog installs already get. Without
it, update_plugin scanned the reviewed tree as an unreviewed community source
and blocked on caution findings, and that blocked every catalog re-pin of
holographic (the reviewed 46b6f908 tree has 14 caution findings).
2026-10-10 20:31:02 -07:00
Teknium
336b11584c fix: replay prior reasoning to every chat-completions route that can read it
Every model reasons and intends that reasoning to be replayed, but the
chat-completions wire echoed reasoning_content only to DeepSeek/Kimi/MiMo/
Ollama (plus a hardcoded Portal stealth model) and reasoning_details only to
OpenRouter; turn_context popped `reasoning` everywhere, so Ollama's /v1
(reads only `reasoning`) never saw any.

- message_sanitization owns one route->carrier table
  (_REASONING_CARRIER_ROUTES): strict schemas (Mistral, Groq, Cerebras,
  SambaNova) get nothing, Fireworks reasoning_content only, OpenRouter-format
  gateways (OpenRouter, Kilo, Vercel AI Gateway, Nous Portal) all three,
  Novita/MiniMax /v1/TokenHub + reasoning_details, local/Ollama/Upstage +
  reasoning, everyone else reasoning_content.
- The " " pad stays limited to the must-echo tier (DeepSeek, Kimi, MiMo);
  Ollama leaves it (no echo requirement) and the stealth hardcode is gone.
- Reactive recovery: a 4xx unknown-field rejection that NAMES a reasoning
  key this request carried is recorded per (provider, host, model) and the
  retry re-shapes api_messages without it. Generic upstream errors never
  count; a recorded key falls through, so no loop. History is untouched.
- Nous Portal reads reasoning_details again: the #118182 "replay budget"
  does not exist in the Portal and does not reproduce live.
- Gemini thought_signature survives on Gemini hosts/providers even when the
  model id is an alias.
- Vendor keep-reasoning flags (docs-only): GLM thinking.clear_thinking=false,
  DashScope preserve_thinking / clear_thinking, Kimi K2.x thinking.keep=all.
- stale_thinking_reaches_wire follows the same table, so compaction trigger
  and tail walks agree with what is sent.
2026-10-10 19:22:07 -07:00
Maciej Lisiewski
c57fbf4584 ci: fix remaining ASYNCXXX linter errors 2026-10-10 15:20:14 +02:00
Maciej Lisiewski
bda3aea496 ci: remove unused variables (linter) 2026-10-10 15:20:14 +02:00
Maciej Lisiewski
0006a5e229 ci: fix PLW1509, PLW1510, PLW2101 linter errors 2026-10-10 15:20:13 +02:00
Teknium
a274d3388f fix: core modules import again on Python 3.11-3.13 (re-quote eager forward references)
#135348 ran ruff's UP037 autofix with ruff.strict.toml's target-version = "py314". On 3.14
annotations are lazy (PEP 649), so stripping quotes is safe there; on 3.11-3.13 a function or
variable annotation is evaluated when the def or statement runs. Every unquoted annotation that
names the enclosing class (`def clone_for_agent(self) -> ContextEngine`), a class bound later in
the module, or a TYPE_CHECKING-only import now raises NameError on import: 300 modules on main
(gateway.config, run_agent, hermes_state, cron.jobs, the Telegram adapter, ...) fail to import
under 3.11, the update path crashes mid-`hermes update` on any install whose interpreter is
older than 3.14, and the e2e-upgrade suite's retry leg fails with
`NameError: name 'ContextEngine' is not defined`.

- Re-quote exactly the 79 annotations (36 files) that name something unbound when they are
  evaluated (including tui_gateway/server.py's `_TurnScopes`, which only reaches that module's
  globals through register() after import); no other text changes.
- agent/turn_scripted_prelude.py: `Generator[...]` gets its third argument back. d76cce4f11
  applied UP043 (default type args, valid only from 3.13) to a module-level alias, so
  `Generator[Y, S]` raises TypeError on 3.11/3.12 and takes gateway.run, agent.conversation_loop
  and tui_gateway.{server,ws,entry} down with it.
- ruff.strict.toml target-version = "py311", the requires-python floor. With it UP037 leaves
  these quotes alone and only strips the two that are genuinely redundant, which this commit
  strips; any future version-gated fix stays valid on every supported Python.
- tests/ci/test_strict_ruff_target.py pins the target to the requires-python floor (red on main).
2026-10-09 14:25:07 -07:00
Mariano Nicolini
3637c512fc docs(image_gen): state the fallback rule as no server accepted the request 2026-10-09 15:36:14 -04:00
Mariano Nicolini
4710e757ab fix(image_gen): fall back only when Krea provably created no job, and apply the Krea default on first-run setup too 2026-10-09 15:36:14 -04:00
Mariano Nicolini
a9ebde190a feat(image_gen): fall back to FAL when managed Krea refuses a request before creating a job 2026-10-09 15:36:14 -04:00
Mariano Nicolini
bcf216d461 feat(image_gen): describe Krea image inputs as style references so the model stops rewriting the style into the prompt 2026-10-09 15:36:14 -04:00
Maciej Lisiewski
2ff58849f4 ci: ruff UP037 fixes 2026-10-09 17:34:38 +02:00
Maciej Lisiewski
9eab688b42 ci: ruff UP017 fixes 2026-10-09 17:34:36 +02:00
Maciej Lisiewski
aff64c3eeb ci: UP007, UP022, UP028, UP031, UP041 linter fixes 2026-10-09 17:31:52 +02:00
Maciej Lisiewski
32b70e9733 ci: FURB188, FURB192 linter fixes 2026-10-09 17:31:52 +02:00
Teknium
a114e7a344 fix(memory): detect providers past the first 8 KB of __init__.py
The discovery heuristic only searched the first 8 KB of a plugin's
__init__.py for the provider contract, so a valid provider with a long
module docstring was invisible. Fall back to _defines_memory_provider
(AST parse, no import) when the cheap text search misses.
2026-10-09 02:33:35 -07:00
Teknium
71704ffb49 refactor: move Spotify out of core into the spotify catalog plugin
The spotify toolset (plugins/spotify) and its loopback-PKCE login (hermes_cli/auth_spotify.py)
now live in NousResearch/hermes-spotify, installed from the plugin catalog. Same pattern as the
Home Assistant move (fb9fc8a3dd): a LEFT_CORE row installs the plugin for every home that was
using it (a Spotify login in auth.json, or the spotify toolset in platform_toolsets) on
`hermes update` / first start; the toolset name, tool names, schemas, env vars and stored login
(providers.spotify) are unchanged, so nobody re-authenticates.

The one user-visible change is the login command: plugins cannot extend `hermes auth`, so the
plugin registers `hermes spotify login|status|logout`. LeftCoreFeature gains a `cli` field and
moved_command_hint() turns `hermes auth [status|logout] spotify`, and `hermes spotify` while the
plugin is not loaded, into a line naming the new command and the install/enable step for this
home. The dashboard's "needs login" hint names the new command too.

Core drops the Spotify auth constants/dispatch, the CONFIGURABLE_TOOLSETS row and post-setup
step, the core `spotify` entries in the shared-metrics toolset enums, its tests, and the docs
(en + zh-Hans) now point at `hermes plugins install spotify` / `hermes spotify login`.
2026-10-08 19:00:45 -07:00
ethernet
8ac5c74432 Harder linter fixes (#135281)
* ci: fix B004 linter rule

* ci: prefix unused variables with underscore (RUF059)

* ci: don't cast int to int

* ci: remove unnecessary if/else where both blocks yield same result

* ci: RUF024

* ci: fix RUF017, RUF018, RUF022

* ci: RUF013, RUF015

* ci: RUF007

* ci: RUF009

* ci: address code review
2026-10-08 18:56:50 -04:00
ethernet
51609d827b fix(solstice): load the transport on first use, not at discovery (#134233)
Provider discovery imports every bundled plugin's __init__, and it runs in
interpreters without the app's HTTP stack: PM's runtime calls
lazy_installs_allowed() -> hermes_cli.config, whose import-time
_inject_profile_env_vars() lists providers. solstice/__init__ imported
transport.py at the top, which pulls agent.gemini_native_adapter -> httpx,
so every install logged "Failed to load bundled provider plugin solstice:
No module named 'httpx'" and solstice went unregistered in that process.

INFERENCE_BASE_URL now lives in __init__ and SolsticeClient is imported
inside create_client, matching the other provider plugins.
2026-10-08 18:06:07 -04:00
Maciej Lisiewski
4278b6aa24 ci: RUF013, RUF015 2026-10-08 21:29:48 +02:00
Maciej Lisiewski
74f1f17a7e ci: fix RUF017, RUF018, RUF022 2026-10-08 19:54:07 +02:00
Maciej Lisiewski
b4e436c115 ci: don't cast int to int 2026-10-08 19:28:23 +02:00
Maciej Lisiewski
aa2d120cba ci: prefix unused variables with underscore (RUF059) 2026-10-08 19:22:03 +02:00
Maciej Lisiewski
d76cce4f11 ci: apply all automatic linter fixes available 2026-10-08 17:22:54 +02:00
Teknium
8a33891bdd revert: dashboard-auth token exchange breaks on gzip IdP responses (partial revert of #133938)
Reverts the plugins/dashboard_auth half of ca38caeea0.

_request_limited_response streamed the body via iter_bytes() (already
gzip-decoded by httpx) and rebuilt an httpx.Response with the ORIGINAL
headers, Content-Encoding: gzip included, so .json() tried to gunzip
plaintext. Every Nous Portal dashboard login failed with
503 "Provider unreachable: Portal token endpoint unreachable: Error -3
while decompressing data: incorrect header check". Self-hosted OIDC
discovery/token/revocation used the same helper.

The urllib half (gateway health probe, ElevenLabs voices) is kept:
urllib does not decode Content-Encoding, and c061068b26 builds on its
_read_dashboard_json_response helper.
2026-10-07 17:52:08 -07:00
Austin Pickett
9aaf2fda3c refactor(kanban): split board metadata/lifecycle into kanban_db_boards; fix ratchet findings
Move board-metadata and board-lifecycle functions (board_metadata_path,
read/write_board_metadata, _default_board_display_name, _dir_holds_board,
create_board, list_boards, remove_board) out of the kanban_db facade into
the topical sibling hermes_cli/kanban_db_boards.py, offsetting this PR's
facade growth past its main-pinned FILE_LINES cap (moved code keeps its
cap; re-exported from the facade tail so imports/monkeypatching are
unchanged).

Also drop the blind except Exception in the dashboard's _ws_board_live:
read_board_metadata never raises (missing/malformed board.json yields the
synthesized entry), so the archived-tombstone check needs no try/except.
2026-10-07 17:21:31 -04:00
Austin Pickett
1a85226e22 fix(kanban): dashboard rejects stale archived/deleted board slugs
_resolve_board 404s archived tombstones, and the /events WebSocket closes
stale-tab streams (1008) at the handshake instead of handing the dead slug
to connect(), which resurrected an empty board (#43243).
2026-10-07 17:21:31 -04:00
Teknium
bc246e1f8a fix(providers): LM Studio and TokenHub profiles declare their credential env vars
#134320 registered ProviderProfiles for lmstudio and tencent-tokenhub without env_vars. That put
both into the provider-catalog E2E matrix for the first time with key_env=None. The shard fixture
then died on keys[""] (KeyError, every row of the shard errored), TokenHub turns could not find a
key, and LM Studio's /model switch could not reach the native /api/v1/models listing the fake
never served.

The profiles now name the same env vars their hermes_cli/auth.py registry rows use (that row stays
the lookup source of truth). The catalog fake also answers LM Studio's native listing.
2026-10-07 13:21:46 +05:30
kshitijk4poor
b278d6528f fix(line): a non-string channel secret or token reads as unset
_credentials coerced every value with str(), so YAML channel_secret: true
keyed the HMAC with the guessable "True" (likewise 123, lists, maps).
Read a value only when it is a string, matching the webhook adapter's
_is_usable_secret policy; scoped-env values stay first. (Lead fold: Sol
r4/r5 + quality lens, converged; probed: true/0 -> "" on head.)
2026-10-07 13:19:35 +05:30
kshitijk4poor
d28c513904 fix(raft): a null or blank bridge token is unset, not a guessable inbound token
`bridge_token:` with an empty YAML value became str(None) == "None" (and a
whitespace-only value stayed "   "). Both are truthy, so connect() skipped
the auto-generated token and _authorized() accepted any wake/activity POST
carrying that header. Same class as the null-secret fixes for LINE and
WhatsApp Cloud in this stack.
2026-10-07 13:19:35 +05:30
kshitijk4poor
adf4a4b48c fix(sms): a non-string or blank standalone-send key fails closed instead of raising
PlatformConfig keeps a numeric YAML api_key as an int, so the .strip() this
stack added to _standalone_send raised AttributeError where origin/main
returned an error dict. Coerce through str(... or "") like the LINE reader,
and pin both the int key and a whitespace-only key (which also gives the
standalone strip a test that fails without it).
2026-10-07 13:19:35 +05:30
kshitijk4poor
ae083f8a22 fix(line): read the standalone-send token through _credentials and pin both strips
_standalone_send still read the access token raw, so a blank or newline-padded token went out as the Bearer header; reuse the hardened _credentials. Pin the token half of the null/blank guard and the env presence strip (both reverted green before). Gate r4 Lows.
2026-10-07 13:19:35 +05:30
kshitijk4poor
62df46fdc4 test(sms): pin the stripped Twilio token in the requirements check
Reverting only the check_sms_requirements strip left the suite green; also drop the dead 'or ""' after reads that already pass a default. Gate r3 Lows.
2026-10-07 13:19:35 +05:30
kshitijk4poor
c5caff330d fix(line): keep a null channel secret unset instead of keying HMAC with "None"
c92ed7e000 wrapped the extra fallback in str(), so a present-but-null config value (an empty YAML key) became the guessable literal "None" and connect passed. Fall through to "" before str(); strip the env presence check too so check_requirements agrees with connect. Gate r3 High (2ab, 2c, quality converge).
2026-10-07 13:19:35 +05:30
kshitijk4poor
84cb790e64 fix(sms): fail closed on a whitespace-only Twilio auth token
Gate r2 finding 2 (same class as the webhook blank-secret fix):
TWILIO_AUTH_TOKEN was read unstripped, so a whitespace-only token
passed check_sms_requirements and keyed the X-Twilio-Signature HMAC
with blanks, letting anyone forge inbound SMS. Strip the token at
every read (adapter init, standalone send, requirements check).

Stripping alone would leave an empty key, and HMAC with an empty key
is equally public (verified by probe: a signature forged with b''
was accepted). So _check_signature now refuses outright when no
auth token is set. This goes one line past the reviewer's FIX, which
only asked for the strip.
2026-10-07 13:19:35 +05:30
kshitijk4poor
a560ba0b08 fix(line): fail closed on a whitespace-only channel secret
Gate r2 finding 1 (same class as the webhook blank-secret fix):
_credentials returned LINE_CHANNEL_SECRET / extra['channel_secret']
unstripped, so a whitespace-only secret passed the `not channel_secret`
check at connect and in verify_line_signature, and the webhook HMAC
was keyed with blanks, making signatures forgeable. Strip both values
in _credentials (matching whatsapp_cloud) so a blank secret reads as
unset and connect refuses with config_missing.
2026-10-07 13:19:35 +05:30
kshitijk4poor
149f39fc9f fix(photon): log the sent-text lookup failure instead of swallowing it
The code-health ratchet (BLE001) rejects a blind except Exception that
neither re-raises nor logs the traceback. The record path already logs
at debug with exc_info; give the lookup path the same.
2026-10-07 13:18:15 +05:30
kshitijk4poor
cb71296ebd fix(photon): a threaded reply to our bubble stays "own" after a restart
Cloud spectrum targets carry no direction and _sent_message_ids is
in-memory, so after a gateway restart a swipe-reply to one of our
bubbles hydrated the quoted text from the sent-text index but was
marked as not ours: the agent saw [Replying to: "..."] instead of
[Replying to your previous message: "..."]. Only our sends are indexed,
so an index hit is itself proof the target was ours.
2026-10-07 13:18:15 +05:30
David Uche
aba1f72ef2 fix(photon): record sent text at the _post_send choke point
Polls (clarify), the plain-text fallback resend and standalone attachment
sends reached the sidecar without recording what they sent, so a threaded
reply to those bubbles lost its quoted context. Record once in _post_send
(plus the standalone attachment leg, which has its own HTTP client) and
reuse _normalize_chat_key instead of a duplicate regex.
2026-10-07 13:18:15 +05:30
David Uche
348a68bb4e fix(photon): keep the user's words when they reply in an iMessage thread (#100663)
spectrum-ts 12.x delivers a threaded (swipe) reply as
{type: 'reply', content, target}. The sidecar had no branch for it, so
the agent received '[Photon content type not handled: reply]' and the
message text was lost.

- sidecar: normalize 'reply' to its inner content plus the target's id,
  direction and text
- adapter: unwrap it and set reply_to_message_id / reply_to_text /
  reply_to_is_own_message on the MessageEvent
- adapter: record outbound text (send, attachments, standalone send) in
  gateway.rich_sent_store and use it when spectrum can't hydrate the
  quoted text, which is the usual case for our own bubbles. DM GUIDs
  and bare E.164 share one key.
2026-10-07 13:18:15 +05:30
Teknium
05eecbcd97 chore(memory): remove the bundled openviking provider
OpenViking now ships from Volcengine's own repo through the plugin catalog
(plugin-catalog/openviking.yaml, pinned in #131267). Delete the in-tree copy
and its tests; memory_provider_migration already lists openviking in
_LEFT_CORE, so homes configured for it get the catalog plugin installed on
hermes update / first agent start.

The catalog pin starts openviking-server with the full Hermes environment
(the bundled copy filtered it). The docs now say so and point at
volcengine/OpenViking#5553; start openviking-server yourself to avoid it.
2026-10-06 23:13:01 -07:00
kshitijk4poor
72a346e8ea refactor(providers): one copy each of TokenHub's effort rule and the generic nested reasoning fallback
Review fold (round 1, simplify reuse + quality):
- tokenhub_effort() in agent/reasoning_effort.py: the tencent-tokenhub profile and the host-based
  branch base_url-only agents take used the same "high unless chosen, else clamp" expression in
  two places, so the two paths could drift apart.
- generic_nested_reasoning(): the OpenAI-compatible extra_body fallback auxiliary calls send when a
  profile declares no reasoning shape. The lmstudio profile's auxiliary branch was a line-for-line
  copy (it must produce it itself because overriding build_api_kwargs_extras turns the aux
  fallback off). Both now call the helper.
- agent/lmstudio_reasoning.py docstring names its real caller (the lmstudio profile).

Behaviour unchanged: 42/42 main-loop request cells and every auxiliary projection cell are
byte-identical to origin/main.
2026-10-07 11:27:35 +05:30
kshitijk4poor
d0c7bb9432 fix(providers): review fold for the LM Studio / TokenHub profiles
- Plugin hooks written against the older build_api_kwargs_extras keywords (no **context) no
  longer raise TypeError on every request: lmstudio_reasoning_options is forwarded only when the
  agent probed it (LM Studio). Pinned by TestProfileHookKeywordCompat, which fails if the field is
  passed unconditionally.
- tencent-tokenhub: drop base_url and supports_model_listing=False. With them, first-time
  `hermes model` setup reached the profile path and offered only the 3 curated Hy models instead
  of probing {base}/models as main does; without a profile base_url, setup and the /model picker
  take the same paths they took with no profile.
- One name for LM Studio's probed options end to end (lmstudio_reasoning_options), matching the
  other provider-prefixed hook keys (ollama_num_ctx, qwen_session_metadata).
- New base-url:github-models-chat snapshot cell (openai/o4-mini, asserted chat_completions): the
  gpt-5.5 cell goes to Responses and never reached the legacy GitHub reasoning branch.
- Comment/docstring truth: the host branches serve agents with no registered profile (not only
  base_url-only ones); the LM Studio effort omission is done by the profile.
2026-10-07 11:27:35 +05:30
kshitijk4poor
4c84ff744e refactor(providers): LM Studio and Tencent TokenHub get provider profiles; base_url-only agents get snapshot cells
lmstudio and tencent-tokenhub were the last chat-completions providers with no ProviderProfile,
so their requests were built by the legacy flag branch in ChatCompletionsTransport.build_kwargs
(name check for LM Studio, host check for TokenHub). They now have profiles that reproduce the
same wire through build_api_kwargs_extras:

- lmstudio: top-level reasoning_effort from the model's published allowed_options. The agent's
  cached probe is passed to the hook as reasoning_options; auxiliary calls (no probe) keep the
  generic nested reasoning they already sent. base_url is left empty on purpose: a loopback
  base_url would map every 127.0.0.1 / localhost endpoint to LM Studio in URL-to-provider
  inference and turn off local-server detection for Ollama, llama.cpp and vLLM. Model listing
  and the doctor probe stay off so the picker keeps LM Studio's own chat-model probe.
- tencent-tokenhub: top-level reasoning_effort clamped to low/medium/high, omitted when thinking
  is off, high when no level is chosen (default_reasoning_config on main-loop turns). Model
  listing and the doctor probe stay off so the picker keeps the curated Hy list.

The name-based LM Studio branch is removed from the legacy path. The host-based branches stay:
an agent built from a base_url alone (run_agent.py main, the batch runner, library callers)
carries provider="" and still reaches the legacy path. 11 base_url-only snapshot cells now pin
those host-detected requests (OpenRouter, Nous, Kimi, Kimi coding, TokenHub, GitHub Models,
NVIDIA NIM, Qwen portal, a localhost LM Studio port, Ollama, a generic endpoint) so the change
that retires the legacy path can prove it sends the same requests.

Deliberate deltas: model ids prefixed lmstudio: / tencent-tokenhub: are now stripped like every
other profiled provider's; TokenHub auxiliary calls with a reasoning config send top-level
reasoning_effort (the main-loop shape) instead of nested extra_body.reasoning.
2026-10-07 11:27:35 +05:30
Ben Barclay
c950e2278d refactor(kanban): drop unenforced column traits from the workflow groundwork
Review of #130067: traits described kernel meaning nothing reads (and two
descriptions were incomplete: archived also satisfies a parent gate; the
kernel re-checks parents on ready/review). Same reasoning as the dropped
event model: Phase 0 keeps only data that is consumed or checked against
live code; column semantics are designed with the kernel switch.

Nits: VALID_STATUSES is the frozenset itself; precise tuple/frozenset types.
2026-10-07 11:19:10 +05:30
Ben Barclay
6fe5dfb1cf fix(kanban): drop the partial kernel-event model from the workflow groundwork
Review of #130067: the event edges claimed every block lands in 'blocked',
but _route_block routes to todo / triage / blocked, and ~20 kernel status
writes are conditional (crash/reclaim return, failure breaker, parent
reopen, import). Rather than publish a partial contract, Phase 0 keeps only
what is checked now (columns, traits, manual matrix, derived copies); the
event model is designed with the kernel switch in Phase 1.

Also from review:
- can_move rejects unknown column keys instead of defaulting (archive of a
  bogus key returned True).
- the matrix test only accepts 400/409 as a refusal; 5xx or 422 fails.
2026-10-07 11:19:10 +05:30
Ben Barclay
96db175da7 feat(kanban): one workflow definition for columns, traits and transitions
Groundwork for user-defined columns and workflow per board (#54818).

- hermes_cli/kanban_workflow.py: Column/Workflow + DEFAULT_WORKFLOW, today's
  board written down as data (column order, traits, default kernel-event
  edges, manual move allow-list). Not consulted by the kernel yet.
- VALID_STATUSES, dashboard BOARD_COLUMNS, the kanban_list status enum and
  CLI status icons now derive from it. Fixes drift: the tool enum lacked
  scheduled/review; CLI icons lacked triage/review.
- GET /workflow on the dashboard plugin API.
- Tests pin DEFAULT_WORKFLOW to real behavior: every default event edge vs.
  its kernel verb, and all 64 PATCH src->dst moves vs. can_move.
2026-10-07 11:19:10 +05:30
beardthelion
8e85a0fd8c fix(plugins/telegram): clamp negative inline-query offsets to page zero
Telegram echoes back the client-controlled inline_query.offset string on
every inline query. build_inline_results passed it through int() and
used it directly as a slice start, so a negative offset indexed the
catalog from the tail: small catalogs served their last few entries with
negative-prefixed result ids, and larger catalogs could return an empty
page with a misleading next_offset that skipped most of the catalog.

Clamp the parsed offset to >= 0. Malformed offsets still fall back to
page zero via the existing except path, and valid pagination is
unchanged.
2026-10-06 22:52:16 -05:00