diff --git a/.agents/skills/epctl-commands/SKILL.md b/.agents/skills/epctl-commands/SKILL.md new file mode 100644 index 0000000..4e861f3 --- /dev/null +++ b/.agents/skills/epctl-commands/SKILL.md @@ -0,0 +1,202 @@ +--- +name: epctl-commands +description: "Pattern for adding CLI commands to epctl utility. Use when: creating new epctl subcommands, extending CLI functionality, working with cmd/epctl/ files. Covers file structure, urfave/cli v3 patterns, output formatting, and command wiring." +argument-hint: "Describe the CLI command you want to add" +--- + +# epctl CLI Commands — Pattern Guide + +You follow this pattern when adding or modifying CLI commands for the `epctl` utility. + +## When to Use + +- Adding a new subcommand to `epctl` +- Modifying existing CLI commands +- Working with files in `cmd/epctl/` +- Creating new command groups (like `plugins`, `config`) + +## Project Structure + +``` +cmd/epctl/ +├── main.go # Entry point: app.Execute() +└── internal/ + ├── output/ + │ └── printer.go # Printer (text/JSON dual output) + └── app/ + ├── root.go # Root command + getPrinter() helper + ├── builder.go # PluginBuilder (business logic) + ├── path_filter.go # PathFilter (business logic) + ├── plugins_build.go # epctl plugins build + ├── plugins_register.go # epctl plugins register + ├── plugins_list.go # epctl plugins list + └── config_validate.go # epctl config validate +``` + +### Shared vs CLI-only + +| Location | Scope | Example | +|----------|-------|---------| +| `internal/config/` | Shared (server + CLI) | Config types, `Validate()` | +| `cmd/epctl/internal/` | CLI-only | Printer, commands, builder | + +**Rule:** if a package is only used by `epctl`, it lives in `cmd/epctl/internal/`. If both server and CLI use it, it stays in `internal/`. + +## CLI Framework + +**urfave/cli v3** (`github.com/urfave/cli/v3`). + +Key patterns: +- Commands are `*cli.Command` structs +- Action signature: `func(ctx context.Context, cmd *cli.Command) error` +- Global flags defined on root command propagate to all subcommands +- Global `--output` / `-o` flag controls text vs JSON output + +## File Naming + +Pattern: `_.go` + +| File | Command | +|------|---------| +| `plugins_build.go` | `epctl plugins build` | +| `plugins_register.go` | `epctl plugins register` | +| `plugins_list.go` | `epctl plugins list` | +| `config_validate.go` | `epctl config validate` | + +If a file defines the parent group command (e.g., `newPluginsCmd()`), it goes in the first action file of that group (currently `plugins_build.go`). + +## Command Pattern + +### Constructor + runner + +Every command consists of two functions: + +```go +// Constructor — returns the *cli.Command definition. +func newCmd() *cli.Command { + return &cli.Command{ + Name: "", + Usage: "Short description", + ArgsUsage: "[optional args]", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "some-flag", + Value: "default", + Usage: "what it does", + }, + }, + Action: run, + } +} + +// Runner — implements the command logic. +func run(ctx context.Context, cmd *cli.Command) error { + printer := getPrinter(cmd) + + // 1. Read flags + someFlag := cmd.String("some-flag") + + // 2. Business logic + result, err := doSomething(ctx, someFlag) + if err != nil { + return fmt.Errorf("doSomething: %w", err) + } + + // 3. Output via Printer + return printer.Table(headers, rows) +} +``` + +### Group command + +```go +func newSomeGroupCmd() *cli.Command { + return &cli.Command{ + Name: "", + Usage: "Group description", + Commands: []*cli.Command{ + newSomeGroupActionCmd(), + newSomeGroupOtherCmd(), + }, + } +} +``` + +## Output via Printer + +**Every command MUST use `getPrinter(cmd)`** for output. Never use `fmt.Println` directly. + +```go +printer := getPrinter(cmd) + +// Simple message +printer.Message("Done!") // text: "Done!\n" json: {"message":"Done!"} + +// Table +printer.Table( + []string{"NAME", "STATUS"}, // headers + [][]string{{"foo", "ok"}}, // rows +) +// text: tabwriter table json: [{"NAME":"foo","STATUS":"ok"}] + +// Error +printer.Error(err) // text: "Error: ...\n" json: {"error":"..."} +``` + +## Wiring a New Command + +1. Create `cmd/epctl/internal/app/_.go` +2. Implement `newCmd()` + `run()` +3. Add to parent group's `Commands` slice: + +```go +// In the group command constructor: +Commands: []*cli.Command{ + newExistingCmd(), + newYourNewCmd(), // ← add here +}, +``` + +4. If it's a new group, add to root in `root.go`: + +```go +Commands: []*cli.Command{ + newPluginsCmd(), + newConfigCmd(), + newYourGroupCmd(), // ← add here +}, +``` + +## Connecting to EasyP Service + +Commands that talk to the service use the SDK: + +```go +client, err := sdk.NewClient(addr, sdk.WithInsecure()) +if err != nil { + return fmt.Errorf("cannot connect to %s: %w", addr, err) +} + +defer func() { _ = client.Close() }() +``` + +Standard flags for service-connected commands: +```go +&cli.StringFlag{ + Name: "addr", + Value: "localhost:8080", + Usage: "gRPC server address", +}, +``` + +## Quick Checklist + +Before submitting a new CLI command, verify: + +- [ ] File named `_.go` in `cmd/epctl/internal/app/` +- [ ] Uses `getPrinter(cmd)` for all output +- [ ] Constructor `newCmd()` returns `*cli.Command` +- [ ] Runner `run(ctx, cmd) error` handles business logic +- [ ] Wired into parent group's `Commands` slice +- [ ] Flags have defaults and usage descriptions +- [ ] Errors wrapped with `fmt.Errorf(": %w", err)` diff --git a/.agents/skills/go-code-style/SKILL.md b/.agents/skills/go-code-style/SKILL.md new file mode 100644 index 0000000..17492aa --- /dev/null +++ b/.agents/skills/go-code-style/SKILL.md @@ -0,0 +1,216 @@ +--- +name: go-code-style +description: "Project-specific Go code style enforcer. Use when: writing Go code, reviewing Go code, refactoring, creating new packages. Covers error wrapping, defer patterns, variable assignment, naming, imports, and other project conventions." +argument-hint: "Describe the Go code you are writing or reviewing" +--- + +# Go Code Style — EasyP Service + +You enforce project-specific Go coding conventions for the EasyP Service codebase. These rules are mandatory and take precedence over general Go style guides. + +## When to Use + +- Writing any Go code in this project +- Reviewing or refactoring existing Go code +- Creating new packages, types, or functions +- Generating code from templates + +Every Go code change MUST comply with these rules. Apply them automatically — do not ask the user for permission. + +## Anti-Patterns (NEVER DO) + +These are the most common mistakes. Memorize and never repeat them. + +### 1. Error wrapping with package prefix + +```go +// ❌ WRONG — never add package name prefix +return fmt.Errorf("goosemigrate: sql.Open: %w", err) +return fmt.Errorf("registry: c.db.Query: %w", err) + +// ✅ CORRECT — only the called function/method name +return fmt.Errorf("sql.Open: %w", err) +return fmt.Errorf("c.db.Query: %w", err) +``` + +### 2. Assignment inside `if` condition + +```go +// ❌ WRONG — never assign and check in the same line +if err = db.PingContext(ctx); err != nil { + return fmt.Errorf("db.PingContext: %w", err) +} + +if _, err = provider.Up(ctx); err != nil { + return fmt.Errorf("provider.Up: %w", err) +} + +// ✅ CORRECT — assignment and check on separate lines +err = db.PingContext(ctx) +if err != nil { + return fmt.Errorf("db.PingContext: %w", err) +} + +_, err = provider.Up(ctx) +if err != nil { + return fmt.Errorf("provider.Up: %w", err) +} +``` + +**Exception:** short variable declaration with `:=` IS allowed in `if` when the variable is scoped to the block: + +```go +// ✅ OK — new variable scoped to the if block +if info, err := c.registry.Get(ctx, name); err != nil { + ... +} +``` + +### 3. Bare `defer Close()` + +```go +// ❌ WRONG — silently ignores close errors +defer db.Close() +defer file.Close() +defer rows.Close() + +// ✅ CORRECT — always log close errors +defer func() { + if err := db.Close(); err != nil { + slog.Error("db.Close", "error", err) + } +}() +``` + +This applies to ALL resources that return an error from Close: `*sql.DB`, `*os.File`, `*sql.Rows`, `io.Closer`, etc. + +### 4. Inline comments on control flow + +```go +// ❌ WRONG — no inline comments on if/for/return +if err != nil { // check error + return err // propagate +} + +// ✅ CORRECT — comment above if needed, or omit entirely +// Validate connection before proceeding. +if err != nil { + return err +} +``` + +## Error Handling Rules + +1. **Wrap at every call site** with `fmt.Errorf(": %w", err)` +2. **Format:** only the function/method name, no package prefix +3. **Domain errors** are sentinel vars in `core/domain.go`: `core.ErrNotFound`, `core.ErrInvalidPluginName`, etc. +4. **Never create domain errors** outside `internal/core/domain.go` +5. **API error mapping:** `ErrorToStatus()` maps domain errors → gRPC status codes + +```go +// Error chain example: +// adapter → core → api +return fmt.Errorf("c.registry.Create: %w", err) // in core +return fmt.Errorf("c.db.QueryRowContext: %w", err) // in adapter +// api layer uses ErrorToStatus() to map, does not wrap +``` + +## Import Ordering + +Three groups, separated by blank lines. Enforced by `gci`: + +```go +import ( + // 1. Standard library + "context" + "fmt" + "log/slog" + + // 2. Third-party + "github.com/pressly/goose/v3" + "google.golang.org/grpc" + + // 3. Project packages + "github.com/easyp-tech/service/internal/core" +) +``` + +## Naming Conventions + +### Files + +| Layer | Pattern | Example | +|-------|---------|---------| +| Domain | `domain.go`, `core.go` | `internal/core/domain.go` | +| Adapters | `.go` | `internal/adapters/registry/registry.go` | +| API | `api.go`, `mcp.go` | `internal/api/api.go` | +| Tracing | `tracing_.go` | `internal/telemetry/tracing_core.go` | +| Tests | `_test.go` | `internal/core/crud_test.go` | + +### Types + +| Category | Exported? | Example | +|----------|-----------|---------| +| Domain entities | Yes | `PluginInfo`, `AuditEntry` | +| Domain interfaces | Yes | `Registry`, `Plugin`, `FeatureGate` | +| Config structs | No | `config`, `server`, `ports` | +| Adapter implementations | Yes | `Store`, `Registry`, `Worker` | + +### Enums + +```go +type Feature int + +const ( + _ Feature = iota + FeatureCodeGeneration + FeaturePluginListing +) +``` + +**Rule:** zero value (`0`) is always reserved with `_` so that uninitialized enum variables are never silently valid. + +## Interface Rules + +1. **Context first:** `func (r *Registry) Get(ctx context.Context, ...) (Plugin, error)` +2. **Error always last** +3. **No `I` prefix:** `Registry`, not `IRegistry` +4. **Single-method interfaces preferred** +5. **Interface in consumer package** + +## Comments + +- **Language:** English only +- **Godoc:** every exported symbol has a comment starting with its name +- **No inline comments** on `if`, `for`, `return` lines unless genuinely non-obvious +- **Package comment:** `// Package provides ...` in one file per package + +## Struct Tags + +| Layer | Tags | Case | +|-------|------|------| +| Domain (`core/`) | None | — | +| Config (`cmd/`) | `env:""` + `yaml:""` | `snake_case` | +| Proto (generated) | Don't modify | — | + +Tag case: always `snake_case` — enforced by `tagliatelle` linter. + +## Concurrency Patterns + +- `errgroup` for parallel server startup +- `WorkerPool` for bounded plugin execution +- `signal.NotifyContext` + `forceShutdown` goroutine +- Audit worker: single goroutine, buffered channel + +## Quick Checklist + +Before submitting any Go code, verify: + +- [ ] Error wrapping uses only function name, no package prefix +- [ ] No assignment inside `if` conditions (except `:=` for new scoped vars) +- [ ] All `defer Close()` wrapped with error logging +- [ ] Imports ordered: stdlib → third-party → project +- [ ] All exported symbols have godoc comments in English +- [ ] No inline comments on control flow lines +- [ ] Domain errors defined only in `core/domain.go` +- [ ] Enum types use `_ = iota` to reserve zero value diff --git a/.agents/skills/go-testing/SKILL.md b/.agents/skills/go-testing/SKILL.md new file mode 100644 index 0000000..8ae16a4 --- /dev/null +++ b/.agents/skills/go-testing/SKILL.md @@ -0,0 +1,394 @@ +--- +name: go-testing +description: "Go testing conventions enforcer. Use when: writing tests, reviewing test code, adding test cases, creating test files. Covers table-driven tests, t.Parallel, mocks, assertions, naming conventions." +argument-hint: "Describe the test you are writing or reviewing" +--- + +# Go Testing — EasyP Service + +You enforce project-specific Go testing conventions for the EasyP Service codebase. These rules are mandatory and take precedence over general Go testing guides. + +## When to Use + +- Writing any Go test in this project +- Reviewing or refactoring existing tests +- Adding test cases to existing table-driven tests +- Creating new test files +- Diagnosing test failures related to shared state or race conditions + +Every Go test change MUST comply with these rules. Apply them automatically — do not ask the user for permission. + +## Anti-Patterns (NEVER DO) + +These are the most common mistakes. Memorize and never repeat them. + +### 1. Test case without a name + +```go +// ❌ WRONG — anonymous struct without name field +tests := []struct { + input string + wantErr bool +}{ + {"foo", false}, + {"", true}, +} + +// ✅ CORRECT — every case has a descriptive name +tests := []struct { + name string + input string + wantErr bool +}{ + { + name: "valid_input", + input: "foo", + }, + { + name: "empty_input", + input: "", + wantErr: true, + }, +} +``` + +### 2. Test without t.Parallel() + +```go +// ❌ WRONG — missing t.Parallel() at both levels +func TestCreatePlugin(t *testing.T) { + tests := []struct{ ... }{...} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // test body + }) + } +} + +// ✅ CORRECT — t.Parallel() at top-level AND inside each sub-test +func TestCreatePlugin(t *testing.T) { + t.Parallel() + + tests := []struct{ ... }{...} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // test body + }) + } +} +``` + +### 3. Shared mutable state between test cases + +```go +// ❌ WRONG — cases share and mutate the same mock +func TestListPlugins(t *testing.T) { + t.Parallel() + + reg := &mockRegistry{} // shared across cases! + + tests := []struct { + name string + setup func() + }{ + { + name: "empty", + setup: func() { reg.listResult = nil }, + }, + { + name: "with_results", + setup: func() { reg.listResult = []PluginInfo{{}} }, // mutates shared reg! + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + tt.setup() // RACE: parallel sub-tests mutate shared state + // ... + }) + } +} + +// ✅ CORRECT — each case creates its own dependencies +func TestListPlugins(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + setup func(*mockRegistry) + // ... + }{ + { + name: "empty", + setup: func(r *mockRegistry) { r.listResult = nil }, + }, + { + name: "with_results", + setup: func(r *mockRegistry) { r.listResult = []PluginInfo{{}} }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + reg := &mockRegistry{} // fresh per sub-test + tt.setup(reg) + // ... + }) + } +} +``` + +> **Red flag:** if one test case can affect another, it means shared mutable state exists. This is a bug in the test (or the code under test). Fix the coupling — do not disable `t.Parallel()`. + +### 4. Using assert for fatal preconditions + +```go +// ❌ WRONG — assert does not stop the test; nil dereference follows +result, err := c.GetPlugin(ctx, req) +assert.NoError(t, err) +assert.Equal(t, "grpc", result.Group) // panic if result is nil + +// ✅ CORRECT — require stops the test on failure +result, err := c.GetPlugin(ctx, req) +require.NoError(t, err) +assert.Equal(t, "grpc", result.Group) +``` + +### 5. Ignoring the error case + +```go +// ❌ WRONG — only checks happy path +require.NoError(t, err) + +// ✅ CORRECT — check specific error with ErrorIs +if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + return +} +require.NoError(t, err) +``` + +## Table-Driven Test Template + +This is the canonical test structure for the project: + +```go +func TestMethodName(t *testing.T) { + t.Parallel() + + tests := []struct { + name string // REQUIRED, always first field + // inputs + req SomeRequest + // setup / dependencies + setup func(*mockDep) + // expected outputs + want *SomeResult + wantErr error + }{ + { + name: "success", + req: SomeRequest{Field: "value"}, + setup: func(m *mockDep) { + m.result = &SomeResult{Field: "value"} + }, + want: &SomeResult{Field: "value"}, + }, + { + name: "not_found", + req: SomeRequest{Field: "missing"}, + setup: func(m *mockDep) { + m.err = ErrNotFound + }, + wantErr: ErrNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Create fresh dependencies per sub-test + dep := &mockDep{} + if tt.setup != nil { + tt.setup(dep) + } + + // Create the system under test + sut := New(dep) + + // Execute + got, err := sut.Method(context.Background(), tt.req) + + // Assert + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} +``` + +### Struct field order + +Always follow this order in the test case struct: + +1. `name string` — descriptive test case name (always first) +2. Input fields — request, arguments, parameters +3. Setup fields — `setup func(...)` for configuring mocks +4. Expected output fields — `want*`, `wantErr` + +## t.Parallel() Rules + +### Every table-driven test uses t.Parallel() at two levels + +1. **Top-level** — `t.Parallel()` right after `func TestXxx(t *testing.T) {` +2. **Sub-test** — `t.Parallel()` right after `t.Run(tt.name, func(t *testing.T) {` + +### Why two levels? + +- **Top-level** `t.Parallel()`: allows this test function to run in parallel with other `Test*` functions in the same package. +- **Sub-test** `t.Parallel()`: allows table-driven sub-tests to run in parallel with each other within the same `Test*` function. + +### When a test cannot be parallel + +If a test case cannot run in parallel, it indicates one of these problems: + +| Symptom | Root Cause | Fix | +|---------|-----------|-----| +| Test modifies package-level variable | Global mutable state | Inject dependency via constructor | +| Test reads file written by another case | Shared filesystem side effect | Use `t.TempDir()` per case | +| Test binds to a fixed port | Port conflict | Use port `0` (OS-assigned) | +| Tests share a mock instance | Shared mutable state | Create mock inside `t.Run` | + +**Rule:** never disable `t.Parallel()` to work around coupling. Refactor the code or test instead. + +## Mock Patterns + +### Inline mocks — no code generation + +All mocks are hand-written structs defined in `_test.go` files. No `mockgen`, `moq`, or similar tools. + +```go +type mockRegistry struct { + getResult Plugin + getErr error + listResult []PluginInfo + createResult *PluginInfo + createErr error +} + +func (m *mockRegistry) Get(ctx context.Context, group, name, version string) (Plugin, error) { + return m.getResult, m.getErr +} + +func (m *mockRegistry) Create(ctx context.Context, info PluginInfo) (*PluginInfo, error) { + return m.createResult, m.createErr +} + +func (m *mockRegistry) List(ctx context.Context, filter PluginFilter) ([]PluginInfo, error) { + return m.listResult, nil +} +``` + +### Mock struct naming + +- Prefix: `mock` (lowercase — unexported) +- Name: the interface being mocked +- Example: `mockRegistry` implements `Registry`, `mockFeatureGate` implements `FeatureGate` + +### Mock location + +Same `_test.go` file that uses them. If multiple test files in a package need the same mock, put it in a shared `helpers_test.go`. + +## Assertion Patterns + +Uses `testify` (`github.com/stretchr/testify`): + +| Function | Package | When to Use | +|----------|---------|-------------| +| `require.NoError(t, err)` | `require` | Error must be nil to continue | +| `require.ErrorIs(t, err, target)` | `require` | Error must match sentinel | +| `require.ErrorContains(t, err, substr)` | `require` | Error message must contain text | +| `require.NotNil(t, result)` | `require` | Result must exist before accessing fields | +| `assert.Equal(t, expected, actual)` | `assert` | Value comparison (test continues on failure) | +| `assert.Len(t, slice, n)` | `assert` | Collection length check | +| `assert.Empty(t, slice)` | `assert` | Collection must be empty | +| `assert.True(t, cond)` | `assert` | Boolean condition | + +**Rule of thumb:** +- `require` — for preconditions: if this fails, the rest of the test is meaningless (stops test) +- `assert` — for assertions: check values, the test can continue to report multiple failures + +## Naming Conventions + +### Test files + +| Source File | Test File | +|-------------|-----------| +| `core.go` | `core_test.go` | +| `crud.go` | `crud_test.go` | +| `registry.go` | `registry_test.go` | + +### Test functions + +| Target | Pattern | Example | +|--------|---------|---------| +| Exported method | `Test` | `TestCreatePlugin` | +| Unexported function | `Test_` | `Test_validateName` | +| Multiple scenarios | `Test_` | `TestCreatePlugin_WithTags` | + +### Test case names + +- Lowercase, descriptive, use underscores for readability +- Describe the scenario, not the assertion + +```go +// ✅ Good names +"success" +"not_found" +"feature_denied" +"invalid_plugin_name" +"empty_version_defaults_to_latest" +"duplicate_returns_conflict" + +// ❌ Bad names +"test1" +"case_2" +"should_work" +"error" +"TestCreatePlugin" // don't repeat the function name +``` + +### Test package + +Tests use the **same package** (not `_test` suffix). This allows access to unexported symbols: + +```go +// internal/core/crud_test.go +package core // same package — NOT core_test +``` + +## Quick Checklist + +Before submitting any Go test code, verify: + +- [ ] Every test case struct has `name string` as the first field +- [ ] `t.Parallel()` is called at top-level AND inside each `t.Run` +- [ ] No shared mutable state between test cases +- [ ] Mocks are created fresh inside each `t.Run` (not shared) +- [ ] `require` is used for preconditions, `assert` for value checks +- [ ] Error cases use `require.ErrorIs(t, err, expectedErr)` +- [ ] Test file uses the same package (no `_test` suffix) +- [ ] Test case names are lowercase and descriptive +- [ ] `setup` function (if used) receives mock pointers, does not capture outer scope diff --git a/.agents/skills/protobuf-expert-skill/SKILL.md b/.agents/skills/protobuf-expert-skill/SKILL.md new file mode 100644 index 0000000..e03cc55 --- /dev/null +++ b/.agents/skills/protobuf-expert-skill/SKILL.md @@ -0,0 +1,185 @@ +--- +name: protobuf-expert-skill +description: "Protocol Buffers expert with deep EasyP CLI knowledge. Use when: writing or reviewing .proto files, configuring easyp.yaml, choosing lint rules, setting up code generation plugins, managing proto dependencies, detecting breaking API changes, debugging easyp errors, following protobuf style guide and API design best practices." +argument-hint: "Describe your protobuf or easyp task (e.g. 'set up linting for my project', 'fix breaking change errors')" +--- + +# Protobuf Expert — EasyP CLI Skill + +You are an expert in Protocol Buffers design and the EasyP CLI toolkit (drop-in buf.build replacement). Help developers write idiomatic .proto files, configure EasyP correctly, and follow protobuf best practices. + +## When to Use + +- Writing, reviewing, or refactoring `.proto` files +- Setting up or modifying `easyp.yaml` configuration +- Choosing which lint rules to enable +- Configuring code generation plugins and managed mode +- Managing proto dependencies (`mod download/update/vendor`) +- Detecting and resolving breaking API changes +- Debugging lint errors or generation failures +- Following protobuf style guide and API design patterns +- Migrating from buf.build to EasyP +- Setting up CI/CD for proto linting and breaking checks +- Installing EasyP + +## Decision Flow + +``` +User wants to... +│ +├─ INSTALL EasyP → see [installation.md](./references/installation.md) +│ +├─ MIGRATE from buf.build → see [migration-from-buf.md](./references/migration-from-buf.md) +│ +├─ START a new project +│ ├─ Quick → `easyp init` +│ └─ Manual → create easyp.yaml, see starter configs in [assets/](./assets/) +│ +├─ LINT proto files +│ ├─ Choose rules → § Lint Rule Selection Guide below +│ ├─ Fix violations → run `easyp lint --debug`, check rule docs +│ └─ Suppress rules → `lint.except`, `lint.ignore`, or `// easyp:off` +│ +├─ GENERATE code +│ ├─ Local plugins → `name` or `path` in `generate.plugins` +│ ├─ Remote plugins → `remote` in `generate.plugins` +│ └─ Managed mode → `generate.managed.enabled: true` +│ +├─ DETECT breaking changes +│ ├─ Run check → `easyp breaking --against main` +│ ├─ Resolve → see § Breaking Change Resolution below +│ └─ Ignore intentional → add to `breaking.ignore` +│ +├─ SET UP CI/CD → see [ci-cd-integration.md](./references/ci-cd-integration.md) +│ +└─ DEBUG an error → see [troubleshooting.md](./references/troubleshooting.md) +``` + +## Core Knowledge + +EasyP is a Go CLI tool (`easyp`) configured via `easyp.yaml`. It provides: + +| Command | Purpose | +|---------|---------| +| `easyp lint` | Lint .proto files against 42+ rules | +| `easyp generate` | Generate code via protoc plugins | +| `easyp breaking` | Detect breaking API changes vs a git ref | +| `easyp mod download/update/vendor` | Manage proto dependencies | +| `easyp init` | Interactive config setup | +| `easyp validate-config` | Validate easyp.yaml | +| `easyp ls-files` | List proto files with imports | +| `easyp completion` | Shell completions (bash/zsh) | + +Global flags: `--cfg ` (default: `easyp.yaml`), `--debug`, `--format text|json`. + +## Procedure + +### 1. Understand the User's Goal + +Identify which workflow applies: +- **New project setup** → `easyp init` or manual `easyp.yaml` creation +- **Lint configuration** → Rule selection, groups, ignores +- **Code generation** → Plugin setup, managed mode, inputs +- **Dependency management** → `deps` config, mod commands +- **Breaking change detection** → `breaking` config, git ref comparison +- **Proto file authoring** → Style guide, naming, structure +- **Debugging** → Error interpretation, config validation + +### 2. Apply the Right Reference + +Load the appropriate reference file for detailed information: + +| Task | Reference | +|------|-----------| +| CLI commands, flags, exit codes | [cli-commands.md](./references/cli-commands.md) | +| Lint rules (42 rules, 5 groups) | [lint-rules.md](./references/lint-rules.md) | +| Breaking change checks | [breaking-checks.md](./references/breaking-checks.md) | +| easyp.yaml full format | [config-reference.md](./references/config-reference.md) | +| Proto file style and API design | [protobuf-best-practices.md](./references/protobuf-best-practices.md) | +| Migrating from buf.build | [migration-from-buf.md](./references/migration-from-buf.md) | +| Troubleshooting & debugging | [troubleshooting.md](./references/troubleshooting.md) | +| CI/CD integration | [ci-cd-integration.md](./references/ci-cd-integration.md) | +| Installation methods | [installation.md](./references/installation.md) | +| Starter configs (Go+gRPC, minimal, strict) | [assets/](./assets/) | + +### 3. Lint Rule Selection Guide + +When helping users choose rules, recommend by project maturity: + +- **Starting out**: Use the `DEFAULT` group (32 rules — covers MINIMAL + BASIC + DEFAULT) +- **Strict API projects**: `DEFAULT` + `COMMENTS` + `UNARY_RPC` (all 42 rules) +- **Internal/rapid prototyping**: `BASIC` group (24 rules — less opinionated) +- **Minimal enforcement**: `MINIMAL` group (4 rules — package consistency only) + +Always suggest `allow_comment_ignores: true` for gradual adoption. + +### 4. Config Authoring + +When creating or modifying `easyp.yaml`: +1. Start with required section (`lint.use` at minimum) +2. Add `deps` for any external proto imports +3. Add `generate` section with plugins, `out`, and `opts` +4. Add `breaking` section if API stability matters +5. Validate with `easyp validate-config` + +### 5. Proto File Review + +When reviewing `.proto` files, check against easyp rules: +- File name: `lower_snake_case.proto` +- Package: matches directory, has version suffix (`v1`, `v2`) +- Naming: Messages/Services/Enums PascalCase, fields lower_snake_case, enum values UPPER_SNAKE_CASE +- Enum zero value: has `_UNSPECIFIED` suffix (or configured suffix) +- RPC: request/response types are unique, named `Request`/`Response` +- Comments: all public entities documented +- Imports: no unused, no public/weak imports + +### 6. Breaking Change Resolution + +When users encounter breaking changes: +1. Identify the change type (field deleted, type changed, service removed, etc.) +2. Explain WHY it's breaking for consumers +3. Suggest backward-compatible alternatives: + - Don't remove fields — deprecate and reserve the number + - Don't change field types — add a new field + - Don't rename enum values — add new value, deprecate old + - Don't remove RPCs — deprecate first +4. If the break is intentional, suggest adding to `breaking.ignore` + +### 7. New Project Setup + +When helping users start a new project: +1. Recommend installation method from [installation.md](./references/installation.md) +2. Run `easyp init` for interactive setup, OR +3. Copy a starter config from [assets/](./assets/): + - `easyp-minimal.yaml` — linting only (DEFAULT group) + - `easyp-go-grpc.yaml` — Go + gRPC with generation + - `easyp-strict.yaml` — all 42 rules, Go + gRPC +4. Run `easyp mod download` if deps are configured +5. Validate with `easyp validate-config` + +### 8. Migration from buf.build + +When users are migrating from buf: +1. Load [migration-from-buf.md](./references/migration-from-buf.md) for the full mapping +2. Convert `buf.yaml` + `buf.gen.yaml` → single `easyp.yaml` +3. Replace BSR deps with Git repository URLs +4. Test: `easyp lint`, `easyp generate`, `easyp breaking` +5. Update CI with [easyp-tech/actions](https://github.com/easyp-tech/actions) + +### 9. CI/CD Setup + +When setting up CI/CD: +1. Load [ci-cd-integration.md](./references/ci-cd-integration.md) +2. For GitHub → use official `easyp-tech/actions/lint@v1` and `easyp-tech/actions/breaking@v1` +3. For GitLab/other → use Docker image `ghcr.io/easyp-tech/easyp:` +4. Always pin EasyP version for reproducibility +5. Breaking checks need full git history (`fetch-depth: 0`) + +## Important Constraints + +- EasyP uses `easyp.yaml` (not `buf.yaml`) but is a drop-in buf.build replacement +- Rule names are UPPER_SNAKE_CASE (e.g., `FIELD_LOWER_SNAKE_CASE`) +- Groups can be used in `lint.use`: `MINIMAL`, `BASIC`, `DEFAULT`, `COMMENTS`, `UNARY_RPC` +- Exit codes: 0 = success, 1 = issues found, 2 = critical error +- `--format json` is available for `lint`, `breaking`, `validate-config`, and `ls-files` +- Dependencies support `@version` or `@commit-hash` suffixes diff --git a/.agents/skills/protobuf-expert-skill/assets/easyp-go-grpc.yaml b/.agents/skills/protobuf-expert-skill/assets/easyp-go-grpc.yaml new file mode 100644 index 0000000..dd0e13b --- /dev/null +++ b/.agents/skills/protobuf-expert-skill/assets/easyp-go-grpc.yaml @@ -0,0 +1,29 @@ +# EasyP starter config: Go + gRPC project +# Usage: copy to your project root as easyp.yaml + +deps: + - github.com/googleapis/googleapis + - github.com/protocolbuffers/protobuf + +lint: + use: + - DEFAULT + - COMMENTS + allow_comment_ignores: true + +generate: + inputs: + - directory: proto + plugins: + - name: go + out: gen/go + opts: + paths: source_relative + - name: go-grpc + out: gen/go + opts: + paths: source_relative + require_unimplemented_servers: false + +breaking: + against_git_ref: main diff --git a/.agents/skills/protobuf-expert-skill/assets/easyp-minimal.yaml b/.agents/skills/protobuf-expert-skill/assets/easyp-minimal.yaml new file mode 100644 index 0000000..6208cdb --- /dev/null +++ b/.agents/skills/protobuf-expert-skill/assets/easyp-minimal.yaml @@ -0,0 +1,7 @@ +# EasyP starter config: minimal linting only +# Usage: copy to your project root as easyp.yaml + +lint: + use: + - DEFAULT + allow_comment_ignores: true diff --git a/.agents/skills/protobuf-expert-skill/assets/easyp-strict.yaml b/.agents/skills/protobuf-expert-skill/assets/easyp-strict.yaml new file mode 100644 index 0000000..7cd4cb5 --- /dev/null +++ b/.agents/skills/protobuf-expert-skill/assets/easyp-strict.yaml @@ -0,0 +1,31 @@ +# EasyP starter config: strict API project (all 42 rules) +# Usage: copy to your project root as easyp.yaml + +deps: + - github.com/googleapis/googleapis + - github.com/protocolbuffers/protobuf + +lint: + use: + - DEFAULT + - COMMENTS + - UNARY_RPC + - PACKAGE_NO_IMPORT_CYCLE + allow_comment_ignores: true + +generate: + inputs: + - directory: proto + plugins: + - name: go + out: gen/go + opts: + paths: source_relative + - name: go-grpc + out: gen/go + opts: + paths: source_relative + require_unimplemented_servers: false + +breaking: + against_git_ref: main diff --git a/.agents/skills/protobuf-expert-skill/references/breaking-checks.md b/.agents/skills/protobuf-expert-skill/references/breaking-checks.md new file mode 100644 index 0000000..5870050 --- /dev/null +++ b/.agents/skills/protobuf-expert-skill/references/breaking-checks.md @@ -0,0 +1,92 @@ +# EasyP Breaking Change Checks Reference + +EasyP detects breaking API changes by comparing the current proto files against a previous git ref (branch, tag, or commit). + +## Usage + +```sh +easyp breaking --path proto --against main +``` + +## Configuration + +```yaml +breaking: + ignore: + - proto/internal # Paths to exclude from checks + against_git_ref: main # Default ref (overridden by --against flag) +``` + +## Check Categories + +### Import Changes + +| Check | Description | +|-------|-------------| +| Import deleted | A previously existing import was removed | + +### Service Changes + +| Check | Description | +|-------|-------------| +| Service deleted | An entire service definition was removed | +| RPC deleted | An RPC method was removed from a service | +| RPC request type changed | The request message type of an RPC was changed | +| RPC response type changed | The response message type of an RPC was changed | + +### Message Changes + +| Check | Description | +|-------|-------------| +| Message deleted | A message definition was removed | +| Field deleted | A field was removed (reports field number and name) | +| Field type changed | A field's type was changed | +| Field became optional | A required/default field was changed to optional | +| Field became not optional | An optional field was changed to required | + +### OneOf Changes + +| Check | Description | +|-------|-------------| +| OneOf deleted | A oneof group was removed | +| OneOf field deleted | A field within a oneof was removed | +| OneOf field type changed | A field type within a oneof was changed | + +### Enum Changes + +| Check | Description | +|-------|-------------| +| Enum deleted | An enum definition was removed | +| Enum value deleted | An enum value was removed | +| Enum value name changed | An enum value was renamed | + +## Backward-Compatible Alternatives + +Instead of making a breaking change, use these patterns: + +| Breaking Change | Safe Alternative | +|----------------|-----------------| +| Remove a field | Mark as `reserved` and deprecate: `reserved 3; reserved "old_field";` | +| Change field type | Add a new field with the new type, deprecate the old one | +| Remove an enum value | Reserve the number and name, add replacement value | +| Rename an enum value | Add new value, deprecate old (both keep same number) | +| Remove an RPC | Deprecate with `option deprecated = true;` first | +| Remove a service | Deprecate first, remove in next major version | +| Change RPC request/response | Create a new RPC with new types | + +## Ignoring Breaking Changes + +For intentional breaks, add paths to `breaking.ignore`: + +```yaml +breaking: + ignore: + - proto/internal/experimental +``` + +Or change the comparison ref to start fresh: + +```yaml +breaking: + against_git_ref: v2.0.0 # Compare against a specific release tag +``` diff --git a/.agents/skills/protobuf-expert-skill/references/ci-cd-integration.md b/.agents/skills/protobuf-expert-skill/references/ci-cd-integration.md new file mode 100644 index 0000000..0138dfd --- /dev/null +++ b/.agents/skills/protobuf-expert-skill/references/ci-cd-integration.md @@ -0,0 +1,171 @@ +# EasyP CI/CD Integration + +## GitHub Actions (Official) + +EasyP provides official GitHub Actions at [easyp-tech/actions](https://github.com/easyp-tech/actions). + +### Lint on push and PR + +```yaml +name: easyp-lint +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: easyp-tech/actions/lint@v1 + with: + version: v0.12.2 +``` + +### Breaking change detection on PR + +```yaml +name: easyp-breaking +on: [pull_request] + +jobs: + breaking: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Required — needs full git history + - uses: easyp-tech/actions/breaking@v1 + with: + version: v0.12.2 + against: origin/main +``` + +### Combined workflow (lint + breaking) + +```yaml +name: easyp +on: + push: + branches: [main, master] + pull_request: + +permissions: + contents: read + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: easyp-tech/actions/lint@v1 + with: + version: v0.12.2 + + breaking: + name: Breaking Changes + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: easyp-tech/actions/breaking@v1 + with: + version: v0.12.2 + against: origin/main +``` + +Features of official actions: +- Pre-built Docker images (fast, no compilation) +- GitHub Annotations (errors appear on PR diff lines) +- Version pinning via `version` input + +## GitLab CI + +```yaml +stages: + - proto + +easyp-lint: + stage: proto + image: ghcr.io/easyp-tech/easyp:v0.12.2 + script: + - easyp lint + rules: + - changes: + - "**/*.proto" + - easyp.yaml + +easyp-breaking: + stage: proto + image: ghcr.io/easyp-tech/easyp:v0.12.2 + script: + - easyp breaking --against origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME + variables: + GIT_DEPTH: 0 + rules: + - if: $CI_MERGE_REQUEST_IID + changes: + - "**/*.proto" + - easyp.yaml +``` + +## Makefile + +```makefile +EASYP_VERSION ?= latest + +.PHONY: proto-lint proto-breaking proto-generate proto-download proto-validate + +proto-lint: + easyp lint + +proto-breaking: + easyp breaking --against main + +proto-generate: + easyp generate + +proto-download: + easyp mod download + +proto-validate: + easyp validate-config + +proto-all: proto-download proto-lint proto-generate +``` + +## Pre-commit Hook + +```bash +#!/bin/sh +# .git/hooks/pre-commit + +# Lint only changed proto files +if git diff --cached --name-only | grep -q '\.proto$'; then + easyp lint + if [ $? -ne 0 ]; then + echo "Proto lint failed. Fix issues before committing." + exit 1 + fi +fi +``` + +## Docker-based CI (generic) + +For any CI system that supports Docker: + +```bash +docker run --rm \ + -v $(pwd):/workspace \ + -w /workspace \ + ghcr.io/easyp-tech/easyp:v0.12.2 \ + lint +``` + +## Important CI Notes + +- **Breaking checks need full git history** — use `fetch-depth: 0` or `GIT_DEPTH: 0` +- **Pin the EasyP version** — avoid `latest` in CI for reproducibility +- **Run `validate-config` first** — catches config issues before lint/generate +- **Cache dependencies** — `easyp mod download` results are cached in `~/.cache/easyp` diff --git a/.agents/skills/protobuf-expert-skill/references/cli-commands.md b/.agents/skills/protobuf-expert-skill/references/cli-commands.md new file mode 100644 index 0000000..f35b334 --- /dev/null +++ b/.agents/skills/protobuf-expert-skill/references/cli-commands.md @@ -0,0 +1,153 @@ +# EasyP CLI Commands Reference + +## Global Flags + +| Flag | Short | Type | Default | Env Var | Description | +|------|-------|------|---------|---------|-------------| +| `--cfg` / `--config` | — | string | `easyp.yaml` | `EASYP_CFG` | Path to config file | +| `--debug` | `-d` | bool | `false` | `EASYP_DEBUG` | Enable debug logging | +| `--format` | `-f` | enum | `text` | `EASYP_FORMAT` | Output format: `text` or `json` | + +--- + +## lint (alias: l) + +Lint `.proto` files against configured rules. + +**Flags:** + +| Flag | Short | Type | Default | Description | +|------|-------|------|---------|-------------| +| `--path` | `-p` | string | `.` | Relative path to proto directory | +| `--root` | `-r` | string | project root | Root directory for file search | + +**Output formats:** +- Text: `path:line:column:source message (rule_name)` +- JSON: Structured issue objects + +**Exit codes:** 0 = no issues, 1 = lint issues found, 2 = critical error (e.g., import failure) + +--- + +## generate (alias: g) + +Generate code from proto files using configured plugins. + +**Flags:** + +| Flag | Short | Type | Default | Description | +|------|-------|------|---------|-------------| +| `--path` | `-p` | string | `.` | Relative path to proto directory | +| `--root` | `-r` | string | project root | Root directory for file search | +| `--descriptor_set_out` | — | string | — | Output path for binary FileDescriptorSet | +| `--include_imports` | — | bool | `false` | Include transitive deps in FileDescriptorSet | + +**Env:** `EASYP_ROOT_GENERATE_PATH` overrides `--path`. + +--- + +## breaking + +Detect breaking API changes by comparing against a git ref. + +**Flags:** + +| Flag | Short | Type | Default | Description | +|------|-------|------|---------|-------------| +| `--path` | `-p` | string | `.` | Relative path to proto directory | +| `--against` | — | string | `master` | Git branch/ref to compare against | + +**Exit codes:** 0 = no breaking changes, 1 = breaking changes found, 2 = critical error + +--- + +## mod (alias: m) + +Package manager for proto dependencies. + +### mod download + +Download all dependencies from `deps` and `generate.inputs[].git_repo` to local cache. + +No additional flags. Exit code 1 if version not found. + +### mod update + +Update cached modules to versions specified in config. + +No additional flags. Exit code 1 if version not found. + +### mod vendor + +Copy proto files from cached dependencies into `vendor/` directory. + +No additional flags. Exit code 1 if version not found. + +--- + +## init (alias: i) + +Interactive configuration setup — creates `easyp.yaml`. + +**Flags:** + +| Flag | Short | Type | Default | Description | +|------|-------|------|---------|-------------| +| `--dir` | `-d` | string | `.` | Directory to initialize | + +**Env:** `EASYP_INIT_DIR` overrides `--dir`. + +Prompts for: lint rule groups, enum zero value suffix, service suffix, breaking check ref, generate plugins, dependencies. + +--- + +## validate-config (alias: validate) + +Validate `easyp.yaml` syntax and structure. + +**Output:** +- JSON: `{"valid": bool, "errors": [...], "warnings": [...]}` +- Text: tabular format with counts + +**Exit codes:** 0 = valid, 1 = validation errors + +--- + +## ls-files (alias: ls) + +List `.proto` files considering inputs and imports. + +**Flags:** + +| Flag | Short | Type | Default | Description | +|------|-------|------|---------|-------------| +| `--include-imports` | `-I` | bool | `true` | Include transitive import dependencies | + +**Output:** JSON or text with roots, files (source, import path, absolute path), and errors. + +--- + +## schema-gen + +Generate JSON Schema artifacts for `easyp.yaml` (for IDE autocompletion). + +**Flags:** + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--out-versioned` | string | `schemas/easyp-config-v1.schema.json` | Versioned schema output | +| `--out-latest` | string | `schemas/easyp-config.schema.json` | Latest schema alias output | + +--- + +## completion + +Generate shell completion scripts. + +### completion bash + +Outputs bash completion function for the `easyp` command. + +### completion zsh + +Outputs zsh completion function for the `easyp` command. diff --git a/.agents/skills/protobuf-expert-skill/references/config-reference.md b/.agents/skills/protobuf-expert-skill/references/config-reference.md new file mode 100644 index 0000000..ee6c350 --- /dev/null +++ b/.agents/skills/protobuf-expert-skill/references/config-reference.md @@ -0,0 +1,150 @@ +# EasyP Configuration Reference (easyp.yaml) + +Complete reference for all configuration sections and options. + +## Minimal Example + +```yaml +lint: + use: + - DEFAULT +``` + +## Full Structure + +```yaml +# ─── Lint ─────────────────────────────────────────────── +lint: + use: # Required. Rule names or group names. + - DEFAULT # Groups: MINIMAL, BASIC, DEFAULT, COMMENTS, UNARY_RPC + - COMMENTS + - PACKAGE_NO_IMPORT_CYCLE # Individual rule names also accepted + + except: # Rules to exclude (even if included by group) + - SERVICE_SUFFIX + - PACKAGE_VERSION_SUFFIX + + enum_zero_value_suffix: _UNSPECIFIED # Suffix for ENUM_ZERO_VALUE_SUFFIX rule (default: _UNSPECIFIED) + service_suffix: Service # Suffix for SERVICE_SUFFIX rule (default: Service) + + allow_comment_ignores: true # Allow // easyp:off / // easyp:on in proto files + + ignore: # Directories to skip entirely + - proto/vendor + - proto/third_party + + ignore_only: # Per-rule path ignores + FIELD_LOWER_SNAKE_CASE: + - proto/legacy + COMMENT_FIELD: + - proto/internal + +# ─── Dependencies ────────────────────────────────────── +deps: + - github.com/googleapis/googleapis # Latest default branch + - github.com/grpc-ecosystem/grpc-gateway@v2.0.0 # Specific tag/version + - github.com/user/repo@abc123def # Specific commit hash + +# ─── Code Generation ────────────────────────────────── +generate: + inputs: # Proto file sources + - directory: proto # Local directory (shorthand) + - directory: # Full form with all fields + path: proto # Path to proto files + root: . # Root directory (default: ".") + + - git_repo: # Remote git repository + url: github.com/user/repo@v1.0.0 + sub_directory: proto # Optional: subdirectory within repo + root: . # Optional: root within subdirectory + + plugins: + - name: go # Plugin source (one of: name, remote, path, command) + out: gen/go # Output directory + opts: # Plugin-specific options (key-value) + paths: source_relative + with_imports: false # Generate code for imported files too + + - remote: buf.build/grpc/go # Remote plugin from registry + out: gen/go + opts: + paths: source_relative + + - path: /usr/local/bin/protoc-gen-custom # Local binary path + out: gen/custom + + - command: # Command array to execute + - docker + - run + - --rm + - protoc-gen-custom + out: gen/custom + + managed: # Managed mode — auto-set file/field options + enabled: true + + disable: # Disable managed mode for specific targets + - module: google.protobuf # By module name + - package: com.example # By proto package + - path: proto/internal # By file path + - file_option: go_package # By file option name + - field_option: "(custom)" # By field option name + - field: pkg.Message.field # By fully qualified field name + + override: # Override specific options + - file_option: go_package # Override a file option + value: github.com/myorg/pkg + module: myapp # Optional: limit to module + package: com.example # Optional: limit to proto package + path: proto/api # Optional: limit to path + + - field_option: "(validate.rules)" # Override a field option + value: true + field: pkg.Message.field # Optional: limit to specific field + + - file_option: java_package + value: com.myorg.proto + +# ─── Breaking Change Detection ──────────────────────── +breaking: + ignore: # Paths to exclude from breaking checks + - proto/internal + - proto/experimental + + against_git_ref: main # Default git ref to compare against +``` + +## Plugin Source Priority + +Each plugin must specify exactly ONE source: + +| Source | Description | Example | +|--------|-------------|---------| +| `name` | Built-in plugin name | `go`, `go-grpc`, `grpc-gateway`, `openapiv2`, `validate-go` | +| `remote` | Registry URL | `buf.build/grpc/go` | +| `path` | Local binary path | `/usr/local/bin/protoc-gen-foo` | +| `command` | Command array | `["docker", "run", "gen-image"]` | + +Plugin `opts` values can be scalars or arrays: + +```yaml +opts: + paths: source_relative # Scalar value + require_unimplemented_servers: false # Boolean value +``` + +## Managed Mode + +When `managed.enabled: true`, EasyP automatically sets file options (like `go_package`, `java_package`) based on the proto file's package and path, reducing boilerplate. + +Use `managed.disable` to exclude specific modules, packages, or paths from managed mode. +Use `managed.override` to set specific option values for specific modules. + +## Validation + +Always validate config after editing: + +```sh +easyp validate-config +easyp validate-config --format json +``` diff --git a/.agents/skills/protobuf-expert-skill/references/installation.md b/.agents/skills/protobuf-expert-skill/references/installation.md new file mode 100644 index 0000000..0005a77 --- /dev/null +++ b/.agents/skills/protobuf-expert-skill/references/installation.md @@ -0,0 +1,58 @@ +# EasyP Installation Guide + +## Homebrew (macOS / Linux) + +```bash +brew install easyp-tech/tap/easyp +``` + +## Go Install + +Requires Go 1.21+: + +```bash +go install github.com/easyp-tech/easyp/cmd/easyp@latest +``` + +## npm + +```bash +npx easyp@latest --help +``` + +## Docker + +```bash +docker run --rm -v $(pwd):/workspace ghcr.io/easyp-tech/easyp:latest lint +``` + +## Binary from GitHub Releases + +Download the appropriate binary from [GitHub Releases](https://github.com/easyp-tech/easyp/releases): + +```bash +# Example for Linux amd64 +curl -Lo easyp https://github.com/easyp-tech/easyp/releases/latest/download/easyp_linux_amd64 +chmod +x easyp +sudo mv easyp /usr/local/bin/ +``` + +## Verify Installation + +```bash +easyp --help +``` + +## Shell Completions + +```bash +# Bash +easyp completion bash >> ~/.bashrc + +# Zsh +easyp completion zsh >> ~/.zshrc +``` + +## Official Documentation + +For the most up-to-date installation methods, see [easyp.tech/docs/guide/introduction/install](https://easyp.tech/docs/guide/introduction/install). diff --git a/.agents/skills/protobuf-expert-skill/references/lint-rules.md b/.agents/skills/protobuf-expert-skill/references/lint-rules.md new file mode 100644 index 0000000..c3b593a --- /dev/null +++ b/.agents/skills/protobuf-expert-skill/references/lint-rules.md @@ -0,0 +1,154 @@ +# EasyP Lint Rules Reference + +EasyP provides 42 lint rules organized into 5 groups. Groups are cumulative — `BASIC` includes `MINIMAL`, `DEFAULT` includes `BASIC`. + +## Rule Groups + +| Group | Rules | Description | +|-------|-------|-------------| +| `MINIMAL` | 4 | Package consistency only | +| `BASIC` | 24 | MINIMAL + naming conventions and import hygiene | +| `DEFAULT` | 32 | BASIC + enum/RPC/service/file standards | +| `COMMENTS` | 7 | Documentation requirements for all entities | +| `UNARY_RPC` | 2 | Disallow streaming RPCs | + +Use a group name directly in `lint.use` to enable all its rules. + +--- + +## MINIMAL Group (4 rules) + +| Rule | What it checks | +|------|----------------| +| `DIRECTORY_SAME_PACKAGE` | All `.proto` files in the same directory must declare the same package | +| `PACKAGE_DEFINED` | Every file must have a `package` declaration | +| `PACKAGE_DIRECTORY_MATCH` | Package name must match the directory path | +| `PACKAGE_SAME_DIRECTORY` | All files declaring the same package must be in the same directory | + +--- + +## BASIC Group (adds 20 rules) + +### Naming + +| Rule | What it checks | +|------|----------------| +| `ENUM_PASCAL_CASE` | Enum type names must be PascalCase | +| `ENUM_VALUE_UPPER_SNAKE_CASE` | Enum values must be UPPER_SNAKE_CASE | +| `FIELD_LOWER_SNAKE_CASE` | Message field names must be lower_snake_case | +| `MESSAGE_PASCAL_CASE` | Message type names must be PascalCase | +| `ONEOF_LOWER_SNAKE_CASE` | Oneof field names must be lower_snake_case | +| `PACKAGE_LOWER_SNAKE_CASE` | Package names must be lower_snake_case | +| `RPC_PASCAL_CASE` | RPC method names must be PascalCase | +| `SERVICE_PASCAL_CASE` | Service names must be PascalCase | + +### Enums + +| Rule | What it checks | +|------|----------------| +| `ENUM_FIRST_VALUE_ZERO` | First enum value must have number 0 | +| `ENUM_NO_ALLOW_ALIAS` | Enums must not use `allow_alias = true` | + +### Imports + +| Rule | What it checks | +|------|----------------| +| `IMPORT_NO_PUBLIC` | No `import public` statements | +| `IMPORT_NO_WEAK` | No `import weak` statements | +| `IMPORT_USED` | All imports must be referenced | + +### Cross-file Package Consistency + +| Rule | What it checks | +|------|----------------| +| `PACKAGE_SAME_CSHARP_NAMESPACE` | Files in same package must have same `csharp_namespace` | +| `PACKAGE_SAME_GO_PACKAGE` | Files in same package must have same `go_package` | +| `PACKAGE_SAME_JAVA_MULTIPLE_FILES` | Files in same package must have same `java_multiple_files` | +| `PACKAGE_SAME_JAVA_PACKAGE` | Files in same package must have same `java_package` | +| `PACKAGE_SAME_PHP_NAMESPACE` | Files in same package must have same `php_namespace` | +| `PACKAGE_SAME_RUBY_PACKAGE` | Files in same package must have same `ruby_package` | +| `PACKAGE_SAME_SWIFT_PREFIX` | Files in same package must have same `swift_prefix` | + +--- + +## DEFAULT Group (adds 8 rules) + +| Rule | What it checks | Config | +|------|----------------|--------| +| `ENUM_VALUE_PREFIX` | Enum values must be prefixed with the enum type name in UPPER_SNAKE_CASE | — | +| `ENUM_ZERO_VALUE_SUFFIX` | Zero-value enum entry must end with a specific suffix | `lint.enum_zero_value_suffix` (default: `_UNSPECIFIED`) | +| `FILE_LOWER_SNAKE_CASE` | `.proto` file names must be lower_snake_case | — | +| `RPC_REQUEST_RESPONSE_UNIQUE` | Each RPC request/response type must be used by only one RPC | — | +| `RPC_REQUEST_STANDARD_NAME` | RPC request type must be named `Request` | — | +| `RPC_RESPONSE_STANDARD_NAME` | RPC response type must be named `Response` | — | +| `PACKAGE_VERSION_SUFFIX` | Package must end with a version (e.g., `.v1`, `.v2beta1`) | — | +| `SERVICE_SUFFIX` | Service names must end with a configurable suffix | `lint.service_suffix` (default: `Service`) | + +--- + +## COMMENTS Group (7 rules) + +| Rule | What it checks | +|------|----------------| +| `COMMENT_ENUM` | Enum types must have a non-empty leading comment | +| `COMMENT_ENUM_VALUE` | Enum values must have a non-empty leading comment | +| `COMMENT_FIELD` | Message fields must have a non-empty leading comment | +| `COMMENT_MESSAGE` | Message types must have a non-empty leading comment | +| `COMMENT_ONEOF` | Oneof fields must have a non-empty leading comment | +| `COMMENT_RPC` | RPC methods must have a non-empty leading comment | +| `COMMENT_SERVICE` | Services must have a non-empty leading comment | + +--- + +## UNARY_RPC Group (2 rules) + +| Rule | What it checks | +|------|----------------| +| `RPC_NO_CLIENT_STREAMING` | RPCs must not use client streaming | +| `RPC_NO_SERVER_STREAMING` | RPCs must not use server streaming | + +--- + +## Uncategorized (1 rule) + +| Rule | What it checks | +|------|----------------| +| `PACKAGE_NO_IMPORT_CYCLE` | Packages must not have circular import dependencies | + +--- + +## Suppression + +### Ignoring paths + +```yaml +lint: + ignore: + - proto/vendor # Ignore entire directories + ignore_only: + FIELD_LOWER_SNAKE_CASE: + - proto/legacy # Ignore specific rule for specific paths +``` + +### Inline comment ignores + +Enable with `lint.allow_comment_ignores: true`, then use in `.proto` files: + +```protobuf +// easyp:off +message legacy_message { // This won't trigger MESSAGE_PASCAL_CASE + string BadField = 1; // This won't trigger FIELD_LOWER_SNAKE_CASE +} +// easyp:on +``` + +### Excluding rules + +```yaml +lint: + use: + - DEFAULT + except: + - SERVICE_SUFFIX # Disable individual rules from a group + - PACKAGE_VERSION_SUFFIX +``` diff --git a/.agents/skills/protobuf-expert-skill/references/migration-from-buf.md b/.agents/skills/protobuf-expert-skill/references/migration-from-buf.md new file mode 100644 index 0000000..110908a --- /dev/null +++ b/.agents/skills/protobuf-expert-skill/references/migration-from-buf.md @@ -0,0 +1,124 @@ +# Migration from buf.build to EasyP + +EasyP is a drop-in replacement for buf.build. This guide maps buf concepts to EasyP equivalents. + +## Command Mapping + +| buf command | EasyP equivalent | Notes | +|-------------|-----------------|-------| +| `buf lint` | `easyp lint` | Same rule names and groups | +| `buf breaking` | `easyp breaking` | Same detection categories | +| `buf generate` | `easyp generate` | Supports local + remote plugins | +| `buf mod update` | `easyp mod update` | Git-native dependencies | +| `buf mod init` | `easyp init` | Interactive setup wizard | +| `buf build` | `easyp generate --descriptor_set_out` | FileDescriptorSet output | +| `buf format` | — | Not yet supported | +| `buf push` | — | Not needed (uses Git repos directly) | +| `buf registry` | — | Not needed (uses Git repos directly) | + +## Config File Mapping + +| buf file | EasyP file | Notes | +|----------|-----------|-------| +| `buf.yaml` | `easyp.yaml` | Single config for all features | +| `buf.gen.yaml` | `easyp.yaml` (`generate` section) | Merged into main config | +| `buf.lock` | — | Uses Git refs for pinning | + +## Config Structure Migration + +### buf.yaml → easyp.yaml: Lint + +```yaml +# buf.yaml # easyp.yaml +version: v1 # (no version field) +lint: lint: + use: use: + - DEFAULT - DEFAULT + except: except: + - SERVICE_SUFFIX - SERVICE_SUFFIX + ignore: ignore: + - proto/vendor - proto/vendor + allow_comment_ignores: true allow_comment_ignores: true + enum_zero_value_suffix: _UNSPECIFIED enum_zero_value_suffix: _UNSPECIFIED + service_suffix: Service service_suffix: Service +``` + +### buf.yaml → easyp.yaml: Breaking + +```yaml +# buf.yaml # easyp.yaml +breaking: breaking: + use: # (no `use` — all checks enabled) + - FILE ignore: + ignore: - proto/internal + - proto/internal against_git_ref: main +``` + +### buf.yaml → easyp.yaml: Dependencies + +```yaml +# buf.yaml # easyp.yaml +deps: deps: + - buf.build/googleapis/googleapis - github.com/googleapis/googleapis + - buf.build/grpc/grpc - github.com/grpc/grpc@v1.60.0 +``` + +Key difference: buf uses BSR module references, EasyP uses **Git repository URLs** with optional `@version` or `@commit` suffixes. + +### buf.gen.yaml → easyp.yaml: Code Generation + +```yaml +# buf.gen.yaml # easyp.yaml +version: v1 generate: +plugins: inputs: + - plugin: go - directory: proto + out: gen/go plugins: + opt: paths=source_relative - name: go + - plugin: buf.build/grpc/go out: gen/go + out: gen/go opts: + opt: paths=source_relative paths: source_relative + - remote: buf.build/grpc/go + out: gen/go + opts: + paths: source_relative +``` + +Key differences: +- `opt` (string) → `opts` (key-value map) +- `plugin` → `name`, `remote`, `path`, or `command` +- Inputs are declared explicitly in `generate.inputs` + +## Lint Rule Compatibility + +EasyP supports the **same rule names and groups** as buf: + +| Group | Rules | Equivalent | +|-------|-------|-----------| +| `MINIMAL` | 4 rules | Identical | +| `BASIC` | 24 rules | Identical | +| `DEFAULT` | 32 rules | Identical | +| `COMMENTS` | 7 rules | Identical | +| `UNARY_RPC` | 2 rules | Identical | + +Inline suppression uses `// easyp:off` / `// easyp:on` (instead of `// buf:lint:ignore`). + +## Dependency Management Differences + +| Concept | buf | EasyP | +|---------|-----|-------| +| Registry | BSR (buf.build) | Git repositories | +| Pinning | `buf.lock` | `@version` or `@commit` in deps URL | +| Download | `buf mod update` | `easyp mod download` | +| Vendoring | — | `easyp mod vendor` | + +## Migration Steps + +1. **Rename config**: Create `easyp.yaml` based on your `buf.yaml` + `buf.gen.yaml` +2. **Convert deps**: Replace BSR module refs with Git repository URLs +3. **Convert plugins**: Map `plugin` + `opt` to `name`/`remote` + `opts` +4. **Download deps**: `easyp mod download` +5. **Test lint**: `easyp lint` — should produce same results +6. **Test generate**: `easyp generate` — verify output matches +7. **Test breaking**: `easyp breaking --against main` +8. **Update CI**: Replace buf actions with [easyp-tech/actions](https://github.com/easyp-tech/actions) +9. **Remove buf files**: Delete `buf.yaml`, `buf.gen.yaml`, `buf.lock` diff --git a/.agents/skills/protobuf-expert-skill/references/protobuf-best-practices.md b/.agents/skills/protobuf-expert-skill/references/protobuf-best-practices.md new file mode 100644 index 0000000..23d7aff --- /dev/null +++ b/.agents/skills/protobuf-expert-skill/references/protobuf-best-practices.md @@ -0,0 +1,237 @@ +# Protobuf Best Practices (EasyP-aligned) + +Best practices for writing `.proto` files that are idiomatic, maintainable, and pass EasyP lint rules. + +## File Organization + +### File naming +- Use `lower_snake_case.proto` for all file names → enforced by `FILE_LOWER_SNAKE_CASE` +- One service per file (or group tightly related messages) +- Name the file after the primary entity: `user_service.proto`, `order.proto` + +### Package structure +- Use dot-separated, lower_snake_case packages → enforced by `PACKAGE_LOWER_SNAKE_CASE` +- End with a version suffix → enforced by `PACKAGE_VERSION_SUFFIX` +- Match directory layout → enforced by `PACKAGE_DIRECTORY_MATCH` + +``` +proto/ + myapp/ + user/ + v1/ + user_service.proto → package myapp.user.v1; + user.proto → package myapp.user.v1; + order/ + v1/ + order_service.proto → package myapp.order.v1; +``` + +### File options +- Set `go_package`, `java_package`, etc. consistently within each package +- Enforced by `PACKAGE_SAME_GO_PACKAGE`, `PACKAGE_SAME_JAVA_PACKAGE`, etc. +- Consider using EasyP's managed mode to auto-set these + +--- + +## Naming Conventions + +| Entity | Convention | Example | EasyP Rule | +|--------|-----------|---------|------------| +| Message | PascalCase | `UserProfile` | `MESSAGE_PASCAL_CASE` | +| Field | lower_snake_case | `first_name` | `FIELD_LOWER_SNAKE_CASE` | +| Service | PascalCase + suffix | `UserService` | `SERVICE_PASCAL_CASE`, `SERVICE_SUFFIX` | +| RPC | PascalCase | `GetUser` | `RPC_PASCAL_CASE` | +| Enum type | PascalCase | `UserStatus` | `ENUM_PASCAL_CASE` | +| Enum value | UPPER_SNAKE_CASE | `USER_STATUS_ACTIVE` | `ENUM_VALUE_UPPER_SNAKE_CASE` | +| Oneof | lower_snake_case | `auth_method` | `ONEOF_LOWER_SNAKE_CASE` | +| Package | lower_snake_case | `myapp.user.v1` | `PACKAGE_LOWER_SNAKE_CASE` | +| File | lower_snake_case | `user_service.proto` | `FILE_LOWER_SNAKE_CASE` | + +--- + +## Enums + +### Zero value +Every enum must have a zero value with a suffix like `_UNSPECIFIED`: + +```protobuf +enum UserStatus { + USER_STATUS_UNSPECIFIED = 0; // Default/unknown + USER_STATUS_ACTIVE = 1; + USER_STATUS_INACTIVE = 2; +} +``` + +- Zero value rule → `ENUM_ZERO_VALUE_SUFFIX` (configurable suffix) +- First value must be 0 → `ENUM_FIRST_VALUE_ZERO` + +### Value prefixing +Prefix all values with the enum type in UPPER_SNAKE_CASE → `ENUM_VALUE_PREFIX`: + +```protobuf +// Good +enum Color { + COLOR_UNSPECIFIED = 0; + COLOR_RED = 1; +} + +// Bad — values lack prefix +enum Color { + UNSPECIFIED = 0; + RED = 1; +} +``` + +### No aliases +Avoid `allow_alias = true` → `ENUM_NO_ALLOW_ALIAS`. Use separate values instead. + +--- + +## Services and RPCs + +### Request/Response pattern +Each RPC should have unique, method-named request and response types: + +```protobuf +service UserService { + rpc GetUser(GetUserRequest) returns (GetUserResponse); + rpc ListUsers(ListUsersRequest) returns (ListUsersResponse); + rpc CreateUser(CreateUserRequest) returns (CreateUserResponse); +} +``` + +- Unique types → `RPC_REQUEST_RESPONSE_UNIQUE` +- Naming convention → `RPC_REQUEST_STANDARD_NAME`, `RPC_RESPONSE_STANDARD_NAME` + +### Service suffix +Use a consistent suffix (default: `Service`) → `SERVICE_SUFFIX` + +### Streaming considerations +If using `UNARY_RPC` rules, streaming is disallowed. Design APIs as unary where possible. Use streaming only when truly needed (large data transfers, real-time updates). + +--- + +## Comments and Documentation + +Document all public entities with leading comments: + +```protobuf +// UserService handles user account operations. +service UserService { + // GetUser retrieves a user by their unique identifier. + rpc GetUser(GetUserRequest) returns (GetUserResponse); +} + +// UserStatus represents the current state of a user account. +enum UserStatus { + // USER_STATUS_UNSPECIFIED is the default value. + USER_STATUS_UNSPECIFIED = 0; + // USER_STATUS_ACTIVE means the user can log in. + USER_STATUS_ACTIVE = 1; +} + +// GetUserRequest contains the parameters for retrieving a user. +message GetUserRequest { + // user_id is the unique identifier of the user. + string user_id = 1; +} +``` + +Enforced by `COMMENT_SERVICE`, `COMMENT_RPC`, `COMMENT_ENUM`, `COMMENT_ENUM_VALUE`, `COMMENT_MESSAGE`, `COMMENT_FIELD`, `COMMENT_ONEOF`. + +--- + +## Imports + +- Remove unused imports → `IMPORT_USED` +- Never use `import public` → `IMPORT_NO_PUBLIC` +- Never use `import weak` → `IMPORT_NO_WEAK` +- Avoid circular package imports → `PACKAGE_NO_IMPORT_CYCLE` + +--- + +## Backward Compatibility + +### Never do + +- Remove or rename fields (reserve instead) +- Change field types or numbers +- Remove enum values (reserve instead) +- Remove or rename RPCs +- Change RPC request/response types + +### Safe changes + +- Add new fields (with new field numbers) +- Add new enum values +- Add new RPCs to existing services +- Add new services +- Add new messages +- Deprecate fields with `[deprecated = true]` + +### Reservations + +When removing a field or enum value, reserve both the number and the name: + +```protobuf +message User { + reserved 3, 7; + reserved "old_field", "legacy_field"; + + string name = 1; + string email = 2; + // field 3 was old_field (removed in v1.2) +} +``` + +--- + +## API Design Patterns + +### Use wrapper messages +Always wrap request and response in dedicated messages (never use primitives directly): + +```protobuf +// Good +rpc GetUser(GetUserRequest) returns (GetUserResponse); + +// Bad — cannot evolve without breaking +rpc GetUser(google.protobuf.StringValue) returns (User); +``` + +### Pagination +For list endpoints, use cursor-based or offset pagination: + +```protobuf +message ListUsersRequest { + int32 page_size = 1; + string page_token = 2; +} + +message ListUsersResponse { + repeated User users = 1; + string next_page_token = 2; +} +``` + +### Field masks +Use `google.protobuf.FieldMask` for partial updates: + +```protobuf +message UpdateUserRequest { + User user = 1; + google.protobuf.FieldMask update_mask = 2; +} +``` + +### Standard method names +Follow consistent verb patterns: `Get`, `List`, `Create`, `Update`, `Delete`, `BatchGet`, `BatchCreate`. + +--- + +## Versioning + +- Use package version suffixes: `myapp.user.v1`, `myapp.user.v2` +- Never make breaking changes within a version — create `v2` instead +- Keep old versions running until all clients migrate +- Enforced by `PACKAGE_VERSION_SUFFIX` diff --git a/.agents/skills/protobuf-expert-skill/references/troubleshooting.md b/.agents/skills/protobuf-expert-skill/references/troubleshooting.md new file mode 100644 index 0000000..e97f85f --- /dev/null +++ b/.agents/skills/protobuf-expert-skill/references/troubleshooting.md @@ -0,0 +1,134 @@ +# EasyP Troubleshooting Guide + +## Exit Codes + +| Code | Meaning | +|------|---------| +| 0 | Success (no issues) | +| 1 | Issues found (lint violations, breaking changes, validation errors) | +| 2 | Critical error (import failure, config error, runtime crash) | + +## Common Errors + +### Import not found + +**Symptom:** `exit code 2` with message like `import "google/protobuf/timestamp.proto" not found` + +**Causes & fixes:** +1. Missing dependency — add to `deps` in `easyp.yaml`: + ```yaml + deps: + - github.com/protocolbuffers/protobuf@v25.0 + ``` +2. Dependencies not downloaded — run `easyp mod download` +3. Wrong `--path` flag — ensure it points to the correct proto directory + +### Config validation errors + +**Symptom:** `easyp validate-config` returns errors + +**Debug:** +```bash +easyp validate-config --format json +``` + +**Common causes:** +- Unknown keys (typos in field names) +- Missing required fields (`lint.use` is required) +- Invalid rule names in `lint.use` or `lint.except` +- Plugin missing exactly one source (`name`, `remote`, `path`, or `command`) + +### Lint rule not working + +**Symptom:** Expected lint violation not reported + +**Check:** +1. Rule is in `lint.use` (either directly or via group) +2. Rule is NOT in `lint.except` +3. File path is NOT in `lint.ignore` +4. Rule is NOT suppressed by `// easyp:off` in the proto file +5. Run with `--debug` for verbose output: `easyp lint --debug` + +### Breaking check fails with "could not get git ref" + +**Symptom:** `easyp breaking --against main` fails + +**Causes:** +- Shallow clone — need full history: `git fetch --unshallow` or `fetch-depth: 0` in CI +- Wrong ref name — verify with `git branch -a` or `git tag -l` +- Detached HEAD — specify explicit branch: `--against origin/main` + +### Generate produces no output + +**Symptom:** `easyp generate` runs but no files are created + +**Check:** +1. `generate.inputs` points to directory with `.proto` files +2. Plugin binary is installed and in `$PATH` (for `name` source) +3. `out` directory exists or plugin can create it +4. Run with `--debug`: `easyp generate --debug` +5. For remote plugins, check network connectivity + +### Dependency version not found + +**Symptom:** `easyp mod download` fails with exit code 1 + +**Causes:** +- Tag doesn't exist — verify tag on the repository: `git ls-remote --tags ` +- Commit hash is wrong or abbreviated — use full hash +- Repository is private — ensure Git credentials are configured + +### Circular import detected + +**Symptom:** `PACKAGE_NO_IMPORT_CYCLE` violation + +**Debug with:** +```bash +easyp ls-files --format json +``` + +This shows the full import graph. Refactor packages to break the cycle: +- Move shared types to a common package +- Use message wrappers instead of direct cross-package imports + +## Debugging Techniques + +### Verbose output + +```bash +easyp lint --debug +easyp generate --debug +``` + +### JSON output for parsing + +```bash +easyp lint --format json +easyp breaking --format json +easyp validate-config --format json +easyp ls-files --format json +``` + +### List resolved files + +```bash +easyp ls-files # With imports +easyp ls-files --include-imports=false # Without imports +``` + +### Validate config first + +Always validate before running other commands: +```bash +easyp validate-config +``` + +## Environment Variables + +| Variable | Overrides | Description | +|----------|----------|-------------| +| `EASYP_CFG` | `--cfg` | Path to config file | +| `EASYP_DEBUG` | `--debug` | Enable debug logging | +| `EASYP_FORMAT` | `--format` | Output format (text/json) | +| `EASYP_INIT_DIR` | `init --dir` | Init directory | +| `EASYP_ROOT_GENERATE_PATH` | `generate --path` | Generate path | diff --git a/.agents/skills/protoc-gen-mcp-skill/SKILL.md b/.agents/skills/protoc-gen-mcp-skill/SKILL.md new file mode 100644 index 0000000..2ec79d4 --- /dev/null +++ b/.agents/skills/protoc-gen-mcp-skill/SKILL.md @@ -0,0 +1,367 @@ +--- +name: protoc-gen-mcp-skill +description: "Build MCP servers from protobuf definitions using protoc-gen-mcp and easyp. Use when: creating an MCP server, generating MCP tools from proto files, building a proto-first MCP server in Go, configuring easyp for MCP generation, adding MCP tool annotations to protobuf services, implementing MCP tool handlers, setting up ProtoJSON-based MCP tools, or any task involving protobuf-to-MCP code generation. Also use when the user mentions protoc-gen-mcp, mcp proto, proto mcp server, easyp mcp, or wants type-safe MCP bindings from .proto files." +--- + +# protoc-gen-mcp — Proto-First MCP Server Generator + +Generate type-safe Go MCP tool bindings from annotated protobuf services. +Protobuf is the source of truth: define your service once in `.proto`, generate +both `*.pb.go` and `*.mcp.go`, implement the handler interface, and serve. + +## When to Use + +- Building a new MCP server and want type-safe, schema-validated tools +- Already have protobuf services and want to expose them as MCP tools +- Need JSON Schema validation on MCP tool inputs derived from proto definitions +- Want ProtoJSON as the wire format for MCP tool requests and responses + +## Prerequisites + +Install [easyp](https://easyp.tech) — the recommended way to lint and generate: + +```bash +brew install easyp-tech/tap/easyp +``` + +Or install from source: + +```bash +go install github.com/easyp-tech/easyp/cmd/easyp@latest +``` + +See https://easyp.tech/docs for full documentation. + +## Step-by-Step Workflow + +### Step 1: Define Your Proto Service + +Create a `.proto` file with service, methods, and MCP annotations: + +```proto +syntax = "proto3"; + +package myapi.v1; + +option go_package = "github.com/you/myproject/myapi/v1;myapiv1"; + +import "mcp/options/v1/options.proto"; +import "google/protobuf/empty.proto"; + +service MyServiceAPI { + option (mcp.options.v1.service) = { + namespace: "myapi" + description: "My tools exposed as MCP tools." + }; + + // CreateItem creates a new item. + rpc CreateItem(CreateItemRequest) returns (CreateItemResponse) { + option (mcp.options.v1.method) = { + title: "Create item" + description: "Create a new item with validation." + annotations: { read_only_hint: false } + }; + } + + // Health returns server status. + rpc Health(google.protobuf.Empty) returns (HealthResponse) { + option (mcp.options.v1.method) = { + title: "Health check" + description: "Verify the server is alive." + annotations: { read_only_hint: true } + }; + } +} + +message CreateItemRequest { + // name is required (singular, non-optional in proto3). + string name = 1 [(mcp.options.v1.field) = { + description: "Item name." + examples: [{ string_value: "Widget" }] + min_length: 1 + max_length: 200 + }]; + + // count has a default and numeric bounds. + int32 count = 2 [(mcp.options.v1.field) = { + default_value: { number_value: 1 } + minimum: 1 + maximum: 1000 + }]; + + // tags is optional because it is repeated. + repeated string tags = 3 [(mcp.options.v1.field) = { + max_items: 20 + unique_items: true + }]; + + // note is optional because of the `optional` keyword. + optional string note = 4; +} + +message CreateItemResponse { + string id = 1; +} + +message HealthResponse { + string status = 1; +} +``` + +### Step 2: Configure easyp + +Create `easyp.yaml` in your project root. This single file drives both +`protoc-gen-go` (standard Go protobuf) and `protoc-gen-mcp` (MCP bindings): + +```yaml +deps: + - github.com/easyp-tech/protoc-gen-mcp@v0.3.1 + +lint: + use: + - PACKAGE_DEFINED + - PACKAGE_VERSION_SUFFIX + - RPC_NO_CLIENT_STREAMING + - RPC_NO_SERVER_STREAMING + +generate: + inputs: + - directory: + path: proto # directory containing your .proto files + root: "." + plugins: + - name: go + out: . + opts: + paths: source_relative + - command: ["go", "run", "github.com/easyp-tech/protoc-gen-mcp/cmd/protoc-gen-mcp@latest"] + out: . + opts: + paths: source_relative +``` + +For reproducible builds, pin a specific version tag instead of `@latest`: + +```yaml + - command: ["go", "run", "github.com/easyp-tech/protoc-gen-mcp/cmd/protoc-gen-mcp@v0.3.1"] +``` + +Why easyp over raw protoc: +- Single `easyp.yaml` config manages all plugins, lint rules, and dependencies +- Both `*.pb.go` and `*.mcp.go` are generated in one command +- Built-in linting catches streaming RPCs and other unsupported patterns early +- Git-native dependency management with lock files for reproducibility +- No need to install `protoc` or manage plugin binaries manually + +### Step 3: Generate Code + +```bash +# Validate config +easyp validate-config + +# Download dependencies +easyp mod download + +# Lint proto files +easyp lint -p proto -r . + +# Generate *.pb.go and *.mcp.go +easyp generate -p proto -r . +``` + +This produces two files next to your `.proto`: +- `myapi.pb.go` — standard protobuf Go types +- `myapi.mcp.go` — MCP tool handler interface + registration + +### Step 4: Implement the Handler + +The generated code exposes a `ToolHandler` interface. Implement it: + +```go +package main + +import ( + "context" + "log" + + myapiv1 "github.com/you/myproject/myapi/v1" + "github.com/modelcontextprotocol/go-sdk/mcp" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +type handler struct{} + +func (handler) CreateItem( + _ context.Context, + req *myapiv1.CreateItemRequest, +) (*myapiv1.CreateItemResponse, error) { + return &myapiv1.CreateItemResponse{Id: "item-1"}, nil +} + +func (handler) Health( + _ context.Context, + _ *emptypb.Empty, +) (*myapiv1.HealthResponse, error) { + return &myapiv1.HealthResponse{Status: "ok"}, nil +} + +func main() { + server := mcp.NewServer(&mcp.Implementation{ + Name: "myapi-mcp", + Version: "v0.1.0", + }, nil) + + if err := myapiv1.RegisterMyServiceAPITools(server, handler{}); err != nil { + log.Fatal(err) + } + + if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil { + log.Fatal(err) + } +} +``` + +### Step 5: Run + +```bash +go run ./cmd/myserver +``` + +The server communicates over stdio. Connect any MCP client to it. The generated +tools are `myapi_CreateItem` and `myapi_Health`. + +## Key Concepts + +### Tool Naming + +Generated tool names follow the pattern `{namespace}_{MethodName}`. Dots in +the namespace are normalized to underscores. Override the method segment with +`mcp.options.v1.method.name`. + +### Requiredness Policy + +Requiredness in generated MCP JSON Schema is determined by proto3 syntax: + +| Proto Pattern | Required? | +|---|---| +| `string name = 1` (singular, no `optional`) | YES | +| `optional string name = 1` | NO | +| `repeated string names = 1` | NO | +| `map m = 1` | NO | +| `oneof choice { ... }` | NO (unless `mcp.options.v1.oneof.required = true`) | + +Fields that are not required accept explicit JSON `null`. + +### ProtoJSON Contract + +MCP tool I/O uses ProtoJSON encoding. Key differences from plain JSON: + +- `int64`/`uint64` are JSON **strings**, not numbers +- `float`/`double` accept `"NaN"`, `"Infinity"`, `"-Infinity"` as strings +- `bytes` use base64 encoding +- Enums use string names (e.g., `"FORECAST_MODE_DAILY"`) +- `Timestamp` → RFC 3339 string, `Duration` → `"3.5s"`, `FieldMask` → `"field1,field2"` + +### Supported Protobuf Features + +- Scalars, enums, nested messages, repeated, maps, `oneof`, `optional` +- Recursive messages via `$defs`/`$ref` +- Well-known types: `Any`, `Empty`, `Timestamp`, `Duration`, `FieldMask`, + `Struct`, `Value`, `ListValue`, and all scalar wrapper types + +### Fail-Fast Rules + +The generator rejects at generation time (not runtime): +- Proto2 syntax +- Streaming RPCs (client, server, or bidirectional) +- Unsupported `google.protobuf.*` types + +## Quick Proto Options Reference + +```proto +import "mcp/options/v1/options.proto"; + +// Service: namespace prefix, description, icons +option (mcp.options.v1.service) = { + namespace: "myapi" + description: "My API tools." +}; + +// Method: tool name, title, description, visibility, agent hints +option (mcp.options.v1.method) = { + name: "CustomName" + title: "Human Title" + description: "What this tool does." + hidden: true + annotations: { + read_only_hint: true + destructive_hint: false + idempotent_hint: true + } +}; + +// Field: description, examples, defaults, validation constraints +[(mcp.options.v1.field) = { + description: "Field purpose." + examples: [{ string_value: "example" }] + default_value: { number_value: 42 } + pattern: "^[A-Z]" + min_length: 1 + max_length: 255 + minimum: 0 + maximum: 100 + min_items: 1 + max_items: 50 + unique_items: true +}]; + +// Oneof: make a oneof group required in the MCP schema +option (mcp.options.v1.oneof) = { required: true }; + +// Enum: title and description for the enum type +option (mcp.options.v1.enum) = { title: "Status" }; + +// Enum value: hide sentinel zero-value from the schema +UNSPECIFIED = 0 [(mcp.options.v1.enum_value) = { hidden: true }]; +``` + +For full options details, read `references/options-reference.md` in this skill. + +## Common Patterns + +### Hide Internal RPCs + +```proto +rpc InternalDebug(DebugRequest) returns (DebugResponse) { + option (mcp.options.v1.method) = { hidden: true }; +}; +``` + +### Read-Only vs Destructive Tools + +```proto +// Read-only query +option (mcp.options.v1.method) = { + annotations: { read_only_hint: true } +}; + +// Destructive mutation +option (mcp.options.v1.method) = { + annotations: { destructive_hint: true } +}; +``` + +### Namespace Override at Registration + +```go +myapiv1.RegisterMyServiceAPITools(server, handler{}, + mcpruntime.WithNamespace("custom_prefix"), +) +``` + +## Reference Files + +For detailed lookup tables, read these files from this skill directory: + +- `references/options-reference.md` — full MCP proto options with all fields and examples +- `references/schema-mapping.md` — proto type → JSON Schema mapping, well-known types, nullability rules diff --git a/.agents/skills/protoc-gen-mcp-skill/references/options-reference.md b/.agents/skills/protoc-gen-mcp-skill/references/options-reference.md new file mode 100644 index 0000000..c05f0ac --- /dev/null +++ b/.agents/skills/protoc-gen-mcp-skill/references/options-reference.md @@ -0,0 +1,212 @@ +# MCP Proto Options Reference + +Complete reference for all `mcp.options.v1` protobuf extension options. + +Import in your `.proto` files: +```proto +import "mcp/options/v1/options.proto"; +``` + +## ServiceOptions + +Applied via `option (mcp.options.v1.service) = { ... };` inside a `service` block. + +| Field | Type | Description | +|---|---|---| +| `namespace` | `string` | Prefix for all generated tool names (e.g., `weather` → `weather_GetForecast`) | +| `description` | `string` | Overrides the service description inferred from proto comments | +| `icons` | `repeated Icon` | Default icon metadata for all tools in this service | + +```proto +service WeatherAPI { + option (mcp.options.v1.service) = { + namespace: "weather" + description: "Weather tools exposed as MCP tools." + icons: [{ + src: "https://example.com/weather.png" + mime_type: "image/png" + }] + }; +} +``` + +## MethodOptions + +Applied via `option (mcp.options.v1.method) = { ... };` inside an `rpc` block. + +| Field | Type | Description | +|---|---|---| +| `name` | `string` | Override the RPC segment of the tool name | +| `title` | `string` | Human-readable tool title | +| `description` | `string` | Override description from proto comments | +| `hidden` | `bool` | Suppress tool generation for this RPC entirely | +| `annotations` | `ToolAnnotations` | Agent hints (see below) | +| `icons` | `repeated Icon` | Per-tool icons, overrides service default | +| `execution` | `ExecutionOptions` | Execution behavior (e.g., `task_support`) | + +```proto +rpc Forecast(GetForecastRequest) returns (GetForecastResponse) { + option (mcp.options.v1.method) = { + name: "GetForecast" + title: "Get forecast" + description: "Fetch the forecast for a city." + annotations: { + read_only_hint: true + idempotent_hint: true + } + }; +} +``` + +### ToolAnnotations + +| Field | Type | Description | +|---|---|---| +| `read_only_hint` | `bool` | Tool only reads data, no side effects | +| `destructive_hint` | `bool` | Tool may delete or irreversibly modify data | +| `idempotent_hint` | `bool` | Repeated calls with same input produce same result | +| `open_world_hint` | `bool` | Tool interacts with external systems | + +### Icon + +| Field | Type | Description | +|---|---|---| +| `src` | `string` | URI to the icon resource | +| `mime_type` | `string` | MIME type (e.g., `image/png`, `image/svg+xml`) | + +### ExecutionOptions + +| Field | Type | Description | +|---|---|---| +| `task_support` | `TaskSupport` | `TASK_SUPPORT_UNSPECIFIED` or `TASK_SUPPORT_OPTIONAL` | + +## FieldOptions + +Applied via `[(mcp.options.v1.field) = { ... }]` on a message field. + +| Field | Type | Description | +|---|---|---| +| `description` | `string` | Override description from proto comments | +| `examples` | `repeated ExampleValue` | Typed example values for the schema | +| `default_value` | `ExampleValue` | Explicit default value | +| `pattern` | `string` | Regex pattern for string fields | +| `format` | `string` | JSON Schema format (e.g., `email`, `date-time`, `uri`) | +| `min_length` | `uint32` | Minimum string length | +| `max_length` | `uint32` | Maximum string length | +| `minimum` | `float` | Minimum numeric value (inclusive) | +| `maximum` | `float` | Maximum numeric value (inclusive) | +| `exclusive_minimum` | `float` | Minimum numeric value (exclusive) | +| `exclusive_maximum` | `float` | Maximum numeric value (exclusive) | +| `multiple_of` | `float` | Number must be a multiple of this value | +| `min_items` | `uint32` | Minimum array length | +| `max_items` | `uint32` | Maximum array length | +| `unique_items` | `bool` | Array items must be unique | +| `read_only` | `bool` | Mark field as read-only | + +```proto +string city = 1 [(mcp.options.v1.field) = { + description: "City name." + examples: [{ string_value: "Paris" }, { string_value: "London" }] + min_length: 1 + max_length: 100 + pattern: "^[A-Z]" +}]; + +int32 count = 2 [(mcp.options.v1.field) = { + default_value: { number_value: 10 } + minimum: 1 + maximum: 1000 +}]; + +repeated string labels = 5 [(mcp.options.v1.field) = { + min_items: 1 + max_items: 50 + unique_items: true +}]; +``` + +### ExampleValue + +Typed example values used in `examples` and `default_value`: + +| Oneof Field | Type | Usage | +|---|---|---| +| `string_value` | `string` | `{ string_value: "Paris" }` | +| `number_value` | `double` | `{ number_value: 42.5 }` | +| `integer_value` | `int64` | `{ integer_value: 10 }` | +| `bool_value` | `bool` | `{ bool_value: true }` | +| `array_value` | `ArrayValue` | `{ array_value: { items: [{ string_value: "a" }] } }` | +| `object_value` | `ObjectValue` | `{ object_value: { fields: [{ key: "k" value: { string_value: "v" } }] } }` | +| `null_value` | `NullValue` | `{ null_value: NULL_VALUE }` | + +## MessageOptions + +Applied via `option (mcp.options.v1.message) = { ... };` inside a `message` block. + +| Field | Type | Description | +|---|---|---| +| `title` | `string` | Human-readable message title | +| `description` | `string` | Message description | +| `examples` | `repeated ExampleValue` | Example message payloads | + +## OneofOptions + +Applied via `option (mcp.options.v1.oneof) = { ... };` inside a `oneof` block. + +| Field | Type | Description | +|---|---|---| +| `description` | `string` | Description of the oneof group | +| `required` | `bool` | If true, exactly one variant must be set | + +```proto +oneof selector { + option (mcp.options.v1.oneof) = { + description: "Select how to find the city." + required: true + }; + string city_alias = 41; + int64 city_id = 42; +} +``` + +## EnumOptions + +Applied via `option (mcp.options.v1.enum) = { ... };` inside an `enum` block. + +| Field | Type | Description | +|---|---|---| +| `title` | `string` | Human-readable enum title | +| `description` | `string` | Enum description | + +## EnumValueOptions + +Applied via `[(mcp.options.v1.enum_value) = { ... }]` on an enum value. + +| Field | Type | Description | +|---|---|---| +| `description` | `string` | Description for this enum value | +| `hidden` | `bool` | Hide this value from the schema (commonly used for sentinel zero-values) | + +```proto +enum ForecastMode { + option (mcp.options.v1.enum) = { + title: "Forecast Mode" + description: "Scope of the forecast." + }; + + FORECAST_MODE_NONE = 0 [(mcp.options.v1.enum_value) = { hidden: true }]; + FORECAST_MODE_DAILY = 1; + FORECAST_MODE_HOURLY = 2; +} +``` + +## Comment-Based Metadata + +Proto comments also contribute to generated metadata: + +- Plain comment lines become descriptions +- `Example: ...` adds a single schema example +- `Examples: ... | ...` adds multiple schema examples (pipe-separated) + +Field options (`mcp.options.v1.field`) take precedence when both comments and +options define the same metadata. diff --git a/.agents/skills/protoc-gen-mcp-skill/references/schema-mapping.md b/.agents/skills/protoc-gen-mcp-skill/references/schema-mapping.md new file mode 100644 index 0000000..60ae9bb --- /dev/null +++ b/.agents/skills/protoc-gen-mcp-skill/references/schema-mapping.md @@ -0,0 +1,91 @@ +# Proto Type → JSON Schema Mapping + +## Scalar Types + +| Proto Type | JSON Schema Type | Notes | +|---|---|---| +| `int32`, `sint32`, `sfixed32` | `integer` | — | +| `uint32`, `fixed32` | `integer`, `minimum: 0` | — | +| `int64`, `sint64`, `sfixed64` | `string` | ProtoJSON encodes as string | +| `uint64`, `fixed64` | `string` | ProtoJSON encodes as string | +| `float`, `double` | `number` | Also accepts `"NaN"`, `"Infinity"`, `"-Infinity"` strings | +| `bool` | `boolean` | — | +| `string` | `string` | — | +| `bytes` | `string` | base64 encoding | +| `enum` | `string` | ProtoJSON enum name strings; hidden zero-values excluded | + +## Compound Types + +| Proto Type | JSON Schema Type | Notes | +|---|---|---| +| `message` | `object` | Nested schema; recursive via `$defs`/`$ref` | +| `repeated T` | `array` of T | — | +| `map` | `object` with `additionalProperties` | Key type determines `propertyNames.pattern` | +| `oneof` | `oneOf` array | Discriminated union of variants | + +## Well-Known Types + +| Proto Type | JSON Schema | ProtoJSON Shape | +|---|---|---| +| `google.protobuf.Timestamp` | `string` (format: `date-time`) | `"2024-01-01T00:00:00Z"` | +| `google.protobuf.Duration` | `string` | `"3.5s"` | +| `google.protobuf.FieldMask` | `string` | `"field1,field2"` | +| `google.protobuf.Struct` | `object` (free-form) | `{ "key": value }` | +| `google.protobuf.Value` | any JSON value | `true`, `1.0`, `"str"`, `null`, `[]`, `{}` | +| `google.protobuf.ListValue` | `array` | `[value, ...]` | +| `google.protobuf.Any` | `object` with `@type` | `{"@type": "type.googleapis.com/...", ...}` | +| `google.protobuf.Empty` | `object` (empty) | `{}` | +| `google.protobuf.*Value` wrappers | unwrapped scalar type | `42`, `"str"`, `true` | + +Supported wrapper types: `BoolValue`, `StringValue`, `BytesValue`, +`Int32Value`, `UInt32Value`, `Int64Value`, `UInt64Value`, `FloatValue`, `DoubleValue`. + +## Map Key Patterns + +| Key Type | `propertyNames.pattern` | +|---|---| +| `string` | (no constraint) | +| `int32`, `sint32`, `sfixed32` | `^-?[0-9]+$` | +| `uint32`, `fixed32`, `uint64`, `fixed64` | `^[0-9]+$` | +| `int64`, `sint64`, `sfixed64` | `^-?[0-9]+$` | +| `bool` | `^(true\|false)$` | + +## Requiredness Decision Tree + +``` +Is the field... +├── proto3 `optional`? → NOT required, nullable +├── `repeated`? → NOT required, nullable +├── `map`? → NOT required, nullable +├── Inside a `oneof`? → NOT required (unless oneof has required=true) +├── Has FieldOptions.optional? → NOT required, nullable +└── Singular (none of above)? → REQUIRED +``` + +## Nullability Rules + +For any field NOT in the `required` array: +- Schema wraps type with null: `"type": ["string", "null"]` +- Or uses `"oneOf": [, {"type": "null"}]` for complex types +- Runtime accepts explicit JSON `null` → treated as unset in ProtoJSON + +This ensures MCP clients that validate cached `inputSchema` do not reject +otherwise valid tool calls. + +## Recursive Messages + +- First occurrence generates full schema in `$defs` +- Subsequent references use `$ref: "#/$defs/MessageName"` +- Prevents infinite schema expansion + +## ProtoJSON Special Encodings + +| Type | Encoding | Example | +|---|---|---| +| `int64`/`uint64` | JSON string | `"123456789"` | +| `float`/`double` special | Strings for non-finite | `"NaN"`, `"Infinity"`, `"-Infinity"` | +| `bytes` | base64 string | `"SGVsbG8="` | +| `enum` | string name | `"REPORT_STATUS_OK"` | +| `Timestamp` | RFC 3339 | `"2024-01-01T00:00:00Z"` | +| `Duration` | seconds with `s` | `"3.5s"` | +| `FieldMask` | comma-separated | `"field1,field2"` | diff --git a/.agents/skills/sdd/SKILL.md b/.agents/skills/sdd/SKILL.md new file mode 100644 index 0000000..1f3ebe9 --- /dev/null +++ b/.agents/skills/sdd/SKILL.md @@ -0,0 +1,293 @@ +--- +name: sdd +version: 1.5.0 +description: > + Spec-driven development pipeline with 6 phases: Explore, Requirements, + Design, Task Plan, Implementation, Review. Enforces human approval gates + between phases. Also provides a standalone documentation workflow for + generating or updating project docs without starting a feature pipeline. + Use when user wants structured feature development, spec-first approach, + or says "I want to add feature X", "new feature", "implement", "build", + "generate documentation", "update docs", "actualize the documentation". + Keywords: spec, requirements, design document, TDD plan, task plan, + implementation, code review, pipeline, approval gates, WHEN/SHALL, + generate docs, update docs, documentation queue. +--- + +# Spec-Driven Development + +You are operating in **spec-driven development mode**. +This project uses a 6-phase pipeline with human approval gates between each phase. + +## Pipeline + +``` +Explore → [APPROVE] → Requirements → [APPROVE] → Design → [APPROVE] → Task Plan → [APPROVE] → Implementation → [APPROVE] → Review → [APPROVE] → Done +``` + +Each phase has a dedicated prompt template. Read the template for the **current** phase before generating any output. + +## Quick Reference + +### Core Commands + +| Action | Command | +|--------|---------| +| Check state | `sh ./scripts/pipeline.sh status` | +| Start feature | `sh ./scripts/pipeline.sh init ` | +| Register output | `sh ./scripts/pipeline.sh artifact [path]` | +| Advance phase | `sh ./scripts/pipeline.sh approve` (only after user says "approve") | +| Mark task done | `sh ./scripts/pipeline.sh task T-N` (implementation phase only) | +| Multi-feature | Add `--feature ` before any command | + +### Decision Points + +At these moments, **ask the user** before running a command: + +#### Starting a feature (`init`) + +If config has `auto_branch: true` or `auto_worktree: true` → use the config default silently. +Otherwise, ASK: *"Create a separate branch for this feature? (branch / worktree / no)"* + +| User answer | Command | +|------------|--------| +| "branch" | `pipeline.sh init --branch ` | +| "worktree" | `pipeline.sh init --worktree ` | +| "no" / "нет" | `pipeline.sh init ` | + +#### Finishing a feature (`finish`) + +After pipeline reaches `done` and docs maintenance is handled, ASK: *"What to do with the branch? (merge / PR / keep / discard)"* + +| User answer | Command | +|------------|--------| +| "merge" | `pipeline.sh finish merge` | +| "PR" / "pull request" | `pipeline.sh finish pr` | +| "keep" / "оставить" | `pipeline.sh finish keep` | +| "discard" / "удалить" | `pipeline.sh finish discard --confirm` | +| On default branch / no git | `pipeline.sh finish keep` (auto, no question) | + +#### Documentation updates + +When `docs-check` reports issues, ASK the user (already described in Pre-flight Checklist step 3). + +| User answer | Command | +|------------|--------| +| "generate docs" | `pipeline.sh docs-init --all` | +| "update docs" | `pipeline.sh docs-init --update` | +| "skip" / "пропустить" | (no command) | + +**Hard rules:** check status first · never skip phases · never auto-approve · save artifacts to `.spec/features//` · max 3 revisions then ask user + +**Config:** `.spec/config.yaml` → `context`, `rules.`, `test_skill`, `test_reference`, `docs_dir`, `auto_branch`, `branch_prefix`, `auto_worktree`, `worktree_dir` + +**Phase flow:** read template → generate artifact → save → `artifact` → present → wait for "approve" → `approve` + +## Phases + +| # | Phase | Template | Produces | +|---|----------------|---------------------------------|---------------------------------| +| 1 | Explore | `./templates/explore.md` | Exploration & research document | +| 2 | Requirements | `./templates/requirements.md` | Formal requirements document | +| 3 | Design | `./templates/design.md` | Architecture & design document | +| 4 | Task Plan | `./templates/task-plan.md` | TDD implementation plan | +| 5 | Implementation | `./templates/implementation.md` | Implementation report | +| 6 | Review | `./templates/review.md` | Code review document | + +## State Machine + +The pipeline state is managed via a shell script: + +```sh +# Check current phase and progress +sh ./scripts/pipeline.sh status + +# Start a new feature pipeline (see Decision Points for branching options) +sh ./scripts/pipeline.sh init + +# Register the artifact you generated for the current phase +sh ./scripts/pipeline.sh artifact [path] + +# Advance to the next phase (only after user says "approve") +sh ./scripts/pipeline.sh approve + +# View revision history +sh ./scripts/pipeline.sh revisions [phase] + +# View all features and their status +sh ./scripts/pipeline.sh history + +# Mark an implementation task as completed (enables resume) +sh ./scripts/pipeline.sh task + +# Validate config file +sh ./scripts/pipeline.sh config-check + +# Inject a pre-written artifact and skip to that phase +sh ./scripts/pipeline.sh inject + +# Abandon an active pipeline +sh ./scripts/pipeline.sh abandon [feature] +``` + +For standalone documentation workflow commands (`docs-init`, `docs-next`, `docs-done`, `docs-status`, `docs-reset`), see `./templates/docs-maintenance.md`. + +For all available flags and options: `sh ./scripts/pipeline.sh help` + +### Parallel Pipelines + +When multiple features are active simultaneously, add `--feature ` before the command: + +```sh +sh ./scripts/pipeline.sh --feature auth-flow status +sh ./scripts/pipeline.sh --feature payment approve +``` + +Without the flag, the pipeline auto-detects the active feature. If more than one is active, it will error and prompt you to use `--feature`. + +## Project Configuration + +If the file `.spec/config.yaml` exists in the project root, read it before starting any phase. See `.spec/config.yaml.example` for a template with all supported keys. + +> **Format limitation:** the pipeline parser reads flat `key: value` pairs only. Nested YAML structures, multi-line values, and quoted strings are not supported. + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `context` | string | — | Project-wide background for ALL phases | +| `rules.` | string | — | Phase-specific rules (supplement template) | +| `rules.docs` | string | — | Rules for documentation generation | +| `test_skill` | string | — | Skill name for delegated test generation | +| `test_reference` | string | — | Glob/paths to representative test files | +| `docs_dir` | string | `.spec` | Directory for project documentation | +| `doc_freshness_days` | integer | `30` | Days before a generated doc is stale | +| `auto_branch` | boolean | `false` | Auto-create git branch on `init` | +| `branch_prefix` | string | `feature/` | Prefix for auto-created branches | +| `auto_worktree` | boolean | `false` | Auto-create git worktree on `init` (mutually exclusive with `auto_branch`) | +| `worktree_dir` | string | `.worktrees` | Directory for worktrees (add to `.gitignore`) | + +Phase-specific rule keys: `rules.explore`, `rules.requirements`, `rules.design`, `rules.task-plan`, `rules.implementation`, `rules.review`, `rules.docs`. + +Injection order: **context → phase rules → template instructions.** + +If the file does not exist, skip this step. + +## Standalone Documentation Workflow + +If the user requests documentation generation or update **without referring to a feature** (e.g. *"generate docs"*, *"update documentation"*, *"actualize the docs"*, *"refresh AUTH.md"*) — **do NOT run `pipeline.sh init`**. This is a standalone workflow with its own state machine. + +1. Read `./templates/docs-maintenance.md` § Standalone Documentation Workflow. +2. Run `pipeline.sh docs-init [--all|--update|