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