@chorus-aidlc/chorus-openclaw-plugin 0.4.0 → 0.5.3
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 +208 -278
- package/dist/commands.d.ts +5 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +147 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +57 -0
- package/dist/config.js.map +1 -0
- package/dist/event-router.d.ts +55 -0
- package/dist/event-router.d.ts.map +1 -0
- package/dist/event-router.js +157 -0
- package/dist/event-router.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +108 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-client.d.ts +37 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +137 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/mcp-registration.d.ts +25 -0
- package/dist/mcp-registration.d.ts.map +1 -0
- package/dist/mcp-registration.js +93 -0
- package/dist/mcp-registration.js.map +1 -0
- package/dist/sse-listener.d.ts +37 -0
- package/dist/sse-listener.d.ts.map +1 -0
- package/dist/sse-listener.js +152 -0
- package/dist/sse-listener.js.map +1 -0
- package/dist/wake.d.ts +67 -0
- package/dist/wake.d.ts.map +1 -0
- package/dist/wake.js +234 -0
- package/dist/wake.js.map +1 -0
- package/openclaw.plugin.json +13 -12
- package/package.json +23 -5
- package/skills/brainstorm/SKILL.md +163 -0
- package/skills/chorus/SKILL.md +114 -97
- package/skills/develop/SKILL.md +197 -52
- package/skills/idea/SKILL.md +136 -150
- package/skills/openspec-aware/SKILL.md +425 -0
- package/skills/proposal/SKILL.md +162 -153
- package/skills/proposal-reviewer/SKILL.md +118 -0
- package/skills/quick-dev/SKILL.md +34 -10
- package/skills/review/SKILL.md +109 -35
- package/skills/task-reviewer/SKILL.md +113 -0
- package/skills/yolo/SKILL.md +501 -0
- package/src/commands.ts +138 -71
- package/src/config.ts +23 -10
- package/src/event-router.ts +46 -54
- package/src/index.ts +56 -83
- package/src/mcp-client.ts +17 -0
- package/src/mcp-registration.ts +142 -0
- package/src/openclaw-sdk.d.ts +95 -0
- package/src/wake.ts +310 -0
- package/src/tools/admin-tools.ts +0 -126
- package/src/tools/common-tools.ts +0 -575
- package/src/tools/dev-tools.ts +0 -105
- package/src/tools/pm-tools.ts +0 -411
package/src/wake.ts
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wake mechanism for the Chorus plugin.
|
|
5
|
+
*
|
|
6
|
+
* Runs an agent turn IN-PROCESS with the wake text as the agent's prompt, via
|
|
7
|
+
* `api.runtime.agent.runEmbeddedAgent(...)` — the same primitive the cron
|
|
8
|
+
* service uses to "run the agent now with this prompt"
|
|
9
|
+
* (`../openclaw/src/cron/isolated-agent/run-executor.ts:266`).
|
|
10
|
+
*
|
|
11
|
+
* WHY NOT enqueueSystemEvent + heartbeat: that path was tried and does NOT
|
|
12
|
+
* deliver our text. `enqueueSystemEvent` only pushes onto the session queue,
|
|
13
|
+
* and the heartbeat prompt builder only renders exec-completion / cron events
|
|
14
|
+
* into a prompt (`../openclaw/src/infra/heartbeat-runner.ts:1240-1252`); a plain
|
|
15
|
+
* notification event is never injected, so the agent turned on the generic
|
|
16
|
+
* `[OpenClaw heartbeat poll]` prompt with no Chorus content. runEmbeddedAgent
|
|
17
|
+
* delivers the prompt directly and runs a real turn that can call the MCP tools.
|
|
18
|
+
*
|
|
19
|
+
* The runtime surface used here is declared on `PluginRuntimeCore.agent` in
|
|
20
|
+
* `../openclaw/src/plugins/runtime/types-core.ts:180-221`. The local SDK shim
|
|
21
|
+
* types `api.runtime` permissively, so we narrow the slice we use inline —
|
|
22
|
+
* mirroring `mcp-registration.ts` (api.runtime.config). Verified signatures:
|
|
23
|
+
* - runEmbeddedAgent(RunEmbeddedAgentParams) — required: sessionId,
|
|
24
|
+
* sessionFile, workspaceDir, prompt, timeoutMs, runId (params.ts:39+).
|
|
25
|
+
* - agent.session.getSessionEntry({ sessionKey, agentId }) → SessionEntry?
|
|
26
|
+
* (store.ts:210); SessionEntry has sessionId + optional sessionFile.
|
|
27
|
+
* - agent.session.resolveSessionFilePath(sessionId, { sessionFile }, { agentId })
|
|
28
|
+
* (paths.ts:267).
|
|
29
|
+
* - agent.resolveAgentWorkspaceDir(cfg, agentId) / resolveAgentDir(cfg, agentId)
|
|
30
|
+
* — positional (agent-scope-config.ts:170/195).
|
|
31
|
+
* - agent.resolveAgentTimeoutMs({ cfg }) → number (timeout.ts:15).
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const FALLBACK_DEFAULT_AGENT_ID = "main";
|
|
35
|
+
const DEFAULT_MAIN_KEY = "main";
|
|
36
|
+
|
|
37
|
+
/** Mirror of OpenClaw's `normalizeAgentId` lowercasing fallback. */
|
|
38
|
+
function normalizeAgentId(value: string | undefined | null): string {
|
|
39
|
+
const trimmed = (value ?? "").trim().toLowerCase();
|
|
40
|
+
if (!trimmed) return FALLBACK_DEFAULT_AGENT_ID;
|
|
41
|
+
// Keep it path-safe + shell-friendly (matches openclaw VALID_ID_RE behavior).
|
|
42
|
+
if (/^[a-z0-9][a-z0-9_-]{0,63}$/.test(trimmed)) return trimmed;
|
|
43
|
+
const collapsed = trimmed
|
|
44
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
45
|
+
.replace(/^-+/, "")
|
|
46
|
+
.replace(/-+$/, "")
|
|
47
|
+
.slice(0, 64);
|
|
48
|
+
return collapsed || FALLBACK_DEFAULT_AGENT_ID;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Mirror of OpenClaw's `normalizeMainKey`. */
|
|
52
|
+
function normalizeMainKey(value: string | undefined | null): string {
|
|
53
|
+
const trimmed = (value ?? "").trim().toLowerCase();
|
|
54
|
+
return trimmed || DEFAULT_MAIN_KEY;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the main agent session key from the host config.
|
|
59
|
+
*
|
|
60
|
+
* This faithfully replicates OpenClaw's `resolveMainSessionKey(cfg)`
|
|
61
|
+
* (`../openclaw/src/config/sessions/main-session.ts:14`): a background plugin
|
|
62
|
+
* has no inbound message route to derive a session key from, so the wake must
|
|
63
|
+
* target the main agent's session. We cannot import the OpenClaw internal here
|
|
64
|
+
* (the resolvable `openclaw` peer is an older build), so we re-derive the key
|
|
65
|
+
* from `api.config` (the live `OpenClawConfig` snapshot) using the same rules:
|
|
66
|
+
*
|
|
67
|
+
* - `session.scope === "global"` → the literal `"global"` queue
|
|
68
|
+
* - otherwise → `agent:<defaultAgentId>:<mainKey>`, where defaultAgentId is
|
|
69
|
+
* the agent flagged `default`, else the first listed agent, else "main".
|
|
70
|
+
*
|
|
71
|
+
* Returns `null` when no session key can be resolved. The design's no-session
|
|
72
|
+
* fallback (graceful drop) then applies — we never fabricate a session.
|
|
73
|
+
*/
|
|
74
|
+
export function resolveSessionKey(api: OpenClawPluginApi): string | null {
|
|
75
|
+
const cfg = api.config as
|
|
76
|
+
| {
|
|
77
|
+
session?: { scope?: string; mainKey?: string };
|
|
78
|
+
agents?: { list?: Array<{ id?: string; default?: boolean }> };
|
|
79
|
+
}
|
|
80
|
+
| undefined;
|
|
81
|
+
|
|
82
|
+
if (!cfg) return null;
|
|
83
|
+
|
|
84
|
+
if (cfg.session?.scope === "global") {
|
|
85
|
+
return "global";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const agents = Array.isArray(cfg.agents?.list) ? cfg.agents!.list! : [];
|
|
89
|
+
const defaultAgentId =
|
|
90
|
+
agents.find((agent) => agent?.default)?.id ?? agents[0]?.id ?? FALLBACK_DEFAULT_AGENT_ID;
|
|
91
|
+
|
|
92
|
+
const key = `agent:${normalizeAgentId(defaultAgentId)}:${normalizeMainKey(cfg.session?.mainKey)}`;
|
|
93
|
+
return key || null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve the default agent id (matches OpenClaw's `resolveDefaultAgentId`):
|
|
98
|
+
* the agent flagged `default`, else the first listed agent, else "main".
|
|
99
|
+
* runEmbeddedAgent and the session/workspace resolvers are all agent-scoped.
|
|
100
|
+
*/
|
|
101
|
+
export function resolveAgentId(api: OpenClawPluginApi): string {
|
|
102
|
+
const cfg = api.config as
|
|
103
|
+
| { agents?: { list?: Array<{ id?: string; default?: boolean }> } }
|
|
104
|
+
| undefined;
|
|
105
|
+
const agents = Array.isArray(cfg?.agents?.list) ? cfg!.agents!.list! : [];
|
|
106
|
+
const id = agents.find((a) => a?.default)?.id ?? agents[0]?.id ?? FALLBACK_DEFAULT_AGENT_ID;
|
|
107
|
+
return normalizeAgentId(id);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Resolve the configured default model into `{ provider, model }` so we can pass
|
|
112
|
+
* them explicitly to runEmbeddedAgent. WHY THIS IS REQUIRED: runEmbeddedAgent
|
|
113
|
+
* does NOT auto-resolve the model from config when `provider`/`model` are
|
|
114
|
+
* omitted — it falls back to the built-in `DEFAULT_MODEL` ("gpt-5.5",
|
|
115
|
+
* `../openclaw/src/agents/defaults.ts:4`), which fails with "Unknown model"
|
|
116
|
+
* unless that happens to be configured. The host's own resolver
|
|
117
|
+
* (`resolveConfiguredModelRef`, `../openclaw/src/agents/model-selection-shared.ts`)
|
|
118
|
+
* is NOT exposed to plugins, so we mirror its primary-ref read: `agents.defaults.model`
|
|
119
|
+
* is either a bare `"provider/model"` string or `{ primary: "provider/model" }`
|
|
120
|
+
* (`resolvePrimaryStringValue`), split on the first "/".
|
|
121
|
+
*
|
|
122
|
+
* Returns null when no default model is configured (then we omit the overrides
|
|
123
|
+
* and let the host fall back — same as before, but at least we tried).
|
|
124
|
+
*/
|
|
125
|
+
export function resolveModelRef(
|
|
126
|
+
api: OpenClawPluginApi,
|
|
127
|
+
): { provider: string; model: string } | null {
|
|
128
|
+
const model = (api.config as { agents?: { defaults?: { model?: unknown } } } | undefined)
|
|
129
|
+
?.agents?.defaults?.model;
|
|
130
|
+
const raw =
|
|
131
|
+
typeof model === "string"
|
|
132
|
+
? model
|
|
133
|
+
: model && typeof model === "object"
|
|
134
|
+
? (model as { primary?: unknown }).primary
|
|
135
|
+
: undefined;
|
|
136
|
+
const ref = typeof raw === "string" ? raw.trim() : "";
|
|
137
|
+
const slash = ref.indexOf("/");
|
|
138
|
+
if (slash <= 0 || slash >= ref.length - 1) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return { provider: ref.slice(0, slash), model: ref.slice(slash + 1) };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Narrowed view of the `api.runtime.agent` surface we use to run a turn. Only
|
|
146
|
+
* the members this plugin touches are typed; the host provides the full
|
|
147
|
+
* `PluginRuntimeCore.agent` at runtime (types-core.ts:180-221).
|
|
148
|
+
*/
|
|
149
|
+
interface SessionEntryLike {
|
|
150
|
+
sessionId: string;
|
|
151
|
+
sessionFile?: string;
|
|
152
|
+
}
|
|
153
|
+
interface RuntimeAgentSlice {
|
|
154
|
+
runEmbeddedAgent: (params: Record<string, unknown>) => Promise<unknown>;
|
|
155
|
+
resolveAgentDir: (cfg: unknown, agentId: string) => string;
|
|
156
|
+
resolveAgentWorkspaceDir: (cfg: unknown, agentId: string) => string;
|
|
157
|
+
resolveAgentTimeoutMs: (opts: { cfg?: unknown }) => number;
|
|
158
|
+
session: {
|
|
159
|
+
getSessionEntry: (options: { sessionKey: string; agentId?: string }) => SessionEntryLike | undefined;
|
|
160
|
+
resolveSessionFilePath: (
|
|
161
|
+
sessionId: string,
|
|
162
|
+
entry?: { sessionFile?: string },
|
|
163
|
+
opts?: { agentId?: string },
|
|
164
|
+
) => string;
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getRuntimeAgent(api: OpenClawPluginApi): RuntimeAgentSlice | null {
|
|
169
|
+
const agent = (api.runtime as { agent?: Partial<RuntimeAgentSlice> } | undefined)?.agent;
|
|
170
|
+
if (
|
|
171
|
+
!agent ||
|
|
172
|
+
typeof agent.runEmbeddedAgent !== "function" ||
|
|
173
|
+
typeof agent.resolveAgentWorkspaceDir !== "function" ||
|
|
174
|
+
typeof agent.resolveAgentTimeoutMs !== "function" ||
|
|
175
|
+
typeof agent.session?.getSessionEntry !== "function" ||
|
|
176
|
+
typeof agent.session?.resolveSessionFilePath !== "function"
|
|
177
|
+
) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
return agent as RuntimeAgentSlice;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Build a stable-ish unique id without Date.now()/Math.random() (some hosts
|
|
185
|
+
* sandbox those). A monotonic counter + the contextKey is enough for runId
|
|
186
|
+
* uniqueness within a process.
|
|
187
|
+
*/
|
|
188
|
+
let wakeCounter = 0;
|
|
189
|
+
function nextRunId(contextKey: string): string {
|
|
190
|
+
wakeCounter += 1;
|
|
191
|
+
return `chorus-wake-${wakeCounter}-${contextKey}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Build a `wake(message, contextKey)` callback bound to the host runtime.
|
|
196
|
+
*
|
|
197
|
+
* Each wake resolves the main agent's session + workspace and runs an agent
|
|
198
|
+
* turn IN-PROCESS with `message` as the prompt (via runEmbeddedAgent). The turn
|
|
199
|
+
* has the agent's normal tool set (including the `chorus__*` MCP tools), so the
|
|
200
|
+
* agent can read the entity, the comment thread, and post back to Chorus.
|
|
201
|
+
*
|
|
202
|
+
* Failure modes (never throw — the SSE service must stay alive):
|
|
203
|
+
* - no resolvable session key / agent runtime unavailable → log + drop;
|
|
204
|
+
* - no existing session entry → run with a fresh sessionId (the runner
|
|
205
|
+
* creates the transcript); the agent starts a new conversation;
|
|
206
|
+
* - runEmbeddedAgent rejects (e.g. a turn is already in flight) → log + drop
|
|
207
|
+
* (the next SSE event re-triggers).
|
|
208
|
+
*
|
|
209
|
+
* Runs are fire-and-forget (the SSE dispatch path is sync); we log completion.
|
|
210
|
+
*/
|
|
211
|
+
export function createWake(
|
|
212
|
+
api: OpenClawPluginApi,
|
|
213
|
+
logger: { info: (msg: string) => void; warn: (msg: string) => void; error: (msg: string) => void },
|
|
214
|
+
): (message: string, contextKey: string) => void {
|
|
215
|
+
return (message: string, contextKey: string) => {
|
|
216
|
+
const sessionKey = resolveSessionKey(api);
|
|
217
|
+
if (!sessionKey) {
|
|
218
|
+
logger.warn(
|
|
219
|
+
`[Chorus] Wake DROPPED — could not resolve a main agent session key (contextKey=${contextKey}). Event: ${message.slice(0, 100)}`,
|
|
220
|
+
);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const agent = getRuntimeAgent(api);
|
|
225
|
+
if (!agent) {
|
|
226
|
+
logger.warn(
|
|
227
|
+
`[Chorus] Wake DROPPED — api.runtime.agent.runEmbeddedAgent (or a required session helper) is unavailable on this host (contextKey=${contextKey}).`,
|
|
228
|
+
);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const cfg = api.config;
|
|
233
|
+
const agentId = resolveAgentId(api);
|
|
234
|
+
|
|
235
|
+
// Resolve the EXISTING main-agent session so the wake turn continues the
|
|
236
|
+
// real conversation (context + MCP tools). If none exists yet, fall back to
|
|
237
|
+
// a fresh session id — runEmbeddedAgent creates the transcript file.
|
|
238
|
+
let sessionId: string;
|
|
239
|
+
let sessionFile: string;
|
|
240
|
+
try {
|
|
241
|
+
const entry = agent.session.getSessionEntry({ sessionKey, agentId });
|
|
242
|
+
sessionId = entry?.sessionId ?? nextRunId(contextKey);
|
|
243
|
+
sessionFile = agent.session.resolveSessionFilePath(
|
|
244
|
+
sessionId,
|
|
245
|
+
entry?.sessionFile ? { sessionFile: entry.sessionFile } : undefined,
|
|
246
|
+
{ agentId },
|
|
247
|
+
);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
logger.warn(`[Chorus] Wake DROPPED — session resolution failed (contextKey=${contextKey}): ${err}`);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let workspaceDir: string;
|
|
254
|
+
let agentDir: string | undefined;
|
|
255
|
+
let timeoutMs: number;
|
|
256
|
+
try {
|
|
257
|
+
workspaceDir = agent.resolveAgentWorkspaceDir(cfg, agentId);
|
|
258
|
+
agentDir = agent.resolveAgentDir(cfg, agentId);
|
|
259
|
+
timeoutMs = agent.resolveAgentTimeoutMs({ cfg });
|
|
260
|
+
} catch (err) {
|
|
261
|
+
logger.warn(`[Chorus] Wake DROPPED — workspace/timeout resolution failed (contextKey=${contextKey}): ${err}`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Resolve the configured model; runEmbeddedAgent otherwise falls back to the
|
|
266
|
+
// built-in DEFAULT_MODEL ("gpt-5.5") and fails with "Unknown model".
|
|
267
|
+
const modelRef = resolveModelRef(api);
|
|
268
|
+
if (!modelRef) {
|
|
269
|
+
logger.warn(
|
|
270
|
+
`[Chorus] No agents.defaults.model configured — wake turn will use the host default model, which may be unavailable (contextKey=${contextKey}).`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const runId = nextRunId(contextKey);
|
|
275
|
+
logger.info(
|
|
276
|
+
`[Chorus] Waking agent via embedded run (sessionKey=${sessionKey}, model=${modelRef ? `${modelRef.provider}/${modelRef.model}` : "host-default"}, contextKey=${contextKey})`,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Fire-and-forget: the SSE dispatch path is synchronous, and a wake turn
|
|
280
|
+
// can take many seconds. We log completion/failure but never await here.
|
|
281
|
+
// `disableMessageTool: true` → headless turn; the agent acts via MCP tools
|
|
282
|
+
// (chorus_add_comment, etc.) rather than replying to a chat channel.
|
|
283
|
+
void Promise.resolve()
|
|
284
|
+
.then(() =>
|
|
285
|
+
agent.runEmbeddedAgent({
|
|
286
|
+
sessionId,
|
|
287
|
+
sessionKey,
|
|
288
|
+
agentId,
|
|
289
|
+
trigger: "manual",
|
|
290
|
+
sessionFile,
|
|
291
|
+
...(agentDir ? { agentDir } : {}),
|
|
292
|
+
workspaceDir,
|
|
293
|
+
config: cfg,
|
|
294
|
+
prompt: message,
|
|
295
|
+
timeoutMs,
|
|
296
|
+
runId,
|
|
297
|
+
disableMessageTool: true,
|
|
298
|
+
...(modelRef ? { provider: modelRef.provider, model: modelRef.model } : {}),
|
|
299
|
+
}),
|
|
300
|
+
)
|
|
301
|
+
.then(() =>
|
|
302
|
+
logger.info(`[Chorus] Wake turn completed (sessionKey=${sessionKey}, contextKey=${contextKey})`),
|
|
303
|
+
)
|
|
304
|
+
.catch((err) =>
|
|
305
|
+
logger.warn(
|
|
306
|
+
`[Chorus] Wake turn failed (sessionKey=${sessionKey}, contextKey=${contextKey}): ${err instanceof Error ? err.message : String(err)}`,
|
|
307
|
+
),
|
|
308
|
+
);
|
|
309
|
+
};
|
|
310
|
+
}
|
package/src/tools/admin-tools.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import type { ChorusMcpClient } from "../mcp-client.js";
|
|
2
|
-
|
|
3
|
-
function toolResult(result: unknown) {
|
|
4
|
-
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }], details: result };
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
-
export function registerAdminTools(api: any, mcpClient: ChorusMcpClient) {
|
|
9
|
-
api.registerTool({
|
|
10
|
-
name: "chorus_admin_create_project",
|
|
11
|
-
label: "Create Project",
|
|
12
|
-
description: "Create a new project. Call chorus_get_project_groups first to find the right groupUuid.",
|
|
13
|
-
parameters: {
|
|
14
|
-
type: "object",
|
|
15
|
-
properties: {
|
|
16
|
-
name: { type: "string", description: "Project name" },
|
|
17
|
-
description: { type: "string", description: "Project description" },
|
|
18
|
-
groupUuid: { type: "string", description: "Project group UUID (optional, use chorus_get_project_groups to list groups)" },
|
|
19
|
-
},
|
|
20
|
-
required: ["name"],
|
|
21
|
-
additionalProperties: false,
|
|
22
|
-
},
|
|
23
|
-
async execute(_id: string, { name, description, groupUuid }: { name: string; description?: string; groupUuid?: string }) {
|
|
24
|
-
const args: Record<string, unknown> = { name };
|
|
25
|
-
if (description) args.description = description;
|
|
26
|
-
if (groupUuid) args.groupUuid = groupUuid;
|
|
27
|
-
const result = await mcpClient.callTool("chorus_admin_create_project", args);
|
|
28
|
-
return toolResult(result);
|
|
29
|
-
},
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
api.registerTool({
|
|
33
|
-
name: "chorus_admin_create_project_group",
|
|
34
|
-
label: "Create Group",
|
|
35
|
-
description: "Create a new project group for organizing projects.",
|
|
36
|
-
parameters: {
|
|
37
|
-
type: "object",
|
|
38
|
-
properties: {
|
|
39
|
-
name: { type: "string", description: "Group name" },
|
|
40
|
-
description: { type: "string", description: "Group description" },
|
|
41
|
-
},
|
|
42
|
-
required: ["name"],
|
|
43
|
-
additionalProperties: false,
|
|
44
|
-
},
|
|
45
|
-
async execute(_id: string, { name, description }: { name: string; description?: string }) {
|
|
46
|
-
const args: Record<string, unknown> = { name };
|
|
47
|
-
if (description) args.description = description;
|
|
48
|
-
const result = await mcpClient.callTool("chorus_admin_create_project_group", args);
|
|
49
|
-
return toolResult(result);
|
|
50
|
-
},
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
api.registerTool({
|
|
54
|
-
name: "chorus_admin_approve_proposal",
|
|
55
|
-
label: "Approve Proposal",
|
|
56
|
-
description:
|
|
57
|
-
"Approve a Proposal (Admin exclusive). On approval, documentDrafts and taskDrafts are automatically materialized into real Document and Task entities — materialized Tasks can then be claimed and executed by agents. " +
|
|
58
|
-
"⚠️ This action is irreversible — unless there is a special reason, you MUST obtain explicit human approval before calling this tool.",
|
|
59
|
-
parameters: {
|
|
60
|
-
type: "object",
|
|
61
|
-
properties: {
|
|
62
|
-
proposalUuid: { type: "string", description: "Proposal UUID" },
|
|
63
|
-
reviewNote: { type: "string", description: "Optional review note" },
|
|
64
|
-
},
|
|
65
|
-
required: ["proposalUuid"],
|
|
66
|
-
additionalProperties: false,
|
|
67
|
-
},
|
|
68
|
-
async execute(_id: string, { proposalUuid, reviewNote }: { proposalUuid: string; reviewNote?: string }) {
|
|
69
|
-
const args: Record<string, unknown> = { proposalUuid };
|
|
70
|
-
if (reviewNote) args.reviewNote = reviewNote;
|
|
71
|
-
const result = await mcpClient.callTool("chorus_admin_approve_proposal", args);
|
|
72
|
-
return toolResult(result);
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
api.registerTool({
|
|
77
|
-
name: "chorus_admin_verify_task",
|
|
78
|
-
label: "Verify Task",
|
|
79
|
-
description:
|
|
80
|
-
"Verify a Task (to_verify → done, Admin exclusive). Marks a task as completed after verification. Downstream tasks that depend on this task will only be unblocked after it is verified. " +
|
|
81
|
-
"⚠️ This action is irreversible — unless there is a special reason, you MUST obtain explicit human approval before calling this tool.",
|
|
82
|
-
parameters: {
|
|
83
|
-
type: "object",
|
|
84
|
-
properties: {
|
|
85
|
-
taskUuid: { type: "string", description: "Task UUID" },
|
|
86
|
-
},
|
|
87
|
-
required: ["taskUuid"],
|
|
88
|
-
additionalProperties: false,
|
|
89
|
-
},
|
|
90
|
-
async execute(_id: string, { taskUuid }: { taskUuid: string }) {
|
|
91
|
-
const result = await mcpClient.callTool("chorus_admin_verify_task", { taskUuid });
|
|
92
|
-
return toolResult(result);
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
api.registerTool({
|
|
97
|
-
name: "chorus_mark_acceptance_criteria",
|
|
98
|
-
label: "Mark AC",
|
|
99
|
-
description: "Mark acceptance criteria as passed or failed (admin verification). Blocked criteria prevent task from being verified (to_verify -> done).",
|
|
100
|
-
parameters: {
|
|
101
|
-
type: "object",
|
|
102
|
-
properties: {
|
|
103
|
-
taskUuid: { type: "string", description: "Task UUID" },
|
|
104
|
-
criteria: {
|
|
105
|
-
type: "array",
|
|
106
|
-
description: "Array of { uuid, status: 'passed'|'failed', evidence?: string }",
|
|
107
|
-
items: {
|
|
108
|
-
type: "object",
|
|
109
|
-
properties: {
|
|
110
|
-
uuid: { type: "string", description: "AcceptanceCriterion UUID" },
|
|
111
|
-
status: { type: "string", description: "Verification result: passed | failed" },
|
|
112
|
-
evidence: { type: "string", description: "Optional evidence/notes" },
|
|
113
|
-
},
|
|
114
|
-
required: ["uuid", "status"],
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
required: ["taskUuid", "criteria"],
|
|
119
|
-
additionalProperties: false,
|
|
120
|
-
},
|
|
121
|
-
async execute(_id: string, { taskUuid, criteria }: { taskUuid: string; criteria: Array<{ uuid: string; status: string; evidence?: string }> }) {
|
|
122
|
-
const result = await mcpClient.callTool("chorus_mark_acceptance_criteria", { taskUuid, criteria });
|
|
123
|
-
return toolResult(result);
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
}
|