@leejungkiin/awkit 1.1.6 → 1.1.9
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/README.md +51 -1
- package/bin/awk.js +2 -2
- package/core/GEMINI.md +45 -7
- package/package.json +8 -5
- package/skill-packs/neural-memory/skills/nm-memory-sync/SKILL.md +14 -1
- package/skills/ab-test-store-listing/SKILL.md +220 -0
- package/skills/android-aso/SKILL.md +197 -0
- package/skills/app-analytics/SKILL.md +210 -0
- package/skills/app-clips/SKILL.md +163 -0
- package/skills/app-icon-optimization/SKILL.md +170 -0
- package/skills/app-launch/SKILL.md +153 -0
- package/skills/app-marketing-context/SKILL.md +129 -0
- package/skills/app-store-featured/SKILL.md +213 -0
- package/skills/apple-search-ads/SKILL.md +205 -0
- package/skills/asc-metrics/SKILL.md +157 -0
- package/skills/aso-audit/SKILL.md +179 -0
- package/skills/competitor-analysis/SKILL.md +163 -0
- package/skills/competitor-tracking/SKILL.md +185 -0
- package/skills/crash-analytics/SKILL.md +181 -0
- package/skills/gitnexus-intelligence/SKILL.md +224 -0
- package/skills/in-app-events/SKILL.md +176 -0
- package/skills/keyword-research/SKILL.md +141 -0
- package/skills/localization/SKILL.md +165 -0
- package/skills/market-movers/SKILL.md +137 -0
- package/skills/market-pulse/SKILL.md +170 -0
- package/skills/metadata-optimization/SKILL.md +170 -0
- package/skills/monetization-strategy/SKILL.md +175 -0
- package/skills/onboarding-optimization/SKILL.md +194 -0
- package/skills/orchestrator/SKILL.md +306 -25
- package/skills/press-and-pr/SKILL.md +204 -0
- package/skills/rating-prompt-strategy/SKILL.md +184 -0
- package/skills/retention-optimization/SKILL.md +165 -0
- package/skills/review-management/SKILL.md +154 -0
- package/skills/screenshot-optimization/SKILL.md +167 -0
- package/skills/seasonal-aso/SKILL.md +141 -0
- package/skills/spec-gate/SKILL.md +312 -0
- package/skills/subscription-lifecycle/SKILL.md +206 -0
- package/skills/swiftui-pro/references/design.md +44 -0
- package/skills/symphony-enforcer/SKILL.md +92 -11
- package/skills/symphony-orchestrator/SKILL.md +9 -7
- package/skills/systematic-debugging/SKILL.md +32 -7
- package/skills/ua-campaign/SKILL.md +207 -0
- package/skills/verification-gate/SKILL.md +23 -2
- package/workflows/gitnexus.md +123 -0
- package/symphony/LICENSE +0 -21
- package/symphony/README.md +0 -178
- package/symphony/app/api/agents/route.js +0 -152
- package/symphony/app/api/events/route.js +0 -22
- package/symphony/app/api/knowledge/route.js +0 -253
- package/symphony/app/api/locks/route.js +0 -29
- package/symphony/app/api/notes/route.js +0 -125
- package/symphony/app/api/preflight/route.js +0 -23
- package/symphony/app/api/projects/route.js +0 -116
- package/symphony/app/api/roles/route.js +0 -134
- package/symphony/app/api/skills/route.js +0 -82
- package/symphony/app/api/status/route.js +0 -18
- package/symphony/app/api/tasks/route.js +0 -157
- package/symphony/app/api/workflows/route.js +0 -61
- package/symphony/app/api/workspaces/route.js +0 -15
- package/symphony/app/globals.css +0 -2605
- package/symphony/app/layout.js +0 -20
- package/symphony/app/page.js +0 -2122
- package/symphony/cli/index.js +0 -1060
- package/symphony/core/agent-manager.js +0 -357
- package/symphony/core/context-bus.js +0 -100
- package/symphony/core/db.js +0 -223
- package/symphony/core/file-lock-manager.js +0 -154
- package/symphony/core/merge-pipeline.js +0 -234
- package/symphony/core/orchestrator.js +0 -236
- package/symphony/core/task-manager.js +0 -335
- package/symphony/core/workspace-manager.js +0 -168
- package/symphony/jsconfig.json +0 -7
- package/symphony/lib/core.mjs +0 -1034
- package/symphony/mcp/index.js +0 -29
- package/symphony/mcp/server.js +0 -110
- package/symphony/mcp/tools/context.js +0 -80
- package/symphony/mcp/tools/locks.js +0 -99
- package/symphony/mcp/tools/status.js +0 -82
- package/symphony/mcp/tools/tasks.js +0 -216
- package/symphony/mcp/tools/workspace.js +0 -143
- package/symphony/next.config.mjs +0 -7
- package/symphony/package.json +0 -53
- package/symphony/scripts/postinstall.js +0 -49
- package/symphony/symphony.config.js +0 -41
package/symphony/lib/core.mjs
DELETED
|
@@ -1,1034 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core module bridge for Next.js API routes.
|
|
3
|
-
*
|
|
4
|
-
* Single centralized DB at ~/.gemini/antigravity/symphony/symphony.db
|
|
5
|
-
* with multi-project support via project_id scoping.
|
|
6
|
-
*
|
|
7
|
-
* - Projects: registered via projects table
|
|
8
|
-
* - Tasks/Events/Locks: scoped by project_id
|
|
9
|
-
* - Agents: global (1 agent works across projects)
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import Database from 'better-sqlite3';
|
|
13
|
-
import path from 'path';
|
|
14
|
-
import os from 'os';
|
|
15
|
-
import fs from 'fs';
|
|
16
|
-
|
|
17
|
-
// ─── Database Singleton ─────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
let dbInstance = null;
|
|
20
|
-
|
|
21
|
-
function getDbPath() {
|
|
22
|
-
const symphonyDir = path.join(os.homedir(), '.gemini', 'antigravity', 'symphony');
|
|
23
|
-
if (!fs.existsSync(symphonyDir)) {
|
|
24
|
-
fs.mkdirSync(symphonyDir, { recursive: true });
|
|
25
|
-
}
|
|
26
|
-
return path.join(symphonyDir, 'symphony.db');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function getDb() {
|
|
30
|
-
if (!dbInstance) {
|
|
31
|
-
const dbPath = getDbPath();
|
|
32
|
-
dbInstance = new Database(dbPath);
|
|
33
|
-
dbInstance.pragma('journal_mode = WAL');
|
|
34
|
-
dbInstance.pragma('foreign_keys = ON');
|
|
35
|
-
ensureSchema(dbInstance);
|
|
36
|
-
}
|
|
37
|
-
return dbInstance;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function ensureSchema(db) {
|
|
41
|
-
db.exec(`
|
|
42
|
-
-- Projects registry
|
|
43
|
-
CREATE TABLE IF NOT EXISTS projects (
|
|
44
|
-
id TEXT PRIMARY KEY,
|
|
45
|
-
name TEXT NOT NULL,
|
|
46
|
-
path TEXT,
|
|
47
|
-
icon TEXT DEFAULT '📁',
|
|
48
|
-
color TEXT DEFAULT '#8888a0',
|
|
49
|
-
is_active INTEGER DEFAULT 0,
|
|
50
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
51
|
-
last_active_at TEXT
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
-- Tasks (scoped by project_id)
|
|
55
|
-
CREATE TABLE IF NOT EXISTS tasks (
|
|
56
|
-
id TEXT PRIMARY KEY,
|
|
57
|
-
project_id TEXT,
|
|
58
|
-
title TEXT NOT NULL,
|
|
59
|
-
description TEXT,
|
|
60
|
-
status TEXT NOT NULL DEFAULT 'ready',
|
|
61
|
-
priority INTEGER DEFAULT 2,
|
|
62
|
-
sort_order INTEGER DEFAULT 0,
|
|
63
|
-
acceptance TEXT,
|
|
64
|
-
phase TEXT,
|
|
65
|
-
epic_id TEXT,
|
|
66
|
-
agent_id TEXT,
|
|
67
|
-
estimated_files TEXT,
|
|
68
|
-
workspace_path TEXT,
|
|
69
|
-
branch TEXT,
|
|
70
|
-
progress INTEGER DEFAULT 0,
|
|
71
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
72
|
-
claimed_at TEXT,
|
|
73
|
-
completed_at TEXT,
|
|
74
|
-
summary TEXT
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
-- Agents (global — not project-scoped)
|
|
78
|
-
CREATE TABLE IF NOT EXISTS agents (
|
|
79
|
-
id TEXT PRIMARY KEY,
|
|
80
|
-
name TEXT,
|
|
81
|
-
status TEXT DEFAULT 'idle',
|
|
82
|
-
current_task_id TEXT,
|
|
83
|
-
specialties TEXT DEFAULT '[]',
|
|
84
|
-
color TEXT DEFAULT '#8888a0',
|
|
85
|
-
max_concurrent INTEGER DEFAULT 1,
|
|
86
|
-
connected_at TEXT DEFAULT (datetime('now')),
|
|
87
|
-
last_heartbeat TEXT DEFAULT (datetime('now')),
|
|
88
|
-
workspace_path TEXT
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
-- File locks (scoped by project_id)
|
|
92
|
-
CREATE TABLE IF NOT EXISTS file_locks (
|
|
93
|
-
file_path TEXT PRIMARY KEY,
|
|
94
|
-
project_id TEXT,
|
|
95
|
-
agent_id TEXT NOT NULL,
|
|
96
|
-
task_id TEXT,
|
|
97
|
-
acquired_at TEXT DEFAULT (datetime('now'))
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
-- Events (scoped by project_id)
|
|
101
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
102
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
103
|
-
project_id TEXT,
|
|
104
|
-
agent_id TEXT,
|
|
105
|
-
task_id TEXT,
|
|
106
|
-
event_type TEXT NOT NULL,
|
|
107
|
-
payload TEXT,
|
|
108
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
-- Indexes
|
|
112
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
113
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_agent ON tasks(agent_id);
|
|
114
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
|
|
115
|
-
CREATE INDEX IF NOT EXISTS idx_events_time ON events(created_at);
|
|
116
|
-
CREATE INDEX IF NOT EXISTS idx_events_project ON events(project_id);
|
|
117
|
-
CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
|
|
118
|
-
|
|
119
|
-
-- Project-scoped agents (multi-agent architecture)
|
|
120
|
-
CREATE TABLE IF NOT EXISTS project_agents (
|
|
121
|
-
id TEXT PRIMARY KEY,
|
|
122
|
-
project_id TEXT NOT NULL,
|
|
123
|
-
name TEXT NOT NULL,
|
|
124
|
-
skills TEXT DEFAULT '[]',
|
|
125
|
-
status TEXT DEFAULT 'offline',
|
|
126
|
-
session_id TEXT,
|
|
127
|
-
current_task_id TEXT,
|
|
128
|
-
last_active_at TEXT,
|
|
129
|
-
idle_since TEXT,
|
|
130
|
-
max_concurrent INTEGER DEFAULT 1,
|
|
131
|
-
icon TEXT DEFAULT '🤖',
|
|
132
|
-
color TEXT DEFAULT '#8888a0',
|
|
133
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
CREATE INDEX IF NOT EXISTS idx_project_agents_project ON project_agents(project_id);
|
|
137
|
-
CREATE INDEX IF NOT EXISTS idx_project_agents_status ON project_agents(status);
|
|
138
|
-
CREATE INDEX IF NOT EXISTS idx_project_agents_session ON project_agents(session_id);
|
|
139
|
-
|
|
140
|
-
-- Notes (artifact & conversation tracking)
|
|
141
|
-
CREATE TABLE IF NOT EXISTS notes (
|
|
142
|
-
id TEXT PRIMARY KEY,
|
|
143
|
-
task_id TEXT,
|
|
144
|
-
project_id TEXT NOT NULL,
|
|
145
|
-
type TEXT NOT NULL,
|
|
146
|
-
title TEXT NOT NULL,
|
|
147
|
-
content TEXT,
|
|
148
|
-
file_path TEXT,
|
|
149
|
-
conversation_id TEXT,
|
|
150
|
-
metadata TEXT DEFAULT '{}',
|
|
151
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
152
|
-
updated_at TEXT DEFAULT (datetime('now'))
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
CREATE INDEX IF NOT EXISTS idx_notes_task ON notes(task_id);
|
|
156
|
-
CREATE INDEX IF NOT EXISTS idx_notes_project ON notes(project_id);
|
|
157
|
-
CREATE INDEX IF NOT EXISTS idx_notes_type ON notes(type);
|
|
158
|
-
CREATE INDEX IF NOT EXISTS idx_notes_conversation ON notes(conversation_id);
|
|
159
|
-
`);
|
|
160
|
-
|
|
161
|
-
// Incremental migrations for existing databases
|
|
162
|
-
migrateExistingDb(db);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Run incremental migrations for databases that may have older schemas.
|
|
167
|
-
*/
|
|
168
|
-
function migrateExistingDb(db) {
|
|
169
|
-
const taskCols = db.pragma('table_info(tasks)').map(c => c.name);
|
|
170
|
-
if (!taskCols.includes('sort_order')) {
|
|
171
|
-
db.exec('ALTER TABLE tasks ADD COLUMN sort_order INTEGER DEFAULT 0');
|
|
172
|
-
}
|
|
173
|
-
if (!taskCols.includes('project_id')) {
|
|
174
|
-
db.exec('ALTER TABLE tasks ADD COLUMN project_id TEXT');
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const agentCols = db.pragma('table_info(agents)').map(c => c.name);
|
|
178
|
-
if (!agentCols.includes('specialties')) {
|
|
179
|
-
db.exec("ALTER TABLE agents ADD COLUMN specialties TEXT DEFAULT '[]'");
|
|
180
|
-
}
|
|
181
|
-
if (!agentCols.includes('color')) {
|
|
182
|
-
db.exec("ALTER TABLE agents ADD COLUMN color TEXT DEFAULT '#8888a0'");
|
|
183
|
-
}
|
|
184
|
-
if (!agentCols.includes('max_concurrent')) {
|
|
185
|
-
db.exec('ALTER TABLE agents ADD COLUMN max_concurrent INTEGER DEFAULT 1');
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const eventCols = db.pragma('table_info(events)').map(c => c.name);
|
|
189
|
-
if (!eventCols.includes('project_id')) {
|
|
190
|
-
db.exec('ALTER TABLE events ADD COLUMN project_id TEXT');
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const lockCols = db.pragma('table_info(file_locks)').map(c => c.name);
|
|
194
|
-
if (!lockCols.includes('project_id')) {
|
|
195
|
-
db.exec('ALTER TABLE file_locks ADD COLUMN project_id TEXT');
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Ensure projects table exists (for DBs created before multi-project)
|
|
199
|
-
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='projects'").get();
|
|
200
|
-
if (!tables) {
|
|
201
|
-
db.exec(`
|
|
202
|
-
CREATE TABLE projects (
|
|
203
|
-
id TEXT PRIMARY KEY,
|
|
204
|
-
name TEXT NOT NULL,
|
|
205
|
-
path TEXT,
|
|
206
|
-
icon TEXT DEFAULT '📁',
|
|
207
|
-
color TEXT DEFAULT '#8888a0',
|
|
208
|
-
is_active INTEGER DEFAULT 0,
|
|
209
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
210
|
-
last_active_at TEXT
|
|
211
|
-
);
|
|
212
|
-
`);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Add required_skills to tasks
|
|
216
|
-
if (!taskCols.includes('required_skills')) {
|
|
217
|
-
db.exec("ALTER TABLE tasks ADD COLUMN required_skills TEXT DEFAULT '[]'");
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Add conversation_id to tasks (Hybrid approach C)
|
|
221
|
-
if (!taskCols.includes('conversation_id')) {
|
|
222
|
-
db.exec('ALTER TABLE tasks ADD COLUMN conversation_id TEXT');
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Ensure notes table exists (for DBs created before notes feature)
|
|
226
|
-
const notesTbl = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='notes'").get();
|
|
227
|
-
if (!notesTbl) {
|
|
228
|
-
db.exec(`
|
|
229
|
-
CREATE TABLE notes (
|
|
230
|
-
id TEXT PRIMARY KEY,
|
|
231
|
-
task_id TEXT,
|
|
232
|
-
project_id TEXT NOT NULL,
|
|
233
|
-
type TEXT NOT NULL,
|
|
234
|
-
title TEXT NOT NULL,
|
|
235
|
-
content TEXT,
|
|
236
|
-
file_path TEXT,
|
|
237
|
-
conversation_id TEXT,
|
|
238
|
-
metadata TEXT DEFAULT '{}',
|
|
239
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
240
|
-
updated_at TEXT DEFAULT (datetime('now'))
|
|
241
|
-
);
|
|
242
|
-
CREATE INDEX idx_notes_task ON notes(task_id);
|
|
243
|
-
CREATE INDEX idx_notes_project ON notes(project_id);
|
|
244
|
-
CREATE INDEX idx_notes_type ON notes(type);
|
|
245
|
-
CREATE INDEX idx_notes_conversation ON notes(conversation_id);
|
|
246
|
-
`);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// ─── Project Operations ─────────────────────────────────────────────────────
|
|
251
|
-
|
|
252
|
-
export function listProjects() {
|
|
253
|
-
const db = getDb();
|
|
254
|
-
return db.prepare('SELECT * FROM projects ORDER BY last_active_at DESC, created_at DESC').all();
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
export function getActiveProject() {
|
|
258
|
-
const db = getDb();
|
|
259
|
-
return db.prepare('SELECT * FROM projects WHERE is_active = 1').get() || null;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Resolve the project ID: use explicit param only.
|
|
264
|
-
* Returns null if no project specified (= show ALL tasks across projects).
|
|
265
|
-
* Active project is for UI display only — does NOT affect queries.
|
|
266
|
-
*/
|
|
267
|
-
function resolveProjectId(projectParam) {
|
|
268
|
-
if (projectParam === '__all__') return null;
|
|
269
|
-
if (projectParam) return projectParam;
|
|
270
|
-
// No auto-scoping — return null to show all
|
|
271
|
-
return null;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export function createProject({ id, name, projectPath, icon, color }) {
|
|
275
|
-
const db = getDb();
|
|
276
|
-
db.prepare(`
|
|
277
|
-
INSERT INTO projects (id, name, path, icon, color, last_active_at)
|
|
278
|
-
VALUES (?, ?, ?, ?, ?, datetime('now'))
|
|
279
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
280
|
-
name = excluded.name,
|
|
281
|
-
path = excluded.path,
|
|
282
|
-
icon = COALESCE(excluded.icon, icon),
|
|
283
|
-
color = COALESCE(excluded.color, color),
|
|
284
|
-
last_active_at = datetime('now')
|
|
285
|
-
`).run(id, name, projectPath || null, icon || '📁', color || '#8888a0');
|
|
286
|
-
return db.prepare('SELECT * FROM projects WHERE id = ?').get(id);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
export function updateProject(id, fields) {
|
|
290
|
-
const db = getDb();
|
|
291
|
-
const allowed = ['name', 'path', 'icon', 'color'];
|
|
292
|
-
const sets = [];
|
|
293
|
-
const params = [];
|
|
294
|
-
|
|
295
|
-
for (const key of allowed) {
|
|
296
|
-
if (fields[key] !== undefined) {
|
|
297
|
-
sets.push(`${key} = ?`);
|
|
298
|
-
params.push(fields[key]);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (sets.length === 0) return db.prepare('SELECT * FROM projects WHERE id = ?').get(id);
|
|
303
|
-
|
|
304
|
-
params.push(id);
|
|
305
|
-
db.prepare(`UPDATE projects SET ${sets.join(', ')} WHERE id = ?`).run(...params);
|
|
306
|
-
return db.prepare('SELECT * FROM projects WHERE id = ?').get(id);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
export function setActiveProject(projectId) {
|
|
310
|
-
const db = getDb();
|
|
311
|
-
db.prepare('UPDATE projects SET is_active = 0 WHERE is_active = 1').run();
|
|
312
|
-
if (projectId) {
|
|
313
|
-
db.prepare("UPDATE projects SET is_active = 1, last_active_at = datetime('now') WHERE id = ?").run(projectId);
|
|
314
|
-
}
|
|
315
|
-
return getActiveProject();
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
export function deleteProject(projectId) {
|
|
319
|
-
const db = getDb();
|
|
320
|
-
// Check for in-progress tasks
|
|
321
|
-
const busyTasks = db.prepare("SELECT COUNT(*) as count FROM tasks WHERE project_id = ? AND status IN ('claimed', 'in_progress')").get(projectId);
|
|
322
|
-
if (busyTasks.count > 0) {
|
|
323
|
-
throw new Error(`Cannot delete project with ${busyTasks.count} active tasks`);
|
|
324
|
-
}
|
|
325
|
-
db.prepare('DELETE FROM projects WHERE id = ?').run(projectId);
|
|
326
|
-
return true;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
export function getProjectStats() {
|
|
330
|
-
const db = getDb();
|
|
331
|
-
const rows = db.prepare(`
|
|
332
|
-
SELECT p.id, p.name, p.icon, p.color, p.is_active,
|
|
333
|
-
COUNT(t.id) as total_tasks,
|
|
334
|
-
SUM(CASE WHEN t.status = 'ready' THEN 1 ELSE 0 END) as ready,
|
|
335
|
-
SUM(CASE WHEN t.status IN ('claimed', 'in_progress') THEN 1 ELSE 0 END) as active,
|
|
336
|
-
SUM(CASE WHEN t.status = 'done' THEN 1 ELSE 0 END) as done
|
|
337
|
-
FROM projects p
|
|
338
|
-
LEFT JOIN tasks t ON t.project_id = p.id
|
|
339
|
-
GROUP BY p.id
|
|
340
|
-
ORDER BY p.is_active DESC, p.last_active_at DESC
|
|
341
|
-
`).all();
|
|
342
|
-
return rows;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// ─── Task Operations (project-scoped) ───────────────────────────────────────
|
|
346
|
-
|
|
347
|
-
export function listTasks({ status, project, limit = 50 } = {}) {
|
|
348
|
-
const db = getDb();
|
|
349
|
-
const projectId = resolveProjectId(project);
|
|
350
|
-
const conditions = [];
|
|
351
|
-
const params = [];
|
|
352
|
-
|
|
353
|
-
if (projectId) {
|
|
354
|
-
conditions.push('project_id = ?');
|
|
355
|
-
params.push(projectId);
|
|
356
|
-
}
|
|
357
|
-
if (status) {
|
|
358
|
-
conditions.push('status = ?');
|
|
359
|
-
params.push(status);
|
|
360
|
-
} else {
|
|
361
|
-
conditions.push("status != 'abandoned'");
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
365
|
-
params.push(limit);
|
|
366
|
-
|
|
367
|
-
return db.prepare(`SELECT * FROM tasks ${where} ORDER BY sort_order ASC, priority ASC, created_at DESC LIMIT ?`)
|
|
368
|
-
.all(...params);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
export function getTaskStats(project) {
|
|
372
|
-
const db = getDb();
|
|
373
|
-
const projectId = resolveProjectId(project);
|
|
374
|
-
|
|
375
|
-
let query = 'SELECT status, COUNT(*) as count FROM tasks';
|
|
376
|
-
const params = [];
|
|
377
|
-
if (projectId) {
|
|
378
|
-
query += ' WHERE project_id = ?';
|
|
379
|
-
params.push(projectId);
|
|
380
|
-
}
|
|
381
|
-
query += ' GROUP BY status';
|
|
382
|
-
|
|
383
|
-
const rows = db.prepare(query).all(...params);
|
|
384
|
-
const stats = { total: 0 };
|
|
385
|
-
for (const row of rows) {
|
|
386
|
-
stats[row.status] = row.count;
|
|
387
|
-
stats.total += row.count;
|
|
388
|
-
}
|
|
389
|
-
return stats;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
export function createTask(title, opts = {}) {
|
|
393
|
-
const db = getDb();
|
|
394
|
-
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
395
|
-
let id = 'sym-';
|
|
396
|
-
for (let i = 0; i < 8; i++) id += chars[Math.floor(Math.random() * chars.length)];
|
|
397
|
-
|
|
398
|
-
const initialStatus = opts.isDraft ? 'draft' : 'ready';
|
|
399
|
-
const projectId = opts.projectId || resolveProjectId(null);
|
|
400
|
-
|
|
401
|
-
db.prepare(`
|
|
402
|
-
INSERT INTO tasks (id, project_id, title, description, status, priority, acceptance, phase, estimated_files, conversation_id)
|
|
403
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
404
|
-
`).run(
|
|
405
|
-
id, projectId, title, opts.description || null, initialStatus,
|
|
406
|
-
opts.priority || 2, opts.acceptance || null,
|
|
407
|
-
opts.phase || null,
|
|
408
|
-
opts.estimatedFiles ? JSON.stringify(opts.estimatedFiles) : null,
|
|
409
|
-
opts.conversationId || null
|
|
410
|
-
);
|
|
411
|
-
|
|
412
|
-
return db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
export function updateTask(id, fields) {
|
|
416
|
-
const db = getDb();
|
|
417
|
-
const allowed = ['title', 'description', 'priority', 'acceptance', 'phase', 'sort_order', 'project_id', 'agent_id', 'conversation_id'];
|
|
418
|
-
const sets = [];
|
|
419
|
-
const params = [];
|
|
420
|
-
|
|
421
|
-
for (const key of allowed) {
|
|
422
|
-
if (fields[key] !== undefined) {
|
|
423
|
-
sets.push(`${key} = ?`);
|
|
424
|
-
params.push(fields[key]);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
if (sets.length === 0) return db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
429
|
-
|
|
430
|
-
params.push(id);
|
|
431
|
-
db.prepare(`UPDATE tasks SET ${sets.join(', ')} WHERE id = ?`).run(...params);
|
|
432
|
-
return db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
export function deleteTask(id) {
|
|
436
|
-
const db = getDb();
|
|
437
|
-
const task = db.prepare('SELECT status FROM tasks WHERE id = ?').get(id);
|
|
438
|
-
if (!task) throw new Error(`Task not found: ${id}`);
|
|
439
|
-
if (!['draft', 'ready'].includes(task.status)) {
|
|
440
|
-
throw new Error(`Cannot delete task in ${task.status} status`);
|
|
441
|
-
}
|
|
442
|
-
db.prepare('DELETE FROM tasks WHERE id = ?').run(id);
|
|
443
|
-
return true;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
export function approveTask(id) {
|
|
447
|
-
const db = getDb();
|
|
448
|
-
const task = db.prepare('SELECT status FROM tasks WHERE id = ?').get(id);
|
|
449
|
-
if (!task) throw new Error(`Task not found: ${id}`);
|
|
450
|
-
if (task.status !== 'draft') {
|
|
451
|
-
throw new Error(`Task ${id} is not a draft (status: ${task.status})`);
|
|
452
|
-
}
|
|
453
|
-
db.prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(id);
|
|
454
|
-
return db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
export function bulkApprove(ids) {
|
|
458
|
-
const db = getDb();
|
|
459
|
-
const stmt = db.prepare("UPDATE tasks SET status = 'ready' WHERE id = ? AND status = 'draft'");
|
|
460
|
-
let count = 0;
|
|
461
|
-
const run = db.transaction(() => {
|
|
462
|
-
for (const id of ids) {
|
|
463
|
-
const result = stmt.run(id);
|
|
464
|
-
count += result.changes;
|
|
465
|
-
}
|
|
466
|
-
});
|
|
467
|
-
run();
|
|
468
|
-
return count;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
export function reopenTask(id) {
|
|
472
|
-
const db = getDb();
|
|
473
|
-
db.prepare(`
|
|
474
|
-
UPDATE tasks
|
|
475
|
-
SET status = 'ready', agent_id = NULL, progress = 0,
|
|
476
|
-
claimed_at = NULL, completed_at = NULL, summary = NULL
|
|
477
|
-
WHERE id = ?
|
|
478
|
-
`).run(id);
|
|
479
|
-
return db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
export function reorderTasks(orderedIds) {
|
|
483
|
-
const db = getDb();
|
|
484
|
-
const stmt = db.prepare('UPDATE tasks SET sort_order = ? WHERE id = ?');
|
|
485
|
-
const run = db.transaction(() => {
|
|
486
|
-
for (let i = 0; i < orderedIds.length; i++) {
|
|
487
|
-
stmt.run(i, orderedIds[i]);
|
|
488
|
-
}
|
|
489
|
-
});
|
|
490
|
-
run();
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
export function claimTask(id, agentId = 'api-user') {
|
|
494
|
-
const db = getDb();
|
|
495
|
-
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
496
|
-
if (!task) throw new Error(`Task not found: ${id}`);
|
|
497
|
-
if (task.status !== 'ready') {
|
|
498
|
-
throw new Error(`Task ${id} is not claimable (status: ${task.status})`);
|
|
499
|
-
}
|
|
500
|
-
db.prepare(`UPDATE tasks SET status = 'claimed', agent_id = ?, claimed_at = datetime('now') WHERE id = ?`)
|
|
501
|
-
.run(agentId, id);
|
|
502
|
-
return db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
export function completeTask(id, summary = '') {
|
|
506
|
-
const db = getDb();
|
|
507
|
-
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
508
|
-
if (!task) throw new Error(`Task not found: ${id}`);
|
|
509
|
-
// Auto-claim if still ready
|
|
510
|
-
if (task.status === 'ready') {
|
|
511
|
-
db.prepare(`UPDATE tasks SET status = 'claimed', agent_id = 'api-user', claimed_at = datetime('now') WHERE id = ?`)
|
|
512
|
-
.run(id);
|
|
513
|
-
}
|
|
514
|
-
db.prepare(`UPDATE tasks SET status = 'done', summary = ?, progress = 100, completed_at = datetime('now') WHERE id = ?`)
|
|
515
|
-
.run(summary || 'Completed', id);
|
|
516
|
-
return db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
export function abandonTask(id, reason = '') {
|
|
520
|
-
const db = getDb();
|
|
521
|
-
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
522
|
-
if (!task) throw new Error(`Task not found: ${id}`);
|
|
523
|
-
db.prepare(`UPDATE tasks SET status = 'ready', agent_id = NULL, summary = ?, progress = 0 WHERE id = ?`)
|
|
524
|
-
.run(reason ? `Abandoned: ${reason}` : null, id);
|
|
525
|
-
return db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// ─── Status Operations ──────────────────────────────────────────────────────
|
|
529
|
-
|
|
530
|
-
export function getSystemStatus(project) {
|
|
531
|
-
const db = getDb();
|
|
532
|
-
const projectId = resolveProjectId(project);
|
|
533
|
-
|
|
534
|
-
const agents = db.prepare('SELECT * FROM agents ORDER BY connected_at DESC').all()
|
|
535
|
-
.map(a => ({
|
|
536
|
-
id: a.id,
|
|
537
|
-
name: a.name,
|
|
538
|
-
status: a.status,
|
|
539
|
-
currentTask: a.current_task_id,
|
|
540
|
-
specialties: a.specialties ? JSON.parse(a.specialties) : [],
|
|
541
|
-
color: a.color || '#8888a0',
|
|
542
|
-
maxConcurrent: a.max_concurrent || 1,
|
|
543
|
-
lastHeartbeat: a.last_heartbeat,
|
|
544
|
-
}));
|
|
545
|
-
|
|
546
|
-
const stats = getTaskStats(project);
|
|
547
|
-
const activeProject = getActiveProject();
|
|
548
|
-
|
|
549
|
-
let lockQuery = 'SELECT * FROM file_locks';
|
|
550
|
-
const lockParams = [];
|
|
551
|
-
if (projectId) {
|
|
552
|
-
lockQuery += ' WHERE project_id = ?';
|
|
553
|
-
lockParams.push(projectId);
|
|
554
|
-
}
|
|
555
|
-
lockQuery += ' ORDER BY acquired_at DESC';
|
|
556
|
-
|
|
557
|
-
const lockedFiles = db.prepare(lockQuery).all(...lockParams)
|
|
558
|
-
.map(l => ({
|
|
559
|
-
file: l.file_path,
|
|
560
|
-
agent: l.agent_id,
|
|
561
|
-
task: l.task_id,
|
|
562
|
-
since: l.acquired_at,
|
|
563
|
-
}));
|
|
564
|
-
|
|
565
|
-
return {
|
|
566
|
-
agents,
|
|
567
|
-
stats,
|
|
568
|
-
lockedFiles,
|
|
569
|
-
activeProject: activeProject || null,
|
|
570
|
-
config: { maxAgents: 3 },
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
/**
|
|
575
|
-
* Preflight check — single call to verify all mandatory gates.
|
|
576
|
-
* Returns everything an AI agent needs to start working.
|
|
577
|
-
*/
|
|
578
|
-
export function getPreflightStatus(projectParam) {
|
|
579
|
-
const db = getDb();
|
|
580
|
-
const activeProject = getActiveProject();
|
|
581
|
-
const projectId = projectParam || (activeProject ? activeProject.id : null);
|
|
582
|
-
|
|
583
|
-
// Task stats (project-scoped if available)
|
|
584
|
-
const stats = getTaskStats(projectId ? projectId : '__all__');
|
|
585
|
-
|
|
586
|
-
// In-progress tasks
|
|
587
|
-
const inProgressTasks = projectId
|
|
588
|
-
? db.prepare("SELECT id, title, priority, phase FROM tasks WHERE project_id = ? AND status IN ('claimed', 'in_progress') ORDER BY priority ASC").all(projectId)
|
|
589
|
-
: db.prepare("SELECT id, title, priority, phase FROM tasks WHERE status IN ('claimed', 'in_progress') ORDER BY priority ASC").all();
|
|
590
|
-
|
|
591
|
-
// Ready tasks (top 5)
|
|
592
|
-
const readyTasks = projectId
|
|
593
|
-
? db.prepare("SELECT id, title, priority, phase FROM tasks WHERE project_id = ? AND status = 'ready' ORDER BY sort_order ASC, priority ASC LIMIT 5").all(projectId)
|
|
594
|
-
: db.prepare("SELECT id, title, priority, phase FROM tasks WHERE status = 'ready' ORDER BY sort_order ASC, priority ASC LIMIT 5").all();
|
|
595
|
-
|
|
596
|
-
// All projects summary
|
|
597
|
-
const projects = db.prepare('SELECT id, name, icon, is_active FROM projects ORDER BY is_active DESC, last_active_at DESC').all();
|
|
598
|
-
|
|
599
|
-
// Project agents (multi-agent architecture)
|
|
600
|
-
const projectAgents = projectId
|
|
601
|
-
? db.prepare('SELECT * FROM project_agents WHERE project_id = ? ORDER BY created_at ASC').all(projectId).map(normalizeProjectAgent)
|
|
602
|
-
: db.prepare('SELECT * FROM project_agents ORDER BY project_id ASC, created_at ASC').all().map(normalizeProjectAgent);
|
|
603
|
-
|
|
604
|
-
// Gate status
|
|
605
|
-
const gates = {
|
|
606
|
-
server: 'PASS',
|
|
607
|
-
project: activeProject ? 'PASS' : (projects.length > 0 ? 'WARN' : 'NO_PROJECTS'),
|
|
608
|
-
tasks: inProgressTasks.length > 0 ? 'HAS_ACTIVE' : (readyTasks.length > 0 ? 'HAS_READY' : 'EMPTY'),
|
|
609
|
-
agents: projectAgents.length > 0 ? 'HAS_AGENTS' : 'NO_AGENTS',
|
|
610
|
-
};
|
|
611
|
-
|
|
612
|
-
const overall = gates.server === 'PASS' ? 'PASS' : 'FAIL';
|
|
613
|
-
|
|
614
|
-
return {
|
|
615
|
-
gate_status: overall,
|
|
616
|
-
gates,
|
|
617
|
-
server: { status: 'running', version: '0.1.0' },
|
|
618
|
-
active_project: activeProject ? { id: activeProject.id, name: activeProject.name, icon: activeProject.icon } : null,
|
|
619
|
-
projects: projects.map(p => ({ id: p.id, name: p.name, icon: p.icon, active: !!p.is_active })),
|
|
620
|
-
tasks: {
|
|
621
|
-
stats,
|
|
622
|
-
in_progress: inProgressTasks,
|
|
623
|
-
ready: readyTasks,
|
|
624
|
-
},
|
|
625
|
-
agents: projectAgents,
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// ─── Event Operations ───────────────────────────────────────────────────────
|
|
630
|
-
|
|
631
|
-
export function queryEvents({ since, eventType, project, limit = 30 } = {}) {
|
|
632
|
-
const db = getDb();
|
|
633
|
-
const projectId = resolveProjectId(project);
|
|
634
|
-
const conditions = [];
|
|
635
|
-
const params = [];
|
|
636
|
-
|
|
637
|
-
if (projectId) { conditions.push('project_id = ?'); params.push(projectId); }
|
|
638
|
-
if (since) { conditions.push('created_at > ?'); params.push(since); }
|
|
639
|
-
if (eventType) { conditions.push('event_type = ?'); params.push(eventType); }
|
|
640
|
-
|
|
641
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
642
|
-
|
|
643
|
-
const rows = db.prepare(`SELECT * FROM events ${where} ORDER BY created_at DESC LIMIT ?`)
|
|
644
|
-
.all(...params, limit);
|
|
645
|
-
|
|
646
|
-
return rows.map(row => ({
|
|
647
|
-
...row,
|
|
648
|
-
payload: row.payload ? JSON.parse(row.payload) : null,
|
|
649
|
-
}));
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// ─── Lock Operations ────────────────────────────────────────────────────────
|
|
653
|
-
|
|
654
|
-
export function forceReleaseLock(filePath) {
|
|
655
|
-
const db = getDb();
|
|
656
|
-
const result = db.prepare('DELETE FROM file_locks WHERE file_path = ?').run(filePath);
|
|
657
|
-
return result.changes > 0;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// ─── Agent Operations (global — not project-scoped) ─────────────────────────
|
|
661
|
-
|
|
662
|
-
export function listAllAgents() {
|
|
663
|
-
const db = getDb();
|
|
664
|
-
return db.prepare('SELECT * FROM agents ORDER BY status ASC, connected_at DESC').all()
|
|
665
|
-
.map(a => ({
|
|
666
|
-
id: a.id,
|
|
667
|
-
name: a.name,
|
|
668
|
-
status: a.status,
|
|
669
|
-
currentTask: a.current_task_id,
|
|
670
|
-
specialties: a.specialties ? JSON.parse(a.specialties) : [],
|
|
671
|
-
color: a.color || '#8888a0',
|
|
672
|
-
maxConcurrent: a.max_concurrent || 1,
|
|
673
|
-
lastHeartbeat: a.last_heartbeat,
|
|
674
|
-
}));
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
export function registerAgent(id, name) {
|
|
678
|
-
const db = getDb();
|
|
679
|
-
db.prepare(`
|
|
680
|
-
INSERT INTO agents (id, name, status) VALUES (?, ?, 'idle')
|
|
681
|
-
ON CONFLICT(id) DO UPDATE SET status = 'idle', last_heartbeat = datetime('now')
|
|
682
|
-
`).run(id, name || id);
|
|
683
|
-
return db.prepare('SELECT * FROM agents WHERE id = ?').get(id);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
export function updateAgentProfile(id, fields) {
|
|
687
|
-
const db = getDb();
|
|
688
|
-
const sets = [];
|
|
689
|
-
const params = [];
|
|
690
|
-
|
|
691
|
-
if (fields.name !== undefined) { sets.push('name = ?'); params.push(fields.name); }
|
|
692
|
-
if (fields.specialties !== undefined) { sets.push('specialties = ?'); params.push(JSON.stringify(fields.specialties)); }
|
|
693
|
-
if (fields.color !== undefined) { sets.push('color = ?'); params.push(fields.color); }
|
|
694
|
-
if (fields.max_concurrent !== undefined) { sets.push('max_concurrent = ?'); params.push(fields.max_concurrent); }
|
|
695
|
-
|
|
696
|
-
if (sets.length === 0) return;
|
|
697
|
-
|
|
698
|
-
params.push(id);
|
|
699
|
-
db.prepare(`UPDATE agents SET ${sets.join(', ')} WHERE id = ?`).run(...params);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
export function removeAgent(id) {
|
|
703
|
-
const db = getDb();
|
|
704
|
-
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(id);
|
|
705
|
-
if (!agent) throw new Error(`Agent not found: ${id}`);
|
|
706
|
-
if (agent.status === 'working') throw new Error(`Cannot remove working agent: ${id}`);
|
|
707
|
-
db.prepare('DELETE FROM file_locks WHERE agent_id = ?').run(id);
|
|
708
|
-
db.prepare('DELETE FROM agents WHERE id = ?').run(id);
|
|
709
|
-
return true;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
export function dispatchTask(agentId, taskId) {
|
|
713
|
-
const db = getDb();
|
|
714
|
-
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
|
|
715
|
-
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
716
|
-
if (task.status !== 'ready') throw new Error(`Task ${taskId} is not ready (status: ${task.status})`);
|
|
717
|
-
|
|
718
|
-
db.prepare(`UPDATE tasks SET status = 'claimed', agent_id = ?, claimed_at = datetime('now') WHERE id = ?`)
|
|
719
|
-
.run(agentId, taskId);
|
|
720
|
-
db.prepare(`UPDATE agents SET status = 'working', current_task_id = ? WHERE id = ?`)
|
|
721
|
-
.run(taskId, agentId);
|
|
722
|
-
|
|
723
|
-
return db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// ─── Workspace Operations ───────────────────────────────────────────────────
|
|
727
|
-
|
|
728
|
-
export function listActiveWorkspaces() {
|
|
729
|
-
const db = getDb();
|
|
730
|
-
db.exec(`
|
|
731
|
-
CREATE TABLE IF NOT EXISTS workspaces (
|
|
732
|
-
id TEXT PRIMARY KEY,
|
|
733
|
-
task_id TEXT NOT NULL,
|
|
734
|
-
type TEXT DEFAULT 'worktree',
|
|
735
|
-
path TEXT NOT NULL,
|
|
736
|
-
branch TEXT NOT NULL,
|
|
737
|
-
status TEXT DEFAULT 'active',
|
|
738
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
739
|
-
merged_at TEXT
|
|
740
|
-
)
|
|
741
|
-
`);
|
|
742
|
-
|
|
743
|
-
const workspaces = db.prepare(`
|
|
744
|
-
SELECT w.*, t.title as task_title, t.status as task_status
|
|
745
|
-
FROM workspaces w
|
|
746
|
-
LEFT JOIN tasks t ON w.task_id = t.id
|
|
747
|
-
WHERE w.status = 'active'
|
|
748
|
-
ORDER BY w.created_at DESC
|
|
749
|
-
`).all();
|
|
750
|
-
|
|
751
|
-
return workspaces;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
// ─── Project Agent Operations (multi-agent architecture) ────────────────────
|
|
755
|
-
|
|
756
|
-
function normalizeProjectAgent(row) {
|
|
757
|
-
return {
|
|
758
|
-
id: row.id,
|
|
759
|
-
projectId: row.project_id,
|
|
760
|
-
name: row.name,
|
|
761
|
-
skills: row.skills ? JSON.parse(row.skills) : [],
|
|
762
|
-
status: row.status,
|
|
763
|
-
sessionId: row.session_id,
|
|
764
|
-
currentTask: row.current_task_id,
|
|
765
|
-
lastActiveAt: row.last_active_at,
|
|
766
|
-
idleSince: row.idle_since,
|
|
767
|
-
maxConcurrent: row.max_concurrent || 1,
|
|
768
|
-
icon: row.icon || '🤖',
|
|
769
|
-
color: row.color || '#8888a0',
|
|
770
|
-
createdAt: row.created_at,
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
export function listProjectAgents(projectId) {
|
|
775
|
-
const db = getDb();
|
|
776
|
-
let rows;
|
|
777
|
-
if (projectId) {
|
|
778
|
-
rows = db.prepare('SELECT * FROM project_agents WHERE project_id = ? ORDER BY created_at ASC').all(projectId);
|
|
779
|
-
} else {
|
|
780
|
-
rows = db.prepare('SELECT * FROM project_agents ORDER BY project_id ASC, created_at ASC').all();
|
|
781
|
-
}
|
|
782
|
-
return rows.map(normalizeProjectAgent);
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
export function getProjectAgent(id) {
|
|
786
|
-
const db = getDb();
|
|
787
|
-
const row = db.prepare('SELECT * FROM project_agents WHERE id = ?').get(id);
|
|
788
|
-
return row ? normalizeProjectAgent(row) : null;
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
export function createProjectAgent({ id, projectId, name, skills, icon, color }) {
|
|
792
|
-
const db = getDb();
|
|
793
|
-
db.prepare(`
|
|
794
|
-
INSERT INTO project_agents (id, project_id, name, skills, icon, color)
|
|
795
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
796
|
-
`).run(id, projectId, name, JSON.stringify(skills || []), icon || '🤖', color || '#8888a0');
|
|
797
|
-
return getProjectAgent(id);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
export function updateProjectAgent(id, fields) {
|
|
801
|
-
const db = getDb();
|
|
802
|
-
const sets = [];
|
|
803
|
-
const params = [];
|
|
804
|
-
|
|
805
|
-
if (fields.name !== undefined) { sets.push('name = ?'); params.push(fields.name); }
|
|
806
|
-
if (fields.skills !== undefined) { sets.push('skills = ?'); params.push(JSON.stringify(fields.skills)); }
|
|
807
|
-
if (fields.icon !== undefined) { sets.push('icon = ?'); params.push(fields.icon); }
|
|
808
|
-
if (fields.color !== undefined) { sets.push('color = ?'); params.push(fields.color); }
|
|
809
|
-
|
|
810
|
-
if (sets.length === 0) return getProjectAgent(id);
|
|
811
|
-
|
|
812
|
-
params.push(id);
|
|
813
|
-
db.prepare(`UPDATE project_agents SET ${sets.join(', ')} WHERE id = ?`).run(...params);
|
|
814
|
-
return getProjectAgent(id);
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
export function removeProjectAgent(id) {
|
|
818
|
-
const db = getDb();
|
|
819
|
-
const agent = db.prepare('SELECT * FROM project_agents WHERE id = ?').get(id);
|
|
820
|
-
if (!agent) throw new Error(`Agent not found: ${id}`);
|
|
821
|
-
if (agent.status === 'working') throw new Error(`Cannot remove working agent`);
|
|
822
|
-
db.prepare('DELETE FROM project_agents WHERE id = ?').run(id);
|
|
823
|
-
return true;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
export function attachProjectAgentSession(agentId, sessionId) {
|
|
827
|
-
const db = getDb();
|
|
828
|
-
db.prepare(`
|
|
829
|
-
UPDATE project_agents
|
|
830
|
-
SET session_id = ?, status = 'idle', last_active_at = datetime('now'), idle_since = datetime('now')
|
|
831
|
-
WHERE id = ?
|
|
832
|
-
`).run(sessionId, agentId);
|
|
833
|
-
return getProjectAgent(agentId);
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
export function detachProjectAgentSession(agentId) {
|
|
837
|
-
const db = getDb();
|
|
838
|
-
db.prepare(`
|
|
839
|
-
UPDATE project_agents
|
|
840
|
-
SET session_id = NULL, status = 'offline', current_task_id = NULL, idle_since = NULL
|
|
841
|
-
WHERE id = ?
|
|
842
|
-
`).run(agentId);
|
|
843
|
-
return getProjectAgent(agentId);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
// ─── Notes Operations (artifact & conversation tracking) ────────────────────
|
|
847
|
-
|
|
848
|
-
function generateNoteId() {
|
|
849
|
-
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
850
|
-
let id = 'note-';
|
|
851
|
-
for (let i = 0; i < 8; i++) id += chars[Math.floor(Math.random() * chars.length)];
|
|
852
|
-
return id;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
/**
|
|
856
|
-
* Create a new note.
|
|
857
|
-
* @param {Object} opts - { projectId, type, title, content, filePath, conversationId, taskId, metadata }
|
|
858
|
-
* @returns {Object} Created note
|
|
859
|
-
*/
|
|
860
|
-
export function createNote({ projectId, type, title, content, filePath, conversationId, taskId, metadata }) {
|
|
861
|
-
const db = getDb();
|
|
862
|
-
if (!projectId) throw new Error('project_id is required');
|
|
863
|
-
if (!type) throw new Error('type is required');
|
|
864
|
-
if (!title) throw new Error('title is required');
|
|
865
|
-
|
|
866
|
-
const validTypes = ['brief', 'plan', 'spec', 'conversation', 'decision', 'reference', 'brainstorm', 'analysis'];
|
|
867
|
-
if (!validTypes.includes(type)) {
|
|
868
|
-
throw new Error(`Invalid type: ${type}. Must be one of: ${validTypes.join(', ')}`);
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
const id = generateNoteId();
|
|
872
|
-
db.prepare(`
|
|
873
|
-
INSERT INTO notes (id, task_id, project_id, type, title, content, file_path, conversation_id, metadata)
|
|
874
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
875
|
-
`).run(
|
|
876
|
-
id, taskId || null, projectId, type, title,
|
|
877
|
-
content || null, filePath || null, conversationId || null,
|
|
878
|
-
metadata ? JSON.stringify(metadata) : '{}'
|
|
879
|
-
);
|
|
880
|
-
|
|
881
|
-
return db.prepare('SELECT * FROM notes WHERE id = ?').get(id);
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
/**
|
|
885
|
-
* Get a single note by ID.
|
|
886
|
-
*/
|
|
887
|
-
export function getNote(id) {
|
|
888
|
-
const db = getDb();
|
|
889
|
-
const row = db.prepare('SELECT * FROM notes WHERE id = ?').get(id);
|
|
890
|
-
return row ? normalizeNote(row) : null;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
/**
|
|
894
|
-
* List notes with optional filters.
|
|
895
|
-
* @param {Object} filter - { projectId, type, conversationId, taskId, limit }
|
|
896
|
-
*/
|
|
897
|
-
export function listNotes({ projectId, type, conversationId, taskId, limit = 50 } = {}) {
|
|
898
|
-
const db = getDb();
|
|
899
|
-
const conditions = [];
|
|
900
|
-
const params = [];
|
|
901
|
-
|
|
902
|
-
if (projectId) { conditions.push('project_id = ?'); params.push(projectId); }
|
|
903
|
-
if (type) { conditions.push('type = ?'); params.push(type); }
|
|
904
|
-
if (conversationId) { conditions.push('conversation_id = ?'); params.push(conversationId); }
|
|
905
|
-
if (taskId) { conditions.push('task_id = ?'); params.push(taskId); }
|
|
906
|
-
|
|
907
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
908
|
-
params.push(limit);
|
|
909
|
-
|
|
910
|
-
const rows = db.prepare(`SELECT * FROM notes ${where} ORDER BY created_at DESC LIMIT ?`)
|
|
911
|
-
.all(...params);
|
|
912
|
-
return rows.map(normalizeNote);
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
/**
|
|
916
|
-
* Update a note.
|
|
917
|
-
* @param {string} id - Note ID
|
|
918
|
-
* @param {Object} fields - { title, content, filePath, taskId, metadata }
|
|
919
|
-
*/
|
|
920
|
-
export function updateNote(id, fields) {
|
|
921
|
-
const db = getDb();
|
|
922
|
-
const allowed = ['title', 'content', 'file_path', 'task_id', 'metadata', 'conversation_id'];
|
|
923
|
-
const sets = ["updated_at = datetime('now')"];
|
|
924
|
-
const params = [];
|
|
925
|
-
|
|
926
|
-
for (const key of allowed) {
|
|
927
|
-
if (fields[key] !== undefined) {
|
|
928
|
-
if (key === 'metadata') {
|
|
929
|
-
sets.push(`${key} = ?`);
|
|
930
|
-
params.push(JSON.stringify(fields[key]));
|
|
931
|
-
} else {
|
|
932
|
-
sets.push(`${key} = ?`);
|
|
933
|
-
params.push(fields[key]);
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
if (sets.length === 1) return getNote(id); // only updated_at, skip
|
|
939
|
-
|
|
940
|
-
params.push(id);
|
|
941
|
-
db.prepare(`UPDATE notes SET ${sets.join(', ')} WHERE id = ?`).run(...params);
|
|
942
|
-
return getNote(id);
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
/**
|
|
946
|
-
* Delete a note.
|
|
947
|
-
*/
|
|
948
|
-
export function deleteNote(id) {
|
|
949
|
-
const db = getDb();
|
|
950
|
-
const note = db.prepare('SELECT id FROM notes WHERE id = ?').get(id);
|
|
951
|
-
if (!note) throw new Error(`Note not found: ${id}`);
|
|
952
|
-
db.prepare('DELETE FROM notes WHERE id = ?').run(id);
|
|
953
|
-
return true;
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
function normalizeNote(row) {
|
|
957
|
-
return {
|
|
958
|
-
...row,
|
|
959
|
-
metadata: row.metadata ? JSON.parse(row.metadata) : {},
|
|
960
|
-
};
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
// ─── Data Migration ─────────────────────────────────────────────────────────
|
|
964
|
-
|
|
965
|
-
/**
|
|
966
|
-
* Migrate data from old ~/.symphony/symphony.db to new location.
|
|
967
|
-
* Only runs once, on first boot with new path.
|
|
968
|
-
*/
|
|
969
|
-
export function migrateFromLegacy() {
|
|
970
|
-
const legacyPath = path.join(os.homedir(), '.symphony', 'symphony.db');
|
|
971
|
-
if (!fs.existsSync(legacyPath)) return { migrated: false, reason: 'no legacy DB' };
|
|
972
|
-
|
|
973
|
-
const db = getDb();
|
|
974
|
-
const existingTasks = db.prepare('SELECT COUNT(*) as count FROM tasks').get();
|
|
975
|
-
if (existingTasks.count > 0) return { migrated: false, reason: 'new DB already has data' };
|
|
976
|
-
|
|
977
|
-
try {
|
|
978
|
-
const legacyDb = new Database(legacyPath, { readonly: true });
|
|
979
|
-
|
|
980
|
-
// Migrate tasks
|
|
981
|
-
const tasks = legacyDb.prepare('SELECT * FROM tasks').all();
|
|
982
|
-
if (tasks.length > 0) {
|
|
983
|
-
const insert = db.prepare(`
|
|
984
|
-
INSERT OR IGNORE INTO tasks (id, title, description, status, priority, sort_order,
|
|
985
|
-
acceptance, phase, epic_id, agent_id, estimated_files, workspace_path, branch,
|
|
986
|
-
progress, created_at, claimed_at, completed_at, summary)
|
|
987
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
988
|
-
`);
|
|
989
|
-
const run = db.transaction(() => {
|
|
990
|
-
for (const t of tasks) {
|
|
991
|
-
insert.run(t.id, t.title, t.description, t.status, t.priority, t.sort_order || 0,
|
|
992
|
-
t.acceptance, t.phase, t.epic_id, t.agent_id, t.estimated_files, t.workspace_path,
|
|
993
|
-
t.branch, t.progress, t.created_at, t.claimed_at, t.completed_at, t.summary);
|
|
994
|
-
}
|
|
995
|
-
});
|
|
996
|
-
run();
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
// Migrate agents
|
|
1000
|
-
const agents = legacyDb.prepare('SELECT * FROM agents').all();
|
|
1001
|
-
if (agents.length > 0) {
|
|
1002
|
-
const insertAgent = db.prepare(`
|
|
1003
|
-
INSERT OR IGNORE INTO agents (id, name, status, current_task_id, specialties, color, max_concurrent, connected_at, last_heartbeat)
|
|
1004
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1005
|
-
`);
|
|
1006
|
-
const runAgents = db.transaction(() => {
|
|
1007
|
-
for (const a of agents) {
|
|
1008
|
-
insertAgent.run(a.id, a.name, a.status, a.current_task_id, a.specialties, a.color, a.max_concurrent || 1, a.connected_at, a.last_heartbeat);
|
|
1009
|
-
}
|
|
1010
|
-
});
|
|
1011
|
-
runAgents();
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// Migrate events
|
|
1015
|
-
const events = legacyDb.prepare('SELECT * FROM events').all();
|
|
1016
|
-
if (events.length > 0) {
|
|
1017
|
-
const insertEvent = db.prepare(`
|
|
1018
|
-
INSERT OR IGNORE INTO events (id, agent_id, task_id, event_type, payload, created_at)
|
|
1019
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
1020
|
-
`);
|
|
1021
|
-
const runEvents = db.transaction(() => {
|
|
1022
|
-
for (const e of events) {
|
|
1023
|
-
insertEvent.run(e.id, e.agent_id, e.task_id, e.event_type, e.payload, e.created_at);
|
|
1024
|
-
}
|
|
1025
|
-
});
|
|
1026
|
-
runEvents();
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
legacyDb.close();
|
|
1030
|
-
return { migrated: true, tasks: tasks.length, agents: agents.length, events: events.length };
|
|
1031
|
-
} catch (err) {
|
|
1032
|
-
return { migrated: false, reason: err.message };
|
|
1033
|
-
}
|
|
1034
|
-
}
|