Compare commits
39
Commits
205e995f26
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f034ea840 | ||
|
|
6d302deb76 | ||
|
|
761fd9978b | ||
|
|
0ab5dbe182 | ||
|
|
cf9781604d | ||
|
|
158075e059 | ||
|
|
144bc2515b | ||
|
|
e36a756bed | ||
|
|
ce9270623d | ||
|
|
d65403efb5 | ||
|
|
d19239bf31 | ||
|
|
4d3edc09ee | ||
|
|
a30e2eb939 | ||
|
|
16e85139b9 | ||
|
|
9f6db963be | ||
|
|
b516e9cc54 | ||
|
|
156d22c3b0 | ||
|
|
452b0c5c75 | ||
|
|
32cd7097e6 | ||
|
|
c3efe60a00 | ||
|
|
346b3bfc61 | ||
|
|
54467a2888 | ||
|
|
eca939b835 | ||
|
|
22c4fcd710 | ||
|
|
2868ba07af | ||
|
|
eaa81f4f8b | ||
|
|
e8bf134554 | ||
|
|
310cddf749 | ||
|
|
fa9a93aa0c | ||
|
|
68a944dbdf | ||
|
|
4e7777527f | ||
|
|
09f57aa1e7 | ||
|
|
5ccc75419d | ||
|
|
e709c585fa | ||
|
|
a5db265f47 | ||
|
|
06ca4ab0c6 | ||
|
|
502f828c6f | ||
|
|
05a7f1e4fb | ||
|
|
fb23b031bc |
No files matched your search
@@ -30,19 +30,6 @@ if ((Test-Path "claude") -and -not (Test-Path "dot_claude")) {
|
|||||||
Write-Host " [OK] claude -> 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)
|
# vim/.vimrc → dot_vimrc (будет ~/.vimrc)
|
||||||
if ((Test-Path "vim/.vimrc") -and -not (Test-Path "dot_vimrc")) {
|
if ((Test-Path "vim/.vimrc") -and -not (Test-Path "dot_vimrc")) {
|
||||||
Move-Item "vim/.vimrc" "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
|
New-Item -ItemType SymbolicLink -Path "$env:USERPROFILE\.claude\skills" -Target $skillsSource
|
||||||
Write-Host "Created: ~/.claude/skills -> ~/.agents/skills"
|
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
|
# Opencode
|
||||||
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.config\opencode" | Out-Null
|
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.config\opencode" | Out-Null
|
||||||
if (Test-Path "$env:USERPROFILE\.config\opencode\skills") {
|
if (Test-Path "$env:USERPROFILE\.config\opencode\skills") {
|
||||||
|
|||||||
+6
-4
@@ -10,10 +10,6 @@ opencode/.config/opencode/package.json
|
|||||||
opencode/.config/opencode/node_modules/
|
opencode/.config/opencode/node_modules/
|
||||||
opencode/.config/opencode/bun.lock
|
opencode/.config/opencode/bun.lock
|
||||||
|
|
||||||
# Codex local files
|
|
||||||
codex/.codex/
|
|
||||||
!codex/.codex/skills/
|
|
||||||
|
|
||||||
# Gemini local files
|
# Gemini local files
|
||||||
gemini/.gemini/
|
gemini/.gemini/
|
||||||
!gemini/.gemini/policies/
|
!gemini/.gemini/policies/
|
||||||
@@ -34,3 +30,9 @@ __pycache__/
|
|||||||
|
|
||||||
# Кэш скиллов, синхронизируемых Claude Code с claude.ai (~/.claude/skills -> agents)
|
# Кэш скиллов, синхронизируемых Claude Code с claude.ai (~/.claude/skills -> agents)
|
||||||
agents/.agents/skills/synced/
|
agents/.agents/skills/synced/
|
||||||
|
|
||||||
|
# Корзина программы синхронизации скиллов: старые копии, в Git не нужны
|
||||||
|
agents/.agents/skills/.trash/
|
||||||
|
|
||||||
|
# Рабочие заметки агентов (задачи, PRD, handoffs) — только локально
|
||||||
|
.scratch/
|
||||||
@@ -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.
|
* **Shared Skills:** Located in `agents/.agents/skills/`. This is the single source of truth for AI instructions.
|
||||||
* **Gemini CLI:** Configured via `gemini/.gemini/`.
|
* **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.
|
* **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.
|
* **Opencode:** MCP-based assistant configuration (`opencode.json`): Context7 для документации и встроенные `websearch`/`webfetch`. Локальный SearXNG в `docker/searxng` — резервный вариант, MCP-сервер отключён.
|
||||||
* **Skill Bridges:** Symlinks are used to share skills across Claude, Codex, and Opencode.
|
* **Skill Bridges:** Claude gets skills via the `~/.claude/skills` symlink; Opencode reads `~/.agents/skills` natively, no bridge needed.
|
||||||
|
|
||||||
## 🚀 Usage & Maintenance
|
## 🚀 Usage & Maintenance
|
||||||
|
|
||||||
@@ -39,6 +39,10 @@ This repository contains personal dotfiles and configurations for a highly produ
|
|||||||
### Adding/Updating Skills
|
### Adding/Updating Skills
|
||||||
1. Modify files in `agents/.agents/skills/`.
|
1. Modify files in `agents/.agents/skills/`.
|
||||||
2. Ensure symlinks in other tool directories are updated if new skills are added.
|
2. Ensure symlinks in other tool directories are updated if new skills are added.
|
||||||
|
3. Устаревшие скиллы время от времени удаляем. Убирай каталог целиком, а
|
||||||
|
вместе с ним ссылки из других скиллов и строку в `.gitignore`, если
|
||||||
|
скилл был внешним. Упоминания в архивах и handoff-записках — история,
|
||||||
|
их не трогаем.
|
||||||
|
|
||||||
## 📋 TODO
|
## 📋 TODO
|
||||||
|
|
||||||
@@ -52,36 +56,29 @@ This repository contains personal dotfiles and configurations for a highly produ
|
|||||||
|
|
||||||
Внутренние рассуждения и промежуточные пометки по ходу работы веди на английском — он экономнее по токенам (кириллица занимает примерно в 1,5–2 раза больше). На русском остаётся всё, что видит и хранит проект: итоговые ответы пользователю, документы, комментарии в коде и сообщения коммитов.
|
Внутренние рассуждения и промежуточные пометки по ходу работы веди на английском — он экономнее по токенам (кириллица занимает примерно в 1,5–2 раза больше). На русском остаётся всё, что видит и хранит проект: итоговые ответы пользователю, документы, комментарии в коде и сообщения коммитов.
|
||||||
|
|
||||||
## Agent skills
|
## Навыки агентов
|
||||||
|
|
||||||
### Issue tracker
|
### Хранилище задач
|
||||||
|
|
||||||
Задачи хранятся в отслеживаемых Git Markdown-файлах текущей ветки под `.scratch/`. Перед `merge --no-ff` они удаляются. См. `docs/agents/issue-tracker.md`.
|
Задачи хранятся в Gitea Issues (`git.dementev.space`), работа с ними — через CLI `tea`. См. `docs/agents/issue-tracker.md`.
|
||||||
|
|
||||||
### Triage labels
|
### Состояния triage
|
||||||
|
|
||||||
Используются стандартные названия состояний triage. См. `docs/agents/triage-labels.md`.
|
Состояния triage — метки Gitea со стандартными названиями. См. `docs/agents/triage-labels.md`.
|
||||||
|
|
||||||
### Domain docs
|
### Документы предметной области
|
||||||
|
|
||||||
Используется схема single-context: корневой `CONTEXT.md` и общие ADR в `docs/adr/`. См. `docs/agents/domain.md`.
|
Одна предметная область на весь репозиторий: корневой `CONTEXT.md` и общие ADR в `docs/adr/`. См. `docs/agents/domain.md`.
|
||||||
|
|
||||||
### Handoffs
|
### Записки для передачи работы
|
||||||
|
|
||||||
Записки для продолжения работы в новой сессии — `.scratch/handoffs/YYYYMMDD-HHMM-<slug>.md` (одноразовые, коммитятся, уборка best-effort; скилл `/handoff` пишет сюда).
|
Записки для продолжения работы в новой сессии — `.scratch/handoffs/YYYYMMDD-HHMM-<slug>.md` (одноразовые, уборка best-effort; скилл `/handoff` пишет сюда).
|
||||||
|
|
||||||
### `.scratch/` — рабочая зона; в master не переносится
|
### `.scratch/` — локальная рабочая зона
|
||||||
|
|
||||||
Весь `.scratch/` — заметки по ходу дела: handoffs, PRD фич, нарезка задач. Коммитится (не в `.gitignore`), чтобы версионировать и передавать между сессиями, но в master ей делать нечего — это процесс, а не продукт.
|
Весь `.scratch/` — заметки по ходу дела: handoffs и черновики (задачи живут в Gitea). Каталог в `.gitignore`: он живёт только на этой машине и в Git не попадает.
|
||||||
|
|
||||||
Долговечное знание держим **вне** `.scratch/` (в `docs/`, `CONTEXT.md`). Прежде чем сливать фиче-ветку, убедись, что долговечная часть уже вынесена туда.
|
Долговечное знание держим **вне** `.scratch/` (в `docs/`, `CONTEXT.md`) и коммитим вместе с работой. Прежде чем сливать фиче-ветку, убедись, что долговечная часть уже вынесена туда: остальное из `.scratch/` потом не восстановить.
|
||||||
|
|
||||||
Слияние фиче-ветки в master:
|
|
||||||
|
|
||||||
1. Последним коммитом ветки — `git rm -r .scratch` (`chore: убрать рабочие заметки перед слиянием в master`).
|
|
||||||
2. `git merge --no-ff` в master.
|
|
||||||
|
|
||||||
`--no-ff` тянет все коммиты ветки в историю master, поэтому удалённый `.scratch/` остаётся достижим из merge-коммита и достаётся из истории в любой момент. Ничего не теряется — тег или архив не нужны.
|
|
||||||
|
|
||||||
## Субагенты и экономия контекста
|
## Субагенты и экономия контекста
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ Chezmoi использует отдельный source dir `chezmoi/` внутр
|
|||||||
| `vscode` | `server-env-setup` для VS Code Server в WSL: секреты + авто-прокси |
|
| `vscode` | `server-env-setup` для VS Code Server в WSL: секреты + авто-прокси |
|
||||||
| `chatgpt` | Отдельный ярлык `ChatGPT Proxy` с запретом прямого запуска при ошибке |
|
| `chatgpt` | Отдельный ярлык `ChatGPT Proxy` с запретом прямого запуска при ошибке |
|
||||||
|
|
||||||
`install.sh` дополнительно настраивает opencode (симлинки, MCP-конфиг), claude (симлинкает `settings.local.json` из репозитория) и подключает bash_custom.
|
`install.sh` дополнительно настраивает opencode (MCP-конфиг), claude (симлинк `~/.claude/skills` на общие скиллы) и подключает bash_custom.
|
||||||
|
|
||||||
### Windows (chezmoi)
|
### Windows (chezmoi)
|
||||||
|
|
||||||
@@ -76,9 +76,9 @@ Chezmoi использует отдельный source dir `chezmoi/` внутр
|
|||||||
| Инструмент | Что настраивается |
|
| Инструмент | Что настраивается |
|
||||||
|------------|-------------------|
|
|------------|-------------------|
|
||||||
| **Agents** | Каноничное хранилище скиллов: `~/.agents/skills/<name>/SKILL.md` |
|
| **Agents** | Каноничное хранилище скиллов: `~/.agents/skills/<name>/SKILL.md` |
|
||||||
| **Claude** | `~/.claude/skills` -> симлинк на agents; `settings.local.json` -> симлинк на репозиторий (базовые permissions + statusLine); `statusline-command.sh` -> копия из репозитория; `settings.json` - управляется Claude Code |
|
| **Claude** | `~/.claude/skills` -> симлинк на agents; настройки (`settings.json`, `settings.local.json`) - управляются Claude Code на машине |
|
||||||
| **[Opencode](https://github.com/anomalyco/opencode)** | MCP-серверы (context7, web-reader, playwright и др.), `~/.config/opencode/skills` -> симлинк на agents |
|
| **[Opencode](https://github.com/anomalyco/opencode)** | MCP-серверы (context7, web-reader, playwright и др.); скиллы opencode сам читает из `~/.agents/skills` |
|
||||||
| **Codex** | `~/.codex/skills/` -> симлинки на agents |
|
| **Codex** | `~/.codex` - обычный каталог, управляется Codex; скиллы - из общего хранилища agents |
|
||||||
| **Gemini** | Политики для CLI: pre-approved readonly команды |
|
| **Gemini** | Политики для CLI: pre-approved readonly команды |
|
||||||
|
|
||||||
## Настройка
|
## Настройка
|
||||||
@@ -114,6 +114,11 @@ Linux/Bash-интерфейс `proxy-client` поддерживает внешн
|
|||||||
Windows не меняется. Если Codex уже запущен, его нужно полностью закрыть перед
|
Windows не меняется. Если Codex уже запущен, его нужно полностью закрыть перед
|
||||||
запуском через `Codex Proxy`.
|
запуском через `Codex Proxy`.
|
||||||
|
|
||||||
|
Codex и Claude Code могут брать прокси из своих настроек, независимо от того,
|
||||||
|
откуда их запустили. Шаблоны лежат в `~/.config/proxy-client/examples/` и
|
||||||
|
копируются вручную; установщик настройки агентов не трогает. Подробнее —
|
||||||
|
[в README proxy-client](proxy-client/README.md#прокси-в-настройках-codex-и-claude-code).
|
||||||
|
|
||||||
`Claude Proxy` поддерживает как обычную установку в
|
`Claude Proxy` поддерживает как обычную установку в
|
||||||
`%LOCALAPPDATA%\AnthropicClaude`, так и пакет из Microsoft Store. В обоих
|
`%LOCALAPPDATA%\AnthropicClaude`, так и пакет из Microsoft Store. В обоих
|
||||||
случаях он передает параметры Electron `--proxy-server` и
|
случаях он передает параметры Electron `--proxy-server` и
|
||||||
@@ -201,7 +206,7 @@ PowerShell и VS Code: одного общего файла у Windows и WSL н
|
|||||||
| Файл | Назначение |
|
| Файл | Назначение |
|
||||||
|------|------------|
|
|------|------------|
|
||||||
| `~/.env.secrets` | API-ключи, подгружается bash и VS Code Server |
|
| `~/.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 |
|
| `~/.bash_aliases` | Персональные алиасы bash |
|
||||||
| `~/.gitconfig.local` | Персональные Git-настройки (`user.name`, `core.editor` и т.д.) |
|
| `~/.gitconfig.local` | Персональные Git-настройки (`user.name`, `core.editor` и т.д.) |
|
||||||
| `~/.local/bin/start-claude-proxy.ps1` | Скрипт запуска Claude Desktop для Windows с переменными прокси и параметрами Electron |
|
| `~/.local/bin/start-claude-proxy.ps1` | Скрипт запуска Claude Desktop для Windows с переменными прокси и параметрами Electron |
|
||||||
@@ -221,5 +226,5 @@ stow проще и покрывает весь Linux/WSL workflow. chezmoi ну
|
|||||||
**Почему Windows PowerShell 5.1 не трогается?**
|
**Почему Windows PowerShell 5.1 не трогается?**
|
||||||
Если форсировать UTF-8 и менять профиль PS 5.1, можно сломать старые скрипты, `.bat`/`.cmd` и консольные утилиты. PS 7 - основной shell, PS 5.1 - режим совместимости.
|
Если форсировать UTF-8 и менять профиль PS 5.1, можно сломать старые скрипты, `.bat`/`.cmd` и консольные утилиты. PS 7 - основной shell, PS 5.1 - режим совместимости.
|
||||||
|
|
||||||
**Почему Claude `settings.local.json` - симлинк, а `settings.json` - нет?**
|
**Почему у Claude в репозитории нет настроек?**
|
||||||
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 Code (`settings.json`, `settings.local.json`) принадлежат самому Claude Code: он перезаписывает их при каждом "always allow" и при смене модели. Список разрешений не ведём вручную - работаем с autoapprove. Из репозитория линкуются только скиллы. Машинно-специфичные настройки (`model`, `effortLevel`) - в `settings.json`, управляются через Claude Code.
|
||||||
@@ -39,7 +39,8 @@ wait-what/
|
|||||||
wizard/
|
wizard/
|
||||||
writing-for-agents/
|
writing-for-agents/
|
||||||
retro/
|
retro/
|
||||||
|
chief-of-staff/
|
||||||
|
pr/
|
||||||
|
|
||||||
# Skills, подключаемые в agents/ как внешние плагины (не версионируются здесь)
|
# Skills, подключаемые в agents/ как внешние плагины (не версионируются здесь)
|
||||||
claude-team-review
|
claude-team-review
|
||||||
adversarial-review
|
|
||||||
@@ -23,8 +23,7 @@ description: >-
|
|||||||
|
|
||||||
**Не разворачивай конвейер на мелочь.** Косметика, один
|
**Не разворачивай конвейер на мелочь.** Косметика, один
|
||||||
файл — сделай сам: накладные расходы процесса многократно превысят цену
|
файл — сделай сам: накладные расходы процесса многократно превысят цену
|
||||||
работы. Очередь зависимых issue с автономным прогоном —
|
работы.
|
||||||
это `/coordinator-loop`, не этот плейбук.
|
|
||||||
|
|
||||||
## Роли
|
## Роли
|
||||||
|
|
||||||
@@ -210,7 +209,7 @@ description: >-
|
|||||||
уверенный вердикт одной (APPROVED без находок) не закрывает находки
|
уверенный вердикт одной (APPROVED без находок) не закрывает находки
|
||||||
другой.
|
другой.
|
||||||
Пофиндинговый триаж — у **исполнителя**, не у тебя (правило
|
Пофиндинговый триаж — у **исполнителя**, не у тебя (правило
|
||||||
`/claude-team-review` и `/coordinator-loop`: судит тот, у кого контекст
|
`/claude-team-review`: судит тот, у кого контекст
|
||||||
кода): перешли Codex индекс находок **дословно**, он чинит или
|
кода): перешли Codex индекс находок **дословно**, он чинит или
|
||||||
**аргументированно отклоняет** — через контракт и тест, не голословно;
|
**аргументированно отклоняет** — через контракт и тест, не голословно;
|
||||||
промпт починки — «закрой перечисленное, больше ничего не меняй».
|
промпт починки — «закрой перечисленное, больше ничего не меняй».
|
||||||
@@ -256,7 +255,7 @@ description: >-
|
|||||||
|
|
||||||
## Пользователь в петле
|
## Пользователь в петле
|
||||||
|
|
||||||
В отличие от `/coordinator-loop`, здесь пользователь рядом: на границах
|
Здесь пользователь рядом: на границах
|
||||||
этапов давай в чат карту прогресса в 4–7 строк обычными словами
|
этапов давай в чат карту прогресса в 4–7 строк обычными словами
|
||||||
(✅ сделано / ▶ идёт / ⬜ дальше); термины плейбука — только с
|
(✅ сделано / ▶ идёт / ⬜ дальше); термины плейбука — только с
|
||||||
расшифровкой: телеграфные статусы в терминах этапов теряют пользователя.
|
расшифровкой: телеграфные статусы в терминах этапов теряют пользователя.
|
||||||
@@ -295,13 +294,8 @@ teammate-субагентов — шум, а не событие: не докл
|
|||||||
|
|
||||||
## Соседние скиллы
|
## Соседние скиллы
|
||||||
|
|
||||||
- `/coordinator-loop` — автономный цикл очереди issue на стороне Codex
|
|
||||||
(координатор в Codex, worker'ы — его субагенты). Выстрадан и отлажен;
|
|
||||||
словарь (индекс, свежие сессии, две ручки стоимости) — общий.
|
|
||||||
- `/claude-team-review` — ревью-петля, где лид сам чинит код (живёт
|
- `/claude-team-review` — ревью-петля, где лид сам чинит код (живёт
|
||||||
отдельным репозиторием, в среде может быть не подключён).
|
отдельным репозиторием, в среде может быть не подключён).
|
||||||
- `/adversarial-review` — разовое адверсарное ревью Кодексом (в среде
|
|
||||||
может быть не подключён).
|
|
||||||
- `codex-subagent-playbook` — близнец этого плейбука для координатора на
|
- `codex-subagent-playbook` — близнец этого плейбука для координатора на
|
||||||
стороне Codex; скрыт от автовызова, зови по имени.
|
стороне Codex; скрыт от автовызова, зови по имени.
|
||||||
- `/to-tickets` — разбиение на тикеты-срезы с рёбрами блокировок (этап 1);
|
- `/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 (план→код→тест; её внешняя проверка готовности в
|
|
||||||
среде без вложенных субагентов недоступна).
|
|
||||||
-272
@@ -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` — для коммитов изменений навыка.
|
|
||||||
-211
@@ -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) как замер.
|
|
||||||
-163
@@ -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-а
|
|
||||||
(находка + его обоснование отклонения) дословно, без интерпретаций
|
|
||||||
координатора.
|
|
||||||
@@ -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,7 +3,7 @@
|
|||||||
|
|
||||||
# Context7 API (https://context7.com)
|
# Context7 API (https://context7.com)
|
||||||
# Используется для поиска документации библиотек
|
# Используется для поиска документации библиотек
|
||||||
export CONTEXT7_API_KEY="your_context7_api_key_here"
|
#export CONTEXT7_API_KEY="your_context7_api_key_here"
|
||||||
|
|
||||||
# Z.AI API (https://z.ai)
|
# Z.AI API (https://z.ai)
|
||||||
# Используется для:
|
# Используется для:
|
||||||
@@ -11,4 +11,6 @@ export CONTEXT7_API_KEY="your_context7_api_key_here"
|
|||||||
# - Поиска в интернете (web-search-prime)
|
# - Поиска в интернете (web-search-prime)
|
||||||
# - Чтения веб-страниц (web-reader)
|
# - Чтения веб-страниц (web-reader)
|
||||||
# - Чтения GitHub репозиториев (zread)
|
# - Чтения GitHub репозиториев (zread)
|
||||||
export ZAI_API_KEY="your_zai_api_key_here"
|
|
||||||
|
# OpenCode Zen API — модель Jev для ревьюера разрешений (opencode-permission-reviewer)
|
||||||
|
#export OPENCODE_API_KEY=...
|
||||||
@@ -2,6 +2,3 @@
|
|||||||
*
|
*
|
||||||
!skills
|
!skills
|
||||||
!.gitignore
|
!.gitignore
|
||||||
!settings.local.json
|
|
||||||
!statusline-command.sh
|
|
||||||
!CLAUDE.md
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# Глобальный словарь рабочего процесса
|
|
||||||
|
|
||||||
Термины владельца, общие для всех проектов на этой машине.
|
|
||||||
|
|
||||||
## Ревью по температуре
|
|
||||||
|
|
||||||
Ось — сколько авторского контекста несёт ревьюер:
|
|
||||||
|
|
||||||
- **Горячее ревью** — автор сам перечитывает свой артефакт глазами читателя,
|
|
||||||
не автора. Контекст полный, дёшево. Текстовые огрехи правит по месту молча;
|
|
||||||
находку уровня решений (противоречие, дыра в охвате) предъявляет владельцу,
|
|
||||||
не исправляя и не замалчивая.
|
|
||||||
- **Тёплое ревью** — другой агент, получивший бриф. Контекст частичный: знает
|
|
||||||
замысел, но не варился в нём.
|
|
||||||
- **Холодное ревью** — свежая сессия или субагент без общего контекста.
|
|
||||||
Независимо, дороже всех.
|
|
||||||
|
|
||||||
Просьба «перечитай свежим взглядом» двусмысленна — уточни температуру;
|
|
||||||
по умолчанию имеется в виду горячее.
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,3 +1,3 @@
|
|||||||
# Claude Code управляется вручную через install.sh (копирование, не симлинк).
|
# Claude Code управляется вручную через install.sh (точечные симлинки).
|
||||||
# Этот файл не даёт stow случайно создать симлинк ~/.claude.
|
# Этот файл не даёт stow случайно создать симлинк ~/.claude.
|
||||||
\.claude
|
\.claude
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../../agents/.agents/skills/conventional-commits
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../../agents/.agents/skills/coordinator-loop
|
|
||||||
@@ -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`.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Документы предметной области
|
# Документы предметной области
|
||||||
|
|
||||||
Репозиторий использует схему single-context.
|
В репозитории одна предметная область: один `CONTEXT.md` в корне и общие ADR.
|
||||||
|
|
||||||
## Что читать перед изучением репозитория
|
## Что читать перед изучением репозитория
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,62 @@
|
|||||||
# Хранилище задач: локальные Markdown-файлы
|
# Хранилище задач: Gitea
|
||||||
|
|
||||||
Задачи и спецификации этого репозитория хранятся в `.scratch/`.
|
Задачи и спецификации этого репозитория хранятся в Gitea Issues на
|
||||||
|
`git.dementev.space` (репозиторий `ddmitry/dotfiles`). Все операции — через
|
||||||
|
CLI `tea`. Внутри клона `tea` сам находит репозиторий и учётную запись по
|
||||||
|
`git remote`; если не нашёл, добавь `--repo ddmitry/dotfiles`.
|
||||||
|
|
||||||
## Структура
|
GitVerse — только зеркало, задачи там не ведём.
|
||||||
|
|
||||||
- Одна возможность на каталог: `.scratch/<feature-slug>/`.
|
Заголовки, описания и комментарии задач пишутся по-русски.
|
||||||
- Спецификация: `.scratch/<feature-slug>/spec.md`.
|
|
||||||
- Каждая задача хранится в отдельном файле:
|
|
||||||
`.scratch/<feature-slug>/issues/<NN>-<slug>.md`.
|
|
||||||
- Нумерация задач начинается с `01`.
|
|
||||||
- Состояние triage записывается строкой `Status:` в начале файла.
|
|
||||||
- Обсуждение дописывается в конец файла под заголовком `## Comments`.
|
|
||||||
|
|
||||||
## Жизненный цикл
|
## Команды
|
||||||
|
|
||||||
- Файлы в `.scratch/` отслеживаются Git.
|
- **Создать задачу**: `tea issues create --title "..." --description-file -`
|
||||||
- Они принадлежат рабочей ветке и считаются эфемерными.
|
с текстом через heredoc. Метки — `--labels "a,b"`.
|
||||||
- Перед `merge --no-ff` относящийся к ветке каталог `.scratch/<feature-slug>/`
|
- **Прочитать задачу**: `tea issues <номер> --comments`.
|
||||||
удаляется и удаление фиксируется в этой же ветке.
|
- **Список задач**: `tea issues list --state open --labels "..." --fields
|
||||||
- После слияния содержимое задач остаётся доступно в истории Git.
|
index,title,labels,assignees --output json`.
|
||||||
|
- **Комментарий**: `tea comment <номер> "..."`.
|
||||||
|
- **Метки**: `tea issues edit <номер> --add-labels "..."` /
|
||||||
|
`--remove-labels "..."`. Если метки ещё нет в репозитории, создай её:
|
||||||
|
`tea labels create --name "..." --color "#..."`.
|
||||||
|
- **Закрыть**: сначала комментарий с причиной, затем `tea issues close <номер>`.
|
||||||
|
|
||||||
|
## PR как источник задач
|
||||||
|
|
||||||
|
**PR как источник запросов: нет.** _(Поставь `да`, если внешние PR в этом
|
||||||
|
репозитории нужно разбирать как запросы на доработку; `/triage` читает этот
|
||||||
|
флаг.)_
|
||||||
|
|
||||||
|
У задач и PR в Gitea общая нумерация: голый `#42` может оказаться и тем, и
|
||||||
|
другим. Проверяй через `tea issues 42` или `tea pulls 42`.
|
||||||
|
|
||||||
## Публикация задачи
|
## Публикация задачи
|
||||||
|
|
||||||
Когда навык просит опубликовать задачу, нужно создать файл внутри
|
Когда навык просит опубликовать задачу, нужно создать задачу в Gitea.
|
||||||
`.scratch/<feature-slug>/`, при необходимости создав каталог.
|
|
||||||
|
|
||||||
## Получение задачи
|
## Получение задачи
|
||||||
|
|
||||||
Нужно прочитать файл по переданному пути. Обычно пользователь указывает путь
|
Нужно выполнить `tea issues <номер> --comments`.
|
||||||
или номер задачи.
|
|
||||||
|
|
||||||
## Работа `/wayfinder`
|
## Работа `/wayfinder`
|
||||||
|
|
||||||
Карта работы хранится отдельно от дочерних задач:
|
Карта — одна задача, дочерние задачи — отдельные задачи Gitea.
|
||||||
|
|
||||||
- Карта: `.scratch/<effort>/map.md`.
|
- **Карта**: задача с меткой `wayfinder:map`; в описании — разделы
|
||||||
- Дочерняя задача: `.scratch/<effort>/issues/NN-<slug>.md`.
|
«Заметки», «Принятые решения» и «Неясности».
|
||||||
- Строка `Type:` содержит тип: `research`, `prototype`, `grilling` или `task`.
|
- **Дочерняя задача**: первой строкой описания — `Часть #<карта>`; в
|
||||||
- Строка `Status:` содержит состояние `claimed` или `resolved`.
|
описании карты — список дочерних задач. Метка типа: `wayfinder:<тип>`
|
||||||
- Зависимости перечисляются строкой `Blocked by: NN, NN`.
|
(`research`, `prototype`, `grilling` или `task`).
|
||||||
- Задача свободна, если она открыта, не заблокирована и не занята.
|
- **Зависимости**: встроенные зависимости Gitea. Добавить блокирующую
|
||||||
- Из нескольких свободных задач первой выбирается задача с меньшим номером.
|
задачу:
|
||||||
- При взятии задачи нужно установить `Status: claimed` и сохранить файл.
|
`tea api -X POST repos/ddmitry/dotfiles/issues/<дочерняя>/dependencies
|
||||||
- При завершении нужно добавить ответ под заголовком `## Answer`, установить
|
-d '{"owner":"ddmitry","repo":"dotfiles","index":<блокирующая>}'`.
|
||||||
`Status: resolved` и добавить краткий итог со ссылкой в раздел решений
|
Посмотреть: `tea api repos/ddmitry/dotfiles/issues/<n>/dependencies`.
|
||||||
файла `map.md`.
|
Задача свободна от блокировок, когда все блокирующие задачи закрыты.
|
||||||
|
- **Свободные задачи**: открытые дочерние задачи карты без открытых
|
||||||
|
блокирующих и без исполнителя; первой берётся та, что раньше в списке карты.
|
||||||
|
- **Взять задачу**: `tea issues edit <n> --add-assignees ddmitry` — первым
|
||||||
|
действием сессии.
|
||||||
|
- **Завершить**: комментарий с ответом, `tea issues close <n>`, затем краткий
|
||||||
|
итог со ссылкой в раздел «Принятые решения» описания карты.
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
# Состояния triage
|
# Состояния triage
|
||||||
|
|
||||||
Навыки используют пять стандартных ролей. Для локального хранилища выбранное
|
Навыки используют пять стандартных ролей. В Gitea каждая роль — метка
|
||||||
значение записывается в строке `Status:` файла задачи.
|
задачи с тем же названием. Если метки ещё нет в репозитории, её нужно
|
||||||
|
создать (`tea labels create`), а не придумывать другое название.
|
||||||
|
|
||||||
| Роль навыка | Значение `Status:` | Смысл |
|
| Роль навыка | Метка в Gitea | Смысл |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `needs-triage` | `needs-triage` | Задачу должен оценить сопровождающий |
|
| `needs-triage` | `needs-triage` | Задачу должен оценить сопровождающий |
|
||||||
| `needs-info` | `needs-info` | Нужна дополнительная информация от автора |
|
| `needs-info` | `needs-info` | Нужна дополнительная информация от автора |
|
||||||
@@ -11,5 +12,5 @@
|
|||||||
| `ready-for-human` | `ready-for-human` | Задачу должен выполнить человек |
|
| `ready-for-human` | `ready-for-human` | Задачу должен выполнить человек |
|
||||||
| `wontfix` | `wontfix` | Задача не будет выполняться |
|
| `wontfix` | `wontfix` | Задача не будет выполняться |
|
||||||
|
|
||||||
Когда навык упоминает роль triage, нужно использовать соответствующее значение
|
Когда навык упоминает роль triage, нужно использовать соответствующую метку
|
||||||
из этой таблицы.
|
из этой таблицы.
|
||||||
@@ -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`
|
||||||
@@ -2,577 +2,226 @@
|
|||||||
|
|
||||||
Дата: 2026-07-15
|
Дата: 2026-07-15
|
||||||
|
|
||||||
Статус: основная реализация принята вручную; самостоятельный установщик
|
Статус: реализовано; 2026-09-28 упрощено по принципу ниже
|
||||||
спроектирован 2026-07-21, но ещё не реализован
|
|
||||||
|
Точное поведение, форматы, сообщения и команды описаны в коде,
|
||||||
|
`proxy-client --help` и [`proxy-client/README.md`](../../proxy-client/README.md).
|
||||||
|
Здесь — то, чего по коду не видно: задача, границы, выбор и его причины, риски.
|
||||||
|
|
||||||
|
## Принцип: личный инструмент
|
||||||
|
|
||||||
|
`proxy-client` пользуются владелец и несколько друзей на своих машинах. Код
|
||||||
|
защищается от реальных ошибок и опасностей:
|
||||||
|
|
||||||
|
- скачанный Xray проверяется по SHA-256 и заменяется атомарно;
|
||||||
|
- секреты профиля не попадают в вывод, служебные файлы закрыты правами 600/700;
|
||||||
|
- локальный порт Xray слушает только loopback;
|
||||||
|
- чужие файлы не перезаписываются;
|
||||||
|
- сигнал уходит только процессам из `state.json`: Xray — если его
|
||||||
|
`/proc/PID/exe` совпадает с запущенным файлом, супервизору — если Xray
|
||||||
|
подтверждён и в командной строке супервизора есть `proxy-client`.
|
||||||
|
|
||||||
|
Пользователь и его собственные файлы — не противник. Сообщение об ошибке
|
||||||
|
называет файл полным путём и подсказывает готовую команду.
|
||||||
|
|
||||||
|
Новая защита появляется вместе со сценарием, который реально случается у
|
||||||
|
пользователя, и этот сценарий записывается рядом с ней. Так же проверяется и
|
||||||
|
прежний код: в 2026-09-28 убраны защиты без такого сценария — повторное
|
||||||
|
использование PID, подложный `state.json`, zip-бомба в проверенном архиве,
|
||||||
|
гонка `status` с параллельным `start`.
|
||||||
|
|
||||||
|
Тесты проверяют поведение через команду `proxy-client`, разбор ссылки
|
||||||
|
(`parse_vless` + `build_xray_config`), установку Xray с подставным
|
||||||
|
скачивателем, функции Bash с поддельным `PROXY_CLIENT_BIN` и установщик пакета
|
||||||
|
во временном HOME.
|
||||||
|
|
||||||
## Проблема
|
## Проблема
|
||||||
|
|
||||||
На удалённых Linux-серверах нет GUI-клиента Hiddify. Перед запуском Claude Code
|
На удалённых Linux-серверах нет GUI-клиента Hiddify. Перед запуском Claude Code
|
||||||
или Codex приходится вручную запускать Xray и выставлять proxy-переменные.
|
или Codex приходится вручную запускать Xray и выставлять proxy-переменные.
|
||||||
Переменные легко забыть, после чего агент стартует без прокси.
|
Переменные легко забыть, и агент стартует без прокси.
|
||||||
|
|
||||||
Нужны привычные команды `pclaude` и `pcodex`, которые не запускают агента,
|
Нужны привычные команды `pclaude` и `pcodex`, которые не запускают агента,
|
||||||
если явно настроенный прокси недоступен. Решение должно работать как часть этого
|
если явно настроенный прокси недоступен. Решение работает и в составе этого
|
||||||
репозитория и отдельно от остальных dotfiles.
|
репозитория, и отдельно от остальных dotfiles.
|
||||||
|
|
||||||
## Контекст
|
## Контекст
|
||||||
|
|
||||||
Существующий дизайн
|
Прежний дизайн
|
||||||
[`pclaude` / `pcodex`](../superpowers/specs/2026-05-19-proxy-cli-launchers-design.md)
|
[`pclaude` / `pcodex`](../superpowers/specs/2026-05-19-proxy-cli-launchers-design.md)
|
||||||
задаёт важный контракт: proxy-переменные видны только дочернему процессу,
|
задаёт договор: proxy-переменные видны только дочернему процессу, аргументы и
|
||||||
аргументы и код возврата сохраняются, а недоступный прокси приводит к отказу
|
код возврата сохраняются, недоступный прокси — отказ до запуска агента. Эта
|
||||||
до запуска агента.
|
спецификация заменяет его Linux-часть; Windows-часть не меняется.
|
||||||
|
|
||||||
Эта спецификация заменяет Linux-часть прежнего дизайна в вопросах расположения
|
Xray принимает конфигурационные файлы, а не `vless://`-ссылки. Его SOCKS-вход
|
||||||
функций, источника настроек и вывода `proxy_on`/`proxy_off`. Windows-часть
|
принимает SOCKS и HTTP на одном порту, поэтому прежняя схема proxy-переменных
|
||||||
прежнего дизайна не меняется.
|
подходит локальному Xray без второго порта. См.
|
||||||
|
|
||||||
Сейчас Bash-функции жёстко используют внешний Hiddify на
|
|
||||||
`hproxy.local:12334`. Принесённый прототип `scripts/setup-xray-proxy.sh`
|
|
||||||
умеет установить и запустить Xray из готового JSON, но смешивает установку и
|
|
||||||
запуск, не разбирает ссылки Hiddify и небезопасно ищет чужие процессы через
|
|
||||||
`pkill`.
|
|
||||||
|
|
||||||
Xray принимает конфигурационные файлы, а не `vless://`-ссылки. При этом его
|
|
||||||
SOCKS-вход принимает SOCKS и HTTP на одном порту, поэтому существующая схема
|
|
||||||
proxy-переменных совместима с локальным Xray без второго порта. См.
|
|
||||||
[командную строку Xray](https://xtls.github.io/en/document/command.html) и
|
[командную строку Xray](https://xtls.github.io/en/document/command.html) и
|
||||||
[SOCKS-вход](https://xtls.github.io/en/config/inbounds/socks.html).
|
[SOCKS-вход](https://xtls.github.io/en/config/inbounds/socks.html).
|
||||||
|
|
||||||
## Цели
|
## Цели
|
||||||
|
|
||||||
- Дать headless Linux-серверу управляемый пользовательский Xray без systemd и
|
- Управляемый пользовательский Xray на Linux-сервере без systemd и root.
|
||||||
прав root.
|
- Явные `pclaude` и `pcodex`, которые нельзя случайно запустить через другой
|
||||||
- Сохранить явные `pclaude` и `pcodex`, которые нельзя случайно запустить через
|
прокси или без прокси.
|
||||||
другой прокси или без настроенного прокси.
|
- Одна VLESS-ссылка Hiddify превращается в клиентский JSON Xray.
|
||||||
- Принимать одну точечную VLESS-ссылку Hiddify и преобразовывать её в клиентский
|
- Внешний Hiddify работает, как раньше.
|
||||||
JSON Xray.
|
- Машинный `NO_PROXY` настраивается без правки файлов репозитория.
|
||||||
- Сохранить совместимость Bash-функций с внешним Hiddify.
|
- Каталог `proxy-client/` можно скопировать отдельно и поставить своим
|
||||||
- Позволить настраивать машинный `NO_PROXY` без правки файлов репозитория.
|
установщиком, без остальных dotfiles и без Stow.
|
||||||
- Поддержать самостоятельную установку из отдельно скопированного каталога
|
|
||||||
`proxy-client/` без зависимости от общего установщика dotfiles и без
|
|
||||||
обязательного stow.
|
|
||||||
|
|
||||||
## Не входит в объём
|
## Не входит в объём
|
||||||
|
|
||||||
- Windows, PowerShell, Windows Codex launcher и VS Code Server.
|
- Windows, PowerShell, Windows Codex launcher и VS Code Server.
|
||||||
- systemd, автозапуск после перезагрузки, sudo, firewall, TUN и изменение
|
- systemd, автозапуск, sudo, firewall, TUN и системная маршрутизация.
|
||||||
системной маршрутизации.
|
- Автоматический выбор между Xray и Hiddify или запасной прокси.
|
||||||
- Автоматический выбор между Xray и Hiddify или переход на запасной прокси.
|
- Несколько одновременных профилей у одного Unix-пользователя.
|
||||||
- Одновременное управление несколькими профилями одного Unix-пользователя.
|
- Импорт подписок или полного JSON-экспорта Hiddify.
|
||||||
- Универсальный импорт подписок или полного JSON-экспорта Hiddify.
|
- Протоколы за пределами двух поддержанных семейств VLESS.
|
||||||
- Протоколы и сочетания за пределами двух явно поддержанных семейств.
|
- pip, venv и сторонние Python-библиотеки.
|
||||||
- pip, venv и любые сторонние Python-библиотеки.
|
- Сетевая изоляция голых `claude` и `codex`: намерение выражают команды с
|
||||||
- Сетевая изоляция голых `claude` и `codex`: как и раньше, гарантию намерения
|
префиксом `p`.
|
||||||
дают явные команды с префиксом `p`.
|
|
||||||
|
|
||||||
## Рассмотренные варианты
|
## Рассмотренные варианты
|
||||||
|
|
||||||
### Только Bash
|
- **Только Bash.** URL-кодирование, JSON, контрольные суммы и управление
|
||||||
|
процессом делают Bash хрупким и плохо тестируемым.
|
||||||
|
- **Bash-контроллер и Python-преобразователь.** Граница между двумя языками
|
||||||
|
усложняет ошибки, тесты и единый источник настроек.
|
||||||
|
- **Один Python CLI и тонкий Bash-модуль.** Выбран. Python отвечает за данные,
|
||||||
|
установку Xray, процессы и запуск дочерних команд. Bash нужен только там, где
|
||||||
|
меняется окружение текущей shell-сессии.
|
||||||
|
- **Без менеджера**: готовые JSON и Xray вручную в tmux. Почти нет кода, но не
|
||||||
|
решает главного — безопасного `pclaude`/`pcodex` из ссылки Hiddify.
|
||||||
|
|
||||||
Ближе всего к принесённому прототипу, но URL-кодирование, JSON, проверка схемы,
|
Для отдельно скопированного пакета рассматривались: только инструкция для
|
||||||
контрольные суммы и безопасное управление процессом делают Bash-реализацию
|
ручных ссылок (легко забыть шаги), копирование файлов в `$HOME` (сложно
|
||||||
хрупкой и плохо тестируемой.
|
обновлять), обязательный Stow (лишняя зависимость на серверах) и свой
|
||||||
|
установщик. Выбран свой установщик, который ставит прямые ссылки: один способ,
|
||||||
|
нет второй копии файлов.
|
||||||
|
|
||||||
### Bash-контроллер и отдельный Python-преобразователь
|
## Решение
|
||||||
|
|
||||||
Оставляет жизненный цикл в Bash, а разбор VLESS переносит в Python. Граница
|
### Компоненты
|
||||||
между двумя реализациями усложняет ошибки, тесты и единый источник настроек.
|
|
||||||
|
|
||||||
### Один Python CLI и тонкий Bash-модуль
|
`proxy-client` — один исполняемый файл на Python 3.8+ без внешних библиотек:
|
||||||
|
установка Xray, запуск и остановка, проверка доступности, разбор VLESS, запуск
|
||||||
|
дочерней команды через прокси. Команды: `install`, `start`, `stop`, `status`,
|
||||||
|
`env`, `run`. Без подкоманды ничего не делается.
|
||||||
|
|
||||||
Python отвечает за данные, установку Xray, процессы и запуск дочерних команд.
|
`shell.bash` — единственный владелец `proxy_on`, `proxy_off`, `pclaude`,
|
||||||
Bash нужен только там, где необходимо изменить окружение текущей shell-сессии.
|
`pcodex` и `myip`; в `bash/.bash_functions` их нет, чтобы не зависеть от
|
||||||
Это выбранный вариант.
|
порядка `source`. Модуль сам конфиг не читает:
|
||||||
|
|
||||||
### Не создавать менеджер
|
- `pclaude` и `pcodex` вызывают `proxy-client run -- COMMAND ARG...`;
|
||||||
|
- `proxy_on` берёт готовые переменные из `proxy-client env` и применяет их,
|
||||||
|
только если команда завершилась успешно; `eval` не используется;
|
||||||
|
- `proxy_off` очищает переменные в обоих регистрах.
|
||||||
|
|
||||||
Можно хранить готовые JSON и вручную запускать Xray в tmux. Вариант почти не
|
`proxy_on`/`proxy_off` больше не запрашивают внешний IP: это замедляло
|
||||||
требует кода, но не решает основную боль: безопасный запуск `pclaude` и
|
переключение. Для проверки есть `myip` и `proxy-client status`.
|
||||||
`pcodex` из точечной ссылки Hiddify.
|
|
||||||
|
|
||||||
### Доставка отдельного пакета
|
### Настройки
|
||||||
|
|
||||||
Рассмотрены четыре способа подключить отдельно скопированный каталог:
|
Два файла в `~/.config/proxy-client/`:
|
||||||
|
|
||||||
- оставить только инструкции для stow и ручных симлинков — просто, но сохраняет
|
- `config` — несекретный машинный INI: тип `xray` или `external`, имя, адрес,
|
||||||
легко забываемые шаги;
|
порт, добавка к `NO_PROXY`. Если файла нет, используется прежний Hiddify на
|
||||||
- копировать файлы в домашний каталог — не требует сохранять исходный каталог,
|
`hproxy.local:12334`.
|
||||||
но усложняет обновления и расходится с моделью dotfiles;
|
- `default.url` — секретная VLESS-ссылка с правами 600; `start -f` берёт другой
|
||||||
- требовать stow — даёт привычное управление ссылками, но добавляет
|
файл.
|
||||||
необязательную зависимость на удалённых серверах;
|
|
||||||
- дать пакету собственный установщик, который использует stow при наличии и
|
|
||||||
создаёт те же симлинки напрямую без него.
|
|
||||||
|
|
||||||
Выбран последний вариант. Он сохраняет один результат установки при разном
|
Они разделены, потому что первый описывает политику машины, а второй — секрет,
|
||||||
составе машины и не требует второй копии обновляемых файлов.
|
который меняется независимо. Выбран INI, а не TOML: `tomllib` появился только
|
||||||
|
в Python 3.11.
|
||||||
|
|
||||||
## Рекомендуемое решение
|
### Явный тип прокси
|
||||||
|
|
||||||
### Границы компонентов
|
`xray` значит: нужен Xray, запущенный через `proxy-client start`. Если его нет,
|
||||||
|
агент не запускается. `external` значит: проверяется только доступность адреса
|
||||||
|
и порта. Ни один тип не переходит на другой сам — предсказуемость канала важнее
|
||||||
|
удобства.
|
||||||
|
|
||||||
`proxy-client` — один исполняемый файл на Python 3.8+ без внешних библиотек. Он
|
`start` работает только при `type=xray` и loopback-адресе в конфиге. Поэтому
|
||||||
отвечает за установку Xray, запуск и остановку процесса, диагностику,
|
нельзя поднять локальный Xray, оставив `pclaude` направленным во внешний
|
||||||
преобразование VLESS и запуск произвольной дочерней команды через прокси.
|
Hiddify. `stop` от типа не зависит: после переключения на `external` старый
|
||||||
Нейтральное публичное имя описывает клиентскую прокси-подсистему целиком:
|
Xray всё равно можно остановить.
|
||||||
внешний прокси и управляемый Xray. Название `xray` остаётся у конкретного
|
|
||||||
движка и значения `type = xray`, где оно действительно важно.
|
|
||||||
|
|
||||||
Переносимый `shell.bash` — единственный владелец `proxy_on`, `proxy_off`,
|
Один порт на HTTP и SOCKS, по умолчанию `12334`; порт настраивается, чтобы
|
||||||
`pclaude`, `pcodex`, `myip` и внутренних proxy-помощников. При реализации они
|
разные пользователи одной машины не мешали друг другу.
|
||||||
удаляются из `bash/.bash_functions`, чтобы результат не зависел от порядка
|
|
||||||
`source`. `update_hproxy` остаётся там как отдельная WSL-утилита управления
|
|
||||||
`/etc/hosts`, а не часть переносимого интерфейса прокси. Модуль не разбирает
|
|
||||||
машинный конфиг самостоятельно:
|
|
||||||
|
|
||||||
- `pclaude` и `pcodex` делегируют запуск форме
|
### VLESS-ссылки
|
||||||
`proxy-client run -- COMMAND ARG...`, где `--` отделяет параметры клиента;
|
|
||||||
- `proxy_on` получает от `proxy-client env` проверенный набор proxy-переменных и
|
|
||||||
применяет его к текущей Bash-сессии;
|
|
||||||
- `proxy_off` очищает верхний и нижний регистр proxy-переменных.
|
|
||||||
|
|
||||||
Общий `bash/.bash_functions` и модуль клиента не зависят друг от друга. В составе
|
Поддержаны две семьи: TCP/RAW с REALITY и XHTTP с TLS. Неизвестный или
|
||||||
dotfiles `.bash_custom` подключает их двумя независимыми строками. Без
|
повторный query-параметр — отказ: он может менять смысл соединения, и его
|
||||||
остальных dotfiles пользователь добавляет только один `source` модуля клиента в
|
нужно добавить в разбор явно. Остальное проверяет `xray run -test`.
|
||||||
`.bashrc`. Для неинтерактивных сценариев и других оболочек остаётся прямой
|
|
||||||
`proxy-client run`.
|
|
||||||
|
|
||||||
`proxy-client env` сначала выполняет ту же проверку доступности, что и `run`, а
|
Разбор следует [спецификации ссылок Xray](https://github.com/XTLS/Xray-core/discussions/716):
|
||||||
затем печатает восемь строк `KEY=value`: HTTP, HTTPS, ALL и NO_PROXY в верхнем
|
поддерживает `ech`, `pqv`, `spx`, `fm`, необязательные поля и значения по
|
||||||
и нижнем регистре. `shell.bash` сначала целиком получает успешный ответ, затем
|
умолчанию. Дополнительные параметры XHTTP переносятся внутрь `extra`, чтобы
|
||||||
разбирает строки по первому `=`, принимает только известные ключи и лишь после
|
ядро их не потеряло. Старые `sessionPlacement`/`sessionKey` преобразуются в
|
||||||
полной проверки экспортирует полученные значения через `printf -v`. `eval` не
|
`sessionIDPlacement`/`sessionIDKey`, в том числе для отдельного скачивания;
|
||||||
используется, частичного изменения окружения при ошибке нет.
|
противоречивые значения отклоняются. Полный список — в README пакета.
|
||||||
|
|
||||||
У `env` stdout зарезервирован только для восьми машинно-читаемых строк.
|
Hiddify Manager добавляет `allowInsecure`/`insecure` даже к профилям с
|
||||||
Предупреждения и ошибки идут в stderr; обычная безопасная сводка этой командой
|
нормальным сертификатом
|
||||||
не печатается.
|
([Hiddify-Manager#5438](https://github.com/hiddify/Hiddify-Manager/issues/5438)),
|
||||||
|
а Xray [26.2.6](https://github.com/XTLS/Xray-core/releases/tag/v26.2.6) эту
|
||||||
|
опцию удалил. Поэтому поля принимаются и игнорируются с предупреждением;
|
||||||
|
TLS-проверку задают `pcs` и `vcn`.
|
||||||
|
|
||||||
Совместимость существующих Bash-команд означает сохранение имён функций,
|
Сводка перед запуском не показывает UUID, ключи REALITY и содержимое XHTTP
|
||||||
набора переменных, fail-fast перед `pclaude`/`pcodex` и изоляции окружения
|
`path` (там бывает токен). Ссылка не передаётся аргументом командной строки.
|
||||||
дочернего процесса. Автоматический запрос внешнего IP из `proxy_on` и
|
|
||||||
`proxy_off` удаляется: он замедляет переключение и смешивает изменение
|
|
||||||
окружения с диагностикой. Для проверки остаются `myip` и `proxy-client status`.
|
|
||||||
|
|
||||||
### Два независимых файла настройки
|
|
||||||
|
|
||||||
Машинный файл `~/.config/proxy-client/config` хранит несекретное описание
|
|
||||||
активного прокси: явный тип `xray` или `external`, отображаемое имя, адрес, порт
|
|
||||||
и добавку к `NO_PROXY`. Формат — INI из стандартной библиотеки Python. Если
|
|
||||||
файла нет, прокси-подсистема сохраняет прежний внешний адрес
|
|
||||||
`hproxy.local:12334` ради обратной совместимости.
|
|
||||||
|
|
||||||
Секретный `~/.config/proxy-client/default.url` содержит одну VLESS-ссылку.
|
|
||||||
Команда запуска принимает другой файл через `-f`. Профили можно хранить в
|
|
||||||
нескольких файлах, но активным у пользователя бывает только один.
|
|
||||||
CLI следует за симлинком, но проверяет права конечного файла: доступ группы или
|
|
||||||
остальных пользователей считается ошибкой с подсказкой выполнить `chmod 600`.
|
|
||||||
Файл должен содержать ровно одну непустую строку без комментариев и служебного
|
|
||||||
текста.
|
|
||||||
|
|
||||||
Базовый `NO_PROXY` всегда включает loopback-адреса и узел самого прокси.
|
|
||||||
Машинная добавка расширяет его локальными IP и доменами. Документация должна
|
|
||||||
предупреждать, что поддержка CIDR в `NO_PROXY` различается между программами.
|
|
||||||
|
|
||||||
### Явная семантика прокси
|
|
||||||
|
|
||||||
Тип `xray` означает, что `pclaude` и `pcodex` требуют запущенный процесс,
|
|
||||||
которым управляет `proxy-client`. Если его нет, агент не запускается, а сообщение
|
|
||||||
предлагает выполнить `proxy-client start`.
|
|
||||||
|
|
||||||
Тип `external` означает внешний процесс, например Hiddify. Проверяется только
|
|
||||||
доступность явно заданного адреса и порта. Ни один тип не переходит на другой
|
|
||||||
автоматически.
|
|
||||||
|
|
||||||
`start` разрешён только при `type=xray`; при `type=external` он завершается с
|
|
||||||
подсказкой изменить машинный конфиг. Для Xray адрес машинного конфига обязан
|
|
||||||
быть loopback-адресом, а порт обязан совпадать с создаваемым входом. Так нельзя
|
|
||||||
случайно поднять локальный Xray, оставив `pclaude` направленным во внешний
|
|
||||||
Hiddify.
|
|
||||||
|
|
||||||
`status` описывает именно активный тип из машинного конфига: состояние процесса
|
|
||||||
и порта для Xray либо доступность адреса для external. `stop` от машинного типа
|
|
||||||
не зависит и завершает записанный управляемый Xray, если он существует. Поэтому
|
|
||||||
после явного переключения на external старый локальный процесс можно штатно
|
|
||||||
остановить.
|
|
||||||
|
|
||||||
HTTP, HTTPS и SOCKS используют один локальный порт, по умолчанию `12334`.
|
|
||||||
Порт остаётся настраиваемым, чтобы разные Unix-пользователи одной машины могли
|
|
||||||
запускать свои экземпляры без конфликта.
|
|
||||||
|
|
||||||
Локальный вход Xray слушает только loopback: `127.0.0.1` и, если поддержано
|
|
||||||
выбранной конфигурацией, `::1`. Он никогда не привязывается к `0.0.0.0`, адресу
|
|
||||||
сетевого интерфейса или имени узла. Это закрывает вход от удалённой сети, но не
|
|
||||||
изолирует его от других пользователей того же Linux-узла. Первая версия не
|
|
||||||
добавляет локальную аутентификацию и предполагает доверенных пользователей
|
|
||||||
машины.
|
|
||||||
|
|
||||||
### Поддерживаемые VLESS-ссылки
|
|
||||||
|
|
||||||
Первая версия поддерживает две семьи:
|
|
||||||
|
|
||||||
- VLESS с TCP/RAW и REALITY;
|
|
||||||
- VLESS с XHTTP и TLS, включая HTTP/1.1, h2 и h3 через ALPN.
|
|
||||||
|
|
||||||
Преобразователь сохраняет значимые параметры ссылки, проверяет обязательные
|
|
||||||
поля и отклоняет неизвестное сочетание целиком. Он не должен молча пропускать
|
|
||||||
неизвестные или несовместимые параметры. Поддержка REALITY опирается на
|
|
||||||
[официальную схему REALITY](https://xtls.github.io/en/config/transports/reality.html),
|
|
||||||
а XHTTP с TLS — на
|
|
||||||
[схему TLS](https://xtls.github.io/en/config/transports/tls.html).
|
|
||||||
|
|
||||||
Для REALITY преобразователь явно сопоставляет UUID с VLESS `id`, узел и порт с
|
|
||||||
адресом назначения, `type=tcp` с RAW, `flow` с режимом VLESS Vision, `sni` с
|
|
||||||
`serverName`, `fp` с `fingerprint`, `pbk` с `password`, а `sid` с `shortId`.
|
|
||||||
`headerType=none` принимается как нейтральное значение, другое значение
|
|
||||||
отклоняется. `hiddify`, `host` и `alpn` для TCP/REALITY считаются метаданными и
|
|
||||||
не меняют Xray JSON; безопасная сводка явно помечает `host` и `alpn` как
|
|
||||||
неиспользованные. Это не скрытая потеря параметра: в целевой
|
|
||||||
[схеме REALITY Xray 26.3.27](https://github.com/XTLS/Xray-core/blob/v26.3.27/infra/conf/transport_internet.go#L3591-L3636)
|
|
||||||
нет полей Host или ALPN. Повтор одного query-параметра считается ошибкой.
|
|
||||||
Остальные правила соответствуют
|
|
||||||
[стандарту VLESS-ссылок Xray](https://github.com/XTLS/Xray-core/discussions/716).
|
|
||||||
|
|
||||||
Основной сценарий XHTTP/TLS — одиночная ссылка Hiddify с `type=xhttp`,
|
|
||||||
`security=tls`, `sni`, `alpn`, `path`, `host`, `encryption`, `fp` и, возможно,
|
|
||||||
`extra`. UUID, узел и порт преобразуются так же, как для REALITY. Остальные
|
|
||||||
поля сопоставляются явно:
|
|
||||||
|
|
||||||
- `type=xhttp` задаёт транспорт XHTTP;
|
|
||||||
- `security=tls` включает TLS;
|
|
||||||
- `sni`, `alpn` и `fp` задают `serverName`, список ALPN и fingerprint;
|
|
||||||
- `path`, `host` и необязательный `mode` переходят в настройки XHTTP;
|
|
||||||
- `encryption` задаёт шифрование VLESS и в первой версии должно быть `none`;
|
|
||||||
- `extra` URL-декодируется, разбирается как JSON-объект без повторяющихся
|
|
||||||
ключей и целиком передаётся в одноимённую секцию XHTTP;
|
|
||||||
- `pcs` и `vcn` для Xray 26.3.27 переходят в `pinnedPeerCertSha256` и
|
|
||||||
`verifyPeerCertByName`;
|
|
||||||
- `hiddify=1` и `core=xray` принимаются как метаданные источника и не меняют
|
|
||||||
JSON;
|
|
||||||
- `headerType=none` принимается как устаревшее нейтральное поле Hiddify, другое
|
|
||||||
значение отклоняется;
|
|
||||||
- fragment после `#` используется только как отображаемое имя профиля.
|
|
||||||
|
|
||||||
Если `mode` отсутствует, преобразователь не придумывает его и оставляет выбор
|
|
||||||
Xray. Параметры query не зависят от порядка, но не могут повторяться. После
|
|
||||||
перечисленных полей неизвестный параметр считается ошибкой: он может менять
|
|
||||||
смысл соединения и требует явного добавления в преобразователь. `path`, `host`,
|
|
||||||
`mode` и `extra` входят в официальный стандарт ссылок Xray; параметры TLS
|
|
||||||
`pcs`/`vcn` заменяют удалённый `allowInsecure`:
|
|
||||||
[стандарт VLESS-ссылок](https://github.com/XTLS/Xray-core/discussions/716).
|
|
||||||
|
|
||||||
Перед запуском CLI печатает компактную безопасную сводку: сервер, порт,
|
|
||||||
транспорт, защиту, SNI, ALPN, Host, наличие и длину пути, fingerprint и локальный
|
|
||||||
порт. XHTTP `path` может содержать tenant ID или bearer-подобный токен, поэтому
|
|
||||||
его содержимое никогда не печатается. Обычный режим не выводит подробный журнал
|
|
||||||
Xray. UUID, REALITY password/public key, short ID и другие чувствительные
|
|
||||||
значения обозначаются без фрагментов исходного значения.
|
|
||||||
В создаваемом JSON журнал доступа отключён, а уровень ошибок по умолчанию —
|
|
||||||
`warning`.
|
|
||||||
|
|
||||||
Hiddify Manager может добавлять `allowInsecure=true` и `insecure=true` даже к
|
|
||||||
профилю с действительным публичным сертификатом — этот дефект описан в
|
|
||||||
[hiddify/Hiddify-Manager#5438](https://github.com/hiddify/Hiddify-Manager/issues/5438).
|
|
||||||
Современный Xray больше не поддерживает `allowInsecure`; удаление зафиксировано
|
|
||||||
в [релизе 26.2.6](https://github.com/XTLS/Xray-core/releases/tag/v26.2.6).
|
|
||||||
Преобразователь принимает для обоих полей только точные строчные значения
|
|
||||||
`true` и `false`. Если присутствуют оба поля, их значения должны совпадать;
|
|
||||||
конфликт и любое другое написание отклоняются до создания JSON. Для Xray
|
|
||||||
26.3.27 поля не переносятся в JSON; истинное значение даёт предупреждение, что
|
|
||||||
сертификат будет проверен обычным способом, а ложное не требует предупреждения.
|
|
||||||
Соединение после этого может успешно заработать или завершиться ошибкой
|
|
||||||
сертификата; отдельного раннего отказа нет.
|
|
||||||
|
|
||||||
### Установка Xray
|
### Установка Xray
|
||||||
|
|
||||||
`proxy-client install` — отдельная явная операция. Она устанавливает Xray в
|
`proxy-client install` ставит в `~/.local/bin/xray` официальный архив одного
|
||||||
`~/.local/bin`, скачивает официальный архив и файл контрольной суммы, затем
|
зафиксированного релиза (сейчас 26.10.10), проверяет SHA-256 по `.dgst` и
|
||||||
проверяет SHA-256. Поддерживается только зафиксированный релиз 26.3.27;
|
заменяет файл атомарно. Официальный установщик Xray не подходит: он требует
|
||||||
параметра версии нет. Официальные релизы публикуют необходимые файлы `.dgst`:
|
root и создаёт systemd-службы. `start` принимает только ту же версию и никогда
|
||||||
[релиз 26.3.27](https://github.com/XTLS/Xray-core/releases/tag/v26.3.27).
|
ничего не скачивает.
|
||||||
|
|
||||||
Для Linux `x86_64`/`amd64` используется `Xray-linux-64.zip`, для
|
|
||||||
`aarch64`/`arm64` — `Xray-linux-arm64-v8a.zip`; остальные архитектуры
|
|
||||||
отклоняются. Официальный `.dgst` содержит записи `MD5`, `SHA1`, `SHA2-256` и
|
|
||||||
`SHA2-512`; принимается ровно один полный набор со строчными hex-значениями,
|
|
||||||
для проверки архива используется `SHA2-256`. Дубликаты, uppercase, отсутствие
|
|
||||||
записи и мусор отклоняются. Архив и извлечённый бинарник ограничены 64 MiB,
|
|
||||||
`.dgst` — 4 KiB; фактический размер считается с limit+1 и должен совпадать с
|
|
||||||
объявленным `Content-Length`. Архив распаковывается в закрытый staging-каталог
|
|
||||||
внутри `~/.local/bin`, проверенный бинарник запускается с `version`, после чего
|
|
||||||
одним `os.replace` атомарно переносится в `~/.local/bin/xray`.
|
|
||||||
|
|
||||||
Корректная существующая версия 26.3.27 даёт no-op без сети. Другой файл или
|
|
||||||
симлинк не заменяется без `--replace`, а каталог не заменяется никогда.
|
|
||||||
Start-lock берётся до проверки назначения и удерживается до завершения
|
|
||||||
`os.replace`; живой, сиротский или неподтверждённый managed-state запрещает
|
|
||||||
установку с подсказкой сначала выполнить `stop`.
|
|
||||||
После установки CLI проверяет, что первый `xray` из `PATH` разрешается в
|
|
||||||
установленный файл; иначе завершается с подсказкой поставить `~/.local/bin` в
|
|
||||||
начало `PATH`, не откатывая готовую установку. `start` всегда показывает
|
|
||||||
фактический путь и версию выбранного бинарника.
|
|
||||||
|
|
||||||
Перед созданием JSON CLI разрешает первый `xray` из `PATH` до одного пути и
|
|
||||||
запускает этим файлом `xray version`. Первая строка должна начинаться с точного
|
|
||||||
токена `Xray VERSION`, после полного токена версии допускается только конец
|
|
||||||
строки либо ASCII-пробел. Принимается только версия 26.3.27; ведущий
|
|
||||||
мусор, похожий более длинный токен, пустой, неверный, завершившийся ошибкой или
|
|
||||||
содержащий другую версию вывод запрещает запуск. Тот же разрешённый файл
|
|
||||||
используется для `run -test` и основного процесса. Версия никогда не выводится
|
|
||||||
из содержимого VLESS-ссылки.
|
|
||||||
|
|
||||||
`start` никогда не скачивает бинарник. Если `xray` отсутствует в `PATH`, команда
|
|
||||||
завершается с подсказкой выполнить `proxy-client install`.
|
|
||||||
|
|
||||||
### Жизненный цикл
|
### Жизненный цикл
|
||||||
|
|
||||||
CLI предоставляет `install`, `start`, `stop`, `status`, `env` и `run`. Запуск
|
`start` остаётся на переднем плане как небольшой Python-супервизор с одним
|
||||||
без подкоманды не устанавливает и не стартует ничего.
|
дочерним Xray; ожидаемое место — панель tmux. Отдельный журнал и команда `logs`
|
||||||
|
не нужны: хватает истории tmux. Второй `start` отсекает блокировка `flock`.
|
||||||
|
`state.json` хранит PID супервизора и Xray, чтобы `stop` работал из любого
|
||||||
|
терминала и мог добить Xray, если супервизор убит.
|
||||||
|
|
||||||
`start` остаётся на переднем плане как небольшой Python-супервизор и занимает
|
Перед запуском обязательны разбор ссылки, `xray run -test` и открытый порт
|
||||||
текущую консоль. Он запускает один дочерний Xray без демонизации и без новой
|
после старта. Проверка внешнего IP — только диагностика: её сбой даёт
|
||||||
сессии процесса, ждёт его завершения и передаёт ему SIGINT, SIGTERM и SIGHUP.
|
предупреждение.
|
||||||
Ожидаемый способ работы — отдельная панель tmux или другого мультиплексора.
|
|
||||||
Отсоединение от tmux не останавливает процесс, но закрытие панели или сессии
|
|
||||||
останавливает Xray. Отдельный постоянный журнал и команда `logs` не нужны:
|
|
||||||
достаточно истории tmux.
|
|
||||||
|
|
||||||
Супервизор на всё время работы удерживает неблокирующую эксклюзивную блокировку
|
`run` никогда не запускает Xray, поэтому у `pclaude` и `pcodex` нет скрытого
|
||||||
`fcntl.flock` в служебном каталоге `~/.local/state/proxy-client/`. Второй `start`
|
автозапуска.
|
||||||
отказывает до запуска процессов и изменения state или сгенерированного
|
|
||||||
конфига. `state.json` нужен для восстановления после сбоя, но не служит
|
|
||||||
блокировкой.
|
|
||||||
|
|
||||||
Файл состояния содержит ровно `supervisor_pid`, `xray_pid`, `xray_path`,
|
|
||||||
`config_path`, `address`, `port`, `supervisor_path`, `supervisor_starttime`,
|
|
||||||
`xray_starttime` и `boot_id`. Время старта — поле 22 `/proc/PID/stat`; поле
|
|
||||||
`comm` сначала отделяется по последней `)`. Для обоих процессов проверяются PID,
|
|
||||||
точное время старта и `boot_id`. У ребёнка дополнительно сверяется канонический
|
|
||||||
`/proc/PID/exe` с `xray_path`. У Python-супервизора операнд скрипта из
|
|
||||||
`/proc/PID/cmdline` разрешается относительно `/proc/PID/cwd` и сравнивается с
|
|
||||||
каноническим `supervisor_path`. Ошибка чтения или разбора означает несовпадение.
|
|
||||||
Непосредственно перед каждым сигналом идентичность читается повторно.
|
|
||||||
|
|
||||||
Проверка состояния различает три исхода. Для здоровой пары `stop` посылает
|
|
||||||
SIGTERM супервизору. Если супервизор исчез, но ребёнок подтверждён, `status`
|
|
||||||
сообщает о сироте с ненулевым кодом, `env` и `run` отказывают, а `stop` посылает
|
|
||||||
сигнал ребёнку напрямую без требования доступности порта и удаляет state и
|
|
||||||
конфиг. При живом, но неподтверждённом PID сигналы не посылаются, файлы
|
|
||||||
сохраняются, а диагностика требует ручной проверки и удаления. Если оба PID
|
|
||||||
мертвы или `boot_id` не совпадает, устаревшие state и конфиг можно безопасно
|
|
||||||
удалить без сигналов.
|
|
||||||
|
|
||||||
У пользователя может быть только один управляемый экземпляр. Повторный `start`
|
|
||||||
не заменяет его другим профилем. Для смены профиля нужны явные `stop` и `start`.
|
|
||||||
|
|
||||||
Перед успешным запуском должны пройти разбор ссылки, проверка поддерживаемого
|
|
||||||
сочетания и `xray run -test`. После запуска процесс должен оставаться живым, а
|
|
||||||
локальный порт — принимать соединения. Проверка внешнего IP через прокси носит
|
|
||||||
диагностический характер: её сбой даёт предупреждение, но не останавливает уже
|
|
||||||
прошедший обязательные проверки Xray.
|
|
||||||
|
|
||||||
Сгенерированный JSON и временные файлы создаются внутри служебного каталога и
|
|
||||||
удаляются супервизором после завершения Xray. Каталоги создаются с правами
|
|
||||||
`0700`, файлы с конфигурацией и состоянием — `0600`. Исходная VLESS-ссылка не
|
|
||||||
попадает в stdout или stderr при успехе, ошибках разбора и сбое `run -test` и
|
|
||||||
никогда не передаётся аргументом командной строки. Те же поверхности не содержат
|
|
||||||
UUID, ключи, short ID и другие уникальные секретные маркеры профиля.
|
|
||||||
|
|
||||||
`run` никогда не запускает Xray. Для типа `xray` он проверяет PID управляемого
|
|
||||||
процесса и локальный порт; для типа `external` — только явно настроенный адрес и
|
|
||||||
порт. Лишь после успешной проверки он выставляет proxy-переменные для дочерней
|
|
||||||
команды. Поэтому `pclaude` и `pcodex` не имеют скрытого автозапуска.
|
|
||||||
|
|
||||||
### Доставка
|
### Доставка
|
||||||
|
|
||||||
Компоненты образуют отдельный stow-совместимый каталог `proxy-client/`. Он
|
Каталог `proxy-client/` раскладывается в `$HOME` ссылками: CLI в
|
||||||
доставляет CLI, Bash-модуль и три шаблона: машинного INI для внешнего прокси,
|
`~/.local/bin/`, `shell.bash` и `examples/` в `~/.config/proxy-client/`.
|
||||||
машинного INI для Xray и файла VLESS-ссылки.
|
Машинный `config` и `default.url` пользователь создаёт сам; в Git они не
|
||||||
Реальные пользовательские файлы не управляются stow и не входят в Git.
|
входят.
|
||||||
CLI раскладывается как `~/.local/bin/proxy-client`. Машинный конфиг, профиль,
|
|
||||||
модуль `shell.bash` и каталог `examples/` находятся вместе в
|
|
||||||
`~/.config/proxy-client/`. Шаблоны не подменяют настоящие пользовательские
|
|
||||||
файлы.
|
|
||||||
|
|
||||||
В корне этого каталога находятся самостоятельный `README.md` и пакетный
|
- Отдельно: `proxy-client/install.sh` ставит эти три ссылки и по умолчанию
|
||||||
`install.sh`. Оба файла относятся к исходному каталогу и исключаются из
|
дописывает блок в `~/.bashrc`. Чужие файлы не трогает. Режим прокси, Xray и
|
||||||
раскладки stow. README остаётся рядом с кодом при выборочном клонировании или
|
сеть не настраивает — это делают отдельные команды по README.
|
||||||
копировании каталога. Корневой README репозитория только кратко описывает
|
- В составе dotfiles: корневой `install.sh` подключает тот же пакет через Stow,
|
||||||
подсистему и ссылается на него.
|
а `.bash_custom` загружает `shell.bash`.
|
||||||
|
|
||||||
Пакетный установщик связывает отдельно скопированный каталог с домашним
|
## Риски
|
||||||
каталогом и не зависит от положения или наличия остальных dotfiles. Если stow
|
|
||||||
доступен, установщик использует его; если нет — создаёт тот же набор симлинков
|
|
||||||
напрямую. Выбранный механизм явно показывается пользователю, но не меняет
|
|
||||||
результат. Исходный каталог должен оставаться на прежнем пути всё время
|
|
||||||
использования ссылок.
|
|
||||||
|
|
||||||
Установщик управляет только тремя публичными артефактами: CLI, `shell.bash` и
|
- Формат ссылок Hiddify расширяется. Строгий отказ на неизвестном параметре
|
||||||
каталогом шаблонов. Машинный `config` и закрытый `default.url` не входят в
|
лучше частично неверного JSON, но новые ссылки могут потребовать правки
|
||||||
раскладку stow, никогда не создаются, не заменяются и не связываются
|
разбора.
|
||||||
установщиком. Их наличие в исходном каталоге также не должно расширять набор
|
- Открытый порт не доказывает, что маршрут работает. Проверка внешнего IP
|
||||||
управляемых путей.
|
отделена от обязательных проверок.
|
||||||
|
- Loopback без аутентификации открыт другим пользователям того же узла.
|
||||||
Домашние `~/.local/bin` и `~/.config/proxy-client` остаются настоящими
|
Пользователи машины считаются доверенными.
|
||||||
каталогами. Stow не должен сворачивать их или их родителей в одну ссылку на
|
- `NO_PROXY` программы понимают по-разному, особенно CIDR. README советует
|
||||||
исходное дерево: ссылками становятся только три перечисленных артефакта. Это
|
точные IP и домены.
|
||||||
одновременно сохраняет место для машинных файлов и делает результат stow
|
- Ссылки установки зависят от пути исходного каталога: переносить его нельзя.
|
||||||
совпадающим с прямой раскладкой через `ln -s`.
|
- Поддержка РЕД ОС, Astra Linux и Alt Linux заявляется только после проверки
|
||||||
|
версии Python и прогона `tests/test_proxy_client_full.sh` на них.
|
||||||
Перед записью установщик проверяет весь набор назначений. Правильная ссылка на
|
- Шаблоны в `examples/` содержат только вымышленные значения; настоящие ссылки,
|
||||||
текущий исходный каталог считается уже установленной; любой другой файл,
|
UUID и ключи в репозиторий не попадают.
|
||||||
каталог или симлинк считается конфликтом. При конфликте установщик ничего не
|
|
||||||
меняет и сообщает все пути, которые пользователь должен разобрать вручную.
|
|
||||||
Принудительной замены и автоматических резервных копий нет.
|
|
||||||
|
|
||||||
По умолчанию установщик идемпотентно подключает Bash-модуль в `~/.bashrc` и при
|
|
||||||
необходимости добавляет `~/.local/bin` в `PATH`. Уже существующее подключение
|
|
||||||
через dotfiles не дублируется. Управляемый блок помечается, повторно не
|
|
||||||
добавляется и может быть полностью отключён параметром `--no-shell-setup`.
|
|
||||||
|
|
||||||
Установщик не выбирает режим прокси, не создаёт конфигурацию, не читает VLESS,
|
|
||||||
не устанавливает Xray и не проверяет сеть. После раскладки он проверяет только
|
|
||||||
локальные требования и исправность CLI и Bash-модуля, затем печатает краткую
|
|
||||||
подсказку для текущей shell-сессии и направляет пользователя в README за
|
|
||||||
настройкой режима. Недоступный прокси не делает установку неуспешной.
|
|
||||||
|
|
||||||
Полный корневой `install.sh` продолжает подключать тот же stow-пакет вместе с
|
|
||||||
остальными dotfiles и не получает отдельной реализации этой логики.
|
|
||||||
|
|
||||||
## Решения и отклонённые альтернативы
|
|
||||||
|
|
||||||
- Выбран Python 3.8+, потому что стандартной библиотеки достаточно для URL,
|
|
||||||
JSON, INI, ZIP, SHA-256, HTTP, процессов и тестов. Более новый синтаксис и
|
|
||||||
сторонние пакеты запрещены ради старых и непредсказуемых серверных систем.
|
|
||||||
- Выбран INI, а не TOML: модуль
|
|
||||||
[`tomllib`](https://docs.python.org/3/library/tomllib.html) добавлен только в
|
|
||||||
Python 3.11, что исключило бы штатный Python 3.10 в Ubuntu 22.04.
|
|
||||||
- Bash оставлен отдельным тонким модулем, потому что дочерний Python-процесс не
|
|
||||||
может изменить окружение родительской shell-сессии.
|
|
||||||
- Выбрано публичное имя `proxy-client`, потому что подсистема обслуживает и
|
|
||||||
внешний прокси, и управляемый Xray. `xray-proxy` отклонено как имя,
|
|
||||||
выпячивающее одну реализацию, а `proxyctl` — как менее понятное пользователю.
|
|
||||||
- Все пользовательские файлы собраны в `~/.config/proxy-client/`. Разделение
|
|
||||||
между общим машинным конфигом и Xray-профилем остаётся логическим, но не
|
|
||||||
требует помнить два каталога.
|
|
||||||
- Машинный прокси и VLESS-ссылка разделены: первый файл описывает локальную
|
|
||||||
политику shell, второй содержит секрет и меняется независимо.
|
|
||||||
- Выбран один порт, потому что SOCKS-вход Xray совместим с HTTP и не требует
|
|
||||||
второго локального слушателя.
|
|
||||||
- Выбран явный тип прокси вместо автообнаружения. Предсказуемость канала важнее
|
|
||||||
удобства автоматического перехода.
|
|
||||||
- Выбрана одна активная конфигурация на пользователя. Именованные процессы и
|
|
||||||
автоматическое распределение портов отложены до появления реальной нужды.
|
|
||||||
- Официальный системный установщик отклонён: он требует root и создаёт
|
|
||||||
systemd-службы. Пользовательская установка соответствует выбранной модели.
|
|
||||||
- Копирование файлов пакета отклонено: оно создаёт отдельные экземпляры,
|
|
||||||
усложняет обновление и нарушает общую с dotfiles модель симлинков.
|
|
||||||
- Обязательный stow отклонён как лишняя зависимость. Прямые симлинки дают тот же
|
|
||||||
результат на машине без stow, а автоматический выбор механизма не меняет
|
|
||||||
публичное поведение пакета.
|
|
||||||
- Автоматическая настройка режима, установка Xray и проверка сети отклонены для
|
|
||||||
установщика пакета. Эти операции зависят от машины, могут требовать секретов
|
|
||||||
или сети и уже имеют отдельные явные команды и инструкции.
|
|
||||||
- Готовые JSON и полный импорт Hiddify отклонены как основной интерфейс: точечная
|
|
||||||
VLESS-ссылка проще для пользователя и ограничивает область преобразования.
|
|
||||||
|
|
||||||
## Риски и меры
|
|
||||||
|
|
||||||
- Формат ссылок Hiddify может расширяться. Строгая проверка и явный отказ
|
|
||||||
безопаснее частично неверного JSON.
|
|
||||||
- Проверка открытого порта не доказывает работоспособность маршрута. CLI
|
|
||||||
разделяет обязательную проверку процесса и необязательную внешнюю диагностику.
|
|
||||||
- VLESS-ссылка содержит чувствительные данные. CLI сокращает секреты в выводе,
|
|
||||||
не журналирует их и использует закрытые пользовательские файлы.
|
|
||||||
- Устаревшие Hiddify-поля `allowInsecure` и `insecure` неоднозначны. Они не
|
|
||||||
переносятся в Xray 26.3.27, сертификат проверяется обычным способом, а
|
|
||||||
истинное значение сопровождается предупреждением.
|
|
||||||
- Старые дистрибутивы могут иметь Python ниже 3.8. CLI проверяет версию до
|
|
||||||
основной работы и сообщает обнаруженную и требуемую версии.
|
|
||||||
- Симлинки зависят от постоянного пути исходного каталога. Установщик явно
|
|
||||||
сообщает эту зависимость и при повторном запуске принимает только ссылки на
|
|
||||||
текущий источник.
|
|
||||||
- Изменение `~/.bashrc` может конфликтовать с пользовательским способом загрузки
|
|
||||||
оболочки. Установщик распознаёт собственный блок и известное подключение из
|
|
||||||
dotfiles, не дублирует их и предоставляет `--no-shell-setup`.
|
|
||||||
- Частичная установка может оставить неоднозначное состояние. Все конфликты
|
|
||||||
назначений проверяются до изменений; проверки охватывают отсутствие новых
|
|
||||||
ссылок и правок shell при отказе.
|
|
||||||
- `NO_PROXY` реализуется программами неодинаково. README рекомендует точные IP
|
|
||||||
и домены, а CIDR описывает как совместимость best-effort.
|
|
||||||
- Разные пользователи делят сетевое пространство имён. Настраиваемый порт и
|
|
||||||
явная ошибка конфликта предотвращают случайный запуск на занятом порту, но
|
|
||||||
loopback без аутентификации не запрещает намеренное подключение другого
|
|
||||||
локального пользователя. Это принятое ограничение первой версии.
|
|
||||||
|
|
||||||
## Проверка реализации
|
|
||||||
|
|
||||||
- Модульные тесты разбирают обезличенные ссылки обеих поддерживаемых семей и
|
|
||||||
все показанные варианты ALPN. Отдельный регрессионный пример повторяет форму
|
|
||||||
типичной ссылки Hiddify XHTTP/TLS с `hiddify`, `core`, `extra`,
|
|
||||||
`headerType=none`, `allowInsecure` и `insecure`.
|
|
||||||
- Неподдерживаемые транспорт, защита, отсутствующие обязательные поля и любой
|
|
||||||
неизвестный query-параметр дают понятный отказ до запуска Xray.
|
|
||||||
- Сгенерированный JSON проходит `xray run -test` на Xray 26.3.27 для заявленных
|
|
||||||
сочетаний. Интеграционная проверка подтверждает, что один локальный вход
|
|
||||||
принимает HTTP и SOCKS.
|
|
||||||
- Тесты отдельно проверяют удаление устаревших insecure-полей.
|
|
||||||
- Тесты подтверждают сокращение секретов и отсутствие полной VLESS-ссылки в
|
|
||||||
stdout, stderr, служебных файлах и аргументах дочернего процесса.
|
|
||||||
- Установка проверяется на корректной и повреждённой SHA-256 без записи вне
|
|
||||||
пользовательского каталога.
|
|
||||||
- Управление процессом покрывает повторный запуск, устаревший PID, чужой PID,
|
|
||||||
повторно использованный PID, передачу сигналов, аварийно исчезнувший
|
|
||||||
супервизор, очистку файлов, смену профиля, остановку и конфликт порта.
|
|
||||||
- Сбой внешней проверки IP не отменяет успешный запуск, но отображается отдельно
|
|
||||||
от состояния процесса и порта.
|
|
||||||
- `run` сохраняет аргументы и код возврата, не оставляет proxy-переменные в
|
|
||||||
родительской Bash-сессии и не запускает команду при недоступном прокси.
|
|
||||||
- `env` не печатает частичный результат при ошибке, а `shell.bash` отклоняет
|
|
||||||
неизвестные строки без `eval` и без частичного экспорта.
|
|
||||||
- Регрессионные Bash-тесты подтверждают прежнее поведение Hiddify при отсутствии
|
|
||||||
машинного конфига, оба регистра proxy-переменных, настраиваемый `NO_PROXY` и
|
|
||||||
отсутствие автоматического запроса внешнего IP в `proxy_on`/`proxy_off`.
|
|
||||||
- Проверки самостоятельной установки покрывают путь со stow и без него,
|
|
||||||
одинаковый набор итоговых ссылок, запуск не из каталога пакета и каталог с
|
|
||||||
пробелами в пути.
|
|
||||||
- В обоих путях домашние каталоги остаются обычными каталогами, а пакетные
|
|
||||||
`README.md` и `install.sh` не появляются в домашнем каталоге через stow.
|
|
||||||
- Повторный запуск не меняет уже правильную установку. Конфликт любого
|
|
||||||
назначения обнаруживается до записи; чужие файлы, каталоги и ссылки остаются
|
|
||||||
неизменными.
|
|
||||||
- Машинный `config` и закрытый `default.url` не управляются установщиком даже
|
|
||||||
при их наличии в исходном каталоге или домашнем каталоге.
|
|
||||||
- Настройка Bash проверяется для нового `~/.bashrc`, уже помеченного блока,
|
|
||||||
подключения из полного dotfiles и режима `--no-shell-setup`. Добавление
|
|
||||||
`~/.local/bin` не дублируется.
|
|
||||||
- Локальная проверка установки не обращается к сети и не требует доступного
|
|
||||||
прокси. Неисправный или отсутствующий машинный конфиг не отменяет успешную
|
|
||||||
раскладку пакета.
|
|
||||||
- Ручная проверка выполняется минимум на Ubuntu с Python 3.10 и на одной целевой
|
|
||||||
удалённой системе. РЕД ОС, Astra Linux и Alt Linux проверяются по мере
|
|
||||||
доступности: недоступная среда явно записывается как неподтверждённая, а
|
|
||||||
поддержка заявляется только после проверки версии Python и полного прогона.
|
|
||||||
|
|
||||||
## Влияние на документацию
|
|
||||||
|
|
||||||
Основная инструкция находится в `proxy-client/README.md` рядом с
|
|
||||||
реализацией. Она должна описывать:
|
|
||||||
|
|
||||||
- различие внешнего Hiddify и управляемого Xray;
|
|
||||||
- самостоятельный `install.sh`, его автоматический выбор stow или прямых
|
|
||||||
симлинков, требование сохранять исходный каталог и режим
|
|
||||||
`--no-shell-setup`;
|
|
||||||
- границу между управляемыми файлами пакета и создаваемыми вручную машинным
|
|
||||||
конфигом и закрытым профилем;
|
|
||||||
- создание машинного INI из шаблона и отдельного файла VLESS-ссылки;
|
|
||||||
- команды управления, `proxy_on/off`, `pclaude/pcodex`;
|
|
||||||
- настройка `NO_PROXY`, ограничения CIDR и диагностика;
|
|
||||||
- поддерживаемые семейства VLESS и современную проверку сертификата;
|
|
||||||
- отсутствие systemd, автозапуска и автоматического перехода между прокси.
|
|
||||||
|
|
||||||
Корневой README получает только короткий обзор, пример подключения пакета и
|
|
||||||
ссылку на полную инструкцию клиента.
|
|
||||||
|
|
||||||
Шаблоны должны содержать только вымышленные значения. Настоящие VLESS-ссылки,
|
|
||||||
UUID и ключи никогда не входят в репозиторий.
|
|
||||||
+76
-57
@@ -144,28 +144,36 @@ wsl_conf_has_boot_command() {
|
|||||||
' "$1" 2>/dev/null
|
' "$1" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
# Миграция: если ~/.claude — stow-симлинк, раскрыть в директорию.
|
# Миграция: если $1 — stow-симлинк (пакет был свёрнут в репозиторий),
|
||||||
migrate_claude_stow_symlink() {
|
# раскрыть в настоящий каталог и перенести содержимое. Остальные аргументы —
|
||||||
[ -L "$HOME/.claude" ] || return 0
|
# имена git-tracked файлов, которые остаются в репозитории.
|
||||||
|
migrate_folded_symlink() {
|
||||||
|
local link="$1"
|
||||||
|
shift
|
||||||
|
[ -L "$link" ] || return 0
|
||||||
|
|
||||||
info "Миграция: ~/.claude — симлинк, преобразую в директорию..."
|
info "Миграция: $link — симлинк, преобразую в директорию..."
|
||||||
local stow_dir
|
local stow_dir
|
||||||
stow_dir="$(readlink -f "$HOME/.claude" 2>/dev/null || true)"
|
stow_dir="$(readlink -f "$link" 2>/dev/null || true)"
|
||||||
rm "$HOME/.claude"
|
rm "$link"
|
||||||
mkdir -p "$HOME/.claude"
|
mkdir -p "$link"
|
||||||
|
|
||||||
if [ -n "$stow_dir" ] && [ -d "$stow_dir" ]; then
|
if [ -n "$stow_dir" ] && [ -d "$stow_dir" ]; then
|
||||||
for item in "$stow_dir"/* "$stow_dir"/.[!.]*; do
|
for item in "$stow_dir"/* "$stow_dir"/.[!.]*; do
|
||||||
[ -e "$item" ] || continue
|
[ -e "$item" ] || [ -L "$item" ] || continue
|
||||||
local base
|
local base skip s
|
||||||
base=$(basename "$item")
|
base=$(basename "$item")
|
||||||
# git-tracked файлы остаются в репозитории
|
# git-tracked файлы остаются в репозитории
|
||||||
case "$base" in .gitignore|settings.local.json|skills) continue ;; esac
|
skip=""
|
||||||
mv "$item" "$HOME/.claude/" 2>/dev/null || true
|
for s in "$@"; do
|
||||||
|
[ "$base" = "$s" ] && skip=1
|
||||||
|
done
|
||||||
|
[ -n "$skip" ] && continue
|
||||||
|
mv "$item" "$link/" 2>/dev/null || true
|
||||||
done
|
done
|
||||||
info "Рантайм-данные перенесены в ~/.claude/"
|
info "Рантайм-данные перенесены в $link/"
|
||||||
else
|
else
|
||||||
warn "Не удалось прочитать источник старого симлинка ~/.claude, продолжаю без миграции данных"
|
warn "Не удалось прочитать источник старого симлинка $link, продолжаю без миграции данных"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +186,7 @@ migrate_claude_stow_symlink() {
|
|||||||
#
|
#
|
||||||
# Обход повторяет логику stow, а не перебирает файлы подряд: если каталога в
|
# Обход повторяет логику stow, а не перебирает файлы подряд: если каталога в
|
||||||
# HOME нет, конфликта быть не может и всё поддерево пропускается. Это заодно
|
# HOME нет, конфликта быть не может и всё поддерево пропускается. Это заодно
|
||||||
# отсекает свёрнутые пакеты (~/.codex, ~/.vscode-server) с их гигабайтами
|
# отсекает свёрнутые пакеты (~/.vscode-server) с их гигабайтами
|
||||||
# рантайм-данных.
|
# рантайм-данных.
|
||||||
backup_conflicting_stow_targets() {
|
backup_conflicting_stow_targets() {
|
||||||
local pkg=""
|
local pkg=""
|
||||||
@@ -370,7 +378,7 @@ if ! command -v "$STOW_BIN" &>/dev/null; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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=()
|
AVAILABLE_PACKAGES=()
|
||||||
|
|
||||||
info "Применяю stow пакеты..."
|
info "Применяю stow пакеты..."
|
||||||
@@ -429,68 +437,79 @@ SKILLS_SRC="$DOTFILES_DIR/agents/.agents/skills"
|
|||||||
OPENCODE_DIR="$HOME/.config/opencode"
|
OPENCODE_DIR="$HOME/.config/opencode"
|
||||||
OPENCODE_SRC_DIR="$DOTFILES_DIR/opencode/.config/opencode"
|
OPENCODE_SRC_DIR="$DOTFILES_DIR/opencode/.config/opencode"
|
||||||
OPENCODE_JSON_SRC="$OPENCODE_SRC_DIR/opencode.json"
|
OPENCODE_JSON_SRC="$OPENCODE_SRC_DIR/opencode.json"
|
||||||
|
OPENCODE_REVIEWER_SRC="$OPENCODE_SRC_DIR/permission-reviewer.jsonc"
|
||||||
|
|
||||||
mkdir -p "$OPENCODE_DIR"
|
mkdir -p "$OPENCODE_DIR"
|
||||||
|
|
||||||
if [ -d "$OPENCODE_SRC_DIR/agents" ]; then
|
# Профили safe-build/safe-plan удалены: агенты встроенные (build/plan),
|
||||||
ensure_symlink "$OPENCODE_SRC_DIR/agents" "$OPENCODE_DIR/agents"
|
# решения об "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
|
fi
|
||||||
|
|
||||||
if [ -d "$SKILLS_SRC" ]; then
|
# Скиллы opencode сам берёт из ~/.agents/skills и ~/.claude/skills. Прежняя
|
||||||
ensure_symlink "$SKILLS_SRC" "$OPENCODE_DIR/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
|
fi
|
||||||
|
|
||||||
if [ -f "$OPENCODE_JSON_SRC" ]; then
|
if [ -f "$OPENCODE_JSON_SRC" ]; then
|
||||||
copy_managed_file "$OPENCODE_JSON_SRC" "$OPENCODE_DIR/opencode.json" "opencode.json"
|
copy_managed_file "$OPENCODE_JSON_SRC" "$OPENCODE_DIR/opencode.json" "opencode.json"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Claude Code ──────────────────────────────────────────────────────
|
# Настройки permission-reviewer: плагин читает их только из глобального
|
||||||
# settings.local.json ЛИНКУЕТСЯ из репозитория (базовые permissions + statusLine).
|
# ~/.config/opencode, из репозитория они не подхватываются — копируем.
|
||||||
# Claude Code не модифицирует settings.local.json — только читает и мержит
|
if [ -f "$OPENCODE_REVIEWER_SRC" ]; then
|
||||||
# с settings.json, который Claude Code создаёт и обновляет самостоятельно.
|
copy_managed_file "$OPENCODE_REVIEWER_SRC" "$OPENCODE_DIR/permission-reviewer.jsonc" "permission-reviewer.jsonc"
|
||||||
# Машинно-специфичные настройки (model, effortLevel) — в settings.json.
|
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"
|
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 (симлинк)
|
# Skills (симлинк)
|
||||||
if [ -d "$SKILLS_SRC" ]; then
|
if [ -d "$SKILLS_SRC" ]; then
|
||||||
ensure_symlink "$SKILLS_SRC" "$HOME/.claude/skills"
|
ensure_symlink "$SKILLS_SRC" "$HOME/.claude/skills"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Глобальный CLAUDE.md — словарь рабочего процесса (симлинк)
|
# ── Codex ────────────────────────────────────────────────────────────
|
||||||
CLAUDE_MD_SRC="$DOTFILES_DIR/claude/.claude/CLAUDE.md"
|
# Пакета в репозитории нет: ~/.codex — обычный каталог, его заполняет Codex.
|
||||||
if [ -f "$CLAUDE_MD_SRC" ]; then
|
# Миграция раскрывает старый свёрнутый симлинк и переносит рантайм-данные.
|
||||||
ensure_symlink "$CLAUDE_MD_SRC" "$HOME/.claude/CLAUDE.md"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# settings.local.json — базовые настройки из репозитория (симлинк)
|
migrate_folded_symlink "$HOME/.codex" .gitkeep
|
||||||
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
|
|
||||||
|
|
||||||
# ── Bash: .bashrc ────────────────────────────────────────────────────
|
# ── Bash: .bashrc ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -1,78 +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
|
|
||||||
---
|
|
||||||
|
|
||||||
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,99 +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
|
|
||||||
"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
|
|
||||||
@@ -1,45 +1,34 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
"default_agent": "safe-plan",
|
// Минимальный шаблон для OpenCode 2.x. Файл раскладывается копией
|
||||||
"permission": {
|
// (install.sh → copy_managed_file), поэтому правки в репозитории
|
||||||
"write": "ask",
|
// попадают в ~/.config/opencode только после ./install.sh.
|
||||||
"edit": "ask",
|
"share": "manual",
|
||||||
"patch": "ask",
|
"permissions": [
|
||||||
"webfetch": "ask",
|
// Порядок важен: побеждает последнее совпавшее правило.
|
||||||
"task": "ask",
|
{ "action": "edit", "resource": "*", "effect": "ask" },
|
||||||
"bash": {
|
{ "action": "webfetch", "resource": "*", "effect": "ask" },
|
||||||
"*": "ask",
|
{ "action": "subagent", "resource": "*", "effect": "ask" },
|
||||||
"git push": "deny",
|
{ "action": "shell", "resource": "*", "effect": "ask" },
|
||||||
"git push *": "deny"
|
{ "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" }
|
||||||
|
],
|
||||||
"mcp": {
|
"mcp": {
|
||||||
"context7": {
|
"servers": {
|
||||||
"type": "remote",
|
"context7": {
|
||||||
"url": "https://mcp.context7.com/mcp",
|
"type": "remote",
|
||||||
"headers": {
|
"url": "https://mcp.context7.com/mcp",
|
||||||
"CONTEXT7_API_KEY": "${env:CONTEXT7_API_KEY}"
|
"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
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"share": "manual"
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
// Как устроена вся система автоподтверждения — см. 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 +0,0 @@
|
|||||||
../../../agents/.agents/skills
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# плагин opencode-permission-reviewer читает конфиг с O_NOFOLLOW:
|
||||||
|
# симлинк игнорируется молча, поэтому файл деплоится копией
|
||||||
|
permission-reviewer\.jsonc
|
||||||
@@ -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
|
||||||
@@ -5,21 +5,17 @@ _proxy_cli() {
|
|||||||
"${PROXY_CLIENT_BIN:-proxy-client}" "$@"
|
"${PROXY_CLIENT_BIN:-proxy-client}" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
_proxy_apply_env_file() {
|
# Применяет вывод "proxy-client env" целиком или не применяет ничего.
|
||||||
local output_file="$1" line key value
|
_proxy_apply_env() {
|
||||||
|
local line key
|
||||||
local -A proxy_values=()
|
local -A proxy_values=()
|
||||||
local -a allowed_keys=(
|
|
||||||
HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY
|
|
||||||
http_proxy https_proxy all_proxy no_proxy
|
|
||||||
)
|
|
||||||
|
|
||||||
while IFS= read -r line || [ -n "$line" ]; do
|
while IFS= read -r line; do
|
||||||
if [[ "$line" != *=* ]]; then
|
if [[ "$line" != *=* ]]; then
|
||||||
echo "Ошибка: proxy-client env вернул строку без '='" >&2
|
echo "Ошибка: proxy-client env вернул строку без '='" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
key="${line%%=*}"
|
key="${line%%=*}"
|
||||||
value="${line#*=}"
|
|
||||||
case "$key" in
|
case "$key" in
|
||||||
HTTP_PROXY|HTTPS_PROXY|ALL_PROXY|NO_PROXY|http_proxy|https_proxy|all_proxy|no_proxy) ;;
|
HTTP_PROXY|HTTPS_PROXY|ALL_PROXY|NO_PROXY|http_proxy|https_proxy|all_proxy|no_proxy) ;;
|
||||||
*)
|
*)
|
||||||
@@ -27,38 +23,20 @@ _proxy_apply_env_file() {
|
|||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
proxy_values["$key"]="$value"
|
proxy_values["$key"]="${line#*=}"
|
||||||
done < "$output_file"
|
done <<< "$1"
|
||||||
|
|
||||||
for key in "${allowed_keys[@]}"; do
|
for key in "${!proxy_values[@]}"; do
|
||||||
if [[ -n "${proxy_values[$key]+set}" ]]; then
|
export "$key=${proxy_values[$key]}"
|
||||||
printf -v "$key" '%s' "${proxy_values[$key]}"
|
|
||||||
export "$key"
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy_on() {
|
proxy_on() {
|
||||||
local output_file old_umask
|
local output
|
||||||
old_umask="$(umask)"
|
if ! output="$(_proxy_cli env)" || ! _proxy_apply_env "$output"; then
|
||||||
umask 077
|
|
||||||
if ! output_file="$(mktemp "${TMPDIR:-/tmp}/proxy-client-env.XXXXXX")"; then
|
|
||||||
umask "$old_umask"
|
|
||||||
echo "Ошибка: не удалось создать временный файл для proxy-env" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
umask "$old_umask"
|
|
||||||
if ! _proxy_cli env > "$output_file"; then
|
|
||||||
rm -f "$output_file"
|
|
||||||
echo "Ошибка: прокси не включён" >&2
|
echo "Ошибка: прокси не включён" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
if ! _proxy_apply_env_file "$output_file"; then
|
|
||||||
rm -f "$output_file"
|
|
||||||
echo "Ошибка: прокси не включён" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
rm -f "$output_file"
|
|
||||||
echo "Прокси включён"
|
echo "Прокси включён"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+482
-1036
File diff suppressed because it is too large.
Load diff
+117
-24
@@ -19,13 +19,16 @@ CLI требует Python 3.8+ и использует только станда
|
|||||||
~/src/proxy-client/install.sh
|
~/src/proxy-client/install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
При наличии Stow установщик использует его, иначе создаёт те же три симлинка
|
Установщик создаёт три ссылки на исходный каталог: CLI, Bash-модуль и каталог
|
||||||
напрямую: CLI, Bash-модуль и каталог шаблонов. Исходный каталог после установки
|
шаблонов. Поэтому исходный каталог после установки нельзя перемещать. Если
|
||||||
нельзя перемещать: домашние ссылки зависят от его постоянного пути.
|
какой-то из путей уже занят чужим файлом или ссылкой, установщик ничего не
|
||||||
|
меняет и перечисляет все такие пути. Повторный запуск безопасен.
|
||||||
|
|
||||||
По умолчанию установщик добавляет в `~/.bashrc` помеченный блок для
|
По умолчанию установщик дописывает в `~/.bashrc` блок между строками
|
||||||
`~/.local/bin` и Bash-модуля. После установки откройте новый Bash или примените
|
`# >>> proxy-client >>>` и `# <<< proxy-client <<<`: он добавляет
|
||||||
блок в текущей сессии:
|
`~/.local/bin` в `PATH` и подключает Bash-модуль. Если блок уже есть, файл не
|
||||||
|
меняется. После установки откройте новый Bash или примените блок в текущей
|
||||||
|
сессии:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
source ~/.bashrc
|
source ~/.bashrc
|
||||||
@@ -253,8 +256,46 @@ proxy-client stop
|
|||||||
плане. Systemd, фоновый автозапуск и скрытый запуск из `run` не используются.
|
плане. Systemd, фоновый автозапуск и скрытый запуск из `run` не используются.
|
||||||
Один loopback-порт принимает HTTP CONNECT и SOCKS5.
|
Один loopback-порт принимает HTTP CONNECT и SOCKS5.
|
||||||
|
|
||||||
`stop` завершает управляемые процессы и удаляет созданные служебные файлы. Он
|
Остановить Xray можно двумя способами: `Ctrl+C` в окне `start` или
|
||||||
работает и после изменения машинного типа на `external`.
|
`proxy-client stop` из любого терминала. Оба гасят Xray и удаляют служебные
|
||||||
|
файлы. Если процесс `start` был убит (например, закрыли окно tmux), Xray может
|
||||||
|
остаться работать: `status` и `env` тогда сообщат об ошибке, а `stop` добьёт его
|
||||||
|
и уберёт файлы. `stop` работает и после изменения машинного типа на `external`.
|
||||||
|
|
||||||
|
Второй `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=*`.
|
||||||
|
|
||||||
## Диагностика
|
## Диагностика
|
||||||
|
|
||||||
@@ -343,7 +384,33 @@ source ~/.config/proxy-client/shell.bash
|
|||||||
|
|
||||||
Поддерживаются VLESS TCP/RAW с REALITY и VLESS XHTTP с TLS. Неизвестные,
|
Поддерживаются VLESS TCP/RAW с REALITY и VLESS XHTTP с TLS. Неизвестные,
|
||||||
повторные и несовместимые query-параметры отклоняются. XHTTP поддерживает ALPN
|
повторные и несовместимые query-параметры отклоняются. XHTTP поддерживает ALPN
|
||||||
`http/1.1`, `h2` и `h3`; `extra` должен быть JSON-объектом без повторных ключей.
|
`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=none`, принимается VLESS-шифрование
|
||||||
`encryption=mlkem768x25519plus.…` из новых ссылок Hiddify; остальную часть
|
`encryption=mlkem768x25519plus.…` из новых ссылок Hiddify; остальную часть
|
||||||
@@ -351,9 +418,9 @@ source ~/.config/proxy-client/shell.bash
|
|||||||
`flow=xtls-rprx-vision`. Косая черта перед параметрами (`…:443/?type=…`)
|
`flow=xtls-rprx-vision`. Косая черта перед параметрами (`…:443/?type=…`)
|
||||||
допустима, другой путь в ссылке — нет.
|
допустима, другой путь в ссылке — нет.
|
||||||
|
|
||||||
Xray 26.3.27 использует `pcs` и `vcn`, а устаревшие `allowInsecure` и
|
Xray 26.10.10 использует `pcs` и `vcn`. Устаревшие `allowInsecure` и `insecure`
|
||||||
`insecure` не переносит. Истинное legacy-значение даёт предупреждение, но
|
принимаются с любым значением, но не переносятся: при значении `1` или `true`
|
||||||
обычная проверка сертификата остаётся включённой.
|
выводится предупреждение, а сертификат проверяется обычным способом.
|
||||||
|
|
||||||
Последняя версия Xray, где `allowInsecure` ещё применяется, —
|
Последняя версия Xray, где `allowInsecure` ещё применяется, —
|
||||||
[25.12.8](https://github.com/XTLS/Xray-core/releases/tag/v25.12.8); опция
|
[25.12.8](https://github.com/XTLS/Xray-core/releases/tag/v25.12.8); опция
|
||||||
@@ -362,18 +429,29 @@ Xray 26.3.27 использует `pcs` и `vcn`, а устаревшие `allow
|
|||||||
|
|
||||||
### Проверяемая установка Xray
|
### Проверяемая установка 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` —
|
`x86_64`/`amd64` используется `Xray-linux-64.zip`, для `aarch64`/`arm64` —
|
||||||
`Xray-linux-arm64-v8a.zip`; остальные архитектуры отклоняются.
|
`Xray-linux-arm64-v8a.zip`; остальные архитектуры отклоняются.
|
||||||
|
|
||||||
CLI скачивает официальный архив и файл `.dgst`, проверяет SHA-256, ограничения
|
[Выпуск 26.10.10](https://github.com/XTLS/Xray-core/releases/tag/v26.10.10)
|
||||||
размера и фактическую версию бинарника, а затем атомарно заменяет назначение.
|
помечен как предварительный. Обновление уже установленного клиента:
|
||||||
Корректная уже установленная версия даёт no-op без сети. Другой файл или симлинк
|
|
||||||
не заменяется без `--replace`; каталог не заменяется никогда.
|
|
||||||
|
|
||||||
`start` использует первый `xray` из `PATH` и принимает только версию 26.3.27.
|
```bash
|
||||||
Если `PATH` выбирает другой бинарник, команда сообщает об ошибке. Во время
|
proxy-client install --replace
|
||||||
живого или неподтверждённого управляемого процесса установка запрещена.
|
```
|
||||||
|
|
||||||
|
CLI скачивает официальный архив и файл `.dgst`, сверяет SHA-256 и фактическую
|
||||||
|
версию бинарника, а затем атомарно заменяет назначение. При любой ошибке старый
|
||||||
|
файл остаётся на месте. Если нужная версия уже установлена, сеть не нужна и
|
||||||
|
ничего не меняется. Другой файл или симлинк не заменяется без `--replace`
|
||||||
|
(симлинк заменяется сам, файл по ссылке не трогается); каталог не заменяется
|
||||||
|
никогда. Если после установки `PATH` находит другой `xray`, `install` сообщает об
|
||||||
|
ошибке и подсказывает команду.
|
||||||
|
|
||||||
|
Устанавливать можно и при работающем Xray: он продолжит работу со старым файлом,
|
||||||
|
а `install` напомнит перезапустить `start`.
|
||||||
|
|
||||||
|
`start` использует первый `xray` из `PATH` и принимает только версию 26.10.10.
|
||||||
|
|
||||||
### Файлы и безопасность
|
### Файлы и безопасность
|
||||||
|
|
||||||
@@ -381,10 +459,25 @@ CLI скачивает официальный архив и файл `.dgst`, п
|
|||||||
|------|------------|
|
|------|------------|
|
||||||
| `~/.config/proxy-client/config` | Несекретный машинный выбор режима |
|
| `~/.config/proxy-client/config` | Несекретный машинный выбор режима |
|
||||||
| `~/.config/proxy-client/default.url` | Закрытая VLESS-ссылка, права `600` |
|
| `~/.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 |
|
| `$XDG_STATE_HOME/proxy-client/` или `~/.local/state/proxy-client/` | Состояние и сгенерированный JSON |
|
||||||
|
|
||||||
Сводка скрывает UUID и ключи, а для XHTTP показывает только длину `path`.
|
Сводка скрывает UUID и ключи, а для XHTTP показывает только длину `path`.
|
||||||
Служебный каталог закрыт для других пользователей и очищается после штатной
|
Если файл профиля доступен другим пользователям, `start` не запустится и
|
||||||
остановки Xray. Сбой определения внешнего IP показывается как предупреждение и
|
покажет готовую команду `chmod 600` для конечного файла (у симлинка — для файла,
|
||||||
не останавливает уже готовый процесс.
|
на который он указывает). Служебный каталог закрыт для других пользователей
|
||||||
|
(`700`, файлы в нём — `600`) и очищается после остановки Xray. Сбой
|
||||||
|
определения внешнего IP показывается как предупреждение и не останавливает уже
|
||||||
|
готовый процесс.
|
||||||
|
|
||||||
|
## Изменение кода
|
||||||
|
|
||||||
|
Перед правкой proxy-client прочитайте
|
||||||
|
[спецификацию](../docs/specs/2026-07-15-proxy-client-design.md): там сказано,
|
||||||
|
от чего клиент защищается, от чего нет и почему, и где живут тесты.
|
||||||
|
|
||||||
|
Все тесты пакета (из корня dotfiles):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/test_proxy_client_full.sh
|
||||||
|
```
|
||||||
+33
-156
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# Ставит proxy-client в домашний каталог: три ссылки на этот каталог и
|
||||||
|
# (по умолчанию) блок в ~/.bashrc. Повторный запуск ничего не меняет.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
PACKAGE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
PACKAGE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||||
STOW_BIN="${STOW_BIN:-stow}"
|
|
||||||
SETUP_SHELL=1
|
SETUP_SHELL=1
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
@@ -15,9 +16,7 @@ EOF
|
|||||||
|
|
||||||
while [ "$#" -gt 0 ]; do
|
while [ "$#" -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--no-shell-setup)
|
--no-shell-setup) SETUP_SHELL=0 ;;
|
||||||
SETUP_SHELL=0
|
|
||||||
;;
|
|
||||||
-h|--help)
|
-h|--help)
|
||||||
usage
|
usage
|
||||||
exit 0
|
exit 0
|
||||||
@@ -31,180 +30,58 @@ while [ "$#" -gt 0 ]; do
|
|||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
SOURCES=(
|
# Машинные config и default.url сюда не входят: они живут только в $HOME.
|
||||||
"$PACKAGE_DIR/.local/bin/proxy-client"
|
LINKS=(
|
||||||
"$PACKAGE_DIR/.config/proxy-client/shell.bash"
|
.local/bin/proxy-client
|
||||||
"$PACKAGE_DIR/.config/proxy-client/examples"
|
.config/proxy-client/shell.bash
|
||||||
)
|
.config/proxy-client/examples
|
||||||
TARGETS=(
|
|
||||||
"$HOME/.local/bin/proxy-client"
|
|
||||||
"$HOME/.config/proxy-client/shell.bash"
|
|
||||||
"$HOME/.config/proxy-client/examples"
|
|
||||||
)
|
|
||||||
STOW_IGNORE_PATTERNS=(
|
|
||||||
'^\.local/bin/proxy-client$'
|
|
||||||
'^\.config/proxy-client/shell\.bash$'
|
|
||||||
'^\.config/proxy-client/examples$'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
source_errors=()
|
# Существующая ссылка на этот же файл пакета — не конфликт, всё остальное
|
||||||
[ -x "${SOURCES[0]}" ] || source_errors+=("${SOURCES[0]}")
|
# (включая битую ссылку) не трогаем.
|
||||||
[ -f "${SOURCES[1]}" ] || source_errors+=("${SOURCES[1]}")
|
|
||||||
[ -d "${SOURCES[2]}" ] || source_errors+=("${SOURCES[2]}")
|
|
||||||
if [ "${#source_errors[@]}" -gt 0 ]; then
|
|
||||||
echo "Ошибка: отсутствуют обязательные файлы исходного пакета:" >&2
|
|
||||||
printf ' %s\n' "${source_errors[@]}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
"${SOURCES[0]}" --help >/dev/null
|
|
||||||
bash -n "${SOURCES[1]}"
|
|
||||||
|
|
||||||
SHELL_BLOCK_START='# >>> proxy-client >>>'
|
|
||||||
SHELL_BLOCK_END='# <<< proxy-client <<<'
|
|
||||||
SHELL_SETUP_NEEDED=0
|
|
||||||
BASH_CUSTOM_HAS_MODULE=0
|
|
||||||
SHELL_PATH_NEEDED=1
|
|
||||||
SHELL_SOURCE_NEEDED=1
|
|
||||||
SHELL_BLOCK_CONFLICT=0
|
|
||||||
if [ "$SETUP_SHELL" -eq 1 ]; then
|
|
||||||
if [ -f "$HOME/.bash_custom" ] \
|
|
||||||
&& grep -Fq 'proxy-client/shell.bash' "$HOME/.bash_custom"; then
|
|
||||||
BASH_CUSTOM_HAS_MODULE=1
|
|
||||||
SHELL_SOURCE_NEEDED=0
|
|
||||||
fi
|
|
||||||
|
|
||||||
bashrc_start_count=0
|
|
||||||
bashrc_end_count=0
|
|
||||||
if [ -f "$HOME/.bashrc" ]; then
|
|
||||||
bashrc_start_count="$(
|
|
||||||
grep -Fxc "$SHELL_BLOCK_START" "$HOME/.bashrc" || true
|
|
||||||
)"
|
|
||||||
bashrc_end_count="$(
|
|
||||||
grep -Fxc "$SHELL_BLOCK_END" "$HOME/.bashrc" || true
|
|
||||||
)"
|
|
||||||
if grep -Fq 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.bashrc"; then
|
|
||||||
SHELL_PATH_NEEDED=0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$bashrc_start_count" -eq 0 ] && [ "$bashrc_end_count" -eq 0 ]; then
|
|
||||||
if [ "$SHELL_PATH_NEEDED" -eq 1 ] \
|
|
||||||
|| [ "$SHELL_SOURCE_NEEDED" -eq 1 ]; then
|
|
||||||
SHELL_SETUP_NEEDED=1
|
|
||||||
fi
|
|
||||||
elif [ "$bashrc_start_count" -eq 1 ] \
|
|
||||||
&& [ "$bashrc_end_count" -eq 1 ] \
|
|
||||||
&& [ "$SHELL_PATH_NEEDED" -eq 0 ] \
|
|
||||||
&& { [ "$BASH_CUSTOM_HAS_MODULE" -eq 1 ] \
|
|
||||||
|| grep -Fq 'proxy-client/shell.bash' "$HOME/.bashrc"; }; then
|
|
||||||
:
|
|
||||||
else
|
|
||||||
SHELL_BLOCK_CONFLICT=1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
conflicts=()
|
conflicts=()
|
||||||
missing_targets=0
|
for link in "${LINKS[@]}"; do
|
||||||
stow_ignore_args=()
|
target="$HOME/$link"
|
||||||
PARENT_DIRS=(
|
|
||||||
"$HOME/.local"
|
|
||||||
"$HOME/.local/bin"
|
|
||||||
"$HOME/.config"
|
|
||||||
"$HOME/.config/proxy-client"
|
|
||||||
)
|
|
||||||
for parent_dir in "${PARENT_DIRS[@]}"; do
|
|
||||||
if { [ -e "$parent_dir" ] || [ -L "$parent_dir" ]; } \
|
|
||||||
&& { [ ! -d "$parent_dir" ] || [ -L "$parent_dir" ]; }; then
|
|
||||||
conflicts+=("$parent_dir")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
for index in "${!TARGETS[@]}"; do
|
|
||||||
target="${TARGETS[$index]}"
|
|
||||||
source_path="${SOURCES[$index]}"
|
|
||||||
if [ -e "$target" ] || [ -L "$target" ]; then
|
if [ -e "$target" ] || [ -L "$target" ]; then
|
||||||
if [ ! -L "$target" ] \
|
if [ "$(readlink -f "$target" 2>/dev/null)" != "$PACKAGE_DIR/$link" ]; then
|
||||||
|| [ "$(readlink -f "$target" 2>/dev/null || true)" != "$source_path" ]; then
|
|
||||||
conflicts+=("$target")
|
conflicts+=("$target")
|
||||||
else
|
|
||||||
stow_ignore_args+=("--ignore=${STOW_IGNORE_PATTERNS[$index]}")
|
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
missing_targets=$((missing_targets + 1))
|
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ "$SHELL_SETUP_NEEDED" -eq 1 ] \
|
|
||||||
&& { [ -e "$HOME/.bashrc" ] || [ -L "$HOME/.bashrc" ]; } \
|
|
||||||
&& [ ! -f "$HOME/.bashrc" ]; then
|
|
||||||
conflicts+=("$HOME/.bashrc")
|
|
||||||
fi
|
|
||||||
if [ "$SHELL_BLOCK_CONFLICT" -eq 1 ]; then
|
|
||||||
conflicts+=("$HOME/.bashrc")
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${#conflicts[@]}" -gt 0 ]; then
|
if [ "${#conflicts[@]}" -gt 0 ]; then
|
||||||
echo "Ошибка: найдены конфликтующие пути:" >&2
|
echo "Ошибка: пути уже заняты, установка не выполнена:" >&2
|
||||||
printf ' %s\n' "${conflicts[@]}" >&2
|
printf ' %s\n' "${conflicts[@]}" >&2
|
||||||
echo "Разберите их вручную и повторите установку." >&2
|
echo "Удалите или переименуйте их и повторите установку." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$HOME/.local/bin" "$HOME/.config/proxy-client"
|
mkdir -p "$HOME/.local/bin" "$HOME/.config/proxy-client"
|
||||||
|
for link in "${LINKS[@]}"; do
|
||||||
|
[ -e "$HOME/$link" ] || ln -s "$PACKAGE_DIR/$link" "$HOME/$link"
|
||||||
|
done
|
||||||
|
|
||||||
if command -v "$STOW_BIN" >/dev/null 2>&1; then
|
SHELL_BLOCK_START='# >>> proxy-client >>>'
|
||||||
echo "Установка с помощью Stow."
|
if [ "$SETUP_SHELL" -eq 0 ]; then
|
||||||
if [ "$missing_targets" -gt 0 ]; then
|
echo "Настройка Bash пропущена (--no-shell-setup)."
|
||||||
"$STOW_BIN" --dir="$(dirname "$PACKAGE_DIR")" --target="$HOME" \
|
# Команда для пользователя: $HOME и $PATH раскроются в его сессии.
|
||||||
"${stow_ignore_args[@]}" \
|
# shellcheck disable=SC2016
|
||||||
"$(basename "$PACKAGE_DIR")"
|
RELOAD_COMMAND='export PATH="$HOME/.local/bin:$PATH"; source ~/.config/proxy-client/shell.bash'
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
echo "Установка без Stow: создаю символические ссылки напрямую."
|
if ! grep -Fqx "$SHELL_BLOCK_START" "$HOME/.bashrc" 2>/dev/null; then
|
||||||
for index in "${!TARGETS[@]}"; do
|
cat >> "$HOME/.bashrc" <<EOF
|
||||||
target="${TARGETS[$index]}"
|
|
||||||
source_path="${SOURCES[$index]}"
|
|
||||||
if [ ! -L "$target" ]; then
|
|
||||||
ln -s "$source_path" "$target"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
"$HOME/.local/bin/proxy-client" --help >/dev/null
|
$SHELL_BLOCK_START
|
||||||
bash -n "$HOME/.config/proxy-client/shell.bash"
|
case ":\$PATH:" in
|
||||||
|
*":\$HOME/.local/bin:"*) ;;
|
||||||
if [ "$SHELL_SETUP_NEEDED" -eq 1 ]; then
|
*) export PATH="\$HOME/.local/bin:\$PATH" ;;
|
||||||
cat >> "$HOME/.bashrc" <<'EOF'
|
|
||||||
|
|
||||||
# >>> proxy-client >>>
|
|
||||||
EOF
|
|
||||||
if [ "$SHELL_PATH_NEEDED" -eq 1 ]; then
|
|
||||||
cat >> "$HOME/.bashrc" <<'EOF'
|
|
||||||
case ":$PATH:" in
|
|
||||||
*":$HOME/.local/bin:"*) ;;
|
|
||||||
*)
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
EOF
|
[ -f "\$HOME/.config/proxy-client/shell.bash" ] && source "\$HOME/.config/proxy-client/shell.bash"
|
||||||
fi
|
|
||||||
if [ "$SHELL_SOURCE_NEEDED" -eq 1 ]; then
|
|
||||||
cat >> "$HOME/.bashrc" <<'EOF'
|
|
||||||
[ -f "$HOME/.config/proxy-client/shell.bash" ] && source "$HOME/.config/proxy-client/shell.bash"
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
cat >> "$HOME/.bashrc" <<'EOF'
|
|
||||||
# <<< proxy-client <<<
|
# <<< proxy-client <<<
|
||||||
EOF
|
EOF
|
||||||
elif [ "$SETUP_SHELL" -eq 0 ]; then
|
echo "В ~/.bashrc добавлен блок proxy-client."
|
||||||
echo "Настройка Bash пропущена (--no-shell-setup)."
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$SETUP_SHELL" -eq 1 ]; then
|
|
||||||
RELOAD_COMMAND='source ~/.bashrc'
|
RELOAD_COMMAND='source ~/.bashrc'
|
||||||
else
|
|
||||||
RELOAD_COMMAND='export PATH="$HOME/.local/bin:$PATH"; source ~/.config/proxy-client/shell.bash'
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|||||||
+73
-65
@@ -21,7 +21,8 @@ assert_file_contains() {
|
|||||||
|
|
||||||
assert_not_exists() {
|
assert_not_exists() {
|
||||||
local path="$1"
|
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() {
|
setup_fixture() {
|
||||||
@@ -37,7 +38,7 @@ setup_fixture() {
|
|||||||
# zed/.config/opencode и т.п.) весят гигабайты, переполняют /tmp и ломают
|
# zed/.config/opencode и т.п.) весят гигабайты, переполняют /tmp и ломают
|
||||||
# stow конфликтами, поэтому в тестовое дерево не попадает.
|
# stow конфликтами, поэтому в тестовое дерево не попадает.
|
||||||
git -C "$REPO_ROOT" ls-files -z --cached --others --exclude-standard -- \
|
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 \
|
claude proxy-client chatgpt scripts \
|
||||||
| tar -C "$REPO_ROOT" --null -T - -cf - \
|
| tar -C "$REPO_ROOT" --null -T - -cf - \
|
||||||
| tar -xf - -C "$repo_root"
|
| tar -xf - -C "$repo_root"
|
||||||
@@ -225,22 +226,71 @@ test_existing_wezterm_config_is_backed_up() {
|
|||||||
assert_file_contains "$backup" 'require("wezterm")'
|
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
|
local fixture repo_root home_dir backup
|
||||||
fixture="$(setup_fixture)"
|
fixture="$(setup_fixture)"
|
||||||
repo_root="$fixture/repo"
|
repo_root="$fixture/repo"
|
||||||
home_dir="$fixture/home"
|
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"
|
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"
|
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)"
|
test_claude_stale_artifacts_are_removed() {
|
||||||
[ -z "$backup" ] || fail "Folded package contents must not be backed up: $backup"
|
local fixture repo_root home_dir
|
||||||
assert_file_contains "$home_dir/.codex/auth.json" "{}"
|
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() {
|
test_migrates_legacy_zed_config_symlink() {
|
||||||
@@ -358,53 +408,7 @@ test_does_not_follow_unrelated_local_symlink() {
|
|||||||
assert_not_exists "$unrelated_local/bin"
|
assert_not_exists "$unrelated_local/bin"
|
||||||
}
|
}
|
||||||
|
|
||||||
test_settings_local_is_symlinked_to_repo() {
|
test_stow_migration_preserves_legacy_files() {
|
||||||
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() {
|
|
||||||
local fixture repo_root home_dir stow_target
|
local fixture repo_root home_dir stow_target
|
||||||
fixture="$(setup_fixture)"
|
fixture="$(setup_fixture)"
|
||||||
repo_root="$fixture/repo"
|
repo_root="$fixture/repo"
|
||||||
@@ -414,14 +418,14 @@ test_stow_migration_preserves_legacy_settings_local() {
|
|||||||
# Simulate old stow layout: ~/.claude → repo dir
|
# Simulate old stow layout: ~/.claude → repo dir
|
||||||
stow_target="$repo_root/claude/.claude"
|
stow_target="$repo_root/claude/.claude"
|
||||||
ln -s "$stow_target" "$home_dir/.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"
|
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
|
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"
|
[ -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" ] || 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
|
# 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"
|
[ -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'
|
cat > "$home_dir/.config/opencode/opencode.json" <<'EOF'
|
||||||
{"source":"local"}
|
{"source":"local"}
|
||||||
EOF
|
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
|
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"
|
[ -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"
|
assert_not_exists "$home_dir/.config/opencode/agents"
|
||||||
[ -L "$home_dir/.config/opencode/skills" ] || fail "Expected ~/.config/opencode/skills symlink"
|
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/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)"
|
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"
|
[ -n "$backup_path" ] || fail "Expected backup for existing opencode.json"
|
||||||
@@ -643,15 +652,14 @@ main() {
|
|||||||
test_foreign_kitty_config_symlink_is_backed_up
|
test_foreign_kitty_config_symlink_is_backed_up
|
||||||
test_rerun_does_not_backup_managed_kitty_config
|
test_rerun_does_not_backup_managed_kitty_config
|
||||||
test_existing_wezterm_config_is_backed_up
|
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_migrates_legacy_zed_config_symlink
|
||||||
test_stops_legacy_migration_when_listing_fails
|
test_stops_legacy_migration_when_listing_fails
|
||||||
test_does_not_migrate_unrelated_config_symlink
|
test_does_not_migrate_unrelated_config_symlink
|
||||||
test_does_not_follow_unrelated_local_symlink
|
test_does_not_follow_unrelated_local_symlink
|
||||||
test_settings_local_is_symlinked_to_repo
|
test_stow_migration_preserves_legacy_files
|
||||||
test_settings_local_backup_on_existing_file
|
|
||||||
test_settings_local_dir_is_skipped
|
|
||||||
test_stow_migration_preserves_legacy_settings_local
|
|
||||||
test_rerun_does_not_duplicate_bashrc_line
|
test_rerun_does_not_duplicate_bashrc_line
|
||||||
test_repairs_nested_skills_directory_bug
|
test_repairs_nested_skills_directory_bug
|
||||||
test_preserves_nonempty_skills_directory
|
test_preserves_nonempty_skills_directory
|
||||||
|
|||||||
+527
-1158
File diff suppressed because it is too large.
Load diff
@@ -1,19 +1,17 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# Все тесты proxy-client: Python-тесты (текущий python3 и, если есть uv,
|
||||||
|
# Python 3.8 — минимальная поддерживаемая версия) и shell-тесты.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||||
cd "$REPO_ROOT"
|
|
||||||
|
|
||||||
if ! command -v uv >/dev/null 2>&1; then
|
|
||||||
echo "Ошибка: для полной проверки требуется uv" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
export PYTHONDONTWRITEBYTECODE=1
|
export PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
||||||
python3 -m unittest discover -s tests
|
python3 -m unittest discover -s tests -p 'test_proxy_*.py'
|
||||||
|
if command -v uv >/dev/null 2>&1; then
|
||||||
|
uv run --python 3.8 --no-project \
|
||||||
|
python -m unittest discover -s tests -p 'test_proxy_*.py'
|
||||||
|
else
|
||||||
|
echo "Предупреждение: uv не найден, прогон под Python 3.8 пропущен" >&2
|
||||||
|
fi
|
||||||
bash tests/test_proxy_launchers.sh
|
bash tests/test_proxy_launchers.sh
|
||||||
bash tests/test_chatgpt_proxy_launcher.sh
|
|
||||||
bash tests/test_proxy_client_install.sh
|
bash tests/test_proxy_client_install.sh
|
||||||
bash tests/test_install.sh
|
|
||||||
uv run --python 3.8 --no-project python -m unittest discover -s tests
|
|
||||||
+143
-367
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# Тесты пакетного установщика proxy-client/install.sh во временном HOME.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
@@ -10,393 +11,168 @@ fail() {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ok() {
|
||||||
|
echo "ok - $*"
|
||||||
|
}
|
||||||
|
|
||||||
assert_link_points_to() {
|
assert_link_points_to() {
|
||||||
local link_path="$1" expected_path="$2" actual_path
|
local link_path="$1" expected_path="$2"
|
||||||
[ -L "$link_path" ] || fail "Expected symlink: $link_path"
|
[ -L "$link_path" ] || fail "нет ссылки $link_path"
|
||||||
actual_path="$(readlink -f "$link_path")"
|
[ "$(readlink -f "$link_path")" = "$expected_path" ] \
|
||||||
[ "$actual_path" = "$expected_path" ] \
|
|| fail "$link_path указывает не на $expected_path"
|
||||||
|| fail "$link_path points to $actual_path, expected $expected_path"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_not_exists() {
|
assert_not_exists() {
|
||||||
local path="$1"
|
[ ! -e "$1" ] && [ ! -L "$1" ] || fail "путь не должен существовать: $1"
|
||||||
[ ! -e "$path" ] && [ ! -L "$path" ] || fail "Path should not exist: $path"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_package() {
|
assert_package_links() {
|
||||||
local fixture_root="$1" package_dir="$fixture_root/source with spaces/proxy-client"
|
local package_dir="$1" home_dir="$2" link
|
||||||
mkdir -p \
|
for link in .local/bin/proxy-client .config/proxy-client/shell.bash \
|
||||||
"$package_dir/.local/bin" \
|
.config/proxy-client/examples; do
|
||||||
"$package_dir/.config/proxy-client"
|
assert_link_points_to "$home_dir/$link" "$package_dir/$link"
|
||||||
cp -a \
|
|
||||||
"$REPO_ROOT/proxy-client/.local/bin/proxy-client" \
|
|
||||||
"$package_dir/.local/bin/proxy-client"
|
|
||||||
cp -a \
|
|
||||||
"$REPO_ROOT/proxy-client/.config/proxy-client/shell.bash" \
|
|
||||||
"$REPO_ROOT/proxy-client/.config/proxy-client/examples" \
|
|
||||||
"$package_dir/.config/proxy-client/"
|
|
||||||
cp -a \
|
|
||||||
"$REPO_ROOT/proxy-client/.stow-local-ignore" \
|
|
||||||
"$REPO_ROOT/proxy-client/README.md" \
|
|
||||||
"$REPO_ROOT/proxy-client/install.sh" \
|
|
||||||
"$package_dir/"
|
|
||||||
printf '%s\n' 'MACHINE_CONFIG_SENTINEL' > "$package_dir/.config/proxy-client/config"
|
|
||||||
printf '%s\n' 'PRIVATE_PROFILE_SENTINEL' > "$package_dir/.config/proxy-client/default.url"
|
|
||||||
printf '%s\n' "$package_dir"
|
|
||||||
}
|
|
||||||
|
|
||||||
run_package_install() {
|
|
||||||
local package_dir="$1" home_dir="$2" stow_bin="$3"
|
|
||||||
shift 3
|
|
||||||
(
|
|
||||||
cd "$TEST_TMP_ROOT"
|
|
||||||
HOME="$home_dir" STOW_BIN="$stow_bin" \
|
|
||||||
"$package_dir/install.sh" "$@"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
test_installs_same_public_artifacts_without_stow() {
|
|
||||||
local fixture_root package_dir home_dir output
|
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
|
||||||
home_dir="$fixture_root/home"
|
|
||||||
mkdir -p "$home_dir"
|
|
||||||
|
|
||||||
output="$(
|
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow --no-shell-setup
|
|
||||||
)"
|
|
||||||
|
|
||||||
printf '%s' "$output" | grep -Fq 'без Stow' \
|
|
||||||
|| fail "Expected direct-symlink method in output"
|
|
||||||
printf '%s' "$output" | grep -Fq \
|
|
||||||
'export PATH="$HOME/.local/bin:$PATH"; source ~/.config/proxy-client/shell.bash' \
|
|
||||||
|| fail "Expected complete current-session hint with --no-shell-setup"
|
|
||||||
[ -d "$home_dir/.local/bin" ] && [ ! -L "$home_dir/.local/bin" ] \
|
|
||||||
|| fail "Expected ~/.local/bin to be a real directory"
|
|
||||||
[ -d "$home_dir/.config/proxy-client" ] \
|
|
||||||
&& [ ! -L "$home_dir/.config/proxy-client" ] \
|
|
||||||
|| fail "Expected proxy-client config home to be a real directory"
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.local/bin/proxy-client" \
|
|
||||||
"$package_dir/.local/bin/proxy-client"
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.config/proxy-client/shell.bash" \
|
|
||||||
"$package_dir/.config/proxy-client/shell.bash"
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.config/proxy-client/examples" \
|
|
||||||
"$package_dir/.config/proxy-client/examples"
|
|
||||||
assert_not_exists "$home_dir/.config/proxy-client/config"
|
|
||||||
assert_not_exists "$home_dir/.config/proxy-client/default.url"
|
|
||||||
assert_not_exists "$home_dir/README.md"
|
|
||||||
assert_not_exists "$home_dir/install.sh"
|
|
||||||
assert_not_exists "$home_dir/.bashrc"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_configures_bash_once_by_default() {
|
|
||||||
local fixture_root package_dir home_dir output marker_count path_count source_count
|
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
|
||||||
home_dir="$fixture_root/home"
|
|
||||||
mkdir -p "$home_dir"
|
|
||||||
|
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow >/dev/null
|
|
||||||
output="$(run_package_install "$package_dir" "$home_dir" missing-stow)"
|
|
||||||
|
|
||||||
marker_count="$(grep -Fxc '# >>> proxy-client >>>' "$home_dir/.bashrc")"
|
|
||||||
path_count="$(grep -Fc 'export PATH="$HOME/.local/bin:$PATH"' "$home_dir/.bashrc")"
|
|
||||||
source_count="$(
|
|
||||||
grep -Fc \
|
|
||||||
'[ -f "$HOME/.config/proxy-client/shell.bash" ] && source "$HOME/.config/proxy-client/shell.bash"' \
|
|
||||||
"$home_dir/.bashrc"
|
|
||||||
)"
|
|
||||||
[ "$marker_count" -eq 1 ] || fail "Expected one managed Bash block"
|
|
||||||
[ "$path_count" -eq 1 ] || fail "Expected one ~/.local/bin PATH line"
|
|
||||||
[ "$source_count" -eq 1 ] || fail "Expected one shell module source line"
|
|
||||||
printf '%s' "$output" | grep -Fq 'source ~/.bashrc' \
|
|
||||||
|| fail "Expected current-session reload hint"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_installs_same_public_artifacts_with_stow() {
|
|
||||||
local fixture_root package_dir home_dir output
|
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
|
||||||
home_dir="$fixture_root/home"
|
|
||||||
mkdir -p "$home_dir"
|
|
||||||
|
|
||||||
output="$(run_package_install "$package_dir" "$home_dir" stow --no-shell-setup)"
|
|
||||||
|
|
||||||
printf '%s' "$output" | grep -Fq 'с помощью Stow' \
|
|
||||||
|| fail "Expected Stow method in output"
|
|
||||||
[ -d "$home_dir/.local/bin" ] && [ ! -L "$home_dir/.local/bin" ] \
|
|
||||||
|| fail "Expected ~/.local/bin to be a real directory"
|
|
||||||
[ -d "$home_dir/.config/proxy-client" ] \
|
|
||||||
&& [ ! -L "$home_dir/.config/proxy-client" ] \
|
|
||||||
|| fail "Expected proxy-client config home to be a real directory"
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.local/bin/proxy-client" \
|
|
||||||
"$package_dir/.local/bin/proxy-client"
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.config/proxy-client/shell.bash" \
|
|
||||||
"$package_dir/.config/proxy-client/shell.bash"
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.config/proxy-client/examples" \
|
|
||||||
"$package_dir/.config/proxy-client/examples"
|
|
||||||
assert_not_exists "$home_dir/.config/proxy-client/config"
|
|
||||||
assert_not_exists "$home_dir/.config/proxy-client/default.url"
|
|
||||||
assert_not_exists "$home_dir/README.md"
|
|
||||||
assert_not_exists "$home_dir/install.sh"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_rejects_linked_parent_directories_before_changes() {
|
|
||||||
local fixture_root package_dir home_dir external_local external_config output
|
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
|
||||||
home_dir="$fixture_root/home"
|
|
||||||
external_local="$fixture_root/external-local"
|
|
||||||
external_config="$fixture_root/external-config"
|
|
||||||
mkdir -p "$home_dir" "$external_local" "$external_config"
|
|
||||||
ln -s "$external_local" "$home_dir/.local"
|
|
||||||
ln -s "$external_config" "$home_dir/.config"
|
|
||||||
|
|
||||||
if output="$(
|
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow 2>&1
|
|
||||||
)"; then
|
|
||||||
fail "Expected linked parent directories to be rejected"
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s' "$output" | grep -Fq "$home_dir/.local" \
|
|
||||||
|| fail "Expected ~/.local conflict in output"
|
|
||||||
printf '%s' "$output" | grep -Fq "$home_dir/.config" \
|
|
||||||
|| fail "Expected ~/.config conflict in output"
|
|
||||||
assert_not_exists "$external_local/bin"
|
|
||||||
assert_not_exists "$external_config/proxy-client"
|
|
||||||
assert_not_exists "$home_dir/.bashrc"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_reports_all_target_conflicts_without_partial_install() {
|
|
||||||
local fixture_root package_dir home_dir other_target output
|
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
|
||||||
home_dir="$fixture_root/home"
|
|
||||||
other_target="$fixture_root/other-target"
|
|
||||||
mkdir -p "$home_dir/.local/bin" "$home_dir/.config/proxy-client/shell.bash"
|
|
||||||
printf '%s\n' 'KEEP_CLI' > "$home_dir/.local/bin/proxy-client"
|
|
||||||
printf '%s\n' 'KEEP_TARGET' > "$other_target"
|
|
||||||
ln -s "$other_target" "$home_dir/.config/proxy-client/examples"
|
|
||||||
|
|
||||||
if output="$(
|
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow 2>&1
|
|
||||||
)"; then
|
|
||||||
fail "Expected all target conflicts to be rejected"
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s' "$output" | grep -Fq "$home_dir/.local/bin/proxy-client" \
|
|
||||||
|| fail "Expected CLI conflict in output"
|
|
||||||
printf '%s' "$output" | grep -Fq "$home_dir/.config/proxy-client/shell.bash" \
|
|
||||||
|| fail "Expected shell module conflict in output"
|
|
||||||
printf '%s' "$output" | grep -Fq "$home_dir/.config/proxy-client/examples" \
|
|
||||||
|| fail "Expected examples conflict in output"
|
|
||||||
grep -Fq 'KEEP_CLI' "$home_dir/.local/bin/proxy-client" \
|
|
||||||
|| fail "Expected conflicting CLI file to stay unchanged"
|
|
||||||
[ -d "$home_dir/.config/proxy-client/shell.bash" ] \
|
|
||||||
|| fail "Expected conflicting shell directory to stay unchanged"
|
|
||||||
assert_link_points_to "$home_dir/.config/proxy-client/examples" "$other_target"
|
|
||||||
assert_not_exists "$home_dir/.bashrc"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_rejects_broken_link_without_creating_other_targets() {
|
|
||||||
local fixture_root package_dir home_dir output
|
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
|
||||||
home_dir="$fixture_root/home"
|
|
||||||
mkdir -p "$home_dir/.local/bin"
|
|
||||||
ln -s "$fixture_root/missing-cli" "$home_dir/.local/bin/proxy-client"
|
|
||||||
|
|
||||||
if output="$(
|
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow 2>&1
|
|
||||||
)"; then
|
|
||||||
fail "Expected broken target link to be rejected"
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s' "$output" | grep -Fq "$home_dir/.local/bin/proxy-client" \
|
|
||||||
|| fail "Expected broken CLI link in output"
|
|
||||||
[ -L "$home_dir/.local/bin/proxy-client" ] \
|
|
||||||
|| fail "Expected broken CLI link to stay unchanged"
|
|
||||||
assert_not_exists "$home_dir/.config"
|
|
||||||
assert_not_exists "$home_dir/.bashrc"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_preserves_machine_files_with_both_methods() {
|
|
||||||
local stow_bin fixture_root package_dir home_dir
|
|
||||||
for stow_bin in missing-stow stow; do
|
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
|
||||||
home_dir="$fixture_root/home"
|
|
||||||
mkdir -p "$home_dir/.config/proxy-client"
|
|
||||||
printf '%s\n' 'HOME_CONFIG_SENTINEL' \
|
|
||||||
> "$home_dir/.config/proxy-client/config"
|
|
||||||
printf '%s\n' 'HOME_PROFILE_SENTINEL' \
|
|
||||||
> "$home_dir/.config/proxy-client/default.url"
|
|
||||||
|
|
||||||
run_package_install "$package_dir" "$home_dir" "$stow_bin" \
|
|
||||||
--no-shell-setup >/dev/null
|
|
||||||
|
|
||||||
[ ! -L "$home_dir/.config/proxy-client/config" ] \
|
|
||||||
|| fail "Machine config must not become a symlink"
|
|
||||||
[ ! -L "$home_dir/.config/proxy-client/default.url" ] \
|
|
||||||
|| fail "Private profile must not become a symlink"
|
|
||||||
grep -Fq 'HOME_CONFIG_SENTINEL' "$home_dir/.config/proxy-client/config" \
|
|
||||||
|| fail "Machine config changed"
|
|
||||||
grep -Fq 'HOME_PROFILE_SENTINEL' "$home_dir/.config/proxy-client/default.url" \
|
|
||||||
|| fail "Private profile changed"
|
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
test_accepts_existing_links_when_method_changes() {
|
# Копия пакета в пути с пробелами и машинными файлами, которые установщик
|
||||||
local fixture_root package_dir home_dir
|
# не должен трогать. Печатает путь к пакету; HOME создаётся рядом.
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
new_fixture() {
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
local fixture package_dir
|
||||||
home_dir="$fixture_root/home"
|
fixture="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
||||||
mkdir -p "$home_dir"
|
package_dir="$fixture/source with spaces/proxy-client"
|
||||||
|
mkdir -p "$package_dir" "$fixture/home"
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow \
|
cp -a "$REPO_ROOT/proxy-client/." "$package_dir/"
|
||||||
--no-shell-setup >/dev/null
|
printf 'MACHINE_CONFIG_SENTINEL\n' > "$package_dir/.config/proxy-client/config"
|
||||||
run_package_install "$package_dir" "$home_dir" stow \
|
printf 'PRIVATE_PROFILE_SENTINEL\n' > "$package_dir/.config/proxy-client/default.url"
|
||||||
--no-shell-setup >/dev/null
|
printf '%s\n' "$fixture"
|
||||||
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.local/bin/proxy-client" \
|
|
||||||
"$package_dir/.local/bin/proxy-client"
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.config/proxy-client/shell.bash" \
|
|
||||||
"$package_dir/.config/proxy-client/shell.bash"
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.config/proxy-client/examples" \
|
|
||||||
"$package_dir/.config/proxy-client/examples"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test_stow_completes_partial_correct_installation() {
|
run_install() {
|
||||||
local fixture_root package_dir home_dir
|
local fixture="$1"
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
shift
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
(cd "$TEST_TMP_ROOT" && HOME="$fixture/home" \
|
||||||
home_dir="$fixture_root/home"
|
"$fixture/source with spaces/proxy-client/install.sh" "$@")
|
||||||
mkdir -p "$home_dir"
|
|
||||||
|
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow \
|
|
||||||
--no-shell-setup >/dev/null
|
|
||||||
rm "$home_dir/.config/proxy-client/shell.bash"
|
|
||||||
|
|
||||||
run_package_install "$package_dir" "$home_dir" stow \
|
|
||||||
--no-shell-setup >/dev/null
|
|
||||||
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.local/bin/proxy-client" \
|
|
||||||
"$package_dir/.local/bin/proxy-client"
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.config/proxy-client/shell.bash" \
|
|
||||||
"$package_dir/.config/proxy-client/shell.bash"
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.config/proxy-client/examples" \
|
|
||||||
"$package_dir/.config/proxy-client/examples"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test_does_not_duplicate_full_dotfiles_shell_setup() {
|
home_snapshot() {
|
||||||
local fixture_root package_dir home_dir source_count
|
find "$1" -printf '%p %y %l %s %T@\n' | sort
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
|
||||||
home_dir="$fixture_root/home"
|
|
||||||
mkdir -p "$home_dir"
|
|
||||||
printf '%s\n' \
|
|
||||||
'[ -f ~/.config/proxy-client/shell.bash ] && source ~/.config/proxy-client/shell.bash' \
|
|
||||||
> "$home_dir/.bash_custom"
|
|
||||||
printf '%s\n' '[ -f ~/.bash_custom ] && source ~/.bash_custom' \
|
|
||||||
> "$home_dir/.bashrc"
|
|
||||||
|
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow >/dev/null
|
|
||||||
|
|
||||||
grep -Fq '# >>> proxy-client >>>' "$home_dir/.bashrc" \
|
|
||||||
|| fail "Expected managed PATH block with full dotfiles"
|
|
||||||
grep -Fq 'export PATH="$HOME/.local/bin:$PATH"' "$home_dir/.bashrc" \
|
|
||||||
|| fail "Expected ~/.local/bin setup with full dotfiles"
|
|
||||||
source_count="$(grep -Fc 'proxy-client/shell.bash' "$home_dir/.bashrc" || true)"
|
|
||||||
[ "$source_count" -eq 0 ] \
|
|
||||||
|| fail "Standalone block duplicated full dotfiles module source"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test_rejects_damaged_managed_bash_block_before_changes() {
|
test_fresh_install() {
|
||||||
local fixture_root package_dir home_dir output
|
local fixture home_dir package_dir output
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
fixture="$(new_fixture)"
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
home_dir="$fixture/home"
|
||||||
home_dir="$fixture_root/home"
|
package_dir="$fixture/source with spaces/proxy-client"
|
||||||
mkdir -p "$home_dir"
|
|
||||||
printf '%s\n' '# >>> proxy-client >>>' > "$home_dir/.bashrc"
|
|
||||||
|
|
||||||
if output="$(
|
run_install "$fixture" >/dev/null
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow 2>&1
|
|
||||||
)"; then
|
assert_package_links "$package_dir" "$home_dir"
|
||||||
fail "Expected damaged managed Bash block to be rejected"
|
[ -x "$home_dir/.local/bin/proxy-client" ] || fail "CLI не исполняемый"
|
||||||
|
assert_not_exists "$home_dir/.config/proxy-client/config"
|
||||||
|
assert_not_exists "$home_dir/.config/proxy-client/default.url"
|
||||||
|
output="$(
|
||||||
|
env -i HOME="$home_dir" PATH=/usr/bin:/bin TERM=dumb bash -ic '
|
||||||
|
command -v proxy-client && declare -F proxy_on
|
||||||
|
' 2>/dev/null
|
||||||
|
)"
|
||||||
|
[ "$output" = "$home_dir/.local/bin/proxy-client"$'\n'proxy_on ] \
|
||||||
|
|| fail "новый Bash не нашёл proxy-client и proxy_on: $output"
|
||||||
|
ok "свежая установка: три ссылки и рабочий блок в ~/.bashrc"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_repeat_changes_nothing() {
|
||||||
|
local fixture home_dir before
|
||||||
|
fixture="$(new_fixture)"
|
||||||
|
home_dir="$fixture/home"
|
||||||
|
run_install "$fixture" >/dev/null
|
||||||
|
before="$(home_snapshot "$home_dir")"
|
||||||
|
|
||||||
|
run_install "$fixture" >/dev/null
|
||||||
|
|
||||||
|
[ "$(home_snapshot "$home_dir")" = "$before" ] \
|
||||||
|
|| fail "повторный запуск изменил HOME"
|
||||||
|
ok "повторный запуск ничего не меняет"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_conflicts_block_whole_install() {
|
||||||
|
local fixture home_dir output
|
||||||
|
fixture="$(new_fixture)"
|
||||||
|
home_dir="$fixture/home"
|
||||||
|
mkdir -p "$home_dir/.local/bin" "$home_dir/.config/proxy-client"
|
||||||
|
printf 'KEEP_CLI\n' > "$home_dir/.local/bin/proxy-client"
|
||||||
|
ln -s "$fixture/missing" "$home_dir/.config/proxy-client/examples"
|
||||||
|
|
||||||
|
if output="$(run_install "$fixture" 2>&1)"; then
|
||||||
|
fail "установка прошла поверх чужих файлов"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf '%s' "$output" | grep -Fq "$home_dir/.bashrc" \
|
printf '%s' "$output" | grep -Fq "$home_dir/.local/bin/proxy-client" \
|
||||||
|| fail "Expected ~/.bashrc conflict in output"
|
|| fail "не назван конфликт CLI"
|
||||||
[ "$(wc -l < "$home_dir/.bashrc")" -eq 1 ] \
|
printf '%s' "$output" | grep -Fq "$home_dir/.config/proxy-client/examples" \
|
||||||
|| fail "Expected damaged Bash block to stay unchanged"
|
|| fail "не названа битая ссылка"
|
||||||
assert_not_exists "$home_dir/.local"
|
grep -Fqx KEEP_CLI "$home_dir/.local/bin/proxy-client" \
|
||||||
assert_not_exists "$home_dir/.config"
|
|| fail "чужой CLI изменён"
|
||||||
}
|
[ "$(readlink "$home_dir/.config/proxy-client/examples")" = "$fixture/missing" ] \
|
||||||
|
|| fail "битая ссылка изменена"
|
||||||
test_does_not_duplicate_existing_local_bin_path() {
|
assert_not_exists "$home_dir/.config/proxy-client/shell.bash"
|
||||||
local fixture_root package_dir home_dir path_count source_count
|
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
|
||||||
home_dir="$fixture_root/home"
|
|
||||||
mkdir -p "$home_dir"
|
|
||||||
printf '%s\n' 'export PATH="$HOME/.local/bin:$PATH"' > "$home_dir/.bashrc"
|
|
||||||
|
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow >/dev/null
|
|
||||||
|
|
||||||
path_count="$(grep -Fc 'export PATH="$HOME/.local/bin:$PATH"' "$home_dir/.bashrc")"
|
|
||||||
source_count="$(grep -Fc 'proxy-client/shell.bash' "$home_dir/.bashrc")"
|
|
||||||
[ "$path_count" -eq 1 ] || fail "Existing ~/.local/bin PATH line was duplicated"
|
|
||||||
[ "$source_count" -eq 1 ] || fail "Expected shell module source in managed block"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_rejects_incomplete_source_before_changes() {
|
|
||||||
local fixture_root package_dir home_dir output
|
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
|
||||||
home_dir="$fixture_root/home"
|
|
||||||
mkdir -p "$home_dir"
|
|
||||||
rm "$package_dir/.config/proxy-client/shell.bash"
|
|
||||||
|
|
||||||
if output="$(
|
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow 2>&1
|
|
||||||
)"; then
|
|
||||||
fail "Expected incomplete source package to be rejected"
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s' "$output" | grep -Fq 'исходного пакета' \
|
|
||||||
|| fail "Expected source package error"
|
|
||||||
assert_not_exists "$home_dir/.local"
|
|
||||||
assert_not_exists "$home_dir/.config"
|
|
||||||
assert_not_exists "$home_dir/.bashrc"
|
assert_not_exists "$home_dir/.bashrc"
|
||||||
|
ok "все конфликты названы сразу, частичной установки нет"
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
test_keeps_machine_files() {
|
||||||
test_installs_same_public_artifacts_without_stow
|
local fixture home_dir
|
||||||
test_configures_bash_once_by_default
|
fixture="$(new_fixture)"
|
||||||
test_installs_same_public_artifacts_with_stow
|
home_dir="$fixture/home"
|
||||||
test_rejects_linked_parent_directories_before_changes
|
mkdir -p "$home_dir/.config/proxy-client"
|
||||||
test_reports_all_target_conflicts_without_partial_install
|
printf 'HOME_CONFIG\n' > "$home_dir/.config/proxy-client/config"
|
||||||
test_rejects_broken_link_without_creating_other_targets
|
printf 'HOME_PROFILE\n' > "$home_dir/.config/proxy-client/default.url"
|
||||||
test_preserves_machine_files_with_both_methods
|
|
||||||
test_accepts_existing_links_when_method_changes
|
run_install "$fixture" --no-shell-setup >/dev/null
|
||||||
test_stow_completes_partial_correct_installation
|
|
||||||
test_does_not_duplicate_full_dotfiles_shell_setup
|
[ ! -L "$home_dir/.config/proxy-client/config" ] || fail "config стал ссылкой"
|
||||||
test_rejects_damaged_managed_bash_block_before_changes
|
[ ! -L "$home_dir/.config/proxy-client/default.url" ] \
|
||||||
test_does_not_duplicate_existing_local_bin_path
|
|| fail "default.url стал ссылкой"
|
||||||
test_rejects_incomplete_source_before_changes
|
grep -Fqx HOME_CONFIG "$home_dir/.config/proxy-client/config" \
|
||||||
echo "All standalone proxy-client install tests passed"
|
|| fail "машинный config изменён"
|
||||||
|
grep -Fqx HOME_PROFILE "$home_dir/.config/proxy-client/default.url" \
|
||||||
|
|| fail "default.url изменён"
|
||||||
|
ok "машинные config и default.url не трогаются"
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
test_no_shell_setup() {
|
||||||
|
local fixture home_dir output
|
||||||
|
fixture="$(new_fixture)"
|
||||||
|
home_dir="$fixture/home"
|
||||||
|
|
||||||
|
output="$(run_install "$fixture" --no-shell-setup)"
|
||||||
|
|
||||||
|
assert_not_exists "$home_dir/.bashrc"
|
||||||
|
printf '%s' "$output" | grep -Fq 'proxy-client/shell.bash' \
|
||||||
|
|| fail "нет подсказки для текущей сессии"
|
||||||
|
ok "--no-shell-setup не трогает ~/.bashrc"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_linked_parent_directories() {
|
||||||
|
local fixture home_dir package_dir
|
||||||
|
fixture="$(new_fixture)"
|
||||||
|
home_dir="$fixture/home"
|
||||||
|
package_dir="$fixture/source with spaces/proxy-client"
|
||||||
|
mkdir -p "$fixture/external-local" "$fixture/external-config"
|
||||||
|
ln -s "$fixture/external-local" "$home_dir/.local"
|
||||||
|
ln -s "$fixture/external-config" "$home_dir/.config"
|
||||||
|
|
||||||
|
run_install "$fixture" --no-shell-setup >/dev/null
|
||||||
|
|
||||||
|
assert_package_links "$package_dir" "$home_dir"
|
||||||
|
[ -L "$fixture/external-local/bin/proxy-client" ] \
|
||||||
|
|| fail "ссылка не попала в каталог, на который указывает ~/.local"
|
||||||
|
ok "каталоги ~/.local и ~/.config могут быть ссылками"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_fresh_install
|
||||||
|
test_repeat_changes_nothing
|
||||||
|
test_conflicts_block_whole_install
|
||||||
|
test_keeps_machine_files
|
||||||
|
test_no_shell_setup
|
||||||
|
test_linked_parent_directories
|
||||||
|
echo "PASS: тесты установщика proxy-client"
|
||||||
+329
-1061
File diff suppressed because it is too large.
Load diff
+85
-131
@@ -1,12 +1,19 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# Тесты Bash-функций proxy-client: вместо настоящего CLI — поддельный
|
||||||
|
# исполняемый файл в PROXY_CLIENT_BIN.
|
||||||
|
# Строчные proxy-переменные задаёт proxy_on, shellcheck этого не видит.
|
||||||
|
# shellcheck disable=SC2154
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
FUNCTIONS_FILE="$REPO_ROOT/bash/.bash_functions"
|
FUNCTIONS_FILE="$REPO_ROOT/bash/.bash_functions"
|
||||||
CUSTOM_FILE="$REPO_ROOT/bash/.bash_custom"
|
CUSTOM_FILE="$REPO_ROOT/bash/.bash_custom"
|
||||||
SHELL_MODULE="$REPO_ROOT/proxy-client/.config/proxy-client/shell.bash"
|
SHELL_MODULE="$REPO_ROOT/proxy-client/.config/proxy-client/shell.bash"
|
||||||
|
TEST_ROOT="$(mktemp -d /tmp/proxy-launchers-test.XXXXXX)"
|
||||||
|
trap 'rm -rf "$TEST_ROOT"' EXIT
|
||||||
|
|
||||||
pass_count=0
|
PROXY_KEYS=(HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY
|
||||||
|
http_proxy https_proxy all_proxy no_proxy)
|
||||||
|
|
||||||
fail() {
|
fail() {
|
||||||
echo "FAIL: $*" >&2
|
echo "FAIL: $*" >&2
|
||||||
@@ -14,177 +21,124 @@ fail() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ok() {
|
ok() {
|
||||||
pass_count=$((pass_count + 1))
|
|
||||||
echo "ok - $*"
|
echo "ok - $*"
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq() {
|
assert_eq() {
|
||||||
local expected="$1" actual="$2" message="$3"
|
[ "$1" = "$2" ] || fail "$3 (ожидалось '$1', получено '$2')"
|
||||||
[ "$expected" = "$actual" ] || fail "$message (ожидалось '$expected', получено '$actual')"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
valid_env() {
|
# Поддельный CLI: на "env" печатает $FAKE_ENV_OUTPUT, на остальное — свои
|
||||||
cat <<'EOF'
|
# аргументы по одному в строке; завершается с кодом $FAKE_RC.
|
||||||
HTTP_PROXY=http://proxy.test:1234
|
PROXY_CLIENT_BIN="$TEST_ROOT/proxy-client"
|
||||||
|
cat > "$PROXY_CLIENT_BIN" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
if [ "$1" = env ]; then
|
||||||
|
printf '%s\n' "$FAKE_ENV_OUTPUT"
|
||||||
|
else
|
||||||
|
printf '<%s>\n' "$@"
|
||||||
|
fi
|
||||||
|
exit "${FAKE_RC:-0}"
|
||||||
|
EOF
|
||||||
|
chmod +x "$PROXY_CLIENT_BIN"
|
||||||
|
export PROXY_CLIENT_BIN
|
||||||
|
|
||||||
|
VALID_ENV='HTTP_PROXY=http://proxy.test:1234
|
||||||
HTTPS_PROXY=http://proxy.test:1234
|
HTTPS_PROXY=http://proxy.test:1234
|
||||||
ALL_PROXY=socks5://proxy.test:1234
|
ALL_PROXY=socks5://proxy.test:1234
|
||||||
NO_PROXY=proxy.test,localhost,127.0.0.1,::1,[::1],.example.test
|
NO_PROXY=proxy.test,localhost,.example.test
|
||||||
http_proxy=http://proxy.test:1234
|
http_proxy=http://proxy.test:1234
|
||||||
https_proxy=http://proxy.test:1234
|
https_proxy=http://proxy.test:1234
|
||||||
all_proxy=socks5://proxy.test:1234
|
all_proxy=socks5://proxy.test:1234
|
||||||
no_proxy=proxy.test,localhost,127.0.0.1,::1,[::1],.example.test
|
no_proxy=proxy.test,localhost,.example.test'
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_five_proxy_commands() {
|
# shellcheck source=/dev/null
|
||||||
local command_name
|
source "$SHELL_MODULE"
|
||||||
for command_name in proxy_on proxy_off pclaude pcodex myip; do
|
|
||||||
declare -F "$command_name" >/dev/null || fail "нет функции $command_name"
|
# proxy_on применяет все восемь переменных.
|
||||||
|
(
|
||||||
|
unset "${PROXY_KEYS[@]}"
|
||||||
|
FAKE_ENV_OUTPUT="$VALID_ENV" proxy_on >/dev/null
|
||||||
|
assert_eq http://proxy.test:1234 "$HTTP_PROXY" HTTP_PROXY
|
||||||
|
assert_eq http://proxy.test:1234 "$https_proxy" https_proxy
|
||||||
|
assert_eq socks5://proxy.test:1234 "$ALL_PROXY" ALL_PROXY
|
||||||
|
assert_eq proxy.test,localhost,.example.test "$no_proxy" no_proxy
|
||||||
|
for key in "${PROXY_KEYS[@]}"; do
|
||||||
|
[ -n "$(printenv "$key")" ] || fail "$key не экспортирован"
|
||||||
done
|
done
|
||||||
}
|
)
|
||||||
|
ok "proxy_on применяет восемь переменных"
|
||||||
|
|
||||||
# Самостоятельный модуль предоставляет весь публичный Bash-интерфейс.
|
# Ошибка CLI или непонятный вывод не меняют окружение ни на одну переменную.
|
||||||
(
|
for case_spec in "1|$VALID_ENV" "0|$VALID_ENV
|
||||||
source "$SHELL_MODULE"
|
EXTRA_PROXY=bad" "0|BROKEN-LINE"; do
|
||||||
assert_five_proxy_commands
|
|
||||||
) && ok "самостоятельное подключение предоставляет пять proxy-команд"
|
|
||||||
|
|
||||||
# Обычная .bash_custom независимо подключает общие функции и proxy-модуль.
|
|
||||||
(
|
|
||||||
fixture="$(mktemp -d /tmp/proxy-bash-custom-test.XXXXXX)"
|
|
||||||
mkdir -p "$fixture/.config/proxy-client"
|
|
||||||
ln -s "$CUSTOM_FILE" "$fixture/.bash_custom"
|
|
||||||
ln -s "$FUNCTIONS_FILE" "$fixture/.bash_functions"
|
|
||||||
ln -s "$SHELL_MODULE" "$fixture/.config/proxy-client/shell.bash"
|
|
||||||
output="$(
|
|
||||||
HOME="$fixture" bash --noprofile --norc -ic '
|
|
||||||
source "$HOME/.bash_custom"
|
|
||||||
for name in proxy_on proxy_off pclaude pcodex myip update_hproxy; do
|
|
||||||
declare -F "$name" >/dev/null || exit 1
|
|
||||||
done
|
|
||||||
printf ready
|
|
||||||
' 2>/dev/null
|
|
||||||
)"
|
|
||||||
assert_eq ready "$output" ".bash_custom не подключил оба независимых модуля"
|
|
||||||
) && ok "обычное подключение предоставляет proxy-команды и update_hproxy"
|
|
||||||
|
|
||||||
# proxy_on применяет восемь известных переменных и не запрашивает IP.
|
|
||||||
(
|
|
||||||
source "$SHELL_MODULE"
|
|
||||||
_proxy_cli() {
|
|
||||||
[ "$1" = env ] || return 99
|
|
||||||
valid_env
|
|
||||||
}
|
|
||||||
curl() { fail "proxy_on не должен вызывать curl"; }
|
|
||||||
unset HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY \
|
|
||||||
http_proxy https_proxy all_proxy no_proxy || true
|
|
||||||
proxy_on >/dev/null
|
|
||||||
assert_eq "http://proxy.test:1234" "$HTTP_PROXY" "HTTP_PROXY"
|
|
||||||
assert_eq "$HTTP_PROXY" "$http_proxy" "http_proxy"
|
|
||||||
assert_eq "http://proxy.test:1234" "$HTTPS_PROXY" "HTTPS_PROXY"
|
|
||||||
assert_eq "$HTTPS_PROXY" "$https_proxy" "https_proxy"
|
|
||||||
assert_eq "socks5://proxy.test:1234" "$ALL_PROXY" "ALL_PROXY"
|
|
||||||
assert_eq "$ALL_PROXY" "$all_proxy" "all_proxy"
|
|
||||||
assert_eq "proxy.test,localhost,127.0.0.1,::1,[::1],.example.test" "$NO_PROXY" "NO_PROXY"
|
|
||||||
assert_eq "$NO_PROXY" "$no_proxy" "no_proxy"
|
|
||||||
) && ok "proxy_on применяет известные ключи без запроса IP"
|
|
||||||
|
|
||||||
# Неизвестная строка в машинном выводе оставляет прежнее окружение целиком.
|
|
||||||
unknown_output="$(valid_env; printf 'EXTRA_PROXY=bad\n')"
|
|
||||||
for bad_output in \
|
|
||||||
"$unknown_output" \
|
|
||||||
"BROKEN-LINE"; do
|
|
||||||
(
|
(
|
||||||
source "$SHELL_MODULE"
|
for key in "${PROXY_KEYS[@]}"; do export "$key=old"; done
|
||||||
export HTTP_PROXY=old-http HTTPS_PROXY=old-https ALL_PROXY=old-all NO_PROXY=old-no
|
if FAKE_RC="${case_spec%%|*}" FAKE_ENV_OUTPUT="${case_spec#*|}" \
|
||||||
export http_proxy=old-http-lower https_proxy=old-https-lower
|
proxy_on >/dev/null 2>&1; then
|
||||||
export all_proxy=old-all-lower no_proxy=old-no-lower
|
fail "proxy_on принял ошибочный ответ: ${case_spec#*|}"
|
||||||
_proxy_cli() { printf '%s\n' "$bad_output"; }
|
|
||||||
if proxy_on >/dev/null 2>&1; then
|
|
||||||
fail "proxy_on принял неверный вывод"
|
|
||||||
fi
|
fi
|
||||||
assert_eq old-http "$HTTP_PROXY" "HTTP_PROXY изменился при ошибке"
|
for key in "${PROXY_KEYS[@]}"; do
|
||||||
assert_eq old-https "$HTTPS_PROXY" "HTTPS_PROXY изменился при ошибке"
|
assert_eq old "${!key}" "$key изменился после ошибки"
|
||||||
assert_eq old-all "$ALL_PROXY" "ALL_PROXY изменился при ошибке"
|
done
|
||||||
assert_eq old-no "$NO_PROXY" "NO_PROXY изменился при ошибке"
|
)
|
||||||
assert_eq old-http-lower "$http_proxy" "http_proxy изменился при ошибке"
|
|
||||||
assert_eq old-https-lower "$https_proxy" "https_proxy изменился при ошибке"
|
|
||||||
assert_eq old-all-lower "$all_proxy" "all_proxy изменился при ошибке"
|
|
||||||
assert_eq old-no-lower "$no_proxy" "no_proxy изменился при ошибке"
|
|
||||||
) || fail "атомарность нарушена для неверного вывода"
|
|
||||||
done
|
done
|
||||||
ok "неизвестная env-строка отклоняется без частичного изменения окружения"
|
ok "неуспешный env не меняет окружение"
|
||||||
|
|
||||||
# Даже полный stdout не применяется, если CLI завершился с ошибкой.
|
# proxy_off очищает оба регистра.
|
||||||
(
|
(
|
||||||
source "$SHELL_MODULE"
|
for key in "${PROXY_KEYS[@]}"; do export "$key=x"; done
|
||||||
export HTTP_PROXY=old HTTPS_PROXY=old ALL_PROXY=old NO_PROXY=old
|
proxy_off >/dev/null
|
||||||
export http_proxy=old https_proxy=old all_proxy=old no_proxy=old
|
for key in "${PROXY_KEYS[@]}"; do
|
||||||
_proxy_cli() {
|
[ -z "${!key+set}" ] || fail "$key не очищен"
|
||||||
valid_env
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if proxy_on >/dev/null 2>&1; then
|
|
||||||
fail "proxy_on принял вывод неуспешного CLI"
|
|
||||||
fi
|
|
||||||
for key in HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY \
|
|
||||||
http_proxy https_proxy all_proxy no_proxy; do
|
|
||||||
assert_eq old "${!key}" "$key изменился после ошибки CLI"
|
|
||||||
done
|
done
|
||||||
) && ok "stdout неуспешного env не изменяет окружение"
|
)
|
||||||
|
ok "proxy_off очищает восемь переменных"
|
||||||
|
|
||||||
# Обёртки передают CLI команду, аргументы и код возврата без искажений.
|
# Обёртки передают CLI команду, аргументы и код возврата без искажений.
|
||||||
(
|
(
|
||||||
source "$SHELL_MODULE"
|
export FAKE_RC=42
|
||||||
_proxy_cli() {
|
|
||||||
printf '<%s>\n' "$@"
|
|
||||||
return 42
|
|
||||||
}
|
|
||||||
set +e
|
set +e
|
||||||
output="$(pclaude "a b" -- --flag)"
|
output="$(pclaude "a b" -- --flag)"
|
||||||
rc=$?
|
rc=$?
|
||||||
set -e
|
set -e
|
||||||
assert_eq 42 "$rc" "pclaude не сохранил код возврата"
|
assert_eq 42 "$rc" "pclaude не сохранил код возврата"
|
||||||
assert_eq $'<run>\n<-->\n<claude>\n<a b>\n<-->\n<--flag>' "$output" "pclaude исказил аргументы"
|
assert_eq $'<run>\n<-->\n<claude>\n<a b>\n<-->\n<--flag>' "$output" \
|
||||||
|
"pclaude исказил аргументы"
|
||||||
|
|
||||||
set +e
|
set +e
|
||||||
output="$(pcodex --search)"
|
output="$(pcodex --search)"
|
||||||
rc=$?
|
rc=$?
|
||||||
set -e
|
set -e
|
||||||
assert_eq 42 "$rc" "pcodex не сохранил код возврата"
|
assert_eq 42 "$rc" "pcodex не сохранил код возврата"
|
||||||
assert_eq $'<run>\n<-->\n<codex>\n<--search>' "$output" "pcodex исказил аргументы"
|
assert_eq $'<run>\n<-->\n<codex>\n<--search>' "$output" \
|
||||||
) && ok "pclaude и pcodex делегируют run и сохраняют аргументы/код"
|
"pcodex исказил аргументы"
|
||||||
|
)
|
||||||
|
ok "pclaude и pcodex передают аргументы и код выхода"
|
||||||
|
|
||||||
# proxy_off очищает оба регистра и не смешивает действие с диагностикой.
|
# .bash_custom подключает и общие функции, и proxy-модуль.
|
||||||
(
|
(
|
||||||
source "$SHELL_MODULE"
|
home="$TEST_ROOT/home"
|
||||||
export HTTP_PROXY=x HTTPS_PROXY=x ALL_PROXY=x NO_PROXY=x
|
mkdir -p "$home/.config/proxy-client"
|
||||||
export http_proxy=x https_proxy=x all_proxy=x no_proxy=x
|
ln -s "$CUSTOM_FILE" "$home/.bash_custom"
|
||||||
curl() { fail "proxy_off не должен вызывать curl"; }
|
ln -s "$FUNCTIONS_FILE" "$home/.bash_functions"
|
||||||
proxy_off >/dev/null
|
ln -s "$SHELL_MODULE" "$home/.config/proxy-client/shell.bash"
|
||||||
for key in HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY \
|
output="$(
|
||||||
http_proxy https_proxy all_proxy no_proxy; do
|
HOME="$home" bash --noprofile --norc -ic '
|
||||||
[ -z "${!key+x}" ] || fail "$key не очищен"
|
source "$HOME/.bash_custom"
|
||||||
done
|
declare -F proxy_on update_hproxy >/dev/null && printf ready
|
||||||
) && ok "proxy_off очищает восемь переменных без запроса IP"
|
' 2>/dev/null
|
||||||
|
)"
|
||||||
# Общий файл больше не владеет proxy-интерфейсом, но сохраняет update_hproxy.
|
assert_eq ready "$output" ".bash_custom не подключил модули"
|
||||||
(
|
)
|
||||||
source "$FUNCTIONS_FILE"
|
ok ".bash_custom подключает общие функции и proxy-модуль"
|
||||||
declare -F update_hproxy >/dev/null || fail "update_hproxy потерян"
|
|
||||||
for name in proxy_on proxy_off pclaude pcodex myip _proxy_run _proxy_cli; do
|
|
||||||
if declare -F "$name" >/dev/null; then
|
|
||||||
fail "$name всё ещё определён в bash/.bash_functions"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
) && ok "владение proxy-функциями перенесено, update_hproxy сохранён"
|
|
||||||
|
|
||||||
if command -v shellcheck >/dev/null 2>&1; then
|
if command -v shellcheck >/dev/null 2>&1; then
|
||||||
shellcheck -s bash "$FUNCTIONS_FILE" "$CUSTOM_FILE" "$SHELL_MODULE" "$0" \
|
shellcheck -s bash "$SHELL_MODULE" "$0" \
|
||||||
|| fail "shellcheck нашёл проблемы"
|
|| fail "shellcheck нашёл проблемы"
|
||||||
ok "shellcheck чист"
|
ok "shellcheck чист"
|
||||||
else
|
else
|
||||||
echo "skip - shellcheck не установлен"
|
echo "skip - shellcheck не установлен"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "PASS: $pass_count проверок пройдено"
|
echo "PASS: тесты Bash-функций proxy-client"
|
||||||
Reference in new issue
Block a user