@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/cli/index.js CHANGED
@@ -3223,6 +3223,49 @@ var init_migrations = __esm(() => {
3223
3223
  CREATE INDEX IF NOT EXISTS idx_local_retrospectives_plan ON local_retrospectives(plan_id);
3224
3224
  CREATE INDEX IF NOT EXISTS idx_local_retrospectives_agent ON local_retrospectives(agent_id);
3225
3225
  INSERT OR IGNORE INTO _migrations (id) VALUES (61);
3226
+ `,
3227
+ `
3228
+ CREATE TABLE IF NOT EXISTS task_run_transactions (
3229
+ id TEXT PRIMARY KEY,
3230
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
3231
+ run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
3232
+ key TEXT NOT NULL,
3233
+ loop_id TEXT,
3234
+ loop_run_id TEXT,
3235
+ metadata TEXT DEFAULT '{}',
3236
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
3237
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
3238
+ UNIQUE(task_id, key)
3239
+ );
3240
+ CREATE INDEX IF NOT EXISTS idx_task_run_transactions_task_key ON task_run_transactions(task_id, key);
3241
+ CREATE INDEX IF NOT EXISTS idx_task_run_transactions_key ON task_run_transactions(key);
3242
+ CREATE INDEX IF NOT EXISTS idx_task_run_transactions_run ON task_run_transactions(run_id);
3243
+
3244
+ CREATE TABLE IF NOT EXISTS task_findings (
3245
+ id TEXT PRIMARY KEY,
3246
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
3247
+ run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
3248
+ fingerprint TEXT NOT NULL,
3249
+ title TEXT NOT NULL,
3250
+ severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low', 'medium', 'high', 'critical')),
3251
+ status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'resolved', 'ignored')),
3252
+ source TEXT,
3253
+ summary TEXT,
3254
+ artifact_path TEXT,
3255
+ metadata TEXT DEFAULT '{}',
3256
+ first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
3257
+ last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
3258
+ resolved_at TEXT,
3259
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
3260
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
3261
+ UNIQUE(task_id, fingerprint)
3262
+ );
3263
+ CREATE INDEX IF NOT EXISTS idx_task_findings_task ON task_findings(task_id);
3264
+ CREATE INDEX IF NOT EXISTS idx_task_findings_run ON task_findings(run_id);
3265
+ CREATE INDEX IF NOT EXISTS idx_task_findings_status ON task_findings(status);
3266
+ CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source);
3267
+ CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint);
3268
+ INSERT OR IGNORE INTO _migrations (id) VALUES (62);
3226
3269
  `
3227
3270
  ];
3228
3271
  });
@@ -3660,6 +3703,47 @@ function ensureSchema(db) {
3660
3703
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_run ON task_run_artifacts(run_id)");
3661
3704
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_task ON task_run_artifacts(task_id)");
3662
3705
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_path ON task_run_artifacts(path)");
3706
+ ensureTable("task_run_transactions", `
3707
+ CREATE TABLE task_run_transactions (
3708
+ id TEXT PRIMARY KEY,
3709
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
3710
+ run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
3711
+ key TEXT NOT NULL,
3712
+ loop_id TEXT,
3713
+ loop_run_id TEXT,
3714
+ metadata TEXT DEFAULT '{}',
3715
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
3716
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
3717
+ UNIQUE(task_id, key)
3718
+ )`);
3719
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_task_key ON task_run_transactions(task_id, key)");
3720
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_key ON task_run_transactions(key)");
3721
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_run ON task_run_transactions(run_id)");
3722
+ ensureTable("task_findings", `
3723
+ CREATE TABLE task_findings (
3724
+ id TEXT PRIMARY KEY,
3725
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
3726
+ run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
3727
+ fingerprint TEXT NOT NULL,
3728
+ title TEXT NOT NULL,
3729
+ severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low', 'medium', 'high', 'critical')),
3730
+ status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'resolved', 'ignored')),
3731
+ source TEXT,
3732
+ summary TEXT,
3733
+ artifact_path TEXT,
3734
+ metadata TEXT DEFAULT '{}',
3735
+ first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
3736
+ last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
3737
+ resolved_at TEXT,
3738
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
3739
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
3740
+ UNIQUE(task_id, fingerprint)
3741
+ )`);
3742
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_task ON task_findings(task_id)");
3743
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_run ON task_findings(run_id)");
3744
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_status ON task_findings(status)");
3745
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source)");
3746
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint)");
3663
3747
  ensureTable("inbox_items", `
3664
3748
  CREATE TABLE inbox_items (
3665
3749
  id TEXT PRIMARY KEY,
@@ -6134,6 +6218,7 @@ var init_event_hooks = __esm(() => {
6134
6218
  "task.blocked",
6135
6219
  "task.started",
6136
6220
  "task.completed",
6221
+ "task.updated",
6137
6222
  "task.due",
6138
6223
  "task.due_soon",
6139
6224
  "task.failed",
@@ -7463,6 +7548,17 @@ function replaceTaskTags(taskId, tags, db) {
7463
7548
  db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
7464
7549
  insertTaskTags(taskId, tags, db);
7465
7550
  }
7551
+ function addMetadataConditions(metadata, conditions, params) {
7552
+ if (!metadata)
7553
+ return;
7554
+ for (const [key, value] of Object.entries(metadata)) {
7555
+ if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
7556
+ throw new Error(`Invalid metadata filter key: ${key}`);
7557
+ }
7558
+ conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
7559
+ params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
7560
+ }
7561
+ }
7466
7562
  function createTask(input, db) {
7467
7563
  const d = db || getDatabase();
7468
7564
  const timestamp = now();
@@ -7645,6 +7741,7 @@ function listTasks(filter = {}, db) {
7645
7741
  params.push(filter.task_type);
7646
7742
  }
7647
7743
  }
7744
+ addMetadataConditions(filter.metadata, conditions, params);
7648
7745
  const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
7649
7746
  if (filter.cursor) {
7650
7747
  try {
@@ -7669,6 +7766,54 @@ function listTasks(filter = {}, db) {
7669
7766
  const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
7670
7767
  return rows.map(rowToTask);
7671
7768
  }
7769
+ function getTaskByFingerprint(fingerprint, db) {
7770
+ const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
7771
+ return tasks[0] ?? null;
7772
+ }
7773
+ function mergeTaskMetadata(current, next, fingerprint) {
7774
+ return {
7775
+ ...current,
7776
+ ...next ?? {},
7777
+ fingerprint
7778
+ };
7779
+ }
7780
+ function upsertTaskByFingerprint(input, db) {
7781
+ const d = db || getDatabase();
7782
+ const fingerprint = input.fingerprint.trim();
7783
+ if (!fingerprint)
7784
+ throw new Error("fingerprint is required");
7785
+ const existing = getTaskByFingerprint(fingerprint, d);
7786
+ const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
7787
+ if (!existing) {
7788
+ const task2 = createTask({ ...input, metadata }, d);
7789
+ return { task: task2, created: true };
7790
+ }
7791
+ const task = updateTask(existing.id, {
7792
+ version: existing.version,
7793
+ title: input.title,
7794
+ description: input.description,
7795
+ status: input.status,
7796
+ priority: input.priority,
7797
+ project_id: input.project_id,
7798
+ assigned_to: input.assigned_to,
7799
+ working_dir: input.working_dir,
7800
+ plan_id: input.plan_id,
7801
+ task_list_id: input.task_list_id,
7802
+ tags: input.tags,
7803
+ metadata,
7804
+ due_at: input.due_at,
7805
+ estimated_minutes: input.estimated_minutes,
7806
+ sla_minutes: input.sla_minutes,
7807
+ confidence: input.confidence,
7808
+ retry_count: input.retry_count,
7809
+ max_retries: input.max_retries,
7810
+ retry_after: input.retry_after,
7811
+ requires_approval: input.requires_approval,
7812
+ recurrence_rule: input.recurrence_rule,
7813
+ task_type: input.task_type
7814
+ }, d);
7815
+ return { task, created: false };
7816
+ }
7672
7817
  function countTasks(filter = {}, db) {
7673
7818
  const d = db || getDatabase();
7674
7819
  const conditions = [];
@@ -7732,6 +7877,7 @@ function countTasks(filter = {}, db) {
7732
7877
  conditions.push("task_list_id = ?");
7733
7878
  params.push(filter.task_list_id);
7734
7879
  }
7880
+ addMetadataConditions(filter.metadata, conditions, params);
7735
7881
  if (!filter.include_archived) {
7736
7882
  conditions.push("archived_at IS NULL");
7737
7883
  }
@@ -7782,6 +7928,10 @@ function updateTask(id, input, db) {
7782
7928
  sets.push("assigned_to = ?");
7783
7929
  params.push(input.assigned_to);
7784
7930
  }
7931
+ if (input.working_dir !== undefined) {
7932
+ sets.push("working_dir = ?");
7933
+ params.push(input.working_dir);
7934
+ }
7785
7935
  if (input.tags !== undefined) {
7786
7936
  sets.push("tags = ?");
7787
7937
  params.push(JSON.stringify(input.tags));
@@ -7870,6 +8020,8 @@ function updateTask(id, input, db) {
7870
8020
  logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
7871
8021
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
7872
8022
  logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
8023
+ if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
8024
+ logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
7873
8025
  if (input.approved_by !== undefined)
7874
8026
  logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
7875
8027
  const updatedTask = {
@@ -7905,6 +8057,10 @@ function updateTask(id, input, db) {
7905
8057
  if (input.approved_by !== undefined) {
7906
8058
  emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
7907
8059
  }
8060
+ const updatePayload = taskEventData(updatedTask);
8061
+ dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
8062
+ emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
8063
+ emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
7908
8064
  return updatedTask;
7909
8065
  }
7910
8066
  function deleteTask(id, db) {
@@ -10903,17 +11059,22 @@ var init_task_commits = __esm(() => {
10903
11059
  var exports_task_runs = {};
10904
11060
  __export(exports_task_runs, {
10905
11061
  verifyTaskRunArtifacts: () => verifyTaskRunArtifacts,
11062
+ summarizeTaskRun: () => summarizeTaskRun,
10906
11063
  startTaskRun: () => startTaskRun,
10907
11064
  resolveTaskRunId: () => resolveTaskRunId,
10908
11065
  redactEvidenceText: () => redactEvidenceText,
10909
11066
  listTaskRuns: () => listTaskRuns,
10910
11067
  getTaskRunLedger: () => getTaskRunLedger,
10911
11068
  getTaskRun: () => getTaskRun,
11069
+ finishTaskRunTransaction: () => finishTaskRunTransaction,
10912
11070
  finishTaskRun: () => finishTaskRun,
11071
+ findTaskRunByTransactionKey: () => findTaskRunByTransactionKey,
11072
+ beginTaskRunTransaction: () => beginTaskRunTransaction,
10913
11073
  addTaskRunFile: () => addTaskRunFile,
10914
11074
  addTaskRunEvent: () => addTaskRunEvent,
10915
11075
  addTaskRunCommand: () => addTaskRunCommand,
10916
- addTaskRunArtifact: () => addTaskRunArtifact
11076
+ addTaskRunArtifact: () => addTaskRunArtifact,
11077
+ LOOP_RUN_TRANSACTION_SCHEMA_VERSION: () => LOOP_RUN_TRANSACTION_SCHEMA_VERSION
10917
11078
  });
10918
11079
  function parseObject(value) {
10919
11080
  if (!value)
@@ -10937,6 +11098,72 @@ function rowToArtifact(row) {
10937
11098
  function getRunRow(runId, db) {
10938
11099
  return db.query("SELECT * FROM task_runs WHERE id = ?").get(runId);
10939
11100
  }
11101
+ function normalizeTransactionKey(input) {
11102
+ const key = (input.key || input.loop_run_id || input.loop_id || "").trim();
11103
+ if (!key)
11104
+ throw new Error("idempotent run transactions require --key, --loop-run-id, or --loop-id");
11105
+ return key.toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 240);
11106
+ }
11107
+ function loopTransactionMetadata(record) {
11108
+ const value = record.metadata["loop_transaction"];
11109
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
11110
+ }
11111
+ function runKey(record) {
11112
+ const tx = loopTransactionMetadata(record);
11113
+ const key = tx["idempotency_key"] ?? record.metadata["idempotency_key"];
11114
+ return typeof key === "string" ? key : null;
11115
+ }
11116
+ function loopId(record) {
11117
+ const tx = loopTransactionMetadata(record);
11118
+ const value = tx["loop_id"] ?? record.metadata["loop_id"];
11119
+ return typeof value === "string" ? value : null;
11120
+ }
11121
+ function loopRunId(record) {
11122
+ const tx = loopTransactionMetadata(record);
11123
+ const value = tx["loop_run_id"] ?? record.metadata["loop_run_id"];
11124
+ return typeof value === "string" ? value : null;
11125
+ }
11126
+ function getTaskRunTransactionByKey(key, taskId, db) {
11127
+ if (taskId) {
11128
+ return db.query("SELECT * FROM task_run_transactions WHERE task_id = ? AND key = ?").get(taskId, key);
11129
+ }
11130
+ const rows = db.query("SELECT * FROM task_run_transactions WHERE key = ? ORDER BY created_at DESC LIMIT 2").all(key);
11131
+ if (rows.length > 1)
11132
+ throw new Error(`Run transaction key is ambiguous across tasks: ${key}. Pass task_id.`);
11133
+ return rows[0] ?? null;
11134
+ }
11135
+ function summarizeTaskRun(run) {
11136
+ return {
11137
+ id: run.id,
11138
+ task_id: run.task_id,
11139
+ agent_id: run.agent_id,
11140
+ title: run.title,
11141
+ status: run.status,
11142
+ summary: run.summary,
11143
+ idempotency_key: runKey(run),
11144
+ loop_id: loopId(run),
11145
+ loop_run_id: loopRunId(run),
11146
+ metadata_keys: Object.keys(run.metadata).sort(),
11147
+ started_at: run.started_at,
11148
+ completed_at: run.completed_at,
11149
+ updated_at: run.updated_at
11150
+ };
11151
+ }
11152
+ function findTaskRunByTransactionKey(key, taskId, db) {
11153
+ const d = db || getDatabase();
11154
+ const normalized = normalizeTransactionKey({ key });
11155
+ const transaction = getTaskRunTransactionByKey(normalized, taskId, d);
11156
+ if (transaction?.run_id)
11157
+ return getTaskRun(transaction.run_id, d);
11158
+ return null;
11159
+ }
11160
+ function loopRunCommands(run, key) {
11161
+ return [
11162
+ run ? `todos runs show ${run.id.slice(0, 8)}` : "todos runs list",
11163
+ run ? `todos findings list --task ${run.task_id.slice(0, 8)} --json` : "todos findings list --json",
11164
+ `todos runs begin <task-id> --key ${key} --apply --json`
11165
+ ];
11166
+ }
10940
11167
  function resolveTaskRunId(idOrPrefix, db) {
10941
11168
  const d = db || getDatabase();
10942
11169
  const rows = d.query("SELECT id FROM task_runs WHERE id = ? OR id LIKE ? ORDER BY created_at DESC LIMIT 2").all(idOrPrefix, `${idOrPrefix}%`);
@@ -10955,7 +11182,7 @@ function startTaskRun(input, db) {
10955
11182
  const d = db || getDatabase();
10956
11183
  if (!getTask(input.task_id, d))
10957
11184
  throw new TaskNotFoundError(input.task_id);
10958
- const id = uuid();
11185
+ const id = input.id ?? uuid();
10959
11186
  const timestamp = input.started_at || now();
10960
11187
  if (input.claim && input.agent_id) {
10961
11188
  startTask(input.task_id, input.agent_id, d);
@@ -10993,6 +11220,97 @@ function startTaskRun(input, db) {
10993
11220
  emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
10994
11221
  return run;
10995
11222
  }
11223
+ function beginTaskRunTransaction(input, db) {
11224
+ const d = db || getDatabase();
11225
+ if (!getTask(input.task_id, d))
11226
+ throw new TaskNotFoundError(input.task_id);
11227
+ const timestamp = input.started_at || now();
11228
+ const key = normalizeTransactionKey(input);
11229
+ const existing = findTaskRunByTransactionKey(key, input.task_id, d);
11230
+ const dryRun = !input.apply;
11231
+ if (existing) {
11232
+ return {
11233
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11234
+ local_only: true,
11235
+ dry_run: dryRun,
11236
+ processed_at: timestamp,
11237
+ action: "matched",
11238
+ key,
11239
+ run: summarizeTaskRun(existing),
11240
+ warnings: existing.status === "running" ? [] : [`matched ${existing.status} run`],
11241
+ commands: loopRunCommands(existing, key)
11242
+ };
11243
+ }
11244
+ if (dryRun) {
11245
+ return {
11246
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11247
+ local_only: true,
11248
+ dry_run: true,
11249
+ processed_at: timestamp,
11250
+ action: "preview",
11251
+ key,
11252
+ run: null,
11253
+ warnings: [],
11254
+ commands: loopRunCommands(null, key)
11255
+ };
11256
+ }
11257
+ const metadata = redactValue({
11258
+ ...input.metadata || {},
11259
+ loop_transaction: {
11260
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11261
+ idempotency_key: key,
11262
+ loop_id: input.loop_id ?? null,
11263
+ loop_run_id: input.loop_run_id ?? null,
11264
+ first_seen_at: timestamp
11265
+ },
11266
+ idempotency_key: key
11267
+ });
11268
+ const created = d.transaction(() => {
11269
+ d.run(`INSERT OR IGNORE INTO task_run_transactions (
11270
+ id, task_id, run_id, key, loop_id, loop_run_id, metadata, created_at, updated_at
11271
+ ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?)`, [
11272
+ uuid(),
11273
+ input.task_id,
11274
+ key,
11275
+ input.loop_id ?? null,
11276
+ input.loop_run_id ?? null,
11277
+ JSON.stringify(metadata),
11278
+ timestamp,
11279
+ timestamp
11280
+ ]);
11281
+ const transaction = getTaskRunTransactionByKey(key, input.task_id, d);
11282
+ if (!transaction)
11283
+ throw new Error(`Could not create run transaction for key: ${key}`);
11284
+ if (transaction.run_id) {
11285
+ const existingRun = getTaskRun(transaction.run_id, d);
11286
+ if (existingRun)
11287
+ return { run: existingRun, action: "matched" };
11288
+ }
11289
+ const run = startTaskRun({
11290
+ id: uuid(),
11291
+ task_id: input.task_id,
11292
+ agent_id: input.agent_id,
11293
+ title: input.title,
11294
+ summary: input.summary,
11295
+ metadata,
11296
+ claim: input.claim,
11297
+ started_at: timestamp
11298
+ }, d);
11299
+ 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]);
11300
+ return { run, action: "created" };
11301
+ })();
11302
+ return {
11303
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11304
+ local_only: true,
11305
+ dry_run: false,
11306
+ processed_at: timestamp,
11307
+ action: created.action,
11308
+ key,
11309
+ run: summarizeTaskRun(created.run),
11310
+ warnings: [],
11311
+ commands: loopRunCommands(created.run, key)
11312
+ };
11313
+ }
10996
11314
  function addTaskRunEvent(input, db) {
10997
11315
  const d = db || getDatabase();
10998
11316
  const runId = resolveTaskRunId(input.run_id, d);
@@ -11172,6 +11490,66 @@ function finishTaskRun(input, db) {
11172
11490
  });
11173
11491
  return updated;
11174
11492
  }
11493
+ function finishTaskRunTransaction(input, db) {
11494
+ const d = db || getDatabase();
11495
+ const timestamp = input.completed_at || now();
11496
+ const status = input.status || "completed";
11497
+ const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
11498
+ const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
11499
+ if (!run) {
11500
+ throw new Error(input.run_id ? `Run not found: ${input.run_id}` : "runs finish requires a run id or --key");
11501
+ }
11502
+ if (input.task_id && run.task_id !== input.task_id) {
11503
+ throw new Error(`Run ${run.id} belongs to task ${run.task_id}, not ${input.task_id}`);
11504
+ }
11505
+ const resolvedKey = key || runKey(run) || run.id;
11506
+ const dryRun = input.apply === false;
11507
+ if (run.status !== "running") {
11508
+ const conflict = run.status !== status;
11509
+ return {
11510
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11511
+ local_only: true,
11512
+ dry_run: dryRun,
11513
+ processed_at: timestamp,
11514
+ action: conflict ? "conflict" : "matched",
11515
+ key: resolvedKey,
11516
+ run: summarizeTaskRun(run),
11517
+ warnings: conflict ? [`run is already ${run.status}; requested ${status}`] : [],
11518
+ commands: loopRunCommands(run, resolvedKey)
11519
+ };
11520
+ }
11521
+ if (dryRun) {
11522
+ return {
11523
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11524
+ local_only: true,
11525
+ dry_run: true,
11526
+ processed_at: timestamp,
11527
+ action: "preview",
11528
+ key: resolvedKey,
11529
+ run: summarizeTaskRun({ ...run, status, summary: input.summary ?? run.summary, completed_at: timestamp, updated_at: timestamp }),
11530
+ warnings: [],
11531
+ commands: loopRunCommands(run, resolvedKey)
11532
+ };
11533
+ }
11534
+ const finished = finishTaskRun({
11535
+ run_id: run.id,
11536
+ status,
11537
+ summary: input.summary,
11538
+ agent_id: input.agent_id,
11539
+ completed_at: timestamp
11540
+ }, d);
11541
+ return {
11542
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11543
+ local_only: true,
11544
+ dry_run: false,
11545
+ processed_at: timestamp,
11546
+ action: "finished",
11547
+ key: resolvedKey,
11548
+ run: summarizeTaskRun(finished),
11549
+ warnings: [],
11550
+ commands: loopRunCommands(finished, resolvedKey)
11551
+ };
11552
+ }
11175
11553
  function listTaskRuns(taskId, db) {
11176
11554
  const d = db || getDatabase();
11177
11555
  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();
@@ -11189,6 +11567,7 @@ function getTaskRunLedger(runId, db) {
11189
11567
  const files = d.query("SELECT * FROM task_files WHERE task_id = ? ORDER BY updated_at DESC, path").all(run.task_id);
11190
11568
  return { run, events, commands, artifacts, files };
11191
11569
  }
11570
+ var LOOP_RUN_TRANSACTION_SCHEMA_VERSION = "todos.loop_run_transaction.v1";
11192
11571
  var init_task_runs = __esm(() => {
11193
11572
  init_artifact_store();
11194
11573
  init_event_hooks();
@@ -11577,6 +11956,7 @@ var init_calendar = __esm(() => {
11577
11956
  var exports_tasks = {};
11578
11957
  __export(exports_tasks, {
11579
11958
  watchTask: () => watchTask,
11959
+ upsertTaskByFingerprint: () => upsertTaskByFingerprint,
11580
11960
  updateTaskBoard: () => updateTaskBoard,
11581
11961
  updateTask: () => updateTask,
11582
11962
  unwatchTask: () => unwatchTask,
@@ -11620,6 +12000,7 @@ __export(exports_tasks, {
11620
12000
  getTaskGraph: () => getTaskGraph,
11621
12001
  getTaskDependents: () => getTaskDependents,
11622
12002
  getTaskDependencies: () => getTaskDependencies,
12003
+ getTaskByFingerprint: () => getTaskByFingerprint,
11623
12004
  getTaskBoard: () => getTaskBoard,
11624
12005
  getTask: () => getTask,
11625
12006
  getStatus: () => getStatus,
@@ -11860,6 +12241,62 @@ function parsePriority(value) {
11860
12241
  }
11861
12242
  return value;
11862
12243
  }
12244
+ function parseJsonObject3(value, flag) {
12245
+ if (!value)
12246
+ return;
12247
+ let parsed;
12248
+ try {
12249
+ parsed = JSON.parse(value);
12250
+ } catch (error) {
12251
+ console.error(chalk2.red(`${flag} must be valid JSON: ${error instanceof Error ? error.message : String(error)}`));
12252
+ process.exit(1);
12253
+ }
12254
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
12255
+ console.error(chalk2.red(`${flag} must be a JSON object`));
12256
+ process.exit(1);
12257
+ }
12258
+ return parsed;
12259
+ }
12260
+ function parseJsonValue(value) {
12261
+ if (value === undefined)
12262
+ return;
12263
+ try {
12264
+ return JSON.parse(value);
12265
+ } catch {
12266
+ return value;
12267
+ }
12268
+ }
12269
+ function parseTags(value) {
12270
+ return value ? value.split(",").map((tag) => tag.trim()).filter(Boolean) : undefined;
12271
+ }
12272
+ function buildExpectationMetadata(opts) {
12273
+ const metadata = parseJsonObject3(opts["metadataJson"], "--metadata-json") ?? {};
12274
+ const expectationId = opts["expectationId"];
12275
+ const expectationFingerprint = opts["expectationFingerprint"];
12276
+ const evidencePaths = opts["evidencePaths"];
12277
+ const originLoopId = opts["originLoopId"];
12278
+ const originRunId = opts["originRunId"];
12279
+ const expected = opts["expected"];
12280
+ const observed = opts["observed"];
12281
+ const acceptance = opts["acceptance"];
12282
+ if (expectationId !== undefined)
12283
+ metadata["expectation_id"] = expectationId;
12284
+ if (expectationFingerprint !== undefined)
12285
+ metadata["expectation_fingerprint"] = expectationFingerprint;
12286
+ if (evidencePaths !== undefined)
12287
+ metadata["evidence_paths"] = String(evidencePaths).split(",").map((path) => path.trim()).filter(Boolean);
12288
+ if (originLoopId !== undefined)
12289
+ metadata["origin_loop_id"] = originLoopId;
12290
+ if (originRunId !== undefined)
12291
+ metadata["origin_run_id"] = originRunId;
12292
+ if (expected !== undefined)
12293
+ metadata["expected"] = parseJsonValue(String(expected));
12294
+ if (observed !== undefined)
12295
+ metadata["observed"] = parseJsonValue(String(observed));
12296
+ if (acceptance !== undefined)
12297
+ metadata["acceptance"] = parseJsonValue(String(acceptance));
12298
+ return metadata;
12299
+ }
11863
12300
  function registerTaskCommands(program2) {
11864
12301
  program2.command("add <title>").description("Create a new task").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("--parent <id>", "Parent task ID").option("-t, --tags <tags>", "Comma-separated tags").option("--tag <tags>", "Comma-separated tags (alias for --tags)").option("--plan <id>", "Assign to a plan").option("--assign <agent>", "Assign to agent").option("--status <status>", "Initial status").option("--list <id>", "Task list ID").option("--task-list <id>", "Task list ID (alias for --list)").option("--estimated <minutes>", "Estimated time in minutes").option("--sla-minutes <minutes>", "SLA minutes before unfinished work is escalated").option("--sla <minutes>", "Alias for --sla-minutes").option("--approval", "Require approval before completion").option("--recurrence <rule>", "Recurrence rule, e.g. 'every day', 'every weekday', 'every 2 weeks'").option("--due <date>", "Due date (ISO string or YYYY-MM-DD)").option("--reason <text>", "Why this task exists").option("--project <id>", "Assign to project by ID or slug (overrides auto-detect)").action((title, opts) => {
11865
12302
  const globalOpts = program2.opts();
@@ -11876,7 +12313,7 @@ function registerTaskCommands(program2) {
11876
12313
  }
11877
12314
  return id;
11878
12315
  })() : undefined;
11879
- const task = createTask({
12316
+ const task2 = createTask({
11880
12317
  title,
11881
12318
  description: opts.description,
11882
12319
  priority: parsePriority(opts.priority),
@@ -11898,10 +12335,53 @@ function registerTaskCommands(program2) {
11898
12335
  reason: opts.reason
11899
12336
  });
11900
12337
  if (globalOpts.json) {
11901
- output(task, true);
12338
+ output(task2, true);
11902
12339
  } else {
11903
12340
  console.log(chalk2.green("Task created:"));
11904
- console.log(formatTaskLine(task));
12341
+ console.log(formatTaskLine(task2));
12342
+ }
12343
+ });
12344
+ const task = program2.command("task").description("Task subcommands for deterministic automation");
12345
+ task.command("upsert").description("Create or update a task by stable metadata fingerprint").requiredOption("--fingerprint <key>", "Stable dedupe fingerprint").requiredOption("--title <text>", "Task title").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("-s, --status <status>", "Task status").option("--list <id>", "Task list ID").option("--task-list <id>", "Task list ID (alias for --list)").option("-t, --tags <tags>", "Comma-separated tags").option("--tag <tags>", "Comma-separated tags (alias for --tags)").option("--metadata-json <json>", "JSON object merged into task metadata").option("--working-dir <path>", "Working directory to store on create/update").option("--project <id>", "Assign to project by ID, slug, or path").option("--assign <agent>", "Assign to agent").option("--expectation-id <id>", "Expectation metadata ID").option("--expectation-fingerprint <key>", "Expectation metadata fingerprint").option("--evidence-paths <paths>", "Comma-separated evidence paths").option("--origin-loop-id <id>", "Origin loop ID").option("--origin-run-id <id>", "Origin run ID").option("--expected <json-or-text>", "Expected value metadata").option("--observed <json-or-text>", "Observed value metadata").option("--acceptance <json-or-text>", "Acceptance metadata").action((opts) => {
12346
+ const globalOpts = program2.opts();
12347
+ opts.tags = opts.tags || opts.tag;
12348
+ opts.list = opts.list || opts.taskList;
12349
+ const explicitProject = opts.project || globalOpts.project;
12350
+ const projectId = explicitProject ? resolveProjectIdOrSlug(explicitProject) : autoProject(globalOpts);
12351
+ const taskListId = opts.list ? (() => {
12352
+ const db = getDatabase();
12353
+ const id = resolvePartialId(db, "task_lists", opts.list);
12354
+ if (!id) {
12355
+ console.error(chalk2.red(`Could not resolve task list ID: ${opts.list}`));
12356
+ process.exit(1);
12357
+ }
12358
+ return id;
12359
+ })() : undefined;
12360
+ let result;
12361
+ try {
12362
+ result = upsertTaskByFingerprint({
12363
+ fingerprint: opts.fingerprint,
12364
+ title: opts.title,
12365
+ description: opts.description,
12366
+ priority: parsePriority(opts.priority),
12367
+ status: opts.status ? normalizeStatus(opts.status) : undefined,
12368
+ task_list_id: taskListId,
12369
+ tags: parseTags(opts.tags),
12370
+ metadata: buildExpectationMetadata(opts),
12371
+ working_dir: opts.workingDir ? resolve8(opts.workingDir) : process.cwd(),
12372
+ project_id: projectId,
12373
+ assigned_to: opts.assign,
12374
+ agent_id: globalOpts.agent,
12375
+ session_id: globalOpts.session
12376
+ });
12377
+ } catch (e) {
12378
+ handleError(e);
12379
+ }
12380
+ if (globalOpts.json) {
12381
+ output(result, true);
12382
+ } else {
12383
+ console.log(chalk2.green(result.created ? "Task created:" : "Task updated:"));
12384
+ console.log(formatTaskLine(result.task));
11905
12385
  }
11906
12386
  });
11907
12387
  program2.command("list").description("List tasks").option("-s, --status <status>", "Filter by status").option("-p, --priority <priority>", "Filter by priority").option("--assigned <agent>", "Filter by assigned agent").option("--tags <tags>", "Filter by tags (comma-separated)").option("--tag <tags>", "Filter by tags (alias for --tags)").option("-a, --all", "Show all tasks (including completed/cancelled)").option("--list <id>", "Filter by task list ID").option("--task-list <id>", "Filter by task list ID (alias for --list)").option("--project-name <name>", "Filter by project name").option("--agent-name <name>", "Filter by agent name/assigned").option("--sort <field>", "Sort by: updated, created, priority, status").option("--format <fmt>", "Output format: table (default), compact, csv, json").option("--due-today", "Only tasks due today or earlier").option("--overdue", "Only overdue tasks (past due_at)").option("--recurring", "Only recurring tasks").option("--limit <n>", "Max tasks to return").action((opts) => {
@@ -12059,89 +12539,89 @@ function registerTaskCommands(program2) {
12059
12539
  program2.command("show <id>").description("Show full task details").action((id) => {
12060
12540
  const globalOpts = program2.opts();
12061
12541
  const resolvedId = resolveTaskId(id);
12062
- const task = getTaskWithRelations(resolvedId);
12063
- if (!task) {
12542
+ const task2 = getTaskWithRelations(resolvedId);
12543
+ if (!task2) {
12064
12544
  console.error(chalk2.red(`Task not found: ${id}`));
12065
12545
  process.exit(1);
12066
12546
  }
12067
12547
  if (globalOpts.json) {
12068
- output(task, true);
12548
+ output(task2, true);
12069
12549
  return;
12070
12550
  }
12071
12551
  console.log(chalk2.bold(`Task Details:
12072
12552
  `));
12073
- console.log(` ${chalk2.dim("ID:")} ${task.id}`);
12074
- console.log(` ${chalk2.dim("Title:")} ${task.title}`);
12075
- console.log(` ${chalk2.dim("Status:")} ${(statusColors[task.status] || chalk2.white)(task.status)}`);
12076
- console.log(` ${chalk2.dim("Priority:")} ${(priorityColors[task.priority] || chalk2.white)(task.priority)}`);
12077
- if (task.description)
12078
- console.log(` ${chalk2.dim("Desc:")} ${task.description}`);
12079
- if (task.assigned_to)
12080
- console.log(` ${chalk2.dim("Assigned:")} ${task.assigned_to}`);
12081
- if (task.agent_id)
12082
- console.log(` ${chalk2.dim("Agent:")} ${task.agent_id}`);
12083
- if (task.session_id)
12084
- console.log(` ${chalk2.dim("Session:")} ${task.session_id}`);
12085
- if (task.locked_by)
12086
- console.log(` ${chalk2.dim("Locked:")} ${task.locked_by} (at ${task.locked_at})`);
12087
- if (task.requires_approval) {
12088
- const approvalStatus = task.approved_by ? chalk2.green(`approved by ${task.approved_by}`) : chalk2.yellow("pending approval");
12553
+ console.log(` ${chalk2.dim("ID:")} ${task2.id}`);
12554
+ console.log(` ${chalk2.dim("Title:")} ${task2.title}`);
12555
+ console.log(` ${chalk2.dim("Status:")} ${(statusColors[task2.status] || chalk2.white)(task2.status)}`);
12556
+ console.log(` ${chalk2.dim("Priority:")} ${(priorityColors[task2.priority] || chalk2.white)(task2.priority)}`);
12557
+ if (task2.description)
12558
+ console.log(` ${chalk2.dim("Desc:")} ${task2.description}`);
12559
+ if (task2.assigned_to)
12560
+ console.log(` ${chalk2.dim("Assigned:")} ${task2.assigned_to}`);
12561
+ if (task2.agent_id)
12562
+ console.log(` ${chalk2.dim("Agent:")} ${task2.agent_id}`);
12563
+ if (task2.session_id)
12564
+ console.log(` ${chalk2.dim("Session:")} ${task2.session_id}`);
12565
+ if (task2.locked_by)
12566
+ console.log(` ${chalk2.dim("Locked:")} ${task2.locked_by} (at ${task2.locked_at})`);
12567
+ if (task2.requires_approval) {
12568
+ const approvalStatus = task2.approved_by ? chalk2.green(`approved by ${task2.approved_by}`) : chalk2.yellow("pending approval");
12089
12569
  console.log(` ${chalk2.dim("Approval:")} ${approvalStatus}`);
12090
12570
  }
12091
- if (task.estimated_minutes)
12092
- console.log(` ${chalk2.dim("Estimate:")} ${task.estimated_minutes} minutes`);
12093
- if (task.sla_minutes)
12094
- console.log(` ${chalk2.dim("SLA:")} ${task.sla_minutes} minutes`);
12095
- if (task.due_at)
12096
- console.log(` ${chalk2.dim("Due:")} ${task.due_at}`);
12097
- if (task.recurrence_rule)
12098
- console.log(` ${chalk2.dim("Repeats:")} ${task.recurrence_rule}`);
12099
- if (task.project_id)
12100
- console.log(` ${chalk2.dim("Project:")} ${task.project_id}`);
12101
- if (task.plan_id)
12102
- console.log(` ${chalk2.dim("Plan:")} ${task.plan_id}`);
12103
- if (task.working_dir)
12104
- console.log(` ${chalk2.dim("WorkDir:")} ${task.working_dir}`);
12105
- if (task.parent)
12106
- console.log(` ${chalk2.dim("Parent:")} ${task.parent.id.slice(0, 8)} | ${task.parent.title}`);
12107
- if (task.tags.length > 0)
12108
- console.log(` ${chalk2.dim("Tags:")} ${task.tags.join(", ")}`);
12109
- console.log(` ${chalk2.dim("Version:")} ${task.version}`);
12110
- console.log(` ${chalk2.dim("Created:")} ${task.created_at}`);
12111
- if (task.started_at)
12112
- console.log(` ${chalk2.dim("Started:")} ${task.started_at}`);
12113
- if (task.completed_at) {
12114
- console.log(` ${chalk2.dim("Done:")} ${task.completed_at}`);
12115
- if (task.started_at) {
12116
- const dur = Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 60000);
12571
+ if (task2.estimated_minutes)
12572
+ console.log(` ${chalk2.dim("Estimate:")} ${task2.estimated_minutes} minutes`);
12573
+ if (task2.sla_minutes)
12574
+ console.log(` ${chalk2.dim("SLA:")} ${task2.sla_minutes} minutes`);
12575
+ if (task2.due_at)
12576
+ console.log(` ${chalk2.dim("Due:")} ${task2.due_at}`);
12577
+ if (task2.recurrence_rule)
12578
+ console.log(` ${chalk2.dim("Repeats:")} ${task2.recurrence_rule}`);
12579
+ if (task2.project_id)
12580
+ console.log(` ${chalk2.dim("Project:")} ${task2.project_id}`);
12581
+ if (task2.plan_id)
12582
+ console.log(` ${chalk2.dim("Plan:")} ${task2.plan_id}`);
12583
+ if (task2.working_dir)
12584
+ console.log(` ${chalk2.dim("WorkDir:")} ${task2.working_dir}`);
12585
+ if (task2.parent)
12586
+ console.log(` ${chalk2.dim("Parent:")} ${task2.parent.id.slice(0, 8)} | ${task2.parent.title}`);
12587
+ if (task2.tags.length > 0)
12588
+ console.log(` ${chalk2.dim("Tags:")} ${task2.tags.join(", ")}`);
12589
+ console.log(` ${chalk2.dim("Version:")} ${task2.version}`);
12590
+ console.log(` ${chalk2.dim("Created:")} ${task2.created_at}`);
12591
+ if (task2.started_at)
12592
+ console.log(` ${chalk2.dim("Started:")} ${task2.started_at}`);
12593
+ if (task2.completed_at) {
12594
+ console.log(` ${chalk2.dim("Done:")} ${task2.completed_at}`);
12595
+ if (task2.started_at) {
12596
+ const dur = Math.round((new Date(task2.completed_at).getTime() - new Date(task2.started_at).getTime()) / 60000);
12117
12597
  console.log(` ${chalk2.dim("Duration:")} ${dur}m`);
12118
12598
  }
12119
12599
  }
12120
- if (task.subtasks.length > 0) {
12600
+ if (task2.subtasks.length > 0) {
12121
12601
  console.log(chalk2.bold(`
12122
- Subtasks (${task.subtasks.length}):`));
12123
- for (const st of task.subtasks) {
12602
+ Subtasks (${task2.subtasks.length}):`));
12603
+ for (const st of task2.subtasks) {
12124
12604
  console.log(` ${formatTaskLine(st)}`);
12125
12605
  }
12126
12606
  }
12127
- if (task.dependencies.length > 0) {
12607
+ if (task2.dependencies.length > 0) {
12128
12608
  console.log(chalk2.bold(`
12129
- Depends on (${task.dependencies.length}):`));
12130
- for (const dep of task.dependencies) {
12609
+ Depends on (${task2.dependencies.length}):`));
12610
+ for (const dep of task2.dependencies) {
12131
12611
  console.log(` ${formatTaskLine(dep)}`);
12132
12612
  }
12133
12613
  }
12134
- if (task.blocked_by.length > 0) {
12614
+ if (task2.blocked_by.length > 0) {
12135
12615
  console.log(chalk2.bold(`
12136
- Blocks (${task.blocked_by.length}):`));
12137
- for (const b of task.blocked_by) {
12616
+ Blocks (${task2.blocked_by.length}):`));
12617
+ for (const b of task2.blocked_by) {
12138
12618
  console.log(` ${formatTaskLine(b)}`);
12139
12619
  }
12140
12620
  }
12141
- if (task.comments.length > 0) {
12621
+ if (task2.comments.length > 0) {
12142
12622
  console.log(chalk2.bold(`
12143
- Comments (${task.comments.length}):`));
12144
- for (const c of task.comments) {
12623
+ Comments (${task2.comments.length}):`));
12624
+ for (const c of task2.comments) {
12145
12625
  const agent = c.agent_id ? chalk2.cyan(`[${c.agent_id}] `) : "";
12146
12626
  console.log(` ${agent}${chalk2.dim(c.created_at)}: ${c.content}`);
12147
12627
  }
@@ -12160,8 +12640,8 @@ function registerTaskCommands(program2) {
12160
12640
  console.error(chalk2.red("No task ID given and no active task found. Pass an ID or use --agent."));
12161
12641
  process.exit(1);
12162
12642
  }
12163
- const task = getTaskWithRelations(resolvedId);
12164
- if (!task) {
12643
+ const task2 = getTaskWithRelations(resolvedId);
12644
+ if (!task2) {
12165
12645
  console.error(chalk2.red(`Task not found: ${id || resolvedId}`));
12166
12646
  process.exit(1);
12167
12647
  }
@@ -12169,55 +12649,55 @@ function registerTaskCommands(program2) {
12169
12649
  const { listTaskFiles: listTaskFiles2 } = await Promise.resolve().then(() => (init_task_files(), exports_task_files));
12170
12650
  const { getTaskCommits: getTaskCommits2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
12171
12651
  try {
12172
- task.files = listTaskFiles2(task.id);
12652
+ task2.files = listTaskFiles2(task2.id);
12173
12653
  } catch (e) {
12174
12654
  console.error(chalk2.dim(`Warning: could not load task files: ${e instanceof Error ? e.message : String(e)}`));
12175
12655
  }
12176
12656
  try {
12177
- task.commits = getTaskCommits2(task.id);
12657
+ task2.commits = getTaskCommits2(task2.id);
12178
12658
  } catch (e) {
12179
12659
  console.error(chalk2.dim(`Warning: could not load task commits: ${e instanceof Error ? e.message : String(e)}`));
12180
12660
  }
12181
- output(task, true);
12661
+ output(task2, true);
12182
12662
  return;
12183
12663
  }
12184
- const sid = task.short_id || task.id.slice(0, 8);
12185
- const statusColor = statusColors[task.status] || chalk2.white;
12186
- const prioColor = priorityColors[task.priority] || chalk2.white;
12664
+ const sid = task2.short_id || task2.id.slice(0, 8);
12665
+ const statusColor = statusColors[task2.status] || chalk2.white;
12666
+ const prioColor = priorityColors[task2.priority] || chalk2.white;
12187
12667
  console.log(chalk2.bold(`
12188
- ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${task.title}
12668
+ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${task2.title}
12189
12669
  `));
12190
- if (task.description) {
12670
+ if (task2.description) {
12191
12671
  console.log(chalk2.dim("Description:"));
12192
- console.log(` ${task.description}
12672
+ console.log(` ${task2.description}
12193
12673
  `);
12194
12674
  }
12195
- if (task.assigned_to)
12196
- console.log(` ${chalk2.dim("Assigned:")} ${task.assigned_to}`);
12197
- if (task.locked_by)
12198
- console.log(` ${chalk2.dim("Locked by:")} ${task.locked_by}`);
12199
- if (task.project_id)
12200
- console.log(` ${chalk2.dim("Project:")} ${task.project_id}`);
12201
- if (task.plan_id)
12202
- console.log(` ${chalk2.dim("Plan:")} ${task.plan_id}`);
12203
- if (task.started_at)
12204
- console.log(` ${chalk2.dim("Started:")} ${task.started_at}`);
12205
- if (task.completed_at) {
12206
- console.log(` ${chalk2.dim("Completed:")} ${task.completed_at}`);
12207
- if (task.started_at) {
12208
- const dur = Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 60000);
12675
+ if (task2.assigned_to)
12676
+ console.log(` ${chalk2.dim("Assigned:")} ${task2.assigned_to}`);
12677
+ if (task2.locked_by)
12678
+ console.log(` ${chalk2.dim("Locked by:")} ${task2.locked_by}`);
12679
+ if (task2.project_id)
12680
+ console.log(` ${chalk2.dim("Project:")} ${task2.project_id}`);
12681
+ if (task2.plan_id)
12682
+ console.log(` ${chalk2.dim("Plan:")} ${task2.plan_id}`);
12683
+ if (task2.started_at)
12684
+ console.log(` ${chalk2.dim("Started:")} ${task2.started_at}`);
12685
+ if (task2.completed_at) {
12686
+ console.log(` ${chalk2.dim("Completed:")} ${task2.completed_at}`);
12687
+ if (task2.started_at) {
12688
+ const dur = Math.round((new Date(task2.completed_at).getTime() - new Date(task2.started_at).getTime()) / 60000);
12209
12689
  console.log(` ${chalk2.dim("Duration:")} ${dur}m`);
12210
12690
  }
12211
12691
  }
12212
- if (task.estimated_minutes)
12213
- console.log(` ${chalk2.dim("Estimate:")} ${task.estimated_minutes}m`);
12214
- if (task.tags.length > 0)
12215
- console.log(` ${chalk2.dim("Tags:")} ${task.tags.join(", ")}`);
12216
- const unfinishedDeps = task.dependencies.filter((d) => d.status !== "completed" && d.status !== "cancelled");
12217
- if (task.dependencies.length > 0) {
12692
+ if (task2.estimated_minutes)
12693
+ console.log(` ${chalk2.dim("Estimate:")} ${task2.estimated_minutes}m`);
12694
+ if (task2.tags.length > 0)
12695
+ console.log(` ${chalk2.dim("Tags:")} ${task2.tags.join(", ")}`);
12696
+ const unfinishedDeps = task2.dependencies.filter((d) => d.status !== "completed" && d.status !== "cancelled");
12697
+ if (task2.dependencies.length > 0) {
12218
12698
  console.log(chalk2.bold(`
12219
- Depends on (${task.dependencies.length}):`));
12220
- for (const dep of task.dependencies) {
12699
+ Depends on (${task2.dependencies.length}):`));
12700
+ for (const dep of task2.dependencies) {
12221
12701
  const blocked = dep.status !== "completed" && dep.status !== "cancelled";
12222
12702
  const icon = blocked ? chalk2.red("\u2717") : chalk2.green("\u2713");
12223
12703
  console.log(` ${icon} ${formatTaskLine(dep)}`);
@@ -12227,21 +12707,21 @@ ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${ta
12227
12707
  console.log(chalk2.red(`
12228
12708
  BLOCKED by ${unfinishedDeps.length} unfinished dep(s)`));
12229
12709
  }
12230
- if (task.blocked_by.length > 0) {
12710
+ if (task2.blocked_by.length > 0) {
12231
12711
  console.log(chalk2.bold(`
12232
- Blocks (${task.blocked_by.length}):`));
12233
- for (const b of task.blocked_by)
12712
+ Blocks (${task2.blocked_by.length}):`));
12713
+ for (const b of task2.blocked_by)
12234
12714
  console.log(` ${formatTaskLine(b)}`);
12235
12715
  }
12236
- if (task.subtasks.length > 0) {
12716
+ if (task2.subtasks.length > 0) {
12237
12717
  console.log(chalk2.bold(`
12238
- Subtasks (${task.subtasks.length}):`));
12239
- for (const st of task.subtasks)
12718
+ Subtasks (${task2.subtasks.length}):`));
12719
+ for (const st of task2.subtasks)
12240
12720
  console.log(` ${formatTaskLine(st)}`);
12241
12721
  }
12242
12722
  try {
12243
12723
  const { listTaskFiles: listTaskFiles2 } = await Promise.resolve().then(() => (init_task_files(), exports_task_files));
12244
- const files = listTaskFiles2(task.id);
12724
+ const files = listTaskFiles2(task2.id);
12245
12725
  if (files.length > 0) {
12246
12726
  console.log(chalk2.bold(`
12247
12727
  Files (${files.length}):`));
@@ -12253,7 +12733,7 @@ ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${ta
12253
12733
  }
12254
12734
  try {
12255
12735
  const { getTaskCommits: getTaskCommits2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
12256
- const commits = getTaskCommits2(task.id);
12736
+ const commits = getTaskCommits2(task2.id);
12257
12737
  if (commits.length > 0) {
12258
12738
  console.log(chalk2.bold(`
12259
12739
  Commits (${commits.length}):`));
@@ -12263,19 +12743,19 @@ ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${ta
12263
12743
  } catch (e) {
12264
12744
  console.error(chalk2.dim(`Warning: could not load task commits: ${e instanceof Error ? e.message : String(e)}`));
12265
12745
  }
12266
- if (task.comments.length > 0) {
12746
+ if (task2.comments.length > 0) {
12267
12747
  console.log(chalk2.bold(`
12268
- Comments (${task.comments.length}):`));
12269
- for (const c of task.comments) {
12748
+ Comments (${task2.comments.length}):`));
12749
+ for (const c of task2.comments) {
12270
12750
  const agent = c.agent_id ? chalk2.cyan(`[${c.agent_id}] `) : "";
12271
12751
  console.log(` ${agent}${chalk2.dim(c.created_at)}: ${c.content}`);
12272
12752
  }
12273
12753
  }
12274
- if (task.checklist && task.checklist.length > 0) {
12275
- const done = task.checklist.filter((c) => c.checked).length;
12754
+ if (task2.checklist && task2.checklist.length > 0) {
12755
+ const done = task2.checklist.filter((c) => c.checked).length;
12276
12756
  console.log(chalk2.bold(`
12277
- Checklist (${done}/${task.checklist.length}):`));
12278
- for (const item of task.checklist) {
12757
+ Checklist (${done}/${task2.checklist.length}):`));
12758
+ for (const item of task2.checklist) {
12279
12759
  const icon = item.checked ? chalk2.green("\u2611") : chalk2.dim("\u2610");
12280
12760
  console.log(` ${icon} ${item.text || item.title}`);
12281
12761
  }
@@ -12328,9 +12808,9 @@ ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${ta
12328
12808
  return resolved;
12329
12809
  })() : undefined;
12330
12810
  const planId = opts.plan ? resolvePlanId(opts.plan) : opts.clearPlan ? null : undefined;
12331
- let task;
12811
+ let task2;
12332
12812
  try {
12333
- task = updateTask(resolvedId, {
12813
+ task2 = updateTask(resolvedId, {
12334
12814
  version: current.version,
12335
12815
  title: opts.title,
12336
12816
  description: opts.description,
@@ -12350,10 +12830,10 @@ ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${ta
12350
12830
  handleError(e);
12351
12831
  }
12352
12832
  if (globalOpts.json) {
12353
- output(task, true);
12833
+ output(task2, true);
12354
12834
  } else {
12355
12835
  console.log(chalk2.green("Task updated:"));
12356
- console.log(formatTaskLine(task));
12836
+ console.log(formatTaskLine(task2));
12357
12837
  }
12358
12838
  });
12359
12839
  program2.command("done <id>").description("Mark a task as completed").option("--attach-ids <ids>", "Comma-separated @hasna/attachments IDs to link as evidence").option("--files-changed <files>", "Comma-separated list of files changed").option("--test-results <results>", "Test results summary").option("--commit-hash <hash>", "Git commit hash").option("--notes <notes>", "Completion notes").option("--confidence <0-1>", "Agent's confidence 0.0-1.0 that the task is fully complete (default: 1.0, <0.7 flagged for review)").action((id, opts) => {
@@ -12363,37 +12843,37 @@ ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${ta
12363
12843
  const filesChanged = opts.filesChanged ? opts.filesChanged.split(",").map((s) => s.trim()) : undefined;
12364
12844
  const confidence = opts.confidence !== undefined ? parseFloat(opts.confidence) : undefined;
12365
12845
  const evidence = attachmentIds || filesChanged || opts.testResults || opts.commitHash || opts.notes ? { attachment_ids: attachmentIds, files_changed: filesChanged, test_results: opts.testResults, commit_hash: opts.commitHash, notes: opts.notes } : undefined;
12366
- let task;
12846
+ let task2;
12367
12847
  try {
12368
- task = completeTask(resolvedId, globalOpts.agent, undefined, { ...evidence, confidence });
12848
+ task2 = completeTask(resolvedId, globalOpts.agent, undefined, { ...evidence, confidence });
12369
12849
  } catch (e) {
12370
12850
  handleError(e);
12371
12851
  }
12372
12852
  if (globalOpts.json) {
12373
- output(task, true);
12853
+ output(task2, true);
12374
12854
  } else {
12375
12855
  console.log(chalk2.green("Task completed:"));
12376
- console.log(formatTaskLine(task));
12856
+ console.log(formatTaskLine(task2));
12377
12857
  }
12378
12858
  });
12379
12859
  program2.command("approve <id>").description("Approve a task that requires approval").action((id) => {
12380
12860
  const globalOpts = program2.opts();
12381
12861
  const resolvedId = resolveTaskId(id);
12382
- const task = getTask(resolvedId);
12383
- if (!task) {
12862
+ const task2 = getTask(resolvedId);
12863
+ if (!task2) {
12384
12864
  console.error(chalk2.red(`Task not found: ${id}`));
12385
12865
  process.exit(1);
12386
12866
  }
12387
- if (!task.requires_approval) {
12867
+ if (!task2.requires_approval) {
12388
12868
  console.log(chalk2.yellow("This task does not require approval."));
12389
12869
  return;
12390
12870
  }
12391
- if (task.approved_by) {
12392
- console.log(chalk2.yellow(`Already approved by ${task.approved_by}.`));
12871
+ if (task2.approved_by) {
12872
+ console.log(chalk2.yellow(`Already approved by ${task2.approved_by}.`));
12393
12873
  return;
12394
12874
  }
12395
12875
  try {
12396
- const updated = updateTask(resolvedId, { approved_by: globalOpts.agent || "cli", version: task.version });
12876
+ const updated = updateTask(resolvedId, { approved_by: globalOpts.agent || "cli", version: task2.version });
12397
12877
  if (globalOpts.json) {
12398
12878
  output(updated, true);
12399
12879
  } else {
@@ -12408,17 +12888,17 @@ ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${ta
12408
12888
  const globalOpts = program2.opts();
12409
12889
  const agentId = globalOpts.agent || "cli";
12410
12890
  const resolvedId = resolveTaskId(id);
12411
- let task;
12891
+ let task2;
12412
12892
  try {
12413
- task = startTask(resolvedId, agentId);
12893
+ task2 = startTask(resolvedId, agentId);
12414
12894
  } catch (e) {
12415
12895
  handleError(e);
12416
12896
  }
12417
12897
  if (globalOpts.json) {
12418
- output(task, true);
12898
+ output(task2, true);
12419
12899
  } else {
12420
12900
  console.log(chalk2.green(`Task started by ${agentId}:`));
12421
- console.log(formatTaskLine(task));
12901
+ console.log(formatTaskLine(task2));
12422
12902
  }
12423
12903
  });
12424
12904
  program2.command("lock <id>").description("Acquire exclusive lock on a task").action((id) => {
@@ -13591,7 +14071,7 @@ function likePattern(query) {
13591
14071
  return null;
13592
14072
  return `%${trimmed}%`;
13593
14073
  }
13594
- function parseJsonObject3(value) {
14074
+ function parseJsonObject4(value) {
13595
14075
  if (!value)
13596
14076
  return {};
13597
14077
  if (typeof value === "object" && !Array.isArray(value))
@@ -13606,7 +14086,7 @@ function parseJsonObject3(value) {
13606
14086
  }
13607
14087
  }
13608
14088
  function rowToTaskRun(row) {
13609
- return { ...row, metadata: parseJsonObject3(row.metadata) };
14089
+ return { ...row, metadata: parseJsonObject4(row.metadata) };
13610
14090
  }
13611
14091
  function taskMatchesSavedFilters(task, filters, db) {
13612
14092
  if (filters.plan_id && task.plan_id !== filters.plan_id)
@@ -14969,7 +15449,7 @@ function packageSource(version) {
14969
15449
  function emptyCounts() {
14970
15450
  return Object.fromEntries(dataKeys.map((key) => [key, 0]));
14971
15451
  }
14972
- function parseJsonObject4(value) {
15452
+ function parseJsonObject5(value) {
14973
15453
  if (!value)
14974
15454
  return {};
14975
15455
  if (typeof value === "object" && !Array.isArray(value))
@@ -15015,34 +15495,34 @@ function rowToTask3(row) {
15015
15495
  return {
15016
15496
  ...row,
15017
15497
  tags: parseJsonArray2(row.tags),
15018
- metadata: parseJsonObject4(row.metadata),
15498
+ metadata: parseJsonObject5(row.metadata),
15019
15499
  requires_approval: Boolean(row.requires_approval)
15020
15500
  };
15021
15501
  }
15022
15502
  function rowToTaskList2(row) {
15023
- return { ...row, metadata: parseJsonObject4(row.metadata) };
15503
+ return { ...row, metadata: parseJsonObject5(row.metadata) };
15024
15504
  }
15025
15505
  function rowWithMetadata(row) {
15026
- return { ...row, metadata: parseJsonObject4(row.metadata) };
15506
+ return { ...row, metadata: parseJsonObject5(row.metadata) };
15027
15507
  }
15028
15508
  function rowToRunEvent(row) {
15029
- return { ...row, data: parseJsonObject4(row.data) };
15509
+ return { ...row, data: parseJsonObject5(row.data) };
15030
15510
  }
15031
15511
  function rowToCommit2(row) {
15032
15512
  return { ...row, files_changed: row.files_changed ? parseJsonArray2(row.files_changed) : null };
15033
15513
  }
15034
15514
  function rowToSavedView(row) {
15035
- return { ...row, filters: parseJsonObject4(row.filters) };
15515
+ return { ...row, filters: parseJsonObject5(row.filters) };
15036
15516
  }
15037
15517
  function rowToTaskBoard2(row) {
15038
15518
  return {
15039
15519
  ...row,
15040
15520
  lanes: parseJsonArray2(row.lanes),
15041
- filters: parseJsonObject4(row.filters)
15521
+ filters: parseJsonObject5(row.filters)
15042
15522
  };
15043
15523
  }
15044
15524
  function rowToCalendarItem2(row) {
15045
- return { ...row, metadata: parseJsonObject4(row.metadata) };
15525
+ return { ...row, metadata: parseJsonObject5(row.metadata) };
15046
15526
  }
15047
15527
  function bridgeStats(data) {
15048
15528
  return Object.fromEntries(dataKeys.map((key) => [key, data[key].length]));
@@ -18389,6 +18869,7 @@ var init_token_utils = __esm(() => {
18389
18869
  "add_task_run_event",
18390
18870
  "add_task_run_file",
18391
18871
  "acknowledge_handoff",
18872
+ "begin_task_run_transaction",
18392
18873
  "build_local_report",
18393
18874
  "cancel_agent_run_dispatch",
18394
18875
  "finish_task_run",
@@ -18436,6 +18917,7 @@ var init_token_utils = __esm(() => {
18436
18917
  "list_local_snapshots",
18437
18918
  "list_retrospectives",
18438
18919
  "list_risks",
18920
+ "list_task_findings",
18439
18921
  "list_task_runs",
18440
18922
  "list_verification_providers",
18441
18923
  "merge_duplicate_task",
@@ -18444,6 +18926,7 @@ var init_token_utils = __esm(() => {
18444
18926
  "remove_review_routing_rule",
18445
18927
  "restore_local_backup",
18446
18928
  "retry_agent_run_dispatch",
18929
+ "resolve_missing_task_findings",
18447
18930
  "resolve_mentions",
18448
18931
  "run_next_agent_dispatch",
18449
18932
  "search_knowledge_records",
@@ -18486,9 +18969,17 @@ var init_token_utils = __esm(() => {
18486
18969
  "unlock_file",
18487
18970
  "unwatch_task",
18488
18971
  "update_comment",
18972
+ "upsert_task_finding",
18489
18973
  "update_risk",
18490
18974
  "watch_task"
18491
18975
  ],
18976
+ loops: [
18977
+ "begin_task_run_transaction",
18978
+ "finish_task_run",
18979
+ "list_task_findings",
18980
+ "resolve_missing_task_findings",
18981
+ "upsert_task_finding"
18982
+ ],
18492
18983
  agents: [
18493
18984
  "auto_assign_task",
18494
18985
  "delete_agent",
@@ -18570,7 +19061,7 @@ var init_token_utils = __esm(() => {
18570
19061
  maintenance: ["extract_todos", "get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task", "watch_source_todos"]
18571
19062
  };
18572
19063
  MCP_PROFILE_GROUPS = {
18573
- minimal: ["core"],
19064
+ minimal: ["core", "loops"],
18574
19065
  core: ["core"],
18575
19066
  standard: ["core", "tasks", "projects", "resources", "agents", "metadata"],
18576
19067
  agent: ["core", "tasks", "projects", "resources"],
@@ -20863,6 +21354,39 @@ async function handleCreateTask(req, ctx, json2, taskToSummary2) {
20863
21354
  return json2({ error: e instanceof Error ? e.message : "Failed to create task" }, 500);
20864
21355
  }
20865
21356
  }
21357
+ async function handleUpsertTask(req, ctx, json2, taskToSummary2) {
21358
+ try {
21359
+ const body = await req.json();
21360
+ if (typeof body["fingerprint"] !== "string" || body["fingerprint"].trim() === "") {
21361
+ return json2({ error: "Missing 'fingerprint'" }, 400);
21362
+ }
21363
+ if (typeof body["title"] !== "string" || body["title"].trim() === "") {
21364
+ return json2({ error: "Missing 'title'" }, 400);
21365
+ }
21366
+ const metadata = body["metadata"] && typeof body["metadata"] === "object" && !Array.isArray(body["metadata"]) ? { ...body["metadata"] } : {};
21367
+ for (const key of ["expectation_id", "expectation_fingerprint", "evidence_paths", "origin_loop_id", "origin_run_id", "expected", "observed", "acceptance"]) {
21368
+ if (body[key] !== undefined)
21369
+ metadata[key] = body[key];
21370
+ }
21371
+ const result = upsertTaskByFingerprint({
21372
+ fingerprint: body["fingerprint"],
21373
+ title: body["title"],
21374
+ description: typeof body["description"] === "string" ? body["description"] : undefined,
21375
+ status: body["status"],
21376
+ priority: body["priority"],
21377
+ project_id: typeof body["project_id"] === "string" ? body["project_id"] : undefined,
21378
+ task_list_id: typeof body["task_list_id"] === "string" ? body["task_list_id"] : undefined,
21379
+ assigned_to: typeof body["assigned_to"] === "string" ? body["assigned_to"] : undefined,
21380
+ working_dir: typeof body["working_dir"] === "string" ? body["working_dir"] : undefined,
21381
+ tags: Array.isArray(body["tags"]) ? body["tags"].filter((tag) => typeof tag === "string") : undefined,
21382
+ metadata
21383
+ });
21384
+ 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 });
21385
+ return json2({ created: result.created, task: taskToSummary2(result.task) }, result.created ? 201 : 200);
21386
+ } catch (e) {
21387
+ return json2({ error: e instanceof Error ? e.message : "Failed to upsert task" }, 500);
21388
+ }
21389
+ }
20866
21390
  function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
20867
21391
  const format = url.searchParams.get("format") || "json";
20868
21392
  const status = url.searchParams.get("status") || undefined;
@@ -26005,6 +26529,61 @@ function registerTaskCrudTools(server, ctx) {
26005
26529
  }
26006
26530
  });
26007
26531
  }
26532
+ if (shouldRegisterTool("upsert_task")) {
26533
+ server.tool("upsert_task", "Create or update a task by stable metadata fingerprint. Metadata is shallow-merged on updates.", {
26534
+ fingerprint: exports_external.string().describe("Stable dedupe fingerprint stored as metadata.fingerprint"),
26535
+ title: exports_external.string().describe("Task title"),
26536
+ description: exports_external.string().optional().describe("Task description"),
26537
+ status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
26538
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
26539
+ project_id: exports_external.string().optional().describe("Project ID"),
26540
+ task_list_id: exports_external.string().optional().describe("Task list ID"),
26541
+ assigned_to: exports_external.string().optional().describe("Agent ID or name to assign to"),
26542
+ tags: exports_external.array(exports_external.string()).optional().describe("Tags for the task"),
26543
+ working_dir: exports_external.string().optional().describe("Working directory associated with the task"),
26544
+ metadata: exports_external.record(exports_external.unknown()).optional().describe("Metadata object to shallow-merge"),
26545
+ expectation_id: exports_external.string().optional(),
26546
+ expectation_fingerprint: exports_external.string().optional(),
26547
+ evidence_paths: exports_external.array(exports_external.string()).optional(),
26548
+ origin_loop_id: exports_external.string().optional(),
26549
+ origin_run_id: exports_external.string().optional(),
26550
+ expected: exports_external.unknown().optional(),
26551
+ observed: exports_external.unknown().optional(),
26552
+ acceptance: exports_external.unknown().optional()
26553
+ }, async (params) => {
26554
+ try {
26555
+ 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;
26556
+ const mergedMetadata = { ...metadata ?? {} };
26557
+ if (expectation_id !== undefined)
26558
+ mergedMetadata["expectation_id"] = expectation_id;
26559
+ if (expectation_fingerprint !== undefined)
26560
+ mergedMetadata["expectation_fingerprint"] = expectation_fingerprint;
26561
+ if (evidence_paths !== undefined)
26562
+ mergedMetadata["evidence_paths"] = evidence_paths;
26563
+ if (origin_loop_id !== undefined)
26564
+ mergedMetadata["origin_loop_id"] = origin_loop_id;
26565
+ if (origin_run_id !== undefined)
26566
+ mergedMetadata["origin_run_id"] = origin_run_id;
26567
+ if (expected !== undefined)
26568
+ mergedMetadata["expected"] = expected;
26569
+ if (observed !== undefined)
26570
+ mergedMetadata["observed"] = observed;
26571
+ if (acceptance !== undefined)
26572
+ mergedMetadata["acceptance"] = acceptance;
26573
+ const resolved = { ...rest, metadata: mergedMetadata };
26574
+ if (assigned_to)
26575
+ resolved.assigned_to = resolveAssignee(assigned_to);
26576
+ if (project_id)
26577
+ resolved.project_id = resolveId(project_id, "projects");
26578
+ if (task_list_id)
26579
+ resolved.task_list_id = resolveId(task_list_id, "task_lists");
26580
+ const result = upsertTaskByFingerprint(resolved);
26581
+ return { content: [{ type: "text", text: compactJson({ created: result.created, task: JSON.parse(mutationTaskResponse(result.task)) }) }] };
26582
+ } catch (e) {
26583
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
26584
+ }
26585
+ });
26586
+ }
26008
26587
  if (shouldRegisterTool("list_tasks")) {
26009
26588
  server.tool("list_tasks", "List tasks with optional filters. Pass empty arrays for multi-value filters (e.g. status=[] shows all).", {
26010
26589
  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"),
@@ -26016,7 +26595,8 @@ function registerTaskCrudTools(server, ctx) {
26016
26595
  created_after: exports_external.string().optional().describe("ISO date \u2014 tasks created after this date"),
26017
26596
  created_before: exports_external.string().optional().describe("ISO date \u2014 tasks created before this date"),
26018
26597
  limit: exports_external.number().optional().describe("Max results (default: 50, max 500)"),
26019
- offset: exports_external.number().optional().describe("Pagination offset")
26598
+ offset: exports_external.number().optional().describe("Pagination offset"),
26599
+ metadata: exports_external.record(exports_external.unknown()).optional().describe("Exact top-level metadata filters")
26020
26600
  }, async (params) => {
26021
26601
  try {
26022
26602
  const resolved = { ...params };
@@ -27604,7 +28184,7 @@ function parseAssignee(line) {
27604
28184
  return mention;
27605
28185
  return line.match(/\bassign(?:ed)?\s+(?:to\s+)?([a-zA-Z0-9._-]+)/i)?.[1] || null;
27606
28186
  }
27607
- function parseTags(line) {
28187
+ function parseTags2(line) {
27608
28188
  return Array.from(new Set(Array.from(line.matchAll(/#([a-zA-Z0-9._-]+)/g)).map((match) => match[1].toLowerCase()))).slice(0, 10);
27609
28189
  }
27610
28190
  function parseDependencies(line) {
@@ -27676,7 +28256,7 @@ function previewNaturalLanguageIntake(input, db) {
27676
28256
  description: `Parsed from local natural-language intake:
27677
28257
  ${line}`,
27678
28258
  priority: parsePriority2(line, fallbackPriority),
27679
- tags: parseTags(line),
28259
+ tags: parseTags2(line),
27680
28260
  assigned_to: parseAssignee(line),
27681
28261
  due_at: parseDue(line, referenceDate),
27682
28262
  depends_on: parseDependencies(line),
@@ -28184,7 +28764,7 @@ function countMovedStandaloneVerifications(db, duplicateId) {
28184
28764
  const rows = db.query("SELECT command FROM task_verifications WHERE task_id = ?").all(duplicateId);
28185
28765
  return rows.filter((row) => !generatedCommands.has(row.command)).length;
28186
28766
  }
28187
- function mergeTaskMetadata(primary, duplicate, input, mergedAt) {
28767
+ function mergeTaskMetadata2(primary, duplicate, input, mergedAt) {
28188
28768
  const mergedDuplicates = Array.isArray(primary.metadata["merged_duplicates"]) ? [...primary.metadata["merged_duplicates"]] : [];
28189
28769
  mergedDuplicates.push({
28190
28770
  id: duplicate.id,
@@ -28245,7 +28825,7 @@ function mergeDuplicateTask(input, db) {
28245
28825
  updateTask(primary.id, {
28246
28826
  version: primary.version,
28247
28827
  tags: mergedTags,
28248
- metadata: mergeTaskMetadata(primary, duplicate, input, mergedAt),
28828
+ metadata: mergeTaskMetadata2(primary, duplicate, input, mergedAt),
28249
28829
  description: mergeTaskDescription(primary, duplicate) ?? undefined
28250
28830
  }, d);
28251
28831
  moved.comments = updateRows(d, "task_comments", "task_id", duplicate.id, primary.id);
@@ -34239,6 +34819,366 @@ var init_task_meta_tools = __esm(() => {
34239
34819
  init_zod();
34240
34820
  });
34241
34821
 
34822
+ // src/db/findings.ts
34823
+ var exports_findings = {};
34824
+ __export(exports_findings, {
34825
+ upsertTaskFinding: () => upsertTaskFinding,
34826
+ resolveMissingTaskFindings: () => resolveMissingTaskFindings,
34827
+ listTaskFindings: () => listTaskFindings,
34828
+ listCompactTaskFindings: () => listCompactTaskFindings,
34829
+ TASK_FINDING_UPSERT_SCHEMA_VERSION: () => TASK_FINDING_UPSERT_SCHEMA_VERSION,
34830
+ TASK_FINDING_SCHEMA_VERSION: () => TASK_FINDING_SCHEMA_VERSION,
34831
+ TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION: () => TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION
34832
+ });
34833
+ function parseObject2(value) {
34834
+ if (!value)
34835
+ return {};
34836
+ try {
34837
+ const parsed = JSON.parse(value);
34838
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
34839
+ } catch {
34840
+ return {};
34841
+ }
34842
+ }
34843
+ function normalizeKey(value) {
34844
+ return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
34845
+ }
34846
+ function normalizeFingerprint(value) {
34847
+ const normalized = normalizeKey(value);
34848
+ if (!normalized)
34849
+ throw new Error("finding fingerprint is required");
34850
+ return normalized.slice(0, 240);
34851
+ }
34852
+ function normalizeSeverity(value) {
34853
+ const normalized = normalizeKey(value || "medium");
34854
+ if (SEVERITIES.has(normalized))
34855
+ return normalized;
34856
+ if (/^(p0|blocker|urgent|highest)$/.test(normalized))
34857
+ return "critical";
34858
+ if (/^(p1|major)$/.test(normalized))
34859
+ return "high";
34860
+ if (/^(p3|minor|info)$/.test(normalized))
34861
+ return "low";
34862
+ return "medium";
34863
+ }
34864
+ function normalizeStatus2(value) {
34865
+ const normalized = normalizeKey(value || "open");
34866
+ if (STATUSES.has(normalized))
34867
+ return normalized;
34868
+ if (normalized === "closed" || normalized === "fixed")
34869
+ return "resolved";
34870
+ return "open";
34871
+ }
34872
+ function normalizeResolutionStatus(value) {
34873
+ const status = normalizeStatus2(value || "resolved");
34874
+ if (status === "open")
34875
+ throw new Error("resolve-missing status must be resolved or ignored");
34876
+ return status;
34877
+ }
34878
+ function redactOptional(value, max = 2000) {
34879
+ if (!value)
34880
+ return null;
34881
+ const redacted = redactEvidenceText(value).trim();
34882
+ if (!redacted)
34883
+ return null;
34884
+ return redacted.length > max ? `${redacted.slice(0, max - 3)}...` : redacted;
34885
+ }
34886
+ function rowToFinding(row) {
34887
+ return {
34888
+ schema_version: TASK_FINDING_SCHEMA_VERSION,
34889
+ ...row,
34890
+ severity: normalizeSeverity(row.severity),
34891
+ status: normalizeStatus2(row.status),
34892
+ metadata: parseObject2(row.metadata)
34893
+ };
34894
+ }
34895
+ function compactFinding(finding2) {
34896
+ return {
34897
+ schema_version: TASK_FINDING_SCHEMA_VERSION,
34898
+ id: finding2.id,
34899
+ task_id: finding2.task_id,
34900
+ run_id: finding2.run_id,
34901
+ fingerprint: finding2.fingerprint,
34902
+ title: finding2.title,
34903
+ severity: finding2.severity,
34904
+ status: finding2.status,
34905
+ source: finding2.source,
34906
+ summary: finding2.summary,
34907
+ artifact_path: finding2.artifact_path,
34908
+ first_seen_at: finding2.first_seen_at,
34909
+ last_seen_at: finding2.last_seen_at,
34910
+ resolved_at: finding2.resolved_at,
34911
+ metadata_keys: Object.keys(finding2.metadata).sort()
34912
+ };
34913
+ }
34914
+ function previewFinding(existing, next, timestamp3) {
34915
+ return {
34916
+ ...existing,
34917
+ run_id: next.run_id,
34918
+ title: next.title,
34919
+ severity: next.severity,
34920
+ status: next.status,
34921
+ source: next.source,
34922
+ summary: next.summary,
34923
+ artifact_path: next.artifact_path,
34924
+ metadata: next.metadata,
34925
+ last_seen_at: timestamp3,
34926
+ resolved_at: next.status === "open" ? null : existing.resolved_at || timestamp3,
34927
+ updated_at: timestamp3
34928
+ };
34929
+ }
34930
+ function upsertAction(existing, next) {
34931
+ if (sameFinding(existing, next))
34932
+ return "matched";
34933
+ return existing.status !== "open" && next.status === "open" ? "reopened" : "updated";
34934
+ }
34935
+ function resolveRunForTask(runId, taskId, db) {
34936
+ if (!runId)
34937
+ return null;
34938
+ const resolved = resolveTaskRunId(runId, db);
34939
+ const run = getTaskRun(resolved, db);
34940
+ if (!run)
34941
+ throw new Error(`Run not found: ${runId}`);
34942
+ if (run.task_id !== taskId)
34943
+ throw new Error(`Run ${resolved} belongs to task ${run.task_id}, not ${taskId}`);
34944
+ return resolved;
34945
+ }
34946
+ function getFindingByFingerprint(taskId, fingerprint2, db) {
34947
+ const row = db.query("SELECT * FROM task_findings WHERE task_id = ? AND fingerprint = ?").get(taskId, fingerprint2);
34948
+ return row ? rowToFinding(row) : null;
34949
+ }
34950
+ function assertTask(taskId, db) {
34951
+ if (!getTask(taskId, db))
34952
+ throw new TaskNotFoundError(taskId);
34953
+ }
34954
+ function nextFinding(input, db) {
34955
+ const fingerprint2 = normalizeFingerprint(input.fingerprint);
34956
+ const title = redactOptional(input.title, 300);
34957
+ if (!title)
34958
+ throw new Error("finding title is required");
34959
+ return {
34960
+ fingerprint: fingerprint2,
34961
+ run_id: resolveRunForTask(input.run_id, input.task_id, db),
34962
+ title,
34963
+ severity: normalizeSeverity(input.severity),
34964
+ status: normalizeStatus2(input.status),
34965
+ source: redactOptional(input.source, 120),
34966
+ summary: redactOptional(input.summary, 2000),
34967
+ artifact_path: redactOptional(input.artifact_path, 1000),
34968
+ metadata: redactValue(input.metadata || {})
34969
+ };
34970
+ }
34971
+ function sameFinding(left, right) {
34972
+ 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);
34973
+ }
34974
+ function upsertTaskFinding(input, db) {
34975
+ const d = db || getDatabase();
34976
+ assertTask(input.task_id, d);
34977
+ const timestamp3 = input.observed_at || now();
34978
+ const warnings = [];
34979
+ const next = nextFinding(input, d);
34980
+ const existing = getFindingByFingerprint(input.task_id, next.fingerprint, d);
34981
+ const dryRun = !input.apply;
34982
+ if (dryRun) {
34983
+ const action2 = existing ? upsertAction(existing, next) : "preview";
34984
+ return {
34985
+ schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
34986
+ local_only: true,
34987
+ dry_run: true,
34988
+ processed_at: timestamp3,
34989
+ action: action2,
34990
+ fingerprint: next.fingerprint,
34991
+ finding: existing ? compactFinding(previewFinding(existing, next, timestamp3)) : null,
34992
+ warnings
34993
+ };
34994
+ }
34995
+ if (!existing) {
34996
+ const id = uuid();
34997
+ d.run(`INSERT INTO task_findings (
34998
+ id, task_id, run_id, fingerprint, title, severity, status, source, summary, artifact_path,
34999
+ metadata, first_seen_at, last_seen_at, resolved_at, created_at, updated_at
35000
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
35001
+ id,
35002
+ input.task_id,
35003
+ next.run_id,
35004
+ next.fingerprint,
35005
+ next.title,
35006
+ next.severity,
35007
+ next.status,
35008
+ next.source,
35009
+ next.summary,
35010
+ next.artifact_path,
35011
+ JSON.stringify(next.metadata),
35012
+ timestamp3,
35013
+ timestamp3,
35014
+ next.status === "open" ? null : timestamp3,
35015
+ timestamp3,
35016
+ timestamp3
35017
+ ]);
35018
+ return {
35019
+ schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
35020
+ local_only: true,
35021
+ dry_run: false,
35022
+ processed_at: timestamp3,
35023
+ action: "created",
35024
+ fingerprint: next.fingerprint,
35025
+ finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
35026
+ warnings
35027
+ };
35028
+ }
35029
+ if (sameFinding(existing, next)) {
35030
+ return {
35031
+ schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
35032
+ local_only: true,
35033
+ dry_run: false,
35034
+ processed_at: timestamp3,
35035
+ action: "matched",
35036
+ fingerprint: next.fingerprint,
35037
+ finding: compactFinding(existing),
35038
+ warnings
35039
+ };
35040
+ }
35041
+ const action = upsertAction(existing, next);
35042
+ d.run(`UPDATE task_findings SET
35043
+ run_id = ?, title = ?, severity = ?, status = ?, source = ?, summary = ?, artifact_path = ?,
35044
+ metadata = ?, last_seen_at = ?, resolved_at = ?, updated_at = ?
35045
+ WHERE id = ?`, [
35046
+ next.run_id,
35047
+ next.title,
35048
+ next.severity,
35049
+ next.status,
35050
+ next.source,
35051
+ next.summary,
35052
+ next.artifact_path,
35053
+ JSON.stringify(next.metadata),
35054
+ timestamp3,
35055
+ next.status === "open" ? null : existing.resolved_at || timestamp3,
35056
+ timestamp3,
35057
+ existing.id
35058
+ ]);
35059
+ return {
35060
+ schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
35061
+ local_only: true,
35062
+ dry_run: false,
35063
+ processed_at: timestamp3,
35064
+ action,
35065
+ fingerprint: next.fingerprint,
35066
+ finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
35067
+ warnings
35068
+ };
35069
+ }
35070
+ function listTaskFindings(filter = {}, db) {
35071
+ const d = db || getDatabase();
35072
+ const conditions = ["1=1"];
35073
+ const params = [];
35074
+ if (filter.task_id) {
35075
+ conditions.push("task_id = ?");
35076
+ params.push(filter.task_id);
35077
+ }
35078
+ if (filter.run_id) {
35079
+ conditions.push("run_id = ?");
35080
+ params.push(resolveTaskRunId(filter.run_id, d));
35081
+ }
35082
+ if (filter.status) {
35083
+ conditions.push("status = ?");
35084
+ params.push(normalizeStatus2(filter.status));
35085
+ }
35086
+ if (filter.source) {
35087
+ conditions.push("source = ?");
35088
+ params.push(redactOptional(filter.source, 120));
35089
+ }
35090
+ const limit = Math.min(Math.max(Math.floor(filter.limit ?? 50), 1), 500);
35091
+ const rows = d.query(`SELECT * FROM task_findings WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC, created_at DESC LIMIT ?`).all(...[...params, limit]);
35092
+ return rows.map(rowToFinding);
35093
+ }
35094
+ function listCompactTaskFindings(filter = {}, db) {
35095
+ return listTaskFindings(filter, db).map(compactFinding);
35096
+ }
35097
+ function resolveMissingTaskFindings(input, db) {
35098
+ const d = db || getDatabase();
35099
+ assertTask(input.task_id, d);
35100
+ const timestamp3 = input.resolved_at || now();
35101
+ const status = normalizeResolutionStatus(input.status);
35102
+ const runId = resolveRunForTask(input.run_id, input.task_id, d);
35103
+ const present = new Set(input.fingerprints.map(normalizeFingerprint));
35104
+ const warnings = [];
35105
+ const conditions = ["task_id = ?", "status = 'open'"];
35106
+ const params = [input.task_id];
35107
+ if (input.source) {
35108
+ conditions.push("source = ?");
35109
+ params.push(redactOptional(input.source, 120));
35110
+ }
35111
+ 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));
35112
+ const limit = Math.min(Math.max(Math.floor(input.limit ?? 50), 1), 200);
35113
+ const display = candidates.slice(0, limit);
35114
+ const omittedCount = Math.max(0, candidates.length - display.length);
35115
+ if (!input.apply) {
35116
+ return {
35117
+ schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
35118
+ local_only: true,
35119
+ dry_run: true,
35120
+ processed_at: timestamp3,
35121
+ action: candidates.length > 0 ? "preview" : "noop",
35122
+ task_id: input.task_id,
35123
+ source: input.source ? redactOptional(input.source, 120) : null,
35124
+ run_id: runId,
35125
+ present_fingerprint_count: present.size,
35126
+ candidate_count: candidates.length,
35127
+ changed_count: 0,
35128
+ omitted_count: omittedCount,
35129
+ findings: display.map(compactFinding),
35130
+ warnings
35131
+ };
35132
+ }
35133
+ const metadataPatch = redactValue({
35134
+ resolved_by: {
35135
+ agent_id: input.agent_id ?? null,
35136
+ run_id: runId,
35137
+ reason: input.reason ? redactEvidenceText(input.reason) : "missing from latest loop finding set"
35138
+ }
35139
+ });
35140
+ const tx = d.transaction(() => {
35141
+ for (const finding2 of candidates) {
35142
+ d.run("UPDATE task_findings SET status = ?, resolved_at = ?, metadata = ?, updated_at = ? WHERE id = ? AND status = 'open'", [
35143
+ status,
35144
+ timestamp3,
35145
+ JSON.stringify({ ...finding2.metadata, ...metadataPatch }),
35146
+ timestamp3,
35147
+ finding2.id
35148
+ ]);
35149
+ }
35150
+ });
35151
+ tx();
35152
+ const updated = candidates.map((finding2) => d.query("SELECT * FROM task_findings WHERE id = ?").get(finding2.id)).filter((row) => Boolean(row)).map(rowToFinding);
35153
+ const visibleUpdated = updated.slice(0, limit);
35154
+ return {
35155
+ schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
35156
+ local_only: true,
35157
+ dry_run: false,
35158
+ processed_at: timestamp3,
35159
+ action: updated.length > 0 ? status : "noop",
35160
+ task_id: input.task_id,
35161
+ source: input.source ? redactOptional(input.source, 120) : null,
35162
+ run_id: runId,
35163
+ present_fingerprint_count: present.size,
35164
+ candidate_count: candidates.length,
35165
+ changed_count: updated.length,
35166
+ omitted_count: omittedCount,
35167
+ findings: visibleUpdated.map(compactFinding),
35168
+ warnings
35169
+ };
35170
+ }
35171
+ 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;
35172
+ var init_findings = __esm(() => {
35173
+ init_redaction();
35174
+ init_types();
35175
+ init_database();
35176
+ init_tasks();
35177
+ init_task_runs();
35178
+ SEVERITIES = new Set(["low", "medium", "high", "critical"]);
35179
+ STATUSES = new Set(["open", "resolved", "ignored"]);
35180
+ });
35181
+
34242
35182
  // src/lib/agent-run-dispatcher.ts
34243
35183
  var exports_agent_run_dispatcher = {};
34244
35184
  __export(exports_agent_run_dispatcher, {
@@ -35167,7 +36107,7 @@ function parseArray2(value) {
35167
36107
  return [];
35168
36108
  }
35169
36109
  }
35170
- function parseObject2(value) {
36110
+ function parseObject3(value) {
35171
36111
  try {
35172
36112
  const parsed = JSON.parse(value || "{}");
35173
36113
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
@@ -35208,7 +36148,7 @@ function rowToKnowledgeRecord(row) {
35208
36148
  agent_id: row.agent_id,
35209
36149
  snapshot_id: row.snapshot_id,
35210
36150
  tags: parseArray2(row.tags),
35211
- metadata: redactValue(parseObject2(row.metadata)),
36151
+ metadata: redactValue(parseObject3(row.metadata)),
35212
36152
  created_at: row.created_at,
35213
36153
  updated_at: row.updated_at
35214
36154
  };
@@ -35427,7 +36367,7 @@ function parseArray3(value) {
35427
36367
  return [];
35428
36368
  }
35429
36369
  }
35430
- function parseObject3(value) {
36370
+ function parseObject4(value) {
35431
36371
  try {
35432
36372
  const parsed = JSON.parse(value || "{}");
35433
36373
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
@@ -35476,7 +36416,7 @@ function rowToRisk(row) {
35476
36416
  plan_id: row.plan_id,
35477
36417
  task_id: row.task_id,
35478
36418
  tags: parseArray3(row.tags),
35479
- metadata: redactValue(parseObject3(row.metadata)),
36419
+ metadata: redactValue(parseObject4(row.metadata)),
35480
36420
  created_at: row.created_at,
35481
36421
  updated_at: row.updated_at,
35482
36422
  closed_at: row.closed_at
@@ -36327,7 +37267,7 @@ function extractUsage(value) {
36327
37267
  }
36328
37268
  return own;
36329
37269
  }
36330
- function parseJsonObject5(value) {
37270
+ function parseJsonObject6(value) {
36331
37271
  if (!value)
36332
37272
  return {};
36333
37273
  try {
@@ -36499,7 +37439,7 @@ function createLocalUsageLedger(options = {}, db) {
36499
37439
  completedRunMs += millisBetween(run.started_at, run.completed_at);
36500
37440
  else
36501
37441
  openRunMs += millisBetween(run.started_at, generatedAt);
36502
- const usage = extractUsage(parseJsonObject5(run.metadata));
37442
+ const usage = extractUsage(parseJsonObject6(run.metadata));
36503
37443
  metadataUsage.tokens += usage.tokens;
36504
37444
  metadataUsage.cost_usd += usage.cost_usd;
36505
37445
  metadataUsage.duration_ms += usage.duration_ms;
@@ -36511,7 +37451,7 @@ function createLocalUsageLedger(options = {}, db) {
36511
37451
  JOIN tasks t ON t.id = e.task_id
36512
37452
  ${runClause}`, runParams);
36513
37453
  for (const event of eventRows) {
36514
- const usage = extractUsage(parseJsonObject5(event.data));
37454
+ const usage = extractUsage(parseJsonObject6(event.data));
36515
37455
  metadataUsage.tokens += usage.tokens;
36516
37456
  metadataUsage.cost_usd += usage.cost_usd;
36517
37457
  metadataUsage.duration_ms += usage.duration_ms;
@@ -40312,6 +41252,38 @@ ${lines.join(`
40312
41252
  }
40313
41253
  });
40314
41254
  }
41255
+ if (shouldRegisterTool("begin_task_run_transaction")) {
41256
+ server.tool("begin_task_run_transaction", "Preview or apply an idempotent local loop run transaction keyed by a stable loop/run id.", {
41257
+ task_id: exports_external.string().describe("Task ID"),
41258
+ key: exports_external.string().optional().describe("Stable idempotency key"),
41259
+ loop_id: exports_external.string().optional().describe("Loop identifier; used as key fallback"),
41260
+ loop_run_id: exports_external.string().optional().describe("Loop run identifier; used as key fallback"),
41261
+ agent_id: exports_external.string().optional().describe("Agent starting the run"),
41262
+ title: exports_external.string().optional().describe("Run title"),
41263
+ summary: exports_external.string().optional().describe("Run summary"),
41264
+ metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata"),
41265
+ claim: exports_external.boolean().optional().describe("Claim/start the task before recording the run"),
41266
+ apply: exports_external.boolean().optional().describe("Apply the transaction; false or omitted is dry-run")
41267
+ }, async ({ task_id, key, loop_id, loop_run_id, agent_id, title, summary, metadata, claim, apply }) => {
41268
+ try {
41269
+ const result = beginTaskRunTransaction({
41270
+ task_id: resolveId(task_id),
41271
+ key,
41272
+ loop_id,
41273
+ loop_run_id,
41274
+ agent_id,
41275
+ title,
41276
+ summary,
41277
+ metadata,
41278
+ claim,
41279
+ apply
41280
+ });
41281
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
41282
+ } catch (e) {
41283
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
41284
+ }
41285
+ });
41286
+ }
40315
41287
  if (shouldRegisterTool("list_task_runs")) {
40316
41288
  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 }) => {
40317
41289
  try {
@@ -40409,15 +41381,117 @@ ${lines.join(`
40409
41381
  });
40410
41382
  }
40411
41383
  if (shouldRegisterTool("finish_task_run")) {
40412
- server.tool("finish_task_run", "Finish a local run ledger entry as completed, failed, or cancelled.", {
40413
- run_id: exports_external.string().describe("Run ID or prefix"),
40414
- status: exports_external.enum(["completed", "failed", "cancelled"]).describe("Final run status"),
41384
+ server.tool("finish_task_run", "Finish a local run ledger entry as completed, failed, or cancelled. Supports idempotent lookup by key.", {
41385
+ run_id: exports_external.string().optional().describe("Run ID or prefix"),
41386
+ key: exports_external.string().optional().describe("Idempotency key when run_id is omitted"),
41387
+ task_id: exports_external.string().optional().describe("Task scope for key lookup"),
41388
+ status: exports_external.enum(["completed", "failed", "cancelled"]).optional().describe("Final run status"),
40415
41389
  summary: exports_external.string().optional().describe("Final summary"),
40416
- agent_id: exports_external.string().optional().describe("Agent finishing the run")
40417
- }, async ({ run_id, status, summary, agent_id }) => {
41390
+ agent_id: exports_external.string().optional().describe("Agent finishing the run"),
41391
+ apply: exports_external.boolean().optional().describe("Apply the transaction; false previews without mutating")
41392
+ }, async ({ run_id, key, task_id, status, summary, agent_id, apply }) => {
40418
41393
  try {
40419
- const run = finishTaskRun({ run_id, status, summary, agent_id });
40420
- return { content: [{ type: "text", text: JSON.stringify(run, null, 2) }] };
41394
+ if (run_id && !key && apply === undefined) {
41395
+ const result2 = finishTaskRunTransaction({ run_id, status: status || "completed", summary, agent_id, apply: true });
41396
+ const run = result2.run ? getTaskRunLedger(result2.run.id).run : null;
41397
+ return { content: [{ type: "text", text: JSON.stringify(run, null, 2) }] };
41398
+ }
41399
+ const result = finishTaskRunTransaction({
41400
+ run_id,
41401
+ key,
41402
+ task_id: task_id ? resolveId(task_id) : undefined,
41403
+ status: status || "completed",
41404
+ summary,
41405
+ agent_id,
41406
+ apply: apply !== false
41407
+ });
41408
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
41409
+ } catch (e) {
41410
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
41411
+ }
41412
+ });
41413
+ }
41414
+ if (shouldRegisterTool("upsert_task_finding")) {
41415
+ server.tool("upsert_task_finding", "Preview or apply an idempotent local finding upsert scoped by task and fingerprint.", {
41416
+ task_id: exports_external.string().describe("Task ID"),
41417
+ fingerprint: exports_external.string().describe("Stable finding fingerprint"),
41418
+ title: exports_external.string().describe("Finding title"),
41419
+ severity: exports_external.enum(["low", "medium", "high", "critical"]).optional().describe("Finding severity"),
41420
+ status: exports_external.enum(["open", "resolved", "ignored"]).optional().describe("Finding status"),
41421
+ source: exports_external.string().optional().describe("Loop/tool source name"),
41422
+ summary: exports_external.string().optional().describe("Bounded finding summary"),
41423
+ artifact_path: exports_external.string().optional().describe("Local artifact path/reference; content is not read"),
41424
+ run_id: exports_external.string().optional().describe("Run ledger ID or prefix"),
41425
+ metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata"),
41426
+ apply: exports_external.boolean().optional().describe("Apply the upsert; false or omitted is dry-run")
41427
+ }, async ({ task_id, fingerprint: fingerprint3, title, severity, status, source: source3, summary, artifact_path, run_id, metadata, apply }) => {
41428
+ try {
41429
+ const result = upsertTaskFinding({
41430
+ task_id: resolveId(task_id),
41431
+ fingerprint: fingerprint3,
41432
+ title,
41433
+ severity,
41434
+ status,
41435
+ source: source3,
41436
+ summary,
41437
+ artifact_path,
41438
+ run_id,
41439
+ metadata,
41440
+ apply
41441
+ });
41442
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
41443
+ } catch (e) {
41444
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
41445
+ }
41446
+ });
41447
+ }
41448
+ if (shouldRegisterTool("list_task_findings")) {
41449
+ server.tool("list_task_findings", "List compact local findings with bounded output.", {
41450
+ task_id: exports_external.string().optional().describe("Filter by task"),
41451
+ run_id: exports_external.string().optional().describe("Filter by run"),
41452
+ status: exports_external.enum(["open", "resolved", "ignored"]).optional().describe("Filter by status"),
41453
+ source: exports_external.string().optional().describe("Filter by source"),
41454
+ limit: exports_external.number().optional().describe("Maximum findings to return")
41455
+ }, async ({ task_id, run_id, status, source: source3, limit }) => {
41456
+ try {
41457
+ const findings = listCompactTaskFindings({
41458
+ task_id: task_id ? resolveId(task_id) : undefined,
41459
+ run_id,
41460
+ status,
41461
+ source: source3,
41462
+ limit
41463
+ });
41464
+ return { content: [{ type: "text", text: JSON.stringify(findings) }] };
41465
+ } catch (e) {
41466
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
41467
+ }
41468
+ });
41469
+ }
41470
+ if (shouldRegisterTool("resolve_missing_task_findings")) {
41471
+ server.tool("resolve_missing_task_findings", "Resolve open findings absent from the latest loop finding set.", {
41472
+ task_id: exports_external.string().describe("Task ID"),
41473
+ fingerprints: exports_external.array(exports_external.string()).optional().describe("Fingerprints still present in the latest run"),
41474
+ source: exports_external.string().optional().describe("Only resolve findings from this source"),
41475
+ run_id: exports_external.string().optional().describe("Run ledger ID or prefix for audit metadata"),
41476
+ status: exports_external.enum(["resolved", "ignored"]).optional().describe("Resolution status"),
41477
+ agent_id: exports_external.string().optional().describe("Agent resolving findings"),
41478
+ reason: exports_external.string().optional().describe("Resolution reason"),
41479
+ limit: exports_external.number().optional().describe("Maximum findings returned"),
41480
+ apply: exports_external.boolean().optional().describe("Apply resolution; false or omitted is dry-run")
41481
+ }, async ({ task_id, fingerprints, source: source3, run_id, status, agent_id, reason, limit, apply }) => {
41482
+ try {
41483
+ const result = resolveMissingTaskFindings({
41484
+ task_id: resolveId(task_id),
41485
+ fingerprints: fingerprints || [],
41486
+ source: source3,
41487
+ run_id,
41488
+ status,
41489
+ agent_id,
41490
+ reason,
41491
+ limit,
41492
+ apply
41493
+ });
41494
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
40421
41495
  } catch (e) {
40422
41496
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
40423
41497
  }
@@ -40753,6 +41827,7 @@ var init_task_resources = __esm(() => {
40753
41827
  init_agents();
40754
41828
  init_task_commits();
40755
41829
  init_task_runs();
41830
+ init_findings();
40756
41831
  init_agent_run_dispatcher();
40757
41832
  init_verification_providers();
40758
41833
  init_release_notes();
@@ -44493,8 +45568,10 @@ function taskToSummary(task2, fields) {
44493
45568
  task_list_id: task2.task_list_id,
44494
45569
  agent_id: task2.agent_id,
44495
45570
  assigned_to: task2.assigned_to,
45571
+ working_dir: task2.working_dir,
44496
45572
  locked_by: task2.locked_by,
44497
45573
  tags: task2.tags,
45574
+ metadata: task2.metadata,
44498
45575
  version: task2.version,
44499
45576
  created_at: task2.created_at,
44500
45577
  updated_at: task2.updated_at,
@@ -44636,6 +45713,9 @@ Dashboard not found at: ${dashboardDir}`);
44636
45713
  if (path === "/api/tasks" && method === "POST") {
44637
45714
  return handleCreateTask(req, ctx, jsonWithCors, taskToSummary);
44638
45715
  }
45716
+ if (path === "/api/tasks/upsert" && method === "POST") {
45717
+ return handleUpsertTask(req, ctx, jsonWithCors, taskToSummary);
45718
+ }
44639
45719
  if (path === "/api/tasks/export" && method === "GET") {
44640
45720
  return handleTasksExport(req, url, ctx, jsonWithCors, taskToSummary);
44641
45721
  }
@@ -44831,7 +45911,7 @@ Dashboard not found at: ${dashboardDir}`);
44831
45911
  } catch {}
44832
45912
  }
44833
45913
  }
44834
- var MIME_TYPES, SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX = 120;
45914
+ var MIME_TYPES, SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX;
44835
45915
  var init_serve = __esm(() => {
44836
45916
  init_database();
44837
45917
  init_api_keys();
@@ -44856,6 +45936,7 @@ var init_serve = __esm(() => {
44856
45936
  "Permissions-Policy": "camera=, microphone=, geolocation="
44857
45937
  };
44858
45938
  rateLimitMap = new Map;
45939
+ RATE_LIMIT_MAX = Number.parseInt(process.env["TODOS_RATE_LIMIT_MAX"] || "120", 10);
44859
45940
  });
44860
45941
 
44861
45942
  // src/cli/components/Header.tsx
@@ -47509,9 +48590,9 @@ function normalizeKind(value) {
47509
48590
  const raw = cleanKey(asString4(value) || "unknown").replace(/-/g, "_");
47510
48591
  return KINDS.has(raw) ? raw : raw || "unknown";
47511
48592
  }
47512
- function normalizeSeverity(value, fallback) {
48593
+ function normalizeSeverity2(value, fallback) {
47513
48594
  const raw = cleanKey(asString4(value) || fallback);
47514
- if (SEVERITIES.has(raw))
48595
+ if (SEVERITIES2.has(raw))
47515
48596
  return raw;
47516
48597
  if (/^(p0|blocker|urgent|highest)$/.test(raw))
47517
48598
  return "critical";
@@ -47606,7 +48687,7 @@ function normalizeTesterIssueReport(value, fallbackPriority = "medium") {
47606
48687
  title,
47607
48688
  summary: truncate3(asString4(input["summary"]), 1000) ?? null,
47608
48689
  kind: normalizeKind(input["kind"] ?? input["type"]),
47609
- severity: normalizeSeverity(input["severity"] ?? input["priority"], fallbackPriority),
48690
+ severity: normalizeSeverity2(input["severity"] ?? input["priority"], fallbackPriority),
47610
48691
  source: source3,
47611
48692
  target,
47612
48693
  failure,
@@ -47904,13 +48985,13 @@ function readTesterIssueReportsPayload(value) {
47904
48985
  return record["issues"];
47905
48986
  return [value];
47906
48987
  }
47907
- var TESTERS_ISSUE_REPORT_SCHEMA_VERSION = "testers.issue_report.v1", TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION = "todos.tester_issue_report_result.v1", TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION = "todos.tester_issue_report_batch_result.v1", PRIORITIES5, SEVERITIES, KINDS;
48988
+ var TESTERS_ISSUE_REPORT_SCHEMA_VERSION = "testers.issue_report.v1", TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION = "todos.tester_issue_report_result.v1", TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION = "todos.tester_issue_report_batch_result.v1", PRIORITIES5, SEVERITIES2, KINDS;
47908
48989
  var init_tester_issue_reports = __esm(() => {
47909
48990
  init_database();
47910
48991
  init_tasks();
47911
48992
  init_redaction();
47912
48993
  PRIORITIES5 = ["low", "medium", "high", "critical"];
47913
- SEVERITIES = new Set(PRIORITIES5);
48994
+ SEVERITIES2 = new Set(PRIORITIES5);
47914
48995
  KINDS = new Set([
47915
48996
  "assertion_failure",
47916
48997
  "runtime_error",
@@ -51092,6 +52173,23 @@ Verifications:`));
51092
52173
  console.log(`${result.status} ${result.provider}: ${result.output_summary || ""}`);
51093
52174
  });
51094
52175
  const runs = program2.command("runs").description("Manage the local run ledger and evidence capture");
52176
+ runs.command("begin <task-id>").description("Preview or apply an idempotent loop run transaction").option("--key <key>", "Stable idempotency key for this loop transaction").option("--loop-id <id>", "Loop identifier; used as the key when --key/--loop-run-id are omitted").option("--loop-run-id <id>", "Loop run identifier; used as the key when --key is omitted").option("--agent <name>", "Agent starting the run").option("--title <text>", "Run title").option("--summary <text>", "Run summary").option("--metadata <json>", "Additional JSON metadata").option("--claim", "Claim/start the task for the agent before recording the run").option("--apply", "Apply the transaction; omitted means dry-run").action(async (taskId, opts) => {
52177
+ const globalOpts = program2.opts();
52178
+ const { beginTaskRunTransaction: beginTaskRunTransaction2 } = await Promise.resolve().then(() => (init_task_runs(), exports_task_runs));
52179
+ const result = beginTaskRunTransaction2({
52180
+ task_id: resolveTaskId(taskId),
52181
+ key: opts.key,
52182
+ loop_id: opts.loopId,
52183
+ loop_run_id: opts.loopRunId,
52184
+ agent_id: opts.agent || globalOpts.agent,
52185
+ title: opts.title,
52186
+ summary: opts.summary,
52187
+ metadata: parseJsonOption(opts.metadata, "--metadata"),
52188
+ claim: opts.claim,
52189
+ apply: opts.apply
52190
+ });
52191
+ output(result, true);
52192
+ });
51095
52193
  runs.command("start <task-id>").description("Start a local run ledger entry for a task").option("--agent <name>", "Agent starting the run").option("--title <text>", "Run title").option("--summary <text>", "Run summary").option("--metadata <json>", "Additional JSON metadata").option("--claim", "Claim/start the task for the agent before recording the run").action(async (taskId, opts) => {
51096
52194
  const globalOpts = program2.opts();
51097
52195
  const { startTaskRun: startTaskRun2 } = await Promise.resolve().then(() => (init_task_runs(), exports_task_runs));
@@ -51303,19 +52401,68 @@ Artifacts:`));
51303
52401
  process.exit(1);
51304
52402
  }
51305
52403
  });
51306
- runs.command("finish <run-id>").description("Finish a run ledger entry").option("--status <status>", "completed, failed, or cancelled", "completed").option("--summary <text>", "Final summary").option("--agent <name>", "Agent finishing the run").action(async (runId, opts) => {
52404
+ runs.command("finish [run-id]").description("Finish a run ledger entry idempotently").option("--key <key>", "Resolve run by idempotency key when run-id is omitted").option("--task <task-id>", "Task scope for --key lookup").option("--status <status>", "completed, failed, or cancelled", "completed").option("--summary <text>", "Final summary").option("--agent <name>", "Agent finishing the run").option("--dry-run", "Preview without mutating").action(async (runId, opts) => {
51307
52405
  const globalOpts = program2.opts();
51308
52406
  if (opts.status !== "completed" && opts.status !== "failed" && opts.status !== "cancelled") {
51309
52407
  console.error(chalk8.red("--status must be completed, failed, or cancelled"));
51310
52408
  process.exit(1);
51311
52409
  }
51312
- const { finishTaskRun: finishTaskRun2 } = await Promise.resolve().then(() => (init_task_runs(), exports_task_runs));
51313
- const run = finishTaskRun2({ run_id: runId, status: opts.status, summary: opts.summary, agent_id: opts.agent || globalOpts.agent });
51314
- if (globalOpts.json) {
51315
- output(run, true);
51316
- return;
51317
- }
51318
- console.log(chalk8.green(`Finished run ${run.id.slice(0, 8)} as ${run.status}`));
52410
+ const { finishTaskRunTransaction: finishTaskRunTransaction2 } = await Promise.resolve().then(() => (init_task_runs(), exports_task_runs));
52411
+ const result = finishTaskRunTransaction2({
52412
+ run_id: runId,
52413
+ key: opts.key,
52414
+ task_id: opts.task ? resolveTaskId(opts.task) : undefined,
52415
+ status: opts.status,
52416
+ summary: opts.summary,
52417
+ agent_id: opts.agent || globalOpts.agent,
52418
+ apply: !opts.dryRun
52419
+ });
52420
+ output(result, true);
52421
+ });
52422
+ const findings = program2.command("findings").description("Manage local task findings for loop dedupe and resolution");
52423
+ findings.command("upsert").description("Preview or apply an idempotent finding upsert").requiredOption("--task <task-id>", "Task ID").requiredOption("--fingerprint <value>", "Stable finding fingerprint").requiredOption("--title <text>", "Finding title").option("--severity <severity>", "low, medium, high, or critical", "medium").option("--status <status>", "open, resolved, or ignored", "open").option("--source <source>", "Loop/tool source name").option("--summary <text>", "Bounded finding summary").option("--artifact <path>", "Local artifact path/reference; content is not read").option("--run <run-id>", "Run ledger ID or prefix").option("--metadata <json>", "Additional JSON metadata").option("--apply", "Apply the upsert; omitted means dry-run").action(async (opts) => {
52424
+ const { upsertTaskFinding: upsertTaskFinding2 } = await Promise.resolve().then(() => (init_findings(), exports_findings));
52425
+ const result = upsertTaskFinding2({
52426
+ task_id: resolveTaskId(opts.task),
52427
+ fingerprint: opts.fingerprint,
52428
+ title: opts.title,
52429
+ severity: opts.severity,
52430
+ status: opts.status,
52431
+ source: opts.source,
52432
+ summary: opts.summary,
52433
+ artifact_path: opts.artifact,
52434
+ run_id: opts.run,
52435
+ metadata: parseJsonOption(opts.metadata, "--metadata"),
52436
+ apply: opts.apply
52437
+ });
52438
+ output(result, true);
52439
+ });
52440
+ findings.command("resolve-missing").description("Resolve open findings absent from the latest loop finding set").requiredOption("--task <task-id>", "Task ID").option("--fingerprints <list>", "Comma-separated fingerprints still present").option("--source <source>", "Only resolve findings from this source").option("--run <run-id>", "Run ledger ID or prefix for audit metadata").option("--status <status>", "resolved or ignored", "resolved").option("--agent <name>", "Agent resolving findings").option("--reason <text>", "Resolution reason").option("--limit <n>", "Maximum findings returned", "50").option("--apply", "Apply resolution; omitted means dry-run").action(async (opts) => {
52441
+ const globalOpts = program2.opts();
52442
+ const { resolveMissingTaskFindings: resolveMissingTaskFindings2 } = await Promise.resolve().then(() => (init_findings(), exports_findings));
52443
+ const result = resolveMissingTaskFindings2({
52444
+ task_id: resolveTaskId(opts.task),
52445
+ fingerprints: listOption(opts.fingerprints) || [],
52446
+ source: opts.source,
52447
+ run_id: opts.run,
52448
+ status: opts.status,
52449
+ agent_id: opts.agent || globalOpts.agent,
52450
+ reason: opts.reason,
52451
+ limit: opts.limit ? Number.parseInt(opts.limit, 10) : undefined,
52452
+ apply: opts.apply
52453
+ });
52454
+ output(result, true);
52455
+ });
52456
+ findings.command("list").description("List compact local findings").option("--task <task-id>", "Filter by task").option("--run <run-id>", "Filter by run").option("--status <status>", "Filter by open, resolved, or ignored").option("--source <source>", "Filter by source").option("--limit <n>", "Maximum findings returned", "50").action(async (opts) => {
52457
+ const { listCompactTaskFindings: listCompactTaskFindings2 } = await Promise.resolve().then(() => (init_findings(), exports_findings));
52458
+ const findings2 = listCompactTaskFindings2({
52459
+ task_id: opts.task ? resolveTaskId(opts.task) : undefined,
52460
+ run_id: opts.run,
52461
+ status: opts.status,
52462
+ source: opts.source,
52463
+ limit: opts.limit ? Number.parseInt(opts.limit, 10) : undefined
52464
+ });
52465
+ output(findings2, true);
51319
52466
  });
51320
52467
  const agentRuns = program2.command("agent-runs").description("Queue and dispatch local agent runs");
51321
52468
  agentRuns.command("adapter-set <name>").description("Create or update a local agent run adapter").option("--command <command>", "Local command template. Supports {task_id}, {run_id}, and {agent_id}").option("--sandbox <name>", "Runner sandbox profile to check before launch").option("--cwd <path>", "Command working directory").option("--env <json>", "Static adapter environment as a JSON object").action(async (name, opts) => {
@@ -52229,7 +53376,7 @@ function parseRecordType(value) {
52229
53376
  console.error(chalk13.red(`type must be one of: ${RECORD_TYPES.join(", ")}`));
52230
53377
  process.exit(1);
52231
53378
  }
52232
- function parseJsonObject6(value, label) {
53379
+ function parseJsonObject7(value, label) {
52233
53380
  if (!value)
52234
53381
  return;
52235
53382
  try {
@@ -52289,7 +53436,7 @@ function registerKnowledgeCommands(program2) {
52289
53436
  plan_id: opts.plan,
52290
53437
  agent_id: opts.agent || globalOpts.agent,
52291
53438
  tags: tagsFromOption(opts.tag),
52292
- metadata: parseJsonObject6(opts.metadataJson, "--metadata-json")
53439
+ metadata: parseJsonObject7(opts.metadataJson, "--metadata-json")
52293
53440
  }, getDatabase());
52294
53441
  if (opts.json || globalOpts.json)
52295
53442
  output(record, true);
@@ -52314,7 +53461,7 @@ function registerKnowledgeCommands(program2) {
52314
53461
  blockers: opts.blocker,
52315
53462
  next_steps: opts.next,
52316
53463
  tags: tagsFromOption(opts.tag),
52317
- metadata: parseJsonObject6(opts.metadataJson, "--metadata-json")
53464
+ metadata: parseJsonObject7(opts.metadataJson, "--metadata-json")
52318
53465
  }, getDatabase());
52319
53466
  if (opts.json || globalOpts.json)
52320
53467
  output(result, true);
@@ -52400,7 +53547,7 @@ function parseChoice(value, choices, label) {
52400
53547
  console.error(chalk14.red(`${label} must be one of: ${choices.join(", ")}`));
52401
53548
  process.exit(1);
52402
53549
  }
52403
- function parseJsonObject7(value, label) {
53550
+ function parseJsonObject8(value, label) {
52404
53551
  if (!value)
52405
53552
  return;
52406
53553
  try {
@@ -52421,8 +53568,8 @@ function tagsFromOption2(value) {
52421
53568
  }
52422
53569
  function commonFilters2(opts) {
52423
53570
  return {
52424
- status: opts.status ? parseChoice(opts.status, STATUSES, "--status") : undefined,
52425
- severity: opts.severity ? parseChoice(opts.severity, SEVERITIES2, "--severity") : undefined,
53571
+ status: opts.status ? parseChoice(opts.status, STATUSES2, "--status") : undefined,
53572
+ severity: opts.severity ? parseChoice(opts.severity, SEVERITIES3, "--severity") : undefined,
52426
53573
  probability: opts.probability ? parseChoice(opts.probability, PROBABILITIES, "--probability") : undefined,
52427
53574
  owner: opts.owner,
52428
53575
  project_id: opts.project,
@@ -52455,8 +53602,8 @@ function registerRiskCommands(program2) {
52455
53602
  const risk = createRisk({
52456
53603
  title,
52457
53604
  description: opts.description,
52458
- status: parseChoice(opts.status, STATUSES, "--status"),
52459
- severity: parseChoice(opts.severity, SEVERITIES2, "--severity"),
53605
+ status: parseChoice(opts.status, STATUSES2, "--status"),
53606
+ severity: parseChoice(opts.severity, SEVERITIES3, "--severity"),
52460
53607
  probability: parseChoice(opts.probability, PROBABILITIES, "--probability"),
52461
53608
  owner: opts.owner,
52462
53609
  mitigation: opts.mitigation,
@@ -52465,7 +53612,7 @@ function registerRiskCommands(program2) {
52465
53612
  plan_id: opts.plan,
52466
53613
  task_id: opts.task,
52467
53614
  tags: tagsFromOption2(opts.tag),
52468
- metadata: parseJsonObject7(opts.metadataJson, "--metadata-json")
53615
+ metadata: parseJsonObject8(opts.metadataJson, "--metadata-json")
52469
53616
  }, getDatabase());
52470
53617
  if (opts.json || globalOpts.json)
52471
53618
  output(risk, true);
@@ -52507,8 +53654,8 @@ function registerRiskCommands(program2) {
52507
53654
  const risk = updateRisk(id, {
52508
53655
  title: opts.title,
52509
53656
  description: opts.description,
52510
- status: opts.status ? parseChoice(opts.status, STATUSES, "--status") : undefined,
52511
- severity: opts.severity ? parseChoice(opts.severity, SEVERITIES2, "--severity") : undefined,
53657
+ status: opts.status ? parseChoice(opts.status, STATUSES2, "--status") : undefined,
53658
+ severity: opts.severity ? parseChoice(opts.severity, SEVERITIES3, "--severity") : undefined,
52512
53659
  probability: opts.probability ? parseChoice(opts.probability, PROBABILITIES, "--probability") : undefined,
52513
53660
  owner: opts.owner,
52514
53661
  mitigation: opts.mitigation,
@@ -52517,7 +53664,7 @@ function registerRiskCommands(program2) {
52517
53664
  plan_id: opts.plan,
52518
53665
  task_id: opts.task,
52519
53666
  tags: opts.tag.length > 0 ? tagsFromOption2(opts.tag) : undefined,
52520
- metadata: parseJsonObject7(opts.metadataJson, "--metadata-json")
53667
+ metadata: parseJsonObject8(opts.metadataJson, "--metadata-json")
52521
53668
  }, getDatabase());
52522
53669
  if (opts.json || globalOpts.json)
52523
53670
  output(risk, true);
@@ -52575,13 +53722,13 @@ function registerRiskCommands(program2) {
52575
53722
  }
52576
53723
  });
52577
53724
  }
52578
- var STATUSES, SEVERITIES2, PROBABILITIES;
53725
+ var STATUSES2, SEVERITIES3, PROBABILITIES;
52579
53726
  var init_risk_commands = __esm(() => {
52580
53727
  init_database();
52581
53728
  init_project_risks();
52582
53729
  init_helpers();
52583
- STATUSES = ["open", "mitigating", "resolved", "accepted"];
52584
- SEVERITIES2 = ["low", "medium", "high", "critical"];
53730
+ STATUSES2 = ["open", "mitigating", "resolved", "accepted"];
53731
+ SEVERITIES3 = ["low", "medium", "high", "critical"];
52585
53732
  PROBABILITIES = ["low", "medium", "high"];
52586
53733
  });
52587
53734
 
@@ -55166,6 +56313,92 @@ var init_json_contracts = __esm(() => {
55166
56313
  },
55167
56314
  optional: {}
55168
56315
  }),
56316
+ contract({
56317
+ id: "loop_run_transaction",
56318
+ name: "Loop Run Transaction",
56319
+ description: "Compact local result for idempotent loop run begin/finish transactions.",
56320
+ surfaces: ["cli", "mcp", "sdk"],
56321
+ stability: "stable",
56322
+ required: {
56323
+ schema_version: field("string", "Result schema version."),
56324
+ local_only: field("boolean", "Always true; loop run transactions use local state."),
56325
+ dry_run: field("boolean", "True when no run ledger mutation was applied."),
56326
+ processed_at: isoDateField,
56327
+ action: field("string", "preview, created, matched, finished, or conflict."),
56328
+ key: field("string", "Stable idempotency key used to dedupe the transaction."),
56329
+ run: field(["object", "null"], "Compact run summary or null for create previews.", true),
56330
+ warnings: field("array", "Non-fatal warnings such as terminal-status conflicts."),
56331
+ commands: field("array", "Follow-up CLI commands for agents and operators.")
56332
+ },
56333
+ optional: {}
56334
+ }),
56335
+ contract({
56336
+ id: "task_finding",
56337
+ name: "Task Finding",
56338
+ description: "Compact local finding record deduped by task and fingerprint.",
56339
+ surfaces: ["cli", "mcp", "sdk"],
56340
+ stability: "stable",
56341
+ required: {
56342
+ schema_version: field("string", "Finding schema version."),
56343
+ id: idField,
56344
+ task_id: idField,
56345
+ run_id: field(["string", "null"], "Optional run ledger ID.", true),
56346
+ fingerprint: field("string", "Stable finding fingerprint scoped to the task."),
56347
+ title: field("string", "Short redacted finding title."),
56348
+ severity: field("string", "low, medium, high, or critical."),
56349
+ status: field("string", "open, resolved, or ignored."),
56350
+ source: field(["string", "null"], "Optional loop/tool source.", true),
56351
+ summary: field(["string", "null"], "Bounded redacted finding summary.", true),
56352
+ artifact_path: field(["string", "null"], "Local artifact path/reference; raw content is not included.", true),
56353
+ first_seen_at: isoDateField,
56354
+ last_seen_at: isoDateField,
56355
+ resolved_at: field(["string", "null"], "Resolution timestamp when closed.", true),
56356
+ metadata_keys: field("array", "Sorted metadata keys; metadata values are intentionally omitted in compact output.")
56357
+ },
56358
+ optional: {}
56359
+ }),
56360
+ contract({
56361
+ id: "task_finding_upsert",
56362
+ name: "Task Finding Upsert Result",
56363
+ description: "Local-only dry-run or applied result from idempotently upserting a task finding.",
56364
+ surfaces: ["cli", "mcp", "sdk"],
56365
+ stability: "stable",
56366
+ required: {
56367
+ schema_version: field("string", "Result schema version."),
56368
+ local_only: field("boolean", "Always true; finding upserts use local state."),
56369
+ dry_run: field("boolean", "True when no finding row was created or updated."),
56370
+ processed_at: isoDateField,
56371
+ action: field("string", "preview, created, matched, updated, or reopened."),
56372
+ fingerprint: field("string", "Normalized finding fingerprint."),
56373
+ finding: field(["object", "null"], "Compact finding summary or null for create previews.", true),
56374
+ warnings: field("array", "Non-fatal warnings.")
56375
+ },
56376
+ optional: {}
56377
+ }),
56378
+ contract({
56379
+ id: "task_finding_resolve_missing",
56380
+ name: "Task Finding Resolve Missing Result",
56381
+ description: "Local-only dry-run or applied result from resolving open findings absent from the latest loop finding set.",
56382
+ surfaces: ["cli", "mcp", "sdk"],
56383
+ stability: "stable",
56384
+ required: {
56385
+ schema_version: field("string", "Result schema version."),
56386
+ local_only: field("boolean", "Always true; finding resolution uses local state."),
56387
+ dry_run: field("boolean", "True when no finding rows were changed."),
56388
+ processed_at: isoDateField,
56389
+ action: field("string", "preview, resolved, ignored, or noop."),
56390
+ task_id: idField,
56391
+ source: field(["string", "null"], "Optional source scope.", true),
56392
+ run_id: field(["string", "null"], "Optional run ledger ID used for audit metadata.", true),
56393
+ present_fingerprint_count: field("integer", "Number of fingerprints supplied as still present."),
56394
+ candidate_count: field("integer", "Open findings missing from the supplied set."),
56395
+ changed_count: field("integer", "Rows resolved or ignored by the applied transaction."),
56396
+ omitted_count: field("integer", "Matching findings omitted from bounded output."),
56397
+ findings: field("array", "Bounded compact finding summaries."),
56398
+ warnings: field("array", "Non-fatal warnings.")
56399
+ },
56400
+ optional: {}
56401
+ }),
55169
56402
  contract({
55170
56403
  id: "verification_provider",
55171
56404
  name: "Verification Provider",