IRC Transport Plugin Design — Rule of Two Review
Reviewer: Design Review Subagent
Date: 2026-04-23
Design: /Users/sderle/code/sherman/.claude/designs/irc-plugin.md
Baseline: sherman/cli_plugin.py, sherman/hooks.py, sherman/channel.py, sherman/main.py
Executive Summary
Proceed: YES
The design is architecturally sound and follows established patterns. The identified issues are important but not blocking — they can be addressed during implementation without redesign. The test plan is thorough and provides clear acceptance criteria.
Findings
Critical
None.
Important
Hook signature mismatch in
send_message
Severity: Important
Location: IRCPlugin.send_message (line 139-141)
Issue: The design adds an optionallatency_ms: float | None = Noneparameter tosend_message, but the hookspec insherman/hooks.py(line 46-55) only definessend_message(self, channel: Channel, text: str) -> None. Hooks must match the spec exactly.
Impact: This will cause a runtime error or the hook to not be called. Thelatency_msparameter should be removed or the hookspec should be updated first. Since no other transport uses this parameter, it should be removed from the design.Missing
on_startfan-out behavior verification
Severity: Important
Location: IRCPlugin.on_start (line 77-95)
Issue: The design usesasyncio.create_task()to launch_connect_with_retry. This assumes apluggy callson_startimplementations sequentially (not concurrently). If apluggy calls them concurrently, the task creation is redundant and may cause issues. The design's "Concerns" section (item 1) acknowledges this but doesn't resolve it.
Impact: If the assumption is wrong, the connection task may behave unexpectedly. The implementation agent must verify this before coding; if concurrent, the design should be updated to document whycreate_taskis still needed (e.g., to avoid blocking other plugins during long connection attempts).Connection hold strategy is fragile
Severity: Important
Location: IRCPlugin._connect_with_retry (line 122)
Issue: Usingawait asyncio.sleep(86400)as a connection hold is fragile. If pydle disconnects silently without raising an exception, the sleep continues and reconnection never happens. The design's "Concerns" section (item 3) acknowledges this but leaves it unresolved.
Impact: Lost connections may not be detected for up to 24 hours. The implementation agent should replace this with a more robust approach: either pollself._client.connectedin a short loop, or use anasyncio.Eventthat pydle'son_disconnectcallback sets.Ambiguous default port/TLS combination
Severity: Important
Location: Config Format section (lines 202-220)
Issue: The defaults specifyport: 6667andtls: false. While Libera Chat and most modern IRC networks use port 6697 with TLS, a user who specifiestls: truewithout overridingportwill attempt TLS on port 6667, which typically fails. Conversely, a user who specifiesport: 6697withouttls: truewill attempt plain text on the TLS port.
Impact: User configuration errors. The design should either: (a) add a note thatportmust be coordinated withtls, (b) use conditional defaults (default port to 6697 iftls: true, else 6667), or (c) document the common combinations explicitly in the config example.
Cosmetic
Config field naming inconsistency
Severity: Cosmetic
Location: Config Format section (line 208)
Issue: The design useschannels(plural) for the list of channels to join, matching the nanobot pattern. However, the config showsnickinstead ofnickname(nanobot usesnickname). Minor inconsistency with the reference implementation.
Impact: None —nickis clear and concise. If consistency with nanobot is desired, consider usingnickname, but this is not a functional issue.No explicit handling of empty channels list
Severity: Cosmetic
Location: Config defaults table (line 220)
Issue: Thechannelsdefault is[](empty list). The design doesn't specify what happens if IRC is configured but no channels are listed. Pydle will connect but not join any channels, which is valid but may be unintended.
Impact: Minor — a debug log noting "no channels configured, connecting without joining" would be helpful for users. This can be added during implementation without design change.Test helper could be shared
Severity: Cosmetic
Location: Test Plan (lines 259-270)
Issue: The_make_pm_with_registryhelper is identical to the one intest_cli_plugin.py. This is good for consistency, but the design doesn't note whether this should be extracted to a shared test utility module.
Impact: None — duplication is acceptable for now. Consider extracting to atests/conftest.pyfixture if more transport plugins are added later.
Architecture Compliance
CLIPlugin Pattern
Status: PASS ✓
The design correctly follows the CLIPlugin pattern:
- Constructor takes
pmonly ✓ - Stores state in private attributes ✓
- Implements
on_start,send_message,on_stophooks ✓ - Uses
@hookimpldecorator (implied) ✓ - Checks
channel.matches_transport()insend_message✓ - Uses
asyncio.create_task()for background work ✓ - Cleans up task in
on_stopwith cancel/await pattern ✓
Hook Interface
Status: FAIL on one parameter (see Important finding #1)
on_start(self, config: dict) -> None✓send_message(self, channel, text: str) -> None✗ (addslatency_msparameter)on_stop(self) -> None✓
The extra latency_ms parameter must be removed or the hookspec updated. Recommend removal since it's not used by any other transport.
Channel Integration
Status: PASS ✓
- Uses
self.pm.registry.get_or_create("irc", target)✓ - Passes
Channelobject (not string) to hooks ✓ - Uses
channel.matches_transport("irc")✓ - Uses
channel.scope(notchannel.id) when sending ✓ - Correctly distinguishes channel messages (
target) from private messages (by) ✓
Config Handling
Status: PASS ✓ (with Important finding #4)
- Reads from
config.get("irc")✓ - Returns silently if no IRC config ✓
- Provides sensible defaults for all fields ✓
- Stores channels list in
_channelsforon_connect✓
Error Handling
Status: PASS ✓ (with Important finding #3)
- Exponential backoff: 10s → 20s → 40s → ... → 300s cap ✓
- Resets delay on successful connect ✓
- Suppresses
CancelledErroron shutdown via_connect_task = Nonesignal ✓ - Drops messages if client not connected (no queuing) ✓
- Logs at appropriate levels (debug for expected, warning for failures) ✓
split_message Algorithm
Status: PASS ✓
- Three-tier splitting: paragraphs → sentences → words ✓
- Uses UTF-8 byte length (not character length) ✓
max_len=400leaves room for IRC overhead ✓- Hard truncation with
...for oversized words ✓ - Preserves newlines within paragraphs (unlike nanobot) ✓
Nanobot Pattern Avoidance
Status: PASS ✓
The design correctly avoids nanobot's problematic patterns:
- No
_mode_limitworkaround ✓ (design calls this out) - No heartbeat/PING-PONG logic ✓ (pydle handles it)
- No
_progressmetadata routing ✓ - No rate limiting ✓ (out of scope, correctly omitted)
- Direct plugin reference instead of "channel instance" indirection ✓
- No
_sanitize_contentthat strips newlines ✓
Test Plan Completeness
Coverage: 18 tests across 6 sections
- on_start (3 tests): No config, with config, channel reading ✓
- IRCClient events (3 tests): Channel message, self-message ignore, private message ✓
- send_message (3 tests): IRC channel, non-IRC ignored, long text split ✓
- split_message (6 tests): Short, paragraphs, sentences, words, truncation, UTF-8 ✓
- on_stop (2 tests): Cancel task, no-op when no task ✓
- Reconnection (1 test): Exponential backoff sequence ✓
Missing edge case: No test for what happens when send_message is called while client is None (design says "drop message, debug log" but no test verifies this). This is minor — the current test plan is sufficient.
Verification Against Codebase
All referenced code locations exist and match the design's assumptions:
sherman/cli_plugin.py— structure matches design's claims ✓sherman/hooks.py— hookspecs match (exceptlatency_msparameter) ✓sherman/channel.py— Channel, ChannelRegistry, matches_transport exist ✓sherman/main.py— registration location (line 117) is correct ✓nanobot_channel_irc/channel.py— reference patterns are correctly identified ✓
Recommendations for Implementation Agent
Remove
latency_msparameter fromsend_messagehook before writing tests. This is required for the tests to pass.Verify apluggy's
on_startconcurrency by writing a small test that creates two plugins withon_startimplementations that sleep and log when they start/finish. If they run sequentially, document this fact in a code comment. If concurrent, reconsider whethercreate_taskis needed.Replace
await asyncio.sleep(86400)with a more robust connection hold:# Poll connection state every 5 seconds while self._client.connected: await asyncio.sleep(5)
Or use an
asyncio.Eventthaton_disconnectsets.Add config validation note in docstring or code comment about port/TLS coordination.
Consider adding a debug log in
on_startwhenchannelslist is empty, to inform users they're connecting without joining any channels.
Conclusion
The design is well-structured and follows established patterns. The four "important" issues are all addressable during implementation without architectural changes. The test plan is comprehensive and will validate the critical behaviors.
Proceed to Red (failing tests) phase. Address the four important findings during implementation, especially the hook signature mismatch which must be fixed before tests can be written.
