clementine-agent 1.0.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.
Files changed (190) hide show
  1. package/.env.example +44 -0
  2. package/LICENSE +21 -0
  3. package/README.md +795 -0
  4. package/dist/agent/agent-manager.d.ts +69 -0
  5. package/dist/agent/agent-manager.js +441 -0
  6. package/dist/agent/assistant.d.ts +225 -0
  7. package/dist/agent/assistant.js +3888 -0
  8. package/dist/agent/auto-update.d.ts +32 -0
  9. package/dist/agent/auto-update.js +186 -0
  10. package/dist/agent/daily-planner.d.ts +24 -0
  11. package/dist/agent/daily-planner.js +379 -0
  12. package/dist/agent/execution-advisor.d.ts +10 -0
  13. package/dist/agent/execution-advisor.js +272 -0
  14. package/dist/agent/hooks.d.ts +45 -0
  15. package/dist/agent/hooks.js +564 -0
  16. package/dist/agent/insight-engine.d.ts +66 -0
  17. package/dist/agent/insight-engine.js +225 -0
  18. package/dist/agent/intent-classifier.d.ts +48 -0
  19. package/dist/agent/intent-classifier.js +214 -0
  20. package/dist/agent/link-extractor.d.ts +19 -0
  21. package/dist/agent/link-extractor.js +90 -0
  22. package/dist/agent/mcp-bridge.d.ts +62 -0
  23. package/dist/agent/mcp-bridge.js +435 -0
  24. package/dist/agent/metacognition.d.ts +66 -0
  25. package/dist/agent/metacognition.js +221 -0
  26. package/dist/agent/orchestrator.d.ts +81 -0
  27. package/dist/agent/orchestrator.js +790 -0
  28. package/dist/agent/profiles.d.ts +22 -0
  29. package/dist/agent/profiles.js +91 -0
  30. package/dist/agent/prompt-cache.d.ts +24 -0
  31. package/dist/agent/prompt-cache.js +68 -0
  32. package/dist/agent/prompt-evolver.d.ts +28 -0
  33. package/dist/agent/prompt-evolver.js +279 -0
  34. package/dist/agent/role-scaffolds.d.ts +28 -0
  35. package/dist/agent/role-scaffolds.js +433 -0
  36. package/dist/agent/safe-restart.d.ts +41 -0
  37. package/dist/agent/safe-restart.js +150 -0
  38. package/dist/agent/self-improve.d.ts +66 -0
  39. package/dist/agent/self-improve.js +1706 -0
  40. package/dist/agent/session-event-log.d.ts +114 -0
  41. package/dist/agent/session-event-log.js +233 -0
  42. package/dist/agent/skill-extractor.d.ts +72 -0
  43. package/dist/agent/skill-extractor.js +435 -0
  44. package/dist/agent/source-mods.d.ts +61 -0
  45. package/dist/agent/source-mods.js +230 -0
  46. package/dist/agent/source-preflight.d.ts +25 -0
  47. package/dist/agent/source-preflight.js +100 -0
  48. package/dist/agent/stall-guard.d.ts +62 -0
  49. package/dist/agent/stall-guard.js +109 -0
  50. package/dist/agent/strategic-planner.d.ts +60 -0
  51. package/dist/agent/strategic-planner.js +352 -0
  52. package/dist/agent/team-bus.d.ts +89 -0
  53. package/dist/agent/team-bus.js +556 -0
  54. package/dist/agent/team-router.d.ts +26 -0
  55. package/dist/agent/team-router.js +37 -0
  56. package/dist/agent/tool-loop-detector.d.ts +59 -0
  57. package/dist/agent/tool-loop-detector.js +242 -0
  58. package/dist/agent/workflow-runner.d.ts +36 -0
  59. package/dist/agent/workflow-runner.js +317 -0
  60. package/dist/agent/workflow-variables.d.ts +16 -0
  61. package/dist/agent/workflow-variables.js +62 -0
  62. package/dist/channels/discord-agent-bot.d.ts +101 -0
  63. package/dist/channels/discord-agent-bot.js +881 -0
  64. package/dist/channels/discord-bot-manager.d.ts +80 -0
  65. package/dist/channels/discord-bot-manager.js +262 -0
  66. package/dist/channels/discord-utils.d.ts +51 -0
  67. package/dist/channels/discord-utils.js +293 -0
  68. package/dist/channels/discord.d.ts +12 -0
  69. package/dist/channels/discord.js +1832 -0
  70. package/dist/channels/slack-agent-bot.d.ts +73 -0
  71. package/dist/channels/slack-agent-bot.js +320 -0
  72. package/dist/channels/slack-bot-manager.d.ts +66 -0
  73. package/dist/channels/slack-bot-manager.js +236 -0
  74. package/dist/channels/slack-utils.d.ts +39 -0
  75. package/dist/channels/slack-utils.js +189 -0
  76. package/dist/channels/slack.d.ts +11 -0
  77. package/dist/channels/slack.js +196 -0
  78. package/dist/channels/telegram.d.ts +10 -0
  79. package/dist/channels/telegram.js +235 -0
  80. package/dist/channels/webhook.d.ts +9 -0
  81. package/dist/channels/webhook.js +78 -0
  82. package/dist/channels/whatsapp.d.ts +11 -0
  83. package/dist/channels/whatsapp.js +181 -0
  84. package/dist/cli/chat.d.ts +14 -0
  85. package/dist/cli/chat.js +220 -0
  86. package/dist/cli/cron.d.ts +17 -0
  87. package/dist/cli/cron.js +552 -0
  88. package/dist/cli/dashboard.d.ts +15 -0
  89. package/dist/cli/dashboard.js +17677 -0
  90. package/dist/cli/index.d.ts +3 -0
  91. package/dist/cli/index.js +2474 -0
  92. package/dist/cli/routes/delegations.d.ts +19 -0
  93. package/dist/cli/routes/delegations.js +154 -0
  94. package/dist/cli/routes/digest.d.ts +17 -0
  95. package/dist/cli/routes/digest.js +375 -0
  96. package/dist/cli/routes/goals.d.ts +14 -0
  97. package/dist/cli/routes/goals.js +258 -0
  98. package/dist/cli/routes/workflows.d.ts +18 -0
  99. package/dist/cli/routes/workflows.js +97 -0
  100. package/dist/cli/setup.d.ts +8 -0
  101. package/dist/cli/setup.js +619 -0
  102. package/dist/cli/tunnel.d.ts +35 -0
  103. package/dist/cli/tunnel.js +141 -0
  104. package/dist/config.d.ts +145 -0
  105. package/dist/config.js +278 -0
  106. package/dist/events/bus.d.ts +43 -0
  107. package/dist/events/bus.js +136 -0
  108. package/dist/gateway/cron-scheduler.d.ts +166 -0
  109. package/dist/gateway/cron-scheduler.js +1767 -0
  110. package/dist/gateway/delivery-queue.d.ts +30 -0
  111. package/dist/gateway/delivery-queue.js +110 -0
  112. package/dist/gateway/heartbeat-scheduler.d.ts +99 -0
  113. package/dist/gateway/heartbeat-scheduler.js +1298 -0
  114. package/dist/gateway/heartbeat.d.ts +3 -0
  115. package/dist/gateway/heartbeat.js +3 -0
  116. package/dist/gateway/lanes.d.ts +24 -0
  117. package/dist/gateway/lanes.js +76 -0
  118. package/dist/gateway/notifications.d.ts +29 -0
  119. package/dist/gateway/notifications.js +75 -0
  120. package/dist/gateway/router.d.ts +210 -0
  121. package/dist/gateway/router.js +1330 -0
  122. package/dist/index.d.ts +12 -0
  123. package/dist/index.js +1015 -0
  124. package/dist/memory/chunker.d.ts +28 -0
  125. package/dist/memory/chunker.js +226 -0
  126. package/dist/memory/consolidation.d.ts +44 -0
  127. package/dist/memory/consolidation.js +171 -0
  128. package/dist/memory/context-assembler.d.ts +50 -0
  129. package/dist/memory/context-assembler.js +149 -0
  130. package/dist/memory/embeddings.d.ts +38 -0
  131. package/dist/memory/embeddings.js +180 -0
  132. package/dist/memory/graph-store.d.ts +66 -0
  133. package/dist/memory/graph-store.js +613 -0
  134. package/dist/memory/mmr.d.ts +21 -0
  135. package/dist/memory/mmr.js +75 -0
  136. package/dist/memory/search.d.ts +26 -0
  137. package/dist/memory/search.js +67 -0
  138. package/dist/memory/store.d.ts +530 -0
  139. package/dist/memory/store.js +2022 -0
  140. package/dist/security/integrity.d.ts +24 -0
  141. package/dist/security/integrity.js +58 -0
  142. package/dist/security/patterns.d.ts +34 -0
  143. package/dist/security/patterns.js +110 -0
  144. package/dist/security/scanner.d.ts +32 -0
  145. package/dist/security/scanner.js +263 -0
  146. package/dist/tools/admin-tools.d.ts +12 -0
  147. package/dist/tools/admin-tools.js +1278 -0
  148. package/dist/tools/external-tools.d.ts +11 -0
  149. package/dist/tools/external-tools.js +1327 -0
  150. package/dist/tools/goal-tools.d.ts +9 -0
  151. package/dist/tools/goal-tools.js +159 -0
  152. package/dist/tools/mcp-server.d.ts +13 -0
  153. package/dist/tools/mcp-server.js +141 -0
  154. package/dist/tools/memory-tools.d.ts +10 -0
  155. package/dist/tools/memory-tools.js +568 -0
  156. package/dist/tools/session-tools.d.ts +6 -0
  157. package/dist/tools/session-tools.js +146 -0
  158. package/dist/tools/shared.d.ts +216 -0
  159. package/dist/tools/shared.js +340 -0
  160. package/dist/tools/team-tools.d.ts +6 -0
  161. package/dist/tools/team-tools.js +447 -0
  162. package/dist/tools/tool-meta.d.ts +34 -0
  163. package/dist/tools/tool-meta.js +133 -0
  164. package/dist/tools/vault-tools.d.ts +8 -0
  165. package/dist/tools/vault-tools.js +457 -0
  166. package/dist/types.d.ts +716 -0
  167. package/dist/types.js +16 -0
  168. package/dist/vault-migrations/0001-add-execution-framework.d.ts +10 -0
  169. package/dist/vault-migrations/0001-add-execution-framework.js +47 -0
  170. package/dist/vault-migrations/0002-add-agentic-communication.d.ts +12 -0
  171. package/dist/vault-migrations/0002-add-agentic-communication.js +79 -0
  172. package/dist/vault-migrations/0003-update-execution-pipeline-narration.d.ts +11 -0
  173. package/dist/vault-migrations/0003-update-execution-pipeline-narration.js +73 -0
  174. package/dist/vault-migrations/helpers.d.ts +14 -0
  175. package/dist/vault-migrations/helpers.js +44 -0
  176. package/dist/vault-migrations/runner.d.ts +14 -0
  177. package/dist/vault-migrations/runner.js +139 -0
  178. package/dist/vault-migrations/types.d.ts +42 -0
  179. package/dist/vault-migrations/types.js +9 -0
  180. package/install.sh +320 -0
  181. package/package.json +84 -0
  182. package/scripts/postinstall.js +125 -0
  183. package/vault/00-System/AGENTS.md +66 -0
  184. package/vault/00-System/CRON.md +71 -0
  185. package/vault/00-System/HEARTBEAT.md +58 -0
  186. package/vault/00-System/MEMORY.md +16 -0
  187. package/vault/00-System/SOUL.md +96 -0
  188. package/vault/05-Tasks/TASKS.md +19 -0
  189. package/vault/06-Templates/_Daily-Template.md +28 -0
  190. package/vault/06-Templates/_People-Template.md +22 -0
@@ -0,0 +1,556 @@
1
+ /**
2
+ * Clementine TypeScript — Inter-agent message bus.
3
+ *
4
+ * Enables async message passing between team agents via gateway.injectContext.
5
+ * Logs to JSONL and optionally mirrors to a Discord channel.
6
+ */
7
+ import { createHash, randomBytes } from 'node:crypto';
8
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+ import pino from 'pino';
12
+ const logger = pino({ name: 'clementine.team-bus' });
13
+ /** Max inter-agent message depth before rejection (anti-loop). */
14
+ const MAX_DEPTH = 3;
15
+ /** Minimum interval between same sender->recipient pair (ms). */
16
+ const COOLDOWN_MS = 60_000;
17
+ /** Window for content dedup — reject identical messages within this period (ms). */
18
+ const CONTENT_DEDUP_MS = 300_000; // 5 minutes
19
+ /** Max recent messages to keep in memory. */
20
+ const RECENT_BUFFER_SIZE = 500;
21
+ export class TeamBus {
22
+ gateway;
23
+ teamRouter;
24
+ commsChannelId;
25
+ logFile;
26
+ recentMessages = [];
27
+ /** "from:to" → last send timestamp (for cooldown). */
28
+ cooldowns = new Map();
29
+ /** "slug" → context shares sent today (resets at midnight). */
30
+ contextShareCounts = new Map();
31
+ /** "from:to:contentHash" → timestamp (for content dedup). */
32
+ contentHashes = new Map();
33
+ statusChangeListeners = [];
34
+ pendingRequests = new Map();
35
+ botManager;
36
+ slackBotManager;
37
+ constructor(gateway, teamRouter, options) {
38
+ this.gateway = gateway;
39
+ this.teamRouter = teamRouter;
40
+ this.commsChannelId = options.commsChannelId || undefined;
41
+ this.logFile = options.logFile;
42
+ this.botManager = options.botManager;
43
+ this.slackBotManager = options.slackBotManager;
44
+ // Ensure log directory exists
45
+ const dir = path.dirname(this.logFile);
46
+ if (!existsSync(dir)) {
47
+ mkdirSync(dir, { recursive: true });
48
+ }
49
+ }
50
+ /** Update the BotManager reference (called if set after construction). */
51
+ setBotManager(botManager) {
52
+ this.botManager = botManager;
53
+ }
54
+ /** Update the SlackBotManager reference (called if set after construction). */
55
+ setSlackBotManager(slackBotManager) {
56
+ this.slackBotManager = slackBotManager;
57
+ }
58
+ /**
59
+ * Resolve a session key for the target agent via BotManager or SlackBotManager.
60
+ */
61
+ resolveSessionKey(toSlug) {
62
+ // Try Discord first
63
+ if (this.botManager) {
64
+ const channelId = this.botManager.getChannelForAgent(toSlug);
65
+ if (channelId) {
66
+ const ownerId = this.botManager.getOwnerId();
67
+ return `discord:channel:${channelId}:${ownerId}`;
68
+ }
69
+ }
70
+ // Try Slack
71
+ if (this.slackBotManager) {
72
+ const channelId = this.slackBotManager.getChannelForAgent(toSlug);
73
+ if (channelId) {
74
+ const ownerId = this.slackBotManager.getOwnerId();
75
+ return `slack:channel:${channelId}:${toSlug}:${ownerId}`;
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+ /**
81
+ * Derive the agent slug that owns a given session key.
82
+ * Returns null for primary/owner sessions (no agent slug).
83
+ */
84
+ deriveSlugFromSession(sessionKey) {
85
+ const parts = sessionKey.split(':');
86
+ // Discord: "discord:channel:{channelId}:{ownerId}" or "discord:member:{channelId}:{userId}"
87
+ if (this.botManager && parts[0] === 'discord' && (parts[1] === 'channel' || parts[1] === 'member') && parts[2]) {
88
+ return this.botManager.getAgentForChannel(parts[2]) ?? null;
89
+ }
90
+ // Slack: "slack:channel:{channelId}:{slug}:{ownerId}" or "slack:channel:{channelId}:{ownerId}"
91
+ if (this.slackBotManager && parts[0] === 'slack' && parts[1] === 'channel' && parts[2]) {
92
+ return this.slackBotManager.getAgentForChannel(parts[2]) ?? null;
93
+ }
94
+ return null;
95
+ }
96
+ /** Agent A sends a direct message to Agent B (fire-and-forget with cooldown/dedup). */
97
+ async send(fromSlug, toSlug, content, depth = 0, sessionKey) {
98
+ // Verify fromSlug matches the session's actual agent identity
99
+ if (sessionKey) {
100
+ const expectedSlug = this.deriveSlugFromSession(sessionKey);
101
+ if (expectedSlug && expectedSlug !== fromSlug) {
102
+ throw new Error(`Identity mismatch: session belongs to '${expectedSlug}' but fromSlug is '${fromSlug}'`);
103
+ }
104
+ }
105
+ // Validate sender — team agents need canMessage permission, primary agent can message anyone
106
+ const fromProfile = this.teamRouter.listTeamAgents().find((a) => a.slug === fromSlug);
107
+ if (fromProfile) {
108
+ // Team agent: enforce canMessage permission
109
+ if (!fromProfile.team?.canMessage.includes(toSlug)) {
110
+ throw new Error(`Agent '${fromSlug}' is not authorized to message '${toSlug}'. ` +
111
+ `Allowed targets: ${fromProfile.team?.canMessage.join(', ') || 'none'}`);
112
+ }
113
+ }
114
+ // If fromProfile is null, sender is the primary agent (no agent.md) — allowed to message anyone
115
+ // Validate recipient exists
116
+ const toProfile = this.teamRouter.listTeamAgents().find((a) => a.slug === toSlug);
117
+ if (!toProfile) {
118
+ throw new Error(`Target agent '${toSlug}' is not a team agent`);
119
+ }
120
+ // Anti-loop: depth check
121
+ if (depth >= MAX_DEPTH) {
122
+ throw new Error(`Message depth limit reached (${MAX_DEPTH}). ` +
123
+ `Agents cannot chain more than ${MAX_DEPTH} messages deep.`);
124
+ }
125
+ // Anti-loop: cooldown check
126
+ const cooldownKey = `${fromSlug}:${toSlug}`;
127
+ const lastSend = this.cooldowns.get(cooldownKey) ?? 0;
128
+ const now = Date.now();
129
+ if (now - lastSend < COOLDOWN_MS) {
130
+ const waitSec = Math.ceil((COOLDOWN_MS - (now - lastSend)) / 1000);
131
+ throw new Error(`Cooldown active: ${fromSlug} -> ${toSlug}. Wait ${waitSec}s before sending again.`);
132
+ }
133
+ this.cooldowns.set(cooldownKey, now);
134
+ // Anti-loop: content dedup — reject identical messages within the dedup window
135
+ const contentHash = createHash('sha256').update(content.trim()).digest('hex').slice(0, 12);
136
+ const dedupKey = `${fromSlug}:${toSlug}:${contentHash}`;
137
+ const lastSame = this.contentHashes.get(dedupKey) ?? 0;
138
+ if (now - lastSame < CONTENT_DEDUP_MS) {
139
+ throw new Error(`Duplicate message: ${fromSlug} already sent this exact content to ${toSlug} recently. ` +
140
+ `Rephrase or wait before resending.`);
141
+ }
142
+ this.contentHashes.set(dedupKey, now);
143
+ // Prune old dedup entries periodically
144
+ if (this.contentHashes.size > 200) {
145
+ for (const [key, ts] of this.contentHashes) {
146
+ if (now - ts > CONTENT_DEDUP_MS)
147
+ this.contentHashes.delete(key);
148
+ }
149
+ }
150
+ // Create the message record
151
+ const message = {
152
+ id: randomBytes(4).toString('hex'),
153
+ fromAgent: fromSlug,
154
+ toAgent: toSlug,
155
+ content,
156
+ timestamp: new Date().toISOString(),
157
+ delivered: false,
158
+ depth,
159
+ };
160
+ await this.deliverMessage(message, fromSlug, toSlug);
161
+ return message;
162
+ }
163
+ /**
164
+ * Send a structured request and wait for the target agent's response.
165
+ * Bypasses cooldown — structured requests are explicitly tracked.
166
+ */
167
+ async request(fromSlug, toSlug, content, timeoutMs = 300_000, sessionKey) {
168
+ if (sessionKey) {
169
+ const expectedSlug = this.deriveSlugFromSession(sessionKey);
170
+ if (expectedSlug && expectedSlug !== fromSlug) {
171
+ throw new Error(`Identity mismatch: session belongs to '${expectedSlug}' but fromSlug is '${fromSlug}'`);
172
+ }
173
+ }
174
+ // Validate sender permissions (same as send — team agents need canMessage)
175
+ const fromProfile = this.teamRouter.listTeamAgents().find((a) => a.slug === fromSlug);
176
+ if (fromProfile) {
177
+ if (!fromProfile.team?.canMessage.includes(toSlug)) {
178
+ throw new Error(`Agent '${fromSlug}' is not authorized to message '${toSlug}'. ` +
179
+ `Allowed targets: ${fromProfile.team?.canMessage.join(', ') || 'none'}`);
180
+ }
181
+ }
182
+ const toProfile = this.teamRouter.listTeamAgents().find((a) => a.slug === toSlug);
183
+ if (!toProfile) {
184
+ throw new Error(`Target agent '${toSlug}' is not a team agent`);
185
+ }
186
+ const requestId = randomBytes(6).toString('hex');
187
+ const message = {
188
+ id: randomBytes(4).toString('hex'),
189
+ fromAgent: fromSlug,
190
+ toAgent: toSlug,
191
+ content,
192
+ timestamp: new Date().toISOString(),
193
+ delivered: false,
194
+ depth: 0,
195
+ protocol: 'request',
196
+ requestId,
197
+ expectedBy: new Date(Date.now() + timeoutMs).toISOString(),
198
+ };
199
+ return new Promise((resolve, reject) => {
200
+ const timer = setTimeout(() => {
201
+ this.pendingRequests.delete(requestId);
202
+ reject(new Error(`Request to ${toSlug} timed out after ${timeoutMs}ms`));
203
+ }, timeoutMs);
204
+ this.pendingRequests.set(requestId, { message, resolve, reject, timer });
205
+ this.deliverMessage(message, fromSlug, toSlug).catch(err => {
206
+ this.pendingRequests.delete(requestId);
207
+ clearTimeout(timer);
208
+ reject(err);
209
+ });
210
+ });
211
+ }
212
+ /** Deliver a response to a pending structured request. */
213
+ async respond(toRequestId, fromSlug, content, _sessionKey) {
214
+ const pending = this.pendingRequests.get(toRequestId);
215
+ if (!pending) {
216
+ throw new Error(`No pending request found with ID: ${toRequestId}`);
217
+ }
218
+ const responseMessage = {
219
+ id: randomBytes(4).toString('hex'),
220
+ fromAgent: fromSlug,
221
+ toAgent: pending.message.fromAgent,
222
+ content,
223
+ timestamp: new Date().toISOString(),
224
+ delivered: true,
225
+ depth: 0,
226
+ protocol: 'response',
227
+ replyTo: toRequestId,
228
+ };
229
+ try {
230
+ appendFileSync(this.logFile, JSON.stringify(responseMessage) + '\n');
231
+ }
232
+ catch { /* non-fatal */ }
233
+ clearTimeout(pending.timer);
234
+ this.pendingRequests.delete(toRequestId);
235
+ pending.resolve(responseMessage);
236
+ this.recentMessages.push(responseMessage);
237
+ if (this.recentMessages.length > RECENT_BUFFER_SIZE) {
238
+ this.recentMessages = this.recentMessages.slice(-RECENT_BUFFER_SIZE);
239
+ }
240
+ for (const cb of this.statusChangeListeners) {
241
+ try {
242
+ cb();
243
+ }
244
+ catch { /* ignore */ }
245
+ }
246
+ return responseMessage;
247
+ }
248
+ /** Broadcast a message to all team agents (optionally scoped to a goal). */
249
+ async broadcast(fromSlug, content, goalId, _sessionKey) {
250
+ const goalsDir = path.join(process.env.CLEMENTINE_HOME || path.join(os.homedir(), '.clementine'), 'goals');
251
+ let targetSlugs = [];
252
+ if (existsSync(goalsDir)) {
253
+ for (const f of readdirSync(goalsDir).filter(f => f.endsWith('.json'))) {
254
+ try {
255
+ const goal = JSON.parse(readFileSync(path.join(goalsDir, f), 'utf-8'));
256
+ if (goal.id === goalId && goal.owner && goal.owner !== fromSlug) {
257
+ targetSlugs.push(goal.owner);
258
+ }
259
+ }
260
+ catch {
261
+ continue;
262
+ }
263
+ }
264
+ }
265
+ const allAgents = this.teamRouter.listTeamAgents();
266
+ for (const agent of allAgents) {
267
+ if (agent.slug !== fromSlug && !targetSlugs.includes(agent.slug)) {
268
+ targetSlugs.push(agent.slug);
269
+ }
270
+ }
271
+ targetSlugs = [...new Set(targetSlugs)].filter(s => s !== fromSlug);
272
+ const results = [];
273
+ for (const toSlug of targetSlugs) {
274
+ try {
275
+ const msg = {
276
+ id: randomBytes(4).toString('hex'),
277
+ fromAgent: fromSlug,
278
+ toAgent: toSlug,
279
+ content: `[Broadcast re: goal ${goalId}] ${content}`,
280
+ timestamp: new Date().toISOString(),
281
+ delivered: false,
282
+ depth: 0,
283
+ protocol: 'broadcast',
284
+ };
285
+ await this.deliverMessage(msg, fromSlug, toSlug);
286
+ results.push(msg);
287
+ }
288
+ catch (err) {
289
+ logger.debug({ err, to: toSlug }, 'Broadcast delivery failed for agent');
290
+ }
291
+ }
292
+ return results;
293
+ }
294
+ /** Get pending structured requests addressed to an agent. */
295
+ getPendingRequests(agentSlug) {
296
+ const pending = [];
297
+ for (const [requestId, entry] of this.pendingRequests) {
298
+ if (entry.message.toAgent === agentSlug) {
299
+ pending.push({
300
+ requestId,
301
+ fromAgent: entry.message.fromAgent,
302
+ content: entry.message.content,
303
+ timestamp: entry.message.timestamp,
304
+ expectedBy: entry.message.expectedBy,
305
+ });
306
+ }
307
+ }
308
+ return pending;
309
+ }
310
+ /** Core delivery logic — sends a message via bot or session injection. */
311
+ async deliverMessage(message, fromSlug, toSlug) {
312
+ const fromProfile = this.teamRouter.listTeamAgents().find((a) => a.slug === fromSlug);
313
+ const toProfile = this.teamRouter.listTeamAgents().find((a) => a.slug === toSlug);
314
+ const senderName = fromProfile?.name ?? fromSlug;
315
+ // Try Discord bot delivery first
316
+ if (this.botManager?.hasBot(toSlug)) {
317
+ const botResponse = await this.botManager.deliverTeamMessage(toSlug, senderName, fromSlug, message.content);
318
+ if (botResponse !== null) {
319
+ message.delivered = true;
320
+ message.response = botResponse;
321
+ }
322
+ }
323
+ // Try Slack bot delivery
324
+ if (!message.delivered && this.slackBotManager?.hasBot(toSlug)) {
325
+ const slackResponse = await this.slackBotManager.deliverTeamMessage(toSlug, senderName, fromSlug, message.content);
326
+ if (slackResponse !== null) {
327
+ message.delivered = true;
328
+ message.response = slackResponse;
329
+ }
330
+ }
331
+ if (!message.delivered) {
332
+ const resolvedKey = this.resolveSessionKey(toSlug);
333
+ if (resolvedKey) {
334
+ this.gateway.setSessionProfile(resolvedKey, toSlug);
335
+ this.gateway.injectContext(resolvedKey, `[Team message from ${senderName} (${fromSlug}), depth=${message.depth}]`, message.content);
336
+ message.delivered = true;
337
+ }
338
+ else {
339
+ logger.warn({ toSlug }, 'No channel found for target agent — message queued for later delivery');
340
+ }
341
+ }
342
+ // Persist to JSONL log
343
+ try {
344
+ appendFileSync(this.logFile, JSON.stringify(message) + '\n');
345
+ }
346
+ catch (err) {
347
+ logger.warn({ err }, 'Failed to write team comms log');
348
+ }
349
+ // Buffer in memory
350
+ this.recentMessages.push(message);
351
+ if (this.recentMessages.length > RECENT_BUFFER_SIZE) {
352
+ this.recentMessages = this.recentMessages.slice(-RECENT_BUFFER_SIZE);
353
+ }
354
+ // Mirror to Discord comms channel if configured
355
+ if (this.commsChannelId && toProfile) {
356
+ const senderProfile = fromProfile ?? {
357
+ slug: fromSlug,
358
+ name: fromSlug.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
359
+ tier: 3,
360
+ description: 'Primary agent',
361
+ systemPromptBody: '',
362
+ };
363
+ this.mirrorToDiscord(message, senderProfile, toProfile).catch((err) => {
364
+ logger.warn({ err }, 'Failed to mirror team message to Discord');
365
+ });
366
+ }
367
+ logger.info({ from: fromSlug, to: toSlug, id: message.id, depth: message.depth, delivered: message.delivered }, 'Team message sent');
368
+ // Emit status change (updates live status embed)
369
+ for (const cb of this.statusChangeListeners) {
370
+ try {
371
+ cb();
372
+ }
373
+ catch { /* ignore */ }
374
+ }
375
+ }
376
+ /** Register a listener that fires when team state changes. */
377
+ onStatusChange(cb) {
378
+ this.statusChangeListeners.push(cb);
379
+ }
380
+ /** Get recent inter-agent messages (for dashboard). */
381
+ getRecentMessages(limit = 50) {
382
+ return this.recentMessages.slice(-limit).reverse();
383
+ }
384
+ /** Get messages for a specific agent (sent or received). */
385
+ getMessagesForAgent(slug, limit = 50) {
386
+ return this.recentMessages
387
+ .filter((m) => m.fromAgent === slug || m.toAgent === slug)
388
+ .slice(-limit)
389
+ .reverse();
390
+ }
391
+ /** Load messages from the JSONL log file (cold start). */
392
+ loadFromLog(limit = 500) {
393
+ if (!existsSync(this.logFile))
394
+ return;
395
+ try {
396
+ const lines = readFileSync(this.logFile, 'utf-8')
397
+ .trim()
398
+ .split('\n')
399
+ .filter(Boolean);
400
+ this.recentMessages = lines
401
+ .slice(-limit)
402
+ .map((l) => JSON.parse(l));
403
+ }
404
+ catch {
405
+ // Non-fatal — start with empty buffer
406
+ }
407
+ }
408
+ /**
409
+ * Deliver any undelivered messages from the JSONL log.
410
+ * Called periodically by the daemon to pick up messages
411
+ * written by the MCP tool (which runs out-of-process).
412
+ */
413
+ async deliverPending() {
414
+ if (!existsSync(this.logFile))
415
+ return 0;
416
+ let delivered = 0;
417
+ try {
418
+ const lines = readFileSync(this.logFile, 'utf-8')
419
+ .trim()
420
+ .split('\n')
421
+ .filter(Boolean);
422
+ const updatedLines = [];
423
+ for (const line of lines) {
424
+ try {
425
+ const msg = JSON.parse(line);
426
+ if (!msg.delivered) {
427
+ // Try Discord bot delivery first
428
+ if (this.botManager?.hasBot(msg.toAgent)) {
429
+ const botDelivered = await this.botManager.deliverTeamMessage(msg.toAgent, msg.fromAgent, msg.fromAgent, msg.content);
430
+ if (botDelivered) {
431
+ msg.delivered = true;
432
+ delivered++;
433
+ }
434
+ }
435
+ // Try Slack bot delivery
436
+ if (!msg.delivered && this.slackBotManager?.hasBot(msg.toAgent)) {
437
+ const slackDelivered = await this.slackBotManager.deliverTeamMessage(msg.toAgent, msg.fromAgent, msg.fromAgent, msg.content);
438
+ if (slackDelivered) {
439
+ msg.delivered = true;
440
+ delivered++;
441
+ }
442
+ }
443
+ if (!msg.delivered) {
444
+ const sessionKey = this.resolveSessionKey(msg.toAgent);
445
+ if (sessionKey) {
446
+ this.gateway.setSessionProfile(sessionKey, msg.toAgent);
447
+ this.gateway.injectContext(sessionKey, `[Team message from ${msg.fromAgent}, depth=${msg.depth}]`, msg.content);
448
+ msg.delivered = true;
449
+ delivered++;
450
+ }
451
+ }
452
+ }
453
+ updatedLines.push(JSON.stringify(msg));
454
+ // Update in-memory buffer too
455
+ const idx = this.recentMessages.findIndex((m) => m.id === msg.id);
456
+ if (idx >= 0) {
457
+ this.recentMessages[idx] = msg;
458
+ }
459
+ else if (msg.delivered) {
460
+ this.recentMessages.push(msg);
461
+ if (this.recentMessages.length > RECENT_BUFFER_SIZE) {
462
+ this.recentMessages = this.recentMessages.slice(-RECENT_BUFFER_SIZE);
463
+ }
464
+ }
465
+ }
466
+ catch {
467
+ updatedLines.push(line); // Keep malformed lines as-is
468
+ }
469
+ }
470
+ // Write back updated log
471
+ if (delivered > 0) {
472
+ writeFileSync(this.logFile, updatedLines.join('\n') + '\n');
473
+ logger.info({ delivered }, 'Delivered pending team messages');
474
+ }
475
+ }
476
+ catch (err) {
477
+ logger.warn({ err }, 'Error delivering pending team messages');
478
+ }
479
+ return delivered;
480
+ }
481
+ /** Post an embed to the team comms Discord channel. */
482
+ async mirrorToDiscord(message, from, to) {
483
+ const token = process.env.DISCORD_TOKEN ?? '';
484
+ if (!token || !this.commsChannelId)
485
+ return;
486
+ // Truncate content for embed
487
+ const truncated = message.content.length > 1024
488
+ ? message.content.slice(0, 1021) + '...'
489
+ : message.content;
490
+ const embed = {
491
+ title: `${from.name} \u2192 ${to.name}`,
492
+ description: truncated,
493
+ color: 0x5865F2, // Discord blurple
494
+ footer: {
495
+ text: `via team_message \u00B7 depth ${message.depth}`,
496
+ },
497
+ timestamp: message.timestamp,
498
+ };
499
+ // Show sender's avatar in the embed
500
+ if (from.avatar) {
501
+ embed.thumbnail = { url: from.avatar };
502
+ }
503
+ const res = await fetch(`https://discord.com/api/v10/channels/${this.commsChannelId}/messages`, {
504
+ method: 'POST',
505
+ headers: {
506
+ Authorization: `Bot ${token}`,
507
+ 'Content-Type': 'application/json',
508
+ },
509
+ body: JSON.stringify({ embeds: [embed] }),
510
+ });
511
+ if (!res.ok) {
512
+ const errText = await res.text();
513
+ logger.warn({ status: res.status, body: errText }, 'Discord mirror failed');
514
+ }
515
+ }
516
+ /**
517
+ * Share context between agents — lightweight, rate-limited updates.
518
+ * Longer cooldown (4 hours) and daily cap (2 per agent) vs regular messages.
519
+ * Used for proactive context sharing, not task delegation.
520
+ */
521
+ async shareContext(fromSlug, toSlug, content) {
522
+ const today = new Date().toISOString().slice(0, 10);
523
+ // Daily cap: max 2 context shares per sender per day
524
+ const tracker = this.contextShareCounts.get(fromSlug);
525
+ if (tracker && tracker.date === today && tracker.count >= 2) {
526
+ logger.debug({ fromSlug }, 'Context share daily cap reached');
527
+ return null;
528
+ }
529
+ // Longer cooldown: 4 hours between context shares for the same pair
530
+ const cooldownKey = `ctx:${fromSlug}:${toSlug}`;
531
+ const lastShare = this.cooldowns.get(cooldownKey) ?? 0;
532
+ const now = Date.now();
533
+ if (now - lastShare < 4 * 3_600_000) {
534
+ logger.debug({ fromSlug, toSlug }, 'Context share cooldown active');
535
+ return null;
536
+ }
537
+ try {
538
+ const message = await this.send(fromSlug, toSlug, content, 0);
539
+ // Track the share
540
+ this.cooldowns.set(cooldownKey, now);
541
+ const current = this.contextShareCounts.get(fromSlug);
542
+ if (current && current.date === today) {
543
+ current.count++;
544
+ }
545
+ else {
546
+ this.contextShareCounts.set(fromSlug, { count: 1, date: today });
547
+ }
548
+ return message;
549
+ }
550
+ catch (err) {
551
+ logger.debug({ err, fromSlug, toSlug }, 'Context share send failed');
552
+ return null;
553
+ }
554
+ }
555
+ }
556
+ //# sourceMappingURL=team-bus.js.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Clementine TypeScript — Team agent routing.
3
+ *
4
+ * Maps agent profiles to their team configuration (channelName, canMessage, etc.).
5
+ * Bot-based agents manage their own Discord channels via AgentBotClient/BotManager.
6
+ */
7
+ import type { AgentProfile } from '../types.js';
8
+ import type { AgentManager } from './agent-manager.js';
9
+ export declare class TeamRouter {
10
+ private profileManager;
11
+ private commsChannelId?;
12
+ constructor(profileManager: AgentManager);
13
+ /** Get the resolved comms channel ID (from config). */
14
+ getCommsChannelId(): string | undefined;
15
+ /** List all team agents (profiles with channelName set). */
16
+ listTeamAgents(): AgentProfile[];
17
+ /** Get the communication graph (who can message whom). */
18
+ getTopology(): {
19
+ nodes: AgentProfile[];
20
+ edges: Array<{
21
+ from: string;
22
+ to: string;
23
+ }>;
24
+ };
25
+ }
26
+ //# sourceMappingURL=team-router.d.ts.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Clementine TypeScript — Team agent routing.
3
+ *
4
+ * Maps agent profiles to their team configuration (channelName, canMessage, etc.).
5
+ * Bot-based agents manage their own Discord channels via AgentBotClient/BotManager.
6
+ */
7
+ import { TEAM_COMMS_CHANNEL } from '../config.js';
8
+ export class TeamRouter {
9
+ profileManager;
10
+ commsChannelId;
11
+ constructor(profileManager) {
12
+ this.profileManager = profileManager;
13
+ this.commsChannelId = TEAM_COMMS_CHANNEL || undefined;
14
+ }
15
+ /** Get the resolved comms channel ID (from config). */
16
+ getCommsChannelId() {
17
+ return this.commsChannelId;
18
+ }
19
+ /** List all team agents (profiles with channelName set). */
20
+ listTeamAgents() {
21
+ return this.profileManager.listAll().filter((p) => p.team?.channelName);
22
+ }
23
+ /** Get the communication graph (who can message whom). */
24
+ getTopology() {
25
+ const agents = this.listTeamAgents();
26
+ const edges = [];
27
+ for (const agent of agents) {
28
+ if (!agent.team?.canMessage)
29
+ continue;
30
+ for (const target of agent.team.canMessage) {
31
+ edges.push({ from: agent.slug, to: target });
32
+ }
33
+ }
34
+ return { nodes: agents, edges };
35
+ }
36
+ }
37
+ //# sourceMappingURL=team-router.js.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Clementine TypeScript — Tool loop detection system.
3
+ *
4
+ * Detects when the agent gets stuck in repetitive tool-call patterns:
5
+ * - generic_repeat: Same tool+input called repeatedly
6
+ * - poll_no_progress: Same tool returning identical results
7
+ * - ping_pong: Alternating between two tool+input combos with no result change
8
+ *
9
+ * Inspired by OpenClaw's loop detection approach.
10
+ */
11
+ export interface LoopCheckResult {
12
+ verdict: 'ok' | 'warn' | 'block';
13
+ detector?: string;
14
+ detail?: string;
15
+ }
16
+ /**
17
+ * Detects repetitive tool-call patterns in a sliding window.
18
+ *
19
+ * Usage:
20
+ * 1. Call `recordCall()` before each tool invocation — it returns a verdict.
21
+ * 2. Call `recordResult()` after the tool returns — it updates the last entry's resultHash.
22
+ * 3. Call `reset()` when rotating sessions to clear state.
23
+ */
24
+ export declare class ToolLoopDetector {
25
+ private window;
26
+ /**
27
+ * Record a new tool call, run all detectors, and return the verdict.
28
+ *
29
+ * @param toolName - Name of the tool being called
30
+ * @param input - Tool input parameters
31
+ * @returns Loop check result with verdict and optional detector/detail info
32
+ */
33
+ recordCall(toolName: string, input: Record<string, unknown>): LoopCheckResult;
34
+ /**
35
+ * Update the most recent call entry with the result hash.
36
+ *
37
+ * @param resultText - The text output returned by the tool
38
+ */
39
+ recordResult(resultText: string): void;
40
+ /** Clear the sliding window (e.g. on session rotation). */
41
+ reset(): void;
42
+ /**
43
+ * generic_repeat: Same tool+inputHash called N times in the window.
44
+ */
45
+ private detectGenericRepeat;
46
+ /**
47
+ * poll_no_progress: Same tool returning identical resultHash N times.
48
+ * Only considers entries that have a resultHash set.
49
+ */
50
+ private detectPollNoProgress;
51
+ /**
52
+ * ping_pong: Alternating between exactly two tool+inputHash combos
53
+ * with no result change. Scans the tail of the window for the pattern.
54
+ */
55
+ private detectPingPong;
56
+ }
57
+ /** Shared singleton instance for the process. */
58
+ export declare const toolLoopDetector: ToolLoopDetector;
59
+ //# sourceMappingURL=tool-loop-detector.d.ts.map