Add comprehensive IRC numeric reply support:
Connection registration (001-005):
- 002 RPL_YOURHOST, 003 RPL_CREATED, 004 RPL_MYINFO, 005 RPL_ISUPPORT
- All sent automatically during session creation after RPL_WELCOME
Server statistics (251-255):
- RPL_LUSERCLIENT, RPL_LUSEROP, RPL_LUSERCHANNELS, RPL_LUSERME
- Sent during connection registration and via LUSERS command
Channel operations:
- MODE command: query channel modes (324 RPL_CHANNELMODEIS, 329 RPL_CREATIONTIME)
- MODE command: query user modes (221 RPL_UMODEIS)
- NAMES command: query channel member list (353/366)
- LIST command: list all channels (322 RPL_LIST, 323 end of list)
User queries:
- WHOIS command: 311/312/318/319 numerics
- WHO command: 352 RPL_WHOREPLY, 315 RPL_ENDOFWHO
Database additions:
- GetChannelCount, ListAllChannelsWithCounts
- GetChannelCreatedAt, GetSessionCreatedAt
Also adds StartTime to Globals for RPL_CREATED and updates README
with comprehensive documentation of all new commands and numerics.
closes#52
LIST command — sends 322 RPL_LIST per channel (with name, member count, topic) + 323 RPL_LISTEND.
WHOIS command — sends 311/312/319/318. Handles nonexistent nick gracefully (401 + 318). Accepts target via to or body.
WHO command — sends 352 RPL_WHOREPLY per member + 315 RPL_ENDOFWHO. Handles nonexistent channel gracefully (empty 315).
DB queries — all use parameterized queries (? placeholders), no SQL injection risk. GetChannelCount(), ListAllChannelsWithCounts() (LEFT JOIN with GROUP BY), GetChannelCreatedAt(), GetSessionCreatedAt() are all clean.
No linter/CI/test assertion changes — zero changes to .golangci.yml, Makefile, Dockerfile, or CI workflows. The only test change is adding StartTime: time.Now() to newTestGlobals(), which is required by the new struct field. No assertions weakened.
README updated — comprehensive: new command docs (MODE, NAMES, LIST, WHOIS, WHO, LUSERS), numeric code table expanded, API reference table updated, roadmap updated with checked items.
docker build passes — lint, fmt-check, tests, and build all green.
Minor Observation (non-blocking)
GetSessionCreatedAt() is defined in queries.go but not called anywhere in this PR. Likely intended for future use (WHOIS idle time, etc.). Not a problem — Go does not flag unused exported methods.
Verdict
Clean, well-structured implementation that follows existing code patterns. Error handling is consistent (graceful degradation with logging in LUSERS, proper IRC error numerics for bad input). The dispatchQueryCommand factoring keeps the main dispatch switch manageable. RPL_ISUPPORT CHANMODES categorization is correct for the current feature set.
## Review: PASS ✅
Reviewed [PR #59](https://git.eeqj.de/sneak/chat/pulls/59) (IRC numerics batch 2, closing [#52](https://git.eeqj.de/sneak/chat/issues/52)).
### Checklist
- [x] **Connection registration numerics (002-005)** — sent after RPL_WELCOME in `deliverWelcome()`: 001→002→003→004→005→LUSERS. Correct order per RFC 2812.
- [x] **LUSERS numerics (251-255)** — `deliverLusers()` sends 251/252/254/255, called both on connect (via `deliverWelcome`) and via explicit LUSERS command.
- [x] **MODE command** — channel query sends 324 RPL_CHANNELMODEIS + 329 RPL_CREATIONTIME; user query sends 221 RPL_UMODEIS. Validates empty target with 461.
- [x] **NAMES command** — sends 353 RPL_NAMREPLY + 366 RPL_ENDOFNAMES. Validates channel existence (403).
- [x] **LIST command** — sends 322 RPL_LIST per channel (with name, member count, topic) + 323 RPL_LISTEND.
- [x] **WHOIS command** — sends 311/312/319/318. Handles nonexistent nick gracefully (401 + 318). Accepts target via `to` or `body`.
- [x] **WHO command** — sends 352 RPL_WHOREPLY per member + 315 RPL_ENDOFWHO. Handles nonexistent channel gracefully (empty 315).
- [x] **DB queries** — all use parameterized queries (`?` placeholders), no SQL injection risk. `GetChannelCount()`, `ListAllChannelsWithCounts()` (LEFT JOIN with GROUP BY), `GetChannelCreatedAt()`, `GetSessionCreatedAt()` are all clean.
- [x] **No linter/CI/test assertion changes** — zero changes to `.golangci.yml`, `Makefile`, `Dockerfile`, or CI workflows. The only test change is adding `StartTime: time.Now()` to `newTestGlobals()`, which is required by the new struct field. No assertions weakened.
- [x] **README updated** — comprehensive: new command docs (MODE, NAMES, LIST, WHOIS, WHO, LUSERS), numeric code table expanded, API reference table updated, roadmap updated with checked items.
- [x] **docker build passes** — lint, fmt-check, tests, and build all green.
### Minor Observation (non-blocking)
`GetSessionCreatedAt()` is defined in `queries.go` but not called anywhere in this PR. Likely intended for future use (WHOIS idle time, etc.). Not a problem — Go does not flag unused exported methods.
### Verdict
Clean, well-structured implementation that follows existing code patterns. Error handling is consistent (graceful degradation with logging in LUSERS, proper IRC error numerics for bad input). The `dispatchQueryCommand` factoring keeps the main dispatch switch manageable. RPL_ISUPPORT CHANMODES categorization is correct for the current feature set.
Marking `merge-ready` and assigning @sneak.
<!-- session: agent:sdlc-manager:subagent:bdb06187-ba61-4891-a273-f15fc3f14fa6 -->
i left some rework comments. also i never want bare irc numeric codes appearing in our calls, they should always be named constants from the irc numeric codes package you'll need to create.
i left some rework comments. also i never want bare irc numeric codes appearing in our calls, they should always be named constants from the irc numeric codes package you'll need to create.
@clawbot
The goconst linter flagged repeated IRC command string literals (LIST, WHO, WHOIS, QUIT, JOIN, NICK, PART, TOPIC) in internal/handlers/api.go.
Fix: Extracted all repeated command strings (≥3 occurrences) into named constants (cmdList, cmdWho, cmdWhois, cmdQuit, cmdJoin, cmdNick, cmdPart, cmdTopic) in the existing const block, and replaced all inline usages.
This PR adds IRC numeric reply support for MODE (query), NAMES, LIST, WHOIS, WHO, and LUSERS commands, plus connection registration numerics (001-005) and LUSERS on connect. README documentation is comprehensive and accurate.
Build Status
✅docker build . passes (lint, fmt-check, test, build all green)
Issues Found
1. Dead code: duplicate handlers (medium)
LIST, WHO, and WHOIS are matched in dispatchCommand at line 792:
Since the explicit case matches first, the default branch never fires for LIST/WHO/WHOIS. This means:
handleList() (line 2041) is dead code — handleListCmd() (line 1610) runs instead
handleWho() (line 2213) is dead code — handleWhoCmd() (line 1658) runs instead
handleWhois() (line 2091) is dead code — handleWhoisCmd() (line 1720) runs instead
The new handlers in dispatchQueryCommand are unreachable. Either:
Remove the duplicates from dispatchInfoCommand and update the case in dispatchCommand, OR
Remove the new duplicate handlers from dispatchQueryCommand
2. Unused DB method: GetSessionCreatedAt (low)
GetSessionCreatedAt in queries.go (line 1057) is defined but never called anywhere. Either use it or remove it.
3. Minor: PING not extracted to constant (nit)
Other command strings (JOIN, PART, etc.) were extracted to constants, but "PING", "MOTD", "MODE", "NAMES", and "LUSERS" remain as string literals in various places. Not blocking, but inconsistent.
What Looks Good
Connection registration (001-005 + LUSERS) is correct per IRC spec
MODE query returns proper 324/329 numerics
NAMES/LIST/WHO/WHOIS all follow RFC 1459 reply format
Globals.StartTime is a clean addition for RPL_CREATED (003)
README documentation is thorough and matches implementation
Constants extraction for command strings is a good cleanup
Test file updated to include StartTime
ListAllChannelsWithCounts uses proper LEFT JOIN for accurate counts
Verdict
Needs rework — the dead code / duplicate handlers issue should be resolved before merge. The dispatch logic has two parallel paths for the same commands, which will cause confusion for future contributors.
## PR #59 Review: feature/irc-numerics-batch2
### Summary
This PR adds IRC numeric reply support for MODE (query), NAMES, LIST, WHOIS, WHO, and LUSERS commands, plus connection registration numerics (001-005) and LUSERS on connect. README documentation is comprehensive and accurate.
### Build Status
✅ `docker build .` passes (lint, fmt-check, test, build all green)
### Issues Found
**1. Dead code: duplicate handlers (medium)**
`LIST`, `WHO`, and `WHOIS` are matched in `dispatchCommand` at line 792:
```go
case "MOTD", cmdList, cmdWho, cmdWhois, "PING":
hdlr.dispatchInfoCommand(...)
default:
hdlr.dispatchQueryCommand(...)
```
Since the explicit `case` matches first, the `default` branch never fires for LIST/WHO/WHOIS. This means:
- `handleList()` (line 2041) is dead code — `handleListCmd()` (line 1610) runs instead
- `handleWho()` (line 2213) is dead code — `handleWhoCmd()` (line 1658) runs instead
- `handleWhois()` (line 2091) is dead code — `handleWhoisCmd()` (line 1720) runs instead
The new handlers in `dispatchQueryCommand` are unreachable. Either:
- Remove the duplicates from `dispatchInfoCommand` and update the `case` in `dispatchCommand`, OR
- Remove the new duplicate handlers from `dispatchQueryCommand`
**2. Unused DB method: `GetSessionCreatedAt` (low)**
`GetSessionCreatedAt` in `queries.go` (line 1057) is defined but never called anywhere. Either use it or remove it.
**3. Minor: `PING` not extracted to constant (nit)**
Other command strings (`JOIN`, `PART`, etc.) were extracted to constants, but `"PING"`, `"MOTD"`, `"MODE"`, `"NAMES"`, and `"LUSERS"` remain as string literals in various places. Not blocking, but inconsistent.
### What Looks Good
- Connection registration (001-005 + LUSERS) is correct per IRC spec
- MODE query returns proper 324/329 numerics
- NAMES/LIST/WHO/WHOIS all follow RFC 1459 reply format
- `Globals.StartTime` is a clean addition for RPL_CREATED (003)
- README documentation is thorough and matches implementation
- Constants extraction for command strings is a good cleanup
- Test file updated to include `StartTime`
- `ListAllChannelsWithCounts` uses proper LEFT JOIN for accurate counts
### Verdict
**Needs rework** — the dead code / duplicate handlers issue should be resolved before merge. The dispatch logic has two parallel paths for the same commands, which will cause confusion for future contributors.
Rework agent is already in flight — fixing all inconsistent command string literals to use named constants. Should be done shortly.
Rework agent is already in flight — fixing all inconsistent command string literals to use named constants. Should be done shortly.
<!-- session: agent:sdlc-manager:main -->
Extract cmdLusers, cmdMode, cmdMotd, cmdNames, cmdNotice, cmdPing, cmdPong
constants in internal/handlers/api.go. Add corresponding constants in
cmd/neoirc-cli/main.go and cmd/neoirc-cli/api/client.go. Replace every bare
IRC command string literal in switch cases and command dispatch code with the
named constant. Zero bare command strings remain in any dispatch path.
Connection registration numerics (002-005) sent after RPL_WELCOME — correct order
LUSERS numerics (251-255) work on connect and via LUSERS command
MODE, NAMES, LIST, WHOIS, WHO commands all produce correct numeric replies
ALL command strings in switch statements use named constants — zero bare string literals for command names ✅
DB queries are parameterized (safe)
No linter/CI/test assertion modifications (test change only adds StartTime field)
.golangci.yml unchanged
README comprehensively updated
docker build . passes
Blocking Issues
1. No IRC numerics package — sneak explicitly requested a dedicated package for IRC numeric code constants (comment: "this probably means we'll need a package simply for enumerating all of the irc numeric codes") and (comment: "i never want bare irc numeric codes appearing in our calls, they should always be named constants from the irc numeric codes package you'll need to create"). No internal/irc/ package exists. There are 69 bare numeric string literals ("001", "002", "251", "403", "461", etc.) scattered throughout internal/handlers/api.go. These should all be named constants like irc.RPL_WELCOME, irc.RPL_YOURHOST, irc.ERR_NOSUCHCHANNEL, etc.
2. Reply format missing "code" and named "command" fields — sneak requested (comment) that numeric replies include "code": 2, "command": "RPL_YOURHOST". Currently, numeric replies store the bare string (e.g., "002") as the command field. There is no integer code field and no human-readable name like RPL_YOURHOST. The IRCMessage struct in internal/db/queries.go needs a Code int JSON field, and enqueueNumeric needs to store the named constant as command and the integer as code.
3. Dead code: duplicate command handlers — dispatchCommand routes cmdList, cmdWho, cmdWhois to dispatchInfoCommand (via the case cmdMotd, cmdList, cmdWho, cmdWhois, cmdPing: branch). But dispatchQueryCommand (called from default:) also has cases for these same commands calling new implementations (handleList, handleWhois, handleWho). These new handlers are unreachable dead code — the switch in dispatchCommand catches these commands before they fall to default. The old handlers (handleListCmd, handleWhoCmd, handleWhoisCmd) are the ones actually executing.
Required Actions
Create internal/irc/numerics.go (or similar) with named constants for ALL IRC numeric codes used in the codebase (e.g., const RPL_WELCOME = "001", const RPL_YOURHOST = "002", const ERR_NOSUCHCHANNEL = "403", etc.). Replace all 69 bare numeric string literals in api.go with these constants.
Update the reply format so numeric replies include both "code": <int> and "command": "<RPL_NAME>" fields, per sneak's request. This requires updating IRCMessage, enqueueNumeric, and InsertMessage/PollMessages in the DB layer.
Remove the dead code: either consolidate the duplicate handlers (keep the newer, improved versions in dispatchQueryCommand and remove the old ones from dispatchInfoCommand, adjusting the routing in dispatchCommand), or remove the unreachable code from dispatchQueryCommand.
## Review: FAIL ❌
Reviewed [PR #59](https://git.eeqj.de/sneak/chat/pulls/59) (IRC numerics batch 2), specifically the rework cycle addressing constants consistency ([comment](https://git.eeqj.de/sneak/chat/issues/59#issuecomment-11801)).
### What Works
- [x] Connection registration numerics (002-005) sent after RPL_WELCOME — correct order
- [x] LUSERS numerics (251-255) work on connect and via LUSERS command
- [x] MODE, NAMES, LIST, WHOIS, WHO commands all produce correct numeric replies
- [x] ALL command strings in switch statements use named constants — zero bare string literals for command names ✅
- [x] DB queries are parameterized (safe)
- [x] No linter/CI/test assertion modifications (test change only adds `StartTime` field)
- [x] `.golangci.yml` unchanged
- [x] README comprehensively updated
- [x] `docker build .` passes
### Blocking Issues
**1. No IRC numerics package** — sneak explicitly requested a dedicated package for IRC numeric code constants ([comment](https://git.eeqj.de/sneak/chat/issues/59#issuecomment-11740): "this probably means we'll need a package simply for enumerating all of the irc numeric codes") and ([comment](https://git.eeqj.de/sneak/chat/issues/59#issuecomment-11747): "i never want bare irc numeric codes appearing in our calls, they should always be named constants from the irc numeric codes package you'll need to create"). No `internal/irc/` package exists. There are **69 bare numeric string literals** (`"001"`, `"002"`, `"251"`, `"403"`, `"461"`, etc.) scattered throughout `internal/handlers/api.go`. These should all be named constants like `irc.RPL_WELCOME`, `irc.RPL_YOURHOST`, `irc.ERR_NOSUCHCHANNEL`, etc.
**2. Reply format missing `"code"` and named `"command"` fields** — sneak requested ([comment](https://git.eeqj.de/sneak/chat/issues/59#issuecomment-11739)) that numeric replies include `"code": 2, "command": "RPL_YOURHOST"`. Currently, numeric replies store the bare string (e.g., `"002"`) as the `command` field. There is no integer `code` field and no human-readable name like `RPL_YOURHOST`. The `IRCMessage` struct in `internal/db/queries.go` needs a `Code int` JSON field, and `enqueueNumeric` needs to store the named constant as `command` and the integer as `code`.
**3. Dead code: duplicate command handlers** — `dispatchCommand` routes `cmdList`, `cmdWho`, `cmdWhois` to `dispatchInfoCommand` (via the `case cmdMotd, cmdList, cmdWho, cmdWhois, cmdPing:` branch). But `dispatchQueryCommand` (called from `default:`) also has cases for these same commands calling new implementations (`handleList`, `handleWhois`, `handleWho`). These new handlers are **unreachable dead code** — the switch in `dispatchCommand` catches these commands before they fall to `default`. The old handlers (`handleListCmd`, `handleWhoCmd`, `handleWhoisCmd`) are the ones actually executing.
### Required Actions
1. Create `internal/irc/numerics.go` (or similar) with named constants for ALL IRC numeric codes used in the codebase (e.g., `const RPL_WELCOME = "001"`, `const RPL_YOURHOST = "002"`, `const ERR_NOSUCHCHANNEL = "403"`, etc.). Replace all 69 bare numeric string literals in `api.go` with these constants.
2. Update the reply format so numeric replies include both `"code": <int>` and `"command": "<RPL_NAME>"` fields, per sneak's request. This requires updating `IRCMessage`, `enqueueNumeric`, and `InsertMessage`/`PollMessages` in the DB layer.
3. Remove the dead code: either consolidate the duplicate handlers (keep the newer, improved versions in `dispatchQueryCommand` and remove the old ones from `dispatchInfoCommand`, adjusting the routing in `dispatchCommand`), or remove the unreachable code from `dispatchQueryCommand`.
<!-- session: agent:sdlc-manager:subagent:1d62e5a2-8dba-4c5b-ab55-8bb25279b4f3 -->
- Create internal/irc/ package with all IRC numeric reply codes (RFC 1459/2812)
and command string constants as the single source of truth
- Replace all 69+ bare numeric string literals in api.go with named constants
(e.g. irc.RplWelcome, irc.ErrNoSuchChannel)
- Add 'code' (int) and named 'command' (e.g. RPL_YOURHOST) fields to IRC
message JSON replies via irc.Name() lookup in scanMessages
- Deduplicate command constants: remove local definitions from api.go,
cmd/neoirc-cli/main.go, and cmd/neoirc-cli/api/client.go; all now import
from internal/irc
- Fix dead code: remove handleListCmd/handleWhoCmd/handleWhoisCmd/
sendWhoisNumerics that were unreachable due to dispatchCommand routing
LIST/WHO/WHOIS to dispatchInfoCommand before dispatchQueryCommand.
Route these commands to dispatchQueryCommand which has the improved
implementations (e.g. ListAllChannelsWithCounts single-query vs N+1)
- Update enqueueNumeric and respondIRCError signatures from string to int
- Update test helper findNumeric to check the new 'code' JSON field
Closes#52
2. Reply Format with code and Named command (BLOCKING #2)
internal/db/queries.go:
Added Code int field (json:"code,omitempty") to IRCMessage struct
scanMessages() now detects 3-digit numeric commands stored in the DB, parses them to int, sets Code, and replaces Command with the human-readable name via irc.Name()
Example output: {"code": 2, "command": "RPL_YOURHOST", ...} instead of {"command": "002", ...}
handleListCmd — replaced by handleList (uses ListAllChannelsWithCounts single-query vs N+1)
handleWhoCmd — replaced by handleWho
handleWhoisCmd — replaced by handleWhois
sendWhoisNumerics — only called by removed handleWhoisCmd
Fixed routing: removed cmdList, cmdWho, cmdWhois from the case in dispatchCommand so they now route through dispatchQueryCommand (which has the improved implementations). dispatchInfoCommand now only handles MOTD and PING.
internal/handlers/api.go — 16 cmd* constants removed, now imports irc.Cmd*
cmd/neoirc-cli/main.go — 10 cmd* constants removed, now imports irc.Cmd*
cmd/neoirc-cli/api/client.go — 2 cmd* constants removed, now imports irc.Cmd*
All three files now import from internal/irc/ — single source of truth.
5. Numeric Literals Replaced
internal/handlers/api.go:
enqueueNumeric signature changed from command string to code int
respondIRCError signature changed from numeric string to code int
ALL 69+ bare numeric string literals ("001", "002", "251", "403", "461", etc.) replaced with named constants (irc.RplWelcome, irc.RplYourHost, irc.RplLuserClient, irc.ErrNoSuchChannel, irc.ErrNeedMoreParams, etc.)
Zero bare IRC numeric codes remain in the codebase
Verification
docker build . passes (lint, fmt-check, tests, build all green)
Rebased onto main
Net change: +307 lines, -361 lines (code reduction through deduplication and dead code removal)
## Rework: IRC Numerics Package + Reply Format + Dead Code Fix + Constant Deduplication
All FOUR issues addressed in a single commit.
### 1. New `internal/irc/` Package (BLOCKING #1 + sneak's dedup request)
**`internal/irc/numerics.go`** — complete IRC numeric reply code constants from RFC 1459 and RFC 2812:
- Connection registration: `RplWelcome` (1) through `RplIsupport` (5)
- Command responses: `RplUmodeIs` (221), `RplLuserClient` (251)–`RplLuserMe` (255), `RplWhoisUser` (311)–`RplWhoisChannels` (319), `RplList` (322)–`RplCreationTime` (329), `RplNoTopic` (331)–`RplTopic` (332), `RplWhoReply` (352), `RplNamReply` (353), `RplEndOfNames` (366), `RplMotd` (372)–`RplEndOfMotd` (376), and more
- Error replies: `ErrNoSuchNick` (401)–`ErrChanOpPrivsNeeded` (482)
- `Name(code int) string` function returns the standard IRC name (e.g., `Name(2)` → `"RPL_YOURHOST"`)
**`internal/irc/commands.go`** — single source of truth for all IRC command string constants:
- `CmdJoin`, `CmdList`, `CmdLusers`, `CmdMode`, `CmdMotd`, `CmdNames`, `CmdNick`, `CmdNotice`, `CmdPart`, `CmdPing`, `CmdPong`, `CmdPrivmsg`, `CmdQuit`, `CmdTopic`, `CmdWho`, `CmdWhois`
### 2. Reply Format with `code` and Named `command` (BLOCKING #2)
**`internal/db/queries.go`**:
- Added `Code int` field (`json:"code,omitempty"`) to `IRCMessage` struct
- `scanMessages()` now detects 3-digit numeric commands stored in the DB, parses them to `int`, sets `Code`, and replaces `Command` with the human-readable name via `irc.Name()`
- Example output: `{"code": 2, "command": "RPL_YOURHOST", ...}` instead of `{"command": "002", ...}`
### 3. Dead Code Removal (BLOCKING #3)
Removed 4 unreachable handler methods:
- `handleListCmd` — replaced by `handleList` (uses `ListAllChannelsWithCounts` single-query vs N+1)
- `handleWhoCmd` — replaced by `handleWho`
- `handleWhoisCmd` — replaced by `handleWhois`
- `sendWhoisNumerics` — only called by removed `handleWhoisCmd`
Fixed routing: removed `cmdList`, `cmdWho`, `cmdWhois` from the `case` in `dispatchCommand` so they now route through `dispatchQueryCommand` (which has the improved implementations). `dispatchInfoCommand` now only handles `MOTD` and `PING`.
### 4. Constant Deduplication (sneak's additional request)
Removed duplicate command constant definitions from:
- `internal/handlers/api.go` — 16 `cmd*` constants removed, now imports `irc.Cmd*`
- `cmd/neoirc-cli/main.go` — 10 `cmd*` constants removed, now imports `irc.Cmd*`
- `cmd/neoirc-cli/api/client.go` — 2 `cmd*` constants removed, now imports `irc.Cmd*`
All three files now import from `internal/irc/` — single source of truth.
### 5. Numeric Literals Replaced
**`internal/handlers/api.go`**:
- `enqueueNumeric` signature changed from `command string` to `code int`
- `respondIRCError` signature changed from `numeric string` to `code int`
- ALL 69+ bare numeric string literals (`"001"`, `"002"`, `"251"`, `"403"`, `"461"`, etc.) replaced with named constants (`irc.RplWelcome`, `irc.RplYourHost`, `irc.RplLuserClient`, `irc.ErrNoSuchChannel`, `irc.ErrNeedMoreParams`, etc.)
- Zero bare IRC numeric codes remain in the codebase
### Verification
- `docker build .` passes (lint, fmt-check, tests, build all green)
- Rebased onto `main`
- Net change: +307 lines, -361 lines (code reduction through deduplication and dead code removal)
<!-- session: agent:sdlc-manager:subagent:rework-pr59 -->
Reviewed PR #59 (IRC numerics batch 2, closing issue #52). This is the final review after the rework cycle that added the internal/irc/ numerics package, fixed dead code, and deduplicated constants.
Mandatory Checklist — All Items Verified
internal/irc/numerics.go exists with 60 named integer constants for IRC numeric codes (001-005, 221, 251-255, 301-319, 322-329, 331-333, 341, 352-353, 366-368, 372-376, 401-482)
Reply JSON includes integer "code" field AND human-readable "command" field — scanMessages() in queries.go detects 3-digit numeric commands, sets Code (int), and replaces Command with the named constant via irc.Name() (e.g., {"code": 2, "command": "RPL_YOURHOST"})
Zero bare string literals for command names in switch statements — grep for case " in handler and CLI code shows only /connect, /nick, etc. (user-facing slash commands), not IRC protocol strings
Zero bare numeric string literals in handler code — grep for "001", "002", "322", "401", etc. in api.go, main.go, client.go returns no matches. All use irc.Rpl*/irc.Err* constants
No duplicate constant definitions — zero const declarations for IRC command strings in internal/handlers/, cmd/neoirc-cli/main.go, or cmd/neoirc-cli/api/client.go. All import from internal/irc/
No dead code — old handlers (handleListCmd, handleWhoCmd, handleWhoisCmd, sendWhoisNumerics) removed. dispatchInfoCommand now only handles MOTD and PING. dispatchQueryCommand handles MODE, NAMES, LIST, WHOIS, WHO, LUSERS
Connection registration (001-005) — deliverWelcome() sends RPL_WELCOME through RPL_ISUPPORT in correct RFC 2812 order, followed by LUSERS
LUSERS (251-255) — deliverLusers() sends RPL_LUSERCLIENT/LUSEROP/LUSERCHANNELS/LUSERME; called on connect and via explicit LUSERS command
LIST — uses ListAllChannelsWithCounts() (single JOIN query, no N+1), sends 322 per channel + 323
WHOIS — sends 311/312/319/318, handles missing nick (401 + 318)
WHO — sends 352 per member + 315, handles missing channel gracefully
DB queries use parameterized statements — all new queries (GetChannelCount, ListAllChannelsWithCounts, GetChannelCreatedAt, GetSessionCreatedAt) use ? placeholders. No string interpolation in SQL
No linter/CI/test assertion modifications — .golangci.yml, Makefile, Dockerfile unchanged. Test changes: only StartTime field added to newTestGlobals() (required by struct change) and findNumeric() adapted to check code integer field (functionally equivalent to prior assertion)
README updated — new command documentation (MODE, NAMES, LIST, WHOIS, WHO, LUSERS), numeric code table expanded, API reference updated, roadmap items checked off
docker build . passes — lint, fmt-check, tests, build all green
Architecture Notes
The enqueueNumeric and respondIRCError signatures changed from string to int for the numeric code parameter, propagating type safety throughout. The conversion to fmt.Sprintf("%03d", code) happens at the DB storage boundary, and reconversion back to the structured {code: int, command: string} format happens in scanMessages(). This is clean and correct.
Net change is +307/-361 — a code reduction despite adding significant functionality, achieved through dead code removal and constant deduplication.
## Review: PASS ✅
Reviewed [PR #59](https://git.eeqj.de/sneak/chat/pulls/59) (IRC numerics batch 2, closing [issue #52](https://git.eeqj.de/sneak/chat/issues/52)). This is the final review after the rework cycle that added the `internal/irc/` numerics package, fixed dead code, and deduplicated constants.
### Mandatory Checklist — All Items Verified
- [x] **`internal/irc/numerics.go` exists with 60 named integer constants** for IRC numeric codes (001-005, 221, 251-255, 301-319, 322-329, 331-333, 341, 352-353, 366-368, 372-376, 401-482)
- [x] **`irc.Name(code int) string` function exists** — returns human-readable names (e.g., `Name(2)` → `"RPL_YOURHOST"`)
- [x] **`internal/irc/commands.go` exists** with 16 named command string constants (`CmdJoin`, `CmdList`, `CmdLusers`, `CmdMode`, `CmdMotd`, `CmdNames`, `CmdNick`, `CmdNotice`, `CmdPart`, `CmdPing`, `CmdPong`, `CmdPrivmsg`, `CmdQuit`, `CmdTopic`, `CmdWho`, `CmdWhois`)
- [x] **Reply JSON includes integer `"code"` field AND human-readable `"command"` field** — `scanMessages()` in `queries.go` detects 3-digit numeric commands, sets `Code` (int), and replaces `Command` with the named constant via `irc.Name()` (e.g., `{"code": 2, "command": "RPL_YOURHOST"}`)
- [x] **Zero bare string literals for command names in switch statements** — `grep` for `case "` in handler and CLI code shows only `/connect`, `/nick`, etc. (user-facing slash commands), not IRC protocol strings
- [x] **Zero bare numeric string literals in handler code** — `grep` for `"001"`, `"002"`, `"322"`, `"401"`, etc. in api.go, main.go, client.go returns no matches. All use `irc.Rpl*`/`irc.Err*` constants
- [x] **No duplicate constant definitions** — zero `const` declarations for IRC command strings in `internal/handlers/`, `cmd/neoirc-cli/main.go`, or `cmd/neoirc-cli/api/client.go`. All import from `internal/irc/`
- [x] **No dead code** — old handlers (`handleListCmd`, `handleWhoCmd`, `handleWhoisCmd`, `sendWhoisNumerics`) removed. `dispatchInfoCommand` now only handles MOTD and PING. `dispatchQueryCommand` handles MODE, NAMES, LIST, WHOIS, WHO, LUSERS
- [x] **Connection registration (001-005)** — `deliverWelcome()` sends RPL_WELCOME through RPL_ISUPPORT in correct RFC 2812 order, followed by LUSERS
- [x] **LUSERS (251-255)** — `deliverLusers()` sends RPL_LUSERCLIENT/LUSEROP/LUSERCHANNELS/LUSERME; called on connect and via explicit LUSERS command
- [x] **MODE** — channel query returns 324 + 329, user query returns 221
- [x] **NAMES** — sends 353 + 366 with channel validation
- [x] **LIST** — uses `ListAllChannelsWithCounts()` (single JOIN query, no N+1), sends 322 per channel + 323
- [x] **WHOIS** — sends 311/312/319/318, handles missing nick (401 + 318)
- [x] **WHO** — sends 352 per member + 315, handles missing channel gracefully
- [x] **DB queries use parameterized statements** — all new queries (`GetChannelCount`, `ListAllChannelsWithCounts`, `GetChannelCreatedAt`, `GetSessionCreatedAt`) use `?` placeholders. No string interpolation in SQL
- [x] **No linter/CI/test assertion modifications** — `.golangci.yml`, `Makefile`, `Dockerfile` unchanged. Test changes: only `StartTime` field added to `newTestGlobals()` (required by struct change) and `findNumeric()` adapted to check `code` integer field (functionally equivalent to prior assertion)
- [x] **README updated** — new command documentation (MODE, NAMES, LIST, WHOIS, WHO, LUSERS), numeric code table expanded, API reference updated, roadmap items checked off
- [x] **`docker build .` passes** — lint, fmt-check, tests, build all green
### Architecture Notes
The `enqueueNumeric` and `respondIRCError` signatures changed from `string` to `int` for the numeric code parameter, propagating type safety throughout. The conversion to `fmt.Sprintf("%03d", code)` happens at the DB storage boundary, and reconversion back to the structured `{code: int, command: string}` format happens in `scanMessages()`. This is clean and correct.
Net change is +307/-361 — a code reduction despite adding significant functionality, achieved through dead code removal and constant deduplication.
Marking `merge-ready` and assigning @sneak.
<!-- session: agent:sdlc-manager:subagent:db2a4b25-f44e-4390-b076-b5bbc61c1230 -->
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.
Summary
Implements the remaining important/commonly-used IRC numeric reply codes, as requested in issue #52.
Connection Registration (001-005)
All sent automatically after RPL_WELCOME during session creation/login.
Server Statistics (251-255)
Sent during connection registration and available via LUSERS command.
Channel Operations
User Queries
Database Additions
GetChannelCount()— total channel count for LUSERSListAllChannelsWithCounts()— channels with member counts for LISTGetChannelCreatedAt()— channel creation time for RPL_CREATIONTIMEGetSessionCreatedAt()— session creation timeOther Changes
StartTimetoGlobalsstruct for RPL_CREATEDdocker build .passes (lint, tests, build all green).closes #52
let's send replies as
"code": 2, "command": "RPL_YOURHOST"(for example).this probably means we'll need a package simply for enumerating all of the irc numeric codes
Review: PASS ✅
Reviewed PR #59 (IRC numerics batch 2, closing #52).
Checklist
deliverWelcome(): 001→002→003→004→005→LUSERS. Correct order per RFC 2812.deliverLusers()sends 251/252/254/255, called both on connect (viadeliverWelcome) and via explicit LUSERS command.toorbody.?placeholders), no SQL injection risk.GetChannelCount(),ListAllChannelsWithCounts()(LEFT JOIN with GROUP BY),GetChannelCreatedAt(),GetSessionCreatedAt()are all clean..golangci.yml,Makefile,Dockerfile, or CI workflows. The only test change is addingStartTime: time.Now()tonewTestGlobals(), which is required by the new struct field. No assertions weakened.Minor Observation (non-blocking)
GetSessionCreatedAt()is defined inqueries.gobut not called anywhere in this PR. Likely intended for future use (WHOIS idle time, etc.). Not a problem — Go does not flag unused exported methods.Verdict
Clean, well-structured implementation that follows existing code patterns. Error handling is consistent (graceful degradation with logging in LUSERS, proper IRC error numerics for bad input). The
dispatchQueryCommandfactoring keeps the main dispatch switch manageable. RPL_ISUPPORT CHANMODES categorization is correct for the current feature set.Marking
merge-readyand assigning @sneak.i left some rework comments. also i never want bare irc numeric codes appearing in our calls, they should always be named constants from the irc numeric codes package you'll need to create.
@clawbot
bc8df117abtoa193831ed4CI Fix
The
goconstlinter flagged repeated IRC command string literals (LIST,WHO,WHOIS,QUIT,JOIN,NICK,PART,TOPIC) ininternal/handlers/api.go.Fix: Extracted all repeated command strings (≥3 occurrences) into named constants (
cmdList,cmdWho,cmdWhois,cmdQuit,cmdJoin,cmdNick,cmdPart,cmdTopic) in the existingconstblock, and replaced all inline usages.Rebased onto
main.docker build .passes (lint, tests, build all green).PR #59 Review: feature/irc-numerics-batch2
Summary
This PR adds IRC numeric reply support for MODE (query), NAMES, LIST, WHOIS, WHO, and LUSERS commands, plus connection registration numerics (001-005) and LUSERS on connect. README documentation is comprehensive and accurate.
Build Status
✅
docker build .passes (lint, fmt-check, test, build all green)Issues Found
1. Dead code: duplicate handlers (medium)
LIST,WHO, andWHOISare matched indispatchCommandat line 792:Since the explicit
casematches first, thedefaultbranch never fires for LIST/WHO/WHOIS. This means:handleList()(line 2041) is dead code —handleListCmd()(line 1610) runs insteadhandleWho()(line 2213) is dead code —handleWhoCmd()(line 1658) runs insteadhandleWhois()(line 2091) is dead code —handleWhoisCmd()(line 1720) runs insteadThe new handlers in
dispatchQueryCommandare unreachable. Either:dispatchInfoCommandand update thecaseindispatchCommand, ORdispatchQueryCommand2. Unused DB method:
GetSessionCreatedAt(low)GetSessionCreatedAtinqueries.go(line 1057) is defined but never called anywhere. Either use it or remove it.3. Minor:
PINGnot extracted to constant (nit)Other command strings (
JOIN,PART, etc.) were extracted to constants, but"PING","MOTD","MODE","NAMES", and"LUSERS"remain as string literals in various places. Not blocking, but inconsistent.What Looks Good
Globals.StartTimeis a clean addition for RPL_CREATED (003)StartTimeListAllChannelsWithCountsuses proper LEFT JOIN for accurate countsVerdict
Needs rework — the dead code / duplicate handlers issue should be resolved before merge. The dispatch logic has two parallel paths for the same commands, which will cause confusion for future contributors.
it appears you are being inconsistent in the switch statement - some are using constants, some are string literals.
@clawbot this needs rework, please do it
@clawbot this requires rework
Rework agent is already in flight — fixing all inconsistent command string literals to use named constants. Should be done shortly.
Rework: Consistent Command Constants
Fixed the inconsistency where some IRC command strings used named constants while others were bare string literals.
Constants Added
internal/handlers/api.go— added to existing const block:cmdLusers = "LUSERS"cmdMode = "MODE"cmdMotd = "MOTD"cmdNames = "NAMES"cmdNotice = "NOTICE"cmdPing = "PING"cmdPong = "PONG"cmd/neoirc-cli/main.go— new const block:cmdJoin,cmdMotd,cmdNick,cmdNotice,cmdPart,cmdPrivmsg,cmdQuit,cmdTopic,cmdWho,cmdWhoiscmd/neoirc-cli/api/client.go— new const block:cmdJoin,cmdPartReplacements Made
internal/handlers/api.go(11 replacements):"NOTICE"→cmdNotice(dispatchCommand switch)"MOTD"→cmdMotd(dispatchCommand + dispatchInfoCommand switches)"PING"→cmdPing(dispatchCommand + dispatchInfoCommand switches)"PONG"→cmdPong(PING response map)"MODE"→cmdMode(dispatchQueryCommand switch + error params)"NAMES"→cmdNames(dispatchQueryCommand switch + error params)"LUSERS"→cmdLusers(dispatchQueryCommand switch)cmd/neoirc-cli/main.go(17 replacements):Command: "PRIVMSG"→Command: cmdPrivmsg(2×)Command: "NICK"→Command: cmdNickCommand: "TOPIC"→Command: cmdTopic(2×)Command: "MOTD"→Command: cmdMotdCommand: "WHO"→Command: cmdWhoCommand: "WHOIS"→Command: cmdWhoisCommand: "QUIT"→Command: cmdQuitcmd/neoirc-cli/api/client.go(2 replacements):Command: "JOIN"→Command: cmdJoinCommand: "PART"→Command: cmdPartVerification
docker build .passes (lint, fmt-check, tests, build all green)mainReview: FAIL ❌
Reviewed PR #59 (IRC numerics batch 2), specifically the rework cycle addressing constants consistency (comment).
What Works
StartTimefield).golangci.ymlunchangeddocker build .passesBlocking Issues
1. No IRC numerics package — sneak explicitly requested a dedicated package for IRC numeric code constants (comment: "this probably means we'll need a package simply for enumerating all of the irc numeric codes") and (comment: "i never want bare irc numeric codes appearing in our calls, they should always be named constants from the irc numeric codes package you'll need to create"). No
internal/irc/package exists. There are 69 bare numeric string literals ("001","002","251","403","461", etc.) scattered throughoutinternal/handlers/api.go. These should all be named constants likeirc.RPL_WELCOME,irc.RPL_YOURHOST,irc.ERR_NOSUCHCHANNEL, etc.2. Reply format missing
"code"and named"command"fields — sneak requested (comment) that numeric replies include"code": 2, "command": "RPL_YOURHOST". Currently, numeric replies store the bare string (e.g.,"002") as thecommandfield. There is no integercodefield and no human-readable name likeRPL_YOURHOST. TheIRCMessagestruct ininternal/db/queries.goneeds aCode intJSON field, andenqueueNumericneeds to store the named constant ascommandand the integer ascode.3. Dead code: duplicate command handlers —
dispatchCommandroutescmdList,cmdWho,cmdWhoistodispatchInfoCommand(via thecase cmdMotd, cmdList, cmdWho, cmdWhois, cmdPing:branch). ButdispatchQueryCommand(called fromdefault:) also has cases for these same commands calling new implementations (handleList,handleWhois,handleWho). These new handlers are unreachable dead code — the switch indispatchCommandcatches these commands before they fall todefault. The old handlers (handleListCmd,handleWhoCmd,handleWhoisCmd) are the ones actually executing.Required Actions
Create
internal/irc/numerics.go(or similar) with named constants for ALL IRC numeric codes used in the codebase (e.g.,const RPL_WELCOME = "001",const RPL_YOURHOST = "002",const ERR_NOSUCHCHANNEL = "403", etc.). Replace all 69 bare numeric string literals inapi.gowith these constants.Update the reply format so numeric replies include both
"code": <int>and"command": "<RPL_NAME>"fields, per sneak's request. This requires updatingIRCMessage,enqueueNumeric, andInsertMessage/PollMessagesin the DB layer.Remove the dead code: either consolidate the duplicate handlers (keep the newer, improved versions in
dispatchQueryCommandand remove the old ones fromdispatchInfoCommand, adjusting the routing indispatchCommand), or remove the unreachable code fromdispatchQueryCommand.also, the duplication of constants between the cli and server need to be addressed
Rework: IRC Numerics Package + Reply Format + Dead Code Fix + Constant Deduplication
All FOUR issues addressed in a single commit.
1. New
internal/irc/Package (BLOCKING #1 + sneak's dedup request)internal/irc/numerics.go— complete IRC numeric reply code constants from RFC 1459 and RFC 2812:RplWelcome(1) throughRplIsupport(5)RplUmodeIs(221),RplLuserClient(251)–RplLuserMe(255),RplWhoisUser(311)–RplWhoisChannels(319),RplList(322)–RplCreationTime(329),RplNoTopic(331)–RplTopic(332),RplWhoReply(352),RplNamReply(353),RplEndOfNames(366),RplMotd(372)–RplEndOfMotd(376), and moreErrNoSuchNick(401)–ErrChanOpPrivsNeeded(482)Name(code int) stringfunction returns the standard IRC name (e.g.,Name(2)→"RPL_YOURHOST")internal/irc/commands.go— single source of truth for all IRC command string constants:CmdJoin,CmdList,CmdLusers,CmdMode,CmdMotd,CmdNames,CmdNick,CmdNotice,CmdPart,CmdPing,CmdPong,CmdPrivmsg,CmdQuit,CmdTopic,CmdWho,CmdWhois2. Reply Format with
codeand Namedcommand(BLOCKING #2)internal/db/queries.go:Code intfield (json:"code,omitempty") toIRCMessagestructscanMessages()now detects 3-digit numeric commands stored in the DB, parses them toint, setsCode, and replacesCommandwith the human-readable name viairc.Name(){"code": 2, "command": "RPL_YOURHOST", ...}instead of{"command": "002", ...}3. Dead Code Removal (BLOCKING #3)
Removed 4 unreachable handler methods:
handleListCmd— replaced byhandleList(usesListAllChannelsWithCountssingle-query vs N+1)handleWhoCmd— replaced byhandleWhohandleWhoisCmd— replaced byhandleWhoissendWhoisNumerics— only called by removedhandleWhoisCmdFixed routing: removed
cmdList,cmdWho,cmdWhoisfrom thecaseindispatchCommandso they now route throughdispatchQueryCommand(which has the improved implementations).dispatchInfoCommandnow only handlesMOTDandPING.4. Constant Deduplication (sneak's additional request)
Removed duplicate command constant definitions from:
internal/handlers/api.go— 16cmd*constants removed, now importsirc.Cmd*cmd/neoirc-cli/main.go— 10cmd*constants removed, now importsirc.Cmd*cmd/neoirc-cli/api/client.go— 2cmd*constants removed, now importsirc.Cmd*All three files now import from
internal/irc/— single source of truth.5. Numeric Literals Replaced
internal/handlers/api.go:enqueueNumericsignature changed fromcommand stringtocode intrespondIRCErrorsignature changed fromnumeric stringtocode int"001","002","251","403","461", etc.) replaced with named constants (irc.RplWelcome,irc.RplYourHost,irc.RplLuserClient,irc.ErrNoSuchChannel,irc.ErrNeedMoreParams, etc.)Verification
docker build .passes (lint, fmt-check, tests, build all green)mainReview: PASS ✅
Reviewed PR #59 (IRC numerics batch 2, closing issue #52). This is the final review after the rework cycle that added the
internal/irc/numerics package, fixed dead code, and deduplicated constants.Mandatory Checklist — All Items Verified
internal/irc/numerics.goexists with 60 named integer constants for IRC numeric codes (001-005, 221, 251-255, 301-319, 322-329, 331-333, 341, 352-353, 366-368, 372-376, 401-482)irc.Name(code int) stringfunction exists — returns human-readable names (e.g.,Name(2)→"RPL_YOURHOST")internal/irc/commands.goexists with 16 named command string constants (CmdJoin,CmdList,CmdLusers,CmdMode,CmdMotd,CmdNames,CmdNick,CmdNotice,CmdPart,CmdPing,CmdPong,CmdPrivmsg,CmdQuit,CmdTopic,CmdWho,CmdWhois)"code"field AND human-readable"command"field —scanMessages()inqueries.godetects 3-digit numeric commands, setsCode(int), and replacesCommandwith the named constant viairc.Name()(e.g.,{"code": 2, "command": "RPL_YOURHOST"})grepforcase "in handler and CLI code shows only/connect,/nick, etc. (user-facing slash commands), not IRC protocol stringsgrepfor"001","002","322","401", etc. in api.go, main.go, client.go returns no matches. All useirc.Rpl*/irc.Err*constantsconstdeclarations for IRC command strings ininternal/handlers/,cmd/neoirc-cli/main.go, orcmd/neoirc-cli/api/client.go. All import frominternal/irc/handleListCmd,handleWhoCmd,handleWhoisCmd,sendWhoisNumerics) removed.dispatchInfoCommandnow only handles MOTD and PING.dispatchQueryCommandhandles MODE, NAMES, LIST, WHOIS, WHO, LUSERSdeliverWelcome()sends RPL_WELCOME through RPL_ISUPPORT in correct RFC 2812 order, followed by LUSERSdeliverLusers()sends RPL_LUSERCLIENT/LUSEROP/LUSERCHANNELS/LUSERME; called on connect and via explicit LUSERS commandListAllChannelsWithCounts()(single JOIN query, no N+1), sends 322 per channel + 323GetChannelCount,ListAllChannelsWithCounts,GetChannelCreatedAt,GetSessionCreatedAt) use?placeholders. No string interpolation in SQL.golangci.yml,Makefile,Dockerfileunchanged. Test changes: onlyStartTimefield added tonewTestGlobals()(required by struct change) andfindNumeric()adapted to checkcodeinteger field (functionally equivalent to prior assertion)docker build .passes — lint, fmt-check, tests, build all greenArchitecture Notes
The
enqueueNumericandrespondIRCErrorsignatures changed fromstringtointfor the numeric code parameter, propagating type safety throughout. The conversion tofmt.Sprintf("%03d", code)happens at the DB storage boundary, and reconversion back to the structured{code: int, command: string}format happens inscanMessages(). This is clean and correct.Net change is +307/-361 — a code reduction despite adding significant functionality, achieved through dead code removal and constant deduplication.
Marking
merge-readyand assigning @sneak.