@leejungkiin/awkit 1.1.7 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -4
- package/bin/awk.js +2 -2
- package/package.json +6 -3
- package/skill-packs/neural-memory/skills/nm-memory-sync/SKILL.md +14 -1
- package/skills/gitnexus-intelligence/SKILL.md +224 -0
- package/skills/orchestrator/SKILL.md +9 -0
- package/skills/symphony-orchestrator/SKILL.md +9 -7
- 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,335 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Symphony Task Manager
|
|
3
|
-
* CRUD operations for tasks with atomic claim/complete semantics.
|
|
4
|
-
*/
|
|
5
|
-
const { nanoid } = require('nanoid');
|
|
6
|
-
const { getDb } = require('./db');
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Create a new task.
|
|
10
|
-
* @param {string} title - Task title
|
|
11
|
-
* @param {Object} [opts] - Optional fields
|
|
12
|
-
* @returns {Object} Created task
|
|
13
|
-
*/
|
|
14
|
-
function createTask(title, opts = {}) {
|
|
15
|
-
const db = getDb();
|
|
16
|
-
const id = opts.id || `sym-${nanoid(8)}`;
|
|
17
|
-
const estimatedFiles = opts.estimatedFiles
|
|
18
|
-
? JSON.stringify(opts.estimatedFiles)
|
|
19
|
-
: null;
|
|
20
|
-
const requiredSkills = opts.requiredSkills
|
|
21
|
-
? JSON.stringify(opts.requiredSkills)
|
|
22
|
-
: '[]';
|
|
23
|
-
|
|
24
|
-
const stmt = db.prepare(`
|
|
25
|
-
INSERT INTO tasks (id, title, description, status, priority, acceptance, phase, epic_id, estimated_files, project_id, required_skills)
|
|
26
|
-
VALUES (?, ?, ?, 'ready', ?, ?, ?, ?, ?, ?, ?)
|
|
27
|
-
`);
|
|
28
|
-
|
|
29
|
-
stmt.run(
|
|
30
|
-
id,
|
|
31
|
-
title,
|
|
32
|
-
opts.description || null,
|
|
33
|
-
opts.priority || 2,
|
|
34
|
-
opts.acceptance || null,
|
|
35
|
-
opts.phase || null,
|
|
36
|
-
opts.epicId || null,
|
|
37
|
-
estimatedFiles,
|
|
38
|
-
opts.projectId || null,
|
|
39
|
-
requiredSkills
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
return getTask(id);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Get a single task by ID.
|
|
47
|
-
* @param {string} id - Task ID
|
|
48
|
-
* @returns {Object|null}
|
|
49
|
-
*/
|
|
50
|
-
function getTask(id) {
|
|
51
|
-
const db = getDb();
|
|
52
|
-
const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
53
|
-
return row ? normalizeTask(row) : null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* List tasks with optional filters.
|
|
58
|
-
* @param {Object} [filter] - { status, agentId, phase, epicId, project, limit }
|
|
59
|
-
* @returns {Object[]}
|
|
60
|
-
*/
|
|
61
|
-
function listTasks(filter = {}) {
|
|
62
|
-
const db = getDb();
|
|
63
|
-
const conditions = [];
|
|
64
|
-
const params = [];
|
|
65
|
-
|
|
66
|
-
if (filter.status) {
|
|
67
|
-
conditions.push('status = ?');
|
|
68
|
-
params.push(filter.status);
|
|
69
|
-
}
|
|
70
|
-
if (filter.agentId) {
|
|
71
|
-
conditions.push('agent_id = ?');
|
|
72
|
-
params.push(filter.agentId);
|
|
73
|
-
}
|
|
74
|
-
if (filter.phase) {
|
|
75
|
-
conditions.push('phase = ?');
|
|
76
|
-
params.push(filter.phase);
|
|
77
|
-
}
|
|
78
|
-
if (filter.epicId) {
|
|
79
|
-
conditions.push('epic_id = ?');
|
|
80
|
-
params.push(filter.epicId);
|
|
81
|
-
}
|
|
82
|
-
if (filter.project) {
|
|
83
|
-
conditions.push('project_id = ?');
|
|
84
|
-
params.push(filter.project);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
88
|
-
const limit = filter.limit ? `LIMIT ${parseInt(filter.limit)}` : '';
|
|
89
|
-
const order = 'ORDER BY priority ASC, created_at ASC';
|
|
90
|
-
|
|
91
|
-
const rows = db.prepare(`SELECT * FROM tasks ${where} ${order} ${limit}`).all(...params);
|
|
92
|
-
return rows.map(normalizeTask);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Claim a task for an agent (atomic operation).
|
|
97
|
-
* @param {string} taskId - Task ID
|
|
98
|
-
* @param {string} agentId - Agent ID
|
|
99
|
-
* @returns {Object} Updated task
|
|
100
|
-
* @throws {Error} If task is not claimable
|
|
101
|
-
*/
|
|
102
|
-
function claimTask(taskId, agentId) {
|
|
103
|
-
const db = getDb();
|
|
104
|
-
|
|
105
|
-
const claim = db.transaction(() => {
|
|
106
|
-
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
|
|
107
|
-
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
108
|
-
if (task.status !== 'ready') {
|
|
109
|
-
throw new Error(`Task ${taskId} is not claimable (status: ${task.status})`);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
db.prepare(`
|
|
113
|
-
UPDATE tasks
|
|
114
|
-
SET status = 'claimed', agent_id = ?, claimed_at = datetime('now')
|
|
115
|
-
WHERE id = ?
|
|
116
|
-
`).run(agentId, taskId);
|
|
117
|
-
|
|
118
|
-
return db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
return normalizeTask(claim());
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Update task progress.
|
|
126
|
-
* @param {string} taskId - Task ID
|
|
127
|
-
* @param {number} progress - Progress 0-100
|
|
128
|
-
* @param {Object} [opts] - { currentFile, lastAction }
|
|
129
|
-
*/
|
|
130
|
-
function updateProgress(taskId, progress, opts = {}) {
|
|
131
|
-
const db = getDb();
|
|
132
|
-
db.prepare(`
|
|
133
|
-
UPDATE tasks SET progress = ?, status = 'in_progress' WHERE id = ?
|
|
134
|
-
`).run(Math.min(100, Math.max(0, progress)), taskId);
|
|
135
|
-
return getTask(taskId);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Complete a task.
|
|
140
|
-
* @param {string} taskId - Task ID
|
|
141
|
-
* @param {string} summary - Completion summary
|
|
142
|
-
* @param {string[]} [filesChanged] - List of changed files
|
|
143
|
-
* @returns {Object} Updated task
|
|
144
|
-
*/
|
|
145
|
-
function completeTask(taskId, summary, filesChanged = []) {
|
|
146
|
-
const db = getDb();
|
|
147
|
-
db.prepare(`
|
|
148
|
-
UPDATE tasks
|
|
149
|
-
SET status = 'done', summary = ?, progress = 100, completed_at = datetime('now')
|
|
150
|
-
WHERE id = ?
|
|
151
|
-
`).run(summary, taskId);
|
|
152
|
-
return getTask(taskId);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Abandon a task (agent gives up).
|
|
157
|
-
* @param {string} taskId - Task ID
|
|
158
|
-
* @param {string} reason - Reason for abandoning
|
|
159
|
-
* @returns {Object} Updated task
|
|
160
|
-
*/
|
|
161
|
-
function abandonTask(taskId, reason) {
|
|
162
|
-
const db = getDb();
|
|
163
|
-
db.prepare(`
|
|
164
|
-
UPDATE tasks
|
|
165
|
-
SET status = 'ready', agent_id = NULL, summary = ?, progress = 0
|
|
166
|
-
WHERE id = ?
|
|
167
|
-
`).run(`Abandoned: ${reason}`, taskId);
|
|
168
|
-
return getTask(taskId);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Move task to review status.
|
|
173
|
-
* @param {string} taskId - Task ID
|
|
174
|
-
* @returns {Object} Updated task
|
|
175
|
-
*/
|
|
176
|
-
function reviewTask(taskId) {
|
|
177
|
-
const db = getDb();
|
|
178
|
-
db.prepare(`UPDATE tasks SET status = 'review' WHERE id = ?`).run(taskId);
|
|
179
|
-
return getTask(taskId);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Get task stats.
|
|
184
|
-
* @returns {Object} { ready, claimed, in_progress, review, done, total }
|
|
185
|
-
*/
|
|
186
|
-
function getStats() {
|
|
187
|
-
const db = getDb();
|
|
188
|
-
const rows = db.prepare(`
|
|
189
|
-
SELECT status, COUNT(*) as count FROM tasks GROUP BY status
|
|
190
|
-
`).all();
|
|
191
|
-
|
|
192
|
-
const stats = { ready: 0, claimed: 0, in_progress: 0, review: 0, done: 0, abandoned: 0, total: 0 };
|
|
193
|
-
for (const row of rows) {
|
|
194
|
-
stats[row.status] = row.count;
|
|
195
|
-
stats.total += row.count;
|
|
196
|
-
}
|
|
197
|
-
return stats;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Normalize a task row from the database.
|
|
202
|
-
*/
|
|
203
|
-
function normalizeTask(row) {
|
|
204
|
-
return {
|
|
205
|
-
...row,
|
|
206
|
-
estimated_files: row.estimated_files ? JSON.parse(row.estimated_files) : [],
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Update task fields.
|
|
212
|
-
* @param {string} taskId - Task ID
|
|
213
|
-
* @param {Object} fields - { title, description, priority, acceptance, phase, sort_order }
|
|
214
|
-
* @returns {Object} Updated task
|
|
215
|
-
*/
|
|
216
|
-
function updateTask(taskId, fields) {
|
|
217
|
-
const db = getDb();
|
|
218
|
-
const allowed = ['title', 'description', 'priority', 'acceptance', 'phase', 'sort_order'];
|
|
219
|
-
const sets = [];
|
|
220
|
-
const params = [];
|
|
221
|
-
|
|
222
|
-
for (const key of allowed) {
|
|
223
|
-
if (fields[key] !== undefined) {
|
|
224
|
-
sets.push(`${key} = ?`);
|
|
225
|
-
params.push(fields[key]);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (sets.length === 0) return getTask(taskId);
|
|
230
|
-
|
|
231
|
-
params.push(taskId);
|
|
232
|
-
db.prepare(`UPDATE tasks SET ${sets.join(', ')} WHERE id = ?`).run(...params);
|
|
233
|
-
return getTask(taskId);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Delete a task (only draft or ready status).
|
|
238
|
-
* @param {string} taskId - Task ID
|
|
239
|
-
* @returns {boolean} Success
|
|
240
|
-
*/
|
|
241
|
-
function deleteTask(taskId) {
|
|
242
|
-
const db = getDb();
|
|
243
|
-
const task = db.prepare('SELECT status FROM tasks WHERE id = ?').get(taskId);
|
|
244
|
-
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
245
|
-
if (!['draft', 'ready'].includes(task.status)) {
|
|
246
|
-
throw new Error(`Cannot delete task in ${task.status} status`);
|
|
247
|
-
}
|
|
248
|
-
db.prepare('DELETE FROM tasks WHERE id = ?').run(taskId);
|
|
249
|
-
return true;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Approve a draft task → ready.
|
|
254
|
-
* @param {string} taskId - Task ID
|
|
255
|
-
* @returns {Object} Updated task
|
|
256
|
-
*/
|
|
257
|
-
function approveTask(taskId) {
|
|
258
|
-
const db = getDb();
|
|
259
|
-
const task = db.prepare('SELECT status FROM tasks WHERE id = ?').get(taskId);
|
|
260
|
-
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
261
|
-
if (task.status !== 'draft') {
|
|
262
|
-
throw new Error(`Task ${taskId} is not a draft (status: ${task.status})`);
|
|
263
|
-
}
|
|
264
|
-
db.prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(taskId);
|
|
265
|
-
return getTask(taskId);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Batch approve multiple draft tasks → ready.
|
|
270
|
-
* @param {string[]} taskIds - Array of task IDs
|
|
271
|
-
* @returns {number} Number of approved tasks
|
|
272
|
-
*/
|
|
273
|
-
function bulkApprove(taskIds) {
|
|
274
|
-
const db = getDb();
|
|
275
|
-
const stmt = db.prepare("UPDATE tasks SET status = 'ready' WHERE id = ? AND status = 'draft'");
|
|
276
|
-
let count = 0;
|
|
277
|
-
const run = db.transaction(() => {
|
|
278
|
-
for (const id of taskIds) {
|
|
279
|
-
const result = stmt.run(id);
|
|
280
|
-
count += result.changes;
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
run();
|
|
284
|
-
return count;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Reopen a done task → ready (reset agent, progress).
|
|
289
|
-
* @param {string} taskId - Task ID
|
|
290
|
-
* @returns {Object} Updated task
|
|
291
|
-
*/
|
|
292
|
-
function reopenTask(taskId) {
|
|
293
|
-
const db = getDb();
|
|
294
|
-
db.prepare(`
|
|
295
|
-
UPDATE tasks
|
|
296
|
-
SET status = 'ready', agent_id = NULL, progress = 0,
|
|
297
|
-
claimed_at = NULL, completed_at = NULL, summary = NULL
|
|
298
|
-
WHERE id = ?
|
|
299
|
-
`).run(taskId);
|
|
300
|
-
return getTask(taskId);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Reorder tasks by setting sort_order based on array position.
|
|
305
|
-
* @param {string[]} orderedIds - Task IDs in desired order
|
|
306
|
-
*/
|
|
307
|
-
function reorderTasks(orderedIds) {
|
|
308
|
-
const db = getDb();
|
|
309
|
-
const stmt = db.prepare('UPDATE tasks SET sort_order = ? WHERE id = ?');
|
|
310
|
-
const run = db.transaction(() => {
|
|
311
|
-
for (let i = 0; i < orderedIds.length; i++) {
|
|
312
|
-
stmt.run(i, orderedIds[i]);
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
run();
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
module.exports = {
|
|
319
|
-
createTask,
|
|
320
|
-
getTask,
|
|
321
|
-
listTasks,
|
|
322
|
-
claimTask,
|
|
323
|
-
updateProgress,
|
|
324
|
-
completeTask,
|
|
325
|
-
abandonTask,
|
|
326
|
-
reviewTask,
|
|
327
|
-
getStats,
|
|
328
|
-
updateTask,
|
|
329
|
-
deleteTask,
|
|
330
|
-
approveTask,
|
|
331
|
-
bulkApprove,
|
|
332
|
-
reopenTask,
|
|
333
|
-
reorderTasks,
|
|
334
|
-
};
|
|
335
|
-
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Symphony Workspace Manager
|
|
3
|
-
* Git worktree lifecycle management with hybrid clone/worktree support.
|
|
4
|
-
*/
|
|
5
|
-
const { execSync } = require('child_process');
|
|
6
|
-
const { nanoid } = require('nanoid');
|
|
7
|
-
const { getDb } = require('./db');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Create a workspace for a task.
|
|
13
|
-
* Uses git worktree (default) or full clone based on hybrid config.
|
|
14
|
-
*
|
|
15
|
-
* @param {string} taskId - Task ID
|
|
16
|
-
* @param {string} repoPath - Path to the main repository
|
|
17
|
-
* @param {Object} [opts] - { type: 'worktree'|'clone', branchPrefix }
|
|
18
|
-
* @returns {Object} { id, path, branch, type }
|
|
19
|
-
*/
|
|
20
|
-
function createWorkspace(taskId, repoPath, opts = {}) {
|
|
21
|
-
const db = getDb();
|
|
22
|
-
const type = opts.type || 'worktree';
|
|
23
|
-
const branchPrefix = opts.branchPrefix || 'symphony/';
|
|
24
|
-
const branch = `${branchPrefix}${sanitizeId(taskId)}`;
|
|
25
|
-
const wsId = `ws-${nanoid(8)}`;
|
|
26
|
-
|
|
27
|
-
const workspaceRoot = path.resolve(repoPath, '.symphony', 'workspaces');
|
|
28
|
-
if (!fs.existsSync(workspaceRoot)) {
|
|
29
|
-
fs.mkdirSync(workspaceRoot, { recursive: true });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const wsPath = path.join(workspaceRoot, sanitizeId(taskId));
|
|
33
|
-
|
|
34
|
-
if (type === 'worktree') {
|
|
35
|
-
createWorktree(repoPath, wsPath, branch);
|
|
36
|
-
} else {
|
|
37
|
-
cloneRepo(repoPath, wsPath, branch);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Record in database
|
|
41
|
-
db.prepare(`
|
|
42
|
-
INSERT INTO workspaces (id, task_id, type, path, branch, status)
|
|
43
|
-
VALUES (?, ?, ?, ?, ?, 'active')
|
|
44
|
-
`).run(wsId, taskId, type, wsPath, branch);
|
|
45
|
-
|
|
46
|
-
return { id: wsId, path: wsPath, branch, type };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Remove a workspace.
|
|
51
|
-
* @param {string} taskId - Task ID
|
|
52
|
-
* @param {string} repoPath - Path to the main repository
|
|
53
|
-
*/
|
|
54
|
-
function removeWorkspace(taskId, repoPath) {
|
|
55
|
-
const db = getDb();
|
|
56
|
-
const ws = db.prepare('SELECT * FROM workspaces WHERE task_id = ? AND status = ?').get(taskId, 'active');
|
|
57
|
-
if (!ws) return;
|
|
58
|
-
|
|
59
|
-
if (ws.type === 'worktree') {
|
|
60
|
-
try {
|
|
61
|
-
execSync(`git worktree remove "${ws.path}" --force`, {
|
|
62
|
-
cwd: repoPath,
|
|
63
|
-
stdio: 'pipe',
|
|
64
|
-
});
|
|
65
|
-
} catch (e) {
|
|
66
|
-
// Fallback: manual cleanup
|
|
67
|
-
if (fs.existsSync(ws.path)) {
|
|
68
|
-
fs.rmSync(ws.path, { recursive: true, force: true });
|
|
69
|
-
}
|
|
70
|
-
try {
|
|
71
|
-
execSync(`git worktree prune`, { cwd: repoPath, stdio: 'pipe' });
|
|
72
|
-
} catch (_) { /* ignore */ }
|
|
73
|
-
}
|
|
74
|
-
} else {
|
|
75
|
-
// Clone: just remove directory
|
|
76
|
-
if (fs.existsSync(ws.path)) {
|
|
77
|
-
fs.rmSync(ws.path, { recursive: true, force: true });
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
db.prepare("UPDATE workspaces SET status = 'cleaned' WHERE task_id = ?").run(taskId);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* List all active workspaces.
|
|
86
|
-
* @returns {Object[]}
|
|
87
|
-
*/
|
|
88
|
-
function listWorkspaces() {
|
|
89
|
-
const db = getDb();
|
|
90
|
-
return db.prepare("SELECT * FROM workspaces WHERE status = 'active' ORDER BY created_at DESC").all();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Get workspace for a task.
|
|
95
|
-
* @param {string} taskId
|
|
96
|
-
* @returns {Object|null}
|
|
97
|
-
*/
|
|
98
|
-
function getWorkspace(taskId) {
|
|
99
|
-
const db = getDb();
|
|
100
|
-
return db.prepare("SELECT * FROM workspaces WHERE task_id = ? AND status = 'active'").get(taskId) || null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Mark workspace as merged.
|
|
105
|
-
* @param {string} taskId
|
|
106
|
-
*/
|
|
107
|
-
function markMerged(taskId) {
|
|
108
|
-
const db = getDb();
|
|
109
|
-
db.prepare("UPDATE workspaces SET status = 'merged', merged_at = datetime('now') WHERE task_id = ?").run(taskId);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ─── Internal Helpers ────────────────────────────────────────────────────────
|
|
113
|
-
|
|
114
|
-
function createWorktree(repoPath, wsPath, branch) {
|
|
115
|
-
try {
|
|
116
|
-
// Create new branch from current HEAD and checkout in worktree
|
|
117
|
-
execSync(`git worktree add -b "${branch}" "${wsPath}"`, {
|
|
118
|
-
cwd: repoPath,
|
|
119
|
-
stdio: 'pipe',
|
|
120
|
-
});
|
|
121
|
-
} catch (e) {
|
|
122
|
-
// Branch might already exist — try without -b
|
|
123
|
-
try {
|
|
124
|
-
execSync(`git worktree add "${wsPath}" "${branch}"`, {
|
|
125
|
-
cwd: repoPath,
|
|
126
|
-
stdio: 'pipe',
|
|
127
|
-
});
|
|
128
|
-
} catch (e2) {
|
|
129
|
-
throw new Error(`Failed to create worktree: ${e2.message}`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function cloneRepo(repoPath, wsPath, branch) {
|
|
135
|
-
const remote = getRemoteUrl(repoPath);
|
|
136
|
-
if (remote) {
|
|
137
|
-
execSync(`git clone "${remote}" "${wsPath}"`, { stdio: 'pipe' });
|
|
138
|
-
execSync(`git checkout -b "${branch}"`, { cwd: wsPath, stdio: 'pipe' });
|
|
139
|
-
} else {
|
|
140
|
-
// Local-only repo: just copy
|
|
141
|
-
execSync(`git clone "${repoPath}" "${wsPath}"`, { stdio: 'pipe' });
|
|
142
|
-
execSync(`git checkout -b "${branch}"`, { cwd: wsPath, stdio: 'pipe' });
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function getRemoteUrl(repoPath) {
|
|
147
|
-
try {
|
|
148
|
-
return execSync('git remote get-url origin', {
|
|
149
|
-
cwd: repoPath,
|
|
150
|
-
encoding: 'utf8',
|
|
151
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
152
|
-
}).trim();
|
|
153
|
-
} catch (_) {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function sanitizeId(id) {
|
|
159
|
-
return id.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
module.exports = {
|
|
163
|
-
createWorkspace,
|
|
164
|
-
removeWorkspace,
|
|
165
|
-
listWorkspaces,
|
|
166
|
-
getWorkspace,
|
|
167
|
-
markMerged,
|
|
168
|
-
};
|