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,185 @@
1
+ /**
2
+ * Pipeline Event Bus — central nervous system for web UI + Telegram.
3
+ *
4
+ * Watches .ai-workflow/status.json for changes (1s poll) and emits events
5
+ * that WebSocket clients and the Telegram bot listen to.
6
+ *
7
+ * ZERO changes to existing terminal CLI code — this watches the file on disk.
8
+ */
9
+ import { EventEmitter } from 'events';
10
+ import { existsSync, readFileSync, unwatchFile, watchFile } from 'fs';
11
+ import { dirname, resolve } from 'path';
12
+ import { fileURLToPath } from 'url';
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+
16
+ /**
17
+ * Find the project root by walking up from cwd until aicc.config.js is found.
18
+ * Mirrors the same logic in utils/pipeline.js — MUST stay in sync.
19
+ */
20
+ function findProjectRoot() {
21
+ let dir = process.cwd();
22
+ while (true) {
23
+ if (existsSync(resolve(dir, 'aicc.config.js'))) return dir;
24
+ const parent = dirname(dir);
25
+ if (parent === dir) break;
26
+ dir = parent;
27
+ }
28
+ // Fallback: package dir (works when package IS the project)
29
+ return resolve(__dirname, '../../..');
30
+ }
31
+
32
+ const ROOT = findProjectRoot();
33
+ const STATUS_FILE = resolve(ROOT, '.ai-workflow/status.json');
34
+
35
+ class PipelineBus extends EventEmitter {
36
+ constructor() {
37
+ super();
38
+ this._lastStatus = null;
39
+ this._lastStatusJson = '';
40
+ this._watching = false;
41
+ this._lastStageChangeTime = Date.now();
42
+ this._stallWatchdog = null;
43
+ this._stallCount = 0; // consecutive stall detections for recovery
44
+ this._stallMaxRetries = 2;
45
+ this._stallThresholdMs = 300_000; // 5 minutes per stall detection
46
+ }
47
+
48
+ /** Start watching status.json for changes */
49
+ startWatching() {
50
+ if (this._watching) return;
51
+ this._watching = true;
52
+
53
+ // Initial read
54
+ this._lastStatus = this._readStatus();
55
+ this._lastStatusJson = JSON.stringify(this._lastStatus);
56
+
57
+ // Poll every 1s — negligible cost, works for any writer (terminal, web, telegram)
58
+ watchFile(STATUS_FILE, { interval: 1000 }, () => {
59
+ const newStatus = this._readStatus();
60
+ const newJson = JSON.stringify(newStatus);
61
+
62
+ if (newJson !== this._lastStatusJson) {
63
+ const oldStage = this._lastStatus?.stage;
64
+ this._lastStatus = newStatus;
65
+ this._lastStatusJson = newJson;
66
+
67
+ // Reset stall timer, idle notify timer, and stall counter when the stage changes
68
+ if (newStatus?.stage !== oldStage) {
69
+ this._lastStageChangeTime = Date.now();
70
+ this._lastIdleNotifyTime = Date.now(); // suppress immediate idle heartbeat after stage change
71
+ this._stallCount = 0; // reset consecutive stall counter
72
+ }
73
+
74
+ this.emit('status', {
75
+ status: newStatus,
76
+ previousStage: oldStage,
77
+ timestamp: new Date().toISOString(),
78
+ });
79
+ }
80
+ });
81
+
82
+ // Stall watchdog: notify user if an active pipeline stage hasn't moved in 5 minutes
83
+ const STALL_THRESHOLD_MS = 5 * 60 * 1000;
84
+ const ACTIVE_STAGES = new Set(['spec', 'arch', 'impl', 'review', 'fix', 'deploy']);
85
+ this._lastIdleNotifyTime = Date.now(); // suppress idle notification on first boot
86
+ this._stallWatchdog = setInterval(() => {
87
+ const s = this._lastStatus;
88
+ if (!s?.stage) return;
89
+
90
+ // Active stage stall detection
91
+ if (ACTIVE_STAGES.has(s.stage)) {
92
+ const stalledMs = Date.now() - this._lastStageChangeTime;
93
+ if (stalledMs >= STALL_THRESHOLD_MS) {
94
+ this._stallCount++;
95
+ this.emitEvent('pipeline_stalled', {
96
+ stage: s.stage,
97
+ feature: s.current_feature,
98
+ minutesElapsed: Math.round(stalledMs / 60000),
99
+ stallCount: this._stallCount,
100
+ message: `⚠️ <b>Pipeline stall detected</b>\n\nStage <code>${s.stage}</code> has been running for <b>${Math.round(stalledMs / 60000)} minutes</b> with no progress.\n\nFeature: <code>${s.current_feature || 'unknown'}</code>\n\nThe AI may still be working on a large response — or something may have gone wrong. Use /status to check.`,
101
+ });
102
+
103
+ // After maxRetries consecutive stalls (10 min total), auto-recover
104
+ if (this._stallCount >= this._stallMaxRetries) {
105
+ this._stallCount = 0;
106
+ this._lastStageChangeTime = Date.now(); // prevent immediate re-trigger
107
+ this.emitEvent('pipeline_stall_recovery', {
108
+ action: 'retry_stage',
109
+ stage: s.stage,
110
+ feature: s.current_feature,
111
+ message: `🔄 <b>Auto-recovery</b>\n\nPipeline stalled for ${Math.round(stalledMs / 60000)}min on <code>${s.stage}</code>. Auto-retrying...`,
112
+ });
113
+ }
114
+ }
115
+ return;
116
+ }
117
+
118
+ // Long-idle heartbeat: if pipeline has been idle/deployed for 30+ min,
119
+ // send a single periodic status update so users know the bot is alive.
120
+ // Also covers waiting-for-action stages: review_complete (approve/reject needed), inbox (feature queued).
121
+ const IDLE_STAGES = new Set(['deployed', 'approved', 'rejected', 'review_complete', 'inbox']);
122
+ const IDLE_NOTIFY_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
123
+ if (IDLE_STAGES.has(s.stage)) {
124
+ const sinceLastNotify = Date.now() - (this._lastIdleNotifyTime || 0);
125
+ if (sinceLastNotify >= IDLE_NOTIFY_INTERVAL_MS) {
126
+ this._lastIdleNotifyTime = Date.now();
127
+ const idleMin = Math.round((Date.now() - this._lastStageChangeTime) / 60000);
128
+ const stageEmoji = { idle: '💤', deployed: '🚀', approved: '✅', rejected: '❌', review_complete: '🔍', inbox: '📬' };
129
+ this.emitEvent('idle_heartbeat', {
130
+ stage: s.stage,
131
+ feature: s.current_feature,
132
+ minutesIdle: idleMin,
133
+ message: `${stageEmoji[s.stage] || '📋'} <b>Status check</b> — pipeline is <code>${s.stage}</code> for ${idleMin} min.${s.current_feature ? `\nFeature: <code>${s.current_feature}</code>` : ''}\n\nBot is online and ready. Send a message or /help to get started.`,
134
+ });
135
+ }
136
+ }
137
+ }, 2 * 60 * 1000); // check every 2 minutes
138
+ }
139
+
140
+ /** Stop watching */
141
+ stopWatching() {
142
+ if (!this._watching) return;
143
+ unwatchFile(STATUS_FILE);
144
+ clearInterval(this._stallWatchdog);
145
+ this._stallWatchdog = null;
146
+ this._watching = false;
147
+ }
148
+
149
+ /** Get current pipeline status */
150
+ getStatus() {
151
+ return this._readStatus();
152
+ }
153
+
154
+ /** Emit a pipeline event (review done, deploy result, etc.) */
155
+ emitEvent(event, data = {}) {
156
+ this.emit('pipeline-event', {
157
+ event,
158
+ data,
159
+ timestamp: new Date().toISOString(),
160
+ });
161
+ }
162
+
163
+ /** Emit a log line */
164
+ emitLog(agent, message, type = 'info') {
165
+ this.emit('log', {
166
+ agent,
167
+ message,
168
+ type,
169
+ timestamp: new Date().toISOString(),
170
+ });
171
+ }
172
+
173
+ /** Read status.json safely */
174
+ _readStatus() {
175
+ try {
176
+ if (!existsSync(STATUS_FILE)) return { stage: 'idle', current_feature: null };
177
+ return JSON.parse(readFileSync(STATUS_FILE, 'utf8'));
178
+ } catch {
179
+ return { stage: 'idle', current_feature: null };
180
+ }
181
+ }
182
+ }
183
+
184
+ export const bus = new PipelineBus();
185
+ export const ROOT_DIR = ROOT;
@@ -0,0 +1,378 @@
1
+ /**
2
+ * AI Control Center Slack Bot — pipeline control + notifications via Slack Web API.
3
+ *
4
+ * Uses native fetch() to call Slack Web API (no @slack/bolt dependency).
5
+ * Listens on the event bus and posts updates to a configured channel.
6
+ * Supports slash commands for pipeline control.
7
+ *
8
+ * Setup:
9
+ * 1. Create a Slack App at https://api.slack.com/apps
10
+ * 2. Add Bot Token Scopes: chat:write, commands
11
+ * 3. Install to workspace, get Bot Token
12
+ * 4. Set env vars:
13
+ * export SLACK_BOT_TOKEN=xoxb-...
14
+ * export SLACK_SIGNING_SECRET=...
15
+ * 5. Configure in aicc.config.js:
16
+ * slack: { enabled: true, channel: '#ai-pipeline' }
17
+ */
18
+ import { getConfig } from '../config.js';
19
+ import * as actions from '../shared/action-runner.js';
20
+ import { bus } from '../shared/event-bus.js';
21
+ import { formatCostSummary, getCostSummary, getActiveBudget } from '../utils/cost-tracker.js';
22
+
23
+ const SLACK_API = 'https://slack.com/api';
24
+
25
+ // ─── Helpers ───────────────────────────────────────────────────────────────────
26
+
27
+ /**
28
+ * Call a Slack Web API method via native fetch.
29
+ *
30
+ * @param {string} method — Slack API method (e.g. 'chat.postMessage')
31
+ * @param {string} token — Bot token
32
+ * @param {object} body — JSON payload
33
+ * @returns {Promise<object>}
34
+ */
35
+ async function slackApiCall(method, token, body) {
36
+ const res = await fetch(`${SLACK_API}/${method}`, {
37
+ method: 'POST',
38
+ headers: {
39
+ 'Authorization': `Bearer ${token}`,
40
+ 'Content-Type': 'application/json; charset=utf-8',
41
+ },
42
+ body: JSON.stringify(body),
43
+ });
44
+ const data = await res.json();
45
+ if (!data.ok) {
46
+ throw new Error(`Slack API ${method} failed: ${data.error || 'unknown error'}`);
47
+ }
48
+ return data;
49
+ }
50
+
51
+ // ─── Module-level state ────────────────────────────────────────────────────────
52
+
53
+ let _bot = null;
54
+
55
+ // ─── SlackBot class ────────────────────────────────────────────────────────────
56
+
57
+ /**
58
+ * Slack bot that bridges pipeline events to Slack channels.
59
+ */
60
+ export class SlackBot {
61
+ /**
62
+ * @param {object} config
63
+ * @param {string} config.token — Slack Bot Token (xoxb-...)
64
+ * @param {string} [config.signingSecret] — Slack Signing Secret
65
+ * @param {string} [config.channel] — Default channel to post to
66
+ */
67
+ constructor(config = {}) {
68
+ this._token = config.token;
69
+ this._signingSecret = config.signingSecret || '';
70
+ this._channel = config.channel || '#ai-pipeline';
71
+ this._subscriptions = [];
72
+ this._running = false;
73
+ }
74
+
75
+ /** Start the bot — subscribe to pipeline events and post to Slack */
76
+ start() {
77
+ if (this._running) return;
78
+ this._running = true;
79
+
80
+ const handler = (event) => this._handleEvent(event);
81
+ bus.on('pipeline-event', handler);
82
+ bus.on('status', handler);
83
+ this._subscriptions.push(
84
+ { event: 'pipeline-event', handler },
85
+ { event: 'status', handler },
86
+ );
87
+
88
+ console.log(` 🔔 Slack bot started → ${this._channel}`);
89
+ }
90
+
91
+ /** Stop the bot — unsubscribe from all events */
92
+ stop() {
93
+ this._running = false;
94
+ for (const { event, handler } of this._subscriptions) {
95
+ bus.removeListener(event, handler);
96
+ }
97
+ this._subscriptions = [];
98
+ console.log(' 🔕 Slack bot stopped');
99
+ }
100
+
101
+ /**
102
+ * Handle a pipeline event and post to Slack.
103
+ * @param {object} event
104
+ */
105
+ async _handleEvent(event) {
106
+ if (!this._running || !this._token) return;
107
+
108
+ try {
109
+ // Room messages — forward agent chat to Slack
110
+ const pipelineEvent = event.event;
111
+ if (pipelineEvent === 'room:message' && event.data) {
112
+ const { formatForSlack } = await import('../roleplay/agent-messenger.js');
113
+ const { isRoleplayEnabled } = await import('../roleplay/roleplay-config.js');
114
+ if (isRoleplayEnabled()) {
115
+ const payload = formatForSlack(event.data.role, event.data.message, event.data.type);
116
+ await slackApiCall('chat.postMessage', this._token, { channel: this._channel, ...payload });
117
+ }
118
+ return;
119
+ }
120
+
121
+ const blocks = this._eventToBlocks(event);
122
+ if (blocks) {
123
+ await slackApiCall('chat.postMessage', this._token, {
124
+ channel: this._channel,
125
+ blocks,
126
+ text: event.type || 'Pipeline update',
127
+ });
128
+ }
129
+ } catch (e) {
130
+ console.error(`[Slack] Failed to post event: ${e.message}`);
131
+ }
132
+ }
133
+
134
+ /** Convert a pipeline event to Slack Block Kit blocks */
135
+ _eventToBlocks(event) {
136
+ const type = event.type || event.event;
137
+ if (!type) return null;
138
+
139
+ const emoji = {
140
+ review_done: '📝', deploy_result: '🚀', feature_start: '✨',
141
+ stage_change: '🔄', error: '❌', approval_needed: '⏳',
142
+ idle: '💤', stall: '⚠️',
143
+ };
144
+
145
+ const icon = emoji[type] || 'ℹ️';
146
+ const header = `${icon} ${type.replace(/_/g, ' ').toUpperCase()}`;
147
+
148
+ const blocks = [
149
+ { type: 'header', text: { type: 'plain_text', text: header } },
150
+ ];
151
+
152
+ if (event.data?.message || event.message) {
153
+ blocks.push({
154
+ type: 'section',
155
+ text: { type: 'mrkdwn', text: event.data?.message || event.message },
156
+ });
157
+ }
158
+
159
+ if (event.data?.stage || event.stage) {
160
+ blocks.push({
161
+ type: 'context',
162
+ elements: [{ type: 'mrkdwn', text: `*Stage:* ${event.data?.stage || event.stage}` }],
163
+ });
164
+ }
165
+
166
+ return blocks;
167
+ }
168
+
169
+ /**
170
+ * Format pipeline status as Slack Block Kit.
171
+ * @param {object} status
172
+ * @returns {Array}
173
+ */
174
+ _formatStatus(status) {
175
+ if (!status) return [{ type: 'section', text: { type: 'mrkdwn', text: '⚠️ No status data available' } }];
176
+
177
+ const stageEmoji = {
178
+ idle: '💤', reviewing: '📝', implementing: '🔧',
179
+ deploying: '🚀', testing: '🧪', fixing: '🔨',
180
+ };
181
+ const icon = stageEmoji[status.stage] || '❓';
182
+
183
+ return [
184
+ { type: 'header', text: { type: 'plain_text', text: `${icon} Pipeline Status` } },
185
+ {
186
+ type: 'section',
187
+ fields: [
188
+ { type: 'mrkdwn', text: `*Stage:*\n${status.stage || 'unknown'}` },
189
+ { type: 'mrkdwn', text: `*Feature:*\n${status.current_feature || '—'}` },
190
+ { type: 'mrkdwn', text: `*Auto-pilot:*\n${status.auto_pilot ? '✅ On' : '❌ Off'}` },
191
+ { type: 'mrkdwn', text: `*Checkpoint:*\n${status.checkpoint || '—'}` },
192
+ ],
193
+ },
194
+ ];
195
+ }
196
+
197
+ /**
198
+ * Format health data as Slack Block Kit.
199
+ * @param {object} health
200
+ * @returns {Array}
201
+ */
202
+ _formatHealth(health) {
203
+ if (!health) return [{ type: 'section', text: { type: 'mrkdwn', text: '⚠️ No health data available' } }];
204
+
205
+ const overallIcon = health.status === 'healthy' ? '🟢' : health.status === 'degraded' ? '🟡' : '🔴';
206
+
207
+ const blocks = [
208
+ { type: 'header', text: { type: 'plain_text', text: `${overallIcon} System Health` } },
209
+ ];
210
+
211
+ if (health.checks && typeof health.checks === 'object') {
212
+ const lines = Object.entries(health.checks).map(([name, check]) => {
213
+ const icon = check.status === 'ok' ? '✅' : check.status === 'warn' ? '⚠️' : '❌';
214
+ return `${icon} *${name}:* ${check.message || check.status}`;
215
+ });
216
+ blocks.push({ type: 'section', text: { type: 'mrkdwn', text: lines.join('\n') } });
217
+ }
218
+
219
+ return blocks;
220
+ }
221
+ }
222
+
223
+ // ─── Slash command handler ─────────────────────────────────────────────────────
224
+
225
+ /**
226
+ * Process a slash command and return a response.
227
+ *
228
+ * @param {string} command — the command name (e.g. 'status', 'deploy')
229
+ * @param {string} args — additional arguments
230
+ * @param {string} userId — Slack user ID
231
+ * @returns {Promise<{text:string, blocks?:Array}>}
232
+ */
233
+ export async function handleSlashCommand(command, args, userId) {
234
+ const cmd = (command || '').trim().toLowerCase();
235
+
236
+ try {
237
+ switch (cmd) {
238
+ case 'status': {
239
+ const status = await actions.getStatusData();
240
+ const bot = new SlackBot({});
241
+ return { text: 'Pipeline status', blocks: bot._formatStatus(status) };
242
+ }
243
+
244
+ case 'feature': {
245
+ if (!args?.trim()) return { text: '❌ Usage: `/aicc feature <description>`' };
246
+ const result = await actions.runNewFeature(args.trim());
247
+ return { text: `✨ Feature started: ${args.trim()}`, blocks: [
248
+ { type: 'section', text: { type: 'mrkdwn', text: `✨ *Feature started:* ${args.trim()}` } },
249
+ ] };
250
+ }
251
+
252
+ case 'deploy': {
253
+ await actions.runDeploy();
254
+ return { text: '🚀 Deploy initiated' };
255
+ }
256
+
257
+ case 'approve': {
258
+ await actions.runApprove();
259
+ return { text: '✅ Approved' };
260
+ }
261
+
262
+ case 'reject': {
263
+ const reason = args?.trim() || 'Rejected via Slack';
264
+ await actions.runReject(reason);
265
+ return { text: `❌ Rejected: ${reason}` };
266
+ }
267
+
268
+ case 'review': {
269
+ await actions.runReview();
270
+ return { text: '📝 Review triggered' };
271
+ }
272
+
273
+ case 'health': {
274
+ const health = await actions.getHealthData();
275
+ const bot = new SlackBot({});
276
+ return { text: 'System health', blocks: bot._formatHealth(health) };
277
+ }
278
+
279
+ case 'cost': {
280
+ const summary = getCostSummary();
281
+ const text = formatCostSummary(summary);
282
+ return { text, blocks: [
283
+ { type: 'section', text: { type: 'mrkdwn', text: `\`\`\`\n${text}\n\`\`\`` } },
284
+ ] };
285
+ }
286
+
287
+ case 'retry': {
288
+ const fresh = args?.trim().toLowerCase() === 'fresh';
289
+ await actions.retryFromCheckpoint(fresh);
290
+ return { text: `🔄 Retry initiated${fresh ? ' (fresh)' : ''}` };
291
+ }
292
+
293
+ default:
294
+ return {
295
+ text: '🤖 *AICC Commands:*\n' +
296
+ '• `status` — pipeline status\n' +
297
+ '• `feature <desc>` — create feature\n' +
298
+ '• `deploy` — deploy\n' +
299
+ '• `approve` — approve\n' +
300
+ '• `reject <reason>` — reject\n' +
301
+ '• `review` — trigger review\n' +
302
+ '• `health` — health check\n' +
303
+ '• `cost` — cost breakdown\n' +
304
+ '• `retry [fresh]` — retry from checkpoint',
305
+ };
306
+ }
307
+ } catch (e) {
308
+ return { text: `❌ Error: ${e.message}` };
309
+ }
310
+ }
311
+
312
+ // ─── Public API ────────────────────────────────────────────────────────────────
313
+
314
+ /**
315
+ * Send a message to a Slack channel.
316
+ *
317
+ * @param {string} channel — Channel name or ID
318
+ * @param {string} text — Fallback text
319
+ * @param {Array} [blocks] — Slack Block Kit blocks
320
+ */
321
+ export async function sendSlackMessage(channel, text, blocks) {
322
+ let token;
323
+ try {
324
+ const cfg = getConfig();
325
+ token = cfg.slack?.token || process.env.SLACK_BOT_TOKEN;
326
+ } catch {
327
+ token = process.env.SLACK_BOT_TOKEN;
328
+ }
329
+
330
+ if (!token) throw new Error('Slack bot token not configured');
331
+
332
+ const body = { channel, text };
333
+ if (blocks) body.blocks = blocks;
334
+ return slackApiCall('chat.postMessage', token, body);
335
+ }
336
+
337
+ /**
338
+ * Start the Slack bot.
339
+ *
340
+ * @param {object} [options]
341
+ * @param {string} [options.token] — Slack Bot Token
342
+ * @param {string} [options.signingSecret] — Slack Signing Secret
343
+ * @param {string} [options.channel] — Default channel
344
+ * @returns {SlackBot}
345
+ */
346
+ export function startSlackBot(options = {}) {
347
+ if (_bot) { _bot.stop(); }
348
+
349
+ let slackConfig = {};
350
+ try {
351
+ const cfg = getConfig();
352
+ slackConfig = cfg.slack || {};
353
+ } catch { /* use options only */ }
354
+
355
+ const config = {
356
+ token: options.token || slackConfig.token || process.env.SLACK_BOT_TOKEN,
357
+ signingSecret: options.signingSecret || slackConfig.signingSecret || process.env.SLACK_SIGNING_SECRET,
358
+ channel: options.channel || slackConfig.channel || '#ai-pipeline',
359
+ };
360
+
361
+ if (!config.token) {
362
+ console.error(' ✗ Slack bot token not configured.');
363
+ console.error(' Set SLACK_BOT_TOKEN env var or slack.token in aicc.config.js');
364
+ return null;
365
+ }
366
+
367
+ _bot = new SlackBot(config);
368
+ _bot.start();
369
+ return _bot;
370
+ }
371
+
372
+ /** Stop the Slack bot */
373
+ export function stopSlackBot() {
374
+ if (_bot) {
375
+ _bot.stop();
376
+ _bot = null;
377
+ }
378
+ }