@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,154 +0,0 @@
1
- /**
2
- * Symphony File Lock Manager
3
- * Pessimistic file locking to prevent 2 agents editing the same file.
4
- */
5
- const { getDb } = require('./db');
6
-
7
- /**
8
- * Acquire locks on files for an agent.
9
- * Atomic: all-or-nothing — if any file is locked by another agent, none are acquired.
10
- *
11
- * @param {string} agentId - Agent requesting locks
12
- * @param {string} taskId - Task the agent is working on
13
- * @param {string[]} files - File paths to lock
14
- * @returns {{ success: boolean, conflicts?: Object[] }}
15
- */
16
- function acquireLocks(agentId, taskId, files) {
17
- const db = getDb();
18
-
19
- return db.transaction(() => {
20
- // Check for conflicts
21
- const conflicts = [];
22
- const checkStmt = db.prepare('SELECT * FROM file_locks WHERE file_path = ?');
23
-
24
- for (const file of files) {
25
- const existing = checkStmt.get(file);
26
- if (existing && existing.agent_id !== agentId) {
27
- conflicts.push({
28
- file,
29
- agent: existing.agent_id,
30
- task: existing.task_id,
31
- since: existing.acquired_at,
32
- });
33
- }
34
- }
35
-
36
- if (conflicts.length > 0) {
37
- return { success: false, conflicts };
38
- }
39
-
40
- // Acquire all locks (upsert)
41
- const upsertStmt = db.prepare(`
42
- INSERT INTO file_locks (file_path, agent_id, task_id)
43
- VALUES (?, ?, ?)
44
- ON CONFLICT(file_path) DO UPDATE SET agent_id = ?, task_id = ?, acquired_at = datetime('now')
45
- `);
46
-
47
- for (const file of files) {
48
- upsertStmt.run(file, agentId, taskId, agentId, taskId);
49
- }
50
-
51
- return { success: true };
52
- })();
53
- }
54
-
55
- /**
56
- * Release all locks held by an agent.
57
- * @param {string} agentId - Agent ID
58
- * @returns {number} Number of locks released
59
- */
60
- function releaseLocks(agentId) {
61
- const db = getDb();
62
- const result = db.prepare('DELETE FROM file_locks WHERE agent_id = ?').run(agentId);
63
- return result.changes;
64
- }
65
-
66
- /**
67
- * Release specific file locks for an agent.
68
- * @param {string} agentId - Agent ID
69
- * @param {string[]} files - File paths to release
70
- * @returns {number} Number of locks released
71
- */
72
- function releaseSpecificLocks(agentId, files) {
73
- const db = getDb();
74
- let released = 0;
75
- const stmt = db.prepare('DELETE FROM file_locks WHERE file_path = ? AND agent_id = ?');
76
- for (const file of files) {
77
- const result = stmt.run(file, agentId);
78
- released += result.changes;
79
- }
80
- return released;
81
- }
82
-
83
- /**
84
- * Check if files are available (not locked by another agent).
85
- * @param {string[]} files - File paths to check
86
- * @returns {{ available: string[], locked: Object[] }}
87
- */
88
- function checkFiles(files) {
89
- const db = getDb();
90
- const stmt = db.prepare('SELECT * FROM file_locks WHERE file_path = ?');
91
-
92
- const available = [];
93
- const locked = [];
94
-
95
- for (const file of files) {
96
- const lock = stmt.get(file);
97
- if (lock) {
98
- locked.push({
99
- file,
100
- agent: lock.agent_id,
101
- task: lock.task_id,
102
- since: lock.acquired_at,
103
- });
104
- } else {
105
- available.push(file);
106
- }
107
- }
108
-
109
- return { available, locked };
110
- }
111
-
112
- /**
113
- * Get all currently locked files.
114
- * @returns {Object[]}
115
- */
116
- function getAllLocks() {
117
- const db = getDb();
118
- return db.prepare('SELECT * FROM file_locks ORDER BY acquired_at DESC').all();
119
- }
120
-
121
- /**
122
- * Force-release a specific file lock (admin operation).
123
- * @param {string} filePath - File path to unlock
124
- * @returns {boolean} Whether a lock was released
125
- */
126
- function forceRelease(filePath) {
127
- const db = getDb();
128
- const result = db.prepare('DELETE FROM file_locks WHERE file_path = ?').run(filePath);
129
- return result.changes > 0;
130
- }
131
-
132
- /**
133
- * Auto-release stale locks older than the given seconds.
134
- * @param {number} maxAge - Max age in seconds (default: 3600)
135
- * @returns {number} Number of locks released
136
- */
137
- function autoReleaseStale(maxAge = 3600) {
138
- const db = getDb();
139
- const result = db.prepare(`
140
- DELETE FROM file_locks
141
- WHERE acquired_at < datetime('now', '-' || ? || ' seconds')
142
- `).run(maxAge);
143
- return result.changes;
144
- }
145
-
146
- module.exports = {
147
- acquireLocks,
148
- releaseLocks,
149
- releaseSpecificLocks,
150
- checkFiles,
151
- getAllLocks,
152
- forceRelease,
153
- autoReleaseStale,
154
- };
@@ -1,234 +0,0 @@
1
- /**
2
- * Symphony Merge Pipeline
3
- * Auto-rebase, merge, and conflict detection for completed tasks.
4
- */
5
- const { execSync } = require('child_process');
6
- const { getDb } = require('./db');
7
- const workspaceManager = require('./workspace-manager');
8
-
9
- /**
10
- * Get the main branch name (main or master).
11
- * @param {string} repoPath
12
- * @returns {string}
13
- */
14
- function getMainBranch(repoPath) {
15
- try {
16
- const branches = execSync('git branch -l main master', {
17
- cwd: repoPath,
18
- encoding: 'utf8',
19
- stdio: ['pipe', 'pipe', 'pipe'],
20
- }).trim();
21
- if (branches.includes('main')) return 'main';
22
- if (branches.includes('master')) return 'master';
23
- } catch (_) { /* ignore */ }
24
- return 'main'; // default
25
- }
26
-
27
- /**
28
- * Get diff stats between task branch and main.
29
- * @param {string} taskId
30
- * @param {string} repoPath
31
- * @returns {{ files: number, insertions: number, deletions: number, summary: string }}
32
- */
33
- function getDiff(taskId, repoPath) {
34
- const ws = workspaceManager.getWorkspace(taskId);
35
- if (!ws) return { files: 0, insertions: 0, deletions: 0, summary: 'No workspace found' };
36
-
37
- const mainBranch = getMainBranch(repoPath);
38
-
39
- try {
40
- const stat = execSync(`git diff ${mainBranch}...${ws.branch} --stat`, {
41
- cwd: repoPath,
42
- encoding: 'utf8',
43
- stdio: ['pipe', 'pipe', 'pipe'],
44
- }).trim();
45
-
46
- const shortstat = execSync(`git diff ${mainBranch}...${ws.branch} --shortstat`, {
47
- cwd: repoPath,
48
- encoding: 'utf8',
49
- stdio: ['pipe', 'pipe', 'pipe'],
50
- }).trim();
51
-
52
- // Parse shortstat: "3 files changed, 10 insertions(+), 2 deletions(-)"
53
- const filesMatch = shortstat.match(/(\d+) files? changed/);
54
- const insertMatch = shortstat.match(/(\d+) insertions?\(\+\)/);
55
- const deleteMatch = shortstat.match(/(\d+) deletions?\(-\)/);
56
-
57
- return {
58
- files: filesMatch ? parseInt(filesMatch[1]) : 0,
59
- insertions: insertMatch ? parseInt(insertMatch[1]) : 0,
60
- deletions: deleteMatch ? parseInt(deleteMatch[1]) : 0,
61
- summary: stat,
62
- };
63
- } catch (e) {
64
- return { files: 0, insertions: 0, deletions: 0, summary: `Error: ${e.message}` };
65
- }
66
- }
67
-
68
- /**
69
- * Check for merge conflicts without actually merging.
70
- * @param {string} taskId
71
- * @param {string} repoPath
72
- * @returns {{ hasConflicts: boolean, conflictingFiles: string[] }}
73
- */
74
- function checkConflicts(taskId, repoPath) {
75
- const ws = workspaceManager.getWorkspace(taskId);
76
- if (!ws) return { hasConflicts: false, conflictingFiles: [] };
77
-
78
- const mainBranch = getMainBranch(repoPath);
79
-
80
- try {
81
- // Try a dry-run merge
82
- execSync(`git merge-tree $(git merge-base ${mainBranch} ${ws.branch}) ${mainBranch} ${ws.branch}`, {
83
- cwd: repoPath,
84
- encoding: 'utf8',
85
- stdio: ['pipe', 'pipe', 'pipe'],
86
- });
87
- return { hasConflicts: false, conflictingFiles: [] };
88
- } catch (e) {
89
- // Parse conflict markers
90
- const output = e.stdout || '';
91
- const conflicting = [];
92
- const regex = /changed in both[\s\S]*?'([^']+)'/g;
93
- let match;
94
- while ((match = regex.exec(output))) {
95
- conflicting.push(match[1]);
96
- }
97
- return { hasConflicts: conflicting.length > 0, conflictingFiles: conflicting };
98
- }
99
- }
100
-
101
- /**
102
- * Auto-merge a completed task branch into main.
103
- * Steps: fetch → rebase → fast-forward merge → cleanup
104
- *
105
- * @param {string} taskId
106
- * @param {string} repoPath
107
- * @returns {{ status: 'merged'|'conflict'|'error', message: string, conflictingFiles?: string[] }}
108
- */
109
- function autoMerge(taskId, repoPath) {
110
- const ws = workspaceManager.getWorkspace(taskId);
111
- if (!ws) {
112
- return { status: 'error', message: 'No active workspace found for this task' };
113
- }
114
-
115
- const mainBranch = getMainBranch(repoPath);
116
-
117
- try {
118
- // Step 1: Fetch latest from remote (if available)
119
- try {
120
- execSync(`git fetch origin ${mainBranch}`, {
121
- cwd: repoPath,
122
- stdio: 'pipe',
123
- });
124
- } catch (_) {
125
- // No remote or fetch failed — continue with local
126
- }
127
-
128
- // Step 2: Rebase task branch onto main
129
- try {
130
- execSync(`git rebase ${mainBranch}`, {
131
- cwd: ws.path,
132
- stdio: 'pipe',
133
- });
134
- } catch (rebaseError) {
135
- // Rebase failed — abort and report conflicts
136
- try {
137
- execSync('git rebase --abort', { cwd: ws.path, stdio: 'pipe' });
138
- } catch (_) { /* ignore */ }
139
-
140
- const conflicts = checkConflicts(taskId, repoPath);
141
- return {
142
- status: 'conflict',
143
- message: `Rebase failed: conflicts detected in ${conflicts.conflictingFiles.length} file(s)`,
144
- conflictingFiles: conflicts.conflictingFiles,
145
- };
146
- }
147
-
148
- // Step 3: Merge into main (fast-forward)
149
- try {
150
- execSync(`git checkout ${mainBranch}`, {
151
- cwd: repoPath,
152
- stdio: 'pipe',
153
- });
154
- execSync(`git merge --ff-only ${ws.branch}`, {
155
- cwd: repoPath,
156
- stdio: 'pipe',
157
- });
158
- } catch (mergeError) {
159
- // FF merge failed — try regular merge
160
- try {
161
- execSync(`git merge ${ws.branch} --no-edit`, {
162
- cwd: repoPath,
163
- stdio: 'pipe',
164
- });
165
- } catch (e) {
166
- execSync('git merge --abort', { cwd: repoPath, stdio: 'pipe' });
167
- return {
168
- status: 'conflict',
169
- message: `Merge failed: could not fast-forward or auto-merge`,
170
- conflictingFiles: [],
171
- };
172
- }
173
- }
174
-
175
- // Step 4: Cleanup
176
- workspaceManager.markMerged(taskId);
177
- workspaceManager.removeWorkspace(taskId, repoPath);
178
-
179
- // Step 5: Delete the branch
180
- try {
181
- execSync(`git branch -d ${ws.branch}`, {
182
- cwd: repoPath,
183
- stdio: 'pipe',
184
- });
185
- } catch (_) { /* branch cleanup is best-effort */ }
186
-
187
- return {
188
- status: 'merged',
189
- message: `Successfully merged ${ws.branch} into ${mainBranch}`,
190
- };
191
- } catch (error) {
192
- return {
193
- status: 'error',
194
- message: `Merge pipeline error: ${error.message}`,
195
- };
196
- }
197
- }
198
-
199
- /**
200
- * Get branch log (commits on task branch not on main).
201
- * @param {string} taskId
202
- * @param {string} repoPath
203
- * @returns {Object[]}
204
- */
205
- function getBranchLog(taskId, repoPath) {
206
- const ws = workspaceManager.getWorkspace(taskId);
207
- if (!ws) return [];
208
-
209
- const mainBranch = getMainBranch(repoPath);
210
-
211
- try {
212
- const log = execSync(
213
- `git log ${mainBranch}..${ws.branch} --oneline --format="%H|%s|%an|%ai"`,
214
- { cwd: repoPath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
215
- ).trim();
216
-
217
- if (!log) return [];
218
-
219
- return log.split('\n').map(line => {
220
- const [hash, subject, author, date] = line.split('|');
221
- return { hash, subject, author, date };
222
- });
223
- } catch (_) {
224
- return [];
225
- }
226
- }
227
-
228
- module.exports = {
229
- autoMerge,
230
- getDiff,
231
- checkConflicts,
232
- getBranchLog,
233
- getMainBranch,
234
- };
@@ -1,236 +0,0 @@
1
- /**
2
- * Symphony Orchestrator
3
- * Central state machine managing agent dispatch, concurrency, and lifecycle.
4
- */
5
- const { getDb } = require('./db');
6
- const taskManager = require('./task-manager');
7
- const fileLockManager = require('./file-lock-manager');
8
- const config = require('../symphony.config');
9
-
10
- /**
11
- * Register a new agent (IDE session connects).
12
- * @param {string} agentId - Agent identifier
13
- * @param {string} [name] - Human-readable name
14
- * @returns {Object} Agent record
15
- */
16
- function registerAgent(agentId, name) {
17
- const db = getDb();
18
- db.prepare(`
19
- INSERT INTO agents (id, name, status) VALUES (?, ?, 'idle')
20
- ON CONFLICT(id) DO UPDATE SET status = 'idle', last_heartbeat = datetime('now')
21
- `).run(agentId, name || agentId);
22
- return db.prepare('SELECT * FROM agents WHERE id = ?').get(agentId);
23
- }
24
-
25
- /**
26
- * Unregister an agent (IDE session disconnects).
27
- * Releases all file locks held by this agent.
28
- * @param {string} agentId
29
- */
30
- function unregisterAgent(agentId) {
31
- const db = getDb();
32
- fileLockManager.releaseLocks(agentId);
33
-
34
- // Reset any claimed tasks back to ready
35
- db.prepare(`
36
- UPDATE tasks SET status = 'ready', agent_id = NULL
37
- WHERE agent_id = ? AND status IN ('claimed', 'in_progress')
38
- `).run(agentId);
39
-
40
- db.prepare("UPDATE agents SET status = 'disconnected' WHERE id = ?").run(agentId);
41
- }
42
-
43
- /**
44
- * Update agent heartbeat.
45
- * @param {string} agentId
46
- */
47
- function heartbeat(agentId) {
48
- const db = getDb();
49
- db.prepare("UPDATE agents SET last_heartbeat = datetime('now') WHERE id = ?").run(agentId);
50
- }
51
-
52
- /**
53
- * Get all connected agents.
54
- * @returns {Object[]}
55
- */
56
- function getAgents() {
57
- const db = getDb();
58
- return db.prepare("SELECT * FROM agents WHERE status != 'disconnected' ORDER BY connected_at").all();
59
- }
60
-
61
- /**
62
- * Get a single agent.
63
- * @param {string} agentId
64
- * @returns {Object|null}
65
- */
66
- function getAgent(agentId) {
67
- const db = getDb();
68
- return db.prepare('SELECT * FROM agents WHERE id = ?').get(agentId) || null;
69
- }
70
-
71
- /**
72
- * Check if there are available slots for new agents.
73
- * @returns {boolean}
74
- */
75
- function hasAvailableSlots() {
76
- const agents = getAgents();
77
- const workingAgents = agents.filter(a => a.status === 'working');
78
- return workingAgents.length < config.maxAgents;
79
- }
80
-
81
- /**
82
- * Get number of available slots.
83
- * @returns {number}
84
- */
85
- function availableSlots() {
86
- const agents = getAgents();
87
- const workingAgents = agents.filter(a => a.status === 'working');
88
- return Math.max(0, config.maxAgents - workingAgents.length);
89
- }
90
-
91
- /**
92
- * Dispatch a task to an agent.
93
- * Sets agent to 'working' status and claims the task.
94
- * @param {string} agentId
95
- * @param {string} taskId
96
- * @returns {Object} The claimed task
97
- */
98
- function dispatchTask(agentId, taskId) {
99
- const db = getDb();
100
-
101
- if (!hasAvailableSlots()) {
102
- const agent = getAgent(agentId);
103
- // Allow re-dispatch to same agent
104
- if (!agent || agent.status !== 'working') {
105
- throw new Error('No available agent slots');
106
- }
107
- }
108
-
109
- const task = taskManager.claimTask(taskId, agentId);
110
-
111
- db.prepare(`
112
- UPDATE agents SET status = 'working', current_task_id = ? WHERE id = ?
113
- `).run(taskId, agentId);
114
-
115
- return task;
116
- }
117
-
118
- /**
119
- * Mark an agent as idle (after task completion).
120
- * @param {string} agentId
121
- */
122
- function markIdle(agentId) {
123
- const db = getDb();
124
- db.prepare(`
125
- UPDATE agents SET status = 'idle', current_task_id = NULL WHERE id = ?
126
- `).run(agentId);
127
- }
128
-
129
- /**
130
- * Get full system status.
131
- * @returns {Object}
132
- */
133
- function getStatus() {
134
- const agents = getAgents();
135
- const locks = fileLockManager.getAllLocks();
136
- const stats = taskManager.getStats();
137
- const readyTasks = taskManager.listTasks({ status: 'ready', limit: 10 });
138
-
139
- return {
140
- agents: agents.map(a => ({
141
- id: a.id,
142
- name: a.name,
143
- status: a.status,
144
- currentTask: a.current_task_id,
145
- lastHeartbeat: a.last_heartbeat,
146
- })),
147
- lockedFiles: locks.map(l => ({
148
- file: l.file_path,
149
- agent: l.agent_id,
150
- task: l.task_id,
151
- since: l.acquired_at,
152
- })),
153
- queue: readyTasks,
154
- stats,
155
- config: {
156
- maxAgents: config.maxAgents,
157
- availableSlots: availableSlots(),
158
- },
159
- };
160
- }
161
-
162
- /**
163
- * Update agent profile (name, specialties, color, maxConcurrent).
164
- * @param {string} agentId - Agent ID
165
- * @param {Object} fields - { name, specialties, color, max_concurrent }
166
- * @returns {Object} Updated agent
167
- */
168
- function updateAgentProfile(agentId, fields) {
169
- const db = getDb();
170
- const sets = [];
171
- const params = [];
172
-
173
- if (fields.name !== undefined) { sets.push('name = ?'); params.push(fields.name); }
174
- if (fields.specialties !== undefined) { sets.push('specialties = ?'); params.push(JSON.stringify(fields.specialties)); }
175
- if (fields.color !== undefined) { sets.push('color = ?'); params.push(fields.color); }
176
- if (fields.max_concurrent !== undefined) { sets.push('max_concurrent = ?'); params.push(fields.max_concurrent); }
177
-
178
- if (sets.length === 0) return getAgent(agentId);
179
-
180
- params.push(agentId);
181
- db.prepare(`UPDATE agents SET ${sets.join(', ')} WHERE id = ?`).run(...params);
182
- return getAgent(agentId);
183
- }
184
-
185
- /**
186
- * Remove an agent (must be idle or disconnected).
187
- * @param {string} agentId - Agent ID
188
- * @returns {boolean} Success
189
- */
190
- function removeAgent(agentId) {
191
- const db = getDb();
192
- const agent = getAgent(agentId);
193
- if (!agent) throw new Error(`Agent not found: ${agentId}`);
194
- if (agent.status === 'working') {
195
- throw new Error(`Cannot remove working agent: ${agentId}`);
196
- }
197
- fileLockManager.releaseLocks(agentId);
198
- db.prepare('DELETE FROM agents WHERE id = ?').run(agentId);
199
- return true;
200
- }
201
-
202
- /**
203
- * List all agents (including disconnected).
204
- * @returns {Object[]}
205
- */
206
- function listAllAgents() {
207
- const db = getDb();
208
- return db.prepare('SELECT * FROM agents ORDER BY status ASC, connected_at DESC').all()
209
- .map(a => ({
210
- id: a.id,
211
- name: a.name,
212
- status: a.status,
213
- currentTask: a.current_task_id,
214
- specialties: a.specialties ? JSON.parse(a.specialties) : [],
215
- color: a.color || '#8888a0',
216
- maxConcurrent: a.max_concurrent || 1,
217
- lastHeartbeat: a.last_heartbeat,
218
- }));
219
- }
220
-
221
- module.exports = {
222
- registerAgent,
223
- unregisterAgent,
224
- heartbeat,
225
- getAgents,
226
- getAgent,
227
- hasAvailableSlots,
228
- availableSlots,
229
- dispatchTask,
230
- markIdle,
231
- getStatus,
232
- updateAgentProfile,
233
- removeAgent,
234
- listAllAgents,
235
- };
236
-