@hasna/todos 0.9.73 → 0.9.75
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/index.js +988 -18
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/handoffs.d.ts +25 -0
- package/dist/db/handoffs.d.ts.map +1 -0
- package/dist/db/projects.d.ts +5 -1
- package/dist/db/projects.d.ts.map +1 -1
- package/dist/db/tasks.d.ts +10 -0
- package/dist/db/tasks.d.ts.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +214 -9
- package/dist/mcp/index.js +385 -13
- package/dist/server/index.js +151 -25
- package/dist/types/index.d.ts +39 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2079,6 +2079,19 @@ var require_commander = __commonJS((exports) => {
|
|
|
2079
2079
|
});
|
|
2080
2080
|
|
|
2081
2081
|
// src/db/database.ts
|
|
2082
|
+
var exports_database = {};
|
|
2083
|
+
__export(exports_database, {
|
|
2084
|
+
uuid: () => uuid,
|
|
2085
|
+
resolvePartialId: () => resolvePartialId,
|
|
2086
|
+
resetDatabase: () => resetDatabase,
|
|
2087
|
+
now: () => now,
|
|
2088
|
+
lockExpiryCutoff: () => lockExpiryCutoff,
|
|
2089
|
+
isLockExpired: () => isLockExpired,
|
|
2090
|
+
getDatabase: () => getDatabase,
|
|
2091
|
+
closeDatabase: () => closeDatabase,
|
|
2092
|
+
clearExpiredLocks: () => clearExpiredLocks,
|
|
2093
|
+
LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
|
|
2094
|
+
});
|
|
2082
2095
|
import { Database } from "bun:sqlite";
|
|
2083
2096
|
import { existsSync, mkdirSync } from "fs";
|
|
2084
2097
|
import { dirname, join, resolve } from "path";
|
|
@@ -2247,6 +2260,18 @@ function ensureSchema(db) {
|
|
|
2247
2260
|
metadata TEXT DEFAULT '{}',
|
|
2248
2261
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2249
2262
|
)`);
|
|
2263
|
+
ensureTable("project_sources", `
|
|
2264
|
+
CREATE TABLE project_sources (
|
|
2265
|
+
id TEXT PRIMARY KEY,
|
|
2266
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
2267
|
+
type TEXT NOT NULL,
|
|
2268
|
+
name TEXT NOT NULL,
|
|
2269
|
+
uri TEXT NOT NULL,
|
|
2270
|
+
description TEXT,
|
|
2271
|
+
metadata TEXT DEFAULT '{}',
|
|
2272
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2273
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2274
|
+
)`);
|
|
2250
2275
|
ensureColumn("projects", "task_list_id", "TEXT");
|
|
2251
2276
|
ensureColumn("projects", "task_prefix", "TEXT");
|
|
2252
2277
|
ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
|
|
@@ -2260,6 +2285,9 @@ function ensureSchema(db) {
|
|
|
2260
2285
|
ensureColumn("tasks", "approved_at", "TEXT");
|
|
2261
2286
|
ensureColumn("tasks", "recurrence_rule", "TEXT");
|
|
2262
2287
|
ensureColumn("tasks", "recurrence_parent_id", "TEXT REFERENCES tasks(id) ON DELETE SET NULL");
|
|
2288
|
+
ensureColumn("tasks", "confidence", "REAL");
|
|
2289
|
+
ensureColumn("tasks", "reason", "TEXT");
|
|
2290
|
+
ensureColumn("tasks", "spawned_from_session", "TEXT");
|
|
2263
2291
|
ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
|
|
2264
2292
|
ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
|
|
2265
2293
|
ensureColumn("agents", "reports_to", "TEXT");
|
|
@@ -2288,6 +2316,8 @@ function ensureSchema(db) {
|
|
|
2288
2316
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_agent ON task_history(agent_id)");
|
|
2289
2317
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_parent ON tasks(recurrence_parent_id)");
|
|
2290
2318
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_rule ON tasks(recurrence_rule) WHERE recurrence_rule IS NOT NULL");
|
|
2319
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_project ON project_sources(project_id)");
|
|
2320
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_type ON project_sources(type)");
|
|
2291
2321
|
}
|
|
2292
2322
|
function backfillTaskTags(db) {
|
|
2293
2323
|
try {
|
|
@@ -2318,6 +2348,15 @@ function backfillTaskTags(db) {
|
|
|
2318
2348
|
}
|
|
2319
2349
|
} catch {}
|
|
2320
2350
|
}
|
|
2351
|
+
function closeDatabase() {
|
|
2352
|
+
if (_db) {
|
|
2353
|
+
_db.close();
|
|
2354
|
+
_db = null;
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
function resetDatabase() {
|
|
2358
|
+
_db = null;
|
|
2359
|
+
}
|
|
2321
2360
|
function now() {
|
|
2322
2361
|
return new Date().toISOString();
|
|
2323
2362
|
}
|
|
@@ -2656,6 +2695,45 @@ var init_database = __esm(() => {
|
|
|
2656
2695
|
ALTER TABLE agents ADD COLUMN session_id TEXT;
|
|
2657
2696
|
ALTER TABLE agents ADD COLUMN working_dir TEXT;
|
|
2658
2697
|
INSERT OR IGNORE INTO _migrations (id) VALUES (17);
|
|
2698
|
+
`,
|
|
2699
|
+
`
|
|
2700
|
+
ALTER TABLE tasks ADD COLUMN confidence REAL;
|
|
2701
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (18);
|
|
2702
|
+
`,
|
|
2703
|
+
`
|
|
2704
|
+
ALTER TABLE tasks ADD COLUMN reason TEXT;
|
|
2705
|
+
ALTER TABLE tasks ADD COLUMN spawned_from_session TEXT;
|
|
2706
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (19);
|
|
2707
|
+
`,
|
|
2708
|
+
`
|
|
2709
|
+
CREATE TABLE IF NOT EXISTS handoffs (
|
|
2710
|
+
id TEXT PRIMARY KEY,
|
|
2711
|
+
agent_id TEXT,
|
|
2712
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
2713
|
+
summary TEXT NOT NULL,
|
|
2714
|
+
completed TEXT,
|
|
2715
|
+
in_progress TEXT,
|
|
2716
|
+
blockers TEXT,
|
|
2717
|
+
next_steps TEXT,
|
|
2718
|
+
created_at TEXT NOT NULL
|
|
2719
|
+
);
|
|
2720
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (20);
|
|
2721
|
+
`,
|
|
2722
|
+
`
|
|
2723
|
+
CREATE TABLE IF NOT EXISTS project_sources (
|
|
2724
|
+
id TEXT PRIMARY KEY,
|
|
2725
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
2726
|
+
type TEXT NOT NULL,
|
|
2727
|
+
name TEXT NOT NULL,
|
|
2728
|
+
uri TEXT NOT NULL,
|
|
2729
|
+
description TEXT,
|
|
2730
|
+
metadata TEXT DEFAULT '{}',
|
|
2731
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2732
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2733
|
+
);
|
|
2734
|
+
CREATE INDEX IF NOT EXISTS idx_project_sources_project ON project_sources(project_id);
|
|
2735
|
+
CREATE INDEX IF NOT EXISTS idx_project_sources_type ON project_sources(type);
|
|
2736
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (21);
|
|
2659
2737
|
`
|
|
2660
2738
|
];
|
|
2661
2739
|
});
|
|
@@ -2770,13 +2848,17 @@ var exports_projects = {};
|
|
|
2770
2848
|
__export(exports_projects, {
|
|
2771
2849
|
updateProject: () => updateProject,
|
|
2772
2850
|
slugify: () => slugify,
|
|
2851
|
+
removeProjectSource: () => removeProjectSource,
|
|
2773
2852
|
nextTaskShortId: () => nextTaskShortId,
|
|
2774
2853
|
listProjects: () => listProjects,
|
|
2854
|
+
listProjectSources: () => listProjectSources,
|
|
2855
|
+
getProjectWithSources: () => getProjectWithSources,
|
|
2775
2856
|
getProjectByPath: () => getProjectByPath,
|
|
2776
2857
|
getProject: () => getProject,
|
|
2777
2858
|
ensureProject: () => ensureProject,
|
|
2778
2859
|
deleteProject: () => deleteProject,
|
|
2779
|
-
createProject: () => createProject
|
|
2860
|
+
createProject: () => createProject,
|
|
2861
|
+
addProjectSource: () => addProjectSource
|
|
2780
2862
|
});
|
|
2781
2863
|
function slugify(name) {
|
|
2782
2864
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -2853,6 +2935,48 @@ function deleteProject(id, db) {
|
|
|
2853
2935
|
const result = d.run("DELETE FROM projects WHERE id = ?", [id]);
|
|
2854
2936
|
return result.changes > 0;
|
|
2855
2937
|
}
|
|
2938
|
+
function rowToSource(row) {
|
|
2939
|
+
return {
|
|
2940
|
+
...row,
|
|
2941
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : {}
|
|
2942
|
+
};
|
|
2943
|
+
}
|
|
2944
|
+
function addProjectSource(input, db) {
|
|
2945
|
+
const d = db || getDatabase();
|
|
2946
|
+
const id = uuid();
|
|
2947
|
+
const timestamp = now();
|
|
2948
|
+
d.run(`INSERT INTO project_sources (id, project_id, type, name, uri, description, metadata, created_at, updated_at)
|
|
2949
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
2950
|
+
id,
|
|
2951
|
+
input.project_id,
|
|
2952
|
+
input.type,
|
|
2953
|
+
input.name,
|
|
2954
|
+
input.uri,
|
|
2955
|
+
input.description || null,
|
|
2956
|
+
JSON.stringify(input.metadata || {}),
|
|
2957
|
+
timestamp,
|
|
2958
|
+
timestamp
|
|
2959
|
+
]);
|
|
2960
|
+
return rowToSource(d.query("SELECT * FROM project_sources WHERE id = ?").get(id));
|
|
2961
|
+
}
|
|
2962
|
+
function removeProjectSource(id, db) {
|
|
2963
|
+
const d = db || getDatabase();
|
|
2964
|
+
const result = d.run("DELETE FROM project_sources WHERE id = ?", [id]);
|
|
2965
|
+
return result.changes > 0;
|
|
2966
|
+
}
|
|
2967
|
+
function listProjectSources(projectId, db) {
|
|
2968
|
+
const d = db || getDatabase();
|
|
2969
|
+
const rows = d.query("SELECT * FROM project_sources WHERE project_id = ? ORDER BY name").all(projectId);
|
|
2970
|
+
return rows.map(rowToSource);
|
|
2971
|
+
}
|
|
2972
|
+
function getProjectWithSources(id, db) {
|
|
2973
|
+
const d = db || getDatabase();
|
|
2974
|
+
const project = getProject(id, d);
|
|
2975
|
+
if (!project)
|
|
2976
|
+
return null;
|
|
2977
|
+
project.sources = listProjectSources(id, d);
|
|
2978
|
+
return project;
|
|
2979
|
+
}
|
|
2856
2980
|
function nextTaskShortId(projectId, db) {
|
|
2857
2981
|
const d = db || getDatabase();
|
|
2858
2982
|
const project = getProject(projectId, d);
|
|
@@ -3317,6 +3441,7 @@ __export(exports_tasks, {
|
|
|
3317
3441
|
setTaskStatus: () => setTaskStatus,
|
|
3318
3442
|
setTaskPriority: () => setTaskPriority,
|
|
3319
3443
|
removeDependency: () => removeDependency,
|
|
3444
|
+
redistributeStaleTasks: () => redistributeStaleTasks,
|
|
3320
3445
|
moveTask: () => moveTask,
|
|
3321
3446
|
lockTask: () => lockTask,
|
|
3322
3447
|
listTasks: () => listTasks,
|
|
@@ -3329,6 +3454,7 @@ __export(exports_tasks, {
|
|
|
3329
3454
|
getTask: () => getTask,
|
|
3330
3455
|
getStatus: () => getStatus,
|
|
3331
3456
|
getStaleTasks: () => getStaleTasks,
|
|
3457
|
+
getOverdueTasks: () => getOverdueTasks,
|
|
3332
3458
|
getNextTask: () => getNextTask,
|
|
3333
3459
|
getBlockingDeps: () => getBlockingDeps,
|
|
3334
3460
|
getActiveWork: () => getActiveWork,
|
|
@@ -3374,8 +3500,8 @@ function createTask(input, db) {
|
|
|
3374
3500
|
const tags = input.tags || [];
|
|
3375
3501
|
const shortId = input.project_id ? nextTaskShortId(input.project_id, d) : null;
|
|
3376
3502
|
const title = shortId ? `${shortId}: ${input.title}` : input.title;
|
|
3377
|
-
d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id)
|
|
3378
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3503
|
+
d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session)
|
|
3504
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3379
3505
|
id,
|
|
3380
3506
|
shortId,
|
|
3381
3507
|
input.project_id || null,
|
|
@@ -3401,7 +3527,9 @@ function createTask(input, db) {
|
|
|
3401
3527
|
null,
|
|
3402
3528
|
input.recurrence_rule || null,
|
|
3403
3529
|
input.recurrence_parent_id || null,
|
|
3404
|
-
input.spawns_template_id || null
|
|
3530
|
+
input.spawns_template_id || null,
|
|
3531
|
+
input.reason || null,
|
|
3532
|
+
input.spawned_from_session || null
|
|
3405
3533
|
]);
|
|
3406
3534
|
if (tags.length > 0) {
|
|
3407
3535
|
insertTaskTags(id, tags, d);
|
|
@@ -3758,13 +3886,21 @@ function completeTask(id, agentId, db, options) {
|
|
|
3758
3886
|
checkCompletionGuard(task, agentId || null, d);
|
|
3759
3887
|
const evidence = options ? { files_changed: options.files_changed, test_results: options.test_results, commit_hash: options.commit_hash, notes: options.notes, attachment_ids: options.attachment_ids } : undefined;
|
|
3760
3888
|
const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes || evidence.attachment_ids);
|
|
3761
|
-
|
|
3762
|
-
|
|
3889
|
+
const completionMeta = {};
|
|
3890
|
+
if (hasEvidence)
|
|
3891
|
+
completionMeta._evidence = evidence;
|
|
3892
|
+
if (options?.confidence !== undefined) {
|
|
3893
|
+
completionMeta._completion = { confidence: options.confidence };
|
|
3894
|
+
}
|
|
3895
|
+
const hasMeta = Object.keys(completionMeta).length > 0;
|
|
3896
|
+
if (hasMeta) {
|
|
3897
|
+
const meta2 = { ...task.metadata, ...completionMeta };
|
|
3763
3898
|
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta2), id]);
|
|
3764
3899
|
}
|
|
3765
3900
|
const timestamp = now();
|
|
3766
|
-
|
|
3767
|
-
|
|
3901
|
+
const confidence = options?.confidence !== undefined ? options.confidence : null;
|
|
3902
|
+
d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
|
|
3903
|
+
WHERE id = ?`, [timestamp, confidence, timestamp, id]);
|
|
3768
3904
|
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
3769
3905
|
dispatchWebhook("task.completed", { id, agent_id: agentId, title: task.title, completed_at: timestamp }, d).catch(() => {});
|
|
3770
3906
|
let spawnedTask = null;
|
|
@@ -3783,14 +3919,14 @@ function completeTask(id, agentId, db, options) {
|
|
|
3783
3919
|
spawnedFromTemplate = createTask(input, d);
|
|
3784
3920
|
} catch {}
|
|
3785
3921
|
}
|
|
3786
|
-
const meta =
|
|
3922
|
+
const meta = hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata;
|
|
3787
3923
|
if (spawnedTask) {
|
|
3788
3924
|
meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
|
|
3789
3925
|
}
|
|
3790
3926
|
if (spawnedFromTemplate) {
|
|
3791
3927
|
meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
|
|
3792
3928
|
}
|
|
3793
|
-
return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, version: task.version + 1, updated_at: timestamp, metadata: meta };
|
|
3929
|
+
return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
|
|
3794
3930
|
}
|
|
3795
3931
|
function lockTask(id, agentId, db) {
|
|
3796
3932
|
const d = db || getDatabase();
|
|
@@ -4241,6 +4377,20 @@ function setTaskPriority(id, priority, _agentId, db) {
|
|
|
4241
4377
|
}
|
|
4242
4378
|
throw new Error(`Failed to set priority after 3 attempts`);
|
|
4243
4379
|
}
|
|
4380
|
+
function redistributeStaleTasks(agentId, options, db) {
|
|
4381
|
+
const d = db || getDatabase();
|
|
4382
|
+
const maxAge = options?.max_age_minutes ?? 60;
|
|
4383
|
+
const stale = getStaleTasks(maxAge, options?.project_id ? { project_id: options.project_id } : undefined, d);
|
|
4384
|
+
const limited = options?.limit ? stale.slice(0, options.limit) : stale;
|
|
4385
|
+
const timestamp = now();
|
|
4386
|
+
const released = [];
|
|
4387
|
+
for (const t of limited) {
|
|
4388
|
+
d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, status = 'pending', version = version + 1, updated_at = ? WHERE id = ?`, [timestamp, t.id]);
|
|
4389
|
+
released.push({ ...t, locked_by: null, locked_at: null, status: "pending" });
|
|
4390
|
+
}
|
|
4391
|
+
const claimed = released.length > 0 ? claimNextTask(agentId, options?.project_id ? { project_id: options.project_id } : undefined, d) : null;
|
|
4392
|
+
return { released, claimed };
|
|
4393
|
+
}
|
|
4244
4394
|
function wouldCreateCycle(taskId, dependsOn, db) {
|
|
4245
4395
|
const visited = new Set;
|
|
4246
4396
|
const queue = [dependsOn];
|
|
@@ -4343,6 +4493,19 @@ function bulkUpdateTasks(taskIds, updates, db) {
|
|
|
4343
4493
|
tx();
|
|
4344
4494
|
return { updated, failed };
|
|
4345
4495
|
}
|
|
4496
|
+
function getOverdueTasks(projectId, db) {
|
|
4497
|
+
const d = db || getDatabase();
|
|
4498
|
+
const nowStr = new Date().toISOString();
|
|
4499
|
+
let query = `SELECT * FROM tasks WHERE due_at IS NOT NULL AND due_at < ? AND status NOT IN ('completed', 'cancelled', 'failed')`;
|
|
4500
|
+
const params = [nowStr];
|
|
4501
|
+
if (projectId) {
|
|
4502
|
+
query += ` AND project_id = ?`;
|
|
4503
|
+
params.push(projectId);
|
|
4504
|
+
}
|
|
4505
|
+
query += ` ORDER BY due_at ASC`;
|
|
4506
|
+
const rows = d.query(query).all(...params);
|
|
4507
|
+
return rows.map(rowToTask);
|
|
4508
|
+
}
|
|
4346
4509
|
var init_tasks = __esm(() => {
|
|
4347
4510
|
init_types();
|
|
4348
4511
|
init_database();
|
|
@@ -9331,6 +9494,77 @@ var init_zod = __esm(() => {
|
|
|
9331
9494
|
init_external();
|
|
9332
9495
|
});
|
|
9333
9496
|
|
|
9497
|
+
// src/db/handoffs.ts
|
|
9498
|
+
var exports_handoffs = {};
|
|
9499
|
+
__export(exports_handoffs, {
|
|
9500
|
+
listHandoffs: () => listHandoffs,
|
|
9501
|
+
getLatestHandoff: () => getLatestHandoff,
|
|
9502
|
+
createHandoff: () => createHandoff
|
|
9503
|
+
});
|
|
9504
|
+
function createHandoff(input, db) {
|
|
9505
|
+
const d = db || getDatabase();
|
|
9506
|
+
const id = uuid();
|
|
9507
|
+
const timestamp = now();
|
|
9508
|
+
d.run(`INSERT INTO handoffs (id, agent_id, project_id, summary, completed, in_progress, blockers, next_steps, created_at)
|
|
9509
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
9510
|
+
id,
|
|
9511
|
+
input.agent_id || null,
|
|
9512
|
+
input.project_id || null,
|
|
9513
|
+
input.summary,
|
|
9514
|
+
input.completed ? JSON.stringify(input.completed) : null,
|
|
9515
|
+
input.in_progress ? JSON.stringify(input.in_progress) : null,
|
|
9516
|
+
input.blockers ? JSON.stringify(input.blockers) : null,
|
|
9517
|
+
input.next_steps ? JSON.stringify(input.next_steps) : null,
|
|
9518
|
+
timestamp
|
|
9519
|
+
]);
|
|
9520
|
+
return {
|
|
9521
|
+
id,
|
|
9522
|
+
agent_id: input.agent_id || null,
|
|
9523
|
+
project_id: input.project_id || null,
|
|
9524
|
+
summary: input.summary,
|
|
9525
|
+
completed: input.completed || null,
|
|
9526
|
+
in_progress: input.in_progress || null,
|
|
9527
|
+
blockers: input.blockers || null,
|
|
9528
|
+
next_steps: input.next_steps || null,
|
|
9529
|
+
created_at: timestamp
|
|
9530
|
+
};
|
|
9531
|
+
}
|
|
9532
|
+
function rowToHandoff(row) {
|
|
9533
|
+
return {
|
|
9534
|
+
...row,
|
|
9535
|
+
completed: row.completed ? JSON.parse(row.completed) : null,
|
|
9536
|
+
in_progress: row.in_progress ? JSON.parse(row.in_progress) : null,
|
|
9537
|
+
blockers: row.blockers ? JSON.parse(row.blockers) : null,
|
|
9538
|
+
next_steps: row.next_steps ? JSON.parse(row.next_steps) : null
|
|
9539
|
+
};
|
|
9540
|
+
}
|
|
9541
|
+
function listHandoffs(projectId, limit = 10, db) {
|
|
9542
|
+
const d = db || getDatabase();
|
|
9543
|
+
if (projectId) {
|
|
9544
|
+
return d.query("SELECT * FROM handoffs WHERE project_id = ? ORDER BY rowid DESC LIMIT ?").all(projectId, limit).map(rowToHandoff);
|
|
9545
|
+
}
|
|
9546
|
+
return d.query("SELECT * FROM handoffs ORDER BY rowid DESC LIMIT ?").all(limit).map(rowToHandoff);
|
|
9547
|
+
}
|
|
9548
|
+
function getLatestHandoff(agentId, projectId, db) {
|
|
9549
|
+
const d = db || getDatabase();
|
|
9550
|
+
let query = "SELECT * FROM handoffs WHERE 1=1";
|
|
9551
|
+
const params = [];
|
|
9552
|
+
if (agentId) {
|
|
9553
|
+
query += " AND agent_id = ?";
|
|
9554
|
+
params.push(agentId);
|
|
9555
|
+
}
|
|
9556
|
+
if (projectId) {
|
|
9557
|
+
query += " AND project_id = ?";
|
|
9558
|
+
params.push(projectId);
|
|
9559
|
+
}
|
|
9560
|
+
query += " ORDER BY rowid DESC LIMIT 1";
|
|
9561
|
+
const row = d.query(query).get(...params);
|
|
9562
|
+
return row ? rowToHandoff(row) : null;
|
|
9563
|
+
}
|
|
9564
|
+
var init_handoffs = __esm(() => {
|
|
9565
|
+
init_database();
|
|
9566
|
+
});
|
|
9567
|
+
|
|
9334
9568
|
// src/mcp/index.ts
|
|
9335
9569
|
var exports_mcp = {};
|
|
9336
9570
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -9517,7 +9751,9 @@ var init_mcp = __esm(() => {
|
|
|
9517
9751
|
estimated_minutes: exports_external.number().optional(),
|
|
9518
9752
|
requires_approval: exports_external.boolean().optional(),
|
|
9519
9753
|
recurrence_rule: exports_external.string().optional(),
|
|
9520
|
-
spawns_template_id: exports_external.string().optional().describe("Template ID to auto-create as next task when this task is completed (pipeline/handoff chains)")
|
|
9754
|
+
spawns_template_id: exports_external.string().optional().describe("Template ID to auto-create as next task when this task is completed (pipeline/handoff chains)"),
|
|
9755
|
+
reason: exports_external.string().optional().describe("Why this task exists \u2014 context for agents picking it up"),
|
|
9756
|
+
spawned_from_session: exports_external.string().optional().describe("Session ID that created this task (for tracing task lineage)")
|
|
9521
9757
|
}, async (params) => {
|
|
9522
9758
|
try {
|
|
9523
9759
|
const resolved = { ...params };
|
|
@@ -9732,12 +9968,13 @@ Parent: ${task.parent.id.slice(0, 8)} | ${task.parent.title}`);
|
|
|
9732
9968
|
test_results: exports_external.string().optional().describe("Summary of test results"),
|
|
9733
9969
|
commit_hash: exports_external.string().optional().describe("Git commit hash associated with this completion"),
|
|
9734
9970
|
notes: exports_external.string().optional().describe("Notes about the completion"),
|
|
9735
|
-
attachment_ids: exports_external.array(exports_external.string()).optional().describe("IDs of attachments uploaded via @hasna/attachments to link as evidence")
|
|
9736
|
-
|
|
9971
|
+
attachment_ids: exports_external.array(exports_external.string()).optional().describe("IDs of attachments uploaded via @hasna/attachments to link as evidence"),
|
|
9972
|
+
confidence: exports_external.number().min(0).max(1).optional().describe("Agent's confidence 0.0-1.0 that the task is fully complete. Default: 1.0. Low confidence (<0.7) is flagged as a signal for review.")
|
|
9973
|
+
}, async ({ id, agent_id, skip_recurrence, files_changed, test_results, commit_hash, notes, attachment_ids, confidence }) => {
|
|
9737
9974
|
try {
|
|
9738
9975
|
const resolvedId = resolveId(id);
|
|
9739
9976
|
const evidence = files_changed || test_results || commit_hash || notes || attachment_ids ? { files_changed, test_results, commit_hash, notes, attachment_ids } : undefined;
|
|
9740
|
-
const task = completeTask(resolvedId, agent_id, undefined, { skip_recurrence, ...evidence });
|
|
9977
|
+
const task = completeTask(resolvedId, agent_id, undefined, { skip_recurrence, confidence, ...evidence });
|
|
9741
9978
|
let text = `completed: ${formatTask(task)}`;
|
|
9742
9979
|
if (task.metadata._next_recurrence) {
|
|
9743
9980
|
const next = task.metadata._next_recurrence;
|
|
@@ -9889,6 +10126,65 @@ ${text}` }] };
|
|
|
9889
10126
|
}
|
|
9890
10127
|
});
|
|
9891
10128
|
}
|
|
10129
|
+
if (shouldRegisterTool("add_project_source")) {
|
|
10130
|
+
server.tool("add_project_source", "Add a data source to a project (S3 bucket, Google Drive folder, local path, GitHub repo, Notion page, etc.). Sources are revealed to agents when they load the project.", {
|
|
10131
|
+
project_id: exports_external.string().describe("Project ID"),
|
|
10132
|
+
type: exports_external.string().describe("Source type: 's3', 'gdrive', 'local', 'github', 'notion', 'http', or any custom label"),
|
|
10133
|
+
name: exports_external.string().describe("Human-readable label for this source"),
|
|
10134
|
+
uri: exports_external.string().describe("The source URI (bucket path, folder URL, local path, repo URL, etc.)"),
|
|
10135
|
+
description: exports_external.string().optional().describe("What this source contains or how agents should use it"),
|
|
10136
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Extra config (e.g. region, access role, subfolder)")
|
|
10137
|
+
}, async (params) => {
|
|
10138
|
+
try {
|
|
10139
|
+
const resolvedProjectId = resolveId(params.project_id, "projects");
|
|
10140
|
+
const source = addProjectSource({ ...params, project_id: resolvedProjectId });
|
|
10141
|
+
return {
|
|
10142
|
+
content: [{
|
|
10143
|
+
type: "text",
|
|
10144
|
+
text: `Source added: ${source.id.slice(0, 8)} | [${source.type}] ${source.name} \u2192 ${source.uri}`
|
|
10145
|
+
}]
|
|
10146
|
+
};
|
|
10147
|
+
} catch (e) {
|
|
10148
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
10149
|
+
}
|
|
10150
|
+
});
|
|
10151
|
+
}
|
|
10152
|
+
if (shouldRegisterTool("remove_project_source")) {
|
|
10153
|
+
server.tool("remove_project_source", "Remove a data source from a project by source ID.", {
|
|
10154
|
+
source_id: exports_external.string().describe("Source ID to remove")
|
|
10155
|
+
}, async ({ source_id }) => {
|
|
10156
|
+
try {
|
|
10157
|
+
const db = getDatabase();
|
|
10158
|
+
const row = db.query("SELECT * FROM project_sources WHERE id LIKE ?").get(`${source_id}%`);
|
|
10159
|
+
if (!row)
|
|
10160
|
+
return { content: [{ type: "text", text: `Source not found: ${source_id}` }], isError: true };
|
|
10161
|
+
removeProjectSource(row.id);
|
|
10162
|
+
return { content: [{ type: "text", text: `Source removed: ${row.name}` }] };
|
|
10163
|
+
} catch (e) {
|
|
10164
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
10165
|
+
}
|
|
10166
|
+
});
|
|
10167
|
+
}
|
|
10168
|
+
if (shouldRegisterTool("list_project_sources")) {
|
|
10169
|
+
server.tool("list_project_sources", "List all data sources attached to a project.", {
|
|
10170
|
+
project_id: exports_external.string().describe("Project ID")
|
|
10171
|
+
}, async ({ project_id }) => {
|
|
10172
|
+
try {
|
|
10173
|
+
const resolvedId = resolveId(project_id, "projects");
|
|
10174
|
+
const sources = listProjectSources(resolvedId);
|
|
10175
|
+
if (sources.length === 0) {
|
|
10176
|
+
return { content: [{ type: "text", text: "No sources configured for this project." }] };
|
|
10177
|
+
}
|
|
10178
|
+
const lines = sources.map((s) => `${s.id.slice(0, 8)} | [${s.type}] ${s.name} \u2192 ${s.uri}${s.description ? `
|
|
10179
|
+
${s.description}` : ""}`);
|
|
10180
|
+
return { content: [{ type: "text", text: `${sources.length} source(s):
|
|
10181
|
+
${lines.join(`
|
|
10182
|
+
`)}` }] };
|
|
10183
|
+
} catch (e) {
|
|
10184
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
10185
|
+
}
|
|
10186
|
+
});
|
|
10187
|
+
}
|
|
9892
10188
|
if (shouldRegisterTool("create_plan")) {
|
|
9893
10189
|
server.tool("create_plan", "Create a plan to group related tasks.", {
|
|
9894
10190
|
name: exports_external.string(),
|
|
@@ -11205,6 +11501,42 @@ ${checks.map((c) => ` ${c.status === "ok" ? "\u2713" : "\u26A0"} ${c.name}: ${c
|
|
|
11205
11501
|
lines.push(`Other agents active: ${others.slice(0, 3).map((w) => `${w.short_id || w.id.slice(0, 8)} (${w.assigned_to || "?"})`).join(", ")}`);
|
|
11206
11502
|
}
|
|
11207
11503
|
}
|
|
11504
|
+
if (project_id) {
|
|
11505
|
+
const resolvedId = resolveId(project_id, "projects");
|
|
11506
|
+
const sources = listProjectSources(resolvedId);
|
|
11507
|
+
if (sources.length > 0) {
|
|
11508
|
+
lines.push("");
|
|
11509
|
+
lines.push(`## Data Sources`);
|
|
11510
|
+
for (const s of sources) {
|
|
11511
|
+
lines.push(`[${s.type}] ${s.name}: ${s.uri}${s.description ? ` \u2014 ${s.description}` : ""}`);
|
|
11512
|
+
}
|
|
11513
|
+
}
|
|
11514
|
+
}
|
|
11515
|
+
lines.push(`
|
|
11516
|
+
as_of: ${new Date().toISOString()}`);
|
|
11517
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
11518
|
+
`) }] };
|
|
11519
|
+
} catch (e) {
|
|
11520
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
11521
|
+
}
|
|
11522
|
+
});
|
|
11523
|
+
}
|
|
11524
|
+
if (shouldRegisterTool("redistribute_stale_tasks")) {
|
|
11525
|
+
server.tool("redistribute_stale_tasks", "Release stale in-progress tasks and optionally claim the best one. Work-stealing for multi-agent.", {
|
|
11526
|
+
agent_id: exports_external.string().describe("Agent ID claiming the next task after releasing stale ones"),
|
|
11527
|
+
max_age_minutes: exports_external.number().optional().describe("Tasks idle longer than this (default: 60) are released"),
|
|
11528
|
+
project_id: exports_external.string().optional().describe("Limit to a specific project"),
|
|
11529
|
+
limit: exports_external.number().optional().describe("Max number of stale tasks to release")
|
|
11530
|
+
}, async ({ agent_id, max_age_minutes, project_id, limit }) => {
|
|
11531
|
+
try {
|
|
11532
|
+
const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
|
|
11533
|
+
const result = redistributeStaleTasks(agent_id, { max_age_minutes, project_id: resolvedProjectId, limit });
|
|
11534
|
+
const lines = [`Released ${result.released.length} stale task(s).`];
|
|
11535
|
+
for (const t of result.released)
|
|
11536
|
+
lines.push(` ${formatTask(t)}`);
|
|
11537
|
+
if (result.claimed)
|
|
11538
|
+
lines.push(`
|
|
11539
|
+
Claimed: ${formatTask(result.claimed)}`);
|
|
11208
11540
|
return { content: [{ type: "text", text: lines.join(`
|
|
11209
11541
|
`) }] };
|
|
11210
11542
|
} catch (e) {
|
|
@@ -11232,6 +11564,9 @@ ${checks.map((c) => ` ${c.status === "ok" ? "\u2713" : "\u26A0"} ${c.name}: ${c
|
|
|
11232
11564
|
"log_progress",
|
|
11233
11565
|
"create_project",
|
|
11234
11566
|
"list_projects",
|
|
11567
|
+
"add_project_source",
|
|
11568
|
+
"remove_project_source",
|
|
11569
|
+
"list_project_sources",
|
|
11235
11570
|
"create_plan",
|
|
11236
11571
|
"list_plans",
|
|
11237
11572
|
"get_plan",
|
|
@@ -11279,6 +11614,7 @@ ${checks.map((c) => ` ${c.status === "ok" ? "\u2713" : "\u26A0"} ${c.name}: ${c
|
|
|
11279
11614
|
"decompose_task",
|
|
11280
11615
|
"set_task_status",
|
|
11281
11616
|
"set_task_priority",
|
|
11617
|
+
"redistribute_stale_tasks",
|
|
11282
11618
|
"search_tools",
|
|
11283
11619
|
"describe_tools"
|
|
11284
11620
|
].filter((name) => shouldRegisterTool(name));
|
|
@@ -11339,6 +11675,15 @@ ${checks.map((c) => ` ${c.status === "ok" ? "\u2713" : "\u26A0"} ${c.name}: ${c
|
|
|
11339
11675
|
Params: name(string, req), path(string, req \u2014 unique absolute path), description(string), task_list_id(string)
|
|
11340
11676
|
Example: {name: 'my-app', path: '/Users/dev/my-app'}`,
|
|
11341
11677
|
list_projects: "List all registered projects. No params.",
|
|
11678
|
+
add_project_source: `Add a data source to a project (S3, GDrive, local path, GitHub, Notion, HTTP, etc.).
|
|
11679
|
+
Params: project_id(string, req), type(string, req \u2014 e.g. 's3','gdrive','local','github','notion','http'), name(string, req), uri(string, req), description(string), metadata(object)
|
|
11680
|
+
Example: {project_id: 'a1b2c3d4', type: 's3', name: 'Assets bucket', uri: 's3://my-bucket/assets/', description: 'Project media files'}`,
|
|
11681
|
+
remove_project_source: `Remove a data source from a project.
|
|
11682
|
+
Params: source_id(string, req \u2014 source ID or prefix)
|
|
11683
|
+
Example: {source_id: 'abc12345'}`,
|
|
11684
|
+
list_project_sources: `List all data sources for a project.
|
|
11685
|
+
Params: project_id(string, req)
|
|
11686
|
+
Example: {project_id: 'a1b2c3d4'}`,
|
|
11342
11687
|
create_plan: `Create a plan to group related tasks.
|
|
11343
11688
|
Params: name(string, req), project_id(string), description(string), status(active|completed|archived, default:active), task_list_id(string), agent_id(string)
|
|
11344
11689
|
Example: {name: 'Sprint 1', project_id: 'a1b2c3d4'}`,
|
|
@@ -11466,6 +11811,9 @@ ${checks.map((c) => ` ${c.status === "ok" ? "\u2713" : "\u26A0"} ${c.name}: ${c
|
|
|
11466
11811
|
set_task_priority: `Set task priority without needing version. Auto-retries on conflict (up to 3 attempts). Use instead of update_task when you only need to change priority.
|
|
11467
11812
|
Params: id(string, req), priority(low|medium|high|critical, req)
|
|
11468
11813
|
Example: {id: 'a1b2c3d4', priority: 'high'}`,
|
|
11814
|
+
redistribute_stale_tasks: `Release stale in-progress tasks and optionally claim the best one. Multi-agent work-stealing.
|
|
11815
|
+
Params: agent_id(string, req), max_age_minutes(number, default:60), project_id(string, optional), limit(number, optional)
|
|
11816
|
+
Example: {agent_id: 'a1b2c3d4', max_age_minutes: 30}`,
|
|
11469
11817
|
search_tools: `List all tool names or filter by substring.
|
|
11470
11818
|
Params: query(string, optional)
|
|
11471
11819
|
Example: {query: 'task'}`,
|
|
@@ -11496,6 +11844,62 @@ ${checks.map((c) => ` ${c.status === "ok" ? "\u2713" : "\u26A0"} ${c.name}: ${c
|
|
|
11496
11844
|
const agents = listAgents();
|
|
11497
11845
|
return { contents: [{ uri: "todos://agents", text: JSON.stringify(agents, null, 2), mimeType: "application/json" }] };
|
|
11498
11846
|
});
|
|
11847
|
+
if (shouldRegisterTool("create_handoff")) {
|
|
11848
|
+
server.tool("create_handoff", "Create a session handoff note for agent coordination.", {
|
|
11849
|
+
agent_id: exports_external.string().optional().describe("Agent creating the handoff"),
|
|
11850
|
+
project_id: exports_external.string().optional().describe("Project ID"),
|
|
11851
|
+
summary: exports_external.string().describe("What was accomplished this session"),
|
|
11852
|
+
completed: exports_external.array(exports_external.string()).optional().describe("Items completed"),
|
|
11853
|
+
in_progress: exports_external.array(exports_external.string()).optional().describe("Items still in progress"),
|
|
11854
|
+
blockers: exports_external.array(exports_external.string()).optional().describe("Blocking issues"),
|
|
11855
|
+
next_steps: exports_external.array(exports_external.string()).optional().describe("Recommended next actions")
|
|
11856
|
+
}, async ({ agent_id, project_id, summary, completed, in_progress, blockers, next_steps }) => {
|
|
11857
|
+
try {
|
|
11858
|
+
const { createHandoff: createHandoff2 } = (init_handoffs(), __toCommonJS(exports_handoffs));
|
|
11859
|
+
const handoff = createHandoff2({
|
|
11860
|
+
agent_id,
|
|
11861
|
+
project_id: project_id ? resolveId(project_id, "projects") : undefined,
|
|
11862
|
+
summary,
|
|
11863
|
+
completed,
|
|
11864
|
+
in_progress,
|
|
11865
|
+
blockers,
|
|
11866
|
+
next_steps
|
|
11867
|
+
});
|
|
11868
|
+
return { content: [{ type: "text", text: `Handoff created: ${handoff.id.slice(0, 8)} by ${handoff.agent_id || "unknown"}` }] };
|
|
11869
|
+
} catch (e) {
|
|
11870
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
11871
|
+
}
|
|
11872
|
+
});
|
|
11873
|
+
}
|
|
11874
|
+
if (shouldRegisterTool("get_latest_handoff")) {
|
|
11875
|
+
server.tool("get_latest_handoff", "Get the most recent handoff for an agent or project.", {
|
|
11876
|
+
agent_id: exports_external.string().optional().describe("Filter by agent"),
|
|
11877
|
+
project_id: exports_external.string().optional().describe("Filter by project")
|
|
11878
|
+
}, async ({ agent_id, project_id }) => {
|
|
11879
|
+
try {
|
|
11880
|
+
const { getLatestHandoff: getLatestHandoff2 } = (init_handoffs(), __toCommonJS(exports_handoffs));
|
|
11881
|
+
const handoff = getLatestHandoff2(agent_id, project_id ? resolveId(project_id, "projects") : undefined);
|
|
11882
|
+
if (!handoff)
|
|
11883
|
+
return { content: [{ type: "text", text: "No handoffs found." }] };
|
|
11884
|
+
const lines = [
|
|
11885
|
+
`${handoff.created_at.slice(0, 16)} ${handoff.agent_id || "unknown"}`,
|
|
11886
|
+
handoff.summary
|
|
11887
|
+
];
|
|
11888
|
+
if (handoff.completed?.length)
|
|
11889
|
+
lines.push(`Done: ${handoff.completed.join(", ")}`);
|
|
11890
|
+
if (handoff.in_progress?.length)
|
|
11891
|
+
lines.push(`In progress: ${handoff.in_progress.join(", ")}`);
|
|
11892
|
+
if (handoff.blockers?.length)
|
|
11893
|
+
lines.push(`Blocked: ${handoff.blockers.join(", ")}`);
|
|
11894
|
+
if (handoff.next_steps?.length)
|
|
11895
|
+
lines.push(`Next: ${handoff.next_steps.join(", ")}`);
|
|
11896
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
11897
|
+
`) }] };
|
|
11898
|
+
} catch (e) {
|
|
11899
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
11900
|
+
}
|
|
11901
|
+
});
|
|
11902
|
+
}
|
|
11499
11903
|
server.resource("task-lists", "todos://task-lists", { description: "All task lists", mimeType: "application/json" }, async () => {
|
|
11500
11904
|
const lists = listTaskLists();
|
|
11501
11905
|
return { contents: [{ uri: "todos://task-lists", text: JSON.stringify(lists, null, 2), mimeType: "application/json" }] };
|
|
@@ -13631,7 +14035,7 @@ function formatTaskLine(t) {
|
|
|
13631
14035
|
return `${chalk.dim(t.id.slice(0, 8))} ${statusFn(t.status.padEnd(11))} ${priorityFn(t.priority.padEnd(8))} ${t.title}${assigned}${lock}${tags}${plan}`;
|
|
13632
14036
|
}
|
|
13633
14037
|
program2.name("todos").description("Universal task management for AI coding agents").version(getPackageVersion()).option("--project <path>", "Project path").option("--json", "Output as JSON").option("--agent <name>", "Agent name").option("--session <id>", "Session ID");
|
|
13634
|
-
program2.command("add <title>").description("Create a new task").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("--parent <id>", "Parent task ID").option("-t, --tags <tags>", "Comma-separated tags").option("--tag <tags>", "Comma-separated tags (alias for --tags)").option("--plan <id>", "Assign to a plan").option("--assign <agent>", "Assign to agent").option("--status <status>", "Initial status").option("--list <id>", "Task list ID").option("--task-list <id>", "Task list ID (alias for --list)").option("--estimated <minutes>", "Estimated time in minutes").option("--approval", "Require approval before completion").option("--recurrence <rule>", "Recurrence rule, e.g. 'every day', 'every weekday', 'every 2 weeks'").option("--due <date>", "Due date (ISO string or YYYY-MM-DD)").action((title, opts) => {
|
|
14038
|
+
program2.command("add <title>").description("Create a new task").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("--parent <id>", "Parent task ID").option("-t, --tags <tags>", "Comma-separated tags").option("--tag <tags>", "Comma-separated tags (alias for --tags)").option("--plan <id>", "Assign to a plan").option("--assign <agent>", "Assign to agent").option("--status <status>", "Initial status").option("--list <id>", "Task list ID").option("--task-list <id>", "Task list ID (alias for --list)").option("--estimated <minutes>", "Estimated time in minutes").option("--approval", "Require approval before completion").option("--recurrence <rule>", "Recurrence rule, e.g. 'every day', 'every weekday', 'every 2 weeks'").option("--due <date>", "Due date (ISO string or YYYY-MM-DD)").option("--reason <text>", "Why this task exists").action((title, opts) => {
|
|
13635
14039
|
const globalOpts = program2.opts();
|
|
13636
14040
|
const projectId = autoProject(globalOpts);
|
|
13637
14041
|
opts.tags = opts.tags || opts.tag;
|
|
@@ -13670,7 +14074,8 @@ program2.command("add <title>").description("Create a new task").option("-d, --d
|
|
|
13670
14074
|
estimated_minutes: opts.estimated ? parseInt(opts.estimated, 10) : undefined,
|
|
13671
14075
|
requires_approval: opts.approval || false,
|
|
13672
14076
|
recurrence_rule: opts.recurrence,
|
|
13673
|
-
due_at: opts.due ? opts.due.length === 10 ? opts.due + "T00:00:00.000Z" : opts.due : undefined
|
|
14077
|
+
due_at: opts.due ? opts.due.length === 10 ? opts.due + "T00:00:00.000Z" : opts.due : undefined,
|
|
14078
|
+
reason: opts.reason
|
|
13674
14079
|
});
|
|
13675
14080
|
if (globalOpts.json) {
|
|
13676
14081
|
output(task, true);
|
|
@@ -13954,15 +14359,16 @@ program2.command("update <id>").description("Update a task").option("--title <te
|
|
|
13954
14359
|
console.log(formatTaskLine(task));
|
|
13955
14360
|
}
|
|
13956
14361
|
});
|
|
13957
|
-
program2.command("done <id>").description("Mark a task as completed").option("--attach-ids <ids>", "Comma-separated @hasna/attachments IDs to link as evidence").option("--files-changed <files>", "Comma-separated list of files changed").option("--test-results <results>", "Test results summary").option("--commit-hash <hash>", "Git commit hash").option("--notes <notes>", "Completion notes").action((id, opts) => {
|
|
14362
|
+
program2.command("done <id>").description("Mark a task as completed").option("--attach-ids <ids>", "Comma-separated @hasna/attachments IDs to link as evidence").option("--files-changed <files>", "Comma-separated list of files changed").option("--test-results <results>", "Test results summary").option("--commit-hash <hash>", "Git commit hash").option("--notes <notes>", "Completion notes").option("--confidence <0-1>", "Agent's confidence 0.0-1.0 that the task is fully complete (default: 1.0, <0.7 flagged for review)").action((id, opts) => {
|
|
13958
14363
|
const globalOpts = program2.opts();
|
|
13959
14364
|
const resolvedId = resolveTaskId(id);
|
|
13960
14365
|
const attachmentIds = opts.attachIds ? opts.attachIds.split(",").map((s) => s.trim()) : undefined;
|
|
13961
14366
|
const filesChanged = opts.filesChanged ? opts.filesChanged.split(",").map((s) => s.trim()) : undefined;
|
|
14367
|
+
const confidence = opts.confidence !== undefined ? parseFloat(opts.confidence) : undefined;
|
|
13962
14368
|
const evidence = attachmentIds || filesChanged || opts.testResults || opts.commitHash || opts.notes ? { attachment_ids: attachmentIds, files_changed: filesChanged, test_results: opts.testResults, commit_hash: opts.commitHash, notes: opts.notes } : undefined;
|
|
13963
14369
|
let task;
|
|
13964
14370
|
try {
|
|
13965
|
-
task = completeTask(resolvedId, globalOpts.agent, undefined, evidence);
|
|
14371
|
+
task = completeTask(resolvedId, globalOpts.agent, undefined, { ...evidence, confidence });
|
|
13966
14372
|
} catch (e) {
|
|
13967
14373
|
handleError(e);
|
|
13968
14374
|
}
|
|
@@ -15275,6 +15681,33 @@ program2.command("stale").description("Find tasks stuck in_progress with no rece
|
|
|
15275
15681
|
console.log(` ${chalk.cyan(id)} | ${t.locked_by || t.assigned_to || "?"} | ${t.title} ${chalk.dim(`(${staleMin}min stale)`)}`);
|
|
15276
15682
|
}
|
|
15277
15683
|
});
|
|
15684
|
+
program2.command("redistribute <agent>").description("Release stale in-progress tasks and claim the best one (work-stealing)").option("--max-age <minutes>", "Stale threshold in minutes", "60").option("--project <id>", "Limit to a specific project").option("--limit <n>", "Max stale tasks to release").option("--json", "Output as JSON").action(async (agent, opts) => {
|
|
15685
|
+
const globalOpts = program2.opts();
|
|
15686
|
+
const db = getDatabase();
|
|
15687
|
+
const projectId = opts.project ? resolveTaskId(opts.project) : autoProject(globalOpts) ?? undefined;
|
|
15688
|
+
const result = redistributeStaleTasks(agent, {
|
|
15689
|
+
max_age_minutes: parseInt(opts.maxAge, 10),
|
|
15690
|
+
project_id: projectId,
|
|
15691
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined
|
|
15692
|
+
}, db);
|
|
15693
|
+
if (opts.json) {
|
|
15694
|
+
console.log(JSON.stringify(result, null, 2));
|
|
15695
|
+
return;
|
|
15696
|
+
}
|
|
15697
|
+
console.log(chalk.bold(`Released ${result.released.length} stale task(s).`));
|
|
15698
|
+
for (const t of result.released) {
|
|
15699
|
+
const id = t.short_id || t.id.slice(0, 8);
|
|
15700
|
+
console.log(` ${chalk.yellow("released")} ${chalk.cyan(id)} ${t.title}`);
|
|
15701
|
+
}
|
|
15702
|
+
if (result.claimed) {
|
|
15703
|
+
const id = result.claimed.short_id || result.claimed.id.slice(0, 8);
|
|
15704
|
+
console.log(chalk.green(`
|
|
15705
|
+
Claimed: ${chalk.cyan(id)} ${result.claimed.title}`));
|
|
15706
|
+
} else {
|
|
15707
|
+
console.log(chalk.dim(`
|
|
15708
|
+
No task claimed (nothing available).`));
|
|
15709
|
+
}
|
|
15710
|
+
});
|
|
15278
15711
|
program2.command("assign <id> <agent>").description("Assign a task to an agent").option("--json", "Output as JSON").action((id, agent, opts) => {
|
|
15279
15712
|
const globalOpts = program2.opts();
|
|
15280
15713
|
const resolvedId = resolveTaskId(id);
|
|
@@ -15590,6 +16023,543 @@ program2.command("report").description("Analytics report: task activity, complet
|
|
|
15590
16023
|
console.log(lines.join(`
|
|
15591
16024
|
`));
|
|
15592
16025
|
});
|
|
16026
|
+
program2.command("today").description("Show task activity from today").option("--json", "Output as JSON").action(async (opts) => {
|
|
16027
|
+
const globalOpts = program2.opts();
|
|
16028
|
+
const db = getDatabase();
|
|
16029
|
+
const { getTasksChangedSince: getTasksChangedSince2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
16030
|
+
const start = new Date;
|
|
16031
|
+
start.setHours(0, 0, 0, 0);
|
|
16032
|
+
const tasks = getTasksChangedSince2(start.toISOString(), undefined, db);
|
|
16033
|
+
const completed = tasks.filter((t) => t.status === "completed");
|
|
16034
|
+
const started = tasks.filter((t) => t.status === "in_progress");
|
|
16035
|
+
const other = tasks.filter((t) => t.status !== "completed" && t.status !== "in_progress");
|
|
16036
|
+
if (opts.json || globalOpts.json) {
|
|
16037
|
+
console.log(JSON.stringify({ date: start.toISOString().slice(0, 10), completed, started, changed: other }));
|
|
16038
|
+
return;
|
|
16039
|
+
}
|
|
16040
|
+
console.log(chalk.bold(`Today \u2014 ${start.toISOString().slice(0, 10)}
|
|
16041
|
+
`));
|
|
16042
|
+
if (completed.length > 0) {
|
|
16043
|
+
console.log(chalk.green(` \u2713 Completed (${completed.length}):`));
|
|
16044
|
+
for (const t of completed)
|
|
16045
|
+
console.log(` ${chalk.cyan(t.short_id || t.id.slice(0, 8))} ${t.title}${t.assigned_to ? chalk.dim(` \u2014 ${t.assigned_to}`) : ""}`);
|
|
16046
|
+
}
|
|
16047
|
+
if (started.length > 0) {
|
|
16048
|
+
console.log(chalk.blue(`
|
|
16049
|
+
\u25B6 Started (${started.length}):`));
|
|
16050
|
+
for (const t of started)
|
|
16051
|
+
console.log(` ${chalk.cyan(t.short_id || t.id.slice(0, 8))} ${t.title}${t.assigned_to ? chalk.dim(` \u2014 ${t.assigned_to}`) : ""}`);
|
|
16052
|
+
}
|
|
16053
|
+
if (completed.length === 0 && started.length === 0)
|
|
16054
|
+
console.log(chalk.dim(" No activity today."));
|
|
16055
|
+
});
|
|
16056
|
+
program2.command("yesterday").description("Show task activity from yesterday").option("--json", "Output as JSON").action(async (opts) => {
|
|
16057
|
+
const globalOpts = program2.opts();
|
|
16058
|
+
const db = getDatabase();
|
|
16059
|
+
const { getTasksChangedSince: getTasksChangedSince2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
16060
|
+
const start = new Date;
|
|
16061
|
+
start.setDate(start.getDate() - 1);
|
|
16062
|
+
start.setHours(0, 0, 0, 0);
|
|
16063
|
+
const end = new Date(start);
|
|
16064
|
+
end.setHours(23, 59, 59, 999);
|
|
16065
|
+
const allChanged = getTasksChangedSince2(start.toISOString(), undefined, db);
|
|
16066
|
+
const tasks = allChanged.filter((t) => t.updated_at <= end.toISOString());
|
|
16067
|
+
const completed = tasks.filter((t) => t.status === "completed");
|
|
16068
|
+
const started = tasks.filter((t) => t.status === "in_progress");
|
|
16069
|
+
if (opts.json || globalOpts.json) {
|
|
16070
|
+
console.log(JSON.stringify({ date: start.toISOString().slice(0, 10), completed, started }));
|
|
16071
|
+
return;
|
|
16072
|
+
}
|
|
16073
|
+
console.log(chalk.bold(`Yesterday \u2014 ${start.toISOString().slice(0, 10)}
|
|
16074
|
+
`));
|
|
16075
|
+
if (completed.length > 0) {
|
|
16076
|
+
console.log(chalk.green(` \u2713 Completed (${completed.length}):`));
|
|
16077
|
+
for (const t of completed)
|
|
16078
|
+
console.log(` ${chalk.cyan(t.short_id || t.id.slice(0, 8))} ${t.title}${t.assigned_to ? chalk.dim(` \u2014 ${t.assigned_to}`) : ""}`);
|
|
16079
|
+
}
|
|
16080
|
+
if (started.length > 0) {
|
|
16081
|
+
console.log(chalk.blue(`
|
|
16082
|
+
\u25B6 Started (${started.length}):`));
|
|
16083
|
+
for (const t of started)
|
|
16084
|
+
console.log(` ${chalk.cyan(t.short_id || t.id.slice(0, 8))} ${t.title}`);
|
|
16085
|
+
}
|
|
16086
|
+
if (completed.length === 0 && started.length === 0)
|
|
16087
|
+
console.log(chalk.dim(" No activity yesterday."));
|
|
16088
|
+
});
|
|
16089
|
+
program2.command("mine").description("Show tasks assigned to you, grouped by status").argument("<agent>", "Agent name or ID").option("--json", "Output as JSON").action(async (agent, opts) => {
|
|
16090
|
+
const globalOpts = program2.opts();
|
|
16091
|
+
const db = getDatabase();
|
|
16092
|
+
const { listTasks: listTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
16093
|
+
const projectId = autoProject(globalOpts) || undefined;
|
|
16094
|
+
const filter = { assigned_to: agent };
|
|
16095
|
+
if (projectId)
|
|
16096
|
+
filter.project_id = projectId;
|
|
16097
|
+
const tasks = listTasks2(filter, db);
|
|
16098
|
+
const filterByAgent = { agent_id: agent };
|
|
16099
|
+
if (projectId)
|
|
16100
|
+
filterByAgent.project_id = projectId;
|
|
16101
|
+
const agentTasks = listTasks2(filterByAgent, db);
|
|
16102
|
+
const seen = new Set(tasks.map((t) => t.id));
|
|
16103
|
+
for (const t of agentTasks) {
|
|
16104
|
+
if (!seen.has(t.id)) {
|
|
16105
|
+
tasks.push(t);
|
|
16106
|
+
seen.add(t.id);
|
|
16107
|
+
}
|
|
16108
|
+
}
|
|
16109
|
+
if (opts.json || globalOpts.json) {
|
|
16110
|
+
console.log(JSON.stringify(tasks));
|
|
16111
|
+
return;
|
|
16112
|
+
}
|
|
16113
|
+
const groups = {};
|
|
16114
|
+
for (const t of tasks) {
|
|
16115
|
+
const s = t.status || "unknown";
|
|
16116
|
+
if (!groups[s])
|
|
16117
|
+
groups[s] = [];
|
|
16118
|
+
groups[s].push(t);
|
|
16119
|
+
}
|
|
16120
|
+
const statusOrder = ["in_progress", "pending", "blocked", "completed", "failed", "cancelled"];
|
|
16121
|
+
const statusIcons2 = { in_progress: "\u25B6", pending: "\u25CB", blocked: "\u2298", completed: "\u2713", failed: "\u2717", cancelled: "\u2014" };
|
|
16122
|
+
const statusColors5 = { in_progress: chalk.blue, pending: chalk.white, blocked: chalk.red, completed: chalk.green, failed: chalk.red, cancelled: chalk.dim };
|
|
16123
|
+
console.log(chalk.bold(`Tasks for ${agent} (${tasks.length} total):
|
|
16124
|
+
`));
|
|
16125
|
+
for (const status of statusOrder) {
|
|
16126
|
+
const group = groups[status];
|
|
16127
|
+
if (!group || group.length === 0)
|
|
16128
|
+
continue;
|
|
16129
|
+
const color = statusColors5[status] || chalk.white;
|
|
16130
|
+
const icon = statusIcons2[status] || "?";
|
|
16131
|
+
console.log(color(` ${icon} ${status.replace("_", " ")} (${group.length}):`));
|
|
16132
|
+
for (const t of group) {
|
|
16133
|
+
const priority = t.priority === "critical" || t.priority === "high" ? chalk.red(` [${t.priority}]`) : "";
|
|
16134
|
+
console.log(` ${chalk.cyan(t.short_id || t.id.slice(0, 8))} ${t.title}${priority}`);
|
|
16135
|
+
}
|
|
16136
|
+
}
|
|
16137
|
+
if (tasks.length === 0)
|
|
16138
|
+
console.log(chalk.dim(` No tasks assigned to ${agent}.`));
|
|
16139
|
+
});
|
|
16140
|
+
program2.command("blocked").description("Show tasks blocked by incomplete dependencies").option("--json", "Output as JSON").option("--project <id>", "Filter to project").action(async (opts) => {
|
|
16141
|
+
const globalOpts = program2.opts();
|
|
16142
|
+
const db = getDatabase();
|
|
16143
|
+
const { listTasks: listTasks2, getBlockingDeps: getBlockingDeps2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
16144
|
+
const projectId = autoProject(globalOpts) || opts.project || undefined;
|
|
16145
|
+
const filter = { status: "pending" };
|
|
16146
|
+
if (projectId)
|
|
16147
|
+
filter.project_id = projectId;
|
|
16148
|
+
const allPending = listTasks2(filter, db);
|
|
16149
|
+
const blockedTasks = [];
|
|
16150
|
+
for (const t of allPending) {
|
|
16151
|
+
const blockers = getBlockingDeps2(t.id, db);
|
|
16152
|
+
if (blockers.length > 0)
|
|
16153
|
+
blockedTasks.push({ task: t, blockers });
|
|
16154
|
+
}
|
|
16155
|
+
if (opts.json || globalOpts.json) {
|
|
16156
|
+
console.log(JSON.stringify(blockedTasks.map((b) => ({ ...b.task, blocked_by: b.blockers.map((bl) => ({ id: bl.id, short_id: bl.short_id, title: bl.title, status: bl.status })) }))));
|
|
16157
|
+
return;
|
|
16158
|
+
}
|
|
16159
|
+
if (blockedTasks.length === 0) {
|
|
16160
|
+
console.log(chalk.green(" No blocked tasks!"));
|
|
16161
|
+
return;
|
|
16162
|
+
}
|
|
16163
|
+
console.log(chalk.bold(`Blocked (${blockedTasks.length}):
|
|
16164
|
+
`));
|
|
16165
|
+
for (const { task, blockers } of blockedTasks) {
|
|
16166
|
+
console.log(` ${chalk.cyan(task.short_id || task.id.slice(0, 8))} ${task.title}`);
|
|
16167
|
+
for (const bl of blockers) {
|
|
16168
|
+
console.log(` ${chalk.red("\u2298")} ${chalk.dim(bl.short_id || bl.id.slice(0, 8))} ${chalk.dim(bl.title)} ${chalk.yellow(`[${bl.status}]`)}`);
|
|
16169
|
+
}
|
|
16170
|
+
}
|
|
16171
|
+
});
|
|
16172
|
+
program2.command("overdue").description("Show tasks past their due date").option("--json", "Output as JSON").option("--project <id>", "Filter to project").action(async (opts) => {
|
|
16173
|
+
const globalOpts = program2.opts();
|
|
16174
|
+
const projectId = autoProject(globalOpts) || opts.project || undefined;
|
|
16175
|
+
const { getOverdueTasks: getOverdueTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
16176
|
+
const tasks = getOverdueTasks2(projectId);
|
|
16177
|
+
if (opts.json || globalOpts.json) {
|
|
16178
|
+
console.log(JSON.stringify(tasks));
|
|
16179
|
+
return;
|
|
16180
|
+
}
|
|
16181
|
+
if (tasks.length === 0) {
|
|
16182
|
+
console.log(chalk.green(" No overdue tasks!"));
|
|
16183
|
+
return;
|
|
16184
|
+
}
|
|
16185
|
+
console.log(chalk.bold.red(`Overdue (${tasks.length}):
|
|
16186
|
+
`));
|
|
16187
|
+
for (const t of tasks) {
|
|
16188
|
+
const dueDate = t.due_at.slice(0, 10);
|
|
16189
|
+
const daysOverdue = Math.floor((Date.now() - new Date(t.due_at).getTime()) / 86400000);
|
|
16190
|
+
const urgency = daysOverdue > 7 ? chalk.bgRed.white(` ${daysOverdue}d `) : chalk.red(`${daysOverdue}d`);
|
|
16191
|
+
console.log(` ${urgency} ${chalk.cyan(t.short_id || t.id.slice(0, 8))} ${t.title}${t.assigned_to ? chalk.dim(` \u2014 ${t.assigned_to}`) : ""} ${chalk.dim(`(due ${dueDate})`)}`);
|
|
16192
|
+
}
|
|
16193
|
+
});
|
|
16194
|
+
program2.command("week").description("Show task activity from the past 7 days").option("--json", "Output as JSON").action(async (opts) => {
|
|
16195
|
+
const globalOpts = program2.opts();
|
|
16196
|
+
const db = getDatabase();
|
|
16197
|
+
const { getTasksChangedSince: getTasksChangedSince2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
16198
|
+
const now2 = new Date;
|
|
16199
|
+
const start = new Date(now2);
|
|
16200
|
+
start.setDate(start.getDate() - 7);
|
|
16201
|
+
start.setHours(0, 0, 0, 0);
|
|
16202
|
+
const tasks = getTasksChangedSince2(start.toISOString(), undefined, db);
|
|
16203
|
+
const days = {};
|
|
16204
|
+
for (let i = 0;i < 7; i++) {
|
|
16205
|
+
const d = new Date(now2);
|
|
16206
|
+
d.setDate(d.getDate() - i);
|
|
16207
|
+
days[d.toISOString().slice(0, 10)] = { completed: [], started: [], other: [] };
|
|
16208
|
+
}
|
|
16209
|
+
for (const t of tasks) {
|
|
16210
|
+
const day = (t.updated_at || t.created_at).slice(0, 10);
|
|
16211
|
+
if (!days[day])
|
|
16212
|
+
days[day] = { completed: [], started: [], other: [] };
|
|
16213
|
+
if (t.status === "completed")
|
|
16214
|
+
days[day].completed.push(t);
|
|
16215
|
+
else if (t.status === "in_progress")
|
|
16216
|
+
days[day].started.push(t);
|
|
16217
|
+
else
|
|
16218
|
+
days[day].other.push(t);
|
|
16219
|
+
}
|
|
16220
|
+
if (opts.json || globalOpts.json) {
|
|
16221
|
+
console.log(JSON.stringify({ from: start.toISOString().slice(0, 10), to: now2.toISOString().slice(0, 10), days }));
|
|
16222
|
+
return;
|
|
16223
|
+
}
|
|
16224
|
+
const totalCompleted = tasks.filter((t) => t.status === "completed").length;
|
|
16225
|
+
const totalStarted = tasks.filter((t) => t.status === "in_progress").length;
|
|
16226
|
+
console.log(chalk.bold(`Week \u2014 ${start.toISOString().slice(0, 10)} to ${now2.toISOString().slice(0, 10)}`));
|
|
16227
|
+
console.log(chalk.dim(` ${totalCompleted} completed, ${totalStarted} in progress, ${tasks.length} total changes
|
|
16228
|
+
`));
|
|
16229
|
+
const sortedDays = Object.keys(days).sort().reverse();
|
|
16230
|
+
for (const day of sortedDays) {
|
|
16231
|
+
const dayData = days[day];
|
|
16232
|
+
if (!dayData)
|
|
16233
|
+
continue;
|
|
16234
|
+
const { completed, started } = dayData;
|
|
16235
|
+
if (completed.length === 0 && started.length === 0)
|
|
16236
|
+
continue;
|
|
16237
|
+
const weekday = new Date(day + "T12:00:00").toLocaleDateString("en-US", { weekday: "short" });
|
|
16238
|
+
console.log(chalk.bold(` ${weekday} ${day}`));
|
|
16239
|
+
for (const t of completed)
|
|
16240
|
+
console.log(` ${chalk.green("\u2713")} ${chalk.cyan(t.short_id || t.id.slice(0, 8))} ${t.title}${t.assigned_to ? chalk.dim(` \u2014 ${t.assigned_to}`) : ""}`);
|
|
16241
|
+
for (const t of started)
|
|
16242
|
+
console.log(` ${chalk.blue("\u25B6")} ${chalk.cyan(t.short_id || t.id.slice(0, 8))} ${t.title}${t.assigned_to ? chalk.dim(` \u2014 ${t.assigned_to}`) : ""}`);
|
|
16243
|
+
}
|
|
16244
|
+
if (tasks.length === 0)
|
|
16245
|
+
console.log(chalk.dim(" No activity this week."));
|
|
16246
|
+
});
|
|
16247
|
+
program2.command("burndown").description("Show task completion velocity over the past 7 days").option("--days <n>", "Number of days", "7").option("--json", "Output as JSON").action(async (opts) => {
|
|
16248
|
+
const globalOpts = program2.opts();
|
|
16249
|
+
const db = getDatabase();
|
|
16250
|
+
const { getRecentActivity: getRecentActivity2 } = (init_audit(), __toCommonJS(exports_audit));
|
|
16251
|
+
const numDays = parseInt(opts.days, 10);
|
|
16252
|
+
const entries = getRecentActivity2(5000, db);
|
|
16253
|
+
const now2 = new Date;
|
|
16254
|
+
const dayStats = [];
|
|
16255
|
+
for (let i = numDays - 1;i >= 0; i--) {
|
|
16256
|
+
const d = new Date(now2);
|
|
16257
|
+
d.setDate(d.getDate() - i);
|
|
16258
|
+
const dateStr = d.toISOString().slice(0, 10);
|
|
16259
|
+
const dayEntries = entries.filter((e) => e.created_at.slice(0, 10) === dateStr);
|
|
16260
|
+
dayStats.push({
|
|
16261
|
+
date: dateStr,
|
|
16262
|
+
completed: dayEntries.filter((e) => e.action === "complete").length,
|
|
16263
|
+
created: dayEntries.filter((e) => e.action === "create").length,
|
|
16264
|
+
failed: dayEntries.filter((e) => e.action === "fail").length
|
|
16265
|
+
});
|
|
16266
|
+
}
|
|
16267
|
+
if (opts.json || globalOpts.json) {
|
|
16268
|
+
console.log(JSON.stringify(dayStats));
|
|
16269
|
+
return;
|
|
16270
|
+
}
|
|
16271
|
+
const maxVal = Math.max(...dayStats.map((d) => Math.max(d.completed, d.created)), 1);
|
|
16272
|
+
const barWidth = 30;
|
|
16273
|
+
console.log(chalk.bold("Burndown (last " + numDays + ` days):
|
|
16274
|
+
`));
|
|
16275
|
+
console.log(chalk.dim(" Date Done New Failed Chart"));
|
|
16276
|
+
for (const day of dayStats) {
|
|
16277
|
+
const weekday = new Date(day.date + "T12:00:00").toLocaleDateString("en-US", { weekday: "short" });
|
|
16278
|
+
const completedBar = chalk.green("\u2588".repeat(Math.round(day.completed / maxVal * barWidth)));
|
|
16279
|
+
const createdBar = chalk.blue("\u2591".repeat(Math.round(day.created / maxVal * barWidth)));
|
|
16280
|
+
const failed = day.failed > 0 ? chalk.red(String(day.failed).padStart(4)) : chalk.dim(" 0");
|
|
16281
|
+
console.log(` ${weekday} ${day.date.slice(5)} ${chalk.green(String(day.completed).padStart(4))} ${chalk.blue(String(day.created).padStart(4))} ${failed} ${completedBar}${createdBar}`);
|
|
16282
|
+
}
|
|
16283
|
+
const totalCompleted = dayStats.reduce((s, d) => s + d.completed, 0);
|
|
16284
|
+
const totalCreated = dayStats.reduce((s, d) => s + d.created, 0);
|
|
16285
|
+
const velocity = (totalCompleted / numDays).toFixed(1);
|
|
16286
|
+
console.log(chalk.dim(`
|
|
16287
|
+
Velocity: ${velocity}/day \xB7 ${totalCompleted} done \xB7 ${totalCreated} created`));
|
|
16288
|
+
});
|
|
16289
|
+
program2.command("log").description("Show recent task activity log (git-log style)").option("--limit <n>", "Number of entries", "30").option("--json", "Output as JSON").action(async (opts) => {
|
|
16290
|
+
const globalOpts = program2.opts();
|
|
16291
|
+
const db = getDatabase();
|
|
16292
|
+
const { getRecentActivity: getRecentActivity2 } = (init_audit(), __toCommonJS(exports_audit));
|
|
16293
|
+
const entries = getRecentActivity2(parseInt(opts.limit, 10), db);
|
|
16294
|
+
if (opts.json || globalOpts.json) {
|
|
16295
|
+
console.log(JSON.stringify(entries));
|
|
16296
|
+
return;
|
|
16297
|
+
}
|
|
16298
|
+
if (entries.length === 0) {
|
|
16299
|
+
console.log(chalk.dim(" No activity yet."));
|
|
16300
|
+
return;
|
|
16301
|
+
}
|
|
16302
|
+
const actionIcons = {
|
|
16303
|
+
create: chalk.green("+"),
|
|
16304
|
+
start: chalk.blue("\u25B6"),
|
|
16305
|
+
complete: chalk.green("\u2713"),
|
|
16306
|
+
fail: chalk.red("\u2717"),
|
|
16307
|
+
update: chalk.yellow("~"),
|
|
16308
|
+
approve: chalk.green("\u2605"),
|
|
16309
|
+
lock: chalk.dim("\uD83D\uDD12"),
|
|
16310
|
+
unlock: chalk.dim("\uD83D\uDD13")
|
|
16311
|
+
};
|
|
16312
|
+
let lastDate = "";
|
|
16313
|
+
for (const e of entries) {
|
|
16314
|
+
const date = e.created_at.slice(0, 10);
|
|
16315
|
+
const time = e.created_at.slice(11, 16);
|
|
16316
|
+
if (date !== lastDate) {
|
|
16317
|
+
console.log(chalk.bold(`
|
|
16318
|
+
${date}`));
|
|
16319
|
+
lastDate = date;
|
|
16320
|
+
}
|
|
16321
|
+
const icon = actionIcons[e.action] || chalk.dim("\xB7");
|
|
16322
|
+
const agent = e.agent_id ? chalk.dim(` (${e.agent_id})`) : "";
|
|
16323
|
+
const taskRef = chalk.cyan(e.task_id.slice(0, 8));
|
|
16324
|
+
let detail = "";
|
|
16325
|
+
if (e.field && e.old_value && e.new_value) {
|
|
16326
|
+
detail = chalk.dim(` ${e.field}: ${e.old_value} \u2192 ${e.new_value}`);
|
|
16327
|
+
} else if (e.field && e.new_value) {
|
|
16328
|
+
detail = chalk.dim(` ${e.field}: ${e.new_value}`);
|
|
16329
|
+
}
|
|
16330
|
+
console.log(` ${chalk.dim(time)} ${icon} ${e.action.padEnd(8)} ${taskRef}${detail}${agent}`);
|
|
16331
|
+
}
|
|
16332
|
+
});
|
|
16333
|
+
program2.command("ready").description("Show all tasks ready to be claimed (pending, unblocked, unlocked)").option("--json", "Output as JSON").option("--project <id>", "Filter to project").option("--limit <n>", "Max tasks to show", "20").action(async (opts) => {
|
|
16334
|
+
const globalOpts = program2.opts();
|
|
16335
|
+
const db = getDatabase();
|
|
16336
|
+
const { listTasks: listTasks2, getBlockingDeps: getBlockingDeps2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
16337
|
+
const { isLockExpired: isLockExpired2 } = (init_database(), __toCommonJS(exports_database));
|
|
16338
|
+
const projectId = autoProject(globalOpts) || opts.project || undefined;
|
|
16339
|
+
const filter = { status: "pending" };
|
|
16340
|
+
if (projectId)
|
|
16341
|
+
filter.project_id = projectId;
|
|
16342
|
+
const pending = listTasks2(filter, db);
|
|
16343
|
+
const ready = pending.filter((t) => {
|
|
16344
|
+
if (t.locked_by && !isLockExpired2(t.locked_at))
|
|
16345
|
+
return false;
|
|
16346
|
+
const blockers = getBlockingDeps2(t.id, db);
|
|
16347
|
+
return blockers.length === 0;
|
|
16348
|
+
});
|
|
16349
|
+
const limited = ready.slice(0, parseInt(opts.limit, 10));
|
|
16350
|
+
if (opts.json || globalOpts.json) {
|
|
16351
|
+
console.log(JSON.stringify(limited));
|
|
16352
|
+
return;
|
|
16353
|
+
}
|
|
16354
|
+
if (limited.length === 0) {
|
|
16355
|
+
console.log(chalk.dim(" No tasks ready to claim."));
|
|
16356
|
+
return;
|
|
16357
|
+
}
|
|
16358
|
+
console.log(chalk.bold(`Ready to claim (${ready.length}${ready.length > limited.length ? `, showing ${limited.length}` : ""}):
|
|
16359
|
+
`));
|
|
16360
|
+
for (const t of limited) {
|
|
16361
|
+
const pri = t.priority === "critical" ? chalk.bgRed.white(" CRIT ") : t.priority === "high" ? chalk.red("[high]") : t.priority === "medium" ? chalk.yellow("[med]") : "";
|
|
16362
|
+
const due = t.due_at ? chalk.dim(` due ${t.due_at.slice(0, 10)}`) : "";
|
|
16363
|
+
console.log(` ${chalk.cyan(t.short_id || t.id.slice(0, 8))} ${t.title} ${pri}${due}`);
|
|
16364
|
+
}
|
|
16365
|
+
});
|
|
16366
|
+
program2.command("sprint").description("Sprint dashboard: in-progress, next up, blockers, and overdue").option("--json", "Output as JSON").option("--project <id>", "Filter to project").action(async (opts) => {
|
|
16367
|
+
const globalOpts = program2.opts();
|
|
16368
|
+
const db = getDatabase();
|
|
16369
|
+
const { listTasks: listTasks2, getBlockingDeps: getBlockingDeps2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
16370
|
+
const projectId = autoProject(globalOpts) || opts.project || undefined;
|
|
16371
|
+
const baseFilter = {};
|
|
16372
|
+
if (projectId)
|
|
16373
|
+
baseFilter.project_id = projectId;
|
|
16374
|
+
const inProgress = listTasks2({ ...baseFilter, status: "in_progress" }, db);
|
|
16375
|
+
const pending = listTasks2({ ...baseFilter, status: "pending" }, db);
|
|
16376
|
+
const nowStr = new Date().toISOString();
|
|
16377
|
+
const blocked = [];
|
|
16378
|
+
for (const t of pending) {
|
|
16379
|
+
const blockers = getBlockingDeps2(t.id, db);
|
|
16380
|
+
if (blockers.length > 0)
|
|
16381
|
+
blocked.push({ task: t, blockers });
|
|
16382
|
+
}
|
|
16383
|
+
const overdue = [...inProgress, ...pending].filter((t) => t.due_at && t.due_at < nowStr);
|
|
16384
|
+
const blockedIds = new Set(blocked.map((b) => b.task.id));
|
|
16385
|
+
const nextUp = pending.filter((t) => !blockedIds.has(t.id)).slice(0, 5);
|
|
16386
|
+
if (opts.json || globalOpts.json) {
|
|
16387
|
+
console.log(JSON.stringify({ in_progress: inProgress, next_up: nextUp, blocked, overdue }));
|
|
16388
|
+
return;
|
|
16389
|
+
}
|
|
16390
|
+
console.log(chalk.bold(`Sprint Dashboard
|
|
16391
|
+
`));
|
|
16392
|
+
console.log(chalk.blue(` \u25B6 In Progress (${inProgress.length}):`));
|
|
16393
|
+
if (inProgress.length === 0)
|
|
16394
|
+
console.log(chalk.dim(" (none)"));
|
|
16395
|
+
for (const t of inProgress) {
|
|
16396
|
+
const agent = t.assigned_to ? chalk.dim(` \u2014 ${t.assigned_to}`) : "";
|
|
16397
|
+
console.log(` ${chalk.cyan(t.short_id || t.id.slice(0, 8))} ${t.title}${agent}`);
|
|
16398
|
+
}
|
|
16399
|
+
console.log(chalk.white(`
|
|
16400
|
+
\u25CB Next Up (${nextUp.length}):`));
|
|
16401
|
+
if (nextUp.length === 0)
|
|
16402
|
+
console.log(chalk.dim(" (none)"));
|
|
16403
|
+
for (const t of nextUp) {
|
|
16404
|
+
const pri = t.priority === "critical" ? chalk.bgRed.white(" CRIT ") : t.priority === "high" ? chalk.red("[high]") : "";
|
|
16405
|
+
console.log(` ${chalk.cyan(t.short_id || t.id.slice(0, 8))} ${t.title} ${pri}`);
|
|
16406
|
+
}
|
|
16407
|
+
if (blocked.length > 0) {
|
|
16408
|
+
console.log(chalk.red(`
|
|
16409
|
+
\u2298 Blocked (${blocked.length}):`));
|
|
16410
|
+
for (const { task, blockers } of blocked) {
|
|
16411
|
+
console.log(` ${chalk.cyan(task.short_id || task.id.slice(0, 8))} ${task.title}`);
|
|
16412
|
+
for (const bl of blockers)
|
|
16413
|
+
console.log(` ${chalk.dim("\u2190 " + (bl.short_id || bl.id.slice(0, 8)) + " " + bl.title)} ${chalk.yellow(`[${bl.status}]`)}`);
|
|
16414
|
+
}
|
|
16415
|
+
}
|
|
16416
|
+
if (overdue.length > 0) {
|
|
16417
|
+
console.log(chalk.red(`
|
|
16418
|
+
\u26A0 Overdue (${overdue.length}):`));
|
|
16419
|
+
for (const t of overdue) {
|
|
16420
|
+
const daysOver = Math.floor((Date.now() - new Date(t.due_at).getTime()) / 86400000);
|
|
16421
|
+
console.log(` ${chalk.red(`${daysOver}d`)} ${chalk.cyan(t.short_id || t.id.slice(0, 8))} ${t.title}`);
|
|
16422
|
+
}
|
|
16423
|
+
}
|
|
16424
|
+
console.log(chalk.dim(`
|
|
16425
|
+
${inProgress.length} active \xB7 ${pending.length} pending \xB7 ${blocked.length} blocked \xB7 ${overdue.length} overdue`));
|
|
16426
|
+
});
|
|
16427
|
+
program2.command("handoff").description("Create or view agent session handoffs").option("--create", "Create a new handoff").option("--agent <name>", "Agent name").option("--summary <text>", "Handoff summary").option("--completed <items>", "Comma-separated completed items").option("--in-progress <items>", "Comma-separated in-progress items").option("--blockers <items>", "Comma-separated blockers").option("--next <items>", "Comma-separated next steps").option("--json", "Output as JSON").option("--limit <n>", "Number of handoffs to show", "5").action(async (opts) => {
|
|
16428
|
+
const globalOpts = program2.opts();
|
|
16429
|
+
const db = getDatabase();
|
|
16430
|
+
const { createHandoff: createHandoff2, listHandoffs: listHandoffs2 } = (init_handoffs(), __toCommonJS(exports_handoffs));
|
|
16431
|
+
const projectId = autoProject(globalOpts) || undefined;
|
|
16432
|
+
if (opts.create || opts.summary) {
|
|
16433
|
+
if (!opts.summary) {
|
|
16434
|
+
console.error(chalk.red(" --summary is required for creating a handoff"));
|
|
16435
|
+
process.exit(1);
|
|
16436
|
+
}
|
|
16437
|
+
const handoff = createHandoff2({
|
|
16438
|
+
agent_id: opts.agent || globalOpts.agent || undefined,
|
|
16439
|
+
project_id: projectId,
|
|
16440
|
+
summary: opts.summary,
|
|
16441
|
+
completed: opts.completed ? opts.completed.split(",").map((s) => s.trim()) : undefined,
|
|
16442
|
+
inProgress: opts.inProgress ? opts.inProgress.split(",").map((s) => s.trim()) : undefined,
|
|
16443
|
+
blockers: opts.blockers ? opts.blockers.split(",").map((s) => s.trim()) : undefined,
|
|
16444
|
+
next_steps: opts.next ? opts.next.split(",").map((s) => s.trim()) : undefined
|
|
16445
|
+
}, db);
|
|
16446
|
+
if (opts.json || globalOpts.json) {
|
|
16447
|
+
console.log(JSON.stringify(handoff));
|
|
16448
|
+
return;
|
|
16449
|
+
}
|
|
16450
|
+
console.log(chalk.green(` \u2713 Handoff created by ${handoff.agent_id || "unknown"}`));
|
|
16451
|
+
return;
|
|
16452
|
+
}
|
|
16453
|
+
const handoffs = listHandoffs2(projectId, parseInt(opts.limit, 10), db);
|
|
16454
|
+
if (opts.json || globalOpts.json) {
|
|
16455
|
+
console.log(JSON.stringify(handoffs));
|
|
16456
|
+
return;
|
|
16457
|
+
}
|
|
16458
|
+
if (handoffs.length === 0) {
|
|
16459
|
+
console.log(chalk.dim(" No handoffs yet."));
|
|
16460
|
+
return;
|
|
16461
|
+
}
|
|
16462
|
+
for (const h of handoffs) {
|
|
16463
|
+
const time = h.created_at.slice(0, 16).replace("T", " ");
|
|
16464
|
+
console.log(chalk.bold(`
|
|
16465
|
+
${time} ${h.agent_id || "unknown"}`));
|
|
16466
|
+
console.log(` ${h.summary}`);
|
|
16467
|
+
if (h.completed?.length) {
|
|
16468
|
+
console.log(chalk.green(` \u2713 Completed:`));
|
|
16469
|
+
for (const c of h.completed)
|
|
16470
|
+
console.log(` - ${c}`);
|
|
16471
|
+
}
|
|
16472
|
+
if (h.in_progress?.length) {
|
|
16473
|
+
console.log(chalk.blue(` \u25B6 In progress:`));
|
|
16474
|
+
for (const c of h.in_progress)
|
|
16475
|
+
console.log(` - ${c}`);
|
|
16476
|
+
}
|
|
16477
|
+
if (h.blockers?.length) {
|
|
16478
|
+
console.log(chalk.red(` \u2298 Blockers:`));
|
|
16479
|
+
for (const c of h.blockers)
|
|
16480
|
+
console.log(` - ${c}`);
|
|
16481
|
+
}
|
|
16482
|
+
if (h.next_steps?.length) {
|
|
16483
|
+
console.log(chalk.cyan(` \u2192 Next steps:`));
|
|
16484
|
+
for (const c of h.next_steps)
|
|
16485
|
+
console.log(` - ${c}`);
|
|
16486
|
+
}
|
|
16487
|
+
}
|
|
16488
|
+
});
|
|
16489
|
+
program2.command("priorities").description("Show task counts grouped by priority").option("--json", "Output as JSON").option("--project <id>", "Filter to project").action(async (opts) => {
|
|
16490
|
+
const globalOpts = program2.opts();
|
|
16491
|
+
const db = getDatabase();
|
|
16492
|
+
const { countTasks: countTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
16493
|
+
const projectId = autoProject(globalOpts) || opts.project || undefined;
|
|
16494
|
+
const base = projectId ? { project_id: projectId } : {};
|
|
16495
|
+
const priorities = ["critical", "high", "medium", "low", "none"];
|
|
16496
|
+
const counts = {};
|
|
16497
|
+
for (const p of priorities) {
|
|
16498
|
+
counts[p] = {
|
|
16499
|
+
total: countTasks2({ ...base, priority: p }, db),
|
|
16500
|
+
pending: countTasks2({ ...base, priority: p, status: "pending" }, db),
|
|
16501
|
+
in_progress: countTasks2({ ...base, priority: p, status: "in_progress" }, db),
|
|
16502
|
+
completed: countTasks2({ ...base, priority: p, status: "completed" }, db)
|
|
16503
|
+
};
|
|
16504
|
+
}
|
|
16505
|
+
if (opts.json || globalOpts.json) {
|
|
16506
|
+
console.log(JSON.stringify(counts));
|
|
16507
|
+
return;
|
|
16508
|
+
}
|
|
16509
|
+
console.log(chalk.bold(`Priority Breakdown:
|
|
16510
|
+
`));
|
|
16511
|
+
const priColors = { critical: chalk.bgRed.white, high: chalk.red, medium: chalk.yellow, low: chalk.blue, none: chalk.dim };
|
|
16512
|
+
for (const p of priorities) {
|
|
16513
|
+
const c = counts[p];
|
|
16514
|
+
if (!c || c.total === 0)
|
|
16515
|
+
continue;
|
|
16516
|
+
const color = priColors[p] || chalk.white;
|
|
16517
|
+
const bar = chalk.green("\u2588".repeat(Math.min(c.completed, 30))) + chalk.blue("\u2591".repeat(Math.min(c.in_progress, 10))) + chalk.dim("\xB7".repeat(Math.min(c.pending, 20)));
|
|
16518
|
+
console.log(` ${color(p.padEnd(9))} ${String(c.total).padStart(4)} total ${chalk.green(String(c.completed).padStart(3))} done ${chalk.blue(String(c.in_progress).padStart(3))} active ${chalk.dim(String(c.pending).padStart(3))} pending ${bar}`);
|
|
16519
|
+
}
|
|
16520
|
+
});
|
|
16521
|
+
program2.command("context").description("Session start context: status, latest handoff, next task, overdue").option("--agent <name>", "Agent name for handoff lookup").option("--json", "Output as JSON").action(async (opts) => {
|
|
16522
|
+
const globalOpts = program2.opts();
|
|
16523
|
+
const db = getDatabase();
|
|
16524
|
+
const { getStatus: getStatus2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
16525
|
+
const { getNextTask: getNextTask2, getOverdueTasks: getOverdueTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
16526
|
+
const { getLatestHandoff: getLatestHandoff2 } = (init_handoffs(), __toCommonJS(exports_handoffs));
|
|
16527
|
+
const projectId = autoProject(globalOpts) || undefined;
|
|
16528
|
+
const agentName = opts.agent || globalOpts.agent || undefined;
|
|
16529
|
+
const filters = projectId ? { project_id: projectId } : undefined;
|
|
16530
|
+
const status = getStatus2(filters, agentName);
|
|
16531
|
+
const nextTask = getNextTask2(agentName, filters, db);
|
|
16532
|
+
const overdue = getOverdueTasks2(projectId, db);
|
|
16533
|
+
const handoff = agentName ? getLatestHandoff2(agentName, projectId, db) : getLatestHandoff2(undefined, projectId, db);
|
|
16534
|
+
if (opts.json || globalOpts.json) {
|
|
16535
|
+
console.log(JSON.stringify({ status, next_task: nextTask, overdue_count: overdue.length, latest_handoff: handoff, as_of: new Date().toISOString() }));
|
|
16536
|
+
return;
|
|
16537
|
+
}
|
|
16538
|
+
console.log(chalk.bold(`Session Context
|
|
16539
|
+
`));
|
|
16540
|
+
console.log(` ${status.pending} pending \xB7 ${status.in_progress} active \xB7 ${status.completed} done \xB7 ${status.total} total`);
|
|
16541
|
+
if (status.stale_count > 0)
|
|
16542
|
+
console.log(chalk.yellow(` \u26A0 ${status.stale_count} stale tasks`));
|
|
16543
|
+
if (overdue.length > 0)
|
|
16544
|
+
console.log(chalk.red(` \u26A0 ${overdue.length} overdue tasks`));
|
|
16545
|
+
if (nextTask) {
|
|
16546
|
+
const pri = nextTask.priority === "critical" || nextTask.priority === "high" ? chalk.red(` [${nextTask.priority}]`) : "";
|
|
16547
|
+
console.log(chalk.bold(`
|
|
16548
|
+
Next up:`));
|
|
16549
|
+
console.log(` ${chalk.cyan(nextTask.short_id || nextTask.id.slice(0, 8))} ${nextTask.title}${pri}`);
|
|
16550
|
+
}
|
|
16551
|
+
if (handoff) {
|
|
16552
|
+
console.log(chalk.bold(`
|
|
16553
|
+
Last handoff (${handoff.agent_id || "unknown"}, ${handoff.created_at.slice(0, 16).replace("T", " ")}):`));
|
|
16554
|
+
console.log(` ${handoff.summary}`);
|
|
16555
|
+
if (handoff.next_steps?.length) {
|
|
16556
|
+
for (const s of handoff.next_steps)
|
|
16557
|
+
console.log(` \u2192 ${s}`);
|
|
16558
|
+
}
|
|
16559
|
+
}
|
|
16560
|
+
console.log(chalk.dim(`
|
|
16561
|
+
as_of: ${new Date().toISOString()}`));
|
|
16562
|
+
});
|
|
15593
16563
|
program2.action(async () => {
|
|
15594
16564
|
if (process.stdout.isTTY) {
|
|
15595
16565
|
try {
|