@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/storage.js
CHANGED
|
@@ -1269,6 +1269,49 @@ var init_migrations = __esm(() => {
|
|
|
1269
1269
|
CREATE INDEX IF NOT EXISTS idx_local_retrospectives_plan ON local_retrospectives(plan_id);
|
|
1270
1270
|
CREATE INDEX IF NOT EXISTS idx_local_retrospectives_agent ON local_retrospectives(agent_id);
|
|
1271
1271
|
INSERT OR IGNORE INTO _migrations (id) VALUES (61);
|
|
1272
|
+
`,
|
|
1273
|
+
`
|
|
1274
|
+
CREATE TABLE IF NOT EXISTS task_run_transactions (
|
|
1275
|
+
id TEXT PRIMARY KEY,
|
|
1276
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1277
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1278
|
+
key TEXT NOT NULL,
|
|
1279
|
+
loop_id TEXT,
|
|
1280
|
+
loop_run_id TEXT,
|
|
1281
|
+
metadata TEXT DEFAULT '{}',
|
|
1282
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1283
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1284
|
+
UNIQUE(task_id, key)
|
|
1285
|
+
);
|
|
1286
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_transactions_task_key ON task_run_transactions(task_id, key);
|
|
1287
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_transactions_key ON task_run_transactions(key);
|
|
1288
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_transactions_run ON task_run_transactions(run_id);
|
|
1289
|
+
|
|
1290
|
+
CREATE TABLE IF NOT EXISTS task_findings (
|
|
1291
|
+
id TEXT PRIMARY KEY,
|
|
1292
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1293
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1294
|
+
fingerprint TEXT NOT NULL,
|
|
1295
|
+
title TEXT NOT NULL,
|
|
1296
|
+
severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low', 'medium', 'high', 'critical')),
|
|
1297
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'resolved', 'ignored')),
|
|
1298
|
+
source TEXT,
|
|
1299
|
+
summary TEXT,
|
|
1300
|
+
artifact_path TEXT,
|
|
1301
|
+
metadata TEXT DEFAULT '{}',
|
|
1302
|
+
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1303
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1304
|
+
resolved_at TEXT,
|
|
1305
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1306
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1307
|
+
UNIQUE(task_id, fingerprint)
|
|
1308
|
+
);
|
|
1309
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_task ON task_findings(task_id);
|
|
1310
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_run ON task_findings(run_id);
|
|
1311
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_status ON task_findings(status);
|
|
1312
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source);
|
|
1313
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint);
|
|
1314
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (62);
|
|
1272
1315
|
`
|
|
1273
1316
|
];
|
|
1274
1317
|
});
|
|
@@ -1706,6 +1749,47 @@ function ensureSchema(db) {
|
|
|
1706
1749
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_run ON task_run_artifacts(run_id)");
|
|
1707
1750
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_task ON task_run_artifacts(task_id)");
|
|
1708
1751
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_path ON task_run_artifacts(path)");
|
|
1752
|
+
ensureTable("task_run_transactions", `
|
|
1753
|
+
CREATE TABLE task_run_transactions (
|
|
1754
|
+
id TEXT PRIMARY KEY,
|
|
1755
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1756
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1757
|
+
key TEXT NOT NULL,
|
|
1758
|
+
loop_id TEXT,
|
|
1759
|
+
loop_run_id TEXT,
|
|
1760
|
+
metadata TEXT DEFAULT '{}',
|
|
1761
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1762
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1763
|
+
UNIQUE(task_id, key)
|
|
1764
|
+
)`);
|
|
1765
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_task_key ON task_run_transactions(task_id, key)");
|
|
1766
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_key ON task_run_transactions(key)");
|
|
1767
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_run ON task_run_transactions(run_id)");
|
|
1768
|
+
ensureTable("task_findings", `
|
|
1769
|
+
CREATE TABLE task_findings (
|
|
1770
|
+
id TEXT PRIMARY KEY,
|
|
1771
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1772
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1773
|
+
fingerprint TEXT NOT NULL,
|
|
1774
|
+
title TEXT NOT NULL,
|
|
1775
|
+
severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low', 'medium', 'high', 'critical')),
|
|
1776
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'resolved', 'ignored')),
|
|
1777
|
+
source TEXT,
|
|
1778
|
+
summary TEXT,
|
|
1779
|
+
artifact_path TEXT,
|
|
1780
|
+
metadata TEXT DEFAULT '{}',
|
|
1781
|
+
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1782
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1783
|
+
resolved_at TEXT,
|
|
1784
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1785
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1786
|
+
UNIQUE(task_id, fingerprint)
|
|
1787
|
+
)`);
|
|
1788
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_task ON task_findings(task_id)");
|
|
1789
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_run ON task_findings(run_id)");
|
|
1790
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_status ON task_findings(status)");
|
|
1791
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source)");
|
|
1792
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint)");
|
|
1709
1793
|
ensureTable("inbox_items", `
|
|
1710
1794
|
CREATE TABLE inbox_items (
|
|
1711
1795
|
id TEXT PRIMARY KEY,
|
|
@@ -4180,6 +4264,7 @@ var LOCAL_EVENT_TYPES = [
|
|
|
4180
4264
|
"task.blocked",
|
|
4181
4265
|
"task.started",
|
|
4182
4266
|
"task.completed",
|
|
4267
|
+
"task.updated",
|
|
4183
4268
|
"task.due",
|
|
4184
4269
|
"task.due_soon",
|
|
4185
4270
|
"task.failed",
|
|
@@ -4418,7 +4503,7 @@ async function testLocalEventHook(name, input) {
|
|
|
4418
4503
|
return emitLocalEventHooks({ ...input, hooks: [hook] });
|
|
4419
4504
|
}
|
|
4420
4505
|
|
|
4421
|
-
// node_modules/.bun/@hasna+events@0.1.
|
|
4506
|
+
// node_modules/.bun/@hasna+events@0.1.10/node_modules/@hasna/events/dist/index.js
|
|
4422
4507
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
4423
4508
|
import { existsSync as existsSync5 } from "fs";
|
|
4424
4509
|
import { homedir } from "os";
|
|
@@ -4465,14 +4550,40 @@ function matchRecord(input, matcher) {
|
|
|
4465
4550
|
return true;
|
|
4466
4551
|
return Object.entries(matcher).every(([path, expected]) => {
|
|
4467
4552
|
const actual = getPathValue(input, path);
|
|
4468
|
-
|
|
4469
|
-
return matchString(actual === undefined ? undefined : String(actual), expected, {
|
|
4470
|
-
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
4471
|
-
});
|
|
4472
|
-
}
|
|
4473
|
-
return actual === expected;
|
|
4553
|
+
return matchField(actual, expected, path);
|
|
4474
4554
|
});
|
|
4475
4555
|
}
|
|
4556
|
+
function matchField(actual, expected, path) {
|
|
4557
|
+
if (isNegativeMatcher(expected)) {
|
|
4558
|
+
return !matchPositiveField(actual, expected.not, path);
|
|
4559
|
+
}
|
|
4560
|
+
return matchPositiveField(actual, expected, path);
|
|
4561
|
+
}
|
|
4562
|
+
function matchPositiveField(actual, expected, path) {
|
|
4563
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
4564
|
+
return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
|
|
4565
|
+
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
4566
|
+
}));
|
|
4567
|
+
}
|
|
4568
|
+
if (Array.isArray(actual)) {
|
|
4569
|
+
return actual.some((item) => item === expected);
|
|
4570
|
+
}
|
|
4571
|
+
return actual === expected;
|
|
4572
|
+
}
|
|
4573
|
+
function stringCandidates(actual) {
|
|
4574
|
+
if (actual === undefined)
|
|
4575
|
+
return [];
|
|
4576
|
+
if (Array.isArray(actual)) {
|
|
4577
|
+
return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
|
|
4578
|
+
}
|
|
4579
|
+
return [String(actual)];
|
|
4580
|
+
}
|
|
4581
|
+
function isPrimitiveFieldValue(value) {
|
|
4582
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
4583
|
+
}
|
|
4584
|
+
function isNegativeMatcher(value) {
|
|
4585
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
|
|
4586
|
+
}
|
|
4476
4587
|
function eventMatchesFilter(event, filter) {
|
|
4477
4588
|
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);
|
|
4478
4589
|
}
|
|
@@ -5079,9 +5190,66 @@ function taskEventData(task, extra = {}) {
|
|
|
5079
5190
|
started_at: task.started_at,
|
|
5080
5191
|
completed_at: task.completed_at,
|
|
5081
5192
|
due_at: task.due_at,
|
|
5193
|
+
requires_approval: task.requires_approval,
|
|
5194
|
+
approved_by: task.approved_by,
|
|
5195
|
+
approved_at: task.approved_at,
|
|
5082
5196
|
...extra
|
|
5083
5197
|
};
|
|
5084
5198
|
}
|
|
5199
|
+
function booleanField(value) {
|
|
5200
|
+
if (typeof value === "boolean")
|
|
5201
|
+
return value;
|
|
5202
|
+
if (typeof value === "number") {
|
|
5203
|
+
if (value === 1)
|
|
5204
|
+
return true;
|
|
5205
|
+
if (value === 0)
|
|
5206
|
+
return false;
|
|
5207
|
+
}
|
|
5208
|
+
if (typeof value === "string") {
|
|
5209
|
+
const normalized = value.trim().toLowerCase();
|
|
5210
|
+
if (["true", "1", "yes", "on"].includes(normalized))
|
|
5211
|
+
return true;
|
|
5212
|
+
if (["false", "0", "no", "off"].includes(normalized))
|
|
5213
|
+
return false;
|
|
5214
|
+
}
|
|
5215
|
+
return;
|
|
5216
|
+
}
|
|
5217
|
+
function objectField(value) {
|
|
5218
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
5219
|
+
}
|
|
5220
|
+
function firstBoolean(records, keys) {
|
|
5221
|
+
for (const record of records) {
|
|
5222
|
+
for (const key of keys) {
|
|
5223
|
+
const value = booleanField(record[key]);
|
|
5224
|
+
if (value !== undefined)
|
|
5225
|
+
return value;
|
|
5226
|
+
}
|
|
5227
|
+
}
|
|
5228
|
+
return;
|
|
5229
|
+
}
|
|
5230
|
+
function routingAutomationMetadata(task) {
|
|
5231
|
+
const automation = objectField(task.metadata.automation);
|
|
5232
|
+
const records = [task.metadata];
|
|
5233
|
+
if (automation)
|
|
5234
|
+
records.push(automation);
|
|
5235
|
+
const result = {};
|
|
5236
|
+
const aliases = [
|
|
5237
|
+
["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
|
|
5238
|
+
["no_auto", ["no_auto", "noAuto"]],
|
|
5239
|
+
["manual", ["manual"]],
|
|
5240
|
+
["manual_required", ["manual_required", "manualRequired"]],
|
|
5241
|
+
["requires_approval", ["requires_approval", "requiresApproval"]],
|
|
5242
|
+
["approval_required", ["approval_required", "approvalRequired"]]
|
|
5243
|
+
];
|
|
5244
|
+
for (const [canonical, keys] of aliases) {
|
|
5245
|
+
const value = firstBoolean(records, keys);
|
|
5246
|
+
if (value !== undefined)
|
|
5247
|
+
result[canonical] = value;
|
|
5248
|
+
}
|
|
5249
|
+
if (task.requires_approval)
|
|
5250
|
+
result.requires_approval = true;
|
|
5251
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
5252
|
+
}
|
|
5085
5253
|
function taskEventMetadata(task) {
|
|
5086
5254
|
const metadata = {
|
|
5087
5255
|
package: "@hasna/todos",
|
|
@@ -5092,6 +5260,14 @@ function taskEventMetadata(task) {
|
|
|
5092
5260
|
task_list_id: task.task_list_id,
|
|
5093
5261
|
working_dir: task.working_dir
|
|
5094
5262
|
};
|
|
5263
|
+
const routeEnabled = booleanField(task.metadata.route_enabled);
|
|
5264
|
+
if (routeEnabled !== undefined) {
|
|
5265
|
+
metadata.route_enabled = routeEnabled;
|
|
5266
|
+
}
|
|
5267
|
+
const automation = routingAutomationMetadata(task);
|
|
5268
|
+
if (automation) {
|
|
5269
|
+
metadata.automation = automation;
|
|
5270
|
+
}
|
|
5095
5271
|
try {
|
|
5096
5272
|
const project = task.project_id ? getProject(task.project_id) : null;
|
|
5097
5273
|
const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
|
|
@@ -5109,9 +5285,6 @@ function taskEventMetadata(task) {
|
|
|
5109
5285
|
if (projectPath) {
|
|
5110
5286
|
metadata.project_kind = classifyProjectKind(projectPath);
|
|
5111
5287
|
metadata.project_is_worktree = isWorktreePath(projectPath);
|
|
5112
|
-
if (typeof task.metadata.route_enabled === "boolean") {
|
|
5113
|
-
metadata.route_enabled = task.metadata.route_enabled;
|
|
5114
|
-
}
|
|
5115
5288
|
metadata.working_dir = task.working_dir ?? projectPath;
|
|
5116
5289
|
}
|
|
5117
5290
|
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;
|
|
@@ -5518,6 +5691,17 @@ function replaceTaskTags(taskId, tags, db) {
|
|
|
5518
5691
|
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
5519
5692
|
insertTaskTags(taskId, tags, db);
|
|
5520
5693
|
}
|
|
5694
|
+
function addMetadataConditions(metadata, conditions, params) {
|
|
5695
|
+
if (!metadata)
|
|
5696
|
+
return;
|
|
5697
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
5698
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
|
|
5699
|
+
throw new Error(`Invalid metadata filter key: ${key}`);
|
|
5700
|
+
}
|
|
5701
|
+
conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
|
|
5702
|
+
params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
|
|
5703
|
+
}
|
|
5704
|
+
}
|
|
5521
5705
|
function createTask(input, db) {
|
|
5522
5706
|
const d = db || getDatabase();
|
|
5523
5707
|
const timestamp = now();
|
|
@@ -5700,6 +5884,7 @@ function listTasks(filter = {}, db) {
|
|
|
5700
5884
|
params.push(filter.task_type);
|
|
5701
5885
|
}
|
|
5702
5886
|
}
|
|
5887
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
5703
5888
|
const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
|
|
5704
5889
|
if (filter.cursor) {
|
|
5705
5890
|
try {
|
|
@@ -5724,6 +5909,54 @@ function listTasks(filter = {}, db) {
|
|
|
5724
5909
|
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
|
|
5725
5910
|
return rows.map(rowToTask);
|
|
5726
5911
|
}
|
|
5912
|
+
function getTaskByFingerprint(fingerprint, db) {
|
|
5913
|
+
const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
|
|
5914
|
+
return tasks[0] ?? null;
|
|
5915
|
+
}
|
|
5916
|
+
function mergeTaskMetadata(current, next, fingerprint) {
|
|
5917
|
+
return {
|
|
5918
|
+
...current,
|
|
5919
|
+
...next ?? {},
|
|
5920
|
+
fingerprint
|
|
5921
|
+
};
|
|
5922
|
+
}
|
|
5923
|
+
function upsertTaskByFingerprint(input, db) {
|
|
5924
|
+
const d = db || getDatabase();
|
|
5925
|
+
const fingerprint = input.fingerprint.trim();
|
|
5926
|
+
if (!fingerprint)
|
|
5927
|
+
throw new Error("fingerprint is required");
|
|
5928
|
+
const existing = getTaskByFingerprint(fingerprint, d);
|
|
5929
|
+
const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
|
|
5930
|
+
if (!existing) {
|
|
5931
|
+
const task2 = createTask({ ...input, metadata }, d);
|
|
5932
|
+
return { task: task2, created: true };
|
|
5933
|
+
}
|
|
5934
|
+
const task = updateTask(existing.id, {
|
|
5935
|
+
version: existing.version,
|
|
5936
|
+
title: input.title,
|
|
5937
|
+
description: input.description,
|
|
5938
|
+
status: input.status,
|
|
5939
|
+
priority: input.priority,
|
|
5940
|
+
project_id: input.project_id,
|
|
5941
|
+
assigned_to: input.assigned_to,
|
|
5942
|
+
working_dir: input.working_dir,
|
|
5943
|
+
plan_id: input.plan_id,
|
|
5944
|
+
task_list_id: input.task_list_id,
|
|
5945
|
+
tags: input.tags,
|
|
5946
|
+
metadata,
|
|
5947
|
+
due_at: input.due_at,
|
|
5948
|
+
estimated_minutes: input.estimated_minutes,
|
|
5949
|
+
sla_minutes: input.sla_minutes,
|
|
5950
|
+
confidence: input.confidence,
|
|
5951
|
+
retry_count: input.retry_count,
|
|
5952
|
+
max_retries: input.max_retries,
|
|
5953
|
+
retry_after: input.retry_after,
|
|
5954
|
+
requires_approval: input.requires_approval,
|
|
5955
|
+
recurrence_rule: input.recurrence_rule,
|
|
5956
|
+
task_type: input.task_type
|
|
5957
|
+
}, d);
|
|
5958
|
+
return { task, created: false };
|
|
5959
|
+
}
|
|
5727
5960
|
function countTasks(filter = {}, db) {
|
|
5728
5961
|
const d = db || getDatabase();
|
|
5729
5962
|
const conditions = [];
|
|
@@ -5787,6 +6020,7 @@ function countTasks(filter = {}, db) {
|
|
|
5787
6020
|
conditions.push("task_list_id = ?");
|
|
5788
6021
|
params.push(filter.task_list_id);
|
|
5789
6022
|
}
|
|
6023
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
5790
6024
|
if (!filter.include_archived) {
|
|
5791
6025
|
conditions.push("archived_at IS NULL");
|
|
5792
6026
|
}
|
|
@@ -5837,6 +6071,10 @@ function updateTask(id, input, db) {
|
|
|
5837
6071
|
sets.push("assigned_to = ?");
|
|
5838
6072
|
params.push(input.assigned_to);
|
|
5839
6073
|
}
|
|
6074
|
+
if (input.working_dir !== undefined) {
|
|
6075
|
+
sets.push("working_dir = ?");
|
|
6076
|
+
params.push(input.working_dir);
|
|
6077
|
+
}
|
|
5840
6078
|
if (input.tags !== undefined) {
|
|
5841
6079
|
sets.push("tags = ?");
|
|
5842
6080
|
params.push(JSON.stringify(input.tags));
|
|
@@ -5925,6 +6163,8 @@ function updateTask(id, input, db) {
|
|
|
5925
6163
|
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
5926
6164
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
5927
6165
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
6166
|
+
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
6167
|
+
logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
5928
6168
|
if (input.approved_by !== undefined)
|
|
5929
6169
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
5930
6170
|
const updatedTask = {
|
|
@@ -5960,6 +6200,10 @@ function updateTask(id, input, db) {
|
|
|
5960
6200
|
if (input.approved_by !== undefined) {
|
|
5961
6201
|
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
5962
6202
|
}
|
|
6203
|
+
const updatePayload = taskEventData(updatedTask);
|
|
6204
|
+
dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
|
|
6205
|
+
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
|
|
6206
|
+
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
|
|
5963
6207
|
return updatedTask;
|
|
5964
6208
|
}
|
|
5965
6209
|
function deleteTask(id, db) {
|
|
@@ -8655,6 +8899,7 @@ function getTaskTraceability(taskId, db) {
|
|
|
8655
8899
|
|
|
8656
8900
|
// src/db/task-runs.ts
|
|
8657
8901
|
init_redaction();
|
|
8902
|
+
var LOOP_RUN_TRANSACTION_SCHEMA_VERSION = "todos.loop_run_transaction.v1";
|
|
8658
8903
|
function parseObject(value) {
|
|
8659
8904
|
if (!value)
|
|
8660
8905
|
return {};
|
|
@@ -8677,6 +8922,72 @@ function rowToArtifact(row) {
|
|
|
8677
8922
|
function getRunRow(runId, db) {
|
|
8678
8923
|
return db.query("SELECT * FROM task_runs WHERE id = ?").get(runId);
|
|
8679
8924
|
}
|
|
8925
|
+
function normalizeTransactionKey(input) {
|
|
8926
|
+
const key = (input.key || input.loop_run_id || input.loop_id || "").trim();
|
|
8927
|
+
if (!key)
|
|
8928
|
+
throw new Error("idempotent run transactions require --key, --loop-run-id, or --loop-id");
|
|
8929
|
+
return key.toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 240);
|
|
8930
|
+
}
|
|
8931
|
+
function loopTransactionMetadata(record) {
|
|
8932
|
+
const value = record.metadata["loop_transaction"];
|
|
8933
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
8934
|
+
}
|
|
8935
|
+
function runKey(record) {
|
|
8936
|
+
const tx = loopTransactionMetadata(record);
|
|
8937
|
+
const key = tx["idempotency_key"] ?? record.metadata["idempotency_key"];
|
|
8938
|
+
return typeof key === "string" ? key : null;
|
|
8939
|
+
}
|
|
8940
|
+
function loopId(record) {
|
|
8941
|
+
const tx = loopTransactionMetadata(record);
|
|
8942
|
+
const value = tx["loop_id"] ?? record.metadata["loop_id"];
|
|
8943
|
+
return typeof value === "string" ? value : null;
|
|
8944
|
+
}
|
|
8945
|
+
function loopRunId(record) {
|
|
8946
|
+
const tx = loopTransactionMetadata(record);
|
|
8947
|
+
const value = tx["loop_run_id"] ?? record.metadata["loop_run_id"];
|
|
8948
|
+
return typeof value === "string" ? value : null;
|
|
8949
|
+
}
|
|
8950
|
+
function getTaskRunTransactionByKey(key, taskId, db) {
|
|
8951
|
+
if (taskId) {
|
|
8952
|
+
return db.query("SELECT * FROM task_run_transactions WHERE task_id = ? AND key = ?").get(taskId, key);
|
|
8953
|
+
}
|
|
8954
|
+
const rows = db.query("SELECT * FROM task_run_transactions WHERE key = ? ORDER BY created_at DESC LIMIT 2").all(key);
|
|
8955
|
+
if (rows.length > 1)
|
|
8956
|
+
throw new Error(`Run transaction key is ambiguous across tasks: ${key}. Pass task_id.`);
|
|
8957
|
+
return rows[0] ?? null;
|
|
8958
|
+
}
|
|
8959
|
+
function summarizeTaskRun(run) {
|
|
8960
|
+
return {
|
|
8961
|
+
id: run.id,
|
|
8962
|
+
task_id: run.task_id,
|
|
8963
|
+
agent_id: run.agent_id,
|
|
8964
|
+
title: run.title,
|
|
8965
|
+
status: run.status,
|
|
8966
|
+
summary: run.summary,
|
|
8967
|
+
idempotency_key: runKey(run),
|
|
8968
|
+
loop_id: loopId(run),
|
|
8969
|
+
loop_run_id: loopRunId(run),
|
|
8970
|
+
metadata_keys: Object.keys(run.metadata).sort(),
|
|
8971
|
+
started_at: run.started_at,
|
|
8972
|
+
completed_at: run.completed_at,
|
|
8973
|
+
updated_at: run.updated_at
|
|
8974
|
+
};
|
|
8975
|
+
}
|
|
8976
|
+
function findTaskRunByTransactionKey(key, taskId, db) {
|
|
8977
|
+
const d = db || getDatabase();
|
|
8978
|
+
const normalized = normalizeTransactionKey({ key });
|
|
8979
|
+
const transaction = getTaskRunTransactionByKey(normalized, taskId, d);
|
|
8980
|
+
if (transaction?.run_id)
|
|
8981
|
+
return getTaskRun(transaction.run_id, d);
|
|
8982
|
+
return null;
|
|
8983
|
+
}
|
|
8984
|
+
function loopRunCommands(run, key) {
|
|
8985
|
+
return [
|
|
8986
|
+
run ? `todos runs show ${run.id.slice(0, 8)}` : "todos runs list",
|
|
8987
|
+
run ? `todos findings list --task ${run.task_id.slice(0, 8)} --json` : "todos findings list --json",
|
|
8988
|
+
`todos runs begin <task-id> --key ${key} --apply --json`
|
|
8989
|
+
];
|
|
8990
|
+
}
|
|
8680
8991
|
function resolveTaskRunId(idOrPrefix, db) {
|
|
8681
8992
|
const d = db || getDatabase();
|
|
8682
8993
|
const rows = d.query("SELECT id FROM task_runs WHERE id = ? OR id LIKE ? ORDER BY created_at DESC LIMIT 2").all(idOrPrefix, `${idOrPrefix}%`);
|
|
@@ -8695,7 +9006,7 @@ function startTaskRun(input, db) {
|
|
|
8695
9006
|
const d = db || getDatabase();
|
|
8696
9007
|
if (!getTask(input.task_id, d))
|
|
8697
9008
|
throw new TaskNotFoundError(input.task_id);
|
|
8698
|
-
const id = uuid();
|
|
9009
|
+
const id = input.id ?? uuid();
|
|
8699
9010
|
const timestamp = input.started_at || now();
|
|
8700
9011
|
if (input.claim && input.agent_id) {
|
|
8701
9012
|
startTask(input.task_id, input.agent_id, d);
|
|
@@ -8733,6 +9044,97 @@ function startTaskRun(input, db) {
|
|
|
8733
9044
|
emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
|
|
8734
9045
|
return run;
|
|
8735
9046
|
}
|
|
9047
|
+
function beginTaskRunTransaction(input, db) {
|
|
9048
|
+
const d = db || getDatabase();
|
|
9049
|
+
if (!getTask(input.task_id, d))
|
|
9050
|
+
throw new TaskNotFoundError(input.task_id);
|
|
9051
|
+
const timestamp = input.started_at || now();
|
|
9052
|
+
const key = normalizeTransactionKey(input);
|
|
9053
|
+
const existing = findTaskRunByTransactionKey(key, input.task_id, d);
|
|
9054
|
+
const dryRun = !input.apply;
|
|
9055
|
+
if (existing) {
|
|
9056
|
+
return {
|
|
9057
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9058
|
+
local_only: true,
|
|
9059
|
+
dry_run: dryRun,
|
|
9060
|
+
processed_at: timestamp,
|
|
9061
|
+
action: "matched",
|
|
9062
|
+
key,
|
|
9063
|
+
run: summarizeTaskRun(existing),
|
|
9064
|
+
warnings: existing.status === "running" ? [] : [`matched ${existing.status} run`],
|
|
9065
|
+
commands: loopRunCommands(existing, key)
|
|
9066
|
+
};
|
|
9067
|
+
}
|
|
9068
|
+
if (dryRun) {
|
|
9069
|
+
return {
|
|
9070
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9071
|
+
local_only: true,
|
|
9072
|
+
dry_run: true,
|
|
9073
|
+
processed_at: timestamp,
|
|
9074
|
+
action: "preview",
|
|
9075
|
+
key,
|
|
9076
|
+
run: null,
|
|
9077
|
+
warnings: [],
|
|
9078
|
+
commands: loopRunCommands(null, key)
|
|
9079
|
+
};
|
|
9080
|
+
}
|
|
9081
|
+
const metadata = redactValue({
|
|
9082
|
+
...input.metadata || {},
|
|
9083
|
+
loop_transaction: {
|
|
9084
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9085
|
+
idempotency_key: key,
|
|
9086
|
+
loop_id: input.loop_id ?? null,
|
|
9087
|
+
loop_run_id: input.loop_run_id ?? null,
|
|
9088
|
+
first_seen_at: timestamp
|
|
9089
|
+
},
|
|
9090
|
+
idempotency_key: key
|
|
9091
|
+
});
|
|
9092
|
+
const created = d.transaction(() => {
|
|
9093
|
+
d.run(`INSERT OR IGNORE INTO task_run_transactions (
|
|
9094
|
+
id, task_id, run_id, key, loop_id, loop_run_id, metadata, created_at, updated_at
|
|
9095
|
+
) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?)`, [
|
|
9096
|
+
uuid(),
|
|
9097
|
+
input.task_id,
|
|
9098
|
+
key,
|
|
9099
|
+
input.loop_id ?? null,
|
|
9100
|
+
input.loop_run_id ?? null,
|
|
9101
|
+
JSON.stringify(metadata),
|
|
9102
|
+
timestamp,
|
|
9103
|
+
timestamp
|
|
9104
|
+
]);
|
|
9105
|
+
const transaction = getTaskRunTransactionByKey(key, input.task_id, d);
|
|
9106
|
+
if (!transaction)
|
|
9107
|
+
throw new Error(`Could not create run transaction for key: ${key}`);
|
|
9108
|
+
if (transaction.run_id) {
|
|
9109
|
+
const existingRun = getTaskRun(transaction.run_id, d);
|
|
9110
|
+
if (existingRun)
|
|
9111
|
+
return { run: existingRun, action: "matched" };
|
|
9112
|
+
}
|
|
9113
|
+
const run = startTaskRun({
|
|
9114
|
+
id: uuid(),
|
|
9115
|
+
task_id: input.task_id,
|
|
9116
|
+
agent_id: input.agent_id,
|
|
9117
|
+
title: input.title,
|
|
9118
|
+
summary: input.summary,
|
|
9119
|
+
metadata,
|
|
9120
|
+
claim: input.claim,
|
|
9121
|
+
started_at: timestamp
|
|
9122
|
+
}, d);
|
|
9123
|
+
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]);
|
|
9124
|
+
return { run, action: "created" };
|
|
9125
|
+
})();
|
|
9126
|
+
return {
|
|
9127
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9128
|
+
local_only: true,
|
|
9129
|
+
dry_run: false,
|
|
9130
|
+
processed_at: timestamp,
|
|
9131
|
+
action: created.action,
|
|
9132
|
+
key,
|
|
9133
|
+
run: summarizeTaskRun(created.run),
|
|
9134
|
+
warnings: [],
|
|
9135
|
+
commands: loopRunCommands(created.run, key)
|
|
9136
|
+
};
|
|
9137
|
+
}
|
|
8736
9138
|
function addTaskRunEvent(input, db) {
|
|
8737
9139
|
const d = db || getDatabase();
|
|
8738
9140
|
const runId = resolveTaskRunId(input.run_id, d);
|
|
@@ -8912,6 +9314,66 @@ function finishTaskRun(input, db) {
|
|
|
8912
9314
|
});
|
|
8913
9315
|
return updated;
|
|
8914
9316
|
}
|
|
9317
|
+
function finishTaskRunTransaction(input, db) {
|
|
9318
|
+
const d = db || getDatabase();
|
|
9319
|
+
const timestamp = input.completed_at || now();
|
|
9320
|
+
const status = input.status || "completed";
|
|
9321
|
+
const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
|
|
9322
|
+
const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
|
|
9323
|
+
if (!run) {
|
|
9324
|
+
throw new Error(input.run_id ? `Run not found: ${input.run_id}` : "runs finish requires a run id or --key");
|
|
9325
|
+
}
|
|
9326
|
+
if (input.task_id && run.task_id !== input.task_id) {
|
|
9327
|
+
throw new Error(`Run ${run.id} belongs to task ${run.task_id}, not ${input.task_id}`);
|
|
9328
|
+
}
|
|
9329
|
+
const resolvedKey = key || runKey(run) || run.id;
|
|
9330
|
+
const dryRun = input.apply === false;
|
|
9331
|
+
if (run.status !== "running") {
|
|
9332
|
+
const conflict = run.status !== status;
|
|
9333
|
+
return {
|
|
9334
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9335
|
+
local_only: true,
|
|
9336
|
+
dry_run: dryRun,
|
|
9337
|
+
processed_at: timestamp,
|
|
9338
|
+
action: conflict ? "conflict" : "matched",
|
|
9339
|
+
key: resolvedKey,
|
|
9340
|
+
run: summarizeTaskRun(run),
|
|
9341
|
+
warnings: conflict ? [`run is already ${run.status}; requested ${status}`] : [],
|
|
9342
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
9343
|
+
};
|
|
9344
|
+
}
|
|
9345
|
+
if (dryRun) {
|
|
9346
|
+
return {
|
|
9347
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9348
|
+
local_only: true,
|
|
9349
|
+
dry_run: true,
|
|
9350
|
+
processed_at: timestamp,
|
|
9351
|
+
action: "preview",
|
|
9352
|
+
key: resolvedKey,
|
|
9353
|
+
run: summarizeTaskRun({ ...run, status, summary: input.summary ?? run.summary, completed_at: timestamp, updated_at: timestamp }),
|
|
9354
|
+
warnings: [],
|
|
9355
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
9356
|
+
};
|
|
9357
|
+
}
|
|
9358
|
+
const finished = finishTaskRun({
|
|
9359
|
+
run_id: run.id,
|
|
9360
|
+
status,
|
|
9361
|
+
summary: input.summary,
|
|
9362
|
+
agent_id: input.agent_id,
|
|
9363
|
+
completed_at: timestamp
|
|
9364
|
+
}, d);
|
|
9365
|
+
return {
|
|
9366
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9367
|
+
local_only: true,
|
|
9368
|
+
dry_run: false,
|
|
9369
|
+
processed_at: timestamp,
|
|
9370
|
+
action: "finished",
|
|
9371
|
+
key: resolvedKey,
|
|
9372
|
+
run: summarizeTaskRun(finished),
|
|
9373
|
+
warnings: [],
|
|
9374
|
+
commands: loopRunCommands(finished, resolvedKey)
|
|
9375
|
+
};
|
|
9376
|
+
}
|
|
8915
9377
|
function listTaskRuns(taskId, db) {
|
|
8916
9378
|
const d = db || getDatabase();
|
|
8917
9379
|
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();
|
package/dist/types/index.d.ts
CHANGED
|
@@ -385,6 +385,14 @@ export interface CreateTaskInput {
|
|
|
385
385
|
assigned_from_project?: string;
|
|
386
386
|
task_type?: string;
|
|
387
387
|
}
|
|
388
|
+
export interface UpsertTaskByFingerprintInput extends CreateTaskInput {
|
|
389
|
+
/** Stable top-level metadata fingerprint used for deterministic dedupe. */
|
|
390
|
+
fingerprint: string;
|
|
391
|
+
}
|
|
392
|
+
export interface UpsertTaskByFingerprintResult {
|
|
393
|
+
task: Task;
|
|
394
|
+
created: boolean;
|
|
395
|
+
}
|
|
388
396
|
export interface UpdateTaskInput {
|
|
389
397
|
title?: string;
|
|
390
398
|
description?: string;
|
|
@@ -392,6 +400,7 @@ export interface UpdateTaskInput {
|
|
|
392
400
|
priority?: TaskPriority;
|
|
393
401
|
project_id?: string | null;
|
|
394
402
|
assigned_to?: string;
|
|
403
|
+
working_dir?: string | null;
|
|
395
404
|
plan_id?: string | null;
|
|
396
405
|
task_list_id?: string;
|
|
397
406
|
cycle_id?: string | null;
|
|
@@ -434,6 +443,8 @@ export interface TaskFilter {
|
|
|
434
443
|
cursor?: string;
|
|
435
444
|
/** When true, include archived tasks. Default: false (archived tasks excluded) */
|
|
436
445
|
include_archived?: boolean;
|
|
446
|
+
/** Exact top-level metadata filters, e.g. { fingerprint: "loop:key" }. */
|
|
447
|
+
metadata?: Record<string, unknown>;
|
|
437
448
|
}
|
|
438
449
|
export interface TaskDependency {
|
|
439
450
|
task_id: string;
|