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,128 @@
1
+ /**
2
+ * OpenAI Codex CLI Integration.
3
+ *
4
+ * Lightweight wrapper for the `codex` CLI tool.
5
+ * Handles availability checks, config, and command building.
6
+ * Actual execution is deferred to the main runner.
7
+ */
8
+
9
+ import { existsSync } from 'fs';
10
+ import { execSync, spawn } from 'child_process';
11
+
12
+ /** Default configuration for the Codex CLI. */
13
+ export const CODEX_DEFAULTS = {
14
+ model: 'codex-mini',
15
+ maxTokens: 8000,
16
+ timeout: 300_000,
17
+ };
18
+
19
+ /**
20
+ * Check whether the `codex` CLI is installed and accessible.
21
+ */
22
+ export function isCodexAvailable() {
23
+ try {
24
+ execSync('which codex', { stdio: 'ignore' });
25
+ return true;
26
+ } catch {
27
+ try {
28
+ execSync('codex --version', { stdio: 'ignore' });
29
+ return true;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Resolve Codex configuration from environment variables and defaults.
38
+ */
39
+ export function getCodexConfig() {
40
+ return {
41
+ model: process.env.CODEX_MODEL || CODEX_DEFAULTS.model,
42
+ maxTokens: Number(process.env.CODEX_MAX_TOKENS) || CODEX_DEFAULTS.maxTokens,
43
+ timeout: Number(process.env.CODEX_TIMEOUT) || CODEX_DEFAULTS.timeout,
44
+ available: isCodexAvailable(),
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Build the shell command and arguments for invoking the Codex CLI.
50
+ *
51
+ * @param {string} prompt – the prompt text
52
+ * @param {object} [options] – overrides for model, maxTokens, etc.
53
+ * @returns {{ command: string, args: string[] }}
54
+ */
55
+ export function buildCodexCommand(prompt, options = {}) {
56
+ const model = options.model || CODEX_DEFAULTS.model;
57
+ const safePrompt = prompt.replace(/"/g, '\\"');
58
+
59
+ const args = [
60
+ '--model', model,
61
+ '--quiet',
62
+ `"${safePrompt}"`,
63
+ ];
64
+
65
+ if (options.maxTokens) {
66
+ args.unshift('--max-tokens', String(options.maxTokens));
67
+ }
68
+
69
+ return {
70
+ command: `codex ${args.join(' ')}`,
71
+ args,
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Execute the Codex CLI and return its output.
77
+ *
78
+ * @param {string} prompt – the prompt text
79
+ * @param {object} [options] – { model, maxTokens, timeout, cwd }
80
+ * @returns {Promise<{ output: string, exitCode: number }>}
81
+ */
82
+ export function runCodex(prompt, options = {}) {
83
+ if (!isCodexAvailable()) {
84
+ return Promise.reject(new Error('Codex CLI is not installed or not in PATH'));
85
+ }
86
+
87
+ const { command } = buildCodexCommand(prompt, options);
88
+ const timeout = options.timeout || CODEX_DEFAULTS.timeout;
89
+ const cwd = options.cwd || process.cwd();
90
+
91
+ return new Promise((resolve, reject) => {
92
+ const proc = spawn('sh', ['-c', command], {
93
+ cwd,
94
+ stdio: ['pipe', 'pipe', 'pipe'],
95
+ env: { ...process.env },
96
+ });
97
+
98
+ let stdout = '';
99
+ let stderr = '';
100
+ let killed = false;
101
+
102
+ const timer = setTimeout(() => {
103
+ killed = true;
104
+ proc.kill('SIGTERM');
105
+ }, timeout);
106
+
107
+ proc.stdout.on('data', (data) => { stdout += data.toString(); });
108
+ proc.stderr.on('data', (data) => { stderr += data.toString(); });
109
+
110
+ proc.on('error', (err) => {
111
+ clearTimeout(timer);
112
+ reject(new Error(`Codex process error: ${err.message}`));
113
+ });
114
+
115
+ proc.on('close', (code) => {
116
+ clearTimeout(timer);
117
+ if (killed) {
118
+ reject(new Error(`Codex timed out after ${timeout}ms`));
119
+ return;
120
+ }
121
+ if (code !== 0) {
122
+ reject(new Error(`Codex exited with code ${code}: ${stderr.trim().slice(0, 2000)}`));
123
+ return;
124
+ }
125
+ resolve({ output: stdout.trim(), exitCode: code });
126
+ });
127
+ });
128
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * GitHub Actions Integration — workflow generation and PR formatting.
3
+ *
4
+ * Generates GitHub Actions workflow YAML for AICC integration,
5
+ * formats pipeline results as PR comments and Check Run payloads.
6
+ */
7
+ import { getConfig } from '../config.js';
8
+
9
+ // ─── Constants ─────────────────────────────────────────────────────────────────
10
+
11
+ export const GITHUB_ACTION_VERSION = 'v1';
12
+
13
+ // ─── Workflow YAML generation ──────────────────────────────────────────────────
14
+
15
+ /**
16
+ * Generate a GitHub Actions workflow YAML string for AICC integration.
17
+ *
18
+ * @param {object} [options]
19
+ * @param {'pr'|'label'|'push'} [options.triggerOn='pr'] — when to trigger
20
+ * @param {boolean} [options.reviewOnPR=true] — run AI review on PRs
21
+ * @param {boolean} [options.deployOnMerge=false] — deploy on merge to main
22
+ * @returns {string} YAML content
23
+ */
24
+ export function generateWorkflowYaml(options = {}) {
25
+ const { triggerOn = 'pr', reviewOnPR = true, deployOnMerge = false } = options;
26
+
27
+ const onTrigger = buildTriggerSection(triggerOn);
28
+ const jobs = [];
29
+
30
+ if (reviewOnPR) {
31
+ jobs.push(buildReviewJob(triggerOn));
32
+ }
33
+
34
+ if (deployOnMerge) {
35
+ jobs.push(buildDeployJob());
36
+ }
37
+
38
+ // Always include workflow_dispatch for manual actions
39
+ const yaml = [
40
+ `name: AI Control Center Pipeline`,
41
+ ``,
42
+ `on:`,
43
+ ...onTrigger,
44
+ ` workflow_dispatch:`,
45
+ ` inputs:`,
46
+ ` action:`,
47
+ ` description: 'Action to perform'`,
48
+ ` required: true`,
49
+ ` type: choice`,
50
+ ` options:`,
51
+ ` - review`,
52
+ ` - feature`,
53
+ ` - deploy`,
54
+ ``,
55
+ `jobs:`,
56
+ ...jobs,
57
+ ];
58
+
59
+ return yaml.join('\n') + '\n';
60
+ }
61
+
62
+ /** Build the 'on' trigger section based on trigger type */
63
+ function buildTriggerSection(triggerOn) {
64
+ switch (triggerOn) {
65
+ case 'label':
66
+ return [
67
+ ` pull_request:`,
68
+ ` types: [labeled]`,
69
+ ];
70
+ case 'push':
71
+ return [
72
+ ` push:`,
73
+ ` branches: [main, master]`,
74
+ ];
75
+ case 'pr':
76
+ default:
77
+ return [
78
+ ` pull_request:`,
79
+ ` types: [opened, synchronize]`,
80
+ ];
81
+ }
82
+ }
83
+
84
+ /** Build the AI review job */
85
+ function buildReviewJob(triggerOn) {
86
+ const condition = triggerOn === 'label'
87
+ ? ` if: github.event.label.name == 'ai-review'`
88
+ : '';
89
+
90
+ const lines = [
91
+ ` ai-review:`,
92
+ ` runs-on: ubuntu-latest`,
93
+ ];
94
+
95
+ if (condition) lines.push(condition);
96
+
97
+ lines.push(
98
+ ` steps:`,
99
+ ` - uses: actions/checkout@v4`,
100
+ ` - uses: actions/setup-node@v4`,
101
+ ` with:`,
102
+ ` node-version: '20'`,
103
+ ` - run: npm install -g ai-control-center`,
104
+ ` - run: aicc review`,
105
+ ` env:`,
106
+ ` GEMINI_API_KEY: \${{ secrets.GEMINI_API_KEY }}`,
107
+ ` - name: Post Review Comment`,
108
+ ` if: github.event_name == 'pull_request'`,
109
+ ` uses: actions/github-script@v7`,
110
+ ` with:`,
111
+ ` script: |`,
112
+ ` const fs = require('fs');`,
113
+ ` const review = fs.readFileSync('.ai-workflow/reviews/latest.md', 'utf8');`,
114
+ ` github.rest.issues.createComment({`,
115
+ ` issue_number: context.issue.number,`,
116
+ ` owner: context.repo.owner,`,
117
+ ` repo: context.repo.repo,`,
118
+ ` body: '## 🤖 AI Code Review\\n\\n' + review`,
119
+ ` });`,
120
+ );
121
+
122
+ return lines.join('\n');
123
+ }
124
+
125
+ /** Build the deploy-on-merge job */
126
+ function buildDeployJob() {
127
+ return [
128
+ ` ai-deploy:`,
129
+ ` runs-on: ubuntu-latest`,
130
+ ` if: github.event_name == 'push' && github.ref == 'refs/heads/main'`,
131
+ ` steps:`,
132
+ ` - uses: actions/checkout@v4`,
133
+ ` - uses: actions/setup-node@v4`,
134
+ ` with:`,
135
+ ` node-version: '20'`,
136
+ ` - run: npm install -g ai-control-center`,
137
+ ` - run: aicc deploy`,
138
+ ` env:`,
139
+ ` GEMINI_API_KEY: \${{ secrets.GEMINI_API_KEY }}`,
140
+ ].join('\n');
141
+ }
142
+
143
+ // ─── PR Comment formatting ─────────────────────────────────────────────────────
144
+
145
+ /**
146
+ * Format a pipeline result as a GitHub PR comment (markdown).
147
+ *
148
+ * @param {object} result — pipeline result
149
+ * @param {string} [result.stage] — pipeline stage
150
+ * @param {string} [result.status] — pass/fail
151
+ * @param {string} [result.summary] — human-readable summary
152
+ * @param {Array} [result.issues] — list of issues found
153
+ * @param {string} [result.review] — full review text
154
+ * @returns {string} Markdown-formatted PR comment
155
+ */
156
+ export function formatPRComment(result) {
157
+ if (!result) return '## 🤖 AI Control Center\n\nNo results available.';
158
+
159
+ const icon = result.status === 'pass' ? '✅' : result.status === 'fail' ? '❌' : '🔄';
160
+ const lines = [
161
+ `## 🤖 AI Control Center — ${result.stage || 'Pipeline'} ${icon}`,
162
+ '',
163
+ ];
164
+
165
+ if (result.summary) {
166
+ lines.push(result.summary, '');
167
+ }
168
+
169
+ if (result.issues?.length) {
170
+ lines.push('### Issues Found', '');
171
+ for (const issue of result.issues) {
172
+ const severity = issue.severity === 'error' ? '🔴' : issue.severity === 'warning' ? '🟡' : '🔵';
173
+ lines.push(`- ${severity} **${issue.file || 'General'}**${issue.line ? ` (L${issue.line})` : ''}: ${issue.message}`);
174
+ }
175
+ lines.push('');
176
+ }
177
+
178
+ if (result.review) {
179
+ lines.push('<details>', '<summary>📝 Full Review</summary>', '', result.review, '', '</details>');
180
+ }
181
+
182
+ lines.push('', `---`, `*Generated by AI Control Center ${GITHUB_ACTION_VERSION}*`);
183
+ return lines.join('\n');
184
+ }
185
+
186
+ // ─── Check Run formatting ──────────────────────────────────────────────────────
187
+
188
+ /**
189
+ * Format a pipeline result as a GitHub Check Run payload.
190
+ *
191
+ * @param {object} result
192
+ * @param {string} [result.stage] — pipeline stage
193
+ * @param {string} [result.status] — pass/fail
194
+ * @param {string} [result.summary] — summary text
195
+ * @param {Array} [result.annotations] — code annotations [{path, startLine, endLine, level, message}]
196
+ * @returns {object} GitHub Check Run API payload
197
+ */
198
+ export function formatCheckRun(result) {
199
+ if (!result) {
200
+ return {
201
+ name: 'AI Control Center',
202
+ status: 'completed',
203
+ conclusion: 'neutral',
204
+ output: {
205
+ title: 'AI Control Center',
206
+ summary: 'No results available',
207
+ },
208
+ };
209
+ }
210
+
211
+ const conclusion = result.status === 'pass' ? 'success'
212
+ : result.status === 'fail' ? 'failure'
213
+ : 'neutral';
214
+
215
+ const payload = {
216
+ name: `AI Control Center — ${result.stage || 'Pipeline'}`,
217
+ status: 'completed',
218
+ conclusion,
219
+ output: {
220
+ title: `${result.stage || 'Pipeline'} ${result.status === 'pass' ? '✅' : '❌'}`,
221
+ summary: result.summary || 'Pipeline check completed',
222
+ },
223
+ };
224
+
225
+ if (result.annotations?.length) {
226
+ payload.output.annotations = result.annotations.map(a => ({
227
+ path: a.path,
228
+ start_line: a.startLine || a.start_line || 1,
229
+ end_line: a.endLine || a.end_line || a.startLine || a.start_line || 1,
230
+ annotation_level: a.level === 'error' ? 'failure' : a.level === 'warning' ? 'warning' : 'notice',
231
+ message: a.message,
232
+ }));
233
+ }
234
+
235
+ return payload;
236
+ }
237
+
238
+ // ─── Default template ──────────────────────────────────────────────────────────
239
+
240
+ /**
241
+ * Returns the default workflow template as a string.
242
+ * This is the recommended starting point for integrating AICC with GitHub Actions.
243
+ *
244
+ * @returns {string} YAML workflow content
245
+ */
246
+ export function getGitHubActionTemplate() {
247
+ return generateWorkflowYaml({ triggerOn: 'pr', reviewOnPR: true, deployOnMerge: false });
248
+ }
@@ -0,0 +1,229 @@
1
+ /**
2
+ * GitHub Reporter — Creates GitHub Issues and PR comments with screenshots.
3
+ *
4
+ * Requires:
5
+ * config.github.token — Personal Access Token (repo scope)
6
+ * config.github.owner — Repository owner (username or org)
7
+ * config.github.repo — Repository name
8
+ * config.github.enabled — true to enable
9
+ *
10
+ * Features:
11
+ * - Creates one GitHub Issue per failed URL (with screenshot embedded)
12
+ * - Adds labels: 'ai-qa', 'bug', severity label
13
+ * - Auto-closes issues when QA verifies the bug is fixed
14
+ * - Tracks issue numbers in .ai-workflow/qa-reports/github-issues.json
15
+ */
16
+
17
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
18
+ import { resolve } from 'path';
19
+ import { getWorkflowDir } from '../utils/pipeline.js';
20
+ import { logActivity } from '../utils/activity-log.js';
21
+
22
+ // Lazy import Octokit to avoid hard dependency crash if not installed
23
+ async function getOctokit(token) {
24
+ const { Octokit } = await import('@octokit/rest');
25
+ return new Octokit({ auth: token });
26
+ }
27
+
28
+ const ISSUES_TRACKING_FILE = () => resolve(getWorkflowDir(), 'qa-reports/github-issues.json');
29
+
30
+ function loadIssueTracking() {
31
+ const file = ISSUES_TRACKING_FILE();
32
+ if (!existsSync(file)) return {};
33
+ try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return {}; }
34
+ }
35
+
36
+ function saveIssueTracking(data) {
37
+ writeFileSync(ISSUES_TRACKING_FILE(), JSON.stringify(data, null, 2));
38
+ }
39
+
40
+ function slugKey(url) {
41
+ return url.replace(/https?:\/\//, '').replace(/[^a-z0-9]/gi, '_').slice(0, 60);
42
+ }
43
+
44
+ function buildIssueBody(bug, screenshotBase64) {
45
+ const screenshotSection = screenshotBase64
46
+ ? `\n### Screenshot\n![screenshot](data:image/png;base64,${screenshotBase64.slice(0, 2000)}...)\n`
47
+ : '';
48
+
49
+ return `## Bug Report (Auto-generated by AI QA Agent)
50
+
51
+ **URL:** \`${bug.url}\`
52
+ **Found at:** ${new Date().toISOString()}
53
+ **Severity:** ${bug.errors.length > 0 ? 'Critical' : 'Warning'}
54
+
55
+ ### Errors
56
+ ${bug.errors.map(e => `- ${e}`).join('\n') || 'None'}
57
+
58
+ ### Warnings
59
+ ${bug.warnings?.map(w => `- ${w}`).join('\n') || 'None'}
60
+ ${screenshotSection}
61
+ ---
62
+ *Created automatically by [AI Control Center](https://www.npmjs.com/package/ai-control-center). Close this issue when the bug is verified fixed.*
63
+ `;
64
+ }
65
+
66
+ /**
67
+ * Report all failed QA tests as GitHub Issues.
68
+ */
69
+ export async function reportBugsToGitHub(qaReport, config) {
70
+ if (!config.github?.enabled || !config.github?.token) {
71
+ logActivity('GITHUB', 'GitHub reporting disabled or no token', 'warn');
72
+ return;
73
+ }
74
+
75
+ const octokit = await getOctokit(config.github.token);
76
+ const { owner, repo } = config.github;
77
+ const tracking = loadIssueTracking();
78
+
79
+ let store = null;
80
+ try {
81
+ const { ScreenshotStore } = await import('./screenshot-store.js');
82
+ store = new ScreenshotStore(resolve(getWorkflowDir(), 'screenshots'));
83
+ } catch { /* screenshot store not available */ }
84
+
85
+ for (const bug of qaReport.failed) {
86
+ const key = slugKey(bug.url);
87
+
88
+ // Skip if issue already open for this URL
89
+ if (tracking[key]?.open) {
90
+ logActivity('GITHUB', `Issue already open for ${bug.url} (#${tracking[key].number})`, 'info');
91
+ continue;
92
+ }
93
+
94
+ try {
95
+ const screenshotBase64 = (bug.screenshot && store) ? store.getBase64(bug.screenshot) : null;
96
+ const body = buildIssueBody(bug, screenshotBase64);
97
+
98
+ const { data: issue } = await octokit.issues.create({
99
+ owner,
100
+ repo,
101
+ title: `[AI-QA] Bug on ${new URL(bug.url).pathname}`,
102
+ body,
103
+ labels: ['ai-qa', 'bug', bug.errors.length > 0 ? 'critical' : 'warning'],
104
+ });
105
+
106
+ tracking[key] = { number: issue.number, url: bug.url, open: true, createdAt: new Date().toISOString() };
107
+ saveIssueTracking(tracking);
108
+
109
+ logActivity('GITHUB', `Created issue #${issue.number} for ${bug.url}`, 'success');
110
+
111
+ } catch (err) {
112
+ logActivity('GITHUB', `Failed to create issue for ${bug.url}: ${err.message}`, 'error');
113
+ }
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Close GitHub Issues for bugs that are now passing in QA.
119
+ */
120
+ export async function closeResolvedIssues(qaReport, config) {
121
+ if (!config.github?.enabled || !config.github?.token) return;
122
+
123
+ const octokit = await getOctokit(config.github.token);
124
+ const { owner, repo } = config.github;
125
+ const tracking = loadIssueTracking();
126
+
127
+ const passingUrls = new Set(qaReport.passed.map(p => p.url));
128
+
129
+ for (const [key, issueInfo] of Object.entries(tracking)) {
130
+ if (!issueInfo.open) continue;
131
+ if (!passingUrls.has(issueInfo.url)) continue;
132
+
133
+ try {
134
+ await octokit.issues.createComment({
135
+ owner, repo, issue_number: issueInfo.number,
136
+ body: `This bug has been verified fixed by the AI QA Agent.\n\nQA run: ${new Date().toISOString()}`,
137
+ });
138
+ await octokit.issues.update({
139
+ owner, repo, issue_number: issueInfo.number, state: 'closed',
140
+ });
141
+
142
+ tracking[key].open = false;
143
+ tracking[key].closedAt = new Date().toISOString();
144
+ saveIssueTracking(tracking);
145
+
146
+ logActivity('GITHUB', `Closed issue #${issueInfo.number} — bug verified fixed`, 'success');
147
+ } catch (err) {
148
+ logActivity('GITHUB', `Failed to close issue #${issueInfo.number}: ${err.message}`, 'error');
149
+ }
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Upload screenshot to GitHub repo and return markdown image link.
155
+ * Uploads to: .ai-qa-screenshots/{name}.png in the repo.
156
+ */
157
+ export async function uploadScreenshotToGitHub(octokit, owner, repo, screenshotPath, name) {
158
+ const content = readFileSync(screenshotPath).toString('base64');
159
+ const path = `.ai-qa-screenshots/${name}.png`;
160
+
161
+ try {
162
+ // Check if file exists (to get SHA for update)
163
+ let sha;
164
+ try {
165
+ const { data } = await octokit.repos.getContent({ owner, repo, path });
166
+ sha = data.sha;
167
+ } catch { /* file doesn't exist yet */ }
168
+
169
+ await octokit.repos.createOrUpdateFileContents({
170
+ owner, repo, path,
171
+ message: `[AI-QA] Screenshot: ${name}`,
172
+ content,
173
+ sha,
174
+ committer: {
175
+ name: 'AI QA Agent',
176
+ email: 'ai-qa@aicc.local',
177
+ },
178
+ });
179
+
180
+ return `https://raw.githubusercontent.com/${owner}/${repo}/main/${path}`;
181
+ } catch (err) {
182
+ logActivity('GITHUB', `Screenshot upload failed: ${err.message}`, 'warn');
183
+ return null;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Add QA summary as PR comment (runs after deploy from a branch).
189
+ */
190
+ export async function commentOnPR(qaReport, config) {
191
+ if (!config.github?.enabled || !config.github?.token) return;
192
+
193
+ const octokit = await getOctokit(config.github.token);
194
+ const { owner, repo } = config.github;
195
+
196
+ try {
197
+ // Find open PRs
198
+ const { data: prs } = await octokit.pulls.list({ owner, repo, state: 'open', per_page: 5 });
199
+ if (prs.length === 0) return;
200
+
201
+ const latestPR = prs[0]; // Most recent PR
202
+ const { passRate, totalRoutes, passed, failed } = qaReport.summary || {};
203
+
204
+ const icon = (failed || 0) === 0 ? '✅' : '❌';
205
+ const body = `${icon} **AI QA Report**
206
+
207
+ | Metric | Value |
208
+ |--------|-------|
209
+ | Routes tested | ${totalRoutes || 0} |
210
+ | Passed | ${passed || 0} |
211
+ | Failed | ${failed || 0} |
212
+ | Pass rate | ${passRate || 0}% |
213
+
214
+ ${(failed || 0) > 0 && qaReport.failed ? `### Failed Pages\n${qaReport.failed.map(f => `- \`${f.url}\`: ${f.errors?.[0] || 'unknown error'}`).join('\n')}` : ''}
215
+
216
+ ---
217
+ *Generated by AI Control Center QA Agent*`;
218
+
219
+ await octokit.issues.createComment({
220
+ owner, repo,
221
+ issue_number: latestPR.number,
222
+ body,
223
+ });
224
+
225
+ logActivity('GITHUB', `Commented on PR #${latestPR.number} with QA results`, 'success');
226
+ } catch (err) {
227
+ logActivity('GITHUB', `Failed to comment on PR: ${err.message}`, 'error');
228
+ }
229
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Screenshot Store — manages screenshot capture, storage, and comparison.
3
+ *
4
+ * Handles:
5
+ * - Capturing full-page screenshots via Playwright page object
6
+ * - Storing baselines (first-run reference images)
7
+ * - Comparing new screenshots against baselines (pixel diff)
8
+ * - Returning structured metadata about each screenshot
9
+ */
10
+
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
12
+ import { join } from 'path';
13
+ import { PNG } from 'pngjs';
14
+ import pixelmatch from 'pixelmatch';
15
+
16
+ export class ScreenshotStore {
17
+ constructor(baseDir) {
18
+ this.baseDir = baseDir;
19
+ this.baselineDir = join(baseDir, 'baselines');
20
+ this.diffDir = join(baseDir, 'diffs');
21
+ this.currentDir = join(baseDir, 'current');
22
+ for (const dir of [this.baselineDir, this.diffDir, this.currentDir]) {
23
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Capture a screenshot of the current page.
29
+ * @param {import('playwright').Page} page - Playwright page object
30
+ * @param {string} name - Slug name for the screenshot
31
+ * @param {object} options - { baseline: true } to force as baseline
32
+ * @returns {object} { name, path, baseline, isBaseline, timestamp }
33
+ */
34
+ async capture(page, name, options = {}) {
35
+ const timestamp = Date.now();
36
+ const filename = `${name}.png`;
37
+ const currentPath = join(this.currentDir, filename);
38
+ const baselinePath = join(this.baselineDir, filename);
39
+
40
+ await page.screenshot({ path: currentPath, fullPage: true });
41
+
42
+ const isFirstRun = !existsSync(baselinePath);
43
+ if (isFirstRun || options.baseline) {
44
+ const data = readFileSync(currentPath);
45
+ writeFileSync(baselinePath, data);
46
+ }
47
+
48
+ return {
49
+ name,
50
+ path: currentPath,
51
+ baseline: baselinePath,
52
+ isBaseline: isFirstRun || !!options.baseline,
53
+ timestamp,
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Compare current screenshot with baseline.
59
+ * @param {object} shotMeta - result from capture()
60
+ * @param {string} name - slug name
61
+ * @returns {boolean} true if visual regression detected (diff > threshold)
62
+ */
63
+ async compareWithBaseline(shotMeta, name) {
64
+ const baselinePath = join(this.baselineDir, `${name}.png`);
65
+ const currentPath = shotMeta.path || join(this.currentDir, `${name}.png`);
66
+
67
+ if (!existsSync(baselinePath) || !existsSync(currentPath)) return false;
68
+ if (shotMeta.isBaseline) return false;
69
+
70
+ try {
71
+ const img1 = PNG.sync.read(readFileSync(baselinePath));
72
+ const img2 = PNG.sync.read(readFileSync(currentPath));
73
+
74
+ if (img1.width !== img2.width || img1.height !== img2.height) {
75
+ return true;
76
+ }
77
+
78
+ const diff = new PNG({ width: img1.width, height: img1.height });
79
+ const numDiff = pixelmatch(img1.data, img2.data, diff.data, img1.width, img1.height, { threshold: 0.1 });
80
+ const totalPx = img1.width * img1.height;
81
+ const diffPct = numDiff / totalPx;
82
+
83
+ if (diffPct > 0.02) {
84
+ const diffPath = join(this.diffDir, `${name}_diff.png`);
85
+ writeFileSync(diffPath, PNG.sync.write(diff));
86
+ return true;
87
+ }
88
+
89
+ return false;
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Get base64 encoded screenshot for embedding in GitHub issues.
97
+ */
98
+ getBase64(shotPath) {
99
+ if (!existsSync(shotPath)) return null;
100
+ return readFileSync(shotPath).toString('base64');
101
+ }
102
+ }