diff --git a/.agents/skills/epctl-commands/SKILL.md b/.agents/skills/epctl-commands/SKILL.md new file mode 100644 index 00000000..122bed31 --- /dev/null +++ b/.agents/skills/epctl-commands/SKILL.md @@ -0,0 +1,112 @@ +--- +name: epctl-commands +description: "Use when adding or changing EasyP CLI commands, handlers in internal/api, or registration in cmd/easyp/main.go. epctl-commands is the retained legacy skill name." +argument-hint: "Describe the EasyP CLI command you want to add or change" +--- + +# EasyP CLI Commands — Legacy Skill Name: epctl-commands + +The name epctl-commands and its installation path are retained for compatibility. This skill routes current CLI work to github.com/easyp-tech/easyp. EasyP uses github.com/urfave/cli/v2, with handlers in internal/api and registration in cmd/easyp/main.go. + +Start with [AGENTS.md](../../../AGENTS.md), the [agent rules](../../../.spec/agent-rules.md), and [.spec/CLI.md](../../../.spec/CLI.md). Read the nearest handler and its tests before extending a command. + +## Source Map + +| Responsibility | Actual source | +|----------------|---------------| +| Root cli.App, logger initialization, registration | [cmd/easyp/main.go](../../../cmd/easyp/main.go) | +| Handler contract: Command() *cli.Command | [internal/api/interface.go](../../../internal/api/interface.go) | +| Small handler example | [internal/api/schema_gen.go](../../../internal/api/schema_gen.go) | +| Group and subcommands | [internal/api/mod.go](../../../internal/api/mod.go) | +| Validation and text/JSON reports | [internal/api/validate.go](../../../internal/api/validate.go) | +| Logger lookup and core construction | [internal/api/runtime.go](../../../internal/api/runtime.go) | +| Root flags and output format selection | [internal/flags/flags.go](../../../internal/flags/flags.go), [format.go](../../../internal/flags/format.go) | + +Use neighboring file names such as schema_gen.go, mod.go, and mod_v1_update.go. Place CLI wiring in internal/api; keep operations in their existing packages: internal/modules for dependencies, internal/generation for generation orchestration, internal/migration for migration, and internal/core for engines. + +## urfave/cli v2 Pattern + +- The root is a *cli.App with a Commands slice. +- Handlers implement Command() *cli.Command, often on a small exported struct. +- Actions have signature func(ctx *cli.Context) error. Read flags through ctx.String, ctx.Bool, and related methods; use ctx.Args() for positional arguments. +- Pass ctx.Context to operations requiring context.Context. +- Groups put children in cli.Command.Subcommands. The root app's Commands and a group's Subcommands are different fields. + +This complete handler-file example adapts the existing SchemaGen handler and uses the real [schemagen API](../../../internal/schemagen/schemagen.go). It shows fresh command-local flags and separate assignment/error checking. It illustrates a replacement pattern for that file, not a second handler to add beside the existing one. + +~~~go +package api + +import ( + "fmt" + + "github.com/urfave/cli/v2" + + "github.com/easyp-tech/easyp/internal/schemagen" +) + +// SchemaGen writes JSON Schemas for the v1 YAML documents. +type SchemaGen struct{} + +var _ Handler = (*SchemaGen)(nil) + +// Command implements Handler. +func (s SchemaGen) Command() *cli.Command { + return &cli.Command{ + Name: "schema-gen", + Usage: "generate JSON Schemas for v1 YAML files", + Action: s.Action, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "out-dir", + Usage: "directory for generated v1 JSON Schemas", + Value: schemagen.DefaultOutDir, + }, + }, + } +} + +// Action writes the schemas to the selected output directory. +func (s SchemaGen) Action(ctx *cli.Context) error { + err := schemagen.Run(schemagen.Options{OutDir: ctx.String("out-dir")}) + if err != nil { + return fmt.Errorf("Run: %w", err) + } + return nil +} +~~~ + +For engine work, follow buildCore and [core.New(core.Options)](../../../internal/core/core.go). Consult the actual engine signature, such as Core.Lint(context.Context, DirWalker) ([]IssueInfo, error) in [core/lint.go](../../../internal/core/lint.go), rather than inventing a service client or server lifecycle. + +## Wiring a Command + +1. Add or update the handler in internal/api, implementing Handler. Keep parsing, output, and CLI error decisions at this boundary. +2. For a top-level command, add the handler value to the existing buildCommand(...) call in main. For example, api.SchemaGen{} is already registered there. The helper calls each handler's Command(). +3. For a subcommand, extend the parent's Subcommands slice. Follow Mod.Command, which binds actions such as m.Download and m.Update. +4. Give flags accurate names, aliases, usage text, defaults, and required behavior. Preserve public spellings and inspect parent/local flag precedence. +5. Add focused action tests and registration/argument tests where needed. Use [go-testing](../go-testing/SKILL.md) for process-state and urfave flag isolation. + +## Output, Logging, and Errors + +- The root defines --cfg (alias --config), --debug, and --format (alias -f). Text/JSON support and default format are command-specific; use flags.GetFormat where appropriate. +- Follow the target command's existing output contract. For writer-based output, use ctx.App.Writer and ctx.App.ErrWriter, with appropriate standard-stream fallbacks when actions can be invoked directly. Validate.Action demonstrates reports through the application writer. +- Preserve output write failures with %w, including buffered flush/JSON encode failures. EasyP has no universal printer abstraction or global --output mode. +- Use getLogger(ctx) for the repository logger. The root installs it in application metadata; do not introduce unrelated global logger state. +- Wrap call failures using only the callee name, such as fmt.Errorf("Run: %w", err), without a receiver/package prefix. Follow [go-code-style](../go-code-style/SKILL.md). +- Inspect the handler, runtime.go, and main.go before changing exits. Some handlers return errors, some use cli.Exit, and some call process-exit helpers. [.spec/ERRORS.md](../../../.spec/ERRORS.md) documents these boundaries; there is no general gRPC status mapper. + +## CLI Test Isolation + +Construct fresh commands, flags, mutable flag values, contexts, and writers per parallel case. urfave/cli v2 shares HelpFlag: use HideHelp: true on the test app and every command/subcommand when help is irrelevant. HideHelpCommand alone is insufficient; use HideVersion: true on the app for the shared version flag too. + +Action-only tests can use a private flag.FlagSet with cli.NewContext. Tests that alter cwd, environment, or global context/CLI hooks stay sequential or run in subprocesses; paths that exit the process need subprocess coverage. See [go-testing](../go-testing/SKILL.md) and [breaking_baseline_test.go](../../../internal/api/breaking_baseline_test.go). + +## Quick Checklist + +- [ ] Work targets the EasyP CLI; the legacy skill name/path remain intact. +- [ ] Handler and action signatures use urfave/cli v2. +- [ ] Registration uses buildCommand for root commands and Subcommands for groups. +- [ ] Operations use actual package APIs and preserve context cancellation. +- [ ] Flags, output formats, logger use, and exit behavior match the command contract. +- [ ] Errors retain causes and use callee-only labels. +- [ ] Tests isolate mutable CLI flags and respect process-state constraints. diff --git a/.agents/skills/go-code-style/SKILL.md b/.agents/skills/go-code-style/SKILL.md new file mode 100644 index 00000000..c32f4589 --- /dev/null +++ b/.agents/skills/go-code-style/SKILL.md @@ -0,0 +1,108 @@ +--- +name: go-code-style +description: "Use when writing, reviewing, or refactoring Go code or creating packages in the github.com/easyp-tech/easyp CLI repository." +argument-hint: "Describe the Go code you are writing or reviewing" +--- + +# Go Code Style — EasyP CLI + +Apply these conventions to github.com/easyp-tech/easyp, the Protocol Buffers CLI toolkit. Start with [AGENTS.md](../../../AGENTS.md), [.spec/README.md](../../../.spec/README.md), and the mandatory [agent rules](../../../.spec/agent-rules.md). Verify relevant source before changing behavior; older code may not follow every current convention. + +## Errors and Resource Cleanup + +- Wrap propagated call failures with fmt.Errorf("<callee>: %w", err). Use only the called function or method name, without a package or receiver prefix. +- For os.Open, use "Open: %w"; for source.Fetch, use "Fetch: %w"; for c.protoInfoRead, use "protoInfoRead: %w". +- Use errors.Is for sentinel identity and errors.As for typed error details. Reuse errors in their owning packages rather than inventing duplicates. +- Never use a bare defer resource.Close() or discard the close error. Wrap the deferred call and log or handle its error. Propagate finalization failures when they affect successful output. +- Log a failure where it is handled or terminates an operation; lower layers normally wrap and return it. + +Error ownership follows actual source: + +| Owner | Examples | +|-------|----------| +| [internal/core/core.go](../../../internal/core/core.go) | ErrInvalidRule, ErrRepositoryDoesNotExist, ErrEmptyInputFiles | +| [internal/core/dom.go](../../../internal/core/dom.go) | OpenImportFileError, GitRefNotFoundError | +| [internal/modules/immutable_versions.go](../../../internal/modules/immutable_versions.go) | ErrLockedVersionChanged | +| [internal/config/v1/legacy_detection.go](../../../internal/config/v1/legacy_detection.go) | ErrLegacyConfiguration | +| [internal/migration](../../../internal/migration) | Contextual planning and apply errors | + +See [.spec/ERRORS.md](../../../.spec/ERRORS.md) and the relevant CLI handler for reporting and exit behavior. There is no central domain-to-gRPC-status mapper. Follow [cmd/easyp/main.go](../../../cmd/easyp/main.go) and the handler's actual return/exit path instead of assigning a universal exit code to a sentinel. + +This complete, generic standalone example illustrates wrapping and cleanup; it is not an existing EasyP helper: + +~~~go +package example + +import ( + "fmt" + "io" + "log/slog" + "os" +) + +// ReadText reads a file and reports any close failure to the logger. +func ReadText(path string) (string, error) { + file, err := os.Open(path) + if err != nil { + return "", fmt.Errorf("Open: %w", err) + } + defer func() { + err := file.Close() + if err != nil { + slog.Error("Close", "error", err) + } + }() + + data, err := io.ReadAll(file) + if err != nil { + return "", fmt.Errorf("ReadAll: %w", err) + } + return string(data), nil +} +~~~ + +## Assignments, Imports, and Comments + +- Assign existing variables and check errors on separate lines. A block-scoped short declaration with := in an if initializer is allowed. +- Put comments above control flow, never inline on if, for, or return lines. +- Group imports as standard library, third-party, then project packages, separated by blank lines. Project imports start with github.com/easyp-tech/easyp. +- Use English comments and godoc. Every exported symbol needs a comment starting with its name. Put a package comment in one file per package. +- Format Go code with gofmt. Import grouping and tag spellings are repository conventions: [.golangci.yml](../../../.golangci.yml) explicitly enables staticcheck, not gci or tagliatelle. Do not infer enabled linters from these conventions. + +## Naming and Package Boundaries + +| Responsibility | Source to follow | +|----------------|------------------| +| CLI handlers | [internal/api](../../../internal/api), implementing Handler.Command() *cli.Command | +| Process entry and command registration | [cmd/easyp/main.go](../../../cmd/easyp/main.go) | +| Lint/breaking engines and low-level plugin execution | [internal/core](../../../internal/core) | +| Dependency resolution and repositories | [internal/modules](../../../internal/modules) | +| Generation orchestration | [internal/generation](../../../internal/generation) | +| v1 configuration and schema source | [internal/config/v1](../../../internal/config/v1) | +| Lint rules and tests | [internal/rules](../../../internal/rules), colocated <rule>.go and <rule>_test.go | + +Use exported domain types and adapter implementations where required by their consumers; keep local configuration structs unexported. Existing public models such as core.Options and v1.Policy stay exported. Follow neighboring filenames and colocate tests as <file>_test.go. + +Reserve zero for new enum types with _ = iota so an unset value is not silently valid. Preserve established public configuration spellings and formats when extending existing types. + +## Interfaces and Context + +- Put context.Context first in methods that need it, and error last in results. Preserve existing contracts that do not take a context. +- Define small interfaces in the consuming package; prefer one method when sufficient. Do not prefix interface names with I. +- Actual contracts include core.Rule and core.CurrentProjectGitWalker in [dom.go](../../../internal/core/dom.go), modules.Source in [resolve.go](../../../internal/modules/resolve.go), and repository/cache interfaces in [repository.go](../../../internal/modules/repository.go). +- Console belongs to [internal/adapters/console/new.go](../../../internal/adapters/console/new.go). Use the current owner when implementing or mocking it. +- CLI actions receive *cli.Context from urfave/cli v2; pass ctx.Context to context-aware operations. Keep cancellation and mutable state scoped to the operation. Choose concurrency from the actual engine/adapter contract. + +## Struct Tags + +Follow the existing model's tags. v1 configuration types live in internal/config/v1, shared engine configuration in internal/config. Use the established YAML/JSON keys, usually snake_case, and retain hyphenated public keys such as linters-settings and exclude-rules. Avoid adding serialization tags to internal-only types without a consumer. Never hand-edit generated protobuf code or schema JSON. + +## Quick Checklist + +- [ ] Error wraps contain the callee name only, with %w and no package/receiver prefix. +- [ ] Existing-variable assignment and error checking are separate; deferred close errors are handled. +- [ ] Imports use the repository module path and the three conventional groups. +- [ ] Exported symbols have English godoc; control-flow comments are on separate lines. +- [ ] Errors and interfaces stay with their actual owners; new enums reserve zero. +- [ ] Public tag spellings are preserved; lint claims match the current configuration. +- [ ] Tests follow [go-testing](../go-testing/SKILL.md); CLI changes follow the legacy-named [epctl-commands](../epctl-commands/SKILL.md). diff --git a/.agents/skills/go-testing/SKILL.md b/.agents/skills/go-testing/SKILL.md new file mode 100644 index 00000000..6edf1951 --- /dev/null +++ b/.agents/skills/go-testing/SKILL.md @@ -0,0 +1,159 @@ +--- +name: go-testing +description: "Use when writing, reviewing, or debugging Go tests in the github.com/easyp-tech/easyp CLI repository, including table tests, test doubles, and CLI state isolation." +argument-hint: "Describe the test you are writing or reviewing" +--- + +# Go Testing — EasyP CLI + +Follow [AGENTS.md](../../../AGENTS.md), the mandatory [agent rules](../../../.spec/agent-rules.md), and [.spec/TESTING.md](../../../.spec/TESTING.md). EasyP tests exercise a CLI, engines, configuration, generation, migration, and Git-backed dependencies. + +## Table Tests and Assertions + +- Give new slice-based cases a descriptive name string field first, followed by inputs, optional setup, and expected outputs. Existing rule tests also use named map keys. +- Prefer lowercase scenario names such as missing_manifest, invalid_policy, or canceled_context; underscores improve readability. +- Use t.Parallel() at the top level and inside isolated subtests. Apply the process-state exceptions below before adding either call. +- Construct mutable dependencies inside each subtest. A setup callback should receive that case's dependency rather than capture shared mutable state. Give filesystem cases separate t.TempDir() directories. +- Use Testify require for fatal preconditions and assert for independent value checks. Check errors and nil pointers before accessing results. +- Use require.ErrorIs for sentinel errors, require.ErrorAs for typed details, and require.ErrorContains for meaningful diagnostics without a sentinel. Use require.NoError on success; do not invent sentinels to simplify tests. + +This complete example uses the real Validate.Action API and follows [validate_test.go](../../../internal/api/validate_test.go). It can live in an internal/api test file. Each case has its own path, flag set, CLI context, and writer; it does not run the shared CLI parser setup. + +~~~go +package api + +import ( + "bytes" + "flag" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urfave/cli/v2" + + "github.com/easyp-tech/easyp/internal/flags" +) + +func TestValidateActionReportExample(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + contents string + wantHead string + wantErr error + }{ + { + name: "valid_policy", + contents: "version: v1\n", + wantHead: "VALID: true\n", + }, + { + name: "invalid_policy", + contents: "linters:\n unknown: true\n", + wantHead: "VALID: false\nERRORS:\n", + wantErr: ErrHasValidateIssue, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + path := filepath.Join(t.TempDir(), "easyp.yaml") + require.NoError(t, os.WriteFile(path, []byte(tt.contents), 0o600)) + var output bytes.Buffer + set := flag.NewFlagSet("validate-config", flag.ContinueOnError) + set.String(flags.Config.Name, "", "") + set.String(flags.Format.Name, "", "") + require.NoError(t, set.Set(flags.Config.Name, path)) + require.NoError(t, set.Set(flags.Format.Name, flags.TextFormat)) + ctx := cli.NewContext(&cli.App{Writer: &output}, set, nil) + ctx.Context = t.Context() + + err := (Validate{}).Action(ctx) + + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + } else { + require.NoError(t, err) + } + assert.True(t, bytes.HasPrefix(output.Bytes(), []byte(tt.wantHead)), output.String()) + }) + } +} +~~~ + +## Context, Cwd, and Environment Isolation + +Tests that change process cwd, environment, or global CLI/context hooks must remain sequential or run the affected behavior in an isolated subprocess. These are legitimate exceptions to parallel execution. + +- With t.Chdir or t.Setenv, neither the test nor any ancestor may call t.Parallel(). Keep its state-dependent subtests sequential too. Prefer these helpers for automatic restoration. +- A private t.TempDir() does not isolate os.Chdir, os.Setenv, os.Args, standard streams, or package-global hooks. Avoid manual process mutation; when unavoidable, restore it with cleanup and check restoration errors. +- For parallel process-level scenarios, configure each child with exec.Cmd.Dir and exec.Cmd.Env, without changing the parent process. Test paths that call os.Exit or default CLI exit handling in subprocesses. +- Ordinary per-test contexts can run in parallel. Use t.Context() or a fresh cancellable child and clean it up. Do not share a mutable *cli.Context, cli.App, metadata map, or cancellation control between cases. +- Prefer absolute paths passed to APIs when a test does not need to exercise cwd behavior. See [get_v1_test.go](../../../internal/api/get_v1_test.go) for sequential cwd/environment cases and [resolve_test.go](../../../internal/modules/resolve_test.go) for isolated cancellation cases. + +## urfave/cli v2 Parallel Tests + +A fresh cli.App alone is insufficient. The library shares cli.HelpFlag, and parsing mutates flag state. Project globals in [internal/flags/flags.go](../../../internal/flags/flags.go) and some command constructors also reuse flag pointers. + +For independent parallel parser tests: + +1. Create an app, command tree, flags, writers, and metadata per case. Allocate fresh flag values and destinations too; a shallow copy can still share GenericFlag.Value, slices, or pointers. +2. Set HideHelp: true on the app and each command/subcommand in the test tree when help is irrelevant. HideHelpCommand alone does not remove the shared help flag. Use HideVersion: true on the app to avoid the shared version flag. +3. Do not reassign cli.HelpFlag, cli.VersionFlag, exit functions, or other package globals in a parallel test. Keep tests of real help/global behavior sequential or subprocess-isolated. +4. To test just an action, use a private standard-library flag.FlagSet and cli.NewContext, as above. This does not test command registration or flag parsing; cover those separately where relevant. + +Follow [breaking_baseline_test.go](../../../internal/api/breaking_baseline_test.go) for fresh flags and help isolation, and [migrate_interactive_test.go](../../../internal/api/migrate_interactive_test.go) for a command tree with help disabled. Inspect each constructor before assuming it returns independent flags. + +## Test Doubles and Mockery + +Small handwritten consumer test doubles are appropriate. Put them in the consuming package's _test.go file, or a shared helpers_test.go when several files use them. Use names such as mockRule or fakeSource that identify the contract and behavior. Add an interface assertion when useful. + +This complete double implements the actual [core.Rule](../../../internal/core/dom.go) interface: + +~~~go +package core_test + +import "github.com/easyp-tech/easyp/internal/core" + +type mockRule struct { + issues []core.Issue + err error +} + +var _ core.Rule = (*mockRule)(nil) + +func (m *mockRule) Message() string { + return "example rule" +} + +func (m *mockRule) Validate(_ core.ProtoInfo) ([]core.Issue, error) { + return m.issues, m.err +} +~~~ + +Create a fresh double and any mutable slices/maps for each parallel case. Update doubles when their consumer interfaces change. + +Optional Mockery generation is also supported through task mock and task mocks in [Taskfile.yml](../../../Taskfile.yml). Check the selected target and actual interface owner before generation; use [.spec/TESTING.md](../../../.spec/TESTING.md) for current tooling limitations. Rule and CurrentProjectGitWalker are in internal/core; Console is in [internal/adapters/console](../../../internal/adapters/console/new.go). Regenerate generated doubles rather than hand-editing them. Handwritten doubles do not require generation. + +## Files, Packages, and Cleanup + +- Colocate tests as <file>_test.go; use Test<Function> or Test<Function>_<scenario>, following nearby names for existing suites. +- Use same-package tests for unexported behavior and external test packages for exported contracts. Both are established here: [core/generate_path_test.go](../../../internal/core/generate_path_test.go) uses package core; [rules/file_lower_snake_case_test.go](../../../internal/rules/file_lower_snake_case_test.go) uses package rules_test. +- Mark helpers with t.Helper(). Use t.Cleanup() for resources and check close errors, following [rules/init_test.go](../../../internal/rules/init_test.go). +- Read fixtures from testdata and write generated test artifacts under temporary directories. Local Git tests can use temporary repositories without a public remote. +- For behavior changes, run affected package tests with -race -count=1, subject to the task's execution constraints. See [.spec/TESTING.md](../../../.spec/TESTING.md) for Task targets; documentation-only repairs do not require a full Go suite. + +## Quick Checklist + +- [ ] Cases are named; inputs, setup, and expected outputs are clear. +- [ ] Parallel cases have private dependencies, files, contexts, flags, and writers. +- [ ] Cwd/environment/global-state cases are sequential or subprocess-isolated. +- [ ] Parallel CLI parsers isolate application flags and the library's shared help/version flags. +- [ ] Fatal preconditions use require; error assertions match the actual error contract. +- [ ] Package choice follows the tested API; doubles match actual interfaces. +- [ ] Cleanup handles errors; relevant behavior tests have been run within the authorized scope. diff --git a/.agents/skills/protobuf-expert-skill/SKILL.md b/.agents/skills/protobuf-expert-skill/SKILL.md new file mode 100644 index 00000000..e03cc550 --- /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 00000000..4c9df3bd --- /dev/null +++ b/.agents/skills/protobuf-expert-skill/assets/easyp-go-grpc.yaml @@ -0,0 +1,26 @@ +# EasyP starter config: Go + gRPC project +# Usage: copy to your project root as easyp.yaml +# Dependencies: copy assets/protobuf-common.mod as protobuf.mod + +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 00000000..6208cdba --- /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 00000000..76e7db88 --- /dev/null +++ b/.agents/skills/protobuf-expert-skill/assets/easyp-strict.yaml @@ -0,0 +1,28 @@ +# EasyP starter config: strict API project (all 42 rules) +# Usage: copy to your project root as easyp.yaml +# Dependencies: copy assets/protobuf-common.mod as protobuf.mod + +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/assets/protobuf-common.mod b/.agents/skills/protobuf-expert-skill/assets/protobuf-common.mod new file mode 100644 index 00000000..0eb301c9 --- /dev/null +++ b/.agents/skills/protobuf-expert-skill/assets/protobuf-common.mod @@ -0,0 +1,4 @@ +direct ( + github.com/googleapis/googleapis + github.com/protocolbuffers/protobuf +) 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 00000000..5870050e --- /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 00000000..0138dfd8 --- /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 00000000..f35b3341 --- /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 00000000..ad74ba1f --- /dev/null +++ b/.agents/skills/protobuf-expert-skill/references/config-reference.md @@ -0,0 +1,160 @@ +# 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 ────────────────────────────────────── +# Declared in protobuf.mod (not easyp.yaml): +# direct ( +# github.com/googleapis/googleapis@v1.0.0 +# ) + +# ─── Generate ────────────────────────────────────────── +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 +``` + +Dependencies are declared in `protobuf.mod` (not in `easyp.yaml`): + +``` +direct ( + github.com/googleapis/googleapis + github.com/grpc-ecosystem/grpc-gateway@v2.0.0 + github.com/user/repo@abc123def +) +``` + +## 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 00000000..0005a77b --- /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 00000000..c3b593af --- /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 00000000..1f4df047 --- /dev/null +++ b/.agents/skills/protobuf-expert-skill/references/migration-from-buf.md @@ -0,0 +1,132 @@ +# 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: Dependencies + +```yaml +# buf.yaml +deps: + - buf.build/googleapis/googleapis + - buf.build/grpc/grpc +``` + +``` +# protobuf.mod +direct ( + github.com/googleapis/googleapis + github.com/grpc/grpc@v1.60.0 +) +``` + +Key difference: buf uses BSR module references, EasyP uses **Git repository URLs** in `protobuf.mod` 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 00000000..23d7affd --- /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 00000000..d0ece6f5 --- /dev/null +++ b/.agents/skills/protobuf-expert-skill/references/troubleshooting.md @@ -0,0 +1,135 @@ +# 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 `protobuf.mod`: + ``` + direct ( + 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) — including legacy `deps` in `easyp.yaml` +- 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 00000000..23c9172b --- /dev/null +++ b/.agents/skills/protoc-gen-mcp-skill/SKILL.md @@ -0,0 +1,370 @@ +--- +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` and `protobuf.mod` in your project root. These drive both +`protoc-gen-go` (standard Go protobuf) and `protoc-gen-mcp` (MCP bindings): + +``` +direct ( + github.com/easyp-tech/protoc-gen-mcp@v0.3.1 +) +``` + +```yaml +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 00000000..c05f0ac0 --- /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 00000000..60ae9bbe --- /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 00000000..1f3ebe90 --- /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|