Author SHA1 Message Date
clawbot 7674efd2b1 Rewrite AGENTS.md sentence by sentence
check / check (push) Waiting to run
Rewrite the agent rules from scratch, with one idea per sentence and
every file name and command in backticks. Drop the rule that models
embed `models.Base`, which no package has. Say that the linter keeps
tests in external test packages, and that an `export_test.go` file
gives them access to unexported code. Limit the rules on doc comments
and blank imports to files outside the tests. State magic numbers as
a preference, and correct the blank-line rule for `return`. State the
branch workflow: pull requests target `next` and only the owner
merges `next` into `main`. Record the three snake_case JSON tags the
HTTP API keeps.

Part of #190.

Model: fable-5-1 (writing); opus-5-5 (rework)
2026-10-27 13:56:57 +00:00
2 changed files with 135 additions and 175 deletions
+56 -38
View File
@@ -1,55 +1,73 @@
# AGENTS.md — Rules for AI Agents
# Rules for coding agents
## Before Every Commit
This file holds the rules for coding agents working in this repository.
`CONVENTIONS.md` defines the architecture. `REPO_POLICIES.md` defines the
repository standards. Both are copies of shared documents. Do not edit them
here.
1. **Format**: `make fmt`
2. **Check**: `make check` — the tests and the linter, which run in Docker, and
the format check: all passing, zero issues
## Before every commit
Never run `go test`, `golangci-lint` or `gofmt` directly; use the `make`
targets.
1. Run `make fmt`.
2. Run `make check`. It runs the tests and the linter in Docker, then the format
check. All three must pass with no findings.
No commit lands on main with lint errors, test failures, or formatting issues.
Never run `go test`, `golangci-lint` or `gofmt` directly. Use the `make` targets
instead, such as `make test` and `make lint`.
## Code Style
## Branches and pull requests
- Follow CONVENTIONS.md exactly — it defines the architecture
- All exported types, functions, methods, and vars must have doc comments
- All packages must have a package doc comment
- JSON tags use camelCase (not snake_case)
- No magic numbers — use named constants
- No inline error handling (`if err := ...; err != nil`) — use plain assignment
- Blank line before every `return` statement
- Blank line separating embedded fields from regular fields in structs
- Blank imports must have a justification comment
- Never commit to `main` or `next` directly. Every change is a pull request from
a feature branch into `next`.
- Only the owner merges `next` into `main`.
- A pull request that closes an issue has the title `Description (closes #N)`.
- One logical change per commit.
- Commit messages are in the imperative mood and short.
## Git Workflow
## Code style
- **Never commit directly to main** — all changes go on feature branches
- Merge to main only when ready and passing all checks
- PR titles: `Description (closes #N)` when closing an issue
- One logical change per commit
- Commit messages: imperative mood, concise
- Follow `CONVENTIONS.md`.
- Outside test files, every package has a package doc comment.
- Outside test files, every exported type, function and variable has a doc
comment.
- JSON tags are camelCase. The HTTP API's `last_id`, `pow_token` and
`hashcash_bits` stay as they are, each with a `//nolint:tagliatelle` comment.
- Prefer named constants to magic numbers. The `mnd` linter checks for them
outside test files.
- No inline error handling such as `if err := f(); err != nil`. Assign the error
on its own line, then test it.
- A `return` that follows another statement in its block has a blank line before
it.
- A blank line between the embedded fields of a struct and its other fields.
- Outside test files, every blank import has a comment saying why it is there.
## Architecture
- Dependency injection via Uber fx — all components are fx providers
- `internal/` for all application packages — one package per concern
- Handlers return `http.HandlerFunc` using the closure pattern
- Request/response structs defined inside handler closures
- Models embed `models.Base` for database access to relation methods
- Database migrations in `internal/db/schema/` — embedded via `go:embed`
- SQLite by default (pure Go driver: modernc.org/sqlite)
- The server uses `go.uber.org/fx` for dependency injection.
`cmd/neoircd/main.go` lists its `fx` providers, such as `db.New` and
`handlers.New`.
- Application packages live under `internal/`, one package per concern.
- `pkg/irc` holds the IRC command names and numeric reply codes.
- The `web` package embeds the built web client.
- Each exported `Handle…` method of `Handlers` returns an `http.HandlerFunc`.
- A request struct is defined inside the method that decodes it.
- The server uses the database only through the methods of `db.Database`.
- The database migrations live in `internal/db/schema/` and are embedded with
`go:embed`.
- The database is SQLite, through the pure Go driver `modernc.org/sqlite`.
## Testing
- Table-driven tests preferred
- Use `testing.T` helpers, not assertion libraries
- Test files live next to the code they test
- `_test.go` suffix, same package for white-box testing
- Prefer table-driven tests.
- Use `testing.T` helpers, not assertion libraries.
- A test file lives next to the code it tests and ends in `_test.go`.
- The linter requires each test file to be in the package's external test
package, such as `handlers_test`. It allows `export_test.go` and files ending
in `_internal_test.go` in the package itself.
- An `export_test.go` file gives the external tests access to the package's
unexported code.
## Dependencies
- Required libraries are listed in CONVENTIONS.md §1
- No new dependencies without justification
- Pure Go preferred (no CGO)
- The required libraries are listed in `CONVENTIONS.md` §1.
- No new dependency without a reason given in the pull request.
- Prefer dependencies written in pure Go, which build without cgo.
+79 -137
View File
@@ -1,156 +1,98 @@
# Message schemas
# Message Schemas
This directory holds JSON Schema files (draft 2020-12) for the messages that the
neoirc HTTP API carries. `message.json` describes the envelope every message
uses. `commands/` has files for some of the IRC commands. `numerics/` has files
for some of the numeric replies. `README.md` at the repository root documents
the protocol.
JSON Schema definitions (draft 2020-12) for the neoirc protocol. Messages use
**IRC command names and numeric reply codes** (RFC 1459/2812) encoded as JSON
over HTTP.
## The envelope
## Envelope
Every message is one JSON object. These are the fields it can have:
- `command`: the IRC command name, such as `PRIVMSG`, or the name of a numeric
reply, such as `RPL_WELCOME`. Always present.
- `code`: the number of a numeric reply, such as `1` for `RPL_WELCOME`. Absent
on other messages.
- `id`: the message's UUID, assigned by the server.
- `from`: the sender's nick, or the server name on a server message.
- `to`: the target, such as a channel name starting with `#` or a nick.
- `params`: other parameters, in order, as an array of strings. `KICK` events
and many numeric replies have it. The server ignores `params` sent by a
client.
- `body`: an array of strings. It is never a bare string. In a `PRIVMSG` or
`NOTICE`, each string is one line of text.
- `ts`: when the server stored the message, as an RFC 3339 time in UTC, such as
`2026-10-27T12:00:00.123Z`.
- `meta`: an object of extra data, such as a hashcash stamp in `meta.hashcash`.
A hashcash stamp is a short string that takes a computer some work to make. A
channel with mode `+H` needs one in every `PRIVMSG`. The server relays the
`meta` of a `PRIVMSG` or `NOTICE` unchanged. It drops the `meta` of every
other command.
A `PRIVMSG` as the IRC listener sends it, and as the HTTP API sends it without
`id`, `ts` and `meta`:
Every message is a JSON object with a `command` field. The format maps directly
to IRC wire format:
```
IRC: :alice!alice@* PRIVMSG #general :hello world
JSON: {"command": "PRIVMSG", "from": "alice", "to": "#general", "body": ["hello world"]}
IRC: :nick PRIVMSG #channel :hello world
JSON: {"command": "PRIVMSG", "from": "nick", "to": "#channel", "body": ["hello world"]}
IRC: :server 353 nick = #channel :user1 @op1 +voice1
JSON: {"command": "353", "to": "nick", "params": ["=", "#channel"], "body": ["user1 @op1 +voice1"]}
Multiline: {"command": "PRIVMSG", "to": "#ch", "body": ["line 1", "line 2"]}
Structured: {"command": "PUBKEY", "body": {"alg": "ed25519", "key": "base64..."}}
```
A numeric reply has its name in `command` and its number in `code`. This `353`
reply lists the members of `#general`, again without `id`, `ts` and `meta`:
Common fields (see `message.json` for full schema):
```
{"command": "RPL_NAMREPLY", "code": 353, "from": "neoirc", "to": "alice",
"params": ["=", "#general"], "body": ["@op1!op1@host1 alice!alice@host2"]}
```
| Field | Type | Description |
| --------- | --------------- | ---------------------------------------------------- |
| `id` | string (uuid) | Server-assigned message UUID |
| `command` | string | IRC command or 3-digit numeric code |
| `from` | string | Source nick or server name (IRC prefix) |
| `to` | string | Target: #channel or nick |
| `params` | string[] | Middle parameters (mainly for numerics) |
| `body` | array \| object | Structured body — never a raw string (see below) |
| `ts` | string | ISO 8601 timestamp (server-assigned, not in raw IRC) |
| `meta` | object | Extensible metadata (signatures, hashes, etc.) |
A `body` with two strings is a two-line message:
**Structured bodies:** `body` is always an array of strings (for text) or an
object (for structured data like PUBKEY). Never a raw string. This enables:
```
{"command": "PRIVMSG", "to": "#general", "body": ["line one", "line two"]}
```
- Multiline messages without escape sequences
- Deterministic canonicalization via RFC 8785 JCS for signing
- Structured data where needed
## Files
## Commands
Commands, in `commands/`:
IRC commands used for client↔server and server↔server communication.
- `PRIVMSG.json`: a message to a channel or a user.
- `NOTICE.json`: a notice. A client must not answer a notice automatically.
- `JOIN.json`: a user joined a channel.
- `PART.json`: a user left a channel.
- `QUIT.json`: a user left the server.
- `NICK.json`: a user changed nick.
- `TOPIC.json`: a channel topic was set.
- `MODE.json`: a channel mode was changed.
- `KICK.json`: a channel operator removed a user from a channel.
- `PING.json`: a check that the server still answers.
- `PONG.json`: the answer to a `PING`.
- `PUBKEY.json`: an announcement of a user's public signing key. The server has
no such command. It answers a `PUBKEY` with `ERR_UNKNOWNCOMMAND` (421).
| Command | File | RFC | Description |
| --------- | ----------------------- | ----------- | -------------------------- |
| `PRIVMSG` | `commands/PRIVMSG.json` | 1459 §4.4.1 | Message to channel or user |
| `NOTICE` | `commands/NOTICE.json` | 1459 §4.4.2 | Notice (no auto-reply) |
| `JOIN` | `commands/JOIN.json` | 1459 §4.2.1 | Join a channel |
| `PART` | `commands/PART.json` | 1459 §4.2.2 | Leave a channel |
| `QUIT` | `commands/QUIT.json` | 1459 §4.1.6 | User disconnected |
| `NICK` | `commands/NICK.json` | 1459 §4.1.2 | Change nickname |
| `TOPIC` | `commands/TOPIC.json` | 1459 §4.2.4 | Get/set channel topic |
| `MODE` | `commands/MODE.json` | 1459 §4.2.3 | Set channel/user modes |
| `KICK` | `commands/KICK.json` | 1459 §4.2.8 | Kick user from channel |
| `PING` | `commands/PING.json` | 1459 §4.6.2 | Keepalive |
| `PONG` | `commands/PONG.json` | 1459 §4.6.3 | Keepalive response |
| `PUBKEY` | `commands/PUBKEY.json` | (extension) | Announce/relay signing key |
Numeric replies, in `numerics/`:
## Numeric Replies
- `001.json`: `RPL_WELCOME`, which welcomes a new user.
- `002.json`: `RPL_YOURHOST`, the server's name and version.
- `003.json`: `RPL_CREATED`, the date the server started.
- `004.json`: `RPL_MYINFO`, the server's version and its lists of modes.
- `322.json`: `RPL_LIST`, one channel in the answer to `LIST`.
- `323.json`: `RPL_LISTEND`, the end of the answer to `LIST`.
- `332.json`: `RPL_TOPIC`, a channel's topic.
- `353.json`: `RPL_NAMREPLY`, a channel's members.
- `366.json`: `RPL_ENDOFNAMES`, the end of the member list.
- `372.json`: `RPL_MOTD`, one line of the message of the day.
- `375.json`: `RPL_MOTDSTART`, the start of the message of the day.
- `376.json`: `RPL_ENDOFMOTD`, the end of the message of the day.
- `401.json`: `ERR_NOSUCHNICK`, the nick does not exist.
- `403.json`: `ERR_NOSUCHCHANNEL`, the channel does not exist.
- `433.json`: `ERR_NICKNAMEINUSE`, the nick is taken.
- `442.json`: `ERR_NOTONCHANNEL`, the user is not on that channel.
- `482.json`: `ERR_CHANOPRIVSNEEDED`, the user is not a channel operator.
Three-digit codes for server responses, per IRC convention.
## Where the files differ from the server
### Success / Informational (0xx–3xx)
Where a schema file and the server disagree, the server is right:
| Code | Name | File | Description |
| ----- | -------------- | ------------------- | ------------------------------ |
| `001` | RPL_WELCOME | `numerics/001.json` | Welcome after session creation |
| `002` | RPL_YOURHOST | `numerics/002.json` | Server host info |
| `003` | RPL_CREATED | `numerics/003.json` | Server creation date |
| `004` | RPL_MYINFO | `numerics/004.json` | Server info and modes |
| `322` | RPL_LIST | `numerics/322.json` | Channel list entry |
| `323` | RPL_LISTEND | `numerics/323.json` | End of channel list |
| `332` | RPL_TOPIC | `numerics/332.json` | Channel topic |
| `353` | RPL_NAMREPLY | `numerics/353.json` | Channel member list |
| `366` | RPL_ENDOFNAMES | `numerics/366.json` | End of NAMES list |
| `372` | RPL_MOTD | `numerics/372.json` | MOTD line |
| `375` | RPL_MOTDSTART | `numerics/375.json` | Start of MOTD |
| `376` | RPL_ENDOFMOTD | `numerics/376.json` | End of MOTD |
- `message.json` and the numeric files put the three-digit code in `command` and
have no `code` field. The server puts the reply's name in `command` and its
number in `code`.
- `message.json` allows an object as `body`. The server reads only arrays of
strings. A `PRIVMSG` with an object gets `ERR_NOTEXTTOSEND` (412).
- `message.json` says `to` is a channel or a nick. A `WALLOPS` message has `*`
there.
- `message.json` allows `meta` on every message. The server keeps `meta` only on
a `PRIVMSG` or `NOTICE`.
- `message.json` describes signature fields in `meta`. The server never checks a
signature.
- `message.json` does not describe `meta.hashcash`. The server reads it in a
`PRIVMSG` to a channel with mode `+H`.
- `JOIN.json` has no `body`. A client's `JOIN` can carry the channel key in
`body`. When a user joins, the server's `JOIN` event repeats the channel name
there.
- `JOIN.json` allows a channel name of any length. The server allows at most 63
characters after the `#`. It answers a longer name with `ERR_NOSUCHCHANNEL`
(403).
- `KICK.json` puts the kicked nick in `params`. The server reads a client's
`KICK` from `body`: the nick, then an optional reason.
- `MODE.json` puts the mode change in `params`. The server uses `body` instead.
A client sends the change and any value as separate strings, such as `"+o"`
and `"bob"`. The server's `MODE` event has one string such as `"+o bob"`, or
the change and its value as two strings, such as `"+k"` and `"secret"`.
- `NOTICE.json` allows `*` as `to`. The server answers that with
`ERR_NOSUCHNICK` (401).
- `NOTICE.json` has an example notice from the server to `*`. The server sends
no such notice.
- `PING.json` and `PONG.json` describe a token that the `PONG` repeats. The
server ignores the `body` of a `PING`. It answers the HTTP request itself with
`{"command": "PONG", "from": "<server name>"}`.
- `PING.json` and `PONG.json` also describe messages between servers. The server
does not connect to other IRC servers.
- `PUBKEY.json` describes a command the server does not have.
- `QUIT.json` says only the server sends `QUIT`. A client can send it too, to
end its session. The client's `QUIT` can carry a reason in `body`.
- `TOPIC.json` says a `TOPIC` without a `body` queries the topic. Over the HTTP
API a `TOPIC` without a `body` gets `ERR_NEEDMOREPARAMS` (461).
- `TOPIC.json` says an empty array clears the topic. The server answers `[]`
with `ERR_NEEDMOREPARAMS` (461). A `body` of `[""]` clears the topic.
- `003.json` describes the server's creation date. The server sends the date it
last started.
- `004.json` has example mode lists. The server sends an empty string as the
user modes and `ikmnostl` as the channel modes.
- `322.json` says `LIST` gets one reply per channel. The server leaves out each
secret channel that the user is not on.
- `353.json` says `body` lists nicks. Over the HTTP API the server lists
`nick!user@host` in place of each nick, such as `@op1!op1@host1`.
- `353.json` says the first parameter marks a channel as public, private or
secret. The server always sends `=`, the mark for a public channel.
- `376.json` gives the text as `End of /MOTD command`. The server adds a full
stop.
- `401.json` gives the text as `No such nick/channel`. For a `PRIVMSG` or
`NOTICE` to an unknown nick, the server sends `No such nick`.
- `433.json` has an example with `*` as `to`. Over the HTTP API the server sends
`433` only in answer to a `NICK`, with the user's nick as `to`.
### Errors (4xx)
The server also accepts some messages that the files do not allow, such as a
channel name without its `#`.
| Code | Name | File | Description |
| ----- | -------------------- | ------------------- | ----------------------- |
| `401` | ERR_NOSUCHNICK | `numerics/401.json` | No such nick/channel |
| `403` | ERR_NOSUCHCHANNEL | `numerics/403.json` | No such channel |
| `433` | ERR_NICKNAMEINUSE | `numerics/433.json` | Nickname already in use |
| `442` | ERR_NOTONCHANNEL | `numerics/442.json` | Not on that channel |
| `482` | ERR_CHANOPRIVSNEEDED | `numerics/482.json` | Not channel operator |
## Federation (S2S)
Server-to-server messages use the same command format. Federated servers relay
messages with an additional `origin` field in `meta` to track the source server.
The PING/PONG commands serve as inter-server keepalives. State sync after link
establishment uses a burst of JOIN, NICK, TOPIC, and MODE commands.