@hasna/todos 0.11.57 → 0.11.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +38 -0
  2. package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
  3. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  4. package/dist/cli/index.js +1622 -285
  5. package/dist/contracts.d.ts.map +1 -1
  6. package/dist/contracts.js +703 -21
  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 +1071 -103
  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 +1186 -115
  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 +703 -21
  32. package/dist/release-provenance.json +3 -3
  33. package/dist/server/index.js +1186 -115
  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 +574 -97
  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.7/node_modules/@hasna/events/dist/index.js
6242
+ // node_modules/.bun/@hasna+events@0.1.9/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";
@@ -6171,17 +6256,30 @@ function getPathValue(input, path) {
6171
6256
  return;
6172
6257
  }, input);
6173
6258
  }
6174
- function wildcardToRegExp(pattern) {
6175
- const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
6176
- return new RegExp(`^${escaped}$`);
6259
+ function wildcardToRegExp(pattern, options = {}) {
6260
+ let body = "";
6261
+ for (let index = 0;index < pattern.length; index += 1) {
6262
+ const char = pattern[index];
6263
+ if (char === "*") {
6264
+ if (pattern[index + 1] === "*") {
6265
+ body += ".*";
6266
+ index += 1;
6267
+ } else {
6268
+ body += options.segmentSafe ? "[^/]*" : ".*";
6269
+ }
6270
+ } else {
6271
+ body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
6272
+ }
6273
+ }
6274
+ return new RegExp(`^${body}$`);
6177
6275
  }
6178
- function matchString(value, matcher) {
6276
+ function matchString(value, matcher, options = {}) {
6179
6277
  if (matcher === undefined)
6180
6278
  return true;
6181
6279
  if (value === undefined)
6182
6280
  return false;
6183
6281
  const matchers = Array.isArray(matcher) ? matcher : [matcher];
6184
- return matchers.some((item) => wildcardToRegExp(item).test(value));
6282
+ return matchers.some((item) => wildcardToRegExp(item, options).test(value));
6185
6283
  }
6186
6284
  function matchRecord(input, matcher) {
6187
6285
  if (!matcher)
@@ -6189,7 +6287,9 @@ function matchRecord(input, matcher) {
6189
6287
  return Object.entries(matcher).every(([path, expected]) => {
6190
6288
  const actual = getPathValue(input, path);
6191
6289
  if (typeof expected === "string" || Array.isArray(expected)) {
6192
- return matchString(actual === undefined ? undefined : String(actual), expected);
6290
+ return matchString(actual === undefined ? undefined : String(actual), expected, {
6291
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
6292
+ });
6193
6293
  }
6194
6294
  return actual === expected;
6195
6295
  });
@@ -6543,7 +6643,7 @@ class EventsClient {
6543
6643
  }
6544
6644
  return deliveries;
6545
6645
  }
6546
- async testChannel(id, input = {}) {
6646
+ async matchChannel(id, input = {}) {
6547
6647
  const channel = await this.store.getChannel(id);
6548
6648
  if (!channel)
6549
6649
  throw new Error(`Channel not found: ${id}`);
@@ -6560,6 +6660,34 @@ class EventsClient {
6560
6660
  time: input.time,
6561
6661
  id: input.id
6562
6662
  });
6663
+ const matched = channelMatchesEvent(channel, event);
6664
+ return {
6665
+ channelId: channel.id,
6666
+ matched,
6667
+ event,
6668
+ filters: channel.filters,
6669
+ reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
6670
+ };
6671
+ }
6672
+ async testChannel(id, input = {}, options = {}) {
6673
+ const channel = await this.store.getChannel(id);
6674
+ if (!channel)
6675
+ throw new Error(`Channel not found: ${id}`);
6676
+ const match = await this.matchChannel(id, input);
6677
+ const event = match.event;
6678
+ if (options.honorFilters && !match.matched) {
6679
+ const timestamp = new Date().toISOString();
6680
+ const result2 = createDeliveryResult(event, channel, [{
6681
+ attempt: 1,
6682
+ status: "skipped",
6683
+ startedAt: timestamp,
6684
+ completedAt: timestamp,
6685
+ error: match.reason
6686
+ }]);
6687
+ result2.metadata = { reason: "filter_mismatch" };
6688
+ await this.store.appendDelivery(result2);
6689
+ return result2;
6690
+ }
6563
6691
  const eventForChannel = await this.applyRedaction(event, channel);
6564
6692
  const result = await this.deliverWithRetry(eventForChannel, channel);
6565
6693
  await this.store.appendDelivery(result);
@@ -6663,6 +6791,90 @@ var init_dist = __esm(() => {
6663
6791
  DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
6664
6792
  });
6665
6793
 
6794
+ // src/db/task-lists.ts
6795
+ function rowToTaskList(row) {
6796
+ return {
6797
+ ...row,
6798
+ metadata: JSON.parse(row.metadata || "{}")
6799
+ };
6800
+ }
6801
+ function createTaskList(input, db) {
6802
+ const d = db || getDatabase();
6803
+ const id = uuid();
6804
+ const timestamp = now();
6805
+ const slug = input.slug || slugify(input.name);
6806
+ if (!input.project_id) {
6807
+ const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
6808
+ if (existing) {
6809
+ throw new Error(`Standalone task list with slug "${slug}" already exists`);
6810
+ }
6811
+ }
6812
+ d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
6813
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
6814
+ return getTaskList(id, d);
6815
+ }
6816
+ function getTaskList(id, db) {
6817
+ const d = db || getDatabase();
6818
+ const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
6819
+ return row ? rowToTaskList(row) : null;
6820
+ }
6821
+ function getTaskListBySlug(slug, projectId, db) {
6822
+ const d = db || getDatabase();
6823
+ let row;
6824
+ if (projectId) {
6825
+ row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
6826
+ } else {
6827
+ row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
6828
+ }
6829
+ return row ? rowToTaskList(row) : null;
6830
+ }
6831
+ function listTaskLists(projectId, db) {
6832
+ const d = db || getDatabase();
6833
+ if (projectId) {
6834
+ return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
6835
+ }
6836
+ return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
6837
+ }
6838
+ function updateTaskList(id, input, db) {
6839
+ const d = db || getDatabase();
6840
+ const existing = getTaskList(id, d);
6841
+ if (!existing)
6842
+ throw new TaskListNotFoundError(id);
6843
+ const sets = ["updated_at = ?"];
6844
+ const params = [now()];
6845
+ if (input.name !== undefined) {
6846
+ sets.push("name = ?");
6847
+ params.push(input.name);
6848
+ }
6849
+ if (input.description !== undefined) {
6850
+ sets.push("description = ?");
6851
+ params.push(input.description);
6852
+ }
6853
+ if (input.metadata !== undefined) {
6854
+ sets.push("metadata = ?");
6855
+ params.push(JSON.stringify(input.metadata));
6856
+ }
6857
+ params.push(id);
6858
+ d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
6859
+ return getTaskList(id, d);
6860
+ }
6861
+ function deleteTaskList(id, db) {
6862
+ const d = db || getDatabase();
6863
+ return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
6864
+ }
6865
+ function ensureTaskList(name, slug, projectId, db) {
6866
+ const d = db || getDatabase();
6867
+ const existing = getTaskListBySlug(slug, projectId, d);
6868
+ if (existing)
6869
+ return existing;
6870
+ return createTaskList({ name, slug, project_id: projectId }, d);
6871
+ }
6872
+ var init_task_lists = __esm(() => {
6873
+ init_types();
6874
+ init_database();
6875
+ init_projects();
6876
+ });
6877
+
6666
6878
  // src/lib/shared-events.ts
6667
6879
  function taskEventData(task, extra = {}) {
6668
6880
  return {
@@ -6692,6 +6904,69 @@ function taskEventData(task, extra = {}) {
6692
6904
  ...extra
6693
6905
  };
6694
6906
  }
6907
+ function taskEventMetadata(task) {
6908
+ const metadata = {
6909
+ package: "@hasna/todos",
6910
+ todos_event_schema_version: 1,
6911
+ task_id: task.id,
6912
+ task_short_id: task.short_id,
6913
+ project_id: task.project_id,
6914
+ task_list_id: task.task_list_id,
6915
+ working_dir: task.working_dir
6916
+ };
6917
+ try {
6918
+ const project = task.project_id ? getProject(task.project_id) : null;
6919
+ const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
6920
+ if (project) {
6921
+ metadata.project_id = project.id;
6922
+ metadata.project_name = project.name;
6923
+ metadata.project_path = projectPath;
6924
+ metadata.project_canonical_path = project.path;
6925
+ metadata.project_default_task_list_slug = project.task_list_id;
6926
+ metadata.root_project_id = inferRootProjectId(project);
6927
+ } else if (projectPath) {
6928
+ metadata.project_path = projectPath;
6929
+ metadata.project_canonical_path = projectPath;
6930
+ }
6931
+ if (projectPath) {
6932
+ metadata.project_kind = classifyProjectKind(projectPath);
6933
+ metadata.project_is_worktree = isWorktreePath(projectPath);
6934
+ if (typeof task.metadata.route_enabled === "boolean") {
6935
+ metadata.route_enabled = task.metadata.route_enabled;
6936
+ }
6937
+ metadata.working_dir = task.working_dir ?? projectPath;
6938
+ }
6939
+ 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;
6940
+ if (taskList) {
6941
+ metadata.task_list_id = taskList.id;
6942
+ metadata.task_list_slug = taskList.slug;
6943
+ metadata.task_list_name = taskList.name;
6944
+ metadata.task_list_project_id = taskList.project_id;
6945
+ metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
6946
+ }
6947
+ } catch {}
6948
+ return metadata;
6949
+ }
6950
+ function classifyProjectKind(path) {
6951
+ return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
6952
+ }
6953
+ function isWorktreePath(path) {
6954
+ return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
6955
+ }
6956
+ function inferRootProjectId(project) {
6957
+ return isWorktreePath(project.path) ? null : project.id;
6958
+ }
6959
+ function readMachineLocalPath(project) {
6960
+ const machineId = process.env["TODOS_MACHINE_ID"];
6961
+ if (!machineId)
6962
+ return null;
6963
+ try {
6964
+ const row = getDatabase().query("SELECT path FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(project.id, machineId);
6965
+ return row?.path ?? null;
6966
+ } catch {
6967
+ return null;
6968
+ }
6969
+ }
6695
6970
  async function emitSharedTaskEvent(input) {
6696
6971
  const data = taskEventData(input.task, input.data);
6697
6972
  await new EventsClient().emit({
@@ -6702,12 +6977,7 @@ async function emitSharedTaskEvent(input) {
6702
6977
  message: input.message ?? `${input.type}: ${input.task.title}`,
6703
6978
  data,
6704
6979
  dedupeKey: input.dedupeKey ?? `${input.type}:${input.task.id}:${input.task.version}`,
6705
- metadata: {
6706
- package: "@hasna/todos",
6707
- task_id: input.task.id,
6708
- project_id: input.task.project_id,
6709
- task_list_id: input.task.task_list_id
6710
- }
6980
+ metadata: taskEventMetadata(input.task)
6711
6981
  }, { deliver: true, dedupe: true });
6712
6982
  }
6713
6983
  function emitSharedTaskEventQuiet(input) {
@@ -6718,6 +6988,9 @@ function emitSharedTaskEventQuiet(input) {
6718
6988
  var SOURCE = "todos";
6719
6989
  var init_shared_events = __esm(() => {
6720
6990
  init_dist();
6991
+ init_database();
6992
+ init_projects();
6993
+ init_task_lists();
6721
6994
  });
6722
6995
 
6723
6996
  // src/lib/secret-redaction.ts
@@ -7275,6 +7548,17 @@ function replaceTaskTags(taskId, tags, db) {
7275
7548
  db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
7276
7549
  insertTaskTags(taskId, tags, db);
7277
7550
  }
7551
+ function addMetadataConditions(metadata, conditions, params) {
7552
+ if (!metadata)
7553
+ return;
7554
+ for (const [key, value] of Object.entries(metadata)) {
7555
+ if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
7556
+ throw new Error(`Invalid metadata filter key: ${key}`);
7557
+ }
7558
+ conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
7559
+ params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
7560
+ }
7561
+ }
7278
7562
  function createTask(input, db) {
7279
7563
  const d = db || getDatabase();
7280
7564
  const timestamp = now();
@@ -7457,6 +7741,7 @@ function listTasks(filter = {}, db) {
7457
7741
  params.push(filter.task_type);
7458
7742
  }
7459
7743
  }
7744
+ addMetadataConditions(filter.metadata, conditions, params);
7460
7745
  const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
7461
7746
  if (filter.cursor) {
7462
7747
  try {
@@ -7481,6 +7766,54 @@ function listTasks(filter = {}, db) {
7481
7766
  const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
7482
7767
  return rows.map(rowToTask);
7483
7768
  }
7769
+ function getTaskByFingerprint(fingerprint, db) {
7770
+ const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
7771
+ return tasks[0] ?? null;
7772
+ }
7773
+ function mergeTaskMetadata(current, next, fingerprint) {
7774
+ return {
7775
+ ...current,
7776
+ ...next ?? {},
7777
+ fingerprint
7778
+ };
7779
+ }
7780
+ function upsertTaskByFingerprint(input, db) {
7781
+ const d = db || getDatabase();
7782
+ const fingerprint = input.fingerprint.trim();
7783
+ if (!fingerprint)
7784
+ throw new Error("fingerprint is required");
7785
+ const existing = getTaskByFingerprint(fingerprint, d);
7786
+ const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
7787
+ if (!existing) {
7788
+ const task2 = createTask({ ...input, metadata }, d);
7789
+ return { task: task2, created: true };
7790
+ }
7791
+ const task = updateTask(existing.id, {
7792
+ version: existing.version,
7793
+ title: input.title,
7794
+ description: input.description,
7795
+ status: input.status,
7796
+ priority: input.priority,
7797
+ project_id: input.project_id,
7798
+ assigned_to: input.assigned_to,
7799
+ working_dir: input.working_dir,
7800
+ plan_id: input.plan_id,
7801
+ task_list_id: input.task_list_id,
7802
+ tags: input.tags,
7803
+ metadata,
7804
+ due_at: input.due_at,
7805
+ estimated_minutes: input.estimated_minutes,
7806
+ sla_minutes: input.sla_minutes,
7807
+ confidence: input.confidence,
7808
+ retry_count: input.retry_count,
7809
+ max_retries: input.max_retries,
7810
+ retry_after: input.retry_after,
7811
+ requires_approval: input.requires_approval,
7812
+ recurrence_rule: input.recurrence_rule,
7813
+ task_type: input.task_type
7814
+ }, d);
7815
+ return { task, created: false };
7816
+ }
7484
7817
  function countTasks(filter = {}, db) {
7485
7818
  const d = db || getDatabase();
7486
7819
  const conditions = [];
@@ -7544,6 +7877,7 @@ function countTasks(filter = {}, db) {
7544
7877
  conditions.push("task_list_id = ?");
7545
7878
  params.push(filter.task_list_id);
7546
7879
  }
7880
+ addMetadataConditions(filter.metadata, conditions, params);
7547
7881
  if (!filter.include_archived) {
7548
7882
  conditions.push("archived_at IS NULL");
7549
7883
  }
@@ -7594,6 +7928,10 @@ function updateTask(id, input, db) {
7594
7928
  sets.push("assigned_to = ?");
7595
7929
  params.push(input.assigned_to);
7596
7930
  }
7931
+ if (input.working_dir !== undefined) {
7932
+ sets.push("working_dir = ?");
7933
+ params.push(input.working_dir);
7934
+ }
7597
7935
  if (input.tags !== undefined) {
7598
7936
  sets.push("tags = ?");
7599
7937
  params.push(JSON.stringify(input.tags));
@@ -7682,6 +8020,8 @@ function updateTask(id, input, db) {
7682
8020
  logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
7683
8021
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
7684
8022
  logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
8023
+ if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
8024
+ logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
7685
8025
  if (input.approved_by !== undefined)
7686
8026
  logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
7687
8027
  const updatedTask = {
@@ -7717,6 +8057,10 @@ function updateTask(id, input, db) {
7717
8057
  if (input.approved_by !== undefined) {
7718
8058
  emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
7719
8059
  }
8060
+ const updatePayload = taskEventData(updatedTask);
8061
+ dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
8062
+ emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
8063
+ emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
7720
8064
  return updatedTask;
7721
8065
  }
7722
8066
  function deleteTask(id, db) {
@@ -10715,17 +11059,22 @@ var init_task_commits = __esm(() => {
10715
11059
  var exports_task_runs = {};
10716
11060
  __export(exports_task_runs, {
10717
11061
  verifyTaskRunArtifacts: () => verifyTaskRunArtifacts,
11062
+ summarizeTaskRun: () => summarizeTaskRun,
10718
11063
  startTaskRun: () => startTaskRun,
10719
11064
  resolveTaskRunId: () => resolveTaskRunId,
10720
11065
  redactEvidenceText: () => redactEvidenceText,
10721
11066
  listTaskRuns: () => listTaskRuns,
10722
11067
  getTaskRunLedger: () => getTaskRunLedger,
10723
11068
  getTaskRun: () => getTaskRun,
11069
+ finishTaskRunTransaction: () => finishTaskRunTransaction,
10724
11070
  finishTaskRun: () => finishTaskRun,
11071
+ findTaskRunByTransactionKey: () => findTaskRunByTransactionKey,
11072
+ beginTaskRunTransaction: () => beginTaskRunTransaction,
10725
11073
  addTaskRunFile: () => addTaskRunFile,
10726
11074
  addTaskRunEvent: () => addTaskRunEvent,
10727
11075
  addTaskRunCommand: () => addTaskRunCommand,
10728
- addTaskRunArtifact: () => addTaskRunArtifact
11076
+ addTaskRunArtifact: () => addTaskRunArtifact,
11077
+ LOOP_RUN_TRANSACTION_SCHEMA_VERSION: () => LOOP_RUN_TRANSACTION_SCHEMA_VERSION
10729
11078
  });
10730
11079
  function parseObject(value) {
10731
11080
  if (!value)
@@ -10749,6 +11098,72 @@ function rowToArtifact(row) {
10749
11098
  function getRunRow(runId, db) {
10750
11099
  return db.query("SELECT * FROM task_runs WHERE id = ?").get(runId);
10751
11100
  }
11101
+ function normalizeTransactionKey(input) {
11102
+ const key = (input.key || input.loop_run_id || input.loop_id || "").trim();
11103
+ if (!key)
11104
+ throw new Error("idempotent run transactions require --key, --loop-run-id, or --loop-id");
11105
+ return key.toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 240);
11106
+ }
11107
+ function loopTransactionMetadata(record) {
11108
+ const value = record.metadata["loop_transaction"];
11109
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
11110
+ }
11111
+ function runKey(record) {
11112
+ const tx = loopTransactionMetadata(record);
11113
+ const key = tx["idempotency_key"] ?? record.metadata["idempotency_key"];
11114
+ return typeof key === "string" ? key : null;
11115
+ }
11116
+ function loopId(record) {
11117
+ const tx = loopTransactionMetadata(record);
11118
+ const value = tx["loop_id"] ?? record.metadata["loop_id"];
11119
+ return typeof value === "string" ? value : null;
11120
+ }
11121
+ function loopRunId(record) {
11122
+ const tx = loopTransactionMetadata(record);
11123
+ const value = tx["loop_run_id"] ?? record.metadata["loop_run_id"];
11124
+ return typeof value === "string" ? value : null;
11125
+ }
11126
+ function getTaskRunTransactionByKey(key, taskId, db) {
11127
+ if (taskId) {
11128
+ return db.query("SELECT * FROM task_run_transactions WHERE task_id = ? AND key = ?").get(taskId, key);
11129
+ }
11130
+ const rows = db.query("SELECT * FROM task_run_transactions WHERE key = ? ORDER BY created_at DESC LIMIT 2").all(key);
11131
+ if (rows.length > 1)
11132
+ throw new Error(`Run transaction key is ambiguous across tasks: ${key}. Pass task_id.`);
11133
+ return rows[0] ?? null;
11134
+ }
11135
+ function summarizeTaskRun(run) {
11136
+ return {
11137
+ id: run.id,
11138
+ task_id: run.task_id,
11139
+ agent_id: run.agent_id,
11140
+ title: run.title,
11141
+ status: run.status,
11142
+ summary: run.summary,
11143
+ idempotency_key: runKey(run),
11144
+ loop_id: loopId(run),
11145
+ loop_run_id: loopRunId(run),
11146
+ metadata_keys: Object.keys(run.metadata).sort(),
11147
+ started_at: run.started_at,
11148
+ completed_at: run.completed_at,
11149
+ updated_at: run.updated_at
11150
+ };
11151
+ }
11152
+ function findTaskRunByTransactionKey(key, taskId, db) {
11153
+ const d = db || getDatabase();
11154
+ const normalized = normalizeTransactionKey({ key });
11155
+ const transaction = getTaskRunTransactionByKey(normalized, taskId, d);
11156
+ if (transaction?.run_id)
11157
+ return getTaskRun(transaction.run_id, d);
11158
+ return null;
11159
+ }
11160
+ function loopRunCommands(run, key) {
11161
+ return [
11162
+ run ? `todos runs show ${run.id.slice(0, 8)}` : "todos runs list",
11163
+ run ? `todos findings list --task ${run.task_id.slice(0, 8)} --json` : "todos findings list --json",
11164
+ `todos runs begin <task-id> --key ${key} --apply --json`
11165
+ ];
11166
+ }
10752
11167
  function resolveTaskRunId(idOrPrefix, db) {
10753
11168
  const d = db || getDatabase();
10754
11169
  const rows = d.query("SELECT id FROM task_runs WHERE id = ? OR id LIKE ? ORDER BY created_at DESC LIMIT 2").all(idOrPrefix, `${idOrPrefix}%`);
@@ -10767,7 +11182,7 @@ function startTaskRun(input, db) {
10767
11182
  const d = db || getDatabase();
10768
11183
  if (!getTask(input.task_id, d))
10769
11184
  throw new TaskNotFoundError(input.task_id);
10770
- const id = uuid();
11185
+ const id = input.id ?? uuid();
10771
11186
  const timestamp = input.started_at || now();
10772
11187
  if (input.claim && input.agent_id) {
10773
11188
  startTask(input.task_id, input.agent_id, d);
@@ -10805,6 +11220,97 @@ function startTaskRun(input, db) {
10805
11220
  emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
10806
11221
  return run;
10807
11222
  }
11223
+ function beginTaskRunTransaction(input, db) {
11224
+ const d = db || getDatabase();
11225
+ if (!getTask(input.task_id, d))
11226
+ throw new TaskNotFoundError(input.task_id);
11227
+ const timestamp = input.started_at || now();
11228
+ const key = normalizeTransactionKey(input);
11229
+ const existing = findTaskRunByTransactionKey(key, input.task_id, d);
11230
+ const dryRun = !input.apply;
11231
+ if (existing) {
11232
+ return {
11233
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11234
+ local_only: true,
11235
+ dry_run: dryRun,
11236
+ processed_at: timestamp,
11237
+ action: "matched",
11238
+ key,
11239
+ run: summarizeTaskRun(existing),
11240
+ warnings: existing.status === "running" ? [] : [`matched ${existing.status} run`],
11241
+ commands: loopRunCommands(existing, key)
11242
+ };
11243
+ }
11244
+ if (dryRun) {
11245
+ return {
11246
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11247
+ local_only: true,
11248
+ dry_run: true,
11249
+ processed_at: timestamp,
11250
+ action: "preview",
11251
+ key,
11252
+ run: null,
11253
+ warnings: [],
11254
+ commands: loopRunCommands(null, key)
11255
+ };
11256
+ }
11257
+ const metadata = redactValue({
11258
+ ...input.metadata || {},
11259
+ loop_transaction: {
11260
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11261
+ idempotency_key: key,
11262
+ loop_id: input.loop_id ?? null,
11263
+ loop_run_id: input.loop_run_id ?? null,
11264
+ first_seen_at: timestamp
11265
+ },
11266
+ idempotency_key: key
11267
+ });
11268
+ const created = d.transaction(() => {
11269
+ d.run(`INSERT OR IGNORE INTO task_run_transactions (
11270
+ id, task_id, run_id, key, loop_id, loop_run_id, metadata, created_at, updated_at
11271
+ ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?)`, [
11272
+ uuid(),
11273
+ input.task_id,
11274
+ key,
11275
+ input.loop_id ?? null,
11276
+ input.loop_run_id ?? null,
11277
+ JSON.stringify(metadata),
11278
+ timestamp,
11279
+ timestamp
11280
+ ]);
11281
+ const transaction = getTaskRunTransactionByKey(key, input.task_id, d);
11282
+ if (!transaction)
11283
+ throw new Error(`Could not create run transaction for key: ${key}`);
11284
+ if (transaction.run_id) {
11285
+ const existingRun = getTaskRun(transaction.run_id, d);
11286
+ if (existingRun)
11287
+ return { run: existingRun, action: "matched" };
11288
+ }
11289
+ const run = startTaskRun({
11290
+ id: uuid(),
11291
+ task_id: input.task_id,
11292
+ agent_id: input.agent_id,
11293
+ title: input.title,
11294
+ summary: input.summary,
11295
+ metadata,
11296
+ claim: input.claim,
11297
+ started_at: timestamp
11298
+ }, d);
11299
+ d.run("UPDATE task_run_transactions SET run_id = ?, loop_id = COALESCE(?, loop_id), loop_run_id = COALESCE(?, loop_run_id), metadata = ?, updated_at = ? WHERE id = ?", [run.id, input.loop_id ?? null, input.loop_run_id ?? null, JSON.stringify(metadata), timestamp, transaction.id]);
11300
+ return { run, action: "created" };
11301
+ })();
11302
+ return {
11303
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11304
+ local_only: true,
11305
+ dry_run: false,
11306
+ processed_at: timestamp,
11307
+ action: created.action,
11308
+ key,
11309
+ run: summarizeTaskRun(created.run),
11310
+ warnings: [],
11311
+ commands: loopRunCommands(created.run, key)
11312
+ };
11313
+ }
10808
11314
  function addTaskRunEvent(input, db) {
10809
11315
  const d = db || getDatabase();
10810
11316
  const runId = resolveTaskRunId(input.run_id, d);
@@ -10984,6 +11490,66 @@ function finishTaskRun(input, db) {
10984
11490
  });
10985
11491
  return updated;
10986
11492
  }
11493
+ function finishTaskRunTransaction(input, db) {
11494
+ const d = db || getDatabase();
11495
+ const timestamp = input.completed_at || now();
11496
+ const status = input.status || "completed";
11497
+ const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
11498
+ const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
11499
+ if (!run) {
11500
+ throw new Error(input.run_id ? `Run not found: ${input.run_id}` : "runs finish requires a run id or --key");
11501
+ }
11502
+ if (input.task_id && run.task_id !== input.task_id) {
11503
+ throw new Error(`Run ${run.id} belongs to task ${run.task_id}, not ${input.task_id}`);
11504
+ }
11505
+ const resolvedKey = key || runKey(run) || run.id;
11506
+ const dryRun = input.apply === false;
11507
+ if (run.status !== "running") {
11508
+ const conflict = run.status !== status;
11509
+ return {
11510
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11511
+ local_only: true,
11512
+ dry_run: dryRun,
11513
+ processed_at: timestamp,
11514
+ action: conflict ? "conflict" : "matched",
11515
+ key: resolvedKey,
11516
+ run: summarizeTaskRun(run),
11517
+ warnings: conflict ? [`run is already ${run.status}; requested ${status}`] : [],
11518
+ commands: loopRunCommands(run, resolvedKey)
11519
+ };
11520
+ }
11521
+ if (dryRun) {
11522
+ return {
11523
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11524
+ local_only: true,
11525
+ dry_run: true,
11526
+ processed_at: timestamp,
11527
+ action: "preview",
11528
+ key: resolvedKey,
11529
+ run: summarizeTaskRun({ ...run, status, summary: input.summary ?? run.summary, completed_at: timestamp, updated_at: timestamp }),
11530
+ warnings: [],
11531
+ commands: loopRunCommands(run, resolvedKey)
11532
+ };
11533
+ }
11534
+ const finished = finishTaskRun({
11535
+ run_id: run.id,
11536
+ status,
11537
+ summary: input.summary,
11538
+ agent_id: input.agent_id,
11539
+ completed_at: timestamp
11540
+ }, d);
11541
+ return {
11542
+ schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
11543
+ local_only: true,
11544
+ dry_run: false,
11545
+ processed_at: timestamp,
11546
+ action: "finished",
11547
+ key: resolvedKey,
11548
+ run: summarizeTaskRun(finished),
11549
+ warnings: [],
11550
+ commands: loopRunCommands(finished, resolvedKey)
11551
+ };
11552
+ }
10987
11553
  function listTaskRuns(taskId, db) {
10988
11554
  const d = db || getDatabase();
10989
11555
  const rows = taskId ? d.query("SELECT * FROM task_runs WHERE task_id = ? ORDER BY started_at DESC, created_at DESC").all(taskId) : d.query("SELECT * FROM task_runs ORDER BY started_at DESC, created_at DESC LIMIT 100").all();
@@ -11001,6 +11567,7 @@ function getTaskRunLedger(runId, db) {
11001
11567
  const files = d.query("SELECT * FROM task_files WHERE task_id = ? ORDER BY updated_at DESC, path").all(run.task_id);
11002
11568
  return { run, events, commands, artifacts, files };
11003
11569
  }
11570
+ var LOOP_RUN_TRANSACTION_SCHEMA_VERSION = "todos.loop_run_transaction.v1";
11004
11571
  var init_task_runs = __esm(() => {
11005
11572
  init_artifact_store();
11006
11573
  init_event_hooks();
@@ -11389,6 +11956,7 @@ var init_calendar = __esm(() => {
11389
11956
  var exports_tasks = {};
11390
11957
  __export(exports_tasks, {
11391
11958
  watchTask: () => watchTask,
11959
+ upsertTaskByFingerprint: () => upsertTaskByFingerprint,
11392
11960
  updateTaskBoard: () => updateTaskBoard,
11393
11961
  updateTask: () => updateTask,
11394
11962
  unwatchTask: () => unwatchTask,
@@ -11432,6 +12000,7 @@ __export(exports_tasks, {
11432
12000
  getTaskGraph: () => getTaskGraph,
11433
12001
  getTaskDependents: () => getTaskDependents,
11434
12002
  getTaskDependencies: () => getTaskDependencies,
12003
+ getTaskByFingerprint: () => getTaskByFingerprint,
11435
12004
  getTaskBoard: () => getTaskBoard,
11436
12005
  getTask: () => getTask,
11437
12006
  getStatus: () => getStatus,
@@ -11672,6 +12241,62 @@ function parsePriority(value) {
11672
12241
  }
11673
12242
  return value;
11674
12243
  }
12244
+ function parseJsonObject3(value, flag) {
12245
+ if (!value)
12246
+ return;
12247
+ let parsed;
12248
+ try {
12249
+ parsed = JSON.parse(value);
12250
+ } catch (error) {
12251
+ console.error(chalk2.red(`${flag} must be valid JSON: ${error instanceof Error ? error.message : String(error)}`));
12252
+ process.exit(1);
12253
+ }
12254
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
12255
+ console.error(chalk2.red(`${flag} must be a JSON object`));
12256
+ process.exit(1);
12257
+ }
12258
+ return parsed;
12259
+ }
12260
+ function parseJsonValue(value) {
12261
+ if (value === undefined)
12262
+ return;
12263
+ try {
12264
+ return JSON.parse(value);
12265
+ } catch {
12266
+ return value;
12267
+ }
12268
+ }
12269
+ function parseTags(value) {
12270
+ return value ? value.split(",").map((tag) => tag.trim()).filter(Boolean) : undefined;
12271
+ }
12272
+ function buildExpectationMetadata(opts) {
12273
+ const metadata = parseJsonObject3(opts["metadataJson"], "--metadata-json") ?? {};
12274
+ const expectationId = opts["expectationId"];
12275
+ const expectationFingerprint = opts["expectationFingerprint"];
12276
+ const evidencePaths = opts["evidencePaths"];
12277
+ const originLoopId = opts["originLoopId"];
12278
+ const originRunId = opts["originRunId"];
12279
+ const expected = opts["expected"];
12280
+ const observed = opts["observed"];
12281
+ const acceptance = opts["acceptance"];
12282
+ if (expectationId !== undefined)
12283
+ metadata["expectation_id"] = expectationId;
12284
+ if (expectationFingerprint !== undefined)
12285
+ metadata["expectation_fingerprint"] = expectationFingerprint;
12286
+ if (evidencePaths !== undefined)
12287
+ metadata["evidence_paths"] = String(evidencePaths).split(",").map((path) => path.trim()).filter(Boolean);
12288
+ if (originLoopId !== undefined)
12289
+ metadata["origin_loop_id"] = originLoopId;
12290
+ if (originRunId !== undefined)
12291
+ metadata["origin_run_id"] = originRunId;
12292
+ if (expected !== undefined)
12293
+ metadata["expected"] = parseJsonValue(String(expected));
12294
+ if (observed !== undefined)
12295
+ metadata["observed"] = parseJsonValue(String(observed));
12296
+ if (acceptance !== undefined)
12297
+ metadata["acceptance"] = parseJsonValue(String(acceptance));
12298
+ return metadata;
12299
+ }
11675
12300
  function registerTaskCommands(program2) {
11676
12301
  program2.command("add <title>").description("Create a new task").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("--parent <id>", "Parent task ID").option("-t, --tags <tags>", "Comma-separated tags").option("--tag <tags>", "Comma-separated tags (alias for --tags)").option("--plan <id>", "Assign to a plan").option("--assign <agent>", "Assign to agent").option("--status <status>", "Initial status").option("--list <id>", "Task list ID").option("--task-list <id>", "Task list ID (alias for --list)").option("--estimated <minutes>", "Estimated time in minutes").option("--sla-minutes <minutes>", "SLA minutes before unfinished work is escalated").option("--sla <minutes>", "Alias for --sla-minutes").option("--approval", "Require approval before completion").option("--recurrence <rule>", "Recurrence rule, e.g. 'every day', 'every weekday', 'every 2 weeks'").option("--due <date>", "Due date (ISO string or YYYY-MM-DD)").option("--reason <text>", "Why this task exists").option("--project <id>", "Assign to project by ID or slug (overrides auto-detect)").action((title, opts) => {
11677
12302
  const globalOpts = program2.opts();
@@ -11688,7 +12313,7 @@ function registerTaskCommands(program2) {
11688
12313
  }
11689
12314
  return id;
11690
12315
  })() : undefined;
11691
- const task = createTask({
12316
+ const task2 = createTask({
11692
12317
  title,
11693
12318
  description: opts.description,
11694
12319
  priority: parsePriority(opts.priority),
@@ -11710,10 +12335,53 @@ function registerTaskCommands(program2) {
11710
12335
  reason: opts.reason
11711
12336
  });
11712
12337
  if (globalOpts.json) {
11713
- output(task, true);
12338
+ output(task2, true);
11714
12339
  } else {
11715
12340
  console.log(chalk2.green("Task created:"));
11716
- console.log(formatTaskLine(task));
12341
+ console.log(formatTaskLine(task2));
12342
+ }
12343
+ });
12344
+ const task = program2.command("task").description("Task subcommands for deterministic automation");
12345
+ task.command("upsert").description("Create or update a task by stable metadata fingerprint").requiredOption("--fingerprint <key>", "Stable dedupe fingerprint").requiredOption("--title <text>", "Task title").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("-s, --status <status>", "Task status").option("--list <id>", "Task list ID").option("--task-list <id>", "Task list ID (alias for --list)").option("-t, --tags <tags>", "Comma-separated tags").option("--tag <tags>", "Comma-separated tags (alias for --tags)").option("--metadata-json <json>", "JSON object merged into task metadata").option("--working-dir <path>", "Working directory to store on create/update").option("--project <id>", "Assign to project by ID, slug, or path").option("--assign <agent>", "Assign to agent").option("--expectation-id <id>", "Expectation metadata ID").option("--expectation-fingerprint <key>", "Expectation metadata fingerprint").option("--evidence-paths <paths>", "Comma-separated evidence paths").option("--origin-loop-id <id>", "Origin loop ID").option("--origin-run-id <id>", "Origin run ID").option("--expected <json-or-text>", "Expected value metadata").option("--observed <json-or-text>", "Observed value metadata").option("--acceptance <json-or-text>", "Acceptance metadata").action((opts) => {
12346
+ const globalOpts = program2.opts();
12347
+ opts.tags = opts.tags || opts.tag;
12348
+ opts.list = opts.list || opts.taskList;
12349
+ const explicitProject = opts.project || globalOpts.project;
12350
+ const projectId = explicitProject ? resolveProjectIdOrSlug(explicitProject) : autoProject(globalOpts);
12351
+ const taskListId = opts.list ? (() => {
12352
+ const db = getDatabase();
12353
+ const id = resolvePartialId(db, "task_lists", opts.list);
12354
+ if (!id) {
12355
+ console.error(chalk2.red(`Could not resolve task list ID: ${opts.list}`));
12356
+ process.exit(1);
12357
+ }
12358
+ return id;
12359
+ })() : undefined;
12360
+ let result;
12361
+ try {
12362
+ result = upsertTaskByFingerprint({
12363
+ fingerprint: opts.fingerprint,
12364
+ title: opts.title,
12365
+ description: opts.description,
12366
+ priority: parsePriority(opts.priority),
12367
+ status: opts.status ? normalizeStatus(opts.status) : undefined,
12368
+ task_list_id: taskListId,
12369
+ tags: parseTags(opts.tags),
12370
+ metadata: buildExpectationMetadata(opts),
12371
+ working_dir: opts.workingDir ? resolve8(opts.workingDir) : process.cwd(),
12372
+ project_id: projectId,
12373
+ assigned_to: opts.assign,
12374
+ agent_id: globalOpts.agent,
12375
+ session_id: globalOpts.session
12376
+ });
12377
+ } catch (e) {
12378
+ handleError(e);
12379
+ }
12380
+ if (globalOpts.json) {
12381
+ output(result, true);
12382
+ } else {
12383
+ console.log(chalk2.green(result.created ? "Task created:" : "Task updated:"));
12384
+ console.log(formatTaskLine(result.task));
11717
12385
  }
11718
12386
  });
11719
12387
  program2.command("list").description("List tasks").option("-s, --status <status>", "Filter by status").option("-p, --priority <priority>", "Filter by priority").option("--assigned <agent>", "Filter by assigned agent").option("--tags <tags>", "Filter by tags (comma-separated)").option("--tag <tags>", "Filter by tags (alias for --tags)").option("-a, --all", "Show all tasks (including completed/cancelled)").option("--list <id>", "Filter by task list ID").option("--task-list <id>", "Filter by task list ID (alias for --list)").option("--project-name <name>", "Filter by project name").option("--agent-name <name>", "Filter by agent name/assigned").option("--sort <field>", "Sort by: updated, created, priority, status").option("--format <fmt>", "Output format: table (default), compact, csv, json").option("--due-today", "Only tasks due today or earlier").option("--overdue", "Only overdue tasks (past due_at)").option("--recurring", "Only recurring tasks").option("--limit <n>", "Max tasks to return").action((opts) => {
@@ -11871,89 +12539,89 @@ function registerTaskCommands(program2) {
11871
12539
  program2.command("show <id>").description("Show full task details").action((id) => {
11872
12540
  const globalOpts = program2.opts();
11873
12541
  const resolvedId = resolveTaskId(id);
11874
- const task = getTaskWithRelations(resolvedId);
11875
- if (!task) {
12542
+ const task2 = getTaskWithRelations(resolvedId);
12543
+ if (!task2) {
11876
12544
  console.error(chalk2.red(`Task not found: ${id}`));
11877
12545
  process.exit(1);
11878
12546
  }
11879
12547
  if (globalOpts.json) {
11880
- output(task, true);
12548
+ output(task2, true);
11881
12549
  return;
11882
12550
  }
11883
12551
  console.log(chalk2.bold(`Task Details:
11884
12552
  `));
11885
- console.log(` ${chalk2.dim("ID:")} ${task.id}`);
11886
- console.log(` ${chalk2.dim("Title:")} ${task.title}`);
11887
- console.log(` ${chalk2.dim("Status:")} ${(statusColors[task.status] || chalk2.white)(task.status)}`);
11888
- console.log(` ${chalk2.dim("Priority:")} ${(priorityColors[task.priority] || chalk2.white)(task.priority)}`);
11889
- if (task.description)
11890
- console.log(` ${chalk2.dim("Desc:")} ${task.description}`);
11891
- if (task.assigned_to)
11892
- console.log(` ${chalk2.dim("Assigned:")} ${task.assigned_to}`);
11893
- if (task.agent_id)
11894
- console.log(` ${chalk2.dim("Agent:")} ${task.agent_id}`);
11895
- if (task.session_id)
11896
- console.log(` ${chalk2.dim("Session:")} ${task.session_id}`);
11897
- if (task.locked_by)
11898
- console.log(` ${chalk2.dim("Locked:")} ${task.locked_by} (at ${task.locked_at})`);
11899
- if (task.requires_approval) {
11900
- const approvalStatus = task.approved_by ? chalk2.green(`approved by ${task.approved_by}`) : chalk2.yellow("pending approval");
12553
+ console.log(` ${chalk2.dim("ID:")} ${task2.id}`);
12554
+ console.log(` ${chalk2.dim("Title:")} ${task2.title}`);
12555
+ console.log(` ${chalk2.dim("Status:")} ${(statusColors[task2.status] || chalk2.white)(task2.status)}`);
12556
+ console.log(` ${chalk2.dim("Priority:")} ${(priorityColors[task2.priority] || chalk2.white)(task2.priority)}`);
12557
+ if (task2.description)
12558
+ console.log(` ${chalk2.dim("Desc:")} ${task2.description}`);
12559
+ if (task2.assigned_to)
12560
+ console.log(` ${chalk2.dim("Assigned:")} ${task2.assigned_to}`);
12561
+ if (task2.agent_id)
12562
+ console.log(` ${chalk2.dim("Agent:")} ${task2.agent_id}`);
12563
+ if (task2.session_id)
12564
+ console.log(` ${chalk2.dim("Session:")} ${task2.session_id}`);
12565
+ if (task2.locked_by)
12566
+ console.log(` ${chalk2.dim("Locked:")} ${task2.locked_by} (at ${task2.locked_at})`);
12567
+ if (task2.requires_approval) {
12568
+ const approvalStatus = task2.approved_by ? chalk2.green(`approved by ${task2.approved_by}`) : chalk2.yellow("pending approval");
11901
12569
  console.log(` ${chalk2.dim("Approval:")} ${approvalStatus}`);
11902
12570
  }
11903
- if (task.estimated_minutes)
11904
- console.log(` ${chalk2.dim("Estimate:")} ${task.estimated_minutes} minutes`);
11905
- if (task.sla_minutes)
11906
- console.log(` ${chalk2.dim("SLA:")} ${task.sla_minutes} minutes`);
11907
- if (task.due_at)
11908
- console.log(` ${chalk2.dim("Due:")} ${task.due_at}`);
11909
- if (task.recurrence_rule)
11910
- console.log(` ${chalk2.dim("Repeats:")} ${task.recurrence_rule}`);
11911
- if (task.project_id)
11912
- console.log(` ${chalk2.dim("Project:")} ${task.project_id}`);
11913
- if (task.plan_id)
11914
- console.log(` ${chalk2.dim("Plan:")} ${task.plan_id}`);
11915
- if (task.working_dir)
11916
- console.log(` ${chalk2.dim("WorkDir:")} ${task.working_dir}`);
11917
- if (task.parent)
11918
- console.log(` ${chalk2.dim("Parent:")} ${task.parent.id.slice(0, 8)} | ${task.parent.title}`);
11919
- if (task.tags.length > 0)
11920
- console.log(` ${chalk2.dim("Tags:")} ${task.tags.join(", ")}`);
11921
- console.log(` ${chalk2.dim("Version:")} ${task.version}`);
11922
- console.log(` ${chalk2.dim("Created:")} ${task.created_at}`);
11923
- if (task.started_at)
11924
- console.log(` ${chalk2.dim("Started:")} ${task.started_at}`);
11925
- if (task.completed_at) {
11926
- console.log(` ${chalk2.dim("Done:")} ${task.completed_at}`);
11927
- if (task.started_at) {
11928
- const dur = Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 60000);
12571
+ if (task2.estimated_minutes)
12572
+ console.log(` ${chalk2.dim("Estimate:")} ${task2.estimated_minutes} minutes`);
12573
+ if (task2.sla_minutes)
12574
+ console.log(` ${chalk2.dim("SLA:")} ${task2.sla_minutes} minutes`);
12575
+ if (task2.due_at)
12576
+ console.log(` ${chalk2.dim("Due:")} ${task2.due_at}`);
12577
+ if (task2.recurrence_rule)
12578
+ console.log(` ${chalk2.dim("Repeats:")} ${task2.recurrence_rule}`);
12579
+ if (task2.project_id)
12580
+ console.log(` ${chalk2.dim("Project:")} ${task2.project_id}`);
12581
+ if (task2.plan_id)
12582
+ console.log(` ${chalk2.dim("Plan:")} ${task2.plan_id}`);
12583
+ if (task2.working_dir)
12584
+ console.log(` ${chalk2.dim("WorkDir:")} ${task2.working_dir}`);
12585
+ if (task2.parent)
12586
+ console.log(` ${chalk2.dim("Parent:")} ${task2.parent.id.slice(0, 8)} | ${task2.parent.title}`);
12587
+ if (task2.tags.length > 0)
12588
+ console.log(` ${chalk2.dim("Tags:")} ${task2.tags.join(", ")}`);
12589
+ console.log(` ${chalk2.dim("Version:")} ${task2.version}`);
12590
+ console.log(` ${chalk2.dim("Created:")} ${task2.created_at}`);
12591
+ if (task2.started_at)
12592
+ console.log(` ${chalk2.dim("Started:")} ${task2.started_at}`);
12593
+ if (task2.completed_at) {
12594
+ console.log(` ${chalk2.dim("Done:")} ${task2.completed_at}`);
12595
+ if (task2.started_at) {
12596
+ const dur = Math.round((new Date(task2.completed_at).getTime() - new Date(task2.started_at).getTime()) / 60000);
11929
12597
  console.log(` ${chalk2.dim("Duration:")} ${dur}m`);
11930
12598
  }
11931
12599
  }
11932
- if (task.subtasks.length > 0) {
12600
+ if (task2.subtasks.length > 0) {
11933
12601
  console.log(chalk2.bold(`
11934
- Subtasks (${task.subtasks.length}):`));
11935
- for (const st of task.subtasks) {
12602
+ Subtasks (${task2.subtasks.length}):`));
12603
+ for (const st of task2.subtasks) {
11936
12604
  console.log(` ${formatTaskLine(st)}`);
11937
12605
  }
11938
12606
  }
11939
- if (task.dependencies.length > 0) {
12607
+ if (task2.dependencies.length > 0) {
11940
12608
  console.log(chalk2.bold(`
11941
- Depends on (${task.dependencies.length}):`));
11942
- for (const dep of task.dependencies) {
12609
+ Depends on (${task2.dependencies.length}):`));
12610
+ for (const dep of task2.dependencies) {
11943
12611
  console.log(` ${formatTaskLine(dep)}`);
11944
12612
  }
11945
12613
  }
11946
- if (task.blocked_by.length > 0) {
12614
+ if (task2.blocked_by.length > 0) {
11947
12615
  console.log(chalk2.bold(`
11948
- Blocks (${task.blocked_by.length}):`));
11949
- for (const b of task.blocked_by) {
12616
+ Blocks (${task2.blocked_by.length}):`));
12617
+ for (const b of task2.blocked_by) {
11950
12618
  console.log(` ${formatTaskLine(b)}`);
11951
12619
  }
11952
12620
  }
11953
- if (task.comments.length > 0) {
12621
+ if (task2.comments.length > 0) {
11954
12622
  console.log(chalk2.bold(`
11955
- Comments (${task.comments.length}):`));
11956
- for (const c of task.comments) {
12623
+ Comments (${task2.comments.length}):`));
12624
+ for (const c of task2.comments) {
11957
12625
  const agent = c.agent_id ? chalk2.cyan(`[${c.agent_id}] `) : "";
11958
12626
  console.log(` ${agent}${chalk2.dim(c.created_at)}: ${c.content}`);
11959
12627
  }
@@ -11972,8 +12640,8 @@ function registerTaskCommands(program2) {
11972
12640
  console.error(chalk2.red("No task ID given and no active task found. Pass an ID or use --agent."));
11973
12641
  process.exit(1);
11974
12642
  }
11975
- const task = getTaskWithRelations(resolvedId);
11976
- if (!task) {
12643
+ const task2 = getTaskWithRelations(resolvedId);
12644
+ if (!task2) {
11977
12645
  console.error(chalk2.red(`Task not found: ${id || resolvedId}`));
11978
12646
  process.exit(1);
11979
12647
  }
@@ -11981,55 +12649,55 @@ function registerTaskCommands(program2) {
11981
12649
  const { listTaskFiles: listTaskFiles2 } = await Promise.resolve().then(() => (init_task_files(), exports_task_files));
11982
12650
  const { getTaskCommits: getTaskCommits2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
11983
12651
  try {
11984
- task.files = listTaskFiles2(task.id);
12652
+ task2.files = listTaskFiles2(task2.id);
11985
12653
  } catch (e) {
11986
12654
  console.error(chalk2.dim(`Warning: could not load task files: ${e instanceof Error ? e.message : String(e)}`));
11987
12655
  }
11988
12656
  try {
11989
- task.commits = getTaskCommits2(task.id);
12657
+ task2.commits = getTaskCommits2(task2.id);
11990
12658
  } catch (e) {
11991
12659
  console.error(chalk2.dim(`Warning: could not load task commits: ${e instanceof Error ? e.message : String(e)}`));
11992
12660
  }
11993
- output(task, true);
12661
+ output(task2, true);
11994
12662
  return;
11995
12663
  }
11996
- const sid = task.short_id || task.id.slice(0, 8);
11997
- const statusColor = statusColors[task.status] || chalk2.white;
11998
- const prioColor = priorityColors[task.priority] || chalk2.white;
12664
+ const sid = task2.short_id || task2.id.slice(0, 8);
12665
+ const statusColor = statusColors[task2.status] || chalk2.white;
12666
+ const prioColor = priorityColors[task2.priority] || chalk2.white;
11999
12667
  console.log(chalk2.bold(`
12000
- ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${task.title}
12668
+ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${task2.title}
12001
12669
  `));
12002
- if (task.description) {
12670
+ if (task2.description) {
12003
12671
  console.log(chalk2.dim("Description:"));
12004
- console.log(` ${task.description}
12672
+ console.log(` ${task2.description}
12005
12673
  `);
12006
12674
  }
12007
- if (task.assigned_to)
12008
- console.log(` ${chalk2.dim("Assigned:")} ${task.assigned_to}`);
12009
- if (task.locked_by)
12010
- console.log(` ${chalk2.dim("Locked by:")} ${task.locked_by}`);
12011
- if (task.project_id)
12012
- console.log(` ${chalk2.dim("Project:")} ${task.project_id}`);
12013
- if (task.plan_id)
12014
- console.log(` ${chalk2.dim("Plan:")} ${task.plan_id}`);
12015
- if (task.started_at)
12016
- console.log(` ${chalk2.dim("Started:")} ${task.started_at}`);
12017
- if (task.completed_at) {
12018
- console.log(` ${chalk2.dim("Completed:")} ${task.completed_at}`);
12019
- if (task.started_at) {
12020
- const dur = Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 60000);
12675
+ if (task2.assigned_to)
12676
+ console.log(` ${chalk2.dim("Assigned:")} ${task2.assigned_to}`);
12677
+ if (task2.locked_by)
12678
+ console.log(` ${chalk2.dim("Locked by:")} ${task2.locked_by}`);
12679
+ if (task2.project_id)
12680
+ console.log(` ${chalk2.dim("Project:")} ${task2.project_id}`);
12681
+ if (task2.plan_id)
12682
+ console.log(` ${chalk2.dim("Plan:")} ${task2.plan_id}`);
12683
+ if (task2.started_at)
12684
+ console.log(` ${chalk2.dim("Started:")} ${task2.started_at}`);
12685
+ if (task2.completed_at) {
12686
+ console.log(` ${chalk2.dim("Completed:")} ${task2.completed_at}`);
12687
+ if (task2.started_at) {
12688
+ const dur = Math.round((new Date(task2.completed_at).getTime() - new Date(task2.started_at).getTime()) / 60000);
12021
12689
  console.log(` ${chalk2.dim("Duration:")} ${dur}m`);
12022
12690
  }
12023
12691
  }
12024
- if (task.estimated_minutes)
12025
- console.log(` ${chalk2.dim("Estimate:")} ${task.estimated_minutes}m`);
12026
- if (task.tags.length > 0)
12027
- console.log(` ${chalk2.dim("Tags:")} ${task.tags.join(", ")}`);
12028
- const unfinishedDeps = task.dependencies.filter((d) => d.status !== "completed" && d.status !== "cancelled");
12029
- if (task.dependencies.length > 0) {
12692
+ if (task2.estimated_minutes)
12693
+ console.log(` ${chalk2.dim("Estimate:")} ${task2.estimated_minutes}m`);
12694
+ if (task2.tags.length > 0)
12695
+ console.log(` ${chalk2.dim("Tags:")} ${task2.tags.join(", ")}`);
12696
+ const unfinishedDeps = task2.dependencies.filter((d) => d.status !== "completed" && d.status !== "cancelled");
12697
+ if (task2.dependencies.length > 0) {
12030
12698
  console.log(chalk2.bold(`
12031
- Depends on (${task.dependencies.length}):`));
12032
- for (const dep of task.dependencies) {
12699
+ Depends on (${task2.dependencies.length}):`));
12700
+ for (const dep of task2.dependencies) {
12033
12701
  const blocked = dep.status !== "completed" && dep.status !== "cancelled";
12034
12702
  const icon = blocked ? chalk2.red("\u2717") : chalk2.green("\u2713");
12035
12703
  console.log(` ${icon} ${formatTaskLine(dep)}`);
@@ -12039,21 +12707,21 @@ ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${ta
12039
12707
  console.log(chalk2.red(`
12040
12708
  BLOCKED by ${unfinishedDeps.length} unfinished dep(s)`));
12041
12709
  }
12042
- if (task.blocked_by.length > 0) {
12710
+ if (task2.blocked_by.length > 0) {
12043
12711
  console.log(chalk2.bold(`
12044
- Blocks (${task.blocked_by.length}):`));
12045
- for (const b of task.blocked_by)
12712
+ Blocks (${task2.blocked_by.length}):`));
12713
+ for (const b of task2.blocked_by)
12046
12714
  console.log(` ${formatTaskLine(b)}`);
12047
12715
  }
12048
- if (task.subtasks.length > 0) {
12716
+ if (task2.subtasks.length > 0) {
12049
12717
  console.log(chalk2.bold(`
12050
- Subtasks (${task.subtasks.length}):`));
12051
- for (const st of task.subtasks)
12718
+ Subtasks (${task2.subtasks.length}):`));
12719
+ for (const st of task2.subtasks)
12052
12720
  console.log(` ${formatTaskLine(st)}`);
12053
12721
  }
12054
12722
  try {
12055
12723
  const { listTaskFiles: listTaskFiles2 } = await Promise.resolve().then(() => (init_task_files(), exports_task_files));
12056
- const files = listTaskFiles2(task.id);
12724
+ const files = listTaskFiles2(task2.id);
12057
12725
  if (files.length > 0) {
12058
12726
  console.log(chalk2.bold(`
12059
12727
  Files (${files.length}):`));
@@ -12065,7 +12733,7 @@ ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${ta
12065
12733
  }
12066
12734
  try {
12067
12735
  const { getTaskCommits: getTaskCommits2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
12068
- const commits = getTaskCommits2(task.id);
12736
+ const commits = getTaskCommits2(task2.id);
12069
12737
  if (commits.length > 0) {
12070
12738
  console.log(chalk2.bold(`
12071
12739
  Commits (${commits.length}):`));
@@ -12075,19 +12743,19 @@ ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${ta
12075
12743
  } catch (e) {
12076
12744
  console.error(chalk2.dim(`Warning: could not load task commits: ${e instanceof Error ? e.message : String(e)}`));
12077
12745
  }
12078
- if (task.comments.length > 0) {
12746
+ if (task2.comments.length > 0) {
12079
12747
  console.log(chalk2.bold(`
12080
- Comments (${task.comments.length}):`));
12081
- for (const c of task.comments) {
12748
+ Comments (${task2.comments.length}):`));
12749
+ for (const c of task2.comments) {
12082
12750
  const agent = c.agent_id ? chalk2.cyan(`[${c.agent_id}] `) : "";
12083
12751
  console.log(` ${agent}${chalk2.dim(c.created_at)}: ${c.content}`);
12084
12752
  }
12085
12753
  }
12086
- if (task.checklist && task.checklist.length > 0) {
12087
- const done = task.checklist.filter((c) => c.checked).length;
12754
+ if (task2.checklist && task2.checklist.length > 0) {
12755
+ const done = task2.checklist.filter((c) => c.checked).length;
12088
12756
  console.log(chalk2.bold(`
12089
- Checklist (${done}/${task.checklist.length}):`));
12090
- for (const item of task.checklist) {
12757
+ Checklist (${done}/${task2.checklist.length}):`));
12758
+ for (const item of task2.checklist) {
12091
12759
  const icon = item.checked ? chalk2.green("\u2611") : chalk2.dim("\u2610");
12092
12760
  console.log(` ${icon} ${item.text || item.title}`);
12093
12761
  }
@@ -12140,9 +12808,9 @@ ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${ta
12140
12808
  return resolved;
12141
12809
  })() : undefined;
12142
12810
  const planId = opts.plan ? resolvePlanId(opts.plan) : opts.clearPlan ? null : undefined;
12143
- let task;
12811
+ let task2;
12144
12812
  try {
12145
- task = updateTask(resolvedId, {
12813
+ task2 = updateTask(resolvedId, {
12146
12814
  version: current.version,
12147
12815
  title: opts.title,
12148
12816
  description: opts.description,
@@ -12162,10 +12830,10 @@ ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${ta
12162
12830
  handleError(e);
12163
12831
  }
12164
12832
  if (globalOpts.json) {
12165
- output(task, true);
12833
+ output(task2, true);
12166
12834
  } else {
12167
12835
  console.log(chalk2.green("Task updated:"));
12168
- console.log(formatTaskLine(task));
12836
+ console.log(formatTaskLine(task2));
12169
12837
  }
12170
12838
  });
12171
12839
  program2.command("done <id>").description("Mark a task as completed").option("--attach-ids <ids>", "Comma-separated @hasna/attachments IDs to link as evidence").option("--files-changed <files>", "Comma-separated list of files changed").option("--test-results <results>", "Test results summary").option("--commit-hash <hash>", "Git commit hash").option("--notes <notes>", "Completion notes").option("--confidence <0-1>", "Agent's confidence 0.0-1.0 that the task is fully complete (default: 1.0, <0.7 flagged for review)").action((id, opts) => {
@@ -12175,37 +12843,37 @@ ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${ta
12175
12843
  const filesChanged = opts.filesChanged ? opts.filesChanged.split(",").map((s) => s.trim()) : undefined;
12176
12844
  const confidence = opts.confidence !== undefined ? parseFloat(opts.confidence) : undefined;
12177
12845
  const evidence = attachmentIds || filesChanged || opts.testResults || opts.commitHash || opts.notes ? { attachment_ids: attachmentIds, files_changed: filesChanged, test_results: opts.testResults, commit_hash: opts.commitHash, notes: opts.notes } : undefined;
12178
- let task;
12846
+ let task2;
12179
12847
  try {
12180
- task = completeTask(resolvedId, globalOpts.agent, undefined, { ...evidence, confidence });
12848
+ task2 = completeTask(resolvedId, globalOpts.agent, undefined, { ...evidence, confidence });
12181
12849
  } catch (e) {
12182
12850
  handleError(e);
12183
12851
  }
12184
12852
  if (globalOpts.json) {
12185
- output(task, true);
12853
+ output(task2, true);
12186
12854
  } else {
12187
12855
  console.log(chalk2.green("Task completed:"));
12188
- console.log(formatTaskLine(task));
12856
+ console.log(formatTaskLine(task2));
12189
12857
  }
12190
12858
  });
12191
12859
  program2.command("approve <id>").description("Approve a task that requires approval").action((id) => {
12192
12860
  const globalOpts = program2.opts();
12193
12861
  const resolvedId = resolveTaskId(id);
12194
- const task = getTask(resolvedId);
12195
- if (!task) {
12862
+ const task2 = getTask(resolvedId);
12863
+ if (!task2) {
12196
12864
  console.error(chalk2.red(`Task not found: ${id}`));
12197
12865
  process.exit(1);
12198
12866
  }
12199
- if (!task.requires_approval) {
12867
+ if (!task2.requires_approval) {
12200
12868
  console.log(chalk2.yellow("This task does not require approval."));
12201
12869
  return;
12202
12870
  }
12203
- if (task.approved_by) {
12204
- console.log(chalk2.yellow(`Already approved by ${task.approved_by}.`));
12871
+ if (task2.approved_by) {
12872
+ console.log(chalk2.yellow(`Already approved by ${task2.approved_by}.`));
12205
12873
  return;
12206
12874
  }
12207
12875
  try {
12208
- const updated = updateTask(resolvedId, { approved_by: globalOpts.agent || "cli", version: task.version });
12876
+ const updated = updateTask(resolvedId, { approved_by: globalOpts.agent || "cli", version: task2.version });
12209
12877
  if (globalOpts.json) {
12210
12878
  output(updated, true);
12211
12879
  } else {
@@ -12220,17 +12888,17 @@ ${chalk2.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${ta
12220
12888
  const globalOpts = program2.opts();
12221
12889
  const agentId = globalOpts.agent || "cli";
12222
12890
  const resolvedId = resolveTaskId(id);
12223
- let task;
12891
+ let task2;
12224
12892
  try {
12225
- task = startTask(resolvedId, agentId);
12893
+ task2 = startTask(resolvedId, agentId);
12226
12894
  } catch (e) {
12227
12895
  handleError(e);
12228
12896
  }
12229
12897
  if (globalOpts.json) {
12230
- output(task, true);
12898
+ output(task2, true);
12231
12899
  } else {
12232
12900
  console.log(chalk2.green(`Task started by ${agentId}:`));
12233
- console.log(formatTaskLine(task));
12901
+ console.log(formatTaskLine(task2));
12234
12902
  }
12235
12903
  });
12236
12904
  program2.command("lock <id>").description("Acquire exclusive lock on a task").action((id) => {
@@ -13403,7 +14071,7 @@ function likePattern(query) {
13403
14071
  return null;
13404
14072
  return `%${trimmed}%`;
13405
14073
  }
13406
- function parseJsonObject3(value) {
14074
+ function parseJsonObject4(value) {
13407
14075
  if (!value)
13408
14076
  return {};
13409
14077
  if (typeof value === "object" && !Array.isArray(value))
@@ -13418,7 +14086,7 @@ function parseJsonObject3(value) {
13418
14086
  }
13419
14087
  }
13420
14088
  function rowToTaskRun(row) {
13421
- return { ...row, metadata: parseJsonObject3(row.metadata) };
14089
+ return { ...row, metadata: parseJsonObject4(row.metadata) };
13422
14090
  }
13423
14091
  function taskMatchesSavedFilters(task, filters, db) {
13424
14092
  if (filters.plan_id && task.plan_id !== filters.plan_id)
@@ -14207,90 +14875,6 @@ var init_sync = __esm(() => {
14207
14875
  init_config();
14208
14876
  });
14209
14877
 
14210
- // src/db/task-lists.ts
14211
- function rowToTaskList(row) {
14212
- return {
14213
- ...row,
14214
- metadata: JSON.parse(row.metadata || "{}")
14215
- };
14216
- }
14217
- function createTaskList(input, db) {
14218
- const d = db || getDatabase();
14219
- const id = uuid();
14220
- const timestamp = now();
14221
- const slug = input.slug || slugify(input.name);
14222
- if (!input.project_id) {
14223
- const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
14224
- if (existing) {
14225
- throw new Error(`Standalone task list with slug "${slug}" already exists`);
14226
- }
14227
- }
14228
- d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
14229
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
14230
- return getTaskList(id, d);
14231
- }
14232
- function getTaskList(id, db) {
14233
- const d = db || getDatabase();
14234
- const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
14235
- return row ? rowToTaskList(row) : null;
14236
- }
14237
- function getTaskListBySlug(slug, projectId, db) {
14238
- const d = db || getDatabase();
14239
- let row;
14240
- if (projectId) {
14241
- row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
14242
- } else {
14243
- row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
14244
- }
14245
- return row ? rowToTaskList(row) : null;
14246
- }
14247
- function listTaskLists(projectId, db) {
14248
- const d = db || getDatabase();
14249
- if (projectId) {
14250
- return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
14251
- }
14252
- return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
14253
- }
14254
- function updateTaskList(id, input, db) {
14255
- const d = db || getDatabase();
14256
- const existing = getTaskList(id, d);
14257
- if (!existing)
14258
- throw new TaskListNotFoundError(id);
14259
- const sets = ["updated_at = ?"];
14260
- const params = [now()];
14261
- if (input.name !== undefined) {
14262
- sets.push("name = ?");
14263
- params.push(input.name);
14264
- }
14265
- if (input.description !== undefined) {
14266
- sets.push("description = ?");
14267
- params.push(input.description);
14268
- }
14269
- if (input.metadata !== undefined) {
14270
- sets.push("metadata = ?");
14271
- params.push(JSON.stringify(input.metadata));
14272
- }
14273
- params.push(id);
14274
- d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
14275
- return getTaskList(id, d);
14276
- }
14277
- function deleteTaskList(id, db) {
14278
- const d = db || getDatabase();
14279
- return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
14280
- }
14281
- function ensureTaskList(name, slug, projectId, db) {
14282
- const d = db || getDatabase();
14283
- const existing = getTaskListBySlug(slug, projectId, d);
14284
- if (existing)
14285
- return existing;
14286
- return createTaskList({ name, slug, project_id: projectId }, d);
14287
- }
14288
- var init_task_lists = __esm(() => {
14289
- init_types();
14290
- init_database();
14291
- init_projects();
14292
- });
14293
-
14294
14878
  // src/lib/project-bootstrap.ts
14295
14879
  var exports_project_bootstrap = {};
14296
14880
  __export(exports_project_bootstrap, {
@@ -14865,7 +15449,7 @@ function packageSource(version) {
14865
15449
  function emptyCounts() {
14866
15450
  return Object.fromEntries(dataKeys.map((key) => [key, 0]));
14867
15451
  }
14868
- function parseJsonObject4(value) {
15452
+ function parseJsonObject5(value) {
14869
15453
  if (!value)
14870
15454
  return {};
14871
15455
  if (typeof value === "object" && !Array.isArray(value))
@@ -14911,34 +15495,34 @@ function rowToTask3(row) {
14911
15495
  return {
14912
15496
  ...row,
14913
15497
  tags: parseJsonArray2(row.tags),
14914
- metadata: parseJsonObject4(row.metadata),
15498
+ metadata: parseJsonObject5(row.metadata),
14915
15499
  requires_approval: Boolean(row.requires_approval)
14916
15500
  };
14917
15501
  }
14918
15502
  function rowToTaskList2(row) {
14919
- return { ...row, metadata: parseJsonObject4(row.metadata) };
15503
+ return { ...row, metadata: parseJsonObject5(row.metadata) };
14920
15504
  }
14921
15505
  function rowWithMetadata(row) {
14922
- return { ...row, metadata: parseJsonObject4(row.metadata) };
15506
+ return { ...row, metadata: parseJsonObject5(row.metadata) };
14923
15507
  }
14924
15508
  function rowToRunEvent(row) {
14925
- return { ...row, data: parseJsonObject4(row.data) };
15509
+ return { ...row, data: parseJsonObject5(row.data) };
14926
15510
  }
14927
15511
  function rowToCommit2(row) {
14928
15512
  return { ...row, files_changed: row.files_changed ? parseJsonArray2(row.files_changed) : null };
14929
15513
  }
14930
15514
  function rowToSavedView(row) {
14931
- return { ...row, filters: parseJsonObject4(row.filters) };
15515
+ return { ...row, filters: parseJsonObject5(row.filters) };
14932
15516
  }
14933
15517
  function rowToTaskBoard2(row) {
14934
15518
  return {
14935
15519
  ...row,
14936
15520
  lanes: parseJsonArray2(row.lanes),
14937
- filters: parseJsonObject4(row.filters)
15521
+ filters: parseJsonObject5(row.filters)
14938
15522
  };
14939
15523
  }
14940
15524
  function rowToCalendarItem2(row) {
14941
- return { ...row, metadata: parseJsonObject4(row.metadata) };
15525
+ return { ...row, metadata: parseJsonObject5(row.metadata) };
14942
15526
  }
14943
15527
  function bridgeStats(data) {
14944
15528
  return Object.fromEntries(dataKeys.map((key) => [key, data[key].length]));
@@ -18285,6 +18869,7 @@ var init_token_utils = __esm(() => {
18285
18869
  "add_task_run_event",
18286
18870
  "add_task_run_file",
18287
18871
  "acknowledge_handoff",
18872
+ "begin_task_run_transaction",
18288
18873
  "build_local_report",
18289
18874
  "cancel_agent_run_dispatch",
18290
18875
  "finish_task_run",
@@ -18332,6 +18917,7 @@ var init_token_utils = __esm(() => {
18332
18917
  "list_local_snapshots",
18333
18918
  "list_retrospectives",
18334
18919
  "list_risks",
18920
+ "list_task_findings",
18335
18921
  "list_task_runs",
18336
18922
  "list_verification_providers",
18337
18923
  "merge_duplicate_task",
@@ -18340,6 +18926,7 @@ var init_token_utils = __esm(() => {
18340
18926
  "remove_review_routing_rule",
18341
18927
  "restore_local_backup",
18342
18928
  "retry_agent_run_dispatch",
18929
+ "resolve_missing_task_findings",
18343
18930
  "resolve_mentions",
18344
18931
  "run_next_agent_dispatch",
18345
18932
  "search_knowledge_records",
@@ -18382,9 +18969,17 @@ var init_token_utils = __esm(() => {
18382
18969
  "unlock_file",
18383
18970
  "unwatch_task",
18384
18971
  "update_comment",
18972
+ "upsert_task_finding",
18385
18973
  "update_risk",
18386
18974
  "watch_task"
18387
18975
  ],
18976
+ loops: [
18977
+ "begin_task_run_transaction",
18978
+ "finish_task_run",
18979
+ "list_task_findings",
18980
+ "resolve_missing_task_findings",
18981
+ "upsert_task_finding"
18982
+ ],
18388
18983
  agents: [
18389
18984
  "auto_assign_task",
18390
18985
  "delete_agent",
@@ -18466,7 +19061,7 @@ var init_token_utils = __esm(() => {
18466
19061
  maintenance: ["extract_todos", "get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task", "watch_source_todos"]
18467
19062
  };
18468
19063
  MCP_PROFILE_GROUPS = {
18469
- minimal: ["core"],
19064
+ minimal: ["core", "loops"],
18470
19065
  core: ["core"],
18471
19066
  standard: ["core", "tasks", "projects", "resources", "agents", "metadata"],
18472
19067
  agent: ["core", "tasks", "projects", "resources"],
@@ -20759,6 +21354,39 @@ async function handleCreateTask(req, ctx, json2, taskToSummary2) {
20759
21354
  return json2({ error: e instanceof Error ? e.message : "Failed to create task" }, 500);
20760
21355
  }
20761
21356
  }
21357
+ async function handleUpsertTask(req, ctx, json2, taskToSummary2) {
21358
+ try {
21359
+ const body = await req.json();
21360
+ if (typeof body["fingerprint"] !== "string" || body["fingerprint"].trim() === "") {
21361
+ return json2({ error: "Missing 'fingerprint'" }, 400);
21362
+ }
21363
+ if (typeof body["title"] !== "string" || body["title"].trim() === "") {
21364
+ return json2({ error: "Missing 'title'" }, 400);
21365
+ }
21366
+ const metadata = body["metadata"] && typeof body["metadata"] === "object" && !Array.isArray(body["metadata"]) ? { ...body["metadata"] } : {};
21367
+ for (const key of ["expectation_id", "expectation_fingerprint", "evidence_paths", "origin_loop_id", "origin_run_id", "expected", "observed", "acceptance"]) {
21368
+ if (body[key] !== undefined)
21369
+ metadata[key] = body[key];
21370
+ }
21371
+ const result = upsertTaskByFingerprint({
21372
+ fingerprint: body["fingerprint"],
21373
+ title: body["title"],
21374
+ description: typeof body["description"] === "string" ? body["description"] : undefined,
21375
+ status: body["status"],
21376
+ priority: body["priority"],
21377
+ project_id: typeof body["project_id"] === "string" ? body["project_id"] : undefined,
21378
+ task_list_id: typeof body["task_list_id"] === "string" ? body["task_list_id"] : undefined,
21379
+ assigned_to: typeof body["assigned_to"] === "string" ? body["assigned_to"] : undefined,
21380
+ working_dir: typeof body["working_dir"] === "string" ? body["working_dir"] : undefined,
21381
+ tags: Array.isArray(body["tags"]) ? body["tags"].filter((tag) => typeof tag === "string") : undefined,
21382
+ metadata
21383
+ });
21384
+ ctx.broadcastEvent({ type: "task", task_id: result.task.id, action: result.created ? "created" : "updated", agent_id: result.task.agent_id, project_id: result.task.project_id });
21385
+ return json2({ created: result.created, task: taskToSummary2(result.task) }, result.created ? 201 : 200);
21386
+ } catch (e) {
21387
+ return json2({ error: e instanceof Error ? e.message : "Failed to upsert task" }, 500);
21388
+ }
21389
+ }
20762
21390
  function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
20763
21391
  const format = url.searchParams.get("format") || "json";
20764
21392
  const status = url.searchParams.get("status") || undefined;
@@ -25901,6 +26529,61 @@ function registerTaskCrudTools(server, ctx) {
25901
26529
  }
25902
26530
  });
25903
26531
  }
26532
+ if (shouldRegisterTool("upsert_task")) {
26533
+ server.tool("upsert_task", "Create or update a task by stable metadata fingerprint. Metadata is shallow-merged on updates.", {
26534
+ fingerprint: exports_external.string().describe("Stable dedupe fingerprint stored as metadata.fingerprint"),
26535
+ title: exports_external.string().describe("Task title"),
26536
+ description: exports_external.string().optional().describe("Task description"),
26537
+ status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
26538
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
26539
+ project_id: exports_external.string().optional().describe("Project ID"),
26540
+ task_list_id: exports_external.string().optional().describe("Task list ID"),
26541
+ assigned_to: exports_external.string().optional().describe("Agent ID or name to assign to"),
26542
+ tags: exports_external.array(exports_external.string()).optional().describe("Tags for the task"),
26543
+ working_dir: exports_external.string().optional().describe("Working directory associated with the task"),
26544
+ metadata: exports_external.record(exports_external.unknown()).optional().describe("Metadata object to shallow-merge"),
26545
+ expectation_id: exports_external.string().optional(),
26546
+ expectation_fingerprint: exports_external.string().optional(),
26547
+ evidence_paths: exports_external.array(exports_external.string()).optional(),
26548
+ origin_loop_id: exports_external.string().optional(),
26549
+ origin_run_id: exports_external.string().optional(),
26550
+ expected: exports_external.unknown().optional(),
26551
+ observed: exports_external.unknown().optional(),
26552
+ acceptance: exports_external.unknown().optional()
26553
+ }, async (params) => {
26554
+ try {
26555
+ const { assigned_to, project_id, task_list_id, metadata, expectation_id, expectation_fingerprint, evidence_paths, origin_loop_id, origin_run_id, expected, observed, acceptance, ...rest } = params;
26556
+ const mergedMetadata = { ...metadata ?? {} };
26557
+ if (expectation_id !== undefined)
26558
+ mergedMetadata["expectation_id"] = expectation_id;
26559
+ if (expectation_fingerprint !== undefined)
26560
+ mergedMetadata["expectation_fingerprint"] = expectation_fingerprint;
26561
+ if (evidence_paths !== undefined)
26562
+ mergedMetadata["evidence_paths"] = evidence_paths;
26563
+ if (origin_loop_id !== undefined)
26564
+ mergedMetadata["origin_loop_id"] = origin_loop_id;
26565
+ if (origin_run_id !== undefined)
26566
+ mergedMetadata["origin_run_id"] = origin_run_id;
26567
+ if (expected !== undefined)
26568
+ mergedMetadata["expected"] = expected;
26569
+ if (observed !== undefined)
26570
+ mergedMetadata["observed"] = observed;
26571
+ if (acceptance !== undefined)
26572
+ mergedMetadata["acceptance"] = acceptance;
26573
+ const resolved = { ...rest, metadata: mergedMetadata };
26574
+ if (assigned_to)
26575
+ resolved.assigned_to = resolveAssignee(assigned_to);
26576
+ if (project_id)
26577
+ resolved.project_id = resolveId(project_id, "projects");
26578
+ if (task_list_id)
26579
+ resolved.task_list_id = resolveId(task_list_id, "task_lists");
26580
+ const result = upsertTaskByFingerprint(resolved);
26581
+ return { content: [{ type: "text", text: compactJson({ created: result.created, task: JSON.parse(mutationTaskResponse(result.task)) }) }] };
26582
+ } catch (e) {
26583
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
26584
+ }
26585
+ });
26586
+ }
25904
26587
  if (shouldRegisterTool("list_tasks")) {
25905
26588
  server.tool("list_tasks", "List tasks with optional filters. Pass empty arrays for multi-value filters (e.g. status=[] shows all).", {
25906
26589
  status: exports_external.union([exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]), exports_external.array(exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]))]).optional().describe("Filter by status"),
@@ -25912,7 +26595,8 @@ function registerTaskCrudTools(server, ctx) {
25912
26595
  created_after: exports_external.string().optional().describe("ISO date \u2014 tasks created after this date"),
25913
26596
  created_before: exports_external.string().optional().describe("ISO date \u2014 tasks created before this date"),
25914
26597
  limit: exports_external.number().optional().describe("Max results (default: 50, max 500)"),
25915
- offset: exports_external.number().optional().describe("Pagination offset")
26598
+ offset: exports_external.number().optional().describe("Pagination offset"),
26599
+ metadata: exports_external.record(exports_external.unknown()).optional().describe("Exact top-level metadata filters")
25916
26600
  }, async (params) => {
25917
26601
  try {
25918
26602
  const resolved = { ...params };
@@ -27500,7 +28184,7 @@ function parseAssignee(line) {
27500
28184
  return mention;
27501
28185
  return line.match(/\bassign(?:ed)?\s+(?:to\s+)?([a-zA-Z0-9._-]+)/i)?.[1] || null;
27502
28186
  }
27503
- function parseTags(line) {
28187
+ function parseTags2(line) {
27504
28188
  return Array.from(new Set(Array.from(line.matchAll(/#([a-zA-Z0-9._-]+)/g)).map((match) => match[1].toLowerCase()))).slice(0, 10);
27505
28189
  }
27506
28190
  function parseDependencies(line) {
@@ -27572,7 +28256,7 @@ function previewNaturalLanguageIntake(input, db) {
27572
28256
  description: `Parsed from local natural-language intake:
27573
28257
  ${line}`,
27574
28258
  priority: parsePriority2(line, fallbackPriority),
27575
- tags: parseTags(line),
28259
+ tags: parseTags2(line),
27576
28260
  assigned_to: parseAssignee(line),
27577
28261
  due_at: parseDue(line, referenceDate),
27578
28262
  depends_on: parseDependencies(line),
@@ -28080,7 +28764,7 @@ function countMovedStandaloneVerifications(db, duplicateId) {
28080
28764
  const rows = db.query("SELECT command FROM task_verifications WHERE task_id = ?").all(duplicateId);
28081
28765
  return rows.filter((row) => !generatedCommands.has(row.command)).length;
28082
28766
  }
28083
- function mergeTaskMetadata(primary, duplicate, input, mergedAt) {
28767
+ function mergeTaskMetadata2(primary, duplicate, input, mergedAt) {
28084
28768
  const mergedDuplicates = Array.isArray(primary.metadata["merged_duplicates"]) ? [...primary.metadata["merged_duplicates"]] : [];
28085
28769
  mergedDuplicates.push({
28086
28770
  id: duplicate.id,
@@ -28141,7 +28825,7 @@ function mergeDuplicateTask(input, db) {
28141
28825
  updateTask(primary.id, {
28142
28826
  version: primary.version,
28143
28827
  tags: mergedTags,
28144
- metadata: mergeTaskMetadata(primary, duplicate, input, mergedAt),
28828
+ metadata: mergeTaskMetadata2(primary, duplicate, input, mergedAt),
28145
28829
  description: mergeTaskDescription(primary, duplicate) ?? undefined
28146
28830
  }, d);
28147
28831
  moved.comments = updateRows(d, "task_comments", "task_id", duplicate.id, primary.id);
@@ -34135,6 +34819,366 @@ var init_task_meta_tools = __esm(() => {
34135
34819
  init_zod();
34136
34820
  });
34137
34821
 
34822
+ // src/db/findings.ts
34823
+ var exports_findings = {};
34824
+ __export(exports_findings, {
34825
+ upsertTaskFinding: () => upsertTaskFinding,
34826
+ resolveMissingTaskFindings: () => resolveMissingTaskFindings,
34827
+ listTaskFindings: () => listTaskFindings,
34828
+ listCompactTaskFindings: () => listCompactTaskFindings,
34829
+ TASK_FINDING_UPSERT_SCHEMA_VERSION: () => TASK_FINDING_UPSERT_SCHEMA_VERSION,
34830
+ TASK_FINDING_SCHEMA_VERSION: () => TASK_FINDING_SCHEMA_VERSION,
34831
+ TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION: () => TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION
34832
+ });
34833
+ function parseObject2(value) {
34834
+ if (!value)
34835
+ return {};
34836
+ try {
34837
+ const parsed = JSON.parse(value);
34838
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
34839
+ } catch {
34840
+ return {};
34841
+ }
34842
+ }
34843
+ function normalizeKey(value) {
34844
+ return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
34845
+ }
34846
+ function normalizeFingerprint(value) {
34847
+ const normalized = normalizeKey(value);
34848
+ if (!normalized)
34849
+ throw new Error("finding fingerprint is required");
34850
+ return normalized.slice(0, 240);
34851
+ }
34852
+ function normalizeSeverity(value) {
34853
+ const normalized = normalizeKey(value || "medium");
34854
+ if (SEVERITIES.has(normalized))
34855
+ return normalized;
34856
+ if (/^(p0|blocker|urgent|highest)$/.test(normalized))
34857
+ return "critical";
34858
+ if (/^(p1|major)$/.test(normalized))
34859
+ return "high";
34860
+ if (/^(p3|minor|info)$/.test(normalized))
34861
+ return "low";
34862
+ return "medium";
34863
+ }
34864
+ function normalizeStatus2(value) {
34865
+ const normalized = normalizeKey(value || "open");
34866
+ if (STATUSES.has(normalized))
34867
+ return normalized;
34868
+ if (normalized === "closed" || normalized === "fixed")
34869
+ return "resolved";
34870
+ return "open";
34871
+ }
34872
+ function normalizeResolutionStatus(value) {
34873
+ const status = normalizeStatus2(value || "resolved");
34874
+ if (status === "open")
34875
+ throw new Error("resolve-missing status must be resolved or ignored");
34876
+ return status;
34877
+ }
34878
+ function redactOptional(value, max = 2000) {
34879
+ if (!value)
34880
+ return null;
34881
+ const redacted = redactEvidenceText(value).trim();
34882
+ if (!redacted)
34883
+ return null;
34884
+ return redacted.length > max ? `${redacted.slice(0, max - 3)}...` : redacted;
34885
+ }
34886
+ function rowToFinding(row) {
34887
+ return {
34888
+ schema_version: TASK_FINDING_SCHEMA_VERSION,
34889
+ ...row,
34890
+ severity: normalizeSeverity(row.severity),
34891
+ status: normalizeStatus2(row.status),
34892
+ metadata: parseObject2(row.metadata)
34893
+ };
34894
+ }
34895
+ function compactFinding(finding2) {
34896
+ return {
34897
+ schema_version: TASK_FINDING_SCHEMA_VERSION,
34898
+ id: finding2.id,
34899
+ task_id: finding2.task_id,
34900
+ run_id: finding2.run_id,
34901
+ fingerprint: finding2.fingerprint,
34902
+ title: finding2.title,
34903
+ severity: finding2.severity,
34904
+ status: finding2.status,
34905
+ source: finding2.source,
34906
+ summary: finding2.summary,
34907
+ artifact_path: finding2.artifact_path,
34908
+ first_seen_at: finding2.first_seen_at,
34909
+ last_seen_at: finding2.last_seen_at,
34910
+ resolved_at: finding2.resolved_at,
34911
+ metadata_keys: Object.keys(finding2.metadata).sort()
34912
+ };
34913
+ }
34914
+ function previewFinding(existing, next, timestamp3) {
34915
+ return {
34916
+ ...existing,
34917
+ run_id: next.run_id,
34918
+ title: next.title,
34919
+ severity: next.severity,
34920
+ status: next.status,
34921
+ source: next.source,
34922
+ summary: next.summary,
34923
+ artifact_path: next.artifact_path,
34924
+ metadata: next.metadata,
34925
+ last_seen_at: timestamp3,
34926
+ resolved_at: next.status === "open" ? null : existing.resolved_at || timestamp3,
34927
+ updated_at: timestamp3
34928
+ };
34929
+ }
34930
+ function upsertAction(existing, next) {
34931
+ if (sameFinding(existing, next))
34932
+ return "matched";
34933
+ return existing.status !== "open" && next.status === "open" ? "reopened" : "updated";
34934
+ }
34935
+ function resolveRunForTask(runId, taskId, db) {
34936
+ if (!runId)
34937
+ return null;
34938
+ const resolved = resolveTaskRunId(runId, db);
34939
+ const run = getTaskRun(resolved, db);
34940
+ if (!run)
34941
+ throw new Error(`Run not found: ${runId}`);
34942
+ if (run.task_id !== taskId)
34943
+ throw new Error(`Run ${resolved} belongs to task ${run.task_id}, not ${taskId}`);
34944
+ return resolved;
34945
+ }
34946
+ function getFindingByFingerprint(taskId, fingerprint2, db) {
34947
+ const row = db.query("SELECT * FROM task_findings WHERE task_id = ? AND fingerprint = ?").get(taskId, fingerprint2);
34948
+ return row ? rowToFinding(row) : null;
34949
+ }
34950
+ function assertTask(taskId, db) {
34951
+ if (!getTask(taskId, db))
34952
+ throw new TaskNotFoundError(taskId);
34953
+ }
34954
+ function nextFinding(input, db) {
34955
+ const fingerprint2 = normalizeFingerprint(input.fingerprint);
34956
+ const title = redactOptional(input.title, 300);
34957
+ if (!title)
34958
+ throw new Error("finding title is required");
34959
+ return {
34960
+ fingerprint: fingerprint2,
34961
+ run_id: resolveRunForTask(input.run_id, input.task_id, db),
34962
+ title,
34963
+ severity: normalizeSeverity(input.severity),
34964
+ status: normalizeStatus2(input.status),
34965
+ source: redactOptional(input.source, 120),
34966
+ summary: redactOptional(input.summary, 2000),
34967
+ artifact_path: redactOptional(input.artifact_path, 1000),
34968
+ metadata: redactValue(input.metadata || {})
34969
+ };
34970
+ }
34971
+ function sameFinding(left, right) {
34972
+ return left.run_id === right.run_id && left.title === right.title && left.severity === right.severity && left.status === right.status && left.source === right.source && left.summary === right.summary && left.artifact_path === right.artifact_path && JSON.stringify(left.metadata) === JSON.stringify(right.metadata);
34973
+ }
34974
+ function upsertTaskFinding(input, db) {
34975
+ const d = db || getDatabase();
34976
+ assertTask(input.task_id, d);
34977
+ const timestamp3 = input.observed_at || now();
34978
+ const warnings = [];
34979
+ const next = nextFinding(input, d);
34980
+ const existing = getFindingByFingerprint(input.task_id, next.fingerprint, d);
34981
+ const dryRun = !input.apply;
34982
+ if (dryRun) {
34983
+ const action2 = existing ? upsertAction(existing, next) : "preview";
34984
+ return {
34985
+ schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
34986
+ local_only: true,
34987
+ dry_run: true,
34988
+ processed_at: timestamp3,
34989
+ action: action2,
34990
+ fingerprint: next.fingerprint,
34991
+ finding: existing ? compactFinding(previewFinding(existing, next, timestamp3)) : null,
34992
+ warnings
34993
+ };
34994
+ }
34995
+ if (!existing) {
34996
+ const id = uuid();
34997
+ d.run(`INSERT INTO task_findings (
34998
+ id, task_id, run_id, fingerprint, title, severity, status, source, summary, artifact_path,
34999
+ metadata, first_seen_at, last_seen_at, resolved_at, created_at, updated_at
35000
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
35001
+ id,
35002
+ input.task_id,
35003
+ next.run_id,
35004
+ next.fingerprint,
35005
+ next.title,
35006
+ next.severity,
35007
+ next.status,
35008
+ next.source,
35009
+ next.summary,
35010
+ next.artifact_path,
35011
+ JSON.stringify(next.metadata),
35012
+ timestamp3,
35013
+ timestamp3,
35014
+ next.status === "open" ? null : timestamp3,
35015
+ timestamp3,
35016
+ timestamp3
35017
+ ]);
35018
+ return {
35019
+ schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
35020
+ local_only: true,
35021
+ dry_run: false,
35022
+ processed_at: timestamp3,
35023
+ action: "created",
35024
+ fingerprint: next.fingerprint,
35025
+ finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
35026
+ warnings
35027
+ };
35028
+ }
35029
+ if (sameFinding(existing, next)) {
35030
+ return {
35031
+ schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
35032
+ local_only: true,
35033
+ dry_run: false,
35034
+ processed_at: timestamp3,
35035
+ action: "matched",
35036
+ fingerprint: next.fingerprint,
35037
+ finding: compactFinding(existing),
35038
+ warnings
35039
+ };
35040
+ }
35041
+ const action = upsertAction(existing, next);
35042
+ d.run(`UPDATE task_findings SET
35043
+ run_id = ?, title = ?, severity = ?, status = ?, source = ?, summary = ?, artifact_path = ?,
35044
+ metadata = ?, last_seen_at = ?, resolved_at = ?, updated_at = ?
35045
+ WHERE id = ?`, [
35046
+ next.run_id,
35047
+ next.title,
35048
+ next.severity,
35049
+ next.status,
35050
+ next.source,
35051
+ next.summary,
35052
+ next.artifact_path,
35053
+ JSON.stringify(next.metadata),
35054
+ timestamp3,
35055
+ next.status === "open" ? null : existing.resolved_at || timestamp3,
35056
+ timestamp3,
35057
+ existing.id
35058
+ ]);
35059
+ return {
35060
+ schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
35061
+ local_only: true,
35062
+ dry_run: false,
35063
+ processed_at: timestamp3,
35064
+ action,
35065
+ fingerprint: next.fingerprint,
35066
+ finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
35067
+ warnings
35068
+ };
35069
+ }
35070
+ function listTaskFindings(filter = {}, db) {
35071
+ const d = db || getDatabase();
35072
+ const conditions = ["1=1"];
35073
+ const params = [];
35074
+ if (filter.task_id) {
35075
+ conditions.push("task_id = ?");
35076
+ params.push(filter.task_id);
35077
+ }
35078
+ if (filter.run_id) {
35079
+ conditions.push("run_id = ?");
35080
+ params.push(resolveTaskRunId(filter.run_id, d));
35081
+ }
35082
+ if (filter.status) {
35083
+ conditions.push("status = ?");
35084
+ params.push(normalizeStatus2(filter.status));
35085
+ }
35086
+ if (filter.source) {
35087
+ conditions.push("source = ?");
35088
+ params.push(redactOptional(filter.source, 120));
35089
+ }
35090
+ const limit = Math.min(Math.max(Math.floor(filter.limit ?? 50), 1), 500);
35091
+ const rows = d.query(`SELECT * FROM task_findings WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC, created_at DESC LIMIT ?`).all(...[...params, limit]);
35092
+ return rows.map(rowToFinding);
35093
+ }
35094
+ function listCompactTaskFindings(filter = {}, db) {
35095
+ return listTaskFindings(filter, db).map(compactFinding);
35096
+ }
35097
+ function resolveMissingTaskFindings(input, db) {
35098
+ const d = db || getDatabase();
35099
+ assertTask(input.task_id, d);
35100
+ const timestamp3 = input.resolved_at || now();
35101
+ const status = normalizeResolutionStatus(input.status);
35102
+ const runId = resolveRunForTask(input.run_id, input.task_id, d);
35103
+ const present = new Set(input.fingerprints.map(normalizeFingerprint));
35104
+ const warnings = [];
35105
+ const conditions = ["task_id = ?", "status = 'open'"];
35106
+ const params = [input.task_id];
35107
+ if (input.source) {
35108
+ conditions.push("source = ?");
35109
+ params.push(redactOptional(input.source, 120));
35110
+ }
35111
+ const candidates = d.query(`SELECT * FROM task_findings WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC, created_at DESC`).all(...params).map(rowToFinding).filter((finding2) => !present.has(finding2.fingerprint));
35112
+ const limit = Math.min(Math.max(Math.floor(input.limit ?? 50), 1), 200);
35113
+ const display = candidates.slice(0, limit);
35114
+ const omittedCount = Math.max(0, candidates.length - display.length);
35115
+ if (!input.apply) {
35116
+ return {
35117
+ schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
35118
+ local_only: true,
35119
+ dry_run: true,
35120
+ processed_at: timestamp3,
35121
+ action: candidates.length > 0 ? "preview" : "noop",
35122
+ task_id: input.task_id,
35123
+ source: input.source ? redactOptional(input.source, 120) : null,
35124
+ run_id: runId,
35125
+ present_fingerprint_count: present.size,
35126
+ candidate_count: candidates.length,
35127
+ changed_count: 0,
35128
+ omitted_count: omittedCount,
35129
+ findings: display.map(compactFinding),
35130
+ warnings
35131
+ };
35132
+ }
35133
+ const metadataPatch = redactValue({
35134
+ resolved_by: {
35135
+ agent_id: input.agent_id ?? null,
35136
+ run_id: runId,
35137
+ reason: input.reason ? redactEvidenceText(input.reason) : "missing from latest loop finding set"
35138
+ }
35139
+ });
35140
+ const tx = d.transaction(() => {
35141
+ for (const finding2 of candidates) {
35142
+ d.run("UPDATE task_findings SET status = ?, resolved_at = ?, metadata = ?, updated_at = ? WHERE id = ? AND status = 'open'", [
35143
+ status,
35144
+ timestamp3,
35145
+ JSON.stringify({ ...finding2.metadata, ...metadataPatch }),
35146
+ timestamp3,
35147
+ finding2.id
35148
+ ]);
35149
+ }
35150
+ });
35151
+ tx();
35152
+ const updated = candidates.map((finding2) => d.query("SELECT * FROM task_findings WHERE id = ?").get(finding2.id)).filter((row) => Boolean(row)).map(rowToFinding);
35153
+ const visibleUpdated = updated.slice(0, limit);
35154
+ return {
35155
+ schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
35156
+ local_only: true,
35157
+ dry_run: false,
35158
+ processed_at: timestamp3,
35159
+ action: updated.length > 0 ? status : "noop",
35160
+ task_id: input.task_id,
35161
+ source: input.source ? redactOptional(input.source, 120) : null,
35162
+ run_id: runId,
35163
+ present_fingerprint_count: present.size,
35164
+ candidate_count: candidates.length,
35165
+ changed_count: updated.length,
35166
+ omitted_count: omittedCount,
35167
+ findings: visibleUpdated.map(compactFinding),
35168
+ warnings
35169
+ };
35170
+ }
35171
+ var TASK_FINDING_SCHEMA_VERSION = "todos.task_finding.v1", TASK_FINDING_UPSERT_SCHEMA_VERSION = "todos.task_finding_upsert.v1", TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION = "todos.task_finding_resolve_missing.v1", SEVERITIES, STATUSES;
35172
+ var init_findings = __esm(() => {
35173
+ init_redaction();
35174
+ init_types();
35175
+ init_database();
35176
+ init_tasks();
35177
+ init_task_runs();
35178
+ SEVERITIES = new Set(["low", "medium", "high", "critical"]);
35179
+ STATUSES = new Set(["open", "resolved", "ignored"]);
35180
+ });
35181
+
34138
35182
  // src/lib/agent-run-dispatcher.ts
34139
35183
  var exports_agent_run_dispatcher = {};
34140
35184
  __export(exports_agent_run_dispatcher, {
@@ -35063,7 +36107,7 @@ function parseArray2(value) {
35063
36107
  return [];
35064
36108
  }
35065
36109
  }
35066
- function parseObject2(value) {
36110
+ function parseObject3(value) {
35067
36111
  try {
35068
36112
  const parsed = JSON.parse(value || "{}");
35069
36113
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
@@ -35104,7 +36148,7 @@ function rowToKnowledgeRecord(row) {
35104
36148
  agent_id: row.agent_id,
35105
36149
  snapshot_id: row.snapshot_id,
35106
36150
  tags: parseArray2(row.tags),
35107
- metadata: redactValue(parseObject2(row.metadata)),
36151
+ metadata: redactValue(parseObject3(row.metadata)),
35108
36152
  created_at: row.created_at,
35109
36153
  updated_at: row.updated_at
35110
36154
  };
@@ -35323,7 +36367,7 @@ function parseArray3(value) {
35323
36367
  return [];
35324
36368
  }
35325
36369
  }
35326
- function parseObject3(value) {
36370
+ function parseObject4(value) {
35327
36371
  try {
35328
36372
  const parsed = JSON.parse(value || "{}");
35329
36373
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
@@ -35372,7 +36416,7 @@ function rowToRisk(row) {
35372
36416
  plan_id: row.plan_id,
35373
36417
  task_id: row.task_id,
35374
36418
  tags: parseArray3(row.tags),
35375
- metadata: redactValue(parseObject3(row.metadata)),
36419
+ metadata: redactValue(parseObject4(row.metadata)),
35376
36420
  created_at: row.created_at,
35377
36421
  updated_at: row.updated_at,
35378
36422
  closed_at: row.closed_at
@@ -36223,7 +37267,7 @@ function extractUsage(value) {
36223
37267
  }
36224
37268
  return own;
36225
37269
  }
36226
- function parseJsonObject5(value) {
37270
+ function parseJsonObject6(value) {
36227
37271
  if (!value)
36228
37272
  return {};
36229
37273
  try {
@@ -36395,7 +37439,7 @@ function createLocalUsageLedger(options = {}, db) {
36395
37439
  completedRunMs += millisBetween(run.started_at, run.completed_at);
36396
37440
  else
36397
37441
  openRunMs += millisBetween(run.started_at, generatedAt);
36398
- const usage = extractUsage(parseJsonObject5(run.metadata));
37442
+ const usage = extractUsage(parseJsonObject6(run.metadata));
36399
37443
  metadataUsage.tokens += usage.tokens;
36400
37444
  metadataUsage.cost_usd += usage.cost_usd;
36401
37445
  metadataUsage.duration_ms += usage.duration_ms;
@@ -36407,7 +37451,7 @@ function createLocalUsageLedger(options = {}, db) {
36407
37451
  JOIN tasks t ON t.id = e.task_id
36408
37452
  ${runClause}`, runParams);
36409
37453
  for (const event of eventRows) {
36410
- const usage = extractUsage(parseJsonObject5(event.data));
37454
+ const usage = extractUsage(parseJsonObject6(event.data));
36411
37455
  metadataUsage.tokens += usage.tokens;
36412
37456
  metadataUsage.cost_usd += usage.cost_usd;
36413
37457
  metadataUsage.duration_ms += usage.duration_ms;
@@ -40208,6 +41252,38 @@ ${lines.join(`
40208
41252
  }
40209
41253
  });
40210
41254
  }
41255
+ if (shouldRegisterTool("begin_task_run_transaction")) {
41256
+ server.tool("begin_task_run_transaction", "Preview or apply an idempotent local loop run transaction keyed by a stable loop/run id.", {
41257
+ task_id: exports_external.string().describe("Task ID"),
41258
+ key: exports_external.string().optional().describe("Stable idempotency key"),
41259
+ loop_id: exports_external.string().optional().describe("Loop identifier; used as key fallback"),
41260
+ loop_run_id: exports_external.string().optional().describe("Loop run identifier; used as key fallback"),
41261
+ agent_id: exports_external.string().optional().describe("Agent starting the run"),
41262
+ title: exports_external.string().optional().describe("Run title"),
41263
+ summary: exports_external.string().optional().describe("Run summary"),
41264
+ metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata"),
41265
+ claim: exports_external.boolean().optional().describe("Claim/start the task before recording the run"),
41266
+ apply: exports_external.boolean().optional().describe("Apply the transaction; false or omitted is dry-run")
41267
+ }, async ({ task_id, key, loop_id, loop_run_id, agent_id, title, summary, metadata, claim, apply }) => {
41268
+ try {
41269
+ const result = beginTaskRunTransaction({
41270
+ task_id: resolveId(task_id),
41271
+ key,
41272
+ loop_id,
41273
+ loop_run_id,
41274
+ agent_id,
41275
+ title,
41276
+ summary,
41277
+ metadata,
41278
+ claim,
41279
+ apply
41280
+ });
41281
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
41282
+ } catch (e) {
41283
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
41284
+ }
41285
+ });
41286
+ }
40211
41287
  if (shouldRegisterTool("list_task_runs")) {
40212
41288
  server.tool("list_task_runs", "List local run ledger entries, optionally scoped to a task.", { task_id: exports_external.string().optional().describe("Optional task ID") }, async ({ task_id }) => {
40213
41289
  try {
@@ -40305,15 +41381,117 @@ ${lines.join(`
40305
41381
  });
40306
41382
  }
40307
41383
  if (shouldRegisterTool("finish_task_run")) {
40308
- server.tool("finish_task_run", "Finish a local run ledger entry as completed, failed, or cancelled.", {
40309
- run_id: exports_external.string().describe("Run ID or prefix"),
40310
- status: exports_external.enum(["completed", "failed", "cancelled"]).describe("Final run status"),
41384
+ server.tool("finish_task_run", "Finish a local run ledger entry as completed, failed, or cancelled. Supports idempotent lookup by key.", {
41385
+ run_id: exports_external.string().optional().describe("Run ID or prefix"),
41386
+ key: exports_external.string().optional().describe("Idempotency key when run_id is omitted"),
41387
+ task_id: exports_external.string().optional().describe("Task scope for key lookup"),
41388
+ status: exports_external.enum(["completed", "failed", "cancelled"]).optional().describe("Final run status"),
40311
41389
  summary: exports_external.string().optional().describe("Final summary"),
40312
- agent_id: exports_external.string().optional().describe("Agent finishing the run")
40313
- }, async ({ run_id, status, summary, agent_id }) => {
41390
+ agent_id: exports_external.string().optional().describe("Agent finishing the run"),
41391
+ apply: exports_external.boolean().optional().describe("Apply the transaction; false previews without mutating")
41392
+ }, async ({ run_id, key, task_id, status, summary, agent_id, apply }) => {
40314
41393
  try {
40315
- const run = finishTaskRun({ run_id, status, summary, agent_id });
40316
- return { content: [{ type: "text", text: JSON.stringify(run, null, 2) }] };
41394
+ if (run_id && !key && apply === undefined) {
41395
+ const result2 = finishTaskRunTransaction({ run_id, status: status || "completed", summary, agent_id, apply: true });
41396
+ const run = result2.run ? getTaskRunLedger(result2.run.id).run : null;
41397
+ return { content: [{ type: "text", text: JSON.stringify(run, null, 2) }] };
41398
+ }
41399
+ const result = finishTaskRunTransaction({
41400
+ run_id,
41401
+ key,
41402
+ task_id: task_id ? resolveId(task_id) : undefined,
41403
+ status: status || "completed",
41404
+ summary,
41405
+ agent_id,
41406
+ apply: apply !== false
41407
+ });
41408
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
41409
+ } catch (e) {
41410
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
41411
+ }
41412
+ });
41413
+ }
41414
+ if (shouldRegisterTool("upsert_task_finding")) {
41415
+ server.tool("upsert_task_finding", "Preview or apply an idempotent local finding upsert scoped by task and fingerprint.", {
41416
+ task_id: exports_external.string().describe("Task ID"),
41417
+ fingerprint: exports_external.string().describe("Stable finding fingerprint"),
41418
+ title: exports_external.string().describe("Finding title"),
41419
+ severity: exports_external.enum(["low", "medium", "high", "critical"]).optional().describe("Finding severity"),
41420
+ status: exports_external.enum(["open", "resolved", "ignored"]).optional().describe("Finding status"),
41421
+ source: exports_external.string().optional().describe("Loop/tool source name"),
41422
+ summary: exports_external.string().optional().describe("Bounded finding summary"),
41423
+ artifact_path: exports_external.string().optional().describe("Local artifact path/reference; content is not read"),
41424
+ run_id: exports_external.string().optional().describe("Run ledger ID or prefix"),
41425
+ metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata"),
41426
+ apply: exports_external.boolean().optional().describe("Apply the upsert; false or omitted is dry-run")
41427
+ }, async ({ task_id, fingerprint: fingerprint3, title, severity, status, source: source3, summary, artifact_path, run_id, metadata, apply }) => {
41428
+ try {
41429
+ const result = upsertTaskFinding({
41430
+ task_id: resolveId(task_id),
41431
+ fingerprint: fingerprint3,
41432
+ title,
41433
+ severity,
41434
+ status,
41435
+ source: source3,
41436
+ summary,
41437
+ artifact_path,
41438
+ run_id,
41439
+ metadata,
41440
+ apply
41441
+ });
41442
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
41443
+ } catch (e) {
41444
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
41445
+ }
41446
+ });
41447
+ }
41448
+ if (shouldRegisterTool("list_task_findings")) {
41449
+ server.tool("list_task_findings", "List compact local findings with bounded output.", {
41450
+ task_id: exports_external.string().optional().describe("Filter by task"),
41451
+ run_id: exports_external.string().optional().describe("Filter by run"),
41452
+ status: exports_external.enum(["open", "resolved", "ignored"]).optional().describe("Filter by status"),
41453
+ source: exports_external.string().optional().describe("Filter by source"),
41454
+ limit: exports_external.number().optional().describe("Maximum findings to return")
41455
+ }, async ({ task_id, run_id, status, source: source3, limit }) => {
41456
+ try {
41457
+ const findings = listCompactTaskFindings({
41458
+ task_id: task_id ? resolveId(task_id) : undefined,
41459
+ run_id,
41460
+ status,
41461
+ source: source3,
41462
+ limit
41463
+ });
41464
+ return { content: [{ type: "text", text: JSON.stringify(findings) }] };
41465
+ } catch (e) {
41466
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
41467
+ }
41468
+ });
41469
+ }
41470
+ if (shouldRegisterTool("resolve_missing_task_findings")) {
41471
+ server.tool("resolve_missing_task_findings", "Resolve open findings absent from the latest loop finding set.", {
41472
+ task_id: exports_external.string().describe("Task ID"),
41473
+ fingerprints: exports_external.array(exports_external.string()).optional().describe("Fingerprints still present in the latest run"),
41474
+ source: exports_external.string().optional().describe("Only resolve findings from this source"),
41475
+ run_id: exports_external.string().optional().describe("Run ledger ID or prefix for audit metadata"),
41476
+ status: exports_external.enum(["resolved", "ignored"]).optional().describe("Resolution status"),
41477
+ agent_id: exports_external.string().optional().describe("Agent resolving findings"),
41478
+ reason: exports_external.string().optional().describe("Resolution reason"),
41479
+ limit: exports_external.number().optional().describe("Maximum findings returned"),
41480
+ apply: exports_external.boolean().optional().describe("Apply resolution; false or omitted is dry-run")
41481
+ }, async ({ task_id, fingerprints, source: source3, run_id, status, agent_id, reason, limit, apply }) => {
41482
+ try {
41483
+ const result = resolveMissingTaskFindings({
41484
+ task_id: resolveId(task_id),
41485
+ fingerprints: fingerprints || [],
41486
+ source: source3,
41487
+ run_id,
41488
+ status,
41489
+ agent_id,
41490
+ reason,
41491
+ limit,
41492
+ apply
41493
+ });
41494
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
40317
41495
  } catch (e) {
40318
41496
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
40319
41497
  }
@@ -40649,6 +41827,7 @@ var init_task_resources = __esm(() => {
40649
41827
  init_agents();
40650
41828
  init_task_commits();
40651
41829
  init_task_runs();
41830
+ init_findings();
40652
41831
  init_agent_run_dispatcher();
40653
41832
  init_verification_providers();
40654
41833
  init_release_notes();
@@ -44389,8 +45568,10 @@ function taskToSummary(task2, fields) {
44389
45568
  task_list_id: task2.task_list_id,
44390
45569
  agent_id: task2.agent_id,
44391
45570
  assigned_to: task2.assigned_to,
45571
+ working_dir: task2.working_dir,
44392
45572
  locked_by: task2.locked_by,
44393
45573
  tags: task2.tags,
45574
+ metadata: task2.metadata,
44394
45575
  version: task2.version,
44395
45576
  created_at: task2.created_at,
44396
45577
  updated_at: task2.updated_at,
@@ -44532,6 +45713,9 @@ Dashboard not found at: ${dashboardDir}`);
44532
45713
  if (path === "/api/tasks" && method === "POST") {
44533
45714
  return handleCreateTask(req, ctx, jsonWithCors, taskToSummary);
44534
45715
  }
45716
+ if (path === "/api/tasks/upsert" && method === "POST") {
45717
+ return handleUpsertTask(req, ctx, jsonWithCors, taskToSummary);
45718
+ }
44535
45719
  if (path === "/api/tasks/export" && method === "GET") {
44536
45720
  return handleTasksExport(req, url, ctx, jsonWithCors, taskToSummary);
44537
45721
  }
@@ -44727,7 +45911,7 @@ Dashboard not found at: ${dashboardDir}`);
44727
45911
  } catch {}
44728
45912
  }
44729
45913
  }
44730
- var MIME_TYPES, SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX = 120;
45914
+ var MIME_TYPES, SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX;
44731
45915
  var init_serve = __esm(() => {
44732
45916
  init_database();
44733
45917
  init_api_keys();
@@ -44752,6 +45936,7 @@ var init_serve = __esm(() => {
44752
45936
  "Permissions-Policy": "camera=, microphone=, geolocation="
44753
45937
  };
44754
45938
  rateLimitMap = new Map;
45939
+ RATE_LIMIT_MAX = Number.parseInt(process.env["TODOS_RATE_LIMIT_MAX"] || "120", 10);
44755
45940
  });
44756
45941
 
44757
45942
  // src/cli/components/Header.tsx
@@ -47405,9 +48590,9 @@ function normalizeKind(value) {
47405
48590
  const raw = cleanKey(asString4(value) || "unknown").replace(/-/g, "_");
47406
48591
  return KINDS.has(raw) ? raw : raw || "unknown";
47407
48592
  }
47408
- function normalizeSeverity(value, fallback) {
48593
+ function normalizeSeverity2(value, fallback) {
47409
48594
  const raw = cleanKey(asString4(value) || fallback);
47410
- if (SEVERITIES.has(raw))
48595
+ if (SEVERITIES2.has(raw))
47411
48596
  return raw;
47412
48597
  if (/^(p0|blocker|urgent|highest)$/.test(raw))
47413
48598
  return "critical";
@@ -47502,7 +48687,7 @@ function normalizeTesterIssueReport(value, fallbackPriority = "medium") {
47502
48687
  title,
47503
48688
  summary: truncate3(asString4(input["summary"]), 1000) ?? null,
47504
48689
  kind: normalizeKind(input["kind"] ?? input["type"]),
47505
- severity: normalizeSeverity(input["severity"] ?? input["priority"], fallbackPriority),
48690
+ severity: normalizeSeverity2(input["severity"] ?? input["priority"], fallbackPriority),
47506
48691
  source: source3,
47507
48692
  target,
47508
48693
  failure,
@@ -47800,13 +48985,13 @@ function readTesterIssueReportsPayload(value) {
47800
48985
  return record["issues"];
47801
48986
  return [value];
47802
48987
  }
47803
- var TESTERS_ISSUE_REPORT_SCHEMA_VERSION = "testers.issue_report.v1", TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION = "todos.tester_issue_report_result.v1", TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION = "todos.tester_issue_report_batch_result.v1", PRIORITIES5, SEVERITIES, KINDS;
48988
+ var TESTERS_ISSUE_REPORT_SCHEMA_VERSION = "testers.issue_report.v1", TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION = "todos.tester_issue_report_result.v1", TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION = "todos.tester_issue_report_batch_result.v1", PRIORITIES5, SEVERITIES2, KINDS;
47804
48989
  var init_tester_issue_reports = __esm(() => {
47805
48990
  init_database();
47806
48991
  init_tasks();
47807
48992
  init_redaction();
47808
48993
  PRIORITIES5 = ["low", "medium", "high", "critical"];
47809
- SEVERITIES = new Set(PRIORITIES5);
48994
+ SEVERITIES2 = new Set(PRIORITIES5);
47810
48995
  KINDS = new Set([
47811
48996
  "assertion_failure",
47812
48997
  "runtime_error",
@@ -50988,6 +52173,23 @@ Verifications:`));
50988
52173
  console.log(`${result.status} ${result.provider}: ${result.output_summary || ""}`);
50989
52174
  });
50990
52175
  const runs = program2.command("runs").description("Manage the local run ledger and evidence capture");
52176
+ runs.command("begin <task-id>").description("Preview or apply an idempotent loop run transaction").option("--key <key>", "Stable idempotency key for this loop transaction").option("--loop-id <id>", "Loop identifier; used as the key when --key/--loop-run-id are omitted").option("--loop-run-id <id>", "Loop run identifier; used as the key when --key is omitted").option("--agent <name>", "Agent starting the run").option("--title <text>", "Run title").option("--summary <text>", "Run summary").option("--metadata <json>", "Additional JSON metadata").option("--claim", "Claim/start the task for the agent before recording the run").option("--apply", "Apply the transaction; omitted means dry-run").action(async (taskId, opts) => {
52177
+ const globalOpts = program2.opts();
52178
+ const { beginTaskRunTransaction: beginTaskRunTransaction2 } = await Promise.resolve().then(() => (init_task_runs(), exports_task_runs));
52179
+ const result = beginTaskRunTransaction2({
52180
+ task_id: resolveTaskId(taskId),
52181
+ key: opts.key,
52182
+ loop_id: opts.loopId,
52183
+ loop_run_id: opts.loopRunId,
52184
+ agent_id: opts.agent || globalOpts.agent,
52185
+ title: opts.title,
52186
+ summary: opts.summary,
52187
+ metadata: parseJsonOption(opts.metadata, "--metadata"),
52188
+ claim: opts.claim,
52189
+ apply: opts.apply
52190
+ });
52191
+ output(result, true);
52192
+ });
50991
52193
  runs.command("start <task-id>").description("Start a local run ledger entry for a task").option("--agent <name>", "Agent starting the run").option("--title <text>", "Run title").option("--summary <text>", "Run summary").option("--metadata <json>", "Additional JSON metadata").option("--claim", "Claim/start the task for the agent before recording the run").action(async (taskId, opts) => {
50992
52194
  const globalOpts = program2.opts();
50993
52195
  const { startTaskRun: startTaskRun2 } = await Promise.resolve().then(() => (init_task_runs(), exports_task_runs));
@@ -51199,19 +52401,68 @@ Artifacts:`));
51199
52401
  process.exit(1);
51200
52402
  }
51201
52403
  });
51202
- runs.command("finish <run-id>").description("Finish a run ledger entry").option("--status <status>", "completed, failed, or cancelled", "completed").option("--summary <text>", "Final summary").option("--agent <name>", "Agent finishing the run").action(async (runId, opts) => {
52404
+ runs.command("finish [run-id]").description("Finish a run ledger entry idempotently").option("--key <key>", "Resolve run by idempotency key when run-id is omitted").option("--task <task-id>", "Task scope for --key lookup").option("--status <status>", "completed, failed, or cancelled", "completed").option("--summary <text>", "Final summary").option("--agent <name>", "Agent finishing the run").option("--dry-run", "Preview without mutating").action(async (runId, opts) => {
51203
52405
  const globalOpts = program2.opts();
51204
52406
  if (opts.status !== "completed" && opts.status !== "failed" && opts.status !== "cancelled") {
51205
52407
  console.error(chalk8.red("--status must be completed, failed, or cancelled"));
51206
52408
  process.exit(1);
51207
52409
  }
51208
- const { finishTaskRun: finishTaskRun2 } = await Promise.resolve().then(() => (init_task_runs(), exports_task_runs));
51209
- const run = finishTaskRun2({ run_id: runId, status: opts.status, summary: opts.summary, agent_id: opts.agent || globalOpts.agent });
51210
- if (globalOpts.json) {
51211
- output(run, true);
51212
- return;
51213
- }
51214
- console.log(chalk8.green(`Finished run ${run.id.slice(0, 8)} as ${run.status}`));
52410
+ const { finishTaskRunTransaction: finishTaskRunTransaction2 } = await Promise.resolve().then(() => (init_task_runs(), exports_task_runs));
52411
+ const result = finishTaskRunTransaction2({
52412
+ run_id: runId,
52413
+ key: opts.key,
52414
+ task_id: opts.task ? resolveTaskId(opts.task) : undefined,
52415
+ status: opts.status,
52416
+ summary: opts.summary,
52417
+ agent_id: opts.agent || globalOpts.agent,
52418
+ apply: !opts.dryRun
52419
+ });
52420
+ output(result, true);
52421
+ });
52422
+ const findings = program2.command("findings").description("Manage local task findings for loop dedupe and resolution");
52423
+ findings.command("upsert").description("Preview or apply an idempotent finding upsert").requiredOption("--task <task-id>", "Task ID").requiredOption("--fingerprint <value>", "Stable finding fingerprint").requiredOption("--title <text>", "Finding title").option("--severity <severity>", "low, medium, high, or critical", "medium").option("--status <status>", "open, resolved, or ignored", "open").option("--source <source>", "Loop/tool source name").option("--summary <text>", "Bounded finding summary").option("--artifact <path>", "Local artifact path/reference; content is not read").option("--run <run-id>", "Run ledger ID or prefix").option("--metadata <json>", "Additional JSON metadata").option("--apply", "Apply the upsert; omitted means dry-run").action(async (opts) => {
52424
+ const { upsertTaskFinding: upsertTaskFinding2 } = await Promise.resolve().then(() => (init_findings(), exports_findings));
52425
+ const result = upsertTaskFinding2({
52426
+ task_id: resolveTaskId(opts.task),
52427
+ fingerprint: opts.fingerprint,
52428
+ title: opts.title,
52429
+ severity: opts.severity,
52430
+ status: opts.status,
52431
+ source: opts.source,
52432
+ summary: opts.summary,
52433
+ artifact_path: opts.artifact,
52434
+ run_id: opts.run,
52435
+ metadata: parseJsonOption(opts.metadata, "--metadata"),
52436
+ apply: opts.apply
52437
+ });
52438
+ output(result, true);
52439
+ });
52440
+ findings.command("resolve-missing").description("Resolve open findings absent from the latest loop finding set").requiredOption("--task <task-id>", "Task ID").option("--fingerprints <list>", "Comma-separated fingerprints still present").option("--source <source>", "Only resolve findings from this source").option("--run <run-id>", "Run ledger ID or prefix for audit metadata").option("--status <status>", "resolved or ignored", "resolved").option("--agent <name>", "Agent resolving findings").option("--reason <text>", "Resolution reason").option("--limit <n>", "Maximum findings returned", "50").option("--apply", "Apply resolution; omitted means dry-run").action(async (opts) => {
52441
+ const globalOpts = program2.opts();
52442
+ const { resolveMissingTaskFindings: resolveMissingTaskFindings2 } = await Promise.resolve().then(() => (init_findings(), exports_findings));
52443
+ const result = resolveMissingTaskFindings2({
52444
+ task_id: resolveTaskId(opts.task),
52445
+ fingerprints: listOption(opts.fingerprints) || [],
52446
+ source: opts.source,
52447
+ run_id: opts.run,
52448
+ status: opts.status,
52449
+ agent_id: opts.agent || globalOpts.agent,
52450
+ reason: opts.reason,
52451
+ limit: opts.limit ? Number.parseInt(opts.limit, 10) : undefined,
52452
+ apply: opts.apply
52453
+ });
52454
+ output(result, true);
52455
+ });
52456
+ findings.command("list").description("List compact local findings").option("--task <task-id>", "Filter by task").option("--run <run-id>", "Filter by run").option("--status <status>", "Filter by open, resolved, or ignored").option("--source <source>", "Filter by source").option("--limit <n>", "Maximum findings returned", "50").action(async (opts) => {
52457
+ const { listCompactTaskFindings: listCompactTaskFindings2 } = await Promise.resolve().then(() => (init_findings(), exports_findings));
52458
+ const findings2 = listCompactTaskFindings2({
52459
+ task_id: opts.task ? resolveTaskId(opts.task) : undefined,
52460
+ run_id: opts.run,
52461
+ status: opts.status,
52462
+ source: opts.source,
52463
+ limit: opts.limit ? Number.parseInt(opts.limit, 10) : undefined
52464
+ });
52465
+ output(findings2, true);
51215
52466
  });
51216
52467
  const agentRuns = program2.command("agent-runs").description("Queue and dispatch local agent runs");
51217
52468
  agentRuns.command("adapter-set <name>").description("Create or update a local agent run adapter").option("--command <command>", "Local command template. Supports {task_id}, {run_id}, and {agent_id}").option("--sandbox <name>", "Runner sandbox profile to check before launch").option("--cwd <path>", "Command working directory").option("--env <json>", "Static adapter environment as a JSON object").action(async (name, opts) => {
@@ -52125,7 +53376,7 @@ function parseRecordType(value) {
52125
53376
  console.error(chalk13.red(`type must be one of: ${RECORD_TYPES.join(", ")}`));
52126
53377
  process.exit(1);
52127
53378
  }
52128
- function parseJsonObject6(value, label) {
53379
+ function parseJsonObject7(value, label) {
52129
53380
  if (!value)
52130
53381
  return;
52131
53382
  try {
@@ -52185,7 +53436,7 @@ function registerKnowledgeCommands(program2) {
52185
53436
  plan_id: opts.plan,
52186
53437
  agent_id: opts.agent || globalOpts.agent,
52187
53438
  tags: tagsFromOption(opts.tag),
52188
- metadata: parseJsonObject6(opts.metadataJson, "--metadata-json")
53439
+ metadata: parseJsonObject7(opts.metadataJson, "--metadata-json")
52189
53440
  }, getDatabase());
52190
53441
  if (opts.json || globalOpts.json)
52191
53442
  output(record, true);
@@ -52210,7 +53461,7 @@ function registerKnowledgeCommands(program2) {
52210
53461
  blockers: opts.blocker,
52211
53462
  next_steps: opts.next,
52212
53463
  tags: tagsFromOption(opts.tag),
52213
- metadata: parseJsonObject6(opts.metadataJson, "--metadata-json")
53464
+ metadata: parseJsonObject7(opts.metadataJson, "--metadata-json")
52214
53465
  }, getDatabase());
52215
53466
  if (opts.json || globalOpts.json)
52216
53467
  output(result, true);
@@ -52296,7 +53547,7 @@ function parseChoice(value, choices, label) {
52296
53547
  console.error(chalk14.red(`${label} must be one of: ${choices.join(", ")}`));
52297
53548
  process.exit(1);
52298
53549
  }
52299
- function parseJsonObject7(value, label) {
53550
+ function parseJsonObject8(value, label) {
52300
53551
  if (!value)
52301
53552
  return;
52302
53553
  try {
@@ -52317,8 +53568,8 @@ function tagsFromOption2(value) {
52317
53568
  }
52318
53569
  function commonFilters2(opts) {
52319
53570
  return {
52320
- status: opts.status ? parseChoice(opts.status, STATUSES, "--status") : undefined,
52321
- severity: opts.severity ? parseChoice(opts.severity, SEVERITIES2, "--severity") : undefined,
53571
+ status: opts.status ? parseChoice(opts.status, STATUSES2, "--status") : undefined,
53572
+ severity: opts.severity ? parseChoice(opts.severity, SEVERITIES3, "--severity") : undefined,
52322
53573
  probability: opts.probability ? parseChoice(opts.probability, PROBABILITIES, "--probability") : undefined,
52323
53574
  owner: opts.owner,
52324
53575
  project_id: opts.project,
@@ -52351,8 +53602,8 @@ function registerRiskCommands(program2) {
52351
53602
  const risk = createRisk({
52352
53603
  title,
52353
53604
  description: opts.description,
52354
- status: parseChoice(opts.status, STATUSES, "--status"),
52355
- severity: parseChoice(opts.severity, SEVERITIES2, "--severity"),
53605
+ status: parseChoice(opts.status, STATUSES2, "--status"),
53606
+ severity: parseChoice(opts.severity, SEVERITIES3, "--severity"),
52356
53607
  probability: parseChoice(opts.probability, PROBABILITIES, "--probability"),
52357
53608
  owner: opts.owner,
52358
53609
  mitigation: opts.mitigation,
@@ -52361,7 +53612,7 @@ function registerRiskCommands(program2) {
52361
53612
  plan_id: opts.plan,
52362
53613
  task_id: opts.task,
52363
53614
  tags: tagsFromOption2(opts.tag),
52364
- metadata: parseJsonObject7(opts.metadataJson, "--metadata-json")
53615
+ metadata: parseJsonObject8(opts.metadataJson, "--metadata-json")
52365
53616
  }, getDatabase());
52366
53617
  if (opts.json || globalOpts.json)
52367
53618
  output(risk, true);
@@ -52403,8 +53654,8 @@ function registerRiskCommands(program2) {
52403
53654
  const risk = updateRisk(id, {
52404
53655
  title: opts.title,
52405
53656
  description: opts.description,
52406
- status: opts.status ? parseChoice(opts.status, STATUSES, "--status") : undefined,
52407
- severity: opts.severity ? parseChoice(opts.severity, SEVERITIES2, "--severity") : undefined,
53657
+ status: opts.status ? parseChoice(opts.status, STATUSES2, "--status") : undefined,
53658
+ severity: opts.severity ? parseChoice(opts.severity, SEVERITIES3, "--severity") : undefined,
52408
53659
  probability: opts.probability ? parseChoice(opts.probability, PROBABILITIES, "--probability") : undefined,
52409
53660
  owner: opts.owner,
52410
53661
  mitigation: opts.mitigation,
@@ -52413,7 +53664,7 @@ function registerRiskCommands(program2) {
52413
53664
  plan_id: opts.plan,
52414
53665
  task_id: opts.task,
52415
53666
  tags: opts.tag.length > 0 ? tagsFromOption2(opts.tag) : undefined,
52416
- metadata: parseJsonObject7(opts.metadataJson, "--metadata-json")
53667
+ metadata: parseJsonObject8(opts.metadataJson, "--metadata-json")
52417
53668
  }, getDatabase());
52418
53669
  if (opts.json || globalOpts.json)
52419
53670
  output(risk, true);
@@ -52471,13 +53722,13 @@ function registerRiskCommands(program2) {
52471
53722
  }
52472
53723
  });
52473
53724
  }
52474
- var STATUSES, SEVERITIES2, PROBABILITIES;
53725
+ var STATUSES2, SEVERITIES3, PROBABILITIES;
52475
53726
  var init_risk_commands = __esm(() => {
52476
53727
  init_database();
52477
53728
  init_project_risks();
52478
53729
  init_helpers();
52479
- STATUSES = ["open", "mitigating", "resolved", "accepted"];
52480
- SEVERITIES2 = ["low", "medium", "high", "critical"];
53730
+ STATUSES2 = ["open", "mitigating", "resolved", "accepted"];
53731
+ SEVERITIES3 = ["low", "medium", "high", "critical"];
52481
53732
  PROBABILITIES = ["low", "medium", "high"];
52482
53733
  });
52483
53734
 
@@ -55062,6 +56313,92 @@ var init_json_contracts = __esm(() => {
55062
56313
  },
55063
56314
  optional: {}
55064
56315
  }),
56316
+ contract({
56317
+ id: "loop_run_transaction",
56318
+ name: "Loop Run Transaction",
56319
+ description: "Compact local result for idempotent loop run begin/finish transactions.",
56320
+ surfaces: ["cli", "mcp", "sdk"],
56321
+ stability: "stable",
56322
+ required: {
56323
+ schema_version: field("string", "Result schema version."),
56324
+ local_only: field("boolean", "Always true; loop run transactions use local state."),
56325
+ dry_run: field("boolean", "True when no run ledger mutation was applied."),
56326
+ processed_at: isoDateField,
56327
+ action: field("string", "preview, created, matched, finished, or conflict."),
56328
+ key: field("string", "Stable idempotency key used to dedupe the transaction."),
56329
+ run: field(["object", "null"], "Compact run summary or null for create previews.", true),
56330
+ warnings: field("array", "Non-fatal warnings such as terminal-status conflicts."),
56331
+ commands: field("array", "Follow-up CLI commands for agents and operators.")
56332
+ },
56333
+ optional: {}
56334
+ }),
56335
+ contract({
56336
+ id: "task_finding",
56337
+ name: "Task Finding",
56338
+ description: "Compact local finding record deduped by task and fingerprint.",
56339
+ surfaces: ["cli", "mcp", "sdk"],
56340
+ stability: "stable",
56341
+ required: {
56342
+ schema_version: field("string", "Finding schema version."),
56343
+ id: idField,
56344
+ task_id: idField,
56345
+ run_id: field(["string", "null"], "Optional run ledger ID.", true),
56346
+ fingerprint: field("string", "Stable finding fingerprint scoped to the task."),
56347
+ title: field("string", "Short redacted finding title."),
56348
+ severity: field("string", "low, medium, high, or critical."),
56349
+ status: field("string", "open, resolved, or ignored."),
56350
+ source: field(["string", "null"], "Optional loop/tool source.", true),
56351
+ summary: field(["string", "null"], "Bounded redacted finding summary.", true),
56352
+ artifact_path: field(["string", "null"], "Local artifact path/reference; raw content is not included.", true),
56353
+ first_seen_at: isoDateField,
56354
+ last_seen_at: isoDateField,
56355
+ resolved_at: field(["string", "null"], "Resolution timestamp when closed.", true),
56356
+ metadata_keys: field("array", "Sorted metadata keys; metadata values are intentionally omitted in compact output.")
56357
+ },
56358
+ optional: {}
56359
+ }),
56360
+ contract({
56361
+ id: "task_finding_upsert",
56362
+ name: "Task Finding Upsert Result",
56363
+ description: "Local-only dry-run or applied result from idempotently upserting a task finding.",
56364
+ surfaces: ["cli", "mcp", "sdk"],
56365
+ stability: "stable",
56366
+ required: {
56367
+ schema_version: field("string", "Result schema version."),
56368
+ local_only: field("boolean", "Always true; finding upserts use local state."),
56369
+ dry_run: field("boolean", "True when no finding row was created or updated."),
56370
+ processed_at: isoDateField,
56371
+ action: field("string", "preview, created, matched, updated, or reopened."),
56372
+ fingerprint: field("string", "Normalized finding fingerprint."),
56373
+ finding: field(["object", "null"], "Compact finding summary or null for create previews.", true),
56374
+ warnings: field("array", "Non-fatal warnings.")
56375
+ },
56376
+ optional: {}
56377
+ }),
56378
+ contract({
56379
+ id: "task_finding_resolve_missing",
56380
+ name: "Task Finding Resolve Missing Result",
56381
+ description: "Local-only dry-run or applied result from resolving open findings absent from the latest loop finding set.",
56382
+ surfaces: ["cli", "mcp", "sdk"],
56383
+ stability: "stable",
56384
+ required: {
56385
+ schema_version: field("string", "Result schema version."),
56386
+ local_only: field("boolean", "Always true; finding resolution uses local state."),
56387
+ dry_run: field("boolean", "True when no finding rows were changed."),
56388
+ processed_at: isoDateField,
56389
+ action: field("string", "preview, resolved, ignored, or noop."),
56390
+ task_id: idField,
56391
+ source: field(["string", "null"], "Optional source scope.", true),
56392
+ run_id: field(["string", "null"], "Optional run ledger ID used for audit metadata.", true),
56393
+ present_fingerprint_count: field("integer", "Number of fingerprints supplied as still present."),
56394
+ candidate_count: field("integer", "Open findings missing from the supplied set."),
56395
+ changed_count: field("integer", "Rows resolved or ignored by the applied transaction."),
56396
+ omitted_count: field("integer", "Matching findings omitted from bounded output."),
56397
+ findings: field("array", "Bounded compact finding summaries."),
56398
+ warnings: field("array", "Non-fatal warnings.")
56399
+ },
56400
+ optional: {}
56401
+ }),
55065
56402
  contract({
55066
56403
  id: "verification_provider",
55067
56404
  name: "Verification Provider",