@hasna/todos 0.9.21 → 0.9.23
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 +112 -76
- package/dist/cli/index.js +158 -1
- package/dist/index.js +24 -1
- package/dist/mcp/index.js +60 -1
- package/dist/server/index.js +125 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -163,20 +163,20 @@ Or start manually via stdio:
|
|
|
163
163
|
todos-mcp
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
-
### MCP Tools (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
166
|
+
### MCP Tools (40)
|
|
167
|
+
|
|
168
|
+
**Tasks:** `create_task`, `list_tasks`, `get_task`, `update_task`, `delete_task`, `start_task`, `complete_task`, `lock_task`, `unlock_task`, `approve_task`
|
|
169
|
+
**Dependencies:** `add_dependency`, `remove_dependency`
|
|
170
|
+
**Comments:** `add_comment`
|
|
171
|
+
**Projects:** `create_project`, `list_projects`
|
|
172
|
+
**Plans:** `create_plan`, `list_plans`, `get_plan`, `update_plan`, `delete_plan`
|
|
173
|
+
**Agents:** `register_agent`, `list_agents`, `get_agent`
|
|
174
|
+
**Task Lists:** `create_task_list`, `list_task_lists`, `get_task_list`, `update_task_list`, `delete_task_list`
|
|
175
|
+
**Search:** `search_tasks`
|
|
176
|
+
**Sync:** `sync`
|
|
177
|
+
**Audit:** `get_task_history`, `get_recent_activity`
|
|
178
|
+
**Webhooks:** `create_webhook`, `list_webhooks`, `delete_webhook`
|
|
179
|
+
**Templates:** `create_template`, `list_templates`, `create_task_from_template`, `delete_template`
|
|
180
180
|
|
|
181
181
|
### MCP Resources
|
|
182
182
|
|
|
@@ -208,28 +208,66 @@ The dashboard auto-refreshes every 30 seconds, supports dark mode, and includes
|
|
|
208
208
|
|
|
209
209
|
## REST API
|
|
210
210
|
|
|
211
|
-
|
|
211
|
+
Start the server with `todos serve` or `todos-serve`. Default port: 19427.
|
|
212
|
+
|
|
213
|
+
### Tasks
|
|
212
214
|
|
|
213
215
|
| Method | Endpoint | Description |
|
|
214
216
|
|--------|----------|-------------|
|
|
215
|
-
| GET | `/api/
|
|
216
|
-
|
|
|
217
|
-
| POST | `/api/tasks` | Create a task |
|
|
217
|
+
| GET | `/api/tasks` | List tasks. Query: `?status=`, `?project_id=`, `?limit=` |
|
|
218
|
+
| POST | `/api/tasks` | Create task. Body: `{ title, description?, priority?, project_id?, estimated_minutes?, requires_approval? }` |
|
|
218
219
|
| GET | `/api/tasks/:id` | Get task details |
|
|
219
|
-
| PATCH | `/api/tasks/:id` | Update
|
|
220
|
-
| DELETE | `/api/tasks/:id` | Delete
|
|
221
|
-
| POST | `/api/tasks/:id/start` | Start
|
|
222
|
-
| POST | `/api/tasks/:id/complete` | Complete
|
|
223
|
-
|
|
|
224
|
-
|
|
|
225
|
-
| GET | `/api/tasks/export?format=
|
|
226
|
-
| GET | `/api/
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
|
231
|
-
|
|
232
|
-
|
|
|
220
|
+
| PATCH | `/api/tasks/:id` | Update task. Body: `{ title?, status?, priority?, description?, assigned_to?, tags?, due_at?, estimated_minutes?, requires_approval?, approved_by? }` |
|
|
221
|
+
| DELETE | `/api/tasks/:id` | Delete task |
|
|
222
|
+
| POST | `/api/tasks/:id/start` | Start task (sets in_progress, locks) |
|
|
223
|
+
| POST | `/api/tasks/:id/complete` | Complete task |
|
|
224
|
+
| GET | `/api/tasks/:id/history` | Get task audit log |
|
|
225
|
+
| POST | `/api/tasks/bulk` | Bulk ops. Body: `{ ids: [...], action: "start" | "complete" | "delete" }` |
|
|
226
|
+
| GET | `/api/tasks/export?format=csv` | Export as CSV |
|
|
227
|
+
| GET | `/api/tasks/export?format=json` | Export as JSON |
|
|
228
|
+
|
|
229
|
+
### Projects
|
|
230
|
+
|
|
231
|
+
| Method | Endpoint | Description |
|
|
232
|
+
|--------|----------|-------------|
|
|
233
|
+
| GET | `/api/projects` | List all projects |
|
|
234
|
+
| POST | `/api/projects` | Create project. Body: `{ name, path, description? }` |
|
|
235
|
+
| DELETE | `/api/projects/:id` | Delete project |
|
|
236
|
+
| POST | `/api/projects/bulk` | Bulk delete. Body: `{ ids: [...], action: "delete" }` |
|
|
237
|
+
|
|
238
|
+
### Plans
|
|
239
|
+
|
|
240
|
+
| Method | Endpoint | Description |
|
|
241
|
+
|--------|----------|-------------|
|
|
242
|
+
| GET | `/api/plans` | List plans. Query: `?project_id=` |
|
|
243
|
+
| POST | `/api/plans` | Create plan. Body: `{ name, description?, project_id?, task_list_id?, agent_id?, status? }` |
|
|
244
|
+
| GET | `/api/plans/:id` | Get plan with its tasks |
|
|
245
|
+
| PATCH | `/api/plans/:id` | Update plan |
|
|
246
|
+
| DELETE | `/api/plans/:id` | Delete plan |
|
|
247
|
+
| POST | `/api/plans/bulk` | Bulk delete |
|
|
248
|
+
|
|
249
|
+
### Agents
|
|
250
|
+
|
|
251
|
+
| Method | Endpoint | Description |
|
|
252
|
+
|--------|----------|-------------|
|
|
253
|
+
| GET | `/api/agents` | List all agents |
|
|
254
|
+
| POST | `/api/agents` | Register agent. Body: `{ name, description?, role? }` |
|
|
255
|
+
| PATCH | `/api/agents/:id` | Update agent. Body: `{ name?, description?, role? }` |
|
|
256
|
+
| DELETE | `/api/agents/:id` | Delete agent |
|
|
257
|
+
| POST | `/api/agents/bulk` | Bulk delete |
|
|
258
|
+
|
|
259
|
+
### Webhooks, Templates, Activity
|
|
260
|
+
|
|
261
|
+
| Method | Endpoint | Description |
|
|
262
|
+
|--------|----------|-------------|
|
|
263
|
+
| GET | `/api/webhooks` | List webhooks |
|
|
264
|
+
| POST | `/api/webhooks` | Create webhook. Body: `{ url, events?, secret? }` |
|
|
265
|
+
| DELETE | `/api/webhooks/:id` | Delete webhook |
|
|
266
|
+
| GET | `/api/templates` | List task templates |
|
|
267
|
+
| POST | `/api/templates` | Create template. Body: `{ name, title_pattern, description?, priority?, tags? }` |
|
|
268
|
+
| DELETE | `/api/templates/:id` | Delete template |
|
|
269
|
+
| GET | `/api/activity` | Recent audit log. Query: `?limit=50` |
|
|
270
|
+
| GET | `/api/stats` | Dashboard statistics |
|
|
233
271
|
|
|
234
272
|
## Sync
|
|
235
273
|
|
|
@@ -274,50 +312,48 @@ Claude uses native Claude Code task lists. Other agents use JSON files under `~/
|
|
|
274
312
|
}
|
|
275
313
|
```
|
|
276
314
|
|
|
277
|
-
## CLI
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
All
|
|
319
|
-
|
|
320
|
-
Partial IDs work everywhere — use the first 8+ characters of any UUID.
|
|
315
|
+
## CLI Reference
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
# Task management
|
|
319
|
+
todos add "title" [-d desc] [-p priority] [--tags t1,t2] [--plan id] [--estimated 30] [--approval]
|
|
320
|
+
todos list [-s status] [-p priority] [--assigned agent] [--project-name name] [--agent-name name] [--sort field] [-a]
|
|
321
|
+
todos show <id> # Full task details with relations
|
|
322
|
+
todos update <id> [--title t] [-s status] [-p priority] [--tags t1,t2] [--estimated 30]
|
|
323
|
+
todos done <id> # Complete a task
|
|
324
|
+
todos start <id> # Claim, lock, and start
|
|
325
|
+
todos delete <id>
|
|
326
|
+
todos approve <id> # Approve a task requiring approval
|
|
327
|
+
todos history <id> # Show task audit log
|
|
328
|
+
todos search <query>
|
|
329
|
+
todos bulk <done|start|delete> <id1> <id2> ...
|
|
330
|
+
todos comment <id> <text>
|
|
331
|
+
todos deps <id> --add <dep_id> # Manage dependencies
|
|
332
|
+
|
|
333
|
+
# Plans
|
|
334
|
+
todos plans [--add name] [--show id] [--delete id] [--complete id]
|
|
335
|
+
|
|
336
|
+
# Templates
|
|
337
|
+
todos templates [--add name --title pattern] [--delete id] [--use id]
|
|
338
|
+
|
|
339
|
+
# Projects & Agents
|
|
340
|
+
todos projects [--add name --path /path]
|
|
341
|
+
todos agents
|
|
342
|
+
todos init <name> # Register an agent
|
|
343
|
+
todos lists # Manage task lists
|
|
344
|
+
|
|
345
|
+
# Utilities
|
|
346
|
+
todos count [--json] # Quick stats
|
|
347
|
+
todos watch [-s status] [-i 5] # Live-updating task list
|
|
348
|
+
todos config [--get key] [--set key=value]
|
|
349
|
+
todos export [--format csv|json]
|
|
350
|
+
todos sync [--task-list id] # Sync with Claude Code
|
|
351
|
+
todos serve [--port 19427] # Start web dashboard
|
|
352
|
+
todos interactive # Launch TUI
|
|
353
|
+
todos upgrade # Update to latest version
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
All output supports `--json` for machine-readable format.
|
|
321
357
|
|
|
322
358
|
## Library Usage
|
|
323
359
|
|
package/dist/cli/index.js
CHANGED
|
@@ -3205,8 +3205,26 @@ function deleteTask(id, db) {
|
|
|
3205
3205
|
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
3206
3206
|
return result.changes > 0;
|
|
3207
3207
|
}
|
|
3208
|
+
function getBlockingDeps(id, db) {
|
|
3209
|
+
const d = db || getDatabase();
|
|
3210
|
+
const deps = getTaskDependencies(id, d);
|
|
3211
|
+
if (deps.length === 0)
|
|
3212
|
+
return [];
|
|
3213
|
+
const blocking = [];
|
|
3214
|
+
for (const dep of deps) {
|
|
3215
|
+
const task = getTask(dep.depends_on, d);
|
|
3216
|
+
if (task && task.status !== "completed")
|
|
3217
|
+
blocking.push(task);
|
|
3218
|
+
}
|
|
3219
|
+
return blocking;
|
|
3220
|
+
}
|
|
3208
3221
|
function startTask(id, agentId, db) {
|
|
3209
3222
|
const d = db || getDatabase();
|
|
3223
|
+
const blocking = getBlockingDeps(id, d);
|
|
3224
|
+
if (blocking.length > 0) {
|
|
3225
|
+
const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
|
|
3226
|
+
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
3227
|
+
}
|
|
3210
3228
|
const cutoff = lockExpiryCutoff();
|
|
3211
3229
|
const timestamp = now();
|
|
3212
3230
|
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
@@ -3222,7 +3240,7 @@ function startTask(id, agentId, db) {
|
|
|
3222
3240
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
3223
3241
|
return getTask(id, d);
|
|
3224
3242
|
}
|
|
3225
|
-
function completeTask(id, agentId, db) {
|
|
3243
|
+
function completeTask(id, agentId, db, evidence) {
|
|
3226
3244
|
const d = db || getDatabase();
|
|
3227
3245
|
const task = getTask(id, d);
|
|
3228
3246
|
if (!task)
|
|
@@ -3231,6 +3249,10 @@ function completeTask(id, agentId, db) {
|
|
|
3231
3249
|
throw new LockError(id, task.locked_by);
|
|
3232
3250
|
}
|
|
3233
3251
|
checkCompletionGuard(task, agentId || null, d);
|
|
3252
|
+
if (evidence) {
|
|
3253
|
+
const meta = { ...task.metadata, _evidence: evidence };
|
|
3254
|
+
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta), id]);
|
|
3255
|
+
}
|
|
3234
3256
|
const timestamp = now();
|
|
3235
3257
|
d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, version = version + 1, updated_at = ?
|
|
3236
3258
|
WHERE id = ?`, [timestamp, timestamp, id]);
|
|
@@ -3293,6 +3315,10 @@ function removeDependency(taskId, dependsOn, db) {
|
|
|
3293
3315
|
const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
|
|
3294
3316
|
return result.changes > 0;
|
|
3295
3317
|
}
|
|
3318
|
+
function getTaskDependencies(taskId, db) {
|
|
3319
|
+
const d = db || getDatabase();
|
|
3320
|
+
return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
|
|
3321
|
+
}
|
|
3296
3322
|
function wouldCreateCycle(taskId, dependsOn, db) {
|
|
3297
3323
|
const visited = new Set;
|
|
3298
3324
|
const queue = [dependsOn];
|
|
@@ -9132,6 +9158,39 @@ ${task.id.slice(0, 8)} | ${task.priority} | ${task.title}` }] };
|
|
|
9132
9158
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
9133
9159
|
}
|
|
9134
9160
|
});
|
|
9161
|
+
server.tool("get_my_tasks", "Get your assigned tasks and stats. Auto-registers if needed.", {
|
|
9162
|
+
agent_name: exports_external.string().describe("Your agent name")
|
|
9163
|
+
}, async ({ agent_name }) => {
|
|
9164
|
+
try {
|
|
9165
|
+
const agent = registerAgent({ name: agent_name });
|
|
9166
|
+
const tasks = listTasks({});
|
|
9167
|
+
const myTasks = tasks.filter((t) => t.assigned_to === agent_name || t.assigned_to === agent.id || t.agent_id === agent.id || t.agent_id === agent_name);
|
|
9168
|
+
const pending = myTasks.filter((t) => t.status === "pending");
|
|
9169
|
+
const inProgress = myTasks.filter((t) => t.status === "in_progress");
|
|
9170
|
+
const completed = myTasks.filter((t) => t.status === "completed");
|
|
9171
|
+
const rate = myTasks.length > 0 ? Math.round(completed.length / myTasks.length * 100) : 0;
|
|
9172
|
+
const lines = [
|
|
9173
|
+
`Agent: ${agent.name} (${agent.id})`,
|
|
9174
|
+
`Tasks: ${myTasks.length} total, ${pending.length} pending, ${inProgress.length} active, ${completed.length} done (${rate}%)`
|
|
9175
|
+
];
|
|
9176
|
+
if (pending.length > 0) {
|
|
9177
|
+
lines.push(`
|
|
9178
|
+
Pending:`);
|
|
9179
|
+
for (const t of pending.slice(0, 10))
|
|
9180
|
+
lines.push(` [${t.priority}] ${t.id.slice(0, 8)} | ${t.title}`);
|
|
9181
|
+
}
|
|
9182
|
+
if (inProgress.length > 0) {
|
|
9183
|
+
lines.push(`
|
|
9184
|
+
In Progress:`);
|
|
9185
|
+
for (const t of inProgress)
|
|
9186
|
+
lines.push(` ${t.id.slice(0, 8)} | ${t.title}`);
|
|
9187
|
+
}
|
|
9188
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
9189
|
+
`) }] };
|
|
9190
|
+
} catch (e) {
|
|
9191
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
9192
|
+
}
|
|
9193
|
+
});
|
|
9135
9194
|
server.resource("tasks", "todos://tasks", { description: "All active tasks", mimeType: "application/json" }, async () => {
|
|
9136
9195
|
const tasks = listTasks({ status: ["pending", "in_progress"] });
|
|
9137
9196
|
return { contents: [{ uri: "todos://tasks", text: JSON.stringify(tasks, null, 2), mimeType: "application/json" }] };
|
|
@@ -9225,6 +9284,19 @@ function taskToSummary(task) {
|
|
|
9225
9284
|
async function startServer(port, options) {
|
|
9226
9285
|
const shouldOpen = options?.open ?? true;
|
|
9227
9286
|
getDatabase();
|
|
9287
|
+
const sseClients = new Set;
|
|
9288
|
+
function broadcastEvent(event) {
|
|
9289
|
+
const data = JSON.stringify({ ...event, timestamp: new Date().toISOString() });
|
|
9290
|
+
for (const controller of sseClients) {
|
|
9291
|
+
try {
|
|
9292
|
+
controller.enqueue(`data: ${data}
|
|
9293
|
+
|
|
9294
|
+
`);
|
|
9295
|
+
} catch {
|
|
9296
|
+
sseClients.delete(controller);
|
|
9297
|
+
}
|
|
9298
|
+
}
|
|
9299
|
+
}
|
|
9228
9300
|
const dashboardDir = resolveDashboardDir();
|
|
9229
9301
|
const dashboardExists = existsSync6(dashboardDir);
|
|
9230
9302
|
if (!dashboardExists) {
|
|
@@ -9250,6 +9322,27 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
9250
9322
|
}
|
|
9251
9323
|
});
|
|
9252
9324
|
}
|
|
9325
|
+
if (path === "/api/events" && method === "GET") {
|
|
9326
|
+
const stream = new ReadableStream({
|
|
9327
|
+
start(controller) {
|
|
9328
|
+
sseClients.add(controller);
|
|
9329
|
+
controller.enqueue(`data: ${JSON.stringify({ type: "connected", timestamp: new Date().toISOString() })}
|
|
9330
|
+
|
|
9331
|
+
`);
|
|
9332
|
+
},
|
|
9333
|
+
cancel(controller) {
|
|
9334
|
+
sseClients.delete(controller);
|
|
9335
|
+
}
|
|
9336
|
+
});
|
|
9337
|
+
return new Response(stream, {
|
|
9338
|
+
headers: {
|
|
9339
|
+
"Content-Type": "text/event-stream",
|
|
9340
|
+
"Cache-Control": "no-cache",
|
|
9341
|
+
Connection: "keep-alive",
|
|
9342
|
+
"Access-Control-Allow-Origin": `http://localhost:${port}`
|
|
9343
|
+
}
|
|
9344
|
+
});
|
|
9345
|
+
}
|
|
9253
9346
|
if (path === "/api/stats" && method === "GET") {
|
|
9254
9347
|
const all = listTasks({ limit: 1e4 });
|
|
9255
9348
|
const projects = listProjects();
|
|
@@ -9287,6 +9380,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
9287
9380
|
priority: body.priority,
|
|
9288
9381
|
project_id: body.project_id
|
|
9289
9382
|
});
|
|
9383
|
+
broadcastEvent({ type: "task", task_id: task.id, action: "created", agent_id: task.agent_id });
|
|
9290
9384
|
return json(taskToSummary(task), 201, port);
|
|
9291
9385
|
} catch (e) {
|
|
9292
9386
|
return json({ error: e instanceof Error ? e.message : "Failed to create task" }, 500, port);
|
|
@@ -9389,6 +9483,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
9389
9483
|
const id = startMatch[1];
|
|
9390
9484
|
try {
|
|
9391
9485
|
const task = startTask(id, "dashboard");
|
|
9486
|
+
broadcastEvent({ type: "task", task_id: task.id, action: "started", agent_id: "dashboard" });
|
|
9392
9487
|
return json(taskToSummary(task), 200, port);
|
|
9393
9488
|
} catch (e) {
|
|
9394
9489
|
return json({ error: e instanceof Error ? e.message : "Failed to start task" }, 500, port);
|
|
@@ -9399,6 +9494,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
9399
9494
|
const id = completeMatch[1];
|
|
9400
9495
|
try {
|
|
9401
9496
|
const task = completeTask(id, "dashboard");
|
|
9497
|
+
broadcastEvent({ type: "task", task_id: task.id, action: "completed", agent_id: "dashboard" });
|
|
9402
9498
|
return json(taskToSummary(task), 200, port);
|
|
9403
9499
|
} catch (e) {
|
|
9404
9500
|
return json({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500, port);
|
|
@@ -9407,6 +9503,67 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
9407
9503
|
if (path === "/api/projects" && method === "GET") {
|
|
9408
9504
|
return json(listProjects(), 200, port);
|
|
9409
9505
|
}
|
|
9506
|
+
if (path === "/api/agents/me" && method === "GET") {
|
|
9507
|
+
const name = url.searchParams.get("name");
|
|
9508
|
+
if (!name)
|
|
9509
|
+
return json({ error: "Missing name param" }, 400, port);
|
|
9510
|
+
const { registerAgent: registerAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
|
|
9511
|
+
const agent = registerAgent2({ name });
|
|
9512
|
+
const tasks = listTasks({ assigned_to: name });
|
|
9513
|
+
const agentIdTasks = listTasks({ agent_id: agent.id });
|
|
9514
|
+
const allTasks = [...tasks, ...agentIdTasks.filter((t) => !tasks.some((tt) => tt.id === t.id))];
|
|
9515
|
+
const pending = allTasks.filter((t) => t.status === "pending");
|
|
9516
|
+
const inProgress = allTasks.filter((t) => t.status === "in_progress");
|
|
9517
|
+
const completed = allTasks.filter((t) => t.status === "completed");
|
|
9518
|
+
return json({
|
|
9519
|
+
agent,
|
|
9520
|
+
pending_tasks: pending.map(taskToSummary),
|
|
9521
|
+
in_progress_tasks: inProgress.map(taskToSummary),
|
|
9522
|
+
stats: {
|
|
9523
|
+
total: allTasks.length,
|
|
9524
|
+
pending: pending.length,
|
|
9525
|
+
in_progress: inProgress.length,
|
|
9526
|
+
completed: completed.length,
|
|
9527
|
+
completion_rate: allTasks.length > 0 ? Math.round(completed.length / allTasks.length * 100) : 0
|
|
9528
|
+
}
|
|
9529
|
+
}, 200, port);
|
|
9530
|
+
}
|
|
9531
|
+
const queueMatch = path.match(/^\/api\/agents\/([^/]+)\/queue$/);
|
|
9532
|
+
if (queueMatch && method === "GET") {
|
|
9533
|
+
const agentId = decodeURIComponent(queueMatch[1]);
|
|
9534
|
+
const pending = listTasks({ status: "pending" });
|
|
9535
|
+
const queue = pending.filter((t) => t.assigned_to === agentId || t.agent_id === agentId || !t.assigned_to && !t.locked_by);
|
|
9536
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
9537
|
+
queue.sort((a, b) => (order[a.priority] ?? 4) - (order[b.priority] ?? 4) || new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
|
9538
|
+
return json(queue.map(taskToSummary), 200, port);
|
|
9539
|
+
}
|
|
9540
|
+
if (path === "/api/tasks/claim" && method === "POST") {
|
|
9541
|
+
try {
|
|
9542
|
+
const body = await req.json();
|
|
9543
|
+
const agentId = body.agent_id || "anonymous";
|
|
9544
|
+
const pending = listTasks({ status: "pending", project_id: body.project_id });
|
|
9545
|
+
const available = pending.filter((t) => !t.locked_by);
|
|
9546
|
+
if (available.length === 0)
|
|
9547
|
+
return json({ task: null }, 200, port);
|
|
9548
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
9549
|
+
available.sort((a, b) => (order[a.priority] ?? 4) - (order[b.priority] ?? 4));
|
|
9550
|
+
const target = available[0];
|
|
9551
|
+
try {
|
|
9552
|
+
const claimed = startTask(target.id, agentId);
|
|
9553
|
+
return json({ task: taskToSummary(claimed) }, 200, port);
|
|
9554
|
+
} catch (e) {
|
|
9555
|
+
const next = available[1] || null;
|
|
9556
|
+
return json({
|
|
9557
|
+
task: null,
|
|
9558
|
+
locked_by: target.locked_by,
|
|
9559
|
+
locked_since: target.locked_at,
|
|
9560
|
+
suggested_task: next ? taskToSummary(next) : null
|
|
9561
|
+
}, 200, port);
|
|
9562
|
+
}
|
|
9563
|
+
} catch (e) {
|
|
9564
|
+
return json({ error: e instanceof Error ? e.message : "Failed to claim" }, 500, port);
|
|
9565
|
+
}
|
|
9566
|
+
}
|
|
9410
9567
|
if (path === "/api/agents" && method === "GET") {
|
|
9411
9568
|
return json(listAgents(), 200, port);
|
|
9412
9569
|
}
|
package/dist/index.js
CHANGED
|
@@ -1122,8 +1122,26 @@ function deleteTask(id, db) {
|
|
|
1122
1122
|
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
1123
1123
|
return result.changes > 0;
|
|
1124
1124
|
}
|
|
1125
|
+
function getBlockingDeps(id, db) {
|
|
1126
|
+
const d = db || getDatabase();
|
|
1127
|
+
const deps = getTaskDependencies(id, d);
|
|
1128
|
+
if (deps.length === 0)
|
|
1129
|
+
return [];
|
|
1130
|
+
const blocking = [];
|
|
1131
|
+
for (const dep of deps) {
|
|
1132
|
+
const task = getTask(dep.depends_on, d);
|
|
1133
|
+
if (task && task.status !== "completed")
|
|
1134
|
+
blocking.push(task);
|
|
1135
|
+
}
|
|
1136
|
+
return blocking;
|
|
1137
|
+
}
|
|
1125
1138
|
function startTask(id, agentId, db) {
|
|
1126
1139
|
const d = db || getDatabase();
|
|
1140
|
+
const blocking = getBlockingDeps(id, d);
|
|
1141
|
+
if (blocking.length > 0) {
|
|
1142
|
+
const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
|
|
1143
|
+
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
1144
|
+
}
|
|
1127
1145
|
const cutoff = lockExpiryCutoff();
|
|
1128
1146
|
const timestamp = now();
|
|
1129
1147
|
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
@@ -1139,7 +1157,7 @@ function startTask(id, agentId, db) {
|
|
|
1139
1157
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
1140
1158
|
return getTask(id, d);
|
|
1141
1159
|
}
|
|
1142
|
-
function completeTask(id, agentId, db) {
|
|
1160
|
+
function completeTask(id, agentId, db, evidence) {
|
|
1143
1161
|
const d = db || getDatabase();
|
|
1144
1162
|
const task = getTask(id, d);
|
|
1145
1163
|
if (!task)
|
|
@@ -1148,6 +1166,10 @@ function completeTask(id, agentId, db) {
|
|
|
1148
1166
|
throw new LockError(id, task.locked_by);
|
|
1149
1167
|
}
|
|
1150
1168
|
checkCompletionGuard(task, agentId || null, d);
|
|
1169
|
+
if (evidence) {
|
|
1170
|
+
const meta = { ...task.metadata, _evidence: evidence };
|
|
1171
|
+
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta), id]);
|
|
1172
|
+
}
|
|
1151
1173
|
const timestamp = now();
|
|
1152
1174
|
d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, version = version + 1, updated_at = ?
|
|
1153
1175
|
WHERE id = ?`, [timestamp, timestamp, id]);
|
|
@@ -2233,6 +2255,7 @@ export {
|
|
|
2233
2255
|
getDatabase,
|
|
2234
2256
|
getCompletionGuardConfig,
|
|
2235
2257
|
getComment,
|
|
2258
|
+
getBlockingDeps,
|
|
2236
2259
|
getAgentByName,
|
|
2237
2260
|
getAgent,
|
|
2238
2261
|
ensureTaskList,
|
package/dist/mcp/index.js
CHANGED
|
@@ -5161,8 +5161,26 @@ function deleteTask(id, db) {
|
|
|
5161
5161
|
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
5162
5162
|
return result.changes > 0;
|
|
5163
5163
|
}
|
|
5164
|
+
function getBlockingDeps(id, db) {
|
|
5165
|
+
const d = db || getDatabase();
|
|
5166
|
+
const deps = getTaskDependencies(id, d);
|
|
5167
|
+
if (deps.length === 0)
|
|
5168
|
+
return [];
|
|
5169
|
+
const blocking = [];
|
|
5170
|
+
for (const dep of deps) {
|
|
5171
|
+
const task = getTask(dep.depends_on, d);
|
|
5172
|
+
if (task && task.status !== "completed")
|
|
5173
|
+
blocking.push(task);
|
|
5174
|
+
}
|
|
5175
|
+
return blocking;
|
|
5176
|
+
}
|
|
5164
5177
|
function startTask(id, agentId, db) {
|
|
5165
5178
|
const d = db || getDatabase();
|
|
5179
|
+
const blocking = getBlockingDeps(id, d);
|
|
5180
|
+
if (blocking.length > 0) {
|
|
5181
|
+
const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
|
|
5182
|
+
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
5183
|
+
}
|
|
5166
5184
|
const cutoff = lockExpiryCutoff();
|
|
5167
5185
|
const timestamp = now();
|
|
5168
5186
|
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
@@ -5178,7 +5196,7 @@ function startTask(id, agentId, db) {
|
|
|
5178
5196
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
5179
5197
|
return getTask(id, d);
|
|
5180
5198
|
}
|
|
5181
|
-
function completeTask(id, agentId, db) {
|
|
5199
|
+
function completeTask(id, agentId, db, evidence) {
|
|
5182
5200
|
const d = db || getDatabase();
|
|
5183
5201
|
const task = getTask(id, d);
|
|
5184
5202
|
if (!task)
|
|
@@ -5187,6 +5205,10 @@ function completeTask(id, agentId, db) {
|
|
|
5187
5205
|
throw new LockError(id, task.locked_by);
|
|
5188
5206
|
}
|
|
5189
5207
|
checkCompletionGuard(task, agentId || null, d);
|
|
5208
|
+
if (evidence) {
|
|
5209
|
+
const meta = { ...task.metadata, _evidence: evidence };
|
|
5210
|
+
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta), id]);
|
|
5211
|
+
}
|
|
5190
5212
|
const timestamp = now();
|
|
5191
5213
|
d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, version = version + 1, updated_at = ?
|
|
5192
5214
|
WHERE id = ?`, [timestamp, timestamp, id]);
|
|
@@ -5249,6 +5271,10 @@ function removeDependency(taskId, dependsOn, db) {
|
|
|
5249
5271
|
const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
|
|
5250
5272
|
return result.changes > 0;
|
|
5251
5273
|
}
|
|
5274
|
+
function getTaskDependencies(taskId, db) {
|
|
5275
|
+
const d = db || getDatabase();
|
|
5276
|
+
return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
|
|
5277
|
+
}
|
|
5252
5278
|
function wouldCreateCycle(taskId, dependsOn, db) {
|
|
5253
5279
|
const visited = new Set;
|
|
5254
5280
|
const queue = [dependsOn];
|
|
@@ -6898,6 +6924,39 @@ server.tool("approve_task", "Approve a task that requires approval before comple
|
|
|
6898
6924
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
6899
6925
|
}
|
|
6900
6926
|
});
|
|
6927
|
+
server.tool("get_my_tasks", "Get your assigned tasks and stats. Auto-registers if needed.", {
|
|
6928
|
+
agent_name: exports_external.string().describe("Your agent name")
|
|
6929
|
+
}, async ({ agent_name }) => {
|
|
6930
|
+
try {
|
|
6931
|
+
const agent = registerAgent({ name: agent_name });
|
|
6932
|
+
const tasks = listTasks({});
|
|
6933
|
+
const myTasks = tasks.filter((t) => t.assigned_to === agent_name || t.assigned_to === agent.id || t.agent_id === agent.id || t.agent_id === agent_name);
|
|
6934
|
+
const pending = myTasks.filter((t) => t.status === "pending");
|
|
6935
|
+
const inProgress = myTasks.filter((t) => t.status === "in_progress");
|
|
6936
|
+
const completed = myTasks.filter((t) => t.status === "completed");
|
|
6937
|
+
const rate = myTasks.length > 0 ? Math.round(completed.length / myTasks.length * 100) : 0;
|
|
6938
|
+
const lines = [
|
|
6939
|
+
`Agent: ${agent.name} (${agent.id})`,
|
|
6940
|
+
`Tasks: ${myTasks.length} total, ${pending.length} pending, ${inProgress.length} active, ${completed.length} done (${rate}%)`
|
|
6941
|
+
];
|
|
6942
|
+
if (pending.length > 0) {
|
|
6943
|
+
lines.push(`
|
|
6944
|
+
Pending:`);
|
|
6945
|
+
for (const t of pending.slice(0, 10))
|
|
6946
|
+
lines.push(` [${t.priority}] ${t.id.slice(0, 8)} | ${t.title}`);
|
|
6947
|
+
}
|
|
6948
|
+
if (inProgress.length > 0) {
|
|
6949
|
+
lines.push(`
|
|
6950
|
+
In Progress:`);
|
|
6951
|
+
for (const t of inProgress)
|
|
6952
|
+
lines.push(` ${t.id.slice(0, 8)} | ${t.title}`);
|
|
6953
|
+
}
|
|
6954
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
6955
|
+
`) }] };
|
|
6956
|
+
} catch (e) {
|
|
6957
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
6958
|
+
}
|
|
6959
|
+
});
|
|
6901
6960
|
server.resource("tasks", "todos://tasks", { description: "All active tasks", mimeType: "application/json" }, async () => {
|
|
6902
6961
|
const tasks = listTasks({ status: ["pending", "in_progress"] });
|
|
6903
6962
|
return { contents: [{ uri: "todos://tasks", text: JSON.stringify(tasks, null, 2), mimeType: "application/json" }] };
|
package/dist/server/index.js
CHANGED
|
@@ -1240,8 +1240,26 @@ function deleteTask(id, db) {
|
|
|
1240
1240
|
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
1241
1241
|
return result.changes > 0;
|
|
1242
1242
|
}
|
|
1243
|
+
function getBlockingDeps(id, db) {
|
|
1244
|
+
const d = db || getDatabase();
|
|
1245
|
+
const deps = getTaskDependencies(id, d);
|
|
1246
|
+
if (deps.length === 0)
|
|
1247
|
+
return [];
|
|
1248
|
+
const blocking = [];
|
|
1249
|
+
for (const dep of deps) {
|
|
1250
|
+
const task = getTask(dep.depends_on, d);
|
|
1251
|
+
if (task && task.status !== "completed")
|
|
1252
|
+
blocking.push(task);
|
|
1253
|
+
}
|
|
1254
|
+
return blocking;
|
|
1255
|
+
}
|
|
1243
1256
|
function startTask(id, agentId, db) {
|
|
1244
1257
|
const d = db || getDatabase();
|
|
1258
|
+
const blocking = getBlockingDeps(id, d);
|
|
1259
|
+
if (blocking.length > 0) {
|
|
1260
|
+
const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
|
|
1261
|
+
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
1262
|
+
}
|
|
1245
1263
|
const cutoff = lockExpiryCutoff();
|
|
1246
1264
|
const timestamp = now();
|
|
1247
1265
|
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
@@ -1257,7 +1275,7 @@ function startTask(id, agentId, db) {
|
|
|
1257
1275
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
1258
1276
|
return getTask(id, d);
|
|
1259
1277
|
}
|
|
1260
|
-
function completeTask(id, agentId, db) {
|
|
1278
|
+
function completeTask(id, agentId, db, evidence) {
|
|
1261
1279
|
const d = db || getDatabase();
|
|
1262
1280
|
const task = getTask(id, d);
|
|
1263
1281
|
if (!task)
|
|
@@ -1266,12 +1284,20 @@ function completeTask(id, agentId, db) {
|
|
|
1266
1284
|
throw new LockError(id, task.locked_by);
|
|
1267
1285
|
}
|
|
1268
1286
|
checkCompletionGuard(task, agentId || null, d);
|
|
1287
|
+
if (evidence) {
|
|
1288
|
+
const meta = { ...task.metadata, _evidence: evidence };
|
|
1289
|
+
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta), id]);
|
|
1290
|
+
}
|
|
1269
1291
|
const timestamp = now();
|
|
1270
1292
|
d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, version = version + 1, updated_at = ?
|
|
1271
1293
|
WHERE id = ?`, [timestamp, timestamp, id]);
|
|
1272
1294
|
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
1273
1295
|
return getTask(id, d);
|
|
1274
1296
|
}
|
|
1297
|
+
function getTaskDependencies(taskId, db) {
|
|
1298
|
+
const d = db || getDatabase();
|
|
1299
|
+
return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
|
|
1300
|
+
}
|
|
1275
1301
|
|
|
1276
1302
|
// src/server/serve.ts
|
|
1277
1303
|
init_projects();
|
|
@@ -1428,6 +1454,19 @@ function taskToSummary(task) {
|
|
|
1428
1454
|
async function startServer(port, options) {
|
|
1429
1455
|
const shouldOpen = options?.open ?? true;
|
|
1430
1456
|
getDatabase();
|
|
1457
|
+
const sseClients = new Set;
|
|
1458
|
+
function broadcastEvent(event) {
|
|
1459
|
+
const data = JSON.stringify({ ...event, timestamp: new Date().toISOString() });
|
|
1460
|
+
for (const controller of sseClients) {
|
|
1461
|
+
try {
|
|
1462
|
+
controller.enqueue(`data: ${data}
|
|
1463
|
+
|
|
1464
|
+
`);
|
|
1465
|
+
} catch {
|
|
1466
|
+
sseClients.delete(controller);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1431
1470
|
const dashboardDir = resolveDashboardDir();
|
|
1432
1471
|
const dashboardExists = existsSync4(dashboardDir);
|
|
1433
1472
|
if (!dashboardExists) {
|
|
@@ -1453,6 +1492,27 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
1453
1492
|
}
|
|
1454
1493
|
});
|
|
1455
1494
|
}
|
|
1495
|
+
if (path === "/api/events" && method === "GET") {
|
|
1496
|
+
const stream = new ReadableStream({
|
|
1497
|
+
start(controller) {
|
|
1498
|
+
sseClients.add(controller);
|
|
1499
|
+
controller.enqueue(`data: ${JSON.stringify({ type: "connected", timestamp: new Date().toISOString() })}
|
|
1500
|
+
|
|
1501
|
+
`);
|
|
1502
|
+
},
|
|
1503
|
+
cancel(controller) {
|
|
1504
|
+
sseClients.delete(controller);
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1507
|
+
return new Response(stream, {
|
|
1508
|
+
headers: {
|
|
1509
|
+
"Content-Type": "text/event-stream",
|
|
1510
|
+
"Cache-Control": "no-cache",
|
|
1511
|
+
Connection: "keep-alive",
|
|
1512
|
+
"Access-Control-Allow-Origin": `http://localhost:${port}`
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1456
1516
|
if (path === "/api/stats" && method === "GET") {
|
|
1457
1517
|
const all = listTasks({ limit: 1e4 });
|
|
1458
1518
|
const projects = listProjects();
|
|
@@ -1490,6 +1550,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
1490
1550
|
priority: body.priority,
|
|
1491
1551
|
project_id: body.project_id
|
|
1492
1552
|
});
|
|
1553
|
+
broadcastEvent({ type: "task", task_id: task.id, action: "created", agent_id: task.agent_id });
|
|
1493
1554
|
return json(taskToSummary(task), 201, port);
|
|
1494
1555
|
} catch (e) {
|
|
1495
1556
|
return json({ error: e instanceof Error ? e.message : "Failed to create task" }, 500, port);
|
|
@@ -1592,6 +1653,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
1592
1653
|
const id = startMatch[1];
|
|
1593
1654
|
try {
|
|
1594
1655
|
const task = startTask(id, "dashboard");
|
|
1656
|
+
broadcastEvent({ type: "task", task_id: task.id, action: "started", agent_id: "dashboard" });
|
|
1595
1657
|
return json(taskToSummary(task), 200, port);
|
|
1596
1658
|
} catch (e) {
|
|
1597
1659
|
return json({ error: e instanceof Error ? e.message : "Failed to start task" }, 500, port);
|
|
@@ -1602,6 +1664,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
1602
1664
|
const id = completeMatch[1];
|
|
1603
1665
|
try {
|
|
1604
1666
|
const task = completeTask(id, "dashboard");
|
|
1667
|
+
broadcastEvent({ type: "task", task_id: task.id, action: "completed", agent_id: "dashboard" });
|
|
1605
1668
|
return json(taskToSummary(task), 200, port);
|
|
1606
1669
|
} catch (e) {
|
|
1607
1670
|
return json({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500, port);
|
|
@@ -1610,6 +1673,67 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
1610
1673
|
if (path === "/api/projects" && method === "GET") {
|
|
1611
1674
|
return json(listProjects(), 200, port);
|
|
1612
1675
|
}
|
|
1676
|
+
if (path === "/api/agents/me" && method === "GET") {
|
|
1677
|
+
const name = url.searchParams.get("name");
|
|
1678
|
+
if (!name)
|
|
1679
|
+
return json({ error: "Missing name param" }, 400, port);
|
|
1680
|
+
const { registerAgent: registerAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
|
|
1681
|
+
const agent = registerAgent2({ name });
|
|
1682
|
+
const tasks = listTasks({ assigned_to: name });
|
|
1683
|
+
const agentIdTasks = listTasks({ agent_id: agent.id });
|
|
1684
|
+
const allTasks = [...tasks, ...agentIdTasks.filter((t) => !tasks.some((tt) => tt.id === t.id))];
|
|
1685
|
+
const pending = allTasks.filter((t) => t.status === "pending");
|
|
1686
|
+
const inProgress = allTasks.filter((t) => t.status === "in_progress");
|
|
1687
|
+
const completed = allTasks.filter((t) => t.status === "completed");
|
|
1688
|
+
return json({
|
|
1689
|
+
agent,
|
|
1690
|
+
pending_tasks: pending.map(taskToSummary),
|
|
1691
|
+
in_progress_tasks: inProgress.map(taskToSummary),
|
|
1692
|
+
stats: {
|
|
1693
|
+
total: allTasks.length,
|
|
1694
|
+
pending: pending.length,
|
|
1695
|
+
in_progress: inProgress.length,
|
|
1696
|
+
completed: completed.length,
|
|
1697
|
+
completion_rate: allTasks.length > 0 ? Math.round(completed.length / allTasks.length * 100) : 0
|
|
1698
|
+
}
|
|
1699
|
+
}, 200, port);
|
|
1700
|
+
}
|
|
1701
|
+
const queueMatch = path.match(/^\/api\/agents\/([^/]+)\/queue$/);
|
|
1702
|
+
if (queueMatch && method === "GET") {
|
|
1703
|
+
const agentId = decodeURIComponent(queueMatch[1]);
|
|
1704
|
+
const pending = listTasks({ status: "pending" });
|
|
1705
|
+
const queue = pending.filter((t) => t.assigned_to === agentId || t.agent_id === agentId || !t.assigned_to && !t.locked_by);
|
|
1706
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
1707
|
+
queue.sort((a, b) => (order[a.priority] ?? 4) - (order[b.priority] ?? 4) || new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
|
1708
|
+
return json(queue.map(taskToSummary), 200, port);
|
|
1709
|
+
}
|
|
1710
|
+
if (path === "/api/tasks/claim" && method === "POST") {
|
|
1711
|
+
try {
|
|
1712
|
+
const body = await req.json();
|
|
1713
|
+
const agentId = body.agent_id || "anonymous";
|
|
1714
|
+
const pending = listTasks({ status: "pending", project_id: body.project_id });
|
|
1715
|
+
const available = pending.filter((t) => !t.locked_by);
|
|
1716
|
+
if (available.length === 0)
|
|
1717
|
+
return json({ task: null }, 200, port);
|
|
1718
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
1719
|
+
available.sort((a, b) => (order[a.priority] ?? 4) - (order[b.priority] ?? 4));
|
|
1720
|
+
const target = available[0];
|
|
1721
|
+
try {
|
|
1722
|
+
const claimed = startTask(target.id, agentId);
|
|
1723
|
+
return json({ task: taskToSummary(claimed) }, 200, port);
|
|
1724
|
+
} catch (e) {
|
|
1725
|
+
const next = available[1] || null;
|
|
1726
|
+
return json({
|
|
1727
|
+
task: null,
|
|
1728
|
+
locked_by: target.locked_by,
|
|
1729
|
+
locked_since: target.locked_at,
|
|
1730
|
+
suggested_task: next ? taskToSummary(next) : null
|
|
1731
|
+
}, 200, port);
|
|
1732
|
+
}
|
|
1733
|
+
} catch (e) {
|
|
1734
|
+
return json({ error: e instanceof Error ? e.message : "Failed to claim" }, 500, port);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1613
1737
|
if (path === "/api/agents" && method === "GET") {
|
|
1614
1738
|
return json(listAgents(), 200, port);
|
|
1615
1739
|
}
|