@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
|
@@ -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
|
-
|