Перейти к основному содержимому

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.sh wrapper
#!/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 .env load)

Добавить после строки 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 символов кириллицы → ждём ok
  • short.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 спеки:

    1. free_models.shmodels.txt.
    2. git add -A && git commit -m "checkpoint iter N" (checkpoint кода/промптов).
    3. run_pool.sh по невыполненным id.
    4. check_articles.sh → статусы; посчитать ok_count.
    5. если все ok → llm_grade.sh на ok-файлах; PASS-все → выход; иначе FAIL-ы в доработку.
    6. classify по category: missing→transport (ротация уже отработала, если всё ещё missing — смотреть worker-лог: ключ/сеть/бинарь); mdx/garbage→ правка *.prompt.md; краш/паника в логе → правка run_pool.sh/Go.
    7. apply fixes авто, коммит.
    8. re-run упавших; если new_ok < ok_countgit reset --hard <checkpoint>, категорию пометить «ручной разбор».
    9. 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. ✓