@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.
Files changed (58) hide show
  1. package/README.md +208 -278
  2. package/dist/commands.d.ts +5 -0
  3. package/dist/commands.d.ts.map +1 -0
  4. package/dist/commands.js +147 -0
  5. package/dist/commands.js.map +1 -0
  6. package/dist/config.d.ts +38 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +57 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/event-router.d.ts +55 -0
  11. package/dist/event-router.d.ts.map +1 -0
  12. package/dist/event-router.js +157 -0
  13. package/dist/event-router.js.map +1 -0
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +108 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/mcp-client.d.ts +37 -0
  19. package/dist/mcp-client.d.ts.map +1 -0
  20. package/dist/mcp-client.js +137 -0
  21. package/dist/mcp-client.js.map +1 -0
  22. package/dist/mcp-registration.d.ts +25 -0
  23. package/dist/mcp-registration.d.ts.map +1 -0
  24. package/dist/mcp-registration.js +93 -0
  25. package/dist/mcp-registration.js.map +1 -0
  26. package/dist/sse-listener.d.ts +37 -0
  27. package/dist/sse-listener.d.ts.map +1 -0
  28. package/dist/sse-listener.js +152 -0
  29. package/dist/sse-listener.js.map +1 -0
  30. package/dist/wake.d.ts +67 -0
  31. package/dist/wake.d.ts.map +1 -0
  32. package/dist/wake.js +234 -0
  33. package/dist/wake.js.map +1 -0
  34. package/openclaw.plugin.json +13 -12
  35. package/package.json +23 -5
  36. package/skills/brainstorm/SKILL.md +163 -0
  37. package/skills/chorus/SKILL.md +114 -97
  38. package/skills/develop/SKILL.md +197 -52
  39. package/skills/idea/SKILL.md +136 -150
  40. package/skills/openspec-aware/SKILL.md +425 -0
  41. package/skills/proposal/SKILL.md +162 -153
  42. package/skills/proposal-reviewer/SKILL.md +118 -0
  43. package/skills/quick-dev/SKILL.md +34 -10
  44. package/skills/review/SKILL.md +109 -35
  45. package/skills/task-reviewer/SKILL.md +113 -0
  46. package/skills/yolo/SKILL.md +501 -0
  47. package/src/commands.ts +138 -71
  48. package/src/config.ts +23 -10
  49. package/src/event-router.ts +46 -54
  50. package/src/index.ts +56 -83
  51. package/src/mcp-client.ts +17 -0
  52. package/src/mcp-registration.ts +142 -0
  53. package/src/openclaw-sdk.d.ts +95 -0
  54. package/src/wake.ts +310 -0
  55. package/src/tools/admin-tools.ts +0 -126
  56. package/src/tools/common-tools.ts +0 -575
  57. package/src/tools/dev-tools.ts +0 -105
  58. 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
- interface CheckinResponse {
6
- checkinTime: string;
7
- agent: {
8
- uuid: string;
9
- name: string;
10
- roles: string[];
11
- persona: string | null;
12
- systemPrompt: string | null;
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
- project: { uuid: string; name: string };
17
+ proposals?: number;
18
+ tasks?: number;
32
19
  }
33
20
 
34
- interface AssignedTask {
21
+ interface TaskTrackerEntry {
35
22
  uuid: string;
36
23
  title: string;
37
24
  status: string;
38
25
  priority: string;
39
- project: { uuid: string; name: string };
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
- ideas: AssignedIdea[];
44
- tasks: AssignedTask[];
53
+ ideaTracker?: Record<string, IdeaTrackerProject>;
54
+ taskTracker?: Record<string, TaskTrackerProject>;
45
55
  }
46
56
 
47
- // ===== Formatting helpers =====
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", emoji: "🎵", description: "Platform overview, tools, setup, and workflow routing" },
51
- { name: "idea", emoji: "💡", description: "Claim ideas, run elaboration, prepare for proposal" },
52
- { name: "proposal", emoji: "📋", description: "Create proposals with document & task drafts, manage DAG" },
53
- { name: "develop", emoji: "🔨", description: "Claim tasks, report work, submit for verification" },
54
- { name: "quick-dev", emoji: "⚡", description: "Skip Idea→Proposal, create tasks directly" },
55
- { name: "review", emoji: "✅", description: "Approve/reject proposals, verify tasks, governance" },
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.emoji} ${s.name.padEnd(12)} ${s.description}`
90
+ (s) => ` ${skillInvocation(s.name).padEnd(nameWidth)} ${s.description}`
61
91
  );
62
- return `Chorus skills (${PLUGIN_SKILLS.length}):\n${lines.join("\n")}\n\nUse: /chorus:<skill-name> (e.g. /chorus:idea)`;
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
- `Assignments: ${checkin?.pending?.ideasCount ?? 0} ideas, ${checkin?.pending?.tasksCount ?? 0} tasks`,
69
- `Notifications: ${checkin?.notifications?.unreadCount ?? 0} unread`,
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(tasks: AssignedTask[] | undefined): string {
76
- if (!tasks?.length) {
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(ideas: AssignedIdea[] | undefined): string {
87
- if (!ideas?.length) {
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: any,
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
- async handler(ctx: { args: string }) {
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: `Failed to check in: ${err instanceof Error ? err.message : String(err)}` };
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?.tasks) };
203
+ return { text: formatTaskList(data?.taskTracker) };
137
204
  } catch (err) {
138
- return { text: `Failed to fetch tasks: ${err instanceof Error ? err.message : String(err)}` };
205
+ return { text: errorText("Failed to fetch tasks", err), isError: true };
139
206
  }
140
207
  }
141
208
 
142
- // /chorus skills
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?.ideas) };
216
+ return { text: formatIdeaList(data?.ideaTracker) };
155
217
  } catch (err) {
156
- return { text: `Failed to fetch ideas: ${err instanceof Error ? err.message : String(err)}` };
218
+ return { text: errorText("Failed to fetch ideas", err), isError: true };
157
219
  }
158
220
  }
159
221
 
160
- // Unknown subcommand
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.
@@ -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
- config: ChorusPluginConfig;
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 config: ChorusPluginConfig;
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.config = opts.config;
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
- await this.handleTaskAssigned(notification);
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 async handleTaskAssigned(n: NotificationDetail): Promise<void> {
156
+ private handleTaskAssigned(n: NotificationDetail): void {
150
157
  const mentionGuidance = this.buildMentionGuidance(n, "task");
151
158
 
152
- if (this.config.autoStart) {
153
- try {
154
- await this.mcpClient.callTool("chorus_claim_task", { taskUuid: n.entityUuid });
155
- this.logger.info(`Auto-claimed task ${n.entityUuid}`);
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.triggerAgent(
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
- { notificationUuid: n.uuid, action: "mentioned", entityUuid: n.entityUuid, projectUuid: n.projectUuid }
172
+ this.contextKeyFor("mentioned", n.entityUuid)
181
173
  );
182
174
  }
183
175
 
184
176
  private handleElaborationRequested(n: NotificationDetail): void {
185
- this.triggerAgent(
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
- { notificationUuid: n.uuid, action: "elaboration_requested", entityUuid: n.entityUuid, projectUuid: n.projectUuid }
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.triggerAgent(
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
- { notificationUuid: n.uuid, action: "proposal_rejected", entityUuid: n.entityUuid, projectUuid: n.projectUuid }
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.triggerAgent(
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
- { notificationUuid: n.uuid, action: "proposal_approved", entityUuid: n.entityUuid, projectUuid: n.projectUuid }
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.triggerAgent(
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
- { notificationUuid: n.uuid, action: "idea_claimed", entityUuid: n.entityUuid, projectUuid: n.projectUuid }
214
+ this.contextKeyFor("idea_claimed", n.entityUuid)
223
215
  );
224
216
  }
225
217
 
226
218
  private handleTaskVerified(n: NotificationDetail): void {
227
- this.triggerAgent(
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
- { notificationUuid: n.uuid, action: "task_verified", entityUuid: n.entityUuid, projectUuid: n.projectUuid }
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.triggerAgent(
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
- { notificationUuid: n.uuid, action: "task_reopened", entityUuid: n.entityUuid, projectUuid: n.projectUuid }
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.triggerAgent(
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
- { notificationUuid: n.uuid, action: "elaboration_answered", entityUuid: n.entityUuid, projectUuid: n.projectUuid }
246
+ this.contextKeyFor("elaboration_answered", n.entityUuid)
255
247
  );
256
248
  }
257
249
  }