feat(skills): add repo-readme skill and create-readme tool (#112)
* feat(tools): add create-readme tool * feat(skills): add repo-readme skill * docs(handoff): add handoff and ADR * docs(handoff): set PR number * docs(project-map): update after structural changes --------- Co-authored-by: opencode-agent <agent@opencode.local>
This commit is contained in:
parent
d8940de4f6
commit
7ea12b982d
5 changed files with 578 additions and 0 deletions
162
.opencode/skills/repo-readme/SKILL.md
Normal file
162
.opencode/skills/repo-readme/SKILL.md
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
---
|
||||||
|
name: repo-readme
|
||||||
|
description: Use when creating or updating README.md for any slaid098 repository. Calls create-readme tool for deterministic structure with delimiter tags for slaid098.dev showcase. Also when user says "оформи README", "обнови описание репо", "readme template", "базовая структура readme".
|
||||||
|
---
|
||||||
|
|
||||||
|
# Repo README
|
||||||
|
|
||||||
|
Стандартизация `README.md` во всех репозиториях slaid098 через тулзу
|
||||||
|
`create-readme`. Тулза гарантирует структуру (разделители, Support block,
|
||||||
|
Quick Start, language switcher). Скилл даёт контекст: когда вызывать тулзу и
|
||||||
|
какие параметры передавать.
|
||||||
|
|
||||||
|
## 1. Когда использовать тулзу
|
||||||
|
|
||||||
|
- **Новый репо** → `create-readme` (mode: `create`) — генерирует
|
||||||
|
стандартизированный README с нуля.
|
||||||
|
- **Проверка существующего README** → `create-readme` (mode: `validate`) —
|
||||||
|
проверяет, что структура соответствует стандарту витрины.
|
||||||
|
- **После ручных правок README** → всегда `validate`. Любая правка руками
|
||||||
|
агента (через Edit/Write) может нарушить разделители — после правок
|
||||||
|
обязательна валидация.
|
||||||
|
|
||||||
|
Не генерируй README вручную через Write — структура критична для парсинга
|
||||||
|
витриной. Только через тулзу `create-readme`.
|
||||||
|
|
||||||
|
## 2. Tier system
|
||||||
|
|
||||||
|
Тулза принимает параметр `tier`:
|
||||||
|
|
||||||
|
- **`flagship`** — флагманские проекты (Anti-Detect MCP, Mobile Whisper и
|
||||||
|
т.п.). Полный набор параметров: `demo_gif` (демо-гифка), `custom_sections`,
|
||||||
|
`telegram`. Используется для заметных проектов на витрине.
|
||||||
|
- **`utility`** (default) — бытовые утилиты. Минимальный набор: `problem` /
|
||||||
|
`solution` (EN+RU), `quick_start`. `demo_gif` игнорируется.
|
||||||
|
|
||||||
|
Выбор tier влияет только на блок `demo_gif` после EN-разделителей. Остальная
|
||||||
|
структура одинакова для обоих tier'ов.
|
||||||
|
|
||||||
|
## 3. GitHub metadata (отдельный шаг, НЕ в тулзе)
|
||||||
|
|
||||||
|
`create-readme` отвечает только за файл `README.md`. Метаданные репозитория
|
||||||
|
настраиваются отдельно через `gh repo edit` (это bash, не тулза):
|
||||||
|
|
||||||
|
- Описание: `gh repo edit --description "короткое описание"`
|
||||||
|
- Topics для поиска: `gh repo edit --add-topic topic1 --add-topic topic2`
|
||||||
|
- Social preview image — через настройки GitHub UI (не CLI).
|
||||||
|
|
||||||
|
Метаданные не дублируют README — они для карточки репо на GitHub и поиска.
|
||||||
|
|
||||||
|
## 4. Workflow
|
||||||
|
|
||||||
|
1. `create-readme` (mode: `create`) → генерирует README с гарантированной
|
||||||
|
структурой.
|
||||||
|
2. Ручные правки если нужно (агент редактирует файл напрямую через Edit) —
|
||||||
|
например, расширить `custom_sections`, поправить формулировки.
|
||||||
|
3. `create-readme` (mode: `validate`) → проверяет, что структура не нарушена.
|
||||||
|
4. Если `validate` fails → фикс нарушения → re-`validate`. Цикл пока не
|
||||||
|
пройдёт.
|
||||||
|
|
||||||
|
Локальный режим (по умолчанию): тулза пишет в `file_path` (default
|
||||||
|
`README.md`) через `fs.writeFileSync`. Удалённый режим: передай `repo`
|
||||||
|
(`owner/name`) — тулза сделает PUT через `gh api
|
||||||
|
repos/{owner}/{repo}/contents/README.md` с base64-контентом и SHA.
|
||||||
|
|
||||||
|
## 5. Зачем разделители (контекст для агента)
|
||||||
|
|
||||||
|
Витрина **slaid098.dev** скачивает raw `README.md` из каждого репо и
|
||||||
|
извлекает фрагмент между разделителями:
|
||||||
|
|
||||||
|
- `<!-- summary-en:start -->` ... `<!-- summary-en:end -->` — английский
|
||||||
|
блок для карточки.
|
||||||
|
- `<!-- summary-ru:start -->` ... `<!-- summary-ru:end -->` — русский блок.
|
||||||
|
|
||||||
|
Поэтому структура README **должна быть гарантирована тулзой**, а не агентом.
|
||||||
|
Агент может менять текст *внутри* разделителей, но не должен удалять/двигать
|
||||||
|
сами разделители. `validate` ловит такие нарушения.
|
||||||
|
|
||||||
|
## 6. Шаблон README (reference)
|
||||||
|
|
||||||
|
Тулза `create-readme` генерирует ровно эту структуру (параметры подставляются):
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# 🚀 {repo_name}
|
||||||
|
> {tagline}
|
||||||
|
|
||||||
|
[English](#-english) | [Русский](#-русская-версия)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🇬🇧 English
|
||||||
|
|
||||||
|
<!-- summary-en:start -->
|
||||||
|
### 🔴 Problem
|
||||||
|
{problem_en}
|
||||||
|
|
||||||
|
### 🟢 Solution
|
||||||
|
{solution_en}
|
||||||
|
{custom_sections_en — доп. секции, если переданы}
|
||||||
|
<!-- summary-en:end -->
|
||||||
|
{demo_gif строка, только если tier=flagship}
|
||||||
|
|
||||||
|
### ⚡ Quick Start
|
||||||
|
\`\`\`bash
|
||||||
|
git clone https://github.com/slaid098/{repo_name}.git
|
||||||
|
{quick_start}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🇷🇺 Русская версия
|
||||||
|
|
||||||
|
<!-- summary-ru:start -->
|
||||||
|
### 🔴 Проблема
|
||||||
|
{problem_ru}
|
||||||
|
|
||||||
|
### 🟢 Решение
|
||||||
|
{solution_ru}
|
||||||
|
{custom_sections_ru — доп. секции, если переданы}
|
||||||
|
<!-- summary-ru:end -->
|
||||||
|
|
||||||
|
### ⚡ Быстрый старт
|
||||||
|
\`\`\`bash
|
||||||
|
git clone https://github.com/slaid098/{repo_name}.git
|
||||||
|
{quick_start}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💬 Support & Contact / Поддержка и связь
|
||||||
|
|
||||||
|
Have questions, need custom features, or want to support this project?
|
||||||
|
👉 **[Visit Support & Contact Page](https://slaid098.dev/support)**
|
||||||
|
{telegram строка, если передан}
|
||||||
|
```
|
||||||
|
|
||||||
|
`validate` проверяет: наличие обоих EN/RU разделителей, непустой контент
|
||||||
|
между ними, ссылку `slaid098.dev/support`, секции Quick Start (EN) и Быстрый
|
||||||
|
старт (RU), language switcher `[English]` / `[Русский]`.
|
||||||
|
|
||||||
|
## 7. Независимость от repo-init
|
||||||
|
|
||||||
|
- Скилл `repo-init` создаёт **пустой** `README.md` как часть инициализации
|
||||||
|
репо.
|
||||||
|
- `repo-readme` (через тулзу `create-readme`) **наполняет** его
|
||||||
|
стандартизированным контентом.
|
||||||
|
- Может применяться к существующим репо без `repo-init` — тулза перезапишет
|
||||||
|
`README.md` (локально) или обновит через GitHub API (с SHA).
|
||||||
|
|
||||||
|
## 8. Параметры тулзы (кратко)
|
||||||
|
|
||||||
|
`create-readme`:
|
||||||
|
|
||||||
|
- `mode` — `"create"` | `"validate"` (обязательный).
|
||||||
|
- `repo_name`, `tagline`, `problem_en`, `solution_en`, `problem_ru`,
|
||||||
|
`solution_ru`, `quick_start` — обязательны для `create`.
|
||||||
|
- `tier` — `"flagship"` | `"utility"` (default `utility`).
|
||||||
|
- `telegram` — username без `@` (optional).
|
||||||
|
- `demo_gif` — путь/URL (только для `flagship`).
|
||||||
|
- `custom_sections_en` / `custom_sections_ru` — массивы
|
||||||
|
`{ title, content }` (optional).
|
||||||
|
- `repo` — `owner/name` для удалённой операции (optional).
|
||||||
|
- `file_path` — локальный путь (default `README.md`).
|
||||||
305
.opencode/tools/create-readme.ts
Normal file
305
.opencode/tools/create-readme.ts
Normal file
|
|
@ -0,0 +1,305 @@
|
||||||
|
import { spawnSync } from "child_process"
|
||||||
|
import { readFileSync, writeFileSync } from "fs"
|
||||||
|
import { tool } from "@opencode-ai/plugin"
|
||||||
|
|
||||||
|
type CustomSection = { title: string; content: string }
|
||||||
|
|
||||||
|
type CreateArgs = {
|
||||||
|
repo_name: string
|
||||||
|
tagline: string
|
||||||
|
problem_en: string
|
||||||
|
solution_en: string
|
||||||
|
problem_ru: string
|
||||||
|
solution_ru: string
|
||||||
|
quick_start: string
|
||||||
|
tier?: "flagship" | "utility"
|
||||||
|
telegram?: string
|
||||||
|
demo_gif?: string
|
||||||
|
custom_sections_en?: CustomSection[]
|
||||||
|
custom_sections_ru?: CustomSection[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractBetween(text: string, start: string, end: string): string | null {
|
||||||
|
const s = text.indexOf(start)
|
||||||
|
const e = text.indexOf(end)
|
||||||
|
if (s === -1 || e === -1 || e <= s) return null
|
||||||
|
return text.substring(s + start.length, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateReadme(args: CreateArgs): string {
|
||||||
|
const customEn = (args.custom_sections_en || [])
|
||||||
|
.map((s) => `\n\n### ${s.title}\n${s.content}`)
|
||||||
|
.join("")
|
||||||
|
const customRu = (args.custom_sections_ru || [])
|
||||||
|
.map((s) => `\n\n### ${s.title}\n${s.content}`)
|
||||||
|
.join("")
|
||||||
|
const demoLine =
|
||||||
|
args.tier === "flagship" && args.demo_gif ? `\n\n` : ""
|
||||||
|
const telegramLine = args.telegram
|
||||||
|
? `💬 **Direct Telegram:** [@${args.telegram}](https://t.me/${args.telegram})`
|
||||||
|
: ""
|
||||||
|
|
||||||
|
return `# 🚀 ${args.repo_name}
|
||||||
|
> ${args.tagline}
|
||||||
|
|
||||||
|
[English](#-english) | [Русский](#-русская-версия)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🇬🇧 English
|
||||||
|
|
||||||
|
<!-- summary-en:start -->
|
||||||
|
### 🔴 Problem
|
||||||
|
${args.problem_en}
|
||||||
|
|
||||||
|
### 🟢 Solution
|
||||||
|
${args.solution_en}${customEn}
|
||||||
|
<!-- summary-en:end -->
|
||||||
|
${demoLine}
|
||||||
|
### ⚡ Quick Start
|
||||||
|
\`\`\`bash
|
||||||
|
git clone https://github.com/slaid098/${args.repo_name}.git
|
||||||
|
${args.quick_start}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🇷🇺 Русская версия
|
||||||
|
|
||||||
|
<!-- summary-ru:start -->
|
||||||
|
### 🔴 Проблема
|
||||||
|
${args.problem_ru}
|
||||||
|
|
||||||
|
### 🟢 Решение
|
||||||
|
${args.solution_ru}${customRu}
|
||||||
|
<!-- summary-ru:end -->
|
||||||
|
|
||||||
|
### ⚡ Быстрый старт
|
||||||
|
\`\`\`bash
|
||||||
|
git clone https://github.com/slaid098/${args.repo_name}.git
|
||||||
|
${args.quick_start}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💬 Support & Contact / Поддержка и связь
|
||||||
|
|
||||||
|
Have questions, need custom features, or want to support this project?
|
||||||
|
👉 **[Visit Support & Contact Page](https://slaid098.dev/support)**
|
||||||
|
${telegramLine}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateReadme(content: string): { ok: boolean; issues: string[] } {
|
||||||
|
const issues: string[] = []
|
||||||
|
|
||||||
|
if (!content.includes("<!-- summary-en:start -->"))
|
||||||
|
issues.push("Missing <!-- summary-en:start --> delimiter")
|
||||||
|
if (!content.includes("<!-- summary-en:end -->"))
|
||||||
|
issues.push("Missing <!-- summary-en:end --> delimiter")
|
||||||
|
if (!content.includes("<!-- summary-ru:start -->"))
|
||||||
|
issues.push("Missing <!-- summary-ru:start --> delimiter")
|
||||||
|
if (!content.includes("<!-- summary-ru:end -->"))
|
||||||
|
issues.push("Missing <!-- summary-ru:end --> delimiter")
|
||||||
|
|
||||||
|
const enContent = extractBetween(
|
||||||
|
content,
|
||||||
|
"<!-- summary-en:start -->",
|
||||||
|
"<!-- summary-en:end -->",
|
||||||
|
)
|
||||||
|
if (enContent !== null && !enContent.trim())
|
||||||
|
issues.push("EN summary content between delimiters is empty")
|
||||||
|
const ruContent = extractBetween(
|
||||||
|
content,
|
||||||
|
"<!-- summary-ru:start -->",
|
||||||
|
"<!-- summary-ru:end -->",
|
||||||
|
)
|
||||||
|
if (ruContent !== null && !ruContent.trim())
|
||||||
|
issues.push("RU summary content between delimiters is empty")
|
||||||
|
|
||||||
|
if (!content.includes("slaid098.dev/support"))
|
||||||
|
issues.push("Missing Support & Contact link (slaid098.dev/support)")
|
||||||
|
if (!content.includes("Quick Start"))
|
||||||
|
issues.push("Missing 'Quick Start' section (English)")
|
||||||
|
if (!content.includes("Быстрый старт"))
|
||||||
|
issues.push("Missing 'Быстрый старт' section (Russian)")
|
||||||
|
if (!content.includes("[English]"))
|
||||||
|
issues.push("Missing [English] language switcher link")
|
||||||
|
if (!content.includes("[Русский]"))
|
||||||
|
issues.push("Missing [Русский] language switcher link")
|
||||||
|
|
||||||
|
return { ok: issues.length === 0, issues }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default tool({
|
||||||
|
description:
|
||||||
|
"Create or validate README.md for slaid098 repositories. 'create' mode generates a standardized bilingual README with delimiter tags (<!-- summary-en:start/end -->, <!-- summary-ru:start/end -->) parsed by the slaid098.dev showcase. 'validate' mode checks an existing README against the standard. Supports local file (fs) and remote (gh api repos/{owner}/{repo}/contents/README.md) operation.",
|
||||||
|
args: {
|
||||||
|
mode: tool.schema
|
||||||
|
.enum(["create", "validate"])
|
||||||
|
.describe("Operation mode: 'create' generates README, 'validate' checks existing README structure"),
|
||||||
|
repo_name: tool.schema
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Repository name (e.g. 'anti-detect-mcp'). Required for create mode."),
|
||||||
|
tagline: tool.schema
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Short English tagline (1 sentence). Required for create mode."),
|
||||||
|
problem_en: tool.schema
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Problem statement in English (1-2 sentences). Required for create mode."),
|
||||||
|
solution_en: tool.schema
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Solution in English (1-2 sentences). Required for create mode."),
|
||||||
|
problem_ru: tool.schema
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Problem statement in Russian (1-2 sentences). Required for create mode."),
|
||||||
|
solution_ru: tool.schema
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Solution in Russian (1-2 sentences). Required for create mode."),
|
||||||
|
quick_start: tool.schema
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Install/setup command (e.g. 'pip install -r requirements.txt'). Required for create mode."),
|
||||||
|
tier: tool.schema
|
||||||
|
.enum(["flagship", "utility"])
|
||||||
|
.optional()
|
||||||
|
.describe("Repository tier: 'flagship' (enables demo_gif) or 'utility' (minimal). Default: 'utility'."),
|
||||||
|
telegram: tool.schema
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Telegram username without @. Optional; adds Direct Telegram line to Support section."),
|
||||||
|
demo_gif: tool.schema
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Path or URL to demo GIF. Only emitted when tier='flagship'."),
|
||||||
|
custom_sections_en: tool.schema
|
||||||
|
.array(
|
||||||
|
tool.schema.object({
|
||||||
|
title: tool.schema.string(),
|
||||||
|
content: tool.schema.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.describe("Additional sections rendered inside EN delimiters (after Solution)."),
|
||||||
|
custom_sections_ru: tool.schema
|
||||||
|
.array(
|
||||||
|
tool.schema.object({
|
||||||
|
title: tool.schema.string(),
|
||||||
|
content: tool.schema.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.describe("Additional sections rendered inside RU delimiters (after Решение)."),
|
||||||
|
repo: tool.schema
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("owner/name for remote operation via GitHub API. If omitted, operates locally on file_path."),
|
||||||
|
file_path: tool.schema
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Local file path (local mode only). Default: 'README.md'."),
|
||||||
|
},
|
||||||
|
async execute(args, context) {
|
||||||
|
try {
|
||||||
|
const file_path = args.file_path ?? "README.md"
|
||||||
|
const tier = args.tier ?? "utility"
|
||||||
|
|
||||||
|
if (args.mode === "create") {
|
||||||
|
const required: Record<string, string | undefined> = {
|
||||||
|
repo_name: args.repo_name,
|
||||||
|
tagline: args.tagline,
|
||||||
|
problem_en: args.problem_en,
|
||||||
|
solution_en: args.solution_en,
|
||||||
|
problem_ru: args.problem_ru,
|
||||||
|
solution_ru: args.solution_ru,
|
||||||
|
quick_start: args.quick_start,
|
||||||
|
}
|
||||||
|
for (const [k, v] of Object.entries(required)) {
|
||||||
|
if (!v) return `❌ ${k} is required for create mode`
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = generateReadme({
|
||||||
|
repo_name: args.repo_name!,
|
||||||
|
tagline: args.tagline!,
|
||||||
|
problem_en: args.problem_en!,
|
||||||
|
solution_en: args.solution_en!,
|
||||||
|
problem_ru: args.problem_ru!,
|
||||||
|
solution_ru: args.solution_ru!,
|
||||||
|
quick_start: args.quick_start!,
|
||||||
|
tier,
|
||||||
|
telegram: args.telegram,
|
||||||
|
demo_gif: args.demo_gif,
|
||||||
|
custom_sections_en: args.custom_sections_en,
|
||||||
|
custom_sections_ru: args.custom_sections_ru,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (args.repo) {
|
||||||
|
const getRes = spawnSync(
|
||||||
|
"gh",
|
||||||
|
["api", `repos/${args.repo}/contents/README.md`],
|
||||||
|
{ encoding: "utf-8", cwd: context.worktree },
|
||||||
|
)
|
||||||
|
let sha: string | undefined
|
||||||
|
if (getRes.status === 0) {
|
||||||
|
try {
|
||||||
|
sha = JSON.parse(getRes.stdout).sha
|
||||||
|
} catch {
|
||||||
|
sha = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const content64 = Buffer.from(content, "utf-8").toString("base64")
|
||||||
|
const putArgs = [
|
||||||
|
"api",
|
||||||
|
"-X",
|
||||||
|
"PUT",
|
||||||
|
`repos/${args.repo}/contents/README.md`,
|
||||||
|
"-f",
|
||||||
|
"message=docs: update README",
|
||||||
|
"-f",
|
||||||
|
`content=${content64}`,
|
||||||
|
]
|
||||||
|
if (sha) putArgs.push("-f", `sha=${sha}`)
|
||||||
|
const putRes = spawnSync("gh", putArgs, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
cwd: context.worktree,
|
||||||
|
})
|
||||||
|
if (putRes.status !== 0) {
|
||||||
|
return `⚠️ create-readme failed: gh api PUT failed (exit ${putRes.status}): ${putRes.stderr || putRes.stdout}`
|
||||||
|
}
|
||||||
|
return `README.md updated in ${args.repo} via GitHub API`
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(file_path, content, "utf-8")
|
||||||
|
return `README.md created at ${file_path}`
|
||||||
|
}
|
||||||
|
|
||||||
|
let content: string
|
||||||
|
if (args.repo) {
|
||||||
|
const getRes = spawnSync(
|
||||||
|
"gh",
|
||||||
|
["api", `repos/${args.repo}/contents/README.md`],
|
||||||
|
{ encoding: "utf-8", cwd: context.worktree },
|
||||||
|
)
|
||||||
|
if (getRes.status !== 0) {
|
||||||
|
return `⚠️ create-readme failed: gh api GET failed (exit ${getRes.status}): ${getRes.stderr || getRes.stdout}`
|
||||||
|
}
|
||||||
|
const data = JSON.parse(getRes.stdout)
|
||||||
|
content = Buffer.from(data.content, "base64").toString("utf-8")
|
||||||
|
} else {
|
||||||
|
content = readFileSync(file_path, "utf-8")
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = validateReadme(content)
|
||||||
|
if (result.ok) return "✅ README structure is valid"
|
||||||
|
return `❌ Validation issues:\n${result.issues.map((i) => `- ${i}`).join("\n")}`
|
||||||
|
} catch (e) {
|
||||||
|
return `⚠️ create-readme failed: ${e instanceof Error ? e.message : String(e)}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
60
docs/decisions/049-pr-112-add-repo-readme-skill-and-tool.md
Normal file
60
docs/decisions/049-pr-112-add-repo-readme-skill-and-tool.md
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
# ADR-049: create-readme tool + repo-readme skill for standardized README
|
||||||
|
|
||||||
|
## Статус
|
||||||
|
Accepted (2026-07-28)
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
Витрина slaid098.dev парсит `README.md` из каждого репозитория: скачивает raw
|
||||||
|
файл и извлекает фрагмент между разделителями `<!-- summary-en:start/end -->` и
|
||||||
|
`<!-- summary-ru:start/end -->`. До сих пор README формировался агентом вручную
|
||||||
|
через Write/Edit — это недетерминированно: разделители могли отсутствовать,
|
||||||
|
секции Support/Quick Start/language switcher — расходиться со стандартом, из-за
|
||||||
|
чего витрина не могла корректно отрендерить карточку. Issue #111 требует тулзу,
|
||||||
|
гарантирующую структуру, и скилл, дающий агенту контекст.
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
Реализовать TS-плагин `create-readme` (`.opencode/tools/create-readme.ts`) с
|
||||||
|
двумя режимами:
|
||||||
|
- `create` — генерирует README по фиксированному шаблону через template
|
||||||
|
literal, подставляя параметры. Структура (разделители, Support block, Quick
|
||||||
|
Start, language switcher) зашита в тулзе, а не в агенте.
|
||||||
|
- `validate` — проверяет существующий README на соответствие стандарту (6
|
||||||
|
проверок).
|
||||||
|
|
||||||
|
Параметр `mode` = `tool.schema.enum(["create","validate"])` (zod через
|
||||||
|
`tool.schema` = `typeof z`, подтверждено `tool.d.ts`). `tier` (flagship/utility)
|
||||||
|
и `file_path` реализованы через `.optional()` + fallback в handler (`args.tier
|
||||||
|
?? "utility"`) вместо zod `.default()` — консистентно с существующими тулзами
|
||||||
|
(они `.default()` не используют) и надёжно независимо от того, парсит opencode
|
||||||
|
args через zod или передаёт raw.
|
||||||
|
|
||||||
|
Локальный режим: `fs.writeFileSync`/`readFileSync`. Удалённый режим: `gh api`
|
||||||
|
через `spawnSync` (GET для SHA, PUT с base64+content+message+sha) — как
|
||||||
|
`create-issue.ts`/`commit.ts` используют `spawnSync` напрямую. `gh api -X PUT`
|
||||||
|
помечен как destructive в `check-permissions.py`, но это правило проверяет
|
||||||
|
bash allow-rules конфига, а не `spawnSync` внутри плагина — тулзе allow-rule не
|
||||||
|
нужен.
|
||||||
|
|
||||||
|
Скилл `repo-readme` описывает: когда вызывать create vs validate, tier system,
|
||||||
|
GitHub metadata как отдельный шаг (`gh repo edit`, не в тулзе), workflow
|
||||||
|
(create → ручные правки → validate), зачем разделители, шаблон как reference,
|
||||||
|
независимость от repo-init.
|
||||||
|
|
||||||
|
## Альтернативы
|
||||||
|
|
||||||
|
- **zod `discriminatedUnion("mode", [createSchema, validateSchema])`** —
|
||||||
|
отвергнуто: `tool()` принимает `args: Args extends z.ZodRawShape` (plain
|
||||||
|
object of zod schemas), а не `ZodDiscriminatedUnion`. Использована плоская
|
||||||
|
schema с `mode: enum` + ручная валидация обязательных полей в handler.
|
||||||
|
- **zod `.default()` для tier/file_path** — отвергнуто ради консистентности с
|
||||||
|
существующими тулзами и независимости от того, вызывает opencode zod-parse.
|
||||||
|
Использован `.optional()` + fallback `??`.
|
||||||
|
- **Импорт zod напрямую** — отвергнут: `tool.schema` уже re-export'ит полный
|
||||||
|
zod (`var schema: typeof z`), отдельный импорт и зависимость `zod` в
|
||||||
|
package.json не нужны (его нет в dependencies, и существующие тулзы его не
|
||||||
|
импортируют).
|
||||||
|
- **Регистрация тулзы в `opencode.json` (agent.tools)** — не требуется:
|
||||||
|
плагины авто-дискаверятся из `.opencode/tools/*.ts` (как `tunnel.ts`,
|
||||||
|
которого нет в конфиге).
|
||||||
49
docs/handoff/pr-112-add-repo-readme-skill-and-tool.md
Normal file
49
docs/handoff/pr-112-add-repo-readme-skill-and-tool.md
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
---
|
||||||
|
pr: 112
|
||||||
|
title: feat(skills): add repo-readme skill and create-readme tool
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что сделано
|
||||||
|
|
||||||
|
Создана тулза `create-readme` (`.opencode/tools/create-readme.ts`, TS-плагин) с
|
||||||
|
двумя режимами:
|
||||||
|
- `create` — генерирует стандартизированный двуязычный README.md по шаблону с
|
||||||
|
разделителями `<!-- summary-en:start/end -->` и `<!-- summary-ru:start/end -->`,
|
||||||
|
Support & Contact блоком, Quick Start, language switcher. Поддерживает
|
||||||
|
`custom_sections_en/ru`, `tier` (flagship/utility), `demo_gif`, `telegram`.
|
||||||
|
Локальный режим (`fs.writeFileSync` в `file_path`) и удалённый (`gh api
|
||||||
|
repos/{owner}/{repo}/contents/README.md` PUT с base64 + SHA).
|
||||||
|
- `validate` — проверяет существующий README на 6 критериев: наличие обоих
|
||||||
|
EN/RU разделителей, непустой контент между ними, ссылку slaid098.dev/support,
|
||||||
|
секции Quick Start (EN) + Быстрый старт (RU), language switcher.
|
||||||
|
|
||||||
|
Создан скилл `repo-readme` (`.opencode/skills/repo-readme/SKILL.md`): когда
|
||||||
|
использовать create vs validate, tier system, GitHub metadata как отдельный
|
||||||
|
шаг, workflow (create → ручные правки → validate), зачем разделители (парсинг
|
||||||
|
витриной slaid098.dev), шаблон README как reference, независимость от
|
||||||
|
repo-init, краткий список параметров тулзы.
|
||||||
|
|
||||||
|
Проверки: `tsc --noEmit` чисто; `check-permissions.py` OK (тулза вызывает
|
||||||
|
`gh api` через `spawnSync` внутри плагина — не bash-команда агента, не требует
|
||||||
|
allow-rule).
|
||||||
|
|
||||||
|
## Почему
|
||||||
|
|
||||||
|
Структура README критична для витрины slaid098.dev — она скачивает raw
|
||||||
|
README и извлекает фрагмент между разделителями. Тулза обеспечивает
|
||||||
|
детерминированную генерацию структуры, скилл даёт контекст агенту (когда
|
||||||
|
вызывать, tier, metadata). Ручная генерация README агентом через Write
|
||||||
|
рискует нарушить разделители.
|
||||||
|
|
||||||
|
## Pending
|
||||||
|
|
||||||
|
—
|
||||||
|
|
||||||
|
## Watch out
|
||||||
|
|
||||||
|
Handoff/ADR файлы названы `pr-0-*` (placeholder) — номер PR подставится после
|
||||||
|
create-pr и отдельного коммита `docs(handoff): set PR number`. Тулза
|
||||||
|
регистрации в `opencode.json` не требует (плагины авто-дискаверятся из
|
||||||
|
`.opencode/tools/*.ts`, как `tunnel.ts`). `tier`/`file_path` реализованы через
|
||||||
|
`.optional()` + fallback в handler (не `.default()`), консистентно с
|
||||||
|
существующими тулзами (commit.ts, create-issue.ts не используют zod `.default()`).
|
||||||
|
|
@ -35,6 +35,7 @@ opencode-config/
|
||||||
│ │ ├── python-development/SKILL.md # Python dev patterns
|
│ │ ├── python-development/SKILL.md # Python dev patterns
|
||||||
│ │ ├── release/SKILL.md # Tag + GitHub Release
|
│ │ ├── release/SKILL.md # Tag + GitHub Release
|
||||||
│ │ ├── repo-init/SKILL.md # New repository bootstrap
|
│ │ ├── repo-init/SKILL.md # New repository bootstrap
|
||||||
|
│ │ ├── repo-readme/SKILL.md # Standardized README generation (create-readme tool: create vs validate, tier system, GitHub metadata) — PR#112
|
||||||
│ │ ├── tunnel/SKILL.md # Cloudflare tunnel toggle (tool `tunnel()`: 1-й вызов start, 2-й stop) — PR#63 (восстановлен, удалён в PR#42)
|
│ │ ├── tunnel/SKILL.md # Cloudflare tunnel toggle (tool `tunnel()`: 1-й вызов start, 2-й stop) — PR#63 (восстановлен, удалён в PR#42)
|
||||||
│ │ ├── run-tests/SKILL.md # Test runner guide
|
│ │ ├── run-tests/SKILL.md # Test runner guide
|
||||||
│ │ └── spec/SKILL.md # 9-phase spec generation
|
│ │ └── spec/SKILL.md # 9-phase spec generation
|
||||||
|
|
@ -43,6 +44,7 @@ opencode-config/
|
||||||
│ │ ├── commit.ts # commit tool wrapper (1 arg message, validates format+staged) — PR#38
|
│ │ ├── commit.ts # commit tool wrapper (1 arg message, validates format+staged) — PR#38
|
||||||
│ │ ├── create-issue.ts # create-issue tool wrapper (3 args, validates format+labels; optional repo?: string) — PR#38, PR#65
|
│ │ ├── create-issue.ts # create-issue tool wrapper (3 args, validates format+labels; optional repo?: string) — PR#38, PR#65
|
||||||
│ │ ├── create-pr.ts # create-pr tool wrapper (3 args, validates format+Closes #N; optional repo?: string) — PR#38, PR#65
|
│ │ ├── create-pr.ts # create-pr tool wrapper (3 args, validates format+Closes #N; optional repo?: string) — PR#38, PR#65
|
||||||
|
│ │ ├── create-readme.ts # create-readme tool (TS plugin, modes: create/validate; standardized bilingual README with slaid098.dev delimiters; local fs + remote gh api) — PR#112
|
||||||
│ │ ├── merge-pr.ts # merge-pr tool wrapper (orchestrator-safe gh pr merge; optional repo?: string) — PR#30, PR#65
|
│ │ ├── merge-pr.ts # merge-pr tool wrapper (orchestrator-safe gh pr merge; optional repo?: string) — PR#30, PR#65
|
||||||
│ │ ├── memory-access.ts # memory-access tool (bump frontmatter last_accessed/access_count, regex replace, atomic write tmp+rename) — PR#101
|
│ │ ├── memory-access.ts # memory-access tool (bump frontmatter last_accessed/access_count, regex replace, atomic write tmp+rename) — PR#101
|
||||||
│ │ ├── memory-doctor.ts # memory-doctor tool (read-only diagnostics: rg, Python src.memory importability, env vars, memory dir, RAG index; markdown ✅/❌ report) — PR#101
|
│ │ ├── memory-doctor.ts # memory-doctor tool (read-only diagnostics: rg, Python src.memory importability, env vars, memory dir, RAG index; markdown ✅/❌ report) — PR#101
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue