opencode-orchestrator-uni 0.5.1 → 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.
- package/README.md +244 -513
- package/README.ru.md +247 -524
- package/docs/en/reference.md +482 -0
- package/docs/ru/reference.md +482 -0
- package/package.json +2 -2
|
@@ -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>
|