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,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autonomous Loop — The main orchestration brain.
|
|
3
|
+
*
|
|
4
|
+
* Runs on a configurable interval (default: 30s).
|
|
5
|
+
* Reads status.json → decides what to do next → executes.
|
|
6
|
+
*
|
|
7
|
+
* This is the ONLY place that initiates pipeline actions.
|
|
8
|
+
* All other code (Telegram, web API, CLI) ONLY writes to status.json
|
|
9
|
+
* or emits events — the loop picks them up.
|
|
10
|
+
*
|
|
11
|
+
* Architecture:
|
|
12
|
+
* status.json (state) + event-bus (signals) → loop reads → decides → acts
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { getStatus, updateStatus } from '../utils/pipeline.js';
|
|
16
|
+
import { logActivity } from '../utils/activity-log.js';
|
|
17
|
+
import { getConfig } from '../config.js';
|
|
18
|
+
import { bus } from '../shared/event-bus.js';
|
|
19
|
+
import { validateTransition } from '../utils/state-machine.js';
|
|
20
|
+
import { acquireLock, releaseLock } from '../utils/pipeline-lock.js';
|
|
21
|
+
import { checkCostGuard } from '../utils/security.js';
|
|
22
|
+
|
|
23
|
+
let loopInterval = null;
|
|
24
|
+
let isRunning = false;
|
|
25
|
+
|
|
26
|
+
// ─── Decision Table ──────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const DECISIONS = {
|
|
29
|
+
/**
|
|
30
|
+
* IDLE: Check if we should run suggestions or if a new assignment came in.
|
|
31
|
+
*/
|
|
32
|
+
async idle(status, config) {
|
|
33
|
+
// Check for pending assignment (written by /assign command)
|
|
34
|
+
const pendingAssign = status.pendingAssignment;
|
|
35
|
+
if (pendingAssign) {
|
|
36
|
+
return { action: 'assign', data: pendingAssign };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check if idle long enough for suggestions
|
|
40
|
+
const idleMs = (config.suggestion?.idleAfterMinutes || 60) * 60 * 1000;
|
|
41
|
+
const idleSince = new Date(status.transitionedAt || 0).getTime();
|
|
42
|
+
if (Date.now() - idleSince > idleMs && config.suggestion?.enabled) {
|
|
43
|
+
return { action: 'suggestion' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check for scheduled QA (cron-triggered, flag set in status.json)
|
|
47
|
+
if (status.scheduledQA) {
|
|
48
|
+
return { action: 'scheduled-qa' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { action: 'wait' };
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* ASSIGN: Parse URL + goal → run initial Browser QA audit.
|
|
56
|
+
*/
|
|
57
|
+
async assign(status, config) {
|
|
58
|
+
const { assignProjectAction } = await import('../actions/assign-project.js');
|
|
59
|
+
const result = await assignProjectAction({
|
|
60
|
+
url: status.assignedUrl,
|
|
61
|
+
goal: status.assignedGoal,
|
|
62
|
+
config,
|
|
63
|
+
});
|
|
64
|
+
return { action: 'transition', nextState: 'browser-qa', data: result };
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* BROWSER-QA: Run QA → triage results.
|
|
69
|
+
*/
|
|
70
|
+
async 'browser-qa'(status, config) {
|
|
71
|
+
const { browserTestAction } = await import('../actions/browser-test.js');
|
|
72
|
+
const result = await browserTestAction({ featureId: status.featureId, config });
|
|
73
|
+
|
|
74
|
+
if (result.skipped || result.success) {
|
|
75
|
+
// No bugs → proceed to spec (new feature) or idle (maintenance)
|
|
76
|
+
const nextState = status.assignedGoal ? 'spec' : 'idle';
|
|
77
|
+
return { action: 'transition', nextState, data: result };
|
|
78
|
+
}
|
|
79
|
+
return { action: 'transition', nextState: 'triage', data: result };
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* TRIAGE: Analyze QA failures → decide bugfix or spec.
|
|
84
|
+
*/
|
|
85
|
+
async triage(status) {
|
|
86
|
+
const failCount = status.qa_fail_count || 0;
|
|
87
|
+
|
|
88
|
+
if (failCount > 0) {
|
|
89
|
+
return {
|
|
90
|
+
action: 'transition',
|
|
91
|
+
nextState: 'bugfix',
|
|
92
|
+
data: { currentCycle: 1, maxCycles: status.maxCycles || 3 },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// No fails (edge case: triage reached with 0 fails) → spec
|
|
96
|
+
return { action: 'transition', nextState: 'spec' };
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* BUGFIX: Run bugfix → re-QA to verify.
|
|
101
|
+
*/
|
|
102
|
+
async bugfix(status) {
|
|
103
|
+
const { fixBugAction } = await import('../actions/fix-bug.js');
|
|
104
|
+
const result = await fixBugAction({ featureId: status.featureId, fromQA: true });
|
|
105
|
+
|
|
106
|
+
return { action: 'transition', nextState: 're-qa', data: result };
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* RE-QA: Re-run QA after bugfix → pass or retry.
|
|
111
|
+
*/
|
|
112
|
+
async 're-qa'(status, config) {
|
|
113
|
+
const { browserTestAction } = await import('../actions/browser-test.js');
|
|
114
|
+
const result = await browserTestAction({ featureId: status.featureId, config });
|
|
115
|
+
|
|
116
|
+
if (result.success || result.skipped) {
|
|
117
|
+
// Bugs fixed → proceed to spec or idle
|
|
118
|
+
const nextState = status.assignedGoal ? 'spec' : 'idle';
|
|
119
|
+
return { action: 'transition', nextState, data: result };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Still failing → check cycle limit
|
|
123
|
+
const cycle = (status.currentCycle || 1) + 1;
|
|
124
|
+
const maxCycles = status.maxCycles || config.browserQA?.maxBugfixCycles || 3;
|
|
125
|
+
|
|
126
|
+
if (cycle > maxCycles) {
|
|
127
|
+
return {
|
|
128
|
+
action: 'transition',
|
|
129
|
+
nextState: 'escalate',
|
|
130
|
+
data: {
|
|
131
|
+
reason: `Bug-fix loop exhausted after ${maxCycles} cycles`,
|
|
132
|
+
remainingFails: result.failCount,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
action: 'transition',
|
|
139
|
+
nextState: 'bugfix',
|
|
140
|
+
data: { currentCycle: cycle, maxCycles },
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* SPEC: Run PM spec stage.
|
|
146
|
+
*/
|
|
147
|
+
async spec(status) {
|
|
148
|
+
const { runNewFeature } = await import('../shared/action-runner.js');
|
|
149
|
+
const result = await runNewFeature(
|
|
150
|
+
status.assignedGoal || status.featureDescription || 'Feature',
|
|
151
|
+
'auto',
|
|
152
|
+
'feature',
|
|
153
|
+
);
|
|
154
|
+
return { action: 'transition', nextState: 'arch', data: result };
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* ARCH: Run architecture stage.
|
|
159
|
+
*/
|
|
160
|
+
async arch(status) {
|
|
161
|
+
const { runNewFeature } = await import('../shared/action-runner.js');
|
|
162
|
+
const result = await runNewFeature(
|
|
163
|
+
status.assignedGoal || 'Architecture',
|
|
164
|
+
'auto',
|
|
165
|
+
'feature',
|
|
166
|
+
);
|
|
167
|
+
return { action: 'transition', nextState: 'impl', data: result };
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* IMPL: Run implementation stage.
|
|
172
|
+
*/
|
|
173
|
+
async impl(status) {
|
|
174
|
+
const { runImplementation } = await import('../shared/action-runner.js');
|
|
175
|
+
const result = await runImplementation();
|
|
176
|
+
return { action: 'transition', nextState: 'review', data: result };
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* REVIEW: Run review stage → deploy or re-impl.
|
|
181
|
+
*/
|
|
182
|
+
async review(status) {
|
|
183
|
+
const { runReview } = await import('../shared/action-runner.js');
|
|
184
|
+
const result = await runReview();
|
|
185
|
+
|
|
186
|
+
if (result.verdict === 'APPROVED' || result.approved) {
|
|
187
|
+
return { action: 'transition', nextState: 'deploy', data: result };
|
|
188
|
+
}
|
|
189
|
+
// Rejected → re-implement
|
|
190
|
+
return { action: 'transition', nextState: 'impl', data: { reviewFeedback: result.feedback } };
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* DEPLOY: Run deploy → post-deploy QA.
|
|
195
|
+
*/
|
|
196
|
+
async deploy(status, config) {
|
|
197
|
+
// Check if auto-deploy is allowed or CEO approval needed
|
|
198
|
+
if (!config.roleplay?.autoDeployOnApproval) {
|
|
199
|
+
bus.emit('pipeline-event', {
|
|
200
|
+
event: 'deploy_approval_needed',
|
|
201
|
+
data: { featureId: status.featureId },
|
|
202
|
+
});
|
|
203
|
+
return { action: 'transition', nextState: 'manual_hold', data: { reason: 'Deploy requires CEO approval' } };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const { runDeploy } = await import('../shared/action-runner.js');
|
|
207
|
+
const result = await runDeploy();
|
|
208
|
+
|
|
209
|
+
return { action: 'transition', nextState: 'deploy-verify', data: result };
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* DEPLOY-VERIFY: Post-deploy QA on production URL.
|
|
214
|
+
*/
|
|
215
|
+
async 'deploy-verify'(status, config) {
|
|
216
|
+
const { browserTestAction } = await import('../actions/browser-test.js');
|
|
217
|
+
const result = await browserTestAction({
|
|
218
|
+
featureId: status.featureId,
|
|
219
|
+
config,
|
|
220
|
+
targetUrl: config.browserQA?.productionUrl || config.browserQA?.targetUrl,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (result.success || result.skipped) {
|
|
224
|
+
logActivity('DEPLOY', 'Post-deploy QA PASSED — feature complete', 'success');
|
|
225
|
+
bus.emit('pipeline-event', {
|
|
226
|
+
event: 'feature_complete',
|
|
227
|
+
data: { featureId: status.featureId, passRate: result.report?.summary?.passRate },
|
|
228
|
+
});
|
|
229
|
+
return { action: 'transition', nextState: 'idle' };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Post-deploy failure — bugfix
|
|
233
|
+
return { action: 'transition', nextState: 'bugfix', data: { postDeploy: true } };
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* ESCALATE: Notify CEO and wait.
|
|
238
|
+
*/
|
|
239
|
+
async escalate(status) {
|
|
240
|
+
bus.emit('pipeline-event', {
|
|
241
|
+
event: 'escalation',
|
|
242
|
+
data: {
|
|
243
|
+
reason: status.escalation?.reason || 'Unknown escalation',
|
|
244
|
+
featureId: status.featureId,
|
|
245
|
+
failedStage: status.previousStage,
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
logActivity('ESCALATE', `Escalated to CEO: ${status.escalation?.reason || 'Unknown'}`, 'error');
|
|
249
|
+
return { action: 'transition', nextState: 'manual_hold' };
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* MANUAL_HOLD: Wait for CEO input. Do nothing.
|
|
254
|
+
*/
|
|
255
|
+
async manual_hold() {
|
|
256
|
+
return { action: 'wait' };
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* SUGGESTION: Run suggestion agent → back to idle.
|
|
261
|
+
*/
|
|
262
|
+
async suggestion() {
|
|
263
|
+
const { runSuggestionAgent } = await import('../agents/suggestion-agent.js');
|
|
264
|
+
const result = await runSuggestionAgent();
|
|
265
|
+
return { action: 'transition', nextState: 'idle', data: result };
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// ─── Main Loop ────────────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
async function tick() {
|
|
272
|
+
if (isRunning) {
|
|
273
|
+
logActivity('LOOP', 'Tick skipped — previous tick still running', 'warn');
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const lockAcquired = acquireLock('pipeline-loop', 120_000);
|
|
278
|
+
if (!lockAcquired) {
|
|
279
|
+
logActivity('LOOP', 'Could not acquire pipeline lock — another process holds it', 'warn');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
isRunning = true;
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const status = getStatus();
|
|
287
|
+
const config = getConfig();
|
|
288
|
+
const currentStage = status.stage || 'idle';
|
|
289
|
+
|
|
290
|
+
// Cost guard check before non-idle stages
|
|
291
|
+
if (currentStage !== 'idle' && currentStage !== 'manual_hold') {
|
|
292
|
+
const costCheck = checkCostGuard(config);
|
|
293
|
+
if (costCheck.exceeded) {
|
|
294
|
+
logActivity('LOOP', costCheck.message, 'error');
|
|
295
|
+
bus.emit('pipeline-event', { event: 'cost_exceeded', data: costCheck });
|
|
296
|
+
updateStatus({
|
|
297
|
+
stage: 'escalate',
|
|
298
|
+
previousStage: currentStage,
|
|
299
|
+
escalation: { reason: costCheck.message, failedStage: currentStage, timestamp: new Date().toISOString() },
|
|
300
|
+
transitionedAt: new Date().toISOString(),
|
|
301
|
+
});
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const handler = DECISIONS[currentStage];
|
|
307
|
+
if (!handler) {
|
|
308
|
+
logActivity('LOOP', `No handler for stage "${currentStage}" — treating as idle`, 'warn');
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const decision = await handler(status, config);
|
|
313
|
+
|
|
314
|
+
switch (decision.action) {
|
|
315
|
+
case 'transition': {
|
|
316
|
+
validateTransition(currentStage, decision.nextState);
|
|
317
|
+
updateStatus({
|
|
318
|
+
stage: decision.nextState,
|
|
319
|
+
previousStage: currentStage,
|
|
320
|
+
transitionedAt: new Date().toISOString(),
|
|
321
|
+
...(decision.data || {}),
|
|
322
|
+
});
|
|
323
|
+
logActivity('LOOP', `Transition: ${currentStage} → ${decision.nextState}`, 'info');
|
|
324
|
+
bus.emit('stage_changed', { from: currentStage, to: decision.nextState, data: decision.data });
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
case 'assign': {
|
|
328
|
+
// Triggered from idle with a pending assignment
|
|
329
|
+
validateTransition('idle', 'assign');
|
|
330
|
+
updateStatus({
|
|
331
|
+
stage: 'assign',
|
|
332
|
+
previousStage: 'idle',
|
|
333
|
+
transitionedAt: new Date().toISOString(),
|
|
334
|
+
assignedUrl: decision.data?.url,
|
|
335
|
+
assignedGoal: decision.data?.goal,
|
|
336
|
+
pendingAssignment: null,
|
|
337
|
+
});
|
|
338
|
+
logActivity('LOOP', `Assignment picked up: ${decision.data?.url}`, 'info');
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
case 'suggestion': {
|
|
342
|
+
validateTransition('idle', 'suggestion');
|
|
343
|
+
updateStatus({
|
|
344
|
+
stage: 'suggestion',
|
|
345
|
+
previousStage: 'idle',
|
|
346
|
+
transitionedAt: new Date().toISOString(),
|
|
347
|
+
});
|
|
348
|
+
logActivity('LOOP', 'Running suggestion agent (idle timeout)', 'info');
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
case 'scheduled-qa': {
|
|
352
|
+
validateTransition('idle', 'browser-qa');
|
|
353
|
+
updateStatus({
|
|
354
|
+
stage: 'browser-qa',
|
|
355
|
+
previousStage: 'idle',
|
|
356
|
+
transitionedAt: new Date().toISOString(),
|
|
357
|
+
scheduledQA: false,
|
|
358
|
+
});
|
|
359
|
+
logActivity('LOOP', 'Scheduled QA triggered', 'info');
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
case 'wait':
|
|
363
|
+
// Do nothing, wait for next tick or external event
|
|
364
|
+
break;
|
|
365
|
+
default:
|
|
366
|
+
logActivity('LOOP', `Unknown decision action: ${decision.action}`, 'warn');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
} catch (err) {
|
|
370
|
+
logActivity('LOOP', `Tick error: ${err.message}`, 'error');
|
|
371
|
+
bus.emit('pipeline-event', { event: 'loop_error', data: { error: err.message } });
|
|
372
|
+
|
|
373
|
+
// On error, don't crash — update status with error info
|
|
374
|
+
try {
|
|
375
|
+
const status = getStatus();
|
|
376
|
+
updateStatus({
|
|
377
|
+
error: err.message,
|
|
378
|
+
errorAt: new Date().toISOString(),
|
|
379
|
+
errorStage: status.stage,
|
|
380
|
+
});
|
|
381
|
+
} catch { /* status write failed — nothing we can do */ }
|
|
382
|
+
|
|
383
|
+
} finally {
|
|
384
|
+
isRunning = false;
|
|
385
|
+
releaseLock('pipeline-loop');
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Start the autonomous loop.
|
|
391
|
+
* @param {number} intervalMs - Tick interval (default: 30s)
|
|
392
|
+
*/
|
|
393
|
+
export function startLoop(intervalMs = 30_000) {
|
|
394
|
+
if (loopInterval) {
|
|
395
|
+
logActivity('LOOP', 'Loop already running', 'warn');
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
logActivity('LOOP', `Starting autonomous loop (interval: ${intervalMs}ms)`, 'info');
|
|
400
|
+
loopInterval = setInterval(tick, intervalMs);
|
|
401
|
+
|
|
402
|
+
// Run first tick immediately
|
|
403
|
+
tick();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Stop the loop.
|
|
408
|
+
*/
|
|
409
|
+
export function stopLoop() {
|
|
410
|
+
if (loopInterval) {
|
|
411
|
+
clearInterval(loopInterval);
|
|
412
|
+
loopInterval = null;
|
|
413
|
+
logActivity('LOOP', 'Autonomous loop stopped', 'info');
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Force a single tick (for testing or manual trigger).
|
|
419
|
+
*/
|
|
420
|
+
export async function forceTick() {
|
|
421
|
+
await tick();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Check if the loop is currently running.
|
|
426
|
+
*/
|
|
427
|
+
export function isLoopRunning() {
|
|
428
|
+
return loopInterval !== null;
|
|
429
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thread Triggers — Automatically create discussion threads on key pipeline events.
|
|
3
|
+
*
|
|
4
|
+
* Wire into event-bus listeners. Call initThreadTriggers() once at startup.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { bus } from '../shared/event-bus.js';
|
|
8
|
+
import { openThread } from '../roleplay/discussion-threads.js';
|
|
9
|
+
|
|
10
|
+
let initialized = false;
|
|
11
|
+
|
|
12
|
+
export function initThreadTriggers() {
|
|
13
|
+
if (initialized) return;
|
|
14
|
+
initialized = true;
|
|
15
|
+
|
|
16
|
+
bus.on('pipeline-event', (event) => {
|
|
17
|
+
try {
|
|
18
|
+
// When QA finds bugs
|
|
19
|
+
if (event.event === 'stage_error' && event.data?.stage === 'browser-qa') {
|
|
20
|
+
openThread({
|
|
21
|
+
title: `Browser QA failures: ${event.data.report?.failed || 'unknown'} pages`,
|
|
22
|
+
initiator: 'QA',
|
|
23
|
+
type: 'bug',
|
|
24
|
+
context: `QA found ${event.data.failedRoutes?.length || 0} failing routes:\n${(event.data.failedRoutes || []).join('\n')}`,
|
|
25
|
+
participants: ['CODER', 'PM'],
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// When bugfix loop exhausted
|
|
30
|
+
if (event.event === 'qa_bugfix_exhausted') {
|
|
31
|
+
openThread({
|
|
32
|
+
title: `Bug-fix loop exhausted after ${event.data?.cycles || '?'} cycles`,
|
|
33
|
+
initiator: 'PM',
|
|
34
|
+
type: 'escalation',
|
|
35
|
+
context: `${event.data?.remainingFails || '?'} page(s) still failing after ${event.data?.cycles || '?'} fix attempts. CEO intervention needed.`,
|
|
36
|
+
participants: ['QA', 'CODER'],
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// When review rejects
|
|
41
|
+
if (event.event === 'stage_complete' && event.data?.stage === 'review' && event.data?.verdict === 'REJECTED') {
|
|
42
|
+
openThread({
|
|
43
|
+
title: `Code review rejected: ${(event.data.reason || 'see feedback').slice(0, 60)}`,
|
|
44
|
+
initiator: 'REVIEWER',
|
|
45
|
+
type: 'review',
|
|
46
|
+
context: event.data.feedback || 'No detailed feedback provided',
|
|
47
|
+
participants: ['CODER', 'ARCHITECT'],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// When cost guard exceeded
|
|
52
|
+
if (event.event === 'cost_exceeded') {
|
|
53
|
+
openThread({
|
|
54
|
+
title: `Cost guard exceeded: $${event.data?.current?.toFixed(2) || '?'}`,
|
|
55
|
+
initiator: 'PM',
|
|
56
|
+
type: 'escalation',
|
|
57
|
+
context: event.data?.message || 'AI spend exceeded the configured limit.',
|
|
58
|
+
participants: ['CEO'],
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
} catch { /* thread creation should never crash the pipeline */ }
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export const AGENT_IDENTITIES = {
|
|
2
|
+
pm: { name: 'PM', emoji: '📋', color: '#4285F4', prefix: '[PM]' },
|
|
3
|
+
architect: { name: 'Architect', emoji: '🏗', color: '#7C3AED', prefix: '[Architect]' },
|
|
4
|
+
coder: { name: 'Coder', emoji: '⚡', color: '#22C55E', prefix: '[Coder]' },
|
|
5
|
+
reviewer: { name: 'Reviewer', emoji: '🔍', color: '#F59E0B', prefix: '[Reviewer]' },
|
|
6
|
+
deployer: { name: 'Deployer', emoji: '📦', color: '#EF4444', prefix: '[Deployer]' },
|
|
7
|
+
system: { name: 'System', emoji: '🤖', color: '#6B7280', prefix: '[System]' },
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Type prefix map for Telegram formatting
|
|
11
|
+
const TYPE_PREFIX = {
|
|
12
|
+
status: '_',
|
|
13
|
+
handoff: '',
|
|
14
|
+
error: '⚠️ ',
|
|
15
|
+
question: '❓ ',
|
|
16
|
+
text: '',
|
|
17
|
+
action: '',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function formatForTelegram(role, message, type = 'text') {
|
|
21
|
+
const id = AGENT_IDENTITIES[role] || AGENT_IDENTITIES.system;
|
|
22
|
+
const prefix = TYPE_PREFIX[type] || '';
|
|
23
|
+
return `${id.emoji} *${id.name}*: ${prefix}${message}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function formatForSlack(role, message, type = 'text') {
|
|
27
|
+
const id = AGENT_IDENTITIES[role] || AGENT_IDENTITIES.system;
|
|
28
|
+
const prefix = TYPE_PREFIX[type] || '';
|
|
29
|
+
return {
|
|
30
|
+
blocks: [
|
|
31
|
+
{
|
|
32
|
+
type: 'context',
|
|
33
|
+
elements: [
|
|
34
|
+
{ type: 'mrkdwn', text: `${id.emoji} *${id.name}*` },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: 'section',
|
|
39
|
+
text: { type: 'mrkdwn', text: `${prefix}${message}` },
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
attachments: [
|
|
43
|
+
{ color: id.color, fallback: `${id.name}: ${message}` },
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function formatForWebSocket(role, message, type = 'text') {
|
|
49
|
+
const id = AGENT_IDENTITIES[role] || AGENT_IDENTITIES.system;
|
|
50
|
+
return {
|
|
51
|
+
role,
|
|
52
|
+
name: id.name,
|
|
53
|
+
emoji: id.emoji,
|
|
54
|
+
color: id.color,
|
|
55
|
+
message,
|
|
56
|
+
type,
|
|
57
|
+
timestamp: new Date().toISOString(),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function formatForOffice(role, message, type = 'text') {
|
|
62
|
+
const id = AGENT_IDENTITIES[role] || AGENT_IDENTITIES.system;
|
|
63
|
+
return {
|
|
64
|
+
role,
|
|
65
|
+
name: id.name,
|
|
66
|
+
emoji: id.emoji,
|
|
67
|
+
message,
|
|
68
|
+
type,
|
|
69
|
+
timestamp: new Date().toISOString(),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getIdentity(role) {
|
|
74
|
+
return AGENT_IDENTITIES[role] || AGENT_IDENTITIES.system;
|
|
75
|
+
}
|