opencode-orchestrator-uni 0.4.0 → 0.5.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.
package/README.md CHANGED
@@ -12,10 +12,10 @@ ships the **orchestrator agent kit**:
12
12
  - `fix-runner` — the only writing subagent (applies the "to close" list);
13
13
  - the kit's process documents, exposed as the **`audit-kit` reference**
14
14
  (`call-mechanics.md`, `process-ready-loop.md`, `root-runbook.md`,
15
- `instruction-other-session.md`, `context-efficiency.md`);
16
- - four deterministic, read-only tools — `orch_uni_validate_report`, `orch_uni_context_lint`,
17
- `orch_uni_snapshot`, `orch_uni_sessions` (see [Tools](#tools));
18
- - two slash commands — `/agents-md` and `/spec` (see [Commands](#commands)).
15
+ `instruction-other-session.md`, `context-efficiency.md`, `session-discipline.md`);
16
+ - five deterministic, read-only tools — `orch_uni_validate_report`, `orch_uni_context_lint`,
17
+ `orch_uni_snapshot`, `orch_uni_sessions`, `orch_uni_session_stats` (see [Tools](#tools));
18
+ - three slash commands — `/agents-md`, `/spec` and `/handoff` (see [Commands](#commands)).
19
19
 
20
20
  Read-only audit agents are enforced by `edit: deny` plus the prompt ban; their shell
21
21
  permission stays allowed so they can run verification commands (an accepted risk, no
@@ -55,8 +55,8 @@ directory (see [Locale](#locale)). OpenCode lists it in session context as an
55
55
  additional local directory, so the orchestrator (and any other agent) can read
56
56
  the process documents directly, for example `<reference path>/call-mechanics.md`.
57
57
 
58
- The plugin also registers four deterministic, **read-only** tools in the `orch_uni`
59
- namespace and two slash commands — see [Tools](#tools) and [Commands](#commands).
58
+ The plugin also registers five deterministic, **read-only** tools in the `orch_uni`
59
+ namespace and three slash commands — see [Tools](#tools) and [Commands](#commands).
60
60
 
61
61
  ## Commands
62
62
 
@@ -64,6 +64,7 @@ namespace and two slash commands — see [Tools](#tools) and [Commands](#command
64
64
  | --- | --- |
65
65
  | `/agents-md` | Injects the AGENTS.md audit/draft workflow: `context-efficiency.md` template, `orch_uni_context_lint`, `context-lint`, approval before writing. |
66
66
  | `/spec` | Injects the SPEC.md workflow: brief, clarifying questions, EARS/AC/traceability, `orch_uni_context_lint`, `context-lint`, approval before writing. |
67
+ | `/handoff` | Injects the slice-closing workflow: session and working-tree facts, the `session-discipline.md` handoff template, writes `docs/planning/handoff-<YYYY-MM-DD>.md`. |
67
68
 
68
69
  A command is a prompt, not a writer: its handler injects a workflow prompt into the
69
70
  current session. The orchestrator then follows the usual evidence and approval rules.
@@ -76,10 +77,11 @@ current session. The orchestrator then follows the usual evidence and approval r
76
77
  | `orch_uni_context_lint` | yes | Deterministic audit of AGENTS.md/SPEC.md: budget, vague phrases, links, sections |
77
78
  | `orch_uni_snapshot` | yes | Deterministic revision of a directory tree (per-file sha256 + aggregate hash) |
78
79
  | `orch_uni_sessions` | yes | Child sessions of a session: agent, model, cost, tokens, time |
80
+ | `orch_uni_session_stats` | yes | Session protocol counters: prompt, compactions, tools/turn, capped outputs |
79
81
 
80
82
  Every tool answers with a JSON text body. Failures are **typed codes** inside the body
81
83
  (`E_INPUT`, `E_NO_JSON`, …) instead of thrown tool errors, so an agent can branch on the
82
- code without parsing prose. None of the four writes to the workspace, the session store
84
+ code without parsing prose. None of the five writes to the workspace, the session store
83
85
  or the configuration.
84
86
 
85
87
  ### `orch_uni_validate_report`
@@ -261,6 +263,134 @@ picks up fixture trees such as `eval/`; pass an explicit path for the canonical
261
263
  `E_INPUT` for malformed arguments, `E_NOT_FOUND` when the parent session does not exist,
262
264
  `E_ABORTED` when the session is stopped.
263
265
 
266
+ ### `orch_uni_session_stats`
267
+
268
+ ```jsonc
269
+ {} // counters of the calling session
270
+ { "sessionID": "ses_…" } // any session on this server, not only a child
271
+ ```
272
+
273
+ ```jsonc
274
+ { "ok": true, "sessionID": "ses_…",
275
+ "counters": {
276
+ "requests": 12, "promptTokensLast": 151204, "promptTokensMax": 161002,
277
+ "promptTokensMedian": 148900, "band": 1, "compactions": 1,
278
+ "compactionAttempts": 1, "compactionFailures": 0, "compactionSource": "event",
279
+ "reminders": 2, "toolCalls": 31, "outputsCapped": 1,
280
+ "outputCharsSaved": 18422, "errors": 0,
281
+ "toolCallsByTool": { "shell": 18, "bash": 6, "webfetch": 2, "read": 5 },
282
+ "toolOutputsCappedByTool": { "shell": 1, "bash": 0, "webfetch": 0, "read": 0 }
283
+ },
284
+ "limits": { "promptTokensSoft": 150000, "maxCompactions": 1, "maxToolOutputChars": 20000 },
285
+ "overBudget": true, "compactionLimitReached": true, "toolsPerTurn": 2.58,
286
+ "tracked": true,
287
+ "guard": { "available": true, "hooksRegistered": true, "sessionEnabled": true,
288
+ "toolOutputEnabled": true,
289
+ "toolOutputTools": ["shell", "bash", "webfetch"],
290
+ "compactionSource": "event", "eventLive": true,
291
+ "streamFailures": 0, "unkeyedCalls": 0 } }
292
+ ```
293
+
294
+ `sessionID` may name **any session on this server**, not only a child of the caller;
295
+ that is deliberate — the orchestrator uses the report to control every slice it runs.
296
+ Counters are per plugin instance, and `tracked` is `true` once any event or hook
297
+ observed the session; a `session.deleted` event forgets it. `band` is the highest
298
+ budget level reached (`0` below the soft threshold); `promptTokensMedian` is the
299
+ median over the last 256 requests; `toolCalls` and the per-tool counters include
300
+ nested Code Mode calls; `toolCallsByTool` and `toolOutputsCappedByTool` split calls
301
+ and capped outputs by the normalized tool name (namespace stripped, lower-case);
302
+ `outputCharsSaved` is a character count, not bytes, and an upper estimate (the
303
+ injected stub is subtracted). `compactions` counts confirmed
304
+ `session.compaction.ended` events only; `compactionAttempts` counts every
305
+ `session.compaction` hook call, so on a host with a dead or absent event stream
306
+ attempts keep growing while `compactions` and `compactionLimitReached` stay put (a
307
+ failed attempt is never a stop signal). A dead stream is reported once in
308
+ `guard.streamFailures`; the per-session `errors` counts hook failures only, so a
309
+ transport incident is not attributed to every session. Duplicate compaction events
310
+ are counted once only when the host supplies `event.id` or a durable position;
311
+ without either identity a re-delivered event may be counted again (fail-open in the
312
+ direction of counting). A
313
+ session the store does not return and this instance never observed is
314
+ `E_NOT_FOUND` (recognized by the typed `_tag`/`name`/`code` — `Session.NotFoundError`,
315
+ `SessionNotFoundError`, `NotFoundError`, `ENOENT` — with a narrow message fallback);
316
+ `session.get` returning `undefined` for an untracked session is the same not-found
317
+ answer; a transient `session.get` failure (a lock, permission, `unknown database`) is
318
+ `E_INTERNAL`; a known session with zero requests is a valid report with
319
+ `tracked: false`. Without `session.get`,
320
+ an untracked session is `E_NO_API`, a tracked one is `ok` with `session: null` and a
321
+ `limitations` entry. Codes: `E_INPUT`, `E_NOT_FOUND`, `E_INTERNAL`, `E_NO_API`,
322
+ `E_UNAVAILABLE` (the guard is not registered or its hooks failed), `E_ABORTED`.
323
+
324
+ ## Session hygiene
325
+
326
+ The plugin implements the [`session-discipline.md`](./docs/en/session-discipline.md)
327
+ protocol: a session is one slice, the prompt budget is ≤150k tokens, at most one
328
+ compaction, tool output goes to a file, and a slice ends with a handoff.
329
+
330
+ The runtime part is three hooks (`session.context`, `session.compaction`,
331
+ `tool.execute.after`) plus a compaction-event subscription:
332
+
333
+ - `session.context` — estimates the prompt (system + messages) and, when it crosses
334
+ a budget level, adds one short reminder. The reminder is injected **once, when a
335
+ new level is crossed**, not on every request; within a level the text is stable,
336
+ so the prefix cache is not broken.
337
+ - compaction events — the `session.compaction` hook is registered on **every** host:
338
+ it counts attempts (`compactionAttempts`) and sends a tentative note to the
339
+ nearest request; the note names no compaction number and claims no limit. Only
340
+ `session.compaction.ended` confirms a compaction (deduplicated by `event.id` or
341
+ the durable position) and raises a confirmed note with the number and the limit;
342
+ `session.compaction.failed` increments `compactionFailures`, drops the tentative
343
+ note and keeps a confirmed note that is still waiting for delivery. If the event
344
+ stream ends or throws, `guard.eventLive` turns `false`,
345
+ `guard.compactionSource` becomes `hook` and `guard.streamFailures` becomes `1`
346
+ (the per-session `errors` stays `0`); the hook keeps counting attempts, but no
347
+ confirmed compaction and no false "limit reached" is invented on that path.
348
+ - `tool.execute.after` — for `shell`, `bash`, `webfetch` it caps output over
349
+ `maxChars`: the full text goes to a private per-process root
350
+ `os.tmpdir()/opencode-tool-output-<random>/` (POSIX 0700 for the root and the
351
+ session directory, 0600 for the file). Windows ignores mode (measured: 0666),
352
+ so privacy there rests on the ACL of `os.tmpdir()` and is not checked by code.
353
+ The model gets a head and the path; the root is created with `mkdtemp` at plugin
354
+ setup, so a
355
+ pre-created `opencode-tool-output` directory or junction is never used; on POSIX
356
+ the root must belong to the current uid with no group/other access, otherwise
357
+ the guard disables itself with a warning (fail-open: results are untouched).
358
+ Session directories (`s-<uuid>`) and files (`c-<uuid>.txt`, written with `wx`)
359
+ are random, and each `realpath` is proven inside the root. The root is removed
360
+ when the plugin unloads (best effort). A lone surrogate is normalized once
361
+ (U+FFFD), so the file, the head and the counters describe the same string.
362
+ Residual risk accepted: a same-user
363
+ attacker can still swap a session directory between `realpath` and `writeFile`
364
+ (Node has no `openat`). If `TMPDIR` points inside the workspace, the files land
365
+ there instead. `read`, `write`, `edit`, `execute` and the kit's own tools are
366
+ not capped, and a kit tool named without its namespace (`session_stats`) is
367
+ refused in the config with a warning.
368
+
369
+ `maxCompactions: 0` disables the compaction limit. The guard is fail-open: a hook
370
+ error neither breaks the request nor corrupts the result. Either part can be
371
+ disabled with its `enabled` flag:
372
+
373
+ ```jsonc
374
+ {
375
+ "plugins": [
376
+ {
377
+ "package": "opencode-orchestrator-uni",
378
+ "options": {
379
+ "sessionGuard": { "enabled": true, "promptTokensSoft": 150000, "maxCompactions": 1 },
380
+ "toolOutputGuard": {
381
+ "enabled": true,
382
+ "maxChars": 20000,
383
+ "headChars": 4000,
384
+ "tools": ["shell", "bash", "webfetch"]
385
+ }
386
+ }
387
+ }
388
+ ]
389
+ }
390
+ ```
391
+
392
+ Slice metrics — `orch_uni_session_stats`; closing a slice — the `/handoff` command.
393
+
264
394
  ## Models
265
395
 
266
396
  Agent definitions carry no `model`; models are configured on the plugin entry in
@@ -408,7 +538,7 @@ npm run lint:fix # biome check --write . (applies safe fixes)
408
538
 
409
539
  The `check:locales` and `verify:kit` scripts need a git checkout of the kit — the npm package ships no `tools/` directory, so in an installed package they are unavailable (`stand: unavailable` plus your own reproducible checks).
410
540
 
411
- `npm test` scans only `./test/` — the shipped suite (289 tests in 9 files, up from
541
+ `npm test` scans only `./test/` — the shipped suite (400 tests in 10 files, up from
412
542
  72 in 5 before the identifier guards). A bare `bun test` from the repository root is red by
413
543
  design: the positional argument of `bun test` is a path substring filter, so the
414
544
  bare command also picks up the intentionally failing fixtures under `eval/cases/`
package/README.ru.md CHANGED
@@ -12,11 +12,11 @@
12
12
  - `fix-runner` — единственный пишущий субагент (применяет список «к закрытию»);
13
13
  - процессные документы комплекта, доступные как **reference `audit-kit`**
14
14
  (`call-mechanics.md`, `process-ready-loop.md`, `root-runbook.md`,
15
- `instruction-other-session.md`, `context-efficiency.md`);
16
- - четыре детерминированных read-only инструмента — `orch_uni_validate_report`,
17
- `orch_uni_context_lint`, `orch_uni_snapshot`, `orch_uni_sessions`
18
- (см. [Инструменты](#инструменты));
19
- - две слэш-команды — `/agents-md` и `/spec` (см. [Команды](#команды)).
15
+ `instruction-other-session.md`, `context-efficiency.md`, `session-discipline.md`);
16
+ - пять детерминированных read-only инструментов — `orch_uni_validate_report`,
17
+ `orch_uni_context_lint`, `orch_uni_snapshot`, `orch_uni_sessions`,
18
+ `orch_uni_session_stats` (см. [Инструменты](#инструменты));
19
+ - три слэш-команды — `/agents-md`, `/spec` и `/handoff` (см. [Команды](#команды)).
20
20
 
21
21
  Read-only аудиторы держатся на `edit: deny` плюс запрете в промпте; shell-разрешение
22
22
  остаётся, чтобы запускать проверки (принятый риск, ни одно разрешение не ослаблено).
@@ -57,8 +57,8 @@ Reference `audit-kit` указывает на per-locale каталог `docs/<l
57
57
  может читать процессные документы напрямую, например
58
58
  `<reference path>/call-mechanics.md`.
59
59
 
60
- Плагин также регистрирует четыре детерминированных **read-only** инструмента в
61
- пространстве имён `orch_uni` и две слэш-команды — см. [Инструменты](#инструменты)
60
+ Плагин также регистрирует пять детерминированных **read-only** инструментов в
61
+ пространстве имён `orch_uni` и три слэш-команды — см. [Инструменты](#инструменты)
62
62
  и [Команды](#команды).
63
63
 
64
64
  ## Команды
@@ -67,6 +67,7 @@ Reference `audit-kit` указывает на per-locale каталог `docs/<l
67
67
  | --- | --- |
68
68
  | `/agents-md` | Вставляет workflow аудита/черновика AGENTS.md: шаблон `context-efficiency.md`, `orch_uni_context_lint`, `context-lint`, подтверждение перед записью. |
69
69
  | `/spec` | Вставляет workflow SPEC.md: бриф, уточняющие вопросы, EARS/AC/трассировка, `orch_uni_context_lint`, `context-lint`, подтверждение перед записью. |
70
+ | `/handoff` | Вставляет workflow закрытия среза: факты сессии и рабочего дерева, шаблон handoff из `session-discipline.md`, запись `docs/planning/handoff-<YYYY-MM-DD>.md`. |
70
71
 
71
72
  Команда — это промпт, а не писатель: обработчик вставляет workflow-промпт в
72
73
  текущую сессию. Дальше оркестратор работает по обычным правилам evidence и
@@ -80,10 +81,11 @@ Reference `audit-kit` указывает на per-locale каталог `docs/<l
80
81
  | `orch_uni_context_lint` | да | Детерминированный аудит AGENTS.md/SPEC.md: бюджет, vague, ссылки, секции |
81
82
  | `orch_uni_snapshot` | да | Детерминированная ревизия дерева каталогов (sha256 на файл + агрегатный хэш) |
82
83
  | `orch_uni_sessions` | да | Дочерние сессии сессии: агент, модель, стоимость, токены, время |
84
+ | `orch_uni_session_stats` | да | Счётчики протокола сессии: промпт, компакции, тулы/ход, урезанные выводы |
83
85
 
84
86
  Каждый инструмент отвечает JSON-текстом. Ошибки — **типизированные коды** внутри
85
87
  тела (`E_INPUT`, `E_NO_JSON`, …), а не исключения, чтобы агент мог ветвиться по
86
- коду, не разбирая прозу. Ни один из четырёх ничего не пишет в воркспейс, хранилище
88
+ коду, не разбирая прозу. Ни один из пяти ничего не пишет в воркспейс, хранилище
87
89
  сессий или конфигурацию.
88
90
 
89
91
  ### `orch_uni_validate_report`
@@ -263,6 +265,136 @@ Commands, HTML-комментарий вроде `<!-- npm install -->` кома
263
265
  `E_INPUT` при некорректных аргументах, `E_NOT_FOUND`, когда родительской сессии
264
266
  нет, `E_ABORTED`, когда сессия остановлена.
265
267
 
268
+ ### `orch_uni_session_stats`
269
+
270
+ ```jsonc
271
+ {} // счётчики вызывающей сессии
272
+ { "sessionID": "ses_…" } // любая сессия на этом сервере, не только дочерняя
273
+ ```
274
+
275
+ ```jsonc
276
+ { "ok": true, "sessionID": "ses_…",
277
+ "counters": {
278
+ "requests": 12, "promptTokensLast": 151204, "promptTokensMax": 161002,
279
+ "promptTokensMedian": 148900, "band": 1, "compactions": 1,
280
+ "compactionAttempts": 1, "compactionFailures": 0, "compactionSource": "event",
281
+ "reminders": 2, "toolCalls": 31, "outputsCapped": 1,
282
+ "outputCharsSaved": 18422, "errors": 0,
283
+ "toolCallsByTool": { "shell": 18, "bash": 6, "webfetch": 2, "read": 5 },
284
+ "toolOutputsCappedByTool": { "shell": 1, "bash": 0, "webfetch": 0, "read": 0 }
285
+ },
286
+ "limits": { "promptTokensSoft": 150000, "maxCompactions": 1, "maxToolOutputChars": 20000 },
287
+ "overBudget": true, "compactionLimitReached": true, "toolsPerTurn": 2.58,
288
+ "tracked": true,
289
+ "guard": { "available": true, "hooksRegistered": true, "sessionEnabled": true,
290
+ "toolOutputEnabled": true,
291
+ "toolOutputTools": ["shell", "bash", "webfetch"],
292
+ "compactionSource": "event", "eventLive": true,
293
+ "streamFailures": 0, "unkeyedCalls": 0 } }
294
+ ```
295
+
296
+ `sessionID` может называть **любую сессию на этом сервере**, не только дочернюю
297
+ вызывающего; это осознанно — по отчёту оркестратор контролирует все свои срезы.
298
+ Счётчики принадлежат инстансу плагина, а `tracked` равен `true`, как только сессию
299
+ наблюдало любое событие или хук; событие `session.deleted` её забывает. `band` —
300
+ максимум достигнутого уровня бюджета (`0` ниже мягкого порога);
301
+ `promptTokensMedian` — медиана по окну последних 256 запросов; `toolCalls` и
302
+ per-tool счётчики включают вложенные вызовы Code Mode; `toolCallsByTool` и
303
+ `toolOutputsCappedByTool` разносят вызовы и урезанные выводы по нормализованному
304
+ имени инструмента (namespace снят, регистр нижний); `outputCharsSaved` — число
305
+ символов, не байтов, и верхняя оценка (внедрённая заглушка вычитается).
306
+ `compactions` считает только подтверждённые `session.compaction.ended`;
307
+ `compactionAttempts` — каждый вызов хука `session.compaction`, поэтому на хосте с
308
+ умершим или отсутствующим потоком событий попытки растут, а `compactions` и
309
+ `compactionLimitReached` стоят (неудачная попытка не становится стоп-сигналом).
310
+ Умерший поток событий один раз виден в `guard.streamFailures`; per-session
311
+ `errors` считает только ошибки хуков, поэтому транспортная авария не
312
+ приписывается каждой сессии. Дубликаты событий компакции считаются один раз
313
+ только при наличии `event.id` или durable-позиции; без identity повторная
314
+ доставка может пересчитать событие (fail-open в сторону счёта).
315
+ Сессия, которую не отдаёт хранилище и не наблюдал этот инстанс, — `E_NOT_FOUND`
316
+ (распознаётся по типизированным `_tag`/`name`/`code` — `Session.NotFoundError`,
317
+ `SessionNotFoundError`, `NotFoundError`, `ENOENT` — с узким fallback по сообщению);
318
+ `session.get`, вернувший `undefined` для ненаблюдаемой сессии, — тот же ответ
319
+ «не найдено»; transient-ошибка `session.get` (блокировка, права, `unknown
320
+ database`) — `E_INTERNAL`; известная сессия без запросов — валидный отчёт с
321
+ `tracked: false`. Без `session.get` ненаблюдаемая сессия —
322
+ `E_NO_API`, наблюдаемая — `ok` с `session: null` и записью в `limitations`.
323
+ Коды: `E_INPUT`, `E_NOT_FOUND`, `E_INTERNAL`, `E_NO_API`, `E_UNAVAILABLE`
324
+ (guard не зарегистрирован или его хуки не установились), `E_ABORTED`.
325
+
326
+ ## Гигиена сессии
327
+
328
+ Плагин реализует протокол [`session-discipline.md`](./docs/ru/session-discipline.md):
329
+ сессия = один срез, бюджет промпта ≤150k токенов, ≤1 компакция, вывод инструментов
330
+ — в файл, в конце среза — handoff.
331
+
332
+ Runtime-часть — три хука (`session.context`, `session.compaction`,
333
+ `tool.execute.after`) и подписка на события компакции:
334
+
335
+ - `session.context` — оценивает промпт (system + messages) и при переходе на
336
+ новый уровень бюджета добавляет одно короткое напоминание. Напоминание
337
+ инжектится **один раз при переходе уровня**, а не в каждом запросе; внутри
338
+ уровня текст стабилен, поэтому префикс-кэш не рвётся.
339
+ - события компакции — хук `session.compaction` регистрируется **всегда**: он
340
+ считает попытки (`compactionAttempts`) и отдаёт tentative-напоминание ближайшему
341
+ запросу; напоминание не называет номер компакции и не заявляет лимит.
342
+ Подтверждает компакцию только `session.compaction.ended` (дедуп по `event.id`
343
+ или durable-позиции) и выставляет confirmed-note с номером и лимитом;
344
+ `session.compaction.failed` увеличивает `compactionFailures`, снимает
345
+ tentative-note и сохраняет confirmed-note, ещё ждущий доставки. Если поток
346
+ событий завершился или упал, `guard.eventLive` становится `false`,
347
+ `guard.compactionSource` — `"hook"`, а `guard.streamFailures` — `1`
348
+ (per-session `errors` остаётся `0`); хук продолжает считать попытки, но
349
+ подтверждённых компакций и ложного «лимит исчерпан» на этом пути нет.
350
+ - `tool.execute.after` — для `shell`, `bash`, `webfetch` урезает вывод больше
351
+ `maxChars`: полный текст пишется в приватный корень на инстанс плагина
352
+ `os.tmpdir()/opencode-tool-output-<random>/` (POSIX: 0700 на корень и каталог
353
+ сессии, 0600 на файл), модель получает голову и путь. На Windows mode не
354
+ действует (замер: 0666), приватность там держится только на ACL каталога
355
+ `os.tmpdir()` и кодом не проверяется. Корень создаётся `mkdtemp` при setup,
356
+ поэтому заранее созданный каталог или
357
+ junction `opencode-tool-output` не используется; на POSIX корень обязан
358
+ принадлежать текущему uid и не давать доступа group/other, иначе guard
359
+ отключается с warning (fail-open: результат не трогается). Каталог сессии
360
+ (`s-<uuid>`) и файл (`c-<uuid>.txt`, запись `wx`) — случайные, их `realpath`
361
+ проверяется внутри корня. При выгрузке плагина корень удаляется best effort.
362
+ Строка с lone surrogate нормализуется один раз (U+FFFD), поэтому файл, голова и
363
+ счётчики описывают одну и ту же строку. Остаточный риск принят: same-user
364
+ атакующий всё ещё может подменить каталог
365
+ сессии между `realpath` и `writeFile` (в Node нет `openat`). Если `TMPDIR`
366
+ указывает внутрь воркспейса, файлы попадут туда. `read`, `write`, `edit`,
367
+ `execute` и инструменты комплекта не урезаются; kit-имя без namespace
368
+ (`session_stats`) отбрасывается в конфиге с warning.
369
+
370
+ `maxCompactions: 0` отключает лимит компакций. `guard.toolOutputEnabled` равен
371
+ `true` только когда приватный корень создан **и** эффективный список инструментов
372
+ непуст; `guard.toolOutputTools` — этот список, `guard.unkeyedCalls` — вызовы
373
+ хуков без непустого `sessionID` (отбрасываются, а не сливаются в бакет `""`).
374
+ Пустой `tools` выключает кап с warning. Guard fail-open: ошибка хука не
375
+ ломает запрос и не портит результат. Отключается флагами `enabled`:
376
+
377
+ ```jsonc
378
+ {
379
+ "plugins": [
380
+ {
381
+ "package": "opencode-orchestrator-uni",
382
+ "options": {
383
+ "sessionGuard": { "enabled": true, "promptTokensSoft": 150000, "maxCompactions": 1 },
384
+ "toolOutputGuard": {
385
+ "enabled": true,
386
+ "maxChars": 20000,
387
+ "headChars": 4000,
388
+ "tools": ["shell", "bash", "webfetch"]
389
+ }
390
+ }
391
+ }
392
+ ]
393
+ }
394
+ ```
395
+
396
+ Метрики среза — `orch_uni_session_stats`; закрытие среза — команда `/handoff`.
397
+
266
398
  ## Модели
267
399
 
268
400
  Определения агентов не несут `model`; модели настраиваются у записи плагина в
@@ -414,7 +546,7 @@ npm run lint:fix # biome check --write . (применяет безопас
414
546
 
415
547
  Команды `check:locales` и `verify:kit` требуют git-чекаута комплекта — в npm-пакете каталог `tools/` не поставляется, поэтому в установленном пакете они недоступны (`stand: unavailable` и собственные воспроизводимые проверки).
416
548
 
417
- `npm test` сканирует только `./test/` — поставляемый набор (289 тестов в 9 файлах;
549
+ `npm test` сканирует только `./test/` — поставляемый набор (400 тестов в 10 файлах;
418
550
  до стражей идентификаторов было 72 в 5). Голый `bun test` из корня репозитория
419
551
  красный по замыслу: позиционный аргумент `bun test` — это фильтр-подстрока пути,
420
552
  поэтому голая команда подхватывает и специально падающие фикстуры в
@@ -26,6 +26,15 @@ GENERAL CYCLE:
26
26
  6. Drive it to the end: fixes → re-check → until the criteria are met. No "left for later" without an explicit record to the owner.
27
27
  7. Outcome: a short summary — what was done, what confirms it, what remains or was accepted as a risk.
28
28
 
29
+ SESSION HYGIENE (the `session-discipline.md` protocol):
30
+ - One slice = one session. Budget: estimated prompt ≤150k tokens, ≤1 compaction; when the limit is reached — `/handoff`, commit and a new session, no pushing on.
31
+ - Investigation and verification go to subagents (clean context, 1–2k tokens returned); implementation goes to one writer; consolidation and decisions are yours.
32
+ - Batching: ≥3 independent commands per turn or one script; for reads use `read` with `limit`/`offset`, not `cat`.
33
+ - Output over 20k chars — to a file and a path; screenshots — as a file, not base64; in `execute` limit the result yourself.
34
+ - Default to the cheap model; the expensive one only for decisions and reviews.
35
+ - At the end of a slice — a handoff per the template and a commit; the next session starts from `state` and SPEC.
36
+ - Slice metrics — `orch_uni_session_stats` (prompt, compactions, tools/turn, capped outputs).
37
+
29
38
  The AUDIT MODE turns on when the task is to verify a change (diff/PR/range), a plan (document/ADR) or a release (tag/publication): the loop through to full ready per the kit's documents (`call-mechanics.md`, `process-ready-loop.md`, `root-runbook.md`, `instruction-other-session.md`) — in the kit's directory (the directory is set by the owner/the task). If the documents are unavailable — work by the inline skeleton below and tell the owner. Release gates = publication of a release/tag; at these gates `meta` (`meta`) is mandatory.
30
39
  - Chain: staleness fact-check (`factcheck`) → the `empiric` ∥ `invariant` pair → arbiter (`arbiter`) on discrepancies → fixes → re-audit of the same sessions → until full ready; at release gates — `redteam` and `meta`.
31
40
  - Restricted environment (Code Mode: actions only via `execute`+directory, `subagent`/direct tools unavailable): before the audit phase, check that `subagent` is available; if not — do not start the loop. The outcome in such an environment is "done; the regular audit is deferred" (state revision + compensating checks as a list + a plan for a run in a normal session); a self-check and an `ai` review do not count as an audit (details — `call-mechanics.md` §8).
@@ -26,6 +26,15 @@ permissions:
26
26
  6. Доводи до конца: исправления → повторная проверка → пока критерии не выполнены. Без «оставил на потом» без явной фиксации владельцу.
27
27
  7. Итог: краткая сводка — что сделано, чем подтверждено, что осталось или принято как риск.
28
28
 
29
+ ГИГИЕНА СЕССИИ (протокол `session-discipline.md`):
30
+ - Один срез = одна сессия. Бюджет: оценка промпта ≤150k токенов, ≤1 компакция; при пороге — `/handoff`, коммит и новая сессия, без «дожимания».
31
+ - Разведка и проверка — субагентам (чистый контекст, ответ 1–2k токенов); реализация — одному писателю; сведение и решения — тебе.
32
+ - Батчинг: ≥3 независимых команд за ход или один скрипт; для чтения — `read` с `limit`/`offset`, а не `cat`.
33
+ - Вывод >20k символов — в файл и путём; скриншоты — файлом, не base64; в `execute` ограничивай результат сам.
34
+ - Модель по умолчанию — дешёвая; дорогая — только решения и ревью.
35
+ - В конце среза — handoff по шаблону и commit; следующая сессия стартует с `state` и SPEC.
36
+ - Метрики среза — `orch_uni_session_stats` (промпт, компакции, тулы/ход, урезанные выводы).
37
+
29
38
  АУДИТОРСКИЙ РЕЖИМ включается, когда задача — проверить изменение (diff/PR/диапазон), план (документ/ADR) или релиз (тег/публикация): цикл до полного ready по документам комплекта (`call-mechanics.md`, `process-ready-loop.md`, `root-runbook.md`, `instruction-other-session.md`) — в каталоге комплекта (каталог задаётся владельцем/заданием). Если документы недоступны — работай по инлайн-скелету ниже и сообщи владельцу. Релизные ворота = публикация релиза/тега; при них мета (`meta`) обязательна.
30
39
  - Цепочка: факт-чек актуальности (`factcheck`) → пара `empiric` ∥ `invariant` → арбитр (`arbiter`) на расхождения → правки → повторный аудит тех же сессий → до полного ready; на релизных воротах — `redteam` и `meta`.
31
40
  - Ограниченная среда (Code Mode: действия только через `execute`+каталог, `subagent`/прямые инструменты недоступны): перед аудиторской фазой проверь наличие `subagent`; нет — цикл не запускай. Итог в такой среде — «готово; штатный аудит отложен» (ревизия + компенсирующие проверки списком + план прогона в обычной сессии); самопроверка и `ai`-ревью аудитом не называются (детали — `call-mechanics.md` §8).
@@ -5,6 +5,8 @@
5
5
  - **Root (the primary agent)** — does not audit: forms the brief, launches subagents, consolidates the reports; for the "unprovable" — the owner's decision: the root frames the question, records the risk and escalates. Maintains the registry of accepted risks, makes fixes as separate commits, decides on "ship".
6
6
  - **Subagents** (read-only): `factcheck`, `empiric`, `invariant`, `arbiter`, (at the gates) `redteam`, `meta`.
7
7
 
8
+ Slice session hygiene — boundaries, context budget (≤150k tokens, ≤1 compaction) and the handoff — in `session-discipline.md`.
9
+
8
10
  ## Brief template `case-<id>.md` (common to all steps)
9
11
 
10
12
  ```