feat(memory,skills): approve/deny gate for memory + skill writes (#38199)

Adds memory.write_mode and skills.write_mode (on|off|approve), applied to
both foreground turns and the background self-improvement review fork — the
source of the unprompted 'wrong assumption' saves users reported.

- on (default): write freely, unchanged behaviour
- off: never write; the tool returns a clean disabled result
- approve: don't commit. Memory foreground writes prompt inline (small,
  reviewable in a chat bubble); background memory writes and ALL skill writes
  stage to a pending store instead (a SKILL.md is too large to review inline,
  and a daemon thread can't block on a prompt)

Review staged writes from CLI or any messaging platform:
  /memory pending|approve|reject|mode
  /skills pending|approve|reject|diff|mode

Skill review respects the size asymmetry: inline you see a one-line gist;
the full unified diff stays out-of-band (/skills diff, dashboard, or the
staged JSON file).

New: tools/write_approval.py (gate + pending store), hermes_cli/
write_approval_commands.py (shared CLI+gateway handlers). Gates wired at the
single entry points memory_tool() and skill_manage(), using the existing
write-origin ContextVar to distinguish foreground from background_review.
This commit is contained in:
Teknium
2026-06-09 21:51:43 -07:00
committed by GitHub
parent fdc90346ea
commit 96af61b6ef
13 changed files with 1270 additions and 11 deletions
+12 -8
View File
@@ -336,19 +336,23 @@ class TestSlackNativeSlashes:
)
def test_includes_aliases_as_first_class_slashes(self):
"""Aliases (/btw, /bg, /reset) must be registered as standalone
"""Aliases (/btw, /bg, /reset, …) must be registered as standalone
slashes — this is the whole point of native-slashes parity.
Note: Slack's manifest hard-caps slash commands at 50
(``_SLACK_MAX_SLASH_COMMANDS``). Canonical names win slots first,
then aliases, so the lowest-priority aliases can be clamped off
once the registry fills the cap (e.g. ``/q`` once ``/version``
landed). The surviving aliases below still prove alias parity;
anything dropped remains reachable via ``/hermes <command>``."""
names = {n for n, _d, _h in slack_native_slashes()}
Asserts the contract (aliases are surfaced as first-class slashes),
not a specific alias's survival of Slack's 50-slash clamp — which alias
lands last shifts whenever a canonical command is added, so pinning one
name (previously ``q``) made this a change-detector.
"""
slashes = slack_native_slashes()
names = {n for n, _d, _h in slashes}
# Aliases that sort early in the registry always fit under the cap.
assert "btw" in names
assert "bg" in names
assert "reset" in names
# And at least one alias is surfaced as an alias entry (description
# carries the "Alias for /…" marker), proving the alias pass ran.
assert any(d.startswith("Alias for /") for _n, d, _h in slashes)
def test_telegram_parity(self):
"""Every Telegram bot command must be registerable on Slack too.