opencode-orchestrator-uni 0.4.0 → 0.5.1
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.
- package/README.md +149 -14
- package/README.ru.md +151 -14
- package/agents/en/orchestrator.md +51 -5
- package/agents/ru/orchestrator.md +51 -5
- package/docs/en/call-mechanics.md +6 -4
- package/docs/en/instruction-other-session.md +6 -6
- package/docs/en/root-runbook.md +3 -1
- package/docs/en/session-discipline.md +230 -0
- package/docs/ru/call-mechanics.md +6 -4
- package/docs/ru/instruction-other-session.md +6 -6
- package/docs/ru/root-runbook.md +3 -1
- package/docs/ru/session-discipline.md +225 -0
- package/index.ts +44 -12
- package/lib/commands.ts +62 -6
- package/lib/session-budget.ts +1236 -0
- package/lib/tools.ts +145 -7
- package/lib/write-guard.ts +283 -0
- package/package.json +1 -1
package/docs/ru/root-runbook.md
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
- **Root (главный агент)** — не аудирует: формирует бриф, запускает субагентов, сводит отчёты; по «недоказуемым» — решение владельца: root оформляет вопрос, фиксирует риск и эскалирует. Ведёт реестр принятых рисков, вносит правки отдельными коммитами, решает «ship».
|
|
6
6
|
- **Субагенты** (read-only): `factcheck`, `empiric`, `invariant`, `arbiter`, (на воротах) `redteam`, `meta`.
|
|
7
7
|
|
|
8
|
+
Сессионная гигиена среза — границы, бюджет контекста (≤150k токенов, ≤1 компакция) и handoff — в `session-discipline.md`.
|
|
9
|
+
|
|
8
10
|
## Шаблон брифа `case-<id>.md` (общий для всех шагов)
|
|
9
11
|
|
|
10
12
|
```
|
|
@@ -26,7 +28,7 @@
|
|
|
26
28
|
3. **Свод root'ом**: распарсить JSON-блоки; вычислить вердикты (ready/not_ready), открытые MAJOR, пересечения и расхождения (по location+механизму). Материализовать свод-артефакт `reports/summary-<id>.md` (определение — `call-mechanics.md` §4): по каждой MAJOR — {id, verifier_role, verdict, evidence}; refuted-список (явный, даже пустой); принятые риски; ревизия состояния (хэш каталога/коммит).
|
|
27
29
|
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
30
|
5. **Арбитр — только при расхождениях**: `subagent(agent="arbiter", prompt="Спорные пункты: <пункт → evidence A → evidence B>. По каждому: подтверждён/опровергнут/недоказуем + команда/цитата.")`. Вердикт свода (и верификация/арбитраж) фиксируются ДО правок и **вместе с ревизией состояния** (хэш каталога/коммит); любая правка после фиксации вердикта → повторный прогон (re-audit) либо явное понижение вердикта: до вердикта — не править.
|
|
29
|
-
6.
|
|
31
|
+
6. **Правки**: только `fix-runner` — root код, доки и тесты не правит; его запись ограничена артефактами аудита/состояния (вход: список «к закрытию» — 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
32
|
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
33
|
8. **Повторяй шаги 3–7, пока не достигнут полный ready** (число кругов не ограничено; все роли ready, MAJOR=0). С 3-го круга нерешённые пункты — арбитр поштучно; повторяющиеся ложные — снять и записать как шум аудита.
|
|
32
34
|
9. **Ворота (релиз; порядок: ред-тим → мета)**: замороженный коммит → `redteam` (мандат фальсификации + корпуса/стенды) → цикл по его находкам (MAJOR — с верификацией сессией противоположной роли) → `meta` (на вход: отчёты + свод `reports/summary-<id>.md`; evidence, полнота, отсутствие «зелёные тесты = проверено»).
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Гигиена сессии: как вести разработку
|
|
2
|
+
|
|
3
|
+
Документ задаёт протокол работы сессии: где её границы, какой бюджет контекста,
|
|
4
|
+
когда делегировать, как обращаться с выводом инструментов и что остаётся после
|
|
5
|
+
сессии. Он дополняет `context-efficiency.md` (бюджет файлов инструкций) и
|
|
6
|
+
`root-runbook.md` (аудиторский цикл): там — про артефакты и вердикты, здесь —
|
|
7
|
+
про то, как не сжечь контекст и не потерять решения по дороге.
|
|
8
|
+
|
|
9
|
+
## Зачем это нужно
|
|
10
|
+
|
|
11
|
+
- Контекст — расходуемый ресурс, а не память проекта. С ростом входа качество
|
|
12
|
+
падает даже на простых задачах (context rot); «влезло в окно» не значит
|
|
13
|
+
«модель это использует».
|
|
14
|
+
- Компакция — lossy-пересказ. После неё агент помнит не решения, а их краткое
|
|
15
|
+
изложение; повторная компакция пересказывает пересказ.
|
|
16
|
+
- Замер длинной сессии: 13 253 хода, 19 компакций, медиана промпта выросла с
|
|
17
|
+
314k до 780k токенов. Почти всё это — повторная пересылка истории, а не работа.
|
|
18
|
+
Это внутренний замер проекта-потребителя: артефакт в репозиторий не приложен,
|
|
19
|
+
из этого репозитория цифры не воспроизводятся.
|
|
20
|
+
|
|
21
|
+
Цель протокола — держать рабочее состояние маленьким, а решения — в файлах,
|
|
22
|
+
которые переживают сессию.
|
|
23
|
+
|
|
24
|
+
## 1. Сессия = срез
|
|
25
|
+
|
|
26
|
+
Сессия — это один срез с измеримым DoD, а не весь проект.
|
|
27
|
+
|
|
28
|
+
| Правило | Значение |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| Бюджет оценки промпта | ≤ 150k токенов |
|
|
31
|
+
| Компакций на срез | ≤ 1 |
|
|
32
|
+
| Стоп-сигнал | первая компакция |
|
|
33
|
+
| Обязательный выход | handoff + коммит |
|
|
34
|
+
|
|
35
|
+
- При первом признаке приближения к бюджету — не «дожимать», а закрывать срез:
|
|
36
|
+
handoff, коммит, новая сессия.
|
|
37
|
+
- После первой компакции срез закрывается в любом случае: суммаризация уже
|
|
38
|
+
потеряла детали, и дальнейшая работа идёт по пересказу.
|
|
39
|
+
- Резать работу по волнам, гейтам или пакетам правок; смена фазы (план →
|
|
40
|
+
реализация → приёмка) — всегда новая сессия.
|
|
41
|
+
|
|
42
|
+
## 2. Разделение ролей
|
|
43
|
+
|
|
44
|
+
- Разведка и проверка («найти», «понять», «воспроизвести») — субагентам: у них
|
|
45
|
+
чистый контекст, а в ответ возвращается 1–2k токенов выводов, а не дамп.
|
|
46
|
+
- Реализация — одному пишущему субагенту на рабочее пространство (root сам код, доки и тесты не правит — только артефакты аудита/состояния): связанные решения должны
|
|
47
|
+
приниматься в одном контексте (иначе параллельные агенты расходятся в стиле и
|
|
48
|
+
контрактах).
|
|
49
|
+
- Решения, сведение и приёмка — root-оркестратору.
|
|
50
|
+
- Аудиторский цикл не меняется: re-audit — resume тех же сессий; короткие
|
|
51
|
+
аудиторские сессии бюджету не угрожают.
|
|
52
|
+
|
|
53
|
+
Практическое правило: делегируй **исследование** широко, **реализацию** — узко,
|
|
54
|
+
**интеграцию** держи централизованной.
|
|
55
|
+
|
|
56
|
+
## 3. Батчинг инструментов
|
|
57
|
+
|
|
58
|
+
Каждый ход — это полная пересылка промпта. Один инструмент за ход умножает
|
|
59
|
+
бюджет на число мелочей.
|
|
60
|
+
|
|
61
|
+
- ≥ 3 независимые команды за ход или один скрипт вместо пяти-десяти вызовов.
|
|
62
|
+
- Длинные команды WSL выноси в скрипт-обёртку: строка на 250 символов в каждом
|
|
63
|
+
ходе дороже, чем файл, который читается один раз.
|
|
64
|
+
- Для чтения файлов — `read` с `limit`/`offset`, а не `cat` целиком.
|
|
65
|
+
- Цель по метрике: ≥ 2 инструментов на ход.
|
|
66
|
+
|
|
67
|
+
## 4. Вывод инструментов
|
|
68
|
+
|
|
69
|
+
- Всё, что больше 20k символов, — в файл; в контекст идёт голова (4k) и путь.
|
|
70
|
+
Это делает guard автоматически для `shell`, `bash`, `webfetch`.
|
|
71
|
+
- Скриншоты и бинарные вложения — файлом и путём, не base64 в контексте.
|
|
72
|
+
- `execute` (Code Mode) guard не режет: ограничивай результат в самом скрипте —
|
|
73
|
+
возвращай сводку и путь, а не сырой JSON на сотни килобайт.
|
|
74
|
+
- `read`, `write`, `edit` и инструменты комплекта не урезаются: у них свой
|
|
75
|
+
контракт, и потеря содержимого ломает работу.
|
|
76
|
+
|
|
77
|
+
## 5. Состояние в файлах
|
|
78
|
+
|
|
79
|
+
- `handoff` — обязательный выход среза (шаблон ниже); путь по умолчанию —
|
|
80
|
+
`docs/planning/handoff-<YYYY-MM-DD>.md`, если каталога нет — корень проекта.
|
|
81
|
+
- `decisions` — решения и причины; одно решение — одна запись; путь по умолчанию —
|
|
82
|
+
`docs/planning/decisions-<YYYY-MM-DD>.md`.
|
|
83
|
+
- `state` — ветка, гейты, открытые риски, следующие три шага; обновляется перед
|
|
84
|
+
новой сессией; путь по умолчанию — `docs/planning/state.md`.
|
|
85
|
+
- Новая сессия начинается с `state` и целевого раздела SPEC, а не с пересказа
|
|
86
|
+
прошлой сессии.
|
|
87
|
+
|
|
88
|
+
## 6. Модели
|
|
89
|
+
|
|
90
|
+
- По умолчанию — дешёвая модель; дорогая — только на архитектурные решения,
|
|
91
|
+
ревью и разбор спорного.
|
|
92
|
+
- Правки документов, чтение и механические проверки не требуют дорогой модели.
|
|
93
|
+
- Замер: 223 хода дорогой модели — $63, 104 хода дешёвой — $0.09. Один
|
|
94
|
+
переключатель модели экономит больше, чем любая оптимизация промпта. Это
|
|
95
|
+
внутренний замер проекта-потребителя: артефакт в репозиторий не приложен, из
|
|
96
|
+
этого репозитория цифры не воспроизводятся.
|
|
97
|
+
|
|
98
|
+
## 7. Метрики
|
|
99
|
+
|
|
100
|
+
| Метрика | Цель | Где смотреть |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| Оценка промпта (медиана) | ≤ 150k | `orch_uni_session_stats` |
|
|
103
|
+
| Компакций на срез | ≤ 1 | `orch_uni_session_stats` |
|
|
104
|
+
| Инструментов на ход (`toolCalls/requests`, он же `toolsPerTurn`) | ≥ 2 | `orch_uni_session_stats` |
|
|
105
|
+
| Доля урезанных выводов среди вызовов инструмента (`toolOutputsCappedByTool.shell / toolCallsByTool.shell`; так же `bash`, `webfetch`) | < 5% | `orch_uni_session_stats` |
|
|
106
|
+
| Handoff в конце среза | есть | рабочее дерево |
|
|
107
|
+
|
|
108
|
+
`orch_uni_session_stats` отдаёт счётчики текущего инстанса плагина и настроенные
|
|
109
|
+
лимиты; исторические прогоны считаются по базе сессий на стороне проекта.
|
|
110
|
+
|
|
111
|
+
## 8. Guard
|
|
112
|
+
|
|
113
|
+
Guard — это хуки плагина и подписка на события компакции, а не запреты:
|
|
114
|
+
|
|
115
|
+
- `session.context` — оценивает промпт и добавляет одно короткое напоминание при
|
|
116
|
+
переходе на новый уровень бюджета. Напоминание инжектится один раз при
|
|
117
|
+
переходе уровня, а не в каждом запросе; внутри уровня текст стабилен, поэтому
|
|
118
|
+
префикс-кэш не рвётся.
|
|
119
|
+
- события компакции — хук `session.compaction` регистрируется всегда: он считает
|
|
120
|
+
попытки (`compactionAttempts`) и отдаёт tentative-напоминание ближайшему
|
|
121
|
+
запросу при задержке терминального события; напоминание не называет номер
|
|
122
|
+
компакции и не заявляет лимит. Подтверждает компакцию только
|
|
123
|
+
`session.compaction.ended` (дедуп по `event.id` или durable-позиции); он же
|
|
124
|
+
двигает `compactions` и лимит. `session.compaction.failed` увеличивает
|
|
125
|
+
`compactionFailures`, снимает tentative-note и сохраняет confirmed-note, ещё
|
|
126
|
+
ждущий доставки. Если поток событий завершился или упал, guard выставляет
|
|
127
|
+
`eventLive: false`, `compactionSource: "hook"` и `streamFailures: 1`
|
|
128
|
+
(per-session `errors` остаётся `0`), хук уже зарегистрирован и попытки
|
|
129
|
+
продолжают считаться; подтверждённых компакций и стопа на этом пути нет —
|
|
130
|
+
ложного «лимит исчерпан» не бывает. Источник, liveness и сбои потока видны в
|
|
131
|
+
`guard`.
|
|
132
|
+
- `tool.execute.after` — урезает вывод настроенных инструментов и складывает
|
|
133
|
+
полный текст в приватный корень на инстанс плагина
|
|
134
|
+
`os.tmpdir()/opencode-tool-output-<random>/` (POSIX: 0700 на корень и каталог
|
|
135
|
+
сессии, 0600 на файл). На Windows mode не действует (замер: 0666 у корня,
|
|
136
|
+
каталога сессии и файла): приватность там держится только на ACL каталога
|
|
137
|
+
`os.tmpdir()` и кодом не проверяется. Корень создаётся `mkdtemp` при
|
|
138
|
+
setup, поэтому заранее созданный каталог или junction `opencode-tool-output` не
|
|
139
|
+
используется; на POSIX корень обязан принадлежать текущему uid и не давать
|
|
140
|
+
доступа group/other, иначе guard отключается с warning (fail-open: результат не
|
|
141
|
+
трогается). Каталог сессии (`s-<uuid>`) и файл (`c-<uuid>.txt`, запись `wx`) —
|
|
142
|
+
случайные, их `realpath` проверяется внутри корня; при выгрузке плагина корень
|
|
143
|
+
удаляется best effort. Строка с lone surrogate нормализуется один раз (U+FFFD),
|
|
144
|
+
и файл, голова и счётчики описывают уже одну и ту же строку. Остаточный риск
|
|
145
|
+
принят: same-user атакующий всё ещё может
|
|
146
|
+
подменить каталог сессии между `realpath` и `writeFile` (в Node нет `openat`).
|
|
147
|
+
Если `TMPDIR` указывает внутрь воркспейса, файлы попадут туда.
|
|
148
|
+
|
|
149
|
+
Конфигурация (значения по умолчанию):
|
|
150
|
+
|
|
151
|
+
```jsonc
|
|
152
|
+
{
|
|
153
|
+
"sessionGuard": {
|
|
154
|
+
"enabled": true,
|
|
155
|
+
"promptTokensSoft": 150000,
|
|
156
|
+
"maxCompactions": 1
|
|
157
|
+
},
|
|
158
|
+
"toolOutputGuard": {
|
|
159
|
+
"enabled": true,
|
|
160
|
+
"maxChars": 20000,
|
|
161
|
+
"headChars": 4000,
|
|
162
|
+
"tools": ["shell", "bash", "webfetch"]
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`maxCompactions: 0` отключает лимит компакций. `guard.toolOutputEnabled` равен
|
|
168
|
+
`true` только когда приватный корень создан **и** эффективный список инструментов
|
|
169
|
+
непуст; `guard.toolOutputTools` — этот список (после нормализации и отбрасывания
|
|
170
|
+
защищённых и kit-имён), `guard.unkeyedCalls` — число вызовов хуков без непустого
|
|
171
|
+
`sessionID` (такой ввод отбрасывается, а не сливается в бакет `""`). Пустой
|
|
172
|
+
`tools` — явный или после отбрасывания всех имён — выключает кап с warning.
|
|
173
|
+
Принятые ограничения: каталог сессии `s-<uuid>` живёт до выгрузки плагина — он не
|
|
174
|
+
удаляется ни при `session.deleted`, ни после отброшенного капа (например, при
|
|
175
|
+
неизменяемом результате), а освобождается вместе с корнем; нормализация lone
|
|
176
|
+
surrogate опирается на ES2024 `String.prototype.isWellFormed`/`toWellFormed`
|
|
177
|
+
(минимальная версия рантайма в `engines` не зафиксирована — на старом рантайме кап
|
|
178
|
+
деградирует fail-open: `errors` +1, результат не тронут).
|
|
179
|
+
В счётчиках `toolCalls` и
|
|
180
|
+
per-tool счётчики включают вложенные вызовы Code Mode, а `outputCharsSaved` —
|
|
181
|
+
верхняя оценка в символах (внедрённая заглушка вычитается). `toolCallsByTool` и
|
|
182
|
+
`toolOutputsCappedByTool` разносят вызовы и урезанные выводы по нормализованному
|
|
183
|
+
имени инструмента (namespace снят, регистр нижний). Дубликаты событий компакции
|
|
184
|
+
считаются один раз только при наличии `event.id` или durable-позиции; без
|
|
185
|
+
identity повторная доставка может пересчитать событие (fail-open в сторону
|
|
186
|
+
счёта).
|
|
187
|
+
|
|
188
|
+
Guard fail-open: ошибка хука не ломает запрос и не портит результат инструмента.
|
|
189
|
+
Он не пишет в рабочее дерево (кроме случая `TMPDIR` внутри воркспейса — см.
|
|
190
|
+
выше) и не блокирует действия — только напоминает и урезает вывод. Отключить
|
|
191
|
+
можно любым из флагов `enabled`.
|
|
192
|
+
|
|
193
|
+
## Шаблон handoff
|
|
194
|
+
|
|
195
|
+
```md
|
|
196
|
+
# Handoff: <срез> — <YYYY-MM-DD>
|
|
197
|
+
|
|
198
|
+
<!-- Файл: docs/planning/handoff-<YYYY-MM-DD>.md; нет каталога — корень проекта. -->
|
|
199
|
+
|
|
200
|
+
## Цель и DoD
|
|
201
|
+
Одно предложение цели и проверяемый критерий готовности.
|
|
202
|
+
|
|
203
|
+
## Сделано
|
|
204
|
+
- Что изменено; чем подтверждено (команда, тест, коммит).
|
|
205
|
+
|
|
206
|
+
## Не сделано
|
|
207
|
+
- Что осталось и почему (блокер, решение владельца, следующая волна).
|
|
208
|
+
|
|
209
|
+
## Решения
|
|
210
|
+
- Решение — причина; ссылка на документ или коммит.
|
|
211
|
+
|
|
212
|
+
## Риски и ограничения
|
|
213
|
+
- Принятый риск — кем и до какого момента.
|
|
214
|
+
|
|
215
|
+
## Следующий шаг
|
|
216
|
+
Одно конкретное действие для следующей сессии.
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Чек-лист среза
|
|
220
|
+
|
|
221
|
+
- [ ] Срез закрыт по DoD, а не по «кончилось время».
|
|
222
|
+
- [ ] Handoff записан и ссылается на доказательства.
|
|
223
|
+
- [ ] Решения и риски зафиксированы в файлах, а не только в переписке.
|
|
224
|
+
- [ ] Рабочее дерево чистое или явно описано в handoff.
|
|
225
|
+
- [ ] Следующая сессия начинается с `state` и SPEC, а не с пересказа.
|
package/index.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* - The `audit-kit` reference is registered only if that name is free; it points
|
|
25
25
|
* at this package's `docs/` directory so sessions can read the process
|
|
26
26
|
* documents directly.
|
|
27
|
-
* - Slash commands (`/agents-md`, `/spec`) are registered through
|
|
27
|
+
* - Slash commands (`/agents-md`, `/spec`, `/handoff`) are registered through
|
|
28
28
|
* `ctx.command.transform`; their handlers inject a workflow prompt into the
|
|
29
29
|
* current session and never write files themselves.
|
|
30
30
|
*
|
|
@@ -53,7 +53,9 @@ import { fileURLToPath } from "node:url"
|
|
|
53
53
|
import type { Agent, Model, Plugin, Reference } from "@opencode/plugin"
|
|
54
54
|
import matter from "gray-matter"
|
|
55
55
|
import { registerKitCommands } from "./lib/commands"
|
|
56
|
+
import { createSessionBudget, readSessionBudgetOptions, safeJson } from "./lib/session-budget"
|
|
56
57
|
import { registerAuditTools } from "./lib/tools"
|
|
58
|
+
import { createWriteGuard } from "./lib/write-guard"
|
|
57
59
|
|
|
58
60
|
/** Stable plugin id: diagnostics, enable/disable selectors, storage scope. */
|
|
59
61
|
export const PLUGIN_ID = "orchestrator.uni"
|
|
@@ -118,10 +120,12 @@ function localeCandidates(): string[] {
|
|
|
118
120
|
const REFERENCE_DESCRIPTIONS: Record<Locale, string> = {
|
|
119
121
|
ru:
|
|
120
122
|
"Процессные документы комплекта оркестратора (аудиторский цикл): call-mechanics.md, " +
|
|
121
|
-
"process-ready-loop.md, root-runbook.md, instruction-other-session.md, context-efficiency.md
|
|
123
|
+
"process-ready-loop.md, root-runbook.md, instruction-other-session.md, context-efficiency.md, " +
|
|
124
|
+
"session-discipline.md.",
|
|
122
125
|
en:
|
|
123
126
|
"Process documents of the orchestrator agent kit (audit loop): call-mechanics.md, " +
|
|
124
|
-
"process-ready-loop.md, root-runbook.md, instruction-other-session.md, context-efficiency.md
|
|
127
|
+
"process-ready-loop.md, root-runbook.md, instruction-other-session.md, context-efficiency.md, " +
|
|
128
|
+
"session-discipline.md.",
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
/** Frontmatter keys this loader understands; anything else is reported and ignored. */
|
|
@@ -353,7 +357,7 @@ function readLocaleOptions(options: unknown): Locale | undefined {
|
|
|
353
357
|
if (raw === undefined) return undefined
|
|
354
358
|
const locale = parseLocaleTag(raw)
|
|
355
359
|
if (!locale) {
|
|
356
|
-
warn(`options.locale: expected one of ${SUPPORTED_LOCALES.join("|")}, got ${
|
|
360
|
+
warn(`options.locale: expected one of ${SUPPORTED_LOCALES.join("|")}, got ${safeJson(raw)}, ignored`)
|
|
357
361
|
}
|
|
358
362
|
return locale
|
|
359
363
|
}
|
|
@@ -408,25 +412,53 @@ async function setup(ctx: Plugin.Context): Promise<Plugin.Cleanup | undefined> {
|
|
|
408
412
|
})
|
|
409
413
|
|
|
410
414
|
// Deterministic, read-only audit tools: `orch_uni_validate_report`,
|
|
411
|
-
// `orch_uni_context_lint`, `orch_uni_snapshot`, `orch_uni_sessions
|
|
412
|
-
// `lib/tools.ts` and README «Tools»). The
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
|
|
415
|
+
// `orch_uni_context_lint`, `orch_uni_snapshot`, `orch_uni_sessions`,
|
|
416
|
+
// `orch_uni_session_stats` (see `lib/tools.ts` and README «Tools»). The session
|
|
417
|
+
// budget registers the protocol hooks (context/compaction/tool output); the
|
|
418
|
+
// tools cleanup stops the background session-index subscription; the commands
|
|
419
|
+
// cleanup disposes the command transform registration. A failure after the
|
|
420
|
+
// tools are registered must abort that subscription too — otherwise a
|
|
421
|
+
// half-registered plugin leaks an event listener for the lifetime of the server.
|
|
422
|
+
const sessionBudget = await createSessionBudget(ctx, readSessionBudgetOptions(ctx.options, warn), locale, warn)
|
|
423
|
+
// The path-root write guard enforces the orchestrator's artifact-only edit
|
|
424
|
+
// contract beyond what flat `permissions` can express (see `lib/write-guard.ts`).
|
|
425
|
+
// It registers on the host's permission hook and stays a no-op on a host without
|
|
426
|
+
// that API — a missing hook is reported, never fatal. Registration happens
|
|
427
|
+
// before the tools so a later failure disposes the guard with the rest of the chain.
|
|
428
|
+
const writeGuard = await createWriteGuard(ctx, {
|
|
429
|
+
locationDirectory: String(ctx.location.directory),
|
|
430
|
+
warn,
|
|
431
|
+
})
|
|
432
|
+
let toolsCleanup: () => void
|
|
433
|
+
try {
|
|
434
|
+
toolsCleanup = await registerAuditTools(ctx, { sessionBudget })
|
|
435
|
+
} catch (error) {
|
|
436
|
+
await writeGuard.cleanup()
|
|
437
|
+
await sessionBudget.cleanup()
|
|
438
|
+
throw error
|
|
439
|
+
}
|
|
418
440
|
let commandsCleanup: Plugin.Cleanup
|
|
419
441
|
try {
|
|
420
442
|
commandsCleanup = await registerKitCommands(ctx, locale)
|
|
421
443
|
} catch (error) {
|
|
422
444
|
toolsCleanup()
|
|
445
|
+
await writeGuard.cleanup()
|
|
446
|
+
await sessionBudget.cleanup()
|
|
423
447
|
throw error
|
|
424
448
|
}
|
|
425
449
|
return async () => {
|
|
426
450
|
try {
|
|
427
451
|
await commandsCleanup()
|
|
428
452
|
} finally {
|
|
429
|
-
|
|
453
|
+
try {
|
|
454
|
+
toolsCleanup()
|
|
455
|
+
} finally {
|
|
456
|
+
try {
|
|
457
|
+
await writeGuard.cleanup()
|
|
458
|
+
} finally {
|
|
459
|
+
await sessionBudget.cleanup()
|
|
460
|
+
}
|
|
461
|
+
}
|
|
430
462
|
}
|
|
431
463
|
}
|
|
432
464
|
}
|
package/lib/commands.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Slash commands of the kit: `/agents-md` and `/
|
|
2
|
+
* Slash commands of the kit: `/agents-md`, `/spec` and `/handoff`.
|
|
3
3
|
*
|
|
4
4
|
* Commands are prompts, not writers: `execute` injects a short workflow prompt
|
|
5
5
|
* into the current session, and the orchestrator runs it with the usual
|
|
6
6
|
* evidence/approval rules. The detailed templates and checklists live in
|
|
7
|
-
* `docs/<locale>/context-efficiency.md`.
|
|
7
|
+
* `docs/<locale>/context-efficiency.md` and `docs/<locale>/session-discipline.md`.
|
|
8
8
|
*/
|
|
9
9
|
import type { Plugin } from "@opencode/plugin"
|
|
10
10
|
|
|
@@ -12,17 +12,22 @@ type CommandLocale = "ru" | "en"
|
|
|
12
12
|
|
|
13
13
|
/** Arguments typed after the slash command, without a duplicated command token. */
|
|
14
14
|
function commandArgs(text: string): string {
|
|
15
|
-
return text.replace(/^\s*\/(?:agents-md|spec)\b/i, "").trim()
|
|
15
|
+
return text.replace(/^\s*\/(?:agents-md|spec|handoff)\b/i, "").trim()
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const DESCRIPTIONS: Record<
|
|
18
|
+
const DESCRIPTIONS: Record<
|
|
19
|
+
CommandLocale,
|
|
20
|
+
{ readonly agentsMd: string; readonly spec: string; readonly handoff: string }
|
|
21
|
+
> = {
|
|
19
22
|
ru: {
|
|
20
23
|
agentsMd: "Создать или проверить AGENTS.md по шаблону context-efficiency.md.",
|
|
21
24
|
spec: "Создать или проверить SPEC.md по шаблону context-efficiency.md.",
|
|
25
|
+
handoff: "Сформировать handoff среза по шаблону session-discipline.md.",
|
|
22
26
|
},
|
|
23
27
|
en: {
|
|
24
28
|
agentsMd: "Create or audit AGENTS.md using the context-efficiency.md template.",
|
|
25
29
|
spec: "Create or audit SPEC.md using the context-efficiency.md template.",
|
|
30
|
+
handoff: "Write the slice handoff using the session-discipline.md template.",
|
|
26
31
|
},
|
|
27
32
|
}
|
|
28
33
|
|
|
@@ -84,14 +89,43 @@ function specPrompt(locale: CommandLocale, brief: string): string {
|
|
|
84
89
|
].join("\n")
|
|
85
90
|
}
|
|
86
91
|
|
|
87
|
-
|
|
92
|
+
function handoffPrompt(locale: CommandLocale, goal: string): string {
|
|
93
|
+
if (locale === "ru") {
|
|
94
|
+
return [
|
|
95
|
+
"[Команда /handoff]",
|
|
96
|
+
`Срез: ${goal}.`,
|
|
97
|
+
"",
|
|
98
|
+
"Работай по документу session-discipline.md из reference audit-kit.",
|
|
99
|
+
"1. Собери факты только из текущей сессии и рабочего дерева: что сделано, чем подтверждено (команды, тесты, коммиты), что не сделано и почему.",
|
|
100
|
+
"2. Заполни шаблон handoff из session-discipline.md: цель и DoD, сделано, не сделано, решения и причины, риски/принятые ограничения, следующий шаг.",
|
|
101
|
+
"3. Запиши файл `docs/planning/handoff-<YYYY-MM-DD>.md` (нет каталога — корень проекта) инструментом `write`, не shell-heredoc.",
|
|
102
|
+
"4. Ничего не додумывай: неизвестное помечай «не проверено»; секреты и токены в handoff не переноси.",
|
|
103
|
+
"5. Покажи путь файла, краткий diff и открытые вопросы; канонические документы не меняй без подтверждения.",
|
|
104
|
+
"6. Если дерево грязное — предложи коммит по правилам проекта; не коммить без подтверждения владельца.",
|
|
105
|
+
].join("\n")
|
|
106
|
+
}
|
|
107
|
+
return [
|
|
108
|
+
"[Command /handoff]",
|
|
109
|
+
`Slice: ${goal}.`,
|
|
110
|
+
"",
|
|
111
|
+
"Follow session-discipline.md from the audit-kit reference.",
|
|
112
|
+
"1. Collect facts from the current session and the working tree only: what is done, what proves it (commands, tests, commits), what is not done and why.",
|
|
113
|
+
"2. Fill the handoff template from session-discipline.md: goal and DoD, done, not done, decisions and reasons, risks/accepted limits, next step.",
|
|
114
|
+
"3. Write `docs/planning/handoff-<YYYY-MM-DD>.md` (use the project root when the directory is absent) with the `write` tool, not a shell heredoc.",
|
|
115
|
+
"4. Do not invent facts: mark the unknown as `unverified`; never copy secrets or tokens into the handoff.",
|
|
116
|
+
"5. Show the file path, a short diff and open questions; do not change canonical documents without approval.",
|
|
117
|
+
"6. If the working tree is dirty, propose a commit per the project rules; do not commit without the owner's approval.",
|
|
118
|
+
].join("\n")
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Register `/agents-md`, `/spec` and `/handoff`; the cleanup disposes the transform registration. */
|
|
88
122
|
export async function registerKitCommands(ctx: Plugin.Context, locale: CommandLocale): Promise<Plugin.Cleanup> {
|
|
89
123
|
// The command API is optional in a host build. A missing `list` was already
|
|
90
124
|
// tolerated; a missing `transform` used to throw out of `setup` and take every
|
|
91
125
|
// agent and tool with it, so it degrades to a warning plus a no-op cleanup.
|
|
92
126
|
const command = (ctx as Partial<Plugin.Context>).command
|
|
93
127
|
if (command === undefined || typeof command.transform !== "function") {
|
|
94
|
-
console.warn("orch-uni: this host has no command API; /agents-md and /
|
|
128
|
+
console.warn("orch-uni: this host has no command API; /agents-md, /spec and /handoff are not registered")
|
|
95
129
|
return async () => {}
|
|
96
130
|
}
|
|
97
131
|
let existing = new Set<string>()
|
|
@@ -141,6 +175,28 @@ export async function registerKitCommands(ctx: Plugin.Context, locale: CommandLo
|
|
|
141
175
|
},
|
|
142
176
|
})
|
|
143
177
|
}
|
|
178
|
+
if (!existing.has("handoff")) {
|
|
179
|
+
editor.add({
|
|
180
|
+
name: "handoff",
|
|
181
|
+
description: DESCRIPTIONS[locale].handoff,
|
|
182
|
+
execute: async (input) => {
|
|
183
|
+
await ctx.session.prompt({
|
|
184
|
+
sessionID: input.sessionID,
|
|
185
|
+
text: handoffPrompt(
|
|
186
|
+
locale,
|
|
187
|
+
commandArgs(input.prompt.text) ||
|
|
188
|
+
(locale === "ru"
|
|
189
|
+
? "текущая сессия — опиши срез по фактам"
|
|
190
|
+
: "the current session — describe the slice from facts"),
|
|
191
|
+
),
|
|
192
|
+
...(input.prompt.files === undefined ? {} : { files: input.prompt.files }),
|
|
193
|
+
...(input.prompt.agents === undefined ? {} : { agents: input.prompt.agents }),
|
|
194
|
+
...(input.prompt.skills === undefined ? {} : { skills: input.prompt.skills }),
|
|
195
|
+
delivery: input.delivery,
|
|
196
|
+
})
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
}
|
|
144
200
|
})
|
|
145
201
|
return async () => {
|
|
146
202
|
await registration.dispose()
|