@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/mcp/index.js
CHANGED
|
@@ -1140,6 +1140,49 @@ var init_migrations = __esm(() => {
|
|
|
1140
1140
|
CREATE INDEX IF NOT EXISTS idx_local_retrospectives_plan ON local_retrospectives(plan_id);
|
|
1141
1141
|
CREATE INDEX IF NOT EXISTS idx_local_retrospectives_agent ON local_retrospectives(agent_id);
|
|
1142
1142
|
INSERT OR IGNORE INTO _migrations (id) VALUES (61);
|
|
1143
|
+
`,
|
|
1144
|
+
`
|
|
1145
|
+
CREATE TABLE IF NOT EXISTS task_run_transactions (
|
|
1146
|
+
id TEXT PRIMARY KEY,
|
|
1147
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1148
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1149
|
+
key TEXT NOT NULL,
|
|
1150
|
+
loop_id TEXT,
|
|
1151
|
+
loop_run_id TEXT,
|
|
1152
|
+
metadata TEXT DEFAULT '{}',
|
|
1153
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1154
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1155
|
+
UNIQUE(task_id, key)
|
|
1156
|
+
);
|
|
1157
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_transactions_task_key ON task_run_transactions(task_id, key);
|
|
1158
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_transactions_key ON task_run_transactions(key);
|
|
1159
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_transactions_run ON task_run_transactions(run_id);
|
|
1160
|
+
|
|
1161
|
+
CREATE TABLE IF NOT EXISTS task_findings (
|
|
1162
|
+
id TEXT PRIMARY KEY,
|
|
1163
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1164
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1165
|
+
fingerprint TEXT NOT NULL,
|
|
1166
|
+
title TEXT NOT NULL,
|
|
1167
|
+
severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low', 'medium', 'high', 'critical')),
|
|
1168
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'resolved', 'ignored')),
|
|
1169
|
+
source TEXT,
|
|
1170
|
+
summary TEXT,
|
|
1171
|
+
artifact_path TEXT,
|
|
1172
|
+
metadata TEXT DEFAULT '{}',
|
|
1173
|
+
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1174
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1175
|
+
resolved_at TEXT,
|
|
1176
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1177
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1178
|
+
UNIQUE(task_id, fingerprint)
|
|
1179
|
+
);
|
|
1180
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_task ON task_findings(task_id);
|
|
1181
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_run ON task_findings(run_id);
|
|
1182
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_status ON task_findings(status);
|
|
1183
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source);
|
|
1184
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint);
|
|
1185
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (62);
|
|
1143
1186
|
`
|
|
1144
1187
|
];
|
|
1145
1188
|
});
|
|
@@ -1577,6 +1620,47 @@ function ensureSchema(db) {
|
|
|
1577
1620
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_run ON task_run_artifacts(run_id)");
|
|
1578
1621
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_task ON task_run_artifacts(task_id)");
|
|
1579
1622
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_path ON task_run_artifacts(path)");
|
|
1623
|
+
ensureTable("task_run_transactions", `
|
|
1624
|
+
CREATE TABLE task_run_transactions (
|
|
1625
|
+
id TEXT PRIMARY KEY,
|
|
1626
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1627
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1628
|
+
key TEXT NOT NULL,
|
|
1629
|
+
loop_id TEXT,
|
|
1630
|
+
loop_run_id TEXT,
|
|
1631
|
+
metadata TEXT DEFAULT '{}',
|
|
1632
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1633
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1634
|
+
UNIQUE(task_id, key)
|
|
1635
|
+
)`);
|
|
1636
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_task_key ON task_run_transactions(task_id, key)");
|
|
1637
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_key ON task_run_transactions(key)");
|
|
1638
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_run ON task_run_transactions(run_id)");
|
|
1639
|
+
ensureTable("task_findings", `
|
|
1640
|
+
CREATE TABLE task_findings (
|
|
1641
|
+
id TEXT PRIMARY KEY,
|
|
1642
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1643
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1644
|
+
fingerprint TEXT NOT NULL,
|
|
1645
|
+
title TEXT NOT NULL,
|
|
1646
|
+
severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low', 'medium', 'high', 'critical')),
|
|
1647
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'resolved', 'ignored')),
|
|
1648
|
+
source TEXT,
|
|
1649
|
+
summary TEXT,
|
|
1650
|
+
artifact_path TEXT,
|
|
1651
|
+
metadata TEXT DEFAULT '{}',
|
|
1652
|
+
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1653
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1654
|
+
resolved_at TEXT,
|
|
1655
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1656
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1657
|
+
UNIQUE(task_id, fingerprint)
|
|
1658
|
+
)`);
|
|
1659
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_task ON task_findings(task_id)");
|
|
1660
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_run ON task_findings(run_id)");
|
|
1661
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_status ON task_findings(status)");
|
|
1662
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source)");
|
|
1663
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint)");
|
|
1580
1664
|
ensureTable("inbox_items", `
|
|
1581
1665
|
CREATE TABLE inbox_items (
|
|
1582
1666
|
id TEXT PRIMARY KEY,
|
|
@@ -8633,6 +8717,7 @@ var init_event_hooks = __esm(() => {
|
|
|
8633
8717
|
"task.blocked",
|
|
8634
8718
|
"task.started",
|
|
8635
8719
|
"task.completed",
|
|
8720
|
+
"task.updated",
|
|
8636
8721
|
"task.due",
|
|
8637
8722
|
"task.due_soon",
|
|
8638
8723
|
"task.failed",
|
|
@@ -9929,6 +10014,17 @@ function replaceTaskTags(taskId, tags, db) {
|
|
|
9929
10014
|
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
9930
10015
|
insertTaskTags(taskId, tags, db);
|
|
9931
10016
|
}
|
|
10017
|
+
function addMetadataConditions(metadata, conditions, params) {
|
|
10018
|
+
if (!metadata)
|
|
10019
|
+
return;
|
|
10020
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
10021
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
|
|
10022
|
+
throw new Error(`Invalid metadata filter key: ${key}`);
|
|
10023
|
+
}
|
|
10024
|
+
conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
|
|
10025
|
+
params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
|
|
10026
|
+
}
|
|
10027
|
+
}
|
|
9932
10028
|
function createTask(input, db) {
|
|
9933
10029
|
const d = db || getDatabase();
|
|
9934
10030
|
const timestamp = now();
|
|
@@ -10111,6 +10207,7 @@ function listTasks(filter = {}, db) {
|
|
|
10111
10207
|
params.push(filter.task_type);
|
|
10112
10208
|
}
|
|
10113
10209
|
}
|
|
10210
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
10114
10211
|
const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
|
|
10115
10212
|
if (filter.cursor) {
|
|
10116
10213
|
try {
|
|
@@ -10135,6 +10232,54 @@ function listTasks(filter = {}, db) {
|
|
|
10135
10232
|
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
|
|
10136
10233
|
return rows.map(rowToTask);
|
|
10137
10234
|
}
|
|
10235
|
+
function getTaskByFingerprint(fingerprint, db) {
|
|
10236
|
+
const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
|
|
10237
|
+
return tasks[0] ?? null;
|
|
10238
|
+
}
|
|
10239
|
+
function mergeTaskMetadata(current, next, fingerprint) {
|
|
10240
|
+
return {
|
|
10241
|
+
...current,
|
|
10242
|
+
...next ?? {},
|
|
10243
|
+
fingerprint
|
|
10244
|
+
};
|
|
10245
|
+
}
|
|
10246
|
+
function upsertTaskByFingerprint(input, db) {
|
|
10247
|
+
const d = db || getDatabase();
|
|
10248
|
+
const fingerprint = input.fingerprint.trim();
|
|
10249
|
+
if (!fingerprint)
|
|
10250
|
+
throw new Error("fingerprint is required");
|
|
10251
|
+
const existing = getTaskByFingerprint(fingerprint, d);
|
|
10252
|
+
const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
|
|
10253
|
+
if (!existing) {
|
|
10254
|
+
const task2 = createTask({ ...input, metadata }, d);
|
|
10255
|
+
return { task: task2, created: true };
|
|
10256
|
+
}
|
|
10257
|
+
const task = updateTask(existing.id, {
|
|
10258
|
+
version: existing.version,
|
|
10259
|
+
title: input.title,
|
|
10260
|
+
description: input.description,
|
|
10261
|
+
status: input.status,
|
|
10262
|
+
priority: input.priority,
|
|
10263
|
+
project_id: input.project_id,
|
|
10264
|
+
assigned_to: input.assigned_to,
|
|
10265
|
+
working_dir: input.working_dir,
|
|
10266
|
+
plan_id: input.plan_id,
|
|
10267
|
+
task_list_id: input.task_list_id,
|
|
10268
|
+
tags: input.tags,
|
|
10269
|
+
metadata,
|
|
10270
|
+
due_at: input.due_at,
|
|
10271
|
+
estimated_minutes: input.estimated_minutes,
|
|
10272
|
+
sla_minutes: input.sla_minutes,
|
|
10273
|
+
confidence: input.confidence,
|
|
10274
|
+
retry_count: input.retry_count,
|
|
10275
|
+
max_retries: input.max_retries,
|
|
10276
|
+
retry_after: input.retry_after,
|
|
10277
|
+
requires_approval: input.requires_approval,
|
|
10278
|
+
recurrence_rule: input.recurrence_rule,
|
|
10279
|
+
task_type: input.task_type
|
|
10280
|
+
}, d);
|
|
10281
|
+
return { task, created: false };
|
|
10282
|
+
}
|
|
10138
10283
|
function countTasks(filter = {}, db) {
|
|
10139
10284
|
const d = db || getDatabase();
|
|
10140
10285
|
const conditions = [];
|
|
@@ -10198,6 +10343,7 @@ function countTasks(filter = {}, db) {
|
|
|
10198
10343
|
conditions.push("task_list_id = ?");
|
|
10199
10344
|
params.push(filter.task_list_id);
|
|
10200
10345
|
}
|
|
10346
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
10201
10347
|
if (!filter.include_archived) {
|
|
10202
10348
|
conditions.push("archived_at IS NULL");
|
|
10203
10349
|
}
|
|
@@ -10248,6 +10394,10 @@ function updateTask(id, input, db) {
|
|
|
10248
10394
|
sets.push("assigned_to = ?");
|
|
10249
10395
|
params.push(input.assigned_to);
|
|
10250
10396
|
}
|
|
10397
|
+
if (input.working_dir !== undefined) {
|
|
10398
|
+
sets.push("working_dir = ?");
|
|
10399
|
+
params.push(input.working_dir);
|
|
10400
|
+
}
|
|
10251
10401
|
if (input.tags !== undefined) {
|
|
10252
10402
|
sets.push("tags = ?");
|
|
10253
10403
|
params.push(JSON.stringify(input.tags));
|
|
@@ -10336,6 +10486,8 @@ function updateTask(id, input, db) {
|
|
|
10336
10486
|
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
10337
10487
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
10338
10488
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
10489
|
+
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
10490
|
+
logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
10339
10491
|
if (input.approved_by !== undefined)
|
|
10340
10492
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
10341
10493
|
const updatedTask = {
|
|
@@ -10371,6 +10523,10 @@ function updateTask(id, input, db) {
|
|
|
10371
10523
|
if (input.approved_by !== undefined) {
|
|
10372
10524
|
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
10373
10525
|
}
|
|
10526
|
+
const updatePayload = taskEventData(updatedTask);
|
|
10527
|
+
dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
|
|
10528
|
+
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
|
|
10529
|
+
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
|
|
10374
10530
|
return updatedTask;
|
|
10375
10531
|
}
|
|
10376
10532
|
function deleteTask(id, db) {
|
|
@@ -13388,6 +13544,72 @@ function rowToArtifact(row) {
|
|
|
13388
13544
|
function getRunRow(runId, db) {
|
|
13389
13545
|
return db.query("SELECT * FROM task_runs WHERE id = ?").get(runId);
|
|
13390
13546
|
}
|
|
13547
|
+
function normalizeTransactionKey(input) {
|
|
13548
|
+
const key = (input.key || input.loop_run_id || input.loop_id || "").trim();
|
|
13549
|
+
if (!key)
|
|
13550
|
+
throw new Error("idempotent run transactions require --key, --loop-run-id, or --loop-id");
|
|
13551
|
+
return key.toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 240);
|
|
13552
|
+
}
|
|
13553
|
+
function loopTransactionMetadata(record) {
|
|
13554
|
+
const value = record.metadata["loop_transaction"];
|
|
13555
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
13556
|
+
}
|
|
13557
|
+
function runKey(record) {
|
|
13558
|
+
const tx = loopTransactionMetadata(record);
|
|
13559
|
+
const key = tx["idempotency_key"] ?? record.metadata["idempotency_key"];
|
|
13560
|
+
return typeof key === "string" ? key : null;
|
|
13561
|
+
}
|
|
13562
|
+
function loopId(record) {
|
|
13563
|
+
const tx = loopTransactionMetadata(record);
|
|
13564
|
+
const value = tx["loop_id"] ?? record.metadata["loop_id"];
|
|
13565
|
+
return typeof value === "string" ? value : null;
|
|
13566
|
+
}
|
|
13567
|
+
function loopRunId(record) {
|
|
13568
|
+
const tx = loopTransactionMetadata(record);
|
|
13569
|
+
const value = tx["loop_run_id"] ?? record.metadata["loop_run_id"];
|
|
13570
|
+
return typeof value === "string" ? value : null;
|
|
13571
|
+
}
|
|
13572
|
+
function getTaskRunTransactionByKey(key, taskId, db) {
|
|
13573
|
+
if (taskId) {
|
|
13574
|
+
return db.query("SELECT * FROM task_run_transactions WHERE task_id = ? AND key = ?").get(taskId, key);
|
|
13575
|
+
}
|
|
13576
|
+
const rows = db.query("SELECT * FROM task_run_transactions WHERE key = ? ORDER BY created_at DESC LIMIT 2").all(key);
|
|
13577
|
+
if (rows.length > 1)
|
|
13578
|
+
throw new Error(`Run transaction key is ambiguous across tasks: ${key}. Pass task_id.`);
|
|
13579
|
+
return rows[0] ?? null;
|
|
13580
|
+
}
|
|
13581
|
+
function summarizeTaskRun(run) {
|
|
13582
|
+
return {
|
|
13583
|
+
id: run.id,
|
|
13584
|
+
task_id: run.task_id,
|
|
13585
|
+
agent_id: run.agent_id,
|
|
13586
|
+
title: run.title,
|
|
13587
|
+
status: run.status,
|
|
13588
|
+
summary: run.summary,
|
|
13589
|
+
idempotency_key: runKey(run),
|
|
13590
|
+
loop_id: loopId(run),
|
|
13591
|
+
loop_run_id: loopRunId(run),
|
|
13592
|
+
metadata_keys: Object.keys(run.metadata).sort(),
|
|
13593
|
+
started_at: run.started_at,
|
|
13594
|
+
completed_at: run.completed_at,
|
|
13595
|
+
updated_at: run.updated_at
|
|
13596
|
+
};
|
|
13597
|
+
}
|
|
13598
|
+
function findTaskRunByTransactionKey(key, taskId, db) {
|
|
13599
|
+
const d = db || getDatabase();
|
|
13600
|
+
const normalized = normalizeTransactionKey({ key });
|
|
13601
|
+
const transaction = getTaskRunTransactionByKey(normalized, taskId, d);
|
|
13602
|
+
if (transaction?.run_id)
|
|
13603
|
+
return getTaskRun(transaction.run_id, d);
|
|
13604
|
+
return null;
|
|
13605
|
+
}
|
|
13606
|
+
function loopRunCommands(run, key) {
|
|
13607
|
+
return [
|
|
13608
|
+
run ? `todos runs show ${run.id.slice(0, 8)}` : "todos runs list",
|
|
13609
|
+
run ? `todos findings list --task ${run.task_id.slice(0, 8)} --json` : "todos findings list --json",
|
|
13610
|
+
`todos runs begin <task-id> --key ${key} --apply --json`
|
|
13611
|
+
];
|
|
13612
|
+
}
|
|
13391
13613
|
function resolveTaskRunId(idOrPrefix, db) {
|
|
13392
13614
|
const d = db || getDatabase();
|
|
13393
13615
|
const rows = d.query("SELECT id FROM task_runs WHERE id = ? OR id LIKE ? ORDER BY created_at DESC LIMIT 2").all(idOrPrefix, `${idOrPrefix}%`);
|
|
@@ -13406,7 +13628,7 @@ function startTaskRun(input, db) {
|
|
|
13406
13628
|
const d = db || getDatabase();
|
|
13407
13629
|
if (!getTask(input.task_id, d))
|
|
13408
13630
|
throw new TaskNotFoundError(input.task_id);
|
|
13409
|
-
const id = uuid();
|
|
13631
|
+
const id = input.id ?? uuid();
|
|
13410
13632
|
const timestamp = input.started_at || now();
|
|
13411
13633
|
if (input.claim && input.agent_id) {
|
|
13412
13634
|
startTask(input.task_id, input.agent_id, d);
|
|
@@ -13444,6 +13666,97 @@ function startTaskRun(input, db) {
|
|
|
13444
13666
|
emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
|
|
13445
13667
|
return run;
|
|
13446
13668
|
}
|
|
13669
|
+
function beginTaskRunTransaction(input, db) {
|
|
13670
|
+
const d = db || getDatabase();
|
|
13671
|
+
if (!getTask(input.task_id, d))
|
|
13672
|
+
throw new TaskNotFoundError(input.task_id);
|
|
13673
|
+
const timestamp = input.started_at || now();
|
|
13674
|
+
const key = normalizeTransactionKey(input);
|
|
13675
|
+
const existing = findTaskRunByTransactionKey(key, input.task_id, d);
|
|
13676
|
+
const dryRun = !input.apply;
|
|
13677
|
+
if (existing) {
|
|
13678
|
+
return {
|
|
13679
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
13680
|
+
local_only: true,
|
|
13681
|
+
dry_run: dryRun,
|
|
13682
|
+
processed_at: timestamp,
|
|
13683
|
+
action: "matched",
|
|
13684
|
+
key,
|
|
13685
|
+
run: summarizeTaskRun(existing),
|
|
13686
|
+
warnings: existing.status === "running" ? [] : [`matched ${existing.status} run`],
|
|
13687
|
+
commands: loopRunCommands(existing, key)
|
|
13688
|
+
};
|
|
13689
|
+
}
|
|
13690
|
+
if (dryRun) {
|
|
13691
|
+
return {
|
|
13692
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
13693
|
+
local_only: true,
|
|
13694
|
+
dry_run: true,
|
|
13695
|
+
processed_at: timestamp,
|
|
13696
|
+
action: "preview",
|
|
13697
|
+
key,
|
|
13698
|
+
run: null,
|
|
13699
|
+
warnings: [],
|
|
13700
|
+
commands: loopRunCommands(null, key)
|
|
13701
|
+
};
|
|
13702
|
+
}
|
|
13703
|
+
const metadata = redactValue({
|
|
13704
|
+
...input.metadata || {},
|
|
13705
|
+
loop_transaction: {
|
|
13706
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
13707
|
+
idempotency_key: key,
|
|
13708
|
+
loop_id: input.loop_id ?? null,
|
|
13709
|
+
loop_run_id: input.loop_run_id ?? null,
|
|
13710
|
+
first_seen_at: timestamp
|
|
13711
|
+
},
|
|
13712
|
+
idempotency_key: key
|
|
13713
|
+
});
|
|
13714
|
+
const created = d.transaction(() => {
|
|
13715
|
+
d.run(`INSERT OR IGNORE INTO task_run_transactions (
|
|
13716
|
+
id, task_id, run_id, key, loop_id, loop_run_id, metadata, created_at, updated_at
|
|
13717
|
+
) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?)`, [
|
|
13718
|
+
uuid(),
|
|
13719
|
+
input.task_id,
|
|
13720
|
+
key,
|
|
13721
|
+
input.loop_id ?? null,
|
|
13722
|
+
input.loop_run_id ?? null,
|
|
13723
|
+
JSON.stringify(metadata),
|
|
13724
|
+
timestamp,
|
|
13725
|
+
timestamp
|
|
13726
|
+
]);
|
|
13727
|
+
const transaction = getTaskRunTransactionByKey(key, input.task_id, d);
|
|
13728
|
+
if (!transaction)
|
|
13729
|
+
throw new Error(`Could not create run transaction for key: ${key}`);
|
|
13730
|
+
if (transaction.run_id) {
|
|
13731
|
+
const existingRun = getTaskRun(transaction.run_id, d);
|
|
13732
|
+
if (existingRun)
|
|
13733
|
+
return { run: existingRun, action: "matched" };
|
|
13734
|
+
}
|
|
13735
|
+
const run = startTaskRun({
|
|
13736
|
+
id: uuid(),
|
|
13737
|
+
task_id: input.task_id,
|
|
13738
|
+
agent_id: input.agent_id,
|
|
13739
|
+
title: input.title,
|
|
13740
|
+
summary: input.summary,
|
|
13741
|
+
metadata,
|
|
13742
|
+
claim: input.claim,
|
|
13743
|
+
started_at: timestamp
|
|
13744
|
+
}, d);
|
|
13745
|
+
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]);
|
|
13746
|
+
return { run, action: "created" };
|
|
13747
|
+
})();
|
|
13748
|
+
return {
|
|
13749
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
13750
|
+
local_only: true,
|
|
13751
|
+
dry_run: false,
|
|
13752
|
+
processed_at: timestamp,
|
|
13753
|
+
action: created.action,
|
|
13754
|
+
key,
|
|
13755
|
+
run: summarizeTaskRun(created.run),
|
|
13756
|
+
warnings: [],
|
|
13757
|
+
commands: loopRunCommands(created.run, key)
|
|
13758
|
+
};
|
|
13759
|
+
}
|
|
13447
13760
|
function addTaskRunEvent(input, db) {
|
|
13448
13761
|
const d = db || getDatabase();
|
|
13449
13762
|
const runId = resolveTaskRunId(input.run_id, d);
|
|
@@ -13623,6 +13936,66 @@ function finishTaskRun(input, db) {
|
|
|
13623
13936
|
});
|
|
13624
13937
|
return updated;
|
|
13625
13938
|
}
|
|
13939
|
+
function finishTaskRunTransaction(input, db) {
|
|
13940
|
+
const d = db || getDatabase();
|
|
13941
|
+
const timestamp = input.completed_at || now();
|
|
13942
|
+
const status = input.status || "completed";
|
|
13943
|
+
const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
|
|
13944
|
+
const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
|
|
13945
|
+
if (!run) {
|
|
13946
|
+
throw new Error(input.run_id ? `Run not found: ${input.run_id}` : "runs finish requires a run id or --key");
|
|
13947
|
+
}
|
|
13948
|
+
if (input.task_id && run.task_id !== input.task_id) {
|
|
13949
|
+
throw new Error(`Run ${run.id} belongs to task ${run.task_id}, not ${input.task_id}`);
|
|
13950
|
+
}
|
|
13951
|
+
const resolvedKey = key || runKey(run) || run.id;
|
|
13952
|
+
const dryRun = input.apply === false;
|
|
13953
|
+
if (run.status !== "running") {
|
|
13954
|
+
const conflict = run.status !== status;
|
|
13955
|
+
return {
|
|
13956
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
13957
|
+
local_only: true,
|
|
13958
|
+
dry_run: dryRun,
|
|
13959
|
+
processed_at: timestamp,
|
|
13960
|
+
action: conflict ? "conflict" : "matched",
|
|
13961
|
+
key: resolvedKey,
|
|
13962
|
+
run: summarizeTaskRun(run),
|
|
13963
|
+
warnings: conflict ? [`run is already ${run.status}; requested ${status}`] : [],
|
|
13964
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
13965
|
+
};
|
|
13966
|
+
}
|
|
13967
|
+
if (dryRun) {
|
|
13968
|
+
return {
|
|
13969
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
13970
|
+
local_only: true,
|
|
13971
|
+
dry_run: true,
|
|
13972
|
+
processed_at: timestamp,
|
|
13973
|
+
action: "preview",
|
|
13974
|
+
key: resolvedKey,
|
|
13975
|
+
run: summarizeTaskRun({ ...run, status, summary: input.summary ?? run.summary, completed_at: timestamp, updated_at: timestamp }),
|
|
13976
|
+
warnings: [],
|
|
13977
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
13978
|
+
};
|
|
13979
|
+
}
|
|
13980
|
+
const finished = finishTaskRun({
|
|
13981
|
+
run_id: run.id,
|
|
13982
|
+
status,
|
|
13983
|
+
summary: input.summary,
|
|
13984
|
+
agent_id: input.agent_id,
|
|
13985
|
+
completed_at: timestamp
|
|
13986
|
+
}, d);
|
|
13987
|
+
return {
|
|
13988
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
13989
|
+
local_only: true,
|
|
13990
|
+
dry_run: false,
|
|
13991
|
+
processed_at: timestamp,
|
|
13992
|
+
action: "finished",
|
|
13993
|
+
key: resolvedKey,
|
|
13994
|
+
run: summarizeTaskRun(finished),
|
|
13995
|
+
warnings: [],
|
|
13996
|
+
commands: loopRunCommands(finished, resolvedKey)
|
|
13997
|
+
};
|
|
13998
|
+
}
|
|
13626
13999
|
function listTaskRuns(taskId, db) {
|
|
13627
14000
|
const d = db || getDatabase();
|
|
13628
14001
|
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();
|
|
@@ -13640,6 +14013,7 @@ function getTaskRunLedger(runId, db) {
|
|
|
13640
14013
|
const files = d.query("SELECT * FROM task_files WHERE task_id = ? ORDER BY updated_at DESC, path").all(run.task_id);
|
|
13641
14014
|
return { run, events, commands, artifacts, files };
|
|
13642
14015
|
}
|
|
14016
|
+
var LOOP_RUN_TRANSACTION_SCHEMA_VERSION = "todos.loop_run_transaction.v1";
|
|
13643
14017
|
var init_task_runs = __esm(() => {
|
|
13644
14018
|
init_artifact_store();
|
|
13645
14019
|
init_event_hooks();
|
|
@@ -14028,6 +14402,7 @@ var init_calendar = __esm(() => {
|
|
|
14028
14402
|
var exports_tasks = {};
|
|
14029
14403
|
__export(exports_tasks, {
|
|
14030
14404
|
watchTask: () => watchTask,
|
|
14405
|
+
upsertTaskByFingerprint: () => upsertTaskByFingerprint,
|
|
14031
14406
|
updateTaskBoard: () => updateTaskBoard,
|
|
14032
14407
|
updateTask: () => updateTask,
|
|
14033
14408
|
unwatchTask: () => unwatchTask,
|
|
@@ -14071,6 +14446,7 @@ __export(exports_tasks, {
|
|
|
14071
14446
|
getTaskGraph: () => getTaskGraph,
|
|
14072
14447
|
getTaskDependents: () => getTaskDependents,
|
|
14073
14448
|
getTaskDependencies: () => getTaskDependencies,
|
|
14449
|
+
getTaskByFingerprint: () => getTaskByFingerprint,
|
|
14074
14450
|
getTaskBoard: () => getTaskBoard,
|
|
14075
14451
|
getTask: () => getTask,
|
|
14076
14452
|
getStatus: () => getStatus,
|
|
@@ -14917,6 +15293,7 @@ var init_token_utils = __esm(() => {
|
|
|
14917
15293
|
"add_task_run_event",
|
|
14918
15294
|
"add_task_run_file",
|
|
14919
15295
|
"acknowledge_handoff",
|
|
15296
|
+
"begin_task_run_transaction",
|
|
14920
15297
|
"build_local_report",
|
|
14921
15298
|
"cancel_agent_run_dispatch",
|
|
14922
15299
|
"finish_task_run",
|
|
@@ -14964,6 +15341,7 @@ var init_token_utils = __esm(() => {
|
|
|
14964
15341
|
"list_local_snapshots",
|
|
14965
15342
|
"list_retrospectives",
|
|
14966
15343
|
"list_risks",
|
|
15344
|
+
"list_task_findings",
|
|
14967
15345
|
"list_task_runs",
|
|
14968
15346
|
"list_verification_providers",
|
|
14969
15347
|
"merge_duplicate_task",
|
|
@@ -14972,6 +15350,7 @@ var init_token_utils = __esm(() => {
|
|
|
14972
15350
|
"remove_review_routing_rule",
|
|
14973
15351
|
"restore_local_backup",
|
|
14974
15352
|
"retry_agent_run_dispatch",
|
|
15353
|
+
"resolve_missing_task_findings",
|
|
14975
15354
|
"resolve_mentions",
|
|
14976
15355
|
"run_next_agent_dispatch",
|
|
14977
15356
|
"search_knowledge_records",
|
|
@@ -15014,9 +15393,17 @@ var init_token_utils = __esm(() => {
|
|
|
15014
15393
|
"unlock_file",
|
|
15015
15394
|
"unwatch_task",
|
|
15016
15395
|
"update_comment",
|
|
15396
|
+
"upsert_task_finding",
|
|
15017
15397
|
"update_risk",
|
|
15018
15398
|
"watch_task"
|
|
15019
15399
|
],
|
|
15400
|
+
loops: [
|
|
15401
|
+
"begin_task_run_transaction",
|
|
15402
|
+
"finish_task_run",
|
|
15403
|
+
"list_task_findings",
|
|
15404
|
+
"resolve_missing_task_findings",
|
|
15405
|
+
"upsert_task_finding"
|
|
15406
|
+
],
|
|
15020
15407
|
agents: [
|
|
15021
15408
|
"auto_assign_task",
|
|
15022
15409
|
"delete_agent",
|
|
@@ -15098,7 +15485,7 @@ var init_token_utils = __esm(() => {
|
|
|
15098
15485
|
maintenance: ["extract_todos", "get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task", "watch_source_todos"]
|
|
15099
15486
|
};
|
|
15100
15487
|
MCP_PROFILE_GROUPS = {
|
|
15101
|
-
minimal: ["core"],
|
|
15488
|
+
minimal: ["core", "loops"],
|
|
15102
15489
|
core: ["core"],
|
|
15103
15490
|
standard: ["core", "tasks", "projects", "resources", "agents", "metadata"],
|
|
15104
15491
|
agent: ["core", "tasks", "projects", "resources"],
|
|
@@ -15178,6 +15565,61 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
15178
15565
|
}
|
|
15179
15566
|
});
|
|
15180
15567
|
}
|
|
15568
|
+
if (shouldRegisterTool("upsert_task")) {
|
|
15569
|
+
server.tool("upsert_task", "Create or update a task by stable metadata fingerprint. Metadata is shallow-merged on updates.", {
|
|
15570
|
+
fingerprint: exports_external.string().describe("Stable dedupe fingerprint stored as metadata.fingerprint"),
|
|
15571
|
+
title: exports_external.string().describe("Task title"),
|
|
15572
|
+
description: exports_external.string().optional().describe("Task description"),
|
|
15573
|
+
status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
|
|
15574
|
+
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
|
|
15575
|
+
project_id: exports_external.string().optional().describe("Project ID"),
|
|
15576
|
+
task_list_id: exports_external.string().optional().describe("Task list ID"),
|
|
15577
|
+
assigned_to: exports_external.string().optional().describe("Agent ID or name to assign to"),
|
|
15578
|
+
tags: exports_external.array(exports_external.string()).optional().describe("Tags for the task"),
|
|
15579
|
+
working_dir: exports_external.string().optional().describe("Working directory associated with the task"),
|
|
15580
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Metadata object to shallow-merge"),
|
|
15581
|
+
expectation_id: exports_external.string().optional(),
|
|
15582
|
+
expectation_fingerprint: exports_external.string().optional(),
|
|
15583
|
+
evidence_paths: exports_external.array(exports_external.string()).optional(),
|
|
15584
|
+
origin_loop_id: exports_external.string().optional(),
|
|
15585
|
+
origin_run_id: exports_external.string().optional(),
|
|
15586
|
+
expected: exports_external.unknown().optional(),
|
|
15587
|
+
observed: exports_external.unknown().optional(),
|
|
15588
|
+
acceptance: exports_external.unknown().optional()
|
|
15589
|
+
}, async (params) => {
|
|
15590
|
+
try {
|
|
15591
|
+
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;
|
|
15592
|
+
const mergedMetadata = { ...metadata ?? {} };
|
|
15593
|
+
if (expectation_id !== undefined)
|
|
15594
|
+
mergedMetadata["expectation_id"] = expectation_id;
|
|
15595
|
+
if (expectation_fingerprint !== undefined)
|
|
15596
|
+
mergedMetadata["expectation_fingerprint"] = expectation_fingerprint;
|
|
15597
|
+
if (evidence_paths !== undefined)
|
|
15598
|
+
mergedMetadata["evidence_paths"] = evidence_paths;
|
|
15599
|
+
if (origin_loop_id !== undefined)
|
|
15600
|
+
mergedMetadata["origin_loop_id"] = origin_loop_id;
|
|
15601
|
+
if (origin_run_id !== undefined)
|
|
15602
|
+
mergedMetadata["origin_run_id"] = origin_run_id;
|
|
15603
|
+
if (expected !== undefined)
|
|
15604
|
+
mergedMetadata["expected"] = expected;
|
|
15605
|
+
if (observed !== undefined)
|
|
15606
|
+
mergedMetadata["observed"] = observed;
|
|
15607
|
+
if (acceptance !== undefined)
|
|
15608
|
+
mergedMetadata["acceptance"] = acceptance;
|
|
15609
|
+
const resolved = { ...rest, metadata: mergedMetadata };
|
|
15610
|
+
if (assigned_to)
|
|
15611
|
+
resolved.assigned_to = resolveAssignee(assigned_to);
|
|
15612
|
+
if (project_id)
|
|
15613
|
+
resolved.project_id = resolveId(project_id, "projects");
|
|
15614
|
+
if (task_list_id)
|
|
15615
|
+
resolved.task_list_id = resolveId(task_list_id, "task_lists");
|
|
15616
|
+
const result = upsertTaskByFingerprint(resolved);
|
|
15617
|
+
return { content: [{ type: "text", text: compactJson({ created: result.created, task: JSON.parse(mutationTaskResponse(result.task)) }) }] };
|
|
15618
|
+
} catch (e) {
|
|
15619
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
15620
|
+
}
|
|
15621
|
+
});
|
|
15622
|
+
}
|
|
15181
15623
|
if (shouldRegisterTool("list_tasks")) {
|
|
15182
15624
|
server.tool("list_tasks", "List tasks with optional filters. Pass empty arrays for multi-value filters (e.g. status=[] shows all).", {
|
|
15183
15625
|
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"),
|
|
@@ -15189,7 +15631,8 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
15189
15631
|
created_after: exports_external.string().optional().describe("ISO date \u2014 tasks created after this date"),
|
|
15190
15632
|
created_before: exports_external.string().optional().describe("ISO date \u2014 tasks created before this date"),
|
|
15191
15633
|
limit: exports_external.number().optional().describe("Max results (default: 50, max 500)"),
|
|
15192
|
-
offset: exports_external.number().optional().describe("Pagination offset")
|
|
15634
|
+
offset: exports_external.number().optional().describe("Pagination offset"),
|
|
15635
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Exact top-level metadata filters")
|
|
15193
15636
|
}, async (params) => {
|
|
15194
15637
|
try {
|
|
15195
15638
|
const resolved = { ...params };
|
|
@@ -18730,7 +19173,7 @@ function countMovedStandaloneVerifications(db, duplicateId) {
|
|
|
18730
19173
|
const rows = db.query("SELECT command FROM task_verifications WHERE task_id = ?").all(duplicateId);
|
|
18731
19174
|
return rows.filter((row) => !generatedCommands.has(row.command)).length;
|
|
18732
19175
|
}
|
|
18733
|
-
function
|
|
19176
|
+
function mergeTaskMetadata2(primary, duplicate, input, mergedAt) {
|
|
18734
19177
|
const mergedDuplicates = Array.isArray(primary.metadata["merged_duplicates"]) ? [...primary.metadata["merged_duplicates"]] : [];
|
|
18735
19178
|
mergedDuplicates.push({
|
|
18736
19179
|
id: duplicate.id,
|
|
@@ -18791,7 +19234,7 @@ function mergeDuplicateTask(input, db) {
|
|
|
18791
19234
|
updateTask(primary.id, {
|
|
18792
19235
|
version: primary.version,
|
|
18793
19236
|
tags: mergedTags,
|
|
18794
|
-
metadata:
|
|
19237
|
+
metadata: mergeTaskMetadata2(primary, duplicate, input, mergedAt),
|
|
18795
19238
|
description: mergeTaskDescription(primary, duplicate) ?? undefined
|
|
18796
19239
|
}, d);
|
|
18797
19240
|
moved.comments = updateRows(d, "task_comments", "task_id", duplicate.id, primary.id);
|
|
@@ -25588,6 +26031,356 @@ var init_task_meta_tools = __esm(() => {
|
|
|
25588
26031
|
init_zod();
|
|
25589
26032
|
});
|
|
25590
26033
|
|
|
26034
|
+
// src/db/findings.ts
|
|
26035
|
+
function parseObject2(value) {
|
|
26036
|
+
if (!value)
|
|
26037
|
+
return {};
|
|
26038
|
+
try {
|
|
26039
|
+
const parsed = JSON.parse(value);
|
|
26040
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
26041
|
+
} catch {
|
|
26042
|
+
return {};
|
|
26043
|
+
}
|
|
26044
|
+
}
|
|
26045
|
+
function normalizeKey(value) {
|
|
26046
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
26047
|
+
}
|
|
26048
|
+
function normalizeFingerprint(value) {
|
|
26049
|
+
const normalized = normalizeKey(value);
|
|
26050
|
+
if (!normalized)
|
|
26051
|
+
throw new Error("finding fingerprint is required");
|
|
26052
|
+
return normalized.slice(0, 240);
|
|
26053
|
+
}
|
|
26054
|
+
function normalizeSeverity(value) {
|
|
26055
|
+
const normalized = normalizeKey(value || "medium");
|
|
26056
|
+
if (SEVERITIES.has(normalized))
|
|
26057
|
+
return normalized;
|
|
26058
|
+
if (/^(p0|blocker|urgent|highest)$/.test(normalized))
|
|
26059
|
+
return "critical";
|
|
26060
|
+
if (/^(p1|major)$/.test(normalized))
|
|
26061
|
+
return "high";
|
|
26062
|
+
if (/^(p3|minor|info)$/.test(normalized))
|
|
26063
|
+
return "low";
|
|
26064
|
+
return "medium";
|
|
26065
|
+
}
|
|
26066
|
+
function normalizeStatus(value) {
|
|
26067
|
+
const normalized = normalizeKey(value || "open");
|
|
26068
|
+
if (STATUSES.has(normalized))
|
|
26069
|
+
return normalized;
|
|
26070
|
+
if (normalized === "closed" || normalized === "fixed")
|
|
26071
|
+
return "resolved";
|
|
26072
|
+
return "open";
|
|
26073
|
+
}
|
|
26074
|
+
function normalizeResolutionStatus(value) {
|
|
26075
|
+
const status = normalizeStatus(value || "resolved");
|
|
26076
|
+
if (status === "open")
|
|
26077
|
+
throw new Error("resolve-missing status must be resolved or ignored");
|
|
26078
|
+
return status;
|
|
26079
|
+
}
|
|
26080
|
+
function redactOptional(value, max = 2000) {
|
|
26081
|
+
if (!value)
|
|
26082
|
+
return null;
|
|
26083
|
+
const redacted = redactEvidenceText(value).trim();
|
|
26084
|
+
if (!redacted)
|
|
26085
|
+
return null;
|
|
26086
|
+
return redacted.length > max ? `${redacted.slice(0, max - 3)}...` : redacted;
|
|
26087
|
+
}
|
|
26088
|
+
function rowToFinding(row) {
|
|
26089
|
+
return {
|
|
26090
|
+
schema_version: TASK_FINDING_SCHEMA_VERSION,
|
|
26091
|
+
...row,
|
|
26092
|
+
severity: normalizeSeverity(row.severity),
|
|
26093
|
+
status: normalizeStatus(row.status),
|
|
26094
|
+
metadata: parseObject2(row.metadata)
|
|
26095
|
+
};
|
|
26096
|
+
}
|
|
26097
|
+
function compactFinding(finding2) {
|
|
26098
|
+
return {
|
|
26099
|
+
schema_version: TASK_FINDING_SCHEMA_VERSION,
|
|
26100
|
+
id: finding2.id,
|
|
26101
|
+
task_id: finding2.task_id,
|
|
26102
|
+
run_id: finding2.run_id,
|
|
26103
|
+
fingerprint: finding2.fingerprint,
|
|
26104
|
+
title: finding2.title,
|
|
26105
|
+
severity: finding2.severity,
|
|
26106
|
+
status: finding2.status,
|
|
26107
|
+
source: finding2.source,
|
|
26108
|
+
summary: finding2.summary,
|
|
26109
|
+
artifact_path: finding2.artifact_path,
|
|
26110
|
+
first_seen_at: finding2.first_seen_at,
|
|
26111
|
+
last_seen_at: finding2.last_seen_at,
|
|
26112
|
+
resolved_at: finding2.resolved_at,
|
|
26113
|
+
metadata_keys: Object.keys(finding2.metadata).sort()
|
|
26114
|
+
};
|
|
26115
|
+
}
|
|
26116
|
+
function previewFinding(existing, next, timestamp3) {
|
|
26117
|
+
return {
|
|
26118
|
+
...existing,
|
|
26119
|
+
run_id: next.run_id,
|
|
26120
|
+
title: next.title,
|
|
26121
|
+
severity: next.severity,
|
|
26122
|
+
status: next.status,
|
|
26123
|
+
source: next.source,
|
|
26124
|
+
summary: next.summary,
|
|
26125
|
+
artifact_path: next.artifact_path,
|
|
26126
|
+
metadata: next.metadata,
|
|
26127
|
+
last_seen_at: timestamp3,
|
|
26128
|
+
resolved_at: next.status === "open" ? null : existing.resolved_at || timestamp3,
|
|
26129
|
+
updated_at: timestamp3
|
|
26130
|
+
};
|
|
26131
|
+
}
|
|
26132
|
+
function upsertAction(existing, next) {
|
|
26133
|
+
if (sameFinding(existing, next))
|
|
26134
|
+
return "matched";
|
|
26135
|
+
return existing.status !== "open" && next.status === "open" ? "reopened" : "updated";
|
|
26136
|
+
}
|
|
26137
|
+
function resolveRunForTask(runId, taskId, db) {
|
|
26138
|
+
if (!runId)
|
|
26139
|
+
return null;
|
|
26140
|
+
const resolved = resolveTaskRunId(runId, db);
|
|
26141
|
+
const run = getTaskRun(resolved, db);
|
|
26142
|
+
if (!run)
|
|
26143
|
+
throw new Error(`Run not found: ${runId}`);
|
|
26144
|
+
if (run.task_id !== taskId)
|
|
26145
|
+
throw new Error(`Run ${resolved} belongs to task ${run.task_id}, not ${taskId}`);
|
|
26146
|
+
return resolved;
|
|
26147
|
+
}
|
|
26148
|
+
function getFindingByFingerprint(taskId, fingerprint2, db) {
|
|
26149
|
+
const row = db.query("SELECT * FROM task_findings WHERE task_id = ? AND fingerprint = ?").get(taskId, fingerprint2);
|
|
26150
|
+
return row ? rowToFinding(row) : null;
|
|
26151
|
+
}
|
|
26152
|
+
function assertTask(taskId, db) {
|
|
26153
|
+
if (!getTask(taskId, db))
|
|
26154
|
+
throw new TaskNotFoundError(taskId);
|
|
26155
|
+
}
|
|
26156
|
+
function nextFinding(input, db) {
|
|
26157
|
+
const fingerprint2 = normalizeFingerprint(input.fingerprint);
|
|
26158
|
+
const title = redactOptional(input.title, 300);
|
|
26159
|
+
if (!title)
|
|
26160
|
+
throw new Error("finding title is required");
|
|
26161
|
+
return {
|
|
26162
|
+
fingerprint: fingerprint2,
|
|
26163
|
+
run_id: resolveRunForTask(input.run_id, input.task_id, db),
|
|
26164
|
+
title,
|
|
26165
|
+
severity: normalizeSeverity(input.severity),
|
|
26166
|
+
status: normalizeStatus(input.status),
|
|
26167
|
+
source: redactOptional(input.source, 120),
|
|
26168
|
+
summary: redactOptional(input.summary, 2000),
|
|
26169
|
+
artifact_path: redactOptional(input.artifact_path, 1000),
|
|
26170
|
+
metadata: redactValue(input.metadata || {})
|
|
26171
|
+
};
|
|
26172
|
+
}
|
|
26173
|
+
function sameFinding(left, right) {
|
|
26174
|
+
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);
|
|
26175
|
+
}
|
|
26176
|
+
function upsertTaskFinding(input, db) {
|
|
26177
|
+
const d = db || getDatabase();
|
|
26178
|
+
assertTask(input.task_id, d);
|
|
26179
|
+
const timestamp3 = input.observed_at || now();
|
|
26180
|
+
const warnings = [];
|
|
26181
|
+
const next = nextFinding(input, d);
|
|
26182
|
+
const existing = getFindingByFingerprint(input.task_id, next.fingerprint, d);
|
|
26183
|
+
const dryRun = !input.apply;
|
|
26184
|
+
if (dryRun) {
|
|
26185
|
+
const action2 = existing ? upsertAction(existing, next) : "preview";
|
|
26186
|
+
return {
|
|
26187
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
26188
|
+
local_only: true,
|
|
26189
|
+
dry_run: true,
|
|
26190
|
+
processed_at: timestamp3,
|
|
26191
|
+
action: action2,
|
|
26192
|
+
fingerprint: next.fingerprint,
|
|
26193
|
+
finding: existing ? compactFinding(previewFinding(existing, next, timestamp3)) : null,
|
|
26194
|
+
warnings
|
|
26195
|
+
};
|
|
26196
|
+
}
|
|
26197
|
+
if (!existing) {
|
|
26198
|
+
const id = uuid();
|
|
26199
|
+
d.run(`INSERT INTO task_findings (
|
|
26200
|
+
id, task_id, run_id, fingerprint, title, severity, status, source, summary, artifact_path,
|
|
26201
|
+
metadata, first_seen_at, last_seen_at, resolved_at, created_at, updated_at
|
|
26202
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
26203
|
+
id,
|
|
26204
|
+
input.task_id,
|
|
26205
|
+
next.run_id,
|
|
26206
|
+
next.fingerprint,
|
|
26207
|
+
next.title,
|
|
26208
|
+
next.severity,
|
|
26209
|
+
next.status,
|
|
26210
|
+
next.source,
|
|
26211
|
+
next.summary,
|
|
26212
|
+
next.artifact_path,
|
|
26213
|
+
JSON.stringify(next.metadata),
|
|
26214
|
+
timestamp3,
|
|
26215
|
+
timestamp3,
|
|
26216
|
+
next.status === "open" ? null : timestamp3,
|
|
26217
|
+
timestamp3,
|
|
26218
|
+
timestamp3
|
|
26219
|
+
]);
|
|
26220
|
+
return {
|
|
26221
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
26222
|
+
local_only: true,
|
|
26223
|
+
dry_run: false,
|
|
26224
|
+
processed_at: timestamp3,
|
|
26225
|
+
action: "created",
|
|
26226
|
+
fingerprint: next.fingerprint,
|
|
26227
|
+
finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
|
|
26228
|
+
warnings
|
|
26229
|
+
};
|
|
26230
|
+
}
|
|
26231
|
+
if (sameFinding(existing, next)) {
|
|
26232
|
+
return {
|
|
26233
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
26234
|
+
local_only: true,
|
|
26235
|
+
dry_run: false,
|
|
26236
|
+
processed_at: timestamp3,
|
|
26237
|
+
action: "matched",
|
|
26238
|
+
fingerprint: next.fingerprint,
|
|
26239
|
+
finding: compactFinding(existing),
|
|
26240
|
+
warnings
|
|
26241
|
+
};
|
|
26242
|
+
}
|
|
26243
|
+
const action = upsertAction(existing, next);
|
|
26244
|
+
d.run(`UPDATE task_findings SET
|
|
26245
|
+
run_id = ?, title = ?, severity = ?, status = ?, source = ?, summary = ?, artifact_path = ?,
|
|
26246
|
+
metadata = ?, last_seen_at = ?, resolved_at = ?, updated_at = ?
|
|
26247
|
+
WHERE id = ?`, [
|
|
26248
|
+
next.run_id,
|
|
26249
|
+
next.title,
|
|
26250
|
+
next.severity,
|
|
26251
|
+
next.status,
|
|
26252
|
+
next.source,
|
|
26253
|
+
next.summary,
|
|
26254
|
+
next.artifact_path,
|
|
26255
|
+
JSON.stringify(next.metadata),
|
|
26256
|
+
timestamp3,
|
|
26257
|
+
next.status === "open" ? null : existing.resolved_at || timestamp3,
|
|
26258
|
+
timestamp3,
|
|
26259
|
+
existing.id
|
|
26260
|
+
]);
|
|
26261
|
+
return {
|
|
26262
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
26263
|
+
local_only: true,
|
|
26264
|
+
dry_run: false,
|
|
26265
|
+
processed_at: timestamp3,
|
|
26266
|
+
action,
|
|
26267
|
+
fingerprint: next.fingerprint,
|
|
26268
|
+
finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
|
|
26269
|
+
warnings
|
|
26270
|
+
};
|
|
26271
|
+
}
|
|
26272
|
+
function listTaskFindings(filter = {}, db) {
|
|
26273
|
+
const d = db || getDatabase();
|
|
26274
|
+
const conditions = ["1=1"];
|
|
26275
|
+
const params = [];
|
|
26276
|
+
if (filter.task_id) {
|
|
26277
|
+
conditions.push("task_id = ?");
|
|
26278
|
+
params.push(filter.task_id);
|
|
26279
|
+
}
|
|
26280
|
+
if (filter.run_id) {
|
|
26281
|
+
conditions.push("run_id = ?");
|
|
26282
|
+
params.push(resolveTaskRunId(filter.run_id, d));
|
|
26283
|
+
}
|
|
26284
|
+
if (filter.status) {
|
|
26285
|
+
conditions.push("status = ?");
|
|
26286
|
+
params.push(normalizeStatus(filter.status));
|
|
26287
|
+
}
|
|
26288
|
+
if (filter.source) {
|
|
26289
|
+
conditions.push("source = ?");
|
|
26290
|
+
params.push(redactOptional(filter.source, 120));
|
|
26291
|
+
}
|
|
26292
|
+
const limit = Math.min(Math.max(Math.floor(filter.limit ?? 50), 1), 500);
|
|
26293
|
+
const rows = d.query(`SELECT * FROM task_findings WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC, created_at DESC LIMIT ?`).all(...[...params, limit]);
|
|
26294
|
+
return rows.map(rowToFinding);
|
|
26295
|
+
}
|
|
26296
|
+
function listCompactTaskFindings(filter = {}, db) {
|
|
26297
|
+
return listTaskFindings(filter, db).map(compactFinding);
|
|
26298
|
+
}
|
|
26299
|
+
function resolveMissingTaskFindings(input, db) {
|
|
26300
|
+
const d = db || getDatabase();
|
|
26301
|
+
assertTask(input.task_id, d);
|
|
26302
|
+
const timestamp3 = input.resolved_at || now();
|
|
26303
|
+
const status = normalizeResolutionStatus(input.status);
|
|
26304
|
+
const runId = resolveRunForTask(input.run_id, input.task_id, d);
|
|
26305
|
+
const present = new Set(input.fingerprints.map(normalizeFingerprint));
|
|
26306
|
+
const warnings = [];
|
|
26307
|
+
const conditions = ["task_id = ?", "status = 'open'"];
|
|
26308
|
+
const params = [input.task_id];
|
|
26309
|
+
if (input.source) {
|
|
26310
|
+
conditions.push("source = ?");
|
|
26311
|
+
params.push(redactOptional(input.source, 120));
|
|
26312
|
+
}
|
|
26313
|
+
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));
|
|
26314
|
+
const limit = Math.min(Math.max(Math.floor(input.limit ?? 50), 1), 200);
|
|
26315
|
+
const display = candidates.slice(0, limit);
|
|
26316
|
+
const omittedCount = Math.max(0, candidates.length - display.length);
|
|
26317
|
+
if (!input.apply) {
|
|
26318
|
+
return {
|
|
26319
|
+
schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
26320
|
+
local_only: true,
|
|
26321
|
+
dry_run: true,
|
|
26322
|
+
processed_at: timestamp3,
|
|
26323
|
+
action: candidates.length > 0 ? "preview" : "noop",
|
|
26324
|
+
task_id: input.task_id,
|
|
26325
|
+
source: input.source ? redactOptional(input.source, 120) : null,
|
|
26326
|
+
run_id: runId,
|
|
26327
|
+
present_fingerprint_count: present.size,
|
|
26328
|
+
candidate_count: candidates.length,
|
|
26329
|
+
changed_count: 0,
|
|
26330
|
+
omitted_count: omittedCount,
|
|
26331
|
+
findings: display.map(compactFinding),
|
|
26332
|
+
warnings
|
|
26333
|
+
};
|
|
26334
|
+
}
|
|
26335
|
+
const metadataPatch = redactValue({
|
|
26336
|
+
resolved_by: {
|
|
26337
|
+
agent_id: input.agent_id ?? null,
|
|
26338
|
+
run_id: runId,
|
|
26339
|
+
reason: input.reason ? redactEvidenceText(input.reason) : "missing from latest loop finding set"
|
|
26340
|
+
}
|
|
26341
|
+
});
|
|
26342
|
+
const tx = d.transaction(() => {
|
|
26343
|
+
for (const finding2 of candidates) {
|
|
26344
|
+
d.run("UPDATE task_findings SET status = ?, resolved_at = ?, metadata = ?, updated_at = ? WHERE id = ? AND status = 'open'", [
|
|
26345
|
+
status,
|
|
26346
|
+
timestamp3,
|
|
26347
|
+
JSON.stringify({ ...finding2.metadata, ...metadataPatch }),
|
|
26348
|
+
timestamp3,
|
|
26349
|
+
finding2.id
|
|
26350
|
+
]);
|
|
26351
|
+
}
|
|
26352
|
+
});
|
|
26353
|
+
tx();
|
|
26354
|
+
const updated = candidates.map((finding2) => d.query("SELECT * FROM task_findings WHERE id = ?").get(finding2.id)).filter((row) => Boolean(row)).map(rowToFinding);
|
|
26355
|
+
const visibleUpdated = updated.slice(0, limit);
|
|
26356
|
+
return {
|
|
26357
|
+
schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
26358
|
+
local_only: true,
|
|
26359
|
+
dry_run: false,
|
|
26360
|
+
processed_at: timestamp3,
|
|
26361
|
+
action: updated.length > 0 ? status : "noop",
|
|
26362
|
+
task_id: input.task_id,
|
|
26363
|
+
source: input.source ? redactOptional(input.source, 120) : null,
|
|
26364
|
+
run_id: runId,
|
|
26365
|
+
present_fingerprint_count: present.size,
|
|
26366
|
+
candidate_count: candidates.length,
|
|
26367
|
+
changed_count: updated.length,
|
|
26368
|
+
omitted_count: omittedCount,
|
|
26369
|
+
findings: visibleUpdated.map(compactFinding),
|
|
26370
|
+
warnings
|
|
26371
|
+
};
|
|
26372
|
+
}
|
|
26373
|
+
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;
|
|
26374
|
+
var init_findings = __esm(() => {
|
|
26375
|
+
init_redaction();
|
|
26376
|
+
init_types();
|
|
26377
|
+
init_database();
|
|
26378
|
+
init_tasks();
|
|
26379
|
+
init_task_runs();
|
|
26380
|
+
SEVERITIES = new Set(["low", "medium", "high", "critical"]);
|
|
26381
|
+
STATUSES = new Set(["open", "resolved", "ignored"]);
|
|
26382
|
+
});
|
|
26383
|
+
|
|
25591
26384
|
// src/lib/agent-run-dispatcher.ts
|
|
25592
26385
|
function dispatcherFromRun(run) {
|
|
25593
26386
|
const value = run.metadata["agent_run_dispatcher"];
|
|
@@ -26248,7 +27041,7 @@ function parseArray2(value) {
|
|
|
26248
27041
|
return [];
|
|
26249
27042
|
}
|
|
26250
27043
|
}
|
|
26251
|
-
function
|
|
27044
|
+
function parseObject3(value) {
|
|
26252
27045
|
try {
|
|
26253
27046
|
const parsed = JSON.parse(value || "{}");
|
|
26254
27047
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
@@ -26289,7 +27082,7 @@ function rowToKnowledgeRecord(row) {
|
|
|
26289
27082
|
agent_id: row.agent_id,
|
|
26290
27083
|
snapshot_id: row.snapshot_id,
|
|
26291
27084
|
tags: parseArray2(row.tags),
|
|
26292
|
-
metadata: redactValue(
|
|
27085
|
+
metadata: redactValue(parseObject3(row.metadata)),
|
|
26293
27086
|
created_at: row.created_at,
|
|
26294
27087
|
updated_at: row.updated_at
|
|
26295
27088
|
};
|
|
@@ -26508,7 +27301,7 @@ function parseArray3(value) {
|
|
|
26508
27301
|
return [];
|
|
26509
27302
|
}
|
|
26510
27303
|
}
|
|
26511
|
-
function
|
|
27304
|
+
function parseObject4(value) {
|
|
26512
27305
|
try {
|
|
26513
27306
|
const parsed = JSON.parse(value || "{}");
|
|
26514
27307
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
@@ -26557,7 +27350,7 @@ function rowToRisk(row) {
|
|
|
26557
27350
|
plan_id: row.plan_id,
|
|
26558
27351
|
task_id: row.task_id,
|
|
26559
27352
|
tags: parseArray3(row.tags),
|
|
26560
|
-
metadata: redactValue(
|
|
27353
|
+
metadata: redactValue(parseObject4(row.metadata)),
|
|
26561
27354
|
created_at: row.created_at,
|
|
26562
27355
|
updated_at: row.updated_at,
|
|
26563
27356
|
closed_at: row.closed_at
|
|
@@ -32399,6 +33192,38 @@ ${lines.join(`
|
|
|
32399
33192
|
}
|
|
32400
33193
|
});
|
|
32401
33194
|
}
|
|
33195
|
+
if (shouldRegisterTool("begin_task_run_transaction")) {
|
|
33196
|
+
server.tool("begin_task_run_transaction", "Preview or apply an idempotent local loop run transaction keyed by a stable loop/run id.", {
|
|
33197
|
+
task_id: exports_external.string().describe("Task ID"),
|
|
33198
|
+
key: exports_external.string().optional().describe("Stable idempotency key"),
|
|
33199
|
+
loop_id: exports_external.string().optional().describe("Loop identifier; used as key fallback"),
|
|
33200
|
+
loop_run_id: exports_external.string().optional().describe("Loop run identifier; used as key fallback"),
|
|
33201
|
+
agent_id: exports_external.string().optional().describe("Agent starting the run"),
|
|
33202
|
+
title: exports_external.string().optional().describe("Run title"),
|
|
33203
|
+
summary: exports_external.string().optional().describe("Run summary"),
|
|
33204
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata"),
|
|
33205
|
+
claim: exports_external.boolean().optional().describe("Claim/start the task before recording the run"),
|
|
33206
|
+
apply: exports_external.boolean().optional().describe("Apply the transaction; false or omitted is dry-run")
|
|
33207
|
+
}, async ({ task_id, key, loop_id, loop_run_id, agent_id, title, summary, metadata, claim, apply }) => {
|
|
33208
|
+
try {
|
|
33209
|
+
const result = beginTaskRunTransaction({
|
|
33210
|
+
task_id: resolveId(task_id),
|
|
33211
|
+
key,
|
|
33212
|
+
loop_id,
|
|
33213
|
+
loop_run_id,
|
|
33214
|
+
agent_id,
|
|
33215
|
+
title,
|
|
33216
|
+
summary,
|
|
33217
|
+
metadata,
|
|
33218
|
+
claim,
|
|
33219
|
+
apply
|
|
33220
|
+
});
|
|
33221
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
33222
|
+
} catch (e) {
|
|
33223
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
33224
|
+
}
|
|
33225
|
+
});
|
|
33226
|
+
}
|
|
32402
33227
|
if (shouldRegisterTool("list_task_runs")) {
|
|
32403
33228
|
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 }) => {
|
|
32404
33229
|
try {
|
|
@@ -32496,15 +33321,117 @@ ${lines.join(`
|
|
|
32496
33321
|
});
|
|
32497
33322
|
}
|
|
32498
33323
|
if (shouldRegisterTool("finish_task_run")) {
|
|
32499
|
-
server.tool("finish_task_run", "Finish a local run ledger entry as completed, failed, or cancelled.", {
|
|
32500
|
-
run_id: exports_external.string().describe("Run ID or prefix"),
|
|
32501
|
-
|
|
33324
|
+
server.tool("finish_task_run", "Finish a local run ledger entry as completed, failed, or cancelled. Supports idempotent lookup by key.", {
|
|
33325
|
+
run_id: exports_external.string().optional().describe("Run ID or prefix"),
|
|
33326
|
+
key: exports_external.string().optional().describe("Idempotency key when run_id is omitted"),
|
|
33327
|
+
task_id: exports_external.string().optional().describe("Task scope for key lookup"),
|
|
33328
|
+
status: exports_external.enum(["completed", "failed", "cancelled"]).optional().describe("Final run status"),
|
|
32502
33329
|
summary: exports_external.string().optional().describe("Final summary"),
|
|
32503
|
-
agent_id: exports_external.string().optional().describe("Agent finishing the run")
|
|
32504
|
-
|
|
33330
|
+
agent_id: exports_external.string().optional().describe("Agent finishing the run"),
|
|
33331
|
+
apply: exports_external.boolean().optional().describe("Apply the transaction; false previews without mutating")
|
|
33332
|
+
}, async ({ run_id, key, task_id, status, summary, agent_id, apply }) => {
|
|
33333
|
+
try {
|
|
33334
|
+
if (run_id && !key && apply === undefined) {
|
|
33335
|
+
const result2 = finishTaskRunTransaction({ run_id, status: status || "completed", summary, agent_id, apply: true });
|
|
33336
|
+
const run = result2.run ? getTaskRunLedger(result2.run.id).run : null;
|
|
33337
|
+
return { content: [{ type: "text", text: JSON.stringify(run, null, 2) }] };
|
|
33338
|
+
}
|
|
33339
|
+
const result = finishTaskRunTransaction({
|
|
33340
|
+
run_id,
|
|
33341
|
+
key,
|
|
33342
|
+
task_id: task_id ? resolveId(task_id) : undefined,
|
|
33343
|
+
status: status || "completed",
|
|
33344
|
+
summary,
|
|
33345
|
+
agent_id,
|
|
33346
|
+
apply: apply !== false
|
|
33347
|
+
});
|
|
33348
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
33349
|
+
} catch (e) {
|
|
33350
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
33351
|
+
}
|
|
33352
|
+
});
|
|
33353
|
+
}
|
|
33354
|
+
if (shouldRegisterTool("upsert_task_finding")) {
|
|
33355
|
+
server.tool("upsert_task_finding", "Preview or apply an idempotent local finding upsert scoped by task and fingerprint.", {
|
|
33356
|
+
task_id: exports_external.string().describe("Task ID"),
|
|
33357
|
+
fingerprint: exports_external.string().describe("Stable finding fingerprint"),
|
|
33358
|
+
title: exports_external.string().describe("Finding title"),
|
|
33359
|
+
severity: exports_external.enum(["low", "medium", "high", "critical"]).optional().describe("Finding severity"),
|
|
33360
|
+
status: exports_external.enum(["open", "resolved", "ignored"]).optional().describe("Finding status"),
|
|
33361
|
+
source: exports_external.string().optional().describe("Loop/tool source name"),
|
|
33362
|
+
summary: exports_external.string().optional().describe("Bounded finding summary"),
|
|
33363
|
+
artifact_path: exports_external.string().optional().describe("Local artifact path/reference; content is not read"),
|
|
33364
|
+
run_id: exports_external.string().optional().describe("Run ledger ID or prefix"),
|
|
33365
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata"),
|
|
33366
|
+
apply: exports_external.boolean().optional().describe("Apply the upsert; false or omitted is dry-run")
|
|
33367
|
+
}, async ({ task_id, fingerprint: fingerprint3, title, severity, status, source: source3, summary, artifact_path, run_id, metadata, apply }) => {
|
|
32505
33368
|
try {
|
|
32506
|
-
const
|
|
32507
|
-
|
|
33369
|
+
const result = upsertTaskFinding({
|
|
33370
|
+
task_id: resolveId(task_id),
|
|
33371
|
+
fingerprint: fingerprint3,
|
|
33372
|
+
title,
|
|
33373
|
+
severity,
|
|
33374
|
+
status,
|
|
33375
|
+
source: source3,
|
|
33376
|
+
summary,
|
|
33377
|
+
artifact_path,
|
|
33378
|
+
run_id,
|
|
33379
|
+
metadata,
|
|
33380
|
+
apply
|
|
33381
|
+
});
|
|
33382
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
33383
|
+
} catch (e) {
|
|
33384
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
33385
|
+
}
|
|
33386
|
+
});
|
|
33387
|
+
}
|
|
33388
|
+
if (shouldRegisterTool("list_task_findings")) {
|
|
33389
|
+
server.tool("list_task_findings", "List compact local findings with bounded output.", {
|
|
33390
|
+
task_id: exports_external.string().optional().describe("Filter by task"),
|
|
33391
|
+
run_id: exports_external.string().optional().describe("Filter by run"),
|
|
33392
|
+
status: exports_external.enum(["open", "resolved", "ignored"]).optional().describe("Filter by status"),
|
|
33393
|
+
source: exports_external.string().optional().describe("Filter by source"),
|
|
33394
|
+
limit: exports_external.number().optional().describe("Maximum findings to return")
|
|
33395
|
+
}, async ({ task_id, run_id, status, source: source3, limit }) => {
|
|
33396
|
+
try {
|
|
33397
|
+
const findings = listCompactTaskFindings({
|
|
33398
|
+
task_id: task_id ? resolveId(task_id) : undefined,
|
|
33399
|
+
run_id,
|
|
33400
|
+
status,
|
|
33401
|
+
source: source3,
|
|
33402
|
+
limit
|
|
33403
|
+
});
|
|
33404
|
+
return { content: [{ type: "text", text: JSON.stringify(findings) }] };
|
|
33405
|
+
} catch (e) {
|
|
33406
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
33407
|
+
}
|
|
33408
|
+
});
|
|
33409
|
+
}
|
|
33410
|
+
if (shouldRegisterTool("resolve_missing_task_findings")) {
|
|
33411
|
+
server.tool("resolve_missing_task_findings", "Resolve open findings absent from the latest loop finding set.", {
|
|
33412
|
+
task_id: exports_external.string().describe("Task ID"),
|
|
33413
|
+
fingerprints: exports_external.array(exports_external.string()).optional().describe("Fingerprints still present in the latest run"),
|
|
33414
|
+
source: exports_external.string().optional().describe("Only resolve findings from this source"),
|
|
33415
|
+
run_id: exports_external.string().optional().describe("Run ledger ID or prefix for audit metadata"),
|
|
33416
|
+
status: exports_external.enum(["resolved", "ignored"]).optional().describe("Resolution status"),
|
|
33417
|
+
agent_id: exports_external.string().optional().describe("Agent resolving findings"),
|
|
33418
|
+
reason: exports_external.string().optional().describe("Resolution reason"),
|
|
33419
|
+
limit: exports_external.number().optional().describe("Maximum findings returned"),
|
|
33420
|
+
apply: exports_external.boolean().optional().describe("Apply resolution; false or omitted is dry-run")
|
|
33421
|
+
}, async ({ task_id, fingerprints, source: source3, run_id, status, agent_id, reason, limit, apply }) => {
|
|
33422
|
+
try {
|
|
33423
|
+
const result = resolveMissingTaskFindings({
|
|
33424
|
+
task_id: resolveId(task_id),
|
|
33425
|
+
fingerprints: fingerprints || [],
|
|
33426
|
+
source: source3,
|
|
33427
|
+
run_id,
|
|
33428
|
+
status,
|
|
33429
|
+
agent_id,
|
|
33430
|
+
reason,
|
|
33431
|
+
limit,
|
|
33432
|
+
apply
|
|
33433
|
+
});
|
|
33434
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
32508
33435
|
} catch (e) {
|
|
32509
33436
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
32510
33437
|
}
|
|
@@ -32840,6 +33767,7 @@ var init_task_resources = __esm(() => {
|
|
|
32840
33767
|
init_agents();
|
|
32841
33768
|
init_task_commits();
|
|
32842
33769
|
init_task_runs();
|
|
33770
|
+
init_findings();
|
|
32843
33771
|
init_agent_run_dispatcher();
|
|
32844
33772
|
init_verification_providers();
|
|
32845
33773
|
init_release_notes();
|
|
@@ -37384,6 +38312,39 @@ async function handleCreateTask(req, ctx, json2, taskToSummary2) {
|
|
|
37384
38312
|
return json2({ error: e instanceof Error ? e.message : "Failed to create task" }, 500);
|
|
37385
38313
|
}
|
|
37386
38314
|
}
|
|
38315
|
+
async function handleUpsertTask(req, ctx, json2, taskToSummary2) {
|
|
38316
|
+
try {
|
|
38317
|
+
const body = await req.json();
|
|
38318
|
+
if (typeof body["fingerprint"] !== "string" || body["fingerprint"].trim() === "") {
|
|
38319
|
+
return json2({ error: "Missing 'fingerprint'" }, 400);
|
|
38320
|
+
}
|
|
38321
|
+
if (typeof body["title"] !== "string" || body["title"].trim() === "") {
|
|
38322
|
+
return json2({ error: "Missing 'title'" }, 400);
|
|
38323
|
+
}
|
|
38324
|
+
const metadata = body["metadata"] && typeof body["metadata"] === "object" && !Array.isArray(body["metadata"]) ? { ...body["metadata"] } : {};
|
|
38325
|
+
for (const key of ["expectation_id", "expectation_fingerprint", "evidence_paths", "origin_loop_id", "origin_run_id", "expected", "observed", "acceptance"]) {
|
|
38326
|
+
if (body[key] !== undefined)
|
|
38327
|
+
metadata[key] = body[key];
|
|
38328
|
+
}
|
|
38329
|
+
const result = upsertTaskByFingerprint({
|
|
38330
|
+
fingerprint: body["fingerprint"],
|
|
38331
|
+
title: body["title"],
|
|
38332
|
+
description: typeof body["description"] === "string" ? body["description"] : undefined,
|
|
38333
|
+
status: body["status"],
|
|
38334
|
+
priority: body["priority"],
|
|
38335
|
+
project_id: typeof body["project_id"] === "string" ? body["project_id"] : undefined,
|
|
38336
|
+
task_list_id: typeof body["task_list_id"] === "string" ? body["task_list_id"] : undefined,
|
|
38337
|
+
assigned_to: typeof body["assigned_to"] === "string" ? body["assigned_to"] : undefined,
|
|
38338
|
+
working_dir: typeof body["working_dir"] === "string" ? body["working_dir"] : undefined,
|
|
38339
|
+
tags: Array.isArray(body["tags"]) ? body["tags"].filter((tag) => typeof tag === "string") : undefined,
|
|
38340
|
+
metadata
|
|
38341
|
+
});
|
|
38342
|
+
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 });
|
|
38343
|
+
return json2({ created: result.created, task: taskToSummary2(result.task) }, result.created ? 201 : 200);
|
|
38344
|
+
} catch (e) {
|
|
38345
|
+
return json2({ error: e instanceof Error ? e.message : "Failed to upsert task" }, 500);
|
|
38346
|
+
}
|
|
38347
|
+
}
|
|
37387
38348
|
function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
|
|
37388
38349
|
const format = url.searchParams.get("format") || "json";
|
|
37389
38350
|
const status = url.searchParams.get("status") || undefined;
|
|
@@ -38064,8 +39025,10 @@ function taskToSummary(task2, fields) {
|
|
|
38064
39025
|
task_list_id: task2.task_list_id,
|
|
38065
39026
|
agent_id: task2.agent_id,
|
|
38066
39027
|
assigned_to: task2.assigned_to,
|
|
39028
|
+
working_dir: task2.working_dir,
|
|
38067
39029
|
locked_by: task2.locked_by,
|
|
38068
39030
|
tags: task2.tags,
|
|
39031
|
+
metadata: task2.metadata,
|
|
38069
39032
|
version: task2.version,
|
|
38070
39033
|
created_at: task2.created_at,
|
|
38071
39034
|
updated_at: task2.updated_at,
|
|
@@ -38207,6 +39170,9 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
38207
39170
|
if (path === "/api/tasks" && method === "POST") {
|
|
38208
39171
|
return handleCreateTask(req, ctx, jsonWithCors, taskToSummary);
|
|
38209
39172
|
}
|
|
39173
|
+
if (path === "/api/tasks/upsert" && method === "POST") {
|
|
39174
|
+
return handleUpsertTask(req, ctx, jsonWithCors, taskToSummary);
|
|
39175
|
+
}
|
|
38210
39176
|
if (path === "/api/tasks/export" && method === "GET") {
|
|
38211
39177
|
return handleTasksExport(req, url, ctx, jsonWithCors, taskToSummary);
|
|
38212
39178
|
}
|
|
@@ -38402,7 +39368,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
38402
39368
|
} catch {}
|
|
38403
39369
|
}
|
|
38404
39370
|
}
|
|
38405
|
-
var MIME_TYPES, SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX
|
|
39371
|
+
var MIME_TYPES, SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX;
|
|
38406
39372
|
var init_serve = __esm(() => {
|
|
38407
39373
|
init_database();
|
|
38408
39374
|
init_api_keys();
|
|
@@ -38427,6 +39393,7 @@ var init_serve = __esm(() => {
|
|
|
38427
39393
|
"Permissions-Policy": "camera=, microphone=, geolocation="
|
|
38428
39394
|
};
|
|
38429
39395
|
rateLimitMap = new Map;
|
|
39396
|
+
RATE_LIMIT_MAX = Number.parseInt(process.env["TODOS_RATE_LIMIT_MAX"] || "120", 10);
|
|
38430
39397
|
});
|
|
38431
39398
|
|
|
38432
39399
|
// src/mcp/index.ts
|