opencode-orchestrator-uni 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -8
- package/README.ru.md +141 -9
- package/agents/en/orchestrator.md +9 -0
- package/agents/ru/orchestrator.md +9 -0
- package/docs/en/root-runbook.md +2 -0
- package/docs/en/session-discipline.md +230 -0
- package/docs/ru/root-runbook.md +2 -0
- package/docs/ru/session-discipline.md +225 -0
- package/index.ts +28 -12
- package/lib/commands.ts +62 -6
- package/lib/session-budget.ts +1236 -0
- package/lib/tools.ts +145 -7
- package/package.json +1 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Session discipline: how to run development
|
|
2
|
+
|
|
3
|
+
This document defines the session protocol: where a session starts and ends, its
|
|
4
|
+
context budget, when to delegate, how to treat tool output, and what survives the
|
|
5
|
+
session. It complements `context-efficiency.md` (the instruction-file budget) and
|
|
6
|
+
`root-runbook.md` (the audit loop): those cover artifacts and verdicts, this one
|
|
7
|
+
covers keeping context small and decisions pinned.
|
|
8
|
+
|
|
9
|
+
## Why it matters
|
|
10
|
+
|
|
11
|
+
- Context is a consumable resource, not the project's memory. As input grows,
|
|
12
|
+
quality degrades even on simple tasks (context rot); "it fits the window" does
|
|
13
|
+
not mean "the model uses it".
|
|
14
|
+
- Compaction is a lossy summary. After it the agent remembers a paraphrase of
|
|
15
|
+
decisions, not the decisions; a second compaction paraphrases a paraphrase.
|
|
16
|
+
- A measured long session: 13 253 turns, 19 compactions, the median prompt grew
|
|
17
|
+
from 314k to 780k tokens. Almost all of that is re-sending history, not work.
|
|
18
|
+
This is an internal measurement of a consumer project: the artifact is not
|
|
19
|
+
attached to the repository, and the figures are not reproducible from it.
|
|
20
|
+
|
|
21
|
+
The protocol keeps the working state small and the decisions in files that
|
|
22
|
+
outlive the session.
|
|
23
|
+
|
|
24
|
+
## 1. A session is one slice
|
|
25
|
+
|
|
26
|
+
A session is one slice with a measurable DoD, not the whole project.
|
|
27
|
+
|
|
28
|
+
| Rule | Value |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| Estimated prompt budget | ≤ 150k tokens |
|
|
31
|
+
| Compactions per slice | ≤ 1 |
|
|
32
|
+
| Stop signal | the first compaction |
|
|
33
|
+
| Required output | handoff + commit |
|
|
34
|
+
|
|
35
|
+
- At the first sign of approaching the budget, close the slice instead of pushing
|
|
36
|
+
on: handoff, commit, new session.
|
|
37
|
+
- After the first compaction the slice always closes: the summary has already lost
|
|
38
|
+
details and further work runs on a paraphrase.
|
|
39
|
+
- Cut work by waves, gates or fix batches; a phase change (plan → implementation →
|
|
40
|
+
acceptance) always starts a new session.
|
|
41
|
+
|
|
42
|
+
## 2. Role separation
|
|
43
|
+
|
|
44
|
+
- Investigation and verification ("find", "understand", "reproduce") go to
|
|
45
|
+
subagents: they get a clean context and return 1–2k tokens of findings instead
|
|
46
|
+
of a dump.
|
|
47
|
+
- Implementation goes to one writer per workspace: related decisions must be made
|
|
48
|
+
in one context (otherwise parallel agents diverge in style and contracts).
|
|
49
|
+
- Decisions, consolidation and acceptance belong to the root orchestrator.
|
|
50
|
+
- The audit loop is unchanged: re-audit resumes the same sessions; short audit
|
|
51
|
+
sessions do not threaten the budget.
|
|
52
|
+
|
|
53
|
+
Rule of thumb: delegate **investigation** broadly, **implementation** narrowly,
|
|
54
|
+
keep **integration** centralized.
|
|
55
|
+
|
|
56
|
+
## 3. Tool batching
|
|
57
|
+
|
|
58
|
+
Every turn re-sends the whole prompt. One tool per turn multiplies the budget by
|
|
59
|
+
the number of small steps.
|
|
60
|
+
|
|
61
|
+
- ≥ 3 independent commands per turn, or one script instead of five to ten calls.
|
|
62
|
+
- Move long WSL commands into a wrapper script: a 250-character line in every turn
|
|
63
|
+
costs more than a file read once.
|
|
64
|
+
- For file reads use `read` with `limit`/`offset`, not a full `cat`.
|
|
65
|
+
- Target metric: ≥ 2 tools per turn.
|
|
66
|
+
|
|
67
|
+
## 4. Tool output
|
|
68
|
+
|
|
69
|
+
- Anything over 20k chars goes to a file; context gets the head (4k) and the path.
|
|
70
|
+
The guard does this automatically for `shell`, `bash`, `webfetch`.
|
|
71
|
+
- Screenshots and binary attachments are files plus a path, never base64 in
|
|
72
|
+
context.
|
|
73
|
+
- `execute` (Code Mode) is not capped by the guard: limit the result in the script
|
|
74
|
+
itself — return a summary and a path, not hundreds of kilobytes of raw JSON.
|
|
75
|
+
- `read`, `write`, `edit` and the kit's own tools are not capped: they have their
|
|
76
|
+
own contract, and losing content breaks the work.
|
|
77
|
+
|
|
78
|
+
## 5. State in files
|
|
79
|
+
|
|
80
|
+
- `handoff` — the required output of a slice (template below); default path
|
|
81
|
+
`docs/planning/handoff-<YYYY-MM-DD>.md`, the project root when the directory
|
|
82
|
+
does not exist.
|
|
83
|
+
- `decisions` — decisions and reasons; one decision per entry; default path
|
|
84
|
+
`docs/planning/decisions-<YYYY-MM-DD>.md`.
|
|
85
|
+
- `state` — branch, gates, open risks, the next three steps; refreshed before a
|
|
86
|
+
new session; default path `docs/planning/state.md`.
|
|
87
|
+
- A new session starts from `state` and the target SPEC section, not from a
|
|
88
|
+
retelling of the previous session.
|
|
89
|
+
|
|
90
|
+
## 6. Models
|
|
91
|
+
|
|
92
|
+
- Default to the cheap model; the expensive one only for architecture decisions,
|
|
93
|
+
reviews and dispute resolution.
|
|
94
|
+
- Document edits, reading and mechanical checks do not need an expensive model.
|
|
95
|
+
- Measurement: 223 turns of the expensive model cost $63, 104 turns of the cheap
|
|
96
|
+
one cost $0.09. One model switch saves more than any prompt tweak. This is an
|
|
97
|
+
internal measurement of a consumer project: the artifact is not attached to the
|
|
98
|
+
repository, and the figures are not reproducible from it.
|
|
99
|
+
|
|
100
|
+
## 7. Metrics
|
|
101
|
+
|
|
102
|
+
| Metric | Target | Where |
|
|
103
|
+
| --- | --- | --- |
|
|
104
|
+
| Estimated prompt (median) | ≤ 150k | `orch_uni_session_stats` |
|
|
105
|
+
| Compactions per slice | ≤ 1 | `orch_uni_session_stats` |
|
|
106
|
+
| Tools per turn (`toolCalls/requests`, reported as `toolsPerTurn`) | ≥ 2 | `orch_uni_session_stats` |
|
|
107
|
+
| Share of capped outputs among the tool's calls (`toolOutputsCappedByTool.shell / toolCallsByTool.shell`; likewise `bash`, `webfetch`) | < 5% | `orch_uni_session_stats` |
|
|
108
|
+
| Handoff at slice end | present | working tree |
|
|
109
|
+
|
|
110
|
+
`orch_uni_session_stats` reports the counters of the current plugin instance and
|
|
111
|
+
the configured limits; historical runs are counted from the session database on
|
|
112
|
+
the project side.
|
|
113
|
+
|
|
114
|
+
## 8. Guard
|
|
115
|
+
|
|
116
|
+
The guard is a set of plugin hooks plus a compaction-event subscription, not a
|
|
117
|
+
set of bans:
|
|
118
|
+
|
|
119
|
+
- `session.context` — estimates the prompt and adds one short reminder when it
|
|
120
|
+
crosses a budget level. The reminder is injected once at a level transition,
|
|
121
|
+
not on every request; within a level the text is stable, so the prefix cache is
|
|
122
|
+
not broken.
|
|
123
|
+
- compaction events — the `session.compaction` hook is registered on every host:
|
|
124
|
+
it counts attempts (`compactionAttempts`) and sends a tentative note to the
|
|
125
|
+
nearest request when the terminal event is delayed; the note names no
|
|
126
|
+
compaction number and claims no limit. Only `session.compaction.ended`
|
|
127
|
+
confirms a compaction (deduplicated by `event.id` or the durable position);
|
|
128
|
+
only it moves `compactions` and the limit. `session.compaction.failed`
|
|
129
|
+
increments `compactionFailures`, drops the tentative note and keeps a
|
|
130
|
+
confirmed note that is still waiting for delivery. When the event stream ends
|
|
131
|
+
or throws, the guard reports `eventLive: false`, `compactionSource: "hook"` and
|
|
132
|
+
`streamFailures: 1` (the per-session `errors` stays `0`); the hook is already
|
|
133
|
+
registered, so attempts keep counting, while no confirmed compaction or stop
|
|
134
|
+
signal is invented — no false "limit reached" on that path. `guard` exposes
|
|
135
|
+
the source, liveness and stream failures.
|
|
136
|
+
- `tool.execute.after` — caps the output of the configured tools and stores the
|
|
137
|
+
full text in a private per-process root
|
|
138
|
+
`os.tmpdir()/opencode-tool-output-<random>/` (POSIX 0700 for the root and the
|
|
139
|
+
session directory, 0600 for the file). Windows ignores mode (measured: 0666 for
|
|
140
|
+
the root, the session directory and the file): privacy there rests on the ACL of
|
|
141
|
+
`os.tmpdir()` and is not checked by code. The root is created
|
|
142
|
+
with `mkdtemp` at setup, so a pre-created directory or junction named
|
|
143
|
+
`opencode-tool-output` is never used; on POSIX the root must belong to the
|
|
144
|
+
current uid with no group/other access, otherwise the guard disables itself with
|
|
145
|
+
a warning (fail-open: the result is untouched). Session directories (`s-<uuid>`)
|
|
146
|
+
and files (`c-<uuid>.txt`, written with `wx`) are random, and each `realpath` is
|
|
147
|
+
proven inside the root; the root is removed when the plugin unloads (best
|
|
148
|
+
effort). A string with a lone surrogate is normalized once (U+FFFD), so the
|
|
149
|
+
file, the head and the counters describe the same string. Residual risk
|
|
150
|
+
accepted: a same-user attacker can still swap a session
|
|
151
|
+
directory between `realpath` and `writeFile` (Node has no `openat`). If `TMPDIR`
|
|
152
|
+
points inside the workspace, the files land there.
|
|
153
|
+
|
|
154
|
+
Configuration (defaults):
|
|
155
|
+
|
|
156
|
+
```jsonc
|
|
157
|
+
{
|
|
158
|
+
"sessionGuard": {
|
|
159
|
+
"enabled": true,
|
|
160
|
+
"promptTokensSoft": 150000,
|
|
161
|
+
"maxCompactions": 1
|
|
162
|
+
},
|
|
163
|
+
"toolOutputGuard": {
|
|
164
|
+
"enabled": true,
|
|
165
|
+
"maxChars": 20000,
|
|
166
|
+
"headChars": 4000,
|
|
167
|
+
"tools": ["shell", "bash", "webfetch"]
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`maxCompactions: 0` disables the compaction limit. `guard.toolOutputEnabled` is
|
|
173
|
+
`true` only when the private root exists **and** the effective tool list is not
|
|
174
|
+
empty; `guard.toolOutputTools` is that list (normalized, protected and kit names
|
|
175
|
+
removed) and `guard.unkeyedCalls` counts hook calls without a non-empty
|
|
176
|
+
`sessionID` (such input is dropped, not bucketed under `""`). An empty `tools`
|
|
177
|
+
list — explicit or after every entry was rejected — disables capping with a
|
|
178
|
+
warning. Accepted limits: a session directory `s-<uuid>` lives until the plugin
|
|
179
|
+
unloads — it is removed neither on `session.deleted` nor after a refused cap
|
|
180
|
+
(a readonly result, for example) and is released with the root; lone-surrogate
|
|
181
|
+
normalization relies on ES2024 `String.prototype.isWellFormed`/`toWellFormed`
|
|
182
|
+
(the minimum runtime version is not pinned in `engines`, so on an old runtime the
|
|
183
|
+
cap degrades fail-open: `errors` +1, the result is untouched).
|
|
184
|
+
In the counters, `toolCalls`
|
|
185
|
+
and the per-tool counters include nested Code Mode calls, and `outputCharsSaved`
|
|
186
|
+
is an upper estimate in characters (the injected stub is subtracted).
|
|
187
|
+
`toolCallsByTool` and `toolOutputsCappedByTool` split calls and capped outputs by
|
|
188
|
+
the normalized tool name (namespace stripped, lower-case). Duplicate compaction
|
|
189
|
+
events are counted once only when the host supplies `event.id` or a durable
|
|
190
|
+
position; without either identity a re-delivered event may be counted again
|
|
191
|
+
(fail-open in the direction of counting).
|
|
192
|
+
|
|
193
|
+
The guard is fail-open: a hook error neither breaks the request nor corrupts the
|
|
194
|
+
tool result. It writes nothing to the working tree (except when `TMPDIR` points
|
|
195
|
+
inside the workspace — see above) and blocks nothing — it only reminds and caps
|
|
196
|
+
output. Either part can be disabled with its `enabled` flag.
|
|
197
|
+
|
|
198
|
+
## Handoff template
|
|
199
|
+
|
|
200
|
+
```md
|
|
201
|
+
# Handoff: <slice> — <YYYY-MM-DD>
|
|
202
|
+
|
|
203
|
+
<!-- File: docs/planning/handoff-<YYYY-MM-DD>.md; use the project root when the directory is absent. -->
|
|
204
|
+
|
|
205
|
+
## Goal and DoD
|
|
206
|
+
One sentence of the goal and a verifiable definition of done.
|
|
207
|
+
|
|
208
|
+
## Done
|
|
209
|
+
- What changed; what proves it (command, test, commit).
|
|
210
|
+
|
|
211
|
+
## Not done
|
|
212
|
+
- What remains and why (blocker, owner decision, next wave).
|
|
213
|
+
|
|
214
|
+
## Decisions
|
|
215
|
+
- Decision — reason; link to the document or commit.
|
|
216
|
+
|
|
217
|
+
## Risks and limits
|
|
218
|
+
- Accepted risk — by whom and until when.
|
|
219
|
+
|
|
220
|
+
## Next step
|
|
221
|
+
One concrete action for the next session.
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Slice checklist
|
|
225
|
+
|
|
226
|
+
- [ ] The slice is closed by DoD, not by "ran out of time".
|
|
227
|
+
- [ ] The handoff is written and references evidence.
|
|
228
|
+
- [ ] Decisions and risks are in files, not only in chat.
|
|
229
|
+
- [ ] The working tree is clean or explicitly described in the handoff.
|
|
230
|
+
- [ ] The next session starts from `state` and SPEC, not from a retelling.
|
package/docs/ru/root-runbook.md
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
- **Root (главный агент)** — не аудирует: формирует бриф, запускает субагентов, сводит отчёты; по «недоказуемым» — решение владельца: root оформляет вопрос, фиксирует риск и эскалирует. Ведёт реестр принятых рисков, вносит правки отдельными коммитами, решает «ship».
|
|
6
6
|
- **Субагенты** (read-only): `factcheck`, `empiric`, `invariant`, `arbiter`, (на воротах) `redteam`, `meta`.
|
|
7
7
|
|
|
8
|
+
Сессионная гигиена среза — границы, бюджет контекста (≤150k токенов, ≤1 компакция) и handoff — в `session-discipline.md`.
|
|
9
|
+
|
|
8
10
|
## Шаблон брифа `case-<id>.md` (общий для всех шагов)
|
|
9
11
|
|
|
10
12
|
```
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Гигиена сессии: как вести разработку
|
|
2
|
+
|
|
3
|
+
Документ задаёт протокол работы сессии: где её границы, какой бюджет контекста,
|
|
4
|
+
когда делегировать, как обращаться с выводом инструментов и что остаётся после
|
|
5
|
+
сессии. Он дополняет `context-efficiency.md` (бюджет файлов инструкций) и
|
|
6
|
+
`root-runbook.md` (аудиторский цикл): там — про артефакты и вердикты, здесь —
|
|
7
|
+
про то, как не сжечь контекст и не потерять решения по дороге.
|
|
8
|
+
|
|
9
|
+
## Зачем это нужно
|
|
10
|
+
|
|
11
|
+
- Контекст — расходуемый ресурс, а не память проекта. С ростом входа качество
|
|
12
|
+
падает даже на простых задачах (context rot); «влезло в окно» не значит
|
|
13
|
+
«модель это использует».
|
|
14
|
+
- Компакция — lossy-пересказ. После неё агент помнит не решения, а их краткое
|
|
15
|
+
изложение; повторная компакция пересказывает пересказ.
|
|
16
|
+
- Замер длинной сессии: 13 253 хода, 19 компакций, медиана промпта выросла с
|
|
17
|
+
314k до 780k токенов. Почти всё это — повторная пересылка истории, а не работа.
|
|
18
|
+
Это внутренний замер проекта-потребителя: артефакт в репозиторий не приложен,
|
|
19
|
+
из этого репозитория цифры не воспроизводятся.
|
|
20
|
+
|
|
21
|
+
Цель протокола — держать рабочее состояние маленьким, а решения — в файлах,
|
|
22
|
+
которые переживают сессию.
|
|
23
|
+
|
|
24
|
+
## 1. Сессия = срез
|
|
25
|
+
|
|
26
|
+
Сессия — это один срез с измеримым DoD, а не весь проект.
|
|
27
|
+
|
|
28
|
+
| Правило | Значение |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| Бюджет оценки промпта | ≤ 150k токенов |
|
|
31
|
+
| Компакций на срез | ≤ 1 |
|
|
32
|
+
| Стоп-сигнал | первая компакция |
|
|
33
|
+
| Обязательный выход | handoff + коммит |
|
|
34
|
+
|
|
35
|
+
- При первом признаке приближения к бюджету — не «дожимать», а закрывать срез:
|
|
36
|
+
handoff, коммит, новая сессия.
|
|
37
|
+
- После первой компакции срез закрывается в любом случае: суммаризация уже
|
|
38
|
+
потеряла детали, и дальнейшая работа идёт по пересказу.
|
|
39
|
+
- Резать работу по волнам, гейтам или пакетам правок; смена фазы (план →
|
|
40
|
+
реализация → приёмка) — всегда новая сессия.
|
|
41
|
+
|
|
42
|
+
## 2. Разделение ролей
|
|
43
|
+
|
|
44
|
+
- Разведка и проверка («найти», «понять», «воспроизвести») — субагентам: у них
|
|
45
|
+
чистый контекст, а в ответ возвращается 1–2k токенов выводов, а не дамп.
|
|
46
|
+
- Реализация — одному писателю на рабочее пространство: связанные решения должны
|
|
47
|
+
приниматься в одном контексте (иначе параллельные агенты расходятся в стиле и
|
|
48
|
+
контрактах).
|
|
49
|
+
- Решения, сведение и приёмка — root-оркестратору.
|
|
50
|
+
- Аудиторский цикл не меняется: re-audit — resume тех же сессий; короткие
|
|
51
|
+
аудиторские сессии бюджету не угрожают.
|
|
52
|
+
|
|
53
|
+
Практическое правило: делегируй **исследование** широко, **реализацию** — узко,
|
|
54
|
+
**интеграцию** держи централизованной.
|
|
55
|
+
|
|
56
|
+
## 3. Батчинг инструментов
|
|
57
|
+
|
|
58
|
+
Каждый ход — это полная пересылка промпта. Один инструмент за ход умножает
|
|
59
|
+
бюджет на число мелочей.
|
|
60
|
+
|
|
61
|
+
- ≥ 3 независимые команды за ход или один скрипт вместо пяти-десяти вызовов.
|
|
62
|
+
- Длинные команды WSL выноси в скрипт-обёртку: строка на 250 символов в каждом
|
|
63
|
+
ходе дороже, чем файл, который читается один раз.
|
|
64
|
+
- Для чтения файлов — `read` с `limit`/`offset`, а не `cat` целиком.
|
|
65
|
+
- Цель по метрике: ≥ 2 инструментов на ход.
|
|
66
|
+
|
|
67
|
+
## 4. Вывод инструментов
|
|
68
|
+
|
|
69
|
+
- Всё, что больше 20k символов, — в файл; в контекст идёт голова (4k) и путь.
|
|
70
|
+
Это делает guard автоматически для `shell`, `bash`, `webfetch`.
|
|
71
|
+
- Скриншоты и бинарные вложения — файлом и путём, не base64 в контексте.
|
|
72
|
+
- `execute` (Code Mode) guard не режет: ограничивай результат в самом скрипте —
|
|
73
|
+
возвращай сводку и путь, а не сырой JSON на сотни килобайт.
|
|
74
|
+
- `read`, `write`, `edit` и инструменты комплекта не урезаются: у них свой
|
|
75
|
+
контракт, и потеря содержимого ломает работу.
|
|
76
|
+
|
|
77
|
+
## 5. Состояние в файлах
|
|
78
|
+
|
|
79
|
+
- `handoff` — обязательный выход среза (шаблон ниже); путь по умолчанию —
|
|
80
|
+
`docs/planning/handoff-<YYYY-MM-DD>.md`, если каталога нет — корень проекта.
|
|
81
|
+
- `decisions` — решения и причины; одно решение — одна запись; путь по умолчанию —
|
|
82
|
+
`docs/planning/decisions-<YYYY-MM-DD>.md`.
|
|
83
|
+
- `state` — ветка, гейты, открытые риски, следующие три шага; обновляется перед
|
|
84
|
+
новой сессией; путь по умолчанию — `docs/planning/state.md`.
|
|
85
|
+
- Новая сессия начинается с `state` и целевого раздела SPEC, а не с пересказа
|
|
86
|
+
прошлой сессии.
|
|
87
|
+
|
|
88
|
+
## 6. Модели
|
|
89
|
+
|
|
90
|
+
- По умолчанию — дешёвая модель; дорогая — только на архитектурные решения,
|
|
91
|
+
ревью и разбор спорного.
|
|
92
|
+
- Правки документов, чтение и механические проверки не требуют дорогой модели.
|
|
93
|
+
- Замер: 223 хода дорогой модели — $63, 104 хода дешёвой — $0.09. Один
|
|
94
|
+
переключатель модели экономит больше, чем любая оптимизация промпта. Это
|
|
95
|
+
внутренний замер проекта-потребителя: артефакт в репозиторий не приложен, из
|
|
96
|
+
этого репозитория цифры не воспроизводятся.
|
|
97
|
+
|
|
98
|
+
## 7. Метрики
|
|
99
|
+
|
|
100
|
+
| Метрика | Цель | Где смотреть |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| Оценка промпта (медиана) | ≤ 150k | `orch_uni_session_stats` |
|
|
103
|
+
| Компакций на срез | ≤ 1 | `orch_uni_session_stats` |
|
|
104
|
+
| Инструментов на ход (`toolCalls/requests`, он же `toolsPerTurn`) | ≥ 2 | `orch_uni_session_stats` |
|
|
105
|
+
| Доля урезанных выводов среди вызовов инструмента (`toolOutputsCappedByTool.shell / toolCallsByTool.shell`; так же `bash`, `webfetch`) | < 5% | `orch_uni_session_stats` |
|
|
106
|
+
| Handoff в конце среза | есть | рабочее дерево |
|
|
107
|
+
|
|
108
|
+
`orch_uni_session_stats` отдаёт счётчики текущего инстанса плагина и настроенные
|
|
109
|
+
лимиты; исторические прогоны считаются по базе сессий на стороне проекта.
|
|
110
|
+
|
|
111
|
+
## 8. Guard
|
|
112
|
+
|
|
113
|
+
Guard — это хуки плагина и подписка на события компакции, а не запреты:
|
|
114
|
+
|
|
115
|
+
- `session.context` — оценивает промпт и добавляет одно короткое напоминание при
|
|
116
|
+
переходе на новый уровень бюджета. Напоминание инжектится один раз при
|
|
117
|
+
переходе уровня, а не в каждом запросе; внутри уровня текст стабилен, поэтому
|
|
118
|
+
префикс-кэш не рвётся.
|
|
119
|
+
- события компакции — хук `session.compaction` регистрируется всегда: он считает
|
|
120
|
+
попытки (`compactionAttempts`) и отдаёт tentative-напоминание ближайшему
|
|
121
|
+
запросу при задержке терминального события; напоминание не называет номер
|
|
122
|
+
компакции и не заявляет лимит. Подтверждает компакцию только
|
|
123
|
+
`session.compaction.ended` (дедуп по `event.id` или durable-позиции); он же
|
|
124
|
+
двигает `compactions` и лимит. `session.compaction.failed` увеличивает
|
|
125
|
+
`compactionFailures`, снимает tentative-note и сохраняет confirmed-note, ещё
|
|
126
|
+
ждущий доставки. Если поток событий завершился или упал, guard выставляет
|
|
127
|
+
`eventLive: false`, `compactionSource: "hook"` и `streamFailures: 1`
|
|
128
|
+
(per-session `errors` остаётся `0`), хук уже зарегистрирован и попытки
|
|
129
|
+
продолжают считаться; подтверждённых компакций и стопа на этом пути нет —
|
|
130
|
+
ложного «лимит исчерпан» не бывает. Источник, liveness и сбои потока видны в
|
|
131
|
+
`guard`.
|
|
132
|
+
- `tool.execute.after` — урезает вывод настроенных инструментов и складывает
|
|
133
|
+
полный текст в приватный корень на инстанс плагина
|
|
134
|
+
`os.tmpdir()/opencode-tool-output-<random>/` (POSIX: 0700 на корень и каталог
|
|
135
|
+
сессии, 0600 на файл). На Windows mode не действует (замер: 0666 у корня,
|
|
136
|
+
каталога сессии и файла): приватность там держится только на ACL каталога
|
|
137
|
+
`os.tmpdir()` и кодом не проверяется. Корень создаётся `mkdtemp` при
|
|
138
|
+
setup, поэтому заранее созданный каталог или junction `opencode-tool-output` не
|
|
139
|
+
используется; на POSIX корень обязан принадлежать текущему uid и не давать
|
|
140
|
+
доступа group/other, иначе guard отключается с warning (fail-open: результат не
|
|
141
|
+
трогается). Каталог сессии (`s-<uuid>`) и файл (`c-<uuid>.txt`, запись `wx`) —
|
|
142
|
+
случайные, их `realpath` проверяется внутри корня; при выгрузке плагина корень
|
|
143
|
+
удаляется best effort. Строка с lone surrogate нормализуется один раз (U+FFFD),
|
|
144
|
+
и файл, голова и счётчики описывают уже одну и ту же строку. Остаточный риск
|
|
145
|
+
принят: same-user атакующий всё ещё может
|
|
146
|
+
подменить каталог сессии между `realpath` и `writeFile` (в Node нет `openat`).
|
|
147
|
+
Если `TMPDIR` указывает внутрь воркспейса, файлы попадут туда.
|
|
148
|
+
|
|
149
|
+
Конфигурация (значения по умолчанию):
|
|
150
|
+
|
|
151
|
+
```jsonc
|
|
152
|
+
{
|
|
153
|
+
"sessionGuard": {
|
|
154
|
+
"enabled": true,
|
|
155
|
+
"promptTokensSoft": 150000,
|
|
156
|
+
"maxCompactions": 1
|
|
157
|
+
},
|
|
158
|
+
"toolOutputGuard": {
|
|
159
|
+
"enabled": true,
|
|
160
|
+
"maxChars": 20000,
|
|
161
|
+
"headChars": 4000,
|
|
162
|
+
"tools": ["shell", "bash", "webfetch"]
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`maxCompactions: 0` отключает лимит компакций. `guard.toolOutputEnabled` равен
|
|
168
|
+
`true` только когда приватный корень создан **и** эффективный список инструментов
|
|
169
|
+
непуст; `guard.toolOutputTools` — этот список (после нормализации и отбрасывания
|
|
170
|
+
защищённых и kit-имён), `guard.unkeyedCalls` — число вызовов хуков без непустого
|
|
171
|
+
`sessionID` (такой ввод отбрасывается, а не сливается в бакет `""`). Пустой
|
|
172
|
+
`tools` — явный или после отбрасывания всех имён — выключает кап с warning.
|
|
173
|
+
Принятые ограничения: каталог сессии `s-<uuid>` живёт до выгрузки плагина — он не
|
|
174
|
+
удаляется ни при `session.deleted`, ни после отброшенного капа (например, при
|
|
175
|
+
неизменяемом результате), а освобождается вместе с корнем; нормализация lone
|
|
176
|
+
surrogate опирается на ES2024 `String.prototype.isWellFormed`/`toWellFormed`
|
|
177
|
+
(минимальная версия рантайма в `engines` не зафиксирована — на старом рантайме кап
|
|
178
|
+
деградирует fail-open: `errors` +1, результат не тронут).
|
|
179
|
+
В счётчиках `toolCalls` и
|
|
180
|
+
per-tool счётчики включают вложенные вызовы Code Mode, а `outputCharsSaved` —
|
|
181
|
+
верхняя оценка в символах (внедрённая заглушка вычитается). `toolCallsByTool` и
|
|
182
|
+
`toolOutputsCappedByTool` разносят вызовы и урезанные выводы по нормализованному
|
|
183
|
+
имени инструмента (namespace снят, регистр нижний). Дубликаты событий компакции
|
|
184
|
+
считаются один раз только при наличии `event.id` или durable-позиции; без
|
|
185
|
+
identity повторная доставка может пересчитать событие (fail-open в сторону
|
|
186
|
+
счёта).
|
|
187
|
+
|
|
188
|
+
Guard fail-open: ошибка хука не ломает запрос и не портит результат инструмента.
|
|
189
|
+
Он не пишет в рабочее дерево (кроме случая `TMPDIR` внутри воркспейса — см.
|
|
190
|
+
выше) и не блокирует действия — только напоминает и урезает вывод. Отключить
|
|
191
|
+
можно любым из флагов `enabled`.
|
|
192
|
+
|
|
193
|
+
## Шаблон handoff
|
|
194
|
+
|
|
195
|
+
```md
|
|
196
|
+
# Handoff: <срез> — <YYYY-MM-DD>
|
|
197
|
+
|
|
198
|
+
<!-- Файл: docs/planning/handoff-<YYYY-MM-DD>.md; нет каталога — корень проекта. -->
|
|
199
|
+
|
|
200
|
+
## Цель и DoD
|
|
201
|
+
Одно предложение цели и проверяемый критерий готовности.
|
|
202
|
+
|
|
203
|
+
## Сделано
|
|
204
|
+
- Что изменено; чем подтверждено (команда, тест, коммит).
|
|
205
|
+
|
|
206
|
+
## Не сделано
|
|
207
|
+
- Что осталось и почему (блокер, решение владельца, следующая волна).
|
|
208
|
+
|
|
209
|
+
## Решения
|
|
210
|
+
- Решение — причина; ссылка на документ или коммит.
|
|
211
|
+
|
|
212
|
+
## Риски и ограничения
|
|
213
|
+
- Принятый риск — кем и до какого момента.
|
|
214
|
+
|
|
215
|
+
## Следующий шаг
|
|
216
|
+
Одно конкретное действие для следующей сессии.
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Чек-лист среза
|
|
220
|
+
|
|
221
|
+
- [ ] Срез закрыт по DoD, а не по «кончилось время».
|
|
222
|
+
- [ ] Handoff записан и ссылается на доказательства.
|
|
223
|
+
- [ ] Решения и риски зафиксированы в файлах, а не только в переписке.
|
|
224
|
+
- [ ] Рабочее дерево чистое или явно описано в handoff.
|
|
225
|
+
- [ ] Следующая сессия начинается с `state` и SPEC, а не с пересказа.
|
package/index.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* - The `audit-kit` reference is registered only if that name is free; it points
|
|
25
25
|
* at this package's `docs/` directory so sessions can read the process
|
|
26
26
|
* documents directly.
|
|
27
|
-
* - Slash commands (`/agents-md`, `/spec`) are registered through
|
|
27
|
+
* - Slash commands (`/agents-md`, `/spec`, `/handoff`) are registered through
|
|
28
28
|
* `ctx.command.transform`; their handlers inject a workflow prompt into the
|
|
29
29
|
* current session and never write files themselves.
|
|
30
30
|
*
|
|
@@ -53,6 +53,7 @@ import { fileURLToPath } from "node:url"
|
|
|
53
53
|
import type { Agent, Model, Plugin, Reference } from "@opencode/plugin"
|
|
54
54
|
import matter from "gray-matter"
|
|
55
55
|
import { registerKitCommands } from "./lib/commands"
|
|
56
|
+
import { createSessionBudget, readSessionBudgetOptions, safeJson } from "./lib/session-budget"
|
|
56
57
|
import { registerAuditTools } from "./lib/tools"
|
|
57
58
|
|
|
58
59
|
/** Stable plugin id: diagnostics, enable/disable selectors, storage scope. */
|
|
@@ -118,10 +119,12 @@ function localeCandidates(): string[] {
|
|
|
118
119
|
const REFERENCE_DESCRIPTIONS: Record<Locale, string> = {
|
|
119
120
|
ru:
|
|
120
121
|
"Процессные документы комплекта оркестратора (аудиторский цикл): call-mechanics.md, " +
|
|
121
|
-
"process-ready-loop.md, root-runbook.md, instruction-other-session.md, context-efficiency.md
|
|
122
|
+
"process-ready-loop.md, root-runbook.md, instruction-other-session.md, context-efficiency.md, " +
|
|
123
|
+
"session-discipline.md.",
|
|
122
124
|
en:
|
|
123
125
|
"Process documents of the orchestrator agent kit (audit loop): call-mechanics.md, " +
|
|
124
|
-
"process-ready-loop.md, root-runbook.md, instruction-other-session.md, context-efficiency.md
|
|
126
|
+
"process-ready-loop.md, root-runbook.md, instruction-other-session.md, context-efficiency.md, " +
|
|
127
|
+
"session-discipline.md.",
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
/** Frontmatter keys this loader understands; anything else is reported and ignored. */
|
|
@@ -353,7 +356,7 @@ function readLocaleOptions(options: unknown): Locale | undefined {
|
|
|
353
356
|
if (raw === undefined) return undefined
|
|
354
357
|
const locale = parseLocaleTag(raw)
|
|
355
358
|
if (!locale) {
|
|
356
|
-
warn(`options.locale: expected one of ${SUPPORTED_LOCALES.join("|")}, got ${
|
|
359
|
+
warn(`options.locale: expected one of ${SUPPORTED_LOCALES.join("|")}, got ${safeJson(raw)}, ignored`)
|
|
357
360
|
}
|
|
358
361
|
return locale
|
|
359
362
|
}
|
|
@@ -408,25 +411,38 @@ async function setup(ctx: Plugin.Context): Promise<Plugin.Cleanup | undefined> {
|
|
|
408
411
|
})
|
|
409
412
|
|
|
410
413
|
// Deterministic, read-only audit tools: `orch_uni_validate_report`,
|
|
411
|
-
// `orch_uni_context_lint`, `orch_uni_snapshot`, `orch_uni_sessions
|
|
412
|
-
// `lib/tools.ts` and README «Tools»). The
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
|
|
414
|
+
// `orch_uni_context_lint`, `orch_uni_snapshot`, `orch_uni_sessions`,
|
|
415
|
+
// `orch_uni_session_stats` (see `lib/tools.ts` and README «Tools»). The session
|
|
416
|
+
// budget registers the protocol hooks (context/compaction/tool output); the
|
|
417
|
+
// tools cleanup stops the background session-index subscription; the commands
|
|
418
|
+
// cleanup disposes the command transform registration. A failure after the
|
|
419
|
+
// tools are registered must abort that subscription too — otherwise a
|
|
420
|
+
// half-registered plugin leaks an event listener for the lifetime of the server.
|
|
421
|
+
const sessionBudget = await createSessionBudget(ctx, readSessionBudgetOptions(ctx.options, warn), locale, warn)
|
|
422
|
+
let toolsCleanup: () => void
|
|
423
|
+
try {
|
|
424
|
+
toolsCleanup = await registerAuditTools(ctx, { sessionBudget })
|
|
425
|
+
} catch (error) {
|
|
426
|
+
await sessionBudget.cleanup()
|
|
427
|
+
throw error
|
|
428
|
+
}
|
|
418
429
|
let commandsCleanup: Plugin.Cleanup
|
|
419
430
|
try {
|
|
420
431
|
commandsCleanup = await registerKitCommands(ctx, locale)
|
|
421
432
|
} catch (error) {
|
|
422
433
|
toolsCleanup()
|
|
434
|
+
await sessionBudget.cleanup()
|
|
423
435
|
throw error
|
|
424
436
|
}
|
|
425
437
|
return async () => {
|
|
426
438
|
try {
|
|
427
439
|
await commandsCleanup()
|
|
428
440
|
} finally {
|
|
429
|
-
|
|
441
|
+
try {
|
|
442
|
+
toolsCleanup()
|
|
443
|
+
} finally {
|
|
444
|
+
await sessionBudget.cleanup()
|
|
445
|
+
}
|
|
430
446
|
}
|
|
431
447
|
}
|
|
432
448
|
}
|