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.
- package/README.md +244 -508
- package/README.ru.md +247 -519
- package/agents/en/orchestrator.md +42 -5
- package/agents/ru/orchestrator.md +42 -5
- package/docs/en/call-mechanics.md +6 -4
- package/docs/en/instruction-other-session.md +6 -6
- package/docs/en/reference.md +482 -0
- package/docs/en/root-runbook.md +1 -1
- package/docs/en/session-discipline.md +1 -1
- package/docs/ru/call-mechanics.md +6 -4
- package/docs/ru/instruction-other-session.md +6 -6
- package/docs/ru/reference.md +482 -0
- package/docs/ru/root-runbook.md +1 -1
- package/docs/ru/session-discipline.md +1 -1
- package/index.ts +17 -1
- package/lib/write-guard.ts +283 -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>
|
package/docs/en/root-runbook.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
- Правки по
|
|
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 не аудирует и не подменяет вердикты; писатель один **на рабочее
|
|
227
|
+
- root не аудирует и не подменяет вердикты; писатель один **на рабочее пространство** — `fix-runner` (средние/крупные) или `general` (крупные задачи вне аудита); root пишет только артефакты аудита/состояния; несколько `fix-runner` — параллельно только в отдельных worktree и с последующим сведением root'ом.
|
|
226
228
|
- Не запускай двух писателей, чьи **наборы файлов** могут пересечься, — безусловно, независимо от рабочего пространства: непересекающиеся списки «к закрытию» не заменяют проверку пересечения наборов файлов (пути + правило поиска класса).
|
|
227
229
|
- Аудиторы read-only; проектная конкретика — только в брифе.
|
|
228
230
|
- Правки начинаются только после фиксации вердикта свода (и верификации MAJOR/арбитража). До вердикта — не править.
|