@hasna/todos 0.11.65 → 0.11.67

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 (55) hide show
  1. package/README.md +78 -49
  2. package/dashboard/dist/assets/{index-aJefI7kh.js → index-DVotjwab.js} +1 -1
  3. package/dashboard/dist/index.html +1 -1
  4. package/dist/cli/commands/project-commands.d.ts.map +1 -1
  5. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  6. package/dist/cli/commands/storage-commands.d.ts +5 -0
  7. package/dist/cli/commands/storage-commands.d.ts.map +1 -1
  8. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  9. package/dist/cli/index.js +1034 -231
  10. package/dist/contracts.js +409 -129
  11. package/dist/db/agents.d.ts.map +1 -1
  12. package/dist/db/audit.d.ts.map +1 -1
  13. package/dist/db/migrations.d.ts.map +1 -1
  14. package/dist/db/plans.d.ts.map +1 -1
  15. package/dist/db/projects.d.ts.map +1 -1
  16. package/dist/db/schema.d.ts.map +1 -1
  17. package/dist/db/storage-tombstones.d.ts +26 -0
  18. package/dist/db/storage-tombstones.d.ts.map +1 -0
  19. package/dist/db/task-crud.d.ts.map +1 -1
  20. package/dist/db/task-lifecycle.d.ts.map +1 -1
  21. package/dist/db/task-lists.d.ts.map +1 -1
  22. package/dist/db/task-runs.d.ts.map +1 -1
  23. package/dist/db/templates.d.ts.map +1 -1
  24. package/dist/index.d.ts +4 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +928 -219
  27. package/dist/lib/approval-gates.d.ts.map +1 -1
  28. package/dist/lib/event-emission-safety.d.ts +9 -0
  29. package/dist/lib/event-emission-safety.d.ts.map +1 -0
  30. package/dist/lib/event-hooks.d.ts +1 -0
  31. package/dist/lib/event-hooks.d.ts.map +1 -1
  32. package/dist/lib/feature-manifest.d.ts.map +1 -1
  33. package/dist/lib/project-bootstrap.d.ts +1 -0
  34. package/dist/lib/project-bootstrap.d.ts.map +1 -1
  35. package/dist/lib/review-queues.d.ts.map +1 -1
  36. package/dist/lib/shared-events.d.ts +1 -0
  37. package/dist/lib/shared-events.d.ts.map +1 -1
  38. package/dist/lib/task-route-contract.d.ts +33 -0
  39. package/dist/lib/task-route-contract.d.ts.map +1 -0
  40. package/dist/lib/task-routing.d.ts +55 -0
  41. package/dist/lib/task-routing.d.ts.map +1 -0
  42. package/dist/mcp/index.js +459 -162
  43. package/dist/registry.js +409 -129
  44. package/dist/release-provenance.json +3 -3
  45. package/dist/server/index.js +482 -185
  46. package/dist/storage/interfaces.d.ts +20 -0
  47. package/dist/storage/interfaces.d.ts.map +1 -1
  48. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  49. package/dist/storage/postgres-sync.d.ts +6 -1
  50. package/dist/storage/postgres-sync.d.ts.map +1 -1
  51. package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
  52. package/dist/storage.js +677 -152
  53. package/dist/types/index.d.ts +22 -0
  54. package/dist/types/index.d.ts.map +1 -1
  55. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1182,6 +1182,22 @@ var init_migrations = __esm(() => {
1182
1182
  CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source);
1183
1183
  CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint);
1184
1184
  INSERT OR IGNORE INTO _migrations (id) VALUES (62);
1185
+ `,
1186
+ `
1187
+ CREATE TABLE IF NOT EXISTS storage_tombstones (
1188
+ id TEXT PRIMARY KEY,
1189
+ object_type TEXT NOT NULL,
1190
+ object_id TEXT NOT NULL,
1191
+ deleted_at TEXT NOT NULL,
1192
+ updated_at TEXT NOT NULL,
1193
+ source_machine_id TEXT,
1194
+ payload TEXT,
1195
+ version INTEGER,
1196
+ UNIQUE(object_type, object_id)
1197
+ );
1198
+ CREATE INDEX IF NOT EXISTS idx_storage_tombstones_object ON storage_tombstones(object_type, object_id);
1199
+ CREATE INDEX IF NOT EXISTS idx_storage_tombstones_updated ON storage_tombstones(updated_at);
1200
+ INSERT OR IGNORE INTO _migrations (id) VALUES (63);
1185
1201
  `
1186
1202
  ];
1187
1203
  });
@@ -1703,6 +1719,20 @@ function ensureSchema(db) {
1703
1719
  )`);
1704
1720
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_project ON project_machine_paths(project_id)");
1705
1721
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_machine ON project_machine_paths(machine_id)");
1722
+ ensureTable("storage_tombstones", `
1723
+ CREATE TABLE storage_tombstones (
1724
+ id TEXT PRIMARY KEY,
1725
+ object_type TEXT NOT NULL,
1726
+ object_id TEXT NOT NULL,
1727
+ deleted_at TEXT NOT NULL,
1728
+ updated_at TEXT NOT NULL,
1729
+ source_machine_id TEXT,
1730
+ payload TEXT,
1731
+ version INTEGER,
1732
+ UNIQUE(object_type, object_id)
1733
+ )`);
1734
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_storage_tombstones_object ON storage_tombstones(object_type, object_id)");
1735
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_storage_tombstones_updated ON storage_tombstones(updated_at)");
1706
1736
  ensureTable("machines", `
1707
1737
  CREATE TABLE machines (
1708
1738
  id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, hostname TEXT, platform TEXT,
@@ -6173,6 +6203,80 @@ init_config();
6173
6203
  init_types();
6174
6204
  init_database();
6175
6205
  init_machines();
6206
+
6207
+ // src/db/storage-tombstones.ts
6208
+ init_database();
6209
+ init_machines();
6210
+ function recordStorageTombstone(input, db) {
6211
+ const d = db ?? getDatabase();
6212
+ const deletedAt = input.deleted_at ?? now();
6213
+ const machineId = input.source_machine_id ?? currentStorageMachineId(d);
6214
+ d.run(`INSERT INTO storage_tombstones (
6215
+ id, object_type, object_id, deleted_at, updated_at, source_machine_id, payload, version
6216
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
6217
+ ON CONFLICT(object_type, object_id) DO UPDATE SET
6218
+ deleted_at = excluded.deleted_at,
6219
+ updated_at = excluded.updated_at,
6220
+ source_machine_id = excluded.source_machine_id,
6221
+ payload = excluded.payload,
6222
+ version = excluded.version
6223
+ WHERE storage_tombstones.updated_at IS NULL OR storage_tombstones.updated_at <= excluded.updated_at`, [
6224
+ uuid(),
6225
+ input.object_type,
6226
+ input.object_id,
6227
+ deletedAt,
6228
+ deletedAt,
6229
+ machineId,
6230
+ input.payload ? JSON.stringify(input.payload) : null,
6231
+ input.version ?? null
6232
+ ]);
6233
+ return getStorageTombstone(input.object_type, input.object_id, d);
6234
+ }
6235
+ function getStorageTombstone(objectType, objectId, db) {
6236
+ const d = db ?? getDatabase();
6237
+ const row = d.query("SELECT * FROM storage_tombstones WHERE object_type = ? AND object_id = ?").get(objectType, objectId);
6238
+ return row ? rowToStorageTombstone(row) : null;
6239
+ }
6240
+ function listStorageTombstones(db) {
6241
+ const d = db ?? getDatabase();
6242
+ return d.query("SELECT * FROM storage_tombstones ORDER BY updated_at ASC, object_type ASC, object_id ASC").all().map(rowToStorageTombstone);
6243
+ }
6244
+ function shouldApplyStorageTombstone(tombstone, existingUpdatedAt) {
6245
+ const tombstoneClock = Date.parse(tombstone.updated_at || tombstone.deleted_at);
6246
+ if (!existingUpdatedAt)
6247
+ return true;
6248
+ const existingClock = Date.parse(existingUpdatedAt);
6249
+ if (Number.isNaN(tombstoneClock))
6250
+ return true;
6251
+ if (Number.isNaN(existingClock))
6252
+ return true;
6253
+ return tombstoneClock >= existingClock;
6254
+ }
6255
+ function rowToStorageTombstone(row) {
6256
+ return {
6257
+ ...row,
6258
+ payload: parsePayload(row.payload)
6259
+ };
6260
+ }
6261
+ function parsePayload(value) {
6262
+ if (!value)
6263
+ return null;
6264
+ try {
6265
+ const parsed = JSON.parse(value);
6266
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
6267
+ } catch {
6268
+ return null;
6269
+ }
6270
+ }
6271
+ function currentStorageMachineId(db) {
6272
+ try {
6273
+ return getMachineId(db);
6274
+ } catch {
6275
+ return null;
6276
+ }
6277
+ }
6278
+
6279
+ // src/db/projects.ts
6176
6280
  function slugify(name) {
6177
6281
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
6178
6282
  }
@@ -6202,8 +6306,9 @@ function createProject(input, db) {
6202
6306
  const timestamp = now();
6203
6307
  const taskListId = input.task_list_id ?? `todos-${slugify(input.name)}`;
6204
6308
  const taskPrefix = input.task_prefix || generatePrefix(input.name, d);
6205
- d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at)
6206
- VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp, timestamp]);
6309
+ const machineId = currentStorageMachineId(d);
6310
+ d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at, machine_id)
6311
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp, timestamp, machineId]);
6207
6312
  return getProject(id, d);
6208
6313
  }
6209
6314
  function getProject(id, db) {
@@ -6282,6 +6387,14 @@ function renameProject(id, input, db) {
6282
6387
  }
6283
6388
  function deleteProject(id, db) {
6284
6389
  const d = db || getDatabase();
6390
+ const project = getProject(id, d);
6391
+ if (!project)
6392
+ return false;
6393
+ recordStorageTombstone({
6394
+ object_type: "projects",
6395
+ object_id: id,
6396
+ payload: project
6397
+ }, d);
6285
6398
  const result = d.run("DELETE FROM projects WHERE id = ?", [id]);
6286
6399
  return result.changes > 0;
6287
6400
  }
@@ -6386,6 +6499,14 @@ function listMachineLocalPaths(projectId, db) {
6386
6499
  function removeMachineLocalPath(projectId, machineId, db) {
6387
6500
  const d = db || getDatabase();
6388
6501
  const mid = machineId ?? getMachineId(d);
6502
+ const existing = d.query("SELECT * FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(projectId, mid);
6503
+ if (!existing)
6504
+ return false;
6505
+ recordStorageTombstone({
6506
+ object_type: "project_machine_paths",
6507
+ object_id: existing.id,
6508
+ payload: existing
6509
+ }, d);
6389
6510
  const result = d.run("DELETE FROM project_machine_paths WHERE project_id = ? AND machine_id = ?", [projectId, mid]);
6390
6511
  return result.changes > 0;
6391
6512
  }
@@ -6434,20 +6555,69 @@ function checkCompletionGuard(task, agentId, db, configOverride) {
6434
6555
  }
6435
6556
  }
6436
6557
 
6558
+ // src/lib/event-emission-safety.ts
6559
+ init_database();
6560
+ init_sync_utils();
6561
+ import { tmpdir } from "os";
6562
+ import { resolve as resolve3, sep } from "path";
6563
+ function envFlag(name) {
6564
+ const value = process.env[name];
6565
+ return value === "1" || value === "true" || value === "yes";
6566
+ }
6567
+ function isUnder(parent, child) {
6568
+ const normalizedParent = resolve3(parent);
6569
+ const normalizedChild = resolve3(child);
6570
+ return normalizedChild === normalizedParent || normalizedChild.startsWith(`${normalizedParent}${sep}`);
6571
+ }
6572
+ function databasePathFromDatabase(db) {
6573
+ const filename = db?.filename;
6574
+ return typeof filename === "string" && filename.trim() ? filename : undefined;
6575
+ }
6576
+ function isEphemeralTodosDatabase(dbPath) {
6577
+ const resolvedPath = dbPath ?? getDatabasePath();
6578
+ if (resolvedPath === ":memory:" || resolvedPath.startsWith("file::memory:"))
6579
+ return true;
6580
+ return isUnder(tmpdir(), resolvedPath);
6581
+ }
6582
+ function hasExplicitSharedEventsStore() {
6583
+ return Boolean(process.env["HASNA_EVENTS_DIR"] || process.env["HASNA_EVENTS_HOME"]);
6584
+ }
6585
+ function usesIsolatedTodosHome() {
6586
+ return isUnder(tmpdir(), getTodosGlobalDir());
6587
+ }
6588
+ function shouldEmitSharedTaskEvents(dbPath) {
6589
+ if (envFlag("TODOS_DISABLE_SHARED_EVENTS"))
6590
+ return false;
6591
+ if (envFlag("TODOS_ALLOW_EPHEMERAL_SHARED_EVENTS"))
6592
+ return true;
6593
+ if (!isEphemeralTodosDatabase(dbPath))
6594
+ return true;
6595
+ return hasExplicitSharedEventsStore();
6596
+ }
6597
+ function shouldDeliverLocalLifecycleHooks(dbPath) {
6598
+ if (envFlag("TODOS_DISABLE_LOCAL_EVENT_HOOKS"))
6599
+ return false;
6600
+ if (envFlag("TODOS_ALLOW_EPHEMERAL_LOCAL_EVENT_HOOKS"))
6601
+ return true;
6602
+ if (!isEphemeralTodosDatabase(dbPath))
6603
+ return true;
6604
+ return usesIsolatedTodosHome();
6605
+ }
6606
+
6437
6607
  // src/lib/event-hooks.ts
6438
6608
  init_redaction();
6439
6609
  import { createHash, randomUUID } from "crypto";
6440
6610
  import { appendFileSync, mkdirSync as mkdirSync3 } from "fs";
6441
- import { dirname as dirname4, resolve as resolve5 } from "path";
6611
+ import { dirname as dirname4, resolve as resolve6 } from "path";
6442
6612
  import { createConnection } from "net";
6443
6613
 
6444
6614
  // src/lib/runner-sandbox.ts
6445
6615
  init_config();
6446
- import { relative as relative2, resolve as resolve4 } from "path";
6616
+ import { relative as relative2, resolve as resolve5 } from "path";
6447
6617
 
6448
6618
  // src/lib/workspace-trust.ts
6449
6619
  init_config();
6450
- import { relative, resolve as resolve3 } from "path";
6620
+ import { relative, resolve as resolve4 } from "path";
6451
6621
  var DEFAULT_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
6452
6622
  var DEFAULT_ENV_REDACTIONS = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
6453
6623
  var PRESET_DEFAULTS = {
@@ -6493,7 +6663,7 @@ var PRESET_DEFAULTS = {
6493
6663
  }
6494
6664
  };
6495
6665
  function normalizePath(path) {
6496
- return resolve3(path);
6666
+ return resolve4(path);
6497
6667
  }
6498
6668
  function unique2(values) {
6499
6669
  return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
@@ -6630,7 +6800,7 @@ function checkWorkspacePermission(input = {}) {
6630
6800
  var DEFAULT_COMMAND_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
6631
6801
  var DEFAULT_ENV_REDACTIONS2 = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
6632
6802
  function normalizePath2(path) {
6633
- return resolve4(path);
6803
+ return resolve5(path);
6634
6804
  }
6635
6805
  function unique3(values) {
6636
6806
  return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
@@ -6976,7 +7146,7 @@ async function deliverHook(hook, envelope) {
6976
7146
  if (hook.target === "stdout") {
6977
7147
  output = line.trim();
6978
7148
  } else if (hook.target === "file") {
6979
- const filePath = resolve5(hook.file_path);
7149
+ const filePath = resolve6(hook.file_path);
6980
7150
  mkdirSync3(dirname4(filePath), { recursive: true });
6981
7151
  appendFileSync(filePath, line);
6982
7152
  } else if (hook.target === "socket") {
@@ -7051,6 +7221,8 @@ async function emitLocalEventHooks(input) {
7051
7221
  return Promise.all(hooks.map((hook) => deliverHook(hook, envelope)));
7052
7222
  }
7053
7223
  function emitLocalEventHooksQuiet(input) {
7224
+ if (!shouldDeliverLocalLifecycleHooks(input.databasePath))
7225
+ return;
7054
7226
  emitLocalEventHooks(input).catch(() => {});
7055
7227
  }
7056
7228
  async function testLocalEventHook(name, input) {
@@ -7367,7 +7539,7 @@ async function dispatchCommand(event, channel) {
7367
7539
  HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
7368
7540
  HASNA_EVENT_JSON: eventJson
7369
7541
  };
7370
- return new Promise((resolve6) => {
7542
+ return new Promise((resolve7) => {
7371
7543
  const child = spawn(channel.command.command, channel.command.args ?? [], {
7372
7544
  cwd: channel.command.cwd,
7373
7545
  env,
@@ -7385,7 +7557,7 @@ async function dispatchCommand(event, channel) {
7385
7557
  });
7386
7558
  child.on("error", (error) => {
7387
7559
  clearTimeout(timeout);
7388
- resolve6({
7560
+ resolve7({
7389
7561
  attempt: 1,
7390
7562
  status: "failed",
7391
7563
  startedAt,
@@ -7398,7 +7570,7 @@ async function dispatchCommand(event, channel) {
7398
7570
  child.on("close", (code, signal) => {
7399
7571
  clearTimeout(timeout);
7400
7572
  const success = code === 0;
7401
- resolve6({
7573
+ resolve7({
7402
7574
  attempt: 1,
7403
7575
  status: success ? "success" : "failed",
7404
7576
  startedAt,
@@ -7666,14 +7838,15 @@ function createTaskList(input, db) {
7666
7838
  const id = uuid();
7667
7839
  const timestamp = now();
7668
7840
  const slug = input.slug || slugify(input.name);
7841
+ const machineId = currentStorageMachineId(d);
7669
7842
  if (!input.project_id) {
7670
7843
  const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
7671
7844
  if (existing) {
7672
7845
  throw new Error(`Standalone task list with slug "${slug}" already exists`);
7673
7846
  }
7674
7847
  }
7675
- d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
7676
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
7848
+ d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at, machine_id)
7849
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp, machineId]);
7677
7850
  return getTaskList(id, d);
7678
7851
  }
7679
7852
  function getTaskList(id, db) {
@@ -7723,6 +7896,14 @@ function updateTaskList(id, input, db) {
7723
7896
  }
7724
7897
  function deleteTaskList(id, db) {
7725
7898
  const d = db || getDatabase();
7899
+ const list = getTaskList(id, d);
7900
+ if (!list)
7901
+ return false;
7902
+ recordStorageTombstone({
7903
+ object_type: "task_lists",
7904
+ object_id: id,
7905
+ payload: list
7906
+ }, d);
7726
7907
  return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
7727
7908
  }
7728
7909
  function ensureTaskList(name, slug, projectId, db) {
@@ -7733,39 +7914,9 @@ function ensureTaskList(name, slug, projectId, db) {
7733
7914
  return createTaskList({ name, slug, project_id: projectId }, d);
7734
7915
  }
7735
7916
 
7736
- // src/lib/shared-events.ts
7737
- var SOURCE = "todos";
7738
- function taskEventData(task, extra = {}) {
7739
- return {
7740
- id: task.id,
7741
- task_id: task.id,
7742
- short_id: task.short_id,
7743
- title: task.title,
7744
- description: task.description,
7745
- status: task.status,
7746
- priority: task.priority,
7747
- project_id: task.project_id,
7748
- parent_id: task.parent_id,
7749
- plan_id: task.plan_id,
7750
- task_list_id: task.task_list_id,
7751
- agent_id: task.agent_id,
7752
- assigned_to: task.assigned_to,
7753
- session_id: task.session_id,
7754
- working_dir: task.working_dir,
7755
- tags: task.tags,
7756
- metadata: task.metadata,
7757
- version: task.version,
7758
- created_at: task.created_at,
7759
- updated_at: task.updated_at,
7760
- started_at: task.started_at,
7761
- completed_at: task.completed_at,
7762
- due_at: task.due_at,
7763
- requires_approval: task.requires_approval,
7764
- approved_by: task.approved_by,
7765
- approved_at: task.approved_at,
7766
- ...extra
7767
- };
7768
- }
7917
+ // src/lib/task-route-contract.ts
7918
+ var TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION = "todos.task_route_state.v1";
7919
+ var TASK_WORKFLOW_POINTER_SCHEMA_VERSION = "todos.task_workflow_pointer.v1";
7769
7920
  function booleanField(value) {
7770
7921
  if (typeof value === "boolean")
7771
7922
  return value;
@@ -7787,21 +7938,31 @@ function booleanField(value) {
7787
7938
  function objectField(value) {
7788
7939
  return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
7789
7940
  }
7790
- function firstBoolean(records, keys) {
7941
+ function collectBooleans(records, keys) {
7942
+ const values = [];
7791
7943
  for (const record of records) {
7944
+ if (!record)
7945
+ continue;
7792
7946
  for (const key of keys) {
7793
7947
  const value = booleanField(record[key]);
7794
7948
  if (value !== undefined)
7795
- return value;
7949
+ values.push(value);
7796
7950
  }
7797
7951
  }
7798
- return;
7952
+ return values;
7799
7953
  }
7800
- function routingAutomationMetadata(task) {
7801
- const automation = objectField(task.metadata.automation);
7802
- const records = [task.metadata];
7803
- if (automation)
7804
- records.push(automation);
7954
+ function mergedBoolean(records, keys, trueWins) {
7955
+ const values = collectBooleans(records, keys);
7956
+ if (values.length === 0)
7957
+ return;
7958
+ if (trueWins)
7959
+ return values.some(Boolean);
7960
+ return values.includes(false) ? false : true;
7961
+ }
7962
+ function routingAutomationMetadata(task, taskList) {
7963
+ const taskAutomation = objectField(task.metadata.automation);
7964
+ const taskListAutomation = taskList ? objectField(taskList.metadata.automation) : undefined;
7965
+ const records = [task.metadata, taskAutomation, taskList?.metadata, taskListAutomation];
7805
7966
  const result = {};
7806
7967
  const aliases = [
7807
7968
  ["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
@@ -7812,7 +7973,7 @@ function routingAutomationMetadata(task) {
7812
7973
  ["approval_required", ["approval_required", "approvalRequired"]]
7813
7974
  ];
7814
7975
  for (const [canonical, keys] of aliases) {
7815
- const value = firstBoolean(records, keys);
7976
+ const value = mergedBoolean(records, keys, canonical !== "allowed");
7816
7977
  if (value !== undefined)
7817
7978
  result[canonical] = value;
7818
7979
  }
@@ -7820,23 +7981,91 @@ function routingAutomationMetadata(task) {
7820
7981
  result.requires_approval = true;
7821
7982
  return Object.keys(result).length > 0 ? result : undefined;
7822
7983
  }
7984
+ function routeEnabledForTask(task, taskList) {
7985
+ const explicit = booleanField(task.metadata.route_enabled);
7986
+ if (explicit !== undefined)
7987
+ return explicit;
7988
+ if (task.tags.includes("auto:route") || task.tags.includes("route:enabled"))
7989
+ return true;
7990
+ const taskListDefault = taskList ? booleanField(taskList.metadata.route_enabled) : undefined;
7991
+ if (taskListDefault !== undefined)
7992
+ return taskListDefault;
7993
+ return;
7994
+ }
7995
+ function stringField(value) {
7996
+ return typeof value === "string" && value.trim() ? value : undefined;
7997
+ }
7998
+ function workflowPointersFromMetadata(metadata) {
7999
+ const nested = objectField(metadata.workflow_invocation) ?? objectField(metadata.workflow) ?? {};
8000
+ return {
8001
+ current_workflow_invocation_id: stringField(metadata.current_workflow_invocation_id) ?? stringField(nested.current_workflow_invocation_id) ?? stringField(nested.invocation_id),
8002
+ current_run_id: stringField(metadata.current_run_id) ?? stringField(nested.current_run_id) ?? stringField(nested.run_id),
8003
+ latest_manifest_path: stringField(metadata.latest_manifest_path) ?? stringField(nested.latest_manifest_path) ?? stringField(nested.manifest_path),
8004
+ latest_evaluation_path: stringField(metadata.latest_evaluation_path) ?? stringField(nested.latest_evaluation_path) ?? stringField(nested.evaluation_path),
8005
+ workflow_state: stringField(metadata.workflow_state) ?? stringField(nested.workflow_state) ?? stringField(nested.state)
8006
+ };
8007
+ }
8008
+ function compactWorkflowPointers(pointers) {
8009
+ return Object.fromEntries(Object.entries(pointers).filter(([, value]) => typeof value === "string" && value.length > 0));
8010
+ }
8011
+ function classifyProjectKind(path) {
8012
+ return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
8013
+ }
8014
+ function isWorktreePath(path) {
8015
+ return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
8016
+ }
8017
+ function inferRootProjectId(project) {
8018
+ return isWorktreePath(project.path) ? null : project.id;
8019
+ }
8020
+
8021
+ // src/lib/shared-events.ts
8022
+ var SOURCE = "todos";
8023
+ function taskEventData(task, extra = {}) {
8024
+ return {
8025
+ id: task.id,
8026
+ task_id: task.id,
8027
+ short_id: task.short_id,
8028
+ title: task.title,
8029
+ description: task.description,
8030
+ status: task.status,
8031
+ priority: task.priority,
8032
+ project_id: task.project_id,
8033
+ parent_id: task.parent_id,
8034
+ plan_id: task.plan_id,
8035
+ task_list_id: task.task_list_id,
8036
+ agent_id: task.agent_id,
8037
+ assigned_to: task.assigned_to,
8038
+ session_id: task.session_id,
8039
+ working_dir: task.working_dir,
8040
+ tags: task.tags,
8041
+ metadata: task.metadata,
8042
+ version: task.version,
8043
+ created_at: task.created_at,
8044
+ updated_at: task.updated_at,
8045
+ started_at: task.started_at,
8046
+ completed_at: task.completed_at,
8047
+ due_at: task.due_at,
8048
+ requires_approval: task.requires_approval,
8049
+ approved_by: task.approved_by,
8050
+ approved_at: task.approved_at,
8051
+ ...extra
8052
+ };
8053
+ }
7823
8054
  function taskEventMetadata(task) {
7824
8055
  const metadata = {
7825
8056
  package: "@hasna/todos",
7826
8057
  todos_event_schema_version: 1,
8058
+ route_state_schema_version: TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION,
7827
8059
  task_id: task.id,
7828
8060
  task_short_id: task.short_id,
7829
8061
  project_id: task.project_id,
7830
8062
  task_list_id: task.task_list_id,
7831
8063
  working_dir: task.working_dir
7832
8064
  };
7833
- const routeEnabled = booleanField(task.metadata.route_enabled);
7834
- if (routeEnabled !== undefined) {
7835
- metadata.route_enabled = routeEnabled;
7836
- }
7837
- const automation = routingAutomationMetadata(task);
7838
- if (automation) {
7839
- metadata.automation = automation;
8065
+ const pointers = workflowPointersFromMetadata(task.metadata);
8066
+ for (const [key, value] of Object.entries(pointers)) {
8067
+ if (value)
8068
+ metadata[key] = value;
7840
8069
  }
7841
8070
  try {
7842
8071
  const project = task.project_id ? getProject(task.project_id) : null;
@@ -7865,18 +8094,20 @@ function taskEventMetadata(task) {
7865
8094
  metadata.task_list_project_id = taskList.project_id;
7866
8095
  metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
7867
8096
  }
8097
+ const routeEnabled = routeEnabledForTask(task, taskList);
8098
+ if (routeEnabled !== undefined) {
8099
+ metadata.route_enabled = routeEnabled;
8100
+ }
8101
+ const automation = routingAutomationMetadata(task, taskList);
8102
+ if (automation) {
8103
+ metadata.automation = automation;
8104
+ metadata.route_blocked_by_no_auto = automation.no_auto === true;
8105
+ metadata.route_blocked_by_manual = automation.manual === true || automation.manual_required === true;
8106
+ metadata.route_blocked_by_approval = (automation.requires_approval === true || automation.approval_required === true) && !task.approved_by;
8107
+ }
7868
8108
  } catch {}
7869
8109
  return metadata;
7870
8110
  }
7871
- function classifyProjectKind(path) {
7872
- return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
7873
- }
7874
- function isWorktreePath(path) {
7875
- return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
7876
- }
7877
- function inferRootProjectId(project) {
7878
- return isWorktreePath(project.path) ? null : project.id;
7879
- }
7880
8111
  function readMachineLocalPath(project) {
7881
8112
  const machineId = process.env["TODOS_MACHINE_ID"];
7882
8113
  if (!machineId)
@@ -7889,6 +8120,8 @@ function readMachineLocalPath(project) {
7889
8120
  }
7890
8121
  }
7891
8122
  async function emitSharedTaskEvent(input) {
8123
+ if (!shouldEmitSharedTaskEvents(input.databasePath))
8124
+ return;
7892
8125
  const data = taskEventData(input.task, input.data);
7893
8126
  await new EventsClient().emit({
7894
8127
  source: SOURCE,
@@ -7913,8 +8146,9 @@ function logTaskChange(taskId, action, field2, oldValue, newValue, agentId, db)
7913
8146
  const d = db || getDatabase();
7914
8147
  const id = uuid();
7915
8148
  const timestamp = now();
7916
- d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at)
7917
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field2 || null, oldValue ?? null, newValue ?? null, agentId || null, timestamp]);
8149
+ const machineId = currentStorageMachineId(d);
8150
+ d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at, machine_id)
8151
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field2 || null, oldValue ?? null, newValue ?? null, agentId || null, timestamp, machineId]);
7918
8152
  try {
7919
8153
  const { logActivity: logActivity2 } = (init_activity_audit(), __toCommonJS(exports_activity_audit));
7920
8154
  logActivity2({
@@ -7927,7 +8161,7 @@ function logTaskChange(taskId, action, field2, oldValue, newValue, agentId, db)
7927
8161
  actor_id: agentId ?? undefined
7928
8162
  }, d);
7929
8163
  } catch {}
7930
- return { id, task_id: taskId, action, field: field2 || null, old_value: oldValue ?? null, new_value: newValue ?? null, agent_id: agentId || null, created_at: timestamp };
8164
+ return { id, task_id: taskId, action, field: field2 || null, old_value: oldValue ?? null, new_value: newValue ?? null, agent_id: agentId || null, created_at: timestamp, machine_id: machineId };
7931
8165
  }
7932
8166
  function getTaskHistory(taskId, db) {
7933
8167
  const d = db || getDatabase();
@@ -8276,13 +8510,14 @@ function createTask(input, db) {
8276
8510
  const d = db || getDatabase();
8277
8511
  const timestamp = now();
8278
8512
  const tags = input.tags || [];
8513
+ const machineId = currentStorageMachineId(d);
8279
8514
  const assignedBy = input.assigned_by || input.agent_id;
8280
8515
  const assignedFromProject = input.assigned_from_project || null;
8281
8516
  let id = uuid();
8282
8517
  for (let attempt = 0;attempt < 3; attempt++) {
8283
8518
  try {
8284
- d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, cycle_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, sla_minutes, confidence, retry_count, max_retries, retry_after, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project, task_type)
8285
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
8519
+ d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, cycle_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, sla_minutes, confidence, retry_count, max_retries, retry_after, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project, task_type, machine_id)
8520
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
8286
8521
  id,
8287
8522
  null,
8288
8523
  input.project_id || null,
@@ -8319,7 +8554,8 @@ function createTask(input, db) {
8319
8554
  input.spawned_from_session || null,
8320
8555
  assignedBy || null,
8321
8556
  assignedFromProject || null,
8322
- input.task_type || null
8557
+ input.task_type || null,
8558
+ machineId
8323
8559
  ]);
8324
8560
  break;
8325
8561
  } catch (e) {
@@ -8335,9 +8571,10 @@ function createTask(input, db) {
8335
8571
  }
8336
8572
  const task = getTask(id, d);
8337
8573
  const payload = taskEventData(task);
8574
+ const databasePath = databasePathFromDatabase(d);
8338
8575
  dispatchWebhook2("task.created", payload, d).catch(() => {});
8339
- emitLocalEventHooksQuiet({ type: "task.created", payload });
8340
- emitSharedTaskEventQuiet({ type: "task.created", task });
8576
+ emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
8577
+ emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
8341
8578
  return task;
8342
8579
  }
8343
8580
  function getTask(id, db) {
@@ -8755,29 +8992,39 @@ function updateTask(id, input, db) {
8755
8992
  approved_by: input.approved_by ?? task.approved_by,
8756
8993
  approved_at: input.approved_by ? timestamp : task.approved_at
8757
8994
  };
8995
+ const databasePath = databasePathFromDatabase(d);
8758
8996
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
8759
8997
  const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
8760
8998
  dispatchWebhook2("task.assigned", payload, d).catch(() => {});
8761
- emitLocalEventHooksQuiet({ type: "task.assigned", payload });
8762
- emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to } });
8999
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
9000
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to }, databasePath });
8763
9001
  }
8764
9002
  if (input.status !== undefined && input.status !== task.status) {
8765
9003
  const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
8766
9004
  dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
8767
- emitLocalEventHooksQuiet({ type: "task.status_changed", payload });
8768
- emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status } });
9005
+ emitLocalEventHooksQuiet({ type: "task.status_changed", payload, databasePath });
9006
+ emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status }, databasePath });
8769
9007
  }
8770
9008
  if (input.approved_by !== undefined) {
8771
- emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
9009
+ emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title }, databasePath });
8772
9010
  }
8773
9011
  const updatePayload = taskEventData(updatedTask);
8774
9012
  dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
8775
- emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
8776
- emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
9013
+ emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
9014
+ emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
8777
9015
  return updatedTask;
8778
9016
  }
8779
9017
  function deleteTask(id, db) {
8780
9018
  const d = db || getDatabase();
9019
+ const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
9020
+ if (!row)
9021
+ return false;
9022
+ recordStorageTombstone({
9023
+ object_type: "tasks",
9024
+ object_id: id,
9025
+ payload: rowToTask(row),
9026
+ version: row.version
9027
+ }, d);
8781
9028
  const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
8782
9029
  return result.changes > 0;
8783
9030
  }
@@ -8909,8 +9156,9 @@ function resolveTemplateId(id, d) {
8909
9156
  function createTemplate(input, db) {
8910
9157
  const d = db || getDatabase();
8911
9158
  const id = uuid();
8912
- d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at)
8913
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
9159
+ const machineId = currentStorageMachineId(d);
9160
+ d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at, machine_id)
9161
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
8914
9162
  id,
8915
9163
  input.name,
8916
9164
  input.title_pattern,
@@ -8921,7 +9169,8 @@ function createTemplate(input, db) {
8921
9169
  input.project_id || null,
8922
9170
  input.plan_id || null,
8923
9171
  JSON.stringify(input.metadata || {}),
8924
- now()
9172
+ now(),
9173
+ machineId
8925
9174
  ]);
8926
9175
  if (input.tasks && input.tasks.length > 0) {
8927
9176
  addTemplateTasks(id, input.tasks, d);
@@ -8945,6 +9194,15 @@ function deleteTemplate(id, db) {
8945
9194
  const resolved = resolveTemplateId(id, d);
8946
9195
  if (!resolved)
8947
9196
  return false;
9197
+ const template = getTemplate(resolved, d);
9198
+ if (!template)
9199
+ return false;
9200
+ recordStorageTombstone({
9201
+ object_type: "templates",
9202
+ object_id: resolved,
9203
+ payload: template,
9204
+ version: template.version
9205
+ }, d);
8948
9206
  return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
8949
9207
  }
8950
9208
  function updateTemplate(id, updates, db) {
@@ -9474,6 +9732,7 @@ function getBlockingDeps(id, db) {
9474
9732
  }
9475
9733
  function startTask(id, agentId, db) {
9476
9734
  const d = db || getDatabase();
9735
+ const databasePath = databasePathFromDatabase(d);
9477
9736
  const task = getTask(id, d);
9478
9737
  if (!task)
9479
9738
  throw new TaskNotFoundError(id);
@@ -9488,7 +9747,8 @@ function startTask(id, agentId, db) {
9488
9747
  agent_id: agentId,
9489
9748
  title: task.title,
9490
9749
  blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
9491
- }
9750
+ },
9751
+ databasePath
9492
9752
  });
9493
9753
  throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
9494
9754
  }
@@ -9510,12 +9770,13 @@ function startTask(id, agentId, db) {
9510
9770
  const startedTask = { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, started_at: task.started_at || timestamp, version: task.version + 1, updated_at: timestamp };
9511
9771
  const payload = taskEventData(startedTask, { agent_id: agentId });
9512
9772
  dispatchWebhook2("task.started", payload, d).catch(() => {});
9513
- emitLocalEventHooksQuiet({ type: "task.started", payload });
9514
- emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId } });
9773
+ emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
9774
+ emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId }, databasePath });
9515
9775
  return startedTask;
9516
9776
  }
9517
9777
  function completeTask(id, agentId, db, options) {
9518
9778
  const d = db || getDatabase();
9779
+ const databasePath = databasePathFromDatabase(d);
9519
9780
  const task = getTask(id, d);
9520
9781
  if (!task)
9521
9782
  throw new TaskNotFoundError(id);
@@ -9561,8 +9822,8 @@ function completeTask(id, agentId, db, options) {
9561
9822
  };
9562
9823
  const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
9563
9824
  dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
9564
- emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload });
9565
- emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp } });
9825
+ emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
9826
+ emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
9566
9827
  let spawnedTask = null;
9567
9828
  if (task.recurrence_rule && !options?.skip_recurrence) {
9568
9829
  spawnedTask = spawnNextRecurrence(task, d, timestamp);
@@ -9606,9 +9867,9 @@ function completeTask(id, agentId, db, options) {
9606
9867
  const depTask = getTask(dep.id, d);
9607
9868
  const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
9608
9869
  dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
9609
- emitLocalEventHooksQuiet({ type: "task.unblocked", payload });
9870
+ emitLocalEventHooksQuiet({ type: "task.unblocked", payload, databasePath });
9610
9871
  if (depTask)
9611
- emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id } });
9872
+ emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id }, databasePath });
9612
9873
  }
9613
9874
  }
9614
9875
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
@@ -9778,6 +10039,7 @@ function getTasksChangedSince(since, filters, db) {
9778
10039
  }
9779
10040
  function failTask(id, agentId, reason, options, db) {
9780
10041
  const d = db || getDatabase();
10042
+ const databasePath = databasePathFromDatabase(d);
9781
10043
  const task = getTask(id, d);
9782
10044
  if (!task)
9783
10045
  throw new TaskNotFoundError(id);
@@ -9806,8 +10068,8 @@ function failTask(id, agentId, reason, options, db) {
9806
10068
  logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
9807
10069
  const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
9808
10070
  dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
9809
- emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload });
9810
- emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning" });
10071
+ emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload, databasePath });
10072
+ emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning", databasePath });
9811
10073
  let retryTask;
9812
10074
  if (options?.retry) {
9813
10075
  const retryCount = (task.retry_count || 0) + 1;
@@ -9867,6 +10129,7 @@ function getStaleTasks(staleQuery = 30, filters, db) {
9867
10129
  }
9868
10130
  function stealTask(agentId, opts, db) {
9869
10131
  const d = db || getDatabase();
10132
+ const databasePath = databasePathFromDatabase(d);
9870
10133
  const staleMinutes = opts?.stale_minutes ?? 30;
9871
10134
  const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
9872
10135
  if (staleTasks.length === 0)
@@ -9885,8 +10148,8 @@ function stealTask(agentId, opts, db) {
9885
10148
  const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
9886
10149
  const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
9887
10150
  dispatchWebhook2("task.assigned", payload, d).catch(() => {});
9888
- emitLocalEventHooksQuiet({ type: "task.assigned", payload });
9889
- emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to } });
10151
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
10152
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to }, databasePath });
9890
10153
  return stolenTask;
9891
10154
  }
9892
10155
  function claimOrSteal(agentId, filters, db) {
@@ -10477,8 +10740,9 @@ function createPlan(input, db) {
10477
10740
  const d = db || getDatabase();
10478
10741
  const id = uuid();
10479
10742
  const timestamp = now();
10480
- d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at)
10481
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
10743
+ const machineId = currentStorageMachineId(d);
10744
+ d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at, machine_id)
10745
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
10482
10746
  id,
10483
10747
  input.project_id || null,
10484
10748
  input.task_list_id || null,
@@ -10487,7 +10751,8 @@ function createPlan(input, db) {
10487
10751
  input.description || null,
10488
10752
  input.status || "active",
10489
10753
  timestamp,
10490
- timestamp
10754
+ timestamp,
10755
+ machineId
10491
10756
  ]);
10492
10757
  return getPlan(id, d);
10493
10758
  }
@@ -10535,12 +10800,21 @@ function updatePlan(id, input, db) {
10535
10800
  const updated = getPlan(id, d);
10536
10801
  emitLocalEventHooksQuiet({
10537
10802
  type: "plan.updated",
10538
- payload: { id, old_status: plan.status, new_status: updated.status, name: updated.name, project_id: updated.project_id }
10803
+ payload: { id, old_status: plan.status, new_status: updated.status, name: updated.name, project_id: updated.project_id },
10804
+ databasePath: databasePathFromDatabase(d)
10539
10805
  });
10540
10806
  return updated;
10541
10807
  }
10542
10808
  function deletePlan(id, db) {
10543
10809
  const d = db || getDatabase();
10810
+ const plan = getPlan(id, d);
10811
+ if (!plan)
10812
+ return false;
10813
+ recordStorageTombstone({
10814
+ object_type: "plans",
10815
+ object_id: id,
10816
+ payload: plan
10817
+ }, d);
10544
10818
  const result = d.run("DELETE FROM plans WHERE id = ?", [id]);
10545
10819
  return result.changes > 0;
10546
10820
  }
@@ -10947,20 +11221,20 @@ init_database();
10947
11221
  init_redaction();
10948
11222
  import { createHash as createHash2 } from "crypto";
10949
11223
  import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
10950
- import { basename, dirname as dirname5, join as join6, resolve as resolve6 } from "path";
10951
- import { tmpdir } from "os";
11224
+ import { basename, dirname as dirname5, join as join6, resolve as resolve7 } from "path";
11225
+ import { tmpdir as tmpdir2 } from "os";
10952
11226
  function isInMemoryDb2(path) {
10953
11227
  return path === ":memory:" || path.startsWith("file::memory:");
10954
11228
  }
10955
11229
  function artifactStoreRoot() {
10956
11230
  if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
10957
- return resolve6(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
11231
+ return resolve7(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
10958
11232
  if (process.env["TODOS_ARTIFACTS_DIR"])
10959
- return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
11233
+ return resolve7(process.env["TODOS_ARTIFACTS_DIR"]);
10960
11234
  const dbPath = getDatabasePath();
10961
11235
  if (isInMemoryDb2(dbPath))
10962
- return join6(tmpdir(), "hasna-todos-artifacts");
10963
- return join6(dirname5(resolve6(dbPath)), "artifacts");
11236
+ return join6(tmpdir2(), "hasna-todos-artifacts");
11237
+ return join6(dirname5(resolve7(dbPath)), "artifacts");
10964
11238
  }
10965
11239
  function artifactStorePath(relativePath) {
10966
11240
  const normalized = relativePath.replace(/\\/g, "/");
@@ -11007,7 +11281,7 @@ function mediaTypeFor(path, textLike) {
11007
11281
  return "application/octet-stream";
11008
11282
  }
11009
11283
  function storeArtifactContent(input) {
11010
- const sourcePath = resolve6(input.path);
11284
+ const sourcePath = resolve7(input.path);
11011
11285
  if (!existsSync8(sourcePath))
11012
11286
  return null;
11013
11287
  const sourceStat = statSync2(sourcePath);
@@ -11163,19 +11437,19 @@ function importStoredArtifactContent(content) {
11163
11437
  var DEFAULT_DELETED_RETENTION_DAYS = 30;
11164
11438
  function getArtifactStoreRoot(dbPath) {
11165
11439
  if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
11166
- return resolve6(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
11440
+ return resolve7(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
11167
11441
  if (process.env["TODOS_ARTIFACTS_DIR"])
11168
- return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
11442
+ return resolve7(process.env["TODOS_ARTIFACTS_DIR"]);
11169
11443
  const path = dbPath ?? getDatabasePath();
11170
11444
  if (isInMemoryDb2(path))
11171
- return join6(tmpdir(), "hasna-todos-artifacts");
11172
- return join6(dirname5(resolve6(path)), "artifacts");
11445
+ return join6(tmpdir2(), "hasna-todos-artifacts");
11446
+ return join6(dirname5(resolve7(path)), "artifacts");
11173
11447
  }
11174
11448
  function computeContentHash(path) {
11175
- return sha256(readFileSync4(resolve6(path)));
11449
+ return sha256(readFileSync4(resolve7(path)));
11176
11450
  }
11177
11451
  function storeArtifactFile(input) {
11178
- const sourcePath = resolve6(input.sourcePath);
11452
+ const sourcePath = resolve7(input.sourcePath);
11179
11453
  if (!existsSync8(sourcePath)) {
11180
11454
  throw new Error(`Source file not found: ${input.sourcePath}`);
11181
11455
  }
@@ -11224,7 +11498,7 @@ function buildArtifactExportManifest(artifacts, dbPath) {
11224
11498
  };
11225
11499
  }
11226
11500
  function writeArtifactExportManifest(manifest, outputPath) {
11227
- const destination = resolve6(outputPath);
11501
+ const destination = resolve7(outputPath);
11228
11502
  mkdirSync4(dirname5(destination), { recursive: true });
11229
11503
  writeFileSync2(destination, `${JSON.stringify(manifest, null, 2)}
11230
11504
  `);
@@ -11611,7 +11885,11 @@ function startTaskRun(input, db) {
11611
11885
  }, d);
11612
11886
  }
11613
11887
  const run = getTaskRun(id, d);
11614
- emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
11888
+ emitLocalEventHooksQuiet({
11889
+ type: "run.started",
11890
+ payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title },
11891
+ databasePath: databasePathFromDatabase(d)
11892
+ });
11615
11893
  return run;
11616
11894
  }
11617
11895
  function beginTaskRunTransaction(input, db) {
@@ -11880,7 +12158,8 @@ function finishTaskRun(input, db) {
11880
12158
  const updated = getTaskRun(run.id, d);
11881
12159
  emitLocalEventHooksQuiet({
11882
12160
  type: `run.${input.status}`,
11883
- payload: { id: updated.id, task_id: updated.task_id, agent_id: updated.agent_id, status: updated.status, summary: updated.summary, completed_at: timestamp }
12161
+ payload: { id: updated.id, task_id: updated.task_id, agent_id: updated.agent_id, status: updated.status, summary: updated.summary, completed_at: timestamp },
12162
+ databasePath: databasePathFromDatabase(d)
11884
12163
  });
11885
12164
  return updated;
11886
12165
  }
@@ -13214,7 +13493,7 @@ function importOnboardingFixture(options = {}) {
13214
13493
  init_database();
13215
13494
  import { createHash as createHash3 } from "crypto";
13216
13495
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
13217
- import { dirname as dirname6, resolve as resolve7 } from "path";
13496
+ import { dirname as dirname6, resolve as resolve8 } from "path";
13218
13497
  import { mkdirSync as mkdirSync6 } from "fs";
13219
13498
  var TODOS_LOCAL_BACKUP_KIND = "hasna.todos.local-backup";
13220
13499
  var TODOS_LOCAL_BACKUP_SCHEMA_VERSION = 1;
@@ -13321,14 +13600,14 @@ function createLocalBackup(options = {}, db) {
13321
13600
  return backup;
13322
13601
  }
13323
13602
  function writeLocalBackupFile(backup, outputPath) {
13324
- const path = resolve7(outputPath);
13603
+ const path = resolve8(outputPath);
13325
13604
  mkdirSync6(dirname6(path), { recursive: true });
13326
13605
  writeFileSync4(path, `${JSON.stringify(backup, null, 2)}
13327
13606
  `);
13328
13607
  return path;
13329
13608
  }
13330
13609
  function readLocalBackupFile(path) {
13331
- return JSON.parse(readFileSync5(resolve7(path), "utf-8"));
13610
+ return JSON.parse(readFileSync5(resolve8(path), "utf-8"));
13332
13611
  }
13333
13612
  function verifyLocalBackup(value, options = {}, db) {
13334
13613
  const verifiedAt = options.verified_at ?? now();
@@ -16296,7 +16575,8 @@ function writeQueue(task2, queue, actor, action, db) {
16296
16575
  logTaskChange(task2.id, `review_queue.${action}`, "review_queue", null, JSON.stringify(queue), actor, d);
16297
16576
  emitLocalEventHooksQuiet({
16298
16577
  type: `review.${action}`,
16299
- payload: { task_id: task2.id, queue: queue.queue, state: queue.state, reviewer: queue.reviewer, claimed_by: queue.claimed_by }
16578
+ payload: { task_id: task2.id, queue: queue.queue, state: queue.state, reviewer: queue.reviewer, claimed_by: queue.claimed_by },
16579
+ databasePath: databasePathFromDatabase(d)
16300
16580
  });
16301
16581
  return itemFromTask(taskOrThrow2(task2.id, d), queue, d);
16302
16582
  }
@@ -16977,7 +17257,7 @@ function canonicalize(value) {
16977
17257
  function hash(value) {
16978
17258
  return createHash5("sha256").update(value).digest("hex");
16979
17259
  }
16980
- function parsePayload(value) {
17260
+ function parsePayload2(value) {
16981
17261
  if (!value)
16982
17262
  return {};
16983
17263
  try {
@@ -17076,7 +17356,7 @@ function taskScopedRows(db, scope) {
17076
17356
  FROM handoffs h
17077
17357
  WHERE h.project_id IS NULL OR h.project_id = COALESCE(?, h.project_id)
17078
17358
  `).all(scope.project_id ?? null).filter((row) => {
17079
- const payload = parsePayload(row.payload_json);
17359
+ const payload = parsePayload2(row.payload_json);
17080
17360
  const taskRefs = parseStringArray(payload["task_ids"]);
17081
17361
  const runRefs = parseStringArray(payload["run_ids"]);
17082
17362
  if (scope.project_id && row.project_id !== scope.project_id)
@@ -17107,7 +17387,7 @@ function toLedgerEntries(rows) {
17107
17387
  });
17108
17388
  let previous = LOCAL_AUDIT_LEDGER_INITIAL_HASH;
17109
17389
  return ordered.map((row, index) => {
17110
- const payload = parsePayload(row.payload_json);
17390
+ const payload = parsePayload2(row.payload_json);
17111
17391
  const payloadHash = hash(canonicalize({ source: row.source, source_id: row.source_id, created_at: row.created_at, payload }));
17112
17392
  const chainHash = hash(`${previous}
17113
17393
  ${payloadHash}`);
@@ -17242,7 +17522,7 @@ function renderLocalAuditLedgerMarkdown(ledger) {
17242
17522
  init_migrations();
17243
17523
  init_schema();
17244
17524
  import { readFileSync as readFileSync6 } from "fs";
17245
- import { join as join9, resolve as resolve8 } from "path";
17525
+ import { join as join9, resolve as resolve9 } from "path";
17246
17526
  import { Database as Database2 } from "bun:sqlite";
17247
17527
  var LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION = 1;
17248
17528
  var EXPECTED_PACKAGE_NAME = "@hasna/todos";
@@ -17384,7 +17664,7 @@ function checkChangelog() {
17384
17664
  ];
17385
17665
  }
17386
17666
  function createReleaseCompatibilityReport(options = {}) {
17387
- const root = resolve8(options.root ?? process.cwd());
17667
+ const root = resolve9(options.root ?? process.cwd());
17388
17668
  const packageJson = readPackageJson(root);
17389
17669
  const simulatedLevels = options.simulated_levels ?? defaultSimulationLevels();
17390
17670
  const checks = [
@@ -21712,6 +21992,7 @@ function rowToAgent(row) {
21712
21992
  }
21713
21993
  function registerAgent(input, db) {
21714
21994
  const d = db || getDatabase();
21995
+ const machineId = currentStorageMachineId(d);
21715
21996
  const existingNames = d.query("SELECT name FROM agents").all().map((row) => row.name);
21716
21997
  const normalizedName = validateAgentName(input.name, existingNames);
21717
21998
  const existing = getAgentByName(normalizedName, d);
@@ -21752,14 +22033,18 @@ function registerAgent(input, db) {
21752
22033
  updates.push("active_project_id = ?");
21753
22034
  params.push(input.project_id);
21754
22035
  }
22036
+ if (!existing.machine_id && machineId) {
22037
+ updates.push("machine_id = ?");
22038
+ params.push(machineId);
22039
+ }
21755
22040
  params.push(existing.id);
21756
22041
  d.run(`UPDATE agents SET ${updates.join(", ")} WHERE id = ?`, params);
21757
22042
  return getAgent(existing.id, d);
21758
22043
  }
21759
22044
  const id = shortUuid();
21760
22045
  const timestamp2 = now();
21761
- d.run(`INSERT INTO agents (id, name, description, role, title, level, permissions, capabilities, reports_to, org_id, metadata, created_at, last_seen_at, session_id, working_dir, active_project_id)
21762
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
22046
+ d.run(`INSERT INTO agents (id, name, description, role, title, level, permissions, capabilities, reports_to, org_id, metadata, created_at, last_seen_at, session_id, working_dir, active_project_id, machine_id)
22047
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
21763
22048
  id,
21764
22049
  normalizedName,
21765
22050
  input.description || null,
@@ -21775,7 +22060,8 @@ function registerAgent(input, db) {
21775
22060
  timestamp2,
21776
22061
  input.session_id || null,
21777
22062
  input.working_dir || null,
21778
- input.project_id && input.session_id ? input.project_id : null
22063
+ input.project_id && input.session_id ? input.project_id : null,
22064
+ machineId
21779
22065
  ]);
21780
22066
  return getAgent(id, d);
21781
22067
  }
@@ -21975,6 +22261,14 @@ var PROJECT_COLUMNS = [
21975
22261
  "machine_id",
21976
22262
  "synced_at"
21977
22263
  ];
22264
+ var PROJECT_MACHINE_PATH_COLUMNS = [
22265
+ "id",
22266
+ "project_id",
22267
+ "machine_id",
22268
+ "path",
22269
+ "created_at",
22270
+ "updated_at"
22271
+ ];
21978
22272
  var TASK_LIST_COLUMNS = [
21979
22273
  "id",
21980
22274
  "project_id",
@@ -22114,11 +22408,13 @@ function exportSqliteTodosStorageSnapshot(db) {
22114
22408
  source: "sqlite",
22115
22409
  tasks: listTasks({ include_archived: true }, d),
22116
22410
  projects: listProjects(d),
22411
+ projectMachinePaths: listProjectMachinePaths(d),
22117
22412
  plans: listPlans(undefined, d),
22118
22413
  agents: listAgents({ include_archived: true }, d),
22119
22414
  taskLists: listTaskLists(undefined, d),
22120
22415
  templates: listTemplates(d),
22121
- auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d)
22416
+ auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d),
22417
+ tombstones: listStorageTombstones(d)
22122
22418
  };
22123
22419
  }
22124
22420
  function importSqliteTodosStorageSnapshot(snapshot, db) {
@@ -22126,13 +22422,19 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
22126
22422
  const result = {
22127
22423
  inserted: 0,
22128
22424
  updated: 0,
22425
+ deleted: 0,
22129
22426
  skipped: 0,
22130
22427
  errors: []
22131
22428
  };
22132
- const applyRows = (table, columns, rows, updateClockColumn, afterUpsert) => {
22429
+ const applyRows = (objectType, table, columns, rows, updateClockColumn, afterUpsert) => {
22133
22430
  for (const row of rows) {
22134
22431
  try {
22135
22432
  const record = asRecord2(row);
22433
+ const tombstone = typeof record["id"] === "string" ? getStorageTombstone(objectType, record["id"], d) : null;
22434
+ if (tombstone && shouldApplyStorageTombstone(tombstone, rowClock(record, updateClockColumn))) {
22435
+ result.skipped += 1;
22436
+ continue;
22437
+ }
22136
22438
  const state = upsertById(d, table, columns, record, updateClockColumn);
22137
22439
  if (state === "inserted")
22138
22440
  result.inserted += 1;
@@ -22146,17 +22448,19 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
22146
22448
  }
22147
22449
  }
22148
22450
  };
22149
- applyRows("projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
22150
- applyRows("agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
22151
- applyRows("task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
22152
- applyRows("plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
22153
- applyRows("task_templates", TEMPLATE_COLUMNS, snapshot.templates);
22154
- applyRows("tasks", TASK_COLUMNS, sortedTasks2(snapshot.tasks), "updated_at", (row, changed) => {
22451
+ applyRows("projects", "projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
22452
+ applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
22453
+ applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
22454
+ applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
22455
+ applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
22456
+ applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
22457
+ applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks2(snapshot.tasks), "updated_at", (row, changed) => {
22155
22458
  if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
22156
22459
  replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
22157
22460
  }
22158
22461
  });
22159
- applyRows("task_history", AUDIT_COLUMNS, snapshot.auditHistory);
22462
+ applyRows("audit_history", "task_history", AUDIT_COLUMNS, snapshot.auditHistory);
22463
+ applyTombstones(d, snapshot.tombstones ?? [], result);
22160
22464
  return result;
22161
22465
  }
22162
22466
  function upsertById(db, table, columns, row, updateClockColumn) {
@@ -22211,6 +22515,85 @@ function sortedTasks2(tasks) {
22211
22515
  visit(task2);
22212
22516
  return result;
22213
22517
  }
22518
+ function applyTombstones(db, tombstones, result) {
22519
+ for (const tombstone of tombstones) {
22520
+ try {
22521
+ recordStorageTombstone({
22522
+ object_type: tombstone.object_type,
22523
+ object_id: tombstone.object_id,
22524
+ deleted_at: tombstone.deleted_at,
22525
+ source_machine_id: tombstone.source_machine_id ?? null,
22526
+ payload: tombstone.payload ?? null,
22527
+ version: tombstone.version ?? null
22528
+ }, db);
22529
+ const table = tableForTombstone(tombstone.object_type);
22530
+ const existing = existingClock(db, table, tombstone.object_id);
22531
+ if (!shouldApplyStorageTombstone(tombstone, existing)) {
22532
+ result.skipped += 1;
22533
+ continue;
22534
+ }
22535
+ const deletedTags = table === "tasks" ? db.run("DELETE FROM task_tags WHERE task_id = ?", [tombstone.object_id]).changes : 0;
22536
+ const deleted = db.run(`DELETE FROM ${table} WHERE id = ?`, [tombstone.object_id]).changes;
22537
+ if (deleted > 0 || deletedTags > 0)
22538
+ result.deleted = (result.deleted ?? 0) + 1;
22539
+ else
22540
+ result.skipped += 1;
22541
+ } catch (error) {
22542
+ result.errors.push(error instanceof Error ? error.message : String(error));
22543
+ }
22544
+ }
22545
+ }
22546
+ function tableForTombstone(objectType) {
22547
+ if (objectType === "tasks")
22548
+ return "tasks";
22549
+ if (objectType === "projects")
22550
+ return "projects";
22551
+ if (objectType === "project_machine_paths")
22552
+ return "project_machine_paths";
22553
+ if (objectType === "plans")
22554
+ return "plans";
22555
+ if (objectType === "agents")
22556
+ return "agents";
22557
+ if (objectType === "task_lists")
22558
+ return "task_lists";
22559
+ if (objectType === "templates")
22560
+ return "task_templates";
22561
+ return "task_history";
22562
+ }
22563
+ function listRows(db, table, columns) {
22564
+ return db.query(`SELECT ${columns.join(", ")} FROM ${table} ORDER BY id`).all();
22565
+ }
22566
+ function listProjectMachinePaths(db) {
22567
+ return listRows(db, "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS).map((row) => ({
22568
+ id: String(row.id),
22569
+ project_id: String(row.project_id),
22570
+ machine_id: String(row.machine_id),
22571
+ path: String(row.path),
22572
+ created_at: String(row.created_at),
22573
+ updated_at: String(row.updated_at)
22574
+ }));
22575
+ }
22576
+ function existingClock(db, table, id) {
22577
+ const clockColumns = clockColumnsForTable(table);
22578
+ const row = db.query(`SELECT ${clockColumns.join(", ")} FROM ${table} WHERE id = ?`).get(id);
22579
+ return row?.updated_at ?? row?.last_seen_at ?? row?.created_at ?? null;
22580
+ }
22581
+ function rowClock(row, updateClockColumn) {
22582
+ const value = updateClockColumn ? row[updateClockColumn] : null;
22583
+ return stringClock(value) ?? stringClock(row["updated_at"]) ?? stringClock(row["last_seen_at"]) ?? stringClock(row["created_at"]);
22584
+ }
22585
+ function stringClock(value) {
22586
+ return typeof value === "string" && value ? value : null;
22587
+ }
22588
+ function clockColumnsForTable(table) {
22589
+ if (table === "agents")
22590
+ return ["last_seen_at", "created_at"];
22591
+ if (table === "task_templates")
22592
+ return ["created_at"];
22593
+ if (table === "task_history")
22594
+ return ["created_at"];
22595
+ return ["updated_at", "created_at"];
22596
+ }
22214
22597
 
22215
22598
  // src/storage/local-sqlite.ts
22216
22599
  function createLocalSqliteTodosStorageAdapter(options = {}) {
@@ -22377,7 +22760,7 @@ class PostgresTodosSyncStore {
22377
22760
  }
22378
22761
  async pullSnapshot(options = {}) {
22379
22762
  const params = [this.service];
22380
- const filters = ["service = $1", "deleted_at IS NULL"];
22763
+ const filters = ["service = $1"];
22381
22764
  if (options.since) {
22382
22765
  params.push(options.since);
22383
22766
  filters.push(`updated_at > $${params.length}::timestamptz`);
@@ -22386,7 +22769,7 @@ class PostgresTodosSyncStore {
22386
22769
  params.push(options.objectTypes);
22387
22770
  filters.push(`object_type = ANY($${params.length}::text[])`);
22388
22771
  }
22389
- const response = await this.client.query(`SELECT object_type, payload FROM ${this.tableName}
22772
+ const response = await this.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version FROM ${this.tableName}
22390
22773
  WHERE ${filters.join(" AND ")}
22391
22774
  ORDER BY updated_at ASC, object_type ASC, object_id ASC`, params);
22392
22775
  return rowsToSnapshot(response.rows);
@@ -22410,11 +22793,20 @@ function snapshotEntries(snapshot) {
22410
22793
  return [
22411
22794
  ...snapshot.tasks.map((payload) => entry("tasks", payload, snapshot.exportedAt)),
22412
22795
  ...snapshot.projects.map((payload) => entry("projects", payload, snapshot.exportedAt)),
22796
+ ...(snapshot.projectMachinePaths ?? []).map((payload) => entry("project_machine_paths", payload, snapshot.exportedAt)),
22413
22797
  ...snapshot.plans.map((payload) => entry("plans", payload, snapshot.exportedAt)),
22414
22798
  ...snapshot.agents.map((payload) => entry("agents", payload, snapshot.exportedAt)),
22415
22799
  ...snapshot.taskLists.map((payload) => entry("task_lists", payload, snapshot.exportedAt)),
22416
22800
  ...snapshot.templates.map((payload) => entry("templates", payload, snapshot.exportedAt)),
22417
- ...snapshot.auditHistory.map((payload) => entry("audit_history", payload, snapshot.exportedAt))
22801
+ ...snapshot.auditHistory.map((payload) => entry("audit_history", payload, snapshot.exportedAt)),
22802
+ ...(snapshot.tombstones ?? []).map((tombstone) => ({
22803
+ type: tombstone.object_type,
22804
+ id: tombstone.object_id,
22805
+ payload: tombstone.payload ?? { id: tombstone.object_id, deleted_at: tombstone.deleted_at },
22806
+ updatedAt: tombstone.updated_at || tombstone.deleted_at,
22807
+ deletedAt: tombstone.deleted_at,
22808
+ version: tombstone.version ?? null
22809
+ }))
22418
22810
  ];
22419
22811
  }
22420
22812
  function entry(type, payload, fallbackUpdatedAt) {
@@ -22436,19 +22828,38 @@ function rowsToSnapshot(rows) {
22436
22828
  source: "postgres",
22437
22829
  tasks: [],
22438
22830
  projects: [],
22831
+ projectMachinePaths: [],
22439
22832
  plans: [],
22440
22833
  agents: [],
22441
22834
  taskLists: [],
22442
22835
  templates: [],
22443
- auditHistory: []
22836
+ auditHistory: [],
22837
+ tombstones: []
22444
22838
  };
22445
22839
  for (const row of rows) {
22446
22840
  const payload = payloadRecord(row.payload);
22841
+ const deletedAt = stringValue(row.deleted_at);
22842
+ if (deletedAt) {
22843
+ snapshot.tombstones ??= [];
22844
+ snapshot.tombstones.push({
22845
+ object_type: row.object_type,
22846
+ object_id: stringValue(row.object_id) ?? stringValue(payload["id"]) ?? "",
22847
+ deleted_at: deletedAt,
22848
+ updated_at: stringValue(row.updated_at) ?? deletedAt,
22849
+ source_machine_id: stringValue(row.source_machine_id),
22850
+ payload,
22851
+ version: numberValue2(row.version)
22852
+ });
22853
+ continue;
22854
+ }
22447
22855
  if (row.object_type === "tasks")
22448
22856
  snapshot.tasks.push(payload);
22449
22857
  else if (row.object_type === "projects")
22450
22858
  snapshot.projects.push(payload);
22451
- else if (row.object_type === "plans")
22859
+ else if (row.object_type === "project_machine_paths") {
22860
+ snapshot.projectMachinePaths ??= [];
22861
+ snapshot.projectMachinePaths.push(payload);
22862
+ } else if (row.object_type === "plans")
22452
22863
  snapshot.plans.push(payload);
22453
22864
  else if (row.object_type === "agents")
22454
22865
  snapshot.agents.push(payload);
@@ -22469,6 +22880,8 @@ function payloadRecord(value) {
22469
22880
  throw new Error("Postgres sync payload must be a JSON object");
22470
22881
  }
22471
22882
  function stringValue(value) {
22883
+ if (value instanceof Date)
22884
+ return value.toISOString();
22472
22885
  return typeof value === "string" && value ? value : null;
22473
22886
  }
22474
22887
  function numberValue2(value) {
@@ -22623,6 +23036,9 @@ class PostgresJsonRecordStore {
22623
23036
  this.tableName = options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE;
22624
23037
  this.cursorTableName = options.cursorTableName ?? DEFAULT_TODOS_POSTGRES_CURSOR_TABLE;
22625
23038
  }
23039
+ machineId(context) {
23040
+ return context?.requestId ?? this.sourceMachineId ?? null;
23041
+ }
22626
23042
  async ensureSchema() {
22627
23043
  this.schemaReady ??= (async () => {
22628
23044
  for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
@@ -22654,6 +23070,25 @@ class PostgresJsonRecordStore {
22654
23070
  updatedAt: stringValue2(row.updated_at) ?? new Date().toISOString()
22655
23071
  }));
22656
23072
  }
23073
+ async listTombstones() {
23074
+ await this.ensureSchema();
23075
+ const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
23076
+ FROM ${this.tableName}
23077
+ WHERE service = $1 AND deleted_at IS NOT NULL
23078
+ ORDER BY updated_at ASC, object_type ASC, object_id ASC`, [this.service]);
23079
+ return result.rows.map((row) => {
23080
+ const deletedAt = stringValue2(row.deleted_at) ?? stringValue2(row.updated_at) ?? new Date().toISOString();
23081
+ return {
23082
+ object_type: row.object_type,
23083
+ object_id: row.object_id,
23084
+ deleted_at: deletedAt,
23085
+ updated_at: stringValue2(row.updated_at) ?? deletedAt,
23086
+ source_machine_id: stringValue2(row.source_machine_id),
23087
+ payload: payloadRecord2(row.payload),
23088
+ version: numberValue3(row.version)
23089
+ };
23090
+ });
23091
+ }
22657
23092
  async upsert(type, value, context = {}) {
22658
23093
  await this.ensureSchema();
22659
23094
  const updatedAt = stringValue2(value.updated_at) ?? stringValue2(value.created_at) ?? new Date().toISOString();
@@ -22666,7 +23101,8 @@ class PostgresJsonRecordStore {
22666
23101
  updated_at = EXCLUDED.updated_at,
22667
23102
  deleted_at = NULL,
22668
23103
  source_machine_id = EXCLUDED.source_machine_id,
22669
- version = EXCLUDED.version`, [
23104
+ version = EXCLUDED.version
23105
+ WHERE ${this.tableName}.updated_at IS NULL OR ${this.tableName}.updated_at <= EXCLUDED.updated_at`, [
22670
23106
  this.service,
22671
23107
  type,
22672
23108
  value.id,
@@ -22683,13 +23119,58 @@ class PostgresJsonRecordStore {
22683
23119
  if (!existing)
22684
23120
  return false;
22685
23121
  const timestamp2 = new Date().toISOString();
22686
- await this.options.client.query(`UPDATE ${this.tableName}
22687
- SET deleted_at = $4::timestamptz,
22688
- updated_at = $4::timestamptz,
22689
- source_machine_id = $5
22690
- WHERE service = $1 AND object_type = $2 AND object_id = $3`, [this.service, type, id, timestamp2, context.requestId ?? this.sourceMachineId ?? null]);
23122
+ return this.tombstone({
23123
+ object_type: type,
23124
+ object_id: id,
23125
+ deleted_at: timestamp2,
23126
+ updated_at: timestamp2,
23127
+ payload: existing,
23128
+ version: numberValue3(existing["version"])
23129
+ }, context);
23130
+ }
23131
+ async tombstone(tombstone, context = {}) {
23132
+ await this.ensureSchema();
23133
+ const deletedAt = stringValue2(tombstone.deleted_at) ?? new Date().toISOString();
23134
+ const updatedAt = stringValue2(tombstone.updated_at) ?? deletedAt;
23135
+ const existing = await this.clock(tombstone.object_type, tombstone.object_id);
23136
+ if (existing && compareClock(existing.updatedAt, updatedAt) > 0)
23137
+ return false;
23138
+ await this.options.client.query(`INSERT INTO ${this.tableName} (
23139
+ service, object_type, object_id, payload, updated_at,
23140
+ deleted_at, source_machine_id, version
23141
+ ) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $6::timestamptz, $7, $8)
23142
+ ON CONFLICT (service, object_type, object_id) DO UPDATE SET
23143
+ payload = EXCLUDED.payload,
23144
+ updated_at = EXCLUDED.updated_at,
23145
+ deleted_at = EXCLUDED.deleted_at,
23146
+ source_machine_id = EXCLUDED.source_machine_id,
23147
+ version = EXCLUDED.version
23148
+ WHERE ${this.tableName}.updated_at IS NULL OR ${this.tableName}.updated_at <= EXCLUDED.updated_at`, [
23149
+ this.service,
23150
+ tombstone.object_type,
23151
+ tombstone.object_id,
23152
+ JSON.stringify(tombstone.payload ?? { id: tombstone.object_id, deleted_at: deletedAt }),
23153
+ updatedAt,
23154
+ deletedAt,
23155
+ tombstone.source_machine_id ?? context.requestId ?? this.sourceMachineId ?? null,
23156
+ tombstone.version ?? null
23157
+ ]);
22691
23158
  return true;
22692
23159
  }
23160
+ async clock(type, id) {
23161
+ await this.ensureSchema();
23162
+ const result = await this.options.client.query(`SELECT object_type, object_id, updated_at, deleted_at
23163
+ FROM ${this.tableName}
23164
+ WHERE service = $1 AND object_type = $2 AND object_id = $3
23165
+ LIMIT 1`, [this.service, type, id]);
23166
+ const row = result.rows[0];
23167
+ if (!row)
23168
+ return null;
23169
+ return {
23170
+ updatedAt: stringValue2(row.updated_at) ?? new Date().toISOString(),
23171
+ deletedAt: stringValue2(row.deleted_at)
23172
+ };
23173
+ }
22693
23174
  async getCursor(name) {
22694
23175
  await this.ensureSchema();
22695
23176
  const result = await this.options.client.query(`SELECT value FROM ${this.cursorTableName} WHERE service = $1 AND cursor_name = $2`, [this.service, name]);
@@ -22758,7 +23239,10 @@ async function createTask2(input, store, context) {
22758
23239
  runner_started_at: null,
22759
23240
  runner_completed_at: null,
22760
23241
  current_step: null,
22761
- total_steps: null
23242
+ total_steps: null,
23243
+ machine_id: store.machineId(context),
23244
+ synced_at: null,
23245
+ archived_at: null
22762
23246
  };
22763
23247
  await store.upsert("tasks", task2, context);
22764
23248
  await logTaskChange2(task2.id, "created", "status", null, task2.status, task2.assigned_by ?? task2.agent_id, store, context);
@@ -22883,7 +23367,9 @@ async function createProject2(input, store, context) {
22883
23367
  task_prefix: input.task_prefix ?? await generateProjectPrefix(input.name, store),
22884
23368
  task_counter: 0,
22885
23369
  created_at: timestamp2,
22886
- updated_at: timestamp2
23370
+ updated_at: timestamp2,
23371
+ machine_id: store.machineId(context),
23372
+ synced_at: null
22887
23373
  };
22888
23374
  return store.upsert("projects", project, context);
22889
23375
  }
@@ -22903,7 +23389,9 @@ async function createPlan2(input, store, context) {
22903
23389
  description: input.description ?? null,
22904
23390
  status: input.status ?? "active",
22905
23391
  created_at: timestamp2,
22906
- updated_at: timestamp2
23392
+ updated_at: timestamp2,
23393
+ machine_id: store.machineId(context),
23394
+ synced_at: null
22907
23395
  }, context);
22908
23396
  }
22909
23397
  async function updatePlan2(id, input, store) {
@@ -22933,7 +23421,9 @@ async function registerAgent2(input, store, context) {
22933
23421
  last_seen_at: timestamp2,
22934
23422
  session_id: input.session_id ?? context?.sessionId ?? existing?.session_id ?? null,
22935
23423
  working_dir: input.working_dir ?? existing?.working_dir ?? null,
22936
- active_project_id: input.project_id ?? context?.projectId ?? existing?.active_project_id ?? null
23424
+ active_project_id: input.project_id ?? context?.projectId ?? existing?.active_project_id ?? null,
23425
+ machine_id: existing?.machine_id ?? store.machineId(context),
23426
+ synced_at: existing?.synced_at ?? null
22937
23427
  };
22938
23428
  return store.upsert("agents", agent, context);
22939
23429
  }
@@ -22960,7 +23450,9 @@ async function createTaskList2(input, store, context) {
22960
23450
  description: input.description ?? null,
22961
23451
  metadata: input.metadata ?? {},
22962
23452
  created_at: timestamp2,
22963
- updated_at: timestamp2
23453
+ updated_at: timestamp2,
23454
+ machine_id: store.machineId(context),
23455
+ synced_at: null
22964
23456
  }, context);
22965
23457
  }
22966
23458
  async function updateTaskList2(id, input, store) {
@@ -22986,7 +23478,9 @@ async function createTemplate2(input, store, context) {
22986
23478
  project_id: input.project_id ?? context?.projectId ?? null,
22987
23479
  plan_id: input.plan_id ?? null,
22988
23480
  metadata: input.metadata ?? {},
22989
- created_at: timestamp2
23481
+ created_at: timestamp2,
23482
+ machine_id: store.machineId(context),
23483
+ synced_at: null
22990
23484
  }, context);
22991
23485
  }
22992
23486
  async function updateTemplate2(id, input, store) {
@@ -23011,7 +23505,8 @@ async function logTaskChange2(taskId, action, field2, oldValue, newValue, agentI
23011
23505
  old_value: oldValue ?? null,
23012
23506
  new_value: newValue ?? null,
23013
23507
  agent_id: agentId ?? context?.agentId ?? null,
23014
- created_at: new Date().toISOString()
23508
+ created_at: new Date().toISOString(),
23509
+ machine_id: store.machineId(context)
23015
23510
  };
23016
23511
  return store.upsert("audit_history", entry2, context);
23017
23512
  }
@@ -23034,18 +23529,21 @@ async function exportSnapshot(store) {
23034
23529
  source: "postgres",
23035
23530
  tasks: await store.list("tasks"),
23036
23531
  projects: await store.list("projects"),
23532
+ projectMachinePaths: await store.list("project_machine_paths"),
23037
23533
  plans: await store.list("plans"),
23038
23534
  agents: await store.list("agents"),
23039
23535
  taskLists: await store.list("task_lists"),
23040
23536
  templates: await store.list("templates"),
23041
- auditHistory: await store.list("audit_history")
23537
+ auditHistory: await store.list("audit_history"),
23538
+ tombstones: await store.listTombstones()
23042
23539
  };
23043
23540
  }
23044
23541
  async function importSnapshot(snapshot, store, context) {
23045
- const result = { inserted: 0, updated: 0, skipped: 0, errors: [] };
23542
+ const result = { inserted: 0, updated: 0, deleted: 0, skipped: 0, errors: [] };
23046
23543
  const entries = [
23047
23544
  ...snapshot.tasks.map((row) => ["tasks", row]),
23048
23545
  ...snapshot.projects.map((row) => ["projects", row]),
23546
+ ...(snapshot.projectMachinePaths ?? []).map((row) => ["project_machine_paths", row]),
23049
23547
  ...snapshot.plans.map((row) => ["plans", row]),
23050
23548
  ...snapshot.agents.map((row) => ["agents", row]),
23051
23549
  ...snapshot.taskLists.map((row) => ["task_lists", row]),
@@ -23064,6 +23562,25 @@ async function importSnapshot(snapshot, store, context) {
23064
23562
  result.errors.push(error instanceof Error ? error.message : String(error));
23065
23563
  }
23066
23564
  }
23565
+ for (const tombstone of snapshot.tombstones ?? []) {
23566
+ try {
23567
+ const deleted = await store.tombstone({
23568
+ object_type: tombstone.object_type,
23569
+ object_id: tombstone.object_id,
23570
+ deleted_at: tombstone.deleted_at,
23571
+ updated_at: tombstone.updated_at,
23572
+ source_machine_id: tombstone.source_machine_id ?? null,
23573
+ payload: tombstone.payload ?? null,
23574
+ version: tombstone.version ?? null
23575
+ }, context);
23576
+ if (deleted)
23577
+ result.deleted = (result.deleted ?? 0) + 1;
23578
+ else
23579
+ result.skipped += 1;
23580
+ } catch (error) {
23581
+ result.errors.push(error instanceof Error ? error.message : String(error));
23582
+ }
23583
+ }
23067
23584
  return result;
23068
23585
  }
23069
23586
  async function requireRecord(type, id, store) {
@@ -23146,8 +23663,17 @@ function payloadRecord2(value) {
23146
23663
  throw new Error("Postgres storage payload must be a JSON object");
23147
23664
  }
23148
23665
  function stringValue2(value) {
23666
+ if (value instanceof Date)
23667
+ return value.toISOString();
23149
23668
  return typeof value === "string" && value ? value : null;
23150
23669
  }
23670
+ function compareClock(left, right) {
23671
+ const leftClock = Date.parse(left);
23672
+ const rightClock = Date.parse(right);
23673
+ if (Number.isNaN(leftClock) || Number.isNaN(rightClock))
23674
+ return left.localeCompare(right);
23675
+ return leftClock - rightClock;
23676
+ }
23151
23677
  function numberValue3(value) {
23152
23678
  return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
23153
23679
  }
@@ -24082,7 +24608,7 @@ class TodosClient {
24082
24608
  return this._fetchWithRetry(path, { method: "DELETE" });
24083
24609
  }
24084
24610
  _sleep(ms) {
24085
- return new Promise((resolve9) => setTimeout(resolve9, ms));
24611
+ return new Promise((resolve10) => setTimeout(resolve10, ms));
24086
24612
  }
24087
24613
  async getHealth() {
24088
24614
  return this._get("/api/health");
@@ -25464,7 +25990,7 @@ function renderRetrospectiveMarkdown(record) {
25464
25990
  // src/lib/project-bootstrap.ts
25465
25991
  init_database();
25466
25992
  import { existsSync as existsSync9, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
25467
- import { basename as basename2, dirname as dirname7, resolve as resolve9 } from "path";
25993
+ import { basename as basename2, dirname as dirname7, resolve as resolve10 } from "path";
25468
25994
  function safeStat(path) {
25469
25995
  try {
25470
25996
  return statSync3(path);
@@ -25473,7 +25999,7 @@ function safeStat(path) {
25473
25999
  }
25474
26000
  }
25475
26001
  function canonicalPath(input) {
25476
- const resolved = resolve9(input);
26002
+ const resolved = resolve10(input);
25477
26003
  const stats2 = safeStat(resolved);
25478
26004
  if (stats2?.isFile())
25479
26005
  return dirname7(resolved);
@@ -25482,7 +26008,7 @@ function canonicalPath(input) {
25482
26008
  function findUp(start, marker) {
25483
26009
  let current = canonicalPath(start);
25484
26010
  while (true) {
25485
- if (existsSync9(resolve9(current, marker)))
26011
+ if (existsSync9(resolve10(current, marker)))
25486
26012
  return current;
25487
26013
  const parent = dirname7(current);
25488
26014
  if (parent === current)
@@ -25493,7 +26019,7 @@ function findUp(start, marker) {
25493
26019
  function readPackageJson2(path) {
25494
26020
  if (!path)
25495
26021
  return null;
25496
- const file = resolve9(path, "package.json");
26022
+ const file = resolve10(path, "package.json");
25497
26023
  if (!existsSync9(file))
25498
26024
  return null;
25499
26025
  try {
@@ -25516,7 +26042,7 @@ function workspaceMarker(root, rootPackage) {
25516
26042
  if (rootPackage?.workspaces)
25517
26043
  markers.push("package.json#workspaces");
25518
26044
  for (const marker of ["pnpm-workspace.yaml", "turbo.json", "nx.json", "lerna.json", "rush.json", "bun.lock", "bun.lockb"]) {
25519
- if (existsSync9(resolve9(root, marker)))
26045
+ if (existsSync9(resolve10(root, marker)))
25520
26046
  markers.push(marker);
25521
26047
  }
25522
26048
  const kind = markers.find((marker) => marker !== "bun.lock" && marker !== "bun.lockb") ?? null;
@@ -25578,7 +26104,19 @@ function bootstrapProject(options = {}, db) {
25578
26104
  }
25579
26105
  setMachineLocalPath(project.id, discovery.projectPath, d);
25580
26106
  const beforeTaskList = d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ?").get(project.id, taskListSlug);
25581
- const taskList = ensureTaskList(`${project.name} Tasks`, taskListSlug, project.id, d);
26107
+ let taskList = ensureTaskList(`${project.name} Tasks`, taskListSlug, project.id, d);
26108
+ if (options.routeEnabled && taskList.metadata.route_enabled !== true) {
26109
+ taskList = updateTaskList(taskList.id, {
26110
+ metadata: {
26111
+ ...taskList.metadata,
26112
+ route_enabled: true,
26113
+ automation: {
26114
+ ...taskList.metadata.automation && typeof taskList.metadata.automation === "object" && !Array.isArray(taskList.metadata.automation) ? taskList.metadata.automation : {},
26115
+ no_auto: false
26116
+ }
26117
+ }
26118
+ }, d);
26119
+ }
25582
26120
  const createdSources = [];
25583
26121
  for (const source9 of [
25584
26122
  addSourceOnce(project.id, "local", "Project root", discovery.projectPath, { role: "project-root" }, d),
@@ -25622,6 +26160,174 @@ function getProjectByPathForBootstrap(path, db) {
25622
26160
  WHERE pmp.path = ?`).get(path);
25623
26161
  return machineRow ?? null;
25624
26162
  }
26163
+ // src/lib/task-routing.ts
26164
+ init_database();
26165
+ function classifyProjectKind2(path) {
26166
+ if (!path)
26167
+ return null;
26168
+ return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
26169
+ }
26170
+ function machineLocalPath(project, db) {
26171
+ const machineId = process.env["TODOS_MACHINE_ID"];
26172
+ if (!machineId)
26173
+ return null;
26174
+ try {
26175
+ const row = db.query("SELECT path FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(project.id, machineId);
26176
+ return row?.path ?? null;
26177
+ } catch {
26178
+ return null;
26179
+ }
26180
+ }
26181
+ function resolveProject(task2, db) {
26182
+ const project = task2.project_id ? getProject(task2.project_id, db) : null;
26183
+ const projectPath = project ? machineLocalPath(project, db) ?? project.path : task2.working_dir;
26184
+ return { project, projectPath: projectPath ?? null };
26185
+ }
26186
+ function resolveTaskList(task2, project, db) {
26187
+ if (task2.task_list_id) {
26188
+ return getTaskList(task2.task_list_id, db) ?? (project ? getTaskListBySlug(task2.task_list_id, project.id, db) : null);
26189
+ }
26190
+ if (project?.task_list_id) {
26191
+ return getTaskListBySlug(project.task_list_id, project.id, db);
26192
+ }
26193
+ return null;
26194
+ }
26195
+ function isTerminal2(status) {
26196
+ return status === "completed" || status === "cancelled" || status === "failed";
26197
+ }
26198
+ function routeConcurrencyKey(task2, project, taskList, projectPath) {
26199
+ if (project?.id)
26200
+ return `project:${project.id}`;
26201
+ if (taskList?.id)
26202
+ return `task-list:${taskList.id}`;
26203
+ if (projectPath)
26204
+ return `path:${projectPath}`;
26205
+ return `task:${task2.id}`;
26206
+ }
26207
+ function getTaskRouteState(taskOrId, db) {
26208
+ const d = db || getDatabase();
26209
+ const task2 = typeof taskOrId === "string" ? getTask(taskOrId, d) : taskOrId;
26210
+ if (!task2)
26211
+ throw new Error(`Task not found: ${taskOrId}`);
26212
+ const { project, projectPath } = resolveProject(task2, d);
26213
+ const taskList = resolveTaskList(task2, project, d);
26214
+ const automation = routingAutomationMetadata(task2, taskList) ?? {};
26215
+ const routeEnabled = routeEnabledForTask(task2, taskList) === true;
26216
+ const tagOptIn = task2.tags.includes("auto:route") || task2.tags.includes("route:enabled");
26217
+ const locked = Boolean(task2.locked_by && !isLockExpired(task2.locked_at));
26218
+ const blockers = getBlockingDeps(task2.id, d);
26219
+ const blocked = blockers.length > 0;
26220
+ const terminal = isTerminal2(task2.status);
26221
+ const requiresApproval = automation.requires_approval === true || task2.requires_approval === true;
26222
+ const approvalRequired = automation.approval_required === true;
26223
+ const approved = Boolean(task2.approved_by);
26224
+ const gates = {
26225
+ route_enabled: routeEnabled,
26226
+ tag_opt_in: tagOptIn,
26227
+ no_auto: automation.no_auto === true,
26228
+ manual: automation.manual === true,
26229
+ manual_required: automation.manual_required === true,
26230
+ requires_approval: requiresApproval,
26231
+ approval_required: approvalRequired,
26232
+ approved,
26233
+ locked,
26234
+ blocked,
26235
+ terminal
26236
+ };
26237
+ const reasons = [];
26238
+ if (task2.status !== "pending")
26239
+ reasons.push("task_not_pending");
26240
+ if (terminal)
26241
+ reasons.push("task_terminal");
26242
+ if (!routeEnabled)
26243
+ reasons.push("route_not_enabled");
26244
+ if (locked)
26245
+ reasons.push("task_locked");
26246
+ if (blocked)
26247
+ reasons.push("task_blocked");
26248
+ if (gates.no_auto)
26249
+ reasons.push("no_auto");
26250
+ if (gates.manual)
26251
+ reasons.push("manual");
26252
+ if (gates.manual_required)
26253
+ reasons.push("manual_required");
26254
+ if (requiresApproval && !approved)
26255
+ reasons.push("requires_approval");
26256
+ if (approvalRequired && !approved)
26257
+ reasons.push("approval_required");
26258
+ if (automation.allowed === false)
26259
+ reasons.push("automation_disallowed");
26260
+ return {
26261
+ schema_version: TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION,
26262
+ task_id: task2.id,
26263
+ task_short_id: task2.short_id,
26264
+ status: task2.status,
26265
+ eligible: reasons.length === 0,
26266
+ reasons,
26267
+ blockers: blockers.map((blocker) => ({
26268
+ id: blocker.id,
26269
+ short_id: blocker.short_id,
26270
+ title: blocker.title,
26271
+ status: blocker.status
26272
+ })),
26273
+ gates,
26274
+ automation: Object.keys(automation).length > 0 ? automation : null,
26275
+ route: {
26276
+ project_id: project?.id ?? task2.project_id,
26277
+ project_path: projectPath,
26278
+ working_dir: task2.working_dir ?? projectPath,
26279
+ project_kind: classifyProjectKind2(projectPath),
26280
+ task_list_id: taskList?.id ?? task2.task_list_id,
26281
+ task_list_slug: taskList?.slug ?? null,
26282
+ task_list_name: taskList?.name ?? null,
26283
+ concurrency_key: routeConcurrencyKey(task2, project, taskList, projectPath)
26284
+ },
26285
+ pointers: workflowPointersFromMetadata(task2.metadata)
26286
+ };
26287
+ }
26288
+ function setTaskWorkflowPointers(taskId, input, db) {
26289
+ const d = db || getDatabase();
26290
+ const task2 = getTask(taskId, d);
26291
+ if (!task2)
26292
+ throw new Error(`Task not found: ${taskId}`);
26293
+ const previous = workflowPointersFromMetadata(task2.metadata);
26294
+ const next = compactWorkflowPointers({
26295
+ current_workflow_invocation_id: pointerPatch(previous.current_workflow_invocation_id, input, "current_workflow_invocation_id"),
26296
+ current_run_id: pointerPatch(previous.current_run_id, input, "current_run_id"),
26297
+ latest_manifest_path: pointerPatch(previous.latest_manifest_path, input, "latest_manifest_path"),
26298
+ latest_evaluation_path: pointerPatch(previous.latest_evaluation_path, input, "latest_evaluation_path"),
26299
+ workflow_state: pointerPatch(previous.workflow_state, input, "workflow_state")
26300
+ });
26301
+ const timestamp2 = now();
26302
+ const {
26303
+ current_workflow_invocation_id,
26304
+ current_run_id,
26305
+ latest_manifest_path,
26306
+ latest_evaluation_path,
26307
+ workflow_state,
26308
+ workflow_invocation,
26309
+ ...baseMetadata
26310
+ } = task2.metadata;
26311
+ const metadata = {
26312
+ ...baseMetadata,
26313
+ ...next,
26314
+ workflow_invocation: {
26315
+ schema_version: TASK_WORKFLOW_POINTER_SCHEMA_VERSION,
26316
+ ...next,
26317
+ updated_at: timestamp2,
26318
+ updated_by: input.actor ?? null
26319
+ }
26320
+ };
26321
+ return updateTask(task2.id, { version: task2.version, metadata }, d);
26322
+ }
26323
+ function pointerPatch(previous, input, key) {
26324
+ if (!Object.prototype.hasOwnProperty.call(input, key))
26325
+ return previous;
26326
+ const value = input[key];
26327
+ if (value === undefined)
26328
+ return previous;
26329
+ return typeof value === "string" && value.trim() ? value : undefined;
26330
+ }
25625
26331
  // src/db/api-keys.ts
25626
26332
  init_database();
25627
26333
  import { createHash as createHash10, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual3 } from "crypto";
@@ -26687,7 +27393,7 @@ function classifyLog(text) {
26687
27393
  async function sleep2(ms) {
26688
27394
  if (ms <= 0)
26689
27395
  return;
26690
- await new Promise((resolve10) => setTimeout(resolve10, ms));
27396
+ await new Promise((resolve11) => setTimeout(resolve11, ms));
26691
27397
  }
26692
27398
  async function runCommandProvider(provider, input) {
26693
27399
  const commandTemplate = input.command || provider.command;
@@ -27043,11 +27749,11 @@ function writeVerificationExport(bundle, path) {
27043
27749
  writeFileSync7(path, JSON.stringify(bundle, null, 2), "utf8");
27044
27750
  }
27045
27751
  // src/lib/policy-packs.ts
27046
- import { relative as relative3, resolve as resolve10 } from "path";
27752
+ import { relative as relative3, resolve as resolve11 } from "path";
27047
27753
  init_database();
27048
27754
  init_config();
27049
27755
  function normalizePath3(path) {
27050
- return resolve10(path);
27756
+ return resolve11(path);
27051
27757
  }
27052
27758
  function unique4(values) {
27053
27759
  return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
@@ -27102,7 +27808,7 @@ function commandMatches(commands, pattern) {
27102
27808
  }
27103
27809
  function pathMatches(paths, pattern, root) {
27104
27810
  return paths.filter((path) => {
27105
- const candidate = path.startsWith("/") ? path : resolve10(root, path);
27811
+ const candidate = path.startsWith("/") ? path : resolve11(root, path);
27106
27812
  if (!isPathInside3(root, candidate))
27107
27813
  return matchesPattern3(path, pattern);
27108
27814
  return matchesPattern3(path, pattern) || matchesPattern3(relative3(root, candidate), pattern);
@@ -27814,7 +28520,7 @@ function resetAgentAdapterCache() {}
27814
28520
  // src/lib/git-traceability.ts
27815
28521
  import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
27816
28522
  import { spawnSync as spawnSync2 } from "child_process";
27817
- import { resolve as resolve11 } from "path";
28523
+ import { resolve as resolve12 } from "path";
27818
28524
  var GIT_TRACEABILITY_SCHEMA_VERSION = "todos.git_traceability.v1";
27819
28525
  function runGit(args, cwd) {
27820
28526
  const result = spawnSync2("git", args, { cwd, encoding: "utf8" });
@@ -27857,7 +28563,7 @@ function inspectGitCommit(sha, cwd) {
27857
28563
  };
27858
28564
  }
27859
28565
  function loadCiSnapshot(path) {
27860
- const target = path ? resolve11(path) : resolve11(process.cwd(), ".todos", "ci-snapshot.json");
28566
+ const target = path ? resolve12(path) : resolve12(process.cwd(), ".todos", "ci-snapshot.json");
27861
28567
  if (!existsSync13(target))
27862
28568
  return null;
27863
28569
  try {
@@ -27955,7 +28661,7 @@ function formatTraceabilityReport(report) {
27955
28661
  }
27956
28662
  // src/lib/mention-resolver.ts
27957
28663
  import { existsSync as existsSync14, readdirSync as readdirSync2, readFileSync as readFileSync12, statSync as statSync4 } from "fs";
27958
- import { basename as basename3, isAbsolute, join as join12, relative as relative4, resolve as resolve12, sep } from "path";
28664
+ import { basename as basename3, isAbsolute, join as join12, relative as relative4, resolve as resolve13, sep as sep2 } from "path";
27959
28665
  init_database();
27960
28666
  var PREFIXES = {
27961
28667
  file: "file",
@@ -28031,11 +28737,11 @@ function backlink(kind, key, label, target = key) {
28031
28737
  return { kind, key, label, target };
28032
28738
  }
28033
28739
  function normalizeWorkspace(workspace) {
28034
- return resolve12(workspace || process.cwd());
28740
+ return resolve13(workspace || process.cwd());
28035
28741
  }
28036
28742
  function isInside(root, absolutePath) {
28037
28743
  const rel = relative4(root, absolutePath);
28038
- return rel === "" || !rel.startsWith("..") && !rel.includes(`..${sep}`) && !isAbsolute(rel);
28744
+ return rel === "" || !rel.startsWith("..") && !rel.includes(`..${sep2}`) && !isAbsolute(rel);
28039
28745
  }
28040
28746
  function normalizeRelativePath(value) {
28041
28747
  const normalized = value.trim().replace(/^\.?\//, "").replace(/\\/g, "/");
@@ -28099,7 +28805,7 @@ function resolveFile(parsed, workspace) {
28099
28805
  resolution.warnings.push("path is empty or escapes the workspace");
28100
28806
  return resolution;
28101
28807
  }
28102
- const absolutePath = resolve12(workspace, relPath);
28808
+ const absolutePath = resolve13(workspace, relPath);
28103
28809
  if (!isInside(workspace, absolutePath)) {
28104
28810
  resolution.path = relPath;
28105
28811
  resolution.warnings.push("path escapes the workspace");
@@ -29035,7 +29741,8 @@ function logApprovalEvent(taskId, action, gate, agentId, db) {
29035
29741
  if (action === "approved" || action === "rejected" || action === "expired") {
29036
29742
  emitLocalEventHooksQuiet({
29037
29743
  type: "approval.decided",
29038
- payload: { task_id: taskId, gate: gate.gate, status: gate.status, reviewer: gate.reviewer, run_id: gate.run_id }
29744
+ payload: { task_id: taskId, gate: gate.gate, status: gate.status, reviewer: gate.reviewer, run_id: gate.run_id },
29745
+ databasePath: databasePathFromDatabase(db)
29039
29746
  });
29040
29747
  }
29041
29748
  }
@@ -31685,14 +32392,14 @@ function renderReleaseNotesMarkdown(document) {
31685
32392
  init_database();
31686
32393
  init_migrations();
31687
32394
  import { existsSync as existsSync19, copyFileSync, mkdirSync as mkdirSync12, readFileSync as readFileSync16, statSync as statSync6, writeFileSync as writeFileSync10, unlinkSync } from "fs";
31688
- import { dirname as dirname11, join as join15, resolve as resolve13 } from "path";
32395
+ import { dirname as dirname11, join as join15, resolve as resolve14 } from "path";
31689
32396
  import { Database as Database3 } from "bun:sqlite";
31690
32397
  var DB_BACKUP_SCHEMA = "todos.db_backup.v1";
31691
32398
  function resolveDbPath(dbPath) {
31692
32399
  if (dbPath)
31693
- return resolve13(dbPath);
32400
+ return resolve14(dbPath);
31694
32401
  if (process.env["TODOS_DB_PATH"] && process.env["TODOS_DB_PATH"] !== ":memory:") {
31695
- return resolve13(process.env["TODOS_DB_PATH"]);
32402
+ return resolve14(process.env["TODOS_DB_PATH"]);
31696
32403
  }
31697
32404
  const db = getDatabase();
31698
32405
  const filename = db.filename;
@@ -31731,7 +32438,7 @@ function restoreDatabase(backupPath, targetPath) {
31731
32438
  if (!integrity.ok) {
31732
32439
  throw new Error(`Backup failed integrity check: ${integrity.errors.join("; ")}`);
31733
32440
  }
31734
- const target = targetPath ? resolve13(targetPath) : resolveDbPath();
32441
+ const target = targetPath ? resolve14(targetPath) : resolveDbPath();
31735
32442
  mkdirSync12(dirname11(target), { recursive: true });
31736
32443
  const staging = `${target}.restore.tmp`;
31737
32444
  copyFileSync(backupPath, staging);
@@ -31750,7 +32457,7 @@ function restoreDatabase(backupPath, targetPath) {
31750
32457
  };
31751
32458
  }
31752
32459
  function checkDatabaseIntegrity(dbPath) {
31753
- const path = dbPath ? resolve13(dbPath) : resolveDbPath();
32460
+ const path = dbPath ? resolve14(dbPath) : resolveDbPath();
31754
32461
  const errors = [];
31755
32462
  if (!existsSync19(path)) {
31756
32463
  return {
@@ -31815,7 +32522,7 @@ function checkDatabaseIntegrity(dbPath) {
31815
32522
  };
31816
32523
  }
31817
32524
  function compactDatabase(dbPath) {
31818
- const path = dbPath ? resolve13(dbPath) : resolveDbPath();
32525
+ const path = dbPath ? resolve14(dbPath) : resolveDbPath();
31819
32526
  const before = statSync6(path).size;
31820
32527
  const db = new Database3(path);
31821
32528
  db.exec("VACUUM");
@@ -31825,7 +32532,7 @@ function compactDatabase(dbPath) {
31825
32532
  return { path, bytes_before: before, bytes_after: after };
31826
32533
  }
31827
32534
  function migrationDryRun(dbPath) {
31828
- const path = dbPath ? resolve13(dbPath) : resolveDbPath();
32535
+ const path = dbPath ? resolve14(dbPath) : resolveDbPath();
31829
32536
  const db = new Database3(path, { readonly: true });
31830
32537
  let current = 0;
31831
32538
  try {
@@ -31849,7 +32556,7 @@ function migrationDryRun(dbPath) {
31849
32556
  };
31850
32557
  }
31851
32558
  function defaultBackupPath(dbPath) {
31852
- const base = dbPath ? dirname11(resolve13(dbPath)) : dirname11(resolveDbPath());
32559
+ const base = dbPath ? dirname11(resolve14(dbPath)) : dirname11(resolveDbPath());
31853
32560
  const stamp = new Date().toISOString().replace(/[:.]/g, "-");
31854
32561
  return join15(base, "backups", `todos-${stamp}.db`);
31855
32562
  }
@@ -35513,8 +36220,8 @@ todos machines topology # full diagnostic report
35513
36220
  import { createHash as createHash12 } from "crypto";
35514
36221
  import { existsSync as existsSync20, readFileSync as readFileSync19, statSync as statSync7 } from "fs";
35515
36222
  import { hostname as hostname3, platform, arch } from "os";
35516
- import { dirname as dirname15, join as join17, resolve as resolve14 } from "path";
35517
- import { tmpdir as tmpdir2 } from "os";
36223
+ import { dirname as dirname15, join as join17, resolve as resolve15 } from "path";
36224
+ import { tmpdir as tmpdir3 } from "os";
35518
36225
  init_database();
35519
36226
  init_redaction();
35520
36227
  init_sync_utils();
@@ -35646,15 +36353,15 @@ function commandEnv(env, includeValues) {
35646
36353
  function defaultSnapshotDir() {
35647
36354
  const dbPath = getDatabasePath();
35648
36355
  if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
35649
- return join17(tmpdir2(), "hasna-todos", "environment-snapshots");
35650
- return join17(dirname15(resolve14(dbPath)), "environment-snapshots");
36356
+ return join17(tmpdir3(), "hasna-todos", "environment-snapshots");
36357
+ return join17(dirname15(resolve15(dbPath)), "environment-snapshots");
35651
36358
  }
35652
36359
  function snapshotWithId(snapshot) {
35653
36360
  const digest = sha2565(JSON.stringify(snapshot)).slice(0, 24);
35654
36361
  return { id: `env_${digest}`, ...snapshot };
35655
36362
  }
35656
36363
  function captureEnvironmentSnapshot(input = {}) {
35657
- const root = resolve14(input.root || process.cwd());
36364
+ const root = resolve15(input.root || process.cwd());
35658
36365
  const env = input.env || process.env;
35659
36366
  const warnings = [];
35660
36367
  const manifests = MANIFEST_FILES.map((file) => manifestRecord(root, file)).filter((file) => Boolean(file));
@@ -35694,13 +36401,13 @@ function captureEnvironmentSnapshot(input = {}) {
35694
36401
  });
35695
36402
  }
35696
36403
  function writeEnvironmentSnapshot(snapshot, outputPath) {
35697
- const path = outputPath ? resolve14(outputPath) : join17(defaultSnapshotDir(), `${snapshot.id}.json`);
36404
+ const path = outputPath ? resolve15(outputPath) : join17(defaultSnapshotDir(), `${snapshot.id}.json`);
35698
36405
  ensureDir2(dirname15(path));
35699
36406
  writeJsonFile(path, snapshot);
35700
36407
  return path;
35701
36408
  }
35702
36409
  function readEnvironmentSnapshot(path) {
35703
- const snapshot = readJsonFile(resolve14(path));
36410
+ const snapshot = readJsonFile(resolve15(path));
35704
36411
  if (!snapshot || snapshot.schema_version !== 1 || typeof snapshot.id !== "string") {
35705
36412
  throw new Error(`Invalid environment snapshot: ${path}`);
35706
36413
  }
@@ -37439,7 +38146,7 @@ function listLinkedTemplates(db, cwd) {
37439
38146
  init_database();
37440
38147
  import { mkdtempSync } from "fs";
37441
38148
  import { join as join21 } from "path";
37442
- import { tmpdir as tmpdir3 } from "os";
38149
+ import { tmpdir as tmpdir4 } from "os";
37443
38150
  var AGENT_WORKFLOW_DEMO_SCHEMA = "todos.agent_workflow_demo.v1";
37444
38151
  var DEMO_DEFAULT_AGENT = "demoagent";
37445
38152
  var DEMO_DEFAULT_PROJECT = "Agent Workflow Demo";
@@ -37454,7 +38161,7 @@ function setupEphemeralDemoDb(options = {}) {
37454
38161
  if (options.db_path) {
37455
38162
  db_path = options.db_path;
37456
38163
  } else if (options.persist) {
37457
- db_path = join21(mkdtempSync(join21(tmpdir3(), "todos-demo-")), "todos.db");
38164
+ db_path = join21(mkdtempSync(join21(tmpdir4(), "todos-demo-")), "todos.db");
37458
38165
  } else {
37459
38166
  db_path = ":memory:";
37460
38167
  }
@@ -38076,11 +38783,6 @@ var ALL_MCP_TOOLS = [
38076
38783
  "template_history",
38077
38784
  "todos_inbox",
38078
38785
  "todos_retro",
38079
- "todos_storage_conflicts",
38080
- "todos_storage_feedback",
38081
- "todos_storage_pull",
38082
- "todos_storage_push",
38083
- "todos_storage_status",
38084
38786
  "trust_workspace",
38085
38787
  "unarchive_agent",
38086
38788
  "unarchive_task",
@@ -38217,12 +38919,6 @@ var MCP_GROUP_DEFS = [
38217
38919
  description: "Sandbox, trust, secrets, crypto, and verification.",
38218
38920
  match: (t) => /sandbox|trust|secret|crypto|verification|policy_pack|redact|scan_/.test(t)
38219
38921
  },
38220
- {
38221
- id: "storage",
38222
- name: "Storage bridge",
38223
- description: "Optional storage sync tools (admin profile).",
38224
- match: (t) => t.startsWith("todos_storage_") || t === "sync_all" || t === "migrate_pg"
38225
- },
38226
38922
  {
38227
38923
  id: "workflow",
38228
38924
  name: "Workflow utilities",
@@ -40313,7 +41009,7 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
40313
41009
  // src/lib/extract.ts
40314
41010
  import { existsSync as existsSync25, readFileSync as readFileSync23, statSync as statSync8 } from "fs";
40315
41011
  import { createHash as createHash14 } from "crypto";
40316
- import { relative as relative6, resolve as resolve15, join as join24 } from "path";
41012
+ import { relative as relative6, resolve as resolve16, join as join24 } from "path";
40317
41013
  var EXTRACT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"];
40318
41014
  var DEFAULT_EXTENSIONS = new Set([
40319
41015
  ".ts",
@@ -40383,7 +41079,7 @@ function normalizePathForMatch(value) {
40383
41079
  return value.replace(/\\/g, "/").replace(/^\.\//, "");
40384
41080
  }
40385
41081
  function readGitignorePatterns(basePath) {
40386
- const root = statSync8(basePath).isFile() ? resolve15(basePath, "..") : basePath;
41082
+ const root = statSync8(basePath).isFile() ? resolve16(basePath, "..") : basePath;
40387
41083
  const gitignorePath = join24(root, ".gitignore");
40388
41084
  if (!existsSync25(gitignorePath))
40389
41085
  return [];
@@ -40519,7 +41215,7 @@ function collectFiles(basePath, extensions, excludes, respectGitignore) {
40519
41215
  return files.sort();
40520
41216
  }
40521
41217
  function buildCodebaseIndex(options) {
40522
- const basePath = resolve15(options.path);
41218
+ const basePath = resolve16(options.path);
40523
41219
  const tags = options.patterns || [...EXTRACT_TAGS];
40524
41220
  const extensions = options.extensions ? new Set(options.extensions.map((e) => e.startsWith(".") ? e : `.${e}`)) : DEFAULT_EXTENSIONS;
40525
41221
  const excludes = options.exclude || [];
@@ -40530,7 +41226,7 @@ function buildCodebaseIndex(options) {
40530
41226
  const fullPath = statSync8(basePath).isFile() ? basePath : join24(basePath, file);
40531
41227
  try {
40532
41228
  const source9 = readFileSync23(fullPath, "utf-8");
40533
- const relPath = statSync8(basePath).isFile() ? relative6(resolve15(basePath, ".."), fullPath) : file;
41229
+ const relPath = statSync8(basePath).isFile() ? relative6(resolve16(basePath, ".."), fullPath) : file;
40534
41230
  indexed.push({
40535
41231
  file: relPath,
40536
41232
  checksum: stableHash(source9).slice(0, 24),
@@ -40550,7 +41246,7 @@ function buildCodebaseIndex(options) {
40550
41246
  };
40551
41247
  }
40552
41248
  function extractTodos(options, db) {
40553
- const basePath = resolve15(options.path);
41249
+ const basePath = resolve16(options.path);
40554
41250
  const tags = options.patterns || [...EXTRACT_TAGS];
40555
41251
  const extensions = options.extensions ? new Set(options.extensions.map((e) => e.startsWith(".") ? e : `.${e}`)) : DEFAULT_EXTENSIONS;
40556
41252
  const excludes = options.exclude || [];
@@ -40561,7 +41257,7 @@ function extractTodos(options, db) {
40561
41257
  const fullPath = statSync8(basePath).isFile() ? basePath : join24(basePath, file);
40562
41258
  try {
40563
41259
  const source9 = readFileSync23(fullPath, "utf-8");
40564
- const relPath = statSync8(basePath).isFile() ? relative6(resolve15(basePath, ".."), fullPath) : file;
41260
+ const relPath = statSync8(basePath).isFile() ? relative6(resolve16(basePath, ".."), fullPath) : file;
40565
41261
  const comments = extractFromSource(source9, relPath, tags);
40566
41262
  allComments.push(...comments);
40567
41263
  } catch {}
@@ -40655,7 +41351,7 @@ async function watchSourceTodos(options, onRun) {
40655
41351
  const interval = Math.max(100, options.interval_ms || 2000);
40656
41352
  const once = options.once !== false && (!options.max_runs || options.max_runs <= 1);
40657
41353
  const maxRuns = options.max_runs ?? (once ? 1 : Number.POSITIVE_INFINITY);
40658
- const root = resolve15(options.path);
41354
+ const root = resolve16(options.path);
40659
41355
  const runs = [];
40660
41356
  let previous = new Map;
40661
41357
  for (let runNumber = 1;runNumber <= maxRuns; runNumber++) {
@@ -41528,7 +42224,7 @@ function renderAgentReplaySimulationMarkdown(simulation) {
41528
42224
  init_config();
41529
42225
  import { createHash as createHash16, createVerify } from "crypto";
41530
42226
  import { existsSync as existsSync26, readdirSync as readdirSync5, readFileSync as readFileSync25, statSync as statSync9 } from "fs";
41531
- import { basename as basename5, join as join25, resolve as resolve16 } from "path";
42227
+ import { basename as basename5, join as join25, resolve as resolve17 } from "path";
41532
42228
  init_redaction();
41533
42229
  function isObject2(value) {
41534
42230
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
@@ -41808,7 +42504,7 @@ function verifyExtensionSignature(input) {
41808
42504
  return verifier.verify(input.public_key, decodeSignature(input.signature));
41809
42505
  }
41810
42506
  function inspectExtensionSource(source9) {
41811
- const resolved = resolve16(source9);
42507
+ const resolved = resolve17(source9);
41812
42508
  if (!existsSync26(resolved))
41813
42509
  throw new Error(`extension source not found: ${source9}`);
41814
42510
  const stat = statSync9(resolved);
@@ -41906,7 +42602,7 @@ function testExtensionCompatibility(sourceOrManifest) {
41906
42602
  function projectExtensionSources(projectPath) {
41907
42603
  if (!projectPath)
41908
42604
  return [];
41909
- const root = resolve16(projectPath);
42605
+ const root = resolve17(projectPath);
41910
42606
  const candidates = [
41911
42607
  join25(root, "todos.extension.json"),
41912
42608
  join25(root, ".todos", "todos.extension.json")
@@ -41925,7 +42621,7 @@ function projectExtensionSources(projectPath) {
41925
42621
  }
41926
42622
  function discoverLocalExtensions(options = {}) {
41927
42623
  const config = loadConfig();
41928
- const projectPath = options.project_path ? resolve16(options.project_path) : null;
42624
+ const projectPath = options.project_path ? resolve17(options.project_path) : null;
41929
42625
  const configuredSources = [
41930
42626
  ...config.extension_sources || [],
41931
42627
  ...projectPath ? config.project_overrides?.[projectPath]?.extension_sources || [] : []
@@ -41933,7 +42629,7 @@ function discoverLocalExtensions(options = {}) {
41933
42629
  const sources = Array.from(new Set([
41934
42630
  ...configuredSources,
41935
42631
  ...projectExtensionSources(projectPath || undefined)
41936
- ])).map((source9) => projectPath && !source9.startsWith("/") ? resolve16(projectPath, source9) : resolve16(source9));
42632
+ ])).map((source9) => projectPath && !source9.startsWith("/") ? resolve17(projectPath, source9) : resolve17(source9));
41937
42633
  const warnings = [];
41938
42634
  const discovered = [];
41939
42635
  for (const source9 of sources) {
@@ -44081,6 +44777,7 @@ export {
44081
44777
  writeBackupManifest,
44082
44778
  writeArtifactExportManifest,
44083
44779
  wrapWithSchemaVersion,
44780
+ workflowPointersFromMetadata,
44084
44781
  watchSourceTodos,
44085
44782
  verifyTaskRunArtifacts,
44086
44783
  verifyStoredArtifact,
@@ -44180,6 +44877,7 @@ export {
44180
44877
  shouldRegisterToolForProfile2 as shouldRegisterToolForAccessProfile,
44181
44878
  setupEphemeralDemoDb,
44182
44879
  setTaskWorkflowState,
44880
+ setTaskWorkflowPointers,
44183
44881
  setTaskStatus,
44184
44882
  setTaskPriorityMeta,
44185
44883
  setTaskPriority,
@@ -44222,6 +44920,8 @@ export {
44222
44920
  runNextAgentDispatch,
44223
44921
  runDueDispatches,
44224
44922
  runAgentWorkflowDemo,
44923
+ routingAutomationMetadata,
44924
+ routeEnabledForTask,
44225
44925
  revokeApiKey,
44226
44926
  returnReviewItem,
44227
44927
  retryAgentRunDispatch,
@@ -44358,6 +45058,7 @@ export {
44358
45058
  parseFeedback,
44359
45059
  parseErrorPaste,
44360
45060
  parseCiLog,
45061
+ objectField,
44361
45062
  now,
44362
45063
  notifyUpcomingDeadlines2 as notifyUpcomingDeadlines,
44363
45064
  normalizeTesterIssueReport,
@@ -44490,6 +45191,7 @@ export {
44490
45191
  linkRunVerification,
44491
45192
  linkRunArtifact,
44492
45193
  issueToTask,
45194
+ isWorktreePath,
44493
45195
  isValidRecurrenceRule,
44494
45196
  isTodosRemoteStorageEnabled,
44495
45197
  isSnapshotStale,
@@ -44503,6 +45205,7 @@ export {
44503
45205
  inspectGitCommit,
44504
45206
  inspectExtensionSource,
44505
45207
  initBuiltinTemplates,
45208
+ inferRootProjectId,
44506
45209
  importUserScaffold,
44507
45210
  importTodosMarkdown,
44508
45211
  importTemplateFromFile,
@@ -44554,6 +45257,7 @@ export {
44554
45257
  getTaskStats,
44555
45258
  getTaskRunLedger,
44556
45259
  getTaskRun,
45260
+ getTaskRouteState,
44557
45261
  getTaskReview,
44558
45262
  getTaskRelationships,
44559
45263
  getTaskRelationship,
@@ -44924,6 +45628,7 @@ export {
44924
45628
  completeAgentRun,
44925
45629
  compareEnvironmentSnapshots,
44926
45630
  compareEnvironmentSnapshotFiles,
45631
+ compactWorkflowPointers,
44927
45632
  compactScaleStorage,
44928
45633
  compactDatabase,
44929
45634
  collectCliCommandEntries,
@@ -44934,6 +45639,7 @@ export {
44934
45639
  clearActiveModel,
44935
45640
  cleanupArtifacts,
44936
45641
  cleanExpiredLocks,
45642
+ classifyProjectKind,
44937
45643
  claimReviewItem,
44938
45644
  claimPlanStep,
44939
45645
  claimOrSteal,
@@ -44978,6 +45684,7 @@ export {
44978
45684
  buildCodebaseIndex,
44979
45685
  buildArtifactExportManifest,
44980
45686
  bootstrapProject,
45687
+ booleanField,
44981
45688
  beginTaskRunTransaction,
44982
45689
  backupDatabase,
44983
45690
  autoReleaseStaleAgents,
@@ -45028,6 +45735,7 @@ export {
45028
45735
  TaskNotFoundError,
45029
45736
  TaskListNotFoundError,
45030
45737
  TUI_DASHBOARD_VIEWS,
45738
+ TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION,
45031
45739
  TODOS_STORAGE_TABLES,
45032
45740
  TODOS_STORAGE_FALLBACK_ENV,
45033
45741
  TODOS_STORAGE_ENV,
@@ -45061,6 +45769,7 @@ export {
45061
45769
  TESTERS_ISSUE_REPORT_SCHEMA_VERSION,
45062
45770
  TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
45063
45771
  TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION,
45772
+ TASK_WORKFLOW_POINTER_SCHEMA_VERSION,
45064
45773
  TASK_STATUSES,
45065
45774
  TASK_SCHEDULING_SCHEMA,
45066
45775
  TASK_PRIORITIES,