@hasna/todos 0.11.57 → 0.11.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +38 -0
  2. package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
  3. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  4. package/dist/cli/index.js +1622 -285
  5. package/dist/contracts.d.ts.map +1 -1
  6. package/dist/contracts.js +703 -21
  7. package/dist/db/findings.d.ts +108 -0
  8. package/dist/db/findings.d.ts.map +1 -0
  9. package/dist/db/migrations.d.ts.map +1 -1
  10. package/dist/db/schema.d.ts.map +1 -1
  11. package/dist/db/task-crud.d.ts +3 -1
  12. package/dist/db/task-crud.d.ts.map +1 -1
  13. package/dist/db/task-runs.d.ts +56 -0
  14. package/dist/db/task-runs.d.ts.map +1 -1
  15. package/dist/db/tasks.d.ts +2 -2
  16. package/dist/db/tasks.d.ts.map +1 -1
  17. package/dist/index.d.ts +5 -3
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1071 -103
  20. package/dist/json-contracts.d.ts.map +1 -1
  21. package/dist/lib/access-profiles.d.ts.map +1 -1
  22. package/dist/lib/event-hooks.d.ts +1 -1
  23. package/dist/lib/event-hooks.d.ts.map +1 -1
  24. package/dist/lib/shared-events.d.ts +1 -1
  25. package/dist/lib/shared-events.d.ts.map +1 -1
  26. package/dist/mcp/index.js +1186 -115
  27. package/dist/mcp/token-utils.d.ts.map +1 -1
  28. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  29. package/dist/mcp/tools/task-resources.d.ts.map +1 -1
  30. package/dist/mcp.js +12 -1
  31. package/dist/registry.js +703 -21
  32. package/dist/release-provenance.json +3 -3
  33. package/dist/server/index.js +1186 -115
  34. package/dist/server/routes.d.ts +1 -0
  35. package/dist/server/routes.d.ts.map +1 -1
  36. package/dist/server/serve.d.ts +2 -0
  37. package/dist/server/serve.d.ts.map +1 -1
  38. package/dist/storage.js +574 -97
  39. package/dist/types/index.d.ts +11 -0
  40. package/dist/types/index.d.ts.map +1 -1
  41. 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.7/node_modules/@hasna/events/dist/index.js
8741
+ // node_modules/.bun/@hasna+events@0.1.9/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";
@@ -8670,17 +8755,30 @@ function getPathValue(input, path) {
8670
8755
  return;
8671
8756
  }, input);
8672
8757
  }
8673
- function wildcardToRegExp(pattern) {
8674
- const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
8675
- return new RegExp(`^${escaped}$`);
8758
+ function wildcardToRegExp(pattern, options = {}) {
8759
+ let body = "";
8760
+ for (let index = 0;index < pattern.length; index += 1) {
8761
+ const char = pattern[index];
8762
+ if (char === "*") {
8763
+ if (pattern[index + 1] === "*") {
8764
+ body += ".*";
8765
+ index += 1;
8766
+ } else {
8767
+ body += options.segmentSafe ? "[^/]*" : ".*";
8768
+ }
8769
+ } else {
8770
+ body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
8771
+ }
8772
+ }
8773
+ return new RegExp(`^${body}$`);
8676
8774
  }
8677
- function matchString(value, matcher) {
8775
+ function matchString(value, matcher, options = {}) {
8678
8776
  if (matcher === undefined)
8679
8777
  return true;
8680
8778
  if (value === undefined)
8681
8779
  return false;
8682
8780
  const matchers = Array.isArray(matcher) ? matcher : [matcher];
8683
- return matchers.some((item) => wildcardToRegExp(item).test(value));
8781
+ return matchers.some((item) => wildcardToRegExp(item, options).test(value));
8684
8782
  }
8685
8783
  function matchRecord(input, matcher) {
8686
8784
  if (!matcher)
@@ -8688,7 +8786,9 @@ function matchRecord(input, matcher) {
8688
8786
  return Object.entries(matcher).every(([path, expected]) => {
8689
8787
  const actual = getPathValue(input, path);
8690
8788
  if (typeof expected === "string" || Array.isArray(expected)) {
8691
- return matchString(actual === undefined ? undefined : String(actual), expected);
8789
+ return matchString(actual === undefined ? undefined : String(actual), expected, {
8790
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
8791
+ });
8692
8792
  }
8693
8793
  return actual === expected;
8694
8794
  });
@@ -9042,7 +9142,7 @@ class EventsClient {
9042
9142
  }
9043
9143
  return deliveries;
9044
9144
  }
9045
- async testChannel(id, input = {}) {
9145
+ async matchChannel(id, input = {}) {
9046
9146
  const channel = await this.store.getChannel(id);
9047
9147
  if (!channel)
9048
9148
  throw new Error(`Channel not found: ${id}`);
@@ -9059,6 +9159,34 @@ class EventsClient {
9059
9159
  time: input.time,
9060
9160
  id: input.id
9061
9161
  });
9162
+ const matched = channelMatchesEvent(channel, event);
9163
+ return {
9164
+ channelId: channel.id,
9165
+ matched,
9166
+ event,
9167
+ filters: channel.filters,
9168
+ reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
9169
+ };
9170
+ }
9171
+ async testChannel(id, input = {}, options = {}) {
9172
+ const channel = await this.store.getChannel(id);
9173
+ if (!channel)
9174
+ throw new Error(`Channel not found: ${id}`);
9175
+ const match = await this.matchChannel(id, input);
9176
+ const event = match.event;
9177
+ if (options.honorFilters && !match.matched) {
9178
+ const timestamp = new Date().toISOString();
9179
+ const result2 = createDeliveryResult(event, channel, [{
9180
+ attempt: 1,
9181
+ status: "skipped",
9182
+ startedAt: timestamp,
9183
+ completedAt: timestamp,
9184
+ error: match.reason
9185
+ }]);
9186
+ result2.metadata = { reason: "filter_mismatch" };
9187
+ await this.store.appendDelivery(result2);
9188
+ return result2;
9189
+ }
9062
9190
  const eventForChannel = await this.applyRedaction(event, channel);
9063
9191
  const result = await this.deliverWithRetry(eventForChannel, channel);
9064
9192
  await this.store.appendDelivery(result);
@@ -9162,6 +9290,90 @@ var init_dist = __esm(() => {
9162
9290
  DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
9163
9291
  });
9164
9292
 
9293
+ // src/db/task-lists.ts
9294
+ function rowToTaskList(row) {
9295
+ return {
9296
+ ...row,
9297
+ metadata: JSON.parse(row.metadata || "{}")
9298
+ };
9299
+ }
9300
+ function createTaskList(input, db) {
9301
+ const d = db || getDatabase();
9302
+ const id = uuid();
9303
+ const timestamp = now();
9304
+ const slug = input.slug || slugify(input.name);
9305
+ if (!input.project_id) {
9306
+ const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
9307
+ if (existing) {
9308
+ throw new Error(`Standalone task list with slug "${slug}" already exists`);
9309
+ }
9310
+ }
9311
+ d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
9312
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
9313
+ return getTaskList(id, d);
9314
+ }
9315
+ function getTaskList(id, db) {
9316
+ const d = db || getDatabase();
9317
+ const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
9318
+ return row ? rowToTaskList(row) : null;
9319
+ }
9320
+ function getTaskListBySlug(slug, projectId, db) {
9321
+ const d = db || getDatabase();
9322
+ let row;
9323
+ if (projectId) {
9324
+ row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
9325
+ } else {
9326
+ row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
9327
+ }
9328
+ return row ? rowToTaskList(row) : null;
9329
+ }
9330
+ function listTaskLists(projectId, db) {
9331
+ const d = db || getDatabase();
9332
+ if (projectId) {
9333
+ return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
9334
+ }
9335
+ return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
9336
+ }
9337
+ function updateTaskList(id, input, db) {
9338
+ const d = db || getDatabase();
9339
+ const existing = getTaskList(id, d);
9340
+ if (!existing)
9341
+ throw new TaskListNotFoundError(id);
9342
+ const sets = ["updated_at = ?"];
9343
+ const params = [now()];
9344
+ if (input.name !== undefined) {
9345
+ sets.push("name = ?");
9346
+ params.push(input.name);
9347
+ }
9348
+ if (input.description !== undefined) {
9349
+ sets.push("description = ?");
9350
+ params.push(input.description);
9351
+ }
9352
+ if (input.metadata !== undefined) {
9353
+ sets.push("metadata = ?");
9354
+ params.push(JSON.stringify(input.metadata));
9355
+ }
9356
+ params.push(id);
9357
+ d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
9358
+ return getTaskList(id, d);
9359
+ }
9360
+ function deleteTaskList(id, db) {
9361
+ const d = db || getDatabase();
9362
+ return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
9363
+ }
9364
+ function ensureTaskList(name, slug, projectId, db) {
9365
+ const d = db || getDatabase();
9366
+ const existing = getTaskListBySlug(slug, projectId, d);
9367
+ if (existing)
9368
+ return existing;
9369
+ return createTaskList({ name, slug, project_id: projectId }, d);
9370
+ }
9371
+ var init_task_lists = __esm(() => {
9372
+ init_types();
9373
+ init_database();
9374
+ init_projects();
9375
+ });
9376
+
9165
9377
  // src/lib/shared-events.ts
9166
9378
  function taskEventData(task, extra = {}) {
9167
9379
  return {
@@ -9191,6 +9403,69 @@ function taskEventData(task, extra = {}) {
9191
9403
  ...extra
9192
9404
  };
9193
9405
  }
9406
+ function taskEventMetadata(task) {
9407
+ const metadata = {
9408
+ package: "@hasna/todos",
9409
+ todos_event_schema_version: 1,
9410
+ task_id: task.id,
9411
+ task_short_id: task.short_id,
9412
+ project_id: task.project_id,
9413
+ task_list_id: task.task_list_id,
9414
+ working_dir: task.working_dir
9415
+ };
9416
+ try {
9417
+ const project = task.project_id ? getProject(task.project_id) : null;
9418
+ const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
9419
+ if (project) {
9420
+ metadata.project_id = project.id;
9421
+ metadata.project_name = project.name;
9422
+ metadata.project_path = projectPath;
9423
+ metadata.project_canonical_path = project.path;
9424
+ metadata.project_default_task_list_slug = project.task_list_id;
9425
+ metadata.root_project_id = inferRootProjectId(project);
9426
+ } else if (projectPath) {
9427
+ metadata.project_path = projectPath;
9428
+ metadata.project_canonical_path = projectPath;
9429
+ }
9430
+ if (projectPath) {
9431
+ metadata.project_kind = classifyProjectKind(projectPath);
9432
+ metadata.project_is_worktree = isWorktreePath(projectPath);
9433
+ if (typeof task.metadata.route_enabled === "boolean") {
9434
+ metadata.route_enabled = task.metadata.route_enabled;
9435
+ }
9436
+ metadata.working_dir = task.working_dir ?? projectPath;
9437
+ }
9438
+ 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;
9439
+ if (taskList) {
9440
+ metadata.task_list_id = taskList.id;
9441
+ metadata.task_list_slug = taskList.slug;
9442
+ metadata.task_list_name = taskList.name;
9443
+ metadata.task_list_project_id = taskList.project_id;
9444
+ metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
9445
+ }
9446
+ } catch {}
9447
+ return metadata;
9448
+ }
9449
+ function classifyProjectKind(path) {
9450
+ return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
9451
+ }
9452
+ function isWorktreePath(path) {
9453
+ return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
9454
+ }
9455
+ function inferRootProjectId(project) {
9456
+ return isWorktreePath(project.path) ? null : project.id;
9457
+ }
9458
+ function readMachineLocalPath(project) {
9459
+ const machineId = process.env["TODOS_MACHINE_ID"];
9460
+ if (!machineId)
9461
+ return null;
9462
+ try {
9463
+ const row = getDatabase().query("SELECT path FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(project.id, machineId);
9464
+ return row?.path ?? null;
9465
+ } catch {
9466
+ return null;
9467
+ }
9468
+ }
9194
9469
  async function emitSharedTaskEvent(input) {
9195
9470
  const data = taskEventData(input.task, input.data);
9196
9471
  await new EventsClient().emit({
@@ -9201,12 +9476,7 @@ async function emitSharedTaskEvent(input) {
9201
9476
  message: input.message ?? `${input.type}: ${input.task.title}`,
9202
9477
  data,
9203
9478
  dedupeKey: input.dedupeKey ?? `${input.type}:${input.task.id}:${input.task.version}`,
9204
- metadata: {
9205
- package: "@hasna/todos",
9206
- task_id: input.task.id,
9207
- project_id: input.task.project_id,
9208
- task_list_id: input.task.task_list_id
9209
- }
9479
+ metadata: taskEventMetadata(input.task)
9210
9480
  }, { deliver: true, dedupe: true });
9211
9481
  }
9212
9482
  function emitSharedTaskEventQuiet(input) {
@@ -9217,6 +9487,9 @@ function emitSharedTaskEventQuiet(input) {
9217
9487
  var SOURCE = "todos";
9218
9488
  var init_shared_events = __esm(() => {
9219
9489
  init_dist();
9490
+ init_database();
9491
+ init_projects();
9492
+ init_task_lists();
9220
9493
  });
9221
9494
 
9222
9495
  // src/lib/secret-redaction.ts
@@ -9741,6 +10014,17 @@ function replaceTaskTags(taskId, tags, db) {
9741
10014
  db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
9742
10015
  insertTaskTags(taskId, tags, db);
9743
10016
  }
10017
+ function addMetadataConditions(metadata, conditions, params) {
10018
+ if (!metadata)
10019
+ return;
10020
+ for (const [key, value] of Object.entries(metadata)) {
10021
+ if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
10022
+ throw new Error(`Invalid metadata filter key: ${key}`);
10023
+ }
10024
+ conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
10025
+ params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
10026
+ }
10027
+ }
9744
10028
  function createTask(input, db) {
9745
10029
  const d = db || getDatabase();
9746
10030
  const timestamp = now();
@@ -9923,6 +10207,7 @@ function listTasks(filter = {}, db) {
9923
10207
  params.push(filter.task_type);
9924
10208
  }
9925
10209
  }
10210
+ addMetadataConditions(filter.metadata, conditions, params);
9926
10211
  const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
9927
10212
  if (filter.cursor) {
9928
10213
  try {
@@ -9947,6 +10232,54 @@ function listTasks(filter = {}, db) {
9947
10232
  const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
9948
10233
  return rows.map(rowToTask);
9949
10234
  }
10235
+ function getTaskByFingerprint(fingerprint, db) {
10236
+ const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
10237
+ return tasks[0] ?? null;
10238
+ }
10239
+ function mergeTaskMetadata(current, next, fingerprint) {
10240
+ return {
10241
+ ...current,
10242
+ ...next ?? {},
10243
+ fingerprint
10244
+ };
10245
+ }
10246
+ function upsertTaskByFingerprint(input, db) {
10247
+ const d = db || getDatabase();
10248
+ const fingerprint = input.fingerprint.trim();
10249
+ if (!fingerprint)
10250
+ throw new Error("fingerprint is required");
10251
+ const existing = getTaskByFingerprint(fingerprint, d);
10252
+ const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
10253
+ if (!existing) {
10254
+ const task2 = createTask({ ...input, metadata }, d);
10255
+ return { task: task2, created: true };
10256
+ }
10257
+ const task = updateTask(existing.id, {
10258
+ version: existing.version,
10259
+ title: input.title,
10260
+ description: input.description,
10261
+ status: input.status,
10262
+ priority: input.priority,
10263
+ project_id: input.project_id,
10264
+ assigned_to: input.assigned_to,
10265
+ working_dir: input.working_dir,
10266
+ plan_id: input.plan_id,
10267
+ task_list_id: input.task_list_id,
10268
+ tags: input.tags,
10269
+ metadata,
10270
+ due_at: input.due_at,
10271
+ estimated_minutes: input.estimated_minutes,
10272
+ sla_minutes: input.sla_minutes,
10273
+ confidence: input.confidence,
10274
+ retry_count: input.retry_count,
10275
+ max_retries: input.max_retries,
10276
+ retry_after: input.retry_after,
10277
+ requires_approval: input.requires_approval,
10278
+ recurrence_rule: input.recurrence_rule,
10279
+ task_type: input.task_type
10280
+ }, d);
10281
+ return { task, created: false };
10282
+ }
9950
10283
  function countTasks(filter = {}, db) {
9951
10284
  const d = db || getDatabase();
9952
10285
  const conditions = [];
@@ -10010,6 +10343,7 @@ function countTasks(filter = {}, db) {
10010
10343
  conditions.push("task_list_id = ?");
10011
10344
  params.push(filter.task_list_id);
10012
10345
  }
10346
+ addMetadataConditions(filter.metadata, conditions, params);
10013
10347
  if (!filter.include_archived) {
10014
10348
  conditions.push("archived_at IS NULL");
10015
10349
  }
@@ -10060,6 +10394,10 @@ function updateTask(id, input, db) {
10060
10394
  sets.push("assigned_to = ?");
10061
10395
  params.push(input.assigned_to);
10062
10396
  }
10397
+ if (input.working_dir !== undefined) {
10398
+ sets.push("working_dir = ?");
10399
+ params.push(input.working_dir);
10400
+ }
10063
10401
  if (input.tags !== undefined) {
10064
10402
  sets.push("tags = ?");
10065
10403
  params.push(JSON.stringify(input.tags));
@@ -10148,6 +10486,8 @@ function updateTask(id, input, db) {
10148
10486
  logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
10149
10487
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
10150
10488
  logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
10489
+ if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
10490
+ logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
10151
10491
  if (input.approved_by !== undefined)
10152
10492
  logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
10153
10493
  const updatedTask = {
@@ -10183,6 +10523,10 @@ function updateTask(id, input, db) {
10183
10523
  if (input.approved_by !== undefined) {
10184
10524
  emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
10185
10525
  }
10526
+ const updatePayload = taskEventData(updatedTask);
10527
+ dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
10528
+ emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
10529
+ emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
10186
10530
  return updatedTask;
10187
10531
  }
10188
10532
  function deleteTask(id, db) {
@@ -13200,6 +13544,72 @@ function rowToArtifact(row) {
13200
13544
  function getRunRow(runId, db) {
13201
13545
  return db.query("SELECT * FROM task_runs WHERE id = ?").get(runId);
13202
13546
  }
13547
+ function normalizeTransactionKey(input) {
13548
+ const key = (input.key || input.loop_run_id || input.loop_id || "").trim();
13549
+ if (!key)
13550
+ throw new Error("idempotent run transactions require --key, --loop-run-id, or --loop-id");
13551
+ return key.toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 240);
13552
+ }
13553
+ function loopTransactionMetadata(record) {
13554
+ const value = record.metadata["loop_transaction"];
13555
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
13556
+ }
13557
+ function runKey(record) {
13558
+ const tx = loopTransactionMetadata(record);
13559
+ const key = tx["idempotency_key"] ?? record.metadata["idempotency_key"];
13560
+ return typeof key === "string" ? key : null;
13561
+ }
13562
+ function loopId(record) {
13563
+ const tx = loopTransactionMetadata(record);
13564
+ const value = tx["loop_id"] ?? record.metadata["loop_id"];
13565
+ return typeof value === "string" ? value : null;
13566
+ }
13567
+ function loopRunId(record) {
13568
+ const tx = loopTransactionMetadata(record);
13569
+ const value = tx["loop_run_id"] ?? record.metadata["loop_run_id"];
13570
+ return typeof value === "string" ? value : null;
13571
+ }
13572
+ function getTaskRunTransactionByKey(key, taskId, db) {
13573
+ if (taskId) {
13574
+ return db.query("SELECT * FROM task_run_transactions WHERE task_id = ? AND key = ?").get(taskId, key);
13575
+ }
13576
+ const rows = db.query("SELECT * FROM task_run_transactions WHERE key = ? ORDER BY created_at DESC LIMIT 2").all(key);
13577
+ if (rows.length > 1)
13578
+ throw new Error(`Run transaction key is ambiguous across tasks: ${key}. Pass task_id.`);
13579
+ return rows[0] ?? null;
13580
+ }
13581
+ function summarizeTaskRun(run) {
13582
+ return {
13583
+ id: run.id,
13584
+ task_id: run.task_id,
13585
+ agent_id: run.agent_id,
13586
+ title: run.title,
13587
+ status: run.status,
13588
+ summary: run.summary,
13589
+ idempotency_key: runKey(run),
13590
+ loop_id: loopId(run),
13591
+ loop_run_id: loopRunId(run),
13592
+ metadata_keys: Object.keys(run.metadata).sort(),
13593
+ started_at: run.started_at,
13594
+ completed_at: run.completed_at,
13595
+ updated_at: run.updated_at
13596
+ };
13597
+ }
13598
+ function findTaskRunByTransactionKey(key, taskId, db) {
13599
+ const d = db || getDatabase();
13600
+ const normalized = normalizeTransactionKey({ key });
13601
+ const transaction = getTaskRunTransactionByKey(normalized, taskId, d);
13602
+ if (transaction?.run_id)
13603
+ return getTaskRun(transaction.run_id, d);
13604
+ return null;
13605
+ }
13606
+ function loopRunCommands(run, key) {
13607
+ return [
13608
+ run ? `todos runs show ${run.id.slice(0, 8)}` : "todos runs list",
13609
+ run ? `todos findings list --task ${run.task_id.slice(0, 8)} --json` : "todos findings list --json",
13610
+ `todos runs begin <task-id> --key ${key} --apply --json`
13611
+ ];
13612
+ }
13203
13613
  function resolveTaskRunId(idOrPrefix, db) {
13204
13614
  const d = db || getDatabase();
13205
13615
  const rows = d.query("SELECT id FROM task_runs WHERE id = ? OR id LIKE ? ORDER BY created_at DESC LIMIT 2").all(idOrPrefix, `${idOrPrefix}%`);
@@ -13218,7 +13628,7 @@ function startTaskRun(input, db) {
13218
13628
  const d = db || getDatabase();
13219
13629
  if (!getTask(input.task_id, d))
13220
13630
  throw new TaskNotFoundError(input.task_id);
13221
- const id = uuid();
13631
+ const id = input.id ?? uuid();
13222
13632
  const timestamp = input.started_at || now();
13223
13633
  if (input.claim && input.agent_id) {
13224
13634
  startTask(input.task_id, input.agent_id, d);
@@ -13256,6 +13666,97 @@ function startTaskRun(input, db) {
13256
13666
  emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
13257
13667
  return run;
13258
13668
  }
13669
+ function beginTaskRunTransaction(input, db) {
13670
+ const d = db || getDatabase();
13671
+ if (!getTask(input.task_id, d))
13672
+ throw new TaskNotFoundError(input.task_id);
13673
+ const timestamp = input.started_at || now();
13674
+ const key = normalizeTransactionKey(input);
13675
+ const existing = findTaskRunByTransactionKey(key, input.task_id, d);
13676
+ const dryRun = !input.apply;
13677
+ if (existing) {
13678
+ return {
13679
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
13680
+ local_only: true,
13681
+ dry_run: dryRun,
13682
+ processed_at: timestamp,
13683
+ action: "matched",
13684
+ key,
13685
+ run: summarizeTaskRun(existing),
13686
+ warnings: existing.status === "running" ? [] : [`matched ${existing.status} run`],
13687
+ commands: loopRunCommands(existing, key)
13688
+ };
13689
+ }
13690
+ if (dryRun) {
13691
+ return {
13692
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
13693
+ local_only: true,
13694
+ dry_run: true,
13695
+ processed_at: timestamp,
13696
+ action: "preview",
13697
+ key,
13698
+ run: null,
13699
+ warnings: [],
13700
+ commands: loopRunCommands(null, key)
13701
+ };
13702
+ }
13703
+ const metadata = redactValue({
13704
+ ...input.metadata || {},
13705
+ loop_transaction: {
13706
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
13707
+ idempotency_key: key,
13708
+ loop_id: input.loop_id ?? null,
13709
+ loop_run_id: input.loop_run_id ?? null,
13710
+ first_seen_at: timestamp
13711
+ },
13712
+ idempotency_key: key
13713
+ });
13714
+ const created = d.transaction(() => {
13715
+ d.run(`INSERT OR IGNORE INTO task_run_transactions (
13716
+ id, task_id, run_id, key, loop_id, loop_run_id, metadata, created_at, updated_at
13717
+ ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?)`, [
13718
+ uuid(),
13719
+ input.task_id,
13720
+ key,
13721
+ input.loop_id ?? null,
13722
+ input.loop_run_id ?? null,
13723
+ JSON.stringify(metadata),
13724
+ timestamp,
13725
+ timestamp
13726
+ ]);
13727
+ const transaction = getTaskRunTransactionByKey(key, input.task_id, d);
13728
+ if (!transaction)
13729
+ throw new Error(`Could not create run transaction for key: ${key}`);
13730
+ if (transaction.run_id) {
13731
+ const existingRun = getTaskRun(transaction.run_id, d);
13732
+ if (existingRun)
13733
+ return { run: existingRun, action: "matched" };
13734
+ }
13735
+ const run = startTaskRun({
13736
+ id: uuid(),
13737
+ task_id: input.task_id,
13738
+ agent_id: input.agent_id,
13739
+ title: input.title,
13740
+ summary: input.summary,
13741
+ metadata,
13742
+ claim: input.claim,
13743
+ started_at: timestamp
13744
+ }, d);
13745
+ d.run("UPDATE task_run_transactions SET run_id = ?, loop_id = COALESCE(?, loop_id), loop_run_id = COALESCE(?, loop_run_id), metadata = ?, updated_at = ? WHERE id = ?", [run.id, input.loop_id ?? null, input.loop_run_id ?? null, JSON.stringify(metadata), timestamp, transaction.id]);
13746
+ return { run, action: "created" };
13747
+ })();
13748
+ return {
13749
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
13750
+ local_only: true,
13751
+ dry_run: false,
13752
+ processed_at: timestamp,
13753
+ action: created.action,
13754
+ key,
13755
+ run: summarizeTaskRun(created.run),
13756
+ warnings: [],
13757
+ commands: loopRunCommands(created.run, key)
13758
+ };
13759
+ }
13259
13760
  function addTaskRunEvent(input, db) {
13260
13761
  const d = db || getDatabase();
13261
13762
  const runId = resolveTaskRunId(input.run_id, d);
@@ -13435,6 +13936,66 @@ function finishTaskRun(input, db) {
13435
13936
  });
13436
13937
  return updated;
13437
13938
  }
13939
+ function finishTaskRunTransaction(input, db) {
13940
+ const d = db || getDatabase();
13941
+ const timestamp = input.completed_at || now();
13942
+ const status = input.status || "completed";
13943
+ const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
13944
+ const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
13945
+ if (!run) {
13946
+ throw new Error(input.run_id ? `Run not found: ${input.run_id}` : "runs finish requires a run id or --key");
13947
+ }
13948
+ if (input.task_id && run.task_id !== input.task_id) {
13949
+ throw new Error(`Run ${run.id} belongs to task ${run.task_id}, not ${input.task_id}`);
13950
+ }
13951
+ const resolvedKey = key || runKey(run) || run.id;
13952
+ const dryRun = input.apply === false;
13953
+ if (run.status !== "running") {
13954
+ const conflict = run.status !== status;
13955
+ return {
13956
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
13957
+ local_only: true,
13958
+ dry_run: dryRun,
13959
+ processed_at: timestamp,
13960
+ action: conflict ? "conflict" : "matched",
13961
+ key: resolvedKey,
13962
+ run: summarizeTaskRun(run),
13963
+ warnings: conflict ? [`run is already ${run.status}; requested ${status}`] : [],
13964
+ commands: loopRunCommands(run, resolvedKey)
13965
+ };
13966
+ }
13967
+ if (dryRun) {
13968
+ return {
13969
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
13970
+ local_only: true,
13971
+ dry_run: true,
13972
+ processed_at: timestamp,
13973
+ action: "preview",
13974
+ key: resolvedKey,
13975
+ run: summarizeTaskRun({ ...run, status, summary: input.summary ?? run.summary, completed_at: timestamp, updated_at: timestamp }),
13976
+ warnings: [],
13977
+ commands: loopRunCommands(run, resolvedKey)
13978
+ };
13979
+ }
13980
+ const finished = finishTaskRun({
13981
+ run_id: run.id,
13982
+ status,
13983
+ summary: input.summary,
13984
+ agent_id: input.agent_id,
13985
+ completed_at: timestamp
13986
+ }, d);
13987
+ return {
13988
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
13989
+ local_only: true,
13990
+ dry_run: false,
13991
+ processed_at: timestamp,
13992
+ action: "finished",
13993
+ key: resolvedKey,
13994
+ run: summarizeTaskRun(finished),
13995
+ warnings: [],
13996
+ commands: loopRunCommands(finished, resolvedKey)
13997
+ };
13998
+ }
13438
13999
  function listTaskRuns(taskId, db) {
13439
14000
  const d = db || getDatabase();
13440
14001
  const rows = taskId ? d.query("SELECT * FROM task_runs WHERE task_id = ? ORDER BY started_at DESC, created_at DESC").all(taskId) : d.query("SELECT * FROM task_runs ORDER BY started_at DESC, created_at DESC LIMIT 100").all();
@@ -13452,6 +14013,7 @@ function getTaskRunLedger(runId, db) {
13452
14013
  const files = d.query("SELECT * FROM task_files WHERE task_id = ? ORDER BY updated_at DESC, path").all(run.task_id);
13453
14014
  return { run, events, commands, artifacts, files };
13454
14015
  }
14016
+ var LOOP_RUN_TRANSACTION_SCHEMA_VERSION = "todos.loop_run_transaction.v1";
13455
14017
  var init_task_runs = __esm(() => {
13456
14018
  init_artifact_store();
13457
14019
  init_event_hooks();
@@ -13840,6 +14402,7 @@ var init_calendar = __esm(() => {
13840
14402
  var exports_tasks = {};
13841
14403
  __export(exports_tasks, {
13842
14404
  watchTask: () => watchTask,
14405
+ upsertTaskByFingerprint: () => upsertTaskByFingerprint,
13843
14406
  updateTaskBoard: () => updateTaskBoard,
13844
14407
  updateTask: () => updateTask,
13845
14408
  unwatchTask: () => unwatchTask,
@@ -13883,6 +14446,7 @@ __export(exports_tasks, {
13883
14446
  getTaskGraph: () => getTaskGraph,
13884
14447
  getTaskDependents: () => getTaskDependents,
13885
14448
  getTaskDependencies: () => getTaskDependencies,
14449
+ getTaskByFingerprint: () => getTaskByFingerprint,
13886
14450
  getTaskBoard: () => getTaskBoard,
13887
14451
  getTask: () => getTask,
13888
14452
  getStatus: () => getStatus,
@@ -14171,90 +14735,6 @@ var init_dispatch = __esm(() => {
14171
14735
  init_tmux();
14172
14736
  });
14173
14737
 
14174
- // src/db/task-lists.ts
14175
- function rowToTaskList(row) {
14176
- return {
14177
- ...row,
14178
- metadata: JSON.parse(row.metadata || "{}")
14179
- };
14180
- }
14181
- function createTaskList(input, db) {
14182
- const d = db || getDatabase();
14183
- const id = uuid();
14184
- const timestamp = now();
14185
- const slug = input.slug || slugify(input.name);
14186
- if (!input.project_id) {
14187
- const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
14188
- if (existing) {
14189
- throw new Error(`Standalone task list with slug "${slug}" already exists`);
14190
- }
14191
- }
14192
- d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
14193
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
14194
- return getTaskList(id, d);
14195
- }
14196
- function getTaskList(id, db) {
14197
- const d = db || getDatabase();
14198
- const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
14199
- return row ? rowToTaskList(row) : null;
14200
- }
14201
- function getTaskListBySlug(slug, projectId, db) {
14202
- const d = db || getDatabase();
14203
- let row;
14204
- if (projectId) {
14205
- row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
14206
- } else {
14207
- row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
14208
- }
14209
- return row ? rowToTaskList(row) : null;
14210
- }
14211
- function listTaskLists(projectId, db) {
14212
- const d = db || getDatabase();
14213
- if (projectId) {
14214
- return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
14215
- }
14216
- return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
14217
- }
14218
- function updateTaskList(id, input, db) {
14219
- const d = db || getDatabase();
14220
- const existing = getTaskList(id, d);
14221
- if (!existing)
14222
- throw new TaskListNotFoundError(id);
14223
- const sets = ["updated_at = ?"];
14224
- const params = [now()];
14225
- if (input.name !== undefined) {
14226
- sets.push("name = ?");
14227
- params.push(input.name);
14228
- }
14229
- if (input.description !== undefined) {
14230
- sets.push("description = ?");
14231
- params.push(input.description);
14232
- }
14233
- if (input.metadata !== undefined) {
14234
- sets.push("metadata = ?");
14235
- params.push(JSON.stringify(input.metadata));
14236
- }
14237
- params.push(id);
14238
- d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
14239
- return getTaskList(id, d);
14240
- }
14241
- function deleteTaskList(id, db) {
14242
- const d = db || getDatabase();
14243
- return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
14244
- }
14245
- function ensureTaskList(name, slug, projectId, db) {
14246
- const d = db || getDatabase();
14247
- const existing = getTaskListBySlug(slug, projectId, d);
14248
- if (existing)
14249
- return existing;
14250
- return createTaskList({ name, slug, project_id: projectId }, d);
14251
- }
14252
- var init_task_lists = __esm(() => {
14253
- init_types();
14254
- init_database();
14255
- init_projects();
14256
- });
14257
-
14258
14738
  // src/mcp/tools/dispatch.ts
14259
14739
  function registerDispatchTools(server, { shouldRegisterTool, resolveId, formatError }) {
14260
14740
  if (shouldRegisterTool("dispatch_tasks")) {
@@ -14813,6 +15293,7 @@ var init_token_utils = __esm(() => {
14813
15293
  "add_task_run_event",
14814
15294
  "add_task_run_file",
14815
15295
  "acknowledge_handoff",
15296
+ "begin_task_run_transaction",
14816
15297
  "build_local_report",
14817
15298
  "cancel_agent_run_dispatch",
14818
15299
  "finish_task_run",
@@ -14860,6 +15341,7 @@ var init_token_utils = __esm(() => {
14860
15341
  "list_local_snapshots",
14861
15342
  "list_retrospectives",
14862
15343
  "list_risks",
15344
+ "list_task_findings",
14863
15345
  "list_task_runs",
14864
15346
  "list_verification_providers",
14865
15347
  "merge_duplicate_task",
@@ -14868,6 +15350,7 @@ var init_token_utils = __esm(() => {
14868
15350
  "remove_review_routing_rule",
14869
15351
  "restore_local_backup",
14870
15352
  "retry_agent_run_dispatch",
15353
+ "resolve_missing_task_findings",
14871
15354
  "resolve_mentions",
14872
15355
  "run_next_agent_dispatch",
14873
15356
  "search_knowledge_records",
@@ -14910,9 +15393,17 @@ var init_token_utils = __esm(() => {
14910
15393
  "unlock_file",
14911
15394
  "unwatch_task",
14912
15395
  "update_comment",
15396
+ "upsert_task_finding",
14913
15397
  "update_risk",
14914
15398
  "watch_task"
14915
15399
  ],
15400
+ loops: [
15401
+ "begin_task_run_transaction",
15402
+ "finish_task_run",
15403
+ "list_task_findings",
15404
+ "resolve_missing_task_findings",
15405
+ "upsert_task_finding"
15406
+ ],
14916
15407
  agents: [
14917
15408
  "auto_assign_task",
14918
15409
  "delete_agent",
@@ -14994,7 +15485,7 @@ var init_token_utils = __esm(() => {
14994
15485
  maintenance: ["extract_todos", "get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task", "watch_source_todos"]
14995
15486
  };
14996
15487
  MCP_PROFILE_GROUPS = {
14997
- minimal: ["core"],
15488
+ minimal: ["core", "loops"],
14998
15489
  core: ["core"],
14999
15490
  standard: ["core", "tasks", "projects", "resources", "agents", "metadata"],
15000
15491
  agent: ["core", "tasks", "projects", "resources"],
@@ -15074,6 +15565,61 @@ function registerTaskCrudTools(server, ctx) {
15074
15565
  }
15075
15566
  });
15076
15567
  }
15568
+ if (shouldRegisterTool("upsert_task")) {
15569
+ server.tool("upsert_task", "Create or update a task by stable metadata fingerprint. Metadata is shallow-merged on updates.", {
15570
+ fingerprint: exports_external.string().describe("Stable dedupe fingerprint stored as metadata.fingerprint"),
15571
+ title: exports_external.string().describe("Task title"),
15572
+ description: exports_external.string().optional().describe("Task description"),
15573
+ status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
15574
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
15575
+ project_id: exports_external.string().optional().describe("Project ID"),
15576
+ task_list_id: exports_external.string().optional().describe("Task list ID"),
15577
+ assigned_to: exports_external.string().optional().describe("Agent ID or name to assign to"),
15578
+ tags: exports_external.array(exports_external.string()).optional().describe("Tags for the task"),
15579
+ working_dir: exports_external.string().optional().describe("Working directory associated with the task"),
15580
+ metadata: exports_external.record(exports_external.unknown()).optional().describe("Metadata object to shallow-merge"),
15581
+ expectation_id: exports_external.string().optional(),
15582
+ expectation_fingerprint: exports_external.string().optional(),
15583
+ evidence_paths: exports_external.array(exports_external.string()).optional(),
15584
+ origin_loop_id: exports_external.string().optional(),
15585
+ origin_run_id: exports_external.string().optional(),
15586
+ expected: exports_external.unknown().optional(),
15587
+ observed: exports_external.unknown().optional(),
15588
+ acceptance: exports_external.unknown().optional()
15589
+ }, async (params) => {
15590
+ try {
15591
+ const { assigned_to, project_id, task_list_id, metadata, expectation_id, expectation_fingerprint, evidence_paths, origin_loop_id, origin_run_id, expected, observed, acceptance, ...rest } = params;
15592
+ const mergedMetadata = { ...metadata ?? {} };
15593
+ if (expectation_id !== undefined)
15594
+ mergedMetadata["expectation_id"] = expectation_id;
15595
+ if (expectation_fingerprint !== undefined)
15596
+ mergedMetadata["expectation_fingerprint"] = expectation_fingerprint;
15597
+ if (evidence_paths !== undefined)
15598
+ mergedMetadata["evidence_paths"] = evidence_paths;
15599
+ if (origin_loop_id !== undefined)
15600
+ mergedMetadata["origin_loop_id"] = origin_loop_id;
15601
+ if (origin_run_id !== undefined)
15602
+ mergedMetadata["origin_run_id"] = origin_run_id;
15603
+ if (expected !== undefined)
15604
+ mergedMetadata["expected"] = expected;
15605
+ if (observed !== undefined)
15606
+ mergedMetadata["observed"] = observed;
15607
+ if (acceptance !== undefined)
15608
+ mergedMetadata["acceptance"] = acceptance;
15609
+ const resolved = { ...rest, metadata: mergedMetadata };
15610
+ if (assigned_to)
15611
+ resolved.assigned_to = resolveAssignee(assigned_to);
15612
+ if (project_id)
15613
+ resolved.project_id = resolveId(project_id, "projects");
15614
+ if (task_list_id)
15615
+ resolved.task_list_id = resolveId(task_list_id, "task_lists");
15616
+ const result = upsertTaskByFingerprint(resolved);
15617
+ return { content: [{ type: "text", text: compactJson({ created: result.created, task: JSON.parse(mutationTaskResponse(result.task)) }) }] };
15618
+ } catch (e) {
15619
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
15620
+ }
15621
+ });
15622
+ }
15077
15623
  if (shouldRegisterTool("list_tasks")) {
15078
15624
  server.tool("list_tasks", "List tasks with optional filters. Pass empty arrays for multi-value filters (e.g. status=[] shows all).", {
15079
15625
  status: exports_external.union([exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]), exports_external.array(exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]))]).optional().describe("Filter by status"),
@@ -15085,7 +15631,8 @@ function registerTaskCrudTools(server, ctx) {
15085
15631
  created_after: exports_external.string().optional().describe("ISO date \u2014 tasks created after this date"),
15086
15632
  created_before: exports_external.string().optional().describe("ISO date \u2014 tasks created before this date"),
15087
15633
  limit: exports_external.number().optional().describe("Max results (default: 50, max 500)"),
15088
- offset: exports_external.number().optional().describe("Pagination offset")
15634
+ offset: exports_external.number().optional().describe("Pagination offset"),
15635
+ metadata: exports_external.record(exports_external.unknown()).optional().describe("Exact top-level metadata filters")
15089
15636
  }, async (params) => {
15090
15637
  try {
15091
15638
  const resolved = { ...params };
@@ -18626,7 +19173,7 @@ function countMovedStandaloneVerifications(db, duplicateId) {
18626
19173
  const rows = db.query("SELECT command FROM task_verifications WHERE task_id = ?").all(duplicateId);
18627
19174
  return rows.filter((row) => !generatedCommands.has(row.command)).length;
18628
19175
  }
18629
- function mergeTaskMetadata(primary, duplicate, input, mergedAt) {
19176
+ function mergeTaskMetadata2(primary, duplicate, input, mergedAt) {
18630
19177
  const mergedDuplicates = Array.isArray(primary.metadata["merged_duplicates"]) ? [...primary.metadata["merged_duplicates"]] : [];
18631
19178
  mergedDuplicates.push({
18632
19179
  id: duplicate.id,
@@ -18687,7 +19234,7 @@ function mergeDuplicateTask(input, db) {
18687
19234
  updateTask(primary.id, {
18688
19235
  version: primary.version,
18689
19236
  tags: mergedTags,
18690
- metadata: mergeTaskMetadata(primary, duplicate, input, mergedAt),
19237
+ metadata: mergeTaskMetadata2(primary, duplicate, input, mergedAt),
18691
19238
  description: mergeTaskDescription(primary, duplicate) ?? undefined
18692
19239
  }, d);
18693
19240
  moved.comments = updateRows(d, "task_comments", "task_id", duplicate.id, primary.id);
@@ -25484,6 +26031,356 @@ var init_task_meta_tools = __esm(() => {
25484
26031
  init_zod();
25485
26032
  });
25486
26033
 
26034
+ // src/db/findings.ts
26035
+ function parseObject2(value) {
26036
+ if (!value)
26037
+ return {};
26038
+ try {
26039
+ const parsed = JSON.parse(value);
26040
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
26041
+ } catch {
26042
+ return {};
26043
+ }
26044
+ }
26045
+ function normalizeKey(value) {
26046
+ return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
26047
+ }
26048
+ function normalizeFingerprint(value) {
26049
+ const normalized = normalizeKey(value);
26050
+ if (!normalized)
26051
+ throw new Error("finding fingerprint is required");
26052
+ return normalized.slice(0, 240);
26053
+ }
26054
+ function normalizeSeverity(value) {
26055
+ const normalized = normalizeKey(value || "medium");
26056
+ if (SEVERITIES.has(normalized))
26057
+ return normalized;
26058
+ if (/^(p0|blocker|urgent|highest)$/.test(normalized))
26059
+ return "critical";
26060
+ if (/^(p1|major)$/.test(normalized))
26061
+ return "high";
26062
+ if (/^(p3|minor|info)$/.test(normalized))
26063
+ return "low";
26064
+ return "medium";
26065
+ }
26066
+ function normalizeStatus(value) {
26067
+ const normalized = normalizeKey(value || "open");
26068
+ if (STATUSES.has(normalized))
26069
+ return normalized;
26070
+ if (normalized === "closed" || normalized === "fixed")
26071
+ return "resolved";
26072
+ return "open";
26073
+ }
26074
+ function normalizeResolutionStatus(value) {
26075
+ const status = normalizeStatus(value || "resolved");
26076
+ if (status === "open")
26077
+ throw new Error("resolve-missing status must be resolved or ignored");
26078
+ return status;
26079
+ }
26080
+ function redactOptional(value, max = 2000) {
26081
+ if (!value)
26082
+ return null;
26083
+ const redacted = redactEvidenceText(value).trim();
26084
+ if (!redacted)
26085
+ return null;
26086
+ return redacted.length > max ? `${redacted.slice(0, max - 3)}...` : redacted;
26087
+ }
26088
+ function rowToFinding(row) {
26089
+ return {
26090
+ schema_version: TASK_FINDING_SCHEMA_VERSION,
26091
+ ...row,
26092
+ severity: normalizeSeverity(row.severity),
26093
+ status: normalizeStatus(row.status),
26094
+ metadata: parseObject2(row.metadata)
26095
+ };
26096
+ }
26097
+ function compactFinding(finding2) {
26098
+ return {
26099
+ schema_version: TASK_FINDING_SCHEMA_VERSION,
26100
+ id: finding2.id,
26101
+ task_id: finding2.task_id,
26102
+ run_id: finding2.run_id,
26103
+ fingerprint: finding2.fingerprint,
26104
+ title: finding2.title,
26105
+ severity: finding2.severity,
26106
+ status: finding2.status,
26107
+ source: finding2.source,
26108
+ summary: finding2.summary,
26109
+ artifact_path: finding2.artifact_path,
26110
+ first_seen_at: finding2.first_seen_at,
26111
+ last_seen_at: finding2.last_seen_at,
26112
+ resolved_at: finding2.resolved_at,
26113
+ metadata_keys: Object.keys(finding2.metadata).sort()
26114
+ };
26115
+ }
26116
+ function previewFinding(existing, next, timestamp3) {
26117
+ return {
26118
+ ...existing,
26119
+ run_id: next.run_id,
26120
+ title: next.title,
26121
+ severity: next.severity,
26122
+ status: next.status,
26123
+ source: next.source,
26124
+ summary: next.summary,
26125
+ artifact_path: next.artifact_path,
26126
+ metadata: next.metadata,
26127
+ last_seen_at: timestamp3,
26128
+ resolved_at: next.status === "open" ? null : existing.resolved_at || timestamp3,
26129
+ updated_at: timestamp3
26130
+ };
26131
+ }
26132
+ function upsertAction(existing, next) {
26133
+ if (sameFinding(existing, next))
26134
+ return "matched";
26135
+ return existing.status !== "open" && next.status === "open" ? "reopened" : "updated";
26136
+ }
26137
+ function resolveRunForTask(runId, taskId, db) {
26138
+ if (!runId)
26139
+ return null;
26140
+ const resolved = resolveTaskRunId(runId, db);
26141
+ const run = getTaskRun(resolved, db);
26142
+ if (!run)
26143
+ throw new Error(`Run not found: ${runId}`);
26144
+ if (run.task_id !== taskId)
26145
+ throw new Error(`Run ${resolved} belongs to task ${run.task_id}, not ${taskId}`);
26146
+ return resolved;
26147
+ }
26148
+ function getFindingByFingerprint(taskId, fingerprint2, db) {
26149
+ const row = db.query("SELECT * FROM task_findings WHERE task_id = ? AND fingerprint = ?").get(taskId, fingerprint2);
26150
+ return row ? rowToFinding(row) : null;
26151
+ }
26152
+ function assertTask(taskId, db) {
26153
+ if (!getTask(taskId, db))
26154
+ throw new TaskNotFoundError(taskId);
26155
+ }
26156
+ function nextFinding(input, db) {
26157
+ const fingerprint2 = normalizeFingerprint(input.fingerprint);
26158
+ const title = redactOptional(input.title, 300);
26159
+ if (!title)
26160
+ throw new Error("finding title is required");
26161
+ return {
26162
+ fingerprint: fingerprint2,
26163
+ run_id: resolveRunForTask(input.run_id, input.task_id, db),
26164
+ title,
26165
+ severity: normalizeSeverity(input.severity),
26166
+ status: normalizeStatus(input.status),
26167
+ source: redactOptional(input.source, 120),
26168
+ summary: redactOptional(input.summary, 2000),
26169
+ artifact_path: redactOptional(input.artifact_path, 1000),
26170
+ metadata: redactValue(input.metadata || {})
26171
+ };
26172
+ }
26173
+ function sameFinding(left, right) {
26174
+ return left.run_id === right.run_id && left.title === right.title && left.severity === right.severity && left.status === right.status && left.source === right.source && left.summary === right.summary && left.artifact_path === right.artifact_path && JSON.stringify(left.metadata) === JSON.stringify(right.metadata);
26175
+ }
26176
+ function upsertTaskFinding(input, db) {
26177
+ const d = db || getDatabase();
26178
+ assertTask(input.task_id, d);
26179
+ const timestamp3 = input.observed_at || now();
26180
+ const warnings = [];
26181
+ const next = nextFinding(input, d);
26182
+ const existing = getFindingByFingerprint(input.task_id, next.fingerprint, d);
26183
+ const dryRun = !input.apply;
26184
+ if (dryRun) {
26185
+ const action2 = existing ? upsertAction(existing, next) : "preview";
26186
+ return {
26187
+ schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
26188
+ local_only: true,
26189
+ dry_run: true,
26190
+ processed_at: timestamp3,
26191
+ action: action2,
26192
+ fingerprint: next.fingerprint,
26193
+ finding: existing ? compactFinding(previewFinding(existing, next, timestamp3)) : null,
26194
+ warnings
26195
+ };
26196
+ }
26197
+ if (!existing) {
26198
+ const id = uuid();
26199
+ d.run(`INSERT INTO task_findings (
26200
+ id, task_id, run_id, fingerprint, title, severity, status, source, summary, artifact_path,
26201
+ metadata, first_seen_at, last_seen_at, resolved_at, created_at, updated_at
26202
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
26203
+ id,
26204
+ input.task_id,
26205
+ next.run_id,
26206
+ next.fingerprint,
26207
+ next.title,
26208
+ next.severity,
26209
+ next.status,
26210
+ next.source,
26211
+ next.summary,
26212
+ next.artifact_path,
26213
+ JSON.stringify(next.metadata),
26214
+ timestamp3,
26215
+ timestamp3,
26216
+ next.status === "open" ? null : timestamp3,
26217
+ timestamp3,
26218
+ timestamp3
26219
+ ]);
26220
+ return {
26221
+ schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
26222
+ local_only: true,
26223
+ dry_run: false,
26224
+ processed_at: timestamp3,
26225
+ action: "created",
26226
+ fingerprint: next.fingerprint,
26227
+ finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
26228
+ warnings
26229
+ };
26230
+ }
26231
+ if (sameFinding(existing, next)) {
26232
+ return {
26233
+ schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
26234
+ local_only: true,
26235
+ dry_run: false,
26236
+ processed_at: timestamp3,
26237
+ action: "matched",
26238
+ fingerprint: next.fingerprint,
26239
+ finding: compactFinding(existing),
26240
+ warnings
26241
+ };
26242
+ }
26243
+ const action = upsertAction(existing, next);
26244
+ d.run(`UPDATE task_findings SET
26245
+ run_id = ?, title = ?, severity = ?, status = ?, source = ?, summary = ?, artifact_path = ?,
26246
+ metadata = ?, last_seen_at = ?, resolved_at = ?, updated_at = ?
26247
+ WHERE id = ?`, [
26248
+ next.run_id,
26249
+ next.title,
26250
+ next.severity,
26251
+ next.status,
26252
+ next.source,
26253
+ next.summary,
26254
+ next.artifact_path,
26255
+ JSON.stringify(next.metadata),
26256
+ timestamp3,
26257
+ next.status === "open" ? null : existing.resolved_at || timestamp3,
26258
+ timestamp3,
26259
+ existing.id
26260
+ ]);
26261
+ return {
26262
+ schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
26263
+ local_only: true,
26264
+ dry_run: false,
26265
+ processed_at: timestamp3,
26266
+ action,
26267
+ fingerprint: next.fingerprint,
26268
+ finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
26269
+ warnings
26270
+ };
26271
+ }
26272
+ function listTaskFindings(filter = {}, db) {
26273
+ const d = db || getDatabase();
26274
+ const conditions = ["1=1"];
26275
+ const params = [];
26276
+ if (filter.task_id) {
26277
+ conditions.push("task_id = ?");
26278
+ params.push(filter.task_id);
26279
+ }
26280
+ if (filter.run_id) {
26281
+ conditions.push("run_id = ?");
26282
+ params.push(resolveTaskRunId(filter.run_id, d));
26283
+ }
26284
+ if (filter.status) {
26285
+ conditions.push("status = ?");
26286
+ params.push(normalizeStatus(filter.status));
26287
+ }
26288
+ if (filter.source) {
26289
+ conditions.push("source = ?");
26290
+ params.push(redactOptional(filter.source, 120));
26291
+ }
26292
+ const limit = Math.min(Math.max(Math.floor(filter.limit ?? 50), 1), 500);
26293
+ const rows = d.query(`SELECT * FROM task_findings WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC, created_at DESC LIMIT ?`).all(...[...params, limit]);
26294
+ return rows.map(rowToFinding);
26295
+ }
26296
+ function listCompactTaskFindings(filter = {}, db) {
26297
+ return listTaskFindings(filter, db).map(compactFinding);
26298
+ }
26299
+ function resolveMissingTaskFindings(input, db) {
26300
+ const d = db || getDatabase();
26301
+ assertTask(input.task_id, d);
26302
+ const timestamp3 = input.resolved_at || now();
26303
+ const status = normalizeResolutionStatus(input.status);
26304
+ const runId = resolveRunForTask(input.run_id, input.task_id, d);
26305
+ const present = new Set(input.fingerprints.map(normalizeFingerprint));
26306
+ const warnings = [];
26307
+ const conditions = ["task_id = ?", "status = 'open'"];
26308
+ const params = [input.task_id];
26309
+ if (input.source) {
26310
+ conditions.push("source = ?");
26311
+ params.push(redactOptional(input.source, 120));
26312
+ }
26313
+ const candidates = d.query(`SELECT * FROM task_findings WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC, created_at DESC`).all(...params).map(rowToFinding).filter((finding2) => !present.has(finding2.fingerprint));
26314
+ const limit = Math.min(Math.max(Math.floor(input.limit ?? 50), 1), 200);
26315
+ const display = candidates.slice(0, limit);
26316
+ const omittedCount = Math.max(0, candidates.length - display.length);
26317
+ if (!input.apply) {
26318
+ return {
26319
+ schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
26320
+ local_only: true,
26321
+ dry_run: true,
26322
+ processed_at: timestamp3,
26323
+ action: candidates.length > 0 ? "preview" : "noop",
26324
+ task_id: input.task_id,
26325
+ source: input.source ? redactOptional(input.source, 120) : null,
26326
+ run_id: runId,
26327
+ present_fingerprint_count: present.size,
26328
+ candidate_count: candidates.length,
26329
+ changed_count: 0,
26330
+ omitted_count: omittedCount,
26331
+ findings: display.map(compactFinding),
26332
+ warnings
26333
+ };
26334
+ }
26335
+ const metadataPatch = redactValue({
26336
+ resolved_by: {
26337
+ agent_id: input.agent_id ?? null,
26338
+ run_id: runId,
26339
+ reason: input.reason ? redactEvidenceText(input.reason) : "missing from latest loop finding set"
26340
+ }
26341
+ });
26342
+ const tx = d.transaction(() => {
26343
+ for (const finding2 of candidates) {
26344
+ d.run("UPDATE task_findings SET status = ?, resolved_at = ?, metadata = ?, updated_at = ? WHERE id = ? AND status = 'open'", [
26345
+ status,
26346
+ timestamp3,
26347
+ JSON.stringify({ ...finding2.metadata, ...metadataPatch }),
26348
+ timestamp3,
26349
+ finding2.id
26350
+ ]);
26351
+ }
26352
+ });
26353
+ tx();
26354
+ const updated = candidates.map((finding2) => d.query("SELECT * FROM task_findings WHERE id = ?").get(finding2.id)).filter((row) => Boolean(row)).map(rowToFinding);
26355
+ const visibleUpdated = updated.slice(0, limit);
26356
+ return {
26357
+ schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
26358
+ local_only: true,
26359
+ dry_run: false,
26360
+ processed_at: timestamp3,
26361
+ action: updated.length > 0 ? status : "noop",
26362
+ task_id: input.task_id,
26363
+ source: input.source ? redactOptional(input.source, 120) : null,
26364
+ run_id: runId,
26365
+ present_fingerprint_count: present.size,
26366
+ candidate_count: candidates.length,
26367
+ changed_count: updated.length,
26368
+ omitted_count: omittedCount,
26369
+ findings: visibleUpdated.map(compactFinding),
26370
+ warnings
26371
+ };
26372
+ }
26373
+ var TASK_FINDING_SCHEMA_VERSION = "todos.task_finding.v1", TASK_FINDING_UPSERT_SCHEMA_VERSION = "todos.task_finding_upsert.v1", TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION = "todos.task_finding_resolve_missing.v1", SEVERITIES, STATUSES;
26374
+ var init_findings = __esm(() => {
26375
+ init_redaction();
26376
+ init_types();
26377
+ init_database();
26378
+ init_tasks();
26379
+ init_task_runs();
26380
+ SEVERITIES = new Set(["low", "medium", "high", "critical"]);
26381
+ STATUSES = new Set(["open", "resolved", "ignored"]);
26382
+ });
26383
+
25487
26384
  // src/lib/agent-run-dispatcher.ts
25488
26385
  function dispatcherFromRun(run) {
25489
26386
  const value = run.metadata["agent_run_dispatcher"];
@@ -26144,7 +27041,7 @@ function parseArray2(value) {
26144
27041
  return [];
26145
27042
  }
26146
27043
  }
26147
- function parseObject2(value) {
27044
+ function parseObject3(value) {
26148
27045
  try {
26149
27046
  const parsed = JSON.parse(value || "{}");
26150
27047
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
@@ -26185,7 +27082,7 @@ function rowToKnowledgeRecord(row) {
26185
27082
  agent_id: row.agent_id,
26186
27083
  snapshot_id: row.snapshot_id,
26187
27084
  tags: parseArray2(row.tags),
26188
- metadata: redactValue(parseObject2(row.metadata)),
27085
+ metadata: redactValue(parseObject3(row.metadata)),
26189
27086
  created_at: row.created_at,
26190
27087
  updated_at: row.updated_at
26191
27088
  };
@@ -26404,7 +27301,7 @@ function parseArray3(value) {
26404
27301
  return [];
26405
27302
  }
26406
27303
  }
26407
- function parseObject3(value) {
27304
+ function parseObject4(value) {
26408
27305
  try {
26409
27306
  const parsed = JSON.parse(value || "{}");
26410
27307
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
@@ -26453,7 +27350,7 @@ function rowToRisk(row) {
26453
27350
  plan_id: row.plan_id,
26454
27351
  task_id: row.task_id,
26455
27352
  tags: parseArray3(row.tags),
26456
- metadata: redactValue(parseObject3(row.metadata)),
27353
+ metadata: redactValue(parseObject4(row.metadata)),
26457
27354
  created_at: row.created_at,
26458
27355
  updated_at: row.updated_at,
26459
27356
  closed_at: row.closed_at
@@ -32295,6 +33192,38 @@ ${lines.join(`
32295
33192
  }
32296
33193
  });
32297
33194
  }
33195
+ if (shouldRegisterTool("begin_task_run_transaction")) {
33196
+ server.tool("begin_task_run_transaction", "Preview or apply an idempotent local loop run transaction keyed by a stable loop/run id.", {
33197
+ task_id: exports_external.string().describe("Task ID"),
33198
+ key: exports_external.string().optional().describe("Stable idempotency key"),
33199
+ loop_id: exports_external.string().optional().describe("Loop identifier; used as key fallback"),
33200
+ loop_run_id: exports_external.string().optional().describe("Loop run identifier; used as key fallback"),
33201
+ agent_id: exports_external.string().optional().describe("Agent starting the run"),
33202
+ title: exports_external.string().optional().describe("Run title"),
33203
+ summary: exports_external.string().optional().describe("Run summary"),
33204
+ metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata"),
33205
+ claim: exports_external.boolean().optional().describe("Claim/start the task before recording the run"),
33206
+ apply: exports_external.boolean().optional().describe("Apply the transaction; false or omitted is dry-run")
33207
+ }, async ({ task_id, key, loop_id, loop_run_id, agent_id, title, summary, metadata, claim, apply }) => {
33208
+ try {
33209
+ const result = beginTaskRunTransaction({
33210
+ task_id: resolveId(task_id),
33211
+ key,
33212
+ loop_id,
33213
+ loop_run_id,
33214
+ agent_id,
33215
+ title,
33216
+ summary,
33217
+ metadata,
33218
+ claim,
33219
+ apply
33220
+ });
33221
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
33222
+ } catch (e) {
33223
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33224
+ }
33225
+ });
33226
+ }
32298
33227
  if (shouldRegisterTool("list_task_runs")) {
32299
33228
  server.tool("list_task_runs", "List local run ledger entries, optionally scoped to a task.", { task_id: exports_external.string().optional().describe("Optional task ID") }, async ({ task_id }) => {
32300
33229
  try {
@@ -32392,15 +33321,117 @@ ${lines.join(`
32392
33321
  });
32393
33322
  }
32394
33323
  if (shouldRegisterTool("finish_task_run")) {
32395
- server.tool("finish_task_run", "Finish a local run ledger entry as completed, failed, or cancelled.", {
32396
- run_id: exports_external.string().describe("Run ID or prefix"),
32397
- status: exports_external.enum(["completed", "failed", "cancelled"]).describe("Final run status"),
33324
+ server.tool("finish_task_run", "Finish a local run ledger entry as completed, failed, or cancelled. Supports idempotent lookup by key.", {
33325
+ run_id: exports_external.string().optional().describe("Run ID or prefix"),
33326
+ key: exports_external.string().optional().describe("Idempotency key when run_id is omitted"),
33327
+ task_id: exports_external.string().optional().describe("Task scope for key lookup"),
33328
+ status: exports_external.enum(["completed", "failed", "cancelled"]).optional().describe("Final run status"),
32398
33329
  summary: exports_external.string().optional().describe("Final summary"),
32399
- agent_id: exports_external.string().optional().describe("Agent finishing the run")
32400
- }, async ({ run_id, status, summary, agent_id }) => {
33330
+ agent_id: exports_external.string().optional().describe("Agent finishing the run"),
33331
+ apply: exports_external.boolean().optional().describe("Apply the transaction; false previews without mutating")
33332
+ }, async ({ run_id, key, task_id, status, summary, agent_id, apply }) => {
33333
+ try {
33334
+ if (run_id && !key && apply === undefined) {
33335
+ const result2 = finishTaskRunTransaction({ run_id, status: status || "completed", summary, agent_id, apply: true });
33336
+ const run = result2.run ? getTaskRunLedger(result2.run.id).run : null;
33337
+ return { content: [{ type: "text", text: JSON.stringify(run, null, 2) }] };
33338
+ }
33339
+ const result = finishTaskRunTransaction({
33340
+ run_id,
33341
+ key,
33342
+ task_id: task_id ? resolveId(task_id) : undefined,
33343
+ status: status || "completed",
33344
+ summary,
33345
+ agent_id,
33346
+ apply: apply !== false
33347
+ });
33348
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
33349
+ } catch (e) {
33350
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33351
+ }
33352
+ });
33353
+ }
33354
+ if (shouldRegisterTool("upsert_task_finding")) {
33355
+ server.tool("upsert_task_finding", "Preview or apply an idempotent local finding upsert scoped by task and fingerprint.", {
33356
+ task_id: exports_external.string().describe("Task ID"),
33357
+ fingerprint: exports_external.string().describe("Stable finding fingerprint"),
33358
+ title: exports_external.string().describe("Finding title"),
33359
+ severity: exports_external.enum(["low", "medium", "high", "critical"]).optional().describe("Finding severity"),
33360
+ status: exports_external.enum(["open", "resolved", "ignored"]).optional().describe("Finding status"),
33361
+ source: exports_external.string().optional().describe("Loop/tool source name"),
33362
+ summary: exports_external.string().optional().describe("Bounded finding summary"),
33363
+ artifact_path: exports_external.string().optional().describe("Local artifact path/reference; content is not read"),
33364
+ run_id: exports_external.string().optional().describe("Run ledger ID or prefix"),
33365
+ metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata"),
33366
+ apply: exports_external.boolean().optional().describe("Apply the upsert; false or omitted is dry-run")
33367
+ }, async ({ task_id, fingerprint: fingerprint3, title, severity, status, source: source3, summary, artifact_path, run_id, metadata, apply }) => {
32401
33368
  try {
32402
- const run = finishTaskRun({ run_id, status, summary, agent_id });
32403
- return { content: [{ type: "text", text: JSON.stringify(run, null, 2) }] };
33369
+ const result = upsertTaskFinding({
33370
+ task_id: resolveId(task_id),
33371
+ fingerprint: fingerprint3,
33372
+ title,
33373
+ severity,
33374
+ status,
33375
+ source: source3,
33376
+ summary,
33377
+ artifact_path,
33378
+ run_id,
33379
+ metadata,
33380
+ apply
33381
+ });
33382
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
33383
+ } catch (e) {
33384
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33385
+ }
33386
+ });
33387
+ }
33388
+ if (shouldRegisterTool("list_task_findings")) {
33389
+ server.tool("list_task_findings", "List compact local findings with bounded output.", {
33390
+ task_id: exports_external.string().optional().describe("Filter by task"),
33391
+ run_id: exports_external.string().optional().describe("Filter by run"),
33392
+ status: exports_external.enum(["open", "resolved", "ignored"]).optional().describe("Filter by status"),
33393
+ source: exports_external.string().optional().describe("Filter by source"),
33394
+ limit: exports_external.number().optional().describe("Maximum findings to return")
33395
+ }, async ({ task_id, run_id, status, source: source3, limit }) => {
33396
+ try {
33397
+ const findings = listCompactTaskFindings({
33398
+ task_id: task_id ? resolveId(task_id) : undefined,
33399
+ run_id,
33400
+ status,
33401
+ source: source3,
33402
+ limit
33403
+ });
33404
+ return { content: [{ type: "text", text: JSON.stringify(findings) }] };
33405
+ } catch (e) {
33406
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33407
+ }
33408
+ });
33409
+ }
33410
+ if (shouldRegisterTool("resolve_missing_task_findings")) {
33411
+ server.tool("resolve_missing_task_findings", "Resolve open findings absent from the latest loop finding set.", {
33412
+ task_id: exports_external.string().describe("Task ID"),
33413
+ fingerprints: exports_external.array(exports_external.string()).optional().describe("Fingerprints still present in the latest run"),
33414
+ source: exports_external.string().optional().describe("Only resolve findings from this source"),
33415
+ run_id: exports_external.string().optional().describe("Run ledger ID or prefix for audit metadata"),
33416
+ status: exports_external.enum(["resolved", "ignored"]).optional().describe("Resolution status"),
33417
+ agent_id: exports_external.string().optional().describe("Agent resolving findings"),
33418
+ reason: exports_external.string().optional().describe("Resolution reason"),
33419
+ limit: exports_external.number().optional().describe("Maximum findings returned"),
33420
+ apply: exports_external.boolean().optional().describe("Apply resolution; false or omitted is dry-run")
33421
+ }, async ({ task_id, fingerprints, source: source3, run_id, status, agent_id, reason, limit, apply }) => {
33422
+ try {
33423
+ const result = resolveMissingTaskFindings({
33424
+ task_id: resolveId(task_id),
33425
+ fingerprints: fingerprints || [],
33426
+ source: source3,
33427
+ run_id,
33428
+ status,
33429
+ agent_id,
33430
+ reason,
33431
+ limit,
33432
+ apply
33433
+ });
33434
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
32404
33435
  } catch (e) {
32405
33436
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
32406
33437
  }
@@ -32736,6 +33767,7 @@ var init_task_resources = __esm(() => {
32736
33767
  init_agents();
32737
33768
  init_task_commits();
32738
33769
  init_task_runs();
33770
+ init_findings();
32739
33771
  init_agent_run_dispatcher();
32740
33772
  init_verification_providers();
32741
33773
  init_release_notes();
@@ -37280,6 +38312,39 @@ async function handleCreateTask(req, ctx, json2, taskToSummary2) {
37280
38312
  return json2({ error: e instanceof Error ? e.message : "Failed to create task" }, 500);
37281
38313
  }
37282
38314
  }
38315
+ async function handleUpsertTask(req, ctx, json2, taskToSummary2) {
38316
+ try {
38317
+ const body = await req.json();
38318
+ if (typeof body["fingerprint"] !== "string" || body["fingerprint"].trim() === "") {
38319
+ return json2({ error: "Missing 'fingerprint'" }, 400);
38320
+ }
38321
+ if (typeof body["title"] !== "string" || body["title"].trim() === "") {
38322
+ return json2({ error: "Missing 'title'" }, 400);
38323
+ }
38324
+ const metadata = body["metadata"] && typeof body["metadata"] === "object" && !Array.isArray(body["metadata"]) ? { ...body["metadata"] } : {};
38325
+ for (const key of ["expectation_id", "expectation_fingerprint", "evidence_paths", "origin_loop_id", "origin_run_id", "expected", "observed", "acceptance"]) {
38326
+ if (body[key] !== undefined)
38327
+ metadata[key] = body[key];
38328
+ }
38329
+ const result = upsertTaskByFingerprint({
38330
+ fingerprint: body["fingerprint"],
38331
+ title: body["title"],
38332
+ description: typeof body["description"] === "string" ? body["description"] : undefined,
38333
+ status: body["status"],
38334
+ priority: body["priority"],
38335
+ project_id: typeof body["project_id"] === "string" ? body["project_id"] : undefined,
38336
+ task_list_id: typeof body["task_list_id"] === "string" ? body["task_list_id"] : undefined,
38337
+ assigned_to: typeof body["assigned_to"] === "string" ? body["assigned_to"] : undefined,
38338
+ working_dir: typeof body["working_dir"] === "string" ? body["working_dir"] : undefined,
38339
+ tags: Array.isArray(body["tags"]) ? body["tags"].filter((tag) => typeof tag === "string") : undefined,
38340
+ metadata
38341
+ });
38342
+ ctx.broadcastEvent({ type: "task", task_id: result.task.id, action: result.created ? "created" : "updated", agent_id: result.task.agent_id, project_id: result.task.project_id });
38343
+ return json2({ created: result.created, task: taskToSummary2(result.task) }, result.created ? 201 : 200);
38344
+ } catch (e) {
38345
+ return json2({ error: e instanceof Error ? e.message : "Failed to upsert task" }, 500);
38346
+ }
38347
+ }
37283
38348
  function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
37284
38349
  const format = url.searchParams.get("format") || "json";
37285
38350
  const status = url.searchParams.get("status") || undefined;
@@ -37960,8 +39025,10 @@ function taskToSummary(task2, fields) {
37960
39025
  task_list_id: task2.task_list_id,
37961
39026
  agent_id: task2.agent_id,
37962
39027
  assigned_to: task2.assigned_to,
39028
+ working_dir: task2.working_dir,
37963
39029
  locked_by: task2.locked_by,
37964
39030
  tags: task2.tags,
39031
+ metadata: task2.metadata,
37965
39032
  version: task2.version,
37966
39033
  created_at: task2.created_at,
37967
39034
  updated_at: task2.updated_at,
@@ -38103,6 +39170,9 @@ Dashboard not found at: ${dashboardDir}`);
38103
39170
  if (path === "/api/tasks" && method === "POST") {
38104
39171
  return handleCreateTask(req, ctx, jsonWithCors, taskToSummary);
38105
39172
  }
39173
+ if (path === "/api/tasks/upsert" && method === "POST") {
39174
+ return handleUpsertTask(req, ctx, jsonWithCors, taskToSummary);
39175
+ }
38106
39176
  if (path === "/api/tasks/export" && method === "GET") {
38107
39177
  return handleTasksExport(req, url, ctx, jsonWithCors, taskToSummary);
38108
39178
  }
@@ -38298,7 +39368,7 @@ Dashboard not found at: ${dashboardDir}`);
38298
39368
  } catch {}
38299
39369
  }
38300
39370
  }
38301
- var MIME_TYPES, SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX = 120;
39371
+ var MIME_TYPES, SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX;
38302
39372
  var init_serve = __esm(() => {
38303
39373
  init_database();
38304
39374
  init_api_keys();
@@ -38323,6 +39393,7 @@ var init_serve = __esm(() => {
38323
39393
  "Permissions-Policy": "camera=, microphone=, geolocation="
38324
39394
  };
38325
39395
  rateLimitMap = new Map;
39396
+ RATE_LIMIT_MAX = Number.parseInt(process.env["TODOS_RATE_LIMIT_MAX"] || "120", 10);
38326
39397
  });
38327
39398
 
38328
39399
  // src/mcp/index.ts