ai-control-center 1.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +584 -0
- package/bin/aicc.js +772 -0
- package/lib/actions/approve.js +71 -0
- package/lib/actions/assign-project.js +132 -0
- package/lib/actions/browser-test.js +64 -0
- package/lib/actions/cleanup.js +174 -0
- package/lib/actions/debug.js +298 -0
- package/lib/actions/deploy.js +1229 -0
- package/lib/actions/fix-bug.js +134 -0
- package/lib/actions/new-feature.js +255 -0
- package/lib/actions/reject.js +307 -0
- package/lib/actions/review.js +706 -0
- package/lib/actions/status.js +47 -0
- package/lib/agents/browser-qa-agent.js +611 -0
- package/lib/agents/payment-agent.js +116 -0
- package/lib/agents/suggestion-agent.js +88 -0
- package/lib/cli.js +303 -0
- package/lib/config.js +243 -0
- package/lib/hub/hub-server.js +440 -0
- package/lib/hub/project-poller.js +75 -0
- package/lib/hub/skill-registry.js +89 -0
- package/lib/hub/state-aggregator.js +204 -0
- package/lib/index.js +471 -0
- package/lib/init/doctor.js +523 -0
- package/lib/init/presets.js +222 -0
- package/lib/init/skill-fetcher.js +77 -0
- package/lib/init/wizard.js +973 -0
- package/lib/integrations/codex-runner.js +128 -0
- package/lib/integrations/github-actions.js +248 -0
- package/lib/integrations/github-reporter.js +229 -0
- package/lib/integrations/screenshot-store.js +102 -0
- package/lib/openclaw/bridge.js +650 -0
- package/lib/openclaw/generate-skill.js +235 -0
- package/lib/openclaw/openclaw.json +64 -0
- package/lib/orchestrator/autonomous-loop.js +429 -0
- package/lib/orchestrator/thread-triggers.js +63 -0
- package/lib/roleplay/agent-messenger.js +75 -0
- package/lib/roleplay/discussion-threads.js +303 -0
- package/lib/roleplay/health-monitor.js +121 -0
- package/lib/roleplay/pm-agent.js +513 -0
- package/lib/roleplay/roleplay-config.js +25 -0
- package/lib/roleplay/room.js +164 -0
- package/lib/shared/action-runner.js +2330 -0
- package/lib/shared/event-bus.js +185 -0
- package/lib/slack/bot.js +378 -0
- package/lib/telegram/bot.js +416 -0
- package/lib/telegram/commands.js +1267 -0
- package/lib/telegram/keyboards.js +113 -0
- package/lib/telegram/notifications.js +247 -0
- package/lib/twitch/bot.js +354 -0
- package/lib/twitch/commands.js +302 -0
- package/lib/twitch/notifications.js +63 -0
- package/lib/utils/achievements.js +191 -0
- package/lib/utils/activity-log.js +182 -0
- package/lib/utils/agent-leaderboard.js +119 -0
- package/lib/utils/audit-logger.js +232 -0
- package/lib/utils/codebase-context.js +288 -0
- package/lib/utils/codebase-indexer.js +381 -0
- package/lib/utils/config-schema.js +230 -0
- package/lib/utils/context-compressor.js +172 -0
- package/lib/utils/correlation.js +63 -0
- package/lib/utils/cost-tracker.js +423 -0
- package/lib/utils/cron-scheduler.js +53 -0
- package/lib/utils/db-adapter.js +293 -0
- package/lib/utils/display.js +272 -0
- package/lib/utils/errors.js +116 -0
- package/lib/utils/format.js +134 -0
- package/lib/utils/intent-engine.js +464 -0
- package/lib/utils/mcp-client.js +238 -0
- package/lib/utils/model-ab-test.js +164 -0
- package/lib/utils/notify.js +122 -0
- package/lib/utils/persona-loader.js +80 -0
- package/lib/utils/pipeline-lock.js +73 -0
- package/lib/utils/pipeline.js +214 -0
- package/lib/utils/plugin-runner.js +234 -0
- package/lib/utils/rate-limiter.js +84 -0
- package/lib/utils/rbac.js +74 -0
- package/lib/utils/runner.js +1809 -0
- package/lib/utils/security.js +191 -0
- package/lib/utils/self-healer.js +144 -0
- package/lib/utils/skill-loader.js +255 -0
- package/lib/utils/spinner.js +132 -0
- package/lib/utils/stage-queue.js +50 -0
- package/lib/utils/state-machine.js +89 -0
- package/lib/utils/status-bar.js +327 -0
- package/lib/utils/token-estimator.js +101 -0
- package/lib/utils/ux-analyzer.js +101 -0
- package/lib/utils/webhook-emitter.js +83 -0
- package/lib/web/public/css/styles.css +417 -0
- package/lib/web/public/dark-mode.js +44 -0
- package/lib/web/public/hub/kanban.html +206 -0
- package/lib/web/public/index.html +45 -0
- package/lib/web/public/js/app.js +71 -0
- package/lib/web/public/js/ask.js +110 -0
- package/lib/web/public/js/dashboard.js +165 -0
- package/lib/web/public/js/deploy.js +72 -0
- package/lib/web/public/js/feature.js +79 -0
- package/lib/web/public/js/health.js +65 -0
- package/lib/web/public/js/logs.js +93 -0
- package/lib/web/public/js/review.js +123 -0
- package/lib/web/public/js/ws-client.js +82 -0
- package/lib/web/public/office/css/office.css +678 -0
- package/lib/web/public/office/index.html +148 -0
- package/lib/web/public/office/js/achievements-ui.js +117 -0
- package/lib/web/public/office/js/character.js +1056 -0
- package/lib/web/public/office/js/chat-bubbles.js +177 -0
- package/lib/web/public/office/js/cost-overlay.js +123 -0
- package/lib/web/public/office/js/day-night.js +68 -0
- package/lib/web/public/office/js/effects.js +632 -0
- package/lib/web/public/office/js/engine.js +146 -0
- package/lib/web/public/office/js/feature-ticket.js +216 -0
- package/lib/web/public/office/js/hub-client.js +60 -0
- package/lib/web/public/office/js/main.js +1757 -0
- package/lib/web/public/office/js/office-layout.js +1524 -0
- package/lib/web/public/office/js/pathfinding.js +144 -0
- package/lib/web/public/office/js/pixel-sprites.js +1454 -0
- package/lib/web/public/office/js/progress-bars.js +117 -0
- package/lib/web/public/office/js/replay.js +191 -0
- package/lib/web/public/office/js/sound-effects.js +91 -0
- package/lib/web/public/office/js/sprite-renderer.js +211 -0
- package/lib/web/public/office/js/stamina-system.js +89 -0
- package/lib/web/public/office/js/ui.js +107 -0
- package/lib/web/public/onboarding/index.html +243 -0
- package/lib/web/public/timeline/index.html +195 -0
- package/lib/web/routes/api.js +499 -0
- package/lib/web/routes/logs.js +20 -0
- package/lib/web/routes/metrics.js +99 -0
- package/lib/web/server.js +183 -0
- package/lib/web/ws/handler.js +65 -0
- package/package.json +67 -0
- package/templates/agent-architect.md +69 -0
- package/templates/agent-gemini-pm.md +49 -0
- package/templates/agent-gemini-reviewer.md +52 -0
- package/templates/copilot-instructions.md +36 -0
- package/templates/pipelines/mobile.json +27 -0
- package/templates/pipelines/nodejs-api.json +27 -0
- package/templates/pipelines/python.json +27 -0
- package/templates/pipelines/react.json +27 -0
- package/templates/pipelines/salesforce.json +27 -0
- package/templates/role-gemini.md +97 -0
- package/templates/skill-architect.md +114 -0
- package/templates/skill-browser-qa.md +50 -0
- package/templates/skill-bug-from-qa.md +58 -0
- package/templates/skill-chatbot.md +93 -0
- package/templates/skill-implement.md +78 -0
- package/templates/skill-openclaw.md +174 -0
- package/templates/skill-payment.md +110 -0
- package/templates/skill-pm-spec.md +77 -0
- package/templates/skill-requirement-capture.md +97 -0
- package/templates/skill-review.md +108 -0
- package/templates/skill-reviewer-qa.md +44 -0
- package/templates/skill-suggestion.md +45 -0
- package/templates/skill-template.md +142 -0
|
@@ -0,0 +1,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
|
+
}
|