opencode-orchestrator-uni 0.2.0 → 0.3.0

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.
@@ -110,15 +110,20 @@ subagent({
110
110
  ### Фаза 5. Правки и повтор
111
111
  - Правки начинаются только после фиксации вердикта свода (и верификации/арбитража). До вердикта — не править.
112
112
  - Правки по порогу: микроправки (1–3 строки, тексты/доки) — root сам; средние/крупные (мультифайл, логика, тесты) — субагент `fix-runner`; спорное до делегирования решает root (после арбитра).
113
+ - Несколько `fix-runner` параллельно — только в отдельных рабочих пространствах, и только когда их **наборы файлов** не могут пересечься. Норма безусловна: «писатель один на рабочее пространство; не запускай двух писателей, чьи **наборы файлов** могут пересечься» (даже в разных пространствах). Непересечение считается по файлам (пути + правило поиска однотипных мест класса), а не по id списка; пересечение наборов ⇒ сериализация фиксеров либо разнесение путей.
114
+ - Рабочее пространство фиксера — git worktree **вне дерева проекта** со своей веткой: основной путь `git worktree add -b fix/<round>-<n> <путь> <base>`; `worktree_create` — только с немедленным `git switch -c fix/<round>-<n>` (detached HEAD для сведения не годится). Посадка сессии: `session_move({sessionID, directory})`, если инструмент доступен, иначе `cd <абс. путь>` и абсолютные пути; если ни то ни другое недоступно — параллельный запуск запрещён.
115
+ - Сведение — строго по **полному hex SHA** из отчёта: `commit` принимается только как полный hex SHA — проверка `^[0-9a-f]{40}$` ДО любых git-команд (`git check-ref-format --branch` барьером НЕ является: refname пропускает `; & | > # $()` и backticks; `git rev-parse --verify '<x>^{commit}'` тоже резолвит имена). SHA передаётся в команды **отдельным аргументом в кавычках** (`git cat-file -t "$sha"`); строки из отчётов в shell-слова не подставлять. Имена веток из отчётов для сведения не использовать вовсе; если всё же нужны — только строгий шаблон `^fix/[A-Za-z0-9][A-Za-z0-9._-]*$` + кавычки. Семантические проверки перед сведением: `git cat-file -t <sha>` = `commit` (после hex-проверки); `git merge-base --is-ancestor <base> <sha>` (sha — потомок base); `git diff --quiet <base> <sha>` должно быть **ложно** (дельта непуста); `<sha> != <base>`. `Already up to date` (пустая дельта) ⇒ статус **`not_consolidated`**, не `closed`; нет `commit` или `workspace` в отчёте ⇒ **`not_closed`** (precondition root'а). Ветку/коммиты без отчёта `closed` не сливать; `closed` и доказательство приёмки (after/stand) — на сведённом commit SHA; worktree не удалять до подтверждённого переноса коммита, после — `git worktree remove --force` и осознанное решение по ветке. Отчёт не пришёл (`no_report`: фиксер упал/таймаут) — root фиксирует в своде работу (worktree, ветка, base SHA) и `no_report`; приоритет — resume в том же worktree, иначе убрать worktree и решить судьбу ветки; частичный коммит без отчёта в основную ветку не вливается.
113
116
  - Вызов фиксера:
114
117
  ```
115
118
  subagent({agent:"fix-runner", prompt:"К закрытию:\n1) <id | локация | механизм |
116
119
  класс/область (пути + правило поиска однотипных мест) | repro-до |
117
- критерий-после | severity>\nГраницы: <пути/ветка>; бюджет/лимит: <время, число
118
- правок>; коммит: <да/нет, сообщение fix-<round>>. Верни fix-runner/v1."})
120
+ критерий-после | severity>\nГраницы: набор файлов: <пути>; рабочее пространство:
121
+ <абс. путь worktree вне дерева проекта>; ветка: fix/<round>-<n>; base: <sha>;
122
+ бюджет/лимит: <время, число правок>; коммит: <да/нет, сообщение fix-<round>>.
123
+ Верни fix-runner/v1."})
119
124
  ```
120
- - Root проверяет дельту (`git show --stat` + ключевой прогон) до re-audit и обновляет реестр.
121
- - Стенд раунда (если в комплекте поставляется проверочный стенд — инструменты `tools/kit-verify` (верификатор) и `tools/kit-sync` (синк копий + снимок) каталога комплекта; в раундах по другим проектам — только если стенд явно включён в границы вызова): baseline → правки → синк копий и повторный прогон стенда; дельта baseline→after прикладывается к отчёту (поле `stand`).
125
+ - Root проверяет дельту (`git diff <round-base>..HEAD --stat` + ключевой прогон) до re-audit и обновляет реестр.
126
+ - Стенд раунда (если в комплекте поставляется проверочный стенд — инструмент `tools/kit-verify` (верификатор) каталога комплекта; в раундах по другим проектам — только если стенд явно включён в границы вызова): baseline → правки → повторный прогон стенда; дельта baseline→after прикладывается к отчёту (поле `stand`). Стенд поставляется в git-чекауте комплекта: в npm-пакете каталог `tools/` не поставляется → `stand: unavailable` и собственные воспроизводимые проверки.
122
127
  - **re-audit = resume тех же сессий**:
123
128
  ```
124
129
  subagent({agent:"empiric", sessionID:<emp>, background:true,
@@ -165,7 +170,7 @@ subagent({agent:"redteam", background:true,
165
170
  | пара → root | полные SGR-отчёты | JSON-блоки |
166
171
  | root → арбитр | только спорные пункты + evidence обеих сторон | список |
167
172
  | root → верификатор | находка MAJOR (id, текст, evidence автора, ревизия состояния) | текст |
168
- | root → fix-runner | список «к закрытию» (id, локация, класс/область, repro-до, критерий-после, severity) + границы/бюджет | текст |
173
+ | root → fix-runner | список «к закрытию» (id, локация, класс/область, repro-до, критерий-после, severity) + границы (набор файлов, рабочее пространство, ветка, base) и бюджет | текст |
169
174
  | root → re-audit | дельта правок + фактический список изменённых файлов + список закрытого | текст |
170
175
  | root → redteam | заявка + свод всех кругов | текст |
171
176
  | redteam → root | SGR | JSON |
@@ -194,6 +199,7 @@ subagent({agent:"redteam", background:true,
194
199
  ## 5. Отказы и восстановление
195
200
 
196
201
  - Субагент упал/прерван → resume с «продолжи с места остановки, не переделывай»; при `Agent not found` — проверить файлы агентов. Источники не смешивать: определения агентов — каталог комплекта (`agents/`), копии — глобальный каталог агентов вашего харнесса (путь зависит от ОС) и проектный каталог агентов харнесса; снимок/восстановление конфигурации — `tools/config-guard` (хранитель конфигурации комплекта, если поставляется), не агенты.
202
+ - Повторный запуск фиксера после инцидента в том же пространстве — resume в том же worktree либо явное удаление/переименование ветки решением root'а (повторный `git worktree add -b fix/<round>-<n>` падает, пока ветка занята); `locked` worktree — `git worktree unlock` либо `git worktree remove -f -f`.
197
203
  - Модель/провайдер недоступны → подождать/сменить модель по решению владельца, сессии сохранить.
198
204
  - Ограниченная среда (Code Mode: действия только через `execute`+каталог, `subagent` недоступен) → аудиторский цикл не запускается; итог — «готово; штатный аудит отложен» с ревизией и планом прогона (§8).
199
205
  - `arbiter` недоступен или бюджет исчерпан → root разбирает спорный пункт поштучно по фактам/цитатам и фиксирует как риск и эскалирует владельцу.
@@ -216,7 +222,8 @@ subagent({agent:"redteam", background:true,
216
222
 
217
223
  ## 7. Инварианты процесса
218
224
 
219
- - root не аудирует и не подменяет вердикты; писатель в каждый момент один: root (микроправки) или `fix-runner` (средние/крупные).
225
+ - root не аудирует и не подменяет вердикты; писатель один **на рабочее пространство**: root (микроправки) или `fix-runner` (средние/крупные); несколько `fix-runner` — параллельно только в отдельных worktree и с последующим сведением root'ом.
226
+ - Не запускай двух писателей, чьи **наборы файлов** могут пересечься, — безусловно, независимо от рабочего пространства: непересекающиеся списки «к закрытию» не заменяют проверку пересечения наборов файлов (пути + правило поиска класса).
220
227
  - Аудиторы read-only; проектная конкретика — только в брифе.
221
228
  - Правки начинаются только после фиксации вердикта свода (и верификации MAJOR/арбитража). До вердикта — не править.
222
229
  - MAJOR-находки входят в правки только после независимой верификации (Фаза 3.5); дефолт верификатора — «не подтверждена» (fail-closed).
@@ -27,7 +27,7 @@
27
27
  Режим: <РЕЖИМ>
28
28
 
29
29
  Правила контура:
30
- - Аудиторы — read-only субагенты; правки вносишь ты (микроправки) или делегируешь `fix-runner` (средние/крупные). Ты — оркестратор: бриф, запуск, свод отчётов, решения, правки. Сам не аудируешь.
30
+ - Аудиторы — read-only субагенты; правки вносишь ты (микроправки) или делегируешь `fix-runner` (средние/крупные). Ты — оркестратор: бриф, запуск, свод отчётов, решения, правки. Сам не аудируешь. Несколько `fix-runner` — только в отдельных worktree **вне дерева проекта** со своей веткой `fix/<round>-<n>`, и только без пересечения **наборов файлов** (пересечение наборов root считает до запуска ⇒ сериализация или разнесение путей); сессию фиксера сажай в её пространство (`session_move`, иначе `cd` + абсолютные пути; ни то ни другое — параллельный запуск запрещён), своди результаты строго по полному hex SHA из отчёта (проверка `^[0-9a-f]{40}$` до git-команд; имена веток из отчётов для сведения не использовать; SHA — отдельным аргументом в кавычках) и прогоняй проверки на сведённом состоянии.
31
31
  - Общение — через бриф-файл и SGR-отчёты. Фоновых субагентов НЕ поллить: уведомления приходят сами.
32
32
  - Цикл живёт до ПОЛНОГО ready: все роли ready, открытых MAJOR нет, MINOR/NIT закрыты или явно приняты.
33
33
  - Спорное решает `arbiter` (только спорные пункты); недоказуемое → решение владельца: root оформляет вопрос, фиксирует риск и эскалирует.
@@ -49,7 +49,7 @@
49
49
  partial/unavailable = «не подтверждена»; сбой/таймаут верификатора — то же + шум. Опровергнутые — в список refuted; статусы пиши в свод reports/summary-<id>.md; неподтверждённая MAJOR в правки не идёт.
50
50
  Конфликты → subagent(agent="arbiter", prompt="Спорные пункты: <пункт → evidence A → evidence B>. По каждому: подтверждён/опровергнут/недоказуем + призма (reproducibility|contract|falsification|not_applicable; несколько — массив) + способ проверки. Только эти пункты.")
51
51
  Вердикт свода (и верификация/арбитраж) фиксируются ДО правок и вместе с ревизией состояния (хэш/коммит); правка после фиксации вердикта → повторный прогон (re-audit) либо явное понижение вердикта: до вердикта — не править.
52
- 5) Правки по порогу: микроправки — сам; средние/крупные — subagent(agent="fix-runner", prompt="К закрытию: <id | локация | механизм | класс/область (пути + правило поиска однотипных мест) | repro-до | критерий-после>. Границы: <…>; бюджет/лимит: <время, число правок>; коммит fix-<round>. Верни fix-runner/v1."). Перед re-audit проверь дельту (git show --stat + ключевой прогон); обнови risks.md (closed / accepted-risk с причиной).
52
+ 5) Правки по порогу: микроправки — сам; средние/крупные — subagent(agent="fix-runner", prompt="К закрытию: <id | локация | механизм | класс/область (пути + правило поиска однотипных мест) | repro-до | критерий-после>. Границы: набор файлов: <пути>; worktree: <абс. путь вне дерева проекта>; ветка: fix/<round>-<n>; base: <sha>; бюджет/лимит: <время, число правок>; коммит fix-<round>. Верни fix-runner/v1."). Перед re-audit проверь дельту (git diff <round-base>..HEAD --stat + ключевой прогон) и сведение по полному hex SHA (^[0-9a-f]{40}$ до git-команд + git cat-file -t <sha> = commit, git merge-base --is-ancestor <base> <sha>, непустая дельта: git diff --quiet <base> <sha> ложно; пустая дельта / Already up to date ⇒ not_consolidated, не closed); обнови risks.md (closed / accepted-risk с причиной).
53
53
  6) Re-audit — resume ТЕХ ЖЕ сессий (background, один блок):
54
54
  subagent(agent="empiric", sessionID="<emp>", background=true, prompt="Правки: <диапазон + фактический список изменённых файлов>; закрыто: <…>. Проверь закрытие своих находок и отсутствие регрессий на новом состоянии; верни обновлённый SGR.")
55
55
  subagent(agent="invariant", sessionID="<inv>", background=true, prompt="… то же …")
@@ -22,6 +22,8 @@
22
22
  │ для коммита/PR: SHIP, если правок после вердикта не было (ред-тим и мета не обязательны)
23
23
  └─ НЕТ ─► [2] ПРАВКИ (закрыть MAJOR; MINOR/NIT — закрыть или явно принять с обоснованием, запись в risks-<id>.md)
24
24
  ▼
25
+ [2a] СВЕДЕНИЕ (если фиксеров было несколько: сведение веток по commit SHA → проверки на сведённом состоянии)
26
+ ▼
25
27
  [3] НОВОЕ СОСТОЯНИЕ X1 (фиксы закоммичены/зафиксированы)
26
28
  ▼
27
29
  повтор [1] на X1 (оба аудитора, тот же бриф)
@@ -39,7 +41,7 @@
39
41
  1. Все роли (эмпирик, инвариантщик; на воротах — и ред-тим) — `verdict=ready`.
40
42
  2. Открытых MAJOR нет. MINOR/NIT: закрыты либо **явно приняты** с причиной (запись в `risks-<id>.md`; в сводке/PR: «принято как долг: …»).
41
43
  3. Все расхождения разрешены арбитром; недоказуемое → решение владельца: root оформляет вопрос, фиксирует риск и эскалирует.
42
- 4. Повторный прогон идёт **по состоянию после правок**, а не по старому диффу. Никаких «переголосований» на том же снапшоте.
44
+ 4. Повторный прогон идёт **по состоянию после правок**, а не по старому диффу. Никаких «переголосований» на том же снапшоте. При нескольких фиксерах X1 достигается сведением их веток по commit SHA и проверками на сведённом состоянии; `closed` ставится после сведения.
43
45
  5. На релизных воротах — `meta` = pass (`gate`=pass), `closing_list` пуст/закрыт, `coverage_gaps` приняты владельцем (запись в `risks-<id>.md`). **SHIP ⟺ `meta.gate`=pass ∧ `closing_list` пуст/закрыт ∧ `coverage_gaps` приняты владельцем (запись в `risks-<id>.md`); иначе ship запрещён.**
44
46
  6. MAJOR-находки входят в правки только после независимой верификации; дефолт верификатора — «не подтверждена» (fail-closed).
45
47
  7. Вердикт и верификация фиксируются вместе с ревизией состояния (хэш каталога/коммит); любая правка после фиксации вердикта → повторный прогон (re-audit) либо явное понижение вердикта.
@@ -26,8 +26,8 @@
26
26
  3. **Свод root'ом**: распарсить JSON-блоки; вычислить вердикты (ready/not_ready), открытые MAJOR, пересечения и расхождения (по location+механизму). Материализовать свод-артефакт `reports/summary-<id>.md` (определение — `call-mechanics.md` §4): по каждой MAJOR — {id, verifier_role, verdict, evidence}; refuted-список (явный, даже пустой); принятые риски; ревизия состояния (хэш каталога/коммит).
27
27
  4. **Верификация MAJOR (перекрёстная, fail-closed)**: каждая MAJOR-находка подтверждается независимо **сессией противоположной роли** (не автором находки) — `subagent(agent="<sibling>", sessionID=<сессия противоположной роли>, prompt="Находка <id> контрагента: <текст>. Evidence автора: <команда→вывод>. Ревизия состояния: <хэш каталога/коммит>. Подтверди или опровергни своим воспроизведением; дефолт — не подтверждена. Верни: {id, verifier_role, verdict: confirmed|refuted|partial|unavailable, evidence}.")`. `verifier_role` ≠ роль автора; `partial`/`unavailable` = «не подтверждена»; `refuted` — в список refuted для сводки; сбой/таймаут верификатора — «не подтверждена» + шум. Не подтверждена → в правки не идёт (спор → шаг 5, арбитр); статусы верификации пишутся в свод `reports/summary-<id>.md` (шаг 3). Если MAJOR-находка независимо воспроизведена обеими ролями в их собственных прогонах — взаимное подтверждение засчитывается как верификация (обе стороны в свод), отдельный прогон не требуется. sessionID при вызове обязателен: без него это новая сессия, а не продолжение — дефект шага.
28
28
  5. **Арбитр — только при расхождениях**: `subagent(agent="arbiter", prompt="Спорные пункты: <пункт → evidence A → evidence B>. По каждому: подтверждён/опровергнут/недоказуем + команда/цитата.")`. Вердикт свода (и верификация/арбитраж) фиксируются ДО правок и **вместе с ревизией состояния** (хэш каталога/коммит); любая правка после фиксации вердикта → повторный прогон (re-audit) либо явное понижение вердикта: до вердикта — не править.
29
- 6. **Правки** по порогу: микро — root сам; средние/крупные — `fix-runner` (вход: список «к закрытию» — id, локация, механизм, **класс/область** (пути + правило поиска однотипных мест), repro-до, критерий-после, severity — плюс границы и **бюджет/лимит (время, число правок)**; выход: fix-runner/v1 с before/after/`proof`/тестами). Отдельные коммиты `fix-<round>`; обновить реестр: `closed` / `accepted-risk` (с причиной). Root проверяет дельту до re-audit. Стенд раунда (если в комплекте поставляется проверочный стенд — инструменты `tools/kit-verify` и `tools/kit-sync` каталога комплекта; в раундах по другим проектам — только если стенд явно включён в границы вызова): baseline стенда в промпт фиксера (если вывод передан — фиксер не дублирует прогон); закрытие — синк копий и повторный прогон стенда; оба вывода — в поле `stand` отчёта. Статусы `fix-runner`: `needs_decision` → решение root (при споре — арбитр; если решение за владельцем — эскалация) с фиксацией в реестре рисков; `not_closed` → повторная итерация с тем же id; поле `out_of_scope` (замечено вне списка) → включить в следующий список «к закрытию» или эскалировать.
30
- 7. **Re-audit = resume те же сессии** (sessionID из шага 2): «Правки: <диапазон + фактический список изменённых файлов>. Проверь закрытие своих находок и отсутствие регрессий; верни обновлённый SGR.» — исторически так и делалось (продолжение сессий GLM/DS). sessionID обязателен: вызов без него (новая сессия вместо продолжения) — дефект шага.
29
+ 6. **Правки** по порогу: микро — root сам; средние/крупные — `fix-runner` (вход: список «к закрытию» — id, локация, механизм, **класс/область** (пути + правило поиска однотипных мест), repro-до, критерий-после, severity — плюс границы и **бюджет/лимит (время, число правок)**; выход: fix-runner/v1 с before/after/`proof`/тестами). Отдельные коммиты `fix-<round>`; обновить реестр: `closed` / `accepted-risk` (с причиной). Root проверяет дельту раунда — `git diff <round-base>..HEAD --stat` (не только `git show --stat`) — до re-audit. Стенд раунда (если в комплекте поставляется проверочный стенд — инструмент `tools/kit-verify` каталога комплекта; в раундах по другим проектам — только если стенд явно включён в границы вызова): baseline стенда в промпт фиксера (если вывод передан — фиксер не дублирует прогон); закрытие — повторный прогон стенда; оба вывода — в поле `stand` отчёта. Стенд поставляется в git-чекауте комплекта: в npm-пакете каталог `tools/` не поставляется → `stand: unavailable` и собственные воспроизводимые проверки. Статусы `fix-runner`: `needs_decision` → решение root (при споре — арбитр; если решение за владельцем — эскалация) с фиксацией в реестре рисков; `not_closed` → повторная итерация с тем же id; `budget_exceeded` → расширить бюджет решением владельца либо разбить список и продолжить с того же id (коммиты и worktree сохранить до закрытия или уборки); поле `out_of_scope` (замечено вне списка) → включить в следующий список «к закрытию» или эскалировать. Статус **`no_report`** (отчёта нет: фиксер упал/таймаут) — root фиксирует в своде выполненную работу (worktree, ветка, base SHA) и `no_report`; ветку/коммиты НЕ сливать; приоритет — resume в том же worktree; иначе убрать: `git worktree remove --force` и решить судьбу ветки. Частичный коммит без отчёта в основную ветку не вливается. Несколько `fix-runner` — только в отдельных worktree **вне дерева проекта** (у каждого своя ветка `fix/<round>-<n>`); путь пространства сверяется префиксами путей (`git -C <project> rev-parse --show-toplevel` не должен быть префиксом канонического пути пространства; иначе — `needs_decision`, пространство не создаётся и не используется); перед запуском root считает пересечение **наборов файлов** (пути + правило поиска класса), а не только списков: пересечение ⇒ сериализация или разнесение путей. Посадка сессии в её пространство: `session_move({sessionID, directory})` (если доступен), иначе `cd <абс. путь>` + абсолютные пути; ни то ни другое — параллельный запуск запрещён. В свежем worktree зависимости ставятся до прогонов (`npm ci` / `bun install`). После завершения root сводит результаты строго по **полному hex SHA** из отчёта: `commit` принимается только как `^[0-9a-f]{40}$` — проверка ДО любых git-команд (имена веток из отчётов для сведения не использовать; `check-ref-format` и `rev-parse --verify` барьером не являются — резолвят имена); SHA — отдельным аргументом в кавычках; семантические проверки: `git cat-file -t <sha>` = `commit`, `git merge-base --is-ancestor <base> <sha>`, `git diff --quiet <base> <sha>` ложно (дельта непуста), `<sha> != <base>`; `Already up to date` (пустая дельта) ⇒ `not_consolidated`, не `closed`; merge/cherry-pick по SHA, прогоняет проверки на сведённом состоянии и только тогда фиксирует `closed`; worktree не удалять до подтверждённого переноса коммита, после — `git worktree remove --force` и осознанное решение по ветке; повторный запуск после инцидента — resume в том же worktree либо явное удаление/переименование ветки решением root'а (повторный `git worktree add -b` падает при занятой ветке), `locked` worktree — `git worktree unlock` либо `git worktree remove -f -f`.
30
+ 7. **Re-audit = resume те же сессии** (sessionID из шага 2): «Правки: <диапазон + фактический список изменённых файлов>. Проверь закрытие своих находок и отсутствие регрессий; верни обновлённый SGR.» — исторически так и делалось (продолжение сессий GLM/DS). sessionID обязателен: вызов без него (новая сессия вместо продолжения) — дефект шага. Re-audit и `closed` — на **сведённом** состоянии: доказательство приёмки (after/stand) перепривязывается к сведённому commit SHA; `closed` без отчёта фиксера (`no_report`) запрещён; нет ключа `commit` или `workspace` в отчёте фиксера ⇒ `not_closed`.
31
31
  8. **Повторяй шаги 3–7, пока не достигнут полный ready** (число кругов не ограничено; все роли ready, MAJOR=0). С 3-го круга нерешённые пункты — арбитр поштучно; повторяющиеся ложные — снять и записать как шум аудита.
32
32
  9. **Ворота (релиз; порядок: ред-тим → мета)**: замороженный коммит → `redteam` (мандат фальсификации + корпуса/стенды) → цикл по его находкам (MAJOR — с верификацией сессией противоположной роли) → `meta` (на вход: отчёты + свод `reports/summary-<id>.md`; evidence, полнота, отсутствие «зелёные тесты = проверено»).
33
33
  - **SHIP ⟺ `meta.gate`=pass ∧ `closing_list` пуст/закрыт ∧ `coverage_gaps` приняты владельцем (запись в `risks-<id>.md`); иначе ship запрещён.**
@@ -41,7 +41,7 @@
41
41
  - root → re-audit: диапазон правок + краткое «что закрыто»;
42
42
  - root → ред-тим: заявка автора + свод всех предыдущих кругов (объект фальсификации);
43
43
  - root → мета: отчёты (пути) + свод `reports/summary-<id>.md`;
44
- - root → fix-runner: список «к закрытию» (с классом/областью) + границы + бюджет/лимит (время, число правок).
44
+ - root → fix-runner: список «к закрытию» (с классом/областью) + границы (включая **набор файлов**, worktree вне дерева проекта, ветку `fix/<round>-<n>`, base SHA) + бюджет/лимит (время, число правок); при параллельном запуске — каждому отдельное пространство и ветка, а наборы файлов root проверяет на пересечение до запуска;
45
45
 
46
46
  ## Тонкости
47
47
 
package/lib/tools.ts CHANGED
@@ -46,25 +46,76 @@ interface SchemaDocument {
46
46
  readonly schema: JsonSchemaNode
47
47
  }
48
48
 
49
+ function errorMessage(error: unknown): string {
50
+ return error instanceof Error ? error.message : String(error)
51
+ }
52
+
53
+ /**
54
+ * Typed failure of the schema registry (`schemas/*.json`): an unreadable
55
+ * directory, an unreadable file, malformed JSON or a document of the wrong
56
+ * shape. `loadSchemas` throws it on purpose — otherwise the plugin would
57
+ * validate against a silently smaller registry — and the `validate_report`
58
+ * executor converts it into an `E_SCHEMA_IO` refusal, so a caller always gets a
59
+ * structured code plus the directory/file instead of a raw IO/parse exception.
60
+ */
61
+ export class SchemaIoError extends Error {
62
+ readonly code = "E_SCHEMA_IO"
63
+ readonly directory: string
64
+ readonly file: string | undefined
65
+
66
+ constructor(message: string, directory: string, file?: string) {
67
+ super(message)
68
+ this.name = "SchemaIoError"
69
+ this.directory = directory
70
+ this.file = file
71
+ }
72
+ }
73
+
49
74
  /**
50
75
  * Read every `schemas/*.json` file into an id → schema map. Files are read in
51
- * sorted order; a malformed document throws (the plugin would otherwise
52
- * validate against a silently smaller registry).
76
+ * sorted order; a malformed document throws a `SchemaIoError` (fail-closed, typed).
53
77
  */
54
78
  export async function loadSchemas(directory: string = SCHEMAS_DIRECTORY): Promise<Map<string, JsonSchemaNode>> {
55
- const entries = await readdir(directory, { withFileTypes: true })
56
- const files = entries
57
- .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
58
- .map((entry) => entry.name)
59
- .sort()
79
+ let names: string[]
80
+ try {
81
+ const entries = await readdir(directory, { withFileTypes: true })
82
+ names = entries
83
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
84
+ .map((entry) => entry.name)
85
+ .sort()
86
+ } catch (error) {
87
+ throw new SchemaIoError(`cannot read schemas directory ${directory}: ${errorMessage(error)}`, directory)
88
+ }
60
89
  const schemas = new Map<string, JsonSchemaNode>()
61
- for (const file of files) {
62
- const source = await readFile(path.join(directory, file), "utf8")
63
- const document = JSON.parse(source) as SchemaDocument
64
- if (typeof document.id !== "string" || typeof document.schema !== "object" || document.schema === null) {
65
- throw new Error(`schemas/${file}: expected { id: string, schema: object }`)
90
+ for (const file of names) {
91
+ const absolute = path.join(directory, file)
92
+ let source: string
93
+ try {
94
+ source = await readFile(absolute, "utf8")
95
+ } catch (error) {
96
+ throw new SchemaIoError(`cannot read schemas/${file} (${absolute}): ${errorMessage(error)}`, directory, absolute)
97
+ }
98
+ let parsed: unknown
99
+ try {
100
+ parsed = JSON.parse(source)
101
+ } catch (error) {
102
+ throw new SchemaIoError(
103
+ `schemas/${file} (${absolute}) is not valid JSON: ${errorMessage(error)}`,
104
+ directory,
105
+ absolute,
106
+ )
107
+ }
108
+ const document = asRecord(parsed) as Partial<SchemaDocument> | undefined
109
+ const id = document?.id
110
+ const schema = document?.schema
111
+ if (typeof id !== "string" || typeof schema !== "object" || schema === null) {
112
+ throw new SchemaIoError(
113
+ `schemas/${file} (${absolute}): expected { id: string, schema: object }`,
114
+ directory,
115
+ absolute,
116
+ )
66
117
  }
67
- schemas.set(document.id, document.schema)
118
+ schemas.set(id, schema as JsonSchemaNode)
68
119
  }
69
120
  return schemas
70
121
  }
@@ -298,13 +349,24 @@ export function resolveRoot(directory: string, input: string): string {
298
349
  return path.isAbsolute(input) ? path.normalize(input) : path.resolve(directory, input)
299
350
  }
300
351
 
352
+ /** Optional seams for tests; the plugin calls this with no options. */
353
+ export interface RegisterAuditToolsOptions {
354
+ /** Directory to read `schemas/*.json` from. Defaults to the shipped `schemas/`. */
355
+ readonly schemasDirectory?: string
356
+ }
357
+
301
358
  /**
302
- * Register the audit tools on the plugin context. Loading the schemas and the
303
- * session index happens before the transform callback, because transforms are
304
- * synchronous and the executors must close over complete, immutable state.
359
+ * Register the audit tools on the plugin context. The session index is loaded
360
+ * before the transform callback, because transforms are synchronous; the report
361
+ * schemas are not: they are re-read on every `validate_report` call, so a schema
362
+ * edited while the plugin is live is applied without a reload (a price of a few
363
+ * small JSON reads per validation).
305
364
  */
306
- export async function registerAuditTools(context: Plugin.Context): Promise<() => void> {
307
- const schemas = await loadSchemas()
365
+ export async function registerAuditTools(
366
+ context: Plugin.Context,
367
+ options: RegisterAuditToolsOptions = {},
368
+ ): Promise<() => void> {
369
+ const schemasDirectory = options.schemasDirectory ?? SCHEMAS_DIRECTORY
308
370
  const directory = context.location.directory
309
371
  const session = context.session
310
372
  const index = new SessionIndex(context.storage)
@@ -337,6 +399,20 @@ export async function registerAuditTools(context: Plugin.Context): Promise<() =>
337
399
  }
338
400
  }
339
401
  if (toolContext.signal.aborted) return failure("E_ABORTED", "validation aborted")
402
+ let schemas: Map<string, JsonSchemaNode>
403
+ try {
404
+ schemas = await loadSchemas(schemasDirectory)
405
+ } catch (error) {
406
+ if (error instanceof SchemaIoError) {
407
+ return failure(error.code, error.message, {
408
+ directory: error.directory,
409
+ ...(error.file === undefined ? {} : { file: error.file }),
410
+ })
411
+ }
412
+ return failure("E_SCHEMA_IO", `cannot load schemas from ${schemasDirectory}: ${errorMessage(error)}`, {
413
+ directory: schemasDirectory,
414
+ })
415
+ }
340
416
  const report = checkReport(source, { ...(expectedSchema === undefined ? {} : { expectedSchema }), schemas })
341
417
  return json(report)
342
418
  }
@@ -479,7 +555,7 @@ export async function registerAuditTools(context: Plugin.Context): Promise<() =>
479
555
  description:
480
556
  "Validate an SGR report: extract the last ```json block, resolve its schema, validate it and list prompt-level warnings. " +
481
557
  "Read-only. Returns JSON {valid, schema, errors, warnings} or a typed code " +
482
- "(E_INPUT|E_NO_JSON|E_PARSE|E_UNKNOWN_SCHEMA|E_SCHEMA_MISMATCH|E_INVALID|E_ABORTED).",
558
+ "(E_INPUT|E_NO_JSON|E_PARSE|E_UNKNOWN_SCHEMA|E_SCHEMA_MISMATCH|E_INVALID|E_SCHEMA_IO|E_ABORTED).",
483
559
  input: VALIDATE_REPORT_INPUT,
484
560
  options: { namespace: TOOL_NAMESPACE, codemode: true },
485
561
  execute: validateReport,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "opencode-orchestrator-uni",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
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.",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -45,6 +45,7 @@
45
45
  "lint": "biome check .",
46
46
  "lint:fix": "biome check --write .",
47
47
  "check:locales": "bun tools/locale-check.ts",
48
+ "verify:kit": "bun tools/kit-verify.ts",
48
49
  "eval": "bun eval/run.ts",
49
50
  "eval:selfcheck": "bun eval/selfcheck.ts",
50
51
  "eval:recompute": "bun eval/recompute.ts"
@@ -7,6 +7,15 @@
7
7
  "schema": { "type": "string", "const": "fix-runner/v1" },
8
8
  "round": { "type": "string" },
9
9
  "commit": { "type": ["string", "null"] },
10
+ "workspace": {
11
+ "type": "object",
12
+ "required": ["branch", "worktree", "base_commit"],
13
+ "properties": {
14
+ "branch": { "type": "string" },
15
+ "worktree": { "type": "string" },
16
+ "base_commit": { "type": "string" }
17
+ }
18
+ },
10
19
  "results": {
11
20
  "type": "array",
12
21
  "items": {
@@ -14,10 +23,7 @@
14
23
  "required": ["id", "status", "change", "before", "after", "proof", "stand", "tests"],
15
24
  "properties": {
16
25
  "id": { "type": "string" },
17
- "status": {
18
- "type": "string",
19
- "enum": ["closed", "not_closed", "needs_decision", "budget_exceeded"]
20
- },
26
+ "status": { "type": "string", "enum": ["closed", "not_closed", "needs_decision", "budget_exceeded"] },
21
27
  "change": { "type": "string" },
22
28
  "before": { "type": "string" },
23
29
  "after": { "type": "string" },
@@ -33,10 +39,7 @@
33
39
  "items": {
34
40
  "type": "object",
35
41
  "required": ["note", "why_suspicious"],
36
- "properties": {
37
- "note": { "type": "string" },
38
- "why_suspicious": { "type": "string" }
39
- }
42
+ "properties": { "note": { "type": "string" }, "why_suspicious": { "type": "string" } }
40
43
  }
41
44
  }
42
45
  }