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