opencode-orchestrator-uni 0.3.0 → 0.4.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/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`) 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,7 @@ 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"
52
56
  import { registerAuditTools } from "./lib/tools"
53
57
 
54
58
  /** Stable plugin id: diagnostics, enable/disable selectors, storage scope. */
@@ -114,10 +118,10 @@ function localeCandidates(): string[] {
114
118
  const REFERENCE_DESCRIPTIONS: Record<Locale, string> = {
115
119
  ru:
116
120
  "Процессные документы комплекта оркестратора (аудиторский цикл): call-mechanics.md, " +
117
- "process-ready-loop.md, root-runbook.md, instruction-other-session.md.",
121
+ "process-ready-loop.md, root-runbook.md, instruction-other-session.md, context-efficiency.md.",
118
122
  en:
119
123
  "Process documents of the orchestrator agent kit (audit loop): call-mechanics.md, " +
120
- "process-ready-loop.md, root-runbook.md, instruction-other-session.md.",
124
+ "process-ready-loop.md, root-runbook.md, instruction-other-session.md, context-efficiency.md.",
121
125
  }
122
126
 
123
127
  /** Frontmatter keys this loader understands; anything else is reported and ignored. */
@@ -404,9 +408,27 @@ async function setup(ctx: Plugin.Context): Promise<Plugin.Cleanup | undefined> {
404
408
  })
405
409
 
406
410
  // Deterministic, read-only audit tools: `orch_uni_validate_report`,
407
- // `orch_uni_snapshot`, `orch_uni_sessions` (see `lib/tools.ts` and README «Tools»).
408
- // The returned cleanup only stops the background session-index subscription.
409
- return await registerAuditTools(ctx)
411
+ // `orch_uni_context_lint`, `orch_uni_snapshot`, `orch_uni_sessions` (see
412
+ // `lib/tools.ts` and README «Tools»). The tools cleanup only stops the
413
+ // background session-index subscription; the commands cleanup disposes the
414
+ // command transform registration. A failure after the tools are registered
415
+ // must abort that subscription too — otherwise a half-registered plugin leaks
416
+ // an event listener for the lifetime of the server.
417
+ const toolsCleanup = await registerAuditTools(ctx)
418
+ let commandsCleanup: Plugin.Cleanup
419
+ try {
420
+ commandsCleanup = await registerKitCommands(ctx, locale)
421
+ } catch (error) {
422
+ toolsCleanup()
423
+ throw error
424
+ }
425
+ return async () => {
426
+ try {
427
+ await commandsCleanup()
428
+ } finally {
429
+ toolsCleanup()
430
+ }
431
+ }
410
432
  }
411
433
 
412
434
  export default { id: PLUGIN_ID, setup } satisfies Plugin.Plugin
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Slash commands of the kit: `/agents-md` and `/spec`.
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`.
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)\b/i, "").trim()
16
+ }
17
+
18
+ const DESCRIPTIONS: Record<CommandLocale, { readonly agentsMd: string; readonly spec: string }> = {
19
+ ru: {
20
+ agentsMd: "Создать или проверить AGENTS.md по шаблону context-efficiency.md.",
21
+ spec: "Создать или проверить SPEC.md по шаблону context-efficiency.md.",
22
+ },
23
+ en: {
24
+ agentsMd: "Create or audit AGENTS.md using the context-efficiency.md template.",
25
+ spec: "Create or audit SPEC.md using the context-efficiency.md template.",
26
+ },
27
+ }
28
+
29
+ function agentsMdPrompt(locale: CommandLocale, goal: string): string {
30
+ if (locale === "ru") {
31
+ const audit = /(?<![\p{L}\p{N}_])(?:аудит|проверь|проверить)(?![\p{L}\p{N}_])/iu.test(goal)
32
+ return [
33
+ "[Команда /agents-md]",
34
+ `Цель: ${goal}.`,
35
+ "",
36
+ "Работай по документу context-efficiency.md из reference audit-kit.",
37
+ audit
38
+ ? "1. Это аудит: сначала вызови orch_uni_context_lint (если доступен), затем context-lint; покажи SGR и closing_list."
39
+ : "1. Это черновик: собери факты (стек, команды, границы, ловушки, never/ask, указатели); задай не более 8 вопросов.",
40
+ "2. Не выдумывай команды, пути и версии; всё непроверенное помечай.",
41
+ "3. Заполни шаблон AGENTS.md из context-efficiency.md; бюджет 150–200 строк.",
42
+ "4. Прогони context-lint, покажи diff и открытые вопросы.",
43
+ "5. Канонический AGENTS.md не меняй без подтверждения; правки — через fix-runner.",
44
+ ].join("\n")
45
+ }
46
+ const audit = /\b(?:audit|review|check)\b/i.test(goal)
47
+ return [
48
+ "[Command /agents-md]",
49
+ `Goal: ${goal}.`,
50
+ "",
51
+ "Follow context-efficiency.md from the audit-kit reference.",
52
+ audit
53
+ ? "1. This is an audit: run orch_uni_context_lint first (when available), then context-lint; show the SGR and closing_list."
54
+ : "1. This is a draft: collect facts (stack, commands, boundaries, traps, never/ask, pointers); ask at most 8 questions.",
55
+ "2. Do not invent commands, paths or versions; mark everything unverified.",
56
+ "3. Fill the AGENTS.md template from context-efficiency.md; keep the budget at 150–200 lines.",
57
+ "4. Run context-lint and show the diff plus open questions.",
58
+ "5. Do not change the canonical AGENTS.md without approval; apply fixes through fix-runner.",
59
+ ].join("\n")
60
+ }
61
+
62
+ function specPrompt(locale: CommandLocale, brief: string): string {
63
+ if (locale === "ru") {
64
+ return [
65
+ "[Команда /spec]",
66
+ `Фича: ${brief}.`,
67
+ "",
68
+ "Работай по документу context-efficiency.md из reference audit-kit.",
69
+ "1. Задай не более 5 уточняющих вопросов; non-goals, приоритеты и метрики утверждает владелец.",
70
+ "2. Заполни шаблон SPEC.md: контекст, цель, non-goals 3–7, сценарии, R1..Rn (EARS), AC с тестами, инварианты, контракты ссылками, NFR, SC, открытые вопросы, трассировка, DoD.",
71
+ "3. Прогони orch_uni_context_lint (если доступен) и context-lint; покажи diff и открытые вопросы.",
72
+ "4. Канонический SPEC.md не меняй без подтверждения; правки — через fix-runner.",
73
+ ].join("\n")
74
+ }
75
+ return [
76
+ "[Command /spec]",
77
+ `Feature: ${brief}.`,
78
+ "",
79
+ "Follow context-efficiency.md from the audit-kit reference.",
80
+ "1. Ask at most 5 clarifying questions; non-goals, priorities and success metrics are owned by the owner.",
81
+ "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.",
82
+ "3. Run orch_uni_context_lint (when available) and context-lint; show the diff and open questions.",
83
+ "4. Do not change the canonical SPEC.md without approval; apply fixes through fix-runner.",
84
+ ].join("\n")
85
+ }
86
+
87
+ /** Register `/agents-md` and `/spec`; the cleanup disposes the transform registration. */
88
+ export async function registerKitCommands(ctx: Plugin.Context, locale: CommandLocale): Promise<Plugin.Cleanup> {
89
+ // The command API is optional in a host build. A missing `list` was already
90
+ // tolerated; a missing `transform` used to throw out of `setup` and take every
91
+ // agent and tool with it, so it degrades to a warning plus a no-op cleanup.
92
+ const command = (ctx as Partial<Plugin.Context>).command
93
+ if (command === undefined || typeof command.transform !== "function") {
94
+ console.warn("orch-uni: this host has no command API; /agents-md and /spec are not registered")
95
+ return async () => {}
96
+ }
97
+ let existing = new Set<string>()
98
+ try {
99
+ const listed = await command.list()
100
+ existing = new Set(listed.data.map((item) => item.name))
101
+ } catch {
102
+ // No listing API: register; the host resolves a collision on its side.
103
+ }
104
+ const registration = await command.transform((editor) => {
105
+ if (!existing.has("agents-md")) {
106
+ editor.add({
107
+ name: "agents-md",
108
+ description: DESCRIPTIONS[locale].agentsMd,
109
+ execute: async (input) => {
110
+ await ctx.session.prompt({
111
+ sessionID: input.sessionID,
112
+ text: agentsMdPrompt(
113
+ locale,
114
+ commandArgs(input.prompt.text) || (locale === "ru" ? "текущий репозиторий" : "the current repository"),
115
+ ),
116
+ ...(input.prompt.files === undefined ? {} : { files: input.prompt.files }),
117
+ ...(input.prompt.agents === undefined ? {} : { agents: input.prompt.agents }),
118
+ ...(input.prompt.skills === undefined ? {} : { skills: input.prompt.skills }),
119
+ delivery: input.delivery,
120
+ })
121
+ },
122
+ })
123
+ }
124
+ if (!existing.has("spec")) {
125
+ editor.add({
126
+ name: "spec",
127
+ description: DESCRIPTIONS[locale].spec,
128
+ execute: async (input) => {
129
+ await ctx.session.prompt({
130
+ sessionID: input.sessionID,
131
+ text: specPrompt(
132
+ locale,
133
+ commandArgs(input.prompt.text) ||
134
+ (locale === "ru" ? "описание фичи не задано — спроси" : "no feature brief — ask for one"),
135
+ ),
136
+ ...(input.prompt.files === undefined ? {} : { files: input.prompt.files }),
137
+ ...(input.prompt.agents === undefined ? {} : { agents: input.prompt.agents }),
138
+ ...(input.prompt.skills === undefined ? {} : { skills: input.prompt.skills }),
139
+ delivery: input.delivery,
140
+ })
141
+ },
142
+ })
143
+ }
144
+ })
145
+ return async () => {
146
+ await registration.dispose()
147
+ }
148
+ }