Clone this repository
For self-hosted knots, clone URLs may differ based on your setup.
Hetzner rotated robotns2.second-ns.de's addresses on 2023-12-04;
the acl block was updated then but the remote/NOTIFY targets were
missed, so pushes to that slave were going to dead IPs.
The built-in `auto` permission mode uses a classifier to gate per-action
approvals — covers the routine-action auto-approve role GouvernAI was
filling. Drops --dangerously-skip-permissions (no longer needed since
the mode handles approvals) and the GouvernAI plugin + fetchFromGitHub.
Sets permissions.defaultMode = "auto" in the wrapper's --settings so
every session boots into auto mode without a CLI flag.
Adds projectStateWipeKeys (default [ "mcpServers" ]). preHook rewrites
~/.claude.json via jq on every invocation, deleting the listed keys
from each projects.<dir> entry. Purges stale `claude mcp add`
registrations that pin GC-able /nix/store paths and shadow the
canonical set the wrapper writes via --settings. Empty list disables
the step.
GouvernAI's hook returns allow/deny/ask_user decisions, but Claude Code's
native permission prompts still fire on top, so safe T1 actions get a
duplicate prompt. With native prompts off, the guardrails hook becomes the
sole gate — silent on T1, notify on T2, ask on T3/T4, hard-block on
NEVER rules.
- new skills/tmux documents the claude- session prefix, run-and-wait
marker pattern, output capture, and parallel sessions
- drops the tmux-mcp MCP server and the 5pkgs/tmux-mcp wrapper package
- adds python3 + tmux to the wrapper's extraPackages so plugin hooks
resolve a real python3 inside Nix dev shells where DEVELOPER_DIR
points at a Nix Apple SDK (otherwise /usr/bin/python3 -> xcrun ->
"tool 'python3' not found")
Make every bot response a thread reply instead of a fresh top-level
message. send_notice gains a thread_root kwarg that adds m.relates_to
with rel_type=m.thread plus the m.in_reply_to fallback. on_message
picks the thread root via thread_root_for(): if the user's message is
already inside a thread we reply into that one, otherwise we start a
new thread rooted on their message.
For Radarr/Sonarr download notifications to thread back to the
original request, tracked_requests.json now stores the request's
event_id alongside the room. load_tracked normalizes legacy bare-
string entries so existing tracked items keep working (just without
threading on the eventual notification).
Three problems were stacked on top of each other:
1. matrix-nio's client.join doesn't expose ?server_name=, so when our
homeserver hadn't fully processed the federation invite by the time
we turned around to join, it returned M_FORBIDDEN with no retry.
2. Even with retry, on_invite only fires once per sync delivery; if
joining failed we never tried again.
3. client.invited_rooms is populated incrementally from the persisted
next_batch, so synapse won't re-send invites that arrived in a prior
session, which means the startup loop was a no-op for stuck invites.
Add a try_join helper that calls /join/{room_id}?server_name=<inviter>
directly via _send and retries with exponential backoff, and a
fetch_pending_invites helper that does a fresh /sync (no since token)
to ask the server for the real invite set. Use both in on_invite and
in the startup loop.
MegolmEvent has no `.message` attribute (the real field is `.ciphertext`),
so the on_megolm callback added in 54d7388 raised AttributeError on the
first undecryptable event and took the bot down. Drop the field from the
log line — sender + session_id are enough.
Also re-raise after logging in the sync_forever wrapper so the process
exits non-zero and systemd's Restart=on-failure actually triggers; the
swallowed exception was why the bot stayed dead for 5 days instead of
restarting.