agent-collab-mcp 1.0.0
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/build/dashboard.d.ts +8 -0
- package/build/dashboard.js +575 -0
- package/build/db.d.ts +22 -0
- package/build/db.js +150 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +68 -0
- package/build/strategies.d.ts +43 -0
- package/build/strategies.js +172 -0
- package/build/templates.d.ts +11 -0
- package/build/templates.js +506 -0
- package/build/tools/context.d.ts +2 -0
- package/build/tools/context.js +37 -0
- package/build/tools/reviews.d.ts +2 -0
- package/build/tools/reviews.js +85 -0
- package/build/tools/setup.d.ts +2 -0
- package/build/tools/setup.js +96 -0
- package/build/tools/status.d.ts +2 -0
- package/build/tools/status.js +106 -0
- package/build/tools/strategy.d.ts +2 -0
- package/build/tools/strategy.js +115 -0
- package/build/tools/tasks.d.ts +2 -0
- package/build/tools/tasks.js +141 -0
- package/package.json +46 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Agent Collaboration Dashboard — real-time web UI for task coordination.
|
|
4
|
+
* Reads from the SQLite database and serves a self-contained HTML dashboard.
|
|
5
|
+
*
|
|
6
|
+
* Usage: node build/dashboard.js [--port 4800]
|
|
7
|
+
*/
|
|
8
|
+
import http from "node:http";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import Database from "better-sqlite3";
|
|
12
|
+
import { getAllStrategies } from "./strategies.js";
|
|
13
|
+
const DEFAULT_PORT = 4800;
|
|
14
|
+
const DB_DIR = ".agent-collab";
|
|
15
|
+
const DB_FILE = "collab.db";
|
|
16
|
+
function findDb() {
|
|
17
|
+
const dbPath = path.join(process.cwd(), DB_DIR, DB_FILE);
|
|
18
|
+
if (!fs.existsSync(dbPath)) {
|
|
19
|
+
console.error(`Database not found: ${dbPath}`);
|
|
20
|
+
console.error("Run init.sh first or start the MCP server.");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const db = new Database(dbPath, { readonly: true });
|
|
24
|
+
db.pragma("journal_mode = WAL");
|
|
25
|
+
return db;
|
|
26
|
+
}
|
|
27
|
+
function getConfig(db, key, fallback) {
|
|
28
|
+
const row = db.prepare("SELECT value FROM config WHERE key = ?").get(key);
|
|
29
|
+
return row?.value ?? fallback;
|
|
30
|
+
}
|
|
31
|
+
function apiOverview(db) {
|
|
32
|
+
const strategy = getConfig(db, "strategy", "architect-builder");
|
|
33
|
+
const engineMode = getConfig(db, "engine_mode", "both");
|
|
34
|
+
const strategyDef = getAllStrategies().find(s => s.id === strategy);
|
|
35
|
+
const counts = db.prepare("SELECT status, COUNT(*) as cnt FROM tasks GROUP BY status").all();
|
|
36
|
+
const total = counts.reduce((s, r) => s + r.cnt, 0);
|
|
37
|
+
const statusMap = {};
|
|
38
|
+
for (const r of counts)
|
|
39
|
+
statusMap[r.status] = r.cnt;
|
|
40
|
+
return {
|
|
41
|
+
strategy: {
|
|
42
|
+
id: strategy,
|
|
43
|
+
name: strategyDef?.name ?? strategy,
|
|
44
|
+
description: strategyDef?.description ?? "",
|
|
45
|
+
primary: strategyDef?.roles.primary.name ?? "Primary",
|
|
46
|
+
secondary: strategyDef?.roles.secondary.name ?? "Secondary",
|
|
47
|
+
},
|
|
48
|
+
engine_mode: engineMode,
|
|
49
|
+
tasks: { total, ...statusMap },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function apiTasks(db) {
|
|
53
|
+
return db.prepare(`
|
|
54
|
+
SELECT t.id, t.title, t.status, t.owner, t.depends_on, t.context, t.acceptance, t.plan,
|
|
55
|
+
t.created_at, t.updated_at,
|
|
56
|
+
(SELECT COUNT(*) FROM reviews r WHERE r.task_id = t.id) as review_rounds,
|
|
57
|
+
(SELECT verdict FROM reviews r WHERE r.task_id = t.id ORDER BY round DESC LIMIT 1) as last_verdict
|
|
58
|
+
FROM tasks t ORDER BY CAST(SUBSTR(t.id, 3) AS INTEGER)
|
|
59
|
+
`).all();
|
|
60
|
+
}
|
|
61
|
+
function apiActivity(db, limit = 50) {
|
|
62
|
+
return db.prepare("SELECT timestamp, agent, action FROM activity_log ORDER BY id DESC LIMIT ?").all(limit);
|
|
63
|
+
}
|
|
64
|
+
function apiTaskDetail(db, taskId) {
|
|
65
|
+
const task = db.prepare("SELECT * FROM tasks WHERE id = ?").get(taskId);
|
|
66
|
+
if (!task)
|
|
67
|
+
return null;
|
|
68
|
+
const reviews = db.prepare("SELECT round, verdict, issues, notes, created_at FROM reviews WHERE task_id = ? ORDER BY round").all(taskId);
|
|
69
|
+
return { ...task, reviews };
|
|
70
|
+
}
|
|
71
|
+
function apiStrategies() {
|
|
72
|
+
return getAllStrategies().map(s => ({
|
|
73
|
+
id: s.id,
|
|
74
|
+
name: s.name,
|
|
75
|
+
description: s.description,
|
|
76
|
+
best_for: s.best_for,
|
|
77
|
+
primary: s.roles.primary.name,
|
|
78
|
+
secondary: s.roles.secondary.name,
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
function parsePort() {
|
|
82
|
+
const idx = process.argv.indexOf("--port");
|
|
83
|
+
if (idx !== -1 && process.argv[idx + 1]) {
|
|
84
|
+
return parseInt(process.argv[idx + 1], 10) || DEFAULT_PORT;
|
|
85
|
+
}
|
|
86
|
+
return DEFAULT_PORT;
|
|
87
|
+
}
|
|
88
|
+
const port = parsePort();
|
|
89
|
+
const db = findDb();
|
|
90
|
+
const server = http.createServer((req, res) => {
|
|
91
|
+
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
92
|
+
if (url.pathname.startsWith("/api/")) {
|
|
93
|
+
res.setHeader("Content-Type", "application/json");
|
|
94
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
95
|
+
try {
|
|
96
|
+
let data;
|
|
97
|
+
switch (url.pathname) {
|
|
98
|
+
case "/api/overview":
|
|
99
|
+
data = apiOverview(db);
|
|
100
|
+
break;
|
|
101
|
+
case "/api/tasks":
|
|
102
|
+
data = apiTasks(db);
|
|
103
|
+
break;
|
|
104
|
+
case "/api/activity":
|
|
105
|
+
data = apiActivity(db, parseInt(url.searchParams.get("limit") ?? "50", 10));
|
|
106
|
+
break;
|
|
107
|
+
case "/api/task": {
|
|
108
|
+
const id = url.searchParams.get("id");
|
|
109
|
+
if (!id) {
|
|
110
|
+
res.writeHead(400);
|
|
111
|
+
res.end('{"error":"missing id"}');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
data = apiTaskDetail(db, id);
|
|
115
|
+
if (!data) {
|
|
116
|
+
res.writeHead(404);
|
|
117
|
+
res.end('{"error":"not found"}');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case "/api/strategies":
|
|
123
|
+
data = apiStrategies();
|
|
124
|
+
break;
|
|
125
|
+
default:
|
|
126
|
+
res.writeHead(404);
|
|
127
|
+
res.end('{"error":"not found"}');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
res.writeHead(200);
|
|
131
|
+
res.end(JSON.stringify(data));
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
res.writeHead(500);
|
|
135
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
140
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
141
|
+
res.writeHead(200);
|
|
142
|
+
res.end(DASHBOARD_HTML);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
res.writeHead(404);
|
|
146
|
+
res.end("Not found");
|
|
147
|
+
});
|
|
148
|
+
server.listen(port, () => {
|
|
149
|
+
console.log(`\n ⚡ Agent Collab Dashboard`);
|
|
150
|
+
console.log(` ➜ http://localhost:${port}\n`);
|
|
151
|
+
});
|
|
152
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
153
|
+
// Dashboard HTML — self-contained, no external dependencies
|
|
154
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
155
|
+
const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
156
|
+
<html lang="en">
|
|
157
|
+
<head>
|
|
158
|
+
<meta charset="utf-8">
|
|
159
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
160
|
+
<title>Agent Collab Dashboard</title>
|
|
161
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
162
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
|
163
|
+
<style>
|
|
164
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
165
|
+
:root{
|
|
166
|
+
--bg-0:#0a0a0f;--bg-1:#12121a;--bg-2:#1a1a27;--bg-3:#242436;
|
|
167
|
+
--fg-0:#e8e8f0;--fg-1:#a0a0b8;--fg-2:#6a6a82;
|
|
168
|
+
--accent:#6c5ce7;--accent-glow:#6c5ce740;
|
|
169
|
+
--green:#00b894;--green-bg:#00b89418;
|
|
170
|
+
--yellow:#fdcb6e;--yellow-bg:#fdcb6e18;
|
|
171
|
+
--blue:#0984e3;--blue-bg:#0984e318;
|
|
172
|
+
--red:#d63031;--red-bg:#d6303118;
|
|
173
|
+
--orange:#e17055;--orange-bg:#e1705518;
|
|
174
|
+
--cyan:#00cec9;--cyan-bg:#00cec918;
|
|
175
|
+
--radius:12px;--radius-sm:8px;
|
|
176
|
+
--font-sans:'Inter',system-ui,sans-serif;
|
|
177
|
+
--font-mono:'JetBrains Mono',monospace;
|
|
178
|
+
--shadow:0 4px 24px rgba(0,0,0,.4);
|
|
179
|
+
}
|
|
180
|
+
html{font-size:15px;background:var(--bg-0);color:var(--fg-0);font-family:var(--font-sans)}
|
|
181
|
+
body{min-height:100vh}
|
|
182
|
+
|
|
183
|
+
/* ── Header ─────────────────────────────────────────────── */
|
|
184
|
+
header{
|
|
185
|
+
background:linear-gradient(135deg,var(--bg-1),var(--bg-2));
|
|
186
|
+
border-bottom:1px solid var(--bg-3);
|
|
187
|
+
padding:1.2rem 2rem;display:flex;align-items:center;gap:1.5rem;
|
|
188
|
+
position:sticky;top:0;z-index:100;backdrop-filter:blur(12px);
|
|
189
|
+
}
|
|
190
|
+
header h1{font-size:1.25rem;font-weight:800;letter-spacing:-.02em;
|
|
191
|
+
background:linear-gradient(135deg,var(--accent),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
|
192
|
+
.header-meta{display:flex;gap:.75rem;margin-left:auto;align-items:center}
|
|
193
|
+
.badge{
|
|
194
|
+
font-size:.72rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;
|
|
195
|
+
padding:.3rem .7rem;border-radius:20px;font-family:var(--font-mono);
|
|
196
|
+
}
|
|
197
|
+
.badge-strategy{background:var(--accent-glow);color:var(--accent);border:1px solid var(--accent)}
|
|
198
|
+
.badge-engine{background:var(--cyan-bg);color:var(--cyan);border:1px solid var(--cyan)}
|
|
199
|
+
.pulse{width:8px;height:8px;border-radius:50%;background:var(--green);
|
|
200
|
+
box-shadow:0 0 6px var(--green);animation:pulse 2s ease-in-out infinite}
|
|
201
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
|
202
|
+
.refresh-label{font-size:.7rem;color:var(--fg-2);font-family:var(--font-mono)}
|
|
203
|
+
|
|
204
|
+
/* ── Layout ─────────────────────────────────────────────── */
|
|
205
|
+
main{display:flex;flex-direction:column;gap:1.2rem;padding:1.5rem 2rem;max-width:1800px;margin:0 auto;min-height:calc(100vh - 64px)}
|
|
206
|
+
.middle{display:grid;grid-template-columns:1fr 320px;gap:1.2rem;flex:1;min-height:0}
|
|
207
|
+
@media(max-width:1100px){.middle{grid-template-columns:1fr;grid-template-rows:auto auto}}
|
|
208
|
+
|
|
209
|
+
/* ── Stats Bar ──────────────────────────────────────────── */
|
|
210
|
+
.stats{grid-column:1/-1;display:flex;gap:1rem;flex-wrap:wrap}
|
|
211
|
+
.stat-card{
|
|
212
|
+
flex:1;min-width:140px;background:var(--bg-1);border:1px solid var(--bg-3);
|
|
213
|
+
border-radius:var(--radius);padding:1rem 1.2rem;
|
|
214
|
+
display:flex;flex-direction:column;gap:.3rem;transition:border-color .2s;
|
|
215
|
+
}
|
|
216
|
+
.stat-card:hover{border-color:var(--accent)}
|
|
217
|
+
.stat-card .label{font-size:.7rem;text-transform:uppercase;letter-spacing:.06em;color:var(--fg-2);font-weight:600}
|
|
218
|
+
.stat-card .value{font-size:1.8rem;font-weight:800;font-family:var(--font-mono);letter-spacing:-.03em}
|
|
219
|
+
.stat-card.total .value{color:var(--fg-0)}
|
|
220
|
+
.stat-card.assigned .value{color:var(--blue)}
|
|
221
|
+
.stat-card.in-progress .value{color:var(--yellow)}
|
|
222
|
+
.stat-card.review .value{color:var(--orange)}
|
|
223
|
+
.stat-card.changes .value{color:var(--red)}
|
|
224
|
+
.stat-card.done .value{color:var(--green)}
|
|
225
|
+
|
|
226
|
+
/* ── Kanban Board ───────────────────────────────────────── */
|
|
227
|
+
.board{overflow-x:auto;overflow-y:visible;min-width:0}
|
|
228
|
+
.board h2{font-size:.9rem;font-weight:700;color:var(--fg-1);text-transform:uppercase;
|
|
229
|
+
letter-spacing:.08em;margin-bottom:1rem}
|
|
230
|
+
.kanban{display:flex;flex-wrap:nowrap;gap:.8rem;min-height:300px;min-width:min-content}
|
|
231
|
+
.kanban .column{width:200px;min-width:200px;flex:0 0 auto}
|
|
232
|
+
.column{background:var(--bg-1);border:1px solid var(--bg-3);border-radius:var(--radius);
|
|
233
|
+
padding:.8rem;display:flex;flex-direction:column;gap:.6rem;min-height:200px}
|
|
234
|
+
.column-header{
|
|
235
|
+
display:flex;align-items:center;gap:.5rem;padding-bottom:.5rem;
|
|
236
|
+
border-bottom:2px solid var(--bg-3);margin-bottom:.2rem;
|
|
237
|
+
}
|
|
238
|
+
.column-header .dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
|
|
239
|
+
.column-header .col-title{font-size:.72rem;font-weight:700;text-transform:uppercase;
|
|
240
|
+
letter-spacing:.06em;color:var(--fg-2)}
|
|
241
|
+
.column-header .col-count{
|
|
242
|
+
margin-left:auto;font-size:.65rem;font-family:var(--font-mono);
|
|
243
|
+
background:var(--bg-3);padding:.15rem .45rem;border-radius:10px;color:var(--fg-2)}
|
|
244
|
+
.col-assigned .dot{background:var(--blue)}
|
|
245
|
+
.col-in-progress .dot{background:var(--yellow)}
|
|
246
|
+
.col-review .dot{background:var(--orange)}
|
|
247
|
+
.col-changes-requested .dot{background:var(--red)}
|
|
248
|
+
.col-done .dot{background:var(--green)}
|
|
249
|
+
|
|
250
|
+
/* ── Task Cards ─────────────────────────────────────────── */
|
|
251
|
+
.task-card{
|
|
252
|
+
background:var(--bg-2);border:1px solid var(--bg-3);border-radius:var(--radius-sm);
|
|
253
|
+
padding:.7rem .8rem;cursor:pointer;transition:all .15s;position:relative;
|
|
254
|
+
overflow:hidden;
|
|
255
|
+
}
|
|
256
|
+
.task-card::before{
|
|
257
|
+
content:'';position:absolute;left:0;top:0;bottom:0;width:3px;
|
|
258
|
+
}
|
|
259
|
+
.col-assigned .task-card::before{background:var(--blue)}
|
|
260
|
+
.col-in-progress .task-card::before{background:var(--yellow)}
|
|
261
|
+
.col-review .task-card::before{background:var(--orange)}
|
|
262
|
+
.col-changes-requested .task-card::before{background:var(--red)}
|
|
263
|
+
.col-done .task-card::before{background:var(--green)}
|
|
264
|
+
.task-card:hover{transform:translateY(-2px);border-color:var(--accent);box-shadow:var(--shadow)}
|
|
265
|
+
.task-card .task-id{font-size:.65rem;font-family:var(--font-mono);color:var(--fg-2);font-weight:600}
|
|
266
|
+
.task-card .task-title{font-size:.82rem;font-weight:600;margin-top:.2rem;line-height:1.3;
|
|
267
|
+
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
268
|
+
.task-card .task-meta{display:flex;gap:.5rem;margin-top:.4rem;align-items:center}
|
|
269
|
+
.task-card .task-owner{font-size:.6rem;font-family:var(--font-mono);color:var(--fg-2);
|
|
270
|
+
background:var(--bg-3);padding:.1rem .4rem;border-radius:6px}
|
|
271
|
+
.task-card .task-reviews{font-size:.6rem;color:var(--fg-2)}
|
|
272
|
+
|
|
273
|
+
/* ── Sidebar ────────────────────────────────────────────── */
|
|
274
|
+
.sidebar{display:flex;flex-direction:column;gap:1.2rem}
|
|
275
|
+
|
|
276
|
+
/* ── Strategy Panel ─────────────────────────────────────── */
|
|
277
|
+
.panel{background:var(--bg-1);border:1px solid var(--bg-3);border-radius:var(--radius);padding:1rem 1.2rem}
|
|
278
|
+
.panel h3{font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;
|
|
279
|
+
color:var(--fg-2);margin-bottom:.8rem;display:flex;align-items:center;gap:.5rem}
|
|
280
|
+
.panel h3 .icon{font-size:1rem}
|
|
281
|
+
.strategy-name{font-size:1.05rem;font-weight:700;color:var(--fg-0);margin-bottom:.3rem}
|
|
282
|
+
.strategy-desc{font-size:.78rem;color:var(--fg-1);line-height:1.5;margin-bottom:.8rem}
|
|
283
|
+
.roles-grid{display:grid;grid-template-columns:1fr 1fr;gap:.6rem}
|
|
284
|
+
.role-box{
|
|
285
|
+
background:var(--bg-2);border-radius:var(--radius-sm);padding:.6rem .7rem;
|
|
286
|
+
border:1px solid var(--bg-3);
|
|
287
|
+
}
|
|
288
|
+
.role-box .role-label{font-size:.6rem;font-weight:700;text-transform:uppercase;
|
|
289
|
+
letter-spacing:.06em;margin-bottom:.25rem}
|
|
290
|
+
.role-box .role-name{font-size:.8rem;font-weight:600}
|
|
291
|
+
.role-box.primary .role-label{color:var(--accent)}
|
|
292
|
+
.role-box.secondary .role-label{color:var(--cyan)}
|
|
293
|
+
.engine-mapping{
|
|
294
|
+
margin-top:.7rem;font-size:.72rem;color:var(--fg-2);font-family:var(--font-mono);
|
|
295
|
+
background:var(--bg-2);padding:.5rem .7rem;border-radius:var(--radius-sm);
|
|
296
|
+
border:1px solid var(--bg-3);line-height:1.7;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* ── Activity Feed ──────────────────────────────────────── */
|
|
300
|
+
.activity-list{display:flex;flex-direction:column;gap:0;max-height:400px;overflow-y:auto}
|
|
301
|
+
.activity-list::-webkit-scrollbar{width:4px}
|
|
302
|
+
.activity-list::-webkit-scrollbar-track{background:transparent}
|
|
303
|
+
.activity-list::-webkit-scrollbar-thumb{background:var(--bg-3);border-radius:4px}
|
|
304
|
+
.activity-item{
|
|
305
|
+
display:grid;grid-template-columns:auto 1fr;gap:.4rem .7rem;
|
|
306
|
+
padding:.55rem 0;border-bottom:1px solid var(--bg-3);align-items:start;
|
|
307
|
+
}
|
|
308
|
+
.activity-item:last-child{border-bottom:none}
|
|
309
|
+
.activity-ts{font-size:.6rem;font-family:var(--font-mono);color:var(--fg-2);white-space:nowrap;padding-top:.1rem}
|
|
310
|
+
.activity-action{font-size:.78rem;color:var(--fg-1);line-height:1.4}
|
|
311
|
+
.activity-agent{font-weight:600;font-size:.68rem;font-family:var(--font-mono);
|
|
312
|
+
padding:.1rem .35rem;border-radius:4px;margin-right:.3rem}
|
|
313
|
+
.activity-agent.cursor{background:var(--accent-glow);color:var(--accent)}
|
|
314
|
+
.activity-agent.claude-code{background:var(--cyan-bg);color:var(--cyan)}
|
|
315
|
+
.activity-agent.unknown{background:var(--bg-3);color:var(--fg-2)}
|
|
316
|
+
.empty-state{text-align:center;padding:2rem 1rem;color:var(--fg-2);font-size:.85rem}
|
|
317
|
+
|
|
318
|
+
/* ── Task Detail Modal ──────────────────────────────────── */
|
|
319
|
+
.modal-overlay{
|
|
320
|
+
position:fixed;inset:0;background:rgba(0,0,0,.6);backdrop-filter:blur(6px);
|
|
321
|
+
z-index:200;display:none;align-items:center;justify-content:center;padding:2rem;
|
|
322
|
+
}
|
|
323
|
+
.modal-overlay.open{display:flex}
|
|
324
|
+
.modal{
|
|
325
|
+
background:var(--bg-1);border:1px solid var(--bg-3);border-radius:var(--radius);
|
|
326
|
+
max-width:720px;width:100%;max-height:80vh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,.5);
|
|
327
|
+
padding:1.5rem 2rem;
|
|
328
|
+
}
|
|
329
|
+
.modal::-webkit-scrollbar{width:6px}
|
|
330
|
+
.modal::-webkit-scrollbar-thumb{background:var(--bg-3);border-radius:4px}
|
|
331
|
+
.modal-header{display:flex;align-items:center;gap:.8rem;margin-bottom:1rem}
|
|
332
|
+
.modal-header .task-id-big{font-family:var(--font-mono);font-weight:700;font-size:.85rem;color:var(--fg-2)}
|
|
333
|
+
.modal-header .task-title-big{font-size:1.1rem;font-weight:700}
|
|
334
|
+
.modal-close{margin-left:auto;background:none;border:none;color:var(--fg-2);font-size:1.3rem;
|
|
335
|
+
cursor:pointer;padding:.3rem;border-radius:6px;transition:all .15s}
|
|
336
|
+
.modal-close:hover{background:var(--bg-3);color:var(--fg-0)}
|
|
337
|
+
.modal-status{
|
|
338
|
+
display:inline-block;font-size:.7rem;font-family:var(--font-mono);font-weight:600;
|
|
339
|
+
padding:.25rem .6rem;border-radius:20px;text-transform:uppercase;letter-spacing:.04em;margin-bottom:1rem;
|
|
340
|
+
}
|
|
341
|
+
.modal-status.assigned{background:var(--blue-bg);color:var(--blue);border:1px solid var(--blue)}
|
|
342
|
+
.modal-status.in-progress{background:var(--yellow-bg);color:var(--yellow);border:1px solid var(--yellow)}
|
|
343
|
+
.modal-status.review{background:var(--orange-bg);color:var(--orange);border:1px solid var(--orange)}
|
|
344
|
+
.modal-status.changes-requested{background:var(--red-bg);color:var(--red);border:1px solid var(--red)}
|
|
345
|
+
.modal-status.done{background:var(--green-bg);color:var(--green);border:1px solid var(--green)}
|
|
346
|
+
.modal-section{margin-bottom:1rem}
|
|
347
|
+
.modal-section h4{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;
|
|
348
|
+
color:var(--fg-2);margin-bottom:.4rem}
|
|
349
|
+
.modal-section pre{
|
|
350
|
+
font-family:var(--font-mono);font-size:.78rem;line-height:1.6;color:var(--fg-1);
|
|
351
|
+
background:var(--bg-2);padding:.8rem 1rem;border-radius:var(--radius-sm);
|
|
352
|
+
border:1px solid var(--bg-3);white-space:pre-wrap;word-break:break-word;
|
|
353
|
+
}
|
|
354
|
+
.review-entry{
|
|
355
|
+
background:var(--bg-2);border:1px solid var(--bg-3);border-radius:var(--radius-sm);
|
|
356
|
+
padding:.8rem 1rem;margin-bottom:.5rem;
|
|
357
|
+
}
|
|
358
|
+
.review-entry .review-header{display:flex;align-items:center;gap:.5rem;margin-bottom:.4rem}
|
|
359
|
+
.review-entry .round-badge{font-family:var(--font-mono);font-size:.65rem;font-weight:600;
|
|
360
|
+
background:var(--bg-3);padding:.15rem .4rem;border-radius:8px;color:var(--fg-2)}
|
|
361
|
+
.review-entry .verdict-badge{font-size:.65rem;font-weight:600;padding:.15rem .5rem;border-radius:8px}
|
|
362
|
+
.verdict-badge.approved{background:var(--green-bg);color:var(--green)}
|
|
363
|
+
.verdict-badge.changes-requested{background:var(--red-bg);color:var(--red)}
|
|
364
|
+
.review-entry .review-notes{font-size:.78rem;color:var(--fg-1);margin-top:.3rem}
|
|
365
|
+
.review-issue{font-size:.75rem;color:var(--fg-1);padding:.2rem 0;font-family:var(--font-mono)}
|
|
366
|
+
.review-issue .issue-loc{color:var(--accent)}
|
|
367
|
+
</style>
|
|
368
|
+
</head>
|
|
369
|
+
<body>
|
|
370
|
+
|
|
371
|
+
<header>
|
|
372
|
+
<h1>Agent Collab</h1>
|
|
373
|
+
<div class="header-meta">
|
|
374
|
+
<span class="badge badge-strategy" id="h-strategy">—</span>
|
|
375
|
+
<span class="badge badge-engine" id="h-engine">—</span>
|
|
376
|
+
<div class="pulse"></div>
|
|
377
|
+
<span class="refresh-label">auto-refresh 3s</span>
|
|
378
|
+
</div>
|
|
379
|
+
</header>
|
|
380
|
+
|
|
381
|
+
<main>
|
|
382
|
+
<section class="stats" id="stats"></section>
|
|
383
|
+
|
|
384
|
+
<div class="middle">
|
|
385
|
+
<section class="board">
|
|
386
|
+
<h2>Task Board</h2>
|
|
387
|
+
<div class="kanban" id="kanban"></div>
|
|
388
|
+
</section>
|
|
389
|
+
|
|
390
|
+
<aside class="sidebar">
|
|
391
|
+
<div class="panel" id="strategy-panel">
|
|
392
|
+
<h3><span class="icon">⚙</span> Strategy</h3>
|
|
393
|
+
<div id="strategy-content"></div>
|
|
394
|
+
</div>
|
|
395
|
+
<div class="panel">
|
|
396
|
+
<h3><span class="icon">⚡</span> Activity</h3>
|
|
397
|
+
<div class="activity-list" id="activity"></div>
|
|
398
|
+
</div>
|
|
399
|
+
</aside>
|
|
400
|
+
</div>
|
|
401
|
+
</main>
|
|
402
|
+
|
|
403
|
+
<div class="modal-overlay" id="modal-overlay">
|
|
404
|
+
<div class="modal" id="modal"></div>
|
|
405
|
+
</div>
|
|
406
|
+
|
|
407
|
+
<script>
|
|
408
|
+
const API = '';
|
|
409
|
+
const COLS = ['assigned','in-progress','review','changes-requested','done'];
|
|
410
|
+
const COL_LABELS = {assigned:'Assigned','in-progress':'In Progress',review:'Review','changes-requested':'Changes Req.',done:'Done'};
|
|
411
|
+
|
|
412
|
+
async function fetchJSON(path) {
|
|
413
|
+
const r = await fetch(API + path);
|
|
414
|
+
return r.json();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function escHtml(s) {
|
|
418
|
+
if (!s) return '';
|
|
419
|
+
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function renderStats(overview) {
|
|
423
|
+
const el = document.getElementById('stats');
|
|
424
|
+
const t = overview.tasks;
|
|
425
|
+
const cards = [
|
|
426
|
+
{cls:'total',label:'Total Tasks',value:t.total||0},
|
|
427
|
+
{cls:'assigned',label:'Assigned',value:t.assigned||0},
|
|
428
|
+
{cls:'in-progress',label:'In Progress',value:t['in-progress']||0},
|
|
429
|
+
{cls:'review',label:'In Review',value:t.review||0},
|
|
430
|
+
{cls:'changes',label:'Changes Req.',value:t['changes-requested']||0},
|
|
431
|
+
{cls:'done',label:'Done',value:t.done||0},
|
|
432
|
+
];
|
|
433
|
+
el.innerHTML = cards.map(c =>
|
|
434
|
+
'<div class="stat-card '+c.cls+'"><span class="label">'+c.label+'</span><span class="value">'+c.value+'</span></div>'
|
|
435
|
+
).join('');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function renderKanban(tasks) {
|
|
439
|
+
const el = document.getElementById('kanban');
|
|
440
|
+
const grouped = {};
|
|
441
|
+
COLS.forEach(c => grouped[c] = []);
|
|
442
|
+
tasks.forEach(t => { if (grouped[t.status]) grouped[t.status].push(t); });
|
|
443
|
+
|
|
444
|
+
el.innerHTML = COLS.map(col => {
|
|
445
|
+
const items = grouped[col];
|
|
446
|
+
const cards = items.length ? items.map(t =>
|
|
447
|
+
'<div class="task-card" data-id="'+escHtml(t.id)+'" onclick="openTask(\\''+escHtml(t.id)+'\\')">'+
|
|
448
|
+
'<div class="task-id">'+escHtml(t.id)+'</div>'+
|
|
449
|
+
'<div class="task-title">'+escHtml(t.title)+'</div>'+
|
|
450
|
+
'<div class="task-meta">'+
|
|
451
|
+
'<span class="task-owner">'+escHtml(t.owner)+'</span>'+
|
|
452
|
+
(t.review_rounds > 0 ? '<span class="task-reviews">'+t.review_rounds+' review(s)</span>' : '')+
|
|
453
|
+
'</div>'+
|
|
454
|
+
'</div>'
|
|
455
|
+
).join('') : '<div class="empty-state">No tasks</div>';
|
|
456
|
+
|
|
457
|
+
return '<div class="column col-'+col+'">'+
|
|
458
|
+
'<div class="column-header">'+
|
|
459
|
+
'<div class="dot"></div>'+
|
|
460
|
+
'<span class="col-title">'+COL_LABELS[col]+'</span>'+
|
|
461
|
+
'<span class="col-count">'+items.length+'</span>'+
|
|
462
|
+
'</div>'+cards+'</div>';
|
|
463
|
+
}).join('');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function renderStrategy(overview) {
|
|
467
|
+
const s = overview.strategy;
|
|
468
|
+
const m = overview.engine_mode;
|
|
469
|
+
document.getElementById('h-strategy').textContent = s.name;
|
|
470
|
+
document.getElementById('h-engine').textContent = m;
|
|
471
|
+
|
|
472
|
+
let mapping = '';
|
|
473
|
+
if (m === 'both') {
|
|
474
|
+
mapping = 'cursor \\u2192 Primary ('+escHtml(s.primary)+')\\nclaude-code \\u2192 Secondary ('+escHtml(s.secondary)+')';
|
|
475
|
+
} else {
|
|
476
|
+
const eng = m.replace('-only','');
|
|
477
|
+
mapping = eng + ' \\u2192 Both roles ('+escHtml(s.primary)+' + '+escHtml(s.secondary)+')';
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
document.getElementById('strategy-content').innerHTML =
|
|
481
|
+
'<div class="strategy-name">'+escHtml(s.name)+'</div>'+
|
|
482
|
+
'<div class="strategy-desc">'+escHtml(s.description)+'</div>'+
|
|
483
|
+
'<div class="roles-grid">'+
|
|
484
|
+
'<div class="role-box primary"><div class="role-label">Primary</div><div class="role-name">'+escHtml(s.primary)+'</div></div>'+
|
|
485
|
+
'<div class="role-box secondary"><div class="role-label">Secondary</div><div class="role-name">'+escHtml(s.secondary)+'</div></div>'+
|
|
486
|
+
'</div>'+
|
|
487
|
+
'<div class="engine-mapping">'+escHtml(mapping).replace(/\\n/g,'<br>')+'</div>';
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function renderActivity(items) {
|
|
491
|
+
const el = document.getElementById('activity');
|
|
492
|
+
if (!items.length) { el.innerHTML = '<div class="empty-state">No activity yet</div>'; return; }
|
|
493
|
+
el.innerHTML = items.map(a => {
|
|
494
|
+
const agentCls = a.agent === 'cursor' ? 'cursor' : a.agent === 'claude-code' ? 'claude-code' : 'unknown';
|
|
495
|
+
const ts = a.timestamp ? a.timestamp.replace('T',' ').slice(0,16) : '';
|
|
496
|
+
return '<div class="activity-item">'+
|
|
497
|
+
'<div class="activity-ts">'+escHtml(ts)+'</div>'+
|
|
498
|
+
'<div class="activity-action"><span class="activity-agent '+agentCls+'">'+escHtml(a.agent)+'</span> '+escHtml(a.action)+'</div>'+
|
|
499
|
+
'</div>';
|
|
500
|
+
}).join('');
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function openTask(id) {
|
|
504
|
+
const data = await fetchJSON('/api/task?id='+encodeURIComponent(id));
|
|
505
|
+
if (!data) return;
|
|
506
|
+
|
|
507
|
+
const reviews = (data.reviews || []).map(r => {
|
|
508
|
+
let issues = '';
|
|
509
|
+
if (r.issues) {
|
|
510
|
+
try {
|
|
511
|
+
const parsed = JSON.parse(r.issues);
|
|
512
|
+
issues = parsed.map(i =>
|
|
513
|
+
'<div class="review-issue"><span class="issue-loc">['+(i.file||'general')+(i.line?':'+i.line:'')+']</span> '+escHtml(i.description)+'</div>'
|
|
514
|
+
).join('');
|
|
515
|
+
} catch(e) { issues = '<div class="review-issue">'+escHtml(r.issues)+'</div>'; }
|
|
516
|
+
}
|
|
517
|
+
return '<div class="review-entry">'+
|
|
518
|
+
'<div class="review-header">'+
|
|
519
|
+
'<span class="round-badge">Round '+r.round+'</span>'+
|
|
520
|
+
'<span class="verdict-badge '+r.verdict+'">'+escHtml(r.verdict)+'</span>'+
|
|
521
|
+
'<span style="font-size:.65rem;color:var(--fg-2)">'+escHtml(r.created_at)+'</span>'+
|
|
522
|
+
'</div>'+
|
|
523
|
+
issues+
|
|
524
|
+
(r.notes ? '<div class="review-notes">'+escHtml(r.notes)+'</div>' : '')+
|
|
525
|
+
'</div>';
|
|
526
|
+
}).join('');
|
|
527
|
+
|
|
528
|
+
document.getElementById('modal').innerHTML =
|
|
529
|
+
'<div class="modal-header">'+
|
|
530
|
+
'<span class="task-id-big">'+escHtml(data.id)+'</span>'+
|
|
531
|
+
'<span class="task-title-big">'+escHtml(data.title)+'</span>'+
|
|
532
|
+
'<button class="modal-close" onclick="closeModal()">\\u2715</button>'+
|
|
533
|
+
'</div>'+
|
|
534
|
+
'<span class="modal-status '+data.status+'">'+escHtml(data.status)+'</span>'+
|
|
535
|
+
'<div style="font-size:.75rem;color:var(--fg-2);margin-bottom:1rem">Owner: '+escHtml(data.owner)+
|
|
536
|
+
(data.depends_on?' • Depends: '+escHtml(data.depends_on):'')+
|
|
537
|
+
' • Created: '+escHtml(data.created_at)+'</div>'+
|
|
538
|
+
(data.context ? '<div class="modal-section"><h4>Context</h4><pre>'+escHtml(data.context)+'</pre></div>' : '')+
|
|
539
|
+
(data.acceptance ? '<div class="modal-section"><h4>Acceptance Criteria</h4><pre>'+escHtml(data.acceptance)+'</pre></div>' : '')+
|
|
540
|
+
(data.plan ? '<div class="modal-section"><h4>Plan</h4><pre>'+escHtml(data.plan)+'</pre></div>' : '')+
|
|
541
|
+
(reviews ? '<div class="modal-section"><h4>Reviews</h4>'+reviews+'</div>' : '');
|
|
542
|
+
|
|
543
|
+
document.getElementById('modal-overlay').classList.add('open');
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function closeModal() {
|
|
547
|
+
document.getElementById('modal-overlay').classList.remove('open');
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
document.getElementById('modal-overlay').addEventListener('click', function(e) {
|
|
551
|
+
if (e.target === this) closeModal();
|
|
552
|
+
});
|
|
553
|
+
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeModal(); });
|
|
554
|
+
|
|
555
|
+
async function refresh() {
|
|
556
|
+
try {
|
|
557
|
+
const [overview, tasks, activity] = await Promise.all([
|
|
558
|
+
fetchJSON('/api/overview'),
|
|
559
|
+
fetchJSON('/api/tasks'),
|
|
560
|
+
fetchJSON('/api/activity?limit=30'),
|
|
561
|
+
]);
|
|
562
|
+
renderStats(overview);
|
|
563
|
+
renderKanban(tasks);
|
|
564
|
+
renderStrategy(overview);
|
|
565
|
+
renderActivity(activity);
|
|
566
|
+
} catch(e) {
|
|
567
|
+
console.error('Refresh failed:', e);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
refresh();
|
|
572
|
+
setInterval(refresh, 3000);
|
|
573
|
+
</script>
|
|
574
|
+
</body>
|
|
575
|
+
</html>`;
|
package/build/db.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { type Strategy, type RoleConfig, type ToolAccess, type EngineMode } from "./strategies.js";
|
|
3
|
+
export declare function isInitialized(): boolean;
|
|
4
|
+
export declare function getDb(): Database.Database;
|
|
5
|
+
export declare function getRole(): string;
|
|
6
|
+
export declare function getEngineMode(): EngineMode;
|
|
7
|
+
export declare function setEngineMode(db: Database.Database, mode: EngineMode): void;
|
|
8
|
+
export declare function isSingleEngine(): boolean;
|
|
9
|
+
export declare function getActiveStrategy(): Strategy;
|
|
10
|
+
export declare function setActiveStrategy(db: Database.Database, strategyId: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* Returns the RoleConfig for the current agent based on engine mode and AGENT_ROLE.
|
|
13
|
+
*
|
|
14
|
+
* - both + cursor → strategy.roles.primary
|
|
15
|
+
* - both + claude-code → strategy.roles.secondary
|
|
16
|
+
* - cursor-only → merged (primary + secondary)
|
|
17
|
+
* - claude-code-only → merged (primary + secondary)
|
|
18
|
+
*/
|
|
19
|
+
export declare function getMyRoleConfig(): RoleConfig;
|
|
20
|
+
export declare function getToolAccess(): ToolAccess;
|
|
21
|
+
export declare function getDefaultOwner(): string;
|
|
22
|
+
export declare function nextTaskId(db: Database.Database): string;
|