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|...]` based on user intent.
+3. Choose execution strategy (subagent recommended when available, sequential as fallback).
+4. Drive the queue: `docs-next` → generate → `docs-done` (sequential), or dispatch up to 3 subagents in parallel (subagent mode).
+
+The standalone workflow is independent from feature pipelines — it does not create `.spec/features//`, does not require approvals, and runs purely from `.spec/.docs-queue.kv` state.
+
+## Pre-flight Checklist
+
+Before starting any pipeline work, follow these steps in order:
+
+1. **Check pipeline state**: run `pipeline.sh status`.
+ - If exactly one active pipeline exists → resume from the current phase. Do NOT run `init` again.
+ - If no active pipeline → proceed to step 2.
+ - If multiple active pipelines → ask the user which feature to work on, then use `--feature ` with all subsequent commands.
+2. **Read project config**: check if `.spec/config.yaml` exists.
+ - If yes → read it, apply `context` to all phases, note `rules.*` for each phase.
+ - If no → proceed without config (defaults apply).
+3. **Check documentation** (MUST — do not skip this step): run `pipeline.sh docs-check`.
+ - **Docs directory missing** → suggest: *"Project documentation (/) not found. I can generate it to better understand your codebase. Say 'generate docs' or 'skip'."* **Wait for the user's response** before proceeding. This is a soft gate — the pipeline works without documentation, but the user must explicitly acknowledge (say 'generate docs' or 'skip').
+ - **Docs exist, stale files found** → suggest: *"Some docs are outdated (: days old). Regenerate before starting? Say 'update docs' or 'skip'."* **Wait for the user's response.** If user agrees, read `./templates/docs-maintenance.md` for the Stale doc regeneration workflow.
+ - **Docs exist, all fresh** → use as supplementary context for ALL phases. Read `/README.md` for the documentation map.
+ - If user says **"generate docs"** or **"update docs"**: read `./templates/docs-maintenance.md`, follow the workflow. Generated documentation files go to `/` (default: `.spec/`), **NOT** to `.spec/features//`.
+ - **Do NOT proceed to step 4 until the user responds** to the documentation suggestion.
+4. **Start pipeline**: run `pipeline.sh init `.
+
+For documentation generation, staleness checks, and regeneration workflows, read `./templates/docs-maintenance.md`.
+
+## When to Use This Pipeline
+
+**Use the pipeline for:**
+- New features ("add user authentication", "implement search")
+- Significant changes to existing features (new behavior, API changes, schema migrations)
+- Bug fixes that require investigation and design (root cause unknown, multiple components affected)
+
+**Do NOT use the pipeline for:**
+- Trivial changes: typo fixes, config tweaks, single-field additions, comment updates
+- Dependency updates with no code changes
+- Pure refactors with no behavioral change (unless they are large and risky)
+
+For trivial changes, just make the change directly — no pipeline needed. The skill is designed for work that **benefits from structured thinking before coding**.
+
+### Fast-track mode
+
+For **bug fixes with a known reproduction** or other small, well-understood changes:
+
+- All 6 phases still apply — do not skip phases.
+- Each phase produces a **minimal artifact**: 1-paragraph exploration, 1–2 requirements, focused design (CPs only for the bug scenario), 4–5 tasks (RED→GREEN→CODE→VERIFY→GATE), brief implementation report, short review.
+- Each template contains a "Fast-track mode" section with phase-specific minimums. Follow those rules when fast-track applies.
+
+**When to activate:** The agent activates fast-track when the user describes a bug with a known reproduction step, or a small, scoped change where investigation is unnecessary. At the start, announce: *"Using fast-track mode — all 6 phases, minimal artifacts."* If the user says "full pipeline", switch to the standard (non-abbreviated) flow.
+
+**Scope:** This pipeline is designed for a **single project or monorepo**. It is not intended for features that span multiple independent repositories. Within a monorepo, use one `.spec/` directory at the repository root.
+
+## Rules
+
+1. **MUST check status first.** Run `pipeline.sh status` before doing anything. Never generate phase output without checking status. If multiple active pipelines exist, use `--feature ` with all commands.
+2. **Never skip phases.** Follow the order: explore → requirements → design → task-plan → implementation → review.
+3. **Never auto-approve.** Wait for the user to explicitly say "approve" or equivalent.
+4. **Read the template.** Before generating output for a phase, read the corresponding template file.
+5. **Save artifacts.** Save phase artifacts (explore, requirements, design, task-plan, implementation, review) to `.spec/features//` and register them with `pipeline.sh artifact`. **Project documentation** (README.md, ARCHITECTURE.md, DOMAIN.md, etc.) goes to `/` (default: `.spec/`), NOT to `.spec/features//` — these are separate directories with separate purposes.
+6. **Each phase produces one artifact** that becomes input for the next phase.
+7. **Artifacts are cumulative.** Each phase reads all prior artifacts.
+8. **Revision limit.** If the user rejects the same artifact 3 times in a row, stop generating and ask: "We've gone through 3 revisions — could you clarify what's missing or what direction you'd prefer?" Do not continue revising without explicit guidance.
+9. **Surface uncertainty.** If you are unsure about intent, scope, or technical approach — say so explicitly. State the assumption you would make and ask the user to confirm or correct it. Never silently assume.
+10. **Write in the user's language.** Detect the user's language from their first message and use it for ALL pipeline artifacts and conversational replies. What stays in English:
+ - Formal grammar keywords: `WHEN`, `SHALL`, `the system`
+ - Requirement IDs: `REQ-X.Y`
+ - Task IDs: `T-N`
+ - Instruction keywords: `CRITICAL`, `IMPORTANT`, `NOTE`, `DO NOT`, `GOAL`
+ - Correctness Property format: `Property N`, `Category`, `For all`, `Validates`
+ - Code identifiers, file paths, shell commands, Mermaid node labels
+ - Documentation in `/` (`.spec/`) — always English (see `templates/docs/README.md`)
+
+ Everything else — prose, section headers, descriptions, interview questions, explanations — is written in the user's language.
+
+## Error Recovery
+
+- **Revising an artifact:** Overwrite the file, re-register with `pipeline.sh artifact`, and present the updated version to the user. The previous version is automatically saved as a revision in the feature’s `revisions/` directory. Use `pipeline.sh revisions` to view past revisions.
+
+
+## Documentation Maintenance
+
+After the pipeline reaches `phase=done`, read `./templates/docs-maintenance.md` § Documentation Maintenance to check if project documentation needs updating.
+
+## Branch Finishing
+
+After documentation maintenance is complete (or skipped), follow the **Finishing a feature** Decision Point in Quick Reference above.
+
+If on the default branch (main/master) or git is unavailable, run `pipeline.sh finish keep` automatically — no question needed.
+
+This is a soft suggestion, not a blocker. If the user ignores it, the pipeline is still complete.
+
+## Quick Start (for the agent)
+
+When the user says something like "I want to add feature X":
+
+1. Follow the **Pre-flight Checklist** (status → config → docs-check → init)
+2. Read `./templates/explore.md` — investigate the problem space (use `.spec/` docs as context if available)
+3. Generate the exploration document → save to `.spec/features//explore.md`
+4. Run `pipeline.sh artifact`
+5. Present to user → wait for "approve"
+6. Run `pipeline.sh approve` → phase advances to requirements
+7. Read `./templates/requirements.md` → follow its interview process
+8. Generate the requirements document → save, register artifact, present, wait for approve
+9. Repeat for design phase
+10. Read `./templates/task-plan.md` → generate TDD implementation plan (no code yet)
+11. Save, register artifact, present, wait for approve
+12. Read `./templates/implementation.md` → execute the task plan (write tests, write code, mark tasks done)
+13. Save implementation report, register artifact, present, wait for approve
+14. Read `./templates/review.md` → review the written code against all prior artifacts
+15. Present review document with findings and verdict → wait for user instructions
+16. If user asks to fix findings → fix → generate new review → present again → wait for approve
+17. After review is approved → `pipeline.sh approve` → pipeline complete
+18. Check if documentation needs updating (see Documentation Maintenance)
+19. Check if the feature branch needs finalizing (see Branch Finishing) → present options → `pipeline.sh finish`
diff --git a/.agents/skills/sdd/scripts/pipeline.sh b/.agents/skills/sdd/scripts/pipeline.sh
new file mode 100755
index 00000000..3cecf09a
--- /dev/null
+++ b/.agents/skills/sdd/scripts/pipeline.sh
@@ -0,0 +1,1723 @@
+#!/usr/bin/env sh
+# Spec-Driven Dev Pipeline — state machine (POSIX sh, zero dependencies)
+# Usage: sh pipeline.sh [--feature ] [args]
+#
+# Shell compatibility: requires sh with `local` support (bash, dash, ash, zsh).
+#
+# Global flags:
+# --feature Specify which feature to operate on (required when
+# multiple pipelines are active simultaneously)
+#
+# Commands:
+# init [--branch|--no-branch]
+# Start a new pipeline for a feature
+# --branch: create git branch (prefix from config, default: feature/)
+# --no-branch: skip branch creation even if auto_branch is set in config
+# status Show current phase, feature, and artifacts
+# approve Advance to next phase (requires artifact)
+# artifact [path] Register artifact for current phase
+# history Show all features and their status
+# revisions [phase] Show revision history for current or specified phase
+# docs-check Check project documentation status
+# task Mark implementation task as completed (resume tracking)
+# version Show version
+# help Show this help message
+
+set -e
+
+PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+FEATURES_DIR="$PROJECT_ROOT/.spec/features"
+CONFIG_FILE="$PROJECT_ROOT/.spec/config.yaml"
+
+# --- helpers ---
+
+VERSION="1.5.0"
+EXPLICIT_FEATURE=""
+
+die() { echo "ERROR: $*" >&2; exit 1; }
+info() { echo "→ $*"; }
+warn() { echo "⚠ $*" >&2; }
+
+iso_now() {
+ date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%SZ"
+}
+
+iso_now_compact() {
+ date -u +"%Y-%m-%dT%H-%M-%SZ" 2>/dev/null || date +"%Y-%m-%dT%H-%M-%SZ"
+}
+
+# Escape a string for safe embedding in JSON values (RFC 8259)
+json_escape() {
+ printf '%s' "$1" | awk '
+ BEGIN { ORS="" }
+ {
+ gsub(/\\/, "\\\\")
+ gsub(/"/, "\\\"")
+ gsub(/\t/, "\\t")
+ gsub(/\r/, "\\r")
+ if (NR > 1) printf "\\n"
+ printf "%s", $0
+ }'
+}
+
+# Read a value from .spec/config.yaml (simple grep-based, no YAML parser)
+# Usage: read_config [default]
+# Returns the value or default (empty string if no default)
+read_config() {
+ local key="$1" default="${2:-}"
+ if [ -f "$CONFIG_FILE" ]; then
+ local val
+ val="$(grep "^${key}:" "$CONFIG_FILE" 2>/dev/null | head -1 | sed "s/^${key}:[[:space:]]*//" | sed 's/[[:space:]]*$//')"
+ if [ -n "$val" ]; then
+ printf '%s' "$val"
+ return
+ fi
+ fi
+ printf '%s' "$default"
+}
+
+# --- per-feature state ---
+
+# Current feature paths (set by set_feature_context / resolve_feature)
+FEATURE_DIR=""
+STATE_FILE=""
+KV_FILE=""
+REVISIONS_DIR=""
+APPROVED_DIR=""
+
+set_feature_context() {
+ # set_feature_context — sets global paths for the feature
+ FEATURE_DIR="$FEATURES_DIR/$1"
+ KV_FILE="$FEATURE_DIR/pipeline.kv"
+ STATE_FILE="$FEATURE_DIR/pipeline.json"
+ REVISIONS_DIR="$FEATURE_DIR/revisions"
+ APPROVED_DIR="$FEATURE_DIR/approved"
+}
+
+ensure_feature_dir() {
+ # ensure_feature_dir — creates feature directory structure
+ local fdir="$FEATURES_DIR/$1"
+ mkdir -p "$fdir" "$fdir/revisions" "$fdir/approved"
+}
+
+read_field() {
+ [ -f "$KV_FILE" ] || return 1
+ local _line
+ _line="$(grep "^$1=" "$KV_FILE" 2>/dev/null | head -1)" || return 1
+ [ -n "$_line" ] || return 1
+ printf '%s' "$_line" | cut -d'=' -f2-
+}
+
+validate_kv() {
+ # Verify required fields exist in KV store; die with diagnostic on failure
+ [ -f "$KV_FILE" ] || die "Pipeline state file missing: $KV_FILE"
+ local missing=""
+ for field in feature phase created_at; do
+ grep -q "^${field}=" "$KV_FILE" 2>/dev/null || missing="$missing $field"
+ done
+ if [ -n "$missing" ]; then
+ die "Corrupted pipeline state ($KV_FILE): missing fields:$missing. Fix the file manually or remove and re-init."
+ fi
+ # Verify every line matches key=value format (key: lowercase + digits + underscore)
+ local line_num=0
+ while IFS= read -r line || [ -n "$line" ]; do
+ line_num=$((line_num + 1))
+ case "$line" in
+ "") continue ;; # skip blank lines
+ [a-z_]*=*) ;; # valid key=value
+ *) die "Corrupted pipeline state ($KV_FILE): invalid line $line_num: $line" ;;
+ esac
+ done < "$KV_FILE"
+}
+
+# Escape a value for safe use in sed replacement string
+kv_escape_sed() {
+ printf '%s' "$1" | sed -e 's/[&\\/|]/\\&/g'
+}
+
+# Validate that a value is safe for the KV store (no =, |, or newlines)
+kv_validate_value() {
+ case "$1" in
+ *'='*) die "KV value must not contain '=': $1" ;;
+ *'|'*) die "KV value must not contain '|': $1" ;;
+ esac
+ # Check for newlines by comparing line count (portable across POSIX shells)
+ local line_count
+ line_count="$(printf '%s' "$1" | wc -l)"
+ if [ "$line_count" -ne 0 ]; then
+ die "KV value must not contain newlines: $1"
+ fi
+}
+
+# Validate artifact path: reject directory traversal and control characters
+validate_artifact_path() {
+ case "$1" in
+ */../*|*/..) die "Artifact path must not contain '..' traversal" ;;
+ ../*|..) die "Artifact path must not contain '..' traversal" ;;
+ esac
+ if printf '%s' "$1" | grep -q '[[:cntrl:]]' 2>/dev/null; then
+ die "Artifact path must not contain control characters"
+ fi
+}
+
+write_field() {
+ kv_validate_value "$2"
+ if [ -f "$KV_FILE" ] && grep -q "^$1=" "$KV_FILE" 2>/dev/null; then
+ local tmp="$KV_FILE.tmp"
+ local escaped
+ escaped="$(kv_escape_sed "$2")"
+ sed "s|^$1=.*|$1=$escaped|" "$KV_FILE" > "$tmp" && mv "$tmp" "$KV_FILE"
+ else
+ echo "$1=$2" >> "$KV_FILE"
+ fi
+}
+
+detect_active_feature() {
+ # Scan all features, return the one with phase != done
+ [ -d "$FEATURES_DIR" ] || return 0
+ local active=""
+ local count=0
+ for kv in "$FEATURES_DIR"/*/pipeline.kv; do
+ [ -f "$kv" ] || continue
+ local phase
+ phase="$(grep "^phase=" "$kv" 2>/dev/null | head -1 | cut -d'=' -f2-)"
+ if [ -n "$phase" ] && [ "$phase" != "done" ]; then
+ local fname
+ fname="$(grep "^feature=" "$kv" 2>/dev/null | head -1 | cut -d'=' -f2-)"
+ active="$fname"
+ count=$((count + 1))
+ fi
+ done
+ if [ "$count" -gt 1 ]; then
+ warn "Multiple active pipelines found:"
+ for kv in "$FEATURES_DIR"/*/pipeline.kv; do
+ [ -f "$kv" ] || continue
+ local phase fname
+ phase="$(grep "^phase=" "$kv" 2>/dev/null | head -1 | cut -d'=' -f2-)"
+ fname="$(grep "^feature=" "$kv" 2>/dev/null | head -1 | cut -d'=' -f2-)"
+ if [ -n "$phase" ] && [ "$phase" != "done" ]; then
+ echo " - $fname (phase: $phase)" >&2
+ fi
+ done
+ return 1
+ fi
+ [ -n "$active" ] && echo "$active"
+}
+
+resolve_feature() {
+ if [ -n "$EXPLICIT_FEATURE" ]; then
+ # Validate that the explicitly specified feature exists
+ if [ ! -f "$FEATURES_DIR/$EXPLICIT_FEATURE/pipeline.kv" ]; then
+ die "Feature '$EXPLICIT_FEATURE' not found. Run 'pipeline.sh history' to list features."
+ fi
+ set_feature_context "$EXPLICIT_FEATURE"
+ validate_kv
+ return 0
+ fi
+ local feat
+ feat="$(detect_active_feature)" || { warn "Hint: use --feature to select one."; return 1; }
+ if [ -z "$feat" ]; then
+ return 1
+ fi
+ set_feature_context "$feat"
+ validate_kv
+ return 0
+}
+
+next_phase() {
+ case "$1" in
+ explore) echo "requirements" ;;
+ requirements) echo "design" ;;
+ design) echo "task-plan" ;;
+ task-plan) echo "implementation" ;;
+ implementation) echo "review" ;;
+ review) echo "done" ;;
+ done) echo "" ;;
+ *) echo "" ;;
+ esac
+}
+
+phase_number() {
+ case "$1" in
+ explore) echo "1" ;;
+ requirements) echo "2" ;;
+ design) echo "3" ;;
+ task-plan) echo "4" ;;
+ implementation) echo "5" ;;
+ review) echo "6" ;;
+ done) echo "✓" ;;
+ *) echo "?" ;;
+ esac
+}
+
+# Numeric ordering for comparisons (phase_number is for display)
+phase_order() {
+ case "$1" in
+ explore) echo 1 ;;
+ requirements) echo 2 ;;
+ design) echo 3 ;;
+ task-plan) echo 4 ;;
+ implementation) echo 5 ;;
+ review) echo 6 ;;
+ done) echo 7 ;;
+ *) echo 0 ;;
+ esac
+}
+
+# Rebuild the JSON file from KV store (for agents to read)
+# Uses atomic write (tmp + mv) to prevent corruption on interruption
+rebuild_json() {
+ validate_kv
+ local feature phase created artifact
+ feature="$(json_escape "$(read_field feature)")"
+ phase="$(read_field phase)"
+ created="$(read_field created_at)"
+ artifact="$(read_field current_artifact)"
+ local history_count
+ history_count="$(read_field history_count)"
+ [ -z "$history_count" ] && history_count=0
+
+ local tmp_file="$STATE_FILE.tmp"
+ {
+ printf '{\n'
+ printf ' "feature": "%s",\n' "$feature"
+ printf ' "phase": "%s",\n' "$phase"
+ printf ' "created_at": "%s",\n' "$created"
+ if [ -n "$artifact" ]; then
+ printf ' "current_artifact": "%s",\n' "$(json_escape "$artifact")"
+ else
+ printf ' "current_artifact": null,\n'
+ fi
+ printf ' "history": [\n'
+
+ local i=0
+ while [ "$i" -lt "$history_count" ]; do
+ local h_phase h_artifact h_approved
+ h_phase="$(read_field "history_${i}_phase")"
+ h_artifact="$(json_escape "$(read_field "history_${i}_artifact")")"
+ h_approved="$(read_field "history_${i}_approved_at")"
+ [ "$i" -gt 0 ] && printf ',\n'
+ printf ' {"phase": "%s", "artifact": "%s", "approved_at": "%s"}' \
+ "$h_phase" "$h_artifact" "$h_approved"
+ i=$((i + 1))
+ done
+
+ printf '\n ],\n'
+
+ # Include review_base_commit if set
+ local rbc
+ rbc="$(read_field review_base_commit 2>/dev/null || echo "")"
+ if [ -n "$rbc" ]; then
+ printf ' "review_base_commit": "%s",\n' "$(json_escape "$rbc")"
+ else
+ printf ' "review_base_commit": null,\n'
+ fi
+
+ # Include branch if set
+ local br
+ br="$(read_field branch 2>/dev/null || echo "")"
+ if [ -n "$br" ]; then
+ printf ' "branch": "%s",\n' "$(json_escape "$br")"
+ else
+ printf ' "branch": null,\n'
+ fi
+
+ # Include worktree if set
+ local wt
+ wt="$(read_field worktree 2>/dev/null || echo "")"
+ if [ -n "$wt" ]; then
+ printf ' "worktree": "%s",\n' "$(json_escape "$wt")"
+ else
+ printf ' "worktree": null,\n'
+ fi
+
+ # Include last_completed_task if set
+ local lct
+ lct="$(read_field last_completed_task 2>/dev/null || echo "")"
+ if [ -n "$lct" ]; then
+ printf ' "last_completed_task": "%s",\n' "$(json_escape "$lct")"
+ else
+ printf ' "last_completed_task": null,\n'
+ fi
+
+ # Include finish fields if set
+ local fa ft fb
+ fa="$(read_field finish_action 2>/dev/null || echo "")"
+ ft="$(read_field finished_at 2>/dev/null || echo "")"
+ fb="$(read_field finish_base 2>/dev/null || echo "")"
+ if [ -n "$fa" ]; then
+ printf ' "finish_action": "%s",\n' "$(json_escape "$fa")"
+ printf ' "finished_at": "%s",\n' "$(json_escape "$ft")"
+ if [ -n "$fb" ]; then
+ printf ' "finish_base": "%s"\n' "$(json_escape "$fb")"
+ else
+ printf ' "finish_base": null\n'
+ fi
+ else
+ printf ' "finish_action": null,\n'
+ printf ' "finished_at": null,\n'
+ printf ' "finish_base": null\n'
+ fi
+
+ printf '}\n'
+ } > "$tmp_file"
+ mv -f "$tmp_file" "$STATE_FILE"
+}
+
+# --- commands ---
+
+cmd_init() {
+ # Parse init-specific flags
+ local do_branch=""
+ local do_worktree=""
+ local feature=""
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ --branch) do_branch="yes"; shift ;;
+ --worktree) do_worktree="yes"; shift ;;
+ --no-branch) do_branch="no"; do_worktree="no"; shift ;;
+ -*) die "Unknown flag for init: $1" ;;
+ *)
+ [ -n "$feature" ] && die "Unexpected argument: $1"
+ feature="$1"; shift
+ ;;
+ esac
+ done
+
+ [ -z "$feature" ] && die "Usage: pipeline.sh init [--branch|--worktree|--no-branch] "
+
+ # Mutual exclusion
+ if [ "$do_branch" = "yes" ] && [ "$do_worktree" = "yes" ]; then
+ die "--branch and --worktree are mutually exclusive."
+ fi
+
+ # Validate feature name (kebab-case)
+ case "$feature" in
+ *[!a-z0-9-]*) die "Feature name must be kebab-case (e.g. grpc-streaming-support)" ;;
+ -*|*-) die "Feature name must be kebab-case (e.g. grpc-streaming-support)" ;;
+ *--*) die "Feature name must be kebab-case (e.g. grpc-streaming-support)" ;;
+ [!a-z]*) die "Feature name must be kebab-case (e.g. grpc-streaming-support)" ;;
+ esac
+
+ if [ ${#feature} -gt 64 ]; then
+ die "Feature name too long (max 64 chars): $feature"
+ fi
+
+ # Resolve branch/worktree creation: flag > config > default (neither)
+ if [ -z "$do_branch" ] && [ -z "$do_worktree" ]; then
+ local auto_branch auto_worktree
+ auto_worktree="$(read_config auto_worktree "false")"
+ case "$auto_worktree" in
+ true|yes|1) do_worktree="yes" ;;
+ esac
+ if [ "$do_worktree" != "yes" ]; then
+ auto_branch="$(read_config auto_branch "false")"
+ case "$auto_branch" in
+ true|yes|1) do_branch="yes" ;;
+ *) do_branch="no" ;;
+ esac
+ fi
+ fi
+
+ local branch_name=""
+ local worktree_path=""
+
+ if [ "$do_worktree" = "yes" ]; then
+ # --- Worktree mode ---
+ if ! command -v git >/dev/null 2>&1; then
+ die "Git not found. Cannot create worktree."
+ fi
+ if ! git rev-parse --git-dir >/dev/null 2>&1; then
+ die "Not a git repository. Cannot create worktree."
+ fi
+
+ local prefix wt_dir
+ prefix="$(read_config branch_prefix "feature/")"
+ wt_dir="$(read_config worktree_dir ".worktrees")"
+ branch_name="${prefix}${feature}"
+ worktree_path="${wt_dir}/${feature}"
+
+ # Check if branch already exists
+ if git rev-parse --verify "$branch_name" >/dev/null 2>&1; then
+ die "Branch '$branch_name' already exists."
+ fi
+
+ # Warn if worktree_dir is not in .gitignore
+ if [ -f ".gitignore" ]; then
+ if ! grep -qx "$wt_dir" .gitignore 2>/dev/null && ! grep -qx "$wt_dir/" .gitignore 2>/dev/null; then
+ warn "Worktree directory '$wt_dir' is not in .gitignore. Consider adding it."
+ fi
+ else
+ warn "No .gitignore found. Consider adding '$wt_dir' to .gitignore."
+ fi
+
+ git worktree add "$worktree_path" -b "$branch_name" || die "Failed to create worktree at '$worktree_path'."
+ info "Created worktree: $worktree_path (branch: $branch_name)"
+
+ elif [ "$do_branch" = "yes" ]; then
+ # Verify git is available
+ if ! command -v git >/dev/null 2>&1; then
+ die "Git not found. Cannot create branch."
+ fi
+ if ! git rev-parse --git-dir >/dev/null 2>&1; then
+ die "Not a git repository. Cannot create branch."
+ fi
+
+ local prefix
+ prefix="$(read_config branch_prefix "feature/")"
+ branch_name="${prefix}${feature}"
+
+ # Check if branch already exists
+ if git rev-parse --verify "$branch_name" >/dev/null 2>&1; then
+ die "Branch '$branch_name' already exists."
+ fi
+
+ # Warn about dirty working tree
+ if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then
+ warn "Working tree has uncommitted changes."
+ fi
+
+ git checkout -b "$branch_name" || die "Failed to create branch '$branch_name'."
+ info "Created branch: $branch_name"
+ fi
+
+ local fdir="$FEATURES_DIR/$feature"
+
+ # Check if feature already exists
+ if [ -f "$fdir/pipeline.kv" ]; then
+ local existing_phase
+ existing_phase="$(grep "^phase=" "$fdir/pipeline.kv" 2>/dev/null | head -1 | cut -d'=' -f2-)"
+ if [ "$existing_phase" = "done" ]; then
+ die "Feature '$feature' already completed. Choose a different name."
+ else
+ warn "Active pipeline for '$feature' exists (phase: $existing_phase)"
+ die "Complete or choose a different feature name."
+ fi
+ fi
+
+ ensure_feature_dir "$feature"
+ set_feature_context "$feature"
+
+ # Initialize KV store
+ {
+ echo "feature=$feature"
+ echo "phase=explore"
+ echo "created_at=$(iso_now)"
+ echo "current_artifact="
+ echo "history_count=0"
+ if [ -n "$branch_name" ]; then
+ echo "branch=$branch_name"
+ fi
+ if [ -n "$worktree_path" ]; then
+ echo "worktree=$worktree_path"
+ fi
+ } > "$KV_FILE"
+
+ rebuild_json
+ info "Pipeline initialized for '$feature'"
+ if [ -n "$worktree_path" ]; then
+ info "Worktree: $worktree_path (branch: $branch_name)"
+ elif [ -n "$branch_name" ]; then
+ info "Branch: $branch_name"
+ fi
+ info "Phase: [1/6] explore"
+ info "Artifacts: .spec/features/$feature/"
+ info "Read template: ./templates/explore.md"
+}
+
+cmd_status() {
+ if ! resolve_feature; then
+ info "No active pipeline."
+ # Show completed features if any
+ if [ -d "$FEATURES_DIR" ]; then
+ local has_completed=0
+ for kv in "$FEATURES_DIR"/*/pipeline.kv; do
+ [ -f "$kv" ] || continue
+ local phase
+ phase="$(grep "^phase=" "$kv" 2>/dev/null | head -1 | cut -d'=' -f2-)"
+ if [ "$phase" = "done" ]; then
+ if [ "$has_completed" -eq 0 ]; then
+ echo ""
+ echo "Completed features:"
+ has_completed=1
+ fi
+ local fname
+ fname="$(grep "^feature=" "$kv" 2>/dev/null | head -1 | cut -d'=' -f2-)"
+ printf " ✓ %s\n" "$fname"
+ fi
+ done
+ fi
+ echo ""
+ info "Run: pipeline.sh init "
+ return 0
+ fi
+
+ local feature phase artifact history_count
+ feature="$(read_field feature)"
+ phase="$(read_field phase)"
+ artifact="$(read_field current_artifact)"
+ history_count="$(read_field history_count)"
+ [ -z "$history_count" ] && history_count=0
+
+ echo ""
+ echo "┌─────────────────────────────────────────────┐"
+ printf "│ Feature: %-35s│\n" "$feature"
+ printf "│ Phase: [%s/6] %-30s│\n" "$(phase_number "$phase")" "$phase"
+ # Show branch/worktree info
+ local br_info wt_info
+ br_info="$(read_field branch 2>/dev/null || echo "")"
+ wt_info="$(read_field worktree 2>/dev/null || echo "")"
+ if [ -n "$wt_info" ]; then
+ printf "│ Worktree: %-34s│\n" "$wt_info"
+ elif [ -n "$br_info" ]; then
+ printf "│ Branch: %-35s│\n" "$br_info"
+ fi
+ if [ -n "$artifact" ]; then
+ printf "│ Artifact: %-34s│\n" "$artifact"
+ else
+ printf "│ Artifact: %-34s│\n" "(none — register before approve)"
+ fi
+ # Show last completed task during implementation phase
+ if [ "$phase" = "implementation" ]; then
+ local lct
+ lct="$(read_field last_completed_task 2>/dev/null || echo "")"
+ if [ -n "$lct" ]; then
+ printf "│ Last task: %-33s│\n" "$lct"
+ fi
+ fi
+ echo "├─────────────────────────────────────────────┤"
+
+ # Show pipeline progress
+ local e_mark="○" r_mark="○" d_mark="○" t_mark="○" i_mark="○" rev_mark="○"
+ case "$phase" in
+ explore) e_mark="●" ;;
+ requirements) e_mark="✓"; r_mark="●" ;;
+ design) e_mark="✓"; r_mark="✓"; d_mark="●" ;;
+ task-plan) e_mark="✓"; r_mark="✓"; d_mark="✓"; t_mark="●" ;;
+ implementation) e_mark="✓"; r_mark="✓"; d_mark="✓"; t_mark="✓"; i_mark="●" ;;
+ review) e_mark="✓"; r_mark="✓"; d_mark="✓"; t_mark="✓"; i_mark="✓"; rev_mark="●" ;;
+ done) e_mark="✓"; r_mark="✓"; d_mark="✓"; t_mark="✓"; i_mark="✓"; rev_mark="✓" ;;
+ esac
+ printf "│ %s Ex → %s Rq → %s Ds → %s Tp → %s Im → %s Rv │\n" "$e_mark" "$r_mark" "$d_mark" "$t_mark" "$i_mark" "$rev_mark"
+ echo "└─────────────────────────────────────────────┘"
+
+ # Show history
+ if [ "$history_count" -gt 0 ]; then
+ echo ""
+ echo "Completed phases:"
+ local i=0
+ while [ "$i" -lt "$history_count" ]; do
+ local h_phase h_artifact h_approved
+ h_phase="$(read_field "history_${i}_phase")"
+ h_artifact="$(read_field "history_${i}_artifact")"
+ h_approved="$(read_field "history_${i}_approved_at")"
+ printf " [%s] %-15s → %s (approved: %s)\n" "$((i+1))" "$h_phase" "$h_artifact" "$h_approved"
+ i=$((i + 1))
+ done
+ fi
+
+ # Hint for next action
+ echo ""
+ if [ "$phase" = "done" ]; then
+ info "Pipeline complete."
+ elif [ -z "$artifact" ]; then
+ info "Next: register artifact with 'pipeline.sh artifact '"
+ info "Then: 'pipeline.sh approve' after user approval"
+ else
+ info "Artifact registered. Ask user to approve, then run 'pipeline.sh approve'"
+ fi
+ echo ""
+}
+
+cmd_artifact() {
+ resolve_feature || die "No active pipeline. Run 'pipeline.sh init ' first."
+
+ local phase
+ phase="$(read_field phase)"
+ [ "$phase" = "done" ] && die "Pipeline is complete. Nothing to register."
+
+ local path="$1"
+
+ # If no path given, use the default: .spec/features//.md
+ if [ -z "$path" ]; then
+ path="$FEATURE_DIR/${phase}.md"
+ fi
+
+ validate_artifact_path "$path"
+ [ -f "$path" ] || die "Artifact file does not exist: $path"
+
+ # Save a snapshot of the artifact being registered (revision tracking)
+ local rev_count
+ rev_count="$(read_field "revision_count_${phase}")"
+ [ -z "$rev_count" ] && rev_count=0
+ rev_count=$((rev_count + 1))
+ local rev_name
+ rev_name="${phase}-rev-${rev_count}-$(iso_now_compact).md"
+ cp "$path" "$REVISIONS_DIR/$rev_name"
+ write_field "revision_count_${phase}" "$rev_count"
+ if [ "$rev_count" -gt 1 ]; then
+ info "Revision $rev_count saved: $rev_name"
+ fi
+
+ write_field current_artifact "$path"
+ rebuild_json
+ info "Artifact registered for phase '$phase': $path"
+}
+
+cmd_approve() {
+ resolve_feature || die "No active pipeline."
+
+ local phase artifact history_count
+ phase="$(read_field phase)"
+ artifact="$(read_field current_artifact)"
+ history_count="$(read_field history_count)"
+ [ -z "$history_count" ] && history_count=0
+
+ [ "$phase" = "done" ] && die "Pipeline already complete."
+ [ -z "$artifact" ] && die "No artifact registered for phase '$phase'. Run 'pipeline.sh artifact ' first."
+ [ -f "$artifact" ] || die "Artifact file no longer exists: $artifact. Re-register with 'pipeline.sh artifact '."
+
+ # Snapshot artifact contents
+ cp "$artifact" "$APPROVED_DIR/${phase}.md"
+
+ # Record base commit for review phase (git diff source)
+ if [ "$phase" = "task-plan" ]; then
+ local base_commit
+ base_commit="$(git rev-parse HEAD 2>/dev/null || echo "")"
+ write_field review_base_commit "$base_commit"
+ fi
+
+ # Record in history
+ write_field "history_${history_count}_phase" "$phase"
+ write_field "history_${history_count}_artifact" "$artifact"
+ write_field "history_${history_count}_approved_at" "$(iso_now)"
+ history_count=$((history_count + 1))
+ write_field history_count "$history_count"
+
+ # Advance phase
+ local next
+ next="$(next_phase "$phase")"
+ write_field phase "$next"
+ write_field current_artifact ""
+
+ # Clear task tracking when leaving implementation
+ if [ "$phase" = "implementation" ]; then
+ write_field last_completed_task ""
+ fi
+
+ rebuild_json
+
+ if [ "$next" = "done" ]; then
+ echo ""
+ echo "✅ Pipeline complete!"
+ echo ""
+ echo "All artifacts:"
+ local i=0
+ while [ "$i" -lt "$history_count" ]; do
+ printf " [%s] %s → %s\n" "$((i+1))" \
+ "$(read_field "history_${i}_phase")" \
+ "$(read_field "history_${i}_artifact")"
+ i=$((i + 1))
+ done
+ echo ""
+ local feat
+ feat="$(read_field feature)"
+ info "Artifacts saved in: .spec/features/$feat/"
+ info "Next: check documentation (docs-check), then finish branch (pipeline.sh finish)"
+ else
+ info "Phase '$phase' approved."
+ info "Advanced to: [$(phase_number "$next")/6] $next"
+ info "Read template: ./templates/${next}.md"
+ fi
+}
+
+cmd_task() {
+ local task_id="$1"
+ [ -z "$task_id" ] && die "Usage: pipeline.sh task "
+
+ resolve_feature || die "No active pipeline."
+
+ local phase
+ phase="$(read_field phase)"
+ [ "$phase" = "implementation" ] || die "Task tracking is only available during implementation phase (current: $phase)."
+
+ write_field last_completed_task "$task_id"
+ rebuild_json
+ info "Task $task_id marked complete"
+}
+
+cmd_history() {
+ if [ ! -d "$FEATURES_DIR" ]; then
+ info "No features found."
+ return 0
+ fi
+
+ local found=0
+ for kv in "$FEATURES_DIR"/*/pipeline.kv; do
+ [ -f "$kv" ] || continue
+ found=1
+ local fname phase created
+ fname="$(grep "^feature=" "$kv" 2>/dev/null | head -1 | cut -d'=' -f2-)"
+ phase="$(grep "^phase=" "$kv" 2>/dev/null | head -1 | cut -d'=' -f2-)"
+ created="$(grep "^created_at=" "$kv" 2>/dev/null | head -1 | cut -d'=' -f2-)"
+
+ local status_icon
+ if [ "$phase" = "done" ]; then
+ status_icon="✓"
+ else
+ status_icon="●"
+ fi
+
+ printf " %s %-25s [%s/6] %-15s (created: %s)\n" \
+ "$status_icon" "$fname" "$(phase_number "$phase")" "$phase" "$created"
+ done
+
+ if [ "$found" -eq 0 ]; then
+ info "No features found."
+ fi
+}
+
+cmd_revisions() {
+ resolve_feature || die "No active pipeline."
+
+ local phase
+ phase="$(read_field phase)"
+
+ local target_phase="${1:-$phase}"
+ # Validate target phase
+ case "$target_phase" in
+ explore|requirements|design|task-plan|implementation|review|all) ;;
+ *) die "Unknown phase: $target_phase. Use: explore, requirements, design, task-plan, implementation, review, or all." ;;
+ esac
+
+ local found=0
+ local tmp
+ tmp="$(mktemp)"
+ if [ "$target_phase" = "all" ]; then
+ echo "All revisions:"
+ for p in explore requirements design task-plan implementation review; do
+ find "$REVISIONS_DIR" -name "${p}-rev-*" 2>/dev/null | sort > "$tmp"
+ while IFS= read -r f; do
+ printf " [%s] %s\n" "$p" "$(basename "$f")"
+ found=1
+ done < "$tmp"
+ done
+ else
+ echo "Revisions for phase '$target_phase':"
+ find "$REVISIONS_DIR" -name "${target_phase}-rev-*" 2>/dev/null | sort > "$tmp"
+ while IFS= read -r f; do
+ printf " %s\n" "$(basename "$f")"
+ found=1
+ done < "$tmp"
+ fi
+ rm -f "$tmp"
+
+ if [ "$found" -eq 0 ]; then
+ info "No revisions recorded yet."
+ fi
+}
+
+cmd_version() {
+ echo "Spec-Driven Dev Pipeline v${VERSION}"
+}
+
+# check_file_staleness
+# Outputs tab-separated: generated template age_days stale scope_changed
+check_file_staleness() {
+ local f="$1" templates_dir="$2" freshness_days="$3" now_epoch="$4"
+ local generated="null" template="null" age_days="null" stale="false" scope_changed="null"
+
+ local first_line
+ first_line="$(head -1 "$f" 2>/dev/null)"
+ case "$first_line" in
+ *""*)
+ local gen_date gen_tmpl
+ gen_date="$(echo "$first_line" | sed 's/.*.*/\1/')"
+ if [ -n "$gen_date" ]; then
+ generated="\"$gen_date\""
+ template="\"$gen_tmpl\""
+ local gen_epoch
+ gen_epoch="$(date -j -f '%Y-%m-%d' "$gen_date" '+%s' 2>/dev/null || date -d "$gen_date" '+%s' 2>/dev/null || echo 0)"
+ if [ "$gen_epoch" -gt 0 ] && [ "$now_epoch" -gt 0 ]; then
+ age_days=$(( (now_epoch - gen_epoch) / 86400 ))
+
+ local tmpl_file="$templates_dir/$gen_tmpl"
+ if [ -f "$tmpl_file" ]; then
+ local scope_line
+ scope_line="$(head -1 "$tmpl_file" 2>/dev/null)"
+ case "$scope_line" in
+ "")
+ local patterns
+ patterns="$(echo "$scope_line" | sed 's///' | sed 's/,[[:space:]]*/\n/g' | tr '\n' ' ' | sed 's/[[:space:]]*$//')"
+ if [ -n "$patterns" ]; then
+ local git_hits
+ # shellcheck disable=SC2086
+ git_hits="$(cd "$PROJECT_ROOT" && set -f && git log --oneline --since="$gen_date" -- $patterns 2>/dev/null | head -1)"
+ if [ -n "$git_hits" ]; then
+ scope_changed="true"
+ if [ "$age_days" -gt "$freshness_days" ]; then
+ stale="true"
+ fi
+ else
+ scope_changed="false"
+ fi
+ fi
+ ;;
+ *)
+ if [ "$age_days" -gt "$freshness_days" ]; then
+ stale="true"
+ fi
+ ;;
+ esac
+ else
+ if [ "$age_days" -gt "$freshness_days" ]; then
+ stale="true"
+ fi
+ fi
+ fi
+ fi
+ ;;
+ esac
+ printf '%s\t%s\t%s\t%s\t%s' "$generated" "$template" "$age_days" "$stale" "$scope_changed"
+}
+
+cmd_docs_check() {
+ local config_file="$CONFIG_FILE"
+ local docs_dir=".spec"
+ local freshness_days=30
+
+ # Read docs_dir and doc_freshness_days from config.yaml if it exists
+ if [ -f "$config_file" ]; then
+ local configured_dir
+ configured_dir="$(grep '^docs_dir:' "$config_file" 2>/dev/null | head -1 | sed 's/^docs_dir:[[:space:]]*//' | sed 's/[[:space:]]*$//')"
+ if [ -n "$configured_dir" ]; then
+ docs_dir="$configured_dir"
+ fi
+ local configured_days
+ configured_days="$(grep '^doc_freshness_days:' "$config_file" 2>/dev/null | head -1 | sed 's/^doc_freshness_days:[[:space:]]*//' | sed 's/[[:space:]]*$//')"
+ if [ -n "$configured_days" ]; then
+ freshness_days="$configured_days"
+ fi
+ fi
+
+ local full_path="$PROJECT_ROOT/$docs_dir"
+ local templates_dir="$SKILL_DIR/templates/docs"
+ local now_epoch
+ now_epoch="$(date +%s 2>/dev/null || echo 0)"
+
+ if [ -d "$full_path" ]; then
+ printf '{"exists": true, "dir": "%s", "freshness_days": %d, "files": [' "$(json_escape "$docs_dir")" "$freshness_days"
+
+ # Single scan: find files into tmpfile, iterate once, capture stale names
+ local tmp_files tmp_stale
+ tmp_files="$(mktemp)"
+ tmp_stale="$(mktemp)"
+ find "$full_path" -maxdepth 1 -type f -name '*.md' 2>/dev/null | sort > "$tmp_files"
+
+ local first=1
+ while IFS= read -r f; do
+ local fname result generated template age_days stale scope_changed
+ fname="$(basename "$f")"
+ result="$(check_file_staleness "$f" "$templates_dir" "$freshness_days" "$now_epoch")"
+
+ generated="$(printf '%s' "$result" | cut -f1)"
+ template="$(printf '%s' "$result" | cut -f2)"
+ age_days="$(printf '%s' "$result" | cut -f3)"
+ stale="$(printf '%s' "$result" | cut -f4)"
+ scope_changed="$(printf '%s' "$result" | cut -f5)"
+
+ if [ "$first" -eq 1 ]; then
+ first=0
+ else
+ printf ', '
+ fi
+ printf '{"name": "%s", "generated": %s, "template": %s, "age_days": %s, "stale": %s, "scope_changed": %s}' \
+ "$(json_escape "$fname")" "$generated" "$template" "$age_days" "$stale" "$scope_changed"
+
+ if [ "$stale" = "true" ]; then
+ echo "$fname" >> "$tmp_stale"
+ fi
+ done < "$tmp_files"
+
+ printf '], "stale": ['
+ # Read stale names from tmpfile (no re-scan needed)
+ local sfirst=1
+ if [ -s "$tmp_stale" ]; then
+ while IFS= read -r sname; do
+ if [ "$sfirst" -eq 1 ]; then
+ sfirst=0
+ else
+ printf ', '
+ fi
+ printf '"%s"' "$(json_escape "$sname")"
+ done < "$tmp_stale"
+ fi
+ printf ']}\n'
+
+ rm -f "$tmp_files" "$tmp_stale"
+ else
+ printf '{"exists": false, "dir": "%s", "freshness_days": %d, "files": [], "stale": []}\n' "$(json_escape "$docs_dir")" "$freshness_days"
+ fi
+}
+
+# --- standalone docs queue ---
+
+DOCS_QUEUE_FILE="$PROJECT_ROOT/.spec/.docs-queue.kv"
+
+docs_queue_read() {
+ # docs_queue_read — read value from queue file
+ [ -f "$DOCS_QUEUE_FILE" ] || return 1
+ local _line
+ _line="$(grep "^$1=" "$DOCS_QUEUE_FILE" 2>/dev/null | head -1)" || return 1
+ [ -n "$_line" ] || return 1
+ printf '%s' "$_line" | sed "s/^$1=//"
+}
+
+docs_queue_write_status() {
+ # docs_queue_write_status
+ local idx="$1" status="$2"
+ local tmp="$DOCS_QUEUE_FILE.tmp"
+ grep -v "^template_${idx}_status=" "$DOCS_QUEUE_FILE" > "$tmp" 2>/dev/null || true
+ echo "template_${idx}_status=$status" >> "$tmp"
+ mv -f "$tmp" "$DOCS_QUEUE_FILE"
+}
+
+docs_template_name() {
+ # Extract template name from generated file metadata (line 1)
+ local f="$1"
+ local first_line
+ first_line="$(head -1 "$f" 2>/dev/null)"
+ case "$first_line" in
+ "")
+ printf '%s' "$first_line" | sed -n 's/.*template:[[:space:]]*\([^[:space:]]*\)\.md[[:space:]]*-->/\1/p'
+ ;;
+ esac
+}
+
+cmd_docs_init() {
+ local mode="all"
+ local explicit_templates=""
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ --all) mode="all"; shift ;;
+ --update) mode="update"; shift ;;
+ -*) die "Unknown flag for docs-init: $1" ;;
+ *)
+ explicit_templates="$explicit_templates $1"
+ mode="explicit"
+ shift
+ ;;
+ esac
+ done
+
+ [ -f "$DOCS_QUEUE_FILE" ] && die "Docs queue already exists at $DOCS_QUEUE_FILE. Run 'pipeline.sh docs-reset' first."
+
+ local templates_dir="$SKILL_DIR/templates/docs"
+ local docs_dir
+ docs_dir="$(read_config docs_dir ".spec")"
+ local full_path="$PROJECT_ROOT/$docs_dir"
+
+ local templates=""
+ case "$mode" in
+ all)
+ for f in "$templates_dir"/*.md; do
+ local name
+ name="$(basename "$f" .md)"
+ [ "$name" = "README" ] && continue
+ templates="$templates $name"
+ done
+ ;;
+ update)
+ [ -d "$full_path" ] || die "Docs directory '$docs_dir' does not exist. Use --all to bootstrap."
+ local freshness_days
+ freshness_days="$(read_config doc_freshness_days "30")"
+ local now_epoch
+ now_epoch="$(date +%s 2>/dev/null || echo 0)"
+ # Iterate stale files, extract template names
+ local tmp_list
+ tmp_list="$(mktemp)"
+ find "$full_path" -maxdepth 1 -type f -name '*.md' 2>/dev/null | sort > "$tmp_list"
+ while IFS= read -r f; do
+ local result stale tmpl
+ result="$(check_file_staleness "$f" "$templates_dir" "$freshness_days" "$now_epoch")"
+ stale="$(printf '%s' "$result" | cut -f4)"
+ if [ "$stale" = "true" ]; then
+ tmpl="$(docs_template_name "$f")"
+ if [ -n "$tmpl" ]; then
+ case " $templates " in
+ *" $tmpl "*) ;;
+ *) templates="$templates $tmpl" ;;
+ esac
+ fi
+ fi
+ done < "$tmp_list"
+ rm -f "$tmp_list"
+ ;;
+ explicit)
+ templates="$explicit_templates"
+ ;;
+ esac
+
+ mkdir -p "$(dirname "$DOCS_QUEUE_FILE")"
+
+ # Build queue file
+ local count=0
+ local tmp_queue="$DOCS_QUEUE_FILE.tmp"
+ {
+ echo "created_at=$(iso_now)"
+ echo "docs_dir=$docs_dir"
+ echo "mode=$mode"
+ } > "$tmp_queue"
+
+ for t in $templates; do
+ if [ ! -f "$templates_dir/$t.md" ]; then
+ warn "Template not found, skipping: $t"
+ continue
+ fi
+ echo "template_${count}=$t" >> "$tmp_queue"
+ echo "template_${count}_status=pending" >> "$tmp_queue"
+ count=$((count + 1))
+ done
+ echo "total=$count" >> "$tmp_queue"
+
+ if [ "$count" -eq 0 ]; then
+ rm -f "$tmp_queue"
+ info "No templates to queue (nothing stale or none selected)."
+ return 0
+ fi
+
+ mv -f "$tmp_queue" "$DOCS_QUEUE_FILE"
+
+ info "Docs queue created: $count template(s), mode=$mode."
+ info "Next: pipeline.sh docs-next"
+ info ""
+ info "Execution strategy:"
+ info " - If your toolset supports subagent dispatch (Task/Composer/etc):"
+ info " use SUBAGENT mode — dispatch up to 3 templates in parallel."
+ info " - Otherwise: SEQUENTIAL mode — one template per iteration."
+ info " See ./templates/docs-maintenance.md § Standalone Documentation Workflow."
+}
+
+cmd_docs_next() {
+ [ -f "$DOCS_QUEUE_FILE" ] || die "No docs queue. Run 'pipeline.sh docs-init' first."
+
+ local total
+ total="$(docs_queue_read total)"
+ [ -z "$total" ] && total=0
+
+ local i=0
+ local templates_dir="$SKILL_DIR/templates/docs"
+ local docs_dir
+ docs_dir="$(docs_queue_read docs_dir)"
+
+ while [ "$i" -lt "$total" ]; do
+ local status
+ status="$(docs_queue_read "template_${i}_status")"
+ if [ "$status" = "pending" ]; then
+ local name
+ name="$(docs_queue_read "template_${i}")"
+ printf '%s\t%s\n' "$name" "$templates_dir/$name.md"
+ # Sequential mode hint after position 3
+ if [ "$i" -ge 3 ]; then
+ info "" >&2
+ info "Tip: if context feels heavy, start a fresh chat and resume" >&2
+ info " with 'pipeline.sh docs-status' (sequential mode)." >&2
+ fi
+ return 0
+ fi
+ i=$((i + 1))
+ done
+
+ info "Docs queue complete: all $total template(s) processed."
+ info "Run 'pipeline.sh docs-reset' to clear the queue."
+ return 0
+}
+
+cmd_docs_done() {
+ local name="${1:-}"
+ [ -z "$name" ] && die "Usage: pipeline.sh docs-done "
+ [ -f "$DOCS_QUEUE_FILE" ] || die "No docs queue. Run 'pipeline.sh docs-init' first."
+
+ local total
+ total="$(docs_queue_read total)"
+ [ -z "$total" ] && total=0
+
+ local i=0
+ while [ "$i" -lt "$total" ]; do
+ local entry
+ entry="$(docs_queue_read "template_${i}")"
+ if [ "$entry" = "$name" ]; then
+ local status
+ status="$(docs_queue_read "template_${i}_status")"
+ if [ "$status" = "done" ]; then
+ warn "Template '$name' already marked done."
+ return 0
+ fi
+ docs_queue_write_status "$i" "done"
+ info "Marked done: $name ($((i + 1))/$total)"
+ # Check if queue is now complete
+ local remaining=0
+ local j=0
+ while [ "$j" -lt "$total" ]; do
+ local s
+ s="$(docs_queue_read "template_${j}_status")"
+ [ "$s" = "pending" ] && remaining=$((remaining + 1))
+ j=$((j + 1))
+ done
+ if [ "$remaining" -eq 0 ]; then
+ info "Docs queue complete. Run 'pipeline.sh docs-reset' to clear it."
+ fi
+ return 0
+ fi
+ i=$((i + 1))
+ done
+
+ die "Template '$name' not found in queue."
+}
+
+cmd_docs_status() {
+ if [ ! -f "$DOCS_QUEUE_FILE" ]; then
+ printf '{"exists": false}\n'
+ return 0
+ fi
+
+ local total docs_dir mode created_at
+ total="$(docs_queue_read total)"
+ docs_dir="$(docs_queue_read docs_dir)"
+ mode="$(docs_queue_read mode)"
+ created_at="$(docs_queue_read created_at)"
+ [ -z "$total" ] && total=0
+
+ local completed=0
+ local pending_list=""
+ local current=""
+ local first_pending_set=0
+ local i=0
+ while [ "$i" -lt "$total" ]; do
+ local name status
+ name="$(docs_queue_read "template_${i}")"
+ status="$(docs_queue_read "template_${i}_status")"
+ if [ "$status" = "done" ]; then
+ completed=$((completed + 1))
+ else
+ if [ "$first_pending_set" -eq 0 ]; then
+ current="$name"
+ first_pending_set=1
+ fi
+ if [ -z "$pending_list" ]; then
+ pending_list="\"$(json_escape "$name")\""
+ else
+ pending_list="$pending_list, \"$(json_escape "$name")\""
+ fi
+ fi
+ i=$((i + 1))
+ done
+
+ printf '{"exists": true, "total": %d, "completed": %d, "current": %s, "pending": [%s], "mode": "%s", "docs_dir": "%s", "created_at": "%s"}\n' \
+ "$total" \
+ "$completed" \
+ "$([ -n "$current" ] && printf '"%s"' "$(json_escape "$current")" || printf 'null')" \
+ "$pending_list" \
+ "$(json_escape "$mode")" \
+ "$(json_escape "$docs_dir")" \
+ "$(json_escape "$created_at")"
+}
+
+cmd_docs_reset() {
+ if [ ! -f "$DOCS_QUEUE_FILE" ]; then
+ info "No docs queue to reset."
+ return 0
+ fi
+
+ local total completed=0
+ total="$(docs_queue_read total)"
+ [ -z "$total" ] && total=0
+ local i=0
+ while [ "$i" -lt "$total" ]; do
+ local s
+ s="$(docs_queue_read "template_${i}_status")"
+ [ "$s" = "done" ] && completed=$((completed + 1))
+ i=$((i + 1))
+ done
+
+ rm -f "$DOCS_QUEUE_FILE"
+ info "Docs queue reset (was: $completed/$total completed)."
+}
+
+cmd_config_check() {
+ [ -f "$CONFIG_FILE" ] || { info "No config file found: $CONFIG_FILE"; return 0; }
+
+ local valid_keys=" context rules.explore rules.requirements rules.design rules.task-plan rules.implementation rules.review rules.docs test_skill test_reference docs_dir doc_freshness_days auto_branch branch_prefix auto_worktree worktree_dir "
+ local errors=0
+
+ info "Checking $CONFIG_FILE ..."
+
+ # Extract keys and validate against whitelist
+ local tmp
+ tmp="$(mktemp)"
+ grep '^[a-z]' "$CONFIG_FILE" 2>/dev/null > "$tmp" || true
+ while IFS= read -r line; do
+ local key
+ key="$(printf '%s' "$line" | sed 's/:.*//')"
+ case "$valid_keys" in
+ *" $key "*) ;;
+ *) warn "Unknown key: '$key'"; errors=$((errors + 1)) ;;
+ esac
+ done < "$tmp"
+ rm -f "$tmp"
+
+ # Type checks
+ local val
+ val="$(read_config doc_freshness_days "")"
+ if [ -n "$val" ]; then
+ case "$val" in
+ *[!0-9]*) warn "doc_freshness_days must be numeric, got: '$val'"; errors=$((errors + 1)) ;;
+ esac
+ fi
+
+ val="$(read_config auto_branch "")"
+ if [ -n "$val" ]; then
+ case "$val" in
+ true|false|yes|no|1|0) ;;
+ *) warn "auto_branch must be boolean (true/false/yes/no/1/0), got: '$val'"; errors=$((errors + 1)) ;;
+ esac
+ fi
+
+ val="$(read_config auto_worktree "")"
+ if [ -n "$val" ]; then
+ case "$val" in
+ true|false|yes|no|1|0) ;;
+ *) warn "auto_worktree must be boolean (true/false/yes/no/1/0), got: '$val'"; errors=$((errors + 1)) ;;
+ esac
+ fi
+
+ if [ "$errors" -eq 0 ]; then
+ info "Config OK — all keys valid."
+ else
+ warn "$errors problem(s) found."
+ return 1
+ fi
+}
+
+cmd_inject() {
+ local target_phase="${1:-}"
+ local artifact_path="${2:-}"
+
+ if [ -z "$target_phase" ] || [ -z "$artifact_path" ]; then
+ die "Usage: pipeline.sh inject "
+ fi
+
+ # Validate target phase
+ case "$target_phase" in
+ explore|requirements|design|task-plan|implementation|review) ;;
+ *) die "Unknown phase: $target_phase. Use: explore, requirements, design, task-plan, implementation, review." ;;
+ esac
+
+ resolve_feature || die "No active pipeline. Run 'pipeline.sh init ' first."
+
+ local current_phase
+ current_phase="$(read_field phase)"
+ [ "$current_phase" = "done" ] && die "Pipeline already complete."
+
+ # Validate current phase <= target phase
+ local current_num target_num
+ current_num="$(phase_order "$current_phase")"
+ target_num="$(phase_order "$target_phase")"
+ if [ "$current_num" -gt "$target_num" ]; then
+ die "Cannot inject backward: current phase is '$current_phase' ($current_num), target is '$target_phase' ($target_num)."
+ fi
+
+ validate_artifact_path "$artifact_path"
+ [ -f "$artifact_path" ] || die "Artifact file does not exist: $artifact_path"
+
+ # Lightweight content validation
+ case "$target_phase" in
+ requirements)
+ if ! grep -q 'WHEN\|SHALL' "$artifact_path" 2>/dev/null; then
+ warn "Requirements artifact should contain WHEN/SHALL keywords."
+ fi
+ ;;
+ design)
+ if ! grep -q 'Correctness\|Property' "$artifact_path" 2>/dev/null; then
+ warn "Design artifact should contain Correctness Properties."
+ fi
+ ;;
+ esac
+
+ # Skip intermediate phases (record as injected in history)
+ local p="$current_phase"
+ local history_count
+ history_count="$(read_field history_count)"
+ [ -z "$history_count" ] && history_count=0
+
+ while [ "$p" != "$target_phase" ]; do
+ write_field "history_${history_count}_phase" "$p"
+ write_field "history_${history_count}_artifact" "(injected)"
+ write_field "history_${history_count}_approved_at" "$(iso_now)"
+ history_count=$((history_count + 1))
+ p="$(next_phase "$p")"
+ done
+
+ # Set to target phase and register artifact
+ write_field phase "$target_phase"
+ write_field current_artifact "$artifact_path"
+ write_field history_count "$history_count"
+
+ # Save revision snapshot
+ local rev_count
+ rev_count="$(read_field "revision_count_${target_phase}")"
+ [ -z "$rev_count" ] && rev_count=0
+ rev_count=$((rev_count + 1))
+ local rev_name
+ rev_name="${target_phase}-rev-${rev_count}-$(iso_now_compact).md"
+ cp "$artifact_path" "$REVISIONS_DIR/$rev_name"
+ write_field "revision_count_${target_phase}" "$rev_count"
+
+ # Capture review_base_commit if injecting into implementation/review and not already set
+ case "$target_phase" in
+ implementation|review)
+ local rbc
+ rbc="$(read_field review_base_commit 2>/dev/null || echo "")"
+ if [ -z "$rbc" ]; then
+ local head_commit
+ head_commit="$(git rev-parse HEAD 2>/dev/null || echo "")"
+ if [ -n "$head_commit" ]; then
+ write_field review_base_commit "$head_commit"
+ info "Captured review_base_commit: $(printf '%.8s' "$head_commit")"
+ fi
+ fi
+ ;;
+ esac
+
+ rebuild_json
+
+ local skipped=$((target_num - current_num))
+ if [ "$skipped" -gt 0 ]; then
+ info "$skipped phase(s) skipped to reach '$target_phase'."
+ fi
+ info "Artifact injected for phase '$target_phase': $artifact_path"
+ info "Ask user to approve, then run 'pipeline.sh approve'"
+}
+
+cmd_finish() {
+ # Parse finish-specific flags and action
+ local action=""
+ local confirm=""
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ --confirm) confirm="yes"; shift ;;
+ -*) die "Unknown flag for finish: $1" ;;
+ *)
+ [ -n "$action" ] && die "Unexpected argument: $1"
+ action="$1"; shift
+ ;;
+ esac
+ done
+
+ [ -z "$action" ] && die "Usage: pipeline.sh finish [--confirm]"
+
+ # Validate action
+ case "$action" in
+ merge|pr|keep|discard) ;;
+ *) die "Unknown finish action: $action. Use: merge, pr, keep, discard." ;;
+ esac
+
+ # Resolve feature (supports completed pipelines)
+ if [ -n "$EXPLICIT_FEATURE" ]; then
+ if [ ! -f "$FEATURES_DIR/$EXPLICIT_FEATURE/pipeline.kv" ]; then
+ die "Feature '$EXPLICIT_FEATURE' not found."
+ fi
+ set_feature_context "$EXPLICIT_FEATURE"
+ validate_kv
+ else
+ # For finish, we need to find a done-but-not-finished feature
+ local found=""
+ local count=0
+ if [ -d "$FEATURES_DIR" ]; then
+ for kv in "$FEATURES_DIR"/*/pipeline.kv; do
+ [ -f "$kv" ] || continue
+ local p fa
+ p="$(grep "^phase=" "$kv" 2>/dev/null | head -1 | cut -d'=' -f2-)"
+ fa="$(grep "^finish_action=" "$kv" 2>/dev/null | head -1 | cut -d'=' -f2-)"
+ if [ "$p" = "done" ] && [ -z "$fa" ]; then
+ local fn
+ fn="$(grep "^feature=" "$kv" 2>/dev/null | head -1 | cut -d'=' -f2-)"
+ found="$fn"
+ count=$((count + 1))
+ fi
+ done
+ fi
+ if [ "$count" -gt 1 ]; then
+ warn "Multiple completed pipelines awaiting finish:"
+ for kv in "$FEATURES_DIR"/*/pipeline.kv; do
+ [ -f "$kv" ] || continue
+ local p fa fn
+ p="$(grep "^phase=" "$kv" 2>/dev/null | head -1 | cut -d'=' -f2-)"
+ fa="$(grep "^finish_action=" "$kv" 2>/dev/null | head -1 | cut -d'=' -f2-)"
+ fn="$(grep "^feature=" "$kv" 2>/dev/null | head -1 | cut -d'=' -f2-)"
+ if [ "$p" = "done" ] && [ -z "$fa" ]; then
+ echo " - $fn" >&2
+ fi
+ done
+ die "Use --feature to select one."
+ fi
+ if [ -z "$found" ]; then
+ die "No completed pipeline awaiting finish. Run 'pipeline.sh history' to list features."
+ fi
+ set_feature_context "$found"
+ validate_kv
+ fi
+
+ local phase
+ phase="$(read_field phase)"
+ [ "$phase" != "done" ] && die "Pipeline not complete (current phase: $phase). Finish is only available after all phases are done."
+
+ # Check idempotency
+ local existing_action
+ existing_action="$(read_field finish_action 2>/dev/null || echo "")"
+ if [ -n "$existing_action" ]; then
+ die "Already finished (action: $existing_action). Nothing to do."
+ fi
+
+ # Git availability check
+ if [ "$action" != "keep" ]; then
+ if ! command -v git >/dev/null 2>&1; then
+ die "Git not found. Use 'finish keep' to skip git operations."
+ fi
+ if ! git rev-parse --git-dir >/dev/null 2>&1; then
+ die "Not a git repository. Use 'finish keep' to skip git operations."
+ fi
+ fi
+
+ local feat branch current_branch worktree
+ feat="$(read_field feature)"
+ branch="$(read_field branch 2>/dev/null || echo "")"
+ worktree="$(read_field worktree 2>/dev/null || echo "")"
+ current_branch="$(git branch --show-current 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")"
+
+ # Determine base branch for merge/discard
+ local base_branch=""
+ if [ "$action" = "merge" ] || [ "$action" = "discard" ]; then
+ # Try to find default branch
+ if git rev-parse --verify main >/dev/null 2>&1; then
+ base_branch="main"
+ elif git rev-parse --verify master >/dev/null 2>&1; then
+ base_branch="master"
+ else
+ die "Cannot determine base branch (no main or master). Merge/discard manually."
+ fi
+
+ # Check for uncommitted changes
+ if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then
+ die "Working tree has uncommitted changes. Commit or stash before '$action'."
+ fi
+ fi
+
+ # Determine which branch to operate on
+ local target_branch="${branch:-$current_branch}"
+
+ case "$action" in
+ merge)
+ [ -z "$target_branch" ] && die "No branch to merge (not on a feature branch and no branch recorded)."
+ [ "$target_branch" = "$base_branch" ] && die "Already on $base_branch. Nothing to merge."
+ # If worktree, remove it first (must not be in worktree dir when removing)
+ if [ -n "$worktree" ] && git worktree list 2>/dev/null | grep -q "$worktree"; then
+ git worktree remove "$worktree" || die "Failed to remove worktree '$worktree'."
+ info "Removed worktree: $worktree"
+ fi
+ git checkout "$base_branch" || die "Failed to checkout $base_branch."
+ git merge "$target_branch" || die "Merge failed. Resolve conflicts and retry."
+ write_field finish_action "merge"
+ write_field finished_at "$(iso_now)"
+ write_field finish_base "$base_branch"
+ rebuild_json
+ info "Merged '$target_branch' into '$base_branch'."
+ info "Tip: run tests to verify, then 'git branch -d $target_branch' to clean up."
+ ;;
+
+ pr)
+ [ -z "$target_branch" ] && die "No branch to push (not on a feature branch and no branch recorded)."
+ [ "$target_branch" = "$base_branch" ] && die "Already on $base_branch. Nothing to push."
+ git push -u origin "$target_branch" || die "Failed to push '$target_branch'."
+ write_field finish_action "pr"
+ write_field finished_at "$(iso_now)"
+ write_field finish_base "${base_branch:-}"
+ rebuild_json
+ info "Branch '$target_branch' pushed to origin."
+ info "Create a pull request: gh pr create --fill"
+ ;;
+
+ keep)
+ write_field finish_action "keep"
+ write_field finished_at "$(iso_now)"
+ rebuild_json
+ info "Branch kept as-is. Handle manually when ready."
+ ;;
+
+ discard)
+ [ "$confirm" != "yes" ] && die "Discard deletes the branch and all unmerged commits. Re-run with --confirm to proceed: pipeline.sh finish discard --confirm"
+ [ -z "$target_branch" ] && die "No branch to discard (not on a feature branch and no branch recorded)."
+ [ "$target_branch" = "$base_branch" ] && die "Cannot discard $base_branch."
+ # If worktree, force-remove it first
+ if [ -n "$worktree" ] && git worktree list 2>/dev/null | grep -q "$worktree"; then
+ git worktree remove --force "$worktree" || die "Failed to remove worktree '$worktree'."
+ info "Removed worktree: $worktree"
+ fi
+ git checkout "$base_branch" || die "Failed to checkout $base_branch."
+ git branch -D "$target_branch" || die "Failed to delete branch '$target_branch'."
+ write_field finish_action "discard"
+ write_field finished_at "$(iso_now)"
+ write_field finish_base "$base_branch"
+ rebuild_json
+ info "Branch '$target_branch' discarded."
+ ;;
+ esac
+}
+
+cmd_abandon() {
+ local feature="${1:-}"
+
+ # If --feature was specified globally, use it
+ if [ -z "$feature" ] && [ -n "$EXPLICIT_FEATURE" ]; then
+ feature="$EXPLICIT_FEATURE"
+ fi
+
+ # If still empty, try to resolve active feature
+ if [ -z "$feature" ]; then
+ feature="$(detect_active_feature)" || die "Multiple active pipelines. Use: pipeline.sh abandon "
+ [ -z "$feature" ] && die "No active pipeline to abandon."
+ fi
+
+ local fdir="$FEATURES_DIR/$feature"
+ [ -f "$fdir/pipeline.kv" ] || die "Feature '$feature' not found."
+
+ local phase
+ phase="$(grep "^phase=" "$fdir/pipeline.kv" 2>/dev/null | head -1 | cut -d'=' -f2-)"
+ [ "$phase" = "done" ] && die "Feature '$feature' is already completed."
+
+ set_feature_context "$feature"
+ write_field phase "done"
+ write_field abandoned_at "$(iso_now)"
+ write_field finish_action "abandoned"
+ write_field finished_at "$(iso_now)"
+
+ # Clean up worktree if present
+ local wt
+ wt="$(read_field worktree 2>/dev/null || echo "")"
+ if [ -n "$wt" ] && command -v git >/dev/null 2>&1 && git rev-parse --git-dir >/dev/null 2>&1; then
+ if git worktree list 2>/dev/null | grep -q "$wt"; then
+ git worktree remove --force "$wt" 2>/dev/null && info "Removed worktree: $wt"
+ fi
+ fi
+
+ rebuild_json
+
+ info "Feature '$feature' abandoned (was in phase: $phase)."
+ info "Artifacts remain in: .spec/features/$feature/"
+}
+
+cmd_help() {
+ echo "Spec-Driven Dev Pipeline v${VERSION}"
+ echo ""
+ echo "Usage: sh pipeline.sh [--feature ] [args]"
+ echo ""
+ echo "Global flags:"
+ echo " --feature Select feature (needed when multiple are active)"
+ echo ""
+ echo "Commands:"
+ echo " init [--branch|--worktree|--no-branch] "
+ echo " Start a new pipeline (kebab-case name)"
+ echo " --branch: create git branch "
+ echo " --worktree: create git worktree in /"
+ echo " --no-branch: skip auto-branch/worktree from config"
+ echo " status Show current phase, artifacts, progress"
+ echo " artifact [path] Register output artifact for current phase"
+ echo " approve Advance to next phase (needs artifact)"
+ echo " revisions [phase] Show revision history (current phase or specify: explore, all)"
+ echo " history Show all features and their status"
+ echo " docs-check Check project documentation status (JSON)"
+ echo " docs-init [--all|--update|...]"
+ echo " Create standalone docs generation queue"
+ echo " --all: queue all available templates"
+ echo " --update: queue only stale templates"
+ echo " ...: queue explicit templates by name"
+ echo " docs-next Print next pending template (name + path)"
+ echo " docs-done Mark template as completed in queue"
+ echo " docs-status Show docs queue progress (JSON)"
+ echo " docs-reset Clear the docs queue"
+ echo " task Mark implementation task as completed (resume tracking)"
+ echo " config-check Validate .spec/config.yaml keys and types"
+ echo " inject "
+ echo " Inject pre-written artifact and skip to that phase"
+ echo " finish Finalize branch after pipeline completes"
+ echo " Actions: merge, pr, keep, discard (--confirm)"
+ echo " abandon [feature] Abandon an active pipeline (marks as done)"
+ echo " version Show version"
+ echo " help Show this message"
+ echo ""
+ echo "Workflow (6 phases):"
+ echo " 1. init my-feature"
+ echo " 2. (agent reads templates/explore.md, investigates)"
+ echo " 3. artifact ← writes .spec/features/my-feature/explore.md"
+ echo " 4. approve ← user confirms"
+ echo " 5. (agent reads templates/requirements.md, generates doc)"
+ echo " 6. artifact ← writes .spec/features/my-feature/requirements.md"
+ echo " 7. approve ← user confirms"
+ echo " 8. (agent reads templates/design.md, generates doc)"
+ echo " 9. artifact ← writes .spec/features/my-feature/design.md"
+ echo " 10. approve ← user confirms"
+ echo " 11. (agent reads templates/task-plan.md, creates TDD plan)"
+ echo " 12. artifact ← writes .spec/features/my-feature/task-plan.md"
+ echo " 13. approve ← user confirms"
+ echo " 14. (agent reads templates/implementation.md, executes TDD plan)"
+ echo " 15. artifact ← writes .spec/features/my-feature/implementation.md"
+ echo " 16. approve ← user confirms"
+ echo " 17. (agent reads templates/review.md, reviews code)"
+ echo " 18. artifact ← writes .spec/features/my-feature/review.md"
+ echo " 19. approve ← user confirms → done!"
+ echo " 20. docs-check ← update project documentation if needed"
+ echo " 21. finish ← merge, push PR, keep, or discard branch"
+ echo ""
+ echo "All artifacts are saved permanently in .spec/features// and tracked by git."
+ echo "Tip: use 'revisions' to see previous versions of an artifact within a phase."
+}
+
+# --- main ---
+
+# Parse global flags before command dispatch
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --feature)
+ [ -n "$2" ] || die "--feature requires a value"
+ EXPLICIT_FEATURE="$2"
+ shift 2
+ ;;
+ *) break ;;
+ esac
+done
+
+case "${1:-help}" in
+ init) shift; cmd_init "$@" ;;
+ status) cmd_status ;;
+ artifact) shift; cmd_artifact "$@" ;;
+ approve) cmd_approve ;;
+ revisions) shift; cmd_revisions "$@" ;;
+ history) cmd_history ;;
+ docs-check) cmd_docs_check ;;
+ task) shift; cmd_task "$@" ;;
+ config-check) cmd_config_check ;;
+ inject) shift; cmd_inject "$@" ;;
+ finish) shift; cmd_finish "$@" ;;
+ abandon) shift; cmd_abandon "$@" ;;
+ docs-init) shift; cmd_docs_init "$@" ;;
+ docs-next) cmd_docs_next ;;
+ docs-done) shift; cmd_docs_done "$@" ;;
+ docs-status) cmd_docs_status ;;
+ docs-reset) cmd_docs_reset ;;
+ version|--version|-v) cmd_version ;;
+ help|--help|-h) cmd_help ;;
+ *) die "Unknown command: $1. Run 'pipeline.sh help' for usage." ;;
+esac
diff --git a/.agents/skills/sdd/templates/_preamble.md b/.agents/skills/sdd/templates/_preamble.md
new file mode 100644
index 00000000..37ae049c
--- /dev/null
+++ b/.agents/skills/sdd/templates/_preamble.md
@@ -0,0 +1,37 @@
+# Phase Preamble — Shared Instructions
+
+This file contains pipeline integration and project context instructions shared by all phase templates.
+
+Each phase template references this file instead of repeating these sections.
+
+---
+
+## Pipeline Integration
+
+Before starting any phase work:
+
+1. **Check pipeline state:**
+ ```
+ sh ./scripts/pipeline.sh status
+ ```
+2. **Read input artifacts:** read all completed phase artifacts listed in the status output (`history[N].artifact`). Later phases build on earlier ones — read them all for context.
+3. **After the user approves your output:**
+ a. Save the document to `.spec/features//.md`
+ b. Register: `sh ./scripts/pipeline.sh artifact` (defaults to `.spec/features//.md`)
+ c. Wait for user to confirm, then: `sh ./scripts/pipeline.sh approve`
+
+> **Directory separation — DO NOT mix these:**
+> - **Phase artifacts** (explore.md, requirements.md, design.md, …) → `.spec/features//`
+> - **Project documentation** (README.md, ARCHITECTURE.md, DOMAIN.md, …) → `/` (default: `.spec/`)
+>
+> Phase artifact instructions above apply ONLY to pipeline phase outputs. Project documentation is a separate workflow — see `./templates/docs-maintenance.md`.
+
+---
+
+## Project Context
+
+If `.spec/config.yaml` exists, read it now and apply:
+- **`context`** → treat as background knowledge about this project.
+- **`rules.`** → treat as additional rules for THIS phase (appended to the rules below, not replacing them). The phase-specific rule key is specified in each template.
+
+If the file does not exist, skip this step.
diff --git a/.agents/skills/sdd/templates/design.md b/.agents/skills/sdd/templates/design.md
new file mode 100644
index 00000000..432c64df
--- /dev/null
+++ b/.agents/skills/sdd/templates/design.md
@@ -0,0 +1,302 @@
+# Phase 3: Design
+
+You are a Software Architect. Your task: accept the approved requirements document and transform it into a detailed design document that fully describes **HOW** to implement the requirements.
+
+You do **NOT** write implementation code.
+You do **NOT** create task lists.
+You **design** the solution: architecture, interfaces, data models, correctness properties, and verification strategy.
+
+---
+
+Read `./templates/_preamble.md` for Pipeline Integration and Project Context instructions.
+- **Phase rule key:** `rules.design`
+- **Input artifacts:** read `history[0..1].artifact` (exploration, requirements documents)
+- **Output:** `.spec/features//design.md`
+
+---
+
+### Fast-track mode
+
+When the requirements artifact contains 1–2 REQs for a small bug fix:
+
+- **§2.1 Overview:** 1 paragraph.
+- **§2.2 Architecture:** simplified diagram — only the modified component(s). Omit unchanged context unless needed for understanding.
+- **§2.3 Components:** only files requiring changes + 1–2 unchanged files for context. Skip exhaustive "NOT Requiring Changes" list.
+- **§2.4 Key Decisions:** 1 ADR max. Omit if the fix is straightforward with no meaningful alternatives.
+- **§2.5 Data Models:** omit entirely if no new types or schema changes.
+- **§2.6 Correctness Properties:** 2–3 properties focused on the bug scenario. Skip categories that don't apply.
+- **§2.7 Error Handling:** 1–2 rows (main error path + fallback if applicable).
+- **§2.8 Testing Strategy:** reference existing test infrastructure. No new test patterns needed unless the bug exposed a gap.
+
+Target artifact size: **≤ 1.5 pages**.
+
+---
+
+## Language
+
+Write the design document in the **user's language** (detected from their first message). This includes:
+- Section headers (translate "Overview", "Architecture", "Key Decisions", "Error Handling", etc.)
+- All prose: overviews, ADR context/rationale/consequences, error scenario descriptions, test descriptions
+- File change descriptions in §2.3 tables (the "Description" column)
+- Correctness Property statements: write in the user's language, but keep the formal frame in English — `Property N`, `Category`, the quantifier `For all`, and `Validates: Requirements X.Y`
+
+Keep in English (do not translate):
+- Code signatures, type definitions, field names, import paths
+- Mermaid diagram node labels (they reference code constructs)
+- File paths in §2.3 tables
+- Project Commands table values (commands must be verbatim)
+- Tag names in test tables: `Feature/`, `Property/`
+
+---
+
+## Step 1: Context Clarification (optional)
+
+Ask clarifying questions when:
+- The requirements admit multiple substantially different architectural solutions
+- You need information about the existing codebase to make sound design decisions
+- There are contradictions or ambiguities in the requirements
+
+Group 2–4 questions in a single message. If the requirements are self-contained and unambiguous, you may skip this step — but then label every design assumption with `[ASSUMPTION: ...]` inline where it appears, so the user can spot and correct unstated beliefs during review.
+
+---
+
+## Step 2: Design Document Generation
+
+Produce a design document with the following sections. All marked **[REQUIRED]** must appear in every design document. Sections marked **[IF APPLICABLE]** should be included when relevant.
+
+---
+
+### 2.1 Overview [REQUIRED]
+
+Provide a brief description of the feature or change being designed. If the task divides into distinct logical parts, list them explicitly.
+
+---
+
+### 2.2 Architecture [REQUIRED]
+
+Describe the overall architecture using one or more **Mermaid diagrams**. Diagrams must visually distinguish:
+
+- **New** components: `fill:#90EE90` (green)
+- **Modified** components: `fill:#FFD700` (yellow)
+- **Existing/unchanged** components: default styling
+
+Also specify the **implementation order** — which parts should be built first and why.
+
+---
+
+### 2.3 Components and Interfaces [REQUIRED]
+
+#### Files Requiring Changes
+
+Provide a table of all files that must be created or modified:
+
+| File | Change Type | Description |
+|------|-------------|-------------|
+| `path/to/file` | `[NEW]` / `[MODIFIED]` / `[DELETED]` | What specifically is added, changed, or removed |
+
+For `[MODIFIED]` files — state **what exactly changes** (not "various changes"). Example:
+- ✓ `[MODIFIED]` — adds `refreshToken()` method, modifies `authenticate()` return type
+- ✗ `[MODIFIED]` — various authentication changes
+
+#### Files NOT Requiring Changes
+
+Explicitly list files that are in scope or might be expected to change, but will **not** be modified:
+
+| File | Reason Unchanged |
+|------|-----------------|
+| `path/to/file` | Explanation of why this file is unaffected |
+
+> Do not leave this table empty or skip it. Explicitly stating what is not changing is part of the design.
+
+For each interface, provide:
+- **Signature only** — no function bodies or implementation details
+- Input and output types
+- Any preconditions or postconditions in prose
+
+---
+
+### 2.4 Key Decisions (ADR) [REQUIRED]
+
+For each significant design decision, document an Architecture Decision Record (ADR):
+
+**Decision: [short title]**
+- **Context:** What problem or trade-off necessitates this decision
+- **Options considered:** List 2–3 alternatives
+- **Decision:** Which option was chosen
+- **Rationale:** Why this option was selected over the others
+- **Consequences:** Any trade-offs or implications of this choice
+
+Include at least one ADR per non-trivial design choice. Every ADR must capture a **choice between alternatives** — not a restatement of the requirements.
+
+If the feature changes a public API, wire protocol, database schema, or configuration contract, include a **Versioning & Backward Compatibility ADR**:
+- **Versioning strategy** — how old clients/schemas coexist with new (e.g., URL versioning, header negotiation, schema migration with rollback)
+- **Breaking change assessment** — what breaks if deployed without coordination
+- **Migration path** — steps for consumers to adopt the new version
+
+---
+
+### 2.5 Data Models [IF APPLICABLE]
+
+Show full type definitions (struct/class/interface/type alias) for all data structures involved in the feature. Include:
+
+- All fields with their types and a brief comment
+- Mark new types as `[NEW]`
+- Mark types that replace or supersede existing types as `[REMOVED: ]`
+- Mark modified types explicitly
+
+Example format:
+
+```
+// [NEW] Represents a scheduled job entry
+JobEntry {
+ id: string // Unique identifier
+ name: string // Human-readable label
+ schedule: string // Cron expression
+ enabled: boolean // Whether the job is active
+ lastRunAt: datetime // Timestamp of most recent execution, nullable
+}
+```
+
+Use the syntax natural to your project's language. The goal is precision and completeness, not adherence to any specific language.
+
+---
+
+### 2.6 Correctness Properties [REQUIRED]
+
+Define formal, verifiable properties that the implementation must satisfy. These serve as the specification for testing.
+
+**Format for each property:**
+
+```
+Property :
+Category:
+Statement: For all ,
+Validates: Requirements
+```
+
+**Category definitions:**
+- **Equivalence** — Two computations that should produce the same result always do
+- **Absence** — A specific error, state, or condition never occurs
+- **Round-trip** — An operation followed by its inverse returns the original value
+- **Propagation** — A change in one place correctly flows through to dependent locations
+- **Exclusion** — Two conditions or states that must never both be true simultaneously
+
+**Rules:**
+- Every property must use the "For all" quantifier — no existential claims
+- Every property must include a `Validates: Requirements X.Y` reference
+- Every requirement from the requirements document must be covered by at least one property
+- Properties must be verifiable — not vague assertions
+- When referenced in tables (Coverage Matrix, Traceability Matrix), use the abbreviated form `CP-N` (e.g., `CP-1`, `CP-2`). The full form `Property N` is used only in definitions above.
+
+**Worked examples** of each category (Equivalence, Absence, Round-trip, Propagation, Exclusion) with §2.6 definitions and §2.8 test table entries: read `./templates/reference/correctness-properties-examples.md`.
+
+---
+
+### 2.7 Error Handling [REQUIRED]
+
+Enumerate all error scenarios and specify how each is detected and handled:
+
+| Scenario | Detection | Action |
+|----------|-----------|--------|
+| Description of what can go wrong | How the system detects this condition | What the system does in response |
+
+Cover edge cases, not just happy-path failures. Include:
+- Invalid or malformed inputs
+- Missing or unavailable dependencies (files, services, connections)
+- Concurrent or race conditions (if applicable)
+- Partial failure states
+
+---
+
+### 2.8 Testing Strategy [REQUIRED]
+
+#### Test Style Source
+
+Before specifying any tests, determine the test style source using the priority cascade defined in `./templates/task-plan.md` § Test Infrastructure Discovery. If `test_skill` is configured, delegate test specification to that skill and skip the rest of §2.8.
+
+**Output:** Include a `Test Style Source` block at the top of §2.8:
+
+```markdown
+**Test Style Source:** Tier <1|2|3>
+-
+-
+```
+
+All tests specified below MUST follow the patterns identified in the Test Style Source.
+
+#### Project Commands
+
+Copy the Verification Commands table from the requirements document (§2.8) into the design document. These exact commands will be used in the implementation plan for all test/build/lint/generate steps. If the requirements document lacks this table, read the exploration document's Build Tooling section or discover commands directly from `Makefile`, `Taskfile.yml`, or `package.json` scripts.
+
+```markdown
+**Project Commands:**
+| Action | Command |
+|----------|-------------|
+| Test | `` |
+| Build | `` |
+| Lint | `` |
+| Generate | `` |
+```
+
+> The **Generate** row is required only if the project uses code generation. These commands are passed verbatim to the implementation phase.
+
+---
+
+#### Unit Tests
+
+Define the tests required to verify the design. Tag each test with the feature or property it validates.
+
+| Test | Description | Tags |
+|------|-------------|------|
+| `test_` | What is being tested and what the expected outcome is | `Feature/` |
+
+#### Property-Based Tests
+
+Use a property-based testing library appropriate for the project's language. For each correctness property defined in section 2.6, provide a corresponding property-based test:
+
+| Test | Property | Generator description | Tags |
+|------|----------|-----------------------|------|
+| `prop_` | Property N from section 2.6 | What inputs are randomly generated | `Property/` |
+
+**Rules:**
+- Every correctness property from section 2.6 must have a corresponding property-based test
+- If no property-based testing library is available for the project's language, substitute targeted unit tests covering representative inputs for each correctness property. Mark them `prop_` and tag with `Property/` as usual. In the Test Style Source block, note: "PBT unavailable — using targeted unit tests as substitute."
+- Every unit test must reference at least one `Feature/` or `Property/` tag
+- Tests are specified by **what they verify** — not by implementation
+
+---
+
+## Quality Control Checklist
+
+Before presenting the design document, verify:
+
+- [ ] Every requirement from the requirements document is covered by at least one correctness property
+- [ ] Every correctness property includes a `Validates: Requirements X.Y` reference
+- [ ] Every correctness property has a corresponding property-based test in section 2.8
+- [ ] Mermaid diagrams use correct colors: green for new, yellow for modified, default for unchanged
+- [ ] The "Files NOT Requiring Changes" table in section 2.3 is filled out
+- [ ] All data types referenced in interfaces are fully defined in section 2.5 (if applicable)
+- [ ] Error handling covers edge cases and partial failure states
+- [ ] Test Style Source (§2.8) is documented with tier and evidence
+- [ ] If the feature changes public APIs, database schemas, or protocols, a versioning/backward-compatibility ADR is present in §2.4
+- [ ] The document is self-contained: a reader unfamiliar with prior context can understand the design
+
+---
+
+## Done when
+
+Do NOT suggest approval until **every** condition is true:
+
+1. Every requirement from the requirements document is traced to at least one correctness property.
+2. Every correctness property has a corresponding property-based test in §2.8.
+3. The "Files NOT Requiring Changes" table in §2.3 is non-empty.
+4. Mermaid diagrams use correct color coding: green (`#90EE90`) = new, yellow (`#FFD700`) = modified, default = unchanged.
+5. At least one ADR is present in §2.4. If changing API/schema/protocol contracts, a versioning ADR is included.
+6. Test Style Source is documented in §2.8 with tier selection and evidence.
+7. Artifact is registered via `pipeline.sh artifact `.
+
+---
+
+## Antipatterns to Avoid
+
+Antipatterns for this phase: read `./templates/reference/antipatterns.md` § Design.
diff --git a/.agents/skills/sdd/templates/docs-maintenance.md b/.agents/skills/sdd/templates/docs-maintenance.md
new file mode 100644
index 00000000..dedf4da2
--- /dev/null
+++ b/.agents/skills/sdd/templates/docs-maintenance.md
@@ -0,0 +1,209 @@
+# Documentation Workflows
+
+This file contains all documentation generation, staleness checking, and post-pipeline maintenance workflows. Referenced from `SKILL.md`.
+
+---
+
+## Standalone Documentation Workflow
+
+**Trigger:** the user requests documentation generation or update **without referring to a feature**. Examples:
+- *"Generate the project documentation"* / *"сгенерируй документацию"*
+- *"Update the docs"* / *"обнови документацию"*
+- *"Refresh AUTH.md"* / *"actualize the architecture doc"*
+
+**DO NOT** run `pipeline.sh init ` for these requests. Documentation generation is a **standalone workflow** with its own state machine — `.spec/.docs-queue.kv` — driven by `pipeline.sh docs-*` commands.
+
+### Step 1: Detect intent
+
+| User intent | Command |
+|-------------|---------|
+| Bootstrap docs from scratch (no `/` yet) | `pipeline.sh docs-init --all` |
+| Update stale docs only | `pipeline.sh docs-init --update` |
+| Regenerate specific docs (user named them, e.g. "regenerate AUTH and API") | `pipeline.sh docs-init auth api` |
+| User unsure | Run `pipeline.sh docs-check`. If `exists: false` → propose `--all`. If `stale: []` → propose `--update`. Wait for user confirmation. |
+
+### Step 2: Initialize the queue
+
+Run the chosen `docs-init` command. It writes `.spec/.docs-queue.kv` with the list of templates to process. If a queue already exists, the command errors — run `pipeline.sh docs-reset` first if you intend to start over.
+
+Then verify with `pipeline.sh docs-status` (returns JSON with `total`, `completed`, `pending`, `current`, `mode`).
+
+### Step 2.5: Evaluate Execution Strategy
+
+Documentation templates are **independent** (no shared state) and **heavy** (each requires reading dozens of source files). This makes parallelization safe and beneficial.
+
+**Default: SUBAGENT mode** — recommended whenever your toolset supports subagent dispatch (e.g. Claude Code `Task` tool, Cursor Composer, GitHub Copilot subagents, or equivalent).
+
+**Fallback: SEQUENTIAL mode** — used only when subagent dispatch is unavailable.
+
+**Skip mode selection if `total = 1`** — sequential always (overhead of dispatch not justified).
+
+#### Subagent mode (recommended default)
+
+You become the **controller**. Loop:
+
+1. Read `pipeline.sh docs-status` to see pending templates.
+2. **Dispatch up to 3 subagents in parallel** (rate-limit + controller context safety). Each subagent gets one template via this **context package**:
+ - Path to template file (e.g. `./templates/docs/auth.md`)
+ - `docs_dir` value (where to save output)
+ - `rules.docs` from `.spec/config.yaml` (if set)
+ - `context` from `.spec/config.yaml` (if set)
+ - One- or two-sentence summary of project stack (language, framework, key patterns)
+3. **Receive each subagent's report** — it should report which file(s) were created and where.
+4. **Verify** each generated file (controller, never delegated):
+ - File exists at the expected path
+ - **Line 1** matches ``
+ - File is non-trivial (≥ 50 lines)
+5. **If verification fails**: re-dispatch with the verification error in the context package. Maximum **2 retries per template**, then escalate to the user.
+6. **If verification passes**: call `pipeline.sh docs-done `.
+7. Repeat from step 1 until `docs-next` reports the queue is complete.
+8. Run `pipeline.sh docs-reset` to clear the queue.
+
+**Subagent rules:**
+- One subagent per template — never bundle multiple templates into one dispatch.
+- Subagents do NOT interact with `pipeline.sh` — only the controller does.
+- Up to 3 in parallel (no more — controller context fills up receiving multiple large outputs at once).
+- Verification is always performed by the controller.
+
+#### Sequential mode (fallback)
+
+Loop:
+
+1. Run `pipeline.sh docs-next` — it prints the next pending template name and path (tab-separated).
+2. Read the template file from the printed path.
+3. Read `rules.docs` and `context` from `.spec/config.yaml` (if set).
+4. Generate the documentation file(s) following the template's instructions. Save to `/`. Verify line 1 has the `` metadata.
+5. Run `pipeline.sh docs-done `.
+6. Repeat until `docs-next` reports the queue is complete.
+
+**Fresh-chat hint:** after position 3, `docs-next` automatically prints a hint suggesting you start a fresh chat to avoid context exhaustion. The queue persists in `.spec/.docs-queue.kv` — in a new chat, run `pipeline.sh docs-status` to see your position and continue with `docs-next`.
+
+### Step 3: Auto-trigger from `docs-check`
+
+When `pipeline.sh docs-check` reports `stale: [...]` non-empty (during pre-pipeline check or on-demand), the workflow is:
+
+1. Inform the user: *"Found N stale doc(s): X, Y, Z. Initialize update queue? Say 'update docs' or 'skip'."*
+2. If user agrees → `pipeline.sh docs-init --update` → proceed to Step 2.5 (execution strategy).
+3. If user declines → no action. The user can run `pipeline.sh docs-init --update` later.
+
+The script does not auto-create the queue — explicit user consent is always required to begin a regeneration cycle.
+
+---
+
+## Documentation Context
+
+The skill supports a self-documenting mechanic via a project documentation directory (default: `.spec/`, configurable via `docs_dir` in `config.yaml`).
+
+> **Directory separation:** Project documentation files (README.md, ARCHITECTURE.md, DOMAIN.md, etc.) live in `/` (default: `.spec/`). Pipeline phase artifacts (explore.md, requirements.md, design.md, etc.) live in `.spec/features//`. **Never place project documentation into the feature directory or vice versa.**
+
+### Pre-pipeline check
+
+When running `pipeline.sh init `, before starting the Explore phase:
+
+1. Determine the docs directory: read `docs_dir` from `.spec/config.yaml`. If not set, default to `.spec`.
+2. Run `pipeline.sh docs-check` to determine if documentation exists and check freshness.
+3. If the docs directory **exists and contains `README.md`**:
+ - Read `README.md` for the documentation map
+ - Use available docs (`ARCHITECTURE.md`, `PACKAGES.md`, etc.) as supplementary context for ALL phases
+ - This is richer than `config.yaml` context and reduces the file-read budget in Explore phase
+ - Check the `stale` array in docs-check output. If stale files exist, suggest: *"Some docs are outdated (: days old). Regenerate before starting? Say 'update docs' or 'skip'."* If user agrees, follow the **Standalone Documentation Workflow** above (`pipeline.sh docs-init --update`).
+4. If the docs directory **does not exist**:
+ - Suggest to the user: *"Project documentation (/) not found. I can generate it to better understand your codebase. Say 'generate docs' or 'skip'."*
+ - If user says **"generate docs"**: follow the **Standalone Documentation Workflow** above (`pipeline.sh docs-init --all`).
+ - If user says **"skip"**: proceed with the pipeline normally — documentation is NOT required
+ - **This is a soft suggestion, not a blocker.** The pipeline works without documentation.
+
+### Stale doc regeneration workflow (legacy ad-hoc)
+
+> **Prefer the Standalone Documentation Workflow** (top of this file) which uses the `docs-init --update` queue. The ad-hoc steps below remain as a reference for manual single-file regeneration.
+
+When `pipeline.sh docs-check` reports stale files (or the user requests a doc update), follow these steps:
+
+1. Parse the `docs-check` JSON output — read the `stale` array.
+2. For each stale file, extract the `template` field from its freshness metadata.
+3. Group stale files by template (one template may generate multiple files).
+4. For each affected template:
+ a. Read the template from `./templates/docs/.md`.
+ b. Read the existing generated file(s) as baseline — preserve project-specific content where possible.
+ c. Regenerate following the template instructions.
+ d. Update the freshness metadata: ``.
+5. Present updated files to the user for review before saving.
+6. **Never auto-overwrite.** Always confirm with the user.
+
+Use this lookup table to find the owner template for any generated file:
+
+| Generated file | Owner template |
+|----------------|----------------|
+| `README.md`, `agent-rules.md` | `bootstrap.md` |
+| `AGENTS.md` | `agents-index.md` |
+| `ARCHITECTURE.md`, `PACKAGES.md`, `DOMAIN.md`, `CODE_STYLE.md` | `core.md` |
+| `TOOLS.md`, `TESTING.md`, `FILES.md` | `development.md` |
+| `ERRORS.md` | `errors.md` |
+| `AUTH.md`, `OAUTH.md` | `auth.md` |
+| `DATABASE.md` | `database.md` |
+| `API.md` | `api.md` |
+| `DEPLOYMENT.md` | `deployment.md` |
+| `SECURITY.md` | `security.md` |
+| `CLIENTS.md` + per-client docs | `clients.md` |
+| `FEATURE_FLAGS.md` | `feature-flags.md` |
+| `BACKGROUND_JOBS.md` | `background-jobs.md` |
+| `.md` (infra) | `infrastructure.md` |
+| `CLI.md` | `cli.md` |
+| `STATE.md` | `state-management.md` |
+| `EVENTS.md` | `events.md` |
+| `COMPONENTS.md` | `components.md` |
+| `ROUTING.md` | `routing.md` |
+
+### Documentation generation templates
+
+Templates for generating project documentation are in `./templates/docs/`. Read the manifest (`./templates/docs/README.md`) to discover available templates. When generating docs:
+- Apply `rules.docs` from `config.yaml` (if present) as additional rules
+- Apply `context` from `config.yaml` as background knowledge
+- Each template is self-contained and generates one or more files in `/`
+- **Freshness metadata**: when generating or updating any file in `/`, MUST add `` as the **first line** of the file (before the title). This enables `pipeline.sh docs-check` to track documentation age and detect stale files.
+- **Freshness metadata validation**: after saving a generated doc, verify that line 1 matches the pattern ``. If the metadata is missing or malformed, fix it immediately — `docs-check` will silently skip files without valid metadata.
+- **Content-aware staleness**: `pipeline.sh docs-check` uses scope metadata from templates (`` first line) combined with `git log --since=` to determine staleness. A doc is marked stale only if (a) files matching its template's scope patterns were changed since generation **and** (b) the doc exceeds the freshness threshold. Docs whose scope shows no changes remain fresh regardless of age. If a template has no scope line, the check falls back to pure age-based staleness. The JSON output includes a `scope_changed` field (`true`/`false`/`null`) per file.
+
+---
+
+## Documentation Maintenance
+
+After the pipeline reaches `phase=done` and artifacts are published, check if project documentation needs updating.
+
+### Step 1: Identify affected docs
+
+Read the design document §2.3 ("Files Requiring Changes" table). Match changed file paths against this pattern table:
+
+| Changed file pattern | Affected doc | Owner template |
+|----------------------|-------------|----------------|
+| `*domain*`, `models/*`, `types/*`, `*entity*` | `DOMAIN.md` | `core.md` |
+| new directory under `internal/`, `pkg/` | `PACKAGES.md` | `core.md` |
+| `cmd/*`, new service, layer changes | `ARCHITECTURE.md` | `core.md` |
+| `*_test*`, `__tests__/`, test config files | `TESTING.md` | `development.md` |
+| `Makefile`, `Taskfile`, `scripts/*`, CI tool changes | `TOOLS.md` | `development.md` |
+| `*error*`, `*errs*`, error codes, error types | `ERRORS.md` | `errors.md` |
+| `*auth*`, `*oauth*`, `*login*`, `*session*` | `AUTH.md` / `OAUTH.md` | `auth.md` |
+| `migrations/*`, `schema*`, `*_repo*`, `*_store*` | `DATABASE.md` | `database.md` |
+| `*handler*`, `*route*`, `*endpoint*`, `*.proto`, `openapi*` | `API.md` | `api.md` |
+| `Dockerfile`, `.github/workflows/*`, `k8s/*`, `docker-compose*` | `DEPLOYMENT.md` | `deployment.md` |
+| `*redis*`, `*kafka*`, `*traefik*`, `*prometheus*`, `*nats*` | `.md` | `infrastructure.md` |
+| `*client*`, `*frontend*`, `*mobile*` | `CLIENTS.md` | `clients.md` |
+| `*cors*`, `*csrf*`, `*rate_limit*`, `*security*`, `*helmet*` | `SECURITY.md` | `security.md` |
+| `*feature_flag*`, `*toggle*`, `*experiment*` | `FEATURE_FLAGS.md` | `feature-flags.md` |
+| `*worker*`, `*job*`, `*queue*`, `*cron*`, `*scheduler*` | `BACKGROUND_JOBS.md` | `background-jobs.md` |
+| new code style rule, naming convention change | `CODE_STYLE.md` | `core.md` |
+| `cmd/*`, `cli/*`, `commands/*`, `*cobra*`, `*clap*`, `*click*` | `CLI.md` | `cli.md` |
+| `*store*`, `*redux*`, `*bloc*`, `*provider*`, `*zustand*`, `*pinia*` | `STATE.md` | `state-management.md` |
+| `*event*`, `*messaging*`, `*pubsub*`, `*subscriber*`, `*producer*`, `*consumer*` | `EVENTS.md` | `events.md` |
+| `components/*`, `ui/*`, `design-system/*`, `*widget*`, `atoms/*`, `molecules/*` | `COMPONENTS.md` | `components.md` |
+| `routes/*`, `router/*`, `pages/*`, `screens/*`, `navigation/*` | `ROUTING.md` | `routing.md` |
+
+### Step 2: Filter and suggest
+
+1. Collect unique affected docs from the pattern matches.
+2. **Filter**: only suggest docs that already exist in `/`. Do not suggest creating new docs post-pipeline.
+3. Present to user: *"This feature touched auth and database files. Update AUTH.md and DATABASE.md? Say 'update docs' or 'skip'."*
+4. If user says **"update docs"**: for each affected doc, read its owner template from `./templates/docs/`, regenerate the doc, update freshness metadata.
+5. If user says **"skip"**: done, no action.
+6. If the docs directory does not exist at all, suggest full generation (same as Pre-flight Checklist step 3).
+7. **Never auto-update documentation.** Always ask the user first.
diff --git a/.agents/skills/sdd/templates/docs/README.md b/.agents/skills/sdd/templates/docs/README.md
new file mode 100644
index 00000000..62398495
--- /dev/null
+++ b/.agents/skills/sdd/templates/docs/README.md
@@ -0,0 +1,91 @@
+# Documentation Templates — Manifest
+
+This directory contains prompt templates for generating project documentation in the `.spec/` directory (configurable via `docs_dir` in `config.yaml`).
+
+> **IMPORTANT — Output directory:** All files generated by these templates go to `/` (default: `.spec/`), **NOT** to `.spec/features//`. The features directory is for pipeline phase artifacts only. These are separate directories with separate purposes.
+
+Each template is a self-contained prompt that instructs the AI agent to analyze the project and generate specific documentation files.
+
+## Available Templates
+
+Execution order: **Bootstrap → Core → Domain-Specific**. Run Bootstrap templates first to create the index, then Core for fundamental docs, then Domain-Specific as needed.
+
+| Template | Stage | Generates | When to Use |
+|----------|-------|-----------|-------------|
+| `bootstrap.md` | Bootstrap | `README.md`, `agent-rules.md` | **Run first** — creates the root index and agent rules |
+| `agents-index.md` | Bootstrap | `AGENTS.md` (root) | First-time setup — creates the entry point that explains `.spec/` to agents |
+| `core.md` | Core | `ARCHITECTURE.md`, `PACKAGES.md`, `DOMAIN.md`, `CODE_STYLE.md` | First-time setup or after significant architectural changes |
+| `development.md` | Core | `TOOLS.md`, `TESTING.md`, `FILES.md` | First-time setup or after tooling/testing convention changes |
+| `errors.md` | Core | `ERRORS.md` | If the project defines custom error types, error codes, or error mapping |
+| `auth.md` | Domain | `AUTH.md` or `OAUTH.md` | If the project has authentication (OAuth, email+password, API keys, etc.) |
+| `database.md` | Domain | `DATABASE.md` | If the project uses SQL/NoSQL databases |
+| `api.md` | Domain | `API.md` | If the project exposes REST/gRPC/GraphQL API |
+| `deployment.md` | Domain | `DEPLOYMENT.md` | If the project has Dockerfile, CI/CD, k8s, or PaaS config |
+| `infrastructure.md` | Domain | One `.md` per infra component | If the project has Redis, Kafka, Traefik, observability, etc. |
+| `clients.md` | Domain | `CLIENTS.md` + per-client docs | If the project has frontend, mobile, Telegram, CLI clients |
+| `security.md` | Domain | `SECURITY.md` | If the project needs a security audit (input validation, CORS, secrets, OWASP) |
+| `feature-flags.md` | Domain | `FEATURE_FLAGS.md` | If the project uses feature flags, A/B testing, or gradual rollouts |
+| `background-jobs.md` | Domain | `BACKGROUND_JOBS.md` | If the project has background workers, cron jobs, task queues, or async processing |
+| `cli.md` | Domain | `CLI.md` | If the project is a CLI application (commands, flags, config, exit codes) |
+| `state-management.md` | Domain | `STATE.md` | If the project uses state management (Redux, Zustand, BLoC, Pinia, MobX, etc.) |
+| `events.md` | Domain | `EVENTS.md` | If the project uses event-driven patterns (Kafka, NATS, domain events, pub/sub) |
+| `components.md` | Domain | `COMPONENTS.md` | If the project has a UI component library or design system |
+| `routing.md` | Domain | `ROUTING.md` | If the project has client-side routing, navigation guards, or page-based structure |
+
+## Usage
+
+The agent reads this manifest to discover available templates when:
+1. **Pre-pipeline check**: `.spec/` directory is missing — agent offers to generate it
+2. **Post-pipeline update**: feature touched files that affect documentation — agent offers targeted regeneration
+
+The agent selects the relevant template(s), reads them, and follows the instructions to generate documentation.
+
+### Multi-Service / Monorepo Projects
+
+When the project contains multiple services, languages, or independently deployable components:
+- **One `.spec/` per repository, not per service.** Documentation is scoped to the repository level.
+- **Core templates** (`core.md`, `development.md`) cover the overall architecture. Use sections within generated files to describe per-service specifics (e.g., `ARCHITECTURE.md` has a section per service).
+- **Domain templates** (`api.md`, `database.md`, etc.) may be run multiple times — once per service that has the relevant concern. Name generated files with a service prefix if needed (e.g., `API-gateway.md`, `API-users.md`).
+- **Template selection is per-concern, not per-service.** Don't generate `DEPLOYMENT.md` for a service with no deployment config.
+
+## Adding a New Template
+
+To add documentation for a new topic (e.g., AUTH.md, OBSERVABILITY.md, CACHING.md):
+
+1. Create a new file `.md` in this directory
+2. Follow the structure convention below
+3. Add an entry to the "Available Templates" table above
+
+### Structure Convention
+
+Every template file must follow this structure:
+
+```markdown
+# Documentation Template
+
+## What This Generates
+
+
+## Instructions
+
+
+### File 1: .spec/.md
+
+
+### File 2: .spec/.md (if multiple)
+
+
+## General Rules
+
+```
+
+### Rules for template authors:
+- **Language**: English only
+- **Language-neutral examples**: Templates use generic or Go-like examples for illustration, but generated documentation MUST use actual code from the project in its real language/framework. Agents should adapt patterns, naming conventions, and idioms to match the project's tech stack.
+- **Examples**: Use generic placeholders, not project-specific code
+- **Self-contained**: Each template must be usable independently
+- **Real code only**: Templates must instruct the agent to use examples from actual project source, not invented ones
+- **Skip if absent**: If a topic doesn't apply to the project, the agent should skip it (not generate empty docs)
+- **Freshness metadata**: Instruct the agent to add `` as the first line of every generated file. This is parsed by `pipeline.sh docs-check` for staleness detection.
+- **Single owner**: Each piece of data must live in exactly one `.spec/` file. If another template already owns a topic, add a one-line pointer (e.g., "See `ERRORS.md`") instead of duplicating content.
+- **Scope metadata**: Every template must include `` as the **first line** (before the title). Patterns are glob expressions matching source files relevant to the template's topic. `pipeline.sh docs-check` reads these patterns to perform content-aware staleness detection — a doc is only marked stale if files matching its scope were actually changed since the last generation. Example: ``.
diff --git a/.agents/skills/sdd/templates/docs/agents-index.md b/.agents/skills/sdd/templates/docs/agents-index.md
new file mode 100644
index 00000000..cb63aecd
--- /dev/null
+++ b/.agents/skills/sdd/templates/docs/agents-index.md
@@ -0,0 +1,74 @@
+
+# Agents Index Documentation Template
+
+## What This Generates
+
+- `AGENTS.md` in the project root — an instruction file for AI agents (Copilot, Cursor, Windsurf, Claude, etc.) explaining how to use the `.spec/` documentation directory.
+
+## Instructions
+
+Create `AGENTS.md` in the project root. This file instructs AI agents on how to work with the `.spec/` directory in this repository.
+
+The file must contain the following sections:
+
+### Section 1: What is `.spec/`
+
+Explain that `.spec/` is a project documentation directory optimized for AI agent (LLM) consumption. It contains:
+- Structured descriptions of architecture, packages, and domain
+- Code and testing conventions
+- Infrastructure and tooling descriptions
+- Agent rules (`agent-rules.md`)
+
+Purpose: give the agent full project context without having to read the entire source code.
+
+### Section 2: How to Use (for agents)
+
+Instructions for the AI agent:
+- When starting work on a project — read `.spec/README.md` for the documentation map
+- Before modifying code — read the relevant document from `.spec/` (e.g., before modifying API — read `ARCHITECTURE.md` and `CODE_STYLE.md`)
+- Always follow rules from `agent-rules.md`
+- If a document appears outdated — suggest an update
+
+### Section 3: File Structure Convention
+
+Describe file naming conventions in `.spec/`:
+- `README.md` — index of all documents, Quick Facts, project structure, ports, commands
+- `agent-rules.md` — mandatory rules for the agent (code style, naming, error handling)
+- `UPPER_CASE.md` — topical documents (`ARCHITECTURE.md`, `TESTING.md`, etc.)
+- `skills/` — directory for agent skills (custom scripts)
+- `workflows/` — directory for agent workflows
+- `prompts/` — prompts for (re)generating documentation
+
+### Section 4: Document Categories
+
+List the recommended document categories:
+- **Core**: Architecture, Packages, Domain, Code Style
+- **Development**: Tools, Testing, Files/Storage
+- **Auth & Security**: OAuth, API Keys, Permissions
+- **Infrastructure**: Observability, Caching, Proxy, Queues, Leader Election
+- **Clients**: Frontend apps, Mobile, Bots
+
+### Section 5: How to Maintain
+
+Rules for keeping documentation current:
+- When adding a new component — update or create the corresponding document
+- When changing architecture — update `ARCHITECTURE.md`
+- When adding a dependency — update `PACKAGES.md`
+- `README.md` must always reflect the current list of documents
+- Documents must contain code examples from the actual project, not abstract ones
+
+### Section 6: How to Add New Document
+
+Steps for adding a new document:
+1. Create a file `TOPIC_NAME.md` in `.spec/`
+2. Use this structure: title → overview → architectural diagram (ASCII) → details → code examples → configuration → testing → key files
+3. Add a link in `README.md` under the appropriate category
+4. If the topic is project-specific — use real code examples
+
+## General Rules
+
+- Format: Markdown with ASCII diagrams, tables, fenced code blocks with language tags
+- Style: concise, structured, no filler. Optimized for quick scanning.
+- Language: English
+- All code examples must come from the actual project source, not invented
+- If a section does not apply to the project, omit it
diff --git a/.agents/skills/sdd/templates/docs/api.md b/.agents/skills/sdd/templates/docs/api.md
new file mode 100644
index 00000000..61d7f12a
--- /dev/null
+++ b/.agents/skills/sdd/templates/docs/api.md
@@ -0,0 +1,156 @@
+
+# API Documentation Template
+
+## What This Generates
+
+- `.spec/API.md` — API endpoint reference, conventions, middleware, and error format
+
+## Instructions
+
+You are a technical documentarian. Create API documentation for the project in the `.spec/` directory.
+Analyze: route definitions, handler files, middleware chain, proto/OpenAPI specs, request/response types, error handling.
+
+### Step 1: Identify API Surface
+
+Determine the API type(s):
+- **REST**: route files, handler functions, HTTP methods
+- **gRPC**: `.proto` files, generated code, service definitions
+- **GraphQL**: schema files, resolvers
+- **WebSocket**: upgrade handlers, event definitions
+
+For each API surface, identify:
+- Router / framework (e.g., `chi`, `gin`, `echo`, `Express`, `FastAPI`, `net/http`)
+- Base path / port
+- Authentication method (Bearer token, API key, cookie)
+
+### Step 2: Create .spec/API.md
+
+#### Structure:
+
+##### 1. Overview
+- One sentence: API type + framework
+- Base URL pattern (e.g., `http://localhost:8080/api/v1`)
+- Authentication method summary
+
+##### 2. Middleware Stack
+
+Ordered list of middleware in the request processing pipeline:
+| Order | Middleware | Purpose |
+|-------|-----------|---------|
+| 1 | Request ID | Assigns unique ID to each request |
+| 2 | Logger | Structured request logging |
+| 3 | Recovery | Panic recovery |
+| 4 | Auth | Token validation |
+| 5 | ... | ... |
+
+Determine order from the actual router setup code.
+Note which middleware is global vs route-specific.
+
+##### 3. Endpoint Reference
+
+Group endpoints by resource/domain:
+
+**Users**
+| Method | Path | Auth | Description |
+|--------|------|------|-------------|
+| POST | `/api/v1/users` | No | Create user |
+| GET | `/api/v1/users/:id` | Yes | Get user by ID |
+
+For each group, include:
+- All endpoints with method, path, auth requirement, description
+- Source file reference (handler file path)
+
+If there are many endpoints (>20), create a summary table first, then detail sections per resource.
+
+##### 4. Request / Response Conventions
+
+Describe the standard patterns:
+- **Request envelope**: is there a wrapper? (`{"data": ...}` vs flat)
+- **Response envelope**: standard response format
+ ```json
+ {
+ "data": { ... },
+ "error": null
+ }
+ ```
+- **Pagination**: pattern used (offset/limit, cursor, page/per_page)
+ - Request parameters
+ - Response metadata (`total`, `next_cursor`, `has_more`)
+- **Sorting**: parameter name and format (e.g., `?sort=name,-created_at`)
+- **Filtering**: parameter patterns (e.g., `?status=active&role=admin`)
+
+Code examples from the actual project.
+
+##### 5. Status Codes & Error Format
+
+**Standard status codes used:**
+| Status | When Used |
+|--------|-----------|
+| 200 | Successful response |
+| 201 | Resource created |
+| 400 | Validation error |
+| 401 | Unauthorized |
+| 403 | Forbidden |
+| 404 | Not found |
+| 500 | Internal error |
+
+**Error response format:**
+```json
+{
+ "error": {
+ "code": "VALIDATION_ERROR",
+ "message": "email is required",
+ "details": [...]
+ }
+}
+```
+
+Show the actual error response structure from the project.
+Reference the error mapping code (where domain errors become HTTP errors).
+
+##### 6. Versioning Strategy (if applicable)
+- How API versions are specified: URL prefix (`/v1/`), header (`Accept-Version`), content type
+- Current version(s)
+- Deprecation policy
+
+##### 7. Rate Limiting (if applicable)
+- Rate limit values (requests per second/minute)
+- Rate limit headers returned (`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `Retry-After`)
+- Per-user vs per-IP vs per-API-key
+- Configuration reference
+
+##### 8. Proto / OpenAPI / GraphQL Schema (if applicable)
+
+For **gRPC**:
+- `.proto` file locations
+- Service definitions (list of RPCs per service)
+- Code generation command
+- Client stub usage example
+
+For **OpenAPI**:
+- Spec file location (e.g., `api/openapi.yaml`)
+- How spec is generated (manual vs auto-generated)
+- Swagger UI URL (if served)
+- Code generation command (if applicable)
+
+For **GraphQL**:
+- Schema file location
+- Key queries and mutations
+- Resolver structure
+
+##### 9. Validation
+- Validation library / approach (struct tags, middleware, manual)
+- Where validation happens (handler, middleware, domain layer)
+- Common validation rules used
+- Custom validators (if any)
+
+Code example of a validated request from the project.
+
+## General Rules
+
+- Language: English
+- All endpoints must come from actual route definitions, not assumed
+- Request/response examples must use realistic but non-sensitive data
+- If the project has no API (library, CLI-only), do not generate this file
+- Group endpoints logically by resource, not by HTTP method
+- After creating, update `.spec/README.md`: add a link under the appropriate section
diff --git a/.agents/skills/sdd/templates/docs/auth.md b/.agents/skills/sdd/templates/docs/auth.md
new file mode 100644
index 00000000..afd5d8cb
--- /dev/null
+++ b/.agents/skills/sdd/templates/docs/auth.md
@@ -0,0 +1,142 @@
+
+# Auth Documentation Template
+
+## What This Generates
+
+- `.spec/AUTH.md` (or `.spec/OAUTH.md` if the project primarily uses OAuth) — authentication and authorization documentation
+
+## Instructions
+
+You are a technical documentarian. Create auth/authorization documentation for the project in the `.spec/` directory.
+Analyze: OAuth adapters, JWT/PASETO/session middleware, login handlers, DB migrations with auth fields.
+
+### Step 1: Identify Authentication Mechanisms
+
+Determine which authentication mechanisms exist in the project:
+- OAuth2 providers (Google, GitHub, Apple, Yandex, Facebook, Twitter, etc.)
+- Email + Password (classic registration)
+- Telegram Login / Mini App initData
+- API Keys
+- SSO (SAML, OIDC)
+- Magic Links
+- 2FA / MFA
+
+### Step 2: Create the Document
+
+Create `.spec/OAUTH.md` if OAuth is the primary mechanism, or `.spec/AUTH.md` otherwise.
+
+#### Structure:
+
+##### 1. Supported Providers / Methods
+
+Table:
+| Provider/Method | Status | Validation Method |
+|-----------------|--------|-------------------|
+| Google | Ready / TODO | ID Token / API call / ... |
+
+Statuses: Ready, TODO, In Progress
+
+##### 2. Architecture (Flow)
+
+ASCII diagram of the authentication flow:
+```
+Frontend → obtains token → sends to backend → validation → session creation → response
+```
+
+Show the full flow from user click to session receipt.
+If multiple flows exist (OAuth, email+password, Telegram) — show a unified diagram.
+
+##### 3. User Lookup / Creation Logic
+
+Flowchart (Mermaid or ASCII):
+- Token received → validation
+- Search by provider_id → found? → login
+- Search by email → found? → account linking
+- Not found → create new user
+- Create session → return token
+
+##### 4. Account Linking (if applicable)
+Description of the mechanism for linking OAuth accounts to an existing user.
+
+##### 5. Authorization (if applicable)
+
+Permission and access control model:
+- Authorization model type: RBAC / ABAC / scope-based / custom
+- Permission matrix table:
+
+| Role | Action | Resource | Allowed |
+|------|--------|----------|---------|
+| admin | * | * | Yes |
+| user | read | own profile | Yes |
+
+- Where authorization checks happen (middleware, guard, decorator, in-handler)
+- Code reference: file path and key function/method for permission checking
+- How roles/permissions are stored (DB table, JWT claims, config)
+
+##### 6. Session Management (if applicable)
+
+Session storage and lifecycle:
+- Storage mechanism: database / Redis / JWT (stateless) / cookie-based
+- Session model (fields: id, user_id, token, expires_at, etc.)
+- Session lifecycle:
+ 1. Creation (on login / OAuth callback)
+ 2. Validation (on each request)
+ 3. Refresh (token rotation)
+ 4. Revocation (logout, password change)
+- Concurrent session policy: allow multiple / single session / limit per device
+- Session cleanup: TTL, cron job, on-demand
+
+##### 7. Token Lifecycle (if applicable)
+
+Token flow and management:
+- Token types used: access token, refresh token, ID token, API key
+- Token format: JWT / PASETO / opaque / custom
+- Token claims / payload structure
+- Issuance flow: what triggers token creation
+- Rotation strategy: how refresh tokens are rotated
+- Revocation mechanism: blocklist, DB flag, Redis TTL
+- Token storage on client side (see also CLIENTS.md if applicable)
+
+##### 8. Account Operations (if applicable)
+
+User account management flows:
+- **Password reset**: flow diagram (request → email → token → new password), token TTL, rate limiting
+- **Email change**: verification flow, old email notification
+- **Account deletion / deactivation**: soft delete vs hard delete, data retention, cascade rules
+- **Profile update**: which fields are mutable, validation rules
+
+For each flow: endpoint reference, code path, edge cases.
+
+##### 9. Configuration
+Configuration example (config.yml, .env, environment variables).
+Do not include real secrets — use placeholders.
+
+##### 10. Per-Provider / Per-Method Implementation
+
+For each provider/method:
+- **File**: path to implementation file
+- **Code**: key fragment (token validation, API call)
+- **Specifics**: what distinguishes this provider
+
+##### 11. Tracing (if applicable)
+Table of tracing spans for auth operations:
+| Span | Description |
+|------|-------------|
+
+##### 12. Adding a New Provider / Method
+Step-by-step guide (numbered list):
+1. Create adapter (code example)
+2. Add configuration
+3. Register in main
+4. DB migration (if needed)
+5. Update handler
+6. Update API validation
+
+## General Rules
+
+- Language: English
+- Do not create sections for providers/methods that do not exist in the project
+- Replace real secrets with placeholders
+- If the project only has email+password without OAuth — name the file `AUTH.md` and adapt the structure
+- All code examples must come from the actual project
+- After creating, update `.spec/README.md`: add a link under Auth & Security
diff --git a/.agents/skills/sdd/templates/docs/background-jobs.md b/.agents/skills/sdd/templates/docs/background-jobs.md
new file mode 100644
index 00000000..2d37e3e8
--- /dev/null
+++ b/.agents/skills/sdd/templates/docs/background-jobs.md
@@ -0,0 +1,107 @@
+
+# Background Jobs Documentation Template
+
+## What This Generates
+
+- `.spec/BACKGROUND_JOBS.md` — documents the background job system, job inventory, retry strategy, and scaling approach
+
+## Instructions
+
+Analyze the project to identify any background job or async task processing. Look for:
+- Job frameworks (Sidekiq, Celery, Temporal, Asynq, Bull, Hangfire, Faktory)
+- Custom worker implementations (goroutines + channels, threads + queues)
+- Cron/scheduler configurations (crontab, systemd timers, Kubernetes CronJobs, `robfig/cron`)
+- Message queue consumers (Kafka consumers, RabbitMQ subscribers, NATS workers, SQS listeners)
+
+If no background job system is found, **skip this template entirely** — do not generate an empty document.
+
+### File: .spec/BACKGROUND_JOBS.md
+
+#### Structure:
+
+##### 1. Overview
+- **System**: which job framework or custom implementation is used
+- **Transport**: underlying queue/broker (Redis, RabbitMQ, Kafka, PostgreSQL, in-memory)
+- **Architecture pattern**: pull-based (workers poll queue) or push-based (broker dispatches)
+- **Entry point**: where workers are started (separate binary, same process, sidecar)
+
+##### 2. Job Inventory
+
+Table of all registered jobs:
+
+| Job Name | Trigger | Frequency / Event | Timeout | Retry | DLQ | Priority |
+|----------|---------|-------------------|---------|-------|-----|----------|
+| `SendWelcomeEmail` | Event: user.created | On event | 30s | 3× | ✅ | normal |
+| `CleanupExpiredSessions` | Cron | Every hour | 5m | 1× | ❌ | low |
+| `ProcessPayment` | Event: order.placed | On event | 60s | 5× exponential | ✅ | high |
+
+Trigger types: `Cron` (scheduled), `Event` (message/webhook), `Manual` (API/CLI), `Cascade` (triggered by another job).
+
+##### 3. Architecture
+
+ASCII diagram showing the job processing flow:
+
+```
+Producer → Queue/Broker → Worker Pool → Result Store
+ ↓ (on failure)
+ Dead Letter Queue → Alert
+```
+
+For each component:
+- **Producer**: which services enqueue jobs, how (SDK call, publish message, HTTP endpoint)
+- **Queue**: queue names, partitioning, priority levels
+- **Worker**: concurrency model (process per job, thread pool, goroutine pool), startup configuration
+- **Result**: where job results are stored (database, cache, nowhere), how callers check completion
+
+##### 4. Retry & Error Handling
+- **Retry policy**: max retries per job, backoff strategy (fixed, exponential, custom)
+- **Backoff configuration**: initial delay, multiplier, max delay, jitter
+- **Dead letter queue (DLQ)**: which jobs have DLQ, how DLQ is monitored, manual replay procedure
+- **Idempotency**: which jobs MUST be idempotent, how idempotency is enforced (unique keys, deduplication)
+- **Error classification**: transient errors (retry) vs permanent errors (DLQ), how the system distinguishes them
+- **Poison messages**: how malformed/unprocessable messages are handled
+
+##### 5. Concurrency & Ordering
+- **Worker count**: how many workers per queue, how it's configured
+- **Parallelism**: can the same job type run in parallel? Are there mutex/lock constraints?
+- **Ordering guarantees**: FIFO per queue? Per partition key? No guarantees?
+- **Rate limiting**: are any jobs rate-limited (e.g., email sending, API calls)?
+- **Distributed locking**: if jobs use locks, which lock mechanism (Redis, DB advisory locks, etcd)
+
+##### 6. Monitoring
+- **Metrics**: which job metrics are collected
+ - Queue depth (pending jobs)
+ - Processing duration (p50, p95, p99)
+ - Success/failure rate
+ - DLQ size
+- **Dashboards**: Grafana/Datadog dashboard links or query examples
+- **Alerting**: alert rules (e.g., "DLQ > 100", "queue depth > 1000 for 5min", "job duration > 2× p99")
+- **Logging**: what is logged per job execution (start, end, duration, error, retry count)
+
+##### 7. Scaling
+- **Horizontal scaling**: how to add more workers (replicas, autoscaling, manual)
+- **Queue partitioning**: are queues partitioned? By what key?
+- **Priority queues**: which priority levels exist, how workers consume them (weighted, strict)
+- **Backpressure**: what happens when the queue is full (block producer, drop, overflow queue)
+- **Resource limits**: CPU/memory per worker, connection pool sizing
+
+##### 8. Development
+Step-by-step guide to add a new background job:
+1. Define job payload struct/schema
+2. Implement handler function
+3. Register handler with the worker
+4. Create producer (enqueue function)
+5. Add retry/DLQ configuration
+6. Write tests (unit + integration)
+7. Add monitoring (metrics, alerts)
+8. Deploy (worker restart required?)
+
+- **Local testing**: how to run workers locally, how to enqueue test jobs
+- **Job simulation**: how to trigger a job manually for debugging (CLI, admin API, console)
+
+## General Rules
+
+- Language: English
+- Use actual job names, queue names, and code references from the project — do not invent examples
+- If no background job system exists, do not generate this file
+- For message queue consumers that are part of the infrastructure layer, defer to `infrastructure.md` for broker-level docs (connection, topology). This template owns the job-level details (handler, retry, monitoring).
diff --git a/.agents/skills/sdd/templates/docs/bootstrap.md b/.agents/skills/sdd/templates/docs/bootstrap.md
new file mode 100644
index 00000000..1d43fdf7
--- /dev/null
+++ b/.agents/skills/sdd/templates/docs/bootstrap.md
@@ -0,0 +1,126 @@
+
+# Bootstrap Documentation Template
+
+## What This Generates
+
+- `.spec/README.md` — project documentation index (Quick Facts, Project Structure, Running, Ports, Key Interfaces, Adding Features)
+- `.spec/agent-rules.md` — mandatory rules for AI agents working on this project (Code Style, Naming, Error Handling, Testing, Dependencies, Formatting)
+
+**Run this template first** — it creates the root index that all other templates reference and update.
+
+## Instructions
+
+You are a technical documentarian. Your task is to analyze the current project and create the foundational documentation in the `.spec/` directory for use by AI agents.
+
+### Step 1: Project Analysis
+
+Study the project structure. Pay attention to:
+- Root files: `go.mod`, `package.json`, `requirements.txt`, `Cargo.toml`, `pom.xml`, etc. → determine language and framework
+- Directory structure: `cmd/`, `src/`, `internal/`, `pkg/`, `app/`, `lib/`, etc.
+- Configuration: `docker-compose.yml`, `Makefile`, `Taskfile.yml`, `.github/`, CI/CD
+- API: proto files, OpenAPI/Swagger, GraphQL schemas
+- Tests: `*_test.go`, `*.test.ts`, `test/`, `tests/`, `spec/`
+- Infrastructure: `Dockerfile`, `kubernetes/`, `terraform/`, `configs/`
+- Client applications: `clients/`, `frontend/`, `web/`, `mobile/`
+- Dependencies: `go.sum`, `package-lock.json`, `poetry.lock`, etc.
+
+### Step 2: Create .spec/README.md
+
+Create `.spec/README.md` with the following structure:
+
+#### Header
+```markdown
+# {Project Name} Documentation
+This folder contains documentation to help LLMs and developers quickly understand the project context.
+```
+
+#### Documentation Index
+Group documents by category. Use only categories relevant to the project:
+
+- **Core** — Architecture, Packages, Domain, Code Style (always needed)
+- **Development** — Tools, Testing, Files/Storage (always needed)
+- **Auth & Security** — if the project has OAuth, JWT, API keys
+- **Infrastructure** — if the project has Docker, monitoring, caching, queues
+- **Clients** — if the project has frontend, mobile apps, bots
+
+For each document, add a link: `- [DOC_NAME.md](./DOC_NAME.md) — brief description`
+
+IMPORTANT: If documents have not been created yet, mark them as "TODO" or add a note that they will be created later.
+
+#### Quick Facts
+Table of key project technologies:
+
+```markdown
+| Aspect | Technology |
+|--------|------------|
+| **Language** | ... |
+| **Architecture** | ... |
+| **API** | ... |
+| **Database** | ... |
+| ... | ... |
+```
+
+Fill in only what actually exists in the project. Do not guess.
+
+#### Project Structure
+ASCII tree of main directories (1-2 levels deep) with comments:
+
+```
+project/
+├── cmd/ # Entry points
+├── internal/ # Private packages
+├── ...
+```
+
+#### Running
+Key commands for running, testing, and building. Source from Makefile/Taskfile/package.json.
+
+#### Ports
+Port table (if server components exist). Determine from docker-compose.yml, configs, code.
+
+#### Key Interfaces / Entry Points
+Main interfaces or application entry points.
+
+#### Adding New Features
+Brief guide: how to add a new endpoint / page / module.
+
+### Step 3: Create .spec/agent-rules.md
+
+Create `.spec/agent-rules.md` with rules for AI agents specific to this project.
+
+Determine rules based on:
+- Programming language (Go → Effective Go, Python → PEP8, TypeScript → project standards)
+- Linter config (`.golangci.yml`, `.eslintrc`, `.prettierrc`, `pyproject.toml`)
+- Existing code (naming patterns, error handling, file structure)
+
+Required sections:
+- **Code Style** — main style rules
+- **Naming Conventions** — variable, function, and file naming
+- **Error Handling** — project's error handling pattern
+- **Testing** — testing conventions (framework, coverage, patterns)
+- **Dependencies** — how to manage dependencies
+- **Formatting** — formatting and linting
+
+Each rule: 1-2 lines. No filler.
+
+### Step 4: Output Recommendations
+
+After creating files, output a list of recommended documents to generate:
+
+```
+Recommended documents for generation:
+1. ARCHITECTURE.md — [reason: detected Clean Architecture / MVC / ... pattern]
+2. PACKAGES.md — [reason: X packages in the project]
+3. ...
+```
+
+Include only documents for which there is real content in the project.
+
+## General Rules
+
+- Language: English
+- Use only facts from actual code — do not invent
+- If something is unclear — mark as "TODO: requires clarification"
+- Format: Markdown, ASCII diagrams, tables
+- Style: concise, no filler
+- After creating files, the README.md should serve as the map for all future documentation
diff --git a/.agents/skills/sdd/templates/docs/cli.md b/.agents/skills/sdd/templates/docs/cli.md
new file mode 100644
index 00000000..2e9f5ac5
--- /dev/null
+++ b/.agents/skills/sdd/templates/docs/cli.md
@@ -0,0 +1,146 @@
+
+# CLI Documentation Template
+
+## What This Generates
+
+- `.spec/CLI.md` — command tree, flags, arguments, configuration, exit codes, I/O contracts
+
+## Instructions
+
+You are a technical documentarian. Create CLI application documentation for the project in the `.spec/` directory.
+Analyze: entry point files, command definitions, flag/argument parsers, configuration loaders, and shell completion scripts.
+
+### Step 1: Identify CLI Framework
+
+Check for the presence of:
+- **Go**: cobra, urfave/cli, kong, pflag, flag (stdlib)
+- **Rust**: clap, structopt, argh
+- **Python**: click, typer, argparse, fire
+- **Node.js**: commander, yargs, oclif, meow, citty
+- **Ruby**: thor, optparse, gli
+- **Dart**: args, dcli
+
+Determine from imports, `go.mod`, `Cargo.toml`, `package.json`, `requirements.txt`, `pubspec.yaml`, etc.
+
+### Step 2: Create .spec/CLI.md
+
+#### Structure:
+
+##### 1. Overview
+- One sentence: what the CLI does and who uses it
+- Installation command (if applicable)
+- Quick start example (the most common invocation)
+
+##### 2. Command Tree
+
+ASCII tree of all commands and subcommands:
+```
+myapp
+├── init # Initialize a new project
+├── run # Run the application
+│ ├── --watch # Watch mode
+│ └── --port N # Port number
+├── config
+│ ├── get # Read config value
+│ ├── set # Write config value
+│ └── list # List all config values
+└── version # Print version
+```
+
+Build from actual command registration code (e.g., `rootCmd.AddCommand()`, `@app.command()`, `.command()`).
+
+##### 3. Commands Reference
+
+For each command (or command group):
+
+```markdown
+### `myapp [flags]`
+
+**Description:** One sentence.
+
+| Flag / Argument | Type | Required | Default | Description |
+|----------------|------|----------|---------|-------------|
+| `` | string | yes | — | What it is |
+| `--flag` / `-f` | bool | no | `false` | What it does |
+| `--output` / `-o` | string | no | `stdout` | Output destination |
+
+**Examples:**
+\```bash
+myapp run --port 8080
+myapp config set key value
+\```
+```
+
+Extract flags from actual struct tags, decorators, or builder calls — do not invent.
+
+##### 4. Configuration
+
+- Config file locations (precedence order): CLI flags → env vars → config file → defaults
+- Config file format (YAML, TOML, JSON, INI)
+- Config file path resolution (XDG, `$HOME/.config/`, project-local)
+- Environment variable naming convention (e.g., `MYAPP_PORT`, `MYAPP_LOG_LEVEL`)
+
+Table:
+| Setting | Flag | Env Var | Config Key | Default |
+|---------|------|---------|------------|---------|
+
+##### 5. Exit Codes
+
+| Code | Meaning |
+|------|---------|
+| `0` | Success |
+| `1` | General error |
+| `2` | Usage error (invalid flags/arguments) |
+| ... | Project-specific codes |
+
+Extract from actual code if defined (e.g., `os.Exit()`, `process.exit()`, `std::process::exit()`).
+
+##### 6. I/O Contracts
+
+- **stdin**: Does the CLI read from stdin? What format? (e.g., piped JSON, line-delimited)
+- **stdout**: What is printed on success? (data output, structured JSON, human-readable text)
+- **stderr**: What goes to stderr? (logs, progress, errors)
+- **Files**: Does the CLI create/modify files? Where?
+
+Piping and composition examples:
+```bash
+cat input.json | myapp process --format json > output.json
+myapp list --json | jq '.[] | .name'
+```
+
+##### 7. Shell Completion
+
+- Supported shells (bash, zsh, fish, PowerShell)
+- Installation commands for each shell
+- How completions are generated (static file, dynamic `completion` subcommand)
+
+If no completion support exists, note it explicitly.
+
+##### 8. Global Flags
+
+Flags available on all commands:
+| Flag | Type | Default | Description |
+|------|------|---------|-------------|
+| `--verbose` / `-v` | bool | `false` | Verbose output |
+| `--config` / `-c` | string | `~/.config/myapp/config.yaml` | Config file path |
+| `--quiet` / `-q` | bool | `false` | Suppress non-error output |
+
+##### 9. Error Messages & Troubleshooting
+
+Common error messages and their resolutions:
+| Error | Cause | Fix |
+|-------|-------|-----|
+
+##### 10. Development
+
+- How to run the CLI locally during development
+- How to build a release binary
+- How to add a new command (step-by-step referencing the framework)
+
+## General Rules
+
+- Language: English
+- All commands, flags, and examples must come from actual source code — do not invent
+- If the CLI has no subcommands (single-command tool), adapt the structure: skip the command tree, expand the flags section
+- If the project is not a CLI application, do not generate this file — skip entirely
+- After creating, update `.spec/README.md`: add a link under the appropriate section
diff --git a/.agents/skills/sdd/templates/docs/clients.md b/.agents/skills/sdd/templates/docs/clients.md
new file mode 100644
index 00000000..6f21ac2b
--- /dev/null
+++ b/.agents/skills/sdd/templates/docs/clients.md
@@ -0,0 +1,175 @@
+
+# Clients Documentation Template
+
+## What This Generates
+
+- `.spec/CLIENTS.md` — overview of all client applications
+- `.spec/.md` — one file per significant client (e.g., `FRONTEND.md`, `TELEGRAM.md`, `MOBILE.md`, `CLI.md`)
+
+## Instructions
+
+You are a technical documentarian. Create client application documentation for the project in the `.spec/` directory.
+Analyze directories: `clients/`, `frontend/`, `web/`, `mobile/`, `app/`, `bot/`, `cli/`, etc.
+
+### Step 1: Identify Client Applications
+
+Check for the presence of:
+- **Web SPA**: React, Vue, Angular, Svelte, Next.js, Nuxt
+- **Mobile**: React Native, Flutter, Swift, Kotlin
+- **Telegram**: Mini App, Bot
+- **Desktop**: Electron, Tauri
+- **CLI**: cobra, click, argparse
+- **Browser Extension**: Chrome, Firefox
+
+For each found client, analyze: `package.json`, `pubspec.yaml`, `build.gradle`, etc.
+
+### Step 2: Create .spec/CLIENTS.md (overview)
+
+#### Structure:
+
+##### 1. Available Clients
+Table:
+| Client | Location | Technology | Purpose |
+|--------|----------|------------|---------|
+
+##### 2. Shared Code
+What clients share:
+- API types (TypeScript interfaces, Dart models)
+- API client (Axios, Dio, fetch wrapper)
+- Commands for syncing shared code
+
+**Code Generation** (if applicable):
+- Source of truth: proto files, OpenAPI spec, GraphQL schema
+- Generation command (e.g., `make gen-client`, `npm run codegen`)
+- Output directory (e.g., `clients/shared/api/`)
+- Sync workflow: when to regenerate, how to verify freshness
+
+**API Version Management** (if applicable):
+- How clients handle API versioning (URL prefix, header, content-type)
+- Backward compatibility strategy (deprecation notices, feature flags)
+- How multiple API versions coexist on the client side
+
+##### 3. API Communication
+ASCII diagram: how clients communicate with the backend:
+```
+┌──────────┐ ┌──────────┐
+│ Client │────>│ Backend │
+│ (React) │ REST│ (API) │
+└──────────┘ └──────────┘
+```
+Protocol (REST, gRPC, WebSocket, GraphQL), ports.
+
+##### 4. Authentication Flow
+Authentication steps from the client perspective:
+1. Registration → endpoint
+2. Login → endpoint → token receipt
+3. Token storage (localStorage, SecureStorage, cookies)
+4. Sending in header
+
+##### 5. Adding a New Client
+Options (React-based, native mobile, PWA, etc.) with step-by-step instructions.
+
+##### 6. Development Workflow
+Commands for running the full stack (backend + clients).
+
+### Step 3: Create Per-Client Documents
+
+Create a separate document if the client:
+- Has its own technology stack
+- Contains > 10 files
+- Has specific logic (Telegram SDK, TON Connect, etc.)
+
+#### Per-Client Template: .spec/{CLIENT_NAME}.md
+
+Example names: `FRONTEND.md`, `TELEGRAM.md`, `MOBILE.md`, `CLI.md`
+
+##### 1. Overview
+One sentence + link to base template (if applicable).
+Path: `clients/web/` or equivalent.
+
+##### 2. Tech Stack
+Table:
+| Technology | Purpose |
+|------------|---------|
+
+Determine from `package.json` / `pubspec.yaml` / `build.gradle`.
+
+##### 3. Directory Structure
+ASCII tree (2 levels):
+```
+clients/web/
+├── src/
+│ ├── api/ # API client and types
+│ ├── components/ # Reusable components
+│ ├── pages/ # Page components
+│ └── ...
+└── package.json
+```
+
+##### 4. Commands
+```bash
+npm install # Install dependencies
+npm run dev # Start dev server
+npm run build # Production build
+...
+```
+
+##### 5. Routes / Navigation
+Route table:
+| Path | Component | Auth Required |
+|------|-----------|---------------|
+
+##### 6. API Client
+- Configuration (base URL, interceptors)
+- Authentication (how the token is stored and sent)
+- Available methods (table: Method | Endpoint | Auth)
+
+##### 7. UI Framework / Theme (if applicable)
+- Which UI kit is used
+- Colors, fonts, styling approach
+
+##### 8. Platform-Specific Features
+
+**For Telegram Mini App:**
+- SDK initialization
+- initData authentication
+- Mock environment for local development
+- Native UI components (@telegram-apps/telegram-ui)
+- TON Connect (if applicable)
+- Auto-login flow
+
+**For Mobile (React Native / Flutter):**
+- Platform-specific code (iOS/Android)
+- Push notifications
+- Deep linking
+- Native modules
+
+**For CLI:**
+- Commands and subcommands
+- Flags and arguments
+- Configuration files
+
+##### 9. Environment Variables
+```bash
+VITE_API_URL=http://localhost:10002
+# or
+NEXT_PUBLIC_API_URL=...
+```
+
+##### 10. Development vs Production
+Differences between dev and prod modes.
+
+##### 11. Deployment
+How to deploy the client (GitHub Pages, Vercel, App Store, npm publish).
+
+##### 12. Adding New Pages / Features
+Step-by-step instructions for adding a new page/feature.
+
+## General Rules
+
+- If a client is trivial (< 5 files) — do not create a separate document, describe it in `CLIENTS.md`
+- Determine technologies from actual `package.json` / dependencies, do not guess
+- Routes must come from the actual router code (`App.tsx`, `routes.tsx`, `router.ts`)
+- API methods must come from the actual API client
+- After creating files, update `.spec/README.md`: add links under the Clients section
+- If there are no client applications in the project, do not generate any files — skip this template entirely
diff --git a/.agents/skills/sdd/templates/docs/components.md b/.agents/skills/sdd/templates/docs/components.md
new file mode 100644
index 00000000..113afa6a
--- /dev/null
+++ b/.agents/skills/sdd/templates/docs/components.md
@@ -0,0 +1,176 @@
+
+# Components Documentation Template
+
+## What This Generates
+
+- `.spec/COMPONENTS.md` — design system, component library, composition patterns, theming
+
+## Instructions
+
+You are a technical documentarian. Create UI component documentation for the project in the `.spec/` directory.
+Analyze: component directories, shared UI libraries, theme/token files, Storybook config, and style systems.
+
+### Step 1: Identify Component Architecture
+
+Check for the presence of:
+- **React**: `components/`, JSX/TSX files, Storybook, Radix, shadcn/ui, MUI, Ant Design, Chakra
+- **Vue**: `components/`, SFC (`.vue` files), Vuetify, PrimeVue, Quasar, Headless UI
+- **Angular**: `components/`, Angular Material, PrimeNG, standalone components
+- **Svelte**: `lib/components/`, SvelteKit components, Skeleton UI, Flowbite
+- **Flutter/Dart**: `widgets/`, `lib/ui/`, custom Widget classes, Material/Cupertino
+- **Web Components**: Custom Elements, Lit, Stencil
+
+Determine from framework files, imports, and component registration patterns.
+
+### Step 2: Create .spec/COMPONENTS.md
+
+#### Structure:
+
+##### 1. Overview
+- One sentence: component architecture approach
+- UI library / framework used (if any)
+- Design methodology: Atomic Design, BEM, component composition, compound components
+- Styling approach: CSS Modules, styled-components, Tailwind, CSS-in-JS, vanilla CSS, SCSS
+
+##### 2. Component Inventory
+
+Master table of all shared/reusable components:
+
+| Component | Location | Category | Props / Inputs | Used By |
+|-----------|----------|----------|----------------|---------|
+| `Button` | `src/components/Button.tsx` | Atom | `variant`, `size`, `disabled`, `onClick` | Throughout |
+| `Modal` | `src/components/Modal.tsx` | Molecule | `isOpen`, `onClose`, `title`, `children` | `Settings`, `Confirm` |
+| `DataTable` | `src/components/DataTable.tsx` | Organism | `columns`, `data`, `onSort`, `pagination` | `Users`, `Orders` |
+
+Categorize by complexity level (Atom / Molecule / Organism / Template) or by domain (Layout / Form / Feedback / Navigation / Data Display) — whichever the project uses.
+
+##### 3. Component API Patterns
+
+Document the conventions used across all components:
+
+**Props / Inputs:**
+- Naming conventions (e.g., `onX` for callbacks, `isX` for booleans, `xRef` for refs)
+- Required vs optional patterns
+- Default values strategy
+- Children / slots / content projection patterns
+
+**Composition patterns used:**
+- Compound components (e.g., `