ai-control-center 1.15.2

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 (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +584 -0
  3. package/bin/aicc.js +772 -0
  4. package/lib/actions/approve.js +71 -0
  5. package/lib/actions/assign-project.js +132 -0
  6. package/lib/actions/browser-test.js +64 -0
  7. package/lib/actions/cleanup.js +174 -0
  8. package/lib/actions/debug.js +298 -0
  9. package/lib/actions/deploy.js +1229 -0
  10. package/lib/actions/fix-bug.js +134 -0
  11. package/lib/actions/new-feature.js +255 -0
  12. package/lib/actions/reject.js +307 -0
  13. package/lib/actions/review.js +706 -0
  14. package/lib/actions/status.js +47 -0
  15. package/lib/agents/browser-qa-agent.js +611 -0
  16. package/lib/agents/payment-agent.js +116 -0
  17. package/lib/agents/suggestion-agent.js +88 -0
  18. package/lib/cli.js +303 -0
  19. package/lib/config.js +243 -0
  20. package/lib/hub/hub-server.js +440 -0
  21. package/lib/hub/project-poller.js +75 -0
  22. package/lib/hub/skill-registry.js +89 -0
  23. package/lib/hub/state-aggregator.js +204 -0
  24. package/lib/index.js +471 -0
  25. package/lib/init/doctor.js +523 -0
  26. package/lib/init/presets.js +222 -0
  27. package/lib/init/skill-fetcher.js +77 -0
  28. package/lib/init/wizard.js +973 -0
  29. package/lib/integrations/codex-runner.js +128 -0
  30. package/lib/integrations/github-actions.js +248 -0
  31. package/lib/integrations/github-reporter.js +229 -0
  32. package/lib/integrations/screenshot-store.js +102 -0
  33. package/lib/openclaw/bridge.js +650 -0
  34. package/lib/openclaw/generate-skill.js +235 -0
  35. package/lib/openclaw/openclaw.json +64 -0
  36. package/lib/orchestrator/autonomous-loop.js +429 -0
  37. package/lib/orchestrator/thread-triggers.js +63 -0
  38. package/lib/roleplay/agent-messenger.js +75 -0
  39. package/lib/roleplay/discussion-threads.js +303 -0
  40. package/lib/roleplay/health-monitor.js +121 -0
  41. package/lib/roleplay/pm-agent.js +513 -0
  42. package/lib/roleplay/roleplay-config.js +25 -0
  43. package/lib/roleplay/room.js +164 -0
  44. package/lib/shared/action-runner.js +2330 -0
  45. package/lib/shared/event-bus.js +185 -0
  46. package/lib/slack/bot.js +378 -0
  47. package/lib/telegram/bot.js +416 -0
  48. package/lib/telegram/commands.js +1267 -0
  49. package/lib/telegram/keyboards.js +113 -0
  50. package/lib/telegram/notifications.js +247 -0
  51. package/lib/twitch/bot.js +354 -0
  52. package/lib/twitch/commands.js +302 -0
  53. package/lib/twitch/notifications.js +63 -0
  54. package/lib/utils/achievements.js +191 -0
  55. package/lib/utils/activity-log.js +182 -0
  56. package/lib/utils/agent-leaderboard.js +119 -0
  57. package/lib/utils/audit-logger.js +232 -0
  58. package/lib/utils/codebase-context.js +288 -0
  59. package/lib/utils/codebase-indexer.js +381 -0
  60. package/lib/utils/config-schema.js +230 -0
  61. package/lib/utils/context-compressor.js +172 -0
  62. package/lib/utils/correlation.js +63 -0
  63. package/lib/utils/cost-tracker.js +423 -0
  64. package/lib/utils/cron-scheduler.js +53 -0
  65. package/lib/utils/db-adapter.js +293 -0
  66. package/lib/utils/display.js +272 -0
  67. package/lib/utils/errors.js +116 -0
  68. package/lib/utils/format.js +134 -0
  69. package/lib/utils/intent-engine.js +464 -0
  70. package/lib/utils/mcp-client.js +238 -0
  71. package/lib/utils/model-ab-test.js +164 -0
  72. package/lib/utils/notify.js +122 -0
  73. package/lib/utils/persona-loader.js +80 -0
  74. package/lib/utils/pipeline-lock.js +73 -0
  75. package/lib/utils/pipeline.js +214 -0
  76. package/lib/utils/plugin-runner.js +234 -0
  77. package/lib/utils/rate-limiter.js +84 -0
  78. package/lib/utils/rbac.js +74 -0
  79. package/lib/utils/runner.js +1809 -0
  80. package/lib/utils/security.js +191 -0
  81. package/lib/utils/self-healer.js +144 -0
  82. package/lib/utils/skill-loader.js +255 -0
  83. package/lib/utils/spinner.js +132 -0
  84. package/lib/utils/stage-queue.js +50 -0
  85. package/lib/utils/state-machine.js +89 -0
  86. package/lib/utils/status-bar.js +327 -0
  87. package/lib/utils/token-estimator.js +101 -0
  88. package/lib/utils/ux-analyzer.js +101 -0
  89. package/lib/utils/webhook-emitter.js +83 -0
  90. package/lib/web/public/css/styles.css +417 -0
  91. package/lib/web/public/dark-mode.js +44 -0
  92. package/lib/web/public/hub/kanban.html +206 -0
  93. package/lib/web/public/index.html +45 -0
  94. package/lib/web/public/js/app.js +71 -0
  95. package/lib/web/public/js/ask.js +110 -0
  96. package/lib/web/public/js/dashboard.js +165 -0
  97. package/lib/web/public/js/deploy.js +72 -0
  98. package/lib/web/public/js/feature.js +79 -0
  99. package/lib/web/public/js/health.js +65 -0
  100. package/lib/web/public/js/logs.js +93 -0
  101. package/lib/web/public/js/review.js +123 -0
  102. package/lib/web/public/js/ws-client.js +82 -0
  103. package/lib/web/public/office/css/office.css +678 -0
  104. package/lib/web/public/office/index.html +148 -0
  105. package/lib/web/public/office/js/achievements-ui.js +117 -0
  106. package/lib/web/public/office/js/character.js +1056 -0
  107. package/lib/web/public/office/js/chat-bubbles.js +177 -0
  108. package/lib/web/public/office/js/cost-overlay.js +123 -0
  109. package/lib/web/public/office/js/day-night.js +68 -0
  110. package/lib/web/public/office/js/effects.js +632 -0
  111. package/lib/web/public/office/js/engine.js +146 -0
  112. package/lib/web/public/office/js/feature-ticket.js +216 -0
  113. package/lib/web/public/office/js/hub-client.js +60 -0
  114. package/lib/web/public/office/js/main.js +1757 -0
  115. package/lib/web/public/office/js/office-layout.js +1524 -0
  116. package/lib/web/public/office/js/pathfinding.js +144 -0
  117. package/lib/web/public/office/js/pixel-sprites.js +1454 -0
  118. package/lib/web/public/office/js/progress-bars.js +117 -0
  119. package/lib/web/public/office/js/replay.js +191 -0
  120. package/lib/web/public/office/js/sound-effects.js +91 -0
  121. package/lib/web/public/office/js/sprite-renderer.js +211 -0
  122. package/lib/web/public/office/js/stamina-system.js +89 -0
  123. package/lib/web/public/office/js/ui.js +107 -0
  124. package/lib/web/public/onboarding/index.html +243 -0
  125. package/lib/web/public/timeline/index.html +195 -0
  126. package/lib/web/routes/api.js +499 -0
  127. package/lib/web/routes/logs.js +20 -0
  128. package/lib/web/routes/metrics.js +99 -0
  129. package/lib/web/server.js +183 -0
  130. package/lib/web/ws/handler.js +65 -0
  131. package/package.json +67 -0
  132. package/templates/agent-architect.md +69 -0
  133. package/templates/agent-gemini-pm.md +49 -0
  134. package/templates/agent-gemini-reviewer.md +52 -0
  135. package/templates/copilot-instructions.md +36 -0
  136. package/templates/pipelines/mobile.json +27 -0
  137. package/templates/pipelines/nodejs-api.json +27 -0
  138. package/templates/pipelines/python.json +27 -0
  139. package/templates/pipelines/react.json +27 -0
  140. package/templates/pipelines/salesforce.json +27 -0
  141. package/templates/role-gemini.md +97 -0
  142. package/templates/skill-architect.md +114 -0
  143. package/templates/skill-browser-qa.md +50 -0
  144. package/templates/skill-bug-from-qa.md +58 -0
  145. package/templates/skill-chatbot.md +93 -0
  146. package/templates/skill-implement.md +78 -0
  147. package/templates/skill-openclaw.md +174 -0
  148. package/templates/skill-payment.md +110 -0
  149. package/templates/skill-pm-spec.md +77 -0
  150. package/templates/skill-requirement-capture.md +97 -0
  151. package/templates/skill-review.md +108 -0
  152. package/templates/skill-reviewer-qa.md +44 -0
  153. package/templates/skill-suggestion.md +45 -0
  154. package/templates/skill-template.md +142 -0
@@ -0,0 +1,650 @@
1
+ /**
2
+ * OpenClaw Bridge — WebSocket client connecting AICC event bus to OpenClaw Gateway.
3
+ *
4
+ * Subscribes to AICC pipeline events and forwards them to OpenClaw Gateway
5
+ * for multi-channel distribution (Telegram, WhatsApp, Slack, Discord, etc.).
6
+ *
7
+ * Features:
8
+ * - Auto-reconnect with exponential backoff (1s → 30s)
9
+ * - Auth via token from ~/.openclaw/openclaw.json
10
+ * - Event formatting for OpenClaw session messages
11
+ * - Graceful shutdown
12
+ */
13
+ import { readFileSync, existsSync } from 'fs';
14
+ import { resolve } from 'path';
15
+ import { logActivity } from '../utils/activity-log.js';
16
+
17
+ let _ws = null;
18
+ let _bus = null;
19
+ let _reconnectTimer = null;
20
+ let _reconnectDelay = 1000;
21
+ let _stopping = false;
22
+ let _connected = false;
23
+
24
+ const MAX_RECONNECT_DELAY = 30000;
25
+ const GATEWAY_PORT = 18789;
26
+
27
+ // ─── Config helpers ──────────────────────────────────────────────────────────
28
+
29
+ /**
30
+ * Read OpenClaw config from ~/.openclaw/openclaw.json.
31
+ * Returns { token, port } or null if not found.
32
+ */
33
+ function readOpenClawConfig() {
34
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
35
+ const configPath = resolve(homeDir, '.openclaw', 'openclaw.json');
36
+ if (!existsSync(configPath)) return null;
37
+ try {
38
+ const raw = JSON.parse(readFileSync(configPath, 'utf8'));
39
+ return {
40
+ token: raw.gateway?.token || raw.auth?.token || null,
41
+ port: raw.gateway?.port || GATEWAY_PORT,
42
+ };
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Get WebSocket URL for OpenClaw Gateway.
50
+ */
51
+ function getGatewayUrl() {
52
+ const cfg = readOpenClawConfig();
53
+ const port = cfg?.port || GATEWAY_PORT;
54
+ return `ws://localhost:${port}`;
55
+ }
56
+
57
+ // ─── Event formatting ────────────────────────────────────────────────────────
58
+
59
+ /**
60
+ * Format an AICC pipeline event into an OpenClaw session message.
61
+ */
62
+ function formatEventMessage(eventType, data) {
63
+ const ts = new Date().toISOString();
64
+
65
+ switch (eventType) {
66
+ case 'status': {
67
+ const s = data.status || {};
68
+ const prev = data.previousStage || 'unknown';
69
+ return {
70
+ type: 'pipeline.status',
71
+ stage: s.stage,
72
+ previousStage: prev,
73
+ feature: s.current_feature || null,
74
+ pipelineMode: s.pipeline_mode || 'manual',
75
+ timestamp: ts,
76
+ summary: `Pipeline: ${prev} → ${s.stage}${s.current_feature ? ` | Feature: ${s.current_feature}` : ''}`,
77
+ };
78
+ }
79
+
80
+ case 'pipeline-event': {
81
+ const evt = data.event || 'unknown';
82
+ const evtData = data.data || {};
83
+ return {
84
+ type: `pipeline.${evt}`,
85
+ event: evt,
86
+ ...evtData,
87
+ timestamp: ts,
88
+ summary: evtData.message || `Pipeline event: ${evt}`,
89
+ };
90
+ }
91
+
92
+ case 'log': {
93
+ return {
94
+ type: 'pipeline.log',
95
+ agent: data.agent,
96
+ message: data.message,
97
+ logType: data.type || 'info',
98
+ timestamp: ts,
99
+ summary: `[${data.agent}] ${data.message}`,
100
+ };
101
+ }
102
+
103
+ default:
104
+ return {
105
+ type: `pipeline.${eventType}`,
106
+ ...data,
107
+ timestamp: ts,
108
+ summary: `Event: ${eventType}`,
109
+ };
110
+ }
111
+ }
112
+
113
+ // ─── WebSocket connection ────────────────────────────────────────────────────
114
+
115
+ /**
116
+ * Create WebSocket connection to OpenClaw Gateway.
117
+ * Uses dynamic import for 'ws' to avoid hard dependency.
118
+ */
119
+ async function connect() {
120
+ if (_stopping) return;
121
+
122
+ const url = getGatewayUrl();
123
+ const cfg = readOpenClawConfig();
124
+
125
+ let WebSocket;
126
+ try {
127
+ const wsModule = await import('ws');
128
+ WebSocket = wsModule.default || wsModule.WebSocket || wsModule;
129
+ } catch {
130
+ logActivity('OPENCLAW', 'WARN', 'ws package not installed — bridge disabled. Run: npm i ws');
131
+ return;
132
+ }
133
+
134
+ try {
135
+ const headers = {};
136
+ if (cfg?.token) {
137
+ headers['Authorization'] = `Bearer ${cfg.token}`;
138
+ }
139
+
140
+ _ws = new WebSocket(url, { headers });
141
+
142
+ _ws.on('open', () => {
143
+ _connected = true;
144
+ _reconnectDelay = 1000; // reset backoff
145
+ logActivity('OPENCLAW', 'SUCCESS', `Bridge connected to ${url}`);
146
+
147
+ // Send connect frame — register as AICC node device
148
+ _ws.send(JSON.stringify({
149
+ type: 'connect',
150
+ client: 'aicc-bridge',
151
+ version: '1.0',
152
+ device: {
153
+ id: 'aicc-pipeline',
154
+ name: 'AI Control Center Pipeline',
155
+ type: 'pipeline-engine',
156
+ capabilities: [
157
+ 'pipeline.status',
158
+ 'pipeline.feature',
159
+ 'pipeline.bug',
160
+ 'pipeline.deploy',
161
+ 'pipeline.approve',
162
+ 'pipeline.reject',
163
+ 'pipeline.review',
164
+ 'pipeline.health',
165
+ 'pipeline.logs',
166
+ 'pipeline.files',
167
+ 'pipeline.read',
168
+ 'pipeline.events',
169
+ 'pipeline.reset',
170
+ 'pipeline.cleanup',
171
+ ],
172
+ },
173
+ // Expose AICC tools so OpenClaw AI can invoke them directly
174
+ tools: buildToolDefinitions(),
175
+ }));
176
+
177
+ // Register cron jobs for pipeline monitoring
178
+ registerCronJobs();
179
+ });
180
+
181
+ _ws.on('message', (raw) => {
182
+ try {
183
+ const msg = JSON.parse(raw.toString());
184
+ handleIncomingMessage(msg);
185
+ } catch {
186
+ // ignore non-JSON messages
187
+ }
188
+ });
189
+
190
+ _ws.on('close', (code) => {
191
+ _connected = false;
192
+ if (!_stopping) {
193
+ logActivity('OPENCLAW', 'WARN', `Bridge disconnected (code ${code}) — reconnecting in ${_reconnectDelay / 1000}s`);
194
+ scheduleReconnect();
195
+ }
196
+ });
197
+
198
+ _ws.on('error', (err) => {
199
+ _connected = false;
200
+ // Only log if not a connection refused (gateway not running)
201
+ if (err.code !== 'ECONNREFUSED') {
202
+ logActivity('OPENCLAW', 'ERROR', `Bridge error: ${err.message}`);
203
+ }
204
+ });
205
+ } catch (err) {
206
+ logActivity('OPENCLAW', 'ERROR', `Bridge connection failed: ${err.message}`);
207
+ scheduleReconnect();
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Schedule a reconnection attempt with exponential backoff.
213
+ */
214
+ function scheduleReconnect() {
215
+ if (_stopping || _reconnectTimer) return;
216
+ _reconnectTimer = setTimeout(() => {
217
+ _reconnectTimer = null;
218
+ _reconnectDelay = Math.min(_reconnectDelay * 2, MAX_RECONNECT_DELAY);
219
+ connect();
220
+ }, _reconnectDelay);
221
+ }
222
+
223
+ /**
224
+ * Send a message to OpenClaw Gateway.
225
+ */
226
+ function send(msg) {
227
+ if (!_ws || _ws.readyState !== 1 /* OPEN */) return false;
228
+ try {
229
+ _ws.send(JSON.stringify(msg));
230
+ return true;
231
+ } catch {
232
+ return false;
233
+ }
234
+ }
235
+
236
+ // ─── Incoming message handler ────────────────────────────────────────────────
237
+
238
+ /**
239
+ * Handle messages received from OpenClaw Gateway.
240
+ * These can be commands from users via Telegram/WhatsApp/etc.
241
+ */
242
+ async function handleIncomingMessage(msg) {
243
+ if (!_bus) return;
244
+
245
+ switch (msg.type) {
246
+ case 'command': {
247
+ // OpenClaw → AICC command execution
248
+ const result = await executeCommand(msg.command, msg.args || [], msg);
249
+ if (result !== undefined) {
250
+ send({
251
+ type: 'command-result',
252
+ requestId: msg.requestId || null,
253
+ command: msg.command,
254
+ result,
255
+ timestamp: new Date().toISOString(),
256
+ });
257
+ }
258
+ break;
259
+ }
260
+
261
+ case 'tool.call': {
262
+ // OpenClaw AI invokes an AICC tool directly
263
+ const toolName = msg.tool || '';
264
+ const toolArgs = msg.parameters || {};
265
+ // Map tool name to command + args
266
+ const cmd = toolName.replace('pipeline.', '');
267
+ const cmdArgs = [];
268
+ if (toolArgs.description) cmdArgs.push(toolArgs.description);
269
+ if (toolArgs.reason) cmdArgs.push(toolArgs.reason);
270
+ if (toolArgs.subdir) cmdArgs.push(toolArgs.subdir);
271
+ if (toolArgs.filename) cmdArgs.push(toolArgs.filename);
272
+ if (toolArgs.testLevel) cmdArgs.push(toolArgs.testLevel);
273
+ if (toolArgs.lines) cmdArgs.push(String(toolArgs.lines));
274
+
275
+ const toolResult = await executeCommand(cmd, cmdArgs, msg);
276
+ send({
277
+ type: 'tool.result',
278
+ callId: msg.callId || null,
279
+ tool: msg.tool,
280
+ result: toolResult,
281
+ timestamp: new Date().toISOString(),
282
+ });
283
+ break;
284
+ }
285
+
286
+ case 'ping': {
287
+ send({ type: 'pong', timestamp: new Date().toISOString() });
288
+ break;
289
+ }
290
+
291
+ case 'ack':
292
+ case 'pong':
293
+ break;
294
+
295
+ default:
296
+ _bus.emit('openclaw-message', msg);
297
+ break;
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Execute an AICC command received from OpenClaw.
303
+ * Maps command names to action-runner functions.
304
+ */
305
+ async function executeCommand(command, args, context) {
306
+ let actions;
307
+ try {
308
+ actions = await import('../shared/action-runner.js');
309
+ } catch (err) {
310
+ return { error: `Failed to load action-runner: ${err.message}` };
311
+ }
312
+
313
+ try {
314
+ switch (command) {
315
+ case 'status':
316
+ return actions.getStatusData();
317
+
318
+ case 'health':
319
+ return await actions.getHealthData();
320
+
321
+ case 'feature': {
322
+ const desc = args[0];
323
+ if (!desc || desc.length < 10) return { error: 'Description must be ≥ 10 characters' };
324
+ return await actions.runNewFeature(desc, 'manual', 'feature');
325
+ }
326
+
327
+ case 'bug': {
328
+ const desc = args[0];
329
+ if (!desc || desc.length < 10) return { error: 'Description must be ≥ 10 characters' };
330
+ return await actions.runNewFeature(desc, 'manual', 'bug');
331
+ }
332
+
333
+ case 'approve':
334
+ return await actions.runApprove();
335
+
336
+ case 'reject': {
337
+ const reason = args[0] || 'Rejected via OpenClaw';
338
+ return await actions.runReject(reason);
339
+ }
340
+
341
+ case 'deploy':
342
+ return await actions.runDeploy(args[0] || 'NoTestRun');
343
+
344
+ case 'review':
345
+ return actions.getLatestReview() || { found: false };
346
+
347
+ case 'logs':
348
+ return actions.getLogsData(parseInt(args[0], 10) || 30);
349
+
350
+ case 'files':
351
+ return actions.listFiles(args[0] || 'specs');
352
+
353
+ case 'read': {
354
+ const subdir = args[0];
355
+ const name = args[1];
356
+ if (!subdir || !name) return { error: 'Usage: read <subdir> <filename>' };
357
+ const content = actions.readFile(subdir, name);
358
+ return content !== null ? { name, content } : { error: 'File not found' };
359
+ }
360
+
361
+ case 'reset':
362
+ return actions.runReset();
363
+
364
+ case 'cleanup':
365
+ return await actions.runCleanup();
366
+
367
+ default:
368
+ return { error: `Unknown command: ${command}` };
369
+ }
370
+ } catch (err) {
371
+ logActivity('OPENCLAW', 'ERROR', `Command "${command}" failed: ${err.message}`);
372
+ return { error: err.message };
373
+ }
374
+ }
375
+
376
+ // ─── Event bus listeners ─────────────────────────────────────────────────────
377
+
378
+ function onStatus(data) {
379
+ const msg = formatEventMessage('status', data);
380
+ // Inject into OpenClaw session so AI has pipeline context
381
+ send({ type: 'event', payload: msg, inject: true, scope: 'session' });
382
+
383
+ // On stage completion, inject the stage's output document into the session
384
+ // so OpenClaw AI has full context when answering user questions
385
+ const stage = data.status?.stage;
386
+ const prev = data.previousStage;
387
+ if (stage && stage !== prev) {
388
+ injectStageDocument(stage);
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Inject stage output documents into OpenClaw session.
394
+ * When a stage completes, the AI on the other end needs context about what was produced.
395
+ */
396
+ async function injectStageDocument(stage) {
397
+ let actions;
398
+ try {
399
+ actions = await import('../shared/action-runner.js');
400
+ } catch { return; }
401
+
402
+ const DOC_MAP = {
403
+ spec_complete: { subdir: 'specs', prefix: 'SPEC-', label: 'PM Spec' },
404
+ arch_complete: { subdir: 'architecture', prefix: 'ARCH-', label: 'Architecture' },
405
+ implementation_complete: { subdir: 'tasks', prefix: 'TASKS-', label: 'Task List' },
406
+ review_complete: { subdir: 'reviews', prefix: 'REVIEW-', label: 'Code Review' },
407
+ };
408
+
409
+ const docInfo = DOC_MAP[stage];
410
+ if (!docInfo) return;
411
+
412
+ // Find and read the latest document for this stage
413
+ const files = actions.listFiles(docInfo.subdir);
414
+ if (!files || files.length === 0) return;
415
+
416
+ const latest = files[0]; // already sorted by reverse date
417
+ const content = actions.readFile(docInfo.subdir, latest.name);
418
+ if (!content) return;
419
+
420
+ // Truncate for session injection (max 3000 chars to avoid flooding)
421
+ const truncated = content.length > 3000
422
+ ? content.slice(0, 3000) + '\n\n... (truncated, use `aicc read` for full content)'
423
+ : content;
424
+
425
+ send({
426
+ type: 'context',
427
+ scope: 'session',
428
+ inject: true,
429
+ label: `${docInfo.label}: ${latest.name}`,
430
+ content: truncated,
431
+ timestamp: new Date().toISOString(),
432
+ });
433
+ }
434
+
435
+ function onPipelineEvent(data) {
436
+ const msg = formatEventMessage('pipeline-event', data);
437
+ // Pipeline events always injected — they drive proactive PM responses
438
+ send({ type: 'event', payload: msg, inject: true, scope: 'session' });
439
+ }
440
+
441
+ function onLog(data) {
442
+ const msg = formatEventMessage('log', data);
443
+ // Logs are event-only (not injected into session to avoid noise)
444
+ send({ type: 'event', payload: msg, inject: false });
445
+ }
446
+
447
+ // ─── Tool definitions for OpenClaw ───────────────────────────────────────────
448
+
449
+ /**
450
+ * Build tool definitions that OpenClaw AI agents can call.
451
+ * These map to AICC CLI commands executed via the bridge.
452
+ */
453
+ function buildToolDefinitions() {
454
+ return [
455
+ {
456
+ name: 'pipeline.status',
457
+ description: 'Get current pipeline status including stage, feature, provider health, and cost.',
458
+ parameters: {},
459
+ },
460
+ {
461
+ name: 'pipeline.feature',
462
+ description: 'Create a new feature request in the pipeline.',
463
+ parameters: { description: { type: 'string', required: true, minLength: 10 } },
464
+ },
465
+ {
466
+ name: 'pipeline.bug',
467
+ description: 'Create a bug fix request in the pipeline.',
468
+ parameters: { description: { type: 'string', required: true, minLength: 10 } },
469
+ },
470
+ {
471
+ name: 'pipeline.deploy',
472
+ description: 'Deploy the approved feature.',
473
+ parameters: { testLevel: { type: 'string', default: 'NoTestRun' } },
474
+ },
475
+ {
476
+ name: 'pipeline.approve',
477
+ description: 'Approve the current feature after review.',
478
+ parameters: {},
479
+ },
480
+ {
481
+ name: 'pipeline.reject',
482
+ description: 'Reject the current feature with feedback.',
483
+ parameters: { reason: { type: 'string', required: true } },
484
+ },
485
+ {
486
+ name: 'pipeline.review',
487
+ description: 'Get the latest code review.',
488
+ parameters: {},
489
+ },
490
+ {
491
+ name: 'pipeline.health',
492
+ description: 'Get system health including AI provider status and circuit breakers.',
493
+ parameters: {},
494
+ },
495
+ {
496
+ name: 'pipeline.logs',
497
+ description: 'Get recent activity logs.',
498
+ parameters: { lines: { type: 'number', default: 30 } },
499
+ },
500
+ {
501
+ name: 'pipeline.files',
502
+ description: 'List workflow files in a subdirectory (specs, architecture, tasks, reviews).',
503
+ parameters: { subdir: { type: 'string', required: true } },
504
+ },
505
+ {
506
+ name: 'pipeline.read',
507
+ description: 'Read a specific workflow file.',
508
+ parameters: { subdir: { type: 'string', required: true }, filename: { type: 'string', required: true } },
509
+ },
510
+ {
511
+ name: 'pipeline.reset',
512
+ description: 'Abandon the current feature and reset pipeline to idle.',
513
+ parameters: {},
514
+ },
515
+ {
516
+ name: 'pipeline.cleanup',
517
+ description: 'Archive old workflow files.',
518
+ parameters: {},
519
+ },
520
+ ];
521
+ }
522
+
523
+ // ─── Cron registration ───────────────────────────────────────────────────────
524
+
525
+ /**
526
+ * Register OpenClaw cron jobs for pipeline monitoring.
527
+ * These run inside the OpenClaw Gateway and invoke the AICC skill.
528
+ */
529
+ function registerCronJobs() {
530
+ const cronJobs = [
531
+ {
532
+ id: 'aicc-active-monitor',
533
+ schedule: '*/2 * * * *', // every 2 minutes
534
+ prompt: 'Check pipeline status with `npx aicc status`. If pipeline is active (not idle/deployed), report progress to the user. If stalled >5 minutes on same stage, warn.',
535
+ condition: 'pipeline_active',
536
+ },
537
+ {
538
+ id: 'aicc-idle-heartbeat',
539
+ schedule: '*/30 * * * *', // every 30 minutes
540
+ prompt: 'Quick health check with `npx aicc health`. Report only if there are issues, banned models, or the pipeline is waiting for user action (review_complete, inbox).',
541
+ condition: 'always',
542
+ },
543
+ {
544
+ id: 'aicc-stall-detector',
545
+ schedule: '*/5 * * * *', // every 5 minutes
546
+ prompt: 'Check `npx aicc status`. If the pipeline has been on the same active stage for >10 minutes, alert the user and suggest retry or reset.',
547
+ condition: 'pipeline_active',
548
+ },
549
+ ];
550
+
551
+ for (const job of cronJobs) {
552
+ send({
553
+ type: 'cron.register',
554
+ cron: job,
555
+ });
556
+ }
557
+ }
558
+
559
+ // ─── Public API ──────────────────────────────────────────────────────────────
560
+
561
+ /**
562
+ * Start the OpenClaw bridge.
563
+ * Connects to gateway and subscribes to AICC event bus.
564
+ *
565
+ * @param {PipelineBus} bus - The AICC event bus instance
566
+ * @returns {Promise<boolean>} true if connection initiated
567
+ */
568
+ export async function startBridge(bus) {
569
+ if (_bus) return true; // already running
570
+
571
+ const cfg = readOpenClawConfig();
572
+ if (!cfg) {
573
+ logActivity('OPENCLAW', 'INFO', 'No ~/.openclaw/openclaw.json found — bridge not started');
574
+ return false;
575
+ }
576
+
577
+ _bus = bus;
578
+ _stopping = false;
579
+
580
+ // Subscribe to event bus
581
+ bus.on('status', onStatus);
582
+ bus.on('pipeline-event', onPipelineEvent);
583
+ bus.on('log', onLog);
584
+
585
+ // Connect to gateway
586
+ await connect();
587
+ return true;
588
+ }
589
+
590
+ /**
591
+ * Stop the OpenClaw bridge gracefully.
592
+ */
593
+ export function stopBridge() {
594
+ _stopping = true;
595
+
596
+ // Unsubscribe from event bus
597
+ if (_bus) {
598
+ _bus.off('status', onStatus);
599
+ _bus.off('pipeline-event', onPipelineEvent);
600
+ _bus.off('log', onLog);
601
+ _bus = null;
602
+ }
603
+
604
+ // Clear reconnect timer
605
+ if (_reconnectTimer) {
606
+ clearTimeout(_reconnectTimer);
607
+ _reconnectTimer = null;
608
+ }
609
+
610
+ // Close WebSocket
611
+ if (_ws) {
612
+ try { _ws.close(1000, 'AICC bridge shutdown'); } catch { /* ok */ }
613
+ _ws = null;
614
+ }
615
+
616
+ _connected = false;
617
+ _reconnectDelay = 1000;
618
+ }
619
+
620
+ /**
621
+ * Check if bridge is currently connected to OpenClaw Gateway.
622
+ */
623
+ export function isBridgeConnected() {
624
+ return _connected && _ws?.readyState === 1;
625
+ }
626
+
627
+ /**
628
+ * Check if OpenClaw is available (config exists).
629
+ */
630
+ export function isOpenClawAvailable() {
631
+ return readOpenClawConfig() !== null;
632
+ }
633
+
634
+ /**
635
+ * Send a direct message to OpenClaw Gateway.
636
+ * Useful for sending pipeline results, notifications, etc.
637
+ *
638
+ * @param {string} channel - Target channel (e.g., 'telegram', 'slack', 'all')
639
+ * @param {string} message - The message text
640
+ * @param {object} [opts] - Additional options
641
+ */
642
+ export function sendToOpenClaw(channel, message, opts = {}) {
643
+ return send({
644
+ type: 'message',
645
+ channel,
646
+ content: message,
647
+ format: opts.format || 'html',
648
+ ...opts,
649
+ });
650
+ }