@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.
Files changed (84) hide show
  1. package/README.md +51 -1
  2. package/bin/awk.js +2 -2
  3. package/core/GEMINI.md +45 -7
  4. package/package.json +8 -5
  5. package/skill-packs/neural-memory/skills/nm-memory-sync/SKILL.md +14 -1
  6. package/skills/ab-test-store-listing/SKILL.md +220 -0
  7. package/skills/android-aso/SKILL.md +197 -0
  8. package/skills/app-analytics/SKILL.md +210 -0
  9. package/skills/app-clips/SKILL.md +163 -0
  10. package/skills/app-icon-optimization/SKILL.md +170 -0
  11. package/skills/app-launch/SKILL.md +153 -0
  12. package/skills/app-marketing-context/SKILL.md +129 -0
  13. package/skills/app-store-featured/SKILL.md +213 -0
  14. package/skills/apple-search-ads/SKILL.md +205 -0
  15. package/skills/asc-metrics/SKILL.md +157 -0
  16. package/skills/aso-audit/SKILL.md +179 -0
  17. package/skills/competitor-analysis/SKILL.md +163 -0
  18. package/skills/competitor-tracking/SKILL.md +185 -0
  19. package/skills/crash-analytics/SKILL.md +181 -0
  20. package/skills/gitnexus-intelligence/SKILL.md +224 -0
  21. package/skills/in-app-events/SKILL.md +176 -0
  22. package/skills/keyword-research/SKILL.md +141 -0
  23. package/skills/localization/SKILL.md +165 -0
  24. package/skills/market-movers/SKILL.md +137 -0
  25. package/skills/market-pulse/SKILL.md +170 -0
  26. package/skills/metadata-optimization/SKILL.md +170 -0
  27. package/skills/monetization-strategy/SKILL.md +175 -0
  28. package/skills/onboarding-optimization/SKILL.md +194 -0
  29. package/skills/orchestrator/SKILL.md +306 -25
  30. package/skills/press-and-pr/SKILL.md +204 -0
  31. package/skills/rating-prompt-strategy/SKILL.md +184 -0
  32. package/skills/retention-optimization/SKILL.md +165 -0
  33. package/skills/review-management/SKILL.md +154 -0
  34. package/skills/screenshot-optimization/SKILL.md +167 -0
  35. package/skills/seasonal-aso/SKILL.md +141 -0
  36. package/skills/spec-gate/SKILL.md +312 -0
  37. package/skills/subscription-lifecycle/SKILL.md +206 -0
  38. package/skills/swiftui-pro/references/design.md +44 -0
  39. package/skills/symphony-enforcer/SKILL.md +92 -11
  40. package/skills/symphony-orchestrator/SKILL.md +9 -7
  41. package/skills/systematic-debugging/SKILL.md +32 -7
  42. package/skills/ua-campaign/SKILL.md +207 -0
  43. package/skills/verification-gate/SKILL.md +23 -2
  44. package/workflows/gitnexus.md +123 -0
  45. package/symphony/LICENSE +0 -21
  46. package/symphony/README.md +0 -178
  47. package/symphony/app/api/agents/route.js +0 -152
  48. package/symphony/app/api/events/route.js +0 -22
  49. package/symphony/app/api/knowledge/route.js +0 -253
  50. package/symphony/app/api/locks/route.js +0 -29
  51. package/symphony/app/api/notes/route.js +0 -125
  52. package/symphony/app/api/preflight/route.js +0 -23
  53. package/symphony/app/api/projects/route.js +0 -116
  54. package/symphony/app/api/roles/route.js +0 -134
  55. package/symphony/app/api/skills/route.js +0 -82
  56. package/symphony/app/api/status/route.js +0 -18
  57. package/symphony/app/api/tasks/route.js +0 -157
  58. package/symphony/app/api/workflows/route.js +0 -61
  59. package/symphony/app/api/workspaces/route.js +0 -15
  60. package/symphony/app/globals.css +0 -2605
  61. package/symphony/app/layout.js +0 -20
  62. package/symphony/app/page.js +0 -2122
  63. package/symphony/cli/index.js +0 -1060
  64. package/symphony/core/agent-manager.js +0 -357
  65. package/symphony/core/context-bus.js +0 -100
  66. package/symphony/core/db.js +0 -223
  67. package/symphony/core/file-lock-manager.js +0 -154
  68. package/symphony/core/merge-pipeline.js +0 -234
  69. package/symphony/core/orchestrator.js +0 -236
  70. package/symphony/core/task-manager.js +0 -335
  71. package/symphony/core/workspace-manager.js +0 -168
  72. package/symphony/jsconfig.json +0 -7
  73. package/symphony/lib/core.mjs +0 -1034
  74. package/symphony/mcp/index.js +0 -29
  75. package/symphony/mcp/server.js +0 -110
  76. package/symphony/mcp/tools/context.js +0 -80
  77. package/symphony/mcp/tools/locks.js +0 -99
  78. package/symphony/mcp/tools/status.js +0 -82
  79. package/symphony/mcp/tools/tasks.js +0 -216
  80. package/symphony/mcp/tools/workspace.js +0 -143
  81. package/symphony/next.config.mjs +0 -7
  82. package/symphony/package.json +0 -53
  83. package/symphony/scripts/postinstall.js +0 -49
  84. package/symphony/symphony.config.js +0 -41
@@ -1,357 +0,0 @@
1
- /**
2
- * Symphony Agent Manager
3
- * Project-scoped agent operations with skill-based task matching.
4
- *
5
- * Agents are configuration templates attached to projects.
6
- * Each Antigravity window attaches to one agent via session_id.
7
- * Agents have skills that determine which tasks they can be assigned.
8
- */
9
- const { getDb } = require('./db');
10
-
11
- const IDLE_THRESHOLD_MINUTES = 15;
12
-
13
- // ─── CRUD Operations ────────────────────────────────────────────────────────
14
-
15
- /**
16
- * Create a new project-scoped agent.
17
- * @param {string} id - Agent ID (e.g., 'giacngo-dev')
18
- * @param {string} projectId - Project ID
19
- * @param {string} name - Display name
20
- * @param {string[]} [skills] - Array of skill strings
21
- * @param {Object} [opts] - { icon, color, max_concurrent }
22
- * @returns {Object} Created agent
23
- */
24
- function createAgent(id, projectId, name, skills = [], opts = {}) {
25
- const db = getDb();
26
-
27
- // Validate project exists
28
- const project = db.prepare('SELECT id FROM projects WHERE id = ?').get(projectId);
29
- if (!project) {
30
- throw new Error(`Project not found: ${projectId}. Register it first.`);
31
- }
32
-
33
- // Check for duplicate
34
- const existing = db.prepare('SELECT id FROM project_agents WHERE id = ?').get(id);
35
- if (existing) {
36
- throw new Error(`Agent already exists: ${id}`);
37
- }
38
-
39
- db.prepare(`
40
- INSERT INTO project_agents (id, project_id, name, skills, icon, color, max_concurrent)
41
- VALUES (?, ?, ?, ?, ?, ?, ?)
42
- `).run(
43
- id,
44
- projectId,
45
- name,
46
- JSON.stringify(skills),
47
- opts.icon || '🤖',
48
- opts.color || '#8888a0',
49
- opts.max_concurrent || 1
50
- );
51
-
52
- return getAgent(id);
53
- }
54
-
55
- /**
56
- * Get a single agent by ID.
57
- * @param {string} id - Agent ID
58
- * @returns {Object|null}
59
- */
60
- function getAgent(id) {
61
- const db = getDb();
62
- const row = db.prepare('SELECT * FROM project_agents WHERE id = ?').get(id);
63
- return row ? normalizeAgent(row) : null;
64
- }
65
-
66
- /**
67
- * List agents with optional filters.
68
- * @param {Object} [filter] - { project, status }
69
- * @returns {Object[]}
70
- */
71
- function listAgents(filter = {}) {
72
- const db = getDb();
73
- const conditions = [];
74
- const params = [];
75
-
76
- if (filter.project) {
77
- conditions.push('project_id = ?');
78
- params.push(filter.project);
79
- }
80
- if (filter.status) {
81
- conditions.push('status = ?');
82
- params.push(filter.status);
83
- }
84
-
85
- const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
86
- const rows = db.prepare(`SELECT * FROM project_agents ${where} ORDER BY project_id ASC, created_at ASC`).all(...params);
87
- return rows.map(normalizeAgent);
88
- }
89
-
90
- /**
91
- * Update agent fields.
92
- * @param {string} id - Agent ID
93
- * @param {Object} fields - { name, skills, icon, color, max_concurrent }
94
- * @returns {Object} Updated agent
95
- */
96
- function updateAgent(id, fields) {
97
- const db = getDb();
98
- const allowed = ['name', 'icon', 'color', 'max_concurrent'];
99
- const sets = [];
100
- const params = [];
101
-
102
- for (const key of allowed) {
103
- if (fields[key] !== undefined) {
104
- sets.push(`${key} = ?`);
105
- params.push(fields[key]);
106
- }
107
- }
108
-
109
- // Handle skills separately (needs JSON)
110
- if (fields.skills !== undefined) {
111
- sets.push('skills = ?');
112
- params.push(JSON.stringify(fields.skills));
113
- }
114
-
115
- if (sets.length === 0) return getAgent(id);
116
-
117
- params.push(id);
118
- db.prepare(`UPDATE project_agents SET ${sets.join(', ')} WHERE id = ?`).run(...params);
119
- return getAgent(id);
120
- }
121
-
122
- /**
123
- * Remove an agent (must be offline).
124
- * @param {string} id - Agent ID
125
- * @returns {boolean}
126
- */
127
- function removeAgent(id) {
128
- const db = getDb();
129
- const agent = db.prepare('SELECT * FROM project_agents WHERE id = ?').get(id);
130
- if (!agent) throw new Error(`Agent not found: ${id}`);
131
- if (agent.status === 'working') {
132
- throw new Error(`Cannot remove working agent: ${id}. Detach first.`);
133
- }
134
- db.prepare('DELETE FROM project_agents WHERE id = ?').run(id);
135
- return true;
136
- }
137
-
138
- // ─── Session Management ─────────────────────────────────────────────────────
139
-
140
- /**
141
- * Attach an Antigravity window (session) to an agent.
142
- * @param {string} agentId - Agent ID
143
- * @param {string} sessionId - Conversation/session ID
144
- * @returns {Object} Updated agent
145
- */
146
- function attachSession(agentId, sessionId) {
147
- const db = getDb();
148
- const agent = db.prepare('SELECT * FROM project_agents WHERE id = ?').get(agentId);
149
- if (!agent) throw new Error(`Agent not found: ${agentId}`);
150
-
151
- if (agent.session_id && agent.session_id !== sessionId && agent.status !== 'offline') {
152
- throw new Error(`Agent ${agentId} already attached to session ${agent.session_id}`);
153
- }
154
-
155
- db.prepare(`
156
- UPDATE project_agents
157
- SET session_id = ?, status = 'idle', last_active_at = datetime('now'), idle_since = datetime('now')
158
- WHERE id = ?
159
- `).run(sessionId, agentId);
160
-
161
- return getAgent(agentId);
162
- }
163
-
164
- /**
165
- * Detach a session from an agent.
166
- * Releases any claimed tasks.
167
- * @param {string} agentId - Agent ID
168
- * @returns {Object} Updated agent
169
- */
170
- function detachSession(agentId) {
171
- const db = getDb();
172
- const agent = db.prepare('SELECT * FROM project_agents WHERE id = ?').get(agentId);
173
- if (!agent) throw new Error(`Agent not found: ${agentId}`);
174
-
175
- // Reset any in-progress tasks back to ready
176
- if (agent.current_task_id) {
177
- db.prepare(`
178
- UPDATE tasks SET status = 'ready', agent_id = NULL
179
- WHERE agent_id = ? AND status IN ('claimed', 'in_progress')
180
- `).run(agentId);
181
- }
182
-
183
- db.prepare(`
184
- UPDATE project_agents
185
- SET session_id = NULL, status = 'offline', current_task_id = NULL, idle_since = NULL
186
- WHERE id = ?
187
- `).run(agentId);
188
-
189
- return getAgent(agentId);
190
- }
191
-
192
- /**
193
- * Update agent's last activity timestamp (call on every meaningful action).
194
- * @param {string} agentId - Agent ID
195
- */
196
- function touchActivity(agentId) {
197
- const db = getDb();
198
- db.prepare(`
199
- UPDATE project_agents
200
- SET last_active_at = datetime('now'), idle_since = NULL
201
- WHERE id = ?
202
- `).run(agentId);
203
- }
204
-
205
- /**
206
- * Mark agent as working on a task.
207
- * @param {string} agentId - Agent ID
208
- * @param {string} taskId - Task ID
209
- * @returns {Object} Updated agent
210
- */
211
- function startWork(agentId, taskId) {
212
- const db = getDb();
213
- db.prepare(`
214
- UPDATE project_agents
215
- SET status = 'working', current_task_id = ?, last_active_at = datetime('now'), idle_since = NULL
216
- WHERE id = ?
217
- `).run(taskId, agentId);
218
- return getAgent(agentId);
219
- }
220
-
221
- /**
222
- * Mark agent as idle (after task completion).
223
- * @param {string} agentId - Agent ID
224
- * @returns {Object} Updated agent
225
- */
226
- function markIdle(agentId) {
227
- const db = getDb();
228
- db.prepare(`
229
- UPDATE project_agents
230
- SET status = 'idle', current_task_id = NULL, idle_since = datetime('now')
231
- WHERE id = ?
232
- `).run(agentId);
233
- return getAgent(agentId);
234
- }
235
-
236
- // ─── Skill Matching ─────────────────────────────────────────────────────────
237
-
238
- /**
239
- * Find agents matching task skill requirements.
240
- * @param {string[]} taskSkills - Required skills for the task
241
- * @param {string} [projectId] - Limit to one project
242
- * @returns {Object[]} Matching agents sorted by relevance
243
- */
244
- function findMatchingAgents(taskSkills, projectId) {
245
- const agents = listAgents(projectId ? { project: projectId } : {});
246
-
247
- if (!taskSkills || taskSkills.length === 0) {
248
- // No skill requirements — all agents match
249
- return agents.filter(a => a.status !== 'offline');
250
- }
251
-
252
- return agents
253
- .filter(a => a.status !== 'offline')
254
- .filter(a => {
255
- // Agent must have at least 1 matching skill
256
- return taskSkills.some(s => a.skills.includes(s));
257
- })
258
- .map(a => {
259
- const matchCount = taskSkills.filter(s => a.skills.includes(s)).length;
260
- return { ...a, matchCount, matchPercent: Math.round((matchCount / taskSkills.length) * 100) };
261
- })
262
- .sort((a, b) => {
263
- // Prefer idle over working
264
- if (a.status === 'idle' && b.status !== 'idle') return -1;
265
- if (b.status === 'idle' && a.status !== 'idle') return 1;
266
- // Then by match count
267
- return b.matchCount - a.matchCount;
268
- });
269
- }
270
-
271
- /**
272
- * Find agents that have been idle beyond the threshold.
273
- * @param {number} [thresholdMinutes] - Minutes of inactivity
274
- * @returns {Object[]} Idle agents
275
- */
276
- function findIdleAgents(thresholdMinutes = IDLE_THRESHOLD_MINUTES) {
277
- const db = getDb();
278
- const rows = db.prepare(`
279
- SELECT * FROM project_agents
280
- WHERE status IN ('idle', 'working')
281
- AND idle_since IS NOT NULL
282
- AND datetime(idle_since, '+' || ? || ' minutes') < datetime('now')
283
- `).all(thresholdMinutes);
284
- return rows.map(normalizeAgent);
285
- }
286
-
287
- /**
288
- * Suggest reassignment for idle agents (cross-project).
289
- * Returns agents with matching ready tasks from any project.
290
- * @returns {Object[]} Array of { agent, suggestedTasks }
291
- */
292
- function suggestReassignment() {
293
- const db = getDb();
294
- const idleAgents = findIdleAgents();
295
- const suggestions = [];
296
-
297
- for (const agent of idleAgents) {
298
- // Find ready tasks matching this agent's skills (any project)
299
- const readyTasks = db.prepare(`
300
- SELECT * FROM tasks WHERE status = 'ready' ORDER BY priority ASC, sort_order ASC LIMIT 10
301
- `).all();
302
-
303
- const matchingTasks = readyTasks.filter(t => {
304
- const reqSkills = t.required_skills ? JSON.parse(t.required_skills) : [];
305
- if (reqSkills.length === 0) return true; // No skill req → matches all
306
- return reqSkills.some(s => agent.skills.includes(s));
307
- });
308
-
309
- if (matchingTasks.length > 0) {
310
- suggestions.push({
311
- agent,
312
- suggestedTasks: matchingTasks.slice(0, 3),
313
- });
314
- }
315
- }
316
-
317
- return suggestions;
318
- }
319
-
320
- // ─── Helpers ────────────────────────────────────────────────────────────────
321
-
322
- /**
323
- * Normalize a raw DB row into a clean agent object.
324
- */
325
- function normalizeAgent(row) {
326
- return {
327
- id: row.id,
328
- projectId: row.project_id,
329
- name: row.name,
330
- skills: row.skills ? JSON.parse(row.skills) : [],
331
- status: row.status,
332
- sessionId: row.session_id,
333
- currentTask: row.current_task_id,
334
- lastActiveAt: row.last_active_at,
335
- idleSince: row.idle_since,
336
- maxConcurrent: row.max_concurrent || 1,
337
- icon: row.icon || '🤖',
338
- color: row.color || '#8888a0',
339
- createdAt: row.created_at,
340
- };
341
- }
342
-
343
- module.exports = {
344
- createAgent,
345
- getAgent,
346
- listAgents,
347
- updateAgent,
348
- removeAgent,
349
- attachSession,
350
- detachSession,
351
- touchActivity,
352
- startWork,
353
- markIdle,
354
- findMatchingAgents,
355
- findIdleAgents,
356
- suggestReassignment,
357
- };
@@ -1,100 +0,0 @@
1
- /**
2
- * Symphony Context Bus
3
- * Event pub/sub for inter-agent communication.
4
- * Agents broadcast changes (schema updates, API changes, etc.)
5
- * so other agents can adapt their work.
6
- */
7
- const { getDb } = require('./db');
8
-
9
- /**
10
- * Publish an event to the context bus.
11
- * @param {string} agentId - Publishing agent
12
- * @param {string} eventType - Event type (file_modified, api_changed, schema_updated, etc.)
13
- * @param {Object} payload - Event payload { files, description, impact }
14
- * @param {string} [taskId] - Related task
15
- */
16
- function publish(agentId, eventType, payload, taskId = null) {
17
- const db = getDb();
18
- db.prepare(`
19
- INSERT INTO events (agent_id, task_id, event_type, payload)
20
- VALUES (?, ?, ?, ?)
21
- `).run(agentId, taskId, eventType, JSON.stringify(payload));
22
- }
23
-
24
- /**
25
- * Query events with optional filters.
26
- * @param {Object} [filter] - { since, eventType, agentId, limit }
27
- * @returns {Object[]}
28
- */
29
- function query(filter = {}) {
30
- const db = getDb();
31
- const conditions = [];
32
- const params = [];
33
-
34
- if (filter.since) {
35
- conditions.push('created_at > ?');
36
- params.push(filter.since);
37
- }
38
- if (filter.eventType) {
39
- conditions.push('event_type = ?');
40
- params.push(filter.eventType);
41
- }
42
- if (filter.agentId) {
43
- conditions.push('agent_id = ?');
44
- params.push(filter.agentId);
45
- }
46
-
47
- const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
48
- const limit = filter.limit ? `LIMIT ${parseInt(filter.limit)}` : 'LIMIT 50';
49
-
50
- const rows = db.prepare(`
51
- SELECT * FROM events ${where} ORDER BY created_at DESC ${limit}
52
- `).all(...params);
53
-
54
- return rows.map(row => ({
55
- ...row,
56
- payload: row.payload ? JSON.parse(row.payload) : null,
57
- }));
58
- }
59
-
60
- /**
61
- * Get most recent events.
62
- * @param {number} [limit=10]
63
- * @returns {Object[]}
64
- */
65
- function getRecent(limit = 10) {
66
- return query({ limit });
67
- }
68
-
69
- /**
70
- * Get events related to specific files.
71
- * @param {string[]} files - File paths to check
72
- * @param {number} [limit=20]
73
- * @returns {Object[]}
74
- */
75
- function getFileEvents(files, limit = 20) {
76
- const db = getDb();
77
- const rows = db.prepare(`
78
- SELECT * FROM events
79
- WHERE event_type IN ('file_modified', 'schema_updated', 'api_changed')
80
- ORDER BY created_at DESC
81
- LIMIT ?
82
- `).all(limit);
83
-
84
- return rows
85
- .map(row => ({
86
- ...row,
87
- payload: row.payload ? JSON.parse(row.payload) : null,
88
- }))
89
- .filter(event => {
90
- if (!event.payload?.files) return false;
91
- return event.payload.files.some(f => files.includes(f));
92
- });
93
- }
94
-
95
- module.exports = {
96
- publish,
97
- query,
98
- getRecent,
99
- getFileEvents,
100
- };
@@ -1,223 +0,0 @@
1
- /**
2
- * Symphony Database Module
3
- * SQLite database wrapper with auto-migration.
4
- */
5
- const Database = require('better-sqlite3');
6
- const path = require('path');
7
- const fs = require('fs');
8
-
9
- const SCHEMA = `
10
- -- Tasks table (source of truth for all work items)
11
- CREATE TABLE IF NOT EXISTS tasks (
12
- id TEXT PRIMARY KEY,
13
- title TEXT NOT NULL,
14
- description TEXT,
15
- status TEXT NOT NULL DEFAULT 'ready',
16
- priority INTEGER DEFAULT 2,
17
- sort_order INTEGER DEFAULT 0,
18
- acceptance TEXT,
19
- phase TEXT,
20
- epic_id TEXT,
21
- agent_id TEXT,
22
- estimated_files TEXT,
23
- workspace_path TEXT,
24
- branch TEXT,
25
- progress INTEGER DEFAULT 0,
26
- created_at DATETIME DEFAULT (datetime('now')),
27
- claimed_at DATETIME,
28
- completed_at DATETIME,
29
- summary TEXT
30
- );
31
-
32
- -- File locks (prevent 2 agents editing same file)
33
- CREATE TABLE IF NOT EXISTS file_locks (
34
- file_path TEXT PRIMARY KEY,
35
- agent_id TEXT NOT NULL,
36
- task_id TEXT NOT NULL,
37
- acquired_at DATETIME DEFAULT (datetime('now'))
38
- );
39
-
40
- -- Agent registry (connected IDE sessions)
41
- CREATE TABLE IF NOT EXISTS agents (
42
- id TEXT PRIMARY KEY,
43
- name TEXT,
44
- status TEXT DEFAULT 'idle',
45
- current_task_id TEXT,
46
- specialties TEXT DEFAULT '[]',
47
- color TEXT DEFAULT '#8888a0',
48
- max_concurrent INTEGER DEFAULT 1,
49
- connected_at DATETIME DEFAULT (datetime('now')),
50
- last_heartbeat DATETIME DEFAULT (datetime('now')),
51
- workspace_path TEXT
52
- );
53
-
54
- -- Context Bus events (inter-agent communication)
55
- CREATE TABLE IF NOT EXISTS events (
56
- id INTEGER PRIMARY KEY AUTOINCREMENT,
57
- agent_id TEXT NOT NULL,
58
- task_id TEXT,
59
- event_type TEXT NOT NULL,
60
- payload TEXT,
61
- created_at DATETIME DEFAULT (datetime('now'))
62
- );
63
-
64
- -- Workspaces (git worktree tracking)
65
- CREATE TABLE IF NOT EXISTS workspaces (
66
- id TEXT PRIMARY KEY,
67
- task_id TEXT NOT NULL,
68
- type TEXT DEFAULT 'worktree',
69
- path TEXT NOT NULL,
70
- branch TEXT NOT NULL,
71
- status TEXT DEFAULT 'active',
72
- created_at DATETIME DEFAULT (datetime('now')),
73
- merged_at DATETIME
74
- );
75
-
76
- -- Indexes for performance
77
- CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
78
- CREATE INDEX IF NOT EXISTS idx_tasks_agent ON tasks(agent_id);
79
- CREATE INDEX IF NOT EXISTS idx_locks_agent ON file_locks(agent_id);
80
- CREATE INDEX IF NOT EXISTS idx_events_time ON events(created_at);
81
- CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
82
-
83
- -- Project-scoped agents (multi-agent architecture)
84
- CREATE TABLE IF NOT EXISTS project_agents (
85
- id TEXT PRIMARY KEY,
86
- project_id TEXT NOT NULL,
87
- name TEXT NOT NULL,
88
- skills TEXT DEFAULT '[]',
89
- status TEXT DEFAULT 'offline',
90
- session_id TEXT,
91
- current_task_id TEXT,
92
- last_active_at TEXT,
93
- idle_since TEXT,
94
- max_concurrent INTEGER DEFAULT 1,
95
- icon TEXT DEFAULT '🤖',
96
- color TEXT DEFAULT '#8888a0',
97
- created_at DATETIME DEFAULT (datetime('now'))
98
- );
99
-
100
- CREATE INDEX IF NOT EXISTS idx_project_agents_project ON project_agents(project_id);
101
- CREATE INDEX IF NOT EXISTS idx_project_agents_status ON project_agents(status);
102
- CREATE INDEX IF NOT EXISTS idx_project_agents_session ON project_agents(session_id);
103
- `;
104
-
105
- let _db = null;
106
-
107
- /**
108
- * Get or create the database connection (singleton).
109
- * @param {string} [dbPath] - Optional custom path for the database file.
110
- * @returns {Database} better-sqlite3 instance
111
- */
112
- function getDb(dbPath) {
113
- if (_db) return _db;
114
-
115
- const resolvedPath = dbPath || resolveDbPath();
116
- const dir = path.dirname(resolvedPath);
117
- if (!fs.existsSync(dir)) {
118
- fs.mkdirSync(dir, { recursive: true });
119
- }
120
-
121
- _db = new Database(resolvedPath);
122
- _db.pragma('journal_mode = WAL');
123
- _db.pragma('foreign_keys = ON');
124
-
125
- // Run schema migrations
126
- _db.exec(SCHEMA);
127
-
128
- // Run incremental migrations
129
- runMigrations(_db);
130
-
131
- return _db;
132
- }
133
-
134
- /**
135
- * Run incremental schema migrations for existing databases.
136
- */
137
- function runMigrations(db) {
138
- const cols = db.pragma('table_info(tasks)').map(c => c.name);
139
- if (!cols.includes('sort_order')) {
140
- db.exec('ALTER TABLE tasks ADD COLUMN sort_order INTEGER DEFAULT 0');
141
- }
142
- if (!cols.includes('project_id')) {
143
- db.exec('ALTER TABLE tasks ADD COLUMN project_id TEXT');
144
- }
145
- const agentCols = db.pragma('table_info(agents)').map(c => c.name);
146
- if (!agentCols.includes('specialties')) {
147
- db.exec('ALTER TABLE agents ADD COLUMN specialties TEXT DEFAULT "[]"');
148
- }
149
- if (!agentCols.includes('color')) {
150
- db.exec('ALTER TABLE agents ADD COLUMN color TEXT DEFAULT "#8888a0"');
151
- }
152
- if (!agentCols.includes('max_concurrent')) {
153
- db.exec('ALTER TABLE agents ADD COLUMN max_concurrent INTEGER DEFAULT 1');
154
- }
155
-
156
- // Add required_skills to tasks
157
- const taskCols2 = db.pragma('table_info(tasks)').map(c => c.name);
158
- if (!taskCols2.includes('required_skills')) {
159
- db.exec("ALTER TABLE tasks ADD COLUMN required_skills TEXT DEFAULT '[]'");
160
- }
161
-
162
- // Add conversation_id to tasks (Hybrid approach C)
163
- if (!taskCols2.includes('conversation_id')) {
164
- db.exec('ALTER TABLE tasks ADD COLUMN conversation_id TEXT');
165
- }
166
-
167
- // Ensure notes table exists
168
- const notesTbl = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='notes'").get();
169
- if (!notesTbl) {
170
- db.exec(`
171
- CREATE TABLE notes (
172
- id TEXT PRIMARY KEY,
173
- task_id TEXT,
174
- project_id TEXT NOT NULL,
175
- type TEXT NOT NULL,
176
- title TEXT NOT NULL,
177
- content TEXT,
178
- file_path TEXT,
179
- conversation_id TEXT,
180
- metadata TEXT DEFAULT '{}',
181
- created_at DATETIME DEFAULT (datetime('now')),
182
- updated_at DATETIME DEFAULT (datetime('now'))
183
- );
184
- CREATE INDEX idx_notes_task ON notes(task_id);
185
- CREATE INDEX idx_notes_project ON notes(project_id);
186
- CREATE INDEX idx_notes_type ON notes(type);
187
- CREATE INDEX idx_notes_conversation ON notes(conversation_id);
188
- `);
189
- }
190
- }
191
-
192
- /**
193
- * Resolve database file path.
194
- * CENTRALIZED: Always use ~/.gemini/antigravity/symphony/symphony.db
195
- * This ensures CLI and API (core.mjs) share the same database.
196
- */
197
- function resolveDbPath() {
198
- const home = process.env.HOME || process.env.USERPROFILE;
199
- const symphonyDir = path.join(home, '.gemini', 'antigravity', 'symphony');
200
- if (!fs.existsSync(symphonyDir)) {
201
- fs.mkdirSync(symphonyDir, { recursive: true });
202
- }
203
- return path.join(symphonyDir, 'symphony.db');
204
- }
205
-
206
- /**
207
- * Close the database connection.
208
- */
209
- function close() {
210
- if (_db) {
211
- _db.close();
212
- _db = null;
213
- }
214
- }
215
-
216
- /**
217
- * Reset the singleton (for testing).
218
- */
219
- function reset() {
220
- close();
221
- }
222
-
223
- module.exports = { getDb, close, reset };