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,53 @@
1
+ /**
2
+ * Cron Scheduler — runs recurring QA on assigned projects.
3
+ *
4
+ * Supports: daily (6 AM), hourly, or manual (no cron).
5
+ * Sets scheduledQA flag in status.json — the autonomous loop picks it up.
6
+ */
7
+
8
+ import cron from 'node-cron';
9
+ import { getConfig } from '../config.js';
10
+ import { getStatus, updateStatus } from '../utils/pipeline.js';
11
+ import { logActivity } from '../utils/activity-log.js';
12
+
13
+ let cronJob = null;
14
+
15
+ export function startQACron() {
16
+ const config = getConfig();
17
+ const schedule = config.browserQA?.schedule;
18
+
19
+ if (!schedule || schedule === 'manual' || schedule === 'on_deploy') {
20
+ return; // No cron needed
21
+ }
22
+
23
+ if (schedule === 'daily') {
24
+ // Run QA daily at 6:00 AM
25
+ cronJob = cron.schedule('0 6 * * *', () => {
26
+ logActivity('CRON', 'Scheduled daily QA triggered', 'info');
27
+ const status = getStatus();
28
+ if (status.stage === 'idle') {
29
+ updateStatus({ scheduledQA: true });
30
+ }
31
+ });
32
+ logActivity('CRON', 'Daily QA cron scheduled (6:00 AM)', 'info');
33
+ }
34
+
35
+ if (schedule === 'hourly') {
36
+ cronJob = cron.schedule('0 * * * *', () => {
37
+ logActivity('CRON', 'Scheduled hourly QA triggered', 'info');
38
+ const status = getStatus();
39
+ if (status.stage === 'idle') {
40
+ updateStatus({ scheduledQA: true });
41
+ }
42
+ });
43
+ logActivity('CRON', 'Hourly QA cron scheduled', 'info');
44
+ }
45
+ }
46
+
47
+ export function stopQACron() {
48
+ if (cronJob) {
49
+ cronJob.stop();
50
+ cronJob = null;
51
+ logActivity('CRON', 'QA cron stopped', 'info');
52
+ }
53
+ }
@@ -0,0 +1,293 @@
1
+ import { createRequire } from 'module';
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { getConfig } from '../config.js';
5
+ import { getStatus, updateStatus } from './pipeline.js';
6
+ import { getCostEntries, trackUsage } from './cost-tracker.js';
7
+
8
+ let _Database = undefined; // undefined = not tried yet, null = not available
9
+
10
+ function getDatabase() {
11
+ if (_Database === undefined) {
12
+ try {
13
+ const require = createRequire(import.meta.url);
14
+ _Database = require('better-sqlite3');
15
+ } catch {
16
+ _Database = null;
17
+ }
18
+ }
19
+ return _Database;
20
+ }
21
+
22
+ const WORKFLOW_DIR = '.ai-workflow';
23
+ const DB_PATH = join(WORKFLOW_DIR, 'aicc.db');
24
+
25
+ const SCHEMA = `
26
+ CREATE TABLE IF NOT EXISTS pipeline_status (
27
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
28
+ feature_id TEXT,
29
+ phase TEXT,
30
+ stage TEXT,
31
+ status TEXT,
32
+ data TEXT,
33
+ updated_at TEXT DEFAULT (datetime('now'))
34
+ );
35
+ CREATE TABLE IF NOT EXISTS cost_entries (
36
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
37
+ ts TEXT,
38
+ provider TEXT,
39
+ model TEXT,
40
+ stage TEXT,
41
+ feature_id TEXT,
42
+ input_tokens INTEGER,
43
+ output_tokens INTEGER,
44
+ estimated_cost REAL,
45
+ duration_ms INTEGER,
46
+ data TEXT,
47
+ created_at TEXT DEFAULT (datetime('now'))
48
+ );
49
+ CREATE TABLE IF NOT EXISTS audit_log (
50
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
51
+ ts TEXT,
52
+ action TEXT,
53
+ user_id TEXT,
54
+ details TEXT,
55
+ created_at TEXT DEFAULT (datetime('now'))
56
+ );
57
+ CREATE TABLE IF NOT EXISTS checkpoints (
58
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
59
+ feature_id TEXT,
60
+ stage TEXT,
61
+ sub_step TEXT,
62
+ content TEXT,
63
+ hash TEXT,
64
+ created_at TEXT DEFAULT (datetime('now')),
65
+ UNIQUE(feature_id, stage, sub_step)
66
+ );
67
+ CREATE INDEX IF NOT EXISTS idx_cost_feature ON cost_entries(feature_id);
68
+ CREATE INDEX IF NOT EXISTS idx_cost_provider ON cost_entries(provider);
69
+ CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log(action);
70
+ CREATE INDEX IF NOT EXISTS idx_checkpoint_feature ON checkpoints(feature_id);
71
+ `;
72
+
73
+ let _storageType = null;
74
+
75
+ export function getStorageType() {
76
+ if (_storageType) return _storageType;
77
+ try {
78
+ const cfg = getConfig();
79
+ _storageType = cfg.storage?.type === 'sqlite' ? 'sqlite' : 'file';
80
+ } catch {
81
+ _storageType = 'file';
82
+ }
83
+ return _storageType;
84
+ }
85
+
86
+ export class StorageAdapter {
87
+ constructor(type = 'file') {
88
+ this.type = type;
89
+ this.db = null;
90
+ if (type === 'sqlite') {
91
+ this._initSQLite();
92
+ }
93
+ }
94
+
95
+ _initSQLite() {
96
+ const Database = getDatabase();
97
+ if (!Database) {
98
+ console.warn('[StorageAdapter] better-sqlite3 not installed. Falling back to file storage.');
99
+ console.warn(' Install with: npm install better-sqlite3');
100
+ this.type = 'file';
101
+ return;
102
+ }
103
+ try {
104
+ if (!existsSync(WORKFLOW_DIR)) mkdirSync(WORKFLOW_DIR, { recursive: true });
105
+ this.db = new Database(DB_PATH);
106
+ this.db.pragma('journal_mode = WAL');
107
+ this.db.pragma('foreign_keys = ON');
108
+ this.db.exec(SCHEMA);
109
+ } catch (e) {
110
+ console.warn(`[StorageAdapter] SQLite init failed: ${e.message}. Falling back to file storage.`);
111
+ this.type = 'file';
112
+ this.db = null;
113
+ }
114
+ }
115
+
116
+ // --- Pipeline Status ---
117
+ readStatus() {
118
+ if (this.type === 'sqlite' && this.db) {
119
+ const row = this.db.prepare('SELECT * FROM pipeline_status ORDER BY updated_at DESC LIMIT 1').get();
120
+ if (!row) return {};
121
+ return { ...JSON.parse(row.data || '{}'), feature_id: row.feature_id, phase: row.phase, stage: row.stage, status: row.status };
122
+ }
123
+ return getStatus();
124
+ }
125
+
126
+ writeStatus(data) {
127
+ if (this.type === 'sqlite' && this.db) {
128
+ const stmt = this.db.prepare(
129
+ 'INSERT INTO pipeline_status (feature_id, phase, stage, status, data) VALUES (?, ?, ?, ?, ?)'
130
+ );
131
+ stmt.run(data.featureId || '', data.phase || '', data.stage || '', data.status || '', JSON.stringify(data));
132
+ return;
133
+ }
134
+ return updateStatus(data);
135
+ }
136
+
137
+ // --- Cost Tracking ---
138
+ readCosts(filters = {}) {
139
+ if (this.type === 'sqlite' && this.db) {
140
+ let query = 'SELECT * FROM cost_entries WHERE 1=1';
141
+ const params = [];
142
+ if (filters.featureId) { query += ' AND feature_id = ?'; params.push(filters.featureId); }
143
+ if (filters.provider) { query += ' AND provider = ?'; params.push(filters.provider); }
144
+ if (filters.stage) { query += ' AND stage = ?'; params.push(filters.stage); }
145
+ if (filters.since) { query += ' AND ts >= ?'; params.push(filters.since); }
146
+ query += ' ORDER BY created_at DESC';
147
+ if (filters.limit) { query += ' LIMIT ?'; params.push(filters.limit); }
148
+ return this.db.prepare(query).all(...params).map(row => ({
149
+ ...JSON.parse(row.data || '{}'),
150
+ ts: row.ts, provider: row.provider, model: row.model,
151
+ stage: row.stage, featureId: row.feature_id,
152
+ inputTokens: row.input_tokens, outputTokens: row.output_tokens,
153
+ estimatedCost: row.estimated_cost, durationMs: row.duration_ms,
154
+ }));
155
+ }
156
+ return getCostEntries(filters);
157
+ }
158
+
159
+ writeCost(entry) {
160
+ if (this.type === 'sqlite' && this.db) {
161
+ const stmt = this.db.prepare(
162
+ 'INSERT INTO cost_entries (ts, provider, model, stage, feature_id, input_tokens, output_tokens, estimated_cost, duration_ms, data) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
163
+ );
164
+ stmt.run(
165
+ entry.ts || new Date().toISOString(), entry.provider || '', entry.model || '',
166
+ entry.stage || '', entry.featureId || '',
167
+ entry.inputTokens || 0, entry.outputTokens || 0,
168
+ entry.estimatedCost || 0, entry.durationMs || 0,
169
+ JSON.stringify(entry)
170
+ );
171
+ return;
172
+ }
173
+ return trackUsage(entry);
174
+ }
175
+
176
+ // --- Audit Log ---
177
+ readAuditLog(filters = {}) {
178
+ if (this.type === 'sqlite' && this.db) {
179
+ let query = 'SELECT * FROM audit_log WHERE 1=1';
180
+ const params = [];
181
+ if (filters.action) { query += ' AND action = ?'; params.push(filters.action); }
182
+ if (filters.userId) { query += ' AND user_id = ?'; params.push(filters.userId); }
183
+ if (filters.since) { query += ' AND ts >= ?'; params.push(filters.since); }
184
+ query += ' ORDER BY created_at DESC';
185
+ if (filters.limit) { query += ' LIMIT ?'; params.push(filters.limit); }
186
+ return this.db.prepare(query).all(...params).map(row => ({
187
+ ts: row.ts, action: row.action, userId: row.user_id,
188
+ ...JSON.parse(row.details || '{}'),
189
+ }));
190
+ }
191
+ // File-based fallback: read from audit.jsonl if it exists
192
+ const auditFile = join(WORKFLOW_DIR, 'audit.jsonl');
193
+ if (!existsSync(auditFile)) return [];
194
+ return readFileSync(auditFile, 'utf8').split('\n').filter(Boolean).map(line => {
195
+ try { return JSON.parse(line); } catch { return null; }
196
+ }).filter(Boolean);
197
+ }
198
+
199
+ writeAuditEntry(entry) {
200
+ if (this.type === 'sqlite' && this.db) {
201
+ const stmt = this.db.prepare(
202
+ 'INSERT INTO audit_log (ts, action, user_id, details) VALUES (?, ?, ?, ?)'
203
+ );
204
+ stmt.run(entry.ts || new Date().toISOString(), entry.action || '', entry.userId || '', JSON.stringify(entry));
205
+ return;
206
+ }
207
+ // File-based fallback
208
+ const auditFile = join(WORKFLOW_DIR, 'audit.jsonl');
209
+ if (!existsSync(WORKFLOW_DIR)) mkdirSync(WORKFLOW_DIR, { recursive: true });
210
+ const line = JSON.stringify({ ts: new Date().toISOString(), ...entry }) + '\n';
211
+ writeFileSync(auditFile, existsSync(auditFile) ? readFileSync(auditFile, 'utf8') + line : line);
212
+ }
213
+
214
+ // --- Checkpoints ---
215
+ readCheckpoint(featureId, stage, subStep) {
216
+ if (this.type === 'sqlite' && this.db) {
217
+ const row = this.db.prepare(
218
+ 'SELECT * FROM checkpoints WHERE feature_id = ? AND stage = ? AND sub_step = ? ORDER BY created_at DESC LIMIT 1'
219
+ ).get(featureId, stage, subStep);
220
+ if (!row) return null;
221
+ return { content: row.content, hash: row.hash, createdAt: row.created_at };
222
+ }
223
+ return null; // File-based checkpoints handled by pipeline.js
224
+ }
225
+
226
+ writeCheckpoint(featureId, stage, subStep, content, hash) {
227
+ if (this.type === 'sqlite' && this.db) {
228
+ const stmt = this.db.prepare(
229
+ 'INSERT OR REPLACE INTO checkpoints (feature_id, stage, sub_step, content, hash) VALUES (?, ?, ?, ?, ?)'
230
+ );
231
+ stmt.run(featureId, stage, subStep, content, hash);
232
+ return;
233
+ }
234
+ // File-based checkpoints handled by pipeline.js
235
+ }
236
+
237
+ // --- Migration ---
238
+ async migrate() {
239
+ if (!this.db) {
240
+ this._initSQLite();
241
+ if (!this.db) throw new Error('Cannot initialize SQLite. Install better-sqlite3: npm install better-sqlite3');
242
+ }
243
+
244
+ let migrated = { costs: 0, audit: 0 };
245
+
246
+ // Migrate costs.jsonl
247
+ const costsFile = join(WORKFLOW_DIR, 'costs.jsonl');
248
+ if (existsSync(costsFile)) {
249
+ const entries = readFileSync(costsFile, 'utf8').split('\n').filter(Boolean);
250
+ const stmt = this.db.prepare(
251
+ 'INSERT OR IGNORE INTO cost_entries (ts, provider, model, stage, feature_id, input_tokens, output_tokens, estimated_cost, duration_ms, data) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
252
+ );
253
+ const insertMany = this.db.transaction((lines) => {
254
+ for (const line of lines) {
255
+ try {
256
+ const e = JSON.parse(line);
257
+ stmt.run(e.ts || '', e.provider || '', e.model || '', e.stage || '', e.featureId || '',
258
+ e.inputTokens || 0, e.outputTokens || 0, e.estimatedCost || 0, e.durationMs || 0, line);
259
+ migrated.costs++;
260
+ } catch { /* skip malformed */ }
261
+ }
262
+ });
263
+ insertMany(entries);
264
+ }
265
+
266
+ // Migrate audit.jsonl
267
+ const auditFile = join(WORKFLOW_DIR, 'audit.jsonl');
268
+ if (existsSync(auditFile)) {
269
+ const entries = readFileSync(auditFile, 'utf8').split('\n').filter(Boolean);
270
+ const stmt = this.db.prepare(
271
+ 'INSERT OR IGNORE INTO audit_log (ts, action, user_id, details) VALUES (?, ?, ?, ?)'
272
+ );
273
+ const insertMany = this.db.transaction((lines) => {
274
+ for (const line of lines) {
275
+ try {
276
+ const e = JSON.parse(line);
277
+ stmt.run(e.ts || '', e.action || '', e.userId || '', line);
278
+ migrated.audit++;
279
+ } catch { /* skip malformed */ }
280
+ }
281
+ });
282
+ insertMany(entries);
283
+ }
284
+
285
+ return migrated;
286
+ }
287
+
288
+ close() {
289
+ if (this.db) { this.db.close(); this.db = null; }
290
+ }
291
+ }
292
+
293
+ export const storage = new StorageAdapter(getStorageType());
@@ -0,0 +1,272 @@
1
+ import chalk from 'chalk';
2
+ import { getConfig } from '../config.js';
3
+ import { getStatus } from './pipeline.js';
4
+ import { statusBar } from './status-bar.js';
5
+
6
+ // ─── Stage registry ────────────────────────────────────────────────────────────
7
+
8
+ const STAGE_META = {
9
+ idle: { label: 'Idle', color: 'gray' },
10
+ inbox: { label: 'Feature Submitted', color: 'yellow' },
11
+ spec_complete: { label: 'PM Spec Ready', color: 'cyan' },
12
+ arch_complete: { label: 'Architecture Ready', color: 'blue' },
13
+ implementation_complete: { label: 'Copilot Done · Needs Review', color: 'magenta' },
14
+ review_complete: { label: 'Review Ready · Approve?', color: 'green' },
15
+ approved: { label: 'Approved · Ready to Deploy', color: 'green' },
16
+ rejected: { label: 'Rejected · Needs Fixes', color: 'red' },
17
+ };
18
+
19
+ // ─── Internal helpers ──────────────────────────────────────────────────────────
20
+
21
+ function cols() {
22
+ // Always read current terminal width — works correctly after resize too.
23
+ return process.stdout.columns || 80;
24
+ }
25
+
26
+ function divider(c = '─') {
27
+ // Full-width line — no alignment needed, always correct.
28
+ return chalk.dim(c.repeat(cols()));
29
+ }
30
+
31
+ // ─── Header ────────────────────────────────────────────────────────────────────
32
+ //
33
+ // Design: Completely flat — no box borders that require exact width math.
34
+ // Looks great at any terminal width. Inspired by Claude Code / Gemini CLI.
35
+ //
36
+ // ◆ ProjectName · AI Control Center v1.0
37
+ // ● Architecture Ready
38
+ // Feature FEATURE-20260223-230241
39
+ // ──────────────────────────────────────────────────────────────────────────────
40
+
41
+ export function printHeader() {
42
+ const status = getStatus();
43
+ const stageMeta = STAGE_META[status.stage] || { label: status.stage || 'Unknown', color: 'white' };
44
+ const VER = 'v1.0';
45
+
46
+ // Feature — truncate to fit terminal
47
+ const featureRaw = status.current_feature || 'no active feature';
48
+ const maxFeat = Math.max(20, cols() - 16);
49
+ const feature = featureRaw.length > maxFeat ? featureRaw.slice(0, maxFeat - 1) + '…' : featureRaw;
50
+
51
+ // Pipeline mode badge — only when a feature is active
52
+ const modeStr = status.current_feature
53
+ ? ` ${chalk.dim('·')} ${status.pipeline_mode === 'auto' ? chalk.cyan('auto') : chalk.dim('manual')}`
54
+ : '';
55
+
56
+ console.clear();
57
+ console.log('');
58
+
59
+ // ── Row 1: ◆ ProjectName · AI Control Center v1.0 ──────
60
+ const projectName = (() => { try { return getConfig().name; } catch { return 'AI Control Center'; } })();
61
+ const titleFixed = ` ◆ ${projectName} · AI Control Center `;
62
+ const versionPad = Math.max(1, cols() - titleFixed.length - VER.length);
63
+ console.log(
64
+ ` ${chalk.cyan.bold('◆')} ${chalk.bold.white(projectName)} ${chalk.dim('·')} ${chalk.white('AI Control Center')}` +
65
+ ' '.repeat(versionPad) +
66
+ chalk.dim(VER)
67
+ );
68
+
69
+ // ── Row 2: ● Stage · feature-id · mode ────────────────────────────
70
+ console.log(
71
+ ` ${chalk[stageMeta.color](`● ${stageMeta.label}`)} ${chalk.dim('·')} ${chalk.dim(feature)}${modeStr}`
72
+ );
73
+
74
+ console.log('');
75
+ console.log(divider());
76
+ console.log('');
77
+
78
+ statusBar.reinstall();
79
+ }
80
+
81
+ // ─── Status / feedback ─────────────────────────────────────────────────────────
82
+
83
+ export function printDivider() {
84
+ console.log(divider());
85
+ }
86
+
87
+ export function printSuccess(msg) {
88
+ console.log(chalk.green(`\n ✓ ${msg}`));
89
+ }
90
+
91
+ export function printError(msg) {
92
+ console.log(chalk.red(`\n ✗ ${msg}`));
93
+ }
94
+
95
+ export function printWarning(msg) {
96
+ console.log(chalk.yellow(`\n ~ ${msg}`));
97
+ }
98
+
99
+ export function printInfo(msg) {
100
+ console.log(chalk.cyan(`\n → ${msg}`));
101
+ }
102
+
103
+ // ─── Content boxes ─────────────────────────────────────────────────────────────
104
+ // Uses indented text blocks — no drawn borders that need width math.
105
+
106
+ export function printBox(title, lines) {
107
+ console.log('');
108
+ console.log(chalk.bold.white(` ${title}`));
109
+ console.log(chalk.dim(` ${'─'.repeat(Math.min(50, cols() - 4))}`));
110
+ lines.forEach(line => console.log(` ${chalk.white(line || '')}`));
111
+ console.log('');
112
+ }
113
+
114
+ // ─── AI handoff prompts ────────────────────────────────────────────────────────
115
+ //
116
+ // Each uses a simple left-rule bar — no right border that needs width matching.
117
+ //
118
+ // ┃ CLAUDE CODE
119
+ // ┃ [prompt text here]
120
+ // ┃
121
+
122
+ function sectionBlock(headerColor, headerText, lines) {
123
+ console.log('');
124
+ console.log(chalk[headerColor].bold(` ┃ ${headerText}`));
125
+ lines.forEach(l => console.log(chalk.dim(' ┃ ') + chalk.white(l || '')));
126
+ console.log(chalk.dim(' ┃'));
127
+ console.log('');
128
+ }
129
+
130
+ export function printClaudePrompt(prompt) {
131
+ sectionBlock('magenta', 'CLAUDE CODE — paste this prompt:', prompt.split('\n'));
132
+ }
133
+
134
+ export function printCopilotPrompt(tasksFile) {
135
+ const archFile = tasksFile.replace('tasks/TASKS', 'architecture/ARCH');
136
+ sectionBlock('blue', 'COPILOT CHAT — paste this prompt:', [
137
+ `Read ${tasksFile}`,
138
+ 'and implement all tasks. Follow the architecture in',
139
+ archFile,
140
+ ]);
141
+ }
142
+
143
+ /**
144
+ * Show a structured verdict panel after Gemini review completes.
145
+ *
146
+ * verdict = {
147
+ * decision: 'APPROVED' | 'REJECTED' | 'UNKNOWN',
148
+ * blockers: string[],
149
+ * warnings: string[],
150
+ * approved: string[],
151
+ * actionItems: string[],
152
+ * }
153
+ */
154
+ export function printVerdictPanel(verdict, reviewFilePath) {
155
+ const isApproved = verdict.decision === 'APPROVED';
156
+ const color = isApproved ? 'green' : verdict.decision === 'UNKNOWN' ? 'yellow' : 'red';
157
+ const symbol = isApproved ? '✓' : '✗';
158
+ const bar = chalk.dim(' ┃ ');
159
+
160
+ console.log('');
161
+ console.log(chalk[color].bold(` ┃ REVIEW VERDICT`));
162
+ console.log(chalk.dim(' ┃'));
163
+
164
+ // Decision line
165
+ const decLabel = chalk[color].bold(`${symbol} ${verdict.decision}`);
166
+ const counts = [
167
+ verdict.blockers.length ? chalk.red(`${verdict.blockers.length} blocker${verdict.blockers.length !== 1 ? 's' : ''}`) : null,
168
+ verdict.warnings.length ? chalk.yellow(`${verdict.warnings.length} warning${verdict.warnings.length !== 1 ? 's' : ''}`) : null,
169
+ verdict.approved.length ? chalk.green(`${verdict.approved.length} approved`) : null,
170
+ ].filter(Boolean).join(chalk.dim(' · '));
171
+
172
+ console.log(`${bar}${decLabel} ${chalk.dim('·')} ${counts || chalk.dim('no issues')}`);
173
+ console.log(chalk.dim(' ┃'));
174
+
175
+ // Blockers
176
+ if (verdict.blockers.length) {
177
+ console.log(`${bar}${chalk.red.bold(`Blockers (must fix before merge)`)}`);
178
+ verdict.blockers.forEach((b, i) => {
179
+ console.log(`${bar} ${chalk.red(`${i + 1}.`)} ${chalk.white(b)}`);
180
+ });
181
+ console.log(chalk.dim(' ┃'));
182
+ }
183
+
184
+ // Warnings
185
+ if (verdict.warnings.length) {
186
+ console.log(`${bar}${chalk.yellow.bold('Warnings (non-blocking)')}`);
187
+ verdict.warnings.forEach((w, i) => {
188
+ console.log(`${bar} ${chalk.yellow(`${i + 1}.`)} ${chalk.white(w)}`);
189
+ });
190
+ console.log(chalk.dim(' ┃'));
191
+ }
192
+
193
+ // Action items
194
+ if (verdict.actionItems.length) {
195
+ console.log(`${bar}${chalk.cyan.bold('Action Items for Copilot')}`);
196
+ verdict.actionItems.forEach((a, i) => {
197
+ console.log(`${bar} ${chalk.cyan(`${i + 1}.`)} ${chalk.white(a)}`);
198
+ });
199
+ console.log(chalk.dim(' ┃'));
200
+ }
201
+
202
+ // File path
203
+ console.log(`${bar}${chalk.dim('Review file')} ${chalk.white(reviewFilePath)}`);
204
+ console.log(chalk.dim(' ┃'));
205
+
206
+ // Next step hint
207
+ if (isApproved) {
208
+ console.log(`${bar}${chalk.dim('Next')} ${chalk.green('Select ✓ Approve Feature in the menu')}`);
209
+ } else {
210
+ console.log(`${bar}${chalk.dim('Next')} ${chalk.yellow('Select ✗ Reject / Request Fixes to dispatch to Copilot')}`);
211
+ }
212
+ console.log('');
213
+ }
214
+
215
+ /**
216
+ * Show a structured deploy error panel.
217
+ * componentErrors: [{ fileName, lineNumber, problem }]
218
+ * testFailures: [{ name, methodName, message }]
219
+ */
220
+ export function printDeployErrorPanel(componentErrors, testFailures, attempt, maxAttempts, rawErrors = '') {
221
+ const total = componentErrors.length + testFailures.length;
222
+ const bar = chalk.dim(' ┃ ');
223
+
224
+ console.log('');
225
+ console.log(chalk.red.bold(' ┃ DEPLOY FAILED') + chalk.dim(` · attempt ${attempt} of ${maxAttempts}`));
226
+ console.log(chalk.dim(' ┃'));
227
+ console.log(`${bar}${chalk.red(`${total} error${total !== 1 ? 's' : ''} total`)} ${chalk.dim('·')} ${chalk.red(`${componentErrors.length} compile`)} ${chalk.dim('·')} ${chalk.red(`${testFailures.length} test`)}`);
228
+ console.log(chalk.dim(' ┃'));
229
+
230
+ if (componentErrors.length) {
231
+ console.log(`${bar}${chalk.red.bold('Compile Errors')}`);
232
+ componentErrors.forEach((e, i) => {
233
+ const loc = e.lineNumber ? chalk.dim(`:${e.lineNumber}`) : '';
234
+ console.log(`${bar} ${chalk.dim(`${i + 1}.`)} ${chalk.yellow(e.fileName || e.fullName)}${loc}`);
235
+ console.log(`${bar} ${chalk.white(e.problem)}`);
236
+ });
237
+ console.log(chalk.dim(' ┃'));
238
+ }
239
+
240
+ if (testFailures.length) {
241
+ console.log(`${bar}${chalk.red.bold('Test Failures')}`);
242
+ testFailures.forEach((f, i) => {
243
+ console.log(`${bar} ${chalk.dim(`${i + 1}.`)} ${chalk.yellow(`${f.name}.${f.methodName}`)}`);
244
+ console.log(`${bar} ${chalk.white((f.message || '').slice(0, 200))}`);
245
+ });
246
+ console.log(chalk.dim(' ┃'));
247
+ }
248
+
249
+ // Show raw output when SF CLI didn't return structured errors
250
+ if (total === 0 && rawErrors) {
251
+ console.log(`${bar}${chalk.yellow.bold('Raw Deploy Output (structured errors unavailable)')}`);
252
+ rawErrors.split('\n').slice(0, 20).forEach(line => {
253
+ console.log(`${bar} ${chalk.white(line)}`);
254
+ });
255
+ console.log(chalk.dim(' ┃'));
256
+ }
257
+
258
+ console.log('');
259
+ }
260
+
261
+ export function printCopilotFixPrompt(reviewFile, actionItems) {
262
+ const lines = [
263
+ `Read ${reviewFile}`,
264
+ 'Fix all ✗ Blockers and ~ Warnings listed in the review.',
265
+ ];
266
+ if (actionItems.length) {
267
+ lines.push('');
268
+ lines.push('Action Items:');
269
+ actionItems.forEach((item, i) => lines.push(` ${i + 1}. ${item}`));
270
+ }
271
+ sectionBlock('blue', 'COPILOT CHAT — paste this prompt:', lines);
272
+ }