To restore this context in a new session, paste this entire file content.
=========================================================================

# iSida-NG Project Context (v1.01)

## Project Overview
iSida-NG is a Python 3 fork of the iSida Jabber (XMPP) bot by diSabler, maintained by Luciferus. A "bot-as-library" XMPP chatroom bot: runs in MUC conferences, listens for `.` prefixed commands, and dispatches them through a per-plugin command table subject to access-level gating. Supports SQLite3 / MySQL / PostgreSQL backends; bundled schema + live DB at `data/sqlite3.db`.

## Architecture
- `isida.py` — entry point. Writes PID/ver, detects VCS (git/svn), loops `execfile('kernel.py')` with hot-update support (soft update / restart / exit).
- `kernel.py` — bot core: DB abstraction (`cur_execute*`, sqlite/mysql/pgsql with `%s`→`?` shim, `ilike`→`like`, reserved-word quoting), XMPP session (`xmpp` = xmpppy lib in `lib/xmpp`), command parsing/dispatch (`com_parser` → `comms`), message/presence/IQ callbacks, MUC helpers, `send_msg`/`sender`/`thr`, config via `get_config/put_config`, localization via `L()`/`GT()`.
- `settings/config.py` — runtime config: bot JID/nick/password, `base_type`, server, paths, debug flags. `Settings`, `SuperAdmin`, `prefix='.'`, `msg_limit`, `base_name/user/host/pass/port`, `loc_folder`, `sqlite_base='data/sqlite3.db'`.
- `settings/demo_config.py` — template/reference config.
- `plugins/main.py` — always-loaded core: access levels (0..9; 9 = bot owner), `levl`/`unlevl` maps, `get_level()`, `levl`/`unlevl`, `comm_on_off`, aliases, `.help`/`.commands`/`.access`, uptime, `.prefix`, `.owner`, `.plugin`/`.load-plugin`/`.unload-plugin`, bot join/leave/rejoin, show_syslogs, vcs_info. Also defines `main.py`'s own command list (registered as plugin `main`).
- `plugins/ai.py` — always-loaded core: AI chat responses, karma-style answers bank, drink/holiday init, active-message hooks (`gactmessage`).
- Plugin loader (kernel.py ~1773 / reload-loop ~1966): for each `plugins/*.py` not in ignore list, `execfile()`s into the kernel global namespace, then folds each tuple of its `execute` list into the global `comms` list. Plugins therefore SHARE the kernel namespace (access module-level globals like `xmpp`, `cur_execute`, `send_msg`, `Settings`, `get_level`, `get_id`, etc.).

### Plugin registration contract
Each (non-core) plugin defines, in the shared kernel namespace, a list named `execute` or `cmd`:
`execute = [(access_level, command_name, handler_func, arg_mode, description, ...), ...]`
- `access_level` int: minimum level to run (0..9). 9 = owner. -1 = ignored, -2 = unknown.
- `arg_mode` (index 3): falsy/0 → handler called as `(type, room, nick, arg_list)`; 1 → `(type, room, nick)`; 2 → `(type, room, nick, raw_text_after_cmd)`.
- On hot-reload, kernel appends `'plugin\r' + desc` as index 4 (plugin-prefixed descriptor) and concatenates `cm[5:]`.
- Core hook lists also recognized: `presence_control`, `message_control`, `message_act_control` (`gactmessage`), `iq_control`, `iq_hook`, `timer` (`gtimer`). Plugins not declaring `execute` (e.g. `mucfilter.py`, `execute.py`, `shell.py`) register hooks only.

### Dispatch
`messageCB` → `com_parser(access_mode, nowname, type, room, nick, text, jid)` iterates `comms`: runs a command only if `access_mode >= cmd_level` and `nick != nowname`; blocks DDOS repeats, honors `commonoff` per-room/per-command disables. First match wins.

## Access Level Legend (0..9)
9 bot owner | 8 moderator+owner | 7 moderator/admin | 6 moderator+member | 5 moderator/none | 4 participant/member | 3 participant/none | 2 visitor/member | 1 visitor/none | 0 minimal (everyone). `-1` ignored; `-2` unknown.

## Plugins Status
Status = `works` where `py_compile` succeeds (all plugins below compile). ⚠️ = relies on external network/service/binary or has caveats.

| Plugin | Status | Access min | Notes |
|---|---|---|---|
| acl | works | 7 | ACL action logging (msg/presence/role/aff/jid/age/ver...) |
| adminmail | works | 8 | owner mail; ext. `os,re,subprocess,time,urllib` |
| age | works | 3 | jid age/seen/when stats |
| ai | works | — | core (always load) — AI chat + answers bank, no user cmds |
| anek | works | 3 | anekdot.ru; ext. `json,random,re` ⚠️ net |
| answers | works | 9 | import/export answers base |
| antitroll | works | 9 | private repeat |
| backup | works | 8 | conf backup/restore |
| bash | works | 3 | башорг/ibash; ext. `html,json,random,re,urllib` ⚠️ net |
| blacklist | works | 9 | room blacklist manage |
| bomb | works | 4 | bomb joke |
| bugmenot | works | 3 | bugmenot.com; ext. `base64,html,re,urllib` ⚠️ net |
| calendar | works | 3 | calendar |
| chat | works | — | core hooks, no commands |
| clear | works | 7 | hidden history clean |
| clients | works | 4 | client/version stats |
| convert | works | 3 | currency; ext. `re,time,urllib,xml` ⚠️ net |
| cron | works | 7 | cron scheduler |
| disco | works | 3 | service discovery; ext. net ⚠️ |
| dist | works | 3 | distances; ext. `json,math,re,urllib` ⚠️ net |
| domain-ng | works | 6 | dnsrecon SRV/TXT; ext. `os,re,subprocess` ⚠️ needs dnsrecon |
| drink | works | 3 | holidays; ext. `urllib` ⚠️ net (cache: hash_rainbow.json) |
| dumpz | works | 3 | paste bin; ext. net ⚠️ |
| execute | works | — | core hooks |
| flibusta | works | 3 | books; ext. `urllib,xml` ⚠️ net |
| fun | works | 0/3/9 | poem/oracle/coin/poke/life/zalgo/godville |
| geoip | works | 3 | geo lookup; ext. `json,socket,urllib` ⚠️ net |
| gis | works | 3 | gismeteo weather ⚠️ net (scraping) |
| here | works | 3 | participant list |
| hideroom | works | 9 | hide conf |
| horo | works | 3 | horoscope |
| idle | works | 0 | idle time |
| inlist | works | 7/8 | ban/member/admin/owner lists |
| invite | works | 4 | invite |
| iq | works | 3 | ver/ping/time/vcard/uptime/stats/iq_*; ext. `base64,datetime,re,sys,time` |
| issue | works | 3 | issues tracker |
| istrue | works | 3 | truth eval |
| jc | works | 3 | jc.jabber.ru |
| jidcatcher | works | 2/4/6/9 | res/serv/search/top |
| juick | works | 3/9 | juick.com miniblog |
| karma | works | 3 | karma |
| known | works | 3 | nick changes |
| logger | works | 9 | history logging |
| mac | works | 3 | MAC vendor; ext. `os,re,subprocess,urllib` ⚠️ net |
| main | works | — | core — always load (help/commands/owner/plugin/alias/uptime) |
| md5 | works | 3/4 | md5/sha1/sha256 + rainbow; ext. `hashlib,json,os` |
| muc | works | 7/8 | ban/kick/role/affil/tban/global_ban MUC admin |
| mucfilter | works | — | MUC filtering hooks (muc_lock table) |
| net | works | 3/4/6 | nslookup/host/dig/port/net_ping/dns/tld; ext. `os,re,socket,subprocess` |
| oboobs | works | 3 | oboobs.ru images ⚠️ net |
| paste | works | 4/5 | paste service |
| phone | works | 3 | DEF/city phone lookup |
| psto | works | 3/9 | psto.net miniblog ⚠️ net |
| qr | works | 3 | QR code; ext. `datetime,hashlib,os,re,subprocess` |
| quotes | works | 3 | skio.ru quotes; ext. `json,random,re,time` ⚠️ net |
| redirect | works | 3 | private/public output redirect |
| rss | works | 4 | RSS/ATOM search/feed; ext. `re,urllib,xml` ⚠️ net |
| say | works | 6/9/7 | say/psay/gsay/topic |
| sayto | works | 3/7 | sayto/sayjid/memo/auto_sayto*; ext. `re,time` |
| setnick | works | 8 | change bot nick |
| shell | works | — | shell_execute hooks (execfile-style) |
| shutdown | works | 9 | quit/restart/update/soft_update |
| sokr | works | 3 | abbreviations |
| sovm | works | 3 | zodiac compatibility |
| spellcheck | works | 0 | spell checker |
| spy | works | 9 | conference activity monitor |
| talkers | works | 3/4 | local/global talkers |
| test | works | 0 | bot activity ping |
| th | works | 7 | threads stat |
| todate | works | 3 | days-until calc |
| train | works | 3 | Yandex train schedule ⚠️ net |
| transhell | works | 3 | translate-shell; ext. `os,subprocess` ⚠️ needs `tr` binary |
| turn | works | 3/9 | keyboard layout fix + `.turn` + chat autoturn; ext. `os,re`; cache `data/ignore_turn.txt`, `data/rugramm.txt` |
| vcs | works | 3 | git/svn log; ext. `json,os,re,subprocess,urllib` ⚠️ net |
| watcher | works | 6/9 | connection watcher/activity |
| whowas | works | 3 | last-day users |
| wtf | works | 0/3/9 | definitions base (wtf/wtfrand/wtfnames/wtfsearch/wtfall/wwtf/wtff/wtfp/dfn/gdfn + helpers) |
| wttr | works | 3/9 | wttr.in weather; ext. `os,re,time,urllib` ⚠️ net; debug log `data/wttr_debug.log` |
| www | works | 3 | web fetch/header/isdown; ext. `re,socket,time,urllib` ⚠️ net |
| wz | works | 3 | WZ weather by airport code |

Core/always-load (not user-command plugins): `main.py`, `ai.py`, `mucfilter.py`, `execute.py`, `shell.py`, `chat.py`.

## Commands Reference
(command `L<min_access_level> | name | description`; `arg_mode` shown as 0/1/2). Full list below; grouped by plugin.

**acl (L7)** — `acl`, `acl show|del|clear` ... actions. arg_mode 2.
**adminmail (L4/L8)** — `msgtoadmin text`, `devmail text`, `devmail_help_ru`, `devmail_help_en`.
**age (L3/L7)** — `age`, `age_split`, `seen`, `seen_split`, `agestat`, `seenjid`, `seenjid_split`, `when`, `whenstat`.
**anek (L3)** — `anek`.
**answers (L9)** — `answers`.
**antitroll (L9)** — `troll nick / [text] / [number]`.
**backup (L8)** — `backup show|now|restore`.
**bash (L3)** — `bash`, `ibash`.
**blacklist (L9)** — `blacklist add|del|show|clear`.
**bomb (L4)** — `bomb`.
**bugmenot (L3)** — `bugmenot`.
**calendar (L3)** — `calendar`.
**clear (L7)** — `clear`.
**clients (L4)** — `clients [global|short|os|user <nick>|filter]`, `clients_help`.
**convert (L3)** — `convert`.
**cron (L7)** — `cron`.
**disco (L3/L4/L7)** — `disco server.tld`, `disco_raw server.tld`, `features`, `whereis`.
**dist (L3)** — `dist`, `city`.
**domain-ng (L6)** — `dnsrecon -srv`, `dnsrecon -txt`.
**drink (L3)** — `drink [name|date]`, `calend [name|date]`.
**dumpz (L3)** — `dumpz`.
**flibusta (L3)** — `flibusta`.
**fun (L0/L3/L9)** — `poem`, `oracle`, `coin`, `poke nick`, `life dd.mm.yy`, `zalgo`, `godville`.
**geoip (L3)** — `geoip` (host/ip).
**gis (L3)** — `gis`, `gis+`.
**here (L3)** — `here`.
**hideroom (L9)** — `hide add|del|show room@conf`.
**horo (L3)** — `horo list|date|<sign>`.
**idle (L0)** — `idle`.
**inlist (L7/L8)** — `inban`, `inmember`, `inadmin`, `inowner`, `reban`.
**invite (L4)** — `invite nick|jid / [reason]`.
**iq (L3)** — `ver`, `ver+`, `caps`, `ping_old`, `ping`, `time_old`, `time`, `time_old_raw`, `time_raw`, `vcard_raw`, `uptime`, `stats server`, `iq_ver server`, `iq_mam server`, `iq_reg server`, `iq_car server`, `iq_srv server`.
**issue (L3)** — `issue show|pending|accept|reject|delete|done id`.
**istrue (L3)** — `true`, `valid`.
**jc (L3)** — `jc`.
**jidcatcher (L2/L4/L6/L9)** — `top`, `res`, `serv`, `search`.
**juick (L3/L9)** — `juick`, `juick_post`.
**karma (L3)** — `karma show|top+/top-|nick`.
**known (L3)** — `known nick / [count|all]`.
**logger (L9)** — `log add|del|show [room@conf]`.
**mac (L3)** — `mac MAC`.
**md5 (L3/L4)** — `hash_help`, `md5/sha1/sha256 text`, `*_save text`, `*_reverse hash`, `hash <algo> text`, `hash_reverse <algo> hash`.
**muc (L7/L8)** — `ban_past`, `ban`, `banjid`, `unban`, `unbanjid`, `tban show|del [jid]`, `none_past`, `none`, `nonejid`, `member_past`, `member`, `memberjid`, `kick`, `participant`, `visitor`, `moderator`, `global_ban` (L8, owner-only). arg_mode 2 with optional reason.
**net (L3/L4/L6)** — `nslookup`, `host`, `dig`, `port`, `net_ping`, `dns`, `tld`.
**oboobs (L3)** — `oboobs`, `obutts`.
**paste (L4/L5)** — `paste`, `ppaste`.
**phone (L3)** — `phone`.
**psto (L3/L9)** — `psto`, `psto_post`.
**qr (L3)** — `qr`, `qr help`.
**quotes (L3)** — `quote`, `afor`.
**redirect (L3)** — `private`, `public`.
**rss (L4)** — `rss_search <url>`.
**say (L6/L7/L9)** — `say`, `psay`, `gsay`, `topic`.
**sayto (L3/L7)** — `sayto jid|nick message`, `sayjid jid message`, `memo`, `auto_sayto on|off`, `auto_sayto_timeout 30-180`, `auto_sayto_max_messages 1-20`, `auto_sayto_max_length 256-2048`.
**setnick (L8)** — `setnick`.
**shutdown (L9)** — `quit`, `restart`, `update`, `soft_update`.
**sokr (L3)** — `sokr`.
**sovm (L3)** — `sovm`.
**spellcheck (L0)** — `spell [uk|ru|en] text`.
**spy (L9)** — `spy add|del|show conference`.
**talkers (L3/L4)** — `talkers`, `gtalkers`.
**test (L0)** — `test`.
**th (L7)** — `th`.
**todate (L3)** — `to_date`, `todate`.
**train (L3)** — `train`.
**transhell (L3)** — `tr`, `transhell`.
**turn (L3/L9)** — `turn <text>`, `turn_reload`, `turn_ignore add|del word`.
**vcs (L3)** — `vcs`, `svn`.
**watcher (L6/L9)** — `watcher`, `activity`.
**whowas (L3)** — `whowas`.
**wtf (L0/L3/L9)** — `wtfrand`, `wtfnames`, `wtfcount`, `wtfsearch`, `wtfall`, `wwtf`, `wtff`, `wtfp word/[nick]`, `wtf`, `dfn word=definition`, `gdfn word=definition` (+ helpers).
**wttr (L3/L9)** — `wttr <city>`, `wttr_help`, `wttr_debug`.
**www (L3)** — `www [count] regexp / url`, `header`, `isdown`.
**wz (L3)** — `wzz`, `wzs`, `wz`, `wzsearch`.

Kernel/core-provided commands (in `main.py`): `help/команды/access/prefix/owner/plugin/` + uptime/owner.

## Database Schema
Backend selected by `base_type` in `settings/config.py`. Default: **sqlite3**, file `data/sqlite3.db`. Postgres/MySQL equivalents in `scripts/*.schema`. Tables (purpose):

- `age` — per-conference presence age/history (join/leave time, status, message). PK-ish (room,jid,nick).
- `jid` — parsed jid login/server/resource index.
- `talkers` — local talkers stats (room,jid,nick,words,frases).
- `wtf` — definitions base (ind,room,jid,nick,wtfword,wtftext,time,lim).
- `answer` — random flood answer phrases.
- `sayto` — saved sayto messages.
- `dist` + `dist_user` — distance/db points for Dist plugin.
- `karma` + `karma_commiters` + `karma_limits` — karma engine + voter throttling.
- `acl` — per-jid/per-command action log (msg/presence/role/aff/.../ver/vcard) with access level + expiry time.
- `gis` — Gismeteo city codes.
- `def_ru_stat` / `def_ua_stat` / `def_ru_mobile` — DEF-code phone regions.
- `wz` — WZ weather airport codes.
- `muc_lock` — MUC-filter lock list (jid per room).
- `versions` — client version/caps tracking.
- `issues` — ticket system.
- `cron` — scheduled commands.
- `config_conf` — per-room config options.
- `config_owner` — owner-level config (unique option).
- `top` — command usage top counter (room unique).
- `alias` — per-room + global (`room='*'`) command aliases.
- `blacklist` — rooms bot must not join (room unique).
- `commonoff` — per-room disabled commands.
- `conference` — joined rooms + passwords.
- `feed` — RSS/ATOM feeds to poll.
- `hiden_rooms` — hidden rooms.
- `bot_ignore` — ignore patterns (jid ilike).
- `bot_owner` — owner jids.
- `log_rooms` — rooms with logging enabled.
- `spy` — conference activity stats.
- `tmp_ban` — temporary bans (room,jid,expire_time) → enforced by `muc.py`.
- `saytoowner` — rate-limit for owner messages.
- `ignore_ban` — rooms excluded from global ban.
- `first_join` — first-join timestamps.
- `url` — URL title store (room,jid,nick,time,url,title).

SQLite note (`cur_execute_sqlite3`): `%s`→`?`, `ilike`→`like`, reserved words backticked, custom `split_part`/ `row_number` funcs attached when SQL contains them. MySQL/PG paths differ only in the `%s`→`?` shim.

## Known Issues / TODOs / Bugs
- `data/url/index.py` exists but not indexed by schema scan (URL title-indexer helper) — review for PG/SQLite parity.
- `plugins/ext/` holds stale `.bak`/`.bac` files (dnsrecon.py.bak, domain.py.bak, turn.py.bak, ai-ng.py.bak, etc.) — not auto-loaded; candidates for cleanup.
- External binaries required by plugins (not present in repo): `dnsrecon` (domain-ng), `transhell`=`tr` (transhell), `translate-shell`, possibly `dig`/`host`/`nslookup` (net), `git`/`svn` (vcs). Will fail/no-op if missing.
- Network-dependent plugins (drink, anek, bash, convert, dist, flibusta, gis, geoip, mac, psto, quotes, rss, train, vcs, wttr, www, oboobs) require outbound HTTP — no auth configured in repo; rely on `urllib`.
- `config.py` contains a live bot JID/password (`isida@ubunix.pro/...`, password string). Should not be committed; use `demo_config.py`.
- `shell.py` has no `execute` list (shell_execute uses `subprocess.run` w/ 15s timeout, `paranoia_mode` gate) — verify exposure/legit.
- `muc.py` previously had a paren bug in the `muc_affiliation_past`/`muc_affiliation`/`muc_role` IQ `Node(...)` one-liners (unmatched `)`); fixed by binding reason list to a local `rnode` before the `Node(...)` call. Re-check if these one-liners are refactored again.
- `hash_rainbow.json` + `data/ignore_turn.txt` + `data/rugramm.txt` are runtime-generated caches (do not commit).
- Repo-root `sqlite3.db` (Aug 14) is a stale/snapshot copy; live DB is `data/sqlite3.db` (Aug 16). Avoid editing the root copy.
- `isida.py` line 98/136: `git log --pretty=format:''` writes empty-ish; version derived from `getsize`. Cosmetic.

## Core Functions (kernel.py + main.py, most-used)
- `cur_execute[_fetchone|fetchall][_sqlite3|_mysql]` — DB exec; auto-commits DDL/DML; returns cursor/rows.
- `get_config(room,item)` / `get_config_int` / `put_config` / `put_session_config` — per-room and global config via `config_conf` / `config_owner`.
- `GT(item)` / `PT(item,value)` — get/set global typed config (from `config_owner`).
- `get_level(jid,nick)` — returns `(access_mode, realjid)` using `megabase`; owner→9 via `is_owner`; -1 if in `bot_ignore`.
- `get_level(jid,nick)` is the canonical access check (0..9 / -1 / -2).
- `com_parser(access_mode, nowname, type, room, nick, text, jid)` — matches `text` against `comms` (prefix `.`, level gate, DDOS/commonoff), dispatches via `thr`.
- `send_msg(mtype, mjid, mnick, mmessage)` — send message (chat/group), length-limited + multi-part.
- `sender(item)` — send a raw XMPP stanza.
- `thr(func, params, name)` — run a function in a named thread.
- `L(*par)` / `L(par,...)` — localization wrapper; strings in `data/locales/{ru|ua}.txt`.
- `get_id()` — unique stanza id.
- `getRoom(jid)` / `getServer(jid)` / `getResourse(jid)` / `getName(jid)` — JID field splitters.
- `get_jid_by_nick(room,nick)` / `get_nick_by_jid(room,jid)` — roster resolution.
- `get_xnick(jid)` / `get_xtype(jid)` — bot's nick/type in a room.
- `join(conference,passwd)` / `leave(conference,sm)` — MUC presence join/leave with 409-retry.
- `caps_and_send(tmp)`, `self_vcard()`, `init_hash()` — presence/caps/vcard init.
- `now_schedule()` / `check_rss()` / `flush_stats()` — cron/schedule loop.
- `messageCB`/`presenceCB`/`iqCB` — XMPP stanza callbacks.
- `un_unix(val,rn='')` — humanize unix-time delta.

## Dependencies
- **Python 3** standard library: `os,sys,re,time,json,hashlib,html,base64,socket,math,random,calendar,datetime,urllib,urllib.request,urllib.error,subprocess,operator,logging,gc,threading,http.client,string`.
- `lib/xmpp` — xmpppy fork (XMPP client + Node/Protocol helpers; `xmpp.Node`, `xmpp.NS_MUC_ADMIN`, `sender`).
- `lib/chardet` — charset detection (only `chardet` import in kernel; Python 3 has built-in `chardet`? — present under `lib/`; `import chardet`).
- `lib/DNS` — DNS resolver helpers (used by `net.py`/`domain-ng.py`).
- `lib/wgpapi` — web API helpers (used by some ext plugins).
- `lib/crontab` — cron parsing (kernel imports `crontab`).
- DB drivers: `sqlite3` (stdlib), `psycopg2` (PG), `mysqldb`/`pymysql` (MySQL, per `base_type`).
- External binaries (runtime, optional): `dnsrecon`, `transhell`/`tr` (translate-shell), `git`/`svn`, `dig`, `host`, `nslookup`.
- External HTTP services: anekdot.ru, башорг.ru, bugmenut.com, gismeteo.ru, wttr.in, juick.com, psto.net, flibusta.net, GitHub/GitLab VCS APIs, currency/DEF-code providers, etc.

### Data files / layout (key)
- `data/sqlite3.db` — live DB. `data/db/*.dump` — seed dumps (defcodes/dist/gis/ru_alias/wz). `data/url/index.py` — URL title indexer.
- `data/date.txt` — holidays (drink.py source). `data/censor.txt` — censor rules. `data/quotes.txt`, `data/sovmestimosti.txt`, `data/poke.txt`, `data/tldlist.txt`, `data/oui.csv` (MAC vendors), `data/ignored.txt`, `data/rugramm.txt`, `data/ignore_turn.txt`.
- `data/locales/ru.txt`, `data/locales/ua.txt`.
- `data/cache/`, `data/log/`, `data/conflogs/`, `data/qr/`, `data/paste/`, `data/ai/`, `data/chat/`, `data/backup/`, `data/url/`.
- `tmp/` — pid, version, starttime, answers.txt, *.back.

## Quick Development Notes
- Entry: `python3 isida.py`. Hot commands: reload plugins via `.plugin`... (owner) or soft-update.
- Always check `execute`/`timers`/`iq_hook` tuples when adding commands; `execute` entries are `(level, cmd, handler, arg_mode, desc)`.
- To add a command: add tuple to `execute` in a plugin and define the handler using kernel globals (`send_msg`, `cur_execute`, `get_level`, `L`). For MUC admin: `xmpp.Node('iq',{'type':'set','to':jid},payload=[xmpp.Node('query',{'xmlns':xmpp.NS_MUC_ADMIN},[xmpp.Node('item',{...},[reason])] ...)])` via `sender(...)`.
- Run `python3 -c "import py_compile,glob; [py_compile.compile(f,doraise=True) for f in glob.glob('plugins/*.py')]"` to validate all plugins parse.

(End of context — generated from full tree scan, Aug 2026.)
