commit 36764b8ab056c02a5203ec87991f4c2117ac6cc6 Author: Oleg Yurchik Date: Sat Aug 8 11:24:44 2026 +0300 Init repository diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95ecb84 --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Virtual environments +.venv/ +venv/ +ENV/ +env/ + +# Environment files (do not commit secrets) +.env +.env.local +.env.*.local + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Pytest +.pytest_cache/ +.coverage +htmlcov/ + +# Mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Ruff +.ruff_cache/ + +# IDEs +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..548162f --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# AI Skills + +This repository contains skills and a superpower used by AI agents when working on this codebase. + +All documents are written in English. They describe technology choices, project structure, architecture, and coding conventions. + +## Index + +| File | Type | Description | +|------|------|-------------| +| [`skills/python-development/SKILL.md`](skills/python-development/SKILL.md) | Superpower | High-level philosophy, when to apply, and skill routing map | +| [`skills/python-development-stack/SKILL.md`](skills/python-development-stack/SKILL.md) | Skill | Technology stack: Python, FastAPI, SQLModel, facet, typer, loguru, uv | +| [`skills/python-development-layout/SKILL.md`](skills/python-development-layout/SKILL.md) | Skill | File and folder organization, service and adapter package layout | +| [`skills/python-development-architecture/SKILL.md`](skills/python-development-architecture/SKILL.md) | Skill | Ports and Adapters, Monolith-over-Microservices, facet hierarchy | +| [`skills/python-development-code-style/SKILL.md`](skills/python-development-code-style/SKILL.md) | Skill | Imports, `__init__.py`, wildcard bans, block grouping | +| [`skills/python-development-settings/SKILL.md`](skills/python-development-settings/SKILL.md) | Skill | Pydantic Settings, BaseModel, YAML/JSON loading, validators | +| [`skills/python-development-cli/SKILL.md`](skills/python-development-cli/SKILL.md) | Skill | Typer composition, callback boot, deferred imports, nesting | + +## Usage + +When an agent starts working on a task, it should load the superpower first. +The superpower contains a decision tree that points to the relevant granular skills. + +## Customizing + +Edit the files in `skills/` to match your actual preferences, frameworks, and team conventions. diff --git a/skills/python-development-architecture/SKILL.md b/skills/python-development-architecture/SKILL.md new file mode 100644 index 0000000..bf17014 --- /dev/null +++ b/skills/python-development-architecture/SKILL.md @@ -0,0 +1,13 @@ +--- +name: python-development-architecture +description: Architectural patterns, service boundaries, API design, resilience, and observability. Use when designing systems, reviewing integration patterns, or investigating production issues. +--- + +## Rules +- Follow Ports and Adapters (Hexagonal Architecture). Define abstract interfaces (ports) at the package level in `interfaces.py` or `base.py`. Place concrete implementations (adapters) in dedicated sub-packages named after the infrastructure type, e.g. `database/`, `filesystem/`, `memory/`. +- When a port has multiple adapter implementations, provide a factory module (conventionally `fabric.py` in the port's package) that selects and instantiates the concrete implementation based on a type discriminator in settings. The rest of the codebase imports only the port; it must never import a concrete adapter directly. +- Follow the Monolith-over-Microservices approach. Each top-level package inside the main project folder is a bounded context and a candidate for future extraction into a standalone service. +- A package becomes an independent runnable service when it contains a `service.py` module with an async service class. The class participates in the hierarchical service tree managed by the service orchestration library. +- The root service of the application aggregates child services through a declarative `dependencies` property. The orchestration library handles the full async lifecycle of the tree: concurrent start, run, and graceful shutdown in reverse dependency order. +- Define a single central composition root (e.g. a resolver or registry class). It reads configuration, lazily instantiates adapters and services, caches resolved instances, and injects them into consumers. Domain code must never construct its own dependencies. +- Package business logic into capability modules that expose operations to higher layers. A capability module declares what it needs (required capabilities) and what it can do (provided capabilities) through class-level declarations. The execution context and accessors are injected by the calling layer at runtime based on these declarations. diff --git a/skills/python-development-cli/SKILL.md b/skills/python-development-cli/SKILL.md new file mode 100644 index 0000000..dbaf0f5 --- /dev/null +++ b/skills/python-development-cli/SKILL.md @@ -0,0 +1,100 @@ +--- +name: python-development-cli +description: CLI design patterns using typer. Use when building, nesting, or extending command-line interfaces. +--- + +## Rules +- Every package that exposes CLI commands contains a `cli.py` module with a `get_cli() -> typer.Typer` factory function. Do not create the Typer instance at import time. +- The root package CLI (`cli.py` in the root package) is the composition point. It calls `cli.add_typer(get_subpackage_cli(), name="subpackage")` to mount sub-package CLI trees as subcommands. +- The root CLI defines a single `callback` function that runs before any command. It is responsible for shared bootstrap: load settings, configure logging, and store the settings object in `ctx.obj["settings"]` so every sub-command can access it. + +Example root CLI `cli.py`: +```python +import pathlib + +import typer +from loguru import logger + +from .service import ApplicationService +from .settings import RootSettings + + +def callback( + ctx: typer.Context, + env_path: pathlib.Path | None = typer.Option( + None, "--env", "-e", + help="Environment variables file location", + ), + config_path: pathlib.Path | None = typer.Option( + None, "--config", "-c", + help="Config file location", + ), +): + ctx.obj = {} + settings = RootSettings.load( + env_prefix="APP__", + env_file=env_path, + config_file=config_path, + ) + ctx.obj["settings"] = settings + logger.configure(handlers=[{"sink": sys.stderr, "level": settings.logging.level}]) + + +def run(ctx: typer.Context): + settings: RootSettings = ctx.obj["settings"] + service = ApplicationService(settings=settings) + asyncio.run(service.run()) + + +def get_cli() -> typer.Typer: + from .agents import get_cli as get_agents_cli + from .cron import get_cli as get_cron_cli + + cli = typer.Typer() + cli.callback()(callback) + cli.command(name="run")(run) + cli.add_typer(get_agents_cli(), name="agents") + cli.add_typer(get_cron_cli(), name="cron") + return cli +``` + +- Sub-package CLI commands receive `ctx: typer.Context` as the first parameter and read settings via `ctx.obj["settings"]`. + +Example sub-package `cli.py`: +```python +import asyncio + +import typer + + +def run( + ctx: typer.Context, + name: str | None = typer.Argument(default=None), + debug: bool = typer.Option(False, "-d", "--debug"), +): + # Deferred imports — heavy project modules are imported inside the command body + from mypackage.resolver import DependencyResolver + from mypackage.settings import RootSettings + from mypackage.channels.tui import TUIChannel, TUIChannelSettings + + settings: RootSettings = ctx.obj["settings"] + resolver = DependencyResolver(settings=settings) + channel = TUIChannel(settings=TUIChannelSettings(debug=debug), ...) + asyncio.run(channel.run()) + + +def get_cli() -> typer.Typer: + cli = typer.Typer() + cli.command(name="run")(run) + return cli +``` + +- Each CLI command is a plain Python function decorated with `cli.command(name="...")`. Keep command functions small: parse arguments, create a resolver or service from settings, and run async code with `asyncio.run()`. No business logic inside the command function. +- The root package `__main__.py` calls `get_cli()` and runs it. Example: +```python +from .cli import get_cli + +if __name__ == "__main__": + cli = get_cli() + cli() +``` diff --git a/skills/python-development-code-style/SKILL.md b/skills/python-development-code-style/SKILL.md new file mode 100644 index 0000000..9217936 --- /dev/null +++ b/skills/python-development-code-style/SKILL.md @@ -0,0 +1,56 @@ +--- +name: python-development-code-style +description: Code conventions, typing, testing habits, and engineering principles. Use when writing, reviewing, or refactoring Python code. +--- + +## Rules +- Follow PEP 8 for all Python code. Use `ruff` for automatic formatting and linting. +- Every package `__init__.py` must declare the public API explicitly with `__all__`. `__all__` is always a tuple, even when it contains a single element. Use inline comment group headers to separate imports by source module. +- Inside `__init__.py` import only from sibling modules. Do not place executable code. +- Group related names together in `__all__` and mirror the grouping with comment headers. Example: + ```python + from .base import BaseService, ServiceMixin + from .dto import ServiceInput, ServiceOutput + from .exceptions import ServiceError, ValidationError + from .fabric import get_service + from .settings import ServiceSettings + + __all__ = ( + # base + "BaseService", + "ServiceMixin", + # dto + "ServiceInput", + "ServiceOutput", + # exceptions + "ServiceError", + "ValidationError", + # fabric + "get_service", + # settings + "ServiceSettings", + ) + ``` +- Wildcard imports (`from module import *`) are forbidden in every module, including `__init__.py`. Always import explicit names. +- Within a package, import sibling modules using relative imports (`from .module import Name`). When importing from a different top-level package or from a parent package that is not a direct sibling, use an absolute import (`from project_name.module import Name`). +- Group imports in every module into three blocks separated by a blank line: + 1. Standard library (`import pathlib`, `from typing import Self`, etc.). + 2. Third-party packages (`import yaml`, `from pydantic import Field`, etc.). + 3. Project-local imports (`from .services import MyService`, `from microclaw.settings import Settings`, etc.). +- Within each block, place `from ... import ...` lines first, then `import ...` lines. Sort all lines alphabetically. Example: + ```python + import json + import pathlib + from typing import Self + + import yaml + from pydantic import Field + from pydantic_settings import BaseSettings + + from .services import MyService + from microclaw.settings import Settings + ``` +- Functions and methods must not return tuples of multiple values. Returning a tuple is allowed only in exceptional cases (e.g. unpacking a well-known pair such as `(key, value)`). When a function needs to return more than one piece of data, define a Pydantic `BaseModel` for the result or reuse an existing model. This makes the return type self-documenting, enables IDE autocomplete, and keeps field names stable during refactoring. +- Do not use `from __future__ import annotations`. Use explicit forward-reference strings (e.g. `"MyClass"`) when necessary. +- Do not use `if TYPE_CHECKING:` blocks. Import the types you need at the top level. Deferred imports inside `TYPE_CHECKING` hide dependencies and break runtime introspection. +- Do not abbreviate variable or parameter names. Write them out in full so the intent is obvious without context. Examples of forbidden abbreviations: `idx` (use `index`), `dep` (use `dependency`), `cfg` (use `config` or `configuration`), `msg` (use `message`), `err` (use `error`), `resp` (use `response`). The same rule applies to class attributes, function parameters, and local variables. diff --git a/skills/python-development-fastapi/SKILL.md b/skills/python-development-fastapi/SKILL.md new file mode 100644 index 0000000..f9bb4ae --- /dev/null +++ b/skills/python-development-fastapi/SKILL.md @@ -0,0 +1,114 @@ +--- +name: python-development-fastapi +description: Patterns for building a FastAPI HTTP service inside the facet service tree. Use when creating REST endpoints, routers, or the api/rest service package. +--- + +## Rules +- The FastAPI service lives in a sub-package named `api/rest/` inside the root package. It is a regular facet service: it has `service.py`, `settings.py`, and `__init__.py`. +- Provide a custom `UvicornServer` that disables signal handlers so the parent facet orchestrator controls startup and shutdown. Example: + ```python + import uvicorn + + class UvicornServer(uvicorn.Server): + def install_signal_handlers(self): + pass + ``` +- The service class inherits from `facet.AsyncioServiceMixin`. Its constructor accepts the settings instance declared in the same package's `settings.py`. Keep the constructor minimal; heavy initialization happens in `start()`. +- The `start()` method builds the FastAPI application asynchronously, configures uvicorn, and adds the server coroutine to the facet task list with `self.add_task(server.serve())`. +- Extract app construction into an `async` `_build_app()` method so router registration and dependency wiring can use `await`. +- Register routers in `_setup_app()` or `_build_app()` using `app.include_router(...)`. Group related endpoints under routers with a `prefix`. +- The settings class inherits from `pydantic.BaseModel` (not `BaseSettings`). It declares the minimum required for the HTTP layer: `host`, `port`, and references to other subsystem settings when the REST layer wires them directly. + +### Minimal complete example + +`api/rest/settings.py`: +```python +from pydantic import BaseModel, conint + + +class RESTAPISettings(BaseModel): + host: str = "127.0.0.1" + port: conint(ge=1, le=65535) = 8000 +``` + +`api/rest/service.py`: +```python +import facet +import fastapi +import uvicorn + +from . import handlers, users +from .settings import RESTAPISettings + + +class UvicornServer(uvicorn.Server): + def install_signal_handlers(self): + pass + + +class RESTAPIService(facet.AsyncioServiceMixin): + def __init__(self, settings: RESTAPISettings): + self._settings = settings + + async def start(self): + app = await self._build_app() + config = uvicorn.Config( + app=app, + host=self._settings.host, + port=self._settings.port, + ) + server = UvicornServer(config) + self.add_task(server.serve()) + + async def _build_app(self) -> fastapi.FastAPI: + app = fastapi.FastAPI() + await self._setup_app(app) + return app + + async def _setup_app(self, app: fastapi.FastAPI): + app.get("/health")(handlers.health) + app.include_router(users.get_users, prefix="/users") +``` + +### Router and handler structure + +- Endpoints must be RESTful. The URL path structure maps directly to the package structure inside `api/rest/`. +- Each URL segment becomes a sub-package. For example `/users/{id}` lives in `api/rest/users/`. +- Every handler sub-package contains at minimum: + - `router.py` — defines `get_router() -> fastapi.APIRouter` and wires each route to a handler function. + - `handlers.py` — plain async functions that implement the endpoint logic. + - `schemas.py` — Pydantic request and response models shared by the handlers in this package. + - `__init__.py` — exports only `get_router`. +- Optional but common additional files: + - `dependencies.py` — FastAPI dependency functions reused across handlers in the package. + - `exceptions.py` — custom HTTP exceptions for this domain. + +Example `api/rest/users/router.py`: +```python +import fastapi + +from . import handlers + + +def get_router() -> fastapi.APIRouter: + router = fastapi.APIRouter() + router.add_api_route(path="/", methods=["GET"], endpoint=handlers.list_users) + router.add_api_route(path="/", methods=["POST"], endpoint=handlers.create_user) + router.add_api_route(path="/{id}", methods=["GET"], endpoint=handlers.get_user) + router.add_api_route(path="/{id}", methods=["PUT"], endpoint=handlers.update_user) + router.add_api_route(path="/{id}", methods=["DELETE"], endpoint=handlers.delete_user) + return router +``` + +Example `api/rest/users/__init__.py`: +```python +from .router import get_router + +__all__ = ( + # router + "get_router", +) +``` + +- Do not construct `fastapi.APIRouter` instances at import time outside `get_router()`. The router is created only when `get_router()` is called during `_setup_app()`. +``` diff --git a/skills/python-development-layout/SKILL.md b/skills/python-development-layout/SKILL.md new file mode 100644 index 0000000..38562a9 --- /dev/null +++ b/skills/python-development-layout/SKILL.md @@ -0,0 +1,45 @@ +--- +name: python-development-layout +description: Folder structure and file organization for Python services. Use when creating a new service, reorganizing modules, or reviewing project structure. +--- + +## Rules +- Every repository must contain a `README.md`. +- Every repository must contain a `.gitignore` that excludes `.venv/`, `*.pyc`, and `__pycache__/`. +- Every repository must contain a `.dockerignore` with content similar to `.gitignore`. +- Every repository must contain a `Dockerfile` for containerized deployment. +- Every repository must contain a `pyproject.toml` created automatically by `uv init`. +- The Python package source folder lives in the repository root (e.g. `my_package/`), not under a `src/` directory. +- `pyproject.toml` must explicitly name the root package folder so the build system knows where the code lives. +- All tool configuration (tests, linters, formatters, type checkers, coverage, etc.) must live inside `pyproject.toml` under the `[tool.*]` tables. Do not use separate dot-files such as `.pylintrc`, `.flake8`, `setup.cfg`, or `tox.ini` for tool settings. +- The repository root may also contain a `tests/` folder and a `docs/` folder alongside the package folder. +- Inside the root package every sub-package is either a service package, an adapter package, or a library package. The structure of each must follow the conventions below. +- The root package must contain a `__main__.py` module. It is the executable entry point when the package is run with `python -m package_name`. Keep it minimal: import only from sibling modules (relative imports), call the factory that builds the CLI or the root service, and invoke it under `if __name__ == "__main__":`. No business logic lives here. Example: + ```python + from .cli import get_cli + + if __name__ == "__main__": + cli = get_cli() + cli() + ``` + +### Service package layout +A package that exposes a runnable service must contain at minimum: +- `service.py` — the service class. It inherits from the async service mixin and implements `start`/`stop` lifecycle methods. The constructor accepts exactly one settings instance — the class declared in the same package's `settings.py`. Example: + ```python + import facet + + from .settings import MyServiceSettings + + class MyService(facet.AsyncioServiceMixin): + def __init__(self, settings: MyServiceSettings): + self._settings = settings + + async def start(self): + # service startup logic + pass + ``` +- `settings.py` — a Pydantic Settings class that declares the configuration schema for the service. +- `interfaces.py` or `base.py` — abstract ports (interfaces) that the service depends on. Consumers import from here, never from adapters. +- `fabric.py` — a factory module that instantiates the service and its collaborators. It receives settings and returns fully constructed objects. +- `__init__.py` — public API of the package. Exports only the types and functions intended for external use. diff --git a/skills/python-development-settings/SKILL.md b/skills/python-development-settings/SKILL.md new file mode 100644 index 0000000..1c2476f --- /dev/null +++ b/skills/python-development-settings/SKILL.md @@ -0,0 +1,38 @@ +--- +name: python-development-settings +description: Application configuration and settings management. Use when defining, loading, validating, or extending application settings. +--- + +## Rules +- The root settings class must inherit from `pydantic_settings.BaseSettings`. This enables automatic loading from environment variables and `.env` files. +- Every nested settings class (for individual services, adapters, or components) must inherit from `pydantic.BaseModel`, not from `BaseSettings`. Only the root class needs the env-loading behavior. +- The root settings class exposes one field per top-level service or subsystem. Provide sensible defaults as plain values for immutable types (str, int, bool, None) and use `Field(default_factory=...)` only when the default is a mutable object (dict, list, or model instance). Never use mutable literals as class-attribute defaults in Pydantic v2. Provide defaults so the application starts without a config file when reasonable. +- A settings class can accept either a settings object or a string key referencing another entry in the same config. The resolver (or a `@model_validator`) replaces string references with resolved instances after initial parsing. This is how services declare their dependencies declaratively. +- Use `@model_validator(mode="after")` on the root settings class to validate cross-references between subsystems, resolve string keys to actual instances, and enforce consistency across the configuration graph. +- Support loading from YAML config files with custom tags. Register `!include` for file inclusion and `!env` for environment variable interpolation inside YAML. The YAML loader builder must live as a `@staticmethod` inside the root settings class. Example: + ```python + @staticmethod + def get_yaml_loader(base_path: pathlib.Path) -> yaml.BaseLoader: + loader = type("Loader", (yaml.SafeLoader,), {}) + loader.add_constructor( + "!include", + yaml_include.Constructor(base_dir=str(base_path)), + ) + loader.add_constructor("!env", construct_env_tag) + return loader + ``` +- Support loading from JSON config files using standard `json.load`. JSON does not support custom tags; use string interpolation or env var resolution handled by `BaseSettings` after load. +- Provide a single `load()` classmethod on the root settings class that reads the config file (YAML or JSON), then passes the parsed dict into the `BaseSettings` constructor along with optional `_env_prefix` and `_env_file`. Example: + ```python + @classmethod + def load(cls, config_file: pathlib.Path | None = None) -> Self: + data = {} + if config_file is not None: + if config_file.suffix in (".yaml", ".yml"): + data.update(yaml.load(config_file.read_text(), Loader=get_loader())) + elif config_file.suffix == ".json": + data.update(json.loads(config_file.read_text())) + return cls(**data) + ``` +- Keep each subsystem's settings in its own `settings.py` module inside the subsystem package. Import and compose them into the root settings class. Do not inline subsystem schemas into the root file. +- Use `pydantic.Field(default_factory=...)` for mutable default values (dicts, lists, models). Never use mutable literals as class-attribute defaults in Pydantic v2. diff --git a/skills/python-development-stack/SKILL.md b/skills/python-development-stack/SKILL.md new file mode 100644 index 0000000..6558120 --- /dev/null +++ b/skills/python-development-stack/SKILL.md @@ -0,0 +1,16 @@ +--- +name: python-development-stack +description: Technology stack decisions for Python services. Use when adding dependencies, choosing infrastructure, bootstrapping projects, or reviewing tool choices. +--- + +## Rules +- Use `uv` as the package manager for all Python dependency operations. +- Use `FastAPI` for REST API and HTTP services. +- Use `SQLModel` and `SQLAlchemy` for database access. Async drivers: `asyncpg` for PostgreSQL, `aiosqlite` for SQLite. +- Use `alembic` for database migrations. +- Use `facet` for hierarchical async service composition with dependency management and graceful shutdown. +- Use `typer` for CLI interfaces. +- Use `pydantic` for DTOs and data validation; use `pydantic-settings` for application configuration. +- Use `loguru` for logging. +- Use `pyyaml` for YAML parsing; use `pyyaml-include` for `!include` tags and `pyyaml-env-tag` for `!env` interpolation inside YAML config files. + diff --git a/skills/python-development/SKILL.md b/skills/python-development/SKILL.md new file mode 100644 index 0000000..d2b76c8 --- /dev/null +++ b/skills/python-development/SKILL.md @@ -0,0 +1,20 @@ +--- +name: python-development +description: Superpower for building robust, maintainable Python backend services. Use whenever you create, modify, review, or debug service code in this repository. +--- + +## Skill Map + +| Situation | Primary Skill | +|---|---| +| Choosing frameworks, libraries, DBs, or infrastructure components | [`python-development-stack`](./python-development-stack/SKILL.md) | +| Bootstrapping a new service or reviewing folder structure | [`python-development-layout`](./python-development-layout/SKILL.md) | +| Designing service boundaries, APIs, data flow, or resilience patterns | [`python-development-architecture`](./python-development-architecture/SKILL.md) | +| Writing or reviewing code, tests, configs, or observability instrumentation | [`python-development-code-style`](./python-development-code-style/SKILL.md) | +| Defining, loading, validating, or extending application settings | [`python-development-settings`](./python-development-settings/SKILL.md) | +| Building, nesting, or extending command-line interfaces | [`python-development-cli`](./python-development-cli/SKILL.md) | +| Creating a FastAPI REST service inside the facet tree | [`python-development-fastapi`](./python-development-fastapi/SKILL.md) | +| Debugging production issues (logs, traces, metrics) | [`python-development-architecture`](../python-development-architecture/SKILL.md) + [`python-development-code-style`](../python-development-code-style/SKILL.md) | + +## Rules +- Python interpreter versions are managed with `pyenv`.