@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/cli/index.js CHANGED
@@ -1011,7 +1011,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1011
1011
  this._exitCallback = (err) => {
1012
1012
  if (err.code !== "commander.executeSubCommandAsync") {
1013
1013
  throw err;
1014
- } else {}
1014
+ }
1015
1015
  };
1016
1016
  }
1017
1017
  return this;
@@ -3266,6 +3266,22 @@ var init_migrations = __esm(() => {
3266
3266
  CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source);
3267
3267
  CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint);
3268
3268
  INSERT OR IGNORE INTO _migrations (id) VALUES (62);
3269
+ `,
3270
+ `
3271
+ CREATE TABLE IF NOT EXISTS storage_tombstones (
3272
+ id TEXT PRIMARY KEY,
3273
+ object_type TEXT NOT NULL,
3274
+ object_id TEXT NOT NULL,
3275
+ deleted_at TEXT NOT NULL,
3276
+ updated_at TEXT NOT NULL,
3277
+ source_machine_id TEXT,
3278
+ payload TEXT,
3279
+ version INTEGER,
3280
+ UNIQUE(object_type, object_id)
3281
+ );
3282
+ CREATE INDEX IF NOT EXISTS idx_storage_tombstones_object ON storage_tombstones(object_type, object_id);
3283
+ CREATE INDEX IF NOT EXISTS idx_storage_tombstones_updated ON storage_tombstones(updated_at);
3284
+ INSERT OR IGNORE INTO _migrations (id) VALUES (63);
3269
3285
  `
3270
3286
  ];
3271
3287
  });
@@ -3787,6 +3803,20 @@ function ensureSchema(db) {
3787
3803
  )`);
3788
3804
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_project ON project_machine_paths(project_id)");
3789
3805
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_machine ON project_machine_paths(machine_id)");
3806
+ ensureTable("storage_tombstones", `
3807
+ CREATE TABLE storage_tombstones (
3808
+ id TEXT PRIMARY KEY,
3809
+ object_type TEXT NOT NULL,
3810
+ object_id TEXT NOT NULL,
3811
+ deleted_at TEXT NOT NULL,
3812
+ updated_at TEXT NOT NULL,
3813
+ source_machine_id TEXT,
3814
+ payload TEXT,
3815
+ version INTEGER,
3816
+ UNIQUE(object_type, object_id)
3817
+ )`);
3818
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_storage_tombstones_object ON storage_tombstones(object_type, object_id)");
3819
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_storage_tombstones_updated ON storage_tombstones(updated_at)");
3790
3820
  ensureTable("machines", `
3791
3821
  CREATE TABLE machines (
3792
3822
  id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, hostname TEXT, platform TEXT,
@@ -4981,6 +5011,80 @@ var init_types = __esm(() => {
4981
5011
  };
4982
5012
  });
4983
5013
 
5014
+ // src/db/storage-tombstones.ts
5015
+ function recordStorageTombstone(input, db) {
5016
+ const d = db ?? getDatabase();
5017
+ const deletedAt = input.deleted_at ?? now();
5018
+ const machineId = input.source_machine_id ?? currentStorageMachineId(d);
5019
+ d.run(`INSERT INTO storage_tombstones (
5020
+ id, object_type, object_id, deleted_at, updated_at, source_machine_id, payload, version
5021
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
5022
+ ON CONFLICT(object_type, object_id) DO UPDATE SET
5023
+ deleted_at = excluded.deleted_at,
5024
+ updated_at = excluded.updated_at,
5025
+ source_machine_id = excluded.source_machine_id,
5026
+ payload = excluded.payload,
5027
+ version = excluded.version
5028
+ WHERE storage_tombstones.updated_at IS NULL OR storage_tombstones.updated_at <= excluded.updated_at`, [
5029
+ uuid(),
5030
+ input.object_type,
5031
+ input.object_id,
5032
+ deletedAt,
5033
+ deletedAt,
5034
+ machineId,
5035
+ input.payload ? JSON.stringify(input.payload) : null,
5036
+ input.version ?? null
5037
+ ]);
5038
+ return getStorageTombstone(input.object_type, input.object_id, d);
5039
+ }
5040
+ function getStorageTombstone(objectType, objectId, db) {
5041
+ const d = db ?? getDatabase();
5042
+ const row = d.query("SELECT * FROM storage_tombstones WHERE object_type = ? AND object_id = ?").get(objectType, objectId);
5043
+ return row ? rowToStorageTombstone(row) : null;
5044
+ }
5045
+ function listStorageTombstones(db) {
5046
+ const d = db ?? getDatabase();
5047
+ return d.query("SELECT * FROM storage_tombstones ORDER BY updated_at ASC, object_type ASC, object_id ASC").all().map(rowToStorageTombstone);
5048
+ }
5049
+ function shouldApplyStorageTombstone(tombstone, existingUpdatedAt) {
5050
+ const tombstoneClock = Date.parse(tombstone.updated_at || tombstone.deleted_at);
5051
+ if (!existingUpdatedAt)
5052
+ return true;
5053
+ const existingClock = Date.parse(existingUpdatedAt);
5054
+ if (Number.isNaN(tombstoneClock))
5055
+ return true;
5056
+ if (Number.isNaN(existingClock))
5057
+ return true;
5058
+ return tombstoneClock >= existingClock;
5059
+ }
5060
+ function rowToStorageTombstone(row) {
5061
+ return {
5062
+ ...row,
5063
+ payload: parsePayload(row.payload)
5064
+ };
5065
+ }
5066
+ function parsePayload(value) {
5067
+ if (!value)
5068
+ return null;
5069
+ try {
5070
+ const parsed = JSON.parse(value);
5071
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
5072
+ } catch {
5073
+ return null;
5074
+ }
5075
+ }
5076
+ function currentStorageMachineId(db) {
5077
+ try {
5078
+ return getMachineId(db);
5079
+ } catch {
5080
+ return null;
5081
+ }
5082
+ }
5083
+ var init_storage_tombstones = __esm(() => {
5084
+ init_database();
5085
+ init_machines();
5086
+ });
5087
+
4984
5088
  // src/db/projects.ts
4985
5089
  var exports_projects = {};
4986
5090
  __export(exports_projects, {
@@ -5032,8 +5136,9 @@ function createProject(input, db) {
5032
5136
  const timestamp = now();
5033
5137
  const taskListId = input.task_list_id ?? `todos-${slugify(input.name)}`;
5034
5138
  const taskPrefix = input.task_prefix || generatePrefix(input.name, d);
5035
- d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at)
5036
- VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp, timestamp]);
5139
+ const machineId = currentStorageMachineId(d);
5140
+ d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at, machine_id)
5141
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp, timestamp, machineId]);
5037
5142
  return getProject(id, d);
5038
5143
  }
5039
5144
  function getProject(id, db) {
@@ -5112,6 +5217,14 @@ function renameProject(id, input, db) {
5112
5217
  }
5113
5218
  function deleteProject(id, db) {
5114
5219
  const d = db || getDatabase();
5220
+ const project = getProject(id, d);
5221
+ if (!project)
5222
+ return false;
5223
+ recordStorageTombstone({
5224
+ object_type: "projects",
5225
+ object_id: id,
5226
+ payload: project
5227
+ }, d);
5115
5228
  const result = d.run("DELETE FROM projects WHERE id = ?", [id]);
5116
5229
  return result.changes > 0;
5117
5230
  }
@@ -5216,6 +5329,14 @@ function listMachineLocalPaths(projectId, db) {
5216
5329
  function removeMachineLocalPath(projectId, machineId, db) {
5217
5330
  const d = db || getDatabase();
5218
5331
  const mid = machineId ?? getMachineId(d);
5332
+ const existing = d.query("SELECT * FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(projectId, mid);
5333
+ if (!existing)
5334
+ return false;
5335
+ recordStorageTombstone({
5336
+ object_type: "project_machine_paths",
5337
+ object_id: existing.id,
5338
+ payload: existing
5339
+ }, d);
5219
5340
  const result = d.run("DELETE FROM project_machine_paths WHERE project_id = ? AND machine_id = ?", [projectId, mid]);
5220
5341
  return result.changes > 0;
5221
5342
  }
@@ -5223,6 +5344,7 @@ var init_projects = __esm(() => {
5223
5344
  init_types();
5224
5345
  init_database();
5225
5346
  init_machines();
5347
+ init_storage_tombstones();
5226
5348
  });
5227
5349
 
5228
5350
  // src/lib/sync-utils.ts
@@ -5467,6 +5589,57 @@ var init_completion_guard = __esm(() => {
5467
5589
  init_projects();
5468
5590
  });
5469
5591
 
5592
+ // src/lib/event-emission-safety.ts
5593
+ import { tmpdir } from "os";
5594
+ import { resolve as resolve3, sep } from "path";
5595
+ function envFlag(name) {
5596
+ const value = process.env[name];
5597
+ return value === "1" || value === "true" || value === "yes";
5598
+ }
5599
+ function isUnder(parent, child) {
5600
+ const normalizedParent = resolve3(parent);
5601
+ const normalizedChild = resolve3(child);
5602
+ return normalizedChild === normalizedParent || normalizedChild.startsWith(`${normalizedParent}${sep}`);
5603
+ }
5604
+ function databasePathFromDatabase(db) {
5605
+ const filename = db?.filename;
5606
+ return typeof filename === "string" && filename.trim() ? filename : undefined;
5607
+ }
5608
+ function isEphemeralTodosDatabase(dbPath) {
5609
+ const resolvedPath = dbPath ?? getDatabasePath();
5610
+ if (resolvedPath === ":memory:" || resolvedPath.startsWith("file::memory:"))
5611
+ return true;
5612
+ return isUnder(tmpdir(), resolvedPath);
5613
+ }
5614
+ function hasExplicitSharedEventsStore() {
5615
+ return Boolean(process.env["HASNA_EVENTS_DIR"] || process.env["HASNA_EVENTS_HOME"]);
5616
+ }
5617
+ function usesIsolatedTodosHome() {
5618
+ return isUnder(tmpdir(), getTodosGlobalDir());
5619
+ }
5620
+ function shouldEmitSharedTaskEvents(dbPath) {
5621
+ if (envFlag("TODOS_DISABLE_SHARED_EVENTS"))
5622
+ return false;
5623
+ if (envFlag("TODOS_ALLOW_EPHEMERAL_SHARED_EVENTS"))
5624
+ return true;
5625
+ if (!isEphemeralTodosDatabase(dbPath))
5626
+ return true;
5627
+ return hasExplicitSharedEventsStore();
5628
+ }
5629
+ function shouldDeliverLocalLifecycleHooks(dbPath) {
5630
+ if (envFlag("TODOS_DISABLE_LOCAL_EVENT_HOOKS"))
5631
+ return false;
5632
+ if (envFlag("TODOS_ALLOW_EPHEMERAL_LOCAL_EVENT_HOOKS"))
5633
+ return true;
5634
+ if (!isEphemeralTodosDatabase(dbPath))
5635
+ return true;
5636
+ return usesIsolatedTodosHome();
5637
+ }
5638
+ var init_event_emission_safety = __esm(() => {
5639
+ init_database();
5640
+ init_sync_utils();
5641
+ });
5642
+
5470
5643
  // src/lib/redaction.ts
5471
5644
  var exports_redaction = {};
5472
5645
  __export(exports_redaction, {
@@ -5587,9 +5760,9 @@ __export(exports_workspace_trust, {
5587
5760
  getWorkspaceTrustStatus: () => getWorkspaceTrustStatus,
5588
5761
  checkWorkspacePermission: () => checkWorkspacePermission
5589
5762
  });
5590
- import { relative, resolve as resolve3 } from "path";
5763
+ import { relative, resolve as resolve4 } from "path";
5591
5764
  function normalizePath(path) {
5592
- return resolve3(path);
5765
+ return resolve4(path);
5593
5766
  }
5594
5767
  function unique2(values) {
5595
5768
  return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
@@ -5780,9 +5953,9 @@ __export(exports_runner_sandbox, {
5780
5953
  explainRunnerSandbox: () => explainRunnerSandbox,
5781
5954
  checkRunnerSandbox: () => checkRunnerSandbox
5782
5955
  });
5783
- import { relative as relative2, resolve as resolve4 } from "path";
5956
+ import { relative as relative2, resolve as resolve5 } from "path";
5784
5957
  function normalizePath2(path) {
5785
- return resolve4(path);
5958
+ return resolve5(path);
5786
5959
  }
5787
5960
  function unique3(values) {
5788
5961
  return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
@@ -5986,7 +6159,7 @@ __export(exports_event_hooks, {
5986
6159
  });
5987
6160
  import { createHash, randomUUID } from "crypto";
5988
6161
  import { appendFileSync, mkdirSync as mkdirSync3 } from "fs";
5989
- import { dirname as dirname4, resolve as resolve5 } from "path";
6162
+ import { dirname as dirname4, resolve as resolve6 } from "path";
5990
6163
  import { createConnection } from "net";
5991
6164
  function safeName(name) {
5992
6165
  const trimmed = name.trim();
@@ -6124,7 +6297,7 @@ async function deliverHook(hook, envelope) {
6124
6297
  if (hook.target === "stdout") {
6125
6298
  output = line.trim();
6126
6299
  } else if (hook.target === "file") {
6127
- const filePath = resolve5(hook.file_path);
6300
+ const filePath = resolve6(hook.file_path);
6128
6301
  mkdirSync3(dirname4(filePath), { recursive: true });
6129
6302
  appendFileSync(filePath, line);
6130
6303
  } else if (hook.target === "socket") {
@@ -6199,6 +6372,8 @@ async function emitLocalEventHooks(input) {
6199
6372
  return Promise.all(hooks.map((hook) => deliverHook(hook, envelope)));
6200
6373
  }
6201
6374
  function emitLocalEventHooksQuiet(input) {
6375
+ if (!shouldDeliverLocalLifecycleHooks(input.databasePath))
6376
+ return;
6202
6377
  emitLocalEventHooks(input).catch(() => {});
6203
6378
  }
6204
6379
  async function testLocalEventHook(name, input) {
@@ -6212,6 +6387,7 @@ var init_event_hooks = __esm(() => {
6212
6387
  init_redaction();
6213
6388
  init_runner_sandbox();
6214
6389
  init_config();
6390
+ init_event_emission_safety();
6215
6391
  LOCAL_EVENT_TYPES = [
6216
6392
  "task.created",
6217
6393
  "task.assigned",
@@ -6544,7 +6720,7 @@ async function dispatchCommand(event, channel) {
6544
6720
  HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
6545
6721
  HASNA_EVENT_JSON: eventJson
6546
6722
  };
6547
- return new Promise((resolve6) => {
6723
+ return new Promise((resolve7) => {
6548
6724
  const child = spawn(channel.command.command, channel.command.args ?? [], {
6549
6725
  cwd: channel.command.cwd,
6550
6726
  env,
@@ -6562,7 +6738,7 @@ async function dispatchCommand(event, channel) {
6562
6738
  });
6563
6739
  child.on("error", (error) => {
6564
6740
  clearTimeout(timeout);
6565
- resolve6({
6741
+ resolve7({
6566
6742
  attempt: 1,
6567
6743
  status: "failed",
6568
6744
  startedAt,
@@ -6575,7 +6751,7 @@ async function dispatchCommand(event, channel) {
6575
6751
  child.on("close", (code, signal) => {
6576
6752
  clearTimeout(timeout);
6577
6753
  const success = code === 0;
6578
- resolve6({
6754
+ resolve7({
6579
6755
  attempt: 1,
6580
6756
  status: success ? "success" : "failed",
6581
6757
  startedAt,
@@ -6842,14 +7018,15 @@ function createTaskList(input, db) {
6842
7018
  const id = uuid();
6843
7019
  const timestamp = now();
6844
7020
  const slug = input.slug || slugify(input.name);
7021
+ const machineId = currentStorageMachineId(d);
6845
7022
  if (!input.project_id) {
6846
7023
  const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
6847
7024
  if (existing) {
6848
7025
  throw new Error(`Standalone task list with slug "${slug}" already exists`);
6849
7026
  }
6850
7027
  }
6851
- d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
6852
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
7028
+ d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at, machine_id)
7029
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp, machineId]);
6853
7030
  return getTaskList(id, d);
6854
7031
  }
6855
7032
  function getTaskList(id, db) {
@@ -6899,6 +7076,14 @@ function updateTaskList(id, input, db) {
6899
7076
  }
6900
7077
  function deleteTaskList(id, db) {
6901
7078
  const d = db || getDatabase();
7079
+ const list = getTaskList(id, d);
7080
+ if (!list)
7081
+ return false;
7082
+ recordStorageTombstone({
7083
+ object_type: "task_lists",
7084
+ object_id: id,
7085
+ payload: list
7086
+ }, d);
6902
7087
  return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
6903
7088
  }
6904
7089
  function ensureTaskList(name, slug, projectId, db) {
@@ -6912,40 +7097,10 @@ var init_task_lists = __esm(() => {
6912
7097
  init_types();
6913
7098
  init_database();
6914
7099
  init_projects();
7100
+ init_storage_tombstones();
6915
7101
  });
6916
7102
 
6917
- // src/lib/shared-events.ts
6918
- function taskEventData(task, extra = {}) {
6919
- return {
6920
- id: task.id,
6921
- task_id: task.id,
6922
- short_id: task.short_id,
6923
- title: task.title,
6924
- description: task.description,
6925
- status: task.status,
6926
- priority: task.priority,
6927
- project_id: task.project_id,
6928
- parent_id: task.parent_id,
6929
- plan_id: task.plan_id,
6930
- task_list_id: task.task_list_id,
6931
- agent_id: task.agent_id,
6932
- assigned_to: task.assigned_to,
6933
- session_id: task.session_id,
6934
- working_dir: task.working_dir,
6935
- tags: task.tags,
6936
- metadata: task.metadata,
6937
- version: task.version,
6938
- created_at: task.created_at,
6939
- updated_at: task.updated_at,
6940
- started_at: task.started_at,
6941
- completed_at: task.completed_at,
6942
- due_at: task.due_at,
6943
- requires_approval: task.requires_approval,
6944
- approved_by: task.approved_by,
6945
- approved_at: task.approved_at,
6946
- ...extra
6947
- };
6948
- }
7103
+ // src/lib/task-route-contract.ts
6949
7104
  function booleanField(value) {
6950
7105
  if (typeof value === "boolean")
6951
7106
  return value;
@@ -6967,21 +7122,31 @@ function booleanField(value) {
6967
7122
  function objectField(value) {
6968
7123
  return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
6969
7124
  }
6970
- function firstBoolean(records, keys) {
7125
+ function collectBooleans(records, keys) {
7126
+ const values = [];
6971
7127
  for (const record of records) {
7128
+ if (!record)
7129
+ continue;
6972
7130
  for (const key of keys) {
6973
7131
  const value = booleanField(record[key]);
6974
7132
  if (value !== undefined)
6975
- return value;
7133
+ values.push(value);
6976
7134
  }
6977
7135
  }
6978
- return;
7136
+ return values;
6979
7137
  }
6980
- function routingAutomationMetadata(task) {
6981
- const automation = objectField(task.metadata.automation);
6982
- const records = [task.metadata];
6983
- if (automation)
6984
- records.push(automation);
7138
+ function mergedBoolean(records, keys, trueWins) {
7139
+ const values = collectBooleans(records, keys);
7140
+ if (values.length === 0)
7141
+ return;
7142
+ if (trueWins)
7143
+ return values.some(Boolean);
7144
+ return values.includes(false) ? false : true;
7145
+ }
7146
+ function routingAutomationMetadata(task, taskList) {
7147
+ const taskAutomation = objectField(task.metadata.automation);
7148
+ const taskListAutomation = taskList ? objectField(taskList.metadata.automation) : undefined;
7149
+ const records = [task.metadata, taskAutomation, taskList?.metadata, taskListAutomation];
6985
7150
  const result = {};
6986
7151
  const aliases = [
6987
7152
  ["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
@@ -6992,7 +7157,7 @@ function routingAutomationMetadata(task) {
6992
7157
  ["approval_required", ["approval_required", "approvalRequired"]]
6993
7158
  ];
6994
7159
  for (const [canonical, keys] of aliases) {
6995
- const value = firstBoolean(records, keys);
7160
+ const value = mergedBoolean(records, keys, canonical !== "allowed");
6996
7161
  if (value !== undefined)
6997
7162
  result[canonical] = value;
6998
7163
  }
@@ -7000,23 +7165,91 @@ function routingAutomationMetadata(task) {
7000
7165
  result.requires_approval = true;
7001
7166
  return Object.keys(result).length > 0 ? result : undefined;
7002
7167
  }
7168
+ function routeEnabledForTask(task, taskList) {
7169
+ const explicit = booleanField(task.metadata.route_enabled);
7170
+ if (explicit !== undefined)
7171
+ return explicit;
7172
+ if (task.tags.includes("auto:route") || task.tags.includes("route:enabled"))
7173
+ return true;
7174
+ const taskListDefault = taskList ? booleanField(taskList.metadata.route_enabled) : undefined;
7175
+ if (taskListDefault !== undefined)
7176
+ return taskListDefault;
7177
+ return;
7178
+ }
7179
+ function stringField(value) {
7180
+ return typeof value === "string" && value.trim() ? value : undefined;
7181
+ }
7182
+ function workflowPointersFromMetadata(metadata) {
7183
+ const nested = objectField(metadata.workflow_invocation) ?? objectField(metadata.workflow) ?? {};
7184
+ return {
7185
+ current_workflow_invocation_id: stringField(metadata.current_workflow_invocation_id) ?? stringField(nested.current_workflow_invocation_id) ?? stringField(nested.invocation_id),
7186
+ current_run_id: stringField(metadata.current_run_id) ?? stringField(nested.current_run_id) ?? stringField(nested.run_id),
7187
+ latest_manifest_path: stringField(metadata.latest_manifest_path) ?? stringField(nested.latest_manifest_path) ?? stringField(nested.manifest_path),
7188
+ latest_evaluation_path: stringField(metadata.latest_evaluation_path) ?? stringField(nested.latest_evaluation_path) ?? stringField(nested.evaluation_path),
7189
+ workflow_state: stringField(metadata.workflow_state) ?? stringField(nested.workflow_state) ?? stringField(nested.state)
7190
+ };
7191
+ }
7192
+ function compactWorkflowPointers(pointers) {
7193
+ return Object.fromEntries(Object.entries(pointers).filter(([, value]) => typeof value === "string" && value.length > 0));
7194
+ }
7195
+ function classifyProjectKind(path) {
7196
+ return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
7197
+ }
7198
+ function isWorktreePath(path) {
7199
+ return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
7200
+ }
7201
+ function inferRootProjectId(project) {
7202
+ return isWorktreePath(project.path) ? null : project.id;
7203
+ }
7204
+ var TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION = "todos.task_route_state.v1", TASK_WORKFLOW_POINTER_SCHEMA_VERSION = "todos.task_workflow_pointer.v1";
7205
+
7206
+ // src/lib/shared-events.ts
7207
+ function taskEventData(task, extra = {}) {
7208
+ return {
7209
+ id: task.id,
7210
+ task_id: task.id,
7211
+ short_id: task.short_id,
7212
+ title: task.title,
7213
+ description: task.description,
7214
+ status: task.status,
7215
+ priority: task.priority,
7216
+ project_id: task.project_id,
7217
+ parent_id: task.parent_id,
7218
+ plan_id: task.plan_id,
7219
+ task_list_id: task.task_list_id,
7220
+ agent_id: task.agent_id,
7221
+ assigned_to: task.assigned_to,
7222
+ session_id: task.session_id,
7223
+ working_dir: task.working_dir,
7224
+ tags: task.tags,
7225
+ metadata: task.metadata,
7226
+ version: task.version,
7227
+ created_at: task.created_at,
7228
+ updated_at: task.updated_at,
7229
+ started_at: task.started_at,
7230
+ completed_at: task.completed_at,
7231
+ due_at: task.due_at,
7232
+ requires_approval: task.requires_approval,
7233
+ approved_by: task.approved_by,
7234
+ approved_at: task.approved_at,
7235
+ ...extra
7236
+ };
7237
+ }
7003
7238
  function taskEventMetadata(task) {
7004
7239
  const metadata = {
7005
7240
  package: "@hasna/todos",
7006
7241
  todos_event_schema_version: 1,
7242
+ route_state_schema_version: TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION,
7007
7243
  task_id: task.id,
7008
7244
  task_short_id: task.short_id,
7009
7245
  project_id: task.project_id,
7010
7246
  task_list_id: task.task_list_id,
7011
7247
  working_dir: task.working_dir
7012
7248
  };
7013
- const routeEnabled = booleanField(task.metadata.route_enabled);
7014
- if (routeEnabled !== undefined) {
7015
- metadata.route_enabled = routeEnabled;
7016
- }
7017
- const automation = routingAutomationMetadata(task);
7018
- if (automation) {
7019
- metadata.automation = automation;
7249
+ const pointers = workflowPointersFromMetadata(task.metadata);
7250
+ for (const [key, value] of Object.entries(pointers)) {
7251
+ if (value)
7252
+ metadata[key] = value;
7020
7253
  }
7021
7254
  try {
7022
7255
  const project = task.project_id ? getProject(task.project_id) : null;
@@ -7045,18 +7278,20 @@ function taskEventMetadata(task) {
7045
7278
  metadata.task_list_project_id = taskList.project_id;
7046
7279
  metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
7047
7280
  }
7281
+ const routeEnabled = routeEnabledForTask(task, taskList);
7282
+ if (routeEnabled !== undefined) {
7283
+ metadata.route_enabled = routeEnabled;
7284
+ }
7285
+ const automation = routingAutomationMetadata(task, taskList);
7286
+ if (automation) {
7287
+ metadata.automation = automation;
7288
+ metadata.route_blocked_by_no_auto = automation.no_auto === true;
7289
+ metadata.route_blocked_by_manual = automation.manual === true || automation.manual_required === true;
7290
+ metadata.route_blocked_by_approval = (automation.requires_approval === true || automation.approval_required === true) && !task.approved_by;
7291
+ }
7048
7292
  } catch {}
7049
7293
  return metadata;
7050
7294
  }
7051
- function classifyProjectKind(path) {
7052
- return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
7053
- }
7054
- function isWorktreePath(path) {
7055
- return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
7056
- }
7057
- function inferRootProjectId(project) {
7058
- return isWorktreePath(project.path) ? null : project.id;
7059
- }
7060
7295
  function readMachineLocalPath(project) {
7061
7296
  const machineId = process.env["TODOS_MACHINE_ID"];
7062
7297
  if (!machineId)
@@ -7069,6 +7304,8 @@ function readMachineLocalPath(project) {
7069
7304
  }
7070
7305
  }
7071
7306
  async function emitSharedTaskEvent(input) {
7307
+ if (!shouldEmitSharedTaskEvents(input.databasePath))
7308
+ return;
7072
7309
  const data = taskEventData(input.task, input.data);
7073
7310
  await new EventsClient().emit({
7074
7311
  source: SOURCE,
@@ -7092,6 +7329,7 @@ var init_shared_events = __esm(() => {
7092
7329
  init_database();
7093
7330
  init_projects();
7094
7331
  init_task_lists();
7332
+ init_event_emission_safety();
7095
7333
  });
7096
7334
 
7097
7335
  // src/lib/secret-redaction.ts
@@ -7347,8 +7585,9 @@ function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
7347
7585
  const d = db || getDatabase();
7348
7586
  const id = uuid();
7349
7587
  const timestamp = now();
7350
- d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at)
7351
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, oldValue ?? null, newValue ?? null, agentId || null, timestamp]);
7588
+ const machineId = currentStorageMachineId(d);
7589
+ d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at, machine_id)
7590
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, oldValue ?? null, newValue ?? null, agentId || null, timestamp, machineId]);
7352
7591
  try {
7353
7592
  const { logActivity: logActivity2 } = (init_activity_audit(), __toCommonJS(exports_activity_audit));
7354
7593
  logActivity2({
@@ -7361,7 +7600,7 @@ function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
7361
7600
  actor_id: agentId ?? undefined
7362
7601
  }, d);
7363
7602
  } catch {}
7364
- return { id, task_id: taskId, action, field: field || null, old_value: oldValue ?? null, new_value: newValue ?? null, agent_id: agentId || null, created_at: timestamp };
7603
+ return { id, task_id: taskId, action, field: field || null, old_value: oldValue ?? null, new_value: newValue ?? null, agent_id: agentId || null, created_at: timestamp, machine_id: machineId };
7365
7604
  }
7366
7605
  function getTaskHistory(taskId, db) {
7367
7606
  const d = db || getDatabase();
@@ -7399,6 +7638,7 @@ function getRecap(hours = 8, projectId, db) {
7399
7638
  }
7400
7639
  var init_audit = __esm(() => {
7401
7640
  init_database();
7641
+ init_storage_tombstones();
7402
7642
  });
7403
7643
 
7404
7644
  // src/db/webhooks.ts
@@ -7664,13 +7904,14 @@ function createTask(input, db) {
7664
7904
  const d = db || getDatabase();
7665
7905
  const timestamp = now();
7666
7906
  const tags = input.tags || [];
7907
+ const machineId = currentStorageMachineId(d);
7667
7908
  const assignedBy = input.assigned_by || input.agent_id;
7668
7909
  const assignedFromProject = input.assigned_from_project || null;
7669
7910
  let id = uuid();
7670
7911
  for (let attempt = 0;attempt < 3; attempt++) {
7671
7912
  try {
7672
- 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)
7673
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
7913
+ 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)
7914
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
7674
7915
  id,
7675
7916
  null,
7676
7917
  input.project_id || null,
@@ -7707,7 +7948,8 @@ function createTask(input, db) {
7707
7948
  input.spawned_from_session || null,
7708
7949
  assignedBy || null,
7709
7950
  assignedFromProject || null,
7710
- input.task_type || null
7951
+ input.task_type || null,
7952
+ machineId
7711
7953
  ]);
7712
7954
  break;
7713
7955
  } catch (e) {
@@ -7723,9 +7965,10 @@ function createTask(input, db) {
7723
7965
  }
7724
7966
  const task = getTask(id, d);
7725
7967
  const payload = taskEventData(task);
7968
+ const databasePath = databasePathFromDatabase(d);
7726
7969
  dispatchWebhook2("task.created", payload, d).catch(() => {});
7727
- emitLocalEventHooksQuiet({ type: "task.created", payload });
7728
- emitSharedTaskEventQuiet({ type: "task.created", task });
7970
+ emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
7971
+ emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
7729
7972
  return task;
7730
7973
  }
7731
7974
  function getTask(id, db) {
@@ -8143,29 +8386,39 @@ function updateTask(id, input, db) {
8143
8386
  approved_by: input.approved_by ?? task.approved_by,
8144
8387
  approved_at: input.approved_by ? timestamp : task.approved_at
8145
8388
  };
8389
+ const databasePath = databasePathFromDatabase(d);
8146
8390
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
8147
8391
  const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
8148
8392
  dispatchWebhook2("task.assigned", payload, d).catch(() => {});
8149
- emitLocalEventHooksQuiet({ type: "task.assigned", payload });
8150
- emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to } });
8393
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
8394
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to }, databasePath });
8151
8395
  }
8152
8396
  if (input.status !== undefined && input.status !== task.status) {
8153
8397
  const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
8154
8398
  dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
8155
- emitLocalEventHooksQuiet({ type: "task.status_changed", payload });
8156
- emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status } });
8399
+ emitLocalEventHooksQuiet({ type: "task.status_changed", payload, databasePath });
8400
+ emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status }, databasePath });
8157
8401
  }
8158
8402
  if (input.approved_by !== undefined) {
8159
- emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
8403
+ emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title }, databasePath });
8160
8404
  }
8161
8405
  const updatePayload = taskEventData(updatedTask);
8162
8406
  dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
8163
- emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
8164
- emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
8407
+ emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
8408
+ emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
8165
8409
  return updatedTask;
8166
8410
  }
8167
8411
  function deleteTask(id, db) {
8168
8412
  const d = db || getDatabase();
8413
+ const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
8414
+ if (!row)
8415
+ return false;
8416
+ recordStorageTombstone({
8417
+ object_type: "tasks",
8418
+ object_id: id,
8419
+ payload: rowToTask(row),
8420
+ version: row.version
8421
+ }, d);
8169
8422
  const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
8170
8423
  return result.changes > 0;
8171
8424
  }
@@ -8173,11 +8426,13 @@ var init_task_crud = __esm(() => {
8173
8426
  init_types();
8174
8427
  init_database();
8175
8428
  init_completion_guard();
8429
+ init_event_emission_safety();
8176
8430
  init_event_hooks();
8177
8431
  init_shared_events();
8178
8432
  init_audit();
8179
8433
  init_webhooks();
8180
8434
  init_checklists();
8435
+ init_storage_tombstones();
8181
8436
  });
8182
8437
 
8183
8438
  // src/lib/recurrence.ts
@@ -8326,8 +8581,9 @@ function resolveTemplateId(id, d) {
8326
8581
  function createTemplate(input, db) {
8327
8582
  const d = db || getDatabase();
8328
8583
  const id = uuid();
8329
- d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at)
8330
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
8584
+ const machineId = currentStorageMachineId(d);
8585
+ d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at, machine_id)
8586
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
8331
8587
  id,
8332
8588
  input.name,
8333
8589
  input.title_pattern,
@@ -8338,7 +8594,8 @@ function createTemplate(input, db) {
8338
8594
  input.project_id || null,
8339
8595
  input.plan_id || null,
8340
8596
  JSON.stringify(input.metadata || {}),
8341
- now()
8597
+ now(),
8598
+ machineId
8342
8599
  ]);
8343
8600
  if (input.tasks && input.tasks.length > 0) {
8344
8601
  addTemplateTasks(id, input.tasks, d);
@@ -8362,6 +8619,15 @@ function deleteTemplate(id, db) {
8362
8619
  const resolved = resolveTemplateId(id, d);
8363
8620
  if (!resolved)
8364
8621
  return false;
8622
+ const template = getTemplate(resolved, d);
8623
+ if (!template)
8624
+ return false;
8625
+ recordStorageTombstone({
8626
+ object_type: "templates",
8627
+ object_id: resolved,
8628
+ payload: template,
8629
+ version: template.version
8630
+ }, d);
8365
8631
  return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
8366
8632
  }
8367
8633
  function updateTemplate(id, updates, db) {
@@ -8731,6 +8997,7 @@ function previewTemplate(templateId, variables, db) {
8731
8997
  var init_templates = __esm(() => {
8732
8998
  init_database();
8733
8999
  init_tasks();
9000
+ init_storage_tombstones();
8734
9001
  });
8735
9002
 
8736
9003
  // src/db/task-graph.ts
@@ -8897,6 +9164,7 @@ function getBlockingDeps(id, db) {
8897
9164
  }
8898
9165
  function startTask(id, agentId, db) {
8899
9166
  const d = db || getDatabase();
9167
+ const databasePath = databasePathFromDatabase(d);
8900
9168
  const task = getTask(id, d);
8901
9169
  if (!task)
8902
9170
  throw new TaskNotFoundError(id);
@@ -8911,7 +9179,8 @@ function startTask(id, agentId, db) {
8911
9179
  agent_id: agentId,
8912
9180
  title: task.title,
8913
9181
  blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
8914
- }
9182
+ },
9183
+ databasePath
8915
9184
  });
8916
9185
  throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
8917
9186
  }
@@ -8933,12 +9202,13 @@ function startTask(id, agentId, db) {
8933
9202
  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 };
8934
9203
  const payload = taskEventData(startedTask, { agent_id: agentId });
8935
9204
  dispatchWebhook2("task.started", payload, d).catch(() => {});
8936
- emitLocalEventHooksQuiet({ type: "task.started", payload });
8937
- emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId } });
9205
+ emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
9206
+ emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId }, databasePath });
8938
9207
  return startedTask;
8939
9208
  }
8940
9209
  function completeTask(id, agentId, db, options) {
8941
9210
  const d = db || getDatabase();
9211
+ const databasePath = databasePathFromDatabase(d);
8942
9212
  const task = getTask(id, d);
8943
9213
  if (!task)
8944
9214
  throw new TaskNotFoundError(id);
@@ -8984,8 +9254,8 @@ function completeTask(id, agentId, db, options) {
8984
9254
  };
8985
9255
  const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
8986
9256
  dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
8987
- emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload });
8988
- emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp } });
9257
+ emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
9258
+ emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
8989
9259
  let spawnedTask = null;
8990
9260
  if (task.recurrence_rule && !options?.skip_recurrence) {
8991
9261
  spawnedTask = spawnNextRecurrence(task, d, timestamp);
@@ -9029,9 +9299,9 @@ function completeTask(id, agentId, db, options) {
9029
9299
  const depTask = getTask(dep.id, d);
9030
9300
  const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
9031
9301
  dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
9032
- emitLocalEventHooksQuiet({ type: "task.unblocked", payload });
9302
+ emitLocalEventHooksQuiet({ type: "task.unblocked", payload, databasePath });
9033
9303
  if (depTask)
9034
- emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id } });
9304
+ emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id }, databasePath });
9035
9305
  }
9036
9306
  }
9037
9307
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
@@ -9201,6 +9471,7 @@ function getTasksChangedSince(since, filters, db) {
9201
9471
  }
9202
9472
  function failTask(id, agentId, reason, options, db) {
9203
9473
  const d = db || getDatabase();
9474
+ const databasePath = databasePathFromDatabase(d);
9204
9475
  const task = getTask(id, d);
9205
9476
  if (!task)
9206
9477
  throw new TaskNotFoundError(id);
@@ -9229,8 +9500,8 @@ function failTask(id, agentId, reason, options, db) {
9229
9500
  logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
9230
9501
  const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
9231
9502
  dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
9232
- emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload });
9233
- emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning" });
9503
+ emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload, databasePath });
9504
+ emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning", databasePath });
9234
9505
  let retryTask;
9235
9506
  if (options?.retry) {
9236
9507
  const retryCount = (task.retry_count || 0) + 1;
@@ -9290,6 +9561,7 @@ function getStaleTasks(staleQuery = 30, filters, db) {
9290
9561
  }
9291
9562
  function stealTask(agentId, opts, db) {
9292
9563
  const d = db || getDatabase();
9564
+ const databasePath = databasePathFromDatabase(d);
9293
9565
  const staleMinutes = opts?.stale_minutes ?? 30;
9294
9566
  const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
9295
9567
  if (staleTasks.length === 0)
@@ -9308,8 +9580,8 @@ function stealTask(agentId, opts, db) {
9308
9580
  const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
9309
9581
  const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
9310
9582
  dispatchWebhook2("task.assigned", payload, d).catch(() => {});
9311
- emitLocalEventHooksQuiet({ type: "task.assigned", payload });
9312
- emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to } });
9583
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
9584
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to }, databasePath });
9313
9585
  return stolenTask;
9314
9586
  }
9315
9587
  function claimOrSteal(agentId, filters, db) {
@@ -9357,6 +9629,7 @@ var init_task_lifecycle = __esm(() => {
9357
9629
  init_types();
9358
9630
  init_database();
9359
9631
  init_completion_guard();
9632
+ init_event_emission_safety();
9360
9633
  init_event_hooks();
9361
9634
  init_shared_events();
9362
9635
  init_audit();
@@ -10064,8 +10337,9 @@ function createPlan(input, db) {
10064
10337
  const d = db || getDatabase();
10065
10338
  const id = uuid();
10066
10339
  const timestamp = now();
10067
- d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at)
10068
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
10340
+ const machineId = currentStorageMachineId(d);
10341
+ d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at, machine_id)
10342
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
10069
10343
  id,
10070
10344
  input.project_id || null,
10071
10345
  input.task_list_id || null,
@@ -10074,7 +10348,8 @@ function createPlan(input, db) {
10074
10348
  input.description || null,
10075
10349
  input.status || "active",
10076
10350
  timestamp,
10077
- timestamp
10351
+ timestamp,
10352
+ machineId
10078
10353
  ]);
10079
10354
  return getPlan(id, d);
10080
10355
  }
@@ -10122,19 +10397,30 @@ function updatePlan(id, input, db) {
10122
10397
  const updated = getPlan(id, d);
10123
10398
  emitLocalEventHooksQuiet({
10124
10399
  type: "plan.updated",
10125
- payload: { id, old_status: plan.status, new_status: updated.status, name: updated.name, project_id: updated.project_id }
10400
+ payload: { id, old_status: plan.status, new_status: updated.status, name: updated.name, project_id: updated.project_id },
10401
+ databasePath: databasePathFromDatabase(d)
10126
10402
  });
10127
10403
  return updated;
10128
10404
  }
10129
10405
  function deletePlan(id, db) {
10130
10406
  const d = db || getDatabase();
10407
+ const plan = getPlan(id, d);
10408
+ if (!plan)
10409
+ return false;
10410
+ recordStorageTombstone({
10411
+ object_type: "plans",
10412
+ object_id: id,
10413
+ payload: plan
10414
+ }, d);
10131
10415
  const result = d.run("DELETE FROM plans WHERE id = ?", [id]);
10132
10416
  return result.changes > 0;
10133
10417
  }
10134
10418
  var init_plans = __esm(() => {
10135
10419
  init_types();
10420
+ init_event_emission_safety();
10136
10421
  init_event_hooks();
10137
10422
  init_database();
10423
+ init_storage_tombstones();
10138
10424
  });
10139
10425
 
10140
10426
  // src/db/boards.ts
@@ -10541,20 +10827,20 @@ var init_boards = __esm(() => {
10541
10827
  // src/lib/artifact-store.ts
10542
10828
  import { createHash as createHash2 } from "crypto";
10543
10829
  import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
10544
- import { basename, dirname as dirname5, join as join6, resolve as resolve6 } from "path";
10545
- import { tmpdir } from "os";
10830
+ import { basename, dirname as dirname5, join as join6, resolve as resolve7 } from "path";
10831
+ import { tmpdir as tmpdir2 } from "os";
10546
10832
  function isInMemoryDb2(path) {
10547
10833
  return path === ":memory:" || path.startsWith("file::memory:");
10548
10834
  }
10549
10835
  function artifactStoreRoot() {
10550
10836
  if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
10551
- return resolve6(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
10837
+ return resolve7(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
10552
10838
  if (process.env["TODOS_ARTIFACTS_DIR"])
10553
- return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
10839
+ return resolve7(process.env["TODOS_ARTIFACTS_DIR"]);
10554
10840
  const dbPath = getDatabasePath();
10555
10841
  if (isInMemoryDb2(dbPath))
10556
- return join6(tmpdir(), "hasna-todos-artifacts");
10557
- return join6(dirname5(resolve6(dbPath)), "artifacts");
10842
+ return join6(tmpdir2(), "hasna-todos-artifacts");
10843
+ return join6(dirname5(resolve7(dbPath)), "artifacts");
10558
10844
  }
10559
10845
  function artifactStorePath(relativePath) {
10560
10846
  const normalized = relativePath.replace(/\\/g, "/");
@@ -10601,7 +10887,7 @@ function mediaTypeFor(path, textLike) {
10601
10887
  return "application/octet-stream";
10602
10888
  }
10603
10889
  function storeArtifactContent(input) {
10604
- const sourcePath = resolve6(input.path);
10890
+ const sourcePath = resolve7(input.path);
10605
10891
  if (!existsSync7(sourcePath))
10606
10892
  return null;
10607
10893
  const sourceStat = statSync2(sourcePath);
@@ -11318,7 +11604,11 @@ function startTaskRun(input, db) {
11318
11604
  }, d);
11319
11605
  }
11320
11606
  const run = getTaskRun(id, d);
11321
- emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
11607
+ emitLocalEventHooksQuiet({
11608
+ type: "run.started",
11609
+ payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title },
11610
+ databasePath: databasePathFromDatabase(d)
11611
+ });
11322
11612
  return run;
11323
11613
  }
11324
11614
  function beginTaskRunTransaction(input, db) {
@@ -11587,7 +11877,8 @@ function finishTaskRun(input, db) {
11587
11877
  const updated = getTaskRun(run.id, d);
11588
11878
  emitLocalEventHooksQuiet({
11589
11879
  type: `run.${input.status}`,
11590
- payload: { id: updated.id, task_id: updated.task_id, agent_id: updated.agent_id, status: updated.status, summary: updated.summary, completed_at: timestamp }
11880
+ payload: { id: updated.id, task_id: updated.task_id, agent_id: updated.agent_id, status: updated.status, summary: updated.summary, completed_at: timestamp },
11881
+ databasePath: databasePathFromDatabase(d)
11591
11882
  });
11592
11883
  return updated;
11593
11884
  }
@@ -11671,6 +11962,7 @@ function getTaskRunLedger(runId, db) {
11671
11962
  var LOOP_RUN_TRANSACTION_SCHEMA_VERSION = "todos.loop_run_transaction.v1";
11672
11963
  var init_task_runs = __esm(() => {
11673
11964
  init_artifact_store();
11965
+ init_event_emission_safety();
11674
11966
  init_event_hooks();
11675
11967
  init_redaction();
11676
11968
  init_types();
@@ -12168,7 +12460,7 @@ __export(exports_helpers, {
12168
12460
  });
12169
12461
  import chalk from "chalk";
12170
12462
  import { execSync } from "child_process";
12171
- import { resolve as resolve7 } from "path";
12463
+ import { resolve as resolve8 } from "path";
12172
12464
  function handleError(e) {
12173
12465
  console.error(chalk.red(e instanceof Error ? e.message : String(e)));
12174
12466
  process.exit(1);
@@ -12197,7 +12489,7 @@ function detectGitRoot() {
12197
12489
  }
12198
12490
  function resolveExplicitProject(input) {
12199
12491
  const db = getDatabase();
12200
- const byPath = getProjectByPath(resolve7(input), db);
12492
+ const byPath = getProjectByPath(resolve8(input), db);
12201
12493
  if (byPath)
12202
12494
  return byPath;
12203
12495
  const id = resolvePartialId(db, "projects", input);
@@ -12298,13 +12590,193 @@ var init_helpers = __esm(() => {
12298
12590
  };
12299
12591
  });
12300
12592
 
12593
+ // src/lib/task-routing.ts
12594
+ var exports_task_routing = {};
12595
+ __export(exports_task_routing, {
12596
+ setTaskWorkflowPointers: () => setTaskWorkflowPointers,
12597
+ getTaskRouteState: () => getTaskRouteState
12598
+ });
12599
+ function classifyProjectKind2(path) {
12600
+ if (!path)
12601
+ return null;
12602
+ return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
12603
+ }
12604
+ function machineLocalPath(project, db) {
12605
+ const machineId = process.env["TODOS_MACHINE_ID"];
12606
+ if (!machineId)
12607
+ return null;
12608
+ try {
12609
+ const row = db.query("SELECT path FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(project.id, machineId);
12610
+ return row?.path ?? null;
12611
+ } catch {
12612
+ return null;
12613
+ }
12614
+ }
12615
+ function resolveProject(task, db) {
12616
+ const project = task.project_id ? getProject(task.project_id, db) : null;
12617
+ const projectPath = project ? machineLocalPath(project, db) ?? project.path : task.working_dir;
12618
+ return { project, projectPath: projectPath ?? null };
12619
+ }
12620
+ function resolveTaskList(task, project, db) {
12621
+ if (task.task_list_id) {
12622
+ return getTaskList(task.task_list_id, db) ?? (project ? getTaskListBySlug(task.task_list_id, project.id, db) : null);
12623
+ }
12624
+ if (project?.task_list_id) {
12625
+ return getTaskListBySlug(project.task_list_id, project.id, db);
12626
+ }
12627
+ return null;
12628
+ }
12629
+ function isTerminal(status) {
12630
+ return status === "completed" || status === "cancelled" || status === "failed";
12631
+ }
12632
+ function routeConcurrencyKey(task, project, taskList, projectPath) {
12633
+ if (project?.id)
12634
+ return `project:${project.id}`;
12635
+ if (taskList?.id)
12636
+ return `task-list:${taskList.id}`;
12637
+ if (projectPath)
12638
+ return `path:${projectPath}`;
12639
+ return `task:${task.id}`;
12640
+ }
12641
+ function getTaskRouteState(taskOrId, db) {
12642
+ const d = db || getDatabase();
12643
+ const task = typeof taskOrId === "string" ? getTask(taskOrId, d) : taskOrId;
12644
+ if (!task)
12645
+ throw new Error(`Task not found: ${taskOrId}`);
12646
+ const { project, projectPath } = resolveProject(task, d);
12647
+ const taskList = resolveTaskList(task, project, d);
12648
+ const automation = routingAutomationMetadata(task, taskList) ?? {};
12649
+ const routeEnabled = routeEnabledForTask(task, taskList) === true;
12650
+ const tagOptIn = task.tags.includes("auto:route") || task.tags.includes("route:enabled");
12651
+ const locked = Boolean(task.locked_by && !isLockExpired(task.locked_at));
12652
+ const blockers = getBlockingDeps(task.id, d);
12653
+ const blocked = blockers.length > 0;
12654
+ const terminal = isTerminal(task.status);
12655
+ const requiresApproval = automation.requires_approval === true || task.requires_approval === true;
12656
+ const approvalRequired = automation.approval_required === true;
12657
+ const approved = Boolean(task.approved_by);
12658
+ const gates = {
12659
+ route_enabled: routeEnabled,
12660
+ tag_opt_in: tagOptIn,
12661
+ no_auto: automation.no_auto === true,
12662
+ manual: automation.manual === true,
12663
+ manual_required: automation.manual_required === true,
12664
+ requires_approval: requiresApproval,
12665
+ approval_required: approvalRequired,
12666
+ approved,
12667
+ locked,
12668
+ blocked,
12669
+ terminal
12670
+ };
12671
+ const reasons = [];
12672
+ if (task.status !== "pending")
12673
+ reasons.push("task_not_pending");
12674
+ if (terminal)
12675
+ reasons.push("task_terminal");
12676
+ if (!routeEnabled)
12677
+ reasons.push("route_not_enabled");
12678
+ if (locked)
12679
+ reasons.push("task_locked");
12680
+ if (blocked)
12681
+ reasons.push("task_blocked");
12682
+ if (gates.no_auto)
12683
+ reasons.push("no_auto");
12684
+ if (gates.manual)
12685
+ reasons.push("manual");
12686
+ if (gates.manual_required)
12687
+ reasons.push("manual_required");
12688
+ if (requiresApproval && !approved)
12689
+ reasons.push("requires_approval");
12690
+ if (approvalRequired && !approved)
12691
+ reasons.push("approval_required");
12692
+ if (automation.allowed === false)
12693
+ reasons.push("automation_disallowed");
12694
+ return {
12695
+ schema_version: TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION,
12696
+ task_id: task.id,
12697
+ task_short_id: task.short_id,
12698
+ status: task.status,
12699
+ eligible: reasons.length === 0,
12700
+ reasons,
12701
+ blockers: blockers.map((blocker) => ({
12702
+ id: blocker.id,
12703
+ short_id: blocker.short_id,
12704
+ title: blocker.title,
12705
+ status: blocker.status
12706
+ })),
12707
+ gates,
12708
+ automation: Object.keys(automation).length > 0 ? automation : null,
12709
+ route: {
12710
+ project_id: project?.id ?? task.project_id,
12711
+ project_path: projectPath,
12712
+ working_dir: task.working_dir ?? projectPath,
12713
+ project_kind: classifyProjectKind2(projectPath),
12714
+ task_list_id: taskList?.id ?? task.task_list_id,
12715
+ task_list_slug: taskList?.slug ?? null,
12716
+ task_list_name: taskList?.name ?? null,
12717
+ concurrency_key: routeConcurrencyKey(task, project, taskList, projectPath)
12718
+ },
12719
+ pointers: workflowPointersFromMetadata(task.metadata)
12720
+ };
12721
+ }
12722
+ function setTaskWorkflowPointers(taskId, input, db) {
12723
+ const d = db || getDatabase();
12724
+ const task = getTask(taskId, d);
12725
+ if (!task)
12726
+ throw new Error(`Task not found: ${taskId}`);
12727
+ const previous = workflowPointersFromMetadata(task.metadata);
12728
+ const next = compactWorkflowPointers({
12729
+ current_workflow_invocation_id: pointerPatch(previous.current_workflow_invocation_id, input, "current_workflow_invocation_id"),
12730
+ current_run_id: pointerPatch(previous.current_run_id, input, "current_run_id"),
12731
+ latest_manifest_path: pointerPatch(previous.latest_manifest_path, input, "latest_manifest_path"),
12732
+ latest_evaluation_path: pointerPatch(previous.latest_evaluation_path, input, "latest_evaluation_path"),
12733
+ workflow_state: pointerPatch(previous.workflow_state, input, "workflow_state")
12734
+ });
12735
+ const timestamp = now();
12736
+ const {
12737
+ current_workflow_invocation_id,
12738
+ current_run_id,
12739
+ latest_manifest_path,
12740
+ latest_evaluation_path,
12741
+ workflow_state,
12742
+ workflow_invocation,
12743
+ ...baseMetadata
12744
+ } = task.metadata;
12745
+ const metadata = {
12746
+ ...baseMetadata,
12747
+ ...next,
12748
+ workflow_invocation: {
12749
+ schema_version: TASK_WORKFLOW_POINTER_SCHEMA_VERSION,
12750
+ ...next,
12751
+ updated_at: timestamp,
12752
+ updated_by: input.actor ?? null
12753
+ }
12754
+ };
12755
+ return updateTask(task.id, { version: task.version, metadata }, d);
12756
+ }
12757
+ function pointerPatch(previous, input, key) {
12758
+ if (!Object.prototype.hasOwnProperty.call(input, key))
12759
+ return previous;
12760
+ const value = input[key];
12761
+ if (value === undefined)
12762
+ return previous;
12763
+ return typeof value === "string" && value.trim() ? value : undefined;
12764
+ }
12765
+ var init_task_routing = __esm(() => {
12766
+ init_database();
12767
+ init_projects();
12768
+ init_task_crud();
12769
+ init_task_lifecycle();
12770
+ init_task_lists();
12771
+ });
12772
+
12301
12773
  // src/cli/commands/task-commands.ts
12302
12774
  var exports_task_commands = {};
12303
12775
  __export(exports_task_commands, {
12304
12776
  registerTaskCommands: () => registerTaskCommands
12305
12777
  });
12306
12778
  import chalk2 from "chalk";
12307
- import { basename as basename3, resolve as resolve8 } from "path";
12779
+ import { basename as basename3, resolve as resolve9 } from "path";
12308
12780
  function resolveProjectIdOrSlug(input) {
12309
12781
  const db = getDatabase();
12310
12782
  const byId = getProject(input, db);
@@ -12314,7 +12786,7 @@ function resolveProjectIdOrSlug(input) {
12314
12786
  if (row)
12315
12787
  return row.id;
12316
12788
  if (isPathLike(input)) {
12317
- const projectPath = resolve8(input);
12789
+ const projectPath = resolve9(input);
12318
12790
  const byPath = getProjectByPath(projectPath, db);
12319
12791
  return (byPath ?? ensureProject(basename3(projectPath), projectPath, db)).id;
12320
12792
  }
@@ -12367,6 +12839,11 @@ function parseJsonValue(value) {
12367
12839
  return value;
12368
12840
  }
12369
12841
  }
12842
+ function pointerOption(value, clear) {
12843
+ if (value !== undefined)
12844
+ return value;
12845
+ return clear ? null : undefined;
12846
+ }
12370
12847
  function parseTags(value) {
12371
12848
  return value ? value.split(",").map((tag) => tag.trim()).filter(Boolean) : undefined;
12372
12849
  }
@@ -12469,7 +12946,7 @@ function registerTaskCommands(program2) {
12469
12946
  task_list_id: taskListId,
12470
12947
  tags: parseTags(opts.tags),
12471
12948
  metadata: buildExpectationMetadata(opts),
12472
- working_dir: opts.workingDir ? resolve8(opts.workingDir) : process.cwd(),
12949
+ working_dir: opts.workingDir ? resolve9(opts.workingDir) : process.cwd(),
12473
12950
  project_id: projectId,
12474
12951
  assigned_to: opts.assign,
12475
12952
  agent_id: globalOpts.agent,
@@ -12485,6 +12962,63 @@ function registerTaskCommands(program2) {
12485
12962
  console.log(formatTaskLine(result.task));
12486
12963
  }
12487
12964
  });
12965
+ task.command("route-state <id>").description("Show deterministic routing eligibility and workflow pointers for a task").action(async (id) => {
12966
+ const globalOpts = program2.opts();
12967
+ const resolvedId = resolveTaskId(id);
12968
+ const { getTaskRouteState: getTaskRouteState2 } = await Promise.resolve().then(() => (init_task_routing(), exports_task_routing));
12969
+ let state;
12970
+ try {
12971
+ state = getTaskRouteState2(resolvedId);
12972
+ } catch (e) {
12973
+ handleError(e);
12974
+ }
12975
+ if (globalOpts.json) {
12976
+ output(state, true);
12977
+ return;
12978
+ }
12979
+ console.log(chalk2.bold("Task route state"));
12980
+ console.log(` ${chalk2.dim("Task:")} ${state.task_short_id || state.task_id.slice(0, 8)}`);
12981
+ console.log(` ${chalk2.dim("Eligible:")} ${state.eligible ? chalk2.green("yes") : chalk2.yellow("no")}`);
12982
+ console.log(` ${chalk2.dim("Reasons:")} ${state.reasons.length > 0 ? state.reasons.join(", ") : "none"}`);
12983
+ console.log(` ${chalk2.dim("Route:")} ${state.route.concurrency_key}`);
12984
+ if (state.pointers.current_workflow_invocation_id) {
12985
+ console.log(` ${chalk2.dim("Invocation:")} ${state.pointers.current_workflow_invocation_id}`);
12986
+ }
12987
+ if (state.pointers.current_run_id) {
12988
+ console.log(` ${chalk2.dim("Run:")} ${state.pointers.current_run_id}`);
12989
+ }
12990
+ if (state.pointers.latest_manifest_path) {
12991
+ console.log(` ${chalk2.dim("Manifest:")} ${state.pointers.latest_manifest_path}`);
12992
+ }
12993
+ });
12994
+ task.command("workflow-pointers <id>").description("Update OpenLoops workflow invocation/run artifact pointers on a task").option("--invocation <id>", "Current workflow invocation ID").option("--run <id>", "Current workflow run ID").option("--manifest <path>", "Latest run manifest path").option("--evaluation <path>", "Latest evaluator artifact path").option("--state <state>", "Human-visible workflow state").option("--actor <agent>", "Agent or workflow updating the pointers").option("--clear", "Clear all workflow pointers before applying explicit pointer values").option("--clear-invocation", "Clear current workflow invocation ID").option("--clear-run", "Clear current workflow run ID").option("--clear-manifest", "Clear latest run manifest path").option("--clear-evaluation", "Clear latest evaluator artifact path").option("--clear-state", "Clear human-visible workflow state").action(async (id, opts) => {
12995
+ const globalOpts = program2.opts();
12996
+ const resolvedId = resolveTaskId(id);
12997
+ const { getTaskRouteState: getTaskRouteState2, setTaskWorkflowPointers: setTaskWorkflowPointers2 } = await Promise.resolve().then(() => (init_task_routing(), exports_task_routing));
12998
+ let taskResult;
12999
+ try {
13000
+ taskResult = setTaskWorkflowPointers2(resolvedId, {
13001
+ current_workflow_invocation_id: pointerOption(opts.invocation, Boolean(opts.clear || opts.clearInvocation)),
13002
+ current_run_id: pointerOption(opts.run, Boolean(opts.clear || opts.clearRun)),
13003
+ latest_manifest_path: pointerOption(opts.manifest, Boolean(opts.clear || opts.clearManifest)),
13004
+ latest_evaluation_path: pointerOption(opts.evaluation, Boolean(opts.clear || opts.clearEvaluation)),
13005
+ workflow_state: pointerOption(opts.state, Boolean(opts.clear || opts.clearState)),
13006
+ actor: opts.actor || globalOpts.agent || "cli"
13007
+ });
13008
+ } catch (e) {
13009
+ handleError(e);
13010
+ }
13011
+ const state = getTaskRouteState2(taskResult.id);
13012
+ if (globalOpts.json) {
13013
+ output({ task: taskResult, route_state: state }, true);
13014
+ return;
13015
+ }
13016
+ console.log(chalk2.green("Workflow pointers updated:"));
13017
+ console.log(formatTaskLine(taskResult));
13018
+ if (state.pointers.latest_manifest_path) {
13019
+ console.log(` ${chalk2.dim("Manifest:")} ${state.pointers.latest_manifest_path}`);
13020
+ }
13021
+ });
12488
13022
  program2.command("list").description("List tasks").option("-s, --status <status>", "Filter by status").option("-p, --priority <priority>", "Filter by priority").option("--assigned <agent>", "Filter by assigned agent").option("--tags <tags>", "Filter by tags (comma-separated)").option("--tag <tags>", "Filter by tags (alias for --tags)").option("-a, --all", "Show all tasks (including completed/cancelled)").option("--list <id>", "Filter by task list ID").option("--task-list <id>", "Filter by task list ID (alias for --list)").option("--project-name <name>", "Filter by project name").option("--agent-name <name>", "Filter by agent name/assigned").option("--sort <field>", "Sort by: updated, created, priority, status").option("--format <fmt>", "Output format: table (default), compact, csv, json").option("--due-today", "Only tasks due today or earlier").option("--overdue", "Only overdue tasks (past due_at)").option("--recurring", "Only recurring tasks").option("--limit <n>", "Max tasks to return").action((opts) => {
12489
13023
  const globalOpts = program2.opts();
12490
13024
  opts.tags = opts.tags || opts.tag;
@@ -14983,7 +15517,7 @@ __export(exports_project_bootstrap, {
14983
15517
  bootstrapProject: () => bootstrapProject
14984
15518
  });
14985
15519
  import { existsSync as existsSync10, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
14986
- import { basename as basename4, dirname as dirname6, resolve as resolve9 } from "path";
15520
+ import { basename as basename4, dirname as dirname6, resolve as resolve10 } from "path";
14987
15521
  function safeStat(path) {
14988
15522
  try {
14989
15523
  return statSync3(path);
@@ -14992,7 +15526,7 @@ function safeStat(path) {
14992
15526
  }
14993
15527
  }
14994
15528
  function canonicalPath(input) {
14995
- const resolved = resolve9(input);
15529
+ const resolved = resolve10(input);
14996
15530
  const stats = safeStat(resolved);
14997
15531
  if (stats?.isFile())
14998
15532
  return dirname6(resolved);
@@ -15001,7 +15535,7 @@ function canonicalPath(input) {
15001
15535
  function findUp(start, marker) {
15002
15536
  let current = canonicalPath(start);
15003
15537
  while (true) {
15004
- if (existsSync10(resolve9(current, marker)))
15538
+ if (existsSync10(resolve10(current, marker)))
15005
15539
  return current;
15006
15540
  const parent = dirname6(current);
15007
15541
  if (parent === current)
@@ -15012,7 +15546,7 @@ function findUp(start, marker) {
15012
15546
  function readPackageJson(path) {
15013
15547
  if (!path)
15014
15548
  return null;
15015
- const file = resolve9(path, "package.json");
15549
+ const file = resolve10(path, "package.json");
15016
15550
  if (!existsSync10(file))
15017
15551
  return null;
15018
15552
  try {
@@ -15035,7 +15569,7 @@ function workspaceMarker(root, rootPackage) {
15035
15569
  if (rootPackage?.workspaces)
15036
15570
  markers.push("package.json#workspaces");
15037
15571
  for (const marker of ["pnpm-workspace.yaml", "turbo.json", "nx.json", "lerna.json", "rush.json", "bun.lock", "bun.lockb"]) {
15038
- if (existsSync10(resolve9(root, marker)))
15572
+ if (existsSync10(resolve10(root, marker)))
15039
15573
  markers.push(marker);
15040
15574
  }
15041
15575
  const kind = markers.find((marker) => marker !== "bun.lock" && marker !== "bun.lockb") ?? null;
@@ -15097,7 +15631,19 @@ function bootstrapProject(options = {}, db) {
15097
15631
  }
15098
15632
  setMachineLocalPath(project.id, discovery.projectPath, d);
15099
15633
  const beforeTaskList = d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ?").get(project.id, taskListSlug);
15100
- const taskList = ensureTaskList(`${project.name} Tasks`, taskListSlug, project.id, d);
15634
+ let taskList = ensureTaskList(`${project.name} Tasks`, taskListSlug, project.id, d);
15635
+ if (options.routeEnabled && taskList.metadata.route_enabled !== true) {
15636
+ taskList = updateTaskList(taskList.id, {
15637
+ metadata: {
15638
+ ...taskList.metadata,
15639
+ route_enabled: true,
15640
+ automation: {
15641
+ ...taskList.metadata.automation && typeof taskList.metadata.automation === "object" && !Array.isArray(taskList.metadata.automation) ? taskList.metadata.automation : {},
15642
+ no_auto: false
15643
+ }
15644
+ }
15645
+ }, d);
15646
+ }
15101
15647
  const createdSources = [];
15102
15648
  for (const source of [
15103
15649
  addSourceOnce(project.id, "local", "Project root", discovery.projectPath, { role: "project-root" }, d),
@@ -15159,7 +15705,7 @@ __export(exports_extract, {
15159
15705
  });
15160
15706
  import { existsSync as existsSync11, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
15161
15707
  import { createHash as createHash3 } from "crypto";
15162
- import { relative as relative3, resolve as resolve10, join as join10 } from "path";
15708
+ import { relative as relative3, resolve as resolve11, join as join10 } from "path";
15163
15709
  function stableHash(value) {
15164
15710
  return createHash3("sha256").update(value).digest("hex");
15165
15711
  }
@@ -15167,7 +15713,7 @@ function normalizePathForMatch(value) {
15167
15713
  return value.replace(/\\/g, "/").replace(/^\.\//, "");
15168
15714
  }
15169
15715
  function readGitignorePatterns(basePath) {
15170
- const root = statSync4(basePath).isFile() ? resolve10(basePath, "..") : basePath;
15716
+ const root = statSync4(basePath).isFile() ? resolve11(basePath, "..") : basePath;
15171
15717
  const gitignorePath = join10(root, ".gitignore");
15172
15718
  if (!existsSync11(gitignorePath))
15173
15719
  return [];
@@ -15303,7 +15849,7 @@ function collectFiles(basePath, extensions, excludes, respectGitignore) {
15303
15849
  return files.sort();
15304
15850
  }
15305
15851
  function buildCodebaseIndex(options) {
15306
- const basePath = resolve10(options.path);
15852
+ const basePath = resolve11(options.path);
15307
15853
  const tags = options.patterns || [...EXTRACT_TAGS];
15308
15854
  const extensions = options.extensions ? new Set(options.extensions.map((e) => e.startsWith(".") ? e : `.${e}`)) : DEFAULT_EXTENSIONS;
15309
15855
  const excludes = options.exclude || [];
@@ -15314,7 +15860,7 @@ function buildCodebaseIndex(options) {
15314
15860
  const fullPath = statSync4(basePath).isFile() ? basePath : join10(basePath, file);
15315
15861
  try {
15316
15862
  const source = readFileSync6(fullPath, "utf-8");
15317
- const relPath = statSync4(basePath).isFile() ? relative3(resolve10(basePath, ".."), fullPath) : file;
15863
+ const relPath = statSync4(basePath).isFile() ? relative3(resolve11(basePath, ".."), fullPath) : file;
15318
15864
  indexed.push({
15319
15865
  file: relPath,
15320
15866
  checksum: stableHash(source).slice(0, 24),
@@ -15334,7 +15880,7 @@ function buildCodebaseIndex(options) {
15334
15880
  };
15335
15881
  }
15336
15882
  function extractTodos(options, db) {
15337
- const basePath = resolve10(options.path);
15883
+ const basePath = resolve11(options.path);
15338
15884
  const tags = options.patterns || [...EXTRACT_TAGS];
15339
15885
  const extensions = options.extensions ? new Set(options.extensions.map((e) => e.startsWith(".") ? e : `.${e}`)) : DEFAULT_EXTENSIONS;
15340
15886
  const excludes = options.exclude || [];
@@ -15345,7 +15891,7 @@ function extractTodos(options, db) {
15345
15891
  const fullPath = statSync4(basePath).isFile() ? basePath : join10(basePath, file);
15346
15892
  try {
15347
15893
  const source = readFileSync6(fullPath, "utf-8");
15348
- const relPath = statSync4(basePath).isFile() ? relative3(resolve10(basePath, ".."), fullPath) : file;
15894
+ const relPath = statSync4(basePath).isFile() ? relative3(resolve11(basePath, ".."), fullPath) : file;
15349
15895
  const comments = extractFromSource(source, relPath, tags);
15350
15896
  allComments.push(...comments);
15351
15897
  } catch {}
@@ -15439,7 +15985,7 @@ async function watchSourceTodos(options, onRun) {
15439
15985
  const interval = Math.max(100, options.interval_ms || 2000);
15440
15986
  const once = options.once !== false && (!options.max_runs || options.max_runs <= 1);
15441
15987
  const maxRuns = options.max_runs ?? (once ? 1 : Number.POSITIVE_INFINITY);
15442
- const root = resolve10(options.path);
15988
+ const root = resolve11(options.path);
15443
15989
  const runs = [];
15444
15990
  let previous = new Map;
15445
15991
  for (let runNumber = 1;runNumber <= maxRuns; runNumber++) {
@@ -16610,7 +17156,7 @@ __export(exports_project_commands, {
16610
17156
  registerProjectCommands: () => registerProjectCommands
16611
17157
  });
16612
17158
  import chalk4 from "chalk";
16613
- import { basename as basename5, resolve as resolve11 } from "path";
17159
+ import { basename as basename5, resolve as resolve12 } from "path";
16614
17160
  function collectOption(value, previous = []) {
16615
17161
  return [...previous, value];
16616
17162
  }
@@ -16703,7 +17249,7 @@ function buildSearchFilters(query, opts, projectId) {
16703
17249
  return Object.fromEntries(Object.entries(filters).filter(([, value]) => value !== undefined));
16704
17250
  }
16705
17251
  function registerProjectCommands(program2) {
16706
- program2.command("project-bootstrap [path]").description("Discover a local workspace and initialize project task state").option("--name <name>", "Project display name").option("--task-list <slug>", "Default task list slug").option("--dry-run", "Show discovery without writing local state").action(async (inputPath, opts) => {
17252
+ program2.command("project-bootstrap [path]").description("Discover a local workspace and initialize project task state").option("--name <name>", "Project display name").option("--task-list <slug>", "Default task list slug").option("--route-enabled", "Mark the default task list as eligible for OpenLoops task-created routing").option("--dry-run", "Show discovery without writing local state").action(async (inputPath, opts) => {
16707
17253
  const globalOpts = program2.opts();
16708
17254
  try {
16709
17255
  const { bootstrapProject: bootstrapProject2 } = await Promise.resolve().then(() => (init_project_bootstrap(), exports_project_bootstrap));
@@ -16711,6 +17257,7 @@ function registerProjectCommands(program2) {
16711
17257
  path: inputPath || globalOpts.project || process.cwd(),
16712
17258
  name: opts.name,
16713
17259
  taskListSlug: opts.taskList,
17260
+ routeEnabled: Boolean(opts.routeEnabled),
16714
17261
  dryRun: opts.dryRun
16715
17262
  });
16716
17263
  if (globalOpts.json) {
@@ -16732,6 +17279,8 @@ function registerProjectCommands(program2) {
16732
17279
  console.log(` ${chalk4.dim("Project:")} ${result.project.id.slice(0, 8)} ${result.created.project ? "(created)" : "(existing)"}`);
16733
17280
  if (result.taskList)
16734
17281
  console.log(` ${chalk4.dim("Task list:")} ${result.taskList.slug} ${result.created.taskList ? "(created)" : "(existing)"}`);
17282
+ if (result.taskList?.metadata.route_enabled === true)
17283
+ console.log(` ${chalk4.dim("Routing:")} route-enabled`);
16735
17284
  if (result.created.sources.length > 0) {
16736
17285
  console.log(` ${chalk4.dim("Sources:")} ${result.created.sources.join(", ")}`);
16737
17286
  }
@@ -16949,7 +17498,7 @@ function registerProjectCommands(program2) {
16949
17498
  program2.command("projects").description("List and manage projects").option("--add <path>", "Register a project by path").option("--name <name>", "Project name (with --add)").option("--task-list-id <id>", "Custom task list ID (with --add)").action(async (opts) => {
16950
17499
  const globalOpts = program2.opts();
16951
17500
  if (opts.add) {
16952
- const projectPath = resolve11(opts.add);
17501
+ const projectPath = resolve12(opts.add);
16953
17502
  const name = opts.name || basename5(projectPath);
16954
17503
  const existing = getProjectByPath(projectPath);
16955
17504
  let project;
@@ -17033,7 +17582,7 @@ function registerProjectCommands(program2) {
17033
17582
  console.error(chalk4.red(`Project not found: ${projectId}`));
17034
17583
  process.exit(1);
17035
17584
  }
17036
- const entry = setMachineLocalPath2(resolved, resolve11(projectPath));
17585
+ const entry = setMachineLocalPath2(resolved, resolve12(projectPath));
17037
17586
  if (useJson) {
17038
17587
  output(entry, true);
17039
17588
  } else {
@@ -17100,7 +17649,7 @@ function registerProjectCommands(program2) {
17100
17649
  const patterns = opts.pattern ? opts.pattern.split(",").map((t) => t.trim().toUpperCase()) : undefined;
17101
17650
  const taskListId = opts.list ? resolveTaskListId(opts.list) : undefined;
17102
17651
  const result = extractTodos2({
17103
- path: resolve11(scanPath),
17652
+ path: resolve12(scanPath),
17104
17653
  patterns,
17105
17654
  project_id: projectId,
17106
17655
  task_list_id: taskListId,
@@ -17161,7 +17710,7 @@ Indexed ${result.index.files.length} file(s), ${result.index.total_symbols} symb
17161
17710
  const taskListId = opts.list ? resolveTaskListId(opts.list) : undefined;
17162
17711
  const maxRuns = opts.maxRuns ? parseInt(opts.maxRuns, 10) : 1;
17163
17712
  const result = await watchSourceTodos2({
17164
- path: resolve11(scanPath),
17713
+ path: resolve12(scanPath),
17165
17714
  patterns,
17166
17715
  project_id: projectId,
17167
17716
  task_list_id: taskListId,
@@ -17198,7 +17747,7 @@ Indexed ${result.index.files.length} file(s), ${result.index.total_symbols} symb
17198
17747
  const writeOutput = async (content) => {
17199
17748
  if (opts.output) {
17200
17749
  const { writeFileSync: writeFileSync5 } = await import("fs");
17201
- writeFileSync5(resolve11(opts.output), content.endsWith(`
17750
+ writeFileSync5(resolve12(opts.output), content.endsWith(`
17202
17751
  `) ? content : `${content}
17203
17752
  `);
17204
17753
  } else {
@@ -17213,12 +17762,12 @@ Indexed ${result.index.files.length} file(s), ${result.index.total_symbols} symb
17213
17762
  const exported = opts.encrypt ? createEncryptedBridgeBundle2(bundle, { profile: opts.encryptionProfile }) : bundle;
17214
17763
  const json = JSON.stringify(exported, null, 2);
17215
17764
  await writeOutput(json);
17216
- emitLocalEventHooksQuiet3({ type: "export.finished", payload: { format: "bridge", encrypted: Boolean(opts.encrypt), project_id: projectId, output: opts.output ? resolve11(opts.output) : null, stats: bundle.stats } });
17765
+ emitLocalEventHooksQuiet3({ type: "export.finished", payload: { format: "bridge", encrypted: Boolean(opts.encrypt), project_id: projectId, output: opts.output ? resolve12(opts.output) : null, stats: bundle.stats } });
17217
17766
  if (!opts.encrypt && !opts.allowPlaintextSensitive) {
17218
17767
  console.error(chalk4.yellow("Warning: bridge exports are plaintext JSON. Use --encrypt for sensitive metadata, evidence, and artifact bundles."));
17219
17768
  }
17220
17769
  if (opts.output && !globalOpts.json) {
17221
- console.log(chalk4.green(`${opts.encrypt ? "Encrypted bridge export" : "Bridge export"} written to ${resolve11(opts.output)}`));
17770
+ console.log(chalk4.green(`${opts.encrypt ? "Encrypted bridge export" : "Bridge export"} written to ${resolve12(opts.output)}`));
17222
17771
  }
17223
17772
  return;
17224
17773
  }
@@ -17231,7 +17780,7 @@ Indexed ${result.index.files.length} file(s), ${result.index.total_symbols} symb
17231
17780
  await writeOutput(JSON.stringify(tasks, null, 2));
17232
17781
  }
17233
17782
  const { emitLocalEventHooksQuiet: emitLocalEventHooksQuiet2 } = await Promise.resolve().then(() => (init_event_hooks(), exports_event_hooks));
17234
- emitLocalEventHooksQuiet2({ type: "export.finished", payload: { format: opts.format, project_id: projectId, output: opts.output ? resolve11(opts.output) : null, count: exportedCount } });
17783
+ emitLocalEventHooksQuiet2({ type: "export.finished", payload: { format: opts.format, project_id: projectId, output: opts.output ? resolve12(opts.output) : null, count: exportedCount } });
17235
17784
  });
17236
17785
  program2.command("bridge-import <file>").description("Dry-run or apply a local hasna/todos bridge export bundle").option("--apply", "Apply the import. Defaults to dry-run.").option("--decrypt", "Decrypt an encrypted bridge export before importing").option("--resolve-conflicts", "Safely merge existing local tasks by filling blank fields, unioning tags, and recording unresolved divergences").action(async (file, opts) => {
17237
17786
  const globalOpts = program2.opts();
@@ -17239,13 +17788,13 @@ Indexed ${result.index.files.length} file(s), ${result.index.total_symbols} symb
17239
17788
  const { readFileSync: readFileSync7 } = await import("fs");
17240
17789
  const { importLocalBridgeBundle: importLocalBridgeBundle2 } = await Promise.resolve().then(() => (init_local_bridge(), exports_local_bridge));
17241
17790
  const { decryptBridgeBundle: decryptBridgeBundle2, isEncryptedBridgeBundle: isEncryptedBridgeBundle2 } = await Promise.resolve().then(() => (init_local_encryption(), exports_local_encryption));
17242
- const parsed = JSON.parse(readFileSync7(resolve11(file), "utf-8"));
17791
+ const parsed = JSON.parse(readFileSync7(resolve12(file), "utf-8"));
17243
17792
  const bundle = isEncryptedBridgeBundle2(parsed) ? opts.decrypt ? decryptBridgeBundle2(parsed) : (() => {
17244
17793
  throw new Error("Bridge bundle is encrypted. Re-run with --decrypt and the configured key environment variable set.");
17245
17794
  })() : parsed;
17246
17795
  const result = importLocalBridgeBundle2(bundle, { dryRun: !opts.apply, conflictStrategy: opts.resolveConflicts ? "safe_merge" : "skip" });
17247
17796
  const { emitLocalEventHooksQuiet: emitLocalEventHooksQuiet2 } = await Promise.resolve().then(() => (init_event_hooks(), exports_event_hooks));
17248
- emitLocalEventHooksQuiet2({ type: "import.finished", payload: { file: resolve11(file), dry_run: result.dry_run, ok: result.ok, inserted: result.inserted, skipped: result.skipped, conflicts: result.conflicts.length, issues: result.issues.length } });
17797
+ emitLocalEventHooksQuiet2({ type: "import.finished", payload: { file: resolve12(file), dry_run: result.dry_run, ok: result.ok, inserted: result.inserted, skipped: result.skipped, conflicts: result.conflicts.length, issues: result.issues.length } });
17249
17798
  if (globalOpts.json) {
17250
17799
  output(result, true);
17251
17800
  return;
@@ -17275,9 +17824,9 @@ Indexed ${result.index.files.length} file(s), ${result.index.total_symbols} symb
17275
17824
  try {
17276
17825
  const { readFileSync: readFileSync7 } = await import("fs");
17277
17826
  const { importTodosMarkdown: importTodosMarkdown2 } = await Promise.resolve().then(() => (init_todos_md(), exports_todos_md));
17278
- const result = importTodosMarkdown2(readFileSync7(resolve11(file), "utf-8"), { dryRun: !opts.apply, conflictStrategy: opts.resolveConflicts ? "safe_merge" : "skip" });
17827
+ const result = importTodosMarkdown2(readFileSync7(resolve12(file), "utf-8"), { dryRun: !opts.apply, conflictStrategy: opts.resolveConflicts ? "safe_merge" : "skip" });
17279
17828
  const { emitLocalEventHooksQuiet: emitLocalEventHooksQuiet2 } = await Promise.resolve().then(() => (init_event_hooks(), exports_event_hooks));
17280
- emitLocalEventHooksQuiet2({ type: "import.finished", payload: { file: resolve11(file), format: "todos.md", dry_run: result.dry_run, ok: result.ok, inserted: result.inserted, skipped: result.skipped, issues: result.issues.length } });
17829
+ emitLocalEventHooksQuiet2({ type: "import.finished", payload: { file: resolve12(file), format: "todos.md", dry_run: result.dry_run, ok: result.ok, inserted: result.inserted, skipped: result.skipped, issues: result.issues.length } });
17281
17830
  if (globalOpts.json) {
17282
17831
  output(result, true);
17283
17832
  return;
@@ -17772,6 +18321,7 @@ function rowToAgent(row) {
17772
18321
  }
17773
18322
  function registerAgent(input, db) {
17774
18323
  const d = db || getDatabase();
18324
+ const machineId = currentStorageMachineId(d);
17775
18325
  const existingNames = d.query("SELECT name FROM agents").all().map((row) => row.name);
17776
18326
  const normalizedName = validateAgentName(input.name, existingNames);
17777
18327
  const existing = getAgentByName(normalizedName, d);
@@ -17812,14 +18362,18 @@ function registerAgent(input, db) {
17812
18362
  updates.push("active_project_id = ?");
17813
18363
  params.push(input.project_id);
17814
18364
  }
18365
+ if (!existing.machine_id && machineId) {
18366
+ updates.push("machine_id = ?");
18367
+ params.push(machineId);
18368
+ }
17815
18369
  params.push(existing.id);
17816
18370
  d.run(`UPDATE agents SET ${updates.join(", ")} WHERE id = ?`, params);
17817
18371
  return getAgent(existing.id, d);
17818
18372
  }
17819
18373
  const id = shortUuid();
17820
18374
  const timestamp = now();
17821
- 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)
17822
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
18375
+ 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)
18376
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
17823
18377
  id,
17824
18378
  normalizedName,
17825
18379
  input.description || null,
@@ -17835,7 +18389,8 @@ function registerAgent(input, db) {
17835
18389
  timestamp,
17836
18390
  input.session_id || null,
17837
18391
  input.working_dir || null,
17838
- input.project_id && input.session_id ? input.project_id : null
18392
+ input.project_id && input.session_id ? input.project_id : null,
18393
+ machineId
17839
18394
  ]);
17840
18395
  return getAgent(id, d);
17841
18396
  }
@@ -18018,6 +18573,7 @@ function getCapableAgents(capabilities, opts, db) {
18018
18573
  }
18019
18574
  var init_agents = __esm(() => {
18020
18575
  init_database();
18576
+ init_storage_tombstones();
18021
18577
  init_agent_names();
18022
18578
  });
18023
18579
 
@@ -19280,7 +19836,7 @@ __export(exports_local_extensions, {
19280
19836
  });
19281
19837
  import { createHash as createHash5, createVerify } from "crypto";
19282
19838
  import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
19283
- import { basename as basename6, join as join11, resolve as resolve12 } from "path";
19839
+ import { basename as basename6, join as join11, resolve as resolve13 } from "path";
19284
19840
  function isObject(value) {
19285
19841
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
19286
19842
  }
@@ -19538,7 +20094,7 @@ function verifyExtensionSignature(input) {
19538
20094
  return verifier.verify(input.public_key, decodeSignature(input.signature));
19539
20095
  }
19540
20096
  function inspectExtensionSource(source2) {
19541
- const resolved = resolve12(source2);
20097
+ const resolved = resolve13(source2);
19542
20098
  if (!existsSync13(resolved))
19543
20099
  throw new Error(`extension source not found: ${source2}`);
19544
20100
  const stat = statSync5(resolved);
@@ -19636,7 +20192,7 @@ function testExtensionCompatibility(sourceOrManifest) {
19636
20192
  function projectExtensionSources(projectPath) {
19637
20193
  if (!projectPath)
19638
20194
  return [];
19639
- const root = resolve12(projectPath);
20195
+ const root = resolve13(projectPath);
19640
20196
  const candidates = [
19641
20197
  join11(root, "todos.extension.json"),
19642
20198
  join11(root, ".todos", "todos.extension.json")
@@ -19655,7 +20211,7 @@ function projectExtensionSources(projectPath) {
19655
20211
  }
19656
20212
  function discoverLocalExtensions(options = {}) {
19657
20213
  const config = loadConfig();
19658
- const projectPath = options.project_path ? resolve12(options.project_path) : null;
20214
+ const projectPath = options.project_path ? resolve13(options.project_path) : null;
19659
20215
  const configuredSources = [
19660
20216
  ...config.extension_sources || [],
19661
20217
  ...projectPath ? config.project_overrides?.[projectPath]?.extension_sources || [] : []
@@ -19663,7 +20219,7 @@ function discoverLocalExtensions(options = {}) {
19663
20219
  const sources = Array.from(new Set([
19664
20220
  ...configuredSources,
19665
20221
  ...projectExtensionSources(projectPath || undefined)
19666
- ])).map((source2) => projectPath && !source2.startsWith("/") ? resolve12(projectPath, source2) : resolve12(source2));
20222
+ ])).map((source2) => projectPath && !source2.startsWith("/") ? resolve13(projectPath, source2) : resolve13(source2));
19667
20223
  const warnings = [];
19668
20224
  const discovered = [];
19669
20225
  for (const source2 of sources) {
@@ -19923,9 +20479,9 @@ __export(exports_policy_packs, {
19923
20479
  getPolicyPack: () => getPolicyPack,
19924
20480
  explainPolicyPack: () => explainPolicyPack
19925
20481
  });
19926
- import { relative as relative4, resolve as resolve13 } from "path";
20482
+ import { relative as relative4, resolve as resolve14 } from "path";
19927
20483
  function normalizePath3(path) {
19928
- return resolve13(path);
20484
+ return resolve14(path);
19929
20485
  }
19930
20486
  function unique5(values) {
19931
20487
  return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
@@ -19980,7 +20536,7 @@ function commandMatches(commands, pattern) {
19980
20536
  }
19981
20537
  function pathMatches(paths, pattern, root) {
19982
20538
  return paths.filter((path) => {
19983
- const candidate = path.startsWith("/") ? path : resolve13(root, path);
20539
+ const candidate = path.startsWith("/") ? path : resolve14(root, path);
19984
20540
  if (!isPathInside3(root, candidate))
19985
20541
  return matchesPattern3(path, pattern);
19986
20542
  return matchesPattern3(path, pattern) || matchesPattern3(relative4(root, candidate), pattern);
@@ -20337,7 +20893,8 @@ function logApprovalEvent(taskId, action, gate, agentId, db) {
20337
20893
  if (action === "approved" || action === "rejected" || action === "expired") {
20338
20894
  emitLocalEventHooksQuiet({
20339
20895
  type: "approval.decided",
20340
- payload: { task_id: taskId, gate: gate.gate, status: gate.status, reviewer: gate.reviewer, run_id: gate.run_id }
20896
+ payload: { task_id: taskId, gate: gate.gate, status: gate.status, reviewer: gate.reviewer, run_id: gate.run_id },
20897
+ databasePath: databasePathFromDatabase(db)
20341
20898
  });
20342
20899
  }
20343
20900
  }
@@ -20483,6 +21040,7 @@ var init_approval_gates = __esm(() => {
20483
21040
  init_task_runs();
20484
21041
  init_tasks();
20485
21042
  init_types();
21043
+ init_event_emission_safety();
20486
21044
  init_event_hooks();
20487
21045
  });
20488
21046
 
@@ -21334,7 +21892,7 @@ var init_doctor = __esm(() => {
21334
21892
  });
21335
21893
 
21336
21894
  // src/server/routes.ts
21337
- import { join as join13, resolve as resolve14, sep } from "path";
21895
+ import { join as join13, resolve as resolve15, sep as sep2 } from "path";
21338
21896
  function parseFieldsParam(url) {
21339
21897
  const fieldsParam = url.searchParams.get("fields");
21340
21898
  return fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
@@ -22072,9 +22630,9 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
22072
22630
  return null;
22073
22631
  if (path !== "/") {
22074
22632
  const filePath = join13(ctx.dashboardDir, path);
22075
- const resolvedFile = resolve14(filePath);
22076
- const resolvedBase = resolve14(ctx.dashboardDir);
22077
- if (!resolvedFile.startsWith(resolvedBase + sep) && resolvedFile !== resolvedBase) {
22633
+ const resolvedFile = resolve15(filePath);
22634
+ const resolvedBase = resolve15(ctx.dashboardDir);
22635
+ if (!resolvedFile.startsWith(resolvedBase + sep2) && resolvedFile !== resolvedBase) {
22078
22636
  return json2({ error: "Forbidden" }, 403);
22079
22637
  }
22080
22638
  const res2 = serveStaticFile2(filePath);
@@ -27077,7 +27635,7 @@ __export(exports_mention_resolver, {
27077
27635
  resolveMentions: () => resolveMentions
27078
27636
  });
27079
27637
  import { existsSync as existsSync15, readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync7 } from "fs";
27080
- import { basename as basename8, isAbsolute, join as join14, relative as relative5, resolve as resolve15, sep as sep2 } from "path";
27638
+ import { basename as basename8, isAbsolute, join as join14, relative as relative5, resolve as resolve16, sep as sep3 } from "path";
27081
27639
  function blankResolution(parsed) {
27082
27640
  return {
27083
27641
  input: parsed.input,
@@ -27100,11 +27658,11 @@ function backlink(kind, key, label, target = key) {
27100
27658
  return { kind, key, label, target };
27101
27659
  }
27102
27660
  function normalizeWorkspace(workspace) {
27103
- return resolve15(workspace || process.cwd());
27661
+ return resolve16(workspace || process.cwd());
27104
27662
  }
27105
27663
  function isInside(root, absolutePath) {
27106
27664
  const rel = relative5(root, absolutePath);
27107
- return rel === "" || !rel.startsWith("..") && !rel.includes(`..${sep2}`) && !isAbsolute(rel);
27665
+ return rel === "" || !rel.startsWith("..") && !rel.includes(`..${sep3}`) && !isAbsolute(rel);
27108
27666
  }
27109
27667
  function normalizeRelativePath(value) {
27110
27668
  const normalized = value.trim().replace(/^\.?\//, "").replace(/\\/g, "/");
@@ -27168,7 +27726,7 @@ function resolveFile(parsed, workspace) {
27168
27726
  resolution.warnings.push("path is empty or escapes the workspace");
27169
27727
  return resolution;
27170
27728
  }
27171
- const absolutePath = resolve15(workspace, relPath);
27729
+ const absolutePath = resolve16(workspace, relPath);
27172
27730
  if (!isInside(workspace, absolutePath)) {
27173
27731
  resolution.path = relPath;
27174
27732
  resolution.warnings.push("path escapes the workspace");
@@ -29695,7 +30253,7 @@ function canonicalize(value) {
29695
30253
  function hash(value) {
29696
30254
  return createHash7("sha256").update(value).digest("hex");
29697
30255
  }
29698
- function parsePayload(value) {
30256
+ function parsePayload2(value) {
29699
30257
  if (!value)
29700
30258
  return {};
29701
30259
  try {
@@ -29794,7 +30352,7 @@ function taskScopedRows(db, scope) {
29794
30352
  FROM handoffs h
29795
30353
  WHERE h.project_id IS NULL OR h.project_id = COALESCE(?, h.project_id)
29796
30354
  `).all(scope.project_id ?? null).filter((row) => {
29797
- const payload = parsePayload(row.payload_json);
30355
+ const payload = parsePayload2(row.payload_json);
29798
30356
  const taskRefs = parseStringArray(payload["task_ids"]);
29799
30357
  const runRefs = parseStringArray(payload["run_ids"]);
29800
30358
  if (scope.project_id && row.project_id !== scope.project_id)
@@ -29825,7 +30383,7 @@ function toLedgerEntries(rows) {
29825
30383
  });
29826
30384
  let previous = LOCAL_AUDIT_LEDGER_INITIAL_HASH;
29827
30385
  return ordered.map((row, index) => {
29828
- const payload = parsePayload(row.payload_json);
30386
+ const payload = parsePayload2(row.payload_json);
29829
30387
  const payloadHash = hash(canonicalize({ source: row.source, source_id: row.source_id, created_at: row.created_at, payload }));
29830
30388
  const chainHash = hash(`${previous}
29831
30389
  ${payloadHash}`);
@@ -29973,7 +30531,7 @@ __export(exports_release_compatibility, {
29973
30531
  LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION: () => LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION
29974
30532
  });
29975
30533
  import { readFileSync as readFileSync9 } from "fs";
29976
- import { join as join15, resolve as resolve16 } from "path";
30534
+ import { join as join15, resolve as resolve17 } from "path";
29977
30535
  import { Database as Database2 } from "bun:sqlite";
29978
30536
  function pass(id, message, details) {
29979
30537
  return { id, status: "passed", message, details };
@@ -30081,7 +30639,7 @@ function checkChangelog() {
30081
30639
  ];
30082
30640
  }
30083
30641
  function createReleaseCompatibilityReport(options = {}) {
30084
- const root = resolve16(options.root ?? process.cwd());
30642
+ const root = resolve17(options.root ?? process.cwd());
30085
30643
  const packageJson = readPackageJson2(root);
30086
30644
  const simulatedLevels = options.simulated_levels ?? defaultSimulationLevels();
30087
30645
  const checks = [
@@ -35761,7 +36319,7 @@ function classifyLog(text) {
35761
36319
  async function sleep3(ms) {
35762
36320
  if (ms <= 0)
35763
36321
  return;
35764
- await new Promise((resolve17) => setTimeout(resolve17, ms));
36322
+ await new Promise((resolve18) => setTimeout(resolve18, ms));
35765
36323
  }
35766
36324
  async function runCommandProvider(provider, input) {
35767
36325
  const commandTemplate = input.command || provider.command;
@@ -37700,7 +38258,7 @@ function limitValue(value) {
37700
38258
  return 20;
37701
38259
  return Math.max(1, Math.min(500, Math.trunc(value)));
37702
38260
  }
37703
- function isTerminal(task) {
38261
+ function isTerminal2(task) {
37704
38262
  return task.status === "completed" || task.status === "failed" || task.status === "cancelled";
37705
38263
  }
37706
38264
  function sameAgent(task, agentId) {
@@ -37742,7 +38300,7 @@ function summarizeTask(task) {
37742
38300
  function overdueTasks(tasks, nowIso) {
37743
38301
  const now4 = Date.parse(nowIso);
37744
38302
  return tasks.filter((task) => {
37745
- if (isTerminal(task) || !task.due_at)
38303
+ if (isTerminal2(task) || !task.due_at)
37746
38304
  return false;
37747
38305
  const due = Date.parse(task.due_at);
37748
38306
  return Number.isFinite(due) && due < now4;
@@ -38111,7 +38669,7 @@ __export(exports_local_backups, {
38111
38669
  });
38112
38670
  import { createHash as createHash8 } from "crypto";
38113
38671
  import { readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
38114
- import { dirname as dirname8, resolve as resolve17 } from "path";
38672
+ import { dirname as dirname8, resolve as resolve18 } from "path";
38115
38673
  import { mkdirSync as mkdirSync7 } from "fs";
38116
38674
  function stableJson(value) {
38117
38675
  if (value === null || typeof value !== "object")
@@ -38213,14 +38771,14 @@ function createLocalBackup(options = {}, db) {
38213
38771
  return backup;
38214
38772
  }
38215
38773
  function writeLocalBackupFile(backup, outputPath) {
38216
- const path = resolve17(outputPath);
38774
+ const path = resolve18(outputPath);
38217
38775
  mkdirSync7(dirname8(path), { recursive: true });
38218
38776
  writeFileSync5(path, `${JSON.stringify(backup, null, 2)}
38219
38777
  `);
38220
38778
  return path;
38221
38779
  }
38222
38780
  function readLocalBackupFile(path) {
38223
- return JSON.parse(readFileSync11(resolve17(path), "utf-8"));
38781
+ return JSON.parse(readFileSync11(resolve18(path), "utf-8"));
38224
38782
  }
38225
38783
  function verifyLocalBackup(value, options = {}, db) {
38226
38784
  const verifiedAt = options.verified_at ?? now();
@@ -42516,7 +43074,8 @@ function writeQueue(task2, queue, actor, action, db) {
42516
43074
  logTaskChange(task2.id, `review_queue.${action}`, "review_queue", null, JSON.stringify(queue), actor, d);
42517
43075
  emitLocalEventHooksQuiet({
42518
43076
  type: `review.${action}`,
42519
- payload: { task_id: task2.id, queue: queue.queue, state: queue.state, reviewer: queue.reviewer, claimed_by: queue.claimed_by }
43077
+ payload: { task_id: task2.id, queue: queue.queue, state: queue.state, reviewer: queue.reviewer, claimed_by: queue.claimed_by },
43078
+ databasePath: databasePathFromDatabase(d)
42520
43079
  });
42521
43080
  return itemFromTask(taskOrThrow2(task2.id, d), queue, d);
42522
43081
  }
@@ -42820,6 +43379,7 @@ var init_review_queues = __esm(() => {
42820
43379
  init_database();
42821
43380
  init_tasks();
42822
43381
  init_config();
43382
+ init_event_emission_safety();
42823
43383
  init_event_hooks();
42824
43384
  init_task_contracts();
42825
43385
  STATES = new Set(["requested", "claimed", "approved", "changes_requested", "returned", "reopened"]);
@@ -44918,8 +45478,8 @@ __export(exports_environment_snapshots, {
44918
45478
  import { createHash as createHash12 } from "crypto";
44919
45479
  import { existsSync as existsSync17, readFileSync as readFileSync13, statSync as statSync8 } from "fs";
44920
45480
  import { hostname as hostname2, platform, arch } from "os";
44921
- import { dirname as dirname9, join as join17, resolve as resolve18 } from "path";
44922
- import { tmpdir as tmpdir2 } from "os";
45481
+ import { dirname as dirname9, join as join17, resolve as resolve19 } from "path";
45482
+ import { tmpdir as tmpdir3 } from "os";
44923
45483
  function sha2566(value) {
44924
45484
  return createHash12("sha256").update(value).digest("hex");
44925
45485
  }
@@ -45032,15 +45592,15 @@ function commandEnv(env, includeValues) {
45032
45592
  function defaultSnapshotDir() {
45033
45593
  const dbPath = getDatabasePath();
45034
45594
  if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
45035
- return join17(tmpdir2(), "hasna-todos", "environment-snapshots");
45036
- return join17(dirname9(resolve18(dbPath)), "environment-snapshots");
45595
+ return join17(tmpdir3(), "hasna-todos", "environment-snapshots");
45596
+ return join17(dirname9(resolve19(dbPath)), "environment-snapshots");
45037
45597
  }
45038
45598
  function snapshotWithId(snapshot) {
45039
45599
  const digest = sha2566(JSON.stringify(snapshot)).slice(0, 24);
45040
45600
  return { id: `env_${digest}`, ...snapshot };
45041
45601
  }
45042
45602
  function captureEnvironmentSnapshot(input = {}) {
45043
- const root = resolve18(input.root || process.cwd());
45603
+ const root = resolve19(input.root || process.cwd());
45044
45604
  const env = input.env || process.env;
45045
45605
  const warnings = [];
45046
45606
  const manifests = MANIFEST_FILES.map((file) => manifestRecord(root, file)).filter((file) => Boolean(file));
@@ -45080,13 +45640,13 @@ function captureEnvironmentSnapshot(input = {}) {
45080
45640
  });
45081
45641
  }
45082
45642
  function writeEnvironmentSnapshot(snapshot, outputPath) {
45083
- const path = outputPath ? resolve18(outputPath) : join17(defaultSnapshotDir(), `${snapshot.id}.json`);
45643
+ const path = outputPath ? resolve19(outputPath) : join17(defaultSnapshotDir(), `${snapshot.id}.json`);
45084
45644
  ensureDir2(dirname9(path));
45085
45645
  writeJsonFile(path, snapshot);
45086
45646
  return path;
45087
45647
  }
45088
45648
  function readEnvironmentSnapshot(path) {
45089
- const snapshot = readJsonFile(resolve18(path));
45649
+ const snapshot = readJsonFile(resolve19(path));
45090
45650
  if (!snapshot || snapshot.schema_version !== 1 || typeof snapshot.id !== "string") {
45091
45651
  throw new Error(`Invalid environment snapshot: ${path}`);
45092
45652
  }
@@ -50321,7 +50881,8 @@ Repairs`));
50321
50881
  });
50322
50882
  const limited2 = ready.slice(0, parseInt(opts.limit, 10));
50323
50883
  if (opts.json || globalOpts.json) {
50324
- console.log(JSON.stringify(limited2));
50884
+ const { getTaskRouteState: getTaskRouteState2 } = await Promise.resolve().then(() => (init_task_routing(), exports_task_routing));
50885
+ console.log(JSON.stringify(limited2.map((task2) => ({ ...task2, route_state: getTaskRouteState2(task2, db) }))));
50325
50886
  return;
50326
50887
  }
50327
50888
  if (limited2.length === 0) {
@@ -52936,7 +53497,7 @@ __export(exports_machines, {
52936
53497
  import chalk10 from "chalk";
52937
53498
  import { execSync as execSync4 } from "child_process";
52938
53499
  import { readFileSync as readFileSync17, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "fs";
52939
- import { tmpdir as tmpdir3 } from "os";
53500
+ import { tmpdir as tmpdir4 } from "os";
52940
53501
  import { join as join21 } from "path";
52941
53502
  function getOrCreateLocalMachineName() {
52942
53503
  return process.env["TODOS_MACHINE_NAME"] || __require("os").hostname() || "unknown";
@@ -52975,7 +53536,7 @@ function remoteTempPath(sshAddress) {
52975
53536
  }
52976
53537
  function readRemoteBridgeBundle(sshAddress) {
52977
53538
  const remotePath = remoteTempPath(sshAddress);
52978
- const localPath = join21(tmpdir3(), `todos-bridge-pull-${uuid()}.json`);
53539
+ const localPath = join21(tmpdir4(), `todos-bridge-pull-${uuid()}.json`);
52979
53540
  try {
52980
53541
  runSsh(sshAddress, `todos export --format bridge --allow-plaintext-sensitive --output ${shellQuote(remotePath)}`, 120000);
52981
53542
  scpFromRemote(sshAddress, remotePath, localPath);
@@ -52990,7 +53551,7 @@ function readRemoteBridgeBundle(sshAddress) {
52990
53551
  }
52991
53552
  }
52992
53553
  function writeLocalBridgeBundle() {
52993
- const localPath = join21(tmpdir3(), `todos-bridge-push-${uuid()}.json`);
53554
+ const localPath = join21(tmpdir4(), `todos-bridge-push-${uuid()}.json`);
52994
53555
  writeFileSync10(localPath, JSON.stringify(createLocalBridgeBundle(), null, 2));
52995
53556
  return localPath;
52996
53557
  }
@@ -54054,7 +54615,7 @@ __export(exports_onboarding_commands, {
54054
54615
  registerOnboardingCommands: () => registerOnboardingCommands
54055
54616
  });
54056
54617
  import chalk17 from "chalk";
54057
- import { resolve as resolve19 } from "path";
54618
+ import { resolve as resolve20 } from "path";
54058
54619
  function registerOnboardingCommands(program2) {
54059
54620
  program2.command("onboarding").alias("demo-fixtures").description("List, show, write, or import bundled local onboarding fixtures").option("--show <name>", "Show one fixture bridge bundle as JSON").option("--write <dir>", "Write all bundled fixture bridge bundles to a directory").option("--import <name>", "Dry-run or apply an onboarding fixture import").option("--apply", "Apply an onboarding fixture import. Defaults to dry-run.").option("--resolve-conflicts", "Safely merge existing local tasks while preserving divergent fields").action(async (opts) => {
54060
54621
  const globalOpts = program2.opts();
@@ -54070,7 +54631,7 @@ function registerOnboardingCommands(program2) {
54070
54631
  return;
54071
54632
  }
54072
54633
  if (opts.write) {
54073
- const result = writeOnboardingFixtureFiles2(resolve19(opts.write));
54634
+ const result = writeOnboardingFixtureFiles2(resolve20(opts.write));
54074
54635
  if (globalOpts.json) {
54075
54636
  output(result, true);
54076
54637
  return;
@@ -57650,7 +58211,7 @@ __export(exports_sdk_fixture_commands, {
57650
58211
  registerSdkFixtureCommands: () => registerSdkFixtureCommands
57651
58212
  });
57652
58213
  import chalk19 from "chalk";
57653
- import { resolve as resolve20 } from "path";
58214
+ import { resolve as resolve21 } from "path";
57654
58215
  function registerSdkFixtureCommands(program2) {
57655
58216
  program2.command("sdk-fixtures").description("List, show, or write local SDK integration fixtures").option("--show", "Print the full fixture pack JSON").option("--write <dir>", "Write fixture pack, bridge fixture, contract snapshots, and example index to a directory").action(async (opts) => {
57656
58217
  const globalOpts = program2.opts();
@@ -57661,7 +58222,7 @@ function registerSdkFixtureCommands(program2) {
57661
58222
  writeSdkIntegrationFixtures: writeSdkIntegrationFixtures2
57662
58223
  } = await Promise.resolve().then(() => (init_sdk_integration_fixtures(), exports_sdk_integration_fixtures));
57663
58224
  if (opts.write) {
57664
- const result = writeSdkIntegrationFixtures2(resolve20(opts.write));
58225
+ const result = writeSdkIntegrationFixtures2(resolve21(opts.write));
57665
58226
  if (globalOpts.json) {
57666
58227
  console.log(JSON.stringify(result));
57667
58228
  return;
@@ -58483,7 +59044,7 @@ __export(exports_local_backup_commands, {
58483
59044
  registerLocalBackupCommands: () => registerLocalBackupCommands
58484
59045
  });
58485
59046
  import chalk26 from "chalk";
58486
- import { resolve as resolve21 } from "path";
59047
+ import { resolve as resolve22 } from "path";
58487
59048
  function globalOptions6(program2) {
58488
59049
  const command = program2;
58489
59050
  return command.optsWithGlobals?.() ?? program2.opts();
@@ -58505,10 +59066,10 @@ function registerLocalBackupCommands(program2) {
58505
59066
  const projectId = opts.projectId ?? autoProject(globalOpts);
58506
59067
  const backupBundle = createLocalBackup2({
58507
59068
  project_id: projectId,
58508
- output_path: opts.output ? resolve21(opts.output) : undefined
59069
+ output_path: opts.output ? resolve22(opts.output) : undefined
58509
59070
  });
58510
59071
  const result = {
58511
- output_path: opts.output ? resolve21(opts.output) : null,
59072
+ output_path: opts.output ? resolve22(opts.output) : null,
58512
59073
  backup: backupBundle
58513
59074
  };
58514
59075
  if (opts.json || globalOpts.json) {
@@ -58767,11 +59328,13 @@ function exportSqliteTodosStorageSnapshot(db) {
58767
59328
  source: "sqlite",
58768
59329
  tasks: listTasks({ include_archived: true }, d),
58769
59330
  projects: listProjects(d),
59331
+ projectMachinePaths: listProjectMachinePaths(d),
58770
59332
  plans: listPlans(undefined, d),
58771
59333
  agents: listAgents({ include_archived: true }, d),
58772
59334
  taskLists: listTaskLists(undefined, d),
58773
59335
  templates: listTemplates(d),
58774
- auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d)
59336
+ auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d),
59337
+ tombstones: listStorageTombstones(d)
58775
59338
  };
58776
59339
  }
58777
59340
  function importSqliteTodosStorageSnapshot(snapshot, db) {
@@ -58779,13 +59342,19 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
58779
59342
  const result = {
58780
59343
  inserted: 0,
58781
59344
  updated: 0,
59345
+ deleted: 0,
58782
59346
  skipped: 0,
58783
59347
  errors: []
58784
59348
  };
58785
- const applyRows = (table, columns, rows, updateClockColumn, afterUpsert) => {
59349
+ const applyRows = (objectType2, table, columns, rows, updateClockColumn, afterUpsert) => {
58786
59350
  for (const row of rows) {
58787
59351
  try {
58788
59352
  const record = asRecord2(row);
59353
+ const tombstone = typeof record["id"] === "string" ? getStorageTombstone(objectType2, record["id"], d) : null;
59354
+ if (tombstone && shouldApplyStorageTombstone(tombstone, rowClock(record, updateClockColumn))) {
59355
+ result.skipped += 1;
59356
+ continue;
59357
+ }
58789
59358
  const state = upsertById(d, table, columns, record, updateClockColumn);
58790
59359
  if (state === "inserted")
58791
59360
  result.inserted += 1;
@@ -58799,17 +59368,19 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
58799
59368
  }
58800
59369
  }
58801
59370
  };
58802
- applyRows("projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
58803
- applyRows("agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
58804
- applyRows("task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
58805
- applyRows("plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
58806
- applyRows("task_templates", TEMPLATE_COLUMNS, snapshot.templates);
58807
- applyRows("tasks", TASK_COLUMNS, sortedTasks2(snapshot.tasks), "updated_at", (row, changed) => {
59371
+ applyRows("projects", "projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
59372
+ applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
59373
+ applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
59374
+ applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
59375
+ applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
59376
+ applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
59377
+ applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks2(snapshot.tasks), "updated_at", (row, changed) => {
58808
59378
  if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
58809
59379
  replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
58810
59380
  }
58811
59381
  });
58812
- applyRows("task_history", AUDIT_COLUMNS, snapshot.auditHistory);
59382
+ applyRows("audit_history", "task_history", AUDIT_COLUMNS, snapshot.auditHistory);
59383
+ applyTombstones(d, snapshot.tombstones ?? [], result);
58813
59384
  return result;
58814
59385
  }
58815
59386
  function upsertById(db, table, columns, row, updateClockColumn) {
@@ -58864,7 +59435,86 @@ function sortedTasks2(tasks) {
58864
59435
  visit(task2);
58865
59436
  return result;
58866
59437
  }
58867
- var PROJECT_COLUMNS, TASK_LIST_COLUMNS, PLAN_COLUMNS, AGENT_COLUMNS, TEMPLATE_COLUMNS, TASK_COLUMNS, AUDIT_COLUMNS, JSON_COLUMNS, BOOLEAN_COLUMNS;
59438
+ function applyTombstones(db, tombstones, result) {
59439
+ for (const tombstone of tombstones) {
59440
+ try {
59441
+ recordStorageTombstone({
59442
+ object_type: tombstone.object_type,
59443
+ object_id: tombstone.object_id,
59444
+ deleted_at: tombstone.deleted_at,
59445
+ source_machine_id: tombstone.source_machine_id ?? null,
59446
+ payload: tombstone.payload ?? null,
59447
+ version: tombstone.version ?? null
59448
+ }, db);
59449
+ const table = tableForTombstone(tombstone.object_type);
59450
+ const existing = existingClock(db, table, tombstone.object_id);
59451
+ if (!shouldApplyStorageTombstone(tombstone, existing)) {
59452
+ result.skipped += 1;
59453
+ continue;
59454
+ }
59455
+ const deletedTags = table === "tasks" ? db.run("DELETE FROM task_tags WHERE task_id = ?", [tombstone.object_id]).changes : 0;
59456
+ const deleted = db.run(`DELETE FROM ${table} WHERE id = ?`, [tombstone.object_id]).changes;
59457
+ if (deleted > 0 || deletedTags > 0)
59458
+ result.deleted = (result.deleted ?? 0) + 1;
59459
+ else
59460
+ result.skipped += 1;
59461
+ } catch (error) {
59462
+ result.errors.push(error instanceof Error ? error.message : String(error));
59463
+ }
59464
+ }
59465
+ }
59466
+ function tableForTombstone(objectType2) {
59467
+ if (objectType2 === "tasks")
59468
+ return "tasks";
59469
+ if (objectType2 === "projects")
59470
+ return "projects";
59471
+ if (objectType2 === "project_machine_paths")
59472
+ return "project_machine_paths";
59473
+ if (objectType2 === "plans")
59474
+ return "plans";
59475
+ if (objectType2 === "agents")
59476
+ return "agents";
59477
+ if (objectType2 === "task_lists")
59478
+ return "task_lists";
59479
+ if (objectType2 === "templates")
59480
+ return "task_templates";
59481
+ return "task_history";
59482
+ }
59483
+ function listRows(db, table, columns) {
59484
+ return db.query(`SELECT ${columns.join(", ")} FROM ${table} ORDER BY id`).all();
59485
+ }
59486
+ function listProjectMachinePaths(db) {
59487
+ return listRows(db, "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS).map((row) => ({
59488
+ id: String(row.id),
59489
+ project_id: String(row.project_id),
59490
+ machine_id: String(row.machine_id),
59491
+ path: String(row.path),
59492
+ created_at: String(row.created_at),
59493
+ updated_at: String(row.updated_at)
59494
+ }));
59495
+ }
59496
+ function existingClock(db, table, id) {
59497
+ const clockColumns = clockColumnsForTable(table);
59498
+ const row = db.query(`SELECT ${clockColumns.join(", ")} FROM ${table} WHERE id = ?`).get(id);
59499
+ return row?.updated_at ?? row?.last_seen_at ?? row?.created_at ?? null;
59500
+ }
59501
+ function rowClock(row, updateClockColumn) {
59502
+ const value = updateClockColumn ? row[updateClockColumn] : null;
59503
+ return stringClock(value) ?? stringClock(row["updated_at"]) ?? stringClock(row["last_seen_at"]) ?? stringClock(row["created_at"]);
59504
+ }
59505
+ function stringClock(value) {
59506
+ return typeof value === "string" && value ? value : null;
59507
+ }
59508
+ function clockColumnsForTable(table) {
59509
+ if (table === "agents")
59510
+ return ["last_seen_at", "created_at"];
59511
+ if (table === "task_templates")
59512
+ return ["created_at"];
59513
+ if (table === "task_history")
59514
+ return ["created_at"];
59515
+ return ["updated_at", "created_at"];
59516
+ }
59517
+ var PROJECT_COLUMNS, PROJECT_MACHINE_PATH_COLUMNS, TASK_LIST_COLUMNS, PLAN_COLUMNS, AGENT_COLUMNS, TEMPLATE_COLUMNS, TASK_COLUMNS, AUDIT_COLUMNS, JSON_COLUMNS, BOOLEAN_COLUMNS;
58868
59518
  var init_sqlite_snapshot = __esm(() => {
58869
59519
  init_database();
58870
59520
  init_agents();
@@ -58874,6 +59524,7 @@ var init_sqlite_snapshot = __esm(() => {
58874
59524
  init_task_lists();
58875
59525
  init_tasks();
58876
59526
  init_templates();
59527
+ init_storage_tombstones();
58877
59528
  PROJECT_COLUMNS = [
58878
59529
  "id",
58879
59530
  "name",
@@ -58887,6 +59538,14 @@ var init_sqlite_snapshot = __esm(() => {
58887
59538
  "machine_id",
58888
59539
  "synced_at"
58889
59540
  ];
59541
+ PROJECT_MACHINE_PATH_COLUMNS = [
59542
+ "id",
59543
+ "project_id",
59544
+ "machine_id",
59545
+ "path",
59546
+ "created_at",
59547
+ "updated_at"
59548
+ ];
58890
59549
  TASK_LIST_COLUMNS = [
58891
59550
  "id",
58892
59551
  "project_id",
@@ -59196,7 +59855,7 @@ class PostgresTodosSyncStore {
59196
59855
  }
59197
59856
  async pullSnapshot(options = {}) {
59198
59857
  const params = [this.service];
59199
- const filters = ["service = $1", "deleted_at IS NULL"];
59858
+ const filters = ["service = $1"];
59200
59859
  if (options.since) {
59201
59860
  params.push(options.since);
59202
59861
  filters.push(`updated_at > $${params.length}::timestamptz`);
@@ -59205,7 +59864,7 @@ class PostgresTodosSyncStore {
59205
59864
  params.push(options.objectTypes);
59206
59865
  filters.push(`object_type = ANY($${params.length}::text[])`);
59207
59866
  }
59208
- const response = await this.client.query(`SELECT object_type, payload FROM ${this.tableName}
59867
+ const response = await this.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version FROM ${this.tableName}
59209
59868
  WHERE ${filters.join(" AND ")}
59210
59869
  ORDER BY updated_at ASC, object_type ASC, object_id ASC`, params);
59211
59870
  return rowsToSnapshot(response.rows);
@@ -59229,11 +59888,20 @@ function snapshotEntries(snapshot) {
59229
59888
  return [
59230
59889
  ...snapshot.tasks.map((payload) => entry("tasks", payload, snapshot.exportedAt)),
59231
59890
  ...snapshot.projects.map((payload) => entry("projects", payload, snapshot.exportedAt)),
59891
+ ...(snapshot.projectMachinePaths ?? []).map((payload) => entry("project_machine_paths", payload, snapshot.exportedAt)),
59232
59892
  ...snapshot.plans.map((payload) => entry("plans", payload, snapshot.exportedAt)),
59233
59893
  ...snapshot.agents.map((payload) => entry("agents", payload, snapshot.exportedAt)),
59234
59894
  ...snapshot.taskLists.map((payload) => entry("task_lists", payload, snapshot.exportedAt)),
59235
59895
  ...snapshot.templates.map((payload) => entry("templates", payload, snapshot.exportedAt)),
59236
- ...snapshot.auditHistory.map((payload) => entry("audit_history", payload, snapshot.exportedAt))
59896
+ ...snapshot.auditHistory.map((payload) => entry("audit_history", payload, snapshot.exportedAt)),
59897
+ ...(snapshot.tombstones ?? []).map((tombstone) => ({
59898
+ type: tombstone.object_type,
59899
+ id: tombstone.object_id,
59900
+ payload: tombstone.payload ?? { id: tombstone.object_id, deleted_at: tombstone.deleted_at },
59901
+ updatedAt: tombstone.updated_at || tombstone.deleted_at,
59902
+ deletedAt: tombstone.deleted_at,
59903
+ version: tombstone.version ?? null
59904
+ }))
59237
59905
  ];
59238
59906
  }
59239
59907
  function entry(type, payload, fallbackUpdatedAt) {
@@ -59255,19 +59923,38 @@ function rowsToSnapshot(rows) {
59255
59923
  source: "postgres",
59256
59924
  tasks: [],
59257
59925
  projects: [],
59926
+ projectMachinePaths: [],
59258
59927
  plans: [],
59259
59928
  agents: [],
59260
59929
  taskLists: [],
59261
59930
  templates: [],
59262
- auditHistory: []
59931
+ auditHistory: [],
59932
+ tombstones: []
59263
59933
  };
59264
59934
  for (const row of rows) {
59265
59935
  const payload = payloadRecord(row.payload);
59936
+ const deletedAt = stringValue(row.deleted_at);
59937
+ if (deletedAt) {
59938
+ snapshot.tombstones ??= [];
59939
+ snapshot.tombstones.push({
59940
+ object_type: row.object_type,
59941
+ object_id: stringValue(row.object_id) ?? stringValue(payload["id"]) ?? "",
59942
+ deleted_at: deletedAt,
59943
+ updated_at: stringValue(row.updated_at) ?? deletedAt,
59944
+ source_machine_id: stringValue(row.source_machine_id),
59945
+ payload,
59946
+ version: numberValue2(row.version)
59947
+ });
59948
+ continue;
59949
+ }
59266
59950
  if (row.object_type === "tasks")
59267
59951
  snapshot.tasks.push(payload);
59268
59952
  else if (row.object_type === "projects")
59269
59953
  snapshot.projects.push(payload);
59270
- else if (row.object_type === "plans")
59954
+ else if (row.object_type === "project_machine_paths") {
59955
+ snapshot.projectMachinePaths ??= [];
59956
+ snapshot.projectMachinePaths.push(payload);
59957
+ } else if (row.object_type === "plans")
59271
59958
  snapshot.plans.push(payload);
59272
59959
  else if (row.object_type === "agents")
59273
59960
  snapshot.agents.push(payload);
@@ -59288,6 +59975,8 @@ function payloadRecord(value) {
59288
59975
  throw new Error("Postgres sync payload must be a JSON object");
59289
59976
  }
59290
59977
  function stringValue(value) {
59978
+ if (value instanceof Date)
59979
+ return value.toISOString();
59291
59980
  return typeof value === "string" && value ? value : null;
59292
59981
  }
59293
59982
  function numberValue2(value) {
@@ -59446,6 +60135,9 @@ class PostgresJsonRecordStore {
59446
60135
  this.tableName = options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE;
59447
60136
  this.cursorTableName = options.cursorTableName ?? DEFAULT_TODOS_POSTGRES_CURSOR_TABLE;
59448
60137
  }
60138
+ machineId(context) {
60139
+ return context?.requestId ?? this.sourceMachineId ?? null;
60140
+ }
59449
60141
  async ensureSchema() {
59450
60142
  this.schemaReady ??= (async () => {
59451
60143
  for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
@@ -59477,6 +60169,25 @@ class PostgresJsonRecordStore {
59477
60169
  updatedAt: stringValue2(row.updated_at) ?? new Date().toISOString()
59478
60170
  }));
59479
60171
  }
60172
+ async listTombstones() {
60173
+ await this.ensureSchema();
60174
+ const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
60175
+ FROM ${this.tableName}
60176
+ WHERE service = $1 AND deleted_at IS NOT NULL
60177
+ ORDER BY updated_at ASC, object_type ASC, object_id ASC`, [this.service]);
60178
+ return result.rows.map((row) => {
60179
+ const deletedAt = stringValue2(row.deleted_at) ?? stringValue2(row.updated_at) ?? new Date().toISOString();
60180
+ return {
60181
+ object_type: row.object_type,
60182
+ object_id: row.object_id,
60183
+ deleted_at: deletedAt,
60184
+ updated_at: stringValue2(row.updated_at) ?? deletedAt,
60185
+ source_machine_id: stringValue2(row.source_machine_id),
60186
+ payload: payloadRecord2(row.payload),
60187
+ version: numberValue3(row.version)
60188
+ };
60189
+ });
60190
+ }
59480
60191
  async upsert(type, value, context = {}) {
59481
60192
  await this.ensureSchema();
59482
60193
  const updatedAt = stringValue2(value.updated_at) ?? stringValue2(value.created_at) ?? new Date().toISOString();
@@ -59489,7 +60200,8 @@ class PostgresJsonRecordStore {
59489
60200
  updated_at = EXCLUDED.updated_at,
59490
60201
  deleted_at = NULL,
59491
60202
  source_machine_id = EXCLUDED.source_machine_id,
59492
- version = EXCLUDED.version`, [
60203
+ version = EXCLUDED.version
60204
+ WHERE ${this.tableName}.updated_at IS NULL OR ${this.tableName}.updated_at <= EXCLUDED.updated_at`, [
59493
60205
  this.service,
59494
60206
  type,
59495
60207
  value.id,
@@ -59506,13 +60218,58 @@ class PostgresJsonRecordStore {
59506
60218
  if (!existing)
59507
60219
  return false;
59508
60220
  const timestamp3 = new Date().toISOString();
59509
- await this.options.client.query(`UPDATE ${this.tableName}
59510
- SET deleted_at = $4::timestamptz,
59511
- updated_at = $4::timestamptz,
59512
- source_machine_id = $5
59513
- WHERE service = $1 AND object_type = $2 AND object_id = $3`, [this.service, type, id, timestamp3, context.requestId ?? this.sourceMachineId ?? null]);
60221
+ return this.tombstone({
60222
+ object_type: type,
60223
+ object_id: id,
60224
+ deleted_at: timestamp3,
60225
+ updated_at: timestamp3,
60226
+ payload: existing,
60227
+ version: numberValue3(existing["version"])
60228
+ }, context);
60229
+ }
60230
+ async tombstone(tombstone, context = {}) {
60231
+ await this.ensureSchema();
60232
+ const deletedAt = stringValue2(tombstone.deleted_at) ?? new Date().toISOString();
60233
+ const updatedAt = stringValue2(tombstone.updated_at) ?? deletedAt;
60234
+ const existing = await this.clock(tombstone.object_type, tombstone.object_id);
60235
+ if (existing && compareClock(existing.updatedAt, updatedAt) > 0)
60236
+ return false;
60237
+ await this.options.client.query(`INSERT INTO ${this.tableName} (
60238
+ service, object_type, object_id, payload, updated_at,
60239
+ deleted_at, source_machine_id, version
60240
+ ) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $6::timestamptz, $7, $8)
60241
+ ON CONFLICT (service, object_type, object_id) DO UPDATE SET
60242
+ payload = EXCLUDED.payload,
60243
+ updated_at = EXCLUDED.updated_at,
60244
+ deleted_at = EXCLUDED.deleted_at,
60245
+ source_machine_id = EXCLUDED.source_machine_id,
60246
+ version = EXCLUDED.version
60247
+ WHERE ${this.tableName}.updated_at IS NULL OR ${this.tableName}.updated_at <= EXCLUDED.updated_at`, [
60248
+ this.service,
60249
+ tombstone.object_type,
60250
+ tombstone.object_id,
60251
+ JSON.stringify(tombstone.payload ?? { id: tombstone.object_id, deleted_at: deletedAt }),
60252
+ updatedAt,
60253
+ deletedAt,
60254
+ tombstone.source_machine_id ?? context.requestId ?? this.sourceMachineId ?? null,
60255
+ tombstone.version ?? null
60256
+ ]);
59514
60257
  return true;
59515
60258
  }
60259
+ async clock(type, id) {
60260
+ await this.ensureSchema();
60261
+ const result = await this.options.client.query(`SELECT object_type, object_id, updated_at, deleted_at
60262
+ FROM ${this.tableName}
60263
+ WHERE service = $1 AND object_type = $2 AND object_id = $3
60264
+ LIMIT 1`, [this.service, type, id]);
60265
+ const row = result.rows[0];
60266
+ if (!row)
60267
+ return null;
60268
+ return {
60269
+ updatedAt: stringValue2(row.updated_at) ?? new Date().toISOString(),
60270
+ deletedAt: stringValue2(row.deleted_at)
60271
+ };
60272
+ }
59516
60273
  async getCursor(name) {
59517
60274
  await this.ensureSchema();
59518
60275
  const result = await this.options.client.query(`SELECT value FROM ${this.cursorTableName} WHERE service = $1 AND cursor_name = $2`, [this.service, name]);
@@ -59581,7 +60338,10 @@ async function createTask3(input, store, context) {
59581
60338
  runner_started_at: null,
59582
60339
  runner_completed_at: null,
59583
60340
  current_step: null,
59584
- total_steps: null
60341
+ total_steps: null,
60342
+ machine_id: store.machineId(context),
60343
+ synced_at: null,
60344
+ archived_at: null
59585
60345
  };
59586
60346
  await store.upsert("tasks", task2, context);
59587
60347
  await logTaskChange2(task2.id, "created", "status", null, task2.status, task2.assigned_by ?? task2.agent_id, store, context);
@@ -59706,7 +60466,9 @@ async function createProject2(input, store, context) {
59706
60466
  task_prefix: input.task_prefix ?? await generateProjectPrefix(input.name, store),
59707
60467
  task_counter: 0,
59708
60468
  created_at: timestamp3,
59709
- updated_at: timestamp3
60469
+ updated_at: timestamp3,
60470
+ machine_id: store.machineId(context),
60471
+ synced_at: null
59710
60472
  };
59711
60473
  return store.upsert("projects", project, context);
59712
60474
  }
@@ -59726,7 +60488,9 @@ async function createPlan2(input, store, context) {
59726
60488
  description: input.description ?? null,
59727
60489
  status: input.status ?? "active",
59728
60490
  created_at: timestamp3,
59729
- updated_at: timestamp3
60491
+ updated_at: timestamp3,
60492
+ machine_id: store.machineId(context),
60493
+ synced_at: null
59730
60494
  }, context);
59731
60495
  }
59732
60496
  async function updatePlan2(id, input, store) {
@@ -59756,7 +60520,9 @@ async function registerAgent2(input, store, context) {
59756
60520
  last_seen_at: timestamp3,
59757
60521
  session_id: input.session_id ?? context?.sessionId ?? existing?.session_id ?? null,
59758
60522
  working_dir: input.working_dir ?? existing?.working_dir ?? null,
59759
- active_project_id: input.project_id ?? context?.projectId ?? existing?.active_project_id ?? null
60523
+ active_project_id: input.project_id ?? context?.projectId ?? existing?.active_project_id ?? null,
60524
+ machine_id: existing?.machine_id ?? store.machineId(context),
60525
+ synced_at: existing?.synced_at ?? null
59760
60526
  };
59761
60527
  return store.upsert("agents", agent, context);
59762
60528
  }
@@ -59783,7 +60549,9 @@ async function createTaskList2(input, store, context) {
59783
60549
  description: input.description ?? null,
59784
60550
  metadata: input.metadata ?? {},
59785
60551
  created_at: timestamp3,
59786
- updated_at: timestamp3
60552
+ updated_at: timestamp3,
60553
+ machine_id: store.machineId(context),
60554
+ synced_at: null
59787
60555
  }, context);
59788
60556
  }
59789
60557
  async function updateTaskList2(id, input, store) {
@@ -59809,7 +60577,9 @@ async function createTemplate2(input, store, context) {
59809
60577
  project_id: input.project_id ?? context?.projectId ?? null,
59810
60578
  plan_id: input.plan_id ?? null,
59811
60579
  metadata: input.metadata ?? {},
59812
- created_at: timestamp3
60580
+ created_at: timestamp3,
60581
+ machine_id: store.machineId(context),
60582
+ synced_at: null
59813
60583
  }, context);
59814
60584
  }
59815
60585
  async function updateTemplate2(id, input, store) {
@@ -59834,7 +60604,8 @@ async function logTaskChange2(taskId, action, field2, oldValue, newValue, agentI
59834
60604
  old_value: oldValue ?? null,
59835
60605
  new_value: newValue ?? null,
59836
60606
  agent_id: agentId ?? context?.agentId ?? null,
59837
- created_at: new Date().toISOString()
60607
+ created_at: new Date().toISOString(),
60608
+ machine_id: store.machineId(context)
59838
60609
  };
59839
60610
  return store.upsert("audit_history", entry2, context);
59840
60611
  }
@@ -59857,18 +60628,21 @@ async function exportSnapshot(store) {
59857
60628
  source: "postgres",
59858
60629
  tasks: await store.list("tasks"),
59859
60630
  projects: await store.list("projects"),
60631
+ projectMachinePaths: await store.list("project_machine_paths"),
59860
60632
  plans: await store.list("plans"),
59861
60633
  agents: await store.list("agents"),
59862
60634
  taskLists: await store.list("task_lists"),
59863
60635
  templates: await store.list("templates"),
59864
- auditHistory: await store.list("audit_history")
60636
+ auditHistory: await store.list("audit_history"),
60637
+ tombstones: await store.listTombstones()
59865
60638
  };
59866
60639
  }
59867
60640
  async function importSnapshot(snapshot, store, context) {
59868
- const result = { inserted: 0, updated: 0, skipped: 0, errors: [] };
60641
+ const result = { inserted: 0, updated: 0, deleted: 0, skipped: 0, errors: [] };
59869
60642
  const entries = [
59870
60643
  ...snapshot.tasks.map((row) => ["tasks", row]),
59871
60644
  ...snapshot.projects.map((row) => ["projects", row]),
60645
+ ...(snapshot.projectMachinePaths ?? []).map((row) => ["project_machine_paths", row]),
59872
60646
  ...snapshot.plans.map((row) => ["plans", row]),
59873
60647
  ...snapshot.agents.map((row) => ["agents", row]),
59874
60648
  ...snapshot.taskLists.map((row) => ["task_lists", row]),
@@ -59887,6 +60661,25 @@ async function importSnapshot(snapshot, store, context) {
59887
60661
  result.errors.push(error instanceof Error ? error.message : String(error));
59888
60662
  }
59889
60663
  }
60664
+ for (const tombstone of snapshot.tombstones ?? []) {
60665
+ try {
60666
+ const deleted = await store.tombstone({
60667
+ object_type: tombstone.object_type,
60668
+ object_id: tombstone.object_id,
60669
+ deleted_at: tombstone.deleted_at,
60670
+ updated_at: tombstone.updated_at,
60671
+ source_machine_id: tombstone.source_machine_id ?? null,
60672
+ payload: tombstone.payload ?? null,
60673
+ version: tombstone.version ?? null
60674
+ }, context);
60675
+ if (deleted)
60676
+ result.deleted = (result.deleted ?? 0) + 1;
60677
+ else
60678
+ result.skipped += 1;
60679
+ } catch (error) {
60680
+ result.errors.push(error instanceof Error ? error.message : String(error));
60681
+ }
60682
+ }
59890
60683
  return result;
59891
60684
  }
59892
60685
  async function requireRecord(type, id, store) {
@@ -59969,8 +60762,17 @@ function payloadRecord2(value) {
59969
60762
  throw new Error("Postgres storage payload must be a JSON object");
59970
60763
  }
59971
60764
  function stringValue2(value) {
60765
+ if (value instanceof Date)
60766
+ return value.toISOString();
59972
60767
  return typeof value === "string" && value ? value : null;
59973
60768
  }
60769
+ function compareClock(left, right) {
60770
+ const leftClock = Date.parse(left);
60771
+ const rightClock = Date.parse(right);
60772
+ if (Number.isNaN(leftClock) || Number.isNaN(rightClock))
60773
+ return left.localeCompare(right);
60774
+ return leftClock - rightClock;
60775
+ }
59974
60776
  function numberValue3(value) {
59975
60777
  return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
59976
60778
  }
@@ -60644,6 +61446,7 @@ var init_native_storage_status = __esm(() => {
60644
61446
  // src/cli/commands/storage-commands.ts
60645
61447
  var exports_storage_commands = {};
60646
61448
  __export(exports_storage_commands, {
61449
+ s3CredentialsFromEnv: () => s3CredentialsFromEnv,
60647
61450
  registerStorageCommands: () => registerStorageCommands
60648
61451
  });
60649
61452
  import chalk27 from "chalk";
@@ -60722,13 +61525,13 @@ function artifactFilter(opts) {
60722
61525
  ...opts.includeAlreadySynced ? { includeAlreadySynced: true } : {}
60723
61526
  };
60724
61527
  }
60725
- function s3CredentialsFromEnv() {
60726
- const { TODOS_STORAGE_ENV: TODOS_STORAGE_ENV2 } = (init_storage(), __toCommonJS(exports_storage));
60727
- const accessKeyId = process.env[TODOS_STORAGE_ENV2.s3AccessKeyId]?.trim();
60728
- const secretAccessKey = process.env[TODOS_STORAGE_ENV2.s3SecretAccessKey]?.trim();
60729
- const sessionToken = process.env[TODOS_STORAGE_ENV2.s3SessionToken]?.trim();
61528
+ function s3CredentialsFromEnv(env = process.env) {
61529
+ const { TODOS_STORAGE_ENV: TODOS_STORAGE_ENV2, TODOS_STORAGE_FALLBACK_ENV: TODOS_STORAGE_FALLBACK_ENV2 } = (init_storage(), __toCommonJS(exports_storage));
61530
+ const accessKeyId = (env[TODOS_STORAGE_ENV2.s3AccessKeyId] ?? env[TODOS_STORAGE_FALLBACK_ENV2.s3AccessKeyId])?.trim();
61531
+ const secretAccessKey = (env[TODOS_STORAGE_ENV2.s3SecretAccessKey] ?? env[TODOS_STORAGE_FALLBACK_ENV2.s3SecretAccessKey])?.trim();
61532
+ const sessionToken = (env[TODOS_STORAGE_ENV2.s3SessionToken] ?? env[TODOS_STORAGE_FALLBACK_ENV2.s3SessionToken])?.trim();
60730
61533
  if (!accessKeyId || !secretAccessKey) {
60731
- throw new Error(`${TODOS_STORAGE_ENV2.s3AccessKeyId} and ${TODOS_STORAGE_ENV2.s3SecretAccessKey} are required for --apply`);
61534
+ throw new Error(`${TODOS_STORAGE_ENV2.s3AccessKeyId}/${TODOS_STORAGE_FALLBACK_ENV2.s3AccessKeyId} and ${TODOS_STORAGE_ENV2.s3SecretAccessKey}/${TODOS_STORAGE_FALLBACK_ENV2.s3SecretAccessKey} are required for --apply`);
60732
61535
  }
60733
61536
  return {
60734
61537
  accessKeyId,