From b3aac737ebe62ec9a594258bacd03101b9c81dc2 Mon Sep 17 00:00:00 2001 From: Sergey <93754860+slaid098@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:37:55 +0300 Subject: [PATCH] feat(spec): project template skill init and check flow (#237) * feat(spec): add project-template skill init and check flow * refactor(spec): remove repo-init migrate references to project-template * test(spec): structural tests for project-template skill and command * fix(test): wrap long lines and use non_templated var in project template tests * fix(ci): reformat test_project_template_skill.py for ruff format --------- Co-authored-by: opencode-agent --- .opencode/commands/project-template.md | 5 + .opencode/skills/add-skill/SKILL.md | 2 +- .opencode/skills/project-template/SKILL.md | 340 +++++++++ .opencode/skills/repo-init/SKILL.md | 759 --------------------- .opencode/skills/repo-readme/SKILL.md | 12 +- .opencode/skills/spec/SKILL.md | 4 +- tests/test_project_template_skill.py | 203 ++++++ 7 files changed, 557 insertions(+), 768 deletions(-) create mode 100644 .opencode/commands/project-template.md create mode 100644 .opencode/skills/project-template/SKILL.md delete mode 100644 .opencode/skills/repo-init/SKILL.md create mode 100644 tests/test_project_template_skill.py diff --git a/.opencode/commands/project-template.md b/.opencode/commands/project-template.md new file mode 100644 index 0000000..4d792ab --- /dev/null +++ b/.opencode/commands/project-template.md @@ -0,0 +1,5 @@ +--- +description: Project template — init new project (cookiecutter + GitHub remote) or check existing (project-status) +agent: build +--- +Load the `project-template` skill via `skill({name: "project-template"})` and follow its ПРОТОКОЛ strictly. Два flow: init (cookiecutter по типу + git + gh repo create + branch protection + project-status) или check (project-status → отчёт → рекомендации через subagents). Главный агент — оркестратор: вопросы юзеру + `project-status` tool (read-only) + делегирование. Не делает cookiecutter/git/gh напрямую. \ No newline at end of file diff --git a/.opencode/skills/add-skill/SKILL.md b/.opencode/skills/add-skill/SKILL.md index b2357ea..e3c38b2 100644 --- a/.opencode/skills/add-skill/SKILL.md +++ b/.opencode/skills/add-skill/SKILL.md @@ -49,7 +49,7 @@ description: /` (issue #229 / PR #235). Проверка +архитектуры — через `project-status` tool (issue #228 / PR #233). + +## ПРОТОКОЛ (ЖЁСТКО) + +1. Определи flow (вопрос юзеру, см. ниже). +2. init flow → шаги 1-6 (см. ниже). +3. check flow → шаги 1-3 (см. ниже). +4. После каждого шага → 1 строка прогресса юзеру (формат: `✅ `). + +### ЗАПРЕЩЕНО + +- Запускать cookiecutter / git / gh напрямую из main agent — делегируй subagent + (Template INIT, Template GITHUB). Main agent = оркестратор: вопросы юзеру + + `project-status` tool (read-only oracle) + делегирование. +- `gh repo create` БЕЗ предварительного git init + initial commit (bare-repo → + empty push → main branch не появляется → branch protection падает). +- Импровизировать тип проекта вне `VALID_TYPES` из `spec-status.py` (см. ниже). +- Запускать `/run-pipeline` автоматически — стоп после init/check, дальше юзер + сам. + +### Остановы + +- Subagent error → 1 retry, потом STOP + report пользователю. +- cookiecutter не установлен → WARN + инструкция, STOP init flow. +- Template для типа не найден → WARN, STOP init flow (предложи check flow или + тип из доступных). +- `project-status` tool вернул `⚠️ ...failed` → WARN, сообщи пользователю, + продолжай без отчёта (не блокирующий). + +## Определение flow + +Вопрос юзеру (один вопрос, multiple choice): + +``` +Это новый проект или проверка существующего? +[1] init — новый проект: cookiecutter по типу + GitHub remote + branch protection +[2] check — аудит существующего: project-status (структура, роуты, качество, README, infra) +``` + +Если юзер выбрал init, но cwd уже git-репо с коммитами и файлами → предложи +check flow (см. Граничные случаи). Если cwd пустой или юзер подтвердил init → +init flow. + +## init flow + +### Шаг 1: Вопрос — тип проекта + +``` +Выбери тип проекта (из spec-status VALID_TYPES): +[1] backend — FastAPI + Tortoise, REST API (cookiecutter template) +[2] fullstack — backend + SvelteKit frontend (cookiecutter template) +[3] cli — Python CLI tool, Typer (cookiecutter template) +[4] mcp-server — MCP + REST сервер (нет cookiecutter template — ручная инициализация) +[5] bot — Telegram bot, aiogram 3 (нет cookiecutter template — ручная инициализация) +[6] worker — Prefect flows (нет cookiecutter template — ручная инициализация) +``` + +Cookiecutter templates доступны для типов: `backend`, `fullstack`, `cli` +(директории в `.opencode/templates/`). Для `mcp-server`, `bot`, `worker` +шаблонов нет → WARN: "cookiecutter template not found for type ``. +Доступные: backend, fullstack, cli. Для остальных типов используй check flow +или создай issue на добавление template." → STOP init flow. + +### Шаг 2: Вопрос — имя, описание, опции + +``` +Имя проекта (kebab-case, станет package name и GitHub repo name): ___ +Описание (1 строка): ___ +GitHub owner: ___ +use_auth: [1] no (default) / [2] yes +use_db: [1] yes (default) / [2] no +``` + +Опции `use_auth` / `use_db` — переменные cookiecutter (см. `cookiecutter.json`). +Значения: `"no"` или `"yes"` (строки, lowercase). + +### Шаг 3: Делегирование — cookiecutter + git init + initial commit + +Subagent (general) — Template INIT ниже. Cookiecutter рендерит проект в +`.//`, post_gen_project hook удаляет файлы условные на +`use_auth`/`use_db`. Затем git init + initial commit внутри `.//`. + +### Шаг 4: Делегирование — GitHub remote + branch protection + +Subagent (general) — Template GITHUB ниже. Мигрировано из repo-init Phase A +(steps 1-3) БЕЗ изменений: `gh repo create`, squash-only merge settings, branch +protection на main. Требует локальный git-репо с initial commit (из шага 3). + +### Шаг 5: Оркестратор — project-status + +Вызови `project-status` tool напрямую (read-only oracle, ALLOWED для +оркестратора — как `pipeline-status` / `spec-status`): + +``` +project-status({}) +``` + +Tool вернёт отчёт: `Project: `, 7 групп `[OK]/[WARN]/[FAIL]`, `Итог:`, +`Рекомендации:`. Покажи отчёт юзеру. Если tool вернул `⚠️ ...failed` → WARN, +продолжай без отчёта. + +### Шаг 6: Финальный репорт + +``` +Project created at .//. +GitHub: https://github.com// +Branch protection: main (PR + required_status_checks + linear history) +Project-status: +Дальше: /run-pipeline для реализации фич, или /spec для генерации spec. +``` + +## check flow + +### Шаг 1: Оркестратор — project-status + +``` +project-status({}) +``` + +Для строгого режима (exit 1 на FAIL) — `project-status({ check: true })`. +Для пропуска медленных remote-проверок (branch protection via gh) — +`project-status({ fast: true })`. По умолчанию — non-blocking (exit 0). + +### Шаг 2: Оркестратор — отчёт + рекомендации + +Покажи полный отчёт юзеру. В разделе `Рекомендации:` — список FAIL-чеков с +путями. Сгруппируй по категориям (Структура / Качество кода / Тесты / README / +Infra / Coverage). + +### Шаг 3: Вопрос — чинить? + +``` +Найдены проблемы: . +Запустить fix-subagents для рекомендаций? +[1] да — делегируй subagent(ов) для каждого FAIL +[2] нет — только отчёт, я починю сам +``` + +Если `да` → для каждого FAIL из `Рекомендации:` создай subagent (general) с +Template FIX (ниже), передав путь и описание проблемы. Subagent чинит, коммитит +через `commit` tool, push. Один FAIL = один subagent (последовательно, не +параллельно — см. AGENTS.md Linear Execution). После всех фиксов → re-run +`project-status` для верификации. + +Если `нет` → STOP, отчёт у юзера. + +## Граничные случаи + +- **Существующий репо (не пустой)** → init flow: предложи check flow. Если юзер + настаивает на init → cookiecutter создаст `.//` рядом (не + перезапишет текущий репо). Уточни: "cwd уже git-репо с файлами. Init создаст + новый проект в подкаталоге `.//`. Продолжить? [1] да / [2] нет, + лучше check flow". +- **GitHub repo уже существует** → skip `gh repo create`, только branch + protection (если ещё не настроена). Subagent проверяет: `gh repo view + /` — если существует, пропускает create, переходит к settings + + branch protection. +- **cookiecutter не установлен** → WARN: "cookiecutter не найден. Установи: + `uv tool install cookiecutter` (или `pipx install cookiecutter`). После + установки повтори init." → STOP init flow. +- **project-status не найден** → WARN: "project-status tool не доступен (issue + #228 / PR #233 не завершён или tool не зарегистрирован). Пропускаю + project-status проверку." → продолжай без отчёта (не блокирующий). +- **Template для типа не найден** (mcp-server/bot/worker) → WARN (см. Шаг 1). +- **gh auth не настроен** → subagent упадёт на `gh repo create`. Сообщи юзеру: + "запусти `gh auth login` и повтори". + +## Prompt templates + +### Template INIT (cookiecutter + git init + initial commit) + +``` +Создай новый проект типа с именем . +Контекст: init flow project-template skill, cwd = . + +1. Проверь cookiecutter: `cookiecutter --version`. Если не установлен → STOP, + верни: "cookiecutter не установлен. Установи: `uv tool install cookiecutter`". +2. Запусти cookiecutter (no-input, переменные из ответов юзера): + `cookiecutter .opencode/templates// --no-input \ + project_name= \ + project_type= \ + description="" \ + use_auth= \ + use_db= \ + python_version=3.13` + Cookiecutter создаст каталог `.//` с рендеренным проектом. + post_gen_project hook удалит файлы условные на use_auth/use_db. +3. `cd ` (все дальнейшие команды — внутри этого каталога). +4. `git init` +5. `git add .` затем `git status` — проверь staged set (только файлы проекта, + без лишнего). Если лишнее — `git restore --staged `. +6. `commit({ message: "chore: initial commit" })` tool (НЕ raw `git commit` — + заблокирован deny). +7. Верни: "done: project created at .//, git init + initial commit". + +Если найдёшь баг вне scope — загрузи skill `bug-discovery` через +`skill("bug-discovery")` и следуй протоколу. НЕ чини баг сам. +``` + +### Template GITHUB (gh repo create + settings + branch protection) + +> Мигрировано из repo-init Phase A (steps 1-3) БЕЗ изменений. Требует локальный +> git-репо с initial commit (из Template INIT). + +``` +Настрой GitHub remote для проекта (cwd = /). +owner = , visibility = public (или private если internal). + +0. Проверь: GitHub repo уже существует? + `gh repo view /` — если exit 0, skip step 1 (create), + переходи к step 2 (settings) и step 3 (branch protection). + +1. Создание репозитория: + `gh repo create / --public --source=. --remote=origin --push` + (или --private если internal) + После создания: `gh auth setup-git` + +2. Настройки репозитория (squash-only merge, auto-delete branch): + `gh api repos// \ + --method PATCH \ + -f allow_squash_merge=true \ + -f allow_merge_commit=false \ + -f allow_rebase_merge=false \ + -f delete_branch_on_merge=true \ + -f squash_merge_commit_title=COMMIT_OR_PR_TITLE \ + -f squash_merge_commit_message=COMMIT_MESSAGES` + +3. Защита ветки main (требовать PR, required_status_checks, linear history): + `gh api repos///rules/branches/main \ + --method POST \ + -F target=branch \ + -f enforcement=active \ + --input - <<'EOF' + { + "conditions": { + "ref_name": { + "include": ["refs/heads/main"], + "exclude": [] + } + }, + "rules": [ + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": false, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": false + } + }, + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks": true, + "do_not_enforce_on_create": false, + "required_status_checks": [] + } + }, + { + "type": "deletion" + }, + { + "type": "non_fast_forward" + } + ] + } + EOF` + + `required_status_checks` заполняется именами CI-джобов после первого пуша + (имена из cookiecutter CI: `lint`, `typecheck`, `test`, `complexity` для + Python; `check` для JS/TS). На этом этапе оставь пустой массив — обновится + после первого CI-прогона. + +4. Верни: "done: GitHub remote created/verified, squash-only merge, branch + protection on main". + +Если найдёшь баг вне scope — загрузи skill `bug-discovery` через +`skill("bug-discovery")` и следуй протоколу. НЕ чини баг сам. +``` + +### Template FIX (check flow — починить FAIL из project-status) + +``` +Почини проблему из project-status отчёта. +Категория: (Структура / Качество кода / Тесты / README / Infra / Coverage) +Проблема: : +Путь: + +1. Прочитай контекст проблемы (файл по пути из рекомендации). +2. Минимальный фикс: добавь/исправь только то, что указано в рекомендации. + Не рефактори unrelated код. +3. Перед коммитом — `git status` для проверки staged set (`commit` tool НЕ + делает `git add` — коммитит только staged; используй + `git add <конкретные-пути>`, НЕ `git add -A`). +4. `commit({ message: "fix(): " })` tool (НЕ raw + `git commit`), push. +5. Верни: "done: fixed , commit ". + +Если найдёшь баг вне scope — загрузи skill `bug-discovery` через +`skill("bug-discovery")` и следуй протоколу. НЕ чини баг сам. +``` + +## VALID_TYPES (из spec-status.py) + +``` +backend, fullstack, mcp-server, cli, bot, worker +``` + +Cookiecutter templates доступны для: `backend`, `fullstack`, `cli` +(директории `.opencode/templates//`). + +## Rules + +- Main agent = оркестратор: вопросы юзеру + `project-status` tool (read-only) + + делегирование subagent'ам (Template INIT / GITHUB / FIX). Не делает + cookiecutter/git/gh напрямую. +- `project-status` tool — read-only oracle, ALLOWED для оркестратора (как + `pipeline-status` / `spec-status`). +- init flow порядок: cookiecutter → git init/commit → gh repo create → branch + protection → project-status. Не меняй порядок. +- check flow: project-status → отчёт → рекомендации → subagents (последовательно). +- Subagent error → 1 retry, потом STOP + report. +- Совместим с spec-pipeline: Phase 8 EXECUTE (spec/SKILL.md Template I) может + вызывать project-template init для scaffolding issue. +- `commit` tool НЕ делает `git add` — коммитит только staged. Используй + `git add <конкретные-пути>`, НЕ `git add -A`. \ No newline at end of file diff --git a/.opencode/skills/repo-init/SKILL.md b/.opencode/skills/repo-init/SKILL.md deleted file mode 100644 index d7d0102..0000000 --- a/.opencode/skills/repo-init/SKILL.md +++ /dev/null @@ -1,759 +0,0 @@ ---- -name: repo-init -description: Sequential checklist: create GitHub remote → configure settings/branch protection → scaffold project files (Python/JS). Use when starting a new repo. Also when user says "новый репо", "создай репозиторий", "настрой репо". ---- - -# Repo Init - -Полный чек-лист инициализации нового репозитория. Все шаблоны — внутри, берутся из эталонных репозиториев (reference repos). - -## Фазы - -- **Phase A — GitHub remote** (шаги 1-3, выполняется один раз): создание репо, настройки merge, защита ветки. Требует локального git-репо с initial commit. -- **Phase B — Project scaffolding** (шаги 4-9, по шаблонам): Python/JS файлы, Dependabot, LICENSE, .editorconfig, pre-commit, верификация. Можно повторно использовать для существующего репо (skip Phase A). - -## Содержание - -1. [Создание репозитория](#1-создание-репозитория) -2. [Настройки репозитория](#2-настройки-репозитория) -3. [Защита ветки main](#3-защита-ветки-main) -4. [Python-проект](#4-python-проект) -5. [JS/TS-проект](#5-jsts-проект) -6. [Dependabot](#6-dependabot) -7. [Общие файлы](#7-общие-файлы) -8. [Установка pre-commit](#8-установка-pre-commit) -9. [Чек-лист верификации](#9-чек-лист-верификации) - ---- - -## Phase A — GitHub remote - -> Шаги 1-3 выполняются один раз для нового репо. Требуют локального git-репо с initial commit. - -## 0. Prerequisite: git init + initial commit - -Перед `gh repo create --source=.` локальный каталог должен быть git-репо с хотя бы одним коммитом (`--source=.` пушит текущую ветку; без коммита — пустой репо). - -```bash -git init -echo "# " > README.md -git add README.md -git status # проверь staged set — только README.md, без лишнего -``` - -> `commit` tool НЕ делает `git add` — коммитит только уже staged файлы. Если -> в индексе лишнее (например `memory-save` stage'нул всё через `git add -A`) -> — не коммить: сначала `git restore --staged ` или не stage'и его -> изначально. Используй `git add <конкретные-пути>`, НЕ `git add -A`. - -Затем через `commit` tool (НЕ raw `git commit` — заблокирован deny): - -``` -commit({ message: "chore: initial commit" }) -``` - -Если bare-repo без initial commit — `gh repo create --source=.` создаст remote, но push будет пустым, а main branch не появится → branch protection упадёт. - ---- - -## 1. Создание репозитория - -```bash -gh repo create / --public --source=. --remote=origin --push -``` - -Или приватный (если internal): - -```bash -gh repo create / --private --source=. --remote=origin --push -``` - -После создания — настроить git auth: - -```bash -gh auth setup-git -``` - ---- - -## 2. Настройки репозитория - -Squash-only merge, auto-delete branch после merge: - -```bash -gh api repos// \ - --method PATCH \ - -f allow_squash_merge=true \ - -f allow_merge_commit=false \ - -f allow_rebase_merge=false \ - -f delete_branch_on_merge=true \ - -f squash_merge_commit_title=COMMIT_OR_PR_TITLE \ - -f squash_merge_commit_message=COMMIT_MESSAGES -``` - ---- - -## 3. Защита ветки main - -Требовать PR, требовать status checks (CI), linear history: - -```bash -gh api repos///rules/branches/main \ - --method POST \ - -F target=branch \ - -f enforcement=active \ - --input - <<'EOF' -{ - "conditions": { - "ref_name": { - "include": ["refs/heads/main"], - "exclude": [] - } - }, - "rules": [ - { - "type": "pull_request", - "parameters": { - "required_approving_review_count": 0, - "dismiss_stale_reviews_on_push": false, - "require_code_owner_review": false, - "require_last_push_approval": false, - "required_review_thread_resolution": false - } - }, - { - "type": "required_status_checks", - "parameters": { - "strict_required_status_checks": true, - "do_not_enforce_on_create": false, - "required_status_checks": [] - } - }, - { - "type": "deletion" - }, - { - "type": "non_fast_forward" - } - ] -} -EOF -``` - -`required_status_checks` заполняется именами CI-джобов после первого пуша (см. шаблоны CI ниже). Имена джобов: `lint`, `typecheck`, `test`, `complexity` (Python) или `check` (JS/TS). - ---- - -## Phase B — Project scaffolding - -> Шаги 4-9 — шаблоны файлов для Python или JS/TS проекта. Можно применять к существующему репо (skip Phase A). Не зависят от GitHub remote. - -## 4. Python-проект - -### Инструменты - -| Инструмент | Назначение | Конфиг в | -|---|---|---| -| **uv** | Package manager, virtual env | `pyproject.toml` (build + deps) | -| **ruff** | Linter + formatter (замена flake8/isort/black) | `pyproject.toml` `[tool.ruff]` | -| **mypy** | Строгая типизация | `pyproject.toml` `[tool.mypy]` | -| **pytest** + **pytest-cov** | Тесты + покрытие | `pyproject.toml` `[tool.pytest]` | -| **xenon** | Анализ сложности кода | CI workflow | -| **pre-commit** | Git hooks (ruff + mypy перед коммитом) | `.pre-commit-config.yaml` | -| **hatchling** | Build backend (wheel) | `pyproject.toml` `[build-system]` | - -### pyproject.toml - -> Заменить ``, ``, `/` на реальные значения. `additional_dependencies` в pre-commit — список runtime-зависимостей (для mypy). - -```toml -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[project] -name = "" -version = "0.1.0" -description = "" -readme = "README.md" -license = "MIT" -requires-python = ">=3.12" -authors = [{ name = "slaid098" }] -keywords = [] -classifiers = [ - "Development Status :: 4 - Beta", - "Environment :: Console", - "Intended Audience :: End Users/Desktop", - "License :: OSI Approved :: MIT License", - "Operating System :: Microsoft :: Windows", - "Operating System :: POSIX :: Linux", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", -] - -dependencies = [] - -[project.optional-dependencies] -dev = [ - "pytest>=8.0", - "pytest-cov>=5.0", - "pytest-timeout>=2.2", - "mypy>=1.10", - "ruff>=0.5", - "xenon>=0.9", - "pre-commit>=3.7", -] - -[project.urls] -Homepage = "https://github.com/slaid098/" -Repository = "https://github.com/slaid098/" -Issues = "https://github.com/slaid098//issues" -Changelog = "https://github.com/slaid098//blob/main/CHANGELOG.md" - -[tool.hatch.build.targets.wheel] -packages = ["src/"] - -# ── Ruff ────────────────────────────────────────────────────────────────── - -[tool.ruff] -target-version = "py312" -line-length = 100 -src = ["src", "tests"] - -[tool.ruff.lint] -select = [ - "E", "W", # pycodestyle - "F", # pyflakes - "I", # isort - "B", # bugbear - "UP", # pyupgrade - "SIM", # simplify - "C90", # mccabe complexity - "PL", # pylint - "RUF", # ruff-specific - "S", # bandit (security) - "TRY", # tryceratops (exception handling) - "LOG", # flake8-logging -] -ignore = [ - "S101", # assert in tests - "S311", # pseudo-random for non-crypto use - "RUF001", # ambiguous Cyrillic chars (we write in Russian) - "RUF002", # same for docstrings - "RUF003", # same for comments - "TRY003", # long messages outside exception class - "PLR2004", # magic values in tests - "S106", # hardcoded passwords in tests -] - -[tool.ruff.lint.mccabe] -max-complexity = 10 - -[tool.ruff.lint.pylint] -max-args = 5 -max-branches = 12 -max-returns = 5 -max-statements = 50 - -[tool.ruff.lint.per-file-ignores] -"tests/*" = ["S101", "PLR2004", "S106", "S603", "S607"] - -# ── mypy ────────────────────────────────────────────────────────────────── - -[tool.mypy] -python_version = "3.12" -strict = true -warn_return_any = true -disallow_untyped_defs = true -disallow_incomplete_defs = true -check_untyped_defs = true -no_implicit_optional = true -warn_redundant_casts = true -warn_unused_ignores = true - -# ── pytest ──────────────────────────────────────────────────────────────── - -[tool.pytest.ini_options] -addopts = "--cov= --cov-report=term-missing --cov-fail-under=90 --timeout=120" -testpaths = ["tests"] - -# ── coverage ────────────────────────────────────────────────────────────── - -[tool.coverage.run] -source = ["src/"] -branch = true - -[tool.coverage.report] -exclude_lines = [ - "pragma: no cover", - "if __name__ == .__main__.:", - "if TYPE_CHECKING:", -] -``` - -### .pre-commit-config.yaml - -```yaml -repos: - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.0 - hooks: - - id: ruff - args: [--fix] - - id: ruff-format - - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.18.2 - hooks: - - id: mypy - additional_dependencies: [] -``` - -`additional_dependencies` — список runtime-зависимостей проекта (из `[project.dependencies]`), чтобы mypy мог резолвить типы. - -### .gitignore (Python) - -```gitignore -# Python -__pycache__/ -*.py[cod] -*$py.class -*.egg-info/ -*.egg -build/ -dist/ -.eggs/ -*.spec - -# Virtual environments -.venv/ -venv/ - -# Environment / secrets -.env -*.env -!.env.template - -# Testing / quality caches -.pytest_cache/ -.coverage -htmlcov/ -.mypy_cache/ -.ruff_cache/ -``` - -### .github/workflows/ci.yml (Python) - -4 job'а: lint → typecheck → test (matrix) → complexity. - -```yaml -name: CI - -on: - pull_request: - branches: [main] - push: - branches: [main] - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v3 - - run: uv sync --extra dev - - run: uv run ruff check src/ tests/ - - run: uv run ruff format --check src/ tests/ - - typecheck: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v3 - - run: uv sync --extra dev - - run: uv run mypy src/ - - test: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python: ["3.12", "3.13", "3.14"] - steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v3 - - run: uv sync --extra dev --python ${{ matrix.python }} - - run: uv run pytest - - complexity: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v3 - - run: uv sync --extra dev - - run: uv run xenon --max-absolute B --max-modules A --max-average A src/ -``` - -### Структура проекта (Python) - -``` -/ -├── .github/ -│ ├── workflows/ -│ │ └── ci.yml -│ └── dependabot.yml -├── src/ -│ └── / -│ ├── __init__.py -│ └── py.typed -├── tests/ -│ └── __init__.py -├── .pre-commit-config.yaml -├── .gitignore -├── LICENSE -├── pyproject.toml -├── README.md -└── uv.lock -``` - ---- - -## 5. JS/TS-проект - -### Инструменты - -| Инструмент | Назначение | Конфиг в | -|---|---|---| -| **npm** | Package manager | `package.json` | -| **Biome** | Linter + formatter (замена ESLint/Prettier) | `biome.json` | -| **TypeScript** | Строгая типизация | `tsconfig.json` | -| **Vitest** + **@vitest/coverage-v8** | Тесты + покрытие | `vitest.config.ts` | -| **Knip** | Dead-code detection | `knip.json` | - -### package.json - -> Заменить ``, `` на реальные значения. `entry` в knip.json — точка входа (для tree-shaking анализа). - -```json -{ - "name": "", - "version": "0.1.0", - "private": true, - "type": "module", - "engines": { - "node": ">=22" - }, - "scripts": { - "dev": "", - "build": "", - "lint": "biome check", - "format": "biome format --write", - "typecheck": "tsc --noEmit", - "test": "vitest run", - "test:watch": "vitest", - "knip": "knip" - }, - "devDependencies": { - "@biomejs/biome": "^1.9.4", - "@types/node": "^22.10.0", - "@vitest/coverage-v8": "^3.0.0", - "happy-dom": "^20.10.0", - "knip": "^6.24.0", - "typescript": "^5.7.0", - "vitest": "^3.0.0" - } -} -``` - -### biome.json - -```json -{ - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": true - }, - "files": { - "ignoreUnknown": true, - "ignore": ["node_modules", "dist", "coverage"] - }, - "formatter": { - "enabled": true, - "indentStyle": "space", - "indentWidth": 2, - "lineWidth": 100, - "lineEnding": "lf" - }, - "javascript": { - "formatter": { - "quoteStyle": "double", - "semicolons": "always", - "trailingCommas": "all" - } - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "suspicious": { - "noExplicitAny": "error" - } - } - } -} -``` - -### knip.json - -```json -{ - "entry": ["src/index.ts"], - "project": ["src/**/*.ts", "src/**/*.tsx"], - "ignore": [] -} -``` - -### tsconfig.json - -```json -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "strict": true, - "noUncheckedIndexedAccess": true, - "noFallthroughCasesInSwitch": true, - "noImplicitOverride": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "types": ["node"] - }, - "include": ["src", "tests"], - "exclude": ["node_modules", "dist"] -} -``` - -### vitest.config.ts - -```typescript -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - environment: "node", - include: ["tests/**/*.test.ts"], - coverage: { - provider: "v8", - reporter: ["text", "html"], - thresholds: { - lines: 60, - functions: 60, - branches: 60, - statements: 60, - }, - exclude: [ - "tests/**", - "dist/**", - "vitest.config.ts", - ], - }, - }, -}); -``` - -### .gitignore (JS/TS) - -```gitignore -node_modules/ -dist/ -coverage/ -*.log -.DS_Store -.env -``` - -### .github/workflows/ci.yml (JS/TS) - -Single job: lint → typecheck → knip → test → build. - -```yaml -name: CI - -on: - pull_request: - branches: [main] - push: - branches: [main] - -jobs: - check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "22" - cache: "npm" - - run: npm ci - - name: Lint (Biome) - run: npm run lint - - name: Typecheck - run: npm run typecheck - - name: Knip - run: npm run knip - - name: Test (Vitest + Coverage) - run: npm run test - - name: Build - run: npm run build -``` - -### Структура проекта (JS/TS) - -``` -/ -├── .github/ -│ ├── workflows/ -│ │ └── ci.yml -│ └── dependabot.yml -├── src/ -│ └── index.ts -├── tests/ -├── .gitignore -├── biome.json -├── knip.json -├── package.json -├── package-lock.json -├── tsconfig.json -├── vitest.config.ts -├── LICENSE -└── README.md -``` - ---- - -## 6. Dependabot - -Автоматическое обновление зависимостей. Еженедельно, 5 PR max. - -### Python (uv/pip) - -```yaml -version: 2 -updates: - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: weekly - open-pull-requests-limit: 5 - - package-ecosystem: github-actions - directory: "/" - schedule: - interval: weekly - open-pull-requests-limit: 5 -``` - -### JS/TS (npm) - -```yaml -version: 2 -updates: - - package-ecosystem: npm - directory: / - schedule: - interval: weekly - open-pull-requests-limit: 5 - - package-ecosystem: github-actions - directory: / - schedule: - interval: weekly - open-pull-requests-limit: 5 -``` - ---- - -## 7. Общие файлы - -### LICENSE (MIT) - -``` -MIT License - -Copyright (c) 2026 slaid098 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -``` - -### .editorconfig - -```ini -root = true - -[*] -charset = utf-8 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true - -[*.{py,toml}] -indent_style = space -indent_size = 4 - -[*.{ts,tsx,js,jsx,json,yml,yaml,css,md}] -indent_style = space -indent_size = 2 -``` - ---- - -## 8. Установка pre-commit - -### Python - -```bash -uv sync --extra dev -uv run pre-commit install -uv run pre-commit run --all-files -``` - -### JS/TS - -Pre-commit hooks не используются. Все quality gates — в CI (lint → typecheck → knip → test → build). - ---- - -## 9. Чек-лист верификации - -- [ ] Репозиторий создан (`gh repo create`) -- [ ] `gh auth setup-git` выполнен (push/pull работает) -- [ ] Squash-only merge, delete branch on merge (Step 2) -- [ ] Branch protection на main (Step 3) -- [ ] CI пайплайн зелёный на первом PR -- [ ] Pre-commit hooks установлены (Python) / CI гоняет (JS/TS) -- [ ] Dependabot включён (Settings → Code security → Dependabot) -- [ ] LICENSE, .gitignore, .editorconfig в репозитории -- [ ] `uv.lock` / `package-lock.json` закоммичен diff --git a/.opencode/skills/repo-readme/SKILL.md b/.opencode/skills/repo-readme/SKILL.md index 586cd10..0a97ce4 100644 --- a/.opencode/skills/repo-readme/SKILL.md +++ b/.opencode/skills/repo-readme/SKILL.md @@ -196,14 +196,14 @@ switcher `[English]` / `[Русский]`, заголовок `## 🇷🇺 Ру `quick_start_steps_*` не влияют на валидацию — они рендерятся вне delimiter-пар (summary/features). -## 6. Независимость от repo-init +## 6. Независимость от project-template -- Скилл `repo-init` создаёт **пустой** `README.md` как часть инициализации - репо. +- Скилл `project-template` (init flow) создаёт проект через cookiecutter — + шаблон уже включает `README.md` с базовой структурой. - `repo-readme` (через тулзу `create-readme`) **наполняет** его - стандартизированным контентом. -- Может применяться к существующим репо без `repo-init` — тулза перезапишет - `README.md` (локально) или обновит через GitHub API (с SHA). + стандартизированным контентом (delimiter tags, bilingual, cover). +- Может применяться к существующим репо без `project-template` — тулза + перезапишет `README.md` (локально) или обновит через GitHub API (с SHA). ## 7. Параметры тулзы (кратко) diff --git a/.opencode/skills/spec/SKILL.md b/.opencode/skills/spec/SKILL.md index ccc3707..d3281b6 100644 --- a/.opencode/skills/spec/SKILL.md +++ b/.opencode/skills/spec/SKILL.md @@ -160,7 +160,7 @@ Prompt template F (см. ниже). ``` Дефолтный roadmap (можешь править): -1. scaffolding — repo structure, CI, .gitignore, LICENSE (через repo-init skill) +1. scaffolding — repo structure, CI, .gitignore, LICENSE (через project-template skill init flow) 2. core: — ... 3. core: — ... 4. auth (если выбран auth в Phase 2) @@ -316,7 +316,7 @@ Default stack для типа (хардкод, добавить всегда): ## Критерии приемки (как проверяем) ## Связанные ресурсы (Part of spec, ref к docs/spec/roadmap.md) - Issue #1 (scaffolding) body ДОЛЖЕН включать: - "Используй repo-init skill для: pyproject.toml, CI, .gitignore, LICENSE, dependabot, pre-commit. Структура — из ## Структура в docs/spec/modules.md." + "Используй project-template skill init flow для: cookiecutter по типу проекта (pyproject.toml, CI, .gitignore, LICENSE, dependabot, pre-commit). Структура — из ## Структура в docs/spec/modules.md." - `create-issue({ title: "type(scope): description", body: "", labels: ["enhancement", "from-spec"] })` tool (НЕ raw `gh issue create` — заблокирован deny) 4. Собери реальные номера issues из вывода tool. 5. Update docs/spec/roadmap.md: добавь реальные #N номера. Update docs/spec/meta.md: executed=true, phase=8. diff --git a/tests/test_project_template_skill.py b/tests/test_project_template_skill.py new file mode 100644 index 0000000..989491f --- /dev/null +++ b/tests/test_project_template_skill.py @@ -0,0 +1,203 @@ +"""Structural tests for the project-template skill and command. + +Validates acceptance criteria from issue #230: +- SKILL.md exists with init + check flow +- command exists with agent: build +- Phase A (GitHub remote + branch protection) migrated from repo-init +- Phase B replaced by cookiecutter +- repo-init skill removed +- references to repo-init updated in spec/SKILL.md, add-skill/SKILL.md, + repo-readme/SKILL.md +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parent.parent +SKILLS_DIR = REPO_ROOT / ".opencode" / "skills" +COMMANDS_DIR = REPO_ROOT / ".opencode" / "commands" + +SKILL_PATH = SKILLS_DIR / "project-template" / "SKILL.md" +COMMAND_PATH = COMMANDS_DIR / "project-template.md" +REPO_INIT_PATH = SKILLS_DIR / "repo-init" / "SKILL.md" + + +def test_skill_file_exists(): + """SKILL.md must exist at the canonical path.""" + assert SKILL_PATH.is_file(), f"missing: {SKILL_PATH}" + + +def test_command_file_exists(): + """Command file must exist at the canonical path.""" + assert COMMAND_PATH.is_file(), f"missing: {COMMAND_PATH}" + + +def test_repo_init_removed(): + """repo-init skill must be removed (acceptance criterion).""" + assert not REPO_INIT_PATH.exists(), f"repo-init still present: {REPO_INIT_PATH}" + assert not (SKILLS_DIR / "repo-init").exists(), "repo-init/ dir still present" + + +def test_skill_frontmatter(): + """SKILL.md frontmatter has name + description.""" + text = SKILL_PATH.read_text(encoding="utf-8") + assert text.startswith("---\n"), "missing frontmatter opening" + end = text.find("\n---\n", 4) + assert end != -1, "missing frontmatter closing" + fm = text[4:end] + assert "name: project-template" in fm, "frontmatter name missing" + assert "description:" in fm, "frontmatter description missing" + + +def test_skill_has_init_and_check_flows(): + """SKILL.md documents both init and check flows.""" + text = SKILL_PATH.read_text(encoding="utf-8") + assert "## init flow" in text, "init flow section missing" + assert "## check flow" in text, "check flow section missing" + + +def test_skill_init_flow_steps(): + """init flow references cookiecutter, git, gh repo create, branch protection, project-status.""" + text = SKILL_PATH.read_text(encoding="utf-8") + assert "cookiecutter" in text, "cookiecutter not referenced" + assert "git init" in text, "git init not referenced" + assert "gh repo create" in text, "gh repo create not referenced" + assert "branch protection" in text.lower() or "Защита ветки" in text, ( + "branch protection missing" + ) + assert "project-status" in text, "project-status not referenced" + + +def test_skill_check_flow_steps(): + """check flow references project-status tool + recommendations + subagents.""" + text = SKILL_PATH.read_text(encoding="utf-8") + assert "project-status({})" in text, "project-status tool call missing" + assert "Рекомендации" in text or "Рекомендации:" in text, "recommendations section missing" + assert "subagent" in text.lower(), "subagent delegation missing" + + +def test_skill_phase_a_migrated(): + """Phase A (squash-only merge, branch protection rules) migrated verbatim.""" + text = SKILL_PATH.read_text(encoding="utf-8") + assert "allow_squash_merge=true" in text, "squash merge setting missing" + assert "allow_merge_commit=false" in text, "merge commit setting missing" + assert "allow_rebase_merge=false" in text, "rebase merge setting missing" + assert "delete_branch_on_merge=true" in text, "delete branch on merge missing" + assert "squash_merge_commit_title=COMMIT_OR_PR_TITLE" in text, "squash title missing" + assert "required_status_checks" in text, "required_status_checks rule missing" + assert "non_fast_forward" in text, "non_fast_forward rule missing" + assert "refs/heads/main" in text, "main branch ref missing" + + +def test_skill_phase_b_cookiecutter(): + """Phase B replaced by cookiecutter (no manual pyproject/biome templates).""" + text = SKILL_PATH.read_text(encoding="utf-8") + assert ".opencode/templates/" in text, "templates dir not referenced" + assert "cookiecutter.json" in text, "cookiecutter.json not referenced" + assert "use_auth" in text, "use_auth variable missing" + assert "use_db" in text, "use_db variable missing" + assert "post_gen_project" in text, "post_gen_project hook not referenced" + + +def test_skill_boundary_cases(): + """Boundary cases documented. + + existing repo, GitHub repo exists, cookiecutter missing, project-status missing. + """ + text = SKILL_PATH.read_text(encoding="utf-8") + assert "Существующий репо" in text or "существующий репо" in text.lower(), ( + "existing repo case missing" + ) + assert "GitHub repo уже существует" in text or "gh repo view" in text, ( + "existing GitHub repo case missing" + ) + assert "cookiecutter не установлен" in text, "cookiecutter missing case missing" + assert "uv tool install cookiecutter" in text, "cookiecutter install instruction missing" + assert "project-status не найден" in text or "project-status tool не доступен" in text, ( + "project-status missing case missing" + ) + + +def test_skill_delegates_not_edits(): + """Skill delegates to subagents (Template INIT/GITHUB/FIX), main agent = orchestrator.""" + text = SKILL_PATH.read_text(encoding="utf-8") + assert "Template INIT" in text, "Template INIT missing" + assert "Template GITHUB" in text, "Template GITHUB missing" + assert "Template FIX" in text, "Template FIX missing" + assert "оркестратор" in text, "orchestrator role not stated" + assert "subagent" in text.lower(), "subagent delegation not mentioned" + + +def test_skill_valid_types_reference(): + """SKILL.md references VALID_TYPES from spec-status.py.""" + text = SKILL_PATH.read_text(encoding="utf-8") + for ptype in ("backend", "fullstack", "mcp-server", "cli", "bot", "worker"): + assert ptype in text, f"project type {ptype} not mentioned" + + +def test_skill_cookiecutter_types_documented(): + """SKILL.md documents which types have cookiecutter templates (backend, fullstack, cli).""" + text = SKILL_PATH.read_text(encoding="utf-8") + templated = ["backend", "fullstack", "cli"] + non_templated = ["mcp-server", "bot", "worker"] + for ptype in templated: + assert ptype in text, f"templated type {ptype} missing" + for ptype in non_templated: + assert ptype in text, f"non-templated type {ptype} missing" + assert "cookiecutter template" in text.lower() or "cookiecutter templates" in text.lower(), ( + "cookiecutter template availability not documented" + ) + + +def test_command_agent_build(): + """Command file has agent: build frontmatter.""" + text = COMMAND_PATH.read_text(encoding="utf-8") + assert "agent: build" in text, "agent: build missing in command" + assert 'skill({name: "project-template"})' in text, "skill load instruction missing" + + +def test_command_no_repo_init_reference(): + """Command file must not reference repo-init.""" + text = COMMAND_PATH.read_text(encoding="utf-8") + assert "repo-init" not in text, "command still references repo-init" + + +@pytest.mark.parametrize( + "rel_path", + [ + ".opencode/skills/spec/SKILL.md", + ".opencode/skills/add-skill/SKILL.md", + ".opencode/skills/repo-readme/SKILL.md", + ], +) +def test_no_repo_init_references_in_skills(rel_path): + """No stale repo-init references in dependent skills (handoff/decisions docs excluded).""" + path = REPO_ROOT / rel_path + assert path.is_file(), f"missing: {rel_path}" + text = path.read_text(encoding="utf-8") + assert "repo-init" not in text, f"stale repo-init reference in {rel_path}" + + +def test_spec_skill_references_project_template(): + """spec/SKILL.md should reference project-template (not repo-init) for scaffolding.""" + text = (REPO_ROOT / ".opencode" / "skills" / "spec" / "SKILL.md").read_text(encoding="utf-8") + assert "project-template" in text, "spec/SKILL.md does not reference project-template" + + +def test_add_skill_tree_lists_project_template(): + """add-skill/SKILL.md skill tree should list project-template (not repo-init).""" + text = (REPO_ROOT / ".opencode" / "skills" / "add-skill" / "SKILL.md").read_text( + encoding="utf-8" + ) + assert "project-template/SKILL.md" in text, "add-skill tree missing project-template" + + +def test_repo_readme_references_project_template(): + """repo-readme/SKILL.md section 6 should reference project-template (not repo-init).""" + text = (REPO_ROOT / ".opencode" / "skills" / "repo-readme" / "SKILL.md").read_text( + encoding="utf-8" + ) + assert "project-template" in text, "repo-readme missing project-template reference"