@hasna/todos 0.10.19 → 0.10.21
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/dist/cli/components/Dashboard.d.ts +7 -0
- package/dist/cli/components/Dashboard.d.ts.map +1 -0
- package/dist/cli/index.js +2151 -559
- package/dist/db/agents.d.ts +19 -4
- package/dist/db/agents.d.ts.map +1 -1
- package/dist/db/audit.d.ts +46 -0
- package/dist/db/audit.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/tasks.d.ts.map +1 -1
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +468 -23
- package/dist/lib/burndown.d.ts +18 -0
- package/dist/lib/burndown.d.ts.map +1 -0
- package/dist/lib/extract.d.ts +51 -0
- package/dist/lib/extract.d.ts.map +1 -0
- package/dist/lib/github.d.ts +25 -0
- package/dist/lib/github.d.ts.map +1 -0
- package/dist/mcp/index.js +1014 -187
- package/dist/server/index.js +129 -25
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -391,6 +391,7 @@ function ensureSchema(db) {
|
|
|
391
391
|
ensureColumn("tasks", "spawned_from_session", "TEXT");
|
|
392
392
|
ensureColumn("tasks", "assigned_by", "TEXT");
|
|
393
393
|
ensureColumn("tasks", "assigned_from_project", "TEXT");
|
|
394
|
+
ensureColumn("tasks", "started_at", "TEXT");
|
|
394
395
|
ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
|
|
395
396
|
ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
|
|
396
397
|
ensureColumn("agents", "reports_to", "TEXT");
|
|
@@ -508,6 +509,16 @@ function resolvePartialId(db, table, partialId) {
|
|
|
508
509
|
return shortIdRows[0].id;
|
|
509
510
|
}
|
|
510
511
|
}
|
|
512
|
+
if (table === "task_lists") {
|
|
513
|
+
const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
|
|
514
|
+
if (slugRow)
|
|
515
|
+
return slugRow.id;
|
|
516
|
+
}
|
|
517
|
+
if (table === "projects") {
|
|
518
|
+
const nameRow = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(partialId.toLowerCase());
|
|
519
|
+
if (nameRow)
|
|
520
|
+
return nameRow.id;
|
|
521
|
+
}
|
|
511
522
|
return null;
|
|
512
523
|
}
|
|
513
524
|
var LOCK_EXPIRY_MINUTES = 30, MIGRATIONS, _db = null;
|
|
@@ -986,6 +997,15 @@ var init_database = __esm(() => {
|
|
|
986
997
|
CREATE INDEX IF NOT EXISTS idx_file_locks_agent ON file_locks(agent_id);
|
|
987
998
|
CREATE INDEX IF NOT EXISTS idx_file_locks_expires ON file_locks(expires_at);
|
|
988
999
|
INSERT OR IGNORE INTO _migrations (id) VALUES (33);
|
|
1000
|
+
`,
|
|
1001
|
+
`
|
|
1002
|
+
ALTER TABLE tasks ADD COLUMN started_at TEXT;
|
|
1003
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (34);
|
|
1004
|
+
`,
|
|
1005
|
+
`
|
|
1006
|
+
ALTER TABLE tasks ADD COLUMN task_type TEXT;
|
|
1007
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type);
|
|
1008
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (35);
|
|
989
1009
|
`
|
|
990
1010
|
];
|
|
991
1011
|
});
|
|
@@ -1273,7 +1293,8 @@ var exports_audit = {};
|
|
|
1273
1293
|
__export(exports_audit, {
|
|
1274
1294
|
logTaskChange: () => logTaskChange,
|
|
1275
1295
|
getTaskHistory: () => getTaskHistory,
|
|
1276
|
-
getRecentActivity: () => getRecentActivity
|
|
1296
|
+
getRecentActivity: () => getRecentActivity,
|
|
1297
|
+
getRecap: () => getRecap
|
|
1277
1298
|
});
|
|
1278
1299
|
function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
1279
1300
|
const d = db || getDatabase();
|
|
@@ -1291,6 +1312,32 @@ function getRecentActivity(limit = 50, db) {
|
|
|
1291
1312
|
const d = db || getDatabase();
|
|
1292
1313
|
return d.query("SELECT * FROM task_history ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
1293
1314
|
}
|
|
1315
|
+
function getRecap(hours = 8, projectId, db) {
|
|
1316
|
+
const d = db || getDatabase();
|
|
1317
|
+
const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
|
1318
|
+
const staleWindow = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
|
1319
|
+
const pf = projectId ? " AND project_id = ?" : "";
|
|
1320
|
+
const tpf = projectId ? " AND t.project_id = ?" : "";
|
|
1321
|
+
const completed = projectId ? d.query(`SELECT id, short_id, title, assigned_to, completed_at, started_at FROM tasks WHERE status = 'completed' AND completed_at > ?${pf} ORDER BY completed_at DESC`).all(since, projectId) : d.query(`SELECT id, short_id, title, assigned_to, completed_at, started_at FROM tasks WHERE status = 'completed' AND completed_at > ? ORDER BY completed_at DESC`).all(since);
|
|
1322
|
+
const created = projectId ? d.query(`SELECT id, short_id, title, agent_id, created_at FROM tasks WHERE created_at > ?${pf} ORDER BY created_at DESC`).all(since, projectId) : d.query(`SELECT id, short_id, title, agent_id, created_at FROM tasks WHERE created_at > ? ORDER BY created_at DESC`).all(since);
|
|
1323
|
+
const in_progress = projectId ? d.query(`SELECT id, short_id, title, assigned_to, started_at FROM tasks WHERE status = 'in_progress' AND project_id = ? ORDER BY updated_at DESC`).all(projectId) : d.query(`SELECT id, short_id, title, assigned_to, started_at FROM tasks WHERE status = 'in_progress' ORDER BY updated_at DESC`).all();
|
|
1324
|
+
const blocked = projectId ? d.query(`SELECT DISTINCT t.id, t.short_id, t.title, t.assigned_to FROM tasks t JOIN task_dependencies td ON td.task_id = t.id JOIN tasks dep ON dep.id = td.depends_on AND dep.status NOT IN ('completed','cancelled') WHERE t.status = 'pending'${tpf}`).all(projectId) : d.query(`SELECT DISTINCT t.id, t.short_id, t.title, t.assigned_to FROM tasks t JOIN task_dependencies td ON td.task_id = t.id JOIN tasks dep ON dep.id = td.depends_on AND dep.status NOT IN ('completed','cancelled') WHERE t.status = 'pending'`).all();
|
|
1325
|
+
const stale = projectId ? d.query(`SELECT id, short_id, title, assigned_to, updated_at FROM tasks WHERE status = 'in_progress' AND updated_at < ? AND project_id = ? ORDER BY updated_at ASC`).all(staleWindow, projectId) : d.query(`SELECT id, short_id, title, assigned_to, updated_at FROM tasks WHERE status = 'in_progress' AND updated_at < ? ORDER BY updated_at ASC`).all(staleWindow);
|
|
1326
|
+
const agents = projectId ? d.query(`SELECT a.name, a.last_seen_at, (SELECT COUNT(*) FROM tasks t WHERE (t.assigned_to = a.id OR t.agent_id = a.id) AND t.status = 'completed' AND t.completed_at > ?${tpf}) as completed_count, (SELECT COUNT(*) FROM tasks t WHERE (t.assigned_to = a.id OR t.agent_id = a.id) AND t.status = 'in_progress'${tpf}) as in_progress_count FROM agents a WHERE a.status = 'active' AND a.last_seen_at > ? ORDER BY completed_count DESC`).all(since, projectId, projectId, since) : d.query(`SELECT a.name, a.last_seen_at, (SELECT COUNT(*) FROM tasks t WHERE (t.assigned_to = a.id OR t.agent_id = a.id) AND t.status = 'completed' AND t.completed_at > ?) as completed_count, (SELECT COUNT(*) FROM tasks t WHERE (t.assigned_to = a.id OR t.agent_id = a.id) AND t.status = 'in_progress') as in_progress_count FROM agents a WHERE a.status = 'active' AND a.last_seen_at > ? ORDER BY completed_count DESC`).all(since, since);
|
|
1327
|
+
return {
|
|
1328
|
+
hours,
|
|
1329
|
+
since,
|
|
1330
|
+
completed: completed.map((r) => ({
|
|
1331
|
+
...r,
|
|
1332
|
+
duration_minutes: r.started_at && r.completed_at ? Math.round((new Date(r.completed_at).getTime() - new Date(r.started_at).getTime()) / 60000) : null
|
|
1333
|
+
})),
|
|
1334
|
+
created,
|
|
1335
|
+
in_progress,
|
|
1336
|
+
blocked,
|
|
1337
|
+
stale,
|
|
1338
|
+
agents
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1294
1341
|
var init_audit = __esm(() => {
|
|
1295
1342
|
init_database();
|
|
1296
1343
|
});
|
|
@@ -1619,8 +1666,8 @@ function createTask(input, db) {
|
|
|
1619
1666
|
const title = shortId ? `${shortId}: ${input.title}` : input.title;
|
|
1620
1667
|
const assignedBy = input.assigned_by || input.agent_id;
|
|
1621
1668
|
const assignedFromProject = input.assigned_from_project || null;
|
|
1622
|
-
d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project)
|
|
1623
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1669
|
+
d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project, task_type)
|
|
1670
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1624
1671
|
id,
|
|
1625
1672
|
shortId,
|
|
1626
1673
|
input.project_id || null,
|
|
@@ -1650,7 +1697,8 @@ function createTask(input, db) {
|
|
|
1650
1697
|
input.reason || null,
|
|
1651
1698
|
input.spawned_from_session || null,
|
|
1652
1699
|
assignedBy || null,
|
|
1653
|
-
assignedFromProject || null
|
|
1700
|
+
assignedFromProject || null,
|
|
1701
|
+
input.task_type || null
|
|
1654
1702
|
]);
|
|
1655
1703
|
if (tags.length > 0) {
|
|
1656
1704
|
insertTaskTags(id, tags, d);
|
|
@@ -1759,6 +1807,15 @@ function listTasks(filter = {}, db) {
|
|
|
1759
1807
|
} else if (filter.has_recurrence === false) {
|
|
1760
1808
|
conditions.push("recurrence_rule IS NULL");
|
|
1761
1809
|
}
|
|
1810
|
+
if (filter.task_type) {
|
|
1811
|
+
if (Array.isArray(filter.task_type)) {
|
|
1812
|
+
conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
|
|
1813
|
+
params.push(...filter.task_type);
|
|
1814
|
+
} else {
|
|
1815
|
+
conditions.push("task_type = ?");
|
|
1816
|
+
params.push(filter.task_type);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1762
1819
|
const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
|
|
1763
1820
|
if (filter.cursor) {
|
|
1764
1821
|
try {
|
|
@@ -1918,6 +1975,10 @@ function updateTask(id, input, db) {
|
|
|
1918
1975
|
sets.push("recurrence_rule = ?");
|
|
1919
1976
|
params.push(input.recurrence_rule);
|
|
1920
1977
|
}
|
|
1978
|
+
if (input.task_type !== undefined) {
|
|
1979
|
+
sets.push("task_type = ?");
|
|
1980
|
+
params.push(input.task_type ?? null);
|
|
1981
|
+
}
|
|
1921
1982
|
params.push(id, input.version);
|
|
1922
1983
|
const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
|
|
1923
1984
|
if (result.changes === 0) {
|
|
@@ -1987,8 +2048,8 @@ function startTask(id, agentId, db) {
|
|
|
1987
2048
|
}
|
|
1988
2049
|
const cutoff = lockExpiryCutoff();
|
|
1989
2050
|
const timestamp = now();
|
|
1990
|
-
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
1991
|
-
WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, id, agentId, cutoff]);
|
|
2051
|
+
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, started_at = COALESCE(started_at, ?), version = version + 1, updated_at = ?
|
|
2052
|
+
WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, timestamp, id, agentId, cutoff]);
|
|
1992
2053
|
if (result.changes === 0) {
|
|
1993
2054
|
if (task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
|
|
1994
2055
|
throw new LockError(id, task.locked_by);
|
|
@@ -1996,7 +2057,7 @@ function startTask(id, agentId, db) {
|
|
|
1996
2057
|
}
|
|
1997
2058
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
1998
2059
|
dispatchWebhook("task.started", { id, agent_id: agentId, title: task.title }, d).catch(() => {});
|
|
1999
|
-
return { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, version: task.version + 1, updated_at: timestamp };
|
|
2060
|
+
return { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, started_at: task.started_at || timestamp, version: task.version + 1, updated_at: timestamp };
|
|
2000
2061
|
}
|
|
2001
2062
|
function completeTask(id, agentId, db, options) {
|
|
2002
2063
|
const d = db || getDatabase();
|
|
@@ -2647,6 +2708,7 @@ __export(exports_agents, {
|
|
|
2647
2708
|
updateAgentActivity: () => updateAgentActivity,
|
|
2648
2709
|
updateAgent: () => updateAgent,
|
|
2649
2710
|
unarchiveAgent: () => unarchiveAgent,
|
|
2711
|
+
releaseAgent: () => releaseAgent,
|
|
2650
2712
|
registerAgent: () => registerAgent,
|
|
2651
2713
|
matchCapabilities: () => matchCapabilities,
|
|
2652
2714
|
listAgents: () => listAgents,
|
|
@@ -2658,10 +2720,29 @@ __export(exports_agents, {
|
|
|
2658
2720
|
getAgentByName: () => getAgentByName,
|
|
2659
2721
|
getAgent: () => getAgent,
|
|
2660
2722
|
deleteAgent: () => deleteAgent,
|
|
2723
|
+
autoReleaseStaleAgents: () => autoReleaseStaleAgents,
|
|
2661
2724
|
archiveAgent: () => archiveAgent
|
|
2662
2725
|
});
|
|
2726
|
+
function getActiveWindowMs() {
|
|
2727
|
+
const env = process.env["TODOS_AGENT_TIMEOUT_MS"];
|
|
2728
|
+
if (env) {
|
|
2729
|
+
const parsed = parseInt(env, 10);
|
|
2730
|
+
if (!isNaN(parsed) && parsed > 0)
|
|
2731
|
+
return parsed;
|
|
2732
|
+
}
|
|
2733
|
+
return 30 * 60 * 1000;
|
|
2734
|
+
}
|
|
2735
|
+
function autoReleaseStaleAgents(db) {
|
|
2736
|
+
if (process.env["TODOS_AGENT_AUTO_RELEASE"] !== "true")
|
|
2737
|
+
return 0;
|
|
2738
|
+
const d = db || getDatabase();
|
|
2739
|
+
const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
|
|
2740
|
+
const result = d.run("UPDATE agents SET session_id = NULL WHERE status = 'active' AND session_id IS NOT NULL AND last_seen_at < ?", [cutoff]);
|
|
2741
|
+
return result.changes;
|
|
2742
|
+
}
|
|
2663
2743
|
function getAvailableNamesFromPool(pool, db) {
|
|
2664
|
-
|
|
2744
|
+
autoReleaseStaleAgents(db);
|
|
2745
|
+
const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
|
|
2665
2746
|
const activeNames = new Set(db.query("SELECT name FROM agents WHERE status = 'active' AND last_seen_at > ?").all(cutoff).map((r) => r.name.toLowerCase()));
|
|
2666
2747
|
return pool.filter((name) => !activeNames.has(name.toLowerCase()));
|
|
2667
2748
|
}
|
|
@@ -2683,22 +2764,19 @@ function registerAgent(input, db) {
|
|
|
2683
2764
|
const existing = getAgentByName(normalizedName, d);
|
|
2684
2765
|
if (existing) {
|
|
2685
2766
|
const lastSeenMs = new Date(existing.last_seen_at).getTime();
|
|
2686
|
-
const
|
|
2767
|
+
const activeWindowMs = getActiveWindowMs();
|
|
2768
|
+
const isActive = Date.now() - lastSeenMs < activeWindowMs;
|
|
2687
2769
|
const sameSession = input.session_id && existing.session_id && input.session_id === existing.session_id;
|
|
2688
2770
|
const differentSession = input.session_id && existing.session_id && input.session_id !== existing.session_id;
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
working_dir: existing.working_dir,
|
|
2699
|
-
suggestions: suggestions.slice(0, 5),
|
|
2700
|
-
message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session \u2026${existing.session_id?.slice(-4)}, dir: ${existing.working_dir ?? "unknown"}). Cannot reclaim an active agent \u2014 choose a different name, or wait for the session to go stale.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
|
|
2701
|
-
};
|
|
2771
|
+
const callerHasNoSession = !input.session_id;
|
|
2772
|
+
const existingHasActiveSession = existing.session_id && isActive;
|
|
2773
|
+
if (!input.force) {
|
|
2774
|
+
if (isActive && differentSession) {
|
|
2775
|
+
return buildConflictError(existing, lastSeenMs, input.pool, d);
|
|
2776
|
+
}
|
|
2777
|
+
if (callerHasNoSession && existingHasActiveSession) {
|
|
2778
|
+
return buildConflictError(existing, lastSeenMs, input.pool, d);
|
|
2779
|
+
}
|
|
2702
2780
|
}
|
|
2703
2781
|
const updates = ["last_seen_at = ?", "status = 'active'"];
|
|
2704
2782
|
const params = [now()];
|
|
@@ -2743,6 +2821,32 @@ function registerAgent(input, db) {
|
|
|
2743
2821
|
function isAgentConflict(result) {
|
|
2744
2822
|
return result.conflict === true;
|
|
2745
2823
|
}
|
|
2824
|
+
function buildConflictError(existing, lastSeenMs, pool, d) {
|
|
2825
|
+
const minutesAgo = Math.round((Date.now() - lastSeenMs) / 60000);
|
|
2826
|
+
const suggestions = pool ? getAvailableNamesFromPool(pool, d) : [];
|
|
2827
|
+
return {
|
|
2828
|
+
conflict: true,
|
|
2829
|
+
existing_id: existing.id,
|
|
2830
|
+
existing_name: existing.name,
|
|
2831
|
+
last_seen_at: existing.last_seen_at,
|
|
2832
|
+
session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
|
|
2833
|
+
working_dir: existing.working_dir,
|
|
2834
|
+
suggestions: suggestions.slice(0, 5),
|
|
2835
|
+
message: `Agent "${existing.name}" is already active (last seen ${minutesAgo}m ago, session ${existing.session_id ? "\u2026" + existing.session_id.slice(-4) : "none"}, dir: ${existing.working_dir ?? "unknown"}). Pass force: true to take over, or choose a different name.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2838
|
+
function releaseAgent(id, session_id, db) {
|
|
2839
|
+
const d = db || getDatabase();
|
|
2840
|
+
const agent = getAgent(id, d);
|
|
2841
|
+
if (!agent)
|
|
2842
|
+
return false;
|
|
2843
|
+
if (session_id && agent.session_id && agent.session_id !== session_id) {
|
|
2844
|
+
return false;
|
|
2845
|
+
}
|
|
2846
|
+
const epoch = new Date(0).toISOString();
|
|
2847
|
+
d.run("UPDATE agents SET session_id = NULL, last_seen_at = ? WHERE id = ?", [epoch, id]);
|
|
2848
|
+
return true;
|
|
2849
|
+
}
|
|
2746
2850
|
function getAgent(id, db) {
|
|
2747
2851
|
const d = db || getDatabase();
|
|
2748
2852
|
const row = d.query("SELECT * FROM agents WHERE id = ?").get(id);
|
|
@@ -2763,6 +2867,7 @@ function listAgents(opts, db) {
|
|
|
2763
2867
|
includeArchived = opts?.include_archived ?? false;
|
|
2764
2868
|
d = db || getDatabase();
|
|
2765
2869
|
}
|
|
2870
|
+
autoReleaseStaleAgents(d);
|
|
2766
2871
|
if (includeArchived) {
|
|
2767
2872
|
return d.query("SELECT * FROM agents ORDER BY name").all().map(rowToAgent);
|
|
2768
2873
|
}
|
|
@@ -2780,8 +2885,19 @@ function updateAgent(id, input, db) {
|
|
|
2780
2885
|
const sets = ["last_seen_at = ?"];
|
|
2781
2886
|
const params = [now()];
|
|
2782
2887
|
if (input.name !== undefined) {
|
|
2888
|
+
const newName = input.name.trim().toLowerCase();
|
|
2889
|
+
const holder = getAgentByName(newName, d);
|
|
2890
|
+
if (holder && holder.id !== id) {
|
|
2891
|
+
const lastSeenMs = new Date(holder.last_seen_at).getTime();
|
|
2892
|
+
const isActive = Date.now() - lastSeenMs < getActiveWindowMs();
|
|
2893
|
+
if (isActive && holder.status === "active") {
|
|
2894
|
+
throw new Error(`Cannot rename: name "${newName}" is held by active agent ${holder.id} (last seen ${Math.round((Date.now() - lastSeenMs) / 60000)}m ago)`);
|
|
2895
|
+
}
|
|
2896
|
+
const evictedName = `${holder.name}__evicted_${holder.id}`;
|
|
2897
|
+
d.run("UPDATE agents SET name = ? WHERE id = ?", [evictedName, holder.id]);
|
|
2898
|
+
}
|
|
2783
2899
|
sets.push("name = ?");
|
|
2784
|
-
params.push(
|
|
2900
|
+
params.push(newName);
|
|
2785
2901
|
}
|
|
2786
2902
|
if (input.description !== undefined) {
|
|
2787
2903
|
sets.push("description = ?");
|
|
@@ -2878,10 +2994,8 @@ function getCapableAgents(capabilities, opts, db) {
|
|
|
2878
2994
|
})).filter((entry) => entry.score >= minScore).sort((a, b) => b.score - a.score);
|
|
2879
2995
|
return opts?.limit ? scored.slice(0, opts.limit) : scored;
|
|
2880
2996
|
}
|
|
2881
|
-
var AGENT_ACTIVE_WINDOW_MS;
|
|
2882
2997
|
var init_agents = __esm(() => {
|
|
2883
2998
|
init_database();
|
|
2884
|
-
AGENT_ACTIVE_WINDOW_MS = 30 * 60 * 1000;
|
|
2885
2999
|
});
|
|
2886
3000
|
|
|
2887
3001
|
// src/db/task-commits.ts
|
|
@@ -2928,160 +3042,63 @@ var init_task_commits = __esm(() => {
|
|
|
2928
3042
|
init_database();
|
|
2929
3043
|
});
|
|
2930
3044
|
|
|
2931
|
-
// src/lib/
|
|
2932
|
-
var
|
|
2933
|
-
__export(
|
|
2934
|
-
|
|
2935
|
-
|
|
3045
|
+
// src/lib/github.ts
|
|
3046
|
+
var exports_github = {};
|
|
3047
|
+
__export(exports_github, {
|
|
3048
|
+
parseGitHubUrl: () => parseGitHubUrl,
|
|
3049
|
+
issueToTask: () => issueToTask,
|
|
3050
|
+
fetchGitHubIssue: () => fetchGitHubIssue
|
|
2936
3051
|
});
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
const
|
|
2940
|
-
if (
|
|
3052
|
+
import { execSync } from "child_process";
|
|
3053
|
+
function parseGitHubUrl(url) {
|
|
3054
|
+
const match = url.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
|
|
3055
|
+
if (!match)
|
|
2941
3056
|
return null;
|
|
2942
|
-
|
|
2943
|
-
const load = new Map;
|
|
2944
|
-
for (const a of agents)
|
|
2945
|
-
load.set(a.name, 0);
|
|
2946
|
-
for (const t of inProgressTasks) {
|
|
2947
|
-
const name = t.assigned_to || t.agent_id;
|
|
2948
|
-
if (name && load.has(name)) {
|
|
2949
|
-
load.set(name, (load.get(name) || 0) + 1);
|
|
2950
|
-
}
|
|
2951
|
-
}
|
|
2952
|
-
let bestAgent = agents[0].name;
|
|
2953
|
-
let bestLoad = load.get(bestAgent) ?? 0;
|
|
2954
|
-
for (const a of agents) {
|
|
2955
|
-
const l = load.get(a.name) ?? 0;
|
|
2956
|
-
if (l < bestLoad) {
|
|
2957
|
-
bestAgent = a.name;
|
|
2958
|
-
bestLoad = l;
|
|
2959
|
-
}
|
|
2960
|
-
}
|
|
2961
|
-
return bestAgent;
|
|
3057
|
+
return { owner: match[1], repo: match[2], number: parseInt(match[3], 10) };
|
|
2962
3058
|
}
|
|
2963
|
-
function
|
|
2964
|
-
const
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
Tags: ${task.tags.join(", ") || "none"}
|
|
2976
|
-
Description: ${task.description?.slice(0, 300) || "none"}
|
|
2977
|
-
|
|
2978
|
-
AVAILABLE AGENTS:
|
|
2979
|
-
${agentList}
|
|
2980
|
-
|
|
2981
|
-
Rules:
|
|
2982
|
-
- Match task tags/content to agent capabilities
|
|
2983
|
-
- Prefer agents with fewer active tasks
|
|
2984
|
-
- Prefer agents whose role fits the task (lead for critical, developer for features, qa for testing)
|
|
2985
|
-
- If no clear match, pick the agent with fewest active tasks
|
|
2986
|
-
|
|
2987
|
-
Respond with ONLY a JSON object: {"agent_name": "<name>", "reason": "<one sentence>"}`;
|
|
2988
|
-
}
|
|
2989
|
-
async function callCerebras(prompt, apiKey) {
|
|
2990
|
-
try {
|
|
2991
|
-
const resp = await fetch(CEREBRAS_API_URL, {
|
|
2992
|
-
method: "POST",
|
|
2993
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
2994
|
-
body: JSON.stringify({
|
|
2995
|
-
model: CEREBRAS_MODEL,
|
|
2996
|
-
messages: [{ role: "user", content: prompt }],
|
|
2997
|
-
max_tokens: 150,
|
|
2998
|
-
temperature: 0
|
|
2999
|
-
}),
|
|
3000
|
-
signal: AbortSignal.timeout(1e4)
|
|
3001
|
-
});
|
|
3002
|
-
if (!resp.ok)
|
|
3003
|
-
return null;
|
|
3004
|
-
const data = await resp.json();
|
|
3005
|
-
const content = data?.choices?.[0]?.message?.content?.trim();
|
|
3006
|
-
if (!content)
|
|
3007
|
-
return null;
|
|
3008
|
-
const match = content.match(/\{[^}]+\}/s);
|
|
3009
|
-
if (!match)
|
|
3010
|
-
return null;
|
|
3011
|
-
return JSON.parse(match[0]);
|
|
3012
|
-
} catch {
|
|
3013
|
-
return null;
|
|
3014
|
-
}
|
|
3059
|
+
function fetchGitHubIssue(owner, repo, number) {
|
|
3060
|
+
const json = execSync(`gh api repos/${owner}/${repo}/issues/${number}`, { encoding: "utf-8", timeout: 15000 });
|
|
3061
|
+
const data = JSON.parse(json);
|
|
3062
|
+
return {
|
|
3063
|
+
number: data.number,
|
|
3064
|
+
title: data.title,
|
|
3065
|
+
body: data.body,
|
|
3066
|
+
labels: (data.labels || []).map((l) => l.name),
|
|
3067
|
+
state: data.state,
|
|
3068
|
+
assignee: data.assignee?.login || null,
|
|
3069
|
+
url: data.html_url
|
|
3070
|
+
};
|
|
3015
3071
|
}
|
|
3016
|
-
|
|
3017
|
-
const
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
id: a.id,
|
|
3033
|
-
name: a.name,
|
|
3034
|
-
role: a.role || "agent",
|
|
3035
|
-
capabilities: a.capabilities || [],
|
|
3036
|
-
in_progress_tasks: workloads.get(a.id) ?? 0
|
|
3037
|
-
}));
|
|
3038
|
-
const result = await callCerebras(buildPrompt({
|
|
3039
|
-
title: task.title,
|
|
3040
|
-
description: task.description,
|
|
3041
|
-
priority: task.priority,
|
|
3042
|
-
tags: task.tags || []
|
|
3043
|
-
}, agentData), apiKey);
|
|
3044
|
-
if (result?.agent_name) {
|
|
3045
|
-
selectedAgent = agents.find((a) => a.name === result.agent_name) ?? null;
|
|
3046
|
-
if (selectedAgent) {
|
|
3047
|
-
method = "cerebras";
|
|
3048
|
-
reason = result.reason;
|
|
3049
|
-
}
|
|
3050
|
-
}
|
|
3051
|
-
}
|
|
3052
|
-
if (!selectedAgent) {
|
|
3053
|
-
const taskTags = task.tags || [];
|
|
3054
|
-
const capable = getCapableAgents(taskTags, { min_score: 0, limit: 10 }, d);
|
|
3055
|
-
if (capable.length > 0) {
|
|
3056
|
-
const sorted = capable.sort((a, b) => {
|
|
3057
|
-
if (b.score !== a.score)
|
|
3058
|
-
return b.score - a.score;
|
|
3059
|
-
return (workloads.get(a.agent.id) ?? 0) - (workloads.get(b.agent.id) ?? 0);
|
|
3060
|
-
});
|
|
3061
|
-
selectedAgent = sorted[0].agent;
|
|
3062
|
-
reason = `Capability match (score: ${sorted[0].score.toFixed(2)})`;
|
|
3063
|
-
} else {
|
|
3064
|
-
selectedAgent = agents.slice().sort((a, b) => (workloads.get(a.id) ?? 0) - (workloads.get(b.id) ?? 0))[0];
|
|
3065
|
-
reason = `Least busy agent (${workloads.get(selectedAgent.id) ?? 0} active tasks)`;
|
|
3072
|
+
function issueToTask(issue, opts) {
|
|
3073
|
+
const labelToPriority = {
|
|
3074
|
+
critical: "critical",
|
|
3075
|
+
"priority:critical": "critical",
|
|
3076
|
+
high: "high",
|
|
3077
|
+
"priority:high": "high",
|
|
3078
|
+
urgent: "high",
|
|
3079
|
+
low: "low",
|
|
3080
|
+
"priority:low": "low"
|
|
3081
|
+
};
|
|
3082
|
+
let priority = "medium";
|
|
3083
|
+
for (const label of issue.labels) {
|
|
3084
|
+
const mapped = labelToPriority[label.toLowerCase()];
|
|
3085
|
+
if (mapped) {
|
|
3086
|
+
priority = mapped;
|
|
3087
|
+
break;
|
|
3066
3088
|
}
|
|
3067
3089
|
}
|
|
3068
|
-
if (selectedAgent) {
|
|
3069
|
-
updateTask(taskId, { assigned_to: selectedAgent.id, version: task.version }, d);
|
|
3070
|
-
}
|
|
3071
3090
|
return {
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3091
|
+
title: `[GH#${issue.number}] ${issue.title}`,
|
|
3092
|
+
description: issue.body ? issue.body.slice(0, 4000) : undefined,
|
|
3093
|
+
tags: issue.labels.slice(0, 10),
|
|
3094
|
+
priority,
|
|
3095
|
+
metadata: { github_url: issue.url, github_number: issue.number, github_state: issue.state },
|
|
3096
|
+
project_id: opts?.project_id,
|
|
3097
|
+
task_list_id: opts?.task_list_id,
|
|
3098
|
+
agent_id: opts?.agent_id
|
|
3077
3099
|
};
|
|
3078
3100
|
}
|
|
3079
|
-
var
|
|
3080
|
-
var init_auto_assign = __esm(() => {
|
|
3081
|
-
init_database();
|
|
3082
|
-
init_tasks();
|
|
3083
|
-
init_agents();
|
|
3084
|
-
});
|
|
3101
|
+
var init_github = () => {};
|
|
3085
3102
|
|
|
3086
3103
|
// src/db/task-files.ts
|
|
3087
3104
|
var exports_task_files = {};
|
|
@@ -3241,12 +3258,254 @@ function bulkAddTaskFiles(taskId, paths, agentId, db) {
|
|
|
3241
3258
|
for (const path of paths) {
|
|
3242
3259
|
results.push(addTaskFile({ task_id: taskId, path, agent_id: agentId }, d));
|
|
3243
3260
|
}
|
|
3244
|
-
});
|
|
3245
|
-
tx();
|
|
3246
|
-
return results;
|
|
3261
|
+
});
|
|
3262
|
+
tx();
|
|
3263
|
+
return results;
|
|
3264
|
+
}
|
|
3265
|
+
var init_task_files = __esm(() => {
|
|
3266
|
+
init_database();
|
|
3267
|
+
});
|
|
3268
|
+
|
|
3269
|
+
// src/lib/burndown.ts
|
|
3270
|
+
var exports_burndown = {};
|
|
3271
|
+
__export(exports_burndown, {
|
|
3272
|
+
getBurndown: () => getBurndown
|
|
3273
|
+
});
|
|
3274
|
+
function getBurndown(opts, db) {
|
|
3275
|
+
const d = db || getDatabase();
|
|
3276
|
+
const conditions = [];
|
|
3277
|
+
const params = [];
|
|
3278
|
+
if (opts.plan_id) {
|
|
3279
|
+
conditions.push("plan_id = ?");
|
|
3280
|
+
params.push(opts.plan_id);
|
|
3281
|
+
}
|
|
3282
|
+
if (opts.project_id) {
|
|
3283
|
+
conditions.push("project_id = ?");
|
|
3284
|
+
params.push(opts.project_id);
|
|
3285
|
+
}
|
|
3286
|
+
if (opts.task_list_id) {
|
|
3287
|
+
conditions.push("task_list_id = ?");
|
|
3288
|
+
params.push(opts.task_list_id);
|
|
3289
|
+
}
|
|
3290
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
3291
|
+
const total = d.query(`SELECT COUNT(*) as c FROM tasks ${where}`).get(...params).c;
|
|
3292
|
+
const completed = d.query(`SELECT COUNT(*) as c FROM tasks ${where}${where ? " AND" : " WHERE"} status = 'completed'`).get(...params).c;
|
|
3293
|
+
const completions = d.query(`SELECT DATE(completed_at) as date, COUNT(*) as count FROM tasks ${where}${where ? " AND" : " WHERE"} status = 'completed' AND completed_at IS NOT NULL GROUP BY DATE(completed_at) ORDER BY date`).all(...params);
|
|
3294
|
+
const firstTask = d.query(`SELECT MIN(created_at) as min_date FROM tasks ${where}`).get(...params);
|
|
3295
|
+
const startDate = firstTask?.min_date ? new Date(firstTask.min_date) : new Date;
|
|
3296
|
+
const endDate = new Date;
|
|
3297
|
+
const days = [];
|
|
3298
|
+
const totalDays = Math.max(1, Math.ceil((endDate.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000)));
|
|
3299
|
+
let cumulative = 0;
|
|
3300
|
+
const completionMap = new Map(completions.map((c) => [c.date, c.count]));
|
|
3301
|
+
const current = new Date(startDate);
|
|
3302
|
+
for (let i = 0;i <= totalDays; i++) {
|
|
3303
|
+
const dateStr = current.toISOString().slice(0, 10);
|
|
3304
|
+
cumulative += completionMap.get(dateStr) || 0;
|
|
3305
|
+
days.push({
|
|
3306
|
+
date: dateStr,
|
|
3307
|
+
completed_cumulative: cumulative,
|
|
3308
|
+
ideal: Math.round(total / totalDays * i)
|
|
3309
|
+
});
|
|
3310
|
+
current.setDate(current.getDate() + 1);
|
|
3311
|
+
}
|
|
3312
|
+
const chart = renderBurndownChart(total, days);
|
|
3313
|
+
return { total, completed, remaining: total - completed, days, chart };
|
|
3314
|
+
}
|
|
3315
|
+
function renderBurndownChart(total, days) {
|
|
3316
|
+
const height = 12;
|
|
3317
|
+
const width = Math.min(60, days.length);
|
|
3318
|
+
const step = Math.max(1, Math.floor(days.length / width));
|
|
3319
|
+
const sampled = days.filter((_, i) => i % step === 0 || i === days.length - 1).slice(0, width);
|
|
3320
|
+
const lines = [];
|
|
3321
|
+
lines.push(` ${total} \u2524`);
|
|
3322
|
+
for (let row = height - 1;row >= 0; row--) {
|
|
3323
|
+
const threshold = Math.round(total / height * row);
|
|
3324
|
+
let line = "";
|
|
3325
|
+
for (const day of sampled) {
|
|
3326
|
+
const remaining = total - day.completed_cumulative;
|
|
3327
|
+
const idealRemaining = total - day.ideal;
|
|
3328
|
+
if (remaining >= threshold && remaining > threshold - Math.round(total / height)) {
|
|
3329
|
+
line += "\u2588";
|
|
3330
|
+
} else if (idealRemaining >= threshold && idealRemaining > threshold - Math.round(total / height)) {
|
|
3331
|
+
line += "\xB7";
|
|
3332
|
+
} else {
|
|
3333
|
+
line += " ";
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
const label = String(threshold).padStart(4);
|
|
3337
|
+
lines.push(`${label} \u2524${line}`);
|
|
3338
|
+
}
|
|
3339
|
+
lines.push(` 0 \u2524${"\u2500".repeat(sampled.length)}`);
|
|
3340
|
+
lines.push(` \u2514${"\u2500".repeat(sampled.length)}`);
|
|
3341
|
+
if (sampled.length > 0) {
|
|
3342
|
+
const first = sampled[0].date.slice(5);
|
|
3343
|
+
const last = sampled[sampled.length - 1].date.slice(5);
|
|
3344
|
+
const pad = sampled.length - first.length - last.length;
|
|
3345
|
+
lines.push(` ${first}${" ".repeat(Math.max(1, pad))}${last}`);
|
|
3346
|
+
}
|
|
3347
|
+
lines.push("");
|
|
3348
|
+
lines.push(` \u2588 actual remaining \xB7 ideal burndown`);
|
|
3349
|
+
return lines.join(`
|
|
3350
|
+
`);
|
|
3351
|
+
}
|
|
3352
|
+
var init_burndown = __esm(() => {
|
|
3353
|
+
init_database();
|
|
3354
|
+
});
|
|
3355
|
+
|
|
3356
|
+
// src/lib/auto-assign.ts
|
|
3357
|
+
var exports_auto_assign = {};
|
|
3358
|
+
__export(exports_auto_assign, {
|
|
3359
|
+
findBestAgent: () => findBestAgent,
|
|
3360
|
+
autoAssignTask: () => autoAssignTask
|
|
3361
|
+
});
|
|
3362
|
+
function findBestAgent(_task, db) {
|
|
3363
|
+
const d = db || getDatabase();
|
|
3364
|
+
const agents = listAgents(d).filter((a) => (a.role || "agent") === "agent");
|
|
3365
|
+
if (agents.length === 0)
|
|
3366
|
+
return null;
|
|
3367
|
+
const inProgressTasks = listTasks({ status: "in_progress" }, d);
|
|
3368
|
+
const load = new Map;
|
|
3369
|
+
for (const a of agents)
|
|
3370
|
+
load.set(a.name, 0);
|
|
3371
|
+
for (const t of inProgressTasks) {
|
|
3372
|
+
const name = t.assigned_to || t.agent_id;
|
|
3373
|
+
if (name && load.has(name)) {
|
|
3374
|
+
load.set(name, (load.get(name) || 0) + 1);
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
let bestAgent = agents[0].name;
|
|
3378
|
+
let bestLoad = load.get(bestAgent) ?? 0;
|
|
3379
|
+
for (const a of agents) {
|
|
3380
|
+
const l = load.get(a.name) ?? 0;
|
|
3381
|
+
if (l < bestLoad) {
|
|
3382
|
+
bestAgent = a.name;
|
|
3383
|
+
bestLoad = l;
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
return bestAgent;
|
|
3387
|
+
}
|
|
3388
|
+
function getAgentWorkloads(d) {
|
|
3389
|
+
const rows = d.query("SELECT assigned_to, COUNT(*) as count FROM tasks WHERE status = 'in_progress' AND assigned_to IS NOT NULL GROUP BY assigned_to").all();
|
|
3390
|
+
return new Map(rows.map((r) => [r.assigned_to, r.count]));
|
|
3391
|
+
}
|
|
3392
|
+
function buildPrompt(task, agents) {
|
|
3393
|
+
const agentList = agents.map((a) => `- ${a.name} (role: ${a.role}, caps: [${a.capabilities.join(", ")}], active_tasks: ${a.in_progress_tasks})`).join(`
|
|
3394
|
+
`);
|
|
3395
|
+
return `You are a task routing assistant. Given a task and available agents, choose the SINGLE best agent.
|
|
3396
|
+
|
|
3397
|
+
TASK:
|
|
3398
|
+
Title: ${task.title}
|
|
3399
|
+
Priority: ${task.priority}
|
|
3400
|
+
Tags: ${task.tags.join(", ") || "none"}
|
|
3401
|
+
Description: ${task.description?.slice(0, 300) || "none"}
|
|
3402
|
+
|
|
3403
|
+
AVAILABLE AGENTS:
|
|
3404
|
+
${agentList}
|
|
3405
|
+
|
|
3406
|
+
Rules:
|
|
3407
|
+
- Match task tags/content to agent capabilities
|
|
3408
|
+
- Prefer agents with fewer active tasks
|
|
3409
|
+
- Prefer agents whose role fits the task (lead for critical, developer for features, qa for testing)
|
|
3410
|
+
- If no clear match, pick the agent with fewest active tasks
|
|
3411
|
+
|
|
3412
|
+
Respond with ONLY a JSON object: {"agent_name": "<name>", "reason": "<one sentence>"}`;
|
|
3413
|
+
}
|
|
3414
|
+
async function callCerebras(prompt, apiKey) {
|
|
3415
|
+
try {
|
|
3416
|
+
const resp = await fetch(CEREBRAS_API_URL, {
|
|
3417
|
+
method: "POST",
|
|
3418
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
3419
|
+
body: JSON.stringify({
|
|
3420
|
+
model: CEREBRAS_MODEL,
|
|
3421
|
+
messages: [{ role: "user", content: prompt }],
|
|
3422
|
+
max_tokens: 150,
|
|
3423
|
+
temperature: 0
|
|
3424
|
+
}),
|
|
3425
|
+
signal: AbortSignal.timeout(1e4)
|
|
3426
|
+
});
|
|
3427
|
+
if (!resp.ok)
|
|
3428
|
+
return null;
|
|
3429
|
+
const data = await resp.json();
|
|
3430
|
+
const content = data?.choices?.[0]?.message?.content?.trim();
|
|
3431
|
+
if (!content)
|
|
3432
|
+
return null;
|
|
3433
|
+
const match = content.match(/\{[^}]+\}/s);
|
|
3434
|
+
if (!match)
|
|
3435
|
+
return null;
|
|
3436
|
+
return JSON.parse(match[0]);
|
|
3437
|
+
} catch {
|
|
3438
|
+
return null;
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
async function autoAssignTask(taskId, db) {
|
|
3442
|
+
const d = db || getDatabase();
|
|
3443
|
+
const task = getTask(taskId, d);
|
|
3444
|
+
if (!task)
|
|
3445
|
+
throw new Error(`Task ${taskId} not found`);
|
|
3446
|
+
const agents = listAgents(d).filter((a) => a.status === "active");
|
|
3447
|
+
if (agents.length === 0) {
|
|
3448
|
+
return { task_id: taskId, assigned_to: null, agent_name: null, method: "no_agents" };
|
|
3449
|
+
}
|
|
3450
|
+
const workloads = getAgentWorkloads(d);
|
|
3451
|
+
const apiKey = process.env["CEREBRAS_API_KEY"];
|
|
3452
|
+
let selectedAgent = null;
|
|
3453
|
+
let method = "capability_match";
|
|
3454
|
+
let reason;
|
|
3455
|
+
if (apiKey) {
|
|
3456
|
+
const agentData = agents.map((a) => ({
|
|
3457
|
+
id: a.id,
|
|
3458
|
+
name: a.name,
|
|
3459
|
+
role: a.role || "agent",
|
|
3460
|
+
capabilities: a.capabilities || [],
|
|
3461
|
+
in_progress_tasks: workloads.get(a.id) ?? 0
|
|
3462
|
+
}));
|
|
3463
|
+
const result = await callCerebras(buildPrompt({
|
|
3464
|
+
title: task.title,
|
|
3465
|
+
description: task.description,
|
|
3466
|
+
priority: task.priority,
|
|
3467
|
+
tags: task.tags || []
|
|
3468
|
+
}, agentData), apiKey);
|
|
3469
|
+
if (result?.agent_name) {
|
|
3470
|
+
selectedAgent = agents.find((a) => a.name === result.agent_name) ?? null;
|
|
3471
|
+
if (selectedAgent) {
|
|
3472
|
+
method = "cerebras";
|
|
3473
|
+
reason = result.reason;
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
if (!selectedAgent) {
|
|
3478
|
+
const taskTags = task.tags || [];
|
|
3479
|
+
const capable = getCapableAgents(taskTags, { min_score: 0, limit: 10 }, d);
|
|
3480
|
+
if (capable.length > 0) {
|
|
3481
|
+
const sorted = capable.sort((a, b) => {
|
|
3482
|
+
if (b.score !== a.score)
|
|
3483
|
+
return b.score - a.score;
|
|
3484
|
+
return (workloads.get(a.agent.id) ?? 0) - (workloads.get(b.agent.id) ?? 0);
|
|
3485
|
+
});
|
|
3486
|
+
selectedAgent = sorted[0].agent;
|
|
3487
|
+
reason = `Capability match (score: ${sorted[0].score.toFixed(2)})`;
|
|
3488
|
+
} else {
|
|
3489
|
+
selectedAgent = agents.slice().sort((a, b) => (workloads.get(a.id) ?? 0) - (workloads.get(b.id) ?? 0))[0];
|
|
3490
|
+
reason = `Least busy agent (${workloads.get(selectedAgent.id) ?? 0} active tasks)`;
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
if (selectedAgent) {
|
|
3494
|
+
updateTask(taskId, { assigned_to: selectedAgent.id, version: task.version }, d);
|
|
3495
|
+
}
|
|
3496
|
+
return {
|
|
3497
|
+
task_id: taskId,
|
|
3498
|
+
assigned_to: selectedAgent?.id ?? null,
|
|
3499
|
+
agent_name: selectedAgent?.name ?? null,
|
|
3500
|
+
method,
|
|
3501
|
+
reason
|
|
3502
|
+
};
|
|
3247
3503
|
}
|
|
3248
|
-
var
|
|
3504
|
+
var CEREBRAS_API_URL = "https://api.cerebras.ai/v1/chat/completions", CEREBRAS_MODEL = "llama-3.3-70b";
|
|
3505
|
+
var init_auto_assign = __esm(() => {
|
|
3249
3506
|
init_database();
|
|
3507
|
+
init_tasks();
|
|
3508
|
+
init_agents();
|
|
3250
3509
|
});
|
|
3251
3510
|
|
|
3252
3511
|
// src/db/file-locks.ts
|
|
@@ -4041,6 +4300,212 @@ var init_agent_metrics = __esm(() => {
|
|
|
4041
4300
|
init_database();
|
|
4042
4301
|
});
|
|
4043
4302
|
|
|
4303
|
+
// src/lib/extract.ts
|
|
4304
|
+
var exports_extract = {};
|
|
4305
|
+
__export(exports_extract, {
|
|
4306
|
+
tagToPriority: () => tagToPriority,
|
|
4307
|
+
extractTodos: () => extractTodos,
|
|
4308
|
+
extractFromSource: () => extractFromSource,
|
|
4309
|
+
EXTRACT_TAGS: () => EXTRACT_TAGS
|
|
4310
|
+
});
|
|
4311
|
+
import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
4312
|
+
import { relative, resolve as resolve2, join as join6 } from "path";
|
|
4313
|
+
function tagToPriority(tag) {
|
|
4314
|
+
switch (tag) {
|
|
4315
|
+
case "BUG":
|
|
4316
|
+
case "FIXME":
|
|
4317
|
+
return "high";
|
|
4318
|
+
case "HACK":
|
|
4319
|
+
case "XXX":
|
|
4320
|
+
return "medium";
|
|
4321
|
+
case "TODO":
|
|
4322
|
+
return "medium";
|
|
4323
|
+
case "NOTE":
|
|
4324
|
+
return "low";
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
function buildTagRegex(tags) {
|
|
4328
|
+
const tagPattern = tags.join("|");
|
|
4329
|
+
return new RegExp(`(?:^|\\s)(?:\\/\\/|\\/\\*|#|\\*|--|;;|%|<!--|\\{-)\\s*(?:@?)(${tagPattern})\\s*[:(]?\\s*(.*)$`, "i");
|
|
4330
|
+
}
|
|
4331
|
+
function extractFromSource(source, filePath, tags = [...EXTRACT_TAGS]) {
|
|
4332
|
+
const regex = buildTagRegex(tags);
|
|
4333
|
+
const results = [];
|
|
4334
|
+
const lines = source.split(`
|
|
4335
|
+
`);
|
|
4336
|
+
for (let i = 0;i < lines.length; i++) {
|
|
4337
|
+
const line = lines[i];
|
|
4338
|
+
const match = line.match(regex);
|
|
4339
|
+
if (match) {
|
|
4340
|
+
const tag = match[1].toUpperCase();
|
|
4341
|
+
let message = match[2].trim();
|
|
4342
|
+
message = message.replace(/\s*\*\/\s*$/, "").replace(/\s*-->\s*$/, "").replace(/\s*-\}\s*$/, "").trim();
|
|
4343
|
+
if (message) {
|
|
4344
|
+
results.push({
|
|
4345
|
+
tag,
|
|
4346
|
+
message,
|
|
4347
|
+
file: filePath,
|
|
4348
|
+
line: i + 1,
|
|
4349
|
+
raw: line
|
|
4350
|
+
});
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
4354
|
+
return results;
|
|
4355
|
+
}
|
|
4356
|
+
function collectFiles(basePath, extensions) {
|
|
4357
|
+
const stat = statSync2(basePath);
|
|
4358
|
+
if (stat.isFile()) {
|
|
4359
|
+
return [basePath];
|
|
4360
|
+
}
|
|
4361
|
+
const glob = new Bun.Glob("**/*");
|
|
4362
|
+
const files = [];
|
|
4363
|
+
for (const entry of glob.scanSync({ cwd: basePath, onlyFiles: true, dot: false })) {
|
|
4364
|
+
const parts = entry.split("/");
|
|
4365
|
+
if (parts.some((p) => SKIP_DIRS.has(p)))
|
|
4366
|
+
continue;
|
|
4367
|
+
const dotIdx = entry.lastIndexOf(".");
|
|
4368
|
+
if (dotIdx === -1)
|
|
4369
|
+
continue;
|
|
4370
|
+
const ext = entry.slice(dotIdx);
|
|
4371
|
+
if (!extensions.has(ext))
|
|
4372
|
+
continue;
|
|
4373
|
+
files.push(entry);
|
|
4374
|
+
}
|
|
4375
|
+
return files.sort();
|
|
4376
|
+
}
|
|
4377
|
+
function extractTodos(options, db) {
|
|
4378
|
+
const basePath = resolve2(options.path);
|
|
4379
|
+
const tags = options.patterns || [...EXTRACT_TAGS];
|
|
4380
|
+
const extensions = options.extensions ? new Set(options.extensions.map((e) => e.startsWith(".") ? e : `.${e}`)) : DEFAULT_EXTENSIONS;
|
|
4381
|
+
const files = collectFiles(basePath, extensions);
|
|
4382
|
+
const allComments = [];
|
|
4383
|
+
for (const file of files) {
|
|
4384
|
+
const fullPath = statSync2(basePath).isFile() ? basePath : join6(basePath, file);
|
|
4385
|
+
try {
|
|
4386
|
+
const source = readFileSync3(fullPath, "utf-8");
|
|
4387
|
+
const relPath = statSync2(basePath).isFile() ? relative(resolve2(basePath, ".."), fullPath) : file;
|
|
4388
|
+
const comments = extractFromSource(source, relPath, tags);
|
|
4389
|
+
allComments.push(...comments);
|
|
4390
|
+
} catch {}
|
|
4391
|
+
}
|
|
4392
|
+
if (options.dry_run) {
|
|
4393
|
+
return { comments: allComments, tasks: [], skipped: 0 };
|
|
4394
|
+
}
|
|
4395
|
+
const tasks = [];
|
|
4396
|
+
let skipped = 0;
|
|
4397
|
+
const existingTasks = options.project_id ? listTasks({ project_id: options.project_id, tags: ["extracted"] }, db) : listTasks({ tags: ["extracted"] }, db);
|
|
4398
|
+
const existingKeys = new Set;
|
|
4399
|
+
for (const t of existingTasks) {
|
|
4400
|
+
const meta = t.metadata;
|
|
4401
|
+
if (meta?.["source_file"] && meta?.["source_line"]) {
|
|
4402
|
+
existingKeys.add(`${meta["source_file"]}:${meta["source_line"]}`);
|
|
4403
|
+
}
|
|
4404
|
+
}
|
|
4405
|
+
for (const comment of allComments) {
|
|
4406
|
+
const dedupKey = `${comment.file}:${comment.line}`;
|
|
4407
|
+
if (existingKeys.has(dedupKey)) {
|
|
4408
|
+
skipped++;
|
|
4409
|
+
continue;
|
|
4410
|
+
}
|
|
4411
|
+
const taskTags = ["extracted", comment.tag.toLowerCase(), ...options.tags || []];
|
|
4412
|
+
const task = createTask({
|
|
4413
|
+
title: `[${comment.tag}] ${comment.message}`,
|
|
4414
|
+
description: `Extracted from code comment in \`${comment.file}\` at line ${comment.line}:
|
|
4415
|
+
\`\`\`
|
|
4416
|
+
${comment.raw.trim()}
|
|
4417
|
+
\`\`\``,
|
|
4418
|
+
priority: tagToPriority(comment.tag),
|
|
4419
|
+
project_id: options.project_id,
|
|
4420
|
+
task_list_id: options.task_list_id,
|
|
4421
|
+
assigned_to: options.assigned_to,
|
|
4422
|
+
agent_id: options.agent_id,
|
|
4423
|
+
tags: taskTags,
|
|
4424
|
+
metadata: {
|
|
4425
|
+
source: "code_comment",
|
|
4426
|
+
comment_type: comment.tag,
|
|
4427
|
+
source_file: comment.file,
|
|
4428
|
+
source_line: comment.line
|
|
4429
|
+
}
|
|
4430
|
+
}, db);
|
|
4431
|
+
addTaskFile({
|
|
4432
|
+
task_id: task.id,
|
|
4433
|
+
path: comment.file,
|
|
4434
|
+
note: `Line ${comment.line}: ${comment.tag} comment`
|
|
4435
|
+
}, db);
|
|
4436
|
+
tasks.push(task);
|
|
4437
|
+
existingKeys.add(dedupKey);
|
|
4438
|
+
}
|
|
4439
|
+
return { comments: allComments, tasks, skipped };
|
|
4440
|
+
}
|
|
4441
|
+
var EXTRACT_TAGS, DEFAULT_EXTENSIONS, SKIP_DIRS;
|
|
4442
|
+
var init_extract = __esm(() => {
|
|
4443
|
+
init_tasks();
|
|
4444
|
+
init_task_files();
|
|
4445
|
+
EXTRACT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"];
|
|
4446
|
+
DEFAULT_EXTENSIONS = new Set([
|
|
4447
|
+
".ts",
|
|
4448
|
+
".tsx",
|
|
4449
|
+
".js",
|
|
4450
|
+
".jsx",
|
|
4451
|
+
".mjs",
|
|
4452
|
+
".cjs",
|
|
4453
|
+
".py",
|
|
4454
|
+
".rb",
|
|
4455
|
+
".go",
|
|
4456
|
+
".rs",
|
|
4457
|
+
".c",
|
|
4458
|
+
".cpp",
|
|
4459
|
+
".h",
|
|
4460
|
+
".hpp",
|
|
4461
|
+
".java",
|
|
4462
|
+
".kt",
|
|
4463
|
+
".swift",
|
|
4464
|
+
".cs",
|
|
4465
|
+
".php",
|
|
4466
|
+
".sh",
|
|
4467
|
+
".bash",
|
|
4468
|
+
".zsh",
|
|
4469
|
+
".lua",
|
|
4470
|
+
".sql",
|
|
4471
|
+
".r",
|
|
4472
|
+
".R",
|
|
4473
|
+
".yaml",
|
|
4474
|
+
".yml",
|
|
4475
|
+
".toml",
|
|
4476
|
+
".css",
|
|
4477
|
+
".scss",
|
|
4478
|
+
".less",
|
|
4479
|
+
".vue",
|
|
4480
|
+
".svelte",
|
|
4481
|
+
".ex",
|
|
4482
|
+
".exs",
|
|
4483
|
+
".erl",
|
|
4484
|
+
".hs",
|
|
4485
|
+
".ml",
|
|
4486
|
+
".mli",
|
|
4487
|
+
".clj",
|
|
4488
|
+
".cljs"
|
|
4489
|
+
]);
|
|
4490
|
+
SKIP_DIRS = new Set([
|
|
4491
|
+
"node_modules",
|
|
4492
|
+
".git",
|
|
4493
|
+
"dist",
|
|
4494
|
+
"build",
|
|
4495
|
+
"out",
|
|
4496
|
+
".next",
|
|
4497
|
+
".turbo",
|
|
4498
|
+
"coverage",
|
|
4499
|
+
"__pycache__",
|
|
4500
|
+
".venv",
|
|
4501
|
+
"venv",
|
|
4502
|
+
"vendor",
|
|
4503
|
+
"target",
|
|
4504
|
+
".cache",
|
|
4505
|
+
".parcel-cache"
|
|
4506
|
+
]);
|
|
4507
|
+
});
|
|
4508
|
+
|
|
4044
4509
|
// src/mcp/index.ts
|
|
4045
4510
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4046
4511
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -8828,14 +9293,14 @@ init_config();
|
|
|
8828
9293
|
init_database();
|
|
8829
9294
|
init_checklists();
|
|
8830
9295
|
init_types();
|
|
8831
|
-
import { readFileSync as
|
|
8832
|
-
import { join as
|
|
9296
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
9297
|
+
import { join as join7, dirname as dirname2 } from "path";
|
|
8833
9298
|
import { fileURLToPath } from "url";
|
|
8834
9299
|
function getMcpVersion() {
|
|
8835
9300
|
try {
|
|
8836
9301
|
const __dir = dirname2(fileURLToPath(import.meta.url));
|
|
8837
|
-
const pkgPath =
|
|
8838
|
-
return JSON.parse(
|
|
9302
|
+
const pkgPath = join7(__dir, "..", "package.json");
|
|
9303
|
+
return JSON.parse(readFileSync4(pkgPath, "utf-8")).version || "0.0.0";
|
|
8839
9304
|
} catch {
|
|
8840
9305
|
return "0.0.0";
|
|
8841
9306
|
}
|
|
@@ -8857,7 +9322,8 @@ var MINIMAL_TOOLS = new Set([
|
|
|
8857
9322
|
"get_next_task",
|
|
8858
9323
|
"bootstrap",
|
|
8859
9324
|
"get_tasks_changed_since",
|
|
8860
|
-
"heartbeat"
|
|
9325
|
+
"heartbeat",
|
|
9326
|
+
"release_agent"
|
|
8861
9327
|
]);
|
|
8862
9328
|
var STANDARD_EXCLUDED = new Set([
|
|
8863
9329
|
"rename_agent",
|
|
@@ -9018,7 +9484,8 @@ if (shouldRegisterTool("create_task")) {
|
|
|
9018
9484
|
spawns_template_id: exports_external.string().optional().describe("Template ID to auto-create as next task when this task is completed (pipeline/handoff chains)"),
|
|
9019
9485
|
reason: exports_external.string().optional().describe("Why this task exists \u2014 context for agents picking it up"),
|
|
9020
9486
|
spawned_from_session: exports_external.string().optional().describe("Session ID that created this task (for tracing task lineage)"),
|
|
9021
|
-
assigned_from_project: exports_external.string().optional().describe("Override: project ID the assigning agent is working from. Auto-detected from agent focus if omitted.")
|
|
9487
|
+
assigned_from_project: exports_external.string().optional().describe("Override: project ID the assigning agent is working from. Auto-detected from agent focus if omitted."),
|
|
9488
|
+
task_type: exports_external.string().optional().describe("Task type: bug, feature, chore, improvement, docs, test, security, or any custom string")
|
|
9022
9489
|
}, async (params) => {
|
|
9023
9490
|
try {
|
|
9024
9491
|
if (!params.agent_id) {
|
|
@@ -9064,6 +9531,7 @@ if (shouldRegisterTool("list_tasks")) {
|
|
|
9064
9531
|
has_recurrence: exports_external.boolean().optional(),
|
|
9065
9532
|
due_today: exports_external.boolean().optional(),
|
|
9066
9533
|
overdue: exports_external.boolean().optional(),
|
|
9534
|
+
task_type: exports_external.union([exports_external.string(), exports_external.array(exports_external.string())]).optional().describe("Filter by task type: bug, feature, chore, improvement, docs, test, security, or custom"),
|
|
9067
9535
|
limit: exports_external.number().optional(),
|
|
9068
9536
|
offset: exports_external.number().optional(),
|
|
9069
9537
|
summary_only: exports_external.boolean().optional().describe("When true, return only id, short_id, title, status, priority \u2014 minimal tokens for navigation"),
|
|
@@ -9198,11 +9666,17 @@ if (shouldRegisterTool("update_task")) {
|
|
|
9198
9666
|
tags: exports_external.array(exports_external.string()).optional(),
|
|
9199
9667
|
metadata: exports_external.record(exports_external.unknown()).optional(),
|
|
9200
9668
|
plan_id: exports_external.string().optional(),
|
|
9201
|
-
task_list_id: exports_external.string().optional()
|
|
9669
|
+
task_list_id: exports_external.string().optional(),
|
|
9670
|
+
task_type: exports_external.string().nullable().optional().describe("Task type: bug, feature, chore, improvement, docs, test, security, or custom. null to clear.")
|
|
9202
9671
|
}, async ({ id, ...rest }) => {
|
|
9203
9672
|
try {
|
|
9204
9673
|
const resolvedId = resolveId(id);
|
|
9205
|
-
const
|
|
9674
|
+
const resolved = { ...rest };
|
|
9675
|
+
if (resolved.task_list_id)
|
|
9676
|
+
resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
|
|
9677
|
+
if (resolved.plan_id)
|
|
9678
|
+
resolved.plan_id = resolveId(resolved.plan_id, "plans");
|
|
9679
|
+
const task = updateTask(resolvedId, resolved);
|
|
9206
9680
|
return { content: [{ type: "text", text: `updated: ${formatTask(task)}` }] };
|
|
9207
9681
|
} catch (e) {
|
|
9208
9682
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
@@ -9842,11 +10316,12 @@ if (shouldRegisterTool("register_agent")) {
|
|
|
9842
10316
|
description: exports_external.string().optional(),
|
|
9843
10317
|
capabilities: exports_external.array(exports_external.string()).optional().describe("Agent capabilities/skills for task routing (e.g. ['typescript', 'testing', 'devops'])"),
|
|
9844
10318
|
session_id: exports_external.string().optional().describe("Unique ID for this coding session (e.g. process PID + timestamp, or env var). Used to detect name collisions across sessions. Store it and pass on every register_agent call."),
|
|
9845
|
-
working_dir: exports_external.string().optional().describe("Working directory of this session \u2014 used to look up the project's agent pool and identify who holds the name in a conflict")
|
|
9846
|
-
|
|
10319
|
+
working_dir: exports_external.string().optional().describe("Working directory of this session \u2014 used to look up the project's agent pool and identify who holds the name in a conflict"),
|
|
10320
|
+
force: exports_external.boolean().optional().describe("Force takeover of an active agent's name. Use with caution \u2014 only when you know the previous session is dead.")
|
|
10321
|
+
}, async ({ name, description, capabilities, session_id, working_dir, force }) => {
|
|
9847
10322
|
try {
|
|
9848
10323
|
const pool = getAgentPoolForProject(working_dir);
|
|
9849
|
-
const result = registerAgent({ name, description, capabilities, session_id, working_dir, pool: pool || undefined });
|
|
10324
|
+
const result = registerAgent({ name, description, capabilities, session_id, working_dir, force, pool: pool || undefined });
|
|
9850
10325
|
if (isAgentConflict(result)) {
|
|
9851
10326
|
const suggestLine = result.suggestions && result.suggestions.length > 0 ? `
|
|
9852
10327
|
Available names: ${result.suggestions.join(", ")}` : "";
|
|
@@ -10065,6 +10540,31 @@ if (shouldRegisterTool("heartbeat")) {
|
|
|
10065
10540
|
}
|
|
10066
10541
|
});
|
|
10067
10542
|
}
|
|
10543
|
+
if (shouldRegisterTool("release_agent")) {
|
|
10544
|
+
server.tool("release_agent", "Explicitly release/logout an agent \u2014 clears session binding and makes the name immediately available. Call this when your session ends instead of waiting for the 30-minute stale timeout.", {
|
|
10545
|
+
agent_id: exports_external.string().describe("Your agent ID or name."),
|
|
10546
|
+
session_id: exports_external.string().optional().describe("Your session ID \u2014 if provided, release only succeeds if it matches (prevents other sessions from releasing your agent).")
|
|
10547
|
+
}, async ({ agent_id, session_id }) => {
|
|
10548
|
+
try {
|
|
10549
|
+
const agent = getAgent(agent_id) || getAgentByName(agent_id);
|
|
10550
|
+
if (!agent) {
|
|
10551
|
+
return { content: [{ type: "text", text: `Agent not found: ${agent_id}` }], isError: true };
|
|
10552
|
+
}
|
|
10553
|
+
const released = releaseAgent(agent.id, session_id);
|
|
10554
|
+
if (!released) {
|
|
10555
|
+
return { content: [{ type: "text", text: `Release denied: session_id does not match agent's current session.` }], isError: true };
|
|
10556
|
+
}
|
|
10557
|
+
return {
|
|
10558
|
+
content: [{
|
|
10559
|
+
type: "text",
|
|
10560
|
+
text: `Agent released: ${agent.name} (${agent.id}) \u2014 session cleared, name is now available.`
|
|
10561
|
+
}]
|
|
10562
|
+
};
|
|
10563
|
+
} catch (e) {
|
|
10564
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
10565
|
+
}
|
|
10566
|
+
});
|
|
10567
|
+
}
|
|
10068
10568
|
if (shouldRegisterTool("create_task_list")) {
|
|
10069
10569
|
server.tool("create_task_list", "Create a task list container for organizing tasks.", {
|
|
10070
10570
|
name: exports_external.string(),
|
|
@@ -10223,6 +10723,178 @@ ${text}` }] };
|
|
|
10223
10723
|
}
|
|
10224
10724
|
});
|
|
10225
10725
|
}
|
|
10726
|
+
if (shouldRegisterTool("recap")) {
|
|
10727
|
+
server.tool("recap", "Get a summary of what happened in the last N hours \u2014 completed tasks with duration, new tasks, in-progress work, blockers, stale tasks, and agent activity. Great for session start or standup prep.", {
|
|
10728
|
+
hours: exports_external.number().optional().describe("Look back N hours (default: 8)"),
|
|
10729
|
+
project_id: exports_external.string().optional().describe("Filter to a specific project")
|
|
10730
|
+
}, async ({ hours, project_id }) => {
|
|
10731
|
+
try {
|
|
10732
|
+
const { getRecap: getRecap2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
|
|
10733
|
+
const recap = getRecap2(hours || 8, project_id);
|
|
10734
|
+
const lines = [`Recap \u2014 last ${recap.hours}h (since ${recap.since})`];
|
|
10735
|
+
if (recap.completed.length > 0) {
|
|
10736
|
+
lines.push(`
|
|
10737
|
+
Completed (${recap.completed.length}):`);
|
|
10738
|
+
for (const t of recap.completed) {
|
|
10739
|
+
const dur = t.duration_minutes != null ? ` (${t.duration_minutes}m)` : "";
|
|
10740
|
+
lines.push(` \u2713 ${t.short_id || t.id.slice(0, 8)} ${t.title}${dur}${t.assigned_to ? ` \u2014 ${t.assigned_to}` : ""}`);
|
|
10741
|
+
}
|
|
10742
|
+
}
|
|
10743
|
+
if (recap.in_progress.length > 0) {
|
|
10744
|
+
lines.push(`
|
|
10745
|
+
In Progress (${recap.in_progress.length}):`);
|
|
10746
|
+
for (const t of recap.in_progress)
|
|
10747
|
+
lines.push(` \u2192 ${t.short_id || t.id.slice(0, 8)} ${t.title}${t.assigned_to ? ` \u2014 ${t.assigned_to}` : ""}`);
|
|
10748
|
+
}
|
|
10749
|
+
if (recap.blocked.length > 0) {
|
|
10750
|
+
lines.push(`
|
|
10751
|
+
Blocked (${recap.blocked.length}):`);
|
|
10752
|
+
for (const t of recap.blocked)
|
|
10753
|
+
lines.push(` \u2717 ${t.short_id || t.id.slice(0, 8)} ${t.title}`);
|
|
10754
|
+
}
|
|
10755
|
+
if (recap.stale.length > 0) {
|
|
10756
|
+
lines.push(`
|
|
10757
|
+
Stale (${recap.stale.length}):`);
|
|
10758
|
+
for (const t of recap.stale)
|
|
10759
|
+
lines.push(` ! ${t.short_id || t.id.slice(0, 8)} ${t.title} \u2014 updated ${t.updated_at}`);
|
|
10760
|
+
}
|
|
10761
|
+
if (recap.agents.length > 0) {
|
|
10762
|
+
lines.push(`
|
|
10763
|
+
Agents:`);
|
|
10764
|
+
for (const a of recap.agents)
|
|
10765
|
+
lines.push(` ${a.name}: ${a.completed_count} done, ${a.in_progress_count} active (seen ${a.last_seen_at})`);
|
|
10766
|
+
}
|
|
10767
|
+
lines.push(`
|
|
10768
|
+
Created: ${recap.created.length} new tasks`);
|
|
10769
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
10770
|
+
`) }] };
|
|
10771
|
+
} catch (e) {
|
|
10772
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
10773
|
+
}
|
|
10774
|
+
});
|
|
10775
|
+
}
|
|
10776
|
+
if (shouldRegisterTool("standup")) {
|
|
10777
|
+
server.tool("standup", "Generate standup notes \u2014 completed tasks since yesterday grouped by agent, in-progress work, and blockers. Copy-paste ready.", {
|
|
10778
|
+
hours: exports_external.number().optional().describe("Look back N hours (default: 24)"),
|
|
10779
|
+
project_id: exports_external.string().optional()
|
|
10780
|
+
}, async ({ hours, project_id }) => {
|
|
10781
|
+
try {
|
|
10782
|
+
const { getRecap: getRecap2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
|
|
10783
|
+
const recap = getRecap2(hours || 24, project_id);
|
|
10784
|
+
const lines = [`Standup \u2014 last ${recap.hours}h`];
|
|
10785
|
+
const byAgent = new Map;
|
|
10786
|
+
for (const t of recap.completed) {
|
|
10787
|
+
const agent = t.assigned_to || "unassigned";
|
|
10788
|
+
if (!byAgent.has(agent))
|
|
10789
|
+
byAgent.set(agent, []);
|
|
10790
|
+
byAgent.get(agent).push(t);
|
|
10791
|
+
}
|
|
10792
|
+
if (byAgent.size > 0) {
|
|
10793
|
+
lines.push(`
|
|
10794
|
+
Done:`);
|
|
10795
|
+
for (const [agent, tasks] of byAgent) {
|
|
10796
|
+
lines.push(` ${agent}:`);
|
|
10797
|
+
for (const t of tasks) {
|
|
10798
|
+
const dur = t.duration_minutes != null ? ` (${t.duration_minutes}m)` : "";
|
|
10799
|
+
lines.push(` \u2713 ${t.short_id || t.id.slice(0, 8)} ${t.title}${dur}`);
|
|
10800
|
+
}
|
|
10801
|
+
}
|
|
10802
|
+
} else {
|
|
10803
|
+
lines.push(`
|
|
10804
|
+
Nothing completed.`);
|
|
10805
|
+
}
|
|
10806
|
+
if (recap.in_progress.length > 0) {
|
|
10807
|
+
lines.push(`
|
|
10808
|
+
In Progress:`);
|
|
10809
|
+
for (const t of recap.in_progress)
|
|
10810
|
+
lines.push(` \u2192 ${t.short_id || t.id.slice(0, 8)} ${t.title}${t.assigned_to ? ` \u2014 ${t.assigned_to}` : ""}`);
|
|
10811
|
+
}
|
|
10812
|
+
if (recap.blocked.length > 0) {
|
|
10813
|
+
lines.push(`
|
|
10814
|
+
Blocked:`);
|
|
10815
|
+
for (const t of recap.blocked)
|
|
10816
|
+
lines.push(` \u2717 ${t.short_id || t.id.slice(0, 8)} ${t.title}`);
|
|
10817
|
+
}
|
|
10818
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
10819
|
+
`) }] };
|
|
10820
|
+
} catch (e) {
|
|
10821
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
10822
|
+
}
|
|
10823
|
+
});
|
|
10824
|
+
}
|
|
10825
|
+
if (shouldRegisterTool("import_github_issue")) {
|
|
10826
|
+
server.tool("import_github_issue", "Import a GitHub issue as a task. Requires gh CLI installed and authenticated.", {
|
|
10827
|
+
url: exports_external.string().describe("GitHub issue URL (e.g. https://github.com/owner/repo/issues/42)"),
|
|
10828
|
+
project_id: exports_external.string().optional(),
|
|
10829
|
+
task_list_id: exports_external.string().optional()
|
|
10830
|
+
}, async ({ url, project_id, task_list_id }) => {
|
|
10831
|
+
try {
|
|
10832
|
+
const { parseGitHubUrl: parseGitHubUrl2, fetchGitHubIssue: fetchGitHubIssue2, issueToTask: issueToTask2 } = await Promise.resolve().then(() => (init_github(), exports_github));
|
|
10833
|
+
const parsed = parseGitHubUrl2(url);
|
|
10834
|
+
if (!parsed)
|
|
10835
|
+
return { content: [{ type: "text", text: "Invalid GitHub issue URL." }], isError: true };
|
|
10836
|
+
const issue = fetchGitHubIssue2(parsed.owner, parsed.repo, parsed.number);
|
|
10837
|
+
const input = issueToTask2(issue, { project_id, task_list_id });
|
|
10838
|
+
const task = createTask(input);
|
|
10839
|
+
return { content: [{ type: "text", text: `Imported GH#${issue.number}: ${issue.title}
|
|
10840
|
+
Task: ${task.short_id || task.id} [${task.priority}]
|
|
10841
|
+
Labels: ${issue.labels.join(", ") || "none"}` }] };
|
|
10842
|
+
} catch (e) {
|
|
10843
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
10844
|
+
}
|
|
10845
|
+
});
|
|
10846
|
+
}
|
|
10847
|
+
if (shouldRegisterTool("blame")) {
|
|
10848
|
+
server.tool("blame", "Show which tasks and agents touched a file \u2014 combines task_files and task_commits data.", {
|
|
10849
|
+
path: exports_external.string().describe("File path to look up")
|
|
10850
|
+
}, async ({ path }) => {
|
|
10851
|
+
try {
|
|
10852
|
+
const { findTasksByFile: findTasksByFile2 } = await Promise.resolve().then(() => (init_task_files(), exports_task_files));
|
|
10853
|
+
const db = getDatabase();
|
|
10854
|
+
const taskFiles = findTasksByFile2(path, db);
|
|
10855
|
+
const commitRows = db.query("SELECT tc.*, t.title, t.short_id FROM task_commits tc JOIN tasks t ON t.id = tc.task_id WHERE tc.files_changed LIKE ? ORDER BY tc.committed_at DESC").all(`%${path}%`);
|
|
10856
|
+
const lines = [`Blame: ${path}`];
|
|
10857
|
+
if (taskFiles.length > 0) {
|
|
10858
|
+
lines.push(`
|
|
10859
|
+
Task File Links (${taskFiles.length}):`);
|
|
10860
|
+
for (const tf of taskFiles) {
|
|
10861
|
+
const task = getTask(tf.task_id, db);
|
|
10862
|
+
lines.push(` ${task?.short_id || tf.task_id.slice(0, 8)} ${task?.title || "?"} \u2014 ${tf.role || "file"}`);
|
|
10863
|
+
}
|
|
10864
|
+
}
|
|
10865
|
+
if (commitRows.length > 0) {
|
|
10866
|
+
lines.push(`
|
|
10867
|
+
Commit Links (${commitRows.length}):`);
|
|
10868
|
+
for (const c of commitRows)
|
|
10869
|
+
lines.push(` ${c.sha?.slice(0, 7)} ${c.short_id || c.task_id.slice(0, 8)} ${c.title || ""} \u2014 ${c.author || ""}`);
|
|
10870
|
+
}
|
|
10871
|
+
if (taskFiles.length === 0 && commitRows.length === 0) {
|
|
10872
|
+
lines.push("No task or commit links found.");
|
|
10873
|
+
}
|
|
10874
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
10875
|
+
`) }] };
|
|
10876
|
+
} catch (e) {
|
|
10877
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
10878
|
+
}
|
|
10879
|
+
});
|
|
10880
|
+
}
|
|
10881
|
+
if (shouldRegisterTool("burndown")) {
|
|
10882
|
+
server.tool("burndown", "ASCII burndown chart showing actual vs ideal progress for a plan, project, or task list.", {
|
|
10883
|
+
plan_id: exports_external.string().optional(),
|
|
10884
|
+
project_id: exports_external.string().optional(),
|
|
10885
|
+
task_list_id: exports_external.string().optional()
|
|
10886
|
+
}, async ({ plan_id, project_id, task_list_id }) => {
|
|
10887
|
+
try {
|
|
10888
|
+
const { getBurndown: getBurndown2 } = await Promise.resolve().then(() => (init_burndown(), exports_burndown));
|
|
10889
|
+
const data = getBurndown2({ plan_id, project_id, task_list_id });
|
|
10890
|
+
return { content: [{ type: "text", text: `Burndown: ${data.completed}/${data.total} done, ${data.remaining} remaining
|
|
10891
|
+
|
|
10892
|
+
${data.chart}` }] };
|
|
10893
|
+
} catch (e) {
|
|
10894
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
10895
|
+
}
|
|
10896
|
+
});
|
|
10897
|
+
}
|
|
10226
10898
|
if (shouldRegisterTool("create_webhook")) {
|
|
10227
10899
|
server.tool("create_webhook", "Register a webhook for task change events.", {
|
|
10228
10900
|
url: exports_external.string(),
|
|
@@ -11028,6 +11700,91 @@ ${checks.map((c) => ` ${c.status === "ok" ? "\u2713" : "\u26A0"} ${c.name}: ${c
|
|
|
11028
11700
|
}
|
|
11029
11701
|
});
|
|
11030
11702
|
}
|
|
11703
|
+
if (shouldRegisterTool("task_context")) {
|
|
11704
|
+
server.tool("task_context", "Full orientation for a specific task \u2014 details, description, dependencies (with blocked status), files, commits, comments, checklist. Use when starting work on a task.", {
|
|
11705
|
+
id: exports_external.string().describe("Task ID, short_id, or partial ID")
|
|
11706
|
+
}, async ({ id }) => {
|
|
11707
|
+
try {
|
|
11708
|
+
const resolvedId = resolveId(id, "tasks");
|
|
11709
|
+
const task = getTaskWithRelations(resolvedId);
|
|
11710
|
+
if (!task)
|
|
11711
|
+
return { content: [{ type: "text", text: `Task not found: ${id}` }], isError: true };
|
|
11712
|
+
const lines = [];
|
|
11713
|
+
const sid = task.short_id || task.id.slice(0, 8);
|
|
11714
|
+
lines.push(`${sid} [${task.status}] [${task.priority}] ${task.title}`);
|
|
11715
|
+
if (task.description)
|
|
11716
|
+
lines.push(`
|
|
11717
|
+
Description:
|
|
11718
|
+
${task.description}`);
|
|
11719
|
+
if (task.assigned_to)
|
|
11720
|
+
lines.push(`Assigned: ${task.assigned_to}`);
|
|
11721
|
+
if (task.started_at)
|
|
11722
|
+
lines.push(`Started: ${task.started_at}`);
|
|
11723
|
+
if (task.completed_at) {
|
|
11724
|
+
lines.push(`Completed: ${task.completed_at}`);
|
|
11725
|
+
if (task.started_at) {
|
|
11726
|
+
const dur = Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 60000);
|
|
11727
|
+
lines.push(`Duration: ${dur}m`);
|
|
11728
|
+
}
|
|
11729
|
+
}
|
|
11730
|
+
if (task.tags.length > 0)
|
|
11731
|
+
lines.push(`Tags: ${task.tags.join(", ")}`);
|
|
11732
|
+
if (task.dependencies.length > 0) {
|
|
11733
|
+
lines.push(`
|
|
11734
|
+
Depends on (${task.dependencies.length}):`);
|
|
11735
|
+
for (const dep of task.dependencies) {
|
|
11736
|
+
const blocked = dep.status !== "completed" && dep.status !== "cancelled";
|
|
11737
|
+
lines.push(` ${blocked ? "\u2717" : "\u2713"} ${dep.short_id || dep.id.slice(0, 8)} [${dep.status}] ${dep.title}`);
|
|
11738
|
+
}
|
|
11739
|
+
const unfinished = task.dependencies.filter((d) => d.status !== "completed" && d.status !== "cancelled");
|
|
11740
|
+
if (unfinished.length > 0)
|
|
11741
|
+
lines.push(`\u26A0 BLOCKED by ${unfinished.length} unfinished dep(s)`);
|
|
11742
|
+
}
|
|
11743
|
+
if (task.blocked_by.length > 0) {
|
|
11744
|
+
lines.push(`
|
|
11745
|
+
Blocks (${task.blocked_by.length}):`);
|
|
11746
|
+
for (const b of task.blocked_by)
|
|
11747
|
+
lines.push(` ${b.short_id || b.id.slice(0, 8)} [${b.status}] ${b.title}`);
|
|
11748
|
+
}
|
|
11749
|
+
if (task.subtasks.length > 0) {
|
|
11750
|
+
lines.push(`
|
|
11751
|
+
Subtasks (${task.subtasks.length}):`);
|
|
11752
|
+
for (const st of task.subtasks)
|
|
11753
|
+
lines.push(` ${st.short_id || st.id.slice(0, 8)} [${st.status}] ${st.title}`);
|
|
11754
|
+
}
|
|
11755
|
+
try {
|
|
11756
|
+
const { listTaskFiles: listTaskFiles2 } = await Promise.resolve().then(() => (init_task_files(), exports_task_files));
|
|
11757
|
+
const files = listTaskFiles2(task.id);
|
|
11758
|
+
if (files.length > 0) {
|
|
11759
|
+
lines.push(`
|
|
11760
|
+
Files (${files.length}):`);
|
|
11761
|
+
for (const f of files)
|
|
11762
|
+
lines.push(` ${f.role || "file"}: ${f.path}`);
|
|
11763
|
+
}
|
|
11764
|
+
} catch {}
|
|
11765
|
+
try {
|
|
11766
|
+
const { getTaskCommits: getTaskCommits2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
|
|
11767
|
+
const commits = getTaskCommits2(task.id);
|
|
11768
|
+
if (commits.length > 0) {
|
|
11769
|
+
lines.push(`
|
|
11770
|
+
Commits (${commits.length}):`);
|
|
11771
|
+
for (const c of commits)
|
|
11772
|
+
lines.push(` ${c.commit_hash?.slice(0, 7)} ${c.message || ""}`);
|
|
11773
|
+
}
|
|
11774
|
+
} catch {}
|
|
11775
|
+
if (task.comments.length > 0) {
|
|
11776
|
+
lines.push(`
|
|
11777
|
+
Comments (${task.comments.length}):`);
|
|
11778
|
+
for (const c of task.comments)
|
|
11779
|
+
lines.push(` [${c.agent_id || "?"}] ${c.created_at}: ${c.content}`);
|
|
11780
|
+
}
|
|
11781
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
11782
|
+
`) }] };
|
|
11783
|
+
} catch (e) {
|
|
11784
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
11785
|
+
}
|
|
11786
|
+
});
|
|
11787
|
+
}
|
|
11031
11788
|
if (shouldRegisterTool("get_context")) {
|
|
11032
11789
|
server.tool("get_context", "Get a compact task summary for agent prompt injection. Returns formatted text.", {
|
|
11033
11790
|
agent_id: exports_external.string().optional(),
|
|
@@ -11290,6 +12047,7 @@ if (shouldRegisterTool("search_tools")) {
|
|
|
11290
12047
|
"delete_agent",
|
|
11291
12048
|
"unarchive_agent",
|
|
11292
12049
|
"heartbeat",
|
|
12050
|
+
"release_agent",
|
|
11293
12051
|
"get_my_tasks",
|
|
11294
12052
|
"get_org_chart",
|
|
11295
12053
|
"set_reports_to",
|
|
@@ -11306,6 +12064,12 @@ if (shouldRegisterTool("search_tools")) {
|
|
|
11306
12064
|
"claim_next_task",
|
|
11307
12065
|
"get_task_history",
|
|
11308
12066
|
"get_recent_activity",
|
|
12067
|
+
"recap",
|
|
12068
|
+
"task_context",
|
|
12069
|
+
"standup",
|
|
12070
|
+
"burndown",
|
|
12071
|
+
"blame",
|
|
12072
|
+
"import_github_issue",
|
|
11309
12073
|
"create_webhook",
|
|
11310
12074
|
"list_webhooks",
|
|
11311
12075
|
"delete_webhook",
|
|
@@ -11430,8 +12194,8 @@ if (shouldRegisterTool("describe_tools")) {
|
|
|
11430
12194
|
suggest_agent_name: `Check available agent names before registering. Shows active agents and, if a pool is configured, which pool names are free.
|
|
11431
12195
|
Params: working_dir(string \u2014 your working directory, used to look up project pool from config)
|
|
11432
12196
|
Example: {working_dir: '/workspace/platform'}`,
|
|
11433
|
-
register_agent: `Register an agent. Any name is allowed \u2014 pool is advisory. Returns CONFLICT if name is held by a recently-active agent.
|
|
11434
|
-
Params: name(string, req), description(string), capabilities(string[]), session_id(string \u2014 unique per session), working_dir(string \u2014 used to determine project pool)
|
|
12197
|
+
register_agent: `Register an agent. Any name is allowed \u2014 pool is advisory. Returns CONFLICT if name is held by a recently-active agent. Use force:true to take over.
|
|
12198
|
+
Params: name(string, req), description(string), capabilities(string[]), session_id(string \u2014 unique per session), working_dir(string \u2014 used to determine project pool), force(boolean \u2014 skip conflict check)
|
|
11435
12199
|
Example: {name: 'my-agent', session_id: 'abc123-1741952000', working_dir: '/workspace/platform'}`,
|
|
11436
12200
|
list_agents: `List all registered agents (active by default). Set include_archived: true to see archived agents.
|
|
11437
12201
|
Params: include_archived(boolean, optional)
|
|
@@ -11451,6 +12215,9 @@ if (shouldRegisterTool("describe_tools")) {
|
|
|
11451
12215
|
heartbeat: `Update last_seen_at timestamp to signal you're still active. Call periodically during long tasks.
|
|
11452
12216
|
Params: agent_id(string, req \u2014 your agent ID or name)
|
|
11453
12217
|
Example: {agent_id: 'maximus'}`,
|
|
12218
|
+
release_agent: `Explicitly release/logout an agent \u2014 clears session binding and makes name immediately available. Call when session ends.
|
|
12219
|
+
Params: agent_id(string, req), session_id(string \u2014 only releases if matching)
|
|
12220
|
+
Example: {agent_id: 'maximus', session_id: 'my-session-123'}`,
|
|
11454
12221
|
get_my_tasks: `Get all tasks assigned to/created by an agent, with stats (pending/active/done/rate).
|
|
11455
12222
|
Params: agent_name(string, req)
|
|
11456
12223
|
Example: {agent_name: 'maximus'}`,
|
|
@@ -11511,6 +12278,24 @@ if (shouldRegisterTool("describe_tools")) {
|
|
|
11511
12278
|
get_recent_activity: `Get recent task changes across all tasks \u2014 global activity feed.
|
|
11512
12279
|
Params: limit(number, default:50)
|
|
11513
12280
|
Example: {limit: 20}`,
|
|
12281
|
+
recap: `Summary of what happened in the last N hours \u2014 completed tasks with durations, new tasks, in-progress, blocked, stale, agent activity.
|
|
12282
|
+
Params: hours(number, default:8), project_id(string)
|
|
12283
|
+
Example: {hours: 4}`,
|
|
12284
|
+
task_context: `Full orientation for a specific task \u2014 description, dependencies with blocked status, files, commits, comments, checklist, duration. Use before starting work.
|
|
12285
|
+
Params: id(string, req)
|
|
12286
|
+
Example: {id: 'OPE-00042'}`,
|
|
12287
|
+
standup: `Generate standup notes \u2014 completed tasks grouped by agent, in-progress, blocked. Copy-paste ready.
|
|
12288
|
+
Params: hours(number, default:24), project_id(string)
|
|
12289
|
+
Example: {hours: 24}`,
|
|
12290
|
+
import_github_issue: `Import a GitHub issue as a task. Requires gh CLI.
|
|
12291
|
+
Params: url(string, req), project_id(string), task_list_id(string)
|
|
12292
|
+
Example: {url: 'https://github.com/owner/repo/issues/42'}`,
|
|
12293
|
+
blame: `Show which tasks/agents touched a file \u2014 combines task_files and task_commits.
|
|
12294
|
+
Params: path(string, req)
|
|
12295
|
+
Example: {path: 'src/db/agents.ts'}`,
|
|
12296
|
+
burndown: `ASCII burndown chart \u2014 actual vs ideal progress for a plan, project, or task list.
|
|
12297
|
+
Params: plan_id(string), project_id(string), task_list_id(string)
|
|
12298
|
+
Example: {plan_id: 'abc123'}`,
|
|
11514
12299
|
create_webhook: `Register a webhook for task change events.
|
|
11515
12300
|
Params: url(string, req), events(string[] \u2014 empty=all), secret(string \u2014 HMAC signing)
|
|
11516
12301
|
Example: {url: 'https://example.com/hook', events: ['task.created', 'task.completed']}`,
|
|
@@ -12292,6 +13077,48 @@ ${lines.join(`
|
|
|
12292
13077
|
}
|
|
12293
13078
|
});
|
|
12294
13079
|
}
|
|
13080
|
+
if (shouldRegisterTool("extract_todos")) {
|
|
13081
|
+
server.tool("extract_todos", "Scan source files for TODO/FIXME/HACK/BUG/XXX/NOTE comments and create tasks from them. Deduplicates on re-runs.", {
|
|
13082
|
+
path: exports_external.string().describe("Directory or file path to scan"),
|
|
13083
|
+
project_id: exports_external.string().optional().describe("Project to assign tasks to"),
|
|
13084
|
+
task_list_id: exports_external.string().optional().describe("Task list to add tasks to"),
|
|
13085
|
+
patterns: exports_external.array(exports_external.enum(["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"])).optional().describe("Tags to search for (default: all)"),
|
|
13086
|
+
tags: exports_external.array(exports_external.string()).optional().describe("Extra tags to add to created tasks"),
|
|
13087
|
+
assigned_to: exports_external.string().optional().describe("Agent to assign tasks to"),
|
|
13088
|
+
agent_id: exports_external.string().optional().describe("Agent performing the extraction"),
|
|
13089
|
+
dry_run: exports_external.boolean().optional().describe("If true, return found comments without creating tasks"),
|
|
13090
|
+
extensions: exports_external.array(exports_external.string()).optional().describe("File extensions to scan (e.g. ['ts', 'py'])")
|
|
13091
|
+
}, async (params) => {
|
|
13092
|
+
try {
|
|
13093
|
+
const { extractTodos: extractTodos2 } = (init_extract(), __toCommonJS(exports_extract));
|
|
13094
|
+
const resolved = { ...params };
|
|
13095
|
+
if (resolved["project_id"])
|
|
13096
|
+
resolved["project_id"] = resolveId(resolved["project_id"], "projects");
|
|
13097
|
+
if (resolved["task_list_id"])
|
|
13098
|
+
resolved["task_list_id"] = resolveId(resolved["task_list_id"], "task_lists");
|
|
13099
|
+
const result = extractTodos2(resolved);
|
|
13100
|
+
if (params.dry_run) {
|
|
13101
|
+
const lines = result.comments.map((c) => `[${c.tag}] ${c.message} \u2014 ${c.file}:${c.line}`);
|
|
13102
|
+
return { content: [{ type: "text", text: `Found ${result.comments.length} comment(s):
|
|
13103
|
+
${lines.join(`
|
|
13104
|
+
`)}` }] };
|
|
13105
|
+
}
|
|
13106
|
+
const summary = [
|
|
13107
|
+
`Created ${result.tasks.length} task(s)`,
|
|
13108
|
+
result.skipped > 0 ? `Skipped ${result.skipped} duplicate(s)` : null,
|
|
13109
|
+
`Total comments found: ${result.comments.length}`
|
|
13110
|
+
].filter(Boolean).join(`
|
|
13111
|
+
`);
|
|
13112
|
+
const taskLines = result.tasks.map((t) => formatTask(t));
|
|
13113
|
+
return { content: [{ type: "text", text: `${summary}
|
|
13114
|
+
|
|
13115
|
+
${taskLines.join(`
|
|
13116
|
+
`)}` }] };
|
|
13117
|
+
} catch (e) {
|
|
13118
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13119
|
+
}
|
|
13120
|
+
});
|
|
13121
|
+
}
|
|
12295
13122
|
server.resource("task-lists", "todos://task-lists", { description: "All task lists", mimeType: "application/json" }, async () => {
|
|
12296
13123
|
const lists = listTaskLists();
|
|
12297
13124
|
return { contents: [{ uri: "todos://task-lists", text: JSON.stringify(lists, null, 2), mimeType: "application/json" }] };
|