opencode-orchestrator-uni 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,482 @@
1
+ # Справочник
2
+
3
+ Подробный справочник комплекта оркестратора: защита записи, пять инструментов,
4
+ протокол сессии, настройка моделей и локали, правила приоритета.
5
+
6
+ [Назад в README](../../README.ru.md)
7
+
8
+ Комплект регистрируется при setup через `ctx.agent.transform` /
9
+ `ctx.reference.transform`. Reference `audit-kit` указывает на per-locale каталог
10
+ `docs/<locale>/` пакета; OpenCode перечисляет его как дополнительный локальный
11
+ каталог, поэтому агенты читают `<reference path>/call-mechanics.md` напрямую.
12
+ Поставляются `call-mechanics.md`, `process-ready-loop.md`, `root-runbook.md`,
13
+ `instruction-other-session.md`, `context-efficiency.md` и
14
+ `session-discipline.md` — например,
15
+ `<reference path>/process-ready-loop.md`.
16
+
17
+ ## Защита записи
18
+
19
+ У самого оркестратора `edit` запрещён на `*`. Плоские `permissions` — лишь
20
+ узкий fallback-allowlist для артефактов аудита/состояния, а реальный guard —
21
+ hook `permission.hook("evaluate")` в `lib/write-guard.ts` с path-root
22
+ семантикой.
23
+
24
+ Аудиторы read-only на уровне инструментов для `edit`/`write`/`patch`
25
+ (`edit: deny`); промпт запрещает остальную запись. Запись через shell (redirect,
26
+ `sed -i`, `tee`) — принятый риск (I-M15), а не песочница.
27
+
28
+ - Каталог сессии читается на каждое событие (`session.get`), поэтому
29
+ перемещённая сессия оценивается от нового каталога. При ошибке чтения
30
+ используется setup-каталог и пишется warning — fail-open.
31
+ - Ресурс вне каталога сессии — deny: guard и есть граница workspace, поэтому
32
+ `reports/`, `docs/planning/` и `audit-*` вне её не пишутся. Внутри каталога
33
+ первый **корневой** сегмент решает.
34
+ - `reports/` — зарезервированный artifact-root: всё поддерево доступно для
35
+ записи (даже `reports/src/x.ts`), поэтому проектный код туда не кладём (путь
36
+ `packages/reports/**` по контракту тоже считается артефактом).
37
+ `docs/planning/**/*.md` разрешён (вложенный Markdown на любой глубине; `.ts` —
38
+ нет). `docs` — отдельная ветвь, не source-root: остальной `docs/**` запрещён.
39
+ - Source-корни (`src`, `lib`, `test`, `tests`, `agents`, `tools`, `schemas`,
40
+ `eval`, `.github`) запрещены даже под артефактным префиксом
41
+ (`src/reports/x.ts`, `src/docs/planning/x.md`).
42
+ - Корневые artifact-файлы (`case-*`, `risks-*`, `handoff-*`, `decisions-*`,
43
+ `state.md`) разрешены только в корне location, под `reports/`,
44
+ `docs/planning/` или в каталоге `audit-*` — `app/state.md` запрещён.
45
+ - Hook выполняется после плоских правил, поэтому локальный `edit: allow` для
46
+ `orchestrator` тоже закрыт. Плоский fallback несёт только этот артефактный
47
+ набор root-правилами (`reports/*`, `case-*.md`, `risks-*.md`, `handoff-*.md`,
48
+ `decisions-*.md`, `state.md`, `docs/planning/*.md`) плюс `audit-*/…`; широкие
49
+ `*/state.md`-шаблоны убраны, поэтому `src/state.md` запрещён и без hook, но
50
+ last-match всё равно не выражает приоритет корней везде
51
+ (`audit-x/src/case-y.md`) — плагин пишет warning.
52
+ - Fallback уже hook'а для вложенных артефактных корней: `lab/x/reports/*` и
53
+ подобные пути пишутся только с hook (root-правила покрывают лишь `reports/*`
54
+ от корня location).
55
+ - Сбой или таймаут чтения сессии (граница 250 мс) даёт fallback на
56
+ setup-каталог и warning один раз на guard; у перемещённой сессии абсолютный
57
+ артефакт в новом каталоге тогда может получить deny (fail-closed для этого
58
+ класса). Один флаг `sessionWarned` без счётчика — принято: сбои после первого
59
+ warning молчат.
60
+ - `shell` остаётся для git и проверок. Запись через shell и symlink/junction
61
+ (хост матчит нормализованный лексический путь, а не `realpath`) — принятые
62
+ риски, как и fail-open (сломанный guard не роняет сессию); ими управляет
63
+ промпт, а не песочница. Отказ инструмента — это правило роли, правка уходит
64
+ субагенту.
65
+
66
+ ## Инструменты
67
+
68
+ Плагин регистрирует пять детерминированных **read-only** инструментов в
69
+ пространстве имён `orch_uni`.
70
+
71
+ | Имя | Read-only | Что делает |
72
+ | -------------------------- | --------- | ----------------------------------------------------------------------------- |
73
+ | `orch_uni_validate_report` | да | Достаёт SGR JSON-блок отчёта, разрешает его схему, валидирует |
74
+ | `orch_uni_context_lint` | да | Детерминированный аудит AGENTS.md/SPEC.md: бюджет, vague, ссылки, секции |
75
+ | `orch_uni_snapshot` | да | Детерминированная ревизия дерева каталогов (sha256 на файл + агрегатный хэш) |
76
+ | `orch_uni_sessions` | да | Дочерние сессии сессии: агент, модель, стоимость, токены, время |
77
+ | `orch_uni_session_stats` | да | Счётчики протокола сессии: промпт, компакции, тулы/ход, урезанные выводы |
78
+
79
+ Каждый инструмент отвечает JSON-текстом. Ошибки — **типизированные коды** внутри
80
+ тела (`E_INPUT`, `E_NO_JSON`, …), а не исключения, чтобы агент мог ветвиться по
81
+ коду, не разбирая прозу. Ни один из пяти ничего не пишет в воркспейс, хранилище
82
+ сессий или конфигурацию.
83
+
84
+ ### `orch_uni_validate_report`
85
+
86
+ ```jsonc
87
+ { "text": "<текст отчёта>" } // или
88
+ { "path": "reports/empiric-r1.md", // абсолютный или относительно каталога локации
89
+ "schema": "empiric/v1" } // опционально: ожидаемый id схемы
90
+ ```
91
+
92
+ Укажите ровно одно из `text` / `path`. Инструмент берёт **последний** fenced-блок ```` ```json ````,
93
+ который парсится и несёт поле `schema`, разрешает схему по этому полю и валидирует документ.
94
+
95
+ ```jsonc
96
+ { "valid": true, "schema": "empiric/v1", "errors": [], "warnings": [ … ] }
97
+ { "valid": false, "code": "E_INVALID", "schema": "empiric/v1",
98
+ "errors": [ { "path": "$.findings[0].severity", "message": "expected one of …" } ],
99
+ "warnings": [] }
100
+ ```
101
+
102
+ | Код | Значение |
103
+ | ------------------- | -------------------------------------------------------------------- |
104
+ | `E_INPUT` | Плохой вызов: нет/оба `text`/`path`, неверные типы, нечитаемый файл |
105
+ | `E_NO_JSON` | Fenced-блок ```` ```json ```` не найден |
106
+ | `E_PARSE` | Блок есть, но ни один не парсится как JSON |
107
+ | `E_UNKNOWN_SCHEMA` | Отчёт называет id, которого нет в комплекте (или не называет вовсе) |
108
+ | `E_SCHEMA_MISMATCH` | Отчёт называет схему, отличную от ожидаемой |
109
+ | `E_INVALID` | Документ не удовлетворяет своей схеме (см. `errors`) |
110
+ | `E_SCHEMA_IO` | Схемы комплекта недоступны или битые (см. `directory`, `file`) |
111
+ | `E_ABORTED` | Сессия остановлена во время чтения отчёта |
112
+
113
+ `warnings` несут правила промпта, которые *структурно валидный* отчёт всё ещё
114
+ может нарушить — то, что JSON Schema выразить не может: отсутствующий `rc` у
115
+ claim'а `empiric` с `method` = `run|measure|compare_base`, элементы `self_check`
116
+ вне 1–19, инвариант `invariant`, помеченный `violated` без контрпримера,
117
+ непокрытая обязательная строка `lines_checked`, расхождение `verdict` и `gate` у
118
+ `meta`, инварианты `closing_list`/`verdict` у `redteam`.
119
+
120
+ Схемы лежат в `schemas/*.json` и выведены **строго** из SGR-секций
121
+ `agents/*.md`, которые остаются источником истины: тестовый набор парсит
122
+ SGR-шаблон каждого агента и падает, когда поле промпта или значение enum не
123
+ принимается его схемой. `context-lint-failure/v1` — типизированный конверт ошибки
124
+ `orch_uni_context_lint`: у него нет шаблона агента, и отчётом он не является. Схемы
125
+ разрешают `additionalProperties` — агенты могут
126
+ добавлять свои поля (`verdict_label`, `check`, …).
127
+
128
+ ### `orch_uni_context_lint`
129
+
130
+ ```jsonc
131
+ { "path": "AGENTS.md" } // файл или каталог, относительно локации
132
+ { "path": ".", "max_lines": 150, "max_tokens": 2000 }
133
+ { "path": ".", "max_words": 2000 } // бюджет слов SPEC; по умолчанию 1500 (цель), потолок 2000
134
+ { "path": ".", "max_file_bytes": 500000, "max_files": 100, "max_findings": 50, "max_total_findings": 500 }
135
+ ```
136
+
137
+ Детерминированный read-only аудит `AGENTS.md`, `SPEC.md` и `CLAUDE.md`. Возвращает
138
+ `{ schema: "context-lint/v1", ok: true, verdict, closing_list, files, summary, truncated? }`;
139
+ каждая находка несёт `id`, `severity`, `kind`, `evidence`, `detail` и `fix`. Проверки:
140
+ бюджет строк/токенов/слов, различные vague-фразы, дубли строк, битые относительные ссылки,
141
+ плейсхолдеры, даты/версии в первых трёх строках, обязательные секции и колонка Test
142
+ у AC. Ссылки и явные цели ограничены **реальным** путём: symlink/junction, ведущий за
143
+ пределы корня, пропускается, а такая цель отклоняется (`E_OUT_OF_ROOT`). Код-фенсы,
144
+ inline-код и HTML-комментарии маскируются: пример не считается правилом — структурные
145
+ проверки (команды, границы, проверка) читают полную маску, а требования, AC-id и
146
+ non-goals — маску фенсов. Цели ссылок передаются из построчного сканера по одной
147
+ (массива кандидатов нет), поэтому память не растёт с числом ссылок; проверки ограничены
148
+ на файл 50 000 кандидатами, сигнал проверяется на каждой строке и каждые 100 кандидатов,
149
+ а кэши ссылок держат не более 5 000 записей, поэтому abort срабатывает; 2001 ссылка
150
+ проверяется обычным образом, 50 000 валидных ссылок дают pass, а 50 001-й кандидат делает
151
+ список неполным (`truncated: true` плюс MAJOR-маркер `link candidates truncated` в
152
+ `closing_list`), он не может дать pass.
153
+ Отдельного потолка на
154
+ битые ссылки нет: вывод ограничивают общие лимиты находок, а переполнение помечается.
155
+ Строка сканируется любой длины: сканер ссылок и регулярное выражение плейсхолдеров линейны,
156
+ поэтому 4100-символьная строка проверяется как любая другая, а битая ссылка или
157
+ плейсхолдер на ней — настоящая находка, а не маркер. Внутреннего потолка дублей нет. Цели
158
+ UNC/сетевые пути никогда не пробуются. Замер (bun 1.4.2, Windows, свежий процесс):
159
+ 50 001 битая ссылка с `max_findings: 2` → ~1,4 с и ~67 МБ пикового RSS; 50 тыс.
160
+ обычных строк → ~0,14 с / ~65 МБ; одна строка 2 МБ со ссылками → ~3,1 с / ~116 МБ
161
+ (~41 МБ без ссылок). Маскирование переиспользует строку как есть, если в ней нет
162
+ backtick и `<pre>`/`<code>`/`<!--`, поэтому не копирует строку посимвольно, а строка
163
+ в 2 МБ остаётся ограниченной 50 000 кандидатами, а не материализует все ссылки.
164
+ Лимиты: ≤ 2 МБ на файл, ≤ 500 файлов за прогон. `max_findings` (на файл) — это N на
165
+ severity: сохраняются до N MAJOR и N MINOR/INFO, излишек заменяется одним маркером
166
+ (MAJOR, если выпал MAJOR, иначе INFO); MAJOR-маркер остаётся в `closing_list`.
167
+ `max_total_findings` (глобально, по умолчанию 1000)
168
+ сохраняет сначала MAJOR; при переполнении MAJOR остаются `max_total` находок плюс один
169
+ MAJOR-маркер `truncated`. Усечённый список помечается `truncated: true`, а не скрывается.
170
+ Маркеры дополнительны к `max_findings` и не расходуют его бюджет: маркер усечённых
171
+ кандидатов-ссылок и маркер усечённых находок дают по маркеру в одном прогоне, поэтому
172
+ `max_findings: 2` на 20 001 битой ссылке даёт 2 сохранённые находки плюс 2 маркера. Маркеры
173
+ указывают на `file:1` в `evidence`.
174
+ Non-goals считаются как элементы верхнего уровня: перенесённый абзац — один элемент,
175
+ inline `;`/`,`/`and`/`и` делят только верхний уровень (не внутри `(…)`, `[…]`, бэктиков
176
+ любой длины или `<code>`), а bullet с отступом 0–4 пробела под лейблом — элемент; фенсы,
177
+ `<pre>` и код с большим отступом — нет.
178
+ Плейсхолдеры — `<...>` (три и более точки) и `TODO`/`TBD`/`FIXME`; `<DoD>` и
179
+ `<name>` — разметка.
180
+ `max_lines` — единственный бюджет корня (200 строк), который применяет инструмент;
181
+ цифры 60/200/100 по уровням в [`docs/`](../) — ориентир для авторов, а не отдельные
182
+ лимиты инструмента. `max_words` по умолчанию равен цели 1500 слов SPEC; передайте
183
+ `max_words: 2000`, чтобы применять документированный потолок. Типизированные ошибки: `E_INPUT`, `E_NOT_FOUND`, `E_NO_FILES`,
184
+ `E_IO`, `E_ABORTED`, `E_LIMIT_FILE`, `E_LIMIT_FILES`, `E_OUT_OF_ROOT`.
185
+ Ошибка — типизированный payload `{schema:"context-lint-failure/v1", ok:false, code, message, path?, bytes?, files?, max?}`,
186
+ который валидируется только по `context-lint-failure/v1`; отчёт —
187
+ `{schema:"context-lint/v1", ok, verdict, closing_list, files, summary, truncated?}`, и каждый файл несёт `lines`,
188
+ `words`, `chars` и `tokens_est` (токены оцениваются как кириллица/3 + прочее/4). Каталог
189
+ сканируется до глубины 3, а `max_files` отказывает рано: обход останавливается на первом файле
190
+ выше лимита. Ничего не пропадает молча: каталог ниже лимита глубины, ссылка или junction вне
191
+ корня workspace и незакрытые fence, `<pre>` или HTML-комментарий дают INFO-находки, поэтому `pass`
192
+ не скрывает непроверенную область. Незакрытый `<code>` — строчная маска: он скрывает
193
+ остаток своей строки и не репортится (INFO дают только fence, `<pre>` и HTML-комментарий,
194
+ которые тянутся через строки). Отмена наблюдается между файлами, каждые 2048 строк
195
+ построчной проверки и в каждом гейте, поэтому худшая латентность — один проход маски по
196
+ файлу под `max_file_bytes` (десятки мс на 2 МБ), а не весь скан. Команды считаются только в коде, таблице задач или секции
197
+ Commands, HTML-комментарий вроде `<!-- npm install -->` командой не является, а `make` требует
198
+ цели, поэтому проза «Make sure …» — не команда. Скан от корня подхватывает и фикстуры вроде
199
+ `eval/`; передайте явный путь, если нужны только канонические файлы.
200
+
201
+ ### `orch_uni_snapshot`
202
+
203
+ ```jsonc
204
+ { "path": "lab/stack-test", "exclude": ["tmp"] } // exclude опционален и аддитивен
205
+ ```
206
+
207
+ ```jsonc
208
+ { "root": "C:\\…\\lab\\stack-test",
209
+ "files": [ { "path": "a.txt", "sha256": "…", "bytes": 1 } ],
210
+ "aggregate": "…" }
211
+ ```
212
+
213
+ - `aggregate = sha256` по отсортированным строкам `"<relpath>\0<sha256>\n"`
214
+ (разделители `/`, порядок кодпойнтов) — стабилен между прогонами, машинами и
215
+ порядком обхода и меняется, когда меняется путь или содержимое любого файла.
216
+ - По умолчанию (всегда) исключены: `node_modules`, `.git`, `.ae`; `exclude`
217
+ добавляет имена, целые относительные пути или относительные префиксы
218
+ (`lab/tmp`). Симлинки пропускаются.
219
+ - Лимиты: ≤ 5000 файлов, ≤ 50 МиБ на файл — `E_LIMIT_FILES` / `E_LIMIT_BYTES`
220
+ (падает весь снапшот; усечённый снапшот дал бы неверный агрегат). `E_NOT_FOUND`
221
+ для отсутствующего пути или не-каталога, `E_ABORTED`, когда сессия остановлена.
222
+
223
+ ### `orch_uni_sessions`
224
+
225
+ ```jsonc
226
+ {} // дети вызывающей сессии
227
+ { "sessionID": "ses_…", "recursive": true } // всё поддерево другой сессии
228
+ ```
229
+
230
+ ```jsonc
231
+ { "sessionID": "ses_parent", "recursive": false, "source": "session.index",
232
+ "authoritative": false, "indexSize": 1, "count": 1,
233
+ "limitations": [ … ],
234
+ "sessions": [ { "sessionID": "ses_child", "parentID": "ses_parent", "agent": "explore",
235
+ "model": "deepseek/deepseek-flash", "variant": "default", "title": "…",
236
+ "cost": 0.0005, "tokens": { "input": 3491, "output": 2 },
237
+ "created": "2026-10-15T04:01:06.627Z", "updated": "…" } ] }
238
+ ```
239
+
240
+ `source` говорит, какой маршрут дал ответ:
241
+
242
+ - `session.list` — эта сборка предоставляет API листинга сессий; ответ
243
+ **авторитетный** и полный (`recursive` обходит поддерево постранично).
244
+ - `session.index` — фолбэк, используемый OpenCode 2.0.28: у `ctx.session` есть
245
+ `get/create/…`, но **нет `list`**, поэтому детей нельзя перечислить из
246
+ хранилища. Плагин при setup подписывается на события `session.created` /
247
+ `session.deleted` и хранит `child → parent` в собственном хранилище плагина;
248
+ детали по каждому ребёнку читаются из `ctx.session.get()` в момент вызова.
249
+ Ответ **не авторитетный**; оговорки возвращаются в `limitations` (сессии,
250
+ созданные до старта инстанса плагина, не видны; пропуск событий скрывает
251
+ детей, созданных пока плагин был выгружен).
252
+ - `E_NO_API` — нет ни одного маршрута. Фолбэк через сканирование контекста
253
+ (поиск частей инструмента `subagent` в `ctx.session.context`) был оценён и
254
+ отклонён: у летящих детей нет id сессии в их tool-части, а компакция стирает
255
+ историю. Открытый вопрос: листинг сессий со стороны плагина (или фильтр по
256
+ `parentID` в `ctx.session.context`) убрал бы фолбэк целиком.
257
+
258
+ `E_INPUT` при некорректных аргументах, `E_NOT_FOUND`, когда родительской сессии
259
+ нет, `E_ABORTED`, когда сессия остановлена.
260
+
261
+ ### `orch_uni_session_stats`
262
+
263
+ ```jsonc
264
+ {} // счётчики вызывающей сессии
265
+ { "sessionID": "ses_…" } // любая сессия на этом сервере, не только дочерняя
266
+ ```
267
+
268
+ ```jsonc
269
+ { "ok": true, "sessionID": "ses_…",
270
+ "counters": {
271
+ "requests": 12, "promptTokensLast": 151204, "promptTokensMax": 161002,
272
+ "promptTokensMedian": 148900, "band": 1, "compactions": 1,
273
+ "compactionAttempts": 1, "compactionFailures": 0, "compactionSource": "event",
274
+ "reminders": 2, "toolCalls": 31, "outputsCapped": 1,
275
+ "outputCharsSaved": 18422, "errors": 0,
276
+ "toolCallsByTool": { "shell": 18, "bash": 6, "webfetch": 2, "read": 5 },
277
+ "toolOutputsCappedByTool": { "shell": 1, "bash": 0, "webfetch": 0, "read": 0 }
278
+ },
279
+ "limits": { "promptTokensSoft": 150000, "maxCompactions": 1, "maxToolOutputChars": 20000 },
280
+ "overBudget": true, "compactionLimitReached": true, "toolsPerTurn": 2.58,
281
+ "tracked": true,
282
+ "guard": { "available": true, "hooksRegistered": true, "sessionEnabled": true,
283
+ "toolOutputEnabled": true,
284
+ "toolOutputTools": ["shell", "bash", "webfetch"],
285
+ "compactionSource": "event", "eventLive": true,
286
+ "streamFailures": 0, "unkeyedCalls": 0 } }
287
+ ```
288
+
289
+ `sessionID` может называть **любую сессию на этом сервере**, не только дочернюю
290
+ вызывающего; это осознанно — по отчёту оркестратор контролирует все свои срезы.
291
+ Счётчики принадлежат инстансу плагина, а `tracked` равен `true`, как только сессию
292
+ наблюдало любое событие или хук; событие `session.deleted` её забывает. `band` —
293
+ максимум достигнутого уровня бюджета (`0` ниже мягкого порога);
294
+ `promptTokensMedian` — медиана по окну последних 256 запросов; `toolCalls` и
295
+ per-tool счётчики включают вложенные вызовы Code Mode; `toolCallsByTool` и
296
+ `toolOutputsCappedByTool` разносят вызовы и урезанные выводы по нормализованному
297
+ имени инструмента (namespace снят, регистр нижний); `outputCharsSaved` — число
298
+ символов, не байтов, и верхняя оценка (внедрённая заглушка вычитается).
299
+ `compactions` считает только подтверждённые `session.compaction.ended`;
300
+ `compactionAttempts` — каждый вызов хука `session.compaction`, поэтому на хосте с
301
+ умершим или отсутствующим потоком событий попытки растут, а `compactions` и
302
+ `compactionLimitReached` стоят (неудачная попытка не становится стоп-сигналом).
303
+ Умерший поток событий один раз виден в `guard.streamFailures`; per-session
304
+ `errors` считает только ошибки хуков, поэтому транспортная авария не
305
+ приписывается каждой сессии. Дубликаты событий компакции считаются один раз
306
+ только при наличии `event.id` или durable-позиции; без identity повторная
307
+ доставка может пересчитать событие (fail-open в сторону счёта).
308
+ Сессия, которую не отдаёт хранилище и не наблюдал этот инстанс, — `E_NOT_FOUND`
309
+ (распознаётся по типизированным `_tag`/`name`/`code` — `Session.NotFoundError`,
310
+ `SessionNotFoundError`, `NotFoundError`, `ENOENT` — с узким fallback по сообщению);
311
+ `session.get`, вернувший `undefined` для ненаблюдаемой сессии, — тот же ответ
312
+ «не найдено»; transient-ошибка `session.get` (блокировка, права, `unknown
313
+ database`) — `E_INTERNAL`; известная сессия без запросов — валидный отчёт с
314
+ `tracked: false`. Без `session.get` ненаблюдаемая сессия —
315
+ `E_NO_API`, наблюдаемая — `ok` с `session: null` и записью в `limitations`.
316
+ Коды: `E_INPUT`, `E_NOT_FOUND`, `E_INTERNAL`, `E_NO_API`, `E_UNAVAILABLE`
317
+ (guard не зарегистрирован или его хуки не установились), `E_ABORTED`.
318
+
319
+ ## Гигиена сессии
320
+
321
+ Плагин реализует протокол [`session-discipline.md`](./session-discipline.md):
322
+ сессия = один срез, бюджет промпта ≤150k токенов, ≤1 компакция, вывод инструментов
323
+ — в файл, в конце среза — handoff.
324
+
325
+ Runtime-часть — три хука (`session.context`, `session.compaction`,
326
+ `tool.execute.after`) и подписка на события компакции:
327
+
328
+ - `session.context` — оценивает промпт (system + messages) и при переходе на
329
+ новый уровень бюджета добавляет одно короткое напоминание. Напоминание
330
+ инжектится **один раз при переходе уровня**, а не в каждом запросе; внутри
331
+ уровня текст стабилен, поэтому префикс-кэш не рвётся.
332
+ - события компакции — хук `session.compaction` регистрируется **всегда**: он
333
+ считает попытки (`compactionAttempts`) и отдаёт tentative-напоминание ближайшему
334
+ запросу; напоминание не называет номер компакции и не заявляет лимит.
335
+ Подтверждает компакцию только `session.compaction.ended` (дедуп по `event.id`
336
+ или durable-позиции) и выставляет confirmed-note с номером и лимитом;
337
+ `session.compaction.failed` увеличивает `compactionFailures`, снимает
338
+ tentative-note и сохраняет confirmed-note, ещё ждущий доставки. Если поток
339
+ событий завершился или упал, `guard.eventLive` становится `false`,
340
+ `guard.compactionSource` — `"hook"`, а `guard.streamFailures` — `1`
341
+ (per-session `errors` остаётся `0`); хук продолжает считать попытки, но
342
+ подтверждённых компакций и ложного «лимит исчерпан» на этом пути нет.
343
+ - `tool.execute.after` — для `shell`, `bash`, `webfetch` урезает вывод больше
344
+ `maxChars`: полный текст пишется в приватный корень на инстанс плагина
345
+ `os.tmpdir()/opencode-tool-output-<random>/` (POSIX: 0700 на корень и каталог
346
+ сессии, 0600 на файл), модель получает голову и путь. На Windows mode не
347
+ действует (замер: 0666), приватность там держится только на ACL каталога
348
+ `os.tmpdir()` и кодом не проверяется. Корень создаётся `mkdtemp` при setup,
349
+ поэтому заранее созданный каталог или
350
+ junction `opencode-tool-output` не используется; на POSIX корень обязан
351
+ принадлежать текущему uid и не давать доступа group/other, иначе guard
352
+ отключается с warning (fail-open: результат не трогается). Каталог сессии
353
+ (`s-<uuid>`) и файл (`c-<uuid>.txt`, запись `wx`) — случайные, их `realpath`
354
+ проверяется внутри корня. При выгрузке плагина корень удаляется best effort.
355
+ Строка с lone surrogate нормализуется один раз (U+FFFD), поэтому файл, голова и
356
+ счётчики описывают одну и ту же строку. Остаточный риск принят: same-user
357
+ атакующий всё ещё может подменить каталог
358
+ сессии между `realpath` и `writeFile` (в Node нет `openat`). Если `TMPDIR`
359
+ указывает внутрь воркспейса, файлы попадут туда. `read`, `write`, `edit`,
360
+ `execute` и инструменты комплекта не урезаются; kit-имя без namespace
361
+ (`session_stats`) отбрасывается в конфиге с warning.
362
+
363
+ `maxCompactions: 0` отключает лимит компакций. `guard.toolOutputEnabled` равен
364
+ `true` только когда приватный корень создан **и** эффективный список инструментов
365
+ непуст; `guard.toolOutputTools` — этот список, `guard.unkeyedCalls` — вызовы
366
+ хуков без непустого `sessionID` (отбрасываются, а не сливаются в бакет `""`).
367
+ Пустой `tools` выключает кап с warning. Guard fail-open: ошибка хука не
368
+ ломает запрос и не портит результат. Отключается флагами `enabled`:
369
+
370
+ ```jsonc
371
+ {
372
+ "plugins": [
373
+ {
374
+ "package": "opencode-orchestrator-uni",
375
+ "options": {
376
+ "sessionGuard": { "enabled": true, "promptTokensSoft": 150000, "maxCompactions": 1 },
377
+ "toolOutputGuard": {
378
+ "enabled": true,
379
+ "maxChars": 20000,
380
+ "headChars": 4000,
381
+ "tools": ["shell", "bash", "webfetch"]
382
+ }
383
+ }
384
+ }
385
+ ]
386
+ }
387
+ ```
388
+
389
+ Метрики среза — `orch_uni_session_stats`; закрытие среза — команда `/handoff`.
390
+
391
+ ## Модели
392
+
393
+ Определения агентов не несут `model`; модели настраиваются у записи плагина в
394
+ `opencode.json(c)`:
395
+
396
+ ```jsonc
397
+ {
398
+ "$schema": "https://opencode.ai/config.json",
399
+ "plugins": [
400
+ {
401
+ "package": "opencode-orchestrator-uni",
402
+ "options": {
403
+ // Модель по умолчанию для всех агентов комплекта.
404
+ "model": "deepseek/deepseek-flash",
405
+ // Переопределения по агентам (id -> строка или развёрнутая форма).
406
+ "agents": {
407
+ "meta": "deepseek/deepseek-reasoner",
408
+ "fix-runner": { "providerID": "anthropic", "model": "claude-sonnet-4-5", "variant": "high" }
409
+ }
410
+ }
411
+ }
412
+ ]
413
+ }
414
+ ```
415
+
416
+ - `model` — модель по умолчанию для всех агентов комплекта; `agents.<id>` —
417
+ переопределение по агенту. Оба принимают `"provider/model[#variant]"` или
418
+ `{ providerID, model, variant }`.
419
+ - Без опций агенты комплекта остаются без модели — субагенты наследуют модель
420
+ сессии.
421
+ - Неверные значения игнорируются с предупреждением (`[orchestrator.uni] …`).
422
+ - Субагенты работают на своей настроенной модели; основной `orchestrator` — на
423
+ модели сессии, поэтому модель, заданная для `orchestrator`, — информационная
424
+ для клиентов (на момент OpenCode 2.0.28).
425
+ - Запись `agents` в конфиге или локальный Markdown-файл агента со своей моделью
426
+ всё равно побеждает — конфигурация применяется после пакетных плагинов.
427
+
428
+ ## Локаль
429
+
430
+ Определения агентов и процессные документы поставляются per-locale — в
431
+ `agents/<locale>/` и `docs/<locale>/`; сейчас это `ru` и `en` (английский
432
+ контент — перевод русских оригиналов). Комплект выбирает локаль в таком порядке:
433
+
434
+ 1. `options.locale` (`"ru"` или `"en"`; полные теги вроде `"ru-RU"` принимаются);
435
+ 2. автодетект: `LC_ALL` / `LC_MESSAGES` / `LANG`, затем Intl-локаль рантайма
436
+ (настройка ОС);
437
+ 3. дефолт: `en`.
438
+
439
+ ```jsonc
440
+ {
441
+ "$schema": "https://opencode.ai/config.json",
442
+ "plugins": [
443
+ {
444
+ "package": "opencode-orchestrator-uni",
445
+ "options": { "locale": "ru", "model": "deepseek/deepseek-flash" }
446
+ }
447
+ ]
448
+ }
449
+ ```
450
+
451
+ Если для запрошенной локали контент не поставлен, происходит откат к дефолтной
452
+ локали с предупреждением в логе плагина. Обе локали регистрируют одних и тех же
453
+ агентов под одними и теми же id — переключение локали меняет только язык
454
+ промптов, процессных документов и описания reference.
455
+
456
+ ## Приоритет
457
+
458
+ **Определения** агентов заполняются только для id, не определённых где-либо ещё:
459
+ агент с тем же id из вашей конфигурации — запись `agents` в конфиге или
460
+ Markdown-файл в `~/.config/opencode/agents/` (либо `.opencode/agents/`) —
461
+ побеждает, и плагин пропускает своё определение. **Настроенная модель** (см.
462
+ [Модели](#модели)) всё равно применяется к агентам комплекта; модель,
463
+ определённая вашей записью агента или файлом, побеждает её, потому что
464
+ конфигурация применяется после пакетных плагинов. То же правило «занято —
465
+ пропускаем» действует для reference `audit-kit`.
466
+
467
+ Если вы раньше устанавливали комплект копированием файлов агентов в свой
468
+ конфиг-каталог, **удалите эти копии** после включения плагина, иначе устаревшие
469
+ копии продолжат перекрывать определения, поставляемые плагином.
470
+
471
+ ## Заметки
472
+
473
+ - Одна рантайм-зависимость: [`gray-matter`](https://www.npmjs.com/package/gray-matter)
474
+ для парсинга фронтматтера. OpenCode сам устанавливает зависимости пакетных
475
+ плагинов.
476
+ - Id плагина: `orchestrator.uni`.
477
+ - Пакет загружается напрямую из TypeScript: `exports` указывает на `index.ts`.
478
+ Шага сборки и каталога `dist/` нет (тот же приём, что в `@opencode/plugin-figma`).
479
+ - Поставляются только файлы из `package.json#files`: `index.ts`, `lib/`, `schemas/`,
480
+ `agents/`, `docs/`, `README.md`, `README.ru.md`, `LICENSE`.
481
+
482
+ <p align="right">(<a href="../../README.ru.md">назад в README</a>)</p>
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "opencode-orchestrator-uni",
4
- "version": "0.5.1",
5
- "description": "Native OpenCode V2 plugin that registers the orchestrator agent kit (root orchestrator, audit subagents, fix-runner) and exposes the kit's process documents as the audit-kit reference.",
4
+ "version": "0.5.2",
5
+ "description": "An OpenCode V2 plugin that runs coding work as an evidence-driven audit loop: a root orchestrator, read-only auditors, one fix-runner, and deterministic verification tools.",
6
6
  "type": "module",
7
7
  "license": "MIT",
8
8
  "author": "OniVe",