opencode-orchestrator-uni 0.3.1 → 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 +239 -10
- package/README.ru.md +240 -10
- package/agents/en/context-lint.md +90 -0
- package/agents/en/orchestrator.md +10 -0
- package/agents/ru/context-lint.md +90 -0
- package/agents/ru/orchestrator.md +10 -0
- package/docs/en/context-efficiency.md +196 -0
- package/docs/en/root-runbook.md +2 -0
- package/docs/en/session-discipline.md +230 -0
- package/docs/ru/context-efficiency.md +196 -0
- package/docs/ru/root-runbook.md +2 -0
- package/docs/ru/session-discipline.md +225 -0
- package/index.ts +45 -7
- package/lib/commands.ts +204 -0
- package/lib/context-lint.ts +2482 -0
- package/lib/report-schema.ts +36 -0
- package/lib/session-budget.ts +1236 -0
- package/lib/tools.ts +276 -9
- package/package.json +1 -1
- package/schemas/context-lint-failure.v1.json +29 -0
- package/schemas/context-lint.v1.json +58 -0
package/index.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Registers the orchestrator agent kit — the `orchestrator` root agent with the
|
|
5
5
|
* built-in audit loop, the read-only audit subagents (`factcheck`,
|
|
6
6
|
* `empiric`, `invariant`, `arbiter`, `meta`,
|
|
7
|
-
* `redteam`), and the `fix-runner` writer subagent — plus the `audit-kit`
|
|
7
|
+
* `redteam`, `context-lint`), and the `fix-runner` writer subagent — plus the `audit-kit`
|
|
8
8
|
* document reference and the kit's deterministic `orch_uni_*` tools, straight from
|
|
9
9
|
* this TypeScript entry point.
|
|
10
10
|
*
|
|
@@ -24,6 +24,9 @@
|
|
|
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`, `/handoff`) are registered through
|
|
28
|
+
* `ctx.command.transform`; their handlers inject a workflow prompt into the
|
|
29
|
+
* current session and never write files themselves.
|
|
27
30
|
*
|
|
28
31
|
* Models are configuration, not definition: the agent files carry no `model`,
|
|
29
32
|
* and the plugin applies models from its `plugins` entry options —
|
|
@@ -49,6 +52,8 @@ import path from "node:path"
|
|
|
49
52
|
import { fileURLToPath } from "node:url"
|
|
50
53
|
import type { Agent, Model, Plugin, Reference } from "@opencode/plugin"
|
|
51
54
|
import matter from "gray-matter"
|
|
55
|
+
import { registerKitCommands } from "./lib/commands"
|
|
56
|
+
import { createSessionBudget, readSessionBudgetOptions, safeJson } from "./lib/session-budget"
|
|
52
57
|
import { registerAuditTools } from "./lib/tools"
|
|
53
58
|
|
|
54
59
|
/** Stable plugin id: diagnostics, enable/disable selectors, storage scope. */
|
|
@@ -114,10 +119,12 @@ function localeCandidates(): string[] {
|
|
|
114
119
|
const REFERENCE_DESCRIPTIONS: Record<Locale, string> = {
|
|
115
120
|
ru:
|
|
116
121
|
"Процессные документы комплекта оркестратора (аудиторский цикл): call-mechanics.md, " +
|
|
117
|
-
"process-ready-loop.md, root-runbook.md, instruction-other-session.md."
|
|
122
|
+
"process-ready-loop.md, root-runbook.md, instruction-other-session.md, context-efficiency.md, " +
|
|
123
|
+
"session-discipline.md.",
|
|
118
124
|
en:
|
|
119
125
|
"Process documents of the orchestrator agent kit (audit loop): call-mechanics.md, " +
|
|
120
|
-
"process-ready-loop.md, root-runbook.md, instruction-other-session.md."
|
|
126
|
+
"process-ready-loop.md, root-runbook.md, instruction-other-session.md, context-efficiency.md, " +
|
|
127
|
+
"session-discipline.md.",
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
/** Frontmatter keys this loader understands; anything else is reported and ignored. */
|
|
@@ -349,7 +356,7 @@ function readLocaleOptions(options: unknown): Locale | undefined {
|
|
|
349
356
|
if (raw === undefined) return undefined
|
|
350
357
|
const locale = parseLocaleTag(raw)
|
|
351
358
|
if (!locale) {
|
|
352
|
-
warn(`options.locale: expected one of ${SUPPORTED_LOCALES.join("|")}, got ${
|
|
359
|
+
warn(`options.locale: expected one of ${SUPPORTED_LOCALES.join("|")}, got ${safeJson(raw)}, ignored`)
|
|
353
360
|
}
|
|
354
361
|
return locale
|
|
355
362
|
}
|
|
@@ -404,9 +411,40 @@ async function setup(ctx: Plugin.Context): Promise<Plugin.Cleanup | undefined> {
|
|
|
404
411
|
})
|
|
405
412
|
|
|
406
413
|
// Deterministic, read-only audit tools: `orch_uni_validate_report`,
|
|
407
|
-
// `orch_uni_snapshot`, `orch_uni_sessions
|
|
408
|
-
//
|
|
409
|
-
|
|
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
|
+
}
|
|
429
|
+
let commandsCleanup: Plugin.Cleanup
|
|
430
|
+
try {
|
|
431
|
+
commandsCleanup = await registerKitCommands(ctx, locale)
|
|
432
|
+
} catch (error) {
|
|
433
|
+
toolsCleanup()
|
|
434
|
+
await sessionBudget.cleanup()
|
|
435
|
+
throw error
|
|
436
|
+
}
|
|
437
|
+
return async () => {
|
|
438
|
+
try {
|
|
439
|
+
await commandsCleanup()
|
|
440
|
+
} finally {
|
|
441
|
+
try {
|
|
442
|
+
toolsCleanup()
|
|
443
|
+
} finally {
|
|
444
|
+
await sessionBudget.cleanup()
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
410
448
|
}
|
|
411
449
|
|
|
412
450
|
export default { id: PLUGIN_ID, setup } satisfies Plugin.Plugin
|
package/lib/commands.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slash commands of the kit: `/agents-md`, `/spec` and `/handoff`.
|
|
3
|
+
*
|
|
4
|
+
* Commands are prompts, not writers: `execute` injects a short workflow prompt
|
|
5
|
+
* into the current session, and the orchestrator runs it with the usual
|
|
6
|
+
* evidence/approval rules. The detailed templates and checklists live in
|
|
7
|
+
* `docs/<locale>/context-efficiency.md` and `docs/<locale>/session-discipline.md`.
|
|
8
|
+
*/
|
|
9
|
+
import type { Plugin } from "@opencode/plugin"
|
|
10
|
+
|
|
11
|
+
type CommandLocale = "ru" | "en"
|
|
12
|
+
|
|
13
|
+
/** Arguments typed after the slash command, without a duplicated command token. */
|
|
14
|
+
function commandArgs(text: string): string {
|
|
15
|
+
return text.replace(/^\s*\/(?:agents-md|spec|handoff)\b/i, "").trim()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DESCRIPTIONS: Record<
|
|
19
|
+
CommandLocale,
|
|
20
|
+
{ readonly agentsMd: string; readonly spec: string; readonly handoff: string }
|
|
21
|
+
> = {
|
|
22
|
+
ru: {
|
|
23
|
+
agentsMd: "Создать или проверить AGENTS.md по шаблону context-efficiency.md.",
|
|
24
|
+
spec: "Создать или проверить SPEC.md по шаблону context-efficiency.md.",
|
|
25
|
+
handoff: "Сформировать handoff среза по шаблону session-discipline.md.",
|
|
26
|
+
},
|
|
27
|
+
en: {
|
|
28
|
+
agentsMd: "Create or audit AGENTS.md using the context-efficiency.md template.",
|
|
29
|
+
spec: "Create or audit SPEC.md using the context-efficiency.md template.",
|
|
30
|
+
handoff: "Write the slice handoff using the session-discipline.md template.",
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function agentsMdPrompt(locale: CommandLocale, goal: string): string {
|
|
35
|
+
if (locale === "ru") {
|
|
36
|
+
const audit = /(?<![\p{L}\p{N}_])(?:аудит|проверь|проверить)(?![\p{L}\p{N}_])/iu.test(goal)
|
|
37
|
+
return [
|
|
38
|
+
"[Команда /agents-md]",
|
|
39
|
+
`Цель: ${goal}.`,
|
|
40
|
+
"",
|
|
41
|
+
"Работай по документу context-efficiency.md из reference audit-kit.",
|
|
42
|
+
audit
|
|
43
|
+
? "1. Это аудит: сначала вызови orch_uni_context_lint (если доступен), затем context-lint; покажи SGR и closing_list."
|
|
44
|
+
: "1. Это черновик: собери факты (стек, команды, границы, ловушки, never/ask, указатели); задай не более 8 вопросов.",
|
|
45
|
+
"2. Не выдумывай команды, пути и версии; всё непроверенное помечай.",
|
|
46
|
+
"3. Заполни шаблон AGENTS.md из context-efficiency.md; бюджет 150–200 строк.",
|
|
47
|
+
"4. Прогони context-lint, покажи diff и открытые вопросы.",
|
|
48
|
+
"5. Канонический AGENTS.md не меняй без подтверждения; правки — через fix-runner.",
|
|
49
|
+
].join("\n")
|
|
50
|
+
}
|
|
51
|
+
const audit = /\b(?:audit|review|check)\b/i.test(goal)
|
|
52
|
+
return [
|
|
53
|
+
"[Command /agents-md]",
|
|
54
|
+
`Goal: ${goal}.`,
|
|
55
|
+
"",
|
|
56
|
+
"Follow context-efficiency.md from the audit-kit reference.",
|
|
57
|
+
audit
|
|
58
|
+
? "1. This is an audit: run orch_uni_context_lint first (when available), then context-lint; show the SGR and closing_list."
|
|
59
|
+
: "1. This is a draft: collect facts (stack, commands, boundaries, traps, never/ask, pointers); ask at most 8 questions.",
|
|
60
|
+
"2. Do not invent commands, paths or versions; mark everything unverified.",
|
|
61
|
+
"3. Fill the AGENTS.md template from context-efficiency.md; keep the budget at 150–200 lines.",
|
|
62
|
+
"4. Run context-lint and show the diff plus open questions.",
|
|
63
|
+
"5. Do not change the canonical AGENTS.md without approval; apply fixes through fix-runner.",
|
|
64
|
+
].join("\n")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function specPrompt(locale: CommandLocale, brief: string): string {
|
|
68
|
+
if (locale === "ru") {
|
|
69
|
+
return [
|
|
70
|
+
"[Команда /spec]",
|
|
71
|
+
`Фича: ${brief}.`,
|
|
72
|
+
"",
|
|
73
|
+
"Работай по документу context-efficiency.md из reference audit-kit.",
|
|
74
|
+
"1. Задай не более 5 уточняющих вопросов; non-goals, приоритеты и метрики утверждает владелец.",
|
|
75
|
+
"2. Заполни шаблон SPEC.md: контекст, цель, non-goals 3–7, сценарии, R1..Rn (EARS), AC с тестами, инварианты, контракты ссылками, NFR, SC, открытые вопросы, трассировка, DoD.",
|
|
76
|
+
"3. Прогони orch_uni_context_lint (если доступен) и context-lint; покажи diff и открытые вопросы.",
|
|
77
|
+
"4. Канонический SPEC.md не меняй без подтверждения; правки — через fix-runner.",
|
|
78
|
+
].join("\n")
|
|
79
|
+
}
|
|
80
|
+
return [
|
|
81
|
+
"[Command /spec]",
|
|
82
|
+
`Feature: ${brief}.`,
|
|
83
|
+
"",
|
|
84
|
+
"Follow context-efficiency.md from the audit-kit reference.",
|
|
85
|
+
"1. Ask at most 5 clarifying questions; non-goals, priorities and success metrics are owned by the owner.",
|
|
86
|
+
"2. Fill the SPEC.md template: context, goal, 3–7 non-goals, scenarios, R1..Rn (EARS), AC with tests, invariants, contract links, NFRs, success metrics, open questions, traceability, DoD.",
|
|
87
|
+
"3. Run orch_uni_context_lint (when available) and context-lint; show the diff and open questions.",
|
|
88
|
+
"4. Do not change the canonical SPEC.md without approval; apply fixes through fix-runner.",
|
|
89
|
+
].join("\n")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function handoffPrompt(locale: CommandLocale, goal: string): string {
|
|
93
|
+
if (locale === "ru") {
|
|
94
|
+
return [
|
|
95
|
+
"[Команда /handoff]",
|
|
96
|
+
`Срез: ${goal}.`,
|
|
97
|
+
"",
|
|
98
|
+
"Работай по документу session-discipline.md из reference audit-kit.",
|
|
99
|
+
"1. Собери факты только из текущей сессии и рабочего дерева: что сделано, чем подтверждено (команды, тесты, коммиты), что не сделано и почему.",
|
|
100
|
+
"2. Заполни шаблон handoff из session-discipline.md: цель и DoD, сделано, не сделано, решения и причины, риски/принятые ограничения, следующий шаг.",
|
|
101
|
+
"3. Запиши файл `docs/planning/handoff-<YYYY-MM-DD>.md` (нет каталога — корень проекта) инструментом `write`, не shell-heredoc.",
|
|
102
|
+
"4. Ничего не додумывай: неизвестное помечай «не проверено»; секреты и токены в handoff не переноси.",
|
|
103
|
+
"5. Покажи путь файла, краткий diff и открытые вопросы; канонические документы не меняй без подтверждения.",
|
|
104
|
+
"6. Если дерево грязное — предложи коммит по правилам проекта; не коммить без подтверждения владельца.",
|
|
105
|
+
].join("\n")
|
|
106
|
+
}
|
|
107
|
+
return [
|
|
108
|
+
"[Command /handoff]",
|
|
109
|
+
`Slice: ${goal}.`,
|
|
110
|
+
"",
|
|
111
|
+
"Follow session-discipline.md from the audit-kit reference.",
|
|
112
|
+
"1. Collect facts from the current session and the working tree only: what is done, what proves it (commands, tests, commits), what is not done and why.",
|
|
113
|
+
"2. Fill the handoff template from session-discipline.md: goal and DoD, done, not done, decisions and reasons, risks/accepted limits, next step.",
|
|
114
|
+
"3. Write `docs/planning/handoff-<YYYY-MM-DD>.md` (use the project root when the directory is absent) with the `write` tool, not a shell heredoc.",
|
|
115
|
+
"4. Do not invent facts: mark the unknown as `unverified`; never copy secrets or tokens into the handoff.",
|
|
116
|
+
"5. Show the file path, a short diff and open questions; do not change canonical documents without approval.",
|
|
117
|
+
"6. If the working tree is dirty, propose a commit per the project rules; do not commit without the owner's approval.",
|
|
118
|
+
].join("\n")
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Register `/agents-md`, `/spec` and `/handoff`; the cleanup disposes the transform registration. */
|
|
122
|
+
export async function registerKitCommands(ctx: Plugin.Context, locale: CommandLocale): Promise<Plugin.Cleanup> {
|
|
123
|
+
// The command API is optional in a host build. A missing `list` was already
|
|
124
|
+
// tolerated; a missing `transform` used to throw out of `setup` and take every
|
|
125
|
+
// agent and tool with it, so it degrades to a warning plus a no-op cleanup.
|
|
126
|
+
const command = (ctx as Partial<Plugin.Context>).command
|
|
127
|
+
if (command === undefined || typeof command.transform !== "function") {
|
|
128
|
+
console.warn("orch-uni: this host has no command API; /agents-md, /spec and /handoff are not registered")
|
|
129
|
+
return async () => {}
|
|
130
|
+
}
|
|
131
|
+
let existing = new Set<string>()
|
|
132
|
+
try {
|
|
133
|
+
const listed = await command.list()
|
|
134
|
+
existing = new Set(listed.data.map((item) => item.name))
|
|
135
|
+
} catch {
|
|
136
|
+
// No listing API: register; the host resolves a collision on its side.
|
|
137
|
+
}
|
|
138
|
+
const registration = await command.transform((editor) => {
|
|
139
|
+
if (!existing.has("agents-md")) {
|
|
140
|
+
editor.add({
|
|
141
|
+
name: "agents-md",
|
|
142
|
+
description: DESCRIPTIONS[locale].agentsMd,
|
|
143
|
+
execute: async (input) => {
|
|
144
|
+
await ctx.session.prompt({
|
|
145
|
+
sessionID: input.sessionID,
|
|
146
|
+
text: agentsMdPrompt(
|
|
147
|
+
locale,
|
|
148
|
+
commandArgs(input.prompt.text) || (locale === "ru" ? "текущий репозиторий" : "the current repository"),
|
|
149
|
+
),
|
|
150
|
+
...(input.prompt.files === undefined ? {} : { files: input.prompt.files }),
|
|
151
|
+
...(input.prompt.agents === undefined ? {} : { agents: input.prompt.agents }),
|
|
152
|
+
...(input.prompt.skills === undefined ? {} : { skills: input.prompt.skills }),
|
|
153
|
+
delivery: input.delivery,
|
|
154
|
+
})
|
|
155
|
+
},
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
if (!existing.has("spec")) {
|
|
159
|
+
editor.add({
|
|
160
|
+
name: "spec",
|
|
161
|
+
description: DESCRIPTIONS[locale].spec,
|
|
162
|
+
execute: async (input) => {
|
|
163
|
+
await ctx.session.prompt({
|
|
164
|
+
sessionID: input.sessionID,
|
|
165
|
+
text: specPrompt(
|
|
166
|
+
locale,
|
|
167
|
+
commandArgs(input.prompt.text) ||
|
|
168
|
+
(locale === "ru" ? "описание фичи не задано — спроси" : "no feature brief — ask for one"),
|
|
169
|
+
),
|
|
170
|
+
...(input.prompt.files === undefined ? {} : { files: input.prompt.files }),
|
|
171
|
+
...(input.prompt.agents === undefined ? {} : { agents: input.prompt.agents }),
|
|
172
|
+
...(input.prompt.skills === undefined ? {} : { skills: input.prompt.skills }),
|
|
173
|
+
delivery: input.delivery,
|
|
174
|
+
})
|
|
175
|
+
},
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
if (!existing.has("handoff")) {
|
|
179
|
+
editor.add({
|
|
180
|
+
name: "handoff",
|
|
181
|
+
description: DESCRIPTIONS[locale].handoff,
|
|
182
|
+
execute: async (input) => {
|
|
183
|
+
await ctx.session.prompt({
|
|
184
|
+
sessionID: input.sessionID,
|
|
185
|
+
text: handoffPrompt(
|
|
186
|
+
locale,
|
|
187
|
+
commandArgs(input.prompt.text) ||
|
|
188
|
+
(locale === "ru"
|
|
189
|
+
? "текущая сессия — опиши срез по фактам"
|
|
190
|
+
: "the current session — describe the slice from facts"),
|
|
191
|
+
),
|
|
192
|
+
...(input.prompt.files === undefined ? {} : { files: input.prompt.files }),
|
|
193
|
+
...(input.prompt.agents === undefined ? {} : { agents: input.prompt.agents }),
|
|
194
|
+
...(input.prompt.skills === undefined ? {} : { skills: input.prompt.skills }),
|
|
195
|
+
delivery: input.delivery,
|
|
196
|
+
})
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
return async () => {
|
|
202
|
+
await registration.dispose()
|
|
203
|
+
}
|
|
204
|
+
}
|