Blame

75f301 Claude (MCP) 2026-04-24 01:05:39
[api] Create: irc-plugin-design-review
1
# IRC Transport Plugin Design — Rule of Two Review
2
3
**Reviewer**: Design Review Subagent
4
**Date**: 2026-04-23
5
**Design**: `/Users/sderle/code/sherman/.claude/designs/irc-plugin.md`
6
**Baseline**: `sherman/cli_plugin.py`, `sherman/hooks.py`, `sherman/channel.py`, `sherman/main.py`
7
8
---
9
10
## Executive Summary
11
12
**Proceed: YES**
13
14
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.
15
16
---
17
18
## Findings
19
20
### Critical
21
None.
22
23
### Important
24
25
1. **Hook signature mismatch in `send_message`**
26
**Severity**: Important
27
**Location**: IRCPlugin.send_message (line 139-141)
28
**Issue**: The design adds an optional `latency_ms: float | None = None` parameter to `send_message`, but the hookspec in `sherman/hooks.py` (line 46-55) only defines `send_message(self, channel: Channel, text: str) -> None`. Hooks must match the spec exactly.
29
**Impact**: This will cause a runtime error or the hook to not be called. The `latency_ms` parameter should be removed or the hookspec should be updated first. Since no other transport uses this parameter, it should be removed from the design.
30
31
2. **Missing `on_start` fan-out behavior verification**
32
**Severity**: Important
33
**Location**: IRCPlugin.on_start (line 77-95)
34
**Issue**: The design uses `asyncio.create_task()` to launch `_connect_with_retry`. This assumes apluggy calls `on_start` implementations 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.
35
**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 why `create_task` is still needed (e.g., to avoid blocking other plugins during long connection attempts).
36
37
3. **Connection hold strategy is fragile**
38
**Severity**: Important
39
**Location**: IRCPlugin._connect_with_retry (line 122)
40
**Issue**: Using `await 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.
41
**Impact**: Lost connections may not be detected for up to 24 hours. The implementation agent should replace this with a more robust approach: either poll `self._client.connected` in a short loop, or use an `asyncio.Event` that pydle's `on_disconnect` callback sets.
42
43
4. **Ambiguous default port/TLS combination**
44
**Severity**: Important
45
**Location**: Config Format section (lines 202-220)
46
**Issue**: The defaults specify `port: 6667` and `tls: false`. While Libera Chat and most modern IRC networks use port 6697 with TLS, a user who specifies `tls: true` without overriding `port` will attempt TLS on port 6667, which typically fails. Conversely, a user who specifies `port: 6697` without `tls: true` will attempt plain text on the TLS port.
47
**Impact**: User configuration errors. The design should either: (a) add a note that `port` must be coordinated with `tls`, (b) use conditional defaults (default port to 6697 if `tls: true`, else 6667), or (c) document the common combinations explicitly in the config example.
48
49
### Cosmetic
50
51
1. **Config field naming inconsistency**
52
**Severity**: Cosmetic
53
**Location**: Config Format section (line 208)
54
**Issue**: The design uses `channels` (plural) for the list of channels to join, matching the nanobot pattern. However, the config shows `nick` instead of `nickname` (nanobot uses `nickname`). Minor inconsistency with the reference implementation.
55
**Impact**: None — `nick` is clear and concise. If consistency with nanobot is desired, consider using `nickname`, but this is not a functional issue.
56
57
2. **No explicit handling of empty channels list**
58
**Severity**: Cosmetic
59
**Location**: Config defaults table (line 220)
60
**Issue**: The `channels` default 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.
61
**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.
62
63
3. **Test helper could be shared**
64
**Severity**: Cosmetic
65
**Location**: Test Plan (lines 259-270)
66
**Issue**: The `_make_pm_with_registry` helper is identical to the one in `test_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.
67
**Impact**: None — duplication is acceptable for now. Consider extracting to a `tests/conftest.py` fixture if more transport plugins are added later.
68
69
---
70
71
## Architecture Compliance
72
73
### CLIPlugin Pattern
74
**Status**: PASS ✓
75
76
The design correctly follows the `CLIPlugin` pattern:
77
- Constructor takes `pm` only ✓
78
- Stores state in private attributes ✓
79
- Implements `on_start`, `send_message`, `on_stop` hooks ✓
80
- Uses `@hookimpl` decorator (implied) ✓
81
- Checks `channel.matches_transport()` in `send_message`
82
- Uses `asyncio.create_task()` for background work ✓
83
- Cleans up task in `on_stop` with cancel/await pattern ✓
84
85
### Hook Interface
86
**Status**: FAIL on one parameter (see Important finding #1)
87
88
- `on_start(self, config: dict) -> None`
89
- `send_message(self, channel, text: str) -> None` ✗ (adds `latency_ms` parameter)
90
- `on_stop(self) -> None`
91
92
The extra `latency_ms` parameter must be removed or the hookspec updated. Recommend removal since it's not used by any other transport.
93
94
### Channel Integration
95
**Status**: PASS ✓
96
97
- Uses `self.pm.registry.get_or_create("irc", target)`
98
- Passes `Channel` object (not string) to hooks ✓
99
- Uses `channel.matches_transport("irc")`
100
- Uses `channel.scope` (not `channel.id`) when sending ✓
101
- Correctly distinguishes channel messages (`target`) from private messages (`by`) ✓
102
103
### Config Handling
104
**Status**: PASS ✓ (with Important finding #4)
105
106
- Reads from `config.get("irc")`
107
- Returns silently if no IRC config ✓
108
- Provides sensible defaults for all fields ✓
109
- Stores channels list in `_channels` for `on_connect`
110
111
### Error Handling
112
**Status**: PASS ✓ (with Important finding #3)
113
114
- Exponential backoff: 10s → 20s → 40s → ... → 300s cap ✓
115
- Resets delay on successful connect ✓
116
- Suppresses `CancelledError` on shutdown via `_connect_task = None` signal ✓
117
- Drops messages if client not connected (no queuing) ✓
118
- Logs at appropriate levels (debug for expected, warning for failures) ✓
119
120
### split_message Algorithm
121
**Status**: PASS ✓
122
123
- Three-tier splitting: paragraphs → sentences → words ✓
124
- Uses UTF-8 byte length (not character length) ✓
125
- `max_len=400` leaves room for IRC overhead ✓
126
- Hard truncation with `...` for oversized words ✓
127
- Preserves newlines within paragraphs (unlike nanobot) ✓
128
129
### Nanobot Pattern Avoidance
130
**Status**: PASS ✓
131
132
The design correctly avoids nanobot's problematic patterns:
133
- No `_mode_limit` workaround ✓ (design calls this out)
134
- No heartbeat/PING-PONG logic ✓ (pydle handles it)
135
- No `_progress` metadata routing ✓
136
- No rate limiting ✓ (out of scope, correctly omitted)
137
- Direct plugin reference instead of "channel instance" indirection ✓
138
- No `_sanitize_content` that strips newlines ✓
139
140
---
141
142
## Test Plan Completeness
143
144
**Coverage**: 18 tests across 6 sections
145
146
1. **on_start** (3 tests): No config, with config, channel reading ✓
147
2. **IRCClient events** (3 tests): Channel message, self-message ignore, private message ✓
148
3. **send_message** (3 tests): IRC channel, non-IRC ignored, long text split ✓
149
4. **split_message** (6 tests): Short, paragraphs, sentences, words, truncation, UTF-8 ✓
150
5. **on_stop** (2 tests): Cancel task, no-op when no task ✓
151
6. **Reconnection** (1 test): Exponential backoff sequence ✓
152
153
**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.
154
155
---
156
157
## Verification Against Codebase
158
159
All referenced code locations exist and match the design's assumptions:
160
- `sherman/cli_plugin.py` — structure matches design's claims ✓
161
- `sherman/hooks.py` — hookspecs match (except `latency_ms` parameter) ✓
162
- `sherman/channel.py` — Channel, ChannelRegistry, matches_transport exist ✓
163
- `sherman/main.py` — registration location (line 117) is correct ✓
164
- `nanobot_channel_irc/channel.py` — reference patterns are correctly identified ✓
165
166
---
167
168
## Recommendations for Implementation Agent
169
170
1. **Remove `latency_ms` parameter** from `send_message` hook before writing tests. This is required for the tests to pass.
171
172
2. **Verify apluggy's `on_start` concurrency** by writing a small test that creates two plugins with `on_start` implementations that sleep and log when they start/finish. If they run sequentially, document this fact in a code comment. If concurrent, reconsider whether `create_task` is needed.
173
174
3. **Replace `await asyncio.sleep(86400)`** with a more robust connection hold:
175
```python
176
# Poll connection state every 5 seconds
177
while self._client.connected:
178
await asyncio.sleep(5)
179
```
180
Or use an `asyncio.Event` that `on_disconnect` sets.
181
182
4. **Add config validation note** in docstring or code comment about port/TLS coordination.
183
184
5. **Consider adding a debug log** in `on_start` when `channels` list is empty, to inform users they're connecting without joining any channels.
185
186
---
187
188
## Conclusion
189
190
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.
191
192
**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.