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