write-proto
Skill2 starsChanged 4 days agoMITView on GitHub
Write or review Protobuf messages, RPCs, types, and enums with buf and compatibility checks. Load for any .proto change; public REST specifications use write-openapi.
In this repository40 other files it gives its agents.
- AGENTS.mdAGENTS.md
- .agents/skills/AGENTS.mdAGENTS.md
- attribute-ai-commitsSkill
- check-accessSkill
- choose-dependencySkill
- coordinate-landingSkill
- develop-featureSkill
- document-codeSkill
- git-conventionsSkill
- implement-featureSkill
- manage-versionsSkill
- monitor-ciSkill
- open-pull-requestSkill
- plan-client-server-boundarySkill
- plan-featureSkill
- plan-ui-designSkill
- prefer-small-solutionsSkill
- prepare-releaseSkill
- register-ai-agentSkill
- resolve-pr-feedbackSkill
- triage-issuesSkill
- use-a-novel-cliSkill
- use-discordSkill
- write-bash-scriptsSkill
- write-design-systemSkill
- write-dockerfilesSkill
- write-frontendSkill
- write-frontend-testsSkill
- write-github-actionsSkill
- write-go-kitSkill
- write-go-serviceSkill
- write-goSkill
- write-go-testsSkill
- write-infraSkill
- write-js-packageSkill
- write-openapiSkill
- write-platformSkill
- write-project-docsSkill
- write-sqlSkill
- write-svelteSkill
The file
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 2 are distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 53
Run these in order. pnpm format:proto must come before pnpm generate:go — buf formats the source files in place, and the generated output …
- Very specific: line 56
After pnpm generate:go, update the Go handler code that uses the changed types, then run pnpm format:go and pnpm lint:go to confirm it …
- Very specific: line 139
Import them with a relative path: import "jwk.proto";.
- Very specific: line 182
Document every service, rpc, message, and field.
- Very specific: line 219
Start at 1 for the first field.
- Distinctive: line 230
Never start numbering at 0 — proto3 uses 0 as the default for numeric types and it conflicts with unset detection.
- Distinctive: line 274
Prefer proto's well-known types over raw primitives for common data shapes:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Protobuf Writing Skill
Proto definitions are the contract between gRPC producers and consumers: once published, they must evolve without breaking existing callers. Treat every field number and type as a durable commitment.
Before touching any proto file, read it and all files it imports. Read buf.yaml and
buf.gen.yaml — they control what gets generated and where. Read the corresponding generated Go
file in internal/handlers/protogen/ to see what callers currently depend on.
Project Layout
internal/models/proto/ # Source .proto files (edit these)
<entity>_<operation>.proto # One file per RPC — service + request/response messages
<entity>.proto # Shared message/enum types (no service definition)
internal/handlers/protogen/ # Generated Go stubs (never edit — always regenerated from scratch)
<entity>_<operation>.pb.go # Message types
<entity>_<operation>_grpc.pb.go # gRPC client/server interfacesThe entire internal/handlers/protogen/ directory is deleted and recreated on every
pnpm generate:go run. Never put hand-written code there.
After Every Edit
pnpm format:proto # format .proto files + sync buf.lock
pnpm lint:proto # validate against buf's STANDARD ruleset
pnpm generate:go # wipe protogen/ and regenerate Go stubs + mocks
pnpm format:go # goimports on the newly generated files
pnpm lint:go # catch any issues in handler code using new typesEvery edit includes a comment-only one. protoc-gen-go copies leading comments into the generated
Go, so rewording a message or field doc changes protogen/ as surely as adding a field does, and
the generated-go job fails on the drift. Commit the regenerated output as its own chore(gen)
commit — git-conventions forbids mixing commit types, and a docs commit carrying generated files
hides why they changed.
Run these in order. pnpm format:proto must come before pnpm generate:go — buf formats the source
files in place, and the generated output reflects the formatted source.
After pnpm generate:go, update the Go handler code that uses the changed types, then run
pnpm format:go and pnpm lint:go to confirm it compiles cleanly.
Then invoke the document-code skill for every .proto file you created or modified. Proto
comments are the public API contract — write them, accurate and complete, before considering the
change done.
Toolchain
This project uses buf v2, not protoc directly. All buf operations run
through go tool -modfile=buf.mod buf.
buf is pinned in its own buf.mod, not in go.mod. It is a generator, never imported: the
emitted stubs link google.golang.org/protobuf, which stays a direct require of the service, while
buf's own dependency graph — more than half of go.mod before the split — stays out of the module
the service ships. The same holds for every tool: one modfile each, and each with its own Renovate
branch prefix.
buf.yaml (linting and breaking-change rules):
version: v2
modules:
- path: internal/models/proto
lint:
use:
- STANDARD
except:
- PACKAGE_DEFINED # no proto package declarations — managed mode handles namespacing
breaking:
use:
- FILE # field renames/removals are breaking at the file levelbuf.gen.yaml (code generation):
version: v2
managed:
enabled: true
override:
- file_option: go_package_prefix
value: github.com/a-novel/service-json-keys/v2/internal/handlers/protogen;protogen
plugins:
- remote: buf.build/protocolbuffers/go # generates message structs (.pb.go)
out: internal/handlers/protogen
opt: paths=source_relative
- remote: buf.build/grpc/go # generates gRPC client/server code (_grpc.pb.go)
out: internal/handlers/protogen
opt:
- paths=source_relative
inputs:
- directory: internal/models/protoManaged mode injects option go_package automatically — do not add option go_package or
package statements to .proto files manually. A manual one conflicts with the managed-mode
settings or produces a duplicate declaration.
buf.gen.docs.yaml converts the protos into the gRPC document of the service's API reference,
the Scalar page each service deploys to GitHub Pages. publish-docs runs it on master, and a
jq step names the document, stamps the package.json version, and drops the Connect headers and
per-field titles the converter adds. Nothing it writes is committed. The converter describes
Connect-style HTTP calls that a grpc-go server does not answer, so openapi.html hides test
requests and code samples for that document.
File Structure
One service per file. A .proto file that defines a service holds exactly that service and
its request/response messages — no other services, no unrelated types.
jwk_get.proto → JwkGetService + JwkGetRequest + JwkGetResponse
claims_sign.proto → ClaimsSignService + ClaimsSignRequest + ClaimsSignResponse
status.proto → StatusService + StatusRequest + StatusResponse + DependencyHealth + DependencyStatus
jwk.proto → Jwk message + JwkUsage enum (shared, no service)Shared types that appear in multiple service files get their own file with no service definition.
Import them with a relative path: import "jwk.proto";.
Naming Conventions
Services and RPCs
| Element | Convention | Example |
| Service name | <Entity><Operation>Service | JwkGetService |
| RPC name | <Entity><Operation> | JwkGet, ClaimsSign |
| Request message | <Entity><Operation>Request | JwkGetRequest |
| Response message | <Entity><Operation>Response | JwkGetResponse |
The RPC name must match the Go service operation name exactly — this is what cmd/grpc/main.go
registers and what pkg/go/client.go calls.
Messages
Messages use PascalCase. Field names always use snake_case, never camelCase — the Go
generator converts them to camelCase getters (GetKeyId(), GetUsage()).
Enums
Enum type names use PascalCase. Enum values use SCREAMING_SNAKE_CASE with the type name as
a prefix, and always start at 0 with an _UNSPECIFIED value:
enum DependencyStatus {
DEPENDENCY_STATUS_UNSPECIFIED = 0; // required zero value — must not be used in requests
DEPENDENCY_STATUS_UP = 1;
DEPENDENCY_STATUS_DOWN = 2;
}proto3 requires _UNSPECIFIED = 0: unset enum fields default to 0, and the application must be
able to detect "not set". Never assign 0 to a meaningful value.
Comments
Document every service, rpc, message, and field. The API reference shows these comments to
every team that calls the service. Comments go immediately above the element, using //
(single-line) or /* */ (multi-line):
// JwkGetService returns a public JSON Web Key by its key ID.
// The returned key may be used by any recipient to verify a token.
service JwkGetService {
rpc JwkGet(JwkGetRequest) returns (JwkGetResponse);
}
// JwkGetRequest identifies the key to retrieve by its key ID.
message JwkGetRequest {
// ID of the key to retrieve. Corresponds to the "kid" field in the JWT header.
string id = 1;
}For enum values, explain what each value means (especially _UNSPECIFIED):
enum DependencyStatus {
// DEPENDENCY_STATUS_UNSPECIFIED means the application has failed to, or has not yet
// assessed the status of the given dependency.
DEPENDENCY_STATUS_UNSPECIFIED = 0;
// DEPENDENCY_STATUS_UP means the dependency was successfully pinged.
DEPENDENCY_STATUS_UP = 1;
}Field Numbering
Field numbers are permanent. They are serialized in binary encoding and must never change or be reused:
- Start at
1for the first field. Use sequential numbers. - Once a field is removed, reserve its number and name. Reusing the number of a deleted field
silently corrupts data for clients that still send the old one:
message JwkGetRequest { reserved 2; reserved "legacy_field"; string id = 1; } - Adding a new field with a new, previously unused number is always safe.
- Never start numbering at 0 — proto3 uses 0 as the default for numeric types and it conflicts with unset detection.
- Field numbers 1–15 are encoded in one byte; 16–2047 in two bytes. Reserve 1–15 for the most frequently used fields.
Wire-Safe Changes vs Breaking Changes
The project uses FILE-level breaking detection. Buf will reject:
| Change | Why it breaks |
| Remove a field | Existing callers setting that field silently lose data |
| Rename a field | Field name affects JSON encoding and Go accessor names |
| Change a field type | Existing serialized data becomes unreadable |
| Rename a message | All Go types derived from it are renamed |
| Rename/renumber an enum value | Existing serialized values decode incorrectly |
| Remove a service or RPC | Existing callers receive "unimplemented" errors |
Wire-safe (non-breaking) changes:
| Change | Why it is safe |
| Add a new field | Old clients ignore unknown fields; new clients see it |
| Add a new message | Unused until referenced |
| Add a new enum value | Old clients receive the numeric value and can ignore it |
| Add a new RPC | Old clients never call it |
| Add or change a comment | No runtime impact |
Renaming is never the safe refactor it looks like — buf rejects it at FILE level. To rename a
field, add a new field with the correct name and a new number, deprecate the old one with a
comment, then remove it in a coordinated release.
An intended break, such as a package that was never released or a planned major, needs no admin
override. A reviewer adds the buf skip breaking label, and bufbuild/buf-action skips the
breaking check on that pull request. a-novel/infra's GitHub root provisions the label in every
repository. service-template skips the check altogether: its contract changes in place, as its
migrations do.
Well-Known Types
Prefer proto's well-known types over raw primitives for common data shapes:
| Use case | Import | Type |
| Arbitrary JSON payload | google/protobuf/any.proto | google.protobuf.Any |
| Timestamps | google/protobuf/timestamp.proto | google.protobuf.Timestamp |
| Durations | google/protobuf/duration.proto | google.protobuf.Duration |
| Optional primitive (nullable) | google/protobuf/wrappers.proto | google.protobuf.StringValue, etc. |
| Empty request/response | google/protobuf/empty.proto | google.protobuf.Empty |
Carry an instant as a Timestamp and a length of time as a Duration. Both keep nanosecond
precision. The generated Go reads them with AsTime() and AsDuration(), so a caller parses no
RFC 3339 string and scales no float of seconds. A Duration carries its unit. Its field name takes
none: oldest_pending_age, not oldest_pending_age_seconds.
Example — importing and using Any (as in claims_sign.proto):
import "google/protobuf/any.proto";
message ClaimsSignRequest {
string usage = 1;
google.protobuf.Any payload = 2;
}Alignment with Go Layers
Proto types are handler-layer only. They are generated into internal/handlers/protogen/
and must never be imported by core/, dao/, or config/. Handlers own all conversions
between proto types and core types.
| Proto element | Generated Go | Used in |
service JwkGetService | protogen.JwkGetServiceServer interface | embedded in handlers.GrpcJwkGet |
service JwkGetService | protogen.JwkGetServiceClient interface | pkg/go/client.go via gRPC dial |
service JwkGetService | protogen.RegisterJwkGetServiceServer | cmd/grpc/main.go |
message JwkGetRequest | protogen.JwkGetRequest struct | handler, converted to service request |
enum DependencyStatus | protogen.DependencyStatus const | handler only |
Adding a New RPC: Step-by-Step
- Create
internal/models/proto/<entity>_<operation>.protowith the service, request, and response messages, following the file structure and naming conventions above. - Run
pnpm format:proto— formats the file and updates buf.lock. - Run
pnpm lint:proto— fix any violations before generating. - Run
pnpm generate:go— wipesprotogen/and regenerates everything. - Create
internal/handlers/grpc.<entity><Operation>.gowith the new handler type. Embedprotogen.Unimplemented<ServiceName>Server, define the service interface, implement the RPC method. - Run
pnpm generate:goagain if you added a new interface (regenerates mocks). - Wire it up in
cmd/grpc/main.go: construct the handler and callprotogen.Register<ServiceName>Server(server, handler). - Invoke the
document-codeskill for the new.protofile and the new Go handler file. - Invoke the
write-go-testsskill to write tests for the new handler. - Run
pnpm format:go,pnpm lint:go, anda-novel test --type=go -yto confirm everything is clean.
Common Pitfalls
repeated on response fields that could be empty. An empty repeated field returns a nil
slice in Go, not an empty slice. Callers must use GetField() (nil-safe accessor) rather than
.Field directly.
The rules above, as a checklist:
- Omitting
_UNSPECIFIED = 0from an enum. - Adding
packageoroption go_packageto a.protofile, which managed mode already injects. - Editing files in
internal/handlers/protogen/, which the nextpnpm generate:gooverwrites. - Reusing the field number of a deleted field instead of reserving it.
- Importing
internal/handlers/protogenfrominternal/coreorinternal/dao. - Running
pnpm generate:gobeforepnpm format:proto. - Renaming a field in place instead of adding a replacement and staging the removal.
- Carrying an instant as an RFC 3339 string, or a length of time as a number of seconds.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

