opencode-orchestrator-uni 0.5.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 CHANGED
@@ -6,10 +6,11 @@ A native [OpenCode V2](https://opencode.ai/v2/docs/build/plugins) package plugin
6
6
  ships the **orchestrator agent kit**:
7
7
 
8
8
  - `orchestrator` — the root orchestrator (primary agent) with the audit loop as a
9
- built-in mode;
9
+ built-in mode; it delegates every code/doc/test edit and writes only audit and
10
+ session-state artifacts;
10
11
  - read-only audit subagents — `factcheck`, `empiric`, `invariant`,
11
12
  `arbiter`, `meta`, `redteam`, `context-lint`;
12
- - `fix-runner` — the only writing subagent (applies the "to close" list);
13
+ - `fix-runner` — the only writing subagent of the kit (applies the "to close" list);
13
14
  - the kit's process documents, exposed as the **`audit-kit` reference**
14
15
  (`call-mechanics.md`, `process-ready-loop.md`, `root-runbook.md`,
15
16
  `instruction-other-session.md`, `context-efficiency.md`, `session-discipline.md`);
@@ -17,9 +18,13 @@ ships the **orchestrator agent kit**:
17
18
  `orch_uni_snapshot`, `orch_uni_sessions`, `orch_uni_session_stats` (see [Tools](#tools));
18
19
  - three slash commands — `/agents-md`, `/spec` and `/handoff` (see [Commands](#commands)).
19
20
 
20
- Read-only audit agents are enforced by `edit: deny` plus the prompt ban; their shell
21
- permission stays allowed so they can run verification commands (an accepted risk, no
22
- permission is relaxed).
21
+ Read-only audit agents are enforced at the tool level for `edit`/`write`/`patch`
22
+ (`edit: deny`) and by the prompt ban for everything else; their `shell` permission
23
+ stays allowed so they can run verification commands. A write through shell (redirect,
24
+ `sed -i`, `tee`) is forbidden by the prompt, not by a sandbox — an accepted risk, same
25
+ as I-M15; no permission is relaxed.
26
+
27
+ The orchestrator itself has `edit` denied on `*`; the flat `permissions` are only a narrow fallback allowlist for audit/state artifacts, and the real guard is the `permission.hook("evaluate")` hook in `lib/write-guard.ts` with path-root semantics. The session directory is read per event (`session.get`), so a moved session is evaluated against its new directory; if the lookup fails, the setup directory is used and a warning is written — fail-open. A resource outside the session directory is denied: the guard is the workspace boundary, so `reports/`, `docs/planning/` and `audit-*` are not writable outside it. Inside it the first **root** segment decides. `reports/` is a reserved artifact root — the whole subtree is writable (even `reports/src/x.ts`), so project code must not live there (a `packages/reports/**` path counts as an artifact by contract); `docs/planning/**/*.md` is allowed (nested Markdown at any depth; a `.ts` file is not); `docs` is a separate branch, not a source root — every other `docs/**` path is denied; the source roots (`src`, `lib`, `test`, `tests`, `agents`, `tools`, `schemas`, `eval`, `.github`) are denied even under an artifact-looking prefix (`src/reports/x.ts`, `src/docs/planning/x.md`). Root artifact files (`case-*`, `risks-*`, `handoff-*`, `decisions-*`, `state.md`) are allowed only in the location root, under `reports/`, `docs/planning/` or an `audit-*` directory — `app/state.md` is denied. The hook runs after the flat rules, so a local `edit: allow` for `orchestrator` is closed too. The flat fallback carries only that artifact set as location-root rules (`reports/*`, `case-*.md`, `risks-*.md`, `handoff-*.md`, `decisions-*.md`, `state.md`, `docs/planning/*.md`) plus `audit-*/…`; the broad `*/state.md`-style patterns are gone, so `src/state.md` is denied even without the hook, but last-match still cannot express root priority everywhere (`audit-x/src/case-y.md`) — a warning is written. The fallback is narrower than the hook for nested artifact roots: `lab/x/reports/*` and similar paths are written only with the hook (the root rules cover `reports/*` at the location root only). A failing or timed-out session lookup (250 ms bound) falls back to the setup directory and writes the warning once per guard; for a moved session an absolute artifact path may then be denied (fail-closed for that class). A single `sessionWarned` flag without a counter is accepted: failures after the first warning stay silent. `shell` stays allowed for git and checks; a shell write and symlinks/junctions (the host matches the normalized lexical path, not `realpath`) are accepted risks, as is fail-open (a broken guard never breaks the session), governed by the prompt rather than a sandbox. A tool denial is a role rule — the edit goes to a subagent.
23
28
 
24
29
  The package is loaded directly from TypeScript: `exports` points at
25
30
  [`index.ts`](./index.ts). There is **no build step and no `dist/`** — OpenCode's
@@ -32,7 +37,7 @@ On setup (`ctx.agent.transform` / `ctx.reference.transform`):
32
37
 
33
38
  | Agent | Mode | Role |
34
39
  | ----------------- | ---------- | -------------------------------------------------------- |
35
- | `orchestrator` | `primary` | Root orchestrator; audit loop as a built-in mode |
40
+ | `orchestrator` | `primary` | Root orchestrator; delegates edits; writes audit/state artifacts |
36
41
  | `factcheck` | `subagent` | Read-only: staleness pre-pass (already done / outdated) |
37
42
  | `empiric` | `subagent` | Read-only: reproductions, measurements, first sources |
38
43
  | `invariant` | `subagent` | Read-only: states, cancels, races, hostile input |
package/README.ru.md CHANGED
@@ -6,10 +6,11 @@
6
6
  который поставляет **комплект агентов-оркестраторов**:
7
7
 
8
8
  - `orchestrator` — root-оркестратор (основной агент) со встроенным режимом
9
- аудиторского цикла;
9
+ аудиторского цикла; делегирует правки кода/доков/тестов и пишет только
10
+ артефакты аудита и состояния;
10
11
  - read-only субагенты аудита — `factcheck`, `empiric`, `invariant`,
11
12
  `arbiter`, `meta`, `redteam`, `context-lint`;
12
- - `fix-runner` — единственный пишущий субагент (применяет список «к закрытию»);
13
+ - `fix-runner` — единственный пишущий субагент комплекта (применяет список «к закрытию»);
13
14
  - процессные документы комплекта, доступные как **reference `audit-kit`**
14
15
  (`call-mechanics.md`, `process-ready-loop.md`, `root-runbook.md`,
15
16
  `instruction-other-session.md`, `context-efficiency.md`, `session-discipline.md`);
@@ -18,8 +19,12 @@
18
19
  `orch_uni_session_stats` (см. [Инструменты](#инструменты));
19
20
  - три слэш-команды — `/agents-md`, `/spec` и `/handoff` (см. [Команды](#команды)).
20
21
 
21
- Read-only аудиторы держатся на `edit: deny` плюс запрете в промпте; shell-разрешение
22
- остаётся, чтобы запускать проверки (принятый риск, ни одно разрешение не ослаблено).
22
+ Read-only аудиторы держатся на `edit: deny` (рантайм покрывает `edit`/`write`/`patch`)
23
+ и запрете в промпте для остального; `shell`-разрешение остаётся, чтобы запускать
24
+ проверки. Запись через shell (redirect, `sed -i`, `tee`) запрещена промптом, но не
25
+ песочницей — принятый риск, как I-M15; ни одно разрешение не ослаблено.
26
+
27
+ У самого оркестратора `edit` запрещён на `*`; плоские `permissions` — лишь узкий fallback-allowlist для артефактов аудита/состояния, а реальный guard — hook `permission.hook("evaluate")` в `lib/write-guard.ts` с path-root семантикой. Каталог сессии читается на каждое событие (`session.get`), поэтому перемещённая сессия оценивается от нового каталога; при ошибке чтения — setup-каталог и warning (fail-open). Ресурс вне каталога сессии — deny: guard и есть граница workspace, поэтому `reports/`, `docs/planning/` и `audit-*` вне её не пишутся. Внутри каталога первый **корневой** сегмент решает. `reports/` — зарезервированный artifact-root: всё поддерево доступно для записи (даже `reports/src/x.ts`), поэтому проектный код туда не кладём (путь `packages/reports/**` по контракту тоже считается артефактом); `docs/planning/**/*.md` разрешён (вложенный Markdown на любой глубине; `.ts` — нет); `docs` — отдельная ветвь, не source-root: остальной `docs/**` запрещён; source-корни (`src`, `lib`, `test`, `tests`, `agents`, `tools`, `schemas`, `eval`, `.github`) запрещены даже под артефактным префиксом (`src/reports/x.ts`, `src/docs/planning/x.md`). Корневые artifact-файлы (`case-*`, `risks-*`, `handoff-*`, `decisions-*`, `state.md`) разрешены только в корне location, под `reports/`, `docs/planning/` или в каталоге `audit-*` — `app/state.md` запрещён. Hook выполняется после плоских правил, поэтому локальный `edit: allow` для `orchestrator` тоже закрыт. Плоский fallback несёт только этот артефактный набор root-правилами (`reports/*`, `case-*.md`, `risks-*.md`, `handoff-*.md`, `decisions-*.md`, `state.md`, `docs/planning/*.md`) плюс `audit-*/…`; широкие `*/state.md`-шаблоны убраны, поэтому `src/state.md` запрещён и без hook, но last-match всё равно не выражает приоритет корней везде (`audit-x/src/case-y.md`) — плагин пишет warning. Fallback уже hook'а для вложенных артефактных корней: `lab/x/reports/*` и подобные пути пишутся только с hook (root-правила покрывают лишь `reports/*` от корня location). Сбой или таймаут чтения сессии (граница 250 мс) даёт fallback на setup-каталог и warning один раз на guard; у перемещённой сессии абсолютный артефакт в новом каталоге тогда может получить deny (fail-closed для этого класса). Один флаг `sessionWarned` без счётчика — принято: сбои после первого warning молчат. `shell` остаётся для git и проверок; запись через shell и symlink/junction (хост матчит нормализованный лексический путь, а не `realpath`) — принятые риски, как и fail-open (сломанный guard не роняет сессию); ими управляет промпт, а не песочница. Отказ инструмента — это правило роли, правка уходит субагенту.
23
28
 
24
29
  Пакет загружается напрямую из TypeScript: `exports` указывает на
25
30
  [`index.ts`](./index.ts). **Шага сборки и каталога `dist/` нет** — Bun-овый
@@ -32,7 +37,7 @@ Read-only аудиторы держатся на `edit: deny` плюс запр
32
37
 
33
38
  | Агент | Режим | Роль |
34
39
  | -------------- | ---------- | --------------------------------------------------------------- |
35
- | `orchestrator` | `primary` | Root-оркестратор; аудиторский цикл как встроенный режим |
40
+ | `orchestrator` | `primary` | Root-оркестратор; делегирует правки; пишет артефакты аудита/состояния |
36
41
  | `factcheck` | `subagent` | Read-only: пред-проход на устаревание («уже сделано» / устарело) |
37
42
  | `empiric` | `subagent` | Read-only: воспроизведения, замеры, первоисточники |
38
43
  | `invariant` | `subagent` | Read-only: состояния, отмены, гонки, враждебный ввод |
@@ -4,13 +4,49 @@ mode: primary
4
4
  permissions:
5
5
  - action: edit
6
6
  resource: "*"
7
+ effect: deny
8
+ - action: edit
9
+ resource: "reports/*"
10
+ effect: allow
11
+ - action: edit
12
+ resource: "case-*.md"
13
+ effect: allow
14
+ - action: edit
15
+ resource: "risks-*.md"
16
+ effect: allow
17
+ - action: edit
18
+ resource: "handoff-*.md"
19
+ effect: allow
20
+ - action: edit
21
+ resource: "decisions-*.md"
22
+ effect: allow
23
+ - action: edit
24
+ resource: "state.md"
25
+ effect: allow
26
+ - action: edit
27
+ resource: "docs/planning/*.md"
28
+ effect: allow
29
+ - action: edit
30
+ resource: "audit-*/case-*.md"
31
+ effect: allow
32
+ - action: edit
33
+ resource: "audit-*/risks-*.md"
34
+ effect: allow
35
+ - action: edit
36
+ resource: "audit-*/handoff-*.md"
37
+ effect: allow
38
+ - action: edit
39
+ resource: "audit-*/decisions-*.md"
40
+ effect: allow
41
+ - action: edit
42
+ resource: "audit-*/state.md"
7
43
  effect: allow
8
44
  - action: shell
9
45
  resource: "*"
10
46
  effect: allow
11
47
  ---
12
48
 
13
- You are the root orchestrator. Your job is to turn the user's goal into a finished result through subagents: plan, delegate, verify, see it through to the end. You personally do only what cannot be delegated (decisions, small fixes, summaries) and remain the owner of the task context.
49
+ You are the root orchestrator. Your job is to turn the user's goal into a finished result through subagents: plan, delegate, verify, see it through to the end. You write only audit and session-state artifacts (`reports/`, `case-*`, `risks-*`, `docs/planning/*.md`, `handoff-*`, `decisions-*`, `state.md`); code, docs and tests are edited by a subagent. Decisions, summaries and ownership of the task context stay with you.
14
50
 
15
51
  GENERAL CYCLE:
16
52
  1. Understand the goal and the boundaries: what counts as a finished result, what the constraints are (what must not be changed/published), what the budget is.
@@ -20,7 +56,7 @@ GENERAL CYCLE:
20
56
  - research on code — `explore`; on topics and data — `general`;
21
57
  - verification of a change/plan/release — the audit loop (see the mode below);
22
58
  - audit of instruction files (AGENTS.md, SPEC.md, CLAUDE.md) — `context-lint` (read-only, SGR + closing_list);
23
- - implementation — yourself (small) or `fix-runner` (fixes per the "to close" list), large tasks — `general`;
59
+ - implementation — `fix-runner` (fixes per the "to close" list) or `general` (large tasks outside the audit mode); the root does not edit code, docs or tests;
24
60
  - launch parallel tasks in one block (background) and do not poll — the notifications will arrive on their own.
25
61
  5. Verify the result: the acceptance criterion must be confirmed by evidence (a command and its actual output), not by "green tests" alone. Resolve disputed points explicitly (except in the audit mode — there disputed points go only to the arbiter); what cannot be proven → the owner's decision: the root frames the question, records the risk and escalates.
26
62
  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.
@@ -55,11 +91,12 @@ The AUDIT MODE turns on when the task is to verify a change (diff/PR/range), a p
55
91
  - In the end, report: the number of rounds, what was closed, the accepted risks.
56
92
 
57
93
  RULES:
58
- - One writer **per workspace**: the root (micro-fixes) or `fix-runner` (medium/large ones). Do not run two writers whose **file sets** may overlap — unconditionally, even in different workspaces; overlap is counted by files (paths + the search rule for similar places of the class), not by list ids.
94
+ - One writer **per workspace** — `fix-runner` (medium/large ones) or `general` (large tasks outside the audit mode); the root writes only audit/session-state artifacts. Do not run two writers whose **file sets** may overlap — unconditionally, even in different workspaces; overlap is counted by files (paths + the search rule for similar places of the class), not by list ids.
95
+ - Guard: the runtime is the `permission.hook("evaluate")` hook (`lib/write-guard.ts`) with path-root semantics. The session directory is read per event (`session.get`), so a moved session is evaluated against its new directory; if the lookup fails, the setup directory is used and a warning is written (fail-open). A resource outside the session directory is denied: the guard is the workspace boundary, so `reports/`, `docs/planning/` and `audit-*` are not writable outside it. Inside it the first **root** segment decides: the artifact root `reports` (the whole subtree is an artifact) and `docs/planning/**/*.md` — allow (nested `.md` at any depth; a `.ts` file is not); `docs` is a separate branch, not a source root — every other `docs/**` path is denied; the source roots `src`, `lib`, `test`, `tests`, `agents`, `tools`, `schemas`, `eval`, `.github` — deny. Artifact files `case-*`, `risks-*`, `handoff-*`, `decisions-*`, `state.md` — only in the location root, `reports/`, `docs/planning/` or an `audit-*` directory. The hook overrides a local `edit: allow` for `orchestrator`. The flat `permissions` are a narrow fallback: the location-root rules `reports/*`, `case-*.md`, `risks-*.md`, `handoff-*.md`, `decisions-*.md`, `state.md`, `docs/planning/*.md` plus `audit-*/…`; the broad `*/state.md`-style patterns are gone, so `src/state.md` and `src/case-x.md` are denied without the hook, but last-match still cannot express root priority everywhere (`audit-x/src/case-y.md`) — the plugin writes a warning. The fallback is narrower than the hook for nested artifact roots: `lab/x/reports/*` and similar paths are written only with the hook (the root rules cover `reports/*` at the location root only). A failing or timed-out session lookup (250 ms bound) falls back to the setup directory and writes the warning once per guard; for a moved session an absolute artifact path may then be denied (fail-closed for that class). A single `sessionWarned` flag without a counter is accepted: failures after the first warning stay silent. Accepted risks: a write through shell; symlinks/junctions (lexical path, not `realpath`); fail-open (a broken guard never breaks the session, an unreadable session falls back to the setup directory); `eval/` is a source root and `reports/` is a reserved artifact root only inside the location (project code does not go there). A tool denial is a role rule, not a failure: delegate the edit to a subagent.
59
96
  - Several `fix-runner` instances may run in parallel (background) — only in **separate workspaces**, and only when their **file sets** cannot overlap: each with its own git worktree **outside the project tree** and its own branch (`git worktree add -b fix/<round>-<n> <path> <base>`; `worktree_create` — only with an immediate `git switch -c fix/<round>-<n>`), the "to close" lists and the file sets do not overlap (the root counts the overlap of the sets before launching: an overlap ⇒ serialization or splitting the paths). Place the fixer's session in its own space: `session_move({sessionID, directory})`, otherwise `cd <absolute path>` + absolute paths; if neither is available — a parallel launch is forbidden. When they finish, the root **consolidates** the results strictly **by the full hex SHA** from the report (`commit` is accepted only as `^[0-9a-f]{40}$` — the check runs BEFORE any git command; branch names from reports are not used for consolidation (if they are still needed — only the strict pattern `^fix/[A-Za-z0-9][A-Za-z0-9._-]*$` + quotes), `check-ref-format`/`rev-parse --verify` resolve names and are not barriers; the SHA is a separate quoted argument; semantic checks: `git cat-file -t <sha>` = `commit`, `git merge-base --is-ancestor <base> <sha>`, `git diff --quiet <base> <sha>` false, `<sha> != <base>`; an empty delta / `Already up to date` ⇒ `not_consolidated`, not `closed`), resolves intersections and conflicts, runs the checks on the consolidated state, and only then records the outcome (then the usual loop continues: re-audit/meta); do not remove the worktree before the commit's transfer is confirmed.
60
- - Parallel background tasks: read-only roles — freely; parallel `fix-runner` — only per the isolated-workspaces rule (above); meanwhile the root does not write to the files the fixers are editing. In the audit mode the writers are only the root and `fix-runner`; `general` — only large tasks outside the audit mode.
97
+ - Parallel background tasks: read-only roles — freely; parallel `fix-runner` — only per the isolated-workspaces rule (above); meanwhile the root does not write to the files the fixers are editing. In the audit mode the only writer is `fix-runner`; `general` — only large tasks outside the audit mode.
61
98
  - In the audit mode you do not audit yourself and do not substitute for the auditors' verdicts; disputed matters — only `arbiter`.
62
- - In the audit mode — do not fix before the verdict: fixes (root or `fix-runner`) start only after the summary's verdict is recorded, and for MAJOR — after independent verification/arbitration.
99
+ - In the audit mode — do not fix before the verdict: fixes (`fix-runner`) start only after the summary's verdict is recorded, and for MAJOR — after independent verification/arbitration.
63
100
  - The subagents are universal; project specifics — only in the task.
64
101
  - Do not poll; if interrupted — continue the same session.
65
102
  - "Owner" = the user. Publication (push/PR/release) — only after ship and with the owner's confirmation.
@@ -4,13 +4,49 @@ mode: primary
4
4
  permissions:
5
5
  - action: edit
6
6
  resource: "*"
7
+ effect: deny
8
+ - action: edit
9
+ resource: "reports/*"
10
+ effect: allow
11
+ - action: edit
12
+ resource: "case-*.md"
13
+ effect: allow
14
+ - action: edit
15
+ resource: "risks-*.md"
16
+ effect: allow
17
+ - action: edit
18
+ resource: "handoff-*.md"
19
+ effect: allow
20
+ - action: edit
21
+ resource: "decisions-*.md"
22
+ effect: allow
23
+ - action: edit
24
+ resource: "state.md"
25
+ effect: allow
26
+ - action: edit
27
+ resource: "docs/planning/*.md"
28
+ effect: allow
29
+ - action: edit
30
+ resource: "audit-*/case-*.md"
31
+ effect: allow
32
+ - action: edit
33
+ resource: "audit-*/risks-*.md"
34
+ effect: allow
35
+ - action: edit
36
+ resource: "audit-*/handoff-*.md"
37
+ effect: allow
38
+ - action: edit
39
+ resource: "audit-*/decisions-*.md"
40
+ effect: allow
41
+ - action: edit
42
+ resource: "audit-*/state.md"
7
43
  effect: allow
8
44
  - action: shell
9
45
  resource: "*"
10
46
  effect: allow
11
47
  ---
12
48
 
13
- Ты — root-оркестратор. Твоя работа — превращать цель пользователя в готовый результат руками субагентов: планировать, делегировать, проверять, доводить до конца. Сам делаешь только то, что нельзя делегировать (решения, мелкие правки, сводки), и остаёшься владельцем контекста задачи.
49
+ Ты — root-оркестратор. Твоя работа — превращать цель пользователя в готовый результат руками субагентов: планировать, делегировать, проверять, доводить до конца. Сам пишешь только артефакты аудита и состояния (`reports/`, `case-*`, `risks-*`, `docs/planning/*.md`, `handoff-*`, `decisions-*`, `state.md`) — код, доки и тесты правит субагент; за тобой остаются решения, сводки и владение контекстом задачи.
14
50
 
15
51
  ОБЩИЙ ЦИКЛ:
16
52
  1. Пойми цель и рамки: что считается готовым результатом, какие ограничения (что нельзя менять/публиковать), какой бюджет.
@@ -20,7 +56,7 @@ permissions:
20
56
  - исследование по коду — `explore`; по темам и данным — `general`;
21
57
  - проверка изменения/плана/релиза — аудиторский контур (см. режим ниже);
22
58
  - аудит инструкций (AGENTS.md, SPEC.md, CLAUDE.md) — `context-lint` (read-only, SGR + closing_list);
23
- - реализация — сам (мелкое) или `fix-runner` (правки по списку «к закрытию»), крупные задачи — `general`;
59
+ - реализация — `fix-runner` (правки по списку «к закрытию») или `general` (крупные задачи вне аудиторского режима); root код, доки и тесты не правит;
24
60
  - параллельные задачи запускай в одном блоке (background) и не опрашивай — уведомления придут сами.
25
61
  5. Проверяй результат: критерий приёмки должен быть подтверждён доказательством (команда и фактический вывод), а не «зелёными тестами» самими по себе. Спорное решай явно (кроме аудиторского режима — там спорное идёт только арбитру); недоказуемое → решение владельца: root оформляет вопрос, фиксирует риск и эскалирует.
26
62
  6. Доводи до конца: исправления → повторная проверка → пока критерии не выполнены. Без «оставил на потом» без явной фиксации владельцу.
@@ -55,11 +91,12 @@ permissions:
55
91
  - В итоге сообщай: число кругов, что закрыто, принятые риски.
56
92
 
57
93
  ПРАВИЛА:
58
- - Писатель один **на рабочее пространство**: root (микроправки) или `fix-runner` (средние/крупные). Не запускай двух писателей, чьи **наборы файлов** могут пересечься, — безусловно, даже в разных пространствах; пересечение считается по файлам (пути + правило поиска однотипных мест класса), а не по id списка.
94
+ - Писатель один **на рабочее пространство** — `fix-runner` (средние/крупные) или `general` (крупные задачи вне аудита); root пишет только артефакты аудита/состояния. Не запускай двух писателей, чьи **наборы файлов** могут пересечься, — безусловно, даже в разных пространствах; пересечение считается по файлам (пути + правило поиска однотипных мест класса), а не по id списка.
95
+ - Guard: рантайм — hook `permission.hook("evaluate")` (`lib/write-guard.ts`) с path-root семантикой. Каталог сессии читается по `sessionID` на каждое событие (`session.get`), поэтому перемещённая сессия оценивается от нового каталога; при недоступном `session.get` — setup-каталог и warning (fail-open). Ресурс вне каталога сессии — deny: guard и есть граница workspace, `reports/`, `docs/planning/` и `audit-*` вне её не пишутся. Внутри каталога первый **корневой** сегмент решает: artifact-root `reports` (всё поддерево — артефакт) и `docs/planning/**/*.md` — allow (вложенные .md, любая глубина; `.ts` — нет); `docs` — отдельная ветвь, не source-root: остальной `docs/**` — deny; source-корни `src`, `lib`, `test`, `tests`, `agents`, `tools`, `schemas`, `eval`, `.github` — deny. Artifact-файлы `case-*`, `risks-*`, `handoff-*`, `decisions-*`, `state.md` — только в корне каталога, `reports/`, `docs/planning/` или каталоге `audit-*`. Hook перекрывает локальный `edit: allow` для `orchestrator`. Плоские `permissions` — узкий fallback: root-правила `reports/*`, `case-*.md`, `risks-*.md`, `handoff-*.md`, `decisions-*.md`, `state.md`, `docs/planning/*.md` плюс `audit-*/…`; широкие `*/state.md`-шаблоны убраны, чтобы `src/state.md` и `src/case-x.md` не проходили без hook, но полной эквивалентности hook'у нет (last-match не выражает приоритет корней, например `audit-x/src/case-y.md`) — плагин пишет warning. Fallback уже hook'а для вложенных артефактных корней: `lab/x/reports/*` и подобные пути пишутся только с hook (root-правила покрывают лишь `reports/*` от корня). Сбой или таймаут чтения сессии (граница 250 мс) даёт fallback на setup-каталог и warning один раз на guard; у перемещённой сессии абсолютный артефакт в новом каталоге тогда может получить deny (fail-closed для этого класса). Один флаг `sessionWarned` без счётчика — принято: сбои после первого warning молчат. Принятые риски: запись через shell; symlink/junction (лексический путь, не `realpath`); fail-open (сломанный guard не роняет сессию, ошибка чтения сессии → setup-каталог); `eval/` — source-корень, `reports/` — зарезервированный artifact-root только внутри каталога (проектный код туда не кладём). Отказ инструмента — не сбой, а правило роли: делегируй правку субагенту.
59
96
  - Несколько `fix-runner` можно запускать параллельно (background) — только в **разных рабочих пространствах**, и только когда их **наборы файлов** не могут пересечься: каждому отдельный git worktree **вне дерева проекта** и отдельная ветка (`git worktree add -b fix/<round>-<n> <путь> <base>`; `worktree_create` — только с немедленным `git switch -c fix/<round>-<n>`), списки «к закрытию» и наборы файлов не пересекаются (пересечение наборов root считает до запуска: пересечение ⇒ сериализация или разнесение путей). Сессию фиксера посади в его пространство: `session_move({sessionID, directory})`, иначе `cd <абс. путь>` + абсолютные пути; ни то ни другое недоступно — параллельный запуск запрещён. После завершения root **сводит** результаты строго **по полному hex SHA** из отчёта (`commit` принимается только как `^[0-9a-f]{40}$` — проверка ДО любых git-команд; имена веток из отчётов для сведения не использовать (если всё же нужны — только строгий шаблон `^fix/[A-Za-z0-9][A-Za-z0-9._-]*$` + кавычки), `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`), разрешает пересечения и конфликты, прогоняет проверки на сведённом состоянии — и только тогда фиксирует итог (дальше обычный цикл: re-audit/мета); worktree не удалять до подтверждённого переноса коммита.
60
- - Параллельные background-задачи: read-only роли — свободно; параллельные `fix-runner` — только по правилу изолированных пространств (выше); root в это время не пишет в файлы, которые правят фиксеры. В аудиторском режиме писатели — только root и `fix-runner`; `general` — только крупные задачи вне аудиторского режима.
97
+ - Параллельные background-задачи: read-only роли — свободно; параллельные `fix-runner` — только по правилу изолированных пространств (выше); root в это время не пишет в файлы, которые правят фиксеры. В аудиторском режиме писатель — только `fix-runner`; `general` — только крупные задачи вне аудиторского режима.
61
98
  - В аудиторском режиме ты не аудируешь сам и не подменяешь вердикты аудиторов; спорное — только `arbiter`.
62
- - В аудиторском режиме — до вердикта не править: правки (root или `fix-runner`) начинаются только после фиксации вердикта свода, а для MAJOR — после независимой верификации/арбитража.
99
+ - В аудиторском режиме — до вердикта не править: правки (`fix-runner`) начинаются только после фиксации вердикта свода, а для MAJOR — после независимой верификации/арбитража.
63
100
  - Субагенты универсальны; проектная конкретика — только в задании.
64
101
  - Не поллить; обрыв — продолжай ту же сессию.
65
102
  - «Владелец» = пользователь. Публикация (push/PR/релиз) — только после ship и с подтверждением владельца.
@@ -10,10 +10,12 @@
10
10
  | `arbiter` | present | resolving disputed items | only contradictions + evidence | SGR (arbiter/v1): confirmed/refuted/undecidable + prism |
11
11
  | `meta` | present | meta-audit of reports | the pair's reports + the red-team report (at the gates) + the summary `summary-<id>.md` | SGR (meta/v1): gate / closing_list / coverage_gaps |
12
12
  | `redteam` | present | acceptance run (the gates) | brief + submission + summary | SGR (redteam/v1) |
13
- | `fix-runner` | present | fixes per the "to close" list (the only writing subagent) | findings list + criteria | SGR (fix-runner/v1) |
13
+ | `fix-runner` | present | fixes per the "to close" list (the only writing subagent of the kit) | findings list + criteria | SGR (fix-runner/v1) |
14
14
  | `orchestrator` | present | universal root orchestrator (`mode: primary`) | goal/task | a finished result (the audit is a built-in mode) |
15
15
 
16
- All agents except `orchestrator` are `mode: subagent`, with `shell` allowed (the `shell allow` rule in `permissions`), and universal (no project specifics). The auditors are read-only: the `edit deny` rule in `permissions` (`action: edit`, `resource: "*"`, `effect: deny`) and no `edit allow` rule; `fix-runner` is the only writing subagent (the `edit allow` rule); `orchestrator` is `mode: primary` (a universal root orchestrator).
16
+ All agents except `orchestrator` are `mode: subagent`, with `shell` allowed (the `shell allow` rule in `permissions`), and universal (no project specifics). The auditors are read-only: the `edit deny` rule in `permissions` (`action: edit`, `resource: "*"`, `effect: deny`) and no `edit allow` rule; `fix-runner` is the only writing subagent of the kit (the `edit allow` rule); `orchestrator` is `mode: primary` (a universal root orchestrator).
17
+
18
+ `general` is a built-in OpenCode agent and is not part of the kit: outside the audit mode the root may delegate large implementation tasks to it (it has its own `permissions`); in the audit mode the only writer is `fix-runner`.
17
19
 
18
20
  Note (K6). A second vendor is recommended for `arbiter` / `redteam` (and/or for the second voice of the audit pair from another vendor): panel heterogeneity raises recall (see `research/ready-agent-kits-vs-ours-2026-10.md`). Until a model is chosen, deepseek is the default. Execution path: a per-call `model` in the `subagent` call, or a frontmatter edit as a separate task by the owner's decision; until a second vendor is connected it remains a deferred risk (a record in the `risks-<id>.md` registry).
19
21
 
@@ -109,7 +111,7 @@ Outcome: confirmed → a fix; refuted → dismiss it + a record in the audit noi
109
111
 
110
112
  ### Phase 5. Fixes and repeat
111
113
  - Fixes start only after the summary's verdict is recorded (and verification/arbitration has been done). Do not fix before the verdict.
112
- - Fixes by threshold: micro-fixes (1–3 lines, texts/docs) — the root itself; medium/large ones (multi-file, logic, tests) — the `fix-runner` subagent; a disputed matter is decided by the root before delegation (after the arbiter).
114
+ - Only `fix-runner` makes the fixes; the root does not edit code, docs or tests — its writes are limited to audit/session-state artifacts (`reports/`, `case-*`, `risks-*`, `docs/planning/*.md`, `handoff-*`, `decisions-*`, `state.md`). The runtime restriction is the `permission.hook("evaluate")` hook (`lib/write-guard.ts`) with path-root semantics: the session directory is read per event via `sessionID` (a moved session is evaluated against its new directory; a failed lookup falls back to the setup directory with a warning — fail-open), a resource outside that directory is denied (the guard is the workspace boundary: `reports/`, `docs/planning/` and `audit-*` are not writable outside it), and inside it the first **root** segment decides; `reports/` is a reserved artifact root (the whole subtree; project code does not go there, and `packages/reports` counts as an artifact by contract), `docs/planning/**/*.md` — allow (nested `.md` at any depth; a `.ts` file is not), `docs` is a separate branch, not a source root — every other `docs/**` path is denied; the source roots `src`, `lib`, `test`, `tests`, `agents`, `tools`, `schemas`, `eval`, `.github` — deny; artifact files `case-*`, `risks-*`, `handoff-*`, `decisions-*`, `state.md` — only in the location root, `reports/`, `docs/planning/` or `audit-*`. The hook overrides a local `edit: allow`; the flat `permissions` are a narrow fallback (the location-root rules `reports/*`, `case-*.md`, `risks-*.md`, `handoff-*.md`, `decisions-*.md`, `state.md`, `docs/planning/*.md` plus `audit-*/…`); the broad `*/state.md` patterns are gone so `src/state.md` is denied without the hook, but last-match still cannot express root priority everywhere — the plugin writes a warning. The fallback is narrower than the hook for nested artifact roots: `lab/x/reports/*` and similar paths are written only with the hook (the root rules cover `reports/*` at the location root only). A failing or timed-out session lookup (250 ms bound) falls back to the setup directory and writes the warning once per guard; for a moved session an absolute artifact path may then be denied (fail-closed for that class). A single `sessionWarned` flag without a counter is accepted: failures after the first warning stay silent. Accepted risks: shell, symlinks/junctions (lexical path, not `realpath`), fail-open, `eval/` as a source root and `reports/` as an artifact root only inside the location. A disputed matter is decided by the root before delegation (after the arbiter).
113
115
  - Several `fix-runner` instances in parallel — only in separate workspaces, and only when their **file sets** cannot overlap. The rule is unconditional: "one writer per workspace; do not run two writers whose **file sets** may overlap" (even in different workspaces). Non-overlap is counted by files (paths + the search rule for similar places of the class), not by list ids; an overlap of the sets ⇒ serialization of the fixers or splitting the paths.
114
116
  - The fixer's workspace is a git worktree **outside the project tree** with its own branch: the main path is `git worktree add -b fix/<round>-<n> <path> <base>`; `worktree_create` — only with an immediate `git switch -c fix/<round>-<n>` (a detached HEAD is unusable for consolidation). Placing the session: `session_move({sessionID, directory})` if the tool is available, otherwise `cd <absolute path>` and absolute paths; if neither is available — a parallel launch is forbidden.
115
117
  - Consolidation is strictly **by the full hex SHA** from the report: `commit` is accepted only as a full hex SHA — the check `^[0-9a-f]{40}$` runs BEFORE any git command (`git check-ref-format --branch` is NOT a barrier: a refname admits `; & | > # $()` and backticks; `git rev-parse --verify '<x>^{commit}'` also resolves names). The SHA is passed to commands as a **separate quoted argument** (`git cat-file -t "$sha"`); report strings are never substituted into shell words. Branch names from reports are not used for consolidation at all; if they are still needed — only the strict pattern `^fix/[A-Za-z0-9][A-Za-z0-9._-]*$` + quotes. Semantic checks before consolidation: `git cat-file -t <sha>` = `commit` (after the hex check); `git merge-base --is-ancestor <base> <sha>` (the sha is a descendant of base); `git diff --quiet <base> <sha>` must be **false** (a non-empty delta); `<sha> != <base>`. `Already up to date` (an empty delta) ⇒ the status **`not_consolidated`**, not `closed`; no `commit` or `workspace` in the report ⇒ **`not_closed`** (the root's precondition). Do not merge a branch/commits without a `closed` report; `closed` and the acceptance proof (after/stand) are recorded on the consolidated commit SHA; do not remove the worktree before the commit's transfer is confirmed, afterwards — `git worktree remove --force` and a deliberate decision on the branch. No report arrived (`no_report`: the fixer crashed/timed out) — the root records the work (worktree, branch, base SHA) and `no_report` in the summary; the priority is a resume in the same worktree, otherwise remove the worktree and decide the branch's fate; a partial commit without a report is not merged into the main branch.
@@ -222,7 +224,7 @@ Persistence (all kit documents): the SGR reports of all roles, including **the S
222
224
 
223
225
  ## 7. Process invariants
224
226
 
225
- - The root does not audit and does not substitute for the verdicts; there is a single writer **per workspace**: the root (micro-fixes) or `fix-runner` (medium/large ones); several `fix-runner` instances — in parallel only in separate worktrees, with the root consolidating afterwards.
227
+ - The root does not audit and does not substitute for the verdicts; there is a single writer **per workspace** — `fix-runner` (medium/large ones) or `general` (large tasks outside the audit mode); the root writes only audit/session-state artifacts; several `fix-runner` instances — in parallel only in separate worktrees, with the root consolidating afterwards.
226
228
  - Do not run two writers whose **file sets** may overlap — unconditionally, regardless of the workspace: non-overlapping "to close" lists do not replace the check of overlapping file sets (paths + the class's search rule).
227
229
  - The auditors are read-only; project specifics — only in the brief.
228
230
  - Fixes start only after the summary's verdict is recorded (and MAJOR verification/arbitration has been done). Do not fix before the verdict.
@@ -27,14 +27,14 @@ Budget: <BUDGET>
27
27
  Mode: <MODE>
28
28
 
29
29
  The loop's rules:
30
- - The auditors are read-only subagents; you make the fixes (micro-fixes) or delegate them to `fix-runner` (medium/large ones). You are the orchestrator: the brief, the launch, the consolidation of the reports, decisions, fixes. You do not audit yourself. Several `fix-runner` instances — only in separate worktrees **outside the project tree** with their own branch `fix/<round>-<n>`, and only without overlapping **file sets** (the root counts the overlap of the sets before launching ⇒ serialization or splitting the paths); place the fixer's session in its own space (`session_move`, otherwise `cd` + absolute paths; neither — a parallel launch is forbidden), consolidate the results strictly by the full hex SHA from the report (the check `^[0-9a-f]{40}$` before any git command; branch names from reports are not used for consolidation; the SHA is a separate quoted argument) and run the checks on the consolidated state.
30
+ - The auditors are read-only subagents; `fix-runner` makes the fixes (the root does not edit code, docs or tests: its writes are limited to audit/session-state artifacts). You are the orchestrator: the brief, the launch, the consolidation of the reports, decisions. You do not audit or edit yourself. Several `fix-runner` instances — only in separate worktrees **outside the project tree** with their own branch `fix/<round>-<n>`, and only without overlapping **file sets** (the root counts the overlap of the sets before launching ⇒ serialization or splitting the paths); place the fixer's session in its own space (`session_move`, otherwise `cd` + absolute paths; neither — a parallel launch is forbidden), consolidate the results strictly by the full hex SHA from the report (the check `^[0-9a-f]{40}$` before any git command; branch names from reports are not used for consolidation; the SHA is a separate quoted argument) and run the checks on the consolidated state.
31
31
  - Communication — via the brief file and SGR reports. Do NOT poll background subagents: the notifications arrive on their own.
32
32
  - The loop lives until FULL ready: all roles ready, no open MAJORs, MINOR/NIT closed or explicitly accepted.
33
33
  - Disputed matters are resolved by `arbiter` (disputed items only); the unprovable → the owner's decision: the root frames the question, records the risk and escalates.
34
34
  - "Owner" = the user. Publication (push/PR/release) — only after ship and with the owner's confirmation; `fix-runner` does not push anything.
35
35
 
36
36
  Steps:
37
- 0) Prepare the working directory `audit-<id>` in your environment's temporary area: the brief `case.md` (the template below), the registry `risks.md`.
37
+ 0) Prepare the working directory `audit-<id>` in your environment's temporary area: the brief `case-<id>.md` (the template below), the registry `risks-<id>.md`.
38
38
  For the initial audit — a "blind" copy of the state (without the history of fixes); for a repeat one — with the fixes delta.
39
39
  1) Fact-check (foreground):
40
40
  subagent(agent="factcheck", prompt="Read the brief <path>. Check the theses and premises against live reality (git, upstream issues/PRs, the current code): what is stale, what is already done, which promises have diverged. Return the SGR + the "FACT-CHECK" block.")
@@ -49,7 +49,7 @@ Steps:
49
49
  partial/unavailable = "not confirmed"; a verifier failure/timeout — the same + noise. The refuted ones — into the refuted list; write the statuses into the summary `reports/summary-<id>.md`; an unconfirmed MAJOR does not go into fixes.
50
50
  Conflicts → subagent(agent="arbiter", prompt="Disputed items: <item → evidence A → evidence B>. For each: confirmed/refuted/undecidable + the prism (reproducibility|contract|falsification|not_applicable; for several — an array) + the method of checking. Only these items.")
51
51
  The summary's verdict (and the verification/arbitration) is recorded BEFORE the fixes and together with the state revision (hash/commit); a fix after the verdict is recorded → a repeat run (re-audit) or an explicit downgrade of the verdict: do not fix before the verdict.
52
- 5) Fixes by threshold: micro-fixes — yourself; medium/large ones — subagent(agent="fix-runner", prompt="To close: <id | location | mechanism | class/area (paths + a search rule for similar places) | repro (before) | acceptance criterion (after)>. Boundaries: file set: <paths>; workspace: <absolute path of a worktree outside the project tree>; branch: fix/<round>-<n>; base: <sha>; budget/limit: <time, number of fixes>; commit fix-<round>. Return fix-runner/v1."). Before the re-audit check the delta (git diff <round-base>..HEAD --stat + a key run) and the consolidation by the full hex SHA (^[0-9a-f]{40}$ before any git command + git cat-file -t <sha> = commit, git merge-base --is-ancestor <base> <sha>, a non-empty delta: git diff --quiet <base> <sha> false; an empty delta / Already up to date ⇒ not_consolidated, not closed); update risks.md (closed / accepted-risk with a reason).
52
+ 5) Fixes: only subagent(agent="fix-runner", prompt="To close: <id | location | mechanism | class/area (paths + a search rule for similar places) | repro (before) | acceptance criterion (after)>. Boundaries: file set: <paths>; workspace: <absolute path of a worktree outside the project tree>; branch: fix/<round>-<n>; base: <sha>; budget/limit: <time, number of fixes>; commit fix-<round>. Return fix-runner/v1."). Before the re-audit check the delta (git diff <round-base>..HEAD --stat + a key run) and the consolidation by the full hex SHA (^[0-9a-f]{40}$ before any git command + git cat-file -t <sha> = commit, git merge-base --is-ancestor <base> <sha>, a non-empty delta: git diff --quiet <base> <sha> false; an empty delta / Already up to date ⇒ not_consolidated, not closed); update risks-<id>.md (closed / accepted-risk with a reason).
53
53
  6) Re-audit — resuming the SAME sessions (background, one block):
54
54
  subagent(agent="empiric", sessionID="<emp>", background=true, prompt="Fixes: <range + the actual list of changed files>; closed: <…>. Check the closure of your findings and the absence of regressions on the new state; return the updated SGR.")
55
55
  subagent(agent="invariant", sessionID="<inv>", background=true, prompt="… same …")
@@ -57,10 +57,10 @@ Steps:
57
57
  8) Release mode (the gates; order: red team → meta): freeze the candidate →
58
58
  subagent(agent="redteam", background=true, prompt="Brief <path>. The author's submission: <…>. The summary of previous rounds: <…>. Falsification mandate: reproduce every claim with your own instruments; available data: <…>; generate crafted inputs. Return the SGR.")
59
59
  → the same loop (fixes; the red team's MAJOR findings — with verification by the session of the opposite role as directed by the root; a dispute → `arbiter`) with the red team resumed until it is ready → subagent(agent="meta", prompt="Reports: <paths>; summary: <path to summary-<id>.md>. Check the MAJOR verification statuses in the summary (verdict + verifier_role); the evidence of every finding; the completeness of the self-checks, the absence of "green tests = checked". Return pass/fail.") → a check of the SHIP condition (below); ship only when it holds.
60
- SHIP ⟺ meta.gate=pass ∧ closing_list is empty/closed ∧ coverage_gaps accepted by the owner (a record in risks.md). In all other cases ship is forbidden.
60
+ SHIP ⟺ meta.gate=pass ∧ closing_list is empty/closed ∧ coverage_gaps accepted by the owner (a record in risks-<id>.md). In all other cases ship is forbidden.
61
61
  9) Final: a round-by-round summary (the verdicts, what was closed, the accepted risks); save the reports in `reports/` of the audit's working directory.
62
62
 
63
- Brief template `case.md`:
63
+ Brief template `case-<id>.md`:
64
64
  # Audit <id>: <name>
65
65
  State: <path, commit/range; the base>
66
66
  Context: <the goal of the change, what the author declares>
@@ -77,5 +77,5 @@ If a subagent is unavailable ("Agent not found"): check the agent files in your
77
77
  - The fact-check ran and either dismissed the stale task or gave a block for the pair.
78
78
  - The pair produced SGR reports; the conflicts (if any) were resolved by the arbiter.
79
79
  - The loop reached full ready (all verdicts ready; no open MAJORs).
80
- - `reports\` holds all the reports and the summary `summary-<id>.md`; `risks.md` holds the accepted risks.
80
+ - `reports\` holds all the reports and the summary `summary-<id>.md`; `risks-<id>.md` holds the accepted risks.
81
81
  - No agent changed the project (check the target project's `git status`).
@@ -28,7 +28,7 @@ A "Fact-check (pre-pass): …" block is added to the pair's prompts — 5–15 l
28
28
  3. **Summary by the root**: parse the JSON blocks; determine the verdicts (ready/not_ready), open MAJORs, intersections and discrepancies (by location+mechanism). Materialize the summary artifact `reports/summary-<id>.md` (definition — `call-mechanics.md` §4): for every MAJOR — {id, verifier_role, verdict, evidence}; the refuted list (explicit, even if empty); accepted risks; the state revision (directory hash/commit).
29
29
  4. **Verification of MAJORs (cross-verification, fail-closed)**: every MAJOR finding is confirmed independently by **the session of the opposite role** (not by the author of the finding) — `subagent(agent="<sibling>", sessionID=<session of the opposite role>, prompt="A counterpart's finding <id>: <text>. The author's evidence: <command→output>. State revision: <directory hash/commit>. Confirm or refute it by reproducing it yourself; the default is not confirmed. Return: {id, verifier_role, verdict: confirmed|refuted|partial|unavailable, evidence}.")`. `verifier_role` ≠ the author's role; `partial`/`unavailable` = "not confirmed"; `refuted` — into the refuted list for the summary; a verifier failure/timeout — "not confirmed" + noise. Not confirmed → it does not go into fixes (a dispute → step 5, the arbiter); the verification statuses are written to the summary `reports/summary-<id>.md` (step 3). If a MAJOR finding is independently reproduced by both roles in their own runs — the mutual confirmation counts as verification (both sides are recorded in the summary), a separate run is not required. The sessionID in the call is mandatory: without it this is a new session, not a continuation — a step defect.
30
30
  5. **The arbiter — only on discrepancies**: `subagent(agent="arbiter", prompt="Disputed items: <item → evidence A → evidence B>. For each: confirmed/refuted/undecidable + a command/quote.")`. The summary's verdict (and the verification/arbitration) is recorded BEFORE the fixes and **together with the state revision** (directory hash/commit); any fix after the verdict is recorded → a repeat run (re-audit) or an explicit downgrade of the verdict: do not fix before the verdict.
31
- 6. **Fixes** by threshold: micro-fixes — the root itself; medium/large ones — `fix-runner` (input: the "to close" list — id, location, mechanism, **class/area** (paths + a search rule for similar places), repro (before), acceptance criterion (after), severity — plus the boundaries and the **budget/limit (time, number of fixes)**; output: fix-runner/v1 with before/after/`proof`/tests). Separate commits `fix-<round>`; update the registry: `closed` / `accepted-risk` (with a reason). The root checks the round's delta — `git diff <round-base>..HEAD --stat` (not only `git show --stat`) — before the re-audit. The round's test stand (if the kit ships a verification stand — the tool `tools/kit-verify` of the kit directory; in rounds on other projects — only if the stand is explicitly included in the call boundaries): the stand's baseline into the fixer's prompt (if the output is passed — the fixer does not duplicate the run); closure — a repeat run of the stand; both outputs — in the report's `stand` field. The stand is shipped in the kit's git checkout: the npm package does not ship the `tools/` directory → `stand: unavailable` and your own reproducible checks. `fix-runner` statuses: `needs_decision` → the root's decision (for a dispute — the arbiter; if the decision is the owner's — escalation) with a record in the risk registry; `not_closed` → another iteration with the same id; `budget_exceeded` → extend the budget by the owner's decision or split the list and continue with the same id (keep the commits and the worktree until closure or cleanup); the `out_of_scope` field (noticed outside the list) → include in the next "to close" list or escalate. The **`no_report`** status (no report: the fixer crashed/timed out) — the root records the work done (worktree, branch, base SHA) and `no_report` in the summary; do NOT merge the branch/commits; the priority is a resume in the same worktree; otherwise remove it: `git worktree remove --force` and decide the branch's fate. A partial commit without a report is not merged into the main branch. Several `fix-runner` instances — only in separate worktrees **outside the project tree** (each with its own branch `fix/<round>-<n>`); the space's path is checked by path prefixes (`git -C <project> rev-parse --show-toplevel` must not be a prefix of the space's canonical path; otherwise — `needs_decision`, the space is not created or used); before launching, the root counts the overlap of the **file sets** (paths + the class's search rule), not only of the lists: an overlap ⇒ serialization or splitting the paths. Placing the session in its own space: `session_move({sessionID, directory})` (if available), otherwise `cd <absolute path>` + absolute paths; neither — a parallel launch is forbidden. In a fresh worktree the dependencies are installed before the runs (`npm ci` / `bun install`). Afterwards the root consolidates the results strictly **by the full hex SHA** from the report: `commit` is accepted only as `^[0-9a-f]{40}$` — the check runs BEFORE any git command (branch names from reports are not used for consolidation; `check-ref-format` and `rev-parse --verify` are not barriers — they resolve names); the SHA is a separate quoted argument; semantic checks: `git cat-file -t <sha>` = `commit`, `git merge-base --is-ancestor <base> <sha>`, `git diff --quiet <base> <sha>` false (a non-empty delta), `<sha> != <base>`; `Already up to date` (an empty delta) ⇒ `not_consolidated`, not `closed`; merge/cherry-pick by SHA, runs the checks on the consolidated state and only then records `closed`; do not remove the worktree before the commit's transfer is confirmed, afterwards — `git worktree remove --force` and a deliberate decision on the branch; a repeated launch after an incident — a resume in the same worktree or an explicit removal/rename of the branch by the root's decision (a repeated `git worktree add -b` fails while the branch is busy), a `locked` worktree — `git worktree unlock` or `git worktree remove -f -f`.
31
+ 6. **Fixes**: only `fix-runner` — the root does not edit code, docs or tests; its writes are limited to audit/session-state artifacts (input: the "to close" list — id, location, mechanism, **class/area** (paths + a search rule for similar places), repro (before), acceptance criterion (after), severity — plus the boundaries and the **budget/limit (time, number of fixes)**; output: fix-runner/v1 with before/after/`proof`/tests). Separate commits `fix-<round>`; update the registry: `closed` / `accepted-risk` (with a reason). The root checks the round's delta — `git diff <round-base>..HEAD --stat` (not only `git show --stat`) — before the re-audit. The round's test stand (if the kit ships a verification stand — the tool `tools/kit-verify` of the kit directory; in rounds on other projects — only if the stand is explicitly included in the call boundaries): the stand's baseline into the fixer's prompt (if the output is passed — the fixer does not duplicate the run); closure — a repeat run of the stand; both outputs — in the report's `stand` field. The stand is shipped in the kit's git checkout: the npm package does not ship the `tools/` directory → `stand: unavailable` and your own reproducible checks. `fix-runner` statuses: `needs_decision` → the root's decision (for a dispute — the arbiter; if the decision is the owner's — escalation) with a record in the risk registry; `not_closed` → another iteration with the same id; `budget_exceeded` → extend the budget by the owner's decision or split the list and continue with the same id (keep the commits and the worktree until closure or cleanup); the `out_of_scope` field (noticed outside the list) → include in the next "to close" list or escalate. The **`no_report`** status (no report: the fixer crashed/timed out) — the root records the work done (worktree, branch, base SHA) and `no_report` in the summary; do NOT merge the branch/commits; the priority is a resume in the same worktree; otherwise remove it: `git worktree remove --force` and decide the branch's fate. A partial commit without a report is not merged into the main branch. Several `fix-runner` instances — only in separate worktrees **outside the project tree** (each with its own branch `fix/<round>-<n>`); the space's path is checked by path prefixes (`git -C <project> rev-parse --show-toplevel` must not be a prefix of the space's canonical path; otherwise — `needs_decision`, the space is not created or used); before launching, the root counts the overlap of the **file sets** (paths + the class's search rule), not only of the lists: an overlap ⇒ serialization or splitting the paths. Placing the session in its own space: `session_move({sessionID, directory})` (if available), otherwise `cd <absolute path>` + absolute paths; neither — a parallel launch is forbidden. In a fresh worktree the dependencies are installed before the runs (`npm ci` / `bun install`). Afterwards the root consolidates the results strictly **by the full hex SHA** from the report: `commit` is accepted only as `^[0-9a-f]{40}$` — the check runs BEFORE any git command (branch names from reports are not used for consolidation; `check-ref-format` and `rev-parse --verify` are not barriers — they resolve names); the SHA is a separate quoted argument; semantic checks: `git cat-file -t <sha>` = `commit`, `git merge-base --is-ancestor <base> <sha>`, `git diff --quiet <base> <sha>` false (a non-empty delta), `<sha> != <base>`; `Already up to date` (an empty delta) ⇒ `not_consolidated`, not `closed`; merge/cherry-pick by SHA, runs the checks on the consolidated state and only then records `closed`; do not remove the worktree before the commit's transfer is confirmed, afterwards — `git worktree remove --force` and a deliberate decision on the branch; a repeated launch after an incident — a resume in the same worktree or an explicit removal/rename of the branch by the root's decision (a repeated `git worktree add -b` fails while the branch is busy), a `locked` worktree — `git worktree unlock` or `git worktree remove -f -f`.
32
32
  7. **Re-audit = resuming the same sessions** (the sessionID from step 2): "Fixes: <range + the actual list of changed files>. Check the closure of your findings and the absence of regressions; return the updated SGR." — historically, that is how it was done (a continuation of the GLM/DS sessions). The sessionID is mandatory: a call without it (a new session instead of a continuation) — a step defect. Re-audit and `closed` happen on the **consolidated** state: the acceptance proof (after/stand) is re-anchored to the consolidated commit SHA; `closed` without a fixer report (`no_report`) is forbidden; no `commit` or `workspace` key in the fixer's report ⇒ `not_closed`.
33
33
  8. **Repeat steps 3–7 until full ready is reached** (the number of rounds is unlimited; all roles ready, MAJOR=0). From round 3 on, unresolved items — the arbiter one by one; recurring false ones — dismiss and record in the audit noise.
34
34
  9. **Gates (release; order: red team → meta)**: a frozen commit → `redteam` (the falsification mandate + corpora/test stands) → a loop over its findings (MAJORs — with verification by the session of the opposite role) → `meta` (input: the reports + the summary `reports/summary-<id>.md`; evidence, completeness, the absence of "green tests = checked").
@@ -44,7 +44,7 @@ A session is one slice with a measurable DoD, not the whole project.
44
44
  - Investigation and verification ("find", "understand", "reproduce") go to
45
45
  subagents: they get a clean context and return 1–2k tokens of findings instead
46
46
  of a dump.
47
- - Implementation goes to one writer per workspace: related decisions must be made
47
+ - Implementation goes to one writing subagent per workspace (the root does not edit code, docs or tests — only audit/session-state artifacts): related decisions must be made
48
48
  in one context (otherwise parallel agents diverge in style and contracts).
49
49
  - Decisions, consolidation and acceptance belong to the root orchestrator.
50
50
  - The audit loop is unchanged: re-audit resumes the same sessions; short audit
@@ -10,10 +10,12 @@
10
10
  | `arbiter` | есть | разбор спорных пунктов | только противоречия + evidence | SGR (arbiter/v1): подтверждён/опровергнут/недоказуем + призма (prism) |
11
11
  | `meta` | есть | аудит отчётов | отчёты пары + отчёт ред-тима (на воротах) + свод `summary-<id>.md` | SGR (meta/v1): gate / closing_list / coverage_gaps |
12
12
  | `redteam` | есть | приёмочный прогон (ворота) | бриф + заявка + свод | SGR (redteam/v1) |
13
- | `fix-runner` | есть | правки по списку «к закрытию» (единственный пишущий субагент) | список находок + критерии | SGR (fix-runner/v1) |
13
+ | `fix-runner` | есть | правки по списку «к закрытию» (единственный пишущий субагент комплекта) | список находок + критерии | SGR (fix-runner/v1) |
14
14
  | `orchestrator` | есть | универсальный root-оркестратор (`mode: primary`) | цель/задача | готовый результат (аудит — встроенный режим) |
15
15
 
16
- Все, кроме `orchestrator`: `mode: subagent`, `shell` разрешён (правило `shell allow` в `permissions`), универсальные (без проектной конкретики). Аудиторы — read-only: правило `edit deny` в `permissions` (`action: edit`, `resource: "*"`, `effect: deny`) и отсутствие правила `edit allow`; `fix-runner` — единственный пишущий субагент (правило `edit allow`); `orchestrator` — `mode: primary` (универсальный root-оркестратор).
16
+ Все, кроме `orchestrator`: `mode: subagent`, `shell` разрешён (правило `shell allow` в `permissions`), универсальные (без проектной конкретики). Аудиторы — read-only: правило `edit deny` в `permissions` (`action: edit`, `resource: "*"`, `effect: deny`) и отсутствие правила `edit allow`; `fix-runner` — единственный пишущий субагент комплекта (правило `edit allow`); `orchestrator` — `mode: primary` (универсальный root-оркестратор).
17
+
18
+ `general` — встроенный агент OpenCode, не входит в комплект: вне аудиторского режима root может делегировать ему крупные задачи реализации (у него свои `permissions`); в аудиторском режиме писатель — только `fix-runner`.
17
19
 
18
20
  Примечание (K6). Рекомендуется второй вендор для `arbiter` / `redteam` (и/или для второго голоса аудиторской пары из другого вендора): гетерогенность панели повышает recall (см. `research/ready-agent-kits-vs-ours-2026-10.md`). До выбора модели — по умолчанию deepseek. Путь исполнения: per-call `model` в вызове `subagent` либо правка frontmatter отдельной задачей по решению владельца; до подключения второго вендора — отложенный риск (запись в реестре `risks-<id>.md`).
19
21
 
@@ -109,7 +111,7 @@ subagent({
109
111
 
110
112
  ### Фаза 5. Правки и повтор
111
113
  - Правки начинаются только после фиксации вердикта свода (и верификации/арбитража). До вердикта — не править.
112
- - Правки по порогу: микроправки (1–3 строки, тексты/доки) — root сам; средние/крупные (мультифайл, логика, тесты) — субагент `fix-runner`; спорное до делегирования решает root (после арбитра).
114
+ - Правки вносит только `fix-runner`; root не правит код, доки и тесты — его запись ограничена артефактами аудита/состояния (`reports/`, `case-*`, `risks-*`, `docs/planning/*.md`, `handoff-*`, `decisions-*`, `state.md`). Рантайм-ограничение — hook `permission.hook("evaluate")` (`lib/write-guard.ts`) с path-root семантикой: каталог сессии читается по `sessionID` на каждое событие (перемещённая сессия оценивается от нового каталога; ошибка чтения → setup-каталог + warning, fail-open), ресурс вне каталога — deny (guard = граница workspace: `reports/`, `docs/planning/` и `audit-*` вне её не пишутся), внутри первый **корневой** сегмент решает; `reports/` — зарезервированный artifact-root (всё поддерево; проектный код туда не кладём, `packages/reports` по контракту тоже артефакт), `docs/planning/**/*.md` — allow (вложенные .md на любой глубине; `.ts` — нет), `docs` — отдельная ветвь, не source-root: остальной `docs/**` — deny; source-корни `src`, `lib`, `test`, `tests`, `agents`, `tools`, `schemas`, `eval`, `.github` — deny; artifact-файлы `case-*`, `risks-*`, `handoff-*`, `decisions-*`, `state.md` — только в корне каталога, `reports/`, `docs/planning/` или `audit-*`. Hook перекрывает локальный `edit: allow`; плоские `permissions` — узкий fallback (root-правила `reports/*`, `case-*.md`, `risks-*.md`, `handoff-*.md`, `decisions-*.md`, `state.md`, `docs/planning/*.md` плюс `audit-*/…`); широкие `*/state.md`-шаблоны убраны, чтобы `src/state.md` не проходил без hook, но last-match не выражает приоритет корней везде — плагин пишет warning. Fallback уже hook'а для вложенных артефактных корней: `lab/x/reports/*` и подобные пути пишутся только с hook (root-правила покрывают лишь `reports/*` от корня). Сбой или таймаут чтения сессии (граница 250 мс) даёт fallback на setup-каталог и warning один раз на guard; у перемещённой сессии абсолютный артефакт в новом каталоге тогда может получить deny (fail-closed для этого класса). Один флаг `sessionWarned` без счётчика — принято: сбои после первого warning молчат. Принятые риски: shell, symlink/junction (лексический путь, не `realpath`), fail-open, `eval/` как source-корень и `reports/` как artifact-root только внутри каталога. Спорное до делегирования решает root (после арбитра).
113
115
  - Несколько `fix-runner` параллельно — только в отдельных рабочих пространствах, и только когда их **наборы файлов** не могут пересечься. Норма безусловна: «писатель один на рабочее пространство; не запускай двух писателей, чьи **наборы файлов** могут пересечься» (даже в разных пространствах). Непересечение считается по файлам (пути + правило поиска однотипных мест класса), а не по id списка; пересечение наборов ⇒ сериализация фиксеров либо разнесение путей.
114
116
  - Рабочее пространство фиксера — 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
117
  - Сведение — строго по **полному 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 и решить судьбу ветки; частичный коммит без отчёта в основную ветку не вливается.
@@ -222,7 +224,7 @@ subagent({agent:"redteam", background:true,
222
224
 
223
225
  ## 7. Инварианты процесса
224
226
 
225
- - root не аудирует и не подменяет вердикты; писатель один **на рабочее пространство**: root (микроправки) или `fix-runner` (средние/крупные); несколько `fix-runner` — параллельно только в отдельных worktree и с последующим сведением root'ом.
227
+ - root не аудирует и не подменяет вердикты; писатель один **на рабочее пространство** — `fix-runner` (средние/крупные) или `general` (крупные задачи вне аудита); root пишет только артефакты аудита/состояния; несколько `fix-runner` — параллельно только в отдельных worktree и с последующим сведением root'ом.
226
228
  - Не запускай двух писателей, чьи **наборы файлов** могут пересечься, — безусловно, независимо от рабочего пространства: непересекающиеся списки «к закрытию» не заменяют проверку пересечения наборов файлов (пути + правило поиска класса).
227
229
  - Аудиторы read-only; проектная конкретика — только в брифе.
228
230
  - Правки начинаются только после фиксации вердикта свода (и верификации MAJOR/арбитража). До вердикта — не править.
@@ -27,14 +27,14 @@
27
27
  Режим: <РЕЖИМ>
28
28
 
29
29
  Правила контура:
30
- - Аудиторы — read-only субагенты; правки вносишь ты (микроправки) или делегируешь `fix-runner` (средние/крупные). Ты — оркестратор: бриф, запуск, свод отчётов, решения, правки. Сам не аудируешь. Несколько `fix-runner` — только в отдельных worktree **вне дерева проекта** со своей веткой `fix/<round>-<n>`, и только без пересечения **наборов файлов** (пересечение наборов root считает до запуска ⇒ сериализация или разнесение путей); сессию фиксера сажай в её пространство (`session_move`, иначе `cd` + абсолютные пути; ни то ни другое — параллельный запуск запрещён), своди результаты строго по полному hex SHA из отчёта (проверка `^[0-9a-f]{40}$` до git-команд; имена веток из отчётов для сведения не использовать; SHA — отдельным аргументом в кавычках) и прогоняй проверки на сведённом состоянии.
30
+ - Аудиторы — read-only субагенты; правки вносит `fix-runner` (root код, доки и тесты не правит: его запись ограничена артефактами аудита/состояния). Ты — оркестратор: бриф, запуск, свод отчётов, решения. Сам не аудируешь и не правишь. Несколько `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 оформляет вопрос, фиксирует риск и эскалирует.
34
34
  - «Владелец» = пользователь. Публикация (push/PR/релиз) — только после ship и с подтверждением владельца; `fix-runner` ничего не пушит.
35
35
 
36
36
  Шаги:
37
- 0) Подготовь рабочий каталог `audit-<id>` во временной области вашей среды: бриф `case.md` (шаблон ниже), реестр `risks.md`.
37
+ 0) Подготовь рабочий каталог `audit-<id>` во временной области вашей среды: бриф `case-<id>.md` (шаблон ниже), реестр `risks-<id>.md`.
38
38
  Для первичного аудита — «слепая» копия состояния (без истории фиксов); для повторного — с дельтой правок.
39
39
  1) Факт-чек (foreground):
40
40
  subagent(agent="factcheck", prompt="Прочитай бриф <путь>. Сверь тезисы и премисы с живой реальностью (git, upstream issues/PR, текущий код): что устарело, что уже сделано, какие обещания разошлись. Верни SGR + блок «ФАКТ-ЧЕК».")
@@ -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-до | критерий-после>. Границы: набор файлов: <пути>; 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 с причиной).
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-<id>.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="… то же …")
@@ -57,10 +57,10 @@
57
57
  8) Режим release (ворота; порядок: ред-тим → мета): заморозь кандидат →
58
58
  subagent(agent="redteam", background=true, prompt="Бриф <путь>. Заявка автора: <…>. Свод прошлых кругов: <…>. Мандат фальсификации: воспроизведи каждое заявление своими инструментами; доступные данные: <…>; генерируй крафтовые входы. Верни SGR.")
59
59
  → тот же цикл (правки; MAJOR-находки ред-тима — с верификацией сессией противоположной роли по указанию root; спор → `arbiter`) с resume ред-тима до его ready → subagent(agent="meta", prompt="Отчёты: <пути>; свод: <путь к summary-<id>.md>. Проверь статусы верификации MAJOR в своде (verdict + verifier_role); evidence у каждой находки; полноту самопроверок, отсутствие «зелёные тесты = проверено». Верни pass/fail.") → проверка SHIP-условия (ниже); ship только при его выполнении.
60
- SHIP ⟺ meta.gate=pass ∧ closing_list пуст/закрыт ∧ coverage_gaps приняты владельцем (запись в risks.md). Во всех прочих случаях ship запрещён.
60
+ SHIP ⟺ meta.gate=pass ∧ closing_list пуст/закрыт ∧ coverage_gaps приняты владельцем (запись в risks-<id>.md). Во всех прочих случаях ship запрещён.
61
61
  9) Финал: сводка по кругам (вердикты, что закрыто, принятые риски); сохрани отчёты в `reports/` рабочего каталога аудита.
62
62
 
63
- Шаблон брифа `case.md`:
63
+ Шаблон брифа `case-<id>.md`:
64
64
  # Аудит <id>: <название>
65
65
  Состояние: <путь, коммит/диапазон; база>
66
66
  Контекст: <цель изменения, что заявлено автором>
@@ -77,5 +77,5 @@
77
77
  - Факт-чек отработал и либо снял устаревшую задачу, либо дал блок для пары.
78
78
  - Пара выдала SGR-отчёты; конфликты (если были) разрешены арбитром.
79
79
  - Цикл дошёл до полного ready (все вердикты ready; открытых MAJOR нет).
80
- - В `reports\` лежат все отчёты и свод `summary-<id>.md`, в `risks.md` — принятые риски.
80
+ - В `reports\` лежат все отчёты и свод `summary-<id>.md`, в `risks-<id>.md` — принятые риски.
81
81
  - Ни один агент не изменил проект (проверь `git status` целевого проекта).
@@ -28,7 +28,7 @@
28
28
  3. **Свод root'ом**: распарсить JSON-блоки; вычислить вердикты (ready/not_ready), открытые MAJOR, пересечения и расхождения (по location+механизму). Материализовать свод-артефакт `reports/summary-<id>.md` (определение — `call-mechanics.md` §4): по каждой MAJOR — {id, verifier_role, verdict, evidence}; refuted-список (явный, даже пустой); принятые риски; ревизия состояния (хэш каталога/коммит).
29
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 при вызове обязателен: без него это новая сессия, а не продолжение — дефект шага.
30
30
  5. **Арбитр — только при расхождениях**: `subagent(agent="arbiter", prompt="Спорные пункты: <пункт → evidence A → evidence B>. По каждому: подтверждён/опровергнут/недоказуем + команда/цитата.")`. Вердикт свода (и верификация/арбитраж) фиксируются ДО правок и **вместе с ревизией состояния** (хэш каталога/коммит); любая правка после фиксации вердикта → повторный прогон (re-audit) либо явное понижение вердикта: до вердикта — не править.
31
- 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`.
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`.
32
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`.
33
33
  8. **Повторяй шаги 3–7, пока не достигнут полный ready** (число кругов не ограничено; все роли ready, MAJOR=0). С 3-го круга нерешённые пункты — арбитр поштучно; повторяющиеся ложные — снять и записать как шум аудита.
34
34
  9. **Ворота (релиз; порядок: ред-тим → мета)**: замороженный коммит → `redteam` (мандат фальсификации + корпуса/стенды) → цикл по его находкам (MAJOR — с верификацией сессией противоположной роли) → `meta` (на вход: отчёты + свод `reports/summary-<id>.md`; evidence, полнота, отсутствие «зелёные тесты = проверено»).
@@ -43,7 +43,7 @@
43
43
 
44
44
  - Разведка и проверка («найти», «понять», «воспроизвести») — субагентам: у них
45
45
  чистый контекст, а в ответ возвращается 1–2k токенов выводов, а не дамп.
46
- - Реализация — одному писателю на рабочее пространство: связанные решения должны
46
+ - Реализация — одному пишущему субагенту на рабочее пространство (root сам код, доки и тесты не правит — только артефакты аудита/состояния): связанные решения должны
47
47
  приниматься в одном контексте (иначе параллельные агенты расходятся в стиле и
48
48
  контрактах).
49
49
  - Решения, сведение и приёмка — root-оркестратору.
package/index.ts CHANGED
@@ -55,6 +55,7 @@ import matter from "gray-matter"
55
55
  import { registerKitCommands } from "./lib/commands"
56
56
  import { createSessionBudget, readSessionBudgetOptions, safeJson } from "./lib/session-budget"
57
57
  import { registerAuditTools } from "./lib/tools"
58
+ import { createWriteGuard } from "./lib/write-guard"
58
59
 
59
60
  /** Stable plugin id: diagnostics, enable/disable selectors, storage scope. */
60
61
  export const PLUGIN_ID = "orchestrator.uni"
@@ -419,10 +420,20 @@ async function setup(ctx: Plugin.Context): Promise<Plugin.Cleanup | undefined> {
419
420
  // tools are registered must abort that subscription too — otherwise a
420
421
  // half-registered plugin leaks an event listener for the lifetime of the server.
421
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
+ })
422
432
  let toolsCleanup: () => void
423
433
  try {
424
434
  toolsCleanup = await registerAuditTools(ctx, { sessionBudget })
425
435
  } catch (error) {
436
+ await writeGuard.cleanup()
426
437
  await sessionBudget.cleanup()
427
438
  throw error
428
439
  }
@@ -431,6 +442,7 @@ async function setup(ctx: Plugin.Context): Promise<Plugin.Cleanup | undefined> {
431
442
  commandsCleanup = await registerKitCommands(ctx, locale)
432
443
  } catch (error) {
433
444
  toolsCleanup()
445
+ await writeGuard.cleanup()
434
446
  await sessionBudget.cleanup()
435
447
  throw error
436
448
  }
@@ -441,7 +453,11 @@ async function setup(ctx: Plugin.Context): Promise<Plugin.Cleanup | undefined> {
441
453
  try {
442
454
  toolsCleanup()
443
455
  } finally {
444
- await sessionBudget.cleanup()
456
+ try {
457
+ await writeGuard.cleanup()
458
+ } finally {
459
+ await sessionBudget.cleanup()
460
+ }
445
461
  }
446
462
  }
447
463
  }
@@ -0,0 +1,283 @@
1
+ /**
2
+ * write-guard — path-root enforcement for the root orchestrator's edits.
3
+ *
4
+ * Flat `permissions` are a last-match-wins list, so they cannot express two
5
+ * properties at once: `reports/` is an artifact subtree (even `reports/src/x`
6
+ * must stay writable) while `src/`, `docs/` and the other source roots are
7
+ * denied at their first segment (even under an artifact-looking name such as
8
+ * `src/reports/x`). The rules also cannot tell a root artifact file
9
+ * (`state.md`) from a source file of the same name (`app/state.md`).
10
+ *
11
+ * This module implements the intended contract as a permission hook:
12
+ *
13
+ * - resolve a relative resource against the session's location directory and
14
+ * strip that prefix; a resource that stays outside the location is denied —
15
+ * the guard is the workspace boundary, so `reports/`, `docs/planning/` and
16
+ * `audit-*` are writable only inside it. The location is read from the
17
+ * session on every event (`session.get`), so a moved session is evaluated
18
+ * against its new directory; the setup location is only a fallback (used when
19
+ * the lookup is missing, fails or exceeds its 250 ms bound, with one warning);
20
+ * - normalize separators and collapse `.`/`..`, then walk the segments left to
21
+ * right and let the first root decide: `reports` → allow (the whole subtree
22
+ * is an artifact), `docs` + `planning` + a path whose last segment is `.md`
23
+ * (any depth) → allow, any source root (`src`, `lib`, `test`, `tests`,
24
+ * `agents`, `tools`, `schemas`, `eval`, `.github`) → deny. `docs` is not part
25
+ * of that set: its own branch above allows nested `docs/planning/` Markdown
26
+ * and denies every other `docs/` path;
27
+ * - with no root in the path, an artifact file (`case-*`, `risks-*`,
28
+ * `handoff-*`, `decisions-*`, `state.md`) is allowed only in the location
29
+ * root, under `reports/`, under `docs/planning/`, or inside an `audit-*`
30
+ * directory;
31
+ * - everything else is denied.
32
+ *
33
+ * The hook only tightens an `edit` evaluation for the `orchestrator` agent: it
34
+ * runs after the flat rules, so a local `edit: allow *` on the agent (a config
35
+ * or a Markdown override) cannot bypass the guard. A failure anywhere in the
36
+ * guard is fail-open — a broken guard must never break the session.
37
+ */
38
+ import type { Plugin } from "@opencode/plugin"
39
+
40
+ /** Source roots that hold project code or kit content; `docs` has its own branch in `decideSegments`. */
41
+ const SOURCE_ROOTS: ReadonlySet<string> = new Set([
42
+ "src",
43
+ "lib",
44
+ "test",
45
+ "tests",
46
+ "agents",
47
+ "tools",
48
+ "schemas",
49
+ "eval",
50
+ ".github",
51
+ ])
52
+
53
+ /** Reserved artifact subtree: everything below it is an audit/state artifact. */
54
+ const ARTIFACT_DIR = "reports"
55
+
56
+ /** Artifact files allowed only in an artifact location (see `artifactLocation`). */
57
+ const ARTIFACT_FILE = /^(?:case-.*|risks-.*|handoff-.*|decisions-.*|state)\.md$/
58
+
59
+ /** Ad-hoc audit workspace directory; any depth below it is an artifact location. */
60
+ const AUDIT_DIR = /^audit-/
61
+
62
+ const MARKDOWN_FILE = /\.md$/
63
+
64
+ /** Message attached to a denied evaluation; also shown to the model. */
65
+ export const WRITE_GUARD_MESSAGE = "root orchestrator may edit only audit/session-state artifacts"
66
+
67
+ /** Upper bound on the per-event session lookup; a hung store must not hold the edit open. */
68
+ const SESSION_LOOKUP_TIMEOUT_MS = 250
69
+
70
+ export type EditDecision = "allow" | "deny"
71
+
72
+ /** Case-fold one segment the way the host resolves paths on `platform`. */
73
+ function fold(segment: string, platform: string): string {
74
+ return platform === "win32" ? segment.toLowerCase() : segment
75
+ }
76
+
77
+ /** Split a path into segments, collapsing `.`/`..` and unifying separators. */
78
+ function normalizeSegments(input: string): string[] {
79
+ const segments: string[] = []
80
+ for (const part of input.replaceAll("\\", "/").split("/")) {
81
+ if (part === "" || part === ".") continue
82
+ if (part === "..") {
83
+ segments.pop()
84
+ continue
85
+ }
86
+ segments.push(part)
87
+ }
88
+ return segments
89
+ }
90
+
91
+ /** Whether a resource names an absolute path (POSIX root or a Windows drive). */
92
+ function isAbsolute(resource: string): boolean {
93
+ const normalized = resource.replaceAll("\\", "/")
94
+ return normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized)
95
+ }
96
+
97
+ /** Drop the location prefix when `absolute` is inside it; `undefined` otherwise. */
98
+ function stripLocation(
99
+ absolute: readonly string[],
100
+ location: readonly string[],
101
+ platform: string,
102
+ ): string[] | undefined {
103
+ if (location.length === 0 || location.length > absolute.length) return undefined
104
+ for (let index = 0; index < location.length; index += 1) {
105
+ if (fold(absolute[index]!, platform) !== fold(location[index]!, platform)) return undefined
106
+ }
107
+ return absolute.slice(location.length)
108
+ }
109
+
110
+ /** `reports/` and `docs/planning/` already allow earlier; roots and files are checked here. */
111
+ function artifactLocation(segments: readonly string[], platform: string): boolean {
112
+ if (segments.length === 1) return true
113
+ return segments.slice(0, -1).some((segment) => AUDIT_DIR.test(fold(segment, platform)))
114
+ }
115
+
116
+ function decideSegments(segments: readonly string[], platform: string): EditDecision {
117
+ for (let index = 0; index < segments.length; index += 1) {
118
+ const segment = segments[index]!
119
+ const key = fold(segment, platform)
120
+ if (key === ARTIFACT_DIR) return "allow"
121
+ // `docs` is not part of SOURCE_ROOTS: this branch allows nested planning
122
+ // Markdown and denies every other `docs/` path.
123
+ if (key === "docs") {
124
+ const next = segments[index + 1]
125
+ const last = segments.at(-1)
126
+ if (
127
+ next !== undefined &&
128
+ fold(next, platform) === "planning" &&
129
+ last !== undefined &&
130
+ MARKDOWN_FILE.test(fold(last, platform))
131
+ ) {
132
+ return "allow"
133
+ }
134
+ return "deny"
135
+ }
136
+ if (SOURCE_ROOTS.has(key)) return "deny"
137
+ }
138
+ const last = segments.at(-1)
139
+ if (last !== undefined && ARTIFACT_FILE.test(fold(last, platform)) && artifactLocation(segments, platform)) {
140
+ return "allow"
141
+ }
142
+ return "deny"
143
+ }
144
+
145
+ /**
146
+ * Decide whether the root orchestrator may write `resource` in `locationDirectory`.
147
+ *
148
+ * A relative resource is resolved against the location directory; an absolute one
149
+ * is used as is. The path is then reduced to its relative form: anything that stays
150
+ * outside the location is denied (the location is the workspace boundary), and an
151
+ * inside path is decided by its first segment.
152
+ */
153
+ export function decideEdit(
154
+ resource: string,
155
+ locationDirectory: string,
156
+ platform: string = process.platform,
157
+ ): EditDecision {
158
+ const location = normalizeSegments(locationDirectory)
159
+ const raw = resource.replaceAll("\\", "/")
160
+ const resolved = isAbsolute(raw) ? normalizeSegments(raw) : normalizeSegments(`${location.join("/")}/${raw}`)
161
+ const relative = stripLocation(resolved, location, platform)
162
+ if (relative === undefined) return "deny"
163
+ return decideSegments(relative, platform)
164
+ }
165
+
166
+ export interface WriteGuardOptions {
167
+ /** Directory the session belongs to; relative resources resolve against it. */
168
+ readonly locationDirectory: string
169
+ /** Reports a missing host API or a registration failure (the guard stays no-op). */
170
+ readonly warn: (message: string) => void
171
+ }
172
+
173
+ export interface WriteGuard {
174
+ /** `true` when the permission hook is registered. */
175
+ readonly available: boolean
176
+ /** Disposes the hook registration; safe to call when the guard is unavailable. */
177
+ readonly cleanup: () => Promise<void>
178
+ }
179
+
180
+ /** The evaluate event fields the guard reads and writes (the host type is structural). */
181
+ interface EvaluateEvent {
182
+ readonly sessionID?: string
183
+ readonly agent?: string
184
+ readonly action: string
185
+ readonly resources: readonly string[]
186
+ effect: "allow" | "deny" | "ask"
187
+ message?: string
188
+ }
189
+
190
+ /** The session surface the guard reads the current location from (structural). */
191
+ interface SessionDirectoryApi {
192
+ readonly get?: (input: { readonly sessionID: string }) => Promise<unknown>
193
+ }
194
+
195
+ /**
196
+ * Register the path-root write guard on the host's permission hook.
197
+ * Never throws: an unavailable or failing host API leaves the guard as a no-op.
198
+ */
199
+ export async function createWriteGuard(ctx: Plugin.Context, options: WriteGuardOptions): Promise<WriteGuard> {
200
+ const noop = async () => {}
201
+ const permission = (ctx as { readonly permission?: Plugin.Context["permission"] }).permission
202
+ if (permission === undefined || typeof permission.hook !== "function") {
203
+ options.warn("write guard: ctx.permission.hook is unavailable; the path-root guard is not registered")
204
+ return { available: false, cleanup: noop }
205
+ }
206
+ const session = (ctx as { readonly session?: SessionDirectoryApi }).session
207
+ // A failing, missing or hanging `session.get` falls back to the setup location;
208
+ // the warning is emitted once per guard so a persistent failure cannot flood
209
+ // the log on every edit (the single flag is deliberate — no counter, no reset).
210
+ let sessionWarned = false
211
+ const warnSessionOnce = (message: string): void => {
212
+ if (sessionWarned) return
213
+ sessionWarned = true
214
+ options.warn(message)
215
+ }
216
+ const locationFor = async (sessionID: string | undefined): Promise<string> => {
217
+ if (typeof session?.get !== "function") {
218
+ warnSessionOnce("write guard: ctx.session.get is unavailable, using the setup location")
219
+ return options.locationDirectory
220
+ }
221
+ if (sessionID === undefined) return options.locationDirectory
222
+ let timer: ReturnType<typeof setTimeout> | undefined
223
+ try {
224
+ // The lookup is bounded: a hung session store must not hold the permission
225
+ // event open (fail-open covers a rejection, not a promise that never settles).
226
+ const lookup = session.get({ sessionID })
227
+ const timeout = new Promise<never>((_, reject) => {
228
+ timer = setTimeout(() => {
229
+ reject(new Error(`timed out after ${SESSION_LOOKUP_TIMEOUT_MS} ms`))
230
+ }, SESSION_LOOKUP_TIMEOUT_MS)
231
+ })
232
+ const info = (await Promise.race([lookup, timeout])) as
233
+ | { readonly location?: { readonly directory?: unknown } }
234
+ | undefined
235
+ const directory = info?.location?.directory
236
+ if (typeof directory === "string" && directory !== "") return directory
237
+ warnSessionOnce("write guard: the session reports no location directory, using the setup location")
238
+ return options.locationDirectory
239
+ } catch (error) {
240
+ // Fail-open: an unreadable session must not break (or disable) the guard.
241
+ warnSessionOnce(
242
+ `write guard: cannot read the session directory, using the setup location: ${
243
+ error instanceof Error ? error.message : String(error)
244
+ }`,
245
+ )
246
+ return options.locationDirectory
247
+ } finally {
248
+ if (timer !== undefined) clearTimeout(timer)
249
+ }
250
+ }
251
+ try {
252
+ const registration = await permission.hook("evaluate", async (event: EvaluateEvent) => {
253
+ try {
254
+ if (event.agent !== "orchestrator" || event.action !== "edit" || event.effect === "deny") return
255
+ // The session may have been moved since setup: the host resolves the
256
+ // resource against the session's current directory, so the guard must too.
257
+ const locationDirectory = await locationFor(event.sessionID)
258
+ const denied = event.resources.some((resource) => decideEdit(resource, locationDirectory) === "deny")
259
+ if (!denied) return
260
+ event.effect = "deny"
261
+ event.message = WRITE_GUARD_MESSAGE
262
+ } catch (error) {
263
+ // Fail-open: a broken guard must never break the session it protects.
264
+ options.warn(`write guard: evaluation failed: ${error instanceof Error ? error.message : String(error)}`)
265
+ }
266
+ })
267
+ let disposed = false
268
+ return {
269
+ available: true,
270
+ cleanup: async () => {
271
+ // Idempotent: a repeated cleanup must not dispose the registration twice.
272
+ if (disposed) return
273
+ disposed = true
274
+ await registration.dispose()
275
+ },
276
+ }
277
+ } catch (error) {
278
+ options.warn(
279
+ `write guard: cannot register the permission hook: ${error instanceof Error ? error.message : String(error)}`,
280
+ )
281
+ return { available: false, cleanup: noop }
282
+ }
283
+ }
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.5.0",
4
+ "version": "0.5.1",
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",