@hasna/todos 0.11.58 → 0.11.60
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 +66 -2
- 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 +1518 -197
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +600 -14
- 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 +968 -15
- 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 +1082 -27
- 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 +600 -14
- package/dist/release-provenance.json +3 -3
- package/dist/server/index.js +1082 -27
- 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 +473 -11
- package/dist/types/index.d.ts +11 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
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",
|
|
@@ -4014,7 +4099,7 @@ var init_event_hooks = __esm(() => {
|
|
|
4014
4099
|
VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
|
|
4015
4100
|
});
|
|
4016
4101
|
|
|
4017
|
-
// node_modules/.bun/@hasna+events@0.1.
|
|
4102
|
+
// node_modules/.bun/@hasna+events@0.1.10/node_modules/@hasna/events/dist/index.js
|
|
4018
4103
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
4019
4104
|
import { existsSync as existsSync6 } from "fs";
|
|
4020
4105
|
import { homedir } from "os";
|
|
@@ -4061,14 +4146,40 @@ function matchRecord(input, matcher) {
|
|
|
4061
4146
|
return true;
|
|
4062
4147
|
return Object.entries(matcher).every(([path, expected]) => {
|
|
4063
4148
|
const actual = getPathValue(input, path);
|
|
4064
|
-
|
|
4065
|
-
return matchString(actual === undefined ? undefined : String(actual), expected, {
|
|
4066
|
-
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
4067
|
-
});
|
|
4068
|
-
}
|
|
4069
|
-
return actual === expected;
|
|
4149
|
+
return matchField(actual, expected, path);
|
|
4070
4150
|
});
|
|
4071
4151
|
}
|
|
4152
|
+
function matchField(actual, expected, path) {
|
|
4153
|
+
if (isNegativeMatcher(expected)) {
|
|
4154
|
+
return !matchPositiveField(actual, expected.not, path);
|
|
4155
|
+
}
|
|
4156
|
+
return matchPositiveField(actual, expected, path);
|
|
4157
|
+
}
|
|
4158
|
+
function matchPositiveField(actual, expected, path) {
|
|
4159
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
4160
|
+
return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
|
|
4161
|
+
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
4162
|
+
}));
|
|
4163
|
+
}
|
|
4164
|
+
if (Array.isArray(actual)) {
|
|
4165
|
+
return actual.some((item) => item === expected);
|
|
4166
|
+
}
|
|
4167
|
+
return actual === expected;
|
|
4168
|
+
}
|
|
4169
|
+
function stringCandidates(actual) {
|
|
4170
|
+
if (actual === undefined)
|
|
4171
|
+
return [];
|
|
4172
|
+
if (Array.isArray(actual)) {
|
|
4173
|
+
return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
|
|
4174
|
+
}
|
|
4175
|
+
return [String(actual)];
|
|
4176
|
+
}
|
|
4177
|
+
function isPrimitiveFieldValue(value) {
|
|
4178
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
4179
|
+
}
|
|
4180
|
+
function isNegativeMatcher(value) {
|
|
4181
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
|
|
4182
|
+
}
|
|
4072
4183
|
function eventMatchesFilter(event, filter) {
|
|
4073
4184
|
return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
|
|
4074
4185
|
}
|
|
@@ -4676,9 +4787,66 @@ function taskEventData(task, extra = {}) {
|
|
|
4676
4787
|
started_at: task.started_at,
|
|
4677
4788
|
completed_at: task.completed_at,
|
|
4678
4789
|
due_at: task.due_at,
|
|
4790
|
+
requires_approval: task.requires_approval,
|
|
4791
|
+
approved_by: task.approved_by,
|
|
4792
|
+
approved_at: task.approved_at,
|
|
4679
4793
|
...extra
|
|
4680
4794
|
};
|
|
4681
4795
|
}
|
|
4796
|
+
function booleanField(value) {
|
|
4797
|
+
if (typeof value === "boolean")
|
|
4798
|
+
return value;
|
|
4799
|
+
if (typeof value === "number") {
|
|
4800
|
+
if (value === 1)
|
|
4801
|
+
return true;
|
|
4802
|
+
if (value === 0)
|
|
4803
|
+
return false;
|
|
4804
|
+
}
|
|
4805
|
+
if (typeof value === "string") {
|
|
4806
|
+
const normalized = value.trim().toLowerCase();
|
|
4807
|
+
if (["true", "1", "yes", "on"].includes(normalized))
|
|
4808
|
+
return true;
|
|
4809
|
+
if (["false", "0", "no", "off"].includes(normalized))
|
|
4810
|
+
return false;
|
|
4811
|
+
}
|
|
4812
|
+
return;
|
|
4813
|
+
}
|
|
4814
|
+
function objectField(value) {
|
|
4815
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
4816
|
+
}
|
|
4817
|
+
function firstBoolean(records, keys) {
|
|
4818
|
+
for (const record of records) {
|
|
4819
|
+
for (const key of keys) {
|
|
4820
|
+
const value = booleanField(record[key]);
|
|
4821
|
+
if (value !== undefined)
|
|
4822
|
+
return value;
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4825
|
+
return;
|
|
4826
|
+
}
|
|
4827
|
+
function routingAutomationMetadata(task) {
|
|
4828
|
+
const automation = objectField(task.metadata.automation);
|
|
4829
|
+
const records = [task.metadata];
|
|
4830
|
+
if (automation)
|
|
4831
|
+
records.push(automation);
|
|
4832
|
+
const result = {};
|
|
4833
|
+
const aliases = [
|
|
4834
|
+
["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
|
|
4835
|
+
["no_auto", ["no_auto", "noAuto"]],
|
|
4836
|
+
["manual", ["manual"]],
|
|
4837
|
+
["manual_required", ["manual_required", "manualRequired"]],
|
|
4838
|
+
["requires_approval", ["requires_approval", "requiresApproval"]],
|
|
4839
|
+
["approval_required", ["approval_required", "approvalRequired"]]
|
|
4840
|
+
];
|
|
4841
|
+
for (const [canonical, keys] of aliases) {
|
|
4842
|
+
const value = firstBoolean(records, keys);
|
|
4843
|
+
if (value !== undefined)
|
|
4844
|
+
result[canonical] = value;
|
|
4845
|
+
}
|
|
4846
|
+
if (task.requires_approval)
|
|
4847
|
+
result.requires_approval = true;
|
|
4848
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
4849
|
+
}
|
|
4682
4850
|
function taskEventMetadata(task) {
|
|
4683
4851
|
const metadata = {
|
|
4684
4852
|
package: "@hasna/todos",
|
|
@@ -4689,6 +4857,14 @@ function taskEventMetadata(task) {
|
|
|
4689
4857
|
task_list_id: task.task_list_id,
|
|
4690
4858
|
working_dir: task.working_dir
|
|
4691
4859
|
};
|
|
4860
|
+
const routeEnabled = booleanField(task.metadata.route_enabled);
|
|
4861
|
+
if (routeEnabled !== undefined) {
|
|
4862
|
+
metadata.route_enabled = routeEnabled;
|
|
4863
|
+
}
|
|
4864
|
+
const automation = routingAutomationMetadata(task);
|
|
4865
|
+
if (automation) {
|
|
4866
|
+
metadata.automation = automation;
|
|
4867
|
+
}
|
|
4692
4868
|
try {
|
|
4693
4869
|
const project = task.project_id ? getProject(task.project_id) : null;
|
|
4694
4870
|
const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
|
|
@@ -4706,9 +4882,6 @@ function taskEventMetadata(task) {
|
|
|
4706
4882
|
if (projectPath) {
|
|
4707
4883
|
metadata.project_kind = classifyProjectKind(projectPath);
|
|
4708
4884
|
metadata.project_is_worktree = isWorktreePath(projectPath);
|
|
4709
|
-
if (typeof task.metadata.route_enabled === "boolean") {
|
|
4710
|
-
metadata.route_enabled = task.metadata.route_enabled;
|
|
4711
|
-
}
|
|
4712
4885
|
metadata.working_dir = task.working_dir ?? projectPath;
|
|
4713
4886
|
}
|
|
4714
4887
|
const taskList = task.task_list_id ? getTaskList(task.task_list_id) ?? (project ? getTaskListBySlug(task.task_list_id, project.id) : null) : project?.task_list_id ? getTaskListBySlug(project.task_list_id, project.id) : null;
|
|
@@ -5290,6 +5463,17 @@ function replaceTaskTags(taskId, tags, db) {
|
|
|
5290
5463
|
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
5291
5464
|
insertTaskTags(taskId, tags, db);
|
|
5292
5465
|
}
|
|
5466
|
+
function addMetadataConditions(metadata, conditions, params) {
|
|
5467
|
+
if (!metadata)
|
|
5468
|
+
return;
|
|
5469
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
5470
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
|
|
5471
|
+
throw new Error(`Invalid metadata filter key: ${key}`);
|
|
5472
|
+
}
|
|
5473
|
+
conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
|
|
5474
|
+
params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
|
|
5475
|
+
}
|
|
5476
|
+
}
|
|
5293
5477
|
function createTask(input, db) {
|
|
5294
5478
|
const d = db || getDatabase();
|
|
5295
5479
|
const timestamp = now();
|
|
@@ -5472,6 +5656,7 @@ function listTasks(filter = {}, db) {
|
|
|
5472
5656
|
params.push(filter.task_type);
|
|
5473
5657
|
}
|
|
5474
5658
|
}
|
|
5659
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
5475
5660
|
const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
|
|
5476
5661
|
if (filter.cursor) {
|
|
5477
5662
|
try {
|
|
@@ -5496,6 +5681,54 @@ function listTasks(filter = {}, db) {
|
|
|
5496
5681
|
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
|
|
5497
5682
|
return rows.map(rowToTask);
|
|
5498
5683
|
}
|
|
5684
|
+
function getTaskByFingerprint(fingerprint, db) {
|
|
5685
|
+
const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
|
|
5686
|
+
return tasks[0] ?? null;
|
|
5687
|
+
}
|
|
5688
|
+
function mergeTaskMetadata(current, next, fingerprint) {
|
|
5689
|
+
return {
|
|
5690
|
+
...current,
|
|
5691
|
+
...next ?? {},
|
|
5692
|
+
fingerprint
|
|
5693
|
+
};
|
|
5694
|
+
}
|
|
5695
|
+
function upsertTaskByFingerprint(input, db) {
|
|
5696
|
+
const d = db || getDatabase();
|
|
5697
|
+
const fingerprint = input.fingerprint.trim();
|
|
5698
|
+
if (!fingerprint)
|
|
5699
|
+
throw new Error("fingerprint is required");
|
|
5700
|
+
const existing = getTaskByFingerprint(fingerprint, d);
|
|
5701
|
+
const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
|
|
5702
|
+
if (!existing) {
|
|
5703
|
+
const task2 = createTask({ ...input, metadata }, d);
|
|
5704
|
+
return { task: task2, created: true };
|
|
5705
|
+
}
|
|
5706
|
+
const task = updateTask(existing.id, {
|
|
5707
|
+
version: existing.version,
|
|
5708
|
+
title: input.title,
|
|
5709
|
+
description: input.description,
|
|
5710
|
+
status: input.status,
|
|
5711
|
+
priority: input.priority,
|
|
5712
|
+
project_id: input.project_id,
|
|
5713
|
+
assigned_to: input.assigned_to,
|
|
5714
|
+
working_dir: input.working_dir,
|
|
5715
|
+
plan_id: input.plan_id,
|
|
5716
|
+
task_list_id: input.task_list_id,
|
|
5717
|
+
tags: input.tags,
|
|
5718
|
+
metadata,
|
|
5719
|
+
due_at: input.due_at,
|
|
5720
|
+
estimated_minutes: input.estimated_minutes,
|
|
5721
|
+
sla_minutes: input.sla_minutes,
|
|
5722
|
+
confidence: input.confidence,
|
|
5723
|
+
retry_count: input.retry_count,
|
|
5724
|
+
max_retries: input.max_retries,
|
|
5725
|
+
retry_after: input.retry_after,
|
|
5726
|
+
requires_approval: input.requires_approval,
|
|
5727
|
+
recurrence_rule: input.recurrence_rule,
|
|
5728
|
+
task_type: input.task_type
|
|
5729
|
+
}, d);
|
|
5730
|
+
return { task, created: false };
|
|
5731
|
+
}
|
|
5499
5732
|
function countTasks(filter = {}, db) {
|
|
5500
5733
|
const d = db || getDatabase();
|
|
5501
5734
|
const conditions = [];
|
|
@@ -5559,6 +5792,7 @@ function countTasks(filter = {}, db) {
|
|
|
5559
5792
|
conditions.push("task_list_id = ?");
|
|
5560
5793
|
params.push(filter.task_list_id);
|
|
5561
5794
|
}
|
|
5795
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
5562
5796
|
if (!filter.include_archived) {
|
|
5563
5797
|
conditions.push("archived_at IS NULL");
|
|
5564
5798
|
}
|
|
@@ -5609,6 +5843,10 @@ function updateTask(id, input, db) {
|
|
|
5609
5843
|
sets.push("assigned_to = ?");
|
|
5610
5844
|
params.push(input.assigned_to);
|
|
5611
5845
|
}
|
|
5846
|
+
if (input.working_dir !== undefined) {
|
|
5847
|
+
sets.push("working_dir = ?");
|
|
5848
|
+
params.push(input.working_dir);
|
|
5849
|
+
}
|
|
5612
5850
|
if (input.tags !== undefined) {
|
|
5613
5851
|
sets.push("tags = ?");
|
|
5614
5852
|
params.push(JSON.stringify(input.tags));
|
|
@@ -5697,6 +5935,8 @@ function updateTask(id, input, db) {
|
|
|
5697
5935
|
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
5698
5936
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
5699
5937
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
5938
|
+
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
5939
|
+
logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
5700
5940
|
if (input.approved_by !== undefined)
|
|
5701
5941
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
5702
5942
|
const updatedTask = {
|
|
@@ -5732,6 +5972,10 @@ function updateTask(id, input, db) {
|
|
|
5732
5972
|
if (input.approved_by !== undefined) {
|
|
5733
5973
|
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
5734
5974
|
}
|
|
5975
|
+
const updatePayload = taskEventData(updatedTask);
|
|
5976
|
+
dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
|
|
5977
|
+
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
|
|
5978
|
+
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
|
|
5735
5979
|
return updatedTask;
|
|
5736
5980
|
}
|
|
5737
5981
|
function deleteTask(id, db) {
|
|
@@ -8749,6 +8993,72 @@ function rowToArtifact(row) {
|
|
|
8749
8993
|
function getRunRow(runId, db) {
|
|
8750
8994
|
return db.query("SELECT * FROM task_runs WHERE id = ?").get(runId);
|
|
8751
8995
|
}
|
|
8996
|
+
function normalizeTransactionKey(input) {
|
|
8997
|
+
const key = (input.key || input.loop_run_id || input.loop_id || "").trim();
|
|
8998
|
+
if (!key)
|
|
8999
|
+
throw new Error("idempotent run transactions require --key, --loop-run-id, or --loop-id");
|
|
9000
|
+
return key.toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 240);
|
|
9001
|
+
}
|
|
9002
|
+
function loopTransactionMetadata(record) {
|
|
9003
|
+
const value = record.metadata["loop_transaction"];
|
|
9004
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
9005
|
+
}
|
|
9006
|
+
function runKey(record) {
|
|
9007
|
+
const tx = loopTransactionMetadata(record);
|
|
9008
|
+
const key = tx["idempotency_key"] ?? record.metadata["idempotency_key"];
|
|
9009
|
+
return typeof key === "string" ? key : null;
|
|
9010
|
+
}
|
|
9011
|
+
function loopId(record) {
|
|
9012
|
+
const tx = loopTransactionMetadata(record);
|
|
9013
|
+
const value = tx["loop_id"] ?? record.metadata["loop_id"];
|
|
9014
|
+
return typeof value === "string" ? value : null;
|
|
9015
|
+
}
|
|
9016
|
+
function loopRunId(record) {
|
|
9017
|
+
const tx = loopTransactionMetadata(record);
|
|
9018
|
+
const value = tx["loop_run_id"] ?? record.metadata["loop_run_id"];
|
|
9019
|
+
return typeof value === "string" ? value : null;
|
|
9020
|
+
}
|
|
9021
|
+
function getTaskRunTransactionByKey(key, taskId, db) {
|
|
9022
|
+
if (taskId) {
|
|
9023
|
+
return db.query("SELECT * FROM task_run_transactions WHERE task_id = ? AND key = ?").get(taskId, key);
|
|
9024
|
+
}
|
|
9025
|
+
const rows = db.query("SELECT * FROM task_run_transactions WHERE key = ? ORDER BY created_at DESC LIMIT 2").all(key);
|
|
9026
|
+
if (rows.length > 1)
|
|
9027
|
+
throw new Error(`Run transaction key is ambiguous across tasks: ${key}. Pass task_id.`);
|
|
9028
|
+
return rows[0] ?? null;
|
|
9029
|
+
}
|
|
9030
|
+
function summarizeTaskRun(run) {
|
|
9031
|
+
return {
|
|
9032
|
+
id: run.id,
|
|
9033
|
+
task_id: run.task_id,
|
|
9034
|
+
agent_id: run.agent_id,
|
|
9035
|
+
title: run.title,
|
|
9036
|
+
status: run.status,
|
|
9037
|
+
summary: run.summary,
|
|
9038
|
+
idempotency_key: runKey(run),
|
|
9039
|
+
loop_id: loopId(run),
|
|
9040
|
+
loop_run_id: loopRunId(run),
|
|
9041
|
+
metadata_keys: Object.keys(run.metadata).sort(),
|
|
9042
|
+
started_at: run.started_at,
|
|
9043
|
+
completed_at: run.completed_at,
|
|
9044
|
+
updated_at: run.updated_at
|
|
9045
|
+
};
|
|
9046
|
+
}
|
|
9047
|
+
function findTaskRunByTransactionKey(key, taskId, db) {
|
|
9048
|
+
const d = db || getDatabase();
|
|
9049
|
+
const normalized = normalizeTransactionKey({ key });
|
|
9050
|
+
const transaction = getTaskRunTransactionByKey(normalized, taskId, d);
|
|
9051
|
+
if (transaction?.run_id)
|
|
9052
|
+
return getTaskRun(transaction.run_id, d);
|
|
9053
|
+
return null;
|
|
9054
|
+
}
|
|
9055
|
+
function loopRunCommands(run, key) {
|
|
9056
|
+
return [
|
|
9057
|
+
run ? `todos runs show ${run.id.slice(0, 8)}` : "todos runs list",
|
|
9058
|
+
run ? `todos findings list --task ${run.task_id.slice(0, 8)} --json` : "todos findings list --json",
|
|
9059
|
+
`todos runs begin <task-id> --key ${key} --apply --json`
|
|
9060
|
+
];
|
|
9061
|
+
}
|
|
8752
9062
|
function resolveTaskRunId(idOrPrefix, db) {
|
|
8753
9063
|
const d = db || getDatabase();
|
|
8754
9064
|
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 +9077,7 @@ function startTaskRun(input, db) {
|
|
|
8767
9077
|
const d = db || getDatabase();
|
|
8768
9078
|
if (!getTask(input.task_id, d))
|
|
8769
9079
|
throw new TaskNotFoundError(input.task_id);
|
|
8770
|
-
const id = uuid();
|
|
9080
|
+
const id = input.id ?? uuid();
|
|
8771
9081
|
const timestamp = input.started_at || now();
|
|
8772
9082
|
if (input.claim && input.agent_id) {
|
|
8773
9083
|
startTask(input.task_id, input.agent_id, d);
|
|
@@ -8805,6 +9115,97 @@ function startTaskRun(input, db) {
|
|
|
8805
9115
|
emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
|
|
8806
9116
|
return run;
|
|
8807
9117
|
}
|
|
9118
|
+
function beginTaskRunTransaction(input, db) {
|
|
9119
|
+
const d = db || getDatabase();
|
|
9120
|
+
if (!getTask(input.task_id, d))
|
|
9121
|
+
throw new TaskNotFoundError(input.task_id);
|
|
9122
|
+
const timestamp = input.started_at || now();
|
|
9123
|
+
const key = normalizeTransactionKey(input);
|
|
9124
|
+
const existing = findTaskRunByTransactionKey(key, input.task_id, d);
|
|
9125
|
+
const dryRun = !input.apply;
|
|
9126
|
+
if (existing) {
|
|
9127
|
+
return {
|
|
9128
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9129
|
+
local_only: true,
|
|
9130
|
+
dry_run: dryRun,
|
|
9131
|
+
processed_at: timestamp,
|
|
9132
|
+
action: "matched",
|
|
9133
|
+
key,
|
|
9134
|
+
run: summarizeTaskRun(existing),
|
|
9135
|
+
warnings: existing.status === "running" ? [] : [`matched ${existing.status} run`],
|
|
9136
|
+
commands: loopRunCommands(existing, key)
|
|
9137
|
+
};
|
|
9138
|
+
}
|
|
9139
|
+
if (dryRun) {
|
|
9140
|
+
return {
|
|
9141
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9142
|
+
local_only: true,
|
|
9143
|
+
dry_run: true,
|
|
9144
|
+
processed_at: timestamp,
|
|
9145
|
+
action: "preview",
|
|
9146
|
+
key,
|
|
9147
|
+
run: null,
|
|
9148
|
+
warnings: [],
|
|
9149
|
+
commands: loopRunCommands(null, key)
|
|
9150
|
+
};
|
|
9151
|
+
}
|
|
9152
|
+
const metadata = redactValue({
|
|
9153
|
+
...input.metadata || {},
|
|
9154
|
+
loop_transaction: {
|
|
9155
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9156
|
+
idempotency_key: key,
|
|
9157
|
+
loop_id: input.loop_id ?? null,
|
|
9158
|
+
loop_run_id: input.loop_run_id ?? null,
|
|
9159
|
+
first_seen_at: timestamp
|
|
9160
|
+
},
|
|
9161
|
+
idempotency_key: key
|
|
9162
|
+
});
|
|
9163
|
+
const created = d.transaction(() => {
|
|
9164
|
+
d.run(`INSERT OR IGNORE INTO task_run_transactions (
|
|
9165
|
+
id, task_id, run_id, key, loop_id, loop_run_id, metadata, created_at, updated_at
|
|
9166
|
+
) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?)`, [
|
|
9167
|
+
uuid(),
|
|
9168
|
+
input.task_id,
|
|
9169
|
+
key,
|
|
9170
|
+
input.loop_id ?? null,
|
|
9171
|
+
input.loop_run_id ?? null,
|
|
9172
|
+
JSON.stringify(metadata),
|
|
9173
|
+
timestamp,
|
|
9174
|
+
timestamp
|
|
9175
|
+
]);
|
|
9176
|
+
const transaction = getTaskRunTransactionByKey(key, input.task_id, d);
|
|
9177
|
+
if (!transaction)
|
|
9178
|
+
throw new Error(`Could not create run transaction for key: ${key}`);
|
|
9179
|
+
if (transaction.run_id) {
|
|
9180
|
+
const existingRun = getTaskRun(transaction.run_id, d);
|
|
9181
|
+
if (existingRun)
|
|
9182
|
+
return { run: existingRun, action: "matched" };
|
|
9183
|
+
}
|
|
9184
|
+
const run = startTaskRun({
|
|
9185
|
+
id: uuid(),
|
|
9186
|
+
task_id: input.task_id,
|
|
9187
|
+
agent_id: input.agent_id,
|
|
9188
|
+
title: input.title,
|
|
9189
|
+
summary: input.summary,
|
|
9190
|
+
metadata,
|
|
9191
|
+
claim: input.claim,
|
|
9192
|
+
started_at: timestamp
|
|
9193
|
+
}, d);
|
|
9194
|
+
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]);
|
|
9195
|
+
return { run, action: "created" };
|
|
9196
|
+
})();
|
|
9197
|
+
return {
|
|
9198
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9199
|
+
local_only: true,
|
|
9200
|
+
dry_run: false,
|
|
9201
|
+
processed_at: timestamp,
|
|
9202
|
+
action: created.action,
|
|
9203
|
+
key,
|
|
9204
|
+
run: summarizeTaskRun(created.run),
|
|
9205
|
+
warnings: [],
|
|
9206
|
+
commands: loopRunCommands(created.run, key)
|
|
9207
|
+
};
|
|
9208
|
+
}
|
|
8808
9209
|
function addTaskRunEvent(input, db) {
|
|
8809
9210
|
const d = db || getDatabase();
|
|
8810
9211
|
const runId = resolveTaskRunId(input.run_id, d);
|
|
@@ -8984,6 +9385,66 @@ function finishTaskRun(input, db) {
|
|
|
8984
9385
|
});
|
|
8985
9386
|
return updated;
|
|
8986
9387
|
}
|
|
9388
|
+
function finishTaskRunTransaction(input, db) {
|
|
9389
|
+
const d = db || getDatabase();
|
|
9390
|
+
const timestamp = input.completed_at || now();
|
|
9391
|
+
const status = input.status || "completed";
|
|
9392
|
+
const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
|
|
9393
|
+
const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
|
|
9394
|
+
if (!run) {
|
|
9395
|
+
throw new Error(input.run_id ? `Run not found: ${input.run_id}` : "runs finish requires a run id or --key");
|
|
9396
|
+
}
|
|
9397
|
+
if (input.task_id && run.task_id !== input.task_id) {
|
|
9398
|
+
throw new Error(`Run ${run.id} belongs to task ${run.task_id}, not ${input.task_id}`);
|
|
9399
|
+
}
|
|
9400
|
+
const resolvedKey = key || runKey(run) || run.id;
|
|
9401
|
+
const dryRun = input.apply === false;
|
|
9402
|
+
if (run.status !== "running") {
|
|
9403
|
+
const conflict = run.status !== status;
|
|
9404
|
+
return {
|
|
9405
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9406
|
+
local_only: true,
|
|
9407
|
+
dry_run: dryRun,
|
|
9408
|
+
processed_at: timestamp,
|
|
9409
|
+
action: conflict ? "conflict" : "matched",
|
|
9410
|
+
key: resolvedKey,
|
|
9411
|
+
run: summarizeTaskRun(run),
|
|
9412
|
+
warnings: conflict ? [`run is already ${run.status}; requested ${status}`] : [],
|
|
9413
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
9414
|
+
};
|
|
9415
|
+
}
|
|
9416
|
+
if (dryRun) {
|
|
9417
|
+
return {
|
|
9418
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9419
|
+
local_only: true,
|
|
9420
|
+
dry_run: true,
|
|
9421
|
+
processed_at: timestamp,
|
|
9422
|
+
action: "preview",
|
|
9423
|
+
key: resolvedKey,
|
|
9424
|
+
run: summarizeTaskRun({ ...run, status, summary: input.summary ?? run.summary, completed_at: timestamp, updated_at: timestamp }),
|
|
9425
|
+
warnings: [],
|
|
9426
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
9427
|
+
};
|
|
9428
|
+
}
|
|
9429
|
+
const finished = finishTaskRun({
|
|
9430
|
+
run_id: run.id,
|
|
9431
|
+
status,
|
|
9432
|
+
summary: input.summary,
|
|
9433
|
+
agent_id: input.agent_id,
|
|
9434
|
+
completed_at: timestamp
|
|
9435
|
+
}, d);
|
|
9436
|
+
return {
|
|
9437
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9438
|
+
local_only: true,
|
|
9439
|
+
dry_run: false,
|
|
9440
|
+
processed_at: timestamp,
|
|
9441
|
+
action: "finished",
|
|
9442
|
+
key: resolvedKey,
|
|
9443
|
+
run: summarizeTaskRun(finished),
|
|
9444
|
+
warnings: [],
|
|
9445
|
+
commands: loopRunCommands(finished, resolvedKey)
|
|
9446
|
+
};
|
|
9447
|
+
}
|
|
8987
9448
|
function listTaskRuns(taskId, db) {
|
|
8988
9449
|
const d = db || getDatabase();
|
|
8989
9450
|
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 +9462,7 @@ function getTaskRunLedger(runId, db) {
|
|
|
9001
9462
|
const files = d.query("SELECT * FROM task_files WHERE task_id = ? ORDER BY updated_at DESC, path").all(run.task_id);
|
|
9002
9463
|
return { run, events, commands, artifacts, files };
|
|
9003
9464
|
}
|
|
9465
|
+
var LOOP_RUN_TRANSACTION_SCHEMA_VERSION = "todos.loop_run_transaction.v1";
|
|
9004
9466
|
var init_task_runs = __esm(() => {
|
|
9005
9467
|
init_artifact_store();
|
|
9006
9468
|
init_event_hooks();
|
|
@@ -9389,6 +9851,7 @@ var init_calendar = __esm(() => {
|
|
|
9389
9851
|
var exports_tasks = {};
|
|
9390
9852
|
__export(exports_tasks, {
|
|
9391
9853
|
watchTask: () => watchTask,
|
|
9854
|
+
upsertTaskByFingerprint: () => upsertTaskByFingerprint,
|
|
9392
9855
|
updateTaskBoard: () => updateTaskBoard,
|
|
9393
9856
|
updateTask: () => updateTask,
|
|
9394
9857
|
unwatchTask: () => unwatchTask,
|
|
@@ -9432,6 +9895,7 @@ __export(exports_tasks, {
|
|
|
9432
9895
|
getTaskGraph: () => getTaskGraph,
|
|
9433
9896
|
getTaskDependents: () => getTaskDependents,
|
|
9434
9897
|
getTaskDependencies: () => getTaskDependencies,
|
|
9898
|
+
getTaskByFingerprint: () => getTaskByFingerprint,
|
|
9435
9899
|
getTaskBoard: () => getTaskBoard,
|
|
9436
9900
|
getTask: () => getTask,
|
|
9437
9901
|
getStatus: () => getStatus,
|
|
@@ -10825,6 +11289,39 @@ async function handleCreateTask(req, ctx, json2, taskToSummary2) {
|
|
|
10825
11289
|
return json2({ error: e instanceof Error ? e.message : "Failed to create task" }, 500);
|
|
10826
11290
|
}
|
|
10827
11291
|
}
|
|
11292
|
+
async function handleUpsertTask(req, ctx, json2, taskToSummary2) {
|
|
11293
|
+
try {
|
|
11294
|
+
const body = await req.json();
|
|
11295
|
+
if (typeof body["fingerprint"] !== "string" || body["fingerprint"].trim() === "") {
|
|
11296
|
+
return json2({ error: "Missing 'fingerprint'" }, 400);
|
|
11297
|
+
}
|
|
11298
|
+
if (typeof body["title"] !== "string" || body["title"].trim() === "") {
|
|
11299
|
+
return json2({ error: "Missing 'title'" }, 400);
|
|
11300
|
+
}
|
|
11301
|
+
const metadata = body["metadata"] && typeof body["metadata"] === "object" && !Array.isArray(body["metadata"]) ? { ...body["metadata"] } : {};
|
|
11302
|
+
for (const key of ["expectation_id", "expectation_fingerprint", "evidence_paths", "origin_loop_id", "origin_run_id", "expected", "observed", "acceptance"]) {
|
|
11303
|
+
if (body[key] !== undefined)
|
|
11304
|
+
metadata[key] = body[key];
|
|
11305
|
+
}
|
|
11306
|
+
const result = upsertTaskByFingerprint({
|
|
11307
|
+
fingerprint: body["fingerprint"],
|
|
11308
|
+
title: body["title"],
|
|
11309
|
+
description: typeof body["description"] === "string" ? body["description"] : undefined,
|
|
11310
|
+
status: body["status"],
|
|
11311
|
+
priority: body["priority"],
|
|
11312
|
+
project_id: typeof body["project_id"] === "string" ? body["project_id"] : undefined,
|
|
11313
|
+
task_list_id: typeof body["task_list_id"] === "string" ? body["task_list_id"] : undefined,
|
|
11314
|
+
assigned_to: typeof body["assigned_to"] === "string" ? body["assigned_to"] : undefined,
|
|
11315
|
+
working_dir: typeof body["working_dir"] === "string" ? body["working_dir"] : undefined,
|
|
11316
|
+
tags: Array.isArray(body["tags"]) ? body["tags"].filter((tag) => typeof tag === "string") : undefined,
|
|
11317
|
+
metadata
|
|
11318
|
+
});
|
|
11319
|
+
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 });
|
|
11320
|
+
return json2({ created: result.created, task: taskToSummary2(result.task) }, result.created ? 201 : 200);
|
|
11321
|
+
} catch (e) {
|
|
11322
|
+
return json2({ error: e instanceof Error ? e.message : "Failed to upsert task" }, 500);
|
|
11323
|
+
}
|
|
11324
|
+
}
|
|
10828
11325
|
function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
|
|
10829
11326
|
const format = url.searchParams.get("format") || "json";
|
|
10830
11327
|
const status = url.searchParams.get("status") || undefined;
|
|
@@ -32569,6 +33066,7 @@ var init_token_utils = __esm(() => {
|
|
|
32569
33066
|
"add_task_run_event",
|
|
32570
33067
|
"add_task_run_file",
|
|
32571
33068
|
"acknowledge_handoff",
|
|
33069
|
+
"begin_task_run_transaction",
|
|
32572
33070
|
"build_local_report",
|
|
32573
33071
|
"cancel_agent_run_dispatch",
|
|
32574
33072
|
"finish_task_run",
|
|
@@ -32616,6 +33114,7 @@ var init_token_utils = __esm(() => {
|
|
|
32616
33114
|
"list_local_snapshots",
|
|
32617
33115
|
"list_retrospectives",
|
|
32618
33116
|
"list_risks",
|
|
33117
|
+
"list_task_findings",
|
|
32619
33118
|
"list_task_runs",
|
|
32620
33119
|
"list_verification_providers",
|
|
32621
33120
|
"merge_duplicate_task",
|
|
@@ -32624,6 +33123,7 @@ var init_token_utils = __esm(() => {
|
|
|
32624
33123
|
"remove_review_routing_rule",
|
|
32625
33124
|
"restore_local_backup",
|
|
32626
33125
|
"retry_agent_run_dispatch",
|
|
33126
|
+
"resolve_missing_task_findings",
|
|
32627
33127
|
"resolve_mentions",
|
|
32628
33128
|
"run_next_agent_dispatch",
|
|
32629
33129
|
"search_knowledge_records",
|
|
@@ -32666,9 +33166,17 @@ var init_token_utils = __esm(() => {
|
|
|
32666
33166
|
"unlock_file",
|
|
32667
33167
|
"unwatch_task",
|
|
32668
33168
|
"update_comment",
|
|
33169
|
+
"upsert_task_finding",
|
|
32669
33170
|
"update_risk",
|
|
32670
33171
|
"watch_task"
|
|
32671
33172
|
],
|
|
33173
|
+
loops: [
|
|
33174
|
+
"begin_task_run_transaction",
|
|
33175
|
+
"finish_task_run",
|
|
33176
|
+
"list_task_findings",
|
|
33177
|
+
"resolve_missing_task_findings",
|
|
33178
|
+
"upsert_task_finding"
|
|
33179
|
+
],
|
|
32672
33180
|
agents: [
|
|
32673
33181
|
"auto_assign_task",
|
|
32674
33182
|
"delete_agent",
|
|
@@ -32750,7 +33258,7 @@ var init_token_utils = __esm(() => {
|
|
|
32750
33258
|
maintenance: ["extract_todos", "get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task", "watch_source_todos"]
|
|
32751
33259
|
};
|
|
32752
33260
|
MCP_PROFILE_GROUPS = {
|
|
32753
|
-
minimal: ["core"],
|
|
33261
|
+
minimal: ["core", "loops"],
|
|
32754
33262
|
core: ["core"],
|
|
32755
33263
|
standard: ["core", "tasks", "projects", "resources", "agents", "metadata"],
|
|
32756
33264
|
agent: ["core", "tasks", "projects", "resources"],
|
|
@@ -32830,6 +33338,61 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
32830
33338
|
}
|
|
32831
33339
|
});
|
|
32832
33340
|
}
|
|
33341
|
+
if (shouldRegisterTool("upsert_task")) {
|
|
33342
|
+
server.tool("upsert_task", "Create or update a task by stable metadata fingerprint. Metadata is shallow-merged on updates.", {
|
|
33343
|
+
fingerprint: exports_external.string().describe("Stable dedupe fingerprint stored as metadata.fingerprint"),
|
|
33344
|
+
title: exports_external.string().describe("Task title"),
|
|
33345
|
+
description: exports_external.string().optional().describe("Task description"),
|
|
33346
|
+
status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
|
|
33347
|
+
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
|
|
33348
|
+
project_id: exports_external.string().optional().describe("Project ID"),
|
|
33349
|
+
task_list_id: exports_external.string().optional().describe("Task list ID"),
|
|
33350
|
+
assigned_to: exports_external.string().optional().describe("Agent ID or name to assign to"),
|
|
33351
|
+
tags: exports_external.array(exports_external.string()).optional().describe("Tags for the task"),
|
|
33352
|
+
working_dir: exports_external.string().optional().describe("Working directory associated with the task"),
|
|
33353
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Metadata object to shallow-merge"),
|
|
33354
|
+
expectation_id: exports_external.string().optional(),
|
|
33355
|
+
expectation_fingerprint: exports_external.string().optional(),
|
|
33356
|
+
evidence_paths: exports_external.array(exports_external.string()).optional(),
|
|
33357
|
+
origin_loop_id: exports_external.string().optional(),
|
|
33358
|
+
origin_run_id: exports_external.string().optional(),
|
|
33359
|
+
expected: exports_external.unknown().optional(),
|
|
33360
|
+
observed: exports_external.unknown().optional(),
|
|
33361
|
+
acceptance: exports_external.unknown().optional()
|
|
33362
|
+
}, async (params) => {
|
|
33363
|
+
try {
|
|
33364
|
+
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;
|
|
33365
|
+
const mergedMetadata = { ...metadata ?? {} };
|
|
33366
|
+
if (expectation_id !== undefined)
|
|
33367
|
+
mergedMetadata["expectation_id"] = expectation_id;
|
|
33368
|
+
if (expectation_fingerprint !== undefined)
|
|
33369
|
+
mergedMetadata["expectation_fingerprint"] = expectation_fingerprint;
|
|
33370
|
+
if (evidence_paths !== undefined)
|
|
33371
|
+
mergedMetadata["evidence_paths"] = evidence_paths;
|
|
33372
|
+
if (origin_loop_id !== undefined)
|
|
33373
|
+
mergedMetadata["origin_loop_id"] = origin_loop_id;
|
|
33374
|
+
if (origin_run_id !== undefined)
|
|
33375
|
+
mergedMetadata["origin_run_id"] = origin_run_id;
|
|
33376
|
+
if (expected !== undefined)
|
|
33377
|
+
mergedMetadata["expected"] = expected;
|
|
33378
|
+
if (observed !== undefined)
|
|
33379
|
+
mergedMetadata["observed"] = observed;
|
|
33380
|
+
if (acceptance !== undefined)
|
|
33381
|
+
mergedMetadata["acceptance"] = acceptance;
|
|
33382
|
+
const resolved = { ...rest, metadata: mergedMetadata };
|
|
33383
|
+
if (assigned_to)
|
|
33384
|
+
resolved.assigned_to = resolveAssignee(assigned_to);
|
|
33385
|
+
if (project_id)
|
|
33386
|
+
resolved.project_id = resolveId(project_id, "projects");
|
|
33387
|
+
if (task_list_id)
|
|
33388
|
+
resolved.task_list_id = resolveId(task_list_id, "task_lists");
|
|
33389
|
+
const result = upsertTaskByFingerprint(resolved);
|
|
33390
|
+
return { content: [{ type: "text", text: compactJson({ created: result.created, task: JSON.parse(mutationTaskResponse(result.task)) }) }] };
|
|
33391
|
+
} catch (e) {
|
|
33392
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
33393
|
+
}
|
|
33394
|
+
});
|
|
33395
|
+
}
|
|
32833
33396
|
if (shouldRegisterTool("list_tasks")) {
|
|
32834
33397
|
server.tool("list_tasks", "List tasks with optional filters. Pass empty arrays for multi-value filters (e.g. status=[] shows all).", {
|
|
32835
33398
|
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 +33404,8 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
32841
33404
|
created_after: exports_external.string().optional().describe("ISO date \u2014 tasks created after this date"),
|
|
32842
33405
|
created_before: exports_external.string().optional().describe("ISO date \u2014 tasks created before this date"),
|
|
32843
33406
|
limit: exports_external.number().optional().describe("Max results (default: 50, max 500)"),
|
|
32844
|
-
offset: exports_external.number().optional().describe("Pagination offset")
|
|
33407
|
+
offset: exports_external.number().optional().describe("Pagination offset"),
|
|
33408
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Exact top-level metadata filters")
|
|
32845
33409
|
}, async (params) => {
|
|
32846
33410
|
try {
|
|
32847
33411
|
const resolved = { ...params };
|
|
@@ -36382,7 +36946,7 @@ function countMovedStandaloneVerifications(db, duplicateId) {
|
|
|
36382
36946
|
const rows = db.query("SELECT command FROM task_verifications WHERE task_id = ?").all(duplicateId);
|
|
36383
36947
|
return rows.filter((row) => !generatedCommands.has(row.command)).length;
|
|
36384
36948
|
}
|
|
36385
|
-
function
|
|
36949
|
+
function mergeTaskMetadata2(primary, duplicate, input, mergedAt) {
|
|
36386
36950
|
const mergedDuplicates = Array.isArray(primary.metadata["merged_duplicates"]) ? [...primary.metadata["merged_duplicates"]] : [];
|
|
36387
36951
|
mergedDuplicates.push({
|
|
36388
36952
|
id: duplicate.id,
|
|
@@ -36443,7 +37007,7 @@ function mergeDuplicateTask(input, db) {
|
|
|
36443
37007
|
updateTask(primary.id, {
|
|
36444
37008
|
version: primary.version,
|
|
36445
37009
|
tags: mergedTags,
|
|
36446
|
-
metadata:
|
|
37010
|
+
metadata: mergeTaskMetadata2(primary, duplicate, input, mergedAt),
|
|
36447
37011
|
description: mergeTaskDescription(primary, duplicate) ?? undefined
|
|
36448
37012
|
}, d);
|
|
36449
37013
|
moved.comments = updateRows(d, "task_comments", "task_id", duplicate.id, primary.id);
|
|
@@ -66620,6 +67184,356 @@ var init_task_meta_tools = __esm(() => {
|
|
|
66620
67184
|
init_zod();
|
|
66621
67185
|
});
|
|
66622
67186
|
|
|
67187
|
+
// src/db/findings.ts
|
|
67188
|
+
function parseObject2(value) {
|
|
67189
|
+
if (!value)
|
|
67190
|
+
return {};
|
|
67191
|
+
try {
|
|
67192
|
+
const parsed = JSON.parse(value);
|
|
67193
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
67194
|
+
} catch {
|
|
67195
|
+
return {};
|
|
67196
|
+
}
|
|
67197
|
+
}
|
|
67198
|
+
function normalizeKey(value) {
|
|
67199
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
67200
|
+
}
|
|
67201
|
+
function normalizeFingerprint(value) {
|
|
67202
|
+
const normalized = normalizeKey(value);
|
|
67203
|
+
if (!normalized)
|
|
67204
|
+
throw new Error("finding fingerprint is required");
|
|
67205
|
+
return normalized.slice(0, 240);
|
|
67206
|
+
}
|
|
67207
|
+
function normalizeSeverity(value) {
|
|
67208
|
+
const normalized = normalizeKey(value || "medium");
|
|
67209
|
+
if (SEVERITIES.has(normalized))
|
|
67210
|
+
return normalized;
|
|
67211
|
+
if (/^(p0|blocker|urgent|highest)$/.test(normalized))
|
|
67212
|
+
return "critical";
|
|
67213
|
+
if (/^(p1|major)$/.test(normalized))
|
|
67214
|
+
return "high";
|
|
67215
|
+
if (/^(p3|minor|info)$/.test(normalized))
|
|
67216
|
+
return "low";
|
|
67217
|
+
return "medium";
|
|
67218
|
+
}
|
|
67219
|
+
function normalizeStatus(value) {
|
|
67220
|
+
const normalized = normalizeKey(value || "open");
|
|
67221
|
+
if (STATUSES.has(normalized))
|
|
67222
|
+
return normalized;
|
|
67223
|
+
if (normalized === "closed" || normalized === "fixed")
|
|
67224
|
+
return "resolved";
|
|
67225
|
+
return "open";
|
|
67226
|
+
}
|
|
67227
|
+
function normalizeResolutionStatus(value) {
|
|
67228
|
+
const status = normalizeStatus(value || "resolved");
|
|
67229
|
+
if (status === "open")
|
|
67230
|
+
throw new Error("resolve-missing status must be resolved or ignored");
|
|
67231
|
+
return status;
|
|
67232
|
+
}
|
|
67233
|
+
function redactOptional(value, max = 2000) {
|
|
67234
|
+
if (!value)
|
|
67235
|
+
return null;
|
|
67236
|
+
const redacted = redactEvidenceText(value).trim();
|
|
67237
|
+
if (!redacted)
|
|
67238
|
+
return null;
|
|
67239
|
+
return redacted.length > max ? `${redacted.slice(0, max - 3)}...` : redacted;
|
|
67240
|
+
}
|
|
67241
|
+
function rowToFinding(row) {
|
|
67242
|
+
return {
|
|
67243
|
+
schema_version: TASK_FINDING_SCHEMA_VERSION,
|
|
67244
|
+
...row,
|
|
67245
|
+
severity: normalizeSeverity(row.severity),
|
|
67246
|
+
status: normalizeStatus(row.status),
|
|
67247
|
+
metadata: parseObject2(row.metadata)
|
|
67248
|
+
};
|
|
67249
|
+
}
|
|
67250
|
+
function compactFinding(finding2) {
|
|
67251
|
+
return {
|
|
67252
|
+
schema_version: TASK_FINDING_SCHEMA_VERSION,
|
|
67253
|
+
id: finding2.id,
|
|
67254
|
+
task_id: finding2.task_id,
|
|
67255
|
+
run_id: finding2.run_id,
|
|
67256
|
+
fingerprint: finding2.fingerprint,
|
|
67257
|
+
title: finding2.title,
|
|
67258
|
+
severity: finding2.severity,
|
|
67259
|
+
status: finding2.status,
|
|
67260
|
+
source: finding2.source,
|
|
67261
|
+
summary: finding2.summary,
|
|
67262
|
+
artifact_path: finding2.artifact_path,
|
|
67263
|
+
first_seen_at: finding2.first_seen_at,
|
|
67264
|
+
last_seen_at: finding2.last_seen_at,
|
|
67265
|
+
resolved_at: finding2.resolved_at,
|
|
67266
|
+
metadata_keys: Object.keys(finding2.metadata).sort()
|
|
67267
|
+
};
|
|
67268
|
+
}
|
|
67269
|
+
function previewFinding(existing, next, timestamp3) {
|
|
67270
|
+
return {
|
|
67271
|
+
...existing,
|
|
67272
|
+
run_id: next.run_id,
|
|
67273
|
+
title: next.title,
|
|
67274
|
+
severity: next.severity,
|
|
67275
|
+
status: next.status,
|
|
67276
|
+
source: next.source,
|
|
67277
|
+
summary: next.summary,
|
|
67278
|
+
artifact_path: next.artifact_path,
|
|
67279
|
+
metadata: next.metadata,
|
|
67280
|
+
last_seen_at: timestamp3,
|
|
67281
|
+
resolved_at: next.status === "open" ? null : existing.resolved_at || timestamp3,
|
|
67282
|
+
updated_at: timestamp3
|
|
67283
|
+
};
|
|
67284
|
+
}
|
|
67285
|
+
function upsertAction(existing, next) {
|
|
67286
|
+
if (sameFinding(existing, next))
|
|
67287
|
+
return "matched";
|
|
67288
|
+
return existing.status !== "open" && next.status === "open" ? "reopened" : "updated";
|
|
67289
|
+
}
|
|
67290
|
+
function resolveRunForTask(runId, taskId, db) {
|
|
67291
|
+
if (!runId)
|
|
67292
|
+
return null;
|
|
67293
|
+
const resolved = resolveTaskRunId(runId, db);
|
|
67294
|
+
const run = getTaskRun(resolved, db);
|
|
67295
|
+
if (!run)
|
|
67296
|
+
throw new Error(`Run not found: ${runId}`);
|
|
67297
|
+
if (run.task_id !== taskId)
|
|
67298
|
+
throw new Error(`Run ${resolved} belongs to task ${run.task_id}, not ${taskId}`);
|
|
67299
|
+
return resolved;
|
|
67300
|
+
}
|
|
67301
|
+
function getFindingByFingerprint(taskId, fingerprint2, db) {
|
|
67302
|
+
const row = db.query("SELECT * FROM task_findings WHERE task_id = ? AND fingerprint = ?").get(taskId, fingerprint2);
|
|
67303
|
+
return row ? rowToFinding(row) : null;
|
|
67304
|
+
}
|
|
67305
|
+
function assertTask(taskId, db) {
|
|
67306
|
+
if (!getTask(taskId, db))
|
|
67307
|
+
throw new TaskNotFoundError(taskId);
|
|
67308
|
+
}
|
|
67309
|
+
function nextFinding(input, db) {
|
|
67310
|
+
const fingerprint2 = normalizeFingerprint(input.fingerprint);
|
|
67311
|
+
const title = redactOptional(input.title, 300);
|
|
67312
|
+
if (!title)
|
|
67313
|
+
throw new Error("finding title is required");
|
|
67314
|
+
return {
|
|
67315
|
+
fingerprint: fingerprint2,
|
|
67316
|
+
run_id: resolveRunForTask(input.run_id, input.task_id, db),
|
|
67317
|
+
title,
|
|
67318
|
+
severity: normalizeSeverity(input.severity),
|
|
67319
|
+
status: normalizeStatus(input.status),
|
|
67320
|
+
source: redactOptional(input.source, 120),
|
|
67321
|
+
summary: redactOptional(input.summary, 2000),
|
|
67322
|
+
artifact_path: redactOptional(input.artifact_path, 1000),
|
|
67323
|
+
metadata: redactValue(input.metadata || {})
|
|
67324
|
+
};
|
|
67325
|
+
}
|
|
67326
|
+
function sameFinding(left, right) {
|
|
67327
|
+
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);
|
|
67328
|
+
}
|
|
67329
|
+
function upsertTaskFinding(input, db) {
|
|
67330
|
+
const d = db || getDatabase();
|
|
67331
|
+
assertTask(input.task_id, d);
|
|
67332
|
+
const timestamp3 = input.observed_at || now();
|
|
67333
|
+
const warnings = [];
|
|
67334
|
+
const next = nextFinding(input, d);
|
|
67335
|
+
const existing = getFindingByFingerprint(input.task_id, next.fingerprint, d);
|
|
67336
|
+
const dryRun = !input.apply;
|
|
67337
|
+
if (dryRun) {
|
|
67338
|
+
const action2 = existing ? upsertAction(existing, next) : "preview";
|
|
67339
|
+
return {
|
|
67340
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
67341
|
+
local_only: true,
|
|
67342
|
+
dry_run: true,
|
|
67343
|
+
processed_at: timestamp3,
|
|
67344
|
+
action: action2,
|
|
67345
|
+
fingerprint: next.fingerprint,
|
|
67346
|
+
finding: existing ? compactFinding(previewFinding(existing, next, timestamp3)) : null,
|
|
67347
|
+
warnings
|
|
67348
|
+
};
|
|
67349
|
+
}
|
|
67350
|
+
if (!existing) {
|
|
67351
|
+
const id = uuid();
|
|
67352
|
+
d.run(`INSERT INTO task_findings (
|
|
67353
|
+
id, task_id, run_id, fingerprint, title, severity, status, source, summary, artifact_path,
|
|
67354
|
+
metadata, first_seen_at, last_seen_at, resolved_at, created_at, updated_at
|
|
67355
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
67356
|
+
id,
|
|
67357
|
+
input.task_id,
|
|
67358
|
+
next.run_id,
|
|
67359
|
+
next.fingerprint,
|
|
67360
|
+
next.title,
|
|
67361
|
+
next.severity,
|
|
67362
|
+
next.status,
|
|
67363
|
+
next.source,
|
|
67364
|
+
next.summary,
|
|
67365
|
+
next.artifact_path,
|
|
67366
|
+
JSON.stringify(next.metadata),
|
|
67367
|
+
timestamp3,
|
|
67368
|
+
timestamp3,
|
|
67369
|
+
next.status === "open" ? null : timestamp3,
|
|
67370
|
+
timestamp3,
|
|
67371
|
+
timestamp3
|
|
67372
|
+
]);
|
|
67373
|
+
return {
|
|
67374
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
67375
|
+
local_only: true,
|
|
67376
|
+
dry_run: false,
|
|
67377
|
+
processed_at: timestamp3,
|
|
67378
|
+
action: "created",
|
|
67379
|
+
fingerprint: next.fingerprint,
|
|
67380
|
+
finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
|
|
67381
|
+
warnings
|
|
67382
|
+
};
|
|
67383
|
+
}
|
|
67384
|
+
if (sameFinding(existing, next)) {
|
|
67385
|
+
return {
|
|
67386
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
67387
|
+
local_only: true,
|
|
67388
|
+
dry_run: false,
|
|
67389
|
+
processed_at: timestamp3,
|
|
67390
|
+
action: "matched",
|
|
67391
|
+
fingerprint: next.fingerprint,
|
|
67392
|
+
finding: compactFinding(existing),
|
|
67393
|
+
warnings
|
|
67394
|
+
};
|
|
67395
|
+
}
|
|
67396
|
+
const action = upsertAction(existing, next);
|
|
67397
|
+
d.run(`UPDATE task_findings SET
|
|
67398
|
+
run_id = ?, title = ?, severity = ?, status = ?, source = ?, summary = ?, artifact_path = ?,
|
|
67399
|
+
metadata = ?, last_seen_at = ?, resolved_at = ?, updated_at = ?
|
|
67400
|
+
WHERE id = ?`, [
|
|
67401
|
+
next.run_id,
|
|
67402
|
+
next.title,
|
|
67403
|
+
next.severity,
|
|
67404
|
+
next.status,
|
|
67405
|
+
next.source,
|
|
67406
|
+
next.summary,
|
|
67407
|
+
next.artifact_path,
|
|
67408
|
+
JSON.stringify(next.metadata),
|
|
67409
|
+
timestamp3,
|
|
67410
|
+
next.status === "open" ? null : existing.resolved_at || timestamp3,
|
|
67411
|
+
timestamp3,
|
|
67412
|
+
existing.id
|
|
67413
|
+
]);
|
|
67414
|
+
return {
|
|
67415
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
67416
|
+
local_only: true,
|
|
67417
|
+
dry_run: false,
|
|
67418
|
+
processed_at: timestamp3,
|
|
67419
|
+
action,
|
|
67420
|
+
fingerprint: next.fingerprint,
|
|
67421
|
+
finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
|
|
67422
|
+
warnings
|
|
67423
|
+
};
|
|
67424
|
+
}
|
|
67425
|
+
function listTaskFindings(filter = {}, db) {
|
|
67426
|
+
const d = db || getDatabase();
|
|
67427
|
+
const conditions = ["1=1"];
|
|
67428
|
+
const params = [];
|
|
67429
|
+
if (filter.task_id) {
|
|
67430
|
+
conditions.push("task_id = ?");
|
|
67431
|
+
params.push(filter.task_id);
|
|
67432
|
+
}
|
|
67433
|
+
if (filter.run_id) {
|
|
67434
|
+
conditions.push("run_id = ?");
|
|
67435
|
+
params.push(resolveTaskRunId(filter.run_id, d));
|
|
67436
|
+
}
|
|
67437
|
+
if (filter.status) {
|
|
67438
|
+
conditions.push("status = ?");
|
|
67439
|
+
params.push(normalizeStatus(filter.status));
|
|
67440
|
+
}
|
|
67441
|
+
if (filter.source) {
|
|
67442
|
+
conditions.push("source = ?");
|
|
67443
|
+
params.push(redactOptional(filter.source, 120));
|
|
67444
|
+
}
|
|
67445
|
+
const limit = Math.min(Math.max(Math.floor(filter.limit ?? 50), 1), 500);
|
|
67446
|
+
const rows = d.query(`SELECT * FROM task_findings WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC, created_at DESC LIMIT ?`).all(...[...params, limit]);
|
|
67447
|
+
return rows.map(rowToFinding);
|
|
67448
|
+
}
|
|
67449
|
+
function listCompactTaskFindings(filter = {}, db) {
|
|
67450
|
+
return listTaskFindings(filter, db).map(compactFinding);
|
|
67451
|
+
}
|
|
67452
|
+
function resolveMissingTaskFindings(input, db) {
|
|
67453
|
+
const d = db || getDatabase();
|
|
67454
|
+
assertTask(input.task_id, d);
|
|
67455
|
+
const timestamp3 = input.resolved_at || now();
|
|
67456
|
+
const status = normalizeResolutionStatus(input.status);
|
|
67457
|
+
const runId = resolveRunForTask(input.run_id, input.task_id, d);
|
|
67458
|
+
const present = new Set(input.fingerprints.map(normalizeFingerprint));
|
|
67459
|
+
const warnings = [];
|
|
67460
|
+
const conditions = ["task_id = ?", "status = 'open'"];
|
|
67461
|
+
const params = [input.task_id];
|
|
67462
|
+
if (input.source) {
|
|
67463
|
+
conditions.push("source = ?");
|
|
67464
|
+
params.push(redactOptional(input.source, 120));
|
|
67465
|
+
}
|
|
67466
|
+
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));
|
|
67467
|
+
const limit = Math.min(Math.max(Math.floor(input.limit ?? 50), 1), 200);
|
|
67468
|
+
const display = candidates.slice(0, limit);
|
|
67469
|
+
const omittedCount = Math.max(0, candidates.length - display.length);
|
|
67470
|
+
if (!input.apply) {
|
|
67471
|
+
return {
|
|
67472
|
+
schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
67473
|
+
local_only: true,
|
|
67474
|
+
dry_run: true,
|
|
67475
|
+
processed_at: timestamp3,
|
|
67476
|
+
action: candidates.length > 0 ? "preview" : "noop",
|
|
67477
|
+
task_id: input.task_id,
|
|
67478
|
+
source: input.source ? redactOptional(input.source, 120) : null,
|
|
67479
|
+
run_id: runId,
|
|
67480
|
+
present_fingerprint_count: present.size,
|
|
67481
|
+
candidate_count: candidates.length,
|
|
67482
|
+
changed_count: 0,
|
|
67483
|
+
omitted_count: omittedCount,
|
|
67484
|
+
findings: display.map(compactFinding),
|
|
67485
|
+
warnings
|
|
67486
|
+
};
|
|
67487
|
+
}
|
|
67488
|
+
const metadataPatch = redactValue({
|
|
67489
|
+
resolved_by: {
|
|
67490
|
+
agent_id: input.agent_id ?? null,
|
|
67491
|
+
run_id: runId,
|
|
67492
|
+
reason: input.reason ? redactEvidenceText(input.reason) : "missing from latest loop finding set"
|
|
67493
|
+
}
|
|
67494
|
+
});
|
|
67495
|
+
const tx = d.transaction(() => {
|
|
67496
|
+
for (const finding2 of candidates) {
|
|
67497
|
+
d.run("UPDATE task_findings SET status = ?, resolved_at = ?, metadata = ?, updated_at = ? WHERE id = ? AND status = 'open'", [
|
|
67498
|
+
status,
|
|
67499
|
+
timestamp3,
|
|
67500
|
+
JSON.stringify({ ...finding2.metadata, ...metadataPatch }),
|
|
67501
|
+
timestamp3,
|
|
67502
|
+
finding2.id
|
|
67503
|
+
]);
|
|
67504
|
+
}
|
|
67505
|
+
});
|
|
67506
|
+
tx();
|
|
67507
|
+
const updated = candidates.map((finding2) => d.query("SELECT * FROM task_findings WHERE id = ?").get(finding2.id)).filter((row) => Boolean(row)).map(rowToFinding);
|
|
67508
|
+
const visibleUpdated = updated.slice(0, limit);
|
|
67509
|
+
return {
|
|
67510
|
+
schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
67511
|
+
local_only: true,
|
|
67512
|
+
dry_run: false,
|
|
67513
|
+
processed_at: timestamp3,
|
|
67514
|
+
action: updated.length > 0 ? status : "noop",
|
|
67515
|
+
task_id: input.task_id,
|
|
67516
|
+
source: input.source ? redactOptional(input.source, 120) : null,
|
|
67517
|
+
run_id: runId,
|
|
67518
|
+
present_fingerprint_count: present.size,
|
|
67519
|
+
candidate_count: candidates.length,
|
|
67520
|
+
changed_count: updated.length,
|
|
67521
|
+
omitted_count: omittedCount,
|
|
67522
|
+
findings: visibleUpdated.map(compactFinding),
|
|
67523
|
+
warnings
|
|
67524
|
+
};
|
|
67525
|
+
}
|
|
67526
|
+
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;
|
|
67527
|
+
var init_findings = __esm(() => {
|
|
67528
|
+
init_redaction();
|
|
67529
|
+
init_types();
|
|
67530
|
+
init_database();
|
|
67531
|
+
init_tasks();
|
|
67532
|
+
init_task_runs();
|
|
67533
|
+
SEVERITIES = new Set(["low", "medium", "high", "critical"]);
|
|
67534
|
+
STATUSES = new Set(["open", "resolved", "ignored"]);
|
|
67535
|
+
});
|
|
67536
|
+
|
|
66623
67537
|
// src/lib/agent-run-dispatcher.ts
|
|
66624
67538
|
function dispatcherFromRun(run) {
|
|
66625
67539
|
const value = run.metadata["agent_run_dispatcher"];
|
|
@@ -67280,7 +68194,7 @@ function parseArray2(value) {
|
|
|
67280
68194
|
return [];
|
|
67281
68195
|
}
|
|
67282
68196
|
}
|
|
67283
|
-
function
|
|
68197
|
+
function parseObject3(value) {
|
|
67284
68198
|
try {
|
|
67285
68199
|
const parsed = JSON.parse(value || "{}");
|
|
67286
68200
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
@@ -67321,7 +68235,7 @@ function rowToKnowledgeRecord(row) {
|
|
|
67321
68235
|
agent_id: row.agent_id,
|
|
67322
68236
|
snapshot_id: row.snapshot_id,
|
|
67323
68237
|
tags: parseArray2(row.tags),
|
|
67324
|
-
metadata: redactValue(
|
|
68238
|
+
metadata: redactValue(parseObject3(row.metadata)),
|
|
67325
68239
|
created_at: row.created_at,
|
|
67326
68240
|
updated_at: row.updated_at
|
|
67327
68241
|
};
|
|
@@ -67540,7 +68454,7 @@ function parseArray3(value) {
|
|
|
67540
68454
|
return [];
|
|
67541
68455
|
}
|
|
67542
68456
|
}
|
|
67543
|
-
function
|
|
68457
|
+
function parseObject4(value) {
|
|
67544
68458
|
try {
|
|
67545
68459
|
const parsed = JSON.parse(value || "{}");
|
|
67546
68460
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
@@ -67589,7 +68503,7 @@ function rowToRisk(row) {
|
|
|
67589
68503
|
plan_id: row.plan_id,
|
|
67590
68504
|
task_id: row.task_id,
|
|
67591
68505
|
tags: parseArray3(row.tags),
|
|
67592
|
-
metadata: redactValue(
|
|
68506
|
+
metadata: redactValue(parseObject4(row.metadata)),
|
|
67593
68507
|
created_at: row.created_at,
|
|
67594
68508
|
updated_at: row.updated_at,
|
|
67595
68509
|
closed_at: row.closed_at
|
|
@@ -73407,6 +74321,38 @@ ${lines.join(`
|
|
|
73407
74321
|
}
|
|
73408
74322
|
});
|
|
73409
74323
|
}
|
|
74324
|
+
if (shouldRegisterTool("begin_task_run_transaction")) {
|
|
74325
|
+
server.tool("begin_task_run_transaction", "Preview or apply an idempotent local loop run transaction keyed by a stable loop/run id.", {
|
|
74326
|
+
task_id: exports_external.string().describe("Task ID"),
|
|
74327
|
+
key: exports_external.string().optional().describe("Stable idempotency key"),
|
|
74328
|
+
loop_id: exports_external.string().optional().describe("Loop identifier; used as key fallback"),
|
|
74329
|
+
loop_run_id: exports_external.string().optional().describe("Loop run identifier; used as key fallback"),
|
|
74330
|
+
agent_id: exports_external.string().optional().describe("Agent starting the run"),
|
|
74331
|
+
title: exports_external.string().optional().describe("Run title"),
|
|
74332
|
+
summary: exports_external.string().optional().describe("Run summary"),
|
|
74333
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata"),
|
|
74334
|
+
claim: exports_external.boolean().optional().describe("Claim/start the task before recording the run"),
|
|
74335
|
+
apply: exports_external.boolean().optional().describe("Apply the transaction; false or omitted is dry-run")
|
|
74336
|
+
}, async ({ task_id, key, loop_id, loop_run_id, agent_id, title, summary, metadata, claim, apply }) => {
|
|
74337
|
+
try {
|
|
74338
|
+
const result = beginTaskRunTransaction({
|
|
74339
|
+
task_id: resolveId(task_id),
|
|
74340
|
+
key,
|
|
74341
|
+
loop_id,
|
|
74342
|
+
loop_run_id,
|
|
74343
|
+
agent_id,
|
|
74344
|
+
title,
|
|
74345
|
+
summary,
|
|
74346
|
+
metadata,
|
|
74347
|
+
claim,
|
|
74348
|
+
apply
|
|
74349
|
+
});
|
|
74350
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
74351
|
+
} catch (e) {
|
|
74352
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
74353
|
+
}
|
|
74354
|
+
});
|
|
74355
|
+
}
|
|
73410
74356
|
if (shouldRegisterTool("list_task_runs")) {
|
|
73411
74357
|
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
74358
|
try {
|
|
@@ -73504,15 +74450,117 @@ ${lines.join(`
|
|
|
73504
74450
|
});
|
|
73505
74451
|
}
|
|
73506
74452
|
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
|
-
|
|
74453
|
+
server.tool("finish_task_run", "Finish a local run ledger entry as completed, failed, or cancelled. Supports idempotent lookup by key.", {
|
|
74454
|
+
run_id: exports_external.string().optional().describe("Run ID or prefix"),
|
|
74455
|
+
key: exports_external.string().optional().describe("Idempotency key when run_id is omitted"),
|
|
74456
|
+
task_id: exports_external.string().optional().describe("Task scope for key lookup"),
|
|
74457
|
+
status: exports_external.enum(["completed", "failed", "cancelled"]).optional().describe("Final run status"),
|
|
73510
74458
|
summary: exports_external.string().optional().describe("Final summary"),
|
|
73511
|
-
agent_id: exports_external.string().optional().describe("Agent finishing the run")
|
|
73512
|
-
|
|
74459
|
+
agent_id: exports_external.string().optional().describe("Agent finishing the run"),
|
|
74460
|
+
apply: exports_external.boolean().optional().describe("Apply the transaction; false previews without mutating")
|
|
74461
|
+
}, async ({ run_id, key, task_id, status, summary, agent_id, apply }) => {
|
|
73513
74462
|
try {
|
|
73514
|
-
|
|
73515
|
-
|
|
74463
|
+
if (run_id && !key && apply === undefined) {
|
|
74464
|
+
const result2 = finishTaskRunTransaction({ run_id, status: status || "completed", summary, agent_id, apply: true });
|
|
74465
|
+
const run = result2.run ? getTaskRunLedger(result2.run.id).run : null;
|
|
74466
|
+
return { content: [{ type: "text", text: JSON.stringify(run, null, 2) }] };
|
|
74467
|
+
}
|
|
74468
|
+
const result = finishTaskRunTransaction({
|
|
74469
|
+
run_id,
|
|
74470
|
+
key,
|
|
74471
|
+
task_id: task_id ? resolveId(task_id) : undefined,
|
|
74472
|
+
status: status || "completed",
|
|
74473
|
+
summary,
|
|
74474
|
+
agent_id,
|
|
74475
|
+
apply: apply !== false
|
|
74476
|
+
});
|
|
74477
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
74478
|
+
} catch (e) {
|
|
74479
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
74480
|
+
}
|
|
74481
|
+
});
|
|
74482
|
+
}
|
|
74483
|
+
if (shouldRegisterTool("upsert_task_finding")) {
|
|
74484
|
+
server.tool("upsert_task_finding", "Preview or apply an idempotent local finding upsert scoped by task and fingerprint.", {
|
|
74485
|
+
task_id: exports_external.string().describe("Task ID"),
|
|
74486
|
+
fingerprint: exports_external.string().describe("Stable finding fingerprint"),
|
|
74487
|
+
title: exports_external.string().describe("Finding title"),
|
|
74488
|
+
severity: exports_external.enum(["low", "medium", "high", "critical"]).optional().describe("Finding severity"),
|
|
74489
|
+
status: exports_external.enum(["open", "resolved", "ignored"]).optional().describe("Finding status"),
|
|
74490
|
+
source: exports_external.string().optional().describe("Loop/tool source name"),
|
|
74491
|
+
summary: exports_external.string().optional().describe("Bounded finding summary"),
|
|
74492
|
+
artifact_path: exports_external.string().optional().describe("Local artifact path/reference; content is not read"),
|
|
74493
|
+
run_id: exports_external.string().optional().describe("Run ledger ID or prefix"),
|
|
74494
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata"),
|
|
74495
|
+
apply: exports_external.boolean().optional().describe("Apply the upsert; false or omitted is dry-run")
|
|
74496
|
+
}, async ({ task_id, fingerprint: fingerprint3, title, severity, status, source: source3, summary, artifact_path, run_id, metadata, apply }) => {
|
|
74497
|
+
try {
|
|
74498
|
+
const result = upsertTaskFinding({
|
|
74499
|
+
task_id: resolveId(task_id),
|
|
74500
|
+
fingerprint: fingerprint3,
|
|
74501
|
+
title,
|
|
74502
|
+
severity,
|
|
74503
|
+
status,
|
|
74504
|
+
source: source3,
|
|
74505
|
+
summary,
|
|
74506
|
+
artifact_path,
|
|
74507
|
+
run_id,
|
|
74508
|
+
metadata,
|
|
74509
|
+
apply
|
|
74510
|
+
});
|
|
74511
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
74512
|
+
} catch (e) {
|
|
74513
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
74514
|
+
}
|
|
74515
|
+
});
|
|
74516
|
+
}
|
|
74517
|
+
if (shouldRegisterTool("list_task_findings")) {
|
|
74518
|
+
server.tool("list_task_findings", "List compact local findings with bounded output.", {
|
|
74519
|
+
task_id: exports_external.string().optional().describe("Filter by task"),
|
|
74520
|
+
run_id: exports_external.string().optional().describe("Filter by run"),
|
|
74521
|
+
status: exports_external.enum(["open", "resolved", "ignored"]).optional().describe("Filter by status"),
|
|
74522
|
+
source: exports_external.string().optional().describe("Filter by source"),
|
|
74523
|
+
limit: exports_external.number().optional().describe("Maximum findings to return")
|
|
74524
|
+
}, async ({ task_id, run_id, status, source: source3, limit }) => {
|
|
74525
|
+
try {
|
|
74526
|
+
const findings = listCompactTaskFindings({
|
|
74527
|
+
task_id: task_id ? resolveId(task_id) : undefined,
|
|
74528
|
+
run_id,
|
|
74529
|
+
status,
|
|
74530
|
+
source: source3,
|
|
74531
|
+
limit
|
|
74532
|
+
});
|
|
74533
|
+
return { content: [{ type: "text", text: JSON.stringify(findings) }] };
|
|
74534
|
+
} catch (e) {
|
|
74535
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
74536
|
+
}
|
|
74537
|
+
});
|
|
74538
|
+
}
|
|
74539
|
+
if (shouldRegisterTool("resolve_missing_task_findings")) {
|
|
74540
|
+
server.tool("resolve_missing_task_findings", "Resolve open findings absent from the latest loop finding set.", {
|
|
74541
|
+
task_id: exports_external.string().describe("Task ID"),
|
|
74542
|
+
fingerprints: exports_external.array(exports_external.string()).optional().describe("Fingerprints still present in the latest run"),
|
|
74543
|
+
source: exports_external.string().optional().describe("Only resolve findings from this source"),
|
|
74544
|
+
run_id: exports_external.string().optional().describe("Run ledger ID or prefix for audit metadata"),
|
|
74545
|
+
status: exports_external.enum(["resolved", "ignored"]).optional().describe("Resolution status"),
|
|
74546
|
+
agent_id: exports_external.string().optional().describe("Agent resolving findings"),
|
|
74547
|
+
reason: exports_external.string().optional().describe("Resolution reason"),
|
|
74548
|
+
limit: exports_external.number().optional().describe("Maximum findings returned"),
|
|
74549
|
+
apply: exports_external.boolean().optional().describe("Apply resolution; false or omitted is dry-run")
|
|
74550
|
+
}, async ({ task_id, fingerprints, source: source3, run_id, status, agent_id, reason, limit, apply }) => {
|
|
74551
|
+
try {
|
|
74552
|
+
const result = resolveMissingTaskFindings({
|
|
74553
|
+
task_id: resolveId(task_id),
|
|
74554
|
+
fingerprints: fingerprints || [],
|
|
74555
|
+
source: source3,
|
|
74556
|
+
run_id,
|
|
74557
|
+
status,
|
|
74558
|
+
agent_id,
|
|
74559
|
+
reason,
|
|
74560
|
+
limit,
|
|
74561
|
+
apply
|
|
74562
|
+
});
|
|
74563
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
73516
74564
|
} catch (e) {
|
|
73517
74565
|
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
73518
74566
|
}
|
|
@@ -73848,6 +74896,7 @@ var init_task_resources = __esm(() => {
|
|
|
73848
74896
|
init_agents();
|
|
73849
74897
|
init_task_commits();
|
|
73850
74898
|
init_task_runs();
|
|
74899
|
+
init_findings();
|
|
73851
74900
|
init_agent_run_dispatcher();
|
|
73852
74901
|
init_verification_providers();
|
|
73853
74902
|
init_release_notes();
|
|
@@ -78423,8 +79472,10 @@ function taskToSummary(task2, fields) {
|
|
|
78423
79472
|
task_list_id: task2.task_list_id,
|
|
78424
79473
|
agent_id: task2.agent_id,
|
|
78425
79474
|
assigned_to: task2.assigned_to,
|
|
79475
|
+
working_dir: task2.working_dir,
|
|
78426
79476
|
locked_by: task2.locked_by,
|
|
78427
79477
|
tags: task2.tags,
|
|
79478
|
+
metadata: task2.metadata,
|
|
78428
79479
|
version: task2.version,
|
|
78429
79480
|
created_at: task2.created_at,
|
|
78430
79481
|
updated_at: task2.updated_at,
|
|
@@ -78566,6 +79617,9 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
78566
79617
|
if (path === "/api/tasks" && method === "POST") {
|
|
78567
79618
|
return handleCreateTask(req, ctx, jsonWithCors, taskToSummary);
|
|
78568
79619
|
}
|
|
79620
|
+
if (path === "/api/tasks/upsert" && method === "POST") {
|
|
79621
|
+
return handleUpsertTask(req, ctx, jsonWithCors, taskToSummary);
|
|
79622
|
+
}
|
|
78569
79623
|
if (path === "/api/tasks/export" && method === "GET") {
|
|
78570
79624
|
return handleTasksExport(req, url, ctx, jsonWithCors, taskToSummary);
|
|
78571
79625
|
}
|
|
@@ -78761,7 +79815,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
78761
79815
|
} catch {}
|
|
78762
79816
|
}
|
|
78763
79817
|
}
|
|
78764
|
-
var MIME_TYPES, SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX
|
|
79818
|
+
var MIME_TYPES, SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX;
|
|
78765
79819
|
var init_serve = __esm(() => {
|
|
78766
79820
|
init_database();
|
|
78767
79821
|
init_api_keys();
|
|
@@ -78786,6 +79840,7 @@ var init_serve = __esm(() => {
|
|
|
78786
79840
|
"Permissions-Policy": "camera=, microphone=, geolocation="
|
|
78787
79841
|
};
|
|
78788
79842
|
rateLimitMap = new Map;
|
|
79843
|
+
RATE_LIMIT_MAX = Number.parseInt(process.env["TODOS_RATE_LIMIT_MAX"] || "120", 10);
|
|
78789
79844
|
});
|
|
78790
79845
|
|
|
78791
79846
|
// src/server/index.ts
|