- Add !cancel command (anyone cancels own blackjack; PL50+ clears all room games)
- Add !wordlestats top-level command (wraps wordle stats function)
- Add !cleanwelcome admin command to purge stale welcome DM records
- !help now hides management section from sub-PL50 users, hides !health from non-admins
- !announce uses nio room cache for join_rule instead of an API call per room
- Fix _INVITEALL_BLOCKED comment (Commands is knock-gated, not restricted)
- welcome.py: skip duplicate DM if a pending welcome already exists for the user
- welcome.py: add clean_stale_dm_messages() helper
- welcome.py: replace no-op post_welcome_message with log_ready()
- bot.py: update import/call to match welcome.py rename
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Commands has join_rule=restricted so the join-rule filter didn't catch
it. Added _INVITEALL_BLOCKED set with the Commands room ID — any room
in that set is skipped regardless of join rule. Invite-only rooms
(Management, Cool Kids, Spam and Stuff) are still excluded by the
existing join_rule check.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
!setpl now iterates every Space room via the hierarchy API and updates
the power_levels state event in each. Setting a user to the room's
users_default cleans up the explicit entry rather than leaving a
stale PL0. Rooms where the bot lacks permission are counted and
reported but don't block the rest.
!inviteall skips rooms with join_rule=invite (Management, Cool Kids,
Spam and Stuff) — only public/restricted rooms get the invite. Also
skips rooms where the target is already a member.
_get_space_room_ids() fetches the Space child list via the v1 hierarchy
API with pagination support.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Five new commands, all gated behind is_elevated() (power level >= 50):
!mkroom <name> — clone #general's power levels, join rules, encryption,
history visibility, and avatar into a fresh v12 room,
auto-adds it to the Lotus Guild Space, and invites
the caller.
!roominfo — show room display name, ID, member count, join rule,
encryption status, and all users with PL > 0.
!topic [text] — set or clear the current room's topic.
!invite @user — invite any Matrix user to the current room.
!setpl @user <n> — update a user's power level (0-100); cannot exceed
the caller's own level.
Also adds urllib.parse.quote and MATRIX_HOMESERVER to imports, and
adds a "Management (PL50+)" section to !help.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add is_elevated() helper that reads the room power level from the nio
client store. Users at PL50+ (Nerdy Council and above) skip the cooldown
check entirely. The timestamp is still recorded so cooldown applies
if their power level is later reduced below 50.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each player now gets exactly one attempt per question. If their guess
is wrong it's recorded and they're locked out for that round. If both
players have guessed wrong the correct answer is revealed and the game
moves to the next question immediately (no need to wait for the 45s
timeout).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of a raw 6-word slice (which left dangling fragments like
'Partly - Not exclusively American; has'), extract the first complete
sentence (up to 10 words). Falls back to the 6-word cap only if no
sentence boundary is found in the response.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reduce word cap from 12 to 6 and add explicit instructions not to use
proper nouns, brand names, or place names in answers. Fixes the case
where the model blurted 'The Grand Canyon State (Arizona)' in response
to a geography question.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a rolling cache of the last 30 answers (persisted to
twentyq_cache.json) and pass the recent list to the model as an
explicit avoid clause. Also prompt the model to vary categories
each round. If the model still returns a cached answer it is
rejected and one retry is attempted automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Allow open-ended questions like "What color is it?" or "How big is it?"
The model now gives brief descriptive answers (capped at 12 words) for
open questions while still answering Yes/No/Sometimes/Partly for binary
ones. Updated command descriptions and in-game prompts accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Import MATRIX_USER_ID in commands.py (was missing — caused !ttt and
!triviaduel to crash with NameError on every invocation)
- Blackjack is now per-player per-room: multiple players can each run
their own game simultaneously; !hit and !stand operate on the caller's
own game only
- !hottake: pick a random topic from 20 categories and pass it to the
model so takes aren't all nostalgia-flavoured
- !nhie: tighter prompt with topic rotation and a word-count cap so
generated scenarios are simpler and more relatable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New commands: numguess/ng, wordchain/wc/endwc, acronym/ac,
20q/q/answer, nhie, hottake, ttt/move, blackjack/hit/stand,
triviaduel/da. All per-room with AI-generated content where
applicable. callbacks.py wired up for new reaction handlers
(acronym votes, nhie, hottake). Help and README updated with
full command reference.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The old \O/ ears row looked like 6 limbs when combined with /|\ arms.
New extended progression:
0-6: same as standard (head → body → arms → legs)
7: left foot (/ in the previously empty row below legs)
8: both feet (/ \)
9: @ head (anguish — full figure visible)
10: X head (dead)
Each stage is visually distinct with no overlapping limb confusion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-r requirements.txt causes pip-audit to spawn an internal venv which
calls ensurepip, failing with exit 127 on the standalone Python build.
--local avoids the venv. CVE-2026-3219 is in pip itself (not our deps)
so we ignore it explicitly with --ignore-vuln.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- !help Games section now includes !guess
- !8ball description mentions --debug flag
- pip-audit now scans only requirements.txt instead of --local (which
was flagging CVE-2026-3219 in pip itself, not our dependencies)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirrors riddle/trivia cache pattern: loads hangman_cache.json on startup,
appends each new word, caps at 30, saves after each game. Recent words
are passed to the model prompt to avoid repeats.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Silent failures (word too long/short, has hyphens, empty hint) are now
logged as warnings showing the actual word/hint returned. Retry up to
2 times before giving up, matching riddle's behavior.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The creative model was producing fortune-cookie mysticism ("navigate
the curve of fate") instead of 8-ball answers. New system prompt
explicitly requires YES/NO/UNCERTAIN category answers, 2-6 words,
funny and direct — no cryptic prophecies.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
phi4-mini refused to argue FOR controversial topics, instead deflecting
with neutral takes. The abliterated model with a committed debater system
prompt will actually take both sides without hedging or disclaimers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added _extract_riddle_answer() with dual fallback: JSON parse first,
then regex extraction of quoted riddle/answer values directly from text
- _generate_riddle() now retries up to 2 times on parse/network failure
- Hangman, scramble, WYR, and trivia now catch JSONDecodeError and log
the raw model output instead of letting the exception propagate silently
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
riddle_cache.json: stores last 30 riddle texts + answers
trivia_cache.json: stores last 20 questions per category
Both files are capped at their respective maxes so they never grow
unboundedly. Loaded on startup, saved after each new question.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
It's used for 8ball, roasts, riddles, WYR, and debate — not just the
magic 8-ball anymore. CREATIVE_MODEL better reflects its role as the
uncensored/abliterated model for creative generation tasks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
wyr:
- Reject options that end on a dangling word (but/and/or/with/never etc.)
so truncated sentences like 'but never' return None and retry
- Add 'via Llama 3.2 3B (abliterated)' credit to the poll message
riddle:
- Add 'via Llama 3.2 3B (abliterated)' credit to the riddle message
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous system prompt was basically empty. Now it explicitly:
- Requires the answer to be unambiguously correct
- Bans vague, ambiguous, or invented facts
- Requires plausible-but-wrong distractors
- Includes a concrete example of a good question
- Tells the model to pick a simpler topic if unsure
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
phi4-mini is too conservative and defaults to the same 2-3 answers.
Use BALL_MODEL (abliterated Llama 3.2) like WYR does.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
riddle:
- Cache answers separately so the same answer (e.g. 'shadow') can't
appear twice in a session even if the riddle text differs
- Explicitly ban 'shadow' in the prompt and append avoid-answers clause
- Ban question endings ('what am I?', 'what could it be?') more strictly
wyr:
- Hard-cap options at 10 words server-side so the model can't ignore
the word limit and generate paragraph-length options
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 3 assistant-turn examples to lock in the JSON format and tone
- Construct the 'question' field from option_a/option_b so it's always
well-formed regardless of what the model puts in the 'question' key
- Switch from phi4-mini to the abliterated Llama 3.2 model for edgier,
uncensored dilemmas
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
riddle:
- Tighten generation prompt with explicit rules: specific noun answer,
no answer word in the riddle, no 'what could it possibly mean', clues
must logically point to ONE answer, prefer concrete things
- Fix answer matching: strip articles (a/an/the), allow partial match
so 'person' hits 'a person' and 'shadow' hits 'my shadow' etc.
wyr:
- Prompt now asks for genuinely difficult dilemmas with real downsides
on both sides; explicitly bans boring options like dolphins/karaoke
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Keep a rolling list of the last 30 riddles used and inject them into
the prompt as an avoid clause, same pattern as trivia's per-category cache.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
nio has a dedicated ReactionEvent type with .reacts_to and .key attributes.
The callback was registered for UnknownEvent so reaction events were silently
dropped. Register for ReactionEvent and use its native attributes; keep the
UnknownEvent fallback for edge cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add edit_html() to utils using m.replace so messages can be updated
- Hangman board now edits in place on every guess — shows progressing
ASCII figure as wrong guesses accumulate instead of spamming new messages
- Extract _hangman_board_html() helper for consistent board rendering
- wyr: add INFO-level logging to reaction callback to diagnose vote tracking
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When Jared asks about a @mentioned third party, give a neutral honest
prediction instead of hijacking the answer to be about Jared.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The oracle should address Leon ('you survived Raccoon City...') not
impersonate him ('I'm not buying it').
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
api/generate has no system role — the model was ignoring the character
context and giving generic one-word answers. Chat API with a proper
system message forces the Leon voice.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _hangman_display compared uppercase word chars against lowercase
guessed_letters set, so letters were never revealed after correct guesses
- Word guess wrong path now shows the board and remaining guesses
- Winner display now includes the guesser's name on correct word guess
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add _WYR_POLLS dict keyed by poll event_id to accumulate votes
- record_wyr_vote() called from callbacks.reaction() on every reaction
- reveal() reads actual vote counts and announces winner with percentage
- Handles tie and zero-vote cases
- Remove the useless 'check the reactions above' message
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
phi4-mini can queue behind other requests and take >20s under load,
causing TimeoutError and silent failures in wyr/riddle/hangman/scramble.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
hangman, scramble, riddle, and wyr all used api/generate which has no
system role. The model would wrap JSON in prose or markdown fences,
causing json.loads() to throw and the command to silently die after
the 'Generating...' message.
Fix for all four: switch to api/chat with a system message enforcing
raw JSON output, strip markdown fences, and use regex to extract the
JSON object even if surrounded by extra text.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switch to api/chat with a system prompt for better JSON compliance,
and use regex extraction to find the JSON object even if the model
wraps it in extra text or markdown fences.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switch from api/generate to api/chat so we can set a system role that
instructs the model to be genuinely savage. Add a few-shot example so
it knows what a roast looks like vs a backhanded compliment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reframe prompt as a consented comedy roast between friends so the
model doesn't refuse on safety grounds
- Add lore for lonely (Cole, 23, dishwasher, gamer) and
natcofragomatic (Nathan, DCO Tech 3 at AWS, ginger, tape-drive nerd)
- Use a lookup table (_ROAST_LORE) so adding new users is one line
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- !hangman: AI picks a 5-8 letter word with hint; players !guess letters/words, 6 wrong = dead
- !scramble: AI picks a word, scrambles it; first correct answer in chat wins (45s timeout)
- !wyr: AI generates Would You Rather with 🅰️/🅱️ reaction voting, 30s reveal
- !riddle: AI generates riddle monitored for 60s, substring match in chat wins
- !roast: AI roasts a target using BALL_MODEL with special Jared/Wynter lore
- !story: collaborative story with !story add <line> and !story end (AI conclusion, max 10 lines)
- !debate: AI writes FOR/AGAINST arguments for any topic using ASK_MODEL
- callbacks.py: route all non-command messages through scramble/riddle answer checkers
- help: updated categories to include all new commands
- Replace flat fallback list with per-category fallback dict so
!trivia music never shows a gaming question when AI is down
- Always show "via <model>" tag on AI questions; show warning tag
on static fallbacks so users know AI was unavailable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New categories: anime, sports, food, history, geography, nature,
mythology, tv (14 total).
Add _trivia_recent dict that tracks the last 20 questions per
category and injects them into the LLM prompt as a avoid list,
preventing duplicate questions within a session.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When Wynter asks a romantic question about Jared ("is he in love
with me", "does he miss me", etc.) the LLM fallback now explicitly
denies the premise instead of giving a generic Jared-wins response.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add explicit Jared/Wynter no-romance lore to all four branch
bio_contexts and prompts — prevents model from implying romantic
feelings between them
- Add _implies_jared_wynter_romance() validator; responses that
suggest romantic connection fall back to the static fallback
- Replace random-list responses for non-Jared/Wynter senders with
AI-generated magic 8-ball predictions via BALL_MODEL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The python-build-standalone tarball ships pip 24.1.2 and setuptools
70.3.0 which have known CVEs. Upgrade them first so --local audit
only sees current, patched versions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The standalone Python 3.10 binary's venv ensurepip step exits 127.
Workaround: install requirements + pip-audit into the same env,
then audit with --local (no internal venv creation).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Debian Bullseye only ships Python 3.9 and python3.10 is not in its
repos. python-dotenv 1.2.2 (vuln fix) requires Python >=3.10.
Use indygreg/python-build-standalone to get a self-contained Python
3.10.15 binary that works on any glibc Linux runner.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
lotusllm, lotusllmben, and llama3.3 70B have been removed from
Ollama on LXC 130 to free ~44 GB disk space.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BALL_MODEL: huihui_ai/llama3.2-abliterate:3b (abliterated 3B,
follows complex persona instructions without censorship)
- ASK_MODEL + OLLAMA_MODEL: phi4-mini:latest (Phi-4 Mini 3.8B,
best instruction-following model available within GPU VRAM)
- Update _MODEL_DISPLAY for new model names
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix about_jared/about_wynter using substring match — "they" matched
"he", "theme" matched "he", etc., routing Wynter's questions to the
wrong branch. Now uses \b word boundaries via re.search.
- Switch BALL_MODEL default from sadiq-bd 1B uncensored to
llama3.2:latest (3B) — the 1B model hallucinates, ignores persona
instructions, and mentions Jared randomly. GPU is now working on
Arc A380 at ~25 tok/s so the larger model is practical.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
gemma3:latest produces garbage output on the Vulkan backend (Intel Arc A380).
llama3.2:latest runs correctly at 100% GPU. Timeout bumped to 120s to handle
cold model loads (~22s) without timing out.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8ball is only AI-powered for specific users (Wynter/Jared); for everyone
else it's a random static response. Games is the correct category.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Model attribution is now only shown when the LLM actually generated the
response. If the model refused or gave an invalid answer and we fell back
to the static response, no 'via ...' line is shown.
Fallback responses for all three Wynter branches are now randomised pools
so the bot doesn't always give the same flat yes/no phrase regardless of
what Wynter actually typed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
help: grouped into AI / Games / Random / Server categories with Option B
purple header; descriptions auto-pulled from the command registry.
Model attribution: added _MODEL_DISPLAY map so 'via lotusllm' becomes
'via Llama 3.2 1B', 'via gemma3:latest' becomes 'via Gemma 3 4B', etc.
Config: OLLAMA_MODEL switched from lotusllm to llama3.2:latest; added
BALL_MODEL (sadiq-bd/llama3.2-1b-uncensored) as a dedicated config var
for the 8ball so it stays on the uncensored model without affecting fortune.
Descriptions: fortune -> AI-generated fortune cookie; ask -> Ask LotusBot;
health -> Bot health & stats (admin only).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Responding 'Wynter is too busy...' in third person to someone who just
asked 'will I...' feels disconnected. Changed the prompt to speak
directly to Wynter using you/your, with her name used only for emphasis.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The LLM was responding with 'She's far too busy...' instead of using
'Wynter' by name. Added explicit instruction to both Wynter branches
to always refer to her by name and never use she/her pronouns.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8ball: color-coded answer text (green=positive, red=negative, amber=neutral)
for both the random and Jared/Wynter AI branches; question shown as small
italic below the answer; AI responses include model attribution.
fortune: teal header, answer in blockquote italics, model attribution shown
only when response came from the LLM (not the static fallback list).
ask: purple header, question in italic, response in blockquote, model
attribution at bottom.
trivia: blue header with category, green reveal answer, model attribution
shown only for LLM-generated questions (not static fallbacks).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Priority Order is stale project tracking that doesn't belong in a README.
vCPUs removed from the infrastructure table — containers are HA and can
migrate between physical hosts so pinning a CPU model is misleading.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each !ask call is stateless — no context is retained between commands,
so ending a response with a question is misleading. Added explicit
instruction to the system prompt to prevent this.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fortune: generates a fresh witty one-liner via Ollama on every call,
falls back to static list if LLM is unavailable.
ask: switched to /api/chat endpoint with a system prompt for better
conversational quality; now uses ASK_MODEL (default: gemma3:latest)
separately from the 8ball OLLAMA_MODEL so each can be tuned independently.
trivia: LLM generates a fresh question each time (no more repeating the
same 25 questions); supports !trivia <category> with six categories
(gaming, tech, general, movies, music, science); falls back to static
questions if JSON generation fails.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove unused imports: logging from bot.py and config.py, RoomMessageText/
UnknownEvent from callbacks.py, functools.partial and MAX_INPUT_LENGTH from
commands.py. Rename unused local variables to _ (resp in cmd_ping, symbols in
render_keyboard_plain, guesses_left in two wordle functions). Move wordle import
to top of commands.py to fix E402.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ruff: add --strip-components=1 to tar extract; the tarball puts the
binary inside ruff-x86_64-unknown-linux-gnu/ not at the root
- gitleaks: path-based allowlists are broken in v8.21.2 --no-git mode
(tested down to bare substrings — still fires). Switched to scanning
only application code directories (matrixbot/, hookshot/, .gitea/,
systemd/, cinny/, landing/) which excludes deploy/ where the
intentional Gitea webhook HMAC secrets live. Also removed the
.gitleaks-baseline.json from the repo (it was flagging itself).
The .gitleaks.toml is kept for any future per-rule overrides.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ruff: download standalone binary instead of using python3 -m ruff
(runner image lacks the PATH entry for pip-installed bin scripts)
- pip-audit: add python3-venv to apt install (pip-audit creates a venv
internally to resolve deps; ensurepip was missing)
- gitleaks: switch from stopwords allowlist to --baseline-path approach.
Stopwords don't suppress findings from git history scans. The baseline
records the 4 known-intentional webhook HMAC secrets; CI now only
fails on findings NOT in the baseline (i.e. newly introduced secrets)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- All Python jobs now install python3-pip via apt first (runner image
has no pip by default)
- Added secret-scan job: gitleaks v8.21.2 scans full git history on
every push/PR with --redact to avoid leaking found secrets in logs
- Added .gitleaks.toml allowlisting deploy/hooks-lxc*.json files
(webhook HMAC secrets are intentional config, not leaks)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- python3 -m pip works in the act runner where bare 'pip' isn't in PATH
- Added python-audit job: pip-audit checks matrixbot/requirements.txt
against the OSV database for known CVEs on every push/PR
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- When Jared asks a question containing Wynter's name, it now uses a
dedicated mock-Wynter prompt instead of the generic positive-Jared
one. The _is_positive_about_jared guard is also skipped for this
branch so negative words aimed at Wynter don't trigger the fallback.
Fallback changed from "Jared is absolutely right!" (nonsensical for
Wynter questions) to "Sounds about right — Wynter had it coming."
- Added ruff Python lint job to .gitea/workflows/lint.yml covering
matrixbot/ on every push and PR.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 16:23:59 -04:00
11 changed files with 4075 additions and 244 deletions
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.