opencode-orchestrator-uni 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,482 @@
1
+ # Reference
2
+
3
+ Deep reference for the orchestrator kit: the write guard, the five tools, the
4
+ session protocol, model and locale configuration, and precedence rules.
5
+
6
+ [Back to the README](../../README.md)
7
+
8
+ The kit registers on setup via `ctx.agent.transform` / `ctx.reference.transform`.
9
+ The `audit-kit` reference points at this package's per-locale `docs/<locale>/`
10
+ directory; OpenCode lists it as an additional local directory, so agents read
11
+ `<reference path>/call-mechanics.md` directly. The reference ships
12
+ `call-mechanics.md`, `process-ready-loop.md`, `root-runbook.md`,
13
+ `instruction-other-session.md`, `context-efficiency.md` and
14
+ `session-discipline.md` — for example
15
+ `<reference path>/process-ready-loop.md`.
16
+
17
+ ## Write Guard
18
+
19
+ The orchestrator itself has `edit` denied on `*`. The flat `permissions` are only
20
+ a narrow fallback allowlist for audit/state artifacts, and the real guard is the
21
+ `permission.hook("evaluate")` hook in `lib/write-guard.ts` with path-root
22
+ semantics.
23
+
24
+ Auditors are read-only at the tool level for `edit`/`write`/`patch` (`edit: deny`);
25
+ the prompt forbids all other writes. A shell write (redirect, `sed -i`, `tee`) is
26
+ an accepted risk (I-M15), not a sandbox.
27
+
28
+ - The session directory is read per event (`session.get`), so a moved session is
29
+ evaluated against its new directory. If the lookup fails, the setup directory
30
+ is used and a warning is written — fail-open.
31
+ - A resource outside the session directory is denied: the guard is the workspace
32
+ boundary, so `reports/`, `docs/planning/` and `audit-*` are not writable
33
+ outside it. Inside it the first **root** segment decides.
34
+ - `reports/` is a reserved artifact root — the whole subtree is writable (even
35
+ `reports/src/x.ts`), so project code must not live there (a
36
+ `packages/reports/**` path counts as an artifact by contract).
37
+ `docs/planning/**/*.md` is allowed (nested Markdown at any depth; a `.ts` file
38
+ is not). `docs` is a separate branch, not a source root — every other
39
+ `docs/**` path is denied.
40
+ - The source roots (`src`, `lib`, `test`, `tests`, `agents`, `tools`, `schemas`,
41
+ `eval`, `.github`) are denied even under an artifact-looking prefix
42
+ (`src/reports/x.ts`, `src/docs/planning/x.md`).
43
+ - Root artifact files (`case-*`, `risks-*`, `handoff-*`, `decisions-*`,
44
+ `state.md`) are allowed only in the location root, under `reports/`,
45
+ `docs/planning/` or an `audit-*` directory — `app/state.md` is denied.
46
+ - The hook runs after the flat rules, so a local `edit: allow` for `orchestrator`
47
+ is closed too. The flat fallback carries only that artifact set as
48
+ location-root rules (`reports/*`, `case-*.md`, `risks-*.md`, `handoff-*.md`,
49
+ `decisions-*.md`, `state.md`, `docs/planning/*.md`) plus `audit-*/…`. The broad
50
+ `*/state.md`-style patterns are gone, so `src/state.md` is denied even without
51
+ the hook, but last-match still cannot express root priority everywhere
52
+ (`audit-x/src/case-y.md`) — a warning is written.
53
+ - The fallback is narrower than the hook for nested artifact roots:
54
+ `lab/x/reports/*` and similar paths are written only with the hook (the root
55
+ rules cover `reports/*` at the location root only).
56
+ - A failing or timed-out session lookup (250 ms bound) falls back to the setup
57
+ directory and writes the warning once per guard; for a moved session an
58
+ absolute artifact path may then be denied (fail-closed for that class). A
59
+ single `sessionWarned` flag without a counter is accepted: failures after the
60
+ first warning stay silent.
61
+ - `shell` stays allowed for git and checks. A shell write and symlinks/junctions
62
+ (the host matches the normalized lexical path, not `realpath`) are accepted
63
+ risks, as is fail-open (a broken guard never breaks the session), governed by
64
+ the prompt rather than a sandbox. A tool denial is a role rule — the edit goes
65
+ to a subagent.
66
+
67
+ ## Tools
68
+
69
+ The plugin registers five deterministic, **read-only** tools in the `orch_uni`
70
+ namespace.
71
+
72
+ | Effective name | Read-only | What it does |
73
+ | -------------------------- | --------- | ------------------------------------------------------------------------------- |
74
+ | `orch_uni_validate_report` | yes | Extract the SGR JSON block of a report, resolve its schema, validate it |
75
+ | `orch_uni_context_lint` | yes | Deterministic audit of AGENTS.md/SPEC.md: budget, vague phrases, links, sections |
76
+ | `orch_uni_snapshot` | yes | Deterministic revision of a directory tree (per-file sha256 + aggregate hash) |
77
+ | `orch_uni_sessions` | yes | Child sessions of a session: agent, model, cost, tokens, time |
78
+ | `orch_uni_session_stats` | yes | Session protocol counters: prompt, compactions, tools/turn, capped outputs |
79
+
80
+ Every tool answers with a JSON text body. Failures are **typed codes** inside the
81
+ body (`E_INPUT`, `E_NO_JSON`, …) instead of thrown tool errors, so an agent can
82
+ branch on the code without parsing prose. None of the five writes to the
83
+ workspace, the session store or the configuration.
84
+
85
+ ### `orch_uni_validate_report`
86
+
87
+ ```jsonc
88
+ { "text": "<report text>" } // or
89
+ { "path": "reports/empiric-r1.md", // absolute, or relative to the location directory
90
+ "schema": "empiric/v1" } // optional: expected schema id
91
+ ```
92
+
93
+ Provide exactly one of `text` / `path`. The tool takes the **last** fenced ```` ```json ````
94
+ block that parses and carries a `schema` field, resolves the schema by that field and
95
+ validates the document.
96
+
97
+ ```jsonc
98
+ { "valid": true, "schema": "empiric/v1", "errors": [], "warnings": [ … ] }
99
+ { "valid": false, "code": "E_INVALID", "schema": "empiric/v1",
100
+ "errors": [ { "path": "$.findings[0].severity", "message": "expected one of …" } ],
101
+ "warnings": [] }
102
+ ```
103
+
104
+ | Code | Meaning |
105
+ | ------------------- | ---------------------------------------------------------------- |
106
+ | `E_INPUT` | Bad call: neither/both of `text`/`path`, wrong types, unreadable file |
107
+ | `E_NO_JSON` | No fenced ```` ```json ```` block found |
108
+ | `E_PARSE` | A block exists but none parses as JSON |
109
+ | `E_UNKNOWN_SCHEMA` | The report names an id that is not shipped (or no id at all) |
110
+ | `E_SCHEMA_MISMATCH` | The report names another schema than the expected one |
111
+ | `E_INVALID` | The document does not satisfy its schema (see `errors`) |
112
+ | `E_SCHEMA_IO` | `schemas/*.json` unreadable/malformed (see `directory`, `file`) |
113
+ | `E_ABORTED` | The session was stopped while reading the report |
114
+
115
+ `warnings` carry the prompt rules a *structurally valid* report can still miss — things
116
+ JSON Schema cannot express: a missing `rc` on an `empiric` claim whose `method` is
117
+ `run|measure|compare_base`, `self_check` items other than 1–19, an `invariant`
118
+ invariant that is `violated` without a counterexample, an uncovered mandatory
119
+ `lines_checked` line, `meta`'s `verdict` diverging from its `gate`, and
120
+ `redteam`'s `closing_list`/`verdict` invariants.
121
+
122
+ The schemas ship as `schemas/*.json` and are derived **strictly** from the SGR
123
+ sections of `agents/*.md`, which stay the source of truth: the test suite parses every
124
+ agent's SGR template and fails when a prompt field or enum value is not accepted by its
125
+ schema. `context-lint-failure/v1` is the typed failure envelope of
126
+ `orch_uni_context_lint` — it has no agent template and is never a report. Schemas set
127
+ `additionalProperties` permissively — agents may add fields of their
128
+ own (`verdict_label`, `check`, …).
129
+
130
+ ### `orch_uni_context_lint`
131
+
132
+ ```jsonc
133
+ { "path": "AGENTS.md" } // file or directory, relative to the location
134
+ { "path": ".", "max_lines": 150, "max_tokens": 2000 }
135
+ { "path": ".", "max_words": 2000 } // SPEC word budget; default 1500 (target), ceiling 2000
136
+ { "path": ".", "max_file_bytes": 500000, "max_files": 100, "max_findings": 50, "max_total_findings": 500 }
137
+ ```
138
+
139
+ Deterministic, read-only audit of `AGENTS.md`, `SPEC.md` and `CLAUDE.md`. It returns
140
+ `{ schema: "context-lint/v1", ok: true, verdict, closing_list, files, summary, truncated? }`;
141
+ each finding carries `id`, `severity`, `kind`, `evidence`, `detail` and `fix`. Checks:
142
+ line/token/word budget, distinct vague phrases, duplicate lines, broken relative links,
143
+ placeholders, dates/versions in the first three lines, required sections and AC test
144
+ columns. Links and explicit targets are confined by their **real** path: a symlink or
145
+ junction that resolves outside the root is skipped, and such a target is refused
146
+ (`E_OUT_OF_ROOT`). Code fences, inline code and HTML comments are masked, so a sample is
147
+ never a rule: structural checks (commands, boundaries, verification) read the full mask,
148
+ while requirements, AC ids and non-goals read the fence mask. Link targets are
149
+ streamed from the line scanner one at a time (no candidate array), so memory does
150
+ not grow with the number of links; checks are capped per file at 50 000
151
+ candidates, the signal is checked per line and every 100 candidates, and the
152
+ link caches keep at most 5 000 entries, so an abort wins; 2001 links are checked
153
+ normally, 50 000 valid links still pass, and the 50 001st candidate makes the list
154
+ incomplete (`truncated: true` plus a MAJOR `link candidates truncated` marker in
155
+ `closing_list`), so a capped run can never pass. Broken
156
+ links have no separate per-file cap: the finding caps bound the output and report the
157
+ overflow. A line is scanned however long it is: the link scanner and the placeholder regex
158
+ are linear, so a 4100-character line is checked like any other and a broken link or
159
+ placeholder on it is the real finding, not a marker. Duplicate lines have no inner cap.
160
+ UNC/network targets are never probed. Measured (bun 1.4.2, Windows, fresh
161
+ process): 50 001 broken links with `max_findings: 2` → ~1.4 s and ~67 MB peak
162
+ RSS; 50k plain lines → ~0.14 s / ~65 MB; a 2 MB single line with links →
163
+ ~3.1 s / ~116 MB (~41 MB without links). Masking reuses a line as-is when it
164
+ has no backtick and no `<pre>`/`<code>`/`<!--`, so it never copies a line
165
+ character by character, and a 2 MB single line stays capped at 50 000
166
+ candidates instead of materializing every link.
167
+ Limits: ≤ 2 MB per file, ≤ 500 files per scan. `max_findings` (per file) is N per
168
+ severity: up to N MAJOR and N MINOR/INFO are kept, and any excess is replaced by one
169
+ marker (MAJOR when a MAJOR was dropped, otherwise INFO); a MAJOR marker stays in
170
+ `closing_list`. `max_total_findings` (global, default 1000) keeps MAJOR
171
+ findings first; when MAJOR overflow, `max_total` findings plus one MAJOR `truncated`
172
+ marker are kept. A capped list sets `truncated: true` instead of hiding findings.
173
+ Markers are additional to `max_findings` and do not consume its budget: a capped
174
+ link-candidate list and a capped findings list report one marker each in the same run, so
175
+ `max_findings: 2` over 20 001 broken links yields 2 kept findings plus 2 markers. Cap
176
+ markers point at `file:1` in `evidence`.
177
+ Non-goals are counted as top-level items: a wrapped paragraph is one item, inline
178
+ `;`/`,`/`and` and their localized equivalents split only at the top level (not inside
179
+ `(…)`, `[…]`, backticks of any width or `<code>`), and a bullet at 0–4 spaces under the label is an item — fenced,
180
+ `<pre>` and deeper-indented code is not.
181
+ Placeholders are `<...>` (three or more dots) and `TODO`/`TBD`/`FIXME`; `<DoD>` and
182
+ `<name>` are markup.
183
+ `max_lines` is the single 200-line root budget the tool applies: the 60/200/100
184
+ per-level figures in [`docs/`](../) are authoring guidance, not separate tool limits.
185
+ `max_words` defaults to the 1500-word SPEC target; pass `max_words: 2000` to enforce the
186
+ documented ceiling instead.
187
+ Typed failures: `E_INPUT`, `E_NOT_FOUND`, `E_NO_FILES`, `E_IO`, `E_ABORTED`,
188
+ `E_LIMIT_FILE`, `E_LIMIT_FILES`, `E_OUT_OF_ROOT`.
189
+ A failure is a typed payload `{schema:"context-lint-failure/v1", ok:false, code, message, path?, bytes?, files?, max?}`
190
+ that validates only against `context-lint-failure/v1`; a report is
191
+ `{schema:"context-lint/v1", ok, verdict, closing_list, files, summary, truncated?}` and every file carries `lines`,
192
+ `words`, `chars` and `tokens_est` (tokens are estimated as Cyrillic/3 + the rest/4). A directory
193
+ target is scanned to depth 3, and `max_files` refuses early: the walk stops at the first file above
194
+ the limit. Nothing is dropped silently: a directory below the depth limit, a link or junction that
195
+ resolves outside the workspace root, and an unclosed fence, `<pre>` or HTML comment are INFO
196
+ findings, so a `pass` never hides an unchecked region. An unclosed `<code>` is a
197
+ line-local mask: it hides the rest of its line and is not reported (only fence,
198
+ `<pre>` and HTML comments span lines and get an INFO marker). An abort is observed
199
+ between files, every 2048 lines of a line-oriented check and at each gate, so the
200
+ worst-case latency is one mask pass over a file capped by `max_file_bytes` (tens of
201
+ milliseconds per 2 MB), not the whole scan. Commands count only inside code, a Task table
202
+ or a Commands section, an HTML comment such as `<!-- npm install -->` is not a command, and `make`
203
+ needs a target, so prose such as "Make sure …" is not a command. A scan started at the root also
204
+ picks up fixture trees such as `eval/`; pass an explicit path for the canonical files.
205
+
206
+ ### `orch_uni_snapshot`
207
+
208
+ ```jsonc
209
+ { "path": "lab/stack-test", "exclude": ["tmp"] } // exclude is optional and additive
210
+ ```
211
+
212
+ ```jsonc
213
+ { "root": "C:\\…\\lab\\stack-test",
214
+ "files": [ { "path": "a.txt", "sha256": "…", "bytes": 1 } ],
215
+ "aggregate": "…" }
216
+ ```
217
+
218
+ - `aggregate = sha256` over the sorted lines `"<relpath>\0<sha256>\n"` (`/` separators,
219
+ codepoint order) — stable across runs, machines and traversal order, and it changes
220
+ whenever one file's path or content changes.
221
+ - Excluded by default (always): `node_modules`, `.git`, `.ae`; `exclude` adds names,
222
+ whole relative paths or relative prefixes (`lab/tmp`). Symlinks are skipped.
223
+ - Limits: ≤ 5000 files, ≤ 50 MiB per file — `E_LIMIT_FILES` / `E_LIMIT_BYTES` (fail the
224
+ whole snapshot; a truncated snapshot would be a wrong aggregate). `E_NOT_FOUND` for a
225
+ missing path or a non-directory, `E_ABORTED` when the session is stopped.
226
+
227
+ ### `orch_uni_sessions`
228
+
229
+ ```jsonc
230
+ {} // children of the calling session
231
+ { "sessionID": "ses_…", "recursive": true } // whole subtree of another session
232
+ ```
233
+
234
+ ```jsonc
235
+ { "sessionID": "ses_parent", "recursive": false, "source": "session.index",
236
+ "authoritative": false, "indexSize": 1, "count": 1,
237
+ "limitations": [ … ],
238
+ "sessions": [ { "sessionID": "ses_child", "parentID": "ses_parent", "agent": "explore",
239
+ "model": "deepseek/deepseek-flash", "variant": "default", "title": "…",
240
+ "cost": 0.0005, "tokens": { "input": 3491, "output": 2 },
241
+ "created": "2026-10-15T04:01:06.627Z", "updated": "…" } ] }
242
+ ```
243
+
244
+ `source` tells which route produced the answer:
245
+
246
+ - `session.list` — this build exposes a session listing API; the answer is
247
+ **authoritative** and complete (`recursive` walks the subtree page by page).
248
+ - `session.index` — the fallback used by OpenCode 2.0.28: `ctx.session` has
249
+ `get/create/…` but **no `list`**, so children cannot be enumerated from the store. The
250
+ plugin subscribes to `session.created` / `session.deleted` events at setup and keeps
251
+ `child → parent` in its plugin-scoped storage; per-child details are read from
252
+ `ctx.session.get()` at call time. The answer is **not authoritative**; the caveats are
253
+ returned in `limitations` (sessions created before the plugin instance started are
254
+ invisible, and an event gap hides children created while the plugin was unloaded).
255
+ - `E_NO_API` — neither route exists. The context-scan fallback (looking for the
256
+ `subagent` tool parts in `ctx.session.context`) was evaluated and rejected: in-flight
257
+ children carry no session id in their tool part and compaction drops the history.
258
+ Open question: a plugin-facing session listing (or a `parentID` filter on
259
+ `ctx.session.context`) would remove the fallback entirely.
260
+
261
+ `E_INPUT` for malformed arguments, `E_NOT_FOUND` when the parent session does not exist,
262
+ `E_ABORTED` when the session is stopped.
263
+
264
+ ### `orch_uni_session_stats`
265
+
266
+ ```jsonc
267
+ {} // counters of the calling session
268
+ { "sessionID": "ses_…" } // any session on this server, not only a child
269
+ ```
270
+
271
+ ```jsonc
272
+ { "ok": true, "sessionID": "ses_…",
273
+ "counters": {
274
+ "requests": 12, "promptTokensLast": 151204, "promptTokensMax": 161002,
275
+ "promptTokensMedian": 148900, "band": 1, "compactions": 1,
276
+ "compactionAttempts": 1, "compactionFailures": 0, "compactionSource": "event",
277
+ "reminders": 2, "toolCalls": 31, "outputsCapped": 1,
278
+ "outputCharsSaved": 18422, "errors": 0,
279
+ "toolCallsByTool": { "shell": 18, "bash": 6, "webfetch": 2, "read": 5 },
280
+ "toolOutputsCappedByTool": { "shell": 1, "bash": 0, "webfetch": 0, "read": 0 }
281
+ },
282
+ "limits": { "promptTokensSoft": 150000, "maxCompactions": 1, "maxToolOutputChars": 20000 },
283
+ "overBudget": true, "compactionLimitReached": true, "toolsPerTurn": 2.58,
284
+ "tracked": true,
285
+ "guard": { "available": true, "hooksRegistered": true, "sessionEnabled": true,
286
+ "toolOutputEnabled": true,
287
+ "toolOutputTools": ["shell", "bash", "webfetch"],
288
+ "compactionSource": "event", "eventLive": true,
289
+ "streamFailures": 0, "unkeyedCalls": 0 } }
290
+ ```
291
+
292
+ `sessionID` may name **any session on this server**, not only a child of the caller;
293
+ that is deliberate — the orchestrator uses the report to control every slice it runs.
294
+ Counters are per plugin instance, and `tracked` is `true` once any event or hook
295
+ observed the session; a `session.deleted` event forgets it. `band` is the highest
296
+ budget level reached (`0` below the soft threshold); `promptTokensMedian` is the
297
+ median over the last 256 requests; `toolCalls` and the per-tool counters include
298
+ nested Code Mode calls; `toolCallsByTool` and `toolOutputsCappedByTool` split calls
299
+ and capped outputs by the normalized tool name (namespace stripped, lower-case);
300
+ `outputCharsSaved` is a character count, not bytes, and an upper estimate (the
301
+ injected stub is subtracted). `compactions` counts confirmed
302
+ `session.compaction.ended` events only; `compactionAttempts` counts every
303
+ `session.compaction` hook call, so on a host with a dead or absent event stream
304
+ attempts keep growing while `compactions` and `compactionLimitReached` stay put (a
305
+ failed attempt is never a stop signal). A dead stream is reported once in
306
+ `guard.streamFailures`; the per-session `errors` counts hook failures only, so a
307
+ transport incident is not attributed to every session. Duplicate compaction events
308
+ are counted once only when the host supplies `event.id` or a durable position;
309
+ without either identity a re-delivered event may be counted again (fail-open in the
310
+ direction of counting). A
311
+ session the store does not return and this instance never observed is
312
+ `E_NOT_FOUND` (recognized by the typed `_tag`/`name`/`code` — `Session.NotFoundError`,
313
+ `SessionNotFoundError`, `NotFoundError`, `ENOENT` — with a narrow message fallback);
314
+ `session.get` returning `undefined` for an untracked session is the same not-found
315
+ answer; a transient `session.get` failure (a lock, permission, `unknown database`) is
316
+ `E_INTERNAL`; a known session with zero requests is a valid report with
317
+ `tracked: false`. Without `session.get`,
318
+ an untracked session is `E_NO_API`, a tracked one is `ok` with `session: null` and a
319
+ `limitations` entry. Codes: `E_INPUT`, `E_NOT_FOUND`, `E_INTERNAL`, `E_NO_API`,
320
+ `E_UNAVAILABLE` (the guard is not registered or its hooks failed), `E_ABORTED`.
321
+
322
+ ## Session Hygiene
323
+
324
+ The plugin implements the [`session-discipline.md`](./session-discipline.md)
325
+ protocol: a session is one slice, the prompt budget is ≤150k tokens, at most one
326
+ compaction, tool output goes to a file, and a slice ends with a handoff.
327
+
328
+ The runtime part is three hooks (`session.context`, `session.compaction`,
329
+ `tool.execute.after`) plus a compaction-event subscription:
330
+
331
+ - `session.context` — estimates the prompt (system + messages) and, when it crosses
332
+ a budget level, adds one short reminder. The reminder is injected **once, when a
333
+ new level is crossed**, not on every request; within a level the text is stable,
334
+ so the prefix cache is not broken.
335
+ - compaction events — the `session.compaction` hook is registered on **every** host:
336
+ it counts attempts (`compactionAttempts`) and sends a tentative note to the
337
+ nearest request; the note names no compaction number and claims no limit. Only
338
+ `session.compaction.ended` confirms a compaction (deduplicated by `event.id` or
339
+ the durable position) and raises a confirmed note with the number and the limit;
340
+ `session.compaction.failed` increments `compactionFailures`, drops the tentative
341
+ note and keeps a confirmed note that is still waiting for delivery. If the event
342
+ stream ends or throws, `guard.eventLive` turns `false`,
343
+ `guard.compactionSource` becomes `hook` and `guard.streamFailures` becomes `1`
344
+ (the per-session `errors` stays `0`); the hook keeps counting attempts, but no
345
+ confirmed compaction and no false "limit reached" is invented on that path.
346
+ - `tool.execute.after` — for `shell`, `bash`, `webfetch` it caps output over
347
+ `maxChars`: the full text goes to a private per-process root
348
+ `os.tmpdir()/opencode-tool-output-<random>/` (POSIX 0700 for the root and the
349
+ session directory, 0600 for the file). Windows ignores mode (measured: 0666),
350
+ so privacy there rests on the ACL of `os.tmpdir()` and is not checked by code.
351
+ The model gets a head and the path; the root is created with
352
+ `mkdtemp` at plugin setup, so a pre-created `opencode-tool-output` directory or
353
+ junction is never used; on POSIX the root must belong to the current uid with no
354
+ group/other access, otherwise the guard disables itself with a warning (fail-open:
355
+ results are untouched). Session directories (`s-<uuid>`) and files
356
+ (`c-<uuid>.txt`, written with `wx`) are random, and each `realpath` is proven
357
+ inside the root. The root is removed when the plugin unloads (best effort). A
358
+ lone surrogate is normalized once (U+FFFD), so the file, the head and the
359
+ counters describe the same string. Residual risk accepted: a same-user attacker
360
+ can still swap a session directory between `realpath` and `writeFile` (Node has
361
+ no `openat`). If `TMPDIR` points inside the workspace, the files land there
362
+ instead. `read`, `write`, `edit`, `execute` and the kit's own tools are not
363
+ capped, and a kit tool named without its namespace (`session_stats`) is refused
364
+ in the config with a warning.
365
+
366
+ `maxCompactions: 0` disables the compaction limit. `guard.toolOutputEnabled` is
367
+ `true` only when the private root was created **and** the effective tool list is
368
+ non-empty; `guard.toolOutputTools` is that list, and `guard.unkeyedCalls` counts
369
+ hook calls without a non-empty `sessionID` (they are dropped, not merged into an
370
+ empty bucket). An empty `tools` list disables the cap with a warning. The guard is
371
+ fail-open: a hook error neither breaks the request nor corrupts the result. Either
372
+ part can be disabled with its `enabled` flag:
373
+
374
+ ```jsonc
375
+ {
376
+ "plugins": [
377
+ {
378
+ "package": "opencode-orchestrator-uni",
379
+ "options": {
380
+ "sessionGuard": { "enabled": true, "promptTokensSoft": 150000, "maxCompactions": 1 },
381
+ "toolOutputGuard": {
382
+ "enabled": true,
383
+ "maxChars": 20000,
384
+ "headChars": 4000,
385
+ "tools": ["shell", "bash", "webfetch"]
386
+ }
387
+ }
388
+ }
389
+ ]
390
+ }
391
+ ```
392
+
393
+ Slice metrics — `orch_uni_session_stats`; closing a slice — the `/handoff` command.
394
+
395
+ ## Models
396
+
397
+ Agent definitions carry no `model`; models are configured on the plugin entry in
398
+ `opencode.json(c)`:
399
+
400
+ ```jsonc
401
+ {
402
+ "$schema": "https://opencode.ai/config.json",
403
+ "plugins": [
404
+ {
405
+ "package": "opencode-orchestrator-uni",
406
+ "options": {
407
+ // Default for every kit agent.
408
+ "model": "deepseek/deepseek-flash",
409
+ // Per-agent overrides (id -> string or expanded form).
410
+ "agents": {
411
+ "meta": "deepseek/deepseek-reasoner",
412
+ "fix-runner": { "providerID": "anthropic", "model": "claude-sonnet-4-5", "variant": "high" }
413
+ }
414
+ }
415
+ }
416
+ ]
417
+ }
418
+ ```
419
+
420
+ - `model` — default model for every kit agent; `agents.<id>` — per-agent override.
421
+ Both accept `"provider/model[#variant]"` or `{ providerID, model, variant }`.
422
+ - Without options, kit agents keep no model, so subagents inherit the session's model.
423
+ - Invalid values are ignored with a warning (`[orchestrator.uni] …`).
424
+ - Subagents run on their configured model; the primary `orchestrator` runs on the
425
+ session's model, so a model set for `orchestrator` is informational for clients
426
+ (as of OpenCode 2.0.28).
427
+ - An `agents` config entry or a local Markdown agent file that defines its own
428
+ model still wins — configuration is applied after package plugins.
429
+
430
+ ## Locale
431
+
432
+ Agent definitions and process documents ship per locale — `agents/<locale>/`
433
+ and `docs/<locale>/`; currently `ru` and `en` (the English content is a
434
+ translation of the Russian originals). The kit picks one locale in this order:
435
+
436
+ 1. `options.locale` (`"ru"` or `"en"`; full tags such as `"ru-RU"` are accepted);
437
+ 2. auto-detection: `LC_ALL` / `LC_MESSAGES` / `LANG`, then the runtime's Intl
438
+ locale (the OS setting);
439
+ 3. the default: `en`.
440
+
441
+ ```jsonc
442
+ {
443
+ "$schema": "https://opencode.ai/config.json",
444
+ "plugins": [
445
+ {
446
+ "package": "opencode-orchestrator-uni",
447
+ "options": { "locale": "ru", "model": "deepseek/deepseek-flash" }
448
+ }
449
+ ]
450
+ }
451
+ ```
452
+
453
+ A requested locale that ships no content falls back to the default locale and is
454
+ reported with a warning in the plugin log. Both locales register the same agents
455
+ under the same ids — switching the locale only changes the language of the
456
+ prompts, the process documents and the reference description.
457
+
458
+ ## Precedence
459
+
460
+ Agent **definitions** are only filled in for ids that are not defined elsewhere: an
461
+ agent with the same id coming from your configuration — an `agents` config entry, or a
462
+ Markdown file under `~/.config/opencode/agents/` (or `.opencode/agents/`) — wins and the
463
+ plugin skips its definition. A **configured model** (see [Models](#models)) is still
464
+ applied to kit agents; a model defined by your own agent entry or file wins over it,
465
+ because configuration is applied after package plugins. The same skip-if-taken rule
466
+ applies to the `audit-kit` reference.
467
+
468
+ If you previously installed the kit by copying agent files into your config directory,
469
+ **remove those copies** after enabling the plugin, otherwise the stale copies keep
470
+ overriding the plugin-provided definitions.
471
+
472
+ ## Notes
473
+
474
+ - One runtime dependency: [`gray-matter`](https://www.npmjs.com/package/gray-matter)
475
+ for frontmatter parsing. OpenCode installs package plugin dependencies automatically.
476
+ - Plugin id: `orchestrator.uni`.
477
+ - The package loads directly from TypeScript: `exports` points at `index.ts`.
478
+ There is no build step and no `dist/` (same pattern as `@opencode/plugin-figma`).
479
+ - Ships only the files listed in `package.json#files`: `index.ts`, `lib/`, `schemas/`,
480
+ `agents/`, `docs/`, `README.md`, `README.ru.md`, `LICENSE`.
481
+
482
+ <p align="right">(<a href="../../README.md">back to README</a>)</p>
@@ -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/арбитража). До вердикта — не править.