@hasna/todos 0.11.58 → 0.11.59
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/README.md +38 -0
- package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +1420 -187
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +502 -4
- package/dist/db/findings.d.ts +108 -0
- package/dist/db/findings.d.ts.map +1 -0
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/task-crud.d.ts +3 -1
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/db/task-runs.d.ts +56 -0
- package/dist/db/task-runs.d.ts.map +1 -1
- package/dist/db/tasks.d.ts +2 -2
- package/dist/db/tasks.d.ts.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +870 -5
- package/dist/json-contracts.d.ts.map +1 -1
- package/dist/lib/access-profiles.d.ts.map +1 -1
- package/dist/lib/event-hooks.d.ts +1 -1
- package/dist/lib/event-hooks.d.ts.map +1 -1
- package/dist/lib/shared-events.d.ts +1 -1
- package/dist/lib/shared-events.d.ts.map +1 -1
- package/dist/mcp/index.js +984 -17
- package/dist/mcp/token-utils.d.ts.map +1 -1
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp/tools/task-resources.d.ts.map +1 -1
- package/dist/mcp.js +12 -1
- package/dist/registry.js +502 -4
- package/dist/release-provenance.json +3 -3
- package/dist/server/index.js +984 -17
- package/dist/server/routes.d.ts +1 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/serve.d.ts +2 -0
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage.js +375 -1
- package/dist/types/index.d.ts +11 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -1189,6 +1189,49 @@ var init_migrations = __esm(() => {
|
|
|
1189
1189
|
CREATE INDEX IF NOT EXISTS idx_local_retrospectives_plan ON local_retrospectives(plan_id);
|
|
1190
1190
|
CREATE INDEX IF NOT EXISTS idx_local_retrospectives_agent ON local_retrospectives(agent_id);
|
|
1191
1191
|
INSERT OR IGNORE INTO _migrations (id) VALUES (61);
|
|
1192
|
+
`,
|
|
1193
|
+
`
|
|
1194
|
+
CREATE TABLE IF NOT EXISTS task_run_transactions (
|
|
1195
|
+
id TEXT PRIMARY KEY,
|
|
1196
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1197
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1198
|
+
key TEXT NOT NULL,
|
|
1199
|
+
loop_id TEXT,
|
|
1200
|
+
loop_run_id TEXT,
|
|
1201
|
+
metadata TEXT DEFAULT '{}',
|
|
1202
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1203
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1204
|
+
UNIQUE(task_id, key)
|
|
1205
|
+
);
|
|
1206
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_transactions_task_key ON task_run_transactions(task_id, key);
|
|
1207
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_transactions_key ON task_run_transactions(key);
|
|
1208
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_transactions_run ON task_run_transactions(run_id);
|
|
1209
|
+
|
|
1210
|
+
CREATE TABLE IF NOT EXISTS task_findings (
|
|
1211
|
+
id TEXT PRIMARY KEY,
|
|
1212
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1213
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1214
|
+
fingerprint TEXT NOT NULL,
|
|
1215
|
+
title TEXT NOT NULL,
|
|
1216
|
+
severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low', 'medium', 'high', 'critical')),
|
|
1217
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'resolved', 'ignored')),
|
|
1218
|
+
source TEXT,
|
|
1219
|
+
summary TEXT,
|
|
1220
|
+
artifact_path TEXT,
|
|
1221
|
+
metadata TEXT DEFAULT '{}',
|
|
1222
|
+
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1223
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1224
|
+
resolved_at TEXT,
|
|
1225
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1226
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1227
|
+
UNIQUE(task_id, fingerprint)
|
|
1228
|
+
);
|
|
1229
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_task ON task_findings(task_id);
|
|
1230
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_run ON task_findings(run_id);
|
|
1231
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_status ON task_findings(status);
|
|
1232
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source);
|
|
1233
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint);
|
|
1234
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (62);
|
|
1192
1235
|
`
|
|
1193
1236
|
];
|
|
1194
1237
|
});
|
|
@@ -1626,6 +1669,47 @@ function ensureSchema(db) {
|
|
|
1626
1669
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_run ON task_run_artifacts(run_id)");
|
|
1627
1670
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_task ON task_run_artifacts(task_id)");
|
|
1628
1671
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_path ON task_run_artifacts(path)");
|
|
1672
|
+
ensureTable("task_run_transactions", `
|
|
1673
|
+
CREATE TABLE task_run_transactions (
|
|
1674
|
+
id TEXT PRIMARY KEY,
|
|
1675
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1676
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1677
|
+
key TEXT NOT NULL,
|
|
1678
|
+
loop_id TEXT,
|
|
1679
|
+
loop_run_id TEXT,
|
|
1680
|
+
metadata TEXT DEFAULT '{}',
|
|
1681
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1682
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1683
|
+
UNIQUE(task_id, key)
|
|
1684
|
+
)`);
|
|
1685
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_task_key ON task_run_transactions(task_id, key)");
|
|
1686
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_key ON task_run_transactions(key)");
|
|
1687
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_run ON task_run_transactions(run_id)");
|
|
1688
|
+
ensureTable("task_findings", `
|
|
1689
|
+
CREATE TABLE task_findings (
|
|
1690
|
+
id TEXT PRIMARY KEY,
|
|
1691
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1692
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1693
|
+
fingerprint TEXT NOT NULL,
|
|
1694
|
+
title TEXT NOT NULL,
|
|
1695
|
+
severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low', 'medium', 'high', 'critical')),
|
|
1696
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'resolved', 'ignored')),
|
|
1697
|
+
source TEXT,
|
|
1698
|
+
summary TEXT,
|
|
1699
|
+
artifact_path TEXT,
|
|
1700
|
+
metadata TEXT DEFAULT '{}',
|
|
1701
|
+
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1702
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1703
|
+
resolved_at TEXT,
|
|
1704
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1705
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1706
|
+
UNIQUE(task_id, fingerprint)
|
|
1707
|
+
)`);
|
|
1708
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_task ON task_findings(task_id)");
|
|
1709
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_run ON task_findings(run_id)");
|
|
1710
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_status ON task_findings(status)");
|
|
1711
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source)");
|
|
1712
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint)");
|
|
1629
1713
|
ensureTable("inbox_items", `
|
|
1630
1714
|
CREATE TABLE inbox_items (
|
|
1631
1715
|
id TEXT PRIMARY KEY,
|
|
@@ -3994,6 +4078,7 @@ var init_event_hooks = __esm(() => {
|
|
|
3994
4078
|
"task.blocked",
|
|
3995
4079
|
"task.started",
|
|
3996
4080
|
"task.completed",
|
|
4081
|
+
"task.updated",
|
|
3997
4082
|
"task.due",
|
|
3998
4083
|
"task.due_soon",
|
|
3999
4084
|
"task.failed",
|
|
@@ -5290,6 +5375,17 @@ function replaceTaskTags(taskId, tags, db) {
|
|
|
5290
5375
|
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
5291
5376
|
insertTaskTags(taskId, tags, db);
|
|
5292
5377
|
}
|
|
5378
|
+
function addMetadataConditions(metadata, conditions, params) {
|
|
5379
|
+
if (!metadata)
|
|
5380
|
+
return;
|
|
5381
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
5382
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
|
|
5383
|
+
throw new Error(`Invalid metadata filter key: ${key}`);
|
|
5384
|
+
}
|
|
5385
|
+
conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
|
|
5386
|
+
params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
|
|
5387
|
+
}
|
|
5388
|
+
}
|
|
5293
5389
|
function createTask(input, db) {
|
|
5294
5390
|
const d = db || getDatabase();
|
|
5295
5391
|
const timestamp = now();
|
|
@@ -5472,6 +5568,7 @@ function listTasks(filter = {}, db) {
|
|
|
5472
5568
|
params.push(filter.task_type);
|
|
5473
5569
|
}
|
|
5474
5570
|
}
|
|
5571
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
5475
5572
|
const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
|
|
5476
5573
|
if (filter.cursor) {
|
|
5477
5574
|
try {
|
|
@@ -5496,6 +5593,54 @@ function listTasks(filter = {}, db) {
|
|
|
5496
5593
|
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
|
|
5497
5594
|
return rows.map(rowToTask);
|
|
5498
5595
|
}
|
|
5596
|
+
function getTaskByFingerprint(fingerprint, db) {
|
|
5597
|
+
const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
|
|
5598
|
+
return tasks[0] ?? null;
|
|
5599
|
+
}
|
|
5600
|
+
function mergeTaskMetadata(current, next, fingerprint) {
|
|
5601
|
+
return {
|
|
5602
|
+
...current,
|
|
5603
|
+
...next ?? {},
|
|
5604
|
+
fingerprint
|
|
5605
|
+
};
|
|
5606
|
+
}
|
|
5607
|
+
function upsertTaskByFingerprint(input, db) {
|
|
5608
|
+
const d = db || getDatabase();
|
|
5609
|
+
const fingerprint = input.fingerprint.trim();
|
|
5610
|
+
if (!fingerprint)
|
|
5611
|
+
throw new Error("fingerprint is required");
|
|
5612
|
+
const existing = getTaskByFingerprint(fingerprint, d);
|
|
5613
|
+
const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
|
|
5614
|
+
if (!existing) {
|
|
5615
|
+
const task2 = createTask({ ...input, metadata }, d);
|
|
5616
|
+
return { task: task2, created: true };
|
|
5617
|
+
}
|
|
5618
|
+
const task = updateTask(existing.id, {
|
|
5619
|
+
version: existing.version,
|
|
5620
|
+
title: input.title,
|
|
5621
|
+
description: input.description,
|
|
5622
|
+
status: input.status,
|
|
5623
|
+
priority: input.priority,
|
|
5624
|
+
project_id: input.project_id,
|
|
5625
|
+
assigned_to: input.assigned_to,
|
|
5626
|
+
working_dir: input.working_dir,
|
|
5627
|
+
plan_id: input.plan_id,
|
|
5628
|
+
task_list_id: input.task_list_id,
|
|
5629
|
+
tags: input.tags,
|
|
5630
|
+
metadata,
|
|
5631
|
+
due_at: input.due_at,
|
|
5632
|
+
estimated_minutes: input.estimated_minutes,
|
|
5633
|
+
sla_minutes: input.sla_minutes,
|
|
5634
|
+
confidence: input.confidence,
|
|
5635
|
+
retry_count: input.retry_count,
|
|
5636
|
+
max_retries: input.max_retries,
|
|
5637
|
+
retry_after: input.retry_after,
|
|
5638
|
+
requires_approval: input.requires_approval,
|
|
5639
|
+
recurrence_rule: input.recurrence_rule,
|
|
5640
|
+
task_type: input.task_type
|
|
5641
|
+
}, d);
|
|
5642
|
+
return { task, created: false };
|
|
5643
|
+
}
|
|
5499
5644
|
function countTasks(filter = {}, db) {
|
|
5500
5645
|
const d = db || getDatabase();
|
|
5501
5646
|
const conditions = [];
|
|
@@ -5559,6 +5704,7 @@ function countTasks(filter = {}, db) {
|
|
|
5559
5704
|
conditions.push("task_list_id = ?");
|
|
5560
5705
|
params.push(filter.task_list_id);
|
|
5561
5706
|
}
|
|
5707
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
5562
5708
|
if (!filter.include_archived) {
|
|
5563
5709
|
conditions.push("archived_at IS NULL");
|
|
5564
5710
|
}
|
|
@@ -5609,6 +5755,10 @@ function updateTask(id, input, db) {
|
|
|
5609
5755
|
sets.push("assigned_to = ?");
|
|
5610
5756
|
params.push(input.assigned_to);
|
|
5611
5757
|
}
|
|
5758
|
+
if (input.working_dir !== undefined) {
|
|
5759
|
+
sets.push("working_dir = ?");
|
|
5760
|
+
params.push(input.working_dir);
|
|
5761
|
+
}
|
|
5612
5762
|
if (input.tags !== undefined) {
|
|
5613
5763
|
sets.push("tags = ?");
|
|
5614
5764
|
params.push(JSON.stringify(input.tags));
|
|
@@ -5697,6 +5847,8 @@ function updateTask(id, input, db) {
|
|
|
5697
5847
|
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
5698
5848
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
5699
5849
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
5850
|
+
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
5851
|
+
logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
5700
5852
|
if (input.approved_by !== undefined)
|
|
5701
5853
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
5702
5854
|
const updatedTask = {
|
|
@@ -5732,6 +5884,10 @@ function updateTask(id, input, db) {
|
|
|
5732
5884
|
if (input.approved_by !== undefined) {
|
|
5733
5885
|
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
5734
5886
|
}
|
|
5887
|
+
const updatePayload = taskEventData(updatedTask);
|
|
5888
|
+
dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
|
|
5889
|
+
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
|
|
5890
|
+
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
|
|
5735
5891
|
return updatedTask;
|
|
5736
5892
|
}
|
|
5737
5893
|
function deleteTask(id, db) {
|
|
@@ -8749,6 +8905,72 @@ function rowToArtifact(row) {
|
|
|
8749
8905
|
function getRunRow(runId, db) {
|
|
8750
8906
|
return db.query("SELECT * FROM task_runs WHERE id = ?").get(runId);
|
|
8751
8907
|
}
|
|
8908
|
+
function normalizeTransactionKey(input) {
|
|
8909
|
+
const key = (input.key || input.loop_run_id || input.loop_id || "").trim();
|
|
8910
|
+
if (!key)
|
|
8911
|
+
throw new Error("idempotent run transactions require --key, --loop-run-id, or --loop-id");
|
|
8912
|
+
return key.toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 240);
|
|
8913
|
+
}
|
|
8914
|
+
function loopTransactionMetadata(record) {
|
|
8915
|
+
const value = record.metadata["loop_transaction"];
|
|
8916
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
8917
|
+
}
|
|
8918
|
+
function runKey(record) {
|
|
8919
|
+
const tx = loopTransactionMetadata(record);
|
|
8920
|
+
const key = tx["idempotency_key"] ?? record.metadata["idempotency_key"];
|
|
8921
|
+
return typeof key === "string" ? key : null;
|
|
8922
|
+
}
|
|
8923
|
+
function loopId(record) {
|
|
8924
|
+
const tx = loopTransactionMetadata(record);
|
|
8925
|
+
const value = tx["loop_id"] ?? record.metadata["loop_id"];
|
|
8926
|
+
return typeof value === "string" ? value : null;
|
|
8927
|
+
}
|
|
8928
|
+
function loopRunId(record) {
|
|
8929
|
+
const tx = loopTransactionMetadata(record);
|
|
8930
|
+
const value = tx["loop_run_id"] ?? record.metadata["loop_run_id"];
|
|
8931
|
+
return typeof value === "string" ? value : null;
|
|
8932
|
+
}
|
|
8933
|
+
function getTaskRunTransactionByKey(key, taskId, db) {
|
|
8934
|
+
if (taskId) {
|
|
8935
|
+
return db.query("SELECT * FROM task_run_transactions WHERE task_id = ? AND key = ?").get(taskId, key);
|
|
8936
|
+
}
|
|
8937
|
+
const rows = db.query("SELECT * FROM task_run_transactions WHERE key = ? ORDER BY created_at DESC LIMIT 2").all(key);
|
|
8938
|
+
if (rows.length > 1)
|
|
8939
|
+
throw new Error(`Run transaction key is ambiguous across tasks: ${key}. Pass task_id.`);
|
|
8940
|
+
return rows[0] ?? null;
|
|
8941
|
+
}
|
|
8942
|
+
function summarizeTaskRun(run) {
|
|
8943
|
+
return {
|
|
8944
|
+
id: run.id,
|
|
8945
|
+
task_id: run.task_id,
|
|
8946
|
+
agent_id: run.agent_id,
|
|
8947
|
+
title: run.title,
|
|
8948
|
+
status: run.status,
|
|
8949
|
+
summary: run.summary,
|
|
8950
|
+
idempotency_key: runKey(run),
|
|
8951
|
+
loop_id: loopId(run),
|
|
8952
|
+
loop_run_id: loopRunId(run),
|
|
8953
|
+
metadata_keys: Object.keys(run.metadata).sort(),
|
|
8954
|
+
started_at: run.started_at,
|
|
8955
|
+
completed_at: run.completed_at,
|
|
8956
|
+
updated_at: run.updated_at
|
|
8957
|
+
};
|
|
8958
|
+
}
|
|
8959
|
+
function findTaskRunByTransactionKey(key, taskId, db) {
|
|
8960
|
+
const d = db || getDatabase();
|
|
8961
|
+
const normalized = normalizeTransactionKey({ key });
|
|
8962
|
+
const transaction = getTaskRunTransactionByKey(normalized, taskId, d);
|
|
8963
|
+
if (transaction?.run_id)
|
|
8964
|
+
return getTaskRun(transaction.run_id, d);
|
|
8965
|
+
return null;
|
|
8966
|
+
}
|
|
8967
|
+
function loopRunCommands(run, key) {
|
|
8968
|
+
return [
|
|
8969
|
+
run ? `todos runs show ${run.id.slice(0, 8)}` : "todos runs list",
|
|
8970
|
+
run ? `todos findings list --task ${run.task_id.slice(0, 8)} --json` : "todos findings list --json",
|
|
8971
|
+
`todos runs begin <task-id> --key ${key} --apply --json`
|
|
8972
|
+
];
|
|
8973
|
+
}
|
|
8752
8974
|
function resolveTaskRunId(idOrPrefix, db) {
|
|
8753
8975
|
const d = db || getDatabase();
|
|
8754
8976
|
const rows = d.query("SELECT id FROM task_runs WHERE id = ? OR id LIKE ? ORDER BY created_at DESC LIMIT 2").all(idOrPrefix, `${idOrPrefix}%`);
|
|
@@ -8767,7 +8989,7 @@ function startTaskRun(input, db) {
|
|
|
8767
8989
|
const d = db || getDatabase();
|
|
8768
8990
|
if (!getTask(input.task_id, d))
|
|
8769
8991
|
throw new TaskNotFoundError(input.task_id);
|
|
8770
|
-
const id = uuid();
|
|
8992
|
+
const id = input.id ?? uuid();
|
|
8771
8993
|
const timestamp = input.started_at || now();
|
|
8772
8994
|
if (input.claim && input.agent_id) {
|
|
8773
8995
|
startTask(input.task_id, input.agent_id, d);
|
|
@@ -8805,6 +9027,97 @@ function startTaskRun(input, db) {
|
|
|
8805
9027
|
emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
|
|
8806
9028
|
return run;
|
|
8807
9029
|
}
|
|
9030
|
+
function beginTaskRunTransaction(input, db) {
|
|
9031
|
+
const d = db || getDatabase();
|
|
9032
|
+
if (!getTask(input.task_id, d))
|
|
9033
|
+
throw new TaskNotFoundError(input.task_id);
|
|
9034
|
+
const timestamp = input.started_at || now();
|
|
9035
|
+
const key = normalizeTransactionKey(input);
|
|
9036
|
+
const existing = findTaskRunByTransactionKey(key, input.task_id, d);
|
|
9037
|
+
const dryRun = !input.apply;
|
|
9038
|
+
if (existing) {
|
|
9039
|
+
return {
|
|
9040
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9041
|
+
local_only: true,
|
|
9042
|
+
dry_run: dryRun,
|
|
9043
|
+
processed_at: timestamp,
|
|
9044
|
+
action: "matched",
|
|
9045
|
+
key,
|
|
9046
|
+
run: summarizeTaskRun(existing),
|
|
9047
|
+
warnings: existing.status === "running" ? [] : [`matched ${existing.status} run`],
|
|
9048
|
+
commands: loopRunCommands(existing, key)
|
|
9049
|
+
};
|
|
9050
|
+
}
|
|
9051
|
+
if (dryRun) {
|
|
9052
|
+
return {
|
|
9053
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9054
|
+
local_only: true,
|
|
9055
|
+
dry_run: true,
|
|
9056
|
+
processed_at: timestamp,
|
|
9057
|
+
action: "preview",
|
|
9058
|
+
key,
|
|
9059
|
+
run: null,
|
|
9060
|
+
warnings: [],
|
|
9061
|
+
commands: loopRunCommands(null, key)
|
|
9062
|
+
};
|
|
9063
|
+
}
|
|
9064
|
+
const metadata = redactValue({
|
|
9065
|
+
...input.metadata || {},
|
|
9066
|
+
loop_transaction: {
|
|
9067
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9068
|
+
idempotency_key: key,
|
|
9069
|
+
loop_id: input.loop_id ?? null,
|
|
9070
|
+
loop_run_id: input.loop_run_id ?? null,
|
|
9071
|
+
first_seen_at: timestamp
|
|
9072
|
+
},
|
|
9073
|
+
idempotency_key: key
|
|
9074
|
+
});
|
|
9075
|
+
const created = d.transaction(() => {
|
|
9076
|
+
d.run(`INSERT OR IGNORE INTO task_run_transactions (
|
|
9077
|
+
id, task_id, run_id, key, loop_id, loop_run_id, metadata, created_at, updated_at
|
|
9078
|
+
) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?)`, [
|
|
9079
|
+
uuid(),
|
|
9080
|
+
input.task_id,
|
|
9081
|
+
key,
|
|
9082
|
+
input.loop_id ?? null,
|
|
9083
|
+
input.loop_run_id ?? null,
|
|
9084
|
+
JSON.stringify(metadata),
|
|
9085
|
+
timestamp,
|
|
9086
|
+
timestamp
|
|
9087
|
+
]);
|
|
9088
|
+
const transaction = getTaskRunTransactionByKey(key, input.task_id, d);
|
|
9089
|
+
if (!transaction)
|
|
9090
|
+
throw new Error(`Could not create run transaction for key: ${key}`);
|
|
9091
|
+
if (transaction.run_id) {
|
|
9092
|
+
const existingRun = getTaskRun(transaction.run_id, d);
|
|
9093
|
+
if (existingRun)
|
|
9094
|
+
return { run: existingRun, action: "matched" };
|
|
9095
|
+
}
|
|
9096
|
+
const run = startTaskRun({
|
|
9097
|
+
id: uuid(),
|
|
9098
|
+
task_id: input.task_id,
|
|
9099
|
+
agent_id: input.agent_id,
|
|
9100
|
+
title: input.title,
|
|
9101
|
+
summary: input.summary,
|
|
9102
|
+
metadata,
|
|
9103
|
+
claim: input.claim,
|
|
9104
|
+
started_at: timestamp
|
|
9105
|
+
}, d);
|
|
9106
|
+
d.run("UPDATE task_run_transactions SET run_id = ?, loop_id = COALESCE(?, loop_id), loop_run_id = COALESCE(?, loop_run_id), metadata = ?, updated_at = ? WHERE id = ?", [run.id, input.loop_id ?? null, input.loop_run_id ?? null, JSON.stringify(metadata), timestamp, transaction.id]);
|
|
9107
|
+
return { run, action: "created" };
|
|
9108
|
+
})();
|
|
9109
|
+
return {
|
|
9110
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9111
|
+
local_only: true,
|
|
9112
|
+
dry_run: false,
|
|
9113
|
+
processed_at: timestamp,
|
|
9114
|
+
action: created.action,
|
|
9115
|
+
key,
|
|
9116
|
+
run: summarizeTaskRun(created.run),
|
|
9117
|
+
warnings: [],
|
|
9118
|
+
commands: loopRunCommands(created.run, key)
|
|
9119
|
+
};
|
|
9120
|
+
}
|
|
8808
9121
|
function addTaskRunEvent(input, db) {
|
|
8809
9122
|
const d = db || getDatabase();
|
|
8810
9123
|
const runId = resolveTaskRunId(input.run_id, d);
|
|
@@ -8984,6 +9297,66 @@ function finishTaskRun(input, db) {
|
|
|
8984
9297
|
});
|
|
8985
9298
|
return updated;
|
|
8986
9299
|
}
|
|
9300
|
+
function finishTaskRunTransaction(input, db) {
|
|
9301
|
+
const d = db || getDatabase();
|
|
9302
|
+
const timestamp = input.completed_at || now();
|
|
9303
|
+
const status = input.status || "completed";
|
|
9304
|
+
const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
|
|
9305
|
+
const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
|
|
9306
|
+
if (!run) {
|
|
9307
|
+
throw new Error(input.run_id ? `Run not found: ${input.run_id}` : "runs finish requires a run id or --key");
|
|
9308
|
+
}
|
|
9309
|
+
if (input.task_id && run.task_id !== input.task_id) {
|
|
9310
|
+
throw new Error(`Run ${run.id} belongs to task ${run.task_id}, not ${input.task_id}`);
|
|
9311
|
+
}
|
|
9312
|
+
const resolvedKey = key || runKey(run) || run.id;
|
|
9313
|
+
const dryRun = input.apply === false;
|
|
9314
|
+
if (run.status !== "running") {
|
|
9315
|
+
const conflict = run.status !== status;
|
|
9316
|
+
return {
|
|
9317
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9318
|
+
local_only: true,
|
|
9319
|
+
dry_run: dryRun,
|
|
9320
|
+
processed_at: timestamp,
|
|
9321
|
+
action: conflict ? "conflict" : "matched",
|
|
9322
|
+
key: resolvedKey,
|
|
9323
|
+
run: summarizeTaskRun(run),
|
|
9324
|
+
warnings: conflict ? [`run is already ${run.status}; requested ${status}`] : [],
|
|
9325
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
9326
|
+
};
|
|
9327
|
+
}
|
|
9328
|
+
if (dryRun) {
|
|
9329
|
+
return {
|
|
9330
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9331
|
+
local_only: true,
|
|
9332
|
+
dry_run: true,
|
|
9333
|
+
processed_at: timestamp,
|
|
9334
|
+
action: "preview",
|
|
9335
|
+
key: resolvedKey,
|
|
9336
|
+
run: summarizeTaskRun({ ...run, status, summary: input.summary ?? run.summary, completed_at: timestamp, updated_at: timestamp }),
|
|
9337
|
+
warnings: [],
|
|
9338
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
9339
|
+
};
|
|
9340
|
+
}
|
|
9341
|
+
const finished = finishTaskRun({
|
|
9342
|
+
run_id: run.id,
|
|
9343
|
+
status,
|
|
9344
|
+
summary: input.summary,
|
|
9345
|
+
agent_id: input.agent_id,
|
|
9346
|
+
completed_at: timestamp
|
|
9347
|
+
}, d);
|
|
9348
|
+
return {
|
|
9349
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9350
|
+
local_only: true,
|
|
9351
|
+
dry_run: false,
|
|
9352
|
+
processed_at: timestamp,
|
|
9353
|
+
action: "finished",
|
|
9354
|
+
key: resolvedKey,
|
|
9355
|
+
run: summarizeTaskRun(finished),
|
|
9356
|
+
warnings: [],
|
|
9357
|
+
commands: loopRunCommands(finished, resolvedKey)
|
|
9358
|
+
};
|
|
9359
|
+
}
|
|
8987
9360
|
function listTaskRuns(taskId, db) {
|
|
8988
9361
|
const d = db || getDatabase();
|
|
8989
9362
|
const rows = taskId ? d.query("SELECT * FROM task_runs WHERE task_id = ? ORDER BY started_at DESC, created_at DESC").all(taskId) : d.query("SELECT * FROM task_runs ORDER BY started_at DESC, created_at DESC LIMIT 100").all();
|
|
@@ -9001,6 +9374,7 @@ function getTaskRunLedger(runId, db) {
|
|
|
9001
9374
|
const files = d.query("SELECT * FROM task_files WHERE task_id = ? ORDER BY updated_at DESC, path").all(run.task_id);
|
|
9002
9375
|
return { run, events, commands, artifacts, files };
|
|
9003
9376
|
}
|
|
9377
|
+
var LOOP_RUN_TRANSACTION_SCHEMA_VERSION = "todos.loop_run_transaction.v1";
|
|
9004
9378
|
var init_task_runs = __esm(() => {
|
|
9005
9379
|
init_artifact_store();
|
|
9006
9380
|
init_event_hooks();
|
|
@@ -9389,6 +9763,7 @@ var init_calendar = __esm(() => {
|
|
|
9389
9763
|
var exports_tasks = {};
|
|
9390
9764
|
__export(exports_tasks, {
|
|
9391
9765
|
watchTask: () => watchTask,
|
|
9766
|
+
upsertTaskByFingerprint: () => upsertTaskByFingerprint,
|
|
9392
9767
|
updateTaskBoard: () => updateTaskBoard,
|
|
9393
9768
|
updateTask: () => updateTask,
|
|
9394
9769
|
unwatchTask: () => unwatchTask,
|
|
@@ -9432,6 +9807,7 @@ __export(exports_tasks, {
|
|
|
9432
9807
|
getTaskGraph: () => getTaskGraph,
|
|
9433
9808
|
getTaskDependents: () => getTaskDependents,
|
|
9434
9809
|
getTaskDependencies: () => getTaskDependencies,
|
|
9810
|
+
getTaskByFingerprint: () => getTaskByFingerprint,
|
|
9435
9811
|
getTaskBoard: () => getTaskBoard,
|
|
9436
9812
|
getTask: () => getTask,
|
|
9437
9813
|
getStatus: () => getStatus,
|
|
@@ -10825,6 +11201,39 @@ async function handleCreateTask(req, ctx, json2, taskToSummary2) {
|
|
|
10825
11201
|
return json2({ error: e instanceof Error ? e.message : "Failed to create task" }, 500);
|
|
10826
11202
|
}
|
|
10827
11203
|
}
|
|
11204
|
+
async function handleUpsertTask(req, ctx, json2, taskToSummary2) {
|
|
11205
|
+
try {
|
|
11206
|
+
const body = await req.json();
|
|
11207
|
+
if (typeof body["fingerprint"] !== "string" || body["fingerprint"].trim() === "") {
|
|
11208
|
+
return json2({ error: "Missing 'fingerprint'" }, 400);
|
|
11209
|
+
}
|
|
11210
|
+
if (typeof body["title"] !== "string" || body["title"].trim() === "") {
|
|
11211
|
+
return json2({ error: "Missing 'title'" }, 400);
|
|
11212
|
+
}
|
|
11213
|
+
const metadata = body["metadata"] && typeof body["metadata"] === "object" && !Array.isArray(body["metadata"]) ? { ...body["metadata"] } : {};
|
|
11214
|
+
for (const key of ["expectation_id", "expectation_fingerprint", "evidence_paths", "origin_loop_id", "origin_run_id", "expected", "observed", "acceptance"]) {
|
|
11215
|
+
if (body[key] !== undefined)
|
|
11216
|
+
metadata[key] = body[key];
|
|
11217
|
+
}
|
|
11218
|
+
const result = upsertTaskByFingerprint({
|
|
11219
|
+
fingerprint: body["fingerprint"],
|
|
11220
|
+
title: body["title"],
|
|
11221
|
+
description: typeof body["description"] === "string" ? body["description"] : undefined,
|
|
11222
|
+
status: body["status"],
|
|
11223
|
+
priority: body["priority"],
|
|
11224
|
+
project_id: typeof body["project_id"] === "string" ? body["project_id"] : undefined,
|
|
11225
|
+
task_list_id: typeof body["task_list_id"] === "string" ? body["task_list_id"] : undefined,
|
|
11226
|
+
assigned_to: typeof body["assigned_to"] === "string" ? body["assigned_to"] : undefined,
|
|
11227
|
+
working_dir: typeof body["working_dir"] === "string" ? body["working_dir"] : undefined,
|
|
11228
|
+
tags: Array.isArray(body["tags"]) ? body["tags"].filter((tag) => typeof tag === "string") : undefined,
|
|
11229
|
+
metadata
|
|
11230
|
+
});
|
|
11231
|
+
ctx.broadcastEvent({ type: "task", task_id: result.task.id, action: result.created ? "created" : "updated", agent_id: result.task.agent_id, project_id: result.task.project_id });
|
|
11232
|
+
return json2({ created: result.created, task: taskToSummary2(result.task) }, result.created ? 201 : 200);
|
|
11233
|
+
} catch (e) {
|
|
11234
|
+
return json2({ error: e instanceof Error ? e.message : "Failed to upsert task" }, 500);
|
|
11235
|
+
}
|
|
11236
|
+
}
|
|
10828
11237
|
function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
|
|
10829
11238
|
const format = url.searchParams.get("format") || "json";
|
|
10830
11239
|
const status = url.searchParams.get("status") || undefined;
|
|
@@ -32569,6 +32978,7 @@ var init_token_utils = __esm(() => {
|
|
|
32569
32978
|
"add_task_run_event",
|
|
32570
32979
|
"add_task_run_file",
|
|
32571
32980
|
"acknowledge_handoff",
|
|
32981
|
+
"begin_task_run_transaction",
|
|
32572
32982
|
"build_local_report",
|
|
32573
32983
|
"cancel_agent_run_dispatch",
|
|
32574
32984
|
"finish_task_run",
|
|
@@ -32616,6 +33026,7 @@ var init_token_utils = __esm(() => {
|
|
|
32616
33026
|
"list_local_snapshots",
|
|
32617
33027
|
"list_retrospectives",
|
|
32618
33028
|
"list_risks",
|
|
33029
|
+
"list_task_findings",
|
|
32619
33030
|
"list_task_runs",
|
|
32620
33031
|
"list_verification_providers",
|
|
32621
33032
|
"merge_duplicate_task",
|
|
@@ -32624,6 +33035,7 @@ var init_token_utils = __esm(() => {
|
|
|
32624
33035
|
"remove_review_routing_rule",
|
|
32625
33036
|
"restore_local_backup",
|
|
32626
33037
|
"retry_agent_run_dispatch",
|
|
33038
|
+
"resolve_missing_task_findings",
|
|
32627
33039
|
"resolve_mentions",
|
|
32628
33040
|
"run_next_agent_dispatch",
|
|
32629
33041
|
"search_knowledge_records",
|
|
@@ -32666,9 +33078,17 @@ var init_token_utils = __esm(() => {
|
|
|
32666
33078
|
"unlock_file",
|
|
32667
33079
|
"unwatch_task",
|
|
32668
33080
|
"update_comment",
|
|
33081
|
+
"upsert_task_finding",
|
|
32669
33082
|
"update_risk",
|
|
32670
33083
|
"watch_task"
|
|
32671
33084
|
],
|
|
33085
|
+
loops: [
|
|
33086
|
+
"begin_task_run_transaction",
|
|
33087
|
+
"finish_task_run",
|
|
33088
|
+
"list_task_findings",
|
|
33089
|
+
"resolve_missing_task_findings",
|
|
33090
|
+
"upsert_task_finding"
|
|
33091
|
+
],
|
|
32672
33092
|
agents: [
|
|
32673
33093
|
"auto_assign_task",
|
|
32674
33094
|
"delete_agent",
|
|
@@ -32750,7 +33170,7 @@ var init_token_utils = __esm(() => {
|
|
|
32750
33170
|
maintenance: ["extract_todos", "get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task", "watch_source_todos"]
|
|
32751
33171
|
};
|
|
32752
33172
|
MCP_PROFILE_GROUPS = {
|
|
32753
|
-
minimal: ["core"],
|
|
33173
|
+
minimal: ["core", "loops"],
|
|
32754
33174
|
core: ["core"],
|
|
32755
33175
|
standard: ["core", "tasks", "projects", "resources", "agents", "metadata"],
|
|
32756
33176
|
agent: ["core", "tasks", "projects", "resources"],
|
|
@@ -32830,6 +33250,61 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
32830
33250
|
}
|
|
32831
33251
|
});
|
|
32832
33252
|
}
|
|
33253
|
+
if (shouldRegisterTool("upsert_task")) {
|
|
33254
|
+
server.tool("upsert_task", "Create or update a task by stable metadata fingerprint. Metadata is shallow-merged on updates.", {
|
|
33255
|
+
fingerprint: exports_external.string().describe("Stable dedupe fingerprint stored as metadata.fingerprint"),
|
|
33256
|
+
title: exports_external.string().describe("Task title"),
|
|
33257
|
+
description: exports_external.string().optional().describe("Task description"),
|
|
33258
|
+
status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
|
|
33259
|
+
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
|
|
33260
|
+
project_id: exports_external.string().optional().describe("Project ID"),
|
|
33261
|
+
task_list_id: exports_external.string().optional().describe("Task list ID"),
|
|
33262
|
+
assigned_to: exports_external.string().optional().describe("Agent ID or name to assign to"),
|
|
33263
|
+
tags: exports_external.array(exports_external.string()).optional().describe("Tags for the task"),
|
|
33264
|
+
working_dir: exports_external.string().optional().describe("Working directory associated with the task"),
|
|
33265
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Metadata object to shallow-merge"),
|
|
33266
|
+
expectation_id: exports_external.string().optional(),
|
|
33267
|
+
expectation_fingerprint: exports_external.string().optional(),
|
|
33268
|
+
evidence_paths: exports_external.array(exports_external.string()).optional(),
|
|
33269
|
+
origin_loop_id: exports_external.string().optional(),
|
|
33270
|
+
origin_run_id: exports_external.string().optional(),
|
|
33271
|
+
expected: exports_external.unknown().optional(),
|
|
33272
|
+
observed: exports_external.unknown().optional(),
|
|
33273
|
+
acceptance: exports_external.unknown().optional()
|
|
33274
|
+
}, async (params) => {
|
|
33275
|
+
try {
|
|
33276
|
+
const { assigned_to, project_id, task_list_id, metadata, expectation_id, expectation_fingerprint, evidence_paths, origin_loop_id, origin_run_id, expected, observed, acceptance, ...rest } = params;
|
|
33277
|
+
const mergedMetadata = { ...metadata ?? {} };
|
|
33278
|
+
if (expectation_id !== undefined)
|
|
33279
|
+
mergedMetadata["expectation_id"] = expectation_id;
|
|
33280
|
+
if (expectation_fingerprint !== undefined)
|
|
33281
|
+
mergedMetadata["expectation_fingerprint"] = expectation_fingerprint;
|
|
33282
|
+
if (evidence_paths !== undefined)
|
|
33283
|
+
mergedMetadata["evidence_paths"] = evidence_paths;
|
|
33284
|
+
if (origin_loop_id !== undefined)
|
|
33285
|
+
mergedMetadata["origin_loop_id"] = origin_loop_id;
|
|
33286
|
+
if (origin_run_id !== undefined)
|
|
33287
|
+
mergedMetadata["origin_run_id"] = origin_run_id;
|
|
33288
|
+
if (expected !== undefined)
|
|
33289
|
+
mergedMetadata["expected"] = expected;
|
|
33290
|
+
if (observed !== undefined)
|
|
33291
|
+
mergedMetadata["observed"] = observed;
|
|
33292
|
+
if (acceptance !== undefined)
|
|
33293
|
+
mergedMetadata["acceptance"] = acceptance;
|
|
33294
|
+
const resolved = { ...rest, metadata: mergedMetadata };
|
|
33295
|
+
if (assigned_to)
|
|
33296
|
+
resolved.assigned_to = resolveAssignee(assigned_to);
|
|
33297
|
+
if (project_id)
|
|
33298
|
+
resolved.project_id = resolveId(project_id, "projects");
|
|
33299
|
+
if (task_list_id)
|
|
33300
|
+
resolved.task_list_id = resolveId(task_list_id, "task_lists");
|
|
33301
|
+
const result = upsertTaskByFingerprint(resolved);
|
|
33302
|
+
return { content: [{ type: "text", text: compactJson({ created: result.created, task: JSON.parse(mutationTaskResponse(result.task)) }) }] };
|
|
33303
|
+
} catch (e) {
|
|
33304
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
33305
|
+
}
|
|
33306
|
+
});
|
|
33307
|
+
}
|
|
32833
33308
|
if (shouldRegisterTool("list_tasks")) {
|
|
32834
33309
|
server.tool("list_tasks", "List tasks with optional filters. Pass empty arrays for multi-value filters (e.g. status=[] shows all).", {
|
|
32835
33310
|
status: exports_external.union([exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]), exports_external.array(exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]))]).optional().describe("Filter by status"),
|
|
@@ -32841,7 +33316,8 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
32841
33316
|
created_after: exports_external.string().optional().describe("ISO date \u2014 tasks created after this date"),
|
|
32842
33317
|
created_before: exports_external.string().optional().describe("ISO date \u2014 tasks created before this date"),
|
|
32843
33318
|
limit: exports_external.number().optional().describe("Max results (default: 50, max 500)"),
|
|
32844
|
-
offset: exports_external.number().optional().describe("Pagination offset")
|
|
33319
|
+
offset: exports_external.number().optional().describe("Pagination offset"),
|
|
33320
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Exact top-level metadata filters")
|
|
32845
33321
|
}, async (params) => {
|
|
32846
33322
|
try {
|
|
32847
33323
|
const resolved = { ...params };
|
|
@@ -36382,7 +36858,7 @@ function countMovedStandaloneVerifications(db, duplicateId) {
|
|
|
36382
36858
|
const rows = db.query("SELECT command FROM task_verifications WHERE task_id = ?").all(duplicateId);
|
|
36383
36859
|
return rows.filter((row) => !generatedCommands.has(row.command)).length;
|
|
36384
36860
|
}
|
|
36385
|
-
function
|
|
36861
|
+
function mergeTaskMetadata2(primary, duplicate, input, mergedAt) {
|
|
36386
36862
|
const mergedDuplicates = Array.isArray(primary.metadata["merged_duplicates"]) ? [...primary.metadata["merged_duplicates"]] : [];
|
|
36387
36863
|
mergedDuplicates.push({
|
|
36388
36864
|
id: duplicate.id,
|
|
@@ -36443,7 +36919,7 @@ function mergeDuplicateTask(input, db) {
|
|
|
36443
36919
|
updateTask(primary.id, {
|
|
36444
36920
|
version: primary.version,
|
|
36445
36921
|
tags: mergedTags,
|
|
36446
|
-
metadata:
|
|
36922
|
+
metadata: mergeTaskMetadata2(primary, duplicate, input, mergedAt),
|
|
36447
36923
|
description: mergeTaskDescription(primary, duplicate) ?? undefined
|
|
36448
36924
|
}, d);
|
|
36449
36925
|
moved.comments = updateRows(d, "task_comments", "task_id", duplicate.id, primary.id);
|
|
@@ -66620,6 +67096,356 @@ var init_task_meta_tools = __esm(() => {
|
|
|
66620
67096
|
init_zod();
|
|
66621
67097
|
});
|
|
66622
67098
|
|
|
67099
|
+
// src/db/findings.ts
|
|
67100
|
+
function parseObject2(value) {
|
|
67101
|
+
if (!value)
|
|
67102
|
+
return {};
|
|
67103
|
+
try {
|
|
67104
|
+
const parsed = JSON.parse(value);
|
|
67105
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
67106
|
+
} catch {
|
|
67107
|
+
return {};
|
|
67108
|
+
}
|
|
67109
|
+
}
|
|
67110
|
+
function normalizeKey(value) {
|
|
67111
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
67112
|
+
}
|
|
67113
|
+
function normalizeFingerprint(value) {
|
|
67114
|
+
const normalized = normalizeKey(value);
|
|
67115
|
+
if (!normalized)
|
|
67116
|
+
throw new Error("finding fingerprint is required");
|
|
67117
|
+
return normalized.slice(0, 240);
|
|
67118
|
+
}
|
|
67119
|
+
function normalizeSeverity(value) {
|
|
67120
|
+
const normalized = normalizeKey(value || "medium");
|
|
67121
|
+
if (SEVERITIES.has(normalized))
|
|
67122
|
+
return normalized;
|
|
67123
|
+
if (/^(p0|blocker|urgent|highest)$/.test(normalized))
|
|
67124
|
+
return "critical";
|
|
67125
|
+
if (/^(p1|major)$/.test(normalized))
|
|
67126
|
+
return "high";
|
|
67127
|
+
if (/^(p3|minor|info)$/.test(normalized))
|
|
67128
|
+
return "low";
|
|
67129
|
+
return "medium";
|
|
67130
|
+
}
|
|
67131
|
+
function normalizeStatus(value) {
|
|
67132
|
+
const normalized = normalizeKey(value || "open");
|
|
67133
|
+
if (STATUSES.has(normalized))
|
|
67134
|
+
return normalized;
|
|
67135
|
+
if (normalized === "closed" || normalized === "fixed")
|
|
67136
|
+
return "resolved";
|
|
67137
|
+
return "open";
|
|
67138
|
+
}
|
|
67139
|
+
function normalizeResolutionStatus(value) {
|
|
67140
|
+
const status = normalizeStatus(value || "resolved");
|
|
67141
|
+
if (status === "open")
|
|
67142
|
+
throw new Error("resolve-missing status must be resolved or ignored");
|
|
67143
|
+
return status;
|
|
67144
|
+
}
|
|
67145
|
+
function redactOptional(value, max = 2000) {
|
|
67146
|
+
if (!value)
|
|
67147
|
+
return null;
|
|
67148
|
+
const redacted = redactEvidenceText(value).trim();
|
|
67149
|
+
if (!redacted)
|
|
67150
|
+
return null;
|
|
67151
|
+
return redacted.length > max ? `${redacted.slice(0, max - 3)}...` : redacted;
|
|
67152
|
+
}
|
|
67153
|
+
function rowToFinding(row) {
|
|
67154
|
+
return {
|
|
67155
|
+
schema_version: TASK_FINDING_SCHEMA_VERSION,
|
|
67156
|
+
...row,
|
|
67157
|
+
severity: normalizeSeverity(row.severity),
|
|
67158
|
+
status: normalizeStatus(row.status),
|
|
67159
|
+
metadata: parseObject2(row.metadata)
|
|
67160
|
+
};
|
|
67161
|
+
}
|
|
67162
|
+
function compactFinding(finding2) {
|
|
67163
|
+
return {
|
|
67164
|
+
schema_version: TASK_FINDING_SCHEMA_VERSION,
|
|
67165
|
+
id: finding2.id,
|
|
67166
|
+
task_id: finding2.task_id,
|
|
67167
|
+
run_id: finding2.run_id,
|
|
67168
|
+
fingerprint: finding2.fingerprint,
|
|
67169
|
+
title: finding2.title,
|
|
67170
|
+
severity: finding2.severity,
|
|
67171
|
+
status: finding2.status,
|
|
67172
|
+
source: finding2.source,
|
|
67173
|
+
summary: finding2.summary,
|
|
67174
|
+
artifact_path: finding2.artifact_path,
|
|
67175
|
+
first_seen_at: finding2.first_seen_at,
|
|
67176
|
+
last_seen_at: finding2.last_seen_at,
|
|
67177
|
+
resolved_at: finding2.resolved_at,
|
|
67178
|
+
metadata_keys: Object.keys(finding2.metadata).sort()
|
|
67179
|
+
};
|
|
67180
|
+
}
|
|
67181
|
+
function previewFinding(existing, next, timestamp3) {
|
|
67182
|
+
return {
|
|
67183
|
+
...existing,
|
|
67184
|
+
run_id: next.run_id,
|
|
67185
|
+
title: next.title,
|
|
67186
|
+
severity: next.severity,
|
|
67187
|
+
status: next.status,
|
|
67188
|
+
source: next.source,
|
|
67189
|
+
summary: next.summary,
|
|
67190
|
+
artifact_path: next.artifact_path,
|
|
67191
|
+
metadata: next.metadata,
|
|
67192
|
+
last_seen_at: timestamp3,
|
|
67193
|
+
resolved_at: next.status === "open" ? null : existing.resolved_at || timestamp3,
|
|
67194
|
+
updated_at: timestamp3
|
|
67195
|
+
};
|
|
67196
|
+
}
|
|
67197
|
+
function upsertAction(existing, next) {
|
|
67198
|
+
if (sameFinding(existing, next))
|
|
67199
|
+
return "matched";
|
|
67200
|
+
return existing.status !== "open" && next.status === "open" ? "reopened" : "updated";
|
|
67201
|
+
}
|
|
67202
|
+
function resolveRunForTask(runId, taskId, db) {
|
|
67203
|
+
if (!runId)
|
|
67204
|
+
return null;
|
|
67205
|
+
const resolved = resolveTaskRunId(runId, db);
|
|
67206
|
+
const run = getTaskRun(resolved, db);
|
|
67207
|
+
if (!run)
|
|
67208
|
+
throw new Error(`Run not found: ${runId}`);
|
|
67209
|
+
if (run.task_id !== taskId)
|
|
67210
|
+
throw new Error(`Run ${resolved} belongs to task ${run.task_id}, not ${taskId}`);
|
|
67211
|
+
return resolved;
|
|
67212
|
+
}
|
|
67213
|
+
function getFindingByFingerprint(taskId, fingerprint2, db) {
|
|
67214
|
+
const row = db.query("SELECT * FROM task_findings WHERE task_id = ? AND fingerprint = ?").get(taskId, fingerprint2);
|
|
67215
|
+
return row ? rowToFinding(row) : null;
|
|
67216
|
+
}
|
|
67217
|
+
function assertTask(taskId, db) {
|
|
67218
|
+
if (!getTask(taskId, db))
|
|
67219
|
+
throw new TaskNotFoundError(taskId);
|
|
67220
|
+
}
|
|
67221
|
+
function nextFinding(input, db) {
|
|
67222
|
+
const fingerprint2 = normalizeFingerprint(input.fingerprint);
|
|
67223
|
+
const title = redactOptional(input.title, 300);
|
|
67224
|
+
if (!title)
|
|
67225
|
+
throw new Error("finding title is required");
|
|
67226
|
+
return {
|
|
67227
|
+
fingerprint: fingerprint2,
|
|
67228
|
+
run_id: resolveRunForTask(input.run_id, input.task_id, db),
|
|
67229
|
+
title,
|
|
67230
|
+
severity: normalizeSeverity(input.severity),
|
|
67231
|
+
status: normalizeStatus(input.status),
|
|
67232
|
+
source: redactOptional(input.source, 120),
|
|
67233
|
+
summary: redactOptional(input.summary, 2000),
|
|
67234
|
+
artifact_path: redactOptional(input.artifact_path, 1000),
|
|
67235
|
+
metadata: redactValue(input.metadata || {})
|
|
67236
|
+
};
|
|
67237
|
+
}
|
|
67238
|
+
function sameFinding(left, right) {
|
|
67239
|
+
return left.run_id === right.run_id && left.title === right.title && left.severity === right.severity && left.status === right.status && left.source === right.source && left.summary === right.summary && left.artifact_path === right.artifact_path && JSON.stringify(left.metadata) === JSON.stringify(right.metadata);
|
|
67240
|
+
}
|
|
67241
|
+
function upsertTaskFinding(input, db) {
|
|
67242
|
+
const d = db || getDatabase();
|
|
67243
|
+
assertTask(input.task_id, d);
|
|
67244
|
+
const timestamp3 = input.observed_at || now();
|
|
67245
|
+
const warnings = [];
|
|
67246
|
+
const next = nextFinding(input, d);
|
|
67247
|
+
const existing = getFindingByFingerprint(input.task_id, next.fingerprint, d);
|
|
67248
|
+
const dryRun = !input.apply;
|
|
67249
|
+
if (dryRun) {
|
|
67250
|
+
const action2 = existing ? upsertAction(existing, next) : "preview";
|
|
67251
|
+
return {
|
|
67252
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
67253
|
+
local_only: true,
|
|
67254
|
+
dry_run: true,
|
|
67255
|
+
processed_at: timestamp3,
|
|
67256
|
+
action: action2,
|
|
67257
|
+
fingerprint: next.fingerprint,
|
|
67258
|
+
finding: existing ? compactFinding(previewFinding(existing, next, timestamp3)) : null,
|
|
67259
|
+
warnings
|
|
67260
|
+
};
|
|
67261
|
+
}
|
|
67262
|
+
if (!existing) {
|
|
67263
|
+
const id = uuid();
|
|
67264
|
+
d.run(`INSERT INTO task_findings (
|
|
67265
|
+
id, task_id, run_id, fingerprint, title, severity, status, source, summary, artifact_path,
|
|
67266
|
+
metadata, first_seen_at, last_seen_at, resolved_at, created_at, updated_at
|
|
67267
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
67268
|
+
id,
|
|
67269
|
+
input.task_id,
|
|
67270
|
+
next.run_id,
|
|
67271
|
+
next.fingerprint,
|
|
67272
|
+
next.title,
|
|
67273
|
+
next.severity,
|
|
67274
|
+
next.status,
|
|
67275
|
+
next.source,
|
|
67276
|
+
next.summary,
|
|
67277
|
+
next.artifact_path,
|
|
67278
|
+
JSON.stringify(next.metadata),
|
|
67279
|
+
timestamp3,
|
|
67280
|
+
timestamp3,
|
|
67281
|
+
next.status === "open" ? null : timestamp3,
|
|
67282
|
+
timestamp3,
|
|
67283
|
+
timestamp3
|
|
67284
|
+
]);
|
|
67285
|
+
return {
|
|
67286
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
67287
|
+
local_only: true,
|
|
67288
|
+
dry_run: false,
|
|
67289
|
+
processed_at: timestamp3,
|
|
67290
|
+
action: "created",
|
|
67291
|
+
fingerprint: next.fingerprint,
|
|
67292
|
+
finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
|
|
67293
|
+
warnings
|
|
67294
|
+
};
|
|
67295
|
+
}
|
|
67296
|
+
if (sameFinding(existing, next)) {
|
|
67297
|
+
return {
|
|
67298
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
67299
|
+
local_only: true,
|
|
67300
|
+
dry_run: false,
|
|
67301
|
+
processed_at: timestamp3,
|
|
67302
|
+
action: "matched",
|
|
67303
|
+
fingerprint: next.fingerprint,
|
|
67304
|
+
finding: compactFinding(existing),
|
|
67305
|
+
warnings
|
|
67306
|
+
};
|
|
67307
|
+
}
|
|
67308
|
+
const action = upsertAction(existing, next);
|
|
67309
|
+
d.run(`UPDATE task_findings SET
|
|
67310
|
+
run_id = ?, title = ?, severity = ?, status = ?, source = ?, summary = ?, artifact_path = ?,
|
|
67311
|
+
metadata = ?, last_seen_at = ?, resolved_at = ?, updated_at = ?
|
|
67312
|
+
WHERE id = ?`, [
|
|
67313
|
+
next.run_id,
|
|
67314
|
+
next.title,
|
|
67315
|
+
next.severity,
|
|
67316
|
+
next.status,
|
|
67317
|
+
next.source,
|
|
67318
|
+
next.summary,
|
|
67319
|
+
next.artifact_path,
|
|
67320
|
+
JSON.stringify(next.metadata),
|
|
67321
|
+
timestamp3,
|
|
67322
|
+
next.status === "open" ? null : existing.resolved_at || timestamp3,
|
|
67323
|
+
timestamp3,
|
|
67324
|
+
existing.id
|
|
67325
|
+
]);
|
|
67326
|
+
return {
|
|
67327
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
67328
|
+
local_only: true,
|
|
67329
|
+
dry_run: false,
|
|
67330
|
+
processed_at: timestamp3,
|
|
67331
|
+
action,
|
|
67332
|
+
fingerprint: next.fingerprint,
|
|
67333
|
+
finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
|
|
67334
|
+
warnings
|
|
67335
|
+
};
|
|
67336
|
+
}
|
|
67337
|
+
function listTaskFindings(filter = {}, db) {
|
|
67338
|
+
const d = db || getDatabase();
|
|
67339
|
+
const conditions = ["1=1"];
|
|
67340
|
+
const params = [];
|
|
67341
|
+
if (filter.task_id) {
|
|
67342
|
+
conditions.push("task_id = ?");
|
|
67343
|
+
params.push(filter.task_id);
|
|
67344
|
+
}
|
|
67345
|
+
if (filter.run_id) {
|
|
67346
|
+
conditions.push("run_id = ?");
|
|
67347
|
+
params.push(resolveTaskRunId(filter.run_id, d));
|
|
67348
|
+
}
|
|
67349
|
+
if (filter.status) {
|
|
67350
|
+
conditions.push("status = ?");
|
|
67351
|
+
params.push(normalizeStatus(filter.status));
|
|
67352
|
+
}
|
|
67353
|
+
if (filter.source) {
|
|
67354
|
+
conditions.push("source = ?");
|
|
67355
|
+
params.push(redactOptional(filter.source, 120));
|
|
67356
|
+
}
|
|
67357
|
+
const limit = Math.min(Math.max(Math.floor(filter.limit ?? 50), 1), 500);
|
|
67358
|
+
const rows = d.query(`SELECT * FROM task_findings WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC, created_at DESC LIMIT ?`).all(...[...params, limit]);
|
|
67359
|
+
return rows.map(rowToFinding);
|
|
67360
|
+
}
|
|
67361
|
+
function listCompactTaskFindings(filter = {}, db) {
|
|
67362
|
+
return listTaskFindings(filter, db).map(compactFinding);
|
|
67363
|
+
}
|
|
67364
|
+
function resolveMissingTaskFindings(input, db) {
|
|
67365
|
+
const d = db || getDatabase();
|
|
67366
|
+
assertTask(input.task_id, d);
|
|
67367
|
+
const timestamp3 = input.resolved_at || now();
|
|
67368
|
+
const status = normalizeResolutionStatus(input.status);
|
|
67369
|
+
const runId = resolveRunForTask(input.run_id, input.task_id, d);
|
|
67370
|
+
const present = new Set(input.fingerprints.map(normalizeFingerprint));
|
|
67371
|
+
const warnings = [];
|
|
67372
|
+
const conditions = ["task_id = ?", "status = 'open'"];
|
|
67373
|
+
const params = [input.task_id];
|
|
67374
|
+
if (input.source) {
|
|
67375
|
+
conditions.push("source = ?");
|
|
67376
|
+
params.push(redactOptional(input.source, 120));
|
|
67377
|
+
}
|
|
67378
|
+
const candidates = d.query(`SELECT * FROM task_findings WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC, created_at DESC`).all(...params).map(rowToFinding).filter((finding2) => !present.has(finding2.fingerprint));
|
|
67379
|
+
const limit = Math.min(Math.max(Math.floor(input.limit ?? 50), 1), 200);
|
|
67380
|
+
const display = candidates.slice(0, limit);
|
|
67381
|
+
const omittedCount = Math.max(0, candidates.length - display.length);
|
|
67382
|
+
if (!input.apply) {
|
|
67383
|
+
return {
|
|
67384
|
+
schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
67385
|
+
local_only: true,
|
|
67386
|
+
dry_run: true,
|
|
67387
|
+
processed_at: timestamp3,
|
|
67388
|
+
action: candidates.length > 0 ? "preview" : "noop",
|
|
67389
|
+
task_id: input.task_id,
|
|
67390
|
+
source: input.source ? redactOptional(input.source, 120) : null,
|
|
67391
|
+
run_id: runId,
|
|
67392
|
+
present_fingerprint_count: present.size,
|
|
67393
|
+
candidate_count: candidates.length,
|
|
67394
|
+
changed_count: 0,
|
|
67395
|
+
omitted_count: omittedCount,
|
|
67396
|
+
findings: display.map(compactFinding),
|
|
67397
|
+
warnings
|
|
67398
|
+
};
|
|
67399
|
+
}
|
|
67400
|
+
const metadataPatch = redactValue({
|
|
67401
|
+
resolved_by: {
|
|
67402
|
+
agent_id: input.agent_id ?? null,
|
|
67403
|
+
run_id: runId,
|
|
67404
|
+
reason: input.reason ? redactEvidenceText(input.reason) : "missing from latest loop finding set"
|
|
67405
|
+
}
|
|
67406
|
+
});
|
|
67407
|
+
const tx = d.transaction(() => {
|
|
67408
|
+
for (const finding2 of candidates) {
|
|
67409
|
+
d.run("UPDATE task_findings SET status = ?, resolved_at = ?, metadata = ?, updated_at = ? WHERE id = ? AND status = 'open'", [
|
|
67410
|
+
status,
|
|
67411
|
+
timestamp3,
|
|
67412
|
+
JSON.stringify({ ...finding2.metadata, ...metadataPatch }),
|
|
67413
|
+
timestamp3,
|
|
67414
|
+
finding2.id
|
|
67415
|
+
]);
|
|
67416
|
+
}
|
|
67417
|
+
});
|
|
67418
|
+
tx();
|
|
67419
|
+
const updated = candidates.map((finding2) => d.query("SELECT * FROM task_findings WHERE id = ?").get(finding2.id)).filter((row) => Boolean(row)).map(rowToFinding);
|
|
67420
|
+
const visibleUpdated = updated.slice(0, limit);
|
|
67421
|
+
return {
|
|
67422
|
+
schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
67423
|
+
local_only: true,
|
|
67424
|
+
dry_run: false,
|
|
67425
|
+
processed_at: timestamp3,
|
|
67426
|
+
action: updated.length > 0 ? status : "noop",
|
|
67427
|
+
task_id: input.task_id,
|
|
67428
|
+
source: input.source ? redactOptional(input.source, 120) : null,
|
|
67429
|
+
run_id: runId,
|
|
67430
|
+
present_fingerprint_count: present.size,
|
|
67431
|
+
candidate_count: candidates.length,
|
|
67432
|
+
changed_count: updated.length,
|
|
67433
|
+
omitted_count: omittedCount,
|
|
67434
|
+
findings: visibleUpdated.map(compactFinding),
|
|
67435
|
+
warnings
|
|
67436
|
+
};
|
|
67437
|
+
}
|
|
67438
|
+
var TASK_FINDING_SCHEMA_VERSION = "todos.task_finding.v1", TASK_FINDING_UPSERT_SCHEMA_VERSION = "todos.task_finding_upsert.v1", TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION = "todos.task_finding_resolve_missing.v1", SEVERITIES, STATUSES;
|
|
67439
|
+
var init_findings = __esm(() => {
|
|
67440
|
+
init_redaction();
|
|
67441
|
+
init_types();
|
|
67442
|
+
init_database();
|
|
67443
|
+
init_tasks();
|
|
67444
|
+
init_task_runs();
|
|
67445
|
+
SEVERITIES = new Set(["low", "medium", "high", "critical"]);
|
|
67446
|
+
STATUSES = new Set(["open", "resolved", "ignored"]);
|
|
67447
|
+
});
|
|
67448
|
+
|
|
66623
67449
|
// src/lib/agent-run-dispatcher.ts
|
|
66624
67450
|
function dispatcherFromRun(run) {
|
|
66625
67451
|
const value = run.metadata["agent_run_dispatcher"];
|
|
@@ -67280,7 +68106,7 @@ function parseArray2(value) {
|
|
|
67280
68106
|
return [];
|
|
67281
68107
|
}
|
|
67282
68108
|
}
|
|
67283
|
-
function
|
|
68109
|
+
function parseObject3(value) {
|
|
67284
68110
|
try {
|
|
67285
68111
|
const parsed = JSON.parse(value || "{}");
|
|
67286
68112
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
@@ -67321,7 +68147,7 @@ function rowToKnowledgeRecord(row) {
|
|
|
67321
68147
|
agent_id: row.agent_id,
|
|
67322
68148
|
snapshot_id: row.snapshot_id,
|
|
67323
68149
|
tags: parseArray2(row.tags),
|
|
67324
|
-
metadata: redactValue(
|
|
68150
|
+
metadata: redactValue(parseObject3(row.metadata)),
|
|
67325
68151
|
created_at: row.created_at,
|
|
67326
68152
|
updated_at: row.updated_at
|
|
67327
68153
|
};
|
|
@@ -67540,7 +68366,7 @@ function parseArray3(value) {
|
|
|
67540
68366
|
return [];
|
|
67541
68367
|
}
|
|
67542
68368
|
}
|
|
67543
|
-
function
|
|
68369
|
+
function parseObject4(value) {
|
|
67544
68370
|
try {
|
|
67545
68371
|
const parsed = JSON.parse(value || "{}");
|
|
67546
68372
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
@@ -67589,7 +68415,7 @@ function rowToRisk(row) {
|
|
|
67589
68415
|
plan_id: row.plan_id,
|
|
67590
68416
|
task_id: row.task_id,
|
|
67591
68417
|
tags: parseArray3(row.tags),
|
|
67592
|
-
metadata: redactValue(
|
|
68418
|
+
metadata: redactValue(parseObject4(row.metadata)),
|
|
67593
68419
|
created_at: row.created_at,
|
|
67594
68420
|
updated_at: row.updated_at,
|
|
67595
68421
|
closed_at: row.closed_at
|
|
@@ -73407,6 +74233,38 @@ ${lines.join(`
|
|
|
73407
74233
|
}
|
|
73408
74234
|
});
|
|
73409
74235
|
}
|
|
74236
|
+
if (shouldRegisterTool("begin_task_run_transaction")) {
|
|
74237
|
+
server.tool("begin_task_run_transaction", "Preview or apply an idempotent local loop run transaction keyed by a stable loop/run id.", {
|
|
74238
|
+
task_id: exports_external.string().describe("Task ID"),
|
|
74239
|
+
key: exports_external.string().optional().describe("Stable idempotency key"),
|
|
74240
|
+
loop_id: exports_external.string().optional().describe("Loop identifier; used as key fallback"),
|
|
74241
|
+
loop_run_id: exports_external.string().optional().describe("Loop run identifier; used as key fallback"),
|
|
74242
|
+
agent_id: exports_external.string().optional().describe("Agent starting the run"),
|
|
74243
|
+
title: exports_external.string().optional().describe("Run title"),
|
|
74244
|
+
summary: exports_external.string().optional().describe("Run summary"),
|
|
74245
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata"),
|
|
74246
|
+
claim: exports_external.boolean().optional().describe("Claim/start the task before recording the run"),
|
|
74247
|
+
apply: exports_external.boolean().optional().describe("Apply the transaction; false or omitted is dry-run")
|
|
74248
|
+
}, async ({ task_id, key, loop_id, loop_run_id, agent_id, title, summary, metadata, claim, apply }) => {
|
|
74249
|
+
try {
|
|
74250
|
+
const result = beginTaskRunTransaction({
|
|
74251
|
+
task_id: resolveId(task_id),
|
|
74252
|
+
key,
|
|
74253
|
+
loop_id,
|
|
74254
|
+
loop_run_id,
|
|
74255
|
+
agent_id,
|
|
74256
|
+
title,
|
|
74257
|
+
summary,
|
|
74258
|
+
metadata,
|
|
74259
|
+
claim,
|
|
74260
|
+
apply
|
|
74261
|
+
});
|
|
74262
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
74263
|
+
} catch (e) {
|
|
74264
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
74265
|
+
}
|
|
74266
|
+
});
|
|
74267
|
+
}
|
|
73410
74268
|
if (shouldRegisterTool("list_task_runs")) {
|
|
73411
74269
|
server.tool("list_task_runs", "List local run ledger entries, optionally scoped to a task.", { task_id: exports_external.string().optional().describe("Optional task ID") }, async ({ task_id }) => {
|
|
73412
74270
|
try {
|
|
@@ -73504,15 +74362,117 @@ ${lines.join(`
|
|
|
73504
74362
|
});
|
|
73505
74363
|
}
|
|
73506
74364
|
if (shouldRegisterTool("finish_task_run")) {
|
|
73507
|
-
server.tool("finish_task_run", "Finish a local run ledger entry as completed, failed, or cancelled.", {
|
|
73508
|
-
run_id: exports_external.string().describe("Run ID or prefix"),
|
|
73509
|
-
|
|
74365
|
+
server.tool("finish_task_run", "Finish a local run ledger entry as completed, failed, or cancelled. Supports idempotent lookup by key.", {
|
|
74366
|
+
run_id: exports_external.string().optional().describe("Run ID or prefix"),
|
|
74367
|
+
key: exports_external.string().optional().describe("Idempotency key when run_id is omitted"),
|
|
74368
|
+
task_id: exports_external.string().optional().describe("Task scope for key lookup"),
|
|
74369
|
+
status: exports_external.enum(["completed", "failed", "cancelled"]).optional().describe("Final run status"),
|
|
73510
74370
|
summary: exports_external.string().optional().describe("Final summary"),
|
|
73511
|
-
agent_id: exports_external.string().optional().describe("Agent finishing the run")
|
|
73512
|
-
|
|
74371
|
+
agent_id: exports_external.string().optional().describe("Agent finishing the run"),
|
|
74372
|
+
apply: exports_external.boolean().optional().describe("Apply the transaction; false previews without mutating")
|
|
74373
|
+
}, async ({ run_id, key, task_id, status, summary, agent_id, apply }) => {
|
|
73513
74374
|
try {
|
|
73514
|
-
|
|
73515
|
-
|
|
74375
|
+
if (run_id && !key && apply === undefined) {
|
|
74376
|
+
const result2 = finishTaskRunTransaction({ run_id, status: status || "completed", summary, agent_id, apply: true });
|
|
74377
|
+
const run = result2.run ? getTaskRunLedger(result2.run.id).run : null;
|
|
74378
|
+
return { content: [{ type: "text", text: JSON.stringify(run, null, 2) }] };
|
|
74379
|
+
}
|
|
74380
|
+
const result = finishTaskRunTransaction({
|
|
74381
|
+
run_id,
|
|
74382
|
+
key,
|
|
74383
|
+
task_id: task_id ? resolveId(task_id) : undefined,
|
|
74384
|
+
status: status || "completed",
|
|
74385
|
+
summary,
|
|
74386
|
+
agent_id,
|
|
74387
|
+
apply: apply !== false
|
|
74388
|
+
});
|
|
74389
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
74390
|
+
} catch (e) {
|
|
74391
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
74392
|
+
}
|
|
74393
|
+
});
|
|
74394
|
+
}
|
|
74395
|
+
if (shouldRegisterTool("upsert_task_finding")) {
|
|
74396
|
+
server.tool("upsert_task_finding", "Preview or apply an idempotent local finding upsert scoped by task and fingerprint.", {
|
|
74397
|
+
task_id: exports_external.string().describe("Task ID"),
|
|
74398
|
+
fingerprint: exports_external.string().describe("Stable finding fingerprint"),
|
|
74399
|
+
title: exports_external.string().describe("Finding title"),
|
|
74400
|
+
severity: exports_external.enum(["low", "medium", "high", "critical"]).optional().describe("Finding severity"),
|
|
74401
|
+
status: exports_external.enum(["open", "resolved", "ignored"]).optional().describe("Finding status"),
|
|
74402
|
+
source: exports_external.string().optional().describe("Loop/tool source name"),
|
|
74403
|
+
summary: exports_external.string().optional().describe("Bounded finding summary"),
|
|
74404
|
+
artifact_path: exports_external.string().optional().describe("Local artifact path/reference; content is not read"),
|
|
74405
|
+
run_id: exports_external.string().optional().describe("Run ledger ID or prefix"),
|
|
74406
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata"),
|
|
74407
|
+
apply: exports_external.boolean().optional().describe("Apply the upsert; false or omitted is dry-run")
|
|
74408
|
+
}, async ({ task_id, fingerprint: fingerprint3, title, severity, status, source: source3, summary, artifact_path, run_id, metadata, apply }) => {
|
|
74409
|
+
try {
|
|
74410
|
+
const result = upsertTaskFinding({
|
|
74411
|
+
task_id: resolveId(task_id),
|
|
74412
|
+
fingerprint: fingerprint3,
|
|
74413
|
+
title,
|
|
74414
|
+
severity,
|
|
74415
|
+
status,
|
|
74416
|
+
source: source3,
|
|
74417
|
+
summary,
|
|
74418
|
+
artifact_path,
|
|
74419
|
+
run_id,
|
|
74420
|
+
metadata,
|
|
74421
|
+
apply
|
|
74422
|
+
});
|
|
74423
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
74424
|
+
} catch (e) {
|
|
74425
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
74426
|
+
}
|
|
74427
|
+
});
|
|
74428
|
+
}
|
|
74429
|
+
if (shouldRegisterTool("list_task_findings")) {
|
|
74430
|
+
server.tool("list_task_findings", "List compact local findings with bounded output.", {
|
|
74431
|
+
task_id: exports_external.string().optional().describe("Filter by task"),
|
|
74432
|
+
run_id: exports_external.string().optional().describe("Filter by run"),
|
|
74433
|
+
status: exports_external.enum(["open", "resolved", "ignored"]).optional().describe("Filter by status"),
|
|
74434
|
+
source: exports_external.string().optional().describe("Filter by source"),
|
|
74435
|
+
limit: exports_external.number().optional().describe("Maximum findings to return")
|
|
74436
|
+
}, async ({ task_id, run_id, status, source: source3, limit }) => {
|
|
74437
|
+
try {
|
|
74438
|
+
const findings = listCompactTaskFindings({
|
|
74439
|
+
task_id: task_id ? resolveId(task_id) : undefined,
|
|
74440
|
+
run_id,
|
|
74441
|
+
status,
|
|
74442
|
+
source: source3,
|
|
74443
|
+
limit
|
|
74444
|
+
});
|
|
74445
|
+
return { content: [{ type: "text", text: JSON.stringify(findings) }] };
|
|
74446
|
+
} catch (e) {
|
|
74447
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
74448
|
+
}
|
|
74449
|
+
});
|
|
74450
|
+
}
|
|
74451
|
+
if (shouldRegisterTool("resolve_missing_task_findings")) {
|
|
74452
|
+
server.tool("resolve_missing_task_findings", "Resolve open findings absent from the latest loop finding set.", {
|
|
74453
|
+
task_id: exports_external.string().describe("Task ID"),
|
|
74454
|
+
fingerprints: exports_external.array(exports_external.string()).optional().describe("Fingerprints still present in the latest run"),
|
|
74455
|
+
source: exports_external.string().optional().describe("Only resolve findings from this source"),
|
|
74456
|
+
run_id: exports_external.string().optional().describe("Run ledger ID or prefix for audit metadata"),
|
|
74457
|
+
status: exports_external.enum(["resolved", "ignored"]).optional().describe("Resolution status"),
|
|
74458
|
+
agent_id: exports_external.string().optional().describe("Agent resolving findings"),
|
|
74459
|
+
reason: exports_external.string().optional().describe("Resolution reason"),
|
|
74460
|
+
limit: exports_external.number().optional().describe("Maximum findings returned"),
|
|
74461
|
+
apply: exports_external.boolean().optional().describe("Apply resolution; false or omitted is dry-run")
|
|
74462
|
+
}, async ({ task_id, fingerprints, source: source3, run_id, status, agent_id, reason, limit, apply }) => {
|
|
74463
|
+
try {
|
|
74464
|
+
const result = resolveMissingTaskFindings({
|
|
74465
|
+
task_id: resolveId(task_id),
|
|
74466
|
+
fingerprints: fingerprints || [],
|
|
74467
|
+
source: source3,
|
|
74468
|
+
run_id,
|
|
74469
|
+
status,
|
|
74470
|
+
agent_id,
|
|
74471
|
+
reason,
|
|
74472
|
+
limit,
|
|
74473
|
+
apply
|
|
74474
|
+
});
|
|
74475
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
73516
74476
|
} catch (e) {
|
|
73517
74477
|
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
73518
74478
|
}
|
|
@@ -73848,6 +74808,7 @@ var init_task_resources = __esm(() => {
|
|
|
73848
74808
|
init_agents();
|
|
73849
74809
|
init_task_commits();
|
|
73850
74810
|
init_task_runs();
|
|
74811
|
+
init_findings();
|
|
73851
74812
|
init_agent_run_dispatcher();
|
|
73852
74813
|
init_verification_providers();
|
|
73853
74814
|
init_release_notes();
|
|
@@ -78423,8 +79384,10 @@ function taskToSummary(task2, fields) {
|
|
|
78423
79384
|
task_list_id: task2.task_list_id,
|
|
78424
79385
|
agent_id: task2.agent_id,
|
|
78425
79386
|
assigned_to: task2.assigned_to,
|
|
79387
|
+
working_dir: task2.working_dir,
|
|
78426
79388
|
locked_by: task2.locked_by,
|
|
78427
79389
|
tags: task2.tags,
|
|
79390
|
+
metadata: task2.metadata,
|
|
78428
79391
|
version: task2.version,
|
|
78429
79392
|
created_at: task2.created_at,
|
|
78430
79393
|
updated_at: task2.updated_at,
|
|
@@ -78566,6 +79529,9 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
78566
79529
|
if (path === "/api/tasks" && method === "POST") {
|
|
78567
79530
|
return handleCreateTask(req, ctx, jsonWithCors, taskToSummary);
|
|
78568
79531
|
}
|
|
79532
|
+
if (path === "/api/tasks/upsert" && method === "POST") {
|
|
79533
|
+
return handleUpsertTask(req, ctx, jsonWithCors, taskToSummary);
|
|
79534
|
+
}
|
|
78569
79535
|
if (path === "/api/tasks/export" && method === "GET") {
|
|
78570
79536
|
return handleTasksExport(req, url, ctx, jsonWithCors, taskToSummary);
|
|
78571
79537
|
}
|
|
@@ -78761,7 +79727,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
78761
79727
|
} catch {}
|
|
78762
79728
|
}
|
|
78763
79729
|
}
|
|
78764
|
-
var MIME_TYPES, SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX
|
|
79730
|
+
var MIME_TYPES, SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX;
|
|
78765
79731
|
var init_serve = __esm(() => {
|
|
78766
79732
|
init_database();
|
|
78767
79733
|
init_api_keys();
|
|
@@ -78786,6 +79752,7 @@ var init_serve = __esm(() => {
|
|
|
78786
79752
|
"Permissions-Policy": "camera=, microphone=, geolocation="
|
|
78787
79753
|
};
|
|
78788
79754
|
rateLimitMap = new Map;
|
|
79755
|
+
RATE_LIMIT_MAX = Number.parseInt(process.env["TODOS_RATE_LIMIT_MAX"] || "120", 10);
|
|
78789
79756
|
});
|
|
78790
79757
|
|
|
78791
79758
|
// src/server/index.ts
|