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,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
|
+
});
|