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,523 @@
1
+ /**
2
+ * aicc doctor — Health check + auto-repair for an initialized project.
3
+ *
4
+ * Checks everything that `aicc init` creates and fixes anything missing,
5
+ * without prompting the user. Safe to run on any already-initialized project.
6
+ */
7
+
8
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'fs';
9
+ import { resolve, dirname } from 'path';
10
+ import { fileURLToPath } from 'url';
11
+ import { spawnSync } from 'child_process';
12
+ import chalk from 'chalk';
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+
16
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
17
+
18
+ function loadTemplate(name) {
19
+ const templateDir = resolve(__dirname, '../../templates');
20
+ return readFileSync(resolve(templateDir, name), 'utf8');
21
+ }
22
+
23
+ function renderTemplate(content, vars) {
24
+ return content
25
+ .replace(/\{\{PROJECT_NAME\}\}/g, vars.name)
26
+ .replace(/\{\{PROJECT_DESCRIPTION\}\}/g, vars.description || 'A project managed by AI Control Center.')
27
+ .replace(/\{\{DESCRIPTION\}\}/g, vars.description || 'A project managed by AI Control Center.')
28
+ .replace(/\{\{PROJECT_ROOT\}\}/g, vars.root || process.cwd())
29
+ .replace(/\{\{WORKFLOW_DIR\}\}/g, vars.workflowDir || '.ai-workflow');
30
+ }
31
+
32
+ function isAvailableTool(cmd) {
33
+ const result = spawnSync(cmd, ['--version'], { encoding: 'utf8', timeout: 5000, stdio: 'pipe' });
34
+ return result.status === 0 || (result.stderr && !result.error);
35
+ }
36
+
37
+ // Check / create a file. Returns 'ok' | 'created' | 'skipped'
38
+ function checkFile(filePath, content, label, results) {
39
+ if (existsSync(filePath)) {
40
+ results.push({ status: 'ok', label });
41
+ return 'ok';
42
+ }
43
+ mkdirSync(dirname(filePath), { recursive: true });
44
+ writeFileSync(filePath, content);
45
+ results.push({ status: 'created', label });
46
+ return 'created';
47
+ }
48
+
49
+ // Check a directory exists. Returns 'ok' | 'created'
50
+ function checkDir(dirPath, label, results) {
51
+ if (existsSync(dirPath)) {
52
+ results.push({ status: 'ok', label });
53
+ return 'ok';
54
+ }
55
+ mkdirSync(dirPath, { recursive: true });
56
+ results.push({ status: 'created', label });
57
+ return 'created';
58
+ }
59
+
60
+ // ─── Main ─────────────────────────────────────────────────────────────────────
61
+
62
+ export async function runDoctor() {
63
+ const cwd = process.cwd();
64
+
65
+ console.log(chalk.cyan.bold('\n 🩺 aicc doctor\n'));
66
+ console.log(chalk.dim(' Checking project setup and repairing anything missing...\n'));
67
+
68
+ // ── Load config ──────────────────────────────────────────────────────────────
69
+ const configPath = resolve(cwd, 'aicc.config.js');
70
+ if (!existsSync(configPath)) {
71
+ console.log(chalk.red(' ✗ aicc.config.js not found'));
72
+ console.log(chalk.yellow('\n This project has not been initialized. Run: aicc init\n'));
73
+ process.exit(1);
74
+ }
75
+
76
+ let config;
77
+ try {
78
+ const imported = await import(`${configPath}?t=${Date.now()}`);
79
+ config = imported.default;
80
+ } catch (err) {
81
+ console.log(chalk.red(` ✗ aicc.config.js failed to load: ${err.message}`));
82
+ process.exit(1);
83
+ }
84
+
85
+ console.log(chalk.dim(` Project: ${chalk.white(config.name || cwd)}`));
86
+ console.log(chalk.dim(` Config: aicc.config.js ✓\n`));
87
+
88
+ const vars = {
89
+ name: config.name || 'My Project',
90
+ description: config.description || '',
91
+ root: cwd,
92
+ workflowDir: config.workflowDir || '.ai-workflow',
93
+ };
94
+
95
+ const skillsDir = resolve(cwd, config.skillsDir || '.claude/skills');
96
+ const agentsDir = resolve(cwd, '.claude/agents');
97
+ const githubDir = resolve(cwd, '.github');
98
+ const workflowDir = resolve(cwd, vars.workflowDir);
99
+
100
+ const results = [];
101
+ let fixCount = 0;
102
+
103
+ // ── Section 1: AI Tools ───────────────────────────────────────────────────────
104
+ console.log(chalk.white(' ── AI Tools ──'));
105
+
106
+ const tools = {
107
+ copilot: isAvailableTool('copilot'),
108
+ gemini: isAvailableTool('gemini'),
109
+ claude: isAvailableTool('claude'),
110
+ openclaw: isAvailableTool('openclaw'),
111
+ gh: isAvailableTool('gh'),
112
+ };
113
+
114
+ const icon = (ok) => ok ? chalk.green('✓') : chalk.yellow('~');
115
+ console.log(` ${icon(tools.copilot)} Copilot CLI ${tools.copilot ? chalk.dim('installed') : chalk.yellow('not found — install from https://github.com/github/gh-copilot')}`);
116
+ console.log(` ${icon(tools.gemini)} Gemini CLI ${tools.gemini ? chalk.dim('installed') : chalk.yellow('not found — run: npm i -g @google/generative-ai-cli')}`);
117
+ console.log(` ${icon(tools.claude)} Claude CLI ${tools.claude ? chalk.dim('installed') : chalk.yellow('not found — install from https://claude.ai/code')}`);
118
+ console.log(` ${icon(tools.openclaw)} OpenClaw ${tools.openclaw ? chalk.dim('installed') : chalk.yellow('not found (optional)')}`);
119
+ console.log(` ${icon(tools.gh)} GitHub CLI ${tools.gh ? chalk.dim('installed') : chalk.yellow('not found — run: brew install gh')}`);
120
+
121
+ // ── Section 2: Skills ─────────────────────────────────────────────────────────
122
+ console.log(chalk.white('\n ── Skills (.claude/skills/) ──'));
123
+
124
+ const skillFiles = [
125
+ { file: resolve(skillsDir, 'architect.md'), template: 'skill-architect.md', label: 'architect.md ← Claude architect prompt' },
126
+ { file: resolve(skillsDir, 'copilot-implement.md'), template: 'skill-implement.md', label: 'copilot-implement.md ← Copilot implementation prompt' },
127
+ { file: resolve(skillsDir, 'code-review.md'), template: 'skill-review.md', label: 'code-review.md ← Gemini code review prompt' },
128
+ { file: resolve(skillsDir, 'chatbot.md'), template: 'skill-chatbot.md', label: 'chatbot.md ← AI chatbot behavior rules' },
129
+ ];
130
+
131
+ for (const { file, template, label } of skillFiles) {
132
+ const content = renderTemplate(loadTemplate(template), vars);
133
+ const status = checkFile(file, content, label, results);
134
+ if (status === 'created') {
135
+ console.log(chalk.green(` ✓ Created: ${label}`));
136
+ fixCount++;
137
+ } else {
138
+ console.log(chalk.dim(` · OK: ${label}`));
139
+ }
140
+ }
141
+
142
+ // ── Section 3: Agents ─────────────────────────────────────────────────────────
143
+ console.log(chalk.white('\n ── Agents (.claude/agents/) ──'));
144
+
145
+ const agentFiles = [
146
+ { file: resolve(agentsDir, 'architect.agent.md'), template: 'agent-architect.md', label: 'architect.agent.md', render: false },
147
+ { file: resolve(agentsDir, 'gemini-pm.agent.md'), template: 'agent-gemini-pm.md', label: 'gemini-pm.agent.md', render: false, requires: 'gemini' },
148
+ { file: resolve(agentsDir, 'gemini-reviewer.agent.md'), template: 'agent-gemini-reviewer.md', label: 'gemini-reviewer.agent.md', render: false, requires: 'gemini' },
149
+ ];
150
+
151
+ for (const { file, template, label, render, requires } of agentFiles) {
152
+ if (requires && !tools[requires]) {
153
+ console.log(chalk.dim(` · Skipped: ${label} (${requires} not installed)`));
154
+ continue;
155
+ }
156
+ const raw = loadTemplate(template);
157
+ const content = render ? renderTemplate(raw, vars) : raw;
158
+ const status = checkFile(file, content, label, results);
159
+ if (status === 'created') {
160
+ console.log(chalk.green(` ✓ Created: ${label}`));
161
+ fixCount++;
162
+ } else {
163
+ console.log(chalk.dim(` · OK: ${label}`));
164
+ }
165
+ }
166
+
167
+ // ── Section 4: Role Files ─────────────────────────────────────────────────────
168
+ console.log(chalk.white('\n ── Role Files ──'));
169
+
170
+ const roleFiles = [
171
+ { file: resolve(cwd, 'GEMINI.md'), template: 'role-gemini.md', label: 'GEMINI.md', render: true, requires: 'gemini' },
172
+ { file: resolve(githubDir, 'copilot-instructions.md'), template: 'copilot-instructions.md', label: '.github/copilot-instructions.md', render: true, requires: 'copilot' },
173
+ ];
174
+
175
+ for (const { file, template, label, render, requires } of roleFiles) {
176
+ if (requires && !tools[requires]) {
177
+ console.log(chalk.dim(` · Skipped: ${label} (${requires} not installed)`));
178
+ continue;
179
+ }
180
+ const raw = loadTemplate(template);
181
+ const content = render ? renderTemplate(raw, vars) : raw;
182
+ const status = checkFile(file, content, label, results);
183
+ if (status === 'created') {
184
+ console.log(chalk.green(` ✓ Created: ${label}`));
185
+ fixCount++;
186
+ } else {
187
+ console.log(chalk.dim(` · OK: ${label}`));
188
+ }
189
+ }
190
+
191
+ // ── Section 5: Workflow Directories ───────────────────────────────────────────
192
+ console.log(chalk.white('\n ── Workflow Directories ──'));
193
+
194
+ const workflowSubdirs = ['specs', 'architecture', 'tasks', 'reviews', 'approved', 'logs', 'inbox', 'implementation-notes', 'deploy-fixes', 'sessions', 'rejected', 'checkpoints', '.pids'];
195
+
196
+ for (const sub of workflowSubdirs) {
197
+ const dir = resolve(workflowDir, sub);
198
+ const status = checkDir(dir, `${vars.workflowDir}/${sub}/`, results);
199
+ if (status === 'created') {
200
+ console.log(chalk.green(` ✓ Created: ${vars.workflowDir}/${sub}/`));
201
+ fixCount++;
202
+ } else {
203
+ console.log(chalk.dim(` · OK: ${vars.workflowDir}/${sub}/`));
204
+ }
205
+ }
206
+
207
+ // ── Section 6: status.json ────────────────────────────────────────────────────
208
+ const statusFile = resolve(workflowDir, 'status.json');
209
+ const statusLabel = `${vars.workflowDir}/status.json`;
210
+ const statusContent = JSON.stringify({ stage: 'idle', current_feature: null, pipeline_mode: 'manual' }, null, 2);
211
+ const statusResult = checkFile(statusFile, statusContent, statusLabel, results);
212
+ if (statusResult === 'created') {
213
+ console.log(chalk.green(` ✓ Created: ${statusLabel}`));
214
+ fixCount++;
215
+ }
216
+
217
+ // ── Section 7: .gitignore entries ────────────────────────────────────────────
218
+ const gitignorePath = resolve(cwd, '.gitignore');
219
+ if (existsSync(gitignorePath)) {
220
+ const gitignore = readFileSync(gitignorePath, 'utf8');
221
+ const logsEntry = `${vars.workflowDir}/logs/`;
222
+ if (!gitignore.includes(logsEntry)) {
223
+ appendFileSync(gitignorePath, `\n# AI workflow logs\n${logsEntry}\n`);
224
+ console.log(chalk.green(`\n ✓ Added ${logsEntry} to .gitignore`));
225
+ fixCount++;
226
+ }
227
+ }
228
+
229
+ // ── Section 7b: .env file check ───────────────────────────────────────────────
230
+ console.log(chalk.white('\n ── Environment (.env) ──'));
231
+ const envPath = resolve(cwd, '.env');
232
+ const envPrefix = config.envPrefix || 'AICC';
233
+ if (!existsSync(envPath)) {
234
+ console.log(chalk.yellow(` ~ .env not found — create one with your credentials`));
235
+ console.log(chalk.dim(` Required for Telegram: ${envPrefix}_TELEGRAM_TOKEN, ${envPrefix}_TELEGRAM_CHAT_ID`));
236
+ console.log(chalk.dim(` Required for AI mode: ${envPrefix}_AI_MODE`));
237
+ } else {
238
+ const envContent = readFileSync(envPath, 'utf8');
239
+ const tokenKey = `${envPrefix}_TELEGRAM_TOKEN`;
240
+ const chatIdKey = `${envPrefix}_TELEGRAM_CHAT_ID`;
241
+ const hasToken = envContent.includes(tokenKey);
242
+ const hasChatId = envContent.includes(chatIdKey);
243
+
244
+ console.log(` ${hasToken ? chalk.green('✓') : chalk.yellow('~')} ${tokenKey} ${hasToken ? chalk.dim('present') : chalk.yellow('missing — Telegram bot will not work')}`);
245
+ console.log(` ${hasChatId ? chalk.green('✓') : chalk.yellow('~')} ${chatIdKey}${hasChatId ? chalk.dim(' present') : chalk.yellow(' missing — push notifications disabled')}`);
246
+
247
+ // Warn if CHAT_ID looks like a bot token ID (same numeric prefix as token)
248
+ if (hasToken && hasChatId) {
249
+ const tokenMatch = envContent.match(new RegExp(`${tokenKey}=([0-9]+):`));
250
+ const chatIdMatch = envContent.match(new RegExp(`${chatIdKey}=([0-9]+)`));
251
+ if (tokenMatch && chatIdMatch && tokenMatch[1] === chatIdMatch[1]) {
252
+ console.log(chalk.yellow(` ⚠ ${chatIdKey} looks like the bot's own ID (${chatIdMatch[1]})`));
253
+ console.log(chalk.dim(' Send a message to your bot then check /getUpdates to find your real chat ID'));
254
+ }
255
+ }
256
+ }
257
+
258
+ // ── Section 8: Pipeline Config Validation ────────────────────────────────────
259
+ console.log(chalk.white('\n ── Pipeline Config ──'));
260
+
261
+ // Warn if using old `models` format without `pipeline`
262
+ if (config.models && !config.pipeline) {
263
+ console.log(chalk.yellow(' ⚠ Using legacy `models` config — will auto-migrate at runtime'));
264
+ console.log(chalk.dim(' Upgrade: replace `models: {...}` with `pipeline: [...]` in aicc.config.js'));
265
+ console.log(chalk.dim(' See README.md for the new format'));
266
+ }
267
+
268
+ const pipeline = config.pipeline || [];
269
+ const REQUIRED_STAGES = ['chat', 'pm', 'architect', 'implement', 'review', 'deploy'];
270
+ const VALID_PROVIDERS = ['copilot', 'claude', 'gemini', 'ollama', 'openclaw'];
271
+
272
+ if (pipeline.length > 0) {
273
+ // Check all required stages exist
274
+ const configuredStages = pipeline.map(e => e.stage);
275
+ for (const stage of REQUIRED_STAGES) {
276
+ const entry = pipeline.find(e => e.stage === stage);
277
+ if (!entry) {
278
+ console.log(chalk.yellow(` ~ Missing stage: "${stage}" — will use default`));
279
+ } else if (!entry.provider || !entry.model) {
280
+ console.log(chalk.yellow(` ~ Stage "${stage}" missing provider or model`));
281
+ } else if (!VALID_PROVIDERS.includes(entry.provider)) {
282
+ console.log(chalk.yellow(` ~ Stage "${stage}" has unknown provider: "${entry.provider}"`));
283
+ } else {
284
+ // Check if the provider's CLI tool is installed
285
+ const providerTool = entry.provider === 'claude' ? 'claude' : entry.provider;
286
+ const toolInstalled = entry.provider === 'ollama' || tools[providerTool];
287
+ if (toolInstalled) {
288
+ console.log(chalk.dim(` · ${stage.padEnd(10)} ${entry.provider.padEnd(8)} ${entry.model}`));
289
+ } else {
290
+ console.log(chalk.yellow(` ~ ${stage.padEnd(10)} ${entry.provider.padEnd(8)} ${entry.model} ← ${entry.provider} CLI not installed`));
291
+ }
292
+ }
293
+ }
294
+
295
+ // Check fallback entries have valid format
296
+ for (const entry of pipeline) {
297
+ if (entry.fallbacks) {
298
+ for (const fb of entry.fallbacks) {
299
+ if (fb !== 'ollama' && fb !== 'gemini' && fb !== 'copilot' && fb !== 'claude' && !fb.includes(':')) {
300
+ console.log(chalk.yellow(` ~ Stage "${entry.stage}" fallback "${fb}" — expected "provider:model" format`));
301
+ }
302
+ }
303
+ }
304
+ }
305
+ } else if (!config.models) {
306
+ console.log(chalk.yellow(' ~ No pipeline config found — using defaults'));
307
+ }
308
+
309
+ // Ollama config check
310
+ const ollamaCfg = config.ollama || {};
311
+ if (ollamaCfg.baseUrl && ollamaCfg.baseUrl !== 'http://localhost:11434') {
312
+ console.log(chalk.dim(` · Ollama: ${ollamaCfg.model || 'llama3.1'} @ ${ollamaCfg.baseUrl}`));
313
+ }
314
+
315
+ // ── OpenClaw integration check ────────────────────────────────────────────────
316
+ console.log(chalk.white('\n ── OpenClaw ──'));
317
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
318
+ const openclawConfigExists = existsSync(resolve(homeDir, '.openclaw', 'openclaw.json'));
319
+
320
+ if (openclawConfigExists) {
321
+ console.log(chalk.green(' ✓ ~/.openclaw/openclaw.json found'));
322
+
323
+ // Check bridge config
324
+ const bridgeEnabled = config.roleplay?.openclawBridge !== false;
325
+ console.log(bridgeEnabled
326
+ ? chalk.green(' ✓ OpenClaw bridge enabled (roleplay.openclawBridge: true)')
327
+ : chalk.dim(' · OpenClaw bridge disabled (set roleplay.openclawBridge: true to enable)'));
328
+
329
+ // Check gateway port
330
+ try {
331
+ const ocCfg = JSON.parse(readFileSync(resolve(homeDir, '.openclaw', 'openclaw.json'), 'utf8'));
332
+ const port = ocCfg.gateway?.port || 18789;
333
+ console.log(chalk.dim(` · Gateway port: ${port}`));
334
+ } catch { /* ok */ }
335
+
336
+ // Validate OpenClaw stage skills exist
337
+ const slug = (config.name || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
338
+ const ocSkillDir = resolve(homeDir, '.openclaw', 'skills', slug);
339
+ const REQUIRED_SKILLS = [
340
+ 'SKILL.md',
341
+ 'SKILL-requirement-capture.md',
342
+ 'SKILL-pm-spec.md',
343
+ 'SKILL-architect.md',
344
+ 'SKILL-implement.md',
345
+ 'SKILL-review.md',
346
+ ];
347
+
348
+ let missingSkills = 0;
349
+ for (const skillFile of REQUIRED_SKILLS) {
350
+ if (existsSync(resolve(ocSkillDir, skillFile))) {
351
+ console.log(chalk.dim(` · ${skillFile}`));
352
+ } else {
353
+ console.log(chalk.yellow(` ~ Missing: ${skillFile}`));
354
+ missingSkills++;
355
+ }
356
+ }
357
+
358
+ // Also check .ai-workflow/skills/ for stage skills
359
+ const wfSkillDir = resolve(workflowDir, 'skills');
360
+ const STAGE_SKILL_FILES = ['SKILL-pm-spec.md', 'SKILL-architect.md', 'SKILL-implement.md', 'SKILL-review.md'];
361
+ for (const sf of STAGE_SKILL_FILES) {
362
+ if (!existsSync(resolve(wfSkillDir, sf))) {
363
+ missingSkills++;
364
+ }
365
+ }
366
+
367
+ // Auto-regenerate if skills are missing
368
+ if (missingSkills > 0) {
369
+ console.log(chalk.yellow(` ~ ${missingSkills} skill(s) missing — regenerating...`));
370
+ try {
371
+ const { generateSkill } = await import('../openclaw/generate-skill.js');
372
+ await generateSkill();
373
+ fixCount++;
374
+ } catch (err) {
375
+ console.log(chalk.yellow(` ~ Skill regeneration failed: ${err.message}`));
376
+ }
377
+ } else if (tools.openclaw) {
378
+ // Skills exist but regenerate to pick up any config changes
379
+ try {
380
+ const { generateSkill } = await import('../openclaw/generate-skill.js');
381
+ await generateSkill();
382
+ } catch {
383
+ console.log(chalk.dim(' · OpenClaw skill refresh skipped'));
384
+ }
385
+ } else {
386
+ console.log(chalk.green(' ✓ All 6 OpenClaw skills present'));
387
+ }
388
+ } else {
389
+ console.log(chalk.dim(' · OpenClaw not configured — install OpenClaw for multi-channel support'));
390
+ console.log(chalk.dim(' Skills will use local .ai-workflow/skills/ only'));
391
+ }
392
+
393
+ // ── Section 9: AI IT Department — Browser QA & Automation ────────────────────
394
+ console.log(chalk.white('\n ── AI IT Department ──'));
395
+
396
+ // 9a. Check Playwright installation
397
+ let playwrightOk = false;
398
+ try {
399
+ const pw = await import('playwright');
400
+ if (pw.chromium) {
401
+ playwrightOk = true;
402
+ console.log(chalk.green(' ✓ Playwright installed'));
403
+ }
404
+ } catch {
405
+ console.log(chalk.yellow(' ✗ Playwright not installed — run: npm install playwright && npx playwright install chromium'));
406
+ }
407
+
408
+ // 9b. Check Playwright browser binary
409
+ if (playwrightOk) {
410
+ try {
411
+ const { spawnSync } = await import('child_process');
412
+ const result = spawnSync('npx', ['playwright', 'install', '--dry-run', 'chromium'], { encoding: 'utf8', timeout: 10000 });
413
+ console.log(chalk.dim(' · Chromium browser binary available'));
414
+ } catch {
415
+ console.log(chalk.yellow(' ~ Chromium binary may be missing — run: npx playwright install chromium'));
416
+ }
417
+ }
418
+
419
+ // 9c. Check @octokit/rest
420
+ try {
421
+ await import('@octokit/rest');
422
+ console.log(chalk.green(' ✓ @octokit/rest installed'));
423
+ } catch {
424
+ console.log(chalk.yellow(' ✗ @octokit/rest not installed — run: npm install @octokit/rest'));
425
+ console.log(chalk.dim(' Required for GitHub issue auto-creation from QA reports'));
426
+ }
427
+
428
+ // 9d. Check browserQA config
429
+ const bqaCfg = config.browserQA;
430
+ if (bqaCfg?.enabled) {
431
+ console.log(chalk.green(' ✓ browserQA.enabled = true'));
432
+ if (bqaCfg.targetUrl) {
433
+ console.log(chalk.dim(` · Target URL: ${bqaCfg.targetUrl}`));
434
+ } else {
435
+ console.log(chalk.yellow(' ✗ browserQA.targetUrl not set — QA agent needs a URL to test'));
436
+ }
437
+ if (bqaCfg.credentials?.email) {
438
+ console.log(chalk.dim(` · QA credentials: ${bqaCfg.credentials.email.slice(0, 3)}***`));
439
+ } else {
440
+ console.log(chalk.dim(' · No QA credentials — agent will test without login'));
441
+ }
442
+ console.log(chalk.dim(` · Max bugfix cycles: ${bqaCfg.maxBugfixCycles || 3}`));
443
+ console.log(chalk.dim(` · Schedule: ${bqaCfg.schedule || 'manual'}`));
444
+ } else {
445
+ console.log(chalk.dim(' · browserQA disabled — enable in aicc.config.js to use Browser QA'));
446
+ }
447
+
448
+ // 9e. Check GitHub integration
449
+ const ghCfg = config.github;
450
+ if (ghCfg?.enabled) {
451
+ if (ghCfg.token || process.env[`${envPrefix}_GITHUB_TOKEN`]) {
452
+ console.log(chalk.green(' ✓ GitHub integration configured'));
453
+ console.log(chalk.dim(` · Repo: ${ghCfg.owner || '?'}/${ghCfg.repo || '?'}`));
454
+ } else {
455
+ console.log(chalk.yellow(' ✗ GitHub enabled but no token — set github.token or GITHUB_TOKEN env var'));
456
+ }
457
+ } else {
458
+ console.log(chalk.dim(' · GitHub integration disabled — QA bugs will not create GitHub issues'));
459
+ }
460
+
461
+ // 9f. Check suggestion agent config
462
+ const sugCfg = config.suggestion;
463
+ if (sugCfg?.enabled) {
464
+ console.log(chalk.green(` ✓ Suggestion agent enabled (idle trigger: ${sugCfg.idleAfterMinutes || 60} min)`));
465
+ } else {
466
+ console.log(chalk.dim(' · Suggestion agent disabled'));
467
+ }
468
+
469
+ // 9g. Validate new pipeline stages exist in config
470
+ const pipelineStages = (config.pipeline || []).map(e => e.stage);
471
+ const IT_STAGES = ['browser-qa', 'bugfix', 'suggestion'];
472
+ for (const itStage of IT_STAGES) {
473
+ if (pipelineStages.includes(itStage)) {
474
+ console.log(chalk.green(` ✓ Pipeline stage "${itStage}" configured`));
475
+ } else {
476
+ console.log(chalk.dim(` · Pipeline stage "${itStage}" not in config — will use defaults`));
477
+ }
478
+ }
479
+
480
+ // 9h. Check new workflow directories
481
+ const IT_DIRS = ['screenshots', 'qa-reports', 'discussion-threads', 'feature-suggestions'];
482
+ for (const itDir of IT_DIRS) {
483
+ const dirPath = resolve(workflowDir, itDir);
484
+ if (existsSync(dirPath)) {
485
+ console.log(chalk.dim(` · ${itDir}/`));
486
+ } else {
487
+ mkdirSync(dirPath, { recursive: true });
488
+ console.log(chalk.green(` + Created ${itDir}/`));
489
+ fixCount++;
490
+ }
491
+ }
492
+
493
+ // 9i. Check new skill templates exist
494
+ const rootDir = config._root || config._rootDir || process.cwd();
495
+ const IT_SKILLS = [
496
+ 'templates/skill-browser-qa.md',
497
+ 'templates/skill-bug-from-qa.md',
498
+ 'templates/skill-suggestion.md',
499
+ 'templates/skill-payment.md',
500
+ 'templates/skill-reviewer-qa.md',
501
+ ];
502
+ for (const skillPath of IT_SKILLS) {
503
+ const fullPath = resolve(rootDir, skillPath);
504
+ if (existsSync(fullPath)) {
505
+ console.log(chalk.dim(` · ${skillPath}`));
506
+ } else {
507
+ console.log(chalk.yellow(` ✗ Missing: ${skillPath}`));
508
+ }
509
+ }
510
+
511
+ // ── Summary ───────────────────────────────────────────────────────────────────
512
+ console.log();
513
+ if (fixCount === 0) {
514
+ console.log(chalk.green(' ✅ Everything looks good! No repairs needed.\n'));
515
+ } else {
516
+ console.log(chalk.green(` ✅ Doctor complete — ${fixCount} item${fixCount === 1 ? '' : 's'} repaired.\n`));
517
+ }
518
+
519
+ // Tip: re-run restart to pick up any skill changes
520
+ if (fixCount > 0) {
521
+ console.log(chalk.dim(' Tip: run `aicc stop && aicc start` to apply changes to running services.\n'));
522
+ }
523
+ }