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.
- package/LICENSE +21 -0
- package/README.md +584 -0
- package/bin/aicc.js +772 -0
- package/lib/actions/approve.js +71 -0
- package/lib/actions/assign-project.js +132 -0
- package/lib/actions/browser-test.js +64 -0
- package/lib/actions/cleanup.js +174 -0
- package/lib/actions/debug.js +298 -0
- package/lib/actions/deploy.js +1229 -0
- package/lib/actions/fix-bug.js +134 -0
- package/lib/actions/new-feature.js +255 -0
- package/lib/actions/reject.js +307 -0
- package/lib/actions/review.js +706 -0
- package/lib/actions/status.js +47 -0
- package/lib/agents/browser-qa-agent.js +611 -0
- package/lib/agents/payment-agent.js +116 -0
- package/lib/agents/suggestion-agent.js +88 -0
- package/lib/cli.js +303 -0
- package/lib/config.js +243 -0
- package/lib/hub/hub-server.js +440 -0
- package/lib/hub/project-poller.js +75 -0
- package/lib/hub/skill-registry.js +89 -0
- package/lib/hub/state-aggregator.js +204 -0
- package/lib/index.js +471 -0
- package/lib/init/doctor.js +523 -0
- package/lib/init/presets.js +222 -0
- package/lib/init/skill-fetcher.js +77 -0
- package/lib/init/wizard.js +973 -0
- package/lib/integrations/codex-runner.js +128 -0
- package/lib/integrations/github-actions.js +248 -0
- package/lib/integrations/github-reporter.js +229 -0
- package/lib/integrations/screenshot-store.js +102 -0
- package/lib/openclaw/bridge.js +650 -0
- package/lib/openclaw/generate-skill.js +235 -0
- package/lib/openclaw/openclaw.json +64 -0
- package/lib/orchestrator/autonomous-loop.js +429 -0
- package/lib/orchestrator/thread-triggers.js +63 -0
- package/lib/roleplay/agent-messenger.js +75 -0
- package/lib/roleplay/discussion-threads.js +303 -0
- package/lib/roleplay/health-monitor.js +121 -0
- package/lib/roleplay/pm-agent.js +513 -0
- package/lib/roleplay/roleplay-config.js +25 -0
- package/lib/roleplay/room.js +164 -0
- package/lib/shared/action-runner.js +2330 -0
- package/lib/shared/event-bus.js +185 -0
- package/lib/slack/bot.js +378 -0
- package/lib/telegram/bot.js +416 -0
- package/lib/telegram/commands.js +1267 -0
- package/lib/telegram/keyboards.js +113 -0
- package/lib/telegram/notifications.js +247 -0
- package/lib/twitch/bot.js +354 -0
- package/lib/twitch/commands.js +302 -0
- package/lib/twitch/notifications.js +63 -0
- package/lib/utils/achievements.js +191 -0
- package/lib/utils/activity-log.js +182 -0
- package/lib/utils/agent-leaderboard.js +119 -0
- package/lib/utils/audit-logger.js +232 -0
- package/lib/utils/codebase-context.js +288 -0
- package/lib/utils/codebase-indexer.js +381 -0
- package/lib/utils/config-schema.js +230 -0
- package/lib/utils/context-compressor.js +172 -0
- package/lib/utils/correlation.js +63 -0
- package/lib/utils/cost-tracker.js +423 -0
- package/lib/utils/cron-scheduler.js +53 -0
- package/lib/utils/db-adapter.js +293 -0
- package/lib/utils/display.js +272 -0
- package/lib/utils/errors.js +116 -0
- package/lib/utils/format.js +134 -0
- package/lib/utils/intent-engine.js +464 -0
- package/lib/utils/mcp-client.js +238 -0
- package/lib/utils/model-ab-test.js +164 -0
- package/lib/utils/notify.js +122 -0
- package/lib/utils/persona-loader.js +80 -0
- package/lib/utils/pipeline-lock.js +73 -0
- package/lib/utils/pipeline.js +214 -0
- package/lib/utils/plugin-runner.js +234 -0
- package/lib/utils/rate-limiter.js +84 -0
- package/lib/utils/rbac.js +74 -0
- package/lib/utils/runner.js +1809 -0
- package/lib/utils/security.js +191 -0
- package/lib/utils/self-healer.js +144 -0
- package/lib/utils/skill-loader.js +255 -0
- package/lib/utils/spinner.js +132 -0
- package/lib/utils/stage-queue.js +50 -0
- package/lib/utils/state-machine.js +89 -0
- package/lib/utils/status-bar.js +327 -0
- package/lib/utils/token-estimator.js +101 -0
- package/lib/utils/ux-analyzer.js +101 -0
- package/lib/utils/webhook-emitter.js +83 -0
- package/lib/web/public/css/styles.css +417 -0
- package/lib/web/public/dark-mode.js +44 -0
- package/lib/web/public/hub/kanban.html +206 -0
- package/lib/web/public/index.html +45 -0
- package/lib/web/public/js/app.js +71 -0
- package/lib/web/public/js/ask.js +110 -0
- package/lib/web/public/js/dashboard.js +165 -0
- package/lib/web/public/js/deploy.js +72 -0
- package/lib/web/public/js/feature.js +79 -0
- package/lib/web/public/js/health.js +65 -0
- package/lib/web/public/js/logs.js +93 -0
- package/lib/web/public/js/review.js +123 -0
- package/lib/web/public/js/ws-client.js +82 -0
- package/lib/web/public/office/css/office.css +678 -0
- package/lib/web/public/office/index.html +148 -0
- package/lib/web/public/office/js/achievements-ui.js +117 -0
- package/lib/web/public/office/js/character.js +1056 -0
- package/lib/web/public/office/js/chat-bubbles.js +177 -0
- package/lib/web/public/office/js/cost-overlay.js +123 -0
- package/lib/web/public/office/js/day-night.js +68 -0
- package/lib/web/public/office/js/effects.js +632 -0
- package/lib/web/public/office/js/engine.js +146 -0
- package/lib/web/public/office/js/feature-ticket.js +216 -0
- package/lib/web/public/office/js/hub-client.js +60 -0
- package/lib/web/public/office/js/main.js +1757 -0
- package/lib/web/public/office/js/office-layout.js +1524 -0
- package/lib/web/public/office/js/pathfinding.js +144 -0
- package/lib/web/public/office/js/pixel-sprites.js +1454 -0
- package/lib/web/public/office/js/progress-bars.js +117 -0
- package/lib/web/public/office/js/replay.js +191 -0
- package/lib/web/public/office/js/sound-effects.js +91 -0
- package/lib/web/public/office/js/sprite-renderer.js +211 -0
- package/lib/web/public/office/js/stamina-system.js +89 -0
- package/lib/web/public/office/js/ui.js +107 -0
- package/lib/web/public/onboarding/index.html +243 -0
- package/lib/web/public/timeline/index.html +195 -0
- package/lib/web/routes/api.js +499 -0
- package/lib/web/routes/logs.js +20 -0
- package/lib/web/routes/metrics.js +99 -0
- package/lib/web/server.js +183 -0
- package/lib/web/ws/handler.js +65 -0
- package/package.json +67 -0
- package/templates/agent-architect.md +69 -0
- package/templates/agent-gemini-pm.md +49 -0
- package/templates/agent-gemini-reviewer.md +52 -0
- package/templates/copilot-instructions.md +36 -0
- package/templates/pipelines/mobile.json +27 -0
- package/templates/pipelines/nodejs-api.json +27 -0
- package/templates/pipelines/python.json +27 -0
- package/templates/pipelines/react.json +27 -0
- package/templates/pipelines/salesforce.json +27 -0
- package/templates/role-gemini.md +97 -0
- package/templates/skill-architect.md +114 -0
- package/templates/skill-browser-qa.md +50 -0
- package/templates/skill-bug-from-qa.md +58 -0
- package/templates/skill-chatbot.md +93 -0
- package/templates/skill-implement.md +78 -0
- package/templates/skill-openclaw.md +174 -0
- package/templates/skill-payment.md +110 -0
- package/templates/skill-pm-spec.md +77 -0
- package/templates/skill-requirement-capture.md +97 -0
- package/templates/skill-review.md +108 -0
- package/templates/skill-reviewer-qa.md +44 -0
- package/templates/skill-suggestion.md +45 -0
- 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
|
+
}
|