@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/registry.js CHANGED
@@ -1182,6 +1182,22 @@ var init_migrations = __esm(() => {
1182
1182
  CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source);
1183
1183
  CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint);
1184
1184
  INSERT OR IGNORE INTO _migrations (id) VALUES (62);
1185
+ `,
1186
+ `
1187
+ CREATE TABLE IF NOT EXISTS storage_tombstones (
1188
+ id TEXT PRIMARY KEY,
1189
+ object_type TEXT NOT NULL,
1190
+ object_id TEXT NOT NULL,
1191
+ deleted_at TEXT NOT NULL,
1192
+ updated_at TEXT NOT NULL,
1193
+ source_machine_id TEXT,
1194
+ payload TEXT,
1195
+ version INTEGER,
1196
+ UNIQUE(object_type, object_id)
1197
+ );
1198
+ CREATE INDEX IF NOT EXISTS idx_storage_tombstones_object ON storage_tombstones(object_type, object_id);
1199
+ CREATE INDEX IF NOT EXISTS idx_storage_tombstones_updated ON storage_tombstones(updated_at);
1200
+ INSERT OR IGNORE INTO _migrations (id) VALUES (63);
1185
1201
  `
1186
1202
  ];
1187
1203
  });
@@ -1703,6 +1719,20 @@ function ensureSchema(db) {
1703
1719
  )`);
1704
1720
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_project ON project_machine_paths(project_id)");
1705
1721
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_machine ON project_machine_paths(machine_id)");
1722
+ ensureTable("storage_tombstones", `
1723
+ CREATE TABLE storage_tombstones (
1724
+ id TEXT PRIMARY KEY,
1725
+ object_type TEXT NOT NULL,
1726
+ object_id TEXT NOT NULL,
1727
+ deleted_at TEXT NOT NULL,
1728
+ updated_at TEXT NOT NULL,
1729
+ source_machine_id TEXT,
1730
+ payload TEXT,
1731
+ version INTEGER,
1732
+ UNIQUE(object_type, object_id)
1733
+ )`);
1734
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_storage_tombstones_object ON storage_tombstones(object_type, object_id)");
1735
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_storage_tombstones_updated ON storage_tombstones(updated_at)");
1706
1736
  ensureTable("machines", `
1707
1737
  CREATE TABLE machines (
1708
1738
  id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, hostname TEXT, platform TEXT,
@@ -6068,6 +6098,80 @@ init_config();
6068
6098
  init_types();
6069
6099
  init_database();
6070
6100
  init_machines();
6101
+
6102
+ // src/db/storage-tombstones.ts
6103
+ init_database();
6104
+ init_machines();
6105
+ function recordStorageTombstone(input, db) {
6106
+ const d = db ?? getDatabase();
6107
+ const deletedAt = input.deleted_at ?? now();
6108
+ const machineId = input.source_machine_id ?? currentStorageMachineId(d);
6109
+ d.run(`INSERT INTO storage_tombstones (
6110
+ id, object_type, object_id, deleted_at, updated_at, source_machine_id, payload, version
6111
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
6112
+ ON CONFLICT(object_type, object_id) DO UPDATE SET
6113
+ deleted_at = excluded.deleted_at,
6114
+ updated_at = excluded.updated_at,
6115
+ source_machine_id = excluded.source_machine_id,
6116
+ payload = excluded.payload,
6117
+ version = excluded.version
6118
+ WHERE storage_tombstones.updated_at IS NULL OR storage_tombstones.updated_at <= excluded.updated_at`, [
6119
+ uuid(),
6120
+ input.object_type,
6121
+ input.object_id,
6122
+ deletedAt,
6123
+ deletedAt,
6124
+ machineId,
6125
+ input.payload ? JSON.stringify(input.payload) : null,
6126
+ input.version ?? null
6127
+ ]);
6128
+ return getStorageTombstone(input.object_type, input.object_id, d);
6129
+ }
6130
+ function getStorageTombstone(objectType, objectId, db) {
6131
+ const d = db ?? getDatabase();
6132
+ const row = d.query("SELECT * FROM storage_tombstones WHERE object_type = ? AND object_id = ?").get(objectType, objectId);
6133
+ return row ? rowToStorageTombstone(row) : null;
6134
+ }
6135
+ function listStorageTombstones(db) {
6136
+ const d = db ?? getDatabase();
6137
+ return d.query("SELECT * FROM storage_tombstones ORDER BY updated_at ASC, object_type ASC, object_id ASC").all().map(rowToStorageTombstone);
6138
+ }
6139
+ function shouldApplyStorageTombstone(tombstone, existingUpdatedAt) {
6140
+ const tombstoneClock = Date.parse(tombstone.updated_at || tombstone.deleted_at);
6141
+ if (!existingUpdatedAt)
6142
+ return true;
6143
+ const existingClock = Date.parse(existingUpdatedAt);
6144
+ if (Number.isNaN(tombstoneClock))
6145
+ return true;
6146
+ if (Number.isNaN(existingClock))
6147
+ return true;
6148
+ return tombstoneClock >= existingClock;
6149
+ }
6150
+ function rowToStorageTombstone(row) {
6151
+ return {
6152
+ ...row,
6153
+ payload: parsePayload(row.payload)
6154
+ };
6155
+ }
6156
+ function parsePayload(value) {
6157
+ if (!value)
6158
+ return null;
6159
+ try {
6160
+ const parsed = JSON.parse(value);
6161
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
6162
+ } catch {
6163
+ return null;
6164
+ }
6165
+ }
6166
+ function currentStorageMachineId(db) {
6167
+ try {
6168
+ return getMachineId(db);
6169
+ } catch {
6170
+ return null;
6171
+ }
6172
+ }
6173
+
6174
+ // src/db/projects.ts
6071
6175
  function slugify(name) {
6072
6176
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
6073
6177
  }
@@ -6097,8 +6201,9 @@ function createProject(input, db) {
6097
6201
  const timestamp = now();
6098
6202
  const taskListId = input.task_list_id ?? `todos-${slugify(input.name)}`;
6099
6203
  const taskPrefix = input.task_prefix || generatePrefix(input.name, d);
6100
- d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at)
6101
- VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp, timestamp]);
6204
+ const machineId = currentStorageMachineId(d);
6205
+ d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at, machine_id)
6206
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp, timestamp, machineId]);
6102
6207
  return getProject(id, d);
6103
6208
  }
6104
6209
  function getProject(id, db) {
@@ -6177,6 +6282,14 @@ function renameProject(id, input, db) {
6177
6282
  }
6178
6283
  function deleteProject(id, db) {
6179
6284
  const d = db || getDatabase();
6285
+ const project = getProject(id, d);
6286
+ if (!project)
6287
+ return false;
6288
+ recordStorageTombstone({
6289
+ object_type: "projects",
6290
+ object_id: id,
6291
+ payload: project
6292
+ }, d);
6180
6293
  const result = d.run("DELETE FROM projects WHERE id = ?", [id]);
6181
6294
  return result.changes > 0;
6182
6295
  }
@@ -6281,6 +6394,14 @@ function listMachineLocalPaths(projectId, db) {
6281
6394
  function removeMachineLocalPath(projectId, machineId, db) {
6282
6395
  const d = db || getDatabase();
6283
6396
  const mid = machineId ?? getMachineId(d);
6397
+ const existing = d.query("SELECT * FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(projectId, mid);
6398
+ if (!existing)
6399
+ return false;
6400
+ recordStorageTombstone({
6401
+ object_type: "project_machine_paths",
6402
+ object_id: existing.id,
6403
+ payload: existing
6404
+ }, d);
6284
6405
  const result = d.run("DELETE FROM project_machine_paths WHERE project_id = ? AND machine_id = ?", [projectId, mid]);
6285
6406
  return result.changes > 0;
6286
6407
  }
@@ -6329,20 +6450,69 @@ function checkCompletionGuard(task, agentId, db, configOverride) {
6329
6450
  }
6330
6451
  }
6331
6452
 
6453
+ // src/lib/event-emission-safety.ts
6454
+ init_database();
6455
+ init_sync_utils();
6456
+ import { tmpdir } from "os";
6457
+ import { resolve as resolve3, sep } from "path";
6458
+ function envFlag(name) {
6459
+ const value = process.env[name];
6460
+ return value === "1" || value === "true" || value === "yes";
6461
+ }
6462
+ function isUnder(parent, child) {
6463
+ const normalizedParent = resolve3(parent);
6464
+ const normalizedChild = resolve3(child);
6465
+ return normalizedChild === normalizedParent || normalizedChild.startsWith(`${normalizedParent}${sep}`);
6466
+ }
6467
+ function databasePathFromDatabase(db) {
6468
+ const filename = db?.filename;
6469
+ return typeof filename === "string" && filename.trim() ? filename : undefined;
6470
+ }
6471
+ function isEphemeralTodosDatabase(dbPath) {
6472
+ const resolvedPath = dbPath ?? getDatabasePath();
6473
+ if (resolvedPath === ":memory:" || resolvedPath.startsWith("file::memory:"))
6474
+ return true;
6475
+ return isUnder(tmpdir(), resolvedPath);
6476
+ }
6477
+ function hasExplicitSharedEventsStore() {
6478
+ return Boolean(process.env["HASNA_EVENTS_DIR"] || process.env["HASNA_EVENTS_HOME"]);
6479
+ }
6480
+ function usesIsolatedTodosHome() {
6481
+ return isUnder(tmpdir(), getTodosGlobalDir());
6482
+ }
6483
+ function shouldEmitSharedTaskEvents(dbPath) {
6484
+ if (envFlag("TODOS_DISABLE_SHARED_EVENTS"))
6485
+ return false;
6486
+ if (envFlag("TODOS_ALLOW_EPHEMERAL_SHARED_EVENTS"))
6487
+ return true;
6488
+ if (!isEphemeralTodosDatabase(dbPath))
6489
+ return true;
6490
+ return hasExplicitSharedEventsStore();
6491
+ }
6492
+ function shouldDeliverLocalLifecycleHooks(dbPath) {
6493
+ if (envFlag("TODOS_DISABLE_LOCAL_EVENT_HOOKS"))
6494
+ return false;
6495
+ if (envFlag("TODOS_ALLOW_EPHEMERAL_LOCAL_EVENT_HOOKS"))
6496
+ return true;
6497
+ if (!isEphemeralTodosDatabase(dbPath))
6498
+ return true;
6499
+ return usesIsolatedTodosHome();
6500
+ }
6501
+
6332
6502
  // src/lib/event-hooks.ts
6333
6503
  init_redaction();
6334
6504
  import { createHash, randomUUID } from "crypto";
6335
6505
  import { appendFileSync, mkdirSync as mkdirSync3 } from "fs";
6336
- import { dirname as dirname4, resolve as resolve5 } from "path";
6506
+ import { dirname as dirname4, resolve as resolve6 } from "path";
6337
6507
  import { createConnection } from "net";
6338
6508
 
6339
6509
  // src/lib/runner-sandbox.ts
6340
6510
  init_config();
6341
- import { relative as relative2, resolve as resolve4 } from "path";
6511
+ import { relative as relative2, resolve as resolve5 } from "path";
6342
6512
 
6343
6513
  // src/lib/workspace-trust.ts
6344
6514
  init_config();
6345
- import { relative, resolve as resolve3 } from "path";
6515
+ import { relative, resolve as resolve4 } from "path";
6346
6516
  var DEFAULT_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
6347
6517
  var DEFAULT_ENV_REDACTIONS = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
6348
6518
  var PRESET_DEFAULTS = {
@@ -6388,7 +6558,7 @@ var PRESET_DEFAULTS = {
6388
6558
  }
6389
6559
  };
6390
6560
  function normalizePath(path) {
6391
- return resolve3(path);
6561
+ return resolve4(path);
6392
6562
  }
6393
6563
  function unique2(values) {
6394
6564
  return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
@@ -6525,7 +6695,7 @@ function checkWorkspacePermission(input = {}) {
6525
6695
  var DEFAULT_COMMAND_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
6526
6696
  var DEFAULT_ENV_REDACTIONS2 = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
6527
6697
  function normalizePath2(path) {
6528
- return resolve4(path);
6698
+ return resolve5(path);
6529
6699
  }
6530
6700
  function unique3(values) {
6531
6701
  return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
@@ -6871,7 +7041,7 @@ async function deliverHook(hook, envelope) {
6871
7041
  if (hook.target === "stdout") {
6872
7042
  output = line.trim();
6873
7043
  } else if (hook.target === "file") {
6874
- const filePath = resolve5(hook.file_path);
7044
+ const filePath = resolve6(hook.file_path);
6875
7045
  mkdirSync3(dirname4(filePath), { recursive: true });
6876
7046
  appendFileSync(filePath, line);
6877
7047
  } else if (hook.target === "socket") {
@@ -6946,6 +7116,8 @@ async function emitLocalEventHooks(input) {
6946
7116
  return Promise.all(hooks.map((hook) => deliverHook(hook, envelope)));
6947
7117
  }
6948
7118
  function emitLocalEventHooksQuiet(input) {
7119
+ if (!shouldDeliverLocalLifecycleHooks(input.databasePath))
7120
+ return;
6949
7121
  emitLocalEventHooks(input).catch(() => {});
6950
7122
  }
6951
7123
  async function testLocalEventHook(name, input) {
@@ -7262,7 +7434,7 @@ async function dispatchCommand(event, channel) {
7262
7434
  HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
7263
7435
  HASNA_EVENT_JSON: eventJson
7264
7436
  };
7265
- return new Promise((resolve6) => {
7437
+ return new Promise((resolve7) => {
7266
7438
  const child = spawn(channel.command.command, channel.command.args ?? [], {
7267
7439
  cwd: channel.command.cwd,
7268
7440
  env,
@@ -7280,7 +7452,7 @@ async function dispatchCommand(event, channel) {
7280
7452
  });
7281
7453
  child.on("error", (error) => {
7282
7454
  clearTimeout(timeout);
7283
- resolve6({
7455
+ resolve7({
7284
7456
  attempt: 1,
7285
7457
  status: "failed",
7286
7458
  startedAt,
@@ -7293,7 +7465,7 @@ async function dispatchCommand(event, channel) {
7293
7465
  child.on("close", (code, signal) => {
7294
7466
  clearTimeout(timeout);
7295
7467
  const success = code === 0;
7296
- resolve6({
7468
+ resolve7({
7297
7469
  attempt: 1,
7298
7470
  status: success ? "success" : "failed",
7299
7471
  startedAt,
@@ -7561,14 +7733,15 @@ function createTaskList(input, db) {
7561
7733
  const id = uuid();
7562
7734
  const timestamp = now();
7563
7735
  const slug = input.slug || slugify(input.name);
7736
+ const machineId = currentStorageMachineId(d);
7564
7737
  if (!input.project_id) {
7565
7738
  const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
7566
7739
  if (existing) {
7567
7740
  throw new Error(`Standalone task list with slug "${slug}" already exists`);
7568
7741
  }
7569
7742
  }
7570
- d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
7571
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
7743
+ d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at, machine_id)
7744
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp, machineId]);
7572
7745
  return getTaskList(id, d);
7573
7746
  }
7574
7747
  function getTaskList(id, db) {
@@ -7618,6 +7791,14 @@ function updateTaskList(id, input, db) {
7618
7791
  }
7619
7792
  function deleteTaskList(id, db) {
7620
7793
  const d = db || getDatabase();
7794
+ const list = getTaskList(id, d);
7795
+ if (!list)
7796
+ return false;
7797
+ recordStorageTombstone({
7798
+ object_type: "task_lists",
7799
+ object_id: id,
7800
+ payload: list
7801
+ }, d);
7621
7802
  return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
7622
7803
  }
7623
7804
  function ensureTaskList(name, slug, projectId, db) {
@@ -7628,39 +7809,9 @@ function ensureTaskList(name, slug, projectId, db) {
7628
7809
  return createTaskList({ name, slug, project_id: projectId }, d);
7629
7810
  }
7630
7811
 
7631
- // src/lib/shared-events.ts
7632
- var SOURCE = "todos";
7633
- function taskEventData(task, extra = {}) {
7634
- return {
7635
- id: task.id,
7636
- task_id: task.id,
7637
- short_id: task.short_id,
7638
- title: task.title,
7639
- description: task.description,
7640
- status: task.status,
7641
- priority: task.priority,
7642
- project_id: task.project_id,
7643
- parent_id: task.parent_id,
7644
- plan_id: task.plan_id,
7645
- task_list_id: task.task_list_id,
7646
- agent_id: task.agent_id,
7647
- assigned_to: task.assigned_to,
7648
- session_id: task.session_id,
7649
- working_dir: task.working_dir,
7650
- tags: task.tags,
7651
- metadata: task.metadata,
7652
- version: task.version,
7653
- created_at: task.created_at,
7654
- updated_at: task.updated_at,
7655
- started_at: task.started_at,
7656
- completed_at: task.completed_at,
7657
- due_at: task.due_at,
7658
- requires_approval: task.requires_approval,
7659
- approved_by: task.approved_by,
7660
- approved_at: task.approved_at,
7661
- ...extra
7662
- };
7663
- }
7812
+ // src/lib/task-route-contract.ts
7813
+ var TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION = "todos.task_route_state.v1";
7814
+ var TASK_WORKFLOW_POINTER_SCHEMA_VERSION = "todos.task_workflow_pointer.v1";
7664
7815
  function booleanField(value) {
7665
7816
  if (typeof value === "boolean")
7666
7817
  return value;
@@ -7682,21 +7833,31 @@ function booleanField(value) {
7682
7833
  function objectField(value) {
7683
7834
  return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
7684
7835
  }
7685
- function firstBoolean(records, keys) {
7836
+ function collectBooleans(records, keys) {
7837
+ const values = [];
7686
7838
  for (const record of records) {
7839
+ if (!record)
7840
+ continue;
7687
7841
  for (const key of keys) {
7688
7842
  const value = booleanField(record[key]);
7689
7843
  if (value !== undefined)
7690
- return value;
7844
+ values.push(value);
7691
7845
  }
7692
7846
  }
7693
- return;
7847
+ return values;
7694
7848
  }
7695
- function routingAutomationMetadata(task) {
7696
- const automation = objectField(task.metadata.automation);
7697
- const records = [task.metadata];
7698
- if (automation)
7699
- records.push(automation);
7849
+ function mergedBoolean(records, keys, trueWins) {
7850
+ const values = collectBooleans(records, keys);
7851
+ if (values.length === 0)
7852
+ return;
7853
+ if (trueWins)
7854
+ return values.some(Boolean);
7855
+ return values.includes(false) ? false : true;
7856
+ }
7857
+ function routingAutomationMetadata(task, taskList) {
7858
+ const taskAutomation = objectField(task.metadata.automation);
7859
+ const taskListAutomation = taskList ? objectField(taskList.metadata.automation) : undefined;
7860
+ const records = [task.metadata, taskAutomation, taskList?.metadata, taskListAutomation];
7700
7861
  const result = {};
7701
7862
  const aliases = [
7702
7863
  ["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
@@ -7707,7 +7868,7 @@ function routingAutomationMetadata(task) {
7707
7868
  ["approval_required", ["approval_required", "approvalRequired"]]
7708
7869
  ];
7709
7870
  for (const [canonical, keys] of aliases) {
7710
- const value = firstBoolean(records, keys);
7871
+ const value = mergedBoolean(records, keys, canonical !== "allowed");
7711
7872
  if (value !== undefined)
7712
7873
  result[canonical] = value;
7713
7874
  }
@@ -7715,23 +7876,91 @@ function routingAutomationMetadata(task) {
7715
7876
  result.requires_approval = true;
7716
7877
  return Object.keys(result).length > 0 ? result : undefined;
7717
7878
  }
7879
+ function routeEnabledForTask(task, taskList) {
7880
+ const explicit = booleanField(task.metadata.route_enabled);
7881
+ if (explicit !== undefined)
7882
+ return explicit;
7883
+ if (task.tags.includes("auto:route") || task.tags.includes("route:enabled"))
7884
+ return true;
7885
+ const taskListDefault = taskList ? booleanField(taskList.metadata.route_enabled) : undefined;
7886
+ if (taskListDefault !== undefined)
7887
+ return taskListDefault;
7888
+ return;
7889
+ }
7890
+ function stringField(value) {
7891
+ return typeof value === "string" && value.trim() ? value : undefined;
7892
+ }
7893
+ function workflowPointersFromMetadata(metadata) {
7894
+ const nested = objectField(metadata.workflow_invocation) ?? objectField(metadata.workflow) ?? {};
7895
+ return {
7896
+ current_workflow_invocation_id: stringField(metadata.current_workflow_invocation_id) ?? stringField(nested.current_workflow_invocation_id) ?? stringField(nested.invocation_id),
7897
+ current_run_id: stringField(metadata.current_run_id) ?? stringField(nested.current_run_id) ?? stringField(nested.run_id),
7898
+ latest_manifest_path: stringField(metadata.latest_manifest_path) ?? stringField(nested.latest_manifest_path) ?? stringField(nested.manifest_path),
7899
+ latest_evaluation_path: stringField(metadata.latest_evaluation_path) ?? stringField(nested.latest_evaluation_path) ?? stringField(nested.evaluation_path),
7900
+ workflow_state: stringField(metadata.workflow_state) ?? stringField(nested.workflow_state) ?? stringField(nested.state)
7901
+ };
7902
+ }
7903
+ function compactWorkflowPointers(pointers) {
7904
+ return Object.fromEntries(Object.entries(pointers).filter(([, value]) => typeof value === "string" && value.length > 0));
7905
+ }
7906
+ function classifyProjectKind(path) {
7907
+ return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
7908
+ }
7909
+ function isWorktreePath(path) {
7910
+ return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
7911
+ }
7912
+ function inferRootProjectId(project) {
7913
+ return isWorktreePath(project.path) ? null : project.id;
7914
+ }
7915
+
7916
+ // src/lib/shared-events.ts
7917
+ var SOURCE = "todos";
7918
+ function taskEventData(task, extra = {}) {
7919
+ return {
7920
+ id: task.id,
7921
+ task_id: task.id,
7922
+ short_id: task.short_id,
7923
+ title: task.title,
7924
+ description: task.description,
7925
+ status: task.status,
7926
+ priority: task.priority,
7927
+ project_id: task.project_id,
7928
+ parent_id: task.parent_id,
7929
+ plan_id: task.plan_id,
7930
+ task_list_id: task.task_list_id,
7931
+ agent_id: task.agent_id,
7932
+ assigned_to: task.assigned_to,
7933
+ session_id: task.session_id,
7934
+ working_dir: task.working_dir,
7935
+ tags: task.tags,
7936
+ metadata: task.metadata,
7937
+ version: task.version,
7938
+ created_at: task.created_at,
7939
+ updated_at: task.updated_at,
7940
+ started_at: task.started_at,
7941
+ completed_at: task.completed_at,
7942
+ due_at: task.due_at,
7943
+ requires_approval: task.requires_approval,
7944
+ approved_by: task.approved_by,
7945
+ approved_at: task.approved_at,
7946
+ ...extra
7947
+ };
7948
+ }
7718
7949
  function taskEventMetadata(task) {
7719
7950
  const metadata = {
7720
7951
  package: "@hasna/todos",
7721
7952
  todos_event_schema_version: 1,
7953
+ route_state_schema_version: TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION,
7722
7954
  task_id: task.id,
7723
7955
  task_short_id: task.short_id,
7724
7956
  project_id: task.project_id,
7725
7957
  task_list_id: task.task_list_id,
7726
7958
  working_dir: task.working_dir
7727
7959
  };
7728
- const routeEnabled = booleanField(task.metadata.route_enabled);
7729
- if (routeEnabled !== undefined) {
7730
- metadata.route_enabled = routeEnabled;
7731
- }
7732
- const automation = routingAutomationMetadata(task);
7733
- if (automation) {
7734
- metadata.automation = automation;
7960
+ const pointers = workflowPointersFromMetadata(task.metadata);
7961
+ for (const [key, value] of Object.entries(pointers)) {
7962
+ if (value)
7963
+ metadata[key] = value;
7735
7964
  }
7736
7965
  try {
7737
7966
  const project = task.project_id ? getProject(task.project_id) : null;
@@ -7760,18 +7989,20 @@ function taskEventMetadata(task) {
7760
7989
  metadata.task_list_project_id = taskList.project_id;
7761
7990
  metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
7762
7991
  }
7992
+ const routeEnabled = routeEnabledForTask(task, taskList);
7993
+ if (routeEnabled !== undefined) {
7994
+ metadata.route_enabled = routeEnabled;
7995
+ }
7996
+ const automation = routingAutomationMetadata(task, taskList);
7997
+ if (automation) {
7998
+ metadata.automation = automation;
7999
+ metadata.route_blocked_by_no_auto = automation.no_auto === true;
8000
+ metadata.route_blocked_by_manual = automation.manual === true || automation.manual_required === true;
8001
+ metadata.route_blocked_by_approval = (automation.requires_approval === true || automation.approval_required === true) && !task.approved_by;
8002
+ }
7763
8003
  } catch {}
7764
8004
  return metadata;
7765
8005
  }
7766
- function classifyProjectKind(path) {
7767
- return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
7768
- }
7769
- function isWorktreePath(path) {
7770
- return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
7771
- }
7772
- function inferRootProjectId(project) {
7773
- return isWorktreePath(project.path) ? null : project.id;
7774
- }
7775
8006
  function readMachineLocalPath(project) {
7776
8007
  const machineId = process.env["TODOS_MACHINE_ID"];
7777
8008
  if (!machineId)
@@ -7784,6 +8015,8 @@ function readMachineLocalPath(project) {
7784
8015
  }
7785
8016
  }
7786
8017
  async function emitSharedTaskEvent(input) {
8018
+ if (!shouldEmitSharedTaskEvents(input.databasePath))
8019
+ return;
7787
8020
  const data = taskEventData(input.task, input.data);
7788
8021
  await new EventsClient().emit({
7789
8022
  source: SOURCE,
@@ -7808,8 +8041,9 @@ function logTaskChange(taskId, action, field2, oldValue, newValue, agentId, db)
7808
8041
  const d = db || getDatabase();
7809
8042
  const id = uuid();
7810
8043
  const timestamp = now();
7811
- d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at)
7812
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field2 || null, oldValue ?? null, newValue ?? null, agentId || null, timestamp]);
8044
+ const machineId = currentStorageMachineId(d);
8045
+ d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at, machine_id)
8046
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field2 || null, oldValue ?? null, newValue ?? null, agentId || null, timestamp, machineId]);
7813
8047
  try {
7814
8048
  const { logActivity: logActivity2 } = (init_activity_audit(), __toCommonJS(exports_activity_audit));
7815
8049
  logActivity2({
@@ -7822,7 +8056,7 @@ function logTaskChange(taskId, action, field2, oldValue, newValue, agentId, db)
7822
8056
  actor_id: agentId ?? undefined
7823
8057
  }, d);
7824
8058
  } catch {}
7825
- return { id, task_id: taskId, action, field: field2 || null, old_value: oldValue ?? null, new_value: newValue ?? null, agent_id: agentId || null, created_at: timestamp };
8059
+ return { id, task_id: taskId, action, field: field2 || null, old_value: oldValue ?? null, new_value: newValue ?? null, agent_id: agentId || null, created_at: timestamp, machine_id: machineId };
7826
8060
  }
7827
8061
  function getTaskHistory(taskId, db) {
7828
8062
  const d = db || getDatabase();
@@ -8171,13 +8405,14 @@ function createTask(input, db) {
8171
8405
  const d = db || getDatabase();
8172
8406
  const timestamp = now();
8173
8407
  const tags = input.tags || [];
8408
+ const machineId = currentStorageMachineId(d);
8174
8409
  const assignedBy = input.assigned_by || input.agent_id;
8175
8410
  const assignedFromProject = input.assigned_from_project || null;
8176
8411
  let id = uuid();
8177
8412
  for (let attempt = 0;attempt < 3; attempt++) {
8178
8413
  try {
8179
- 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)
8180
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
8414
+ 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)
8415
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
8181
8416
  id,
8182
8417
  null,
8183
8418
  input.project_id || null,
@@ -8214,7 +8449,8 @@ function createTask(input, db) {
8214
8449
  input.spawned_from_session || null,
8215
8450
  assignedBy || null,
8216
8451
  assignedFromProject || null,
8217
- input.task_type || null
8452
+ input.task_type || null,
8453
+ machineId
8218
8454
  ]);
8219
8455
  break;
8220
8456
  } catch (e) {
@@ -8230,9 +8466,10 @@ function createTask(input, db) {
8230
8466
  }
8231
8467
  const task = getTask(id, d);
8232
8468
  const payload = taskEventData(task);
8469
+ const databasePath = databasePathFromDatabase(d);
8233
8470
  dispatchWebhook2("task.created", payload, d).catch(() => {});
8234
- emitLocalEventHooksQuiet({ type: "task.created", payload });
8235
- emitSharedTaskEventQuiet({ type: "task.created", task });
8471
+ emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
8472
+ emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
8236
8473
  return task;
8237
8474
  }
8238
8475
  function getTask(id, db) {
@@ -8650,29 +8887,39 @@ function updateTask(id, input, db) {
8650
8887
  approved_by: input.approved_by ?? task.approved_by,
8651
8888
  approved_at: input.approved_by ? timestamp : task.approved_at
8652
8889
  };
8890
+ const databasePath = databasePathFromDatabase(d);
8653
8891
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
8654
8892
  const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
8655
8893
  dispatchWebhook2("task.assigned", payload, d).catch(() => {});
8656
- emitLocalEventHooksQuiet({ type: "task.assigned", payload });
8657
- emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to } });
8894
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
8895
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to }, databasePath });
8658
8896
  }
8659
8897
  if (input.status !== undefined && input.status !== task.status) {
8660
8898
  const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
8661
8899
  dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
8662
- emitLocalEventHooksQuiet({ type: "task.status_changed", payload });
8663
- emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status } });
8900
+ emitLocalEventHooksQuiet({ type: "task.status_changed", payload, databasePath });
8901
+ emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status }, databasePath });
8664
8902
  }
8665
8903
  if (input.approved_by !== undefined) {
8666
- emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
8904
+ emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title }, databasePath });
8667
8905
  }
8668
8906
  const updatePayload = taskEventData(updatedTask);
8669
8907
  dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
8670
- emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
8671
- emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
8908
+ emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
8909
+ emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
8672
8910
  return updatedTask;
8673
8911
  }
8674
8912
  function deleteTask(id, db) {
8675
8913
  const d = db || getDatabase();
8914
+ const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
8915
+ if (!row)
8916
+ return false;
8917
+ recordStorageTombstone({
8918
+ object_type: "tasks",
8919
+ object_id: id,
8920
+ payload: rowToTask(row),
8921
+ version: row.version
8922
+ }, d);
8676
8923
  const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
8677
8924
  return result.changes > 0;
8678
8925
  }
@@ -8804,8 +9051,9 @@ function resolveTemplateId(id, d) {
8804
9051
  function createTemplate(input, db) {
8805
9052
  const d = db || getDatabase();
8806
9053
  const id = uuid();
8807
- d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at)
8808
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
9054
+ const machineId = currentStorageMachineId(d);
9055
+ d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at, machine_id)
9056
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
8809
9057
  id,
8810
9058
  input.name,
8811
9059
  input.title_pattern,
@@ -8816,7 +9064,8 @@ function createTemplate(input, db) {
8816
9064
  input.project_id || null,
8817
9065
  input.plan_id || null,
8818
9066
  JSON.stringify(input.metadata || {}),
8819
- now()
9067
+ now(),
9068
+ machineId
8820
9069
  ]);
8821
9070
  if (input.tasks && input.tasks.length > 0) {
8822
9071
  addTemplateTasks(id, input.tasks, d);
@@ -8840,6 +9089,15 @@ function deleteTemplate(id, db) {
8840
9089
  const resolved = resolveTemplateId(id, d);
8841
9090
  if (!resolved)
8842
9091
  return false;
9092
+ const template = getTemplate(resolved, d);
9093
+ if (!template)
9094
+ return false;
9095
+ recordStorageTombstone({
9096
+ object_type: "templates",
9097
+ object_id: resolved,
9098
+ payload: template,
9099
+ version: template.version
9100
+ }, d);
8843
9101
  return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
8844
9102
  }
8845
9103
  function updateTemplate(id, updates, db) {
@@ -9369,6 +9627,7 @@ function getBlockingDeps(id, db) {
9369
9627
  }
9370
9628
  function startTask(id, agentId, db) {
9371
9629
  const d = db || getDatabase();
9630
+ const databasePath = databasePathFromDatabase(d);
9372
9631
  const task = getTask(id, d);
9373
9632
  if (!task)
9374
9633
  throw new TaskNotFoundError(id);
@@ -9383,7 +9642,8 @@ function startTask(id, agentId, db) {
9383
9642
  agent_id: agentId,
9384
9643
  title: task.title,
9385
9644
  blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
9386
- }
9645
+ },
9646
+ databasePath
9387
9647
  });
9388
9648
  throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
9389
9649
  }
@@ -9405,12 +9665,13 @@ function startTask(id, agentId, db) {
9405
9665
  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 };
9406
9666
  const payload = taskEventData(startedTask, { agent_id: agentId });
9407
9667
  dispatchWebhook2("task.started", payload, d).catch(() => {});
9408
- emitLocalEventHooksQuiet({ type: "task.started", payload });
9409
- emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId } });
9668
+ emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
9669
+ emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId }, databasePath });
9410
9670
  return startedTask;
9411
9671
  }
9412
9672
  function completeTask(id, agentId, db, options) {
9413
9673
  const d = db || getDatabase();
9674
+ const databasePath = databasePathFromDatabase(d);
9414
9675
  const task = getTask(id, d);
9415
9676
  if (!task)
9416
9677
  throw new TaskNotFoundError(id);
@@ -9456,8 +9717,8 @@ function completeTask(id, agentId, db, options) {
9456
9717
  };
9457
9718
  const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
9458
9719
  dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
9459
- emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload });
9460
- emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp } });
9720
+ emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
9721
+ emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
9461
9722
  let spawnedTask = null;
9462
9723
  if (task.recurrence_rule && !options?.skip_recurrence) {
9463
9724
  spawnedTask = spawnNextRecurrence(task, d, timestamp);
@@ -9501,9 +9762,9 @@ function completeTask(id, agentId, db, options) {
9501
9762
  const depTask = getTask(dep.id, d);
9502
9763
  const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
9503
9764
  dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
9504
- emitLocalEventHooksQuiet({ type: "task.unblocked", payload });
9765
+ emitLocalEventHooksQuiet({ type: "task.unblocked", payload, databasePath });
9505
9766
  if (depTask)
9506
- emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id } });
9767
+ emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id }, databasePath });
9507
9768
  }
9508
9769
  }
9509
9770
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
@@ -9673,6 +9934,7 @@ function getTasksChangedSince(since, filters, db) {
9673
9934
  }
9674
9935
  function failTask(id, agentId, reason, options, db) {
9675
9936
  const d = db || getDatabase();
9937
+ const databasePath = databasePathFromDatabase(d);
9676
9938
  const task = getTask(id, d);
9677
9939
  if (!task)
9678
9940
  throw new TaskNotFoundError(id);
@@ -9701,8 +9963,8 @@ function failTask(id, agentId, reason, options, db) {
9701
9963
  logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
9702
9964
  const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
9703
9965
  dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
9704
- emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload });
9705
- emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning" });
9966
+ emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload, databasePath });
9967
+ emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning", databasePath });
9706
9968
  let retryTask;
9707
9969
  if (options?.retry) {
9708
9970
  const retryCount = (task.retry_count || 0) + 1;
@@ -9762,6 +10024,7 @@ function getStaleTasks(staleQuery = 30, filters, db) {
9762
10024
  }
9763
10025
  function stealTask(agentId, opts, db) {
9764
10026
  const d = db || getDatabase();
10027
+ const databasePath = databasePathFromDatabase(d);
9765
10028
  const staleMinutes = opts?.stale_minutes ?? 30;
9766
10029
  const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
9767
10030
  if (staleTasks.length === 0)
@@ -9780,8 +10043,8 @@ function stealTask(agentId, opts, db) {
9780
10043
  const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
9781
10044
  const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
9782
10045
  dispatchWebhook2("task.assigned", payload, d).catch(() => {});
9783
- emitLocalEventHooksQuiet({ type: "task.assigned", payload });
9784
- emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to } });
10046
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
10047
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to }, databasePath });
9785
10048
  return stolenTask;
9786
10049
  }
9787
10050
  function claimOrSteal(agentId, filters, db) {
@@ -10372,8 +10635,9 @@ function createPlan(input, db) {
10372
10635
  const d = db || getDatabase();
10373
10636
  const id = uuid();
10374
10637
  const timestamp = now();
10375
- d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at)
10376
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
10638
+ const machineId = currentStorageMachineId(d);
10639
+ d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at, machine_id)
10640
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
10377
10641
  id,
10378
10642
  input.project_id || null,
10379
10643
  input.task_list_id || null,
@@ -10382,7 +10646,8 @@ function createPlan(input, db) {
10382
10646
  input.description || null,
10383
10647
  input.status || "active",
10384
10648
  timestamp,
10385
- timestamp
10649
+ timestamp,
10650
+ machineId
10386
10651
  ]);
10387
10652
  return getPlan(id, d);
10388
10653
  }
@@ -10430,12 +10695,21 @@ function updatePlan(id, input, db) {
10430
10695
  const updated = getPlan(id, d);
10431
10696
  emitLocalEventHooksQuiet({
10432
10697
  type: "plan.updated",
10433
- payload: { id, old_status: plan.status, new_status: updated.status, name: updated.name, project_id: updated.project_id }
10698
+ payload: { id, old_status: plan.status, new_status: updated.status, name: updated.name, project_id: updated.project_id },
10699
+ databasePath: databasePathFromDatabase(d)
10434
10700
  });
10435
10701
  return updated;
10436
10702
  }
10437
10703
  function deletePlan(id, db) {
10438
10704
  const d = db || getDatabase();
10705
+ const plan = getPlan(id, d);
10706
+ if (!plan)
10707
+ return false;
10708
+ recordStorageTombstone({
10709
+ object_type: "plans",
10710
+ object_id: id,
10711
+ payload: plan
10712
+ }, d);
10439
10713
  const result = d.run("DELETE FROM plans WHERE id = ?", [id]);
10440
10714
  return result.changes > 0;
10441
10715
  }
@@ -10842,20 +11116,20 @@ init_database();
10842
11116
  init_redaction();
10843
11117
  import { createHash as createHash2 } from "crypto";
10844
11118
  import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
10845
- import { basename, dirname as dirname5, join as join6, resolve as resolve6 } from "path";
10846
- import { tmpdir } from "os";
11119
+ import { basename, dirname as dirname5, join as join6, resolve as resolve7 } from "path";
11120
+ import { tmpdir as tmpdir2 } from "os";
10847
11121
  function isInMemoryDb2(path) {
10848
11122
  return path === ":memory:" || path.startsWith("file::memory:");
10849
11123
  }
10850
11124
  function artifactStoreRoot() {
10851
11125
  if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
10852
- return resolve6(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
11126
+ return resolve7(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
10853
11127
  if (process.env["TODOS_ARTIFACTS_DIR"])
10854
- return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
11128
+ return resolve7(process.env["TODOS_ARTIFACTS_DIR"]);
10855
11129
  const dbPath = getDatabasePath();
10856
11130
  if (isInMemoryDb2(dbPath))
10857
- return join6(tmpdir(), "hasna-todos-artifacts");
10858
- return join6(dirname5(resolve6(dbPath)), "artifacts");
11131
+ return join6(tmpdir2(), "hasna-todos-artifacts");
11132
+ return join6(dirname5(resolve7(dbPath)), "artifacts");
10859
11133
  }
10860
11134
  function artifactStorePath(relativePath) {
10861
11135
  const normalized = relativePath.replace(/\\/g, "/");
@@ -10902,7 +11176,7 @@ function mediaTypeFor(path, textLike) {
10902
11176
  return "application/octet-stream";
10903
11177
  }
10904
11178
  function storeArtifactContent(input) {
10905
- const sourcePath = resolve6(input.path);
11179
+ const sourcePath = resolve7(input.path);
10906
11180
  if (!existsSync8(sourcePath))
10907
11181
  return null;
10908
11182
  const sourceStat = statSync2(sourcePath);
@@ -11058,19 +11332,19 @@ function importStoredArtifactContent(content) {
11058
11332
  var DEFAULT_DELETED_RETENTION_DAYS = 30;
11059
11333
  function getArtifactStoreRoot(dbPath) {
11060
11334
  if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
11061
- return resolve6(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
11335
+ return resolve7(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
11062
11336
  if (process.env["TODOS_ARTIFACTS_DIR"])
11063
- return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
11337
+ return resolve7(process.env["TODOS_ARTIFACTS_DIR"]);
11064
11338
  const path = dbPath ?? getDatabasePath();
11065
11339
  if (isInMemoryDb2(path))
11066
- return join6(tmpdir(), "hasna-todos-artifacts");
11067
- return join6(dirname5(resolve6(path)), "artifacts");
11340
+ return join6(tmpdir2(), "hasna-todos-artifacts");
11341
+ return join6(dirname5(resolve7(path)), "artifacts");
11068
11342
  }
11069
11343
  function computeContentHash(path) {
11070
- return sha256(readFileSync4(resolve6(path)));
11344
+ return sha256(readFileSync4(resolve7(path)));
11071
11345
  }
11072
11346
  function storeArtifactFile(input) {
11073
- const sourcePath = resolve6(input.sourcePath);
11347
+ const sourcePath = resolve7(input.sourcePath);
11074
11348
  if (!existsSync8(sourcePath)) {
11075
11349
  throw new Error(`Source file not found: ${input.sourcePath}`);
11076
11350
  }
@@ -11119,7 +11393,7 @@ function buildArtifactExportManifest(artifacts, dbPath) {
11119
11393
  };
11120
11394
  }
11121
11395
  function writeArtifactExportManifest(manifest, outputPath) {
11122
- const destination = resolve6(outputPath);
11396
+ const destination = resolve7(outputPath);
11123
11397
  mkdirSync4(dirname5(destination), { recursive: true });
11124
11398
  writeFileSync2(destination, `${JSON.stringify(manifest, null, 2)}
11125
11399
  `);
@@ -11506,7 +11780,11 @@ function startTaskRun(input, db) {
11506
11780
  }, d);
11507
11781
  }
11508
11782
  const run = getTaskRun(id, d);
11509
- emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
11783
+ emitLocalEventHooksQuiet({
11784
+ type: "run.started",
11785
+ payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title },
11786
+ databasePath: databasePathFromDatabase(d)
11787
+ });
11510
11788
  return run;
11511
11789
  }
11512
11790
  function beginTaskRunTransaction(input, db) {
@@ -11775,7 +12053,8 @@ function finishTaskRun(input, db) {
11775
12053
  const updated = getTaskRun(run.id, d);
11776
12054
  emitLocalEventHooksQuiet({
11777
12055
  type: `run.${input.status}`,
11778
- payload: { id: updated.id, task_id: updated.task_id, agent_id: updated.agent_id, status: updated.status, summary: updated.summary, completed_at: timestamp }
12056
+ payload: { id: updated.id, task_id: updated.task_id, agent_id: updated.agent_id, status: updated.status, summary: updated.summary, completed_at: timestamp },
12057
+ databasePath: databasePathFromDatabase(d)
11779
12058
  });
11780
12059
  return updated;
11781
12060
  }
@@ -13109,7 +13388,7 @@ function importOnboardingFixture(options = {}) {
13109
13388
  init_database();
13110
13389
  import { createHash as createHash3 } from "crypto";
13111
13390
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
13112
- import { dirname as dirname6, resolve as resolve7 } from "path";
13391
+ import { dirname as dirname6, resolve as resolve8 } from "path";
13113
13392
  import { mkdirSync as mkdirSync6 } from "fs";
13114
13393
  var TODOS_LOCAL_BACKUP_KIND = "hasna.todos.local-backup";
13115
13394
  var TODOS_LOCAL_BACKUP_SCHEMA_VERSION = 1;
@@ -13216,14 +13495,14 @@ function createLocalBackup(options = {}, db) {
13216
13495
  return backup;
13217
13496
  }
13218
13497
  function writeLocalBackupFile(backup, outputPath) {
13219
- const path = resolve7(outputPath);
13498
+ const path = resolve8(outputPath);
13220
13499
  mkdirSync6(dirname6(path), { recursive: true });
13221
13500
  writeFileSync4(path, `${JSON.stringify(backup, null, 2)}
13222
13501
  `);
13223
13502
  return path;
13224
13503
  }
13225
13504
  function readLocalBackupFile(path) {
13226
- return JSON.parse(readFileSync5(resolve7(path), "utf-8"));
13505
+ return JSON.parse(readFileSync5(resolve8(path), "utf-8"));
13227
13506
  }
13228
13507
  function verifyLocalBackup(value, options = {}, db) {
13229
13508
  const verifiedAt = options.verified_at ?? now();
@@ -16191,7 +16470,8 @@ function writeQueue(task2, queue, actor, action, db) {
16191
16470
  logTaskChange(task2.id, `review_queue.${action}`, "review_queue", null, JSON.stringify(queue), actor, d);
16192
16471
  emitLocalEventHooksQuiet({
16193
16472
  type: `review.${action}`,
16194
- payload: { task_id: task2.id, queue: queue.queue, state: queue.state, reviewer: queue.reviewer, claimed_by: queue.claimed_by }
16473
+ payload: { task_id: task2.id, queue: queue.queue, state: queue.state, reviewer: queue.reviewer, claimed_by: queue.claimed_by },
16474
+ databasePath: databasePathFromDatabase(d)
16195
16475
  });
16196
16476
  return itemFromTask(taskOrThrow2(task2.id, d), queue, d);
16197
16477
  }
@@ -16872,7 +17152,7 @@ function canonicalize(value) {
16872
17152
  function hash(value) {
16873
17153
  return createHash5("sha256").update(value).digest("hex");
16874
17154
  }
16875
- function parsePayload(value) {
17155
+ function parsePayload2(value) {
16876
17156
  if (!value)
16877
17157
  return {};
16878
17158
  try {
@@ -16971,7 +17251,7 @@ function taskScopedRows(db, scope) {
16971
17251
  FROM handoffs h
16972
17252
  WHERE h.project_id IS NULL OR h.project_id = COALESCE(?, h.project_id)
16973
17253
  `).all(scope.project_id ?? null).filter((row) => {
16974
- const payload = parsePayload(row.payload_json);
17254
+ const payload = parsePayload2(row.payload_json);
16975
17255
  const taskRefs = parseStringArray(payload["task_ids"]);
16976
17256
  const runRefs = parseStringArray(payload["run_ids"]);
16977
17257
  if (scope.project_id && row.project_id !== scope.project_id)
@@ -17002,7 +17282,7 @@ function toLedgerEntries(rows) {
17002
17282
  });
17003
17283
  let previous = LOCAL_AUDIT_LEDGER_INITIAL_HASH;
17004
17284
  return ordered.map((row, index) => {
17005
- const payload = parsePayload(row.payload_json);
17285
+ const payload = parsePayload2(row.payload_json);
17006
17286
  const payloadHash = hash(canonicalize({ source: row.source, source_id: row.source_id, created_at: row.created_at, payload }));
17007
17287
  const chainHash = hash(`${previous}
17008
17288
  ${payloadHash}`);
@@ -17137,7 +17417,7 @@ function renderLocalAuditLedgerMarkdown(ledger) {
17137
17417
  init_migrations();
17138
17418
  init_schema();
17139
17419
  import { readFileSync as readFileSync6 } from "fs";
17140
- import { join as join9, resolve as resolve8 } from "path";
17420
+ import { join as join9, resolve as resolve9 } from "path";
17141
17421
  import { Database as Database2 } from "bun:sqlite";
17142
17422
  var LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION = 1;
17143
17423
  var EXPECTED_PACKAGE_NAME = "@hasna/todos";
@@ -17279,7 +17559,7 @@ function checkChangelog() {
17279
17559
  ];
17280
17560
  }
17281
17561
  function createReleaseCompatibilityReport(options = {}) {
17282
- const root = resolve8(options.root ?? process.cwd());
17562
+ const root = resolve9(options.root ?? process.cwd());
17283
17563
  const packageJson = readPackageJson(root);
17284
17564
  const simulatedLevels = options.simulated_levels ?? defaultSimulationLevels();
17285
17565
  const checks = [