process-mock-videos Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Собрать self-healing loop-скилл process-mock-videos, который гоняет очередь роликов в статьи, проверяет результат и авто-исправляет причины сбоя.
Architecture: Claude ведёт цикл по SKILL.md, вызывая bash-инструменты: free_models.sh (выбор free-моделей), run_pool.sh (прогон + ротация модели), check_articles.sh (проверки 1–3), llm_grade.sh (проверка 4, финал). Self-fix (правка промптов/кода) — Claude, с git-checkpoint и откатом при регрессе.
Tech Stack: bash, curl, python3 (парс JSON), node (mdxcheck), git, существующий Go-бинарь chatgtp-darwin-arm64.
Global Constraints
- Все новые файлы под
.claude/skills/process-mock-videos/; правка толькоmock/chatgtp/run_pool.sh. - Не изменять
blog/*. Писать только вblog-draft/. - Только free OpenRouter модели (
pricing.prompt=="0" && pricing.completion=="0"). - Источник моделей: публичный
GET https://openrouter.ai/api/v1/models(рейтингового API нет). Fallback на существующийmodels.txt. - MAX итераций цикла = 5.
- Скрипты запускаются из корня репо или из своей папки; пути внутри — абсолютные или относительно
mock/chatgtp. - Bash:
set -o pipefail,LC_ALL=en_US.UTF-8.
Task 1: free_models.sh — выбор free-моделей
Files:
- Create:
.claude/skills/process-mock-videos/scripts/free_models.sh - Create:
.claude/skills/process-mock-videos/scripts/lib/rank_free_models.py - Test: ручной прогон (сеть) + оффлайн unit на фикстуре JSON
Interfaces:
-
Produces: файл
.claude/skills/process-mock-videos/models.txt— упорядоченный список model id, по одному в строке. stdout — сводка. -
Step 1: Write ranking helper (pure, testable)
lib/rank_free_models.py читает JSON /api/v1/models со stdin, печатает упорядоченные id:
import json, sys
# curated priority prefixes (проверенные instruct-модели), выше = приоритетнее
CURATED = [
"qwen/qwen3", "meta-llama/llama-3.3-70b-instruct",
"openai/gpt-oss-120b", "openai/gpt-oss-20b",
"google/gemma", "nvidia/nemotron", "deepseek",
]
def is_free(m):
p = m.get("pricing", {})
return p.get("prompt") == "0" and p.get("completion") == "0"
def is_text(m):
a = m.get("architecture", {}) or {}
ins = a.get("input_modalities") or []
outs = a.get("output_modalities") or []
# text->text; отсечь аудио/картинко-генераторы (lyria, voice)
return "text" in ins and outs == ["text"]
def rank_key(m):
mid = m["id"]
for i, pref in enumerate(CURATED):
if mid.startswith(pref):
return (0, i, -(m.get("context_length") or 0))
return (1, 0, -(m.get("context_length") or 0))
def main():
data = json.load(sys.stdin).get("data", [])
free = [m for m in data if is_free(m) and is_text(m)]
for m in sorted(free, key=rank_key):
print(m["id"])
if __name__ == "__main__":
main()
- Step 2: Write offline test on a fixture
Мини-фикстура scratchpad/models_fixture.json:
{"data":[
{"id":"qwen/qwen3-coder:free","pricing":{"prompt":"0","completion":"0"},"architecture":{"input_modalities":["text"],"output_modalities":["text"]},"context_length":1048576},
{"id":"google/lyria-3-pro-preview","pricing":{"prompt":"0","completion":"0"},"architecture":{"input_modalities":["text"],"output_modalities":["audio"]},"context_length":1048576},
{"id":"meta-llama/llama-3.3-70b-instruct:free","pricing":{"prompt":"0","completion":"0"},"architecture":{"input_modalities":["text"],"output_modalities":["text"]},"context_length":131072},
{"id":"some/paid-model","pricing":{"prompt":"0.001","completion":"0.002"},"architecture":{"input_modalities":["text"],"output_modalities":["text"]},"context_length":200000}
]}
Run:
python3 .claude/skills/process-mock-videos/scripts/lib/rank_free_models.py \
< scratchpad/models_fixture.json
Expected (3 строки, порядок):
qwen/qwen3-coder:free
meta-llama/llama-3.3-70b-instruct:free
(lyria отсечена как audio; paid-model отсечена; qwen выше llama по curated-приоритету)
Ждём именно 2 строки — обе curated. Проверить что lyria и paid отсутствуют.
- Step 3: Write
free_models.shwrapper
#!/usr/bin/env bash
set -o pipefail
export LC_ALL="${LC_ALL:-en_US.UTF-8}"
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$DIR")"
OUT="$SKILL_DIR/models.txt"
API="https://openrouter.ai/api/v1/models"
tmp="$(mktemp)"
if curl -sf -m 25 "$API" \
| python3 "$DIR/lib/rank_free_models.py" > "$tmp" && [ -s "$tmp" ]; then
mv "$tmp" "$OUT"
echo "free_models: $(wc -l < "$OUT" | tr -d ' ') моделей → $OUT"
head -5 "$OUT"
else
rm -f "$tmp"
if [ -s "$OUT" ]; then
echo "free_models: API недоступен, использую существующий $OUT ($(wc -l < "$OUT" | tr -d ' ') строк)"
else
echo "free_models: API недоступен и нет fallback models.txt" >&2
exit 1
fi
fi
chmod +x.
- Step 4: Run live
Run:
chmod +x .claude/skills/process-mock-videos/scripts/free_models.sh
.claude/skills/process-mock-videos/scripts/free_models.sh
Expected: непустой models.txt, первая строка — curated free-модель (qwen/llama/gpt-oss).
- Step 5: Commit
git add .claude/skills/process-mock-videos/scripts/free_models.sh \
.claude/skills/process-mock-videos/scripts/lib/rank_free_models.py \
.claude/skills/process-mock-videos/models.txt
git commit -m "feat(process-mock-videos): free-model picker via openrouter /api/v1/models"
Task 2: run_pool.sh — per-video ротация модели
Files:
- Modify:
mock/chatgtp/run_pool.sh(worker-функция + чтениеmodels.txt)
Interfaces:
-
Consumes:
.claude/skills/process-mock-videos/models.txt(Task 1). -
Produces: те же DONE/FAIL записи в
process.md; FAIL только когда все модели исчерпаны. -
Step 1: Load model list near top (after
.envload)
Добавить после строки AI_MODEL="${AI_MODEL_OVERRIDE:-$AI_MODEL}":
# ordered free-model list for per-video rotation; falls back to single AI_MODEL
MODELS_FILE="${MODELS_FILE:-$REPO_ROOT/.claude/skills/process-mock-videos/models.txt}"
MODELS=()
if [ -s "$MODELS_FILE" ]; then
while IFS= read -r m; do [ -n "$m" ] && MODELS+=("$m"); done < "$MODELS_FILE"
fi
[ ${#MODELS[@]} -eq 0 ] && MODELS=("$AI_MODEL")
- Step 2: Rotate models inside worker on FAIL
Заменить блок if "$BIN" once ... ; then ... else ... fi в worker() на попытку по всем моделям:
start=$(date +%s)
log "$wid" "$id" "START"
ok=0
for model in "${MODELS[@]}"; do
if "$BIN" once \
--ai-source="$AI_SOURCE" \
--ai-model="$model" \
--openrouter-api-key="$key" \
--video-id="$id" \
--file-output="$OUT_DIR" \
--resume \
>> "$wlog" 2>&1; then
ok=1
end=$(date +%s); dur=$((end - start))
log "$wid" "$id" "DONE (${dur}s) model=$model"
break
else
printf -- '-- model %s failed for %s, rotating\n' "$model" "$id" >> "$wlog"
fi
done
if [ "$ok" -eq 0 ]; then
end=$(date +%s); dur=$((end - start))
log "$wid" "$id" "FAIL (${dur}s) — all models exhausted, see $wlog"
fi
- Step 3: Syntax check
Run: bash -n mock/chatgtp/run_pool.sh
Expected: без вывода (0 exit).
- Step 4: Dry smoke (1 id, no full run)
Проверить что скрипт стартует и читает модели: временно MODELS_FILE на тестовый список из 1 несуществующей модели + короткая очередь — убедиться что видно rotating и финальный FAIL ... all models exhausted. (Ручной, коротко; не гнать всю очередь.)
- Step 5: Commit
git add mock/chatgtp/run_pool.sh
git commit -m "feat(chatgtp): per-video model rotation in run_pool from models.txt"
Task 3: check_articles.sh — проверки 1–3
Files:
- Create:
.claude/skills/process-mock-videos/scripts/check_articles.sh - Create:
.claude/skills/process-mock-videos/scripts/lib/content_check.py
Interfaces:
-
Consumes:
videotobe.md(id-список),blog-draft/,process.md,mdxcheck.mjs. -
Produces: stdout, по строке на id:
<id>\t<status>\t<category>, где category ∈ok|missing|mdx|garbage. Exit 0 всегда (статусы в выводе). -
Step 1: Content checker (pure, testable)
lib/content_check.py <file> → печатает ok или garbage, exit 0/1:
import re, sys
MIN_LEN = 1500 # символов тела (без frontmatter)
REFUSALS = [
"i cannot", "i'm sorry", "i am sorry", "as an ai", "as a language model",
"как ии", "как языковая модель", "не могу помочь", "cannot assist",
]
def check(path):
raw = open(path, encoding="utf-8").read()
body = re.sub(r"^---\n.*?\n---\n", "", raw, count=1, flags=re.S)
if not raw.lstrip().startswith("---"):
return "garbage", "no frontmatter"
if len(body.strip()) < MIN_LEN:
return "garbage", f"too short ({len(body.strip())}<{MIN_LEN})"
if not re.search(r"[а-яё]", body, re.I):
return "garbage", "no cyrillic"
low = body.lower()
for r in REFUSALS:
if r in low:
return "garbage", f"refusal: {r}"
return "ok", ""
if __name__ == "__main__":
status, reason = check(sys.argv[1])
print(status + ("\t" + reason if reason else ""))
sys.exit(0 if status == "ok" else 1)
- Step 2: Test content checker
Создать 3 фикстуры в scratchpad:
good.md: frontmatter + ≥1500 символов кириллицы → ждёмokshort.md: frontmatter + «Привет» → ждёмgarbage(too short)refusal.md: frontmatter + 1500 символов, где есть «I cannot help» → ждёмgarbage(refusal)
Run каждый:
python3 .claude/skills/process-mock-videos/scripts/lib/content_check.py scratchpad/good.md
python3 .claude/skills/process-mock-videos/scripts/lib/content_check.py scratchpad/short.md
python3 .claude/skills/process-mock-videos/scripts/lib/content_check.py scratchpad/refusal.md
Expected: ok, garbage\ttoo short..., garbage\trefusal: i cannot.
- Step 3:
check_articles.sh
#!/usr/bin/env bash
set -o pipefail
export LC_ALL="${LC_ALL:-en_US.UTF-8}"
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$DIR/../../../.." && pwd)"
CHATGTP="$REPO_ROOT/mock/chatgtp"
QUEUE="$CHATGTP/videotobe.md"
LOG="$CHATGTP/process.md"
DRAFT="$REPO_ROOT/blog-draft"
MDXCHECK="$REPO_ROOT/mdxcheck.mjs"
mapfile -t IDS < <(grep -oE 'VIDEO_ID_[0-9]+=[A-Za-z0-9_-]{11}' "$QUEUE" | sed -E 's/^.*=//')
for id in "${IDS[@]}"; do
# проверка 1: файл существует
file="$(ls "$DRAFT"/*"$id"*.md "$DRAFT"/*"$id"*.mdx 2>/dev/null | head -1)"
if [ -z "$file" ]; then
printf '%s\t%s\t%s\n' "$id" "fail" "missing"; continue
fi
# проверка 2: mdxcheck
if ! node "$MDXCHECK" "$file" 2>/dev/null | grep -q '^OK$'; then
printf '%s\t%s\t%s\n' "$id" "fail" "mdx"; continue
fi
# проверка 3: контент
if python3 "$DIR/lib/content_check.py" "$file" >/dev/null 2>&1; then
printf '%s\t%s\t%s\n' "$id" "ok" "ok"
else
printf '%s\t%s\t%s\n' "$id" "fail" "garbage"
fi
done
chmod +x. (Примечание: проверка «DONE в логе» избыточна при наличии валидного файла — файл сильнее лога; лог остаётся для диагностики в SKILL.)
- Step 4: Run on real blog-draft
Run:
chmod +x .claude/skills/process-mock-videos/scripts/check_articles.sh
.claude/skills/process-mock-videos/scripts/check_articles.sh | head -20
Expected: таблица id status category; крошечные (141–232B) файлы → garbage, большие валидные → ok, отсутствующие → missing.
- Step 5: Commit
git add .claude/skills/process-mock-videos/scripts/check_articles.sh \
.claude/skills/process-mock-videos/scripts/lib/content_check.py
git commit -m "feat(process-mock-videos): article checker (file/mdx/content)"
Task 4: llm_grade.sh — LLM-качество (финал)
Files:
- Create:
.claude/skills/process-mock-videos/scripts/llm_grade.sh
Interfaces:
-
Consumes: список файлов (аргументы или stdin), первая модель из
models.txt,OPENROUTER_API_KEY_1изmock/chatgtp/.env. -
Produces: stdout по строке:
<file>\t<PASS|FAIL>\t<reason>. -
Step 1: Write script
#!/usr/bin/env bash
set -o pipefail
export LC_ALL="${LC_ALL:-en_US.UTF-8}"
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$DIR")"
REPO_ROOT="$(cd "$SKILL_DIR/../../.." && pwd)"
set -a; . "$REPO_ROOT/mock/chatgtp/.env"; set +a
KEY="${OPENROUTER_API_KEY_1:-$OPENROUTER_API_KEY}"
MODEL="$(head -1 "$SKILL_DIR/models.txt")"
grade_one() {
local f="$1"
local body; body="$(sed -E '1{/^---$/,/^---$/d;}' "$f" | head -c 12000)"
local payload; payload="$(python3 - "$MODEL" <<'PY'
import json,sys
model=sys.argv[1]
body=sys.stdin.read()
prompt=("Оцени статью-разбор технического собеседования. Верни строго JSON "
"{\"verdict\":\"PASS|FAIL\",\"reason\":\"...\"}. FAIL если ответы обрывочны, "
"бессвязны, не по теме или пустые.\n\nСТАТЬЯ:\n"+body)
print(json.dumps({"model":model,"messages":[{"role":"user","content":prompt}]}))
PY
)" <<<"$body"
local resp; resp="$(curl -sf -m 60 https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d "$payload")"
python3 - "$f" <<PY
import json,sys,re
f=sys.argv[1]
try:
c=json.loads('''$resp''')["choices"][0]["message"]["content"]
m=re.search(r'\{.*\}', c, re.S); j=json.loads(m.group(0))
print(f+"\t"+j.get("verdict","FAIL")+"\t"+j.get("reason","")[:120])
except Exception as e:
print(f+"\tFAIL\tgrade-error: "+str(e)[:80])
PY
}
files=("$@"); [ ${#files[@]} -eq 0 ] && mapfile -t files
for f in "${files[@]}"; do [ -n "$f" ] && grade_one "$f"; done
chmod +x.
- Step 2: Smoke on one real article
Run:
chmod +x .claude/skills/process-mock-videos/scripts/llm_grade.sh
f=$(ls blog-draft/*.md | xargs -I{} bash -c 'test $(wc -c <"{}") -gt 50000 && echo {}' | head -1)
.claude/skills/process-mock-videos/scripts/llm_grade.sh "$f"
Expected: строка <file>\tPASS\t... (или FAIL с причиной). Проверить что не падает.
- Step 3: Commit
git add .claude/skills/process-mock-videos/scripts/llm_grade.sh
git commit -m "feat(process-mock-videos): final LLM quality grader"
Task 5: SKILL.md — драйвер цикла
Files:
- Create:
.claude/skills/process-mock-videos/SKILL.md
Interfaces:
-
Consumes: все скрипты Task 1–4.
-
Produces: инструкции, по которым Claude ведёт loop.
-
Step 1: Write SKILL.md
Содержание (frontmatter + тело):
-
frontmatter
name,descriptionс триггерами: «обнови ролики», «прогони очередь», «обработай videotobe», «регенерируй статьи», «self-healing прогон». -
Раздел «Когда»: очередь
videotobe.mdнепуста, пользователь просит обработать/регенерировать. -
Раздел «Алгоритм цикла» — точный псевдокод из §7 спеки:
free_models.sh→models.txt.git add -A && git commit -m "checkpoint iter N"(checkpoint кода/промптов).run_pool.shпо невыполненным id.check_articles.sh→ статусы; посчитатьok_count.- если все ok →
llm_grade.shна ok-файлах; PASS-все → выход; иначе FAIL-ы в доработку. - classify по category:
missing→transport (ротация уже отработала, если всё ещё missing — смотреть worker-лог: ключ/сеть/бинарь);mdx/garbage→ правка*.prompt.md; краш/паника в логе → правкаrun_pool.sh/Go. - apply fixes авто, коммит.
- re-run упавших; если
new_ok < ok_count→git reset --hard <checkpoint>, категорию пометить «ручной разбор». iter++;iter>5→ выход.
-
Раздел «Проверки» — таблица из §5.
-
Раздел «Self-fix» — таблица категория→фикс из §6, с явным правилом «правки промптов/кода авто, git-checkpoint перед итерацией, откат при регрессе».
-
Раздел «Финальный отчёт» — что вывести (§8): всего/OK/PASS/осталось+почему/коммиты правок/модель-победитель.
-
Раздел «Failure modes»: API моделей лёг → fallback models.txt; все модели дают missing → проверить ключи в
.envиyt-dlp; mdxcheck нет node →npm i. -
Step 2: Sanity — скилл виден и ссылки на скрипты валидны
Run:
ls .claude/skills/process-mock-videos/scripts/*.sh \
.claude/skills/process-mock-videos/scripts/lib/*.py \
.claude/skills/process-mock-videos/SKILL.md
Expected: все 6 файлов на месте.
- Step 3: Commit
git add .claude/skills/process-mock-videos/SKILL.md
git commit -m "feat(process-mock-videos): self-healing loop skill driver"
Self-Review
Spec coverage:
- §3 драйвер Claude → Task 5. ✓
- §4.1 free_models → Task 1. ✓
- §4.2 ротация → Task 2. ✓
- §4.3 check 1–3 → Task 3. ✓
- §4.4 llm_grade финал → Task 4. ✓
- §5 проверки → Task 3+4, сведены в SKILL. ✓
- §6 self-fix классификация → Task 5 (Claude-driven, не код). ✓
- §7 loop+git → Task 5 псевдокод. ✓
- §8 отчёт → Task 5. ✓
- §9 success criteria → покрыты тестами Task 1–4 + драйвером Task 5. ✓
Placeholder scan: код во всех code-шагах полный, тестовые фикстуры заданы. ✓
Type consistency: models.txt (Task1) читается Task2/4; content_check.py контракт (ok/garbage exit 0/1) един в Task3. category-значения ok|missing|mdx|garbage согласованы Task3↔Task5. ✓