@hasna/todos 0.9.82 → 0.10.1
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 +1109 -14
- package/dist/db/agent-metrics.d.ts +34 -0
- package/dist/db/agent-metrics.d.ts.map +1 -0
- package/dist/db/agents.d.ts +21 -0
- package/dist/db/agents.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/kg.d.ts +70 -0
- package/dist/db/kg.d.ts.map +1 -0
- package/dist/db/patrol.d.ts +35 -0
- package/dist/db/patrol.d.ts.map +1 -0
- package/dist/db/task-relationships.d.ts +36 -0
- package/dist/db/task-relationships.d.ts.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +678 -5
- package/dist/lib/config.d.ts +15 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/mcp/index.js +1105 -12
- package/dist/server/index.js +128 -3
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -659,6 +659,44 @@ var MIGRATIONS = [
|
|
|
659
659
|
ALTER TABLE tasks ADD COLUMN assigned_from_project TEXT;
|
|
660
660
|
CREATE INDEX IF NOT EXISTS idx_tasks_assigned_by ON tasks(assigned_by);
|
|
661
661
|
INSERT OR IGNORE INTO _migrations (id) VALUES (26);
|
|
662
|
+
`,
|
|
663
|
+
`
|
|
664
|
+
CREATE TABLE IF NOT EXISTS task_relationships (
|
|
665
|
+
id TEXT PRIMARY KEY,
|
|
666
|
+
source_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
667
|
+
target_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
668
|
+
relationship_type TEXT NOT NULL CHECK(relationship_type IN ('related_to', 'conflicts_with', 'similar_to', 'duplicates', 'supersedes', 'modifies_same_file')),
|
|
669
|
+
metadata TEXT DEFAULT '{}',
|
|
670
|
+
created_by TEXT,
|
|
671
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
672
|
+
CHECK (source_task_id != target_task_id)
|
|
673
|
+
);
|
|
674
|
+
CREATE INDEX IF NOT EXISTS idx_task_rel_source ON task_relationships(source_task_id);
|
|
675
|
+
CREATE INDEX IF NOT EXISTS idx_task_rel_target ON task_relationships(target_task_id);
|
|
676
|
+
CREATE INDEX IF NOT EXISTS idx_task_rel_type ON task_relationships(relationship_type);
|
|
677
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (27);
|
|
678
|
+
`,
|
|
679
|
+
`
|
|
680
|
+
CREATE TABLE IF NOT EXISTS kg_edges (
|
|
681
|
+
id TEXT PRIMARY KEY,
|
|
682
|
+
source_id TEXT NOT NULL,
|
|
683
|
+
source_type TEXT NOT NULL,
|
|
684
|
+
target_id TEXT NOT NULL,
|
|
685
|
+
target_type TEXT NOT NULL,
|
|
686
|
+
relation_type TEXT NOT NULL,
|
|
687
|
+
weight REAL NOT NULL DEFAULT 1.0,
|
|
688
|
+
metadata TEXT DEFAULT '{}',
|
|
689
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
690
|
+
UNIQUE(source_id, source_type, target_id, target_type, relation_type)
|
|
691
|
+
);
|
|
692
|
+
CREATE INDEX IF NOT EXISTS idx_kg_source ON kg_edges(source_id, source_type);
|
|
693
|
+
CREATE INDEX IF NOT EXISTS idx_kg_target ON kg_edges(target_id, target_type);
|
|
694
|
+
CREATE INDEX IF NOT EXISTS idx_kg_relation ON kg_edges(relation_type);
|
|
695
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (28);
|
|
696
|
+
`,
|
|
697
|
+
`
|
|
698
|
+
ALTER TABLE agents ADD COLUMN capabilities TEXT DEFAULT '[]';
|
|
699
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (29);
|
|
662
700
|
`
|
|
663
701
|
];
|
|
664
702
|
var _db = null;
|
|
@@ -796,6 +834,30 @@ function ensureSchema(db) {
|
|
|
796
834
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
797
835
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
798
836
|
)`);
|
|
837
|
+
ensureTable("task_relationships", `
|
|
838
|
+
CREATE TABLE task_relationships (
|
|
839
|
+
id TEXT PRIMARY KEY,
|
|
840
|
+
source_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
841
|
+
target_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
842
|
+
relationship_type TEXT NOT NULL,
|
|
843
|
+
metadata TEXT DEFAULT '{}',
|
|
844
|
+
created_by TEXT,
|
|
845
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
846
|
+
CHECK (source_task_id != target_task_id)
|
|
847
|
+
)`);
|
|
848
|
+
ensureTable("kg_edges", `
|
|
849
|
+
CREATE TABLE kg_edges (
|
|
850
|
+
id TEXT PRIMARY KEY,
|
|
851
|
+
source_id TEXT NOT NULL,
|
|
852
|
+
source_type TEXT NOT NULL,
|
|
853
|
+
target_id TEXT NOT NULL,
|
|
854
|
+
target_type TEXT NOT NULL,
|
|
855
|
+
relation_type TEXT NOT NULL,
|
|
856
|
+
weight REAL NOT NULL DEFAULT 1.0,
|
|
857
|
+
metadata TEXT DEFAULT '{}',
|
|
858
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
859
|
+
UNIQUE(source_id, source_type, target_id, target_type, relation_type)
|
|
860
|
+
)`);
|
|
799
861
|
ensureColumn("projects", "task_list_id", "TEXT");
|
|
800
862
|
ensureColumn("projects", "task_prefix", "TEXT");
|
|
801
863
|
ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
|
|
@@ -820,6 +882,7 @@ function ensureSchema(db) {
|
|
|
820
882
|
ensureColumn("agents", "title", "TEXT");
|
|
821
883
|
ensureColumn("agents", "level", "TEXT");
|
|
822
884
|
ensureColumn("agents", "org_id", "TEXT");
|
|
885
|
+
ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
|
|
823
886
|
ensureColumn("projects", "org_id", "TEXT");
|
|
824
887
|
ensureColumn("plans", "task_list_id", "TEXT");
|
|
825
888
|
ensureColumn("plans", "agent_id", "TEXT");
|
|
@@ -846,6 +909,12 @@ function ensureSchema(db) {
|
|
|
846
909
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_project ON project_sources(project_id)");
|
|
847
910
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_type ON project_sources(type)");
|
|
848
911
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_assigned_by ON tasks(assigned_by)");
|
|
912
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_source ON task_relationships(source_task_id)");
|
|
913
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_target ON task_relationships(target_task_id)");
|
|
914
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_type ON task_relationships(relationship_type)");
|
|
915
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_source ON kg_edges(source_id, source_type)");
|
|
916
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_target ON kg_edges(target_id, target_type)");
|
|
917
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_relation ON kg_edges(relation_type)");
|
|
849
918
|
}
|
|
850
919
|
function backfillTaskTags(db) {
|
|
851
920
|
try {
|
|
@@ -2759,6 +2828,11 @@ function deleteComment(id, db) {
|
|
|
2759
2828
|
}
|
|
2760
2829
|
// src/db/agents.ts
|
|
2761
2830
|
var AGENT_ACTIVE_WINDOW_MS = 30 * 60 * 1000;
|
|
2831
|
+
function getAvailableNamesFromPool(pool, db) {
|
|
2832
|
+
const cutoff = new Date(Date.now() - AGENT_ACTIVE_WINDOW_MS).toISOString();
|
|
2833
|
+
const activeNames = new Set(db.query("SELECT name FROM agents WHERE last_seen_at > ?").all(cutoff).map((r) => r.name.toLowerCase()));
|
|
2834
|
+
return pool.filter((name) => !activeNames.has(name.toLowerCase()));
|
|
2835
|
+
}
|
|
2762
2836
|
function shortUuid() {
|
|
2763
2837
|
return crypto.randomUUID().slice(0, 8);
|
|
2764
2838
|
}
|
|
@@ -2766,12 +2840,31 @@ function rowToAgent(row) {
|
|
|
2766
2840
|
return {
|
|
2767
2841
|
...row,
|
|
2768
2842
|
permissions: JSON.parse(row.permissions || '["*"]'),
|
|
2843
|
+
capabilities: JSON.parse(row.capabilities || "[]"),
|
|
2769
2844
|
metadata: JSON.parse(row.metadata || "{}")
|
|
2770
2845
|
};
|
|
2771
2846
|
}
|
|
2772
2847
|
function registerAgent(input, db) {
|
|
2773
2848
|
const d = db || getDatabase();
|
|
2774
2849
|
const normalizedName = input.name.trim().toLowerCase();
|
|
2850
|
+
if (input.pool && input.pool.length > 0) {
|
|
2851
|
+
const poolLower = input.pool.map((n) => n.toLowerCase());
|
|
2852
|
+
if (!poolLower.includes(normalizedName)) {
|
|
2853
|
+
const available = getAvailableNamesFromPool(input.pool, d);
|
|
2854
|
+
const suggestion = available.length > 0 ? available[0] : null;
|
|
2855
|
+
return {
|
|
2856
|
+
conflict: true,
|
|
2857
|
+
pool_violation: true,
|
|
2858
|
+
existing_id: "",
|
|
2859
|
+
existing_name: normalizedName,
|
|
2860
|
+
last_seen_at: "",
|
|
2861
|
+
session_hint: null,
|
|
2862
|
+
working_dir: input.working_dir || null,
|
|
2863
|
+
suggestions: available.slice(0, 5),
|
|
2864
|
+
message: `"${normalizedName}" is not in this project's agent pool [${input.pool.join(", ")}]. ${available.length > 0 ? `Try: ${available.slice(0, 3).join(", ")}` : "No names are currently available \u2014 wait for an active agent to go stale."}${suggestion ? ` Suggested: ${suggestion}` : ""}`
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2775
2868
|
const existing = getAgentByName(normalizedName, d);
|
|
2776
2869
|
if (existing) {
|
|
2777
2870
|
const lastSeenMs = new Date(existing.last_seen_at).getTime();
|
|
@@ -2780,6 +2873,7 @@ function registerAgent(input, db) {
|
|
|
2780
2873
|
const differentSession = input.session_id && existing.session_id && input.session_id !== existing.session_id;
|
|
2781
2874
|
if (isActive && differentSession) {
|
|
2782
2875
|
const minutesAgo = Math.round((Date.now() - lastSeenMs) / 60000);
|
|
2876
|
+
const suggestions = input.pool ? getAvailableNamesFromPool(input.pool, d) : [];
|
|
2783
2877
|
return {
|
|
2784
2878
|
conflict: true,
|
|
2785
2879
|
existing_id: existing.id,
|
|
@@ -2787,7 +2881,8 @@ function registerAgent(input, db) {
|
|
|
2787
2881
|
last_seen_at: existing.last_seen_at,
|
|
2788
2882
|
session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
|
|
2789
2883
|
working_dir: existing.working_dir,
|
|
2790
|
-
|
|
2884
|
+
suggestions: suggestions.slice(0, 5),
|
|
2885
|
+
message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session ${existing.session_id?.slice(0, 8)}\u2026, dir: ${existing.working_dir ?? "unknown"}). Are you that agent? If so, pass session_id="${existing.session_id}" to reclaim it. Otherwise choose a different name.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
|
|
2791
2886
|
};
|
|
2792
2887
|
}
|
|
2793
2888
|
const updates = ["last_seen_at = ?"];
|
|
@@ -2810,8 +2905,8 @@ function registerAgent(input, db) {
|
|
|
2810
2905
|
}
|
|
2811
2906
|
const id = shortUuid();
|
|
2812
2907
|
const timestamp = now();
|
|
2813
|
-
d.run(`INSERT INTO agents (id, name, description, role, title, level, permissions, reports_to, org_id, metadata, created_at, last_seen_at, session_id, working_dir)
|
|
2814
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
2908
|
+
d.run(`INSERT INTO agents (id, name, description, role, title, level, permissions, capabilities, reports_to, org_id, metadata, created_at, last_seen_at, session_id, working_dir)
|
|
2909
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
2815
2910
|
id,
|
|
2816
2911
|
normalizedName,
|
|
2817
2912
|
input.description || null,
|
|
@@ -2819,6 +2914,7 @@ function registerAgent(input, db) {
|
|
|
2819
2914
|
input.title || null,
|
|
2820
2915
|
input.level || null,
|
|
2821
2916
|
JSON.stringify(input.permissions || ["*"]),
|
|
2917
|
+
JSON.stringify(input.capabilities || []),
|
|
2822
2918
|
input.reports_to || null,
|
|
2823
2919
|
input.org_id || null,
|
|
2824
2920
|
JSON.stringify(input.metadata || {}),
|
|
@@ -2874,6 +2970,10 @@ function updateAgent(id, input, db) {
|
|
|
2874
2970
|
sets.push("permissions = ?");
|
|
2875
2971
|
params.push(JSON.stringify(input.permissions));
|
|
2876
2972
|
}
|
|
2973
|
+
if (input.capabilities !== undefined) {
|
|
2974
|
+
sets.push("capabilities = ?");
|
|
2975
|
+
params.push(JSON.stringify(input.capabilities));
|
|
2976
|
+
}
|
|
2877
2977
|
if (input.title !== undefined) {
|
|
2878
2978
|
sets.push("title = ?");
|
|
2879
2979
|
params.push(input.title);
|
|
@@ -2921,6 +3021,28 @@ function getOrgChart(db) {
|
|
|
2921
3021
|
}
|
|
2922
3022
|
return buildTree(null);
|
|
2923
3023
|
}
|
|
3024
|
+
function matchCapabilities(agentCapabilities, requiredCapabilities) {
|
|
3025
|
+
if (requiredCapabilities.length === 0)
|
|
3026
|
+
return 1;
|
|
3027
|
+
if (agentCapabilities.length === 0)
|
|
3028
|
+
return 0;
|
|
3029
|
+
const agentSet = new Set(agentCapabilities.map((c) => c.toLowerCase()));
|
|
3030
|
+
let matches = 0;
|
|
3031
|
+
for (const req of requiredCapabilities) {
|
|
3032
|
+
if (agentSet.has(req.toLowerCase()))
|
|
3033
|
+
matches++;
|
|
3034
|
+
}
|
|
3035
|
+
return matches / requiredCapabilities.length;
|
|
3036
|
+
}
|
|
3037
|
+
function getCapableAgents(capabilities, opts, db) {
|
|
3038
|
+
const agents = listAgents(db);
|
|
3039
|
+
const minScore = opts?.min_score ?? 0.1;
|
|
3040
|
+
const scored = agents.map((agent) => ({
|
|
3041
|
+
agent,
|
|
3042
|
+
score: matchCapabilities(agent.capabilities, capabilities)
|
|
3043
|
+
})).filter((entry) => entry.score >= minScore).sort((a, b) => b.score - a.score);
|
|
3044
|
+
return opts?.limit ? scored.slice(0, opts.limit) : scored;
|
|
3045
|
+
}
|
|
2924
3046
|
// src/db/task-lists.ts
|
|
2925
3047
|
function rowToTaskList(row) {
|
|
2926
3048
|
return {
|
|
@@ -3241,8 +3363,537 @@ function deleteOrg(id, db) {
|
|
|
3241
3363
|
const d = db || getDatabase();
|
|
3242
3364
|
return d.run("DELETE FROM orgs WHERE id = ?", [id]).changes > 0;
|
|
3243
3365
|
}
|
|
3244
|
-
// src/
|
|
3366
|
+
// src/db/task-relationships.ts
|
|
3367
|
+
var RELATIONSHIP_TYPES = [
|
|
3368
|
+
"related_to",
|
|
3369
|
+
"conflicts_with",
|
|
3370
|
+
"similar_to",
|
|
3371
|
+
"duplicates",
|
|
3372
|
+
"supersedes",
|
|
3373
|
+
"modifies_same_file"
|
|
3374
|
+
];
|
|
3375
|
+
function rowToRelationship(row) {
|
|
3376
|
+
return {
|
|
3377
|
+
...row,
|
|
3378
|
+
relationship_type: row.relationship_type,
|
|
3379
|
+
metadata: JSON.parse(row.metadata || "{}")
|
|
3380
|
+
};
|
|
3381
|
+
}
|
|
3382
|
+
function addTaskRelationship(input, db) {
|
|
3383
|
+
const d = db || getDatabase();
|
|
3384
|
+
const id = uuid();
|
|
3385
|
+
const timestamp = now();
|
|
3386
|
+
if (input.source_task_id === input.target_task_id) {
|
|
3387
|
+
throw new Error("Cannot create a relationship between a task and itself");
|
|
3388
|
+
}
|
|
3389
|
+
const symmetric = ["related_to", "conflicts_with", "similar_to", "modifies_same_file"];
|
|
3390
|
+
if (symmetric.includes(input.relationship_type)) {
|
|
3391
|
+
const existing = d.query(`SELECT id FROM task_relationships
|
|
3392
|
+
WHERE relationship_type = ?
|
|
3393
|
+
AND ((source_task_id = ? AND target_task_id = ?) OR (source_task_id = ? AND target_task_id = ?))`).get(input.relationship_type, input.source_task_id, input.target_task_id, input.target_task_id, input.source_task_id);
|
|
3394
|
+
if (existing) {
|
|
3395
|
+
return getTaskRelationship(existing.id, d);
|
|
3396
|
+
}
|
|
3397
|
+
} else {
|
|
3398
|
+
const existing = d.query("SELECT id FROM task_relationships WHERE source_task_id = ? AND target_task_id = ? AND relationship_type = ?").get(input.source_task_id, input.target_task_id, input.relationship_type);
|
|
3399
|
+
if (existing) {
|
|
3400
|
+
return getTaskRelationship(existing.id, d);
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
d.run(`INSERT INTO task_relationships (id, source_task_id, target_task_id, relationship_type, metadata, created_by, created_at)
|
|
3404
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
|
|
3405
|
+
id,
|
|
3406
|
+
input.source_task_id,
|
|
3407
|
+
input.target_task_id,
|
|
3408
|
+
input.relationship_type,
|
|
3409
|
+
JSON.stringify(input.metadata || {}),
|
|
3410
|
+
input.created_by || null,
|
|
3411
|
+
timestamp
|
|
3412
|
+
]);
|
|
3413
|
+
return getTaskRelationship(id, d);
|
|
3414
|
+
}
|
|
3415
|
+
function getTaskRelationship(id, db) {
|
|
3416
|
+
const d = db || getDatabase();
|
|
3417
|
+
const row = d.query("SELECT * FROM task_relationships WHERE id = ?").get(id);
|
|
3418
|
+
return row ? rowToRelationship(row) : null;
|
|
3419
|
+
}
|
|
3420
|
+
function removeTaskRelationship(id, db) {
|
|
3421
|
+
const d = db || getDatabase();
|
|
3422
|
+
return d.run("DELETE FROM task_relationships WHERE id = ?", [id]).changes > 0;
|
|
3423
|
+
}
|
|
3424
|
+
function removeTaskRelationshipByPair(sourceTaskId, targetTaskId, relationshipType, db) {
|
|
3425
|
+
const d = db || getDatabase();
|
|
3426
|
+
const symmetric = ["related_to", "conflicts_with", "similar_to", "modifies_same_file"];
|
|
3427
|
+
if (symmetric.includes(relationshipType)) {
|
|
3428
|
+
return d.run(`DELETE FROM task_relationships
|
|
3429
|
+
WHERE relationship_type = ?
|
|
3430
|
+
AND ((source_task_id = ? AND target_task_id = ?) OR (source_task_id = ? AND target_task_id = ?))`, [relationshipType, sourceTaskId, targetTaskId, targetTaskId, sourceTaskId]).changes > 0;
|
|
3431
|
+
}
|
|
3432
|
+
return d.run("DELETE FROM task_relationships WHERE source_task_id = ? AND target_task_id = ? AND relationship_type = ?", [sourceTaskId, targetTaskId, relationshipType]).changes > 0;
|
|
3433
|
+
}
|
|
3434
|
+
function getTaskRelationships(taskId, relationshipType, db) {
|
|
3435
|
+
const d = db || getDatabase();
|
|
3436
|
+
let sql = "SELECT * FROM task_relationships WHERE (source_task_id = ? OR target_task_id = ?)";
|
|
3437
|
+
const params = [taskId, taskId];
|
|
3438
|
+
if (relationshipType) {
|
|
3439
|
+
sql += " AND relationship_type = ?";
|
|
3440
|
+
params.push(relationshipType);
|
|
3441
|
+
}
|
|
3442
|
+
sql += " ORDER BY created_at DESC";
|
|
3443
|
+
return d.query(sql).all(...params).map(rowToRelationship);
|
|
3444
|
+
}
|
|
3445
|
+
function findRelatedTaskIds(taskId, relationshipType, db) {
|
|
3446
|
+
const rels = getTaskRelationships(taskId, relationshipType, db);
|
|
3447
|
+
const ids = new Set;
|
|
3448
|
+
for (const rel of rels) {
|
|
3449
|
+
if (rel.source_task_id === taskId)
|
|
3450
|
+
ids.add(rel.target_task_id);
|
|
3451
|
+
else
|
|
3452
|
+
ids.add(rel.source_task_id);
|
|
3453
|
+
}
|
|
3454
|
+
return [...ids];
|
|
3455
|
+
}
|
|
3456
|
+
function autoDetectFileRelationships(taskId, db) {
|
|
3457
|
+
const d = db || getDatabase();
|
|
3458
|
+
const files = d.query("SELECT path FROM task_files WHERE task_id = ? AND status != 'removed'").all(taskId);
|
|
3459
|
+
const created = [];
|
|
3460
|
+
for (const file of files) {
|
|
3461
|
+
const others = d.query("SELECT DISTINCT task_id FROM task_files WHERE path = ? AND task_id != ? AND status != 'removed'").all(file.path, taskId);
|
|
3462
|
+
for (const other of others) {
|
|
3463
|
+
try {
|
|
3464
|
+
const rel = addTaskRelationship({
|
|
3465
|
+
source_task_id: taskId,
|
|
3466
|
+
target_task_id: other.task_id,
|
|
3467
|
+
relationship_type: "modifies_same_file",
|
|
3468
|
+
metadata: { shared_file: file.path }
|
|
3469
|
+
}, d);
|
|
3470
|
+
created.push(rel);
|
|
3471
|
+
} catch {}
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
return created;
|
|
3475
|
+
}
|
|
3476
|
+
// src/db/kg.ts
|
|
3477
|
+
function rowToEdge(row) {
|
|
3478
|
+
return {
|
|
3479
|
+
...row,
|
|
3480
|
+
metadata: JSON.parse(row.metadata || "{}")
|
|
3481
|
+
};
|
|
3482
|
+
}
|
|
3483
|
+
function syncKgEdges(db) {
|
|
3484
|
+
const d = db || getDatabase();
|
|
3485
|
+
let synced = 0;
|
|
3486
|
+
const tx = d.transaction(() => {
|
|
3487
|
+
const deps = d.query("SELECT task_id, depends_on FROM task_dependencies").all();
|
|
3488
|
+
for (const dep of deps) {
|
|
3489
|
+
synced += upsertEdge(d, dep.task_id, "task", dep.depends_on, "task", "depends_on");
|
|
3490
|
+
}
|
|
3491
|
+
const assignments = d.query("SELECT id, assigned_to FROM tasks WHERE assigned_to IS NOT NULL").all();
|
|
3492
|
+
for (const a of assignments) {
|
|
3493
|
+
synced += upsertEdge(d, a.id, "task", a.assigned_to, "agent", "assigned_to");
|
|
3494
|
+
}
|
|
3495
|
+
const agents = d.query("SELECT id, reports_to FROM agents WHERE reports_to IS NOT NULL").all();
|
|
3496
|
+
for (const a of agents) {
|
|
3497
|
+
synced += upsertEdge(d, a.id, "agent", a.reports_to, "agent", "reports_to");
|
|
3498
|
+
}
|
|
3499
|
+
const files = d.query("SELECT task_id, path FROM task_files WHERE status != 'removed'").all();
|
|
3500
|
+
for (const f of files) {
|
|
3501
|
+
synced += upsertEdge(d, f.task_id, "task", f.path, "file", "references_file");
|
|
3502
|
+
}
|
|
3503
|
+
const taskProjects = d.query("SELECT id, project_id FROM tasks WHERE project_id IS NOT NULL").all();
|
|
3504
|
+
for (const tp of taskProjects) {
|
|
3505
|
+
synced += upsertEdge(d, tp.id, "task", tp.project_id, "project", "in_project");
|
|
3506
|
+
}
|
|
3507
|
+
const taskPlans = d.query("SELECT id, plan_id FROM tasks WHERE plan_id IS NOT NULL").all();
|
|
3508
|
+
for (const tp of taskPlans) {
|
|
3509
|
+
synced += upsertEdge(d, tp.id, "task", tp.plan_id, "plan", "in_plan");
|
|
3510
|
+
}
|
|
3511
|
+
try {
|
|
3512
|
+
const rels = d.query("SELECT source_task_id, target_task_id, relationship_type FROM task_relationships").all();
|
|
3513
|
+
for (const r of rels) {
|
|
3514
|
+
synced += upsertEdge(d, r.source_task_id, "task", r.target_task_id, "task", r.relationship_type);
|
|
3515
|
+
}
|
|
3516
|
+
} catch {}
|
|
3517
|
+
});
|
|
3518
|
+
tx();
|
|
3519
|
+
return { synced };
|
|
3520
|
+
}
|
|
3521
|
+
function upsertEdge(d, sourceId, sourceType, targetId, targetType, relationType, weight = 1) {
|
|
3522
|
+
try {
|
|
3523
|
+
d.run(`INSERT OR IGNORE INTO kg_edges (id, source_id, source_type, target_id, target_type, relation_type, weight, metadata, created_at)
|
|
3524
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, '{}', ?)`, [uuid(), sourceId, sourceType, targetId, targetType, relationType, weight, now()]);
|
|
3525
|
+
return 1;
|
|
3526
|
+
} catch {
|
|
3527
|
+
return 0;
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
function getRelated(entityId, opts, db) {
|
|
3531
|
+
const d = db || getDatabase();
|
|
3532
|
+
const direction = opts?.direction || "both";
|
|
3533
|
+
const conditions = [];
|
|
3534
|
+
const params = [];
|
|
3535
|
+
if (direction === "outgoing" || direction === "both") {
|
|
3536
|
+
conditions.push("source_id = ?");
|
|
3537
|
+
params.push(entityId);
|
|
3538
|
+
}
|
|
3539
|
+
if (direction === "incoming" || direction === "both") {
|
|
3540
|
+
conditions.push("target_id = ?");
|
|
3541
|
+
params.push(entityId);
|
|
3542
|
+
}
|
|
3543
|
+
let sql = `SELECT * FROM kg_edges WHERE (${conditions.join(" OR ")})`;
|
|
3544
|
+
if (opts?.relation_type) {
|
|
3545
|
+
sql += " AND relation_type = ?";
|
|
3546
|
+
params.push(opts.relation_type);
|
|
3547
|
+
}
|
|
3548
|
+
if (opts?.entity_type) {
|
|
3549
|
+
sql += " AND (source_type = ? OR target_type = ?)";
|
|
3550
|
+
params.push(opts.entity_type, opts.entity_type);
|
|
3551
|
+
}
|
|
3552
|
+
sql += " ORDER BY weight DESC, created_at DESC";
|
|
3553
|
+
if (opts?.limit) {
|
|
3554
|
+
sql += " LIMIT ?";
|
|
3555
|
+
params.push(opts.limit);
|
|
3556
|
+
}
|
|
3557
|
+
return d.query(sql).all(...params).map(rowToEdge);
|
|
3558
|
+
}
|
|
3559
|
+
function findPath(sourceId, targetId, opts, db) {
|
|
3560
|
+
const d = db || getDatabase();
|
|
3561
|
+
const maxDepth = opts?.max_depth || 5;
|
|
3562
|
+
const visited = new Set;
|
|
3563
|
+
const queue = [{ id: sourceId, path: [] }];
|
|
3564
|
+
const results = [];
|
|
3565
|
+
visited.add(sourceId);
|
|
3566
|
+
while (queue.length > 0) {
|
|
3567
|
+
const current = queue.shift();
|
|
3568
|
+
if (current.path.length >= maxDepth)
|
|
3569
|
+
continue;
|
|
3570
|
+
let sql = "SELECT * FROM kg_edges WHERE source_id = ?";
|
|
3571
|
+
const params = [current.id];
|
|
3572
|
+
if (opts?.relation_types && opts.relation_types.length > 0) {
|
|
3573
|
+
const placeholders = opts.relation_types.map(() => "?").join(",");
|
|
3574
|
+
sql += ` AND relation_type IN (${placeholders})`;
|
|
3575
|
+
params.push(...opts.relation_types);
|
|
3576
|
+
}
|
|
3577
|
+
const edges = d.query(sql).all(...params).map(rowToEdge);
|
|
3578
|
+
for (const edge of edges) {
|
|
3579
|
+
const nextId = edge.target_id;
|
|
3580
|
+
const newPath = [...current.path, edge];
|
|
3581
|
+
if (nextId === targetId) {
|
|
3582
|
+
results.push(newPath);
|
|
3583
|
+
if (results.length >= 3)
|
|
3584
|
+
return results;
|
|
3585
|
+
continue;
|
|
3586
|
+
}
|
|
3587
|
+
if (!visited.has(nextId)) {
|
|
3588
|
+
visited.add(nextId);
|
|
3589
|
+
queue.push({ id: nextId, path: newPath });
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
return results;
|
|
3594
|
+
}
|
|
3595
|
+
function getImpactAnalysis(entityId, opts, db) {
|
|
3596
|
+
const d = db || getDatabase();
|
|
3597
|
+
const maxDepth = opts?.max_depth || 3;
|
|
3598
|
+
const results = [];
|
|
3599
|
+
const visited = new Set;
|
|
3600
|
+
visited.add(entityId);
|
|
3601
|
+
const queue = [{ id: entityId, depth: 0 }];
|
|
3602
|
+
while (queue.length > 0) {
|
|
3603
|
+
const current = queue.shift();
|
|
3604
|
+
if (current.depth >= maxDepth)
|
|
3605
|
+
continue;
|
|
3606
|
+
let sql = "SELECT * FROM kg_edges WHERE source_id = ?";
|
|
3607
|
+
const params = [current.id];
|
|
3608
|
+
if (opts?.relation_types && opts.relation_types.length > 0) {
|
|
3609
|
+
const placeholders = opts.relation_types.map(() => "?").join(",");
|
|
3610
|
+
sql += ` AND relation_type IN (${placeholders})`;
|
|
3611
|
+
params.push(...opts.relation_types);
|
|
3612
|
+
}
|
|
3613
|
+
const edges = d.query(sql).all(...params).map(rowToEdge);
|
|
3614
|
+
for (const edge of edges) {
|
|
3615
|
+
if (!visited.has(edge.target_id)) {
|
|
3616
|
+
visited.add(edge.target_id);
|
|
3617
|
+
results.push({
|
|
3618
|
+
entity_id: edge.target_id,
|
|
3619
|
+
entity_type: edge.target_type,
|
|
3620
|
+
depth: current.depth + 1,
|
|
3621
|
+
relation: edge.relation_type
|
|
3622
|
+
});
|
|
3623
|
+
queue.push({ id: edge.target_id, depth: current.depth + 1 });
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
return results;
|
|
3628
|
+
}
|
|
3629
|
+
function getCriticalPath(opts, db) {
|
|
3630
|
+
const d = db || getDatabase();
|
|
3631
|
+
let sql = `SELECT source_id, target_id FROM kg_edges WHERE relation_type = 'depends_on'`;
|
|
3632
|
+
const params = [];
|
|
3633
|
+
if (opts?.project_id) {
|
|
3634
|
+
sql += ` AND source_id IN (SELECT id FROM tasks WHERE project_id = ?)`;
|
|
3635
|
+
params.push(opts.project_id);
|
|
3636
|
+
}
|
|
3637
|
+
const edges = d.query(sql).all(...params);
|
|
3638
|
+
const blocks = new Map;
|
|
3639
|
+
for (const e of edges) {
|
|
3640
|
+
if (!blocks.has(e.target_id))
|
|
3641
|
+
blocks.set(e.target_id, new Set);
|
|
3642
|
+
blocks.get(e.target_id).add(e.source_id);
|
|
3643
|
+
}
|
|
3644
|
+
const results = [];
|
|
3645
|
+
for (const [taskId] of blocks) {
|
|
3646
|
+
const visited = new Set;
|
|
3647
|
+
const q = [taskId];
|
|
3648
|
+
let maxDepth = 0;
|
|
3649
|
+
let depth = 0;
|
|
3650
|
+
let levelSize = q.length;
|
|
3651
|
+
while (q.length > 0) {
|
|
3652
|
+
const node = q.shift();
|
|
3653
|
+
levelSize--;
|
|
3654
|
+
const downstream = blocks.get(node);
|
|
3655
|
+
if (downstream) {
|
|
3656
|
+
for (const d2 of downstream) {
|
|
3657
|
+
if (!visited.has(d2)) {
|
|
3658
|
+
visited.add(d2);
|
|
3659
|
+
q.push(d2);
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
if (levelSize === 0) {
|
|
3664
|
+
depth++;
|
|
3665
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
3666
|
+
levelSize = q.length;
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
if (visited.size > 0) {
|
|
3670
|
+
results.push({ task_id: taskId, blocking_count: visited.size, depth: maxDepth });
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
results.sort((a, b) => b.blocking_count - a.blocking_count);
|
|
3674
|
+
return results.slice(0, opts?.limit || 20);
|
|
3675
|
+
}
|
|
3676
|
+
function addKgEdge(sourceId, sourceType, targetId, targetType, relationType, weight = 1, metadata, db) {
|
|
3677
|
+
const d = db || getDatabase();
|
|
3678
|
+
const id = uuid();
|
|
3679
|
+
const timestamp = now();
|
|
3680
|
+
d.run(`INSERT OR IGNORE INTO kg_edges (id, source_id, source_type, target_id, target_type, relation_type, weight, metadata, created_at)
|
|
3681
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, sourceId, sourceType, targetId, targetType, relationType, weight, JSON.stringify(metadata || {}), timestamp]);
|
|
3682
|
+
return { id, source_id: sourceId, source_type: sourceType, target_id: targetId, target_type: targetType, relation_type: relationType, weight, metadata: metadata || {}, created_at: timestamp };
|
|
3683
|
+
}
|
|
3684
|
+
function removeKgEdges(sourceId, targetId, relationType, db) {
|
|
3685
|
+
const d = db || getDatabase();
|
|
3686
|
+
if (relationType) {
|
|
3687
|
+
return d.run("DELETE FROM kg_edges WHERE source_id = ? AND target_id = ? AND relation_type = ?", [sourceId, targetId, relationType]).changes;
|
|
3688
|
+
}
|
|
3689
|
+
return d.run("DELETE FROM kg_edges WHERE source_id = ? AND target_id = ?", [sourceId, targetId]).changes;
|
|
3690
|
+
}
|
|
3691
|
+
// src/db/patrol.ts
|
|
3245
3692
|
function rowToTask2(row) {
|
|
3693
|
+
return {
|
|
3694
|
+
...row,
|
|
3695
|
+
status: row.status,
|
|
3696
|
+
priority: row.priority,
|
|
3697
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
3698
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
3699
|
+
requires_approval: Boolean(row.requires_approval)
|
|
3700
|
+
};
|
|
3701
|
+
}
|
|
3702
|
+
function patrolTasks(opts, db) {
|
|
3703
|
+
const d = db || getDatabase();
|
|
3704
|
+
const stuckMinutes = opts?.stuck_minutes || 60;
|
|
3705
|
+
const confidenceThreshold = opts?.confidence_threshold || 0.5;
|
|
3706
|
+
const issues = [];
|
|
3707
|
+
let projectFilter = "";
|
|
3708
|
+
const projectParams = [];
|
|
3709
|
+
if (opts?.project_id) {
|
|
3710
|
+
projectFilter = " AND project_id = ?";
|
|
3711
|
+
projectParams.push(opts.project_id);
|
|
3712
|
+
}
|
|
3713
|
+
const stuckCutoff = new Date(Date.now() - stuckMinutes * 60 * 1000).toISOString();
|
|
3714
|
+
const stuckRows = d.query(`SELECT * FROM tasks WHERE status = 'in_progress' AND updated_at < ?${projectFilter} ORDER BY updated_at ASC`).all(stuckCutoff, ...projectParams);
|
|
3715
|
+
for (const row of stuckRows) {
|
|
3716
|
+
const task = rowToTask2(row);
|
|
3717
|
+
const minutesStuck = Math.round((Date.now() - new Date(task.updated_at).getTime()) / 60000);
|
|
3718
|
+
issues.push({
|
|
3719
|
+
type: "stuck",
|
|
3720
|
+
task_id: task.id,
|
|
3721
|
+
task_title: task.title,
|
|
3722
|
+
severity: minutesStuck > 480 ? "critical" : minutesStuck > 120 ? "high" : "medium",
|
|
3723
|
+
detail: `In progress for ${minutesStuck} minutes without update`
|
|
3724
|
+
});
|
|
3725
|
+
}
|
|
3726
|
+
const lowConfRows = d.query(`SELECT * FROM tasks WHERE status = 'completed' AND confidence IS NOT NULL AND confidence < ?${projectFilter} ORDER BY confidence ASC`).all(confidenceThreshold, ...projectParams);
|
|
3727
|
+
for (const row of lowConfRows) {
|
|
3728
|
+
const task = rowToTask2(row);
|
|
3729
|
+
issues.push({
|
|
3730
|
+
type: "low_confidence",
|
|
3731
|
+
task_id: task.id,
|
|
3732
|
+
task_title: task.title,
|
|
3733
|
+
severity: (task.confidence ?? 0) < 0.3 ? "high" : "medium",
|
|
3734
|
+
detail: `Completed with confidence ${task.confidence} (threshold: ${confidenceThreshold})`
|
|
3735
|
+
});
|
|
3736
|
+
}
|
|
3737
|
+
const orphanedRows = d.query(`SELECT * FROM tasks WHERE status = 'pending' AND project_id IS NULL AND agent_id IS NULL AND assigned_to IS NULL ORDER BY created_at ASC`).all();
|
|
3738
|
+
for (const row of orphanedRows) {
|
|
3739
|
+
const task = rowToTask2(row);
|
|
3740
|
+
issues.push({
|
|
3741
|
+
type: "orphaned",
|
|
3742
|
+
task_id: task.id,
|
|
3743
|
+
task_title: task.title,
|
|
3744
|
+
severity: "low",
|
|
3745
|
+
detail: "Pending task with no project, no agent, and no assignee"
|
|
3746
|
+
});
|
|
3747
|
+
}
|
|
3748
|
+
const needsReviewRows = d.query(`SELECT * FROM tasks WHERE status = 'completed' AND requires_approval = 1 AND approved_by IS NULL${projectFilter} ORDER BY completed_at DESC`).all(...projectParams);
|
|
3749
|
+
for (const row of needsReviewRows) {
|
|
3750
|
+
const task = rowToTask2(row);
|
|
3751
|
+
issues.push({
|
|
3752
|
+
type: "needs_review",
|
|
3753
|
+
task_id: task.id,
|
|
3754
|
+
task_title: task.title,
|
|
3755
|
+
severity: "medium",
|
|
3756
|
+
detail: "Completed but requires approval \u2014 not yet reviewed"
|
|
3757
|
+
});
|
|
3758
|
+
}
|
|
3759
|
+
const pendingWithDeps = d.query(`SELECT t.* FROM tasks t
|
|
3760
|
+
WHERE t.status = 'pending'${projectFilter}
|
|
3761
|
+
AND t.id IN (SELECT task_id FROM task_dependencies)`).all(...projectParams);
|
|
3762
|
+
for (const row of pendingWithDeps) {
|
|
3763
|
+
const deps = d.query(`SELECT d.depends_on, t.status FROM task_dependencies d
|
|
3764
|
+
JOIN tasks t ON t.id = d.depends_on
|
|
3765
|
+
WHERE d.task_id = ?`).all(row.id);
|
|
3766
|
+
if (deps.length > 0 && deps.every((dep) => dep.status === "failed" || dep.status === "cancelled")) {
|
|
3767
|
+
const task = rowToTask2(row);
|
|
3768
|
+
issues.push({
|
|
3769
|
+
type: "zombie_blocked",
|
|
3770
|
+
task_id: task.id,
|
|
3771
|
+
task_title: task.title,
|
|
3772
|
+
severity: "high",
|
|
3773
|
+
detail: `Blocked by ${deps.length} task(s) that are all failed/cancelled \u2014 will never unblock`
|
|
3774
|
+
});
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
3778
|
+
issues.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
3779
|
+
return {
|
|
3780
|
+
issues,
|
|
3781
|
+
total_issues: issues.length,
|
|
3782
|
+
scanned_at: new Date().toISOString()
|
|
3783
|
+
};
|
|
3784
|
+
}
|
|
3785
|
+
function getReviewQueue(opts, db) {
|
|
3786
|
+
const d = db || getDatabase();
|
|
3787
|
+
let sql = `SELECT * FROM tasks WHERE status = 'completed' AND (
|
|
3788
|
+
(requires_approval = 1 AND approved_by IS NULL)
|
|
3789
|
+
OR (confidence IS NOT NULL AND confidence < 0.5)
|
|
3790
|
+
)`;
|
|
3791
|
+
const params = [];
|
|
3792
|
+
if (opts?.project_id) {
|
|
3793
|
+
sql += " AND project_id = ?";
|
|
3794
|
+
params.push(opts.project_id);
|
|
3795
|
+
}
|
|
3796
|
+
sql += " ORDER BY CASE WHEN requires_approval = 1 AND approved_by IS NULL THEN 0 ELSE 1 END, confidence ASC";
|
|
3797
|
+
if (opts?.limit) {
|
|
3798
|
+
sql += " LIMIT ?";
|
|
3799
|
+
params.push(opts.limit);
|
|
3800
|
+
}
|
|
3801
|
+
return d.query(sql).all(...params).map(rowToTask2);
|
|
3802
|
+
}
|
|
3803
|
+
// src/db/agent-metrics.ts
|
|
3804
|
+
function getAgentMetrics(agentId, opts, db) {
|
|
3805
|
+
const d = db || getDatabase();
|
|
3806
|
+
const agent = d.query("SELECT id, name FROM agents WHERE id = ? OR LOWER(name) = LOWER(?)").get(agentId, agentId);
|
|
3807
|
+
if (!agent)
|
|
3808
|
+
return null;
|
|
3809
|
+
let projectFilter = "";
|
|
3810
|
+
const params = [agent.id, agent.id];
|
|
3811
|
+
if (opts?.project_id) {
|
|
3812
|
+
projectFilter = " AND project_id = ?";
|
|
3813
|
+
params.push(opts.project_id);
|
|
3814
|
+
}
|
|
3815
|
+
const completed = d.query(`SELECT COUNT(*) as count FROM tasks WHERE (agent_id = ? OR assigned_to = ?) AND status = 'completed'${projectFilter}`).get(...params).count;
|
|
3816
|
+
const failed = d.query(`SELECT COUNT(*) as count FROM tasks WHERE (agent_id = ? OR assigned_to = ?) AND status = 'failed'${projectFilter}`).get(...params).count;
|
|
3817
|
+
const inProgress = d.query(`SELECT COUNT(*) as count FROM tasks WHERE (agent_id = ? OR assigned_to = ?) AND status = 'in_progress'${projectFilter}`).get(...params).count;
|
|
3818
|
+
const total = completed + failed;
|
|
3819
|
+
const completionRate = total > 0 ? completed / total : 0;
|
|
3820
|
+
const avgTime = d.query(`SELECT AVG(
|
|
3821
|
+
(julianday(completed_at) - julianday(created_at)) * 24 * 60
|
|
3822
|
+
) as avg_minutes
|
|
3823
|
+
FROM tasks
|
|
3824
|
+
WHERE (agent_id = ? OR assigned_to = ?) AND status = 'completed' AND completed_at IS NOT NULL${projectFilter}`).get(...params);
|
|
3825
|
+
const avgConf = d.query(`SELECT AVG(confidence) as avg_confidence
|
|
3826
|
+
FROM tasks
|
|
3827
|
+
WHERE (agent_id = ? OR assigned_to = ?) AND status = 'completed' AND confidence IS NOT NULL${projectFilter}`).get(...params);
|
|
3828
|
+
const reviewTasks = d.query(`SELECT metadata FROM tasks
|
|
3829
|
+
WHERE (agent_id = ? OR assigned_to = ?) AND status = 'completed'${projectFilter}
|
|
3830
|
+
AND metadata LIKE '%_review_score%'`).all(...params);
|
|
3831
|
+
let reviewScoreAvg = null;
|
|
3832
|
+
if (reviewTasks.length > 0) {
|
|
3833
|
+
let total2 = 0;
|
|
3834
|
+
let count = 0;
|
|
3835
|
+
for (const row of reviewTasks) {
|
|
3836
|
+
try {
|
|
3837
|
+
const meta = JSON.parse(row.metadata);
|
|
3838
|
+
if (typeof meta._review_score === "number") {
|
|
3839
|
+
total2 += meta._review_score;
|
|
3840
|
+
count++;
|
|
3841
|
+
}
|
|
3842
|
+
} catch {}
|
|
3843
|
+
}
|
|
3844
|
+
if (count > 0)
|
|
3845
|
+
reviewScoreAvg = total2 / count;
|
|
3846
|
+
}
|
|
3847
|
+
const speedScore = avgTime?.avg_minutes != null ? Math.max(0, 1 - avgTime.avg_minutes / (60 * 24)) : 0.5;
|
|
3848
|
+
const confidenceScore = avgConf?.avg_confidence ?? 0.5;
|
|
3849
|
+
const volumeScore = Math.min(1, completed / 50);
|
|
3850
|
+
const compositeScore = completionRate * 0.3 + speedScore * 0.2 + confidenceScore * 0.3 + volumeScore * 0.2;
|
|
3851
|
+
return {
|
|
3852
|
+
agent_id: agent.id,
|
|
3853
|
+
agent_name: agent.name,
|
|
3854
|
+
tasks_completed: completed,
|
|
3855
|
+
tasks_failed: failed,
|
|
3856
|
+
tasks_in_progress: inProgress,
|
|
3857
|
+
completion_rate: Math.round(completionRate * 1000) / 1000,
|
|
3858
|
+
avg_completion_minutes: avgTime?.avg_minutes != null ? Math.round(avgTime.avg_minutes * 10) / 10 : null,
|
|
3859
|
+
avg_confidence: avgConf?.avg_confidence != null ? Math.round(avgConf.avg_confidence * 1000) / 1000 : null,
|
|
3860
|
+
review_score_avg: reviewScoreAvg != null ? Math.round(reviewScoreAvg * 1000) / 1000 : null,
|
|
3861
|
+
composite_score: Math.round(compositeScore * 1000) / 1000
|
|
3862
|
+
};
|
|
3863
|
+
}
|
|
3864
|
+
function getLeaderboard(opts, db) {
|
|
3865
|
+
const d = db || getDatabase();
|
|
3866
|
+
const agents = d.query("SELECT id FROM agents ORDER BY name").all();
|
|
3867
|
+
const entries = [];
|
|
3868
|
+
for (const agent of agents) {
|
|
3869
|
+
const metrics = getAgentMetrics(agent.id, { project_id: opts?.project_id }, d);
|
|
3870
|
+
if (metrics && (metrics.tasks_completed > 0 || metrics.tasks_failed > 0 || metrics.tasks_in_progress > 0)) {
|
|
3871
|
+
entries.push(metrics);
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
entries.sort((a, b) => b.composite_score - a.composite_score);
|
|
3875
|
+
const limit = opts?.limit || 20;
|
|
3876
|
+
return entries.slice(0, limit).map((entry, idx) => ({
|
|
3877
|
+
...entry,
|
|
3878
|
+
rank: idx + 1
|
|
3879
|
+
}));
|
|
3880
|
+
}
|
|
3881
|
+
function scoreTask(taskId, score, reviewerId, db) {
|
|
3882
|
+
const d = db || getDatabase();
|
|
3883
|
+
if (score < 0 || score > 1)
|
|
3884
|
+
throw new Error("Score must be between 0 and 1");
|
|
3885
|
+
const task = d.query("SELECT metadata FROM tasks WHERE id = ?").get(taskId);
|
|
3886
|
+
if (!task)
|
|
3887
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
3888
|
+
const metadata = JSON.parse(task.metadata || "{}");
|
|
3889
|
+
metadata._review_score = score;
|
|
3890
|
+
if (reviewerId)
|
|
3891
|
+
metadata._reviewed_by = reviewerId;
|
|
3892
|
+
metadata._reviewed_at = now();
|
|
3893
|
+
d.run("UPDATE tasks SET metadata = ?, updated_at = ? WHERE id = ?", [JSON.stringify(metadata), now(), taskId]);
|
|
3894
|
+
}
|
|
3895
|
+
// src/lib/search.ts
|
|
3896
|
+
function rowToTask3(row) {
|
|
3246
3897
|
return {
|
|
3247
3898
|
...row,
|
|
3248
3899
|
tags: JSON.parse(row.tags || "[]"),
|
|
@@ -3342,7 +3993,7 @@ function searchTasks(options, projectId, taskListId, db) {
|
|
|
3342
3993
|
t.created_at DESC`;
|
|
3343
3994
|
}
|
|
3344
3995
|
const rows = d.query(sql).all(...params);
|
|
3345
|
-
return rows.map(
|
|
3996
|
+
return rows.map(rowToTask3);
|
|
3346
3997
|
}
|
|
3347
3998
|
// src/lib/claude-tasks.ts
|
|
3348
3999
|
import { existsSync as existsSync4, readFileSync as readFileSync2, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
@@ -3876,25 +4527,32 @@ export {
|
|
|
3876
4527
|
taskFromTemplate,
|
|
3877
4528
|
syncWithAgents,
|
|
3878
4529
|
syncWithAgent,
|
|
4530
|
+
syncKgEdges,
|
|
3879
4531
|
startTask,
|
|
3880
4532
|
slugify,
|
|
3881
4533
|
setTaskStatus,
|
|
3882
4534
|
setTaskPriority,
|
|
3883
4535
|
searchTasks,
|
|
4536
|
+
scoreTask,
|
|
3884
4537
|
resolvePartialId,
|
|
3885
4538
|
resetDatabase,
|
|
4539
|
+
removeTaskRelationshipByPair,
|
|
4540
|
+
removeTaskRelationship,
|
|
3886
4541
|
removeTaskFile,
|
|
3887
4542
|
removeProjectSource,
|
|
4543
|
+
removeKgEdges,
|
|
3888
4544
|
removeDependency,
|
|
3889
4545
|
removeChecklistItem,
|
|
3890
4546
|
releaseLock,
|
|
3891
4547
|
registerAgent,
|
|
3892
4548
|
redistributeStaleTasks,
|
|
4549
|
+
patrolTasks,
|
|
3893
4550
|
parseRecurrenceRule,
|
|
3894
4551
|
now,
|
|
3895
4552
|
nextTaskShortId,
|
|
3896
4553
|
nextOccurrence,
|
|
3897
4554
|
moveTask,
|
|
4555
|
+
matchCapabilities,
|
|
3898
4556
|
logTaskChange,
|
|
3899
4557
|
logProgress,
|
|
3900
4558
|
lockTask,
|
|
@@ -3919,6 +4577,8 @@ export {
|
|
|
3919
4577
|
getTasksChangedSince,
|
|
3920
4578
|
getTaskWithRelations,
|
|
3921
4579
|
getTaskStats,
|
|
4580
|
+
getTaskRelationships,
|
|
4581
|
+
getTaskRelationship,
|
|
3922
4582
|
getTaskListBySlug,
|
|
3923
4583
|
getTaskList,
|
|
3924
4584
|
getTaskHistory,
|
|
@@ -3930,6 +4590,8 @@ export {
|
|
|
3930
4590
|
getStatus,
|
|
3931
4591
|
getStaleTasks,
|
|
3932
4592
|
getSession,
|
|
4593
|
+
getReviewQueue,
|
|
4594
|
+
getRelated,
|
|
3933
4595
|
getRecentActivity,
|
|
3934
4596
|
getProjectWithSources,
|
|
3935
4597
|
getProjectByPath,
|
|
@@ -3940,18 +4602,25 @@ export {
|
|
|
3940
4602
|
getOrgByName,
|
|
3941
4603
|
getOrg,
|
|
3942
4604
|
getNextTask,
|
|
4605
|
+
getLeaderboard,
|
|
3943
4606
|
getLatestHandoff,
|
|
4607
|
+
getImpactAnalysis,
|
|
3944
4608
|
getDirectReports,
|
|
3945
4609
|
getDatabase,
|
|
4610
|
+
getCriticalPath,
|
|
3946
4611
|
getCompletionGuardConfig,
|
|
3947
4612
|
getComment,
|
|
3948
4613
|
getChecklistStats,
|
|
3949
4614
|
getChecklist,
|
|
4615
|
+
getCapableAgents,
|
|
3950
4616
|
getBlockingDeps,
|
|
4617
|
+
getAgentMetrics,
|
|
3951
4618
|
getAgentByName,
|
|
3952
4619
|
getAgent,
|
|
3953
4620
|
getActiveWork,
|
|
3954
4621
|
findTasksByFile,
|
|
4622
|
+
findRelatedTaskIds,
|
|
4623
|
+
findPath,
|
|
3955
4624
|
failTask,
|
|
3956
4625
|
ensureTaskList,
|
|
3957
4626
|
ensureProject,
|
|
@@ -3991,8 +4660,11 @@ export {
|
|
|
3991
4660
|
bulkUpdateTasks,
|
|
3992
4661
|
bulkCreateTasks,
|
|
3993
4662
|
bulkAddTaskFiles,
|
|
4663
|
+
autoDetectFileRelationships,
|
|
4664
|
+
addTaskRelationship,
|
|
3994
4665
|
addTaskFile,
|
|
3995
4666
|
addProjectSource,
|
|
4667
|
+
addKgEdge,
|
|
3996
4668
|
addDependency,
|
|
3997
4669
|
addComment,
|
|
3998
4670
|
addChecklistItem,
|
|
@@ -4003,6 +4675,7 @@ export {
|
|
|
4003
4675
|
TaskListNotFoundError,
|
|
4004
4676
|
TASK_STATUSES,
|
|
4005
4677
|
TASK_PRIORITIES,
|
|
4678
|
+
RELATIONSHIP_TYPES,
|
|
4006
4679
|
ProjectNotFoundError,
|
|
4007
4680
|
PlanNotFoundError,
|
|
4008
4681
|
PLAN_STATUSES,
|