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,1267 @@
1
+ /**
2
+ * Telegram bot command handlers.
3
+ *
4
+ * Full parity with the terminal CLI main menu:
5
+ * 1. New Feature /feature <desc>
6
+ * 2. Fix a Bug /bug <desc>
7
+ * 3. Review Code /review
8
+ * 4. Approve /approve
9
+ * 5. Reject /reject <reason>
10
+ * 6. Deploy /deploy
11
+ * 7. Debug / Logs /logs
12
+ * 8. Pipeline Status /status
13
+ * 9. Clean Up /cleanup
14
+ * A. Toggle Auto-Pilot /autopilot
15
+ * R. Reset / Abandon /reset
16
+ * Health Check /health
17
+ * Main Menu /menu
18
+ * Ask AI /ask <question>
19
+ */
20
+ import { getConfig } from '../config.js';
21
+ import * as actions from '../shared/action-runner.js';
22
+ import { bus } from '../shared/event-bus.js';
23
+ import { formatCostSummary, getCostSummary, getActiveBudget } from '../utils/cost-tracker.js';
24
+ import { formatForPlatform } from '../utils/format.js';
25
+ import { learnPhrase, matchIntent, matchLearnedPhrase } from '../utils/intent-engine.js';
26
+ import {
27
+ confirmActionKeyboard,
28
+ deployKeyboard,
29
+ docPageKeyboard,
30
+ docsKeyboard,
31
+ featureModeKeyboard,
32
+ logPageKeyboard,
33
+ logsKeyboard,
34
+ mainMenuKeyboard,
35
+ reviewActionsKeyboard
36
+ } from './keyboards.js';
37
+
38
+ /** Emit a user action event so hub can push it to office canvas */
39
+ function emitUserAction(action, extra = {}) {
40
+ bus.emitEvent('user_action', { action, ...extra });
41
+ }
42
+
43
+ // Helper: send AI markdown response formatted for Telegram HTML
44
+ function replyAI(ctx, text) {
45
+ const { text: formatted, options } = formatForPlatform(text, 'telegram');
46
+ // Split at 4000 chars but don't break mid-HTML-tag
47
+ const chunks = formatted.match(/[\s\S]{1,4000}/g) || ['No response.'];
48
+ return Promise.all(chunks.map(chunk => ctx.reply(chunk, options)));
49
+ }
50
+
51
+ // Persistent pending feature storage โ€” survives bot restarts.
52
+ // Falls back to in-memory Map for projects where getWorkflowDir() isn't available yet.
53
+ const _pendingFallback = new Map();
54
+
55
+ function setPendingFeature(chatId, pending) {
56
+ _pendingFallback.set(chatId, pending); // always keep in-memory copy too
57
+ try { actions.savePendingChat(chatId, pending); } catch { /* non-fatal */ }
58
+ }
59
+
60
+ function getPendingFeature(chatId) {
61
+ // Disk-backed first, fallback to in-memory
62
+ try {
63
+ const disk = actions.loadPendingChat(chatId);
64
+ if (disk) return disk;
65
+ } catch { /* non-fatal */ }
66
+ return _pendingFallback.get(chatId) || null;
67
+ }
68
+
69
+ function deletePendingFeature(chatId) {
70
+ _pendingFallback.delete(chatId);
71
+ try { actions.clearPendingChat(chatId); } catch { /* non-fatal */ }
72
+ }
73
+
74
+ // Per-chat conversation history (in-memory, survives bot restarts via OpenClaw session)
75
+ const chatHistories = new Map();
76
+ const MAX_HISTORY_MESSAGES = 12; // last 6 exchanges
77
+
78
+ function getHistory(chatId) {
79
+ return chatHistories.get(chatId) || [];
80
+ }
81
+
82
+ function addToHistory(chatId, role, content) {
83
+ const history = getHistory(chatId);
84
+ history.push({ role, content: content.slice(0, 600) });
85
+ if (history.length > MAX_HISTORY_MESSAGES) history.splice(0, history.length - MAX_HISTORY_MESSAGES);
86
+ chatHistories.set(chatId, history);
87
+ }
88
+
89
+ function buildHistoryContext(chatId) {
90
+ const history = getHistory(chatId);
91
+ if (!history.length) return '';
92
+ return 'Recent conversation (for context):\n' +
93
+ history.map(m => `[${m.role === 'user' ? 'User' : 'Assistant'}]: ${m.content}`).join('\n');
94
+ }
95
+
96
+ export function registerCommands(bot) {
97
+
98
+ // โ”€โ”€ /start โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
99
+ bot.command('start', (ctx) => {
100
+ console.log(` Chat ID: ${ctx.chat.id} (save this as ${getConfig().envPrefix}_TELEGRAM_CHAT_ID)`);
101
+ ctx.reply(
102
+ `*${getConfig().name} AI Pipeline*\n\n` +
103
+ `*Pipeline Actions:*\n` +
104
+ `/feature \\<desc\\> โ€” New Feature\n` +
105
+ `/bug \\<desc\\> โ€” Fix a Bug\n` +
106
+ `/review โ€” Review Code \\(Reviewer\\)\n` +
107
+ `/approve โ€” Approve current feature\n` +
108
+ `/reject \\<reason\\> โ€” Reject / Request Fixes\n\n` +
109
+ `*Deploy & Monitor:*\n` +
110
+ `/deploy โ€” Deploy to org\n` +
111
+ `/status โ€” Pipeline status\n` +
112
+ `/logs โ€” Recent session log entries\n` +
113
+ `/health โ€” System health check\n\n` +
114
+ `*Management:*\n` +
115
+ `/autopilot โ€” Toggle auto\\-pilot\n` +
116
+ `/cleanup โ€” Clean up workspace\n` +
117
+ `/reset โ€” Reset / Abandon feature\n` +
118
+ `*Local AI \\(Ollama\\):*\n` +
119
+ `/ask \\<question\\> โ€” Ask local AI\n` +
120
+ `/aimode โ€” Toggle AI mode \\(hybrid/cloud/local\\)\n\n` +
121
+ `*Observability:*\n` +
122
+ `/costs โ€” AI usage \\& cost summary\n` +
123
+ `/audit โ€” Recent audit log\n\n` +
124
+ `/menu โ€” Show main menu buttons\n\n` +
125
+ `_Pipeline events are pushed automatically\\._`,
126
+ { parse_mode: 'MarkdownV2' }
127
+ );
128
+ });
129
+
130
+ // โ”€โ”€ /menu โ€” inline keyboard with all options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
131
+ bot.command('menu', (ctx) => {
132
+ ctx.reply('Main Menu โ€” tap an action:', { reply_markup: mainMenuKeyboard() });
133
+ });
134
+
135
+ // โ”€โ”€ /status โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
136
+ bot.command('status', async (ctx) => {
137
+ const s = actions.getStatusData();
138
+ const stage = s.stage || 'idle';
139
+ const feature = s.current_feature || 'none';
140
+ const mode = s.pipeline_mode || 'manual';
141
+ const next = s.next || '-';
142
+ const updated = s.updated_at ? new Date(s.updated_at).toLocaleTimeString() : '-';
143
+
144
+ const stageEmoji = {
145
+ idle: '๐Ÿ’ค',
146
+ inbox: '๐Ÿ“ฅ',
147
+ spec_complete: '๐Ÿ“',
148
+ arch_complete: '๐Ÿ—',
149
+ implementation_complete: 'โšก',
150
+ implementation_failed: 'โŒ',
151
+ review_complete: '๐Ÿ”',
152
+ approved: 'โœ…',
153
+ rejected: 'โŒ',
154
+ deployed: '๐Ÿš€',
155
+ };
156
+
157
+ const icon = stageEmoji[stage] || '๐Ÿ”„';
158
+ const modeIcon = mode === 'auto' ? 'โšก auto' : '๐Ÿ– manual';
159
+
160
+ // Show banned models if any
161
+ const banned = s._bannedModels || [];
162
+ const bannedLine = banned.length > 0
163
+ ? `\nโš ๏ธ <b>Rate-limited:</b> ${banned.map(b => `${b.model} (${b.type}, clears ~${b.clearsIn})`).join(', ')}\n`
164
+ : '';
165
+
166
+ // Predictive pipeline duration
167
+ let predictionLine = '';
168
+ try {
169
+ const { predictCompletion, formatPrediction } = await import('../utils/pipeline.js');
170
+ const { getCostEntries } = await import('../utils/cost-tracker.js');
171
+ if (stage && stage !== 'idle') {
172
+ const entries = getCostEntries();
173
+ const prediction = predictCompletion(stage, entries);
174
+ predictionLine = '\n' + formatPrediction(prediction);
175
+ }
176
+ } catch { /* prediction is optional */ }
177
+
178
+ ctx.reply(
179
+ `${icon} <b>Pipeline Status</b>\n\n` +
180
+ `Stage: <code>${stage}</code>\n` +
181
+ `Feature: <code>${feature}</code>\n` +
182
+ `Mode: ${modeIcon}\n` +
183
+ `Next: ${next}\n` +
184
+ `Updated: ${updated}\n` +
185
+ bannedLine + predictionLine + `\n` +
186
+ `Use /docs to view pipeline documents\n` +
187
+ `Use /logs for recent activity`,
188
+ { parse_mode: 'HTML' }
189
+ );
190
+ });
191
+
192
+ // โ”€โ”€ /leaderboard โ€” AI model performance leaderboard โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
193
+ bot.command('leaderboard', async (ctx) => {
194
+ try {
195
+ const { getLeaderboard, formatLeaderboard } = await import('../utils/agent-leaderboard.js');
196
+ const entries = getLeaderboard();
197
+ const text = formatLeaderboard(entries);
198
+ await ctx.reply(text, { parse_mode: undefined });
199
+ } catch (e) {
200
+ await ctx.reply(`โŒ Leaderboard error: ${e.message}`);
201
+ }
202
+ });
203
+
204
+ // โ”€โ”€ /docs โ€” Browse pipeline documents โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
205
+ bot.command('docs', (ctx) => {
206
+ const docs = actions.listAvailableDocs();
207
+ if (docs.length === 0) {
208
+ ctx.reply('No pipeline documents yet. Start a feature first with /feature or /bug');
209
+ return;
210
+ }
211
+ ctx.reply('๐Ÿ“‚ <b>Pipeline Documents</b>\n\nChoose a document to view:', {
212
+ parse_mode: 'HTML',
213
+ reply_markup: docsKeyboard(docs),
214
+ });
215
+ });
216
+
217
+ // โ”€โ”€ doc: callbacks โ€” paginated document viewer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
218
+ bot.callbackQuery('doc:list', async (ctx) => {
219
+ try { await ctx.answerCallbackQuery(); } catch { /* query expired */ }
220
+ const docs = actions.listAvailableDocs();
221
+ if (docs.length === 0) {
222
+ ctx.editMessageText('No pipeline documents yet.');
223
+ return;
224
+ }
225
+ ctx.editMessageText('๐Ÿ“‚ <b>Pipeline Documents</b>\n\nChoose a document to view:', {
226
+ parse_mode: 'HTML',
227
+ reply_markup: docsKeyboard(docs),
228
+ });
229
+ });
230
+
231
+ bot.callbackQuery(/^doc:view:(.+):(\d+)$/, async (ctx) => {
232
+ const key = ctx.match[1];
233
+ const page = parseInt(ctx.match[2], 10);
234
+ try { await ctx.answerCallbackQuery(); } catch { /* query expired */ }
235
+
236
+ const doc = actions.readDocPaged(key, page);
237
+ if (!doc) {
238
+ ctx.editMessageText('Document not found.');
239
+ return;
240
+ }
241
+
242
+ const header = `๐Ÿ“„ <b>${doc.name}</b> (${doc.page + 1}/${doc.totalPages}, ${doc.totalChars} chars)\n\n`;
243
+ // Escape HTML special chars in the document content
244
+ const safe = doc.content
245
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
246
+
247
+ ctx.editMessageText(header + `<code>${safe}</code>`, {
248
+ parse_mode: 'HTML',
249
+ reply_markup: docPageKeyboard(key, doc.page, doc.totalPages),
250
+ });
251
+ });
252
+
253
+
254
+ bot.command('feature', (ctx) => {
255
+ const desc = ctx.message.text.replace(/^\/feature\s*/, '').trim();
256
+ if (!desc || desc.length < 10) {
257
+ ctx.reply('Usage: `/feature <description>`\n\nDescription must be at least 10 characters.\n\nExample: `/feature Add MYOB inbound sync for invoices`', { parse_mode: 'Markdown' });
258
+ return;
259
+ }
260
+ setPendingFeature(ctx.chat.id, { description: desc, type: 'feature' });
261
+ ctx.reply(`New Feature: "${desc}"\n\nSelect pipeline mode:`, { reply_markup: featureModeKeyboard('feature') });
262
+ });
263
+
264
+ // โ”€โ”€ /bug <description> โ€” Fix a Bug โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
265
+ bot.command('bug', (ctx) => {
266
+ const desc = ctx.message.text.replace(/^\/bug\s*/, '').trim();
267
+ if (!desc || desc.length < 10) {
268
+ ctx.reply('Usage: `/bug <description>`\n\nDescription must be at least 10 characters.\n\nExample: `/bug Circuit breaker not resetting after cooldown`', { parse_mode: 'Markdown' });
269
+ return;
270
+ }
271
+ setPendingFeature(ctx.chat.id, { description: desc, type: 'bug' });
272
+ ctx.reply(`Bug Fix: "${desc}"\n\nSelect pipeline mode:`, { reply_markup: featureModeKeyboard('bug') });
273
+ });
274
+
275
+ // Handle feature/bug mode selection callback
276
+ bot.callbackQuery(/^fmode:(feature|bug):(manual|auto)$/, async (ctx) => {
277
+ const type = ctx.match[1];
278
+ const mode = ctx.match[2];
279
+ try { await ctx.answerCallbackQuery(); } catch { /* query expired */ }
280
+
281
+ const pending = getPendingFeature(ctx.chat.id);
282
+ if (!pending) {
283
+ ctx.reply('Session expired. Please submit the feature/bug again.');
284
+ return;
285
+ }
286
+ deletePendingFeature(ctx.chat.id);
287
+
288
+ ctx.reply(`Starting ${type === 'bug' ? 'bug fix' : 'feature'} pipeline (${mode} mode)... This may take several minutes.`);
289
+ emitUserAction('feature_submitted', { type, mode, username: ctx.from?.username, description: pending.description.slice(0, 80) });
290
+
291
+ const result = await actions.runNewFeature(pending.description, mode, pending.type);
292
+ if (result.success) {
293
+ ctx.reply(`Pipeline started!\n\nFeature: \`${result.featureId}\`\nMode: ${mode}\nStage: \`${result.status?.stage}\``, { parse_mode: 'Markdown' });
294
+ } else {
295
+ ctx.reply(`Failed to start: ${result.error}`);
296
+ }
297
+ });
298
+
299
+ // โ”€โ”€ /review โ€” trigger code review โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
300
+ bot.command('review', async (ctx) => {
301
+ const status = actions.getStatusData();
302
+ const hasFeature = !!status.current_feature;
303
+ const canReview = hasFeature && [
304
+ 'inbox', 'spec_complete', 'arch_complete',
305
+ 'implementation_complete', 'rejected', 'review_complete',
306
+ ].includes(status.stage);
307
+
308
+ if (!canReview) {
309
+ // Show latest review if one exists
310
+ const review = actions.getLatestReview();
311
+ if (review) {
312
+ const preview = review.content.slice(0, 500).replace(/[_*[\]()~`>#+=|{}.!-]/g, '\\$&');
313
+ ctx.reply(
314
+ `*Latest Review: ${review.verdict}*\n\nFile: \`${review.name}\`\n\n${preview}...`,
315
+ { parse_mode: 'Markdown' }
316
+ );
317
+ } else {
318
+ ctx.reply('No active feature to review. Start a feature first with `/feature <description>`', { parse_mode: 'Markdown' });
319
+ }
320
+ return;
321
+ }
322
+
323
+ ctx.reply('Starting code review... This may take a few minutes.');
324
+
325
+ // JS-native review โ€” no shell script dependency
326
+ try {
327
+ const result = await actions.runReview();
328
+ if (!result.success) {
329
+ ctx.reply(`Review failed: ${result.error}`);
330
+ return;
331
+ }
332
+ const review = actions.getLatestReview();
333
+ if (!review) {
334
+ ctx.reply('Review finished but no review file found.');
335
+ return;
336
+ }
337
+
338
+ const verdict = review.verdict === 'APPROVED' ? 'APPROVED' : 'REJECTED';
339
+ const preview = review.content.slice(0, 800);
340
+ ctx.reply(`Review complete: *${verdict}*\n\n\`\`\`\n${preview}\n\`\`\``, { parse_mode: 'Markdown' });
341
+
342
+ if (review.verdict === 'APPROVED') {
343
+ ctx.reply('Feature approved by reviewer! What next?', { reply_markup: reviewActionsKeyboard() });
344
+ return;
345
+ }
346
+
347
+ // โ”€โ”€ REJECTED โ†’ auto-fix then re-review (one cycle) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
348
+ ctx.reply('๐Ÿ”ง Coder is fixing the blockers from the review...');
349
+ const fixResult = await actions.runFix();
350
+ if (!fixResult.success) {
351
+ ctx.reply(
352
+ `โŒ Auto-fix failed: ${fixResult.error}\n\nUse /reject <instructions> to send specific fixes to Coder, or /review to retry manually.`
353
+ );
354
+ return;
355
+ }
356
+
357
+ ctx.reply('โœ… Fixes applied โ€” running review again...');
358
+ const result2 = await actions.runReview();
359
+ if (!result2.success) {
360
+ ctx.reply(`Re-review failed: ${result2.error}`);
361
+ return;
362
+ }
363
+ const review2 = actions.getLatestReview();
364
+ if (!review2) {
365
+ ctx.reply('Re-review finished but no review file found.');
366
+ return;
367
+ }
368
+
369
+ const verdict2 = review2.verdict === 'APPROVED' ? 'APPROVED' : 'REJECTED';
370
+ const preview2 = review2.content.slice(0, 800);
371
+ ctx.reply(`Re-review complete: *${verdict2}*\n\n\`\`\`\n${preview2}\n\`\`\``, { parse_mode: 'Markdown' });
372
+
373
+ if (review2.verdict === 'APPROVED') {
374
+ ctx.reply('โœ… Feature approved after fixes! What next?', { reply_markup: reviewActionsKeyboard() });
375
+ } else {
376
+ ctx.reply(
377
+ 'โš ๏ธ Still rejected after one fix cycle.\n\nOptions:\n' +
378
+ 'โ€ข /reject <specific instructions> โ€” give Coder targeted guidance\n' +
379
+ 'โ€ข /review โ€” run another fix+review cycle\n' +
380
+ 'โ€ข /approve โ€” force-approve and deploy anyway'
381
+ );
382
+ }
383
+ } catch (err) {
384
+ ctx.reply(`Review failed: ${err.message}`);
385
+ }
386
+ });
387
+
388
+ // โ”€โ”€ /implement โ€” Manually trigger or restart Coder implementation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
389
+ bot.command('implement', async (ctx) => {
390
+ emitUserAction('implement_start', { username: ctx.from?.username });
391
+ const s = actions.getStatusData();
392
+ const allowedStages = ['arch_complete', 'implementation_failed'];
393
+ if (!allowedStages.includes(s.stage)) {
394
+ ctx.reply(
395
+ `โš ๏ธ Cannot start implementation from stage <code>${s.stage}</code>\n\n` +
396
+ `Implementation can only run from:\n` +
397
+ `โ€ข <code>arch_complete</code> โ€” after architecture is done\n` +
398
+ `โ€ข <code>implementation_failed</code> โ€” to retry a failed run\n\n` +
399
+ `Current stage: <code>${s.stage}</code> (next: <code>${s.next || '-'}</code>)`,
400
+ { parse_mode: 'HTML' }
401
+ );
402
+ return;
403
+ }
404
+ ctx.reply('โšก Starting Coder implementationโ€ฆ');
405
+ const result = await actions.runImplementation();
406
+ if (!result.success) {
407
+ ctx.reply(`โŒ Implementation failed: ${result.error}`);
408
+ }
409
+ // Success notifications come via the event bus / pipeline push-events
410
+ });
411
+
412
+ // โ”€โ”€ /approve โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
413
+ bot.command('approve', async (ctx) => {
414
+ emitUserAction('approve', { username: ctx.from?.username });
415
+ const result = await actions.runApprove();
416
+ if (result.success) {
417
+ ctx.reply('Feature approved! Ready to deploy.', { reply_markup: deployKeyboard() });
418
+ } else {
419
+ ctx.reply(`Cannot approve: ${result.error}`);
420
+ }
421
+ });
422
+
423
+ // โ”€โ”€ /reject <reason> โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
424
+ bot.command('reject', async (ctx) => {
425
+ const reason = ctx.message.text.replace(/^\/reject\s*/, '').trim();
426
+ if (!reason) {
427
+ ctx.reply('Usage: `/reject <reason>`\n\nExample: `/reject Missing error handling in main service`', { parse_mode: 'Markdown' });
428
+ return;
429
+ }
430
+ emitUserAction('reject', { reason, username: ctx.from?.username });
431
+ const result = await actions.runReject(reason);
432
+ if (result.success) {
433
+ ctx.reply(`Rejected. Reason: ${reason}\n\nFixes will be dispatched to Coder.`);
434
+ } else {
435
+ ctx.reply(`Cannot reject: ${result.error}`);
436
+ }
437
+ });
438
+
439
+ // โ”€โ”€ /deploy โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
440
+ bot.command('deploy', (ctx) => {
441
+ emitUserAction('deploy_start', { username: ctx.from?.username });
442
+ ctx.reply('Deploy โ€” select test level:', { reply_markup: deployKeyboard() });
443
+ });
444
+
445
+ bot.callbackQuery(/^deploy:(.+)$/, async (ctx) => {
446
+ const testLevel = ctx.match[1];
447
+ try { await ctx.answerCallbackQuery(); } catch { /* query expired */ }
448
+ emitUserAction('deploy_confirm', { testLevel, username: ctx.from?.username });
449
+ ctx.reply(`Deploying with ${testLevel}... (this may take a few minutes)`);
450
+
451
+ const result = await actions.runDeploy(testLevel);
452
+ if (result.success) {
453
+ ctx.reply('Deploy successful!');
454
+ } else {
455
+ const error = result.stderr?.slice(0, 300) || result.error || 'Unknown error';
456
+ ctx.reply(`Deploy failed:\n\`\`\`\n${error}\n\`\`\``, { parse_mode: 'Markdown' });
457
+ }
458
+ });
459
+
460
+ // โ”€โ”€ /logs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
461
+ bot.command('logs', (ctx) => {
462
+ const sessions = actions.listAvailableLogs();
463
+ if (sessions.length === 0) {
464
+ ctx.reply('No session logs available yet.');
465
+ return;
466
+ }
467
+ ctx.reply('๐Ÿ—’ <b>Session Logs</b>\n\nChoose a log to view:', { parse_mode: 'HTML', reply_markup: logsKeyboard(sessions) });
468
+ });
469
+
470
+ // โ”€โ”€ /health โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
471
+ bot.command('health', async (ctx) => {
472
+ const data = await actions.getHealthData();
473
+ const ok = (v) => v ? 'โœ“' : 'โœ—';
474
+
475
+ let ollamaLine = `ollama: ${ok(data.ollama?.available)}`;
476
+ if (data.ollama?.available) {
477
+ ollamaLine += ` (${data.ollama.model}, ${data.ollama.models?.length || 0} models)`;
478
+ }
479
+
480
+ ctx.reply(
481
+ `*System Health*\n\n` +
482
+ `*Cloud AI:*\n` +
483
+ `gemini: ${ok(data.gemini?.available)}\n` +
484
+ `claude: ${ok(data.claude?.available)}\n` +
485
+ `copilot: ${ok(data.copilot?.available)}\n\n` +
486
+ `*Local AI:*\n` +
487
+ `${ollamaLine}\n` +
488
+ `AI mode: ${data.ollama?.mode || 'hybrid'}\n\n` +
489
+ `*Tools:*\n` +
490
+ `sf: ${ok(data.sf?.available)}\n` +
491
+ `gh: ${ok(data.gh?.available)}\n\n` +
492
+ `*Workflow Dirs:*\n` +
493
+ `ai-workflow: ${ok(data.workflow?.aiWorkflow)}\n` +
494
+ `skills: ${ok(data.workflow?.skills)}\n` +
495
+ `agents: ${ok(data.workflow?.agents)}\n\n` +
496
+ `Stage: \`${data.pipeline?.stage || 'idle'}\``,
497
+ { parse_mode: 'Markdown' }
498
+ );
499
+ });
500
+
501
+ // โ”€โ”€ /autopilot โ€” Toggle auto-pilot โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
502
+ bot.command('autopilot', (ctx) => {
503
+ const result = actions.toggleAutoPilot();
504
+ if (result.success) {
505
+ const icon = result.mode === 'auto' ? 'โšก' : 'โธ';
506
+ ctx.reply(`${icon} Auto-Pilot is now *${result.mode.toUpperCase()}*`, { parse_mode: 'Markdown' });
507
+ } else {
508
+ ctx.reply(`Cannot toggle: ${result.error}`);
509
+ }
510
+ });
511
+
512
+ // โ”€โ”€ /cleanup โ€” Clean up workspace โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
513
+ bot.command('cleanup', async (ctx) => {
514
+ ctx.reply('Cleaning up workspace...');
515
+ const result = await actions.runCleanup();
516
+ if (result.success) {
517
+ ctx.reply('Workspace cleaned up successfully.');
518
+ } else {
519
+ ctx.reply(`Cleanup failed: ${result.error}`);
520
+ }
521
+ });
522
+
523
+ // โ”€โ”€ /reset โ€” Reset / Abandon feature โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
524
+ bot.command('reset', (ctx) => {
525
+ const result = actions.runReset();
526
+ if (result.success) {
527
+ ctx.reply(`Feature "${result.feature}" abandoned. Pipeline reset to idle.`);
528
+ } else {
529
+ ctx.reply(`Cannot reset: ${result.error}`);
530
+ }
531
+ });
532
+
533
+ // โ”€โ”€ /retry โ€” Retry current stage from checkpoint โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
534
+ bot.command('retry', async (ctx) => {
535
+ const arg = ctx.message.text.replace(/^\/retry\s*/, '').trim().toLowerCase();
536
+ const fresh = arg === 'fresh';
537
+ const result = await actions.retryFromCheckpoint(fresh);
538
+ if (result.success) {
539
+ ctx.reply(`๐Ÿ”„ ${result.message}`, { parse_mode: 'HTML' });
540
+ } else {
541
+ ctx.reply(`โŒ Cannot retry: ${result.error}`);
542
+ }
543
+ });
544
+
545
+ // โ”€โ”€ /cost โ€” Show cost breakdown and budget info โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
546
+ bot.command('cost', (ctx) => {
547
+ const status = actions.getStatusData();
548
+ const featureId = status.current_feature;
549
+ const summary = getCostSummary(featureId);
550
+ const budget = featureId ? getActiveBudget(featureId) : null;
551
+
552
+ let msg = formatCostSummary(summary, 'html');
553
+
554
+ if (budget) {
555
+ msg += `\n\n๐Ÿ“Š <b>Token Budget</b>\n`;
556
+ msg += `${budget.formatGauge()}\n`;
557
+ msg += `Tokens: ${budget.usedTokens.toLocaleString()} / ${budget.maxTokens.toLocaleString()}\n`;
558
+ if (budget.savedByCheckpoints > 0) {
559
+ msg += `Saved by checkpoints: ~${budget.savedByCheckpoints.toLocaleString()} tokens\n`;
560
+ }
561
+ }
562
+
563
+ ctx.reply(msg, { parse_mode: 'HTML' });
564
+ });
565
+
566
+ // โ”€โ”€ /dryrun โ€” Run pipeline dry run โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
567
+ bot.command('dryrun', async (ctx) => {
568
+ const desc = ctx.message.text.replace(/^\/dryrun\s*/, '').trim() || 'Test feature (dry run)';
569
+ ctx.reply('๐Ÿงช Starting dry run...');
570
+ const result = await actions.runDryRun(desc);
571
+ if (result.success) {
572
+ ctx.reply(`โœ… ${result.message}`, { parse_mode: 'HTML' });
573
+ } else {
574
+ ctx.reply(`โŒ Dry run failed: ${result.error}`);
575
+ }
576
+ });
577
+
578
+ // โ”€โ”€ /ask <question> โ€” Ask local AI (Ollama) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
579
+ bot.command('ask', async (ctx) => {
580
+ const question = ctx.message.text.replace(/^\/ask\s*/, '').trim();
581
+ if (!question) {
582
+ ctx.reply('Usage: `/ask <question>`\n\nExample: `/ask How does the authentication flow work?`', { parse_mode: 'Markdown' });
583
+ return;
584
+ }
585
+
586
+ try {
587
+ await ctx.replyWithChatAction('typing');
588
+ const typingInterval = setInterval(() => ctx.replyWithChatAction('typing').catch(() => {}), 4000);
589
+ addToHistory(ctx.chat.id, 'user', question);
590
+ const result = await actions.askAI(question, buildHistoryContext(ctx.chat.id)).finally(() => clearInterval(typingInterval));
591
+ if (typeof result === 'string') {
592
+ addToHistory(ctx.chat.id, 'assistant', result);
593
+ await replyAI(ctx, result);
594
+ } else if (result?.error) {
595
+ await ctx.reply(`AI error: ${result.error}`);
596
+ } else {
597
+ await ctx.reply('No response from AI.');
598
+ }
599
+ } catch (err) {
600
+ console.error(` [BOT] Reply failed: ${err.message}`);
601
+ }
602
+ });
603
+
604
+ // โ”€โ”€ /aimode โ€” Toggle AI mode (hybrid/cloud/local) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
605
+ bot.command('aimode', async (ctx) => {
606
+ const status = await actions.getAIProviderStatus();
607
+ const ollamaOk = status.ollama.available ? 'โœ“' : 'โœ—';
608
+ const models = status.ollama.models?.slice(0, 5).join(', ') || 'none';
609
+
610
+ ctx.reply(
611
+ `*AI Mode: ${status.mode.toUpperCase()}*\n\n` +
612
+ `Ollama: ${ollamaOk} (model: \`${status.ollama.model}\`)\n` +
613
+ `Installed: ${models}\n\n` +
614
+ `Modes:\n` +
615
+ `โ€ข \`hybrid\` โ€” Ollama for light tasks, cloud for heavy\n` +
616
+ `โ€ข \`cloud\` โ€” Cloud AI only (skip Ollama)\n` +
617
+ `โ€ข \`local\` โ€” Ollama only (no cloud)\n\n` +
618
+ `Set mode: \`${getConfig().envPrefix}_AI_MODE=hybrid\` env var\n` +
619
+ `Set model: \`OLLAMA_MODEL=llama3.1\` env var`,
620
+ { parse_mode: 'Markdown' }
621
+ );
622
+ });
623
+
624
+ // โ”€โ”€ /costs โ€” AI usage cost summary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
625
+ bot.command('costs', (ctx) => {
626
+ const featureId = ctx.match?.trim() || undefined;
627
+ const data = actions.getCostsData(featureId);
628
+ if (!data || !data.summary) {
629
+ ctx.reply('No cost data recorded yet.');
630
+ return;
631
+ }
632
+ const s = data.summary;
633
+ const title = featureId ? `๐Ÿ’ฐ Costs: <code>${featureId}</code>` : '๐Ÿ’ฐ Overall AI Costs';
634
+ const lines = [
635
+ title,
636
+ '',
637
+ `Total calls: <b>${s.totalCalls}</b>`,
638
+ `Est. cost: <b>$${s.totalCost?.toFixed(4) || '0.0000'}</b>`,
639
+ `Tokens (est): ${s.totalInputTokens || 0} in / ${s.totalOutputTokens || 0} out`,
640
+ ];
641
+ if (s.byProvider && Object.keys(s.byProvider).length) {
642
+ lines.push('', '<b>By provider:</b>');
643
+ for (const [prov, info] of Object.entries(s.byProvider)) {
644
+ lines.push(` โ€ข ${prov}: ${info.calls} calls, $${info.cost?.toFixed(4) || '0'}`);
645
+ }
646
+ }
647
+ ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
648
+ });
649
+
650
+ // โ”€โ”€ /audit โ€” Recent audit log entries โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
651
+ bot.command('audit', (ctx) => {
652
+ const arg = ctx.match?.trim() || '';
653
+ const filters = { limit: 15 };
654
+ if (arg) filters.event = arg.toUpperCase();
655
+ const data = actions.getAuditData(filters);
656
+ if (!data || !data.length) {
657
+ ctx.reply(arg ? `No audit entries for event: ${arg}` : 'No audit entries yet.');
658
+ return;
659
+ }
660
+ const lines = ['๐Ÿ“‹ <b>Audit Log</b> (last 15)', ''];
661
+ for (const e of data.slice(0, 15)) {
662
+ const ts = new Date(e.timestamp).toLocaleTimeString();
663
+ lines.push(`<code>${ts}</code> <b>${e.event}</b> ${e.featureId || ''}`);
664
+ }
665
+ if (arg) lines.push(`\nFilter: <code>${arg.toUpperCase()}</code>`);
666
+ lines.push('\nUsage: /audit [EVENT_TYPE]');
667
+ ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
668
+ });
669
+
670
+ // โ”€โ”€ /assign โ€” Assign project to AI IT department โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
671
+ bot.command('assign', async (ctx) => {
672
+ const text = (ctx.match || '').trim();
673
+ const parts = text.split(/\s+/);
674
+ const url = parts[0];
675
+ const goal = parts.slice(1).join(' ');
676
+ if (!url || !goal) {
677
+ ctx.reply('Usage: /assign &lt;url&gt; &lt;goal&gt;\nExample: /assign https://myapp.com Build e-commerce store', { parse_mode: 'HTML', link_preview_options: { is_disabled: true } });
678
+ return;
679
+ }
680
+ await ctx.reply(`Starting project assignment for ${url}...`, { link_preview_options: { is_disabled: true } });
681
+ try {
682
+ const { assignProjectAction } = await import('../actions/assign-project.js');
683
+ const result = await assignProjectAction({ url, goal });
684
+ if (!result.success) {
685
+ await ctx.reply('Assignment failed.');
686
+ return;
687
+ }
688
+ await ctx.reply(`Project assigned: ${result.projectId}\nStarting Browser QA audit...`, { link_preview_options: { is_disabled: true } });
689
+
690
+ // Auto-kick browser QA
691
+ const cfg = getConfig();
692
+ if (cfg.browserQA?.enabled) {
693
+ try {
694
+ const qaResult = await actions.runBrowserQA({ featureId: result.projectId, config: cfg });
695
+ if (qaResult.skipped) {
696
+ await ctx.reply('Browser QA skipped (not configured).');
697
+ } else if (qaResult.report?.summary) {
698
+ const s = qaResult.report.summary;
699
+ const icon = s.failed === 0 ? 'โœ…' : 'โŒ';
700
+ await ctx.reply(`${icon} QA: ${s.passed}/${s.totalRoutes} passed (${s.passRate}%)${s.failed > 0 ? ` โ€” ${s.failed} issue(s) found` : ' โ€” No issues!'}`);
701
+
702
+ if (qaResult.needsBugfix) {
703
+ const maxCycles = cfg.browserQA?.maxBugfixCycles || 3;
704
+ for (let cycle = 1; cycle <= maxCycles; cycle++) {
705
+ await ctx.reply(`๐Ÿ”ง Bug-fix cycle ${cycle}/${maxCycles}...`);
706
+ try {
707
+ const { fixBugAction } = await import('../actions/fix-bug.js');
708
+ await fixBugAction({ featureId: result.projectId, fromQA: true });
709
+ const reQA = await actions.runBrowserQA({ featureId: result.projectId, config: cfg });
710
+ if (!reQA.needsBugfix || reQA.failCount === 0) {
711
+ await ctx.reply(`โœ… Bugs fixed after ${cycle} cycle(s)!`);
712
+ break;
713
+ }
714
+ if (cycle === maxCycles) {
715
+ await ctx.reply(`โš ๏ธ ${maxCycles} cycles exhausted. ${reQA.failCount} issue(s) remain. Escalating.`);
716
+ }
717
+ } catch (fixErr) {
718
+ await ctx.reply(`Fix error: ${fixErr.message.slice(0, 100)}`);
719
+ break;
720
+ }
721
+ }
722
+ }
723
+ }
724
+ } catch (qaErr) {
725
+ await ctx.reply(`QA error: ${qaErr.message.slice(0, 150)}`);
726
+ }
727
+ } else {
728
+ await ctx.reply('Browser QA disabled. Enable in aicc.config.js to auto-test.');
729
+ }
730
+ } catch (err) {
731
+ await ctx.reply(`Assignment failed: ${err.message.slice(0, 150)}`);
732
+ }
733
+ });
734
+
735
+ // โ”€โ”€ /suggest โ€” Run suggestion agent โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
736
+ bot.command('suggest', async (ctx) => {
737
+ await ctx.reply('Running suggestion analysis...');
738
+ try {
739
+ const { runSuggestionAgent } = await import('../agents/suggestion-agent.js');
740
+ const result = await runSuggestionAgent();
741
+ if (result.success && result.suggestions?.length > 0) {
742
+ const titles = result.suggestions.map((s, i) => `${i + 1}. ${s.title}`).join('\n');
743
+ await ctx.reply(`Feature suggestions:\n${titles}`);
744
+ } else if (result.skipped) {
745
+ await ctx.reply('Suggestion agent is disabled in config.');
746
+ } else {
747
+ await ctx.reply('No suggestions generated.');
748
+ }
749
+ } catch (err) {
750
+ await ctx.reply(`Suggestion failed: ${err.message.slice(0, 150)}`);
751
+ }
752
+ });
753
+
754
+ // โ”€โ”€ /qa โ€” Run browser QA test โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
755
+ bot.command('qa', async (ctx) => {
756
+ await ctx.reply('Starting browser QA...');
757
+ try {
758
+ const result = await actions.runBrowserQA();
759
+ if (result.skipped) {
760
+ await ctx.reply('Browser QA is disabled. Enable in aicc.config.js: browserQA.enabled = true');
761
+ } else if (result.report?.summary) {
762
+ const s = result.report.summary;
763
+ await ctx.reply(`QA complete: ${s.passed}/${s.totalRoutes} passed (${s.passRate}% pass rate). ${s.failed > 0 ? `${s.failed} issues found.` : 'No issues!'}`);
764
+ } else {
765
+ await ctx.reply(`QA result: ${result.error || 'unknown'}`);
766
+ }
767
+ } catch (err) {
768
+ await ctx.reply(`QA failed: ${err.message.slice(0, 150)}`);
769
+ }
770
+ });
771
+
772
+ // โ”€โ”€ /threads โ€” List open discussion threads โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
773
+ bot.command('threads', async (ctx) => {
774
+ try {
775
+ const { getOpenThreads } = await import('../roleplay/discussion-threads.js');
776
+ const threads = getOpenThreads();
777
+ if (threads.length === 0) {
778
+ await ctx.reply('No open discussion threads.');
779
+ return;
780
+ }
781
+ const lines = threads.slice(0, 10).map(t =>
782
+ `โ€ข ${t.title} (${t.status}, ${t.messages.length} msg)`
783
+ );
784
+ await ctx.reply(`Open threads:\n${lines.join('\n')}`);
785
+ } catch (err) {
786
+ await ctx.reply(`Failed to list threads: ${err.message.slice(0, 100)}`);
787
+ }
788
+ });
789
+
790
+ // โ”€โ”€ Inline keyboard callbacks from /menu โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
791
+ bot.callbackQuery(/^menu:(.+)$/, async (ctx) => {
792
+ const action = ctx.match[1];
793
+ try { await ctx.answerCallbackQuery(); } catch { /* query expired */ }
794
+
795
+ switch (action) {
796
+ case 'feature':
797
+ ctx.reply('Send a feature description:\n`/feature <description>`', { parse_mode: 'Markdown' });
798
+ break;
799
+ case 'bug':
800
+ ctx.reply('Send a bug description:\n`/bug <description>`', { parse_mode: 'Markdown' });
801
+ break;
802
+ case 'review': {
803
+ // Trigger review inline
804
+ const status = actions.getStatusData();
805
+ const canReview = status.current_feature && [
806
+ 'inbox', 'spec_complete', 'arch_complete',
807
+ 'implementation_complete', 'rejected', 'review_complete',
808
+ ].includes(status.stage);
809
+ if (!canReview) {
810
+ const review = actions.getLatestReview();
811
+ ctx.reply(review ? `Latest: *${review.verdict}* โ€” \`${review.name}\`` : 'No review available.', { parse_mode: 'Markdown' });
812
+ } else {
813
+ ctx.reply('Starting review... use `/review` for full output.', { parse_mode: 'Markdown' });
814
+ }
815
+ break;
816
+ }
817
+ case 'status': {
818
+ const s = actions.getStatusData();
819
+ const stageEmoji = { idle:'๐Ÿ’ค', inbox:'๐Ÿ“ฅ', spec_complete:'๐Ÿ“', arch_complete:'๐Ÿ—', implementation_complete:'โšก', review_complete:'๐Ÿ”', approved:'โœ…', rejected:'โŒ', deployed:'๐Ÿš€' };
820
+ const icon = stageEmoji[s.stage] || '๐Ÿ”„';
821
+ ctx.reply(`${icon} <code>${s.stage || 'idle'}</code>\n๐Ÿ“ฆ <code>${s.current_feature || 'none'}</code>\n๐Ÿ”ง ${s.pipeline_mode || 'manual'} mode`, { parse_mode: 'HTML' });
822
+ break;
823
+ }
824
+ case 'docs': {
825
+ const docs = actions.listAvailableDocs();
826
+ if (docs.length === 0) {
827
+ ctx.reply('No pipeline documents yet.');
828
+ } else {
829
+ ctx.reply('๐Ÿ“‚ <b>Pipeline Documents</b>\n\nChoose a document to view:', { parse_mode: 'HTML', reply_markup: docsKeyboard(docs) });
830
+ }
831
+ break;
832
+ }
833
+ case 'approve': {
834
+ const r = await actions.runApprove();
835
+ ctx.reply(r.success ? 'Feature approved!' : `Cannot approve: ${r.error}`);
836
+ break;
837
+ }
838
+ case 'reject':
839
+ ctx.reply('Send rejection reason:\n`/reject <reason>`', { parse_mode: 'Markdown' });
840
+ break;
841
+ case 'deploy':
842
+ ctx.reply('Select test level:', { reply_markup: deployKeyboard() });
843
+ break;
844
+ case 'logs': {
845
+ const sessions = actions.listAvailableLogs();
846
+ if (sessions.length === 0) {
847
+ ctx.reply('No session logs available yet.');
848
+ } else {
849
+ ctx.reply('๐Ÿ—’ <b>Session Logs</b>\n\nChoose a log to view:', { parse_mode: 'HTML', reply_markup: logsKeyboard(sessions) });
850
+ }
851
+ break;
852
+ }
853
+ case 'autopilot': {
854
+ const r = actions.toggleAutoPilot();
855
+ ctx.reply(r.success ? `Auto-Pilot: *${r.mode.toUpperCase()}*` : r.error, { parse_mode: 'Markdown' });
856
+ break;
857
+ }
858
+ case 'cleanup': {
859
+ ctx.reply('Cleaning up...');
860
+ const r = await actions.runCleanup();
861
+ ctx.reply(r.success ? 'Done!' : `Failed: ${r.error}`);
862
+ break;
863
+ }
864
+ case 'health': {
865
+ const d = await actions.getHealthData();
866
+ const ok = (v) => v ? 'โœ“' : 'โœ—';
867
+ ctx.reply(`gemini: ${ok(d.gemini?.available)} | claude: ${ok(d.claude?.available)} | sf: ${ok(d.sf?.available)} | Stage: \`${d.pipeline?.stage || 'idle'}\``, { parse_mode: 'Markdown' });
868
+ break;
869
+ }
870
+ case 'reset': {
871
+ const r = actions.runReset();
872
+ ctx.reply(r.success ? `"${r.feature}" abandoned.` : r.error);
873
+ break;
874
+ }
875
+ case 'ask':
876
+ ctx.reply('Ask AI a question:\n`/ask <question>`', { parse_mode: 'Markdown' });
877
+ break;
878
+ case 'aimode': {
879
+ const ai = await actions.getAIProviderStatus();
880
+ ctx.reply(`AI Mode: *${ai.mode.toUpperCase()}* | Ollama: ${ai.ollama.available ? 'โœ“' : 'โœ—'} (\`${ai.ollama.model}\`)`, { parse_mode: 'Markdown' });
881
+ break;
882
+ }
883
+ }
884
+ });
885
+
886
+ // โ”€โ”€ Action button callbacks (inline keyboard shortcuts) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
887
+ bot.callbackQuery(/^action:(approve|reject|deploy|review|fix|docs|status)$/, async (ctx) => {
888
+ const action = ctx.match[1];
889
+ try { await ctx.answerCallbackQuery(`Running: ${action}...`); } catch { /* query expired */ }
890
+
891
+ if (action === 'approve') {
892
+ const r = await actions.runApprove();
893
+ ctx.reply(r.success ? 'โœ… Feature approved!' : `โŒ Cannot approve: ${r.error}`);
894
+ } else if (action === 'reject') {
895
+ ctx.reply('Send rejection reason:\n<code>/reject &lt;reason&gt;</code>', { parse_mode: 'HTML' });
896
+ } else if (action === 'deploy') {
897
+ ctx.reply('๐Ÿš€ Starting deployment...');
898
+ const r = await actions.runDeploy('RunLocalTests');
899
+ ctx.reply(r.success ? 'โœ… Deployment complete!' : `โŒ Deploy failed: ${r.error?.slice(0, 200) || 'unknown'}`);
900
+ } else if (action === 'review') {
901
+ ctx.reply('๐Ÿ” Starting code review...');
902
+ const r = await actions.runReview();
903
+ ctx.reply(r.success ? 'โœ… Review complete โ€” use /docs to read it.' : `โŒ Review failed: ${r.error?.slice(0, 200) || 'unknown'}`);
904
+ } else if (action === 'fix') {
905
+ ctx.reply('๐Ÿ”ง Running auto-fix...');
906
+ const r = await actions.runFix();
907
+ ctx.reply(r.success ? 'โœ… Fix complete โ€” run /review again to re-check.' : `โŒ Fix failed: ${r.error?.slice(0, 200) || 'unknown'}`);
908
+ } else if (action === 'docs') {
909
+ const docs = actions.listAvailableDocs();
910
+ if (docs.length === 0) {
911
+ ctx.reply('No pipeline documents yet.');
912
+ } else {
913
+ const { InlineKeyboard } = await import('grammy');
914
+ const kb = new InlineKeyboard();
915
+ docs.slice(0, 8).forEach((d, i) => kb.text(d.label, `doc:view:${i}:0`).row());
916
+ ctx.reply('๐Ÿ“‚ <b>Pipeline Documents</b>\n\nChoose a document:', { parse_mode: 'HTML', reply_markup: kb });
917
+ }
918
+ } else if (action === 'status') {
919
+ const status = actions.getStatusData();
920
+ const stage = status.stage || 'unknown';
921
+ const feature = status.current_feature || '-';
922
+ const mode = status.mode === 'auto' ? 'โšก auto' : '๐Ÿ– manual';
923
+ ctx.reply(`๐Ÿ“‹ <b>Status</b>\n\nStage: <code>${stage}</code>\nFeature: <code>${feature}</code>\nMode: ${mode}`, { parse_mode: 'HTML' });
924
+ }
925
+ });
926
+
927
+ // โ”€โ”€ Log list callback โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
928
+ bot.callbackQuery('log:list', async (ctx) => {
929
+ try { await ctx.answerCallbackQuery(); } catch { /* query expired */ }
930
+ const sessions = actions.listAvailableLogs();
931
+ if (sessions.length === 0) {
932
+ ctx.reply('No session logs available yet.');
933
+ } else {
934
+ ctx.editMessageText('๐Ÿ—’ <b>Session Logs</b>\n\nChoose a log to view:', { parse_mode: 'HTML', reply_markup: logsKeyboard(sessions) });
935
+ }
936
+ });
937
+
938
+ // โ”€โ”€ Log page view callback โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
939
+ bot.callbackQuery(/^log:view:(\d+):(\d+)$/, async (ctx) => {
940
+ try { await ctx.answerCallbackQuery(); } catch { /* query expired */ }
941
+ const [, idx, pageStr] = ctx.match;
942
+ const page = parseInt(pageStr, 10) || 0;
943
+ const result = actions.readLogPaged(idx, page);
944
+ if (!result) {
945
+ ctx.reply('Log not found.');
946
+ return;
947
+ }
948
+ const header = `๐Ÿ—’ <b>${result.name}</b> โ€” Page ${result.page + 1}/${result.totalPages} (${result.totalLines} lines)\n\n`;
949
+ const body = `<pre>${result.content.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</pre>`;
950
+ const text = header + body;
951
+ ctx.editMessageText(text, { parse_mode: 'HTML', reply_markup: logPageKeyboard(idx, result.page, result.totalPages) });
952
+ });
953
+
954
+ // โ”€โ”€ Opus model confirmation callbacks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
955
+ bot.callbackQuery(/^opus:(confirm|decline):(.+)$/, async (ctx) => {
956
+ const [, choice, featureId] = ctx.match;
957
+ try { await ctx.answerCallbackQuery(); } catch { /* query expired */ }
958
+ const confirmed = choice === 'confirm';
959
+ actions.resolveOpusConfirmation(featureId, confirmed);
960
+ if (confirmed) {
961
+ ctx.editMessageText(
962
+ `โœ… Using <b>Claude Opus 4.6</b> for architecture (premium).\n\nPipeline continuing...`,
963
+ { parse_mode: 'HTML' }
964
+ );
965
+ } else {
966
+ ctx.editMessageText(
967
+ `โšก Architect is working on the design.\n\nPipeline continuing...`,
968
+ { parse_mode: 'HTML' }
969
+ );
970
+ }
971
+ });
972
+
973
+ // โ”€โ”€ Review loop confirmation callbacks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
974
+ bot.callbackQuery(/^loop:(continue|stop):(.+)$/, async (ctx) => {
975
+ const [, choice, featureId] = ctx.match;
976
+ try { await ctx.answerCallbackQuery(); } catch { /* query expired */ }
977
+ const continueLoop = choice === 'continue';
978
+ actions.resolveLoopConfirmation(featureId, continueLoop);
979
+ if (continueLoop) {
980
+ ctx.editMessageText(
981
+ `๐Ÿ” Continuing auto-pilot โ€” Coder will fix and re-review...`,
982
+ { parse_mode: 'HTML' }
983
+ );
984
+ } else {
985
+ ctx.editMessageText(
986
+ `๐Ÿ›‘ Auto-pilot stopped. Use /reject to send specific fixes to Coder when ready.`,
987
+ { parse_mode: 'HTML' }
988
+ );
989
+ }
990
+ });
991
+
992
+ // โ”€โ”€ Feature/Bug confirmation button callbacks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
993
+ bot.callbackQuery(/^confirm:(.+)$/, async (ctx) => {
994
+ const data = ctx.match[1];
995
+ try { await ctx.answerCallbackQuery(); } catch { /* query expired */ }
996
+
997
+ // confirm:no โ€” user is just chatting
998
+ if (data === 'no') {
999
+ ctx.editMessageText('๐Ÿ’ฌ Got it โ€” just chatting! Ask me anything.');
1000
+ return;
1001
+ }
1002
+
1003
+ // confirm:{type}:yes:{encodedDesc} or confirm:{type}:edit:{encodedDesc}
1004
+ const parts = data.split(':');
1005
+ if (parts.length < 3) return;
1006
+ const [type, choice, ...descParts] = parts;
1007
+ const desc = decodeURIComponent(descParts.join(':'));
1008
+
1009
+ if (choice === 'yes') {
1010
+ // Proceed to pipeline mode selection (same as /feature or /bug flow)
1011
+ setPendingFeature(ctx.chat.id, { description: desc, type });
1012
+ const label = type === 'bug' ? '๐Ÿ› Bug Fix' : 'โœจ Feature';
1013
+ ctx.editMessageText(`${label}: "${desc}"\n\nSelect pipeline mode:`, {
1014
+ parse_mode: 'HTML',
1015
+ reply_markup: featureModeKeyboard(type),
1016
+ });
1017
+ emitUserAction('confirm_feature', { type, description: desc, username: ctx.from?.username });
1018
+ // Learn this interaction
1019
+ learnPhrase(desc, type === 'bug' ? 'fix' : 'implement', { type });
1020
+ } else if (choice === 'edit') {
1021
+ ctx.editMessageText(`โœ๏ธ Sure! Send me a refined description for your ${type === 'bug' ? 'bug fix' : 'feature'}:`);
1022
+ }
1023
+ });
1024
+
1025
+ // โ”€โ”€ Pipeline action confirmation button callbacks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1026
+ bot.callbackQuery(/^pipeconfirm:(.+)$/, async (ctx) => {
1027
+ const data = ctx.match[1];
1028
+ try { await ctx.answerCallbackQuery(); } catch { /* query expired */ }
1029
+
1030
+ // pipeconfirm:no โ€” user is just chatting
1031
+ if (data === 'no') {
1032
+ ctx.editMessageText('๐Ÿ’ฌ Got it โ€” just chatting!');
1033
+ return;
1034
+ }
1035
+
1036
+ // pipeconfirm:{action}:yes
1037
+ const parts = data.split(':');
1038
+ if (parts.length < 2) return;
1039
+ const action = parts[0];
1040
+
1041
+ const pipeActions = {
1042
+ run_fix: async () => { ctx.editMessageText('๐Ÿ”ง Running Coder fix...'); const r = await actions.runFix(); await ctx.reply(r.success ? 'โœ… Coder is on it.' : `โŒ Fix failed: ${r.error}`); },
1043
+ run_review: async () => { ctx.editMessageText('๐Ÿ” Starting code review...'); const r = await actions.runReview(); if (!r.success) await ctx.reply(`โŒ Review failed: ${r.error}`); },
1044
+ run_fix_and_review: async () => {
1045
+ ctx.editMessageText('๐Ÿ”ง Running Coder fix...');
1046
+ const fr = await actions.runFix();
1047
+ if (!fr.success) { await ctx.reply(`โŒ Fix failed: ${fr.error}`); return; }
1048
+ await ctx.reply('โœ… Fixes applied โ€” running review...');
1049
+ const rr = await actions.runReview();
1050
+ if (!rr.success) await ctx.reply(`โŒ Review failed: ${rr.error}`);
1051
+ },
1052
+ run_approve: async () => { ctx.editMessageText('โœ… Approving...'); const r = await actions.runApprove(); await ctx.reply(r.success ? 'โœ… Feature approved!' : `โŒ ${r.error}`); },
1053
+ run_deploy: async () => { ctx.editMessageText('๐Ÿš€ Deploying...'); const r = await actions.runDeploy('RunLocalTests'); await ctx.reply(r.success ? 'โœ… Deployed!' : `โŒ ${r.error?.slice(0,200)}`); },
1054
+ run_implement: async () => { const st = actions.getStatusData(); ctx.editMessageText(`โšก Triggering Coder for ${st.current_feature}โ€ฆ`); const r = await actions.runImplementation(); if (!r.success) await ctx.reply(`โŒ ${r.error}`); },
1055
+ };
1056
+
1057
+ if (pipeActions[action]) {
1058
+ emitUserAction(action, { username: ctx.from?.username, trigger: 'confirmed_button' });
1059
+ await pipeActions[action]();
1060
+ // Learn this interaction
1061
+ learnPhrase(action, action.replace('run_', ''), {});
1062
+ }
1063
+ });
1064
+
1065
+ // โ”€โ”€ Fallback: plain text โ†’ treat as /ask (natural conversation) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1066
+ bot.on('message:text', async (ctx) => {
1067
+ const text = (ctx.message.text || '').trim();
1068
+ if (!text || text.startsWith('/')) return;
1069
+
1070
+ // โ”€โ”€ Roleplay: dispatch CEO message to the room and stop (PM handles it) โ”€โ”€โ”€โ”€โ”€โ”€
1071
+ try {
1072
+ const { isRoleplayEnabled } = await import('../roleplay/roleplay-config.js');
1073
+ if (isRoleplayEnabled()) {
1074
+ const { dispatchCEOMessage } = await import('../roleplay/room.js');
1075
+ dispatchCEOMessage(text, ctx);
1076
+ return; // PM owns the response โ€” don't also trigger the AI pipeline
1077
+ }
1078
+ } catch { /* roleplay optional โ€” non-fatal */ }
1079
+
1080
+ // โ”€โ”€ Natural language โ†’ pipeline action dispatcher (dynamic intent engine) โ”€โ”€
1081
+ // Uses learnable patterns instead of hardcoded regex. Learned phrases are
1082
+ // checked first (fastest, from past interactions), then dynamic patterns.
1083
+ const s = actions.getStatusData();
1084
+ const learned = matchLearnedPhrase(text);
1085
+ const intent = learned || matchIntent(text, s);
1086
+
1087
+ if (intent) {
1088
+ const trigger = learned ? 'learned_phrase' : 'natural_language';
1089
+ const intentActions = {
1090
+ fix_and_review: async () => {
1091
+ await ctx.reply('๐Ÿ”ง Running Coder fix...');
1092
+ emitUserAction('fix_start', { username: ctx.from?.username, trigger });
1093
+ const fr = await actions.runFix();
1094
+ if (!fr.success) { await ctx.reply(`โŒ Fix failed: ${fr.error}`); return; }
1095
+ await ctx.reply('โœ… Fixes applied โ€” running review...');
1096
+ emitUserAction('review_start', { username: ctx.from?.username, trigger });
1097
+ const rr = await actions.runReview();
1098
+ if (!rr.success) await ctx.reply(`โŒ Review failed: ${rr.error}`);
1099
+ },
1100
+ fix: async () => {
1101
+ await ctx.reply('๐Ÿ”ง Dispatching fixes to Coder...');
1102
+ emitUserAction('fix_start', { username: ctx.from?.username, trigger });
1103
+ const r = await actions.runFix();
1104
+ await ctx.reply(r.success ? 'โœ… Coder is on it.' : `โŒ Fix failed: ${r.error}`);
1105
+ },
1106
+ review: async () => {
1107
+ await ctx.reply('๐Ÿ” Starting code review...');
1108
+ emitUserAction('review_start', { username: ctx.from?.username, trigger });
1109
+ const r = await actions.runReview();
1110
+ if (!r.success) await ctx.reply(`โŒ Review failed: ${r.error}`);
1111
+ },
1112
+ approve: async () => {
1113
+ await ctx.reply('โœ… Approving feature...');
1114
+ emitUserAction('approve', { username: ctx.from?.username, trigger });
1115
+ const r = await actions.runApprove();
1116
+ await ctx.reply(r.success ? 'โœ… Feature approved!' : `โŒ Cannot approve: ${r.error}`);
1117
+ },
1118
+ deploy: async () => {
1119
+ await ctx.reply('๐Ÿš€ Starting deployment...');
1120
+ emitUserAction('deploy_start', { username: ctx.from?.username, trigger });
1121
+ const r = await actions.runDeploy('RunLocalTests');
1122
+ await ctx.reply(r.success ? 'โœ… Deployment complete!' : `โŒ Deploy failed: ${r.error?.slice(0, 200) || 'unknown'}`);
1123
+ },
1124
+ implement: async () => {
1125
+ await ctx.reply(`โšก Triggering Coder implementation for <code>${s.current_feature}</code>โ€ฆ`, { parse_mode: 'HTML' });
1126
+ emitUserAction('implement_start', { username: ctx.from?.username, trigger });
1127
+ const r = await actions.runImplementation();
1128
+ if (!r.success) await ctx.reply(`โŒ Implementation failed: ${r.error}`);
1129
+ },
1130
+ rewrite_docs: async () => {
1131
+ await ctx.reply('๐Ÿ”„ Rewriting spec and architecture docs...');
1132
+ emitUserAction('rewrite_docs_start', { username: ctx.from?.username, trigger });
1133
+ const r = await actions.runRewriteDocs(text);
1134
+ if (!r.success) await ctx.reply(`โŒ Rewrite failed: ${r.error}`);
1135
+ },
1136
+ reset: async () => {
1137
+ emitUserAction('reset', { username: ctx.from?.username, trigger });
1138
+ const r = actions.runReset();
1139
+ if (r.success) {
1140
+ await ctx.reply(`๐Ÿ”„ Pipeline reset to idle. Feature "${r.feature}" abandoned.`);
1141
+ } else {
1142
+ // No active feature โ€” just force idle
1143
+ try { actions.runCleanup(); } catch {}
1144
+ await ctx.reply('๐Ÿ’ค Pipeline is now idle.');
1145
+ }
1146
+ },
1147
+ status: async () => {
1148
+ const st = actions.getStatusData();
1149
+ const stageEmoji = { idle:'๐Ÿ’ค', inbox:'๐Ÿ“ฅ', spec_complete:'๐Ÿ“', arch_complete:'๐Ÿ—', implementation_complete:'โšก', review_complete:'๐Ÿ”', approved:'โœ…', rejected:'โŒ', deployed:'๐Ÿš€' };
1150
+ const icon = stageEmoji[st.stage] || '๐Ÿ”„';
1151
+ await ctx.reply(`${icon} Stage: <code>${st.stage || 'idle'}</code>\nFeature: <code>${st.current_feature || 'none'}</code>\nMode: ${st.pipeline_mode || 'manual'}`, { parse_mode: 'HTML' });
1152
+ },
1153
+ cleanup: async () => {
1154
+ await ctx.reply('๐Ÿงน Cleaning up workspace...');
1155
+ emitUserAction('cleanup', { username: ctx.from?.username, trigger });
1156
+ const r = await actions.runCleanup();
1157
+ await ctx.reply(r.success ? 'โœ… Workspace cleaned up.' : `โŒ Cleanup failed: ${r.error}`);
1158
+ },
1159
+ };
1160
+
1161
+ const handler = intentActions[intent.intent];
1162
+ if (handler) {
1163
+ await handler();
1164
+ learnPhrase(text, intent.intent, { trigger });
1165
+ return;
1166
+ }
1167
+ }
1168
+
1169
+ try {
1170
+ // Show typing indicator โ€” refresh every 4s while AI is thinking
1171
+ await ctx.replyWithChatAction('typing');
1172
+ const typingInterval = setInterval(() => ctx.replyWithChatAction('typing').catch(() => {}), 4000);
1173
+ addToHistory(ctx.chat.id, 'user', text);
1174
+ const result = await actions.askAI(text, buildHistoryContext(ctx.chat.id)).finally(() => clearInterval(typingInterval));
1175
+ if (typeof result === 'string') {
1176
+ addToHistory(ctx.chat.id, 'assistant', result);
1177
+ // Strip markdown code fences then find _action JSON (AI may wrap JSON in ```json...```)
1178
+ const stripped = result.replace(/```(?:json)?\s*/gi, '').replace(/```/g, '');
1179
+ // Use greedy match to handle descriptions containing special chars
1180
+ const actionMatch = stripped.match(/\{"_action"\s*:\s*"[^"]+".+?\}(?=\s|$)/s);
1181
+ // Debug: log what AI returned so we can diagnose action parsing failures
1182
+ console.error(` [BOT] AI response (${stripped.length} chars): ${stripped.slice(0, 300).replace(/\n/g, '\\n')}${stripped.length > 300 ? '...' : ''}`);
1183
+ if (!actionMatch) {
1184
+ const hasAction = stripped.includes('_action');
1185
+ if (hasAction) {
1186
+ console.error(` [BOT] _action found in response but regex failed to parse. Response (last 500): ${stripped.slice(-500)}`);
1187
+ }
1188
+ }
1189
+ if (actionMatch) {
1190
+ let action;
1191
+ try {
1192
+ action = JSON.parse(actionMatch[0]);
1193
+ } catch {
1194
+ // Try to extract valid JSON by finding balanced braces
1195
+ const start = stripped.indexOf('{"_action"');
1196
+ if (start >= 0) {
1197
+ let depth = 0;
1198
+ let end = start;
1199
+ for (let i = start; i < stripped.length; i++) {
1200
+ if (stripped[i] === '{') depth++;
1201
+ else if (stripped[i] === '}') { depth--; if (depth === 0) { end = i + 1; break; } }
1202
+ }
1203
+ try { action = JSON.parse(stripped.slice(start, end)); } catch { /* give up */ }
1204
+ }
1205
+ }
1206
+ if (!action) {
1207
+ console.error(` [BOT] Failed to parse _action JSON: ${actionMatch[0].slice(0, 300)}`);
1208
+ }
1209
+ if (action) {
1210
+ const explanation = stripped.slice(0, actionMatch.index).trim();
1211
+
1212
+ // โ”€โ”€ Pipeline execution actions (AI decided to run something) โ”€โ”€โ”€โ”€โ”€โ”€
1213
+ const pipelineActions = {
1214
+ run_fix: async () => { await ctx.reply('๐Ÿ”ง Running Coder fix...'); const r = await actions.runFix(); await ctx.reply(r.success ? 'โœ… Coder is on it.' : `โŒ Fix failed: ${r.error}`); },
1215
+ run_review: async () => { if (explanation) await replyAI(ctx, explanation); await ctx.reply('๐Ÿ” Starting code review...'); const r = await actions.runReview(); if (!r.success) await ctx.reply(`โŒ Review failed: ${r.error}`); },
1216
+ run_fix_and_review: async () => { await ctx.reply('๐Ÿ”ง Running Coder fix...'); const fr = await actions.runFix(); if (!fr.success) { await ctx.reply(`โŒ Fix failed: ${fr.error}`); return; } await ctx.reply('โœ… Fixes applied โ€” running review...'); const rr = await actions.runReview(); if (!rr.success) await ctx.reply(`โŒ Review failed: ${rr.error}`); },
1217
+ run_approve: async () => { await ctx.reply('โœ… Approving...'); const r = await actions.runApprove(); await ctx.reply(r.success ? 'โœ… Feature approved!' : `โŒ ${r.error}`); },
1218
+ run_deploy: async () => { await ctx.reply('๐Ÿš€ Deploying...'); const r = await actions.runDeploy('RunLocalTests'); await ctx.reply(r.success ? 'โœ… Deployed!' : `โŒ ${r.error?.slice(0,200)}`); },
1219
+ run_implement: async () => { const st = actions.getStatusData(); await ctx.reply(`โšก Triggering Coder for <code>${st.current_feature}</code>โ€ฆ`, { parse_mode: 'HTML' }); const r = await actions.runImplementation(); if (!r.success) await ctx.reply(`โŒ ${r.error}`); },
1220
+ coder_fix: async () => { await ctx.reply('๐Ÿ”ง Running Coder fix...'); const r = await actions.runFix(); await ctx.reply(r.success ? 'โœ… Coder is on it.' : `โŒ Fix failed: ${r.error}`); },
1221
+ run_rewrite_docs: async () => { await ctx.reply('๐Ÿ”„ Rewriting spec and architecture docs...'); const r = await actions.runRewriteDocs(action.description || ''); if (!r.success) await ctx.reply(`โŒ Rewrite failed: ${r.error}`); },
1222
+ run_reset: async () => { const r = actions.runReset(); if (r.success) { await ctx.reply(`๐Ÿ”„ Pipeline reset to idle. Feature "${r.feature}" abandoned.`); } else { try { await actions.runCleanup(); } catch {} await ctx.reply('๐Ÿ’ค Pipeline is now idle.'); } },
1223
+ run_cleanup: async () => { await ctx.reply('๐Ÿงน Cleaning up...'); const r = await actions.runCleanup(); await ctx.reply(r.success ? 'โœ… Workspace cleaned up.' : `โŒ Cleanup failed: ${r.error}`); },
1224
+ };
1225
+ if (pipelineActions[action._action]) {
1226
+ if (explanation) await replyAI(ctx, explanation);
1227
+ emitUserAction(action._action, { username: ctx.from?.username, trigger: 'ai_decision' });
1228
+ await pipelineActions[action._action]();
1229
+ return;
1230
+ }
1231
+
1232
+ // โ”€โ”€ Feature/bug creation (with optional inline confirmation) โ”€โ”€โ”€โ”€โ”€
1233
+ if (action._action === 'create_feature') {
1234
+ const desc = (action.description || text).trim();
1235
+ const type = action.type === 'bug' ? 'bug' : 'feature';
1236
+ const label = type === 'bug' ? '๐Ÿ› Bug Fix' : 'โœจ Feature';
1237
+
1238
+ if (action.confirm) {
1239
+ // Show inline confirmation buttons โ€” saves an extra AI request
1240
+ if (explanation) await replyAI(ctx, explanation);
1241
+ await ctx.reply(`${label}: "${desc}"\n\nWant me to set this up?`, { reply_markup: confirmActionKeyboard(type, desc) });
1242
+ } else {
1243
+ // Legacy path: skip confirmation, go straight to pipeline mode
1244
+ setPendingFeature(ctx.chat.id, { description: desc, type });
1245
+ if (explanation) await replyAI(ctx, explanation);
1246
+ await ctx.reply(`${label}: "${desc}"\n\nSelect pipeline mode:`, { reply_markup: featureModeKeyboard(type) });
1247
+ }
1248
+ return;
1249
+ }
1250
+ }
1251
+ }
1252
+ // Strip any _action JSON blobs the AI embedded before showing to user
1253
+ const cleanResult = result
1254
+ .replace(/\{"_action"\s*:[^}]+\}/g, '')
1255
+ .replace(/\n{3,}/g, '\n\n')
1256
+ .trim();
1257
+ if (cleanResult) await replyAI(ctx, cleanResult);
1258
+ } else if (result?.error) {
1259
+ await ctx.reply(`AI error: ${result.error}`);
1260
+ } else {
1261
+ await ctx.reply('No response from AI.');
1262
+ }
1263
+ } catch (err) {
1264
+ console.error(` [BOT] Reply failed: ${err.message}`);
1265
+ }
1266
+ });
1267
+ }