Skip to content

feat: add config helpers, decorator stacks, and lifecycle managers to… - #129

Merged
rolling-codes merged 5 commits into
mainfrom
feat/reduce-boilerplate-core
Aug 11, 2026
Merged

feat: add config helpers, decorator stacks, and lifecycle managers to…#129
rolling-codes merged 5 commits into
mainfrom
feat/reduce-boilerplate-core

Conversation

@rolling-codes

@rolling-codes rolling-codes commented Aug 11, 2026

Copy link
Copy Markdown
Owner

… eliminate plugin boilerplate

Additional improvements in plugin infrastructure:

  1. PluginConfigHelper mixin: Eliminates repeated load→mutate→save ceremony

    • await plugin.config_set(guild_id, key, value) instead of 5-line pattern
    • await plugin.config_get/config_update/config_delete/config_mutate shortcuts
    • All operations are atomic under per-guild locks
  2. Pre-composed decorator stacks: Common command patterns in single decorator

    • @slash_admin_command() replaces @slash + require_admin=True
    • @slash_management_command() for manage_guild permission gates
    • @slash_mod_command() for moderation commands with auto-perms
    • @slash_user_command() for public commands
    • @slash_with_confirm() for dangerous operations needing confirmation
  3. TaskManager: Automatic task lifecycle tracking

    • Eliminates manual _tasks: dict + on_unload() cancellation
    • await tasks.start_recurring(fn, interval) auto-cancels on unload
    • await tasks.start_once(name, coro) prevents duplicate tasks
    • Single await tasks.cancel_all() cleans up everything
  4. TimerManager: Hierarchical timer tracking for delayed events

    • Replaces manual _timers: dict[int, dict[int, asyncio.Task]] patterns
    • await timers.schedule(timer_id, fn, seconds, guild_id=...) auto-cancels on unload
    • await timers.cancel_guild(timer_id, guild_id) bulk-cancels per guild
    • Single await timers.cancel_all() cleans up all in-flight timers

Example plugin reductions:

  • Before: BirthdayPlugin ~400 lines (includes lock/store ceremonies)

  • After: ~250 lines (with TaskManager + PluginConfigHelper)

  • Before: GiveawayPlugin ~300 lines (timer tracking boilerplate)

  • After: ~150 lines (with TimerManager)

Summary by Sourcery

Introduce helper utilities to reduce boilerplate in plugin configuration, command declaration, and task/timer lifecycle management.

New Features:

  • Add PluginConfigHelper mixin to provide atomic, lock-backed CRUD helpers for per-guild plugin configuration.
  • Add pre-composed slash command decorators for common admin, management, moderation, user, and confirmation command patterns.
  • Add TaskManager helper to track asyncio tasks and simplify starting recurring/one-off tasks with unified cancellation.
  • Add TimerManager helper to schedule delayed tasks with hierarchical per-guild tracking and bulk cancellation APIs.

… eliminate plugin boilerplate

Additional improvements in plugin infrastructure:

1. **PluginConfigHelper mixin**: Eliminates repeated load→mutate→save ceremony
   - `await plugin.config_set(guild_id, key, value)` instead of 5-line pattern
   - `await plugin.config_get/config_update/config_delete/config_mutate` shortcuts
   - All operations are atomic under per-guild locks

2. **Pre-composed decorator stacks**: Common command patterns in single decorator
   - `@slash_admin_command()` replaces `@slash` + `require_admin=True`
   - `@slash_management_command()` for `manage_guild` permission gates
   - `@slash_mod_command()` for moderation commands with auto-perms
   - `@slash_user_command()` for public commands
   - `@slash_with_confirm()` for dangerous operations needing confirmation

3. **TaskManager**: Automatic task lifecycle tracking
   - Eliminates manual `_tasks: dict` + `on_unload()` cancellation
   - `await tasks.start_recurring(fn, interval)` auto-cancels on unload
   - `await tasks.start_once(name, coro)` prevents duplicate tasks
   - Single `await tasks.cancel_all()` cleans up everything

4. **TimerManager**: Hierarchical timer tracking for delayed events
   - Replaces manual `_timers: dict[int, dict[int, asyncio.Task]]` patterns
   - `await timers.schedule(timer_id, fn, seconds, guild_id=...)` auto-cancels on unload
   - `await timers.cancel_guild(timer_id, guild_id)` bulk-cancels per guild
   - Single `await timers.cancel_all()` cleans up all in-flight timers

Example plugin reductions:
- **Before**: BirthdayPlugin ~400 lines (includes lock/store ceremonies)
- **After**: ~250 lines (with TaskManager + PluginConfigHelper)

- **Before**: GiveawayPlugin ~300 lines (timer tracking boilerplate)
- **After**: ~150 lines (with TimerManager)
@sourcery-ai

sourcery-ai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduces helper mixins and utility classes to reduce boilerplate in plugins: a PluginConfigHelper for atomic, lock-guarded config CRUD; pre-composed slash-command decorator stacks for common permission/cooldown patterns; and TaskManager/TimerManager classes for centralized tracking and automatic cancellation of background tasks and timers in plugin lifecycles.

Sequence diagram for atomic config_set operation

sequenceDiagram
    participant Plugin
    participant GuildLockManager
    participant ServerConfigStore
    participant ServerConfig

    Plugin->>Plugin: config_set(guild_id, key, value, section)
    Plugin->>Plugin: _apply(cfg)

    Plugin->>GuildLockManager: lock(guild_id)
    activate GuildLockManager
    GuildLockManager-->>Plugin: lock context

    Plugin->>ServerConfigStore: load(guild_id)
    activate ServerConfigStore
    ServerConfigStore-->>Plugin: ServerConfig
    deactivate ServerConfigStore

    Plugin->>ServerConfig: get_other(section, {})
    activate ServerConfig
    ServerConfig-->>Plugin: data dict
    Plugin->>ServerConfig: set_other(section, data with key=value)
    deactivate ServerConfig

    Plugin->>ServerConfigStore: save(ServerConfig)
    activate ServerConfigStore
    ServerConfigStore-->>Plugin: save complete
    deactivate ServerConfigStore

    Plugin-->>GuildLockManager: release lock
    deactivate GuildLockManager
Loading

File-Level Changes

Change Details Files
Add PluginConfigHelper mixin to encapsulate atomic, lock-guarded config CRUD operations for plugins using ServerConfigStore and GuildLockManager.
  • Define PluginConfigHelper with typed attributes for store, lock manager, and optional section name defaulting to plugin name.
  • Implement config_get for simple load-and-read access without locking, using plugin-named sections.
  • Implement config_set, config_update, config_delete, config_mutate, and config_clear that acquire per-guild locks, load, mutate, and save configs atomically via shared helper closures.
  • Provide convenience patterns for multi-key updates and custom mutation functions that return results while preserving section isolation.
easycord/_plugin_config_helper.py
Introduce pre-composed slash-command decorator stacks to standardize common command patterns for admins, management, moderation, public users, and confirmation flows.
  • Import base decorators (slash, cooldown, require_permissions, describe) and wrap them into typed factory functions returning decorators over callables.
  • Implement slash_admin_command to set require_admin=True and default ephemeral responses while allowing cooldown and bot permission configuration.
  • Implement slash_management_command to default required user permissions to manage_guild with configurable cooldown, bot permissions, and ephemerality.
  • Implement slash_mod_command that bundles typical moderation permissions, adds required bot permissions, and enforces ephemeral responses.
  • Implement slash_user_command for public commands with optional cooldown and guild-only gating.
  • Implement slash_with_confirm as a marker stack for dangerous commands, delegating actual confirmation UI to ctx.confirm while standardizing slash options.
easycord/_decorator_stacks.py
Add TaskManager and TimerManager utility classes to centralize tracking and lifecycle management of background tasks and delayed timers in plugins, including bulk cancellation on unload.
  • Implement TaskManager with internal name→Task mapping, track() registration with auto-cleanup callbacks, start_once for idempotent task creation, start_recurring for interval-driven loops with error logging, and cancel_all for bulk cancellation and gathering.
  • Implement TimerManager with hierarchical timer_id→guild_id→Task storage for delayed callbacks, schedule() to create and record delayed tasks with optional guild scoping, and cleanup of entries on completion.
  • Provide cancel_timer and cancel_guild helpers to cancel specific timers by ID and guild scope, plus cancel_all to bulk cancel and await all active timers before clearing state.
  • Document example plugin usage showing elimination of manual task dicts and on_unload cancellation logic using these managers.
easycord/_plugin_lifecycle_helpers.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added reusable command decorators for administrative, management, moderation, public-user, and confirmation-enabled slash commands.
    • Added plugin configuration helpers for reading, updating, deleting, mutating, and clearing settings.
    • Added task and timer management for recurring work, delayed actions, cancellation, and shutdown cleanup.
  • Tests

    • Added comprehensive coverage for command decorators, configuration operations, tasks, timers, cancellation, and concurrent updates.

Walkthrough

This PR adds reusable slash-command decorators, atomic plugin configuration helpers, asynchronous task and timer managers, and comprehensive tests for their behavior.

Changes

Plugin helper foundations

Layer / File(s) Summary
Slash-command decorator stacks
easycord/_decorator_stacks.py, tests/test_plugin_helpers.py
Adds decorator factories for admin, management, moderation, public-user, and confirmation-marked slash commands.
Atomic plugin configuration helpers
easycord/_plugin_config_helper.py, tests/test_plugin_helpers.py
Adds section resolution and atomic get, set, update, delete, mutate, and clear operations through ServerConfigStore.
Task and timer lifecycle management
easycord/_plugin_lifecycle_helpers.py, tests/test_plugin_helpers.py
Adds named task tracking, recurring tasks, timer scheduling, cancellation, cleanup, and shutdown handling.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: plugin

Suggested reviewers: tee

Poem

A rabbit stacks commands neat and bright,
Stores guild settings safe at night.
Tasks and timers hop in line,
Tests watch each helper work fine.
Thump, thump—the plugin blooms! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: configuration helpers, decorator stacks, and lifecycle managers.
Description check ✅ Passed The description directly explains the new plugin infrastructure helpers and their purpose of reducing boilerplate.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/reduce-boilerplate-core

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the enhancement New feature or request label Aug 11, 2026
Comment on lines +27 to +32
from .decorators import (
slash as _slash,
cooldown as _cooldown,
require_permissions as _require_permissions,
describe as _describe,
)
Comment thread easycord/_plugin_lifecycle_helpers.py Fixed
try:
await asyncio.sleep(seconds)
await fn(*args, **kwargs)
except asyncio.CancelledError:
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add plugin helpers for config CRUD, command decorator stacks, and task/timer lifecycle

✨ Enhancement 🕐 40+ Minutes

Grey Divider

AI Description

• Add mixin helpers for atomic per-guild plugin config CRUD under locks.
• Introduce pre-composed slash-command decorator stacks for common permission patterns.
• Provide TaskManager/TimerManager utilities to standardize background task and timer cleanup.
Diagram

graph TD
  P["Plugin code"] --> DS["_decorator_stacks"] --> DEC["decorators (slash)"]
  P --> CH["PluginConfigHelper"] --> SCS[("ServerConfigStore")]
  CH --> GLM["GuildLockManager"]
  P --> LH["Task/Timer managers"] --> RT(("asyncio runtime"))
  subgraph Legend
    direction LR
    _m["Module"] ~~~ _db[("Storage")] ~~~ _rt(("Runtime"))
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use ServerConfigStore.mutate() for atomic config updates
  • ➕ Avoids maintaining a second locking mechanism around config writes
  • ➕ Guarantees atomicity using the store’s own read-modify-write API
  • ➕ Reduces risk of lock-ordering issues (external lock + internal store lock)
  • ➖ Requires writing mutations against ServerConfig objects (slightly less direct dict-style APIs)
  • ➖ Less flexible if plugins intentionally want to coordinate locking across multiple stores/resources
2. Fold helpers into Plugin base (built-in managers)
  • ➕ Simplest adoption (no mixin/composition wiring per plugin)
  • ➕ Enables framework-managed lifecycle hooks (auto-cancel on unload)
  • ➖ Increases Plugin surface area and tightens coupling for plugins that don’t need these features
  • ➖ Harder to evolve APIs without impacting all plugin authors
3. Leverage existing discord.py patterns (tasks.loop / app_commands checks)
  • ➕ Uses well-known primitives with established semantics
  • ➕ Potentially less custom infrastructure to maintain
  • ➖ May not map cleanly to EasyCord’s plugin unload/reload lifecycle requirements
  • ➖ Doesn’t address config boilerplate reduction by itself

Recommendation: The overall direction (small, focused helper modules) is a good fit for reducing plugin boilerplate without overloading the Plugin base. Before merging, strongly consider (1) switching config writes to ServerConfigStore.mutate() or explicitly documenting lock ordering to avoid dual-lock pitfalls, and (2) tightening lifecycle helper semantics (e.g., ensure timers are trackable/cancellable even when guild_id is omitted). Also double-check decorator stack defaults to avoid surprising behavior (e.g., admin stack forcing ephemeral responses).

Files changed (3) +718 / -0

Enhancement (3) +718 / -0
_decorator_stacks.pyAdd pre-composed slash command decorator stacks +245/-0

Add pre-composed slash command decorator stacks

• Introduces helper decorator factories that wrap the existing @slash decorator for common permission and UX patterns (admin, management, moderation, user, and confirmation-marked commands). This reduces repeated decorator layering across plugins and standardizes default permission/cooldown/ephemeral behavior.

easycord/_decorator_stacks.py

_plugin_config_helper.pyAdd PluginConfigHelper mixin for per-guild config CRUD +251/-0

Add PluginConfigHelper mixin for per-guild config CRUD

• Adds a mixin that provides config_get/set/update/delete/mutate/clear helpers for plugin configuration stored under ServerConfigStore 'other' sections. Mutating operations run under a per-guild lock to make read-modify-write sequences atomic and reduce repetitive load/mutate/save code.

easycord/_plugin_config_helper.py

_plugin_lifecycle_helpers.pyAdd TaskManager and TimerManager for plugin async lifecycle management +222/-0

Add TaskManager and TimerManager for plugin async lifecycle management

• Adds TaskManager to track named asyncio tasks, prevent duplicate starts, and support bulk cancellation during plugin unload. Adds TimerManager to schedule delayed handlers with hierarchical tracking keyed by (timer_id, guild_id) and provide targeted or global cancellation APIs.

easycord/_plugin_lifecycle_helpers.py

@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.30233% with 27 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
easycord/_plugin_lifecycle_helpers.py 67.07% 27 Missing ⚠️

📢 Thoughts on this report? Let us know!

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In slash_admin_command, ephemeral is effectively forced to True via ephemeral=ephemeral or True, so passing ephemeral=False is ignored; consider using a sentinel or ephemeral if ephemeral is not None else True to respect explicit False.
  • In TimerManager.schedule, timers created without a guild_id are never stored in self.timers, so they cannot be cancelled via cancel_timer/cancel_all; if non-guild timers are expected, you may want to track them under a special key or separate collection.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `slash_admin_command`, `ephemeral` is effectively forced to `True` via `ephemeral=ephemeral or True`, so passing `ephemeral=False` is ignored; consider using a sentinel or `ephemeral if ephemeral is not None else True` to respect explicit False.
- In `TimerManager.schedule`, timers created without a `guild_id` are never stored in `self.timers`, so they cannot be cancelled via `cancel_timer`/`cancel_all`; if non-guild timers are expected, you may want to track them under a special key or separate collection.

## Individual Comments

### Comment 1
<location path="easycord/_decorator_stacks.py" line_range="71" />
<code_context>
+            require_admin=True,
+            cooldown=cooldown,
+            bot_permissions=bot_permissions,
+            ephemeral=ephemeral or True,
+        )(func)
+        return func
</code_context>
<issue_to_address>
**issue (bug_risk):** The `ephemeral` argument to `slash_admin_command` is effectively forced to `True`, ignoring any `False` passed by callers.

`ephemeral=ephemeral or True` will always be `True`, so callers cannot set it to `False`, and the docstring’s stated default of `False` is incorrect.

If you want a default of `True` but still allow `False`, either:
- Use `ephemeral: bool | None = None` and `ephemeral = True if ephemeral is None else ephemeral`, or
- Keep `ephemeral: bool = True` and pass it through as `ephemeral=ephemeral`.

This will align the behavior with the signature and documentation.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread easycord/_decorator_stacks.py Outdated
Issue: Timers scheduled without a guild_id were created but never stored
in self.timers, making them uncancellable via cancel_all().

Solution: Store timers without a guild_id under a special "__ungrouped__"
key so they can be cancelled with cancel_all().
@qodo-code-review

qodo-code-review Bot commented Aug 11, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Timers not cancelled ✓ Resolved 🐞 Bug ☼ Reliability
Description
TimerManager.schedule() only tracks tasks when the keyword-only guild_id parameter is provided;
the class doc example passes guild_id positionally, so those timers will be untracked and
cancel_all()/cancel_guild() won’t cancel them on unload.
Code

easycord/_plugin_lifecycle_helpers.py[R184-187]

+        task = asyncio.create_task(delayed())
+        if guild_id is not None:
+            self.timers.setdefault(timer_id, {})[guild_id] = task
+        return task
Relevance

●●● Strong

Example/API mismatch causes timers to be untracked and uncancelled; straightforward
reliability/documentation fix.

PR-#82

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The doc example passes guild_id positionally, but schedule() only tracks when the keyword-only
guild_id parameter is set; cancel_all() only cancels tracked timers, so these timers will survive
cancellation.

easycord/_plugin_lifecycle_helpers.py[124-133]
easycord/_plugin_lifecycle_helpers.py[146-187]
easycord/_plugin_lifecycle_helpers.py[212-222]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`TimerManager.schedule()` only inserts the created task into `self.timers` when `guild_id` (a keyword-only arg) is provided. However, the doc example calls `schedule(..., guild_id, message_id)` positionally, so `guild_id` is treated as a normal `*args` value, tracking never happens, and later `cancel_all()` cannot cancel the timer.

### Issue Context
- `schedule()` declares `*args` before `guild_id`, making `guild_id` keyword-only.
- The example in the class docstring does **not** pass `guild_id=`.
- `cancel_all()` only cancels tasks found in `self.timers`.

### Fix Focus Areas
- easycord/_plugin_lifecycle_helpers.py[124-133]
- easycord/_plugin_lifecycle_helpers.py[146-187]
- easycord/_plugin_lifecycle_helpers.py[212-222]

### Suggested fix options
Pick one consistent contract:
1) **Track all timers:** store tasks regardless of whether `guild_id` is provided (e.g., keep a separate bucket for `guild_id=None`, or treat `None` as a key), so `cancel_all()` really cancels everything.
2) **Enforce grouping:** make `guild_id` required for `schedule()` if the manager promises automatic lifecycle cancellation, and update the example to use `guild_id=guild_id`.

Also update the doc example to pass `guild_id=` to match the signature.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Config helper bypasses mutate() ✓ Resolved 📘 Rule violation ≡ Correctness
Description
PluginConfigHelper implements multiple read→modify→write sequences via load() + save() under
an external lock instead of using ServerConfigStore.mutate(), risking lost updates and violating
the required atomic mutation API.
Code

easycord/_plugin_config_helper.py[R113-116]

+        async with self._locks.lock(guild_id):
+            cfg = await self._store.load(guild_id)
+            await _apply(cfg)
+            await self._store.save(cfg)
Relevance

●●● Strong

Matches stated PR intent (“atomic”) and referenced policy; likely switched to
ServerConfigStore.mutate().

PR-#86

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 473031 requires all ServerConfigStore read-modify-write operations to use
mutate() (single atomic call with sync callback). The new helper uses `async with
self._locks.lock(guild_id) followed by load() and save()` in multiple methods, implementing
manual read-modify-write instead of mutate().

Rule 473031: Use ServerConfigStore.mutate for all config read-modify-write sequences
easycord/_plugin_config_helper.py[108-116]
easycord/_plugin_config_helper.py[142-145]
easycord/server_config.py[234-246]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`PluginConfigHelper` performs read-modify-write by calling `await self._store.load(guild_id)` then `await self._store.save(cfg)` under `self._locks.lock(guild_id)`. Compliance requires all read-modify-write sequences to be done via a single `await ServerConfigStore.mutate(guild_id, fn)` call with a **synchronous** callback.

## Issue Context
`ServerConfigStore` already provides `mutate()` which holds its per-guild lock across load/modify/save and documents the requirement that the callback be synchronous and avoid I/O while the lock is held.

## Fix Focus Areas
- easycord/_plugin_config_helper.py[108-116]
- easycord/_plugin_config_helper.py[137-145]
- easycord/_plugin_config_helper.py[171-179]
- easycord/_plugin_config_helper.py[217-225]
- easycord/_plugin_config_helper.py[245-251]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. config_set example wrong ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
PluginConfigHelper’s top-level example calls config_set(guild_id, "section", "key", value), but
the implemented signature is config_set(guild_id, key, value, section=None), so copying the
example will persist the wrong data.
Code

easycord/_plugin_config_helper.py[R12-15]

+You can now write:
+
+    await self.config_set(guild_id, "section", "key", value)
+"""
Relevance

●●● Strong

Doc/example contradicts signature; low-risk maintainability fix that prevents user misuse.

PR-#106

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The docstring shows a 4-argument positional call, but the function signature only supports
(guild_id, key, value, section=None), making the documented call semantically wrong.

easycord/_plugin_config_helper.py[1-15]
easycord/_plugin_config_helper.py[86-107]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The module docstring advertises `await self.config_set(guild_id, "section", "key", value)`, but `config_set` is implemented as `(guild_id, key, value, section=None)`. Using the documented 4-arg call will not raise immediately, but will store `key="section"` and `value="key"` into `section=<value>`.

### Issue Context
The same file includes another example using the 3-argument form (`await self.config_set(ctx.guild.id, key, value)`), so the top-level example is inconsistent.

### Fix Focus Areas
- easycord/_plugin_config_helper.py[1-15]
- easycord/_plugin_config_helper.py[86-107]

### Suggested fix
Update the doc example to one of:
- `await self.config_set(guild_id, "key", value, section="section")`
- or, if you intended the API to be `(guild_id, section, key, value)`, change the method signature accordingly and update all internal examples.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Admin always ephemeral ✓ Resolved 🐞 Bug ≡ Correctness
Description
slash_admin_command() hard-codes ephemeral=True via ephemeral=ephemeral or True, so callers cannot
create a non-ephemeral admin command even when passing ephemeral=False.
Code

easycord/_decorator_stacks.py[R68-71]

+            require_admin=True,
+            cooldown=cooldown,
+            bot_permissions=bot_permissions,
+            ephemeral=ephemeral or True,
Relevance

●●● Strong

Clear boolean logic bug: ephemeral or True ignores explicit False; likely fixed to respect caller
intent.

PR-#82

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The decorator stack forces ephemeral to True regardless of caller input, and the core slash()
decorator records this value verbatim as _slash_ephemeral which is then used during command
registration.

easycord/_decorator_stacks.py[65-73]
easycord/decorators.py[280-363]
easycord/_plugin_scanner.py[121-130]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`slash_admin_command()` always passes `ephemeral=True` to `@slash` because it uses `ephemeral=ephemeral or True`. This makes the `ephemeral` parameter ineffective and forces all admin commands to be ephemeral.

### Issue Context
The underlying `slash()` decorator stores the provided `ephemeral` value directly in `_slash_ephemeral`, so the forced True changes command registration metadata.

### Fix Focus Areas
- easycord/_decorator_stacks.py[66-72]
- easycord/decorators.py[280-363]

### Suggested fix
Change `ephemeral=ephemeral or True` to `ephemeral=ephemeral` (or change the default parameter to `ephemeral: bool = True` if you want admin commands to default to ephemeral, while still allowing explicit False).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View review recommended (2)
5. config_get bypasses lock 🐞 Bug ☼ Reliability
Description
PluginConfigHelper.config_get() does not acquire the per-guild lock used by
config_set/update/delete/mutate/clear, so it can return a stale snapshot relative to concurrent
lock-serialized writes.
Code

easycord/_plugin_config_helper.py[R81-84]

+        section = section or (self._section_name or self.name)
+        cfg = await self._store.load(guild_id)
+        data = cfg.get_other(section, {})
+        return data.get(key, default)
Relevance

●● Moderate

Locking reads is design-dependent; team may accept eventual consistency for config_get to avoid lock
contention.

PR-#123

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
config_get() calls store.load() directly with no external lock, while the mutation helpers
explicitly acquire the per-guild lock before load/save; ServerConfigStore.load() returns independent
copies, making this a stale-read risk rather than shared-state mutation.

easycord/_plugin_config_helper.py[57-85]
easycord/_plugin_config_helper.py[113-117]
easycord/server_config.py[218-227]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
All write helpers in `PluginConfigHelper` serialize operations with `async with self._locks.lock(guild_id)`, but `config_get()` reads without that lock. As a result, reads are not ordered with respect to the helper’s writes and can observe stale data.

### Issue Context
`ServerConfigStore.load()` returns an independent, deep-copied config object each call, so this is primarily an ordering/consistency issue (not in-memory corruption). If the helper claims lock-backed atomic CRUD, reads should participate in the same lock.

### Fix Focus Areas
- easycord/_plugin_config_helper.py[57-85]
- easycord/_plugin_config_helper.py[113-117]
- easycord/server_config.py[218-227]

### Suggested fix
Either:
- Wrap the load/read in `async with self._locks.lock(guild_id)` to make reads consistent with writes, or
- Explicitly document that `config_get()` is intentionally unlocked and may return stale values under concurrent writes (and provide a `config_get_locked()` if needed).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Task name reuse race ✓ Resolved 🐞 Bug ☼ Reliability
Description
TaskManager.track() removes tasks by name in a done-callback; if a new task is tracked under the
same name before the old task’s callback runs, the old callback can remove the new task from
tracking, causing it to be skipped by cancel_all().
Code

easycord/_plugin_lifecycle_helpers.py[R51-53]

+        self.tasks[name] = task
+        task.add_done_callback(lambda t: self.tasks.pop(name, None))
+        return task
Relevance

●● Moderate

Subtle race; fix may add complexity (task identity checks). Acceptance uncertain without precedent.

PR-#82

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
track() unconditionally pops by name on completion, and cancel_all() operates only on tasks still
present in the tracking dict—so if a newer task is popped by an older callback it won’t be
cancelled.

easycord/_plugin_lifecycle_helpers.py[43-53]
easycord/_plugin_lifecycle_helpers.py[102-110]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`TaskManager.track()` registers a done callback that does `self.tasks.pop(name, None)`. If task A is stored under `name`, then task B later overwrites `self.tasks[name]` before task A’s done callback runs, task A’s callback will pop `name` and remove task B. The replacement task becomes untracked and may not be cancelled by `cancel_all()`.

### Issue Context
This can happen if code starts a recurring task twice with the same explicit name, or uses `track()` directly.

### Fix Focus Areas
- easycord/_plugin_lifecycle_helpers.py[46-53]
- easycord/_plugin_lifecycle_helpers.py[102-109]

### Suggested fix
In the done callback, only remove the dict entry if it still points to the same task:
- `def _cleanup(t):
   if self.tasks.get(name) is t:
       self.tasks.pop(name, None)`
Then attach `_cleanup` as the callback.

Optionally, refuse to overwrite an existing running task in `track()` (raise or cancel existing).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context
✅ Compliance rules (platform): 41 rules

Grey Divider

Tip of the day
💡 Did you know, you can group findings by type and pick your Finding display, from Minimal to Full

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread easycord/_plugin_config_helper.py Outdated
Comment thread easycord/_decorator_stacks.py Outdated
Comment thread easycord/_plugin_lifecycle_helpers.py
Comment thread easycord/_plugin_lifecycle_helpers.py
Comment thread easycord/_plugin_config_helper.py Outdated
Comment thread easycord/_plugin_config_helper.py Outdated
Issue: `ephemeral=ephemeral or True` forces ephemeral to always be True,
ignoring explicit False values from callers.

Solution: Use ternary to default to True only when not explicitly set.
This allows callers to override the default if needed.
Comment thread easycord/_plugin_lifecycle_helpers.py Fixed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f83ac03c1f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread easycord/_decorator_stacks.py
Comment thread easycord/_plugin_lifecycle_helpers.py Outdated
Comment thread easycord/_plugin_lifecycle_helpers.py Outdated
Comment thread easycord/_decorator_stacks.py
Comment thread easycord/_plugin_lifecycle_helpers.py
Comment thread easycord/_plugin_config_helper.py Outdated
Comment thread easycord/_decorator_stacks.py
Comment thread easycord/_plugin_lifecycle_helpers.py
Comment thread easycord/_plugin_lifecycle_helpers.py
Comment thread easycord/_decorator_stacks.py
@augmentcode

augmentcode Bot commented Aug 11, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: This PR introduces helper abstractions intended to reduce custom-plugin boilerplate.

Changes:

  • Adds pre-composed slash-command decorators for admin, management, moderation, user, and confirmation flows.
  • Adds PluginConfigHelper CRUD and mutation shortcuts around per-guild configuration sections.
  • Adds TaskManager for named one-shot and recurring background-task lifecycle tracking.
  • Adds TimerManager for delayed callbacks with optional per-guild cancellation.

Technical notes: The helpers wrap existing slash metadata, configuration storage, and asyncio tasks. Their stated purpose is to centralize persistence and unload cleanup that plugins previously implemented manually.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 5 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread easycord/_plugin_config_helper.py Outdated
Comment thread easycord/_plugin_config_helper.py Outdated
Comment thread easycord/_decorator_stacks.py Outdated
Comment thread easycord/_plugin_lifecycle_helpers.py
Comment thread easycord/_plugin_lifecycle_helpers.py Outdated
- PluginConfigHelper: route all writes through ServerConfigStore.mutate so
  load-modify-save is atomic under the store's own per-guild lock (one lock
  domain, no lost updates); drop the separate GuildLockManager requirement.
  Fix the module-docstring example arg order and guard against a missing _store.
- TaskManager.track: in the done-callback, only evict a task if it is still the
  one registered under that name, so a replacement started under the same name
  isn't untracked (and left uncancellable) by the finished task's callback.
- Remove an unused TYPE_CHECKING import.
- Add tests/test_plugin_helpers.py (22 tests) covering the decorator stacks,
  TaskManager/TimerManager, and PluginConfigHelper, including the identity-race
  and concurrent no-lost-updates regressions.
- Reframe the concurrent-writes test: empirically the synchronous store never
  suspends between load and save, so the old separate-lock pattern did not lose
  updates (verified 200/200 both ways). The rewrite to store.mutate is a
  simplification (single lock domain, drops the _locks dependency), not a
  data-loss fix, so the test no longer overclaims to guard a cross-domain race.
- Make the TaskManager tests deterministic: await the task then a single
  sleep(0) rather than a wall-clock sleep.
- config_mutate docstring example now returns a meaningful (non-None) value.
def test_slash_admin_command_defaults_to_admin_and_ephemeral() -> None:
@slash_admin_command(description="d")
async def cmd(self, ctx): # pragma: no cover - body never runs
...
# Regression: ephemeral=False must be honored, not overridden to True.
@slash_admin_command(description="d", ephemeral=False)
async def cmd(self, ctx): # pragma: no cover
...
Comment thread tests/test_plugin_helpers.py Dismissed
def test_slash_management_command_respects_ephemeral_false() -> None:
@slash_management_command(description="d", ephemeral=False)
async def cmd(self, ctx): # pragma: no cover
...
def test_slash_mod_command_sets_user_and_bot_perms() -> None:
@slash_mod_command(description="d")
async def cmd(self, ctx): # pragma: no cover
...
def test_slash_with_confirm_registers_slash() -> None:
@slash_with_confirm(description="d", permissions=["manage_guild"])
async def cmd(self, ctx): # pragma: no cover
...
async def test_track_removes_task_when_done() -> None:
tm = TaskManager()
task = await tm.start_once("job", asyncio.sleep, 0)
await task
second = await tm.start_once("w", waiter)
assert first is second
ev.set()
await first
replacement = asyncio.create_task(asyncio.sleep(3600))
tm.track("bg", replacement) # replace under the same name, synchronously

await finishing # deterministically wait for the first task to complete
tmr = TimerManager()

async def cb() -> None: # pragma: no cover - never fires within the test
...

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@easycord/_decorator_stacks.py`:
- Around line 61-63: Update the user-facing response strings in nuke and the
other referenced command examples to pass through ctx.localize(...), preserving
each existing message and response options such as ephemeral=True.
- Around line 148-156: In _decorator_stacks.py, replace timeout_members with
moderate_members in the user permission list and add moderate_members to the
default bot permissions. In tests/test_plugin_helpers.py, update the relevant
assertions to verify the complete expected user and bot permission lists,
including moderate_members. Apply changes at easycord/_decorator_stacks.py lines
148-156 and tests/test_plugin_helpers.py lines 65-72.

In `@easycord/_plugin_config_helper.py`:
- Line 49: Update the command response strings in the relevant plugin
configuration helper methods to use ctx.localize(...) instead of hardcoded text,
including both responses near the Set and Remove operations. Preserve the
existing key and value interpolation while routing each message through the
localization API.
- Line 48: Add an `assert ctx.guild is not None` immediately before each
`ctx.guild.id` access in the affected command methods, including the
`config_set` calls at both locations. Preserve the existing guild configuration
behavior for valid guild invocations.
- Around line 229-235: Update the _apply mutation callback to detect whether
fn(data) returns an awaitable before calling cfg.set_other. Close coroutine
results, raise TypeError for any asynchronous result, and ensure cfg.set_other
is only called for synchronous results so unchanged data is not saved.

In `@easycord/_plugin_lifecycle_helpers.py`:
- Around line 95-109: Update the recurring-task registration flow around loop
and track so an existing active task with the same name is not orphaned when
creating a replacement. Reuse and return the active task, or cancel the
previously tracked task before calling track(name, task), ensuring cancel_all()
retains control of every running recurring task.
- Around line 184-199: The timer registration flow around delayed and the
self.timers assignment must handle duplicate timer_id/grouping keys by
cancelling or rejecting the existing task before storing its replacement. Update
delayed’s finally cleanup to remove the entry only when the stored task is the
task that is finishing, preserving tracking of any replacement for
cancel_timer() and cancel_all().
- Around line 221-223: Align cancel_guild with its documented all-timers scope
by retrieving and cancelling every timer associated with guild_id, rather than
forwarding only the supplied timer_id; alternatively, rename the method to
reflect single-timer cancellation and update its docstring and callers
consistently.
- Around line 135-141: Update the timer scheduling call in the giveaway
lifecycle flow to pass guild_id as the keyword argument expected by
TimerManager.schedule, while retaining message_id as the callback argument for
_end_giveaway. Ensure the timer is registered under its guild grouping key so
per-guild cancellation can locate it.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 792ce096-d33a-42d7-88a2-e74d64036c26

📥 Commits

Reviewing files that changed from the base of the PR and between 931f369 and 9944917.

📒 Files selected for processing (4)
  • easycord/_decorator_stacks.py
  • easycord/_plugin_config_helper.py
  • easycord/_plugin_lifecycle_helpers.py
  • tests/test_plugin_helpers.py
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: Analyze (Python)
  • GitHub Check: coverage
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{py,md}

📄 CodeRabbit inference engine (AGENTS.md)

Before answering questions about EasyCord architecture, module relationships, data flows, or cross-cutting patterns, query graphify-out/graph.json with /graphify; do not manually read source first.

Files:

  • easycord/_plugin_config_helper.py
  • tests/test_plugin_helpers.py
  • easycord/_plugin_lifecycle_helpers.py
  • easycord/_decorator_stacks.py
easycord/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

easycord/**/*.py: Add bot-level behavior to the appropriate _bot_<area>.py mixin rather than directly expanding bot.py; context behavior belongs in _context_<area>.py mixins.
Treat only symbols re-exported from easycord/__init__.py as stable public API; modules prefixed with _ are internal.
Use @ai_tool functions with an explicit ToolSafety permission annotation and register them through ToolRegistry.
Await every ToolLimiter method because all per-tool rate-limiting methods are asynchronous.
Route all per-guild state through the database layer; never store per-guild state directly on the Bot instance.
Use ctx.user or ctx.member; ctx.author does not exist.
Treat ctx.is_admin as a property and do not call it as ctx.is_admin().
Use float("-inf"), rather than 0.0, as the default cooldown sentinel so first-message events pass on fresh runners.

Files:

  • easycord/_plugin_config_helper.py
  • easycord/_plugin_lifecycle_helpers.py
  • easycord/_decorator_stacks.py
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

**/*: Before answering any question about the repository, query the knowledge graph at graphify-out/graph.json using graphify query, graphify path, or graphify explain before opening source, documentation, or context files.
Treat documentation and context files as potentially stale secondary references; prefer knowledge-graph results, do not rebuild the graph unless explicitly asked, and never read the deprecated vault at C:\Users\Tom\Desktop\Wiki.

Files:

  • easycord/_plugin_config_helper.py
  • tests/test_plugin_helpers.py
  • easycord/_plugin_lifecycle_helpers.py
  • easycord/_decorator_stacks.py
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use ctx.user or ctx.member; ctx.author does not exist. Treat ctx.is_admin as a property and never call it as a method.
For guild-only slash commands, narrow ctx.guild with assert ctx.guild is not None; for event handlers that may run in DMs, return when ctx.guild is None.
Never hardcode user-facing command response strings; use ctx.localize(...) for localization.
AI providers must implement the AIProvider protocol, and providers should be lazy-imported from plugins/ai_*.
Use the MiddlewareFn chain for middleware with signature Callable[[Context, next], Awaitable[None]]; outer middleware executes before inner middleware.
Use EventBus.subscribe(event, callback) and .publish(event, **kwargs) for asynchronous plugin pub/sub; listeners execute in registration order and subscriber failures must be logged with handler identity.
Use @deprecated(version, replacement) and @version_introduced(version) from easycord.decorators; deprecations emit DeprecationWarning at call time and include a migration hint.
ToolLimiter methods are asynchronous; always await check_limit(...).
@ai_tool requires an explicit ToolSafety annotation to register a tool.
Route every destructive action in event-path plugins such as @on("message") through one governed method that owns rate limiting, channel narrowing, and Discord error handling; catch at least discord.Forbidden, discord.NotFound, and discord.HTTPException.
Before calling .send() on a channel obtained from ctx or Discord, narrow it with isinstance(channel, SENDABLE_CHANNEL_TYPES) from easycord.helpers.tools.
For config read-modify-write operations, use ServerConfigStore.mutate(guild_id, fn); fn must be synchronous and perform no Discord I/O. A single load() or save() is atomic, but a load-modify-save sequence is not.
sync_commands() raises RuntimeError when removals are detected unless confirm_removals=True is passed explicitly.
For plugin construction in t...

Files:

  • easycord/_plugin_config_helper.py
  • tests/test_plugin_helpers.py
  • easycord/_plugin_lifecycle_helpers.py
  • easycord/_decorator_stacks.py
tests/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

tests/**/*.py: Use pytest and pytest-asyncio with asyncio_mode = "auto"; asynchronous tests do not need manual event-loop setup.
Run the full test suite with pytest tests/; individual tests may be run using their test node IDs.

tests/**/*.py: Prefer EasyCord testing helpers such as invoke, FakeContextBuilder, and PluginTestSuite for command, context, and plugin tests.
Use pytest-asyncio with asyncio_mode = "auto"; do not manually create or manage an event loop in tests.

Files:

  • tests/test_plugin_helpers.py
🔇 Additional comments (8)
easycord/_decorator_stacks.py (1)

27-32: Remove the unused decorator imports.

_cooldown, _require_permissions, and _describe are unused. This duplicates the existing CodeQL finding.

tests/test_plugin_helpers.py (1)

30-62: LGTM!

Also applies to: 74-262

easycord/_plugin_lifecycle_helpers.py (5)

1-72: LGTM!


111-118: LGTM!


151-164: LGTM!


202-219: LGTM!


225-235: LGTM!

easycord/_plugin_config_helper.py (1)

1-36: LGTM!

Also applies to: 57-190, 237-257

Comment thread easycord/_decorator_stacks.py
Comment thread easycord/_decorator_stacks.py
Comment thread easycord/_plugin_config_helper.py
Comment thread easycord/_plugin_config_helper.py
Comment thread easycord/_plugin_config_helper.py
Comment thread easycord/_plugin_lifecycle_helpers.py
Comment thread easycord/_plugin_lifecycle_helpers.py
Comment thread easycord/_plugin_lifecycle_helpers.py
Comment thread easycord/_plugin_lifecycle_helpers.py
@rolling-codes
rolling-codes merged commit 57f7a3b into main Aug 11, 2026
54 checks passed
@rolling-codes
rolling-codes deleted the feat/reduce-boilerplate-core branch August 11, 2026 20:35
rolling-codes added a commit that referenced this pull request Aug 11, 2026
…eout_members) (#131)

## Summary

Follow-up to #129 (merged). Fixes the one **real, still-present** issue
from that PR's review; the rest of the bot-review findings were already
resolved by the fixes merged into #129.

**Bug:** `slash_mod_command()` set its default permissions to
`["kick_members", "ban_members", "timeout_members"]`, but
`timeout_members` is **not** a `discord.Permissions` attribute —
discord.py (2.7.1) calls it **`moderate_members`**. Permissions are
enforced with `getattr(member.guild_permissions, name, False)`, so the
`timeout_members` entry was silently always-`False`, breaking the
intended gate for every command built on this stack.

**Also:** removed three imports CodeQL flagged as unused (`cooldown`,
`require_permissions`, `describe`) — the decorator stacks only call
`slash()`.

## Changes
- `easycord/_decorator_stacks.py`: `timeout_members` →
`moderate_members` (default list + docstring); trim unused imports.
- `tests/test_plugin_helpers.py`: assert `moderate_members` is present
and `timeout_members` is not.

## Test plan
- [x] Decorator-stack tests pass; full suite 1865 passed.
- [x] `ruff --select E9,F63,F7,F82` clean; pyright clean on changed
files.

## Note on the #129 review
CodeRabbit/qodo/augment/Sourcery posted ~46 inline comments on #129, but
most were against its **original** commit and were already fixed before
merge (the `ephemeral or True` bug is now `True if ephemeral is None
else ephemeral`; the TaskManager name-reuse race now has the identity
check; the config helper routes through `store.mutate`; the `config_set`
docstring order and the unused `Plugin` import were corrected). The
remaining valid items were the `moderate_members` bug and the unused
imports fixed here. (The over-broad `bot_permissions` on
`slash_mod_command` and the CodeQL "statement has no effect" notes on
`...` test stubs are low-value and left as-is.)

## Summary by Sourcery

Correct default moderation permissions for slash commands and remove
unused decorator imports.

Bug Fixes:
- Replace the nonexistent timeout_members permission with
moderate_members in the default permissions for slash_mod_command to
ensure moderation commands are properly gated.

Enhancements:
- Clean up _decorator_stacks by trimming unused decorator imports.

Tests:
- Extend plugin helper tests to validate that moderate_members is
included and timeout_members is excluded from slash_mod_command
permissions.

Co-authored-by: tee <Thomas_BIRRELL@proton.me>
@rolling-codes rolling-codes mentioned this pull request Aug 11, 2026
5 tasks
rolling-codes added a commit that referenced this pull request Aug 11, 2026
Release **v5.61.0**, bundling everything merged since v5.60.0.

## Included
- **#128** — offline `easycord try` CLI (run a slash command without
Discord)
- **#129** — plugin ergonomics helpers: `PluginConfigHelper` (atomic
config via `store.mutate`), decorator stacks (`slash_admin_command`
etc.), `TaskManager`/`TimerManager`
- **#130** — `discord_errors()` middleware + `server_stats`
`store.mutate` migration
- **#131** — `slash_mod_command` now uses `moderate_members` (real perm)
instead of the nonexistent `timeout_members`

## Version bump
`5.60.0 → 5.61.0` (minor: additive features + fixes, no breaking
changes) across `pyproject.toml`, `easycord/__init__.py`, `README.md`,
`docs/getting-started.md`; CHANGELOG entry added.

## Gates (per RELEASE.md)
- [x] `check_release_metadata.py` — no drift
- [x] `ruff check ... --select E9,F63,F7,F82` — clean
- [x] `verify_plugin_tests.py` — thresholds met
- [x] `pytest` — 1865 passed
- [ ] `pyright easycord tests` — 12 errors / 135 warnings, all
**pre-existing on main** and unrelated to this release (CI does not gate
on pyright); noted, not introduced here.

After merge: tag `v5.61.0` and publish a GitHub Release with the built
wheel + sdist. PyPI `twine upload` left as a manual step (requires
credentials).

## Summary by Sourcery

Release EasyCord v5.61.0 with new CLI tooling, plugin ergonomics
helpers, Discord error handling middleware, and minor fixes, plus
corresponding version and metadata updates.

New Features:
- Add offline `easycord try` CLI to run registered slash commands
without Discord connectivity.
- Introduce plugin ergonomics helpers including config mixin, decorator
stacks, and task/timer lifecycle managers.
- Add `discord_errors()` middleware to handle common Discord API errors
with user-facing messages.

Bug Fixes:
- Correct `slash_mod_command` to require the real `moderate_members`
permission instead of nonexistent `timeout_members`.
- Fix `TaskManager.track` race that could leave replacement tasks
untracked and uncancellable.
- Remove unused imports in decorator stacks flagged by static analysis.

Enhancements:
- Route `server_stats` configuration writes through
`ServerConfigStore.mutate` for consolidated locking and consistency.

Build:
- Bump project version to 5.61.0 in pyproject, package metadata, and
module `__version__`, and add corresponding changelog entry.
- Update release metadata entries for wheel and source distribution
artifacts.

Documentation:
- Update README and getting-started docs to reference the v5.61.0
release, download URLs, and badges.

Co-authored-by: tee <Thomas_BIRRELL@proton.me>
rolling-codes added a commit that referenced this pull request Aug 11, 2026
The v5.61.0 entry undersold the release — it flattened #129's many
additions into one bullet and omitted the work bundled via the
docs/hardening PR. Expanded to break out the config mixin, five
decorator stacks, and task/timer managers; document the
starboard/suggestions resilience fixes, new giveaway/tickets test
suites, shared-helper refinements, and the CLAUDE.md context framework +
CI workflow refresh. Release-metadata gate still passes.

## Summary by Sourcery

Expand the v5.61.0 changelog entry to fully document the release scope,
including new helpers, resilience fixes, tests, and internal/process
updates.

New Features:
- Document the PluginConfigHelper mixin, pre-composed decorator stacks,
and TaskManager/TimerManager helpers as first-class additions in
v5.61.0.
- Clarify the `easycord try` CLI feature with its formatter and
developer-toolkit documentation reference.

Bug Fixes:
- Describe the starboard and suggestions resilience improvements that
prevent Discord permission/HTTP errors from crashing handlers.
- Call out the corrected `slash_mod_command` permission and TaskManager
race fix as part of the release notes.
- Note removal of unused imports flagged by CodeQL as a minor cleanup.

Enhancements:
- Highlight refinements to shared plugin helpers and the updated
`server_stats` config pattern as guidance for future plugin migrations.

CI:
- Record the refresh of CodeQL, release-drafter, stale, and
Claude-related CI workflows in the changelog.

Documentation:
- Broaden the v5.61.0 changelog section to cover all major features,
fixes, tests, and internal changes.
- Summarize the CLAUDE.md context framework updates, including restored
context, troubleshooting guidance, documentation freshness, and
contributor invariants.

Tests:
- Add a dedicated tests section noting new giveaway and tickets command
suites and expanded coverage for suggestions, shared helpers, CLI, and
middleware.

Chores:
- Capture internal maintenance work such as the CLAUDE.md framework and
CI workflow updates under the release metadata narrative.

Co-authored-by: tee <Thomas_BIRRELL@proton.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request plugin tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants