plugins_cmd_install.recorded_install wraps the install attempt in every
plugin install entry point: `hermes plugins install` (catalog name, git
URL or shorthand), the dashboard/TUI/connector install
(dashboard_install_plugin) and plugin packs. Each emits one
hermes.extension.install.count event:
- source: catalog for a catalog entry (name = entry name), local for a
file:// repo, url for any other git source (name None)
- outcome: failed when the clone/scan/publish raises, success otherwise
A reinstall over an already-installed plugin and `plugins update` (which
calls the installer core directly) are not counted.
cmd_install printed each known issue on its own line right after
entry_capability_summary(), which already ends with "Known issues: ...",
so the text appeared twice on the CLI. Keep the summary line only.
dashboard_install_plugin gets the same list through a 6-line
_known_issue_warnings(entry) helper, so neither function's cyclomatic
complexity grows past the main baseline (cmd_install 20, dashboard 11).
Follow-up to #124058 (happy5318); no behaviour change for the dashboard
result dict or the memory_provider_migration path.
Relaxes the fail-closed install gate from #124037 per teknium1 review
2026-09-27 (known_issues informational; guard belongs at mode-selection seam):
- dashboard_install_plugin: no longer refuses known_issues entries — the text
flows into the result warnings plus a machine-readable "known_issues" key so
the UI can show it; the memory-provider migration paths (migrate_all_homes /
recover_at_startup via _install_into) install hindsight again in any mode.
- cmd_install: prints the yellow "Known issue:" lines but drops the TTY
requirement and the y/N confirmation; non-interactive installs proceed.
- entry_capability_summary: includes "Known issues: ..." so install prompts and
the catalog UI surface the text.
- scripts/validate_plugin_catalog.py: register known_issues in KNOWN_KEYS.
- tests: keep parse round-trip + install-summary display; drop the live-catalog
prose pin (test_live_catalog_hindsight_declares_known_issues) and the
zoneinfo import hack.
- contributors: map 5318happy@users.noreply.github.com -> happy5318.
## Thinking Path
`plugin-catalog/hindsight.yaml` already documented in prose that
`local_embedded` mode is unsupported on PM-managed Hermes with the current
pin — it still calls the retired lazy-install path for `hindsight-all` and
loops update takeover on every conversation — yet `hermes plugins install`
and the dashboard install path accepted the entry with no gate at all. Users
only discovered the trap after the death loop started. The catalog knew;
the installers didn't act on it.
## What Changed
- `hermes_cli/plugin_catalog.py`: new `known_issues: List[str]` field on
`PluginCatalogEntry` (parsed from `known_issues` in each `plugin-catalog/*.yaml`,
serialized into `to_dict()`, defaults to empty for backward compatibility).
- `plugin-catalog/hindsight.yaml`: declares the documented local-embedded trap
as machine-readable `known_issues`.
- `hermes_cli/plugins_cmd_install.py`:
- `cmd_install`: for catalog entries with `known_issues`, prints each issue
and a bold-red notice, then requires explicit confirmation. Non-interactive
(non-TTY) runs fail closed; interactive `y/N` runs require `y`.
- `dashboard_install_plugin`: non-interactive path refuses the entry outright
(no GUI bypass, mirroring the existing kill-list posture) and returns the
issue list in the error payload.
## Why This Shape
The trap is machine-readable in the catalog, so the enforcement lives in the
install entry points rather than in hindsight-specific code — any future
catalog entry that documents a known trap gets the same gate for free. The
non-interactive path fails closed because it cannot ask for the confirmation
the interactive path requires.
## Verification
- RED/GREEN double proof via git stash: pre-fix 7 failed / 3 passed, post-fix
10/10 passed (catalog parsing round-trip, hindsight.yaml declares the trap,
non-TTY refuse, TTY yes proceeds, TTY no cancels, custom-source install not
gated, dashboard refuse + unaffected path).
- Adjacent suites: test_plugin_catalog.py, test_plugin_validate.py,
test_plugins_hub_live_catalog.py all green (36 passed). 9 errors in
test_plugins_cmd_catalog.py reproduce identically with the fix stashed
(existing local `pm` / uv-sync environment limitation, unrelated).
- ruff clean on all changed files.
## Contract Change
New optional catalog field `known_issues` (list of strings). Entries without
it behave exactly as before. Install behavior changes only for entries that
declare `known_issues`.
## Risks
- Catalog entries that declare `known_issues` become gated installs. Authors
of future entries should only declare issues they want surfaced — this is
the intended trade (a documented trap must not install silently).
- `cmd_install` non-TTY automation installing hindsight by name now fails.
That is intentional: the trap cannot be confirmed non-interactively.
## Model Used
jd-deepseek-v4-flash-0731 (main session); pytest + ruff in the isolated
worktree.
## Related
#124037 (issue). Related: #122326 (resulting takeover loop), #7718
(`local_embedded` needs `hindsight-all`).
64860c9c20 pasted the same try/except around the security scan in
_install_plugin_core and update_plugin, and plugins_transaction reached
into plugins_cmd_install for the private _preserved_files_note.
_scan_merged_tree now lives next to _scan_plugin_tree/PluginScanBlocked
in plugins_cmd and both sites call it once. Message, scan_result and the
chained cause are unchanged (checked for matched, unmatched, empty and
missing-scan_result cases). _preserved_files_note is typed
(PluginScanBlocked, list[str]) and drops the nested getattr/str() guards
for the module's usual `scan_result.findings if scan_result is not None`
form.
_install_plugin_core ran the security scan and the portable-package check
on the pristine clone, then ran both again after before_swap had merged in
user files. The second pass existed only because file-count/size limits
apply to the merged tree. before_swap needs only the manifest and the
staged tree, and both exist before the first scan. It now runs there, and
one scan/portable check admits the final bytes. Subdir updates scan once
(probe: 2 scans -> 1, and that scan sees the carried files).
A dangerous finding in carried user data (a cached page, a notes file)
blocked the update with a report that read as if the pristine upstream
revision were malicious. Carry callbacks now return the paths they
preserved. When a scan blocks, the message names the findings that sit in
those files, or, when none of the findings can be matched to them, notes
that the tree included preserved user files. Both the no-git reclone path
and update_plugin's catalog/git carry do this.
plugins_cmd.py had grown to 2,858 lines, past the ~2,000-line gate. Move
each verb family into a plugins_cmd_<topic>.py sibling: git (install
metadata + git plumbing), install, update (plus adopt / trust-update-url /
check-updates), remove, capabilities, toggle (composite UI) and listing.
The facade keeps the shared primitives, enable/disable selection,
discovery and the dispatch table, and re-exports the names other modules,
tests and the old-updater surface import (978 lines now).
Siblings never import the facade at module level; they read facade names
through _pc() at call time, so monkeypatching plugins_cmd.<name> still
intercepts calls made from a sibling. No behaviour change. Tests that
imported three sibling-only helpers now import them from the defining
module, and two subprocess.run patches target subprocess directly.