@hasna/todos 0.11.58 → 0.11.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +66 -2
  2. package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
  3. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  4. package/dist/cli/index.js +1518 -197
  5. package/dist/contracts.d.ts.map +1 -1
  6. package/dist/contracts.js +600 -14
  7. package/dist/db/findings.d.ts +108 -0
  8. package/dist/db/findings.d.ts.map +1 -0
  9. package/dist/db/migrations.d.ts.map +1 -1
  10. package/dist/db/schema.d.ts.map +1 -1
  11. package/dist/db/task-crud.d.ts +3 -1
  12. package/dist/db/task-crud.d.ts.map +1 -1
  13. package/dist/db/task-runs.d.ts +56 -0
  14. package/dist/db/task-runs.d.ts.map +1 -1
  15. package/dist/db/tasks.d.ts +2 -2
  16. package/dist/db/tasks.d.ts.map +1 -1
  17. package/dist/index.d.ts +5 -3
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +968 -15
  20. package/dist/json-contracts.d.ts.map +1 -1
  21. package/dist/lib/access-profiles.d.ts.map +1 -1
  22. package/dist/lib/event-hooks.d.ts +1 -1
  23. package/dist/lib/event-hooks.d.ts.map +1 -1
  24. package/dist/lib/shared-events.d.ts +1 -1
  25. package/dist/lib/shared-events.d.ts.map +1 -1
  26. package/dist/mcp/index.js +1082 -27
  27. package/dist/mcp/token-utils.d.ts.map +1 -1
  28. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  29. package/dist/mcp/tools/task-resources.d.ts.map +1 -1
  30. package/dist/mcp.js +12 -1
  31. package/dist/registry.js +600 -14
  32. package/dist/release-provenance.json +3 -3
  33. package/dist/server/index.js +1082 -27
  34. package/dist/server/routes.d.ts +1 -0
  35. package/dist/server/routes.d.ts.map +1 -1
  36. package/dist/server/serve.d.ts +2 -0
  37. package/dist/server/serve.d.ts.map +1 -1
  38. package/dist/storage.js +473 -11
  39. package/dist/types/index.d.ts +11 -0
  40. package/dist/types/index.d.ts.map +1 -1
  41. package/package.json +2 -2
package/dist/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",
@@ -6154,7 +6239,7 @@ var init_event_hooks = __esm(() => {
6154
6239
  VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
6155
6240
  });
6156
6241
 
6157
- // node_modules/.bun/@hasna+events@0.1.9/node_modules/@hasna/events/dist/index.js
6242
+ // node_modules/.bun/@hasna+events@0.1.10/node_modules/@hasna/events/dist/index.js
6158
6243
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
6159
6244
  import { existsSync as existsSync6 } from "fs";
6160
6245
  import { homedir } from "os";
@@ -6201,14 +6286,40 @@ function matchRecord(input, matcher) {
6201
6286
  return true;
6202
6287
  return Object.entries(matcher).every(([path, expected]) => {
6203
6288
  const actual = getPathValue(input, path);
6204
- if (typeof expected === "string" || Array.isArray(expected)) {
6205
- return matchString(actual === undefined ? undefined : String(actual), expected, {
6206
- segmentSafe: path.endsWith("_path") || path.endsWith(".path")
6207
- });
6208
- }
6209
- return actual === expected;
6289
+ return matchField(actual, expected, path);
6210
6290
  });
6211
6291
  }
6292
+ function matchField(actual, expected, path) {
6293
+ if (isNegativeMatcher(expected)) {
6294
+ return !matchPositiveField(actual, expected.not, path);
6295
+ }
6296
+ return matchPositiveField(actual, expected, path);
6297
+ }
6298
+ function matchPositiveField(actual, expected, path) {
6299
+ if (typeof expected === "string" || Array.isArray(expected)) {
6300
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
6301
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
6302
+ }));
6303
+ }
6304
+ if (Array.isArray(actual)) {
6305
+ return actual.some((item) => item === expected);
6306
+ }
6307
+ return actual === expected;
6308
+ }
6309
+ function stringCandidates(actual) {
6310
+ if (actual === undefined)
6311
+ return [];
6312
+ if (Array.isArray(actual)) {
6313
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
6314
+ }
6315
+ return [String(actual)];
6316
+ }
6317
+ function isPrimitiveFieldValue(value) {
6318
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
6319
+ }
6320
+ function isNegativeMatcher(value) {
6321
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
6322
+ }
6212
6323
  function eventMatchesFilter(event, filter) {
6213
6324
  return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
6214
6325
  }
@@ -6816,9 +6927,66 @@ function taskEventData(task, extra = {}) {
6816
6927
  started_at: task.started_at,
6817
6928
  completed_at: task.completed_at,
6818
6929
  due_at: task.due_at,
6930
+ requires_approval: task.requires_approval,
6931
+ approved_by: task.approved_by,
6932
+ approved_at: task.approved_at,
6819
6933
  ...extra
6820
6934
  };
6821
6935
  }
6936
+ function booleanField(value) {
6937
+ if (typeof value === "boolean")
6938
+ return value;
6939
+ if (typeof value === "number") {
6940
+ if (value === 1)
6941
+ return true;
6942
+ if (value === 0)
6943
+ return false;
6944
+ }
6945
+ if (typeof value === "string") {
6946
+ const normalized = value.trim().toLowerCase();
6947
+ if (["true", "1", "yes", "on"].includes(normalized))
6948
+ return true;
6949
+ if (["false", "0", "no", "off"].includes(normalized))
6950
+ return false;
6951
+ }
6952
+ return;
6953
+ }
6954
+ function objectField(value) {
6955
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
6956
+ }
6957
+ function firstBoolean(records, keys) {
6958
+ for (const record of records) {
6959
+ for (const key of keys) {
6960
+ const value = booleanField(record[key]);
6961
+ if (value !== undefined)
6962
+ return value;
6963
+ }
6964
+ }
6965
+ return;
6966
+ }
6967
+ function routingAutomationMetadata(task) {
6968
+ const automation = objectField(task.metadata.automation);
6969
+ const records = [task.metadata];
6970
+ if (automation)
6971
+ records.push(automation);
6972
+ const result = {};
6973
+ const aliases = [
6974
+ ["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
6975
+ ["no_auto", ["no_auto", "noAuto"]],
6976
+ ["manual", ["manual"]],
6977
+ ["manual_required", ["manual_required", "manualRequired"]],
6978
+ ["requires_approval", ["requires_approval", "requiresApproval"]],
6979
+ ["approval_required", ["approval_required", "approvalRequired"]]
6980
+ ];
6981
+ for (const [canonical, keys] of aliases) {
6982
+ const value = firstBoolean(records, keys);
6983
+ if (value !== undefined)
6984
+ result[canonical] = value;
6985
+ }
6986
+ if (task.requires_approval)
6987
+ result.requires_approval = true;
6988
+ return Object.keys(result).length > 0 ? result : undefined;
6989
+ }
6822
6990
  function taskEventMetadata(task) {
6823
6991
  const metadata = {
6824
6992
  package: "@hasna/todos",
@@ -6829,6 +6997,14 @@ function taskEventMetadata(task) {
6829
6997
  task_list_id: task.task_list_id,
6830
6998
  working_dir: task.working_dir
6831
6999
  };
7000
+ const routeEnabled = booleanField(task.metadata.route_enabled);
7001
+ if (routeEnabled !== undefined) {
7002
+ metadata.route_enabled = routeEnabled;
7003
+ }
7004
+ const automation = routingAutomationMetadata(task);
7005
+ if (automation) {
7006
+ metadata.automation = automation;
7007
+ }
6832
7008
  try {
6833
7009
  const project = task.project_id ? getProject(task.project_id) : null;
6834
7010
  const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
@@ -6846,9 +7022,6 @@ function taskEventMetadata(task) {
6846
7022
  if (projectPath) {
6847
7023
  metadata.project_kind = classifyProjectKind(projectPath);
6848
7024
  metadata.project_is_worktree = isWorktreePath(projectPath);
6849
- if (typeof task.metadata.route_enabled === "boolean") {
6850
- metadata.route_enabled = task.metadata.route_enabled;
6851
- }
6852
7025
  metadata.working_dir = task.working_dir ?? projectPath;
6853
7026
  }
6854
7027
  const taskList = task.task_list_id ? getTaskList(task.task_list_id) ?? (project ? getTaskListBySlug(task.task_list_id, project.id) : null) : project?.task_list_id ? getTaskListBySlug(project.task_list_id, project.id) : null;
@@ -7463,6 +7636,17 @@ function replaceTaskTags(taskId, tags, db) {
7463
7636
  db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
7464
7637
  insertTaskTags(taskId, tags, db);
7465
7638
  }
7639
+ function addMetadataConditions(metadata, conditions, params) {
7640
+ if (!metadata)
7641
+ return;
7642
+ for (const [key, value] of Object.entries(metadata)) {
7643
+ if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
7644
+ throw new Error(`Invalid metadata filter key: ${key}`);
7645
+ }
7646
+ conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
7647
+ params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
7648
+ }
7649
+ }
7466
7650
  function createTask(input, db) {
7467
7651
  const d = db || getDatabase();
7468
7652
  const timestamp = now();
@@ -7645,6 +7829,7 @@ function listTasks(filter = {}, db) {
7645
7829
  params.push(filter.task_type);
7646
7830
  }
7647
7831
  }
7832
+ addMetadataConditions(filter.metadata, conditions, params);
7648
7833
  const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
7649
7834
  if (filter.cursor) {
7650
7835
  try {
@@ -7669,6 +7854,54 @@ function listTasks(filter = {}, db) {
7669
7854
  const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
7670
7855
  return rows.map(rowToTask);
7671
7856
  }
7857
+ function getTaskByFingerprint(fingerprint, db) {
7858
+ const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
7859
+ return tasks[0] ?? null;
7860
+ }
7861
+ function mergeTaskMetadata(current, next, fingerprint) {
7862
+ return {
7863
+ ...current,
7864
+ ...next ?? {},
7865
+ fingerprint
7866
+ };
7867
+ }
7868
+ function upsertTaskByFingerprint(input, db) {
7869
+ const d = db || getDatabase();
7870
+ const fingerprint = input.fingerprint.trim();
7871
+ if (!fingerprint)
7872
+ throw new Error("fingerprint is required");
7873
+ const existing = getTaskByFingerprint(fingerprint, d);
7874
+ const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
7875
+ if (!existing) {
7876
+ const task2 = createTask({ ...input, metadata }, d);
7877
+ return { task: task2, created: true };
7878
+ }
7879
+ const task = updateTask(existing.id, {
7880
+ version: existing.version,
7881
+ title: input.title,
7882
+ description: input.description,
7883
+ status: input.status,
7884
+ priority: input.priority,
7885
+ project_id: input.project_id,
7886
+ assigned_to: input.assigned_to,
7887
+ working_dir: input.working_dir,
7888
+ plan_id: input.plan_id,
7889
+ task_list_id: input.task_list_id,
7890
+ tags: input.tags,
7891
+ metadata,
7892
+ due_at: input.due_at,
7893
+ estimated_minutes: input.estimated_minutes,
7894
+ sla_minutes: input.sla_minutes,
7895
+ confidence: input.confidence,
7896
+ retry_count: input.retry_count,
7897
+ max_retries: input.max_retries,
7898
+ retry_after: input.retry_after,
7899
+ requires_approval: input.requires_approval,
7900
+ recurrence_rule: input.recurrence_rule,
7901
+ task_type: input.task_type
7902
+ }, d);
7903
+ return { task, created: false };
7904
+ }
7672
7905
  function countTasks(filter = {}, db) {
7673
7906
  const d = db || getDatabase();
7674
7907
  const conditions = [];
@@ -7732,6 +7965,7 @@ function countTasks(filter = {}, db) {
7732
7965
  conditions.push("task_list_id = ?");
7733
7966
  params.push(filter.task_list_id);
7734
7967
  }
7968
+ addMetadataConditions(filter.metadata, conditions, params);
7735
7969
  if (!filter.include_archived) {
7736
7970
  conditions.push("archived_at IS NULL");
7737
7971
  }
@@ -7782,6 +8016,10 @@ function updateTask(id, input, db) {
7782
8016
  sets.push("assigned_to = ?");
7783
8017
  params.push(input.assigned_to);
7784
8018
  }
8019
+ if (input.working_dir !== undefined) {
8020
+ sets.push("working_dir = ?");
8021
+ params.push(input.working_dir);
8022
+ }
7785
8023
  if (input.tags !== undefined) {
7786
8024
  sets.push("tags = ?");
7787
8025
  params.push(JSON.stringify(input.tags));
@@ -7870,6 +8108,8 @@ function updateTask(id, input, db) {
7870
8108
  logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
7871
8109
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
7872
8110
  logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
8111
+ if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
8112
+ logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
7873
8113
  if (input.approved_by !== undefined)
7874
8114
  logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
7875
8115
  const updatedTask = {
@@ -7905,6 +8145,10 @@ function updateTask(id, input, db) {
7905
8145
  if (input.approved_by !== undefined) {
7906
8146
  emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
7907
8147
  }
8148
+ const updatePayload = taskEventData(updatedTask);
8149
+ dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
8150
+ emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
8151
+ emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
7908
8152
  return updatedTask;
7909
8153
  }
7910
8154
  function deleteTask(id, db) {
@@ -10903,17 +11147,22 @@ var init_task_commits = __esm(() => {
10903
11147
  var exports_task_runs = {};
10904
11148
  __export(exports_task_runs, {
10905
11149
  verifyTaskRunArtifacts: () => verifyTaskRunArtifacts,
11150
+ summarizeTaskRun: () => summarizeTaskRun,
10906
11151
  startTaskRun: () => startTaskRun,
10907
11152
  resolveTaskRunId: () => resolveTaskRunId,
10908
11153
  redactEvidenceText: () => redactEvidenceText,
10909
11154
  listTaskRuns: () => listTaskRuns,
10910
11155
  getTaskRunLedger: () => getTaskRunLedger,
10911
11156
  getTaskRun: () => getTaskRun,
11157
+ finishTaskRunTransaction: () => finishTaskRunTransaction,
10912
11158
  finishTaskRun: () => finishTaskRun,
11159
+ findTaskRunByTransactionKey: () => findTaskRunByTransactionKey,
11160
+ beginTaskRunTransaction: () => beginTaskRunTransaction,
10913
11161
  addTaskRunFile: () => addTaskRunFile,
10914
11162
  addTaskRunEvent: () => addTaskRunEvent,
10915
11163
  addTaskRunCommand: () => addTaskRunCommand,
10916
- addTaskRunArtifact: () => addTaskRunArtifact
11164
+ addTaskRunArtifact: () => addTaskRunArtifact,
11165
+ LOOP_RUN_TRANSACTION_SCHEMA_VERSION: () => LOOP_RUN_TRANSACTION_SCHEMA_VERSION
10917
11166
  });
10918
11167
  function parseObject(value) {
10919
11168
  if (!value)
@@ -10937,6 +11186,72 @@ function rowToArtifact(row) {
10937
11186
  function getRunRow(runId, db) {
10938
11187
  return db.query("SELECT * FROM task_runs WHERE id = ?").get(runId);
10939
11188
  }
11189
+ function normalizeTransactionKey(input) {
11190
+ const key = (input.key || input.loop_run_id || input.loop_id || "").trim();
11191
+ if (!key)
11192
+ throw new Error("idempotent run transactions require --key, --loop-run-id, or --loop-id");
11193
+ return key.toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 240);
11194
+ }
11195
+ function loopTransactionMetadata(record) {
11196
+ const value = record.metadata["loop_transaction"];
11197
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
11198
+ }
11199
+ function runKey(record) {
11200
+ const tx = loopTransactionMetadata(record);
11201
+ const key = tx["idempotency_key"] ?? record.metadata["idempotency_key"];
11202
+ return typeof key === "string" ? key : null;
11203
+ }
11204
+ function loopId(record) {
11205
+ const tx = loopTransactionMetadata(record);
11206
+ const value = tx["loop_id"] ?? record.metadata["loop_id"];
11207
+ return typeof value === "string" ? value : null;
11208
+ }
11209
+ function loopRunId(record) {
11210
+ const tx = loopTransactionMetadata(record);
11211
+ const value = tx["loop_run_id"] ?? record.metadata["loop_run_id"];
11212
+ return typeof value === "string" ? value : null;
11213
+ }
11214
+ function getTaskRunTransactionByKey(key, taskId, db) {
11215
+ if (taskId) {
11216
+ return db.query("SELECT * FROM task_run_transactions WHERE task_id = ? AND key = ?").get(taskId, key);
11217
+ }
11218
+ const rows = db.query("SELECT * FROM task_run_transactions WHERE key = ? ORDER BY created_at DESC LIMIT 2").all(key);
11219
+ if (rows.length > 1)
11220
+ throw new Error(`Run transaction key is ambiguous across tasks: ${key}. Pass task_id.`);
11221
+ return rows[0] ?? null;
11222
+ }
11223
+ function summarizeTaskRun(run) {
11224
+ return {
11225
+ id: run.id,
11226
+ task_id: run.task_id,
11227
+ agent_id: run.agent_id,
11228
+ title: run.title,
11229
+ status: run.status,
11230
+ summary: run.summary,
11231
+ idempotency_key: runKey(run),
11232
+ loop_id: loopId(run),
11233
+ loop_run_id: loopRunId(run),
11234
+ metadata_keys: Object.keys(run.metadata).sort(),
11235
+ started_at: run.started_at,
11236
+ completed_at: run.completed_at,
11237
+ updated_at: run.updated_at
11238
+ };
11239
+ }
11240
+ function findTaskRunByTransactionKey(key, taskId, db) {
11241
+ const d = db || getDatabase();
11242
+ const normalized = normalizeTransactionKey({ key });
11243
+ const transaction = getTaskRunTransactionByKey(normalized, taskId, d);
11244
+ if (transaction?.run_id)
11245
+ return getTaskRun(transaction.run_id, d);
11246
+ return null;
11247
+ }
11248
+ function loopRunCommands(run, key) {
11249
+ return [
11250
+ run ? `todos runs show ${run.id.slice(0, 8)}` : "todos runs list",
11251
+ run ? `todos findings list --task ${run.task_id.slice(0, 8)} --json` : "todos findings list --json",
11252
+ `todos runs begin <task-id> --key ${key} --apply --json`
11253
+ ];
11254
+ }
10940
11255
  function resolveTaskRunId(idOrPrefix, db) {
10941
11256
  const d = db || getDatabase();
10942
11257
  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 +11270,7 @@ function startTaskRun(input, db) {
10955
11270
  const d = db || getDatabase();
10956
11271
  if (!getTask(input.task_id, d))
10957
11272
  throw new TaskNotFoundError(input.task_id);
10958
- const id = uuid();
11273
+ const id = input.id ?? uuid();
10959
11274
  const timestamp = input.started_at || now();
10960
11275
  if (input.claim && input.agent_id) {
10961
11276
  startTask(input.task_id, input.agent_id, d);
@@ -10993,6 +11308,97 @@ function startTaskRun(input, db) {
10993
11308
  emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
10994
11309
  return run;
10995
11310
  }
11311
+ function beginTaskRunTransaction(input, db) {
11312
+ const d = db || getDatabase();
11313
+ if (!getTask(input.task_id, d))
11314
+ throw new TaskNotFoundError(input.task_id);
11315
+ const timestamp = input.started_at || now();
11316
+ const key = normalizeTransactionKey(input);
11317
+ const existing = findTaskRunByTransactionKey(key, input.task_id, d);
11318
+ const dryRun = !input.apply;
11319
+ if (existing) {
11320
+ return {
11321
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11322
+ local_only: true,
11323
+ dry_run: dryRun,
11324
+ processed_at: timestamp,
11325
+ action: "matched",
11326
+ key,
11327
+ run: summarizeTaskRun(existing),
11328
+ warnings: existing.status === "running" ? [] : [`matched ${existing.status} run`],
11329
+ commands: loopRunCommands(existing, key)
11330
+ };
11331
+ }
11332
+ if (dryRun) {
11333
+ return {
11334
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11335
+ local_only: true,
11336
+ dry_run: true,
11337
+ processed_at: timestamp,
11338
+ action: "preview",
11339
+ key,
11340
+ run: null,
11341
+ warnings: [],
11342
+ commands: loopRunCommands(null, key)
11343
+ };
11344
+ }
11345
+ const metadata = redactValue({
11346
+ ...input.metadata || {},
11347
+ loop_transaction: {
11348
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11349
+ idempotency_key: key,
11350
+ loop_id: input.loop_id ?? null,
11351
+ loop_run_id: input.loop_run_id ?? null,
11352
+ first_seen_at: timestamp
11353
+ },
11354
+ idempotency_key: key
11355
+ });
11356
+ const created = d.transaction(() => {
11357
+ d.run(`INSERT OR IGNORE INTO task_run_transactions (
11358
+ id, task_id, run_id, key, loop_id, loop_run_id, metadata, created_at, updated_at
11359
+ ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?)`, [
11360
+ uuid(),
11361
+ input.task_id,
11362
+ key,
11363
+ input.loop_id ?? null,
11364
+ input.loop_run_id ?? null,
11365
+ JSON.stringify(metadata),
11366
+ timestamp,
11367
+ timestamp
11368
+ ]);
11369
+ const transaction = getTaskRunTransactionByKey(key, input.task_id, d);
11370
+ if (!transaction)
11371
+ throw new Error(`Could not create run transaction for key: ${key}`);
11372
+ if (transaction.run_id) {
11373
+ const existingRun = getTaskRun(transaction.run_id, d);
11374
+ if (existingRun)
11375
+ return { run: existingRun, action: "matched" };
11376
+ }
11377
+ const run = startTaskRun({
11378
+ id: uuid(),
11379
+ task_id: input.task_id,
11380
+ agent_id: input.agent_id,
11381
+ title: input.title,
11382
+ summary: input.summary,
11383
+ metadata,
11384
+ claim: input.claim,
11385
+ started_at: timestamp
11386
+ }, d);
11387
+ 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]);
11388
+ return { run, action: "created" };
11389
+ })();
11390
+ return {
11391
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11392
+ local_only: true,
11393
+ dry_run: false,
11394
+ processed_at: timestamp,
11395
+ action: created.action,
11396
+ key,
11397
+ run: summarizeTaskRun(created.run),
11398
+ warnings: [],
11399
+ commands: loopRunCommands(created.run, key)
11400
+ };
11401
+ }
10996
11402
  function addTaskRunEvent(input, db) {
10997
11403
  const d = db || getDatabase();
10998
11404
  const runId = resolveTaskRunId(input.run_id, d);
@@ -11172,6 +11578,66 @@ function finishTaskRun(input, db) {
11172
11578
  });
11173
11579
  return updated;
11174
11580
  }
11581
+ function finishTaskRunTransaction(input, db) {
11582
+ const d = db || getDatabase();
11583
+ const timestamp = input.completed_at || now();
11584
+ const status = input.status || "completed";
11585
+ const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
11586
+ const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
11587
+ if (!run) {
11588
+ throw new Error(input.run_id ? `Run not found: ${input.run_id}` : "runs finish requires a run id or --key");
11589
+ }
11590
+ if (input.task_id && run.task_id !== input.task_id) {
11591
+ throw new Error(`Run ${run.id} belongs to task ${run.task_id}, not ${input.task_id}`);
11592
+ }
11593
+ const resolvedKey = key || runKey(run) || run.id;
11594
+ const dryRun = input.apply === false;
11595
+ if (run.status !== "running") {
11596
+ const conflict = run.status !== status;
11597
+ return {
11598
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11599
+ local_only: true,
11600
+ dry_run: dryRun,
11601
+ processed_at: timestamp,
11602
+ action: conflict ? "conflict" : "matched",
11603
+ key: resolvedKey,
11604
+ run: summarizeTaskRun(run),
11605
+ warnings: conflict ? [`run is already ${run.status}; requested ${status}`] : [],
11606
+ commands: loopRunCommands(run, resolvedKey)
11607
+ };
11608
+ }
11609
+ if (dryRun) {
11610
+ return {
11611
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11612
+ local_only: true,
11613
+ dry_run: true,
11614
+ processed_at: timestamp,
11615
+ action: "preview",
11616
+ key: resolvedKey,
11617
+ run: summarizeTaskRun({ ...run, status, summary: input.summary ?? run.summary, completed_at: timestamp, updated_at: timestamp }),
11618
+ warnings: [],
11619
+ commands: loopRunCommands(run, resolvedKey)
11620
+ };
11621
+ }
11622
+ const finished = finishTaskRun({
11623
+ run_id: run.id,
11624
+ status,
11625
+ summary: input.summary,
11626
+ agent_id: input.agent_id,
11627
+ completed_at: timestamp
11628
+ }, d);
11629
+ return {
11630
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11631
+ local_only: true,
11632
+ dry_run: false,
11633
+ processed_at: timestamp,
11634
+ action: "finished",
11635
+ key: resolvedKey,
11636
+ run: summarizeTaskRun(finished),
11637
+ warnings: [],
11638
+ commands: loopRunCommands(finished, resolvedKey)
11639
+ };
11640
+ }
11175
11641
  function listTaskRuns(taskId, db) {
11176
11642
  const d = db || getDatabase();
11177
11643
  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 +11655,7 @@ function getTaskRunLedger(runId, db) {
11189
11655
  const files = d.query("SELECT * FROM task_files WHERE task_id = ? ORDER BY updated_at DESC, path").all(run.task_id);
11190
11656
  return { run, events, commands, artifacts, files };
11191
11657
  }
11658
+ var LOOP_RUN_TRANSACTION_SCHEMA_VERSION = "todos.loop_run_transaction.v1";
11192
11659
  var init_task_runs = __esm(() => {
11193
11660
  init_artifact_store();
11194
11661
  init_event_hooks();
@@ -11577,6 +12044,7 @@ var init_calendar = __esm(() => {
11577
12044
  var exports_tasks = {};
11578
12045
  __export(exports_tasks, {
11579
12046
  watchTask: () => watchTask,
12047
+ upsertTaskByFingerprint: () => upsertTaskByFingerprint,
11580
12048
  updateTaskBoard: () => updateTaskBoard,
11581
12049
  updateTask: () => updateTask,
11582
12050
  unwatchTask: () => unwatchTask,
@@ -11620,6 +12088,7 @@ __export(exports_tasks, {
11620
12088
  getTaskGraph: () => getTaskGraph,
11621
12089
  getTaskDependents: () => getTaskDependents,
11622
12090
  getTaskDependencies: () => getTaskDependencies,
12091
+ getTaskByFingerprint: () => getTaskByFingerprint,
11623
12092
  getTaskBoard: () => getTaskBoard,
11624
12093
  getTask: () => getTask,
11625
12094
  getStatus: () => getStatus,
@@ -11860,6 +12329,62 @@ function parsePriority(value) {
11860
12329
  }
11861
12330
  return value;
11862
12331
  }
12332
+ function parseJsonObject3(value, flag) {
12333
+ if (!value)
12334
+ return;
12335
+ let parsed;
12336
+ try {
12337
+ parsed = JSON.parse(value);
12338
+ } catch (error) {
12339
+ console.error(chalk2.red(`${flag} must be valid JSON: ${error instanceof Error ? error.message : String(error)}`));
12340
+ process.exit(1);
12341
+ }
12342
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
12343
+ console.error(chalk2.red(`${flag} must be a JSON object`));
12344
+ process.exit(1);
12345
+ }
12346
+ return parsed;
12347
+ }
12348
+ function parseJsonValue(value) {
12349
+ if (value === undefined)
12350
+ return;
12351
+ try {
12352
+ return JSON.parse(value);
12353
+ } catch {
12354
+ return value;
12355
+ }
12356
+ }
12357
+ function parseTags(value) {
12358
+ return value ? value.split(",").map((tag) => tag.trim()).filter(Boolean) : undefined;
12359
+ }
12360
+ function buildExpectationMetadata(opts) {
12361
+ const metadata = parseJsonObject3(opts["metadataJson"], "--metadata-json") ?? {};
12362
+ const expectationId = opts["expectationId"];
12363
+ const expectationFingerprint = opts["expectationFingerprint"];
12364
+ const evidencePaths = opts["evidencePaths"];
12365
+ const originLoopId = opts["originLoopId"];
12366
+ const originRunId = opts["originRunId"];
12367
+ const expected = opts["expected"];
12368
+ const observed = opts["observed"];
12369
+ const acceptance = opts["acceptance"];
12370
+ if (expectationId !== undefined)
12371
+ metadata["expectation_id"] = expectationId;
12372
+ if (expectationFingerprint !== undefined)
12373
+ metadata["expectation_fingerprint"] = expectationFingerprint;
12374
+ if (evidencePaths !== undefined)
12375
+ metadata["evidence_paths"] = String(evidencePaths).split(",").map((path) => path.trim()).filter(Boolean);
12376
+ if (originLoopId !== undefined)
12377
+ metadata["origin_loop_id"] = originLoopId;
12378
+ if (originRunId !== undefined)
12379
+ metadata["origin_run_id"] = originRunId;
12380
+ if (expected !== undefined)
12381
+ metadata["expected"] = parseJsonValue(String(expected));
12382
+ if (observed !== undefined)
12383
+ metadata["observed"] = parseJsonValue(String(observed));
12384
+ if (acceptance !== undefined)
12385
+ metadata["acceptance"] = parseJsonValue(String(acceptance));
12386
+ return metadata;
12387
+ }
11863
12388
  function registerTaskCommands(program2) {
11864
12389
  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
12390
  const globalOpts = program2.opts();
@@ -11876,7 +12401,7 @@ function registerTaskCommands(program2) {
11876
12401
  }
11877
12402
  return id;
11878
12403
  })() : undefined;
11879
- const task = createTask({
12404
+ const task2 = createTask({
11880
12405
  title,
11881
12406
  description: opts.description,
11882
12407
  priority: parsePriority(opts.priority),
@@ -11898,10 +12423,53 @@ function registerTaskCommands(program2) {
11898
12423
  reason: opts.reason
11899
12424
  });
11900
12425
  if (globalOpts.json) {
11901
- output(task, true);
12426
+ output(task2, true);
11902
12427
  } else {
11903
12428
  console.log(chalk2.green("Task created:"));
11904
- console.log(formatTaskLine(task));
12429
+ console.log(formatTaskLine(task2));
12430
+ }
12431
+ });
12432
+ const task = program2.command("task").description("Task subcommands for deterministic automation");
12433
+ 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) => {
12434
+ const globalOpts = program2.opts();
12435
+ opts.tags = opts.tags || opts.tag;
12436
+ opts.list = opts.list || opts.taskList;
12437
+ const explicitProject = opts.project || globalOpts.project;
12438
+ const projectId = explicitProject ? resolveProjectIdOrSlug(explicitProject) : autoProject(globalOpts);
12439
+ const taskListId = opts.list ? (() => {
12440
+ const db = getDatabase();
12441
+ const id = resolvePartialId(db, "task_lists", opts.list);
12442
+ if (!id) {
12443
+ console.error(chalk2.red(`Could not resolve task list ID: ${opts.list}`));
12444
+ process.exit(1);
12445
+ }
12446
+ return id;
12447
+ })() : undefined;
12448
+ let result;
12449
+ try {
12450
+ result = upsertTaskByFingerprint({
12451
+ fingerprint: opts.fingerprint,
12452
+ title: opts.title,
12453
+ description: opts.description,
12454
+ priority: parsePriority(opts.priority),
12455
+ status: opts.status ? normalizeStatus(opts.status) : undefined,
12456
+ task_list_id: taskListId,
12457
+ tags: parseTags(opts.tags),
12458
+ metadata: buildExpectationMetadata(opts),
12459
+ working_dir: opts.workingDir ? resolve8(opts.workingDir) : process.cwd(),
12460
+ project_id: projectId,
12461
+ assigned_to: opts.assign,
12462
+ agent_id: globalOpts.agent,
12463
+ session_id: globalOpts.session
12464
+ });
12465
+ } catch (e) {
12466
+ handleError(e);
12467
+ }
12468
+ if (globalOpts.json) {
12469
+ output(result, true);
12470
+ } else {
12471
+ console.log(chalk2.green(result.created ? "Task created:" : "Task updated:"));
12472
+ console.log(formatTaskLine(result.task));
11905
12473
  }
11906
12474
  });
11907
12475
  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 +12627,89 @@ function registerTaskCommands(program2) {
12059
12627
  program2.command("show <id>").description("Show full task details").action((id) => {
12060
12628
  const globalOpts = program2.opts();
12061
12629
  const resolvedId = resolveTaskId(id);
12062
- const task = getTaskWithRelations(resolvedId);
12063
- if (!task) {
12630
+ const task2 = getTaskWithRelations(resolvedId);
12631
+ if (!task2) {
12064
12632
  console.error(chalk2.red(`Task not found: ${id}`));
12065
12633
  process.exit(1);
12066
12634
  }
12067
12635
  if (globalOpts.json) {
12068
- output(task, true);
12636
+ output(task2, true);
12069
12637
  return;
12070
12638
  }
12071
12639
  console.log(chalk2.bold(`Task Details:
12072
12640
  `));
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");
12641
+ console.log(` ${chalk2.dim("ID:")} ${task2.id}`);
12642
+ console.log(` ${chalk2.dim("Title:")} ${task2.title}`);
12643
+ console.log(` ${chalk2.dim("Status:")} ${(statusColors[task2.status] || chalk2.white)(task2.status)}`);
12644
+ console.log(` ${chalk2.dim("Priority:")} ${(priorityColors[task2.priority] || chalk2.white)(task2.priority)}`);
12645
+ if (task2.description)
12646
+ console.log(` ${chalk2.dim("Desc:")} ${task2.description}`);
12647
+ if (task2.assigned_to)
12648
+ console.log(` ${chalk2.dim("Assigned:")} ${task2.assigned_to}`);
12649
+ if (task2.agent_id)
12650
+ console.log(` ${chalk2.dim("Agent:")} ${task2.agent_id}`);
12651
+ if (task2.session_id)
12652
+ console.log(` ${chalk2.dim("Session:")} ${task2.session_id}`);
12653
+ if (task2.locked_by)
12654
+ console.log(` ${chalk2.dim("Locked:")} ${task2.locked_by} (at ${task2.locked_at})`);
12655
+ if (task2.requires_approval) {
12656
+ const approvalStatus = task2.approved_by ? chalk2.green(`approved by ${task2.approved_by}`) : chalk2.yellow("pending approval");
12089
12657
  console.log(` ${chalk2.dim("Approval:")} ${approvalStatus}`);
12090
12658
  }
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);
12659
+ if (task2.estimated_minutes)
12660
+ console.log(` ${chalk2.dim("Estimate:")} ${task2.estimated_minutes} minutes`);
12661
+ if (task2.sla_minutes)
12662
+ console.log(` ${chalk2.dim("SLA:")} ${task2.sla_minutes} minutes`);
12663
+ if (task2.due_at)
12664
+ console.log(` ${chalk2.dim("Due:")} ${task2.due_at}`);
12665
+ if (task2.recurrence_rule)
12666
+ console.log(` ${chalk2.dim("Repeats:")} ${task2.recurrence_rule}`);
12667
+ if (task2.project_id)
12668
+ console.log(` ${chalk2.dim("Project:")} ${task2.project_id}`);
12669
+ if (task2.plan_id)
12670
+ console.log(` ${chalk2.dim("Plan:")} ${task2.plan_id}`);
12671
+ if (task2.working_dir)
12672
+ console.log(` ${chalk2.dim("WorkDir:")} ${task2.working_dir}`);
12673
+ if (task2.parent)
12674
+ console.log(` ${chalk2.dim("Parent:")} ${task2.parent.id.slice(0, 8)} | ${task2.parent.title}`);
12675
+ if (task2.tags.length > 0)
12676
+ console.log(` ${chalk2.dim("Tags:")} ${task2.tags.join(", ")}`);
12677
+ console.log(` ${chalk2.dim("Version:")} ${task2.version}`);
12678
+ console.log(` ${chalk2.dim("Created:")} ${task2.created_at}`);
12679
+ if (task2.started_at)
12680
+ console.log(` ${chalk2.dim("Started:")} ${task2.started_at}`);
12681
+ if (task2.completed_at) {
12682
+ console.log(` ${chalk2.dim("Done:")} ${task2.completed_at}`);
12683
+ if (task2.started_at) {
12684
+ const dur = Math.round((new Date(task2.completed_at).getTime() - new Date(task2.started_at).getTime()) / 60000);
12117
12685
  console.log(` ${chalk2.dim("Duration:")} ${dur}m`);
12118
12686
  }
12119
12687
  }
12120
- if (task.subtasks.length > 0) {
12688
+ if (task2.subtasks.length > 0) {
12121
12689
  console.log(chalk2.bold(`
12122
- Subtasks (${task.subtasks.length}):`));
12123
- for (const st of task.subtasks) {
12690
+ Subtasks (${task2.subtasks.length}):`));
12691
+ for (const st of task2.subtasks) {
12124
12692
  console.log(` ${formatTaskLine(st)}`);
12125
12693
  }
12126
12694
  }
12127
- if (task.dependencies.length > 0) {
12695
+ if (task2.dependencies.length > 0) {
12128
12696
  console.log(chalk2.bold(`
12129
- Depends on (${task.dependencies.length}):`));
12130
- for (const dep of task.dependencies) {
12697
+ Depends on (${task2.dependencies.length}):`));
12698
+ for (const dep of task2.dependencies) {
12131
12699
  console.log(` ${formatTaskLine(dep)}`);
12132
12700
  }
12133
12701
  }
12134
- if (task.blocked_by.length > 0) {
12702
+ if (task2.blocked_by.length > 0) {
12135
12703
  console.log(chalk2.bold(`
12136
- Blocks (${task.blocked_by.length}):`));
12137
- for (const b of task.blocked_by) {
12704
+ Blocks (${task2.blocked_by.length}):`));
12705
+ for (const b of task2.blocked_by) {
12138
12706
  console.log(` ${formatTaskLine(b)}`);
12139
12707
  }
12140
12708
  }
12141
- if (task.comments.length > 0) {
12709
+ if (task2.comments.length > 0) {
12142
12710
  console.log(chalk2.bold(`
12143
- Comments (${task.comments.length}):`));
12144
- for (const c of task.comments) {
12711
+ Comments (${task2.comments.length}):`));
12712
+ for (const c of task2.comments) {
12145
12713
  const agent = c.agent_id ? chalk2.cyan(`[${c.agent_id}] `) : "";
12146
12714
  console.log(` ${agent}${chalk2.dim(c.created_at)}: ${c.content}`);
12147
12715
  }
@@ -12160,8 +12728,8 @@ function registerTaskCommands(program2) {
12160
12728
  console.error(chalk2.red("No task ID given and no active task found. Pass an ID or use --agent."));
12161
12729
  process.exit(1);
12162
12730
  }
12163
- const task = getTaskWithRelations(resolvedId);
12164
- if (!task) {
12731
+ const task2 = getTaskWithRelations(resolvedId);
12732
+ if (!task2) {
12165
12733
  console.error(chalk2.red(`Task not found: ${id || resolvedId}`));
12166
12734
  process.exit(1);
12167
12735
  }
@@ -12169,55 +12737,55 @@ function registerTaskCommands(program2) {
12169
12737
  const { listTaskFiles: listTaskFiles2 } = await Promise.resolve().then(() => (init_task_files(), exports_task_files));
12170
12738
  const { getTaskCommits: getTaskCommits2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
12171
12739
  try {
12172
- task.files = listTaskFiles2(task.id);
12740
+ task2.files = listTaskFiles2(task2.id);
12173
12741
  } catch (e) {
12174
12742
  console.error(chalk2.dim(`Warning: could not load task files: ${e instanceof Error ? e.message : String(e)}`));
12175
12743
  }
12176
12744
  try {
12177
- task.commits = getTaskCommits2(task.id);
12745
+ task2.commits = getTaskCommits2(task2.id);
12178
12746
  } catch (e) {
12179
12747
  console.error(chalk2.dim(`Warning: could not load task commits: ${e instanceof Error ? e.message : String(e)}`));
12180
12748
  }
12181
- output(task, true);
12749
+ output(task2, true);
12182
12750
  return;
12183
12751
  }
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;
12752
+ const sid = task2.short_id || task2.id.slice(0, 8);
12753
+ const statusColor = statusColors[task2.status] || chalk2.white;
12754
+ const prioColor = priorityColors[task2.priority] || chalk2.white;
12187
12755
  console.log(chalk2.bold(`
12188
- ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${task.title}
12756
+ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${task2.title}
12189
12757
  `));
12190
- if (task.description) {
12758
+ if (task2.description) {
12191
12759
  console.log(chalk2.dim("Description:"));
12192
- console.log(` ${task.description}
12760
+ console.log(` ${task2.description}
12193
12761
  `);
12194
12762
  }
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);
12763
+ if (task2.assigned_to)
12764
+ console.log(` ${chalk2.dim("Assigned:")} ${task2.assigned_to}`);
12765
+ if (task2.locked_by)
12766
+ console.log(` ${chalk2.dim("Locked by:")} ${task2.locked_by}`);
12767
+ if (task2.project_id)
12768
+ console.log(` ${chalk2.dim("Project:")} ${task2.project_id}`);
12769
+ if (task2.plan_id)
12770
+ console.log(` ${chalk2.dim("Plan:")} ${task2.plan_id}`);
12771
+ if (task2.started_at)
12772
+ console.log(` ${chalk2.dim("Started:")} ${task2.started_at}`);
12773
+ if (task2.completed_at) {
12774
+ console.log(` ${chalk2.dim("Completed:")} ${task2.completed_at}`);
12775
+ if (task2.started_at) {
12776
+ const dur = Math.round((new Date(task2.completed_at).getTime() - new Date(task2.started_at).getTime()) / 60000);
12209
12777
  console.log(` ${chalk2.dim("Duration:")} ${dur}m`);
12210
12778
  }
12211
12779
  }
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) {
12780
+ if (task2.estimated_minutes)
12781
+ console.log(` ${chalk2.dim("Estimate:")} ${task2.estimated_minutes}m`);
12782
+ if (task2.tags.length > 0)
12783
+ console.log(` ${chalk2.dim("Tags:")} ${task2.tags.join(", ")}`);
12784
+ const unfinishedDeps = task2.dependencies.filter((d) => d.status !== "completed" && d.status !== "cancelled");
12785
+ if (task2.dependencies.length > 0) {
12218
12786
  console.log(chalk2.bold(`
12219
- Depends on (${task.dependencies.length}):`));
12220
- for (const dep of task.dependencies) {
12787
+ Depends on (${task2.dependencies.length}):`));
12788
+ for (const dep of task2.dependencies) {
12221
12789
  const blocked = dep.status !== "completed" && dep.status !== "cancelled";
12222
12790
  const icon = blocked ? chalk2.red("\u2717") : chalk2.green("\u2713");
12223
12791
  console.log(` ${icon} ${formatTaskLine(dep)}`);
@@ -12227,21 +12795,21 @@ ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${ta
12227
12795
  console.log(chalk2.red(`
12228
12796
  BLOCKED by ${unfinishedDeps.length} unfinished dep(s)`));
12229
12797
  }
12230
- if (task.blocked_by.length > 0) {
12798
+ if (task2.blocked_by.length > 0) {
12231
12799
  console.log(chalk2.bold(`
12232
- Blocks (${task.blocked_by.length}):`));
12233
- for (const b of task.blocked_by)
12800
+ Blocks (${task2.blocked_by.length}):`));
12801
+ for (const b of task2.blocked_by)
12234
12802
  console.log(` ${formatTaskLine(b)}`);
12235
12803
  }
12236
- if (task.subtasks.length > 0) {
12804
+ if (task2.subtasks.length > 0) {
12237
12805
  console.log(chalk2.bold(`
12238
- Subtasks (${task.subtasks.length}):`));
12239
- for (const st of task.subtasks)
12806
+ Subtasks (${task2.subtasks.length}):`));
12807
+ for (const st of task2.subtasks)
12240
12808
  console.log(` ${formatTaskLine(st)}`);
12241
12809
  }
12242
12810
  try {
12243
12811
  const { listTaskFiles: listTaskFiles2 } = await Promise.resolve().then(() => (init_task_files(), exports_task_files));
12244
- const files = listTaskFiles2(task.id);
12812
+ const files = listTaskFiles2(task2.id);
12245
12813
  if (files.length > 0) {
12246
12814
  console.log(chalk2.bold(`
12247
12815
  Files (${files.length}):`));
@@ -12253,7 +12821,7 @@ ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${ta
12253
12821
  }
12254
12822
  try {
12255
12823
  const { getTaskCommits: getTaskCommits2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
12256
- const commits = getTaskCommits2(task.id);
12824
+ const commits = getTaskCommits2(task2.id);
12257
12825
  if (commits.length > 0) {
12258
12826
  console.log(chalk2.bold(`
12259
12827
  Commits (${commits.length}):`));
@@ -12263,19 +12831,19 @@ ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${ta
12263
12831
  } catch (e) {
12264
12832
  console.error(chalk2.dim(`Warning: could not load task commits: ${e instanceof Error ? e.message : String(e)}`));
12265
12833
  }
12266
- if (task.comments.length > 0) {
12834
+ if (task2.comments.length > 0) {
12267
12835
  console.log(chalk2.bold(`
12268
- Comments (${task.comments.length}):`));
12269
- for (const c of task.comments) {
12836
+ Comments (${task2.comments.length}):`));
12837
+ for (const c of task2.comments) {
12270
12838
  const agent = c.agent_id ? chalk2.cyan(`[${c.agent_id}] `) : "";
12271
12839
  console.log(` ${agent}${chalk2.dim(c.created_at)}: ${c.content}`);
12272
12840
  }
12273
12841
  }
12274
- if (task.checklist && task.checklist.length > 0) {
12275
- const done = task.checklist.filter((c) => c.checked).length;
12842
+ if (task2.checklist && task2.checklist.length > 0) {
12843
+ const done = task2.checklist.filter((c) => c.checked).length;
12276
12844
  console.log(chalk2.bold(`
12277
- Checklist (${done}/${task.checklist.length}):`));
12278
- for (const item of task.checklist) {
12845
+ Checklist (${done}/${task2.checklist.length}):`));
12846
+ for (const item of task2.checklist) {
12279
12847
  const icon = item.checked ? chalk2.green("\u2611") : chalk2.dim("\u2610");
12280
12848
  console.log(` ${icon} ${item.text || item.title}`);
12281
12849
  }
@@ -12328,9 +12896,9 @@ ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${ta
12328
12896
  return resolved;
12329
12897
  })() : undefined;
12330
12898
  const planId = opts.plan ? resolvePlanId(opts.plan) : opts.clearPlan ? null : undefined;
12331
- let task;
12899
+ let task2;
12332
12900
  try {
12333
- task = updateTask(resolvedId, {
12901
+ task2 = updateTask(resolvedId, {
12334
12902
  version: current.version,
12335
12903
  title: opts.title,
12336
12904
  description: opts.description,
@@ -12350,10 +12918,10 @@ ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${ta
12350
12918
  handleError(e);
12351
12919
  }
12352
12920
  if (globalOpts.json) {
12353
- output(task, true);
12921
+ output(task2, true);
12354
12922
  } else {
12355
12923
  console.log(chalk2.green("Task updated:"));
12356
- console.log(formatTaskLine(task));
12924
+ console.log(formatTaskLine(task2));
12357
12925
  }
12358
12926
  });
12359
12927
  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 +12931,37 @@ ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${ta
12363
12931
  const filesChanged = opts.filesChanged ? opts.filesChanged.split(",").map((s) => s.trim()) : undefined;
12364
12932
  const confidence = opts.confidence !== undefined ? parseFloat(opts.confidence) : undefined;
12365
12933
  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;
12934
+ let task2;
12367
12935
  try {
12368
- task = completeTask(resolvedId, globalOpts.agent, undefined, { ...evidence, confidence });
12936
+ task2 = completeTask(resolvedId, globalOpts.agent, undefined, { ...evidence, confidence });
12369
12937
  } catch (e) {
12370
12938
  handleError(e);
12371
12939
  }
12372
12940
  if (globalOpts.json) {
12373
- output(task, true);
12941
+ output(task2, true);
12374
12942
  } else {
12375
12943
  console.log(chalk2.green("Task completed:"));
12376
- console.log(formatTaskLine(task));
12944
+ console.log(formatTaskLine(task2));
12377
12945
  }
12378
12946
  });
12379
12947
  program2.command("approve <id>").description("Approve a task that requires approval").action((id) => {
12380
12948
  const globalOpts = program2.opts();
12381
12949
  const resolvedId = resolveTaskId(id);
12382
- const task = getTask(resolvedId);
12383
- if (!task) {
12950
+ const task2 = getTask(resolvedId);
12951
+ if (!task2) {
12384
12952
  console.error(chalk2.red(`Task not found: ${id}`));
12385
12953
  process.exit(1);
12386
12954
  }
12387
- if (!task.requires_approval) {
12955
+ if (!task2.requires_approval) {
12388
12956
  console.log(chalk2.yellow("This task does not require approval."));
12389
12957
  return;
12390
12958
  }
12391
- if (task.approved_by) {
12392
- console.log(chalk2.yellow(`Already approved by ${task.approved_by}.`));
12959
+ if (task2.approved_by) {
12960
+ console.log(chalk2.yellow(`Already approved by ${task2.approved_by}.`));
12393
12961
  return;
12394
12962
  }
12395
12963
  try {
12396
- const updated = updateTask(resolvedId, { approved_by: globalOpts.agent || "cli", version: task.version });
12964
+ const updated = updateTask(resolvedId, { approved_by: globalOpts.agent || "cli", version: task2.version });
12397
12965
  if (globalOpts.json) {
12398
12966
  output(updated, true);
12399
12967
  } else {
@@ -12408,17 +12976,17 @@ ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${ta
12408
12976
  const globalOpts = program2.opts();
12409
12977
  const agentId = globalOpts.agent || "cli";
12410
12978
  const resolvedId = resolveTaskId(id);
12411
- let task;
12979
+ let task2;
12412
12980
  try {
12413
- task = startTask(resolvedId, agentId);
12981
+ task2 = startTask(resolvedId, agentId);
12414
12982
  } catch (e) {
12415
12983
  handleError(e);
12416
12984
  }
12417
12985
  if (globalOpts.json) {
12418
- output(task, true);
12986
+ output(task2, true);
12419
12987
  } else {
12420
12988
  console.log(chalk2.green(`Task started by ${agentId}:`));
12421
- console.log(formatTaskLine(task));
12989
+ console.log(formatTaskLine(task2));
12422
12990
  }
12423
12991
  });
12424
12992
  program2.command("lock <id>").description("Acquire exclusive lock on a task").action((id) => {
@@ -13591,7 +14159,7 @@ function likePattern(query) {
13591
14159
  return null;
13592
14160
  return `%${trimmed}%`;
13593
14161
  }
13594
- function parseJsonObject3(value) {
14162
+ function parseJsonObject4(value) {
13595
14163
  if (!value)
13596
14164
  return {};
13597
14165
  if (typeof value === "object" && !Array.isArray(value))
@@ -13606,7 +14174,7 @@ function parseJsonObject3(value) {
13606
14174
  }
13607
14175
  }
13608
14176
  function rowToTaskRun(row) {
13609
- return { ...row, metadata: parseJsonObject3(row.metadata) };
14177
+ return { ...row, metadata: parseJsonObject4(row.metadata) };
13610
14178
  }
13611
14179
  function taskMatchesSavedFilters(task, filters, db) {
13612
14180
  if (filters.plan_id && task.plan_id !== filters.plan_id)
@@ -14969,7 +15537,7 @@ function packageSource(version) {
14969
15537
  function emptyCounts() {
14970
15538
  return Object.fromEntries(dataKeys.map((key) => [key, 0]));
14971
15539
  }
14972
- function parseJsonObject4(value) {
15540
+ function parseJsonObject5(value) {
14973
15541
  if (!value)
14974
15542
  return {};
14975
15543
  if (typeof value === "object" && !Array.isArray(value))
@@ -15015,34 +15583,34 @@ function rowToTask3(row) {
15015
15583
  return {
15016
15584
  ...row,
15017
15585
  tags: parseJsonArray2(row.tags),
15018
- metadata: parseJsonObject4(row.metadata),
15586
+ metadata: parseJsonObject5(row.metadata),
15019
15587
  requires_approval: Boolean(row.requires_approval)
15020
15588
  };
15021
15589
  }
15022
15590
  function rowToTaskList2(row) {
15023
- return { ...row, metadata: parseJsonObject4(row.metadata) };
15591
+ return { ...row, metadata: parseJsonObject5(row.metadata) };
15024
15592
  }
15025
15593
  function rowWithMetadata(row) {
15026
- return { ...row, metadata: parseJsonObject4(row.metadata) };
15594
+ return { ...row, metadata: parseJsonObject5(row.metadata) };
15027
15595
  }
15028
15596
  function rowToRunEvent(row) {
15029
- return { ...row, data: parseJsonObject4(row.data) };
15597
+ return { ...row, data: parseJsonObject5(row.data) };
15030
15598
  }
15031
15599
  function rowToCommit2(row) {
15032
15600
  return { ...row, files_changed: row.files_changed ? parseJsonArray2(row.files_changed) : null };
15033
15601
  }
15034
15602
  function rowToSavedView(row) {
15035
- return { ...row, filters: parseJsonObject4(row.filters) };
15603
+ return { ...row, filters: parseJsonObject5(row.filters) };
15036
15604
  }
15037
15605
  function rowToTaskBoard2(row) {
15038
15606
  return {
15039
15607
  ...row,
15040
15608
  lanes: parseJsonArray2(row.lanes),
15041
- filters: parseJsonObject4(row.filters)
15609
+ filters: parseJsonObject5(row.filters)
15042
15610
  };
15043
15611
  }
15044
15612
  function rowToCalendarItem2(row) {
15045
- return { ...row, metadata: parseJsonObject4(row.metadata) };
15613
+ return { ...row, metadata: parseJsonObject5(row.metadata) };
15046
15614
  }
15047
15615
  function bridgeStats(data) {
15048
15616
  return Object.fromEntries(dataKeys.map((key) => [key, data[key].length]));
@@ -18389,6 +18957,7 @@ var init_token_utils = __esm(() => {
18389
18957
  "add_task_run_event",
18390
18958
  "add_task_run_file",
18391
18959
  "acknowledge_handoff",
18960
+ "begin_task_run_transaction",
18392
18961
  "build_local_report",
18393
18962
  "cancel_agent_run_dispatch",
18394
18963
  "finish_task_run",
@@ -18436,6 +19005,7 @@ var init_token_utils = __esm(() => {
18436
19005
  "list_local_snapshots",
18437
19006
  "list_retrospectives",
18438
19007
  "list_risks",
19008
+ "list_task_findings",
18439
19009
  "list_task_runs",
18440
19010
  "list_verification_providers",
18441
19011
  "merge_duplicate_task",
@@ -18444,6 +19014,7 @@ var init_token_utils = __esm(() => {
18444
19014
  "remove_review_routing_rule",
18445
19015
  "restore_local_backup",
18446
19016
  "retry_agent_run_dispatch",
19017
+ "resolve_missing_task_findings",
18447
19018
  "resolve_mentions",
18448
19019
  "run_next_agent_dispatch",
18449
19020
  "search_knowledge_records",
@@ -18486,9 +19057,17 @@ var init_token_utils = __esm(() => {
18486
19057
  "unlock_file",
18487
19058
  "unwatch_task",
18488
19059
  "update_comment",
19060
+ "upsert_task_finding",
18489
19061
  "update_risk",
18490
19062
  "watch_task"
18491
19063
  ],
19064
+ loops: [
19065
+ "begin_task_run_transaction",
19066
+ "finish_task_run",
19067
+ "list_task_findings",
19068
+ "resolve_missing_task_findings",
19069
+ "upsert_task_finding"
19070
+ ],
18492
19071
  agents: [
18493
19072
  "auto_assign_task",
18494
19073
  "delete_agent",
@@ -18570,7 +19149,7 @@ var init_token_utils = __esm(() => {
18570
19149
  maintenance: ["extract_todos", "get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task", "watch_source_todos"]
18571
19150
  };
18572
19151
  MCP_PROFILE_GROUPS = {
18573
- minimal: ["core"],
19152
+ minimal: ["core", "loops"],
18574
19153
  core: ["core"],
18575
19154
  standard: ["core", "tasks", "projects", "resources", "agents", "metadata"],
18576
19155
  agent: ["core", "tasks", "projects", "resources"],
@@ -20863,6 +21442,39 @@ async function handleCreateTask(req, ctx, json2, taskToSummary2) {
20863
21442
  return json2({ error: e instanceof Error ? e.message : "Failed to create task" }, 500);
20864
21443
  }
20865
21444
  }
21445
+ async function handleUpsertTask(req, ctx, json2, taskToSummary2) {
21446
+ try {
21447
+ const body = await req.json();
21448
+ if (typeof body["fingerprint"] !== "string" || body["fingerprint"].trim() === "") {
21449
+ return json2({ error: "Missing 'fingerprint'" }, 400);
21450
+ }
21451
+ if (typeof body["title"] !== "string" || body["title"].trim() === "") {
21452
+ return json2({ error: "Missing 'title'" }, 400);
21453
+ }
21454
+ const metadata = body["metadata"] && typeof body["metadata"] === "object" && !Array.isArray(body["metadata"]) ? { ...body["metadata"] } : {};
21455
+ for (const key of ["expectation_id", "expectation_fingerprint", "evidence_paths", "origin_loop_id", "origin_run_id", "expected", "observed", "acceptance"]) {
21456
+ if (body[key] !== undefined)
21457
+ metadata[key] = body[key];
21458
+ }
21459
+ const result = upsertTaskByFingerprint({
21460
+ fingerprint: body["fingerprint"],
21461
+ title: body["title"],
21462
+ description: typeof body["description"] === "string" ? body["description"] : undefined,
21463
+ status: body["status"],
21464
+ priority: body["priority"],
21465
+ project_id: typeof body["project_id"] === "string" ? body["project_id"] : undefined,
21466
+ task_list_id: typeof body["task_list_id"] === "string" ? body["task_list_id"] : undefined,
21467
+ assigned_to: typeof body["assigned_to"] === "string" ? body["assigned_to"] : undefined,
21468
+ working_dir: typeof body["working_dir"] === "string" ? body["working_dir"] : undefined,
21469
+ tags: Array.isArray(body["tags"]) ? body["tags"].filter((tag) => typeof tag === "string") : undefined,
21470
+ metadata
21471
+ });
21472
+ 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 });
21473
+ return json2({ created: result.created, task: taskToSummary2(result.task) }, result.created ? 201 : 200);
21474
+ } catch (e) {
21475
+ return json2({ error: e instanceof Error ? e.message : "Failed to upsert task" }, 500);
21476
+ }
21477
+ }
20866
21478
  function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
20867
21479
  const format = url.searchParams.get("format") || "json";
20868
21480
  const status = url.searchParams.get("status") || undefined;
@@ -26005,6 +26617,61 @@ function registerTaskCrudTools(server, ctx) {
26005
26617
  }
26006
26618
  });
26007
26619
  }
26620
+ if (shouldRegisterTool("upsert_task")) {
26621
+ server.tool("upsert_task", "Create or update a task by stable metadata fingerprint. Metadata is shallow-merged on updates.", {
26622
+ fingerprint: exports_external.string().describe("Stable dedupe fingerprint stored as metadata.fingerprint"),
26623
+ title: exports_external.string().describe("Task title"),
26624
+ description: exports_external.string().optional().describe("Task description"),
26625
+ status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
26626
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
26627
+ project_id: exports_external.string().optional().describe("Project ID"),
26628
+ task_list_id: exports_external.string().optional().describe("Task list ID"),
26629
+ assigned_to: exports_external.string().optional().describe("Agent ID or name to assign to"),
26630
+ tags: exports_external.array(exports_external.string()).optional().describe("Tags for the task"),
26631
+ working_dir: exports_external.string().optional().describe("Working directory associated with the task"),
26632
+ metadata: exports_external.record(exports_external.unknown()).optional().describe("Metadata object to shallow-merge"),
26633
+ expectation_id: exports_external.string().optional(),
26634
+ expectation_fingerprint: exports_external.string().optional(),
26635
+ evidence_paths: exports_external.array(exports_external.string()).optional(),
26636
+ origin_loop_id: exports_external.string().optional(),
26637
+ origin_run_id: exports_external.string().optional(),
26638
+ expected: exports_external.unknown().optional(),
26639
+ observed: exports_external.unknown().optional(),
26640
+ acceptance: exports_external.unknown().optional()
26641
+ }, async (params) => {
26642
+ try {
26643
+ 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;
26644
+ const mergedMetadata = { ...metadata ?? {} };
26645
+ if (expectation_id !== undefined)
26646
+ mergedMetadata["expectation_id"] = expectation_id;
26647
+ if (expectation_fingerprint !== undefined)
26648
+ mergedMetadata["expectation_fingerprint"] = expectation_fingerprint;
26649
+ if (evidence_paths !== undefined)
26650
+ mergedMetadata["evidence_paths"] = evidence_paths;
26651
+ if (origin_loop_id !== undefined)
26652
+ mergedMetadata["origin_loop_id"] = origin_loop_id;
26653
+ if (origin_run_id !== undefined)
26654
+ mergedMetadata["origin_run_id"] = origin_run_id;
26655
+ if (expected !== undefined)
26656
+ mergedMetadata["expected"] = expected;
26657
+ if (observed !== undefined)
26658
+ mergedMetadata["observed"] = observed;
26659
+ if (acceptance !== undefined)
26660
+ mergedMetadata["acceptance"] = acceptance;
26661
+ const resolved = { ...rest, metadata: mergedMetadata };
26662
+ if (assigned_to)
26663
+ resolved.assigned_to = resolveAssignee(assigned_to);
26664
+ if (project_id)
26665
+ resolved.project_id = resolveId(project_id, "projects");
26666
+ if (task_list_id)
26667
+ resolved.task_list_id = resolveId(task_list_id, "task_lists");
26668
+ const result = upsertTaskByFingerprint(resolved);
26669
+ return { content: [{ type: "text", text: compactJson({ created: result.created, task: JSON.parse(mutationTaskResponse(result.task)) }) }] };
26670
+ } catch (e) {
26671
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
26672
+ }
26673
+ });
26674
+ }
26008
26675
  if (shouldRegisterTool("list_tasks")) {
26009
26676
  server.tool("list_tasks", "List tasks with optional filters. Pass empty arrays for multi-value filters (e.g. status=[] shows all).", {
26010
26677
  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 +26683,8 @@ function registerTaskCrudTools(server, ctx) {
26016
26683
  created_after: exports_external.string().optional().describe("ISO date \u2014 tasks created after this date"),
26017
26684
  created_before: exports_external.string().optional().describe("ISO date \u2014 tasks created before this date"),
26018
26685
  limit: exports_external.number().optional().describe("Max results (default: 50, max 500)"),
26019
- offset: exports_external.number().optional().describe("Pagination offset")
26686
+ offset: exports_external.number().optional().describe("Pagination offset"),
26687
+ metadata: exports_external.record(exports_external.unknown()).optional().describe("Exact top-level metadata filters")
26020
26688
  }, async (params) => {
26021
26689
  try {
26022
26690
  const resolved = { ...params };
@@ -27604,7 +28272,7 @@ function parseAssignee(line) {
27604
28272
  return mention;
27605
28273
  return line.match(/\bassign(?:ed)?\s+(?:to\s+)?([a-zA-Z0-9._-]+)/i)?.[1] || null;
27606
28274
  }
27607
- function parseTags(line) {
28275
+ function parseTags2(line) {
27608
28276
  return Array.from(new Set(Array.from(line.matchAll(/#([a-zA-Z0-9._-]+)/g)).map((match) => match[1].toLowerCase()))).slice(0, 10);
27609
28277
  }
27610
28278
  function parseDependencies(line) {
@@ -27676,7 +28344,7 @@ function previewNaturalLanguageIntake(input, db) {
27676
28344
  description: `Parsed from local natural-language intake:
27677
28345
  ${line}`,
27678
28346
  priority: parsePriority2(line, fallbackPriority),
27679
- tags: parseTags(line),
28347
+ tags: parseTags2(line),
27680
28348
  assigned_to: parseAssignee(line),
27681
28349
  due_at: parseDue(line, referenceDate),
27682
28350
  depends_on: parseDependencies(line),
@@ -28184,7 +28852,7 @@ function countMovedStandaloneVerifications(db, duplicateId) {
28184
28852
  const rows = db.query("SELECT command FROM task_verifications WHERE task_id = ?").all(duplicateId);
28185
28853
  return rows.filter((row) => !generatedCommands.has(row.command)).length;
28186
28854
  }
28187
- function mergeTaskMetadata(primary, duplicate, input, mergedAt) {
28855
+ function mergeTaskMetadata2(primary, duplicate, input, mergedAt) {
28188
28856
  const mergedDuplicates = Array.isArray(primary.metadata["merged_duplicates"]) ? [...primary.metadata["merged_duplicates"]] : [];
28189
28857
  mergedDuplicates.push({
28190
28858
  id: duplicate.id,
@@ -28245,7 +28913,7 @@ function mergeDuplicateTask(input, db) {
28245
28913
  updateTask(primary.id, {
28246
28914
  version: primary.version,
28247
28915
  tags: mergedTags,
28248
- metadata: mergeTaskMetadata(primary, duplicate, input, mergedAt),
28916
+ metadata: mergeTaskMetadata2(primary, duplicate, input, mergedAt),
28249
28917
  description: mergeTaskDescription(primary, duplicate) ?? undefined
28250
28918
  }, d);
28251
28919
  moved.comments = updateRows(d, "task_comments", "task_id", duplicate.id, primary.id);
@@ -34239,6 +34907,366 @@ var init_task_meta_tools = __esm(() => {
34239
34907
  init_zod();
34240
34908
  });
34241
34909
 
34910
+ // src/db/findings.ts
34911
+ var exports_findings = {};
34912
+ __export(exports_findings, {
34913
+ upsertTaskFinding: () => upsertTaskFinding,
34914
+ resolveMissingTaskFindings: () => resolveMissingTaskFindings,
34915
+ listTaskFindings: () => listTaskFindings,
34916
+ listCompactTaskFindings: () => listCompactTaskFindings,
34917
+ TASK_FINDING_UPSERT_SCHEMA_VERSION: () => TASK_FINDING_UPSERT_SCHEMA_VERSION,
34918
+ TASK_FINDING_SCHEMA_VERSION: () => TASK_FINDING_SCHEMA_VERSION,
34919
+ TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION: () => TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION
34920
+ });
34921
+ function parseObject2(value) {
34922
+ if (!value)
34923
+ return {};
34924
+ try {
34925
+ const parsed = JSON.parse(value);
34926
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
34927
+ } catch {
34928
+ return {};
34929
+ }
34930
+ }
34931
+ function normalizeKey(value) {
34932
+ return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
34933
+ }
34934
+ function normalizeFingerprint(value) {
34935
+ const normalized = normalizeKey(value);
34936
+ if (!normalized)
34937
+ throw new Error("finding fingerprint is required");
34938
+ return normalized.slice(0, 240);
34939
+ }
34940
+ function normalizeSeverity(value) {
34941
+ const normalized = normalizeKey(value || "medium");
34942
+ if (SEVERITIES.has(normalized))
34943
+ return normalized;
34944
+ if (/^(p0|blocker|urgent|highest)$/.test(normalized))
34945
+ return "critical";
34946
+ if (/^(p1|major)$/.test(normalized))
34947
+ return "high";
34948
+ if (/^(p3|minor|info)$/.test(normalized))
34949
+ return "low";
34950
+ return "medium";
34951
+ }
34952
+ function normalizeStatus2(value) {
34953
+ const normalized = normalizeKey(value || "open");
34954
+ if (STATUSES.has(normalized))
34955
+ return normalized;
34956
+ if (normalized === "closed" || normalized === "fixed")
34957
+ return "resolved";
34958
+ return "open";
34959
+ }
34960
+ function normalizeResolutionStatus(value) {
34961
+ const status = normalizeStatus2(value || "resolved");
34962
+ if (status === "open")
34963
+ throw new Error("resolve-missing status must be resolved or ignored");
34964
+ return status;
34965
+ }
34966
+ function redactOptional(value, max = 2000) {
34967
+ if (!value)
34968
+ return null;
34969
+ const redacted = redactEvidenceText(value).trim();
34970
+ if (!redacted)
34971
+ return null;
34972
+ return redacted.length > max ? `${redacted.slice(0, max - 3)}...` : redacted;
34973
+ }
34974
+ function rowToFinding(row) {
34975
+ return {
34976
+ schema_version: TASK_FINDING_SCHEMA_VERSION,
34977
+ ...row,
34978
+ severity: normalizeSeverity(row.severity),
34979
+ status: normalizeStatus2(row.status),
34980
+ metadata: parseObject2(row.metadata)
34981
+ };
34982
+ }
34983
+ function compactFinding(finding2) {
34984
+ return {
34985
+ schema_version: TASK_FINDING_SCHEMA_VERSION,
34986
+ id: finding2.id,
34987
+ task_id: finding2.task_id,
34988
+ run_id: finding2.run_id,
34989
+ fingerprint: finding2.fingerprint,
34990
+ title: finding2.title,
34991
+ severity: finding2.severity,
34992
+ status: finding2.status,
34993
+ source: finding2.source,
34994
+ summary: finding2.summary,
34995
+ artifact_path: finding2.artifact_path,
34996
+ first_seen_at: finding2.first_seen_at,
34997
+ last_seen_at: finding2.last_seen_at,
34998
+ resolved_at: finding2.resolved_at,
34999
+ metadata_keys: Object.keys(finding2.metadata).sort()
35000
+ };
35001
+ }
35002
+ function previewFinding(existing, next, timestamp3) {
35003
+ return {
35004
+ ...existing,
35005
+ run_id: next.run_id,
35006
+ title: next.title,
35007
+ severity: next.severity,
35008
+ status: next.status,
35009
+ source: next.source,
35010
+ summary: next.summary,
35011
+ artifact_path: next.artifact_path,
35012
+ metadata: next.metadata,
35013
+ last_seen_at: timestamp3,
35014
+ resolved_at: next.status === "open" ? null : existing.resolved_at || timestamp3,
35015
+ updated_at: timestamp3
35016
+ };
35017
+ }
35018
+ function upsertAction(existing, next) {
35019
+ if (sameFinding(existing, next))
35020
+ return "matched";
35021
+ return existing.status !== "open" && next.status === "open" ? "reopened" : "updated";
35022
+ }
35023
+ function resolveRunForTask(runId, taskId, db) {
35024
+ if (!runId)
35025
+ return null;
35026
+ const resolved = resolveTaskRunId(runId, db);
35027
+ const run = getTaskRun(resolved, db);
35028
+ if (!run)
35029
+ throw new Error(`Run not found: ${runId}`);
35030
+ if (run.task_id !== taskId)
35031
+ throw new Error(`Run ${resolved} belongs to task ${run.task_id}, not ${taskId}`);
35032
+ return resolved;
35033
+ }
35034
+ function getFindingByFingerprint(taskId, fingerprint2, db) {
35035
+ const row = db.query("SELECT * FROM task_findings WHERE task_id = ? AND fingerprint = ?").get(taskId, fingerprint2);
35036
+ return row ? rowToFinding(row) : null;
35037
+ }
35038
+ function assertTask(taskId, db) {
35039
+ if (!getTask(taskId, db))
35040
+ throw new TaskNotFoundError(taskId);
35041
+ }
35042
+ function nextFinding(input, db) {
35043
+ const fingerprint2 = normalizeFingerprint(input.fingerprint);
35044
+ const title = redactOptional(input.title, 300);
35045
+ if (!title)
35046
+ throw new Error("finding title is required");
35047
+ return {
35048
+ fingerprint: fingerprint2,
35049
+ run_id: resolveRunForTask(input.run_id, input.task_id, db),
35050
+ title,
35051
+ severity: normalizeSeverity(input.severity),
35052
+ status: normalizeStatus2(input.status),
35053
+ source: redactOptional(input.source, 120),
35054
+ summary: redactOptional(input.summary, 2000),
35055
+ artifact_path: redactOptional(input.artifact_path, 1000),
35056
+ metadata: redactValue(input.metadata || {})
35057
+ };
35058
+ }
35059
+ function sameFinding(left, right) {
35060
+ 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);
35061
+ }
35062
+ function upsertTaskFinding(input, db) {
35063
+ const d = db || getDatabase();
35064
+ assertTask(input.task_id, d);
35065
+ const timestamp3 = input.observed_at || now();
35066
+ const warnings = [];
35067
+ const next = nextFinding(input, d);
35068
+ const existing = getFindingByFingerprint(input.task_id, next.fingerprint, d);
35069
+ const dryRun = !input.apply;
35070
+ if (dryRun) {
35071
+ const action2 = existing ? upsertAction(existing, next) : "preview";
35072
+ return {
35073
+ schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
35074
+ local_only: true,
35075
+ dry_run: true,
35076
+ processed_at: timestamp3,
35077
+ action: action2,
35078
+ fingerprint: next.fingerprint,
35079
+ finding: existing ? compactFinding(previewFinding(existing, next, timestamp3)) : null,
35080
+ warnings
35081
+ };
35082
+ }
35083
+ if (!existing) {
35084
+ const id = uuid();
35085
+ d.run(`INSERT INTO task_findings (
35086
+ id, task_id, run_id, fingerprint, title, severity, status, source, summary, artifact_path,
35087
+ metadata, first_seen_at, last_seen_at, resolved_at, created_at, updated_at
35088
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
35089
+ id,
35090
+ input.task_id,
35091
+ next.run_id,
35092
+ next.fingerprint,
35093
+ next.title,
35094
+ next.severity,
35095
+ next.status,
35096
+ next.source,
35097
+ next.summary,
35098
+ next.artifact_path,
35099
+ JSON.stringify(next.metadata),
35100
+ timestamp3,
35101
+ timestamp3,
35102
+ next.status === "open" ? null : timestamp3,
35103
+ timestamp3,
35104
+ timestamp3
35105
+ ]);
35106
+ return {
35107
+ schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
35108
+ local_only: true,
35109
+ dry_run: false,
35110
+ processed_at: timestamp3,
35111
+ action: "created",
35112
+ fingerprint: next.fingerprint,
35113
+ finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
35114
+ warnings
35115
+ };
35116
+ }
35117
+ if (sameFinding(existing, next)) {
35118
+ return {
35119
+ schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
35120
+ local_only: true,
35121
+ dry_run: false,
35122
+ processed_at: timestamp3,
35123
+ action: "matched",
35124
+ fingerprint: next.fingerprint,
35125
+ finding: compactFinding(existing),
35126
+ warnings
35127
+ };
35128
+ }
35129
+ const action = upsertAction(existing, next);
35130
+ d.run(`UPDATE task_findings SET
35131
+ run_id = ?, title = ?, severity = ?, status = ?, source = ?, summary = ?, artifact_path = ?,
35132
+ metadata = ?, last_seen_at = ?, resolved_at = ?, updated_at = ?
35133
+ WHERE id = ?`, [
35134
+ next.run_id,
35135
+ next.title,
35136
+ next.severity,
35137
+ next.status,
35138
+ next.source,
35139
+ next.summary,
35140
+ next.artifact_path,
35141
+ JSON.stringify(next.metadata),
35142
+ timestamp3,
35143
+ next.status === "open" ? null : existing.resolved_at || timestamp3,
35144
+ timestamp3,
35145
+ existing.id
35146
+ ]);
35147
+ return {
35148
+ schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
35149
+ local_only: true,
35150
+ dry_run: false,
35151
+ processed_at: timestamp3,
35152
+ action,
35153
+ fingerprint: next.fingerprint,
35154
+ finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
35155
+ warnings
35156
+ };
35157
+ }
35158
+ function listTaskFindings(filter = {}, db) {
35159
+ const d = db || getDatabase();
35160
+ const conditions = ["1=1"];
35161
+ const params = [];
35162
+ if (filter.task_id) {
35163
+ conditions.push("task_id = ?");
35164
+ params.push(filter.task_id);
35165
+ }
35166
+ if (filter.run_id) {
35167
+ conditions.push("run_id = ?");
35168
+ params.push(resolveTaskRunId(filter.run_id, d));
35169
+ }
35170
+ if (filter.status) {
35171
+ conditions.push("status = ?");
35172
+ params.push(normalizeStatus2(filter.status));
35173
+ }
35174
+ if (filter.source) {
35175
+ conditions.push("source = ?");
35176
+ params.push(redactOptional(filter.source, 120));
35177
+ }
35178
+ const limit = Math.min(Math.max(Math.floor(filter.limit ?? 50), 1), 500);
35179
+ const rows = d.query(`SELECT * FROM task_findings WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC, created_at DESC LIMIT ?`).all(...[...params, limit]);
35180
+ return rows.map(rowToFinding);
35181
+ }
35182
+ function listCompactTaskFindings(filter = {}, db) {
35183
+ return listTaskFindings(filter, db).map(compactFinding);
35184
+ }
35185
+ function resolveMissingTaskFindings(input, db) {
35186
+ const d = db || getDatabase();
35187
+ assertTask(input.task_id, d);
35188
+ const timestamp3 = input.resolved_at || now();
35189
+ const status = normalizeResolutionStatus(input.status);
35190
+ const runId = resolveRunForTask(input.run_id, input.task_id, d);
35191
+ const present = new Set(input.fingerprints.map(normalizeFingerprint));
35192
+ const warnings = [];
35193
+ const conditions = ["task_id = ?", "status = 'open'"];
35194
+ const params = [input.task_id];
35195
+ if (input.source) {
35196
+ conditions.push("source = ?");
35197
+ params.push(redactOptional(input.source, 120));
35198
+ }
35199
+ 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));
35200
+ const limit = Math.min(Math.max(Math.floor(input.limit ?? 50), 1), 200);
35201
+ const display = candidates.slice(0, limit);
35202
+ const omittedCount = Math.max(0, candidates.length - display.length);
35203
+ if (!input.apply) {
35204
+ return {
35205
+ schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
35206
+ local_only: true,
35207
+ dry_run: true,
35208
+ processed_at: timestamp3,
35209
+ action: candidates.length > 0 ? "preview" : "noop",
35210
+ task_id: input.task_id,
35211
+ source: input.source ? redactOptional(input.source, 120) : null,
35212
+ run_id: runId,
35213
+ present_fingerprint_count: present.size,
35214
+ candidate_count: candidates.length,
35215
+ changed_count: 0,
35216
+ omitted_count: omittedCount,
35217
+ findings: display.map(compactFinding),
35218
+ warnings
35219
+ };
35220
+ }
35221
+ const metadataPatch = redactValue({
35222
+ resolved_by: {
35223
+ agent_id: input.agent_id ?? null,
35224
+ run_id: runId,
35225
+ reason: input.reason ? redactEvidenceText(input.reason) : "missing from latest loop finding set"
35226
+ }
35227
+ });
35228
+ const tx = d.transaction(() => {
35229
+ for (const finding2 of candidates) {
35230
+ d.run("UPDATE task_findings SET status = ?, resolved_at = ?, metadata = ?, updated_at = ? WHERE id = ? AND status = 'open'", [
35231
+ status,
35232
+ timestamp3,
35233
+ JSON.stringify({ ...finding2.metadata, ...metadataPatch }),
35234
+ timestamp3,
35235
+ finding2.id
35236
+ ]);
35237
+ }
35238
+ });
35239
+ tx();
35240
+ const updated = candidates.map((finding2) => d.query("SELECT * FROM task_findings WHERE id = ?").get(finding2.id)).filter((row) => Boolean(row)).map(rowToFinding);
35241
+ const visibleUpdated = updated.slice(0, limit);
35242
+ return {
35243
+ schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
35244
+ local_only: true,
35245
+ dry_run: false,
35246
+ processed_at: timestamp3,
35247
+ action: updated.length > 0 ? status : "noop",
35248
+ task_id: input.task_id,
35249
+ source: input.source ? redactOptional(input.source, 120) : null,
35250
+ run_id: runId,
35251
+ present_fingerprint_count: present.size,
35252
+ candidate_count: candidates.length,
35253
+ changed_count: updated.length,
35254
+ omitted_count: omittedCount,
35255
+ findings: visibleUpdated.map(compactFinding),
35256
+ warnings
35257
+ };
35258
+ }
35259
+ 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;
35260
+ var init_findings = __esm(() => {
35261
+ init_redaction();
35262
+ init_types();
35263
+ init_database();
35264
+ init_tasks();
35265
+ init_task_runs();
35266
+ SEVERITIES = new Set(["low", "medium", "high", "critical"]);
35267
+ STATUSES = new Set(["open", "resolved", "ignored"]);
35268
+ });
35269
+
34242
35270
  // src/lib/agent-run-dispatcher.ts
34243
35271
  var exports_agent_run_dispatcher = {};
34244
35272
  __export(exports_agent_run_dispatcher, {
@@ -35167,7 +36195,7 @@ function parseArray2(value) {
35167
36195
  return [];
35168
36196
  }
35169
36197
  }
35170
- function parseObject2(value) {
36198
+ function parseObject3(value) {
35171
36199
  try {
35172
36200
  const parsed = JSON.parse(value || "{}");
35173
36201
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
@@ -35208,7 +36236,7 @@ function rowToKnowledgeRecord(row) {
35208
36236
  agent_id: row.agent_id,
35209
36237
  snapshot_id: row.snapshot_id,
35210
36238
  tags: parseArray2(row.tags),
35211
- metadata: redactValue(parseObject2(row.metadata)),
36239
+ metadata: redactValue(parseObject3(row.metadata)),
35212
36240
  created_at: row.created_at,
35213
36241
  updated_at: row.updated_at
35214
36242
  };
@@ -35427,7 +36455,7 @@ function parseArray3(value) {
35427
36455
  return [];
35428
36456
  }
35429
36457
  }
35430
- function parseObject3(value) {
36458
+ function parseObject4(value) {
35431
36459
  try {
35432
36460
  const parsed = JSON.parse(value || "{}");
35433
36461
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
@@ -35476,7 +36504,7 @@ function rowToRisk(row) {
35476
36504
  plan_id: row.plan_id,
35477
36505
  task_id: row.task_id,
35478
36506
  tags: parseArray3(row.tags),
35479
- metadata: redactValue(parseObject3(row.metadata)),
36507
+ metadata: redactValue(parseObject4(row.metadata)),
35480
36508
  created_at: row.created_at,
35481
36509
  updated_at: row.updated_at,
35482
36510
  closed_at: row.closed_at
@@ -36327,7 +37355,7 @@ function extractUsage(value) {
36327
37355
  }
36328
37356
  return own;
36329
37357
  }
36330
- function parseJsonObject5(value) {
37358
+ function parseJsonObject6(value) {
36331
37359
  if (!value)
36332
37360
  return {};
36333
37361
  try {
@@ -36499,7 +37527,7 @@ function createLocalUsageLedger(options = {}, db) {
36499
37527
  completedRunMs += millisBetween(run.started_at, run.completed_at);
36500
37528
  else
36501
37529
  openRunMs += millisBetween(run.started_at, generatedAt);
36502
- const usage = extractUsage(parseJsonObject5(run.metadata));
37530
+ const usage = extractUsage(parseJsonObject6(run.metadata));
36503
37531
  metadataUsage.tokens += usage.tokens;
36504
37532
  metadataUsage.cost_usd += usage.cost_usd;
36505
37533
  metadataUsage.duration_ms += usage.duration_ms;
@@ -36511,7 +37539,7 @@ function createLocalUsageLedger(options = {}, db) {
36511
37539
  JOIN tasks t ON t.id = e.task_id
36512
37540
  ${runClause}`, runParams);
36513
37541
  for (const event of eventRows) {
36514
- const usage = extractUsage(parseJsonObject5(event.data));
37542
+ const usage = extractUsage(parseJsonObject6(event.data));
36515
37543
  metadataUsage.tokens += usage.tokens;
36516
37544
  metadataUsage.cost_usd += usage.cost_usd;
36517
37545
  metadataUsage.duration_ms += usage.duration_ms;
@@ -40312,6 +41340,38 @@ ${lines.join(`
40312
41340
  }
40313
41341
  });
40314
41342
  }
41343
+ if (shouldRegisterTool("begin_task_run_transaction")) {
41344
+ server.tool("begin_task_run_transaction", "Preview or apply an idempotent local loop run transaction keyed by a stable loop/run id.", {
41345
+ task_id: exports_external.string().describe("Task ID"),
41346
+ key: exports_external.string().optional().describe("Stable idempotency key"),
41347
+ loop_id: exports_external.string().optional().describe("Loop identifier; used as key fallback"),
41348
+ loop_run_id: exports_external.string().optional().describe("Loop run identifier; used as key fallback"),
41349
+ agent_id: exports_external.string().optional().describe("Agent starting the run"),
41350
+ title: exports_external.string().optional().describe("Run title"),
41351
+ summary: exports_external.string().optional().describe("Run summary"),
41352
+ metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata"),
41353
+ claim: exports_external.boolean().optional().describe("Claim/start the task before recording the run"),
41354
+ apply: exports_external.boolean().optional().describe("Apply the transaction; false or omitted is dry-run")
41355
+ }, async ({ task_id, key, loop_id, loop_run_id, agent_id, title, summary, metadata, claim, apply }) => {
41356
+ try {
41357
+ const result = beginTaskRunTransaction({
41358
+ task_id: resolveId(task_id),
41359
+ key,
41360
+ loop_id,
41361
+ loop_run_id,
41362
+ agent_id,
41363
+ title,
41364
+ summary,
41365
+ metadata,
41366
+ claim,
41367
+ apply
41368
+ });
41369
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
41370
+ } catch (e) {
41371
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
41372
+ }
41373
+ });
41374
+ }
40315
41375
  if (shouldRegisterTool("list_task_runs")) {
40316
41376
  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
41377
  try {
@@ -40409,15 +41469,117 @@ ${lines.join(`
40409
41469
  });
40410
41470
  }
40411
41471
  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"),
41472
+ server.tool("finish_task_run", "Finish a local run ledger entry as completed, failed, or cancelled. Supports idempotent lookup by key.", {
41473
+ run_id: exports_external.string().optional().describe("Run ID or prefix"),
41474
+ key: exports_external.string().optional().describe("Idempotency key when run_id is omitted"),
41475
+ task_id: exports_external.string().optional().describe("Task scope for key lookup"),
41476
+ status: exports_external.enum(["completed", "failed", "cancelled"]).optional().describe("Final run status"),
40415
41477
  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 }) => {
41478
+ agent_id: exports_external.string().optional().describe("Agent finishing the run"),
41479
+ apply: exports_external.boolean().optional().describe("Apply the transaction; false previews without mutating")
41480
+ }, async ({ run_id, key, task_id, status, summary, agent_id, apply }) => {
40418
41481
  try {
40419
- const run = finishTaskRun({ run_id, status, summary, agent_id });
40420
- return { content: [{ type: "text", text: JSON.stringify(run, null, 2) }] };
41482
+ if (run_id && !key && apply === undefined) {
41483
+ const result2 = finishTaskRunTransaction({ run_id, status: status || "completed", summary, agent_id, apply: true });
41484
+ const run = result2.run ? getTaskRunLedger(result2.run.id).run : null;
41485
+ return { content: [{ type: "text", text: JSON.stringify(run, null, 2) }] };
41486
+ }
41487
+ const result = finishTaskRunTransaction({
41488
+ run_id,
41489
+ key,
41490
+ task_id: task_id ? resolveId(task_id) : undefined,
41491
+ status: status || "completed",
41492
+ summary,
41493
+ agent_id,
41494
+ apply: apply !== false
41495
+ });
41496
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
41497
+ } catch (e) {
41498
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
41499
+ }
41500
+ });
41501
+ }
41502
+ if (shouldRegisterTool("upsert_task_finding")) {
41503
+ server.tool("upsert_task_finding", "Preview or apply an idempotent local finding upsert scoped by task and fingerprint.", {
41504
+ task_id: exports_external.string().describe("Task ID"),
41505
+ fingerprint: exports_external.string().describe("Stable finding fingerprint"),
41506
+ title: exports_external.string().describe("Finding title"),
41507
+ severity: exports_external.enum(["low", "medium", "high", "critical"]).optional().describe("Finding severity"),
41508
+ status: exports_external.enum(["open", "resolved", "ignored"]).optional().describe("Finding status"),
41509
+ source: exports_external.string().optional().describe("Loop/tool source name"),
41510
+ summary: exports_external.string().optional().describe("Bounded finding summary"),
41511
+ artifact_path: exports_external.string().optional().describe("Local artifact path/reference; content is not read"),
41512
+ run_id: exports_external.string().optional().describe("Run ledger ID or prefix"),
41513
+ metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata"),
41514
+ apply: exports_external.boolean().optional().describe("Apply the upsert; false or omitted is dry-run")
41515
+ }, async ({ task_id, fingerprint: fingerprint3, title, severity, status, source: source3, summary, artifact_path, run_id, metadata, apply }) => {
41516
+ try {
41517
+ const result = upsertTaskFinding({
41518
+ task_id: resolveId(task_id),
41519
+ fingerprint: fingerprint3,
41520
+ title,
41521
+ severity,
41522
+ status,
41523
+ source: source3,
41524
+ summary,
41525
+ artifact_path,
41526
+ run_id,
41527
+ metadata,
41528
+ apply
41529
+ });
41530
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
41531
+ } catch (e) {
41532
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
41533
+ }
41534
+ });
41535
+ }
41536
+ if (shouldRegisterTool("list_task_findings")) {
41537
+ server.tool("list_task_findings", "List compact local findings with bounded output.", {
41538
+ task_id: exports_external.string().optional().describe("Filter by task"),
41539
+ run_id: exports_external.string().optional().describe("Filter by run"),
41540
+ status: exports_external.enum(["open", "resolved", "ignored"]).optional().describe("Filter by status"),
41541
+ source: exports_external.string().optional().describe("Filter by source"),
41542
+ limit: exports_external.number().optional().describe("Maximum findings to return")
41543
+ }, async ({ task_id, run_id, status, source: source3, limit }) => {
41544
+ try {
41545
+ const findings = listCompactTaskFindings({
41546
+ task_id: task_id ? resolveId(task_id) : undefined,
41547
+ run_id,
41548
+ status,
41549
+ source: source3,
41550
+ limit
41551
+ });
41552
+ return { content: [{ type: "text", text: JSON.stringify(findings) }] };
41553
+ } catch (e) {
41554
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
41555
+ }
41556
+ });
41557
+ }
41558
+ if (shouldRegisterTool("resolve_missing_task_findings")) {
41559
+ server.tool("resolve_missing_task_findings", "Resolve open findings absent from the latest loop finding set.", {
41560
+ task_id: exports_external.string().describe("Task ID"),
41561
+ fingerprints: exports_external.array(exports_external.string()).optional().describe("Fingerprints still present in the latest run"),
41562
+ source: exports_external.string().optional().describe("Only resolve findings from this source"),
41563
+ run_id: exports_external.string().optional().describe("Run ledger ID or prefix for audit metadata"),
41564
+ status: exports_external.enum(["resolved", "ignored"]).optional().describe("Resolution status"),
41565
+ agent_id: exports_external.string().optional().describe("Agent resolving findings"),
41566
+ reason: exports_external.string().optional().describe("Resolution reason"),
41567
+ limit: exports_external.number().optional().describe("Maximum findings returned"),
41568
+ apply: exports_external.boolean().optional().describe("Apply resolution; false or omitted is dry-run")
41569
+ }, async ({ task_id, fingerprints, source: source3, run_id, status, agent_id, reason, limit, apply }) => {
41570
+ try {
41571
+ const result = resolveMissingTaskFindings({
41572
+ task_id: resolveId(task_id),
41573
+ fingerprints: fingerprints || [],
41574
+ source: source3,
41575
+ run_id,
41576
+ status,
41577
+ agent_id,
41578
+ reason,
41579
+ limit,
41580
+ apply
41581
+ });
41582
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
40421
41583
  } catch (e) {
40422
41584
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
40423
41585
  }
@@ -40753,6 +41915,7 @@ var init_task_resources = __esm(() => {
40753
41915
  init_agents();
40754
41916
  init_task_commits();
40755
41917
  init_task_runs();
41918
+ init_findings();
40756
41919
  init_agent_run_dispatcher();
40757
41920
  init_verification_providers();
40758
41921
  init_release_notes();
@@ -44493,8 +45656,10 @@ function taskToSummary(task2, fields) {
44493
45656
  task_list_id: task2.task_list_id,
44494
45657
  agent_id: task2.agent_id,
44495
45658
  assigned_to: task2.assigned_to,
45659
+ working_dir: task2.working_dir,
44496
45660
  locked_by: task2.locked_by,
44497
45661
  tags: task2.tags,
45662
+ metadata: task2.metadata,
44498
45663
  version: task2.version,
44499
45664
  created_at: task2.created_at,
44500
45665
  updated_at: task2.updated_at,
@@ -44636,6 +45801,9 @@ Dashboard not found at: ${dashboardDir}`);
44636
45801
  if (path === "/api/tasks" && method === "POST") {
44637
45802
  return handleCreateTask(req, ctx, jsonWithCors, taskToSummary);
44638
45803
  }
45804
+ if (path === "/api/tasks/upsert" && method === "POST") {
45805
+ return handleUpsertTask(req, ctx, jsonWithCors, taskToSummary);
45806
+ }
44639
45807
  if (path === "/api/tasks/export" && method === "GET") {
44640
45808
  return handleTasksExport(req, url, ctx, jsonWithCors, taskToSummary);
44641
45809
  }
@@ -44831,7 +45999,7 @@ Dashboard not found at: ${dashboardDir}`);
44831
45999
  } catch {}
44832
46000
  }
44833
46001
  }
44834
- var MIME_TYPES, SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX = 120;
46002
+ var MIME_TYPES, SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX;
44835
46003
  var init_serve = __esm(() => {
44836
46004
  init_database();
44837
46005
  init_api_keys();
@@ -44856,6 +46024,7 @@ var init_serve = __esm(() => {
44856
46024
  "Permissions-Policy": "camera=, microphone=, geolocation="
44857
46025
  };
44858
46026
  rateLimitMap = new Map;
46027
+ RATE_LIMIT_MAX = Number.parseInt(process.env["TODOS_RATE_LIMIT_MAX"] || "120", 10);
44859
46028
  });
44860
46029
 
44861
46030
  // src/cli/components/Header.tsx
@@ -47509,9 +48678,9 @@ function normalizeKind(value) {
47509
48678
  const raw = cleanKey(asString4(value) || "unknown").replace(/-/g, "_");
47510
48679
  return KINDS.has(raw) ? raw : raw || "unknown";
47511
48680
  }
47512
- function normalizeSeverity(value, fallback) {
48681
+ function normalizeSeverity2(value, fallback) {
47513
48682
  const raw = cleanKey(asString4(value) || fallback);
47514
- if (SEVERITIES.has(raw))
48683
+ if (SEVERITIES2.has(raw))
47515
48684
  return raw;
47516
48685
  if (/^(p0|blocker|urgent|highest)$/.test(raw))
47517
48686
  return "critical";
@@ -47606,7 +48775,7 @@ function normalizeTesterIssueReport(value, fallbackPriority = "medium") {
47606
48775
  title,
47607
48776
  summary: truncate3(asString4(input["summary"]), 1000) ?? null,
47608
48777
  kind: normalizeKind(input["kind"] ?? input["type"]),
47609
- severity: normalizeSeverity(input["severity"] ?? input["priority"], fallbackPriority),
48778
+ severity: normalizeSeverity2(input["severity"] ?? input["priority"], fallbackPriority),
47610
48779
  source: source3,
47611
48780
  target,
47612
48781
  failure,
@@ -47904,13 +49073,13 @@ function readTesterIssueReportsPayload(value) {
47904
49073
  return record["issues"];
47905
49074
  return [value];
47906
49075
  }
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;
49076
+ 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
49077
  var init_tester_issue_reports = __esm(() => {
47909
49078
  init_database();
47910
49079
  init_tasks();
47911
49080
  init_redaction();
47912
49081
  PRIORITIES5 = ["low", "medium", "high", "critical"];
47913
- SEVERITIES = new Set(PRIORITIES5);
49082
+ SEVERITIES2 = new Set(PRIORITIES5);
47914
49083
  KINDS = new Set([
47915
49084
  "assertion_failure",
47916
49085
  "runtime_error",
@@ -51092,6 +52261,23 @@ Verifications:`));
51092
52261
  console.log(`${result.status} ${result.provider}: ${result.output_summary || ""}`);
51093
52262
  });
51094
52263
  const runs = program2.command("runs").description("Manage the local run ledger and evidence capture");
52264
+ 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) => {
52265
+ const globalOpts = program2.opts();
52266
+ const { beginTaskRunTransaction: beginTaskRunTransaction2 } = await Promise.resolve().then(() => (init_task_runs(), exports_task_runs));
52267
+ const result = beginTaskRunTransaction2({
52268
+ task_id: resolveTaskId(taskId),
52269
+ key: opts.key,
52270
+ loop_id: opts.loopId,
52271
+ loop_run_id: opts.loopRunId,
52272
+ agent_id: opts.agent || globalOpts.agent,
52273
+ title: opts.title,
52274
+ summary: opts.summary,
52275
+ metadata: parseJsonOption(opts.metadata, "--metadata"),
52276
+ claim: opts.claim,
52277
+ apply: opts.apply
52278
+ });
52279
+ output(result, true);
52280
+ });
51095
52281
  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
52282
  const globalOpts = program2.opts();
51097
52283
  const { startTaskRun: startTaskRun2 } = await Promise.resolve().then(() => (init_task_runs(), exports_task_runs));
@@ -51303,19 +52489,68 @@ Artifacts:`));
51303
52489
  process.exit(1);
51304
52490
  }
51305
52491
  });
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) => {
52492
+ 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
52493
  const globalOpts = program2.opts();
51308
52494
  if (opts.status !== "completed" && opts.status !== "failed" && opts.status !== "cancelled") {
51309
52495
  console.error(chalk8.red("--status must be completed, failed, or cancelled"));
51310
52496
  process.exit(1);
51311
52497
  }
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}`));
52498
+ const { finishTaskRunTransaction: finishTaskRunTransaction2 } = await Promise.resolve().then(() => (init_task_runs(), exports_task_runs));
52499
+ const result = finishTaskRunTransaction2({
52500
+ run_id: runId,
52501
+ key: opts.key,
52502
+ task_id: opts.task ? resolveTaskId(opts.task) : undefined,
52503
+ status: opts.status,
52504
+ summary: opts.summary,
52505
+ agent_id: opts.agent || globalOpts.agent,
52506
+ apply: !opts.dryRun
52507
+ });
52508
+ output(result, true);
52509
+ });
52510
+ const findings = program2.command("findings").description("Manage local task findings for loop dedupe and resolution");
52511
+ 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) => {
52512
+ const { upsertTaskFinding: upsertTaskFinding2 } = await Promise.resolve().then(() => (init_findings(), exports_findings));
52513
+ const result = upsertTaskFinding2({
52514
+ task_id: resolveTaskId(opts.task),
52515
+ fingerprint: opts.fingerprint,
52516
+ title: opts.title,
52517
+ severity: opts.severity,
52518
+ status: opts.status,
52519
+ source: opts.source,
52520
+ summary: opts.summary,
52521
+ artifact_path: opts.artifact,
52522
+ run_id: opts.run,
52523
+ metadata: parseJsonOption(opts.metadata, "--metadata"),
52524
+ apply: opts.apply
52525
+ });
52526
+ output(result, true);
52527
+ });
52528
+ 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) => {
52529
+ const globalOpts = program2.opts();
52530
+ const { resolveMissingTaskFindings: resolveMissingTaskFindings2 } = await Promise.resolve().then(() => (init_findings(), exports_findings));
52531
+ const result = resolveMissingTaskFindings2({
52532
+ task_id: resolveTaskId(opts.task),
52533
+ fingerprints: listOption(opts.fingerprints) || [],
52534
+ source: opts.source,
52535
+ run_id: opts.run,
52536
+ status: opts.status,
52537
+ agent_id: opts.agent || globalOpts.agent,
52538
+ reason: opts.reason,
52539
+ limit: opts.limit ? Number.parseInt(opts.limit, 10) : undefined,
52540
+ apply: opts.apply
52541
+ });
52542
+ output(result, true);
52543
+ });
52544
+ 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) => {
52545
+ const { listCompactTaskFindings: listCompactTaskFindings2 } = await Promise.resolve().then(() => (init_findings(), exports_findings));
52546
+ const findings2 = listCompactTaskFindings2({
52547
+ task_id: opts.task ? resolveTaskId(opts.task) : undefined,
52548
+ run_id: opts.run,
52549
+ status: opts.status,
52550
+ source: opts.source,
52551
+ limit: opts.limit ? Number.parseInt(opts.limit, 10) : undefined
52552
+ });
52553
+ output(findings2, true);
51319
52554
  });
51320
52555
  const agentRuns = program2.command("agent-runs").description("Queue and dispatch local agent runs");
51321
52556
  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 +53464,7 @@ function parseRecordType(value) {
52229
53464
  console.error(chalk13.red(`type must be one of: ${RECORD_TYPES.join(", ")}`));
52230
53465
  process.exit(1);
52231
53466
  }
52232
- function parseJsonObject6(value, label) {
53467
+ function parseJsonObject7(value, label) {
52233
53468
  if (!value)
52234
53469
  return;
52235
53470
  try {
@@ -52289,7 +53524,7 @@ function registerKnowledgeCommands(program2) {
52289
53524
  plan_id: opts.plan,
52290
53525
  agent_id: opts.agent || globalOpts.agent,
52291
53526
  tags: tagsFromOption(opts.tag),
52292
- metadata: parseJsonObject6(opts.metadataJson, "--metadata-json")
53527
+ metadata: parseJsonObject7(opts.metadataJson, "--metadata-json")
52293
53528
  }, getDatabase());
52294
53529
  if (opts.json || globalOpts.json)
52295
53530
  output(record, true);
@@ -52314,7 +53549,7 @@ function registerKnowledgeCommands(program2) {
52314
53549
  blockers: opts.blocker,
52315
53550
  next_steps: opts.next,
52316
53551
  tags: tagsFromOption(opts.tag),
52317
- metadata: parseJsonObject6(opts.metadataJson, "--metadata-json")
53552
+ metadata: parseJsonObject7(opts.metadataJson, "--metadata-json")
52318
53553
  }, getDatabase());
52319
53554
  if (opts.json || globalOpts.json)
52320
53555
  output(result, true);
@@ -52400,7 +53635,7 @@ function parseChoice(value, choices, label) {
52400
53635
  console.error(chalk14.red(`${label} must be one of: ${choices.join(", ")}`));
52401
53636
  process.exit(1);
52402
53637
  }
52403
- function parseJsonObject7(value, label) {
53638
+ function parseJsonObject8(value, label) {
52404
53639
  if (!value)
52405
53640
  return;
52406
53641
  try {
@@ -52421,8 +53656,8 @@ function tagsFromOption2(value) {
52421
53656
  }
52422
53657
  function commonFilters2(opts) {
52423
53658
  return {
52424
- status: opts.status ? parseChoice(opts.status, STATUSES, "--status") : undefined,
52425
- severity: opts.severity ? parseChoice(opts.severity, SEVERITIES2, "--severity") : undefined,
53659
+ status: opts.status ? parseChoice(opts.status, STATUSES2, "--status") : undefined,
53660
+ severity: opts.severity ? parseChoice(opts.severity, SEVERITIES3, "--severity") : undefined,
52426
53661
  probability: opts.probability ? parseChoice(opts.probability, PROBABILITIES, "--probability") : undefined,
52427
53662
  owner: opts.owner,
52428
53663
  project_id: opts.project,
@@ -52455,8 +53690,8 @@ function registerRiskCommands(program2) {
52455
53690
  const risk = createRisk({
52456
53691
  title,
52457
53692
  description: opts.description,
52458
- status: parseChoice(opts.status, STATUSES, "--status"),
52459
- severity: parseChoice(opts.severity, SEVERITIES2, "--severity"),
53693
+ status: parseChoice(opts.status, STATUSES2, "--status"),
53694
+ severity: parseChoice(opts.severity, SEVERITIES3, "--severity"),
52460
53695
  probability: parseChoice(opts.probability, PROBABILITIES, "--probability"),
52461
53696
  owner: opts.owner,
52462
53697
  mitigation: opts.mitigation,
@@ -52465,7 +53700,7 @@ function registerRiskCommands(program2) {
52465
53700
  plan_id: opts.plan,
52466
53701
  task_id: opts.task,
52467
53702
  tags: tagsFromOption2(opts.tag),
52468
- metadata: parseJsonObject7(opts.metadataJson, "--metadata-json")
53703
+ metadata: parseJsonObject8(opts.metadataJson, "--metadata-json")
52469
53704
  }, getDatabase());
52470
53705
  if (opts.json || globalOpts.json)
52471
53706
  output(risk, true);
@@ -52507,8 +53742,8 @@ function registerRiskCommands(program2) {
52507
53742
  const risk = updateRisk(id, {
52508
53743
  title: opts.title,
52509
53744
  description: opts.description,
52510
- status: opts.status ? parseChoice(opts.status, STATUSES, "--status") : undefined,
52511
- severity: opts.severity ? parseChoice(opts.severity, SEVERITIES2, "--severity") : undefined,
53745
+ status: opts.status ? parseChoice(opts.status, STATUSES2, "--status") : undefined,
53746
+ severity: opts.severity ? parseChoice(opts.severity, SEVERITIES3, "--severity") : undefined,
52512
53747
  probability: opts.probability ? parseChoice(opts.probability, PROBABILITIES, "--probability") : undefined,
52513
53748
  owner: opts.owner,
52514
53749
  mitigation: opts.mitigation,
@@ -52517,7 +53752,7 @@ function registerRiskCommands(program2) {
52517
53752
  plan_id: opts.plan,
52518
53753
  task_id: opts.task,
52519
53754
  tags: opts.tag.length > 0 ? tagsFromOption2(opts.tag) : undefined,
52520
- metadata: parseJsonObject7(opts.metadataJson, "--metadata-json")
53755
+ metadata: parseJsonObject8(opts.metadataJson, "--metadata-json")
52521
53756
  }, getDatabase());
52522
53757
  if (opts.json || globalOpts.json)
52523
53758
  output(risk, true);
@@ -52575,13 +53810,13 @@ function registerRiskCommands(program2) {
52575
53810
  }
52576
53811
  });
52577
53812
  }
52578
- var STATUSES, SEVERITIES2, PROBABILITIES;
53813
+ var STATUSES2, SEVERITIES3, PROBABILITIES;
52579
53814
  var init_risk_commands = __esm(() => {
52580
53815
  init_database();
52581
53816
  init_project_risks();
52582
53817
  init_helpers();
52583
- STATUSES = ["open", "mitigating", "resolved", "accepted"];
52584
- SEVERITIES2 = ["low", "medium", "high", "critical"];
53818
+ STATUSES2 = ["open", "mitigating", "resolved", "accepted"];
53819
+ SEVERITIES3 = ["low", "medium", "high", "critical"];
52585
53820
  PROBABILITIES = ["low", "medium", "high"];
52586
53821
  });
52587
53822
 
@@ -55166,6 +56401,92 @@ var init_json_contracts = __esm(() => {
55166
56401
  },
55167
56402
  optional: {}
55168
56403
  }),
56404
+ contract({
56405
+ id: "loop_run_transaction",
56406
+ name: "Loop Run Transaction",
56407
+ description: "Compact local result for idempotent loop run begin/finish transactions.",
56408
+ surfaces: ["cli", "mcp", "sdk"],
56409
+ stability: "stable",
56410
+ required: {
56411
+ schema_version: field("string", "Result schema version."),
56412
+ local_only: field("boolean", "Always true; loop run transactions use local state."),
56413
+ dry_run: field("boolean", "True when no run ledger mutation was applied."),
56414
+ processed_at: isoDateField,
56415
+ action: field("string", "preview, created, matched, finished, or conflict."),
56416
+ key: field("string", "Stable idempotency key used to dedupe the transaction."),
56417
+ run: field(["object", "null"], "Compact run summary or null for create previews.", true),
56418
+ warnings: field("array", "Non-fatal warnings such as terminal-status conflicts."),
56419
+ commands: field("array", "Follow-up CLI commands for agents and operators.")
56420
+ },
56421
+ optional: {}
56422
+ }),
56423
+ contract({
56424
+ id: "task_finding",
56425
+ name: "Task Finding",
56426
+ description: "Compact local finding record deduped by task and fingerprint.",
56427
+ surfaces: ["cli", "mcp", "sdk"],
56428
+ stability: "stable",
56429
+ required: {
56430
+ schema_version: field("string", "Finding schema version."),
56431
+ id: idField,
56432
+ task_id: idField,
56433
+ run_id: field(["string", "null"], "Optional run ledger ID.", true),
56434
+ fingerprint: field("string", "Stable finding fingerprint scoped to the task."),
56435
+ title: field("string", "Short redacted finding title."),
56436
+ severity: field("string", "low, medium, high, or critical."),
56437
+ status: field("string", "open, resolved, or ignored."),
56438
+ source: field(["string", "null"], "Optional loop/tool source.", true),
56439
+ summary: field(["string", "null"], "Bounded redacted finding summary.", true),
56440
+ artifact_path: field(["string", "null"], "Local artifact path/reference; raw content is not included.", true),
56441
+ first_seen_at: isoDateField,
56442
+ last_seen_at: isoDateField,
56443
+ resolved_at: field(["string", "null"], "Resolution timestamp when closed.", true),
56444
+ metadata_keys: field("array", "Sorted metadata keys; metadata values are intentionally omitted in compact output.")
56445
+ },
56446
+ optional: {}
56447
+ }),
56448
+ contract({
56449
+ id: "task_finding_upsert",
56450
+ name: "Task Finding Upsert Result",
56451
+ description: "Local-only dry-run or applied result from idempotently upserting a task finding.",
56452
+ surfaces: ["cli", "mcp", "sdk"],
56453
+ stability: "stable",
56454
+ required: {
56455
+ schema_version: field("string", "Result schema version."),
56456
+ local_only: field("boolean", "Always true; finding upserts use local state."),
56457
+ dry_run: field("boolean", "True when no finding row was created or updated."),
56458
+ processed_at: isoDateField,
56459
+ action: field("string", "preview, created, matched, updated, or reopened."),
56460
+ fingerprint: field("string", "Normalized finding fingerprint."),
56461
+ finding: field(["object", "null"], "Compact finding summary or null for create previews.", true),
56462
+ warnings: field("array", "Non-fatal warnings.")
56463
+ },
56464
+ optional: {}
56465
+ }),
56466
+ contract({
56467
+ id: "task_finding_resolve_missing",
56468
+ name: "Task Finding Resolve Missing Result",
56469
+ description: "Local-only dry-run or applied result from resolving open findings absent from the latest loop finding set.",
56470
+ surfaces: ["cli", "mcp", "sdk"],
56471
+ stability: "stable",
56472
+ required: {
56473
+ schema_version: field("string", "Result schema version."),
56474
+ local_only: field("boolean", "Always true; finding resolution uses local state."),
56475
+ dry_run: field("boolean", "True when no finding rows were changed."),
56476
+ processed_at: isoDateField,
56477
+ action: field("string", "preview, resolved, ignored, or noop."),
56478
+ task_id: idField,
56479
+ source: field(["string", "null"], "Optional source scope.", true),
56480
+ run_id: field(["string", "null"], "Optional run ledger ID used for audit metadata.", true),
56481
+ present_fingerprint_count: field("integer", "Number of fingerprints supplied as still present."),
56482
+ candidate_count: field("integer", "Open findings missing from the supplied set."),
56483
+ changed_count: field("integer", "Rows resolved or ignored by the applied transaction."),
56484
+ omitted_count: field("integer", "Matching findings omitted from bounded output."),
56485
+ findings: field("array", "Bounded compact finding summaries."),
56486
+ warnings: field("array", "Non-fatal warnings.")
56487
+ },
56488
+ optional: {}
56489
+ }),
55169
56490
  contract({
55170
56491
  id: "verification_provider",
55171
56492
  name: "Verification Provider",