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,116 @@
1
+ /**
2
+ * Payment Agent โ€” Routes payment-related implementation requests
3
+ * to the correct provider skill template.
4
+ *
5
+ * This agent does NOT process payments itself. It:
6
+ * 1. Detects payment intent from user request
7
+ * 2. Selects the correct provider template (Stripe, PayOS, etc.)
8
+ * 3. Injects the payment skill into the implementation prompt
9
+ * 4. Passes to the standard impl stage
10
+ *
11
+ * This keeps payment logic in skills (templates), not in code.
12
+ */
13
+
14
+ import { getConfig } from '../config.js';
15
+ import { logActivity } from '../utils/activity-log.js';
16
+ import { injectSkills } from '../utils/skill-loader.js';
17
+
18
+ const PROVIDER_TEMPLATES = {
19
+ stripe: 'skill-payment-stripe.md',
20
+ payos: 'skill-payment-payos.md',
21
+ momo: 'skill-payment-momo.md',
22
+ vnpay: 'skill-payment-vnpay.md',
23
+ zalopay: 'skill-payment-zalopay.md',
24
+ };
25
+
26
+ /**
27
+ * Detect if a feature request involves payment.
28
+ * Also exported as isPaymentRelated for backwards compatibility.
29
+ */
30
+ export function detectPaymentIntent(description) {
31
+ const paymentKeywords = [
32
+ 'payment', 'checkout', 'stripe', 'payos', 'momo', 'vnpay', 'zalopay',
33
+ 'subscription', 'billing', 'invoice', 'cart', 'purchase', 'buy',
34
+ 'credit card', 'debit card', 'wallet', 'refund', 'pricing',
35
+ 'plan', 'upgrade', 'downgrade', 'trial',
36
+ 'thanh toรกn', 'mua hร ng', 'giแป hร ng', 'ฤ‘ฦกn hร ng', // Vietnamese
37
+ ];
38
+
39
+ const lower = (description || '').toLowerCase();
40
+ return paymentKeywords.some(kw => lower.includes(kw));
41
+ }
42
+
43
+ // Backwards-compatible alias
44
+ export const isPaymentRelated = detectPaymentIntent;
45
+
46
+ /**
47
+ * Get payment context for the implementation prompt.
48
+ */
49
+ export function getPaymentContext(config) {
50
+ const cfg = config || getConfig();
51
+ const provider = cfg.payment?.provider || 'stripe';
52
+ const sandbox = cfg.payment?.sandbox !== false; // default true
53
+
54
+ return {
55
+ provider,
56
+ sandbox,
57
+ template: PROVIDER_TEMPLATES[provider] || PROVIDER_TEMPLATES.stripe,
58
+ envVars: getPaymentEnvVars(provider),
59
+ };
60
+ }
61
+
62
+ function getPaymentEnvVars(provider) {
63
+ switch (provider) {
64
+ case 'stripe':
65
+ return {
66
+ secretKey: process.env.STRIPE_SECRET_KEY,
67
+ publishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
68
+ webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
69
+ };
70
+ case 'payos':
71
+ return {
72
+ clientId: process.env.PAYOS_CLIENT_ID,
73
+ apiKey: process.env.PAYOS_API_KEY,
74
+ checksumKey: process.env.PAYOS_CHECKSUM_KEY,
75
+ };
76
+ default:
77
+ return {};
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Build payment-aware implementation prompt.
83
+ * Called by action-runner.js when payment intent is detected.
84
+ */
85
+ export async function buildPaymentPrompt(originalPrompt, description) {
86
+ const config = getConfig();
87
+ const ctx = getPaymentContext(config);
88
+
89
+ let paymentSkill = '';
90
+ try {
91
+ paymentSkill = await injectSkills('', 'payment');
92
+ } catch { /* skill may not exist */ }
93
+
94
+ const hasKeys = Object.values(ctx.envVars).some(v => !!v);
95
+ const keyWarning = hasKeys
96
+ ? ''
97
+ : `\nWARNING: No ${ctx.provider} API keys found in environment. Implement with placeholder env vars and document required keys.\n`;
98
+
99
+ return `${paymentSkill}
100
+
101
+ ## Payment Provider: ${ctx.provider} (${ctx.sandbox ? 'SANDBOX' : 'PRODUCTION'})
102
+ ${keyWarning}
103
+
104
+ ## Rules for Payment Implementation
105
+ - ALWAYS use sandbox/test mode keys in development
106
+ - NEVER log or store raw card numbers
107
+ - ALWAYS validate webhook signatures
108
+ - ALWAYS use idempotency keys for payment creation
109
+ - Handle these states: pending, succeeded, failed, refunded
110
+ - Create a /webhooks/${ctx.provider} endpoint for async payment events
111
+ - Store payment records in your database with: paymentId, amount, currency, status, provider, createdAt
112
+
113
+ ---
114
+
115
+ ${originalPrompt}`;
116
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Suggestion Agent โ€” Proactively suggests new features when the pipeline is idle.
3
+ *
4
+ * Analyzes:
5
+ * - QA history (recurring failures -> suggest fixes)
6
+ * - Current routes (missing features typical for the app type)
7
+ * - Activity logs (patterns of user friction)
8
+ *
9
+ * Triggered by:
10
+ * - PM Agent idle timer (configurable, default 60 min)
11
+ * - Manual /suggest command
12
+ *
13
+ * Output:
14
+ * - Posts suggestions to Virtual Office room
15
+ * - Creates .ai-workflow/feature-suggestions/SUGGEST-{date}.json
16
+ * - Optionally creates feature inbox items
17
+ */
18
+
19
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
20
+ import { resolve } from 'path';
21
+ import { getConfig } from '../config.js';
22
+ import { getWorkflowDir } from '../utils/pipeline.js';
23
+ import { logActivity } from '../utils/activity-log.js';
24
+ import { analyzeQAHistory, analyzeActivityLogs } from '../utils/ux-analyzer.js';
25
+ import { injectSkills } from '../utils/skill-loader.js';
26
+ import { runAI } from '../utils/runner.js';
27
+
28
+ const SUGGESTION_DIR = () => resolve(getWorkflowDir(), 'feature-suggestions');
29
+
30
+ export async function runSuggestionAgent(options = {}) {
31
+ const config = getConfig();
32
+
33
+ if (!config.suggestion?.enabled) {
34
+ logActivity('SUGGEST', 'Suggestion agent disabled', 'info');
35
+ return { skipped: true };
36
+ }
37
+
38
+ logActivity('SUGGEST', 'Running suggestion analysis', 'info');
39
+
40
+ const qaAnalysis = analyzeQAHistory();
41
+ const logAnalysis = analyzeActivityLogs();
42
+
43
+ const contextParts = [];
44
+
45
+ if (qaAnalysis) {
46
+ contextParts.push(`## QA Analysis (last ${qaAnalysis.totalReports} runs)
47
+ Average pass rate: ${qaAnalysis.avgPassRate}%
48
+ Recurring failures: ${JSON.stringify(qaAnalysis.recurringFailures)}
49
+ Recurring warnings: ${JSON.stringify(qaAnalysis.recurringWarnings)}
50
+ Current routes: ${qaAnalysis.testedRoutes.join(', ')}`);
51
+ }
52
+
53
+ if (logAnalysis) {
54
+ contextParts.push(`## Activity Log Patterns
55
+ Total error lines: ${logAnalysis.totalErrorLines}
56
+ Sample errors: ${logAnalysis.sampleErrors.slice(0, 5).join('\n')}`);
57
+ }
58
+
59
+ contextParts.push(`## Project
60
+ Name: ${config.name}
61
+ Description: ${config.description || 'Not specified'}
62
+ Framework: ${config.framework || 'Not specified'}`);
63
+
64
+ const skill = injectSkills('', 'suggestion');
65
+ const prompt = `${skill}\n\n${contextParts.join('\n\n')}\n\nGenerate exactly 3 feature suggestions as JSON array.`;
66
+
67
+ const raw = await runAI(prompt, { weight: 'light' });
68
+
69
+ let suggestions = [];
70
+ try {
71
+ const jsonMatch = raw.match(/\[[\s\S]*\]/);
72
+ if (jsonMatch) suggestions = JSON.parse(jsonMatch[0]);
73
+ } catch {
74
+ logActivity('SUGGEST', 'Failed to parse suggestion JSON, using raw output', 'warn');
75
+ suggestions = [{ title: 'AI suggestion', description: raw.slice(0, 200), priority: 'medium', effort: 'medium' }];
76
+ }
77
+
78
+ // Save to disk
79
+ const dir = SUGGESTION_DIR();
80
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
81
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
82
+ const filePath = resolve(dir, `SUGGEST-${timestamp}.json`);
83
+ writeFileSync(filePath, JSON.stringify({ timestamp, suggestions, qaAnalysis }, null, 2));
84
+
85
+ logActivity('SUGGEST', `Generated ${suggestions.length} suggestions`, 'success');
86
+
87
+ return { success: true, suggestions, filePath };
88
+ }
package/lib/cli.js ADDED
@@ -0,0 +1,303 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * AI Control Center CLI โ€” thin wrapper around action-runner.js.
4
+ *
5
+ * Used by OpenClaw's bash tool to execute pipeline actions.
6
+ * Each command prints JSON to stdout for the AI to parse.
7
+ *
8
+ * Usage:
9
+ * node cli.js status
10
+ * node cli.js feature "Add user auth" --mode auto --type feature
11
+ * node cli.js deploy --test-level RunLocalTests
12
+ * node cli.js approve
13
+ * node cli.js reject "Missing error handling"
14
+ * node cli.js review
15
+ * node cli.js logs --lines 30
16
+ * node cli.js health
17
+ * node cli.js cleanup
18
+ * node cli.js autopilot
19
+ * node cli.js reset
20
+ * node cli.js files <subdir>
21
+ * node cli.js read <subdir> <filename>
22
+ * node cli.js ai-status
23
+ * node cli.js ask "How does authentication work?"
24
+ */
25
+ import * as actions from './shared/action-runner.js';
26
+ import { logActivity } from './utils/activity-log.js';
27
+
28
+ const args = process.argv.slice(2);
29
+ const command = args[0];
30
+
31
+ function parseFlag(flag, defaultValue) {
32
+ const idx = args.indexOf(flag);
33
+ if (idx === -1) return defaultValue;
34
+ return args[idx + 1] || defaultValue;
35
+ }
36
+
37
+ function out(data) {
38
+ console.log(JSON.stringify(data, null, 2));
39
+ }
40
+
41
+ async function main() {
42
+ switch (command) {
43
+
44
+ case 'status':
45
+ out(actions.getStatusData());
46
+ break;
47
+
48
+ case 'health':
49
+ out(await actions.getHealthData());
50
+ break;
51
+
52
+ case 'ai-status':
53
+ out(await actions.getAIProviderStatus());
54
+ break;
55
+
56
+ case 'feature': {
57
+ const desc = args[1];
58
+ const mode = parseFlag('--mode', 'manual');
59
+ const type = parseFlag('--type', 'feature');
60
+ out(await actions.runNewFeature(desc, mode, type));
61
+ break;
62
+ }
63
+
64
+ case 'approve':
65
+ out(await actions.runApprove());
66
+ break;
67
+
68
+ case 'reject': {
69
+ const reason = args[1] || 'Rejected via OpenClaw';
70
+ out(await actions.runReject(reason));
71
+ break;
72
+ }
73
+
74
+ case 'deploy': {
75
+ const testLevel = parseFlag('--test-level', 'NoTestRun');
76
+ out(await actions.runDeploy(testLevel));
77
+ break;
78
+ }
79
+
80
+ case 'review': {
81
+ const review = actions.getLatestReview();
82
+ out(review || { found: false });
83
+ break;
84
+ }
85
+
86
+ case 'logs': {
87
+ const lines = parseInt(parseFlag('--lines', '30'), 10);
88
+ out(actions.getLogsData(lines));
89
+ break;
90
+ }
91
+
92
+ case 'cleanup':
93
+ out(await actions.runCleanup());
94
+ break;
95
+
96
+ case 'autopilot':
97
+ out(actions.toggleAutoPilot());
98
+ break;
99
+
100
+ case 'reset':
101
+ out(actions.runReset());
102
+ break;
103
+
104
+ case 'files': {
105
+ const subdir = args[1];
106
+ if (!subdir) { out({ error: 'Usage: cli.js files <subdir>' }); break; }
107
+ out(actions.listFiles(subdir));
108
+ break;
109
+ }
110
+
111
+ case 'read': {
112
+ const subdir = args[1];
113
+ const name = args[2];
114
+ if (!subdir || !name) { out({ error: 'Usage: cli.js read <subdir> <filename>' }); break; }
115
+ const content = actions.readFile(subdir, name);
116
+ out(content !== null ? { name, content } : { error: 'File not found' });
117
+ break;
118
+ }
119
+
120
+ case 'ask': {
121
+ const question = args[1];
122
+ if (!question) { out({ error: 'Usage: cli.js ask "your question"' }); break; }
123
+ const result = await actions.askAI(question);
124
+ out(typeof result === 'string' ? { answer: result } : result);
125
+ break;
126
+ }
127
+
128
+ case 'assign': {
129
+ const url = args[1];
130
+ const goal = args.slice(2).join(' ');
131
+ if (!url || !goal) {
132
+ out({ error: 'Usage: cli.js assign <url> <goal>' });
133
+ break;
134
+ }
135
+ const { assignProjectAction } = await import('./actions/assign-project.js');
136
+ const assignResult = await assignProjectAction({ url, goal });
137
+ out(assignResult);
138
+
139
+ // Auto-kick browser QA if assignment succeeded and browserQA is enabled
140
+ if (assignResult.success) {
141
+ const { getConfig } = await import('./config.js');
142
+ const cfg = getConfig();
143
+ const { bus } = await import('./shared/event-bus.js');
144
+
145
+ if (cfg.browserQA?.enabled) {
146
+ logActivity('ASSIGN', 'Auto-starting Browser QA...', 'info');
147
+ try {
148
+ const { browserTestAction } = await import('./actions/browser-test.js');
149
+ const qaResult = await browserTestAction({ featureId: assignResult.projectId, config: cfg });
150
+ out({ browserQA: qaResult.report?.summary || { skipped: qaResult.skipped, error: qaResult.error } });
151
+
152
+ // Notify channels about QA results
153
+ if (qaResult.report?.summary) {
154
+ const s = qaResult.report.summary;
155
+ const authNote = s.authGated ? `\n๐Ÿ”’ ${s.authGated} route(s) need elevated permissions (skipped)` : '';
156
+ bus.emitEvent('qa_report_ready', {
157
+ message: `๐Ÿ” <b>Browser QA complete</b>\n๐Ÿ“Š ${s.passed}/${s.testedRoutes || s.totalRoutes} routes passed (${s.passRate}%)${s.failed > 0 ? `\nโŒ ${s.failed} failed route(s)` : '\nโœ… All routes healthy'}${authNote}`,
158
+ projectId: assignResult.projectId,
159
+ summary: s,
160
+ });
161
+ }
162
+
163
+ // If QA found bugs, auto-trigger bugfix
164
+ if (qaResult.needsBugfix && qaResult.report) {
165
+ const failCount = qaResult.failCount;
166
+ logActivity('ASSIGN', `QA found ${failCount} issue(s) โ€” starting bugfix loop`, 'info');
167
+ bus.emitEvent('bugfix_loop_start', {
168
+ message: `๐Ÿ”ง <b>Starting bugfix loop</b> โ€” ${failCount} issue(s) found, up to ${cfg.browserQA?.maxBugfixCycles || 3} cycles`,
169
+ projectId: assignResult.projectId,
170
+ failCount,
171
+ maxCycles: cfg.browserQA?.maxBugfixCycles || 3,
172
+ });
173
+
174
+ const { fixBugAction } = await import('./actions/fix-bug.js');
175
+ const maxCycles = cfg.browserQA?.maxBugfixCycles || 3;
176
+ for (let cycle = 1; cycle <= maxCycles; cycle++) {
177
+ logActivity('BUGFIX', `Bug-fix cycle ${cycle}/${maxCycles}`, 'info');
178
+ bus.emitEvent('bugfix_cycle_start', {
179
+ message: `๐Ÿ”„ <b>Bugfix cycle ${cycle}/${maxCycles}</b> โ€” AI is analyzing and fixing issues...`,
180
+ cycle, maxCycles, projectId: assignResult.projectId,
181
+ });
182
+
183
+ const fixResult = await fixBugAction({ featureId: assignResult.projectId, fromQA: true });
184
+
185
+ // Check if the AI pipeline actually ran successfully
186
+ if (fixResult && !fixResult.success) {
187
+ logActivity('BUGFIX', `Pipeline failed: ${fixResult.error || 'unknown error'}`, 'error');
188
+ bus.emitEvent('bugfix_loop_complete', {
189
+ message: `โš ๏ธ <b>Bugfix pipeline failed</b> โ€” ${fixResult.error || 'AI provider unavailable'}.\nCheck that Claude Code, Copilot, or Gemini CLI is installed and authenticated.`,
190
+ cycle, maxCycles, projectId: assignResult.projectId,
191
+ success: false, error: fixResult.error,
192
+ });
193
+ out({ bugfix: { success: false, cycles: cycle, error: fixResult.error } });
194
+ break;
195
+ }
196
+
197
+ // Re-run QA to verify fixes
198
+ bus.emitEvent('bugfix_retest', {
199
+ message: `๐Ÿงช Re-running Browser QA to verify fixes (cycle ${cycle}/${maxCycles})...`,
200
+ cycle, maxCycles, projectId: assignResult.projectId,
201
+ });
202
+ const reQA = await browserTestAction({ featureId: assignResult.projectId, config: cfg });
203
+
204
+ if (!reQA.needsBugfix || reQA.failCount === 0) {
205
+ logActivity('BUGFIX', `All bugs fixed after ${cycle} cycle(s)`, 'success');
206
+ bus.emitEvent('bugfix_loop_complete', {
207
+ message: `โœ… <b>All bugs fixed!</b> Took ${cycle} cycle(s). All ${reQA.report?.summary?.totalRoutes || '?'} routes passing.`,
208
+ cycle, maxCycles, projectId: assignResult.projectId, success: true,
209
+ });
210
+ out({ bugfix: { success: true, cycles: cycle } });
211
+ break;
212
+ }
213
+
214
+ if (cycle === maxCycles) {
215
+ logActivity('BUGFIX', `Exhausted ${maxCycles} bugfix cycles โ€” escalating`, 'error');
216
+ bus.emitEvent('bugfix_loop_complete', {
217
+ message: `โš ๏ธ <b>Bugfix exhausted</b> โ€” ${reQA.failCount} issue(s) remain after ${maxCycles} cycles. Needs manual attention.`,
218
+ cycle, maxCycles, projectId: assignResult.projectId,
219
+ success: false, remainingFails: reQA.failCount,
220
+ });
221
+ out({ bugfix: { success: false, cycles: maxCycles, remainingFails: reQA.failCount } });
222
+ }
223
+ }
224
+ }
225
+ } catch (qaErr) {
226
+ logActivity('ASSIGN', `Browser QA failed: ${qaErr.message}`, 'error');
227
+ bus.emitEvent('stage_error', {
228
+ stage: 'browser-qa', error: qaErr.message,
229
+ message: `๐Ÿ’ฅ <b>Browser QA crashed:</b> ${qaErr.message.slice(0, 200)}`,
230
+ });
231
+ out({ browserQA: { error: qaErr.message } });
232
+ }
233
+ }
234
+ }
235
+ break;
236
+ }
237
+
238
+ case 'suggest': {
239
+ const { runSuggestionAgent } = await import('./agents/suggestion-agent.js');
240
+ out(await runSuggestionAgent());
241
+ break;
242
+ }
243
+
244
+ case 'threads': {
245
+ const { getOpenThreads, getAllThreads } = await import('./roleplay/discussion-threads.js');
246
+ const showAll = args.includes('--all');
247
+ out(showAll ? getAllThreads() : getOpenThreads());
248
+ break;
249
+ }
250
+
251
+ case 'browser-test':
252
+ case 'qa': {
253
+ const result = await actions.runBrowserQA();
254
+ if (result.skipped) {
255
+ out({ skipped: true, message: 'Browser QA is disabled. Enable in aicc.config.js: browserQA.enabled = true' });
256
+ } else {
257
+ out(result.report?.summary || result);
258
+ }
259
+ break;
260
+ }
261
+
262
+ case 'events': {
263
+ // Stream pipeline events as NDJSON (one JSON object per line)
264
+ const { bus } = await import('./shared/event-bus.js');
265
+ bus.startWatching();
266
+
267
+ const write = (type, data) => {
268
+ process.stdout.write(JSON.stringify({ type, ...data, timestamp: new Date().toISOString() }) + '\n');
269
+ };
270
+
271
+ bus.on('status', (data) => write('status', data));
272
+ bus.on('pipeline-event', (data) => write('pipeline-event', data));
273
+ bus.on('log', (data) => write('log', data));
274
+
275
+ write('connected', { message: 'Streaming pipeline events (Ctrl+C to stop)' });
276
+
277
+ // Keep process alive
278
+ process.on('SIGINT', () => {
279
+ bus.stopWatching();
280
+ process.exit(0);
281
+ });
282
+ // Block โ€” don't exit main()
283
+ await new Promise(() => {});
284
+ break;
285
+ }
286
+
287
+ default:
288
+ out({
289
+ error: `Unknown command: ${command || '(none)'}`,
290
+ commands: [
291
+ 'status', 'health', 'ai-status', 'feature', 'approve', 'reject',
292
+ 'deploy', 'review', 'logs', 'cleanup', 'autopilot', 'reset',
293
+ 'files', 'read', 'ask', 'events',
294
+ ],
295
+ });
296
+ process.exitCode = 1;
297
+ }
298
+ }
299
+
300
+ main().catch(err => {
301
+ out({ error: err.message });
302
+ process.exitCode = 1;
303
+ });