@hasna/todos 0.11.58 → 0.11.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +38 -0
  2. package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
  3. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  4. package/dist/cli/index.js +1420 -187
  5. package/dist/contracts.d.ts.map +1 -1
  6. package/dist/contracts.js +502 -4
  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 +870 -5
  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 +984 -17
  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 +502 -4
  32. package/dist/release-provenance.json +3 -3
  33. package/dist/server/index.js +984 -17
  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 +375 -1
  39. package/dist/types/index.d.ts +11 -0
  40. package/dist/types/index.d.ts.map +1 -1
  41. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1139,6 +1139,49 @@ var init_migrations = __esm(() => {
1139
1139
  CREATE INDEX IF NOT EXISTS idx_local_retrospectives_plan ON local_retrospectives(plan_id);
1140
1140
  CREATE INDEX IF NOT EXISTS idx_local_retrospectives_agent ON local_retrospectives(agent_id);
1141
1141
  INSERT OR IGNORE INTO _migrations (id) VALUES (61);
1142
+ `,
1143
+ `
1144
+ CREATE TABLE IF NOT EXISTS task_run_transactions (
1145
+ id TEXT PRIMARY KEY,
1146
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1147
+ run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
1148
+ key TEXT NOT NULL,
1149
+ loop_id TEXT,
1150
+ loop_run_id TEXT,
1151
+ metadata TEXT DEFAULT '{}',
1152
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1153
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
1154
+ UNIQUE(task_id, key)
1155
+ );
1156
+ CREATE INDEX IF NOT EXISTS idx_task_run_transactions_task_key ON task_run_transactions(task_id, key);
1157
+ CREATE INDEX IF NOT EXISTS idx_task_run_transactions_key ON task_run_transactions(key);
1158
+ CREATE INDEX IF NOT EXISTS idx_task_run_transactions_run ON task_run_transactions(run_id);
1159
+
1160
+ CREATE TABLE IF NOT EXISTS task_findings (
1161
+ id TEXT PRIMARY KEY,
1162
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1163
+ run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
1164
+ fingerprint TEXT NOT NULL,
1165
+ title TEXT NOT NULL,
1166
+ severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low', 'medium', 'high', 'critical')),
1167
+ status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'resolved', 'ignored')),
1168
+ source TEXT,
1169
+ summary TEXT,
1170
+ artifact_path TEXT,
1171
+ metadata TEXT DEFAULT '{}',
1172
+ first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
1173
+ last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
1174
+ resolved_at TEXT,
1175
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1176
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
1177
+ UNIQUE(task_id, fingerprint)
1178
+ );
1179
+ CREATE INDEX IF NOT EXISTS idx_task_findings_task ON task_findings(task_id);
1180
+ CREATE INDEX IF NOT EXISTS idx_task_findings_run ON task_findings(run_id);
1181
+ CREATE INDEX IF NOT EXISTS idx_task_findings_status ON task_findings(status);
1182
+ CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source);
1183
+ CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint);
1184
+ INSERT OR IGNORE INTO _migrations (id) VALUES (62);
1142
1185
  `
1143
1186
  ];
1144
1187
  });
@@ -1576,6 +1619,47 @@ function ensureSchema(db) {
1576
1619
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_run ON task_run_artifacts(run_id)");
1577
1620
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_task ON task_run_artifacts(task_id)");
1578
1621
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_path ON task_run_artifacts(path)");
1622
+ ensureTable("task_run_transactions", `
1623
+ CREATE TABLE task_run_transactions (
1624
+ id TEXT PRIMARY KEY,
1625
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1626
+ run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
1627
+ key TEXT NOT NULL,
1628
+ loop_id TEXT,
1629
+ loop_run_id TEXT,
1630
+ metadata TEXT DEFAULT '{}',
1631
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1632
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
1633
+ UNIQUE(task_id, key)
1634
+ )`);
1635
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_task_key ON task_run_transactions(task_id, key)");
1636
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_key ON task_run_transactions(key)");
1637
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_run ON task_run_transactions(run_id)");
1638
+ ensureTable("task_findings", `
1639
+ CREATE TABLE task_findings (
1640
+ id TEXT PRIMARY KEY,
1641
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1642
+ run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
1643
+ fingerprint TEXT NOT NULL,
1644
+ title TEXT NOT NULL,
1645
+ severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low', 'medium', 'high', 'critical')),
1646
+ status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'resolved', 'ignored')),
1647
+ source TEXT,
1648
+ summary TEXT,
1649
+ artifact_path TEXT,
1650
+ metadata TEXT DEFAULT '{}',
1651
+ first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
1652
+ last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
1653
+ resolved_at TEXT,
1654
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1655
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
1656
+ UNIQUE(task_id, fingerprint)
1657
+ )`);
1658
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_task ON task_findings(task_id)");
1659
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_run ON task_findings(run_id)");
1660
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_status ON task_findings(status)");
1661
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source)");
1662
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint)");
1579
1663
  ensureTable("inbox_items", `
1580
1664
  CREATE TABLE inbox_items (
1581
1665
  id TEXT PRIMARY KEY,
@@ -3681,6 +3765,7 @@ var MCP_TOOL_GROUPS = {
3681
3765
  "add_task_run_event",
3682
3766
  "add_task_run_file",
3683
3767
  "acknowledge_handoff",
3768
+ "begin_task_run_transaction",
3684
3769
  "build_local_report",
3685
3770
  "cancel_agent_run_dispatch",
3686
3771
  "finish_task_run",
@@ -3728,6 +3813,7 @@ var MCP_TOOL_GROUPS = {
3728
3813
  "list_local_snapshots",
3729
3814
  "list_retrospectives",
3730
3815
  "list_risks",
3816
+ "list_task_findings",
3731
3817
  "list_task_runs",
3732
3818
  "list_verification_providers",
3733
3819
  "merge_duplicate_task",
@@ -3736,6 +3822,7 @@ var MCP_TOOL_GROUPS = {
3736
3822
  "remove_review_routing_rule",
3737
3823
  "restore_local_backup",
3738
3824
  "retry_agent_run_dispatch",
3825
+ "resolve_missing_task_findings",
3739
3826
  "resolve_mentions",
3740
3827
  "run_next_agent_dispatch",
3741
3828
  "search_knowledge_records",
@@ -3778,9 +3865,17 @@ var MCP_TOOL_GROUPS = {
3778
3865
  "unlock_file",
3779
3866
  "unwatch_task",
3780
3867
  "update_comment",
3868
+ "upsert_task_finding",
3781
3869
  "update_risk",
3782
3870
  "watch_task"
3783
3871
  ],
3872
+ loops: [
3873
+ "begin_task_run_transaction",
3874
+ "finish_task_run",
3875
+ "list_task_findings",
3876
+ "resolve_missing_task_findings",
3877
+ "upsert_task_finding"
3878
+ ],
3784
3879
  agents: [
3785
3880
  "auto_assign_task",
3786
3881
  "delete_agent",
@@ -3862,7 +3957,7 @@ var MCP_TOOL_GROUPS = {
3862
3957
  maintenance: ["extract_todos", "get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task", "watch_source_todos"]
3863
3958
  };
3864
3959
  var MCP_PROFILE_GROUPS = {
3865
- minimal: ["core"],
3960
+ minimal: ["core", "loops"],
3866
3961
  core: ["core"],
3867
3962
  standard: ["core", "tasks", "projects", "resources", "agents", "metadata"],
3868
3963
  agent: ["core", "tasks", "projects", "resources"],
@@ -4935,6 +5030,92 @@ var TODOS_JSON_CONTRACTS = [
4935
5030
  },
4936
5031
  optional: {}
4937
5032
  }),
5033
+ contract({
5034
+ id: "loop_run_transaction",
5035
+ name: "Loop Run Transaction",
5036
+ description: "Compact local result for idempotent loop run begin/finish transactions.",
5037
+ surfaces: ["cli", "mcp", "sdk"],
5038
+ stability: "stable",
5039
+ required: {
5040
+ schema_version: field("string", "Result schema version."),
5041
+ local_only: field("boolean", "Always true; loop run transactions use local state."),
5042
+ dry_run: field("boolean", "True when no run ledger mutation was applied."),
5043
+ processed_at: isoDateField,
5044
+ action: field("string", "preview, created, matched, finished, or conflict."),
5045
+ key: field("string", "Stable idempotency key used to dedupe the transaction."),
5046
+ run: field(["object", "null"], "Compact run summary or null for create previews.", true),
5047
+ warnings: field("array", "Non-fatal warnings such as terminal-status conflicts."),
5048
+ commands: field("array", "Follow-up CLI commands for agents and operators.")
5049
+ },
5050
+ optional: {}
5051
+ }),
5052
+ contract({
5053
+ id: "task_finding",
5054
+ name: "Task Finding",
5055
+ description: "Compact local finding record deduped by task and fingerprint.",
5056
+ surfaces: ["cli", "mcp", "sdk"],
5057
+ stability: "stable",
5058
+ required: {
5059
+ schema_version: field("string", "Finding schema version."),
5060
+ id: idField,
5061
+ task_id: idField,
5062
+ run_id: field(["string", "null"], "Optional run ledger ID.", true),
5063
+ fingerprint: field("string", "Stable finding fingerprint scoped to the task."),
5064
+ title: field("string", "Short redacted finding title."),
5065
+ severity: field("string", "low, medium, high, or critical."),
5066
+ status: field("string", "open, resolved, or ignored."),
5067
+ source: field(["string", "null"], "Optional loop/tool source.", true),
5068
+ summary: field(["string", "null"], "Bounded redacted finding summary.", true),
5069
+ artifact_path: field(["string", "null"], "Local artifact path/reference; raw content is not included.", true),
5070
+ first_seen_at: isoDateField,
5071
+ last_seen_at: isoDateField,
5072
+ resolved_at: field(["string", "null"], "Resolution timestamp when closed.", true),
5073
+ metadata_keys: field("array", "Sorted metadata keys; metadata values are intentionally omitted in compact output.")
5074
+ },
5075
+ optional: {}
5076
+ }),
5077
+ contract({
5078
+ id: "task_finding_upsert",
5079
+ name: "Task Finding Upsert Result",
5080
+ description: "Local-only dry-run or applied result from idempotently upserting a task finding.",
5081
+ surfaces: ["cli", "mcp", "sdk"],
5082
+ stability: "stable",
5083
+ required: {
5084
+ schema_version: field("string", "Result schema version."),
5085
+ local_only: field("boolean", "Always true; finding upserts use local state."),
5086
+ dry_run: field("boolean", "True when no finding row was created or updated."),
5087
+ processed_at: isoDateField,
5088
+ action: field("string", "preview, created, matched, updated, or reopened."),
5089
+ fingerprint: field("string", "Normalized finding fingerprint."),
5090
+ finding: field(["object", "null"], "Compact finding summary or null for create previews.", true),
5091
+ warnings: field("array", "Non-fatal warnings.")
5092
+ },
5093
+ optional: {}
5094
+ }),
5095
+ contract({
5096
+ id: "task_finding_resolve_missing",
5097
+ name: "Task Finding Resolve Missing Result",
5098
+ description: "Local-only dry-run or applied result from resolving open findings absent from the latest loop finding set.",
5099
+ surfaces: ["cli", "mcp", "sdk"],
5100
+ stability: "stable",
5101
+ required: {
5102
+ schema_version: field("string", "Result schema version."),
5103
+ local_only: field("boolean", "Always true; finding resolution uses local state."),
5104
+ dry_run: field("boolean", "True when no finding rows were changed."),
5105
+ processed_at: isoDateField,
5106
+ action: field("string", "preview, resolved, ignored, or noop."),
5107
+ task_id: idField,
5108
+ source: field(["string", "null"], "Optional source scope.", true),
5109
+ run_id: field(["string", "null"], "Optional run ledger ID used for audit metadata.", true),
5110
+ present_fingerprint_count: field("integer", "Number of fingerprints supplied as still present."),
5111
+ candidate_count: field("integer", "Open findings missing from the supplied set."),
5112
+ changed_count: field("integer", "Rows resolved or ignored by the applied transaction."),
5113
+ omitted_count: field("integer", "Matching findings omitted from bounded output."),
5114
+ findings: field("array", "Bounded compact finding summaries."),
5115
+ warnings: field("array", "Non-fatal warnings.")
5116
+ },
5117
+ optional: {}
5118
+ }),
4938
5119
  contract({
4939
5120
  id: "verification_provider",
4940
5121
  name: "Verification Provider",
@@ -6640,6 +6821,7 @@ var LOCAL_EVENT_TYPES = [
6640
6821
  "task.blocked",
6641
6822
  "task.started",
6642
6823
  "task.completed",
6824
+ "task.updated",
6643
6825
  "task.due",
6644
6826
  "task.due_soon",
6645
6827
  "task.failed",
@@ -7978,6 +8160,17 @@ function replaceTaskTags(taskId, tags, db) {
7978
8160
  db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
7979
8161
  insertTaskTags(taskId, tags, db);
7980
8162
  }
8163
+ function addMetadataConditions(metadata, conditions, params) {
8164
+ if (!metadata)
8165
+ return;
8166
+ for (const [key, value] of Object.entries(metadata)) {
8167
+ if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
8168
+ throw new Error(`Invalid metadata filter key: ${key}`);
8169
+ }
8170
+ conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
8171
+ params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
8172
+ }
8173
+ }
7981
8174
  function createTask(input, db) {
7982
8175
  const d = db || getDatabase();
7983
8176
  const timestamp = now();
@@ -8160,6 +8353,7 @@ function listTasks(filter = {}, db) {
8160
8353
  params.push(filter.task_type);
8161
8354
  }
8162
8355
  }
8356
+ addMetadataConditions(filter.metadata, conditions, params);
8163
8357
  const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
8164
8358
  if (filter.cursor) {
8165
8359
  try {
@@ -8184,6 +8378,54 @@ function listTasks(filter = {}, db) {
8184
8378
  const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
8185
8379
  return rows.map(rowToTask);
8186
8380
  }
8381
+ function getTaskByFingerprint(fingerprint, db) {
8382
+ const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
8383
+ return tasks[0] ?? null;
8384
+ }
8385
+ function mergeTaskMetadata(current, next, fingerprint) {
8386
+ return {
8387
+ ...current,
8388
+ ...next ?? {},
8389
+ fingerprint
8390
+ };
8391
+ }
8392
+ function upsertTaskByFingerprint(input, db) {
8393
+ const d = db || getDatabase();
8394
+ const fingerprint = input.fingerprint.trim();
8395
+ if (!fingerprint)
8396
+ throw new Error("fingerprint is required");
8397
+ const existing = getTaskByFingerprint(fingerprint, d);
8398
+ const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
8399
+ if (!existing) {
8400
+ const task2 = createTask({ ...input, metadata }, d);
8401
+ return { task: task2, created: true };
8402
+ }
8403
+ const task = updateTask(existing.id, {
8404
+ version: existing.version,
8405
+ title: input.title,
8406
+ description: input.description,
8407
+ status: input.status,
8408
+ priority: input.priority,
8409
+ project_id: input.project_id,
8410
+ assigned_to: input.assigned_to,
8411
+ working_dir: input.working_dir,
8412
+ plan_id: input.plan_id,
8413
+ task_list_id: input.task_list_id,
8414
+ tags: input.tags,
8415
+ metadata,
8416
+ due_at: input.due_at,
8417
+ estimated_minutes: input.estimated_minutes,
8418
+ sla_minutes: input.sla_minutes,
8419
+ confidence: input.confidence,
8420
+ retry_count: input.retry_count,
8421
+ max_retries: input.max_retries,
8422
+ retry_after: input.retry_after,
8423
+ requires_approval: input.requires_approval,
8424
+ recurrence_rule: input.recurrence_rule,
8425
+ task_type: input.task_type
8426
+ }, d);
8427
+ return { task, created: false };
8428
+ }
8187
8429
  function countTasks(filter = {}, db) {
8188
8430
  const d = db || getDatabase();
8189
8431
  const conditions = [];
@@ -8247,6 +8489,7 @@ function countTasks(filter = {}, db) {
8247
8489
  conditions.push("task_list_id = ?");
8248
8490
  params.push(filter.task_list_id);
8249
8491
  }
8492
+ addMetadataConditions(filter.metadata, conditions, params);
8250
8493
  if (!filter.include_archived) {
8251
8494
  conditions.push("archived_at IS NULL");
8252
8495
  }
@@ -8297,6 +8540,10 @@ function updateTask(id, input, db) {
8297
8540
  sets.push("assigned_to = ?");
8298
8541
  params.push(input.assigned_to);
8299
8542
  }
8543
+ if (input.working_dir !== undefined) {
8544
+ sets.push("working_dir = ?");
8545
+ params.push(input.working_dir);
8546
+ }
8300
8547
  if (input.tags !== undefined) {
8301
8548
  sets.push("tags = ?");
8302
8549
  params.push(JSON.stringify(input.tags));
@@ -8385,6 +8632,8 @@ function updateTask(id, input, db) {
8385
8632
  logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
8386
8633
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
8387
8634
  logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
8635
+ if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
8636
+ logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
8388
8637
  if (input.approved_by !== undefined)
8389
8638
  logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
8390
8639
  const updatedTask = {
@@ -8420,6 +8669,10 @@ function updateTask(id, input, db) {
8420
8669
  if (input.approved_by !== undefined) {
8421
8670
  emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
8422
8671
  }
8672
+ const updatePayload = taskEventData(updatedTask);
8673
+ dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
8674
+ emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
8675
+ emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
8423
8676
  return updatedTask;
8424
8677
  }
8425
8678
  function deleteTask(id, db) {
@@ -11115,6 +11368,7 @@ function getTaskTraceability(taskId, db) {
11115
11368
 
11116
11369
  // src/db/task-runs.ts
11117
11370
  init_redaction();
11371
+ var LOOP_RUN_TRANSACTION_SCHEMA_VERSION = "todos.loop_run_transaction.v1";
11118
11372
  function parseObject(value) {
11119
11373
  if (!value)
11120
11374
  return {};
@@ -11137,6 +11391,72 @@ function rowToArtifact(row) {
11137
11391
  function getRunRow(runId, db) {
11138
11392
  return db.query("SELECT * FROM task_runs WHERE id = ?").get(runId);
11139
11393
  }
11394
+ function normalizeTransactionKey(input) {
11395
+ const key = (input.key || input.loop_run_id || input.loop_id || "").trim();
11396
+ if (!key)
11397
+ throw new Error("idempotent run transactions require --key, --loop-run-id, or --loop-id");
11398
+ return key.toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 240);
11399
+ }
11400
+ function loopTransactionMetadata(record) {
11401
+ const value = record.metadata["loop_transaction"];
11402
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
11403
+ }
11404
+ function runKey(record) {
11405
+ const tx = loopTransactionMetadata(record);
11406
+ const key = tx["idempotency_key"] ?? record.metadata["idempotency_key"];
11407
+ return typeof key === "string" ? key : null;
11408
+ }
11409
+ function loopId(record) {
11410
+ const tx = loopTransactionMetadata(record);
11411
+ const value = tx["loop_id"] ?? record.metadata["loop_id"];
11412
+ return typeof value === "string" ? value : null;
11413
+ }
11414
+ function loopRunId(record) {
11415
+ const tx = loopTransactionMetadata(record);
11416
+ const value = tx["loop_run_id"] ?? record.metadata["loop_run_id"];
11417
+ return typeof value === "string" ? value : null;
11418
+ }
11419
+ function getTaskRunTransactionByKey(key, taskId, db) {
11420
+ if (taskId) {
11421
+ return db.query("SELECT * FROM task_run_transactions WHERE task_id = ? AND key = ?").get(taskId, key);
11422
+ }
11423
+ const rows = db.query("SELECT * FROM task_run_transactions WHERE key = ? ORDER BY created_at DESC LIMIT 2").all(key);
11424
+ if (rows.length > 1)
11425
+ throw new Error(`Run transaction key is ambiguous across tasks: ${key}. Pass task_id.`);
11426
+ return rows[0] ?? null;
11427
+ }
11428
+ function summarizeTaskRun(run) {
11429
+ return {
11430
+ id: run.id,
11431
+ task_id: run.task_id,
11432
+ agent_id: run.agent_id,
11433
+ title: run.title,
11434
+ status: run.status,
11435
+ summary: run.summary,
11436
+ idempotency_key: runKey(run),
11437
+ loop_id: loopId(run),
11438
+ loop_run_id: loopRunId(run),
11439
+ metadata_keys: Object.keys(run.metadata).sort(),
11440
+ started_at: run.started_at,
11441
+ completed_at: run.completed_at,
11442
+ updated_at: run.updated_at
11443
+ };
11444
+ }
11445
+ function findTaskRunByTransactionKey(key, taskId, db) {
11446
+ const d = db || getDatabase();
11447
+ const normalized = normalizeTransactionKey({ key });
11448
+ const transaction = getTaskRunTransactionByKey(normalized, taskId, d);
11449
+ if (transaction?.run_id)
11450
+ return getTaskRun(transaction.run_id, d);
11451
+ return null;
11452
+ }
11453
+ function loopRunCommands(run, key) {
11454
+ return [
11455
+ run ? `todos runs show ${run.id.slice(0, 8)}` : "todos runs list",
11456
+ run ? `todos findings list --task ${run.task_id.slice(0, 8)} --json` : "todos findings list --json",
11457
+ `todos runs begin <task-id> --key ${key} --apply --json`
11458
+ ];
11459
+ }
11140
11460
  function resolveTaskRunId(idOrPrefix, db) {
11141
11461
  const d = db || getDatabase();
11142
11462
  const rows = d.query("SELECT id FROM task_runs WHERE id = ? OR id LIKE ? ORDER BY created_at DESC LIMIT 2").all(idOrPrefix, `${idOrPrefix}%`);
@@ -11155,7 +11475,7 @@ function startTaskRun(input, db) {
11155
11475
  const d = db || getDatabase();
11156
11476
  if (!getTask(input.task_id, d))
11157
11477
  throw new TaskNotFoundError(input.task_id);
11158
- const id = uuid();
11478
+ const id = input.id ?? uuid();
11159
11479
  const timestamp = input.started_at || now();
11160
11480
  if (input.claim && input.agent_id) {
11161
11481
  startTask(input.task_id, input.agent_id, d);
@@ -11193,6 +11513,97 @@ function startTaskRun(input, db) {
11193
11513
  emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
11194
11514
  return run;
11195
11515
  }
11516
+ function beginTaskRunTransaction(input, db) {
11517
+ const d = db || getDatabase();
11518
+ if (!getTask(input.task_id, d))
11519
+ throw new TaskNotFoundError(input.task_id);
11520
+ const timestamp = input.started_at || now();
11521
+ const key = normalizeTransactionKey(input);
11522
+ const existing = findTaskRunByTransactionKey(key, input.task_id, d);
11523
+ const dryRun = !input.apply;
11524
+ if (existing) {
11525
+ return {
11526
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11527
+ local_only: true,
11528
+ dry_run: dryRun,
11529
+ processed_at: timestamp,
11530
+ action: "matched",
11531
+ key,
11532
+ run: summarizeTaskRun(existing),
11533
+ warnings: existing.status === "running" ? [] : [`matched ${existing.status} run`],
11534
+ commands: loopRunCommands(existing, key)
11535
+ };
11536
+ }
11537
+ if (dryRun) {
11538
+ return {
11539
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11540
+ local_only: true,
11541
+ dry_run: true,
11542
+ processed_at: timestamp,
11543
+ action: "preview",
11544
+ key,
11545
+ run: null,
11546
+ warnings: [],
11547
+ commands: loopRunCommands(null, key)
11548
+ };
11549
+ }
11550
+ const metadata = redactValue({
11551
+ ...input.metadata || {},
11552
+ loop_transaction: {
11553
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11554
+ idempotency_key: key,
11555
+ loop_id: input.loop_id ?? null,
11556
+ loop_run_id: input.loop_run_id ?? null,
11557
+ first_seen_at: timestamp
11558
+ },
11559
+ idempotency_key: key
11560
+ });
11561
+ const created = d.transaction(() => {
11562
+ d.run(`INSERT OR IGNORE INTO task_run_transactions (
11563
+ id, task_id, run_id, key, loop_id, loop_run_id, metadata, created_at, updated_at
11564
+ ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?)`, [
11565
+ uuid(),
11566
+ input.task_id,
11567
+ key,
11568
+ input.loop_id ?? null,
11569
+ input.loop_run_id ?? null,
11570
+ JSON.stringify(metadata),
11571
+ timestamp,
11572
+ timestamp
11573
+ ]);
11574
+ const transaction = getTaskRunTransactionByKey(key, input.task_id, d);
11575
+ if (!transaction)
11576
+ throw new Error(`Could not create run transaction for key: ${key}`);
11577
+ if (transaction.run_id) {
11578
+ const existingRun = getTaskRun(transaction.run_id, d);
11579
+ if (existingRun)
11580
+ return { run: existingRun, action: "matched" };
11581
+ }
11582
+ const run = startTaskRun({
11583
+ id: uuid(),
11584
+ task_id: input.task_id,
11585
+ agent_id: input.agent_id,
11586
+ title: input.title,
11587
+ summary: input.summary,
11588
+ metadata,
11589
+ claim: input.claim,
11590
+ started_at: timestamp
11591
+ }, d);
11592
+ 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]);
11593
+ return { run, action: "created" };
11594
+ })();
11595
+ return {
11596
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11597
+ local_only: true,
11598
+ dry_run: false,
11599
+ processed_at: timestamp,
11600
+ action: created.action,
11601
+ key,
11602
+ run: summarizeTaskRun(created.run),
11603
+ warnings: [],
11604
+ commands: loopRunCommands(created.run, key)
11605
+ };
11606
+ }
11196
11607
  function addTaskRunEvent(input, db) {
11197
11608
  const d = db || getDatabase();
11198
11609
  const runId = resolveTaskRunId(input.run_id, d);
@@ -11372,6 +11783,66 @@ function finishTaskRun(input, db) {
11372
11783
  });
11373
11784
  return updated;
11374
11785
  }
11786
+ function finishTaskRunTransaction(input, db) {
11787
+ const d = db || getDatabase();
11788
+ const timestamp = input.completed_at || now();
11789
+ const status = input.status || "completed";
11790
+ const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
11791
+ const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
11792
+ if (!run) {
11793
+ throw new Error(input.run_id ? `Run not found: ${input.run_id}` : "runs finish requires a run id or --key");
11794
+ }
11795
+ if (input.task_id && run.task_id !== input.task_id) {
11796
+ throw new Error(`Run ${run.id} belongs to task ${run.task_id}, not ${input.task_id}`);
11797
+ }
11798
+ const resolvedKey = key || runKey(run) || run.id;
11799
+ const dryRun = input.apply === false;
11800
+ if (run.status !== "running") {
11801
+ const conflict = run.status !== status;
11802
+ return {
11803
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11804
+ local_only: true,
11805
+ dry_run: dryRun,
11806
+ processed_at: timestamp,
11807
+ action: conflict ? "conflict" : "matched",
11808
+ key: resolvedKey,
11809
+ run: summarizeTaskRun(run),
11810
+ warnings: conflict ? [`run is already ${run.status}; requested ${status}`] : [],
11811
+ commands: loopRunCommands(run, resolvedKey)
11812
+ };
11813
+ }
11814
+ if (dryRun) {
11815
+ return {
11816
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11817
+ local_only: true,
11818
+ dry_run: true,
11819
+ processed_at: timestamp,
11820
+ action: "preview",
11821
+ key: resolvedKey,
11822
+ run: summarizeTaskRun({ ...run, status, summary: input.summary ?? run.summary, completed_at: timestamp, updated_at: timestamp }),
11823
+ warnings: [],
11824
+ commands: loopRunCommands(run, resolvedKey)
11825
+ };
11826
+ }
11827
+ const finished = finishTaskRun({
11828
+ run_id: run.id,
11829
+ status,
11830
+ summary: input.summary,
11831
+ agent_id: input.agent_id,
11832
+ completed_at: timestamp
11833
+ }, d);
11834
+ return {
11835
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11836
+ local_only: true,
11837
+ dry_run: false,
11838
+ processed_at: timestamp,
11839
+ action: "finished",
11840
+ key: resolvedKey,
11841
+ run: summarizeTaskRun(finished),
11842
+ warnings: [],
11843
+ commands: loopRunCommands(finished, resolvedKey)
11844
+ };
11845
+ }
11375
11846
  function listTaskRuns(taskId, db) {
11376
11847
  const d = db || getDatabase();
11377
11848
  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();
@@ -17552,7 +18023,7 @@ function countMovedStandaloneVerifications(db, duplicateId) {
17552
18023
  const rows = db.query("SELECT command FROM task_verifications WHERE task_id = ?").all(duplicateId);
17553
18024
  return rows.filter((row) => !generatedCommands.has(row.command)).length;
17554
18025
  }
17555
- function mergeTaskMetadata(primary, duplicate, input, mergedAt) {
18026
+ function mergeTaskMetadata2(primary, duplicate, input, mergedAt) {
17556
18027
  const mergedDuplicates = Array.isArray(primary.metadata["merged_duplicates"]) ? [...primary.metadata["merged_duplicates"]] : [];
17557
18028
  mergedDuplicates.push({
17558
18029
  id: duplicate.id,
@@ -17613,7 +18084,7 @@ function mergeDuplicateTask(input, db) {
17613
18084
  updateTask(primary.id, {
17614
18085
  version: primary.version,
17615
18086
  tags: mergedTags,
17616
- metadata: mergeTaskMetadata(primary, duplicate, input, mergedAt),
18087
+ metadata: mergeTaskMetadata2(primary, duplicate, input, mergedAt),
17617
18088
  description: mergeTaskDescription(primary, duplicate) ?? undefined
17618
18089
  }, d);
17619
18090
  moved.comments = updateRows(d, "task_comments", "task_id", duplicate.id, primary.id);
@@ -20084,6 +20555,33 @@ var TODOS_API_ROUTES = [
20084
20555
  tags: ["tasks", "mutation"],
20085
20556
  stability: "stable"
20086
20557
  },
20558
+ {
20559
+ id: "tasks.upsert",
20560
+ method: "POST",
20561
+ path: "/api/tasks/upsert",
20562
+ description: "Create or update a task by stable metadata fingerprint, merging metadata on updates.",
20563
+ auth: "optional-api-key",
20564
+ requestSchema: {
20565
+ type: "object",
20566
+ properties: {
20567
+ fingerprint: { type: "string" },
20568
+ title: { type: "string" },
20569
+ description: { type: "string" },
20570
+ priority: { type: "string", enum: TASK_PRIORITIES },
20571
+ status: { type: "string", enum: TASK_STATUSES },
20572
+ project_id: { type: "string" },
20573
+ task_list_id: { type: "string" },
20574
+ working_dir: { type: "string" },
20575
+ tags: { type: "array", items: { type: "string" } },
20576
+ metadata: objectSchema
20577
+ },
20578
+ required: ["fingerprint", "title"],
20579
+ additionalProperties: true
20580
+ },
20581
+ responseSchema: objectSchema,
20582
+ tags: ["tasks", "mutation", "dedupe"],
20583
+ stability: "stable"
20584
+ },
20087
20585
  {
20088
20586
  id: "tasks.read",
20089
20587
  method: "GET",
@@ -28962,6 +29460,7 @@ var READ_ONLY_TOOLS = new Set([
28962
29460
  "describe_tools",
28963
29461
  "search_tools",
28964
29462
  "inspect_git_commit",
29463
+ "list_task_findings",
28965
29464
  "scan_text_for_secrets",
28966
29465
  "check_workspace_permission",
28967
29466
  "check_sandbox_command",
@@ -28987,7 +29486,12 @@ var MINIMAL_TOOLS = new Set([
28987
29486
  "bootstrap",
28988
29487
  "get_tasks_changed_since",
28989
29488
  "heartbeat",
28990
- "release_agent"
29489
+ "release_agent",
29490
+ "begin_task_run_transaction",
29491
+ "finish_task_run",
29492
+ "upsert_task_finding",
29493
+ "list_task_findings",
29494
+ "resolve_missing_task_findings"
28991
29495
  ]);
28992
29496
  var AGENT_SAFE_TOOLS = new Set([
28993
29497
  ...READ_ONLY_TOOLS,
@@ -41825,6 +42329,353 @@ async function dispatchToMultiple(input, opts = {}, db) {
41825
42329
  }
41826
42330
  return dispatches;
41827
42331
  }
42332
+ // src/db/findings.ts
42333
+ init_redaction();
42334
+ init_types();
42335
+ init_database();
42336
+ var TASK_FINDING_SCHEMA_VERSION = "todos.task_finding.v1";
42337
+ var TASK_FINDING_UPSERT_SCHEMA_VERSION = "todos.task_finding_upsert.v1";
42338
+ var TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION = "todos.task_finding_resolve_missing.v1";
42339
+ var SEVERITIES2 = new Set(["low", "medium", "high", "critical"]);
42340
+ var STATUSES = new Set(["open", "resolved", "ignored"]);
42341
+ function parseObject4(value) {
42342
+ if (!value)
42343
+ return {};
42344
+ try {
42345
+ const parsed = JSON.parse(value);
42346
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
42347
+ } catch {
42348
+ return {};
42349
+ }
42350
+ }
42351
+ function normalizeKey(value) {
42352
+ return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
42353
+ }
42354
+ function normalizeFingerprint(value) {
42355
+ const normalized = normalizeKey(value);
42356
+ if (!normalized)
42357
+ throw new Error("finding fingerprint is required");
42358
+ return normalized.slice(0, 240);
42359
+ }
42360
+ function normalizeSeverity2(value) {
42361
+ const normalized = normalizeKey(value || "medium");
42362
+ if (SEVERITIES2.has(normalized))
42363
+ return normalized;
42364
+ if (/^(p0|blocker|urgent|highest)$/.test(normalized))
42365
+ return "critical";
42366
+ if (/^(p1|major)$/.test(normalized))
42367
+ return "high";
42368
+ if (/^(p3|minor|info)$/.test(normalized))
42369
+ return "low";
42370
+ return "medium";
42371
+ }
42372
+ function normalizeStatus(value) {
42373
+ const normalized = normalizeKey(value || "open");
42374
+ if (STATUSES.has(normalized))
42375
+ return normalized;
42376
+ if (normalized === "closed" || normalized === "fixed")
42377
+ return "resolved";
42378
+ return "open";
42379
+ }
42380
+ function normalizeResolutionStatus(value) {
42381
+ const status = normalizeStatus(value || "resolved");
42382
+ if (status === "open")
42383
+ throw new Error("resolve-missing status must be resolved or ignored");
42384
+ return status;
42385
+ }
42386
+ function redactOptional(value, max = 2000) {
42387
+ if (!value)
42388
+ return null;
42389
+ const redacted = redactEvidenceText(value).trim();
42390
+ if (!redacted)
42391
+ return null;
42392
+ return redacted.length > max ? `${redacted.slice(0, max - 3)}...` : redacted;
42393
+ }
42394
+ function rowToFinding(row) {
42395
+ return {
42396
+ schema_version: TASK_FINDING_SCHEMA_VERSION,
42397
+ ...row,
42398
+ severity: normalizeSeverity2(row.severity),
42399
+ status: normalizeStatus(row.status),
42400
+ metadata: parseObject4(row.metadata)
42401
+ };
42402
+ }
42403
+ function compactFinding(finding2) {
42404
+ return {
42405
+ schema_version: TASK_FINDING_SCHEMA_VERSION,
42406
+ id: finding2.id,
42407
+ task_id: finding2.task_id,
42408
+ run_id: finding2.run_id,
42409
+ fingerprint: finding2.fingerprint,
42410
+ title: finding2.title,
42411
+ severity: finding2.severity,
42412
+ status: finding2.status,
42413
+ source: finding2.source,
42414
+ summary: finding2.summary,
42415
+ artifact_path: finding2.artifact_path,
42416
+ first_seen_at: finding2.first_seen_at,
42417
+ last_seen_at: finding2.last_seen_at,
42418
+ resolved_at: finding2.resolved_at,
42419
+ metadata_keys: Object.keys(finding2.metadata).sort()
42420
+ };
42421
+ }
42422
+ function previewFinding(existing, next, timestamp2) {
42423
+ return {
42424
+ ...existing,
42425
+ run_id: next.run_id,
42426
+ title: next.title,
42427
+ severity: next.severity,
42428
+ status: next.status,
42429
+ source: next.source,
42430
+ summary: next.summary,
42431
+ artifact_path: next.artifact_path,
42432
+ metadata: next.metadata,
42433
+ last_seen_at: timestamp2,
42434
+ resolved_at: next.status === "open" ? null : existing.resolved_at || timestamp2,
42435
+ updated_at: timestamp2
42436
+ };
42437
+ }
42438
+ function upsertAction(existing, next) {
42439
+ if (sameFinding(existing, next))
42440
+ return "matched";
42441
+ return existing.status !== "open" && next.status === "open" ? "reopened" : "updated";
42442
+ }
42443
+ function resolveRunForTask(runId, taskId, db) {
42444
+ if (!runId)
42445
+ return null;
42446
+ const resolved = resolveTaskRunId(runId, db);
42447
+ const run = getTaskRun(resolved, db);
42448
+ if (!run)
42449
+ throw new Error(`Run not found: ${runId}`);
42450
+ if (run.task_id !== taskId)
42451
+ throw new Error(`Run ${resolved} belongs to task ${run.task_id}, not ${taskId}`);
42452
+ return resolved;
42453
+ }
42454
+ function getFindingByFingerprint(taskId, fingerprint4, db) {
42455
+ const row = db.query("SELECT * FROM task_findings WHERE task_id = ? AND fingerprint = ?").get(taskId, fingerprint4);
42456
+ return row ? rowToFinding(row) : null;
42457
+ }
42458
+ function assertTask(taskId, db) {
42459
+ if (!getTask(taskId, db))
42460
+ throw new TaskNotFoundError(taskId);
42461
+ }
42462
+ function nextFinding(input, db) {
42463
+ const fingerprint4 = normalizeFingerprint(input.fingerprint);
42464
+ const title = redactOptional(input.title, 300);
42465
+ if (!title)
42466
+ throw new Error("finding title is required");
42467
+ return {
42468
+ fingerprint: fingerprint4,
42469
+ run_id: resolveRunForTask(input.run_id, input.task_id, db),
42470
+ title,
42471
+ severity: normalizeSeverity2(input.severity),
42472
+ status: normalizeStatus(input.status),
42473
+ source: redactOptional(input.source, 120),
42474
+ summary: redactOptional(input.summary, 2000),
42475
+ artifact_path: redactOptional(input.artifact_path, 1000),
42476
+ metadata: redactValue(input.metadata || {})
42477
+ };
42478
+ }
42479
+ function sameFinding(left, right) {
42480
+ 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);
42481
+ }
42482
+ function upsertTaskFinding(input, db) {
42483
+ const d = db || getDatabase();
42484
+ assertTask(input.task_id, d);
42485
+ const timestamp2 = input.observed_at || now();
42486
+ const warnings = [];
42487
+ const next = nextFinding(input, d);
42488
+ const existing = getFindingByFingerprint(input.task_id, next.fingerprint, d);
42489
+ const dryRun = !input.apply;
42490
+ if (dryRun) {
42491
+ const action2 = existing ? upsertAction(existing, next) : "preview";
42492
+ return {
42493
+ schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
42494
+ local_only: true,
42495
+ dry_run: true,
42496
+ processed_at: timestamp2,
42497
+ action: action2,
42498
+ fingerprint: next.fingerprint,
42499
+ finding: existing ? compactFinding(previewFinding(existing, next, timestamp2)) : null,
42500
+ warnings
42501
+ };
42502
+ }
42503
+ if (!existing) {
42504
+ const id = uuid();
42505
+ d.run(`INSERT INTO task_findings (
42506
+ id, task_id, run_id, fingerprint, title, severity, status, source, summary, artifact_path,
42507
+ metadata, first_seen_at, last_seen_at, resolved_at, created_at, updated_at
42508
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
42509
+ id,
42510
+ input.task_id,
42511
+ next.run_id,
42512
+ next.fingerprint,
42513
+ next.title,
42514
+ next.severity,
42515
+ next.status,
42516
+ next.source,
42517
+ next.summary,
42518
+ next.artifact_path,
42519
+ JSON.stringify(next.metadata),
42520
+ timestamp2,
42521
+ timestamp2,
42522
+ next.status === "open" ? null : timestamp2,
42523
+ timestamp2,
42524
+ timestamp2
42525
+ ]);
42526
+ return {
42527
+ schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
42528
+ local_only: true,
42529
+ dry_run: false,
42530
+ processed_at: timestamp2,
42531
+ action: "created",
42532
+ fingerprint: next.fingerprint,
42533
+ finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
42534
+ warnings
42535
+ };
42536
+ }
42537
+ if (sameFinding(existing, next)) {
42538
+ return {
42539
+ schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
42540
+ local_only: true,
42541
+ dry_run: false,
42542
+ processed_at: timestamp2,
42543
+ action: "matched",
42544
+ fingerprint: next.fingerprint,
42545
+ finding: compactFinding(existing),
42546
+ warnings
42547
+ };
42548
+ }
42549
+ const action = upsertAction(existing, next);
42550
+ d.run(`UPDATE task_findings SET
42551
+ run_id = ?, title = ?, severity = ?, status = ?, source = ?, summary = ?, artifact_path = ?,
42552
+ metadata = ?, last_seen_at = ?, resolved_at = ?, updated_at = ?
42553
+ WHERE id = ?`, [
42554
+ next.run_id,
42555
+ next.title,
42556
+ next.severity,
42557
+ next.status,
42558
+ next.source,
42559
+ next.summary,
42560
+ next.artifact_path,
42561
+ JSON.stringify(next.metadata),
42562
+ timestamp2,
42563
+ next.status === "open" ? null : existing.resolved_at || timestamp2,
42564
+ timestamp2,
42565
+ existing.id
42566
+ ]);
42567
+ return {
42568
+ schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
42569
+ local_only: true,
42570
+ dry_run: false,
42571
+ processed_at: timestamp2,
42572
+ action,
42573
+ fingerprint: next.fingerprint,
42574
+ finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
42575
+ warnings
42576
+ };
42577
+ }
42578
+ function listTaskFindings(filter = {}, db) {
42579
+ const d = db || getDatabase();
42580
+ const conditions = ["1=1"];
42581
+ const params = [];
42582
+ if (filter.task_id) {
42583
+ conditions.push("task_id = ?");
42584
+ params.push(filter.task_id);
42585
+ }
42586
+ if (filter.run_id) {
42587
+ conditions.push("run_id = ?");
42588
+ params.push(resolveTaskRunId(filter.run_id, d));
42589
+ }
42590
+ if (filter.status) {
42591
+ conditions.push("status = ?");
42592
+ params.push(normalizeStatus(filter.status));
42593
+ }
42594
+ if (filter.source) {
42595
+ conditions.push("source = ?");
42596
+ params.push(redactOptional(filter.source, 120));
42597
+ }
42598
+ const limit = Math.min(Math.max(Math.floor(filter.limit ?? 50), 1), 500);
42599
+ const rows = d.query(`SELECT * FROM task_findings WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC, created_at DESC LIMIT ?`).all(...[...params, limit]);
42600
+ return rows.map(rowToFinding);
42601
+ }
42602
+ function listCompactTaskFindings(filter = {}, db) {
42603
+ return listTaskFindings(filter, db).map(compactFinding);
42604
+ }
42605
+ function resolveMissingTaskFindings(input, db) {
42606
+ const d = db || getDatabase();
42607
+ assertTask(input.task_id, d);
42608
+ const timestamp2 = input.resolved_at || now();
42609
+ const status = normalizeResolutionStatus(input.status);
42610
+ const runId = resolveRunForTask(input.run_id, input.task_id, d);
42611
+ const present = new Set(input.fingerprints.map(normalizeFingerprint));
42612
+ const warnings = [];
42613
+ const conditions = ["task_id = ?", "status = 'open'"];
42614
+ const params = [input.task_id];
42615
+ if (input.source) {
42616
+ conditions.push("source = ?");
42617
+ params.push(redactOptional(input.source, 120));
42618
+ }
42619
+ 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));
42620
+ const limit = Math.min(Math.max(Math.floor(input.limit ?? 50), 1), 200);
42621
+ const display = candidates.slice(0, limit);
42622
+ const omittedCount = Math.max(0, candidates.length - display.length);
42623
+ if (!input.apply) {
42624
+ return {
42625
+ schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
42626
+ local_only: true,
42627
+ dry_run: true,
42628
+ processed_at: timestamp2,
42629
+ action: candidates.length > 0 ? "preview" : "noop",
42630
+ task_id: input.task_id,
42631
+ source: input.source ? redactOptional(input.source, 120) : null,
42632
+ run_id: runId,
42633
+ present_fingerprint_count: present.size,
42634
+ candidate_count: candidates.length,
42635
+ changed_count: 0,
42636
+ omitted_count: omittedCount,
42637
+ findings: display.map(compactFinding),
42638
+ warnings
42639
+ };
42640
+ }
42641
+ const metadataPatch = redactValue({
42642
+ resolved_by: {
42643
+ agent_id: input.agent_id ?? null,
42644
+ run_id: runId,
42645
+ reason: input.reason ? redactEvidenceText(input.reason) : "missing from latest loop finding set"
42646
+ }
42647
+ });
42648
+ const tx = d.transaction(() => {
42649
+ for (const finding2 of candidates) {
42650
+ d.run("UPDATE task_findings SET status = ?, resolved_at = ?, metadata = ?, updated_at = ? WHERE id = ? AND status = 'open'", [
42651
+ status,
42652
+ timestamp2,
42653
+ JSON.stringify({ ...finding2.metadata, ...metadataPatch }),
42654
+ timestamp2,
42655
+ finding2.id
42656
+ ]);
42657
+ }
42658
+ });
42659
+ tx();
42660
+ const updated = candidates.map((finding2) => d.query("SELECT * FROM task_findings WHERE id = ?").get(finding2.id)).filter((row) => Boolean(row)).map(rowToFinding);
42661
+ const visibleUpdated = updated.slice(0, limit);
42662
+ return {
42663
+ schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
42664
+ local_only: true,
42665
+ dry_run: false,
42666
+ processed_at: timestamp2,
42667
+ action: updated.length > 0 ? status : "noop",
42668
+ task_id: input.task_id,
42669
+ source: input.source ? redactOptional(input.source, 120) : null,
42670
+ run_id: runId,
42671
+ present_fingerprint_count: present.size,
42672
+ candidate_count: candidates.length,
42673
+ changed_count: updated.length,
42674
+ omitted_count: omittedCount,
42675
+ findings: visibleUpdated.map(compactFinding),
42676
+ warnings
42677
+ };
42678
+ }
41828
42679
 
41829
42680
  // src/index.ts
41830
42681
  init_redaction();
@@ -43155,6 +44006,8 @@ export {
43155
44006
  upsertTesterIssueReports,
43156
44007
  upsertTesterIssueReport,
43157
44008
  upsertTerminalNotificationRule,
44009
+ upsertTaskFinding,
44010
+ upsertTaskByFingerprint,
43158
44011
  upsertSecretSafetyConfig,
43159
44012
  upsertRunnerSandboxProfile,
43160
44013
  upsertReviewRoutingRule,
@@ -43204,6 +44057,7 @@ export {
43204
44057
  syncWithAgent,
43205
44058
  syncKgEdges,
43206
44059
  supersedeDecisionRecord,
44060
+ summarizeTaskRun,
43207
44061
  summarizeRoadmap,
43208
44062
  summarizeMilestone,
43209
44063
  suggestAgentNames,
@@ -43281,6 +44135,7 @@ export {
43281
44135
  resolvePlanRef,
43282
44136
  resolvePlanId,
43283
44137
  resolvePartialId,
44138
+ resolveMissingTaskFindings,
43284
44139
  resolveMentions,
43285
44140
  resolveGitRoot,
43286
44141
  resolveCommandQuery,
@@ -43455,6 +44310,7 @@ export {
43455
44310
  listTasks,
43456
44311
  listTaskRuns,
43457
44312
  listTaskLists,
44313
+ listTaskFindings,
43458
44314
  listTaskFiles,
43459
44315
  listTaskBoards,
43460
44316
  listSubscriptions,
@@ -43507,6 +44363,7 @@ export {
43507
44363
  listCyclesWithStats,
43508
44364
  listCycles,
43509
44365
  listCustomFieldDefinitions,
44366
+ listCompactTaskFindings,
43510
44367
  listComments,
43511
44368
  listCommandAliases,
43512
44369
  listCapacityProfiles,
@@ -43613,6 +44470,7 @@ export {
43613
44470
  getTaskCustomFields,
43614
44471
  getTaskContract,
43615
44472
  getTaskCommits,
44473
+ getTaskByFingerprint,
43616
44474
  getTaskBoard,
43617
44475
  getTask,
43618
44476
  getStoredHandoffAsPacket,
@@ -43792,10 +44650,12 @@ export {
43792
44650
  formatDecisionRecordMarkdown,
43793
44651
  formatAgentWorkflowDemoReport,
43794
44652
  formatActivityRecordText,
44653
+ finishTaskRunTransaction,
43795
44654
  finishTaskRun,
43796
44655
  fingerprintTesterIssueReport,
43797
44656
  fingerprintInboxInput,
43798
44657
  findTasksByFile,
44658
+ findTaskRunByTransactionKey,
43799
44659
  findTaskByCommit,
43800
44660
  findRelatedTaskIds,
43801
44661
  findPath,
@@ -44017,6 +44877,7 @@ export {
44017
44877
  buildCodebaseIndex,
44018
44878
  buildArtifactExportManifest,
44019
44879
  bootstrapProject,
44880
+ beginTaskRunTransaction,
44020
44881
  backupDatabase,
44021
44882
  autoReleaseStaleAgents,
44022
44883
  autoDetectFileRelationships,
@@ -44102,6 +44963,9 @@ export {
44102
44963
  TASK_STATUSES,
44103
44964
  TASK_SCHEDULING_SCHEMA,
44104
44965
  TASK_PRIORITIES,
44966
+ TASK_FINDING_UPSERT_SCHEMA_VERSION,
44967
+ TASK_FINDING_SCHEMA_VERSION,
44968
+ TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
44105
44969
  STORAGE_TABLES,
44106
44970
  SECRET_REDACTION_SCHEMA,
44107
44971
  SCHEMA_SEMVER,
@@ -44138,6 +45002,7 @@ export {
44138
45002
  MANPAGE_SCHEMA,
44139
45003
  MACHINE_TOPOLOGY_SCHEMA,
44140
45004
  LockError,
45005
+ LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
44141
45006
  LOCAL_USAGE_LEDGER_SCHEMA_VERSION,
44142
45007
  LOCAL_ROADMAP_SCHEMA_VERSION,
44143
45008
  LOCAL_REPORT_TYPES,