@hasna/todos 0.10.19 → 0.10.20
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 +2131 -561
- 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/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +446 -20
- 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 +987 -182
- package/dist/server/index.js +107 -22
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2346,6 +2346,7 @@ function ensureSchema(db) {
|
|
|
2346
2346
|
ensureColumn("tasks", "spawned_from_session", "TEXT");
|
|
2347
2347
|
ensureColumn("tasks", "assigned_by", "TEXT");
|
|
2348
2348
|
ensureColumn("tasks", "assigned_from_project", "TEXT");
|
|
2349
|
+
ensureColumn("tasks", "started_at", "TEXT");
|
|
2349
2350
|
ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
|
|
2350
2351
|
ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
|
|
2351
2352
|
ensureColumn("agents", "reports_to", "TEXT");
|
|
@@ -2463,6 +2464,16 @@ function resolvePartialId(db, table, partialId) {
|
|
|
2463
2464
|
return shortIdRows[0].id;
|
|
2464
2465
|
}
|
|
2465
2466
|
}
|
|
2467
|
+
if (table === "task_lists") {
|
|
2468
|
+
const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
|
|
2469
|
+
if (slugRow)
|
|
2470
|
+
return slugRow.id;
|
|
2471
|
+
}
|
|
2472
|
+
if (table === "projects") {
|
|
2473
|
+
const nameRow = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(partialId.toLowerCase());
|
|
2474
|
+
if (nameRow)
|
|
2475
|
+
return nameRow.id;
|
|
2476
|
+
}
|
|
2466
2477
|
return null;
|
|
2467
2478
|
}
|
|
2468
2479
|
var LOCK_EXPIRY_MINUTES = 30, MIGRATIONS, _db = null;
|
|
@@ -2941,6 +2952,10 @@ var init_database = __esm(() => {
|
|
|
2941
2952
|
CREATE INDEX IF NOT EXISTS idx_file_locks_agent ON file_locks(agent_id);
|
|
2942
2953
|
CREATE INDEX IF NOT EXISTS idx_file_locks_expires ON file_locks(expires_at);
|
|
2943
2954
|
INSERT OR IGNORE INTO _migrations (id) VALUES (33);
|
|
2955
|
+
`,
|
|
2956
|
+
`
|
|
2957
|
+
ALTER TABLE tasks ADD COLUMN started_at TEXT;
|
|
2958
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (34);
|
|
2944
2959
|
`
|
|
2945
2960
|
];
|
|
2946
2961
|
});
|
|
@@ -3413,7 +3428,8 @@ var exports_audit = {};
|
|
|
3413
3428
|
__export(exports_audit, {
|
|
3414
3429
|
logTaskChange: () => logTaskChange,
|
|
3415
3430
|
getTaskHistory: () => getTaskHistory,
|
|
3416
|
-
getRecentActivity: () => getRecentActivity
|
|
3431
|
+
getRecentActivity: () => getRecentActivity,
|
|
3432
|
+
getRecap: () => getRecap
|
|
3417
3433
|
});
|
|
3418
3434
|
function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
3419
3435
|
const d = db || getDatabase();
|
|
@@ -3431,6 +3447,32 @@ function getRecentActivity(limit = 50, db) {
|
|
|
3431
3447
|
const d = db || getDatabase();
|
|
3432
3448
|
return d.query("SELECT * FROM task_history ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
3433
3449
|
}
|
|
3450
|
+
function getRecap(hours = 8, projectId, db) {
|
|
3451
|
+
const d = db || getDatabase();
|
|
3452
|
+
const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
|
3453
|
+
const staleWindow = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
|
3454
|
+
const pf = projectId ? " AND project_id = ?" : "";
|
|
3455
|
+
const tpf = projectId ? " AND t.project_id = ?" : "";
|
|
3456
|
+
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);
|
|
3457
|
+
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);
|
|
3458
|
+
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();
|
|
3459
|
+
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();
|
|
3460
|
+
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);
|
|
3461
|
+
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);
|
|
3462
|
+
return {
|
|
3463
|
+
hours,
|
|
3464
|
+
since,
|
|
3465
|
+
completed: completed.map((r) => ({
|
|
3466
|
+
...r,
|
|
3467
|
+
duration_minutes: r.started_at && r.completed_at ? Math.round((new Date(r.completed_at).getTime() - new Date(r.started_at).getTime()) / 60000) : null
|
|
3468
|
+
})),
|
|
3469
|
+
created,
|
|
3470
|
+
in_progress,
|
|
3471
|
+
blocked,
|
|
3472
|
+
stale,
|
|
3473
|
+
agents
|
|
3474
|
+
};
|
|
3475
|
+
}
|
|
3434
3476
|
var init_audit = __esm(() => {
|
|
3435
3477
|
init_database();
|
|
3436
3478
|
});
|
|
@@ -4141,8 +4183,8 @@ function startTask(id, agentId, db) {
|
|
|
4141
4183
|
}
|
|
4142
4184
|
const cutoff = lockExpiryCutoff();
|
|
4143
4185
|
const timestamp = now();
|
|
4144
|
-
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
4145
|
-
WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, id, agentId, cutoff]);
|
|
4186
|
+
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 = ?
|
|
4187
|
+
WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, timestamp, id, agentId, cutoff]);
|
|
4146
4188
|
if (result.changes === 0) {
|
|
4147
4189
|
if (task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
|
|
4148
4190
|
throw new LockError(id, task.locked_by);
|
|
@@ -4150,7 +4192,7 @@ function startTask(id, agentId, db) {
|
|
|
4150
4192
|
}
|
|
4151
4193
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
4152
4194
|
dispatchWebhook("task.started", { id, agent_id: agentId, title: task.title }, d).catch(() => {});
|
|
4153
|
-
return { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, version: task.version + 1, updated_at: timestamp };
|
|
4195
|
+
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 };
|
|
4154
4196
|
}
|
|
4155
4197
|
function completeTask(id, agentId, db, options) {
|
|
4156
4198
|
const d = db || getDatabase();
|
|
@@ -4801,6 +4843,7 @@ __export(exports_agents, {
|
|
|
4801
4843
|
updateAgentActivity: () => updateAgentActivity,
|
|
4802
4844
|
updateAgent: () => updateAgent,
|
|
4803
4845
|
unarchiveAgent: () => unarchiveAgent,
|
|
4846
|
+
releaseAgent: () => releaseAgent,
|
|
4804
4847
|
registerAgent: () => registerAgent,
|
|
4805
4848
|
matchCapabilities: () => matchCapabilities,
|
|
4806
4849
|
listAgents: () => listAgents,
|
|
@@ -4812,10 +4855,29 @@ __export(exports_agents, {
|
|
|
4812
4855
|
getAgentByName: () => getAgentByName,
|
|
4813
4856
|
getAgent: () => getAgent,
|
|
4814
4857
|
deleteAgent: () => deleteAgent,
|
|
4858
|
+
autoReleaseStaleAgents: () => autoReleaseStaleAgents,
|
|
4815
4859
|
archiveAgent: () => archiveAgent
|
|
4816
4860
|
});
|
|
4861
|
+
function getActiveWindowMs() {
|
|
4862
|
+
const env = process.env["TODOS_AGENT_TIMEOUT_MS"];
|
|
4863
|
+
if (env) {
|
|
4864
|
+
const parsed = parseInt(env, 10);
|
|
4865
|
+
if (!isNaN(parsed) && parsed > 0)
|
|
4866
|
+
return parsed;
|
|
4867
|
+
}
|
|
4868
|
+
return 30 * 60 * 1000;
|
|
4869
|
+
}
|
|
4870
|
+
function autoReleaseStaleAgents(db) {
|
|
4871
|
+
if (process.env["TODOS_AGENT_AUTO_RELEASE"] !== "true")
|
|
4872
|
+
return 0;
|
|
4873
|
+
const d = db || getDatabase();
|
|
4874
|
+
const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
|
|
4875
|
+
const result = d.run("UPDATE agents SET session_id = NULL WHERE status = 'active' AND session_id IS NOT NULL AND last_seen_at < ?", [cutoff]);
|
|
4876
|
+
return result.changes;
|
|
4877
|
+
}
|
|
4817
4878
|
function getAvailableNamesFromPool(pool, db) {
|
|
4818
|
-
|
|
4879
|
+
autoReleaseStaleAgents(db);
|
|
4880
|
+
const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
|
|
4819
4881
|
const activeNames = new Set(db.query("SELECT name FROM agents WHERE status = 'active' AND last_seen_at > ?").all(cutoff).map((r) => r.name.toLowerCase()));
|
|
4820
4882
|
return pool.filter((name) => !activeNames.has(name.toLowerCase()));
|
|
4821
4883
|
}
|
|
@@ -4837,22 +4899,19 @@ function registerAgent(input, db) {
|
|
|
4837
4899
|
const existing = getAgentByName(normalizedName, d);
|
|
4838
4900
|
if (existing) {
|
|
4839
4901
|
const lastSeenMs = new Date(existing.last_seen_at).getTime();
|
|
4840
|
-
const
|
|
4902
|
+
const activeWindowMs = getActiveWindowMs();
|
|
4903
|
+
const isActive = Date.now() - lastSeenMs < activeWindowMs;
|
|
4841
4904
|
const sameSession = input.session_id && existing.session_id && input.session_id === existing.session_id;
|
|
4842
4905
|
const differentSession = input.session_id && existing.session_id && input.session_id !== existing.session_id;
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
working_dir: existing.working_dir,
|
|
4853
|
-
suggestions: suggestions.slice(0, 5),
|
|
4854
|
-
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(", ")}` : ""}`
|
|
4855
|
-
};
|
|
4906
|
+
const callerHasNoSession = !input.session_id;
|
|
4907
|
+
const existingHasActiveSession = existing.session_id && isActive;
|
|
4908
|
+
if (!input.force) {
|
|
4909
|
+
if (isActive && differentSession) {
|
|
4910
|
+
return buildConflictError(existing, lastSeenMs, input.pool, d);
|
|
4911
|
+
}
|
|
4912
|
+
if (callerHasNoSession && existingHasActiveSession) {
|
|
4913
|
+
return buildConflictError(existing, lastSeenMs, input.pool, d);
|
|
4914
|
+
}
|
|
4856
4915
|
}
|
|
4857
4916
|
const updates = ["last_seen_at = ?", "status = 'active'"];
|
|
4858
4917
|
const params = [now()];
|
|
@@ -4897,6 +4956,32 @@ function registerAgent(input, db) {
|
|
|
4897
4956
|
function isAgentConflict(result) {
|
|
4898
4957
|
return result.conflict === true;
|
|
4899
4958
|
}
|
|
4959
|
+
function buildConflictError(existing, lastSeenMs, pool, d) {
|
|
4960
|
+
const minutesAgo = Math.round((Date.now() - lastSeenMs) / 60000);
|
|
4961
|
+
const suggestions = pool ? getAvailableNamesFromPool(pool, d) : [];
|
|
4962
|
+
return {
|
|
4963
|
+
conflict: true,
|
|
4964
|
+
existing_id: existing.id,
|
|
4965
|
+
existing_name: existing.name,
|
|
4966
|
+
last_seen_at: existing.last_seen_at,
|
|
4967
|
+
session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
|
|
4968
|
+
working_dir: existing.working_dir,
|
|
4969
|
+
suggestions: suggestions.slice(0, 5),
|
|
4970
|
+
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(", ")}` : ""}`
|
|
4971
|
+
};
|
|
4972
|
+
}
|
|
4973
|
+
function releaseAgent(id, session_id, db) {
|
|
4974
|
+
const d = db || getDatabase();
|
|
4975
|
+
const agent = getAgent(id, d);
|
|
4976
|
+
if (!agent)
|
|
4977
|
+
return false;
|
|
4978
|
+
if (session_id && agent.session_id && agent.session_id !== session_id) {
|
|
4979
|
+
return false;
|
|
4980
|
+
}
|
|
4981
|
+
const epoch = new Date(0).toISOString();
|
|
4982
|
+
d.run("UPDATE agents SET session_id = NULL, last_seen_at = ? WHERE id = ?", [epoch, id]);
|
|
4983
|
+
return true;
|
|
4984
|
+
}
|
|
4900
4985
|
function getAgent(id, db) {
|
|
4901
4986
|
const d = db || getDatabase();
|
|
4902
4987
|
const row = d.query("SELECT * FROM agents WHERE id = ?").get(id);
|
|
@@ -4917,6 +5002,7 @@ function listAgents(opts, db) {
|
|
|
4917
5002
|
includeArchived = opts?.include_archived ?? false;
|
|
4918
5003
|
d = db || getDatabase();
|
|
4919
5004
|
}
|
|
5005
|
+
autoReleaseStaleAgents(d);
|
|
4920
5006
|
if (includeArchived) {
|
|
4921
5007
|
return d.query("SELECT * FROM agents ORDER BY name").all().map(rowToAgent);
|
|
4922
5008
|
}
|
|
@@ -4934,8 +5020,19 @@ function updateAgent(id, input, db) {
|
|
|
4934
5020
|
const sets = ["last_seen_at = ?"];
|
|
4935
5021
|
const params = [now()];
|
|
4936
5022
|
if (input.name !== undefined) {
|
|
5023
|
+
const newName = input.name.trim().toLowerCase();
|
|
5024
|
+
const holder = getAgentByName(newName, d);
|
|
5025
|
+
if (holder && holder.id !== id) {
|
|
5026
|
+
const lastSeenMs = new Date(holder.last_seen_at).getTime();
|
|
5027
|
+
const isActive = Date.now() - lastSeenMs < getActiveWindowMs();
|
|
5028
|
+
if (isActive && holder.status === "active") {
|
|
5029
|
+
throw new Error(`Cannot rename: name "${newName}" is held by active agent ${holder.id} (last seen ${Math.round((Date.now() - lastSeenMs) / 60000)}m ago)`);
|
|
5030
|
+
}
|
|
5031
|
+
const evictedName = `${holder.name}__evicted_${holder.id}`;
|
|
5032
|
+
d.run("UPDATE agents SET name = ? WHERE id = ?", [evictedName, holder.id]);
|
|
5033
|
+
}
|
|
4937
5034
|
sets.push("name = ?");
|
|
4938
|
-
params.push(
|
|
5035
|
+
params.push(newName);
|
|
4939
5036
|
}
|
|
4940
5037
|
if (input.description !== undefined) {
|
|
4941
5038
|
sets.push("description = ?");
|
|
@@ -5032,10 +5129,8 @@ function getCapableAgents(capabilities, opts, db) {
|
|
|
5032
5129
|
})).filter((entry) => entry.score >= minScore).sort((a, b) => b.score - a.score);
|
|
5033
5130
|
return opts?.limit ? scored.slice(0, opts.limit) : scored;
|
|
5034
5131
|
}
|
|
5035
|
-
var AGENT_ACTIVE_WINDOW_MS;
|
|
5036
5132
|
var init_agents = __esm(() => {
|
|
5037
5133
|
init_database();
|
|
5038
|
-
AGENT_ACTIVE_WINDOW_MS = 30 * 60 * 1000;
|
|
5039
5134
|
});
|
|
5040
5135
|
|
|
5041
5136
|
// src/db/task-lists.ts
|
|
@@ -5867,139 +5962,555 @@ var init_sync = __esm(() => {
|
|
|
5867
5962
|
init_config();
|
|
5868
5963
|
});
|
|
5869
5964
|
|
|
5870
|
-
//
|
|
5871
|
-
var
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
return ZodParsedType.null;
|
|
5894
|
-
}
|
|
5895
|
-
if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") {
|
|
5896
|
-
return ZodParsedType.promise;
|
|
5897
|
-
}
|
|
5898
|
-
if (typeof Map !== "undefined" && data instanceof Map) {
|
|
5899
|
-
return ZodParsedType.map;
|
|
5900
|
-
}
|
|
5901
|
-
if (typeof Set !== "undefined" && data instanceof Set) {
|
|
5902
|
-
return ZodParsedType.set;
|
|
5903
|
-
}
|
|
5904
|
-
if (typeof Date !== "undefined" && data instanceof Date) {
|
|
5905
|
-
return ZodParsedType.date;
|
|
5906
|
-
}
|
|
5907
|
-
return ZodParsedType.object;
|
|
5908
|
-
default:
|
|
5909
|
-
return ZodParsedType.unknown;
|
|
5965
|
+
// src/db/task-files.ts
|
|
5966
|
+
var exports_task_files = {};
|
|
5967
|
+
__export(exports_task_files, {
|
|
5968
|
+
updateTaskFileStatus: () => updateTaskFileStatus,
|
|
5969
|
+
removeTaskFile: () => removeTaskFile,
|
|
5970
|
+
listTaskFiles: () => listTaskFiles,
|
|
5971
|
+
listActiveFiles: () => listActiveFiles,
|
|
5972
|
+
getTaskFile: () => getTaskFile,
|
|
5973
|
+
getFileHeatMap: () => getFileHeatMap,
|
|
5974
|
+
findTasksByFile: () => findTasksByFile,
|
|
5975
|
+
detectFileConflicts: () => detectFileConflicts,
|
|
5976
|
+
bulkFindTasksByFiles: () => bulkFindTasksByFiles,
|
|
5977
|
+
bulkAddTaskFiles: () => bulkAddTaskFiles,
|
|
5978
|
+
addTaskFile: () => addTaskFile
|
|
5979
|
+
});
|
|
5980
|
+
function addTaskFile(input, db) {
|
|
5981
|
+
const d = db || getDatabase();
|
|
5982
|
+
const id = uuid();
|
|
5983
|
+
const timestamp = now();
|
|
5984
|
+
const existing = d.query("SELECT id FROM task_files WHERE task_id = ? AND path = ?").get(input.task_id, input.path);
|
|
5985
|
+
if (existing) {
|
|
5986
|
+
d.run("UPDATE task_files SET status = ?, agent_id = ?, note = ?, updated_at = ? WHERE id = ?", [input.status || "active", input.agent_id || null, input.note || null, timestamp, existing.id]);
|
|
5987
|
+
return getTaskFile(existing.id, d);
|
|
5910
5988
|
}
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
(
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
}
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
(
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5989
|
+
d.run(`INSERT INTO task_files (id, task_id, path, status, agent_id, note, created_at, updated_at)
|
|
5990
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.task_id, input.path, input.status || "active", input.agent_id || null, input.note || null, timestamp, timestamp]);
|
|
5991
|
+
return getTaskFile(id, d);
|
|
5992
|
+
}
|
|
5993
|
+
function getTaskFile(id, db) {
|
|
5994
|
+
const d = db || getDatabase();
|
|
5995
|
+
return d.query("SELECT * FROM task_files WHERE id = ?").get(id);
|
|
5996
|
+
}
|
|
5997
|
+
function listTaskFiles(taskId, db) {
|
|
5998
|
+
const d = db || getDatabase();
|
|
5999
|
+
return d.query("SELECT * FROM task_files WHERE task_id = ? ORDER BY path").all(taskId);
|
|
6000
|
+
}
|
|
6001
|
+
function findTasksByFile(path, db) {
|
|
6002
|
+
const d = db || getDatabase();
|
|
6003
|
+
return d.query("SELECT * FROM task_files WHERE path = ? AND status != 'removed' ORDER BY updated_at DESC").all(path);
|
|
6004
|
+
}
|
|
6005
|
+
function updateTaskFileStatus(taskId, path, status, agentId, db) {
|
|
6006
|
+
const d = db || getDatabase();
|
|
6007
|
+
const timestamp = now();
|
|
6008
|
+
d.run("UPDATE task_files SET status = ?, agent_id = COALESCE(?, agent_id), updated_at = ? WHERE task_id = ? AND path = ?", [status, agentId || null, timestamp, taskId, path]);
|
|
6009
|
+
const row = d.query("SELECT * FROM task_files WHERE task_id = ? AND path = ?").get(taskId, path);
|
|
6010
|
+
return row;
|
|
6011
|
+
}
|
|
6012
|
+
function removeTaskFile(taskId, path, db) {
|
|
6013
|
+
const d = db || getDatabase();
|
|
6014
|
+
const result = d.run("DELETE FROM task_files WHERE task_id = ? AND path = ?", [taskId, path]);
|
|
6015
|
+
return result.changes > 0;
|
|
6016
|
+
}
|
|
6017
|
+
function detectFileConflicts(taskId, paths, db) {
|
|
6018
|
+
const d = db || getDatabase();
|
|
6019
|
+
if (paths.length === 0)
|
|
6020
|
+
return [];
|
|
6021
|
+
const placeholders = paths.map(() => "?").join(", ");
|
|
6022
|
+
const rows = d.query(`
|
|
6023
|
+
SELECT tf.path, tf.agent_id AS conflicting_agent_id, t.id AS conflicting_task_id,
|
|
6024
|
+
t.title AS conflicting_task_title, t.status AS conflicting_task_status
|
|
6025
|
+
FROM task_files tf
|
|
6026
|
+
JOIN tasks t ON tf.task_id = t.id
|
|
6027
|
+
WHERE tf.path IN (${placeholders})
|
|
6028
|
+
AND tf.task_id != ?
|
|
6029
|
+
AND tf.status != 'removed'
|
|
6030
|
+
AND t.status = 'in_progress'
|
|
6031
|
+
ORDER BY tf.updated_at DESC
|
|
6032
|
+
`).all(...paths, taskId);
|
|
6033
|
+
return rows;
|
|
6034
|
+
}
|
|
6035
|
+
function bulkFindTasksByFiles(paths, db) {
|
|
6036
|
+
const d = db || getDatabase();
|
|
6037
|
+
if (paths.length === 0)
|
|
6038
|
+
return [];
|
|
6039
|
+
const placeholders = paths.map(() => "?").join(", ");
|
|
6040
|
+
const rows = d.query(`SELECT tf.*, t.status AS task_status FROM task_files tf
|
|
6041
|
+
JOIN tasks t ON tf.task_id = t.id
|
|
6042
|
+
WHERE tf.path IN (${placeholders}) AND tf.status != 'removed'
|
|
6043
|
+
ORDER BY tf.updated_at DESC`).all(...paths);
|
|
6044
|
+
const byPath = new Map;
|
|
6045
|
+
for (const path of paths)
|
|
6046
|
+
byPath.set(path, []);
|
|
6047
|
+
for (const row of rows) {
|
|
6048
|
+
byPath.get(row.path)?.push(row);
|
|
6049
|
+
}
|
|
6050
|
+
return paths.map((path) => {
|
|
6051
|
+
const tasks = byPath.get(path) ?? [];
|
|
6052
|
+
const inProgressCount = tasks.filter((t) => t.task_status === "in_progress").length;
|
|
6053
|
+
return {
|
|
6054
|
+
path,
|
|
6055
|
+
tasks,
|
|
6056
|
+
has_conflict: inProgressCount > 1,
|
|
6057
|
+
in_progress_count: inProgressCount
|
|
5975
6058
|
};
|
|
5976
|
-
})
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6059
|
+
});
|
|
6060
|
+
}
|
|
6061
|
+
function listActiveFiles(db) {
|
|
6062
|
+
const d = db || getDatabase();
|
|
6063
|
+
return d.query(`
|
|
6064
|
+
SELECT
|
|
6065
|
+
tf.path,
|
|
6066
|
+
tf.status AS file_status,
|
|
6067
|
+
tf.agent_id AS file_agent_id,
|
|
6068
|
+
tf.note,
|
|
6069
|
+
tf.updated_at,
|
|
6070
|
+
t.id AS task_id,
|
|
6071
|
+
t.short_id AS task_short_id,
|
|
6072
|
+
t.title AS task_title,
|
|
6073
|
+
t.status AS task_status,
|
|
6074
|
+
t.locked_by AS task_locked_by,
|
|
6075
|
+
t.locked_at AS task_locked_at,
|
|
6076
|
+
a.id AS agent_id,
|
|
6077
|
+
a.name AS agent_name
|
|
6078
|
+
FROM task_files tf
|
|
6079
|
+
JOIN tasks t ON tf.task_id = t.id
|
|
6080
|
+
LEFT JOIN agents a ON (tf.agent_id = a.id OR (tf.agent_id IS NULL AND t.assigned_to = a.id))
|
|
6081
|
+
WHERE t.status = 'in_progress'
|
|
6082
|
+
AND tf.status != 'removed'
|
|
6083
|
+
ORDER BY tf.updated_at DESC
|
|
6084
|
+
`).all();
|
|
6085
|
+
}
|
|
6086
|
+
function getFileHeatMap(opts, db) {
|
|
6087
|
+
const d = db || getDatabase();
|
|
6088
|
+
const limit = opts?.limit ?? 20;
|
|
6089
|
+
const minEdits = opts?.min_edits ?? 1;
|
|
6090
|
+
const rows = d.query(`
|
|
6091
|
+
SELECT
|
|
6092
|
+
tf.path,
|
|
6093
|
+
COUNT(*) AS edit_count,
|
|
6094
|
+
COUNT(DISTINCT COALESCE(tf.agent_id, t.assigned_to)) AS unique_agents,
|
|
6095
|
+
GROUP_CONCAT(DISTINCT COALESCE(tf.agent_id, t.assigned_to)) AS agent_ids,
|
|
6096
|
+
MAX(tf.updated_at) AS last_edited_at,
|
|
6097
|
+
SUM(CASE WHEN t.status = 'in_progress' THEN 1 ELSE 0 END) AS active_task_count
|
|
6098
|
+
FROM task_files tf
|
|
6099
|
+
JOIN tasks t ON tf.task_id = t.id
|
|
6100
|
+
WHERE tf.status != 'removed'
|
|
6101
|
+
${opts?.project_id ? `AND t.project_id = '${opts.project_id}'` : ""}
|
|
6102
|
+
GROUP BY tf.path
|
|
6103
|
+
HAVING edit_count >= ${minEdits}
|
|
6104
|
+
ORDER BY edit_count DESC, last_edited_at DESC
|
|
6105
|
+
LIMIT ${limit}
|
|
6106
|
+
`).all();
|
|
6107
|
+
return rows.map((r) => ({
|
|
6108
|
+
path: r.path,
|
|
6109
|
+
edit_count: r.edit_count,
|
|
6110
|
+
unique_agents: r.unique_agents,
|
|
6111
|
+
agent_ids: r.agent_ids ? r.agent_ids.split(",").filter(Boolean) : [],
|
|
6112
|
+
last_edited_at: r.last_edited_at,
|
|
6113
|
+
active_task_count: r.active_task_count
|
|
6114
|
+
}));
|
|
6115
|
+
}
|
|
6116
|
+
function bulkAddTaskFiles(taskId, paths, agentId, db) {
|
|
6117
|
+
const d = db || getDatabase();
|
|
6118
|
+
const results = [];
|
|
6119
|
+
const tx = d.transaction(() => {
|
|
6120
|
+
for (const path of paths) {
|
|
6121
|
+
results.push(addTaskFile({ task_id: taskId, path, agent_id: agentId }, d));
|
|
6122
|
+
}
|
|
6123
|
+
});
|
|
6124
|
+
tx();
|
|
6125
|
+
return results;
|
|
6126
|
+
}
|
|
6127
|
+
var init_task_files = __esm(() => {
|
|
6128
|
+
init_database();
|
|
6129
|
+
});
|
|
6130
|
+
|
|
6131
|
+
// src/db/task-commits.ts
|
|
6132
|
+
var exports_task_commits = {};
|
|
6133
|
+
__export(exports_task_commits, {
|
|
6134
|
+
unlinkTaskCommit: () => unlinkTaskCommit,
|
|
6135
|
+
linkTaskToCommit: () => linkTaskToCommit,
|
|
6136
|
+
getTaskCommits: () => getTaskCommits,
|
|
6137
|
+
findTaskByCommit: () => findTaskByCommit
|
|
6138
|
+
});
|
|
6139
|
+
function rowToCommit(row) {
|
|
6140
|
+
return {
|
|
6141
|
+
...row,
|
|
6142
|
+
files_changed: row.files_changed ? JSON.parse(row.files_changed) : null
|
|
6143
|
+
};
|
|
6144
|
+
}
|
|
6145
|
+
function linkTaskToCommit(input, db) {
|
|
6146
|
+
const d = db || getDatabase();
|
|
6147
|
+
const existing = d.query("SELECT * FROM task_commits WHERE task_id = ? AND sha = ?").get(input.task_id, input.sha);
|
|
6148
|
+
if (existing) {
|
|
6149
|
+
d.run("UPDATE task_commits SET message = COALESCE(?, message), author = COALESCE(?, author), files_changed = COALESCE(?, files_changed), committed_at = COALESCE(?, committed_at) WHERE id = ?", [input.message ?? null, input.author ?? null, input.files_changed ? JSON.stringify(input.files_changed) : null, input.committed_at ?? null, existing.id]);
|
|
6150
|
+
return rowToCommit(d.query("SELECT * FROM task_commits WHERE id = ?").get(existing.id));
|
|
6151
|
+
}
|
|
6152
|
+
const id = uuid();
|
|
6153
|
+
d.run("INSERT INTO task_commits (id, task_id, sha, message, author, files_changed, committed_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [id, input.task_id, input.sha, input.message ?? null, input.author ?? null, input.files_changed ? JSON.stringify(input.files_changed) : null, input.committed_at ?? null, now()]);
|
|
6154
|
+
return rowToCommit(d.query("SELECT * FROM task_commits WHERE id = ?").get(id));
|
|
6155
|
+
}
|
|
6156
|
+
function getTaskCommits(taskId, db) {
|
|
6157
|
+
const d = db || getDatabase();
|
|
6158
|
+
return d.query("SELECT * FROM task_commits WHERE task_id = ? ORDER BY committed_at DESC, created_at DESC").all(taskId).map(rowToCommit);
|
|
6159
|
+
}
|
|
6160
|
+
function findTaskByCommit(sha, db) {
|
|
6161
|
+
const d = db || getDatabase();
|
|
6162
|
+
const row = d.query("SELECT * FROM task_commits WHERE sha = ? OR sha LIKE ? LIMIT 1").get(sha, `${sha}%`);
|
|
6163
|
+
if (!row)
|
|
6164
|
+
return null;
|
|
6165
|
+
return { task_id: row.task_id, commit: rowToCommit(row) };
|
|
6166
|
+
}
|
|
6167
|
+
function unlinkTaskCommit(taskId, sha, db) {
|
|
6168
|
+
const d = db || getDatabase();
|
|
6169
|
+
return d.run("DELETE FROM task_commits WHERE task_id = ? AND (sha = ? OR sha LIKE ?)", [taskId, sha, `${sha}%`]).changes > 0;
|
|
6170
|
+
}
|
|
6171
|
+
var init_task_commits = __esm(() => {
|
|
6172
|
+
init_database();
|
|
6173
|
+
});
|
|
6174
|
+
|
|
6175
|
+
// src/lib/extract.ts
|
|
6176
|
+
var exports_extract = {};
|
|
6177
|
+
__export(exports_extract, {
|
|
6178
|
+
tagToPriority: () => tagToPriority,
|
|
6179
|
+
extractTodos: () => extractTodos,
|
|
6180
|
+
extractFromSource: () => extractFromSource,
|
|
6181
|
+
EXTRACT_TAGS: () => EXTRACT_TAGS
|
|
6182
|
+
});
|
|
6183
|
+
import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
6184
|
+
import { relative, resolve as resolve2, join as join6 } from "path";
|
|
6185
|
+
function tagToPriority(tag) {
|
|
6186
|
+
switch (tag) {
|
|
6187
|
+
case "BUG":
|
|
6188
|
+
case "FIXME":
|
|
6189
|
+
return "high";
|
|
6190
|
+
case "HACK":
|
|
6191
|
+
case "XXX":
|
|
6192
|
+
return "medium";
|
|
6193
|
+
case "TODO":
|
|
6194
|
+
return "medium";
|
|
6195
|
+
case "NOTE":
|
|
6196
|
+
return "low";
|
|
6197
|
+
}
|
|
6198
|
+
}
|
|
6199
|
+
function buildTagRegex(tags) {
|
|
6200
|
+
const tagPattern = tags.join("|");
|
|
6201
|
+
return new RegExp(`(?:^|\\s)(?:\\/\\/|\\/\\*|#|\\*|--|;;|%|<!--|\\{-)\\s*(?:@?)(${tagPattern})\\s*[:(]?\\s*(.*)$`, "i");
|
|
6202
|
+
}
|
|
6203
|
+
function extractFromSource(source, filePath, tags = [...EXTRACT_TAGS]) {
|
|
6204
|
+
const regex = buildTagRegex(tags);
|
|
6205
|
+
const results = [];
|
|
6206
|
+
const lines = source.split(`
|
|
6207
|
+
`);
|
|
6208
|
+
for (let i = 0;i < lines.length; i++) {
|
|
6209
|
+
const line = lines[i];
|
|
6210
|
+
const match = line.match(regex);
|
|
6211
|
+
if (match) {
|
|
6212
|
+
const tag = match[1].toUpperCase();
|
|
6213
|
+
let message = match[2].trim();
|
|
6214
|
+
message = message.replace(/\s*\*\/\s*$/, "").replace(/\s*-->\s*$/, "").replace(/\s*-\}\s*$/, "").trim();
|
|
6215
|
+
if (message) {
|
|
6216
|
+
results.push({
|
|
6217
|
+
tag,
|
|
6218
|
+
message,
|
|
6219
|
+
file: filePath,
|
|
6220
|
+
line: i + 1,
|
|
6221
|
+
raw: line
|
|
6222
|
+
});
|
|
6223
|
+
}
|
|
6224
|
+
}
|
|
6225
|
+
}
|
|
6226
|
+
return results;
|
|
6227
|
+
}
|
|
6228
|
+
function collectFiles(basePath, extensions) {
|
|
6229
|
+
const stat = statSync2(basePath);
|
|
6230
|
+
if (stat.isFile()) {
|
|
6231
|
+
return [basePath];
|
|
6232
|
+
}
|
|
6233
|
+
const glob = new Bun.Glob("**/*");
|
|
6234
|
+
const files = [];
|
|
6235
|
+
for (const entry of glob.scanSync({ cwd: basePath, onlyFiles: true, dot: false })) {
|
|
6236
|
+
const parts = entry.split("/");
|
|
6237
|
+
if (parts.some((p) => SKIP_DIRS.has(p)))
|
|
6238
|
+
continue;
|
|
6239
|
+
const dotIdx = entry.lastIndexOf(".");
|
|
6240
|
+
if (dotIdx === -1)
|
|
6241
|
+
continue;
|
|
6242
|
+
const ext = entry.slice(dotIdx);
|
|
6243
|
+
if (!extensions.has(ext))
|
|
6244
|
+
continue;
|
|
6245
|
+
files.push(entry);
|
|
6246
|
+
}
|
|
6247
|
+
return files.sort();
|
|
6248
|
+
}
|
|
6249
|
+
function extractTodos(options, db) {
|
|
6250
|
+
const basePath = resolve2(options.path);
|
|
6251
|
+
const tags = options.patterns || [...EXTRACT_TAGS];
|
|
6252
|
+
const extensions = options.extensions ? new Set(options.extensions.map((e) => e.startsWith(".") ? e : `.${e}`)) : DEFAULT_EXTENSIONS;
|
|
6253
|
+
const files = collectFiles(basePath, extensions);
|
|
6254
|
+
const allComments = [];
|
|
6255
|
+
for (const file of files) {
|
|
6256
|
+
const fullPath = statSync2(basePath).isFile() ? basePath : join6(basePath, file);
|
|
6257
|
+
try {
|
|
6258
|
+
const source = readFileSync3(fullPath, "utf-8");
|
|
6259
|
+
const relPath = statSync2(basePath).isFile() ? relative(resolve2(basePath, ".."), fullPath) : file;
|
|
6260
|
+
const comments = extractFromSource(source, relPath, tags);
|
|
6261
|
+
allComments.push(...comments);
|
|
6262
|
+
} catch {}
|
|
6263
|
+
}
|
|
6264
|
+
if (options.dry_run) {
|
|
6265
|
+
return { comments: allComments, tasks: [], skipped: 0 };
|
|
6266
|
+
}
|
|
6267
|
+
const tasks = [];
|
|
6268
|
+
let skipped = 0;
|
|
6269
|
+
const existingTasks = options.project_id ? listTasks({ project_id: options.project_id, tags: ["extracted"] }, db) : listTasks({ tags: ["extracted"] }, db);
|
|
6270
|
+
const existingKeys = new Set;
|
|
6271
|
+
for (const t of existingTasks) {
|
|
6272
|
+
const meta = t.metadata;
|
|
6273
|
+
if (meta?.["source_file"] && meta?.["source_line"]) {
|
|
6274
|
+
existingKeys.add(`${meta["source_file"]}:${meta["source_line"]}`);
|
|
6275
|
+
}
|
|
6276
|
+
}
|
|
6277
|
+
for (const comment of allComments) {
|
|
6278
|
+
const dedupKey = `${comment.file}:${comment.line}`;
|
|
6279
|
+
if (existingKeys.has(dedupKey)) {
|
|
6280
|
+
skipped++;
|
|
6281
|
+
continue;
|
|
6282
|
+
}
|
|
6283
|
+
const taskTags = ["extracted", comment.tag.toLowerCase(), ...options.tags || []];
|
|
6284
|
+
const task = createTask({
|
|
6285
|
+
title: `[${comment.tag}] ${comment.message}`,
|
|
6286
|
+
description: `Extracted from code comment in \`${comment.file}\` at line ${comment.line}:
|
|
6287
|
+
\`\`\`
|
|
6288
|
+
${comment.raw.trim()}
|
|
6289
|
+
\`\`\``,
|
|
6290
|
+
priority: tagToPriority(comment.tag),
|
|
6291
|
+
project_id: options.project_id,
|
|
6292
|
+
task_list_id: options.task_list_id,
|
|
6293
|
+
assigned_to: options.assigned_to,
|
|
6294
|
+
agent_id: options.agent_id,
|
|
6295
|
+
tags: taskTags,
|
|
6296
|
+
metadata: {
|
|
6297
|
+
source: "code_comment",
|
|
6298
|
+
comment_type: comment.tag,
|
|
6299
|
+
source_file: comment.file,
|
|
6300
|
+
source_line: comment.line
|
|
6301
|
+
}
|
|
6302
|
+
}, db);
|
|
6303
|
+
addTaskFile({
|
|
6304
|
+
task_id: task.id,
|
|
6305
|
+
path: comment.file,
|
|
6306
|
+
note: `Line ${comment.line}: ${comment.tag} comment`
|
|
6307
|
+
}, db);
|
|
6308
|
+
tasks.push(task);
|
|
6309
|
+
existingKeys.add(dedupKey);
|
|
6310
|
+
}
|
|
6311
|
+
return { comments: allComments, tasks, skipped };
|
|
6312
|
+
}
|
|
6313
|
+
var EXTRACT_TAGS, DEFAULT_EXTENSIONS, SKIP_DIRS;
|
|
6314
|
+
var init_extract = __esm(() => {
|
|
6315
|
+
init_tasks();
|
|
6316
|
+
init_task_files();
|
|
6317
|
+
EXTRACT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"];
|
|
6318
|
+
DEFAULT_EXTENSIONS = new Set([
|
|
6319
|
+
".ts",
|
|
6320
|
+
".tsx",
|
|
6321
|
+
".js",
|
|
6322
|
+
".jsx",
|
|
6323
|
+
".mjs",
|
|
6324
|
+
".cjs",
|
|
6325
|
+
".py",
|
|
6326
|
+
".rb",
|
|
6327
|
+
".go",
|
|
6328
|
+
".rs",
|
|
6329
|
+
".c",
|
|
6330
|
+
".cpp",
|
|
6331
|
+
".h",
|
|
6332
|
+
".hpp",
|
|
6333
|
+
".java",
|
|
6334
|
+
".kt",
|
|
6335
|
+
".swift",
|
|
6336
|
+
".cs",
|
|
6337
|
+
".php",
|
|
6338
|
+
".sh",
|
|
6339
|
+
".bash",
|
|
6340
|
+
".zsh",
|
|
6341
|
+
".lua",
|
|
6342
|
+
".sql",
|
|
6343
|
+
".r",
|
|
6344
|
+
".R",
|
|
6345
|
+
".yaml",
|
|
6346
|
+
".yml",
|
|
6347
|
+
".toml",
|
|
6348
|
+
".css",
|
|
6349
|
+
".scss",
|
|
6350
|
+
".less",
|
|
6351
|
+
".vue",
|
|
6352
|
+
".svelte",
|
|
6353
|
+
".ex",
|
|
6354
|
+
".exs",
|
|
6355
|
+
".erl",
|
|
6356
|
+
".hs",
|
|
6357
|
+
".ml",
|
|
6358
|
+
".mli",
|
|
6359
|
+
".clj",
|
|
6360
|
+
".cljs"
|
|
6361
|
+
]);
|
|
6362
|
+
SKIP_DIRS = new Set([
|
|
6363
|
+
"node_modules",
|
|
6364
|
+
".git",
|
|
6365
|
+
"dist",
|
|
6366
|
+
"build",
|
|
6367
|
+
"out",
|
|
6368
|
+
".next",
|
|
6369
|
+
".turbo",
|
|
6370
|
+
"coverage",
|
|
6371
|
+
"__pycache__",
|
|
6372
|
+
".venv",
|
|
6373
|
+
"venv",
|
|
6374
|
+
"vendor",
|
|
6375
|
+
"target",
|
|
6376
|
+
".cache",
|
|
6377
|
+
".parcel-cache"
|
|
6378
|
+
]);
|
|
6379
|
+
});
|
|
6380
|
+
|
|
6381
|
+
// node_modules/zod/v3/helpers/util.js
|
|
6382
|
+
var util, objectUtil, ZodParsedType, getParsedType = (data) => {
|
|
6383
|
+
const t = typeof data;
|
|
6384
|
+
switch (t) {
|
|
6385
|
+
case "undefined":
|
|
6386
|
+
return ZodParsedType.undefined;
|
|
6387
|
+
case "string":
|
|
6388
|
+
return ZodParsedType.string;
|
|
6389
|
+
case "number":
|
|
6390
|
+
return Number.isNaN(data) ? ZodParsedType.nan : ZodParsedType.number;
|
|
6391
|
+
case "boolean":
|
|
6392
|
+
return ZodParsedType.boolean;
|
|
6393
|
+
case "function":
|
|
6394
|
+
return ZodParsedType.function;
|
|
6395
|
+
case "bigint":
|
|
6396
|
+
return ZodParsedType.bigint;
|
|
6397
|
+
case "symbol":
|
|
6398
|
+
return ZodParsedType.symbol;
|
|
6399
|
+
case "object":
|
|
6400
|
+
if (Array.isArray(data)) {
|
|
6401
|
+
return ZodParsedType.array;
|
|
6402
|
+
}
|
|
6403
|
+
if (data === null) {
|
|
6404
|
+
return ZodParsedType.null;
|
|
6405
|
+
}
|
|
6406
|
+
if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") {
|
|
6407
|
+
return ZodParsedType.promise;
|
|
6408
|
+
}
|
|
6409
|
+
if (typeof Map !== "undefined" && data instanceof Map) {
|
|
6410
|
+
return ZodParsedType.map;
|
|
6411
|
+
}
|
|
6412
|
+
if (typeof Set !== "undefined" && data instanceof Set) {
|
|
6413
|
+
return ZodParsedType.set;
|
|
6414
|
+
}
|
|
6415
|
+
if (typeof Date !== "undefined" && data instanceof Date) {
|
|
6416
|
+
return ZodParsedType.date;
|
|
6417
|
+
}
|
|
6418
|
+
return ZodParsedType.object;
|
|
6419
|
+
default:
|
|
6420
|
+
return ZodParsedType.unknown;
|
|
6421
|
+
}
|
|
6422
|
+
};
|
|
6423
|
+
var init_util = __esm(() => {
|
|
6424
|
+
(function(util2) {
|
|
6425
|
+
util2.assertEqual = (_) => {};
|
|
6426
|
+
function assertIs(_arg) {}
|
|
6427
|
+
util2.assertIs = assertIs;
|
|
6428
|
+
function assertNever(_x) {
|
|
6429
|
+
throw new Error;
|
|
6430
|
+
}
|
|
6431
|
+
util2.assertNever = assertNever;
|
|
6432
|
+
util2.arrayToEnum = (items) => {
|
|
6433
|
+
const obj = {};
|
|
6434
|
+
for (const item of items) {
|
|
6435
|
+
obj[item] = item;
|
|
6436
|
+
}
|
|
6437
|
+
return obj;
|
|
6438
|
+
};
|
|
6439
|
+
util2.getValidEnumValues = (obj) => {
|
|
6440
|
+
const validKeys = util2.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number");
|
|
6441
|
+
const filtered = {};
|
|
6442
|
+
for (const k of validKeys) {
|
|
6443
|
+
filtered[k] = obj[k];
|
|
6444
|
+
}
|
|
6445
|
+
return util2.objectValues(filtered);
|
|
6446
|
+
};
|
|
6447
|
+
util2.objectValues = (obj) => {
|
|
6448
|
+
return util2.objectKeys(obj).map(function(e) {
|
|
6449
|
+
return obj[e];
|
|
6450
|
+
});
|
|
6451
|
+
};
|
|
6452
|
+
util2.objectKeys = typeof Object.keys === "function" ? (obj) => Object.keys(obj) : (object) => {
|
|
6453
|
+
const keys = [];
|
|
6454
|
+
for (const key in object) {
|
|
6455
|
+
if (Object.prototype.hasOwnProperty.call(object, key)) {
|
|
6456
|
+
keys.push(key);
|
|
6457
|
+
}
|
|
6458
|
+
}
|
|
6459
|
+
return keys;
|
|
6460
|
+
};
|
|
6461
|
+
util2.find = (arr, checker) => {
|
|
6462
|
+
for (const item of arr) {
|
|
6463
|
+
if (checker(item))
|
|
6464
|
+
return item;
|
|
6465
|
+
}
|
|
6466
|
+
return;
|
|
6467
|
+
};
|
|
6468
|
+
util2.isInteger = typeof Number.isInteger === "function" ? (val) => Number.isInteger(val) : (val) => typeof val === "number" && Number.isFinite(val) && Math.floor(val) === val;
|
|
6469
|
+
function joinValues(array, separator = " | ") {
|
|
6470
|
+
return array.map((val) => typeof val === "string" ? `'${val}'` : val).join(separator);
|
|
6471
|
+
}
|
|
6472
|
+
util2.joinValues = joinValues;
|
|
6473
|
+
util2.jsonStringifyReplacer = (_, value) => {
|
|
6474
|
+
if (typeof value === "bigint") {
|
|
6475
|
+
return value.toString();
|
|
6476
|
+
}
|
|
6477
|
+
return value;
|
|
6478
|
+
};
|
|
6479
|
+
})(util || (util = {}));
|
|
6480
|
+
(function(objectUtil2) {
|
|
6481
|
+
objectUtil2.mergeShapes = (first, second) => {
|
|
6482
|
+
return {
|
|
6483
|
+
...first,
|
|
6484
|
+
...second
|
|
6485
|
+
};
|
|
6486
|
+
};
|
|
6487
|
+
})(objectUtil || (objectUtil = {}));
|
|
6488
|
+
ZodParsedType = util.arrayToEnum([
|
|
6489
|
+
"string",
|
|
6490
|
+
"nan",
|
|
6491
|
+
"number",
|
|
6492
|
+
"integer",
|
|
6493
|
+
"float",
|
|
6494
|
+
"boolean",
|
|
6495
|
+
"date",
|
|
6496
|
+
"bigint",
|
|
6497
|
+
"symbol",
|
|
6498
|
+
"function",
|
|
6499
|
+
"undefined",
|
|
6500
|
+
"null",
|
|
6501
|
+
"array",
|
|
6502
|
+
"object",
|
|
6503
|
+
"unknown",
|
|
6504
|
+
"promise",
|
|
6505
|
+
"void",
|
|
6506
|
+
"never",
|
|
6507
|
+
"map",
|
|
6508
|
+
"set"
|
|
6509
|
+
]);
|
|
6510
|
+
});
|
|
6511
|
+
|
|
6512
|
+
// node_modules/zod/v3/ZodError.js
|
|
6513
|
+
var ZodIssueCode, quotelessJson = (obj) => {
|
|
6003
6514
|
const json = JSON.stringify(obj, null, 2);
|
|
6004
6515
|
return json.replace(/"([^"]+)":/g, "$1:");
|
|
6005
6516
|
}, ZodError;
|
|
@@ -9833,47 +10344,148 @@ var init_zod = __esm(() => {
|
|
|
9833
10344
|
init_external();
|
|
9834
10345
|
});
|
|
9835
10346
|
|
|
9836
|
-
// src/
|
|
9837
|
-
var
|
|
9838
|
-
__export(
|
|
9839
|
-
|
|
9840
|
-
|
|
9841
|
-
|
|
9842
|
-
findTaskByCommit: () => findTaskByCommit
|
|
10347
|
+
// src/lib/github.ts
|
|
10348
|
+
var exports_github = {};
|
|
10349
|
+
__export(exports_github, {
|
|
10350
|
+
parseGitHubUrl: () => parseGitHubUrl,
|
|
10351
|
+
issueToTask: () => issueToTask,
|
|
10352
|
+
fetchGitHubIssue: () => fetchGitHubIssue
|
|
9843
10353
|
});
|
|
9844
|
-
|
|
10354
|
+
import { execSync } from "child_process";
|
|
10355
|
+
function parseGitHubUrl(url) {
|
|
10356
|
+
const match = url.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
|
|
10357
|
+
if (!match)
|
|
10358
|
+
return null;
|
|
10359
|
+
return { owner: match[1], repo: match[2], number: parseInt(match[3], 10) };
|
|
10360
|
+
}
|
|
10361
|
+
function fetchGitHubIssue(owner, repo, number) {
|
|
10362
|
+
const json = execSync(`gh api repos/${owner}/${repo}/issues/${number}`, { encoding: "utf-8", timeout: 15000 });
|
|
10363
|
+
const data = JSON.parse(json);
|
|
9845
10364
|
return {
|
|
9846
|
-
|
|
9847
|
-
|
|
10365
|
+
number: data.number,
|
|
10366
|
+
title: data.title,
|
|
10367
|
+
body: data.body,
|
|
10368
|
+
labels: (data.labels || []).map((l) => l.name),
|
|
10369
|
+
state: data.state,
|
|
10370
|
+
assignee: data.assignee?.login || null,
|
|
10371
|
+
url: data.html_url
|
|
9848
10372
|
};
|
|
9849
10373
|
}
|
|
9850
|
-
function
|
|
9851
|
-
const
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
10374
|
+
function issueToTask(issue, opts) {
|
|
10375
|
+
const labelToPriority = {
|
|
10376
|
+
critical: "critical",
|
|
10377
|
+
"priority:critical": "critical",
|
|
10378
|
+
high: "high",
|
|
10379
|
+
"priority:high": "high",
|
|
10380
|
+
urgent: "high",
|
|
10381
|
+
low: "low",
|
|
10382
|
+
"priority:low": "low"
|
|
10383
|
+
};
|
|
10384
|
+
let priority = "medium";
|
|
10385
|
+
for (const label of issue.labels) {
|
|
10386
|
+
const mapped = labelToPriority[label.toLowerCase()];
|
|
10387
|
+
if (mapped) {
|
|
10388
|
+
priority = mapped;
|
|
10389
|
+
break;
|
|
10390
|
+
}
|
|
9856
10391
|
}
|
|
9857
|
-
|
|
9858
|
-
|
|
9859
|
-
|
|
9860
|
-
|
|
9861
|
-
|
|
9862
|
-
|
|
9863
|
-
|
|
10392
|
+
return {
|
|
10393
|
+
title: `[GH#${issue.number}] ${issue.title}`,
|
|
10394
|
+
description: issue.body ? issue.body.slice(0, 4000) : undefined,
|
|
10395
|
+
tags: issue.labels.slice(0, 10),
|
|
10396
|
+
priority,
|
|
10397
|
+
metadata: { github_url: issue.url, github_number: issue.number, github_state: issue.state },
|
|
10398
|
+
project_id: opts?.project_id,
|
|
10399
|
+
task_list_id: opts?.task_list_id,
|
|
10400
|
+
agent_id: opts?.agent_id
|
|
10401
|
+
};
|
|
9864
10402
|
}
|
|
9865
|
-
|
|
10403
|
+
var init_github = () => {};
|
|
10404
|
+
|
|
10405
|
+
// src/lib/burndown.ts
|
|
10406
|
+
var exports_burndown = {};
|
|
10407
|
+
__export(exports_burndown, {
|
|
10408
|
+
getBurndown: () => getBurndown
|
|
10409
|
+
});
|
|
10410
|
+
function getBurndown(opts, db) {
|
|
9866
10411
|
const d = db || getDatabase();
|
|
9867
|
-
const
|
|
9868
|
-
|
|
9869
|
-
|
|
9870
|
-
|
|
10412
|
+
const conditions = [];
|
|
10413
|
+
const params = [];
|
|
10414
|
+
if (opts.plan_id) {
|
|
10415
|
+
conditions.push("plan_id = ?");
|
|
10416
|
+
params.push(opts.plan_id);
|
|
10417
|
+
}
|
|
10418
|
+
if (opts.project_id) {
|
|
10419
|
+
conditions.push("project_id = ?");
|
|
10420
|
+
params.push(opts.project_id);
|
|
10421
|
+
}
|
|
10422
|
+
if (opts.task_list_id) {
|
|
10423
|
+
conditions.push("task_list_id = ?");
|
|
10424
|
+
params.push(opts.task_list_id);
|
|
10425
|
+
}
|
|
10426
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
10427
|
+
const total = d.query(`SELECT COUNT(*) as c FROM tasks ${where}`).get(...params).c;
|
|
10428
|
+
const completed = d.query(`SELECT COUNT(*) as c FROM tasks ${where}${where ? " AND" : " WHERE"} status = 'completed'`).get(...params).c;
|
|
10429
|
+
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);
|
|
10430
|
+
const firstTask = d.query(`SELECT MIN(created_at) as min_date FROM tasks ${where}`).get(...params);
|
|
10431
|
+
const startDate = firstTask?.min_date ? new Date(firstTask.min_date) : new Date;
|
|
10432
|
+
const endDate = new Date;
|
|
10433
|
+
const days = [];
|
|
10434
|
+
const totalDays = Math.max(1, Math.ceil((endDate.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000)));
|
|
10435
|
+
let cumulative = 0;
|
|
10436
|
+
const completionMap = new Map(completions.map((c) => [c.date, c.count]));
|
|
10437
|
+
const current = new Date(startDate);
|
|
10438
|
+
for (let i = 0;i <= totalDays; i++) {
|
|
10439
|
+
const dateStr = current.toISOString().slice(0, 10);
|
|
10440
|
+
cumulative += completionMap.get(dateStr) || 0;
|
|
10441
|
+
days.push({
|
|
10442
|
+
date: dateStr,
|
|
10443
|
+
completed_cumulative: cumulative,
|
|
10444
|
+
ideal: Math.round(total / totalDays * i)
|
|
10445
|
+
});
|
|
10446
|
+
current.setDate(current.getDate() + 1);
|
|
10447
|
+
}
|
|
10448
|
+
const chart = renderBurndownChart(total, days);
|
|
10449
|
+
return { total, completed, remaining: total - completed, days, chart };
|
|
9871
10450
|
}
|
|
9872
|
-
function
|
|
9873
|
-
const
|
|
9874
|
-
|
|
10451
|
+
function renderBurndownChart(total, days) {
|
|
10452
|
+
const height = 12;
|
|
10453
|
+
const width = Math.min(60, days.length);
|
|
10454
|
+
const step = Math.max(1, Math.floor(days.length / width));
|
|
10455
|
+
const sampled = days.filter((_, i) => i % step === 0 || i === days.length - 1).slice(0, width);
|
|
10456
|
+
const lines = [];
|
|
10457
|
+
lines.push(` ${total} \u2524`);
|
|
10458
|
+
for (let row = height - 1;row >= 0; row--) {
|
|
10459
|
+
const threshold = Math.round(total / height * row);
|
|
10460
|
+
let line = "";
|
|
10461
|
+
for (const day of sampled) {
|
|
10462
|
+
const remaining = total - day.completed_cumulative;
|
|
10463
|
+
const idealRemaining = total - day.ideal;
|
|
10464
|
+
if (remaining >= threshold && remaining > threshold - Math.round(total / height)) {
|
|
10465
|
+
line += "\u2588";
|
|
10466
|
+
} else if (idealRemaining >= threshold && idealRemaining > threshold - Math.round(total / height)) {
|
|
10467
|
+
line += "\xB7";
|
|
10468
|
+
} else {
|
|
10469
|
+
line += " ";
|
|
10470
|
+
}
|
|
10471
|
+
}
|
|
10472
|
+
const label = String(threshold).padStart(4);
|
|
10473
|
+
lines.push(`${label} \u2524${line}`);
|
|
10474
|
+
}
|
|
10475
|
+
lines.push(` 0 \u2524${"\u2500".repeat(sampled.length)}`);
|
|
10476
|
+
lines.push(` \u2514${"\u2500".repeat(sampled.length)}`);
|
|
10477
|
+
if (sampled.length > 0) {
|
|
10478
|
+
const first = sampled[0].date.slice(5);
|
|
10479
|
+
const last = sampled[sampled.length - 1].date.slice(5);
|
|
10480
|
+
const pad = sampled.length - first.length - last.length;
|
|
10481
|
+
lines.push(` ${first}${" ".repeat(Math.max(1, pad))}${last}`);
|
|
10482
|
+
}
|
|
10483
|
+
lines.push("");
|
|
10484
|
+
lines.push(` \u2588 actual remaining \xB7 ideal burndown`);
|
|
10485
|
+
return lines.join(`
|
|
10486
|
+
`);
|
|
9875
10487
|
}
|
|
9876
|
-
var
|
|
10488
|
+
var init_burndown = __esm(() => {
|
|
9877
10489
|
init_database();
|
|
9878
10490
|
});
|
|
9879
10491
|
|
|
@@ -9919,283 +10531,117 @@ function buildPrompt(task, agents) {
|
|
|
9919
10531
|
return `You are a task routing assistant. Given a task and available agents, choose the SINGLE best agent.
|
|
9920
10532
|
|
|
9921
10533
|
TASK:
|
|
9922
|
-
Title: ${task.title}
|
|
9923
|
-
Priority: ${task.priority}
|
|
9924
|
-
Tags: ${task.tags.join(", ") || "none"}
|
|
9925
|
-
Description: ${task.description?.slice(0, 300) || "none"}
|
|
9926
|
-
|
|
9927
|
-
AVAILABLE AGENTS:
|
|
9928
|
-
${agentList}
|
|
9929
|
-
|
|
9930
|
-
Rules:
|
|
9931
|
-
- Match task tags/content to agent capabilities
|
|
9932
|
-
- Prefer agents with fewer active tasks
|
|
9933
|
-
- Prefer agents whose role fits the task (lead for critical, developer for features, qa for testing)
|
|
9934
|
-
- If no clear match, pick the agent with fewest active tasks
|
|
9935
|
-
|
|
9936
|
-
Respond with ONLY a JSON object: {"agent_name": "<name>", "reason": "<one sentence>"}`;
|
|
9937
|
-
}
|
|
9938
|
-
async function callCerebras(prompt, apiKey) {
|
|
9939
|
-
try {
|
|
9940
|
-
const resp = await fetch(CEREBRAS_API_URL, {
|
|
9941
|
-
method: "POST",
|
|
9942
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
9943
|
-
body: JSON.stringify({
|
|
9944
|
-
model: CEREBRAS_MODEL,
|
|
9945
|
-
messages: [{ role: "user", content: prompt }],
|
|
9946
|
-
max_tokens: 150,
|
|
9947
|
-
temperature: 0
|
|
9948
|
-
}),
|
|
9949
|
-
signal: AbortSignal.timeout(1e4)
|
|
9950
|
-
});
|
|
9951
|
-
if (!resp.ok)
|
|
9952
|
-
return null;
|
|
9953
|
-
const data = await resp.json();
|
|
9954
|
-
const content = data?.choices?.[0]?.message?.content?.trim();
|
|
9955
|
-
if (!content)
|
|
9956
|
-
return null;
|
|
9957
|
-
const match = content.match(/\{[^}]+\}/s);
|
|
9958
|
-
if (!match)
|
|
9959
|
-
return null;
|
|
9960
|
-
return JSON.parse(match[0]);
|
|
9961
|
-
} catch {
|
|
9962
|
-
return null;
|
|
9963
|
-
}
|
|
9964
|
-
}
|
|
9965
|
-
async function autoAssignTask(taskId, db) {
|
|
9966
|
-
const d = db || getDatabase();
|
|
9967
|
-
const task = getTask(taskId, d);
|
|
9968
|
-
if (!task)
|
|
9969
|
-
throw new Error(`Task ${taskId} not found`);
|
|
9970
|
-
const agents = listAgents(d).filter((a) => a.status === "active");
|
|
9971
|
-
if (agents.length === 0) {
|
|
9972
|
-
return { task_id: taskId, assigned_to: null, agent_name: null, method: "no_agents" };
|
|
9973
|
-
}
|
|
9974
|
-
const workloads = getAgentWorkloads(d);
|
|
9975
|
-
const apiKey = process.env["CEREBRAS_API_KEY"];
|
|
9976
|
-
let selectedAgent = null;
|
|
9977
|
-
let method = "capability_match";
|
|
9978
|
-
let reason;
|
|
9979
|
-
if (apiKey) {
|
|
9980
|
-
const agentData = agents.map((a) => ({
|
|
9981
|
-
id: a.id,
|
|
9982
|
-
name: a.name,
|
|
9983
|
-
role: a.role || "agent",
|
|
9984
|
-
capabilities: a.capabilities || [],
|
|
9985
|
-
in_progress_tasks: workloads.get(a.id) ?? 0
|
|
9986
|
-
}));
|
|
9987
|
-
const result = await callCerebras(buildPrompt({
|
|
9988
|
-
title: task.title,
|
|
9989
|
-
description: task.description,
|
|
9990
|
-
priority: task.priority,
|
|
9991
|
-
tags: task.tags || []
|
|
9992
|
-
}, agentData), apiKey);
|
|
9993
|
-
if (result?.agent_name) {
|
|
9994
|
-
selectedAgent = agents.find((a) => a.name === result.agent_name) ?? null;
|
|
9995
|
-
if (selectedAgent) {
|
|
9996
|
-
method = "cerebras";
|
|
9997
|
-
reason = result.reason;
|
|
9998
|
-
}
|
|
9999
|
-
}
|
|
10000
|
-
}
|
|
10001
|
-
if (!selectedAgent) {
|
|
10002
|
-
const taskTags = task.tags || [];
|
|
10003
|
-
const capable = getCapableAgents(taskTags, { min_score: 0, limit: 10 }, d);
|
|
10004
|
-
if (capable.length > 0) {
|
|
10005
|
-
const sorted = capable.sort((a, b) => {
|
|
10006
|
-
if (b.score !== a.score)
|
|
10007
|
-
return b.score - a.score;
|
|
10008
|
-
return (workloads.get(a.agent.id) ?? 0) - (workloads.get(b.agent.id) ?? 0);
|
|
10009
|
-
});
|
|
10010
|
-
selectedAgent = sorted[0].agent;
|
|
10011
|
-
reason = `Capability match (score: ${sorted[0].score.toFixed(2)})`;
|
|
10012
|
-
} else {
|
|
10013
|
-
selectedAgent = agents.slice().sort((a, b) => (workloads.get(a.id) ?? 0) - (workloads.get(b.id) ?? 0))[0];
|
|
10014
|
-
reason = `Least busy agent (${workloads.get(selectedAgent.id) ?? 0} active tasks)`;
|
|
10015
|
-
}
|
|
10016
|
-
}
|
|
10017
|
-
if (selectedAgent) {
|
|
10018
|
-
updateTask(taskId, { assigned_to: selectedAgent.id, version: task.version }, d);
|
|
10019
|
-
}
|
|
10020
|
-
return {
|
|
10021
|
-
task_id: taskId,
|
|
10022
|
-
assigned_to: selectedAgent?.id ?? null,
|
|
10023
|
-
agent_name: selectedAgent?.name ?? null,
|
|
10024
|
-
method,
|
|
10025
|
-
reason
|
|
10026
|
-
};
|
|
10027
|
-
}
|
|
10028
|
-
var CEREBRAS_API_URL = "https://api.cerebras.ai/v1/chat/completions", CEREBRAS_MODEL = "llama-3.3-70b";
|
|
10029
|
-
var init_auto_assign = __esm(() => {
|
|
10030
|
-
init_database();
|
|
10031
|
-
init_tasks();
|
|
10032
|
-
init_agents();
|
|
10033
|
-
});
|
|
10034
|
-
|
|
10035
|
-
// src/db/task-files.ts
|
|
10036
|
-
var exports_task_files = {};
|
|
10037
|
-
__export(exports_task_files, {
|
|
10038
|
-
updateTaskFileStatus: () => updateTaskFileStatus,
|
|
10039
|
-
removeTaskFile: () => removeTaskFile,
|
|
10040
|
-
listTaskFiles: () => listTaskFiles,
|
|
10041
|
-
listActiveFiles: () => listActiveFiles,
|
|
10042
|
-
getTaskFile: () => getTaskFile,
|
|
10043
|
-
getFileHeatMap: () => getFileHeatMap,
|
|
10044
|
-
findTasksByFile: () => findTasksByFile,
|
|
10045
|
-
detectFileConflicts: () => detectFileConflicts,
|
|
10046
|
-
bulkFindTasksByFiles: () => bulkFindTasksByFiles,
|
|
10047
|
-
bulkAddTaskFiles: () => bulkAddTaskFiles,
|
|
10048
|
-
addTaskFile: () => addTaskFile
|
|
10049
|
-
});
|
|
10050
|
-
function addTaskFile(input, db) {
|
|
10051
|
-
const d = db || getDatabase();
|
|
10052
|
-
const id = uuid();
|
|
10053
|
-
const timestamp = now();
|
|
10054
|
-
const existing = d.query("SELECT id FROM task_files WHERE task_id = ? AND path = ?").get(input.task_id, input.path);
|
|
10055
|
-
if (existing) {
|
|
10056
|
-
d.run("UPDATE task_files SET status = ?, agent_id = ?, note = ?, updated_at = ? WHERE id = ?", [input.status || "active", input.agent_id || null, input.note || null, timestamp, existing.id]);
|
|
10057
|
-
return getTaskFile(existing.id, d);
|
|
10058
|
-
}
|
|
10059
|
-
d.run(`INSERT INTO task_files (id, task_id, path, status, agent_id, note, created_at, updated_at)
|
|
10060
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.task_id, input.path, input.status || "active", input.agent_id || null, input.note || null, timestamp, timestamp]);
|
|
10061
|
-
return getTaskFile(id, d);
|
|
10062
|
-
}
|
|
10063
|
-
function getTaskFile(id, db) {
|
|
10064
|
-
const d = db || getDatabase();
|
|
10065
|
-
return d.query("SELECT * FROM task_files WHERE id = ?").get(id);
|
|
10066
|
-
}
|
|
10067
|
-
function listTaskFiles(taskId, db) {
|
|
10068
|
-
const d = db || getDatabase();
|
|
10069
|
-
return d.query("SELECT * FROM task_files WHERE task_id = ? ORDER BY path").all(taskId);
|
|
10070
|
-
}
|
|
10071
|
-
function findTasksByFile(path, db) {
|
|
10072
|
-
const d = db || getDatabase();
|
|
10073
|
-
return d.query("SELECT * FROM task_files WHERE path = ? AND status != 'removed' ORDER BY updated_at DESC").all(path);
|
|
10074
|
-
}
|
|
10075
|
-
function updateTaskFileStatus(taskId, path, status, agentId, db) {
|
|
10076
|
-
const d = db || getDatabase();
|
|
10077
|
-
const timestamp = now();
|
|
10078
|
-
d.run("UPDATE task_files SET status = ?, agent_id = COALESCE(?, agent_id), updated_at = ? WHERE task_id = ? AND path = ?", [status, agentId || null, timestamp, taskId, path]);
|
|
10079
|
-
const row = d.query("SELECT * FROM task_files WHERE task_id = ? AND path = ?").get(taskId, path);
|
|
10080
|
-
return row;
|
|
10081
|
-
}
|
|
10082
|
-
function removeTaskFile(taskId, path, db) {
|
|
10083
|
-
const d = db || getDatabase();
|
|
10084
|
-
const result = d.run("DELETE FROM task_files WHERE task_id = ? AND path = ?", [taskId, path]);
|
|
10085
|
-
return result.changes > 0;
|
|
10086
|
-
}
|
|
10087
|
-
function detectFileConflicts(taskId, paths, db) {
|
|
10088
|
-
const d = db || getDatabase();
|
|
10089
|
-
if (paths.length === 0)
|
|
10090
|
-
return [];
|
|
10091
|
-
const placeholders = paths.map(() => "?").join(", ");
|
|
10092
|
-
const rows = d.query(`
|
|
10093
|
-
SELECT tf.path, tf.agent_id AS conflicting_agent_id, t.id AS conflicting_task_id,
|
|
10094
|
-
t.title AS conflicting_task_title, t.status AS conflicting_task_status
|
|
10095
|
-
FROM task_files tf
|
|
10096
|
-
JOIN tasks t ON tf.task_id = t.id
|
|
10097
|
-
WHERE tf.path IN (${placeholders})
|
|
10098
|
-
AND tf.task_id != ?
|
|
10099
|
-
AND tf.status != 'removed'
|
|
10100
|
-
AND t.status = 'in_progress'
|
|
10101
|
-
ORDER BY tf.updated_at DESC
|
|
10102
|
-
`).all(...paths, taskId);
|
|
10103
|
-
return rows;
|
|
10104
|
-
}
|
|
10105
|
-
function bulkFindTasksByFiles(paths, db) {
|
|
10106
|
-
const d = db || getDatabase();
|
|
10107
|
-
if (paths.length === 0)
|
|
10108
|
-
return [];
|
|
10109
|
-
const placeholders = paths.map(() => "?").join(", ");
|
|
10110
|
-
const rows = d.query(`SELECT tf.*, t.status AS task_status FROM task_files tf
|
|
10111
|
-
JOIN tasks t ON tf.task_id = t.id
|
|
10112
|
-
WHERE tf.path IN (${placeholders}) AND tf.status != 'removed'
|
|
10113
|
-
ORDER BY tf.updated_at DESC`).all(...paths);
|
|
10114
|
-
const byPath = new Map;
|
|
10115
|
-
for (const path of paths)
|
|
10116
|
-
byPath.set(path, []);
|
|
10117
|
-
for (const row of rows) {
|
|
10118
|
-
byPath.get(row.path)?.push(row);
|
|
10119
|
-
}
|
|
10120
|
-
return paths.map((path) => {
|
|
10121
|
-
const tasks = byPath.get(path) ?? [];
|
|
10122
|
-
const inProgressCount = tasks.filter((t) => t.task_status === "in_progress").length;
|
|
10123
|
-
return {
|
|
10124
|
-
path,
|
|
10125
|
-
tasks,
|
|
10126
|
-
has_conflict: inProgressCount > 1,
|
|
10127
|
-
in_progress_count: inProgressCount
|
|
10128
|
-
};
|
|
10129
|
-
});
|
|
10130
|
-
}
|
|
10131
|
-
function listActiveFiles(db) {
|
|
10132
|
-
const d = db || getDatabase();
|
|
10133
|
-
return d.query(`
|
|
10134
|
-
SELECT
|
|
10135
|
-
tf.path,
|
|
10136
|
-
tf.status AS file_status,
|
|
10137
|
-
tf.agent_id AS file_agent_id,
|
|
10138
|
-
tf.note,
|
|
10139
|
-
tf.updated_at,
|
|
10140
|
-
t.id AS task_id,
|
|
10141
|
-
t.short_id AS task_short_id,
|
|
10142
|
-
t.title AS task_title,
|
|
10143
|
-
t.status AS task_status,
|
|
10144
|
-
t.locked_by AS task_locked_by,
|
|
10145
|
-
t.locked_at AS task_locked_at,
|
|
10146
|
-
a.id AS agent_id,
|
|
10147
|
-
a.name AS agent_name
|
|
10148
|
-
FROM task_files tf
|
|
10149
|
-
JOIN tasks t ON tf.task_id = t.id
|
|
10150
|
-
LEFT JOIN agents a ON (tf.agent_id = a.id OR (tf.agent_id IS NULL AND t.assigned_to = a.id))
|
|
10151
|
-
WHERE t.status = 'in_progress'
|
|
10152
|
-
AND tf.status != 'removed'
|
|
10153
|
-
ORDER BY tf.updated_at DESC
|
|
10154
|
-
`).all();
|
|
10534
|
+
Title: ${task.title}
|
|
10535
|
+
Priority: ${task.priority}
|
|
10536
|
+
Tags: ${task.tags.join(", ") || "none"}
|
|
10537
|
+
Description: ${task.description?.slice(0, 300) || "none"}
|
|
10538
|
+
|
|
10539
|
+
AVAILABLE AGENTS:
|
|
10540
|
+
${agentList}
|
|
10541
|
+
|
|
10542
|
+
Rules:
|
|
10543
|
+
- Match task tags/content to agent capabilities
|
|
10544
|
+
- Prefer agents with fewer active tasks
|
|
10545
|
+
- Prefer agents whose role fits the task (lead for critical, developer for features, qa for testing)
|
|
10546
|
+
- If no clear match, pick the agent with fewest active tasks
|
|
10547
|
+
|
|
10548
|
+
Respond with ONLY a JSON object: {"agent_name": "<name>", "reason": "<one sentence>"}`;
|
|
10155
10549
|
}
|
|
10156
|
-
function
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
last_edited_at: r.last_edited_at,
|
|
10183
|
-
active_task_count: r.active_task_count
|
|
10184
|
-
}));
|
|
10550
|
+
async function callCerebras(prompt, apiKey) {
|
|
10551
|
+
try {
|
|
10552
|
+
const resp = await fetch(CEREBRAS_API_URL, {
|
|
10553
|
+
method: "POST",
|
|
10554
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
10555
|
+
body: JSON.stringify({
|
|
10556
|
+
model: CEREBRAS_MODEL,
|
|
10557
|
+
messages: [{ role: "user", content: prompt }],
|
|
10558
|
+
max_tokens: 150,
|
|
10559
|
+
temperature: 0
|
|
10560
|
+
}),
|
|
10561
|
+
signal: AbortSignal.timeout(1e4)
|
|
10562
|
+
});
|
|
10563
|
+
if (!resp.ok)
|
|
10564
|
+
return null;
|
|
10565
|
+
const data = await resp.json();
|
|
10566
|
+
const content = data?.choices?.[0]?.message?.content?.trim();
|
|
10567
|
+
if (!content)
|
|
10568
|
+
return null;
|
|
10569
|
+
const match = content.match(/\{[^}]+\}/s);
|
|
10570
|
+
if (!match)
|
|
10571
|
+
return null;
|
|
10572
|
+
return JSON.parse(match[0]);
|
|
10573
|
+
} catch {
|
|
10574
|
+
return null;
|
|
10575
|
+
}
|
|
10185
10576
|
}
|
|
10186
|
-
function
|
|
10577
|
+
async function autoAssignTask(taskId, db) {
|
|
10187
10578
|
const d = db || getDatabase();
|
|
10188
|
-
const
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10579
|
+
const task = getTask(taskId, d);
|
|
10580
|
+
if (!task)
|
|
10581
|
+
throw new Error(`Task ${taskId} not found`);
|
|
10582
|
+
const agents = listAgents(d).filter((a) => a.status === "active");
|
|
10583
|
+
if (agents.length === 0) {
|
|
10584
|
+
return { task_id: taskId, assigned_to: null, agent_name: null, method: "no_agents" };
|
|
10585
|
+
}
|
|
10586
|
+
const workloads = getAgentWorkloads(d);
|
|
10587
|
+
const apiKey = process.env["CEREBRAS_API_KEY"];
|
|
10588
|
+
let selectedAgent = null;
|
|
10589
|
+
let method = "capability_match";
|
|
10590
|
+
let reason;
|
|
10591
|
+
if (apiKey) {
|
|
10592
|
+
const agentData = agents.map((a) => ({
|
|
10593
|
+
id: a.id,
|
|
10594
|
+
name: a.name,
|
|
10595
|
+
role: a.role || "agent",
|
|
10596
|
+
capabilities: a.capabilities || [],
|
|
10597
|
+
in_progress_tasks: workloads.get(a.id) ?? 0
|
|
10598
|
+
}));
|
|
10599
|
+
const result = await callCerebras(buildPrompt({
|
|
10600
|
+
title: task.title,
|
|
10601
|
+
description: task.description,
|
|
10602
|
+
priority: task.priority,
|
|
10603
|
+
tags: task.tags || []
|
|
10604
|
+
}, agentData), apiKey);
|
|
10605
|
+
if (result?.agent_name) {
|
|
10606
|
+
selectedAgent = agents.find((a) => a.name === result.agent_name) ?? null;
|
|
10607
|
+
if (selectedAgent) {
|
|
10608
|
+
method = "cerebras";
|
|
10609
|
+
reason = result.reason;
|
|
10610
|
+
}
|
|
10192
10611
|
}
|
|
10193
|
-
}
|
|
10194
|
-
|
|
10195
|
-
|
|
10612
|
+
}
|
|
10613
|
+
if (!selectedAgent) {
|
|
10614
|
+
const taskTags = task.tags || [];
|
|
10615
|
+
const capable = getCapableAgents(taskTags, { min_score: 0, limit: 10 }, d);
|
|
10616
|
+
if (capable.length > 0) {
|
|
10617
|
+
const sorted = capable.sort((a, b) => {
|
|
10618
|
+
if (b.score !== a.score)
|
|
10619
|
+
return b.score - a.score;
|
|
10620
|
+
return (workloads.get(a.agent.id) ?? 0) - (workloads.get(b.agent.id) ?? 0);
|
|
10621
|
+
});
|
|
10622
|
+
selectedAgent = sorted[0].agent;
|
|
10623
|
+
reason = `Capability match (score: ${sorted[0].score.toFixed(2)})`;
|
|
10624
|
+
} else {
|
|
10625
|
+
selectedAgent = agents.slice().sort((a, b) => (workloads.get(a.id) ?? 0) - (workloads.get(b.id) ?? 0))[0];
|
|
10626
|
+
reason = `Least busy agent (${workloads.get(selectedAgent.id) ?? 0} active tasks)`;
|
|
10627
|
+
}
|
|
10628
|
+
}
|
|
10629
|
+
if (selectedAgent) {
|
|
10630
|
+
updateTask(taskId, { assigned_to: selectedAgent.id, version: task.version }, d);
|
|
10631
|
+
}
|
|
10632
|
+
return {
|
|
10633
|
+
task_id: taskId,
|
|
10634
|
+
assigned_to: selectedAgent?.id ?? null,
|
|
10635
|
+
agent_name: selectedAgent?.name ?? null,
|
|
10636
|
+
method,
|
|
10637
|
+
reason
|
|
10638
|
+
};
|
|
10196
10639
|
}
|
|
10197
|
-
var
|
|
10640
|
+
var CEREBRAS_API_URL = "https://api.cerebras.ai/v1/chat/completions", CEREBRAS_MODEL = "llama-3.3-70b";
|
|
10641
|
+
var init_auto_assign = __esm(() => {
|
|
10198
10642
|
init_database();
|
|
10643
|
+
init_tasks();
|
|
10644
|
+
init_agents();
|
|
10199
10645
|
});
|
|
10200
10646
|
|
|
10201
10647
|
// src/db/file-locks.ts
|
|
@@ -10997,14 +11443,14 @@ __export(exports_mcp, {
|
|
|
10997
11443
|
});
|
|
10998
11444
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10999
11445
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11000
|
-
import { readFileSync as
|
|
11001
|
-
import { join as
|
|
11446
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
11447
|
+
import { join as join7, dirname as dirname2 } from "path";
|
|
11002
11448
|
import { fileURLToPath } from "url";
|
|
11003
11449
|
function getMcpVersion() {
|
|
11004
11450
|
try {
|
|
11005
11451
|
const __dir = dirname2(fileURLToPath(import.meta.url));
|
|
11006
|
-
const pkgPath =
|
|
11007
|
-
return JSON.parse(
|
|
11452
|
+
const pkgPath = join7(__dir, "..", "package.json");
|
|
11453
|
+
return JSON.parse(readFileSync4(pkgPath, "utf-8")).version || "0.0.0";
|
|
11008
11454
|
} catch {
|
|
11009
11455
|
return "0.0.0";
|
|
11010
11456
|
}
|
|
@@ -11168,7 +11614,8 @@ var init_mcp = __esm(() => {
|
|
|
11168
11614
|
"get_next_task",
|
|
11169
11615
|
"bootstrap",
|
|
11170
11616
|
"get_tasks_changed_since",
|
|
11171
|
-
"heartbeat"
|
|
11617
|
+
"heartbeat",
|
|
11618
|
+
"release_agent"
|
|
11172
11619
|
]);
|
|
11173
11620
|
STANDARD_EXCLUDED = new Set([
|
|
11174
11621
|
"rename_agent",
|
|
@@ -11390,7 +11837,12 @@ Checklist (${done}/${task.checklist.length}):`);
|
|
|
11390
11837
|
}, async ({ id, ...rest }) => {
|
|
11391
11838
|
try {
|
|
11392
11839
|
const resolvedId = resolveId(id);
|
|
11393
|
-
const
|
|
11840
|
+
const resolved = { ...rest };
|
|
11841
|
+
if (resolved.task_list_id)
|
|
11842
|
+
resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
|
|
11843
|
+
if (resolved.plan_id)
|
|
11844
|
+
resolved.plan_id = resolveId(resolved.plan_id, "plans");
|
|
11845
|
+
const task = updateTask(resolvedId, resolved);
|
|
11394
11846
|
return { content: [{ type: "text", text: `updated: ${formatTask(task)}` }] };
|
|
11395
11847
|
} catch (e) {
|
|
11396
11848
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
@@ -12030,11 +12482,12 @@ ${text}` }] };
|
|
|
12030
12482
|
description: exports_external.string().optional(),
|
|
12031
12483
|
capabilities: exports_external.array(exports_external.string()).optional().describe("Agent capabilities/skills for task routing (e.g. ['typescript', 'testing', 'devops'])"),
|
|
12032
12484
|
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."),
|
|
12033
|
-
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")
|
|
12034
|
-
|
|
12485
|
+
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"),
|
|
12486
|
+
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.")
|
|
12487
|
+
}, async ({ name, description, capabilities, session_id, working_dir, force }) => {
|
|
12035
12488
|
try {
|
|
12036
12489
|
const pool = getAgentPoolForProject(working_dir);
|
|
12037
|
-
const result = registerAgent({ name, description, capabilities, session_id, working_dir, pool: pool || undefined });
|
|
12490
|
+
const result = registerAgent({ name, description, capabilities, session_id, working_dir, force, pool: pool || undefined });
|
|
12038
12491
|
if (isAgentConflict(result)) {
|
|
12039
12492
|
const suggestLine = result.suggestions && result.suggestions.length > 0 ? `
|
|
12040
12493
|
Available names: ${result.suggestions.join(", ")}` : "";
|
|
@@ -12253,6 +12706,31 @@ ID: ${updated.id}`
|
|
|
12253
12706
|
}
|
|
12254
12707
|
});
|
|
12255
12708
|
}
|
|
12709
|
+
if (shouldRegisterTool("release_agent")) {
|
|
12710
|
+
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.", {
|
|
12711
|
+
agent_id: exports_external.string().describe("Your agent ID or name."),
|
|
12712
|
+
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).")
|
|
12713
|
+
}, async ({ agent_id, session_id }) => {
|
|
12714
|
+
try {
|
|
12715
|
+
const agent = getAgent(agent_id) || getAgentByName(agent_id);
|
|
12716
|
+
if (!agent) {
|
|
12717
|
+
return { content: [{ type: "text", text: `Agent not found: ${agent_id}` }], isError: true };
|
|
12718
|
+
}
|
|
12719
|
+
const released = releaseAgent(agent.id, session_id);
|
|
12720
|
+
if (!released) {
|
|
12721
|
+
return { content: [{ type: "text", text: `Release denied: session_id does not match agent's current session.` }], isError: true };
|
|
12722
|
+
}
|
|
12723
|
+
return {
|
|
12724
|
+
content: [{
|
|
12725
|
+
type: "text",
|
|
12726
|
+
text: `Agent released: ${agent.name} (${agent.id}) \u2014 session cleared, name is now available.`
|
|
12727
|
+
}]
|
|
12728
|
+
};
|
|
12729
|
+
} catch (e) {
|
|
12730
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
12731
|
+
}
|
|
12732
|
+
});
|
|
12733
|
+
}
|
|
12256
12734
|
if (shouldRegisterTool("create_task_list")) {
|
|
12257
12735
|
server.tool("create_task_list", "Create a task list container for organizing tasks.", {
|
|
12258
12736
|
name: exports_external.string(),
|
|
@@ -12356,56 +12834,228 @@ Slug: ${list.slug}`
|
|
|
12356
12834
|
}
|
|
12357
12835
|
});
|
|
12358
12836
|
}
|
|
12359
|
-
if (shouldRegisterTool("delete_task_list")) {
|
|
12360
|
-
server.tool("delete_task_list", "Delete a task list. Tasks are orphaned, not deleted.", {
|
|
12361
|
-
id: exports_external.string()
|
|
12362
|
-
}, async ({ id }) => {
|
|
12837
|
+
if (shouldRegisterTool("delete_task_list")) {
|
|
12838
|
+
server.tool("delete_task_list", "Delete a task list. Tasks are orphaned, not deleted.", {
|
|
12839
|
+
id: exports_external.string()
|
|
12840
|
+
}, async ({ id }) => {
|
|
12841
|
+
try {
|
|
12842
|
+
const resolvedId = resolveId(id, "task_lists");
|
|
12843
|
+
const deleted = deleteTaskList(resolvedId);
|
|
12844
|
+
return {
|
|
12845
|
+
content: [{
|
|
12846
|
+
type: "text",
|
|
12847
|
+
text: deleted ? `Task list ${id} deleted.` : `Task list ${id} not found.`
|
|
12848
|
+
}]
|
|
12849
|
+
};
|
|
12850
|
+
} catch (e) {
|
|
12851
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
12852
|
+
}
|
|
12853
|
+
});
|
|
12854
|
+
}
|
|
12855
|
+
if (shouldRegisterTool("get_task_history")) {
|
|
12856
|
+
server.tool("get_task_history", "Get audit log \u2014 field changes with timestamps and actors.", {
|
|
12857
|
+
task_id: exports_external.string()
|
|
12858
|
+
}, async ({ task_id }) => {
|
|
12859
|
+
try {
|
|
12860
|
+
const resolvedId = resolveId(task_id);
|
|
12861
|
+
const { getTaskHistory: getTaskHistory2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
|
|
12862
|
+
const history = getTaskHistory2(resolvedId);
|
|
12863
|
+
if (history.length === 0)
|
|
12864
|
+
return { content: [{ type: "text", text: "No history for this task." }] };
|
|
12865
|
+
const text = history.map((h) => `${h.created_at} | ${h.action}${h.field ? ` ${h.field}` : ""}${h.old_value ? ` from "${h.old_value}"` : ""}${h.new_value ? ` to "${h.new_value}"` : ""}${h.agent_id ? ` by ${h.agent_id}` : ""}`).join(`
|
|
12866
|
+
`);
|
|
12867
|
+
return { content: [{ type: "text", text: `${history.length} change(s):
|
|
12868
|
+
${text}` }] };
|
|
12869
|
+
} catch (e) {
|
|
12870
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
12871
|
+
}
|
|
12872
|
+
});
|
|
12873
|
+
}
|
|
12874
|
+
if (shouldRegisterTool("get_recent_activity")) {
|
|
12875
|
+
server.tool("get_recent_activity", "Get recent task changes \u2014 global activity feed.", {
|
|
12876
|
+
limit: exports_external.number().optional()
|
|
12877
|
+
}, async ({ limit }) => {
|
|
12878
|
+
try {
|
|
12879
|
+
const { getRecentActivity: getRecentActivity2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
|
|
12880
|
+
const activity = getRecentActivity2(limit || 50);
|
|
12881
|
+
if (activity.length === 0)
|
|
12882
|
+
return { content: [{ type: "text", text: "No recent activity." }] };
|
|
12883
|
+
const text = activity.map((h) => `${h.created_at} | ${h.task_id.slice(0, 8)} | ${h.action}${h.field ? ` ${h.field}` : ""}${h.agent_id ? ` by ${h.agent_id}` : ""}`).join(`
|
|
12884
|
+
`);
|
|
12885
|
+
return { content: [{ type: "text", text: `${activity.length} recent change(s):
|
|
12886
|
+
${text}` }] };
|
|
12887
|
+
} catch (e) {
|
|
12888
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
12889
|
+
}
|
|
12890
|
+
});
|
|
12891
|
+
}
|
|
12892
|
+
if (shouldRegisterTool("recap")) {
|
|
12893
|
+
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.", {
|
|
12894
|
+
hours: exports_external.number().optional().describe("Look back N hours (default: 8)"),
|
|
12895
|
+
project_id: exports_external.string().optional().describe("Filter to a specific project")
|
|
12896
|
+
}, async ({ hours, project_id }) => {
|
|
12897
|
+
try {
|
|
12898
|
+
const { getRecap: getRecap2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
|
|
12899
|
+
const recap = getRecap2(hours || 8, project_id);
|
|
12900
|
+
const lines = [`Recap \u2014 last ${recap.hours}h (since ${recap.since})`];
|
|
12901
|
+
if (recap.completed.length > 0) {
|
|
12902
|
+
lines.push(`
|
|
12903
|
+
Completed (${recap.completed.length}):`);
|
|
12904
|
+
for (const t of recap.completed) {
|
|
12905
|
+
const dur = t.duration_minutes != null ? ` (${t.duration_minutes}m)` : "";
|
|
12906
|
+
lines.push(` \u2713 ${t.short_id || t.id.slice(0, 8)} ${t.title}${dur}${t.assigned_to ? ` \u2014 ${t.assigned_to}` : ""}`);
|
|
12907
|
+
}
|
|
12908
|
+
}
|
|
12909
|
+
if (recap.in_progress.length > 0) {
|
|
12910
|
+
lines.push(`
|
|
12911
|
+
In Progress (${recap.in_progress.length}):`);
|
|
12912
|
+
for (const t of recap.in_progress)
|
|
12913
|
+
lines.push(` \u2192 ${t.short_id || t.id.slice(0, 8)} ${t.title}${t.assigned_to ? ` \u2014 ${t.assigned_to}` : ""}`);
|
|
12914
|
+
}
|
|
12915
|
+
if (recap.blocked.length > 0) {
|
|
12916
|
+
lines.push(`
|
|
12917
|
+
Blocked (${recap.blocked.length}):`);
|
|
12918
|
+
for (const t of recap.blocked)
|
|
12919
|
+
lines.push(` \u2717 ${t.short_id || t.id.slice(0, 8)} ${t.title}`);
|
|
12920
|
+
}
|
|
12921
|
+
if (recap.stale.length > 0) {
|
|
12922
|
+
lines.push(`
|
|
12923
|
+
Stale (${recap.stale.length}):`);
|
|
12924
|
+
for (const t of recap.stale)
|
|
12925
|
+
lines.push(` ! ${t.short_id || t.id.slice(0, 8)} ${t.title} \u2014 updated ${t.updated_at}`);
|
|
12926
|
+
}
|
|
12927
|
+
if (recap.agents.length > 0) {
|
|
12928
|
+
lines.push(`
|
|
12929
|
+
Agents:`);
|
|
12930
|
+
for (const a of recap.agents)
|
|
12931
|
+
lines.push(` ${a.name}: ${a.completed_count} done, ${a.in_progress_count} active (seen ${a.last_seen_at})`);
|
|
12932
|
+
}
|
|
12933
|
+
lines.push(`
|
|
12934
|
+
Created: ${recap.created.length} new tasks`);
|
|
12935
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
12936
|
+
`) }] };
|
|
12937
|
+
} catch (e) {
|
|
12938
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
12939
|
+
}
|
|
12940
|
+
});
|
|
12941
|
+
}
|
|
12942
|
+
if (shouldRegisterTool("standup")) {
|
|
12943
|
+
server.tool("standup", "Generate standup notes \u2014 completed tasks since yesterday grouped by agent, in-progress work, and blockers. Copy-paste ready.", {
|
|
12944
|
+
hours: exports_external.number().optional().describe("Look back N hours (default: 24)"),
|
|
12945
|
+
project_id: exports_external.string().optional()
|
|
12946
|
+
}, async ({ hours, project_id }) => {
|
|
12947
|
+
try {
|
|
12948
|
+
const { getRecap: getRecap2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
|
|
12949
|
+
const recap = getRecap2(hours || 24, project_id);
|
|
12950
|
+
const lines = [`Standup \u2014 last ${recap.hours}h`];
|
|
12951
|
+
const byAgent = new Map;
|
|
12952
|
+
for (const t of recap.completed) {
|
|
12953
|
+
const agent = t.assigned_to || "unassigned";
|
|
12954
|
+
if (!byAgent.has(agent))
|
|
12955
|
+
byAgent.set(agent, []);
|
|
12956
|
+
byAgent.get(agent).push(t);
|
|
12957
|
+
}
|
|
12958
|
+
if (byAgent.size > 0) {
|
|
12959
|
+
lines.push(`
|
|
12960
|
+
Done:`);
|
|
12961
|
+
for (const [agent, tasks] of byAgent) {
|
|
12962
|
+
lines.push(` ${agent}:`);
|
|
12963
|
+
for (const t of tasks) {
|
|
12964
|
+
const dur = t.duration_minutes != null ? ` (${t.duration_minutes}m)` : "";
|
|
12965
|
+
lines.push(` \u2713 ${t.short_id || t.id.slice(0, 8)} ${t.title}${dur}`);
|
|
12966
|
+
}
|
|
12967
|
+
}
|
|
12968
|
+
} else {
|
|
12969
|
+
lines.push(`
|
|
12970
|
+
Nothing completed.`);
|
|
12971
|
+
}
|
|
12972
|
+
if (recap.in_progress.length > 0) {
|
|
12973
|
+
lines.push(`
|
|
12974
|
+
In Progress:`);
|
|
12975
|
+
for (const t of recap.in_progress)
|
|
12976
|
+
lines.push(` \u2192 ${t.short_id || t.id.slice(0, 8)} ${t.title}${t.assigned_to ? ` \u2014 ${t.assigned_to}` : ""}`);
|
|
12977
|
+
}
|
|
12978
|
+
if (recap.blocked.length > 0) {
|
|
12979
|
+
lines.push(`
|
|
12980
|
+
Blocked:`);
|
|
12981
|
+
for (const t of recap.blocked)
|
|
12982
|
+
lines.push(` \u2717 ${t.short_id || t.id.slice(0, 8)} ${t.title}`);
|
|
12983
|
+
}
|
|
12984
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
12985
|
+
`) }] };
|
|
12986
|
+
} catch (e) {
|
|
12987
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
12988
|
+
}
|
|
12989
|
+
});
|
|
12990
|
+
}
|
|
12991
|
+
if (shouldRegisterTool("import_github_issue")) {
|
|
12992
|
+
server.tool("import_github_issue", "Import a GitHub issue as a task. Requires gh CLI installed and authenticated.", {
|
|
12993
|
+
url: exports_external.string().describe("GitHub issue URL (e.g. https://github.com/owner/repo/issues/42)"),
|
|
12994
|
+
project_id: exports_external.string().optional(),
|
|
12995
|
+
task_list_id: exports_external.string().optional()
|
|
12996
|
+
}, async ({ url, project_id, task_list_id }) => {
|
|
12363
12997
|
try {
|
|
12364
|
-
const
|
|
12365
|
-
const
|
|
12366
|
-
|
|
12367
|
-
content: [{
|
|
12368
|
-
|
|
12369
|
-
|
|
12370
|
-
|
|
12371
|
-
}
|
|
12998
|
+
const { parseGitHubUrl: parseGitHubUrl2, fetchGitHubIssue: fetchGitHubIssue2, issueToTask: issueToTask2 } = await Promise.resolve().then(() => (init_github(), exports_github));
|
|
12999
|
+
const parsed = parseGitHubUrl2(url);
|
|
13000
|
+
if (!parsed)
|
|
13001
|
+
return { content: [{ type: "text", text: "Invalid GitHub issue URL." }], isError: true };
|
|
13002
|
+
const issue = fetchGitHubIssue2(parsed.owner, parsed.repo, parsed.number);
|
|
13003
|
+
const input = issueToTask2(issue, { project_id, task_list_id });
|
|
13004
|
+
const task = createTask(input);
|
|
13005
|
+
return { content: [{ type: "text", text: `Imported GH#${issue.number}: ${issue.title}
|
|
13006
|
+
Task: ${task.short_id || task.id} [${task.priority}]
|
|
13007
|
+
Labels: ${issue.labels.join(", ") || "none"}` }] };
|
|
12372
13008
|
} catch (e) {
|
|
12373
13009
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
12374
13010
|
}
|
|
12375
13011
|
});
|
|
12376
13012
|
}
|
|
12377
|
-
if (shouldRegisterTool("
|
|
12378
|
-
server.tool("
|
|
12379
|
-
|
|
12380
|
-
}, async ({
|
|
13013
|
+
if (shouldRegisterTool("blame")) {
|
|
13014
|
+
server.tool("blame", "Show which tasks and agents touched a file \u2014 combines task_files and task_commits data.", {
|
|
13015
|
+
path: exports_external.string().describe("File path to look up")
|
|
13016
|
+
}, async ({ path }) => {
|
|
12381
13017
|
try {
|
|
12382
|
-
const
|
|
12383
|
-
const
|
|
12384
|
-
const
|
|
12385
|
-
|
|
12386
|
-
|
|
12387
|
-
|
|
12388
|
-
`
|
|
12389
|
-
|
|
12390
|
-
|
|
13018
|
+
const { findTasksByFile: findTasksByFile2 } = await Promise.resolve().then(() => (init_task_files(), exports_task_files));
|
|
13019
|
+
const db = getDatabase();
|
|
13020
|
+
const taskFiles = findTasksByFile2(path, db);
|
|
13021
|
+
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}%`);
|
|
13022
|
+
const lines = [`Blame: ${path}`];
|
|
13023
|
+
if (taskFiles.length > 0) {
|
|
13024
|
+
lines.push(`
|
|
13025
|
+
Task File Links (${taskFiles.length}):`);
|
|
13026
|
+
for (const tf of taskFiles) {
|
|
13027
|
+
const task = getTask(tf.task_id, db);
|
|
13028
|
+
lines.push(` ${task?.short_id || tf.task_id.slice(0, 8)} ${task?.title || "?"} \u2014 ${tf.role || "file"}`);
|
|
13029
|
+
}
|
|
13030
|
+
}
|
|
13031
|
+
if (commitRows.length > 0) {
|
|
13032
|
+
lines.push(`
|
|
13033
|
+
Commit Links (${commitRows.length}):`);
|
|
13034
|
+
for (const c of commitRows)
|
|
13035
|
+
lines.push(` ${c.sha?.slice(0, 7)} ${c.short_id || c.task_id.slice(0, 8)} ${c.title || ""} \u2014 ${c.author || ""}`);
|
|
13036
|
+
}
|
|
13037
|
+
if (taskFiles.length === 0 && commitRows.length === 0) {
|
|
13038
|
+
lines.push("No task or commit links found.");
|
|
13039
|
+
}
|
|
13040
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
13041
|
+
`) }] };
|
|
12391
13042
|
} catch (e) {
|
|
12392
13043
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
12393
13044
|
}
|
|
12394
13045
|
});
|
|
12395
13046
|
}
|
|
12396
|
-
if (shouldRegisterTool("
|
|
12397
|
-
server.tool("
|
|
12398
|
-
|
|
12399
|
-
|
|
13047
|
+
if (shouldRegisterTool("burndown")) {
|
|
13048
|
+
server.tool("burndown", "ASCII burndown chart showing actual vs ideal progress for a plan, project, or task list.", {
|
|
13049
|
+
plan_id: exports_external.string().optional(),
|
|
13050
|
+
project_id: exports_external.string().optional(),
|
|
13051
|
+
task_list_id: exports_external.string().optional()
|
|
13052
|
+
}, async ({ plan_id, project_id, task_list_id }) => {
|
|
12400
13053
|
try {
|
|
12401
|
-
const {
|
|
12402
|
-
const
|
|
12403
|
-
|
|
12404
|
-
|
|
12405
|
-
|
|
12406
|
-
`);
|
|
12407
|
-
return { content: [{ type: "text", text: `${activity.length} recent change(s):
|
|
12408
|
-
${text}` }] };
|
|
13054
|
+
const { getBurndown: getBurndown2 } = await Promise.resolve().then(() => (init_burndown(), exports_burndown));
|
|
13055
|
+
const data = getBurndown2({ plan_id, project_id, task_list_id });
|
|
13056
|
+
return { content: [{ type: "text", text: `Burndown: ${data.completed}/${data.total} done, ${data.remaining} remaining
|
|
13057
|
+
|
|
13058
|
+
${data.chart}` }] };
|
|
12409
13059
|
} catch (e) {
|
|
12410
13060
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
12411
13061
|
}
|
|
@@ -13216,6 +13866,91 @@ ${checks.map((c) => ` ${c.status === "ok" ? "\u2713" : "\u26A0"} ${c.name}: ${c
|
|
|
13216
13866
|
}
|
|
13217
13867
|
});
|
|
13218
13868
|
}
|
|
13869
|
+
if (shouldRegisterTool("task_context")) {
|
|
13870
|
+
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.", {
|
|
13871
|
+
id: exports_external.string().describe("Task ID, short_id, or partial ID")
|
|
13872
|
+
}, async ({ id }) => {
|
|
13873
|
+
try {
|
|
13874
|
+
const resolvedId = resolveId(id, "tasks");
|
|
13875
|
+
const task = getTaskWithRelations(resolvedId);
|
|
13876
|
+
if (!task)
|
|
13877
|
+
return { content: [{ type: "text", text: `Task not found: ${id}` }], isError: true };
|
|
13878
|
+
const lines = [];
|
|
13879
|
+
const sid = task.short_id || task.id.slice(0, 8);
|
|
13880
|
+
lines.push(`${sid} [${task.status}] [${task.priority}] ${task.title}`);
|
|
13881
|
+
if (task.description)
|
|
13882
|
+
lines.push(`
|
|
13883
|
+
Description:
|
|
13884
|
+
${task.description}`);
|
|
13885
|
+
if (task.assigned_to)
|
|
13886
|
+
lines.push(`Assigned: ${task.assigned_to}`);
|
|
13887
|
+
if (task.started_at)
|
|
13888
|
+
lines.push(`Started: ${task.started_at}`);
|
|
13889
|
+
if (task.completed_at) {
|
|
13890
|
+
lines.push(`Completed: ${task.completed_at}`);
|
|
13891
|
+
if (task.started_at) {
|
|
13892
|
+
const dur = Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 60000);
|
|
13893
|
+
lines.push(`Duration: ${dur}m`);
|
|
13894
|
+
}
|
|
13895
|
+
}
|
|
13896
|
+
if (task.tags.length > 0)
|
|
13897
|
+
lines.push(`Tags: ${task.tags.join(", ")}`);
|
|
13898
|
+
if (task.dependencies.length > 0) {
|
|
13899
|
+
lines.push(`
|
|
13900
|
+
Depends on (${task.dependencies.length}):`);
|
|
13901
|
+
for (const dep of task.dependencies) {
|
|
13902
|
+
const blocked = dep.status !== "completed" && dep.status !== "cancelled";
|
|
13903
|
+
lines.push(` ${blocked ? "\u2717" : "\u2713"} ${dep.short_id || dep.id.slice(0, 8)} [${dep.status}] ${dep.title}`);
|
|
13904
|
+
}
|
|
13905
|
+
const unfinished = task.dependencies.filter((d) => d.status !== "completed" && d.status !== "cancelled");
|
|
13906
|
+
if (unfinished.length > 0)
|
|
13907
|
+
lines.push(`\u26A0 BLOCKED by ${unfinished.length} unfinished dep(s)`);
|
|
13908
|
+
}
|
|
13909
|
+
if (task.blocked_by.length > 0) {
|
|
13910
|
+
lines.push(`
|
|
13911
|
+
Blocks (${task.blocked_by.length}):`);
|
|
13912
|
+
for (const b of task.blocked_by)
|
|
13913
|
+
lines.push(` ${b.short_id || b.id.slice(0, 8)} [${b.status}] ${b.title}`);
|
|
13914
|
+
}
|
|
13915
|
+
if (task.subtasks.length > 0) {
|
|
13916
|
+
lines.push(`
|
|
13917
|
+
Subtasks (${task.subtasks.length}):`);
|
|
13918
|
+
for (const st of task.subtasks)
|
|
13919
|
+
lines.push(` ${st.short_id || st.id.slice(0, 8)} [${st.status}] ${st.title}`);
|
|
13920
|
+
}
|
|
13921
|
+
try {
|
|
13922
|
+
const { listTaskFiles: listTaskFiles2 } = await Promise.resolve().then(() => (init_task_files(), exports_task_files));
|
|
13923
|
+
const files = listTaskFiles2(task.id);
|
|
13924
|
+
if (files.length > 0) {
|
|
13925
|
+
lines.push(`
|
|
13926
|
+
Files (${files.length}):`);
|
|
13927
|
+
for (const f of files)
|
|
13928
|
+
lines.push(` ${f.role || "file"}: ${f.path}`);
|
|
13929
|
+
}
|
|
13930
|
+
} catch {}
|
|
13931
|
+
try {
|
|
13932
|
+
const { getTaskCommits: getTaskCommits2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
|
|
13933
|
+
const commits = getTaskCommits2(task.id);
|
|
13934
|
+
if (commits.length > 0) {
|
|
13935
|
+
lines.push(`
|
|
13936
|
+
Commits (${commits.length}):`);
|
|
13937
|
+
for (const c of commits)
|
|
13938
|
+
lines.push(` ${c.commit_hash?.slice(0, 7)} ${c.message || ""}`);
|
|
13939
|
+
}
|
|
13940
|
+
} catch {}
|
|
13941
|
+
if (task.comments.length > 0) {
|
|
13942
|
+
lines.push(`
|
|
13943
|
+
Comments (${task.comments.length}):`);
|
|
13944
|
+
for (const c of task.comments)
|
|
13945
|
+
lines.push(` [${c.agent_id || "?"}] ${c.created_at}: ${c.content}`);
|
|
13946
|
+
}
|
|
13947
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
13948
|
+
`) }] };
|
|
13949
|
+
} catch (e) {
|
|
13950
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13951
|
+
}
|
|
13952
|
+
});
|
|
13953
|
+
}
|
|
13219
13954
|
if (shouldRegisterTool("get_context")) {
|
|
13220
13955
|
server.tool("get_context", "Get a compact task summary for agent prompt injection. Returns formatted text.", {
|
|
13221
13956
|
agent_id: exports_external.string().optional(),
|
|
@@ -13478,6 +14213,7 @@ ${stack_trace.slice(0, 1500)}
|
|
|
13478
14213
|
"delete_agent",
|
|
13479
14214
|
"unarchive_agent",
|
|
13480
14215
|
"heartbeat",
|
|
14216
|
+
"release_agent",
|
|
13481
14217
|
"get_my_tasks",
|
|
13482
14218
|
"get_org_chart",
|
|
13483
14219
|
"set_reports_to",
|
|
@@ -13494,6 +14230,12 @@ ${stack_trace.slice(0, 1500)}
|
|
|
13494
14230
|
"claim_next_task",
|
|
13495
14231
|
"get_task_history",
|
|
13496
14232
|
"get_recent_activity",
|
|
14233
|
+
"recap",
|
|
14234
|
+
"task_context",
|
|
14235
|
+
"standup",
|
|
14236
|
+
"burndown",
|
|
14237
|
+
"blame",
|
|
14238
|
+
"import_github_issue",
|
|
13497
14239
|
"create_webhook",
|
|
13498
14240
|
"list_webhooks",
|
|
13499
14241
|
"delete_webhook",
|
|
@@ -13618,8 +14360,8 @@ ${stack_trace.slice(0, 1500)}
|
|
|
13618
14360
|
suggest_agent_name: `Check available agent names before registering. Shows active agents and, if a pool is configured, which pool names are free.
|
|
13619
14361
|
Params: working_dir(string \u2014 your working directory, used to look up project pool from config)
|
|
13620
14362
|
Example: {working_dir: '/workspace/platform'}`,
|
|
13621
|
-
register_agent: `Register an agent. Any name is allowed \u2014 pool is advisory. Returns CONFLICT if name is held by a recently-active agent.
|
|
13622
|
-
Params: name(string, req), description(string), capabilities(string[]), session_id(string \u2014 unique per session), working_dir(string \u2014 used to determine project pool)
|
|
14363
|
+
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.
|
|
14364
|
+
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)
|
|
13623
14365
|
Example: {name: 'my-agent', session_id: 'abc123-1741952000', working_dir: '/workspace/platform'}`,
|
|
13624
14366
|
list_agents: `List all registered agents (active by default). Set include_archived: true to see archived agents.
|
|
13625
14367
|
Params: include_archived(boolean, optional)
|
|
@@ -13639,6 +14381,9 @@ ${stack_trace.slice(0, 1500)}
|
|
|
13639
14381
|
heartbeat: `Update last_seen_at timestamp to signal you're still active. Call periodically during long tasks.
|
|
13640
14382
|
Params: agent_id(string, req \u2014 your agent ID or name)
|
|
13641
14383
|
Example: {agent_id: 'maximus'}`,
|
|
14384
|
+
release_agent: `Explicitly release/logout an agent \u2014 clears session binding and makes name immediately available. Call when session ends.
|
|
14385
|
+
Params: agent_id(string, req), session_id(string \u2014 only releases if matching)
|
|
14386
|
+
Example: {agent_id: 'maximus', session_id: 'my-session-123'}`,
|
|
13642
14387
|
get_my_tasks: `Get all tasks assigned to/created by an agent, with stats (pending/active/done/rate).
|
|
13643
14388
|
Params: agent_name(string, req)
|
|
13644
14389
|
Example: {agent_name: 'maximus'}`,
|
|
@@ -13699,6 +14444,24 @@ ${stack_trace.slice(0, 1500)}
|
|
|
13699
14444
|
get_recent_activity: `Get recent task changes across all tasks \u2014 global activity feed.
|
|
13700
14445
|
Params: limit(number, default:50)
|
|
13701
14446
|
Example: {limit: 20}`,
|
|
14447
|
+
recap: `Summary of what happened in the last N hours \u2014 completed tasks with durations, new tasks, in-progress, blocked, stale, agent activity.
|
|
14448
|
+
Params: hours(number, default:8), project_id(string)
|
|
14449
|
+
Example: {hours: 4}`,
|
|
14450
|
+
task_context: `Full orientation for a specific task \u2014 description, dependencies with blocked status, files, commits, comments, checklist, duration. Use before starting work.
|
|
14451
|
+
Params: id(string, req)
|
|
14452
|
+
Example: {id: 'OPE-00042'}`,
|
|
14453
|
+
standup: `Generate standup notes \u2014 completed tasks grouped by agent, in-progress, blocked. Copy-paste ready.
|
|
14454
|
+
Params: hours(number, default:24), project_id(string)
|
|
14455
|
+
Example: {hours: 24}`,
|
|
14456
|
+
import_github_issue: `Import a GitHub issue as a task. Requires gh CLI.
|
|
14457
|
+
Params: url(string, req), project_id(string), task_list_id(string)
|
|
14458
|
+
Example: {url: 'https://github.com/owner/repo/issues/42'}`,
|
|
14459
|
+
blame: `Show which tasks/agents touched a file \u2014 combines task_files and task_commits.
|
|
14460
|
+
Params: path(string, req)
|
|
14461
|
+
Example: {path: 'src/db/agents.ts'}`,
|
|
14462
|
+
burndown: `ASCII burndown chart \u2014 actual vs ideal progress for a plan, project, or task list.
|
|
14463
|
+
Params: plan_id(string), project_id(string), task_list_id(string)
|
|
14464
|
+
Example: {plan_id: 'abc123'}`,
|
|
13702
14465
|
create_webhook: `Register a webhook for task change events.
|
|
13703
14466
|
Params: url(string, req), events(string[] \u2014 empty=all), secret(string \u2014 HMAC signing)
|
|
13704
14467
|
Example: {url: 'https://example.com/hook', events: ['task.created', 'task.completed']}`,
|
|
@@ -14474,6 +15237,48 @@ ${lines.join(`
|
|
|
14474
15237
|
const lines = entries.map((e) => `#${e.rank} ${e.agent_name.padEnd(15)} score:${(e.composite_score * 100).toFixed(0).padStart(3)}% done:${String(e.tasks_completed).padStart(3)} rate:${(e.completion_rate * 100).toFixed(0)}%`);
|
|
14475
15238
|
return { content: [{ type: "text", text: `Leaderboard:
|
|
14476
15239
|
${lines.join(`
|
|
15240
|
+
`)}` }] };
|
|
15241
|
+
} catch (e) {
|
|
15242
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
15243
|
+
}
|
|
15244
|
+
});
|
|
15245
|
+
}
|
|
15246
|
+
if (shouldRegisterTool("extract_todos")) {
|
|
15247
|
+
server.tool("extract_todos", "Scan source files for TODO/FIXME/HACK/BUG/XXX/NOTE comments and create tasks from them. Deduplicates on re-runs.", {
|
|
15248
|
+
path: exports_external.string().describe("Directory or file path to scan"),
|
|
15249
|
+
project_id: exports_external.string().optional().describe("Project to assign tasks to"),
|
|
15250
|
+
task_list_id: exports_external.string().optional().describe("Task list to add tasks to"),
|
|
15251
|
+
patterns: exports_external.array(exports_external.enum(["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"])).optional().describe("Tags to search for (default: all)"),
|
|
15252
|
+
tags: exports_external.array(exports_external.string()).optional().describe("Extra tags to add to created tasks"),
|
|
15253
|
+
assigned_to: exports_external.string().optional().describe("Agent to assign tasks to"),
|
|
15254
|
+
agent_id: exports_external.string().optional().describe("Agent performing the extraction"),
|
|
15255
|
+
dry_run: exports_external.boolean().optional().describe("If true, return found comments without creating tasks"),
|
|
15256
|
+
extensions: exports_external.array(exports_external.string()).optional().describe("File extensions to scan (e.g. ['ts', 'py'])")
|
|
15257
|
+
}, async (params) => {
|
|
15258
|
+
try {
|
|
15259
|
+
const { extractTodos: extractTodos2 } = (init_extract(), __toCommonJS(exports_extract));
|
|
15260
|
+
const resolved = { ...params };
|
|
15261
|
+
if (resolved["project_id"])
|
|
15262
|
+
resolved["project_id"] = resolveId(resolved["project_id"], "projects");
|
|
15263
|
+
if (resolved["task_list_id"])
|
|
15264
|
+
resolved["task_list_id"] = resolveId(resolved["task_list_id"], "task_lists");
|
|
15265
|
+
const result = extractTodos2(resolved);
|
|
15266
|
+
if (params.dry_run) {
|
|
15267
|
+
const lines = result.comments.map((c) => `[${c.tag}] ${c.message} \u2014 ${c.file}:${c.line}`);
|
|
15268
|
+
return { content: [{ type: "text", text: `Found ${result.comments.length} comment(s):
|
|
15269
|
+
${lines.join(`
|
|
15270
|
+
`)}` }] };
|
|
15271
|
+
}
|
|
15272
|
+
const summary = [
|
|
15273
|
+
`Created ${result.tasks.length} task(s)`,
|
|
15274
|
+
result.skipped > 0 ? `Skipped ${result.skipped} duplicate(s)` : null,
|
|
15275
|
+
`Total comments found: ${result.comments.length}`
|
|
15276
|
+
].filter(Boolean).join(`
|
|
15277
|
+
`);
|
|
15278
|
+
const taskLines = result.tasks.map((t) => formatTask(t));
|
|
15279
|
+
return { content: [{ type: "text", text: `${summary}
|
|
15280
|
+
|
|
15281
|
+
${taskLines.join(`
|
|
14477
15282
|
`)}` }] };
|
|
14478
15283
|
} catch (e) {
|
|
14479
15284
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
@@ -14561,26 +15366,26 @@ __export(exports_serve, {
|
|
|
14561
15366
|
startServer: () => startServer
|
|
14562
15367
|
});
|
|
14563
15368
|
import { existsSync as existsSync6 } from "fs";
|
|
14564
|
-
import { join as
|
|
15369
|
+
import { join as join8, dirname as dirname3, extname, resolve as resolve3, sep } from "path";
|
|
14565
15370
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
14566
15371
|
function resolveDashboardDir() {
|
|
14567
15372
|
const candidates = [];
|
|
14568
15373
|
try {
|
|
14569
15374
|
const scriptDir = dirname3(fileURLToPath2(import.meta.url));
|
|
14570
|
-
candidates.push(
|
|
14571
|
-
candidates.push(
|
|
15375
|
+
candidates.push(join8(scriptDir, "..", "dashboard", "dist"));
|
|
15376
|
+
candidates.push(join8(scriptDir, "..", "..", "dashboard", "dist"));
|
|
14572
15377
|
} catch {}
|
|
14573
15378
|
if (process.argv[1]) {
|
|
14574
15379
|
const mainDir = dirname3(process.argv[1]);
|
|
14575
|
-
candidates.push(
|
|
14576
|
-
candidates.push(
|
|
15380
|
+
candidates.push(join8(mainDir, "..", "dashboard", "dist"));
|
|
15381
|
+
candidates.push(join8(mainDir, "..", "..", "dashboard", "dist"));
|
|
14577
15382
|
}
|
|
14578
|
-
candidates.push(
|
|
15383
|
+
candidates.push(join8(process.cwd(), "dashboard", "dist"));
|
|
14579
15384
|
for (const candidate of candidates) {
|
|
14580
15385
|
if (existsSync6(candidate))
|
|
14581
15386
|
return candidate;
|
|
14582
15387
|
}
|
|
14583
|
-
return
|
|
15388
|
+
return join8(process.cwd(), "dashboard", "dist");
|
|
14584
15389
|
}
|
|
14585
15390
|
function json(data, status = 200, port) {
|
|
14586
15391
|
return new Response(JSON.stringify(data), {
|
|
@@ -15384,9 +16189,9 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
|
|
|
15384
16189
|
}
|
|
15385
16190
|
if (dashboardExists && (method === "GET" || method === "HEAD")) {
|
|
15386
16191
|
if (path !== "/") {
|
|
15387
|
-
const filePath =
|
|
15388
|
-
const resolvedFile =
|
|
15389
|
-
const resolvedBase =
|
|
16192
|
+
const filePath = join8(dashboardDir, path);
|
|
16193
|
+
const resolvedFile = resolve3(filePath);
|
|
16194
|
+
const resolvedBase = resolve3(dashboardDir);
|
|
15390
16195
|
if (!resolvedFile.startsWith(resolvedBase + sep) && resolvedFile !== resolvedBase) {
|
|
15391
16196
|
return json({ error: "Forbidden" }, 403, port);
|
|
15392
16197
|
}
|
|
@@ -15394,7 +16199,7 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
|
|
|
15394
16199
|
if (res2)
|
|
15395
16200
|
return res2;
|
|
15396
16201
|
}
|
|
15397
|
-
const indexPath =
|
|
16202
|
+
const indexPath = join8(dashboardDir, "index.html");
|
|
15398
16203
|
const res = serveStaticFile(indexPath);
|
|
15399
16204
|
if (res)
|
|
15400
16205
|
return res;
|
|
@@ -16483,21 +17288,307 @@ function App({ projectId }) {
|
|
|
16483
17288
|
]
|
|
16484
17289
|
}, undefined, true, undefined, this);
|
|
16485
17290
|
}
|
|
16486
|
-
function renderApp(projectId) {
|
|
16487
|
-
render(/* @__PURE__ */ jsxDEV7(App, {
|
|
16488
|
-
projectId
|
|
16489
|
-
}, undefined, false, undefined, this));
|
|
16490
|
-
}
|
|
16491
|
-
var init_App = __esm(() => {
|
|
16492
|
-
init_Header();
|
|
16493
|
-
init_TaskList();
|
|
16494
|
-
init_TaskDetail();
|
|
16495
|
-
init_TaskForm();
|
|
16496
|
-
init_ProjectList();
|
|
16497
|
-
init_SearchView();
|
|
17291
|
+
function renderApp(projectId) {
|
|
17292
|
+
render(/* @__PURE__ */ jsxDEV7(App, {
|
|
17293
|
+
projectId
|
|
17294
|
+
}, undefined, false, undefined, this));
|
|
17295
|
+
}
|
|
17296
|
+
var init_App = __esm(() => {
|
|
17297
|
+
init_Header();
|
|
17298
|
+
init_TaskList();
|
|
17299
|
+
init_TaskDetail();
|
|
17300
|
+
init_TaskForm();
|
|
17301
|
+
init_ProjectList();
|
|
17302
|
+
init_SearchView();
|
|
17303
|
+
init_tasks();
|
|
17304
|
+
init_projects();
|
|
17305
|
+
init_search();
|
|
17306
|
+
});
|
|
17307
|
+
|
|
17308
|
+
// src/cli/components/Dashboard.tsx
|
|
17309
|
+
var exports_Dashboard = {};
|
|
17310
|
+
__export(exports_Dashboard, {
|
|
17311
|
+
Dashboard: () => Dashboard
|
|
17312
|
+
});
|
|
17313
|
+
import { useState as useState4, useEffect as useEffect2 } from "react";
|
|
17314
|
+
import { Box as Box8, Text as Text7, useApp as useApp2, useInput as useInput4 } from "ink";
|
|
17315
|
+
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
17316
|
+
function AgentStatus({ name, lastSeen, sessionId }) {
|
|
17317
|
+
const ago = Math.round((Date.now() - new Date(lastSeen).getTime()) / 60000);
|
|
17318
|
+
const color = ago < 5 ? "green" : ago < 15 ? "yellow" : "red";
|
|
17319
|
+
const symbol = ago < 5 ? "\u25CF" : ago < 15 ? "\u25D0" : "\u25CB";
|
|
17320
|
+
return /* @__PURE__ */ jsxDEV8(Box8, {
|
|
17321
|
+
children: [
|
|
17322
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17323
|
+
color,
|
|
17324
|
+
children: [
|
|
17325
|
+
symbol,
|
|
17326
|
+
" "
|
|
17327
|
+
]
|
|
17328
|
+
}, undefined, true, undefined, this),
|
|
17329
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17330
|
+
bold: true,
|
|
17331
|
+
children: name
|
|
17332
|
+
}, undefined, false, undefined, this),
|
|
17333
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17334
|
+
dimColor: true,
|
|
17335
|
+
children: [
|
|
17336
|
+
" ",
|
|
17337
|
+
ago,
|
|
17338
|
+
"m ago"
|
|
17339
|
+
]
|
|
17340
|
+
}, undefined, true, undefined, this),
|
|
17341
|
+
sessionId && /* @__PURE__ */ jsxDEV8(Text7, {
|
|
17342
|
+
dimColor: true,
|
|
17343
|
+
children: [
|
|
17344
|
+
" [",
|
|
17345
|
+
sessionId.slice(0, 8),
|
|
17346
|
+
"]"
|
|
17347
|
+
]
|
|
17348
|
+
}, undefined, true, undefined, this)
|
|
17349
|
+
]
|
|
17350
|
+
}, undefined, true, undefined, this);
|
|
17351
|
+
}
|
|
17352
|
+
function Dashboard({ projectId, refreshMs = 2000 }) {
|
|
17353
|
+
const { exit } = useApp2();
|
|
17354
|
+
const [tick, setTick] = useState4(0);
|
|
17355
|
+
const [recap, setRecap] = useState4(null);
|
|
17356
|
+
const [counts, setCounts] = useState4({ pending: 0, in_progress: 0, completed: 0, failed: 0, total: 0 });
|
|
17357
|
+
const [agents, setAgents] = useState4([]);
|
|
17358
|
+
useInput4((input) => {
|
|
17359
|
+
if (input === "q" || input === "Q")
|
|
17360
|
+
exit();
|
|
17361
|
+
});
|
|
17362
|
+
useEffect2(() => {
|
|
17363
|
+
const timer = setInterval(() => setTick((t) => t + 1), refreshMs);
|
|
17364
|
+
return () => clearInterval(timer);
|
|
17365
|
+
}, [refreshMs]);
|
|
17366
|
+
useEffect2(() => {
|
|
17367
|
+
try {
|
|
17368
|
+
const db = getDatabase();
|
|
17369
|
+
const filters = projectId ? { project_id: projectId } : {};
|
|
17370
|
+
const pending = countTasks({ ...filters, status: "pending" }, db);
|
|
17371
|
+
const in_progress = countTasks({ ...filters, status: "in_progress" }, db);
|
|
17372
|
+
const completed = countTasks({ ...filters, status: "completed" }, db);
|
|
17373
|
+
const failed = countTasks({ ...filters, status: "failed" }, db);
|
|
17374
|
+
setCounts({ pending, in_progress, completed, failed, total: pending + in_progress + completed + failed });
|
|
17375
|
+
setAgents(listAgents());
|
|
17376
|
+
setRecap(getRecap(1, projectId, db));
|
|
17377
|
+
} catch {}
|
|
17378
|
+
}, [tick, projectId]);
|
|
17379
|
+
return /* @__PURE__ */ jsxDEV8(Box8, {
|
|
17380
|
+
flexDirection: "column",
|
|
17381
|
+
padding: 1,
|
|
17382
|
+
children: [
|
|
17383
|
+
/* @__PURE__ */ jsxDEV8(Box8, {
|
|
17384
|
+
marginBottom: 1,
|
|
17385
|
+
children: [
|
|
17386
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17387
|
+
bold: true,
|
|
17388
|
+
color: "cyan",
|
|
17389
|
+
children: " todos dashboard "
|
|
17390
|
+
}, undefined, false, undefined, this),
|
|
17391
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17392
|
+
dimColor: true,
|
|
17393
|
+
children: [
|
|
17394
|
+
"| refreshing every ",
|
|
17395
|
+
refreshMs / 1000,
|
|
17396
|
+
"s | press q to quit"
|
|
17397
|
+
]
|
|
17398
|
+
}, undefined, true, undefined, this)
|
|
17399
|
+
]
|
|
17400
|
+
}, undefined, true, undefined, this),
|
|
17401
|
+
/* @__PURE__ */ jsxDEV8(Box8, {
|
|
17402
|
+
marginBottom: 1,
|
|
17403
|
+
children: [
|
|
17404
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17405
|
+
color: "yellow",
|
|
17406
|
+
children: [
|
|
17407
|
+
counts.pending,
|
|
17408
|
+
" pending"
|
|
17409
|
+
]
|
|
17410
|
+
}, undefined, true, undefined, this),
|
|
17411
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17412
|
+
dimColor: true,
|
|
17413
|
+
children: " | "
|
|
17414
|
+
}, undefined, false, undefined, this),
|
|
17415
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17416
|
+
color: "blue",
|
|
17417
|
+
children: [
|
|
17418
|
+
counts.in_progress,
|
|
17419
|
+
" active"
|
|
17420
|
+
]
|
|
17421
|
+
}, undefined, true, undefined, this),
|
|
17422
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17423
|
+
dimColor: true,
|
|
17424
|
+
children: " | "
|
|
17425
|
+
}, undefined, false, undefined, this),
|
|
17426
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17427
|
+
color: "green",
|
|
17428
|
+
children: [
|
|
17429
|
+
counts.completed,
|
|
17430
|
+
" done"
|
|
17431
|
+
]
|
|
17432
|
+
}, undefined, true, undefined, this),
|
|
17433
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17434
|
+
dimColor: true,
|
|
17435
|
+
children: " | "
|
|
17436
|
+
}, undefined, false, undefined, this),
|
|
17437
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17438
|
+
color: "red",
|
|
17439
|
+
children: [
|
|
17440
|
+
counts.failed,
|
|
17441
|
+
" failed"
|
|
17442
|
+
]
|
|
17443
|
+
}, undefined, true, undefined, this),
|
|
17444
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17445
|
+
dimColor: true,
|
|
17446
|
+
children: " | "
|
|
17447
|
+
}, undefined, false, undefined, this),
|
|
17448
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17449
|
+
children: [
|
|
17450
|
+
counts.total,
|
|
17451
|
+
" total"
|
|
17452
|
+
]
|
|
17453
|
+
}, undefined, true, undefined, this)
|
|
17454
|
+
]
|
|
17455
|
+
}, undefined, true, undefined, this),
|
|
17456
|
+
agents.length > 0 && /* @__PURE__ */ jsxDEV8(Box8, {
|
|
17457
|
+
flexDirection: "column",
|
|
17458
|
+
marginBottom: 1,
|
|
17459
|
+
children: [
|
|
17460
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17461
|
+
bold: true,
|
|
17462
|
+
children: [
|
|
17463
|
+
"Agents (",
|
|
17464
|
+
agents.length,
|
|
17465
|
+
"):"
|
|
17466
|
+
]
|
|
17467
|
+
}, undefined, true, undefined, this),
|
|
17468
|
+
agents.map((a) => /* @__PURE__ */ jsxDEV8(AgentStatus, {
|
|
17469
|
+
name: a.name,
|
|
17470
|
+
lastSeen: a.last_seen_at,
|
|
17471
|
+
sessionId: a.session_id
|
|
17472
|
+
}, a.id, false, undefined, this))
|
|
17473
|
+
]
|
|
17474
|
+
}, undefined, true, undefined, this),
|
|
17475
|
+
recap && recap.in_progress.length > 0 && /* @__PURE__ */ jsxDEV8(Box8, {
|
|
17476
|
+
flexDirection: "column",
|
|
17477
|
+
marginBottom: 1,
|
|
17478
|
+
children: [
|
|
17479
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17480
|
+
bold: true,
|
|
17481
|
+
color: "blue",
|
|
17482
|
+
children: [
|
|
17483
|
+
"In Progress (",
|
|
17484
|
+
recap.in_progress.length,
|
|
17485
|
+
"):"
|
|
17486
|
+
]
|
|
17487
|
+
}, undefined, true, undefined, this),
|
|
17488
|
+
recap.in_progress.slice(0, 8).map((t) => /* @__PURE__ */ jsxDEV8(Box8, {
|
|
17489
|
+
children: [
|
|
17490
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17491
|
+
color: "cyan",
|
|
17492
|
+
children: [
|
|
17493
|
+
t.short_id || t.id.slice(0, 8),
|
|
17494
|
+
" "
|
|
17495
|
+
]
|
|
17496
|
+
}, undefined, true, undefined, this),
|
|
17497
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17498
|
+
children: t.title
|
|
17499
|
+
}, undefined, false, undefined, this),
|
|
17500
|
+
t.assigned_to && /* @__PURE__ */ jsxDEV8(Text7, {
|
|
17501
|
+
dimColor: true,
|
|
17502
|
+
children: [
|
|
17503
|
+
" \u2014 ",
|
|
17504
|
+
t.assigned_to
|
|
17505
|
+
]
|
|
17506
|
+
}, undefined, true, undefined, this)
|
|
17507
|
+
]
|
|
17508
|
+
}, t.id, true, undefined, this))
|
|
17509
|
+
]
|
|
17510
|
+
}, undefined, true, undefined, this),
|
|
17511
|
+
recap && recap.completed.length > 0 && /* @__PURE__ */ jsxDEV8(Box8, {
|
|
17512
|
+
flexDirection: "column",
|
|
17513
|
+
marginBottom: 1,
|
|
17514
|
+
children: [
|
|
17515
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17516
|
+
bold: true,
|
|
17517
|
+
color: "green",
|
|
17518
|
+
children: [
|
|
17519
|
+
"Completed (last 1h: ",
|
|
17520
|
+
recap.completed.length,
|
|
17521
|
+
"):"
|
|
17522
|
+
]
|
|
17523
|
+
}, undefined, true, undefined, this),
|
|
17524
|
+
recap.completed.slice(0, 5).map((t) => /* @__PURE__ */ jsxDEV8(Box8, {
|
|
17525
|
+
children: [
|
|
17526
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17527
|
+
color: "green",
|
|
17528
|
+
children: "\u2713 "
|
|
17529
|
+
}, undefined, false, undefined, this),
|
|
17530
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17531
|
+
color: "cyan",
|
|
17532
|
+
children: [
|
|
17533
|
+
t.short_id || t.id.slice(0, 8),
|
|
17534
|
+
" "
|
|
17535
|
+
]
|
|
17536
|
+
}, undefined, true, undefined, this),
|
|
17537
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17538
|
+
children: t.title
|
|
17539
|
+
}, undefined, false, undefined, this),
|
|
17540
|
+
t.duration_minutes != null && /* @__PURE__ */ jsxDEV8(Text7, {
|
|
17541
|
+
dimColor: true,
|
|
17542
|
+
children: [
|
|
17543
|
+
" (",
|
|
17544
|
+
t.duration_minutes,
|
|
17545
|
+
"m)"
|
|
17546
|
+
]
|
|
17547
|
+
}, undefined, true, undefined, this)
|
|
17548
|
+
]
|
|
17549
|
+
}, t.id, true, undefined, this))
|
|
17550
|
+
]
|
|
17551
|
+
}, undefined, true, undefined, this),
|
|
17552
|
+
recap && recap.stale.length > 0 && /* @__PURE__ */ jsxDEV8(Box8, {
|
|
17553
|
+
flexDirection: "column",
|
|
17554
|
+
children: [
|
|
17555
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17556
|
+
bold: true,
|
|
17557
|
+
color: "red",
|
|
17558
|
+
children: [
|
|
17559
|
+
"Stale (",
|
|
17560
|
+
recap.stale.length,
|
|
17561
|
+
"):"
|
|
17562
|
+
]
|
|
17563
|
+
}, undefined, true, undefined, this),
|
|
17564
|
+
recap.stale.slice(0, 3).map((t) => /* @__PURE__ */ jsxDEV8(Box8, {
|
|
17565
|
+
children: [
|
|
17566
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17567
|
+
color: "red",
|
|
17568
|
+
children: "! "
|
|
17569
|
+
}, undefined, false, undefined, this),
|
|
17570
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17571
|
+
color: "cyan",
|
|
17572
|
+
children: [
|
|
17573
|
+
t.short_id || t.id.slice(0, 8),
|
|
17574
|
+
" "
|
|
17575
|
+
]
|
|
17576
|
+
}, undefined, true, undefined, this),
|
|
17577
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17578
|
+
children: t.title
|
|
17579
|
+
}, undefined, false, undefined, this)
|
|
17580
|
+
]
|
|
17581
|
+
}, t.id, true, undefined, this))
|
|
17582
|
+
]
|
|
17583
|
+
}, undefined, true, undefined, this)
|
|
17584
|
+
]
|
|
17585
|
+
}, undefined, true, undefined, this);
|
|
17586
|
+
}
|
|
17587
|
+
var init_Dashboard = __esm(() => {
|
|
17588
|
+
init_database();
|
|
17589
|
+
init_agents();
|
|
16498
17590
|
init_tasks();
|
|
16499
|
-
|
|
16500
|
-
init_search();
|
|
17591
|
+
init_audit();
|
|
16501
17592
|
});
|
|
16502
17593
|
|
|
16503
17594
|
// node_modules/commander/esm.mjs
|
|
@@ -16528,14 +17619,14 @@ init_search();
|
|
|
16528
17619
|
init_sync();
|
|
16529
17620
|
init_config();
|
|
16530
17621
|
import chalk from "chalk";
|
|
16531
|
-
import { execSync } from "child_process";
|
|
16532
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as
|
|
16533
|
-
import { basename, dirname as dirname4, join as
|
|
17622
|
+
import { execSync as execSync2 } from "child_process";
|
|
17623
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
17624
|
+
import { basename, dirname as dirname4, join as join9, resolve as resolve4 } from "path";
|
|
16534
17625
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
16535
17626
|
function getPackageVersion() {
|
|
16536
17627
|
try {
|
|
16537
|
-
const pkgPath =
|
|
16538
|
-
return JSON.parse(
|
|
17628
|
+
const pkgPath = join9(dirname4(fileURLToPath3(import.meta.url)), "..", "..", "package.json");
|
|
17629
|
+
return JSON.parse(readFileSync5(pkgPath, "utf-8")).version || "0.0.0";
|
|
16539
17630
|
} catch {
|
|
16540
17631
|
return "0.0.0";
|
|
16541
17632
|
}
|
|
@@ -16567,14 +17658,14 @@ function resolveTaskId(partialId) {
|
|
|
16567
17658
|
}
|
|
16568
17659
|
function detectGitRoot() {
|
|
16569
17660
|
try {
|
|
16570
|
-
return
|
|
17661
|
+
return execSync2("git rev-parse --show-toplevel", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
16571
17662
|
} catch {
|
|
16572
17663
|
return null;
|
|
16573
17664
|
}
|
|
16574
17665
|
}
|
|
16575
17666
|
function autoDetectProject(opts) {
|
|
16576
17667
|
if (opts.project) {
|
|
16577
|
-
return getProjectByPath(
|
|
17668
|
+
return getProjectByPath(resolve4(opts.project)) ?? undefined;
|
|
16578
17669
|
}
|
|
16579
17670
|
if (process.env["TODOS_AUTO_PROJECT"] === "false")
|
|
16580
17671
|
return;
|
|
@@ -16842,8 +17933,15 @@ program2.command("show <id>").description("Show full task details").action((id)
|
|
|
16842
17933
|
console.log(` ${chalk.dim("Tags:")} ${task.tags.join(", ")}`);
|
|
16843
17934
|
console.log(` ${chalk.dim("Version:")} ${task.version}`);
|
|
16844
17935
|
console.log(` ${chalk.dim("Created:")} ${task.created_at}`);
|
|
16845
|
-
if (task.
|
|
17936
|
+
if (task.started_at)
|
|
17937
|
+
console.log(` ${chalk.dim("Started:")} ${task.started_at}`);
|
|
17938
|
+
if (task.completed_at) {
|
|
16846
17939
|
console.log(` ${chalk.dim("Done:")} ${task.completed_at}`);
|
|
17940
|
+
if (task.started_at) {
|
|
17941
|
+
const dur = Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 60000);
|
|
17942
|
+
console.log(` ${chalk.dim("Duration:")} ${dur}m`);
|
|
17943
|
+
}
|
|
17944
|
+
}
|
|
16847
17945
|
if (task.subtasks.length > 0) {
|
|
16848
17946
|
console.log(chalk.bold(`
|
|
16849
17947
|
Subtasks (${task.subtasks.length}):`));
|
|
@@ -16874,6 +17972,137 @@ program2.command("show <id>").description("Show full task details").action((id)
|
|
|
16874
17972
|
}
|
|
16875
17973
|
}
|
|
16876
17974
|
});
|
|
17975
|
+
program2.command("inspect [id]").description("Full orientation for a task \u2014 details, description, dependencies, blockers, files, commits, comments. If no ID given, shows current in-progress task for --agent.").action((id) => {
|
|
17976
|
+
const globalOpts = program2.opts();
|
|
17977
|
+
let resolvedId = id ? resolveTaskId(id) : null;
|
|
17978
|
+
if (!resolvedId && globalOpts.agent) {
|
|
17979
|
+
const { listTasks: listTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
17980
|
+
const active = listTasks2({ status: "in_progress", assigned_to: globalOpts.agent });
|
|
17981
|
+
if (active.length > 0)
|
|
17982
|
+
resolvedId = active[0].id;
|
|
17983
|
+
}
|
|
17984
|
+
if (!resolvedId) {
|
|
17985
|
+
console.error(chalk.red("No task ID given and no active task found. Pass an ID or use --agent."));
|
|
17986
|
+
process.exit(1);
|
|
17987
|
+
}
|
|
17988
|
+
const task = getTaskWithRelations(resolvedId);
|
|
17989
|
+
if (!task) {
|
|
17990
|
+
console.error(chalk.red(`Task not found: ${id || resolvedId}`));
|
|
17991
|
+
process.exit(1);
|
|
17992
|
+
}
|
|
17993
|
+
if (globalOpts.json) {
|
|
17994
|
+
const { listTaskFiles: listTaskFiles2 } = (init_task_files(), __toCommonJS(exports_task_files));
|
|
17995
|
+
const { getTaskCommits: getTaskCommits2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
|
|
17996
|
+
try {
|
|
17997
|
+
task.files = listTaskFiles2(task.id);
|
|
17998
|
+
} catch {
|
|
17999
|
+
task.files = [];
|
|
18000
|
+
}
|
|
18001
|
+
try {
|
|
18002
|
+
task.commits = getTaskCommits2(task.id);
|
|
18003
|
+
} catch {
|
|
18004
|
+
task.commits = [];
|
|
18005
|
+
}
|
|
18006
|
+
output(task, true);
|
|
18007
|
+
return;
|
|
18008
|
+
}
|
|
18009
|
+
const sid = task.short_id || task.id.slice(0, 8);
|
|
18010
|
+
const statusColor = statusColors4[task.status] || chalk.white;
|
|
18011
|
+
const prioColor = priorityColors2[task.priority] || chalk.white;
|
|
18012
|
+
console.log(chalk.bold(`
|
|
18013
|
+
${chalk.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${task.title}
|
|
18014
|
+
`));
|
|
18015
|
+
if (task.description) {
|
|
18016
|
+
console.log(chalk.dim("Description:"));
|
|
18017
|
+
console.log(` ${task.description}
|
|
18018
|
+
`);
|
|
18019
|
+
}
|
|
18020
|
+
if (task.assigned_to)
|
|
18021
|
+
console.log(` ${chalk.dim("Assigned:")} ${task.assigned_to}`);
|
|
18022
|
+
if (task.locked_by)
|
|
18023
|
+
console.log(` ${chalk.dim("Locked by:")} ${task.locked_by}`);
|
|
18024
|
+
if (task.project_id)
|
|
18025
|
+
console.log(` ${chalk.dim("Project:")} ${task.project_id}`);
|
|
18026
|
+
if (task.plan_id)
|
|
18027
|
+
console.log(` ${chalk.dim("Plan:")} ${task.plan_id}`);
|
|
18028
|
+
if (task.started_at)
|
|
18029
|
+
console.log(` ${chalk.dim("Started:")} ${task.started_at}`);
|
|
18030
|
+
if (task.completed_at) {
|
|
18031
|
+
console.log(` ${chalk.dim("Completed:")} ${task.completed_at}`);
|
|
18032
|
+
if (task.started_at) {
|
|
18033
|
+
const dur = Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 60000);
|
|
18034
|
+
console.log(` ${chalk.dim("Duration:")} ${dur}m`);
|
|
18035
|
+
}
|
|
18036
|
+
}
|
|
18037
|
+
if (task.estimated_minutes)
|
|
18038
|
+
console.log(` ${chalk.dim("Estimate:")} ${task.estimated_minutes}m`);
|
|
18039
|
+
if (task.tags.length > 0)
|
|
18040
|
+
console.log(` ${chalk.dim("Tags:")} ${task.tags.join(", ")}`);
|
|
18041
|
+
const unfinishedDeps = task.dependencies.filter((d) => d.status !== "completed" && d.status !== "cancelled");
|
|
18042
|
+
if (task.dependencies.length > 0) {
|
|
18043
|
+
console.log(chalk.bold(`
|
|
18044
|
+
Depends on (${task.dependencies.length}):`));
|
|
18045
|
+
for (const dep of task.dependencies) {
|
|
18046
|
+
const blocked = dep.status !== "completed" && dep.status !== "cancelled";
|
|
18047
|
+
const icon = blocked ? chalk.red("\u2717") : chalk.green("\u2713");
|
|
18048
|
+
console.log(` ${icon} ${formatTaskLine(dep)}`);
|
|
18049
|
+
}
|
|
18050
|
+
}
|
|
18051
|
+
if (unfinishedDeps.length > 0) {
|
|
18052
|
+
console.log(chalk.red(`
|
|
18053
|
+
BLOCKED by ${unfinishedDeps.length} unfinished dep(s)`));
|
|
18054
|
+
}
|
|
18055
|
+
if (task.blocked_by.length > 0) {
|
|
18056
|
+
console.log(chalk.bold(`
|
|
18057
|
+
Blocks (${task.blocked_by.length}):`));
|
|
18058
|
+
for (const b of task.blocked_by)
|
|
18059
|
+
console.log(` ${formatTaskLine(b)}`);
|
|
18060
|
+
}
|
|
18061
|
+
if (task.subtasks.length > 0) {
|
|
18062
|
+
console.log(chalk.bold(`
|
|
18063
|
+
Subtasks (${task.subtasks.length}):`));
|
|
18064
|
+
for (const st of task.subtasks)
|
|
18065
|
+
console.log(` ${formatTaskLine(st)}`);
|
|
18066
|
+
}
|
|
18067
|
+
try {
|
|
18068
|
+
const { listTaskFiles: listTaskFiles2 } = (init_task_files(), __toCommonJS(exports_task_files));
|
|
18069
|
+
const files = listTaskFiles2(task.id);
|
|
18070
|
+
if (files.length > 0) {
|
|
18071
|
+
console.log(chalk.bold(`
|
|
18072
|
+
Files (${files.length}):`));
|
|
18073
|
+
for (const f of files)
|
|
18074
|
+
console.log(` ${chalk.dim(f.role || "file")} ${f.path}`);
|
|
18075
|
+
}
|
|
18076
|
+
} catch {}
|
|
18077
|
+
try {
|
|
18078
|
+
const { getTaskCommits: getTaskCommits2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
|
|
18079
|
+
const commits = getTaskCommits2(task.id);
|
|
18080
|
+
if (commits.length > 0) {
|
|
18081
|
+
console.log(chalk.bold(`
|
|
18082
|
+
Commits (${commits.length}):`));
|
|
18083
|
+
for (const c of commits)
|
|
18084
|
+
console.log(` ${chalk.yellow(c.commit_hash.slice(0, 7))} ${c.message || ""}`);
|
|
18085
|
+
}
|
|
18086
|
+
} catch {}
|
|
18087
|
+
if (task.comments.length > 0) {
|
|
18088
|
+
console.log(chalk.bold(`
|
|
18089
|
+
Comments (${task.comments.length}):`));
|
|
18090
|
+
for (const c of task.comments) {
|
|
18091
|
+
const agent = c.agent_id ? chalk.cyan(`[${c.agent_id}] `) : "";
|
|
18092
|
+
console.log(` ${agent}${chalk.dim(c.created_at)}: ${c.content}`);
|
|
18093
|
+
}
|
|
18094
|
+
}
|
|
18095
|
+
if (task.checklist && task.checklist.length > 0) {
|
|
18096
|
+
const done = task.checklist.filter((c) => c.checked).length;
|
|
18097
|
+
console.log(chalk.bold(`
|
|
18098
|
+
Checklist (${done}/${task.checklist.length}):`));
|
|
18099
|
+
for (const item of task.checklist) {
|
|
18100
|
+
const icon = item.checked ? chalk.green("\u2611") : chalk.dim("\u2610");
|
|
18101
|
+
console.log(` ${icon} ${item.text || item.title}`);
|
|
18102
|
+
}
|
|
18103
|
+
}
|
|
18104
|
+
console.log();
|
|
18105
|
+
});
|
|
16877
18106
|
program2.command("history <id>").description("Show change history for a task (audit log)").action((id) => {
|
|
16878
18107
|
const globalOpts = program2.opts();
|
|
16879
18108
|
const resolvedId = resolveTaskId(id);
|
|
@@ -17377,7 +18606,7 @@ program2.command("deps <id>").description("Manage task dependencies").option("--
|
|
|
17377
18606
|
program2.command("projects").description("List and manage projects").option("--add <path>", "Register a project by path").option("--name <name>", "Project name (with --add)").option("--task-list-id <id>", "Custom task list ID (with --add)").action((opts) => {
|
|
17378
18607
|
const globalOpts = program2.opts();
|
|
17379
18608
|
if (opts.add) {
|
|
17380
|
-
const projectPath =
|
|
18609
|
+
const projectPath = resolve4(opts.add);
|
|
17381
18610
|
const name = opts.name || basename(projectPath);
|
|
17382
18611
|
const existing = getProjectByPath(projectPath);
|
|
17383
18612
|
let project;
|
|
@@ -17414,6 +18643,55 @@ program2.command("projects").description("List and manage projects").option("--a
|
|
|
17414
18643
|
console.log(`${chalk.dim(p.id.slice(0, 8))} ${chalk.bold(p.name)} ${chalk.dim(p.path)}${taskList}${p.description ? ` - ${p.description}` : ""}`);
|
|
17415
18644
|
}
|
|
17416
18645
|
});
|
|
18646
|
+
program2.command("extract <path>").description("Extract TODO/FIXME/HACK/BUG/XXX/NOTE comments from source files and create tasks").option("--dry-run", "Show extracted comments without creating tasks").option("--pattern <tags>", "Comma-separated tags to look for (default: TODO,FIXME,HACK,XXX,BUG,NOTE)").option("-t, --tags <tags>", "Extra comma-separated tags to add to created tasks").option("--assign <agent>", "Assign extracted tasks to an agent").option("--list <id>", "Task list ID").option("--ext <extensions>", "Comma-separated file extensions to scan (e.g. ts,py,go)").action((scanPath, opts) => {
|
|
18647
|
+
try {
|
|
18648
|
+
const globalOpts = program2.opts();
|
|
18649
|
+
const projectId = autoProject(globalOpts);
|
|
18650
|
+
const { extractTodos: extractTodos2, EXTRACT_TAGS: EXTRACT_TAGS2 } = (init_extract(), __toCommonJS(exports_extract));
|
|
18651
|
+
const patterns = opts.pattern ? opts.pattern.split(",").map((t) => t.trim().toUpperCase()) : undefined;
|
|
18652
|
+
const taskListId = opts.list ? (() => {
|
|
18653
|
+
const db = getDatabase();
|
|
18654
|
+
const id = resolvePartialId(db, "task_lists", opts.list);
|
|
18655
|
+
if (!id) {
|
|
18656
|
+
console.error(chalk.red(`Could not resolve task list ID: ${opts.list}`));
|
|
18657
|
+
process.exit(1);
|
|
18658
|
+
}
|
|
18659
|
+
return id;
|
|
18660
|
+
})() : undefined;
|
|
18661
|
+
const result = extractTodos2({
|
|
18662
|
+
path: resolve4(scanPath),
|
|
18663
|
+
patterns,
|
|
18664
|
+
project_id: projectId,
|
|
18665
|
+
task_list_id: taskListId,
|
|
18666
|
+
tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
|
|
18667
|
+
assigned_to: opts.assign,
|
|
18668
|
+
agent_id: globalOpts.agent,
|
|
18669
|
+
dry_run: opts.dryRun,
|
|
18670
|
+
extensions: opts.ext ? opts.ext.split(",").map((e) => e.trim()) : undefined
|
|
18671
|
+
});
|
|
18672
|
+
if (globalOpts.json) {
|
|
18673
|
+
console.log(JSON.stringify(opts.dryRun ? { comments: result.comments } : { tasks_created: result.tasks.length, skipped: result.skipped, comments: result.comments.length }, null, 2));
|
|
18674
|
+
} else if (opts.dryRun) {
|
|
18675
|
+
console.log(chalk.cyan(`Found ${result.comments.length} comment(s):
|
|
18676
|
+
`));
|
|
18677
|
+
for (const c of result.comments) {
|
|
18678
|
+
console.log(` ${chalk.yellow(`[${c.tag}]`)} ${c.message}`);
|
|
18679
|
+
console.log(` ${chalk.gray(`${c.file}:${c.line}`)}`);
|
|
18680
|
+
}
|
|
18681
|
+
} else {
|
|
18682
|
+
console.log(chalk.green(`Created ${result.tasks.length} task(s)`));
|
|
18683
|
+
if (result.skipped > 0) {
|
|
18684
|
+
console.log(chalk.gray(`Skipped ${result.skipped} duplicate(s)`));
|
|
18685
|
+
}
|
|
18686
|
+
console.log(chalk.gray(`Total comments found: ${result.comments.length}`));
|
|
18687
|
+
for (const t of result.tasks) {
|
|
18688
|
+
console.log(formatTaskLine(t));
|
|
18689
|
+
}
|
|
18690
|
+
}
|
|
18691
|
+
} catch (e) {
|
|
18692
|
+
handleError(e);
|
|
18693
|
+
}
|
|
18694
|
+
});
|
|
17417
18695
|
program2.command("export").description("Export tasks").option("-f, --format <format>", "Format: json or md", "json").action((opts) => {
|
|
17418
18696
|
const globalOpts = program2.opts();
|
|
17419
18697
|
const projectId = autoProject(globalOpts);
|
|
@@ -17479,11 +18757,11 @@ var hooks = program2.command("hooks").description("Manage Claude Code hook integ
|
|
|
17479
18757
|
hooks.command("install").description("Install Claude Code hooks for auto-sync").action(() => {
|
|
17480
18758
|
let todosBin = "todos";
|
|
17481
18759
|
try {
|
|
17482
|
-
const p =
|
|
18760
|
+
const p = execSync2("which todos", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
17483
18761
|
if (p)
|
|
17484
18762
|
todosBin = p;
|
|
17485
18763
|
} catch {}
|
|
17486
|
-
const hooksDir =
|
|
18764
|
+
const hooksDir = join9(process.cwd(), ".claude", "hooks");
|
|
17487
18765
|
if (!existsSync7(hooksDir))
|
|
17488
18766
|
mkdirSync3(hooksDir, { recursive: true });
|
|
17489
18767
|
const hookScript = `#!/usr/bin/env bash
|
|
@@ -17509,11 +18787,11 @@ esac
|
|
|
17509
18787
|
|
|
17510
18788
|
exit 0
|
|
17511
18789
|
`;
|
|
17512
|
-
const hookPath =
|
|
18790
|
+
const hookPath = join9(hooksDir, "todos-sync.sh");
|
|
17513
18791
|
writeFileSync3(hookPath, hookScript);
|
|
17514
|
-
|
|
18792
|
+
execSync2(`chmod +x "${hookPath}"`);
|
|
17515
18793
|
console.log(chalk.green(`Hook script created: ${hookPath}`));
|
|
17516
|
-
const settingsPath =
|
|
18794
|
+
const settingsPath = join9(process.cwd(), ".claude", "settings.json");
|
|
17517
18795
|
const settings = readJsonFile2(settingsPath);
|
|
17518
18796
|
if (!settings["hooks"]) {
|
|
17519
18797
|
settings["hooks"] = {};
|
|
@@ -17556,11 +18834,11 @@ program2.command("mcp").description("Start MCP server (stdio)").option("--regist
|
|
|
17556
18834
|
var HOME2 = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
17557
18835
|
function getMcpBinaryPath() {
|
|
17558
18836
|
try {
|
|
17559
|
-
const p =
|
|
18837
|
+
const p = execSync2("which todos-mcp", { encoding: "utf-8" }).trim();
|
|
17560
18838
|
if (p)
|
|
17561
18839
|
return p;
|
|
17562
18840
|
} catch {}
|
|
17563
|
-
const bunBin =
|
|
18841
|
+
const bunBin = join9(HOME2, ".bun", "bin", "todos-mcp");
|
|
17564
18842
|
if (existsSync7(bunBin))
|
|
17565
18843
|
return bunBin;
|
|
17566
18844
|
return "todos-mcp";
|
|
@@ -17569,7 +18847,7 @@ function readJsonFile2(path) {
|
|
|
17569
18847
|
if (!existsSync7(path))
|
|
17570
18848
|
return {};
|
|
17571
18849
|
try {
|
|
17572
|
-
return JSON.parse(
|
|
18850
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
17573
18851
|
} catch {
|
|
17574
18852
|
return {};
|
|
17575
18853
|
}
|
|
@@ -17584,7 +18862,7 @@ function writeJsonFile2(path, data) {
|
|
|
17584
18862
|
function readTomlFile(path) {
|
|
17585
18863
|
if (!existsSync7(path))
|
|
17586
18864
|
return "";
|
|
17587
|
-
return
|
|
18865
|
+
return readFileSync5(path, "utf-8");
|
|
17588
18866
|
}
|
|
17589
18867
|
function writeTomlFile(path, content) {
|
|
17590
18868
|
const dir = dirname4(path);
|
|
@@ -17596,8 +18874,8 @@ function registerClaude(binPath, global) {
|
|
|
17596
18874
|
const scope = global ? "user" : "project";
|
|
17597
18875
|
const cmd = `claude mcp add --transport stdio --scope ${scope} todos -- ${binPath}`;
|
|
17598
18876
|
try {
|
|
17599
|
-
const { execSync:
|
|
17600
|
-
|
|
18877
|
+
const { execSync: execSync3 } = __require("child_process");
|
|
18878
|
+
execSync3(cmd, { stdio: "pipe" });
|
|
17601
18879
|
console.log(chalk.green(`Claude Code (${scope}): registered via 'claude mcp add'`));
|
|
17602
18880
|
} catch {
|
|
17603
18881
|
console.log(chalk.yellow(`Claude Code: could not auto-register. Run this command manually:`));
|
|
@@ -17606,8 +18884,8 @@ function registerClaude(binPath, global) {
|
|
|
17606
18884
|
}
|
|
17607
18885
|
function unregisterClaude(_global) {
|
|
17608
18886
|
try {
|
|
17609
|
-
const { execSync:
|
|
17610
|
-
|
|
18887
|
+
const { execSync: execSync3 } = __require("child_process");
|
|
18888
|
+
execSync3("claude mcp remove todos", { stdio: "pipe" });
|
|
17611
18889
|
console.log(chalk.green(`Claude Code: removed todos MCP server`));
|
|
17612
18890
|
} catch {
|
|
17613
18891
|
console.log(chalk.yellow(`Claude Code: could not auto-remove. Run manually:`));
|
|
@@ -17615,7 +18893,7 @@ function unregisterClaude(_global) {
|
|
|
17615
18893
|
}
|
|
17616
18894
|
}
|
|
17617
18895
|
function registerCodex(binPath) {
|
|
17618
|
-
const configPath =
|
|
18896
|
+
const configPath = join9(HOME2, ".codex", "config.toml");
|
|
17619
18897
|
let content = readTomlFile(configPath);
|
|
17620
18898
|
content = removeTomlBlock(content, "mcp_servers.todos");
|
|
17621
18899
|
const block = `
|
|
@@ -17629,7 +18907,7 @@ args = []
|
|
|
17629
18907
|
console.log(chalk.green(`Codex CLI: registered in ${configPath}`));
|
|
17630
18908
|
}
|
|
17631
18909
|
function unregisterCodex() {
|
|
17632
|
-
const configPath =
|
|
18910
|
+
const configPath = join9(HOME2, ".codex", "config.toml");
|
|
17633
18911
|
let content = readTomlFile(configPath);
|
|
17634
18912
|
if (!content.includes("[mcp_servers.todos]")) {
|
|
17635
18913
|
console.log(chalk.dim(`Codex CLI: todos not found in ${configPath}`));
|
|
@@ -17662,7 +18940,7 @@ function removeTomlBlock(content, blockName) {
|
|
|
17662
18940
|
`);
|
|
17663
18941
|
}
|
|
17664
18942
|
function registerGemini(binPath) {
|
|
17665
|
-
const configPath =
|
|
18943
|
+
const configPath = join9(HOME2, ".gemini", "settings.json");
|
|
17666
18944
|
const config = readJsonFile2(configPath);
|
|
17667
18945
|
if (!config["mcpServers"]) {
|
|
17668
18946
|
config["mcpServers"] = {};
|
|
@@ -17676,7 +18954,7 @@ function registerGemini(binPath) {
|
|
|
17676
18954
|
console.log(chalk.green(`Gemini CLI: registered in ${configPath}`));
|
|
17677
18955
|
}
|
|
17678
18956
|
function unregisterGemini() {
|
|
17679
|
-
const configPath =
|
|
18957
|
+
const configPath = join9(HOME2, ".gemini", "settings.json");
|
|
17680
18958
|
const config = readJsonFile2(configPath);
|
|
17681
18959
|
const servers = config["mcpServers"];
|
|
17682
18960
|
if (!servers || !("todos" in servers)) {
|
|
@@ -17724,6 +19002,115 @@ function unregisterMcp(agent, global) {
|
|
|
17724
19002
|
}
|
|
17725
19003
|
}
|
|
17726
19004
|
}
|
|
19005
|
+
program2.command("import <url>").description("Import a GitHub issue as a task").option("--project <id>", "Project ID").option("--list <id>", "Task list ID").action((url, opts) => {
|
|
19006
|
+
const globalOpts = program2.opts();
|
|
19007
|
+
const { parseGitHubUrl: parseGitHubUrl2, fetchGitHubIssue: fetchGitHubIssue2, issueToTask: issueToTask2 } = (init_github(), __toCommonJS(exports_github));
|
|
19008
|
+
const parsed = parseGitHubUrl2(url);
|
|
19009
|
+
if (!parsed) {
|
|
19010
|
+
console.error(chalk.red("Invalid GitHub issue URL. Expected: https://github.com/owner/repo/issues/123"));
|
|
19011
|
+
process.exit(1);
|
|
19012
|
+
}
|
|
19013
|
+
try {
|
|
19014
|
+
const issue = fetchGitHubIssue2(parsed.owner, parsed.repo, parsed.number);
|
|
19015
|
+
const projectId = opts.project || autoProject(globalOpts) || undefined;
|
|
19016
|
+
const input = issueToTask2(issue, { project_id: projectId, task_list_id: opts.list });
|
|
19017
|
+
const task = createTask(input);
|
|
19018
|
+
if (globalOpts.json) {
|
|
19019
|
+
output(task, true);
|
|
19020
|
+
return;
|
|
19021
|
+
}
|
|
19022
|
+
console.log(chalk.green(`Imported GH#${issue.number}: ${issue.title}`));
|
|
19023
|
+
console.log(` ${chalk.dim("Task ID:")} ${task.short_id || task.id}`);
|
|
19024
|
+
console.log(` ${chalk.dim("Labels:")} ${issue.labels.join(", ") || "none"}`);
|
|
19025
|
+
console.log(` ${chalk.dim("Priority:")} ${task.priority}`);
|
|
19026
|
+
} catch (e) {
|
|
19027
|
+
if (e.message?.includes("gh")) {
|
|
19028
|
+
console.error(chalk.red("GitHub CLI (gh) not found or not authenticated. Install: https://cli.github.com"));
|
|
19029
|
+
} else {
|
|
19030
|
+
console.error(chalk.red(`Import failed: ${e.message}`));
|
|
19031
|
+
}
|
|
19032
|
+
process.exit(1);
|
|
19033
|
+
}
|
|
19034
|
+
});
|
|
19035
|
+
program2.command("link-commit <task-id> <sha>").description("Link a git commit to a task").option("--message <text>", "Commit message").option("--author <name>", "Commit author").option("--files <list>", "Comma-separated list of changed files").action((taskId, sha, opts) => {
|
|
19036
|
+
const globalOpts = program2.opts();
|
|
19037
|
+
const resolvedId = resolveTaskId(taskId);
|
|
19038
|
+
const { linkTaskToCommit: linkTaskToCommit2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
|
|
19039
|
+
const commit = linkTaskToCommit2({
|
|
19040
|
+
task_id: resolvedId,
|
|
19041
|
+
sha,
|
|
19042
|
+
message: opts.message,
|
|
19043
|
+
author: opts.author,
|
|
19044
|
+
files_changed: opts.files ? opts.files.split(",").filter(Boolean) : undefined
|
|
19045
|
+
});
|
|
19046
|
+
if (globalOpts.json) {
|
|
19047
|
+
output(commit, true);
|
|
19048
|
+
return;
|
|
19049
|
+
}
|
|
19050
|
+
console.log(chalk.green(`Linked commit ${sha.slice(0, 7)} to task ${taskId}`));
|
|
19051
|
+
});
|
|
19052
|
+
var hookCmd = program2.command("hook").description("Manage git hooks for auto-linking commits to tasks");
|
|
19053
|
+
hookCmd.command("install").description("Install post-commit hook that auto-links commits to tasks").action(() => {
|
|
19054
|
+
const { execSync: execSync3 } = __require("child_process");
|
|
19055
|
+
try {
|
|
19056
|
+
const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
|
|
19057
|
+
const hookPath = `${gitDir}/hooks/post-commit`;
|
|
19058
|
+
const { existsSync: existsSync8, readFileSync: readFileSync6, writeFileSync: writeFileSync4, chmodSync } = __require("fs");
|
|
19059
|
+
const marker = "# todos-auto-link";
|
|
19060
|
+
if (existsSync8(hookPath)) {
|
|
19061
|
+
const existing = readFileSync6(hookPath, "utf-8");
|
|
19062
|
+
if (existing.includes(marker)) {
|
|
19063
|
+
console.log(chalk.yellow("Hook already installed."));
|
|
19064
|
+
return;
|
|
19065
|
+
}
|
|
19066
|
+
writeFileSync4(hookPath, existing + `
|
|
19067
|
+
${marker}
|
|
19068
|
+
$(dirname "$0")/../../scripts/post-commit-hook.sh
|
|
19069
|
+
`);
|
|
19070
|
+
} else {
|
|
19071
|
+
writeFileSync4(hookPath, `#!/usr/bin/env bash
|
|
19072
|
+
${marker}
|
|
19073
|
+
$(dirname "$0")/../../scripts/post-commit-hook.sh
|
|
19074
|
+
`);
|
|
19075
|
+
chmodSync(hookPath, 493);
|
|
19076
|
+
}
|
|
19077
|
+
console.log(chalk.green("Post-commit hook installed. Commits with task IDs (e.g. OPE-00042) will auto-link."));
|
|
19078
|
+
} catch (e) {
|
|
19079
|
+
console.error(chalk.red("Not in a git repository or hook install failed."));
|
|
19080
|
+
process.exit(1);
|
|
19081
|
+
}
|
|
19082
|
+
});
|
|
19083
|
+
hookCmd.command("uninstall").description("Remove the todos post-commit hook").action(() => {
|
|
19084
|
+
const { execSync: execSync3 } = __require("child_process");
|
|
19085
|
+
try {
|
|
19086
|
+
const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
|
|
19087
|
+
const hookPath = `${gitDir}/hooks/post-commit`;
|
|
19088
|
+
const { existsSync: existsSync8, readFileSync: readFileSync6, writeFileSync: writeFileSync4 } = __require("fs");
|
|
19089
|
+
const marker = "# todos-auto-link";
|
|
19090
|
+
if (!existsSync8(hookPath)) {
|
|
19091
|
+
console.log(chalk.dim("No post-commit hook found."));
|
|
19092
|
+
return;
|
|
19093
|
+
}
|
|
19094
|
+
const content = readFileSync6(hookPath, "utf-8");
|
|
19095
|
+
if (!content.includes(marker)) {
|
|
19096
|
+
console.log(chalk.dim("Hook not managed by todos."));
|
|
19097
|
+
return;
|
|
19098
|
+
}
|
|
19099
|
+
const cleaned = content.split(`
|
|
19100
|
+
`).filter((l) => !l.includes(marker) && !l.includes("post-commit-hook.sh")).join(`
|
|
19101
|
+
`).trim();
|
|
19102
|
+
if (cleaned === "#!/usr/bin/env bash" || cleaned === "") {
|
|
19103
|
+
__require("fs").unlinkSync(hookPath);
|
|
19104
|
+
} else {
|
|
19105
|
+
writeFileSync4(hookPath, cleaned + `
|
|
19106
|
+
`);
|
|
19107
|
+
}
|
|
19108
|
+
console.log(chalk.green("Post-commit hook removed."));
|
|
19109
|
+
} catch (e) {
|
|
19110
|
+
console.error(chalk.red("Not in a git repository or hook removal failed."));
|
|
19111
|
+
process.exit(1);
|
|
19112
|
+
}
|
|
19113
|
+
});
|
|
17727
19114
|
program2.command("init <name>").description("Register an agent and get a short UUID").option("-d, --description <text>", "Agent description").action((name, opts) => {
|
|
17728
19115
|
const globalOpts = program2.opts();
|
|
17729
19116
|
try {
|
|
@@ -17765,6 +19152,30 @@ program2.command("heartbeat [agent]").description("Update last_seen_at to signal
|
|
|
17765
19152
|
console.log(chalk.green(`\u2665 ${a.name} (${a.id.slice(0, 8)}) \u2014 heartbeat sent`));
|
|
17766
19153
|
}
|
|
17767
19154
|
});
|
|
19155
|
+
program2.command("release [agent]").description("Release/logout an agent \u2014 clears session binding so the name is immediately available").option("--session-id <id>", "Only release if session ID matches").action((agent, opts) => {
|
|
19156
|
+
const globalOpts = program2.opts();
|
|
19157
|
+
const agentId = agent || globalOpts.agent;
|
|
19158
|
+
if (!agentId) {
|
|
19159
|
+
console.error(chalk.red("Agent ID or name required. Use --agent or pass as argument."));
|
|
19160
|
+
process.exit(1);
|
|
19161
|
+
}
|
|
19162
|
+
const { getAgent: getAgent2, getAgentByName: getAgentByName2 } = (init_agents(), __toCommonJS(exports_agents));
|
|
19163
|
+
const a = getAgent2(agentId) || getAgentByName2(agentId);
|
|
19164
|
+
if (!a) {
|
|
19165
|
+
console.error(chalk.red(`Agent not found: ${agentId}`));
|
|
19166
|
+
process.exit(1);
|
|
19167
|
+
}
|
|
19168
|
+
const released = releaseAgent(a.id, opts?.sessionId);
|
|
19169
|
+
if (!released) {
|
|
19170
|
+
console.error(chalk.red("Release denied: session_id does not match agent's current session."));
|
|
19171
|
+
process.exit(1);
|
|
19172
|
+
}
|
|
19173
|
+
if (globalOpts.json) {
|
|
19174
|
+
console.log(JSON.stringify({ agent_id: a.id, name: a.name, released: true }));
|
|
19175
|
+
} else {
|
|
19176
|
+
console.log(chalk.green(`\u2713 ${a.name} (${a.id}) released \u2014 name is now available.`));
|
|
19177
|
+
}
|
|
19178
|
+
});
|
|
17768
19179
|
program2.command("focus [project]").description("Focus on a project (or clear focus if no project given)").action((project) => {
|
|
17769
19180
|
const globalOpts = program2.opts();
|
|
17770
19181
|
const agentId = globalOpts.agent;
|
|
@@ -17990,13 +19401,13 @@ Update available: ${currentVersion} \u2192 ${latestVersion}`));
|
|
|
17990
19401
|
}
|
|
17991
19402
|
let useBun = false;
|
|
17992
19403
|
try {
|
|
17993
|
-
|
|
19404
|
+
execSync2("which bun", { stdio: "ignore" });
|
|
17994
19405
|
useBun = true;
|
|
17995
19406
|
} catch {}
|
|
17996
19407
|
const cmd = useBun ? "bun add -g @hasna/todos@latest" : "npm install -g @hasna/todos@latest";
|
|
17997
19408
|
console.log(chalk.dim(`
|
|
17998
19409
|
Running: ${cmd}`));
|
|
17999
|
-
|
|
19410
|
+
execSync2(cmd, { stdio: "inherit" });
|
|
18000
19411
|
console.log(chalk.green(`
|
|
18001
19412
|
Updated to ${latestVersion}!`));
|
|
18002
19413
|
} catch (e) {
|
|
@@ -18005,7 +19416,7 @@ Updated to ${latestVersion}!`));
|
|
|
18005
19416
|
});
|
|
18006
19417
|
program2.command("config").description("View or update configuration").option("--get <key>", "Get a config value").option("--set <key=value>", "Set a config value (e.g. completion_guard.enabled=true)").action((opts) => {
|
|
18007
19418
|
const globalOpts = program2.opts();
|
|
18008
|
-
const configPath =
|
|
19419
|
+
const configPath = join9(process.env["HOME"] || "~", ".todos", "config.json");
|
|
18009
19420
|
if (opts.get) {
|
|
18010
19421
|
const config2 = loadConfig();
|
|
18011
19422
|
const keys = opts.get.split(".");
|
|
@@ -18031,7 +19442,7 @@ program2.command("config").description("View or update configuration").option("-
|
|
|
18031
19442
|
}
|
|
18032
19443
|
let config2 = {};
|
|
18033
19444
|
try {
|
|
18034
|
-
config2 = JSON.parse(
|
|
19445
|
+
config2 = JSON.parse(readFileSync5(configPath, "utf-8"));
|
|
18035
19446
|
} catch {}
|
|
18036
19447
|
const keys = key.split(".");
|
|
18037
19448
|
let obj = config2;
|
|
@@ -18188,6 +19599,51 @@ program2.command("interactive").description("Launch interactive TUI").action(asy
|
|
|
18188
19599
|
const projectId = autoProject(globalOpts);
|
|
18189
19600
|
renderApp2(projectId);
|
|
18190
19601
|
});
|
|
19602
|
+
program2.command("blame <file>").description("Show which tasks/agents touched a file and why \u2014 combines task_files + task_commits").action((filePath) => {
|
|
19603
|
+
const globalOpts = program2.opts();
|
|
19604
|
+
const { findTasksByFile: findTasksByFile2 } = (init_task_files(), __toCommonJS(exports_task_files));
|
|
19605
|
+
const { getTask: getTask2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
19606
|
+
const db = getDatabase();
|
|
19607
|
+
const taskFiles = findTasksByFile2(filePath, db);
|
|
19608
|
+
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(`%${filePath}%`);
|
|
19609
|
+
if (globalOpts.json) {
|
|
19610
|
+
output({ file: filePath, task_files: taskFiles, commits: commitRows }, true);
|
|
19611
|
+
return;
|
|
19612
|
+
}
|
|
19613
|
+
console.log(chalk.bold(`
|
|
19614
|
+
Blame: ${filePath}
|
|
19615
|
+
`));
|
|
19616
|
+
if (taskFiles.length > 0) {
|
|
19617
|
+
console.log(chalk.bold("Task File Links:"));
|
|
19618
|
+
for (const tf of taskFiles) {
|
|
19619
|
+
const task = getTask2(tf.task_id, db);
|
|
19620
|
+
const title = task ? task.title : "unknown";
|
|
19621
|
+
const sid = task?.short_id || tf.task_id.slice(0, 8);
|
|
19622
|
+
console.log(` ${chalk.cyan(sid)} ${title} \u2014 ${chalk.dim(tf.role || "file")} ${chalk.dim(tf.updated_at)}`);
|
|
19623
|
+
}
|
|
19624
|
+
}
|
|
19625
|
+
if (commitRows.length > 0) {
|
|
19626
|
+
console.log(chalk.bold(`
|
|
19627
|
+
Commit Links (${commitRows.length}):`));
|
|
19628
|
+
for (const c of commitRows) {
|
|
19629
|
+
const sid = c.short_id || c.task_id.slice(0, 8);
|
|
19630
|
+
console.log(` ${chalk.yellow(c.sha?.slice(0, 7) || "?")} ${chalk.cyan(sid)} ${c.title || ""} \u2014 ${chalk.dim(c.author || "")} ${chalk.dim(c.committed_at || "")}`);
|
|
19631
|
+
}
|
|
19632
|
+
}
|
|
19633
|
+
if (taskFiles.length === 0 && commitRows.length === 0) {
|
|
19634
|
+
console.log(chalk.dim("No task or commit links found for this file."));
|
|
19635
|
+
console.log(chalk.dim("Use 'todos hook install' to auto-link future commits."));
|
|
19636
|
+
}
|
|
19637
|
+
console.log();
|
|
19638
|
+
});
|
|
19639
|
+
program2.command("dashboard").description("Live-updating dashboard showing project health, agents, task flow").option("--project <id>", "Filter to project").option("--refresh <ms>", "Refresh interval in ms (default: 2000)", "2000").action(async (opts) => {
|
|
19640
|
+
const { render: render2 } = await import("ink");
|
|
19641
|
+
const React = await import("react");
|
|
19642
|
+
const { Dashboard: Dashboard2 } = await Promise.resolve().then(() => (init_Dashboard(), exports_Dashboard));
|
|
19643
|
+
const globalOpts = program2.opts();
|
|
19644
|
+
const projectId = opts.project || autoProject(globalOpts) || undefined;
|
|
19645
|
+
render2(React.createElement(Dashboard2, { projectId, refreshMs: parseInt(opts.refresh, 10) }));
|
|
19646
|
+
});
|
|
18191
19647
|
program2.command("next").description("Show the best pending task to work on next").option("--agent <id>", "Prefer tasks assigned to this agent").option("--project <id>", "Filter to project").option("--json", "Output as JSON").action(async (opts) => {
|
|
18192
19648
|
const db = getDatabase();
|
|
18193
19649
|
const filters = {};
|
|
@@ -18253,6 +19709,120 @@ Next up:`));
|
|
|
18253
19709
|
console.log(` ${chalk.cyan(t.short_id || t.id.slice(0, 8))} ${chalk.yellow(t.priority)} ${t.title}`);
|
|
18254
19710
|
}
|
|
18255
19711
|
});
|
|
19712
|
+
program2.command("recap").description("Show what happened in the last N hours \u2014 completed tasks, new tasks, agent activity, blockers").option("--hours <n>", "Look back N hours (default: 8)", "8").option("--project <id>", "Filter to project").action((opts) => {
|
|
19713
|
+
const globalOpts = program2.opts();
|
|
19714
|
+
const { getRecap: getRecap2 } = (init_audit(), __toCommonJS(exports_audit));
|
|
19715
|
+
const recap = getRecap2(parseInt(opts.hours, 10), opts.project);
|
|
19716
|
+
if (globalOpts.json) {
|
|
19717
|
+
output(recap, true);
|
|
19718
|
+
return;
|
|
19719
|
+
}
|
|
19720
|
+
console.log(chalk.bold(`
|
|
19721
|
+
Recap \u2014 last ${recap.hours} hours (since ${new Date(recap.since).toLocaleString()})
|
|
19722
|
+
`));
|
|
19723
|
+
if (recap.completed.length > 0) {
|
|
19724
|
+
console.log(chalk.green.bold(`Completed (${recap.completed.length}):`));
|
|
19725
|
+
for (const t of recap.completed) {
|
|
19726
|
+
const id = t.short_id || t.id.slice(0, 8);
|
|
19727
|
+
const dur = t.duration_minutes != null ? ` (${t.duration_minutes}m)` : "";
|
|
19728
|
+
console.log(` ${chalk.green("\u2713")} ${chalk.cyan(id)} ${t.title}${dur}${t.assigned_to ? ` \u2014 ${chalk.dim(t.assigned_to)}` : ""}`);
|
|
19729
|
+
}
|
|
19730
|
+
} else {
|
|
19731
|
+
console.log(chalk.dim("No tasks completed in this period."));
|
|
19732
|
+
}
|
|
19733
|
+
if (recap.in_progress.length > 0) {
|
|
19734
|
+
console.log(chalk.blue.bold(`
|
|
19735
|
+
In Progress (${recap.in_progress.length}):`));
|
|
19736
|
+
for (const t of recap.in_progress) {
|
|
19737
|
+
const id = t.short_id || t.id.slice(0, 8);
|
|
19738
|
+
console.log(` ${chalk.blue("\u2192")} ${chalk.cyan(id)} ${t.title}${t.assigned_to ? ` \u2014 ${chalk.dim(t.assigned_to)}` : ""}`);
|
|
19739
|
+
}
|
|
19740
|
+
}
|
|
19741
|
+
if (recap.blocked.length > 0) {
|
|
19742
|
+
console.log(chalk.red.bold(`
|
|
19743
|
+
Blocked (${recap.blocked.length}):`));
|
|
19744
|
+
for (const t of recap.blocked) {
|
|
19745
|
+
const id = t.short_id || t.id.slice(0, 8);
|
|
19746
|
+
console.log(` ${chalk.red("\u2717")} ${chalk.cyan(id)} ${t.title}`);
|
|
19747
|
+
}
|
|
19748
|
+
}
|
|
19749
|
+
if (recap.stale.length > 0) {
|
|
19750
|
+
console.log(chalk.yellow.bold(`
|
|
19751
|
+
Stale (${recap.stale.length}):`));
|
|
19752
|
+
for (const t of recap.stale) {
|
|
19753
|
+
const id = t.short_id || t.id.slice(0, 8);
|
|
19754
|
+
const ago = Math.round((Date.now() - new Date(t.updated_at).getTime()) / 60000);
|
|
19755
|
+
console.log(` ${chalk.yellow("!")} ${chalk.cyan(id)} ${t.title} \u2014 last update ${ago}m ago`);
|
|
19756
|
+
}
|
|
19757
|
+
}
|
|
19758
|
+
if (recap.created.length > 0) {
|
|
19759
|
+
console.log(chalk.dim.bold(`
|
|
19760
|
+
Created (${recap.created.length}):`));
|
|
19761
|
+
for (const t of recap.created.slice(0, 10)) {
|
|
19762
|
+
const id = t.short_id || t.id.slice(0, 8);
|
|
19763
|
+
console.log(` ${chalk.dim("+")} ${chalk.cyan(id)} ${t.title}`);
|
|
19764
|
+
}
|
|
19765
|
+
if (recap.created.length > 10)
|
|
19766
|
+
console.log(chalk.dim(` ... and ${recap.created.length - 10} more`));
|
|
19767
|
+
}
|
|
19768
|
+
if (recap.agents.length > 0) {
|
|
19769
|
+
console.log(chalk.bold(`
|
|
19770
|
+
Agents:`));
|
|
19771
|
+
for (const a of recap.agents) {
|
|
19772
|
+
const seen = Math.round((Date.now() - new Date(a.last_seen_at).getTime()) / 60000);
|
|
19773
|
+
console.log(` ${a.name}: ${chalk.green(a.completed_count + " done")} | ${chalk.blue(a.in_progress_count + " active")} | last seen ${seen}m ago`);
|
|
19774
|
+
}
|
|
19775
|
+
}
|
|
19776
|
+
console.log();
|
|
19777
|
+
});
|
|
19778
|
+
program2.command("standup").description("Generate standup notes \u2014 completed since yesterday, in progress, blocked. Grouped by agent.").option("--since <date>", "ISO date or 'yesterday' (default: yesterday)").option("--project <id>", "Filter to project").action((opts) => {
|
|
19779
|
+
const globalOpts = program2.opts();
|
|
19780
|
+
const sinceDate = opts.since === "yesterday" || !opts.since ? new Date(Date.now() - 24 * 60 * 60 * 1000) : new Date(opts.since);
|
|
19781
|
+
const hours = Math.max(1, Math.round((Date.now() - sinceDate.getTime()) / (60 * 60 * 1000)));
|
|
19782
|
+
const { getRecap: getRecap2 } = (init_audit(), __toCommonJS(exports_audit));
|
|
19783
|
+
const recap = getRecap2(hours, opts.project);
|
|
19784
|
+
if (globalOpts.json) {
|
|
19785
|
+
output(recap, true);
|
|
19786
|
+
return;
|
|
19787
|
+
}
|
|
19788
|
+
console.log(chalk.bold(`
|
|
19789
|
+
Standup \u2014 since ${sinceDate.toLocaleDateString()}
|
|
19790
|
+
`));
|
|
19791
|
+
const byAgent = new Map;
|
|
19792
|
+
for (const t of recap.completed) {
|
|
19793
|
+
const agent = t.assigned_to || "unassigned";
|
|
19794
|
+
if (!byAgent.has(agent))
|
|
19795
|
+
byAgent.set(agent, []);
|
|
19796
|
+
byAgent.get(agent).push(t);
|
|
19797
|
+
}
|
|
19798
|
+
if (byAgent.size > 0) {
|
|
19799
|
+
console.log(chalk.green.bold("Done:"));
|
|
19800
|
+
for (const [agent, tasks] of byAgent) {
|
|
19801
|
+
console.log(` ${chalk.cyan(agent)}:`);
|
|
19802
|
+
for (const t of tasks) {
|
|
19803
|
+
const dur = t.duration_minutes != null ? ` (${t.duration_minutes}m)` : "";
|
|
19804
|
+
console.log(` ${chalk.green("\u2713")} ${t.short_id || t.id.slice(0, 8)} ${t.title}${dur}`);
|
|
19805
|
+
}
|
|
19806
|
+
}
|
|
19807
|
+
} else {
|
|
19808
|
+
console.log(chalk.dim("Nothing completed."));
|
|
19809
|
+
}
|
|
19810
|
+
if (recap.in_progress.length > 0) {
|
|
19811
|
+
console.log(chalk.blue.bold(`
|
|
19812
|
+
In Progress:`));
|
|
19813
|
+
for (const t of recap.in_progress) {
|
|
19814
|
+
console.log(` ${chalk.blue("\u2192")} ${t.short_id || t.id.slice(0, 8)} ${t.title}${t.assigned_to ? ` \u2014 ${chalk.dim(t.assigned_to)}` : ""}`);
|
|
19815
|
+
}
|
|
19816
|
+
}
|
|
19817
|
+
if (recap.blocked.length > 0) {
|
|
19818
|
+
console.log(chalk.red.bold(`
|
|
19819
|
+
Blocked:`));
|
|
19820
|
+
for (const t of recap.blocked) {
|
|
19821
|
+
console.log(` ${chalk.red("\u2717")} ${t.short_id || t.id.slice(0, 8)} ${t.title}`);
|
|
19822
|
+
}
|
|
19823
|
+
}
|
|
19824
|
+
console.log();
|
|
19825
|
+
});
|
|
18256
19826
|
program2.command("fail <id>").description("Mark a task as failed with optional reason and retry").option("--reason <text>", "Why it failed").option("--agent <id>", "Agent reporting the failure").option("--retry", "Auto-create a retry copy").option("--json", "Output as JSON").action(async (id, opts) => {
|
|
18257
19827
|
const db = getDatabase();
|
|
18258
19828
|
const resolvedId = resolvePartialId(db, "tasks", id);
|
|
@@ -18541,11 +20111,11 @@ program2.command("health").description("Check todos system health \u2014 databas
|
|
|
18541
20111
|
try {
|
|
18542
20112
|
const db = getDatabase();
|
|
18543
20113
|
const row = db.query("SELECT COUNT(*) as count FROM tasks").get();
|
|
18544
|
-
const { statSync:
|
|
20114
|
+
const { statSync: statSync3 } = __require("fs");
|
|
18545
20115
|
const dbPath = process.env["TODOS_DB_PATH"] || __require("path").join(process.env["HOME"] || "~", ".todos", "todos.db");
|
|
18546
20116
|
let size = "unknown";
|
|
18547
20117
|
try {
|
|
18548
|
-
size = `${(
|
|
20118
|
+
size = `${(statSync3(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
|
|
18549
20119
|
} catch {}
|
|
18550
20120
|
checks.push({ name: "Database", ok: true, message: `${row.count} tasks \xB7 ${size} \xB7 ${chalk.dim(dbPath)}` });
|
|
18551
20121
|
} catch (e) {
|