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 <agent@opencode.local>
This commit is contained in:
Sergey 2026-08-03 17:37:55 +03:00 committed by GitHub
parent 51a5300423
commit b3aac737eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 557 additions and 768 deletions

View file

@ -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 напрямую.

View file

@ -49,7 +49,7 @@ description: <when to load this skill, in English. Example: Use when ... Also wh
├── memory/SKILL.md
├── python-development/SKILL.md
├── release/SKILL.md
├── repo-init/SKILL.md
├── project-template/SKILL.md
├── run-pipeline/SKILL.md
├── run-tests/SKILL.md
├── spec/SKILL.md

View file

@ -0,0 +1,340 @@
---
name: project-template
description: Init new project from cookiecutter template by type + GitHub remote + branch protection, or audit existing project via project-status. Replaces repo-init. Also when user says "новый проект", "инициализируй репо", "проверь проект", "create project", "project template".
---
# Project Template
Два flow: **init** (новый проект: cookiecutter по типу + GitHub remote) и
**check** (существующий проект: project-status аудит с рекомендациями).
Замена монолитному `repo-init`: Phase A (GitHub remote + branch protection)
мигрирована без изменений, Phase B (ручное scaffolding) заменена на cookiecutter
шаблоны из `.opencode/templates/<type>/` (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 строка прогресса юзеру (формат: `✅ <step> — <done>`).
### ЗАПРЕЩЕНО
- Запускать 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 `<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 рендерит проект в
`./<project_name>/`, post_gen_project hook удаляет файлы условные на
`use_auth`/`use_db`. Затем git init + initial commit внутри `./<project_name>/`.
### Шаг 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: <type>`, 7 групп `[OK]/[WARN]/[FAIL]`, `Итог:`,
`Рекомендации:`. Покажи отчёт юзеру. Если tool вернул `⚠️ ...failed` → WARN,
продолжай без отчёта.
### Шаг 6: Финальный репорт
```
Project <project_name> created at ./<project_name>/.
GitHub: https://github.com/<owner>/<project_name>
Branch protection: main (PR + required_status_checks + linear history)
Project-status: <summary из шага 5>
Дальше: /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: Вопрос — чинить?
```
Найдены проблемы: <N FAIL, M WARN>.
Запустить 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 создаст `./<project_name>/` рядом (не
перезапишет текущий репо). Уточни: "cwd уже git-репо с файлами. Init создаст
новый проект в подкаталоге `./<project_name>/`. Продолжить? [1] да / [2] нет,
лучше check flow".
- **GitHub repo уже существует** → skip `gh repo create`, только branch
protection (если ещё не настроена). Subagent проверяет: `gh repo view
<owner>/<name>` — если существует, пропускает 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)
```
Создай новый проект типа <type> с именем <project_name>.
Контекст: init flow project-template skill, cwd = <cwd>.
1. Проверь cookiecutter: `cookiecutter --version`. Если не установлен → STOP,
верни: "cookiecutter не установлен. Установи: `uv tool install cookiecutter`".
2. Запусти cookiecutter (no-input, переменные из ответов юзера):
`cookiecutter .opencode/templates/<type>/ --no-input \
project_name=<project_name> \
project_type=<type> \
description="<description>" \
use_auth=<auth> \
use_db=<db> \
python_version=3.13`
Cookiecutter создаст каталог `./<project_name>/` с рендеренным проектом.
post_gen_project hook удалит файлы условные на use_auth/use_db.
3. `cd <project_name>` (все дальнейшие команды — внутри этого каталога).
4. `git init`
5. `git add .` затем `git status` — проверь staged set (только файлы проекта,
без лишнего). Если лишнее — `git restore --staged <file>`.
6. `commit({ message: "chore: initial commit" })` tool (НЕ raw `git commit`
заблокирован deny).
7. Верни: "done: project created at ./<project_name>/, 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 для проекта <project_name> (cwd = <cwd>/<project_name>).
owner = <owner>, visibility = public (или private если internal).
0. Проверь: GitHub repo уже существует?
`gh repo view <owner>/<project_name>` — если exit 0, skip step 1 (create),
переходи к step 2 (settings) и step 3 (branch protection).
1. Создание репозитория:
`gh repo create <owner>/<project_name> --public --source=. --remote=origin --push`
(или --private если internal)
После создания: `gh auth setup-git`
2. Настройки репозитория (squash-only merge, auto-delete branch):
`gh api repos/<owner>/<project_name> \
--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/<owner>/<project_name>/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 отчёта.
Категория: <category> (Структура / Качество кода / Тесты / README / Infra / Coverage)
Проблема: <name>: <detail>
Путь: <path из рекомендации>
1. Прочитай контекст проблемы (файл по пути из рекомендации).
2. Минимальный фикс: добавь/исправь только то, что указано в рекомендации.
Не рефактори unrelated код.
3. Перед коммитом — `git status` для проверки staged set (`commit` tool НЕ
делает `git add` — коммитит только staged; используй
`git add <конкретные-пути>`, НЕ `git add -A`).
4. `commit({ message: "fix(<scope>): <description>" })` tool (НЕ raw
`git commit`), push.
5. Верни: "done: fixed <name>, commit <hash>".
Если найдёшь баг вне 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/<type>/`).
## 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`.

View file

@ -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 "# <repo-name>" > 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 <file>` или не 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 <owner>/<repo-name> --public --source=. --remote=origin --push
```
Или приватный (если internal):
```bash
gh repo create <owner>/<repo-name> --private --source=. --remote=origin --push
```
После создания — настроить git auth:
```bash
gh auth setup-git
```
---
## 2. Настройки репозитория
Squash-only merge, auto-delete branch после merge:
```bash
gh api repos/<owner>/<repo-name> \
--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/<owner>/<repo-name>/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
> Заменить `<package-name>`, `<description>`, `<owner>/<repo>` на реальные значения. `additional_dependencies` в pre-commit — список runtime-зависимостей (для mypy).
```toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "<package-name>"
version = "0.1.0"
description = "<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/<repo>"
Repository = "https://github.com/slaid098/<repo>"
Issues = "https://github.com/slaid098/<repo>/issues"
Changelog = "https://github.com/slaid098/<repo>/blob/main/CHANGELOG.md"
[tool.hatch.build.targets.wheel]
packages = ["src/<package_name>"]
# ── 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=<package_name> --cov-report=term-missing --cov-fail-under=90 --timeout=120"
testpaths = ["tests"]
# ── coverage ──────────────────────────────────────────────────────────────
[tool.coverage.run]
source = ["src/<package_name>"]
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)
```
<repo>/
├── .github/
│ ├── workflows/
│ │ └── ci.yml
│ └── dependabot.yml
├── src/
│ └── <package_name>/
│ ├── __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
> Заменить `<name>`, `<description>` на реальные значения. `entry` в knip.json — точка входа (для tree-shaking анализа).
```json
{
"name": "<name>",
"version": "0.1.0",
"private": true,
"type": "module",
"engines": {
"node": ">=22"
},
"scripts": {
"dev": "<dev-command>",
"build": "<build-command>",
"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)
```
<repo>/
├── .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` закоммичен

View file

@ -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. Параметры тулзы (кратко)

View file

@ -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: <module 1> — ...
3. core: <module 2> — ...
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: "<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.

View file

@ -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"