@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/commands.ts
CHANGED
|
@@ -1,97 +1,158 @@
|
|
|
1
1
|
import type { ChorusMcpClient } from "./mcp-client.js";
|
|
2
2
|
|
|
3
3
|
// ===== Response types from Chorus MCP tools =====
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
assignments: {
|
|
15
|
-
ideas: AssignedIdea[];
|
|
16
|
-
tasks: AssignedTask[];
|
|
17
|
-
};
|
|
18
|
-
pending: {
|
|
19
|
-
ideasCount: number;
|
|
20
|
-
tasksCount: number;
|
|
21
|
-
};
|
|
22
|
-
notifications: {
|
|
23
|
-
unreadCount: number;
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface AssignedIdea {
|
|
4
|
+
//
|
|
5
|
+
// These mirror the CURRENT (Chorus 0.7.2+) tool output shapes:
|
|
6
|
+
// - chorus_checkin → { checkinTime, agent, ideaTracker, notifications }
|
|
7
|
+
// - chorus_get_my_assignments → { ideaTracker, taskTracker }
|
|
8
|
+
// Both `ideaTracker` and `taskTracker` are Records keyed by project UUID, with
|
|
9
|
+
// the work items nested inside each project bucket. Every field is read
|
|
10
|
+
// defensively (optional chaining) so a missing/renamed field degrades to "0"
|
|
11
|
+
// or "none" rather than throwing.
|
|
12
|
+
|
|
13
|
+
interface IdeaTrackerEntry {
|
|
28
14
|
uuid: string;
|
|
29
15
|
title: string;
|
|
30
16
|
status: string;
|
|
31
|
-
|
|
17
|
+
proposals?: number;
|
|
18
|
+
tasks?: number;
|
|
32
19
|
}
|
|
33
20
|
|
|
34
|
-
interface
|
|
21
|
+
interface TaskTrackerEntry {
|
|
35
22
|
uuid: string;
|
|
36
23
|
title: string;
|
|
37
24
|
status: string;
|
|
38
25
|
priority: string;
|
|
39
|
-
|
|
26
|
+
ac?: { passed?: number; total?: number };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface IdeaTrackerProject {
|
|
30
|
+
name: string;
|
|
31
|
+
ideas?: IdeaTrackerEntry[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface TaskTrackerProject {
|
|
35
|
+
name: string;
|
|
36
|
+
tasks?: TaskTrackerEntry[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface CheckinResponse {
|
|
40
|
+
checkinTime?: string;
|
|
41
|
+
agent?: {
|
|
42
|
+
uuid?: string;
|
|
43
|
+
name?: string;
|
|
44
|
+
persona?: string | null;
|
|
45
|
+
};
|
|
46
|
+
ideaTracker?: Record<string, IdeaTrackerProject>;
|
|
47
|
+
notifications?: {
|
|
48
|
+
unread?: number;
|
|
49
|
+
};
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
interface AssignmentsResponse {
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
ideaTracker?: Record<string, IdeaTrackerProject>;
|
|
54
|
+
taskTracker?: Record<string, TaskTrackerProject>;
|
|
45
55
|
}
|
|
46
56
|
|
|
47
|
-
// =====
|
|
57
|
+
// ===== Skill catalog =====
|
|
58
|
+
//
|
|
59
|
+
// All 9 skills bundled with the Chorus OpenClaw plugin
|
|
60
|
+
// (packages/openclaw-plugin/skills/*/SKILL.md). The `name` here matches each
|
|
61
|
+
// skill's SKILL.md frontmatter `name`, which is exactly the slash command
|
|
62
|
+
// OpenClaw exposes (see invocation hint below).
|
|
48
63
|
|
|
49
64
|
const PLUGIN_SKILLS = [
|
|
50
|
-
{ name: "chorus",
|
|
51
|
-
{ name: "idea",
|
|
52
|
-
{ name: "
|
|
53
|
-
{ name: "
|
|
54
|
-
{ name: "
|
|
55
|
-
{ name: "
|
|
65
|
+
{ name: "chorus", description: "Platform overview, common tools, setup, and workflow routing" },
|
|
66
|
+
{ name: "idea", description: "Claim ideas, run elaboration rounds, prepare for proposal" },
|
|
67
|
+
{ name: "brainstorm", description: "Optional divergent-then-convergent dialogue for fuzzy ideas" },
|
|
68
|
+
{ name: "proposal", description: "Create proposals with document & task drafts, manage the dependency DAG" },
|
|
69
|
+
{ name: "develop", description: "Claim tasks, report work, manage sessions, run wave-based execution" },
|
|
70
|
+
{ name: "quick-dev", description: "Skip Idea→Proposal — create tasks directly, execute, verify" },
|
|
71
|
+
{ name: "review", description: "Approve/reject proposals, verify tasks, project governance" },
|
|
72
|
+
{ name: "yolo", description: "Full-auto AI-DLC pipeline — from prompt to done" },
|
|
73
|
+
{ name: "openspec-aware", description: "Opt-in OpenSpec authoring for PM workflows when the openspec CLI is present" },
|
|
56
74
|
] as const;
|
|
57
75
|
|
|
76
|
+
// ===== Formatting helpers =====
|
|
77
|
+
|
|
78
|
+
// OpenClaw invokes a skill via a BARE slash command of its SKILL.md `name`
|
|
79
|
+
// (verified against OpenClaw 2026.5.30 docs/tools/skills.md: "the skill's
|
|
80
|
+
// visible name, slash command ... come from SKILL.md frontmatter name ... a
|
|
81
|
+
// nested skill with name: research is still invoked as /research"). OpenClaw
|
|
82
|
+
// does NOT use Claude Code's `/chorus:<skill>` namespace form.
|
|
83
|
+
function skillInvocation(name: string): string {
|
|
84
|
+
return `/${name}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
58
87
|
function formatSkillsList(): string {
|
|
88
|
+
const nameWidth = Math.max(...PLUGIN_SKILLS.map((s) => skillInvocation(s.name).length));
|
|
59
89
|
const lines = PLUGIN_SKILLS.map(
|
|
60
|
-
(s) => ` ${s.
|
|
90
|
+
(s) => ` ${skillInvocation(s.name).padEnd(nameWidth)} ${s.description}`
|
|
61
91
|
);
|
|
62
|
-
return
|
|
92
|
+
return [
|
|
93
|
+
`Chorus skills (${PLUGIN_SKILLS.length}):`,
|
|
94
|
+
...lines,
|
|
95
|
+
"",
|
|
96
|
+
"Invoke a skill with its slash command, e.g. /develop or /idea.",
|
|
97
|
+
].join("\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Sum a count across every project bucket in a tracker Record.
|
|
101
|
+
function countTracker<T>(
|
|
102
|
+
tracker: Record<string, { ideas?: T[]; tasks?: T[] }> | undefined,
|
|
103
|
+
key: "ideas" | "tasks"
|
|
104
|
+
): number {
|
|
105
|
+
if (!tracker) return 0;
|
|
106
|
+
return Object.values(tracker).reduce((total, project) => {
|
|
107
|
+
const items = key === "ideas" ? project.ideas : project.tasks;
|
|
108
|
+
return total + (items?.length ?? 0);
|
|
109
|
+
}, 0);
|
|
63
110
|
}
|
|
64
111
|
|
|
65
112
|
function formatStatus(checkin: CheckinResponse, connectionStatus: string): string {
|
|
113
|
+
const ideaCount = countTracker(checkin?.ideaTracker, "ideas");
|
|
66
114
|
const lines: string[] = [
|
|
67
115
|
`Connection: ${connectionStatus}`,
|
|
68
|
-
`
|
|
69
|
-
`
|
|
116
|
+
`Agent: ${checkin?.agent?.name ?? "unknown"}`,
|
|
117
|
+
`Assigned ideas: ${ideaCount}`,
|
|
118
|
+
`Notifications: ${checkin?.notifications?.unread ?? 0} unread`,
|
|
70
119
|
`Skills: ${PLUGIN_SKILLS.map((s) => s.name).join(", ")}`,
|
|
71
120
|
];
|
|
72
121
|
return lines.join("\n");
|
|
73
122
|
}
|
|
74
123
|
|
|
75
|
-
function formatTaskList(
|
|
76
|
-
|
|
124
|
+
function formatTaskList(taskTracker: Record<string, TaskTrackerProject> | undefined): string {
|
|
125
|
+
const lines: string[] = [];
|
|
126
|
+
let total = 0;
|
|
127
|
+
for (const project of Object.values(taskTracker ?? {})) {
|
|
128
|
+
for (const t of project.tasks ?? []) {
|
|
129
|
+
total += 1;
|
|
130
|
+
const ac =
|
|
131
|
+
t.ac && typeof t.ac.total === "number" && t.ac.total > 0
|
|
132
|
+
? ` (AC ${t.ac.passed ?? 0}/${t.ac.total})`
|
|
133
|
+
: "";
|
|
134
|
+
lines.push(`[${t.status}] [${t.priority}] ${t.title} (${project.name})${ac}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (total === 0) {
|
|
77
138
|
return "No assigned tasks.";
|
|
78
139
|
}
|
|
79
|
-
|
|
80
|
-
const lines = tasks.map(
|
|
81
|
-
(t) => `[${t.status}] [${t.priority}] ${t.title} (${t.project.name})`
|
|
82
|
-
);
|
|
83
|
-
return `Assigned tasks (${tasks.length}):\n${lines.join("\n")}`;
|
|
140
|
+
return `Assigned tasks (${total}):\n${lines.join("\n")}`;
|
|
84
141
|
}
|
|
85
142
|
|
|
86
|
-
function formatIdeaList(
|
|
87
|
-
|
|
143
|
+
function formatIdeaList(ideaTracker: Record<string, IdeaTrackerProject> | undefined): string {
|
|
144
|
+
const lines: string[] = [];
|
|
145
|
+
let total = 0;
|
|
146
|
+
for (const project of Object.values(ideaTracker ?? {})) {
|
|
147
|
+
for (const i of project.ideas ?? []) {
|
|
148
|
+
total += 1;
|
|
149
|
+
lines.push(`[${i.status}] ${i.title} (${project.name})`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (total === 0) {
|
|
88
153
|
return "No assigned ideas.";
|
|
89
154
|
}
|
|
90
|
-
|
|
91
|
-
const lines = ideas.map(
|
|
92
|
-
(i) => `[${i.status}] ${i.title} (${i.project.name})`
|
|
93
|
-
);
|
|
94
|
-
return `Assigned ideas (${ideas.length}):\n${lines.join("\n")}`;
|
|
155
|
+
return `Assigned ideas (${total}):\n${lines.join("\n")}`;
|
|
95
156
|
}
|
|
96
157
|
|
|
97
158
|
const HELP_TEXT = [
|
|
@@ -103,61 +164,67 @@ const HELP_TEXT = [
|
|
|
103
164
|
" /chorus skills List available Chorus skills",
|
|
104
165
|
].join("\n");
|
|
105
166
|
|
|
167
|
+
function errorText(prefix: string, err: unknown): string {
|
|
168
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
169
|
+
return `${prefix}: ${detail}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
106
172
|
// ===== Registration =====
|
|
107
173
|
|
|
108
174
|
export function registerChorusCommands(
|
|
109
|
-
api:
|
|
175
|
+
api: { registerCommand: (command: unknown) => void },
|
|
110
176
|
mcpClient: ChorusMcpClient,
|
|
111
177
|
getStatus: () => string
|
|
112
178
|
): void {
|
|
113
179
|
api.registerCommand({
|
|
114
180
|
name: "chorus",
|
|
115
|
-
description: "Chorus plugin commands: status, tasks, ideas",
|
|
116
|
-
|
|
181
|
+
description: "Chorus plugin commands: status, tasks, ideas, skills",
|
|
182
|
+
acceptsArgs: true,
|
|
183
|
+
async handler(ctx: { args?: string }) {
|
|
117
184
|
const sub = (ctx.args ?? "").trim().toLowerCase();
|
|
118
185
|
|
|
119
|
-
// /chorus or /chorus status
|
|
186
|
+
// /chorus or /chorus status — connection + checkin summary via slim client.
|
|
120
187
|
if (!sub || sub === "status") {
|
|
121
188
|
try {
|
|
122
189
|
const checkin = (await mcpClient.callTool("chorus_checkin", {})) as CheckinResponse;
|
|
123
190
|
return { text: formatStatus(checkin, getStatus()) };
|
|
124
191
|
} catch (err) {
|
|
125
|
-
return { text:
|
|
192
|
+
return { text: errorText("Failed to check in", err), isError: true };
|
|
126
193
|
}
|
|
127
194
|
}
|
|
128
195
|
|
|
129
|
-
// /chorus tasks
|
|
196
|
+
// /chorus tasks — assigned tasks via chorus_get_my_assignments.
|
|
130
197
|
if (sub === "tasks") {
|
|
131
198
|
try {
|
|
132
199
|
const data = (await mcpClient.callTool(
|
|
133
200
|
"chorus_get_my_assignments",
|
|
134
201
|
{}
|
|
135
202
|
)) as AssignmentsResponse;
|
|
136
|
-
return { text: formatTaskList(data?.
|
|
203
|
+
return { text: formatTaskList(data?.taskTracker) };
|
|
137
204
|
} catch (err) {
|
|
138
|
-
return { text:
|
|
205
|
+
return { text: errorText("Failed to fetch tasks", err), isError: true };
|
|
139
206
|
}
|
|
140
207
|
}
|
|
141
208
|
|
|
142
|
-
// /chorus
|
|
143
|
-
if (sub === "skills") {
|
|
144
|
-
return { text: formatSkillsList() };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// /chorus ideas
|
|
209
|
+
// /chorus ideas — assigned ideas via chorus_get_my_assignments.
|
|
148
210
|
if (sub === "ideas") {
|
|
149
211
|
try {
|
|
150
212
|
const data = (await mcpClient.callTool(
|
|
151
213
|
"chorus_get_my_assignments",
|
|
152
214
|
{}
|
|
153
215
|
)) as AssignmentsResponse;
|
|
154
|
-
return { text: formatIdeaList(data?.
|
|
216
|
+
return { text: formatIdeaList(data?.ideaTracker) };
|
|
155
217
|
} catch (err) {
|
|
156
|
-
return { text:
|
|
218
|
+
return { text: errorText("Failed to fetch ideas", err), isError: true };
|
|
157
219
|
}
|
|
158
220
|
}
|
|
159
221
|
|
|
160
|
-
//
|
|
222
|
+
// /chorus skills — static catalog of all 9 bundled skills.
|
|
223
|
+
if (sub === "skills") {
|
|
224
|
+
return { text: formatSkillsList() };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Unknown subcommand → help.
|
|
161
228
|
return { text: HELP_TEXT };
|
|
162
229
|
},
|
|
163
230
|
});
|
package/src/config.ts
CHANGED
|
@@ -3,6 +3,15 @@ import { z } from "zod";
|
|
|
3
3
|
export const CONFIG_FILE_PATH = "~/.openclaw/openclaw.json";
|
|
4
4
|
export const CONFIG_KEY_PATH = "plugins.entries.chorus-openclaw-plugin.config";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* In-code zod schema for typing `api.pluginConfig`.
|
|
8
|
+
*
|
|
9
|
+
* The canonical config contract is the JSON-Schema `configSchema` in
|
|
10
|
+
* `openclaw.plugin.json`, which OpenClaw validates BEFORE plugin code runs.
|
|
11
|
+
* This zod schema exists only for in-code typing and friendly missing-field
|
|
12
|
+
* messages — its accepted property set (chorusUrl, apiKey) MUST stay identical
|
|
13
|
+
* to the manifest `configSchema.properties`.
|
|
14
|
+
*/
|
|
6
15
|
export const chorusConfigSchema = z.object({
|
|
7
16
|
chorusUrl: z
|
|
8
17
|
.string()
|
|
@@ -14,20 +23,24 @@ export const chorusConfigSchema = z.object({
|
|
|
14
23
|
.startsWith("cho_")
|
|
15
24
|
.optional()
|
|
16
25
|
.describe("Chorus API Key (cho_ prefix)"),
|
|
17
|
-
projectUuids: z
|
|
18
|
-
.array(z.string().uuid())
|
|
19
|
-
.optional()
|
|
20
|
-
.default([])
|
|
21
|
-
.describe("Project UUIDs to monitor. Empty = all projects"),
|
|
22
|
-
autoStart: z
|
|
23
|
-
.boolean()
|
|
24
|
-
.optional()
|
|
25
|
-
.default(true)
|
|
26
|
-
.describe("Auto-claim and start work on task_assigned events"),
|
|
27
26
|
});
|
|
28
27
|
|
|
29
28
|
export type ChorusPluginConfig = z.infer<typeof chorusConfigSchema>;
|
|
30
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Normalize a raw `api.pluginConfig` bag into a typed `ChorusPluginConfig`.
|
|
32
|
+
*
|
|
33
|
+
* The host has already validated the bag against the manifest JSON Schema, so
|
|
34
|
+
* this only fills defaults and narrows types; it does not re-validate shape.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveConfig(pluginConfig: Record<string, unknown> | undefined): ChorusPluginConfig {
|
|
37
|
+
const raw = pluginConfig ?? {};
|
|
38
|
+
return {
|
|
39
|
+
chorusUrl: (raw.chorusUrl as string | undefined) || undefined,
|
|
40
|
+
apiKey: (raw.apiKey as string | undefined) || undefined,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
/**
|
|
32
45
|
* Check required config fields and warn about missing ones.
|
|
33
46
|
* Returns true if all required fields are present, false otherwise.
|
package/src/event-router.ts
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import type { ChorusMcpClient } from "./mcp-client.js";
|
|
2
|
-
import type { ChorusPluginConfig } from "./config.js";
|
|
3
2
|
import type { SseNotificationEvent } from "./sse-listener.js";
|
|
4
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Wake callback injected by the entry. Runs an embedded agent turn on the main
|
|
6
|
+
* agent's session with `message` as the prompt (see `wake.ts` → createWake,
|
|
7
|
+
* which calls `api.runtime.agent.runEmbeddedAgent`).
|
|
8
|
+
*
|
|
9
|
+
* `contextKey` identifies the originating Chorus action+entity (e.g.
|
|
10
|
+
* `chorus:mentioned:<uuid>`); it is used for the run id / logging. The wake
|
|
11
|
+
* resolves the main agent session + model and DROPS (logs + returns) when it
|
|
12
|
+
* cannot run — it never throws, so the SSE service stays alive.
|
|
13
|
+
*/
|
|
14
|
+
export type ChorusWakeFn = (message: string, contextKey: string) => void;
|
|
15
|
+
|
|
5
16
|
export interface ChorusEventRouterOptions {
|
|
6
17
|
mcpClient: ChorusMcpClient;
|
|
7
|
-
|
|
8
|
-
triggerAgent: (message: string, metadata?: Record<string, unknown>) => void;
|
|
18
|
+
wake: ChorusWakeFn;
|
|
9
19
|
logger: { info: (msg: string) => void; warn: (msg: string) => void; error: (msg: string) => void };
|
|
10
20
|
}
|
|
11
21
|
|
|
@@ -28,17 +38,13 @@ interface NotificationDetail {
|
|
|
28
38
|
|
|
29
39
|
export class ChorusEventRouter {
|
|
30
40
|
private readonly mcpClient: ChorusMcpClient;
|
|
31
|
-
private readonly
|
|
32
|
-
private readonly triggerAgent: ChorusEventRouterOptions["triggerAgent"];
|
|
41
|
+
private readonly wake: ChorusWakeFn;
|
|
33
42
|
private readonly logger: ChorusEventRouterOptions["logger"];
|
|
34
|
-
private readonly projectFilter: Set<string>;
|
|
35
43
|
|
|
36
44
|
constructor(opts: ChorusEventRouterOptions) {
|
|
37
45
|
this.mcpClient = opts.mcpClient;
|
|
38
|
-
this.
|
|
39
|
-
this.triggerAgent = opts.triggerAgent;
|
|
46
|
+
this.wake = opts.wake;
|
|
40
47
|
this.logger = opts.logger;
|
|
41
|
-
this.projectFilter = new Set(opts.config.projectUuids ?? []);
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
/**
|
|
@@ -67,6 +73,15 @@ export class ChorusEventRouter {
|
|
|
67
73
|
// Internal
|
|
68
74
|
// ---------------------------------------------------------------------------
|
|
69
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Build the dedupe contextKey for a notification. Identical action+entity
|
|
78
|
+
* bursts collapse to the same key so OpenClaw's queue suppresses the
|
|
79
|
+
* duplicate wake.
|
|
80
|
+
*/
|
|
81
|
+
private contextKeyFor(action: string, entityUuid: string): string {
|
|
82
|
+
return `chorus:${action}:${entityUuid}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
70
85
|
private async fetchAndRoute(notificationUuid: string): Promise<void> {
|
|
71
86
|
// Fetch notification details via MCP — use autoMarkRead=false so we don't
|
|
72
87
|
// consume all unread notifications, and status=unread since we just received it
|
|
@@ -88,19 +103,11 @@ export class ChorusEventRouter {
|
|
|
88
103
|
return;
|
|
89
104
|
}
|
|
90
105
|
|
|
91
|
-
// Project filter: if projectUuids is configured, ignore events from other projects
|
|
92
|
-
if (this.projectFilter.size > 0 && !this.projectFilter.has(notification.projectUuid)) {
|
|
93
|
-
this.logger.info(
|
|
94
|
-
`Notification for project ${notification.projectUuid} filtered out`
|
|
95
|
-
);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
106
|
// Route based on action (which corresponds to notificationType)
|
|
100
107
|
try {
|
|
101
108
|
switch (notification.action) {
|
|
102
109
|
case "task_assigned":
|
|
103
|
-
|
|
110
|
+
this.handleTaskAssigned(notification);
|
|
104
111
|
break;
|
|
105
112
|
case "mentioned":
|
|
106
113
|
this.handleMentioned(notification);
|
|
@@ -146,57 +153,42 @@ export class ChorusEventRouter {
|
|
|
146
153
|
);
|
|
147
154
|
}
|
|
148
155
|
|
|
149
|
-
private
|
|
156
|
+
private handleTaskAssigned(n: NotificationDetail): void {
|
|
150
157
|
const mentionGuidance = this.buildMentionGuidance(n, "task");
|
|
151
158
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
} catch (err) {
|
|
157
|
-
this.logger.warn(`Failed to auto-claim task ${n.entityUuid}: ${err}`);
|
|
158
|
-
// Still trigger agent even if claim fails — let the agent handle it
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
this.triggerAgent(
|
|
162
|
-
`[Chorus] Task assigned: ${n.entityTitle}. Task UUID: ${n.entityUuid}, Project UUID: ${n.projectUuid}. Use chorus_get_task to see details and begin work.\n${mentionGuidance}`,
|
|
163
|
-
{ notificationUuid: n.uuid, action: "task_assigned", entityUuid: n.entityUuid, projectUuid: n.projectUuid }
|
|
164
|
-
);
|
|
165
|
-
} else {
|
|
166
|
-
this.triggerAgent(
|
|
167
|
-
`[Chorus] Task assigned: ${n.entityTitle}. Task UUID: ${n.entityUuid}, Project UUID: ${n.projectUuid}. Use chorus_get_task to review when ready.\n${mentionGuidance}`,
|
|
168
|
-
{ notificationUuid: n.uuid, action: "task_assigned", entityUuid: n.entityUuid, projectUuid: n.projectUuid }
|
|
169
|
-
);
|
|
170
|
-
}
|
|
159
|
+
this.wake(
|
|
160
|
+
`[Chorus] Task assigned: ${n.entityTitle}. Task UUID: ${n.entityUuid}, Project UUID: ${n.projectUuid}. Use chorus_get_task to review the task, then chorus_claim_task to start work.\n${mentionGuidance}`,
|
|
161
|
+
this.contextKeyFor("task_assigned", n.entityUuid)
|
|
162
|
+
);
|
|
171
163
|
}
|
|
172
164
|
|
|
173
165
|
private handleMentioned(n: NotificationDetail): void {
|
|
174
166
|
const mentionGuidance = this.buildMentionGuidance(n, n.entityType);
|
|
175
167
|
|
|
176
|
-
this.
|
|
168
|
+
this.wake(
|
|
177
169
|
`[Chorus] You were @mentioned in ${n.entityType} '${n.entityTitle}' (entityType: ${n.entityType}, entityUuid: ${n.entityUuid}, projectUuid: ${n.projectUuid}): ${n.message}\n` +
|
|
178
170
|
`Review the ${n.entityType} content and use chorus_get_comments (targetType: "${n.entityType}", targetUuid: "${n.entityUuid}") to see the full conversation, then respond.\n` +
|
|
179
171
|
mentionGuidance,
|
|
180
|
-
|
|
172
|
+
this.contextKeyFor("mentioned", n.entityUuid)
|
|
181
173
|
);
|
|
182
174
|
}
|
|
183
175
|
|
|
184
176
|
private handleElaborationRequested(n: NotificationDetail): void {
|
|
185
|
-
this.
|
|
177
|
+
this.wake(
|
|
186
178
|
`[Chorus] Elaboration requested for idea '${n.entityTitle}' (ideaUuid: ${n.entityUuid}, projectUuid: ${n.projectUuid}). Use chorus_get_elaboration to review questions.`,
|
|
187
|
-
|
|
179
|
+
this.contextKeyFor("elaboration_requested", n.entityUuid)
|
|
188
180
|
);
|
|
189
181
|
}
|
|
190
182
|
|
|
191
183
|
private handleProposalRejected(n: NotificationDetail): void {
|
|
192
184
|
const mentionGuidance = this.buildMentionGuidance(n, "proposal");
|
|
193
185
|
|
|
194
|
-
this.
|
|
186
|
+
this.wake(
|
|
195
187
|
`[Chorus] Proposal '${n.entityTitle}' was REJECTED (proposalUuid: ${n.entityUuid}, projectUuid: ${n.projectUuid}). Review note: "${n.message}". ` +
|
|
196
188
|
`Use chorus_get_proposal to review the proposal, then fix issues with chorus_update_task_draft / chorus_update_document_draft. ` +
|
|
197
189
|
`After fixing, call chorus_validate_proposal then chorus_submit_proposal to resubmit.\n` +
|
|
198
190
|
mentionGuidance,
|
|
199
|
-
|
|
191
|
+
this.contextKeyFor("proposal_rejected", n.entityUuid)
|
|
200
192
|
);
|
|
201
193
|
}
|
|
202
194
|
|
|
@@ -204,54 +196,54 @@ export class ChorusEventRouter {
|
|
|
204
196
|
const mentionGuidance = this.buildMentionGuidance(n, "proposal");
|
|
205
197
|
|
|
206
198
|
const reviewInfo = n.message.includes("Note: ") ? ` Review note: "${n.message.split("Note: ").pop()}"` : "";
|
|
207
|
-
this.
|
|
199
|
+
this.wake(
|
|
208
200
|
`[Chorus] Proposal '${n.entityTitle}' was APPROVED (proposalUuid: ${n.entityUuid}, projectUuid: ${n.projectUuid})!${reviewInfo} Documents and tasks have been created. ` +
|
|
209
201
|
`Use chorus_get_available_tasks with projectUuid: "${n.projectUuid}" to see the new tasks ready for work.\n` +
|
|
210
202
|
mentionGuidance,
|
|
211
|
-
|
|
203
|
+
this.contextKeyFor("proposal_approved", n.entityUuid)
|
|
212
204
|
);
|
|
213
205
|
}
|
|
214
206
|
|
|
215
207
|
private handleIdeaClaimed(n: NotificationDetail): void {
|
|
216
208
|
const mentionGuidance = this.buildMentionGuidance(n, "idea");
|
|
217
209
|
|
|
218
|
-
this.
|
|
210
|
+
this.wake(
|
|
219
211
|
`[Chorus] Idea '${n.entityTitle}' has been assigned to you (ideaUuid: ${n.entityUuid}, projectUuid: ${n.projectUuid}). ` +
|
|
220
212
|
`Use chorus_get_idea to review the idea, then chorus_claim_idea to start elaboration.\n` +
|
|
221
213
|
mentionGuidance,
|
|
222
|
-
|
|
214
|
+
this.contextKeyFor("idea_claimed", n.entityUuid)
|
|
223
215
|
);
|
|
224
216
|
}
|
|
225
217
|
|
|
226
218
|
private handleTaskVerified(n: NotificationDetail): void {
|
|
227
|
-
this.
|
|
219
|
+
this.wake(
|
|
228
220
|
`[Chorus] Task '${n.entityTitle}' has been verified and is now done (taskUuid: ${n.entityUuid}, projectUuid: ${n.projectUuid}). ` +
|
|
229
221
|
`Check if this unblocks other tasks: use chorus_get_unblocked_tasks with projectUuid "${n.projectUuid}" to find tasks that are now ready to start.`,
|
|
230
|
-
|
|
222
|
+
this.contextKeyFor("task_verified", n.entityUuid)
|
|
231
223
|
);
|
|
232
224
|
}
|
|
233
225
|
|
|
234
226
|
private handleTaskReopened(n: NotificationDetail): void {
|
|
235
227
|
const mentionGuidance = this.buildMentionGuidance(n, "task");
|
|
236
228
|
|
|
237
|
-
this.
|
|
229
|
+
this.wake(
|
|
238
230
|
`[Chorus] Task '${n.entityTitle}' has been reopened and needs rework (taskUuid: ${n.entityUuid}, projectUuid: ${n.projectUuid}). ` +
|
|
239
231
|
`Use chorus_get_task to review the task and chorus_get_comments to see verification feedback, then fix the issues.\n${mentionGuidance}`,
|
|
240
|
-
|
|
232
|
+
this.contextKeyFor("task_reopened", n.entityUuid)
|
|
241
233
|
);
|
|
242
234
|
}
|
|
243
235
|
|
|
244
236
|
private handleElaborationAnswered(n: NotificationDetail): void {
|
|
245
237
|
const mentionGuidance = this.buildMentionGuidance(n, "idea");
|
|
246
238
|
|
|
247
|
-
this.
|
|
239
|
+
this.wake(
|
|
248
240
|
`[Chorus] Elaboration answers submitted for idea '${n.entityTitle}' (ideaUuid: ${n.entityUuid}, projectUuid: ${n.projectUuid}). ` +
|
|
249
241
|
`Review the answers with chorus_get_elaboration, then either:\n` +
|
|
250
242
|
`- Call chorus_validate_elaboration with empty issues [] to resolve and proceed to proposal creation\n` +
|
|
251
243
|
`- Call chorus_validate_elaboration with issues + followUpQuestions for another round\n\n` +
|
|
252
244
|
`After reviewing, @mention the answerer to ask if they have any further questions before you proceed.\n` +
|
|
253
245
|
mentionGuidance,
|
|
254
|
-
|
|
246
|
+
this.contextKeyFor("elaboration_answered", n.entityUuid)
|
|
255
247
|
);
|
|
256
248
|
}
|
|
257
249
|
}
|