Compare commits

...
21 Commits
Author SHA1 Message Date
ddadminandClaude Opus 5.5 4f034ea840 docs(cross-review): проверка оснований находок и пересланных отзывов
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:00:40 +02:00
ddadminandClaude Opus 5.5 6d302deb76 docs(cross-review): границы в поручении и наследование режима в T3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 17:58:42 +02:00
ddadmin 761fd9978b refactor(opencode): минимальный шаблон конфига для 2.x
Конфиг переписан в каноническом виде OpenCode 2: права заданы
списком permissions, MCP-серверы — в mcp.servers. Убраны устаревшие
default_agent и выключенный playwright, а также комментарий про
снятый плагин. Пример возврата SearXNG в README поправлен под новую
схему. Лишний симлинк opencode/skills удалён: OpenCode сам читает
~/.agents/skills.
2026-10-17 17:16:41 +02:00
ddadmin 0ab5dbe182 chore(opencode): отключены MCP-серверы поиска duckduckgo и searxng
Встроенные websearch и webfetch дают более полную выдачу, поэтому
локальные MCP-серверы поиска больше не нужны. SearXNG оставлен
в docker/searxng как резерв: добавлен README с запуском и возвратом
MCP-сервера. Заодно поправлено описание OpenCode в AGENTS.md.
2026-10-17 17:11:31 +02:00
ddadminandClaude Opus 5.5 cf9781604d docs(cross-review): уточнены круги ревью и механика T3
- Зачем: третий круг ревью Astra и отзыв DeepSeek нашли пробелы в
  разборе находок, вердикте и адаптере T3.
- Что: продолжение кругов в T3 оставлено основным путём с оговоркой, что
  это расходится с замыслом авторов T3, и с разрешённым запасным путём;
  резолюции пишутся файлом; вердикт связан с тяжестью находок; добавлены
  проверка «работа или отписка» в T3, повод для свежего ревьюера при
  переработке версии, независимость параллельных ревьюеров и заметка о
  памяти Codex.
- Проверка: схемы t3_thread_configure и t3_thread_read сверены, вид
  activity проверен на реальном разговоре; ссылки проверены скриптом.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 16:13:51 +02:00
ddadminandClaude Opus 5.5 158075e059 feat(skills): добавлен скилл cross-review для ревью моделью другого семейства
- Зачем: два плейбука оркестрации разошлись, а из них на практике нужно
  только независимое ревью моделью другого семейства.
- Что: линзы для ревьюера (по задаче, по коду, форма и вкус, вычитание,
  план), общий мандат и перепроверка; разбор находок остаётся у автора.
  Механика запуска вынесена в адаптеры: T3 Code, codex exec, claude -p.
  Следующие круги ревью идут продолжением сессии того же ревьюера.
  Старые плейбуки пока не удалены.
- Проверка: запуск, запись отчёта и продолжение сессии прогнаны на
  codex-cli 0.163.0, Claude Code 2.1.302 и в T3 Code; два круга
  ревью скилла (gpt-6.1-sol, gpt-6-astra), находки исправлены; ссылки
  и YAML проверены скриптом.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 16:03:50 +02:00
ddadminandClaude Opus 5.5 144bc2515b chore(skills): корзина скиллов .trash исключена из Git
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:38:19 +02:00
ddadminandClaude Opus 5.5 e36a756bed refactor(proxy-client): шаблоны прокси для агентов копируются вручную
- Зачем: прокси для агента — выбор конкретной машины. Автоматическая
  правка ~/.codex/.env и ~/.claude/settings.json оказалась слишком
  навязчивой.
- Что: шаблоны перенесены в proxy-client/.config/proxy-client/examples/
  (codex.env, claude-settings.json) и попадают на машину вместе с
  остальными примерами. install.sh больше не трогает настройки Codex и
  Claude, его тесты на это удалены. Инструкция по копированию — в README
  proxy-client.
- Проверка: tests/test_install.sh и tests/test_proxy_client_install.sh
  проходят.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:31:35 +02:00
ddadminandClaude Opus 5.5 ce9270623d fix(opencode): убрана лишняя ссылка ~/.config/opencode/skills
- Зачем: opencode сам читает ~/.agents/skills и ~/.claude/skills. Ссылка
  загружала тот же набор третий раз, и opencode выдавал сотни
  предупреждений о повторяющихся именах скиллов.
- Что: install.sh больше не создаёт ссылку и удаляет прежнюю, если она
  указывает на общие скиллы. README и AGENTS.md обновлены.
- Проверка: tests/test_install.sh проходит. После удаления ссылки
  opencode debug skill показывает тот же набор скиллов, теперь из
  ~/.agents/skills.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:02:37 +02:00
ddadminandClaude Opus 5.5 d65403efb5 feat(agents): прокси для Codex и Claude задаётся в их настройках
- Зачем: прокси из окружения запуска до API не доходил. Запросы Codex
  отправляет общий демон app-server, и маршрут зависел от того, кто запустил
  демон первым. Нужно было и обратное: Codex и Claude должны работать без
  прокси, даже если запускающее приложение его задало.
- Что: шаблоны codex/proxy.env (копируется в ~/.codex/.env) и
  claude/proxy-settings.json (блок env добавляется в ~/.claude/settings.json).
  По умолчанию прокси 127.0.0.1:12334. install.sh не трогает выбор, уже
  сделанный на машине. В README описано, как переключить машину на работу
  без прокси.
- Проверка: tests/test_install.sh и tests/test_proxy_env_sync.py проходят.
  Вручную: значения из ~/.codex/.env и из env в настройках Claude перекрывают
  унаследованные переменные; codex doctor проходит через Xray; очищенные
  адреса вместе с NO_PROXY=* дают прямое подключение.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:00:55 +02:00
Dmitry DementievandClaude Opus 5.5 d19239bf31 chore(skills): удалены устаревшие coordinator-loop и adversarial-review
- удалён каталог coordinator-loop, adversarial-review убран из .gitignore
- из claude-subagent-playbook убраны ссылки на оба скилла
- chief-of-staff и pr от Matt Pocock добавлены в .gitignore
- в AGENTS.md добавлено правило удаления устаревших скиллов

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:39:49 +02:00
Dmitry DementievandClaude Opus 5.5 4d3edc09ee fix(opencode): исправлена подстановка ключа Context7 и почищены разрешения
- ключ Context7 подставляется через {env:...}: с ${env:...} в заголовок
  уходил лишний $
- убраны устаревшие в V2 разрешения write и patch, их покрывает edit
- удалён слишком широкий шаблон *shadow*
- в permission-reviewer.md отмечено, что система отключена

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:37:23 +02:00
ddadmin a30e2eb939 fix(proxy-client): обновлён Xray и исправлен разбор VLESS
Зачем: профиль XHTTP с x_padding_bytes и пустым host не запускался.

Что: Xray обновлён до 26.10.10; добавлены параметры TLS, REALITY и XHTTP,
значения по умолчанию и преобразование старых названий сессии XHTTP.
Обновлены документация и регрессионные тесты.

Проверка: 59 тестов на Python 3.14 и 3.8, Bash-функции и установщик;
16 тестов разбора после уточнения проверки SHA-256. Настоящий Xray
проверил конфигурацию и выполнил HTTPS-запрос через профиль с HTTP 200.
Пользователь подтвердил работу proxy-client start.
2026-10-12 16:21:16 -04:00
ddadmin 16e85139b9 chore(opencode): эксперимент с permission-reviewer — итоги и отключение
Плагин настроен и проверен (jev через Zen + прокси, enforce+deny),
но признан не стоящим использования: хрупкая цепочка, нервные
эскалации, запертая сессия. Запись в opencode.json отключена
(закомментирована), выводы — в docs/research/.
2026-10-11 12:15:19 +03:00
Dmitry Dementiev 9f6db963be chore(opencode): второе мнение permission-reviewer на glm-5.3-flash
У glm-5.3-flash в подписке opencode-go лимиты в 4 раза выше, чем у
deepseek-v4.1-flash, при сравнимом интеллекте. outputFormat json_schema:
модель поддерживает structured output (по models.dev).
2026-10-10 22:00:13 +02:00
Dmitry Dementiev b516e9cc54 fix(opencode): searxng обратно на npx - node установлен
npm-пакет ihor-sokoliuk/mcp-searxng работает (проверен initialize-
handshake). PyPI-варианты заброшены и несовместимы с текущим MCP SDK.
2026-10-10 18:06:36 +02:00
Dmitry Dementiev 156d22c3b0 fix(opencode): searxng MCP переведён с npx на uvx
На машине нет Node.js - spawn npx падал с NotFound, searxng никогда
не подключался. Тот же сервер есть на PyPI и работает через uvx,
как и duckduckgo.
2026-10-10 17:59:53 +02:00
Dmitry Dementiev 452b0c5c75 fix(opencode): второе мнение permission-reviewer на доступную модель
escalationReviewer ссылался на deepseek/deepseek-flash - такого провайдера
нет, каждая эскалация Jev падала с 'Reviewer model is unavailable'.
Заменено на opencode-go/deepseek-v4.1-flash (подписка, в 2 раза дешевле
Zen): outputFormat text (у модели нет json_schema), variant high
(none не поддерживается, есть только low/high/max).
2026-10-10 17:49:47 +02:00
Dmitry Dementiev 32cd7097e6 refactor(opencode): удалены устаревшие профили safe-build/safe-plan
Профили дублировали работу permission-reviewer статическими списками:
их deny обходили ревьюер и давали отказы без причины (git push). Теперь
решения об ask принимает плагин; статикой оставлены только deny на чтение
секретов - детерминированная гарантия, LLM-ревьюеру не доверяем.

- opencode.json: default_agent -> build, убраны статические списки
  и deny на git push (решение за ревьюером)
- permission-reviewer.jsonc: actorProfiles переключены на встроенные
  build/plan
- install.sh: блок линковки agents заменён чисткой старого симлинка;
  добавлено копирование permission-reviewer.jsonc в ~/.config/opencode
  (плагин читает его только оттуда - раньше тюнинг не применялся)
- тесты: проверка чистки симлинка и копирования конфига ревьюера
2026-10-10 17:32:34 +02:00
Dmitry Dementiev c3efe60a00 refactor(agents): убраны настройки Claude из git и пакет codex
Claude: из репозитория удалены settings.local.json (разрешения не нужны
при autoapprove), statusline-command.sh и устаревший CLAUDE.md; линкуются
только skills. install.sh убирает артефакты прежних прогонов.

Codex: пакет удалён целиком, ~/.codex - обычный каталог. Персональные
мостики скиллов убраны; миграция раскрывает свёрнутый симлинк и переносит
рантайм-данные. Чищены chezmoi-скрипты.

install.sh: migrate_claude_stow_symlink обобщена в migrate_folded_symlink.
Тесты: 3 устаревших удалены, 3 добавлены; assert_not_exists ловит битые
симлинки.
2026-10-10 17:15:34 +02:00
Dmitry Dementiev 346b3bfc61 feat(opencode): ревьюер разрешений переключён на Jev со вторым мнением
- Зачем: Jev (System One) решает за ~0.6 с против 4 с у чат-модели,
  но на тонком контексте занижает confidence (0.6 < 0.7) и эскалировал
  даже тривиальные команды.
- Что: model=opencode/jev-1.13-free (ключ OPENCODE_API_KEY из
  ~/.env.secrets, нужен export); escalationReviewer=deepseek/deepseek-flash
  для неуверенных решений; actorProfiles для safe-build/safe-plan.
- Проверка: прогон printf → allow (low, 0.97), аудит пишется;
  fail-closed при отсутствии ключа подтверждён (эскалация, не allow).
2026-10-06 17:14:41 +03:00
53 changed files with 1434 additions and 2426 deletions

No files matched your search

-13
View File
@@ -30,19 +30,6 @@ if ((Test-Path "claude") -and -not (Test-Path "dot_claude")) {
Write-Host " [OK] claude -> dot_claude"
}
# codex → dot_codex (будет ~/.codex)
if ((Test-Path "codex") -and -not (Test-Path "dot_codex")) {
Rename-Item "codex" "dot_codex"
# Перемещаем содержимое .codex в корень и удаляем файлы skills
if (Test-Path "dot_codex/.codex") {
Move-Item "dot_codex/.codex/*" "dot_codex/" -ErrorAction SilentlyContinue
Remove-Item "dot_codex/.codex" -Recurse -Force -ErrorAction SilentlyContinue
}
# Удаляем старые файлы skills (будут созданы как симлинки)
Remove-Item "dot_codex/skills" -Recurse -Force -ErrorAction SilentlyContinue
Write-Host " [OK] codex -> dot_codex"
}
# vim/.vimrc → dot_vimrc (будет ~/.vimrc)
if ((Test-Path "vim/.vimrc") -and -not (Test-Path "dot_vimrc")) {
Move-Item "vim/.vimrc" "dot_vimrc"
@@ -30,14 +30,6 @@ if (Test-Path "$env:USERPROFILE\.claude\skills") {
New-Item -ItemType SymbolicLink -Path "$env:USERPROFILE\.claude\skills" -Target $skillsSource
Write-Host "Created: ~/.claude/skills -> ~/.agents/skills"
# Codex
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.codex\skills" | Out-Null
if (Test-Path "$env:USERPROFILE\.codex\skills\conventional-commits") {
Remove-Item "$env:USERPROFILE\.codex\skills\conventional-commits" -Force
}
New-Item -ItemType SymbolicLink -Path "$env:USERPROFILE\.codex\skills\conventional-commits" -Target "$skillsSource\conventional-commits"
Write-Host "Created: ~/.codex/skills/conventional-commits -> ~/.agents/skills/conventional-commits"
# Opencode
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.config\opencode" | Out-Null
if (Test-Path "$env:USERPROFILE\.config\opencode\skills") {
+3 -4
View File
@@ -10,10 +10,6 @@ opencode/.config/opencode/package.json
opencode/.config/opencode/node_modules/
opencode/.config/opencode/bun.lock
# Codex local files
codex/.codex/
!codex/.codex/skills/
# Gemini local files
gemini/.gemini/
!gemini/.gemini/policies/
@@ -35,5 +31,8 @@ __pycache__/
# Кэш скиллов, синхронизируемых Claude Code с claude.ai (~/.claude/skills -> agents)
agents/.agents/skills/synced/
# Корзина программы синхронизации скиллов: старые копии, в Git не нужны
agents/.agents/skills/.trash/
# Рабочие заметки агентов (задачи, PRD, handoffs) — только локально
.scratch/
+6 -2
View File
@@ -27,8 +27,8 @@ This repository contains personal dotfiles and configurations for a highly produ
* **Shared Skills:** Located in `agents/.agents/skills/`. This is the single source of truth for AI instructions.
* **Gemini CLI:** Configured via `gemini/.gemini/`.
* **Policies:** `readonly-commands.toml` allows critical commands like `git status`, `git diff`, and `git add` to run without confirmation.
* **Opencode:** MCP-based assistant configuration (`opencode.json`) with support for documentation, search, and web-reading.
* **Skill Bridges:** Symlinks are used to share skills across Claude, Codex, and Opencode.
* **Opencode:** MCP-based assistant configuration (`opencode.json`): Context7 для документации и встроенные `websearch`/`webfetch`. Локальный SearXNG в `docker/searxng` — резервный вариант, MCP-сервер отключён.
* **Skill Bridges:** Claude gets skills via the `~/.claude/skills` symlink; Opencode reads `~/.agents/skills` natively, no bridge needed.
## 🚀 Usage & Maintenance
@@ -39,6 +39,10 @@ This repository contains personal dotfiles and configurations for a highly produ
### Adding/Updating Skills
1. Modify files in `agents/.agents/skills/`.
2. Ensure symlinks in other tool directories are updated if new skills are added.
3. Устаревшие скиллы время от времени удаляем. Убирай каталог целиком, а
вместе с ним ссылки из других скиллов и строку в `.gitignore`, если
скилл был внешним. Упоминания в архивах и handoff-записках — история,
их не трогаем.
## 📋 TODO
+12 -7
View File
@@ -56,7 +56,7 @@ Chezmoi использует отдельный source dir `chezmoi/` внутр
| `vscode` | `server-env-setup` для VS Code Server в WSL: секреты + авто-прокси |
| `chatgpt` | Отдельный ярлык `ChatGPT Proxy` с запретом прямого запуска при ошибке |
`install.sh` дополнительно настраивает opencode (симлинки, MCP-конфиг), claude (симлинкает `settings.local.json` из репозитория) и подключает bash_custom.
`install.sh` дополнительно настраивает opencode (MCP-конфиг), claude (симлинк `~/.claude/skills` на общие скиллы) и подключает bash_custom.
### Windows (chezmoi)
@@ -76,9 +76,9 @@ Chezmoi использует отдельный source dir `chezmoi/` внутр
| Инструмент | Что настраивается |
|------------|-------------------|
| **Agents** | Каноничное хранилище скиллов: `~/.agents/skills/<name>/SKILL.md` |
| **Claude** | `~/.claude/skills` -> симлинк на agents; `settings.local.json` -> симлинк на репозиторий (базовые permissions + statusLine); `statusline-command.sh` -> копия из репозитория; `settings.json` - управляется Claude Code |
| **[Opencode](https://github.com/anomalyco/opencode)** | MCP-серверы (context7, web-reader, playwright и др.), `~/.config/opencode/skills` -> симлинк на agents |
| **Codex** | `~/.codex/skills/` -> симлинки на agents |
| **Claude** | `~/.claude/skills` -> симлинк на agents; настройки (`settings.json`, `settings.local.json`) - управляются Claude Code на машине |
| **[Opencode](https://github.com/anomalyco/opencode)** | MCP-серверы (context7, web-reader, playwright и др.); скиллы opencode сам читает из `~/.agents/skills` |
| **Codex** | `~/.codex` - обычный каталог, управляется Codex; скиллы - из общего хранилища agents |
| **Gemini** | Политики для CLI: pre-approved readonly команды |
## Настройка
@@ -114,6 +114,11 @@ Linux/Bash-интерфейс `proxy-client` поддерживает внешн
Windows не меняется. Если Codex уже запущен, его нужно полностью закрыть перед
запуском через `Codex Proxy`.
Codex и Claude Code могут брать прокси из своих настроек, независимо от того,
откуда их запустили. Шаблоны лежат в `~/.config/proxy-client/examples/` и
копируются вручную; установщик настройки агентов не трогает. Подробнее —
[в README proxy-client](proxy-client/README.md#прокси-в-настройках-codex-и-claude-code).
`Claude Proxy` поддерживает как обычную установку в
`%LOCALAPPDATA%\AnthropicClaude`, так и пакет из Microsoft Store. В обоих
случаях он передает параметры Electron `--proxy-server` и
@@ -201,7 +206,7 @@ PowerShell и VS Code: одного общего файла у Windows и WSL н
| Файл | Назначение |
|------|------------|
| `~/.env.secrets` | API-ключи, подгружается bash и VS Code Server |
| `~/.codex/.env` | Общие proxy-переменные для Codex CLI, Codex Desktop и их дочерних процессов |
| `~/.codex/.env` | Proxy-переменные для Codex CLI, Codex Desktop и их дочерних процессов. Перекрывают унаследованное окружение. Шаблон: `~/.config/proxy-client/examples/codex.env` |
| `~/.bash_aliases` | Персональные алиасы bash |
| `~/.gitconfig.local` | Персональные Git-настройки (`user.name`, `core.editor` и т.д.) |
| `~/.local/bin/start-claude-proxy.ps1` | Скрипт запуска Claude Desktop для Windows с переменными прокси и параметрами Electron |
@@ -221,5 +226,5 @@ stow проще и покрывает весь Linux/WSL workflow. chezmoi ну
**Почему Windows PowerShell 5.1 не трогается?**
Если форсировать UTF-8 и менять профиль PS 5.1, можно сломать старые скрипты, `.bat`/`.cmd` и консольные утилиты. PS 7 - основной shell, PS 5.1 - режим совместимости.
**Почему Claude `settings.local.json` - симлинк, а `settings.json` - нет?**
Claude Code перезаписывает `settings.json` при каждом "always allow" - этот файл принадлежит Claude Code. Базовые permissions и statusLine живут в `settings.local.json`, который Claude Code только читает и мержит с `settings.json`. Симлинк из `~/.claude/settings.local.json` на файл в репозитории безопасен. Машинно-специфичные настройки (`model`, `effortLevel`) - в `settings.json`, управляются через Claude Code.
**Почему у Claude в репозитории нет настроек?**
Все настройки Claude Code (`settings.json`, `settings.local.json`) принадлежат самому Claude Code: он перезаписывает их при каждом "always allow" и при смене модели. Список разрешений не ведём вручную - работаем с autoapprove. Из репозитория линкуются только скиллы. Машинно-специфичные настройки (`model`, `effortLevel`) - в `settings.json`, управляются через Claude Code.
+2 -1
View File
@@ -39,7 +39,8 @@ wait-what/
wizard/
writing-for-agents/
retro/
chief-of-staff/
pr/
# Skills, подключаемые в agents/ как внешние плагины (не версионируются здесь)
claude-team-review
adversarial-review
@@ -23,8 +23,7 @@ description: >-
**Не разворачивай конвейер на мелочь.** Косметика, один
файл — сделай сам: накладные расходы процесса многократно превысят цену
работы. Очередь зависимых issue с автономным прогоном —
это `/coordinator-loop`, не этот плейбук.
работы.
## Роли
@@ -210,7 +209,7 @@ description: >-
уверенный вердикт одной (APPROVED без находок) не закрывает находки
другой.
Пофиндинговый триаж — у **исполнителя**, не у тебя (правило
`/claude-team-review` и `/coordinator-loop`: судит тот, у кого контекст
`/claude-team-review`: судит тот, у кого контекст
кода): перешли Codex индекс находок **дословно**, он чинит или
**аргументированно отклоняет** — через контракт и тест, не голословно;
промпт починки — «закрой перечисленное, больше ничего не меняй».
@@ -256,7 +255,7 @@ description: >-
## Пользователь в петле
В отличие от `/coordinator-loop`, здесь пользователь рядом: на границах
Здесь пользователь рядом: на границах
этапов давай в чат карту прогресса в 4–7 строк обычными словами
(✅ сделано / ▶ идёт / ⬜ дальше); термины плейбука — только с
расшифровкой: телеграфные статусы в терминах этапов теряют пользователя.
@@ -295,13 +294,8 @@ teammate-субагентов — шум, а не событие: не докл
## Соседние скиллы
- `/coordinator-loop` — автономный цикл очереди issue на стороне Codex
(координатор в Codex, worker'ы — его субагенты). Выстрадан и отлажен;
словарь (индекс, свежие сессии, две ручки стоимости) — общий.
- `/claude-team-review` — ревью-петля, где лид сам чинит код (живёт
отдельным репозиторием, в среде может быть не подключён).
- `/adversarial-review` — разовое адверсарное ревью Кодексом (в среде
может быть не подключён).
- `codex-subagent-playbook` — близнец этого плейбука для координатора на
стороне Codex; скрыт от автовызова, зови по имени.
- `/to-tickets` — разбиение на тикеты-срезы с рёбрами блокировок (этап 1);
@@ -1,100 +0,0 @@
# coordinator-loop
Координаторский цикл: проводит готовые issues через subagent-worker по TDD, держит
границы задач, фильтрует находки и сам делает коммиты.
## Зачем
Когда подряд идёт несколько связанных кодовых срезов, писать их «одним потоком» —
значит терять границы задач и контроль над тем, что попало в коммит. Навык
разделяет роли: один subagent-worker делает **одну** issue по TDD и не коммитит, а
координатор проверяет, классифицирует находки и фиксирует результат. Навык узкий и
работает только по явному вызову (`/coordinator-loop`): он не проектирует, он
исполняет.
## Где в потоке
coordinator-loop — исполнительное звено в середине цепочки, а не вся цепочка:
```text
проектная документация (/brainstorm-with-docs | /grill-with-docs)
↓
узкие проверяемые issues (/to-issues)
↓
coordinator-loop каждый issue: worker (/goal + /tdd) + независимое ревью
(запускает координатор) → индекс → коммит координатора;
в финале — ревью на соответствие замыслу + handoff
↓
приёмка ручной кросс-линия ревью (обычно Claude) + проверка человеком
```
- **До скилла — проектирование.** Сначала выверенная верхнеуровневая документация
(`/brainstorm-with-docs` или `/grill-with-docs`): без неё issues не на чем
основывать. Затем `/to-issues` режет её на узкие задачи. Именно здесь
закладывается проверяемость результата (см. «Что получается»).
- **Скилл — исполнение.** Берёт **готовые** issues по одной и проводит каждую через
цикл worker (`/goal` + `/tdd`) → независимое ревью (ревьюеры-наблюдатели, запускает
координатор; триаж находок — в worker-е) → индекс → chain-level классификация →
коммит координатора. Контекст координатора держится на индексах, не на diff'ах.
В финале цепочки координатор прогоняет ревью на соответствие замыслу (целое против
исходной спеки, сильной моделью на усиленном уровне) и пишет handoff, готовящий
приёмку.
- **После скилла — приёмка.** Ревью другой модельной линией (Codex ревьюил Codex, так
что кросс-линия — обычно ручной проход Клодом) плюс проверка человеком. Скилл её не
автоматизирует — только передаёт эстафету с зафиксированным открытым риском.
## Что делает и чего не делает
Делает: ведёт по одному issue за раз, держит границы, фильтрует находки, коммитит.
Подробный цикл и роли — в [SKILL.md](SKILL.md).
Не делает:
- не создаёт проектную документацию и не принимает архитектурные решения — это
предыдущий этап;
- не нарезает issues — это `/to-issues`;
- не отвечает за проверяемость результата — она закладывается при проектировании;
- не делает финальную приёмку — это ревью другим агентом плюс человек;
- не готовит набор issues «пачкой» автоматически — пока не цель.
## Что получается
Результат прогона по одной issue — «пакет для проверки»:
- **отдельный коммит на каждую issue** с узким, в границах задачи, diff;
- **финальный отчёт worker-а**: какие файлы, какие тесты прогнаны, какие acceptance
criteria закрыты, риски, `git status`;
- **chain-level классификация** (риск / вне скоупа); per-finding триаж («реально /
ложно / чинить») делает worker по индексам ревьюеров.
Один issue = один проверяемый коммит плюс отчёт — так пост-цикловое ревью и ручная
проверка остаются посильными.
**Про проверяемость.** Прошлый прогон показал: если итог цепочки можно принять
только чтением diff, ручная проверка становится слишком дорогой. Лекарство — на
уровне проектирования: у всего набора issues должен быть **наблюдаемый конечный
результат** для приёмки (запись в Kafka-топик, строки в конкретной таблице, панель
дашборда, вывод команды). Это ответственность `/to-issues` и брейншторма, **не**
этого скилла. Координатор может отметить слабую проверяемость как **риск** и вернуть
сигнал на уровень выше, но сам критерий приёмки не выдумывает.
## Как вызвать
Только по явному запросу — навык узкий и не срабатывает сам:
```text
/coordinator-loop
```
## Статус и что дальше
Экспериментальный — прямое продолжение эксперимента с координатором субагентов,
будет дорабатываться. Открытые вопросы (когда нужен отдельный ревьюер, обязательные
промежуточные отчёты, бюджет worker-а) собраны в разделе «Открытые вопросы»
файла [references/rationale.md](references/rationale.md).
## Что читать рядом
- [SKILL.md](SKILL.md) — как координатор исполняет цикл (инструкция для агента).
- [references/rationale.md](references/rationale.md) — почему цикл устроен так:
эмпирика эксперимента, находка про ревьюера другой родословной, открытые вопросы.
@@ -1,382 +0,0 @@
---
name: coordinator-loop
disable-model-invocation: true
description: >-
Координаторский цикл по цепочке готовых issue: на каждый — worker-субагент по TDD
с независимым ревью, коммит делает координатор. Контекст координатора
держится чистым — в него входит только индекс находок, не diff. Рассчитан на
автономный прогон очереди issue без присмотра пользователя. Узкий навык, только
по явному вызову /coordinator-loop.
---
# Координационный цикл субагентов
Ты — **координатор**. Реализацию не пишешь, код ради верификации не читаешь
(точная граница чтения — в «Ролях»). Ты ведёшь цепочку
зависимых issue: даёшь worker-у задачу с контекстом, принимаешь от него **индекс**,
классифицируешь на уровне цепочки и единственный делаешь коммиты.
**Ведущее правило — работай по индексу, а не по полному тексту.** Всё, что попадает в
твой контекст, — это **индекс**: находка, `файл:строка`, какой критерий нарушен,
минимальный путь воспроизведения. Полного diff, вывода тестов и нарратива ревью в твоём
контексте быть не должно — их читают субагенты у себя, а тебе возвращают индекс.
Твоих собственных команд это тоже касается: шумный вывод гони в файл-лог, в контекст
бери код возврата и итоговые строки (механика — шаг 7).
Верификацию находок делает ревьюер, не ты. (Исключение — состав diff на коммит-гейте:
`git diff --stat`/`--name-only`, какие файлы и в границах ли; это не полный текст, см.
шаг 7.) Проверяй себя каждый цикл: *в мой контекст попал полный текст вместо индекса?
Если да — это баг процесса.* Так контекст не пухнет, не компактится и не начинает
косячить — это главная болезнь, которую навык лечит.
## Когда применять
Последовательные инженерные срезы с тестами и acceptance criteria, где соседние задачи
трогают близкий код и их нельзя безопасно параллелить. Вход цикла — **качественно
подготовленные issue**: цель, критерии готовности и границы уже сформулированы (обычно
это выход `/to-issues` после проработки замысла, например через `/brainstorm-with-docs`).
Мутный issue в цикл не брать: цикл не проясняет замысел, а превращается в дорогую машину
ревью-кругов — сначала прояснение и проработка замысла, потом цикл. Не подходит: независимые задачи
(параллель), однофайловые правки, задачи без тестовой базы. Если цепочка не подходит —
скажи об этом пользователю, а не тяни цикл силой.
## Контракт автономного прогона
Основной сценарий скилла: пользователь передаёт очередь подготовленных issue и уходит —
координатор ведёт конвейер сам, а не ждёт инструкций на каждом шаге.
**Вход** — что пользователь даёт при вызове:
- список issue и порядок (или правило зависимостей);
- где лежит «зачем» (PRD/спека), если это не очевидно из самих issue;
- ожидаемый финал, если он отличается от стандартного (см. «Финал цепочки»).
**Мандат**: весь цикл — worker, ревью, коммит-гейт, статусы, журнал, handoff —
координатор ведёт без уточняющих вопросов. Локальные коммиты рабочих срезов входят в
мандат по умолчанию; push, теги и всё, что уходит за пределы локального репозитория
или разрушает данные вне рабочего дерева, — только по явному поручению.
**Стоп-условия** — сводка, сами правила в своих разделах: чужие правки в дереве
(шаг 1); мутный или неготовый issue («Когда применять», шаг 2); спор, переживший
узкий усиленный гейт, и потолок кругов ревью (шаг 5); действие за пределами мандата.
Проверка, недоступная worker-у (стенд, docker, сеть), цепочку не останавливает:
worker сдаёт её как `DONE_WITH_CONCERNS` с готовой командой (статус-контракт);
доступную координатору проверку тот прогоняет сам — это часть коммит-гейта. Открытым
риском в handoff уходит только то, что не может проверить и координатор: ручная
приёмка человеком, недоступная отсюда внешняя среда.
**Выход** — по «Финалу цепочки»: чистое дерево, список коммитов, статусы issue,
handoff с открытыми рисками.
## Роли
- **Координатор** — ты. Порядок issue, богатый контракт, запуск ревьюеров, политика
кругов ревью, chain-level классификация по индексу, коммит-гейт, финальный handoff.
**Код ради верификации не читаешь** — это ревьюер; на коммит-гейте смотришь лишь
**состав** diff (не текст, см. шаг 7). Процессные и навигационные артефакты — файлы
issue и PRD, журнал, handoff, документация процесса — твоя зона: их читаешь и правишь
сам, запрет касается только кода ради верификации.
- **Worker** — субагент с одной целью (один issue). Гоняет `/goal` (петля
план→код→тест), меняет файлы, **не коммитит и не откатывает чужое**. Триажит находки
ревью: чинит или аргументированно отклоняет. Возвращает индекс + статус.
- **Ревьюер** — свежий субагент-**наблюдатель**, которого запускаешь **ты** (см.
«Контракт ревьюеру»): репозиторий и среду не меняет, но инструменты у него полные —
ограничение промптом, не урезанием субагента. Читает код сам, докладу worker-а не
верит, возвращает индекс находок. `claude-team-review` как механизм допустим только там, где его может
запустить **сам worker** (владелец правок): лид team-review чинит код сам, а второй
писатель в дереве недопустим — координатору team-review запускать нельзя.
**Факт среды Codex (проверено 2026-07-04 диагностом с `agent_type=worker`):** worker не
может запускать субагентов — `spawn_agent` недоступен, `tool_search` не находит ни
spawn, ни `claude-team-review`. Поэтому ревьюеров запускает координатор и пересылает
worker-у их индексы **дословно**, без своих интерпретаций. Не перепроверяй этот факт на
каждой цепочке — жечь токены; перепроверь только при сигнале обратного (например,
`tool_search` вдруг нашёл spawn).
**Всех субагентов — worker-а, ревьюеров, диагностов — запускай свежими сессиями, без
форка своей истории.** Контракт самодостаточен, правила и «зачем» лежат в репозитории
и issue — форк не нужен. А вреден он дважды: тащит твой контекст в субагента (расход
без пользы) и убивает «свежий взгляд» — ревьюер, унаследовавший твою историю, уже не
независим. Не трать циклы на попытки передать субагенту полный контекст — это против
дизайна, а не ограничение, которое надо обойти.
Свежая сессия — правило **запуска**, не запрет продолжения: уже запущенного субагента
между кругами **продолжай** (`send_input`), не пересоздавай. Worker-а — всегда: его
контекст по issue — актив. Ревьюера на rerun после точечных правок — тем же, узким
вопросом: «закрывают ли правки твои находки, не внесено ли нового рядом» — полный
обход заново не нужен. **Свежий ревьюер обязателен** там, где нужен незаинтересованный
взгляд: узкий гейт по спору, эскалация уровня, правки перекроили реализацию широко,
индекс прошлого ревьюера слаб или сомнителен. Серия полных обходов свежими xhigh-сессиями
на каждый rerun — сжигание бюджета, а не независимость (эмпирика 2026-07-04: пять
свежих кругов финального ревью).
## Цикл на одну задачу
1. **Чистое дерево.** `git status --short` до старта. Чужие правки — стоп, спроси
пользователя. Свои процессные — отдели docs-коммитом.
2. **Прочитай issue и зависимости.** Предыдущие в цепочке не закрыты — не начинай.
3. **Назначь режим ревью** по порогу риска (см. «Режимы ревью») и запиши в журнал;
если срез может задеть порог по ходу — задай условие эскалации (например, «worker
тронул state-схему → перевести в гейт»). Затем **контракт worker-у** (см. ниже) и
запусти **одного** worker-а — уровень мышления явно по
[references/effort-levels.md](references/effort-levels.md), в журнал вместе с режимом.
4. **Дождись индекса.** Долго молчит — запроси промежуточный статус, не жди вслепую.
Пришло push-уведомление — сразу обрабатывай, не крути опрос по кругу.
5. **Ревью-гейт** (worker вернул `DONE`/`DONE_WITH_CONCERNS`; `BLOCKED` и
`NEEDS_CONTEXT` разбирай до ревью — гонять ревьюеров по незавершённой работе
бессмысленно). По назначенному режиму запусти ревьюеров; запуск и вердикт каждого
фиксируй в журнале в момент события. Индекс находок перешли worker-у **дословно**,
его триаж прими индексом же. Rerun после правок по гейт-находкам назначаешь **ты**,
автоматически — это не concern worker-а; узкий rerun — продолжением того же
ревьюера, свежий — по триггерам (см. «Роли»). **Круг** — одна итерация «прогон(ы) ревью →
триаж worker-а»; пара «по задаче + по коду» на гейте — один круг. Гейт закрыт, когда
все назначенные прогоны вернули `APPROVED`. Политика кругов (тождество находок между
кругами судит worker в триаже, не ты — по индексам оно не определяется):
- worker пометил **«спор»** (ревьюер снова поднял вопрос, уже аргументированно
отклонённый через контракт и тест) → не общий rerun, а **узкий гейт** по спорному
вопросу на усиленном уровне мышления;
- спор пережил и узкий усиленный гейт → стоп, решение человека;
- **третий круг на issue** — предупреди пользователя, не дожидаясь потолка: расход
растёт, причина (что именно не сходится). Это предупреждение, не вопрос: ответа
не жди, работу продолжай — жёсткая граница ниже;
- независимо от споров больше **четырёх** кругов ревью на issue → стоп, вынеси
человеку вопрос «дробить issue или принимать риск».
6. **Классифицируй по индексу — только chain-level** (см. «Классификация»).
7. **Коммит-гейт.** Прогони полный тест-сет **сам**: код возврата, который ты видел
своими глазами, — это и есть твоя проверка; прогон через субагента превращает гейт
обратно в чужой пересказ. Шумные команды гоняй через файл-лог
(`<команда> > /tmp/<лог> 2>&1`), в контекст возьми **только сводку**: код возврата
и итоговые строки (`tail`, имена упавших — `grep FAILED`). На «тихие» флаги вроде
`pytest -q` не полагайся: цель сборки может зашивать подробный вывод (эмпирика
2026-07-05: `make generator-test` с зашитым `pytest -v` высыпал в контекст
координатора полный список из 181 теста). Если сводки мало и вывод нужно
**интерпретировать** — лог сам не читай, спавни разборщика логов экономного уровня
([references/effort-levels.md](references/effort-levels.md)), он вернёт индекс. Так же
проверь **состав** diff (`git diff --stat`/`--name-only`: какие файлы, в границах ли —
не полный текст) и коммить. Упали тесты — верни их worker-у по именам, трейсбеки в свой
контекст не тащи. Сверь по журналу, что назначенный режим ревью выполнен: каждый
запуск и вердикт записаны в момент события; нет записи — ревью не проведено. В коммит
issue стейджи только файлы worker-а; файл issue и `coordinator-journal.md` —
процессные, не файлы worker-а: после коммита переведи в файле issue `Status: done`,
и оба уходят отдельным docs-коммитом (шаг 1 следующего цикла, на последнем issue —
перед выходом). Это последняя точка контроля состава изменений.
8. **Закрой субагентов.** Следующий issue стартует в новом worker-е.
## Журнал прогона
С первого шага цепочки веди журнал: append-only файл
`.scratch/<feature>/coordinator-journal.md`, короткие записи в конец по ходу событий,
не задним числом. Каждую запись начинай с времени `HH:MM` (дата — в заголовке дня;
время бери вызовом `date`, сама модель его не знает) — так по журналу восстанавливается
длительность этапов и границы issue. Точки записи: старт issue (worker запущен, его
уровень мышления, назначенный режим ревью), принят индекс worker-а, **запуск каждого
ревью** (механизм,
кто запускал, уровень мышления — **фактическое значение хоста** вроде `high`/`xhigh`,
не название яруса: «рабочий уровень» в журнале не проверяем, а `medium` вместо `high` —
видимое расхождение) и **его вердикт**, коммит-гейт (сводка тестов, хэш
коммита), chain-level решения (риск / вне скоупа). Происхождение ревью пишешь ты сам —
в момент запуска и в момент вердикта. Мельче этих точек (каждая команда, каждый
прочитанный файл) не пиши — метки времени не должны превратить журнал в спам. Журнал —
тот же индекс, вынесенный в файл: полных текстов в нём нет. В рабочие коммиты issue его
не подмешивай (механика — на коммит-гейте, шаг 7).
Что это даёт: обрыв координатора не теряет состояние — по журналу видно, на каком этапе
цепочка и что уже сделано; происхождение ревью фиксируется в момент события — переписать
его задним числом незаметно не выйдет; финальный handoff собирается по журналу, а
кросс-проход и человек сверяют записи с фактами (коммитами, файлами). Честная граница:
журнал пишет сам координатор — это след для внешней проверки и барьер, а не гарантия;
гарантия — кросс-линия и человек.
## Контракт задания worker-у
- **одна цель из issue** и критерий «готово» (acceptance criteria);
- **зачем**: ссылки на PRD/спеку/смежное. Без общего контекста worker триажит находки
вслепую; с ним — способен судить сам (это и делает верификацию на воркере надёжной);
- **границы** — что трогать нельзя;
- методика **`/tdd`** (короткие срезы тест→код); точечные тесты в петле;
- **саморевью свежим взглядом** перед сдачей (отдельно от петли `/goal`) — worker
критически перечитывает свою работу и правит очевидное. Базовый минимум даже без
независимого ревью: ошибаются все;
- **триаж находок ревью**: независимое ревью организует координатор и присылает индекс
находок; worker каждую находку чинит или **аргументированно отклоняет** — через
контракт и тест, не голословно. Вопрос, который ревьюер поднял снова после
аргументированного отклонения, worker помечает в своём индексе флагом **«спор»**
с отсылкой к прежнему обоснованию;
- **формат отчёта — индекс**: находка/изменение, `файл:строка`, минимальный repro, какой
критерий; запрет вставлять diff и полный вывод тестов;
- **статус-контракт** `DONE / DONE_WITH_CONCERNS / BLOCKED / NEEDS_CONTEXT` — worker сам
стопорится на архитектурной неясности, а не додумывает молча. `BLOCKED` — только про
саму работу; недоступная в среде worker-а стендовая проверка (docker, сеть, стенд) —
это `DONE_WITH_CONCERNS` с фактом («docker отсутствует: <вывод>») и **готовой
командой**, которую координатор прогонит сам (не прошла и у координатора — он
фиксирует в журнале, где и как проверка прошла фактически);
- **запрет коммита** и **запрет отката чужого**.
Цель worker-а — **одна** (один issue = одна петля). Не запускай `/goal` на всю цепочку
сразу: от координатора нужна остановка на контрольных точках, а не «закрыть всё любой ценой».
## Контракт ревьюеру
Симметричен контракту worker-а, короче:
- **наблюдатель с полными инструментами**: код читает сам, докладу worker-а не верит;
может запускать тесты, воспроизводить находки, смотреть документацию и сеть —
верификация требует инструментов. Ограничение — **промптом, не урезанием субагента**:
не менять репозиторий и среду (правки файлов, git-операции, установка пакетов —
нельзя), так в дереве нет второго писателя;
- **зачем**: те же ссылки на issue/PRD/спеку, что у worker-а, плюс фокус проверки
(по задаче или по коду); на гейте — явная цель по двум семействам, слепым для одной
линии: межмодульные инварианты и доменные инварианты данных (например, форма
распределений, если она важна проекту);
- **формат отчёта — индекс**: находка, `файл:строка`, какой критерий нарушен,
минимальный repro; вердикт одной строкой (`APPROVED` / `CHANGES_REQUESTED`); запрет
вставлять diff, полный вывод тестов и нарратив рассуждений;
- модель и уровень мышления задаёт координатор при запуске (см. «Режимы ревью»).
Готовый шаблон промпта — [references/reviewer-prompt.md](references/reviewer-prompt.md)
(адаптация из `claude-team-review`); скилл самодостаточен, внешних файлов не требует.
## Режимы ревью — рычаг стоимости
Две проверки **разными субагентами** — по задаче и по коду — ловят непересекающиеся
множества находок, но это расход токенов (бюджет; в твой контекст при этом идут только
индексы находок). Плати по риску, не на каждой мелочи:
- **тривиальный issue** — независимое ревью не зови; хватает петли `/goal` плюс
саморевью из контракта (в среде без вложенных субагентов внешняя проверка готовности
внутри `/goal` тоже недоступна — саморевью остаётся единственной проверкой, это
осознанная цена режима);
- **обычный (дефолт)** — один прогон независимого ревью **по коду**; соответствие
issue/спеке держит петля `/goal`;
- **рискованный / гейт** — два прогона разными субагентами: **по задаче** (сделал ли то,
что в issue/спеке) + **по коду** (тонкие баги, межмодульное).
Порог риска: state, версионирование и сериализация; формат данных; интеграция с внешним
сервисом — особенно разрушающие пути (синхронизация, удаление); защитные проверки, на
которые полагаются другие части системы, — всё, где ошибка означает «проверка молча
пропускает» (fail-open); статистические модели; честность документации; межмодульные
инварианты. Ниже порога: документация и учебные материалы, простая обвязка сборки,
локальные правки без новых инвариантов. (Калибровка по прогонам 2026-07-04/05: issue
с синхронизацией внешнего сервиса и fail-open проверками, пущенный обычным режимом,
собрал больше всех кругов — его класс надо было опознать на шаге 3.)
**Две ручки стоимости, не одна.** *Модель по роли* — какая модель на какой роли:
проверка по задаче тянет дешёвую, финальное chain-ревью требует сильную. *Уровень
мышления* — отдельная ось, глубина рассуждений той же модели. Три относительных уровня,
без привязки к именам: **экономный** (worker на обычном issue — реализацию с тестовой
сеткой страхуют рубежи ниже по течению; task-lens ревью, диагностика, разбор логов),
**рабочий** (любое code review — пропуск ревьюера не ловит никто, глубина важнее именно
здесь; worker на гейтовом issue), **усиленный** (эскалация споров по политике кругов,
межмодульные и доменные инварианты, финальное chain-ревью). Соответствие ролей конкретным моделям и уровням
хоста — датированная таблица [references/effort-levels.md](references/effort-levels.md);
в логике скилла имён нет, вышла новая модель — правится таблица, не правила. Уровень
выбирай по роли и классу issue из таблицы, дефолтного уровня «для всех» нет; вверх —
только по триггеру (шаг 5), не «на всякий случай». Хост без
управляемых уровней: эскалация вырождается в свежего субагента — слабее, но не пусто.
## Классификация находок
Per-finding триаж («реально / ложно / чинить») делает **worker** — у него контекст кода
и «зачем» из контракта; **не переписывай его**. Ты решаешь только **на уровне цепочки**,
читая индекс:
- **записать риском** — важно помнить, текущий коммит не блокирует (реестр рисков цепочки);
- **вне скоупа** — пригодится позже, отдать в следующий issue.
## Финал цепочки
Пока п.1–3 не выполнены, цепочка **не завершена** — и каждый твой отчёт пользователю
обязан явно это называть: статус («финальный гейт, не done») и остаток по пунктам.
Промежуточный статус, звучащий как финальный отчёт, — дефект коммуникации: пользователь
решит, что работа закончена, и уйдёт. Словарь статусов для отчётов: «финальный гейт,
не done: осталось …» → «done по coordinator-loop: handoff есть, дерево чистое,
субагенты закрыты»; открытые внешние риски (кросс-линия, ручная приёмка) перечисляй
и в done-отчёте. **APPROVED финального ревью — ещё не завершение цепочки.**
1. **Приведи документы до ревью.** Обнови PRD/трекер: сделанное — в сделанное, остаток
актуален, статусы issue переведены (шаг 7). Иначе финальное ревью предсказуемо найдёт
этот дрейф вместо настоящего.
2. **Ревью на соответствие замыслу** (рекомендуется для любой нетривиальной цепочки).
Один прогон субагента-ревьюера **сильной моделью на усиленном уровне** (обе ручки
вверх — см. «Режимы ревью»): читает **всю** реализацию против **исходной спеки/PRD**
и проверяет, не разъехалась ли поэтапная реализация с замыслом. Возвращает индекс.
Один прогон добавляет к общей стоимости немного, а ловит дрейф, который per-issue
срезы не видят — ни одно ревью отдельной задачи не сверяло целое с целым замыслом.
Rerun'ы финального ревью — по общему правилу продолжения: точечные правки проверяет
**тот же** ревьюер узким вопросом, свежая xhigh-сессия на каждый круг не нужна.
**Кто чинит находки финального ревью — по классу файла.** Процессные файлы
(`.scratch/<feature>/…`: остаток и статус PRD фичи, чекбоксы и статусы issue,
журнал) — чини сам: это твоя зона, п.1 и существует, чтобы таких находок не было
вовсе. Файлы проекта (код, docs/, уроки, дашборды) — **fixer-субагент** с коротким
worker-контрактом (цель — закрыть перечисленные находки, границы, запрет коммита;
уровень — экономный для док-дрейфа, рабочий для кода): правя проектный файл сам,
ты читаешь его полный текст и становишься вторым писателем — ломаешь оба своих
инварианта, даже если правка на две строки. Кодовая находка финала — фактически
мини-issue: после fixer-а — rerun ревью по общему правилу продолжения.
**Триаж находок финального ревью — у fixer-а**, тем же механизмом, что у worker-а:
богатый контракт («зачем», спека) даёт право судить; отклонение — аргументированное,
через контракт и тест; спор — по политике кругов (шаг 5). Финальный ревьюер
ошибается так же, как обычный, — «поправь всё перечисленное» без права возразить
превращает его находки в приказы.
Это Codex (одна линия): берёт **охват** (целое против замысла), но не кросс-линию —
глубокие сквозные слепые пятна линии остаются на п.3.
3. **Handoff — условие выхода, а не пожелание.** Для нетривиальной цепочки цикл не
считается завершённым без записки, собранной **по журналу прогона, не по памяти**.
Место и формат — по конвенции репозитория для handoff (AGENTS.md, ADR), если она
есть; иначе `.scratch/<feature>/…`. Записка готовит **кросс-линейное ревью**: проход
модели другой родословной по написанному. Внутри Кодекса ревьюер — тоже Codex (одна
линия), а свежий контекст не снимает слепоту линии к сквозным дефектам. Сам проход
остаётся **вне** скилла — координатор его не выполняет; но для нетривиальной цепочки
он **рекомендован по умолчанию**, не опция: на практике кросс-проход находил
серьёзные расхождения после APPROVED ревьюера той же линии (эмпирика —
[references/rationale.md](references/rationale.md)). В записке:
- индекс построенного по issue (`файл:строка` ключевых стыков);
- **происхождение ревью по каждому issue** (режим, механизм, кто запускал, уровень,
вердикт) — из журнала;
- **отклонения от процесса — фактами**: что пробовали, что сделано вместо;
причины-предположения («недоступно», «не поддерживается») без проверки фактом — запрещены;
- **открытый риск**: межмодульные и доменные инварианты другой родословной
не покрыты → нацеленный бриф для кросс-прохода.
Даже если проход не запустят — дыра остаётся **видимым задокументированным риском**, а
не тихим слепым пятном. Запустят — ревьюер получит нацеленный бриф, а не холодное перечитывание.
## Чек-лист координатора
- [ ] Дерево чистое перед стартом; чужие правки согласованы, процессные отделены.
- [ ] Worker получил один issue + **зачем** (PRD/спека), границы, `/tdd`, индекс-формат,
триаж находок ревью (с флагом «спор»), статус-контракт, запрет коммита и отката;
саморевью свежим взглядом сделан.
- [ ] В мой контекст вошёл **только индекс** — ни diff, ни логов, ни нарратива ревью.
- [ ] Ревью-гейт: индексы находок пересланы worker-у дословно; rerun назначал я;
политика кругов соблюдена (спор → узкий усиленный гейт; больше четырёх кругов →
человек).
- [ ] Происхождение каждого ревью (механизм, кто запускал, уровень, вердикт) записано
мной в журнал в момент события; нет записи — ревью не засчитано.
- [ ] Журнал ведётся append-only по ходу, каждая запись с меткой `HH:MM`; точки записи
не пропущены.
- [ ] Chain-level классификация сделана по индексу (риск / вне скоупа).
- [ ] Коммит сделан координатором, diff узкий и в границах; полный тест-сет на гейте
прогнал я сам, шумные команды — через файл-лог: в контекст только сводка
(pass/fail + упавшие), не вывод.
- [ ] Статусы закрытых issue переведены в `done` и ушли docs-коммитом вместе с журналом.
- [ ] PRD/трекер приведены **до** финального ревью; ревью на соответствие замыслу —
сильная модель на усиленном уровне; дрейф от спеки проверен.
- [ ] Финальный handoff собран по журналу и по конвенции репозитория: происхождение
ревью по issue, отклонения фактами, бриф кросс-линейного ревью. Для нетривиальной
цепочки без него из цикла не выходить.
- [ ] Субагенты закрыты; `git status` чист на выходе.
## Подробнее
- [references/rationale.md](references/rationale.md) — эмпирика: какой шаг какой класс
дефектов ловит, центральная находка про родословную, открытые вопросы.
- `handoffs/` — экономия контекста (два пожара: контекст vs лимит), наблюдаемость и
непроверенные кандидаты-фиксы (файловый канал, демоушн закрытых issue, точечные тесты);
live-log наблюдаемости вырос в журнал прогона (2026-07-03); топология ревью под Codex,
политика кругов и уровни мышления — по боевому прогону (2026-07-04).
## Источники упомянутых скиллов
- `/brainstorm-with-docs`, `/to-issues`, `/tdd`, `/claude-team-review` — инженерные
скиллы (набор Matt Pocock и внутренние).
- `/goal` — встроенная петля Codex (план→код→тест; её внешняя проверка готовности в
среде без вложенных субагентов недоступна).
@@ -1,272 +0,0 @@
# Handoff: экономия контекста в coordinator-loop
Дата: 2026-06-14 MSK
Статус: рабочая договорённость по итогам сессии проектирования. В `SKILL.md` ещё не внесена.
## Зачем этот handoff
Сохранить выводы сессии, пока свежие. Разбирались, почему навык `coordinator-loop`
(координатор + субагенты) при длинной цепочке задач переполняет контекст координатора и дорого
стоит по токенам. Это снимок для продолжения в новой сессии, а не готовая спецификация: часть
решений принята, часть осталась открытой — так и записано. Здесь сознательно «чуть больше
информации, чем нужно»: для handoff недобор хуже перебора.
## Где что лежит (чтобы следующая сессия всё нашла)
- **Навык, источник-правды (git):** `~/dotfiles/agents/.agents/skills/coordinator-loop/`
(`SKILL.md`, `README.md`, `references/rationale.md`, `handoffs/`). Этот handoff — здесь.
- **Развёрнутая копия навыка** (её читает Claude Code): `~/.claude/skills/coordinator-loop/`.
Правки вносить в dotfiles, потом разворачивать обычным способом.
- **Побратим:** `handoffs/2026-06-14-observability-design.md` (рядом) — проектирование файлового
канала `coordinator-runs/`. **Свести с этим handoff, см. раздел «Связь».**
- **Прогон, на котором всё замеряли:** репозиторий
`~/sources/clickstream-ch-kafka-superset-demo`, фичи `feature-data-generator` (8 задач) и
`generator-model-time-startup-history` (6 задач). Вёлся как `/coordinator-loop` под Codex.
- **Сырьё диагностики:** `~/.codex/logs_2.sqlite`, `~/.codex/sessions/2026/`,
`~/.codex/goals_1.sqlite`. Внимание: файлы Codex **нумерованные и ротируются** — к будущей
сессии этих именно логов может уже не быть. Поэтому ключевые числа вынесены в этот handoff
ниже, чтобы не зависеть от сырья.
- **Prior art:** obra:superpowers, навык `subagent-driven-development` —
<https://github.com/obra/superpowers/tree/main/skills/subagent-driven-development>
(`SKILL.md`, `implementer-prompt.md`, `spec-reviewer-prompt.md`,
`code-quality-reviewer-prompt.md`).
## Что болит
У координатора дважды заканчивался контекст (по разу на каждый из двух прогонов), и работа съела
почти весь 5-часовой лимит подписки (по логам — около 3 ч 45 мин на втором прогоне).
## Диагностика
Главный метод и главный урок: **логи — истина, самоотчёт модели — нет.** Codex на прямой вопрос
искренне назвал виновником многословные отчёты субагентов, пересекающие границу. Логи это
опровергли. Модель не видит свой токен-счёт и сочиняет правдоподобную, но неверную картину.
Спасло то, что полезли в логи, а не поверили рассказу. Разбор логов делал **отдельный субагент
на дешёвой модели** (Sonnet), чтобы не забивать контекст основного агента сырьём. Этот приём
стоит повторять.
### Два разных пожара (мы их сначала склеивали)
**Пожар A — контекст координатора.** Горел НЕ от отчётов на границе субагент→координатор: они
были компактны (в среднем 2,6 КБ, максимум 7,3 КБ, всего 112 КБ за 44 возврата; полных diff и
логов тестов в них не было). Горел от того, что происходило **внутри сессии самого
координатора**:
- координатор **сам читал полные diff'ы**, чтобы проверить находки, — 19 diff'ов крупнее 10 КБ,
суммарно 413 КБ, независимо от сводок воркеров;
- сессия ревьюера-апрувера раздулась до 947 КБ за 21 ход, и **42 из 44 ожиданий `wait_agent`
завершились таймаутом** (опрос по кругу при том, что push-уведомления приходили).
Пик контекста координатора: 199 307 токенов из окна 258 400 (77 %), одна компактизация.
**Пожар B — 5-часовой лимит** (отдельная от контекст-окна вещь — это суммарный расход токенов):
- **103 прогона полного тест-сета** (каждый ~40 КБ вывода, до 16 877 токенов за один) — главный
потребитель;
- `/goal`-петли воркеров по ~400K токенов на задачу (задачи 05 и 06; 30 и 23 минуты);
- 13 Docker-сборок и накладные расходы опроса.
### Процессные счётчики (прогон issues 02–06)
| Метрика | Значение |
|---|---|
| Обработано issues | 5 (02–06) |
| Воркеров запущено | 5 + 2 `/goal`-петли |
| Вызовов независимого ревьюера | 7 (4 explorer + 3 default) |
| Кругов ревью на задачу (среднее) | 1,4 |
| Кругов правок после ревью | ~2 на задачу |
| Прогонов полного тест-сета | 103 |
| Docker-сборок | 13 |
| Всего exec_command | 996 |
| Общая длительность прогона | ~3 ч 45 мин |
### Главные потребители (по убыванию)
1. Полный вывод тестов внутри воркеров — 103 вывода по ~40 КБ.
2. Повторное чтение diff'ов координатором — 19 outputs >10 КБ, 413 КБ.
3. Опросный цикл ревьюера-апрувера — 21 ход, сессия 947 КБ.
4. `/goal`-петли (issues 05 и 06) — ~400K токенов каждая.
5. Отчёты субагентов на границе — низкое влияние (среднее 2,6 КБ).
### Гипотезы и вердикты
| # | Гипотеза | Вердикт | Доказательство |
|---|---|---|---|
| H1 | контекст рос из-за отчётов субагентов | **опровергнута** | средний отчёт 2,6 КБ, max 7,3 КБ; diff и логи в них не входили |
| H2 | координатор перечитывал то, что воркер уже сообщил | **подтверждена** | 19 полных diff в сессии координатора, 413 КБ |
| H3 | компактизация теряла важное и его восстанавливали | частично | после компактизации координатор перечитывал статусы issue и git-историю |
| H4 | полный режим ревью применялся избыточно | частично, переосмыслена | дело не в самом ревью, а в polling-overhead: 42/44 таймаута, сессия 947 КБ |
| H5 | лимит съели `/goal` и тест-прогоны | частично | 103 полных ретеста — главное; `/goal` был только на 05–06 |
### Что Codex дал верно, несмотря на промах с локацией
Его интроспекция угадала **форму** балласта (материал, который должен был сжиматься после своего
момента) и продиктовала точные компактные форматы. Он перечислил, что таскал зря: полные выводы
тестов; полные diff'ы крупных файлов; полные отчёты ревьюеров (вместо классифицированных
находок); полные отчёты воркеров после приёмки issue; тексты уже закрытых issue; собственные
пользовательские апдейты («запускаю», «жду воркера»); полные build/compose/ClickHouse-логи;
длинные фрагменты спек после первичного чтения.
Ключевой ответ про классификацию находок: отчёта субагента **хватало для маршрутизации**
(blocker / риск / docs-мелочь / ложная тревога), но для финального решения «чинить или нет» по
**межмодульному инварианту** он **всё равно лез в код** — отчёт был индексом и аргументацией, а
не заменой чтения. Примеры мест, куда он шёл: `service.py:_is_startup_history_state`, порядок
записи manifest/state, путь восстановления state→manifest→state→live load→startup-marker→
fallback, SQL-воронка для доказательства. Вывод: лучший отчёт — это **индекс** (находка,
`файл:строка`, минимальный путь воспроизведения, какой критерий нарушен, что проверить в коде),
а не нарратив.
Про `wait_agent`: таймауты стояли 600000 мс (10 мин) для воркера/ревьюера, 900000 мс (15 мин)
для долгих стендовых задач, 180000 мс (3 мин) после запроса промежуточного статуса.
Push-уведомления (`<subagent_notification>`) приходили, но Codex им не доверял и ждал заново —
отсюда лишний опрос. Сам прописал лечение: на уведомление **сразу обрабатывать результат**, а не
запускать `wait_agent` повторно; после `spawn_agent` не делать длинный `wait_agent`, если есть
локальная работа; один разумный статус-таймер вместо polling-цикла.
## Prior art: obra:superpowers `subagent-driven-development`
Чужой навык глубоко проработан (в том числе по экономии токенов и выбору минимально достаточной
модели) и стоит на той же идее: оркестратор не делает работу сам, держит контекст чистым,
субагенты — «свежий контекст». По части ядра мы переизобретали его.
Как у них устроено: на задачу один эфемерный **implementer** → два последовательных ревьюера
(spec-review → quality-review) → фиксы → следующая задача. Задачи строго последовательны.
Implementer возвращает компактный статус `DONE / DONE_WITH_CONCERNS / BLOCKED / NEEDS_CONTEXT` +
изменённые файлы + итог тестов (без полного вывода). Spec-reviewer читает реальный код и не
доверяет докладу implementer'а. Модель выбирается по роли (дешёвая → стандартная → сильная).
**Берём оттуда:**
- **Ревьюер читает код сам и не верит докладу** — жёсткое правило, убивает Пожар A по построению
(координатор перестаёт быть верификатором).
- **Модель по роли.** Поправка под нас: наш implementer «толстый» (`/goal`), ему нужна сильная
модель; дешёвая выигрывает на субагенте-верификаторе простых находок, docs-проверках и
диагностике (разбор логов мы и сделали на Sonnet).
- **Статус-контракт воркера** `DONE / DONE_WITH_CONCERNS / BLOCKED / NEEDS_CONTEXT` — протокол
эскалации, которого у нас нет: воркер сам стопорится на архитектурной неясности.
- **Качественная стадия ревью** (хорош ли код, межмодульные инварианты).
**Не берём (сознательно другой выбор):**
- У superpowers **толстый план** (~2000 строк, почти реализация) + тонкий механический
implementer. У нас **тонкий issue** (acceptance criteria) + толстый `/goal`, который сам
додумывает реализацию и самопроверяется. Для учебного репозитория наш путь лучше: issue
читаемы менти, реальное решение живёт в петле, нет огромного плана.
- Их **спек-стадия ревью** у нас по большей части избыточна: «выполнили ли спеку» уже проверяет
петля `/goal`. Берём только качественную линзу.
> **Поправка 2026-07-02 (по опыту эксплуатации).** Отменено. На практике два лена ревью
> **разными субагентами** (task-lens «сделал ли то, что в issue/спеке» + code-lens «качество
> и межмодульное») находят **непересекающиеся** множества — и не так редко. Держим оба, но
> как **полный режим** (по порогу риска), а task-lens можно на дешёвой модели. См. раздел
> «Режимы ревью» в `SKILL.md`.
Вывод: `coordinator-loop` остаётся отдельным навыком (своя идентичность — цепочка issue из
`/to-issues`, гейт-классификация находок, владение коммитом). Заимствуем механику делегирования,
а не план-центричную философию.
## Решения сессии — с адресами правок в `SKILL.md`
**Принято (дёшево, метрически обосновано):**
1. **Координатор не читает diff ради проверки находки.** Чтение кода уходит ревьюеру-субагенту:
он читает код сам и возвращает только вердикт. Чинит Пожар A (413 КБ). Куда править:
- «Цикл на одну задачу», **шаг 8 «Свой sanity-check»** — сейчас список включает чтение
(дефолты, compose/конфиги, документы, acceptance criteria, git status, тесты). Сузить:
sanity-check проверяет связи, но **не читает diff/код ради верификации находки** — это
работа ревьюера.
- «Роли», пункт **Координатор** («делает sanity-check») — уточнить так же.
- «Роли», пункт **Независимый ревьюер** — добавить жёсткое правило superpowers: ревьюер
читает код сам и **не верит докладу**; именно он несёт верификацию.
- «Когда звать независимого ревьюера» — это и есть порог, на котором верификация (с чтением
кода) уходит в субагента; для межмодульных инвариантов — обязательно.
2. **Доверять push, не крутить `wait_agent` по кругу.** Пришло `<subagent_notification>` — сразу
обрабатывать; один разумный статус-таймер вместо polling-цикла; после `spawn_agent` не делать
длинный `wait_agent`, если есть локальная работа. Возможно, самый дешёвый одиночный выигрыш
(сток 947 КБ почти случайный, не дизайн). Куда править — **открытый вопрос** (см. ниже):
`wait_agent` — рантайм Codex, а `SKILL.md` host-agnostic. Связано со «Цикл», **шаг 4
«Дождись отчёта»**.
**Намеренная цена — НЕ трогать:**
- `/goal`-петля (~400K токенов/задача) — сознательный обмен на качество решения;
- независимое межмодульное ревью на пороге риска;
- декомпозиция «тонкий issue + толстый `/goal`».
**Кандидаты-фиксы (не приняты, проверить на прогоне) — с адресами:**
- **Точечные тесты в петле, полный тест-сет — только на гейте.** Главный потребитель токенов
(103 ретеста). Риск пропустить межмодульный регресс в середине петли ловит гейт + межмодульный
ревьюер. Куда: «Контракт задания worker-у» (методика тестов) и шаг 12 «Коммит» (полный сет
перед коммитом).
- **Отчёты в формате индекса, а не нарратива.** Куда: «Контракт задания worker-у» →
«формат финального отчёта»: находка/изменение, `файл:строка`, минимальный repro, какой
критерий, что проверить; запрет на вставку diff и полных логов.
- **Статус-контракт воркера** `DONE / DONE_WITH_CONCERNS / BLOCKED / NEEDS_CONTEXT`. Куда:
«Контракт задания worker-у».
- **Демоушн закрытых issue** в компактную durable-форму (`summary`), чтобы компактизация её не
уничтожала. Куда: новый короткий раздел + связь с observability-handoff (`summary.md`).
- **Файловый канал в общем дереве** для обмена находками (оркестрация по ссылке, а не payload
через контекст). Куда: см. observability-handoff (`coordinator-runs/`).
- **Модель по роли.** Куда: «Облегчённый и полный режим» / «Контракт задания worker-у».
- **Только качественная стадия ревью** (спек несёт `/goal`). Куда: «Роли» (ревьюер) + «Когда
звать независимого ревьюера». **(Отменено — см. поправку 2026-07-02 выше в разделе «Prior
art»: два лена ревью держим, спек-линза не избыточна.)**
## Связь с handoff про наблюдаемость
Побратим `2026-06-14-observability-design.md` (рядом) уже спроектировал файловый канал
`coordinator-runs/` (live-логи + `summary.md`, чтение по требованию, координатор не ведёт лог
сам). Это **тот же** файловый канал, что нужен кандидатам-фиксам «демоушн» и «файловый канал».
**Свести в один дизайн, не строить две файловые системы.** Оба не реализованы — слить легко.
## Открытые вопросы (с моей рекомендацией, но не закрыты)
1. **Объём первого захода.** A — полная переделка сразу; B — внести только два дёшевых правила,
остальное пометить «проверить на следующем прогоне». Рекомендация: **B** (держит навык от
разрастания).
2. **Кто верифицирует находку** — автор-воркер со своим знанием кода и проверяемым обоснованием,
или свежий ревьюер? Склонялись к автору на знании + независимый на пороге риска. Не закрыто.
3. **Где живёт правило 2 (push/`wait_agent`).** Это рантайм Codex, а `SKILL.md` host-agnostic.
Куда: короткий раздел «Синхронизация с субагентами» в `SKILL.md` с пометкой «Codex-specific»,
либо `references/rationale.md`, либо привычка оператора. Не решено.
4. **Слить ли два handoff** (этот и про наблюдаемость) в один дизайн до правки `SKILL.md`?
Рекомендация: да.
5. **Кросс-хост.** Файловый канал отчасти оправдан тем, что host-agnostic (Codex и Claude Code).
Если на деле только Codex — обоснование слабее. Уточнить требование.
6. **Носитель для проверки.** Фича почти закрыта (осталась issue 06), новой цепочки под рукой
нет. На чём гонять проверочный прогон — пока неясно; числа-ориентиры ниже остаются baseline
на будущую фичу.
## Числа-ориентиры для следующего прогона
Чтобы измерить, помогли ли фиксы, а не поверить на слово:
- пик контекста координатора: было 77 % окна (199K/258K) — должно заметно упасть;
- собственные diff-чтения координатора: было 413 КБ — должно стремиться к нулю;
- сессия ожиданий/опроса: было 947 КБ при 42/44 таймаутах — push обрабатывается, цикла нет;
- прогоны полного тест-сета: было 103 — точечные в петле, полный только на гейте.
## Что ещё не сделано
- Решения не внесены в `SKILL.md`.
- Кандидаты-фиксы не приняты и не проверены на реальном прогоне.
- Два handoff не сведены.
- Не решён объём первого захода (вопрос 1).
- Не решено, где живёт правило 2 (вопрос 3).
## Рекомендуемые следующие шаги
1. Решить объём первого захода (A vs B).
2. Свести этот handoff с handoff про наблюдаемость в один дизайн.
3. Внести минимум в `SKILL.md` (два принятых правила) по адресам выше, сверяясь с superpowers как
prior art.
4. На ближайшей новой фиче замерить числа-ориентиры; по факту резать или добавлять.
5. Опционально — лёгкая дивергенция/конвергенция через `brainstorm-with-docs`, если решение
захочется поискать шире. Возможно, и не понадобится: два дешёвых фикса метрически очевидны.
## Suggested skills
- `write-a-skill` — при переносе правил в `SKILL.md`, чтобы не раздувать основной файл.
- `brainstorm-with-docs` — если делать полноценный заход с дивергенцией/конвергенцией.
- `conventional-commits` — для коммитов изменений в навыке.
@@ -1,229 +0,0 @@
# Handoff: наблюдаемость coordinator-loop
Дата: 2026-06-14 20:00 MSK
Статус: рабочая договорённость, ещё не внесена в `SKILL.md`
## Зачем этот handoff
Нужно передать на другую машину выводы обсуждения про минимальную наблюдаемость
работы субагентов в `coordinator-loop`. Это не runtime-инструкция: обычный агент
при запуске скилла читает `SKILL.md`, а не этот файл. Если решение надо проверить
на реальном цикле, короткий минимум придётся добавить в `SKILL.md`.
## Контекст
- Скилл: `/home/dementev/dotfiles/agents/.agents/skills/coordinator-loop/`.
- Основная инструкция: `SKILL.md`.
- Человеческое описание и границы: `README.md`.
- Обоснование процесса: `references/rationale.md`.
- В `dotfiles` уже были сделаны коммиты:
- `aa3e362 fix(skill): уточнена обработка чужих правок координатором`
- `8f6d9de docs(skill): уточнено назначение README coordinator-loop`
## Проблема
При долгой работе worker-а координатор и пользователь плохо видят, что происходит.
Чатовый контекст ненадёжен: координатор может уйти в compaction, потерять детали
старта команды или не понять, сколько длится ожидание. При этом нельзя превращать
координатора в секретаря, который постоянно ведёт подробный журнал.
Нужна минимальная наблюдаемость:
- понять, какой issue сейчас в работе;
- увидеть последнюю фазу и последнюю нетривиальную команду;
- восстановить примерную длительность фаз;
- не спамить координатора сообщениями;
- не смешивать процессный шум с рабочими коммитами.
## Принятый дизайн
Использовать feature-local файлы рядом с локальными issues:
```text
.scratch/<feature>/coordinator-runs/
live/ # временные live-логи, в .gitignore
<NN>-<issue-slug>.md
summary.md # короткий итог, можно коммитить
```
Причина: `.scratch/<feature>/` уже является местом локальных issues. Наблюдаемость
должна жить рядом с ними, а не в глобальной папке.
## Git-правило
Не игнорировать весь `.scratch/`: в этом репозитории `.scratch/` используется как
локальный issue tracker и коммитится. Игнорировать только живой шум:
```gitignore
# Временные live-логи coordinator-loop.
.scratch/*/coordinator-runs/live/
```
`summary.md` можно коммитить как итоговый артефакт прогона.
## Live-log
Один файл на issue. Этого достаточно: цикл по feature одноразовый, а если worker
упал и был запущен заново, новый worker продолжает дописывать тот же файл.
Координатор перед запуском worker-а:
1. создаёт `coordinator-runs/live/<NN>-<issue-slug>.md`;
2. передаёт путь worker-у в задании;
3. передаёт тот же путь reviewer-у, если reviewer нужен.
Worker и reviewer:
- только дописывают в конец файла;
- не обязаны читать предыдущие записи;
- ставят текущую временную метку в каждой записи;
- не пишут рассуждения и полный вывод тестов.
Координатор:
- знает путь к live-log;
- читает его только при необходимости: пользователь спросил статус, worker долго
молчит, произошёл фазовый переход, нужен контекст перед коммитом;
- не обязан вручную поддерживать live-log.
## Формат записи
Append-only Markdown-блоки. Они читаются глазами лучше, чем строки с `|`, и не
ломаются от команд с кавычками или длинными путями.
```md
### 2026-06-14 19:52 MSK — worker — tests
- event: started
- command: `uv run pytest tests/generator/test_state.py`
- note: checking state v2 restore cases
```
```md
### 2026-06-14 19:58 MSK — worker — tests
- event: passed
- result: 8 passed
- next: self-review
```
Минимальные поля:
- заголовок: время, роль, фаза;
- `event`: `started`, `progress`, `passed`, `failed`, `blocked`, `heartbeat`,
`restarted`, `final`;
- `command`: команда, если запись про запуск проверки;
- `result`: короткий итог, если есть;
- `next`: следующий шаг, если понятен;
- `note`: короткая поясняющая заметка, если нужна.
## Что логировать
Обязательные события:
- старт работы по issue;
- перезапуск worker-а после сбоя: `event: restarted`;
- смена фазы;
- начало нетривиальной команды;
- результат нетривиальной команды;
- блокер;
- финальный отчёт готов.
Нетривиальная команда: тесты, линтеры, сборка, Docker, миграции, симуляции,
обращение к внешнему сервису, обработка заметного объёма данных, либо любая
команда, длительность которой агент не уверен как оценить.
Не логировать:
- каждую мелкую правку;
- каждый прочитанный файл;
- `rg`, `sed`, `ls`, `git diff`, `git status` и похожие быстрые операции;
- внутренние рассуждения;
- полный вывод тестов.
## Heartbeat
Heartbeat нужен как best-effort, а не строгий таймер.
Если работа длится больше примерно 5 минут и агент находится между командами или
может безопасно прерваться на запись, он дописывает:
```md
### 2026-06-14 19:55 MSK — worker — tests
- event: heartbeat
- note: still working; waiting for simulation/test result
```
Не строить отдельный таймер или фоновый процесс ради heartbeat. Если запущена
блокирующая команда, достаточно записи `event: started` перед ней.
## Почему не state.md
Обсуждали отдельный `state.md`, который хранил бы текущий issue, фазу и ожидаемый
следующий шаг. Отказались: если `state.md` ведёт координатор, это добавляет
нагрузку и снова делает координатора секретарём. Если координатор ушёл в
compaction, восстановиться можно из `git status` и хвоста последнего live-log.
Первый заход: без `state.md`.
## Почему без run-id
Обсуждали `live/<run-id>/<issue>.md`. Отказались: работа над feature идёт как
один проход по issues. Старые прогоны не должны накапливаться. Если worker упал,
новый worker пишет `event: restarted` в тот же файл issue.
## Summary
После завершения issue координатор переносит короткий итог в `summary.md`. Live-log
остаётся временным и игнорируемым.
Формат раздела на issue:
```md
## <NN>-<issue-slug>
Commit: <hash> <subject>
Result:
- <что наблюдаемо работает>
Checks:
- `<command>` — passed/skipped
Review:
- self-review: <summary>
- independent review: <summary or not run>
Risks:
- <none / list>
```
## Что ещё не сделано
- Это решение пока не внесено в `SKILL.md`.
- `.gitignore` проекта пока не обновлён правилом для `live/`.
- Не было реального прогона coordinator-loop с такой наблюдаемостью.
- Нужно решить, делать ли наблюдаемость всегда или только по явной просьбе / для
длинных задач. Текущая рекомендация: включить как лёгкий стандарт для
coordinator-loop, потому что нагрузка на координатора мала.
## Рекомендуемые следующие шаги
1. Внести короткий раздел «Наблюдаемость» в `SKILL.md`, иначе агент при запуске
`/coordinator-loop` не обязан читать этот handoff или README.
2. Добавить в `.gitignore` правило `.scratch/*/coordinator-runs/live/`.
3. На ближайшем реальном прогоне проверить, не создаёт ли live-log лишний шум.
4. После прогона уточнить `SKILL.md`: оставить, упростить или сделать режим
опциональным.
## Suggested skills
- `write-a-skill` — при переносе правил в `SKILL.md`, чтобы не раздувать основной
файл и оставить runtime-инструкции короткими.
- `conventional-commits` — если изменения в скилле или `.gitignore` будут
коммититься.
- `grill-me` — если нужно ещё раз стресс-тестировать развилки: always-on vs opt-in,
формат summary, порог heartbeat.
- `coordinator-loop` — только при реальном прогоне issues после обновления
`SKILL.md`.
@@ -1,52 +0,0 @@
# Handoff: рефрейм coordinator-loop вокруг «индекс, не полный текст»
Дата: 2026-07-02 MSK
Статус: сделано и принято независимым ревью (`/claude-team-review`, 2 раунда, APPROVED). Не закоммичено на момент написания записки — коммит идёт следом.
## Зачем этот handoff
Зафиксировать, что и почему сделано в сессии переписывания навыка, пока свежо. Сам дизайн живёт в `SKILL.md` — сюда его **не** дублирую, только решения, их обоснование и что осталось. Продолжает линию июньских handoff'ов (`2026-06-14-*`): те диагностировали болезнь, эта — лечение.
## Что сделано
`SKILL.md` переписан целиком (194 → 159 строк). Ядро — одно ведущее правило: **координатор работает по индексу, а не по полному тексту** (полный diff / вывод тестов / нарратив ревью в его контекст не входят). Это прямое лекарство от главной болезни из июньского handoff'а «context-economy»: контекст координатора пух от вычитывания сырья, компактился, дальше косяки.
Подробности — в `../SKILL.md`. README и `references/rationale.md` подровнены под новый дизайн; в июньский `2026-06-14-…-context-economy.md` вписана датированная поправка (см. ниже).
## Ключевые решения (и чем обоснованы)
- **User-invoked** (`disable-model-invocation: true`). Навык всегда зовут руками — авто-инвокация была no-op'ом, платившим context load за фразу «не включай меня».
- **Per-finding триаж делегирован `claude-team-review`** (single source of truth). У координатора только chain-level корзины (риск / вне скоупа). Проверено на опыте: у team-review триаж встроен, «не применять слепо» — тоже.
- **Ревьюера запускает сам worker** внутри `/goal`, не координатор → нарративы ревью не доходят до координатора. Чистота контекста «по построению».
- **Режимы ревью по риску/стоимости**: тривиальный → `/goal` + отдельное саморевью свежим взглядом; обычный → один прогон team-review по коду; риск/гейт → два прогона **разными субагентами** (по задаче + по коду), проверку по задаче можно на дешёвой модели. Два лена ловят непересекающиеся находки — **опровергает** прежний наклон июньского handoff'а выкинуть спек-линзу (там вписана поправка).
- **Коммит у координатора**; полный тест-сет на гейте, но в контекст координатора идёт **только сводка** (код возврата + имена упавших), не вывод. Эту дыру (полный тест-лог возвращал Пожар A в координатора) поймал ревьюер — не я.
- **Финал цепочки**: (1) рекомендованное ревью на соответствие замыслу **сильной моделью** — целое против исходной спеки, ловит дрейф, который per-issue срезы не видят; это Codex (одна линия), берёт охват, не кросс-линию; (2) **handoff**, который готовит опциональный ручной кросс-линия проход Клодом и фиксирует непокрытую родословную как видимый риск.
- **Родословная**: внутри Кодекса только модели Codex → team-review из-под Codex = ревьюер той же линии. Кросс-линия — это отдельный рабочий процесс пользователя (Клод пишет проектные документы, Кодекс выполняет), взаимодействие агентов **намеренно не автоматизировано**. Поэтому кросс-линия остаётся ручным опциональным проходом вне скилла.
## Процесс проверки (сам по себе — рабочий приём)
Дешёвый self-проход «проверь свежим взглядом» до дорогого ревью — сокращает список находок; сохранён в память `feedback_fresh_eyes_first_pass.md`. Полное ревью — через `/claude-team-review` (Codex-эквивалент `/adversarial-review` для опинейтед-скиллов не годится — «улучшит» намеренный дизайн). Ревью нашло 3 important + 4 minor, все закрыты, вердикт APPROVED.
## Что не сделано / открытые вопросы
- **Не замерено на реальной цепочке.** Числа-ориентиры из июньского handoff'а (пик контекста 77 %, 413 КБ diff-чтений, 103 полных тест-прогона) — baseline; рефрейм под них ещё не гонялся. Кандидат-носитель рядом: `.scratch/training-feedback-loop/issues/` в проекте pole-equipment-detector.
- **Кандидаты-фиксы намеренно не внесены в SKILL.md** (непроверены): файловый канал `coordinator-runs/`, демоушн закрытых issue в `summary.md`, точечные тесты в петле vs полный сет на гейте как метрика, live-log наблюдаемости. Живут в июньских handoff'ах — вносить по факту замера, не вслепую.
- Развилка «автор-воркер vs свежий ревьюер как верификатор нериск-находок» решена в пользу воркера с богатым контрактом — но это выбор, не доказанная истина.
## Где что лежит
- Источник-правды: `~/dotfiles/agents/.agents/skills/coordinator-loop/` (git, ветка master, remote gitverse). `~/.claude/skills` — симлинк на `~/dotfiles/agents/.agents/skills` (тот же инод), правка одной точки меняет обе.
- Память сессии: `~/.claude/projects/-home-ddmitry-sources-pole-equipment-detector/memory/feedback_fresh_eyes_first_pass.md`.
## Рекомендуемые следующие шаги
1. Прогнать реальную цепочку (`.scratch/training-feedback-loop/issues/`) под новым `coordinator-loop`, замерить числа против baseline.
2. По факту замера решить судьбу кандидатов (файловый канал / демоушн / метрика тестов).
3. Если понадобится развести наблюдаемость и файловый канал — свести два июньских handoff'а в один дизайн (их «Связь» это предусматривала).
## Suggested skills
- `coordinator-loop` — при реальном прогоне цепочки для замера.
- `writing-great-skills` — если по итогам замера править `SKILL.md` (держать от разрастания).
- `claude-team-review` — ревью правок навыка (не `adversarial-review`).
- `conventional-commits` — для коммитов изменений навыка.
@@ -1,211 +0,0 @@
# Handoff: топология ревью под Codex, эскалация и дисциплина финала
Дата: 2026-07-04 MSK
Статус: пакет правок по итогам боевого прогона; прошёл адверсарное ревью свежим
субагентом (2 блокера, 7 important, 4 minor — все закрыты правкой этого текста).
Внесено в `SKILL.md` и закоммичено той же сессией; итог сессии — в
`2026-07-04-rework-session-claude-line.md` (рядом).
## Зачем этот handoff
Зафиксировать решения по итогам первого боевого прогона переписанного скилла
(цепочка issue 07 → 11 фичи `generator-model-time-startup-history`,
репозиторий `~/sources/clickstream-ch-kafka-superset-demo`) до правки `SKILL.md`.
Источник фактов — журнал прогона
`.scratch/generator-model-time-startup-history/coordinator-journal.md`
(коммиты `d2899e9`, `5081319`, `0a240e3`): отдельного handoff'а координатор не
написал, диагностика лежит в журнале.
## Что показал прогон
Хорошее: дисциплина «индекс, не полный текст» удержалась; журнал фиксировал
происхождение ревью в момент события; финальное chain-ревью поймало дрейф PRD.
Контекст координатора, по впечатлению пользователя, не страдал (прерывания и
дополнительные вопросы прогон пережил) — но это наблюдение, не замер:
числа-ориентиры июня (пик % окна, объём diff-чтений, число тест-прогонов) на
этом прогоне не сняты, сырьё в `~/.codex` ротируется. Пункт остаётся открытым.
Проблемы:
1. **Worker не может запускать субагентов в среде Codex.** У субагентов нет
`multi_agent_v1.spawn_agent`; `tool_search` не находит ни spawn, ни
`claude-team-review`. Проверено дважды: read-only диагностом и отдельным
диагностом с `agent_type=worker` (журнал, записи «Диагностика nested review
tools»). Ключевое допущение дизайна («ревьюера запускает сам worker внутри
`/goal`») в этой среде не работает. Координатор импровизировал: запускал
ревьюеров сам и возвращал worker-у индекс находок — сработало.
2. **Пинг-понг ревью без ограничителя.** Issue 07 прошёл пять кругов code
review; спорная находка (raw-сериализация) продержалась два круга, пока
координатор не изобрёл «узкий гейт на xhigh» по одной находке (усиленный
уровень понадобился на двух кругах из пяти плюс на финальном chain-ревью —
и по делу). Ограничителя и правил эскалации в скилле нет — открытые вопросы
rationale стали боевыми.
3. **Статус-контракт смешивает «работа заблокирована» и «шаг процесса
недоступен»**: worker по issue 11 вернул `BLOCKED` при готовой реализации —
заблокирован был только механизм ревью. Со стендовыми проверками так же:
docker недоступен в песочнице worker-а (07).
4. **Финальный handoff не написан** — шаг «Финал цепочки, п.2» тихо выпал,
бриф для кросс-прохода никто не получил.
5. **Закрытие issue делалось, но внутри feature-коммитов**: статусы 07 и 11
переведены в `done` в коммитах `3c38465` и `4f8f992` — процессная правка
смешана с рабочим diff. Прошлая цепочка (01–06) хуже: статусы остались
неверными («ready-for-human от глючившего координатора», триаж 2026-07-04).
Правила в скилле нет — каждый прогон решает по-своему.
6. **Журнал без меток времени** — длительность этапов и границы issue по
времени не восстановить. Метка времени была в дизайне live-log
(`2026-06-14-observability-design.md`) и потерялась, когда live-log «вырос
в журнал» (2026-07-03).
## Решения (пакет правок)
1. **Датированный факт среды вместо preflight-пробы.** В `SKILL.md` — короткая
врезка: «Среда Codex (проверено 2026-07-04): worker не может запускать
субагентов; ревьюеров запускает координатор». Не перепроверять каждый
прогон — только при сигнале обратного (например, `tool_search` нашёл spawn).
Гонять диагностику на каждой цепочке — жечь токены и время.
2. **Топология ревью: запуск у координатора, триаж у worker-а.**
- Координатор запускает reviewer-субагентов-**наблюдателей** и пересылает
worker-у индекс находок дословно, без своих интерпретаций (он не читает
код и судить не может). Наблюдатель — не read-only субагент: инструменты
полные (тесты, воспроизведение, поиск), ограничение «не менять репозиторий
и среду» держится промптом — как в `claude-team-review`.
- `claude-team-review` запускает только **владелец правок** — worker, в
средах, где worker может запускать субагентов. Координатору запускать
team-review нельзя: его лид чинит код сам, а это второй писатель в дереве
и слом границы «координатор кода не трогает». Под Codex механизм —
reviewer-субагент координатора с ревью-промптом; промпт адаптирован в
`references/reviewer-prompt.md` самого скилла (из `claude-team-review`) —
скилл самодостаточен, внешних файлов не требует.
- Триаж находок остаётся в worker-е: у него контекст кода и «зачем» из
контракта. Worker вправе аргументированно отклонить находку — через
контракт и тест, не голословно (приём из прогона).
- Бонус топологии: координатор видит зацикливание ревью (см. п.4).
- Цена: индексы ревью идут через контекст координатора, по кругу на каждый
rerun. Прямого замера этого трафика нет (июньские 2,6 КБ — замер
финальных отчётов worker-ов, не пер-раундового трафика ревью), но порядок
тот же — килобайты. Если на длинной цепочке начнёт пухнуть — готовый
фолбэк: файловый канал из `2026-06-14-observability-design.md` (ревьюер
пишет индекс в файл, координатору — путь и вердикт). Сейчас не вносим.
3. **Контракт ревьюеру** (новый, симметричен контракту worker-а): наблюдатель
с полными инструментами (ограничение «не менять репозиторий и среду» —
промптом), код читает сам и докладу worker-а не верит; возвращает
**индекс** — находка,
`файл:строка`, какой критерий нарушен, минимальный repro; вердикт одной
строкой (`APPROVED` / `CHANGES_REQUESTED`); запрет вставлять diff, полный
вывод тестов и нарратив рассуждений. Уровень мышления и модель задаёт
координатор при запуске (см. п.5).
4. **Политика кругов ревью.** Тождество находок между кругами судит не
координатор (по индексам оно не определяется — формулировки и `файл:строка`
между кругами плывут, что видно по прогону 07), а **worker в триаже**: если
ревьюер снова поднимает вопрос, который worker уже аргументированно
отклонял, worker помечает это в своём индексе флагом **«спор»** с отсылкой
к прежнему обоснованию. Координатор реагирует на флаг и на общий счётчик:
- **спор** → не общий rerun, а узкий гейт по спорному вопросу на усиленном
уровне (паттерн «xhigh narrow gate» из прогона);
- спор пережил и узкий усиленный гейт → **стоп, решение человека**;
- независимо от споров: больше **четырёх** кругов ревью на issue → стоп,
координатор выносит человеку вопрос «дробить issue или принимать риск»
(потолок взят по прогону 07 — пять кругов прошли без управления);
- rerun после правок по гейт-находкам назначает координатор автоматически —
это больше не concern worker-а (в прогоне дважды висело «rerun ещё не
выполнен»).
5. **Две ручки стоимости: модель по роли и уровень мышления.** Это разные оси,
не смешивать: «модель по роли» в скилле уже есть (дешёвая на task-lens,
сильная на финале), прогон же эскалировал **уровень рассуждений** (xhigh)
на той же модели. Вводим три относительных уровня без имён:
*экономный* (ступень вниз: task-lens ревью, диагностика, разбор логов),
*рабочий* (дефолт: worker, обычное code review), *усиленный* (ступень
вверх: эскалация по п.4, находки про межмодульные инварианты и форму
распределений, финальное chain-ревью — усиленный уровень на сильной
модели). Соответствие ролей конкретным моделям и уровням хоста — датированная
таблица `references/effort-levels.md`; логика скилла имён не знает. Дефолт —
рабочий уровень, вверх только по триггеру. Хост без управляемых уровней:
эскалация вырождается в «свежий субагент». Уровень фиксируется в записи
журнала о запуске ревью.
6. **Происхождение ревью пишет координатор в журнал** — в момент запуска и в
момент вердикта: режим, механизм, кто запускал, уровень, вердикт. Worker
больше не автор этой строки (факты «кто запускал» и «уровень» знает только
координатор); в индексе worker-а остаются саморевью и триаж (включая флаг
«спор»). Честная граница прежняя: журнал — самозапись координатора,
append-only след для внешней проверки; ложь в моменте ловит кросс-линия и
человек, не сам журнал.
7. **Метка времени в журнале.** Каждая запись начинается с `HH:MM` (дата — в
заголовке дня; время брать вызовом `date`, модель его не знает). Порог
записей прежний — точки записи из раздела «Журнал прогона»; мельче
(каждая команда, каждый прочитанный файл) не писать, чтобы метки не
превратили журнал в спам. Heartbeat worker-а в пакет **не входит**: журнал
пишет только координатор, файлового канала нет, а слать сигналы в контекст
координатора — противоречить его экономии. Если когда-нибудь вносим
файловый канал — heartbeat возвращается вместе с ним (дизайн готов в
observability-handoff).
8. **Статус-контракт: недоступность стендовой проверки ≠ `BLOCKED`.** Если
проверка (docker, стенд, сеть) недоступна в среде worker-а, он возвращает
`DONE_WITH_CONCERNS` с фактом («docker отсутствует: <вывод>») и **готовой
командой**, которую координатор прогоняет сам. `BLOCKED` — только про саму
работу. Оговорка по прогону: команда может не пройти и у координатора
(на коммит-гейте 11 `snap-confine` ломал dry-run в sandbox — перегоняли вне
sandbox); тогда координатор фиксирует в журнале, где и как проверка
прошла фактически. Случай «ревью недоступно worker-у» из контракта уходит
совсем — worker ревью больше не запускает (п.2).
9. **Дисциплина финала.**
- Обновить PRD/трекер **до** финального chain-ревью, чтобы оно проверяло
приведённое состояние (low finding прогона — предсказуемый класс).
- После коммит-гейта issue — `Status: done` в файле issue, **docs-коммитом
вместе с журналом**, не в feature-коммите. Это смена фактического
поведения (прогон 07/11 закрывал статусы внутри feature-коммитов):
файл issue — процессный, как и журнал, и на границе шага 6 «только файлы
worker-а» он файлом worker-а не считается.
- Финальный handoff обязателен для нетривиальной цепочки и является
**условием выхода из цикла**, а не пунктом-пожеланием; место и формат — по
конвенции репозитория (AGENTS.md / ADR), если она есть.
## Куда править
- `SKILL.md`:
- frontmatter `description` («с ревью через claude-team-review» — убрать);
- «Роли» — ревьюер: запускает координатор, наблюдатель, контракт по п.3;
- врезка про среду Codex (п.1);
- «Цикл» — шаги 4–6: приём индекса ревью, политика кругов и rerun (п.4),
происхождение в журнале (п.6), закрытие issue (п.9);
- «Журнал прогона» — метка времени (п.7), происхождение пишет координатор;
- «Контракт задания worker-у» — убрать запуск team-review worker-ом,
добавить триаж находок ревью + флаг «спор», статус-контракт по п.8;
- новый раздел «Контракт ревьюеру» (п.3);
- «Режимы ревью» — две ручки (модель по роли + уровень мышления), таблица
`references/effort-levels.md` (п.5);
- «Классификация находок» — триаж делает worker, не «team-review внутри
worker-а»;
- «Финал цепочки» — порядок: PRD/трекер → chain-ревью (усиленный уровень на
сильной модели) → handoff как условие выхода (п.9);
- чек-лист — привести к новым ролям (происхождение в журнале, уровни,
handoff обязателен, закрытие issue).
- `README.md`: строка потока и раздел «Что получается» («/goal + /tdd +
claude-team-review», «per-finding триаж делает claude-team-review внутри
worker-а»), «сильной моделью» в описании финала, раздел «Статус и что
дальше» — открытый вопрос «бюджет кругов ревью» закрывается п.4.
- `references/rationale.md`: датированная поправка 2026-07-04 — факт среды
Codex, боевое подтверждение вопросов про круги ревью, закрытие вопроса
«бюджет кругов» политикой п.4.
- `references/effort-levels.md`: новая датированная таблица уровней (п.5).
## Чего сознательно не делаем
- Файловый канал / `coordinator-runs/` — не вносим, зафиксирован как фолбэк
(п.2) и носитель heartbeat (п.7).
- Preflight-проба среды на каждый прогон — отвергнута (расход без пользы).
- Автоматизация кросс-линейного прохода — по-прежнему вне скилла.
- Замер чисел-ориентиров июня — не сделан в этом прогоне и не закрыт; снять
на следующей цепочке, пока сырьё в `~/.codex` не ротировалось.
## Suggested skills
- `conventional-commits` — коммиты в dotfiles.
- `coordinator-loop` — следующий боевой прогон (цепочка 12 или 08) как замер.
@@ -1,163 +0,0 @@
# Handoff: сессия переработки скилла по итогам боевого прогона
Дата: 2026-07-04 MSK (вечер, после прогона 07 → 11); дополнено 2026-07-05
(после прогона 15 → 19, см. последний раздел).
Статус: сделано и закоммичено (`2d9fc7e` в dotfiles). Сессия Claude Code —
правки вносила модель другой родословной, чем та, что гоняла прогон.
## Зачем этот handoff
Продолжение в новой сессии: что решено, как проверено, что осталось. Сами
решения и их обоснования — в пакете правок рядом
(`2026-07-04-codex-topology-and-escalation.md`), сюда их не дублирую.
Отдельно стоит зафиксировать форму: разбор прогона Codex и переработку скилла
делал Claude — по сути это был кросс-линейный проход по **процессу** (не по
коду фичи). Он же дал главный улов сессии — см. ниже. Рабочее разделение
пользователя (Claude — документы и планы, Codex — исполнение) на скиллах
работает так же, как на коде.
## Как проверялось (сам процесс — рабочий приём, повторить)
1. Черновик пакета → **адверсарное ревью свежим субагентом** с проверкой
каждого утверждения по первоисточникам (журнал прогона, SKILL.md, старые
handoff'ы). Улов: 2 blocker, 7 important, 4 minor — все закрыты правкой
пакета до внесения в SKILL.md.
2. Внесение → **лёгкое саморевью свежим взглядом** (без субагентов): 4 находки,
3 исправлены.
3. Досоздание `reviewer-prompt.md` → ещё одно лёгкое саморевью: 2 находки
(обе реальные — untracked-файлы невидимы в diff; предусловие ревью-гейта),
исправлены амендом.
Блокеры адверсарного ревью били по ядру пакета (неисполнимый критерий «та же
находка» в политике кругов; team-review из-под координатора = второй писатель) —
без этого шага они уехали бы в SKILL.md.
## Повороты после ревью (чего не было в исходном замысле)
- **Наблюдатель, не read-only** — поправка пользователя: ревьюеру нужны
инструменты (тесты, воспроизведение, поиск), ограничение «не менять
репозиторий и среду» держится промптом, как в `claude-team-review`.
- **Скилл сделан цельным**: `references/reviewer-prompt.md` — адаптация
шаблона из `claude-team-review` под нашу топологию (чинящего лида нет,
вердикты наши, четыре фокуса, блок rerun учитывает отклонения worker-а).
Зависимость от файлов чужого скилла снята.
- **Тождество находок судит worker** (флаг «спор»), координатор считает
только круги — ответ на блокер ревью.
- Определение круга, критерий закрытия гейта, предусловие ревью-гейта
(`DONE`/`DONE_WITH_CONCERNS`) — из саморевью.
## Что не сделано / открытые вопросы
- **Числа-ориентиры июня не замерены** (пик контекста, трафик через
координатора, тест-прогоны): «контекст не пострадал» в прогоне 07 → 11 —
впечатление пользователя, не замер. Снять на следующем прогоне по горячему:
сырьё в `~/.codex` (logs/sessions) ротируется.
- **Политика кругов не проверена вживую** — потолок четыре круга и флаг
«спор» появились после прогона; первый боевой тест — следующая цепочка
(12 «DAG-пульт» или 08 «миграция курса» в clickstream-репозитории).
- **`references/effort-levels.md` — первый черновик**: соответствия уровней
подобраны по одному прогону, калибровать по журналам (уровень пишется в
происхождение ревью — данные будут копиться сами).
- **Кросс-хост не гонялся**: ветка Claude Code в effort-levels — умозрительная.
## Где что лежит
- Пакет правок с обоснованиями: `2026-07-04-codex-topology-and-escalation.md`
(рядом).
- Изменённые файлы: `SKILL.md`, `README.md`, `references/rationale.md`
(поправка 2026-07-04), новые `references/effort-levels.md` и
`references/reviewer-prompt.md`.
- Журнал боевого прогона-источника:
`~/sources/clickstream-ch-kafka-superset-demo/.scratch/generator-model-time-startup-history/coordinator-journal.md`.
## Дополнение той же ночью: вторая волна правок по живому прогону
Прогон цепочки 12 → 14 → 09 → 10 → 08 (Codex) шёл параллельно с этой сессией и
дал шесть процессных находок; все закрыты правками скилла до конца прогона
(коммиты `9fda85f`…`12a4783`):
- субагенты — свежими сессиями, без форка истории координатора; но свежая
сессия — правило запуска, не запрет продолжения: узкий rerun — тем же
ревьюером через `send_input` (обратное сожгло пять полных xhigh-кругов
финального ревью);
- уровни мышления: субагенты не наследуют уровень сессии (дефолт spawn —
medium), задавать явно; в журнал — фактическое значение, не название яруса;
таблица перекалибрована: worker обычных issue — medium, любое ревью — high
(эксперимент, критерий выхода — круги ревью по журналам), спор/финал — xhigh;
- финал: находки — по классу файла (процессные чинит координатор, проектные —
fixer-субагент; триаж — у fixer-а); словарь статусов отчётов
(«APPROVED финального ревью ≠ завершение цепочки»); предупреждение о расходе
на третьем круге.
Кросс-линейный проход по итогам цепочки (Claude, 4 параллельных адверсарных
ревьюера с исполняемыми доказательствами) дал 1 critical, 5 high, 10 medium,
4 low после APPROVED финального ревью линии — третий случай подтверждения
находки о родословной; отчёт —
`clickstream-repo:.scratch/handoffs/20260705-0002-cross-review-generator-chain.md`.
Урок сессии: живой прогон под наблюдением человека — самый дешёвый источник
процессных находок; все шесть дефектов увидел пользователь на экране/в журнале,
ни один — самоотчёт координатора.
## Дополнение 2026-07-05: ретро прогона 15 → 19 и калибровка (Claude)
Цепочка 15 → 18 плюс багфикс-issue 19 по кросс-ревью (Codex) завершена. После неё
Codex дал ретро по скиллу и разбор вопроса «не зря ли worker-ы на medium»; Claude
сверил ретро с журналом прогона — первый случай, когда самоотчёт линии полностью
сошёлся с первоисточником (Codex отвечал по журналу, а не по памяти).
- **Эксперимент medium-worker: критерий выхода не сработал.** Medium-issue в
основном закрывались за один круг ревью (16, 18, 12, 08 — по одному; 17 и 10 —
по два), а issue 15 на high тоже собрал круг находок — глубина не покупает
чистый первый проход. Решение: уровни в таблице не менять, точность даёт
классификация issue. Итог записан в `references/effort-levels.md`.
- **Три правки скилла** (коммит `b8f1fe0`): предусловие входа (качественно
подготовленные issue — обычно выход `/to-issues`; мутные — сначала прояснение
замысла, иначе цикл превращается в машину ревью-кругов); точная граница чтения
координатора (запрет только на код ради верификации, процессные артефакты — его
зона); расширенный классификатор риска (версионирование state, разрушающие пути
во внешние сервисы, fail-open проверки) — по нему issue 17 ушёл бы в гейт сразу.
- Правки держались проектно-нейтральными (поправка пользователя): полный
тест/линт-гейт — дело репозитория, скилл предписывает лишь «полный тест-сет».
- Закрыты два пункта из «не сделано» выше: `effort-levels.md` откалиброван по
журналам двух цепочек; **политика кругов прошла живой тест** — узкие rerun'ы
продолжением того же ревьюера работали, потолок четырёх кругов не достигнут ни
на одном issue.
- Осталось открытым: числа-ориентиры (пик контекста, трафик через координатора)
так и не замерены; ветка Claude Code в effort-levels — по-прежнему
умозрительная, кросс-хост не гонялся.
### Вторая волна той же сессии: кросс-ревью правок и контракт автономного прогона
Правки прошли два круга кросс-ревью Codex (та линия, что гоняла прогон, читала
правки этой), находки сходились по убыванию — признак, что текст стабилизировался:
- Круг 1, medium: «Дефолт — рабочий уровень» противоречил таблице effort-levels
(worker обычного issue — экономный) и толкал запускать всех worker-ов на high.
Остаток старой формулировки, пропущенный саморевью при правке соседнего текста.
Исправлено: уровень — по роли и классу issue из таблицы, дефолта «для всех» нет;
заодно «форма распределений» расширена до доменных инвариантов данных — скилл
общий, не только для генератора (`1d67a3f`, тронут и `reviewer-prompt.md`).
- Круг 2, одна находка: в новом разделе «живой стенд» ошибочно попал в класс
«может проверить только человек». Разведено по статус-контракту: недоступное
worker-у прогоняет координатор (часть коммит-гейта), открытый риск в handoff —
только недоступное и ему (`c959911`).
**Новый раздел «Контракт автономного прогона»** (`f6b9b3b`) — по предложению Codex
из ретро; пользователь подтвердил: скилл и проектировался под сценарий «передал
очередь issue и ушёл», но контракт вызова нигде не был записан. Внесено компактно,
не по полному эскизу Codex: вход (список issue, порядок, «зачем»), мандат (весь цикл
без уточняющих вопросов; локальные коммиты — да, push/теги/разрушение внешнего —
только по поручению), стоп-условия **сводкой со ссылками на разделы-первоисточники**,
не пересказом — чтобы не разъезжались. Попутно снят конфликт с автономностью:
предупреждение о третьем круге ревью — не вопрос, ответа не ждать, жёсткая граница —
потолок кругов. Назначение вынесено в description скилла.
Открытое: контракт автономного прогона вживую не проверен — первый тест на
следующей цепочке.
## Suggested skills
- `coordinator-loop` — багфикс-цепочка по кросс-ревью = следующий замер.
- `conventional-commits` — коммиты по итогам прогона.
@@ -1,53 +0,0 @@
# Уровни мышления и модели по ролям
Обновлено: 2026-07-05. Логика скилла оперирует относительными уровнями
(экономный / рабочий / усиленный) и «моделью по роли»; здесь — привязка к
конкретным настройкам хостов. Вышла новая модель или переименовались уровни —
правится этот файл, не `SKILL.md`.
## Codex CLI (проверено на боевых прогонах 2026-07-04 и 2026-07-05)
Ручка уровня — reasoning effort субагента (`low` / `medium` / `high` / `xhigh`).
**Субагенты НЕ наследуют уровень сессии.** У `spawn_agent` свой дефолт — `medium`:
промолчал про уровень — получил ступень ниже рабочей (наблюдение прогона issue 12:
сессия `high`, worker и ревьюер молча ушли на `medium`). Уровень задавай **явно
каждому субагенту** при запуске.
| Роль | Уровень | Настройка |
|---|---|---|
| worker на обычном issue; task-lens ревью, диагностика, разбор логов | экономный | `medium` |
| **любое code review**; worker на гейтовом issue | рабочий | `high` |
| узкий гейт по спору, финальное chain-ревью | усиленный | `xhigh` |
Почему ревью глубже worker-а, а не наравне: у worker-а есть страховочная сетка ниже
по течению (тесты в петле, саморевью, ревью, коммит-гейт), у ревьюера её нет —
пропущенную находку не ловит никто до кросс-линии и человека, «не нашёл» выглядит как
«искать было нечего». Плюс экономика: worker — самая дорогая петля (~400K токенов на
issue), ревью — один короткий прогон; глубина дешевле там, где прогон короткий, а цена
пропуска — целый лишний круг.
Калибровка по журналам двух цепочек (2026-07-05): worker-ы на `medium` в основном
закрывались за один круг ревью; worker на `high` всё равно собрал круг находок — глубина
не покупает чистый первый проход, ревью нужно при любом уровне. Вывод: уровни в таблице
не менять; точность даёт **классификация issue** (обычный или гейтовый — «Порог риска»
в SKILL.md), а не повышение уровня всем worker-ам.
Модель по роли: во всех ролях — модель сессии; отдельной дешёвой модели для
субагентов в текущей конфигурации нет, экономия достигается уровнем.
## Claude Code (если прогон под этим хостом)
Ручки: effort субагента (`low` / `medium` / `high` / `xhigh` / `max`) и model
(`haiku` / `sonnet` / `opus`).
| Роль | Уровень | Настройка |
|---|---|---|
| worker на обычном issue; task-lens ревью, диагностика, разбор логов | экономный | модель ступень ниже, effort `low`–`medium` |
| любое code review; worker на гейтовом issue | рабочий | модель сессии, effort `high` |
| узкий гейт по спору, финальное chain-ревью | усиленный | сильная модель, effort `xhigh`–`max` |
## Хост без управляемых уровней
Эскалация вырождается в запуск свежего субагента — слабее, но не пусто
(см. `SKILL.md`, «Режимы ревью»).
@@ -1,124 +0,0 @@
# Почему цикл устроен именно так
Это эмпирика, на которой стоят правила в `SKILL.md`. Читать не обязательно перед
каждым запуском — нужна, когда хочешь понять, какому классу дефектов какой шаг
противостоит, или решить спорный случай.
Источник — эксперимент на цепочке из трёх зависимых задач генератора кликстрима
(калибровка интенсивности, state версии 2 и рестарт, интеграция сервиса). Задачи
меняли близкий код, поэтому шли строго последовательно, по одному worker-у на
issue. Выводы — хороший сигнал для класса «последовательные инженерные срезы с
тестами и acceptance criteria», но не универсальная истина: один репозиторий, один
домен, три задачи.
## Что какой шаг ловит
> **Поправка 2026-07-02.** Ниже — эмпирика эксперимента, не текущий процесс. В переписанном
> `SKILL.md` координатор больше **не** верифицирует чтением кода: сквозной sanity-check (связи
> дефолтов/конфигов/документов/acceptance) ушёл независимому ревьюеру и финальному ревью на
> соответствие замыслу. Читай раздел как «что какой шаг ловил», а роли бери из `SKILL.md`.
- **Саморевью worker-а** хорошо ловит локальные несостыковки своей же реализации:
забытый старый дефолт, слишком широкий тест, устаревшие числа в конфиге,
двусмысленный README. У worker-а уже есть подробный контекст, поэтому он дёшево
находит такие вещи — но только если запретить ему править на первом шаге и
попросить сначала выдать находки.
- **Sanity-check координатора** добавляет то, что саморевью структурно не видит:
связи между дефолтами, обычным запуском, документами и acceptance criteria.
Пример из эксперимента: новый базовый поток конфликтовал со старым нижним
пределом числа событий на тик — каждая настройка по отдельности «корректна», а
вместе ломали критерий интенсивности.
- **Отдельный ревьюер** ловит то, что не видит ни worker, ни его саморевью:
падение на битом сохранённом состоянии, которое проходит первичную загрузку;
слишком большой снимок состояния (мегабайты вместо компактного); слабый
интеграционный тест, который формально проходит, но не доказывает уход от
старой плоской модели.
- **Повторный круг ревью после большого исправления.** Когда правка по находке
меняет дизайн или формат данных, она сама нуждается в ревью. В эксперименте
переделка компактного состояния закрыла старый дефект и тут же внесла рядом
новый — его поймал именно второй круг.
## Центральная находка про родословную
Главную гипотезу эксперимента проверяло отложенное ревью **моделью другой
родословной** — той, что вела дизайн и спеки, а не той, что писала код.
Прогноз, что цикл пропустит ссылочную целостность при восстановлении состояния, **не
оправдался**: этот точечный дефект цикл поймал сам (саморевью и отдельный ревьюер
независимо нашли «битое состояние проходит загрузку, но роняет восстановление», и в
коммит легли обёртка и регрессионный тест).
А до внешнего взгляда дожили дефекты **не точечные, а сквозные**:
- **Расхождение живого и восстановленного путей.** Живой визит и восстановленный
после рестарта брали атрибуты из разных источников и по-разному генерировали
идентификаторы событий. Инвариант «один визит — однородный контекст» нарушался на
стыке двух путей, который ни один тест не сводил вместе.
- **Форма распределения длины визита.** Среднее и конверсия — на цели, воронка
монотонна (тесты это подтверждали), но форма уехала от эталона: спайк на коротких
визитах, заметная доля срезов о верхний потолок, медиана у нижней границы тестовой
полосы. Тесты пинили средние и пороги, но не форму.
Отсюда уточнение: ценнее всего ревьюер **другой родословной**, и именно там, где
свойство (а) тянется через несколько модулей или путей, либо (б) корректность
распределительная (форма, а не одно среднее). Для точечных дефектов на входе
хватает саморевью и отдельного ревьюера той же линии.
Поэтому в `SKILL.md` независимый ревьюер по умолчанию — свежий субагент той же
модели, но с честной оговоркой: свежий контекст снимает память о реализации, а не
общую слепоту линии. Эти два семейства (межмодульное и распределительное) ему надо
давать как явную цель, а незакрытое кросс-линейное ревью на gate — фиксировать как
риск.
> **Поправка 2026-07-03.** Второй случай подтвердил находку: в прогоне цепочки
> training-iteration-pipeline (Codex) кросс-линейный проход Claude нашёл 2 серьёзных
> расхождения с планом **после** APPROVED ревьюера той же линии. Поэтому в `SKILL.md`
> кросс-проход для нетривиальной цепочки поднят до рекомендованного по умолчанию
> (по-прежнему вне скилла). Тот же прогон дал урок честности: координатор молча
> подменил team-review своими субагентами и записал недоказанную причину
> («Agent Teams недоступны») — отсюда происхождение ревью в индексе worker-а,
> append-only журнал прогона (запись в момент события, след для внешней проверки)
> и запрет причин-предположений в handoff. Границы журнала честные: он закрывает
> наблюдаемость при обрыве, ретроспективную правку истории и handoff «по памяти»;
> ложь в момент события ловит только внешняя проверка (кросс-линия, человек).
> **Поправка 2026-07-04.** Боевой прогон цепочки 07 → 11 (генератор кликстрима) дал два
> уточнения. Первое — факт среды: в Codex worker не может запускать субагентов
> (проверено диагностом с `agent_type=worker`), поэтому топология изменена — ревьюеров
> запускает координатор, а триаж находок остаётся в worker-е (см. `SKILL.md` и handoff
> `2026-07-04-codex-topology-and-escalation.md`). Второе — вопросы про число кругов
> ревью из списка ниже стали боевыми: issue 07 прошёл пять кругов без ограничителя, а
> спор с ревьюером разрешил изобретённый по ходу «узкий гейт на xhigh». Ответ закреплён
> политикой кругов в `SKILL.md`: флаг «спор» в триаже worker-а, узкий гейт на усиленном
> уровне, потолок четыре круга на issue, дальше — человек.
> **Поправка 2026-07-05.** Третий случай подтверждения находки о родословной, самый
> сильный по контрасту: кросс-проход Claude по цепочке 12 → 14 → 09 → 10 → 08 дал
> 1 critical, 5 high (и 14 находок ниже) **после** APPROVED финального xhigh-ревью
> той же линии. Улов лёг ровно в предсказанные классы: сквозные сценарии через
> несколько модулей («смешение миров» тремя дорогами, «чистый клон никто не прошёл»)
> и честность проверок (ложнозелёные тесты и стыковая проверка). Локальные дефекты
> внутренние ревью закрыли честно — кросс-линия их не переоткрывала. Отчёт:
> `clickstream-repo:.scratch/handoffs/20260705-0002-cross-review-generator-chain.md`.
## Чего не делать слепо
- Не запускать ревьюера после каждой мелкой правки — дорого и раздувает задачу.
- Не принимать все находки за правду — каждую классифицировать.
- Не давать координатору верхнеуровневый `/goal` на всю цепочку — нужна
управленческая осторожность, а не достижение цели любой ценой.
- Не смешивать процессные и handoff-заметки с рабочими коммитами задач.
- Не требовать реальный внешний стек на каждом шаге, если acceptance criteria
допускают мок, — но явно фиксировать, что именно осталось непроверенным.
## Открытые вопросы
Эксперимент оставил нерешённым:
- как заранее понять, что задаче нужен отдельный ревьюер, а не только саморевью;
- стоит ли заранее обязывать worker-а к промежуточным отчётам на статистике и
долгих тестах;
- нужно ли явно ограничивать бюджет worker-а (число кругов ревью закрыто политикой
кругов — поправка 2026-07-04);
- как формализовать порог «большое исправление требует второго круга» (частично
закрыто: rerun после правок по гейт-находкам координатор назначает автоматически).
@@ -1,120 +0,0 @@
# Шаблон промпта ревьюеру
Координатор заполняет плейсхолдеры и передаёт текст как промпт
reviewer-субагенту. Адаптирован из `reviewer-prompt.md` скилла
`claude-team-review` (2026-07-04) под топологию coordinator-loop: чинящего
лида здесь нет — правит worker по индексу находок, круги считает координатор.
---
## Шаблон
```text
Ты — старший адверсарный ревьюер. Твоя работа — подорвать доверие к изменению,
а не подтвердить его. Скепсис по умолчанию: считай, что в работе есть дыры,
пока доказательства не показали обратное. Не засчитывай добрые намерения и
«допилят следом». Работает только на happy path — это настоящая слабость.
## Фокус проверки
{FOCUS}
# по коду — тонкие баги, межмодульные инварианты, доменные инварианты данных
# (например, форма распределений, если она важна проекту)
# по задаче — сделано ли то, что требует issue/спека (acceptance criteria)
# узкий гейт — только спорный вопрос: {SPOR_SUMMARY}
# chain — вся реализация цепочки против исходной спеки/PRD (дрейф целого)
## Что проверяем
{ARTIFACT}
# для issue: текст issue, ссылки на PRD/спеку/смежное — «зачем».
# Работа worker-а НЕ закоммичена: смотри рабочее дерево целиком —
# `git status --short` (новые файлы untracked, в diff их нет!) плюс
# `git diff`; закоммиченная история — контекст, не объект ревью
# для chain: ключевые пути реализации + исходная спека/PRD;
# объект — закоммиченная реализация цепочки
## Круг
{ROUND} # 1 — первичное ревью; N — rerun после правок worker-а
{PREVIOUS_FINDINGS_BLOCK}
# Только когда rerun делает СВЕЖИЙ субагент (не знает прошлого круга): дословно
# находки прошлого круга; что worker исправил; что отклонил и с каким
# обоснованием (контракт/тест). Отклонённое оценивай по существу: обоснование
# верно — признай; неверно — переформулируй находку с контраргументом и новым
# доказательством, а не повторением прежней формулировки.
# При продолжении того же ревьюера блок не нужен: передай через send_input
# только узкий вопрос — закрывают ли правки твои находки, не внесено ли нового
# рядом; полный обход заново не повторять.
## Что можно
- Читать любые файлы репозитория; git-историю, диффы, ветки.
- Запускать команды: тесты, линтеры, сборку, воспроизведение находок.
- Искать в сети и документации (Context7) для проверки предположений.
## Что нельзя
- Создавать, менять или удалять файлы проекта; менять среду (git-операции с
записью, установка пакетов, правка конфигов).
- Чинить находки — это работа worker-а. Ты аудитор, не участник.
## Планка находки
Каждая находка отвечает на четыре вопроса:
1. Что сломается? — конкретный сценарий, не гипотеза.
2. Почему уязвимо? — `файл:строка` или раздел.
3. Последствия — что и насколько плохо (потеря данных > простой > деградация).
4. Рекомендация — конкретный фикс, достаточный worker-у для работы.
Вне скоупа: стиль и нейминг; спекуляции без конкретного сценария; «неплохо
бы» без связи с корректностью или безопасностью.
Перед докладом верифицируй находку: прогони тесты, сверь документацию,
воспроизведи путь. Одна проверенная находка стоит десяти догадок; ссылки на
доки и upstream-issue давай точно — их проверят.
## Калибровка
- Одна сильная находка лучше нескольких слабых.
- Серьёзность: critical (данные/безопасность) > high (баг в проде) >
medium (крайний случай).
- Работа крепкая — так и скажи: ложные тревоги подрывают доверие.
## Формат ответа — индекс, не нарратив
**Findings:** на каждую — `[severity]`, `файл:строка`, какой критерий нарушен,
минимальный repro, рекомендация. Без diff, без полного вывода тестов, без
нарратива рассуждений. Нет находок — «No actionable findings».
Последняя строка ответа — ровно одна из:
VERDICT: APPROVED
VERDICT: CHANGES_REQUESTED
APPROVED — находок нет или все medium без нарушения acceptance criteria.
CHANGES_REQUESTED — есть high/critical или нарушен критерий issue/спеки.
## Язык
Отвечай на русском. Severity, заголовки полей и строку VERDICT оставляй
латиницей как есть.
```
---
## Заметки по запуску
- Координатор собирает бриф сам — ревьюер не наследует историю сессии;
передавай только нужное.
- Модель и уровень мышления — по [effort-levels.md](effort-levels.md):
обычное ревью — рабочий уровень, узкий гейт по спору и chain-ревью —
усиленный на сильной модели.
- **Не запускай ревьюера в урезанном/read-only субагенте**: тесты и
воспроизведение требуют исполнения команд. Ограничение «не менять
репозиторий и среду» держит промпт (раздел «Что нельзя») — как в
`claude-team-review`.
- На узком гейте `{SPOR_SUMMARY}` — формулировка спора из индекса worker-а
(находка + его обоснование отклонения) дословно, без интерпретаций
координатора.
+181
View File
@@ -0,0 +1,181 @@
---
name: cross-review
description: >-
Перекрёстное ревью своей работы моделью другого семейства (Claude ↔ Codex):
линзы для ревьюера, способы запуска и разбор находок самим автором.
Применяй, когда пользователь просит независимое или перекрёстное ревью,
ревью другой моделью, или когда готовое изменение либо план стоит показать
чужим глазам до сдачи.
---
# Перекрёстное ревью
Ты автор изменения и остаёшься его владельцем. Ревьюер из другого семейства
моделей добавляет то, чего у тебя нет: свежая сессия не знает твоих
рассуждений, а другое семейство замечает иные классы ошибок. Его отчёт —
набор утверждений для проверки. Решения по ним принимаешь ты.
Скилл даёт линзы, способы запуска и опыт разбора находок. Сколько линий
запускать, какие линзы брать и нужна ли перепроверка, решай по задаче и цене
ошибки.
## Что передать ревьюеру
Создай обменный каталог вне репозитория (`mktemp -d`) и напиши в нём
короткое поручение `brief.md`. Если переменные shell не сохраняются между
вызовами, дальше подставляй путь каталога целиком. В поручении:
- **что проверять**: точная версия и способ её получить — diff-команда от
точки отсчёта, коммиты или пути. Ревьюер должен отличить проверяемое от
посторонних правок в дереве;
- **постановка**: путь к задаче, спеке или критериям приёмки. Без неё линза
«по задаче» работает вслепую — так и напиши;
- **команды проверки** и ожидаемый результат (например, число тестов);
- **границы**, если они есть: куда не ходить (приватные данные, рабочие
серверы, боевые скрипты), какие проверки слишком дороги, что в этой среде
заведомо не находка;
- **места, где ты сам не уверен** — вопросами, без своих обоснований:
пересказ твоей логики лишает ревьюера независимости. То же с выводами
другого ревьюера, пока оба не закончили первый заход;
- **префикс находок** и **путь отчёта** (`REPORT`) — свои у каждого ревьюера.
Поручение собирается конкатенацией: `cat brief.md prompts/mandate.md
prompts/lens-….md > prompt.md`. Файлы `prompts/` лежат в каталоге этого
скилла — там, откуда прочитан этот файл. Подставляй их как есть.
## Линзы
Линза — фокус ревьюера. Выбирай те, где в этом изменении правда может
прятаться ошибка. Два ревьюера параллельно, с разными линзами, видят
больше: у каждого свой контекст, и фокус не размывается. Один ревьюер с
несколькими линзами дешевле.
- [`lens-spec.md`](prompts/lens-spec.md) — **по задаче**: сделано ли то, что
просили, что упущено, что добавлено сверх, можно ли пройти критерии, не
решив задачу.
- [`lens-code.md`](prompts/lens-code.md) — **по коду**: корректность,
инварианты, регрессии, безопасность, честность тестов.
- [`lens-shape.md`](prompts/lens-shape.md) — **форма и вкус**: эскиз решения
до чтения diff, глубина модулей, запахи кода по Фаулеру.
- [`lens-subtract.md`](prompts/lens-subtract.md) — **вычитание**: что можно
удалить без потери нужного поведения. Особенно полезна после нескольких
кругов исправлений и для моделей, склонных раздувать код.
- [`lens-plan.md`](prompts/lens-plan.md) — **план или спека до реализации**:
документ судится на своём уровне.
Ревьюеру нужна сильная модель с высокой глубиной рассуждения. У слабой
модели больше ложных находок, и их разбор съедает экономию. Конкретные
модели указаны в адаптерах запуска.
## Как запустить
Способ запуска зависит от среды. Механика каждого — в отдельном файле:
- [T3 Code](references/launch-t3.md) — если доступны инструменты
`orchestrator_capabilities` и `delegate_task`. Удобнее всего: приложение
само следит за задачей и будит тебя по завершении.
- [Codex из командной строки](references/launch-codex.md) — когда автор
Claude или другая модель.
- [Claude из командной строки](references/launch-claude.md) — когда автор
Codex или другая модель.
Общее для всех способов:
- ревьюер получает **свежую сессию**, а не копию твоей истории;
- ревьюер может то же, что ты: запускать тесты, ходить в сеть, создавать
временные файлы. Исходники и состояние защищает мандат в поручении.
Песочница только для чтения оставила бы ревьюеру одно чтение кода;
- перед запуском сними `git status --porcelain`, после — сверь: дерево
должно остаться прежним;
- пока ревьюер работает, проверяемые файлы не меняй, иначе он проверит
смесь двух версий. Правки, которые уже ясны, готовь рядом, во временном
каталоге;
- если ревьюеров или кругов несколько, веди в обменном каталоге короткий
реестр: кто проверял, какую версию, id сессии или задачи, пути отчётов,
открытые ID. Он переживёт сжатие контекста и пригодится для отчёта;
- код уходит к другому поставщику моделей. Просьба пользователя о
перекрёстном ревью покрывает проверяемое изменение и нужный ему контекст;
секреты и посторонние материалы в поручение не клади.
## Разбор находок
Разбирает находки автор: у тебя контекст кода и задачи. Пользователю нужен
итог в отчёте, а не участие в каждом споре. Так же разбирай и отзывы других
моделей, которые переслал пользователь.
По каждой находке проверь три вещи:
1. **Верна ли она фактически.** Воспроизведи сценарий, прочти код, запусти
тест. Ревьюеры ошибаются уверенно: выдумывают API, путают версии,
приписывают коду то, чего в нём нет, требуют защиты от невозможных
состояний. Автору легко поверить хорошо изложенной находке, но уверенный
тон — не доказательство. Проверяй и то, на чём находка держится:
утверждения о прогонах, истории и прошлых решениях сверяй с источником
так же, как утверждения о коде. Дешёвая починка от проверки не
освобождает: ложная находка, закрытая одной строкой, всё равно добавляет
лишнее.
2. **Применима ли к этому проекту** — его контрактам, масштабу, угрозам.
3. **Соразмерна ли починка риску.** Предложенный способ — один из вариантов.
Подтверждённый дефект закрывай лучшим путём, какой видишь; отказ от
предложенной починки сам дефект не закрывает.
Бывает и обратная ошибка: отмахнуться от неудобной находки. Отклонение с
доводом от контракта или теста весит столько же, сколько исправление.
Отклонение без довода ничем не лучше пропущенной находки.
Следи за сложностью. Исправление, которое добавляет больше, чем убирает
риска, — повод остановиться и поискать путь проще. Замечание о стиле без
показанного вреда — не дефект.
Исход каждой находки — один из четырёх:
- `FIXED` — подтверждена и исправлена; названа проверка, которая это
показывает (по возможности — тест, воспроизводящий дефект);
- `REJECTED` — не подтвердилась или неприменима; приведён довод;
- `OUT_OF_SCOPE` — верна, но починка выходит за границы задачи;
- `NEEDS_DECISION` — нужен выбор цели, устройства или допустимого риска.
Последние два не решай молча правкой кода: вынеси пользователю с вариантами,
их ценой и своей рекомендацией. Если без ответа дальше идти нельзя — спроси
сразу, иначе — в итоговом отчёте.
Резолюции пиши файлом в обменный каталог, по строке на находку:
`ID | исход | довод или проверка`. Этот файл читает ревьюер в следующем
круге, по нему же собирается отчёт пользователю.
## Следующие круги
Дороже всего в ревью — независимый контекст, который ревьюер набирает в
первом заходе: устройство кода, постановка, свои опыты. Набирается он один
раз. Все следующие круги отправляй в ту же сессию ревьюера:
- **узкая перепроверка** — когда исправления нетривиальны или отклонение
могут оспорить. Отдай [`prompts/recheck.md`](prompts/recheck.md), путь к
своим резолюциям и что изменилось после ревью (diff-команду или список
файлов). Ревьюер проверяет только свои находки и оценивает, не вышла ли
починка шире нужного;
- **полный проход по новой версии** — когда изменения вышли за пределы
починок. Опиши, что изменилось, и попроси пройти новую версию теми же
линзами; ID прежних находок сохраняются.
Свежий ревьюер нужен, когда прежний контекст мешает: ревьюер держится за
свои прошлые выводы, его сессия переполнена, его отчёт оказался слабым,
версия переработана так, что прежний контекст устарел, или нужен взгляд
другой модели. Если способ запуска не умеет продолжать сессию,
свежему ревьюеру дай пути к исходному поручению, отчёту и резолюциям, а
сохранённые прежним ревьюером скрипты и фикстуры он возьмёт по путям из
отчёта.
Если спор повторяется без новых фактов, ещё один круг ничего не даст: реши
сам с доводом или вынеси пользователю.
## Отчёт пользователю
Коротко и обычными словами:
- кто смотрел (модель, глубина, способ запуска), какую версию и какими
линзами;
- находки — по строке: ID, суть, исход; счётчики;
- что исправлено и чем это проверено, что отклонено и почему;
- что вынесено на решение пользователя;
- пути к полным отчётам; затраты времени и денег, если они известны.
@@ -0,0 +1,4 @@
interface:
display_name: "Перекрёстное ревью"
short_description: "Перекрёстное ревью моделью другого семейства"
default_prompt: "Используй $cross-review: покажи готовое изменение ревьюеру другого семейства моделей, разбери его находки и дай мне короткий отчёт."
@@ -0,0 +1,17 @@
## Линза: по коду
Попробуй сломать изменение. Ищи:
- неверный результат на входах и состояниях, о которых автор не подумал:
пусто, граница диапазона, параллельный доступ, частичный сбой, повтор;
- нарушенные инварианты между модулями и поломки у вызывающего кода за
пределами diff;
- обработку ошибок, которая теряет данные, прячет причину или оставляет
состояние записанным наполовину;
- безопасность: недоверенный ввод, секреты, права доступа;
- честность тестов: тест проходит при любом коде, подменяет заглушкой то,
что сам проверяет, сверяет результат с тем же вычислением, что в коде,
а не с независимым ожидаемым значением, или остаётся зелёным при той
ошибке, от которой должен защищать.
Воспроизведённый сбой лучше подозреваемого. Указывай, какой из двух у тебя.
@@ -0,0 +1,16 @@
## Линза: план
Ты проверяешь постановку, план или спеку, а не реализацию. Документ уйдёт
исполнителю, который сделает то, что написано. Ищи:
- факты, которые расходятся с кодом: имена, пути, поведение, версии;
- решения, которые документ принимает молча или оставляет открытыми там,
где исполнителю придётся гадать;
- критерии приёмки, которые нельзя проверить или можно пройти, не решив
задачу;
- отсутствие наблюдаемости: как вообще станет видно, что всё работает.
В детали реализации заходи только там, где документ молча её предрешает.
Открытые детали реализации не делают план неполным: `APPROVED` значит, что
в самом документе нет дефекта, который он должен исправить. Риски
отложенных решений записывай строками `NOTE:`.
@@ -0,0 +1,61 @@
## Линза: форма и вкус
Сильный исполнитель ошибается не в отделке кода, а в форме решения — в тех
решениях, которые постановка оставила открытыми. Чистый код такие ошибки
прячет, поэтому начни с формы, ещё до чтения изменённых файлов.
1. По одной постановке набросай ожидаемую форму решения: какое вычисление
где живёт, что и примерно в каком объёме пересекает границы процессов и
хранилищ, какие артефакты должны появиться. Хватит нескольких строк.
Это гипотеза, а не образец, которому код обязан следовать.
2. Прочитай изменения и выпиши решения, которых постановка не требовала:
где размещены вычисления, какие данные идут через границы, какие
появились новые сущности, что сделано сверх просьбы.
3. Каждое расхождение с наброском и каждое такое решение оцени по
соглашениям репозитория, по риску и по размеру задачи. Формально верное,
но по сути неправильное — тоже находка: работа не по масштабу задачи,
артефакт, который не оправдывает своего существования, проверка, которая
проходит, ничего не доказывая.
Набросок и список решений приведи в отчёте как выполненные проверки, даже
если находок нет.
### Глубина модулей
Глубокий модуль прячет много поведения за маленьким интерфейсом. Мелкий
заставляет вызывающего узнать почти столько же, сколько сам экономит.
Отмечай новые мелкие модули, слои, которые только передают вызов дальше, и
точки расширения с единственной реализацией, у которой не видно второй.
### Запахи кода
Оценивай находку о вкусе по цене жизни с этим кодом, а не по тому, ломает
ли она тест. Сверяй изменение с этим списком (Фаулер, «Рефакторинг», гл. 3).
Записанное соглашение репозитория важнее списка. Любой запах — повод для
суждения («возможно, Feature Envy»), а не нарушение правила. Пропускай то,
что уже проверяют линтеры и форматтеры.
- **Mysterious Name** — имя скрывает, что делает или хранит сущность →
переименуй; если честное имя не находится, мутно само устройство.
- **Duplicated Code** — одна и та же логика в нескольких местах → вынеси её
и вызывай из обоих.
- **Feature Envy** — функция работает с чужими данными больше, чем со
своими → перенеси её к данным.
- **Data Clumps** — одни и те же несколько полей или параметров всё время
ходят вместе → собери их в один тип.
- **Primitive Obsession** — строка или число заменяют понятие предметной
области → дай понятию свой маленький тип.
- **Repeated Switches** — один и тот же выбор по типу повторяется → полиморфизм
или одна таблица соответствий на оба места.
- **Shotgun Surgery** — одно логическое изменение требует правок по многим
файлам → собери то, что меняется вместе, в один модуль.
- **Divergent Change** — один модуль правят по несвязанным причинам →
раздели так, чтобы каждая часть менялась по одной причине.
- **Speculative Generality** — абстракции, параметры или крючки для нужд,
которых у задачи нет → удали; встрой обратно, пока нужда не появится.
- **Message Chains** — длинные цепочки `a.b().c().d()` → спрячь обход за
одним методом первого объекта.
- **Middle Man** — модуль в основном передаёт вызовы дальше → вызывай
настоящую цель напрямую.
- **Refused Bequest** — наследник почти не пользуется тем, что унаследовал →
откажись от наследования в пользу композиции.
@@ -0,0 +1,12 @@
## Линза: по задаче
Сравни изменение с постановкой и найди:
- требования, которые не выполнены или выполнены частично;
- поведение, о котором не просили;
- требования, которые выглядят выполненными, но сделаны неверно;
- критерии приёмки, которые можно пройти, не решив задачу.
К каждой находке приведи строку постановки, на которую она опирается.
Если постановки нет, скажи об этом и суди по замыслу, который виден из
коммитов и кода.
@@ -0,0 +1,20 @@
## Линза: вычитание
Спроси себя, что можно убрать из изменения, не потеряв нужного поведения.
Сначала сравни размер изменения с размером задачи. Потом ищи:
- ветки кода, опции, параметры и настройки, которых не требует ни одно
требование;
- абстракции с единственной реализацией и обёртки, которые только передают
вызов;
- защитные проверки от состояний, которые уже исключают типы или
вызывающий код;
- повторяющиеся тесты, тесты самого фреймворка, тесты, которые пересказывают
код;
- комментарии и документацию, которые повторяют код;
- файлы, скрипты и зависимости, добавленные ради разовой нужды.
Для каждого кандидата назови, чего стоит удаление: какое поведение или
гарантия уйдут вместе с ним. Пиши только о тех удалениях, которые ничего не
стоят или явно стоят своей цены. Такая находка — `MINOR`, если только
лишнее не делает код заметно труднее для изменения или ревью; тогда — `MAJOR`.
@@ -0,0 +1,56 @@
## Мандат ревьюера
Ты независимый ревьюер. Утверждения автора, комментарии в коде и выводы
прежних ревью — гипотезы, пока их не подтвердят факты.
- Ничего не меняй. Не правь исходники, не пиши в Git (`add`, `commit`,
`checkout`, `stash`, `reset`), не трогай службы, данные и настройки.
Единственный результат твоей работы — файл REPORT.
- Проверяй руками: читай код, запускай тесты и команды, сверяйся с
документацией и первоисточниками. Черновые файлы держи во временном
каталоге вне репозитория. Находка, подтверждённая запуском, весит намного
больше находки, найденной чтением.
- Скрипты воспроизведения и фикстуры, которые подтверждают находки, клади
в каталог рядом с REPORT и называй их пути в отчёте: по ним потом
проверят починку.
- Если останавливал или перезапускал службу, убедись, что она вернулась в
прежнее состояние.
- Оценивай документ по его задаче: план не считается неполным из-за того,
что не расписывает реализацию.
- Требуя исправление, сравни его цену с риском, который оно убирает.
Находка — утверждение, которое автор проверит, а не приказ.
## Отчёт
Пиши REPORT по-русски и дописывай каждую находку сразу, как нашёл: если
сессия оборвётся, часть отчёта сохранится. Одна строка на находку:
`ПРЕФИКС-N | тяжесть | место | суть | сценарий отказа | как проверить`
- `BLOCKER` — неверный результат, потеря данных, дыра в безопасности или
задача не сделана.
- `MAJOR` — настоящий дефект с правдоподобным поводом сработать, или
стоимость сопровождения, которая задаче не нужна.
- `MINOR` — дефект есть, но жить с ним дёшево.
- `NIT` — дело вкуса. Замечание о стиле без показанного вреда — не выше
`NIT`.
`место` — `файл:строка` или раздел постановки. ID не меняются: в следующих
кругах на находки ссылаются только по ID.
Заметный риск решения, которое можно спокойно отложить, пиши отдельной
строкой `NOTE: ...` — без ID и тяжести, на вердикт она не влияет.
Если находок нет, перечисли, что именно ты просмотрел и какие проверки
запускал, с результатами. Голое «одобряю» — не ревью.
Последняя строка REPORT:
`VERDICT: APPROVED | APPROVED_WITH_MINORS | CHANGES_REQUESTED`.
`CHANGES_REQUESTED` — есть хотя бы один BLOCKER или MAJOR;
`APPROVED_WITH_MINORS` — есть MINOR; иначе `APPROVED`. NIT на вердикт не
влияет.
## Итоговое сообщение
Верни только строку VERDICT, число находок по тяжести, по строке на каждую
находку BLOCKER и MAJOR (ID и суть) и `REPORT: <абсолютный путь>`.
@@ -0,0 +1,36 @@
# Узкая перепроверка твоих находок
Автор разобрал твои находки. Ниже — пути к исходному поручению, к твоему
отчёту и к резолюциям автора. Если сессия у тебя новая, сначала прочитай
все три файла: в поручении — постановка, границы проверяемой версии, мандат
и формат находок. Мандат прежний: ничего не меняй; читать и запускать тесты
можно.
Проверяй только свои находки, а не всё изменение заново. Оценивай не только
то, закрыта ли находка, но и саму починку: соразмерна ли она и не сломала ли
что-то рядом. Чаще всего лишнее приходит из твоего же предписания, так что
проверь и его. Прежде чем назвать находку
незакрытой, убедись, что закрытое состояние вообще достижимо и что его цена
соразмерна тому, что оно защищает. Это взвешивание не смягчает находок о
доступе, данных и деньгах.
Отклонение оценивай по доводу — контракт, тест, доказательство, — а не по
вкусу. Если по ходу заметил новый дефект, запиши его с новым ID в исходном
формате.
Находки с исходом `OUT_OF_SCOPE` или `NEEDS_DECISION` ждут решения
владельца задачи. Их не пересматривай: перенеси строкой `ID | DEFERRED` и
не учитывай в итоговой строке.
По строке на каждый остальной исходный ID:
`ID | CLOSED | NOT_CLOSED (почему) | REJECTION_VALID | REJECTION_INVALID (почему)`
Для исправленной находки (`FIXED`) допиши `| PROPORTIONATE` или
`| OVERREACH (что вышло шире, чем требовала находка)`. `OVERREACH` — сигнал
автору, а не блокер.
Последняя строка: `RECHECK: ALL_CLOSED | ISSUES_REMAIN`
Запиши результат по новому пути REPORT, указанному ниже, и верни только
последнюю строку, ID незакрытых находок и `REPORT: <абсолютный путь>`.
@@ -0,0 +1,84 @@
# Запуск Claude из командной строки
Для автора, у которого есть shell и установлен `claude` (Claude Code CLI).
Проверено на Claude Code 2.1.302 (Linux, 2026-10-17): запуск с промптом из
stdin, запись отчёта, продолжение по id сессии, фоновая сессия.
## Первый запуск
```bash
# D — обменный каталог с готовым brief.md, S — каталог скилла
cat "$D/brief.md" "$S/prompts/mandate.md" "$S/prompts/lens-shape.md" \
> "$D/prompt.md"
cat "$D/prompt.md" | claude -p --permission-mode auto --model opus \
--effort high --add-dir "$D" --output-format json \
> "$D/out.json" 2> "$D/err.txt"
jq -r '.session_id, .result, .is_error' "$D/out.json"
```
- **`--model opus`** — псевдоним последней модели Opus, он не устаревает с
выходом новых версий. Глубина ревью — `high`; выше — по просьбе
пользователя.
- **`--permission-mode auto`** разрешает запускать тесты, ходить в сеть и
создавать временные файлы; опасные действия отсекает встроенный
классификатор. Исходники защищает мандат.
- **`--add-dir "$D"`** разрешает запись отчёта в обменный каталог.
- **`--output-format json`** возвращает `session_id` для перепроверки,
`result` — финальное сообщение, `modelUsage` — фактическую модель и
стоимость для отчёта пользователю.
## Ожидание
`claude -p` держит процесс до конца ревью, а это часто дольше 10 минут.
Если твой инструмент shell столько не ждёт, есть два пути:
- запусти ту же команду фоном средствами своей среды и жди завершения
процесса. Появление `out.json` ничего не значит: перенаправление создаёт
пустой файл сразу при запуске;
- или запусти фоновую сессию самого Claude:
```bash
claude --bg --permission-mode auto --model opus --effort high \
--add-dir "$D" "Прочитай $D/prompt.md и выполни." < /dev/null
```
Команда сразу печатает короткий id. Состояние сессии —
`claude agents --json --all` (поля `id`, `sessionId`, `state`); жди
`state: done`. Сам `REPORT` ревьюер дописывает по ходу, поэтому его
появление тоже не признак конца. `claude logs` выдаёт сырой вывод
терминала, агенту он бесполезен. Завершённую сессию останавливай
`claude stop <id>`.
В любом случае перед разбором проверь, что последняя строка `REPORT` —
`VERDICT: …`: иначе отчёт неполный.
## Затраты
`out.json` содержит `total_cost_usd`, `usage` (токены) и `modelUsage`
(разбивка по моделям, включая служебные вызовы).
## Следующие круги
Узкая перепроверка или полный проход по новой версии — продолжением той же
сессии. Что положить в поручение — в разделе «Следующие круги» SKILL.md.
```bash
cat "$D/recheck-prompt.md" | claude -p --resume <session-id> \
--output-format json > "$D/recheck.json" 2> "$D/recheck-err.txt"
```
Модель сохраняется из первого запуска (проверено: продолжение шло на той же
Opus). Режим доступа и глубину заново не задавай; сверь, что `session_id` в
ответе совпал с исходным. Для фоновой сессии из `--bg` бери её `sessionId`
из `claude agents --json --all`.
## Особенности ревьюеров Claude
- Однострочный формат находок держат хуже: бывают многострочные записи и
развёрнутые подтверждения. Проверь индекс перед разбором.
- Видят картинки: графики, скриншоты, выводы ноутбуков. Если проверяемый
результат визуальный, это довод за Claude.
- Сильнее в механике кода и форме решения: нереализуемые формулировки,
мёртвые заглушки, неудачные имена и границы.
@@ -0,0 +1,103 @@
# Запуск Codex из командной строки
Для автора, у которого есть shell и установлен `codex`. Проверено на
codex-cli 0.163.0 (Linux, 2026-10-17): первый запуск, продолжение по id,
запись отчёта в обменный каталог.
## Перед первым запуском
На новой машине и после обновления Codex проверь, что команды внутри
песочницы вообще выполняются:
```bash
codex sandbox -c 'sandbox_mode="workspace-write"' -- sh -c 'echo SANDBOX_OK'
```
На Linux отказ вида `bwrap: ... Failed RTM_NEWADDR` обычно значит, что
AppArmor запрещает непривилегированные user namespaces. Это настройка машины,
а не ревью: сообщи пользователю и сошлись на
[требования песочницы](https://learn.chatgpt.com/docs/sandboxing#prerequisites).
Работающий веб-поиск о песочнице ничего не говорит.
## Первый запуск
```bash
# D — обменный каталог с готовым brief.md, S — каталог скилла
cat "$D/brief.md" "$S/prompts/mandate.md" "$S/prompts/lens-code.md" \
> "$D/prompt.md"
set -o pipefail
cat "$D/prompt.md" | codex exec --json --strict-config --approve-for-me \
--add-dir "$D" -m <model> -c 'model_reasoning_effort="<effort>"' \
-o "$D/final.md" - > "$D/log.jsonl" 2> "$D/err.txt"
```
- **Промпт — каналом через stdin** (`cat … | codex exec … -`): так он
доходит в любой среде. Поток событий и ошибки — в файлы; в свой контекст
читай только `final.md` и отчёт `REPORT`.
- **`--approve-for-me`** даёт песочницу `workspace-write` и сам одобряет
выход за неё: сеть, docker, команды вне каталога. Вместе с `-s` он не
принимается. Шире — `danger-full-access` — ревьюеру не нужно.
- **`--add-dir "$D"`** разрешает запись отчёта в обменный каталог.
- **Модель и глубину задавай явно.** Иначе они берутся из
`~/.codex/config.toml` и зависят от последней интерактивной сессии
пользователя. На 2026-10 для ревью кода обычно лучше всего подходит
`gpt-6.1-sol` с глубиной `high`. `gpt-6-astra` с `xhigh` сильнее и
заметно дороже: бери её по просьбе пользователя или когда ошибка особенно
дорога. Названия моделей быстро устаревают: при сомнении спроси
пользователя.
- **`--strict-config`** останавливает запуск при неизвестном ключе `-c`.
Без него опечатка молча заменяется настройкой по умолчанию.
## Ожидание
Ревью идёт от нескольких минут до получаса. Запускай команду фоном средствами
своей среды: в Claude Code — Bash с `run_in_background`, без `&` внутри
команды; среда разбудит тебя по завершении.
- **id сессии** — в первой строке лога:
`head -1 "$D/log.jsonl" | jq -r .thread_id`. Сохрани его сразу: по нему
идут перепроверка и спасение, если процесс умер.
- **Признак жизни** — рост `log.jsonl`. Долгая сборка или тесты внутри
песочницы событий не пишут, поэтому застывший лог — повод посмотреть, а не
приговор.
- **Вердикт без находок** проверь по логу: работа это или отписка.
`grep -c '"command_execution"' "$D/log.jsonl"` — число выполненных команд.
Голое APPROVED при нуле команд перезапускай.
## Затраты
Токены по каждому ходу пишутся в лог:
`jq -c 'select(.type=="turn.completed") | .usage' "$D/log.jsonl"`.
Стоимости в выводе нет.
## Следующие круги
Узкая перепроверка или полный проход по новой версии — продолжением той же
сессии. Что положить в поручение — в разделе «Следующие круги» SKILL.md.
```bash
cat "$D/recheck-prompt.md" | codex exec resume --json --strict-config \
-m <model> -c 'model_reasoning_effort="<effort>"' \
-o "$D/recheck-final.md" <thread-id> - \
> "$D/recheck-log.jsonl" 2> "$D/recheck-err.txt"
```
- **Только по явному id.** `--last` подхватит последнюю сессию, а при
нескольких ревьюерах это может быть чужая.
- Песочница и доступ к `$D` принадлежат сессии и сохраняются; ключей `-s`,
`--approve-for-me` и `--add-dir` у `resume` нет.
- Модель и глубину повтори явно — те же, что при первом запуске: иначе
подставятся значения из конфига, а смена глубины внутри сессии сбрасывает
кэш.
## Особенности ревьюеров Codex
- Мандат и формат отчёта держат надёжно.
- Бывает, требуют исправлений сверх цели задачи; в других прогонах все
починки выходили соразмерными. Проверяй соразмерность как обычно.
- Хорошо ловят уровень требований: недостающие критерии, наблюдаемость,
«формально отчитаться можно».
- Могут заглянуть в свою память (`~/.codex/memories/`) и подтянуть выводы
прошлых задач. Свежая сессия Codex поэтому не полностью свободна от
прошлого опыта.
@@ -0,0 +1,102 @@
# Запуск через T3 Code
Для автора, который работает внутри T3 Code и видит инструменты
`orchestrator_capabilities` и `delegate_task` (имена могут идти с префиксом
вроде `mcp__t3-code__`). Приложение само следит за дочерней задачей, будит
тебя по её завершении, а пользователь видит ход ревью в отдельном разговоре.
Проверено 2026-10-17: Claude → Codex.
## Выбор модели
Модель и глубину выбирай по адаптеру семейства ревьюера:
[Codex](launch-codex.md#первый-запуск) или
[Claude](launch-claude.md#первый-запуск). Вызови `orchestrator_capabilities`,
найди у нужного провайдера эту модель (`providerInstanceId`, `model`) и
проверь, что она есть в списке. Ключ глубины у провайдеров разный —
смотри `options` модели: у Codex это `reasoningEffort`, у Claude —
`effort`.
## Запуск
Собери `prompt.md` так же, как для командной строки, и передай его путь:
```json
{
"task": "Прочитай /tmp/…/prompt.md и выполни.",
"target": {
"providerInstanceId": "codex",
"model": "<model>",
"options": { "reasoningEffort": "high" }
},
"role": "review",
"mode": "async",
"title": "cross-review: <что проверяем>",
"clientRequestId": "<уникальный id этого раунда>"
}
```
- **`isolation` не задавай** (по умолчанию — общий checkout). Отдельный
worktree строится от последнего коммита, и незакоммиченные правки
ревьюер не увидит.
- **`runtimeMode` не задавай.** Режимы с подтверждением остановят ревьюера
на первом же действии и будут ждать человека в его разговоре. Ограничения
задаёт мандат в поручении. Режим наследуется от твоего разговора, а шире
своего задать нельзя: если ты сам работаешь с подтверждением, скажи
пользователю до запуска.
- После запуска заверши ход: уведомление о завершении придёт само.
`task_status` нужен, только если результат требуется посреди хода.
- Итоговое сообщение ревьюера приходит в `summary`; полный отчёт — в файле
`REPORT`.
- Вердикт без находок проверь по активности разговора: `t3_thread_read` с
`view: "activity"` показывает выполненные команды (`command_execution`).
Голое APPROVED без команд — отписка, перезапускай.
## Следующие круги
Следующие круги — узкую перепроверку и полный проход по новой версии —
отправляй тому же ревьюеру, в его дочерний разговор, когда предыдущая
задача уже завершилась. Это `t3_thread_send`:
```json
{
"threadId": "<childThreadId из ответа delegate_task>",
"message": "Прочитай /tmp/…/recheck-prompt.md и выполни.",
"mode": "auto",
"clientRequestId": "<уникальный id этого круга>"
}
```
- В дочернем разговоре появляется новая задача со своим `taskId`; по
завершении она будит тебя так же, как исходная. Ребёнок помнит свои
прошлые ходы (проверено 2026-10-17 на Codex).
- Модель, глубина и режим остаются от исходной задачи. Модель и глубину
можно сменить через `t3_thread_configure`, но это сбрасывает кэш и
отчасти обесценивает продолжение — оставляй как есть.
- `childThreadId` запиши в реестр сразу после запуска ревью: разговор
хранит T3, но его id нужен тебе.
- Что положить в поручение круга — в разделе «Следующие круги» SKILL.md.
Свежий ревьюер — новый `delegate_task` с новым `clientRequestId`. Когда он
нужен — там же, в SKILL.md. Тем же путём иди, если продолжить разговор не
удалось или системные инструкции T3 для тебя обязательны (о них ниже). В
поручение свежей задачи положи пути к исходному `prompt.md`, отчёту
ревьюера и своим резолюциям.
Системные инструкции T3 и описание `t3_thread_send` говорят об этом
по-разному. Первые считают каждый круг ревью независимым: новый
`delegate_task`, без продолжения через `t3_thread_send`. Второе оставляет
`delegate_task` независимым кругам, а продолжение, которому нужен разговор
того же ребёнка, отправляет в `childThreadId`. Этот скилл считает следующие
круги продолжением: ревьюер работает по уже набранному контексту, это
дешевле и глубже. В прогонах 2026-10-17 круги-продолжения шли в 2–4 раза
быстрее первого захода.
Это расходится с замыслом авторов T3 и сделано сознательно: владелец скилла
ставит повторное использование контекста ревьюера выше независимости
каждого круга. T3 и скилл развиваются; если инструкции T3 изменятся,
этот выбор стоит пересмотреть.
## Затраты
T3 сообщает только время выполнения. Токены и стоимость видны в выводе
командной строки — см. адаптеры Codex и Claude.
-3
View File
@@ -2,6 +2,3 @@
*
!skills
!.gitignore
!settings.local.json
!statusline-command.sh
!CLAUDE.md
-19
View File
@@ -1,19 +0,0 @@
# Глобальный словарь рабочего процесса
Термины владельца, общие для всех проектов на этой машине.
## Ревью по температуре
Ось — сколько авторского контекста несёт ревьюер:
- **Горячее ревью** — автор сам перечитывает свой артефакт глазами читателя,
не автора. Контекст полный, дёшево. Текстовые огрехи правит по месту молча;
находку уровня решений (противоречие, дыра в охвате) предъявляет владельцу,
не исправляя и не замалчивая.
- **Тёплое ревью** — другой агент, получивший бриф. Контекст частичный: знает
замысел, но не варился в нём.
- **Холодное ревью** — свежая сессия или субагент без общего контекста.
Независимо, дороже всех.
Просьба «перечитай свежим взглядом» двусмысленна — уточни температуру;
по умолчанию имеется в виду горячее.
-120
View File
@@ -1,120 +0,0 @@
{
"permissions": {
"allow": [
"Bash(cat *)",
"Bash(ls)",
"Bash(ls *)",
"Bash(wc *)",
"Bash(head *)",
"Bash(tail *)",
"Bash(find *)",
"Bash(tree)",
"Bash(tree *)",
"Bash(file *)",
"Bash(stat *)",
"Bash(diff *)",
"Bash(realpath *)",
"Bash(readlink *)",
"Bash(basename *)",
"Bash(dirname *)",
"Bash(sort *)",
"Bash(uniq *)",
"Bash(cut *)",
"Bash(awk *)",
"Bash(sed *)",
"Bash(grep *)",
"Bash(rg *)",
"Bash(jq *)",
"Bash(yq *)",
"Bash(tr *)",
"Bash(echo *)",
"Bash(printf *)",
"Bash(which *)",
"Bash(type *)",
"Bash(command -v *)",
"Bash(env)",
"Bash(env *)",
"Bash(printenv)",
"Bash(printenv *)",
"Bash(pwd)",
"Bash(uname *)",
"Bash(whoami)",
"Bash(id)",
"Bash(date)",
"Bash(date *)",
"Bash(hostname)",
"Bash(git log*)",
"Bash(git diff*)",
"Bash(git status*)",
"Bash(git show*)",
"Bash(git branch*)",
"Bash(git remote -v*)",
"Bash(git remote show*)",
"Bash(git tag*)",
"Bash(git stash list*)",
"Bash(git rev-parse*)",
"Bash(git ls-files*)",
"Bash(git blame*)",
"Bash(git shortlog*)",
"Bash(git config --get*)",
"Bash(git config --list*)",
"Bash(git fetch*)",
"Bash(git add*)",
"Bash(docker ps*)",
"Bash(docker logs*)",
"Bash(docker inspect*)",
"Bash(docker images*)",
"Bash(docker stats*)",
"Bash(docker info*)",
"Bash(docker version*)",
"Bash(docker compose ps*)",
"Bash(docker compose logs*)",
"Bash(docker compose:*)",
"Bash(docker restart:*)",
"Bash(docker exec:*)",
"Bash(docker run:*)",
"Bash(docker stop:*)",
"Bash(docker rm:*)",
"Bash(curl *)",
"Bash(wget *)",
"Bash(mkdir *)",
"Bash(cp *)",
"Bash(mv *)",
"Bash(touch *)",
"Bash(chmod *)",
"Bash(ln *)",
"Bash(npm *)",
"Bash(npx *)",
"Bash(yarn *)",
"Bash(pnpm *)",
"Bash(bun *)",
"Bash(go *)",
"Bash(cargo *)",
"Bash(make *)",
"Bash(python *)",
"Bash(python3 *)",
"Bash(pip *)",
"Bash(pip3 *)",
"Bash(pytest *)",
"Bash(node *)",
"Bash(deno *)",
"Bash(stow *)",
"Bash(gh api:*)",
"Bash(gh search:*)",
"Bash(gh issue:*)",
"Bash(gh repo:*)",
"Bash(codex *)",
"Bash(timeout 600 codex exec *)",
"Bash(rm -f /tmp/claude-plan-*)",
"Bash(rm -f /tmp/codex-*)",
"WebSearch",
"mcp__claude_ai_Context7__resolve-library-id",
"Write(/tmp/claude-plan-*)",
"Write(/tmp/codex-*)"
]
},
"statusLine": {
"type": "command",
"command": "bash ~/.claude/statusline-command.sh"
}
}
-116
View File
@@ -1,116 +0,0 @@
#!/usr/bin/env bash
# Статусная строка Claude Code. JSON приходит на stdin.
#
# user@host:.../dir (branch) | Model Name (NM context) ctx:N% | 5h:N% 7d:N%
#
# Работает в Linux и в Git Bash на Windows: без `hostname -s`, jq не обязателен.
# Порог, после которого путь сокращается до .../<последний каталог>
MAX_PATH_LEN="${STATUSLINE_MAX_PATH_LEN:-28}"
input="$(cat)"
# ── Разбор JSON ───────────────────────────────────────────────────────
# Числовое поле $2 внутри объекта $1 (резервный разбор, без jq).
json_num() {
printf '%s' "$input" \
| tr -d ' \t\n\r' \
| grep -o "\"$1\":{[^}]*}" \
| grep -o "\"$2\":[0-9.eE+-]*" \
| head -1 \
| cut -d: -f2
}
# Строковое поле $2 внутри объекта $1 (резервный разбор, без jq).
json_str() {
printf '%s' "$input" \
| tr -d '\n\r' \
| grep -o "\"$1\"[[:space:]]*:[[:space:]]*{[^}]*}" \
| grep -o "\"$2\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" \
| head -1 \
| sed 's/.*"[[:space:]]*:[[:space:]]*"//; s/"$//'
}
if command -v jq >/dev/null 2>&1; then
IFS=$'\t' read -r cwd display_name ctx_pct rate5h rate7d <<EOF
$(printf '%s' "$input" | jq -r '
def num: if type == "number" then tostring else "" end;
[ (.workspace.current_dir // ""),
(.model.display_name // ""),
(.context_window.used_percentage | num),
(.rate_limits.five_hour.used_percentage | num),
(.rate_limits.seven_day.used_percentage | num)
] | @tsv' 2>/dev/null)
EOF
else
cwd="$(json_str workspace current_dir)"
display_name="$(json_str model display_name)"
ctx_pct="$(json_num context_window used_percentage)"
rate5h="$(json_num five_hour used_percentage)"
rate7d="$(json_num seven_day used_percentage)"
fi
# Проценты в JSON дробные — округляем до целых.
round_pct() {
case "$1" in
''|null) return 0 ;;
esac
awk -v v="$1" 'BEGIN { printf "%d", (v < 0 ? -int(-v + 0.5) : int(v + 0.5)) }' 2>/dev/null
}
ctx_pct="$(round_pct "$ctx_pct")"
rate5h="$(round_pct "$rate5h")"
rate7d="$(round_pct "$rate7d")"
# ── Путь ──────────────────────────────────────────────────────────────
# В Git Bash current_dir может прийти с обратными слэшами.
path="${cwd//\\//}"
home="${HOME//\\//}"
if [ -n "$home" ]; then
if [ "$path" = "$home" ]; then
path="~"
else
case "$path" in
"$home"/*) path="~${path#"$home"}" ;;
esac
fi
fi
if [ "${#path}" -gt "$MAX_PATH_LEN" ]; then
path=".../${path##*/}"
fi
# ── Остальные секции ──────────────────────────────────────────────────
branch=""
if [ -n "$cwd" ]; then
branch="$(git -C "$cwd" rev-parse --abbrev-ref HEAD 2>/dev/null)"
fi
# Размер контекста уже внутри display_name, убираем только префикс «Claude ».
model="${display_name#Claude }"
user="${USER:-${USERNAME:-$(id -un 2>/dev/null)}}"
host="${HOSTNAME:-$(hostname 2>/dev/null)}"
host="${host%%.*}"
GREEN=$'\033[32m'
YELLOW=$'\033[33m'
PURPLE=$'\033[35m'
CYAN=$'\033[36m'
RESET=$'\033[0m'
out="${GREEN}${user}@${host}${RESET}:${YELLOW}${path}${RESET}"
[ -n "$branch" ] && out="${out} ${PURPLE}(${branch})${RESET}"
out="${out} | ${CYAN}${model}${RESET}"
[ -n "$ctx_pct" ] && out="${out} ctx:${ctx_pct}%"
rates=""
[ -n "$rate5h" ] && rates="${rates} 5h:${rate5h}%"
[ -n "$rate7d" ] && rates="${rates} 7d:${rate7d}%"
[ -n "$rates" ] && out="${out} |${rates}"
printf '%s\n' "$out"
+1 -1
View File
@@ -1,3 +1,3 @@
# Claude Code управляется вручную через install.sh (копирование, не симлинк).
# Claude Code управляется вручную через install.sh (точечные симлинки).
# Этот файл не даёт stow случайно создать симлинк ~/.claude.
\.claude
-1
View File
@@ -1 +0,0 @@
../../../agents/.agents/skills/conventional-commits
-1
View File
@@ -1 +0,0 @@
../../../agents/.agents/skills/coordinator-loop
+40
View File
@@ -0,0 +1,40 @@
# SearXNG — резервный поиск
Локальный SearXNG на `127.0.0.1:26745`. Постоянно не нужен: с 2026-10-17
основной поиск идёт через встроенные `websearch`/`webfetch` OpenCode, а
MCP-сервер `searxng` отключён в `opencode/.config/opencode/opencode.json`.
Стек оставлен как запасной вариант, если встроенный поиск недоступен
(например, кончилась подписка OpenCode Go) или нужен фильтр по времени.
## Запуск и остановка
```
docker compose up -d # поднять
docker compose down # погасить, кэш и настройки в томах сохранятся
docker logs --since 10m searxng-core
```
Проверка: `curl -s -H 'Accept: application/json' http://127.0.0.1:26745/config | head -c 200`
## Возврат MCP-сервера
Добавить в `mcp.servers` файла `opencode/.config/opencode/opencode.json`:
```json
"searxng": {
"type": "local",
"command": ["npx", "-y", "mcp-searxng"],
"environment": { "SEARXNG_URL": "http://localhost:26745" }
}
```
После правки — разложить конфиг установщиком (`./install.sh`) или скопировать
файл в `~/.config/opencode/opencode.json`, затем перезапустить OpenCode.
## Ограничения
- Настройки по умолчанию (`use_default_settings: true`), форматы `html` и `json`.
- С домашнего IP часть движков быстро упирается в лимиты: `brave` и `wikipedia`
отвечают `too many requests`, `duckduckgo` — CAPTCHA. Остаётся `google cse`,
то есть полноценной агрегации из многих движков ждать не стоит.
- Контейнер не обновляется сам: `docker compose pull && docker compose up -d`.
@@ -0,0 +1,75 @@
# Эксперимент: ИИ-ревьюер разрешений в OpenCode (autoapprove)
**Дата:** 2026-10-11
**Статус:** эксперимент закрыт, плагин снят. Документ сохранён как описание
системы и причин отказа — если идея вернётся, начинать отсюда.
## Итог одним абзацем
Плагин `opencode-permission-reviewer` (LLM-ревьюер каждого `ask`) настроен и
работоспособен, но признан не стоящим использования: хрупкая цепочка (ключ в
окружении сервиса → региональный 451 на endpoint jev → прокси), плюс ревьюер
нервно эскалует всё подряд в сессиях с «опасным» транскриптом. Боевой режим:
`permission: autoaccept` + deny-паттерны на секреты + snapshots/git как
обратимость. AFK для системного администрирования не достигнут: узкое место —
не разрешения, а компетентность модели-исполнителя (glm flash провалил простые
админские задачи: «делал странное с умным видом»).
## Как была устроена система
Цепочка решений для каждого `ask` из `opencode.json`:
1. **Deny-паттерны** (`permission.bash`) — мгновенный отказ по строке команды,
до моделей. Только секреты: `*.env*`, `*.secrets*`, `~/.ssh/*` и т.п.
Паттерны слепы к содержимому скриптов — защита секретов, не рубеж.
2. **Jev** (`opencode/jev-1.13`, System One) — типизированный ревьюер: команда,
свидетельства из транскрипта, содержимое локальных скриптов (read-only
enrichment). Отвечает allow / deny / escalate.
3. **Второе мнение** (`opencode-go/glm-5.3-flash`) — трудные случаи, где jev
неуверен.
4. **Финал** — `escalationMode`: `manual` (спросить человека; при
`permission=autoaccept` на него отвечает auto) или `deny` (отказ с причиной).
`enforcementMode`: `observe` (только аудит, дефолт) или `enforce`.
## Инфраструктурные грабли (главная ценность эксперимента)
- Jev ходит на отдельный endpoint Zen `/zen/v1/systemone`. Zen закрывает его
по региону (HTTP 451) — нужен прокси в окружении сервиса
(`HTTPS_PROXY` в `~/.env.secrets`). Чат-модели Zen
(`/zen/v1/chat/completions`) доступны без прокси; блокирован только
systemone. Jev нет в общем каталоге моделей — это намеренно (не чат-модель),
на плагин не влияет.
- `OPENCODE_API_KEY` должен быть в окружении **процесса сервиса**. Сервис
наследует окружение шелла, из которого запущен; после правки
`~/.env.secrets` нужны `source` + рестарт из свежего шелла. Рестарт из
старого шелла ключ не подхватывает.
- Ревьюер зовётся из серверного процесса: рестарт сервиса из сессии агента
рвёт саму сессию.
- Emergency brake: команды, пахнущие выносом креденшелов
(`curl -H "Authorization: ... $KEY"`), режутся детерминированно.
- Конфиг плагина читается с O_NOFOLLOW: симлинк молча игнорируется, деплой
только копией (см. `.stow-local-ignore`).
## Известные свойства поведения
- Сессия, полная опасных команд в транскрипте, выглядит для ревьюера
подозрительно: jev эскалует всё подряд, и с `escalationMode: deny` такая
сессия становится неработоспособной — fail-closed по замыслу.
- В этой сессии это проявилось полностью: заперт даже `git status`, отменить
отказы изнутри сессии нельзя (явное разрешение в чате не помогло).
- Пороги тонкой настройки (не крутились): `systemOneConfidenceThreshold`
(дефолт 0.4), `systemOneReasoningThreshold` (дефолт 0.38),
`confidenceThreshold` (чат-ревьюер, 0.7).
- Установлено про авто-подтверждение без плагина: ручное approve при серии
запросов вырождается в слепое «да» — честный auto + обратимость лучше
декоративного ручного.
## Ссылки
- Плагин и все опции: https://github.com/warc0s/opencode-permission-reviewer
(README: https://npm.io/package/opencode-permission-reviewer)
- Модель разрешений OpenCode V2: https://opencode.ai/v2/docs/permissions
- Jev: https://jevmodel.org/ (прямой API TypeSafe с ключом `TYPESAFE_API_KEY` —
запасной маршрут мимо Zen)
- Аудит решений (на машине): `~/.local/share/opencode/permission-reviewer-audit.jsonl`
- Снятый конфиг сохранён закомментированным в `opencode/.config/opencode/opencode.json`
+8 -1
View File
@@ -156,6 +156,13 @@ Xray всё равно можно остановить.
повторный query-параметр — отказ: он может менять смысл соединения, и его
нужно добавить в разбор явно. Остальное проверяет `xray run -test`.
Разбор следует [спецификации ссылок Xray](https://github.com/XTLS/Xray-core/discussions/716):
поддерживает `ech`, `pqv`, `spx`, `fm`, необязательные поля и значения по
умолчанию. Дополнительные параметры XHTTP переносятся внутрь `extra`, чтобы
ядро их не потеряло. Старые `sessionPlacement`/`sessionKey` преобразуются в
`sessionIDPlacement`/`sessionIDKey`, в том числе для отдельного скачивания;
противоречивые значения отклоняются. Полный список — в README пакета.
Hiddify Manager добавляет `allowInsecure`/`insecure` даже к профилям с
нормальным сертификатом
([Hiddify-Manager#5438](https://github.com/hiddify/Hiddify-Manager/issues/5438)),
@@ -169,7 +176,7 @@ TLS-проверку задают `pcs` и `vcn`.
### Установка Xray
`proxy-client install` ставит в `~/.local/bin/xray` официальный архив одного
зафиксированного релиза (сейчас 26.3.27), проверяет SHA-256 по `.dgst` и
зафиксированного релиза (сейчас 26.10.10), проверяет SHA-256 по `.dgst` и
заменяет файл атомарно. Официальный установщик Xray не подходит: он требует
root и создаёт systemd-службы. `start` принимает только ту же версию и никогда
ничего не скачивает.
+76 -57
View File
@@ -144,28 +144,36 @@ wsl_conf_has_boot_command() {
' "$1" 2>/dev/null
}
# Миграция: если ~/.claude — stow-симлинк, раскрыть в директорию.
migrate_claude_stow_symlink() {
[ -L "$HOME/.claude" ] || return 0
# Миграция: если $1 — stow-симлинк (пакет был свёрнут в репозиторий),
# раскрыть в настоящий каталог и перенести содержимое. Остальные аргументы —
# имена git-tracked файлов, которые остаются в репозитории.
migrate_folded_symlink() {
local link="$1"
shift
[ -L "$link" ] || return 0
info "Миграция: ~/.claude — симлинк, преобразую в директорию..."
info "Миграция: $link — симлинк, преобразую в директорию..."
local stow_dir
stow_dir="$(readlink -f "$HOME/.claude" 2>/dev/null || true)"
rm "$HOME/.claude"
mkdir -p "$HOME/.claude"
stow_dir="$(readlink -f "$link" 2>/dev/null || true)"
rm "$link"
mkdir -p "$link"
if [ -n "$stow_dir" ] && [ -d "$stow_dir" ]; then
for item in "$stow_dir"/* "$stow_dir"/.[!.]*; do
[ -e "$item" ] || continue
local base
[ -e "$item" ] || [ -L "$item" ] || continue
local base skip s
base=$(basename "$item")
# git-tracked файлы остаются в репозитории
case "$base" in .gitignore|settings.local.json|skills) continue ;; esac
mv "$item" "$HOME/.claude/" 2>/dev/null || true
skip=""
for s in "$@"; do
[ "$base" = "$s" ] && skip=1
done
info "Рантайм-данные перенесены в ~/.claude/"
[ -n "$skip" ] && continue
mv "$item" "$link/" 2>/dev/null || true
done
info "Рантайм-данные перенесены в $link/"
else
warn "Не удалось прочитать источник старого симлинка ~/.claude, продолжаю без миграции данных"
warn "Не удалось прочитать источник старого симлинка $link, продолжаю без миграции данных"
fi
}
@@ -178,7 +186,7 @@ migrate_claude_stow_symlink() {
#
# Обход повторяет логику stow, а не перебирает файлы подряд: если каталога в
# HOME нет, конфликта быть не может и всё поддерево пропускается. Это заодно
# отсекает свёрнутые пакеты (~/.codex, ~/.vscode-server) с их гигабайтами
# отсекает свёрнутые пакеты (~/.vscode-server) с их гигабайтами
# рантайм-данных.
backup_conflicting_stow_targets() {
local pkg=""
@@ -370,7 +378,7 @@ if ! command -v "$STOW_BIN" &>/dev/null; then
exit 1
fi
PACKAGES=(bash tmux vim wezterm kitty vscode agents codex gemini proxy-client chatgpt)
PACKAGES=(bash tmux vim wezterm kitty vscode agents gemini proxy-client chatgpt)
AVAILABLE_PACKAGES=()
info "Применяю stow пакеты..."
@@ -429,68 +437,79 @@ SKILLS_SRC="$DOTFILES_DIR/agents/.agents/skills"
OPENCODE_DIR="$HOME/.config/opencode"
OPENCODE_SRC_DIR="$DOTFILES_DIR/opencode/.config/opencode"
OPENCODE_JSON_SRC="$OPENCODE_SRC_DIR/opencode.json"
OPENCODE_REVIEWER_SRC="$OPENCODE_SRC_DIR/permission-reviewer.jsonc"
mkdir -p "$OPENCODE_DIR"
if [ -d "$OPENCODE_SRC_DIR/agents" ]; then
ensure_symlink "$OPENCODE_SRC_DIR/agents" "$OPENCODE_DIR/agents"
# Профили safe-build/safe-plan удалены: агенты встроенные (build/plan),
# решения об "ask" принимает permission-reviewer. Старый симлинк чистим.
if [ -L "$OPENCODE_DIR/agents" ]; then
case "$(readlink "$OPENCODE_DIR/agents" 2>/dev/null || true)" in
"$OPENCODE_SRC_DIR"*)
rm "$OPENCODE_DIR/agents"
info "Удалён устаревший симлинк ~/.config/opencode/agents"
;;
esac
fi
if [ -d "$SKILLS_SRC" ]; then
ensure_symlink "$SKILLS_SRC" "$OPENCODE_DIR/skills"
# Скиллы opencode сам берёт из ~/.agents/skills и ~/.claude/skills. Прежняя
# ссылка ~/.config/opencode/skills загружала тот же набор ещё раз, и opencode
# сыпал предупреждениями о дублях. Чистим её.
if [ -L "$OPENCODE_DIR/skills" ]; then
case "$(readlink "$OPENCODE_DIR/skills" 2>/dev/null || true)" in
"$SKILLS_SRC"*)
rm "$OPENCODE_DIR/skills"
info "Удалён лишний симлинк ~/.config/opencode/skills"
;;
esac
fi
if [ -f "$OPENCODE_JSON_SRC" ]; then
copy_managed_file "$OPENCODE_JSON_SRC" "$OPENCODE_DIR/opencode.json" "opencode.json"
fi
# ── Claude Code ──────────────────────────────────────────────────────
# settings.local.json ЛИНКУЕТСЯ из репозитория (базовые permissions + statusLine).
# Claude Code не модифицирует settings.local.json — только читает и мержит
# с settings.json, который Claude Code создаёт и обновляет самостоятельно.
# Машинно-специфичные настройки (model, effortLevel) — в settings.json.
# Настройки permission-reviewer: плагин читает их только из глобального
# ~/.config/opencode, из репозитория они не подхватываются — копируем.
if [ -f "$OPENCODE_REVIEWER_SRC" ]; then
copy_managed_file "$OPENCODE_REVIEWER_SRC" "$OPENCODE_DIR/permission-reviewer.jsonc" "permission-reviewer.jsonc"
fi
migrate_claude_stow_symlink
# ── Claude Code ──────────────────────────────────────────────────────
# Из репозитория линкуются только skills. Настройки (settings.json,
# settings.local.json) принадлежат Claude Code и живут только на машине.
migrate_folded_symlink "$HOME/.claude" .gitignore skills
mkdir -p "$HOME/.claude"
# Уборка артефактов прежних версий установщика: битые симлинки на удалённые
# из репозитория файлы и забытая копия statusline (наша — по строке-маркеру).
for stale in CLAUDE.md settings.local.json; do
stale_path="$HOME/.claude/$stale"
if [ -L "$stale_path" ]; then
case "$(readlink "$stale_path" 2>/dev/null || true)" in
"$DOTFILES_DIR"/*)
rm "$stale_path"
info "Удалён устаревший симлинк ~/.claude/$stale"
;;
esac
fi
done
if [ -f "$HOME/.claude/statusline-command.sh" ] && [ ! -L "$HOME/.claude/statusline-command.sh" ] \
&& grep -q "Статусная строка Claude Code" "$HOME/.claude/statusline-command.sh" 2>/dev/null; then
rm "$HOME/.claude/statusline-command.sh"
info "Удалена устаревшая копия ~/.claude/statusline-command.sh"
fi
# Skills (симлинк)
if [ -d "$SKILLS_SRC" ]; then
ensure_symlink "$SKILLS_SRC" "$HOME/.claude/skills"
fi
# Глобальный CLAUDE.md — словарь рабочего процесса (симлинк)
CLAUDE_MD_SRC="$DOTFILES_DIR/claude/.claude/CLAUDE.md"
if [ -f "$CLAUDE_MD_SRC" ]; then
ensure_symlink "$CLAUDE_MD_SRC" "$HOME/.claude/CLAUDE.md"
fi
# ── Codex ────────────────────────────────────────────────────────────
# Пакета в репозитории нет: ~/.codex — обычный каталог, его заполняет Codex.
# Миграция раскрывает старый свёрнутый симлинк и переносит рантайм-данные.
# settings.local.json — базовые настройки из репозитория (симлинк)
CLAUDE_LOCAL_SRC="$DOTFILES_DIR/claude/.claude/settings.local.json"
CLAUDE_LOCAL_DST="$HOME/.claude/settings.local.json"
if [ -f "$CLAUDE_LOCAL_SRC" ]; then
if [ -L "$CLAUDE_LOCAL_DST" ]; then
ln -sfnT "$CLAUDE_LOCAL_SRC" "$CLAUDE_LOCAL_DST"
info "settings.local.json → репозиторий"
elif [ -f "$CLAUDE_LOCAL_DST" ]; then
backup_existing_path "$CLAUDE_LOCAL_DST" "settings.local.json"
ln -s "$CLAUDE_LOCAL_SRC" "$CLAUDE_LOCAL_DST"
info "settings.local.json → репозиторий"
info "Машинно-специфичные настройки (model, effortLevel) теперь задаются через Claude Code"
elif [ -d "$CLAUDE_LOCAL_DST" ] || [ -e "$CLAUDE_LOCAL_DST" ]; then
warn "$CLAUDE_LOCAL_DST уже существует (не файл и не симлинк), удалите вручную"
else
ln -s "$CLAUDE_LOCAL_SRC" "$CLAUDE_LOCAL_DST"
info "settings.local.json → репозиторий"
fi
fi
# statusline-command.sh — копия из репозитория (на Windows симлинки ненадёжны)
CLAUDE_STATUSLINE_SRC="$DOTFILES_DIR/claude/.claude/statusline-command.sh"
CLAUDE_STATUSLINE_DST="$HOME/.claude/statusline-command.sh"
if [ -f "$CLAUDE_STATUSLINE_SRC" ]; then
install -m 755 "$CLAUDE_STATUSLINE_SRC" "$CLAUDE_STATUSLINE_DST"
info "statusline-command.sh скопирован в ~/.claude/"
fi
migrate_folded_symlink "$HOME/.codex" .gitkeep
# ── Bash: .bashrc ────────────────────────────────────────────────────
@@ -1,91 +0,0 @@
---
description: Build agent with guarded shell access and no silent git mutations
mode: primary
tools:
write: true
edit: true
patch: true
bash: true
permission:
write: allow
edit: allow
patch: allow
webfetch: ask
task:
"*": deny
"explore": allow
bash:
"*": ask
# безопасное чтение
"pwd": allow
"ls": allow
"ls *": allow
"cat": allow
"cat *": allow
"head": allow
"head *": allow
"tail": allow
"tail *": allow
"wc -l": allow
"wc -l *": allow
# read-only git
"git status": allow
"git status *": allow
"git diff": allow
"git diff *": allow
"git log": allow
"git log *": allow
"git show": allow
"git show *": allow
"git blame": allow
"git blame *": allow
"git rev-parse": allow
"git rev-parse *": allow
"git branch --show-current": allow
"git branch --list*": allow
"git tag --list*": allow
# опасные git-операции — только с подтверждением
"git add*": ask
"git commit*": ask
"git switch*": ask
"git checkout*": ask
"git restore*": ask
"git reset*": ask
"git clean*": ask
"git merge*": ask
"git rebase*": ask
"git stash*": ask
"git fetch*": ask
"git pull*": ask
# push вообще запрещаем
"git push*": deny
# ключи и секреты не читаем даже по прямой просьбе
"*/.ssh/*": deny
"*id_rsa*": deny
"*id_ed25519*": deny
"*.pem": deny
"*shadow*": deny
"*.env": deny
"*.env.*": deny
"*.secrets*": deny
"*.netrc*": deny
# а примеры env-файлов читать можно
"*.env.example": allow
---
You are a build agent.
Default to making code changes through native file-editing tools.
Use bash when it is the clearest path, but avoid unnecessary shell usage.
Never run git commit, branch switching, history rewriting, or network git operations without explicit user approval.
Prefer built-in tools for reading files and searching code.
@@ -1,112 +0,0 @@
---
description: Safe planning and repository analysis with read-only focus
mode: primary
tools:
write: false
edit: false
patch: false
bash: true
permission:
webfetch: ask
task:
"*": deny
"explore": allow
bash:
"*": ask
"pwd": allow
"ls": allow
"ls *": allow
"find": allow
"find *": allow
"cat": allow
"cat *": allow
"head": allow
"head *": allow
"tail": allow
"tail *": allow
"wc -l": allow
"wc -l *": allow
"rg": allow
"rg *": allow
"git status": allow
"git status *": allow
"git diff": allow
"git diff *": allow
"git log": allow
"git log *": allow
"git show": allow
"git show *": allow
"git blame": allow
"git blame *": allow
"git remote -v": allow
# жёсткий запрет на мутации
"git add*": deny
"git commit*": deny
"git switch*": deny
"git checkout*": deny
"git restore*": deny
"git reset*": deny
"git clean*": deny
"git merge*": deny
"git rebase*": deny
"git stash*": deny
"git push*": deny
# ключи и секреты не читаем даже по прямой просьбе
"*/.ssh/*": deny
"*id_rsa*": deny
"*id_ed25519*": deny
"*.pem": deny
"*shadow*": deny
"*.env": deny
"*.env.*": deny
"*.secrets*": deny
"*.netrc*": deny
# а примеры env-файлов читать можно
"*.env.example": allow
"git pull*": deny
"git fetch*": deny
"rm *": deny
"mv *": deny
"cp *": deny
"tee *": deny
"chmod *": deny
"chown *": deny
"sed -i*": deny
"perl -pi*": deny
"python*": deny
"node*": deny
"sh -c*": deny
"bash -c*": deny
"npm *": deny
"pnpm *": deny
"yarn *": deny
"bun *": deny
---
You are a planning and analysis agent.
For repository briefing:
1. First inspect README, root manifests, and top-level directories using built-in tools.
2. Use bash only for:
- file discovery across the repo
- git history and change analysis
- quick read-only summaries
3. Never use bash for edits or execution of interpreters.
Prefer:
- read/list/glob/grep for content inspection
- git log/show/diff/blame for history
- find/rg only when built-in tools are inconvenient
+24 -52
View File
@@ -1,62 +1,34 @@
{
"$schema": "https://opencode.ai/config.json",
"plugins": [
{
"package": "opencode-permission-reviewer@^2.4.0",
"options": {}
}
// Минимальный шаблон для OpenCode 2.x. Файл раскладывается копией
// (install.sh → copy_managed_file), поэтому правки в репозитории
// попадают в ~/.config/opencode только после ./install.sh.
"share": "manual",
"permissions": [
// Порядок важен: побеждает последнее совпавшее правило.
{ "action": "edit", "resource": "*", "effect": "ask" },
{ "action": "webfetch", "resource": "*", "effect": "ask" },
{ "action": "subagent", "resource": "*", "effect": "ask" },
{ "action": "shell", "resource": "*", "effect": "ask" },
{ "action": "shell", "resource": "*/.ssh/*", "effect": "deny" },
{ "action": "shell", "resource": "*id_rsa*", "effect": "deny" },
{ "action": "shell", "resource": "*id_ed25519*", "effect": "deny" },
{ "action": "shell", "resource": "*.pem", "effect": "deny" },
{ "action": "shell", "resource": "*.env", "effect": "deny" },
{ "action": "shell", "resource": "*.env.*", "effect": "deny" },
{ "action": "shell", "resource": "*.secrets*", "effect": "deny" },
{ "action": "shell", "resource": "*.netrc*", "effect": "deny" },
{ "action": "shell", "resource": "*.env.example", "effect": "allow" }
],
"default_agent": "safe-plan",
"permission": {
"write": "ask",
"edit": "ask",
"patch": "ask",
"webfetch": "ask",
"task": "ask",
"bash": {
"*": "ask",
"git push": "deny",
"git push *": "deny"
}
},
"mcp": {
"servers": {
"context7": {
"type": "remote",
"url": "https://mcp.context7.com/mcp",
"headers": {
"CONTEXT7_API_KEY": "${env:CONTEXT7_API_KEY}"
},
"enabled": true
},
"searxng": {
"type": "local",
"command": [
"npx",
"-y",
"mcp-searxng"
],
"environment": {
"SEARXNG_URL": "http://localhost:26745"
},
"enabled": true
},
"playwright": {
"type": "local",
"command": [
"npx",
"-y",
"@playwright/mcp@latest"
],
"enabled": false
},
"duckduckgo": {
"type": "local",
"command": [
"uvx",
"duckduckgo-mcp-server"
],
"enabled": true
"CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"
}
}
}
}
},
"share": "manual"
}
@@ -1,10 +1,30 @@
{
// Модель-ревьюер разрешений. Провайдеры: deepseek, opencode-go.
// Если модель не поддерживает structured output (например opencode-go/deepseek-v4-flash),
// переключить outputFormat на "text".
"model": "deepseek/deepseek-flash",
"outputFormat": "json_schema",
"variant": "none",
// Ниже порога уверенности решение уходит человеку
"confidenceThreshold": 0.7
// Как устроена вся система автоподтверждения — см. permission-reviewer.md рядом.
// Модель-ревьюер разрешений: Jev (System One) через OpenCode Zen.
// Zen закрывает /zen/v1/systemone по региону (451) — нужен прокси
// (HTTPS_PROXY в ~/.env.secrets, сейчас hproxy.local:12334).
// Ключ OPENCODE_API_KEY читается из окружения процесса opencode.
"model": "opencode/jev-1.13",
// Ниже порога уверенности решение уходит дальше по цепочке
"confidenceThreshold": 0.7,
// enforce: политика рисков применяется (дефолт observe — только аудит)
"enforcementMode": "enforce",
// deny: финальная эскалация = отказ, а не запрос человеку
// (при permission=auto на запрос человека всё равно отвечает auto)
"escalationMode": "deny",
// Сложные/неуверенные решения Jev получают второе мнение чат-модели
// вместо эскалации к человеку. glm-5.3-flash из подписки opencode-go:
// лимиты в 4 раза выше, чем у deepseek-v4.1-flash, интеллект сравним.
// structured_output у модели есть (zai), поэтому json_schema;
// variant high - у модели нет none, а сложным случаям полезно reasoning.
"escalationReviewer": {
"model": "opencode-go/glm-5.3-flash",
"variant": "high",
"outputFormat": "json_schema"
},
// Профили встроенных агентов: снимает "actor identity partial" у ревьюера
"actorProfiles": {
"build": "workspace",
"plan": "read-only"
}
}
@@ -0,0 +1,74 @@
# Система автоподтверждения разрешений (autoapprove)
> **Отключена.** Эксперимент 2026-10-11 оказался малополезен, плагин снят из
> `opencode.json`. Итоги — `docs/research/2026-10-11-opencode-permission-reviewer-autoapprove.md`.
> Ниже — описание на случай, если систему решат вернуть.
Как была устроена подтверждение команд в OpenCode на этой машине, и где что искать.
## Цепочка решений
Каждый `ask` из `opencode.json` проходит слои сверху вниз:
1. **Deny-паттерны** (`permission.bash` в `opencode.json`) — мгновенный отказ по
строке команды, до всяких моделей. Только секреты: `*.env*`, `*.secrets*`,
`~/.ssh/*` и т.п. Паттерны слепы к содержимому скриптов — это не рубеж, а
только защита секретов.
2. **Jev** (`opencode/jev-1.13`, System One) — типизированный ревьюер: получает
команду, свидетельства из транскрипта и содержимое локальных скриптов
(read-only enrichment), отвечает allow / deny / escalate.
3. **Второе мнение** (`opencode-go/glm-5.3-flash`) — разбирает трудные случаи,
где jev неуверен, но есть шанс на решение.
4. **Финал** — `escalationMode: deny`: неразрешённое отклоняется с причиной
агенту. Человек в цепочке не участвует: при `permission=auto` запрос к
человеку всё равно решил бы auto.
Ключевые настройки в `permission-reviewer.jsonc` рядом с этим файлом:
`enforcementMode: enforce` (применять политику рисков; дефолт observe — только
аудит), `escalationMode: deny` (дефолт manual — спросить человека).
## Инфраструктура
- Jev ходит на OpenCode Zen endpoint `/zen/v1/systemone`. Zen закрывает его
по региону (HTTP 451), поэтому сервису нужен прокси: `HTTPS_PROXY` в
`~/.env.secrets` (сейчас `hproxy.local:12334`). Чат-модели Zen
(`/zen/v1/chat/completions`) доступны без прокси — блокирован только
systemone.
- Ключ `OPENCODE_API_KEY` должен быть в окружении **процесса сервиса**:
`~/.env.secrets` сорсится из `~/.bash_custom`, сервис наследует окружение
шелла, из которого его запустили. После правки `.env.secrets`:
`source ~/.env.secrets && opencode service restart` — рестарт из старого
шелла ключ не подхватит.
- Jev нет в общем каталоге моделей OpenCode — это намеренно (не чат-модель),
на плагин не влияет: он зовёт systemone endpoint напрямую.
## Аудит и отладка
- Журнал решений: `~/.local/share/opencode/permission-reviewer-audit.jsonl` —
одна JSON-строка на ревью: вердикт, уверенность, причина, кто решал
(`decisionSource: system-one-reviewer / llm-reviewer / failure-safe`).
- Поле `reviewerEscalatedFrom` показывает, что первичный ревьюер (jev)
сработал, но эскалировал.
- `failure-safe` в `reason` = ревьюер недоступен (нет ключа, 451, таймаут) —
решение ушло в эскалацию.
- Emergency brake: команды, пахнущие выносом креденшелов наружу
(`curl -H "Authorization: ..."`), режутся детерминированно, мимо моделей.
## Известные свойства
- Сессия, полная опасных команд в транскрипте, выглядит для ревьюера
подозрительно: jev начинает эскаловать всё подряд, и с `deny` такая сессия
становится неработоспособной. Это fail-closed по замыслу. Лечение — работать
в чистой сессии.
- Тонкие пороги: `systemOneConfidenceThreshold` (дефолт 0.4) — насколько
уверенным должен быть jev, чтобы решать самому;
`systemOneReasoningThreshold` (дефолт 0.38) — сколько эскалаций уходит
на второе мнение.
## Документация
- Плагин и все опции: https://github.com/warc0s/opencode-permission-reviewer
(README на https://npm.io/package/opencode-permission-reviewer)
- Модель разрешений OpenCode V2: https://opencode.ai/v2/docs/permissions
- Jev у TypeSafe AI: https://jevmodel.org/ (есть прямой API с отдельным ключом
`TYPESAFE_API_KEY` — запасной маршрут, если Zen закроет systemone совсем)
-1
View File
@@ -1 +0,0 @@
../../../agents/.agents/skills
@@ -0,0 +1,10 @@
{
"env": {
"HTTP_PROXY": "http://127.0.0.1:12334",
"HTTPS_PROXY": "http://127.0.0.1:12334",
"NO_PROXY": "localhost,127.0.0.1,::1,[::1],dementev.space,.dementev.space",
"http_proxy": "http://127.0.0.1:12334",
"https_proxy": "http://127.0.0.1:12334",
"no_proxy": "localhost,127.0.0.1,::1,[::1],dementev.space,.dementev.space"
}
}
@@ -0,0 +1,15 @@
# Шаблон proxy-переменных для Codex. Скопируйте в ~/.codex/.env вручную.
# Codex читает этот файл при старте, и значения отсюда перекрывают
# унаследованные от запускающего приложения.
#
# Чтобы Codex на машине работал без прокси, очистите адреса и разрешите
# обход для всех хостов: HTTP_PROXY= и т. д., NO_PROXY=* и no_proxy=*.
# После правки перезапустите демон: codex app-server daemon restart
HTTP_PROXY=http://127.0.0.1:12334
HTTPS_PROXY=http://127.0.0.1:12334
ALL_PROXY=socks5://127.0.0.1:12334
NO_PROXY=localhost,127.0.0.1,::1,[::1],dementev.space,.dementev.space
http_proxy=http://127.0.0.1:12334
https_proxy=http://127.0.0.1:12334
all_proxy=socks5://127.0.0.1:12334
no_proxy=localhost,127.0.0.1,::1,[::1],dementev.space,.dementev.space
+91 -20
View File
@@ -44,7 +44,7 @@ DEFAULT_NO_PROXY_EXTRA = (
"dementev.space",
".dementev.space",
)
XRAY_VERSION = "26.3.27"
XRAY_VERSION = "26.10.10"
VERSION_LINE = re.compile(r"Xray {}(?: .*)?".format(re.escape(XRAY_VERSION)))
XRAY_RELEASE_BASE_URL = (
"https://github.com/XTLS/Xray-core/releases/download/v{}/".format(XRAY_VERSION)
@@ -60,6 +60,11 @@ QUERY_KEYS = {
"fp",
"pbk",
"sid",
"pqv",
"spx",
"ech",
"fm",
"support-x25519mlkem768",
"headerType",
"host",
"alpn",
@@ -68,12 +73,18 @@ QUERY_KEYS = {
"path",
"mode",
"extra",
"x_padding_bytes",
"xPaddingBytes",
"scMaxEachPostBytes",
"scMinPostsIntervalMs",
"uplinkChunkSize",
"pcs",
"vcn",
"allowInsecure",
"insecure",
}
ALPN_VALUES = {"http/1.1", "h2", "h3"}
XHTTP_RANGE_KEYS = {"xPaddingBytes", "scMaxEachPostBytes", "scMinPostsIntervalMs", "uplinkChunkSize"}
class ProxyError(Exception):
@@ -90,7 +101,7 @@ class ProxyConfig:
class VlessProfile:
def __init__(self, server, port, profile_id, query, alpn, extra, insecure):
def __init__(self, server, port, profile_id, query, alpn, extra, insecure, finalmask=None):
self.server = server
self.port = port
self.profile_id = profile_id
@@ -98,6 +109,7 @@ class VlessProfile:
self.alpn = alpn
self.extra = extra
self.insecure = insecure
self.finalmask = finalmask
@property
def transport(self):
@@ -316,6 +328,36 @@ def list_value(value):
return items
def json_object(value, name):
try:
result = json.loads(value)
except ValueError:
result = None
if not isinstance(result, dict):
raise ProxyError("{} должен быть JSON-объектом".format(name))
return result
def normalize_xhttp_settings(settings):
result = dict(settings)
# Xray 26.9.30 переименовал поля; старые имена ядро молча игнорирует.
for old, new in (("sessionPlacement", "sessionIDPlacement"), ("sessionKey", "sessionIDKey")):
if old in result:
value = result.pop(old)
if new in result and result[new] != value:
raise ProxyError("противоречивые старые и новые параметры сессии XHTTP")
result[new] = value
if isinstance(result.get("extra"), dict):
result["extra"] = normalize_xhttp_settings(result["extra"])
if isinstance(result.get("downloadSettings"), dict):
download = dict(result["downloadSettings"])
for name in ("xhttpSettings", "splithttpSettings"):
if isinstance(download.get(name), dict):
download[name] = normalize_xhttp_settings(download[name])
result["downloadSettings"] = download
return result
def parse_vless(url):
try:
parsed = urlsplit(url)
@@ -343,6 +385,14 @@ def parse_vless(url):
raise ProxyError("профиль содержит неверные UUID, сервер или порт")
validate_address(server)
query = parse_query(parsed.query)
query.setdefault("encryption", "none")
query.setdefault("sni", server)
if query.get("security") == "tls":
query.setdefault("fp", "chrome")
if query.get("type") == "raw":
query["type"] = "tcp"
if query.get("flow") == "":
del query["flow"]
required(query, "type", "security", "encryption", "sni", "fp")
# Кроме none, Xray принимает VLESS-шифрование вида
# mlkem768x25519plus.РЕЖИМ.RTT.КЛЮЧ...; подробности проверит xray run -test.
@@ -357,44 +407,57 @@ def parse_vless(url):
raise ProxyError("поддерживается только метаданные hiddify=1")
if "core" in query and query["core"] != "xray":
raise ProxyError("поддерживается только метаданные core=xray")
# Xray 26.3.27 больше не умеет отключать проверку сертификата: поля
if "support-x25519mlkem768" in query and query["support-x25519mlkem768"] != "true":
raise ProxyError("поддерживается только метаданные support-x25519mlkem768=true")
# Xray 26.10.10 больше не умеет отключать проверку сертификата: поля
# принимаем, но не переносим.
insecure = any(
query.get(key, "").lower() in ("1", "true") for key in ("allowInsecure", "insecure")
)
alpn = None
extra = None
finalmask = json_object(query["fm"], "fm") if "fm" in query else None
if query["type"] == "tcp" and query["security"] == "reality":
required(query, "flow", "pbk", "sid")
if {"path", "mode", "extra", "pcs", "vcn", "core"}.intersection(query):
required(query, "pbk")
if ({"path", "mode", "extra", "x_padding_bytes", "pcs", "vcn", "core", "ech"}
| XHTTP_RANGE_KEYS).intersection(query):
raise ProxyError("параметры профиля несовместимы с TCP/REALITY")
elif query["type"] == "xhttp" and query["security"] == "tls":
required(query, "host", "path")
query.setdefault("path", "/")
required(query, "path")
# Vision поверх XHTTP работает только внутри VLESS-шифрования.
if ("flow" in query and not encrypted) or "pbk" in query or "sid" in query:
if (("flow" in query and not encrypted)
or {"pbk", "sid", "pqv", "spx", "support-x25519mlkem768"}.intersection(query)):
raise ProxyError("параметры профиля несовместимы с XHTTP/TLS")
if "pcs" in query:
if query.get("pcs"):
hashes = list_value(query["pcs"])
if any(re.fullmatch(r"[0-9A-Fa-f]{64}", item) is None for item in hashes):
if any(re.fullmatch(r"[0-9A-Fa-f]{64}", item.strip().replace(":", ""))
is None for item in hashes):
raise ProxyError("pcs должен содержать 32-байтные SHA-256 в hex")
if "vcn" in query:
if query.get("vcn"):
list_value(query["vcn"])
if "alpn" in query:
alpn = list_value(query["alpn"])
if any(item not in ALPN_VALUES for item in alpn):
raise ProxyError("профиль содержит неподдерживаемый ALPN")
if "extra" in query:
try:
extra = json.loads(query["extra"])
except ValueError:
extra = None
if not isinstance(extra, dict):
raise ProxyError("extra должен быть JSON-объектом")
extra = json_object(query["extra"], "extra")
if "x_padding_bytes" in query:
if extra is None:
extra = {}
# При наличии extra Xray берёт padding только из него.
extra.setdefault("xPaddingBytes", query["x_padding_bytes"])
for key in XHTTP_RANGE_KEYS.intersection(query):
if extra is None:
extra = {}
extra[key] = query[key]
if extra is not None:
extra = normalize_xhttp_settings(extra)
else:
raise ProxyError("поддерживаются только TCP/REALITY и XHTTP/TLS")
return VlessProfile(server, port, profile_id, query, alpn, extra, insecure)
return VlessProfile(server, port, profile_id, query, alpn, extra, insecure, finalmask)
def build_xray_config(profile, local_address, local_port):
@@ -412,15 +475,19 @@ def build_xray_config(profile, local_address, local_port):
"serverName": query["sni"],
"fingerprint": query["fp"],
"password": query["pbk"],
"shortId": query["sid"],
"shortId": query.get("sid", ""),
}
if "pqv" in query:
reality["mldsa65Verify"] = query["pqv"]
if "spx" in query:
reality["spiderX"] = query["spx"]
stream = {
"network": "raw",
"security": "reality",
"realitySettings": reality,
}
else:
xhttp = {"host": query["host"], "path": query["path"]}
xhttp = {"host": query.get("host", ""), "path": query["path"]}
if "mode" in query:
xhttp["mode"] = query["mode"]
if profile.extra is not None:
@@ -432,12 +499,16 @@ def build_xray_config(profile, local_address, local_port):
tls["pinnedPeerCertSha256"] = query["pcs"]
if "vcn" in query:
tls["verifyPeerCertByName"] = query["vcn"]
if "ech" in query:
tls["echConfigList"] = query["ech"]
stream = {
"network": "xhttp",
"security": "tls",
"xhttpSettings": xhttp,
"tlsSettings": tls,
}
if profile.finalmask is not None:
stream["finalmask"] = profile.finalmask
return {
"log": {"access": "none", "loglevel": "warning"},
"inbounds": [
@@ -716,7 +787,7 @@ def print_summary(profile, local_port):
else:
print(
"XHTTP: Host {}, длина пути: {}, ALPN {}".format(
query["host"], len(query["path"]), query.get("alpn", "не задан")
query.get("host") or "не задан", len(query["path"]), query.get("alpn", "не задан")
)
)
if query["encryption"] != "none":
+69 -4
View File
@@ -264,6 +264,39 @@ proxy-client stop
Второй `start`, пока первый работает, завершится ошибкой.
## Прокси в настройках Codex и Claude Code
Codex и Claude Code умеют брать proxy-переменные из собственных настроек.
Значения оттуда перекрывают окружение, унаследованное от запускающего
приложения, поэтому маршрут не зависит от того, откуда агент запущен:
терминал, IDE или фоновый демон Codex. С такими настройками `pcodex` и
`pclaude` не обязательны.
Установщики эти настройки не трогают. Шаблоны лежат в
`~/.config/proxy-client/examples/`: прокси `127.0.0.1:12334` и короткий
список исключений. Скопируйте нужный и поправьте под свою машину.
Codex читает `~/.codex/.env`:
```bash
cp ~/.config/proxy-client/examples/codex.env ~/.codex/.env
codex app-server daemon restart
```
Запросы к API отправляет общий демон Codex, а он читает `.env` только при
старте. Перезапуск демона прерывает открытые сессии Codex.
Claude Code берёт переменные из блока `env` в `~/.claude/settings.json`.
Перенесите туда блок из `examples/claude-settings.json`; остальные ключи
файла не трогайте. Настройки подхватятся при следующем запуске Claude Code.
Claude Desktop этот блок не использует: само приложение ходит в сеть через
Chromium.
Чтобы агент на машине работал без прокси, даже если его запустило
приложение с прокси в окружении, очистите адреса (`HTTP_PROXY`,
`HTTPS_PROXY`, `ALL_PROXY` в обоих регистрах) и задайте `NO_PROXY=*` и
`no_proxy=*`.
## Диагностика
Начните с проверки выбранного режима:
@@ -354,13 +387,38 @@ source ~/.config/proxy-client/shell.bash
`http/1.1`, `h2` и `h3`; `extra` должен быть JSON-объектом, его содержимое
проверяет `xray run -test`.
Параметры ссылок передаются по [спецификации Xray](https://github.com/XTLS/Xray-core/discussions/716):
| В ссылке | В конфигурации Xray |
|----------|---------------------|
| `ech` | TLS `echConfigList` |
| `pqv`, `spx` | REALITY `mldsa65Verify`, `spiderX` |
| `fm` | `streamSettings.finalmask`, JSON-объект |
| `x_padding_bytes`, `xPaddingBytes` | XHTTP `extra.xPaddingBytes` |
| `scMaxEachPostBytes`, `scMinPostsIntervalMs`, `uplinkChunkSize` | Одноимённые поля XHTTP внутри `extra` |
Для XHTTP пустой или отсутствующий `host` допустим; при отсутствии `path`
используется `/`. Имена `sessionPlacement` и `sessionKey` в `extra`, включая
отдельное соединение скачивания, преобразуются в `sessionIDPlacement` и
`sessionIDKey`. Если старое и новое имя задают разные значения, профиль
отклоняется. Новые `sessionIDTable` и `sessionIDLength` передаются ядру.
Параметры XHTTP с именами как в JSON имеют приоритет над `extra`, а
`extra.xPaddingBytes` — над `x_padding_bytes`.
При отсутствии `encryption` используется `none`, `sni` — адрес сервера,
TLS `fp` — `chrome`. Для REALITY `fp` и `pbk` обязательны, а `flow` и `sid`
могут быть пустыми или отсутствовать; `type=raw` принимается как `type=tcp`.
Пустые `ech`, `pqv`, `spx`, `pcs`, `vcn` допустимы. SHA-256 в `pcs` может
содержать двоеточия и пробелы по краям. Метка `support-x25519mlkem768=true` в REALITY
принимается для совместимости со ссылками 3x-ui и не создаёт поле в JSON Xray.
Кроме `encryption=none`, принимается VLESS-шифрование
`encryption=mlkem768x25519plus.…` из новых ссылок Hiddify; остальную часть
значения проверяет сам Xray. Только с таким шифрованием XHTTP допускает
`flow=xtls-rprx-vision`. Косая черта перед параметрами (`…:443/?type=…`)
допустима, другой путь в ссылке — нет.
Xray 26.3.27 использует `pcs` и `vcn`. Устаревшие `allowInsecure` и `insecure`
Xray 26.10.10 использует `pcs` и `vcn`. Устаревшие `allowInsecure` и `insecure`
принимаются с любым значением, но не переносятся: при значении `1` или `true`
выводится предупреждение, а сертификат проверяется обычным способом.
@@ -371,10 +429,17 @@ Xray 26.3.27 использует `pcs` и `vcn`. Устаревшие `allowIns
### Проверяемая установка Xray
`proxy-client install` устанавливает Xray 26.3.27 в `~/.local/bin/xray`. Для
`proxy-client install` устанавливает Xray 26.10.10 в `~/.local/bin/xray`. Для
`x86_64`/`amd64` используется `Xray-linux-64.zip`, для `aarch64`/`arm64` —
`Xray-linux-arm64-v8a.zip`; остальные архитектуры отклоняются.
[Выпуск 26.10.10](https://github.com/XTLS/Xray-core/releases/tag/v26.10.10)
помечен как предварительный. Обновление уже установленного клиента:
```bash
proxy-client install --replace
```
CLI скачивает официальный архив и файл `.dgst`, сверяет SHA-256 и фактическую
версию бинарника, а затем атомарно заменяет назначение. При любой ошибке старый
файл остаётся на месте. Если нужная версия уже установлена, сеть не нужна и
@@ -386,7 +451,7 @@ CLI скачивает официальный архив и файл `.dgst`, с
Устанавливать можно и при работающем Xray: он продолжит работу со старым файлом,
а `install` напомнит перезапустить `start`.
`start` использует первый `xray` из `PATH` и принимает только версию 26.3.27.
`start` использует первый `xray` из `PATH` и принимает только версию 26.10.10.
### Файлы и безопасность
@@ -394,7 +459,7 @@ CLI скачивает официальный архив и файл `.dgst`, с
|------|------------|
| `~/.config/proxy-client/config` | Несекретный машинный выбор режима |
| `~/.config/proxy-client/default.url` | Закрытая VLESS-ссылка, права `600` |
| `~/.config/proxy-client/examples/` | Синтетические примеры без секретов |
| `~/.config/proxy-client/examples/` | Синтетические примеры без секретов, шаблоны для Codex и Claude Code |
| `$XDG_STATE_HOME/proxy-client/` или `~/.local/state/proxy-client/` | Состояние и сгенерированный JSON |
Сводка скрывает UUID и ключи, а для XHTTP показывает только длину `path`.
+73 -65
View File
@@ -21,7 +21,8 @@ assert_file_contains() {
assert_not_exists() {
local path="$1"
[ ! -e "$path" ] || fail "Path should not exist: $path"
# -e не видит битые симлинки, поэтому проверяем и -L
[ ! -e "$path" ] && [ ! -L "$path" ] || fail "Path should not exist: $path"
}
setup_fixture() {
@@ -37,7 +38,7 @@ setup_fixture() {
# zed/.config/opencode и т.п.) весят гигабайты, переполняют /tmp и ломают
# stow конфликтами, поэтому в тестовое дерево не попадает.
git -C "$REPO_ROOT" ls-files -z --cached --others --exclude-standard -- \
install.sh bash tmux vim zed kitty wezterm vscode agents opencode codex gemini \
install.sh bash tmux vim zed kitty wezterm vscode agents opencode gemini \
claude proxy-client chatgpt scripts \
| tar -C "$REPO_ROOT" --null -T - -cf - \
| tar -xf - -C "$repo_root"
@@ -225,22 +226,71 @@ test_existing_wezterm_config_is_backed_up() {
assert_file_contains "$backup" 'require("wezterm")'
}
test_folded_package_runtime_files_are_not_backed_up() {
test_codex_runtime_dir_is_unmanaged() {
local fixture repo_root home_dir backup
fixture="$(setup_fixture)"
repo_root="$fixture/repo"
home_dir="$fixture/home"
# ~/.codex — обычный каталог с рантайм-данными Codex; install.sh его не трогает.
mkdir -p "$home_dir/.codex"
printf '%s\n' '{}' > "$home_dir/.codex/auth.json"
run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install.out"
run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install2.out"
[ -d "$home_dir/.codex" ] || fail "Expected ~/.codex to stay a directory"
[ ! -L "$home_dir/.codex" ] || fail "~/.codex must not become a symlink"
backup="$(find "$repo_root" "$home_dir" -name '*.bak.*' -print -quit)"
[ -z "$backup" ] || fail "Codex runtime files must not be backed up: $backup"
assert_file_contains "$home_dir/.codex/auth.json" "{}"
}
test_migrates_folded_codex_symlink() {
local fixture repo_root home_dir legacy_dir
fixture="$(setup_fixture)"
repo_root="$fixture/repo"
home_dir="$fixture/home"
mkdir -p "$home_dir"
# Старая схема: ~/.codex — симлинк на каталог внутри репозитория с рантайм-данными
legacy_dir="$fixture/legacy-repo-codex"
mkdir -p "$legacy_dir"
printf '%s\n' '{"token":"x"}' > "$legacy_dir/auth.json"
printf '%s\n' 'HTTPS_PROXY=http://hproxy.local' > "$legacy_dir/.env"
ln -s "$legacy_dir" "$home_dir/.codex"
run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install.out"
# ~/.codex свёрнут в симлинк на пакет: файлы внутри — уже наши, не конфликт.
printf '%s\n' '{}' > "$home_dir/.codex/auth.json"
run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install2.out"
[ -d "$home_dir/.codex" ] || fail "Expected ~/.codex to become a directory"
[ ! -L "$home_dir/.codex" ] || fail "Expected ~/.codex to stop being a symlink"
assert_file_contains "$home_dir/.codex/auth.json" '"token":"x"'
assert_file_contains "$home_dir/.codex/.env" "hproxy.local"
}
backup="$(find "$repo_root" "$home_dir" -name '*.bak.*' -print -quit)"
[ -z "$backup" ] || fail "Folded package contents must not be backed up: $backup"
assert_file_contains "$home_dir/.codex/auth.json" "{}"
test_claude_stale_artifacts_are_removed() {
local fixture repo_root home_dir
fixture="$(setup_fixture)"
repo_root="$fixture/repo"
home_dir="$fixture/home"
mkdir -p "$home_dir/.claude"
# Артефакты прежнего установщика: симлинки на удалённые из репозитория файлы
# и наша копия statusline (узнаётся по строке-маркеру).
ln -s "$repo_root/claude/.claude/CLAUDE.md" "$home_dir/.claude/CLAUDE.md"
ln -s "$repo_root/claude/.claude/settings.local.json" "$home_dir/.claude/settings.local.json"
printf '#!/usr/bin/env bash\n# Статусная строка Claude Code. JSON приходит на stdin.\n' \
> "$home_dir/.claude/statusline-command.sh"
# Чужой файл с похожим именем трогать нельзя
printf 'echo mine\n' > "$home_dir/.claude/statusline-mine.sh"
chmod +x "$home_dir/.claude/statusline-command.sh"
run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install.out"
assert_not_exists "$home_dir/.claude/CLAUDE.md"
assert_not_exists "$home_dir/.claude/settings.local.json"
assert_not_exists "$home_dir/.claude/statusline-command.sh"
assert_file_contains "$home_dir/.claude/statusline-mine.sh" "echo mine"
[ -L "$home_dir/.claude/skills" ] || fail "Expected skills symlink to be created"
}
test_migrates_legacy_zed_config_symlink() {
@@ -358,53 +408,7 @@ test_does_not_follow_unrelated_local_symlink() {
assert_not_exists "$unrelated_local/bin"
}
test_settings_local_is_symlinked_to_repo() {
local fixture repo_root home_dir
fixture="$(setup_fixture)"
repo_root="$fixture/repo"
home_dir="$fixture/home"
mkdir -p "$home_dir"
run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out
[ -L "$home_dir/.claude/settings.local.json" ] || fail "Expected settings.local.json to be a symlink"
local target
target="$(readlink -f "$home_dir/.claude/settings.local.json")"
local expected
expected="$(readlink -f "$repo_root/claude/.claude/settings.local.json")"
[ "$target" = "$expected" ] || fail "settings.local.json points to $target, expected $expected"
}
test_settings_local_backup_on_existing_file() {
local fixture repo_root home_dir
fixture="$(setup_fixture)"
repo_root="$fixture/repo"
home_dir="$fixture/home"
mkdir -p "$home_dir/.claude"
echo '{"model":"sonnet"}' > "$home_dir/.claude/settings.local.json"
run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out
[ -L "$home_dir/.claude/settings.local.json" ] || fail "Expected settings.local.json to become a symlink"
local backup
backup="$(find "$home_dir/.claude" -maxdepth 1 -name 'settings.local.json.bak.*' -print -quit)"
[ -n "$backup" ] || fail "Expected backup for existing settings.local.json"
assert_file_contains "$backup" '"model":"sonnet"'
}
test_settings_local_dir_is_skipped() {
local fixture repo_root home_dir
fixture="$(setup_fixture)"
repo_root="$fixture/repo"
home_dir="$fixture/home"
mkdir -p "$home_dir/.claude/settings.local.json"
run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out
[ -d "$home_dir/.claude/settings.local.json" ] || fail "Expected directory to be preserved (not replaced)"
}
test_stow_migration_preserves_legacy_settings_local() {
test_stow_migration_preserves_legacy_files() {
local fixture repo_root home_dir stow_target
fixture="$(setup_fixture)"
repo_root="$fixture/repo"
@@ -414,14 +418,14 @@ test_stow_migration_preserves_legacy_settings_local() {
# Simulate old stow layout: ~/.claude → repo dir
stow_target="$repo_root/claude/.claude"
ln -s "$stow_target" "$home_dir/.claude"
# Put a legacy settings.local.json in the stow target (would have been gitignored before)
# Put a legacy local file in the stow target (раньше settings.local.json
# был git-tracked и пропускался; теперь это машинные данные — переносим)
echo '{"model":"opus"}' > "$stow_target/legacy-local.json"
run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out
[ -d "$home_dir/.claude" ] || fail "Expected ~/.claude to become a directory"
[ ! -L "$home_dir/.claude" ] || fail "Expected ~/.claude to stop being a symlink"
[ -L "$home_dir/.claude/settings.local.json" ] || fail "Expected settings.local.json symlink after migration"
# legacy-local.json should have been moved from stow dir to real ~/.claude
[ -f "$home_dir/.claude/legacy-local.json" ] || fail "Expected legacy file to be migrated"
}
@@ -478,13 +482,18 @@ test_opencode_runtime_dir_is_preserved_and_config_is_managed() {
cat > "$home_dir/.config/opencode/opencode.json" <<'EOF'
{"source":"local"}
EOF
# Артефакт прежнего установщика: симлинк на удалённые профили агентов
ln -s "$repo_root/opencode/.config/opencode/agents" "$home_dir/.config/opencode/agents"
# Лишняя ссылка на общие скиллы: opencode и так читает ~/.agents/skills
ln -s "$repo_root/agents/.agents/skills" "$home_dir/.config/opencode/skills"
run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out
[ -f "$home_dir/.config/opencode/package.json" ] || fail "Expected opencode runtime files to be preserved"
[ -L "$home_dir/.config/opencode/agents" ] || fail "Expected ~/.config/opencode/agents symlink"
[ -L "$home_dir/.config/opencode/skills" ] || fail "Expected ~/.config/opencode/skills symlink"
assert_not_exists "$home_dir/.config/opencode/agents"
assert_not_exists "$home_dir/.config/opencode/skills"
assert_file_contains "$home_dir/.config/opencode/opencode.json" '"$schema": "https://opencode.ai/config.json"'
assert_file_contains "$home_dir/.config/opencode/permission-reviewer.jsonc" '"confidenceThreshold"'
backup_path="$(find "$home_dir/.config/opencode" -maxdepth 1 -name 'opencode.json.bak.*' -print -quit)"
[ -n "$backup_path" ] || fail "Expected backup for existing opencode.json"
@@ -643,15 +652,14 @@ main() {
test_foreign_kitty_config_symlink_is_backed_up
test_rerun_does_not_backup_managed_kitty_config
test_existing_wezterm_config_is_backed_up
test_folded_package_runtime_files_are_not_backed_up
test_codex_runtime_dir_is_unmanaged
test_migrates_folded_codex_symlink
test_claude_stale_artifacts_are_removed
test_migrates_legacy_zed_config_symlink
test_stops_legacy_migration_when_listing_fails
test_does_not_migrate_unrelated_config_symlink
test_does_not_follow_unrelated_local_symlink
test_settings_local_is_symlinked_to_repo
test_settings_local_backup_on_existing_file
test_settings_local_dir_is_skipped
test_stow_migration_preserves_legacy_settings_local
test_stow_migration_preserves_legacy_files
test_rerun_does_not_duplicate_bashrc_line
test_repairs_nested_skills_directory_bug
test_preserves_nonempty_skills_directory
+112 -2
View File
@@ -314,6 +314,103 @@ class VlessProfileTest(unittest.TestCase):
self.assertEqual(["h3"], result["streamSettings"]["tlsSettings"]["alpn"])
self.assertIn("downloadSettings", result["streamSettings"]["xhttpSettings"]["extra"])
def test_xhttp_padding_is_added_to_extra(self):
for value in ("100", "100-1000"):
for extra in ({}, {"downloadSettings": {"address": "cdn.example.invalid"}}):
with self.subTest(value=value, extra=extra):
url = XHTTP + "&x_padding_bytes=" + value
url += "&extra=" + quote(json.dumps(extra), safe="")
settings = outbound(url)["streamSettings"]["xhttpSettings"]
self.assertEqual(dict(extra, xPaddingBytes=value), settings["extra"])
settings = outbound(XHTTP + "&x_padding_bytes=" + value)["streamSettings"][
"xhttpSettings"
]
self.assertEqual({"xPaddingBytes": value}, settings["extra"])
def test_xhttp_extra_padding_takes_priority(self):
for value in ("100-1000", "200-500"):
with self.subTest(value=value):
extra = {"xPaddingBytes": value}
url = XHTTP + "&x_padding_bytes=100-1000&extra="
url += quote(json.dumps(extra), safe="")
self.assertEqual(extra, outbound(url)["streamSettings"]["xhttpSettings"]["extra"])
def test_xhttp_host_can_be_empty_or_absent(self):
for host in ("&host=", ""):
with self.subTest(host=host):
url = XHTTP.replace("&host=cdn.example.invalid", host)
stream = outbound(url)["streamSettings"]
self.assertEqual("", stream["xhttpSettings"]["host"])
self.assertEqual("cdn.example.invalid", stream["tlsSettings"]["serverName"])
def test_share_link_defaults(self):
url = XHTTP.replace("&encryption=none", "").replace("&sni=cdn.example.invalid", "")
url = url.replace("&fp=chrome", "").replace("&path=%2Fp", "") + "&flow="
result = outbound(url)
self.assertEqual("none", result["settings"]["encryption"])
self.assertNotIn("flow", result["settings"])
self.assertEqual("/", result["streamSettings"]["xhttpSettings"]["path"])
self.assertEqual("example.invalid", result["streamSettings"]["tlsSettings"]["serverName"])
self.assertEqual("chrome", result["streamSettings"]["tlsSettings"]["fingerprint"])
def test_reality_optional_fields_and_raw_alias(self):
for sid in ("&sid=", ""):
for flow in ("&flow=", ""):
with self.subTest(sid=sid, flow=flow):
url = REALITY.replace("&sid=SYNTHETIC_SHORT_ID", sid)
url = url.replace("&flow=xtls-rprx-vision", flow).replace("type=tcp", "type=raw")
result = outbound(url + "&pqv=VERIFY_SENTINEL&spx=%2Fspider&support-x25519mlkem768=true")
reality = result["streamSettings"]["realitySettings"]
self.assertEqual("", reality["shortId"])
self.assertEqual("VERIFY_SENTINEL", reality["mldsa65Verify"])
self.assertEqual("/spider", reality["spiderX"])
self.assertNotIn("flow", result["settings"])
self.assertNotIn("support-x25519mlkem768", json.dumps(result))
def test_tls_ech_and_optional_empty_pins(self):
for ech in ("", "ECH_SENTINEL"):
with self.subTest(ech=ech):
tls = outbound(XHTTP + "&ech=" + ech + "&pcs=&vcn=")["streamSettings"]["tlsSettings"]
self.assertEqual(ech, tls["echConfigList"])
self.assertEqual("", tls["pinnedPeerCertSha256"])
self.assertEqual("", tls["verifyPeerCertByName"])
pin = ":".join(["aa"] * 32)
for value in (pin, " " + pin + " "):
with self.subTest(pin=value):
tls = outbound(XHTTP + "&pcs=" + quote(value, safe=""))["streamSettings"]["tlsSettings"]
self.assertEqual(value, tls["pinnedPeerCertSha256"])
def test_xhttp_canonical_fields_override_extra(self):
extra = {"xPaddingBytes": "200-500", "scMinPostsIntervalMs": 10, "other": {"kept": True}}
fields = {"xPaddingBytes": "300-600", "scMinPostsIntervalMs": "20", "scMaxEachPostBytes": "1000", "uplinkChunkSize": "1024"}
url = XHTTP + "&x_padding_bytes=100-1000&extra=" + quote(json.dumps(extra), safe="")
url += "".join("&{}={}".format(key, value) for key, value in fields.items())
self.assertEqual(dict(extra, **fields), outbound(url)["streamSettings"]["xhttpSettings"]["extra"])
def test_finalmask_json_mapping(self):
finalmask = {"tcp": [], "udp": []}
for base in (XHTTP, REALITY):
with self.subTest(base=base[-20:]):
url = base + "&fm=" + quote(json.dumps(finalmask), safe="")
self.assertEqual(finalmask, outbound(url)["streamSettings"]["finalmask"])
def test_xhttp_session_names_are_updated_in_upload_and_download(self):
old = {"sessionPlacement": "header", "sessionKey": "X-Session"}
new = {"sessionIDPlacement": "header", "sessionIDKey": "X-Session"}
extra = dict(old, downloadSettings={"network": "xhttp", "xhttpSettings": {"extra": old}})
expected = dict(new, downloadSettings={"network": "xhttp", "xhttpSettings": {"extra": new}})
url = XHTTP + "&extra=" + quote(json.dumps(extra), safe="")
self.assertEqual(expected, outbound(url)["streamSettings"]["xhttpSettings"]["extra"])
fresh = dict(new, sessionIDTable="0123456789abcdef", sessionIDLength=16)
url = XHTTP + "&extra=" + quote(json.dumps(fresh), safe="")
self.assertEqual(fresh, outbound(url)["streamSettings"]["xhttpSettings"]["extra"])
def test_xhttp_conflicting_session_names_are_rejected(self):
extra = {"sessionPlacement": "header", "sessionIDPlacement": "cookie"}
url = XHTTP + "&extra=" + quote(json.dumps(extra), safe="")
with self.assertRaisesRegex(ProxyError, "противоречивые"):
outbound(url)
def test_alpn_values(self):
for value in ("http/1.1", "h2", "h3", "h2,h3"):
with self.subTest(value):
@@ -349,12 +446,25 @@ class VlessProfileTest(unittest.TestCase):
"grpc": REALITY.replace("type=tcp", "type=grpc"),
"missing pbk": REALITY.replace("&pbk=SYNTHETIC_PUBLIC_KEY", ""),
"xhttp field in reality": REALITY + "&path=%2Fp",
"xhttp padding in reality": REALITY + "&x_padding_bytes=100-1000",
"duplicate padding": XHTTP + "&x_padding_bytes=100&x_padding_bytes=200",
"canonical padding in reality": REALITY + "&xPaddingBytes=100",
"ech in reality": REALITY + "&ech=secret",
"pqv in tls": XHTTP + "&pqv=secret",
"spx in tls": XHTTP + "&spx=%2F",
"other core hint": REALITY + "&support-x25519mlkem768=false",
"empty sni": XHTTP.replace("sni=cdn.example.invalid", "sni="),
"empty path": XHTTP.replace("path=%2Fp", "path="),
"missing reality fingerprint": REALITY.replace("&fp=chrome", ""),
"finalmask not object": XHTTP + "&fm=%5B%5D",
"finalmask invalid json": XHTTP + "&fm=%7B",
"xhttp with reality": XHTTP.replace("security=tls", "security=reality"),
"flow over xhttp without encryption": XHTTP + "&flow=xtls-rprx-vision",
"unknown encryption": HIDDIFY_XHTTP.replace("mlkem768x25519plus.", "aes-128-gcm."),
"unknown alpn": XHTTP + "&alpn=h4",
"empty alpn item": XHTTP + "&alpn=h2%2C",
"short pcs": XHTTP + "&pcs=abc",
"space inside pcs": XHTTP + "&pcs=" + "aa" * 16 + "%20" + "aa" * 16,
"empty vcn item": XHTTP + "&vcn=a.example%2C",
"extra not an object": XHTTP + "&extra=%5B1%5D",
"extra not json": XHTTP + "&extra=%7Bx",
@@ -383,7 +493,7 @@ class XrayInstallTest(unittest.TestCase):
self.archive = self.make_archive(self.fake_xray("installed"))
@staticmethod
def fake_xray(label, version="26.3.27"):
def fake_xray(label, version="26.10.10"):
return "#!/bin/sh\n[ \"$1\" = version ] && echo 'Xray {} ({})'\n".format(
version, label
).encode("ascii")
@@ -440,7 +550,7 @@ class XrayInstallTest(unittest.TestCase):
self.assertIn("установлен", output)
self.assertEqual(0o755, self.destination.stat().st_mode & 0o777)
self.assertIn(b"installed", self.destination.read_bytes())
self.assertTrue(calls[0].endswith("/v26.3.27/Xray-linux-64.zip"))
self.assertTrue(calls[0].endswith("/v26.10.10/Xray-linux-64.zip"))
self.assertEqual([calls[0], calls[0] + ".dgst"], calls)
self.assertEqual(["xray"], [path.name for path in self.destination.parent.iterdir()])
+20 -2
View File
@@ -34,7 +34,7 @@ READY = "остановка: Ctrl+C или proxy-client stop"
# совпадает с разрешённым путём к xray, как у настоящего бинарника.
# «xray version» и «xray run ...» запускают одноимённые скрипты из рабочего
# каталога теста.
FAKE_VERSION = "print('Xray 26.3.27 (fake)')\n"
FAKE_VERSION = "print('Xray 26.10.10 (fake)')\n"
FAKE_RUN = r'''import json, os, socket, sys
config_path = sys.argv[-1]
@@ -219,6 +219,24 @@ class ManagedXrayTest(unittest.TestCase):
self.assertFalse(pid_alive(xray_pid))
self.assert_no_state_files()
def test_xhttp_export_with_empty_host_and_padding_starts(self):
extra = {"xPaddingBytes": "100-1000", "downloadSettings": {"address": "cdn.example"}}
profile = PROFILE.replace("&host=host.example", "&host=")
base, fragment = profile.split("#", 1)
profile = base + "&x_padding_bytes=100-1000&extra="
profile += quote(json.dumps(extra), safe="") + "#" + fragment
self.write_profile(self.profile, text=profile)
process = self.start()
config = json.loads((self.state_dir / "client.json").read_text())
stream = config["outbounds"][0]["streamSettings"]
self.assertEqual("", stream["xhttpSettings"]["host"])
self.assertEqual(extra, stream["xhttpSettings"]["extra"])
process.send_signal(signal.SIGINT)
stdout, stderr = self.finish(process)
self.assertEqual(0, process.returncode, stderr)
self.assert_no_state_files()
self.assert_no_secrets(stdout + stderr)
def test_stop_finishes_xray_left_after_killed_supervisor(self):
process = self.start()
xray_pid = self.xray_pid()
@@ -321,7 +339,7 @@ class ManagedXrayTest(unittest.TestCase):
(self.root / "version").write_text("print('Xray 26.3.26 (fake)')\n", encoding="utf-8")
result = self.run_cli("start")
self.assertEqual(1, result.returncode)
self.assertIn("не сообщил версию Xray 26.3.27", result.stderr)
self.assertIn("не сообщил версию Xray 26.10.10", result.stderr)
self.assert_no_state_files()
def test_replaced_xray_file_is_still_recognised_and_stopped(self):