@hasna/todos 0.15.35 → 0.15.41

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 (85) hide show
  1. package/dist/cli/cloud-router.d.ts +52 -18
  2. package/dist/cli/cloud-router.d.ts.map +1 -1
  3. package/dist/cli/commands/agent-commands.d.ts.map +1 -1
  4. package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
  5. package/dist/cli/commands/project-commands.d.ts.map +1 -1
  6. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  7. package/dist/cli/commands/task-subtree-transfer-commands.d.ts +3 -0
  8. package/dist/cli/commands/task-subtree-transfer-commands.d.ts.map +1 -0
  9. package/dist/cli/index.js +3299 -796
  10. package/dist/cli/stage-a.d.ts.map +1 -1
  11. package/dist/contracts.js +62 -54
  12. package/dist/db/plan-comments.d.ts +14 -0
  13. package/dist/db/plan-comments.d.ts.map +1 -0
  14. package/dist/db/schema.d.ts.map +1 -1
  15. package/dist/db/task-crud.d.ts.map +1 -1
  16. package/dist/db/task-lifecycle.d.ts.map +1 -1
  17. package/dist/db/task-lists.d.ts.map +1 -1
  18. package/dist/db/task-relations.d.ts.map +1 -1
  19. package/dist/index.d.ts +3 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1787 -309
  22. package/dist/lib/comment-cursor.d.ts +19 -0
  23. package/dist/lib/comment-cursor.d.ts.map +1 -1
  24. package/dist/lib/import-export-bridge.d.ts.map +1 -1
  25. package/dist/mcp/index.js +2273 -471
  26. package/dist/mcp.js +8 -4
  27. package/dist/project-registration.js +207 -63
  28. package/dist/registry.d.ts +1 -1
  29. package/dist/registry.d.ts.map +1 -1
  30. package/dist/registry.js +69 -54
  31. package/dist/release-provenance.json +5 -5
  32. package/dist/sdk/index.d.ts +1 -1
  33. package/dist/sdk/index.d.ts.map +1 -1
  34. package/dist/sdk/index.js +35 -0
  35. package/dist/sdk/v1.generated.d.ts +141 -0
  36. package/dist/sdk/v1.generated.d.ts.map +1 -1
  37. package/dist/server/cloud.d.ts +12 -2
  38. package/dist/server/cloud.d.ts.map +1 -1
  39. package/dist/server/index.js +5171 -8535
  40. package/dist/server/openapi.d.ts +2714 -1937
  41. package/dist/server/openapi.d.ts.map +1 -1
  42. package/dist/server/v1.d.ts +2 -1
  43. package/dist/server/v1.d.ts.map +1 -1
  44. package/dist/storage/config.d.ts +12 -30
  45. package/dist/storage/config.d.ts.map +1 -1
  46. package/dist/storage/index.d.ts +2 -2
  47. package/dist/storage/index.d.ts.map +1 -1
  48. package/dist/storage/interfaces.d.ts +13 -2
  49. package/dist/storage/interfaces.d.ts.map +1 -1
  50. package/dist/storage/local-sqlite.d.ts.map +1 -1
  51. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  52. package/dist/storage/timestamp-backfill.d.ts +62 -0
  53. package/dist/storage/timestamp-backfill.d.ts.map +1 -0
  54. package/dist/storage.d.ts +2 -2
  55. package/dist/storage.d.ts.map +1 -1
  56. package/dist/storage.js +199 -63
  57. package/dist/task-subtree-transfer/authority.d.ts +27 -0
  58. package/dist/task-subtree-transfer/authority.d.ts.map +1 -0
  59. package/dist/task-subtree-transfer/backend.d.ts +9 -0
  60. package/dist/task-subtree-transfer/backend.d.ts.map +1 -0
  61. package/dist/task-subtree-transfer/canonical.d.ts +14 -0
  62. package/dist/task-subtree-transfer/canonical.d.ts.map +1 -0
  63. package/dist/task-subtree-transfer/http.d.ts +16 -0
  64. package/dist/task-subtree-transfer/http.d.ts.map +1 -0
  65. package/dist/task-subtree-transfer/index.d.ts +6 -0
  66. package/dist/task-subtree-transfer/index.d.ts.map +1 -0
  67. package/dist/task-subtree-transfer/planner.d.ts +24 -0
  68. package/dist/task-subtree-transfer/planner.d.ts.map +1 -0
  69. package/dist/task-subtree-transfer/postgres.d.ts +19 -0
  70. package/dist/task-subtree-transfer/postgres.d.ts.map +1 -0
  71. package/dist/task-subtree-transfer/schema-sql.d.ts +5 -0
  72. package/dist/task-subtree-transfer/schema-sql.d.ts.map +1 -0
  73. package/dist/task-subtree-transfer/sqlite.d.ts +16 -0
  74. package/dist/task-subtree-transfer/sqlite.d.ts.map +1 -0
  75. package/dist/task-subtree-transfer/types.d.ts +148 -0
  76. package/dist/task-subtree-transfer/types.d.ts.map +1 -0
  77. package/dist/task-subtree-transfer.d.ts +2 -0
  78. package/dist/task-subtree-transfer.d.ts.map +1 -0
  79. package/dist/task-subtree-transfer.js +1869 -0
  80. package/dist/testing.d.ts +41 -7
  81. package/dist/testing.d.ts.map +1 -1
  82. package/dist/testing.js +44 -1
  83. package/dist/types/index.d.ts +24 -0
  84. package/dist/types/index.d.ts.map +1 -1
  85. package/package.json +8 -4
package/dist/index.js CHANGED
@@ -2676,6 +2676,20 @@ function ensureSchema(db) {
2676
2676
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
2677
2677
  ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
2678
2678
  ensureColumn("task_comments", "progress_pct", "INTEGER");
2679
+ ensureTable("plan_comments", `
2680
+ CREATE TABLE plan_comments (
2681
+ id TEXT PRIMARY KEY,
2682
+ plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
2683
+ agent_id TEXT,
2684
+ session_id TEXT,
2685
+ content TEXT NOT NULL,
2686
+ type TEXT DEFAULT 'comment' CHECK(type IN ('comment', 'progress', 'note')),
2687
+ progress_pct INTEGER CHECK(progress_pct IS NULL OR (progress_pct >= 0 AND progress_pct <= 100)),
2688
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2689
+ machine_id TEXT,
2690
+ synced_at TEXT
2691
+ )`);
2692
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_plan_comments_plan ON plan_comments(plan_id)");
2679
2693
  ensureColumn("projects", "machine_id", "TEXT");
2680
2694
  ensureColumn("projects", "synced_at", "TEXT");
2681
2695
  ensureColumn("tasks", "machine_id", "TEXT");
@@ -3657,6 +3671,7 @@ var init_machines = __esm(() => {
3657
3671
  "task_lists",
3658
3672
  "plans",
3659
3673
  "task_comments",
3674
+ "plan_comments",
3660
3675
  "sessions",
3661
3676
  "task_history",
3662
3677
  "webhooks",
@@ -4160,20 +4175,16 @@ var init_sync_utils = __esm(() => {
4160
4175
  // src/storage/config.ts
4161
4176
  var exports_config = {};
4162
4177
  __export(exports_config, {
4163
- parseStorageMode: () => parseStorageMode,
4164
4178
  parseStorageBackend: () => parseStorageBackend,
4165
4179
  loadTodosStorageConfig: () => loadTodosStorageConfig,
4166
4180
  loadStorageConfig: () => loadStorageConfig,
4167
4181
  isTodosShadowEnabled: () => isTodosShadowEnabled,
4168
- isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
4169
4182
  isTodosPostgresBackend: () => isTodosPostgresBackend,
4170
4183
  getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
4171
- getTodosStorageMode: () => getTodosStorageMode,
4172
4184
  getTodosStorageEnvName: () => getTodosStorageEnvName,
4173
4185
  getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
4174
4186
  getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
4175
4187
  getTodosStorageBackend: () => getTodosStorageBackend,
4176
- getStorageMode: () => getStorageMode,
4177
4188
  getStorageDatabaseUrl: () => getStorageDatabaseUrl,
4178
4189
  getStorageDatabaseEnv: () => getStorageDatabaseEnv,
4179
4190
  getCanonicalTodosRdsConfig: () => getCanonicalTodosRdsConfig,
@@ -4183,7 +4194,6 @@ __export(exports_config, {
4183
4194
  TODOS_STORAGE_FALLBACK_ENV: () => TODOS_STORAGE_FALLBACK_ENV,
4184
4195
  TODOS_STORAGE_ENV: () => TODOS_STORAGE_ENV,
4185
4196
  STORAGE_TABLES: () => STORAGE_TABLES,
4186
- REMOVED_STORAGE_MODE_ENV_KEYS: () => REMOVED_STORAGE_MODE_ENV_KEYS,
4187
4197
  CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV: () => CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV,
4188
4198
  CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
4189
4199
  CANONICAL_TODOS_RDS_CLUSTER_ENV: () => CANONICAL_TODOS_RDS_CLUSTER_ENV
@@ -4238,9 +4248,6 @@ function loadStorageConfig(env = process.env) {
4238
4248
  function isTodosPostgresBackend(config) {
4239
4249
  return config.mode === "postgres";
4240
4250
  }
4241
- function isTodosRemoteStorageEnabled(config) {
4242
- return isTodosPostgresBackend(config);
4243
- }
4244
4251
  function assertTodosRemoteStorageConfig(config) {
4245
4252
  if (!isTodosPostgresBackend(config))
4246
4253
  return;
@@ -4256,32 +4263,15 @@ function parseStorageBackend(value) {
4256
4263
  return "sqlite";
4257
4264
  if (normalized === "postgres" || normalized === "postgresql")
4258
4265
  return "postgres";
4259
- if (["local", "remote", "cloud", "hybrid", "self_hosted"].includes(normalized)) {
4260
- throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} selects the backend. Deployment modes no longer exist: ` + `delete the storage-mode variable and set ${TODOS_STORAGE_ENV.databaseUrl} to select the ` + `postgresql backend, or leave it unset for sqlite.`);
4261
- }
4262
4266
  throw new Error(`Storage backend must be sqlite or postgres`);
4263
4267
  }
4264
- function parseStorageMode(value) {
4265
- return parseStorageBackend(value);
4266
- }
4267
4268
  function getTodosStorageBackend(env = process.env) {
4268
- for (const key of REMOVED_STORAGE_MODE_ENV_KEYS) {
4269
- if (Object.hasOwn(env, key) && env[key] !== undefined) {
4270
- throw new Error(`${key} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `Set ${TODOS_STORAGE_ENV.databaseUrl} to select the postgresql backend, ` + `or leave it unset for sqlite.`);
4271
- }
4272
- }
4273
4269
  if (!getTodosStorageDatabaseUrl(env))
4274
4270
  return "sqlite";
4275
4271
  if (isTodosShadowEnabled(env))
4276
4272
  return "sqlite";
4277
4273
  return "postgres";
4278
4274
  }
4279
- function getTodosStorageMode(env = process.env) {
4280
- return getTodosStorageBackend(env);
4281
- }
4282
- function getStorageMode(env = process.env) {
4283
- return getTodosStorageBackend(env);
4284
- }
4285
4275
  function isTodosShadowEnabled(env = process.env) {
4286
4276
  return parseBoolean(readStorageEnv(env, "shadow").value, false);
4287
4277
  }
@@ -4348,7 +4338,7 @@ function parsePositiveInteger(value, fallback) {
4348
4338
  }
4349
4339
  return parsed;
4350
4340
  }
4351
- var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV, REMOVED_STORAGE_MODE_ENV_KEYS, CANONICAL_TODOS_RDS_CLUSTER_ENV = "HASNA_TODOS_RDS_CLUSTER", CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV = "HASNA_TODOS_RDS_RUNTIME_PATH", CANONICAL_TODOS_RDS_DATABASE = "todos";
4341
+ var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV, CANONICAL_TODOS_RDS_CLUSTER_ENV = "HASNA_TODOS_RDS_CLUSTER", CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV = "HASNA_TODOS_RDS_RUNTIME_PATH", CANONICAL_TODOS_RDS_DATABASE = "todos";
4352
4342
  var init_config = __esm(() => {
4353
4343
  TODOS_STORAGE_TABLES = [
4354
4344
  "todos_sync_records",
@@ -4387,12 +4377,6 @@ var init_config = __esm(() => {
4387
4377
  syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
4388
4378
  syncDryRun: "TODOS_SYNC_DRY_RUN"
4389
4379
  };
4390
- REMOVED_STORAGE_MODE_ENV_KEYS = [
4391
- "HASNA_TODOS_STORAGE_MODE",
4392
- "HASNA_TODOS_MODE",
4393
- "TODOS_STORAGE_MODE",
4394
- "TODOS_MODE"
4395
- ];
4396
4380
  });
4397
4381
 
4398
4382
  // src/storage/shadow-outbox-schema.ts
@@ -6857,22 +6841,31 @@ function updateTaskList(id, input, db) {
6857
6841
  throw new TaskListNotFoundError(id);
6858
6842
  const sets = ["updated_at = ?"];
6859
6843
  const params = [now()];
6860
- if (input.slug !== undefined) {
6861
- const slug = slugify(input.slug);
6862
- if (!slug)
6863
- throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
6864
- const duplicate = existing.project_id ? d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ? AND id != ?").get(existing.project_id, slug, id) : d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ? AND id != ?").get(slug, id);
6844
+ const projectId = input.project_id === "" || input.project_id === null ? null : input.project_id === undefined ? existing.project_id : input.project_id;
6845
+ const scopeChanged = projectId !== existing.project_id;
6846
+ const effectiveSlug = input.slug !== undefined ? slugify(input.slug) : existing.slug;
6847
+ if (input.slug !== undefined && !effectiveSlug) {
6848
+ throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
6849
+ }
6850
+ if (scopeChanged || input.slug !== undefined && effectiveSlug !== existing.slug) {
6851
+ const duplicate = projectId ? d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ? AND id != ?").get(projectId, effectiveSlug, id) : d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ? AND id != ?").get(effectiveSlug, id);
6865
6852
  if (duplicate) {
6866
- throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${slug}" already exists in this scope`);
6853
+ throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
6867
6854
  }
6868
- if (slug !== existing.slug) {
6855
+ if (scopeChanged || effectiveSlug !== existing.slug) {
6869
6856
  releaseCanonicalSlugClaims("task_list", id, d);
6870
- if (!claimCanonicalSlug("task_list", taskListSlugScopeKey(existing.project_id), slug, id, d)) {
6871
- throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${slug}" already exists in this scope`);
6857
+ if (!claimCanonicalSlug("task_list", taskListSlugScopeKey(projectId), effectiveSlug, id, d)) {
6858
+ throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
6872
6859
  }
6873
6860
  }
6861
+ }
6862
+ if (input.slug !== undefined) {
6874
6863
  sets.push("slug = ?");
6875
- params.push(slug);
6864
+ params.push(effectiveSlug);
6865
+ }
6866
+ if (projectId !== existing.project_id) {
6867
+ sets.push("project_id = ?");
6868
+ params.push(projectId);
6876
6869
  }
6877
6870
  if (input.name !== undefined) {
6878
6871
  sets.push("name = ?");
@@ -9321,8 +9314,8 @@ function failTask(id, agentId, reason, options, db) {
9321
9314
  const safeMeta = sanitizePreWriteValue(meta, "task.failure.metadata");
9322
9315
  const timestamp2 = now();
9323
9316
  const failTx = d.transaction(() => {
9324
- const res = d.run(`UPDATE tasks SET status = 'failed', reason = ?, locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
9325
- WHERE id = ? AND version = ?`, [safeReason, JSON.stringify(safeMeta), timestamp2, id, task.version]);
9317
+ const res = d.run(`UPDATE tasks SET status = 'failed', reason = ?, locked_by = NULL, locked_at = NULL, completed_at = COALESCE(completed_at, ?), metadata = ?, version = version + 1, updated_at = ?
9318
+ WHERE id = ? AND version = ?`, [safeReason, timestamp2, JSON.stringify(safeMeta), timestamp2, id, task.version]);
9326
9319
  if (res.changes === 0) {
9327
9320
  const current = getTask(id, d);
9328
9321
  throw new VersionConflictError(id, task.version, current?.version ?? -1);
@@ -9334,6 +9327,7 @@ function failTask(id, agentId, reason, options, db) {
9334
9327
  status: "failed",
9335
9328
  locked_by: null,
9336
9329
  locked_at: null,
9330
+ completed_at: task.completed_at ?? timestamp2,
9337
9331
  reason: safeReason,
9338
9332
  metadata: safeMeta,
9339
9333
  version: task.version + 1,
@@ -9982,13 +9976,20 @@ function updateTaskStored(id, input, db) {
9982
9976
  if (isTerminalStatus(input.status)) {
9983
9977
  sets.push("locked_by = NULL");
9984
9978
  sets.push("locked_at = NULL");
9985
- }
9986
- if (input.status === "completed") {
9987
- sets.push("completed_at = ?");
9988
- params.push(completionTimestamp);
9979
+ if (input.status === "completed") {
9980
+ sets.push("completed_at = ?");
9981
+ params.push(completionTimestamp);
9982
+ } else {
9983
+ sets.push("completed_at = COALESCE(completed_at, ?)");
9984
+ params.push(completionTimestamp);
9985
+ }
9989
9986
  } else if (task.status === "completed" && input.completed_at === undefined) {
9990
9987
  sets.push("completed_at = NULL");
9991
9988
  }
9989
+ if (input.status === "in_progress") {
9990
+ sets.push("started_at = COALESCE(started_at, ?)");
9991
+ params.push(timestamp2);
9992
+ }
9992
9993
  }
9993
9994
  if (input.priority !== undefined) {
9994
9995
  sets.push("priority = ?");
@@ -10126,7 +10127,6 @@ function updateTaskStored(id, input, db) {
10126
10127
  if (input.approved_by !== undefined)
10127
10128
  logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
10128
10129
  const reopened = input.status !== undefined && input.status !== "completed" && task.status === "completed" && input.completed_at === undefined;
10129
- const completedNow = input.status === "completed";
10130
10130
  const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
10131
10131
  const updatedTask = {
10132
10132
  ...task,
@@ -10137,7 +10137,8 @@ function updateTaskStored(id, input, db) {
10137
10137
  updated_at: timestamp2,
10138
10138
  locked_by: terminalNow ? null : task.locked_by,
10139
10139
  locked_at: terminalNow ? null : task.locked_at,
10140
- completed_at: completedNow ? completionTimestamp : reopened ? null : input.completed_at !== undefined ? input.completed_at : task.completed_at,
10140
+ completed_at: input.status === "completed" ? completionTimestamp : input.completed_at !== undefined ? input.completed_at : terminalNow ? task.completed_at ?? completionTimestamp : reopened ? null : task.completed_at,
10141
+ started_at: input.status === "in_progress" ? task.started_at ?? timestamp2 : task.started_at,
10141
10142
  sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
10142
10143
  actual_minutes: input.actual_minutes ?? task.actual_minutes,
10143
10144
  confidence: input.confidence !== undefined ? input.confidence : task.confidence,
@@ -10198,6 +10199,9 @@ function deleteTask(id, db) {
10198
10199
  version: row.version
10199
10200
  }, d);
10200
10201
  const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
10202
+ if (result.changes > 0) {
10203
+ d.run("DELETE FROM task_dependencies WHERE task_id = ? OR depends_on = ?", [id, id]);
10204
+ }
10201
10205
  return result.changes > 0;
10202
10206
  }
10203
10207
  var init_task_crud = __esm(() => {
@@ -12882,10 +12886,10 @@ function cancelDispatch(id, db) {
12882
12886
  _db2.run("UPDATE dispatches SET status = 'cancelled' WHERE id = ?", [id]);
12883
12887
  return getDispatch(id, _db2);
12884
12888
  }
12885
- function updateDispatchStatus(id, status2, opts = {}, db) {
12889
+ function updateDispatchStatus(id, status3, opts = {}, db) {
12886
12890
  const _db2 = db ?? getDatabase();
12887
12891
  const error = opts.error ? sanitizePreWriteText(opts.error, "dispatch.error") : null;
12888
- _db2.run("UPDATE dispatches SET status = ?, error = ?, sent_at = ? WHERE id = ?", [status2, error, opts.sent_at ?? null, id]);
12892
+ _db2.run("UPDATE dispatches SET status = ?, error = ?, sent_at = ? WHERE id = ?", [status3, error, opts.sent_at ?? null, id]);
12889
12893
  }
12890
12894
  function createDispatchLog(log, db) {
12891
12895
  const _db2 = db ?? getDatabase();
@@ -12918,7 +12922,7 @@ var init_dispatches = __esm(() => {
12918
12922
  // package.json
12919
12923
  var package_default = {
12920
12924
  name: "@hasna/todos",
12921
- version: "0.15.35",
12925
+ version: "0.15.41",
12922
12926
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12923
12927
  type: "module",
12924
12928
  main: "dist/index.js",
@@ -12964,6 +12968,10 @@ var package_default = {
12964
12968
  "./task-manifest": {
12965
12969
  types: "./dist/task-manifest.d.ts",
12966
12970
  import: "./dist/task-manifest.js"
12971
+ },
12972
+ "./task-subtree-transfer": {
12973
+ types: "./dist/task-subtree-transfer.d.ts",
12974
+ import: "./dist/task-subtree-transfer.js"
12967
12975
  }
12968
12976
  },
12969
12977
  workspaces: [
@@ -12977,8 +12985,8 @@ var package_default = {
12977
12985
  "README.md"
12978
12986
  ],
12979
12987
  scripts: {
12980
- build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
12981
- "build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
12988
+ build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --root src --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
12989
+ "build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
12982
12990
  migrate: "bun run src/server/index.ts migrate",
12983
12991
  "backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
12984
12992
  "generate:sdk": "bun run scripts/generate-sdk.ts",
@@ -13032,7 +13040,7 @@ var package_default = {
13032
13040
  author: "Andrei Hasna <andrei@hasna.com>",
13033
13041
  license: "Apache-2.0",
13034
13042
  dependencies: {
13035
- "@hasna/contracts": "0.5.2",
13043
+ "@hasna/contracts": "0.13.3",
13036
13044
  "@hasna/events": "^0.1.11",
13037
13045
  "@modelcontextprotocol/sdk": "^1.12.1",
13038
13046
  chalk: "^5.4.1",
@@ -25218,6 +25226,13 @@ var TODOS_PACKAGE_EXPORTS = [
25218
25226
  types: "./dist/task-manifest.d.ts",
25219
25227
  description: "Transactional package-owned authority for deterministic plan and task graph manifests.",
25220
25228
  stability: "stable"
25229
+ },
25230
+ {
25231
+ subpath: "./task-subtree-transfer",
25232
+ import: "./dist/task-subtree-transfer.js",
25233
+ types: "./dist/task-subtree-transfer.d.ts",
25234
+ description: "Atomic package-owned authority for existing task-subtree project transfers and rollback.",
25235
+ stability: "stable"
25221
25236
  }
25222
25237
  ];
25223
25238
  function source8(version) {
@@ -25410,6 +25425,40 @@ init_tasks();
25410
25425
  init_projects();
25411
25426
  init_plans();
25412
25427
 
25428
+ // src/db/plan-comments.ts
25429
+ init_types();
25430
+ init_database();
25431
+ init_plans();
25432
+ init_prewrite_secrets();
25433
+ function addPlanComment(input, db) {
25434
+ const d = db || getDatabase();
25435
+ if (!getPlan(input.plan_id, d)) {
25436
+ throw new PlanNotFoundError(input.plan_id);
25437
+ }
25438
+ const id = uuid();
25439
+ const timestamp3 = now();
25440
+ d.run(`INSERT INTO plan_comments (id, plan_id, agent_id, session_id, content, type, progress_pct, created_at)
25441
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
25442
+ id,
25443
+ input.plan_id,
25444
+ input.agent_id || null,
25445
+ input.session_id || null,
25446
+ sanitizePreWriteText(input.content, "comment.content"),
25447
+ input.type || "comment",
25448
+ input.progress_pct ?? null,
25449
+ timestamp3
25450
+ ]);
25451
+ return getPlanComment(id, d);
25452
+ }
25453
+ function getPlanComment(id, db) {
25454
+ const d = db || getDatabase();
25455
+ return d.query("SELECT * FROM plan_comments WHERE id = ?").get(id);
25456
+ }
25457
+ function listPlanComments(planId, db) {
25458
+ const d = db || getDatabase();
25459
+ return d.query("SELECT * FROM plan_comments WHERE plan_id = ? ORDER BY created_at, rowid").all(planId);
25460
+ }
25461
+
25413
25462
  // src/db/agents.ts
25414
25463
  init_types();
25415
25464
  init_database();
@@ -26067,6 +26116,20 @@ init_task_lists();
26067
26116
  init_templates();
26068
26117
  init_audit();
26069
26118
  init_comments();
26119
+
26120
+ // src/lib/comment-cursor.ts
26121
+ function isStrictlyOlder(comment, before) {
26122
+ return comment.created_at < before.created_at || comment.created_at === before.created_at && comment.id < before.id;
26123
+ }
26124
+ function compareCommentKeyset(left, right) {
26125
+ if (isStrictlyOlder(left, right))
26126
+ return -1;
26127
+ if (isStrictlyOlder(right, left))
26128
+ return 1;
26129
+ return 0;
26130
+ }
26131
+
26132
+ // src/storage/local-sqlite.ts
26070
26133
  init_database();
26071
26134
 
26072
26135
  // src/db/integrity.ts
@@ -27163,7 +27226,22 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
27163
27226
  list: (projectId) => listPlans(projectId, database()),
27164
27227
  update: (id, input) => updatePlan(id, input, database()),
27165
27228
  completeAtRevision: (id, expectedUpdatedAt) => completePlanAtRevision(id, expectedUpdatedAt, database()),
27166
- delete: (id) => deletePlan(id, database())
27229
+ delete: (id) => deletePlan(id, database()),
27230
+ addComment: (input) => addPlanComment(input, database()),
27231
+ getComments: (planId) => listPlanComments(planId, database()),
27232
+ getCommentsPage: (planId, options2) => {
27233
+ if (options2?.limit !== undefined && (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 1001)) {
27234
+ throw new Error("Plan comment limit must be an integer between 1 and 1001");
27235
+ }
27236
+ let comments = listPlanComments(planId, database());
27237
+ comments = comments.sort(compareCommentKeyset);
27238
+ if (options2?.before) {
27239
+ const before = options2.before;
27240
+ comments = comments.filter((comment) => isStrictlyOlder(comment, before));
27241
+ }
27242
+ const limit = options2?.limit ?? 100;
27243
+ return comments.slice(-limit);
27244
+ }
27167
27245
  },
27168
27246
  planProjectLinks: {
27169
27247
  apply: (input) => applyPlanProjectLinkSqlite(input, database()),
@@ -27204,10 +27282,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
27204
27282
  throw new Error("Comment limit must be an integer between 1 and 1001");
27205
27283
  }
27206
27284
  let comments = listComments(taskId, database());
27207
- comments = comments.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id));
27285
+ comments = comments.sort(compareCommentKeyset);
27208
27286
  if (options2?.before) {
27209
27287
  const before = options2.before;
27210
- comments = comments.filter((comment) => comment.created_at < before.created_at || comment.created_at === before.created_at && comment.id < before.id);
27288
+ comments = comments.filter((comment) => isStrictlyOlder(comment, before));
27211
27289
  }
27212
27290
  if (options2?.limit !== undefined)
27213
27291
  comments = comments.slice(-options2.limit);
@@ -27737,7 +27815,26 @@ function createPostgresTodosStorageAdapter(options) {
27737
27815
  list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
27738
27816
  update: (id, input) => updatePlan2(id, input, store),
27739
27817
  completeAtRevision: (id, expectedUpdatedAt, context) => store.completePlanAtRevision(id, expectedUpdatedAt, context),
27740
- delete: (id, context) => store.deletePlan(id, context)
27818
+ delete: (id, context) => store.deletePlan(id, context),
27819
+ addComment: (input, context) => addPlanComment2(input, store, context),
27820
+ getComments: async (planId) => {
27821
+ const pages = [];
27822
+ let before;
27823
+ while (true) {
27824
+ const page = await store.listPlanComments(planId, { limit: 1000, ...before ? { before } : {} });
27825
+ if (page.length === 0)
27826
+ break;
27827
+ pages.unshift(page);
27828
+ if (page.length < 1000)
27829
+ break;
27830
+ const oldest = page[0];
27831
+ before = { created_at: oldest.created_at, id: oldest.id };
27832
+ }
27833
+ return pages.flat().map(redactPlanComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
27834
+ },
27835
+ getCommentsPage: async (planId, options2) => {
27836
+ return (await store.listPlanComments(planId, options2)).map(redactPlanComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
27837
+ }
27741
27838
  },
27742
27839
  planProjectLinks: {
27743
27840
  apply: (input, context) => store.applyPlanProjectLink(input, context),
@@ -27955,6 +28052,27 @@ class PostgresJsonRecordStore {
27955
28052
  LIMIT $${params.length}`, params);
27956
28053
  return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
27957
28054
  }
28055
+ async listPlanComments(planId, options = {}) {
28056
+ await this.ensureSchema();
28057
+ const limit = options.limit ?? 100;
28058
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1001) {
28059
+ throw new Error("Postgres plan comment limit must be an integer between 1 and 1001");
28060
+ }
28061
+ const params = [this.service, planId];
28062
+ let cursorPredicate = "";
28063
+ if (options.before) {
28064
+ params.push(options.before.created_at, options.before.id);
28065
+ cursorPredicate = `AND (payload->>'created_at', object_id) < ($3, $4)`;
28066
+ }
28067
+ params.push(limit);
28068
+ const result = await this.options.client.query(`/* todos:list-plan-comments */ SELECT payload FROM ${this.tableName}
28069
+ WHERE service = $1 AND object_type = 'plan_comments' AND deleted_at IS NULL
28070
+ AND payload->>'plan_id' = $2
28071
+ ${cursorPredicate}
28072
+ ORDER BY payload->>'created_at' DESC, object_id DESC
28073
+ LIMIT $${params.length}`, params);
28074
+ return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
28075
+ }
27958
28076
  async listRecords(type) {
27959
28077
  await this.ensureSchema();
27960
28078
  const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
@@ -29116,6 +29234,15 @@ class PostgresJsonRecordStore {
29116
29234
  WHERE r.service = $1 AND r.object_type = 'tasks' AND r.deleted_at IS NULL
29117
29235
  AND r.payload->>'plan_id' = $2 AND EXISTS (SELECT 1 FROM target)
29118
29236
  RETURNING 1
29237
+ ), deleted_plan_comments AS (
29238
+ UPDATE ${this.tableName} r SET
29239
+ deleted_at = $3::timestamptz,
29240
+ updated_at = $3::timestamptz,
29241
+ source_machine_id = COALESCE($4, r.source_machine_id),
29242
+ version = COALESCE(r.version, 0) + 1
29243
+ WHERE r.service = $1 AND r.object_type = 'plan_comments' AND r.deleted_at IS NULL
29244
+ AND r.payload->>'plan_id' = $2 AND EXISTS (SELECT 1 FROM target)
29245
+ RETURNING 1
29119
29246
  ), deleted_plan AS (
29120
29247
  UPDATE ${this.tableName} r SET
29121
29248
  deleted_at = $3::timestamptz,
@@ -29291,7 +29418,8 @@ async function updateTask2(id, input, store, context) {
29291
29418
  task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id,
29292
29419
  parent_id: input.parent_id !== undefined ? input.parent_id : existing.parent_id,
29293
29420
  created_by: existing.created_by,
29294
- completed_at: reopened ? null : input.completed_at !== undefined ? input.completed_at : existing.completed_at
29421
+ completed_at: input.status === "completed" ? input.completed_at ?? new Date().toISOString() : input.completed_at !== undefined ? input.completed_at : input.status !== undefined && isTerminalStatus(input.status) ? existing.completed_at ?? new Date().toISOString() : reopened ? null : existing.completed_at,
29422
+ started_at: input.status === "in_progress" ? existing.started_at ?? new Date().toISOString() : existing.started_at
29295
29423
  };
29296
29424
  const storedTask = await store.upsertTaskWithPlanMembershipGuard(task2, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined, context, {
29297
29425
  operation: "update",
@@ -29336,7 +29464,8 @@ async function failTask2(id, agentId, reason, options, store) {
29336
29464
  status: "failed",
29337
29465
  assigned_to: task2.assigned_to ?? agentId ?? null,
29338
29466
  reason: reason ?? task2.reason,
29339
- retry_after: options?.retry_after ?? task2.retry_after
29467
+ retry_after: options?.retry_after ?? task2.retry_after,
29468
+ completed_at: task2.completed_at ?? new Date().toISOString()
29340
29469
  }, store);
29341
29470
  if (!options?.retry)
29342
29471
  return { task: failed };
@@ -29784,16 +29913,22 @@ async function createTaskList2(input, store, context) {
29784
29913
  async function updateTaskList2(id, input, store) {
29785
29914
  const list = await requireRecord("task_lists", id, store);
29786
29915
  const patch = definedPatch(input);
29916
+ const projectId = input.project_id === "" || input.project_id === null ? null : input.project_id === undefined ? list.project_id : input.project_id;
29787
29917
  if (input.slug !== undefined) {
29788
29918
  const slug = slugifyRaw(input.slug);
29789
29919
  if (!slug)
29790
29920
  throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
29791
- const duplicate = (await store.list("task_lists")).find((candidate) => candidate.id !== id && candidate.project_id === list.project_id && candidate.slug === slug);
29921
+ patch.slug = slug;
29922
+ }
29923
+ const effectiveSlug = patch.slug ?? list.slug;
29924
+ if (projectId !== list.project_id || effectiveSlug !== list.slug) {
29925
+ const duplicate = (await store.list("task_lists")).find((candidate) => candidate.id !== id && candidate.project_id === projectId && candidate.slug === effectiveSlug);
29792
29926
  if (duplicate) {
29793
- throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${slug}" already exists in this scope`);
29927
+ throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
29794
29928
  }
29795
- patch.slug = slug;
29796
29929
  }
29930
+ if (projectId !== list.project_id)
29931
+ patch.project_id = projectId;
29797
29932
  return store.upsert("task_lists", {
29798
29933
  ...list,
29799
29934
  ...patch,
@@ -29886,6 +30021,22 @@ async function addComment2(input, store, context) {
29886
30021
  function redactComment(comment) {
29887
30022
  return { ...comment, content: redactEvidenceText(comment.content) };
29888
30023
  }
30024
+ async function addPlanComment2(input, store, context) {
30025
+ const comment = {
30026
+ id: randomUUID3(),
30027
+ plan_id: input.plan_id,
30028
+ agent_id: input.agent_id ?? context?.agentId ?? null,
30029
+ session_id: input.session_id ?? context?.sessionId ?? null,
30030
+ content: redactEvidenceText(input.content),
30031
+ type: input.type ?? "comment",
30032
+ progress_pct: input.progress_pct ?? null,
30033
+ created_at: new Date().toISOString()
30034
+ };
30035
+ return store.upsert("plan_comments", comment, context);
30036
+ }
30037
+ function redactPlanComment(comment) {
30038
+ return { ...comment, content: redactEvidenceText(comment.content) };
30039
+ }
29889
30040
  async function exportSnapshot(store) {
29890
30041
  return {
29891
30042
  exportedAt: new Date().toISOString(),
@@ -43624,6 +43775,1314 @@ class TodosTaskManifestHttpClient {
43624
43775
  function createTodosTaskManifestHttpClient(options) {
43625
43776
  return new TodosTaskManifestHttpClient(options);
43626
43777
  }
43778
+ // src/task-subtree-transfer/types.ts
43779
+ var TODOS_TASK_SUBTREE_TRANSFER_ROUTE = "todos.task-subtree-transfer.v1";
43780
+ var TODOS_TASK_SUBTREE_TRANSFER_SCHEMA_VERSION = 1;
43781
+
43782
+ class TodosTaskSubtreeTransferError extends Error {
43783
+ code;
43784
+ details;
43785
+ constructor(code, message, details = {}) {
43786
+ super(message);
43787
+ this.code = code;
43788
+ this.details = details;
43789
+ this.name = "TodosTaskSubtreeTransferError";
43790
+ }
43791
+ }
43792
+
43793
+ // src/task-subtree-transfer/canonical.ts
43794
+ function taskSubtreeTransferRequestDigest(input) {
43795
+ const { idempotency_key: _idempotencyKey, ...request } = input;
43796
+ return canonicalDigest(request);
43797
+ }
43798
+ function taskSubtreeTransferRollbackRequestDigest(input) {
43799
+ const { idempotency_key: _idempotencyKey, ...request } = input;
43800
+ return canonicalDigest(request);
43801
+ }
43802
+ function deriveTodosTaskSubtreeTransferIdempotencyKey(input) {
43803
+ return `tstk_${canonicalDigest({
43804
+ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
43805
+ ...input
43806
+ }).slice(0, 48)}`;
43807
+ }
43808
+
43809
+ // src/task-subtree-transfer/planner.ts
43810
+ function sortedTaskImage(task3) {
43811
+ return {
43812
+ task_id: task3.task_id,
43813
+ project_id: task3.project_id,
43814
+ parent_id: task3.parent_id,
43815
+ plan_id: task3.plan_id,
43816
+ task_list_id: task3.task_list_id,
43817
+ version: task3.version,
43818
+ updated_at: task3.updated_at
43819
+ };
43820
+ }
43821
+ function sourcePopulationDigest(tasks) {
43822
+ return canonicalDigest(tasks.map((task3) => ({
43823
+ ...sortedTaskImage(task3),
43824
+ archived_at: task3.archived_at ?? null
43825
+ })).sort((left, right) => left.task_id.localeCompare(right.task_id)));
43826
+ }
43827
+ function closure(tasks, rootTaskId) {
43828
+ const byId = new Map(tasks.map((task3) => [task3.task_id, task3]));
43829
+ if (!byId.has(rootTaskId)) {
43830
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND", `Root task not found in the exact source project: ${rootTaskId}`, { root_task_id: rootTaskId });
43831
+ }
43832
+ const children = new Map;
43833
+ for (const task3 of tasks) {
43834
+ if (!task3.parent_id)
43835
+ continue;
43836
+ const siblings = children.get(task3.parent_id) ?? [];
43837
+ siblings.push(task3.task_id);
43838
+ children.set(task3.parent_id, siblings);
43839
+ }
43840
+ for (const ids2 of children.values())
43841
+ ids2.sort();
43842
+ const visiting = new Set;
43843
+ const visited = new Set;
43844
+ const result = [];
43845
+ const visit = (id) => {
43846
+ if (visiting.has(id)) {
43847
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_HIERARCHY_CYCLE", "Task hierarchy contains a cycle in the requested descendant closure", { task_id: id });
43848
+ }
43849
+ if (visited.has(id))
43850
+ return;
43851
+ const task3 = byId.get(id);
43852
+ if (!task3) {
43853
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_FOREIGN_REFERENCE", "Task hierarchy references a parent outside the exact source population", { task_id: id });
43854
+ }
43855
+ visiting.add(id);
43856
+ result.push(task3);
43857
+ for (const child of children.get(id) ?? [])
43858
+ visit(child);
43859
+ visiting.delete(id);
43860
+ visited.add(id);
43861
+ };
43862
+ visit(rootTaskId);
43863
+ return result.sort((left, right) => left.task_id.localeCompare(right.task_id));
43864
+ }
43865
+ function prepareTransfer(snapshot, input) {
43866
+ if (!snapshot.destination_project_found || !snapshot.destination_task_list_found || !snapshot.destination_parent_found) {
43867
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_FOREIGN_REFERENCE", "Destination project, task list, or parent does not match the exact requested target");
43868
+ }
43869
+ const moved = closure(snapshot.source_tasks, input.root_task_id);
43870
+ const movedIds = new Set(moved.map((task3) => task3.task_id));
43871
+ if (input.destination_parent_id && movedIds.has(input.destination_parent_id)) {
43872
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_HIERARCHY_CYCLE", "Destination parent cannot be inside the transferred subtree", { destination_parent_id: input.destination_parent_id });
43873
+ }
43874
+ const planIds = [...new Set(moved.map((task3) => task3.plan_id).filter((id) => Boolean(id)))].sort();
43875
+ const allPlanTasks = new Map;
43876
+ for (const task3 of snapshot.plan_tasks) {
43877
+ if (!task3.plan_id)
43878
+ continue;
43879
+ const rows = allPlanTasks.get(task3.plan_id) ?? [];
43880
+ rows.push(task3);
43881
+ allPlanTasks.set(task3.plan_id, rows);
43882
+ }
43883
+ const contained = [];
43884
+ const shared = [];
43885
+ const planById = new Map(snapshot.plans.map((plan) => [plan.plan_id, plan]));
43886
+ for (const planId of planIds) {
43887
+ const plan = planById.get(planId);
43888
+ if (!plan || plan.project_id !== input.source_project_id) {
43889
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "A moved task references a plan absent from the exact source project", { plan_id: planId });
43890
+ }
43891
+ const members = allPlanTasks.get(planId) ?? [];
43892
+ if (members.length === 0) {
43893
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "A moved task references a plan with no authoritative task membership", { plan_id: planId });
43894
+ }
43895
+ if (members.every((task3) => movedIds.has(task3.task_id)))
43896
+ contained.push(planId);
43897
+ else
43898
+ shared.push(planId);
43899
+ }
43900
+ const inspection = {
43901
+ source_project_id: input.source_project_id,
43902
+ destination_project_id: input.destination_project_id,
43903
+ destination_task_list_id: input.destination_task_list_id,
43904
+ root_task_id: input.root_task_id,
43905
+ destination_parent_id: input.destination_parent_id,
43906
+ expected_root_parent_id: moved.find((task3) => task3.task_id === input.root_task_id).parent_id,
43907
+ source_population_digest: sourcePopulationDigest(snapshot.source_tasks),
43908
+ expected_tasks: moved.map((task3) => ({ task_id: task3.task_id, version: task3.version })),
43909
+ contained_plan_ids: contained,
43910
+ shared_plan_ids: shared,
43911
+ complete: true
43912
+ };
43913
+ return {
43914
+ inspection,
43915
+ prior_tasks: moved.map(sortedTaskImage),
43916
+ prior_plans: contained.map((id) => planById.get(id)),
43917
+ task_plan_targets: new Map(moved.map((task3) => [task3.task_id, task3.plan_id]))
43918
+ };
43919
+ }
43920
+ function validateApplySnapshot(prepared, request, plans) {
43921
+ if (request.expected_root_parent_id !== prepared.inspection.expected_root_parent_id) {
43922
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Root parent changed since inspection");
43923
+ }
43924
+ if (request.source_population_digest !== prepared.inspection.source_population_digest) {
43925
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_POPULATION_DRIFT", "Complete source-project task population changed since inspection");
43926
+ }
43927
+ const expected = request.expected_tasks.map((task3) => ({ task_id: task3.task_id, version: task3.version })).sort((left, right) => left.task_id.localeCompare(right.task_id));
43928
+ if (JSON.stringify(expected) !== JSON.stringify(prepared.inspection.expected_tasks)) {
43929
+ const actualById = new Map(prepared.inspection.expected_tasks.map((task3) => [task3.task_id, task3.version]));
43930
+ const sameIds = expected.length === prepared.inspection.expected_tasks.length && expected.every((task3) => actualById.has(task3.task_id));
43931
+ throw new TodosTaskSubtreeTransferError(sameIds ? "TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT" : "TODOS_TASK_SUBTREE_TRANSFER_CLOSURE_DRIFT", sameIds ? "At least one exact task version changed since inspection" : "Exact descendant closure changed since inspection");
43932
+ }
43933
+ const requestedSources = request.shared_plan_splits.map((mapping) => mapping.source_plan_id).sort();
43934
+ if (JSON.stringify(requestedSources) !== JSON.stringify(prepared.inspection.shared_plan_ids)) {
43935
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PARTIAL_PLAN", "Shared plans require one exact explicit destination-plan mapping each", {
43936
+ expected_shared_plan_ids: prepared.inspection.shared_plan_ids,
43937
+ received_shared_plan_ids: requestedSources
43938
+ });
43939
+ }
43940
+ const planById = new Map(plans.map((plan) => [plan.plan_id, plan]));
43941
+ const destinationIds = new Set;
43942
+ for (const mapping of request.shared_plan_splits) {
43943
+ if (destinationIds.has(mapping.destination_plan_id)) {
43944
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "Two shared source plans cannot collapse into one destination plan");
43945
+ }
43946
+ destinationIds.add(mapping.destination_plan_id);
43947
+ const destination = planById.get(mapping.destination_plan_id);
43948
+ if (!destination || destination.project_id !== request.destination_project_id || destination.task_list_id !== request.destination_task_list_id) {
43949
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "Shared-plan destination does not match the exact destination project and task list", { destination_plan_id: mapping.destination_plan_id });
43950
+ }
43951
+ for (const [taskId, planId] of prepared.task_plan_targets) {
43952
+ if (planId === mapping.source_plan_id) {
43953
+ prepared.task_plan_targets.set(taskId, mapping.destination_plan_id);
43954
+ }
43955
+ }
43956
+ }
43957
+ }
43958
+
43959
+ // src/task-subtree-transfer/schema-sql.ts
43960
+ function sqliteTodosTaskSubtreeTransferSchemaSql() {
43961
+ return `
43962
+ CREATE TABLE IF NOT EXISTS todos_task_subtree_transfer_receipts (
43963
+ receipt_id TEXT PRIMARY KEY,
43964
+ tenant_id TEXT NOT NULL,
43965
+ kind TEXT NOT NULL CHECK(kind IN ('apply', 'rollback')),
43966
+ operation_id TEXT NOT NULL,
43967
+ step_id TEXT NOT NULL,
43968
+ idempotency_key TEXT NOT NULL,
43969
+ request_digest TEXT NOT NULL,
43970
+ precondition_digest TEXT NOT NULL,
43971
+ result_digest TEXT NOT NULL,
43972
+ apply_receipt_id TEXT,
43973
+ request_json TEXT NOT NULL,
43974
+ result_json TEXT NOT NULL,
43975
+ created_at TEXT NOT NULL,
43976
+ UNIQUE(tenant_id, kind, operation_id, step_id),
43977
+ UNIQUE(tenant_id, kind, idempotency_key)
43978
+ );
43979
+ CREATE TRIGGER IF NOT EXISTS todos_task_subtree_transfer_receipts_immutable_update
43980
+ BEFORE UPDATE ON todos_task_subtree_transfer_receipts BEGIN
43981
+ SELECT RAISE(ABORT, 'todos task subtree transfer receipts are immutable');
43982
+ END;
43983
+ CREATE TRIGGER IF NOT EXISTS todos_task_subtree_transfer_receipts_immutable_delete
43984
+ BEFORE DELETE ON todos_task_subtree_transfer_receipts BEGIN
43985
+ SELECT RAISE(ABORT, 'todos task subtree transfer receipts are immutable');
43986
+ END;
43987
+ `;
43988
+ }
43989
+ function ensureSqliteTodosTaskSubtreeTransferSchema(db) {
43990
+ db.exec(sqliteTodosTaskSubtreeTransferSchemaSql());
43991
+ }
43992
+ function postgresTodosTaskSubtreeTransferSchemaSql() {
43993
+ return [
43994
+ `CREATE TABLE IF NOT EXISTS todos_task_subtree_transfer_receipts (
43995
+ receipt_id text PRIMARY KEY,
43996
+ tenant_id text NOT NULL,
43997
+ kind text NOT NULL CHECK(kind IN ('apply', 'rollback')),
43998
+ operation_id text NOT NULL,
43999
+ step_id text NOT NULL,
44000
+ idempotency_key text NOT NULL,
44001
+ request_digest text NOT NULL,
44002
+ precondition_digest text NOT NULL,
44003
+ result_digest text NOT NULL,
44004
+ apply_receipt_id text,
44005
+ request_json jsonb NOT NULL,
44006
+ result_json jsonb NOT NULL,
44007
+ created_at timestamptz NOT NULL,
44008
+ UNIQUE(tenant_id, kind, operation_id, step_id),
44009
+ UNIQUE(tenant_id, kind, idempotency_key)
44010
+ )`,
44011
+ `CREATE OR REPLACE FUNCTION reject_todos_task_subtree_transfer_receipt_mutation()
44012
+ RETURNS trigger LANGUAGE plpgsql AS $$
44013
+ BEGIN
44014
+ RAISE EXCEPTION 'todos task subtree transfer receipts are immutable';
44015
+ END $$`,
44016
+ `DROP TRIGGER IF EXISTS todos_task_subtree_transfer_receipts_immutable_update
44017
+ ON todos_task_subtree_transfer_receipts`,
44018
+ `CREATE TRIGGER todos_task_subtree_transfer_receipts_immutable_update
44019
+ BEFORE UPDATE OR DELETE ON todos_task_subtree_transfer_receipts
44020
+ FOR EACH ROW EXECUTE FUNCTION reject_todos_task_subtree_transfer_receipt_mutation()`
44021
+ ];
44022
+ }
44023
+
44024
+ // src/task-subtree-transfer/postgres.ts
44025
+ function safeIdentifier3(value, fallback) {
44026
+ const candidate = value ?? fallback;
44027
+ if (!/^[a-z_][a-z0-9_]*$/.test(candidate)) {
44028
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "PostgreSQL table name must be a safe identifier");
44029
+ }
44030
+ return candidate;
44031
+ }
44032
+ function timestamp4(value) {
44033
+ return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
44034
+ }
44035
+ function payloadOf(row) {
44036
+ return typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload;
44037
+ }
44038
+ function taskRecord(row) {
44039
+ const payload = payloadOf(row);
44040
+ return {
44041
+ task_id: String(payload.id ?? row.object_id),
44042
+ project_id: payload.project_id == null ? null : String(payload.project_id),
44043
+ parent_id: payload.parent_id == null ? null : String(payload.parent_id),
44044
+ plan_id: payload.plan_id == null ? null : String(payload.plan_id),
44045
+ task_list_id: payload.task_list_id == null ? null : String(payload.task_list_id),
44046
+ version: Number(payload.version ?? row.version ?? 1),
44047
+ updated_at: timestamp4(payload.updated_at ?? row.updated_at),
44048
+ archived_at: payload.archived_at == null ? null : String(payload.archived_at)
44049
+ };
44050
+ }
44051
+ function planRecord(row) {
44052
+ const payload = payloadOf(row);
44053
+ return {
44054
+ plan_id: String(payload.id ?? row.object_id),
44055
+ project_id: payload.project_id == null ? null : String(payload.project_id),
44056
+ task_list_id: payload.task_list_id == null ? null : String(payload.task_list_id),
44057
+ updated_at: timestamp4(payload.updated_at ?? row.updated_at)
44058
+ };
44059
+ }
44060
+
44061
+ class PostgresTodosTaskSubtreeTransferBackend {
44062
+ client;
44063
+ kind = "postgresql";
44064
+ service;
44065
+ tableName;
44066
+ tenantId;
44067
+ schemaReady = null;
44068
+ constructor(client, options = {}) {
44069
+ this.client = client;
44070
+ this.service = options.service ?? "todos";
44071
+ this.tableName = safeIdentifier3(options.tableName, DEFAULT_TODOS_POSTGRES_SYNC_TABLE);
44072
+ this.tenantId = options.tenantId ?? "default";
44073
+ }
44074
+ async ensureSchema() {
44075
+ this.schemaReady ??= (async () => {
44076
+ for (const sql of postgresTodosSyncSchemaSql(this.tableName))
44077
+ await this.client.query(sql);
44078
+ for (const sql of postgresTodosTaskSubtreeTransferSchemaSql())
44079
+ await this.client.query(sql);
44080
+ })();
44081
+ await this.schemaReady;
44082
+ }
44083
+ async snapshot(client, input, forUpdate = false) {
44084
+ const lock = forUpdate ? " FOR UPDATE" : "";
44085
+ const source9 = await client.query(`SELECT object_id, payload, updated_at, version FROM ${this.tableName}
44086
+ WHERE service = $1 AND object_type = 'tasks' AND deleted_at IS NULL
44087
+ AND payload->>'project_id' = $2 ORDER BY object_id${lock}`, [this.service, input.source_project_id]);
44088
+ const allTasks = await client.query(`SELECT object_id, payload, updated_at, version FROM ${this.tableName}
44089
+ WHERE service = $1 AND object_type = 'tasks' AND deleted_at IS NULL ORDER BY object_id${lock}`, [this.service]);
44090
+ const plans = await client.query(`SELECT object_id, payload, updated_at, version FROM ${this.tableName}
44091
+ WHERE service = $1 AND object_type = 'plans' AND deleted_at IS NULL ORDER BY object_id${lock}`, [this.service]);
44092
+ const destinationProject = await client.query(`SELECT 1 FROM ${this.tableName}
44093
+ WHERE service = $1 AND object_type = 'projects' AND object_id = $2
44094
+ AND deleted_at IS NULL LIMIT 1${lock}`, [this.service, input.destination_project_id]);
44095
+ const destinationList = await client.query(`SELECT 1 FROM ${this.tableName}
44096
+ WHERE service = $1 AND object_type = 'task_lists' AND object_id = $2
44097
+ AND deleted_at IS NULL AND payload->>'project_id' = $3 LIMIT 1`, [this.service, input.destination_task_list_id, input.destination_project_id]);
44098
+ const destinationParent = input.destination_parent_id === null ? true : (await client.query(`SELECT 1 FROM ${this.tableName}
44099
+ WHERE service = $1 AND object_type = 'tasks' AND object_id = $2
44100
+ AND deleted_at IS NULL AND payload->>'project_id' = $3 LIMIT 1`, [this.service, input.destination_parent_id, input.destination_project_id])).rows.length === 1;
44101
+ return {
44102
+ source_tasks: source9.rows.map(taskRecord),
44103
+ plan_tasks: allTasks.rows.map(taskRecord),
44104
+ plans: plans.rows.map(planRecord),
44105
+ destination_project_found: destinationProject.rows.length === 1,
44106
+ destination_task_list_found: destinationList.rows.length === 1,
44107
+ destination_parent_found: destinationParent
44108
+ };
44109
+ }
44110
+ async inspect(input) {
44111
+ await this.ensureSchema();
44112
+ return prepareTransfer(await this.snapshot(this.client, input), input).inspection;
44113
+ }
44114
+ async insertPayload(client, task3, update, now4) {
44115
+ const current = await client.query(`SELECT payload FROM ${this.tableName}
44116
+ WHERE service = $1 AND object_type = 'tasks' AND object_id = $2
44117
+ AND deleted_at IS NULL FOR UPDATE`, [this.service, task3.task_id]);
44118
+ const row = current.rows[0];
44119
+ if (!row)
44120
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND", `Task not found: ${task3.task_id}`);
44121
+ const payload = payloadOf(row);
44122
+ if (Number(payload.version ?? 1) !== task3.version) {
44123
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Task changed during transfer", { task_id: task3.task_id });
44124
+ }
44125
+ const next = {
44126
+ ...payload,
44127
+ ...update,
44128
+ version: task3.version + 1,
44129
+ updated_at: now4
44130
+ };
44131
+ const result = await client.query(`UPDATE ${this.tableName} SET payload = $1::jsonb, updated_at = $2, version = $3
44132
+ WHERE service = $4 AND object_type = 'tasks' AND object_id = $5
44133
+ AND deleted_at IS NULL AND version = $6
44134
+ RETURNING object_id`, [canonicalJson(next), now4, task3.version + 1, this.service, task3.task_id, task3.version]);
44135
+ if (result.rows.length !== 1) {
44136
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Task CAS failed", { task_id: task3.task_id });
44137
+ }
44138
+ }
44139
+ async apply(input, options) {
44140
+ await this.ensureSchema();
44141
+ if (!this.client.transaction) {
44142
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ATOMICITY_UNAVAILABLE", "PostgreSQL subtree transfer requires an authoritative transaction-capable client");
44143
+ }
44144
+ return this.client.transaction(async (tx) => {
44145
+ await tx.query("SELECT pg_advisory_xact_lock(hashtextextended('todos:task-parent-integrity', 0))");
44146
+ const requestDigest = taskSubtreeTransferRequestDigest(input);
44147
+ const existing = await tx.query(`SELECT result_json, request_digest, precondition_digest, idempotency_key
44148
+ FROM todos_task_subtree_transfer_receipts
44149
+ WHERE tenant_id = $1 AND kind = 'apply'
44150
+ AND (idempotency_key = $2 OR (operation_id = $3 AND step_id = $4))
44151
+ ORDER BY created_at ASC LIMIT 1`, [this.tenantId, input.idempotency_key, input.operation_id, input.step_id]);
44152
+ if (existing.rows[0]) {
44153
+ const row = existing.rows[0];
44154
+ if (row.request_digest !== requestDigest || row.precondition_digest !== input.precondition_digest || row.idempotency_key !== input.idempotency_key) {
44155
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_CONFLICT", "The operation was already accepted for a different request");
44156
+ }
44157
+ return { ...typeof row.result_json === "string" ? JSON.parse(row.result_json) : row.result_json, duplicate: true };
44158
+ }
44159
+ const snapshot = await this.snapshot(tx, input, true);
44160
+ const prepared = prepareTransfer(snapshot, input);
44161
+ validateApplySnapshot(prepared, input, snapshot.plans);
44162
+ const now4 = options.now?.() ?? new Date().toISOString();
44163
+ const priorImage = {
44164
+ tasks: prepared.prior_tasks,
44165
+ plans: prepared.prior_plans
44166
+ };
44167
+ for (const task3 of prepared.prior_tasks) {
44168
+ await this.insertPayload(tx, task3, {
44169
+ project_id: input.destination_project_id,
44170
+ task_list_id: input.destination_task_list_id,
44171
+ plan_id: prepared.task_plan_targets.get(task3.task_id) ?? null,
44172
+ parent_id: task3.task_id === input.root_task_id ? input.destination_parent_id : task3.parent_id
44173
+ }, now4);
44174
+ }
44175
+ if (options.faultInjector && await options.faultInjector("after_task_writes"))
44176
+ throw new Error("Injected task-subtree-transfer fault at after_task_writes");
44177
+ for (const plan of prepared.prior_plans) {
44178
+ const row = await tx.query(`SELECT payload FROM ${this.tableName}
44179
+ WHERE service = $1 AND object_type = 'plans' AND object_id = $2
44180
+ AND deleted_at IS NULL FOR UPDATE`, [this.service, plan.plan_id]);
44181
+ const current = row.rows[0] ? payloadOf(row.rows[0]) : null;
44182
+ if (!current || String(current.project_id ?? "") !== String(plan.project_id ?? "") || (current.task_list_id ?? null) !== plan.task_list_id || timestamp4(current.updated_at ?? row.rows[0]?.updated_at) !== plan.updated_at) {
44183
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Contained plan changed during transfer", { plan_id: plan.plan_id });
44184
+ }
44185
+ const result2 = await tx.query(`UPDATE ${this.tableName} SET payload = jsonb_set(jsonb_set(jsonb_set(payload, '{project_id}', to_jsonb($1::text)), '{task_list_id}', to_jsonb($2::text)), '{updated_at}', to_jsonb($3::text)), updated_at = $3
44186
+ WHERE service = $4 AND object_type = 'plans' AND object_id = $5 AND deleted_at IS NULL
44187
+ RETURNING object_id`, [input.destination_project_id, input.destination_task_list_id, now4, this.service, plan.plan_id]);
44188
+ if (result2.rows.length !== 1) {
44189
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Contained plan update did not affect exactly one row", { plan_id: plan.plan_id });
44190
+ }
44191
+ }
44192
+ if (options.faultInjector && await options.faultInjector("after_plan_writes"))
44193
+ throw new Error("Injected task-subtree-transfer fault at after_plan_writes");
44194
+ const postImage = {
44195
+ tasks: prepared.prior_tasks.map((task3) => ({
44196
+ ...task3,
44197
+ project_id: input.destination_project_id,
44198
+ task_list_id: input.destination_task_list_id,
44199
+ plan_id: prepared.task_plan_targets.get(task3.task_id) ?? null,
44200
+ parent_id: task3.task_id === input.root_task_id ? input.destination_parent_id : task3.parent_id,
44201
+ version: task3.version + 1,
44202
+ updated_at: now4
44203
+ })),
44204
+ plans: prepared.prior_plans.map((plan) => ({ ...plan, project_id: input.destination_project_id, task_list_id: input.destination_task_list_id, updated_at: now4 }))
44205
+ };
44206
+ const resultDigest = canonicalDigest({ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE, request_digest: requestDigest, prior_image: priorImage, post_image: postImage });
44207
+ const receipt = {
44208
+ receipt_id: deterministicUuid(TODOS_TASK_SUBTREE_TRANSFER_ROUTE, "apply", input.operation_id, input.step_id, input.idempotency_key, requestDigest),
44209
+ authority: "todos",
44210
+ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
44211
+ schema_version: 1,
44212
+ kind: "apply",
44213
+ operation_id: input.operation_id,
44214
+ step_id: input.step_id,
44215
+ idempotency_key: input.idempotency_key,
44216
+ request_digest: requestDigest,
44217
+ precondition_digest: input.precondition_digest,
44218
+ result_digest: resultDigest,
44219
+ apply_receipt_id: null,
44220
+ source_project_id: input.source_project_id,
44221
+ destination_project_id: input.destination_project_id,
44222
+ destination_task_list_id: input.destination_task_list_id,
44223
+ root_task_id: input.root_task_id,
44224
+ source_population_digest: input.source_population_digest,
44225
+ prior_image: priorImage,
44226
+ post_image: postImage,
44227
+ shared_plan_splits: input.shared_plan_splits,
44228
+ created_at: now4
44229
+ };
44230
+ const result = { duplicate: false, receipt, moved_task_ids: prepared.prior_tasks.map((task3) => task3.task_id), moved_plan_ids: prepared.prior_plans.map((plan) => plan.plan_id), complete: true };
44231
+ await tx.query(`INSERT INTO todos_task_subtree_transfer_receipts (
44232
+ receipt_id, tenant_id, kind, operation_id, step_id, idempotency_key,
44233
+ request_digest, precondition_digest, result_digest, apply_receipt_id,
44234
+ request_json, result_json, created_at
44235
+ ) VALUES ($1, $2, 'apply', $3, $4, $5, $6, $7, $8, NULL, $9::jsonb, $10::jsonb, $11)`, [receipt.receipt_id, this.tenantId, receipt.operation_id, receipt.step_id, receipt.idempotency_key, receipt.request_digest, receipt.precondition_digest, receipt.result_digest, canonicalJson(input), canonicalJson(result), now4]);
44236
+ if (options.faultInjector && await options.faultInjector("after_receipt_write"))
44237
+ throw new Error("Injected task-subtree-transfer fault at after_receipt_write");
44238
+ return result;
44239
+ });
44240
+ }
44241
+ async readExact(receiptId2) {
44242
+ await this.ensureSchema();
44243
+ const row = await this.client.query(`SELECT result_json FROM todos_task_subtree_transfer_receipts WHERE tenant_id = $1 AND receipt_id = $2 LIMIT 1`, [this.tenantId, receiptId2]);
44244
+ if (!row.rows[0])
44245
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND", `Transfer receipt not found: ${receiptId2}`);
44246
+ return { ...typeof row.rows[0].result_json === "string" ? JSON.parse(String(row.rows[0].result_json)) : row.rows[0].result_json, duplicate: false };
44247
+ }
44248
+ async rollback(input, options) {
44249
+ await this.ensureSchema();
44250
+ if (!this.client.transaction)
44251
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ATOMICITY_UNAVAILABLE", "PostgreSQL subtree rollback requires a transaction-capable client");
44252
+ return this.client.transaction(async (tx) => {
44253
+ await tx.query("SELECT pg_advisory_xact_lock(hashtextextended('todos:task-parent-integrity', 0))");
44254
+ const requestDigest = taskSubtreeTransferRollbackRequestDigest(input);
44255
+ const existing = await tx.query(`SELECT result_json, request_digest, precondition_digest, idempotency_key
44256
+ FROM todos_task_subtree_transfer_receipts
44257
+ WHERE tenant_id = $1 AND kind = 'rollback'
44258
+ AND (idempotency_key = $2 OR (operation_id = $3 AND step_id = $4))
44259
+ ORDER BY created_at ASC LIMIT 1`, [this.tenantId, input.idempotency_key, input.operation_id, input.step_id]);
44260
+ if (existing.rows[0]) {
44261
+ const row = existing.rows[0];
44262
+ if (row.request_digest !== requestDigest || row.precondition_digest !== input.precondition_digest || row.idempotency_key !== input.idempotency_key)
44263
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_CONFLICT", "The rollback operation was already accepted for a different request");
44264
+ return { ...typeof row.result_json === "string" ? JSON.parse(String(row.result_json)) : row.result_json, duplicate: true };
44265
+ }
44266
+ const applyRow = await tx.query(`SELECT result_json FROM todos_task_subtree_transfer_receipts WHERE tenant_id = $1 AND kind = 'apply' AND receipt_id = $2 LIMIT 1`, [this.tenantId, input.receipt_id]);
44267
+ if (!applyRow.rows[0])
44268
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND", `Apply receipt not found: ${input.receipt_id}`);
44269
+ const applied = typeof applyRow.rows[0].result_json === "string" ? JSON.parse(String(applyRow.rows[0].result_json)) : applyRow.rows[0].result_json;
44270
+ if (applied.receipt.operation_id !== input.operation_id) {
44271
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback operation does not match the apply receipt");
44272
+ }
44273
+ const expectedPrecondition = canonicalDigest({ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE, direction: "rollback", operation_id: input.operation_id, step_id: input.step_id, apply_receipt_id: input.receipt_id, apply_result_digest: applied.receipt.result_digest });
44274
+ if (input.precondition_digest !== expectedPrecondition)
44275
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_DIGEST_MISMATCH", "Rollback precondition digest does not match the exact apply receipt", { expected_precondition_digest: expectedPrecondition });
44276
+ const now4 = options.now?.() ?? new Date().toISOString();
44277
+ for (const task3 of applied.receipt.post_image.tasks) {
44278
+ const row = await tx.query(`SELECT payload FROM ${this.tableName} WHERE service = $1 AND object_type = 'tasks' AND object_id = $2 AND deleted_at IS NULL FOR UPDATE`, [this.service, task3.task_id]);
44279
+ if (!row.rows[0])
44280
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Transferred task disappeared", { task_id: task3.task_id });
44281
+ const payload = payloadOf(row.rows[0]);
44282
+ if (Number(payload.version ?? 1) !== task3.version || String(payload.project_id ?? "") !== String(task3.project_id ?? "") || (payload.parent_id ?? null) !== task3.parent_id || (payload.plan_id ?? null) !== task3.plan_id || (payload.task_list_id ?? null) !== task3.task_list_id)
44283
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Transferred task changed after apply", { task_id: task3.task_id });
44284
+ }
44285
+ for (const task3 of applied.receipt.prior_image.tasks) {
44286
+ const row = await tx.query(`SELECT payload FROM ${this.tableName} WHERE service = $1 AND object_type = 'tasks' AND object_id = $2 AND deleted_at IS NULL`, [this.service, task3.task_id]);
44287
+ const payload = row.rows[0] ? payloadOf(row.rows[0]) : null;
44288
+ if (!payload)
44289
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Task disappeared during rollback", { task_id: task3.task_id });
44290
+ const next = { ...payload, project_id: task3.project_id, task_list_id: task3.task_list_id, plan_id: task3.plan_id, parent_id: task3.parent_id, version: task3.version + 2, updated_at: now4 };
44291
+ const result2 = await tx.query(`UPDATE ${this.tableName} SET payload = $1::jsonb, updated_at = $2, version = $3
44292
+ WHERE service = $4 AND object_type = 'tasks' AND object_id = $5 AND deleted_at IS NULL AND version = $6
44293
+ RETURNING object_id`, [canonicalJson(next), now4, task3.version + 2, this.service, task3.task_id, task3.version + 1]);
44294
+ if (result2.rows.length !== 1) {
44295
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Task rollback did not affect exactly one row", { task_id: task3.task_id });
44296
+ }
44297
+ }
44298
+ if (options.faultInjector && await options.faultInjector("after_rollback_task_writes"))
44299
+ throw new Error("Injected task-subtree-transfer fault at after_rollback_task_writes");
44300
+ for (const plan of applied.receipt.prior_image.plans) {
44301
+ const post = applied.receipt.post_image.plans.find((candidate) => candidate.plan_id === plan.plan_id);
44302
+ const result2 = await tx.query(`UPDATE ${this.tableName}
44303
+ SET payload = jsonb_set(
44304
+ jsonb_set(
44305
+ jsonb_set(payload, '{project_id}', to_jsonb($1::text)),
44306
+ '{task_list_id}',
44307
+ COALESCE(to_jsonb($2::text), 'null'::jsonb)
44308
+ ),
44309
+ '{updated_at}',
44310
+ to_jsonb($3::text)
44311
+ ), updated_at = $3
44312
+ WHERE service = $4 AND object_type = 'plans' AND object_id = $5 AND deleted_at IS NULL
44313
+ AND payload->>'project_id' = $6 AND COALESCE(payload->>'task_list_id', '') = COALESCE($7, '') AND payload->>'updated_at' = $8
44314
+ RETURNING object_id`, [plan.project_id, plan.task_list_id, now4, this.service, plan.plan_id, post.project_id, post.task_list_id, post.updated_at]);
44315
+ if (result2.rows.length !== 1) {
44316
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Contained plan rollback did not affect exactly one row", { plan_id: plan.plan_id });
44317
+ }
44318
+ }
44319
+ if (options.faultInjector && await options.faultInjector("after_rollback_plan_writes"))
44320
+ throw new Error("Injected task-subtree-transfer fault at after_rollback_plan_writes");
44321
+ const restored = {
44322
+ tasks: applied.receipt.prior_image.tasks.map((task3) => ({ ...task3, version: task3.version + 2, updated_at: now4 })),
44323
+ plans: applied.receipt.prior_image.plans.map((plan) => ({ ...plan, updated_at: now4 }))
44324
+ };
44325
+ const resultDigest = canonicalDigest({ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE, direction: "rollback", apply_receipt_id: input.receipt_id, prior_image: applied.receipt.post_image, post_image: restored });
44326
+ const receipt = { ...applied.receipt, receipt_id: deterministicUuid(TODOS_TASK_SUBTREE_TRANSFER_ROUTE, "rollback", input.operation_id, input.step_id, input.idempotency_key, input.receipt_id), kind: "rollback", operation_id: input.operation_id, step_id: input.step_id, idempotency_key: input.idempotency_key, request_digest: requestDigest, precondition_digest: input.precondition_digest, result_digest: resultDigest, apply_receipt_id: input.receipt_id, prior_image: applied.receipt.post_image, post_image: restored, created_at: now4 };
44327
+ const result = { duplicate: false, receipt, moved_task_ids: applied.moved_task_ids, moved_plan_ids: applied.moved_plan_ids, complete: true };
44328
+ await tx.query(`INSERT INTO todos_task_subtree_transfer_receipts (
44329
+ receipt_id, tenant_id, kind, operation_id, step_id, idempotency_key,
44330
+ request_digest, precondition_digest, result_digest, apply_receipt_id,
44331
+ request_json, result_json, created_at
44332
+ ) VALUES ($1, $2, 'rollback', $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11::jsonb, $12)`, [receipt.receipt_id, this.tenantId, receipt.operation_id, receipt.step_id, receipt.idempotency_key, receipt.request_digest, receipt.precondition_digest, receipt.result_digest, receipt.apply_receipt_id, canonicalJson(input), canonicalJson(result), now4]);
44333
+ if (options.faultInjector && await options.faultInjector("after_rollback_receipt_write"))
44334
+ throw new Error("Injected task-subtree-transfer fault at after_rollback_receipt_write");
44335
+ return result;
44336
+ });
44337
+ }
44338
+ }
44339
+
44340
+ // src/task-subtree-transfer/sqlite.ts
44341
+ var sqliteTails2 = new WeakMap;
44342
+ async function fault3(options, point) {
44343
+ if (options.faultInjector && await options.faultInjector(point) === true) {
44344
+ throw new Error(`Injected task-subtree-transfer fault at ${point}`);
44345
+ }
44346
+ }
44347
+ function parseResult(value, duplicate) {
44348
+ return { ...JSON.parse(value), duplicate };
44349
+ }
44350
+ function rowImage(row) {
44351
+ return {
44352
+ task_id: String(row.id),
44353
+ project_id: row.project_id == null ? null : String(row.project_id),
44354
+ parent_id: row.parent_id == null ? null : String(row.parent_id),
44355
+ plan_id: row.plan_id == null ? null : String(row.plan_id),
44356
+ task_list_id: row.task_list_id == null ? null : String(row.task_list_id),
44357
+ version: Number(row.version),
44358
+ updated_at: String(row.updated_at),
44359
+ archived_at: row.archived_at == null ? null : String(row.archived_at)
44360
+ };
44361
+ }
44362
+ function planImage(row) {
44363
+ return {
44364
+ plan_id: String(row.id),
44365
+ project_id: row.project_id == null ? null : String(row.project_id),
44366
+ task_list_id: row.task_list_id == null ? null : String(row.task_list_id),
44367
+ updated_at: String(row.updated_at)
44368
+ };
44369
+ }
44370
+ function imageFromTaskRow(row) {
44371
+ const image = rowImage(row);
44372
+ const { archived_at: _archivedAt, ...withoutArchive } = image;
44373
+ return withoutArchive;
44374
+ }
44375
+ function imageFromPlanRow(row) {
44376
+ return planImage(row);
44377
+ }
44378
+ function receiptId2(input) {
44379
+ return deterministicUuid(TODOS_TASK_SUBTREE_TRANSFER_ROUTE, "apply", input.operation_id, input.step_id, input.idempotency_key, taskSubtreeTransferRequestDigest(input));
44380
+ }
44381
+ function rollbackReceiptId(input) {
44382
+ return deterministicUuid(TODOS_TASK_SUBTREE_TRANSFER_ROUTE, "rollback", input.operation_id, input.step_id, input.idempotency_key, input.receipt_id);
44383
+ }
44384
+ function taskWhere(db, taskId) {
44385
+ const row = db.query(`SELECT id, project_id, parent_id, plan_id, task_list_id, version, updated_at, archived_at
44386
+ FROM tasks WHERE id = ? LIMIT 1`).get(taskId);
44387
+ if (!row) {
44388
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND", `Task not found: ${taskId}`, { task_id: taskId });
44389
+ }
44390
+ return row;
44391
+ }
44392
+
44393
+ class SqliteTodosTaskSubtreeTransferBackend {
44394
+ db;
44395
+ tenantId;
44396
+ kind = "sqlite";
44397
+ constructor(db, tenantId = "default") {
44398
+ this.db = db;
44399
+ this.tenantId = tenantId;
44400
+ ensureSqliteTodosTaskSubtreeTransferSchema(db);
44401
+ }
44402
+ async serialized(run) {
44403
+ const previous = sqliteTails2.get(this.db) ?? Promise.resolve();
44404
+ let release;
44405
+ const current = new Promise((resolve10) => {
44406
+ release = resolve10;
44407
+ });
44408
+ const tail = previous.then(() => current);
44409
+ sqliteTails2.set(this.db, tail);
44410
+ await previous;
44411
+ try {
44412
+ this.db.exec("BEGIN IMMEDIATE");
44413
+ try {
44414
+ const result = await run();
44415
+ this.db.exec("COMMIT");
44416
+ return result;
44417
+ } catch (error) {
44418
+ this.db.exec("ROLLBACK");
44419
+ throw error;
44420
+ }
44421
+ } finally {
44422
+ release();
44423
+ if (sqliteTails2.get(this.db) === tail)
44424
+ sqliteTails2.delete(this.db);
44425
+ }
44426
+ }
44427
+ snapshot(input) {
44428
+ const sourceTasks = this.db.query(`SELECT id, project_id, parent_id, plan_id, task_list_id, version, updated_at, archived_at
44429
+ FROM tasks WHERE project_id = ? ORDER BY id`).all(input.source_project_id).map(rowImage);
44430
+ const allTasks = this.db.query(`SELECT id, project_id, parent_id, plan_id, task_list_id, version, updated_at, archived_at
44431
+ FROM tasks ORDER BY id`).all().map(rowImage);
44432
+ const plans = this.db.query(`SELECT id, project_id, task_list_id, updated_at
44433
+ FROM plans ORDER BY id`).all().map(planImage);
44434
+ const destinationProjectFound = Boolean(this.db.query("SELECT id FROM projects WHERE id = ? LIMIT 1").get(input.destination_project_id));
44435
+ const destinationTaskList = this.db.query("SELECT id FROM task_lists WHERE id = ? AND project_id = ? LIMIT 1").get(input.destination_task_list_id, input.destination_project_id);
44436
+ const destinationParentFound = input.destination_parent_id === null || Boolean(this.db.query("SELECT id FROM tasks WHERE id = ? AND project_id = ? LIMIT 1").get(input.destination_parent_id, input.destination_project_id));
44437
+ return {
44438
+ source_tasks: sourceTasks,
44439
+ plan_tasks: allTasks,
44440
+ plans,
44441
+ destination_project_found: destinationProjectFound,
44442
+ destination_task_list_found: Boolean(destinationTaskList),
44443
+ destination_parent_found: destinationParentFound
44444
+ };
44445
+ }
44446
+ async inspect(input) {
44447
+ return this.serialized(() => prepareTransfer(this.snapshot(input), input).inspection);
44448
+ }
44449
+ async apply(input, options) {
44450
+ return this.serialized(async () => {
44451
+ const requestDigest = taskSubtreeTransferRequestDigest(input);
44452
+ const existing = this.db.query(`SELECT result_json, request_digest, precondition_digest, idempotency_key
44453
+ FROM todos_task_subtree_transfer_receipts
44454
+ WHERE tenant_id = ? AND kind = 'apply'
44455
+ AND (idempotency_key = ? OR (operation_id = ? AND step_id = ?))
44456
+ ORDER BY created_at ASC LIMIT 1`).get(this.tenantId, input.idempotency_key, input.operation_id, input.step_id);
44457
+ if (existing) {
44458
+ if (existing.request_digest !== requestDigest || existing.precondition_digest !== input.precondition_digest || existing.idempotency_key !== input.idempotency_key) {
44459
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_CONFLICT", "The operation/step or idempotency key was already accepted for a different request");
44460
+ }
44461
+ return parseResult(String(existing.result_json), true);
44462
+ }
44463
+ const snapshot = this.snapshot(input);
44464
+ const prepared = prepareTransfer(snapshot, input);
44465
+ validateApplySnapshot(prepared, input, snapshot.plans);
44466
+ const priorImage = {
44467
+ tasks: prepared.prior_tasks,
44468
+ plans: prepared.prior_plans
44469
+ };
44470
+ const now4 = options.now?.() ?? new Date().toISOString();
44471
+ const movedTaskIds = prepared.prior_tasks.map((task3) => task3.task_id);
44472
+ for (const task3 of prepared.prior_tasks) {
44473
+ const current = imageFromTaskRow(taskWhere(this.db, task3.task_id));
44474
+ if (canonicalJson(current) !== canonicalJson(task3)) {
44475
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "A task changed after inspection", { task_id: task3.task_id });
44476
+ }
44477
+ }
44478
+ for (const task3 of prepared.prior_tasks) {
44479
+ const targetPlan = prepared.task_plan_targets.get(task3.task_id) ?? null;
44480
+ const parentId = task3.task_id === input.root_task_id ? input.destination_parent_id : task3.parent_id;
44481
+ const result2 = this.db.query(`UPDATE tasks
44482
+ SET project_id = ?, task_list_id = ?, plan_id = ?, parent_id = ?, version = version + 1, updated_at = ?
44483
+ WHERE id = ? AND version = ?`).run(input.destination_project_id, input.destination_task_list_id, targetPlan, parentId, now4, task3.task_id, task3.version);
44484
+ if (result2.changes !== 1) {
44485
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "A task version changed during the atomic transfer", { task_id: task3.task_id });
44486
+ }
44487
+ }
44488
+ await fault3(options, "after_task_writes");
44489
+ for (const plan of prepared.prior_plans) {
44490
+ const result2 = this.db.query(`UPDATE plans SET project_id = ?, task_list_id = ?, updated_at = ?
44491
+ WHERE id = ? AND project_id IS ? AND task_list_id IS ? AND updated_at = ?`).run(input.destination_project_id, input.destination_task_list_id, now4, plan.plan_id, plan.project_id, plan.task_list_id, plan.updated_at);
44492
+ if (result2.changes !== 1) {
44493
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "A contained plan changed during the atomic transfer", { plan_id: plan.plan_id });
44494
+ }
44495
+ }
44496
+ await fault3(options, "after_plan_writes");
44497
+ const postImage = {
44498
+ tasks: prepared.prior_tasks.map((task3) => ({
44499
+ ...task3,
44500
+ project_id: input.destination_project_id,
44501
+ task_list_id: input.destination_task_list_id,
44502
+ plan_id: prepared.task_plan_targets.get(task3.task_id) ?? null,
44503
+ parent_id: task3.task_id === input.root_task_id ? input.destination_parent_id : task3.parent_id,
44504
+ version: task3.version + 1,
44505
+ updated_at: now4
44506
+ })),
44507
+ plans: prepared.prior_plans.map((plan) => ({
44508
+ ...plan,
44509
+ project_id: input.destination_project_id,
44510
+ task_list_id: input.destination_task_list_id,
44511
+ updated_at: now4
44512
+ }))
44513
+ };
44514
+ const resultDigest = canonicalDigest({
44515
+ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
44516
+ request_digest: requestDigest,
44517
+ prior_image: priorImage,
44518
+ post_image: postImage
44519
+ });
44520
+ const receipt = {
44521
+ receipt_id: receiptId2(input),
44522
+ authority: "todos",
44523
+ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
44524
+ schema_version: 1,
44525
+ kind: "apply",
44526
+ operation_id: input.operation_id,
44527
+ step_id: input.step_id,
44528
+ idempotency_key: input.idempotency_key,
44529
+ request_digest: requestDigest,
44530
+ precondition_digest: input.precondition_digest,
44531
+ result_digest: resultDigest,
44532
+ apply_receipt_id: null,
44533
+ source_project_id: input.source_project_id,
44534
+ destination_project_id: input.destination_project_id,
44535
+ destination_task_list_id: input.destination_task_list_id,
44536
+ root_task_id: input.root_task_id,
44537
+ source_population_digest: input.source_population_digest,
44538
+ prior_image: priorImage,
44539
+ post_image: postImage,
44540
+ shared_plan_splits: input.shared_plan_splits,
44541
+ created_at: now4
44542
+ };
44543
+ const result = {
44544
+ duplicate: false,
44545
+ receipt,
44546
+ moved_task_ids: movedTaskIds,
44547
+ moved_plan_ids: prepared.prior_plans.map((plan) => plan.plan_id),
44548
+ complete: true
44549
+ };
44550
+ this.db.query(`INSERT INTO todos_task_subtree_transfer_receipts (
44551
+ receipt_id, tenant_id, kind, operation_id, step_id, idempotency_key,
44552
+ request_digest, precondition_digest, result_digest, apply_receipt_id,
44553
+ request_json, result_json, created_at
44554
+ ) VALUES (?, ?, 'apply', ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)`).run(receipt.receipt_id, this.tenantId, receipt.operation_id, receipt.step_id, receipt.idempotency_key, receipt.request_digest, receipt.precondition_digest, receipt.result_digest, canonicalJson(input), canonicalJson(result), now4);
44555
+ await fault3(options, "after_receipt_write");
44556
+ return result;
44557
+ });
44558
+ }
44559
+ async readExact(receiptIdValue) {
44560
+ const row = this.db.query(`SELECT result_json FROM todos_task_subtree_transfer_receipts
44561
+ WHERE tenant_id = ? AND receipt_id = ? LIMIT 1`).get(this.tenantId, receiptIdValue);
44562
+ if (!row) {
44563
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND", `Transfer receipt not found: ${receiptIdValue}`);
44564
+ }
44565
+ return parseResult(String(row.result_json), false);
44566
+ }
44567
+ async rollback(input, options) {
44568
+ return this.serialized(async () => {
44569
+ const requestDigest = taskSubtreeTransferRollbackRequestDigest(input);
44570
+ const existing = this.db.query(`SELECT result_json, request_digest, precondition_digest, idempotency_key
44571
+ FROM todos_task_subtree_transfer_receipts
44572
+ WHERE tenant_id = ? AND kind = 'rollback'
44573
+ AND (idempotency_key = ? OR (operation_id = ? AND step_id = ?))
44574
+ ORDER BY created_at ASC LIMIT 1`).get(this.tenantId, input.idempotency_key, input.operation_id, input.step_id);
44575
+ if (existing) {
44576
+ if (existing.request_digest !== requestDigest || existing.precondition_digest !== input.precondition_digest || existing.idempotency_key !== input.idempotency_key) {
44577
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_CONFLICT", "The rollback operation/step or idempotency key was already accepted for a different request");
44578
+ }
44579
+ return parseResult(String(existing.result_json), true);
44580
+ }
44581
+ const applyRow = this.db.query(`SELECT result_json FROM todos_task_subtree_transfer_receipts
44582
+ WHERE tenant_id = ? AND kind = 'apply' AND receipt_id = ? LIMIT 1`).get(this.tenantId, input.receipt_id);
44583
+ if (!applyRow) {
44584
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND", `Apply receipt not found: ${input.receipt_id}`);
44585
+ }
44586
+ const applied = parseResult(String(applyRow.result_json), false);
44587
+ if (applied.receipt.operation_id !== input.operation_id) {
44588
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback operation does not match the apply receipt");
44589
+ }
44590
+ const expectedPrecondition = canonicalDigest({
44591
+ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
44592
+ direction: "rollback",
44593
+ operation_id: input.operation_id,
44594
+ step_id: input.step_id,
44595
+ apply_receipt_id: input.receipt_id,
44596
+ apply_result_digest: applied.receipt.result_digest
44597
+ });
44598
+ if (input.precondition_digest !== expectedPrecondition) {
44599
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_DIGEST_MISMATCH", "Rollback precondition digest does not match the exact apply receipt", { expected_precondition_digest: expectedPrecondition });
44600
+ }
44601
+ const now4 = options.now?.() ?? new Date().toISOString();
44602
+ for (const task3 of applied.receipt.post_image.tasks) {
44603
+ const current = imageFromTaskRow(taskWhere(this.db, task3.task_id));
44604
+ if (canonicalJson(current) !== canonicalJson(task3)) {
44605
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback refused because a transferred task changed after apply", { task_id: task3.task_id });
44606
+ }
44607
+ }
44608
+ for (const plan of applied.receipt.post_image.plans) {
44609
+ const row = this.db.query("SELECT id, project_id, task_list_id, updated_at FROM plans WHERE id = ? LIMIT 1").get(plan.plan_id);
44610
+ if (!row || canonicalJson(imageFromPlanRow(row)) !== canonicalJson(plan)) {
44611
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback refused because a transferred plan changed after apply", { plan_id: plan.plan_id });
44612
+ }
44613
+ }
44614
+ for (const task3 of applied.receipt.prior_image.tasks) {
44615
+ const result2 = this.db.query(`UPDATE tasks SET project_id = ?, task_list_id = ?, plan_id = ?, parent_id = ?,
44616
+ version = version + 1, updated_at = ?
44617
+ WHERE id = ? AND version = ?`).run(task3.project_id, task3.task_list_id, task3.plan_id, task3.parent_id, now4, task3.task_id, applied.receipt.post_image.tasks.find((candidate) => candidate.task_id === task3.task_id).version);
44618
+ if (result2.changes !== 1) {
44619
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback task CAS failed", { task_id: task3.task_id });
44620
+ }
44621
+ }
44622
+ await fault3(options, "after_rollback_task_writes");
44623
+ for (const plan of applied.receipt.prior_image.plans) {
44624
+ const post = applied.receipt.post_image.plans.find((candidate) => candidate.plan_id === plan.plan_id);
44625
+ const result2 = this.db.query(`UPDATE plans SET project_id = ?, task_list_id = ?, updated_at = ?
44626
+ WHERE id = ? AND project_id IS ? AND task_list_id IS ? AND updated_at = ?`).run(plan.project_id, plan.task_list_id, now4, plan.plan_id, post.project_id, post.task_list_id, post.updated_at);
44627
+ if (result2.changes !== 1) {
44628
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback plan CAS failed", { plan_id: plan.plan_id });
44629
+ }
44630
+ }
44631
+ await fault3(options, "after_rollback_plan_writes");
44632
+ const restored = {
44633
+ tasks: applied.receipt.prior_image.tasks.map((task3) => ({
44634
+ ...task3,
44635
+ version: applied.receipt.post_image.tasks.find((candidate) => candidate.task_id === task3.task_id).version + 1,
44636
+ updated_at: now4
44637
+ })),
44638
+ plans: applied.receipt.prior_image.plans.map((plan) => ({ ...plan, updated_at: now4 }))
44639
+ };
44640
+ const resultDigest = canonicalDigest({
44641
+ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
44642
+ direction: "rollback",
44643
+ apply_receipt_id: input.receipt_id,
44644
+ prior_image: applied.receipt.post_image,
44645
+ post_image: restored
44646
+ });
44647
+ const receipt = {
44648
+ ...applied.receipt,
44649
+ receipt_id: rollbackReceiptId(input),
44650
+ kind: "rollback",
44651
+ step_id: input.step_id,
44652
+ idempotency_key: input.idempotency_key,
44653
+ request_digest: requestDigest,
44654
+ precondition_digest: input.precondition_digest,
44655
+ result_digest: resultDigest,
44656
+ apply_receipt_id: input.receipt_id,
44657
+ prior_image: applied.receipt.post_image,
44658
+ post_image: restored,
44659
+ created_at: now4
44660
+ };
44661
+ const result = {
44662
+ duplicate: false,
44663
+ receipt,
44664
+ moved_task_ids: applied.moved_task_ids,
44665
+ moved_plan_ids: applied.moved_plan_ids,
44666
+ complete: true
44667
+ };
44668
+ this.db.query(`INSERT INTO todos_task_subtree_transfer_receipts (
44669
+ receipt_id, tenant_id, kind, operation_id, step_id, idempotency_key,
44670
+ request_digest, precondition_digest, result_digest, apply_receipt_id,
44671
+ request_json, result_json, created_at
44672
+ ) VALUES (?, ?, 'rollback', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(receipt.receipt_id, this.tenantId, receipt.operation_id, receipt.step_id, receipt.idempotency_key, receipt.request_digest, receipt.precondition_digest, receipt.result_digest, receipt.apply_receipt_id, canonicalJson(input), canonicalJson(result), now4);
44673
+ await fault3(options, "after_rollback_receipt_write");
44674
+ return result;
44675
+ });
44676
+ }
44677
+ }
44678
+
44679
+ // src/task-subtree-transfer/authority.ts
44680
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
44681
+ var DIGEST = /^[0-9a-f]{64}$/i;
44682
+ function boundedString(value, field2, max = 200) {
44683
+ if (typeof value !== "string" || value.length === 0 || value.length > max) {
44684
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `${field2} must be a non-empty bounded string`);
44685
+ }
44686
+ return value;
44687
+ }
44688
+ function uuid3(value, field2) {
44689
+ const parsed = boundedString(value, field2, 80);
44690
+ if (!UUID.test(parsed)) {
44691
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `${field2} must be a UUID`);
44692
+ }
44693
+ return parsed;
44694
+ }
44695
+ function digest2(value, field2) {
44696
+ const parsed = boundedString(value, field2, 64);
44697
+ if (!DIGEST.test(parsed)) {
44698
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `${field2} must be a SHA-256 digest`);
44699
+ }
44700
+ return parsed.toLowerCase();
44701
+ }
44702
+ function nullableUuid(value, field2) {
44703
+ if (value === null)
44704
+ return null;
44705
+ return uuid3(value, field2);
44706
+ }
44707
+ function strictObject(input, fields) {
44708
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
44709
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Request must be a JSON object");
44710
+ }
44711
+ const object = input;
44712
+ const allowed = new Set(fields);
44713
+ for (const key2 of Object.keys(object)) {
44714
+ if (!allowed.has(key2)) {
44715
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `Unknown field: ${key2}`);
44716
+ }
44717
+ }
44718
+ return object;
44719
+ }
44720
+ var INSPECT_FIELDS = [
44721
+ "source_project_id",
44722
+ "destination_project_id",
44723
+ "destination_task_list_id",
44724
+ "root_task_id",
44725
+ "destination_parent_id"
44726
+ ];
44727
+ function parseTodosTaskSubtreeTransferInspect(input) {
44728
+ const value = strictObject(input, INSPECT_FIELDS);
44729
+ const request = {
44730
+ source_project_id: uuid3(value.source_project_id, "source_project_id"),
44731
+ destination_project_id: uuid3(value.destination_project_id, "destination_project_id"),
44732
+ destination_task_list_id: uuid3(value.destination_task_list_id, "destination_task_list_id"),
44733
+ root_task_id: uuid3(value.root_task_id, "root_task_id"),
44734
+ destination_parent_id: nullableUuid(value.destination_parent_id, "destination_parent_id")
44735
+ };
44736
+ if (request.source_project_id === request.destination_project_id) {
44737
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "source_project_id and destination_project_id must identify different Projects");
44738
+ }
44739
+ return request;
44740
+ }
44741
+ var APPLY_FIELDS = [
44742
+ ...INSPECT_FIELDS,
44743
+ "version",
44744
+ "operation_id",
44745
+ "step_id",
44746
+ "idempotency_key",
44747
+ "precondition_digest",
44748
+ "expected_root_parent_id",
44749
+ "source_population_digest",
44750
+ "expected_tasks",
44751
+ "shared_plan_splits"
44752
+ ];
44753
+ function parseTodosTaskSubtreeTransferApply(input) {
44754
+ const value = strictObject(input, APPLY_FIELDS);
44755
+ if (value.version !== 1) {
44756
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "version must be 1");
44757
+ }
44758
+ if (!Array.isArray(value.expected_tasks) || value.expected_tasks.length === 0) {
44759
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "expected_tasks must be a non-empty array");
44760
+ }
44761
+ const expected_tasks = value.expected_tasks.map((item, index) => {
44762
+ const row = strictObject(item, ["task_id", "version"]);
44763
+ const version = row.version;
44764
+ if (!Number.isSafeInteger(version) || Number(version) < 1) {
44765
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `expected_tasks.${index}.version must be a positive integer`);
44766
+ }
44767
+ return { task_id: uuid3(row.task_id, `expected_tasks.${index}.task_id`), version: Number(version) };
44768
+ });
44769
+ const ids2 = new Set;
44770
+ for (const row of expected_tasks) {
44771
+ if (ids2.has(row.task_id)) {
44772
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "expected_tasks must not contain duplicate task IDs");
44773
+ }
44774
+ ids2.add(row.task_id);
44775
+ }
44776
+ if (!Array.isArray(value.shared_plan_splits)) {
44777
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "shared_plan_splits must be an array");
44778
+ }
44779
+ const shared_plan_splits = value.shared_plan_splits.map((item, index) => {
44780
+ const row = strictObject(item, ["source_plan_id", "destination_plan_id"]);
44781
+ return {
44782
+ source_plan_id: uuid3(row.source_plan_id, `shared_plan_splits.${index}.source_plan_id`),
44783
+ destination_plan_id: uuid3(row.destination_plan_id, `shared_plan_splits.${index}.destination_plan_id`)
44784
+ };
44785
+ });
44786
+ return {
44787
+ ...parseTodosTaskSubtreeTransferInspect({
44788
+ source_project_id: value.source_project_id,
44789
+ destination_project_id: value.destination_project_id,
44790
+ destination_task_list_id: value.destination_task_list_id,
44791
+ root_task_id: value.root_task_id,
44792
+ destination_parent_id: value.destination_parent_id
44793
+ }),
44794
+ version: 1,
44795
+ operation_id: boundedString(value.operation_id, "operation_id"),
44796
+ step_id: boundedString(value.step_id, "step_id"),
44797
+ idempotency_key: boundedString(value.idempotency_key, "idempotency_key", 240),
44798
+ precondition_digest: digest2(value.precondition_digest, "precondition_digest"),
44799
+ expected_root_parent_id: nullableUuid(value.expected_root_parent_id, "expected_root_parent_id"),
44800
+ source_population_digest: digest2(value.source_population_digest, "source_population_digest"),
44801
+ expected_tasks,
44802
+ shared_plan_splits
44803
+ };
44804
+ }
44805
+ function parseTodosTaskSubtreeTransferRollback(input) {
44806
+ const value = strictObject(input, [
44807
+ "receipt_id",
44808
+ "operation_id",
44809
+ "step_id",
44810
+ "idempotency_key",
44811
+ "precondition_digest"
44812
+ ]);
44813
+ return {
44814
+ receipt_id: uuid3(value.receipt_id, "receipt_id"),
44815
+ operation_id: boundedString(value.operation_id, "operation_id"),
44816
+ step_id: boundedString(value.step_id, "step_id"),
44817
+ idempotency_key: boundedString(value.idempotency_key, "idempotency_key", 240),
44818
+ precondition_digest: digest2(value.precondition_digest, "precondition_digest")
44819
+ };
44820
+ }
44821
+ function deriveTodosTaskSubtreeTransferApplyPreconditionDigest(input) {
44822
+ const {
44823
+ source_project_id,
44824
+ destination_project_id,
44825
+ destination_task_list_id,
44826
+ root_task_id,
44827
+ expected_root_parent_id,
44828
+ destination_parent_id,
44829
+ source_population_digest,
44830
+ expected_tasks,
44831
+ shared_plan_splits
44832
+ } = input;
44833
+ return canonicalDigest({
44834
+ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
44835
+ direction: "apply",
44836
+ source_project_id,
44837
+ destination_project_id,
44838
+ destination_task_list_id,
44839
+ root_task_id,
44840
+ expected_root_parent_id,
44841
+ destination_parent_id,
44842
+ source_population_digest,
44843
+ expected_tasks,
44844
+ shared_plan_splits
44845
+ });
44846
+ }
44847
+ function deriveTodosTaskSubtreeTransferRollbackPreconditionDigest(input) {
44848
+ return canonicalDigest({
44849
+ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
44850
+ direction: "rollback",
44851
+ apply_receipt_id: input.receipt_id,
44852
+ operation_id: input.operation_id,
44853
+ step_id: input.step_id,
44854
+ apply_result_digest: input.apply_result_digest
44855
+ });
44856
+ }
44857
+ function resolveTenantId2(value) {
44858
+ const tenant = value ?? "default";
44859
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/.test(tenant)) {
44860
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "tenantId must be a bounded exact authority identifier");
44861
+ }
44862
+ return tenant;
44863
+ }
44864
+
44865
+ class PackageOwnedTodosTaskSubtreeTransferAuthority {
44866
+ backend;
44867
+ tenantId;
44868
+ backendOptions;
44869
+ constructor(backend, options = {}) {
44870
+ this.backend = backend;
44871
+ this.tenantId = resolveTenantId2(options.tenantId);
44872
+ this.backendOptions = { ...options, tenantId: this.tenantId };
44873
+ }
44874
+ async capability() {
44875
+ return {
44876
+ authority: "todos",
44877
+ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
44878
+ schema_version: TODOS_TASK_SUBTREE_TRANSFER_SCHEMA_VERSION,
44879
+ tenant_id: this.tenantId,
44880
+ backend: this.backend.kind,
44881
+ exact_descendant_closure: true,
44882
+ complete_source_population_digest: true,
44883
+ per_task_version_cas: true,
44884
+ explicit_shared_plan_splits: true,
44885
+ atomic_apply: true,
44886
+ immutable_forward_inverse_receipts: true,
44887
+ prior_image_receipts: true,
44888
+ cas_protected_rollback: true,
44889
+ preserves_descendant_parent_ids: true,
44890
+ preserves_task_and_relation_identities: true
44891
+ };
44892
+ }
44893
+ async inspect(input) {
44894
+ return this.backend.inspect(parseTodosTaskSubtreeTransferInspect(input));
44895
+ }
44896
+ async apply(input) {
44897
+ const request = parseTodosTaskSubtreeTransferApply(input);
44898
+ const expected = deriveTodosTaskSubtreeTransferApplyPreconditionDigest(request);
44899
+ if (request.precondition_digest !== expected) {
44900
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_DIGEST_MISMATCH", "precondition_digest does not match the exact transfer target and source snapshot", { expected_precondition_digest: expected });
44901
+ }
44902
+ const requestDigest = taskSubtreeTransferRequestDigest(request);
44903
+ const expectedKey = deriveTodosTaskSubtreeTransferIdempotencyKey({
44904
+ operation_id: request.operation_id,
44905
+ step_id: request.step_id,
44906
+ direction: "apply",
44907
+ target_selector: request.root_task_id,
44908
+ request_digest: requestDigest,
44909
+ precondition_digest: request.precondition_digest
44910
+ });
44911
+ if (request.idempotency_key !== expectedKey) {
44912
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_MISMATCH", "idempotency_key does not match the exact transfer request", { expected_idempotency_key: expectedKey });
44913
+ }
44914
+ return this.backend.apply(request, this.backendOptions);
44915
+ }
44916
+ async readExact(receiptIdValue) {
44917
+ return this.backend.readExact(uuid3(receiptIdValue, "receipt_id"));
44918
+ }
44919
+ async rollback(input) {
44920
+ const request = parseTodosTaskSubtreeTransferRollback(input);
44921
+ const requestDigest = taskSubtreeTransferRollbackRequestDigest(request);
44922
+ const expectedKey = deriveTodosTaskSubtreeTransferIdempotencyKey({
44923
+ operation_id: request.operation_id,
44924
+ step_id: request.step_id,
44925
+ direction: "rollback",
44926
+ target_selector: request.receipt_id,
44927
+ request_digest: requestDigest,
44928
+ precondition_digest: request.precondition_digest
44929
+ });
44930
+ if (request.idempotency_key !== expectedKey) {
44931
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_MISMATCH", "rollback idempotency_key does not match the exact rollback request", { expected_idempotency_key: expectedKey });
44932
+ }
44933
+ return this.backend.rollback(request, this.backendOptions);
44934
+ }
44935
+ }
44936
+ function createSqliteTodosTaskSubtreeTransferAuthority(options) {
44937
+ return new PackageOwnedTodosTaskSubtreeTransferAuthority(new SqliteTodosTaskSubtreeTransferBackend(options.database, resolveTenantId2(options.tenantId)), options);
44938
+ }
44939
+ function createPostgresTodosTaskSubtreeTransferAuthority(client, options = {}) {
44940
+ return new PackageOwnedTodosTaskSubtreeTransferAuthority(new PostgresTodosTaskSubtreeTransferBackend(client, options), options);
44941
+ }
44942
+ // src/task-subtree-transfer/http.ts
44943
+ var JSON_HEADERS3 = { "Content-Type": "application/json" };
44944
+ var REQUEST_BYTES = 2 * 1024 * 1024;
44945
+ var RESPONSE_BYTES = 8 * 1024 * 1024;
44946
+ function json3(body2, status2 = 200) {
44947
+ return new Response(JSON.stringify(body2), { status: status2, headers: JSON_HEADERS3 });
44948
+ }
44949
+ function status2(error) {
44950
+ switch (error.code) {
44951
+ case "TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT":
44952
+ case "TODOS_TASK_SUBTREE_TRANSFER_FOREIGN_REFERENCE":
44953
+ case "TODOS_TASK_SUBTREE_TRANSFER_HIERARCHY_CYCLE":
44954
+ return 400;
44955
+ case "TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND":
44956
+ case "TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND":
44957
+ return 404;
44958
+ case "TODOS_TASK_SUBTREE_TRANSFER_ATOMICITY_UNAVAILABLE":
44959
+ return 503;
44960
+ default:
44961
+ return 409;
44962
+ }
44963
+ }
44964
+ async function boundedText2(message, limit) {
44965
+ if (!message.body)
44966
+ return "";
44967
+ const reader = message.body.getReader();
44968
+ const decoder = new TextDecoder;
44969
+ let bytes = 0;
44970
+ let text = "";
44971
+ try {
44972
+ while (true) {
44973
+ const { done, value } = await reader.read();
44974
+ if (done)
44975
+ break;
44976
+ bytes += value.byteLength;
44977
+ if (bytes > limit) {
44978
+ await reader.cancel();
44979
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Task-subtree-transfer HTTP body exceeds the byte bound");
44980
+ }
44981
+ text += decoder.decode(value, { stream: true });
44982
+ }
44983
+ return text + decoder.decode();
44984
+ } finally {
44985
+ reader.releaseLock();
44986
+ }
44987
+ }
44988
+ async function readBody(request) {
44989
+ const declaredLength = Number(request.headers.get("content-length") ?? 0);
44990
+ if (Number.isFinite(declaredLength) && declaredLength > REQUEST_BYTES) {
44991
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Task-subtree-transfer request exceeds the byte bound");
44992
+ }
44993
+ const text = await boundedText2(request, REQUEST_BYTES);
44994
+ try {
44995
+ return JSON.parse(text);
44996
+ } catch {
44997
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Invalid JSON body");
44998
+ }
44999
+ }
45000
+ async function handleTodosTaskSubtreeTransferHttpRequest(request, url, authority, basePath = "/v1/task-subtree-transfer") {
45001
+ if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`))
45002
+ return null;
45003
+ const action = url.pathname.slice(basePath.length).split("/").filter(Boolean).join("/");
45004
+ try {
45005
+ if ((action === "" || action === "capability") && request.method === "GET") {
45006
+ return json3({ capability: await authority.capability() });
45007
+ }
45008
+ if (request.method !== "POST")
45009
+ return json3({ error: "method not allowed" }, 405);
45010
+ if (action === "inspect")
45011
+ return json3({ inspection: await authority.inspect(await readBody(request)) });
45012
+ if (action === "apply")
45013
+ return json3({ result: await authority.apply(await readBody(request)) }, 201);
45014
+ if (action === "rollback")
45015
+ return json3({ result: await authority.rollback(await readBody(request)) }, 201);
45016
+ if (action === "read-exact") {
45017
+ const input = await readBody(request);
45018
+ if (!input || typeof input.receipt_id !== "string") {
45019
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "receipt_id is required");
45020
+ }
45021
+ return json3({ result: await authority.readExact(input.receipt_id) });
45022
+ }
45023
+ return json3({ error: "unknown task-subtree-transfer route" }, 404);
45024
+ } catch (cause) {
45025
+ if (cause instanceof TodosTaskSubtreeTransferError) {
45026
+ return json3({
45027
+ error: cause.message,
45028
+ code: cause.code,
45029
+ details: cause.details,
45030
+ authoritative: true
45031
+ }, status2(cause));
45032
+ }
45033
+ return json3({ error: "internal task-subtree-transfer error" }, 500);
45034
+ }
45035
+ }
45036
+
45037
+ class TodosTaskSubtreeTransferHttpClient {
45038
+ baseUrl;
45039
+ fetchImpl;
45040
+ headers;
45041
+ constructor(options) {
45042
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
45043
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
45044
+ this.headers = {
45045
+ ...options.headers,
45046
+ ...options.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {},
45047
+ "Content-Type": "application/json"
45048
+ };
45049
+ }
45050
+ async request(action, init = {}) {
45051
+ const response = await this.fetchImpl(`${this.baseUrl}/v1/task-subtree-transfer${action}`, {
45052
+ ...init,
45053
+ headers: { ...this.headers, ...init.headers ?? {} }
45054
+ });
45055
+ const text = await boundedText2(response, RESPONSE_BYTES);
45056
+ let payload;
45057
+ try {
45058
+ payload = JSON.parse(text);
45059
+ } catch {
45060
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_HTTP_ERROR", `Task-subtree-transfer HTTP ${response.status} returned invalid JSON`);
45061
+ }
45062
+ if (!response.ok) {
45063
+ throw new TodosTaskSubtreeTransferError(typeof payload.code === "string" ? payload.code : "TODOS_TASK_SUBTREE_TRANSFER_HTTP_ERROR", typeof payload.error === "string" ? payload.error : `Task-subtree-transfer HTTP ${response.status}`, payload.details && typeof payload.details === "object" ? payload.details : {});
45064
+ }
45065
+ return payload;
45066
+ }
45067
+ async capability() {
45068
+ return (await this.request("/capability")).capability;
45069
+ }
45070
+ async inspect(input) {
45071
+ return (await this.request("/inspect", { method: "POST", body: JSON.stringify(input) })).inspection;
45072
+ }
45073
+ async apply(input) {
45074
+ return (await this.request("/apply", { method: "POST", body: JSON.stringify(input) })).result;
45075
+ }
45076
+ async readExact(receiptId3) {
45077
+ return (await this.request("/read-exact", { method: "POST", body: JSON.stringify({ receipt_id: receiptId3 }) })).result;
45078
+ }
45079
+ async rollback(input) {
45080
+ return (await this.request("/rollback", { method: "POST", body: JSON.stringify(input) })).result;
45081
+ }
45082
+ }
45083
+ function createTodosTaskSubtreeTransferHttpClient(options) {
45084
+ return new TodosTaskSubtreeTransferHttpClient(options);
45085
+ }
43627
45086
  // src/ai-tools.ts
43628
45087
  import { createHash as createHash16 } from "crypto";
43629
45088
  // src/cli/cloud-router.ts
@@ -43635,26 +45094,26 @@ var retryCapabilityCache = new Map;
43635
45094
  var taskCreatorCapabilityCache = new Map;
43636
45095
  var gitRefCapabilityCache = new Map;
43637
45096
  var remoteCommandCapabilityCache = new Map;
43638
- var LEGACY_STORAGE_MODE_KEYS = [
43639
- "HASNA_TODOS_STORAGE_MODE",
43640
- "HASNA_TODOS_MODE",
43641
- "TODOS_STORAGE_MODE",
43642
- "TODOS_MODE"
43643
- ];
43644
- function assertNoLegacyStorageMode(env = process.env) {
43645
- for (const key2 of LEGACY_STORAGE_MODE_KEYS) {
43646
- if (Object.hasOwn(env, key2) && env[key2] !== undefined) {
43647
- throw new Error(`REMOTE_STORAGE_MODE_REMOVED: ${key2} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `The client uses the on-box SQLite store, or the HTTP API selected by ` + `HASNA_TODOS_API_URL + HASNA_TODOS_API_KEY. ` + `On the server, set HASNA_TODOS_DATABASE_URL to select the postgresql backend, ` + `or leave it unset for sqlite.`);
43648
- }
43649
- }
45097
+ var todosLocalFallbackNoticeEmitted = false;
45098
+ function emitTodosLocalFallbackNotice(env) {
45099
+ if (todosLocalFallbackNoticeEmitted)
45100
+ return;
45101
+ todosLocalFallbackNoticeEmitted = true;
45102
+ const notice = {
45103
+ event: "todos-local-fallback",
45104
+ transport: "sqlite",
45105
+ source: "default",
45106
+ apiUrlPresent: Boolean(env.HASNA_TODOS_API_URL?.trim()),
45107
+ apiKeyPresent: Boolean(env.HASNA_TODOS_API_KEY?.trim()),
45108
+ notice: "No hosted API config (HASNA_TODOS_API_URL + HASNA_TODOS_API_KEY) is present; " + "using local SQLite. Hosted todos are NOT visible in this output."
45109
+ };
45110
+ console.error(JSON.stringify(notice));
43650
45111
  }
43651
- function resolveTodosCliStorageMode(env = process.env) {
43652
- assertNoLegacyStorageMode(env);
45112
+ function resolveTodosCliTransport(env = process.env) {
43653
45113
  const urlValue = env.HASNA_TODOS_API_URL?.trim();
43654
45114
  const keyValue = env.HASNA_TODOS_API_KEY?.trim();
43655
45115
  if (urlValue && keyValue) {
43656
45116
  return {
43657
- mode: "http",
43658
45117
  transport: "http",
43659
45118
  selected: true,
43660
45119
  source: "HASNA_TODOS_API_URL+HASNA_TODOS_API_KEY"
@@ -43666,15 +45125,15 @@ function resolveTodosCliStorageMode(env = process.env) {
43666
45125
  if (keyValue) {
43667
45126
  throw new Error("REMOTE_API_URL_MISSING: remote Todos storage requires HASNA_TODOS_API_URL; local SQLite fallback is disabled");
43668
45127
  }
45128
+ emitTodosLocalFallbackNotice(env);
43669
45129
  return {
43670
- mode: "sqlite",
43671
45130
  transport: "sqlite",
43672
45131
  selected: false,
43673
45132
  source: "default"
43674
45133
  };
43675
45134
  }
43676
45135
  function requestedTransport(env) {
43677
- return resolveTodosCliStorageMode(env).transport;
45136
+ return resolveTodosCliTransport(env).transport;
43678
45137
  }
43679
45138
  function normalizeRemoteAuthorityUrl(value) {
43680
45139
  const trimmed = value?.trim();
@@ -43708,13 +45167,13 @@ function normalizeRemoteAuthorityUrl(value) {
43708
45167
  function getTodosRemoteAuthorityConfigStatus(env = process.env) {
43709
45168
  let resolution;
43710
45169
  try {
43711
- resolution = resolveTodosCliStorageMode(env);
45170
+ resolution = resolveTodosCliTransport(env);
43712
45171
  } catch (error) {
43713
45172
  const issue = error instanceof Error ? error.message : String(error);
43714
45173
  return {
43715
45174
  selected: true,
43716
45175
  ok: false,
43717
- mode: "invalid",
45176
+ transport: "invalid",
43718
45177
  api_url_configured: Boolean(env.HASNA_TODOS_API_URL?.trim()),
43719
45178
  api_key_configured: Boolean(env.HASNA_TODOS_API_KEY?.trim()),
43720
45179
  v1_base_url: null,
@@ -43722,12 +45181,12 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
43722
45181
  local_fallback: false
43723
45182
  };
43724
45183
  }
43725
- const { mode, selected } = resolution;
45184
+ const { transport, selected } = resolution;
43726
45185
  if (!selected) {
43727
45186
  return {
43728
45187
  selected: false,
43729
45188
  ok: true,
43730
- mode,
45189
+ transport,
43731
45190
  api_url_configured: false,
43732
45191
  api_key_configured: false,
43733
45192
  v1_base_url: null,
@@ -43752,7 +45211,7 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
43752
45211
  return {
43753
45212
  selected: true,
43754
45213
  ok: issues.length === 0,
43755
- mode,
45214
+ transport,
43756
45215
  api_url_configured: apiUrl !== null,
43757
45216
  api_key_configured: apiKeyConfigured,
43758
45217
  v1_base_url: apiUrl ? `${apiUrl}/v1` : null,
@@ -43761,24 +45220,24 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
43761
45220
  };
43762
45221
  }
43763
45222
  function requireTodosRemoteAuthorityEnv(env) {
43764
- const status2 = getTodosRemoteAuthorityConfigStatus(env);
43765
- if (!status2.ok)
43766
- throw new Error(status2.issues[0]);
45223
+ const status3 = getTodosRemoteAuthorityConfigStatus(env);
45224
+ if (!status3.ok)
45225
+ throw new Error(status3.issues[0]);
43767
45226
  return {
43768
45227
  ...env,
43769
- HASNA_TODOS_API_URL: status2.v1_base_url.replace(/\/v1$/, ""),
45228
+ HASNA_TODOS_API_URL: status3.v1_base_url.replace(/\/v1$/, ""),
43770
45229
  HASNA_TODOS_API_KEY: env.HASNA_TODOS_API_KEY.trim()
43771
45230
  };
43772
45231
  }
43773
45232
  function classifyRemoteRequestError(baseUrl, route, error) {
43774
- const status2 = error && typeof error === "object" ? error.status : undefined;
43775
- if (status2 === 401) {
45233
+ const status3 = error && typeof error === "object" ? error.status : undefined;
45234
+ if (status3 === 401) {
43776
45235
  throw new Error(`REMOTE_API_UNAUTHORIZED: configured Todos authority ${baseUrl} rejected HASNA_TODOS_API_KEY for ${route}; ` + "local SQLite fallback is disabled", { cause: error });
43777
45236
  }
43778
- if (status2 === 403) {
45237
+ if (status3 === 403) {
43779
45238
  throw new Error(`REMOTE_API_FORBIDDEN: configured Todos authority ${baseUrl} denied ${route}; local SQLite fallback is disabled`, { cause: error });
43780
45239
  }
43781
- if (status2 === 409) {
45240
+ if (status3 === 409) {
43782
45241
  const body2 = error && typeof error === "object" ? error.body : undefined;
43783
45242
  if (body2 && typeof body2 === "object" && !Array.isArray(body2)) {
43784
45243
  const remoteError = body2;
@@ -43788,17 +45247,18 @@ function classifyRemoteRequestError(baseUrl, route, error) {
43788
45247
  }
43789
45248
  throw error;
43790
45249
  }
43791
- if (typeof status2 === "number" && status2 >= 300 && status2 < 400) {
45250
+ if (typeof status3 === "number" && status3 >= 300 && status3 < 400) {
43792
45251
  throw new Error(`REMOTE_API_REDIRECT_REJECTED: configured Todos authority ${baseUrl} redirected ${route}; ` + "authenticated redirects are disabled to prevent credential leakage", { cause: error });
43793
45252
  }
43794
- if (typeof status2 === "number" && status2 >= 500) {
43795
- throw new Error(`REMOTE_API_UNAVAILABLE: configured Todos authority ${baseUrl} returned HTTP ${status2} for ${route}; ` + "local SQLite fallback is disabled", { cause: error });
45253
+ if (typeof status3 === "number" && status3 >= 500) {
45254
+ throw new Error(`REMOTE_API_UNAVAILABLE: configured Todos authority ${baseUrl} returned HTTP ${status3} for ${route}; ` + "local SQLite fallback is disabled", { cause: error });
43796
45255
  }
43797
45256
  const message = error instanceof Error ? error.message : String(error);
43798
45257
  if (error instanceof Error && error.name === "AbortError" || /abort|timed?\s*out/i.test(message)) {
43799
- throw new Error(`REMOTE_API_TIMEOUT: configured Todos authority ${baseUrl} timed out for ${route}; local SQLite fallback is disabled`, { cause: error });
45258
+ const unboundedNote = route === "/tasks" ? "; this is likely an UNBOUNDED task read timing out (bounded task reads from this authority return in ~1 s) \u2014 retry with a bounded/count-specific shape such as todos list --limit; local SQLite fallback is disabled" : "; local SQLite fallback is disabled";
45259
+ throw new Error(`REMOTE_API_TIMEOUT: configured Todos authority ${baseUrl} timed out for ${route}${unboundedNote}`, { cause: error });
43800
45260
  }
43801
- if (status2 === undefined) {
45261
+ if (status3 === undefined) {
43802
45262
  throw new Error(`REMOTE_API_UNREACHABLE: configured Todos authority ${baseUrl} could not be reached for ${route}; ` + "local SQLite fallback is disabled", { cause: error });
43803
45263
  }
43804
45264
  throw error;
@@ -43866,8 +45326,8 @@ async function requiredRemoteRoute(client, route, request, recognized404Codes =
43866
45326
  try {
43867
45327
  return await request();
43868
45328
  } catch (error) {
43869
- const status2 = error && typeof error === "object" ? error.status : undefined;
43870
- if (status2 === 404 || status2 === 405) {
45329
+ const status3 = error && typeof error === "object" ? error.status : undefined;
45330
+ if (status3 === 404 || status3 === 405) {
43871
45331
  const body2 = error && typeof error === "object" ? error.body : undefined;
43872
45332
  const code = body2 && typeof body2 === "object" && !Array.isArray(body2) ? body2.code : undefined;
43873
45333
  if (typeof code === "string" && recognized404Codes.includes(code))
@@ -43884,11 +45344,10 @@ function getTodosCloudClient(env = process.env, requestTimeoutMs = REMOTE_REQUES
43884
45344
  fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" }),
43885
45345
  timeoutMs: requestTimeoutMs
43886
45346
  });
43887
- if (resolved.transport === "cloud-http")
43888
- return protectRemoteClient(resolved.client, requestTimeoutMs);
43889
45347
  const transportName = resolved.transport;
43890
- if (transportName === "http")
45348
+ if (transportName === "cloud-http" || transportName === "http") {
43891
45349
  return protectRemoteClient(resolved.client, requestTimeoutMs);
45350
+ }
43892
45351
  return null;
43893
45352
  }
43894
45353
  function unwrapTask(raw) {
@@ -44039,9 +45498,9 @@ async function cloudListTasks(client, filter = {}) {
44039
45498
  const { status: _status, limit, offset, ...baseFilter } = filter;
44040
45499
  const start = offset ?? 0;
44041
45500
  const windowEnd = typeof limit === "number" ? start + limit : undefined;
44042
- const pages = await Promise.all(statuses.map((status2) => requestCloudTaskPage(client, {
45501
+ const pages = await Promise.all(statuses.map((status3) => requestCloudTaskPage(client, {
44043
45502
  ...baseFilter,
44044
- status: status2,
45503
+ status: status3,
44045
45504
  ...windowEnd === undefined ? {} : { limit: windowEnd }
44046
45505
  })));
44047
45506
  const seen = new Set;
@@ -44319,11 +45778,11 @@ init_audit();
44319
45778
  init_database();
44320
45779
  function upsertCheckpoint(task_id, step, updates, db) {
44321
45780
  const d = db || getDatabase();
44322
- const timestamp4 = now();
45781
+ const timestamp5 = now();
44323
45782
  const existing = d.query("SELECT id FROM task_checkpoints WHERE task_id = ? AND step = ?").get(task_id, step);
44324
45783
  const id = existing?.id ?? uuid();
44325
45784
  const agentId = updates.agent_id ?? null;
44326
- const status2 = updates.status ?? "pending";
45785
+ const status3 = updates.status ?? "pending";
44327
45786
  const data = updates.data ? JSON.stringify(updates.data) : JSON.stringify({});
44328
45787
  const error = updates.error ?? null;
44329
45788
  const attempt = updates.attempt ?? 1;
@@ -44337,22 +45796,22 @@ function upsertCheckpoint(task_id, step, updates, db) {
44337
45796
  task_id,
44338
45797
  agentId,
44339
45798
  step,
44340
- status2,
45799
+ status3,
44341
45800
  data,
44342
45801
  error,
44343
45802
  attempt,
44344
45803
  maxAttempts,
44345
45804
  startedAt,
44346
45805
  completedAt2,
44347
- timestamp4,
44348
- status2,
45806
+ timestamp5,
45807
+ status3,
44349
45808
  data,
44350
45809
  error,
44351
45810
  attempt,
44352
45811
  maxAttempts,
44353
45812
  startedAt,
44354
45813
  completedAt2,
44355
- timestamp4
45814
+ timestamp5
44356
45815
  ]);
44357
45816
  return rowToCheckpoint(d.query("SELECT * FROM task_checkpoints WHERE id = ?").get(id));
44358
45817
  }
@@ -44415,10 +45874,10 @@ function stepForGate(gate) {
44415
45874
  throw new Error("Approval gate name is required");
44416
45875
  return `approval:${trimmed}`;
44417
45876
  }
44418
- function approvalStatusToCheckpointStatus(status2) {
44419
- if (status2 === "approved")
45877
+ function approvalStatusToCheckpointStatus(status3) {
45878
+ if (status3 === "approved")
44420
45879
  return "completed";
44421
- if (status2 === "pending")
45880
+ if (status3 === "pending")
44422
45881
  return "pending";
44423
45882
  return "failed";
44424
45883
  }
@@ -44440,12 +45899,12 @@ function isExpired(expiresAt, at = new Date) {
44440
45899
  return Boolean(expiresAt && new Date(expiresAt).getTime() <= at.getTime());
44441
45900
  }
44442
45901
  function gateFromCheckpoint(checkpoint) {
44443
- const status2 = checkpointStatusToApprovalStatus(checkpoint);
45902
+ const status3 = checkpointStatusToApprovalStatus(checkpoint);
44444
45903
  return {
44445
45904
  id: checkpoint.id,
44446
45905
  task_id: checkpoint.task_id,
44447
45906
  gate: String(checkpoint.data["approval_gate_name"] || checkpoint.step.replace(/^approval:/, "")),
44448
- status: status2,
45907
+ status: status3,
44449
45908
  reviewer: typeof checkpoint.data["reviewer"] === "string" ? checkpoint.data["reviewer"] : null,
44450
45909
  requester: typeof checkpoint.data["requester"] === "string" ? checkpoint.data["requester"] : null,
44451
45910
  reason: typeof checkpoint.data["reason"] === "string" ? checkpoint.data["reason"] : null,
@@ -44490,13 +45949,13 @@ function logApprovalEvent(taskId, action, gate, agentId, db) {
44490
45949
  });
44491
45950
  }
44492
45951
  }
44493
- function writeGate(input, status2, decision, db) {
45952
+ function writeGate(input, status3, decision, db) {
44494
45953
  const d = db || getDatabase();
44495
45954
  ensureTask(input.task_id, d);
44496
45955
  const step = stepForGate(input.gate);
44497
45956
  const existing = getCheckpoint(input.task_id, step, d);
44498
45957
  const existingData = existing?.data || {};
44499
- const timestamp4 = decision?.decided_at || now();
45958
+ const timestamp5 = decision?.decided_at || now();
44500
45959
  const existingRunId = typeof existingData["run_id"] === "string" ? existingData["run_id"] : undefined;
44501
45960
  const runId = input.run_id ? resolveTaskRunId(input.run_id, d) : existingRunId;
44502
45961
  const data = {
@@ -44504,7 +45963,7 @@ function writeGate(input, status2, decision, db) {
44504
45963
  ...input.metadata || {},
44505
45964
  approval_gate: true,
44506
45965
  approval_gate_name: input.gate.trim(),
44507
- approval_status: status2,
45966
+ approval_status: status3,
44508
45967
  requester: input.requester ?? existingData["requester"] ?? null,
44509
45968
  reviewer: input.reviewer ?? decision?.decided_by ?? existingData["reviewer"] ?? null,
44510
45969
  reason: decision?.reason ?? input.reason ?? existingData["reason"] ?? null,
@@ -44517,11 +45976,11 @@ function writeGate(input, status2, decision, db) {
44517
45976
  };
44518
45977
  const checkpoint = upsertCheckpoint(input.task_id, step, {
44519
45978
  agent_id: decision?.decided_by || input.requester || input.reviewer,
44520
- status: approvalStatusToCheckpointStatus(status2),
45979
+ status: approvalStatusToCheckpointStatus(status3),
44521
45980
  data,
44522
- error: status2 === "rejected" || status2 === "expired" ? String(data.reason || status2) : null,
44523
- started_at: existing?.started_at || timestamp4,
44524
- completed_at: status2 === "pending" ? null : timestamp4
45981
+ error: status3 === "rejected" || status3 === "expired" ? String(data.reason || status3) : null,
45982
+ started_at: existing?.started_at || timestamp5,
45983
+ completed_at: status3 === "pending" ? null : timestamp5
44525
45984
  }, d);
44526
45985
  return gateFromCheckpoint(checkpoint);
44527
45986
  }
@@ -45369,7 +46828,7 @@ async function executeListTasks(adapter, state, request, input) {
45369
46828
  const planId = boundedOptionalString(record, "plan_id", TODOS_AI_READ_TOOL_LIMITS.max_identifier_bytes);
45370
46829
  const taskListId = boundedOptionalString(record, "task_list_id", TODOS_AI_READ_TOOL_LIMITS.max_identifier_bytes);
45371
46830
  const assignedTo = boundedOptionalString(record, "assigned_to", TODOS_AI_READ_TOOL_LIMITS.max_filter_string_bytes);
45372
- const status2 = boundedEnum(record, "status", TASK_STATUSES);
46831
+ const status3 = boundedEnum(record, "status", TASK_STATUSES);
45373
46832
  const priority = boundedEnum(record, "priority", TASK_PRIORITIES);
45374
46833
  const tags = boundedTags(record);
45375
46834
  if (projectId !== undefined)
@@ -45380,8 +46839,8 @@ async function executeListTasks(adapter, state, request, input) {
45380
46839
  filter.task_list_id = taskListId;
45381
46840
  if (assignedTo !== undefined)
45382
46841
  filter.assigned_to = assignedTo;
45383
- if (status2 !== undefined)
45384
- filter.status = status2;
46842
+ if (status3 !== undefined)
46843
+ filter.status = status3;
45385
46844
  if (priority !== undefined)
45386
46845
  filter.priority = priority;
45387
46846
  if (tags !== undefined)
@@ -45723,29 +47182,29 @@ function getNativeStorageStatus(env = process.env) {
45723
47182
  };
45724
47183
  }
45725
47184
  function getNativeStorageSyncPlan(env = process.env, options = {}) {
45726
- const status2 = getNativeStorageStatus(env);
47185
+ const status3 = getNativeStorageStatus(env);
45727
47186
  const steps = [
45728
47187
  "Read local SQLite snapshot",
45729
- status2.remote_enabled ? "Prepare Postgres sync table upserts" : "Skip remote database writes in local mode",
45730
- status2.object_storage.configured ? "Plan S3 artifact object writes" : "Keep artifact storage local",
47188
+ status3.remote_enabled ? "Prepare Postgres sync table upserts" : "Skip remote database writes in local mode",
47189
+ status3.object_storage.configured ? "Plan S3 artifact object writes" : "Keep artifact storage local",
45731
47190
  "Report planned changes without opening network connections"
45732
47191
  ];
45733
47192
  return {
45734
- ok: status2.ok,
47193
+ ok: status3.ok,
45735
47194
  service: "todos",
45736
47195
  dry_run: true,
45737
47196
  no_network: true,
45738
- status: status2,
47197
+ status: status3,
45739
47198
  postgres: {
45740
- required: status2.remote_enabled,
45741
- configured: status2.database.configured,
47199
+ required: status3.remote_enabled,
47200
+ configured: status3.database.configured,
45742
47201
  schema_sql: options.includeSchemaSql ? postgresTodosSyncSchemaSql() : []
45743
47202
  },
45744
47203
  object_storage: {
45745
- required: status2.remote_enabled,
45746
- configured: status2.object_storage.configured,
45747
- bucket: status2.object_storage.bucket,
45748
- prefix: status2.object_storage.prefix
47204
+ required: status3.remote_enabled,
47205
+ configured: status3.object_storage.configured,
47206
+ bucket: status3.object_storage.bucket,
47207
+ prefix: status3.object_storage.prefix
45749
47208
  },
45750
47209
  steps
45751
47210
  };
@@ -45851,8 +47310,8 @@ function resolveTaskList(task3, project, db) {
45851
47310
  }
45852
47311
  return null;
45853
47312
  }
45854
- function isTerminal2(status2) {
45855
- return status2 === "completed" || status2 === "cancelled" || status2 === "failed";
47313
+ function isTerminal2(status3) {
47314
+ return status3 === "completed" || status3 === "cancelled" || status3 === "failed";
45856
47315
  }
45857
47316
  function routeConcurrencyKey(task3, project, taskList, projectPath) {
45858
47317
  if (project?.id)
@@ -46033,7 +47492,7 @@ function setTaskWorkflowPointers(taskId, input, db) {
46033
47492
  latest_evaluation_path: pointerPatch(previous.latest_evaluation_path, input, "latest_evaluation_path"),
46034
47493
  workflow_state: pointerPatch(previous.workflow_state, input, "workflow_state")
46035
47494
  });
46036
- const timestamp4 = now();
47495
+ const timestamp5 = now();
46037
47496
  const {
46038
47497
  current_workflow_invocation_id,
46039
47498
  current_run_id,
@@ -46049,7 +47508,7 @@ function setTaskWorkflowPointers(taskId, input, db) {
46049
47508
  workflow_invocation: {
46050
47509
  schema_version: TASK_WORKFLOW_POINTER_SCHEMA_VERSION,
46051
47510
  ...next,
46052
- updated_at: timestamp4,
47511
+ updated_at: timestamp5,
46053
47512
  updated_by: input.actor ?? null
46054
47513
  }
46055
47514
  };
@@ -46083,8 +47542,8 @@ function normalizePath3(input) {
46083
47542
  return resolve10(input);
46084
47543
  }
46085
47544
  function sourceStoreId(sourceDbPath) {
46086
- const digest2 = createHash17("sha256").update(sourceDbPath).digest("hex").slice(0, 16);
46087
- return `sqlite:${digest2}`;
47545
+ const digest3 = createHash17("sha256").update(sourceDbPath).digest("hex").slice(0, 16);
47546
+ return `sqlite:${digest3}`;
46088
47547
  }
46089
47548
  function inferSourceRepoPath(sourceDbPath) {
46090
47549
  const normalized = normalizePath3(sourceDbPath);
@@ -46855,7 +48314,7 @@ function rowToSnapshot(row) {
46855
48314
  function saveSnapshot(input, db) {
46856
48315
  const d = db || getDatabase();
46857
48316
  const id = uuid();
46858
- const timestamp4 = now();
48317
+ const timestamp5 = now();
46859
48318
  d.run(`INSERT INTO context_snapshots (id, agent_id, task_id, project_id, snapshot_type, plan_summary, files_open, attempts, blockers, next_steps, metadata, created_at)
46860
48319
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
46861
48320
  id,
@@ -46869,7 +48328,7 @@ function saveSnapshot(input, db) {
46869
48328
  JSON.stringify(input.blockers || []),
46870
48329
  input.next_steps || null,
46871
48330
  JSON.stringify(input.metadata || {}),
46872
- timestamp4
48331
+ timestamp5
46873
48332
  ]);
46874
48333
  return {
46875
48334
  id,
@@ -46883,7 +48342,7 @@ function saveSnapshot(input, db) {
46883
48342
  blockers: input.blockers || [],
46884
48343
  next_steps: input.next_steps || null,
46885
48344
  metadata: input.metadata || {},
46886
- created_at: timestamp4
48345
+ created_at: timestamp5
46887
48346
  };
46888
48347
  }
46889
48348
  function getLatestSnapshot(agentId, taskId, db) {
@@ -46999,7 +48458,7 @@ function createKnowledgeRecord(input, db) {
46999
48458
  if (!title)
47000
48459
  throw new Error("Knowledge record title is required");
47001
48460
  const d = db || getDatabase();
47002
- const timestamp4 = now();
48461
+ const timestamp5 = now();
47003
48462
  const id = uuid();
47004
48463
  d.run(`INSERT INTO project_knowledge_records (
47005
48464
  id, record_type, title, content, decision, rationale, alternatives,
@@ -47019,8 +48478,8 @@ function createKnowledgeRecord(input, db) {
47019
48478
  input.snapshot_id || null,
47020
48479
  JSON.stringify(normalizeTags(input.tags)),
47021
48480
  JSON.stringify(input.metadata || {}),
47022
- timestamp4,
47023
- timestamp4
48481
+ timestamp5,
48482
+ timestamp5
47024
48483
  ]);
47025
48484
  return getKnowledgeRecord(id, d);
47026
48485
  }
@@ -47211,9 +48670,9 @@ function normalizeText4(value) {
47211
48670
  const trimmed = value.trim();
47212
48671
  return trimmed.length > 0 ? trimmed : null;
47213
48672
  }
47214
- function assertStatus(status2) {
47215
- if (!VALID_STATUSES.has(status2))
47216
- throw new Error(`Invalid risk status: ${status2}`);
48673
+ function assertStatus(status3) {
48674
+ if (!VALID_STATUSES.has(status3))
48675
+ throw new Error(`Invalid risk status: ${status3}`);
47217
48676
  }
47218
48677
  function assertSeverity(severity) {
47219
48678
  if (!VALID_SEVERITIES2.has(severity))
@@ -47258,16 +48717,16 @@ function createRisk(input, db) {
47258
48717
  const title = input.title.trim();
47259
48718
  if (!title)
47260
48719
  throw new Error("Risk title is required");
47261
- const status2 = input.status || "open";
48720
+ const status3 = input.status || "open";
47262
48721
  const severity = input.severity || "medium";
47263
48722
  const probability = input.probability || "medium";
47264
- assertStatus(status2);
48723
+ assertStatus(status3);
47265
48724
  assertSeverity(severity);
47266
48725
  assertProbability(probability);
47267
48726
  const d = db || getDatabase();
47268
- const timestamp4 = now();
48727
+ const timestamp5 = now();
47269
48728
  const id = uuid();
47270
- const closedAt = status2 === "resolved" || status2 === "accepted" ? timestamp4 : null;
48729
+ const closedAt = status3 === "resolved" || status3 === "accepted" ? timestamp5 : null;
47271
48730
  d.run(`INSERT INTO project_risks (
47272
48731
  id, title, description, status, severity, probability, owner, mitigation, due_at,
47273
48732
  project_id, plan_id, task_id, tags, metadata, created_at, updated_at, closed_at
@@ -47275,7 +48734,7 @@ function createRisk(input, db) {
47275
48734
  id,
47276
48735
  title,
47277
48736
  normalizeText4(input.description),
47278
- status2,
48737
+ status3,
47279
48738
  severity,
47280
48739
  probability,
47281
48740
  normalizeText4(input.owner),
@@ -47286,8 +48745,8 @@ function createRisk(input, db) {
47286
48745
  resolveKnownId2("tasks", input.task_id, d),
47287
48746
  JSON.stringify(normalizeTags2(input.tags)),
47288
48747
  JSON.stringify(input.metadata || {}),
47289
- timestamp4,
47290
- timestamp4,
48748
+ timestamp5,
48749
+ timestamp5,
47291
48750
  closedAt
47292
48751
  ]);
47293
48752
  return getRisk(id, d);
@@ -47370,8 +48829,8 @@ function updateRisk(id, input, db) {
47370
48829
  d.run(`UPDATE project_risks SET ${sets.join(", ")} WHERE id = ?`, params);
47371
48830
  return getRisk(resolved, d);
47372
48831
  }
47373
- function closeRisk(id, status2 = "resolved", db) {
47374
- return updateRisk(id, { status: status2 }, db);
48832
+ function closeRisk(id, status3 = "resolved", db) {
48833
+ return updateRisk(id, { status: status3 }, db);
47375
48834
  }
47376
48835
  function buildRiskFilters(options, db) {
47377
48836
  const conditions = [];
@@ -47499,7 +48958,7 @@ function scoreHealth(scope, scopeId, db) {
47499
48958
  };
47500
48959
  const totalPenalty = Object.values(penalties).reduce((sum, penalty) => sum + penalty, 0);
47501
48960
  const score = Math.max(0, 100 - Math.min(100, totalPenalty));
47502
- const status2 = score < 40 ? "critical" : score < 70 ? "at_risk" : score < 85 ? "watch" : "healthy";
48961
+ const status3 = score < 40 ? "critical" : score < 70 ? "at_risk" : score < 85 ? "watch" : "healthy";
47503
48962
  const recommendations = [];
47504
48963
  if (blocked.length > 0)
47505
48964
  recommendations.push("Clear blocking dependencies before starting more work.");
@@ -47523,7 +48982,7 @@ function scoreHealth(scope, scopeId, db) {
47523
48982
  scope,
47524
48983
  scope_id: scopeInfo.id,
47525
48984
  score,
47526
- status: status2,
48985
+ status: status3,
47527
48986
  components: {
47528
48987
  total_tasks: tasks.length,
47529
48988
  blocked_tasks: blocked.length,
@@ -47770,7 +49229,7 @@ function createRetrospective(input, db) {
47770
49229
  const d = db || getDatabase();
47771
49230
  const report = buildReport(input, d);
47772
49231
  const scope = scopeFromInput(input, d);
47773
- const timestamp4 = now();
49232
+ const timestamp5 = now();
47774
49233
  if (input.create_followups) {
47775
49234
  for (const followUp of report.follow_up_tasks) {
47776
49235
  const task3 = createTask({
@@ -47786,7 +49245,7 @@ function createRetrospective(input, db) {
47786
49245
  }
47787
49246
  const id = uuid();
47788
49247
  d.run(`INSERT INTO local_retrospectives (id, title, scope, project_id, plan_id, agent_id, report_json, created_at, updated_at)
47789
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, report.title, report.scope, scope.projectId, scope.planId, input.agent_id || null, JSON.stringify(report), timestamp4, timestamp4]);
49248
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, report.title, report.scope, scope.projectId, scope.planId, input.agent_id || null, JSON.stringify(report), timestamp5, timestamp5]);
47790
49249
  return getRetrospective(id, d);
47791
49250
  }
47792
49251
  function getRetrospective(id, db) {
@@ -48075,7 +49534,7 @@ function createApiKey(input, db) {
48075
49534
  if (!name)
48076
49535
  throw new Error("API key name is required");
48077
49536
  const key2 = generatePlaintextKey();
48078
- const timestamp4 = now();
49537
+ const timestamp5 = now();
48079
49538
  const id = uuid();
48080
49539
  const prefix = key2.slice(0, 12);
48081
49540
  d.run(`INSERT INTO api_keys (id, name, key_hash, prefix, permissions, created_at, expires_at)
@@ -48085,7 +49544,7 @@ function createApiKey(input, db) {
48085
49544
  hashApiKey(key2),
48086
49545
  prefix,
48087
49546
  JSON.stringify(input.permissions?.length ? input.permissions : ["*"]),
48088
- timestamp4,
49547
+ timestamp5,
48089
49548
  input.expires_at || null
48090
49549
  ]);
48091
49550
  const row = d.query("SELECT * FROM api_keys WHERE id = ?").get(id);
@@ -48139,15 +49598,15 @@ function rowToSession(row) {
48139
49598
  function createSession(input, db) {
48140
49599
  const d = db || getDatabase();
48141
49600
  const id = uuid();
48142
- const timestamp4 = now();
49601
+ const timestamp5 = now();
48143
49602
  d.run(`INSERT INTO sessions (id, agent_id, project_id, working_dir, started_at, last_activity, metadata)
48144
49603
  VALUES (?, ?, ?, ?, ?, ?, ?)`, [
48145
49604
  id,
48146
49605
  input.agent_id || null,
48147
49606
  input.project_id || null,
48148
49607
  input.working_dir || null,
48149
- timestamp4,
48150
- timestamp4,
49608
+ timestamp5,
49609
+ timestamp5,
48151
49610
  JSON.stringify(input.metadata || {})
48152
49611
  ]);
48153
49612
  return getSession(id, d);
@@ -48320,7 +49779,7 @@ function rowToArtifact2(row) {
48320
49779
  function addArtifact(input, db, dbPath) {
48321
49780
  const d = db || getDatabase();
48322
49781
  const id = uuid();
48323
- const timestamp4 = now();
49782
+ const timestamp5 = now();
48324
49783
  const storageMode = input.storage_mode ?? "copy";
48325
49784
  const stored = storeArtifactFile({
48326
49785
  artifactId: id,
@@ -48345,8 +49804,8 @@ function addArtifact(input, db, dbPath) {
48345
49804
  stored.sizeBytes,
48346
49805
  input.redaction_status ?? "none",
48347
49806
  JSON.stringify(input.metadata ?? {}),
48348
- timestamp4,
48349
- timestamp4
49807
+ timestamp5,
49808
+ timestamp5
48350
49809
  ]);
48351
49810
  return getArtifact(id, d);
48352
49811
  }
@@ -48379,14 +49838,14 @@ function listArtifacts(filter = {}, db) {
48379
49838
  }
48380
49839
  function updateArtifactRedaction(id, redactionStatus, db) {
48381
49840
  const d = db || getDatabase();
48382
- const timestamp4 = now();
48383
- d.run("UPDATE artifacts SET redaction_status = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL", [redactionStatus, timestamp4, id]);
49841
+ const timestamp5 = now();
49842
+ d.run("UPDATE artifacts SET redaction_status = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL", [redactionStatus, timestamp5, id]);
48384
49843
  return getArtifact(id, d);
48385
49844
  }
48386
49845
  function softDeleteArtifact(id, db) {
48387
49846
  const d = db || getDatabase();
48388
- const timestamp4 = now();
48389
- const result = d.run("UPDATE artifacts SET deleted_at = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL", [timestamp4, timestamp4, id]);
49847
+ const timestamp5 = now();
49848
+ const result = d.run("UPDATE artifacts SET deleted_at = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL", [timestamp5, timestamp5, id]);
48390
49849
  return result.changes > 0;
48391
49850
  }
48392
49851
  function purgeArtifact(id, db, dbPath) {
@@ -48524,7 +49983,7 @@ init_redaction();
48524
49983
  function createHandoff(input, db) {
48525
49984
  const d = db || getDatabase();
48526
49985
  const id = uuid();
48527
- const timestamp4 = now();
49986
+ const timestamp5 = now();
48528
49987
  d.run(`INSERT INTO handoffs (id, agent_id, project_id, session_id, summary, completed, in_progress, blockers, next_steps, task_ids, relevant_files, run_ids, created_at)
48529
49988
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
48530
49989
  id,
@@ -48539,7 +49998,7 @@ function createHandoff(input, db) {
48539
49998
  toJson(input.task_ids),
48540
49999
  toJson(input.relevant_files),
48541
50000
  toJson(input.run_ids),
48542
- timestamp4
50001
+ timestamp5
48543
50002
  ]);
48544
50003
  return {
48545
50004
  id,
@@ -48554,7 +50013,7 @@ function createHandoff(input, db) {
48554
50013
  task_ids: input.task_ids || null,
48555
50014
  relevant_files: input.relevant_files || null,
48556
50015
  run_ids: input.run_ids || null,
48557
- created_at: timestamp4,
50016
+ created_at: timestamp5,
48558
50017
  acknowledged_by: []
48559
50018
  };
48560
50019
  }
@@ -49066,7 +50525,7 @@ function upsertVerificationProvider(input) {
49066
50525
  const name = normalizeName(input.name);
49067
50526
  const config = loadConfig();
49068
50527
  const existing = config.verification_providers?.[name];
49069
- const timestamp4 = new Date().toISOString();
50528
+ const timestamp5 = new Date().toISOString();
49070
50529
  const provider = {
49071
50530
  ...existing,
49072
50531
  name,
@@ -49077,8 +50536,8 @@ function upsertVerificationProvider(input) {
49077
50536
  capabilities: input.capabilities ?? existing?.capabilities,
49078
50537
  retry: input.retry ? retryConfig(input.retry) : existing?.retry,
49079
50538
  timeout_ms: timeoutMs(input.timeout_ms ?? existing?.timeout_ms),
49080
- created_at: existing?.created_at || timestamp4,
49081
- updated_at: timestamp4
50539
+ created_at: existing?.created_at || timestamp5,
50540
+ updated_at: timestamp5
49082
50541
  };
49083
50542
  saveConfig({
49084
50543
  ...config,
@@ -49639,7 +51098,7 @@ function upsertPolicyPack(input) {
49639
51098
  const existing = config.policy_packs?.[input.name];
49640
51099
  const root = normalizePath4(input.root || existing?.root || process.cwd());
49641
51100
  const base = existing || defaultPolicyPack(input.name, root);
49642
- const timestamp4 = new Date().toISOString();
51101
+ const timestamp5 = new Date().toISOString();
49643
51102
  const pack = {
49644
51103
  ...base,
49645
51104
  name: input.name,
@@ -49657,8 +51116,8 @@ function upsertPolicyPack(input) {
49657
51116
  require_artifact: input.require_artifact ?? base.require_artifact,
49658
51117
  evidence_min_count: input.evidence_min_count ?? base.evidence_min_count,
49659
51118
  branch_pattern: input.branch_pattern ?? base.branch_pattern,
49660
- created_at: existing?.created_at || timestamp4,
49661
- updated_at: timestamp4
51119
+ created_at: existing?.created_at || timestamp5,
51120
+ updated_at: timestamp5
49662
51121
  };
49663
51122
  saveConfig({
49664
51123
  ...config,
@@ -50000,7 +51459,7 @@ function summarizeOutput(stdout, stderr) {
50000
51459
  function upsertAgentRunAdapter(input) {
50001
51460
  const config = loadConfig();
50002
51461
  const existing = config.agent_run_adapters?.[input.name];
50003
- const timestamp4 = new Date().toISOString();
51462
+ const timestamp5 = new Date().toISOString();
50004
51463
  const adapter = {
50005
51464
  ...existing,
50006
51465
  name: input.name,
@@ -50008,8 +51467,8 @@ function upsertAgentRunAdapter(input) {
50008
51467
  sandbox: input.sandbox ?? existing?.sandbox,
50009
51468
  cwd: input.cwd ?? existing?.cwd,
50010
51469
  env: input.env ?? existing?.env,
50011
- created_at: existing?.created_at || timestamp4,
50012
- updated_at: timestamp4
51470
+ created_at: existing?.created_at || timestamp5,
51471
+ updated_at: timestamp5
50013
51472
  };
50014
51473
  saveConfig({
50015
51474
  ...config,
@@ -50133,7 +51592,7 @@ async function runNextAgentDispatch(input = {}, db) {
50133
51592
  proc.exited
50134
51593
  ]);
50135
51594
  const outputSummary = summarizeOutput(stdout, stderr);
50136
- const status2 = exitCode === 0 ? "completed" : "failed";
51595
+ const status3 = exitCode === 0 ? "completed" : "failed";
50137
51596
  addTaskRunCommand({
50138
51597
  run_id: next.run.id,
50139
51598
  command,
@@ -50142,15 +51601,15 @@ async function runNextAgentDispatch(input = {}, db) {
50142
51601
  output_summary: outputSummary ?? undefined,
50143
51602
  agent_id: next.run.agent_id ?? undefined
50144
51603
  }, d);
50145
- const completed = { ...running, state: status2, completed_at: new Date().toISOString(), last_error: exitCode === 0 ? undefined : outputSummary || `exit ${exitCode}` };
51604
+ const completed = { ...running, state: status3, completed_at: new Date().toISOString(), last_error: exitCode === 0 ? undefined : outputSummary || `exit ${exitCode}` };
50146
51605
  updateDispatcherMetadata(next.run.id, completed, d);
50147
51606
  finishTaskRun({
50148
51607
  run_id: next.run.id,
50149
- status: status2,
50150
- summary: outputSummary || `agent run ${status2}`,
51608
+ status: status3,
51609
+ summary: outputSummary || `agent run ${status3}`,
50151
51610
  agent_id: next.run.agent_id ?? undefined
50152
51611
  }, d);
50153
- return { run_id: next.run.id, task_id: next.run.task_id, command, dry_run: false, status: status2, exit_code: exitCode, output_summary: outputSummary };
51612
+ return { run_id: next.run.id, task_id: next.run.task_id, command, dry_run: false, status: status3, exit_code: exitCode, output_summary: outputSummary };
50154
51613
  }
50155
51614
  function listAgentRuns(filter = {}, db) {
50156
51615
  let runs = listAgentRunQueue(db).map(toAgentRun);
@@ -51119,10 +52578,10 @@ function extractEmbeddedBridge(markdown) {
51119
52578
  const match = markdown.match(/<!--\s*hasna\.todos\.bridge\s*\n([\s\S]*?)\n\s*-->/);
51120
52579
  if (!match)
51121
52580
  return null;
51122
- const json3 = match[1].split(`
52581
+ const json4 = match[1].split(`
51123
52582
  `).map((line) => line.replace(/^ /, "")).join(`
51124
52583
  `);
51125
- return JSON.parse(json3);
52584
+ return JSON.parse(json4);
51126
52585
  }
51127
52586
  function frontmatterValue(markdown, key2) {
51128
52587
  const match = markdown.match(/^---\n([\s\S]*?)\n---/);
@@ -51143,14 +52602,14 @@ function parseTaskLine(line, currentPlan) {
51143
52602
  const match = line.match(/^\s*[-*]\s+\[([ xX])\]\s+(.+)$/);
51144
52603
  if (!match)
51145
52604
  return null;
51146
- const status2 = match[1].toLowerCase() === "x" ? "completed" : "pending";
52605
+ const status3 = match[1].toLowerCase() === "x" ? "completed" : "pending";
51147
52606
  const rawTitle = stripInlineMetadata(match[2]).replace(/^\*\*(.*?)\*\*$/, "$1");
51148
52607
  const tags = [...rawTitle.matchAll(/(^|\s)#([A-Za-z0-9_-]+)/g)].map((tag) => tag[2]);
51149
52608
  const assignee = rawTitle.match(/(^|\s)@([A-Za-z0-9_-]+)/)?.[2];
51150
52609
  const title = rawTitle.replace(/(^|\s)#[A-Za-z0-9_-]+/g, "").replace(/(^|\s)@[A-Za-z0-9_-]+/g, "").replace(/\s+/g, " ").trim();
51151
52610
  return {
51152
52611
  title,
51153
- status: status2,
52612
+ status: status3,
51154
52613
  priority: "medium",
51155
52614
  tags,
51156
52615
  assigned_to: assignee,
@@ -52755,7 +54214,7 @@ function normalizeJiraIssue(raw) {
52755
54214
  if (!summary)
52756
54215
  return null;
52757
54216
  const statusRec = asRecord3(fields.status);
52758
- const status2 = typeof statusRec?.name === "string" ? statusRec.name : undefined;
54217
+ const status3 = typeof statusRec?.name === "string" ? statusRec.name : undefined;
52759
54218
  const labels = labelNames2(fields.labels);
52760
54219
  const description = typeof fields.description === "string" ? fields.description.slice(0, 8000) : typeof asRecord3(fields.description)?.content === "string" ? String(asRecord3(fields.description)?.content).slice(0, 8000) : undefined;
52761
54220
  return {
@@ -52764,13 +54223,13 @@ function normalizeJiraIssue(raw) {
52764
54223
  external_id: key2,
52765
54224
  title: `[${key2}] ${summary}`,
52766
54225
  description,
52767
- status: status2,
54226
+ status: status3,
52768
54227
  priority: priorityFromJira(fields.priority),
52769
54228
  tags: [...labels.slice(0, 10), "jira-import"],
52770
54229
  url: typeof raw.self === "string" ? raw.self : undefined,
52771
54230
  raw_metadata: {
52772
54231
  jira_key: key2,
52773
- jira_status: status2,
54232
+ jira_status: status3,
52774
54233
  jira_priority: asRecord3(fields.priority)?.name,
52775
54234
  jira_id: raw.id
52776
54235
  }
@@ -55515,6 +56974,10 @@ function importBundle(bundle, options = {}, db) {
55515
56974
  }
55516
56975
  for (const dep of bundle.dependencies) {
55517
56976
  try {
56977
+ if (!getTask(dep.task_id, d) || !getTask(dep.depends_on, d)) {
56978
+ bump(result.skipped, "dependencies");
56979
+ continue;
56980
+ }
55518
56981
  d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [dep.task_id, dep.depends_on]);
55519
56982
  bump(result.created, "dependencies");
55520
56983
  } catch (e) {
@@ -56320,11 +57783,11 @@ function createTuiDashboardSnapshot(options = {}, db) {
56320
57783
  const d = db || getDatabase();
56321
57784
  const limit = limited(options.limit);
56322
57785
  const filter = projectFilter(options.project_id);
56323
- const counts = TASK_STATUSES2.reduce((acc, status2) => {
56324
- acc[status2] = countTasks({ ...filter, status: status2 }, d);
57786
+ const counts = TASK_STATUSES2.reduce((acc, status3) => {
57787
+ acc[status3] = countTasks({ ...filter, status: status3 }, d);
56325
57788
  return acc;
56326
57789
  }, { total: 0 });
56327
- counts.total = TASK_STATUSES2.reduce((sum, status2) => sum + counts[status2], 0);
57790
+ counts.total = TASK_STATUSES2.reduce((sum, status3) => sum + counts[status3], 0);
56328
57791
  const projectRows = listProjects(d).filter((project) => !options.project_id || project.id === options.project_id).slice(0, limit).map((project) => ({
56329
57792
  id: project.id,
56330
57793
  name: project.name,
@@ -57525,12 +58988,12 @@ function captureGit(root, warnings) {
57525
58988
  }
57526
58989
  const branch = runLocalCommand(root, ["git", "branch", "--show-current"]);
57527
58990
  const commit = runLocalCommand(root, ["git", "rev-parse", "HEAD"]);
57528
- const status2 = runLocalCommand(root, ["git", "status", "--porcelain=v1"]);
58991
+ const status3 = runLocalCommand(root, ["git", "status", "--porcelain=v1"]);
57529
58992
  if (commit.exitCode !== 0)
57530
58993
  warnings.push(`git commit unavailable: ${commit.stderr || commit.stdout || "unknown error"}`);
57531
- if (status2.exitCode !== 0)
57532
- warnings.push(`git status unavailable: ${status2.stderr || status2.stdout || "unknown error"}`);
57533
- const lines = status2.stdout ? status2.stdout.split(/\r?\n/).filter(Boolean) : [];
58994
+ if (status3.exitCode !== 0)
58995
+ warnings.push(`git status unavailable: ${status3.stderr || status3.stdout || "unknown error"}`);
58996
+ const lines = status3.stdout ? status3.stdout.split(/\r?\n/).filter(Boolean) : [];
57534
58997
  return {
57535
58998
  present: true,
57536
58999
  branch: branch.stdout || null,
@@ -57572,8 +59035,8 @@ function defaultSnapshotDir() {
57572
59035
  return join18(dirname15(resolve17(dbPath)), "environment-snapshots");
57573
59036
  }
57574
59037
  function snapshotWithId(snapshot) {
57575
- const digest2 = sha2566(JSON.stringify(snapshot)).slice(0, 24);
57576
- return { id: `env_${digest2}`, ...snapshot };
59038
+ const digest3 = sha2566(JSON.stringify(snapshot)).slice(0, 24);
59039
+ return { id: `env_${digest3}`, ...snapshot };
57577
59040
  }
57578
59041
  function captureEnvironmentSnapshot(input = {}) {
57579
59042
  const root = resolve17(input.root || process.cwd());
@@ -57773,7 +59236,7 @@ function createDecisionRecord(input, db) {
57773
59236
  const ts = now();
57774
59237
  const sequenceNum = nextSequenceNum(input.project_id, d);
57775
59238
  const shortRef = buildShortRef(input.project_id, sequenceNum, d);
57776
- const status2 = input.status ?? "proposed";
59239
+ const status3 = input.status ?? "proposed";
57777
59240
  d.run(`INSERT INTO decision_records (
57778
59241
  id, project_id, task_id, plan_id, agent_id, sequence_num, short_ref,
57779
59242
  title, status, context, decision, consequences, alternatives, tags,
@@ -57787,7 +59250,7 @@ function createDecisionRecord(input, db) {
57787
59250
  sequenceNum,
57788
59251
  shortRef,
57789
59252
  input.title,
57790
- status2,
59253
+ status3,
57791
59254
  input.context ?? null,
57792
59255
  input.decision,
57793
59256
  input.consequences ?? null,
@@ -57865,15 +59328,15 @@ function updateDecisionRecord(id, input, db) {
57865
59328
  ]);
57866
59329
  return getDecisionRecord(id, d);
57867
59330
  }
57868
- function setDecisionStatus(id, status2, db) {
57869
- if (!DECISION_STATUSES.includes(status2)) {
57870
- throw new Error(`Invalid decision status: ${status2}`);
59331
+ function setDecisionStatus(id, status3, db) {
59332
+ if (!DECISION_STATUSES.includes(status3)) {
59333
+ throw new Error(`Invalid decision status: ${status3}`);
57871
59334
  }
57872
59335
  const d = db || getDatabase();
57873
59336
  const existing = getDecisionRecord(id, d);
57874
59337
  if (!existing)
57875
59338
  throw new Error(`Decision record not found: ${id}`);
57876
- d.run(`UPDATE decision_records SET status = ?, updated_at = ? WHERE id = ?`, [status2, now(), id]);
59339
+ d.run(`UPDATE decision_records SET status = ?, updated_at = ? WHERE id = ?`, [status3, now(), id]);
57877
59340
  return getDecisionRecord(id, d);
57878
59341
  }
57879
59342
  function supersedeDecisionRecord(id, input, db) {
@@ -58154,17 +59617,17 @@ function buildReportExportData(input, db) {
58154
59617
  throw new Error("Project not found");
58155
59618
  projectId = project.id;
58156
59619
  title = `Project: ${project.name}`;
58157
- const status2 = getStatus({ project_id: project.id }, undefined, undefined, d);
59620
+ const status3 = getStatus({ project_id: project.id }, undefined, undefined, d);
58158
59621
  sections.push(section("overview", "Overview", [
58159
59622
  `Name: ${project.name}`,
58160
59623
  `Path: ${project.path}`,
58161
59624
  `Prefix: ${project.task_prefix ?? "\u2014"}`
58162
59625
  ]), section("status", "Status", [
58163
- `Pending: ${status2.pending}`,
58164
- `In progress: ${status2.in_progress}`,
58165
- `Completed: ${status2.completed}`,
58166
- `Total: ${status2.total}`,
58167
- `Stale: ${status2.stale_count}`
59626
+ `Pending: ${status3.pending}`,
59627
+ `In progress: ${status3.in_progress}`,
59628
+ `Completed: ${status3.completed}`,
59629
+ `Total: ${status3.total}`,
59630
+ `Stale: ${status3.stale_count}`
58168
59631
  ]));
58169
59632
  const tasks = listTasks({ project_id: project.id, limit: 50 }, d);
58170
59633
  sections.push(section("tasks", "Recent tasks", tasks.length ? tasks.map(taskLine2) : ["No tasks"]));
@@ -58985,8 +60448,8 @@ function getGitStatus(root, branch, includeGitStatus) {
58985
60448
  return { has_git: false, current_branch: null, branch_exists: false, dirty_files: [] };
58986
60449
  const currentBranch = runGit3(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
58987
60450
  const branchExists = runGit3(root, ["show-ref", "--verify", `refs/heads/${branch}`]) !== null;
58988
- const status2 = runGit3(root, ["status", "--short"]) || "";
58989
- const dirtyFiles = status2.split(`
60451
+ const status3 = runGit3(root, ["status", "--short"]) || "";
60452
+ const dirtyFiles = status3.split(`
58990
60453
  `).map((line) => line.trim()).filter(Boolean).map((line) => normalizePath6(line.replace(/^.. /, "").replace(/^.* -> /, ""))).filter((path) => Boolean(path));
58991
60454
  return { has_git: true, current_branch: currentBranch || null, branch_exists: branchExists, dirty_files: dirtyFiles };
58992
60455
  }
@@ -59384,8 +60847,8 @@ var AGENT_WORKFLOW_DEMO_SCHEMA = "todos.agent_workflow_demo.v1";
59384
60847
  var DEMO_DEFAULT_AGENT = "demoagent";
59385
60848
  var DEMO_DEFAULT_PROJECT = "Agent Workflow Demo";
59386
60849
  var DEMO_PROJECT_PATH = "/tmp/todos-agent-workflow-demo";
59387
- function pushStep(steps, action, detail, status2 = "ok") {
59388
- steps.push({ step: steps.length + 1, action, detail, status: status2 });
60850
+ function pushStep(steps, action, detail, status3 = "ok") {
60851
+ steps.push({ step: steps.length + 1, action, detail, status: status3 });
59389
60852
  }
59390
60853
  function setupEphemeralDemoDb(options = {}) {
59391
60854
  const prevPath = process.env["TODOS_DB_PATH"];
@@ -59512,7 +60975,7 @@ function runAgentWorkflowDemo(options = {}) {
59512
60975
  pushStep(steps, "complete_run_record", `Run record status=${finishedRecord.status}`);
59513
60976
  completeTask(third.id, agent.id, d, { notes: "Demo: finished via agent run + run record" });
59514
60977
  pushStep(steps, "complete_task", `Completed ${third.short_id ?? third.id.slice(0, 8)}`);
59515
- const status2 = getStatus({ project_id: project.id }, agent.id, undefined, d);
60978
+ const status3 = getStatus({ project_id: project.id }, agent.id, undefined, d);
59516
60979
  const tasks = listTasks({ project_id: project.id }, d).map((t) => ({
59517
60980
  id: t.id,
59518
60981
  short_id: t.short_id,
@@ -59544,14 +61007,14 @@ function runAgentWorkflowDemo(options = {}) {
59544
61007
  objective: finishedRecord.objective
59545
61008
  },
59546
61009
  status_summary: {
59547
- pending: status2.pending,
59548
- in_progress: status2.in_progress,
59549
- completed: status2.completed,
59550
- total: status2.total
61010
+ pending: status3.pending,
61011
+ in_progress: status3.in_progress,
61012
+ completed: status3.completed,
61013
+ total: status3.total
59551
61014
  },
59552
61015
  steps,
59553
61016
  completed_at: exportedAt2,
59554
- message: `Demo complete: ${status2.completed}/${status2.total} tasks done, 1 agent run, 1 run record`
61017
+ message: `Demo complete: ${status3.completed}/${status3.total} tasks done, 1 agent run, 1 run record`
59555
61018
  };
59556
61019
  } finally {
59557
61020
  if (dbHandle)
@@ -60525,8 +61988,8 @@ function rowToOrg(row) {
60525
61988
  function createOrg(input, db) {
60526
61989
  const d = db || getDatabase();
60527
61990
  const id = uuid();
60528
- const timestamp4 = now();
60529
- d.run(`INSERT INTO orgs (id, name, description, metadata, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, [id, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp4, timestamp4]);
61991
+ const timestamp5 = now();
61992
+ d.run(`INSERT INTO orgs (id, name, description, metadata, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, [id, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp5, timestamp5]);
60530
61993
  return getOrg(id, d);
60531
61994
  }
60532
61995
  function getOrg(id, db) {
@@ -60774,10 +62237,10 @@ function getCriticalPath2(opts, db) {
60774
62237
  function addKgEdge(sourceId, sourceType, targetId, targetType, relationType, weight = 1, metadata, db) {
60775
62238
  const d = db || getDatabase();
60776
62239
  const id = uuid();
60777
- const timestamp4 = now();
62240
+ const timestamp5 = now();
60778
62241
  d.run(`INSERT OR IGNORE INTO kg_edges (id, source_id, source_type, target_id, target_type, relation_type, weight, metadata, created_at)
60779
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, sourceId, sourceType, targetId, targetType, relationType, weight, JSON.stringify(metadata || {}), timestamp4]);
60780
- return { id, source_id: sourceId, source_type: sourceType, target_id: targetId, target_type: targetType, relation_type: relationType, weight, metadata: metadata || {}, created_at: timestamp4 };
62242
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, sourceId, sourceType, targetId, targetType, relationType, weight, JSON.stringify(metadata || {}), timestamp5]);
62243
+ return { id, source_id: sourceId, source_type: sourceType, target_id: targetId, target_type: targetType, relation_type: relationType, weight, metadata: metadata || {}, created_at: timestamp5 };
60781
62244
  }
60782
62245
  function removeKgEdges(sourceId, targetId, relationType, db) {
60783
62246
  const d = db || getDatabase();
@@ -61677,7 +63140,7 @@ function runSavedSearch(filters = {}, scope = "tasks", db) {
61677
63140
  function saveSearchView(input, db) {
61678
63141
  const d = db || getDatabase();
61679
63142
  const name = normalizeName3(input.name);
61680
- const timestamp4 = now();
63143
+ const timestamp5 = now();
61681
63144
  const existing = getSearchView(name, d);
61682
63145
  if (existing) {
61683
63146
  d.run(`UPDATE saved_search_views
@@ -61686,7 +63149,7 @@ function saveSearchView(input, db) {
61686
63149
  input.description ?? existing.description,
61687
63150
  normalizeScope(input.scope ?? existing.scope),
61688
63151
  JSON.stringify(input.filters ?? existing.filters),
61689
- timestamp4,
63152
+ timestamp5,
61690
63153
  existing.id
61691
63154
  ]);
61692
63155
  return getSearchView(existing.id, d);
@@ -61699,8 +63162,8 @@ function saveSearchView(input, db) {
61699
63162
  input.description ?? null,
61700
63163
  normalizeScope(input.scope),
61701
63164
  JSON.stringify(input.filters ?? {}),
61702
- timestamp4,
61703
- timestamp4
63165
+ timestamp5,
63166
+ timestamp5
61704
63167
  ]);
61705
63168
  return getSearchView(id, d);
61706
63169
  }
@@ -61742,14 +63205,14 @@ function readClaudeTask(dir, filename) {
61742
63205
  function writeClaudeTask(dir, task3) {
61743
63206
  writeJsonFile(join23(dir, `${task3.id}.json`), task3);
61744
63207
  }
61745
- function toClaudeStatus(status2) {
61746
- if (status2 === "pending" || status2 === "in_progress" || status2 === "completed") {
61747
- return status2;
63208
+ function toClaudeStatus(status3) {
63209
+ if (status3 === "pending" || status3 === "in_progress" || status3 === "completed") {
63210
+ return status3;
61748
63211
  }
61749
63212
  return "completed";
61750
63213
  }
61751
- function toSqliteStatus(status2) {
61752
- return status2;
63214
+ function toSqliteStatus(status3) {
63215
+ return status3;
61753
63216
  }
61754
63217
  function readPrefixCounter(dir) {
61755
63218
  const path = join23(dir, ".prefix-counter");
@@ -62710,7 +64173,7 @@ init_database();
62710
64173
  function logTrace(input, db) {
62711
64174
  const d = db || getDatabase();
62712
64175
  const id = uuid();
62713
- const timestamp4 = now();
64176
+ const timestamp5 = now();
62714
64177
  d.run(`INSERT INTO task_traces (id, task_id, agent_id, trace_type, name, input_summary, output_summary, duration_ms, tokens, cost_usd, created_at)
62715
64178
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
62716
64179
  id,
@@ -62723,7 +64186,7 @@ function logTrace(input, db) {
62723
64186
  input.duration_ms ?? null,
62724
64187
  input.tokens ?? null,
62725
64188
  input.cost_usd ?? null,
62726
- timestamp4
64189
+ timestamp5
62727
64190
  ]);
62728
64191
  return {
62729
64192
  id,
@@ -62736,7 +64199,7 @@ function logTrace(input, db) {
62736
64199
  duration_ms: input.duration_ms ?? null,
62737
64200
  tokens: input.tokens ?? null,
62738
64201
  cost_usd: input.cost_usd ?? null,
62739
- created_at: timestamp4
64202
+ created_at: timestamp5
62740
64203
  };
62741
64204
  }
62742
64205
  function getTaskTraces(taskId, db) {
@@ -62760,7 +64223,7 @@ init_database();
62760
64223
  init_tasks();
62761
64224
  function setBudget(agentId, opts, db) {
62762
64225
  const d = db || getDatabase();
62763
- const timestamp4 = now();
64226
+ const timestamp5 = now();
62764
64227
  d.run(`INSERT INTO agent_budgets (agent_id, max_concurrent, max_cost_usd, max_task_minutes, period_hours, created_at, updated_at)
62765
64228
  VALUES (?, ?, ?, ?, ?, ?, ?)
62766
64229
  ON CONFLICT(agent_id) DO UPDATE SET
@@ -62774,13 +64237,13 @@ function setBudget(agentId, opts, db) {
62774
64237
  opts.max_cost_usd ?? null,
62775
64238
  opts.max_task_minutes ?? null,
62776
64239
  opts.period_hours ?? 24,
62777
- timestamp4,
62778
- timestamp4,
64240
+ timestamp5,
64241
+ timestamp5,
62779
64242
  opts.max_concurrent ?? null,
62780
64243
  opts.max_cost_usd ?? null,
62781
64244
  opts.max_task_minutes ?? null,
62782
64245
  opts.period_hours ?? null,
62783
- timestamp4
64246
+ timestamp5
62784
64247
  ]);
62785
64248
  return getBudget(agentId, d);
62786
64249
  }
@@ -63083,11 +64546,11 @@ function resolveWorkflowState(input, projectPath) {
63083
64546
  return { input, state: byCanonical, matched_by: "canonical_status" };
63084
64547
  throw new Error(`Unknown workflow state: ${input}`);
63085
64548
  }
63086
- function stateForCanonicalStatus(status2, projectPath) {
63087
- const configured = (getWorkflowConfig(projectPath)?.states || []).map(normalizeState).find((state) => Boolean(state && state.canonical_status === status2));
64549
+ function stateForCanonicalStatus(status3, projectPath) {
64550
+ const configured = (getWorkflowConfig(projectPath)?.states || []).map(normalizeState).find((state) => Boolean(state && state.canonical_status === status3));
63088
64551
  if (configured)
63089
64552
  return configured;
63090
- return listWorkflowStates(projectPath).find((state) => state.canonical_status === status2) || resolveWorkflowState(status2, projectPath).state;
64553
+ return listWorkflowStates(projectPath).find((state) => state.canonical_status === status3) || resolveWorkflowState(status3, projectPath).state;
63091
64554
  }
63092
64555
  function getTaskWorkflowState(taskId, db, projectPath) {
63093
64556
  const d = db || getDatabase();
@@ -63251,9 +64714,9 @@ function unpackFixture(input) {
63251
64714
  const contextPack = input["context_pack"];
63252
64715
  return isObject(contextPack) ? contextPack : input;
63253
64716
  }
63254
- function approvalStatus(status2) {
63255
- if (status2 === "approved" || status2 === "rejected" || status2 === "expired")
63256
- return status2;
64717
+ function approvalStatus(status3) {
64718
+ if (status3 === "approved" || status3 === "rejected" || status3 === "expired")
64719
+ return status3;
63257
64720
  return "pending";
63258
64721
  }
63259
64722
  function approvalsFromFixture(fixture, runs) {
@@ -63350,17 +64813,17 @@ function simulateAgentReplay(input, options = {}) {
63350
64813
  if (["completed", "failed", "cancelled"].includes(runStatus))
63351
64814
  currentStatus = runStatus;
63352
64815
  for (const command of asArray2(run["commands"])) {
63353
- const status2 = normalizedStatus(command["status"]);
64816
+ const status3 = normalizedStatus(command["status"]);
63354
64817
  const commandText = text(command["command"]) || "command";
63355
64818
  push({
63356
64819
  type: "command",
63357
64820
  source: "run.commands",
63358
64821
  label: commandText,
63359
- status: status2,
64822
+ status: status3,
63360
64823
  command: commandText,
63361
64824
  message: text(command["output_summary"])
63362
64825
  });
63363
- if (status2 === "failed")
64826
+ if (status3 === "failed")
63364
64827
  failures.push({ source: "run.commands", message: text(command["output_summary"]) || "command failed", command: commandText });
63365
64828
  }
63366
64829
  for (const file of asArray2(run["files"])) {
@@ -63377,17 +64840,17 @@ function simulateAgentReplay(input, options = {}) {
63377
64840
  }
63378
64841
  }
63379
64842
  for (const verification2 of verifications) {
63380
- const status2 = normalizedStatus(verification2["status"]);
64843
+ const status3 = normalizedStatus(verification2["status"]);
63381
64844
  const commandText = text(verification2["command"]) || "verification";
63382
64845
  push({
63383
64846
  type: "command",
63384
64847
  source: "traceability.verifications",
63385
64848
  label: commandText,
63386
- status: status2,
64849
+ status: status3,
63387
64850
  command: commandText,
63388
64851
  message: text(verification2["output_summary"])
63389
64852
  });
63390
- if (status2 === "failed")
64853
+ if (status3 === "failed")
63391
64854
  failures.push({ source: "traceability.verifications", message: text(verification2["output_summary"]) || "verification failed", command: commandText });
63392
64855
  }
63393
64856
  const approvals = approvalsFromFixture(fixture, runs);
@@ -63923,7 +65386,7 @@ function installLocalExtension(input) {
63923
65386
  const validation = inspected.validation;
63924
65387
  if (!validation.ok)
63925
65388
  throw new Error(`extension manifest invalid: ${validation.errors.join("; ")}`);
63926
- const timestamp4 = new Date().toISOString();
65389
+ const timestamp5 = new Date().toISOString();
63927
65390
  const config = loadConfig();
63928
65391
  const existing = config.extension_registry?.[inspected.manifest.name];
63929
65392
  const trusted = Boolean(input.trust);
@@ -63943,8 +65406,8 @@ function installLocalExtension(input) {
63943
65406
  ...trusted ? [] : ["extension installed as needs_review until trusted explicitly"]
63944
65407
  ],
63945
65408
  diagnostics: testExtensionCompatibility(inspected.source),
63946
- installed_at: existing?.installed_at || timestamp4,
63947
- updated_at: timestamp4
65409
+ installed_at: existing?.installed_at || timestamp5,
65410
+ updated_at: timestamp5
63948
65411
  };
63949
65412
  saveConfig({
63950
65413
  ...config,
@@ -64306,9 +65769,9 @@ async function sendToTmux(target, message, delayMs, options = false) {
64306
65769
  ensureTmuxAvailable();
64307
65770
  if (!opts.confirmBusy) {
64308
65771
  const pane = await inspectTmuxPane(target);
64309
- const status2 = tmuxPaneBusyStatus(pane);
64310
- if (status2.busy) {
64311
- throw new Error(`tmux target "${target}" appears busy (${status2.reason}). Re-run with --confirm-busy to send anyway.`);
65772
+ const status3 = tmuxPaneBusyStatus(pane);
65773
+ if (status3.busy) {
65774
+ throw new Error(`tmux target "${target}" appears busy (${status3.reason}). Re-run with --confirm-busy to send anyway.`);
64312
65775
  }
64313
65776
  }
64314
65777
  const sendProc = Bun.spawn(["tmux", "send-keys", "-t", target, message, ""], {
@@ -64453,10 +65916,10 @@ function normalizeStatus(value) {
64453
65916
  return "open";
64454
65917
  }
64455
65918
  function normalizeResolutionStatus(value) {
64456
- const status2 = normalizeStatus(value || "resolved");
64457
- if (status2 === "open")
65919
+ const status3 = normalizeStatus(value || "resolved");
65920
+ if (status3 === "open")
64458
65921
  throw new Error("resolve-missing status must be resolved or ignored");
64459
- return status2;
65922
+ return status3;
64460
65923
  }
64461
65924
  function redactOptional(value, max = 2000) {
64462
65925
  if (!value)
@@ -64494,7 +65957,7 @@ function compactFinding(finding2) {
64494
65957
  metadata_keys: Object.keys(finding2.metadata).sort()
64495
65958
  };
64496
65959
  }
64497
- function previewFinding(existing, next, timestamp4) {
65960
+ function previewFinding(existing, next, timestamp5) {
64498
65961
  return {
64499
65962
  ...existing,
64500
65963
  run_id: next.run_id,
@@ -64505,9 +65968,9 @@ function previewFinding(existing, next, timestamp4) {
64505
65968
  summary: next.summary,
64506
65969
  artifact_path: next.artifact_path,
64507
65970
  metadata: next.metadata,
64508
- last_seen_at: timestamp4,
64509
- resolved_at: next.status === "open" ? null : existing.resolved_at || timestamp4,
64510
- updated_at: timestamp4
65971
+ last_seen_at: timestamp5,
65972
+ resolved_at: next.status === "open" ? null : existing.resolved_at || timestamp5,
65973
+ updated_at: timestamp5
64511
65974
  };
64512
65975
  }
64513
65976
  function upsertAction(existing, next) {
@@ -64557,7 +66020,7 @@ function sameFinding(left, right) {
64557
66020
  function upsertTaskFinding(input, db) {
64558
66021
  const d = db || getDatabase();
64559
66022
  assertTask(input.task_id, d);
64560
- const timestamp4 = input.observed_at || now();
66023
+ const timestamp5 = input.observed_at || now();
64561
66024
  const warnings = [];
64562
66025
  const next = nextFinding(input, d);
64563
66026
  const existing = getFindingByFingerprint(input.task_id, next.fingerprint, d);
@@ -64568,10 +66031,10 @@ function upsertTaskFinding(input, db) {
64568
66031
  schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
64569
66032
  local_only: true,
64570
66033
  dry_run: true,
64571
- processed_at: timestamp4,
66034
+ processed_at: timestamp5,
64572
66035
  action: action2,
64573
66036
  fingerprint: next.fingerprint,
64574
- finding: existing ? compactFinding(previewFinding(existing, next, timestamp4)) : null,
66037
+ finding: existing ? compactFinding(previewFinding(existing, next, timestamp5)) : null,
64575
66038
  warnings
64576
66039
  };
64577
66040
  }
@@ -64592,17 +66055,17 @@ function upsertTaskFinding(input, db) {
64592
66055
  next.summary,
64593
66056
  next.artifact_path,
64594
66057
  JSON.stringify(next.metadata),
64595
- timestamp4,
64596
- timestamp4,
64597
- next.status === "open" ? null : timestamp4,
64598
- timestamp4,
64599
- timestamp4
66058
+ timestamp5,
66059
+ timestamp5,
66060
+ next.status === "open" ? null : timestamp5,
66061
+ timestamp5,
66062
+ timestamp5
64600
66063
  ]);
64601
66064
  return {
64602
66065
  schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
64603
66066
  local_only: true,
64604
66067
  dry_run: false,
64605
- processed_at: timestamp4,
66068
+ processed_at: timestamp5,
64606
66069
  action: "created",
64607
66070
  fingerprint: next.fingerprint,
64608
66071
  finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
@@ -64614,7 +66077,7 @@ function upsertTaskFinding(input, db) {
64614
66077
  schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
64615
66078
  local_only: true,
64616
66079
  dry_run: false,
64617
- processed_at: timestamp4,
66080
+ processed_at: timestamp5,
64618
66081
  action: "matched",
64619
66082
  fingerprint: next.fingerprint,
64620
66083
  finding: compactFinding(existing),
@@ -64634,16 +66097,16 @@ function upsertTaskFinding(input, db) {
64634
66097
  next.summary,
64635
66098
  next.artifact_path,
64636
66099
  JSON.stringify(next.metadata),
64637
- timestamp4,
64638
- next.status === "open" ? null : existing.resolved_at || timestamp4,
64639
- timestamp4,
66100
+ timestamp5,
66101
+ next.status === "open" ? null : existing.resolved_at || timestamp5,
66102
+ timestamp5,
64640
66103
  existing.id
64641
66104
  ]);
64642
66105
  return {
64643
66106
  schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
64644
66107
  local_only: true,
64645
66108
  dry_run: false,
64646
- processed_at: timestamp4,
66109
+ processed_at: timestamp5,
64647
66110
  action,
64648
66111
  fingerprint: next.fingerprint,
64649
66112
  finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
@@ -64680,8 +66143,8 @@ function listCompactTaskFindings(filter = {}, db) {
64680
66143
  function resolveMissingTaskFindings(input, db) {
64681
66144
  const d = db || getDatabase();
64682
66145
  assertTask(input.task_id, d);
64683
- const timestamp4 = input.resolved_at || now();
64684
- const status2 = normalizeResolutionStatus(input.status);
66146
+ const timestamp5 = input.resolved_at || now();
66147
+ const status3 = normalizeResolutionStatus(input.status);
64685
66148
  const runId = resolveRunForTask(input.run_id, input.task_id, d);
64686
66149
  const present = new Set(input.fingerprints.map(normalizeTaskFindingFingerprint));
64687
66150
  const warnings = [];
@@ -64700,7 +66163,7 @@ function resolveMissingTaskFindings(input, db) {
64700
66163
  schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
64701
66164
  local_only: true,
64702
66165
  dry_run: true,
64703
- processed_at: timestamp4,
66166
+ processed_at: timestamp5,
64704
66167
  action: candidates.length > 0 ? "preview" : "noop",
64705
66168
  task_id: input.task_id,
64706
66169
  source: input.source ? redactOptional(input.source, 120) : null,
@@ -64723,10 +66186,10 @@ function resolveMissingTaskFindings(input, db) {
64723
66186
  const tx = d.transaction(() => {
64724
66187
  for (const finding2 of candidates) {
64725
66188
  d.run("UPDATE task_findings SET status = ?, resolved_at = ?, metadata = ?, updated_at = ? WHERE id = ? AND status = 'open'", [
64726
- status2,
64727
- timestamp4,
66189
+ status3,
66190
+ timestamp5,
64728
66191
  JSON.stringify({ ...finding2.metadata, ...metadataPatch }),
64729
- timestamp4,
66192
+ timestamp5,
64730
66193
  finding2.id
64731
66194
  ]);
64732
66195
  }
@@ -64738,8 +66201,8 @@ function resolveMissingTaskFindings(input, db) {
64738
66201
  schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
64739
66202
  local_only: true,
64740
66203
  dry_run: false,
64741
- processed_at: timestamp4,
64742
- action: updated.length > 0 ? status2 : "noop",
66204
+ processed_at: timestamp5,
66205
+ action: updated.length > 0 ? status3 : "noop",
64743
66206
  task_id: input.task_id,
64744
66207
  source: input.source ? redactOptional(input.source, 120) : null,
64745
66208
  run_id: runId,
@@ -65632,7 +67095,7 @@ init_tasks();
65632
67095
  init_task_relations();
65633
67096
  init_config2();
65634
67097
  var LOCAL_CAPACITY_SCHEMA_VERSION = 1;
65635
- function timestamp4() {
67098
+ function timestamp5() {
65636
67099
  return new Date().toISOString();
65637
67100
  }
65638
67101
  function todayIso() {
@@ -65716,7 +67179,7 @@ function upsertCapacityProfile(input) {
65716
67179
  const store = readStore2();
65717
67180
  const key2 = profileKey(agentId, projectId);
65718
67181
  const existing = store.profiles[key2];
65719
- const now4 = timestamp4();
67182
+ const now4 = timestamp5();
65720
67183
  const profile = {
65721
67184
  id: existing?.id ?? key2,
65722
67185
  agent_id: agentId,
@@ -65797,7 +67260,7 @@ function getPlanningForecast(input = {}, db) {
65797
67260
  return {
65798
67261
  schema_version: LOCAL_CAPACITY_SCHEMA_VERSION,
65799
67262
  local_only: true,
65800
- generated_at: timestamp4(),
67263
+ generated_at: timestamp5(),
65801
67264
  filters: {
65802
67265
  project_id: input.project_id ?? null,
65803
67266
  plan_id: input.plan_id ?? null,
@@ -66185,6 +67648,8 @@ export {
66185
67648
  testTerminalNotificationRule,
66186
67649
  testLocalEventHook,
66187
67650
  tasksFromTemplate,
67651
+ taskSubtreeTransferRollbackRequestDigest,
67652
+ taskSubtreeTransferRequestDigest,
66188
67653
  taskManifestRequestDigest,
66189
67654
  taskManifestCompensationRequestDigest,
66190
67655
  taskFromTemplate,
@@ -66207,6 +67672,7 @@ export {
66207
67672
  startTaskRun,
66208
67673
  startTask,
66209
67674
  startFocusSession,
67675
+ sqliteTodosTaskSubtreeTransferSchemaSql,
66210
67676
  sqliteTodosTaskManifestSchemaSql,
66211
67677
  sqliteTodosProjectRegistrationSchemaSql,
66212
67678
  softDeleteArtifact,
@@ -66398,6 +67864,7 @@ export {
66398
67864
  previewInstalledTemplate,
66399
67865
  previewInboxIntake,
66400
67866
  previewBuiltinTemplate,
67867
+ postgresTodosTaskSubtreeTransferSchemaSql,
66401
67868
  postgresTodosTaskManifestSchemaSql,
66402
67869
  postgresTodosSyncSchemaSql,
66403
67870
  postgresTodosProjectRegistrationSchemaSql,
@@ -66406,6 +67873,9 @@ export {
66406
67873
  planPlanProjectLink,
66407
67874
  pauseFocusSession,
66408
67875
  patrolTasks,
67876
+ parseTodosTaskSubtreeTransferRollback,
67877
+ parseTodosTaskSubtreeTransferInspect,
67878
+ parseTodosTaskSubtreeTransferApply,
66409
67879
  parseTodosTaskManifestCompensation,
66410
67880
  parseTodosTaskManifestBindingLookup,
66411
67881
  parseTodosTaskManifest,
@@ -66413,7 +67883,6 @@ export {
66413
67883
  parseTodosAiOutputSchema,
66414
67884
  parseTodosAiJson,
66415
67885
  parseTmuxTarget,
66416
- parseStorageMode,
66417
67886
  parseStorageBackend,
66418
67887
  parseRecurrenceRule,
66419
67888
  parseQuietHours,
@@ -66564,7 +68033,6 @@ export {
66564
68033
  issueToTask,
66565
68034
  isWorktreePath,
66566
68035
  isValidRecurrenceRule,
66567
- isTodosRemoteStorageEnabled,
66568
68036
  isTodosPostgresBackend,
66569
68037
  isTodosAiUpdateTaskResult,
66570
68038
  isTodosAiRuntimeEvent,
@@ -66604,6 +68072,7 @@ export {
66604
68072
  importActivityLog,
66605
68073
  hasSecretFindings,
66606
68074
  hasActiveApiKeys,
68075
+ handleTodosTaskSubtreeTransferHttpRequest,
66607
68076
  handleTodosTaskManifestHttpRequest,
66608
68077
  handleTodosProjectRegistrationHttpRequest,
66609
68078
  getWorkspaceTrustStatus,
@@ -66617,7 +68086,6 @@ export {
66617
68086
  getUnlockImpact,
66618
68087
  getTraceStats,
66619
68088
  getTopologyDocs,
66620
- getTodosStorageMode,
66621
68089
  getTodosStorageEnvName,
66622
68090
  getTodosStorageDatabaseUrl,
66623
68091
  getTodosStorageDatabaseEnv,
@@ -66662,7 +68130,6 @@ export {
66662
68130
  getTaskBoard,
66663
68131
  getTask,
66664
68132
  getStoredHandoffAsPacket,
66665
- getStorageMode,
66666
68133
  getStorageDatabaseUrl,
66667
68134
  getStorageDatabaseEnv,
66668
68135
  getStatus,
@@ -66921,6 +68388,9 @@ export {
66921
68388
  detectInboxSourceType,
66922
68389
  detectCyclesFromEdges,
66923
68390
  describeTerminalNotificationRule,
68391
+ deriveTodosTaskSubtreeTransferRollbackPreconditionDigest,
68392
+ deriveTodosTaskSubtreeTransferIdempotencyKey,
68393
+ deriveTodosTaskSubtreeTransferApplyPreconditionDigest,
66924
68394
  deriveTodosTaskManifestIdempotencyKey,
66925
68395
  deriveTodosTaskManifestCompensationPreconditionDigest,
66926
68396
  deriveTodosTaskManifestApplyPreconditionDigest,
@@ -66957,6 +68427,7 @@ export {
66957
68427
  createVerificationEvidence,
66958
68428
  createUserScaffold,
66959
68429
  createTuiDashboardSnapshot,
68430
+ createTodosTaskSubtreeTransferHttpClient,
66960
68431
  createTodosTaskManifestHttpClient,
66961
68432
  createTodosStorageAdapter,
66962
68433
  createTodosS3ArtifactStore,
@@ -66971,6 +68442,7 @@ export {
66971
68442
  createTaskList,
66972
68443
  createTaskBoard,
66973
68444
  createTask,
68445
+ createSqliteTodosTaskSubtreeTransferAuthority,
66974
68446
  createSqliteTodosTaskManifestAuthority,
66975
68447
  createSessionRecoveryHandoff,
66976
68448
  createSession,
@@ -66986,6 +68458,7 @@ export {
66986
68458
  createReminder,
66987
68459
  createReleaseCompatibilityReport,
66988
68460
  createProject,
68461
+ createPostgresTodosTaskSubtreeTransferAuthority,
66989
68462
  createPostgresTodosTaskManifestAuthority,
66990
68463
  createPostgresTodosSyncStore,
66991
68464
  createPostgresTodosStorageAdapter,
@@ -67154,6 +68627,8 @@ export {
67154
68627
  VersionConflictError,
67155
68628
  VERIFICATION_EVIDENCE_SCHEMA,
67156
68629
  USER_SCAFFOLD_SCHEMA,
68630
+ TodosTaskSubtreeTransferHttpClient,
68631
+ TodosTaskSubtreeTransferError,
67157
68632
  TodosTaskManifestHttpClient,
67158
68633
  TodosTaskManifestError,
67159
68634
  TodosProjectRegistrationHttpClient,
@@ -67165,6 +68640,8 @@ export {
67165
68640
  TaskNotFoundError,
67166
68641
  TaskListNotFoundError,
67167
68642
  TUI_DASHBOARD_VIEWS,
68643
+ TODOS_TASK_SUBTREE_TRANSFER_SCHEMA_VERSION,
68644
+ TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
67168
68645
  TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION,
67169
68646
  TODOS_TASK_MANIFEST_SCHEMA_VERSION,
67170
68647
  TODOS_TASK_MANIFEST_ROUTE,
@@ -67271,6 +68748,7 @@ export {
67271
68748
  PostgresTodosProjectRegistrationBackend,
67272
68749
  PlanProjectLinkError,
67273
68750
  PlanNotFoundError,
68751
+ PackageOwnedTodosTaskSubtreeTransferAuthority,
67274
68752
  PackageOwnedTodosTaskManifestAuthority,
67275
68753
  PackageOwnedTodosProjectRegistrationAuthority,
67276
68754
  PROJECT_DEPENDENCY_GRAPH_SCHEMA,