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
|
||||
│ │ ├── release/SKILL.md # Tag + GitHub Release
|
||||
│ │ ├── 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)
|
||||
│ │ ├── run-tests/SKILL.md # Test runner guide
|
||||
│ │ └── 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
|
||||
│ │ ├── 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-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
|
||||
│ │ ├── 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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue