Claude Code как супервизор воркеров OpenCode

Введение

В этой схеме Claude Code не делает правку сам. Он отдаёт задачу OpenCode в контейнере Docker, проверяет то, что вернулось, и отправляет воркеру, что не так.

Сторона воркера, скрипт и параметры opencode run описаны в статье Claude Code как супервизор, OpenCode в Docker как воркер.

Эта страница — сторона Claude Code: что запускает цикл, что он читает и пишет и что проверяет, прежде чем назвать правку готовой.

Почему не субагент Claude Code?

Воркер работает на другой модели, по умолчанию на бесплатной модели OpenCode, и под --auto в контейнере, который видит один worktree (OpenCode в Docker).

Claude Code читает только отчёт и diff git, поэтому его собственный контекст остаётся небольшим.

Фраза, с которой всё начинается

Без фразы Claude Code делает задачу сам, как обычно.

Сообщение, которое начинается с одной из этих фраз, передаёт задачу воркеру:

assign to opencode: <task> delegate to opencode: <task> /assign-to-opencode <task>

Последняя — это сам навык, вызванный как слэш-команда: любой навык в .claude/skills/ можно вызвать по имени.

Навык

Claude Code загружает навык, когда сообщение совпадает с его описанием (как писать навыки для ИИ-ассистентов).

В описании названы фразы, а тело — это и есть цикл.

Это навык, который использовался для задачи на этой странице; строки шага ревью, специфичные для проекта, сокращены до того, что есть в любом репозитории:

mkdir -p .claude/skills/assign-to-opencode

vi .claude/skills/assign-to-opencode/SKILL.md

--- name: assign-to-opencode description: "The user says \"assign to opencode ...\" (also \"delegate to opencode\", \"give it to opencode\", /assign-to-opencode): you supervise and review, OpenCode in Docker does the work through container/tools/ai/delegate.py" --- ## Use when The user's message starts with "assign to opencode", "delegate to opencode" or "give it to opencode", or they run `/assign-to-opencode <task>`. You are the supervisor: you write the brief, start the worker, review its result on the host and send fixes. You do not make the change yourself, not even a small part of it. ## Do not use for - OpenCode in the container: this skill is for Claude Code on the host. - OpenCode on the host: use `delegate-to-container`; the same `delegate.py` and container, with OpenCode as the supervisor. - A task without the phrase: do it yourself as usual. ## Workflow 1. Name the task with a slug: lower case and dashes, e.g. `erlang-lesson-32`. The worker gets the new worktree `../site-agent-<slug>` on the branch `agent/<slug>` from `main`; pass `--worktree PATH` to give it an existing one. Tell the user in one line: slug, worktree, model. 2. Write the brief to the scratchpad (template below). The worker starts cold: name the files, the skills to load, what done means and the checks it can run in the container. Read the target files yourself only as far as the brief needs. 3. Start it with Bash `run_in_background` and wait for the notification; do not poll: `python3 container/tools/ai/delegate.py start <slug> --brief <file>` Add `--rw <path>` when the task edits outside container/src, `-m <provider/model>` only when the user named a model. Exit 3 means the model left the catalog: ask the user before you pick a paid model. 4. Review when it ends. The report shows the status, errors, the worker's last answer and the changed paths. Then, on the host: - `git -C <wt> status --short` and `git -C <wt> diff`; read new files. For big generated batches: tests and a sample of pages, not every file. - The linters on the touched files and a render of the touched pages with the docroot of the worktree. - Tests with the venv of the main checkout and no cache, so the worktree gets no ignored files (the launcher refuses the next round): `cd <wt>/container && <main>/container/.venv/bin/python -m pytest -p no:cacheprovider <tests> -q` - The rules of AGENTS.md: version headers, line endings, links, the language of the page. - It does what the user asked, and nothing else. 5. Problems: write a fix file, one bullet per issue with `file:line`, what is wrong and what to do, and send it into the same session, in the background: `python3 container/tools/ai/delegate.py fix <slug> --brief <fix-file>`. The worker keeps its context, so do not repeat the task. At most 3 fix rounds; after that, report what is still wrong and ask the user. 6. A round that failed before doing the work (an `error:` line, a usage limit, "Background service failed to start", `interrupted`): read the report, fix the cause, then send one `fix` that says to continue. 7. Done: tell the user what changed (paths), the checks and their results, the rounds and the cost, and what the worker left for the host. Then ask about the commit, with explicit-path `git -C <wt> add` and `git -C <wt> commit` commands. Never commit, merge or remove the worktree yourself. `delegate.py status` lists every task; `status <slug>` prints the last report again. State and logs: `container/reports/delegate/<slug>/`. ## Brief template ``` # Task: <one line> <what and why in 2-5 lines; the user's words where they matter> ## Files - edit or create: <paths> - read first: <paths> ## Load first <skills from the routing table of AGENTS.md> ## Done when - <checkable outcomes> ## Checks you can run here - php -l <files>; <other checks that need neither git nor the local site> ## Out of scope - commits (there is no git), anything outside the files above End with: the files you changed, the checks you ran and their result, and what is left to run on the host. ```

OpenCode тоже читает .claude/skills/, и в контейнере он увидел бы этот навык.

Первая строка под «Do not use for» говорит воркеру, что навык не для него.

Вторая строка отправляет OpenCode на хосте к его собственному навыку, delegate-to-container: тот же цикл, запускаемый сообщением, которое начинается со слов delegate to container (OpenCode как супервизор).

Маршрут в AGENTS.md

Задача про страницу подходит и навыкам для страниц.

Строка в начале таблицы маршрутизации в AGENTS.md говорит, что фраза идёт первой, и Claude Code читает эту таблицу в начале каждой сессии:

vi AGENTS.md

| Task | Load these skills first | |---|---| | Claude Code only: the message starts with "assign to opencode" (or "delegate to opencode") | `assign-to-opencode`: you supervise and review, OpenCode in Docker does the work |

Разрешение для скрипта

Claude Code запускает delegate.py один раз за раунд и ещё по разу на каждый status.

Правило allow в .claude/settings.json избавляет вас от запроса каждый раз.

Сам Claude Code не может добавить его в режиме auto. Его попытка была заблокирована:

Permission for this action was denied by the Claude Code auto mode classifier. Reason: [Self-Modification].

Поэтому правило добавляете вы.

Эта команда сохраняет все правила, которые уже есть, и добавляет новое в список allow:

jq '.permissions.allow = ((.permissions.allow // []) + ["Bash(python3 container/tools/ai/delegate.py *)"] | unique)' .claude/settings.json > .claude/settings.json.new && mv .claude/settings.json.new .claude/settings.json

jq '.permissions.allow, (.permissions.deny | length)' .claude/settings.json

[ "Bash(python3 container/tools/ai/delegate.py *)" ] 12

Эти 12 правил deny — те, что были в файле раньше (правила deny для OpenCode и Claude Code).

Добавить правило можно и вручную, командой /permissions.

Бриф

Задача для этой страницы пришла одним сообщением:

assign to opencode: "http://127.0.0.1:8080/dev/cpp/sfml2_tetris.php#video this page has no video chapter but has video in table of contents it should be fixed. also it has legacy Статьи про С++ related articles block - it should be removed. also it is not translated to english - need to be translated, also it has empty introduction section - need to be written"

Воркер стартует холодным, поэтому перед брифом Claude Code читает страницу ровно настолько, насколько нужно брифу.

Его вопросы и что дало чтение:

Сам бриф, без названий навыков этого репозитория:

# Task: fix and translate the English page /dev/cpp/sfml2_tetris.php (Tetris in C++ with SFML 2) The page lives on the English site but is still in Russian. The user asked for four things: 1. The table of contents links to `#video`, but the page has no video section, and no video was ever there. Remove the `'#video', 'Видео'` entry from `$tcont_urls`. Do not add a video section. 2. Remove the legacy "Статьи о C++" related-articles block: the line `include('_cpp_articles_inc.php');` near the end of the page. The page already shows related articles through `RelatedArticles` and `ArticlePage`, so keep those. Leave `_cpp_articles_inc.php` itself alone, because other pages still include it. 3. Translate everything still in Russian into English: the `$page_*` variables at the top, the same values in the `new ArticlePage(...)` arguments, the `Код` heading (→ "Code"), and every Russian comment in the C++ code block. Set `$page_lang = 'ru';` to `'en'`. Keep the code itself and its HTML escaping exactly as they are, and change only the comment text. 4. Write the empty Introduction: 1-3 short paragraphs on what the page gives the reader, based only on what the code on the page really does. Read the code first and invent nothing. Link the first mention of SFML and of C++ to their articles, and use English pages only. ## Files - edit: container/src/dev/cpp/sfml2_tetris.php (LF line endings; keep them) - read first: container/src/dev/cpp/RelatedArticles.php, container/src/dev/cpp/sfml.php - for reference only, do not edit: container/src/ru/code/cpp/sfml2_tetris.php (the Russian twin) ## Done when - no `#video` in `$tcont_urls`, no `_cpp_articles_inc.php` include in the page - no Cyrillic left in the file: `grep -n '[А-Яа-яЁё]' container/src/dev/cpp/sfml2_tetris.php` prints nothing - the Introduction has real English text with the links above ## Checks you can run here - `php -l container/src/dev/cpp/sfml2_tetris.php` - render with a shim that sets DOCUMENT_ROOT to /work/container/src, expect exit 0 ## Out of scope - every other file, the code logic, the class of the code block - commits (there is no git), the sitemap End with: the files you changed, the checks you ran and their result, and what is left to run on the host.

Два факта в первой версии этого брифа были неверны: путь инструмента, который находит статьи для ссылок, и класс блока кода.

Оба пришли из памяти. Claude Code сверил каждый путь и класс брифа с репозиторием до запуска и исправил их.

Воркер идёт по неверному пути так же охотно, как по верному.

Запуск и ожидание

Claude Code запускает раунд инструментом Bash с параметром run_in_background:

python3 container/tools/ai/delegate.py start cpp-sfml2-tetris --brief cpp-sfml2-tetris-brief.md

Command running in background with ID: bepj3sbgq. Output is being written to: /tmp/claude-1000/.../tasks/bepj3sbgq.output. You will be notified when it completes

Затем он заканчивает свой ход одной строкой для вас: слаг, worktree и модель.

Опросом он не занимается. Claude Code получает уведомление, когда процесс завершается, и только тогда читает отчёт.

Тем временем вы можете дать ему другую работу — как с этой страницей, которая писалась, пока работал воркер.

У OpenCode в роли супервизора такого параметра нет: его инструмент shell останавливает команду через две минуты, если в вызове не задан более длинный таймаут. Он запускает раунд с --detach, а затем вызывает delegate.py wait, пока раунд не закончится (--detach и ожидание).

Ревью

В отчёте было done, и воркер перечислил свои проверки как пройденные.

Claude Code проверяет ещё раз на хосте, потому что список воркера — это утверждение, а не доказательство.

Изменились только комментарии в коде. Перевод затронул длинный блок кода. Короткий скрипт сравнивает каждую строку кода старого и нового файла с отрезанным комментарием:

python3 -I - container/src/dev/cpp/sfml2_tetris.php <<'EOF'

import re, subprocess, sys f = sys.argv[1] old = subprocess.run(["git", "show", f"HEAD:{f}"], capture_output=True, text=True, check=True).stdout new = open(f, encoding="utf-8").read() def code(text): out = [] for line in text.splitlines(): if line.startswith("<code>"): out.append(re.split(r"//|&#47;&#47;", line, maxsplit=1)[0].rstrip()) return out a, b = code(old), code(new) print("code lines old/new:", len(a), len(b)) print("non-comment differences:", sum(x != y for x, y in zip(a, b)))

EOF

code lines old/new: 441 441 non-comment differences: 0

Введение говорит только о том, что делает код. В нём названы поле 15 на 25, семь фигур, падение каждые 0,3 секунды, 0,05 секунды со стрелкой вниз и одно очко за полную линию.

Каждое число было сверено по коду страницы:

grep -nE 'field_height =|field_width =|float</span> delay|delay = 0.05|userPoints \+= 1' container/src/dev/cpp/sfml2_tetris.php

126:<code><span class="blue">const</span> <span class="blue">int</span> field_height = 25;</code> 128:<code><span class="blue">const</span> <span class="blue">int</span> field_width = 15;</code> 174:<code> userPoints += 1;</code> 416:<code> <span class="blue">float</span> delay = 0.3;</code> 459:<code> <span class="violet">if</span> (sf::Keyboard::isKeyPressed(sf::Keyboard::Down)) delay = 0.05;</code>

Сама страница. php -l, рендер с docroot рабочего worktree и линтер PHP:

No syntax errors detected in container/src/dev/cpp/sfml2_tetris.php render exit 0 OK: all selected tools clean.

В отрендеренной странице: <html lang="en">, кириллицы нет, #video нет и старого блока нет.

В заголовке C++ записан как C&plus;&plus;, как и на других страницах сайта про C++.

Правка или конец

В этой задаче ревью ничего не нашло, поэтому раунда правок не было.

Когда оно что-то находит, Claude Code пишет по одному пункту на проблему с файлом и строкой, что не так и что делать, и отправляет это в ту же сессию (отправить правку).

Ключ для платной модели

Другая задача этой схемы — проверка путей к картинкам на сайте — провела первый раунд на бесплатной модели OpenCode.

Ревью оставило длинный список для второго раунда, и Claude Code выбрал для него платную модель, opencode-go/deepseek-v4.1-flash из OpenCode Go, в той же сессии (другая модель со следующего раунда).

Этой модели нужен ключ OpenCode Go. Claude Code попросил файл с ключом, и вот как это прошло.

1. Файл создаёте вы, в своём терминале. Не через ! в сессии Claude Code: вывод команды ! попадает в разговор, а запрос, который ждёт вашего ввода, там может не сработать.

И не вставляйте ключ в чат: он останется в транскрипте.

mkdir -p ~/.config/opencode-agent && chmod 700 ~/.config/opencode-agent

( umask 077; read -rsp 'OpenCode Go key: ' k; echo; printf '%s\n' "$k" > ~/.config/opencode-agent/opencode.key )

read -s не показывает то, что вы набираете.

Ключ попадает в саму оболочку, а не в командную строку, поэтому его нет в истории оболочки, а printf, встроенная команда оболочки, пишет его без процесса, который показал бы его в списке процессов.

umask 077 внутри скобок делает новый файл читаемым только для вас.

Проверено на фиктивном ключе:

stat -c '%a %U %n' ~/.config/opencode-agent ~/.config/opencode-agent/opencode.key

700 me /home/me/.config/opencode-agent 600 me /home/me/.config/opencode-agent/opencode.key

Страница OpenRouter делает свой файл с ключом через install -m 600 и редактор (ключ для контейнера). Файл тот же; read -s держит ключ подальше от экрана.

2. Claude Code проверяет, что файл на месте, и не читает его.

test -r ~/.config/opencode-agent/opencode.key && echo present

present

Затем он запускает раунд с путём к файлу:

python3 container/tools/ai/delegate.py fix img-src-check -m opencode-go/deepseek-v4.1-flash --opencode-key ~/.config/opencode-agent/opencode.key --brief fix-1.md

3. Что происходит с ключом.

4. Что всё-таки остаётся открытым. Пока идёт запуск, ключ находится в окружении контейнера.

Воркер может прочитать его через env, а любой, кто может пользоваться Docker на вашей машине, — через docker inspect. Проверено на работающем контейнере с фиктивным ключом:

docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' opencode-agent-site-agent-delegate-selftest-161385 | grep OPENCODE_API_KEY

OPENCODE_API_KEY=dummy-not-a-real-key

Это цена такой схемы.

Под --auto воркер может пользоваться ключом, поэтому дайте ему собственный ключ с лимитом трат, который можно удалить, не трогая остальные ваши ключи.

5. Не рекомендуется: сохранённый вход. opencode auth login в контейнере кладёт ключ в общий том сессий, и каждый следующий запуск воркера его видит, хотели вы этого или нет (сохранённый вход: проверить, использовать, создать или удалить).

В этой схеме проверка нашла один такой вход:

docker run --rm -v opencode-agent-data:/home/agent/.local/share/opencode opencode-agent auth list

OpenCode Go OpenCode Go stored

С ним запуск на opencode-go/ работает без всякого файла с ключом, а воркер любой задачи, в том числе бесплатных, может добраться до ключа в томе.

Удалите его и держите ключ в файле из шага 1.

Дождитесь, пока не работает ни один раунд: выход из аккаунта пишет в ту же базу в томе, которой пользуется работающий воркер.

Здесь второй раунд ещё шёл, поэтому Claude Code дождался конца его процесса (tail --pid) и только потом удалил вход:

docker run --rm -v opencode-agent-data:/home/agent/.local/share/opencode opencode-agent auth logout opencode-go "OpenCode Go"

┌ Log out of an account │ ◇ Removed account from OpenCode Go │ └ Done

docker run --rm -v opencode-agent-data:/home/agent/.local/share/opencode opencode-agent auth list

No authenticated integrations

Теперь запуску на opencode-go/ нужен --opencode-key.

Запуску на бесплатной модели вход не нужен, и он работает как раньше.

6. Когда ключ отвергнут. Следующая задача — список файлов картинок на сайте — стартовала на той же модели с файлом ключа.

Её раунд закончился через 30 секунд:

python3 container/tools/ai/delegate.py start image-list --base agent/img-src-check -m opencode-go/deepseek-v4.1-flash --opencode-key ~/.config/opencode-agent/opencode.key --rw container/tests --rw container/docs --rw container/tools/images --rw container/tools/ai/docker --rw .pre-commit-config.yaml --brief brief-image-list.md

model: opencode-go/deepseek-v4.1-flash: not checked: only opencode/<id> models have a public catalog worktree: created /home/me/github/site-agent-image-list on agent/image-list from agent/img-src-check round 1 of image-list started 2026-11-04T18:11:02+02:00; events: /home/me/github/site/container/reports/delegate/image-list/round-1.jsonl task: image-list, round 1 (start), branch agent/image-list worktree: /home/me/github/site-agent-image-list model: opencode-go/deepseek-v4.1-flash session: ses_e58aba3daffeZbYfWe45LlfgqI started: 2026-11-04T18:11:02+02:00, ended 2026-11-04T18:11:32+02:00 status: failed, exit 1 events: error 1 cost: $0.0000, tokens 0 in / 0 out error: provider.auth: Upstream request failed: Invalid credential worker's last answer: none worktree changes: none files: /home/me/github/site/container/reports/delegate/image-list/round-1.{prompt.md,jsonl,err}

Запрос отвергли ещё до первого шага: ни изменений в worktree, ни затрат.

Второй раунд проверки картинок работал на этой модели до выхода из аккаунта в шаге 5. Он пользовался сохранённым входом: ключ в файле никогда не проверяли.

Файл записали заново, и раунд, отправленный сразу же, упал так же, через 12 секунд.

Проверяйте ключ в оболочке контейнера, прежде чем снова отправлять раунд: один короткий запрос стоит меньше раунда и отличает неверный ключ от проблемы одной модели.

Проверьте ключ сами, в своём терминале, в оболочке контейнера с файлом ключа.

В worktree упавшей задачи изменений нет, поэтому оболочка может пользоваться им:

container/tools/ai/opencode-docker.sh --opencode-key ~/.config/opencode-agent/opencode.key ../site-agent-image-list shell

На приглашении /work $:

opencode auth list

OpenCode Go OPENCODE_API_KEY environment OpenCode Console OPENCODE_API_KEY environment

Ключ дошёл до контейнера.

environment говорит только, что переменная задана, а не что ключ работает (как узнать, есть ли сохранённый вход).

Один короткий запрос на двух моделях OpenCode Go:

opencode run -m opencode-go/deepseek-v4.1-flash "say ok"

> build · deepseek-v4.1-flash Error: Upstream request failed: Invalid credential

opencode run -m opencode-go/deepseek-v4-pro "say ok"

Error: Upstream request failed: Invalid credential

Что значат ответы:

Здесь упали обе модели.

Ключ скопировали заново, файл записали снова, и та же проверка в новой оболочке:

opencode run -m opencode-go/deepseek-v4.1-flash "say ok"

> build · deepseek-v4.1-flash ok

opencode run -m opencode-go/deepseek-v4-pro "say ok"

> build · deepseek-v4-pro ok

После этого Claude Code проверяет файл, не читая ключ: его права, размер и время записи.

stat -c '%a %s %y' ~/.config/opencode-agent/opencode.key

600 52 2026-11-04 21:08:23.768605839 +0200

Новое время значит, что файл записали заново.

Файл отвергнутого ключа весил 31 байт.

Файл, который намного короче файла рабочего ключа, стоит перепроверить: ключ обрезался при вставке или это другой ключ.

Затем задача продолжается в своей сессии. Отвергнутые раунды не сделали ни шага, поэтому бриф первого раунда остаётся задачей.

Раунду достаточно сказать это:

python3 container/tools/ai/delegate.py fix image-list --message "The previous attempts failed on an authentication error before any step ran; the key is fixed now. Do the whole task from your first message, from the start, and end with the summary it asks for."

round 3 of image-list started 2026-11-04T21:10:14+02:00; events: /home/me/github/site/container/reports/delegate/image-list/round-3.jsonl task: image-list, round 3 (fix), branch agent/image-list worktree: /home/me/github/site-agent-image-list model: opencode-go/deepseek-v4.1-flash session: ses_e58aba3daffeZbYfWe45LlfgqI started: 2026-11-04T21:10:14+02:00, ended 2026-11-04T21:19:49+02:00 status: done, exit 0 events: step_finish 85, step_start 86, text 4, tool_use 92 cost: $0.0726, tokens 135579 in / 23566 out

Тот же id сессии, что у отвергнутого первого раунда, и работа сделана за 10 минут.

fix сохраняет модель и файл ключа задачи, поэтому команде не нужно ни то, ни другое.

Чего Claude Code не делает

Настоящая задача в цифрах

Автор статьи: Клавдия Кондакова

Изображение баннера