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,513 @@
1
+ import { getRoleplayConfig } from './roleplay-config.js';
2
+ import { postMessage, postAction, askCEO, getRoomHistory, onCEOMessage, dispatchCEOMessage } from './room.js';
3
+ import { startMonitor, stopMonitor, getProviderStatus, getStageHealth, getAvailableProviders, suggestRecoveryAction } from './health-monitor.js';
4
+ import { openThread, postToThread, escalateThread } from './discussion-threads.js';
5
+
6
+ // Lazy-loaded askAI — avoid circular import with action-runner
7
+ let _askAI = null;
8
+ async function getAskAI() {
9
+ if (!_askAI) {
10
+ const mod = await import('../shared/action-runner.js');
11
+ _askAI = mod.askAI;
12
+ }
13
+ return _askAI;
14
+ }
15
+
16
+ let _bus = null;
17
+ let _room = null;
18
+ let _running = false;
19
+ let _progressTimer = null;
20
+ let _recoveryWatcher = null;
21
+
22
+ const PM_SYSTEM_PROMPT = `You are the PM (Project Manager) of a software development team. You communicate in a group chat with your boss (the CEO/user) and your team (Architect, Coder, Reviewer, Deployer).
23
+
24
+ Rules:
25
+ - Keep messages SHORT (1-3 sentences max)
26
+ - Be professional and informative
27
+ - Use emoji sparingly (max 1-2 per message)
28
+ - Never show raw error logs — summarize issues clearly
29
+ - Reference team members with @Name format
30
+ - Stay positive and solution-focused
31
+ `;
32
+
33
+ // PM internal state — tracked per feature
34
+ const pmState = {
35
+ featureId: null,
36
+ retryCount: {},
37
+ providerSwitches: {},
38
+ reviewCycles: 0,
39
+ totalErrors: 0,
40
+ lastErrorTime: null,
41
+ consecutiveErrors: 0,
42
+ stallRecoveries: 0,
43
+ providerRecoveryWatcher: null,
44
+ stageStartTimes: {},
45
+ currentStage: null,
46
+ lastProgressUpdate: 0,
47
+ lastSuggestionTime: 0,
48
+
49
+ MAX_RETRIES_PER_STAGE: 5,
50
+ MAX_REVIEW_CYCLES: 3,
51
+ MAX_CONSECUTIVE_ERRORS: 8,
52
+ MAX_STALL_RECOVERIES: 3,
53
+ PROVIDER_RECOVERY_INTERVAL: 60_000,
54
+ ALL_DOWN_RETRY_INTERVAL: 300_000,
55
+ ALL_DOWN_MAX_WAIT: 1_800_000,
56
+ };
57
+
58
+ function resetPMState(featureId) {
59
+ pmState.featureId = featureId;
60
+ pmState.retryCount = {};
61
+ pmState.providerSwitches = {};
62
+ pmState.reviewCycles = 0;
63
+ pmState.totalErrors = 0;
64
+ pmState.lastErrorTime = null;
65
+ pmState.consecutiveErrors = 0;
66
+ pmState.stallRecoveries = 0;
67
+ pmState.stageStartTimes = {};
68
+ pmState.currentStage = null;
69
+ pmState.lastProgressUpdate = 0;
70
+ }
71
+
72
+ // Generate a PM message using a simple template approach
73
+ // (In production this would call runAI with haiku, but we keep it deterministic for testability)
74
+ function generatePMMessage(situation, context = {}) {
75
+ const messages = {
76
+ feature_received: (ctx) => `Got it! Analyzing "${ctx.feature?.slice(0, 60) || 'your request'}"...`,
77
+ writing_spec: () => `Writing the spec now. Give me ~2 minutes.`,
78
+ spec_done: (ctx) => `Spec done! ${ctx.lines ? `(${ctx.lines} lines) ` : ''}@Architect, please design this.`,
79
+ arch_done: (ctx) => `Architecture ready${ctx.summary ? `: ${ctx.summary}` : ''}. @Coder, start implementing.`,
80
+ impl_done: () => `Implementation complete! @Reviewer, please review.`,
81
+ review_found_issues: (ctx) => `Review found ${ctx.count || 'some'} issue(s). @Coder, please fix: ${ctx.summary || 'see review output'}.`,
82
+ review_approved: () => `Code approved! ✅ Ready to deploy, boss.`,
83
+ still_working: (ctx) => `${ctx.role || 'Team'} is still working... ${ctx.stage ? `(${ctx.stage} stage)` : ''} Hang tight.`,
84
+ switching_provider: (ctx) => `Switching from ${ctx.from || 'current provider'} to ${ctx.to || 'backup'}. Back on track shortly.`,
85
+ provider_recovered: (ctx) => `${ctx.provider || 'Provider'} is back online. Resuming pipeline.`,
86
+ all_providers_down: () => `All AI providers are currently unavailable. I'll keep retrying every 5 minutes.`,
87
+ retrying: (ctx) => `Retry ${ctx.attempt || 1}: trying again...`,
88
+ stage_complete: (ctx) => `${ctx.stage || 'Stage'} complete. Moving on.`,
89
+ error_handled: (ctx) => `Had an issue: ${ctx.summary || 'something went wrong'}. Switched to ${ctx.action || 'backup'}. Continuing.`,
90
+ review_loop_broken: () => `This is review cycle 3 — remaining issues are minor. Auto-approving and noting for follow-up.`,
91
+ deploy_ask: () => `Feature ready! Deploy now? [Yes/No]`,
92
+ deploy_done: (ctx) => `Deployed! 🚀 Summary: ${ctx.duration ? `⏱ ${ctx.duration}` : ''} ${ctx.cost ? `💰 ${ctx.cost}` : ''}`,
93
+ pipeline_paused: () => `Pipeline paused — all providers down. I'll auto-resume when any provider recovers.`,
94
+ absent_decision: (ctx) => `I went ahead with ${ctx.decision || 'the best option'} since you were away. Let me know if you want to change it.`,
95
+ };
96
+
97
+ const fn = messages[situation];
98
+ if (fn) return fn(context);
99
+ return `Update: ${situation}`;
100
+ }
101
+
102
+ export function startPMAgent(bus, room) {
103
+ if (_running) return;
104
+ _bus = bus;
105
+ _room = room;
106
+ _running = true;
107
+
108
+ const config = getRoleplayConfig();
109
+
110
+ // Start health monitor
111
+ startMonitor(bus, { checkInterval: 30_000 });
112
+
113
+ // Listen to CEO messages
114
+ onCEOMessage((message, ctx) => {
115
+ handleCEOMessage(message, ctx).catch(() => {});
116
+ });
117
+
118
+ // Listen to pipeline events
119
+ bus.on('pipeline-event', ({ event, data }) => {
120
+ try {
121
+ handlePipelineEvent(event, data);
122
+ } catch { /* non-fatal */ }
123
+ });
124
+
125
+ // Progress update timer
126
+ if (config.progressUpdates) {
127
+ _progressTimer = setInterval(() => {
128
+ checkProgress();
129
+ }, Math.min(config.progressInterval || 60_000, 120_000));
130
+ }
131
+ }
132
+
133
+ export function stopPMAgent() {
134
+ _running = false;
135
+ if (_progressTimer) { clearInterval(_progressTimer); _progressTimer = null; }
136
+ if (_recoveryWatcher) { clearInterval(_recoveryWatcher); _recoveryWatcher = null; }
137
+ stopMonitor();
138
+ }
139
+
140
+ // Track pending feature from AI response so text confirmations ("ok", "yes") work
141
+ let _pendingFeature = null;
142
+
143
+ async function handleCEOMessage(message, ctx) {
144
+ if (!_running) return;
145
+
146
+ const lower = message.toLowerCase().trim();
147
+
148
+ // ── /assign command ──────────────────────────────────────────────────────
149
+ if (lower.startsWith('/assign') || lower.startsWith('assign ')) {
150
+ const parts = message.replace(/^\/?(assign)\s*/i, '').trim().split(/\s+/);
151
+ const url = parts[0];
152
+ const goal = parts.slice(1).join(' ');
153
+
154
+ if (!url || !goal) {
155
+ postMessage('pm', 'Usage: /assign <url> <project goal>\nExample: /assign https://myapp.com Build a complete e-commerce store with payment');
156
+ return;
157
+ }
158
+
159
+ postMessage('pm', `Assigning project: ${url}\nGoal: ${goal}\nStarting setup...`);
160
+
161
+ try {
162
+ const { assignProjectAction } = await import('../actions/assign-project.js');
163
+ await assignProjectAction({ url, goal });
164
+ } catch (err) {
165
+ postMessage('pm', `Assignment failed: ${err.message.slice(0, 150)}`);
166
+ }
167
+ return;
168
+ }
169
+
170
+ // ── /suggest command ──────────────────────────────────────────────────────
171
+ if (lower === '/suggest' || lower === 'suggest') {
172
+ postMessage('pm', 'Running suggestion analysis...');
173
+ try {
174
+ const { runSuggestionAgent } = await import('../agents/suggestion-agent.js');
175
+ const result = await runSuggestionAgent();
176
+ if (result.success && result.suggestions?.length > 0) {
177
+ const titles = result.suggestions.map((s, i) => `${i + 1}. "${s.title}"`).join('\n');
178
+ postMessage('pm', `Feature suggestions:\n${titles}`);
179
+ } else if (result.skipped) {
180
+ postMessage('pm', 'Suggestion agent is disabled. Enable in aicc.config.js: suggestion.enabled = true');
181
+ } else {
182
+ postMessage('pm', 'No suggestions generated.');
183
+ }
184
+ } catch (err) {
185
+ postMessage('pm', `Suggestion failed: ${err.message.slice(0, 100)}`);
186
+ }
187
+ return;
188
+ }
189
+
190
+ // Only intercept explicit slash commands — everything else goes to AI
191
+ if (lower.startsWith('/feature') || lower.startsWith('feature ')) {
192
+ const desc = message.replace(/^\/?feature\s*/i, '').trim();
193
+ postMessage('pm', generatePMMessage('feature_received', { feature: desc }));
194
+ setTimeout(() => {
195
+ postMessage('pm', generatePMMessage('writing_spec'));
196
+ }, 500);
197
+ resetPMState(desc.slice(0, 40));
198
+ return;
199
+ }
200
+
201
+ // ── Text confirmation for pending feature ──────────────────────────────────
202
+ // When PM showed create_feature buttons but user replied with text instead of clicking
203
+ const confirmPatterns = /^(ok|yes|yep|yea|yeah|sure|go|go ahead|do it|let's go|lgtm|confirm|làm đi|tạo đi|được|ừ|ờ|oke|okie|vâng|đồng ý|bắt đầu|start|create|duyệt)$/i;
204
+ if (_pendingFeature && confirmPatterns.test(lower)) {
205
+ const { description, type } = _pendingFeature;
206
+ _pendingFeature = null;
207
+ postMessage('pm', `Starting ${type === 'bug' ? 'bug fix' : 'feature'} pipeline (auto mode)...`);
208
+ resetPMState(description.slice(0, 40));
209
+ try {
210
+ const actions = await import('../shared/action-runner.js');
211
+ const result = await actions.runNewFeature(description, 'auto', type);
212
+ if (!result.success) {
213
+ postMessage('pm', `Failed to start pipeline: ${result.error}`);
214
+ }
215
+ } catch (err) {
216
+ postMessage('pm', `Pipeline error: ${err.message?.slice(0, 100)}`);
217
+ }
218
+ return;
219
+ }
220
+
221
+ // All other messages → AI with conversation history for context
222
+ try {
223
+ const askAI = await getAskAI();
224
+ // Build conversation context from room history so AI remembers previous messages
225
+ const history = getRoomHistory(20);
226
+ const contextLines = history
227
+ .filter(h => h.type === 'text' || h.type === 'question')
228
+ .map(h => `[${h.role === 'ceo' ? 'User' : h.role.toUpperCase()}]: ${h.message}`)
229
+ .join('\n');
230
+ const conversationContext = contextLines
231
+ ? `## Recent Conversation\n${contextLines}`
232
+ : '';
233
+
234
+ const result = await askAI(message, conversationContext);
235
+ // askAI returns a string on success, or { error: "..." } on failure
236
+ if (typeof result === 'string' && result.trim()) {
237
+ // Parse action JSON from AI response (same pattern as telegram/commands.js)
238
+ const stripped = result.replace(/```(?:json)?\s*/gi, '').replace(/```/g, '');
239
+ const actionMatch = stripped.match(/\{"_?action"\s*:\s*"[^"]+".+?\}(?=\s|$)/s);
240
+ if (actionMatch) {
241
+ let action;
242
+ try {
243
+ action = JSON.parse(actionMatch[0]);
244
+ } catch {
245
+ // Try balanced-brace extraction if simple regex match isn't valid JSON
246
+ const start = stripped.indexOf('{"_action"') >= 0 ? stripped.indexOf('{"_action"') : stripped.indexOf('{"action"');
247
+ if (start >= 0) {
248
+ let depth = 0, end = start;
249
+ for (let i = start; i < stripped.length; i++) {
250
+ if (stripped[i] === '{') depth++;
251
+ else if (stripped[i] === '}') { depth--; if (depth === 0) { end = i + 1; break; } }
252
+ }
253
+ try { action = JSON.parse(stripped.slice(start, end)); } catch { /* give up */ }
254
+ }
255
+ }
256
+ if (action) {
257
+ const explanation = stripped.slice(0, actionMatch.index).trim();
258
+ const actionName = action._action || action.action;
259
+
260
+ if (actionName === 'create_feature' || actionName === 'createfeature') {
261
+ const desc = (action.description || message).trim();
262
+ const type = action.type === 'bug' ? 'bug' : 'feature';
263
+ const label = type === 'bug' ? 'Bug Fix' : 'Feature';
264
+ const encodedDesc = encodeURIComponent(desc.slice(0, 50));
265
+ // Save pending feature so text confirmation ("ok", "yes") works
266
+ _pendingFeature = { description: desc, type };
267
+ if (explanation) postMessage('pm', explanation);
268
+ // Show confirmation with inline buttons (same callback format as keyboards.js)
269
+ postMessage('pm', `${label}: "${desc.slice(0, 120)}"\n\nWant me to set this up?`, {
270
+ buttons: [
271
+ { text: type === 'bug' ? '🐛 Yes, create bug fix' : '✨ Yes, create feature', callback: `confirm:${type}:yes:${encodedDesc}` },
272
+ { text: '✏️ Let me refine it', callback: `confirm:${type}:edit:${encodedDesc}` },
273
+ { text: '💬 Just discussing', callback: 'confirm:no' },
274
+ ],
275
+ });
276
+ return;
277
+ }
278
+
279
+ // Pipeline execution actions
280
+ if (_bus && actionName) {
281
+ if (explanation) postMessage('pm', explanation);
282
+ _bus.emitEvent('room:pipeline_action', { action: actionName, data: action });
283
+ return;
284
+ }
285
+ }
286
+ }
287
+
288
+ // Clean response: strip any action JSON blobs before displaying
289
+ const cleanResult = result
290
+ .replace(/```(?:json)?\s*\{[^}]*"_?action"[^}]*\}\s*```/g, '')
291
+ .replace(/\{"_?action"\s*:[^}]+\}/g, '')
292
+ .replace(/\n{3,}/g, '\n\n')
293
+ .trim();
294
+ if (cleanResult) {
295
+ postMessage('pm', cleanResult);
296
+ } else {
297
+ postMessage('pm', `Noted. I'll keep you posted on progress.`);
298
+ }
299
+ } else if (result && typeof result === 'object' && result.error) {
300
+ postMessage('pm', `Having trouble reaching AI providers right now. ${result.error.slice(0, 100)}`);
301
+ } else {
302
+ postMessage('pm', `Noted. I'll keep you posted on progress.`);
303
+ }
304
+ } catch (err) {
305
+ postMessage('pm', `AI is temporarily unavailable. I'll keep you posted on progress.`);
306
+ }
307
+ }
308
+
309
+ function handlePipelineEvent(event, data) {
310
+ if (!_running) return;
311
+
312
+ switch (event) {
313
+ case 'stage_start':
314
+ pmState.currentStage = data.stage;
315
+ pmState.stageStartTimes[data.stage] = Date.now();
316
+ // Don't duplicate — action-runner.js already sends the stage_start notification
317
+ break;
318
+
319
+ case 'stage_complete':
320
+ pmState.consecutiveErrors = 0;
321
+ // Don't duplicate stage completion messages — action-runner.js notify() already
322
+ // sends detailed messages for every stage. PM agent only adds value for errors
323
+ // and provider switching, which are handled in 'stage_error' below.
324
+ if (data.stage === 'review' || data.stage === 'review_complete') {
325
+ const hasIssues = data.verdict === 'REJECTED';
326
+ if (hasIssues) {
327
+ pmState.reviewCycles++;
328
+ if (pmState.reviewCycles >= pmState.MAX_REVIEW_CYCLES) {
329
+ postMessage('pm', generatePMMessage('review_loop_broken'));
330
+ }
331
+ // Don't send review_found_issues — action-runner already sends detailed review
332
+ }
333
+ // Don't send review_approved or deploy_ask — action-runner sends approve/deploy flow
334
+ }
335
+ break;
336
+
337
+ case 'stage_error':
338
+ handleStageError(data.stage, data.error, data);
339
+ break;
340
+
341
+ case 'pipeline_stalled':
342
+ handleStall(data.stage, data.minutesElapsed * 60_000);
343
+ break;
344
+
345
+ case 'pipeline_stall_recovery':
346
+ postMessage('pm', `Stage ${data.stage || ''} was stuck. Restarting...`);
347
+ break;
348
+
349
+ case 'feature_created':
350
+ resetPMState(data.feature || data.description);
351
+ break;
352
+
353
+ case 'feature_reset':
354
+ case 'feature_abandoned':
355
+ // Clear PM state to stop stale heartbeat/progress messages after /reset
356
+ resetPMState(null);
357
+ break;
358
+
359
+ case 'health_status':
360
+ checkProviderRecovery(data.providers);
361
+ break;
362
+
363
+ case 'qa_bugfix_exhausted': {
364
+ try {
365
+ const threadId = openThread(
366
+ 'bug_found',
367
+ `QA bugs unresolved after ${data.cycles} fix attempts`,
368
+ data,
369
+ 'pm'
370
+ );
371
+ postToThread(threadId, 'pm', `Tried ${data.cycles} times to fix ${data.remainingFails} failing page(s). Need your input.`);
372
+ escalateThread(threadId, 'Automatic bug fix loop exhausted');
373
+ } catch { /* threads module not available */ }
374
+ break;
375
+ }
376
+ }
377
+ }
378
+
379
+ function handleStageError(stage, error, data = {}) {
380
+ pmState.totalErrors++;
381
+ pmState.consecutiveErrors++;
382
+ pmState.retryCount[stage] = (pmState.retryCount[stage] || 0) + 1;
383
+
384
+ if (pmState.consecutiveErrors >= pmState.MAX_CONSECUTIVE_ERRORS) {
385
+ postMessage('pm', generatePMMessage('pipeline_paused'), { type: 'error' });
386
+ startRecoveryWatcher();
387
+ return;
388
+ }
389
+
390
+ const action = suggestRecoveryAction(stage, error);
391
+ const available = getAvailableProviders(stage);
392
+
393
+ if (action === 'switch_provider' && available.length > 0) {
394
+ const to = available[0];
395
+ postMessage('pm', generatePMMessage('switching_provider', {
396
+ from: data.model_used || 'current provider',
397
+ to,
398
+ }));
399
+ if (_bus) {
400
+ _bus.emitEvent('room:agent_switch', { stage, from: data.model_used, to });
401
+ }
402
+ } else if (action === 'wait_and_retry') {
403
+ postMessage('pm', generatePMMessage('retrying', { attempt: pmState.retryCount[stage] }));
404
+ } else if (action === 'escalate_ceo') {
405
+ postMessage('pm', generatePMMessage('all_providers_down'), { type: 'error' });
406
+ startRecoveryWatcher();
407
+ } else {
408
+ postMessage('pm', generatePMMessage('error_handled', {
409
+ summary: String(error || 'unknown error').slice(0, 80),
410
+ action: action.replace(/_/g, ' '),
411
+ }));
412
+ }
413
+ }
414
+
415
+ function handleStall(stage, durationMs) {
416
+ pmState.stallRecoveries++;
417
+ const health = getStageHealth(stage, pmState.stageStartTimes[stage]);
418
+
419
+ if (health.status === 'slow') {
420
+ // Only post if we haven't posted recently
421
+ const now = Date.now();
422
+ if (now - pmState.lastProgressUpdate > 180_000) {
423
+ pmState.lastProgressUpdate = now;
424
+ postMessage('pm', generatePMMessage('still_working', { stage }));
425
+ }
426
+ } else if (health.status === 'stalled') {
427
+ postMessage('pm', `Stage ${stage} appears stuck. Auto-recovering...`, { type: 'status' });
428
+ }
429
+ }
430
+
431
+ function checkProgress() {
432
+ if (!_running) return;
433
+
434
+ // ── Active stage progress ──────────────────────────────────────────────────
435
+ if (pmState.currentStage) {
436
+ const stageStart = pmState.stageStartTimes[pmState.currentStage];
437
+ if (stageStart) {
438
+ const health = getStageHealth(pmState.currentStage, stageStart);
439
+ const now = Date.now();
440
+
441
+ if (health.status === 'slow' && now - pmState.lastProgressUpdate > (getRoleplayConfig().progressInterval || 180_000)) {
442
+ pmState.lastProgressUpdate = now;
443
+ postMessage('pm', generatePMMessage('still_working', { role: pmState.currentStage, stage: pmState.currentStage }));
444
+ }
445
+ }
446
+ return;
447
+ }
448
+
449
+ // ── Idle — run suggestion agent if enabled ────────────────────────────────
450
+ const config = getRoleplayConfig();
451
+ const idleMs = (config.suggestion?.idleAfterMinutes || 60) * 60 * 1000;
452
+ const now = Date.now();
453
+
454
+ if (
455
+ config.suggestion?.enabled !== false &&
456
+ (now - pmState.lastSuggestionTime) > idleMs
457
+ ) {
458
+ pmState.lastSuggestionTime = now;
459
+ import('../agents/suggestion-agent.js')
460
+ .then(mod => mod.runSuggestionAgent())
461
+ .then(result => {
462
+ if (result.success && result.suggestions?.length > 0) {
463
+ const titles = result.suggestions.map(s => `"${s.title}"`).join(', ');
464
+ postMessage('pm', `Idle analysis complete. Suggestions: ${titles}. Type /suggest to review.`);
465
+ }
466
+ })
467
+ .catch(() => {});
468
+ }
469
+ }
470
+
471
+ function checkProviderRecovery(providers) {
472
+ if (!_recoveryWatcher) return; // only relevant when in recovery mode
473
+ const anyHealthy = Object.values(providers).some(s => s === 'healthy' || s === 'degraded');
474
+ if (anyHealthy) {
475
+ clearInterval(_recoveryWatcher);
476
+ _recoveryWatcher = null;
477
+ pmState.consecutiveErrors = 0;
478
+ postMessage('pm', generatePMMessage('provider_recovered', {
479
+ provider: Object.entries(providers).find(([,s]) => s !== 'down')?.[0] || 'provider',
480
+ }));
481
+ }
482
+ }
483
+
484
+ function startRecoveryWatcher() {
485
+ if (_recoveryWatcher) return;
486
+ let retries = 0;
487
+ const maxRetries = Math.ceil(pmState.ALL_DOWN_MAX_WAIT / pmState.ALL_DOWN_RETRY_INTERVAL);
488
+
489
+ _recoveryWatcher = setInterval(() => {
490
+ retries++;
491
+ const providers = getProviderStatus();
492
+ const anyHealthy = Object.values(providers).some(s => s !== 'down');
493
+
494
+ if (anyHealthy) {
495
+ clearInterval(_recoveryWatcher);
496
+ _recoveryWatcher = null;
497
+ pmState.consecutiveErrors = 0;
498
+ postMessage('pm', generatePMMessage('provider_recovered', {
499
+ provider: Object.entries(providers).find(([,s]) => s !== 'down')?.[0] || 'provider',
500
+ }));
501
+ } else if (retries >= maxRetries) {
502
+ clearInterval(_recoveryWatcher);
503
+ _recoveryWatcher = null;
504
+ // After 30 min, just stay paused — no CEO escalation for provider failures
505
+ postMessage('pm', `Pipeline paused for 30+ minutes due to provider outage. Will auto-resume when providers recover.`);
506
+ } else {
507
+ postMessage('pm', `Still down. Retry ${retries}/${maxRetries}.`);
508
+ }
509
+ }, pmState.ALL_DOWN_RETRY_INTERVAL);
510
+ }
511
+
512
+ // Expose for testing
513
+ export { handleCEOMessage, handlePipelineEvent, handleStageError, pmState, resetPMState };
@@ -0,0 +1,25 @@
1
+ import { getConfig } from '../config.js';
2
+
3
+ export const ROLEPLAY_DEFAULTS = {
4
+ enabled: true,
5
+ verbosity: 'normal',
6
+ pmPersonality: 'professional',
7
+ agentChat: true,
8
+ progressUpdates: true,
9
+ progressInterval: 60000,
10
+ maxReviewCycles: 3,
11
+ autoDeployOnApproval: false,
12
+ };
13
+
14
+ export function getRoleplayConfig() {
15
+ try {
16
+ const config = getConfig();
17
+ return { ...ROLEPLAY_DEFAULTS, ...(config.roleplay || {}) };
18
+ } catch {
19
+ return { ...ROLEPLAY_DEFAULTS };
20
+ }
21
+ }
22
+
23
+ export function isRoleplayEnabled() {
24
+ return getRoleplayConfig().enabled === true;
25
+ }