@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/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",
|
|
@@ -8653,7 +8738,7 @@ var init_event_hooks = __esm(() => {
|
|
|
8653
8738
|
VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
|
|
8654
8739
|
});
|
|
8655
8740
|
|
|
8656
|
-
// node_modules/.bun/@hasna+events@0.1.
|
|
8741
|
+
// node_modules/.bun/@hasna+events@0.1.10/node_modules/@hasna/events/dist/index.js
|
|
8657
8742
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
8658
8743
|
import { existsSync as existsSync5 } from "fs";
|
|
8659
8744
|
import { homedir } from "os";
|
|
@@ -8700,14 +8785,40 @@ function matchRecord(input, matcher) {
|
|
|
8700
8785
|
return true;
|
|
8701
8786
|
return Object.entries(matcher).every(([path, expected]) => {
|
|
8702
8787
|
const actual = getPathValue(input, path);
|
|
8703
|
-
|
|
8704
|
-
return matchString(actual === undefined ? undefined : String(actual), expected, {
|
|
8705
|
-
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
8706
|
-
});
|
|
8707
|
-
}
|
|
8708
|
-
return actual === expected;
|
|
8788
|
+
return matchField(actual, expected, path);
|
|
8709
8789
|
});
|
|
8710
8790
|
}
|
|
8791
|
+
function matchField(actual, expected, path) {
|
|
8792
|
+
if (isNegativeMatcher(expected)) {
|
|
8793
|
+
return !matchPositiveField(actual, expected.not, path);
|
|
8794
|
+
}
|
|
8795
|
+
return matchPositiveField(actual, expected, path);
|
|
8796
|
+
}
|
|
8797
|
+
function matchPositiveField(actual, expected, path) {
|
|
8798
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
8799
|
+
return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
|
|
8800
|
+
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
8801
|
+
}));
|
|
8802
|
+
}
|
|
8803
|
+
if (Array.isArray(actual)) {
|
|
8804
|
+
return actual.some((item) => item === expected);
|
|
8805
|
+
}
|
|
8806
|
+
return actual === expected;
|
|
8807
|
+
}
|
|
8808
|
+
function stringCandidates(actual) {
|
|
8809
|
+
if (actual === undefined)
|
|
8810
|
+
return [];
|
|
8811
|
+
if (Array.isArray(actual)) {
|
|
8812
|
+
return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
|
|
8813
|
+
}
|
|
8814
|
+
return [String(actual)];
|
|
8815
|
+
}
|
|
8816
|
+
function isPrimitiveFieldValue(value) {
|
|
8817
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
8818
|
+
}
|
|
8819
|
+
function isNegativeMatcher(value) {
|
|
8820
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
|
|
8821
|
+
}
|
|
8711
8822
|
function eventMatchesFilter(event, filter) {
|
|
8712
8823
|
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);
|
|
8713
8824
|
}
|
|
@@ -9315,9 +9426,66 @@ function taskEventData(task, extra = {}) {
|
|
|
9315
9426
|
started_at: task.started_at,
|
|
9316
9427
|
completed_at: task.completed_at,
|
|
9317
9428
|
due_at: task.due_at,
|
|
9429
|
+
requires_approval: task.requires_approval,
|
|
9430
|
+
approved_by: task.approved_by,
|
|
9431
|
+
approved_at: task.approved_at,
|
|
9318
9432
|
...extra
|
|
9319
9433
|
};
|
|
9320
9434
|
}
|
|
9435
|
+
function booleanField(value) {
|
|
9436
|
+
if (typeof value === "boolean")
|
|
9437
|
+
return value;
|
|
9438
|
+
if (typeof value === "number") {
|
|
9439
|
+
if (value === 1)
|
|
9440
|
+
return true;
|
|
9441
|
+
if (value === 0)
|
|
9442
|
+
return false;
|
|
9443
|
+
}
|
|
9444
|
+
if (typeof value === "string") {
|
|
9445
|
+
const normalized = value.trim().toLowerCase();
|
|
9446
|
+
if (["true", "1", "yes", "on"].includes(normalized))
|
|
9447
|
+
return true;
|
|
9448
|
+
if (["false", "0", "no", "off"].includes(normalized))
|
|
9449
|
+
return false;
|
|
9450
|
+
}
|
|
9451
|
+
return;
|
|
9452
|
+
}
|
|
9453
|
+
function objectField(value) {
|
|
9454
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
9455
|
+
}
|
|
9456
|
+
function firstBoolean(records, keys) {
|
|
9457
|
+
for (const record of records) {
|
|
9458
|
+
for (const key of keys) {
|
|
9459
|
+
const value = booleanField(record[key]);
|
|
9460
|
+
if (value !== undefined)
|
|
9461
|
+
return value;
|
|
9462
|
+
}
|
|
9463
|
+
}
|
|
9464
|
+
return;
|
|
9465
|
+
}
|
|
9466
|
+
function routingAutomationMetadata(task) {
|
|
9467
|
+
const automation = objectField(task.metadata.automation);
|
|
9468
|
+
const records = [task.metadata];
|
|
9469
|
+
if (automation)
|
|
9470
|
+
records.push(automation);
|
|
9471
|
+
const result = {};
|
|
9472
|
+
const aliases = [
|
|
9473
|
+
["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
|
|
9474
|
+
["no_auto", ["no_auto", "noAuto"]],
|
|
9475
|
+
["manual", ["manual"]],
|
|
9476
|
+
["manual_required", ["manual_required", "manualRequired"]],
|
|
9477
|
+
["requires_approval", ["requires_approval", "requiresApproval"]],
|
|
9478
|
+
["approval_required", ["approval_required", "approvalRequired"]]
|
|
9479
|
+
];
|
|
9480
|
+
for (const [canonical, keys] of aliases) {
|
|
9481
|
+
const value = firstBoolean(records, keys);
|
|
9482
|
+
if (value !== undefined)
|
|
9483
|
+
result[canonical] = value;
|
|
9484
|
+
}
|
|
9485
|
+
if (task.requires_approval)
|
|
9486
|
+
result.requires_approval = true;
|
|
9487
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
9488
|
+
}
|
|
9321
9489
|
function taskEventMetadata(task) {
|
|
9322
9490
|
const metadata = {
|
|
9323
9491
|
package: "@hasna/todos",
|
|
@@ -9328,6 +9496,14 @@ function taskEventMetadata(task) {
|
|
|
9328
9496
|
task_list_id: task.task_list_id,
|
|
9329
9497
|
working_dir: task.working_dir
|
|
9330
9498
|
};
|
|
9499
|
+
const routeEnabled = booleanField(task.metadata.route_enabled);
|
|
9500
|
+
if (routeEnabled !== undefined) {
|
|
9501
|
+
metadata.route_enabled = routeEnabled;
|
|
9502
|
+
}
|
|
9503
|
+
const automation = routingAutomationMetadata(task);
|
|
9504
|
+
if (automation) {
|
|
9505
|
+
metadata.automation = automation;
|
|
9506
|
+
}
|
|
9331
9507
|
try {
|
|
9332
9508
|
const project = task.project_id ? getProject(task.project_id) : null;
|
|
9333
9509
|
const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
|
|
@@ -9345,9 +9521,6 @@ function taskEventMetadata(task) {
|
|
|
9345
9521
|
if (projectPath) {
|
|
9346
9522
|
metadata.project_kind = classifyProjectKind(projectPath);
|
|
9347
9523
|
metadata.project_is_worktree = isWorktreePath(projectPath);
|
|
9348
|
-
if (typeof task.metadata.route_enabled === "boolean") {
|
|
9349
|
-
metadata.route_enabled = task.metadata.route_enabled;
|
|
9350
|
-
}
|
|
9351
9524
|
metadata.working_dir = task.working_dir ?? projectPath;
|
|
9352
9525
|
}
|
|
9353
9526
|
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;
|
|
@@ -9929,6 +10102,17 @@ function replaceTaskTags(taskId, tags, db) {
|
|
|
9929
10102
|
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
9930
10103
|
insertTaskTags(taskId, tags, db);
|
|
9931
10104
|
}
|
|
10105
|
+
function addMetadataConditions(metadata, conditions, params) {
|
|
10106
|
+
if (!metadata)
|
|
10107
|
+
return;
|
|
10108
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
10109
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
|
|
10110
|
+
throw new Error(`Invalid metadata filter key: ${key}`);
|
|
10111
|
+
}
|
|
10112
|
+
conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
|
|
10113
|
+
params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
|
|
10114
|
+
}
|
|
10115
|
+
}
|
|
9932
10116
|
function createTask(input, db) {
|
|
9933
10117
|
const d = db || getDatabase();
|
|
9934
10118
|
const timestamp = now();
|
|
@@ -10111,6 +10295,7 @@ function listTasks(filter = {}, db) {
|
|
|
10111
10295
|
params.push(filter.task_type);
|
|
10112
10296
|
}
|
|
10113
10297
|
}
|
|
10298
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
10114
10299
|
const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
|
|
10115
10300
|
if (filter.cursor) {
|
|
10116
10301
|
try {
|
|
@@ -10135,6 +10320,54 @@ function listTasks(filter = {}, db) {
|
|
|
10135
10320
|
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
|
|
10136
10321
|
return rows.map(rowToTask);
|
|
10137
10322
|
}
|
|
10323
|
+
function getTaskByFingerprint(fingerprint, db) {
|
|
10324
|
+
const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
|
|
10325
|
+
return tasks[0] ?? null;
|
|
10326
|
+
}
|
|
10327
|
+
function mergeTaskMetadata(current, next, fingerprint) {
|
|
10328
|
+
return {
|
|
10329
|
+
...current,
|
|
10330
|
+
...next ?? {},
|
|
10331
|
+
fingerprint
|
|
10332
|
+
};
|
|
10333
|
+
}
|
|
10334
|
+
function upsertTaskByFingerprint(input, db) {
|
|
10335
|
+
const d = db || getDatabase();
|
|
10336
|
+
const fingerprint = input.fingerprint.trim();
|
|
10337
|
+
if (!fingerprint)
|
|
10338
|
+
throw new Error("fingerprint is required");
|
|
10339
|
+
const existing = getTaskByFingerprint(fingerprint, d);
|
|
10340
|
+
const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
|
|
10341
|
+
if (!existing) {
|
|
10342
|
+
const task2 = createTask({ ...input, metadata }, d);
|
|
10343
|
+
return { task: task2, created: true };
|
|
10344
|
+
}
|
|
10345
|
+
const task = updateTask(existing.id, {
|
|
10346
|
+
version: existing.version,
|
|
10347
|
+
title: input.title,
|
|
10348
|
+
description: input.description,
|
|
10349
|
+
status: input.status,
|
|
10350
|
+
priority: input.priority,
|
|
10351
|
+
project_id: input.project_id,
|
|
10352
|
+
assigned_to: input.assigned_to,
|
|
10353
|
+
working_dir: input.working_dir,
|
|
10354
|
+
plan_id: input.plan_id,
|
|
10355
|
+
task_list_id: input.task_list_id,
|
|
10356
|
+
tags: input.tags,
|
|
10357
|
+
metadata,
|
|
10358
|
+
due_at: input.due_at,
|
|
10359
|
+
estimated_minutes: input.estimated_minutes,
|
|
10360
|
+
sla_minutes: input.sla_minutes,
|
|
10361
|
+
confidence: input.confidence,
|
|
10362
|
+
retry_count: input.retry_count,
|
|
10363
|
+
max_retries: input.max_retries,
|
|
10364
|
+
retry_after: input.retry_after,
|
|
10365
|
+
requires_approval: input.requires_approval,
|
|
10366
|
+
recurrence_rule: input.recurrence_rule,
|
|
10367
|
+
task_type: input.task_type
|
|
10368
|
+
}, d);
|
|
10369
|
+
return { task, created: false };
|
|
10370
|
+
}
|
|
10138
10371
|
function countTasks(filter = {}, db) {
|
|
10139
10372
|
const d = db || getDatabase();
|
|
10140
10373
|
const conditions = [];
|
|
@@ -10198,6 +10431,7 @@ function countTasks(filter = {}, db) {
|
|
|
10198
10431
|
conditions.push("task_list_id = ?");
|
|
10199
10432
|
params.push(filter.task_list_id);
|
|
10200
10433
|
}
|
|
10434
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
10201
10435
|
if (!filter.include_archived) {
|
|
10202
10436
|
conditions.push("archived_at IS NULL");
|
|
10203
10437
|
}
|
|
@@ -10248,6 +10482,10 @@ function updateTask(id, input, db) {
|
|
|
10248
10482
|
sets.push("assigned_to = ?");
|
|
10249
10483
|
params.push(input.assigned_to);
|
|
10250
10484
|
}
|
|
10485
|
+
if (input.working_dir !== undefined) {
|
|
10486
|
+
sets.push("working_dir = ?");
|
|
10487
|
+
params.push(input.working_dir);
|
|
10488
|
+
}
|
|
10251
10489
|
if (input.tags !== undefined) {
|
|
10252
10490
|
sets.push("tags = ?");
|
|
10253
10491
|
params.push(JSON.stringify(input.tags));
|
|
@@ -10336,6 +10574,8 @@ function updateTask(id, input, db) {
|
|
|
10336
10574
|
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
10337
10575
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
10338
10576
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
10577
|
+
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
10578
|
+
logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
10339
10579
|
if (input.approved_by !== undefined)
|
|
10340
10580
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
10341
10581
|
const updatedTask = {
|
|
@@ -10371,6 +10611,10 @@ function updateTask(id, input, db) {
|
|
|
10371
10611
|
if (input.approved_by !== undefined) {
|
|
10372
10612
|
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
10373
10613
|
}
|
|
10614
|
+
const updatePayload = taskEventData(updatedTask);
|
|
10615
|
+
dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
|
|
10616
|
+
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
|
|
10617
|
+
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
|
|
10374
10618
|
return updatedTask;
|
|
10375
10619
|
}
|
|
10376
10620
|
function deleteTask(id, db) {
|
|
@@ -13388,6 +13632,72 @@ function rowToArtifact(row) {
|
|
|
13388
13632
|
function getRunRow(runId, db) {
|
|
13389
13633
|
return db.query("SELECT * FROM task_runs WHERE id = ?").get(runId);
|
|
13390
13634
|
}
|
|
13635
|
+
function normalizeTransactionKey(input) {
|
|
13636
|
+
const key = (input.key || input.loop_run_id || input.loop_id || "").trim();
|
|
13637
|
+
if (!key)
|
|
13638
|
+
throw new Error("idempotent run transactions require --key, --loop-run-id, or --loop-id");
|
|
13639
|
+
return key.toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 240);
|
|
13640
|
+
}
|
|
13641
|
+
function loopTransactionMetadata(record) {
|
|
13642
|
+
const value = record.metadata["loop_transaction"];
|
|
13643
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
13644
|
+
}
|
|
13645
|
+
function runKey(record) {
|
|
13646
|
+
const tx = loopTransactionMetadata(record);
|
|
13647
|
+
const key = tx["idempotency_key"] ?? record.metadata["idempotency_key"];
|
|
13648
|
+
return typeof key === "string" ? key : null;
|
|
13649
|
+
}
|
|
13650
|
+
function loopId(record) {
|
|
13651
|
+
const tx = loopTransactionMetadata(record);
|
|
13652
|
+
const value = tx["loop_id"] ?? record.metadata["loop_id"];
|
|
13653
|
+
return typeof value === "string" ? value : null;
|
|
13654
|
+
}
|
|
13655
|
+
function loopRunId(record) {
|
|
13656
|
+
const tx = loopTransactionMetadata(record);
|
|
13657
|
+
const value = tx["loop_run_id"] ?? record.metadata["loop_run_id"];
|
|
13658
|
+
return typeof value === "string" ? value : null;
|
|
13659
|
+
}
|
|
13660
|
+
function getTaskRunTransactionByKey(key, taskId, db) {
|
|
13661
|
+
if (taskId) {
|
|
13662
|
+
return db.query("SELECT * FROM task_run_transactions WHERE task_id = ? AND key = ?").get(taskId, key);
|
|
13663
|
+
}
|
|
13664
|
+
const rows = db.query("SELECT * FROM task_run_transactions WHERE key = ? ORDER BY created_at DESC LIMIT 2").all(key);
|
|
13665
|
+
if (rows.length > 1)
|
|
13666
|
+
throw new Error(`Run transaction key is ambiguous across tasks: ${key}. Pass task_id.`);
|
|
13667
|
+
return rows[0] ?? null;
|
|
13668
|
+
}
|
|
13669
|
+
function summarizeTaskRun(run) {
|
|
13670
|
+
return {
|
|
13671
|
+
id: run.id,
|
|
13672
|
+
task_id: run.task_id,
|
|
13673
|
+
agent_id: run.agent_id,
|
|
13674
|
+
title: run.title,
|
|
13675
|
+
status: run.status,
|
|
13676
|
+
summary: run.summary,
|
|
13677
|
+
idempotency_key: runKey(run),
|
|
13678
|
+
loop_id: loopId(run),
|
|
13679
|
+
loop_run_id: loopRunId(run),
|
|
13680
|
+
metadata_keys: Object.keys(run.metadata).sort(),
|
|
13681
|
+
started_at: run.started_at,
|
|
13682
|
+
completed_at: run.completed_at,
|
|
13683
|
+
updated_at: run.updated_at
|
|
13684
|
+
};
|
|
13685
|
+
}
|
|
13686
|
+
function findTaskRunByTransactionKey(key, taskId, db) {
|
|
13687
|
+
const d = db || getDatabase();
|
|
13688
|
+
const normalized = normalizeTransactionKey({ key });
|
|
13689
|
+
const transaction = getTaskRunTransactionByKey(normalized, taskId, d);
|
|
13690
|
+
if (transaction?.run_id)
|
|
13691
|
+
return getTaskRun(transaction.run_id, d);
|
|
13692
|
+
return null;
|
|
13693
|
+
}
|
|
13694
|
+
function loopRunCommands(run, key) {
|
|
13695
|
+
return [
|
|
13696
|
+
run ? `todos runs show ${run.id.slice(0, 8)}` : "todos runs list",
|
|
13697
|
+
run ? `todos findings list --task ${run.task_id.slice(0, 8)} --json` : "todos findings list --json",
|
|
13698
|
+
`todos runs begin <task-id> --key ${key} --apply --json`
|
|
13699
|
+
];
|
|
13700
|
+
}
|
|
13391
13701
|
function resolveTaskRunId(idOrPrefix, db) {
|
|
13392
13702
|
const d = db || getDatabase();
|
|
13393
13703
|
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 +13716,7 @@ function startTaskRun(input, db) {
|
|
|
13406
13716
|
const d = db || getDatabase();
|
|
13407
13717
|
if (!getTask(input.task_id, d))
|
|
13408
13718
|
throw new TaskNotFoundError(input.task_id);
|
|
13409
|
-
const id = uuid();
|
|
13719
|
+
const id = input.id ?? uuid();
|
|
13410
13720
|
const timestamp = input.started_at || now();
|
|
13411
13721
|
if (input.claim && input.agent_id) {
|
|
13412
13722
|
startTask(input.task_id, input.agent_id, d);
|
|
@@ -13444,6 +13754,97 @@ function startTaskRun(input, db) {
|
|
|
13444
13754
|
emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
|
|
13445
13755
|
return run;
|
|
13446
13756
|
}
|
|
13757
|
+
function beginTaskRunTransaction(input, db) {
|
|
13758
|
+
const d = db || getDatabase();
|
|
13759
|
+
if (!getTask(input.task_id, d))
|
|
13760
|
+
throw new TaskNotFoundError(input.task_id);
|
|
13761
|
+
const timestamp = input.started_at || now();
|
|
13762
|
+
const key = normalizeTransactionKey(input);
|
|
13763
|
+
const existing = findTaskRunByTransactionKey(key, input.task_id, d);
|
|
13764
|
+
const dryRun = !input.apply;
|
|
13765
|
+
if (existing) {
|
|
13766
|
+
return {
|
|
13767
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
13768
|
+
local_only: true,
|
|
13769
|
+
dry_run: dryRun,
|
|
13770
|
+
processed_at: timestamp,
|
|
13771
|
+
action: "matched",
|
|
13772
|
+
key,
|
|
13773
|
+
run: summarizeTaskRun(existing),
|
|
13774
|
+
warnings: existing.status === "running" ? [] : [`matched ${existing.status} run`],
|
|
13775
|
+
commands: loopRunCommands(existing, key)
|
|
13776
|
+
};
|
|
13777
|
+
}
|
|
13778
|
+
if (dryRun) {
|
|
13779
|
+
return {
|
|
13780
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
13781
|
+
local_only: true,
|
|
13782
|
+
dry_run: true,
|
|
13783
|
+
processed_at: timestamp,
|
|
13784
|
+
action: "preview",
|
|
13785
|
+
key,
|
|
13786
|
+
run: null,
|
|
13787
|
+
warnings: [],
|
|
13788
|
+
commands: loopRunCommands(null, key)
|
|
13789
|
+
};
|
|
13790
|
+
}
|
|
13791
|
+
const metadata = redactValue({
|
|
13792
|
+
...input.metadata || {},
|
|
13793
|
+
loop_transaction: {
|
|
13794
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
13795
|
+
idempotency_key: key,
|
|
13796
|
+
loop_id: input.loop_id ?? null,
|
|
13797
|
+
loop_run_id: input.loop_run_id ?? null,
|
|
13798
|
+
first_seen_at: timestamp
|
|
13799
|
+
},
|
|
13800
|
+
idempotency_key: key
|
|
13801
|
+
});
|
|
13802
|
+
const created = d.transaction(() => {
|
|
13803
|
+
d.run(`INSERT OR IGNORE INTO task_run_transactions (
|
|
13804
|
+
id, task_id, run_id, key, loop_id, loop_run_id, metadata, created_at, updated_at
|
|
13805
|
+
) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?)`, [
|
|
13806
|
+
uuid(),
|
|
13807
|
+
input.task_id,
|
|
13808
|
+
key,
|
|
13809
|
+
input.loop_id ?? null,
|
|
13810
|
+
input.loop_run_id ?? null,
|
|
13811
|
+
JSON.stringify(metadata),
|
|
13812
|
+
timestamp,
|
|
13813
|
+
timestamp
|
|
13814
|
+
]);
|
|
13815
|
+
const transaction = getTaskRunTransactionByKey(key, input.task_id, d);
|
|
13816
|
+
if (!transaction)
|
|
13817
|
+
throw new Error(`Could not create run transaction for key: ${key}`);
|
|
13818
|
+
if (transaction.run_id) {
|
|
13819
|
+
const existingRun = getTaskRun(transaction.run_id, d);
|
|
13820
|
+
if (existingRun)
|
|
13821
|
+
return { run: existingRun, action: "matched" };
|
|
13822
|
+
}
|
|
13823
|
+
const run = startTaskRun({
|
|
13824
|
+
id: uuid(),
|
|
13825
|
+
task_id: input.task_id,
|
|
13826
|
+
agent_id: input.agent_id,
|
|
13827
|
+
title: input.title,
|
|
13828
|
+
summary: input.summary,
|
|
13829
|
+
metadata,
|
|
13830
|
+
claim: input.claim,
|
|
13831
|
+
started_at: timestamp
|
|
13832
|
+
}, d);
|
|
13833
|
+
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]);
|
|
13834
|
+
return { run, action: "created" };
|
|
13835
|
+
})();
|
|
13836
|
+
return {
|
|
13837
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
13838
|
+
local_only: true,
|
|
13839
|
+
dry_run: false,
|
|
13840
|
+
processed_at: timestamp,
|
|
13841
|
+
action: created.action,
|
|
13842
|
+
key,
|
|
13843
|
+
run: summarizeTaskRun(created.run),
|
|
13844
|
+
warnings: [],
|
|
13845
|
+
commands: loopRunCommands(created.run, key)
|
|
13846
|
+
};
|
|
13847
|
+
}
|
|
13447
13848
|
function addTaskRunEvent(input, db) {
|
|
13448
13849
|
const d = db || getDatabase();
|
|
13449
13850
|
const runId = resolveTaskRunId(input.run_id, d);
|
|
@@ -13623,6 +14024,66 @@ function finishTaskRun(input, db) {
|
|
|
13623
14024
|
});
|
|
13624
14025
|
return updated;
|
|
13625
14026
|
}
|
|
14027
|
+
function finishTaskRunTransaction(input, db) {
|
|
14028
|
+
const d = db || getDatabase();
|
|
14029
|
+
const timestamp = input.completed_at || now();
|
|
14030
|
+
const status = input.status || "completed";
|
|
14031
|
+
const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
|
|
14032
|
+
const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
|
|
14033
|
+
if (!run) {
|
|
14034
|
+
throw new Error(input.run_id ? `Run not found: ${input.run_id}` : "runs finish requires a run id or --key");
|
|
14035
|
+
}
|
|
14036
|
+
if (input.task_id && run.task_id !== input.task_id) {
|
|
14037
|
+
throw new Error(`Run ${run.id} belongs to task ${run.task_id}, not ${input.task_id}`);
|
|
14038
|
+
}
|
|
14039
|
+
const resolvedKey = key || runKey(run) || run.id;
|
|
14040
|
+
const dryRun = input.apply === false;
|
|
14041
|
+
if (run.status !== "running") {
|
|
14042
|
+
const conflict = run.status !== status;
|
|
14043
|
+
return {
|
|
14044
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
14045
|
+
local_only: true,
|
|
14046
|
+
dry_run: dryRun,
|
|
14047
|
+
processed_at: timestamp,
|
|
14048
|
+
action: conflict ? "conflict" : "matched",
|
|
14049
|
+
key: resolvedKey,
|
|
14050
|
+
run: summarizeTaskRun(run),
|
|
14051
|
+
warnings: conflict ? [`run is already ${run.status}; requested ${status}`] : [],
|
|
14052
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
14053
|
+
};
|
|
14054
|
+
}
|
|
14055
|
+
if (dryRun) {
|
|
14056
|
+
return {
|
|
14057
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
14058
|
+
local_only: true,
|
|
14059
|
+
dry_run: true,
|
|
14060
|
+
processed_at: timestamp,
|
|
14061
|
+
action: "preview",
|
|
14062
|
+
key: resolvedKey,
|
|
14063
|
+
run: summarizeTaskRun({ ...run, status, summary: input.summary ?? run.summary, completed_at: timestamp, updated_at: timestamp }),
|
|
14064
|
+
warnings: [],
|
|
14065
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
14066
|
+
};
|
|
14067
|
+
}
|
|
14068
|
+
const finished = finishTaskRun({
|
|
14069
|
+
run_id: run.id,
|
|
14070
|
+
status,
|
|
14071
|
+
summary: input.summary,
|
|
14072
|
+
agent_id: input.agent_id,
|
|
14073
|
+
completed_at: timestamp
|
|
14074
|
+
}, d);
|
|
14075
|
+
return {
|
|
14076
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
14077
|
+
local_only: true,
|
|
14078
|
+
dry_run: false,
|
|
14079
|
+
processed_at: timestamp,
|
|
14080
|
+
action: "finished",
|
|
14081
|
+
key: resolvedKey,
|
|
14082
|
+
run: summarizeTaskRun(finished),
|
|
14083
|
+
warnings: [],
|
|
14084
|
+
commands: loopRunCommands(finished, resolvedKey)
|
|
14085
|
+
};
|
|
14086
|
+
}
|
|
13626
14087
|
function listTaskRuns(taskId, db) {
|
|
13627
14088
|
const d = db || getDatabase();
|
|
13628
14089
|
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 +14101,7 @@ function getTaskRunLedger(runId, db) {
|
|
|
13640
14101
|
const files = d.query("SELECT * FROM task_files WHERE task_id = ? ORDER BY updated_at DESC, path").all(run.task_id);
|
|
13641
14102
|
return { run, events, commands, artifacts, files };
|
|
13642
14103
|
}
|
|
14104
|
+
var LOOP_RUN_TRANSACTION_SCHEMA_VERSION = "todos.loop_run_transaction.v1";
|
|
13643
14105
|
var init_task_runs = __esm(() => {
|
|
13644
14106
|
init_artifact_store();
|
|
13645
14107
|
init_event_hooks();
|
|
@@ -14028,6 +14490,7 @@ var init_calendar = __esm(() => {
|
|
|
14028
14490
|
var exports_tasks = {};
|
|
14029
14491
|
__export(exports_tasks, {
|
|
14030
14492
|
watchTask: () => watchTask,
|
|
14493
|
+
upsertTaskByFingerprint: () => upsertTaskByFingerprint,
|
|
14031
14494
|
updateTaskBoard: () => updateTaskBoard,
|
|
14032
14495
|
updateTask: () => updateTask,
|
|
14033
14496
|
unwatchTask: () => unwatchTask,
|
|
@@ -14071,6 +14534,7 @@ __export(exports_tasks, {
|
|
|
14071
14534
|
getTaskGraph: () => getTaskGraph,
|
|
14072
14535
|
getTaskDependents: () => getTaskDependents,
|
|
14073
14536
|
getTaskDependencies: () => getTaskDependencies,
|
|
14537
|
+
getTaskByFingerprint: () => getTaskByFingerprint,
|
|
14074
14538
|
getTaskBoard: () => getTaskBoard,
|
|
14075
14539
|
getTask: () => getTask,
|
|
14076
14540
|
getStatus: () => getStatus,
|
|
@@ -14917,6 +15381,7 @@ var init_token_utils = __esm(() => {
|
|
|
14917
15381
|
"add_task_run_event",
|
|
14918
15382
|
"add_task_run_file",
|
|
14919
15383
|
"acknowledge_handoff",
|
|
15384
|
+
"begin_task_run_transaction",
|
|
14920
15385
|
"build_local_report",
|
|
14921
15386
|
"cancel_agent_run_dispatch",
|
|
14922
15387
|
"finish_task_run",
|
|
@@ -14964,6 +15429,7 @@ var init_token_utils = __esm(() => {
|
|
|
14964
15429
|
"list_local_snapshots",
|
|
14965
15430
|
"list_retrospectives",
|
|
14966
15431
|
"list_risks",
|
|
15432
|
+
"list_task_findings",
|
|
14967
15433
|
"list_task_runs",
|
|
14968
15434
|
"list_verification_providers",
|
|
14969
15435
|
"merge_duplicate_task",
|
|
@@ -14972,6 +15438,7 @@ var init_token_utils = __esm(() => {
|
|
|
14972
15438
|
"remove_review_routing_rule",
|
|
14973
15439
|
"restore_local_backup",
|
|
14974
15440
|
"retry_agent_run_dispatch",
|
|
15441
|
+
"resolve_missing_task_findings",
|
|
14975
15442
|
"resolve_mentions",
|
|
14976
15443
|
"run_next_agent_dispatch",
|
|
14977
15444
|
"search_knowledge_records",
|
|
@@ -15014,9 +15481,17 @@ var init_token_utils = __esm(() => {
|
|
|
15014
15481
|
"unlock_file",
|
|
15015
15482
|
"unwatch_task",
|
|
15016
15483
|
"update_comment",
|
|
15484
|
+
"upsert_task_finding",
|
|
15017
15485
|
"update_risk",
|
|
15018
15486
|
"watch_task"
|
|
15019
15487
|
],
|
|
15488
|
+
loops: [
|
|
15489
|
+
"begin_task_run_transaction",
|
|
15490
|
+
"finish_task_run",
|
|
15491
|
+
"list_task_findings",
|
|
15492
|
+
"resolve_missing_task_findings",
|
|
15493
|
+
"upsert_task_finding"
|
|
15494
|
+
],
|
|
15020
15495
|
agents: [
|
|
15021
15496
|
"auto_assign_task",
|
|
15022
15497
|
"delete_agent",
|
|
@@ -15098,7 +15573,7 @@ var init_token_utils = __esm(() => {
|
|
|
15098
15573
|
maintenance: ["extract_todos", "get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task", "watch_source_todos"]
|
|
15099
15574
|
};
|
|
15100
15575
|
MCP_PROFILE_GROUPS = {
|
|
15101
|
-
minimal: ["core"],
|
|
15576
|
+
minimal: ["core", "loops"],
|
|
15102
15577
|
core: ["core"],
|
|
15103
15578
|
standard: ["core", "tasks", "projects", "resources", "agents", "metadata"],
|
|
15104
15579
|
agent: ["core", "tasks", "projects", "resources"],
|
|
@@ -15178,6 +15653,61 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
15178
15653
|
}
|
|
15179
15654
|
});
|
|
15180
15655
|
}
|
|
15656
|
+
if (shouldRegisterTool("upsert_task")) {
|
|
15657
|
+
server.tool("upsert_task", "Create or update a task by stable metadata fingerprint. Metadata is shallow-merged on updates.", {
|
|
15658
|
+
fingerprint: exports_external.string().describe("Stable dedupe fingerprint stored as metadata.fingerprint"),
|
|
15659
|
+
title: exports_external.string().describe("Task title"),
|
|
15660
|
+
description: exports_external.string().optional().describe("Task description"),
|
|
15661
|
+
status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
|
|
15662
|
+
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
|
|
15663
|
+
project_id: exports_external.string().optional().describe("Project ID"),
|
|
15664
|
+
task_list_id: exports_external.string().optional().describe("Task list ID"),
|
|
15665
|
+
assigned_to: exports_external.string().optional().describe("Agent ID or name to assign to"),
|
|
15666
|
+
tags: exports_external.array(exports_external.string()).optional().describe("Tags for the task"),
|
|
15667
|
+
working_dir: exports_external.string().optional().describe("Working directory associated with the task"),
|
|
15668
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Metadata object to shallow-merge"),
|
|
15669
|
+
expectation_id: exports_external.string().optional(),
|
|
15670
|
+
expectation_fingerprint: exports_external.string().optional(),
|
|
15671
|
+
evidence_paths: exports_external.array(exports_external.string()).optional(),
|
|
15672
|
+
origin_loop_id: exports_external.string().optional(),
|
|
15673
|
+
origin_run_id: exports_external.string().optional(),
|
|
15674
|
+
expected: exports_external.unknown().optional(),
|
|
15675
|
+
observed: exports_external.unknown().optional(),
|
|
15676
|
+
acceptance: exports_external.unknown().optional()
|
|
15677
|
+
}, async (params) => {
|
|
15678
|
+
try {
|
|
15679
|
+
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;
|
|
15680
|
+
const mergedMetadata = { ...metadata ?? {} };
|
|
15681
|
+
if (expectation_id !== undefined)
|
|
15682
|
+
mergedMetadata["expectation_id"] = expectation_id;
|
|
15683
|
+
if (expectation_fingerprint !== undefined)
|
|
15684
|
+
mergedMetadata["expectation_fingerprint"] = expectation_fingerprint;
|
|
15685
|
+
if (evidence_paths !== undefined)
|
|
15686
|
+
mergedMetadata["evidence_paths"] = evidence_paths;
|
|
15687
|
+
if (origin_loop_id !== undefined)
|
|
15688
|
+
mergedMetadata["origin_loop_id"] = origin_loop_id;
|
|
15689
|
+
if (origin_run_id !== undefined)
|
|
15690
|
+
mergedMetadata["origin_run_id"] = origin_run_id;
|
|
15691
|
+
if (expected !== undefined)
|
|
15692
|
+
mergedMetadata["expected"] = expected;
|
|
15693
|
+
if (observed !== undefined)
|
|
15694
|
+
mergedMetadata["observed"] = observed;
|
|
15695
|
+
if (acceptance !== undefined)
|
|
15696
|
+
mergedMetadata["acceptance"] = acceptance;
|
|
15697
|
+
const resolved = { ...rest, metadata: mergedMetadata };
|
|
15698
|
+
if (assigned_to)
|
|
15699
|
+
resolved.assigned_to = resolveAssignee(assigned_to);
|
|
15700
|
+
if (project_id)
|
|
15701
|
+
resolved.project_id = resolveId(project_id, "projects");
|
|
15702
|
+
if (task_list_id)
|
|
15703
|
+
resolved.task_list_id = resolveId(task_list_id, "task_lists");
|
|
15704
|
+
const result = upsertTaskByFingerprint(resolved);
|
|
15705
|
+
return { content: [{ type: "text", text: compactJson({ created: result.created, task: JSON.parse(mutationTaskResponse(result.task)) }) }] };
|
|
15706
|
+
} catch (e) {
|
|
15707
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
15708
|
+
}
|
|
15709
|
+
});
|
|
15710
|
+
}
|
|
15181
15711
|
if (shouldRegisterTool("list_tasks")) {
|
|
15182
15712
|
server.tool("list_tasks", "List tasks with optional filters. Pass empty arrays for multi-value filters (e.g. status=[] shows all).", {
|
|
15183
15713
|
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 +15719,8 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
15189
15719
|
created_after: exports_external.string().optional().describe("ISO date \u2014 tasks created after this date"),
|
|
15190
15720
|
created_before: exports_external.string().optional().describe("ISO date \u2014 tasks created before this date"),
|
|
15191
15721
|
limit: exports_external.number().optional().describe("Max results (default: 50, max 500)"),
|
|
15192
|
-
offset: exports_external.number().optional().describe("Pagination offset")
|
|
15722
|
+
offset: exports_external.number().optional().describe("Pagination offset"),
|
|
15723
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Exact top-level metadata filters")
|
|
15193
15724
|
}, async (params) => {
|
|
15194
15725
|
try {
|
|
15195
15726
|
const resolved = { ...params };
|
|
@@ -18730,7 +19261,7 @@ function countMovedStandaloneVerifications(db, duplicateId) {
|
|
|
18730
19261
|
const rows = db.query("SELECT command FROM task_verifications WHERE task_id = ?").all(duplicateId);
|
|
18731
19262
|
return rows.filter((row) => !generatedCommands.has(row.command)).length;
|
|
18732
19263
|
}
|
|
18733
|
-
function
|
|
19264
|
+
function mergeTaskMetadata2(primary, duplicate, input, mergedAt) {
|
|
18734
19265
|
const mergedDuplicates = Array.isArray(primary.metadata["merged_duplicates"]) ? [...primary.metadata["merged_duplicates"]] : [];
|
|
18735
19266
|
mergedDuplicates.push({
|
|
18736
19267
|
id: duplicate.id,
|
|
@@ -18791,7 +19322,7 @@ function mergeDuplicateTask(input, db) {
|
|
|
18791
19322
|
updateTask(primary.id, {
|
|
18792
19323
|
version: primary.version,
|
|
18793
19324
|
tags: mergedTags,
|
|
18794
|
-
metadata:
|
|
19325
|
+
metadata: mergeTaskMetadata2(primary, duplicate, input, mergedAt),
|
|
18795
19326
|
description: mergeTaskDescription(primary, duplicate) ?? undefined
|
|
18796
19327
|
}, d);
|
|
18797
19328
|
moved.comments = updateRows(d, "task_comments", "task_id", duplicate.id, primary.id);
|
|
@@ -25588,6 +26119,356 @@ var init_task_meta_tools = __esm(() => {
|
|
|
25588
26119
|
init_zod();
|
|
25589
26120
|
});
|
|
25590
26121
|
|
|
26122
|
+
// src/db/findings.ts
|
|
26123
|
+
function parseObject2(value) {
|
|
26124
|
+
if (!value)
|
|
26125
|
+
return {};
|
|
26126
|
+
try {
|
|
26127
|
+
const parsed = JSON.parse(value);
|
|
26128
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
26129
|
+
} catch {
|
|
26130
|
+
return {};
|
|
26131
|
+
}
|
|
26132
|
+
}
|
|
26133
|
+
function normalizeKey(value) {
|
|
26134
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
26135
|
+
}
|
|
26136
|
+
function normalizeFingerprint(value) {
|
|
26137
|
+
const normalized = normalizeKey(value);
|
|
26138
|
+
if (!normalized)
|
|
26139
|
+
throw new Error("finding fingerprint is required");
|
|
26140
|
+
return normalized.slice(0, 240);
|
|
26141
|
+
}
|
|
26142
|
+
function normalizeSeverity(value) {
|
|
26143
|
+
const normalized = normalizeKey(value || "medium");
|
|
26144
|
+
if (SEVERITIES.has(normalized))
|
|
26145
|
+
return normalized;
|
|
26146
|
+
if (/^(p0|blocker|urgent|highest)$/.test(normalized))
|
|
26147
|
+
return "critical";
|
|
26148
|
+
if (/^(p1|major)$/.test(normalized))
|
|
26149
|
+
return "high";
|
|
26150
|
+
if (/^(p3|minor|info)$/.test(normalized))
|
|
26151
|
+
return "low";
|
|
26152
|
+
return "medium";
|
|
26153
|
+
}
|
|
26154
|
+
function normalizeStatus(value) {
|
|
26155
|
+
const normalized = normalizeKey(value || "open");
|
|
26156
|
+
if (STATUSES.has(normalized))
|
|
26157
|
+
return normalized;
|
|
26158
|
+
if (normalized === "closed" || normalized === "fixed")
|
|
26159
|
+
return "resolved";
|
|
26160
|
+
return "open";
|
|
26161
|
+
}
|
|
26162
|
+
function normalizeResolutionStatus(value) {
|
|
26163
|
+
const status = normalizeStatus(value || "resolved");
|
|
26164
|
+
if (status === "open")
|
|
26165
|
+
throw new Error("resolve-missing status must be resolved or ignored");
|
|
26166
|
+
return status;
|
|
26167
|
+
}
|
|
26168
|
+
function redactOptional(value, max = 2000) {
|
|
26169
|
+
if (!value)
|
|
26170
|
+
return null;
|
|
26171
|
+
const redacted = redactEvidenceText(value).trim();
|
|
26172
|
+
if (!redacted)
|
|
26173
|
+
return null;
|
|
26174
|
+
return redacted.length > max ? `${redacted.slice(0, max - 3)}...` : redacted;
|
|
26175
|
+
}
|
|
26176
|
+
function rowToFinding(row) {
|
|
26177
|
+
return {
|
|
26178
|
+
schema_version: TASK_FINDING_SCHEMA_VERSION,
|
|
26179
|
+
...row,
|
|
26180
|
+
severity: normalizeSeverity(row.severity),
|
|
26181
|
+
status: normalizeStatus(row.status),
|
|
26182
|
+
metadata: parseObject2(row.metadata)
|
|
26183
|
+
};
|
|
26184
|
+
}
|
|
26185
|
+
function compactFinding(finding2) {
|
|
26186
|
+
return {
|
|
26187
|
+
schema_version: TASK_FINDING_SCHEMA_VERSION,
|
|
26188
|
+
id: finding2.id,
|
|
26189
|
+
task_id: finding2.task_id,
|
|
26190
|
+
run_id: finding2.run_id,
|
|
26191
|
+
fingerprint: finding2.fingerprint,
|
|
26192
|
+
title: finding2.title,
|
|
26193
|
+
severity: finding2.severity,
|
|
26194
|
+
status: finding2.status,
|
|
26195
|
+
source: finding2.source,
|
|
26196
|
+
summary: finding2.summary,
|
|
26197
|
+
artifact_path: finding2.artifact_path,
|
|
26198
|
+
first_seen_at: finding2.first_seen_at,
|
|
26199
|
+
last_seen_at: finding2.last_seen_at,
|
|
26200
|
+
resolved_at: finding2.resolved_at,
|
|
26201
|
+
metadata_keys: Object.keys(finding2.metadata).sort()
|
|
26202
|
+
};
|
|
26203
|
+
}
|
|
26204
|
+
function previewFinding(existing, next, timestamp3) {
|
|
26205
|
+
return {
|
|
26206
|
+
...existing,
|
|
26207
|
+
run_id: next.run_id,
|
|
26208
|
+
title: next.title,
|
|
26209
|
+
severity: next.severity,
|
|
26210
|
+
status: next.status,
|
|
26211
|
+
source: next.source,
|
|
26212
|
+
summary: next.summary,
|
|
26213
|
+
artifact_path: next.artifact_path,
|
|
26214
|
+
metadata: next.metadata,
|
|
26215
|
+
last_seen_at: timestamp3,
|
|
26216
|
+
resolved_at: next.status === "open" ? null : existing.resolved_at || timestamp3,
|
|
26217
|
+
updated_at: timestamp3
|
|
26218
|
+
};
|
|
26219
|
+
}
|
|
26220
|
+
function upsertAction(existing, next) {
|
|
26221
|
+
if (sameFinding(existing, next))
|
|
26222
|
+
return "matched";
|
|
26223
|
+
return existing.status !== "open" && next.status === "open" ? "reopened" : "updated";
|
|
26224
|
+
}
|
|
26225
|
+
function resolveRunForTask(runId, taskId, db) {
|
|
26226
|
+
if (!runId)
|
|
26227
|
+
return null;
|
|
26228
|
+
const resolved = resolveTaskRunId(runId, db);
|
|
26229
|
+
const run = getTaskRun(resolved, db);
|
|
26230
|
+
if (!run)
|
|
26231
|
+
throw new Error(`Run not found: ${runId}`);
|
|
26232
|
+
if (run.task_id !== taskId)
|
|
26233
|
+
throw new Error(`Run ${resolved} belongs to task ${run.task_id}, not ${taskId}`);
|
|
26234
|
+
return resolved;
|
|
26235
|
+
}
|
|
26236
|
+
function getFindingByFingerprint(taskId, fingerprint2, db) {
|
|
26237
|
+
const row = db.query("SELECT * FROM task_findings WHERE task_id = ? AND fingerprint = ?").get(taskId, fingerprint2);
|
|
26238
|
+
return row ? rowToFinding(row) : null;
|
|
26239
|
+
}
|
|
26240
|
+
function assertTask(taskId, db) {
|
|
26241
|
+
if (!getTask(taskId, db))
|
|
26242
|
+
throw new TaskNotFoundError(taskId);
|
|
26243
|
+
}
|
|
26244
|
+
function nextFinding(input, db) {
|
|
26245
|
+
const fingerprint2 = normalizeFingerprint(input.fingerprint);
|
|
26246
|
+
const title = redactOptional(input.title, 300);
|
|
26247
|
+
if (!title)
|
|
26248
|
+
throw new Error("finding title is required");
|
|
26249
|
+
return {
|
|
26250
|
+
fingerprint: fingerprint2,
|
|
26251
|
+
run_id: resolveRunForTask(input.run_id, input.task_id, db),
|
|
26252
|
+
title,
|
|
26253
|
+
severity: normalizeSeverity(input.severity),
|
|
26254
|
+
status: normalizeStatus(input.status),
|
|
26255
|
+
source: redactOptional(input.source, 120),
|
|
26256
|
+
summary: redactOptional(input.summary, 2000),
|
|
26257
|
+
artifact_path: redactOptional(input.artifact_path, 1000),
|
|
26258
|
+
metadata: redactValue(input.metadata || {})
|
|
26259
|
+
};
|
|
26260
|
+
}
|
|
26261
|
+
function sameFinding(left, right) {
|
|
26262
|
+
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);
|
|
26263
|
+
}
|
|
26264
|
+
function upsertTaskFinding(input, db) {
|
|
26265
|
+
const d = db || getDatabase();
|
|
26266
|
+
assertTask(input.task_id, d);
|
|
26267
|
+
const timestamp3 = input.observed_at || now();
|
|
26268
|
+
const warnings = [];
|
|
26269
|
+
const next = nextFinding(input, d);
|
|
26270
|
+
const existing = getFindingByFingerprint(input.task_id, next.fingerprint, d);
|
|
26271
|
+
const dryRun = !input.apply;
|
|
26272
|
+
if (dryRun) {
|
|
26273
|
+
const action2 = existing ? upsertAction(existing, next) : "preview";
|
|
26274
|
+
return {
|
|
26275
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
26276
|
+
local_only: true,
|
|
26277
|
+
dry_run: true,
|
|
26278
|
+
processed_at: timestamp3,
|
|
26279
|
+
action: action2,
|
|
26280
|
+
fingerprint: next.fingerprint,
|
|
26281
|
+
finding: existing ? compactFinding(previewFinding(existing, next, timestamp3)) : null,
|
|
26282
|
+
warnings
|
|
26283
|
+
};
|
|
26284
|
+
}
|
|
26285
|
+
if (!existing) {
|
|
26286
|
+
const id = uuid();
|
|
26287
|
+
d.run(`INSERT INTO task_findings (
|
|
26288
|
+
id, task_id, run_id, fingerprint, title, severity, status, source, summary, artifact_path,
|
|
26289
|
+
metadata, first_seen_at, last_seen_at, resolved_at, created_at, updated_at
|
|
26290
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
26291
|
+
id,
|
|
26292
|
+
input.task_id,
|
|
26293
|
+
next.run_id,
|
|
26294
|
+
next.fingerprint,
|
|
26295
|
+
next.title,
|
|
26296
|
+
next.severity,
|
|
26297
|
+
next.status,
|
|
26298
|
+
next.source,
|
|
26299
|
+
next.summary,
|
|
26300
|
+
next.artifact_path,
|
|
26301
|
+
JSON.stringify(next.metadata),
|
|
26302
|
+
timestamp3,
|
|
26303
|
+
timestamp3,
|
|
26304
|
+
next.status === "open" ? null : timestamp3,
|
|
26305
|
+
timestamp3,
|
|
26306
|
+
timestamp3
|
|
26307
|
+
]);
|
|
26308
|
+
return {
|
|
26309
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
26310
|
+
local_only: true,
|
|
26311
|
+
dry_run: false,
|
|
26312
|
+
processed_at: timestamp3,
|
|
26313
|
+
action: "created",
|
|
26314
|
+
fingerprint: next.fingerprint,
|
|
26315
|
+
finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
|
|
26316
|
+
warnings
|
|
26317
|
+
};
|
|
26318
|
+
}
|
|
26319
|
+
if (sameFinding(existing, next)) {
|
|
26320
|
+
return {
|
|
26321
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
26322
|
+
local_only: true,
|
|
26323
|
+
dry_run: false,
|
|
26324
|
+
processed_at: timestamp3,
|
|
26325
|
+
action: "matched",
|
|
26326
|
+
fingerprint: next.fingerprint,
|
|
26327
|
+
finding: compactFinding(existing),
|
|
26328
|
+
warnings
|
|
26329
|
+
};
|
|
26330
|
+
}
|
|
26331
|
+
const action = upsertAction(existing, next);
|
|
26332
|
+
d.run(`UPDATE task_findings SET
|
|
26333
|
+
run_id = ?, title = ?, severity = ?, status = ?, source = ?, summary = ?, artifact_path = ?,
|
|
26334
|
+
metadata = ?, last_seen_at = ?, resolved_at = ?, updated_at = ?
|
|
26335
|
+
WHERE id = ?`, [
|
|
26336
|
+
next.run_id,
|
|
26337
|
+
next.title,
|
|
26338
|
+
next.severity,
|
|
26339
|
+
next.status,
|
|
26340
|
+
next.source,
|
|
26341
|
+
next.summary,
|
|
26342
|
+
next.artifact_path,
|
|
26343
|
+
JSON.stringify(next.metadata),
|
|
26344
|
+
timestamp3,
|
|
26345
|
+
next.status === "open" ? null : existing.resolved_at || timestamp3,
|
|
26346
|
+
timestamp3,
|
|
26347
|
+
existing.id
|
|
26348
|
+
]);
|
|
26349
|
+
return {
|
|
26350
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
26351
|
+
local_only: true,
|
|
26352
|
+
dry_run: false,
|
|
26353
|
+
processed_at: timestamp3,
|
|
26354
|
+
action,
|
|
26355
|
+
fingerprint: next.fingerprint,
|
|
26356
|
+
finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
|
|
26357
|
+
warnings
|
|
26358
|
+
};
|
|
26359
|
+
}
|
|
26360
|
+
function listTaskFindings(filter = {}, db) {
|
|
26361
|
+
const d = db || getDatabase();
|
|
26362
|
+
const conditions = ["1=1"];
|
|
26363
|
+
const params = [];
|
|
26364
|
+
if (filter.task_id) {
|
|
26365
|
+
conditions.push("task_id = ?");
|
|
26366
|
+
params.push(filter.task_id);
|
|
26367
|
+
}
|
|
26368
|
+
if (filter.run_id) {
|
|
26369
|
+
conditions.push("run_id = ?");
|
|
26370
|
+
params.push(resolveTaskRunId(filter.run_id, d));
|
|
26371
|
+
}
|
|
26372
|
+
if (filter.status) {
|
|
26373
|
+
conditions.push("status = ?");
|
|
26374
|
+
params.push(normalizeStatus(filter.status));
|
|
26375
|
+
}
|
|
26376
|
+
if (filter.source) {
|
|
26377
|
+
conditions.push("source = ?");
|
|
26378
|
+
params.push(redactOptional(filter.source, 120));
|
|
26379
|
+
}
|
|
26380
|
+
const limit = Math.min(Math.max(Math.floor(filter.limit ?? 50), 1), 500);
|
|
26381
|
+
const rows = d.query(`SELECT * FROM task_findings WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC, created_at DESC LIMIT ?`).all(...[...params, limit]);
|
|
26382
|
+
return rows.map(rowToFinding);
|
|
26383
|
+
}
|
|
26384
|
+
function listCompactTaskFindings(filter = {}, db) {
|
|
26385
|
+
return listTaskFindings(filter, db).map(compactFinding);
|
|
26386
|
+
}
|
|
26387
|
+
function resolveMissingTaskFindings(input, db) {
|
|
26388
|
+
const d = db || getDatabase();
|
|
26389
|
+
assertTask(input.task_id, d);
|
|
26390
|
+
const timestamp3 = input.resolved_at || now();
|
|
26391
|
+
const status = normalizeResolutionStatus(input.status);
|
|
26392
|
+
const runId = resolveRunForTask(input.run_id, input.task_id, d);
|
|
26393
|
+
const present = new Set(input.fingerprints.map(normalizeFingerprint));
|
|
26394
|
+
const warnings = [];
|
|
26395
|
+
const conditions = ["task_id = ?", "status = 'open'"];
|
|
26396
|
+
const params = [input.task_id];
|
|
26397
|
+
if (input.source) {
|
|
26398
|
+
conditions.push("source = ?");
|
|
26399
|
+
params.push(redactOptional(input.source, 120));
|
|
26400
|
+
}
|
|
26401
|
+
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));
|
|
26402
|
+
const limit = Math.min(Math.max(Math.floor(input.limit ?? 50), 1), 200);
|
|
26403
|
+
const display = candidates.slice(0, limit);
|
|
26404
|
+
const omittedCount = Math.max(0, candidates.length - display.length);
|
|
26405
|
+
if (!input.apply) {
|
|
26406
|
+
return {
|
|
26407
|
+
schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
26408
|
+
local_only: true,
|
|
26409
|
+
dry_run: true,
|
|
26410
|
+
processed_at: timestamp3,
|
|
26411
|
+
action: candidates.length > 0 ? "preview" : "noop",
|
|
26412
|
+
task_id: input.task_id,
|
|
26413
|
+
source: input.source ? redactOptional(input.source, 120) : null,
|
|
26414
|
+
run_id: runId,
|
|
26415
|
+
present_fingerprint_count: present.size,
|
|
26416
|
+
candidate_count: candidates.length,
|
|
26417
|
+
changed_count: 0,
|
|
26418
|
+
omitted_count: omittedCount,
|
|
26419
|
+
findings: display.map(compactFinding),
|
|
26420
|
+
warnings
|
|
26421
|
+
};
|
|
26422
|
+
}
|
|
26423
|
+
const metadataPatch = redactValue({
|
|
26424
|
+
resolved_by: {
|
|
26425
|
+
agent_id: input.agent_id ?? null,
|
|
26426
|
+
run_id: runId,
|
|
26427
|
+
reason: input.reason ? redactEvidenceText(input.reason) : "missing from latest loop finding set"
|
|
26428
|
+
}
|
|
26429
|
+
});
|
|
26430
|
+
const tx = d.transaction(() => {
|
|
26431
|
+
for (const finding2 of candidates) {
|
|
26432
|
+
d.run("UPDATE task_findings SET status = ?, resolved_at = ?, metadata = ?, updated_at = ? WHERE id = ? AND status = 'open'", [
|
|
26433
|
+
status,
|
|
26434
|
+
timestamp3,
|
|
26435
|
+
JSON.stringify({ ...finding2.metadata, ...metadataPatch }),
|
|
26436
|
+
timestamp3,
|
|
26437
|
+
finding2.id
|
|
26438
|
+
]);
|
|
26439
|
+
}
|
|
26440
|
+
});
|
|
26441
|
+
tx();
|
|
26442
|
+
const updated = candidates.map((finding2) => d.query("SELECT * FROM task_findings WHERE id = ?").get(finding2.id)).filter((row) => Boolean(row)).map(rowToFinding);
|
|
26443
|
+
const visibleUpdated = updated.slice(0, limit);
|
|
26444
|
+
return {
|
|
26445
|
+
schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
26446
|
+
local_only: true,
|
|
26447
|
+
dry_run: false,
|
|
26448
|
+
processed_at: timestamp3,
|
|
26449
|
+
action: updated.length > 0 ? status : "noop",
|
|
26450
|
+
task_id: input.task_id,
|
|
26451
|
+
source: input.source ? redactOptional(input.source, 120) : null,
|
|
26452
|
+
run_id: runId,
|
|
26453
|
+
present_fingerprint_count: present.size,
|
|
26454
|
+
candidate_count: candidates.length,
|
|
26455
|
+
changed_count: updated.length,
|
|
26456
|
+
omitted_count: omittedCount,
|
|
26457
|
+
findings: visibleUpdated.map(compactFinding),
|
|
26458
|
+
warnings
|
|
26459
|
+
};
|
|
26460
|
+
}
|
|
26461
|
+
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;
|
|
26462
|
+
var init_findings = __esm(() => {
|
|
26463
|
+
init_redaction();
|
|
26464
|
+
init_types();
|
|
26465
|
+
init_database();
|
|
26466
|
+
init_tasks();
|
|
26467
|
+
init_task_runs();
|
|
26468
|
+
SEVERITIES = new Set(["low", "medium", "high", "critical"]);
|
|
26469
|
+
STATUSES = new Set(["open", "resolved", "ignored"]);
|
|
26470
|
+
});
|
|
26471
|
+
|
|
25591
26472
|
// src/lib/agent-run-dispatcher.ts
|
|
25592
26473
|
function dispatcherFromRun(run) {
|
|
25593
26474
|
const value = run.metadata["agent_run_dispatcher"];
|
|
@@ -26248,7 +27129,7 @@ function parseArray2(value) {
|
|
|
26248
27129
|
return [];
|
|
26249
27130
|
}
|
|
26250
27131
|
}
|
|
26251
|
-
function
|
|
27132
|
+
function parseObject3(value) {
|
|
26252
27133
|
try {
|
|
26253
27134
|
const parsed = JSON.parse(value || "{}");
|
|
26254
27135
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
@@ -26289,7 +27170,7 @@ function rowToKnowledgeRecord(row) {
|
|
|
26289
27170
|
agent_id: row.agent_id,
|
|
26290
27171
|
snapshot_id: row.snapshot_id,
|
|
26291
27172
|
tags: parseArray2(row.tags),
|
|
26292
|
-
metadata: redactValue(
|
|
27173
|
+
metadata: redactValue(parseObject3(row.metadata)),
|
|
26293
27174
|
created_at: row.created_at,
|
|
26294
27175
|
updated_at: row.updated_at
|
|
26295
27176
|
};
|
|
@@ -26508,7 +27389,7 @@ function parseArray3(value) {
|
|
|
26508
27389
|
return [];
|
|
26509
27390
|
}
|
|
26510
27391
|
}
|
|
26511
|
-
function
|
|
27392
|
+
function parseObject4(value) {
|
|
26512
27393
|
try {
|
|
26513
27394
|
const parsed = JSON.parse(value || "{}");
|
|
26514
27395
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
@@ -26557,7 +27438,7 @@ function rowToRisk(row) {
|
|
|
26557
27438
|
plan_id: row.plan_id,
|
|
26558
27439
|
task_id: row.task_id,
|
|
26559
27440
|
tags: parseArray3(row.tags),
|
|
26560
|
-
metadata: redactValue(
|
|
27441
|
+
metadata: redactValue(parseObject4(row.metadata)),
|
|
26561
27442
|
created_at: row.created_at,
|
|
26562
27443
|
updated_at: row.updated_at,
|
|
26563
27444
|
closed_at: row.closed_at
|
|
@@ -32399,6 +33280,38 @@ ${lines.join(`
|
|
|
32399
33280
|
}
|
|
32400
33281
|
});
|
|
32401
33282
|
}
|
|
33283
|
+
if (shouldRegisterTool("begin_task_run_transaction")) {
|
|
33284
|
+
server.tool("begin_task_run_transaction", "Preview or apply an idempotent local loop run transaction keyed by a stable loop/run id.", {
|
|
33285
|
+
task_id: exports_external.string().describe("Task ID"),
|
|
33286
|
+
key: exports_external.string().optional().describe("Stable idempotency key"),
|
|
33287
|
+
loop_id: exports_external.string().optional().describe("Loop identifier; used as key fallback"),
|
|
33288
|
+
loop_run_id: exports_external.string().optional().describe("Loop run identifier; used as key fallback"),
|
|
33289
|
+
agent_id: exports_external.string().optional().describe("Agent starting the run"),
|
|
33290
|
+
title: exports_external.string().optional().describe("Run title"),
|
|
33291
|
+
summary: exports_external.string().optional().describe("Run summary"),
|
|
33292
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata"),
|
|
33293
|
+
claim: exports_external.boolean().optional().describe("Claim/start the task before recording the run"),
|
|
33294
|
+
apply: exports_external.boolean().optional().describe("Apply the transaction; false or omitted is dry-run")
|
|
33295
|
+
}, async ({ task_id, key, loop_id, loop_run_id, agent_id, title, summary, metadata, claim, apply }) => {
|
|
33296
|
+
try {
|
|
33297
|
+
const result = beginTaskRunTransaction({
|
|
33298
|
+
task_id: resolveId(task_id),
|
|
33299
|
+
key,
|
|
33300
|
+
loop_id,
|
|
33301
|
+
loop_run_id,
|
|
33302
|
+
agent_id,
|
|
33303
|
+
title,
|
|
33304
|
+
summary,
|
|
33305
|
+
metadata,
|
|
33306
|
+
claim,
|
|
33307
|
+
apply
|
|
33308
|
+
});
|
|
33309
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
33310
|
+
} catch (e) {
|
|
33311
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
33312
|
+
}
|
|
33313
|
+
});
|
|
33314
|
+
}
|
|
32402
33315
|
if (shouldRegisterTool("list_task_runs")) {
|
|
32403
33316
|
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
33317
|
try {
|
|
@@ -32496,15 +33409,117 @@ ${lines.join(`
|
|
|
32496
33409
|
});
|
|
32497
33410
|
}
|
|
32498
33411
|
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
|
-
|
|
33412
|
+
server.tool("finish_task_run", "Finish a local run ledger entry as completed, failed, or cancelled. Supports idempotent lookup by key.", {
|
|
33413
|
+
run_id: exports_external.string().optional().describe("Run ID or prefix"),
|
|
33414
|
+
key: exports_external.string().optional().describe("Idempotency key when run_id is omitted"),
|
|
33415
|
+
task_id: exports_external.string().optional().describe("Task scope for key lookup"),
|
|
33416
|
+
status: exports_external.enum(["completed", "failed", "cancelled"]).optional().describe("Final run status"),
|
|
32502
33417
|
summary: exports_external.string().optional().describe("Final summary"),
|
|
32503
|
-
agent_id: exports_external.string().optional().describe("Agent finishing the run")
|
|
32504
|
-
|
|
33418
|
+
agent_id: exports_external.string().optional().describe("Agent finishing the run"),
|
|
33419
|
+
apply: exports_external.boolean().optional().describe("Apply the transaction; false previews without mutating")
|
|
33420
|
+
}, async ({ run_id, key, task_id, status, summary, agent_id, apply }) => {
|
|
33421
|
+
try {
|
|
33422
|
+
if (run_id && !key && apply === undefined) {
|
|
33423
|
+
const result2 = finishTaskRunTransaction({ run_id, status: status || "completed", summary, agent_id, apply: true });
|
|
33424
|
+
const run = result2.run ? getTaskRunLedger(result2.run.id).run : null;
|
|
33425
|
+
return { content: [{ type: "text", text: JSON.stringify(run, null, 2) }] };
|
|
33426
|
+
}
|
|
33427
|
+
const result = finishTaskRunTransaction({
|
|
33428
|
+
run_id,
|
|
33429
|
+
key,
|
|
33430
|
+
task_id: task_id ? resolveId(task_id) : undefined,
|
|
33431
|
+
status: status || "completed",
|
|
33432
|
+
summary,
|
|
33433
|
+
agent_id,
|
|
33434
|
+
apply: apply !== false
|
|
33435
|
+
});
|
|
33436
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
33437
|
+
} catch (e) {
|
|
33438
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
33439
|
+
}
|
|
33440
|
+
});
|
|
33441
|
+
}
|
|
33442
|
+
if (shouldRegisterTool("upsert_task_finding")) {
|
|
33443
|
+
server.tool("upsert_task_finding", "Preview or apply an idempotent local finding upsert scoped by task and fingerprint.", {
|
|
33444
|
+
task_id: exports_external.string().describe("Task ID"),
|
|
33445
|
+
fingerprint: exports_external.string().describe("Stable finding fingerprint"),
|
|
33446
|
+
title: exports_external.string().describe("Finding title"),
|
|
33447
|
+
severity: exports_external.enum(["low", "medium", "high", "critical"]).optional().describe("Finding severity"),
|
|
33448
|
+
status: exports_external.enum(["open", "resolved", "ignored"]).optional().describe("Finding status"),
|
|
33449
|
+
source: exports_external.string().optional().describe("Loop/tool source name"),
|
|
33450
|
+
summary: exports_external.string().optional().describe("Bounded finding summary"),
|
|
33451
|
+
artifact_path: exports_external.string().optional().describe("Local artifact path/reference; content is not read"),
|
|
33452
|
+
run_id: exports_external.string().optional().describe("Run ledger ID or prefix"),
|
|
33453
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata"),
|
|
33454
|
+
apply: exports_external.boolean().optional().describe("Apply the upsert; false or omitted is dry-run")
|
|
33455
|
+
}, async ({ task_id, fingerprint: fingerprint3, title, severity, status, source: source3, summary, artifact_path, run_id, metadata, apply }) => {
|
|
32505
33456
|
try {
|
|
32506
|
-
const
|
|
32507
|
-
|
|
33457
|
+
const result = upsertTaskFinding({
|
|
33458
|
+
task_id: resolveId(task_id),
|
|
33459
|
+
fingerprint: fingerprint3,
|
|
33460
|
+
title,
|
|
33461
|
+
severity,
|
|
33462
|
+
status,
|
|
33463
|
+
source: source3,
|
|
33464
|
+
summary,
|
|
33465
|
+
artifact_path,
|
|
33466
|
+
run_id,
|
|
33467
|
+
metadata,
|
|
33468
|
+
apply
|
|
33469
|
+
});
|
|
33470
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
33471
|
+
} catch (e) {
|
|
33472
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
33473
|
+
}
|
|
33474
|
+
});
|
|
33475
|
+
}
|
|
33476
|
+
if (shouldRegisterTool("list_task_findings")) {
|
|
33477
|
+
server.tool("list_task_findings", "List compact local findings with bounded output.", {
|
|
33478
|
+
task_id: exports_external.string().optional().describe("Filter by task"),
|
|
33479
|
+
run_id: exports_external.string().optional().describe("Filter by run"),
|
|
33480
|
+
status: exports_external.enum(["open", "resolved", "ignored"]).optional().describe("Filter by status"),
|
|
33481
|
+
source: exports_external.string().optional().describe("Filter by source"),
|
|
33482
|
+
limit: exports_external.number().optional().describe("Maximum findings to return")
|
|
33483
|
+
}, async ({ task_id, run_id, status, source: source3, limit }) => {
|
|
33484
|
+
try {
|
|
33485
|
+
const findings = listCompactTaskFindings({
|
|
33486
|
+
task_id: task_id ? resolveId(task_id) : undefined,
|
|
33487
|
+
run_id,
|
|
33488
|
+
status,
|
|
33489
|
+
source: source3,
|
|
33490
|
+
limit
|
|
33491
|
+
});
|
|
33492
|
+
return { content: [{ type: "text", text: JSON.stringify(findings) }] };
|
|
33493
|
+
} catch (e) {
|
|
33494
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
33495
|
+
}
|
|
33496
|
+
});
|
|
33497
|
+
}
|
|
33498
|
+
if (shouldRegisterTool("resolve_missing_task_findings")) {
|
|
33499
|
+
server.tool("resolve_missing_task_findings", "Resolve open findings absent from the latest loop finding set.", {
|
|
33500
|
+
task_id: exports_external.string().describe("Task ID"),
|
|
33501
|
+
fingerprints: exports_external.array(exports_external.string()).optional().describe("Fingerprints still present in the latest run"),
|
|
33502
|
+
source: exports_external.string().optional().describe("Only resolve findings from this source"),
|
|
33503
|
+
run_id: exports_external.string().optional().describe("Run ledger ID or prefix for audit metadata"),
|
|
33504
|
+
status: exports_external.enum(["resolved", "ignored"]).optional().describe("Resolution status"),
|
|
33505
|
+
agent_id: exports_external.string().optional().describe("Agent resolving findings"),
|
|
33506
|
+
reason: exports_external.string().optional().describe("Resolution reason"),
|
|
33507
|
+
limit: exports_external.number().optional().describe("Maximum findings returned"),
|
|
33508
|
+
apply: exports_external.boolean().optional().describe("Apply resolution; false or omitted is dry-run")
|
|
33509
|
+
}, async ({ task_id, fingerprints, source: source3, run_id, status, agent_id, reason, limit, apply }) => {
|
|
33510
|
+
try {
|
|
33511
|
+
const result = resolveMissingTaskFindings({
|
|
33512
|
+
task_id: resolveId(task_id),
|
|
33513
|
+
fingerprints: fingerprints || [],
|
|
33514
|
+
source: source3,
|
|
33515
|
+
run_id,
|
|
33516
|
+
status,
|
|
33517
|
+
agent_id,
|
|
33518
|
+
reason,
|
|
33519
|
+
limit,
|
|
33520
|
+
apply
|
|
33521
|
+
});
|
|
33522
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
32508
33523
|
} catch (e) {
|
|
32509
33524
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
32510
33525
|
}
|
|
@@ -32840,6 +33855,7 @@ var init_task_resources = __esm(() => {
|
|
|
32840
33855
|
init_agents();
|
|
32841
33856
|
init_task_commits();
|
|
32842
33857
|
init_task_runs();
|
|
33858
|
+
init_findings();
|
|
32843
33859
|
init_agent_run_dispatcher();
|
|
32844
33860
|
init_verification_providers();
|
|
32845
33861
|
init_release_notes();
|
|
@@ -37384,6 +38400,39 @@ async function handleCreateTask(req, ctx, json2, taskToSummary2) {
|
|
|
37384
38400
|
return json2({ error: e instanceof Error ? e.message : "Failed to create task" }, 500);
|
|
37385
38401
|
}
|
|
37386
38402
|
}
|
|
38403
|
+
async function handleUpsertTask(req, ctx, json2, taskToSummary2) {
|
|
38404
|
+
try {
|
|
38405
|
+
const body = await req.json();
|
|
38406
|
+
if (typeof body["fingerprint"] !== "string" || body["fingerprint"].trim() === "") {
|
|
38407
|
+
return json2({ error: "Missing 'fingerprint'" }, 400);
|
|
38408
|
+
}
|
|
38409
|
+
if (typeof body["title"] !== "string" || body["title"].trim() === "") {
|
|
38410
|
+
return json2({ error: "Missing 'title'" }, 400);
|
|
38411
|
+
}
|
|
38412
|
+
const metadata = body["metadata"] && typeof body["metadata"] === "object" && !Array.isArray(body["metadata"]) ? { ...body["metadata"] } : {};
|
|
38413
|
+
for (const key of ["expectation_id", "expectation_fingerprint", "evidence_paths", "origin_loop_id", "origin_run_id", "expected", "observed", "acceptance"]) {
|
|
38414
|
+
if (body[key] !== undefined)
|
|
38415
|
+
metadata[key] = body[key];
|
|
38416
|
+
}
|
|
38417
|
+
const result = upsertTaskByFingerprint({
|
|
38418
|
+
fingerprint: body["fingerprint"],
|
|
38419
|
+
title: body["title"],
|
|
38420
|
+
description: typeof body["description"] === "string" ? body["description"] : undefined,
|
|
38421
|
+
status: body["status"],
|
|
38422
|
+
priority: body["priority"],
|
|
38423
|
+
project_id: typeof body["project_id"] === "string" ? body["project_id"] : undefined,
|
|
38424
|
+
task_list_id: typeof body["task_list_id"] === "string" ? body["task_list_id"] : undefined,
|
|
38425
|
+
assigned_to: typeof body["assigned_to"] === "string" ? body["assigned_to"] : undefined,
|
|
38426
|
+
working_dir: typeof body["working_dir"] === "string" ? body["working_dir"] : undefined,
|
|
38427
|
+
tags: Array.isArray(body["tags"]) ? body["tags"].filter((tag) => typeof tag === "string") : undefined,
|
|
38428
|
+
metadata
|
|
38429
|
+
});
|
|
38430
|
+
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 });
|
|
38431
|
+
return json2({ created: result.created, task: taskToSummary2(result.task) }, result.created ? 201 : 200);
|
|
38432
|
+
} catch (e) {
|
|
38433
|
+
return json2({ error: e instanceof Error ? e.message : "Failed to upsert task" }, 500);
|
|
38434
|
+
}
|
|
38435
|
+
}
|
|
37387
38436
|
function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
|
|
37388
38437
|
const format = url.searchParams.get("format") || "json";
|
|
37389
38438
|
const status = url.searchParams.get("status") || undefined;
|
|
@@ -38064,8 +39113,10 @@ function taskToSummary(task2, fields) {
|
|
|
38064
39113
|
task_list_id: task2.task_list_id,
|
|
38065
39114
|
agent_id: task2.agent_id,
|
|
38066
39115
|
assigned_to: task2.assigned_to,
|
|
39116
|
+
working_dir: task2.working_dir,
|
|
38067
39117
|
locked_by: task2.locked_by,
|
|
38068
39118
|
tags: task2.tags,
|
|
39119
|
+
metadata: task2.metadata,
|
|
38069
39120
|
version: task2.version,
|
|
38070
39121
|
created_at: task2.created_at,
|
|
38071
39122
|
updated_at: task2.updated_at,
|
|
@@ -38207,6 +39258,9 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
38207
39258
|
if (path === "/api/tasks" && method === "POST") {
|
|
38208
39259
|
return handleCreateTask(req, ctx, jsonWithCors, taskToSummary);
|
|
38209
39260
|
}
|
|
39261
|
+
if (path === "/api/tasks/upsert" && method === "POST") {
|
|
39262
|
+
return handleUpsertTask(req, ctx, jsonWithCors, taskToSummary);
|
|
39263
|
+
}
|
|
38210
39264
|
if (path === "/api/tasks/export" && method === "GET") {
|
|
38211
39265
|
return handleTasksExport(req, url, ctx, jsonWithCors, taskToSummary);
|
|
38212
39266
|
}
|
|
@@ -38402,7 +39456,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
38402
39456
|
} catch {}
|
|
38403
39457
|
}
|
|
38404
39458
|
}
|
|
38405
|
-
var MIME_TYPES, SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX
|
|
39459
|
+
var MIME_TYPES, SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX;
|
|
38406
39460
|
var init_serve = __esm(() => {
|
|
38407
39461
|
init_database();
|
|
38408
39462
|
init_api_keys();
|
|
@@ -38427,6 +39481,7 @@ var init_serve = __esm(() => {
|
|
|
38427
39481
|
"Permissions-Policy": "camera=, microphone=, geolocation="
|
|
38428
39482
|
};
|
|
38429
39483
|
rateLimitMap = new Map;
|
|
39484
|
+
RATE_LIMIT_MAX = Number.parseInt(process.env["TODOS_RATE_LIMIT_MAX"] || "120", 10);
|
|
38430
39485
|
});
|
|
38431
39486
|
|
|
38432
39487
|
// src/mcp/index.ts
|