@hasna/todos 0.9.83 → 0.10.2
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 +1035 -59
- 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 +16 -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 +652 -22
- package/dist/lib/config.d.ts +2 -7
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/mcp/index.js +1033 -56
- package/dist/server/index.js +101 -35
- package/dist/types/index.d.ts +3 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2282,6 +2282,30 @@ function ensureSchema(db) {
|
|
|
2282
2282
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2283
2283
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2284
2284
|
)`);
|
|
2285
|
+
ensureTable("task_relationships", `
|
|
2286
|
+
CREATE TABLE task_relationships (
|
|
2287
|
+
id TEXT PRIMARY KEY,
|
|
2288
|
+
source_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
2289
|
+
target_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
2290
|
+
relationship_type TEXT NOT NULL,
|
|
2291
|
+
metadata TEXT DEFAULT '{}',
|
|
2292
|
+
created_by TEXT,
|
|
2293
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2294
|
+
CHECK (source_task_id != target_task_id)
|
|
2295
|
+
)`);
|
|
2296
|
+
ensureTable("kg_edges", `
|
|
2297
|
+
CREATE TABLE kg_edges (
|
|
2298
|
+
id TEXT PRIMARY KEY,
|
|
2299
|
+
source_id TEXT NOT NULL,
|
|
2300
|
+
source_type TEXT NOT NULL,
|
|
2301
|
+
target_id TEXT NOT NULL,
|
|
2302
|
+
target_type TEXT NOT NULL,
|
|
2303
|
+
relation_type TEXT NOT NULL,
|
|
2304
|
+
weight REAL NOT NULL DEFAULT 1.0,
|
|
2305
|
+
metadata TEXT DEFAULT '{}',
|
|
2306
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2307
|
+
UNIQUE(source_id, source_type, target_id, target_type, relation_type)
|
|
2308
|
+
)`);
|
|
2285
2309
|
ensureColumn("projects", "task_list_id", "TEXT");
|
|
2286
2310
|
ensureColumn("projects", "task_prefix", "TEXT");
|
|
2287
2311
|
ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
|
|
@@ -2306,6 +2330,7 @@ function ensureSchema(db) {
|
|
|
2306
2330
|
ensureColumn("agents", "title", "TEXT");
|
|
2307
2331
|
ensureColumn("agents", "level", "TEXT");
|
|
2308
2332
|
ensureColumn("agents", "org_id", "TEXT");
|
|
2333
|
+
ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
|
|
2309
2334
|
ensureColumn("projects", "org_id", "TEXT");
|
|
2310
2335
|
ensureColumn("plans", "task_list_id", "TEXT");
|
|
2311
2336
|
ensureColumn("plans", "agent_id", "TEXT");
|
|
@@ -2332,6 +2357,12 @@ function ensureSchema(db) {
|
|
|
2332
2357
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_project ON project_sources(project_id)");
|
|
2333
2358
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_type ON project_sources(type)");
|
|
2334
2359
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_assigned_by ON tasks(assigned_by)");
|
|
2360
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_source ON task_relationships(source_task_id)");
|
|
2361
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_target ON task_relationships(target_task_id)");
|
|
2362
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_type ON task_relationships(relationship_type)");
|
|
2363
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_source ON kg_edges(source_id, source_type)");
|
|
2364
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_target ON kg_edges(target_id, target_type)");
|
|
2365
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_relation ON kg_edges(relation_type)");
|
|
2335
2366
|
}
|
|
2336
2367
|
function backfillTaskTags(db) {
|
|
2337
2368
|
try {
|
|
@@ -2801,6 +2832,44 @@ var init_database = __esm(() => {
|
|
|
2801
2832
|
ALTER TABLE tasks ADD COLUMN assigned_from_project TEXT;
|
|
2802
2833
|
CREATE INDEX IF NOT EXISTS idx_tasks_assigned_by ON tasks(assigned_by);
|
|
2803
2834
|
INSERT OR IGNORE INTO _migrations (id) VALUES (26);
|
|
2835
|
+
`,
|
|
2836
|
+
`
|
|
2837
|
+
CREATE TABLE IF NOT EXISTS task_relationships (
|
|
2838
|
+
id TEXT PRIMARY KEY,
|
|
2839
|
+
source_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
2840
|
+
target_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
2841
|
+
relationship_type TEXT NOT NULL CHECK(relationship_type IN ('related_to', 'conflicts_with', 'similar_to', 'duplicates', 'supersedes', 'modifies_same_file')),
|
|
2842
|
+
metadata TEXT DEFAULT '{}',
|
|
2843
|
+
created_by TEXT,
|
|
2844
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2845
|
+
CHECK (source_task_id != target_task_id)
|
|
2846
|
+
);
|
|
2847
|
+
CREATE INDEX IF NOT EXISTS idx_task_rel_source ON task_relationships(source_task_id);
|
|
2848
|
+
CREATE INDEX IF NOT EXISTS idx_task_rel_target ON task_relationships(target_task_id);
|
|
2849
|
+
CREATE INDEX IF NOT EXISTS idx_task_rel_type ON task_relationships(relationship_type);
|
|
2850
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (27);
|
|
2851
|
+
`,
|
|
2852
|
+
`
|
|
2853
|
+
CREATE TABLE IF NOT EXISTS kg_edges (
|
|
2854
|
+
id TEXT PRIMARY KEY,
|
|
2855
|
+
source_id TEXT NOT NULL,
|
|
2856
|
+
source_type TEXT NOT NULL,
|
|
2857
|
+
target_id TEXT NOT NULL,
|
|
2858
|
+
target_type TEXT NOT NULL,
|
|
2859
|
+
relation_type TEXT NOT NULL,
|
|
2860
|
+
weight REAL NOT NULL DEFAULT 1.0,
|
|
2861
|
+
metadata TEXT DEFAULT '{}',
|
|
2862
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2863
|
+
UNIQUE(source_id, source_type, target_id, target_type, relation_type)
|
|
2864
|
+
);
|
|
2865
|
+
CREATE INDEX IF NOT EXISTS idx_kg_source ON kg_edges(source_id, source_type);
|
|
2866
|
+
CREATE INDEX IF NOT EXISTS idx_kg_target ON kg_edges(target_id, target_type);
|
|
2867
|
+
CREATE INDEX IF NOT EXISTS idx_kg_relation ON kg_edges(relation_type);
|
|
2868
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (28);
|
|
2869
|
+
`,
|
|
2870
|
+
`
|
|
2871
|
+
ALTER TABLE agents ADD COLUMN capabilities TEXT DEFAULT '[]';
|
|
2872
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (29);
|
|
2804
2873
|
`
|
|
2805
2874
|
];
|
|
2806
2875
|
});
|
|
@@ -3137,8 +3206,7 @@ __export(exports_config, {
|
|
|
3137
3206
|
getCompletionGuardConfig: () => getCompletionGuardConfig,
|
|
3138
3207
|
getAgentTasksDir: () => getAgentTasksDir,
|
|
3139
3208
|
getAgentTaskListId: () => getAgentTaskListId,
|
|
3140
|
-
getAgentPoolForProject: () => getAgentPoolForProject
|
|
3141
|
-
DEFAULT_AGENT_POOL: () => DEFAULT_AGENT_POOL
|
|
3209
|
+
getAgentPoolForProject: () => getAgentPoolForProject
|
|
3142
3210
|
});
|
|
3143
3211
|
import { existsSync as existsSync3 } from "fs";
|
|
3144
3212
|
import { join as join3 } from "path";
|
|
@@ -3198,7 +3266,7 @@ function getAgentPoolForProject(workingDir) {
|
|
|
3198
3266
|
return config.project_pools[bestKey];
|
|
3199
3267
|
}
|
|
3200
3268
|
}
|
|
3201
|
-
return config.agent_pool ||
|
|
3269
|
+
return config.agent_pool || null;
|
|
3202
3270
|
}
|
|
3203
3271
|
function getCompletionGuardConfig(projectPath) {
|
|
3204
3272
|
const config = loadConfig();
|
|
@@ -3208,26 +3276,9 @@ function getCompletionGuardConfig(projectPath) {
|
|
|
3208
3276
|
}
|
|
3209
3277
|
return global;
|
|
3210
3278
|
}
|
|
3211
|
-
var
|
|
3279
|
+
var cached = null, GUARD_DEFAULTS;
|
|
3212
3280
|
var init_config = __esm(() => {
|
|
3213
3281
|
init_sync_utils();
|
|
3214
|
-
DEFAULT_AGENT_POOL = [
|
|
3215
|
-
"maximus",
|
|
3216
|
-
"cassius",
|
|
3217
|
-
"aurelius",
|
|
3218
|
-
"brutus",
|
|
3219
|
-
"titus",
|
|
3220
|
-
"nero",
|
|
3221
|
-
"cicero",
|
|
3222
|
-
"seneca",
|
|
3223
|
-
"cato",
|
|
3224
|
-
"julius",
|
|
3225
|
-
"marcus",
|
|
3226
|
-
"lucius",
|
|
3227
|
-
"quintus",
|
|
3228
|
-
"gaius",
|
|
3229
|
-
"publius"
|
|
3230
|
-
];
|
|
3231
3282
|
GUARD_DEFAULTS = {
|
|
3232
3283
|
enabled: false,
|
|
3233
3284
|
min_work_seconds: 30,
|
|
@@ -4679,10 +4730,12 @@ __export(exports_agents, {
|
|
|
4679
4730
|
updateAgentActivity: () => updateAgentActivity,
|
|
4680
4731
|
updateAgent: () => updateAgent,
|
|
4681
4732
|
registerAgent: () => registerAgent,
|
|
4733
|
+
matchCapabilities: () => matchCapabilities,
|
|
4682
4734
|
listAgents: () => listAgents,
|
|
4683
4735
|
isAgentConflict: () => isAgentConflict,
|
|
4684
4736
|
getOrgChart: () => getOrgChart,
|
|
4685
4737
|
getDirectReports: () => getDirectReports,
|
|
4738
|
+
getCapableAgents: () => getCapableAgents,
|
|
4686
4739
|
getAvailableNamesFromPool: () => getAvailableNamesFromPool,
|
|
4687
4740
|
getAgentByName: () => getAgentByName,
|
|
4688
4741
|
getAgent: () => getAgent,
|
|
@@ -4700,30 +4753,13 @@ function rowToAgent(row) {
|
|
|
4700
4753
|
return {
|
|
4701
4754
|
...row,
|
|
4702
4755
|
permissions: JSON.parse(row.permissions || '["*"]'),
|
|
4756
|
+
capabilities: JSON.parse(row.capabilities || "[]"),
|
|
4703
4757
|
metadata: JSON.parse(row.metadata || "{}")
|
|
4704
4758
|
};
|
|
4705
4759
|
}
|
|
4706
4760
|
function registerAgent(input, db) {
|
|
4707
4761
|
const d = db || getDatabase();
|
|
4708
4762
|
const normalizedName = input.name.trim().toLowerCase();
|
|
4709
|
-
if (input.pool && input.pool.length > 0) {
|
|
4710
|
-
const poolLower = input.pool.map((n) => n.toLowerCase());
|
|
4711
|
-
if (!poolLower.includes(normalizedName)) {
|
|
4712
|
-
const available = getAvailableNamesFromPool(input.pool, d);
|
|
4713
|
-
const suggestion = available.length > 0 ? available[0] : null;
|
|
4714
|
-
return {
|
|
4715
|
-
conflict: true,
|
|
4716
|
-
pool_violation: true,
|
|
4717
|
-
existing_id: "",
|
|
4718
|
-
existing_name: normalizedName,
|
|
4719
|
-
last_seen_at: "",
|
|
4720
|
-
session_hint: null,
|
|
4721
|
-
working_dir: input.working_dir || null,
|
|
4722
|
-
suggestions: available.slice(0, 5),
|
|
4723
|
-
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}` : ""}`
|
|
4724
|
-
};
|
|
4725
|
-
}
|
|
4726
|
-
}
|
|
4727
4763
|
const existing = getAgentByName(normalizedName, d);
|
|
4728
4764
|
if (existing) {
|
|
4729
4765
|
const lastSeenMs = new Date(existing.last_seen_at).getTime();
|
|
@@ -4764,8 +4800,8 @@ function registerAgent(input, db) {
|
|
|
4764
4800
|
}
|
|
4765
4801
|
const id = shortUuid();
|
|
4766
4802
|
const timestamp = now();
|
|
4767
|
-
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)
|
|
4768
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
4803
|
+
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)
|
|
4804
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
4769
4805
|
id,
|
|
4770
4806
|
normalizedName,
|
|
4771
4807
|
input.description || null,
|
|
@@ -4773,6 +4809,7 @@ function registerAgent(input, db) {
|
|
|
4773
4809
|
input.title || null,
|
|
4774
4810
|
input.level || null,
|
|
4775
4811
|
JSON.stringify(input.permissions || ["*"]),
|
|
4812
|
+
JSON.stringify(input.capabilities || []),
|
|
4776
4813
|
input.reports_to || null,
|
|
4777
4814
|
input.org_id || null,
|
|
4778
4815
|
JSON.stringify(input.metadata || {}),
|
|
@@ -4828,6 +4865,10 @@ function updateAgent(id, input, db) {
|
|
|
4828
4865
|
sets.push("permissions = ?");
|
|
4829
4866
|
params.push(JSON.stringify(input.permissions));
|
|
4830
4867
|
}
|
|
4868
|
+
if (input.capabilities !== undefined) {
|
|
4869
|
+
sets.push("capabilities = ?");
|
|
4870
|
+
params.push(JSON.stringify(input.capabilities));
|
|
4871
|
+
}
|
|
4831
4872
|
if (input.title !== undefined) {
|
|
4832
4873
|
sets.push("title = ?");
|
|
4833
4874
|
params.push(input.title);
|
|
@@ -4875,6 +4916,28 @@ function getOrgChart(db) {
|
|
|
4875
4916
|
}
|
|
4876
4917
|
return buildTree(null);
|
|
4877
4918
|
}
|
|
4919
|
+
function matchCapabilities(agentCapabilities, requiredCapabilities) {
|
|
4920
|
+
if (requiredCapabilities.length === 0)
|
|
4921
|
+
return 1;
|
|
4922
|
+
if (agentCapabilities.length === 0)
|
|
4923
|
+
return 0;
|
|
4924
|
+
const agentSet = new Set(agentCapabilities.map((c) => c.toLowerCase()));
|
|
4925
|
+
let matches = 0;
|
|
4926
|
+
for (const req of requiredCapabilities) {
|
|
4927
|
+
if (agentSet.has(req.toLowerCase()))
|
|
4928
|
+
matches++;
|
|
4929
|
+
}
|
|
4930
|
+
return matches / requiredCapabilities.length;
|
|
4931
|
+
}
|
|
4932
|
+
function getCapableAgents(capabilities, opts, db) {
|
|
4933
|
+
const agents = listAgents(db);
|
|
4934
|
+
const minScore = opts?.min_score ?? 0.1;
|
|
4935
|
+
const scored = agents.map((agent) => ({
|
|
4936
|
+
agent,
|
|
4937
|
+
score: matchCapabilities(agent.capabilities, capabilities)
|
|
4938
|
+
})).filter((entry) => entry.score >= minScore).sort((a, b) => b.score - a.score);
|
|
4939
|
+
return opts?.limit ? scored.slice(0, opts.limit) : scored;
|
|
4940
|
+
}
|
|
4878
4941
|
var AGENT_ACTIVE_WINDOW_MS;
|
|
4879
4942
|
var init_agents = __esm(() => {
|
|
4880
4943
|
init_database();
|
|
@@ -9810,6 +9873,584 @@ var init_handoffs = __esm(() => {
|
|
|
9810
9873
|
init_database();
|
|
9811
9874
|
});
|
|
9812
9875
|
|
|
9876
|
+
// src/db/task-relationships.ts
|
|
9877
|
+
var exports_task_relationships = {};
|
|
9878
|
+
__export(exports_task_relationships, {
|
|
9879
|
+
removeTaskRelationshipByPair: () => removeTaskRelationshipByPair,
|
|
9880
|
+
removeTaskRelationship: () => removeTaskRelationship,
|
|
9881
|
+
getTaskRelationships: () => getTaskRelationships,
|
|
9882
|
+
getTaskRelationship: () => getTaskRelationship,
|
|
9883
|
+
findRelatedTaskIds: () => findRelatedTaskIds,
|
|
9884
|
+
autoDetectFileRelationships: () => autoDetectFileRelationships,
|
|
9885
|
+
addTaskRelationship: () => addTaskRelationship,
|
|
9886
|
+
RELATIONSHIP_TYPES: () => RELATIONSHIP_TYPES
|
|
9887
|
+
});
|
|
9888
|
+
function rowToRelationship(row) {
|
|
9889
|
+
return {
|
|
9890
|
+
...row,
|
|
9891
|
+
relationship_type: row.relationship_type,
|
|
9892
|
+
metadata: JSON.parse(row.metadata || "{}")
|
|
9893
|
+
};
|
|
9894
|
+
}
|
|
9895
|
+
function addTaskRelationship(input, db) {
|
|
9896
|
+
const d = db || getDatabase();
|
|
9897
|
+
const id = uuid();
|
|
9898
|
+
const timestamp = now();
|
|
9899
|
+
if (input.source_task_id === input.target_task_id) {
|
|
9900
|
+
throw new Error("Cannot create a relationship between a task and itself");
|
|
9901
|
+
}
|
|
9902
|
+
const symmetric = ["related_to", "conflicts_with", "similar_to", "modifies_same_file"];
|
|
9903
|
+
if (symmetric.includes(input.relationship_type)) {
|
|
9904
|
+
const existing = d.query(`SELECT id FROM task_relationships
|
|
9905
|
+
WHERE relationship_type = ?
|
|
9906
|
+
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);
|
|
9907
|
+
if (existing) {
|
|
9908
|
+
return getTaskRelationship(existing.id, d);
|
|
9909
|
+
}
|
|
9910
|
+
} else {
|
|
9911
|
+
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);
|
|
9912
|
+
if (existing) {
|
|
9913
|
+
return getTaskRelationship(existing.id, d);
|
|
9914
|
+
}
|
|
9915
|
+
}
|
|
9916
|
+
d.run(`INSERT INTO task_relationships (id, source_task_id, target_task_id, relationship_type, metadata, created_by, created_at)
|
|
9917
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
|
|
9918
|
+
id,
|
|
9919
|
+
input.source_task_id,
|
|
9920
|
+
input.target_task_id,
|
|
9921
|
+
input.relationship_type,
|
|
9922
|
+
JSON.stringify(input.metadata || {}),
|
|
9923
|
+
input.created_by || null,
|
|
9924
|
+
timestamp
|
|
9925
|
+
]);
|
|
9926
|
+
return getTaskRelationship(id, d);
|
|
9927
|
+
}
|
|
9928
|
+
function getTaskRelationship(id, db) {
|
|
9929
|
+
const d = db || getDatabase();
|
|
9930
|
+
const row = d.query("SELECT * FROM task_relationships WHERE id = ?").get(id);
|
|
9931
|
+
return row ? rowToRelationship(row) : null;
|
|
9932
|
+
}
|
|
9933
|
+
function removeTaskRelationship(id, db) {
|
|
9934
|
+
const d = db || getDatabase();
|
|
9935
|
+
return d.run("DELETE FROM task_relationships WHERE id = ?", [id]).changes > 0;
|
|
9936
|
+
}
|
|
9937
|
+
function removeTaskRelationshipByPair(sourceTaskId, targetTaskId, relationshipType, db) {
|
|
9938
|
+
const d = db || getDatabase();
|
|
9939
|
+
const symmetric = ["related_to", "conflicts_with", "similar_to", "modifies_same_file"];
|
|
9940
|
+
if (symmetric.includes(relationshipType)) {
|
|
9941
|
+
return d.run(`DELETE FROM task_relationships
|
|
9942
|
+
WHERE relationship_type = ?
|
|
9943
|
+
AND ((source_task_id = ? AND target_task_id = ?) OR (source_task_id = ? AND target_task_id = ?))`, [relationshipType, sourceTaskId, targetTaskId, targetTaskId, sourceTaskId]).changes > 0;
|
|
9944
|
+
}
|
|
9945
|
+
return d.run("DELETE FROM task_relationships WHERE source_task_id = ? AND target_task_id = ? AND relationship_type = ?", [sourceTaskId, targetTaskId, relationshipType]).changes > 0;
|
|
9946
|
+
}
|
|
9947
|
+
function getTaskRelationships(taskId, relationshipType, db) {
|
|
9948
|
+
const d = db || getDatabase();
|
|
9949
|
+
let sql = "SELECT * FROM task_relationships WHERE (source_task_id = ? OR target_task_id = ?)";
|
|
9950
|
+
const params = [taskId, taskId];
|
|
9951
|
+
if (relationshipType) {
|
|
9952
|
+
sql += " AND relationship_type = ?";
|
|
9953
|
+
params.push(relationshipType);
|
|
9954
|
+
}
|
|
9955
|
+
sql += " ORDER BY created_at DESC";
|
|
9956
|
+
return d.query(sql).all(...params).map(rowToRelationship);
|
|
9957
|
+
}
|
|
9958
|
+
function findRelatedTaskIds(taskId, relationshipType, db) {
|
|
9959
|
+
const rels = getTaskRelationships(taskId, relationshipType, db);
|
|
9960
|
+
const ids = new Set;
|
|
9961
|
+
for (const rel of rels) {
|
|
9962
|
+
if (rel.source_task_id === taskId)
|
|
9963
|
+
ids.add(rel.target_task_id);
|
|
9964
|
+
else
|
|
9965
|
+
ids.add(rel.source_task_id);
|
|
9966
|
+
}
|
|
9967
|
+
return [...ids];
|
|
9968
|
+
}
|
|
9969
|
+
function autoDetectFileRelationships(taskId, db) {
|
|
9970
|
+
const d = db || getDatabase();
|
|
9971
|
+
const files = d.query("SELECT path FROM task_files WHERE task_id = ? AND status != 'removed'").all(taskId);
|
|
9972
|
+
const created = [];
|
|
9973
|
+
for (const file of files) {
|
|
9974
|
+
const others = d.query("SELECT DISTINCT task_id FROM task_files WHERE path = ? AND task_id != ? AND status != 'removed'").all(file.path, taskId);
|
|
9975
|
+
for (const other of others) {
|
|
9976
|
+
try {
|
|
9977
|
+
const rel = addTaskRelationship({
|
|
9978
|
+
source_task_id: taskId,
|
|
9979
|
+
target_task_id: other.task_id,
|
|
9980
|
+
relationship_type: "modifies_same_file",
|
|
9981
|
+
metadata: { shared_file: file.path }
|
|
9982
|
+
}, d);
|
|
9983
|
+
created.push(rel);
|
|
9984
|
+
} catch {}
|
|
9985
|
+
}
|
|
9986
|
+
}
|
|
9987
|
+
return created;
|
|
9988
|
+
}
|
|
9989
|
+
var RELATIONSHIP_TYPES;
|
|
9990
|
+
var init_task_relationships = __esm(() => {
|
|
9991
|
+
init_database();
|
|
9992
|
+
RELATIONSHIP_TYPES = [
|
|
9993
|
+
"related_to",
|
|
9994
|
+
"conflicts_with",
|
|
9995
|
+
"similar_to",
|
|
9996
|
+
"duplicates",
|
|
9997
|
+
"supersedes",
|
|
9998
|
+
"modifies_same_file"
|
|
9999
|
+
];
|
|
10000
|
+
});
|
|
10001
|
+
|
|
10002
|
+
// src/db/kg.ts
|
|
10003
|
+
var exports_kg = {};
|
|
10004
|
+
__export(exports_kg, {
|
|
10005
|
+
syncKgEdges: () => syncKgEdges,
|
|
10006
|
+
removeKgEdges: () => removeKgEdges,
|
|
10007
|
+
getRelated: () => getRelated,
|
|
10008
|
+
getImpactAnalysis: () => getImpactAnalysis,
|
|
10009
|
+
getCriticalPath: () => getCriticalPath,
|
|
10010
|
+
findPath: () => findPath,
|
|
10011
|
+
addKgEdge: () => addKgEdge
|
|
10012
|
+
});
|
|
10013
|
+
function rowToEdge(row) {
|
|
10014
|
+
return {
|
|
10015
|
+
...row,
|
|
10016
|
+
metadata: JSON.parse(row.metadata || "{}")
|
|
10017
|
+
};
|
|
10018
|
+
}
|
|
10019
|
+
function syncKgEdges(db) {
|
|
10020
|
+
const d = db || getDatabase();
|
|
10021
|
+
let synced = 0;
|
|
10022
|
+
const tx = d.transaction(() => {
|
|
10023
|
+
const deps = d.query("SELECT task_id, depends_on FROM task_dependencies").all();
|
|
10024
|
+
for (const dep of deps) {
|
|
10025
|
+
synced += upsertEdge(d, dep.task_id, "task", dep.depends_on, "task", "depends_on");
|
|
10026
|
+
}
|
|
10027
|
+
const assignments = d.query("SELECT id, assigned_to FROM tasks WHERE assigned_to IS NOT NULL").all();
|
|
10028
|
+
for (const a of assignments) {
|
|
10029
|
+
synced += upsertEdge(d, a.id, "task", a.assigned_to, "agent", "assigned_to");
|
|
10030
|
+
}
|
|
10031
|
+
const agents = d.query("SELECT id, reports_to FROM agents WHERE reports_to IS NOT NULL").all();
|
|
10032
|
+
for (const a of agents) {
|
|
10033
|
+
synced += upsertEdge(d, a.id, "agent", a.reports_to, "agent", "reports_to");
|
|
10034
|
+
}
|
|
10035
|
+
const files = d.query("SELECT task_id, path FROM task_files WHERE status != 'removed'").all();
|
|
10036
|
+
for (const f of files) {
|
|
10037
|
+
synced += upsertEdge(d, f.task_id, "task", f.path, "file", "references_file");
|
|
10038
|
+
}
|
|
10039
|
+
const taskProjects = d.query("SELECT id, project_id FROM tasks WHERE project_id IS NOT NULL").all();
|
|
10040
|
+
for (const tp of taskProjects) {
|
|
10041
|
+
synced += upsertEdge(d, tp.id, "task", tp.project_id, "project", "in_project");
|
|
10042
|
+
}
|
|
10043
|
+
const taskPlans = d.query("SELECT id, plan_id FROM tasks WHERE plan_id IS NOT NULL").all();
|
|
10044
|
+
for (const tp of taskPlans) {
|
|
10045
|
+
synced += upsertEdge(d, tp.id, "task", tp.plan_id, "plan", "in_plan");
|
|
10046
|
+
}
|
|
10047
|
+
try {
|
|
10048
|
+
const rels = d.query("SELECT source_task_id, target_task_id, relationship_type FROM task_relationships").all();
|
|
10049
|
+
for (const r of rels) {
|
|
10050
|
+
synced += upsertEdge(d, r.source_task_id, "task", r.target_task_id, "task", r.relationship_type);
|
|
10051
|
+
}
|
|
10052
|
+
} catch {}
|
|
10053
|
+
});
|
|
10054
|
+
tx();
|
|
10055
|
+
return { synced };
|
|
10056
|
+
}
|
|
10057
|
+
function upsertEdge(d, sourceId, sourceType, targetId, targetType, relationType, weight = 1) {
|
|
10058
|
+
try {
|
|
10059
|
+
d.run(`INSERT OR IGNORE INTO kg_edges (id, source_id, source_type, target_id, target_type, relation_type, weight, metadata, created_at)
|
|
10060
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, '{}', ?)`, [uuid(), sourceId, sourceType, targetId, targetType, relationType, weight, now()]);
|
|
10061
|
+
return 1;
|
|
10062
|
+
} catch {
|
|
10063
|
+
return 0;
|
|
10064
|
+
}
|
|
10065
|
+
}
|
|
10066
|
+
function getRelated(entityId, opts, db) {
|
|
10067
|
+
const d = db || getDatabase();
|
|
10068
|
+
const direction = opts?.direction || "both";
|
|
10069
|
+
const conditions = [];
|
|
10070
|
+
const params = [];
|
|
10071
|
+
if (direction === "outgoing" || direction === "both") {
|
|
10072
|
+
conditions.push("source_id = ?");
|
|
10073
|
+
params.push(entityId);
|
|
10074
|
+
}
|
|
10075
|
+
if (direction === "incoming" || direction === "both") {
|
|
10076
|
+
conditions.push("target_id = ?");
|
|
10077
|
+
params.push(entityId);
|
|
10078
|
+
}
|
|
10079
|
+
let sql = `SELECT * FROM kg_edges WHERE (${conditions.join(" OR ")})`;
|
|
10080
|
+
if (opts?.relation_type) {
|
|
10081
|
+
sql += " AND relation_type = ?";
|
|
10082
|
+
params.push(opts.relation_type);
|
|
10083
|
+
}
|
|
10084
|
+
if (opts?.entity_type) {
|
|
10085
|
+
sql += " AND (source_type = ? OR target_type = ?)";
|
|
10086
|
+
params.push(opts.entity_type, opts.entity_type);
|
|
10087
|
+
}
|
|
10088
|
+
sql += " ORDER BY weight DESC, created_at DESC";
|
|
10089
|
+
if (opts?.limit) {
|
|
10090
|
+
sql += " LIMIT ?";
|
|
10091
|
+
params.push(opts.limit);
|
|
10092
|
+
}
|
|
10093
|
+
return d.query(sql).all(...params).map(rowToEdge);
|
|
10094
|
+
}
|
|
10095
|
+
function findPath(sourceId, targetId, opts, db) {
|
|
10096
|
+
const d = db || getDatabase();
|
|
10097
|
+
const maxDepth = opts?.max_depth || 5;
|
|
10098
|
+
const visited = new Set;
|
|
10099
|
+
const queue = [{ id: sourceId, path: [] }];
|
|
10100
|
+
const results = [];
|
|
10101
|
+
visited.add(sourceId);
|
|
10102
|
+
while (queue.length > 0) {
|
|
10103
|
+
const current = queue.shift();
|
|
10104
|
+
if (current.path.length >= maxDepth)
|
|
10105
|
+
continue;
|
|
10106
|
+
let sql = "SELECT * FROM kg_edges WHERE source_id = ?";
|
|
10107
|
+
const params = [current.id];
|
|
10108
|
+
if (opts?.relation_types && opts.relation_types.length > 0) {
|
|
10109
|
+
const placeholders = opts.relation_types.map(() => "?").join(",");
|
|
10110
|
+
sql += ` AND relation_type IN (${placeholders})`;
|
|
10111
|
+
params.push(...opts.relation_types);
|
|
10112
|
+
}
|
|
10113
|
+
const edges = d.query(sql).all(...params).map(rowToEdge);
|
|
10114
|
+
for (const edge of edges) {
|
|
10115
|
+
const nextId = edge.target_id;
|
|
10116
|
+
const newPath = [...current.path, edge];
|
|
10117
|
+
if (nextId === targetId) {
|
|
10118
|
+
results.push(newPath);
|
|
10119
|
+
if (results.length >= 3)
|
|
10120
|
+
return results;
|
|
10121
|
+
continue;
|
|
10122
|
+
}
|
|
10123
|
+
if (!visited.has(nextId)) {
|
|
10124
|
+
visited.add(nextId);
|
|
10125
|
+
queue.push({ id: nextId, path: newPath });
|
|
10126
|
+
}
|
|
10127
|
+
}
|
|
10128
|
+
}
|
|
10129
|
+
return results;
|
|
10130
|
+
}
|
|
10131
|
+
function getImpactAnalysis(entityId, opts, db) {
|
|
10132
|
+
const d = db || getDatabase();
|
|
10133
|
+
const maxDepth = opts?.max_depth || 3;
|
|
10134
|
+
const results = [];
|
|
10135
|
+
const visited = new Set;
|
|
10136
|
+
visited.add(entityId);
|
|
10137
|
+
const queue = [{ id: entityId, depth: 0 }];
|
|
10138
|
+
while (queue.length > 0) {
|
|
10139
|
+
const current = queue.shift();
|
|
10140
|
+
if (current.depth >= maxDepth)
|
|
10141
|
+
continue;
|
|
10142
|
+
let sql = "SELECT * FROM kg_edges WHERE source_id = ?";
|
|
10143
|
+
const params = [current.id];
|
|
10144
|
+
if (opts?.relation_types && opts.relation_types.length > 0) {
|
|
10145
|
+
const placeholders = opts.relation_types.map(() => "?").join(",");
|
|
10146
|
+
sql += ` AND relation_type IN (${placeholders})`;
|
|
10147
|
+
params.push(...opts.relation_types);
|
|
10148
|
+
}
|
|
10149
|
+
const edges = d.query(sql).all(...params).map(rowToEdge);
|
|
10150
|
+
for (const edge of edges) {
|
|
10151
|
+
if (!visited.has(edge.target_id)) {
|
|
10152
|
+
visited.add(edge.target_id);
|
|
10153
|
+
results.push({
|
|
10154
|
+
entity_id: edge.target_id,
|
|
10155
|
+
entity_type: edge.target_type,
|
|
10156
|
+
depth: current.depth + 1,
|
|
10157
|
+
relation: edge.relation_type
|
|
10158
|
+
});
|
|
10159
|
+
queue.push({ id: edge.target_id, depth: current.depth + 1 });
|
|
10160
|
+
}
|
|
10161
|
+
}
|
|
10162
|
+
}
|
|
10163
|
+
return results;
|
|
10164
|
+
}
|
|
10165
|
+
function getCriticalPath(opts, db) {
|
|
10166
|
+
const d = db || getDatabase();
|
|
10167
|
+
let sql = `SELECT source_id, target_id FROM kg_edges WHERE relation_type = 'depends_on'`;
|
|
10168
|
+
const params = [];
|
|
10169
|
+
if (opts?.project_id) {
|
|
10170
|
+
sql += ` AND source_id IN (SELECT id FROM tasks WHERE project_id = ?)`;
|
|
10171
|
+
params.push(opts.project_id);
|
|
10172
|
+
}
|
|
10173
|
+
const edges = d.query(sql).all(...params);
|
|
10174
|
+
const blocks = new Map;
|
|
10175
|
+
for (const e of edges) {
|
|
10176
|
+
if (!blocks.has(e.target_id))
|
|
10177
|
+
blocks.set(e.target_id, new Set);
|
|
10178
|
+
blocks.get(e.target_id).add(e.source_id);
|
|
10179
|
+
}
|
|
10180
|
+
const results = [];
|
|
10181
|
+
for (const [taskId] of blocks) {
|
|
10182
|
+
const visited = new Set;
|
|
10183
|
+
const q = [taskId];
|
|
10184
|
+
let maxDepth = 0;
|
|
10185
|
+
let depth = 0;
|
|
10186
|
+
let levelSize = q.length;
|
|
10187
|
+
while (q.length > 0) {
|
|
10188
|
+
const node = q.shift();
|
|
10189
|
+
levelSize--;
|
|
10190
|
+
const downstream = blocks.get(node);
|
|
10191
|
+
if (downstream) {
|
|
10192
|
+
for (const d2 of downstream) {
|
|
10193
|
+
if (!visited.has(d2)) {
|
|
10194
|
+
visited.add(d2);
|
|
10195
|
+
q.push(d2);
|
|
10196
|
+
}
|
|
10197
|
+
}
|
|
10198
|
+
}
|
|
10199
|
+
if (levelSize === 0) {
|
|
10200
|
+
depth++;
|
|
10201
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
10202
|
+
levelSize = q.length;
|
|
10203
|
+
}
|
|
10204
|
+
}
|
|
10205
|
+
if (visited.size > 0) {
|
|
10206
|
+
results.push({ task_id: taskId, blocking_count: visited.size, depth: maxDepth });
|
|
10207
|
+
}
|
|
10208
|
+
}
|
|
10209
|
+
results.sort((a, b) => b.blocking_count - a.blocking_count);
|
|
10210
|
+
return results.slice(0, opts?.limit || 20);
|
|
10211
|
+
}
|
|
10212
|
+
function addKgEdge(sourceId, sourceType, targetId, targetType, relationType, weight = 1, metadata, db) {
|
|
10213
|
+
const d = db || getDatabase();
|
|
10214
|
+
const id = uuid();
|
|
10215
|
+
const timestamp = now();
|
|
10216
|
+
d.run(`INSERT OR IGNORE INTO kg_edges (id, source_id, source_type, target_id, target_type, relation_type, weight, metadata, created_at)
|
|
10217
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, sourceId, sourceType, targetId, targetType, relationType, weight, JSON.stringify(metadata || {}), timestamp]);
|
|
10218
|
+
return { id, source_id: sourceId, source_type: sourceType, target_id: targetId, target_type: targetType, relation_type: relationType, weight, metadata: metadata || {}, created_at: timestamp };
|
|
10219
|
+
}
|
|
10220
|
+
function removeKgEdges(sourceId, targetId, relationType, db) {
|
|
10221
|
+
const d = db || getDatabase();
|
|
10222
|
+
if (relationType) {
|
|
10223
|
+
return d.run("DELETE FROM kg_edges WHERE source_id = ? AND target_id = ? AND relation_type = ?", [sourceId, targetId, relationType]).changes;
|
|
10224
|
+
}
|
|
10225
|
+
return d.run("DELETE FROM kg_edges WHERE source_id = ? AND target_id = ?", [sourceId, targetId]).changes;
|
|
10226
|
+
}
|
|
10227
|
+
var init_kg = __esm(() => {
|
|
10228
|
+
init_database();
|
|
10229
|
+
});
|
|
10230
|
+
|
|
10231
|
+
// src/db/patrol.ts
|
|
10232
|
+
var exports_patrol = {};
|
|
10233
|
+
__export(exports_patrol, {
|
|
10234
|
+
patrolTasks: () => patrolTasks,
|
|
10235
|
+
getReviewQueue: () => getReviewQueue
|
|
10236
|
+
});
|
|
10237
|
+
function rowToTask3(row) {
|
|
10238
|
+
return {
|
|
10239
|
+
...row,
|
|
10240
|
+
status: row.status,
|
|
10241
|
+
priority: row.priority,
|
|
10242
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
10243
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
10244
|
+
requires_approval: Boolean(row.requires_approval)
|
|
10245
|
+
};
|
|
10246
|
+
}
|
|
10247
|
+
function patrolTasks(opts, db) {
|
|
10248
|
+
const d = db || getDatabase();
|
|
10249
|
+
const stuckMinutes = opts?.stuck_minutes || 60;
|
|
10250
|
+
const confidenceThreshold = opts?.confidence_threshold || 0.5;
|
|
10251
|
+
const issues = [];
|
|
10252
|
+
let projectFilter = "";
|
|
10253
|
+
const projectParams = [];
|
|
10254
|
+
if (opts?.project_id) {
|
|
10255
|
+
projectFilter = " AND project_id = ?";
|
|
10256
|
+
projectParams.push(opts.project_id);
|
|
10257
|
+
}
|
|
10258
|
+
const stuckCutoff = new Date(Date.now() - stuckMinutes * 60 * 1000).toISOString();
|
|
10259
|
+
const stuckRows = d.query(`SELECT * FROM tasks WHERE status = 'in_progress' AND updated_at < ?${projectFilter} ORDER BY updated_at ASC`).all(stuckCutoff, ...projectParams);
|
|
10260
|
+
for (const row of stuckRows) {
|
|
10261
|
+
const task = rowToTask3(row);
|
|
10262
|
+
const minutesStuck = Math.round((Date.now() - new Date(task.updated_at).getTime()) / 60000);
|
|
10263
|
+
issues.push({
|
|
10264
|
+
type: "stuck",
|
|
10265
|
+
task_id: task.id,
|
|
10266
|
+
task_title: task.title,
|
|
10267
|
+
severity: minutesStuck > 480 ? "critical" : minutesStuck > 120 ? "high" : "medium",
|
|
10268
|
+
detail: `In progress for ${minutesStuck} minutes without update`
|
|
10269
|
+
});
|
|
10270
|
+
}
|
|
10271
|
+
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);
|
|
10272
|
+
for (const row of lowConfRows) {
|
|
10273
|
+
const task = rowToTask3(row);
|
|
10274
|
+
issues.push({
|
|
10275
|
+
type: "low_confidence",
|
|
10276
|
+
task_id: task.id,
|
|
10277
|
+
task_title: task.title,
|
|
10278
|
+
severity: (task.confidence ?? 0) < 0.3 ? "high" : "medium",
|
|
10279
|
+
detail: `Completed with confidence ${task.confidence} (threshold: ${confidenceThreshold})`
|
|
10280
|
+
});
|
|
10281
|
+
}
|
|
10282
|
+
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();
|
|
10283
|
+
for (const row of orphanedRows) {
|
|
10284
|
+
const task = rowToTask3(row);
|
|
10285
|
+
issues.push({
|
|
10286
|
+
type: "orphaned",
|
|
10287
|
+
task_id: task.id,
|
|
10288
|
+
task_title: task.title,
|
|
10289
|
+
severity: "low",
|
|
10290
|
+
detail: "Pending task with no project, no agent, and no assignee"
|
|
10291
|
+
});
|
|
10292
|
+
}
|
|
10293
|
+
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);
|
|
10294
|
+
for (const row of needsReviewRows) {
|
|
10295
|
+
const task = rowToTask3(row);
|
|
10296
|
+
issues.push({
|
|
10297
|
+
type: "needs_review",
|
|
10298
|
+
task_id: task.id,
|
|
10299
|
+
task_title: task.title,
|
|
10300
|
+
severity: "medium",
|
|
10301
|
+
detail: "Completed but requires approval \u2014 not yet reviewed"
|
|
10302
|
+
});
|
|
10303
|
+
}
|
|
10304
|
+
const pendingWithDeps = d.query(`SELECT t.* FROM tasks t
|
|
10305
|
+
WHERE t.status = 'pending'${projectFilter}
|
|
10306
|
+
AND t.id IN (SELECT task_id FROM task_dependencies)`).all(...projectParams);
|
|
10307
|
+
for (const row of pendingWithDeps) {
|
|
10308
|
+
const deps = d.query(`SELECT d.depends_on, t.status FROM task_dependencies d
|
|
10309
|
+
JOIN tasks t ON t.id = d.depends_on
|
|
10310
|
+
WHERE d.task_id = ?`).all(row.id);
|
|
10311
|
+
if (deps.length > 0 && deps.every((dep) => dep.status === "failed" || dep.status === "cancelled")) {
|
|
10312
|
+
const task = rowToTask3(row);
|
|
10313
|
+
issues.push({
|
|
10314
|
+
type: "zombie_blocked",
|
|
10315
|
+
task_id: task.id,
|
|
10316
|
+
task_title: task.title,
|
|
10317
|
+
severity: "high",
|
|
10318
|
+
detail: `Blocked by ${deps.length} task(s) that are all failed/cancelled \u2014 will never unblock`
|
|
10319
|
+
});
|
|
10320
|
+
}
|
|
10321
|
+
}
|
|
10322
|
+
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
10323
|
+
issues.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
10324
|
+
return {
|
|
10325
|
+
issues,
|
|
10326
|
+
total_issues: issues.length,
|
|
10327
|
+
scanned_at: new Date().toISOString()
|
|
10328
|
+
};
|
|
10329
|
+
}
|
|
10330
|
+
function getReviewQueue(opts, db) {
|
|
10331
|
+
const d = db || getDatabase();
|
|
10332
|
+
let sql = `SELECT * FROM tasks WHERE status = 'completed' AND (
|
|
10333
|
+
(requires_approval = 1 AND approved_by IS NULL)
|
|
10334
|
+
OR (confidence IS NOT NULL AND confidence < 0.5)
|
|
10335
|
+
)`;
|
|
10336
|
+
const params = [];
|
|
10337
|
+
if (opts?.project_id) {
|
|
10338
|
+
sql += " AND project_id = ?";
|
|
10339
|
+
params.push(opts.project_id);
|
|
10340
|
+
}
|
|
10341
|
+
sql += " ORDER BY CASE WHEN requires_approval = 1 AND approved_by IS NULL THEN 0 ELSE 1 END, confidence ASC";
|
|
10342
|
+
if (opts?.limit) {
|
|
10343
|
+
sql += " LIMIT ?";
|
|
10344
|
+
params.push(opts.limit);
|
|
10345
|
+
}
|
|
10346
|
+
return d.query(sql).all(...params).map(rowToTask3);
|
|
10347
|
+
}
|
|
10348
|
+
var init_patrol = __esm(() => {
|
|
10349
|
+
init_database();
|
|
10350
|
+
});
|
|
10351
|
+
|
|
10352
|
+
// src/db/agent-metrics.ts
|
|
10353
|
+
var exports_agent_metrics = {};
|
|
10354
|
+
__export(exports_agent_metrics, {
|
|
10355
|
+
scoreTask: () => scoreTask,
|
|
10356
|
+
getLeaderboard: () => getLeaderboard,
|
|
10357
|
+
getAgentMetrics: () => getAgentMetrics
|
|
10358
|
+
});
|
|
10359
|
+
function getAgentMetrics(agentId, opts, db) {
|
|
10360
|
+
const d = db || getDatabase();
|
|
10361
|
+
const agent = d.query("SELECT id, name FROM agents WHERE id = ? OR LOWER(name) = LOWER(?)").get(agentId, agentId);
|
|
10362
|
+
if (!agent)
|
|
10363
|
+
return null;
|
|
10364
|
+
let projectFilter = "";
|
|
10365
|
+
const params = [agent.id, agent.id];
|
|
10366
|
+
if (opts?.project_id) {
|
|
10367
|
+
projectFilter = " AND project_id = ?";
|
|
10368
|
+
params.push(opts.project_id);
|
|
10369
|
+
}
|
|
10370
|
+
const completed = d.query(`SELECT COUNT(*) as count FROM tasks WHERE (agent_id = ? OR assigned_to = ?) AND status = 'completed'${projectFilter}`).get(...params).count;
|
|
10371
|
+
const failed = d.query(`SELECT COUNT(*) as count FROM tasks WHERE (agent_id = ? OR assigned_to = ?) AND status = 'failed'${projectFilter}`).get(...params).count;
|
|
10372
|
+
const inProgress = d.query(`SELECT COUNT(*) as count FROM tasks WHERE (agent_id = ? OR assigned_to = ?) AND status = 'in_progress'${projectFilter}`).get(...params).count;
|
|
10373
|
+
const total = completed + failed;
|
|
10374
|
+
const completionRate = total > 0 ? completed / total : 0;
|
|
10375
|
+
const avgTime = d.query(`SELECT AVG(
|
|
10376
|
+
(julianday(completed_at) - julianday(created_at)) * 24 * 60
|
|
10377
|
+
) as avg_minutes
|
|
10378
|
+
FROM tasks
|
|
10379
|
+
WHERE (agent_id = ? OR assigned_to = ?) AND status = 'completed' AND completed_at IS NOT NULL${projectFilter}`).get(...params);
|
|
10380
|
+
const avgConf = d.query(`SELECT AVG(confidence) as avg_confidence
|
|
10381
|
+
FROM tasks
|
|
10382
|
+
WHERE (agent_id = ? OR assigned_to = ?) AND status = 'completed' AND confidence IS NOT NULL${projectFilter}`).get(...params);
|
|
10383
|
+
const reviewTasks = d.query(`SELECT metadata FROM tasks
|
|
10384
|
+
WHERE (agent_id = ? OR assigned_to = ?) AND status = 'completed'${projectFilter}
|
|
10385
|
+
AND metadata LIKE '%_review_score%'`).all(...params);
|
|
10386
|
+
let reviewScoreAvg = null;
|
|
10387
|
+
if (reviewTasks.length > 0) {
|
|
10388
|
+
let total2 = 0;
|
|
10389
|
+
let count = 0;
|
|
10390
|
+
for (const row of reviewTasks) {
|
|
10391
|
+
try {
|
|
10392
|
+
const meta = JSON.parse(row.metadata);
|
|
10393
|
+
if (typeof meta._review_score === "number") {
|
|
10394
|
+
total2 += meta._review_score;
|
|
10395
|
+
count++;
|
|
10396
|
+
}
|
|
10397
|
+
} catch {}
|
|
10398
|
+
}
|
|
10399
|
+
if (count > 0)
|
|
10400
|
+
reviewScoreAvg = total2 / count;
|
|
10401
|
+
}
|
|
10402
|
+
const speedScore = avgTime?.avg_minutes != null ? Math.max(0, 1 - avgTime.avg_minutes / (60 * 24)) : 0.5;
|
|
10403
|
+
const confidenceScore = avgConf?.avg_confidence ?? 0.5;
|
|
10404
|
+
const volumeScore = Math.min(1, completed / 50);
|
|
10405
|
+
const compositeScore = completionRate * 0.3 + speedScore * 0.2 + confidenceScore * 0.3 + volumeScore * 0.2;
|
|
10406
|
+
return {
|
|
10407
|
+
agent_id: agent.id,
|
|
10408
|
+
agent_name: agent.name,
|
|
10409
|
+
tasks_completed: completed,
|
|
10410
|
+
tasks_failed: failed,
|
|
10411
|
+
tasks_in_progress: inProgress,
|
|
10412
|
+
completion_rate: Math.round(completionRate * 1000) / 1000,
|
|
10413
|
+
avg_completion_minutes: avgTime?.avg_minutes != null ? Math.round(avgTime.avg_minutes * 10) / 10 : null,
|
|
10414
|
+
avg_confidence: avgConf?.avg_confidence != null ? Math.round(avgConf.avg_confidence * 1000) / 1000 : null,
|
|
10415
|
+
review_score_avg: reviewScoreAvg != null ? Math.round(reviewScoreAvg * 1000) / 1000 : null,
|
|
10416
|
+
composite_score: Math.round(compositeScore * 1000) / 1000
|
|
10417
|
+
};
|
|
10418
|
+
}
|
|
10419
|
+
function getLeaderboard(opts, db) {
|
|
10420
|
+
const d = db || getDatabase();
|
|
10421
|
+
const agents = d.query("SELECT id FROM agents ORDER BY name").all();
|
|
10422
|
+
const entries = [];
|
|
10423
|
+
for (const agent of agents) {
|
|
10424
|
+
const metrics = getAgentMetrics(agent.id, { project_id: opts?.project_id }, d);
|
|
10425
|
+
if (metrics && (metrics.tasks_completed > 0 || metrics.tasks_failed > 0 || metrics.tasks_in_progress > 0)) {
|
|
10426
|
+
entries.push(metrics);
|
|
10427
|
+
}
|
|
10428
|
+
}
|
|
10429
|
+
entries.sort((a, b) => b.composite_score - a.composite_score);
|
|
10430
|
+
const limit = opts?.limit || 20;
|
|
10431
|
+
return entries.slice(0, limit).map((entry, idx) => ({
|
|
10432
|
+
...entry,
|
|
10433
|
+
rank: idx + 1
|
|
10434
|
+
}));
|
|
10435
|
+
}
|
|
10436
|
+
function scoreTask(taskId, score, reviewerId, db) {
|
|
10437
|
+
const d = db || getDatabase();
|
|
10438
|
+
if (score < 0 || score > 1)
|
|
10439
|
+
throw new Error("Score must be between 0 and 1");
|
|
10440
|
+
const task = d.query("SELECT metadata FROM tasks WHERE id = ?").get(taskId);
|
|
10441
|
+
if (!task)
|
|
10442
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
10443
|
+
const metadata = JSON.parse(task.metadata || "{}");
|
|
10444
|
+
metadata._review_score = score;
|
|
10445
|
+
if (reviewerId)
|
|
10446
|
+
metadata._reviewed_by = reviewerId;
|
|
10447
|
+
metadata._reviewed_at = now();
|
|
10448
|
+
d.run("UPDATE tasks SET metadata = ?, updated_at = ? WHERE id = ?", [JSON.stringify(metadata), now(), taskId]);
|
|
10449
|
+
}
|
|
10450
|
+
var init_agent_metrics = __esm(() => {
|
|
10451
|
+
init_database();
|
|
10452
|
+
});
|
|
10453
|
+
|
|
9813
10454
|
// src/mcp/index.ts
|
|
9814
10455
|
var exports_mcp = {};
|
|
9815
10456
|
__export(exports_mcp, {
|
|
@@ -10839,25 +11480,28 @@ ${text}` }] };
|
|
|
10839
11480
|
});
|
|
10840
11481
|
}
|
|
10841
11482
|
if (shouldRegisterTool("register_agent")) {
|
|
10842
|
-
server.tool("register_agent", "Register an agent.
|
|
10843
|
-
name: exports_external.string().describe("Agent name \u2014
|
|
11483
|
+
server.tool("register_agent", "Register an agent. Any name is allowed \u2014 the configured pool is advisory, not enforced. Returns a conflict error if the name is held by a recently-active agent.", {
|
|
11484
|
+
name: exports_external.string().describe("Agent name \u2014 any name is allowed. Use suggest_agent_name to see pool suggestions and avoid conflicts."),
|
|
10844
11485
|
description: exports_external.string().optional(),
|
|
11486
|
+
capabilities: exports_external.array(exports_external.string()).optional().describe("Agent capabilities/skills for task routing (e.g. ['typescript', 'testing', 'devops'])"),
|
|
10845
11487
|
session_id: exports_external.string().optional().describe("Unique ID for this coding session (e.g. process PID + timestamp, or env var). Used to detect name collisions across sessions. Store it and pass on every register_agent call."),
|
|
10846
11488
|
working_dir: exports_external.string().optional().describe("Working directory of this session \u2014 used to look up the project's agent pool and identify who holds the name in a conflict")
|
|
10847
|
-
}, async ({ name, description, session_id, working_dir }) => {
|
|
11489
|
+
}, async ({ name, description, capabilities, session_id, working_dir }) => {
|
|
10848
11490
|
try {
|
|
10849
11491
|
const pool = getAgentPoolForProject(working_dir);
|
|
10850
|
-
const result = registerAgent({ name, description, session_id, working_dir, pool });
|
|
11492
|
+
const result = registerAgent({ name, description, capabilities, session_id, working_dir, pool: pool || undefined });
|
|
10851
11493
|
if (isAgentConflict(result)) {
|
|
10852
11494
|
const suggestLine = result.suggestions && result.suggestions.length > 0 ? `
|
|
10853
11495
|
Available names: ${result.suggestions.join(", ")}` : "";
|
|
10854
|
-
const hint =
|
|
11496
|
+
const hint = `CONFLICT: ${result.message}${suggestLine}`;
|
|
10855
11497
|
return {
|
|
10856
11498
|
content: [{ type: "text", text: hint }],
|
|
10857
11499
|
isError: true
|
|
10858
11500
|
};
|
|
10859
11501
|
}
|
|
10860
11502
|
const agent = result;
|
|
11503
|
+
const poolLine = pool ? `
|
|
11504
|
+
Pool: [${pool.join(", ")}]` : "";
|
|
10861
11505
|
return {
|
|
10862
11506
|
content: [{
|
|
10863
11507
|
type: "text",
|
|
@@ -10865,8 +11509,7 @@ Available names: ${result.suggestions.join(", ")}` : "";
|
|
|
10865
11509
|
ID: ${agent.id}
|
|
10866
11510
|
Name: ${agent.name}${agent.description ? `
|
|
10867
11511
|
Description: ${agent.description}` : ""}
|
|
10868
|
-
Session: ${agent.session_id ?? "unbound"}
|
|
10869
|
-
Pool: [${pool.join(", ")}]
|
|
11512
|
+
Session: ${agent.session_id ?? "unbound"}${poolLine}
|
|
10870
11513
|
Created: ${agent.created_at}
|
|
10871
11514
|
Last seen: ${agent.last_seen_at}`
|
|
10872
11515
|
}]
|
|
@@ -10877,21 +11520,32 @@ Last seen: ${agent.last_seen_at}`
|
|
|
10877
11520
|
});
|
|
10878
11521
|
}
|
|
10879
11522
|
if (shouldRegisterTool("suggest_agent_name")) {
|
|
10880
|
-
server.tool("suggest_agent_name", "Get available agent names for a project.
|
|
10881
|
-
working_dir: exports_external.string().optional().describe("Your working directory \u2014 used to look up the project's allowed name pool")
|
|
11523
|
+
server.tool("suggest_agent_name", "Get available agent names for a project. Shows configured pool, active agents, and suggestions. If no pool is configured, any name is allowed.", {
|
|
11524
|
+
working_dir: exports_external.string().optional().describe("Your working directory \u2014 used to look up the project's allowed name pool from config")
|
|
10882
11525
|
}, async ({ working_dir }) => {
|
|
10883
11526
|
try {
|
|
10884
11527
|
const pool = getAgentPoolForProject(working_dir);
|
|
10885
|
-
const available = getAvailableNamesFromPool(pool, getDatabase());
|
|
10886
11528
|
const cutoff = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
|
10887
|
-
const
|
|
11529
|
+
const allActive = listAgents().filter((a) => a.last_seen_at > cutoff);
|
|
11530
|
+
if (!pool) {
|
|
11531
|
+
const lines2 = [
|
|
11532
|
+
"No agent pool configured \u2014 any name is allowed.",
|
|
11533
|
+
allActive.length > 0 ? `Active agents (avoid these names): ${allActive.map((a) => `${a.name} (seen ${Math.round((Date.now() - new Date(a.last_seen_at).getTime()) / 60000)}m ago)`).join(", ")}` : "No active agents.",
|
|
11534
|
+
`
|
|
11535
|
+
To restrict names, configure agent_pool or project_pools in ~/.todos/config.json`
|
|
11536
|
+
];
|
|
11537
|
+
return { content: [{ type: "text", text: lines2.join(`
|
|
11538
|
+
`) }] };
|
|
11539
|
+
}
|
|
11540
|
+
const available = getAvailableNamesFromPool(pool, getDatabase());
|
|
11541
|
+
const activeInPool = allActive.filter((a) => pool.map((n) => n.toLowerCase()).includes(a.name));
|
|
10888
11542
|
const lines = [
|
|
10889
11543
|
`Project pool: ${pool.join(", ")}`,
|
|
10890
11544
|
`Available now (${available.length}): ${available.length > 0 ? available.join(", ") : "none \u2014 all names in use"}`,
|
|
10891
|
-
|
|
11545
|
+
activeInPool.length > 0 ? `Active agents: ${activeInPool.map((a) => `${a.name} (seen ${Math.round((Date.now() - new Date(a.last_seen_at).getTime()) / 60000)}m ago)`).join(", ")}` : "Active agents: none",
|
|
10892
11546
|
available.length > 0 ? `
|
|
10893
11547
|
Suggested: ${available[0]}` : `
|
|
10894
|
-
No names available
|
|
11548
|
+
No names available. Wait for an active agent to go stale (30min timeout).`
|
|
10895
11549
|
];
|
|
10896
11550
|
return { content: [{ type: "text", text: lines.join(`
|
|
10897
11551
|
`) }] };
|
|
@@ -12234,12 +12888,12 @@ Claimed: ${formatTask(result.claimed)}`);
|
|
|
12234
12888
|
delete_plan: `Delete a plan. Tasks in the plan are orphaned, not deleted.
|
|
12235
12889
|
Params: id(string, req)
|
|
12236
12890
|
Example: {id: 'a1b2c3d4'}`,
|
|
12237
|
-
suggest_agent_name: `
|
|
12238
|
-
Params: working_dir(string \u2014 your working directory, used to look up project pool)
|
|
12891
|
+
suggest_agent_name: `Check available agent names before registering. Shows active agents and, if a pool is configured, which pool names are free.
|
|
12892
|
+
Params: working_dir(string \u2014 your working directory, used to look up project pool from config)
|
|
12239
12893
|
Example: {working_dir: '/workspace/platform'}`,
|
|
12240
|
-
register_agent: `Register an agent.
|
|
12241
|
-
Params: name(string, req), description(string), session_id(string \u2014 unique per session
|
|
12242
|
-
Example: {name: '
|
|
12894
|
+
register_agent: `Register an agent. Any name is allowed \u2014 pool is advisory. Returns CONFLICT if name is held by a recently-active agent.
|
|
12895
|
+
Params: name(string, req), description(string), capabilities(string[]), session_id(string \u2014 unique per session), working_dir(string \u2014 used to determine project pool)
|
|
12896
|
+
Example: {name: 'my-agent', session_id: 'abc123-1741952000', working_dir: '/workspace/platform'}`,
|
|
12243
12897
|
list_agents: "List all registered agents with IDs, names, and last seen timestamps. No params.",
|
|
12244
12898
|
get_agent: `Get agent details by ID or name. Provide one of id or name.
|
|
12245
12899
|
Params: id(string), name(string)
|
|
@@ -12497,6 +13151,328 @@ ${lines.join(`
|
|
|
12497
13151
|
}
|
|
12498
13152
|
});
|
|
12499
13153
|
}
|
|
13154
|
+
if (shouldRegisterTool("add_task_relationship")) {
|
|
13155
|
+
server.tool("add_task_relationship", "Create a semantic relationship between two tasks (related_to, conflicts_with, similar_to, duplicates, supersedes, modifies_same_file).", {
|
|
13156
|
+
source_task_id: exports_external.string().describe("Source task ID"),
|
|
13157
|
+
target_task_id: exports_external.string().describe("Target task ID"),
|
|
13158
|
+
relationship_type: exports_external.enum(["related_to", "conflicts_with", "similar_to", "duplicates", "supersedes", "modifies_same_file"]).describe("Type of relationship"),
|
|
13159
|
+
created_by: exports_external.string().optional().describe("Agent ID who created this relationship")
|
|
13160
|
+
}, async ({ source_task_id, target_task_id, relationship_type, created_by }) => {
|
|
13161
|
+
try {
|
|
13162
|
+
const { addTaskRelationship: addTaskRelationship2 } = (init_task_relationships(), __toCommonJS(exports_task_relationships));
|
|
13163
|
+
const rel = addTaskRelationship2({
|
|
13164
|
+
source_task_id: resolveId(source_task_id),
|
|
13165
|
+
target_task_id: resolveId(target_task_id),
|
|
13166
|
+
relationship_type,
|
|
13167
|
+
created_by
|
|
13168
|
+
});
|
|
13169
|
+
return { content: [{ type: "text", text: `Relationship created: ${rel.source_task_id.slice(0, 8)} --[${rel.relationship_type}]--> ${rel.target_task_id.slice(0, 8)}` }] };
|
|
13170
|
+
} catch (e) {
|
|
13171
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13172
|
+
}
|
|
13173
|
+
});
|
|
13174
|
+
}
|
|
13175
|
+
if (shouldRegisterTool("remove_task_relationship")) {
|
|
13176
|
+
server.tool("remove_task_relationship", "Remove a semantic relationship between tasks by ID or by source+target+type.", {
|
|
13177
|
+
id: exports_external.string().optional().describe("Relationship ID to remove"),
|
|
13178
|
+
source_task_id: exports_external.string().optional().describe("Source task ID (use with target_task_id + type)"),
|
|
13179
|
+
target_task_id: exports_external.string().optional().describe("Target task ID"),
|
|
13180
|
+
relationship_type: exports_external.enum(["related_to", "conflicts_with", "similar_to", "duplicates", "supersedes", "modifies_same_file"]).optional()
|
|
13181
|
+
}, async ({ id, source_task_id, target_task_id, relationship_type }) => {
|
|
13182
|
+
try {
|
|
13183
|
+
const { removeTaskRelationship: removeTaskRelationship2, removeTaskRelationshipByPair: removeTaskRelationshipByPair2 } = (init_task_relationships(), __toCommonJS(exports_task_relationships));
|
|
13184
|
+
let removed = false;
|
|
13185
|
+
if (id) {
|
|
13186
|
+
removed = removeTaskRelationship2(id);
|
|
13187
|
+
} else if (source_task_id && target_task_id && relationship_type) {
|
|
13188
|
+
removed = removeTaskRelationshipByPair2(resolveId(source_task_id), resolveId(target_task_id), relationship_type);
|
|
13189
|
+
} else {
|
|
13190
|
+
return { content: [{ type: "text", text: "Provide either 'id' or 'source_task_id + target_task_id + relationship_type'" }], isError: true };
|
|
13191
|
+
}
|
|
13192
|
+
return { content: [{ type: "text", text: removed ? "Relationship removed." : "Relationship not found." }] };
|
|
13193
|
+
} catch (e) {
|
|
13194
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13195
|
+
}
|
|
13196
|
+
});
|
|
13197
|
+
}
|
|
13198
|
+
if (shouldRegisterTool("get_task_relationships")) {
|
|
13199
|
+
server.tool("get_task_relationships", "Get all semantic relationships for a task.", {
|
|
13200
|
+
task_id: exports_external.string().describe("Task ID"),
|
|
13201
|
+
relationship_type: exports_external.enum(["related_to", "conflicts_with", "similar_to", "duplicates", "supersedes", "modifies_same_file"]).optional()
|
|
13202
|
+
}, async ({ task_id, relationship_type }) => {
|
|
13203
|
+
try {
|
|
13204
|
+
const { getTaskRelationships: getTaskRelationships2 } = (init_task_relationships(), __toCommonJS(exports_task_relationships));
|
|
13205
|
+
const rels = getTaskRelationships2(resolveId(task_id), relationship_type);
|
|
13206
|
+
if (rels.length === 0)
|
|
13207
|
+
return { content: [{ type: "text", text: "No relationships found." }] };
|
|
13208
|
+
const lines = rels.map((r) => `${r.source_task_id.slice(0, 8)} --[${r.relationship_type}]--> ${r.target_task_id.slice(0, 8)}${r.metadata && Object.keys(r.metadata).length > 0 ? ` (${JSON.stringify(r.metadata)})` : ""}`);
|
|
13209
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
13210
|
+
`) }] };
|
|
13211
|
+
} catch (e) {
|
|
13212
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13213
|
+
}
|
|
13214
|
+
});
|
|
13215
|
+
}
|
|
13216
|
+
if (shouldRegisterTool("detect_file_relationships")) {
|
|
13217
|
+
server.tool("detect_file_relationships", "Auto-detect tasks that modify the same files and create modifies_same_file relationships.", {
|
|
13218
|
+
task_id: exports_external.string().describe("Task ID to detect file relationships for")
|
|
13219
|
+
}, async ({ task_id }) => {
|
|
13220
|
+
try {
|
|
13221
|
+
const { autoDetectFileRelationships: autoDetectFileRelationships2 } = (init_task_relationships(), __toCommonJS(exports_task_relationships));
|
|
13222
|
+
const created = autoDetectFileRelationships2(resolveId(task_id));
|
|
13223
|
+
return { content: [{ type: "text", text: created.length > 0 ? `Created ${created.length} file relationship(s).` : "No file overlaps detected." }] };
|
|
13224
|
+
} catch (e) {
|
|
13225
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13226
|
+
}
|
|
13227
|
+
});
|
|
13228
|
+
}
|
|
13229
|
+
if (shouldRegisterTool("sync_kg")) {
|
|
13230
|
+
server.tool("sync_kg", "Sync all existing relationships into the knowledge graph edges table. Idempotent.", {}, async () => {
|
|
13231
|
+
try {
|
|
13232
|
+
const { syncKgEdges: syncKgEdges2 } = (init_kg(), __toCommonJS(exports_kg));
|
|
13233
|
+
const result = syncKgEdges2();
|
|
13234
|
+
return { content: [{ type: "text", text: `Knowledge graph synced: ${result.synced} edge(s) processed.` }] };
|
|
13235
|
+
} catch (e) {
|
|
13236
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13237
|
+
}
|
|
13238
|
+
});
|
|
13239
|
+
}
|
|
13240
|
+
if (shouldRegisterTool("get_related_entities")) {
|
|
13241
|
+
server.tool("get_related_entities", "Get entities related to a given entity in the knowledge graph.", {
|
|
13242
|
+
entity_id: exports_external.string().describe("Entity ID (task, agent, project, file path)"),
|
|
13243
|
+
relation_type: exports_external.string().optional().describe("Filter by relation type (depends_on, assigned_to, reports_to, references_file, in_project, in_plan, etc.)"),
|
|
13244
|
+
entity_type: exports_external.string().optional().describe("Filter by entity type (task, agent, project, file, plan)"),
|
|
13245
|
+
direction: exports_external.enum(["outgoing", "incoming", "both"]).optional().describe("Edge direction"),
|
|
13246
|
+
limit: exports_external.number().optional().describe("Max results")
|
|
13247
|
+
}, async ({ entity_id, relation_type, entity_type, direction, limit }) => {
|
|
13248
|
+
try {
|
|
13249
|
+
const { getRelated: getRelated2 } = (init_kg(), __toCommonJS(exports_kg));
|
|
13250
|
+
const edges = getRelated2(entity_id, { relation_type, entity_type, direction, limit });
|
|
13251
|
+
if (edges.length === 0)
|
|
13252
|
+
return { content: [{ type: "text", text: "No related entities found." }] };
|
|
13253
|
+
const lines = edges.map((e) => `${e.source_id.slice(0, 12)}(${e.source_type}) --[${e.relation_type}]--> ${e.target_id.slice(0, 12)}(${e.target_type})`);
|
|
13254
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
13255
|
+
`) }] };
|
|
13256
|
+
} catch (e) {
|
|
13257
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13258
|
+
}
|
|
13259
|
+
});
|
|
13260
|
+
}
|
|
13261
|
+
if (shouldRegisterTool("find_path")) {
|
|
13262
|
+
server.tool("find_path", "Find paths between two entities in the knowledge graph.", {
|
|
13263
|
+
source_id: exports_external.string().describe("Starting entity ID"),
|
|
13264
|
+
target_id: exports_external.string().describe("Target entity ID"),
|
|
13265
|
+
max_depth: exports_external.number().optional().describe("Maximum path depth (default: 5)"),
|
|
13266
|
+
relation_types: exports_external.array(exports_external.string()).optional().describe("Filter by relation types")
|
|
13267
|
+
}, async ({ source_id, target_id, max_depth, relation_types }) => {
|
|
13268
|
+
try {
|
|
13269
|
+
const { findPath: findPath2 } = (init_kg(), __toCommonJS(exports_kg));
|
|
13270
|
+
const paths = findPath2(source_id, target_id, { max_depth, relation_types });
|
|
13271
|
+
if (paths.length === 0)
|
|
13272
|
+
return { content: [{ type: "text", text: "No path found." }] };
|
|
13273
|
+
const lines = paths.map((path, i) => {
|
|
13274
|
+
const steps = path.map((e) => `${e.source_id.slice(0, 8)} --[${e.relation_type}]--> ${e.target_id.slice(0, 8)}`);
|
|
13275
|
+
return `Path ${i + 1} (${path.length} hops):
|
|
13276
|
+
${steps.join(`
|
|
13277
|
+
`)}`;
|
|
13278
|
+
});
|
|
13279
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
13280
|
+
|
|
13281
|
+
`) }] };
|
|
13282
|
+
} catch (e) {
|
|
13283
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13284
|
+
}
|
|
13285
|
+
});
|
|
13286
|
+
}
|
|
13287
|
+
if (shouldRegisterTool("get_impact_analysis")) {
|
|
13288
|
+
server.tool("get_impact_analysis", "Analyze what entities are affected if a given entity changes. Traverses the knowledge graph.", {
|
|
13289
|
+
entity_id: exports_external.string().describe("Entity ID to analyze impact for"),
|
|
13290
|
+
max_depth: exports_external.number().optional().describe("Maximum traversal depth (default: 3)"),
|
|
13291
|
+
relation_types: exports_external.array(exports_external.string()).optional().describe("Filter by relation types")
|
|
13292
|
+
}, async ({ entity_id, max_depth, relation_types }) => {
|
|
13293
|
+
try {
|
|
13294
|
+
const { getImpactAnalysis: getImpactAnalysis2 } = (init_kg(), __toCommonJS(exports_kg));
|
|
13295
|
+
const impact = getImpactAnalysis2(entity_id, { max_depth, relation_types });
|
|
13296
|
+
if (impact.length === 0)
|
|
13297
|
+
return { content: [{ type: "text", text: "No downstream impact detected." }] };
|
|
13298
|
+
const byDepth = new Map;
|
|
13299
|
+
for (const i of impact) {
|
|
13300
|
+
if (!byDepth.has(i.depth))
|
|
13301
|
+
byDepth.set(i.depth, []);
|
|
13302
|
+
byDepth.get(i.depth).push(i);
|
|
13303
|
+
}
|
|
13304
|
+
const lines = [`Impact analysis: ${impact.length} affected entities`];
|
|
13305
|
+
for (const [depth, entities] of [...byDepth.entries()].sort((a, b) => a[0] - b[0])) {
|
|
13306
|
+
lines.push(`
|
|
13307
|
+
Depth ${depth}:`);
|
|
13308
|
+
for (const e of entities) {
|
|
13309
|
+
lines.push(` ${e.entity_id.slice(0, 12)} (${e.entity_type}) via ${e.relation}`);
|
|
13310
|
+
}
|
|
13311
|
+
}
|
|
13312
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
13313
|
+
`) }] };
|
|
13314
|
+
} catch (e) {
|
|
13315
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13316
|
+
}
|
|
13317
|
+
});
|
|
13318
|
+
}
|
|
13319
|
+
if (shouldRegisterTool("get_critical_path")) {
|
|
13320
|
+
server.tool("get_critical_path", "Find tasks that block the most downstream work (critical path analysis).", {
|
|
13321
|
+
project_id: exports_external.string().optional().describe("Filter by project"),
|
|
13322
|
+
limit: exports_external.number().optional().describe("Max results (default: 20)")
|
|
13323
|
+
}, async ({ project_id, limit }) => {
|
|
13324
|
+
try {
|
|
13325
|
+
const { getCriticalPath: getCriticalPath2 } = (init_kg(), __toCommonJS(exports_kg));
|
|
13326
|
+
const result = getCriticalPath2({ project_id: project_id ? resolveId(project_id, "projects") : undefined, limit });
|
|
13327
|
+
if (result.length === 0)
|
|
13328
|
+
return { content: [{ type: "text", text: "No critical path data. Run sync_kg first to populate the knowledge graph." }] };
|
|
13329
|
+
const lines = result.map((r, i) => `${i + 1}. ${r.task_id.slice(0, 8)} blocks ${r.blocking_count} task(s), max depth ${r.depth}`);
|
|
13330
|
+
return { content: [{ type: "text", text: `Critical path:
|
|
13331
|
+
${lines.join(`
|
|
13332
|
+
`)}` }] };
|
|
13333
|
+
} catch (e) {
|
|
13334
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13335
|
+
}
|
|
13336
|
+
});
|
|
13337
|
+
}
|
|
13338
|
+
if (shouldRegisterTool("get_capable_agents")) {
|
|
13339
|
+
server.tool("get_capable_agents", "Find agents that match given capabilities, sorted by match score.", {
|
|
13340
|
+
capabilities: exports_external.array(exports_external.string()).describe("Required capabilities to match against"),
|
|
13341
|
+
min_score: exports_external.number().optional().describe("Minimum match score 0.0-1.0 (default: 0.1)"),
|
|
13342
|
+
limit: exports_external.number().optional().describe("Max results")
|
|
13343
|
+
}, async ({ capabilities, min_score, limit }) => {
|
|
13344
|
+
try {
|
|
13345
|
+
const { getCapableAgents: getCapableAgents2 } = (init_agents(), __toCommonJS(exports_agents));
|
|
13346
|
+
const results = getCapableAgents2(capabilities, { min_score, limit });
|
|
13347
|
+
if (results.length === 0)
|
|
13348
|
+
return { content: [{ type: "text", text: "No agents match the given capabilities." }] };
|
|
13349
|
+
const lines = results.map((r) => `${r.agent.name} (${r.agent.id}) score:${(r.score * 100).toFixed(0)}% caps:[${r.agent.capabilities.join(",")}]`);
|
|
13350
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
13351
|
+
`) }] };
|
|
13352
|
+
} catch (e) {
|
|
13353
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13354
|
+
}
|
|
13355
|
+
});
|
|
13356
|
+
}
|
|
13357
|
+
if (shouldRegisterTool("patrol_tasks")) {
|
|
13358
|
+
server.tool("patrol_tasks", "Scan for task issues: stuck tasks, low-confidence completions, orphaned tasks, zombie-blocked tasks, and pending reviews.", {
|
|
13359
|
+
stuck_minutes: exports_external.number().optional().describe("Minutes threshold for stuck detection (default: 60)"),
|
|
13360
|
+
confidence_threshold: exports_external.number().optional().describe("Confidence threshold for low-confidence detection (default: 0.5)"),
|
|
13361
|
+
project_id: exports_external.string().optional().describe("Filter by project")
|
|
13362
|
+
}, async ({ stuck_minutes, confidence_threshold, project_id }) => {
|
|
13363
|
+
try {
|
|
13364
|
+
const { patrolTasks: patrolTasks2 } = (init_patrol(), __toCommonJS(exports_patrol));
|
|
13365
|
+
const result = patrolTasks2({
|
|
13366
|
+
stuck_minutes,
|
|
13367
|
+
confidence_threshold,
|
|
13368
|
+
project_id: project_id ? resolveId(project_id, "projects") : undefined
|
|
13369
|
+
});
|
|
13370
|
+
if (result.total_issues === 0)
|
|
13371
|
+
return { content: [{ type: "text", text: "All clear \u2014 no issues detected." }] };
|
|
13372
|
+
const lines = [`Found ${result.total_issues} issue(s):
|
|
13373
|
+
`];
|
|
13374
|
+
for (const issue of result.issues) {
|
|
13375
|
+
lines.push(`[${issue.severity.toUpperCase()}] ${issue.type}: ${issue.task_title.slice(0, 60)} (${issue.task_id.slice(0, 8)})`);
|
|
13376
|
+
lines.push(` ${issue.detail}`);
|
|
13377
|
+
}
|
|
13378
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
13379
|
+
`) }] };
|
|
13380
|
+
} catch (e) {
|
|
13381
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13382
|
+
}
|
|
13383
|
+
});
|
|
13384
|
+
}
|
|
13385
|
+
if (shouldRegisterTool("get_review_queue")) {
|
|
13386
|
+
server.tool("get_review_queue", "Get tasks that need review: requires_approval but unapproved, or low confidence completions.", {
|
|
13387
|
+
project_id: exports_external.string().optional().describe("Filter by project"),
|
|
13388
|
+
limit: exports_external.number().optional().describe("Max results (default: all)")
|
|
13389
|
+
}, async ({ project_id, limit }) => {
|
|
13390
|
+
try {
|
|
13391
|
+
const { getReviewQueue: getReviewQueue2 } = (init_patrol(), __toCommonJS(exports_patrol));
|
|
13392
|
+
const tasks = getReviewQueue2({
|
|
13393
|
+
project_id: project_id ? resolveId(project_id, "projects") : undefined,
|
|
13394
|
+
limit
|
|
13395
|
+
});
|
|
13396
|
+
if (tasks.length === 0)
|
|
13397
|
+
return { content: [{ type: "text", text: "Review queue is empty." }] };
|
|
13398
|
+
const lines = tasks.map((t) => {
|
|
13399
|
+
const conf = t.confidence != null ? ` confidence:${t.confidence}` : "";
|
|
13400
|
+
const approval = t.requires_approval && !t.approved_by ? " [needs approval]" : "";
|
|
13401
|
+
return `${t.short_id || t.id.slice(0, 8)} ${t.title.slice(0, 60)}${conf}${approval}`;
|
|
13402
|
+
});
|
|
13403
|
+
return { content: [{ type: "text", text: `Review queue (${tasks.length}):
|
|
13404
|
+
${lines.join(`
|
|
13405
|
+
`)}` }] };
|
|
13406
|
+
} catch (e) {
|
|
13407
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13408
|
+
}
|
|
13409
|
+
});
|
|
13410
|
+
}
|
|
13411
|
+
if (shouldRegisterTool("score_task")) {
|
|
13412
|
+
server.tool("score_task", "Score a completed task's quality (0.0-1.0). Stores in task metadata for agent performance tracking.", {
|
|
13413
|
+
task_id: exports_external.string().describe("Task ID to score"),
|
|
13414
|
+
score: exports_external.number().min(0).max(1).describe("Quality score 0.0-1.0"),
|
|
13415
|
+
reviewer_id: exports_external.string().optional().describe("Agent ID of reviewer")
|
|
13416
|
+
}, async ({ task_id, score, reviewer_id }) => {
|
|
13417
|
+
try {
|
|
13418
|
+
const { scoreTask: scoreTask2 } = (init_agent_metrics(), __toCommonJS(exports_agent_metrics));
|
|
13419
|
+
scoreTask2(resolveId(task_id), score, reviewer_id);
|
|
13420
|
+
return { content: [{ type: "text", text: `Task ${task_id.slice(0, 8)} scored: ${score}` }] };
|
|
13421
|
+
} catch (e) {
|
|
13422
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13423
|
+
}
|
|
13424
|
+
});
|
|
13425
|
+
}
|
|
13426
|
+
if (shouldRegisterTool("get_agent_metrics")) {
|
|
13427
|
+
server.tool("get_agent_metrics", "Get performance metrics for an agent: completion rate, speed, confidence, review scores.", {
|
|
13428
|
+
agent_id: exports_external.string().describe("Agent ID or name"),
|
|
13429
|
+
project_id: exports_external.string().optional().describe("Filter by project")
|
|
13430
|
+
}, async ({ agent_id, project_id }) => {
|
|
13431
|
+
try {
|
|
13432
|
+
const { getAgentMetrics: getAgentMetrics2 } = (init_agent_metrics(), __toCommonJS(exports_agent_metrics));
|
|
13433
|
+
const metrics = getAgentMetrics2(agent_id, {
|
|
13434
|
+
project_id: project_id ? resolveId(project_id, "projects") : undefined
|
|
13435
|
+
});
|
|
13436
|
+
if (!metrics)
|
|
13437
|
+
return { content: [{ type: "text", text: `Agent not found: ${agent_id}` }], isError: true };
|
|
13438
|
+
const lines = [
|
|
13439
|
+
`Agent: ${metrics.agent_name} (${metrics.agent_id})`,
|
|
13440
|
+
`Completed: ${metrics.tasks_completed} | Failed: ${metrics.tasks_failed} | In Progress: ${metrics.tasks_in_progress}`,
|
|
13441
|
+
`Completion Rate: ${(metrics.completion_rate * 100).toFixed(1)}%`,
|
|
13442
|
+
metrics.avg_completion_minutes != null ? `Avg Completion Time: ${metrics.avg_completion_minutes} min` : null,
|
|
13443
|
+
metrics.avg_confidence != null ? `Avg Confidence: ${(metrics.avg_confidence * 100).toFixed(1)}%` : null,
|
|
13444
|
+
metrics.review_score_avg != null ? `Avg Review Score: ${(metrics.review_score_avg * 100).toFixed(1)}%` : null,
|
|
13445
|
+
`Composite Score: ${(metrics.composite_score * 100).toFixed(1)}%`
|
|
13446
|
+
].filter(Boolean);
|
|
13447
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
13448
|
+
`) }] };
|
|
13449
|
+
} catch (e) {
|
|
13450
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13451
|
+
}
|
|
13452
|
+
});
|
|
13453
|
+
}
|
|
13454
|
+
if (shouldRegisterTool("get_leaderboard")) {
|
|
13455
|
+
server.tool("get_leaderboard", "Get agent leaderboard ranked by composite performance score.", {
|
|
13456
|
+
project_id: exports_external.string().optional().describe("Filter by project"),
|
|
13457
|
+
limit: exports_external.number().optional().describe("Max entries (default: 20)")
|
|
13458
|
+
}, async ({ project_id, limit }) => {
|
|
13459
|
+
try {
|
|
13460
|
+
const { getLeaderboard: getLeaderboard2 } = (init_agent_metrics(), __toCommonJS(exports_agent_metrics));
|
|
13461
|
+
const entries = getLeaderboard2({
|
|
13462
|
+
project_id: project_id ? resolveId(project_id, "projects") : undefined,
|
|
13463
|
+
limit
|
|
13464
|
+
});
|
|
13465
|
+
if (entries.length === 0)
|
|
13466
|
+
return { content: [{ type: "text", text: "No agents with task activity found." }] };
|
|
13467
|
+
const lines = entries.map((e) => `#${e.rank} ${e.agent_name.padEnd(15)} score:${(e.composite_score * 100).toFixed(0).padStart(3)}% done:${String(e.tasks_completed).padStart(3)} rate:${(e.completion_rate * 100).toFixed(0)}%`);
|
|
13468
|
+
return { content: [{ type: "text", text: `Leaderboard:
|
|
13469
|
+
${lines.join(`
|
|
13470
|
+
`)}` }] };
|
|
13471
|
+
} catch (e) {
|
|
13472
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13473
|
+
}
|
|
13474
|
+
});
|
|
13475
|
+
}
|
|
12500
13476
|
server.resource("task-lists", "todos://task-lists", { description: "All task lists", mimeType: "application/json" }, async () => {
|
|
12501
13477
|
const lists = listTaskLists();
|
|
12502
13478
|
return { contents: [{ uri: "todos://task-lists", text: JSON.stringify(lists, null, 2), mimeType: "application/json" }] };
|